Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions .github/workflows/build-prebuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Build canvas prebuild

on:
workflow_dispatch:
inputs:
release_tag:
description: "Release tag (must match v$(package.json version), e.g. v3.2.3-memfix.0)"
required: true
default: v3.2.3-memfix.0

permissions:
contents: write

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
target: linux-x64
- os: macos-14
target: darwin-arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24

- name: Install build deps (Linux)
if: matrix.target == 'linux-x64'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential python3 \
libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev \
patchelf

- name: Install build deps (macOS)
if: matrix.target == 'darwin-arm64'
run: |
brew install pkg-config cairo pango jpeg giflib librsvg dylibbundler

- name: Sanity-check package.json version matches input
run: |
pkg_version="v$(node -p "require('./package.json').version")"
if [ "$pkg_version" != "${{ inputs.release_tag }}" ]; then
echo "::error::package.json version ($pkg_version) does not match release_tag (${{ inputs.release_tag }})"
exit 1
fi

- name: Compile native binding
run: |
npm ci --ignore-scripts --no-audit --no-fund
npx node-gyp rebuild --release

- name: Bundle shared libs (Linux)
if: matrix.target == 'linux-x64'
run: |
set -ex
rm -rf build/Release/.deps build/Release/obj.target
patchelf --set-rpath '$ORIGIN' build/Release/canvas.node
copies=$(ldd build/Release/canvas.node \
| awk '/=> \// {print $3}' \
| grep -vE '/(ld-linux|libc|libm|libdl|libpthread|librt|libresolv|libnsl|libutil|libcrypt)[-.][^/]*\.so' \
| sort -u)
if [ -z "$copies" ]; then
echo "::error::ldd resolved no bundleable deps"
ldd build/Release/canvas.node
exit 1
fi
for so in $copies; do
base=$(basename "$so")
cp -L "$so" "build/Release/$base"
patchelf --set-rpath '$ORIGIN' "build/Release/$base"
done
find build/Release -type f -name '*.so*' -exec strip --strip-unneeded {} \;
ls -la build/Release

- name: Bundle shared libs (macOS)
if: matrix.target == 'darwin-arm64'
run: |
set -ex
rm -rf build/Release/.deps build/Release/obj.target

# dylibbundler clobbers the binary when -x and -d resolve to the same
# directory. Stage the binary in a temp dir, let dylibbundler rewrite
# its install names relative to @loader_path, then move it back.
stage=$(mktemp -d)
cp build/Release/canvas.node "$stage/canvas.node"

dylibbundler -od -b -of \
-x "$stage/canvas.node" \
-d build/Release/ \
-p '@loader_path/'

mv -f "$stage/canvas.node" build/Release/canvas.node
ls -la build/Release

- name: Pack tarball
id: pack
run: |
tag="${{ inputs.release_tag }}"
filename="canvas-${tag}-napi-v7-${{ matrix.target }}.tar.gz"
tar -czf "$filename" build
echo "filename=$filename" >> "$GITHUB_OUTPUT"

- name: Smoke test (Linux, clean container)
if: matrix.target == 'linux-x64'
run: |
docker run --rm \
-v "$PWD/${{ steps.pack.outputs.filename }}:/tmp/prebuild.tar.gz:ro" \
-w /work node:24-slim bash -c '
set -ex
mkdir -p /work && cd /work
tar -xzf /tmp/prebuild.tar.gz
node -e "process.dlopen({exports:{}}, \"./build/Release/canvas.node\"); console.log(\"loaded ok\")"
'

- name: Smoke test (macOS, fresh dir, no DYLD_*)
if: matrix.target == 'darwin-arm64'
run: |
set -ex
workdir=$(mktemp -d)
tar -xzf "${{ steps.pack.outputs.filename }}" -C "$workdir"
cd "$workdir"
unset DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH
node -e 'process.dlopen({exports:{}}, "./build/Release/canvas.node"); console.log("loaded ok")'

- uses: actions/upload-artifact@v4
with:
name: prebuild-${{ matrix.target }}
path: ${{ steps.pack.outputs.filename }}
if-no-files-found: error

publish:
needs: build
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

- name: List artifacts
run: ls -la artifacts/

- name: Publish release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${{ inputs.release_tag }}"
if gh release view "$tag" >/dev/null 2>&1; then
gh release upload "$tag" artifacts/*.tar.gz --clobber
else
gh release create "$tag" \
--target "${{ github.sha }}" \
--title "$tag" \
--notes "Temporary prebuild with memory-leak fix from PR Automattic/node-canvas#2562. Built by ${{ github.workflow }} run ${{ github.run_id }}." \
artifacts/*.tar.gz
fi
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ out.pdf
out.svg
.pomo
node_modules
package-lock.json

# Vim cruft
*.swp
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ project adheres to [Semantic Versioning](http://semver.org/).
(Unreleased)
==================
### Changed
* Upgrade node-addon-api from 7.x to 8.x
### Added
### Fixed
* Fix memory leak caused by N-API weak reference callbacks being deferred to SetImmediate instead of running during GC. Enabled `NAPI_EXPERIMENTAL` to use `node_api_nogc_finalize` for ObjectWrap destructor. (#2436)

3.2.3
==================
Expand Down
2 changes: 1 addition & 1 deletion binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
{
'target_name': 'canvas',
'include_dirs': ["<!(node -p \"require('node-addon-api').include_dir\")"],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS', 'NODE_ADDON_API_ENABLE_MAYBE' ],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS', 'NODE_ADDON_API_ENABLE_MAYBE', 'NAPI_EXPERIMENTAL' ],
'sources': [
'src/bmp/BMPParser.cc',
'src/Canvas.cc',
Expand Down
6 changes: 5 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ function loadImage (src) {
}

image.onload = () => { cleanup(); resolve(image) }
image.onerror = (err) => { cleanup(); reject(err) }
image.onerror = (err) => {
cleanup()
image.src = Buffer.alloc(0)
reject(err)
}

image.src = src
})
Expand Down
Loading