Skip to content

Commit 3708414

Browse files
committed
Implement darwin cross-compile tooling for node-capnp
This commit introduces tooling to allow a linux host to compile node-capnp for a darwin target. To do this, tooling is also provided to allow for cross-compiling capnproto itself, as this didn't really seem to function before. It's all still a little bit hacky and relies upon you having the osxcross toolchain available, but I have at least a moderate belief that it might work. Approved-by: Henry Harrod
1 parent 4909074 commit 3708414

File tree

6 files changed

+218
-19
lines changed

6 files changed

+218
-19
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
node_modules
22
build
3+
capnp-darwin.env
4+
build-capnp/
5+
package-lock.json

README-DARWIN.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
So, you want to cross-compile for darwin? Have fun.
2+
3+
Jokes, I've done all the work for you. Simply:
4+
5+
0. Make the osxcross darwin clang toolchain available on your PATH (rewind-server has a handy script
6+
to do all the building for you)
7+
1. `./build-capnp-darwin.sh` (this builds capnproto so we have darwin libs and headers available)
8+
2. `. capnp-darwin.env` (this exports `CAPNP_LIBDIR` and `CAPNP_INCDIR` so capnp can be found by the node
9+
module build script)
10+
3. `npm run build:darwin` (this builds the capnp.node native module for darwin)
11+
4. ...
12+
5. Profit!!!
13+
14+
NOTE that the generated capnp dylibs will not yet have been patched up to have the correct link
15+
paths, and so are still named `*.so` - you'll want to use `install_name_tool` to fix them up before
16+
bundling them into your application. The node module binary itself *IS* patched by this tooling (see
17+
`build.js` for more fascinating details).
18+
19+
(ymmv, e&oe...)

build-capnp-darwin.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/bash
2+
3+
# Fourier Rewind
4+
# Copyright Fourier Audio Ltd. 2021. All Rights Reserved.
5+
6+
set -euf -o pipefail
7+
8+
o64-clang --version
9+
o64-clang++ --version
10+
11+
# Get directory of this script in a relatively robust fashion (pun intended); see
12+
# http://www.binaryphile.com/bash/2020/01/12/determining-the-location-of-your-script-in-bash.html
13+
HERE=$(cd "$(dirname "$BASH_SOURCE")"; cd -P "$(dirname "$(readlink "$BASH_SOURCE" || echo -e "$BASH_SOURCE")")"; pwd)
14+
15+
BUILD="${HERE}/build-capnp"
16+
17+
rm -rf "${BUILD}"
18+
mkdir -p "${BUILD}"
19+
pushd $_
20+
21+
curl -O https://capnproto.org/capnproto-c++-0.8.0.tar.gz
22+
tar zxf capnproto-c++-0.8.0.tar.gz
23+
pushd capnproto-c++-0.8.0
24+
25+
CC=o64-clang CXX=o64-clang++ ./configure --build=x86_64-apple-darwin --host=x86_64-linux-gnu
26+
27+
# libtool seems to decide that we must build shared libraries with -nostdlib as the linker will
28+
# necessarily link against the wrong stdlib. I don't believe this to be true for our toolchain, but
29+
# I'm also not enough of a GNU greybeard to know how to dissuade libtool from emitting this linker
30+
# flag. So, we'll do it the brute-force-and-ignorance way.
31+
sed -i 's/ -nostdlib//g' ./libtool
32+
33+
# Similarly, the configure script seems to misdetect the value of the max_cmd_length variable,
34+
# setting it to an empty string. This results in the libtool linker command going bang approximately
35+
# half way through. Force the value to be set.
36+
ARG_MAX=`getconf ARG_MAX`
37+
sed -i "s/max_cmd_len=$/max_cmd_len=${ARG_MAX}/" ./libtool
38+
39+
# The capnp makefile tries to run the capnpc tool as a smoke test approximately half way through the
40+
# process. Unsurprisingly, when we are cross-compiling, this approach is met with limited success.
41+
# Expunge this from the makefile, replacing the test_capnp_outputs rule with an empty recipe.
42+
sed -i 's/(test_capnpc_outputs): test_capnpc_middleman$/(test_capnpc_outputs):\n\t@:/' Makefile
43+
44+
CC=o64-clang CXX=o64-clang++ make -j
45+
46+
make install-data DESTDIR=capnp-root
47+
48+
export CAPNP_LIBDIR=`pwd`/.libs
49+
export CAPNP_INCDIR=`pwd`/capnp-root/usr/local/include
50+
51+
popd
52+
popd
53+
54+
cat << EOF > capnp-darwin.env
55+
export CAPNP_LIBDIR="${CAPNP_LIBDIR}"
56+
export CAPNP_INCDIR="${CAPNP_INCDIR}"
57+
EOF
58+
59+
echo "Written env file $(pwd)/capnp-darwin.env !";

build.js

Lines changed: 132 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,56 @@ var force = false, debug = false;
3131
var
3232
arch = process.arch,
3333
platform = process.platform,
34-
v8 = /[0-9]+\.[0-9]+/.exec(process.versions.v8)[0];
34+
v8 = /[0-9]+\.[0-9]+/.exec(process.versions.v8)[0],
35+
environment = { ...process.env };
36+
37+
var patchLibPath = false, patchTool = null;
38+
3539
var args = process.argv.slice(2).filter(function(arg) {
3640
if (arg === '-f') {
3741
force = true;
3842
return false;
3943
} else if (arg.substring(0, 13) === '--target_arch') {
4044
arch = arg.substring(14);
45+
force = true; // No point trying to run a binary that isn't the host's arch
46+
} else if (arg.substring(0, 13) === '--target_plat') {
47+
platform = arg.substring(14);
48+
force = true; // No point trying to run a binary that isn't the host's platform
49+
environment = patchEnvironment(environment);
50+
return false;
4151
} else if (arg === '--debug') {
4252
debug = true;
43-
}
53+
} else if (arg.substring(0, 12) === '--patch_path') {
54+
patchLibPath = arg.substring(13);
55+
56+
if (!environment.hasOwnProperty("PATCH_TOOL")) {
57+
console.error("--patch_path was requested but PATCH_TOOL env was not set; aborting");
58+
console.error("be sure to set --target_plat first if cross-compiling as it will set PATCH_TOOL for you");
59+
process.exit(1);
60+
}
61+
62+
return false;
63+
}
64+
4465
return true;
4566
});
67+
68+
// If we are cross-compiling for darwin, we must instruct gyp to generate a makefile which is
69+
// compatible with the MacOS tooling. YOU MUST use the -f variant (NOT --format=) because it
70+
// overrides -f set by node-gyp, and node-gyp isn't clever enough to know that they're aliases.
71+
var configure_args = (platform != process.platform && platform == "darwin")
72+
? ["-f", "make-mac"]
73+
: [];
74+
4675
if (!{ia32: true, x64: true, arm: true}.hasOwnProperty(arch)) {
4776
console.error('Unsupported (?) architecture: `'+ arch+ '`');
4877
process.exit(1);
4978
}
5079

