Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 13 additions & 25 deletions forge-ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 \
Expand Down
55 changes: 13 additions & 42 deletions opencode/Dockerfile.forge
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading