From d6d42c3600878d8e53f055947c743202a214ad24 Mon Sep 17 00:00:00 2001 From: youcef zr <93142224+youcefzemmar@users.noreply.github.com> Date: Fri, 29 May 2026 08:25:03 +0100 Subject: [PATCH] fix(pack): bundle download and host packages in Linux AppImage assembly (#2845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linux AppImage path assembles INTERNAL_PACKAGES as `file:` tarballs and runs `npm install --omit=dev` in an isolated app directory. `pnpm pack` rewrites each tarball's `workspace:*` refs to a concrete version, so any runtime @open-design/* dependency missing from INTERNAL_PACKAGES is resolved from the public npm registry and 404s. Linux ships webOutputMode "server" and tarball-installs every INTERNAL_PACKAGES entry, including @open-design/desktop and @open-design/web. @open-design/host (dep of web + desktop, added in #2246) and @open-design/download (dep of desktop, added in #2677) landed after the Linux package list was written and were never added to it, so `pnpm exec tools-pack linux build --to appimage` fails with: npm error 404 Not Found - GET .../@open-design%2fdownload mac/win default to "standalone", where desktop/web/packaged/daemon are prebundled with esbuild and excluded from the tarball install (shouldInstallInternalPackageFor{Mac,Win}Prebundle). The packages they do install have no download/host dependency, so those lanes correctly omit them and need no change — this fix stays scoped to linux.ts and touches no mac/win or workspace-build code. Add both packages to the Linux INTERNAL_PACKAGES and build them in buildWorkspaceArtifacts (download depends on platform). Add a cross-lane regression test that, for each lane, derives the set it actually installs (honoring the standalone prebundle exclusion) and asserts that set is closed under its runtime @open-design/* dependencies. The test is red on the linux lane without this fix and green with it, while mac/win pass either way — encoding why only Linux needs these packages. --- tools/pack/src/linux.ts | 6 +- .../tests/internal-packages-closure.test.ts | 83 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tools/pack/tests/internal-packages-closure.test.ts diff --git a/tools/pack/src/linux.ts b/tools/pack/src/linux.ts index 5508c6c6c..adb677626 100644 --- a/tools/pack/src/linux.ts +++ b/tools/pack/src/linux.ts @@ -46,12 +46,14 @@ const CONTAINER_PNPM_HOME = "/tmp/pnpm-home"; const CONTAINER_NODE_VERSION = "24.14.1"; const CONTAINER_TOOLS_PACK_CLI_PATH = "tools/pack/bin/tools-pack.mjs"; -const INTERNAL_PACKAGES = [ +export const INTERNAL_PACKAGES = [ { directory: "packages/contracts", name: "@open-design/contracts" }, { directory: "packages/registry-protocol", name: "@open-design/registry-protocol" }, { directory: "packages/sidecar-proto", name: "@open-design/sidecar-proto" }, { directory: "packages/sidecar", name: "@open-design/sidecar" }, { directory: "packages/platform", name: "@open-design/platform" }, + { directory: "packages/download", name: "@open-design/download" }, + { directory: "packages/host", name: "@open-design/host" }, { directory: "packages/agui-adapter", name: "@open-design/agui-adapter" }, { directory: "packages/plugin-runtime", name: "@open-design/plugin-runtime" }, { directory: "packages/diagnostics", name: "@open-design/diagnostics" }, @@ -392,6 +394,8 @@ async function buildWorkspaceArtifacts(config: ToolPackConfig): Promise { await runPnpm(config, ["--filter", "@open-design/sidecar-proto", "build"]); await runPnpm(config, ["--filter", "@open-design/sidecar", "build"]); await runPnpm(config, ["--filter", "@open-design/platform", "build"]); + await runPnpm(config, ["--filter", "@open-design/host", "build"]); + await runPnpm(config, ["--filter", "@open-design/download", "build"]); await runPnpm(config, ["--filter", "@open-design/agui-adapter", "build"]); await runPnpm(config, ["--filter", "@open-design/plugin-runtime", "build"]); await runPnpm(config, ["--filter", "@open-design/diagnostics", "build"]); diff --git a/tools/pack/tests/internal-packages-closure.test.ts b/tools/pack/tests/internal-packages-closure.test.ts new file mode 100644 index 000000000..2f398266e --- /dev/null +++ b/tools/pack/tests/internal-packages-closure.test.ts @@ -0,0 +1,83 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +import { INTERNAL_PACKAGES as LINUX_INTERNAL_PACKAGES } from "../src/linux.js"; +import { INTERNAL_PACKAGES as MAC_INTERNAL_PACKAGES } from "../src/mac/constants.js"; +import { shouldInstallInternalPackageForMacPrebundle } from "../src/mac-prebundle.js"; +import { INTERNAL_PACKAGES as WIN_INTERNAL_PACKAGES } from "../src/win/constants.js"; +import { shouldInstallInternalPackageForWinPrebundle } from "../src/win-prebundle.js"; + +const workspaceRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + +type PackageEntry = { readonly directory: string; readonly name: string }; + +function runtimeWorkspaceDeps(directory: string): string[] { + const manifest = JSON.parse( + readFileSync(join(workspaceRoot, directory, "package.json"), "utf8"), + ) as { dependencies?: Record }; + return Object.keys(manifest.dependencies ?? {}).filter((dep) => dep.startsWith("@open-design/")); +} + +// Each pack lane assembles its packaged app by `pnpm pack`-ing a subset of +// INTERNAL_PACKAGES into tarballs, wiring them as `file:` dependencies, and +// running an npm/pnpm install in the isolated app directory. `pnpm pack` +// rewrites every `workspace:*` ref to a concrete version, so the install +// resolves each tarball's runtime `@open-design/*` dependencies. Any such +// dependency that is NOT also installed as a local tarball is fetched from the +// public npm registry and 404s — these packages are workspace-only and never +// published. +// +// The invariant: the set a lane actually installs must be closed under its +// runtime `@open-design/*` dependencies. +// +// The lanes diverge by web output mode: +// - linux ships "server" mode and tarball-installs every INTERNAL_PACKAGES +// entry, including @open-design/desktop and @open-design/web — so it must +// also install their runtime deps (@open-design/download, @open-design/host). +// - mac/win default to "standalone", where desktop/web/packaged/daemon are +// prebundled with esbuild and excluded from the tarball install. The +// packages they do install have no download/host dependency, so those +// lanes correctly omit them. Adding download/host there would be dead +// weight and would drag in the shared workspace-build cache. +const LANES: { name: string; packages: readonly PackageEntry[]; isInstalled: (pkg: PackageEntry) => boolean }[] = [ + { + name: "linux", + packages: LINUX_INTERNAL_PACKAGES, + isInstalled: () => true, + }, + { + name: "mac", + packages: MAC_INTERNAL_PACKAGES, + isInstalled: (pkg) => + shouldInstallInternalPackageForMacPrebundle({ packageName: pkg.name, webOutputMode: "standalone" }), + }, + { + name: "win", + packages: WIN_INTERNAL_PACKAGES, + isInstalled: (pkg) => + shouldInstallInternalPackageForWinPrebundle({ packageName: pkg.name, webOutputMode: "standalone" }), + }, +]; + +describe("pack lane INTERNAL_PACKAGES dependency closure", () => { + for (const lane of LANES) { + it(`${lane.name}: every installed package's runtime @open-design deps are installed`, () => { + const installed = lane.packages.filter((pkg) => lane.isInstalled(pkg)); + const installedNames = new Set(installed.map((pkg) => pkg.name)); + const missing: { dependency: string; dependent: string }[] = []; + + for (const pkg of installed) { + for (const dependency of runtimeWorkspaceDeps(pkg.directory)) { + if (!installedNames.has(dependency)) { + missing.push({ dependency, dependent: pkg.name }); + } + } + } + + expect(missing).toEqual([]); + }); + } +});