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
148 changes: 45 additions & 103 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,118 +1,60 @@
name: CI
name: Patched native packages
on:
push:
branches:
- main
- 'temp-*'
tags:
- 'v*'
branches: [main]
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions: {}
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: native-${{ github.ref }}
cancel-in-progress: true
jobs:
check-latest-versions:
permissions:
contents: read
runs-on: 'ubuntu-24.04'
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Check latest versions
run: ./check-latest-versions.sh && git diff --quiet
build:
permissions:
contents: read
name: build-${{ matrix.platform }}
runs-on: ${{ matrix.os }}
needs: check-latest-versions
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- os: 'ubuntu-24.04'
platform: 'linux-x64'
- os: 'ubuntu-24.04'
platform: 'linux-armv6'
- os: 'ubuntu-24.04'
platform: 'linuxmusl-x64'
- os: 'ubuntu-24.04'
platform: 'linux-ppc64le'
- os: 'ubuntu-24.04'
platform: 'linux-riscv64'
- os: 'ubuntu-24.04'
platform: 'linux-s390x'
- os: 'ubuntu-24.04'
platform: 'dev-wasm32'
- os: 'ubuntu-24.04'
platform: 'win32-ia32'
- os: 'ubuntu-24.04'
platform: 'win32-x64'
- os: 'ubuntu-24.04'
platform: 'win32-arm64v8'
- os: 'macos-15-intel'
platform: 'darwin-x64'
- os: 'macos-15'
platform: 'darwin-arm64v8'
- os: 'ubuntu-24.04-arm'
platform: 'linux-arm64v8'
- os: 'ubuntu-24.04-arm'
platform: 'linuxmusl-arm64v8'
- runner: ubuntu-24.04
platform: linux-x64
- runner: ubuntu-24.04
platform: linuxmusl-x64
- runner: ubuntu-24.04-arm
platform: linuxmusl-arm64v8
- runner: macos-15
platform: darwin-arm64v8
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install dependencies (macOS)
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24'
- name: Install macOS build tools
if: runner.os == 'macOS'
run: |
pipx install meson
pipx install meson==1.12.0
brew install automake nasm
- name: Build ${{ matrix.platform }}
run: ./build.sh ${{ matrix.platform }}
- name: Upload ${{ matrix.platform }} artifact
uses: actions/upload-artifact@v6
- name: Build native dependencies
env:
BUILD_PLATFORM: ${{ matrix.platform }}
run: ./build.sh "$BUILD_PLATFORM"
- name: Pack Cosmos package
env:
BUILD_PLATFORM: ${{ matrix.platform }}
run: node scripts/pack-cosmos.mjs "$BUILD_PLATFORM"
- name: Verify native package with upstream Sharp
timeout-minutes: 10
env:
BUILD_PLATFORM: ${{ matrix.platform }}
run: bash scripts/test-package.sh "$BUILD_PLATFORM"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-${{ matrix.platform }}
path: '*.tar.gz'
compression-level: 0
retention-days: 1
name: npm-${{ matrix.platform }}
path: dist/*.tgz
if-no-files-found: error
populate-and-publish-npm-workspace:
permissions:
contents: write
id-token: write
needs: build
runs-on: ubuntu-24.04
steps:
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Checkout
uses: actions/checkout@v6
- name: Download build artifacts
uses: actions/download-artifact@v7
with:
merge-multiple: true
- name: Populate npm workspace
run: ./populate-npm-workspace.sh
- name: Create npm workspace tarball
run: tar -vcaf npm-workspace.tar.xz -C npm .
- name: Generate release notes
run: ./generate-release-notes.sh
- name: Create GitHub release for tag
if: startsWith(github.ref, 'refs/tags/v')
uses: ncipollo/release-action@v1
with:
artifacts: npm-workspace.tar.xz
artifactContentType: application/x-xz
bodyFile: release-notes.md
prerelease: ${{ contains(github.ref, '-rc') }}
makeLatest: ${{ !contains(github.ref, '-rc') }}
- name: Publish npm packages
if: startsWith(github.ref, 'refs/tags/v')
run: cd npm && npm publish --workspaces --tag=${{ contains(github.ref, '-rc') && 'next' || 'latest' }}
retention-days: 14
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ npm/*/*
!npm/dev-wasm32/THIRD-PARTY-NOTICES.md
npm/img-sharp-libvips-*.tgz
release-notes.md

