Skip to content
Open
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
12 changes: 11 additions & 1 deletion frontend/scripts/build-server-desktop-macos-package.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { spawn } from 'node:child_process';
import { chmod, cp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
import { basename, join, relative, resolve } from 'node:path';
import { relativeToAppContents } from './macos-app-bundle-policy.mjs';
import {
nonRelocatableBundleComponentPlist,
relativeToAppContents,
} from './macos-app-bundle-policy.mjs';
import { verifyMachOPayload as verifyContainedMachOPayload } from './macos-macho.mjs';

const [target] = process.argv.slice(2);
Expand All @@ -24,12 +27,17 @@ const appSource = await findServerDesktopApp(targetRoot);
const appContract = await verifyAppBundle(appSource, target);
const packageName = `ACCORE.ERP.Server.Desktop_${version}_macos_${targetDefinition.architecture}.pkg`;
const packagePath = join(outputRoot, packageName);
const componentPlist = join(stageRoot, 'components.plist');

await rm(stageRoot, { recursive: true, force: true });
await rm(outputRoot, { recursive: true, force: true });
await mkdir(join(stageRoot, 'payload', 'Applications'), { recursive: true });
await mkdir(join(stageRoot, 'scripts'), { recursive: true });
await mkdir(outputRoot, { recursive: true });
await writeFile(
componentPlist,
nonRelocatableBundleComponentPlist(join('Applications', basename(appSource)))
);

const stagedApp = join(stageRoot, 'payload', 'Applications', basename(appSource));
await cp(appSource, stagedApp, { recursive: true, verbatimSymlinks: true });
Expand All @@ -38,6 +46,8 @@ await writeInstallerScripts(appContract);
await run('pkgbuild', [
'--root',
join(stageRoot, 'payload'),
'--component-plist',
componentPlist,
'--scripts',
join(stageRoot, 'scripts'),
'--ownership',
Expand Down
25 changes: 25 additions & 0 deletions frontend/scripts/macos-app-bundle-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,28 @@ export function relativeToAppContents(relativePath) {
}
return normalized.slice(prefix.length);
}

export function nonRelocatableBundleComponentPlist(rootRelativeBundlePath) {
const normalized = rootRelativeBundlePath.replaceAll('\\', '/');
if (
!normalized ||
normalized.startsWith('/') ||
normalized.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')
) {
throw new Error(`expected a safe destination-root bundle path: ${rootRelativeBundlePath}`);
}

return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>RootRelativeBundlePath</key>
<string>${normalized}</string>
<key>BundleIsRelocatable</key>
<false/>
</dict>
</array>
</plist>
`;
}
30 changes: 29 additions & 1 deletion frontend/scripts/macos-app-bundle-policy.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { relativeToAppContents } from './macos-app-bundle-policy.mjs';
import {
nonRelocatableBundleComponentPlist,
relativeToAppContents,
} from './macos-app-bundle-policy.mjs';

test('converts an app-root relative sidecar path to a Contents-relative path once', () => {
assert.equal(
Expand All @@ -16,3 +19,28 @@ test('converts an app-root relative sidecar path to a Contents-relative path onc
test('rejects a sidecar path outside the app Contents root', () => {
assert.throws(() => relativeToAppContents('MacOS/accore-server-agent'), /Contents/);
});

test('creates a component policy that installs the application at its declared destination', () => {
assert.equal(
nonRelocatableBundleComponentPlist('Applications/ACCORE ERP Server Desktop.app'),
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>RootRelativeBundlePath</key>
<string>Applications/ACCORE ERP Server Desktop.app</string>
<key>BundleIsRelocatable</key>
<false/>
</dict>
</array>
</plist>
`
);
});

test('rejects unsafe destination-root bundle paths', () => {
for (const path of ['', '/Applications/ACCORE ERP Server Desktop.app', '../Desktop.app']) {
assert.throws(() => nonRelocatableBundleComponentPlist(path), /safe destination-root/);
}
});
Loading