nodejs: fix cross compilation for armv7l

The v8 library supports only limited number of build platforms based on
the host architecture. The rule there is the bitness (the mixing of
32bit and 64bit architectures seems to be in most cases disallowed).
Thus this adds usage of the emulator of the host platform and builds
tools for that.

Co-authored-by: Ivan Trubach <mr.trubach@icloud.com>

+138
pkgs/development/web/nodejs/configure-emulator-node18.patch
···
+
From 4b83f714c821d6d4d2306673ee3a87907cfec80e Mon Sep 17 00:00:00 2001
+
From: Ivan Trubach <mr.trubach@icloud.com>
+
Date: Fri, 19 Jul 2024 10:45:13 +0300
+
Subject: [PATCH] build: support setting an emulator from configure script
+
MIME-Version: 1.0
+
Content-Type: text/plain; charset=UTF-8
+
Content-Transfer-Encoding: 8bit
+
+
V8’s JIT infrastructure requires binaries such as mksnapshot to be run
+
during the build. However, these binaries must have the same bit-width
+
as the host platform (e.g. a x86_64 build platform targeting ARMv6 needs
+
to produce a 32-bit binary).
+
+
To work around this issue, allow building the binaries for the host
+
platform and running them on the build platform with an emulator.
+
+
Based on Buildroot’s nodejs-src 0001-add-qemu-wrapper-support.patch.
+
https://gitlab.com/buildroot.org/buildroot/-/blob/c1d5eada4d4db9eeaa1c44dd1dea95a67c8a70ca/package/nodejs/nodejs-src/0001-add-qemu-wrapper-support.patch
+
+
Upstream: https://github.com/nodejs/node/pull/53899
+
---
+
common.gypi | 1 +
+
configure.py | 14 ++++++++++++++
+
node.gyp | 3 +++
+
tools/v8_gypfiles/v8.gyp | 4 ++++
+
4 files changed, 22 insertions(+)
+
+
diff --git a/common.gypi b/common.gypi
+
index ec92c9df4c..6474495ab6 100644
+
--- a/common.gypi
+
+++ b/common.gypi
+
@@ -13,6 +13,7 @@
+
'enable_pgo_generate%': '0',
+
'enable_pgo_use%': '0',
+
'python%': 'python',
+
+ 'emulator%': [],
+
+
'node_shared%': 'false',
+
'force_dynamic_crt%': 0,
+
diff --git a/configure.py b/configure.py
+
index 82916748fd..10dc0becbb 100755
+
--- a/configure.py
+
+++ b/configure.py
+
@@ -112,6 +112,12 @@ parser.add_argument('--dest-cpu',
+
choices=valid_arch,
+
help=f"CPU architecture to build for ({', '.join(valid_arch)})")
+
+
+parser.add_argument('--emulator',
+
+ action='store',
+
+ dest='emulator',
+
+ default=None,
+
+ help='emulator command that can run executables built for the target system')
+
+
+
parser.add_argument('--cross-compiling',
+
action='store_true',
+
dest='cross_compiling',
+
@@ -2160,6 +2166,14 @@ write('config.mk', do_not_edit + config_str)
+
gyp_args = ['--no-parallel', '-Dconfiguring_node=1']
+
gyp_args += ['-Dbuild_type=' + config['BUILDTYPE']]
+
+
+if options.emulator is not None:
+
+ if not options.cross_compiling:
+
+ # Note that emulator is a list so we have to quote the variable.
+
+ gyp_args += ['-Demulator=' + shlex.quote(options.emulator)]
+
+ else:
+
+ # TODO: perhaps use emulator for tests?
+
+ warn('The `--emulator` option has no effect when cross-compiling.')
+
+
+
if options.use_ninja:
+
gyp_args += ['-f', 'ninja-' + flavor]
+
elif flavor == 'win' and sys.platform != 'msys':
+
diff --git a/node.gyp b/node.gyp
+
index 08cb3f38e8..515b305933 100644
+
--- a/node.gyp
+
+++ b/node.gyp
+
@@ -332,6 +332,7 @@
+
'<(SHARED_INTERMEDIATE_DIR)/node_snapshot.cc',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(node_mksnapshot_exec)',
+
'--build-snapshot',
+
'<(node_snapshot_main)',
+
@@ -351,6 +352,7 @@
+
'<(SHARED_INTERMEDIATE_DIR)/node_snapshot.cc',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
],
+
@@ -1520,6 +1522,7 @@
+
'<(PRODUCT_DIR)/<(node_core_target_name).def',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(PRODUCT_DIR)/gen_node_def.exe',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
diff --git a/tools/v8_gypfiles/v8.gyp b/tools/v8_gypfiles/v8.gyp
+
index ba8b161f0f..d5c90dad50 100644
+
--- a/tools/v8_gypfiles/v8.gyp
+
+++ b/tools/v8_gypfiles/v8.gyp
+
@@ -99,6 +99,7 @@
+
'<@(torque_outputs_inc)',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)torque<(EXECUTABLE_SUFFIX)',
+
'-o', '<(SHARED_INTERMEDIATE_DIR)/torque-generated',
+
'-v8-root', '<(V8_ROOT)',
+
@@ -219,6 +220,7 @@
+
'action': [
+
'<(python)',
+
'<(V8_ROOT)/tools/run.py',
+
+ '<@(emulator)',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
],
+
@@ -442,6 +444,7 @@
+
}],
+
],
+
'action': [
+
+ '<@(emulator)',
+
'>@(_inputs)',
+
'>@(mksnapshot_flags)',
+
],
+
@@ -1577,6 +1580,7 @@
+
'action': [
+
'<(python)',
+
'<(V8_ROOT)/tools/run.py',
+
+ '<@(emulator)',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
],
+
--
+
2.44.1
+
+146
pkgs/development/web/nodejs/configure-emulator.patch
···
+
From 999d918bc8fefec1752243743a47c0ce5380bcec Mon Sep 17 00:00:00 2001
+
From: Ivan Trubach <mr.trubach@icloud.com>
+
Date: Wed, 17 Jul 2024 10:16:02 +0300
+
Subject: [PATCH] build: support setting an emulator from configure script
+
MIME-Version: 1.0
+
Content-Type: text/plain; charset=UTF-8
+
Content-Transfer-Encoding: 8bit
+
+
V8’s JIT infrastructure requires binaries such as mksnapshot to be run
+
during the build. However, these binaries must have the same bit-width
+
as the host platform (e.g. a x86_64 build platform targeting ARMv6 needs
+
to produce a 32-bit binary).
+
+
To work around this issue, allow building the binaries for the host
+
platform and running them on the build platform with an emulator.
+
+
Based on Buildroot’s nodejs-src 0001-add-qemu-wrapper-support.patch.
+
https://gitlab.com/buildroot.org/buildroot/-/blob/c1d5eada4d4db9eeaa1c44dd1dea95a67c8a70ca/package/nodejs/nodejs-src/0001-add-qemu-wrapper-support.patch
+
+
Upstream: https://github.com/nodejs/node/pull/53899
+
---
+
common.gypi | 1 +
+
configure.py | 14 ++++++++++++++
+
node.gyp | 4 ++++
+
tools/v8_gypfiles/v8.gyp | 4 ++++
+
4 files changed, 23 insertions(+)
+
+
diff --git a/common.gypi b/common.gypi
+
index 154bbf2a0d..54d2afe3b3 100644
+
--- a/common.gypi
+
+++ b/common.gypi
+
@@ -13,6 +13,7 @@
+
'enable_pgo_generate%': '0',
+
'enable_pgo_use%': '0',
+
'python%': 'python',
+
+ 'emulator%': [],
+
+
'node_shared%': 'false',
+
'force_dynamic_crt%': 0,
+
diff --git a/configure.py b/configure.py
+
index f7e3310723..f7c7acdf4f 100755
+
--- a/configure.py
+
+++ b/configure.py
+
@@ -112,6 +112,12 @@ parser.add_argument('--dest-cpu',
+
choices=valid_arch,
+
help=f"CPU architecture to build for ({', '.join(valid_arch)})")
+
+
+parser.add_argument('--emulator',
+
+ action='store',
+
+ dest='emulator',
+
+ default=None,
+
+ help='emulator command that can run executables built for the target system')
+
+
+
parser.add_argument('--cross-compiling',
+
action='store_true',
+
dest='cross_compiling',
+
@@ -2276,6 +2282,14 @@ if flavor == 'win' and python.lower().endswith('.exe'):
+
# will fail to run python scripts.
+
gyp_args += ['-Dpython=' + python]
+
+
+if options.emulator is not None:
+
+ if not options.cross_compiling:
+
+ # Note that emulator is a list so we have to quote the variable.
+
+ gyp_args += ['-Demulator=' + shlex.quote(options.emulator)]
+
+ else:
+
+ # TODO: perhaps use emulator for tests?
+
+ warn('The `--emulator` option has no effect when cross-compiling.')
+
+
+
if options.use_ninja:
+
gyp_args += ['-f', 'ninja-' + flavor]
+
elif flavor == 'win' and sys.platform != 'msys':
+
diff --git a/node.gyp b/node.gyp
+
index 9617596760..439c76aca6 100644
+
--- a/node.gyp
+
+++ b/node.gyp
+
@@ -703,6 +703,7 @@
+
'<(SHARED_INTERMEDIATE_DIR)/node_snapshot.cc',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(node_mksnapshot_exec)',
+
'--build-snapshot',
+
'<(node_snapshot_main)',
+
@@ -722,6 +723,7 @@
+
'<(SHARED_INTERMEDIATE_DIR)/node_snapshot.cc',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
],
+
@@ -1010,6 +1012,7 @@
+
'<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(node_js2c_exec)',
+
'<@(_outputs)',
+
'lib',
+
@@ -1477,6 +1480,7 @@
+
'<(PRODUCT_DIR)/<(node_core_target_name).def',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(PRODUCT_DIR)/gen_node_def.exe',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
diff --git a/tools/v8_gypfiles/v8.gyp b/tools/v8_gypfiles/v8.gyp
+
index d65a5c268e..5cd6c36b86 100644
+
--- a/tools/v8_gypfiles/v8.gyp
+
+++ b/tools/v8_gypfiles/v8.gyp
+
@@ -112,6 +112,7 @@
+
'<@(torque_outputs_inc)',
+
],
+
'action': [
+
+ '<@(emulator)',
+
'<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)torque<(EXECUTABLE_SUFFIX)',
+
'-o', '<(SHARED_INTERMEDIATE_DIR)/torque-generated',
+
'-v8-root', '<(V8_ROOT)',
+
@@ -232,6 +233,7 @@
+
'action': [
+
'<(python)',
+
'<(V8_ROOT)/tools/run.py',
+
+ '<@(emulator)',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
],
+
@@ -453,6 +455,7 @@
+
}],
+
],
+
'action': [
+
+ '<@(emulator)',
+
'>@(_inputs)',
+
'>@(mksnapshot_flags)',
+
],
+
@@ -1842,6 +1845,7 @@
+
'action': [
+
'<(python)',
+
'<(V8_ROOT)/tools/run.py',
+
+ '<@(emulator)',
+
'<@(_inputs)',
+
'<@(_outputs)',
+
],
+
--
+
2.44.1
+
+118 -73
pkgs/development/web/nodejs/nodejs.nix
···
-
{ lib, stdenv, fetchurl, openssl, python, zlib, libuv, util-linux, http-parser, bash
+
{ lib, stdenv, fetchurl, openssl, python, zlib, libuv, http-parser, icu, bash
, pkg-config, which, buildPackages
, testers
# for `.pkgs` attribute
···
, writeScript, coreutils, gnugrep, jq, curl, common-updater-scripts, nix, runtimeShell
, gnupg
, darwin, xcbuild
-
, procps, icu
, installShellFiles
}:
···
let
inherit (darwin.apple_sdk.frameworks) CoreServices ApplicationServices;
-
isCross = stdenv.hostPlatform != stdenv.buildPlatform;
-
majorVersion = lib.versions.major version;
minorVersion = lib.versions.minor version;
pname = if enableNpm then "nodejs" else "nodejs-slim";
+
canExecute = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
+
emulator = stdenv.hostPlatform.emulator buildPackages;
+
+
# See valid_os and valid_arch in configure.py.
+
destOS =
+
let
+
platform = stdenv.hostPlatform;
+
in
+
if platform.isiOS then
+
"ios"
+
else if platform.isAndroid then
+
"android"
+
else if platform.isWindows then
+
"win"
+
else if platform.isDarwin then
+
"mac"
+
else if platform.isLinux then
+
"linux"
+
else if platform.isOpenBSD then
+
"openbsd"
+
else if platform.isFreeBSD then
+
"freebsd"
+
else
+
throw "unsupported os ${platform.uname.system}";
+
destCPU =
+
let
+
platform = stdenv.hostPlatform;
+
in
+
if platform.isAarch then
+
"arm" + lib.optionalString platform.is64bit "64"
+
else if platform.isMips32 then
+
"mips" + lib.optionalString platform.isLittleEndian "le"
+
else if platform.isMips64 && platform.isLittleEndian then
+
"mips64el"
+
else if platform.isPower then
+
"ppc" + lib.optionalString platform.is64bit "64"
+
else if platform.isx86_64 then
+
"x64"
+
else if platform.isx86_32 then
+
"ia32"
+
else if platform.isS390x then
+
"s390x"
+
else if platform.isRiscV64 then
+
"riscv64"
+
else if platform.isLoongArch64 then
+
"loong64"
+
else
+
throw "unsupported cpu ${platform.uname.processor}";
+
destARMFPU =
+
let
+
platform = stdenv.hostPlatform;
+
in
+
if platform.isAarch32 && platform ? gcc.fpu then
+
lib.throwIfNot (builtins.elem platform.gcc.fpu [
+
"vfp"
+
"vfpv3"
+
"vfpv3-d16"
+
"neon"
+
]) "unsupported ARM FPU ${platform.gcc.fpu}" platform.gcc.fpu
+
else
+
null;
+
destARMFloatABI =
+
let
+
platform = stdenv.hostPlatform;
+
in
+
if platform.isAarch32 && platform ? gcc.float-abi then
+
lib.throwIfNot (builtins.elem platform.gcc.float-abi [
+
"soft"
+
"softfp"
+
"hard"
+
]) "unsupported ARM float ABI ${platform.gcc.float-abi}" platform.gcc.float-abi
+
else
+
null;
+
# TODO: also handle MIPS flags (mips_arch, mips_fpu, mips_float_abi).
+
useSharedHttpParser = !stdenv.isDarwin && lib.versionOlder "${majorVersion}.${minorVersion}" "11.4";
sharedLibDeps = { inherit openssl zlib libuv; } // (lib.optionalAttrs useSharedHttpParser { inherit http-parser; });
-
sharedConfigureFlags = lib.concatMap (name: [
-
"--shared-${name}"
-
"--shared-${name}-libpath=${lib.getLib sharedLibDeps.${name}}/lib"
-
/** Closure notes: we explicitly avoid specifying --shared-*-includes,
-
* as that would put the paths into bin/nodejs.
-
* Including pkg-config in build inputs would also have the same effect!
-
*/
-
]) (builtins.attrNames sharedLibDeps) ++ [
-
"--with-intl=system-icu"
-
"--openssl-use-def-ca-store"
-
];
-
copyLibHeaders =
map
(name: "${lib.getDev sharedLibDeps.${name}}/include/*")
(builtins.attrNames sharedLibDeps);
-
-
extraConfigFlags = lib.optionals (!enableNpm) [ "--without-npm" ];
package = stdenv.mkDerivation (finalAttrs:
let
···
# Otherwise, nodejs would require the 11.0 SDK and macOS 10.15+.
NIX_CFLAGS_COMPILE = "-D__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__=101300";
};
-
-
depsBuildBuild = [ buildPackages.stdenv.cc openssl libuv zlib icu ];
# NB: technically, we do not need bash in build inputs since all scripts are
# wrappers over the corresponding JS scripts. There are some packages though
···
setOutputFlags = false;
moveToDev = false;
-
configureFlags = let
-
inherit (stdenv.hostPlatform) gcc isAarch32;
-
in sharedConfigureFlags ++ lib.optionals (lib.versionOlder version "19") [
-
"--without-dtrace"
-
] ++ (lib.optionals isCross [
-
"--cross-compiling"
-
"--dest-cpu=${let platform = stdenv.hostPlatform; in
-
if platform.isAarch32 then "arm"
-
else if platform.isAarch64 then "arm64"
-
else if platform.isMips32 && platform.isLittleEndian then "mipsel"
-
else if platform.isMips32 && !platform.isLittleEndian then "mips"
-
else if platform.isMips64 && platform.isLittleEndian then "mips64el"
-
else if platform.isPower && platform.is32bit then "ppc"
-
else if platform.isPower && platform.is64bit then "ppc64"
-
else if platform.isx86_64 then "x86_64"
-
else if platform.isx86_32 then "x86"
-
else if platform.isS390 && platform.is64bit then "s390x"
-
else if platform.isRiscV && platform.is64bit then "riscv64"
-
else throw "unsupported cpu ${stdenv.hostPlatform.uname.processor}"}"
-
]) ++ (lib.optionals (isCross && isAarch32 && lib.hasAttr "fpu" gcc) [
-
"--with-arm-fpu=${gcc.fpu}"
-
]) ++ (lib.optionals (isCross && isAarch32 && lib.hasAttr "float-abi" gcc) [
-
"--with-arm-float-abi=${gcc.float-abi}"
-
]) ++ extraConfigFlags;
+
configureFlags =
+
[
+
"--no-cross-compiling"
+
"--dest-os=${destOS}"
+
"--dest-cpu=${destCPU}"
+
]
+
++ lib.optionals (destARMFPU != null) [ "--with-arm-fpu=${destARMFPU}" ]
+
++ lib.optionals (destARMFloatABI != null) [ "--with-arm-float-abi=${destARMFloatABI}" ]
+
++ lib.optionals (!canExecute) [
+
# Node.js requires matching bitness between build and host platforms, e.g.
+
# for V8 startup snapshot builder (see tools/snapshot) and some other
+
# tools. We apply a patch that runs these tools using a host platform
+
# emulator and avoid cross-compiling altogether (from the build system’s
+
# perspective).
+
"--emulator=${emulator}"
+
]
+
++ lib.optionals (lib.versionOlder version "19") [ "--without-dtrace" ]
+
++ lib.optionals (!enableNpm) [ "--without-npm" ]
+
++ lib.concatMap (name: [
+
"--shared-${name}"
+
"--shared-${name}-libpath=${lib.getLib sharedLibDeps.${name}}/lib"
+
/**
+
Closure notes: we explicitly avoid specifying --shared-*-includes,
+
as that would put the paths into bin/nodejs.
+
Including pkg-config in build inputs would also have the same effect!
+
+
FIXME: the statement above is outdated, we have to include pkg-config
+
in build inputs for system-icu.
+
*/
+
]) (builtins.attrNames sharedLibDeps)
+
++ [
+
"--with-intl=system-icu"
+
"--openssl-use-def-ca-store"
+
];
-
configurePlatforms = [];
+
configurePlatforms = [ ];
dontDisableStatic = true;
configureScript = writeScript "nodejs-configure" ''
-
export CC_host="$CC_FOR_BUILD" CXX_host="$CXX_FOR_BUILD"
exec ${python.executable} configure.py "$@"
'';
···
__darwinAllowLocalNetworking = true; # for tests
-
# TODO: what about tests when cross-compiling?
-
# Note that currently stdenv does not run check phase if build ≠ host.
-
doCheck = true;
+
doCheck = canExecute;
# Some dependencies required for tools/doc/node_modules (and therefore
# test-addons, jstest and others) target are not included in the tarball.
···
postInstall = ''
HOST_PATH=$out/bin patchShebangs --host $out
-
${lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
-
$out/bin/${self.meta.mainProgram} --completion-bash > ${self.meta.mainProgram}.bash
-
installShellCompletion ${self.meta.mainProgram}.bash
+
${lib.optionalString canExecute ''
+
$out/bin/node --completion-bash > node.bash
+
installShellCompletion node.bash
''}
-
${lib.optionalString (enableNpm) ''
+
${lib.optionalString enableNpm ''
mkdir -p $out/share/bash-completion/completions
ln -s $out/lib/node_modules/npm/lib/utils/completion.sh \
$out/share/bash-completion/completions/npm
···
''}
# install the missing headers for node-gyp
+
# TODO: add dev output and use propagatedBuildInputs instead of copying headers.
cp -r ${lib.concatStringsSep " " copyLibHeaders} $out/include/node
# assemble a static v8 library and put it in the 'libv8' output
mkdir -p $libv8/lib
pushd out/Release/obj.target
find . -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" | sort -u >files
-
${if stdenv.buildPlatform.isGnu then ''
-
ar -cqs $libv8/lib/libv8.a @files
-
'' else ''
-
# llvm-ar supports response files, so take advantage of it if it’s available.
-
if [ "$(basename $(readlink -f $(command -v ar)))" = "llvm-ar" ]; then
-
ar -cqs $libv8/lib/libv8.a @files
-
else
-
cat files | while read -r file; do
-
ar -cqS $libv8/lib/libv8.a $file
-
done
-
fi
-
''}
+
$AR -cqs $libv8/lib/libv8.a @files
popd
# copy v8 headers
···
platforms = platforms.linux ++ platforms.darwin;
mainProgram = "node";
knownVulnerabilities = optional (versionOlder version "18") "This NodeJS release has reached its end of life. See https://nodejs.org/en/about/releases/.";
-
-
# Node.js build system does not have separate host and target OS
-
# configurations (architectures are defined as host_arch and target_arch,
-
# but there is no such thing as host_os and target_os).
-
#
-
# We may be missing something here, but it doesn’t look like it is
-
# possible to cross-compile between different operating systems.
-
broken = stdenv.buildPlatform.parsed.kernel.name != stdenv.hostPlatform.parsed.kernel.name;
};
passthru.python = python; # to ensure nodeEnv uses the same version
+1
pkgs/development/web/nodejs/v18.nix
···
version = "18.20.4";
sha256 = "sha256-p2x+oblq62ljoViAYmDICUtiRNZKaWUp0CBUe5qVyio=";
patches = [
+
./configure-emulator-node18.patch
./disable-darwin-v8-system-instrumentation.patch
./bypass-darwin-xcrun-node16.patch
./revert-arm64-pointer-auth.patch
+1
pkgs/development/web/nodejs/v20.nix
···
version = "20.15.1";
sha256 = "sha256-/dU6VynZNmkaKhFRBG+0iXchy4sPyir5V4I6m0D+DDQ=";
patches = [
+
./configure-emulator.patch
./disable-darwin-v8-system-instrumentation-node19.patch
./bypass-darwin-xcrun-node16.patch
./node-npm-build-npm-package-logic.patch
+1
pkgs/development/web/nodejs/v22.nix
···
version = "22.4.1";
sha256 = "sha256-ZfyFf1qoJWqvyQCzRMARXJrq4loCVB/Vzg29Tf0cX7k=";
patches = [
+
./configure-emulator.patch
./disable-darwin-v8-system-instrumentation-node19.patch
./bypass-darwin-xcrun-node16.patch
./node-npm-build-npm-package-logic.patch