Skip to content

Commit a8f68ba

Browse files
lovellRReverser
andauthoredNov 9, 2023
Add infrastructure to build and publish as wasm32 (#3840)
Co-authored-by: Ingvar Stepanyan <me@rreverser.com>
1 parent 475bf16 commit a8f68ba

17 files changed

+241
-20
lines changed
 

‎.github/workflows/ci.yml

+32
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,38 @@ jobs:
161161
npm install --ignore-scripts
162162
npx mocha --no-config --spec=test/unit/io.js --timeout=30000
163163
[[ -n $prebuild_upload ]] && cd src && ln -s ../package.json && npx prebuild || true
164+
github-runner-emscripten:
165+
permissions:
166+
contents: write
167+
name: wasm32 - prebuild
168+
runs-on: ubuntu-22.04
169+
container: "emscripten/emsdk:3.1.48"
170+
steps:
171+
- name: Checkout
172+
uses: actions/checkout@v4
173+
- name: Dependencies
174+
run: apt-get update && apt-get install -y pkg-config
175+
- name: Dependencies (Node.js)
176+
uses: actions/setup-node@v3
177+
with:
178+
node-version: "20"
179+
- name: Install
180+
run: emmake npm install --build-from-source
181+
- name: Test
182+
run: emmake npm test
183+
- name: Test packaging
184+
run: |
185+
emmake npm run package-from-local-build
186+
npm pkg set "optionalDependencies.@img/sharp-wasm32=file:./npm/wasm32"
187+
npm run clean
188+
npm install --cpu=wasm32
189+
npm test
190+
- name: Prebuild
191+
if: startsWith(github.ref, 'refs/tags/')
192+
env:
193+
npm_config_nodedir: emscripten
194+
prebuild_upload: ${{ secrets.GITHUB_TOKEN }}
195+
run: cd src && ln -s ../package.json && emmake npx prebuild --platform=emscripten --arch=wasm32 --strip=0
164196
macstadium-runner:
165197
permissions:
166198
contents: write

‎docs/changelog.md

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Requires libvips v8.15.0
1414

1515
* Remove `sharp.vendor`.
1616

17+
* Add experimental support for WebAssembly-based runtimes.
18+
[@RReverser](https://github.com/RReverser)
19+
1720
* Options for `trim` operation must be an Object, add new `lineArt` option.
1821
[#2363](https://github.com/lovell/sharp/issues/2363)
1922

‎docs/humans.txt

+3
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,6 @@ GitHub: https://github.com/bianjunjie1981
278278

279279
Name: Dennis Beatty
280280
GitHub: https://github.com/dnsbty
281+
282+
Name: Ingvar Stepanyan
283+
GitHub: https://github.com/RReverser

‎docs/install.md

+9
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ For cross-compiling, the `--platform`, `--arch` and `--libc` npm flags
7878
(or the `npm_config_platform`, `npm_config_arch` and `npm_config_libc` environment variables)
7979
can be used to configure the target environment.
8080

81+
## WebAssembly
82+
83+
Experimental support is provided for runtime environments that provide
84+
multi-threaded Wasm via Workers.
85+
86+
```sh
87+
npm install --cpu=wasm32 sharp
88+
```
89+
8190
## FreeBSD
8291

8392
The `vips` package must be installed before `npm install` is run.

‎docs/search-index.json

+1-1
Large diffs are not rendered by default.

‎lib/libvips.js

+24-6
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,23 @@ const runtimePlatformArch = () => `${process.platform}${runtimeLibc()}-${process
4141

4242
/* istanbul ignore next */
4343
const buildPlatformArch = () => {
44+
if (isEmscripten()) {
45+
return 'wasm32';
46+
}
4447
/* eslint camelcase: ["error", { allow: ["^npm_config_"] }] */
4548
const { npm_config_arch, npm_config_platform, npm_config_libc } = process.env;
46-
return `${npm_config_platform || process.platform}${npm_config_libc || runtimeLibc()}-${npm_config_arch || process.arch}`;
49+
const libc = typeof npm_config_libc === 'string' ? npm_config_libc : runtimeLibc();
50+
return `${npm_config_platform || process.platform}${libc}-${npm_config_arch || process.arch}`;
4751
};
4852

4953
const buildSharpLibvipsIncludeDir = () => {
5054
try {
51-
return require('@img/sharp-libvips-dev/include');
52-
} catch {}
55+
return require(`@img/sharp-libvips-dev-${buildPlatformArch()}/include`);
56+
} catch {
57+
try {
58+
return require('@img/sharp-libvips-dev/include');
59+
} catch {}
60+
}
5361
/* istanbul ignore next */
5462
return '';
5563
};
@@ -64,12 +72,22 @@ const buildSharpLibvipsCPlusPlusDir = () => {
6472

6573
const buildSharpLibvipsLibDir = () => {
6674
try {
67-
return require(`@img/sharp-libvips-${buildPlatformArch()}/lib`);
68-
} catch {}
75+
return require(`@img/sharp-libvips-dev-${buildPlatformArch()}/lib`);
76+
} catch {
77+
try {
78+
return require(`@img/sharp-libvips-${buildPlatformArch()}/lib`);
79+
} catch {}
80+
}
6981
/* istanbul ignore next */
7082
return '';
7183
};
7284

85+
/* istanbul ignore next */
86+
const isEmscripten = () => {
87+
const { CC } = process.env;
88+
return Boolean(CC && CC.endsWith('/emcc'));
89+
};
90+
7391
const isRosetta = () => {
7492
/* istanbul ignore next */
7593
if (process.platform === 'darwin' && process.arch === 'x64') {
@@ -81,7 +99,7 @@ const isRosetta = () => {
8199

82100
/* istanbul ignore next */
83101
const spawnRebuild = () =>
84-
spawnSync('node-gyp rebuild --directory=src', {
102+
spawnSync(`node-gyp rebuild --directory=src ${isEmscripten() ? '--nodedir=emscripten' : ''}`, {
85103
...spawnSyncOptions,
86104
stdio: 'inherit'
87105
}).status;

‎lib/sharp.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ const runtimePlatform = runtimePlatformArch();
1212

1313
const paths = [
1414
`../src/build/Release/sharp-${runtimePlatform}.node`,
15-
`@img/sharp-${runtimePlatform}/sharp.node`
15+
'../src/build/Release/sharp-wasm32.node',
16+
`@img/sharp-${runtimePlatform}/sharp.node`,
17+
'@img/sharp-wasm32/sharp.node'
1618
];
1719

1820
const errors = [];
@@ -47,6 +49,8 @@ if (!module.exports) {
4749
help.push(` npm install --force @img/sharp-${runtimePlatform}`);
4850
} else {
4951
help.push(`- Manually install libvips >= ${minimumLibvipsVersion}`);
52+
help.push('- Add experimental WebAssembly-based dependencies:');
53+
help.push(' npm install --cpu=wasm32 sharp');
5054
}
5155
if (isLinux && /symbol not found/i.test(messages)) {
5256
try {

‎lib/utility.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,17 @@ let versions = {
5959
};
6060
/* istanbul ignore next */
6161
if (!libvipsVersion.isGlobal) {
62-
try {
63-
versions = require(`@img/sharp-${runtimePlatform}/versions`);
64-
} catch (_) {
62+
if (!libvipsVersion.isWasm) {
6563
try {
66-
versions = require(`@img/sharp-libvips-${runtimePlatform}/versions`);
64+
versions = require(`@img/sharp-${runtimePlatform}/versions`);
65+
} catch (_) {
66+
try {
67+
versions = require(`@img/sharp-libvips-${runtimePlatform}/versions`);
68+
} catch (_) {}
69+
}
70+
} else {
71+
try {
72+
versions = require('@img/sharp-wasm32/versions');
6773
} catch (_) {}
6874
}
6975
}

‎npm/from-github-release.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ limitations under the License.
3838
`;
3939

4040
workspaces.map(async platform => {
41-
const url = `https://github.com/lovell/sharp/releases/download/v${version}/sharp-v${version}-napi-v9-${platform}.tar.gz`;
41+
const prebuildPlatform = platform === 'wasm32' ? 'emscripten-wasm32' : platform;
42+
const url = `https://github.com/lovell/sharp/releases/download/v${version}/sharp-v${version}-napi-v9-${prebuildPlatform}.tar.gz`;
4243
const dir = path.join(__dirname, platform);
4344
const response = await fetch(url);
4445
if (!response.ok) {
@@ -58,8 +59,8 @@ workspaces.map(async platform => {
5859
await writeFile(path.join(dir, 'README.md'), `# \`${name}\`\n\n${description}.\n${licensing}`);
5960
// Copy Apache-2.0 LICENSE
6061
await copyFile(path.join(__dirname, '..', 'LICENSE'), path.join(dir, 'LICENSE'));
61-
// Copy Windows-specific files
62-
if (platform.startsWith('win32-')) {
62+
// Copy files for packages without an explicit sharp-libvips dependency (Windows, wasm)
63+
if (platform.startsWith('win') || platform.startsWith('wasm')) {
6364
const sharpLibvipsDir = path.join(require(`@img/sharp-libvips-${platform}/lib`), '..');
6465
// Copy versions.json
6566
await copyFile(path.join(sharpLibvipsDir, 'versions.json'), path.join(dir, 'versions.json'));

‎npm/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"linux-x64",
1212
"linuxmusl-arm64",
1313
"linuxmusl-x64",
14+
"wasm32",
1415
"win32-ia32",
1516
"win32-x64"
1617
]

‎npm/wasm32/package.json

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "@img/sharp-wasm32",
3+
"version": "0.33.0-alpha.10",
4+
"description": "Prebuilt sharp for use with wasm32",
5+
"author": "Lovell Fuller <npm@lovell.info>",
6+
"homepage": "https://sharp.pixelplumbing.com",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/lovell/sharp.git",
10+
"directory": "npm/wasm32"
11+
},
12+
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
13+
"funding": {
14+
"url": "https://opencollective.com/libvips"
15+
},
16+
"preferUnplugged": true,
17+
"files": [
18+
"lib",
19+
"versions.json"
20+
],
21+
"publishConfig": {
22+
"access": "public"
23+
},
24+
"type": "commonjs",
25+
"exports": {
26+
"./sharp.node": "./lib/sharp-wasm32.node.js",
27+
"./package": "./package.json",
28+
"./versions": "./versions.json"
29+
},
30+
"engines": {
31+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
32+
"npm": ">=9.6.5",
33+
"yarn": ">=3.2.0",
34+
"pnpm": ">=7.1.0"
35+
},
36+
"dependencies": {
37+
"@emnapi/runtime": "^0.43.1"
38+
},
39+
"cpu": [
40+
"wasm32"
41+
]
42+
}

‎package.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@
8787
"Brahim Ait elhaj <brahima@gmail.com>",
8888
"Mart Jansink <m.jansink@gmail.com>",
8989
"Lachlan Newman <lachnewman007@gmail.com>",
90-
"Dennis Beatty <dennis@dcbeatty.com>"
90+
"Dennis Beatty <dennis@dcbeatty.com>",
91+
"Ingvar Stepanyan <me@rreverser.com>"
9192
],
9293
"scripts": {
9394
"install": "node install/check",
@@ -156,16 +157,20 @@
156157
"@img/sharp-linux-x64": "0.33.0-alpha.10",
157158
"@img/sharp-linuxmusl-arm64": "0.33.0-alpha.10",
158159
"@img/sharp-linuxmusl-x64": "0.33.0-alpha.10",
160+
"@img/sharp-wasm32": "0.33.0-alpha.10",
159161
"@img/sharp-win32-ia32": "0.33.0-alpha.10",
160162
"@img/sharp-win32-x64": "0.33.0-alpha.10"
161163
},
162164
"devDependencies": {
165+
"@emnapi/runtime": "^0.43.1",
163166
"@img/sharp-libvips-dev": "0.0.3",
167+
"@img/sharp-libvips-dev-wasm32": "0.0.3",
164168
"@img/sharp-libvips-win32-ia32": "0.0.3",
165169
"@img/sharp-libvips-win32-x64": "0.0.3",
166170
"@types/node": "*",
167171
"async": "^3.2.5",
168172
"cc": "^3.0.1",
173+
"emnapi": "^0.43.1",
169174
"exif-reader": "^2.0.0",
170175
"extract-zip": "^2.0.1",
171176
"icc": "^3.0.0",
@@ -203,6 +208,11 @@
203208
"build/include"
204209
]
205210
},
211+
"nyc": {
212+
"include": [
213+
"lib"
214+
]
215+
},
206216
"tsd": {
207217
"directory": "test/types/"
208218
}

‎src/binding.gyp

+20
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,26 @@
179179
'-Wl,-rpath=\'$$ORIGIN/../../../node_modules/@img/sharp-libvips-<(platform_and_arch)/lib\''
180180
]
181181
}
182+
}],
183+
['OS == "emscripten"', {
184+
'product_extension': 'node.js',
185+
'link_settings': {
186+
'ldflags': [
187+
'-fexceptions',
188+
'--pre-js=<!(node -p "require.resolve(\'./emscripten/pre.js\')")',
189+
'-Oz',
190+
'-sALLOW_MEMORY_GROWTH',
191+
'-sENVIRONMENT=node',
192+
'-sEXPORTED_FUNCTIONS=["_vips_shutdown", "_uv_library_shutdown"]',
193+
'-sNODERAWFS',
194+
'-sTEXTDECODER=0',
195+
'-sWASM_ASYNC_COMPILATION=0',
196+
'-sWASM_BIGINT'
197+
],
198+
'libraries': [
199+
'<!@(PKG_CONFIG_PATH="<!(node -p "require(\'@img/sharp-libvips-dev-wasm32/lib\')")/pkgconfig" pkg-config --static --libs vips-cpp)'
200+
],
201+
}
182202
}]
183203
]
184204
}]

