T-386: the engine's JS surface through one shim; T-387 (parts): one component-install script, a derived push list

T-386 (cavekit-engine-update R4b): render/goanna/modules/JihadEngine.jsm (resource://gre/modules/)
holds the engine modules the components import (LightweightThemeManager, AddonManager,
PermissionsUtils) and the about:addons markup the Personas pane decorates (ids, row anonids and
classes, the view controller, the URLs). jihadPersonas.js and jihadBundledAddons.js go through it;
check-engine-api.py reads its SURFACE block (one list, not two) and fails a component importing an
engine module around it (check 4b); its mozconfig search is one level down (a recursive glob had
walked the objdirs for over an hour). test/jihad-engine-shim-test.js passes; device (Enyo):
about:addons shows Personas and Uninstall, the Personas view lists the 14 and Default look in use.

T-387 (R6), two of three: build/install-components.sh installs every
render/goanna/components/*.manifest (now files, not heredocs) with the files it names and the shim,
for the device bundle and the desktop dist alike (the bundle is identical: same files, same
registrations); push-engine-update.sh derives the engine's loose files from the branch's diff and
the objdir's install records (build/engine/engine-loose-files.py): videocontrols.xml,
AddonManager.jsm, amContentHandler.js and amInstallTrigger.js now go with XPIProvider.jsm (they did
not before), verified on the device. build-goanna.sh stops on a failed mach step (a failed
configure had been hidden by an && list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J67ZH8aYy8uzhgZDZ6Nvx9
This commit is contained in:
NotAlexNoyleandClaude Opus 5.5 committed 2026-10-18 18:06:27 -04:00
1 parent 9d8d0d7c31
commit e3e2417899
25 files changed
+416 -203

No files matched your search

+10 -44
View File
@@ -50,10 +50,14 @@ fi
cd "$SRC"
# ./mach is a shell/python polyglot that re-execs itself under python3.
# Each step's failure stops the script (T-387, 2026-10-18): `./mach configure && ./mach build` hid a
# failed configure from set -e (a non-final command of an && list), and the script went on to
# install our components into the OLD dist and report "done" over an engine that never built.
case "$STAGE" in
configure) ./mach configure ;;
build) ./mach build ;;
all) ./mach configure && ./mach build ;;
configure) ./mach configure || { echo "ERROR: mach configure failed"; exit 1; } ;;
build) ./mach build || { echo "ERROR: mach build failed"; exit 1; } ;;
all) ./mach configure || { echo "ERROR: mach configure failed"; exit 1; }
./mach build || { echo "ERROR: mach build failed"; exit 1; } ;;
*) echo "unknown stage: $STAGE (use configure|build|all)"; exit 2 ;;
esac
@@ -88,30 +92,17 @@ fi
# modal XUL window that cannot open here, and every install is cancelled).
PROMPT_JS=/jihad/render/goanna/components/jihadInstallPrompt.js
if [ -f "$PROMPT_JS" ] && [ -d "$DISTBIN/components" ]; then
cp -L "$PROMPT_JS" "$DISTBIN/components/jihadInstallPrompt.js"
cat > "$DISTBIN/components/jihad-install-prompt.manifest" <<'MANIFEST'
component {5cf8e2a6-91a1-44f5-9d33-8ab6f2c40d17} jihadInstallPrompt.js
contract @mozilla.org/addons/web-install-prompt;1 {5cf8e2a6-91a1-44f5-9d33-8ab6f2c40d17}
MANIFEST
if ! grep -q '^manifest components/jihad-install-prompt.manifest$' "$DISTBIN/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-install-prompt.manifest' >> "$DISTBIN/chrome.manifest"
fi
echo " xpi prompt: registered in the desktop dist"
fi
# --- Jihad's own XPCOM components and the engine shim: the device bundle's script (R6, T-387) ----
sh /jihad/build/install-components.sh "$DISTBIN" /jihad
# --- Personas (components/jihadPersonas.js + the bundled set), into the DESKTOP dist -------------
# Mirrors make-device-bundle.sh, so about:addons → Personas and about:personas behave as on the
# device (cavekit-gre-widgets.md R7; inspector 2026-10-08 found the desktop gallery empty).
PERSONAS_JS=/jihad/render/goanna/components/jihadPersonas.js
if [ -f "$PERSONAS_JS" ] && [ -d "$DISTBIN/components" ]; then
cp -L "$PERSONAS_JS" "$DISTBIN/components/jihadPersonas.js"
cat > "$DISTBIN/components/jihad-personas.manifest" <<'MANIFEST'
component {6f0d2b1c-7a3e-4b58-9c21-3e9a5d7f1b02} jihadPersonas.js
contract @riverstonerelay.net/jihad/personas;1 {6f0d2b1c-7a3e-4b58-9c21-3e9a5d7f1b02}
MANIFEST
if ! grep -q '^manifest components/jihad-personas.manifest$' "$DISTBIN/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-personas.manifest' >> "$DISTBIN/chrome.manifest"
fi
rm -rf "$DISTBIN/personas"; mkdir -p "$DISTBIN/personas"
cp -L /jihad/render/goanna/personas/personas.json "$DISTBIN/personas/"
python3 -c 'import json,sys; [print(p["file"]) for p in json.load(open(sys.argv[1]))["personas"]]' \
@@ -126,23 +117,8 @@ fi
CERTS_JS=/jihad/render/goanna/components/jihadCerts.js
PROXY_JS=/jihad/render/goanna/components/jihadProxy.js
if [ -f "$CERTS_JS" ] && [ -f "$PROXY_JS" ] && [ -d "$DISTBIN/components" ]; then
cp -L "$CERTS_JS" "$DISTBIN/components/jihadCerts.js"
cp -L "$PROXY_JS" "$DISTBIN/components/jihadProxy.js"
mkdir -p "$DISTBIN/certs"
cp -L /jihad/render/goanna/certs/jihad-cert-lists.json "$DISTBIN/certs/"
cat > "$DISTBIN/components/jihad-certs.manifest" <<'MANIFEST'
component {3c8e2f1a-5d47-4b9e-a2c6-7f0b1d9e4a63} jihadCerts.js
contract @riverstonerelay.net/jihad/certs;1 {3c8e2f1a-5d47-4b9e-a2c6-7f0b1d9e4a63}
component {8a1d5e3b-2c94-4f07-b6e1-0d7c3a9f5b28} jihadCerts.js
contract @mozilla.org/nsCertificateDialogs;1 {8a1d5e3b-2c94-4f07-b6e1-0d7c3a9f5b28}
component {5b7e2c94-1d3a-4f68-8e0b-a49c6d2f7e13} jihadCerts.js
contract @riverstonerelay.net/jihad/cert-policy;1 {5b7e2c94-1d3a-4f68-8e0b-a49c6d2f7e13}
category content-policy JihadCertPolicy @riverstonerelay.net/jihad/cert-policy;1
MANIFEST
cat > "$DISTBIN/components/jihad-proxy.manifest" <<'MANIFEST'
component {d47b9c02-6e15-4a8f-93b1-5c2e8f0a7d14} jihadProxy.js
contract @riverstonerelay.net/jihad/proxy;1 {d47b9c02-6e15-4a8f-93b1-5c2e8f0a7d14}
MANIFEST
for m in jihad-certs jihad-proxy; do
grep -q "^manifest components/$m.manifest$" "$DISTBIN/chrome.manifest" 2>/dev/null || echo "manifest components/$m.manifest" >> "$DISTBIN/chrome.manifest"
done
@@ -160,19 +136,9 @@ if [ -d "$PREFSUI" ] && [ -f "$ABOUT_JS" ] && [ -d "$DISTBIN/components" ]; then
mkdir -p "$DISTBIN/prefsui"
cp -R "$PREFSUI/content" "$DISTBIN/prefsui/"
cp -L "$PREFSUI/jihad-prefsui.manifest" "$DISTBIN/"
cp -L "$ABOUT_JS" "$DISTBIN/components/jihadAboutPreferences.js"
cat > "$DISTBIN/components/jihad-about-preferences.manifest" <<'MANIFEST'
component {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806} jihadAboutPreferences.js
contract @mozilla.org/network/protocol/about;1?what=preferences {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
contract @mozilla.org/network/protocol/about;1?what=settings {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
contract @mozilla.org/network/protocol/about;1?what=personas {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
MANIFEST
if ! grep -q '^manifest jihad-prefsui.manifest$' "$DISTBIN/chrome.manifest" 2>/dev/null; then
echo 'manifest jihad-prefsui.manifest' >> "$DISTBIN/chrome.manifest"
fi
if ! grep -q '^manifest components/jihad-about-preferences.manifest$' "$DISTBIN/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-about-preferences.manifest' >> "$DISTBIN/chrome.manifest"
fi
for f in jihad-prefsui.manifest prefsui/content/preferences.html prefsui/content/preferences.js \
prefsui/content/preferences.css components/jihadAboutPreferences.js \
components/jihad-about-preferences.manifest; do
+38 -18
View File
@@ -59,23 +59,26 @@ IMPLEMENTED = [
("xpcom/system/nsIXULRuntime.idl", "nsIXULRuntime"),
("xpcom/system/nsIPlatformInfo.idl", "nsIPlatformInfo"),
]
# Engine JS: file, the names that must still be there.
JS_NEEDS = [
("toolkit/mozapps/extensions/LightweightThemeManager.jsm",
["parseTheme", "previewTheme", "resetPreview", "currentTheme", "usedThemes", "forgetUsedTheme",
'"lightweightThemes."', "usedThemes", "lightweight-theme-styling-update", "@personas.mozilla.org"]),
("toolkit/mozapps/extensions/AddonManager.jsm",
["getInstallForFile", "getAddonByID", "addAddonListener"]),
("toolkit/mozapps/extensions/content/extensions.xul",
['id="list-view"', 'id="addon-list"', 'id="addon-list-empty"', 'id="addons-page"']),
("toolkit/mozapps/extensions/content/extensions.xml",
['anonid="enable-btn"', 'anonid="disable-btn"', 'anonid="remove-btn"', 'anonid="version"',
'anonid="description"', 'anonid="control-container"', 'class="content-inner-container"',
'class="disabled-postfix"']),
("toolkit/mozapps/extensions/content/extensions.js",
["gViewController", "currentViewId", '"ViewChanged"', "extensions.ui.lastCategory",
'"category-" + aId']), # the Personas pane renames #category-theme
]
# Engine JS: file, the names that must still be there. T-386 (R4b): read from the shim every
# component goes through, render/goanna/modules/JihadEngine.jsm (its SURFACE block is JSON), so the
# checker checks exactly what the components use.
SHIM = os.path.join(REPO, "render", "goanna", "modules", "JihadEngine.jsm")
# engine modules a component may import directly: stable for years, not the surface's business
DIRECT_OK = {"XPCOMUtils", "Services", "NetUtil", "FileUtils", "Timer", "JihadEngine"}
def shim_surface():
with open(SHIM, encoding="utf-8") as f:
t = f.read()
m = re.search(r"// SURFACE-BEGIN[^\n]*\nconst SURFACE = (\{.*?\});\n// SURFACE-END", t, re.S)
if not m:
raise SystemExit("check-engine-api: no SURFACE block in " + SHIM)
return json.loads(m.group(1))
SURFACE = shim_surface()
JS_NEEDS = ([(m["source"], m["needs"]) for m in SURFACE["modules"].values()] +
sorted(SURFACE["addons"]["markup"].items()))
CLIPBOARD_IDL = "widget/nsIClipboardHelper.idl"
# Prefs the project sets that the engine has no default for, on purpose — read by our own code.
OURS_ONLY = {
@@ -301,6 +304,21 @@ def check(uxp, rep):
else:
rep.ok("%s (%d names)" % (f, len(needs)))
print("4b. components reach the engine's modules through JihadEngine.jsm")
comp_dir = os.path.join(REPO, "render", "goanna", "components")
direct = []
for f in sorted(glob.glob(os.path.join(comp_dir, "*.js"))):
for n, line in enumerate(read(f).splitlines(), 1):
if line.lstrip().startswith(("//", "*", "/*")):
continue
for mod in re.findall(r"resource://gre/modules/(?:[A-Za-z]+/)*([A-Za-z]+)\.jsm", line):
if mod not in DIRECT_OK:
direct.append("%s:%d imports %s" % (os.path.basename(f), n, mod))
if direct:
rep.fail("engine modules imported around the shim: " + "; ".join(direct))
else:
rep.ok("no component imports an engine module around the shim (%d modules in the surface)" % len(SURFACE["modules"]))
print("5. engine prefs the project sets")
defaults = ""
# Default-pref files only — not tests, which mention prefs that no longer exist.
@@ -348,7 +366,9 @@ def check(uxp, rep):
conf += read(f)
except OSError:
pass
for mc in glob.glob(os.path.join(REPO, "build/**/mozconfig*"), recursive=True):
# one level down (build/<dir>/mozconfig*): a recursive glob walked the objdirs under build/
# (millions of files) and the check took over an hour (T-386, 2026-10-18)
for mc in sorted(glob.glob(os.path.join(REPO, "build", "*", "mozconfig*"))):
for opt in re.findall(r"ac_add_options\s+--(?:enable|disable|with|without)-([\w-]+)", read(mc)):
if not re.search(r"(enable|disable|with|without)-" + re.escape(opt) + r"\b", conf):
rep.fail("%s: option --*-%s is not in the engine's configure" % (os.path.relpath(mc, REPO), opt))
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
# Copyright 2026 NotAlexNoyle. MPL-2.0.
"""engine-loose-files.py — cavekit-engine-update.md R6 (T-387): the engine's own files that this
project's engine branch changes and that ship as LOOSE files next to libxul (script modules,
components, chrome bindings), so a libxul push alone would leave them stale on the device.
Derived, not named: the files the branch changes since the release it sits on
(`git diff --name-only UXP_TAG UXP_PIN` in the engine tree), mapped to where the build put them
in dist/bin by the objdir's own records: the install manifest (`_build_manifests/install/dist_bin`),
the preprocessor's dependency files (`**/.deps/*.pp`, for preprocessed modules such as
XPIProvider.jsm) and dist/bin's links into the source (chrome files). Prints the dist-relative
paths that also exist in the bundle, one per line.
engine-loose-files.py --objdir O --uxp TREE --base TAG --head REV [--bundle DIR] [--why]
"""
import argparse, glob, os, subprocess, sys
SRC_PREFIX = "/src/uxp/" # the engine tree as the build container mounts it
def changed_files(uxp, base, head):
out = subprocess.check_output(["git", "-C", uxp, "diff", "--name-only", base, head], text=True)
return {l.strip() for l in out.splitlines() if l.strip()}
def source_rel(path, uxp):
for p in (SRC_PREFIX, os.path.abspath(uxp) + "/"):
if path.startswith(p):
return path[len(p):]
return None
def dist_map(objdir, uxp):
"""source path (engine-relative) -> set of dist/bin-relative destinations"""
m = {}
def add(src, dest):
rel = source_rel(src, uxp)
if rel:
m.setdefault(rel, set()).add(os.path.normpath(dest))
# 1. the install manifest: 1 symlink, 2 copy, 7 preprocess: type, dest, source, ...
man = os.path.join(objdir, "_build_manifests", "install", "dist_bin")
if os.path.exists(man):
with open(man, encoding="utf-8", errors="replace") as f:
for line in f.read().splitlines()[1:]:
p = line.split("\x1f")
if len(p) >= 3 and p[0] in ("1", "2", "7"):
add(p[2], p[1])
# 2. the preprocessor's records: "<target>: <source> <includes...>"
distbin = os.path.normpath(os.path.join(objdir, "dist", "bin")) + "/"
for pp in glob.glob(os.path.join(objdir, "**", ".deps", "*.pp"), recursive=True):
try:
first = open(pp, encoding="utf-8", errors="replace").readline()
except OSError:
continue
if ":" not in first:
continue
target, deps = first.split(":", 1)
tgt = os.path.normpath(os.path.join(os.path.dirname(os.path.dirname(pp)), target.strip()))
if not tgt.startswith(distbin):
continue
for d in deps.split():
add(d, tgt[len(distbin):])
# 3. dist/bin's links into the source (chrome files, jar.mn)
root = os.path.join(objdir, "dist", "bin")
for dp, dirs, files in os.walk(root):
for n in files:
fp = os.path.join(dp, n)
if os.path.islink(fp):
add(os.readlink(fp), os.path.relpath(fp, root))
return m
def main():
a = argparse.ArgumentParser()
a.add_argument("--objdir", required=True)
a.add_argument("--uxp", required=True)
a.add_argument("--base", required=True)
a.add_argument("--head", required=True)
a.add_argument("--bundle")
a.add_argument("--why", action="store_true", help="also print the source each path comes from")
o = a.parse_args()
changed = changed_files(o.uxp, o.base, o.head)
m = dist_map(o.objdir, o.uxp)
if not m:
sys.exit("engine-loose-files: no install records in " + o.objdir)
out = []
for src in sorted(changed):
for dest in sorted(m.get(src, ())):
if o.bundle and not os.path.exists(os.path.join(o.bundle, dest)):
continue
out.append((dest, src))
for dest, src in out:
print(dest + (" <- " + src if o.why else ""))
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# Copyright 2026 NotAlexNoyle. MPL-2.0.
# install-components.sh DEST REPO — cavekit-engine-update.md R6 (T-387): Jihad's own XPCOM
# components into an engine dist, the one script both the device bundle (make-device-bundle.sh)
# and the desktop dist (build/desktop/build-goanna.sh) use. Every render/goanna/components/*.manifest
# is copied with the JS files its "component" lines name and registered from DEST/chrome.manifest
# (idempotent); the engine shim the components import goes to DEST/modules/JihadEngine.jsm. A
# manifest naming a file that is not there fails the build, not the device.
set -eu
DEST="${1:?usage: install-components.sh DEST REPO}"
SRC="${2:?usage: install-components.sh DEST REPO}/render/goanna"
mkdir -p "$DEST/components" "$DEST/modules"
touch "$DEST/chrome.manifest"
n=0
for m in "$SRC"/components/*.manifest; do
[ -f "$m" ] || continue
name=$(basename "$m")
for js in $(awk '$1 == "component" { print $3 }' "$m" | sort -u); do
[ -s "$SRC/components/$js" ] || { echo "ERROR: $name names components/$js, which is missing" >&2; exit 1; }
cp -L "$SRC/components/$js" "$DEST/components/$js"
done
cp -L "$m" "$DEST/components/$name"
grep -q "^manifest components/$name\$" "$DEST/chrome.manifest" || echo "manifest components/$name" >> "$DEST/chrome.manifest"
n=$((n + 1))
done
[ "$n" -gt 0 ] || { echo "ERROR: no component manifests in $SRC/components" >&2; exit 1; }
cp -L "$SRC/modules/JihadEngine.jsm" "$DEST/modules/JihadEngine.jsm"
echo " components: $n manifests registered, modules/JihadEngine.jsm"
+5 -105
View File
@@ -236,6 +236,10 @@ fi
# prompt above) resolving both about: names to chrome://jihad-prefs/content/preferences.html.
# chrome://, not resource://, because the page writes prefs and therefore needs the system
# principal; content cannot reach it because the module does not set URI_SAFE_FOR_UNTRUSTED_CONTENT.
# Jihad's own XPCOM components and the engine shim, by the script the desktop dist uses too
# (cavekit-engine-update.md R6): every render/goanna/components/*.manifest with the files it names.
# The blocks below add each component's data and check what landed.
sh "$REPO/build/install-components.sh" "$OUT" "$REPO"
PREFSUI="$REPO/packaging/prefsui"
ABOUT_JS="$REPO/render/goanna/components/jihadAboutPreferences.js"
if [ -d "$PREFSUI" ] && [ -f "$ABOUT_JS" ]; then
@@ -243,23 +247,10 @@ if [ -d "$PREFSUI" ] && [ -f "$ABOUT_JS" ]; then
mkdir -p "$OUT/prefsui" "$OUT/components"
cp -R "$PREFSUI/content" "$OUT/prefsui/"
cp -L "$PREFSUI/jihad-prefsui.manifest" "$OUT/"
cp -L "$ABOUT_JS" "$OUT/components/jihadAboutPreferences.js"
cat > "$OUT/components/jihad-about-preferences.manifest" <<'MANIFEST'
# Jihad Browser — about:preferences / about:settings. Copyright 2026 NotAlexNoyle. MPL-2.0.
# Both names resolve to the same document (chrome://jihad-prefs/content/preferences.html):
# about:preferences is what Basilisk users type, about:settings is the other name asked for.
component {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806} jihadAboutPreferences.js
contract @mozilla.org/network/protocol/about;1?what=preferences {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
contract @mozilla.org/network/protocol/about;1?what=settings {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
contract @mozilla.org/network/protocol/about;1?what=personas {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
MANIFEST
# Idempotent, same reason as the branding line above.
if ! grep -q '^manifest jihad-prefsui.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest jihad-prefsui.manifest' >> "$OUT/chrome.manifest"
fi
if ! grep -q '^manifest components/jihad-about-preferences.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-about-preferences.manifest' >> "$OUT/chrome.manifest"
fi
# Fail the BUILD, not the device: a half-landed package means about:preferences resolves to
# nothing, and "unknown protocol" points nowhere near this block.
for f in jihad-prefsui.manifest prefsui/content/preferences.html \
@@ -290,18 +281,7 @@ fi
PROMPT_JS="$REPO/render/goanna/components/jihadInstallPrompt.js"
if [ -f "$PROMPT_JS" ]; then
mkdir -p "$OUT/components"
cp -L "$PROMPT_JS" "$OUT/components/jihadInstallPrompt.js"
# Registered from the GRE root manifest, next to the engine's own component manifests.
cat > "$OUT/components/jihad-install-prompt.manifest" <<'MANIFEST'
# Jihad Browser — XPI web-install confirm. Copyright 2026 NotAlexNoyle. MPL-2.0.
# Claims the hook amWebInstallListener checks before falling back to its own modal XUL
# window, which cannot open in this headless embedding.
component {5cf8e2a6-91a1-44f5-9d33-8ab6f2c40d17} jihadInstallPrompt.js
contract @mozilla.org/addons/web-install-prompt;1 {5cf8e2a6-91a1-44f5-9d33-8ab6f2c40d17}
MANIFEST
if ! grep -q '^manifest components/jihad-install-prompt.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-install-prompt.manifest' >> "$OUT/chrome.manifest"
fi
for f in components/jihadInstallPrompt.js components/jihad-install-prompt.manifest; do
[ -s "$OUT/$f" ] || { echo "ERROR: xpi-prompt file missing/empty in bundle: $f" >&2; exit 1; }
done
@@ -313,15 +293,6 @@ fi
PRINT_JS="$REPO/render/goanna/components/jihadPrint.js"
if [ -f "$PRINT_JS" ]; then
mkdir -p "$OUT/components"
cp -L "$PRINT_JS" "$OUT/components/jihadPrint.js"
cat > "$OUT/components/jihad-print.manifest" <<'MANIFEST'
# Jihad Browser — printing over IPP. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {b7f1a6f0-0c5e-4d0b-9a55-3f6f2f7c1d42} jihadPrint.js
contract @riverstonerelay.net/jihad/print;1 {b7f1a6f0-0c5e-4d0b-9a55-3f6f2f7c1d42}
MANIFEST
if ! grep -q '^manifest components/jihad-print.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-print.manifest' >> "$OUT/chrome.manifest"
fi
for f in components/jihadPrint.js components/jihad-print.manifest; do
[ -s "$OUT/$f" ] || { echo "ERROR: print component file missing/empty in bundle: $f" >&2; exit 1; }
done
@@ -333,15 +304,6 @@ fi
PAGECMD_JS="$REPO/render/goanna/components/jihadPageCommands.js"
if [ -f "$PAGECMD_JS" ]; then
mkdir -p "$OUT/components"
cp -L "$PAGECMD_JS" "$OUT/components/jihadPageCommands.js"
cat > "$OUT/components/jihad-page-commands.manifest" <<'MANIFEST'
# Jihad Browser — the page's half of the long-press menu. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {4e0c1b7a-6a53-4a5e-9d0f-0f6a3d1b8c27} jihadPageCommands.js
contract @riverstonerelay.net/jihad/page-commands;1 {4e0c1b7a-6a53-4a5e-9d0f-0f6a3d1b8c27}
MANIFEST
if ! grep -q '^manifest components/jihad-page-commands.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-page-commands.manifest' >> "$OUT/chrome.manifest"
fi
for f in components/jihadPageCommands.js components/jihad-page-commands.manifest; do
[ -s "$OUT/$f" ] || { echo "ERROR: page-commands component file missing/empty in bundle: $f" >&2; exit 1; }
done
@@ -364,15 +326,6 @@ if [ -f "$BUNDLED_JS" ]; then
mkdir -p "$OUT/bundled-addons" "$OUT/components"
cp -L "$PALEFILL_XPI" "$OUT/bundled-addons/palefill@addons.martoks-place.de.xpi"
cp -L "$REPO/third_party/palefill/README.md" "$OUT/bundled-addons/palefill-README.md"
cp -L "$BUNDLED_JS" "$OUT/components/jihadBundledAddons.js"
cat > "$OUT/components/jihad-bundled-addons.manifest" <<'MANIFEST'
# Jihad Browser — offers the add-ons that come with the browser. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {0b9f6a4e-2f3d-4c6e-8f0a-6d2c9b1e7a55} jihadBundledAddons.js
contract @riverstonerelay.net/jihad/bundled-addons;1 {0b9f6a4e-2f3d-4c6e-8f0a-6d2c9b1e7a55}
MANIFEST
if ! grep -q '^manifest components/jihad-bundled-addons.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-bundled-addons.manifest' >> "$OUT/chrome.manifest"
fi
for f in components/jihadBundledAddons.js components/jihad-bundled-addons.manifest \
"bundled-addons/palefill@addons.martoks-place.de.xpi"; do
[ -s "$OUT/$f" ] || { echo "ERROR: bundled add-on file missing/empty in bundle: $f" >&2; exit 1; }
@@ -399,62 +352,26 @@ done
mkdir -p "$OUT/blocklists" "$OUT/components"
cp -L "$EASYLIST_DIR/easylist.txt" "$EASYLIST_DIR/easyprivacy.txt" "$OUT/blocklists/"
cp -L "$EASYLIST_DIR/README.md" "$OUT/blocklists/README.md"
cp -L "$BLOCKER_JS" "$OUT/components/jihadContentBlocker.js"
cat > "$OUT/components/jihad-content-blocker.manifest" <<'MANIFEST'
# Jihad Browser — ads and trackers blocked in the engine (EasyList, EasyPrivacy). Copyright 2026 NotAlexNoyle. MPL-2.0.
component {3c7e1b9a-5d24-4f6e-8a0b-9e2d4c6f1a37} jihadContentBlocker.js
contract @riverstonerelay.net/jihad/content-blocker;1 {3c7e1b9a-5d24-4f6e-8a0b-9e2d4c6f1a37}
category content-policy @riverstonerelay.net/jihad/content-blocker;1 @riverstonerelay.net/jihad/content-blocker;1
MANIFEST
if ! grep -q '^manifest components/jihad-content-blocker.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-content-blocker.manifest' >> "$OUT/chrome.manifest"
fi
echo " content blocking: components/jihadContentBlocker.js + blocklists/ (EasyList, EasyPrivacy; sha256 verified)"
# The browser's identity, adjusted to the page (cavekit-modern-web.md R3): components/jihadUserAgent.js
# learns the sites whose bot check turns the page away and gives them a current Firefox identity.
UA_JS="$REPO/render/goanna/components/jihadUserAgent.js"
[ -f "$UA_JS" ] || { echo "ERROR: $UA_JS is missing" >&2; exit 1; }
mkdir -p "$OUT/components"
cp -L "$UA_JS" "$OUT/components/jihadUserAgent.js"
cat > "$OUT/components/jihad-user-agent.manifest" <<'MANIFEST'
# Jihad Browser — the browser's identity, adjusted to the needs of the page. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {8d4f2c6a-1b7e-4e3a-9f5c-2a6d8e0b4c71} jihadUserAgent.js
contract @riverstonerelay.net/jihad/user-agent;1 {8d4f2c6a-1b7e-4e3a-9f5c-2a6d8e0b4c71}
MANIFEST
if ! grep -q '^manifest components/jihad-user-agent.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-user-agent.manifest' >> "$OUT/chrome.manifest"
fi
echo " identity: components/jihadUserAgent.js (52.9 by default; a learned current identity per site)"
# Web compatibility shims (cavekit-modern-web.md R1/R6): components/jihadWebCompat.js supplies the
# few platform pieces the engine lacks (CSSStyleSheet.replace, popover, inert, Intl.ListFormat …)
# to each page before its scripts run; jihad.compat.trace is the diagnosis mode.
COMPAT_JS="$REPO/render/goanna/components/jihadWebCompat.js"
[ -f "$COMPAT_JS" ] || { echo "ERROR: $COMPAT_JS is missing" >&2; exit 1; }
cp -L "$COMPAT_JS" "$OUT/components/jihadWebCompat.js"
cat > "$OUT/components/jihad-web-compat.manifest" <<'MANIFEST'
# Jihad Browser — web compatibility shims. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {5a1c7e2d-9b3f-4c6e-8d21-7f0a4b9c2e13} jihadWebCompat.js
contract @riverstonerelay.net/jihad/web-compat;1 {5a1c7e2d-9b3f-4c6e-8d21-7f0a4b9c2e13}
MANIFEST
if ! grep -q '^manifest components/jihad-web-compat.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-web-compat.manifest' >> "$OUT/chrome.manifest"
fi
echo " web compat: components/jihadWebCompat.js (shims for what the engine lacks, per page)"
[ -s "$OUT/modules/JihadEngine.jsm" ] || { echo "ERROR: the engine shim is missing from the bundle" >&2; exit 1; }
# Lightweight themes ("personas" — Firefox 52 / Pale Moon / Basilisk): the browser's half of
# the engine's LightweightThemeManager (components/jihadPersonas.js). Required: the kit promises
# the feature (cavekit-addons-extensions.md R10).
PERSONAS_JS="$REPO/render/goanna/components/jihadPersonas.js"
[ -f "$PERSONAS_JS" ] || { echo "ERROR: $PERSONAS_JS is missing" >&2; exit 1; }
mkdir -p "$OUT/components"
cp -L "$PERSONAS_JS" "$OUT/components/jihadPersonas.js"
cat > "$OUT/components/jihad-personas.manifest" <<'MANIFEST'
# Jihad Browser — lightweight themes (personas): web install, apply, tell the cards. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {6f0d2b1c-7a3e-4b58-9c21-3e9a5d7f1b02} jihadPersonas.js
contract @riverstonerelay.net/jihad/personas;1 {6f0d2b1c-7a3e-4b58-9c21-3e9a5d7f1b02}
MANIFEST
if ! grep -q '^manifest components/jihad-personas.manifest$' "$OUT/chrome.manifest" 2>/dev/null; then
echo 'manifest components/jihad-personas.manifest' >> "$OUT/chrome.manifest"
fi
# The manager's modules travel with libxul (modules/LightweightThemeManager.jsm,
# modules/addons/LightweightThemeImageOptimizer.jsm — both in the dist tree the loop above copied).
for f in modules/LightweightThemeManager.jsm modules/addons/LightweightThemeImageOptimizer.jsm; do
@@ -476,24 +393,7 @@ done
python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); assert j["hpStock"]["count"]>100 and j["mozillaRemoved"]["count"]>50 and len(j["relay"]["sha256"])==64' "$CERT_LISTS" \
|| { echo "ERROR: $CERT_LISTS is not a complete list (rebuild it with make-cert-lists.py)" >&2; exit 1; }
mkdir -p "$OUT/components" "$OUT/certs"
cp -L "$CERTS_JS" "$OUT/components/jihadCerts.js"
cp -L "$PROXY_JS" "$OUT/components/jihadProxy.js"
cp -L "$CERT_LISTS" "$OUT/certs/jihad-cert-lists.json"
cat > "$OUT/components/jihad-certs.manifest" <<'MANIFEST'
# Jihad Browser — certificates beyond the engine's own, and the CA question. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {3c8e2f1a-5d47-4b9e-a2c6-7f0b1d9e4a63} jihadCerts.js
contract @riverstonerelay.net/jihad/certs;1 {3c8e2f1a-5d47-4b9e-a2c6-7f0b1d9e4a63}
component {8a1d5e3b-2c94-4f07-b6e1-0d7c3a9f5b28} jihadCerts.js
contract @mozilla.org/nsCertificateDialogs;1 {8a1d5e3b-2c94-4f07-b6e1-0d7c3a9f5b28}
component {5b7e2c94-1d3a-4f68-8e0b-a49c6d2f7e13} jihadCerts.js
contract @riverstonerelay.net/jihad/cert-policy;1 {5b7e2c94-1d3a-4f68-8e0b-a49c6d2f7e13}
category content-policy JihadCertPolicy @riverstonerelay.net/jihad/cert-policy;1
MANIFEST
cat > "$OUT/components/jihad-proxy.manifest" <<'MANIFEST'
# Jihad Browser — following webOS's proxy setting. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {d47b9c02-6e15-4a8f-93b1-5c2e8f0a7d14} jihadProxy.js
contract @riverstonerelay.net/jihad/proxy;1 {d47b9c02-6e15-4a8f-93b1-5c2e8f0a7d14}
MANIFEST
for m in jihad-certs jihad-proxy; do
grep -q "^manifest components/$m.manifest$" "$OUT/chrome.manifest" 2>/dev/null || echo "manifest components/$m.manifest" >> "$OUT/chrome.manifest"
done
+15 -7
View File
@@ -76,9 +76,17 @@ PREFS_MD5=$(md5sum "$TMP/goanna.js" | cut -c1-32)
say "libxul $(du -h "$TMP/libxul.so" | cut -f1) md5=$XUL_MD5; daemon $(du -h "$TMP/jihad-browserserver" | cut -f1) md5=$BS_MD5; goanna.js md5=$PREFS_MD5"
# This project's own chrome scripts, relative to the bundle root.
# ...and the engine's own script modules that this project's engine branch changes (they are
# loose files in the bundle, so a libxul push does not carry them): the add-on manager's provider.
OWN="chrome.manifest jihad-bundle.id jihad-prefsui.manifest modules/addons/XPIProvider.jsm modules/addons/XPIProviderUtils.js $(cd "$BUNDLE" && { ls components/jihad* prefsui/content/* bundled-addons/* personas/* certs/* 2>/dev/null || true; } | tr '\n' ' ')"
# ...and the engine's own loose files (script modules, components, chrome bindings) that this
# project's engine branch changes, which a libxul push does not carry. DERIVED (cavekit-engine-update
# R6, T-387): the files the branch changes since its release (UXP_TAG..UXP_PIN) mapped to where the
# build put them by the objdir's install records (build/engine/engine-loose-files.py); goanna.js,
# which all.js becomes, is pushed above with the prefs.
. "$HERE/../engine/engine-version.env"
ENGINE_LOOSE=$(python3 "$HERE/../engine/engine-loose-files.py" --objdir "$(dirname "$DIST")" --uxp "$HERE/../../third_party/uxp" \
--base "$UXP_TAG" --head "$UXP_PIN" --bundle "$BUNDLE") || die "could not derive the engine's loose files (engine-loose-files.py)"
ENGINE_LOOSE=$(echo "$ENGINE_LOOSE" | grep -v '^goanna\.js$' | tr '\n' ' ')
say "the engine's loose files this branch changes: ${ENGINE_LOOSE:-none}"
OWN="chrome.manifest jihad-bundle.id jihad-prefsui.manifest modules/JihadEngine.jsm $ENGINE_LOOSE $(cd "$BUNDLE" && { ls components/jihad* prefsui/content/* bundled-addons/* personas/* certs/* 2>/dev/null || true; } | tr '\n' ' ')"
[ -f "$BUNDLE/chrome.manifest" ] || die "no chrome.manifest in $BUNDLE — run make-device-bundle.sh"
[ -f "$BUNDLE/jihad-bundle.id" ] || die "no jihad-bundle.id in $BUNDLE — run make-device-bundle.sh"
# The add-on manager's provider (modules/addons/*) belongs to the engine it was staged with.
@@ -147,10 +155,10 @@ for v in $VARIANTS; do
[ -e "$BUNDLE/personas/$f" ] || { novacom run file://bin/rm -- -f "$HL/personas/$f" >/dev/null 2>&1 || true; say "[$v] removed personas/$f (no longer bundled)"; }
done
for f in $OWN; do
# The add-on manager's provider belongs to the engine it was built with. A daemon-only
# push leaves libxul alone, so the provider goes only when the device already runs the
# bundle's libxul (else it waits for the next full push with it).
case "$f" in modules/addons/*)
# The engine's loose files belong to the engine they were built with. A daemon-only push
# leaves libxul alone, so they go only when the device already runs the bundle's libxul
# (else they wait for the next full push with it).
case " $ENGINE_LOOSE " in *" $f "*)
# Compared with the BUNDLE's libxul (what the provider was staged with), not dist's:
# a bundle staged from an older engine build must not pair its provider with a newer
# libxul either. A full push already proved bundle == dist above; a daemon-only push
+1
View File
@@ -9,3 +9,4 @@ Build site: context/plans/build-site.md
| Task | Status | Notes |
|------|--------|-------|
| T-385 | DONE | `render/goanna/EngineCalls.h` (Tier 81) wraps ElementFromPoint, GetViewportInfo, LoadURI, LoadURIWithOptions, NodesFromRect, ResizeReflowIgnoreOverride, SendKeyEvent, SendMouseEventToWindow, SendTouchEventToWindow. `build/engine/check-engine-calls.sh` (new) fails on a direct call in `render/` (proved by a planted call); fixed `render/goanna/test/embed_load.cpp:290`. 19 call sites. |
| T-386 | DONE | `render/goanna/modules/JihadEngine.jsm` (SURFACE JSON + accessors), `components/jihadPersonas.js` and `jihadBundledAddons.js` through it, installed by `make-device-bundle.sh`, `build-goanna.sh`, `push-engine-update.sh`; `check-engine-api.py` derives JS_NEEDS from the surface and adds check 4b; `test/jihad-engine-shim-test.js`; device Enyo about:addons + Personas view verified (screenshots). Not in scope: `packaging/prefsui/content/addons.css` also names about:addons ids (a style sheet, not the components). |
+4 -2
View File
@@ -74,8 +74,10 @@ Reference: `docs/ENGINE.md` (the guide), `docs/ENGINE-SOURCE.md` (the MPL record
### R4b: The engine's JS surface
**Acceptance Criteria:**
- [ ] The engine modules the components import (LightweightThemeManager, AddonManager) and the
about:addons markup the Personas pane decorates are reached through one shim.
- [x] The engine modules the components import (LightweightThemeManager, AddonManager) and the
about:addons markup the Personas pane decorates are reached through one shim. *(2026-10-18,
T-386: `render/goanna/modules/JihadEngine.jsm`; its SURFACE block is also the checker's list;
device: about:addons and its Personas view on Enyo.)*
**Dependencies:** R4
### R5: Written for a human
+1 -1
View File
@@ -727,7 +727,7 @@ Owner: "lay the groundwork for a uxp/goanna update by making our interaction wit
| T-383 | check-engine-api.py + baseline | cavekit-engine-update.md | R4 | T-382 | no | M | done |
| T-384 | docs/ENGINE.md; ENGINE-SOURCE.md brought up to date | cavekit-engine-update.md | R5 | T-380..T-383 | no | M | done |
| T-385 | Wrappers for the positional engine calls | cavekit-engine-update.md | R3 | T-382 | yes | M | DONE 2026-10-18 (the wrappers landed with Tier 81 as `render/goanna/EngineCalls.h`; `build/engine/check-engine-calls.sh` now guards them: the wrapped methods are read from the header and any direct call in `render/` fails it; it found one, `test/embed_load.cpp`'s `LoadURI`, now `engine_calls::LoadURIPlain`, compiled in the desktop container; 19 call sites) |
| T-386 | JS shim for engine modules and about:addons markup | cavekit-engine-update.md | R4b | — | yes | M | pending |
| T-386 | JS shim for engine modules and about:addons markup | cavekit-engine-update.md | R4b | — | yes | M | DONE 2026-10-18 (`render/goanna/modules/JihadEngine.jsm`, installed as `resource://gre/modules/JihadEngine.jsm` by the bundle, the desktop build and the engine-update push: LightweightThemeManager, AddonManager, PermissionsUtils and about:addons' ids, row anonids/classes, view controller and URLs; `jihadPersonas.js` and `jihadBundledAddons.js` go through it; `check-engine-api.py` reads its SURFACE block (one list) and fails a component importing an engine module around it; `test/jihad-engine-shim-test.js` passes; device (Enyo): about:addons shows "Personas" and "Uninstall", the Personas view lists "the 14 you have" and Default look in use, no errors) |
| T-387 | Shared component install; derived push list; desktop tests for the uncovered paths | cavekit-engine-update.md | R6 | T-380 | no | L | pending |
## Tier 73 — Personas UI on about:addons (2026-10-08; added to the site, not a regeneration)
+1 -2
View File
@@ -135,6 +135,7 @@ version add-ons are judged against — it follows the engine's compatibility lev
|---|---|
| `build/engine/update-engine.sh [TAG] / --finish / --abort / --pins` | moves the engine to an upstream release: the queue carried across with a three-way merge, the overlaps listed, the API checked, every pin rewritten (§6) |
| `build/engine/check-engine-pins.sh` | every pin and version string agrees with `engine-version.env` |
| `render/goanna/modules/JihadEngine.jsm` | the engine's JS surface the components use (modules, about:addons markup), in one place; `check-engine-api.py` checks its SURFACE block and that no component imports an engine module around it |
| `build/engine/check-engine-calls.sh` | the positional engine calls go through `render/goanna/EngineCalls.h` only (the wrapped methods are read from the header; a direct call anywhere in `render/` fails it) |
| `build/engine/patch-queue.sh --check` | the patch queue reproduces the engine branch exactly |
| `build/engine/check-engine-api.py [--uxp TREE]` | what in TREE no longer matches what Jihad uses: our exports (§4.1), IDL signatures and implemented interfaces (§4.2) against `engine-api-baseline.json`, the engine JS and about:addons markup (§4.3), prefs (§4.4), mozconfig options and the ICU file (§4.5). Run it on a candidate before building. |
@@ -209,8 +210,6 @@ about:settings open; printing finds the test sink (`render/goanna/test/ipp-sink.
## 8. Next steps (not done yet)
- A JS shim for the engine modules the components import (LightweightThemeManager,
AddonManager), and for the about:addons markup the Personas pane relies on.
- One component-install script shared by `build-goanna.sh` and `make-device-bundle.sh` (their
manifest blocks duplicate each other).
- Derive `push-engine-update.sh`'s loose-file list from the engine branch's diff instead of
@@ -0,0 +1,7 @@
# Jihad Browser — about:preferences / about:settings. Copyright 2026 NotAlexNoyle. MPL-2.0.
# Both names resolve to the same document (chrome://jihad-prefs/content/preferences.html):
# about:preferences is what Basilisk users type, about:settings is the other name asked for.
component {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806} jihadAboutPreferences.js
contract @mozilla.org/network/protocol/about;1?what=preferences {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
contract @mozilla.org/network/protocol/about;1?what=settings {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
contract @mozilla.org/network/protocol/about;1?what=personas {0f2b8f4e-6c31-4a7b-9f2d-5c1e73a4b806}
@@ -0,0 +1,3 @@
# Jihad Browser — offers the add-ons that come with the browser. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {0b9f6a4e-2f3d-4c6e-8f0a-6d2c9b1e7a55} jihadBundledAddons.js
contract @riverstonerelay.net/jihad/bundled-addons;1 {0b9f6a4e-2f3d-4c6e-8f0a-6d2c9b1e7a55}
@@ -0,0 +1,8 @@
# Jihad Browser — certificates beyond the engine's own, and the CA question. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {3c8e2f1a-5d47-4b9e-a2c6-7f0b1d9e4a63} jihadCerts.js
contract @riverstonerelay.net/jihad/certs;1 {3c8e2f1a-5d47-4b9e-a2c6-7f0b1d9e4a63}
component {8a1d5e3b-2c94-4f07-b6e1-0d7c3a9f5b28} jihadCerts.js
contract @mozilla.org/nsCertificateDialogs;1 {8a1d5e3b-2c94-4f07-b6e1-0d7c3a9f5b28}
component {5b7e2c94-1d3a-4f68-8e0b-a49c6d2f7e13} jihadCerts.js
contract @riverstonerelay.net/jihad/cert-policy;1 {5b7e2c94-1d3a-4f68-8e0b-a49c6d2f7e13}
category content-policy JihadCertPolicy @riverstonerelay.net/jihad/cert-policy;1
@@ -0,0 +1,4 @@
# Jihad Browser — ads and trackers blocked in the engine (EasyList, EasyPrivacy). Copyright 2026 NotAlexNoyle. MPL-2.0.
component {3c7e1b9a-5d24-4f6e-8a0b-9e2d4c6f1a37} jihadContentBlocker.js
contract @riverstonerelay.net/jihad/content-blocker;1 {3c7e1b9a-5d24-4f6e-8a0b-9e2d4c6f1a37}
category content-policy @riverstonerelay.net/jihad/content-blocker;1 @riverstonerelay.net/jihad/content-blocker;1
@@ -0,0 +1,5 @@
# Jihad Browser — XPI web-install confirm. Copyright 2026 NotAlexNoyle. MPL-2.0.
# Claims the hook amWebInstallListener checks before falling back to its own modal XUL
# window, which cannot open in this headless embedding.
component {5cf8e2a6-91a1-44f5-9d33-8ab6f2c40d17} jihadInstallPrompt.js
contract @mozilla.org/addons/web-install-prompt;1 {5cf8e2a6-91a1-44f5-9d33-8ab6f2c40d17}
@@ -0,0 +1,3 @@
# Jihad Browser — the page's half of the long-press menu. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {4e0c1b7a-6a53-4a5e-9d0f-0f6a3d1b8c27} jihadPageCommands.js
contract @riverstonerelay.net/jihad/page-commands;1 {4e0c1b7a-6a53-4a5e-9d0f-0f6a3d1b8c27}
@@ -0,0 +1,3 @@
# Jihad Browser — lightweight themes (personas): web install, apply, tell the cards. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {6f0d2b1c-7a3e-4b58-9c21-3e9a5d7f1b02} jihadPersonas.js
contract @riverstonerelay.net/jihad/personas;1 {6f0d2b1c-7a3e-4b58-9c21-3e9a5d7f1b02}
@@ -0,0 +1,3 @@
# Jihad Browser — printing over IPP. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {b7f1a6f0-0c5e-4d0b-9a55-3f6f2f7c1d42} jihadPrint.js
contract @riverstonerelay.net/jihad/print;1 {b7f1a6f0-0c5e-4d0b-9a55-3f6f2f7c1d42}
@@ -0,0 +1,3 @@
# Jihad Browser — following webOS's proxy setting. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {d47b9c02-6e15-4a8f-93b1-5c2e8f0a7d14} jihadProxy.js
contract @riverstonerelay.net/jihad/proxy;1 {d47b9c02-6e15-4a8f-93b1-5c2e8f0a7d14}
@@ -0,0 +1,3 @@
# Jihad Browser — the browser's identity, adjusted to the needs of the page. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {8d4f2c6a-1b7e-4e3a-9f5c-2a6d8e0b4c71} jihadUserAgent.js
contract @riverstonerelay.net/jihad/user-agent;1 {8d4f2c6a-1b7e-4e3a-9f5c-2a6d8e0b4c71}
@@ -0,0 +1,3 @@
# Jihad Browser — web compatibility shims. Copyright 2026 NotAlexNoyle. MPL-2.0.
component {5a1c7e2d-9b3f-4c6e-8d21-7f0a4b9c2e13} jihadWebCompat.js
contract @riverstonerelay.net/jihad/web-compat;1 {5a1c7e2d-9b3f-4c6e-8d21-7f0a4b9c2e13}
@@ -27,6 +27,8 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
// The engine's modules through one shim (cavekit-engine-update.md R4b)
Cu.import("resource://gre/modules/JihadEngine.jsm");
// What ships. `version` is the version of the bundled FILE (the package's make-device-bundle.sh
// pins the file by SHA-256, so this cannot drift from it unnoticed).
@@ -94,7 +96,7 @@ function install(AddonManager, b, why) {
function offerAll(attempt) {
let AddonManager = null;
try {
AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager;
AddonManager = JihadEngine.AddonManager;
// Throws until the manager has started; that is the readiness test.
AddonManager.getAddonByID(BUNDLED[0].id, function() {});
} catch (e) {
+23 -23
View File
@@ -35,6 +35,9 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
// The engine's modules and about:addons' markup, through one shim (cavekit-engine-update.md R4b)
Cu.import("resource://gre/modules/JihadEngine.jsm");
const ADDONS = JihadEngine.addons;
const PREF_CURRENT = "jihad.theme.current";
// Where a static theme's images are unpacked (under the profile), served to the theme manager
@@ -56,7 +59,7 @@ function notify(category, text) {
}
function manager() {
return Cu.import("resource://gre/modules/LightweightThemeManager.jsm", {}).LightweightThemeManager;
return JihadEngine.LightweightThemeManager;
}
function hostOf(url) {
@@ -797,7 +800,7 @@ function previewAllowed(origin) {
// R11); the add-on manager imports it only when an install is checked, so it is imported here too
// (once per run; it does nothing after that).
try {
Cu.import("resource://gre/modules/PermissionsUtils.jsm", {}).PermissionsUtils.importFromPrefs("xpinstall.", "install");
JihadEngine.PermissionsUtils.importFromPrefs("xpinstall.", "install");
} catch (e) {}
try { return Services.perms.testPermission(uri, "install") === Services.perms.ALLOW_ACTION; }
catch (e) { return false; }
@@ -899,12 +902,12 @@ function themeIdOf(addonId) {
const XPI_DIRS = ["/media/internal/downloads", "/media/internal"];
const PANE_CSS =
"@namespace url(\"" + XUL_NS + "\");\n" +
"#addons-page button:not(.button-link) { min-height: 44px !important; min-width: 64px !important; }\n" +
"#addons-page .category { min-height: 52px; }\n" +
"#addons-page[jihad-themes-view=\"true\"] #addon-list-empty { display: none; }\n" +
ADDONS.selector("page") + " button:not(.button-link) { min-height: 44px !important; min-width: 64px !important; }\n" +
ADDONS.selector("page") + " .category { min-height: 52px; }\n" +
ADDONS.selector("page") + "[jihad-themes-view=\"true\"] " + ADDONS.selector("addonListEmpty") + " { display: none; }\n" +
// Only the worn Persona here: the list lives in the gallery, which the card can scroll (the owner,
// 2026-10-12: "I cant scroll down in personas in about:addons").
"#addons-page[jihad-themes-view=\"true\"] #addon-list > richlistitem:not([active=\"true\"]) { display: none !important; }\n" +
ADDONS.selector("page") + "[jihad-themes-view=\"true\"] " + ADDONS.selector("addonList") + " > richlistitem:not([active=\"true\"]) { display: none !important; }\n" +
"#jihad-themes-box { padding: 10px 12px 4px; }\n" +
"#jihad-themes-box .jihad-themes-intro { margin: 0 0 8px; font-size: 1.05em; }\n" +
"#jihad-themes-box .jihad-row { min-height: 64px; padding: 6px 10px; margin: 0 0 8px;\n" +
@@ -946,9 +949,7 @@ function isDiscoverDocument(doc) {
}
function isAddonsDocument(doc) {
let u = "";
try { u = String(doc.documentURI || ""); } catch (e) { return false; }
return u.indexOf("about:addons") === 0 || u.indexOf("chrome://mozapps/content/extensions/extensions.xul") === 0;
return ADDONS.isDocument(doc);
}
function xul(doc, tag, attrs) {
@@ -1439,7 +1440,7 @@ function decorateRow(doc, item, theme, isCurrent) {
item.style.setProperty("color", text || "#ffffff", "important");
item.style.setProperty("min-height", "96px", "important");
item.style.MozBoxOrdinalGroup = isCurrent ? "0" : "1";
let anon = function(attr, val) { return doc.getAnonymousElementByAttribute(item, attr, val); };
let anon = function(attr, val) { return ADDONS.part(doc, item, attr, val); };
// The text sits on a pane in the opposite of its colour, so the name reads over any picture.
let pane = anon("class", "content-inner-container");
if (pane) {
@@ -1487,14 +1488,14 @@ function decorateRow(doc, item, theme, isCurrent) {
// the tall page as it pans any other (cavekit-chrome-look.md R3).
function growToList(win) {
let doc = win.document, root = doc.documentElement;
let gVC = win.gViewController;
let gVC = ADDONS.viewController(win);
// Every view of the manager is laid out at full size inside its deck, shown or not: only the one
// shown counts (inspector 2026-10-12).
let panel = gVC && gVC.viewPort ? gVC.viewPort.selectedPanel : null;
root.style.minHeight = ""; // measured as it lays out without the stretch
let need = 0;
if (panel) {
let list = panel.id === "detail-view" ? null : panel.querySelector("richlistbox");
let list = panel.id === ADDONS.ids.detailView ? null : panel.querySelector("richlistbox");
if (list) {
// The list's items, not its box (a stretched box measures its own stretch).
let rect = list.getBoundingClientRect(), first = null, last = null;
@@ -1516,19 +1517,18 @@ function growToList(win) {
function decoratePane(win) {
let doc = win.document;
later(200, function() { try { growToList(win); } catch (e) { log("about:addons height: " + e); } });
let gVC = win.gViewController;
let viewId = String((gVC && gVC.currentViewId) || "");
let isThemes = viewId.indexOf("addons://list/theme") === 0;
let viewId = ADDONS.currentViewId(win);
let isThemes = viewId.indexOf(ADDONS.themeView) === 0;
doc.documentElement.setAttribute("jihad-themes-view", isThemes ? "true" : "false");
// The category is called Personas, as everywhere else in this browser (owner 2026-10-08).
let cat = doc.getElementById("category-theme");
let cat = ADDONS.element(doc, "categoryTheme");
if (cat && cat.getAttribute("name") !== "Personas") { cat.setAttribute("name", "Personas"); }
let listView = doc.getElementById("list-view"), list = doc.getElementById("addon-list");
let listView = ADDONS.element(doc, "listView"), list = ADDONS.element(doc, "addonList");
let box = doc.getElementById("jihad-themes-box");
// "Uninstall", not "Remove", on every add-on, in every category (owner 2026-10-08).
if (list) {
for (let i = 0; i < list.children.length; i++) {
let rm = doc.getAnonymousElementByAttribute(list.children[i], "anonid", "remove-btn");
let rm = ADDONS.part(doc, list.children[i], "anonid", "remove-btn");
if (rm && rm.getAttribute("label") !== "Uninstall") { rm.setAttribute("label", "Uninstall"); }
}
}
@@ -1613,8 +1613,8 @@ function attachPane(win) {
// The pane's window may be gone by the time a deferred call runs (a card closing while a
// page loads logged "themes pane: TypeError: can't access dead object", round 2, 2026-10-15).
let again = function() { try { if (Cu.isDeadWrapper(win) || win.closed) { return; } decoratePane(win); } catch (e) { log("themes pane: " + e); } };
doc.addEventListener("ViewChanged", again, true);
let list = doc.getElementById("addon-list");
doc.addEventListener(ADDONS.viewChangedEvent, again, true);
let list = ADDONS.element(doc, "addonList");
if (list) {
let pending = 0;
new win.MutationObserver(function() {
@@ -1624,7 +1624,7 @@ function attachPane(win) {
}
// An add-on's preferences arrive in its page after the view has changed (the manager loads
// them): the page's height is measured again when they do.
let rows = doc.getElementById("detail-rows");
let rows = ADDONS.element(doc, "detailRows");
if (rows) {
let measuring = 0;
new win.MutationObserver(function() {
@@ -1859,7 +1859,7 @@ function galleryRequest(bag) {
let id = get("id");
if (!installed[id]) { reply({ok: false}); return; }
try {
let AM = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager;
let AM = JihadEngine.AddonManager;
AM.getAddonByID(id + ID_SUFFIX, function(addon) {
try { if (addon) { addon.uninstall(); } } catch (e) { log("remove " + id + ": " + e); }
try { Services.obs.notifyObservers(null, "jihad-personas-mine", id); } catch (e) {}
@@ -1930,7 +1930,7 @@ function JihadPersonas() {
Services.obs.addObserver(this, "content-document-global-created", false);
Services.obs.addObserver(this, "chrome-document-global-created", false);
try {
let AM = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager;
let AM = JihadEngine.AddonManager;
AM.addAddonListener({
onUninstalled: function(addon) {
if (!addon || addon.type !== "theme") { return; }
+120
View File
@@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Jihad Browser — JihadEngine.jsm: the engine's JS surface Jihad's components rely on, in one place
* (cavekit-engine-update.md R4b, T-386).
*
* The engine modules the components import (LightweightThemeManager, AddonManager,
* PermissionsUtils) and the about:addons markup the Personas pane decorates (element ids, the
* anonids of an add-on's row, classes, the view controller) change between UXP releases without the
* compiler saying so. Every component reaches them through here, and
* build/engine/check-engine-api.py reads the SURFACE block below (the same JSON) to check a
* candidate engine against exactly what the components use: one list, not two.
*
* Installed as resource://gre/modules/JihadEngine.jsm (modules/ in the GRE).
*/
"use strict";
this.EXPORTED_SYMBOLS = ["JihadEngine"];
const Cu = Components.utils;
// SURFACE-BEGIN (JSON: check-engine-api.py parses the text between the markers)
const SURFACE = {
"modules": {
"LightweightThemeManager": {
"url": "resource://gre/modules/LightweightThemeManager.jsm",
"source": "toolkit/mozapps/extensions/LightweightThemeManager.jsm",
"needs": ["parseTheme", "previewTheme", "resetPreview", "currentTheme", "usedThemes", "forgetUsedTheme",
"\"lightweightThemes.\"", "lightweight-theme-styling-update", "@personas.mozilla.org"]
},
"AddonManager": {
"url": "resource://gre/modules/AddonManager.jsm",
"source": "toolkit/mozapps/extensions/AddonManager.jsm",
"needs": ["getInstallForFile", "getAddonByID", "addAddonListener"]
},
"PermissionsUtils": {
"url": "resource://gre/modules/PermissionsUtils.jsm",
"source": "toolkit/modules/PermissionsUtils.jsm",
"needs": ["importFromPrefs"]
}
},
"addons": {
"urls": ["about:addons", "chrome://mozapps/content/extensions/extensions.xul"],
"ids": {
"page": "addons-page",
"listView": "list-view",
"addonList": "addon-list",
"addonListEmpty": "addon-list-empty",
"detailView": "detail-view",
"detailRows": "detail-rows",
"categoryTheme": "category-theme"
},
"themeView": "addons://list/theme",
"anonids": ["name", "description", "date-updated", "version", "details-btn", "enable-btn", "disable-btn", "remove-btn"],
"classes": ["content-inner-container", "disabled-postfix"],
"markup": {
"toolkit/mozapps/extensions/content/extensions.xul": ["id=\"addons-page\"", "id=\"list-view\"", "id=\"addon-list\"",
"id=\"addon-list-empty\"", "id=\"detail-view\"", "id=\"detail-rows\""],
"toolkit/mozapps/extensions/content/extensions.xml": ["anonid=\"name\"", "anonid=\"description\"", "anonid=\"date-updated\"",
"anonid=\"version\"", "anonid=\"details-btn\"", "anonid=\"enable-btn\"", "anonid=\"disable-btn\"", "anonid=\"remove-btn\"",
"anonid=\"control-container\"", "class=\"content-inner-container\"", "class=\"disabled-postfix\""],
"toolkit/mozapps/extensions/content/extensions.js": ["gViewController", "currentViewId", "viewPort", "\"ViewChanged\"",
"extensions.ui.lastCategory", "\"category-\" + aId", "addons://list/"]
}
}
};
// SURFACE-END
this.JihadEngine = {
surface: SURFACE,
// An engine module by its name in SURFACE.modules (throws for a name not listed there, so a new
// dependency is added to the surface, and so to the checker, first).
module(name) {
const m = SURFACE.modules[name];
if (!m) { throw new Error("JihadEngine: " + name + " is not in the surface"); }
return Cu.import(m.url, {})[name];
},
get LightweightThemeManager() { return this.module("LightweightThemeManager"); },
get AddonManager() { return this.module("AddonManager"); },
get PermissionsUtils() { return this.module("PermissionsUtils"); },
// about:addons
addons: {
ids: SURFACE.addons.ids,
themeView: SURFACE.addons.themeView,
// is this document the add-on manager?
isDocument(doc) {
let u = "";
try { u = String(doc.documentURI || ""); } catch (e) { return false; }
return SURFACE.addons.urls.some(function(p) { return u.indexOf(p) === 0; });
},
// an element by its key in SURFACE.addons.ids
element(doc, key) {
const id = SURFACE.addons.ids[key];
if (!id) { throw new Error("JihadEngine: no about:addons id " + key); }
return doc.getElementById(id);
},
// "#id" for a style sheet
selector(key) {
const id = SURFACE.addons.ids[key];
if (!id) { throw new Error("JihadEngine: no about:addons id " + key); }
return "#" + id;
},
// a row's part by anonid or class (both listed in the surface)
part(doc, item, attr, value) {
const listed = attr === "anonid" ? SURFACE.addons.anonids : attr === "class" ? SURFACE.addons.classes : [];
if (listed.indexOf(value) < 0) { throw new Error("JihadEngine: about:addons " + attr + " " + value + " is not in the surface"); }
return doc.getAnonymousElementByAttribute(item, attr, value);
},
// the manager's view controller and the view shown
viewController(win) { return win.gViewController || null; },
currentViewId(win) {
const c = win.gViewController;
return String((c && c.currentViewId) || "");
},
viewChangedEvent: "ViewChanged",
},
};
@@ -0,0 +1,25 @@
// Copyright 2026 NotAlexNoyle. MPL-2.0.
// jihad-engine-shim-test.js — T-386 (cavekit-engine-update R4b): JihadEngine.jsm outside the engine,
// with a fake Components and document. Run: node render/goanna/test/jihad-engine-shim-test.js render/goanna/modules/JihadEngine.jsm
// unit test of the shim outside the engine: a fake Components and document
const fs = require("fs");
const src = fs.readFileSync(process.argv[2], "utf8");
const imported = [];
const Components = { utils: { import(url, scope) { imported.push(url); const n = url.replace(/.*\//, "").replace(".jsm", ""); return { [n]: { name: n } }; } } };
const ctx = {};
new Function("Components", src + "\nreturn this;").call(ctx, Components);
const E = ctx.JihadEngine;
let fails = 0; const ok = (c, m) => { if (!c) { fails++; console.log("FAIL", m); } };
ok(E.LightweightThemeManager.name === "LightweightThemeManager", "LTM by name");
ok(E.AddonManager.name === "AddonManager", "AM by name");
ok(imported[1] === "resource://gre/modules/AddonManager.jsm", "AM url");
let threw = false; try { E.module("Nope"); } catch (e) { threw = true; } ok(threw, "unknown module throws");
ok(E.addons.selector("page") === "#addons-page", "selector");
ok(E.addons.isDocument({ documentURI: "about:addons" }) && !E.addons.isDocument({ documentURI: "about:blank" }), "isDocument");
const doc = { getElementById: id => ({ id }), getAnonymousElementByAttribute: (i, a, v) => ({ a, v }) };
ok(E.addons.element(doc, "categoryTheme").id === "category-theme", "element");
ok(E.addons.part(doc, {}, "anonid", "remove-btn").v === "remove-btn", "part");
threw = false; try { E.addons.part(doc, {}, "anonid", "not-listed"); } catch (e) { threw = true; } ok(threw, "unlisted part throws");
ok(E.addons.currentViewId({ gViewController: { currentViewId: "addons://list/theme" } }) === "addons://list/theme", "view id");
console.log(fails ? fails + " FAILED" : "shim test: all passed");
process.exit(fails ? 1 : 0);