1{ lib, stdenv, fetchurl, openssl, python, zlib, libuv, http-parser, icu, bash
2, ninja, pkgconf, unixtools, runCommand, buildPackages
3, testers
4# for `.pkgs` attribute
5, callPackage
6# Updater dependencies
7, writeScript, coreutils, gnugrep, jq, curl, common-updater-scripts, nix, runtimeShell
8, gnupg
9, darwin
10, installShellFiles
11}:
12
13{ enableNpm ? true, version, sha256, patches ? [] } @args:
14
15let
16 inherit (darwin.apple_sdk.frameworks) CoreServices ApplicationServices;
17
18 majorVersion = lib.versions.major version;
19 minorVersion = lib.versions.minor version;
20
21 pname = if enableNpm then "nodejs" else "nodejs-slim";
22
23 canExecute = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
24 emulator = stdenv.hostPlatform.emulator buildPackages;
25
26 # See valid_os and valid_arch in configure.py.
27 destOS =
28 let
29 platform = stdenv.hostPlatform;
30 in
31 if platform.isiOS then
32 "ios"
33 else if platform.isAndroid then
34 "android"
35 else if platform.isWindows then
36 "win"
37 else if platform.isDarwin then
38 "mac"
39 else if platform.isLinux then
40 "linux"
41 else if platform.isOpenBSD then
42 "openbsd"
43 else if platform.isFreeBSD then
44 "freebsd"
45 else
46 throw "unsupported os ${platform.uname.system}";
47 destCPU =
48 let
49 platform = stdenv.hostPlatform;
50 in
51 if platform.isAarch then
52 "arm" + lib.optionalString platform.is64bit "64"
53 else if platform.isMips32 then
54 "mips" + lib.optionalString platform.isLittleEndian "le"
55 else if platform.isMips64 && platform.isLittleEndian then
56 "mips64el"
57 else if platform.isPower then
58 "ppc" + lib.optionalString platform.is64bit "64"
59 else if platform.isx86_64 then
60 "x64"
61 else if platform.isx86_32 then
62 "ia32"
63 else if platform.isS390x then
64 "s390x"
65 else if platform.isRiscV64 then
66 "riscv64"
67 else if platform.isLoongArch64 then
68 "loong64"
69 else
70 throw "unsupported cpu ${platform.uname.processor}";
71 destARMFPU =
72 let
73 platform = stdenv.hostPlatform;
74 in
75 if platform.isAarch32 && platform ? gcc.fpu then
76 lib.throwIfNot (builtins.elem platform.gcc.fpu [
77 "vfp"
78 "vfpv2"
79 "vfpv3"
80 "vfpv3-d16"
81 "neon"
82 ]) "unsupported ARM FPU ${platform.gcc.fpu}" platform.gcc.fpu
83 else
84 null;
85 destARMFloatABI =
86 let
87 platform = stdenv.hostPlatform;
88 in
89 if platform.isAarch32 && platform ? gcc.float-abi then
90 lib.throwIfNot (builtins.elem platform.gcc.float-abi [
91 "soft"
92 "softfp"
93 "hard"
94 ]) "unsupported ARM float ABI ${platform.gcc.float-abi}" platform.gcc.float-abi
95 else
96 null;
97 # TODO: also handle MIPS flags (mips_arch, mips_fpu, mips_float_abi).
98
99 useSharedHttpParser = !stdenv.hostPlatform.isDarwin && lib.versionOlder "${majorVersion}.${minorVersion}" "11.4";
100
101 sharedLibDeps = { inherit openssl zlib libuv; } // (lib.optionalAttrs useSharedHttpParser { inherit http-parser; });
102
103 copyLibHeaders =
104 map
105 (name: "${lib.getDev sharedLibDeps.${name}}/include/*")
106 (builtins.attrNames sharedLibDeps);
107
108 # Currently stdenv sets CC/LD/AR/etc environment variables to program names
109 # instead of absolute paths. If we add cctools to nativeBuildInputs, that
110 # would shadow stdenv’s bintools and potentially break other parts of the
111 # build. The correct behavior is to use absolute paths, and there is a PR for
112 # that, see https://github.com/NixOS/nixpkgs/pull/314920. As a temporary
113 # workaround, we use only a single program we need (and that is not part of
114 # the stdenv).
115 darwin-cctools-only-libtool =
116 # Would be nice to have onlyExe builder similar to onlyBin…
117 runCommand "darwin-cctools-only-libtool" { cctools = lib.getBin buildPackages.cctools; } ''
118 mkdir -p "$out/bin"
119 ln -s "$cctools/bin/libtool" "$out/bin/libtool"
120 '';
121
122 package = stdenv.mkDerivation (finalAttrs:
123 let
124 /** the final package fixed point, after potential overrides */
125 self = finalAttrs.finalPackage;
126 in
127 {
128 inherit pname version;
129
130 src = fetchurl {
131 url = "https://nodejs.org/dist/v${version}/node-v${version}.tar.xz";
132 inherit sha256;
133 };
134
135 strictDeps = true;
136
137 env = {
138 # Tell ninja to avoid ANSI sequences, otherwise we don’t see build
139 # progress in Nix logs.
140 #
141 # Note: do not set TERM=dumb environment variable globally, it is used in
142 # test-ci-js test suite to skip tests that otherwise run fine.
143 NINJA = "TERM=dumb ninja";
144 } // lib.optionalAttrs (stdenv.hostPlatform.isDarwin && stdenv.hostPlatform.isx86_64) {
145 # Make sure libc++ uses `posix_memalign` instead of `aligned_alloc` on x86_64-darwin.
146 # Otherwise, nodejs would require the 11.0 SDK and macOS 10.15+.
147 NIX_CFLAGS_COMPILE = "-D__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__=101300 -Wno-macro-redefined";
148 };
149
150 # NB: technically, we do not need bash in build inputs since all scripts are
151 # wrappers over the corresponding JS scripts. There are some packages though
152 # that use bash wrappers, e.g. polaris-web.
153 buildInputs = lib.optionals stdenv.hostPlatform.isDarwin [ CoreServices ApplicationServices ]
154 ++ [ zlib libuv openssl http-parser icu bash ];
155
156 nativeBuildInputs =
157 [
158 installShellFiles
159 ninja
160 pkgconf
161 python
162 ]
163 ++ lib.optionals stdenv.buildPlatform.isDarwin [
164 # gyp checks `sysctl -n hw.memsize` if `sys.platform == "darwin"`.
165 unixtools.sysctl
166 ]
167 ++ lib.optionals stdenv.hostPlatform.isDarwin [
168 # For gyp-mac-tool if `flavor == "mac"`.
169 darwin-cctools-only-libtool
170 ];
171
172 # We currently rely on Makefile and stdenv for build phases, so do not let
173 # ninja’s setup hook to override default stdenv phases.
174 dontUseNinjaBuild = true;
175 dontUseNinjaCheck = true;
176 dontUseNinjaInstall = true;
177
178 outputs = [ "out" "libv8" ];
179 setOutputFlags = false;
180 moveToDev = false;
181
182 configureFlags =
183 [
184 "--ninja"
185 "--with-intl=system-icu"
186 "--openssl-use-def-ca-store"
187 "--no-cross-compiling"
188 "--dest-os=${destOS}"
189 "--dest-cpu=${destCPU}"
190 ]
191 ++ lib.optionals (destARMFPU != null) [ "--with-arm-fpu=${destARMFPU}" ]
192 ++ lib.optionals (destARMFloatABI != null) [ "--with-arm-float-abi=${destARMFloatABI}" ]
193 ++ lib.optionals (!canExecute) [
194 # Node.js requires matching bitness between build and host platforms, e.g.
195 # for V8 startup snapshot builder (see tools/snapshot) and some other
196 # tools. We apply a patch that runs these tools using a host platform
197 # emulator and avoid cross-compiling altogether (from the build system’s
198 # perspective).
199 "--emulator=${emulator}"
200 ]
201 ++ lib.optionals (lib.versionOlder version "19") [ "--without-dtrace" ]
202 ++ lib.optionals (!enableNpm) [ "--without-npm" ]
203 ++ lib.concatMap (name: [
204 "--shared-${name}"
205 "--shared-${name}-libpath=${lib.getLib sharedLibDeps.${name}}/lib"
206 /**
207 Closure notes: we explicitly avoid specifying --shared-*-includes,
208 as that would put the paths into bin/nodejs.
209 Including pkg-config in build inputs would also have the same effect!
210
211 FIXME: the statement above is outdated, we have to include pkg-config
212 in build inputs for system-icu.
213 */
214 ]) (builtins.attrNames sharedLibDeps);
215
216 configurePlatforms = [ ];
217
218 dontDisableStatic = true;
219
220 configureScript = writeScript "nodejs-configure" ''
221 exec ${python.executable} configure.py "$@"
222 '';
223
224 enableParallelBuilding = true;
225
226 # Don't allow enabling content addressed conversion as `nodejs`
227 # checksums it's image before conversion happens and image loading
228 # breaks:
229 # $ nix build -f. nodejs --arg config '{ contentAddressedByDefault = true; }'
230 # $ ./result/bin/node
231 # Check failed: VerifyChecksum(blob).
232 __contentAddressed = false;
233
234 passthru.interpreterName = "nodejs";
235
236 passthru.pkgs = callPackage ../../node-packages/default.nix {
237 nodejs = self;
238 };
239
240 setupHook = ./setup-hook.sh;
241
242 pos = builtins.unsafeGetAttrPos "version" args;
243
244 inherit patches;
245
246 __darwinAllowLocalNetworking = true; # for tests
247
248 doCheck = canExecute;
249
250 # See https://github.com/nodejs/node/issues/22006
251 enableParallelChecking = false;
252
253 # Some dependencies required for tools/doc/node_modules (and therefore
254 # test-addons, jstest and others) target are not included in the tarball.
255 # Run test targets that do not require network access.
256 checkTarget = lib.concatStringsSep " " ([
257 "build-js-native-api-tests"
258 "build-node-api-tests"
259 "tooltest"
260 "cctest"
261 ] ++ lib.optionals (!stdenv.buildPlatform.isDarwin || lib.versionAtLeast version "20") [
262 # There are some test failures on macOS before v20 that are not worth the
263 # time to debug for a version that would be eventually removed in less
264 # than a year (Node.js 18 will be EOL at 2025-04-30). Note that these
265 # failures are specific to Nix sandbox on macOS and should not affect
266 # actual functionality.
267 "test-ci-js"
268 ]);
269
270 checkFlags = [
271 # Do not create __pycache__ when running tests.
272 "PYTHONDONTWRITEBYTECODE=1"
273 ] ++ lib.optionals (stdenv.buildPlatform.isDarwin && stdenv.buildPlatform.isx86_64) [
274 # Python 3.12 introduced a warning for calling `os.fork()` in a
275 # multi‐threaded program. For some reason, the Node.js
276 # `tools/pseudo-tty.py` program used for PTY‐related tests
277 # triggers this warning on Hydra, on `x86_64-darwin` only,
278 # despite not creating any threads itself. This causes the
279 # Node.js test runner to misinterpret the warnings as part of the
280 # test output and fail. It does not reproduce reliably off Hydra
281 # on Intel Macs, or occur on the `aarch64-darwin` builds.
282 #
283 # This seems likely to be related to Rosetta 2, but it could also
284 # be some strange x86‐64‐only threading behaviour of the Darwin
285 # system libraries, or a bug in CPython, or something else
286 # haunted about the Nixpkgs/Hydra build environment. We silence
287 # the warnings in the hope that closing our eyes will make the
288 # ghosts go away.
289 "PYTHONWARNINGS=ignore::DeprecationWarning"
290 ] ++ lib.optionals (!stdenv.buildPlatform.isDarwin || lib.versionAtLeast version "20") [
291 "FLAKY_TESTS=skip"
292 # Skip some tests that are not passing in this context
293 "CI_SKIP_TESTS=${lib.concatStringsSep "," ([
294 "test-child-process-exec-env"
295 "test-child-process-uid-gid"
296 "test-fs-write-stream-eagain"
297 "test-process-euid-egid"
298 "test-process-initgroups"
299 "test-process-setgroups"
300 "test-process-uid-gid"
301 "test-setproctitle"
302 # This is a bit weird, but for some reason fs watch tests fail with
303 # sandbox.
304 "test-fs-promises-watch"
305 "test-fs-watch"
306 "test-fs-watch-encoding"
307 "test-fs-watch-non-recursive"
308 "test-fs-watch-recursive-add-file"
309 "test-fs-watch-recursive-add-file-to-existing-subfolder"
310 "test-fs-watch-recursive-add-file-to-new-folder"
311 "test-fs-watch-recursive-add-file-with-url"
312 "test-fs-watch-recursive-add-folder"
313 "test-fs-watch-recursive-assert-leaks"
314 "test-fs-watch-recursive-promise"
315 "test-fs-watch-recursive-symlink"
316 "test-fs-watch-recursive-sync-write"
317 "test-fs-watch-recursive-update-file"
318 "test-fs-watchfile"
319 "test-runner-run"
320 "test-runner-watch-mode"
321 "test-watch-mode-files_watcher"
322 ] ++ lib.optionals (!lib.versionAtLeast version "22") [
323 "test-tls-multi-key"
324 ] ++ lib.optionals stdenv.hostPlatform.is32bit [
325 # utime (actually utimensat) fails with EINVAL on 2038 timestamp
326 "test-fs-utimes-y2K38"
327 ] ++ lib.optionals stdenv.buildPlatform.isDarwin [
328 # Disable tests that don’t work under macOS sandbox.
329 "test-macos-app-sandbox"
330 "test-os"
331 "test-os-process-priority"
332 ] ++ lib.optionals (stdenv.buildPlatform.isDarwin && stdenv.buildPlatform.isx86_64) [
333 # These tests fail on x86_64-darwin (even without sandbox).
334 # TODO: revisit at a later date.
335 "test-fs-readv"
336 "test-fs-readv-sync"
337 "test-vm-memleak"
338 ])}"
339 ];
340
341 postInstall = ''
342 HOST_PATH=$out/bin patchShebangs --host $out
343
344 ${lib.optionalString canExecute ''
345 $out/bin/node --completion-bash > node.bash
346 installShellCompletion node.bash
347 ''}
348
349 ${lib.optionalString enableNpm ''
350 mkdir -p $out/share/bash-completion/completions
351 ln -s $out/lib/node_modules/npm/lib/utils/completion.sh \
352 $out/share/bash-completion/completions/npm
353 for dir in "$out/lib/node_modules/npm/man/"*; do
354 mkdir -p $out/share/man/$(basename "$dir")
355 for page in "$dir"/*; do
356 ln -rs $page $out/share/man/$(basename "$dir")
357 done
358 done
359 ''}
360
361 # install the missing headers for node-gyp
362 # TODO: add dev output and use propagatedBuildInputs instead of copying headers.
363 cp -r ${lib.concatStringsSep " " copyLibHeaders} $out/include/node
364
365 # assemble a static v8 library and put it in the 'libv8' output
366 mkdir -p $libv8/lib
367 pushd out/Release/obj
368 find . -path "**/torque_*/**/*.o" -or -path "**/v8*/**/*.o" \
369 -and -not -name "torque.*" \
370 -and -not -name "mksnapshot.*" \
371 -and -not -name "gen-regexp-special-case.*" \
372 -and -not -name "bytecode_builtins_list_generator.*" \
373 | sort -u >files
374 test -s files # ensure that the list is not empty
375 $AR -cqs $libv8/lib/libv8.a @files
376 popd
377
378 # copy v8 headers
379 cp -r deps/v8/include $libv8/
380
381 # create a pkgconfig file for v8
382 major=$(grep V8_MAJOR_VERSION deps/v8/include/v8-version.h | cut -d ' ' -f 3)
383 minor=$(grep V8_MINOR_VERSION deps/v8/include/v8-version.h | cut -d ' ' -f 3)
384 patch=$(grep V8_PATCH_LEVEL deps/v8/include/v8-version.h | cut -d ' ' -f 3)
385 mkdir -p $libv8/lib/pkgconfig
386 cat > $libv8/lib/pkgconfig/v8.pc << EOF
387 Name: v8
388 Description: V8 JavaScript Engine
389 Version: $major.$minor.$patch
390 Libs: -L$libv8/lib -lv8 -pthread -licui18n -licuuc
391 Cflags: -I$libv8/include
392 EOF
393 '';
394
395 passthru.tests = {
396 version = testers.testVersion {
397 package = self;
398 version = "v${version}";
399 };
400 };
401
402 passthru.updateScript = import ./update.nix {
403 inherit writeScript coreutils gnugrep jq curl common-updater-scripts gnupg nix runtimeShell;
404 inherit lib;
405 inherit majorVersion;
406 };
407
408 meta = with lib; {
409 description = "Event-driven I/O framework for the V8 JavaScript engine";
410 homepage = "https://nodejs.org";
411 changelog = "https://github.com/nodejs/node/releases/tag/v${version}";
412 license = licenses.mit;
413 maintainers = with maintainers; [ aduh95 ];
414 platforms = platforms.linux ++ platforms.darwin;
415 mainProgram = "node";
416 knownVulnerabilities = optional (versionOlder version "18") "This NodeJS release has reached its end of life. See https://nodejs.org/en/about/releases/.";
417 };
418
419 passthru.python = python; # to ensure nodeEnv uses the same version
420 });
421in package