Skip to content

Commit

Permalink
Add infrastructure to build and publish as wasm32
Browse files Browse the repository at this point in the history
Co-authored-by: Ingvar Stepanyan <me@rreverser.com>
  • Loading branch information
lovell and RReverser committed Nov 6, 2023
1 parent 0740043 commit daf2897
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 17 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,37 @@ jobs:
npm install --ignore-scripts
npx mocha --no-config --spec=test/unit/io.js --timeout=30000
[[ -n $prebuild_upload ]] && cd src && ln -s ../package.json && npx prebuild || true
github-runner-emscripten:
permissions:
contents: write
name: wasm32 - prebuild
runs-on: ubuntu-22.04
container: "emscripten/emsdk:3.1.48"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dependencies
run: apt-get update && apt-get install -y pkg-config
- name: Dependencies (Node.js)
uses: actions/setup-node@v3
with:
node-version: "20"
- name: Install
run: emmake npm install --build-from-source
- name: Test
run: emmake npm test
- name: Test packaging
run: |
emmake npm run package-from-local-build
npm pkg set "optionalDependencies.@img/sharp-wasm32=file:./npm/wasm32"
npm run clean
npm install --cpu=wasm32
npm test
- name: Prebuild
if: startsWith(github.ref, 'refs/tags/')
env:
prebuild_upload: ${{ secrets.GITHUB_TOKEN }}
run: cd src && ln -s ../package.json && emmake npx prebuild
macstadium-runner:
permissions:
contents: write
Expand Down
9 changes: 9 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ For cross-compiling, the `--platform`, `--arch` and `--libc` npm flags
(or the `npm_config_platform`, `npm_config_arch` and `npm_config_libc` environment variables)
can be used to configure the target environment.

## WebAssembly

Experimental support is provided for runtime environments that provide
multi-threaded Wasm via Workers.

```sh
npm install --cpu=wasm32 sharp
```

## FreeBSD

