Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,10 @@ jobs:
- name: Build unsigned macOS packages
run: pnpm package:mac:unsigned

- name: Inspect macOS zip
run: pnpm inspect:package -- release/*.zip
- name: Inspect macOS packages
run: |
pnpm inspect:package -- release/*.zip
pnpm inspect:dmg -- release/*.dmg

- name: Stage macOS artifacts
shell: bash
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"package:mac:unsigned": "node scripts/package-mac-unsigned.mjs",
"package:dir": "pnpm prepackage && node scripts/run-electron-builder.mjs --linux --x64 --dir",
"inspect:package": "node scripts/inspect-package.mjs",
"test:packaging": "node --test scripts/packaging.test.mjs",
"inspect:dmg": "node scripts/inspect-macos-dmg.mjs",
"test:packaging": "node --test scripts/packaging.test.mjs scripts/inspect-macos-dmg.test.mjs",
"test:tooling": "node --test scripts/check-structure.test.mjs scripts/check-provenance.test.mjs scripts/deploy-site.test.mjs scripts/tailnet-gateway.test.mjs scripts/tailnet-service.test.mjs",
"check:structure": "node scripts/check-structure.mjs",
"check:provenance": "node scripts/check-provenance.mjs",
Expand Down
143 changes: 143 additions & 0 deletions scripts/inspect-macos-dmg.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { mkdtempSync, readdirSync, rmdirSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { extname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";

import { inspectPackage, locateAppRoot } from "./inspect-package.mjs";

function asError(error) {
return error instanceof Error ? error : new Error(String(error));
}

function appendCleanupError(primary, cleanup, message) {
const cleanupError = asError(cleanup);
if (primary === null) return cleanupError;
return new AggregateError([primary, cleanupError], message);
}

function defaultRunCommand(command, args) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${command} exited with status ${result.status ?? "unknown"}`);
}
}

function defaultIsFile(path) {
try {
return statSync(path).isFile();
} catch {
return false;
}
}

function defaultReadMountedEntries(path) {
return readdirSync(path, { withFileTypes: true });
}

export function findMountedApp(mountPoint, readMountedEntries = defaultReadMountedEntries) {
const apps = readMountedEntries(mountPoint)
.filter((entry) => entry.isDirectory() && extname(entry.name).toLowerCase() === ".app")
.map((entry) => join(mountPoint, entry.name));
if (apps.length !== 1) {
throw new Error(`expected exactly one macOS app in mounted DMG; found ${apps.length}`);
}
return apps[0];
}

export function inspectMacosDmg(dmgPath, dependencies = {}) {
const platform = dependencies.platform ?? process.platform;
if (platform !== "darwin") {
throw new Error(`DMG inspection requires macOS (darwin); current platform is ${platform}`);
}

const absoluteDmg = resolve(dmgPath);
const isFile = dependencies.isFile ?? defaultIsFile;
if (extname(absoluteDmg).toLowerCase() !== ".dmg" || !isFile(absoluteDmg)) {
throw new Error(`DMG does not exist or is not a file: ${absoluteDmg}`);
}

const createMountPoint =
dependencies.createMountPoint ?? (() => mkdtempSync(join(tmpdir(), "t4-code-dmg-")));
const removeMountPoint = dependencies.removeMountPoint ?? rmdirSync;
const readMountedEntries = dependencies.readMountedEntries ?? defaultReadMountedEntries;
const inspectArtifact = dependencies.inspectArtifact ?? inspectPackage;
const resolveAppRoot = dependencies.resolveAppRoot ?? locateAppRoot;
const runCommand = dependencies.runCommand ?? defaultRunCommand;
const mountPoint = createMountPoint();

let failure = null;
let mounted = false;
let detached = false;
let inspection;
try {
runCommand("hdiutil", [
"attach",
"-readonly",
"-nobrowse",
"-noautoopen",
"-mountpoint",
mountPoint,
absoluteDmg,
]);
mounted = true;
const appPath = findMountedApp(mountPoint, readMountedEntries);
inspection = inspectArtifact(resolveAppRoot(appPath));
} catch (error) {
failure = asError(error);
} finally {
if (mounted) {
try {
runCommand("hdiutil", ["detach", mountPoint]);
detached = true;
} catch (normalDetachError) {
try {
runCommand("hdiutil", ["detach", "-force", mountPoint]);
detached = true;
} catch (forceDetachError) {
failure = appendCleanupError(
failure,
new AggregateError(
[asError(normalDetachError), asError(forceDetachError)],
`could not detach mounted DMG at ${mountPoint}`,
),
"DMG inspection failed and cleanup could not detach the image",
);
}
}
}

if (!mounted || detached) {
try {
removeMountPoint(mountPoint);
} catch (error) {
failure = appendCleanupError(
failure,
error,
"DMG inspection failed and cleanup could not remove its mount point",
);
}
}
}

if (failure !== null) throw failure;
return inspection;
}

const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
if (isMain) {
const paths = process.argv.slice(2).filter((argument) => argument !== "--");
if (paths.length !== 1) {
console.error("usage: pnpm inspect:dmg -- <artifact.dmg>");
process.exitCode = 1;
} else {
try {
const result = inspectMacosDmg(paths[0]);
console.log(`${paths[0]}: mounted read-only and inspected ${result.asarEntries} ASAR entries`);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
}
112 changes: 112 additions & 0 deletions scripts/inspect-macos-dmg.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { resolve } from "node:path";

import { findMountedApp, inspectMacosDmg } from "./inspect-macos-dmg.mjs";

const dmgPath = "/tmp/T4-Code-0.1.3-mac-arm64.dmg";
const mountPoint = "/tmp/t4-code-dmg-test";

function directory(name) {
return { name, isDirectory: () => true };
}

function fixture(overrides = {}) {
const calls = [];
const dependencies = {
platform: "darwin",
isFile: () => true,
createMountPoint: () => mountPoint,
readMountedEntries: () => [directory("T4 Code.app")],
resolveAppRoot: (path) => `${path}/Contents`,
inspectArtifact: (path) => {
calls.push(["inspect", path]);
return { asarEntries: 42 };
},
runCommand: (command, args) => calls.push([command, ...args]),
removeMountPoint: (path) => calls.push(["remove", path]),
...overrides,
};
return { calls, dependencies };
}

test("DMG inspection mounts read-only, inspects the app, detaches, then removes the mount point", () => {
const { calls, dependencies } = fixture();
assert.deepEqual(inspectMacosDmg(dmgPath, dependencies), { asarEntries: 42 });
assert.deepEqual(calls, [
[
"hdiutil",
"attach",
"-readonly",
"-nobrowse",
"-noautoopen",
"-mountpoint",
mountPoint,
dmgPath,
],
["inspect", `${mountPoint}/T4 Code.app/Contents`],
["hdiutil", "detach", mountPoint],
["remove", mountPoint],
]);
});

test("DMG inspection detaches even when package inspection fails", () => {
const { calls, dependencies } = fixture({
inspectArtifact: (path) => {
calls.push(["inspect", path]);
throw new Error("bad package");
},
});
assert.throws(() => inspectMacosDmg(dmgPath, dependencies), /bad package/);
assert.deepEqual(calls.slice(-2), [
["hdiutil", "detach", mountPoint],
["remove", mountPoint],
]);
});

test("DMG inspection force-detaches when the normal detach is busy", () => {
const { calls, dependencies } = fixture({
runCommand: (command, args) => {
calls.push([command, ...args]);
if (args[0] === "detach" && args[1] !== "-force") throw new Error("busy");
},
});
assert.deepEqual(inspectMacosDmg(dmgPath, dependencies), { asarEntries: 42 });
assert.deepEqual(calls.slice(-3), [
["hdiutil", "detach", mountPoint],
["hdiutil", "detach", "-force", mountPoint],
["remove", mountPoint],
]);
});

test("DMG inspection fails closed and preserves the mount point when detach cannot complete", () => {
const { calls, dependencies } = fixture({
runCommand: (command, args) => {
calls.push([command, ...args]);
if (args[0] === "detach") throw new Error("still busy");
},
});
assert.throws(() => inspectMacosDmg(dmgPath, dependencies), /could not detach mounted DMG/);
assert.equal(calls.some(([command]) => command === "remove"), false);
});

test("mounted DMGs must contain exactly one application bundle", () => {
assert.equal(
findMountedApp(mountPoint, () => [directory("T4 Code.app")]),
`${mountPoint}/T4 Code.app`,
);
assert.throws(() => findMountedApp(mountPoint, () => []), /found 0/);
assert.throws(
() => findMountedApp(mountPoint, () => [directory("One.app"), directory("Two.app")]),
/found 2/,
);
});

test("release workflow inspects the DMG before staging it for publication", () => {
const workflow = readFileSync(resolve(import.meta.dirname, "../.github/workflows/release.yml"), "utf8");
const inspect = workflow.indexOf("pnpm inspect:dmg -- release/*.dmg");
const stage = workflow.indexOf("cp release/T4-Code-*.dmg");
assert.ok(inspect >= 0, "release workflow must invoke the DMG inspector");
assert.ok(stage > inspect, "DMG inspection must run before staging the artifact");
});
Loading