diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 243ae116f..6909a5c1a 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -301,6 +301,7 @@ jobs: npm install --force @rollup/rollup-linux-x64-gnu npm install --force @rollup/rollup-linux-arm64-gnu npm install --force @rollup/rollup-linux-arm-gnueabihf + node scripts/install-linux-sharp.cjs # Bundled plugins are counted as downloads in the plugin store, so a # build only fetches them when the pins change. diff --git a/scripts/install-linux-sharp.cjs b/scripts/install-linux-sharp.cjs new file mode 100644 index 000000000..9e32536f4 --- /dev/null +++ b/scripts/install-linux-sharp.cjs @@ -0,0 +1,40 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); + +function findPackage(start) { + let directory = path.dirname(start); + while (directory !== path.dirname(directory)) { + const manifest = path.join(directory, "package.json"); + if (fs.existsSync(manifest)) return JSON.parse(fs.readFileSync(manifest)); + directory = path.dirname(directory); + } + throw new Error("Could not locate the installed sharp package manifest"); +} + +// The build runs on x64 but ships arm64 and armv7l too, so npm ci alone +// only installs the x64 binaries. +function linuxSharpPackages(sharpPackage) { + return ["x64", "arm64", "arm"].flatMap((arch) => + [`@img/sharp-linux-${arch}`, `@img/sharp-libvips-linux-${arch}`].map( + (name) => { + const version = sharpPackage.optionalDependencies?.[name]; + if (!version) { + throw new Error(`sharp does not declare ${name} as a dependency`); + } + return `${name}@${version}`; + }, + ), + ); +} + +module.exports = { linuxSharpPackages }; + +if (require.main === module) { + const sharpPackage = findPackage(require.resolve("sharp")); + execFileSync( + "npm", + ["install", "--force", "--no-save", ...linuxSharpPackages(sharpPackage)], + { stdio: "inherit" }, + ); +} diff --git a/scripts/install-linux-sharp.test.ts b/scripts/install-linux-sharp.test.ts new file mode 100644 index 000000000..4aafc8944 --- /dev/null +++ b/scripts/install-linux-sharp.test.ts @@ -0,0 +1,58 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const require = createRequire(import.meta.url); +const { linuxSharpPackages } = require("./install-linux-sharp.cjs"); + +const lock = JSON.parse( + fs.readFileSync(path.join(__dirname, "../package-lock.json"), "utf8"), +); +const sharpManifest = lock.packages["node_modules/sharp"]; + +describe("Linux sharp packaging", () => { + // The build runs on x64 but ships arm64 and armv7l too, so npm ci alone + // only installs the x64 binaries and the arm AppImages cannot load sharp. + it("pins the sharp and libvips binaries of every shipped Linux architecture", () => { + const packages = linuxSharpPackages({ + optionalDependencies: { + "@img/sharp-linux-x64": "1.0.0", + "@img/sharp-linux-arm64": "1.0.1", + "@img/sharp-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-x64": "2.0.0", + "@img/sharp-libvips-linux-arm64": "2.0.1", + "@img/sharp-libvips-linux-arm": "2.0.2", + "@img/sharp-win32-x64": "9.9.9", + }, + }); + + expect([...packages].sort()).toEqual([ + "@img/sharp-libvips-linux-arm64@2.0.1", + "@img/sharp-libvips-linux-arm@2.0.2", + "@img/sharp-libvips-linux-x64@2.0.0", + "@img/sharp-linux-arm64@1.0.1", + "@img/sharp-linux-arm@1.0.2", + "@img/sharp-linux-x64@1.0.0", + ]); + }); + + it("only names packages the installed sharp really declares", () => { + const packages = linuxSharpPackages(sharpManifest); + expect(packages).toHaveLength(6); + for (const spec of packages) { + expect(spec).toMatch(/^@img\/sharp-[a-z0-9-]+@\d+\.\d+\.\d+$/); + } + }); + + it("fails loudly when sharp stops declaring one of them", () => { + expect(() => + linuxSharpPackages({ + optionalDependencies: { + "@img/sharp-linux-x64": "1.0.0", + "@img/sharp-libvips-linux-x64": "2.0.0", + }, + }), + ).toThrow(/sharp-linux-arm64/); + }); +});