5180
// Test for pre-built library
5281
var modPath = platform+ '-'+ arch+ '-v8-'+ v8;
82+
var command = process.platform === 'win32' ? 'node-gyp.cmd' : 'node-gyp';
83+
5384
if (!force) {
5485
try {
5586
fs.statSync(path.join(__dirname, 'bin', modPath, 'capnp.node'));
@@ -70,36 +101,46 @@ if (!force) {
70101
build();
71102
}
72103

73-
// Build it
74-
function build() {
104+
function nodeGyp(task, arguments = [], gyp_args = []) {
105+
var final_args = [task].concat(arguments, "--", gyp_args);
106+
console.log(`Executing ${command} with the following args: `);
107+
console.log(final_args);
108+
75109
var sp = cp.spawn(
76-
process.platform === 'win32' ? 'node-gyp.cmd' : 'node-gyp',
77-
['rebuild'].concat(args),
78-
{customFds: [0, 1, 2]});
110+
command,
111+
final_args,
112+
{
113+
stdio: 'inherit',
114+
env: environment,
115+
});
79116

80-
sp
81-
.on('close', function(){ afterBuild(); })
82-
.on('exit', function(err) {
117+
sp.on('exit', function(err) {
83118
if (err) {
84119
if (err === 127) {
85120
console.error(
86121
'node-gyp not found! Please upgrade your install of npm! You need at least 1.1.5 (I think) '+
87122
'and preferably 1.1.30.'
88123
);
89124
} else {
90-
console.error('Build failed');
125+
console.error(`Build step ${task} failed with exit code ${err}`);
91126
}
92127
return process.exit(err);
93128
}
94129
});
95130

96-
if (sp.stdout) {
97-
sp.stdout.pipe(process.stdout);
98-
}
131+
return sp;
132+
}
99133

100-
if (sp.stderr) {
101-
sp.stderr.pipe(process.stderr);
102-
}
134+
// Build it
135+
function build() {
136+
console.log("Building with the following environment:");
137+
console.log(environment);
138+
139+
nodeGyp("clean").on('close', () => {
140+
nodeGyp("configure", args, configure_args).on('close', () => {
141+
nodeGyp("build", args).on('close', () => afterBuild());
142+
});
143+
});
103144
}
104145

105146
// Move it to expected location
@@ -120,6 +161,10 @@ function afterBuild() {
120161
process.exit(1);
121162
}
122163

164+
if (patchLibPath !== false) {
165+
patchLibs(environment.PATCH_TOOL, patchLibPath, targetPath);
166+
}
167+
123168
try {
124169
fs.renameSync(targetPath, installPath);
125170
console.log('Installed in `'+ installPath+ '`');
@@ -128,3 +173,73 @@ function afterBuild() {
128173
}
129174
}
130175

176+
/**
177+
* Rewrite the dynamic library references in the produced bundle to have appropriate paths for
178+
* relative layout within the final application bundle.
179+
*
180+
* @param patchTool - command name to use for editing the binary
181+
* @param patchPath - the path to be stripped out of the binary
182+
* @param target - The target binary to be modified
183+
*/
184+
function patchLibs(patchTool, patchPath, target) {
185+
var libVersion = "0.8.0";
186+
var libExt = "so";
187+
var libs = [
188+
"libkj",
189+
"libkj-async",
190+
"libcapnp",
191+
"libcapnpc",
192+
"libcapnp-rpc",
193+
];
194+
195+
var patchArgs = libs.flatMap((lib) => {
196+
return ["-change", `${patchPath}/${lib}-${libVersion}.${libExt}`, `@executable_path/${lib}-${libVersion}.dylib`];
197+
}).concat([target]);
198+
199+
console.log(`Invoking binary patch tool ${patchTool} with the following arguments:`);
200+
console.log(patchArgs);
201+
202+
/*
203+
* In future if this errors, we may need to set headerpad_max_install_names in LDFLAGS too...
204+
* (this provides more space for the dylib names in the object so that they can be rewritten
205+
* post-link) - looks like we don't need to do this for now though?
206+
*/
207+
var patchProcess = cp.spawnSync(patchTool, patchArgs, {
208+
stdio: 'inherit',
209+
});
210+
211+
if (patchProcess.error) {
212+
console.error(`Failed to patch binary: ${patchProcess.error}`);
213+
process.exit(1);
214+
} else if (patchProcess.status !== 0) {
215+
console.error(`Failed to patch binary: ${patchTool} exited with code ${patchProcess.status}`);
216+
process.exit(1);
217+
} else {
218+
console.log("Binary patched successfully.");
219+
}
220+
}
221+
222+
/**
223+
* Modify the provided environment object to be appropriate for cross-compilation for a darwin
224+
* target from a linux host
225+
*/
226+
function patchEnvironment(env) {
227+
if (!env.hasOwnProperty("CAPNP_LIBDIR")) {
228+
console.error("CAPNP_LIBDIR must be set to a place I can find compiled libcapnp, libkj and friends for the target plat/arch");
229+
process.exit(1);
230+
}
231+
232+
if (!env.hasOwnProperty("CAPNP_INCDIR")) {
233+
console.error("CAPNP_INCDIR must be set to a place I can find headers for libcapnp and libkj!");
234+
process.exit(1);
235+
}
236+
237+
return {
238+
CC: "o64-clang",
239+
CXX: "o64-clang++",
240+
CXXFLAGS: "-mmacosx-version-min=10.7 -std=c++14 -stdlib=libc++ -I" + env.CAPNP_INCDIR,
241+
LDFLAGS: "-L" + env.CAPNP_LIBDIR,
242+
PATCH_TOOL: "x86_64-apple-darwin20.2-install_name_tool",
243+
...env
244+
};
245+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"types": "src/node-capnp/capnp.d.ts",
2323
"scripts": {
2424
"install": "node ./build.js --dist-url=https://electronjs.org/headers --target=12.0.2",
25-
"test": "node src/node-capnp/capnp-test.js"
25+
"test": "node src/node-capnp/capnp-test.js",
26+
"build:darwin": "npm run install -- --target_plat=darwin --patch_path=.libs"
2627
},
2728
"dependencies": {
2829
"nan": "^2.7.0"

src/node-capnp/capnp.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ if (false) {
4646
}
4747
}
4848

49-
var v8capnp = require("../../bin/linux-x64-v8-8.4/capnp.node");
49+
var v8capnp = process.platform == "darwin"
50+
? require("../../bin/darwin-x64-v8.8.4/capnp.node")
51+
: require("../../bin/linux-x64-v8-8.4/capnp.node");
5052

5153
var importPath = [];
5254
for (var i in module.paths) {

0 commit comments

Comments
 (0)