diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94050bd..ab18733 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,6 +155,7 @@ jobs: runs-on: ubuntu-latest env: WASM_PACK: "0.15.0" + CARGO_ABOUT: "0.9.2" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -165,6 +166,11 @@ jobs: - run: | curl -sSL "https://github.com/wasm-bindgen/wasm-pack/releases/download/v$WASM_PACK/wasm-pack-v$WASM_PACK-x86_64-unknown-linux-musl.tar.gz" \ | tar -xz --strip-components=1 -C "$HOME/.cargo/bin" "wasm-pack-v$WASM_PACK-x86_64-unknown-linux-musl/wasm-pack" + # `cargo-about` writes the licence notices `pack.sh` puts in the + # package, from the same release archives `wasm-pack` comes from. + - run: | + curl -sSL "https://github.com/EmbarkStudios/cargo-about/releases/download/$CARGO_ABOUT/cargo-about-$CARGO_ABOUT-x86_64-unknown-linux-musl.tar.gz" \ + | tar -xz --strip-components=1 -C "$HOME/.cargo/bin" "cargo-about-$CARGO_ABOUT-x86_64-unknown-linux-musl/cargo-about" # Every command below runs without the workflow's `RUSTFLAGS`: the # variable replaces the flags of `.cargo/config.toml` instead of # adding to them, and this target would be built without `simd128` diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index c59007e..268a510 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -33,6 +33,7 @@ jobs: contents: write env: WASM_PACK: "0.15.0" + CARGO_ABOUT: "0.9.2" PLAN: ${{ inputs.plan }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -53,6 +54,11 @@ jobs: - run: | curl -sSL "https://github.com/wasm-bindgen/wasm-pack/releases/download/v$WASM_PACK/wasm-pack-v$WASM_PACK-x86_64-unknown-linux-musl.tar.gz" \ | tar -xz --strip-components=1 -C "$HOME/.cargo/bin" "wasm-pack-v$WASM_PACK-x86_64-unknown-linux-musl/wasm-pack" + # `cargo-about` writes the licence notices `pack.sh` puts in the + # package, from the same release archives `wasm-pack` comes from. + - run: | + curl -sSL "https://github.com/EmbarkStudios/cargo-about/releases/download/$CARGO_ABOUT/cargo-about-$CARGO_ABOUT-x86_64-unknown-linux-musl.tar.gz" \ + | tar -xz --strip-components=1 -C "$HOME/.cargo/bin" "cargo-about-$CARGO_ABOUT-x86_64-unknown-linux-musl/cargo-about" - run: crates/sqzer-wasm/pack.sh # The package carries the workspace version, which dist has already # matched against the tag for the binaries. Checked again here diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e28b6a..9bdd8a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Bug Fixes +* **wasm:** the npm package carries `THIRD-PARTY-LICENSES.txt`, the licence notice of every crate compiled into `sqzer_bg.wasm`, generated by `cargo about` from the wasm32 dependency tree as `pack.sh` builds the package. The C library inside `libdeflate-sys` has its own MIT notice there, and `avif-parse`, under MPL-2.0, is listed with its repository. The package shipped its own two licence texts and nothing for the 170-odd crates in the module ([#54](https://github.com/sqzer-dev/sqzer/issues/54)) * **sqzer:** an explicit quality with no format, on a graphic the portable tier sends to lossless WebP, is reported as what it was: `Output::target` is `Resolved::Lossless` and `Output::lossless` is `true`, where the record said the quality asked for and `lossless: false` over the same lossless bytes. `--json`, `{quality}` in a `--template` and the npm package follow ([#56](https://github.com/sqzer-dev/sqzer/issues/56)) ## 0.3.0 diff --git a/CLAUDE.md b/CLAUDE.md index 9648d8b..cc59a86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ cargo build --workspace # portable tier cargo build -p sqzer-cli --features native # needs C libs on PATH / vcpkg cargo build -p sqzer-wasm --target wasm32-unknown-unknown # proves portable builds without a libc; needs clang (cd crates/sqzer-wasm && wasm-pack test --node) # the JavaScript API over the fixtures; needs wasm-pack, and no RUSTFLAGS set -crates/sqzer-wasm/pack.sh # the npm package as a tag publishes it, packed and run from its tarball +crates/sqzer-wasm/pack.sh # the npm package as a tag publishes it, packed and run from its tarball; needs cargo-about and jq cargo test --workspace cargo clippy --workspace --all-targets --all-features # pedantic is on, warnings are errors in CI cargo fmt --all diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d219096..fe627f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,10 @@ cargo build -p sqzer-wasm --target wasm32-unknown-unknown # the browser build's JavaScript API over the fixtures, in Node; needs wasm-pack (cd crates/sqzer-wasm && wasm-pack test --node) + +# the npm package as a tag publishes it, with the licence notices of every crate in +# the module; needs wasm-pack, cargo-about (`cargo install cargo-about --features cli`), jq and Node +crates/sqzer-wasm/pack.sh ``` > **Note**: The golden tests compare each encoder's SSIMULACRA2 score on the fixtures against a committed value. If a change moves a score, say why in the PR; do not just update the number. diff --git a/about.toml b/about.toml new file mode 100644 index 0000000..a96bbe0 --- /dev/null +++ b/about.toml @@ -0,0 +1,116 @@ +# `cargo about` writes the licence notices of everything compiled into a +# build: `crates/sqzer-wasm/pack.sh` runs it over the npm package into +# `THIRD-PARTY-LICENSES.txt` with `crates/sqzer-wasm/about.hbs`. The +# accepted list is the allow-list of `deny.toml`, in the order a crate +# that offers a choice is taken: a crate that is `MIT OR Apache-2.0` +# ships its MIT notice. +accepted = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "MPL-2.0", + "Unicode-3.0", + "Unicode-DFS-2016", + "CC0-1.0", +] +# The package is the wasm32 build; what only a desktop target compiles +# is not in it. Build scripts and their dependencies (`cc`) do not ship, +# and neither does `sqzer-wasm` itself, `publish = false`: the package's +# own licence is next to the file. The four published workspace crates +# are left out by the template, on their repository. +targets = ["wasm32-unknown-unknown"] +ignore-build-dependencies = true +ignore-dev-dependencies = true +private = { ignore = true } +# A notice lives at the root of a crate. Scanned deeper, the tree gave a +# C fuzzing harness in `qoi` and Rust source headers in `kamadak-exif` +# as licence texts, each with a notice inside it. +max-depth = 1 + +# What the scan cannot read on its own, clarified per crate. Every +# checksum pins a file's text, not a crate version: a bump keeps working +# until the notice itself changes, and then `pack.sh` fails until the +# entry is checked again. `files` are read from the crate as published, +# `git` from its repository at the commit it was published from, for a +# crate that ships no notice and keeps one at the root of its repository. +# Two crates stay on the bare licence text: `simd_helpers` has no notice +# in its repository either, and `zune-inflate` names no repository. + +# Vendors and compiles `libdeflate`, a C library under its own MIT +# notice, which the crate's Apache-2.0 field does not carry. +[libdeflate-sys] +clarify = { license = "Apache-2.0 AND MIT", files = [ + { path = "LICENSE", license = "Apache-2.0", checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2" }, + { path = "libdeflate/COPYING", license = "MIT", checksum = "4ad69099cb4374836fd27583d9991e2838cd86b6e4666ab26b2d32582c91e73a" }, +] } + +# Names its licence by file alone, `license-file = "LICENSE"`. +[quantizr] +clarify = { license = "MIT", files = [ + { path = "LICENSE", checksum = "407b2d167f73ef78da1fcbfb6c2f1051f195b1984b9bfeeac0a39b34c198a7fe" }, +] } + +# Notices under a name or in a form the scan does not take. +[libm] +clarify = { license = "MIT", files = [ + { path = "LICENSE.txt", checksum = "3823dda7cf046602f4b4e77ec8e227863dc4736037cc85bb33d9f19febe16bb7" }, +] } +[brotli-decompressor] +clarify = { license = "BSD-3-Clause", files = [ + { path = "LICENSE", checksum = "c0c56f26d9c051cac4d200c34c84e7ae9aaa853e01a982a1df08b09931e518ae" }, +] } +[exr] +clarify = { license = "BSD-3-Clause", files = [ + { path = "LICENSE.md", checksum = "97e4d3aa7a9e8ac31979c16ca8bb2b628a9cb72c3362185ad2bf4ad77f43551f" }, +] } +[tsify] +clarify = { license = "MIT OR Apache-2.0", files = [ + { path = "LICENSE_MIT", license = "MIT", checksum = "23b988ff99c59aba4e37e7709d3d2102096203bb83a077cbc1aad04dd7a77244" }, + { path = "LICENSE_APACHE", license = "Apache-2.0", checksum = "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a" }, +] } + +# Crates published without their notice, read from the repository. +[tsify-macros] +clarify = { license = "MIT", git = [ + { path = "LICENSE_MIT", checksum = "23b988ff99c59aba4e37e7709d3d2102096203bb83a077cbc1aad04dd7a77244" }, +] } +[archmage-macros] +clarify = { license = "MIT", git = [ + { path = "LICENSE-MIT", checksum = "ea17bff5189bc1a38dcad7bd93abeb5a348da5ebeee5891a012b5db27a2a084b" }, +] } +[magetypes] +clarify = { license = "MIT", git = [ + { path = "LICENSE-MIT", checksum = "ea17bff5189bc1a38dcad7bd93abeb5a348da5ebeee5891a012b5db27a2a084b" }, +] } +[enough] +clarify = { license = "MIT", git = [ + { path = "LICENSE-MIT", checksum = "ffa509f359743823fe3cdba2c4dc8feee00be8f07ab299fbbe2b5a50d6ed1ecf" }, +] } +[atomig-macro] +clarify = { license = "MIT", git = [ + { path = "LICENSE-MIT", checksum = "6f54826bbf67526f2d41937c1904ae2de63ef5707b80c68deadbf073b5669c40" }, +] } +[profiling] +clarify = { license = "MIT", git = [ + { path = "LICENSE-MIT", checksum = "c8167fdeeed46d3f244d3f85c5bf998ce889343691c32be2c61a8bc4b5c08333" }, +] } +[profiling-procmacros] +clarify = { license = "MIT", git = [ + { path = "LICENSE-MIT", checksum = "c8167fdeeed46d3f244d3f85c5bf998ce889343691c32be2c61a8bc4b5c08333" }, +] } +[pulp-wasm-simd-flag] +clarify = { license = "MIT", git = [ + { path = "LICENSE", checksum = "d64f878c89bd5f1e5ada7e4aad57690b122727cf5229b7b87ea1980c188da1d9" }, +] } +[alloc-stdlib] +clarify = { license = "BSD-3-Clause", git = [ + { path = "LICENSE", checksum = "c0c56f26d9c051cac4d200c34c84e7ae9aaa853e01a982a1df08b09931e518ae" }, +] } +[fast-ssim2] +clarify = { license = "BSD-2-Clause", git = [ + { path = "LICENSE", checksum = "acc9e236b0011c638714a16f5634fa5bcd3be111182e237c3049db700c3521e2" }, +] } diff --git a/crates/sqzer-wasm/README.md b/crates/sqzer-wasm/README.md index 4a2941b..eaf7a8c 100644 --- a/crates/sqzer-wasm/README.md +++ b/crates/sqzer-wasm/README.md @@ -146,4 +146,4 @@ await init({ module_or_path: await readFile(new URL(import.meta.resolve("sqzer/s ## Licence -MIT or Apache-2.0, at your option. +MIT or Apache-2.0, at your option. `THIRD-PARTY-LICENSES.txt` in the package carries the notice of every crate compiled into `sqzer_bg.wasm`, the C library inside `libdeflate-sys` included; a page that bundles the package ships it alongside, as most of those licences ask. diff --git a/crates/sqzer-wasm/about.hbs b/crates/sqzer-wasm/about.hbs new file mode 100644 index 0000000..2ff059f --- /dev/null +++ b/crates/sqzer-wasm/about.hbs @@ -0,0 +1,22 @@ +Third-party notices for the npm package sqzer + +The package itself is MIT OR Apache-2.0, in LICENSE-MIT and LICENSE-APACHE +next to this file. sqzer_bg.wasm is compiled from the crates below, each +under the licence named, with the notice the crate ships, or the one its +repository holds where the crate ships none. A crate under MPL-2.0 has its +source at the repository given with it. + +{{#each licenses}} +{{#unless (eq used_by.0.crate.repository "https://github.com/sqzer-dev/sqzer")}} +================================================================================ +{{{name}}} + +Used by: +{{#each used_by}} + {{{crate.name}}} {{{crate.version}}}{{#if crate.repository}} {{{crate.repository}}}{{/if}} +{{/each}} + +{{{text}}} + +{{/unless}} +{{/each}} diff --git a/crates/sqzer-wasm/pack.sh b/crates/sqzer-wasm/pack.sh index e385555..e7af69b 100755 --- a/crates/sqzer-wasm/pack.sh +++ b/crates/sqzer-wasm/pack.sh @@ -4,7 +4,7 @@ # (ADR-0011 D5), and what the wasm job of `ci.yml` rehearses on every pull # request. Leaves `pkg/sqzer-.tgz`. # -# Needs `wasm-pack`, `clang`, `jq` and Node 22.14 or later. +# Needs `wasm-pack`, `cargo-about`, `clang`, `jq` and Node 22.14 or later. set -euo pipefail cd "$(dirname "$0")" @@ -19,7 +19,25 @@ env -u RUSTFLAGS wasm-pack build --release --target web --out-name sqzer # the licence texts in the crate directory; they are at the repository # root. cp ../../LICENSE-MIT ../../LICENSE-APACHE pkg/ -jq '.name = "sqzer" | .files += ["snippets", "LICENSE-MIT", "LICENSE-APACHE"]' \ + +# The notices of every crate compiled into the module, from `about.toml` +# at the repository root and `about.hbs` here. Most of their licences ask +# that the copyright notice travels with a binary; this is where it +# travels. `--fail` refuses a crate whose licence cannot be read. A stale +# clarification in `about.toml`, a crate whose licence file moved with a +# bump, is only a warning to `cargo about`, which then drops that notice +# and goes on; here a warning fails the build, so nothing is left out +# without anyone knowing. +about_log=$(cargo about generate --fail --locked --target wasm32-unknown-unknown \ + -o pkg/THIRD-PARTY-LICENSES.txt about.hbs 2>&1) || { echo "$about_log" >&2; exit 1; } +if grep -q WARN <<< "$about_log"; then + echo "$about_log" >&2 + echo "cargo about warned: a notice is missing, or a clarification in about.toml is stale" >&2 + exit 1 +fi +test -s pkg/THIRD-PARTY-LICENSES.txt + +jq '.name = "sqzer" | .files += ["snippets", "LICENSE-MIT", "LICENSE-APACHE", "THIRD-PARTY-LICENSES.txt"]' \ pkg/package.json > pkg/package.json.new mv pkg/package.json.new pkg/package.json diff --git a/crates/sqzer-wasm/tests/package.mjs b/crates/sqzer-wasm/tests/package.mjs index d232ed7..822e8fa 100644 --- a/crates/sqzer-wasm/tests/package.mjs +++ b/crates/sqzer-wasm/tests/package.mjs @@ -17,6 +17,12 @@ assert.equal(manifest.license, "MIT OR Apache-2.0"); for (const file of ["LICENSE-MIT", "LICENSE-APACHE", "README.md", "sqzer.d.ts"]) { await readFile(installed(file)); } +// The notices of the crates in the module, one `Used by` block per +// licence text, with the C library inside `libdeflate-sys` among them. +const notices = await readFile(installed("THIRD-PARTY-LICENSES.txt"), "utf8"); +for (const needle of ["Used by:", "ravif", "mozjpeg-rs", "libdeflate-sys", "Eric Biggers", "avif-parse"]) { + assert.ok(notices.includes(needle), `THIRD-PARTY-LICENSES.txt lacks ${needle}`); +} await init({ module_or_path: await readFile(installed("sqzer_bg.wasm")) });