architecture.md gains a "UI layout: one core, two skin trees" section — the folder map, the barrel contract that lets main.ts stay ignorant of both trees, why duplicate selectors across trees are safe, and where a new rule belongs (core / a skin / shared tokens). Its "Window kinds" section now describes two-axis root selection, and the "Skins" section is no longer stale prose about a deleted design: it covers the stylesheet split and the two-phase nature of a skin change (CSS live, tree and frame at next window open). gotchas.md gains a section for the split, all of it earned during the work: `.win7` cannot be renamed because 7.scoped.css is precompiled against that literal; reach core through @core/* rather than relative paths, which is what let 67 imports survive two relocations; @font-face url() rebases when a stylesheet moves and fails silently; the @layer statement must stay first; duplicate selectors across trees are safe; and ngtsc type-checks components nothing imports, so a green build does say something about the inactive skin. Also corrected the component paths that moved under skins/, and the updater.service.ts link that moved under core/. Checked every ../ link in docs/ resolves. Co-Authored-By: Claude <noreply@anthropic.com>
8.8 KiB
Build & release
Versioning — "Build n"
Sublime-style: the version IS the commit count. scripts/gen-version.ts runs
git rev-list --count HEAD + git rev-parse --short HEAD and writes the
gitignored src/shared/version.gen.ts (BUILD = {number, commit, date, electrobun}). It is regenerated by postinstall and at the start of every
dev/build/start/release script; committing it would bump the count it
records, hence gitignored. Fresh checkouts fail tsc until bun install has
run once (postinstall creates the file). BRANDING.version stays
semver-shaped (0.1.<n>) for electrobun's config/updater; BRANDING.buildLabel
("Build n") is what humans see (About box, options popover).
Dev loop
bun run dev— gen-version → scripts/dev.ts: startsng serve(HMR) andelectrobun dev --watch, withDEV_SERVER_URLpointing the webviews at the dev server (only honored on the dev channel and only if the server answers).bun run build— gen-version → Angular build →electrobun build(dev channel — fine for boot checks, NOT a release).scripts/node-version-shim.ts(preloaded viabunfig.toml, root AND ui/) fakes a Node version the Angular CLI accepts under Bun. Both preloads are load-bearing.
Release builds
bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64|linux-x64]
(default: host).
Steps: gen-version → gen-icons → Angular build → electrobun build --env=stable → copy/rename artifacts into
release/<target>/miniSCP-Build<n>-<target>.*.
Facts the script encodes:
--env=stableis mandatory for releases — the CLI defaults to the dev channel (different app nameminiSCP-dev, no distributable artifacts).- No OS cross-compile. The CLI builds for its host (its
targetsconfig field is unread in 1.18.1); packaging needs host tools (codesign/hdiutil on macOS, rcedit/PowerShell on Windows). Hence one script run per artifact, on the matching machine;win-x64on a non-Windows host andlinux-x64on a non-Linux host both error out. - mac-x64 from the arm64 Mac (Rosetta): the script downloads electrobun's
x64 CLI binary once (
electrobun-cli-darwin-x64.tar.gzfrom the GitHub release matching the installed version, cached at~/.cache/miniscp/electrobun-cli-x64-<ver>/) and runs it directly — Rosetta translates it, the CLI seesprocess.arch === "x64", downloads thedist-macos-x64core into the electrobun package dir on first use, and packages a genuine x64 app. Note: Bun'sfetch→Bun.writestreaming hung on the GitHub redirect chain; the script usescurl -fsSLdeliberately. - Core binary downloads: the installed npm package ships mac-arm64
binaries only; other
dist-<os>-<arch>cores are fetched fromgithub.com/blackboardsh/electrobun/releasesat build time — network to GitHub is a build-time requirement on every fresh machine/runner. - Artifact shapes (stable channel, in
artifacts/): macOS →stable-macos-<arch>-miniSCP.dmg+.app.tar.zst+update.json(updater feed); Windows →stable-win-x64-miniSCP-Setup.zip(self-extracting installer →%LOCALAPPDATA%) plus the script's own portable zip made from the assembled app folder underbuild/stable-win-x64/(layout to confirm on the first real Windows build — the step warns instead of failing); Linux → onlystable-linux-x64-miniSCP.tar.zst+update.json, i.e. the updater feed and nothing installable, so the script's.tar.gzof the assembled app folder underbuild/stable-linux-x64/IS the release artifact (same best-effort warn). No AppImage/.deb/.desktop — unpack and run theminiSCP.shwrapper the script writes besidebin/launcher(it applies the NVIDIA/DMABUF workaround, which the app cannot set for itself — see gotchas.md). - Linux builds need the AppIndicator dev libraries present to RUN (not to
build) — see platform-notes.md; a packaged tarball started on a machine
without them dies at
dlopen. - The script filters artifacts by app name, so stale artifacts from older brandings don't get collected.
CI (Gitea Actions)
.gitea/workflows/release.yml — tag-triggered (build-*), macos + windows
runner matrix, each running the same release script; inert until act_runners
with those labels are registered. Needs fetch-depth: 0 (the build number is
the commit count). Runner prereqs are commented in the workflow file.
Download host (site/ + Caddy)
site/ is a self-contained landing page and download host, served by Caddy on an
internal box (plain HTTP :80, no domain/TLS). Deploys to /srv/miniscp/.
site/index.html— pure static, no JS, tinygrad-minimal. Download buttons link to fixed stable filenames (/dl/stable-macos-arm64-miniSCP.dmg, …) so the page never changes per release. Logo/screenshot<img>slots are commented in, images live insite/assets/.site/Caddyfile— one flat/dl/*folder withfile_server browse+ range requests, and per-typeCache-Control:*.patchimmutable (unique hashed names),*update.jsonno-cache(updater must see new versions), installers (*.dmg *.zip *.tar.zst)max-age=300(stable names overwritten each release). Landing page has no SPA fallback → unknown paths 404. Path matcher gotcha: use suffix globs like*update.json, not*/*update.json.- One flat folder, no per-build dirs — this layout IS electrobun's
artifacts/feed, so it doubles as the (future) updaterbaseUrl. Publishing = drop this machine'sartifacts/stable-*into/dl/; installers overwrite in place,.patchfiles accumulate (never delete them, or older installs lose their incremental-update trail). Tradeoff: only the latest full installer is downloadable; old full versions are overwritten. site/deploy.sh— run per build machine afterscripts/release.ts; copiesartifacts/stable-*and renames the build-numbered win portable zip to the stablestable-win-x64-miniSCP-portable.zip.MINISCP_DL_DIRoverrides the target (stage locally, then rsync to the server).release.baseUrl = https://miniscp.com/dlis set inelectrobun.config.ts;miniscp.comis fronted by an external proxy for TLS (Caddy itself stays on:80behind it). The value is baked intoversion.jsonat build time, so only builds made after it was added check for updates. WithgeneratePatch(default true), release builds now need build-time network tominiscp.com/dlso electrobun can fetch the currently-published version to diff a.patch(the first build after enabling it has nothing to diff and just ships the full artifacts).
Auto-updater (app side)
Wired on top of the download host above. See the "Auto-updater" section in architecture.md for the runtime flow.
- Backend (src/bun/index.ts):
Updater.checkForUpdate()runs ~5s after launch and every 6h; onupdateAvailableit pushesrpc.send.updateStatus({phase:"available",…}).Updater.onStatusChangestreamsdownload-progress(0–100) →updateStatus{downloading}.checkForUpdateself-guards thedevchannel, sobun run devnever shows the pill. - RPC (src/shared/rpc.ts): requests
checkForUpdate/downloadUpdate/applyUpdate(handlers in rpc-handlers.ts call the electrobunUpdater); push messageupdateStatus. - UI:
UpdaterService(ui/src/app/core/updater.service.ts) holds signals (state/version/progress); the tab-bar "update pill" (tab-bar.ts) is one accent (bg-primary) control that announces → fills as a progress bar while downloading → "Restart to apply".stateisavailable → downloading → ready(ready derived from thedownloadUpdateresponse). App-menu "Check for Updates…" (menu.ts) routes acheck-for-updatesmenu action intoUpdaterService.check(). - Unsigned is fine:
applyUpdate()strips the macOS quarantine xattr before relaunch, so the swapped bundle isn't Gatekeeper-blocked.
Icons
scripts/gen-icons.ts (resvg, dev-time): renders assets/logo.svg into the
macOS iconset + icon.png, a multi-size assets/icon.ico (Windows installer/
taskbar, wired via electrobun.config.ts win.icon), the macOS tray template
PNG from assets/tray/tray-glyph.svg, and a small colored Windows tray PNG.
Rerun after touching either SVG; outputs are committed (build inputs).
electrobun config notes
electrobun.config.ts: app identity comes from BRANDING; build.bun.external
must keep ["cpu-features", "*.node"] (ssh2's optional native accelerators —
bundling them breaks the build; runtime falls back to pure JS);
build.copy ships the Angular dist and tray assets into views://;
bundleCEF: false everywhere (system WebKit / WebView2).