‎src/emscripten/common.gypi

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2013 Lovell Fuller and others.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
{
5+
'variables': {
6+
'OS': 'emscripten'
7+
},
8+
'target_defaults': {
9+
'default_configuration': 'Release',
10+
'type': 'executable',
11+
'cflags': [
12+
'-pthread',
13+
'-sDEFAULT_TO_CXX=0'
14+
],
15+
'cflags_cc': [
16+
'-pthread'
17+
],
18+
'ldflags': [
19+
'--js-library=<!(node -p "require(\'emnapi\').js_library")',
20+
'-sAUTO_JS_LIBRARIES=0',
21+
'-sAUTO_NATIVE_LIBRARIES=0',
22+
'-sNODEJS_CATCH_EXIT=0',
23+
'-sNODEJS_CATCH_REJECTION=0'
24+
],
25+
'defines': [
26+
'__STDC_FORMAT_MACROS',
27+
'BUILDING_NODE_EXTENSION',
28+
'EMNAPI_WORKER_POOL_SIZE=1'
29+
],
30+
'include_dirs': [
31+
'<!(node -p "require(\'emnapi\').include")'
32+
],
33+
'sources': [
34+
'<!@(node -p "require(\'emnapi\').sources.map(x => JSON.stringify(path.relative(process.cwd(), x))).join(\' \')")'
35+
],
36+
'configurations': {
37+
'Release': {}
38+
}
39+
}
40+
}

