forked from boos/BoosAPI
Merge pull request #170 from cita-777/codex/release-mac-arch-guard-main-20260319
[codex] harden mac release architecture verification
This commit is contained in:
@@ -234,33 +234,7 @@ jobs:
|
||||
env:
|
||||
expectedMacArch: ${{ matrix.expectedMacArch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t app_binaries < <(find release -type f -path '*/Metapi.app/Contents/MacOS/Metapi')
|
||||
if [ "${#app_binaries[@]}" -eq 0 ]; then
|
||||
echo "No packaged macOS app binary found under release/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for binary in "${app_binaries[@]}"; do
|
||||
archs="$(lipo -archs "$binary" 2>/dev/null | xargs)"
|
||||
echo "$binary => ${archs:-unknown}"
|
||||
if [ -z "${archs}" ]; then
|
||||
echo "Unable to detect architecture via lipo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${expectedMacArch}" = "x64" ]; then
|
||||
[ "${archs}" = "x86_64" ] || {
|
||||
echo "Expected x86_64-only binary but got: ${archs}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
[ "${archs}" = "arm64" ] || {
|
||||
echo "Expected arm64-only binary but got: ${archs}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
done
|
||||
node scripts/desktop/verifyMacArchitecture.mjs --release-dir release --expected-arch "$expectedMacArch"
|
||||
|
||||
- name: Upload desktop package
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -11,6 +11,6 @@ describe('release workflow', () => {
|
||||
expect(workflow).toContain('expectedMacArch: x64');
|
||||
expect(workflow).toContain('expectedMacArch: arm64');
|
||||
expect(workflow).toContain('Verify packaged macOS architecture');
|
||||
expect(workflow).toContain('lipo -archs');
|
||||
expect(workflow).toContain('node scripts/desktop/verifyMacArchitecture.mjs');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const MAC_BINARY_SEGMENTS = ['Metapi.app', 'Contents', 'MacOS', 'Metapi'];
|
||||
|
||||
function walkDirectories(rootDir) {
|
||||
const queue = [rootDir];
|
||||
const files = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentDir = queue.shift();
|
||||
if (!currentDir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
queue.push(entryPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function findPackagedMacBinaries(releaseDir) {
|
||||
const normalizedSuffix = MAC_BINARY_SEGMENTS.join('/');
|
||||
|
||||
return walkDirectories(releaseDir).filter((filePath) =>
|
||||
filePath.replaceAll('\\', '/').endsWith(normalizedSuffix),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeExpectedMacArch(expectedArch) {
|
||||
if (expectedArch === 'x64') {
|
||||
return 'x86_64';
|
||||
}
|
||||
|
||||
if (expectedArch === 'arm64') {
|
||||
return 'arm64';
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported expected mac architecture: ${expectedArch}`);
|
||||
}
|
||||
|
||||
export function inspectBinaryArchsWithLipo(binaryPath) {
|
||||
try {
|
||||
return execFileSync('lipo', ['-archs', binaryPath], { encoding: 'utf8' }).trim();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Unable to detect architecture via lipo for ${binaryPath}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyMacArchitecture({
|
||||
releaseDir,
|
||||
expectedArch,
|
||||
inspectBinaryArchs = inspectBinaryArchsWithLipo,
|
||||
}) {
|
||||
const binaries = findPackagedMacBinaries(releaseDir);
|
||||
if (binaries.length === 0) {
|
||||
throw new Error(`No packaged macOS app binary found under ${releaseDir}`);
|
||||
}
|
||||
|
||||
const expectedBinaryArch = normalizeExpectedMacArch(expectedArch);
|
||||
|
||||
return binaries.map((binaryPath) => {
|
||||
const archs = inspectBinaryArchs(binaryPath).trim().replace(/\s+/g, ' ');
|
||||
if (!archs) {
|
||||
throw new Error(`Unable to detect architecture via lipo for ${binaryPath}`);
|
||||
}
|
||||
|
||||
if (archs !== expectedBinaryArch) {
|
||||
throw new Error(`Expected ${expectedBinaryArch}-only binary but got: ${archs}`);
|
||||
}
|
||||
|
||||
return { binaryPath, archs };
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgValue(args, name) {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return args[index + 1];
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const releaseDirArg = parseArgValue(args, '--release-dir');
|
||||
const expectedArch = parseArgValue(args, '--expected-arch');
|
||||
|
||||
if (!releaseDirArg || !expectedArch) {
|
||||
throw new Error('Usage: node scripts/desktop/verifyMacArchitecture.mjs --release-dir <dir> --expected-arch <x64|arm64>');
|
||||
}
|
||||
|
||||
const releaseDir = resolve(releaseDirArg);
|
||||
const verifiedBinaries = verifyMacArchitecture({ releaseDir, expectedArch });
|
||||
|
||||
for (const { binaryPath, archs } of verifiedBinaries) {
|
||||
console.log(`${binaryPath} => ${archs}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
import { verifyMacArchitecture } from './verifyMacArchitecture.mjs';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function createReleaseBinary() {
|
||||
const releaseDir = mkdtempSync(join(tmpdir(), 'metapi-mac-arch-'));
|
||||
tempDirs.push(releaseDir);
|
||||
|
||||
const binaryPath = join(releaseDir, 'mac-x64', 'Metapi.app', 'Contents', 'MacOS', 'Metapi');
|
||||
mkdirSync(dirname(binaryPath), { recursive: true });
|
||||
writeFileSync(binaryPath, 'binary');
|
||||
|
||||
return { releaseDir, binaryPath };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (dir) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('verifyMacArchitecture', () => {
|
||||
it('accepts x64 binaries when lipo reports x86_64', () => {
|
||||
const { releaseDir, binaryPath } = createReleaseBinary();
|
||||
|
||||
expect(
|
||||
verifyMacArchitecture({
|
||||
releaseDir,
|
||||
expectedArch: 'x64',
|
||||
inspectBinaryArchs: () => 'x86_64',
|
||||
}),
|
||||
).toEqual([{ archs: 'x86_64', binaryPath }]);
|
||||
});
|
||||
|
||||
it('rejects x64 binaries when lipo reports arm64', () => {
|
||||
const { releaseDir } = createReleaseBinary();
|
||||
|
||||
expect(() =>
|
||||
verifyMacArchitecture({
|
||||
releaseDir,
|
||||
expectedArch: 'x64',
|
||||
inspectBinaryArchs: () => 'arm64',
|
||||
}),
|
||||
).toThrow(/Expected x86_64-only binary but got: arm64/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user