The `vips` package must be installed before `npm install` is run.
Expand Down
30 changes: 24 additions & 6 deletions lib/libvips.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,23 @@ const runtimePlatformArch = () => `${process.platform}${runtimeLibc()}-${process

/* istanbul ignore next */
const buildPlatformArch = () => {
if (isEmscripten()) {
return 'wasm32';
}
/* eslint camelcase: ["error", { allow: ["^npm_config_"] }] */
const { npm_config_arch, npm_config_platform, npm_config_libc } = process.env;
return `${npm_config_platform || process.platform}${npm_config_libc || runtimeLibc()}-${npm_config_arch || process.arch}`;
const libc = typeof npm_config_libc === 'string' ? npm_config_libc : runtimeLibc();
return `${npm_config_platform || process.platform}${libc}-${npm_config_arch || process.arch}`;
};

const buildSharpLibvipsIncludeDir = () => {
try {
return require('@img/sharp-libvips-dev/include');
} catch {}
return require(`@img/sharp-libvips-dev-${buildPlatformArch()}/include`);
} catch {
try {
return require('@img/sharp-libvips-dev/include');
} catch {}
}
/* istanbul ignore next */
return '';
};
Expand All @@ -64,12 +72,22 @@ const buildSharpLibvipsCPlusPlusDir = () => {

const buildSharpLibvipsLibDir = () => {
try {
return require(`@img/sharp-libvips-${buildPlatformArch()}/lib`);
} catch {}
return require(`@img/sharp-libvips-dev-${buildPlatformArch()}/lib`);
} catch {
try {
return require(`@img/sharp-libvips-${buildPlatformArch()}/lib`);
} catch {}
}
/* istanbul ignore next */
return '';
};

/* istanbul ignore next */
const isEmscripten = () => {
const { CC } = process.env;
return Boolean(CC && CC.endsWith('/emcc'));
};

const isRosetta = () => {
/* istanbul ignore next */
if (process.platform === 'darwin' && process.arch === 'x64') {
Expand All @@ -81,7 +99,7 @@ const isRosetta = () => {

/* istanbul ignore next */
const spawnRebuild = () =>
spawnSync('node-gyp rebuild --directory=src', {
spawnSync(`node-gyp rebuild --directory=src ${isEmscripten() ? '--nodedir=emscripten' : ''}`, {
...spawnSyncOptions,
stdio: 'inherit'
}).status;
Expand Down
6 changes: 5 additions & 1 deletion lib/sharp.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const runtimePlatform = runtimePlatformArch();

const paths = [
`../src/build/Release/sharp-${runtimePlatform}.node`,
`@img/sharp-${runtimePlatform}/sharp.node`
'../src/build/Release/sharp-wasm32.node',
`@img/sharp-${runtimePlatform}/sharp.node`,
'@img/sharp-wasm32/sharp.node'
];

const errors = [];
Expand Down Expand Up @@ -47,6 +49,8 @@ if (!module.exports) {
help.push(` npm install --force @img/sharp-${runtimePlatform}`);
} else {
help.push(`- Manually install libvips >= ${minimumLibvipsVersion}`);
help.push('- Add experimental WebAssembly-based dependencies:');
help.push(' npm install --cpu=wasm32 sharp');
}
if (isLinux && /symbol not found/i.test(messages)) {
try {
Expand Down
14 changes: 10 additions & 4 deletions lib/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ let versions = {
};
/* istanbul ignore next */
if (!libvipsVersion.isGlobal) {
try {
versions = require(`@img/sharp-${runtimePlatform}/versions`);
} catch (_) {
if (!libvipsVersion.isWasm) {
try {
versions = require(`@img/sharp-libvips-${runtimePlatform}/versions`);
versions = require(`@img/sharp-${runtimePlatform}/versions`);
} catch (_) {
try {
versions = require(`@img/sharp-libvips-${runtimePlatform}/versions`);
} catch (_) {}
}
} else {
try {
versions = require('@img/sharp-wasm32/versions');
} catch (_) {}
}
}
Expand Down
4 changes: 2 additions & 2 deletions npm/from-github-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ workspaces.map(async platform => {
await writeFile(path.join(dir, 'README.md'), `# \`${name}\`\n\n${description}.\n${licensing}`);
// Copy Apache-2.0 LICENSE
await copyFile(path.join(__dirname, '..', 'LICENSE'), path.join(dir, 'LICENSE'));
// Copy Windows-specific files
if (platform.startsWith('win32-')) {
// Copy files for packages without an explicit sharp-libvips dependency (Windows, wasm)
if (platform.startsWith('win') || platform.startsWith('wasm')) {
const sharpLibvipsDir = path.join(require(`@img/sharp-libvips-${platform}/lib`), '..');
// Copy versions.json
await copyFile(path.join(sharpLibvipsDir, 'versions.json'), path.join(dir, 'versions.json'));
Expand Down
1 change: 1 addition & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"linux-x64",
"linuxmusl-arm64",
"linuxmusl-x64",
"wasm32",
"win32-ia32",
"win32-x64"
]
Expand Down
42 changes: 42 additions & 0 deletions npm/wasm32/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@img/sharp-wasm32",
"version": "0.33.0-alpha.10",
"description": "Prebuilt sharp for use with wasm32",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp.git",
"directory": "npm/wasm32"
},
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"files": [
"lib",
"versions.json"
],
"publishConfig": {
"access": "public"
},
"type": "commonjs",
"exports": {
"./sharp.node": "./lib/sharp-wasm32.node.js",
"./package": "./package.json",
"./versions": "./versions.json"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"yarn": ">=3.2.0",
"pnpm": ">=7.1.0"
},
"dependencies": {
"@emnapi/runtime": "^0.43.1"
},
"cpu": [
"wasm32"
]
}
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,20 @@
"@img/sharp-linux-x64": "0.33.0-alpha.10",
"@img/sharp-linuxmusl-arm64": "0.33.0-alpha.10",
"@img/sharp-linuxmusl-x64": "0.33.0-alpha.10",
"@img/sharp-wasm32": "0.33.0-alpha.10",
"@img/sharp-win32-ia32": "0.33.0-alpha.10",
"@img/sharp-win32-x64": "0.33.0-alpha.10"
},
"devDependencies": {
"@emnapi/runtime": "^0.43.1",
"@img/sharp-libvips-dev": "0.0.3",
"@img/sharp-libvips-dev-wasm32": "0.0.3",
"@img/sharp-libvips-win32-ia32": "0.0.3",
"@img/sharp-libvips-win32-x64": "0.0.3",
"@types/node": "*",
"async": "^3.2.5",
"cc": "^3.0.1",
"emnapi": "^0.43.1",
"exif-reader": "^2.0.0",
"extract-zip": "^2.0.1",
"icc": "^3.0.0",
Expand Down Expand Up @@ -203,6 +207,11 @@
"build/include"
]
},
"nyc": {
"include": [
"lib"
]
},
"tsd": {
"directory": "test/types/"
}
Expand Down
20 changes: 20 additions & 0 deletions src/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,26 @@
'-Wl,-rpath=\'$$ORIGIN/../../../node_modules/@img/sharp-libvips-<(platform_and_arch)/lib\''
]
}
}],
['OS == "emscripten"', {
'product_extension': 'node.js',
'link_settings': {
'ldflags': [
'-fexceptions',
'--pre-js=<!(node -p "require.resolve(\'./emscripten/pre.js\')")',
'-Oz',
'-sALLOW_MEMORY_GROWTH',
'-sENVIRONMENT=node',
'-sEXPORTED_FUNCTIONS=["_vips_shutdown", "_uv_library_shutdown"]',
'-sNODERAWFS',
'-sTEXTDECODER=0',
'-sWASM_ASYNC_COMPILATION=0',
'-sWASM_BIGINT'
],
'libraries': [
'<!@(PKG_CONFIG_PATH="<!(node -p "require(\'@img/sharp-libvips-dev-wasm32/lib\')")/pkgconfig" pkg-config --static --libs vips-cpp)'
],
}
}]
]
}]
Expand Down
40 changes: 40 additions & 0 deletions src/emscripten/common.gypi
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2013 Lovell Fuller and others.
# SPDX-License-Identifier: Apache-2.0

