From 731b4cb8e6ed124e7eb05508fdafb1947ae0f178 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Thu, 26 Sep 2024 11:48:35 -0400 Subject: [PATCH] feat: build wasm in common wasm-builder Refs: https://github.com/nodejs/security-wg/issues/1236 Update to build in docker using wasm-builder image maintained in https://github.com/nodejs/wasm-builder. As a side effect this also updates the versions of the wasm tools to a newer version. undici and amaro have already been moved over and I plan to document in Node.js docs that this is the projects standard way to build WASM blobs. Signed-off-by: Michael Dawson --- .github/workflows/build.yml | 18 ++---------- Makefile | 19 +------------ README.md | 16 ++++++----- build/Makefile | 13 +++++++++ build/wasm.js | 54 ++++++++++++++++++++++++++++++++++++ lib/lexer.wasm | Bin 23849 -> 22164 bytes 6 files changed, 80 insertions(+), 40 deletions(-) create mode 100755 build/Makefile create mode 100644 build/wasm.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3231523..ef0213c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,8 +7,6 @@ on: branches: main env: - WASI_VERSION: 12 - WASI_VERSION_FULL: "12.0" WABT_VERSION: "1.0.24" EMCC_VERSION: "1.40.1-fastcomp" @@ -25,22 +23,12 @@ jobs: export PWD=$(pwd); echo "::set-output name=PROJ_ROOT::$PWD"; + - name: Set up Docker + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + - name: Install run: npm install - - name: Install wasi-sdk - env: - PROJ_ROOT: ${{ steps.preparation.outputs.PROJ_ROOT }} - run: | - cd $PROJ_ROOT; - cd ../; - export WASI_OS="linux" - curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sdk-${WASI_VERSION_FULL}-${WASI_OS}.tar.gz -O - # check if package downloaded - ls -la - tar xvf wasi-sdk-${WASI_VERSION_FULL}-${WASI_OS}.tar.gz - # print clang version - ./wasi-sdk-${WASI_VERSION_FULL}/bin/clang --version - name: Install wabt env: PROJ_ROOT: ${{ steps.preparation.outputs.PROJ_ROOT }} diff --git a/Makefile b/Makefile index 961b336..9a75850 100755 --- a/Makefile +++ b/Makefile @@ -1,33 +1,16 @@ -# These flags depend on the system and may be overridden -WASM_CC := ../wasi-sdk-12.0/bin/clang -WASM_CFLAGS := --sysroot=../wasi-sdk-12.0/share/wasi-sysroot -WASM_LDFLAGS := -nostartfiles - WASM2WAT := ../wabt/bin/wasm2wat WASM_OPT := ../binaryen/bin/wasm-opt -# These are project-specific and are expected to be kept intact -WASM_EXTRA_CFLAGS := -I include-wasm/ -Wno-logical-op-parentheses -Wno-parentheses -Oz -WASM_EXTRA_LDFLAGS := -Wl,-z,stack-size=13312,--no-entry,--compress-relocations,--strip-all -WASM_EXTRA_LDFLAGS += -Wl,--export=__heap_base,--export=parseCJS,--export=sa -WASM_EXTRA_LDFLAGS += -Wl,--export=e,--export=re,--export=es,--export=ee -WASM_EXTRA_LDFLAGS += -Wl,--export=rre,--export=ree,--export=res,--export=ru,--export=us,--export=ue - .PHONY: optimize clean lib/lexer.wat: lib/lexer.wasm $(WASM2WAT) lib/lexer.wasm -o lib/lexer.wat lib/lexer.wasm: include-wasm/cjs-module-lexer.h src/lexer.c | lib/ - $(WASM_CC) $(WASM_CFLAGS) $(WASM_EXTRA_CFLAGS) \ - src/lexer.c -o lib/lexer.wasm \ - $(WASM_LDFLAGS) $(WASM_EXTRA_LDFLAGS) + node build/wasm.js --docker lib/: @mkdir -p $@ -optimize: lib/lexer.wasm - $(WASM_OPT) -Oz lib/lexer.wasm -o lib/lexer.wasm - clean: $(RM) lib/* diff --git a/README.md b/README.md index 2716709..cc7ca50 100755 --- a/README.md +++ b/README.md @@ -442,17 +442,19 @@ test/samples/*.js (3635 KiB) ### Wasm Build Steps -To build download the WASI SDK from https://github.com/WebAssembly/wasi-sdk/releases. +The build uses docker and make, they must be installed first. -The Makefile assumes the existence of "wasi-sdk-11.0" and "wabt" (optional) as sibling folders to this project. +To build the lexer wasm run `npm run build-wasm`. -The build through the Makefile is then run via `make lib/lexer.wasm`, which can also be triggered via `npm run build-wasm` to create `dist/lexer.js`. +Optimization passes are run with [Binaryen](https://github.com/WebAssembly/binaryen) +prior to publish to reduce the Web Assembly footprint. -On Windows it may be preferable to use the Linux subsystem. +After building the lexer wasm, build the final distribution components +(lexer.js and lexer.mjs) by running `npm run build`. -After the Web Assembly build, the CJS build can be triggered via `npm run build`. - -Optimization passes are run with [Binaryen](https://github.com/WebAssembly/binaryen) prior to publish to reduce the Web Assembly footprint. +If you need to build lib/lexer.wat (optional) you must first install +[wabt](https://github.com/WebAssembly/wabt) as a sibling folder to this +project. The wat file is then build by running `make lib/lexer.wat` ### License diff --git a/build/Makefile b/build/Makefile new file mode 100755 index 0000000..f13c390 --- /dev/null +++ b/build/Makefile @@ -0,0 +1,13 @@ +lib/lexer.wasm: include-wasm/cjs-module-lexer.h src/lexer.c + @mkdir -p lib + clang --sysroot=/usr/share/wasi-sysroot -target wasm32-unknown-wasi src/lexer.c -I include-wasm -o lib/lexer.wasm -nostartfiles \ + -Wl,-z,stack-size=13312,--no-entry,--compress-relocations,--strip-all,--export=__heap_base,\ + --export=parseCJS,--export=sa,--export=e,--export=re,--export=es,--export=ee,--export=rre,--export=ree,--export=res,--export=ru,--export=us,--export=ue \ + -Wno-logical-op-parentheses -Wno-parentheses \ + -Oz + +optimize: lib/lexer.wasm + ${WASM_OPT} -Oz lib/lexer.wasm -o lib/lexer.wasm + +clean: + rm lib/* diff --git a/build/wasm.js b/build/wasm.js new file mode 100644 index 0000000..58fb640 --- /dev/null +++ b/build/wasm.js @@ -0,0 +1,54 @@ +'use strict' + +const WASM_BUILDER_CONTAINER = 'ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970' // v0.0.9 + +const WASM_OPT = './wasm-opt' + +const { execSync } = require('node:child_process') +const { writeFileSync, readFileSync, existsSync, mkdirSync } = require('node:fs') +const { join, resolve } = require('node:path') + +const ROOT = resolve(__dirname, '../') + +let platform = process.env.WASM_PLATFORM +if (!platform && process.argv[2]) { + platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim() +} + +if (process.argv[2] === '--docker') { + let cmd = `docker run --rm --platform=${platform.toString().trim()} ` + if (process.platform === 'linux') { + cmd += ` --user ${process.getuid()}:${process.getegid()}` + } + + if (!existsSync(`${ROOT}/dist`)){ + mkdirSync(`${ROOT}/dist`); + } + + cmd += ` --mount type=bind,source=${ROOT}/lib,target=/home/node/build/lib \ + --mount type=bind,source=${ROOT}/src,target=/home/node/build/src \ + --mount type=bind,source=${ROOT}/dist,target=/home/node/build/dist \ + --mount type=bind,source=${ROOT}/node_modules,target=/home/node/build/node_modules \ + --mount type=bind,source=${ROOT}/build/wasm.js,target=/home/node/build/wasm.js \ + --mount type=bind,source=${ROOT}/build/Makefile,target=/home/node/build/Makefile \ + --mount type=bind,source=${ROOT}/build.js,target=/home/node/build/build.js \ + --mount type=bind,source=${ROOT}/package.json,target=/home/node/build/package.json \ + --mount type=bind,source=${ROOT}/include-wasm,target=/home/node/build/include-wasm \ + -t ${WASM_BUILDER_CONTAINER} node wasm.js` + console.log(`> ${cmd}\n\n`) + execSync(cmd, { stdio: 'inherit' }) + process.exit(0) +} + +const hasOptimizer = (function () { + try { execSync(`${WASM_OPT} --version`); return true } catch (error) { return false } +})() + +// Build wasm binary +console.log('Building wasm'); +execSync(`make lib/lexer.wasm`, { stdio: 'inherit' }) +if (hasOptimizer) { + console.log('Optimizing wasm'); + execSync(`make optimize`, { stdio: 'inherit' }) +} +execSync(`node build.js`, { stdio: 'inherit' }) diff --git a/lib/lexer.wasm b/lib/lexer.wasm index 261b415f529ccc0163c5827929fcf62d4bfa5975..ae6ac5a0f43621081c1ad45c3524b27f230c6f6f 100755 GIT binary patch literal 22164 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 literal 23849 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