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