dist/
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# Cosmos patched Sharp native packages

ENG-14549: this fork builds libheif 1.23.4 with libvips 8.18.6 for upstream Sharp 0.35.4.
The Cosmos CI produces scoped `@cosmos-entity/sharp-libvips-<platform>@1.3.3-cosmos.1`
packages. It never publishes to public npm and has no Nexus credentials.

After CI succeeds, the trusted `cosmos-actions` vendor publisher verifies the build
repository, run, and commit before publishing the exact tarballs to Nexus npm-hosted.
Consumers read through `https://nexus.m.cos.ms/repository/npm-group/` and override
`@img/sharp-libvips-<platform>` with the matching scoped Cosmos package. Test every
native addon/platform pairing; upgrading a lockfile alone does not prove the loaded
library changed. Deployments must verify their final artifacts.

Production targets are glibc x64 (Lambda) and musl arm64 (cosmos-www). CI also builds
musl x64 and macOS arm64. Other platforms are not yet covered by this remediation.
All packages retain upstream notices and a BUILD-PROVENANCE.json with source/run IDs.
The source for modified libraries remains available through the recorded versions;
this repository contains the build recipes. Upstream's uncompressed codec and HEVC
feature choices are preserved; adding HEVC decoding is a separate change (ENG-11148).

Remove consumer overrides once upstream publishes and verifies equivalent fixes.
Review libheif releases regularly; 1.23.4 is a security baseline, not a guarantee
against future vulnerabilities. Build system packages still track distro updates;
we do not claim bit-for-bit reproducibility.

## Upstream documentation

# Packaging scripts

libvips and its dependencies are provided as pre-compiled shared libraries
Expand Down
14 changes: 10 additions & 4 deletions build/posix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ CURL="curl --silent --location --retry 3 --retry-max-time 30"

if [ "$DARWIN" = true ]; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
| sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly-2026-09-01
export RUSTFLAGS+=" -Zlocation-detail=none -Zfmt-debug=none"
CFLAGS= cargo install cargo-c --locked
CFLAGS= cargo install cargo-c --version 0.10.25 --locked
fi

if [ "${PLATFORM%-*}" == "linuxmusl" ] || [ "$DARWIN" = true ]; then
Expand Down Expand Up @@ -187,7 +187,13 @@ AOM_AS_FLAGS="${FLAGS}" cmake -G"Unix Makefiles" \
make install/strip

mkdir ${DEPS}/heif
$CURL https://github.com/strukturag/libheif/releases/download/v${VERSION_HEIF}/libheif-${VERSION_HEIF}.tar.gz | tar xzC ${DEPS}/heif --strip-components=1
$CURL https://github.com/strukturag/libheif/releases/download/v${VERSION_HEIF}/libheif-${VERSION_HEIF}.tar.gz > ${DEPS}/libheif.tar.gz
if [ "$DARWIN" = true ]; then
echo "d0c02b4b0e978f34a1974b6f3eea7975a537bf7a9195ffeea38e7242ff316fdd ${DEPS}/libheif.tar.gz" | shasum -a 256 -c -
else
echo "d0c02b4b0e978f34a1974b6f3eea7975a537bf7a9195ffeea38e7242ff316fdd ${DEPS}/libheif.tar.gz" | sha256sum -c -
fi
tar xzf ${DEPS}/libheif.tar.gz -C ${DEPS}/heif --strip-components=1
cd ${DEPS}/heif
CFLAGS="${CFLAGS} -O3" CXXFLAGS="${CXXFLAGS} -O3" cmake -G"Unix Makefiles" \
-DCMAKE_TOOLCHAIN_FILE=${ROOT}/Toolchain.cmake -DCMAKE_INSTALL_PREFIX=${TARGET} -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_BUILD_TYPE=Release \
Expand Down Expand Up @@ -477,7 +483,7 @@ printf "{\n\
}" >versions.json

# Add third-party notices
$CURL -O https://raw.githubusercontent.com/lovell/sharp-libvips/main/THIRD-PARTY-NOTICES.md
cp "${PACKAGE}/THIRD-PARTY-NOTICES.md" .

