1{
2 lib,
3 stdenv,
4 fetchurl,
5 openssl,
6 python,
7 zlib,
8 libuv,
9 sqlite,
10 icu,
11 bash,
12 ninja,
13 pkgconf,
14 unixtools,
15 runCommand,
16 buildPackages,
17 testers,
18 # for `.pkgs` attribute
19 callPackage,
20 # Updater dependencies
21 writeScript,
22 coreutils,
23 gnugrep,
24 jq,
25 curl,
26 common-updater-scripts,
27 runtimeShell,
28 gnupg,
29 installShellFiles,
30 darwin,
31}:
32
33{
34 enableNpm ? true,
35 version,
36 sha256,
37 patches ? [ ],
38}@args:
39
40let
41
42 majorVersion = lib.versions.major version;
43 minorVersion = lib.versions.minor version;
44
45 pname = if enableNpm then "nodejs" else "nodejs-slim";
46
47 canExecute = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
48 emulator = stdenv.hostPlatform.emulator buildPackages;
49 canEmulate = stdenv.hostPlatform.emulatorAvailable buildPackages;
50 buildNode = buildPackages."${pname}_${majorVersion}";
51
52 # See valid_os and valid_arch in configure.py.
53 destOS =
54 let
55 platform = stdenv.hostPlatform;
56 in
57 if platform.isiOS then
58 "ios"
59 else if platform.isAndroid then
60 "android"
61 else if platform.isWindows then
62 "win"
63 else if platform.isDarwin then
64 "mac"
65 else if platform.isLinux then
66 "linux"
67 else if platform.isOpenBSD then
68 "openbsd"
69 else if platform.isFreeBSD then
70 "freebsd"
71 else
72 throw "unsupported os ${platform.uname.system}";
73 destARMFPU =
74 let
75 platform = stdenv.hostPlatform;
76 in
77 if platform.isAarch32 && platform ? gcc.fpu then
78 lib.throwIfNot (builtins.elem platform.gcc.fpu [
79 "vfp"
80 "vfpv2"
81 "vfpv3"
82 "vfpv3-d16"
83 "neon"
84 ]) "unsupported ARM FPU ${platform.gcc.fpu}" platform.gcc.fpu
85 else
86 null;
87 destARMFloatABI =
88 let
89 platform = stdenv.hostPlatform;
90 in
91 if platform.isAarch32 && platform ? gcc.float-abi then
92 lib.throwIfNot (builtins.elem platform.gcc.float-abi [
93 "soft"
94 "softfp"
95 "hard"
96 ]) "unsupported ARM float ABI ${platform.gcc.float-abi}" platform.gcc.float-abi
97 else
98 null;
99 # TODO: also handle MIPS flags (mips_arch, mips_fpu, mips_float_abi).
100
101 useSharedSQLite = lib.versionAtLeast version "22.5";
102
103 sharedLibDeps = {
104 inherit openssl zlib libuv;
105 }
106 // (lib.optionalAttrs useSharedSQLite {
107 inherit sqlite;
108 });
109
110 copyLibHeaders = map (name: "${lib.getDev sharedLibDeps.${name}}/include/*") (
111 builtins.attrNames sharedLibDeps
112 );
113
114 # Currently stdenv sets CC/LD/AR/etc environment variables to program names
115 # instead of absolute paths. If we add cctools to nativeBuildInputs, that
116 # would shadow stdenv’s bintools and potentially break other parts of the
117 # build. The correct behavior is to use absolute paths, and there is a PR for
118 # that, see https://github.com/NixOS/nixpkgs/pull/314920. As a temporary
119 # workaround, we use only a single program we need (and that is not part of
120 # the stdenv).
121 darwin-cctools-only-libtool =
122 # Would be nice to have onlyExe builder similar to onlyBin…
123 runCommand "darwin-cctools-only-libtool" { cctools = lib.getBin buildPackages.cctools; } ''
124 mkdir -p "$out/bin"
125 ln -s "$cctools/bin/libtool" "$out/bin/libtool"
126 '';
127
128 # a script which claims to be a different script but switches to simply touching its output
129 # when an environment variable is set. See CC_host, --cross-compiling, and postConfigure.
130 touchScript =
131 real:
132 writeScript "touch-script" ''
133 #!${stdenv.shell}
134 if [ -z "$FAKE_TOUCH" ]; then
135 exec "${real}" "$@"
136 fi
137 while [ "$#" != "0" ]; do
138 if [ "$1" == "-o" ]; then
139 shift
140 touch "$1"
141 fi
142 shift
143 done
144 '';
145
146 downloadDir = if lib.strings.hasInfix "-rc." version then "download/rc" else "dist";
147
148 package = stdenv.mkDerivation (
149 finalAttrs:
150 let
151 /**
152 the final package fixed point, after potential overrides
153 */
154 self = finalAttrs.finalPackage;
155 in
156 {
157 inherit pname version;
158
159 src = fetchurl {
160 url = "https://nodejs.org/${downloadDir}/v${version}/node-v${version}.tar.xz";
161 inherit sha256;
162 };
163
164 strictDeps = true;
165
166 env = {
167 # Tell ninja to avoid ANSI sequences, otherwise we don’t see build
168 # progress in Nix logs.
169 #
170 # Note: do not set TERM=dumb environment variable globally, it is used in
171 # test-ci-js test suite to skip tests that otherwise run fine.
172 NINJA = "TERM=dumb ninja";
173 }
174 // lib.optionalAttrs (!canExecute && !canEmulate) {
175 # these are used in the --cross-compiling case. see comment at postConfigure.
176 CC_host = touchScript "${buildPackages.stdenv.cc}/bin/cc";
177 CXX_host = touchScript "${buildPackages.stdenv.cc}/bin/c++";
178 AR_host = touchScript "${buildPackages.stdenv.cc}/bin/ar";
179 };
180
181 # NB: technically, we do not need bash in build inputs since all scripts are
182 # wrappers over the corresponding JS scripts. There are some packages though
183 # that use bash wrappers, e.g. polaris-web.
184 buildInputs = [
185 zlib
186 libuv
187 openssl
188 icu
189 bash
190 ]
191 ++ lib.optionals useSharedSQLite [ sqlite ];
192
193 nativeBuildInputs = [
194 installShellFiles
195 ninja
196 pkgconf
197 python
198 ]
199 ++ lib.optionals stdenv.buildPlatform.isDarwin [
200 # gyp checks `sysctl -n hw.memsize` if `sys.platform == "darwin"`.
201 unixtools.sysctl
202 ]
203 ++ lib.optionals stdenv.hostPlatform.isDarwin [
204 # For gyp-mac-tool if `flavor == "mac"`.
205 darwin-cctools-only-libtool
206 ];
207
208 # We currently rely on Makefile and stdenv for build phases, so do not let
209 # ninja’s setup hook to override default stdenv phases.
210 dontUseNinjaBuild = true;
211 dontUseNinjaCheck = true;
212 dontUseNinjaInstall = true;
213
214 outputs = [
215 "out"
216 "libv8"
217 ]
218 ++ lib.optionals (stdenv.hostPlatform == stdenv.buildPlatform) [ "dev" ];
219 setOutputFlags = false;
220 moveToDev = false;
221
222 configureFlags = [
223 "--ninja"
224 "--with-intl=system-icu"
225 "--openssl-use-def-ca-store"
226 # --cross-compiling flag enables use of CC_host et. al
227 (if canExecute || canEmulate then "--no-cross-compiling" else "--cross-compiling")
228 "--dest-os=${destOS}"
229 "--dest-cpu=${stdenv.hostPlatform.node.arch}"
230 ]
231 ++ lib.optionals (destARMFPU != null) [ "--with-arm-fpu=${destARMFPU}" ]
232 ++ lib.optionals (destARMFloatABI != null) [ "--with-arm-float-abi=${destARMFloatABI}" ]
233 ++ lib.optionals (!canExecute && canEmulate) [
234 # Node.js requires matching bitness between build and host platforms, e.g.
235 # for V8 startup snapshot builder (see tools/snapshot) and some other
236 # tools. We apply a patch that runs these tools using a host platform
237 # emulator and avoid cross-compiling altogether (from the build system’s
238 # perspective).
239 "--emulator=${emulator}"
240 ]
241 ++ lib.optionals (lib.versionOlder version "19") [ "--without-dtrace" ]
242 ++ lib.optionals (!enableNpm) [ "--without-npm" ]
243 ++ lib.concatMap (name: [
244 "--shared-${name}"
245 "--shared-${name}-libpath=${lib.getLib sharedLibDeps.${name}}/lib"
246 /**
247 Closure notes: we explicitly avoid specifying --shared-*-includes,
248 as that would put the paths into bin/nodejs.
249 Including pkg-config in build inputs would also have the same effect!
250
251 FIXME: the statement above is outdated, we have to include pkg-config
252 in build inputs for system-icu.
253 */
254 ]) (builtins.attrNames sharedLibDeps);
255
256 configurePlatforms = [ ];
257
258 dontDisableStatic = true;
259
260 configureScript = writeScript "nodejs-configure" ''
261 exec ${python.executable} configure.py "$@"
262 '';
263
264 # In order to support unsupported cross configurations, we copy some intermediate executables
265 # from a native build and replace all the build-system tools with a script which simply touches
266 # its outfile. We have to indiana-jones-swap the build-system-targeted tools since they are
267 # tested for efficacy at configure time.
268 postConfigure = lib.optionalString (!canEmulate && !canExecute) ''
269 cp ${buildNode.dev}/bin/* out/Release
270 export FAKE_TOUCH=1
271 '';
272
273 enableParallelBuilding = true;
274
275 # Don't allow enabling content addressed conversion as `nodejs`
276 # checksums it's image before conversion happens and image loading
277 # breaks:
278 # $ nix build -f. nodejs --arg config '{ contentAddressedByDefault = true; }'
279 # $ ./result/bin/node
280 # Check failed: VerifyChecksum(blob).
281 __contentAddressed = false;
282
283 passthru.interpreterName = "nodejs";
284
285 passthru.pkgs = callPackage ../../node-packages/default.nix {
286 nodejs = self;
287 };
288
289 setupHook = ./setup-hook.sh;
290
291 pos = builtins.unsafeGetAttrPos "version" args;
292
293 inherit patches;
294
295 postPatch = lib.optionalString stdenv.hostPlatform.isDarwin ''
296 substituteInPlace test/parallel/test-macos-app-sandbox.js \
297 --subst-var-by codesign '${darwin.sigtool}/bin/codesign'
298 '';
299
300 __darwinAllowLocalNetworking = true; # for tests
301
302 doCheck = canExecute;
303
304 # See https://github.com/nodejs/node/issues/22006
305 enableParallelChecking = false;
306
307 # Some dependencies required for tools/doc/node_modules (and therefore
308 # test-addons, jstest and others) target are not included in the tarball.
309 # Run test targets that do not require network access.
310 checkTarget = lib.concatStringsSep " " (
311 [
312 "build-js-native-api-tests"
313 "build-node-api-tests"
314 "tooltest"
315 "cctest"
316 ]
317 ++ lib.optionals (!stdenv.buildPlatform.isDarwin || lib.versionAtLeast version "20") [
318 # There are some test failures on macOS before v20 that are not worth the
319 # time to debug for a version that would be eventually removed in less
320 # than a year (Node.js 18 will be EOL at 2025-04-30). Note that these
321 # failures are specific to Nix sandbox on macOS and should not affect
322 # actual functionality.
323 "test-ci-js"
324 ]
325 );
326
327 checkFlags = [
328 # Do not create __pycache__ when running tests.
329 "PYTHONDONTWRITEBYTECODE=1"
330 ]
331 ++ lib.optionals (stdenv.buildPlatform.isDarwin && stdenv.buildPlatform.isx86_64) [
332 # Python 3.12 introduced a warning for calling `os.fork()` in a
333 # multi‐threaded program. For some reason, the Node.js
334 # `tools/pseudo-tty.py` program used for PTY‐related tests
335 # triggers this warning on Hydra, on `x86_64-darwin` only,
336 # despite not creating any threads itself. This causes the
337 # Node.js test runner to misinterpret the warnings as part of the
338 # test output and fail. It does not reproduce reliably off Hydra
339 # on Intel Macs, or occur on the `aarch64-darwin` builds.
340 #
341 # This seems likely to be related to Rosetta 2, but it could also
342 # be some strange x86‐64‐only threading behaviour of the Darwin
343 # system libraries, or a bug in CPython, or something else
344 # haunted about the Nixpkgs/Hydra build environment. We silence
345 # the warnings in the hope that closing our eyes will make the
346 # ghosts go away.
347 "PYTHONWARNINGS=ignore::DeprecationWarning"
348 ]
349 ++ lib.optionals (!stdenv.buildPlatform.isDarwin || lib.versionAtLeast version "20") [
350 "FLAKY_TESTS=skip"
351 # Skip some tests that are not passing in this context
352 "CI_SKIP_TESTS=${
353 lib.concatStringsSep "," (
354 [
355 # Tests don't work in sandbox.
356 "test-child-process-exec-env"
357 "test-child-process-uid-gid"
358 "test-fs-write-stream-eagain"
359 "test-process-euid-egid"
360 "test-process-initgroups"
361 "test-process-setgroups"
362 "test-process-uid-gid"
363 # This is a bit weird, but for some reason fs watch tests fail with
364 # sandbox.
365 "test-fs-promises-watch"
366 "test-fs-watch"
367 "test-fs-watch-encoding"
368 "test-fs-watch-non-recursive"
369 "test-fs-watch-recursive-add-file"
370 "test-fs-watch-recursive-add-file-to-existing-subfolder"
371 "test-fs-watch-recursive-add-file-to-new-folder"
372 "test-fs-watch-recursive-add-file-with-url"
373 "test-fs-watch-recursive-add-folder"
374 "test-fs-watch-recursive-assert-leaks"
375 "test-fs-watch-recursive-promise"
376 "test-fs-watch-recursive-symlink"
377 "test-fs-watch-recursive-sync-write"
378 "test-fs-watch-recursive-update-file"
379 "test-fs-watchfile"
380 "test-runner-run"
381 "test-runner-watch-mode"
382 "test-watch-mode-files_watcher"
383 ]
384 ++ lib.optionals (!lib.versionAtLeast version "22") [
385 "test-tls-multi-key"
386 ]
387 ++ lib.optionals stdenv.hostPlatform.is32bit [
388 # utime (actually utimensat) fails with EINVAL on 2038 timestamp
389 "test-fs-utimes-y2K38"
390 ]
391 ++ lib.optionals stdenv.buildPlatform.isDarwin [
392 # Disable tests that don’t work under macOS sandbox.
393 # uv_os_setpriority returned EPERM (operation not permitted)
394 "test-os"
395 "test-os-process-priority"
396
397 # Debugger tests failing on macOS 15.4
398 "test-debugger-extract-function-name"
399 "test-debugger-random-port-with-inspect-port"
400 "test-debugger-launch"
401 "test-debugger-pid"
402
403 # Those are annoyingly flaky, but not enough to be marked as such upstream.
404 "test-wasi"
405 ]
406 ++ lib.optionals stdenv.hostPlatform.isMusl [
407 # Doesn't work in sandbox on x86_64.
408 "test-dns-set-default-order"
409 ]
410 ++ lib.optionals (stdenv.buildPlatform.isDarwin && stdenv.buildPlatform.isx86_64) [
411 # These tests fail on x86_64-darwin (even without sandbox).
412 # TODO: revisit at a later date.
413 "test-fs-readv"
414 "test-fs-readv-sync"
415 "test-vm-memleak"
416
417 # Those are annoyingly flaky, but not enough to be marked as such upstream.
418 "test-tick-processor-arguments"
419 "test-set-raw-mode-reset-signal"
420 ]
421 # Those are annoyingly flaky, but not enough to be marked as such upstream.
422 ++ lib.optional (majorVersion == "22") "test-child-process-stdout-flush-exit"
423 )
424 }"
425 ];
426
427 sandboxProfile = ''
428 (allow file-read*
429 (literal "/Library/Keychains/System.keychain")
430 (literal "/private/var/db/mds/system/mdsDirectory.db")
431 (literal "/private/var/db/mds/system/mdsObject.db"))
432
433 ; Allow files written by Module Directory Services (MDS), which is used
434 ; by Security.framework: https://apple.stackexchange.com/a/411476
435 ; These rules are based on the system sandbox profiles found in
436 ; /System/Library/Sandbox/Profiles.
437 (allow file-write*
438 (regex #"^/private/var/folders/[^/]+/[^/]+/C/mds/mdsDirectory\.db$")
439 (regex #"^/private/var/folders/[^/]+/[^/]+/C/mds/mdsObject\.db_?$")
440 (regex #"^/private/var/folders/[^/]+/[^/]+/C/mds/mds\.lock$"))
441
442 (allow mach-lookup
443 (global-name "com.apple.FSEvents")
444 (global-name "com.apple.SecurityServer")
445 (global-name "com.apple.system.opendirectoryd.membership"))
446 '';
447
448 postInstall =
449 let
450 # nodejs_18 does not have node_js2c, and we don't want to rebuild the other ones
451 # FIXME: fix this cleanly in staging
452 tools =
453 if majorVersion == "18" then
454 "{bytecode_builtins_list_generator,mksnapshot,torque,gen-regexp-special-case}"
455 else
456 "{bytecode_builtins_list_generator,mksnapshot,torque,node_js2c,gen-regexp-special-case}";
457 in
458 lib.optionalString (stdenv.hostPlatform == stdenv.buildPlatform) ''
459 mkdir -p $dev/bin
460 cp out/Release/${tools} $dev/bin
461 ''
462 + ''
463
464 HOST_PATH=$out/bin patchShebangs --host $out
465
466 ${lib.optionalString canExecute ''
467 $out/bin/node --completion-bash > node.bash
468 installShellCompletion node.bash
469 ''}
470
471 ${lib.optionalString enableNpm ''
472 mkdir -p $out/share/bash-completion/completions
473 ln -s $out/lib/node_modules/npm/lib/utils/completion.sh \
474 $out/share/bash-completion/completions/npm
475 for dir in "$out/lib/node_modules/npm/man/"*; do
476 mkdir -p $out/share/man/$(basename "$dir")
477 for page in "$dir"/*; do
478 ln -rs $page $out/share/man/$(basename "$dir")
479 done
480 done
481 ''}
482
483 # install the missing headers for node-gyp
484 # TODO: use propagatedBuildInputs instead of copying headers.
485 cp -r ${lib.concatStringsSep " " copyLibHeaders} $out/include/node
486
487 # assemble a static v8 library and put it in the 'libv8' output
488 mkdir -p $libv8/lib
489 pushd out/Release/obj
490 find . -path "**/torque_*/**/*.o" -or -path "**/v8*/**/*.o" \
491 -and -not -name "torque.*" \
492 -and -not -name "mksnapshot.*" \
493 -and -not -name "gen-regexp-special-case.*" \
494 -and -not -name "bytecode_builtins_list_generator.*" \
495 | sort -u >files
496 test -s files # ensure that the list is not empty
497 $AR -cqs $libv8/lib/libv8.a @files
498 popd
499
500 # copy v8 headers
501 cp -r deps/v8/include $libv8/
502
503 # create a pkgconfig file for v8
504 major=$(grep V8_MAJOR_VERSION deps/v8/include/v8-version.h | cut -d ' ' -f 3)
505 minor=$(grep V8_MINOR_VERSION deps/v8/include/v8-version.h | cut -d ' ' -f 3)
506 patch=$(grep V8_PATCH_LEVEL deps/v8/include/v8-version.h | cut -d ' ' -f 3)
507 mkdir -p $libv8/lib/pkgconfig
508 cat > $libv8/lib/pkgconfig/v8.pc << EOF
509 Name: v8
510 Description: V8 JavaScript Engine
511 Version: $major.$minor.$patch
512 Libs: -L$libv8/lib -lv8 -pthread -licui18n -licuuc
513 Cflags: -I$libv8/include
514 EOF
515 ''
516 + lib.optionalString (stdenv.hostPlatform == stdenv.buildPlatform) ''
517 cp -r $out/include $dev/include
518 '';
519
520 passthru.tests = {
521 version = testers.testVersion {
522 package = self;
523 version = "v${lib.head (lib.strings.splitString "-rc." version)}";
524 };
525 };
526
527 passthru.updateScript = import ./update.nix {
528 inherit
529 writeScript
530 common-updater-scripts
531 coreutils
532 curl
533 fetchurl
534 gnugrep
535 gnupg
536 jq
537 runtimeShell
538 ;
539 inherit lib;
540 inherit majorVersion;
541 };
542
543 meta = with lib; {
544 description = "Event-driven I/O framework for the V8 JavaScript engine";
545 homepage = "https://nodejs.org";
546 changelog = "https://github.com/nodejs/node/releases/tag/v${version}";
547 license = licenses.mit;
548 maintainers = with maintainers; [ aduh95 ];
549 platforms = platforms.linux ++ platforms.darwin ++ platforms.freebsd;
550 # This broken condition is likely too conservative. Feel free to loosen it if it works.
551 broken =
552 !canExecute && !canEmulate && (stdenv.buildPlatform.parsed.cpu != stdenv.hostPlatform.parsed.cpu);
553 mainProgram = "node";
554 knownVulnerabilities = optional (versionOlder version "18") "This NodeJS release has reached its end of life. See https://nodejs.org/en/about/releases/.";
555 };
556
557 passthru.python = python; # to ensure nodeEnv uses the same version
558 }
559 );
560in
561package