# 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.`) for electrobun's config/updater; `BRANDING.buildLabel` ("Build n") is what humans see (About box, Settings → About). ## Dev loop - `bun run dev` — gen-version → `electrobun prepare` → scripts/dev.ts: starts `ng serve` (HMR) and `electrobun dev --watch`, with `DEV_SERVER_URL` pointing the webviews at the dev server (only honored on the dev channel and only if the server answers). - `bun run build` — gen-version → `electrobun prepare` → Angular build → `electrobun build` (**dev channel** — fine for boot checks, NOT a release). - `scripts/node-version-shim.ts` (preloaded via `bunfig.toml`, root AND ui/) fakes a Node version the Angular CLI accepts under Bun. Both preloads are load-bearing. - **Electrobun 2.x is driven by Hutch.** The `electrobun` devDependency (exact `2.0.2`) is only a bootstrap: its first command downloads the paired Hutch (0.27.1) from the Electrobun GitHub release, checks its SHA-256 against `hutch-artifacts.json`, caches it under `~/.hutch/npm/electrobun//`, and forwards every `electrobun …` to `hutch electrobun …`. Hutch then fetches the Electrobun core, its build-time Cottontail and the pinned Bun 1.4.0 (the runtime the app ships) into `~/.hutch` (~200 MB on a fresh machine) and projects the SDK into the gitignored `.hutch/devkit/` (plus a gitignored `.cottontail-tmp/` scratch dir). The devDependency version IS the toolchain pin: moving Electrobun = bump it + `bun install`. No Node needed — the bootstrap's `#!/usr/bin/env node` runs under `bunx`. - `bunx electrobun prepare` (re)projects `.hutch/devkit`. It runs from `postinstall` **and** at the start of `dev`/`start`/`build`/`release` (~0.5 s when current — the slot the v1 CLI re-sign step held; that step is gone, Hutch's binaries carry valid signatures). tsc (the root tsconfig extends `.hutch/devkit/tsconfig.json`) and the Angular build / `ng serve` (the `electrobun/view` path in `ui/tsconfig.json`) resolve the SDK there *before* any electrobun command would project it; without it both fail on unresolved `electrobun/*` imports. Bun stays the package manager: Hutch's own resolver (`hutch.lock`, no lifecycle scripts) only runs on `hutch install`, which nothing calls — hence no `hutch.config.ts`. ## Release builds `bun scripts/release.ts [--target mac-arm64|win-x64|linux-x64]` (default: host). Steps: gen-version → electrobun prepare → gen-icons → Angular build → empty `artifacts/` → `electrobun build --env=stable` → copy/rename the installers into `release//miniSCP-Build-.*`. Facts the script encodes: - **`--env=stable` is mandatory for releases** — `electrobun build` defaults to the dev channel (different app name `miniSCP-dev`, no distributable artifacts). - **No cross-compile, and no Intel Mac.** Hutch builds for its host OS *and* arch only; packaging needs host tools (codesign/hdiutil on macOS, rcedit/PowerShell on Windows). Hence one script run per artifact, on the matching machine; `win-x64` on a non-Windows host and `linux-x64` on a non-Linux host both error out. **Electrobun 2.x publishes no macOS x64 core (nor an x64 Hutch)**, so mac-x64 was dropped — the v1 trick of running the x64 CLI under Rosetta has nothing left to run. `--target mac-x64`, and any build on an Intel Mac, fail with that message. - **Runtime downloads**: on first use per Electrobun version Hutch fetches the platform core from `github.com/blackboardsh/electrobun/releases` (SHA-256-checked against `electrobun-artifacts.json`), plus Cottontail and the pinned Bun, into `~/.hutch` — network is a build-time requirement on every fresh machine/runner. - **`artifacts/` is emptied before the build** — `site/deploy.sh` publishes it verbatim, so it must hold exactly this build (stale files, e.g. v1-named installers, would otherwise ship). - **Artifact shapes** (stable channel, in `artifacts/`): 2.x installers carry **no channel prefix**; only the updater feed keeps `stable---` (the names v1.18.1+ clients request). macOS → `macos-arm64-miniSCP.dmg` + `stable-macos-arm64-miniSCP.app.tar.zst` + `stable-macos-arm64-update.json` (verified); Windows → `win-x64-miniSCP-Setup.zip` (setup exe + hidden payload) + the `stable-win-x64-*` feed, plus the script's own **portable zip** of `build/stable-win-x64/miniSCP` (warns instead of failing if that folder is missing); Linux → `linux-x64-miniSCP-Setup.tar.gz` (a self-extracting `installer` + README) + the `stable-linux-x64-*` feed. The official Linux installer IS the release artifact — the v1 portable tarball and its `miniSCP.sh` NVIDIA wrapper are gone, so an installed build on NVIDIA needs `WEBKIT_DISABLE_DMABUF_RENDERER=1` from the user (see gotchas.md). The Windows/Linux names come from the 2.x docs — confirm on the first real 2.x build there. A build that can reach the previous release at `release.baseUrl` also emits `stable---.patch`. - **Linux builds need the AppIndicator dev libraries present to RUN** (not to build) — see platform-notes.md; an installed build 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. - **v1 → 2.x bridge**: Windows users on 0.1.78 (the only feed live on miniscp.com/dl) update through their v1 updater because `app.name`, `app.identifier` and `release.baseUrl` are unchanged, the build is `--env=stable`, and `artifacts/` is published unrenamed. 2.x keeps the v1 top-level `update.json` fields (`version`/`hash`/`platform`/`arch`) and the physical `stable` state root, so user data stays put. Unverified until the first 2.x Windows release reaches a v1 install. ## CI (Gitea Actions) `.gitea/workflows/release.yml` — tag-triggered (`build-*`), macos (mac-arm64) + windows (win-x64) 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); `bun install --frozen-lockfile` already prepares the devkit (postinstall). 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/macos-arm64-miniSCP.dmg`, `/dl/win-x64-miniSCP-Setup.zip`, the portable zip) so the page never changes per release. 2.x dropped the `stable-` prefix from installer names (the feed kept it), so the v1 `stable-…-Setup.zip`/`.dmg` installers still on the server are stale — delete them by hand. Logo/screenshot `` slots are commented in, images live in `site/assets/`. - `site/Caddyfile` — one flat `/dl/*` folder with `file_server browse` + range requests, and per-type `Cache-Control`: `*.patch` immutable (unique hashed names), `*update.json` `no-cache` (updater must see new versions), installers (`*.dmg *.zip *.tar.zst *.tar.gz`) `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 updater `baseUrl`. Publishing = drop this machine's whole `artifacts/` into `/dl/`, unrenamed; installers overwrite in place, `.patch` files 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 after `scripts/release.ts`; copies `artifacts/*` verbatim and renames the build-numbered win portable zip to the stable `stable-win-x64-miniSCP-portable.zip`. `MINISCP_DL_DIR` overrides the target (stage locally, then rsync to the server). - `release.baseUrl = https://miniscp.com/dl` is set in `electrobun.config.ts`; `miniscp.com` is fronted by an external proxy for TLS (Caddy itself stays on `:80` behind it). The value is baked into `version.json` at build time, so only builds made after it was added check for updates. With `generatePatch` (default true), release builds need **build-time network to `miniscp.com/dl`** so Hutch can fetch the currently-published version to diff a `.patch` (a target with no published feed yet — currently mac and Linux — 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](architecture.md) for the runtime flow. - **Backend** ([src/bun/index.ts](../src/bun/index.ts)): `Updater.checkForUpdate()` runs ~5s after launch and every 6h; on `updateAvailable` it pushes `rpc.send.updateStatus({phase:"available",…})`. `Updater.onStatusChange` streams `download-progress` (0–100) → `updateStatus{downloading}`. `checkForUpdate` self-guards the `dev` channel, so `bun run dev` never shows the pill. - **RPC** ([src/shared/rpc.ts](../src/shared/rpc.ts)): requests `checkForUpdate` / `downloadUpdate` / `applyUpdate` (handlers in [rpc-handlers.ts](../src/bun/rpc-handlers.ts) call the electrobun `Updater`; `applyUpdate` goes through `events.applyUpdate` in index.ts); push message `updateStatus`. - **`applyUpdate()` asks the before-quit guard first (2.x).** With transfers running or dirty editors the guard vetoes and shows its Quit Anyway / Cancel box. `events.applyUpdate` marks the quit as update-initiated, so Quit Anyway re-runs `Updater.applyUpdate()` (a plain `Utils.quit()` would drop the downloaded update); Cancel leaves the pill at "Restart to apply". - **UI**: `UpdaterService` ([ui/src/app/core/updater.service.ts](../ui/src/app/core/updater.service.ts)) holds signals (`state`/`version`/`progress`); the tab-bar "update pill" ([tab-bar.ts](../ui/src/app/skins/shadlike/tab-bar/tab-bar.ts)) is one accent (`bg-primary`) control that announces → fills as a progress bar while downloading → "Restart to apply". `state` is `available → downloading → ready` (ready derived from the `downloadUpdate` response). App-menu "Check for Updates…" ([menu.ts](../src/bun/menu.ts)) routes a `check-for-updates` menu action into `UpdaterService.check()`. - **Unsigned was fine on 1.18.1:** `applyUpdate()` stripped the macOS quarantine xattr before relaunch. 2.x hands the swap to a native post-exit helper (transactional replace, restores the old app on failure) — re-check Gatekeeper on the first unsigned 2.x mac update. ## 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.mainProcess: "bun"` is load-bearing** — 2.x defaults to Cottontail, which ignores the whole `build.bun` block (verified: without the line, `bunx electrobun config --env=stable` resolves `mainProcess: "cottontail"` with no externals); `build.bun.external` must keep `["cpu-features", "*.node"]` (ssh2's optional native accelerators — bundling them breaks the build; runtime falls back to pure JS; a 2.x stable bundle keeps both as runtime `require`s); `build.copy` ships the Angular dist and tray assets into `views://`; `bundleCEF: false` everywhere (system WebKit / WebView2). `bunx electrobun config --env=stable` prints the merged config Hutch will build with.