{
'variables': {
'OS': 'emscripten'
},
'target_defaults': {
'default_configuration': 'Release',
'type': 'executable',
'cflags': [
'-pthread',
'-sDEFAULT_TO_CXX=0'
],
'cflags_cc': [
'-pthread'
],
'ldflags': [
'--js-library=<!(node -p "require(\'emnapi\').js_library")',
'-sAUTO_JS_LIBRARIES=0',
'-sAUTO_NATIVE_LIBRARIES=0',
'-sNODEJS_CATCH_EXIT=0',
'-sNODEJS_CATCH_REJECTION=0'
],
'defines': [
'__STDC_FORMAT_MACROS',
'BUILDING_NODE_EXTENSION',
'EMNAPI_WORKER_POOL_SIZE=1'
],
'include_dirs': [
'<!(node -p "require(\'emnapi\').include")'
],
'sources': [
'<!@(node -p "require(\'emnapi\').sources.map(x => JSON.stringify(path.relative(process.cwd(), x))).join(\' \')")'
],
'configurations': {
'Release': {}
}
}
}
19 changes: 19 additions & 0 deletions src/emscripten/pre.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2013 Lovell Fuller and others.
// SPDX-License-Identifier: Apache-2.0

/* global Module, ENV, _vips_shutdown, _uv_library_shutdown */

Module.preRun = () => {
ENV.VIPS_CONCURRENCY = Number(process.env.VIPS_CONCURRENCY) || 1;
};

Module.onRuntimeInitialized = () => {
module.exports = Module.emnapiInit({
context: require('@emnapi/runtime').getDefaultContext()
});

process.once('exit', () => {
_vips_shutdown();
_uv_library_shutdown();
});
};
5 changes: 5 additions & 0 deletions src/utilities.cc
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ Napi::Value libvipsVersion(const Napi::CallbackInfo& info) {
version.Set("isGlobal", Napi::Boolean::New(env, true));
#else
version.Set("isGlobal", Napi::Boolean::New(env, false));
#endif
#ifdef __EMSCRIPTEN__
version.Set("isWasm", Napi::Boolean::New(env, true));
#else
version.Set("isWasm", Napi::Boolean::New(env, false));
#endif
return version;
}
Expand Down
16 changes: 12 additions & 4 deletions test/unit/libvips.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,25 @@ describe('libvips binaries', function () {
});

describe('Build time platform detection', () => {
it('Can override platform with npm_config_platform and npm_config_libc', () => {
it('Can override platform with npm_config_platform and npm_config_libc', function () {
process.env.npm_config_platform = 'testplatform';
process.env.npm_config_libc = 'testlibc';
const [platform] = libvips.buildPlatformArch().split('-');
const platformArch = libvips.buildPlatformArch();
if (platformArch === 'wasm32') {
return this.skip();
}
const [platform] = platformArch.split('-');
assert.strictEqual(platform, 'testplatformtestlibc');
delete process.env.npm_config_platform;
delete process.env.npm_config_libc;
});
it('Can override arch with npm_config_arch', () => {
it('Can override arch with npm_config_arch', function () {
process.env.npm_config_arch = 'test';
const [, arch] = libvips.buildPlatformArch().split('-');
const platformArch = libvips.buildPlatformArch();
if (platformArch === 'wasm32') {
return this.skip();
}
const [, arch] = platformArch.split('-');
assert.strictEqual(arch, 'test');
delete process.env.npm_config_arch;
});
Expand Down

0 comments on commit daf2897

Please sign in to comment.