]> git.ekhem.eu.org Git - guix.git/commitdiff
[home] Add npm-install-build-system.
authorJakub Czajka <jakub@ekhem.eu.org>
Tue, 7 Jul 2026 16:10:10 +0000 (18:10 +0200)
committerJakub Czajka <jakub@ekhem.eu.org>
Wed, 8 Jul 2026 21:53:21 +0000 (23:53 +0200)
README
conf/home/npm.scm [new file with mode: 0644]
scripts/generate-npm-hash [new file with mode: 0755]

diff --git a/README b/README
index 400401e8b18c79a867e23a960ae59715a94bccb3..d57e93185c7f64731a07e8932d8c847323cd9db8 100644 (file)
--- a/README
+++ b/README
@@ -27,6 +27,23 @@ Files
                  configurations.
 * <program>/   - 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 <name> <version>
+
+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 (file)
index 0000000..b1227df
--- /dev/null
@@ -0,0 +1,136 @@
+;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;; 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 (executable)
index 0000000..39060a4
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# generate-npm-hash — produce a pre-built npm tree and print its guix hash.
+#
+# Usage: generate-npm-hash <name> <version>
+#
+# npm-installs the named package at the given version, creates a
+# <name>-built-<version>/ 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 <name> <version>" >&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/<name> 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")"