‎src/emscripten/pre.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2013 Lovell Fuller and others.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/* global Module, ENV, _vips_shutdown, _uv_library_shutdown */
5+
6+
Module.preRun = () => {
7+
ENV.VIPS_CONCURRENCY = Number(process.env.VIPS_CONCURRENCY) || 1;
8+
};
9+
10+
Module.onRuntimeInitialized = () => {
11+
module.exports = Module.emnapiInit({
12+
context: require('@emnapi/runtime').getDefaultContext()
13+
});
14+
15+
process.once('exit', () => {
16+
_vips_shutdown();
17+
_uv_library_shutdown();
18+
});
19+
};

‎src/utilities.cc

+5
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ Napi::Value libvipsVersion(const Napi::CallbackInfo& info) {
102102
version.Set("isGlobal", Napi::Boolean::New(env, true));
103103
#else
104104
version.Set("isGlobal", Napi::Boolean::New(env, false));
105+
#endif
106+
#ifdef __EMSCRIPTEN__
107+
version.Set("isWasm", Napi::Boolean::New(env, true));
108+
#else
109+
version.Set("isWasm", Napi::Boolean::New(env, false));
105110
#endif
106111
return version;
107112
}

‎test/unit/libvips.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,25 @@ describe('libvips binaries', function () {
6969
});
7070