# Create the tarball
ls -al lib
Expand Down
20 changes: 20 additions & 0 deletions docs/superpowers/plans/2026-09-18-patched-libheif.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Patched libheif native packages

ENG-14549. Approved design: retain upstream Sharp, rebuild its native dependency,
and publish distinct Cosmos packages through the private cosmos-actions publisher.

- [x] Fork upstream at f5f1839e29ed98c2a1d0030e23bf2d403ab3b699 (libheif 1.23.4, libvips 8.18.6).
- [ ] Build glibc x64 (Lambda), musl arm64 (web production), musl x64 (CI), Darwin arm64 (development).
- [ ] Package exact versions with provenance and third-party notices.
- [ ] Smoke-test clean Sharp 0.35.4 installs using dependency overrides in target runtimes.
- [ ] Publish successful exact-head artifacts through cosmos-actions, verify Nexus download integrity.
- [ ] Verify clean Nexus consumer installs before unblocking ENG-14550 and ENG-14551.

Local validation: node --check scripts/pack-cosmos.mjs, node --check scripts/smoke.cjs,
bash -n scripts/test-package.sh, actionlint .github/workflows/ci.yml.
Native compilation and image-decoding regression checks belong to CI.

Do not publish from public fork workflows or expose Nexus credentials there.
Source dependency versions are in versions.properties; runner and distro package
updates mean builds are not claimed to be bit-for-bit reproducible. Record the
exact source SHA, build run, versions and immutable package integrity for consumers.
4 changes: 2 additions & 2 deletions platforms/linux-x64/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ RUN \
curl https://sh.rustup.rs -sSf | sh -s -- -y \
--no-modify-path \
--profile minimal \
--default-toolchain nightly \
--default-toolchain nightly-2026-09-01 \
&& \
cargo install cargo-c --locked && \
cargo install cargo-c --version 0.10.25 --locked && \
pip3.12 install meson ninja

# Compiler settings
Expand Down
4 changes: 2 additions & 2 deletions platforms/linuxmusl-arm64v8/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ RUN \
--no-modify-path \
--profile minimal \
--default-host aarch64-unknown-linux-musl \
--default-toolchain nightly \
--default-toolchain nightly-2026-09-01 \
&& \
cargo install cargo-c --locked && \
cargo install cargo-c --version 0.10.25 --locked && \
pipx install meson

# Compiler settings
Expand Down
4 changes: 2 additions & 2 deletions platforms/linuxmusl-x64/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ RUN \
curl https://sh.rustup.rs -sSf | sh -s -- -y \
--no-modify-path \
--profile minimal \
--default-toolchain nightly \
--default-toolchain nightly-2026-09-01 \
&& \
cargo install cargo-c --locked && \
cargo install cargo-c --version 0.10.25 --locked && \
pipx install meson

# Compiler settings
Expand Down
38 changes: 38 additions & 0 deletions scripts/pack-cosmos.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { resolve } from 'node:path';

const platforms = new Map([
['linux-x64', 'linux-x64'],
['linuxmusl-x64', 'linuxmusl-x64'],
['linuxmusl-arm64v8', 'linuxmusl-arm64'],
['darwin-arm64v8', 'darwin-arm64'],
]);
const platform = process.argv[2];
const target = platforms.get(platform);
if (!target) throw new Error('Unsupported Cosmos platform');
const directory = resolve('npm', target);
execFileSync('tar', ['xzf', `sharp-libvips-${platform}.tar.gz`, '-C', directory]);
const versions = JSON.parse(readFileSync(`${directory}/versions.json`));
if (versions.heif !== '1.23.4' || versions.vips !== '8.18.6') {
throw new Error('Unexpected libheif/libvips version');
}
const pkg = JSON.parse(readFileSync(`${directory}/package.json`));
pkg.name = `@cosmos-entity/sharp-libvips-${target}`;
pkg.version = '1.3.3-cosmos.1';
pkg.repository.url = 'git+https://github.com/Cosmos-Entity/sharp-libvips.git';
delete pkg.publishConfig;
pkg.files = ['lib', 'versions.json', 'BUILD-PROVENANCE.json', 'THIRD-PARTY-NOTICES.md', 'SOURCE-LICENSE'];
pkg.exports['./binary'] = `./lib/${target.startsWith('darwin') ? 'libvips-cpp.8.18.6.dylib' : 'libvips-cpp.so.8.18.6'}`;
writeFileSync(`${directory}/package.json`, `${JSON.stringify(pkg, null, 2)}\n`);
writeFileSync(`${directory}/lib/index.js`, 'module.exports = __dirname;\n');
copyFileSync('LICENSE', `${directory}/SOURCE-LICENSE`);
writeFileSync(`${directory}/BUILD-PROVENANCE.json`, `${JSON.stringify({
repository: 'https://github.com/Cosmos-Entity/sharp-libvips',
commit: execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(),
upstreamBase: 'f5f1839e29ed98c2a1d0030e23bf2d403ab3b699',
platform, versions,
buildRun: process.env.GITHUB_RUN_ID ?? null,
}, null, 2)}\n`);
mkdirSync('dist', { recursive: true });
execFileSync('npm', ['pack', directory, '--ignore-scripts', '--pack-destination', resolve('dist')], { stdio: 'inherit' });
29 changes: 29 additions & 0 deletions scripts/smoke.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const assert = require('node:assert/strict');
const sharp = require('sharp');
const deadline = setTimeout(() => {
console.error('Image decoder smoke test exceeded 60 seconds');
process.exit(1);
}, 60_000);
deadline.unref();

