From 57ed02eecb083230b757f82508a8a034a32e14d3 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 23 Sep 2026 16:21:36 -0700 Subject: [PATCH] Build the Linux and Windows desktop packages in a container from a Mac, with DOCKER choosing the container runtime --- README.md | 12 +++++++----- scripts/package-desktop.mjs | 17 +++++++++++------ scripts/release.mjs | 22 +++++++++++++++------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6633dcf8..271ddf3f 100644 --- a/README.md +++ b/README.md @@ -142,9 +142,10 @@ Wayland docks can identify it. It leaves existing user and system launchers alon entry from a development run, and updates the entry when an update renames the AppImage. Windows uses the executable's icon resources. -Linux packaging requires Linux. Windows packaging from Linux uses the pinned official +Linux packaging from macOS and Windows packaging from Linux or macOS use the pinned official `electronuserland/builder` Wine image through Docker, mounting only a temporary copy of the prepared -Electron project. Native addons need a target-OS ABI rebuild and cannot use this cross-build path. +Electron project. Set `DOCKER=podman` in `.env.local` to use Podman instead. Native addons need a +target-OS ABI rebuild and cannot use this cross-build path. Native Windows preparation needs Bash on PATH, for example Git Bash. DMG creation requires macOS. On Linux, `pnpm run package:desktop:macos --dir` prepares unsigned bundles for inspection only, and verifies nothing about the macOS runtime, Gatekeeper, or signing. @@ -206,12 +207,13 @@ run and prints the command to pick up from there. | `fdroid` | reruns F-Droid's own preparation and build against the tag in a throwaway worktree | | `play` | `bundleRelease` signed with the upload key, uploaded to a Play track as a draft | | `ios` | `cap build ios` to an archive and IPA, uploaded with `altool` | -| `desktop` | `package:desktop:*` for this OS: Linux and Windows from Linux, signed and notarized macOS from a Mac | +| `desktop` | `package:desktop:*` for this OS: Linux and Windows from Linux, all three from a Mac, with macOS signed and notarized | | `gitea` | creates a draft release from the changelog, attaches the APK, desktop packages and update manifests, and publishes it once every platform is there | | `zapstore` | `zsp publish zapstore.yaml` | -Linux builds the Linux and Windows packages and a Mac builds the macOS ones, so a release takes a -run on each, both ending in `gitea`. Gitea's latest release is the desktop update feed, so the release stays a +A Mac builds every desktop package, the Linux and Windows ones in a container. Linux can't build +the macOS ones, so a release run from Linux needs a `pnpm release desktop gitea` on a Mac as well. +Gitea's latest release is the desktop update feed, so the release stays a draft, hidden from updaters and Obtainium, until it has the APK and all three `latest*.yml` manifests. Each manifest is uploaded after the files it lists. A mobile-only release can't be published, so package the desktop apps for every release. diff --git a/scripts/package-desktop.mjs b/scripts/package-desktop.mjs index c05ec23c..030aa5ac 100644 --- a/scripts/package-desktop.mjs +++ b/scripts/package-desktop.mjs @@ -8,7 +8,11 @@ import {loadEnv} from "vite" const root = fileURLToPath(new URL("../", import.meta.url)) const [target, option, ...rest] = process.argv.slice(2) const platforms = {linux: "--linux", windows: "--win", macos: "--mac"} -const env = {...process.env, ...loadEnv("production", root, "VITE_"), NODE_ENV: "production"} +const env = { + ...process.env, + ...loadEnv("production", root, ["VITE_", "DOCKER"]), + NODE_ENV: "production", +} const run = (command, args, options = {}) => new Promise((resolve, reject) => { @@ -70,12 +74,13 @@ try { // --dir replaces configured targets, including their architectures. args.push("--x64", "--arm64") } - if (target === "windows" && process.platform === "linux") { + if ( + (target === "windows" && process.platform !== "win32") || + (target === "linux" && process.platform !== "linux") + ) { const files = await readdir(join(root, "electron/vendor"), {recursive: true}) if (files.some(file => file.endsWith(".node"))) { - throw new Error( - "Native Electron addons require a Windows ABI rebuild before packaging on Windows", - ) + throw new Error(`Native Electron addons require a rebuild before packaging for ${target}`) } await mkdir(join(root, "electron/dist"), {recursive: true}) const directory = await mkdtemp(join(root, "electron/dist/package-")) @@ -92,7 +97,7 @@ try { ]) { await cp(join(root, "electron", file), join(directory, "electron", file), {recursive: true}) } - await run("docker", [ + await run(env.DOCKER || "docker", [ "run", "--rm", "--platform", diff --git a/scripts/release.mjs b/scripts/release.mjs index 07e7842c..e93d2c99 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -107,8 +107,13 @@ const apkMetadata = async () => { return element } -const desktopTargets = {darwin: ["macos"], linux: ["linux", "windows"], win32: []} +const desktopTargets = { + darwin: ["macos", "linux", "windows"], + linux: ["linux", "windows"], + win32: [], +} const hostTargets = desktopTargets[process.platform] ?? [] +const docker = process.env.DOCKER || "docker" // Gitea's latest release is the desktop update feed, so it only goes public once every platform's // manifest is on it @@ -292,15 +297,16 @@ const steps = [ missing: () => [ ...(hostTargets.length > 0 ? [] : [`desktop packaging on ${process.platform}`]), ...(existsSync(join(root, "electron/node_modules")) ? [] : ["electron dependencies"]), - ...(hostTargets.includes("windows") && !installed("docker") ? ["docker"] : []), + ...(hostTargets.includes("windows") && !installed(docker) ? [docker] : []), ...(hostTargets.includes("macos") ? missingEnv("CSC_NAME", "ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_KEY_PATH") : []), ], setup: [ "Run npm ci --prefix electron. Each platform's packages have to be built on that platform,", - "so run pnpm release desktop gitea on the others to add theirs to the same release.", - "Linux also cross-builds the Windows installer, which needs docker.", + "so on Linux run pnpm release desktop gitea on a Mac to add the macOS ones to the release.", + "Linux and macOS build the other platforms' packages in a container, which needs docker,", + "or set DOCKER=podman in .env.local.", "macOS only installs updates to a signed app, so macOS packages are signed and notarized:", "set CSC_NAME to the name of the Developer ID Application certificate in your keychain,", "without its prefix, and the ASC_* key the ios step uses notarizes them.", @@ -324,9 +330,11 @@ const steps = [ .flat() .filter(target => !hostTargets.includes(target)) - followUps.push( - `Desktop: ${elsewhere.join(" and ")} packages have to be built on those platforms, then attached with pnpm release desktop gitea`, - ) + if (elsewhere.length > 0) { + followUps.push( + `Desktop: ${elsewhere.join(" and ")} packages have to be built on those platforms, then attached with pnpm release desktop gitea`, + ) + } }, }, {