ReviewOS

also looking at this

craft-native/craft

fix(packaging): stop macOS packages relocating themselves elsewhere

#46
Merged glennmichael123 wants to merge fix/pkg-bundle-relocation into main
4 files +164 -16
packages/typescript/src/package.test.tsmodified+56-1
Changes to packages/typescript/src/package.test.ts
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'bun:test'
22import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
33import { tmpdir } from 'node:os'
44import { join } from 'node:path'
5import { candleArguments, codesignArguments, dmgCapacityMegabytes, dmgCreateArguments, formatPackagingCommandError, macOSInfoPlist, notarytoolArguments, packageApp, productbuildArguments, renderWixSource, shouldRetryHdiutil, windowsArchitecture, windowsExecutableName } from './package.js'
5import { candleArguments, codesignArguments, dmgCapacityMegabytes, dmgCreateArguments, formatPackagingCommandError, macOSInfoPlist, notarytoolArguments, packageApp, pkgbuildArguments, pkgbuildComponentPlist, productbuildArguments, renderWixSource, shouldRetryHdiutil, windowsArchitecture, windowsExecutableName } from './package.js'
66
77describe('Windows MSI packaging', () => {
88 it('renders a deterministic major-upgrade installer without shell interpolation', () => {
@@ -51,6 +51,61 @@ describe('Windows MSI packaging', () => {
5151 })
5252})
5353
54describe('macOS pkg relocation', () => {
55 it('pins the install path instead of letting the bundle be relocated', () => {
56 const plist = pkgbuildComponentPlist('Applications/Craft App.app')
57
58 // The one that matters. Left to pkgbuild, this is true, and the package
59 // then carries <relocate><bundle id="..."/></relocate> — which lets
60 // `installer` write the payload over any copy of that bundle the system
61 // has registered elsewhere and still exit 0. "The install was successful"
62 // with nothing at /Applications is that, and it is intermittent because it
63 // depends on what the system has indexed.
64 expect(plist).toContain('<key>BundleIsRelocatable</key>\n <false/>')
65
66 // Off so that installing an older version over a newer one is not skipped;
67 // with it on, a rollback silently becomes a no-op.
68 expect(plist).toContain('<key>BundleIsVersionChecked</key>\n <false/>')
69
70 expect(plist).toContain('<string>Applications/Craft App.app</string>')
71 expect(plist).toContain('<?xml version="1.0" encoding="UTF-8"?>')
72 })
73
74 it('escapes the bundle path rather than emitting broken XML', () => {
75 expect(pkgbuildComponentPlist('Applications/Ben & Co.app')).toContain('Applications/Ben &amp; Co.app')
76 })
77
78 it('always passes the component plist to pkgbuild', () => {
79 const args = pkgbuildArguments({
80 root: '/tmp/stage/root',
81 componentPlistPath: '/tmp/stage/component.plist',
82 identifier: 'dev.craft.lifecycle',
83 version: '1.0.0',
84 outputPath: '/tmp/out.pkg',
85 })
86 expect(args).toEqual([
87 '--root', '/tmp/stage/root',
88 '--component-plist', '/tmp/stage/component.plist',
89 '--identifier', 'dev.craft.lifecycle',
90 '--version', '1.0.0',
91 '--install-location', '/',
92 '/tmp/out.pkg',
93 ])
94
95 // Signing is additive and must not displace the component plist.
96 const signed = pkgbuildArguments({
97 root: '/tmp/stage/root',
98 componentPlistPath: '/tmp/stage/component.plist',
99 identifier: 'dev.craft.lifecycle',
100 version: '1.0.0',
101 outputPath: '/tmp/out.pkg',
102 installerIdentity: '3rd Party Mac Developer Installer: Example',
103 })
104 expect(signed).toContain('--component-plist')
105 expect(signed.slice(-3, -1)).toEqual(['--sign', '3rd Party Mac Developer Installer: Example'])
106 })
107})
108
54109describe('macOS packaging diagnostics', () => {
55110 it('retains hdiutil output in a failed package result', () => {
56111 expect(formatPackagingCommandError('hdiutil', 1, '', 'create failed - Resource temporarily unavailable\n'))