# # 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:=52 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 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 # reload, never restart: rpcd holds sessions in memory, so `restart` logs out every LuCI user, # including the admin who just clicked Update. A session survives `reload` and dies across # `restart` — verified on a live router. # # AND RELOAD RE-READS THE PLUGINS, not only /usr/share/rpcd/acl.d/*. That correction matters, # because it is what makes the check below necessary rather than decorative. Measured on a # SNAPSHOT stand, where the `luci` object comes from the ucode plugin # /usr/share/rpcd/ucode/luci (24.10 and 25.12 use /usr/libexec/rpcd/luci): # # file removed + reload -> `ubus list` loses `luci` (and it does NOT come back on its own) # file removed + restart -> still gone # file restored + reload -> back # # So a reload that lands while another package is replacing that file leaves rpcd without the # object, and every luci/getFeatures, luci/getTimezones and luci/getMountPoints call answers # `-32000 Object not found` until something reloads it again — the page then renders with the # system time blank and an RPCError box per call. Reported from the field on a SNAPSHOT router, # cleared by a reboot; nine days of uptime before it, so it was an upgrade that did it. # # Hence: reload, then ASK whether the object is there, and reload once more if it is not. A second # reload and not a restart, because the table above is the whole argument — reload already does # everything restart would do to the plugin set, and restart adds only the logout. If the object is # still missing after that, it is not this package's reload that removed it, and throwing every # admin out of LuCI would not bring it back. define Package/luci-theme-footstrap/postinst #!/bin/sh [ -n "$${IPKG_INSTROOT}" ] || { # uci-defaults registers the theme; refresh rpcd + drop caches so LuCI sees it 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 /etc/init.d/rpcd reload >/dev/null 2>&1 || true # see the note above the define: a reload that raced another package's file replacement leaves # rpcd without the `luci` object, and nothing brings it back on its own ubus list 2>/dev/null | grep -qx luci || { sleep 1 /etc/init.d/rpcd reload >/dev/null 2>&1 || true } } 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 /etc/init.d/rpcd reload >/dev/null 2>&1 || true # see the postinst note: reload keeps sessions } 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-` 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//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