also looking at this
fix(packaging): stop macOS packages relocating themselves elsewhere
#46
4 files
+164
-16
| @@ -844,6 +844,74 @@ async function createDMG(opts: { | ||
| 844 | 844 | * - a signed `productbuild` submission package for the Mac App Store, which is |
| 845 | 845 | * the only form App Store Connect accepts. |
| 846 | 846 | */ |
| 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 | */ | |
| 873 | export 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. */ | |
| 896 | export 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 | ||
| 847 | 915 | async function createPKG(opts: { |
| 848 | 916 | appBundlePath: string |
| 849 | 917 | outputPath: string |
| @@ -879,18 +947,29 @@ async function createPKG(opts: { | ||
| 879 | 947 | // path it should land in. |
| 880 | 948 | const tempDir = mkdtempSync(join(tmpdir(), 'craft-pkg-')) |
| 881 | 949 | 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 | })) | |
| 894 | 973 | return built.success |
| 895 | 974 | ? { success: true, outputPath: opts.outputPath } |
| 896 | 975 | : { success: false, error: built.error } |