@hot-updater/apple-helper 1.2.0-rc.0 → 1.2.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -154,7 +154,7 @@ var AscCommandError = class extends Error {
154
154
  this.exitCode = exitCode;
155
155
  }
156
156
  };
157
- const TERMINATION_SIGNALS$2 = [
157
+ const TERMINATION_SIGNALS$3 = [
158
158
  "SIGHUP",
159
159
  "SIGINT",
160
160
  "SIGTERM"
@@ -178,10 +178,10 @@ const createWorkDirectory = async () => {
178
178
  };
179
179
  const stop = () => {
180
180
  process.off("exit", remove);
181
- for (const signal of TERMINATION_SIGNALS$2) process.off(signal, onSignal);
181
+ for (const signal of TERMINATION_SIGNALS$3) process.off(signal, onSignal);
182
182
  };
183
183
  process.on("exit", remove);
184
- for (const signal of TERMINATION_SIGNALS$2) process.on(signal, onSignal);
184
+ for (const signal of TERMINATION_SIGNALS$3) process.on(signal, onSignal);
185
185
  for (const directory of [
186
186
  "home",
187
187
  "session",
@@ -578,8 +578,55 @@ const resolvePodInstallPlan = ({ version, env }) => {
578
578
  };
579
579
  //#endregion
580
580
  //#region src/utils/createRandomTmpDir.ts
581
+ const TERMINATION_SIGNALS$2 = [
582
+ "SIGHUP",
583
+ "SIGINT",
584
+ "SIGTERM"
585
+ ];
586
+ const REMOVE_OPTIONS = {
587
+ force: true,
588
+ recursive: true,
589
+ maxRetries: 3
590
+ };
591
+ const liveDirectories = /* @__PURE__ */ new Set();
592
+ const onSignal = (signal) => {
593
+ removeLiveDirectories();
594
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
595
+ };
596
+ const removeLiveDirectories = () => {
597
+ for (const directory of liveDirectories) try {
598
+ (0, node_fs.rmSync)(directory, REMOVE_OPTIONS);
599
+ } catch {}
600
+ liveDirectories.clear();
601
+ stopListening();
602
+ };
603
+ const startListening = () => {
604
+ process.on("exit", removeLiveDirectories);
605
+ for (const signal of TERMINATION_SIGNALS$2) process.on(signal, onSignal);
606
+ };
607
+ const stopListening = () => {
608
+ process.off("exit", removeLiveDirectories);
609
+ for (const signal of TERMINATION_SIGNALS$2) process.off(signal, onSignal);
610
+ };
611
+ /**
612
+ * Creates a directory in the system's temporary directory, which
613
+ * `removeTmpDir` removes, and which is otherwise removed when the process
614
+ * exits or is stopped by a signal.
615
+ */
581
616
  const createRandomTmpDir = async (prefix = "apple-helper-") => {
582
- return await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), prefix));
617
+ const directory = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), prefix));
618
+ if (liveDirectories.size === 0) startListening();
619
+ liveDirectories.add(directory);
620
+ return directory;
621
+ };
622
+ /**
623
+ * Removes a directory `createRandomTmpDir` created. It stays tracked until
624
+ * it is gone, so a signal during a removal that takes seconds, such as
625
+ * DerivedData's, still finishes it.
626
+ */
627
+ const removeTmpDir = async (directory) => {
628
+ await (0, node_fs_promises.rm)(directory, REMOVE_OPTIONS);
629
+ if (liveDirectories.delete(directory) && liveDirectories.size === 0) stopListening();
583
630
  };
584
631
  //#endregion
585
632
  //#region src/utils/platform.ts
@@ -883,53 +930,62 @@ const buildSettingsOverScheme = ({ minBundleId, authenticationArgs }) => ({
883
930
  });
884
931
  //#endregion
885
932
  //#region src/builder/archiveXcodeProject.ts
