From 910e147fb346cc69abbd181645e33adf45cc503e Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Sat, 26 Sep 2026 13:57:09 +0200 Subject: [PATCH] fix(local): fail pnpm dist on a missing toolchain and show the install command pnpm dist staged audio.cpp and sd.cpp leniently, so a machine without CMake or GCC 13 built an installer with no local audio and only a warning in the scrollback. It now passes --strict, as release CI does, and stages the native runtimes first so a missing tool stops it in about a second. The failure names every missing tool at once and prints the command that installs them with the machine's package manager, without a stack trace. predev stays lenient. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build-audiocpp.yml | 1 + .github/workflows/build-sdcpp.yml | 1 + docs/architecture/packaging.md | 10 +-- surfsense_local/electron/package.json | 2 +- .../electron/scripts/audiocpp/compile.mjs | 16 +++-- .../electron/scripts/audiocpp/stage.mjs | 23 ++++--- .../scripts/not-staged/install-commands.mjs | 18 +++++ .../electron/scripts/not-staged/message.mjs | 20 ++++++ .../scripts/not-staged/message.test.mjs | 69 +++++++++++++++++++ .../scripts/not-staged/package-manager.mjs | 22 ++++++ .../electron/scripts/not-staged/tools.mjs | 48 +++++++++++++ .../electron/scripts/sdcpp/compile.mjs | 15 ++-- .../electron/scripts/sdcpp/stage.mjs | 33 ++++++--- 13 files changed, 241 insertions(+), 37 deletions(-) create mode 100644 surfsense_local/electron/scripts/not-staged/install-commands.mjs create mode 100644 surfsense_local/electron/scripts/not-staged/message.mjs create mode 100644 surfsense_local/electron/scripts/not-staged/message.test.mjs create mode 100644 surfsense_local/electron/scripts/not-staged/package-manager.mjs create mode 100644 surfsense_local/electron/scripts/not-staged/tools.mjs diff --git a/.github/workflows/build-audiocpp.yml b/.github/workflows/build-audiocpp.yml index 78e3c643b8..d3990ead41 100644 --- a/.github/workflows/build-audiocpp.yml +++ b/.github/workflows/build-audiocpp.yml @@ -14,6 +14,7 @@ on: pull_request: paths: - surfsense_local/electron/scripts/audiocpp/** + - surfsense_local/electron/scripts/not-staged/** - surfsense_local/electron/scripts/msvc-runtime.mjs - surfsense_local/electron/scripts/pinned-download.mjs - surfsense_local/backend/modules/llm/providers/audiocpp/** diff --git a/.github/workflows/build-sdcpp.yml b/.github/workflows/build-sdcpp.yml index 8fb266b34b..8c32a92d1f 100644 --- a/.github/workflows/build-sdcpp.yml +++ b/.github/workflows/build-sdcpp.yml @@ -14,6 +14,7 @@ on: pull_request: paths: - surfsense_local/electron/scripts/sdcpp/** + - surfsense_local/electron/scripts/not-staged/** - .github/workflows/build-sdcpp.yml permissions: diff --git a/docs/architecture/packaging.md b/docs/architecture/packaging.md index 19dd3d1b0d..33219a53e8 100644 --- a/docs/architecture/packaging.md +++ b/docs/architecture/packaging.md @@ -64,19 +64,19 @@ The release workflow runs the three scripts directly. Without the parser pack, D - `scripts/sdcpp/stage.mjs`, which `build:sdcpp` runs, stages stable-diffusion.cpp's `sd-server` into `electron/sdcpp/` with the libraries it loads from beside itself and its licences, leaving `sd-cli` behind. It runs `sd-server --help` from the staged folder before it swaps the folder into place. - Windows downloads upstream's Vulkan archive, checked against its pinned SHA-256, and copies the MSVC and OpenMP runtimes beside it, which the archive needs and does not carry. - Linux and macOS compile the pinned commit, because upstream's Linux archives need glibc 2.38 and its macOS archive needs macOS 26.0. The recipe, `scripts/sdcpp/recipe.mjs`, builds Vulkan on Linux, with one ggml library per micro-architecture, and Metal on macOS, for 13.3. - - Compiling needs CMake, a C++ compiler and the Vulkan SDK (`glslc` and headers) on Linux, or Xcode's command line tools on macOS; Windows needs Visual Studio 2022 or newer with the C++ tools, for the runtime it ships. Without them the script stages an empty folder and says why, and the app runs without local images. Release CI passes `--strict`, which fails instead. + - Compiling needs CMake, a C++ compiler and the Vulkan SDK (`glslc` and headers) on Linux, or Xcode's command line tools on macOS; Windows needs Visual Studio 2022 or newer with the C++ tools, for the runtime it ships. Without them the script stages an empty folder and says why, and the app runs without local images. Release CI and `pnpm dist` pass `--strict`, which fails instead, naming everything missing and the command that installs it with the machine's package manager. - `scripts/audiocpp/stage.mjs`, which `build:audiocpp` runs, stages audio.cpp's `audiocpp_server` into `electron/audiocpp/` with the model specs of the three curated families. It adds eSpeak-ng 1.52.0 from the pinned `espeakng-loader` wheel, which Kokoro and Kitten phonemise through (Kokoro finds it through the server's environment, Kitten through `server.json`, [`local-models/catalog.md`](local-models/catalog.md)), and eSpeak-ng's GPL licence text, which the wheel does not carry. It runs `--list-devices` from the staged folder before it swaps the folder into place. - macOS downloads upstream's archive, checked against its pinned SHA-256. - Windows and Linux compile the pinned commit, because upstream's Linux archives need glibc 2.38 and its Windows archive compiles AVX-512 into the executable. The recipe, `scripts/audiocpp/recipe.mjs`, builds only the CPU backend, since the server runs with `--backend cpu`, with one ggml library per micro-architecture and only the curated model families. - - Compiling needs CMake and GCC 13 or newer on Linux, or Visual Studio 2022 or newer with the C++ tools on Windows. Without them the script stages an empty folder and says why, and the app runs without local audio. Release CI passes `--strict`, which fails instead. + - Compiling needs CMake and GCC 13 or newer on Linux, or Visual Studio 2022 or newer with the C++ tools on Windows. Without them the script stages an empty folder and says why, and the app runs without local audio. Release CI and `pnpm dist` pass `--strict`, which fails instead, naming everything missing and the command that installs it with the machine's package manager. -`pnpm dist` in `electron/` runs every staging step, every native runtime included, before `electron-builder`. +`pnpm dist` in `electron/` runs every staging step before `electron-builder`, the native runtimes first, so a missing toolchain stops it before the frontend and the Python binaries are built. It stages sd.cpp and audio.cpp with `--strict`, so an installer never ships without them; `predev` does not, so dev runs without a toolchain. `frontend/` and `electron/` each pin their pnpm in `packageManager` (`pnpm@11.27.1`), which pnpm switches to locally and the desktop workflows read through `package_json_file`. The repository root's `packageManager` is the web app's, `pnpm@10.26.0`, and does not apply here. ## Building audio.cpp -[`build-audiocpp.yml`](../../.github/workflows/build-audiocpp.yml) compiles the Windows and Linux builds with `stage.mjs --strict`, checks them, and hands each staged folder on as an artifact. `release-local.yml` calls it, and its packaging jobs unpack the artifact before `stage.mjs`, which then finds the server already staged. A pull request that changes `scripts/audiocpp/`, the audio.cpp adapter or engine, the manifest or the voicing test runs it too. It caches the staged folder by the scripts' contents, so a release that does not change them reuses the last checked build, and voices with it again. +[`build-audiocpp.yml`](../../.github/workflows/build-audiocpp.yml) compiles the Windows and Linux builds with `stage.mjs --strict`, checks them, and hands each staged folder on as an artifact. `release-local.yml` calls it, and its packaging jobs unpack the artifact before `stage.mjs`, which then finds the server already staged. A pull request that changes `scripts/audiocpp/`, `scripts/not-staged/`, the audio.cpp adapter or engine, the manifest or the voicing test runs it too. It caches the staged folder by the scripts' contents, so a release that does not change them reuses the last checked build, and voices with it again. Why it compiles rather than downloads, in plain words: upstream's ready-made programs for Linux and Windows do not run on many of the computers the app supports, for a different reason on each. @@ -97,7 +97,7 @@ It is a stopgap. Once upstream publishes archives that meet both floors, the app ## Building sd.cpp -[`build-sdcpp.yml`](../../.github/workflows/build-sdcpp.yml) compiles the Linux and macOS builds with `stage.mjs --strict`, checks each against the app's floors, and hands each staged folder on as an artifact, as `build-audiocpp.yml` does. A pull request that changes `scripts/sdcpp/` runs it too, so a compile that a pin bump or a new runner image breaks fails in review rather than in a release. It caches the staged folder by the scripts' contents. Windows is a pinned download, so the release job stages it itself, as it does llama.cpp. +[`build-sdcpp.yml`](../../.github/workflows/build-sdcpp.yml) compiles the Linux and macOS builds with `stage.mjs --strict`, checks each against the app's floors, and hands each staged folder on as an artifact, as `build-audiocpp.yml` does. A pull request that changes `scripts/sdcpp/` or `scripts/not-staged/` runs it too, so a compile that a pin bump or a new runner image breaks fails in review rather than in a release. It caches the staged folder by the scripts' contents. Windows is a pinned download, so the release job stages it itself, as it does llama.cpp. Why it compiles on Linux and macOS, in plain words: upstream's ready-made programs start only on the newest systems there, Ubuntu 24.04 (glibc 2.38) and macOS 26.0, while the app runs on Ubuntu 22.04, RHEL 9 and macOS 13.3, the floor its llama.cpp and audio.cpp builds already set. Upstream's Windows program runs on any x64 processor, since its AVX-512 code sits only in the per-processor ggml libraries ggml picks at start, so Windows takes it as built. diff --git a/surfsense_local/electron/package.json b/surfsense_local/electron/package.json index e8f4e6f697..4a48ea43ce 100644 --- a/surfsense_local/electron/package.json +++ b/surfsense_local/electron/package.json @@ -24,7 +24,7 @@ "build:llamacpp": "node scripts/fetch-llamacpp.mjs", "build:sdcpp": "node scripts/sdcpp/stage.mjs", "build:audiocpp": "node scripts/audiocpp/stage.mjs", - "dist": "pnpm build:frontend && pnpm build:binaries && pnpm build:model && pnpm build:voice && pnpm build:parser && pnpm build:llamacpp && pnpm build:sdcpp && pnpm build:audiocpp && pnpm build && electron-builder", + "dist": "pnpm build:llamacpp && pnpm build:sdcpp --strict && pnpm build:audiocpp --strict && pnpm build:frontend && pnpm build:binaries && pnpm build:model && pnpm build:voice && pnpm build:parser && pnpm build && electron-builder", "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test src/main/*.test.ts src/main/**/*.test.ts scripts/**/*.test.mjs", "typecheck": "tsc --noEmit -p tsconfig.json", "check:sidecars": "node scripts/check-sidecars.mjs" diff --git a/surfsense_local/electron/scripts/audiocpp/compile.mjs b/surfsense_local/electron/scripts/audiocpp/compile.mjs index 3f5b857904..4547bbaa8e 100644 --- a/surfsense_local/electron/scripts/audiocpp/compile.mjs +++ b/surfsense_local/electron/scripts/audiocpp/compile.mjs @@ -10,6 +10,7 @@ import { availableParallelism } from "node:os" import { join } from "node:path" import { visualStudio } from "../msvc-runtime.mjs" +import { CMAKE, GCC_13, VISUAL_STUDIO } from "../not-staged/tools.mjs" import { COMMIT, SOURCE, TAG } from "./pins.mjs" import { configureArgs } from "./recipe.mjs" @@ -21,17 +22,20 @@ function output(cmd, args) { } } -/** What this machine lacks to compile it, or null. audio.cpp requires GCC 13. */ +/** Everything this machine lacks to compile it, empty if nothing. audio.cpp requires GCC 13. */ export function missingToolchain() { - if (output("cmake", ["--version"]) == null) return "CMake" + const missing = [] + if (output("cmake", ["--version"]) == null) missing.push(CMAKE) if (process.platform === "win32") { - return visualStudio() ? null : "Visual Studio 2022 or newer with the C++ tools" + if (!visualStudio()) missing.push(VISUAL_STUDIO) + return missing } const cxx = process.env.CXX ?? "g++" const version = output(cxx, ["-dumpversion"]) - if (version == null) return "GCC 13 or newer" - if (Number(version.split(".")[0]) < 13) return `GCC 13 or newer (${cxx} is ${version})` - return null + if (version == null) missing.push(GCC_13) + // No package fixes an old one: the distribution's g++ is already installed. + else if (Number(version.split(".")[0]) < 13) missing.push({ name: `${GCC_13.name} (${cxx} is ${version})` }) + return missing } /** Build the server in `work`; returns where its files and its source are. */ diff --git a/surfsense_local/electron/scripts/audiocpp/stage.mjs b/surfsense_local/electron/scripts/audiocpp/stage.mjs index 53adca822f..40090d7b54 100644 --- a/surfsense_local/electron/scripts/audiocpp/stage.mjs +++ b/surfsense_local/electron/scripts/audiocpp/stage.mjs @@ -2,7 +2,7 @@ // electron/audiocpp, swapped in whole once the server lists its devices. // macOS downloads upstream's archive; Windows and Linux compile the pinned // source. Without a toolchain the app runs without local audio, unless -// --strict, which release CI passes. +// --strict, which release CI and `pnpm dist` pass. import { execFileSync } from "node:child_process" import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync } from "node:fs" import { tmpdir } from "node:os" @@ -12,6 +12,8 @@ import { fileURLToPath } from "node:url" import { compile, missingToolchain } from "./compile.mjs" import { stageEspeak } from "./espeak.mjs" import { copyMsvcRuntime } from "../msvc-runtime.mjs" +import { NotStaged, notStaged } from "../not-staged/message.mjs" +import { packageManager } from "../not-staged/package-manager.mjs" import { TAG } from "./pins.mjs" import { copyServerFiles, SERVER } from "./server-files.mjs" import { unpackUpstream } from "./upstream-archive.mjs" @@ -48,16 +50,21 @@ async function main() { } const host = `${process.platform}-${process.arch}` - const missing = !HOSTS.includes(host) - ? `a supported host, not ${host}` + const reason = !HOSTS.includes(host) + ? `audio.cpp is not staged: it has no build for ${host}.` : process.platform === "darwin" ? null - : missingToolchain() - if (missing) { - if (strict) throw new Error(`staging audio.cpp needs ${missing}`) + : notStaged( + "audio.cpp", + `on ${process.platform === "win32" ? "Windows" : "Linux"} it is compiled from source`, + missingToolchain(), + packageManager() + ) + if (reason) { + if (strict) throw new NotStaged(reason) // Empty, so the packager still finds the folder; the sidecar never starts. mkdirSync(OUT, { recursive: true }) - console.warn(`audio.cpp not staged: it needs ${missing}. Local audio is unavailable.`) + console.warn(`${reason}\n\nThe app runs without local audio.`) return } @@ -92,6 +99,6 @@ async function main() { } main().catch((error) => { - console.error(error) + console.error(error instanceof NotStaged ? error.message : error) process.exitCode = 1 }) diff --git a/surfsense_local/electron/scripts/not-staged/install-commands.mjs b/surfsense_local/electron/scripts/not-staged/install-commands.mjs new file mode 100644 index 0000000000..bed8115083 --- /dev/null +++ b/surfsense_local/electron/scripts/not-staged/install-commands.mjs @@ -0,0 +1,18 @@ +// The commands that install what a stage lacks, with this machine's package +// manager. A tool with no package there is named in the message, not here. + +const INSTALL = { + apt: (packages) => [`sudo apt install ${packages.join(" ")}`], + dnf: (packages) => [`sudo dnf install ${packages.join(" ")}`], + pacman: (packages) => [`sudo pacman -S --needed ${packages.join(" ")}`], + brew: (packages) => [`brew install ${packages.join(" ")}`], + // winget takes one package per call. + winget: (packages) => packages.map((id) => `winget install ${id}`), +} + +export function installCommands(missing, manager) { + const commands = missing.flatMap((tool) => tool.command ?? []) + const packages = [...new Set(missing.flatMap((tool) => tool.packages?.[manager] ?? []))] + if (packages.length) commands.push(...INSTALL[manager](packages)) + return commands +} diff --git a/surfsense_local/electron/scripts/not-staged/message.mjs b/surfsense_local/electron/scripts/not-staged/message.mjs new file mode 100644 index 0000000000..d2ded1d64c --- /dev/null +++ b/surfsense_local/electron/scripts/not-staged/message.mjs @@ -0,0 +1,20 @@ +// Why a native runtime was not staged on this machine. It is setup to fix, +// not a fault in the script, so it is printed alone, without a stack trace. + +import { installCommands } from "./install-commands.mjs" + +/** Thrown under --strict; a stage script prints its message and exits 1. */ +export class NotStaged extends Error {} + +/** Every missing tool and the command that installs them all; null if none is. */ +export function notStaged(runtime, how, missing, manager) { + if (missing.length === 0) return null + const names = missing.map((tool) => tool.name) + const lacks = + names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0] + const found = `${runtime} is not staged: ${how}, and this machine lacks ${lacks}.` + const commands = installCommands(missing, manager) + return commands.length + ? `${found} Install what is missing, then run the build again:\n\n${commands.map((command) => ` ${command}`).join("\n")}` + : `${found} Install what is missing and run the build again.` +} diff --git a/surfsense_local/electron/scripts/not-staged/message.test.mjs b/surfsense_local/electron/scripts/not-staged/message.test.mjs new file mode 100644 index 0000000000..5adab2b040 --- /dev/null +++ b/surfsense_local/electron/scripts/not-staged/message.test.mjs @@ -0,0 +1,69 @@ +import assert from "node:assert/strict" +import test from "node:test" + +import { notStaged } from "./message.mjs" +import { CMAKE, CXX_COMPILER, GCC_13, VISUAL_STUDIO, VULKAN_SDK, XCODE_TOOLS } from "./tools.mjs" + +const HOW = "on Linux it is compiled from source" + +test("says nothing when nothing is missing", () => { + assert.equal(notStaged("audio.cpp", HOW, [], "apt"), null) +}) + +test("names the one tool missing and what to do", () => { + assert.equal( + notStaged("audio.cpp", HOW, [CMAKE], null), + "audio.cpp is not staged: on Linux it is compiled from source, and this machine lacks CMake. Install what is missing and run the build again." + ) +}) + +test("names every missing tool at once, so one install fixes the build", () => { + assert.equal( + notStaged("audio.cpp", HOW, [CMAKE, GCC_13], null), + "audio.cpp is not staged: on Linux it is compiled from source, and this machine lacks CMake and GCC 13 or newer. Install what is missing and run the build again." + ) + assert.equal( + notStaged("sd-server", HOW, [CMAKE, CXX_COMPILER, VULKAN_SDK], null), + "sd-server is not staged: on Linux it is compiled from source, and this machine lacks CMake, a C++ compiler and the Vulkan SDK (glslc and headers). Install what is missing and run the build again." + ) +}) + +test("shows the one command that installs everything missing", () => { + assert.equal( + notStaged("audio.cpp", HOW, [CMAKE, GCC_13], "apt"), + `audio.cpp is not staged: on Linux it is compiled from source, and this machine lacks CMake and GCC 13 or newer. Install what is missing, then run the build again: + + sudo apt install cmake g++` + ) +}) + +test("on Windows, one winget call per package", () => { + assert.equal( + notStaged("audio.cpp", "on Windows it is compiled from source", [CMAKE, VISUAL_STUDIO], "winget"), + `audio.cpp is not staged: on Windows it is compiled from source, and this machine lacks CMake and Visual Studio 2022 or newer with the C++ tools. Install what is missing, then run the build again: + + winget install Kitware.CMake + winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"` + ) +}) + +test("on macOS, Xcode's tools come from xcode-select, not Homebrew", () => { + assert.equal( + notStaged("sd-server", "on macOS it is compiled from source", [CMAKE, XCODE_TOOLS], "brew"), + `sd-server is not staged: on macOS it is compiled from source, and this machine lacks CMake and Xcode's command line tools. Install what is missing, then run the build again: + + xcode-select --install + brew install cmake` + ) +}) + +test("a tool no package fixes is named but left out of the command", () => { + // The distribution's g++ is already installed, and too old. + const oldGcc = { name: "GCC 13 or newer (g++ is 11.4.0)" } + assert.equal( + notStaged("audio.cpp", HOW, [CMAKE, oldGcc], "apt"), + `audio.cpp is not staged: on Linux it is compiled from source, and this machine lacks CMake and GCC 13 or newer (g++ is 11.4.0). Install what is missing, then run the build again: + + sudo apt install cmake` + ) +}) diff --git a/surfsense_local/electron/scripts/not-staged/package-manager.mjs b/surfsense_local/electron/scripts/not-staged/package-manager.mjs new file mode 100644 index 0000000000..cf098c5a97 --- /dev/null +++ b/surfsense_local/electron/scripts/not-staged/package-manager.mjs @@ -0,0 +1,22 @@ +// The package manager that installs build tools here: the platform's, or on +// Linux the distribution family's, read from os-release. null where unknown. +import { readFileSync } from "node:fs" + +const LINUX = [ + ["apt", ["debian", "ubuntu"]], + ["dnf", ["fedora", "rhel"]], + ["pacman", ["arch"]], +] + +export function packageManager() { + if (process.platform === "darwin") return "brew" + if (process.platform === "win32") return "winget" + let release + try { + release = readFileSync("/etc/os-release", "utf8") + } catch { + return null + } + const ids = [...release.matchAll(/^ID(?:_LIKE)?="?([^"\n]*)"?$/gm)].flatMap((m) => m[1].split(" ")) + return LINUX.find(([, family]) => family.some((id) => ids.includes(id)))?.[0] ?? null +} diff --git a/surfsense_local/electron/scripts/not-staged/tools.mjs b/surfsense_local/electron/scripts/not-staged/tools.mjs new file mode 100644 index 0000000000..470f794867 --- /dev/null +++ b/surfsense_local/electron/scripts/not-staged/tools.mjs @@ -0,0 +1,48 @@ +// What a stage can lack, and the packages that provide it per package manager. +// Vulkan's are llama.cpp's (docs/build.md): its loader does not always pull in +// SPIR-V's headers. GCC 13 is each distribution's default g++ only from +// Ubuntu 24.04 and Fedora 39. + +export const CMAKE = { + name: "CMake", + packages: { + apt: ["cmake"], + dnf: ["cmake"], + pacman: ["cmake"], + brew: ["cmake"], + winget: ["Kitware.CMake"], + }, +} + +export const GCC_13 = { + name: "GCC 13 or newer", + packages: { apt: ["g++"], dnf: ["gcc-c++"], pacman: ["gcc"] }, +} + +export const CXX_COMPILER = { + name: "a C++ compiler", + packages: { apt: ["g++"], dnf: ["gcc-c++"], pacman: ["gcc"] }, +} + +export const VULKAN_SDK = { + name: "the Vulkan SDK (glslc and headers)", + packages: { + apt: ["glslc", "libvulkan-dev", "spirv-headers"], + dnf: ["glslc", "vulkan-loader-devel", "spirv-headers-devel"], + pacman: ["shaderc", "vulkan-icd-loader", "vulkan-headers", "spirv-headers"], + }, +} + +export const XCODE_TOOLS = { + name: "Xcode's command line tools", + command: "xcode-select --install", +} + +export const VISUAL_STUDIO = { + name: "Visual Studio 2022 or newer with the C++ tools", + packages: { + winget: [ + 'Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"', + ], + }, +} diff --git a/surfsense_local/electron/scripts/sdcpp/compile.mjs b/surfsense_local/electron/scripts/sdcpp/compile.mjs index 5485454745..7c8b030133 100644 --- a/surfsense_local/electron/scripts/sdcpp/compile.mjs +++ b/surfsense_local/electron/scripts/sdcpp/compile.mjs @@ -9,6 +9,7 @@ import { copyFileSync } from "node:fs" import { availableParallelism } from "node:os" import { join } from "node:path" +import { CMAKE, CXX_COMPILER, VULKAN_SDK, XCODE_TOOLS } from "../not-staged/tools.mjs" import { COMMIT, SOURCE, SUBMODULES, TAG } from "./pins.mjs" import { configureArgs } from "./recipe.mjs" @@ -20,16 +21,18 @@ function output(cmd, args) { } } -/** What this machine lacks to compile it, or null. */ +/** Everything this machine lacks to compile it, empty if nothing. */ export function missingToolchain() { - if (output("cmake", ["--version"]) == null) return "CMake" + const missing = [] + if (output("cmake", ["--version"]) == null) missing.push(CMAKE) if (process.platform === "darwin") { - return output("xcrun", ["--find", "clang"]) == null ? "Xcode's command line tools" : null + if (output("xcrun", ["--find", "clang"]) == null) missing.push(XCODE_TOOLS) + return missing } - if (output(process.env.CXX ?? "g++", ["--version"]) == null) return "a C++ compiler" + if (output(process.env.CXX ?? "g++", ["--version"]) == null) missing.push(CXX_COMPILER) // ggml's Vulkan shaders are compiled at build time. - if (output("glslc", ["--version"]) == null) return "the Vulkan SDK (glslc and headers)" - return null + if (output("glslc", ["--version"]) == null) missing.push(VULKAN_SDK) + return missing } /** Build every target in `work`; returns the folder holding the server. */ diff --git a/surfsense_local/electron/scripts/sdcpp/stage.mjs b/surfsense_local/electron/scripts/sdcpp/stage.mjs index 4a7146ca71..e5b864cabe 100644 --- a/surfsense_local/electron/scripts/sdcpp/stage.mjs +++ b/surfsense_local/electron/scripts/sdcpp/stage.mjs @@ -2,7 +2,7 @@ // electron/sdcpp, swapped in whole once it starts from its own folder. // Windows downloads upstream's archive; Linux and macOS compile the pinned // source. Without a toolchain the app runs without local images, unless -// --strict, which release CI passes. +// --strict, which release CI and `pnpm dist` pass. import { execFileSync } from "node:child_process" import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync } from "node:fs" import { tmpdir } from "node:os" @@ -10,6 +10,9 @@ import { join } from "node:path" import { fileURLToPath } from "node:url" import { copyMsvcRuntime, visualStudio } from "../msvc-runtime.mjs" +import { NotStaged, notStaged } from "../not-staged/message.mjs" +import { packageManager } from "../not-staged/package-manager.mjs" +import { VISUAL_STUDIO } from "../not-staged/tools.mjs" import { compile, missingToolchain } from "./compile.mjs" import { TAG } from "./pins.mjs" import { copyServerFiles, SERVER } from "./server-files.mjs" @@ -44,18 +47,26 @@ async function main() { } const host = `${process.platform}-${process.arch}` - const missing = !HOSTS.includes(host) - ? `a supported host, not ${host}` + const reason = !HOSTS.includes(host) + ? `sd-server is not staged: it has no build for ${host}.` : process.platform === "win32" - ? visualStudio() - ? null - : "Visual Studio 2022 or newer with the C++ tools, for the runtime it ships" - : missingToolchain() - if (missing) { - if (strict) throw new Error(`staging sd-server needs ${missing}`) + ? notStaged( + "sd-server", + "on Windows it ships Visual Studio's C++ runtime", + visualStudio() ? [] : [VISUAL_STUDIO], + packageManager() + ) + : notStaged( + "sd-server", + `on ${process.platform === "darwin" ? "macOS" : "Linux"} it is compiled from source`, + missingToolchain(), + packageManager() + ) + if (reason) { + if (strict) throw new NotStaged(reason) // Empty, so the packager still finds the folder; the sidecar never starts. mkdirSync(OUT, { recursive: true }) - console.warn(`sd-server not staged: it needs ${missing}. Local images are unavailable.`) + console.warn(`${reason}\n\nThe app runs without local images.`) return } @@ -90,6 +101,6 @@ async function main() { } main().catch((error) => { - console.error(error) + console.error(error instanceof NotStaged ? error.message : error) process.exitCode = 1 })