diff --git a/forge-ui/Dockerfile b/forge-ui/Dockerfile index 7d1d0b0..89b3c18 100644 --- a/forge-ui/Dockerfile +++ b/forge-ui/Dockerfile @@ -25,7 +25,7 @@ FROM oven/bun:1.3.14 AS builder # Build deps: forge-ui shares the opencode workspace tree, which pulls tree- # sitter-* and other native modules compiled by node-gyp at install time. -RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 git && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 git jq patch && rm -rf /var/lib/apt/lists/* WORKDIR /build @@ -34,40 +34,28 @@ WORKDIR /build # depends on workspace:* packages defined inside it. COPY opencode/package.json opencode/bun.lock opencode/bunfig.toml opencode/turbo.json opencode/tsconfig.json ./opencode/ COPY opencode/packages ./opencode/packages -# opencode/patches/ contains bun patchedDependencies referenced in -# opencode/package.json (photon-node, npmcli/agent, standard-openapi, etc.). -# bun install resolves them by relative path; missing => "Couldn't find patch -# file" and the whole install aborts. Copy before the install step. COPY opencode/patches ./opencode/patches COPY forge-ui/package.json ./forge-ui/ -# Drop unused workspace packages (desktop, console, storybook, docs, web) so -# bun install only downloads dependencies actually needed for forge-ui. +# Drop unused workspace packages (desktop, console, storybook, docs, web) and strip patchedDependencies RUN rm -rf opencode/packages/desktop opencode/packages/console opencode/packages/storybook opencode/packages/docs opencode/packages/web +RUN jq 'del(.patchedDependencies)' opencode/package.json > opencode/package.tmp.json && mv opencode/package.tmp.json opencode/package.json -# Strip bunfig.toml's minimumReleaseAge before install. See -# opencode/Dockerfile.forge for the full reasoning: the 3-day supply-chain -# gate is invisible on the host (cached) but breaks fresh Docker builds for -# every package that got a recent point release. +# Strip bunfig.toml's minimumReleaseAge before install. RUN sed -i '/minimumReleaseAge/d' opencode/bunfig.toml || true -# Install workspace deps from the opencode root. --linker=hoisted matches -# the user's host layout (npm-style flat node_modules). -# -# DIAGNOSTIC: same exit-code-suppression as opencode/Dockerfile.forge. -# Bun reports "Failed to install N packages" with FileNotFound errors for a -# specific subset of transitive deps. Suppressing here lets the Vite build -# proceed; if any of those packages are actually needed, the build step -# below will fail with a clear "Cannot find module" pointing at the real -# culprit instead of bun's misleading summary. -# -# tmpfs cache mount: same fix as opencode/Dockerfile.forge — bun's patch-apply -# fails with `EINVAL` on filesystems it flags as "slow/network" (e.g. NAS -# volumes), so we give its install cache a RAM-backed tmpfs. Keeps the build -# filesystem-agnostic on any host. +# Install workspace deps cleanly without bun's patch applier engine RUN --mount=type=tmpfs,target=/root/.bun/install/cache \ cd opencode && bun install --no-save --linker=hoisted || true +# Apply patches explicitly using POSIX GNU patch +RUN cd opencode \ + && patch -p1 -d node_modules/@ai-sdk/xai < patches/@ai-sdk-xai@3.0.82.patch \ + && patch -p1 -d node_modules/@npmcli/agent < patches/@npmcli-agent@4.0.0.patch \ + && patch -p1 -d node_modules/@silvia-odwyer/photon-node < patches/@silvia-odwyer-photon-node@0.3.4.patch \ + && patch -p1 -d node_modules/@standard-community/standard-openapi < patches/@standard-community-standard-openapi@0.2.9.patch \ + && patch -p1 -d node_modules/solid-js < patches/solid-js@1.9.10.patch + # Verify what's present so the log gives us a clear before/after for the # packages bun complained about. RUN cd opencode && for pkg in @ai-sdk/anthropic @smithy/eventstream-codec @shikijs/transformers minimatch; do \ diff --git a/opencode/Dockerfile.forge b/opencode/Dockerfile.forge index 24f3206..76b6898 100644 --- a/opencode/Dockerfile.forge +++ b/opencode/Dockerfile.forge @@ -27,7 +27,7 @@ FROM oven/bun:1.3.14 # tree-sitter-* packages ship native bindings compiled by node-gyp during # `bun install`. RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential python3 python3-pip ripgrep git curl jq \ + build-essential python3 python3-pip ripgrep git curl jq patch \ && rm -rf /var/lib/apt/lists/* WORKDIR /opencode @@ -37,57 +37,28 @@ WORKDIR /opencode COPY package.json bun.lock bunfig.toml turbo.json tsconfig.json ./ COPY packages ./packages # patches/ holds bun patchedDependencies referenced in package.json. -# bun install reads them by the relative paths in patchedDependencies -# (e.g. patches/@silvia-odwyer-photon-node@0.3.4.patch); missing files -# fail the install. Copy them in BEFORE bun install. COPY patches ./patches -# Two things in the workspace tree don't belong in the server image: -# 1. The root package.json declares "../forge-ui" as a workspace, but that -# path is outside this build context (we only ship opencode/). -# 2. packages/desktop is the Electron app; it `depends_on` @forge/ui -# (workspace:*) which we don't ship here, so bun install fails resolving -# that dep — even after stripping ../forge-ui from the root workspaces. -# Strip both before install. Build-time only; source on disk is untouched. -RUN jq '.workspaces.packages |= map(select(. != "../forge-ui"))' package.json > package.tmp.json \ +# Strip ../forge-ui from workspaces and strip patchedDependencies so bun install +# doesn't invoke Bun's buggy native patch applier on NAS storage. +RUN jq '.workspaces.packages |= map(select(. != "../forge-ui")) | del(.patchedDependencies)' package.json > package.tmp.json \ && mv package.tmp.json package.json \ && rm -rf packages/desktop -# Strip bunfig.toml's minimumReleaseAge=259200 before install. That setting -# is a supply-chain-security gate that blocks installation of any package -# version published in the last 3 days. On a developer host with a populated -# bun cache the constraint is harmless (old resolutions are already cached). -# In a fresh Docker build every package is a re-download and bun hard-fails -# on whichever ones got a recent point release — surfaced as the misleading -# "Failed to install N packages" + FileNotFound cascade. minimumReleaseAge -# belongs in CI gates, not in container builds. Drop it for the build only; -# the file on disk is untouched. +# Strip bunfig.toml's minimumReleaseAge before install. RUN sed -i '/minimumReleaseAge/d' bunfig.toml || true -# Install workspace deps using bun's DEFAULT (isolated) linker. Earlier -# attempts forced --linker=hoisted because the install seemed to succeed -# with it; in fact bun's runtime loader doesn't walk up from the script's -# directory to the workspace root, so hoisted lands every package at -# /opencode/node_modules/X but the runtime looks at -# /opencode/packages/opencode/node_modules/X and ENOENTs. Isolated places -# deps at the per-workspace-member path the runtime actually uses. -# -# First pass populates bun's local cache despite the same "Failed to install -# N packages" cascade; the retry is strict — if it still can't materialise the -# tree we fail the build with bun's own error. -# -# tmpfs cache mount (filesystem-agnostic install): bun's install + patch-apply -# IO is sensitive to the underlying filesystem. On some Docker storage backends -# (notably Synology NAS volumes) bun prints "Slow filesystem detected … network -# drive" and then fails patch-apply with `EINVAL: Invalid argument (read())` on -# every patchedDependency. bun's own remediation is to put its install cache on -# a fast local folder — so we mount a RAM-backed tmpfs at the cache path for the -# install step. The build then runs identically on a laptop, a cloud VM, or a -# NAS with no host-side prep. Both passes share the one tmpfs RUN so the strict -# retry reuses the warmed cache. (Needs the documented build RAM headroom.) +# Install workspace deps cleanly without bun's patch applier engine RUN --mount=type=tmpfs,target=/root/.bun/install/cache \ bun install --no-save || bun install --no-save +# Apply patches explicitly using POSIX GNU patch for 100% NAS/cloud/mac filesystem compatibility +RUN patch -p1 -d node_modules/@ai-sdk/xai < patches/@ai-sdk-xai@3.0.82.patch \ + && patch -p1 -d node_modules/@npmcli/agent < patches/@npmcli-agent@4.0.0.patch \ + && patch -p1 -d node_modules/@silvia-odwyer/photon-node < patches/@silvia-odwyer-photon-node@0.3.4.patch \ + && patch -p1 -d node_modules/@standard-community/standard-openapi < patches/@standard-community-standard-openapi@0.2.9.patch \ + && patch -p1 -d node_modules/solid-js < patches/solid-js@1.9.10.patch + # Bun's isolated linker doesn't fully materialise every transitive dep into # per-workspace-member node_modules — registry deps often end up only at # the workspace root /opencode/node_modules. Bun's runtime loader resolves