Files
op-packages/luci-theme-footstrap/Makefile
T

284 lines
18 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#
# Copyright (C) 2026
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-footstrap
# luci.mk keys the Build/Prepare hook name on LUCI_NAME, which defaults to the checkout's
# directory name — a differently-named checkout symlinked into the feed would silently skip
# the CSS build. Pin it.
LUCI_NAME:=luci-theme-footstrap
# Version is git-derived; never hardcode PKG_VERSION. CI injects FOOTSTRAP_VERSION from the
# tag (an SDK build has no .git, so it cannot derive one).
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=65
endif
LUCI_TITLE:=Footstrap Theme
# +luci-base is the WHOLE list, and keeping it that way is the constraint. Nothing this package ships
# fetches anything at run time, so there is no second name to justify — and the rule that got it here
# stands: the one convenience tool that ever wanted a dep was `curl`, which is NOT in OpenWrt's
# default set, on an 8 MB device, for one API call. Fall back to what the base image has
# (uclient-fetch, jsonfilter, sha256sum) or do without.
LUCI_DEPENDS:=+luci-base
LUCI_PKGARCH:=all
# Else luci.mk defaults these to LuCI's own, and the package claims MAINTAINER "OpenWrt LuCI
# community" / URL openwrt/luci.
LUCI_MAINTAINER:=Ivan Kvashonkin <vizzlef@gmail.com>
LUCI_URL:=https://github.com/VizzleTF/luci-theme-footstrap
# CSS: ship verbatim. luci.mk would run csstidy over it, which is old enough to mangle :has(),
# color-mix() and nested calc() and break the layout. build-css.sh minifies instead.
LUCI_MINIFY_CSS:=0
#
# JS: two minify paths, chosen by who is building.
#
# CI (the released packages) pre-minifies with terser BEFORE the SDK build — tools/minify-js.mjs
# mangles top-level identifiers too, which jsmin cannot: ~41 KB against jsmin's ~57 KB, 27% — and
# signals it with FOOTSTRAP_PREMIN=1.
# jsmin must then stay away from the already-minified output: terser legitimately emits
# `return/^v/.test(s)`, the exact one-character-lookback trap below, so LUCI_MINIFY_JS goes to 0.
#
# Everyone else (an SDK user, the buildbot — no node) builds the untouched source and keeps
# luci.mk's default jsmin: ~105 KB of this tree's JS is comments, and uhttpd serves /www with NO
# compression, so those are wire and flash bytes either way.
#
# The catch on the jsmin path, and why the gates exist: jsmin decides regex-or-division from a
# one-character lookback, and `n` (of `return`) and `>` (of `=>`) are not on its allow-list, so
# `return /re/` makes it eat the rest of the file AND EXIT 0 (openwrt/luci#8299). eslint's
# `wrap-regex` bans the shape in the source; tools/jsmin-verify.mjs proves jsmin's output is
# token-identical to it.
FOOTSTRAP_PREMIN?=
ifneq ($(FOOTSTRAP_PREMIN),)
LUCI_MINIFY_JS:=0
endif
# Apache-2.0, and not a free choice: styles/base began as a fork of luci-theme-bootstrap's
# cascade.css, the ucode templates derive from LuCI's own, and a few JS helpers are verbatim copies.
# The notices travel with it.
#
# No OFL-1.1: the theme ships no Font Software, so declaring it would be a false statement about the
# package's contents.
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
# /etc/config/footstrap is SHIPPED as an empty stub and WRITTEN AT RUNTIME: Appearance ->
# "Save as default" has rpcd uci-set the router-wide axes into that very file. Without this define
# the package manager owns it as an ordinary file and REPLACES it on upgrade, so the admin's saved
# defaults are wiped by the theme's own one-click Update, silently and reported as success.
# OpenWrt honours the define for BOTH formats (include/package-pack.mk), so one covers 24.10's opkg
# and 25.12's apk alike. Any future root/etc/config/* must be listed here too.
define Package/luci-theme-footstrap/conffiles
/etc/config/footstrap
endef
# NO `rpcd reload` HERE, on install or removal — it used to be, and it was the more dangerous of
# the two rpcd calls this package could make. Measured on an owrt2512 stand:
#
# 1. RELOAD IS A RE-EXEC, NOT A REFRESH. `/etc/init.d/rpcd reload` is `procd_send_signal rpcd` ->
# SIGHUP -> rpcd freezes sessions and calls exec_self(): a full re-exec that re-scans the plugin
# dirs and dlopen()s every .so. A plugin file that is absent or mid-write at that instant is
# DROPPED, past one stderr line, and does NOT come back on its own:
#
# mv /usr/lib/rpcd/file.so /tmp/ ; rpcd reload -> `ubus list` loses `file`
# file restored, no reload -> still missing
# reload (or restart) -> back
#
# First measured this way on the `luci` ucode plugin (24.10/25.12: /usr/libexec/rpcd/luci) — a
# reload racing another package's file replacement left every luci/getFeatures,
# luci/getTimezones and luci/getMountPoints call answering `-32000 Object not found`, reported
# from the field on a SNAPSHOT router, cleared only by a reboot. The SAME race hits `file`, and
# losing `file` is worse: it is what makes System -> Backup/Flash Firmware's "Reset to
# defaults" row silently disappear. view/system/flash.js computes has_rootfs_data from
# /proc/mtd and /proc/mounts read through fs.trimmed(), wrapped in L.resolveDefault(..., '') —
# so the -32000 `file` answers with reads as "no overlay" instead of as an error (OpenWrt forum
# thread 251930, posts 97 and 109 — "fixed" by installing an unrelated package whose OWN
# postinst reload happened to re-register `file`).
#
# 2. THIS PACKAGE HAS NOTHING FOR A RELOAD TO REFRESH. It registers no rpcd object at all — no
# /usr/libexec/rpcd/* script, no /usr/lib/rpcd/*.so, only root/usr/share/rpcd/acl.d/*.json — so
# a reload here only runs the dlopen risk above for every OTHER plugin, for zero gain of its own.
#
# 3. rpcd READS acl.d/*.json AT LOGIN, not only at start or on SIGHUP. Measured: narrow
# rpcd.@login[0].read/write from '*' to an explicit group list, remove the ACL file with rpcd
# already running -> `session access` for uci footstrap write answers false; put the file back
# with NO reload, log in again -> true. A fresh login already sees a just-installed ACL.
#
# 4. On a stock router, root's rpcd login grants read='*' write='*' — blanket. `session access`
# answers true even for an INVENTED object (measured against `uci totally_made_up_pkg_xyz`), so
# the theme's ACL group is irrelevant to a root session from the start.
#
# The one case #3 does not cover: a session that logged in BEFORE this install, on a RESTRICTED
# (non-'*') rpcd login — its in-memory ACL set predates the new grant, and only a fresh login
# re-reads acl.d. Measured: that session stays false until it is forced to re-authenticate.
# Ending the session (not reloading rpcd) fixes exactly that, with a narrower blast radius: the
# postinst below ends a session only when a fresh login would actually gain the scope — denied
# uci/footstrap/write right now, AND its own username's rpcd login lists this theme's ACL group —
# so a restricted login the theme's grant was never added to (a second admin account, a monitoring
# script's login) is left running: re-authenticating would deny it exactly the same as before.
# Verified verbatim on the stand: a blanket-root session survived, a stale restricted session was
# destroyed, the unauthenticated all-zero session was left untouched, and a fresh login answered
# true. The one visible price: an admin on a restricted account WHOSE LOGIN ALREADY LISTS the
# theme's group is logged out by their own install action and must log back in — the narrowest set
# this can be, since a session whose login never grants the group gains nothing from re-logging in.
define Package/luci-theme-footstrap/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
# uci-defaults registers the theme; drop the LuCI caches so it is seen without a reboot.
# Calling it ourselves is belt-and-braces: OpenWrt's default_postinst also runs (then
# deletes) every /etc/uci-defaults/* we ship, so it executes twice per install. It is
# idempotent: the second pass finds the theme already registered and changes nothing.
[ -f /etc/uci-defaults/30_luci-theme-footstrap ] && \
sh /etc/uci-defaults/30_luci-theme-footstrap >/dev/null 2>&1 || true
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
# See the note above the define: no reload. Instead, end a session only when re-authenticating
# would actually hand it the theme's scope: it must be denied uci/footstrap/write right now
# AND its own rpcd login entry must list this theme's ACL group, in either read or write — a
# denied session whose login never grants that group (a second admin account or a monitoring
# script's login the theme was never added to) stays denied after a fresh login too, so ending
# it would only cost someone a session for nothing. Guarded on the `session` object existing at
# all (a router with rpcd built without ubus session support just does nothing here).
ubus list 2>/dev/null | grep -qx session && {
for s in $$(ubus call session list 2>/dev/null | grep -o '"ubus_rpc_session": "[a-f0-9]*"' | cut -d'"' -f4); do
# skips the all-zero session: that one is the UNAUTHENTICATED default and is never
# denied anything by an ACL, so it is not this sweep's business.
case "$$s" in *[!0]*) ;; *) continue ;; esac
ubus call session access "{\"ubus_rpc_session\":\"$$s\",\"scope\":\"uci\",\"object\":\"footstrap\",\"function\":\"write\"}" 2>/dev/null | grep -q true && continue
# A '*' login already answered true above and never reaches here. What is left is a
# denied session: only destroy it if ITS OWN username's rpcd login actually lists the
# theme's ACL group, so a fresh login would gain the scope this sweep is chasing.
u=$$(ubus call session get "{\"ubus_rpc_session\":\"$$s\"}" 2>/dev/null | grep -o '"username": *"[^"]*"' | cut -d'"' -f4)
[ -n "$$u" ] || continue
# Enumerate every login section from `uci show`, not a `uci -q get` index count: the
# latter stops at the FIRST section with no username option (a login block missing that
# option is legal UCI) and never looks at any section past it — fails safe (a session is
# kept, never wrongly destroyed) but skips real sections. `while read` takes one line at
# a time instead of `for`'s word-splitting, because a username may itself contain a
# space, which would otherwise split one section's line into two and skip it. The loop
# runs in a subshell, so its answer cannot survive in a variable — it is echoed out and
# captured instead. A show line's value comes single-quoted
# (`rpcd.@login[0].username='root'`), stripped below.
grant=$$(uci show rpcd 2>/dev/null | grep -F .username= | while read -r kv; do
sect=$${kv%%.username=*}
un=$${kv#*.username=}
un=$${un#\'}
un=$${un%\'}
[ "$$un" = "$$u" ] || continue
for grp in $$(uci -q get "$$sect.read") $$(uci -q get "$$sect.write"); do
[ "$$grp" = luci-theme-footstrap ] && { echo 1; break 2; }
done
done)
[ -n "$$grant" ] || continue
ubus call session destroy "{\"ubus_rpc_session\":\"$$s\"}" >/dev/null 2>&1 || true
done
}
}
exit 0
endef
define Package/luci-theme-footstrap/postrm
#!/bin/sh
# opkg runs the OLD package's postrm with arg "upgrade" during a version upgrade and "remove" on a
# real removal. On upgrade this script MUST change nothing: reverting mediaurlbase and wiping the
# theme registration here is what flipped every updating 24.10 user back to bootstrap. apk never
# runs this on upgrade (it uses the new package's pre/post-upgrade), so guarding on the arg is
# correct for both managers.
case "$$1" in *upgrade*) exit 0 ;; esac
[ -n "$${IPKG_INSTROOT}" ] || {
uci -q delete luci.themes.Footstrap
# Don't leave the active theme pointing at the media dir we just removed: a theme needs its
# media dir AND its ucode template to render, so both are checked before handing the UI over.
case "$$(uci -q get luci.main.mediaurlbase)" in
/luci-static/footstrap*)
[ -d /www/luci-static/bootstrap ] && \
[ -f /usr/share/ucode/luci/template/themes/bootstrap/header.ut ] && \
uci set luci.main.mediaurlbase=/luci-static/bootstrap ;;
esac
uci commit luci
# The admin-uploaded login background (kept out of the package on purpose, so it survives an
# upgrade). A real removal is the one time it should go — this branch never runs on upgrade.
rm -rf /etc/footstrap >/dev/null 2>&1 || true
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
# No rpcd reload here either — see the postinst note. A removal drops nothing rpcd needs to
# be told about (no plugin object, ever); a session still holding the now-gone ACL group is
# simply denied uci/footstrap/write on its next call, which is the correct answer anyway.
}
endef
# Runs right after luci.mk copies luasrc/ucode/htdocs/root/src into PKG_BUILD_DIR, so it rewrites
# the build copy and never the source tree.
#
# 1. cascade.css is generated, not committed — and styles/ is not in luci.mk's copy list, so
# build-css.sh reads it from $(CURDIR) and writes into the build tree. cat and awk only, which
# is why it runs on an OpenWrt buildbot.
# 2. Stamp the git-derived version into fs-version.js. Unconditional: with PKG_VERSION empty it
# falls back to PKG_SRC_VERSION, which luci.mk always defines, so only an un-built source
# checkout keeps the literal '0.0.0-dev'.
#
# The catalogue is luci.mk's business and nothing here compiles it. `po/` is the directory
# LUCI_LANGUAGES globs, so luci.mk emits one `luci-i18n-footstrap-<lang>` package per language, the
# way it does for every luci-app — which is what a package in the luci tree has to look like, and
# what Weblate can see. It was `i18n/` while a fielded self-update script mis-picked a multi-asset
# release with `head -1` (issue #6); that script is retired and owfeed builds one theme artifact
# per format regardless.
define Build/Prepare/luci-theme-footstrap
# luci.mk's Build/Prepare copies only src/ luasrc/ htdocs/ root/ ucode/ into
# $(PKG_BUILD_DIR), and PKG_LICENSE_FILES resolves against THAT — so the Apache text has to
# be put there by hand or the declaration points at nothing.
$(CP) $(CURDIR)/LICENSE $(PKG_BUILD_DIR)/
$(SHELL) $(CURDIR)/build-css.sh $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css
# The private `--fs-*` names are 16% of the sheet and mean nothing to a browser — the same trade
# terser makes for the JS. BEFORE strip-templates.sh on purpose: the reserved set is derived by
# reading the JS and the templates, so it must see them whole. Over-reserving costs bytes,
# under-reserving breaks the theme silently.
# The reserved set is read from $(CURDIR), the SOURCE, never from $(PKG_BUILD_DIR): in CI the
# build tree's JS has already been through terser, so its comments are gone and five names that
# are only MENTIONED in a comment stop being reserved. That made the shipped sheet depend on WHO
# built it — 10 reserved names via CI, 15 via a plain SDK build, same source. Reading the source
# over-reserves by ~1 KB and is the safe direction.
$(SHELL) $(CURDIR)/mangle-tokens.sh \
$(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css \
$(CURDIR)/htdocs/luci-static/resources $(CURDIR)/ucode
# Templates are mostly comments and shipped them all. jsmin/terser strip the JS, build-css.sh the
# CSS; the .ut files were simply never included in that trade. Only `{# … #}` goes — see the
# script for why the ucode-code `/* … */` deliberately stays. -22 KB of 60, git keeps every word,
# and dev-sync.sh does NOT run this: a router you are debugging on keeps its comments.
$(SHELL) $(CURDIR)/strip-templates.sh $(PKG_BUILD_DIR)/ucode
# The gate-only exports (`/* fs:probe */`). A handful of module-private functions are also
# listed in a module's baseclass so a gate in the source repository can call them — nothing on a
# router does. The functions stay; the export line goes, so the surface a package carries is the
# one the theme itself uses.
$(SHELL) $(CURDIR)/strip-probes.sh $(PKG_BUILD_DIR)/htdocs/luci-static/resources
# …and the same for the shell under root/ (71% and 95% comment lines). Whole-line `#` only.
$(SHELL) $(CURDIR)/strip-shell.sh $(PKG_BUILD_DIR)/root
# The two static assets luci.mk has no step for: the SVG favicon carries a 753-byte comment and
# the manifest its indentation, and both are fetched by every browser over a link uhttpd does
# not compress. awk only, so this runs on a buildbot with no node.
$(SHELL) $(CURDIR)/strip-assets.sh $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap
$(SED) "s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$(if $(PKG_VERSION),$(PKG_VERSION),$(PKG_SRC_VERSION))'#" \
$(PKG_BUILD_DIR)/htdocs/luci-static/resources/fs-version.js
endef
# Absolute, not a relative path: CI rsyncs this package into package/, not into the feed.
include $(TOPDIR)/feeds/luci/luci.mk
# The line below is load-bearing and is NOT a signature — do not delete it as boilerplate.
#
# include/scan.mk builds the package list by GREPPING the Makefiles, not by parsing them:
# find -L package -name Makefile | xargs grep -aHE 'call (Build/DefaultTargets|BuildPackage|KernelPackage)'
# A Makefile that does not match is not in the list, so it is never dumped, no CONFIG_PACKAGE_*
# symbol is emitted for it, and `make package/<name>/compile` answers "No rule to make target" with
# no error naming the package anywhere in the build.
#
# This theme never calls BuildPackage itself; luci.mk does, at the include above, and the grep
# cannot see that. So the only thing that puts this package into the SDK's list is the literal text
# on the next line. tools/scan-marker.sh re-derives the grep from scan.mk and fails the build if it
# goes missing.
# call BuildPackage - OpenWrt buildroot signature