886
- const archiveXcodeProject = async ({ sourceDir, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, minBundleId, authenticationArgs }) => {
933
+ /**
934
+ * Archives the scheme into `archiveDirectory`. The build's xcconfig is
935
+ * written to a temporary directory, removed whether the archive succeeds or
936
+ * not.
937
+ */
938
+ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, minBundleId, authenticationArgs }) => {
887
939
  if (installPods) await installPodsIfNeeded(sourceDir);
888
940
  const xcodeProject = await parseXcodeProjectInfo(sourceDir);
889
- const tmpDir = await createRandomTmpDir();
890
941
  const archiveName = `${xcodeProject.name.replace(".xcworkspace", "").replace(".xcodeproj", "")}.xcarchive`;
891
- const archivePath = path.default.join(tmpDir, archiveName);
892
- const build = await writeBuildXcconfig({
893
- directory: tmpDir,
894
- sourceDir,
895
- xcconfig,
896
- extraParams,
897
- settings: buildSettingsOverScheme({
942
+ const archivePath = path.default.join(archiveDirectory, archiveName);
943
+ const tmpDir = await createRandomTmpDir();
944
+ try {
945
+ const build = await writeBuildXcconfig({
946
+ directory: tmpDir,
947
+ sourceDir,
948
+ xcconfig,
949
+ extraParams,
950
+ settings: buildSettingsOverScheme({
951
+ minBundleId,
952
+ authenticationArgs
953
+ })
954
+ });
955
+ const archiveArgs = prepareArchiveArgs({
956
+ authenticationArgs,
898
957
  minBundleId,
899
- authenticationArgs
900
- })
901
- });
902
- const archiveArgs = prepareArchiveArgs({
903
- authenticationArgs,
904
- minBundleId,
905
- archivePath,
906
- platform,
907
- sourceDir,
908
- xcodeProject,
909
- configuration,
910
- extraParams: build.extraParams,
911
- destination,
912
- xcconfig: build.xcconfig,
913
- xcodeScheme
914
- });
915
- _hot_updater_cli_tools.p.log.info(`Xcode Archive Settings:
958
+ archivePath,
959
+ platform,
960
+ sourceDir,
961
+ xcodeProject,
962
+ configuration,
963
+ extraParams: build.extraParams,
964
+ destination,
965
+ xcconfig: build.xcconfig,
966
+ xcodeScheme
967
+ });
968
+ _hot_updater_cli_tools.p.log.info(`Xcode Archive Settings:
916
969
  Project ${xcodeProject.name}
917
970
  Scheme ${xcodeScheme}
918
971
  Platform ${platform}
919
972
  Command xcodebuild ${archiveArgs.join(" ")}
920
973
  `);
921
- try {
922
- await runXcodebuildWithLogging({
923
- args: archiveArgs,
924
- env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
925
- failureMessage: "Archive failed",
926
- logPrefix,
927
- sourceDir,
928
- successMessage: "Archive completed successfully"
929
- });
930
- return { archivePath };
931
- } catch (error) {
932
- throw prettifyXcodebuildError(error);
974
+ try {
975
+ await runXcodebuildWithLogging({
976
+ args: archiveArgs,
977
+ env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
978
+ failureMessage: "Archive failed",
979
+ logPrefix,
980
+ sourceDir,
981
+ successMessage: "Archive completed successfully"
982
+ });
983
+ return { archivePath };
984
+ } catch (error) {
985
+ throw prettifyXcodebuildError(error);
986
+ }
987
+ } finally {
988
+ await removeTmpDir(tmpDir);
933
989
  }
934
990
  };
935
991
  const prepareArchiveArgs = ({ xcodeScheme, archivePath, platform, sourceDir, xcodeProject, configuration = "Release", xcconfig, extraParams, destination = [], minBundleId, authenticationArgs }) => {
@@ -962,69 +1018,83 @@ const prepareArchiveArgs = ({ xcodeScheme, archivePath, platform, sourceDir, xco
962
1018
  };
963
1019
  //#endregion
964
1020
  //#region src/builder/buildXcodeProject.ts
965
- const buildXcodeProject = async ({ sourceDir, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs }) => {
1021
+ /**
1022
+ * Builds the scheme in a temporary DerivedData directory, copies the built
1023
+ * `.app` to `outputDirectory`, and removes DerivedData, which takes 10 GB or
1024
+ * more, whether the build succeeds or not.
1025
+ */
1026
+ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs }) => {
966
1027
  if (installPods) await installPodsIfNeeded(sourceDir);
967
1028
  const xcodeProject = await parseXcodeProjectInfo(sourceDir);
968
- const derivedDataPath = await createRandomTmpDir();
969
- const resolvedDestinations = resolveDestinations({
970
- destinations: destination,
971
- useGeneric: useGenericDestination
972
- });
973
- if (resolvedDestinations.length === 0) resolvedDestinations.push(getDefaultDestination({
974
- deviceType,
975
- platform,
976
- useGeneric: useGenericDestination
977
- }));
978
- const build = await writeBuildXcconfig({
979
- directory: await createRandomTmpDir(),
980
- sourceDir,
981
- extraParams,
982
- settings: buildSettingsOverScheme({
983
- minBundleId,
984
- authenticationArgs
985
- })
986
- });
987
- const buildArgs = prepareBuildArgs({
988
- authenticationArgs,
989
- xcconfig: build.xcconfig,
990
- minBundleId,
991
- configuration,
992
- derivedDataPath,
993
- deviceType,
994
- resolvedDestinations,
995
- extraParams: build.extraParams,
996
- platform,
997
- xcodeScheme,
998
- sourceDir,
999
- xcodeProject
1000
- });
1001
- _hot_updater_cli_tools.p.log.info(`Xcode Build Settings:
1002
- Project ${xcodeProject.name}
1003
- Scheme ${xcodeScheme}
1004
- Configuration ${configuration}
1005
- Platform ${platform}
1006
- Device Type ${deviceType}
1007
- Command xcodebuild ${buildArgs.join(" ")}
1008
- `);
1029
+ const tmpDir = await createRandomTmpDir();
1009
1030
  try {
1010
- await runXcodebuildWithLogging({
1011
- args: buildArgs,
1012
- env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
1031
+ const derivedDataPath = path.default.join(tmpDir, "DerivedData");
1032
+ const resolvedDestinations = resolveDestinations({
1033
+ destinations: destination,
1034
+ useGeneric: useGenericDestination
1035
+ });
1036
+ if (resolvedDestinations.length === 0) resolvedDestinations.push(getDefaultDestination({
1037
+ deviceType,
1038
+ platform,
1039
+ useGeneric: useGenericDestination
1040
+ }));
1041
+ const build = await writeBuildXcconfig({
1042
+ directory: tmpDir,
1013
1043
  sourceDir,
1014
- logPrefix,
1015
- successMessage: "Build completed successfully",
1016
- failureMessage: "Build failed"
1044
+ extraParams,
1045
+ settings: buildSettingsOverScheme({
1046
+ minBundleId,
1047
+ authenticationArgs
1048
+ })
1017
1049
  });
1018
- return await getBuildSettings({
1050
+ const buildArgs = prepareBuildArgs({
1051
+ authenticationArgs,
1052
+ xcconfig: build.xcconfig,
1053
+ minBundleId,
1019
1054
  configuration,
1020
1055
  derivedDataPath,
1056
+ deviceType,
1021
1057
  resolvedDestinations,
1058
+ extraParams: build.extraParams,
1059
+ platform,
1022
1060
  xcodeScheme,
1023
1061
  sourceDir,
1024
1062
  xcodeProject
1025
1063
  });
1026
- } catch (error) {
1027
- throw prettifyXcodebuildError(error);
1064
+ _hot_updater_cli_tools.p.log.info(`Xcode Build Settings:
1065
+ Project ${xcodeProject.name}
1066
+ Scheme ${xcodeScheme}
1067
+ Configuration ${configuration}
1068
+ Platform ${platform}
1069
+ Device Type ${deviceType}
1070
+ Command xcodebuild ${buildArgs.join(" ")}
1071
+ `);
1072
+ let builtAppPath;
1073
+ try {
1074
+ await runXcodebuildWithLogging({
1075
+ args: buildArgs,
1076
+ env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
1077
+ sourceDir,
1078
+ logPrefix,
1079
+ successMessage: "Build completed successfully",
1080
+ failureMessage: "Build failed"
1081
+ });
1082
+ ({appPath: builtAppPath} = await getBuildSettings({
1083
+ configuration,
1084
+ derivedDataPath,
1085
+ resolvedDestinations,
1086
+ xcodeScheme,
1087
+ sourceDir,
1088
+ xcodeProject
1089
+ }));
1090
+ } catch (error) {
1091
+ throw prettifyXcodebuildError(error);
1092
+ }
1093
+ const appPath = path.default.join(outputDirectory, path.default.basename(builtAppPath));
1094
+ await fs.default.promises.cp(builtAppPath, appPath, { recursive: true });
1095
+ return { appPath };
1096
+ } finally {
1097
+ await removeTmpDir(tmpDir);
1028
1098
  }
1029
1099
  };
1030
1100
  const prepareBuildArgs = ({ xcodeProject, sourceDir, platform, xcodeScheme, configuration, deviceType, resolvedDestinations, derivedDataPath, extraParams, minBundleId, authenticationArgs, xcconfig }) => {
@@ -1081,8 +1151,8 @@ const getBuildSettings = async ({ xcodeProject, sourceDir, xcodeScheme, configur
1081
1151
  };
1082
1152
  //#endregion
1083
1153
  //#region src/builder/exportXcodeArchive.ts
1084
- const exportXcodeArchive = async ({ archivePath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, authenticationArgs }) => {
1085
- const exportPath = await createRandomTmpDir();
1154
+ /** Exports the archive into `exportPath`. */
1155
+ const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, authenticationArgs }) => {
1086
1156
  const exportArgs = prepareExportArgs({
1087
1157
  authenticationArgs,
1088
1158
  archivePath,
@@ -2112,21 +2182,41 @@ const readBuiltApp = async (appPath) => {
2112
2182
  * Builds a simulator `.app` for a `simulator` scheme, and otherwise archives
2113
2183
  * and exports an `.ipa`, or returns the `.xcarchive` of a scheme it cannot
2114
2184
  * export: one with neither `exportOptionsPlist` nor `signing.ascApiKey`.
2185
+ *
2186
+ * The artifact is in a temporary `buildDirectory`, which `dispose` removes
2187
+ * once the caller has copied it out. A failed build removes it, and every
2188
+ * other directory the build used, itself.
2115
2189
  */
2116
2190
  const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools.createMinBundleId)(), credentials }) => {
2117
2191
  await assertXcodebuildExist();
2118
- const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
2192
+ if (schemeConfig.simulator && schemeConfig.destination.length) {
2193
+ _hot_updater_cli_tools.p.log.error("The scheme option 'simulator' is not compatible with the 'destination' option.");
2194
+ process.exit(1);
2195
+ }
2119
2196
  const buildDirectory = await createRandomTmpDir();
2120
- await fs.default.promises.mkdir(path.default.join(buildDirectory, "archive"), { recursive: true });
2197
+ try {
2198
+ return {
2199
+ ...await buildInto({
2200
+ schemeConfig,
2201
+ minBundleId,
2202
+ credentials,
2203
+ buildDirectory
2204
+ }),
2205
+ dispose: () => removeTmpDir(buildDirectory)
2206
+ };
2207
+ } catch (error) {
2208
+ await removeTmpDir(buildDirectory);
2209
+ throw error;
2210
+ }
2211
+ };
2212
+ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirectory }) => {
2213
+ const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
2121
2214
  if (schemeConfig.simulator) {
2122
- if (schemeConfig.destination.length) {
2123
- _hot_updater_cli_tools.p.log.error("The scheme option 'simulator' is not compatible with the 'destination' option.");
2124
- process.exit(1);
2125
- }
2126
2215
  const appDir = path.default.join(buildDirectory, "app");
2127
2216
  await fs.default.promises.mkdir(appDir, { recursive: true });
2128
2217
  const { appPath } = await buildXcodeProject({
2129
2218
  sourceDir: iosProjectRoot,
2219
+ outputDirectory: appDir,
2130
2220
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
2131
2221
  platform: schemeConfig.platform,
2132
2222
  xcodeScheme: schemeConfig.scheme,
@@ -2137,13 +2227,11 @@ const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools
2137
2227
  useGenericDestination: true,
2138
2228
  minBundleId
2139
2229
  });
2140
- const finalAppPath = path.default.join(appDir, path.default.basename(appPath));
2141
- await fs.default.promises.cp(appPath, finalAppPath, { recursive: true });
2142
2230
  _hot_updater_cli_tools.p.log.success(`.app file created: ${path.default.basename(appPath)}`);
2143
2231
  return {
2144
2232
  buildDirectory,
2145
- buildArtifactPath: finalAppPath,
2146
- builtApp: await readBuiltApp(finalAppPath)
2233
+ buildArtifactPath: appPath,
2234
+ builtApp: await readBuiltApp(appPath)
2147
2235
  };
2148
2236
  } else {
2149
2237
  const signing = await resolveXcodeSigning({
@@ -2177,7 +2265,9 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2177
2265
  const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
2178
2266
  const archiveDir = path.default.join(buildDirectory, "archive");
2179
2267
  const exportDir = path.default.join(buildDirectory, "export");
2268
+ await fs.default.promises.mkdir(archiveDir, { recursive: true });
2180
2269
  const { archivePath } = await archiveXcodeProject({
2270
+ archiveDirectory: archiveDir,
2181
2271
  authenticationArgs,
2182
2272
  minBundleId,
2183
2273
  platform: schemeConfig.platform,
@@ -2190,9 +2280,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2190
2280
  installPods: schemeConfig.installPods,
2191
2281
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build-archive`
2192
2282
  });
2193
- const finalArchivePath = path.default.join(archiveDir, path.default.basename(archivePath));
2194
- await fs.default.promises.cp(archivePath, finalArchivePath, { recursive: true });
2195
- const appFromArchive = extractAppFromXcarchive(finalArchivePath);
2283
+ const appFromArchive = extractAppFromXcarchive(archivePath);
2196
2284
  const archivedAppPath = appFromArchive ? path.default.join(archiveDir, path.default.basename(appFromArchive)) : void 0;
2197
2285
  if (appFromArchive && archivedAppPath) {
2198
2286
  await fs.default.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
@@ -2200,20 +2288,28 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2200
2288
  }
2201
2289
  if (!schemeConfig.exportOptionsPlist && !authenticationArgs) return {
2202
2290
  buildDirectory,
2203
- buildArtifactPath: finalArchivePath,
2291
+ buildArtifactPath: archivePath,
2204
2292
  builtApp: archivedAppPath ? await readBuiltApp(archivedAppPath) : void 0
2205
2293
  };
2206
2294
  await fs.default.promises.mkdir(exportDir, { recursive: true });
2207
- const exportOptionsPlist = schemeConfig.exportOptionsPlist ?? await writeAppStoreConnectExportOptions(await createRandomTmpDir());
2208
- const { exportPath } = await exportXcodeArchive({
2295
+ const exportArchive = (exportOptionsPlist) => exportXcodeArchive({
2209
2296
  archivePath,
2297
+ exportPath: exportDir,
2210
2298
  authenticationArgs,
2211
2299
  exportExtraParams: schemeConfig.exportExtraParams,
2212
2300
  exportOptionsPlist,
2213
2301
  sourceDir: iosProjectRoot,
2214
2302
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build-export`
2215
2303
  });
2216
- await fs.default.promises.cp(exportPath, exportDir, { recursive: true });
2304
+ if (schemeConfig.exportOptionsPlist) await exportArchive(schemeConfig.exportOptionsPlist);
2305
+ else {
2306
+ const exportOptionsDir = await createRandomTmpDir();
2307
+ try {
2308
+ await exportArchive(await writeAppStoreConnectExportOptions(exportOptionsDir));
2309
+ } finally {
2310
+ await removeTmpDir(exportOptionsDir);
2311
+ }
2312
+ }
2217
2313
  const ipaFile = fs.default.readdirSync(exportDir).find((file) => file.endsWith(".ipa"));
2218
2314
  if (!ipaFile) throw new Error("IPA file not found after export");
2219
2315
  return {
package/dist/index.d.cts CHANGED
@@ -75,6 +75,10 @@ declare const enrichNativeBuildIosScheme: ({ scheme, hotUpdaterSchemeName }: {
75
75
  * Builds a simulator `.app` for a `simulator` scheme, and otherwise archives
76
76
  * and exports an `.ipa`, or returns the `.xcarchive` of a scheme it cannot
77
77
  * export: one with neither `exportOptionsPlist` nor `signing.ascApiKey`.
78
+ *
79
+ * The artifact is in a temporary `buildDirectory`, which `dispose` removes
80
+ * once the caller has copied it out. A failed build removes it, and every
81
+ * other directory the build used, itself.
78
82
  */
79
83
  declare const buildIos: ({ schemeConfig, minBundleId, credentials }: {
80
84
  schemeConfig: EnrichedNativeBuildIosScheme;
@@ -92,6 +96,8 @@ declare const buildIos: ({ schemeConfig, minBundleId, credentials }: {
92
96
  /** The app's `CFBundleVersion`. */
93
97
  buildNumber: string | null;
94
98
  };
99
+ /** Removes `buildDirectory`, once the artifact is copied out of it. */
100
+ dispose: () => Promise<void>;
95
101
  }>;
96
102
  //#endregion
97
103
  //#region src/uploadToTestFlight.d.ts
package/dist/index.d.mts CHANGED
@@ -76,6 +76,10 @@ declare const enrichNativeBuildIosScheme: ({ scheme, hotUpdaterSchemeName }: {
76
76
  * Builds a simulator `.app` for a `simulator` scheme, and otherwise archives
77
77
  * and exports an `.ipa`, or returns the `.xcarchive` of a scheme it cannot
78
78
  * export: one with neither `exportOptionsPlist` nor `signing.ascApiKey`.
79
+ *
80
+ * The artifact is in a temporary `buildDirectory`, which `dispose` removes
81
+ * once the caller has copied it out. A failed build removes it, and every
82
+ * other directory the build used, itself.
79
83
  */
80
84
  declare const buildIos: ({ schemeConfig, minBundleId, credentials }: {
81
85
  schemeConfig: EnrichedNativeBuildIosScheme;
@@ -93,6 +97,8 @@ declare const buildIos: ({ schemeConfig, minBundleId, credentials }: {
93
97
  /** The app's `CFBundleVersion`. */
94
98
  buildNumber: string | null;
95
99
  };
100
+ /** Removes `buildDirectory`, once the artifact is copied out of it. */
101
+ dispose: () => Promise<void>;
96
102
  }>;
97
103
  //#endregion
98
104
  //#region src/uploadToTestFlight.d.ts
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { createReadStream, rmSync } from "node:fs";
3
- import fs, { mkdtemp } from "node:fs/promises";
3
+ import fs, { mkdtemp, rm } from "node:fs/promises";
4
4
  import net from "node:net";
5
5
  import os, { tmpdir } from "node:os";
6
6
  import path, { join } from "node:path";
@@ -122,7 +122,7 @@ var AscCommandError = class extends Error {
122
122
  this.exitCode = exitCode;
123
123
  }
124
124
  };
125
- const TERMINATION_SIGNALS$2 = [
125
+ const TERMINATION_SIGNALS$3 = [
126
126
  "SIGHUP",
127
127
  "SIGINT",
128
128
  "SIGTERM"
@@ -146,10 +146,10 @@ const createWorkDirectory = async () => {
146
146
  };
147
147
  const stop = () => {
148
148
  process.off("exit", remove);
149
- for (const signal of TERMINATION_SIGNALS$2) process.off(signal, onSignal);
149
+ for (const signal of TERMINATION_SIGNALS$3) process.off(signal, onSignal);
150
150
  };
151
151
  process.on("exit", remove);
152
- for (const signal of TERMINATION_SIGNALS$2) process.on(signal, onSignal);
152
+ for (const signal of TERMINATION_SIGNALS$3) process.on(signal, onSignal);
153
153
  for (const directory of [
154
154
  "home",
155
155
  "session",
@@ -546,8 +546,55 @@ const resolvePodInstallPlan = ({ version, env }) => {
546
546
  };
547
547
  //#endregion
548
548
  //#region src/utils/createRandomTmpDir.ts
549
+ const TERMINATION_SIGNALS$2 = [
550
+ "SIGHUP",
551
+ "SIGINT",
552
+ "SIGTERM"
553
+ ];
554
+ const REMOVE_OPTIONS = {
555
+ force: true,
556
+ recursive: true,
557
+ maxRetries: 3
558
+ };
559
+ const liveDirectories = /* @__PURE__ */ new Set();
560
+ const onSignal = (signal) => {
561
+ removeLiveDirectories();
562
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
563
+ };
564
+ const removeLiveDirectories = () => {
565
+ for (const directory of liveDirectories) try {
566
+ rmSync(directory, REMOVE_OPTIONS);
567
+ } catch {}
568
+ liveDirectories.clear();
569
+ stopListening();
570
+ };
571
+ const startListening = () => {
572
+ process.on("exit", removeLiveDirectories);
573
+ for (const signal of TERMINATION_SIGNALS$2) process.on(signal, onSignal);
574
+ };
575
+ const stopListening = () => {
576
+ process.off("exit", removeLiveDirectories);
577
+ for (const signal of TERMINATION_SIGNALS$2) process.off(signal, onSignal);
578
+ };
579
+ /**
580
+ * Creates a directory in the system's temporary directory, which
581
+ * `removeTmpDir` removes, and which is otherwise removed when the process
582
+ * exits or is stopped by a signal.
583
+ */
549
584
  const createRandomTmpDir = async (prefix = "apple-helper-") => {
550
- return await mkdtemp(join(tmpdir(), prefix));
585
+ const directory = await mkdtemp(join(tmpdir(), prefix));
586
+ if (liveDirectories.size === 0) startListening();
587
+ liveDirectories.add(directory);
588
+ return directory;
589
+ };
590
+ /**
591
+ * Removes a directory `createRandomTmpDir` created. It stays tracked until
592
+ * it is gone, so a signal during a removal that takes seconds, such as
593
+ * DerivedData's, still finishes it.
594
+ */
595
+ const removeTmpDir = async (directory) => {
596
+ await rm(directory, REMOVE_OPTIONS);
597
+ if (liveDirectories.delete(directory) && liveDirectories.size === 0) stopListening();
551
598
  };
552
599
  //#endregion
553
600
  //#region src/utils/platform.ts
@@ -851,53 +898,62 @@ const buildSettingsOverScheme = ({ minBundleId, authenticationArgs }) => ({
851
898
  });
852
899
  //#endregion
853
900
  //#region src/builder/archiveXcodeProject.ts
854
- const archiveXcodeProject = async ({ sourceDir, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, minBundleId, authenticationArgs }) => {
901
+ /**
902
+ * Archives the scheme into `archiveDirectory`. The build's xcconfig is
903
+ * written to a temporary directory, removed whether the archive succeeds or
904
+ * not.
905
+ */
906
+ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, minBundleId, authenticationArgs }) => {
855
907
  if (installPods) await installPodsIfNeeded(sourceDir);
856
908
  const xcodeProject = await parseXcodeProjectInfo(sourceDir);
857
- const tmpDir = await createRandomTmpDir();
858
909
  const archiveName = `${xcodeProject.name.replace(".xcworkspace", "").replace(".xcodeproj", "")}.xcarchive`;
859
- const archivePath = path$1.join(tmpDir, archiveName);
860
- const build = await writeBuildXcconfig({
861
- directory: tmpDir,
862
- sourceDir,
863
- xcconfig,
864
- extraParams,
865
- settings: buildSettingsOverScheme({
910
+ const archivePath = path$1.join(archiveDirectory, archiveName);
911
+ const tmpDir = await createRandomTmpDir();
912
+ try {
913
+ const build = await writeBuildXcconfig({
914
+ directory: tmpDir,
915
+ sourceDir,
916
+ xcconfig,
917
+ extraParams,
918
+ settings: buildSettingsOverScheme({
919
+ minBundleId,
920
+ authenticationArgs
921
+ })
922
+ });
923
+ const archiveArgs = prepareArchiveArgs({
924
+ authenticationArgs,
866
925
  minBundleId,
867
- authenticationArgs
868
- })
869
- });
870
- const archiveArgs = prepareArchiveArgs({
871
- authenticationArgs,
872
- minBundleId,
873
- archivePath,
874
- platform,
875
- sourceDir,
876
- xcodeProject,
877
- configuration,
878
- extraParams: build.extraParams,
879
- destination,
880
- xcconfig: build.xcconfig,
881
- xcodeScheme
882
- });
883
- p.log.info(`Xcode Archive Settings:
926
+ archivePath,
927
+ platform,
928
+ sourceDir,
929
+ xcodeProject,
930
+ configuration,
931
+ extraParams: build.extraParams,
932
+ destination,
933
+ xcconfig: build.xcconfig,
934
+ xcodeScheme
935
+ });
936
+ p.log.info(`Xcode Archive Settings:
884
937
  Project ${xcodeProject.name}
885
938
  Scheme ${xcodeScheme}
886
939
  Platform ${platform}
887
940
  Command xcodebuild ${archiveArgs.join(" ")}
888
941
  `);
889
- try {
890
- await runXcodebuildWithLogging({
891
- args: archiveArgs,
892
- env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
893
- failureMessage: "Archive failed",
894
- logPrefix,
895
- sourceDir,
896
- successMessage: "Archive completed successfully"
897
- });
898
- return { archivePath };
899
- } catch (error) {
900
- throw prettifyXcodebuildError(error);
942
+ try {
943
+ await runXcodebuildWithLogging({
944
+ args: archiveArgs,
945
+ env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
946
+ failureMessage: "Archive failed",
947
+ logPrefix,
948
+ sourceDir,
949
+ successMessage: "Archive completed successfully"
950
+ });
951
+ return { archivePath };
952
+ } catch (error) {
953
+ throw prettifyXcodebuildError(error);
954
+ }
955
+ } finally {
956
+ await removeTmpDir(tmpDir);
901
957
  }
902
958
  };
903
959
  const prepareArchiveArgs = ({ xcodeScheme, archivePath, platform, sourceDir, xcodeProject, configuration = "Release", xcconfig, extraParams, destination = [], minBundleId, authenticationArgs }) => {
@@ -930,69 +986,83 @@ const prepareArchiveArgs = ({ xcodeScheme, archivePath, platform, sourceDir, xco
930
986
  };
931
987
  //#endregion
932
988
  //#region src/builder/buildXcodeProject.ts
933
- const buildXcodeProject = async ({ sourceDir, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs }) => {
989
+ /**
990
+ * Builds the scheme in a temporary DerivedData directory, copies the built
991
+ * `.app` to `outputDirectory`, and removes DerivedData, which takes 10 GB or
992
+ * more, whether the build succeeds or not.
993
+ */
994
+ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs }) => {
934
995
  if (installPods) await installPodsIfNeeded(sourceDir);
935
996
  const xcodeProject = await parseXcodeProjectInfo(sourceDir);
936
- const derivedDataPath = await createRandomTmpDir();
937
- const resolvedDestinations = resolveDestinations({
938
- destinations: destination,
939
- useGeneric: useGenericDestination
940
- });
941
- if (resolvedDestinations.length === 0) resolvedDestinations.push(getDefaultDestination({
942
- deviceType,
943
- platform,
944
- useGeneric: useGenericDestination
945
- }));
946
- const build = await writeBuildXcconfig({
947
- directory: await createRandomTmpDir(),
948
- sourceDir,
949
- extraParams,
950
- settings: buildSettingsOverScheme({
951
- minBundleId,
952
- authenticationArgs
953
- })
954
- });
955
- const buildArgs = prepareBuildArgs({
956
- authenticationArgs,
957
- xcconfig: build.xcconfig,
958
- minBundleId,
959
- configuration,
960
- derivedDataPath,
961
- deviceType,
962
- resolvedDestinations,
963
- extraParams: build.extraParams,
964
- platform,
965
- xcodeScheme,
966
- sourceDir,
967
- xcodeProject
968
- });
969
- p.log.info(`Xcode Build Settings:
970
- Project ${xcodeProject.name}
971
- Scheme ${xcodeScheme}
972
- Configuration ${configuration}
973
- Platform ${platform}
974
- Device Type ${deviceType}
975
- Command xcodebuild ${buildArgs.join(" ")}
976
- `);
997
+ const tmpDir = await createRandomTmpDir();
977
998
  try {
978
- await runXcodebuildWithLogging({
979
- args: buildArgs,
980
- env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
999
+ const derivedDataPath = path$1.join(tmpDir, "DerivedData");
1000
+ const resolvedDestinations = resolveDestinations({
1001
+ destinations: destination,
1002
+ useGeneric: useGenericDestination
1003
+ });
1004
+ if (resolvedDestinations.length === 0) resolvedDestinations.push(getDefaultDestination({
1005
+ deviceType,
1006
+ platform,
1007
+ useGeneric: useGenericDestination
1008
+ }));
1009
+ const build = await writeBuildXcconfig({
1010
+ directory: tmpDir,
981
1011
  sourceDir,
982
- logPrefix,
983
- successMessage: "Build completed successfully",
984
- failureMessage: "Build failed"
1012
+ extraParams,
1013
+ settings: buildSettingsOverScheme({
1014
+ minBundleId,
1015
+ authenticationArgs
1016
+ })
985
1017
  });
986
- return await getBuildSettings({
1018
+ const buildArgs = prepareBuildArgs({
1019
+ authenticationArgs,
1020
+ xcconfig: build.xcconfig,
1021
+ minBundleId,
987
1022
  configuration,
988
1023
  derivedDataPath,
1024
+ deviceType,
989
1025
  resolvedDestinations,
1026
+ extraParams: build.extraParams,
1027
+ platform,
990
1028
  xcodeScheme,
991
1029
  sourceDir,
992
1030
  xcodeProject
993
1031
  });
994
- } catch (error) {
995
- throw prettifyXcodebuildError(error);
1032
+ p.log.info(`Xcode Build Settings:
1033
+ Project ${xcodeProject.name}
1034
+ Scheme ${xcodeScheme}
1035
+ Configuration ${configuration}
1036
+ Platform ${platform}
1037
+ Device Type ${deviceType}
1038
+ Command xcodebuild ${buildArgs.join(" ")}
1039
+ `);
1040
+ let builtAppPath;
1041
+ try {
1042
+ await runXcodebuildWithLogging({
1043
+ args: buildArgs,
1044
+ env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
1045
+ sourceDir,
1046
+ logPrefix,
1047
+ successMessage: "Build completed successfully",
1048
+ failureMessage: "Build failed"
1049
+ });
1050
+ ({appPath: builtAppPath} = await getBuildSettings({
1051
+ configuration,
1052
+ derivedDataPath,
1053
+ resolvedDestinations,
1054
+ xcodeScheme,
1055
+ sourceDir,
1056
+ xcodeProject
1057
+ }));
1058
+ } catch (error) {
1059
+ throw prettifyXcodebuildError(error);
1060
+ }
1061
+ const appPath = path$1.join(outputDirectory, path$1.basename(builtAppPath));
1062
+ await fs$1.promises.cp(builtAppPath, appPath, { recursive: true });
1063
+ return { appPath };
1064
+ } finally {
1065
+ await removeTmpDir(tmpDir);
996
1066
  }
997
1067
  };
998
1068
  const prepareBuildArgs = ({ xcodeProject, sourceDir, platform, xcodeScheme, configuration, deviceType, resolvedDestinations, derivedDataPath, extraParams, minBundleId, authenticationArgs, xcconfig }) => {
@@ -1049,8 +1119,8 @@ const getBuildSettings = async ({ xcodeProject, sourceDir, xcodeScheme, configur
1049
1119
  };
1050
1120
  //#endregion
1051
1121
  //#region src/builder/exportXcodeArchive.ts
1052
- const exportXcodeArchive = async ({ archivePath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, authenticationArgs }) => {
1053
- const exportPath = await createRandomTmpDir();
1122
+ /** Exports the archive into `exportPath`. */
1123
+ const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, authenticationArgs }) => {
1054
1124
  const exportArgs = prepareExportArgs({
1055
1125
  authenticationArgs,
1056
1126
  archivePath,
@@ -2080,21 +2150,41 @@ const readBuiltApp = async (appPath) => {
2080
2150
  * Builds a simulator `.app` for a `simulator` scheme, and otherwise archives
2081
2151
  * and exports an `.ipa`, or returns the `.xcarchive` of a scheme it cannot
2082
2152
  * export: one with neither `exportOptionsPlist` nor `signing.ascApiKey`.
2153
+ *
2154
+ * The artifact is in a temporary `buildDirectory`, which `dispose` removes
2155
+ * once the caller has copied it out. A failed build removes it, and every
2156
+ * other directory the build used, itself.
2083
2157
  */
2084
2158
  const buildIos = async ({ schemeConfig, minBundleId = createMinBundleId(), credentials }) => {
2085
2159
  await assertXcodebuildExist();
2086
- const iosProjectRoot = path$1.join(getCwd(), "ios");
2160
+ if (schemeConfig.simulator && schemeConfig.destination.length) {
2161
+ p.log.error("The scheme option 'simulator' is not compatible with the 'destination' option.");
2162
+ process.exit(1);
2163
+ }
2087
2164
  const buildDirectory = await createRandomTmpDir();
2088
- await fs$1.promises.mkdir(path$1.join(buildDirectory, "archive"), { recursive: true });
2165
+ try {
2166
+ return {
2167
+ ...await buildInto({
2168
+ schemeConfig,
2169
+ minBundleId,
2170
+ credentials,
2171
+ buildDirectory
2172
+ }),
2173
+ dispose: () => removeTmpDir(buildDirectory)
2174
+ };
2175
+ } catch (error) {
2176
+ await removeTmpDir(buildDirectory);
2177
+ throw error;
2178
+ }
2179
+ };
2180
+ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirectory }) => {
2181
+ const iosProjectRoot = path$1.join(getCwd(), "ios");
2089
2182
  if (schemeConfig.simulator) {
2090
- if (schemeConfig.destination.length) {
2091
- p.log.error("The scheme option 'simulator' is not compatible with the 'destination' option.");
2092
- process.exit(1);
2093
- }
2094
2183
  const appDir = path$1.join(buildDirectory, "app");
2095
2184
  await fs$1.promises.mkdir(appDir, { recursive: true });
2096
2185
  const { appPath } = await buildXcodeProject({
2097
2186
  sourceDir: iosProjectRoot,
2187
+ outputDirectory: appDir,
2098
2188
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
2099
2189
  platform: schemeConfig.platform,
2100
2190
  xcodeScheme: schemeConfig.scheme,
@@ -2105,13 +2195,11 @@ const buildIos = async ({ schemeConfig, minBundleId = createMinBundleId(), crede
2105
2195
  useGenericDestination: true,
2106
2196
  minBundleId
2107
2197
  });
2108
- const finalAppPath = path$1.join(appDir, path$1.basename(appPath));
2109
- await fs$1.promises.cp(appPath, finalAppPath, { recursive: true });
2110
2198
  p.log.success(`.app file created: ${path$1.basename(appPath)}`);
2111
2199
  return {
2112
2200
  buildDirectory,
2113
- buildArtifactPath: finalAppPath,
2114
- builtApp: await readBuiltApp(finalAppPath)
2201
+ buildArtifactPath: appPath,
2202
+ builtApp: await readBuiltApp(appPath)
2115
2203
  };
2116
2204
  } else {
2117
2205
  const signing = await resolveXcodeSigning({
@@ -2145,7 +2233,9 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2145
2233
  const iosProjectRoot = path$1.join(getCwd(), "ios");
2146
2234
  const archiveDir = path$1.join(buildDirectory, "archive");
2147
2235
  const exportDir = path$1.join(buildDirectory, "export");
2236
+ await fs$1.promises.mkdir(archiveDir, { recursive: true });
2148
2237
  const { archivePath } = await archiveXcodeProject({
2238
+ archiveDirectory: archiveDir,
2149
2239
  authenticationArgs,
2150
2240
  minBundleId,
2151
2241
  platform: schemeConfig.platform,
@@ -2158,9 +2248,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2158
2248
  installPods: schemeConfig.installPods,
2159
2249
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build-archive`
2160
2250
  });
2161
- const finalArchivePath = path$1.join(archiveDir, path$1.basename(archivePath));
2162
- await fs$1.promises.cp(archivePath, finalArchivePath, { recursive: true });
2163
- const appFromArchive = extractAppFromXcarchive(finalArchivePath);
2251
+ const appFromArchive = extractAppFromXcarchive(archivePath);
2164
2252
  const archivedAppPath = appFromArchive ? path$1.join(archiveDir, path$1.basename(appFromArchive)) : void 0;
2165
2253
  if (appFromArchive && archivedAppPath) {
2166
2254
  await fs$1.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
@@ -2168,20 +2256,28 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2168
2256
  }
2169
2257
  if (!schemeConfig.exportOptionsPlist && !authenticationArgs) return {
2170
2258
  buildDirectory,
2171
- buildArtifactPath: finalArchivePath,
2259
+ buildArtifactPath: archivePath,
2172
2260
  builtApp: archivedAppPath ? await readBuiltApp(archivedAppPath) : void 0
2173
2261
  };
2174
2262
  await fs$1.promises.mkdir(exportDir, { recursive: true });
2175
- const exportOptionsPlist = schemeConfig.exportOptionsPlist ?? await writeAppStoreConnectExportOptions(await createRandomTmpDir());
2176
- const { exportPath } = await exportXcodeArchive({
2263
+ const exportArchive = (exportOptionsPlist) => exportXcodeArchive({
2177
2264
  archivePath,
2265
+ exportPath: exportDir,
2178
2266
  authenticationArgs,
2179
2267
  exportExtraParams: schemeConfig.exportExtraParams,
2180
2268
  exportOptionsPlist,
2181
2269
  sourceDir: iosProjectRoot,
2182
2270
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build-export`
2183
2271
  });
2184
- await fs$1.promises.cp(exportPath, exportDir, { recursive: true });
2272
+ if (schemeConfig.exportOptionsPlist) await exportArchive(schemeConfig.exportOptionsPlist);
2273
+ else {
2274
+ const exportOptionsDir = await createRandomTmpDir();
2275
+ try {
2276
+ await exportArchive(await writeAppStoreConnectExportOptions(exportOptionsDir));
2277
+ } finally {
2278
+ await removeTmpDir(exportOptionsDir);
2279
+ }
2280
+ }
2185
2281
  const ipaFile = fs$1.readdirSync(exportDir).find((file) => file.endsWith(".ipa"));
2186
2282
  if (!ipaFile) throw new Error("IPA file not found after export");
2187
2283
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/apple-helper",
3
- "version": "1.2.0-rc.0",
3
+ "version": "1.2.0-rc.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -29,8 +29,8 @@
29
29
  "dependencies": {
30
30
  "execa": "9.5.2",
31
31
  "fast-xml-parser": "5.8.0",
32
- "@hot-updater/cli-tools": "1.2.0-rc.0",
33
- "@hot-updater/plugin-core": "1.2.0-rc.0"
32
+ "@hot-updater/cli-tools": "1.2.0-rc.2",
33
+ "@hot-updater/plugin-core": "1.2.0-rc.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20"