From: Jakub Czajka Date: Tue, 7 Jul 2026 16:10:10 +0000 (+0200) Subject: [home] Add npm-install-build-system. X-Git-Url: https://git.ekhem.eu.org/?a=commitdiff_plain;h=afdaf59a14143484f0e35c14d9fe6920142370fb;p=guix.git [home] Add npm-install-build-system. --- diff --git a/README b/README index 400401e..d57e931 100644 --- a/README +++ b/README @@ -27,6 +27,23 @@ Files configurations. * / - program-specific dotfiles. +Updating npm-based packages +--------------------------- + +Some packages are built from pre-built npm trees because their dependency +trees are too large for recursive Guix packaging. +``scripts/generate-npm-hash`` handles the update: + + $ scripts/generate-npm-hash + +This npm-installs the named package at the given version and prints the +``guix hash`` for the resulting ``node_modules/`` tree. Copy that hash +into the package definition's ``local-file`` reference. + +You only need to run this script when bumping the npm package version +-- ``guix home reconfigure`` uses the hash baked into the definition and +never runs ``npm install`` itself. + TODO ---- @@ -43,3 +60,4 @@ TODO 2. Or use EnterWorktree for automatic isolation. 3. Or add a PreToolUse hook that auto-creates/switches to a per-session branch before Edit/Write. + diff --git a/conf/home/npm.scm b/conf/home/npm.scm new file mode 100644 index 0000000..b1227df --- /dev/null +++ b/conf/home/npm.scm @@ -0,0 +1,136 @@ +;; Copyright (c) 2026 Jakub Czajka +;; License: GPL-3.0 or later. +;; +;; npm.scm — npm-install-build-system for npm/Node.js packages. +;; +;; This module provides a build system that installs npm packages +;; inside a fixed-output derivation and wraps them with a shell +;; script. The two-phase design avoids running npm during normal +;; guix builds: +;; +;; 1. Fixed-output derivation — npm install fetches the package +;; and its dependency tree into a content-addressed directory. +;; The hash pins the exact dependency set; network access is +;; granted because the output hash is declared up front. +;; +;; 2. Wrapper script — a minimal shell script that invokes Node.js +;; on the installed entry point. This runs in a normal +;; (sandboxed) derivation. + +(define-module (conf home npm) + #:use-module (guix gexp) + #:use-module (guix build-system) + #:use-module (guix build-system trivial) + #:use-module (guix utils) + #:use-module (gnu packages bash) + #:use-module (gnu packages base) + #:use-module (gnu packages node) + #:export (npm-install-build-system)) + +(define (npm-prebuilt-drv bin-name npm-package version hash) + "Return a computed-file that npm-installs NPM-PACKAGE at VERSION." + (computed-file (string-append bin-name "-built-" version) + #~(begin + (use-modules (guix build utils)) + (setenv "HOME" "/tmp") + (let* ((out #$output) + (node #$(file-append node-lts "/bin/node")) + (npm-cli #$(file-append node-lts + "/lib/node_modules/npm/bin/npm-cli.js")) + (bash #$(file-append bash-minimal "/bin/bash")) + (build-dir "/tmp/npm-build") + (bin-dir "/tmp/bin")) + ;; Phase 1: npm install (fixed-output). + (mkdir-p build-dir) + (with-output-to-file (string-append build-dir + "/package.json") + (lambda () + (format #t "{\"dependencies\": {\"~a\": \"~a\"}}~%" + #$npm-package + #$version))) + ;; sh symlink for lifecycle scripts. + (mkdir-p bin-dir) + (symlink bash + (string-append bin-dir "/sh")) + ;; PATH must include node, bash, npm itself, + ;; and the sh symlink so lifecycle scripts + ;; that run before npm's bin/ is populated + ;; can find a shell. + (setenv "PATH" + (string-append (dirname (dirname npm-cli)) + "/bin:" + (dirname node) + ":" + (dirname bash) + ":" + bin-dir + ":" + (getenv "PATH"))) + (with-directory-excursion build-dir + (invoke node npm-cli "install" "--production" + "--ignore-scripts")) + (mkdir-p out) + (copy-recursively (string-append build-dir + "/node_modules") + (string-append out "/node_modules")))) + #:options `(#:hash ,hash + #:hash-algo sha256 + #:recursive? #t + #:leaked-env-vars ("http_proxy" "https_proxy") + #:modules ((guix build utils))))) + +(define (npm-build-gexp bin-name entry hash npm-package version) + "Return a builder gexp for an npm-install-build-system package." + (let ((built (npm-prebuilt-drv bin-name npm-package version hash)) + (mkdir (file-append coreutils "/bin/mkdir")) + (chmod (file-append coreutils "/bin/chmod"))) + #~(begin + ;; Phase 2: generate shell wrapper script. + (let* ((out (getenv "out")) + (bindir (string-append out "/bin")) + (script (string-append bindir "/" + #$bin-name))) + (system (string-append #$mkdir " -p " bindir)) + (with-output-to-file script + (lambda () + (format #t "#!~a~%" + #$(file-append bash-minimal "/bin/bash")) + (format #t "exec ~a ~a \"$@\"~%" + #$(file-append node-lts "/bin/node") + (string-append #$built "/node_modules/" + #$entry)))) + (system (string-append #$chmod " +x " script)))))) + +(define* (lower-npm-install name + #:key npm-package + version + entry + hash + (bin-name name) + (wrapper-name name) + source + target + system + inputs + native-inputs + outputs + #:allow-other-keys #:rest arguments) + "Lower an npm-install-build-system package to a bag." + (let ((builder (npm-build-gexp wrapper-name entry hash npm-package version))) + ((build-system-lower trivial-build-system) + name + #:source source + #:target target + #:system system + #:inputs inputs + #:native-inputs `(("coreutils" ,coreutils) + ,@native-inputs) + #:outputs outputs + #:builder builder))) + +(define npm-install-build-system + (build-system (name 'npm-install) + (description + "npm install in a fixed-output derivation, wrap with a Node.js +shell script") + (lower lower-npm-install))) diff --git a/scripts/generate-npm-hash b/scripts/generate-npm-hash new file mode 100755 index 0000000..39060a4 --- /dev/null +++ b/scripts/generate-npm-hash @@ -0,0 +1,51 @@ +#!/bin/sh +# Copyright (c) 2026 Jakub Czajka +# License: GPL-3.0 or later. +# +# generate-npm-hash — produce a pre-built npm tree and print its guix hash. +# +# Usage: generate-npm-hash +# +# npm-installs the named package at the given version, creates a +# -built-/ directory with node_modules/, and prints the +# guix hash for a local-file reference. +# +# Run this only when bumping the npm package version — guix home +# reconfigure uses the hash baked into the package definition and never +# runs npm install itself. + +set -o errexit -o nounset + +if [ $# -ne 2 ]; then + echo "Usage: generate-npm-hash " >&2 + exit 1 +fi + +name=$1 +version=$2 + +case $name in + *) + echo "Unknown package: $name" >&2 + exit 1 + ;; +esac + +dir="${name}-built-${version}" +tmp="${name}-tmp" + +rm -rf "$dir" "$tmp" +mkdir "$tmp" +echo "{\"dependencies\": {\"$npm_pkg\": \"$version\"}}" > "$tmp/package.json" +(cd "$tmp" && npm install --production) +mkdir -p "$dir" +cp --recursive "$tmp/node_modules" "$dir/" +rm -rf "$tmp" + +# Verify that npm's .bin/ symlink exists. +if [ ! -e "$dir/node_modules/.bin/$name" ]; then + echo "ERROR: entry point .bin/$name missing in ${dir}/node_modules/" >&2 + exit 1 +fi + +echo "Done. guix hash: $(guix hash --recursive "$dir")"