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