fix(pack): bundle download and host packages in Linux AppImage assembly (#2845)

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.
This commit is contained in:
youcef zr 2026-05-29 08:25:03 +01:00 committed by GitHub
parent da19ff3ca0
commit d6d42c3600
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 88 additions and 1 deletions

View file

@ -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<void> {
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"]);

View file

@ -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<string, string> };
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([]);
});
}
});