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
5 changes: 4 additions & 1 deletion packages/codev-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ containerized (any machine with Docker):
the optional office layer: `image/office/install-office.sh` translates the
codev-office setup script system-wide — pandoc + LibreOffice + fonts from
Debian, .NET 8 SDK at `/opt/dotnet`, the pinned Python package set, the
bundle's pure-JS node-module trees on `NODE_PATH`, and the office skills at
bundle's node-module trees (pure-JS ones verbatim; sharp's native binary
re-resolved for the guest arch) reachable three ways — `NODE_PATH`,
`~/.node_modules` via skel, and `/usr/local/lib/node`, the one that needs
neither the environment nor `$HOME` — and the office skills at
`/opt/codev-skills` with an `/etc/skel` codev.json pointing at them. Fresh
sandbox homes get skel (vz at image build; bwrap seeded by the boot script),
and images declare extra env in `/etc/codev-guest.env`.
Expand Down
79 changes: 75 additions & 4 deletions packages/codev-sandbox/image/office/install-office.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,14 @@ python3 -c 'import pandas, openpyxl, markitdown, reportlab, pypdf, matplotlib, d

# --- JS module trees ---
# Merged into one tree the way the setup script merges them under
# ~/.codev-office. Reached two ways on purpose: NODE_PATH in the guest env
# (which the server exports, so its shell tool inherits it) and an
# /etc/skel/.node_modules symlink — one of Node's global require() fallback
# folders — so require() also resolves from a plain shell in any directory.
# ~/.codev-office. Reached three ways on purpose, because each of the first two
# has a way to not be there: NODE_PATH in the guest env (the server exports it,
# so its shell tool inherits it — unless something along the way replaces the
# environment), an /etc/skel/.node_modules symlink (one of Node's global
# require() fallback folders — but only for a process whose HOME is a home the
# skel was copied into, which a root shell's /root is not), and
# /usr/local/lib/node below, which is the same fallback list without either
# dependency.
mkdir -p "$OFFICE_HOME/node_modules"
staging="$(mktemp -d)"
for mod_zip in "$BUNDLE"/*-node-modules.zip; do
Expand All @@ -188,6 +192,73 @@ done
rm -rf "$staging"
mkdir -p /etc/skel
ln -sfn "$OFFICE_HOME/node_modules" /etc/skel/.node_modules
# $PREFIX/lib/node — the entry in Node's global fallback list that depends on
# neither the environment nor $HOME. Node derives it from its own binary
# (execPath/../../lib/node, so /usr/local/lib/node for the tarball install in
# the Dockerfile), which means require("pptxgenjs") resolves for any user, from
# any directory, with NODE_PATH unset. That is worth having even when the other
# two work: an agent that cannot require() the module goes looking for it, and
# `npm install -g pptxgenjs` cannot fix it either (npm -g installs into
# $PREFIX/lib/node_modules, which is NOT on require()'s search path) in a guest
# with no route off loopback.
#
# The fallback list is consulted only after the full node_modules walk-up, so a
# workspace project's own dependencies always win. The one behaviour this does
# change: a project MISSING a dependency the office tree happens to carry
# (react, react-dom) resolves it from here instead of failing.
mkdir -p /usr/local/lib
ln -sfn "$OFFICE_HOME/node_modules" /usr/local/lib/node

# Those trees are copied verbatim because they are pure JS — one build serves
# every arch. sharp (the pptx skill's react-icons -> SVG -> PNG path) is the
# exception: it loads a prebuilt libvips binary from an @img/sharp-<os>-<cpu>
# optional dependency, and the bundle carries the linux-x64 one. On arm64 that
# binary cannot load, so re-resolve it here exactly as every other
# arch-specific component in this file is handled — same version, arch-correct
# build. dpkg's arm64 is npm's arm64, and the Dockerfile refuses any arch that
# is not amd64 or arm64, so ARCH doubles as the --cpu value on this branch.
#
# Resolved in a scratch dir and copied in, NOT installed into the shared tree:
# `npm install` in a node_modules dir with no package.json reifies the tree
# from scratch and drops everything it cannot account for, which would take
# PptxGenJS and Playwright with it.
sharp_pkg="$OFFICE_HOME/node_modules/sharp/package.json"
if [ "$ARCH" != "amd64" ] && [ -f "$sharp_pkg" ]; then
SHARP_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$sharp_pkg")"
sharp_scratch="$(mktemp -d)"
( cd "$sharp_scratch" && npm init -y >/dev/null \
&& npm install --no-audit --no-fund --os=linux --cpu="$ARCH" --libc=glibc "sharp@$SHARP_VERSION" >/dev/null )
# A silently-ignored --cpu yields a tree that looks right and throws only in
# the guest, so require the binary package by name rather than trusting npm.
[ -d "$sharp_scratch/node_modules/@img/sharp-linux-$ARCH" ] \
|| { echo "no @img/sharp-linux-$ARCH for sharp $SHARP_VERSION (npm >= 10 needed for --cpu)" >&2; exit 1; }
rm -rf "$OFFICE_HOME/node_modules/@img"
cp -R "$sharp_scratch/node_modules/." "$OFFICE_HOME/node_modules/"
rm -rf "$sharp_scratch"
fi

# Use the trees rather than just checking they unpacked, the way the LibreOffice
# step converts a document instead of resolving a binary. They arrive by a
# wildcard glob, so a zip that failed to unpack, or a native binary built for
# the other arch, is otherwise invisible until an agent tries to build a deck.
#
# Deliberately run with NODE_PATH unset, from a directory with no relationship
# to the tree: that is the guest's worst case (a shell whose environment did not
# survive, in the user's workspace), so it is what the image should be proven
# against. Passing here means the /usr/local/lib/node fallback carries it alone.
office_node() { ( cd / && env -u NODE_PATH -u NODE_OPTIONS node -e "$1" ); }
if [ -d "$OFFICE_HOME/node_modules/pptxgenjs" ]; then
office_node 'const P=require("pptxgenjs");const p=new P();p.layout="LAYOUT_16x9";p.addSlide().addText("image check",{x:1,y:1,w:8,h:1,fontSize:24});p.writeFile({fileName:"/tmp/office-check.pptx"}).then(function(){process.exit(0);}).catch(function(e){console.error(e);process.exit(1);});'
python3 -m markitdown /tmp/office-check.pptx >/dev/null
rm -f /tmp/office-check.pptx
fi
# Icons are optional per the skill's own docs, so this warns like the playwright
# steps below rather than failing the build: without them decks still come out
# of shapes, text and charts.
if [ -d "$OFFICE_HOME/node_modules/sharp" ]; then
office_node 'const React=require("react"),RDS=require("react-dom/server"),sharp=require("sharp");const {FaCheckCircle}=require("react-icons/fa");const svg=RDS.renderToStaticMarkup(React.createElement(FaCheckCircle,{color:"#000000",size:"64"}));sharp(Buffer.from(svg)).png().toBuffer().then(function(b){process.exit(b.length?0:1);}).catch(function(e){console.error(e);process.exit(1);});' \
|| echo "warn: the pptx icon toolchain is unusable; react-icons artwork will be unavailable" >&2
fi

# --- Chromium for Playwright (pdf cover rendering) ---
# Playwright launches the browser build matching its own version, so the
Expand Down
Loading