Files
small-packages/luci-theme-footstrap/Makefile
T
action ba0a17863f
marry-jell / merge (push) Canceled after 0s
update 2026-09-25 19:24:22
2026-09-25 19:24:22 +08:00

258 lines
16 KiB
Makefile

#
# 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. luci.mk falls back to PKG_SRC_VERSION,
# which it always defines, when nothing here sets it — see docs/package.md for why no override
# lives here.
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: the released package is built by owfeed (tools/stage.sh), never by this Makefile through
# the SDK — so LUCI_MINIFY_JS keeps luci.mk's default jsmin here. 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.
# 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.
# default_postinst also runs it; the second pass is idempotent.
[ -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.
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