async function main() {
assert.equal(sharp.versions.heif, '1.23.4');
assert.equal(sharp.versions.vips, '8.18.6');
const pixels = Buffer.alloc(32 * 32 * 3, 128);
for (const format of ['png', 'jpeg', 'webp', 'avif']) {
const encoded = await sharp(pixels, { raw: { width: 32, height: 32, channels: 3 } })
.toFormat(format).toBuffer();
const metadata = await sharp(encoded).metadata();
assert.equal(metadata.width, 32);
const resized = await sharp(encoded).resize(16, 16).png().toBuffer();
assert.equal((await sharp(resized).metadata()).width, 16);
if (format === 'avif') {
await assert.rejects(sharp(encoded.subarray(0, Math.floor(encoded.length / 2))).raw().toBuffer());
// A failed HEIF parse must not prevent decoding a subsequent valid image.
assert.equal((await sharp(encoded).raw().toBuffer()).length, pixels.length);
}
}
await assert.rejects(sharp(Buffer.from('not an image')).metadata());
console.log(JSON.stringify(sharp.versions));
}
main().catch(error => { console.error(error); process.exitCode = 1; });
27 changes: 27 additions & 0 deletions scripts/test-package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
platform="$1"
case "$platform" in
linux-x64) runtime_platform=linux-x64 ;;
linuxmusl-x64) runtime_platform=linuxmusl-x64 ;;
linuxmusl-arm64v8) runtime_platform=linuxmusl-arm64 ;;
darwin-arm64v8) runtime_platform=darwin-arm64 ;;
*) exit 2 ;;
esac
package="$PWD/dist/cosmos-entity-sharp-libvips-${runtime_platform}-1.3.3-cosmos.1.tgz"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
cp "$package" "$test_dir/native.tgz"
cp scripts/smoke.cjs "$test_dir/smoke.cjs"
printf '{"private":true,"dependencies":{"sharp":"0.35.4"},"overrides":{"@img/sharp-libvips-%s":"file:./native.tgz"}}\n' "$runtime_platform" > "$test_dir/package.json"
case "$platform" in
linux-x64)
docker run --rm --user "$(id -u):$(id -g)" -e npm_config_cache=/tmp/npm-cache --memory=1g --cpus=2 --pids-limit=256 --entrypoint /bin/bash -v "$test_dir:/var/task" -w /var/task public.ecr.aws/lambda/nodejs:24 -c 'npm install --ignore-scripts && node --max-old-space-size=256 smoke.cjs'
;;
linuxmusl-*)
docker run --rm --user "$(id -u):$(id -g)" -e npm_config_cache=/tmp/npm-cache --memory=1g --cpus=2 --pids-limit=256 -v "$test_dir:/app" -w /app node:24-alpine sh -c 'npm install --ignore-scripts && node --max-old-space-size=256 smoke.cjs'
;;
darwin-arm64v8)
(cd "$test_dir" && npm install --ignore-scripts && node smoke.cjs)
;;
esac