Build the Linux and Windows desktop packages in a container from a Mac, with DOCKER choosing the container runtime

This commit is contained in:
Jon Staab 2026-09-23 16:21:36 -07:00
parent 7910c5c184
commit 57ed02eecb
3 changed files with 33 additions and 18 deletions

View file

@ -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.

View file

@ -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",

View file

@ -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`,
)
}
},
},
{