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.tsmodified+91-12
Changes to packages/typescript/src/package.ts
@@ -844,6 +844,74 @@ async function createDMG(opts: {
844844 * - a signed `productbuild` submission package for the Mac App Store, which is
845845 * the only form App Store Connect accepts.
846846 */
847/**
848 * The component property list `pkgbuild` is given for the app bundle.
849 *
850 * Written out rather than left to `pkgbuild`'s inference, because its default
851 * for a bundle is `BundleIsRelocatable = true`, which puts this in the package:
852 *
853 * <relocate><bundle id="dev.example.app"/></relocate>
854 *
855 * That directive tells `installer` the payload path is only a suggestion: it
856 * looks the bundle identifier up on the target volume and, if the system has a
857 * copy of that bundle registered anywhere, writes the payload over *that* copy
858 * instead — and exits 0. The user sees "The install was successful" and finds
859 * nothing at /Applications.
860 *
861 * It is intermittent by nature, since it turns on whether the system has
862 * indexed some other copy yet, which is exactly how it behaved: the native
863 * lifecycle workflow's macOS legs failed only on slow runs, on both
864 * architectures, with `installer` reporting success and the app absent.
865 *
866 * `BundleIsVersionChecked` is off for a related reason — with it on, installing
867 * an *older* version over a newer one is skipped, which silently turns a
868 * rollback into a no-op.
869 *
870 * The `pkg-info` attribute `relocatable="false"` that appears either way is not
871 * this setting and does not govern it; only the `<relocate>` element does.
872 */
873export function pkgbuildComponentPlist(rootRelativeBundlePath: string): string {
874 return `<?xml version="1.0" encoding="UTF-8"?>
875<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
876<plist version="1.0">
877<array>
878 <dict>
879 <key>BundleHasStrictIdentifier</key>
880 <true/>
881 <key>BundleIsRelocatable</key>
882 <false/>
883 <key>BundleIsVersionChecked</key>
884 <false/>
885 <key>BundleOverwriteAction</key>
886 <string>upgrade</string>
887 <key>RootRelativeBundlePath</key>
888 <string>${xml(rootRelativeBundlePath)}</string>
889 </dict>
890</array>
891</plist>
892`
893}
894
895/** Build the `pkgbuild` argument list for a non-App-Store package. */
896export function pkgbuildArguments(opts: {
897 root: string
898 componentPlistPath: string
899 identifier: string
900 version: string
901 outputPath: string
902 installerIdentity?: string
903}): string[] {
904 return [
905 '--root', opts.root,
906 '--component-plist', opts.componentPlistPath,
907 '--identifier', opts.identifier,
908 '--version', opts.version,
909 '--install-location', '/',
910 ...(opts.installerIdentity ? ['--sign', opts.installerIdentity] : []),
911 opts.outputPath,
912 ]
913}
914
847915async function createPKG(opts: {
848916 appBundlePath: string
849917 outputPath: string
@@ -879,18 +947,29 @@ async function createPKG(opts: {
879947 // path it should land in.
880948 const tempDir = mkdtempSync(join(tmpdir(), 'craft-pkg-'))
881949 try {
882 const appsDir = join(tempDir, 'Applications')
883 mkdirSync(appsDir, { recursive: true })
884 cpSync(opts.appBundlePath, join(appsDir, basename(opts.appBundlePath)), { recursive: true })
885
886 const built = await runTool('pkgbuild', [
887 '--root', tempDir,
888 '--identifier', opts.identifier,
889 '--version', opts.version,
890 '--install-location', '/',
891 ...(opts.installerIdentity ? ['--sign', opts.installerIdentity] : []),
892 opts.outputPath,
893 ])
950 // A `root` child rather than tempDir itself, created 0755. mkdtemp makes
951 // its directory 0700, pkgbuild records the --root directory's own mode as
952 // the mode of `.`, and `.` under `--install-location /` is the target
953 // volume's root. Shipping a package that asks for `/` to be 0700 is not
954 // something to leave to the installer's discretion.
955 const root = join(tempDir, 'root')
956 const appsDir = join(root, 'Applications')
957 mkdirSync(appsDir, { recursive: true, mode: 0o755 })
958 chmodSync(root, 0o755)
959 const bundleName = basename(opts.appBundlePath)
960 cpSync(opts.appBundlePath, join(appsDir, bundleName), { recursive: true })
961
962 const componentPlistPath = join(tempDir, 'component.plist')
963 writeFileSync(componentPlistPath, pkgbuildComponentPlist(`Applications/${bundleName}`))
964
965 const built = await runTool('pkgbuild', pkgbuildArguments({
966 root,
967 componentPlistPath,
968 identifier: opts.identifier,
969 version: opts.version,
970 outputPath: opts.outputPath,
971 installerIdentity: opts.installerIdentity,
972 }))
894973 return built.success
895974 ? { success: true, outputPath: opts.outputPath }
896975 : { success: false, error: built.error }