7171
describe('Build time platform detection', () => {
72-
it('Can override platform with npm_config_platform and npm_config_libc', () => {
72+
it('Can override platform with npm_config_platform and npm_config_libc', function () {
7373
process.env.npm_config_platform = 'testplatform';
7474
process.env.npm_config_libc = 'testlibc';
75-
const [platform] = libvips.buildPlatformArch().split('-');
75+
const platformArch = libvips.buildPlatformArch();
76+
if (platformArch === 'wasm32') {
77+
return this.skip();
78+
}
79+
const [platform] = platformArch.split('-');
7680
assert.strictEqual(platform, 'testplatformtestlibc');
7781
delete process.env.npm_config_platform;
7882
delete process.env.npm_config_libc;
7983
});
80-
it('Can override arch with npm_config_arch', () => {
84+
it('Can override arch with npm_config_arch', function () {
8185
process.env.npm_config_arch = 'test';
82-
const [, arch] = libvips.buildPlatformArch().split('-');
86+
const platformArch = libvips.buildPlatformArch();
87+
if (platformArch === 'wasm32') {
88+
return this.skip();
89+
}
90+
const [, arch] = platformArch.split('-');
8391
assert.strictEqual(arch, 'test');
8492
delete process.env.npm_config_arch;
8593
});

0 commit comments

Comments
 (0)
Please sign in to comment.