mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
fix: package sharp for both macOS architectures (#1344)
This commit is contained in:
@@ -427,6 +427,7 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
npm install --force @rollup/rollup-darwin-arm64
|
npm install --force @rollup/rollup-darwin-arm64
|
||||||
npm install dmg-license
|
npm install dmg-license
|
||||||
|
node scripts/install-macos-sharp.cjs
|
||||||
|
|
||||||
- name: Check for Code Signing Certificates
|
- name: Check for Code Signing Certificates
|
||||||
id: check_certs
|
id: check_certs
|
||||||
@@ -538,6 +539,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
npx electron-builder --mac dmg --universal --x64 --arm64 --publish never
|
npx electron-builder --mac dmg --universal --x64 --arm64 --publish never
|
||||||
|
|
||||||
|
- name: Verify macOS sharp packaging
|
||||||
|
run: |
|
||||||
|
artifacts=(release/termix_macos_*_dmg.dmg)
|
||||||
|
if [ -f release/termix_macos_universal_mas.pkg ]; then
|
||||||
|
artifacts+=(release/termix_macos_universal_mas.pkg)
|
||||||
|
fi
|
||||||
|
node scripts/verify-macos-sharp.cjs "${artifacts[@]}"
|
||||||
|
|
||||||
- name: Upload macOS MAS PKG
|
- name: Upload macOS MAS PKG
|
||||||
if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/termix_macos_universal_mas.pkg') != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit')
|
if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/termix_macos_universal_mas.pkg') != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit')
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -923,6 +932,9 @@ jobs:
|
|||||||
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
|
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
|
||||||
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
|
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Verify macOS sharp packaging
|
||||||
|
run: node scripts/verify-macos-sharp.cjs release_asset/termix_macos_universal_dmg.dmg
|
||||||
|
|
||||||
- name: Prepare Homebrew submission files
|
- name: Prepare Homebrew submission files
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.package-version.outputs.version }}"
|
VERSION="${{ steps.package-version.outputs.version }}"
|
||||||
@@ -1013,6 +1025,7 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
npm install --force @rollup/rollup-darwin-arm64
|
npm install --force @rollup/rollup-darwin-arm64
|
||||||
npm install dmg-license
|
npm install dmg-license
|
||||||
|
node scripts/install-macos-sharp.cjs
|
||||||
|
|
||||||
- name: Check for Code Signing Certificates
|
- name: Check for Code Signing Certificates
|
||||||
id: check_certs
|
id: check_certs
|
||||||
@@ -1154,6 +1167,10 @@ jobs:
|
|||||||
BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}"
|
BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}"
|
||||||
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"
|
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"
|
||||||
|
|
||||||
|
- name: Verify macOS sharp packaging
|
||||||
|
if: steps.check_certs.outputs.has_certs == 'true'
|
||||||
|
run: node scripts/verify-macos-sharp.cjs release/termix_macos_universal_mas.pkg
|
||||||
|
|
||||||
- name: Generate App Store release notes
|
- name: Generate App Store release notes
|
||||||
id: asc_notes
|
id: asc_notes
|
||||||
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharpPackage = findPackage(require.resolve("sharp"));
|
||||||
|
const packages = [
|
||||||
|
"@img/sharp-darwin-arm64",
|
||||||
|
"@img/sharp-darwin-x64",
|
||||||
|
"@img/sharp-libvips-darwin-arm64",
|
||||||
|
"@img/sharp-libvips-darwin-x64",
|
||||||
|
].map((name) => `${name}@${sharpPackage.optionalDependencies[name]}`);
|
||||||
|
|
||||||
|
execFileSync(
|
||||||
|
process.platform === "win32" ? "npm.cmd" : "npm",
|
||||||
|
["install", "--force", "--no-save", ...packages],
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const os = require("node:os");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { execFileSync, spawnSync } = require("node:child_process");
|
||||||
|
|
||||||
|
const architectures = {
|
||||||
|
x64: ["x64"],
|
||||||
|
arm64: ["arm64"],
|
||||||
|
universal: ["x64", "arm64"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function expectedArchitecture(artifact) {
|
||||||
|
const name = path.basename(artifact);
|
||||||
|
if (name.includes("_x64_")) return "x64";
|
||||||
|
if (name.includes("_arm64_")) return "arm64";
|
||||||
|
if (name.includes("_universal_")) return "universal";
|
||||||
|
throw new Error(`Cannot determine architecture from artifact name: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findApp(root) {
|
||||||
|
const pending = [root];
|
||||||
|
while (pending.length) {
|
||||||
|
const current = pending.pop();
|
||||||
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
|
const candidate = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory() && entry.name.endsWith(".app")) return candidate;
|
||||||
|
if (entry.isDirectory()) pending.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`No .app bundle found below ${root}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsFile(root, suffix) {
|
||||||
|
const pending = [root];
|
||||||
|
while (pending.length) {
|
||||||
|
const current = pending.pop();
|
||||||
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
|
const candidate = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) pending.push(candidate);
|
||||||
|
if (entry.isFile() && entry.name.endsWith(suffix)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyApp(app, architecture, runtimeCheck) {
|
||||||
|
const modules = path.join(
|
||||||
|
app,
|
||||||
|
"Contents/Resources/app.asar.unpacked/node_modules",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const arch of architectures[architecture]) {
|
||||||
|
for (const [packageName, nativeSuffix] of [
|
||||||
|
[`sharp-darwin-${arch}`, ".node"],
|
||||||
|
[`sharp-libvips-darwin-${arch}`, ".dylib"],
|
||||||
|
]) {
|
||||||
|
const packagePath = path.join(modules, "@img", packageName);
|
||||||
|
if (
|
||||||
|
!fs.existsSync(packagePath) ||
|
||||||
|
!containsFile(packagePath, nativeSuffix)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`${path.basename(app)} is missing the native binary from @img/${packageName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runtimeCheck) return;
|
||||||
|
|
||||||
|
const executable = path.join(app, "Contents/MacOS/Termix");
|
||||||
|
const sharpPath = path.join(modules, "sharp");
|
||||||
|
const smoke = [
|
||||||
|
"const sharp = require(process.argv[1]);",
|
||||||
|
"sharp({create:{width:1,height:1,channels:4,background:'#000'}})",
|
||||||
|
".png().toBuffer().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });",
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
for (const arch of architectures[architecture]) {
|
||||||
|
const result = spawnSync(
|
||||||
|
"arch",
|
||||||
|
[
|
||||||
|
arch === "x64" ? "-x86_64" : "-arm64",
|
||||||
|
executable,
|
||||||
|
"-e",
|
||||||
|
smoke,
|
||||||
|
sharpPath,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`${path.basename(app)} failed the ${arch} sharp runtime smoke test:\n${result.stderr || result.stdout}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyArtifact(artifact) {
|
||||||
|
const architecture = expectedArchitecture(artifact);
|
||||||
|
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "termix-sharp-"));
|
||||||
|
let mountedAt;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (artifact.endsWith(".dmg")) {
|
||||||
|
mountedAt = path.join(temporaryRoot, "mounted");
|
||||||
|
fs.mkdirSync(mountedAt);
|
||||||
|
execFileSync("hdiutil", [
|
||||||
|
"attach",
|
||||||
|
artifact,
|
||||||
|
"-readonly",
|
||||||
|
"-nobrowse",
|
||||||
|
"-mountpoint",
|
||||||
|
mountedAt,
|
||||||
|
]);
|
||||||
|
verifyApp(findApp(mountedAt), architecture, true);
|
||||||
|
} else if (artifact.endsWith(".pkg")) {
|
||||||
|
const expanded = path.join(temporaryRoot, "expanded");
|
||||||
|
execFileSync("pkgutil", ["--expand-full", artifact, expanded]);
|
||||||
|
verifyApp(findApp(expanded), architecture, false);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported macOS artifact: ${artifact}`);
|
||||||
|
}
|
||||||
|
console.log(`Verified macOS sharp packaging: ${path.basename(artifact)}`);
|
||||||
|
} finally {
|
||||||
|
if (mountedAt) {
|
||||||
|
spawnSync("hdiutil", ["detach", mountedAt], { stdio: "ignore" });
|
||||||
|
}
|
||||||
|
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { expectedArchitecture, verifyApp };
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
throw new Error("macOS sharp artifact verification must run on macOS");
|
||||||
|
}
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
throw new Error(
|
||||||
|
"Usage: node scripts/verify-macos-sharp.cjs <artifact> [...]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const artifact of process.argv.slice(2))
|
||||||
|
verifyArtifact(path.resolve(artifact));
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const { expectedArchitecture, verifyApp } = require("./verify-macos-sharp.cjs");
|
||||||
|
|
||||||
|
const temporaryDirectories: string[] = [];
|
||||||
|
|
||||||
|
function createApp(architectures: string[]) {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-sharp-test-"));
|
||||||
|
temporaryDirectories.push(root);
|
||||||
|
const app = path.join(root, "Termix.app");
|
||||||
|
const modules = path.join(
|
||||||
|
app,
|
||||||
|
"Contents/Resources/app.asar.unpacked/node_modules/@img",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const architecture of architectures) {
|
||||||
|
const sharp = path.join(modules, `sharp-darwin-${architecture}/lib`);
|
||||||
|
const libvips = path.join(
|
||||||
|
modules,
|
||||||
|
`sharp-libvips-darwin-${architecture}/lib`,
|
||||||
|
);
|
||||||
|
fs.mkdirSync(sharp, { recursive: true });
|
||||||
|
fs.mkdirSync(libvips, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(sharp, `sharp-darwin-${architecture}.node`), "");
|
||||||
|
fs.writeFileSync(path.join(libvips, "libvips.dylib"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const directory of temporaryDirectories.splice(0)) {
|
||||||
|
fs.rmSync(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("macOS sharp artifact verification", () => {
|
||||||
|
it("derives the expected architecture from artifact names", () => {
|
||||||
|
expect(expectedArchitecture("termix_macos_x64_dmg.dmg")).toBe("x64");
|
||||||
|
expect(expectedArchitecture("termix_macos_arm64_dmg.dmg")).toBe("arm64");
|
||||||
|
expect(expectedArchitecture("termix_macos_universal_mas.pkg")).toBe(
|
||||||
|
"universal",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a universal app with both sharp architectures", () => {
|
||||||
|
expect(() =>
|
||||||
|
verifyApp(createApp(["x64", "arm64"]), "universal", false),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an x64 app containing only arm64 sharp binaries", () => {
|
||||||
|
expect(() => verifyApp(createApp(["arm64"]), "x64", false)).toThrow(
|
||||||
|
/sharp-darwin-x64/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user