@hot-updater/apple-helper 1.2.0-rc.7 → 1.2.0-rc.8

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
@@ -783,11 +783,23 @@ const ASC_INTEGRATIONS_URL = "https://appstoreconnect.apple.com/access/integrati
783
783
  const issueAscApiKey = (request) => createAscKeyIssuer()(request);
784
784
  //#endregion
785
785
  //#region src/utils/cocoapods.ts
786
+ /**
787
+ * A failed command's error: the command, then the last lines of its
788
+ * output, without execa's full message.
789
+ */
790
+ const commandFailure = (command, error) => {
791
+ const { stderr, stdout } = error ?? {};
792
+ const detail = String(stderr || stdout || "").trimEnd().split("\n").slice(-12).join("\n") || (error instanceof Error ? error.message : String(error));
793
+ return new Error(`${command} failed.\n${detail}`, { cause: error });
794
+ };
786
795
  const installPodsIfNeeded = async (sourceDir) => {
787
796
  const podfilePath = path.default.join(sourceDir, "Podfile");
788
797
  if (!fs.default.existsSync(podfilePath)) {
789
798
  _hot_updater_cli_tools.p.log.info("No Podfile found, skipping CocoaPods installation");
790
- return;
799
+ return {
800
+ codegen: false,
801
+ pods: false
802
+ };
791
803
  }
792
804
  const gemfilePaths = [path.default.join(sourceDir, "Gemfile"), path.default.join((0, _hot_updater_cli_tools.getCwd)(), "Gemfile")];
793
805
  const shouldUseBundler = (await Promise.all(gemfilePaths.map(checkShouldUseBundler))).some(Boolean);
@@ -801,36 +813,39 @@ const installPodsIfNeeded = async (sourceDir) => {
801
813
  reactNativePath: reactNative.packagePath,
802
814
  sourceDir
803
815
  });
804
- try {
805
- if (shouldUseBundler) {
806
- _hot_updater_cli_tools.p.log.info("Using bundler for CocoaPods installation");
807
- const bundleSpinner = _hot_updater_cli_tools.p.spinner();
808
- bundleSpinner.start("Installing Ruby gems");
816
+ if (shouldUseBundler) {
817
+ const bundleSpinner = _hot_updater_cli_tools.p.spinner();
818
+ bundleSpinner.start("Installing Ruby gems");
819
+ try {
809
820
  await (0, execa.execa)("bundle", ["install"], { cwd: sourceDir });
810
- bundleSpinner.stop("Ruby gems installed");
811
- const podSpinner = _hot_updater_cli_tools.p.spinner();
812
- podSpinner.start("Installing CocoaPods dependencies");
813
- await (0, execa.execa)("bundle", [
814
- "exec",
815
- "pod",
816
- "install"
817
- ], {
818
- cwd: sourceDir,
819
- env: plan.env
820
- });
821
- podSpinner.stop("CocoaPods dependencies installed");
822
- } else {
823
- const spinner = _hot_updater_cli_tools.p.spinner();
824
- spinner.start("Installing CocoaPods dependencies");
825
- await (0, execa.execa)("pod", ["install"], {
826
- cwd: sourceDir,
827
- env: plan.env
828
- });
829
- spinner.stop("CocoaPods dependencies installed");
821
+ } catch (error) {
822
+ bundleSpinner.error("bundle install failed");
823
+ throw commandFailure("bundle install", error);
830
824
  }
825
+ bundleSpinner.stop("Ruby gems installed");
826
+ }
827
+ const podCommand = shouldUseBundler ? [
828
+ "bundle",
829
+ "exec",
830
+ "pod",
831
+ "install"
832
+ ] : ["pod", "install"];
833
+ const podSpinner = _hot_updater_cli_tools.p.spinner();
834
+ podSpinner.start("Installing CocoaPods dependencies");
835
+ try {
836
+ await (0, execa.execa)(podCommand[0], podCommand.slice(1), {
837
+ cwd: sourceDir,
838
+ env: plan.env
839
+ });
831
840
  } catch (error) {
832
- throw new Error(`pod install failed: ${error}`);
841
+ podSpinner.error(`${podCommand.join(" ")} failed`);
842
+ throw commandFailure(podCommand.join(" "), error);
833
843
  }
844
+ podSpinner.stop("CocoaPods dependencies installed");
845
+ return {
846
+ codegen: plan.runCodegen,
847
+ pods: true
848
+ };
834
849
  };
835
850
  const checkShouldUseBundler = async (gemfilePath) => {
836
851
  try {
@@ -863,8 +878,8 @@ const runReactNativeCodegen = async ({ projectRoot, reactNativePath, sourceDir }
863
878
  "ios"
864
879
  ], { cwd: projectRoot });
865
880
  } catch (error) {
866
- spinner.stop("React Native codegen failed");
867
- throw new Error(`React Native codegen failed: ${error}`);
881
+ spinner.error("React Native codegen failed");
882
+ throw commandFailure("React Native codegen", error);
868
883
  }
869
884
  spinner.stop("React Native codegen done");
870
885
  };
@@ -994,26 +1009,6 @@ const getDefaultDestination = ({ platform, useGeneric, deviceType }) => {
994
1009
  });
995
1010
  };
996
1011
  //#endregion
997
- //#region src/utils/formatCommand.ts
998
- /**
999
- * A command to print, broken at spaces with ` \` once a line would pass
1000
- * `width`, each continuation indented two spaces, so a terminal never wraps
1001
- * it mid-word and it still pastes as one command.
1002
- */
1003
- const formatCommand = (words, width = 74) => {
1004
- const lines = [];
1005
- let line = "";
1006
- for (const word of words) {
1007
- const next = line ? `${line} ${word}` : word;
1008
- if (line && next.length > width - 2) {
1009
- lines.push(`${line} \\`);
1010
- line = ` ${word}`;
1011
- } else line = next;
1012
- }
1013
- lines.push(line);
1014
- return lines.join("\n");
1015
- };
1016
- //#endregion
1017
1012
  //#region src/utils/parseXcodeProjectInfo.ts
1018
1013
  const parseXcodeProjectInfo = async (sourceDir) => {
1019
1014
  const files = await fs.default.promises.readdir(sourceDir);
@@ -1036,80 +1031,133 @@ const parseXcodeProjectInfo = async (sourceDir) => {
1036
1031
  const prettifyXcodebuildError = (error) => {
1037
1032
  if (error instanceof _hot_updater_cli_tools.BuildFailedError) {
1038
1033
  const errorLines = error.summary.join("\n");
1039
- const message = mentionsProgramLicenseAgreement(errorLines) ? PROGRAM_LICENSE_AGREEMENT_HINT : errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "No valid provisioning profile is set in the project.\nCheck the Signing & Capabilities tab in Xcode." : error.message;
1040
- return new _hot_updater_cli_tools.BuildFailedError(message, {
1034
+ const hint = mentionsProgramLicenseAgreement(errorLines) ? PROGRAM_LICENSE_AGREEMENT_HINT : errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "No valid provisioning profile is set in the project.\nCheck the Signing & Capabilities tab in Xcode." : null;
1035
+ return new _hot_updater_cli_tools.BuildFailedError(hint ?? error.message, {
1041
1036
  logPath: error.logPath,
1042
1037
  summary: error.summary,
1038
+ hint,
1043
1039
  cause: error.cause
1044
1040
  });
1045
1041
  }
1046
1042
  return /* @__PURE__ */ new Error(`Xcodebuild failed: ${error}`);
1047
1043
  };
1048
1044
  //#endregion
1045
+ //#region src/utils/xcodebuildPhase.ts
1046
+ /** The `(in target 'X' from project 'Y')` that ends an action's line. */
1047
+ const TARGET = /\(in target '([^']+)' from project '[^']+'\)\s*$/;
1048
+ /** A `PhaseScriptExecution` line's script phase, its spaces escaped. */
1049
+ const SCRIPT = /^PhaseScriptExecution ((?:\\.|[^\s\\])+)/;
1050
+ /** A batched `SwiftCompile`'s files: `Compiling\ A.swift,\ B.swift`. */
1051
+ const SWIFT_BATCH = /^SwiftCompile \S+ \S+ Compiling\\ ((?:\\.|[^\s\\])+)/;
1052
+ const unescape = (value) => value.replace(/\\(.)/g, "$1");
1053
+ /** Actions whose lines say what xcodebuild is doing, by their first word. */
1054
+ const ACTIONS = {
1055
+ CompileAssetCatalog: "Compiling assets",
1056
+ CompileAssetCatalogVariant: "Compiling assets",
1057
+ LinkAssetCatalog: "Compiling assets",
1058
+ CompileStoryboard: "Compiling storyboards",
1059
+ CompileXIB: "Compiling storyboards",
1060
+ LinkStoryboards: "Compiling storyboards",
1061
+ Ld: "Linking",
1062
+ Libtool: "Linking",
1063
+ CodeSign: "Signing",
1064
+ ProcessInfoPlistFile: "Processing",
1065
+ ProcessProductPackaging: "Processing",
1066
+ ProcessProductPackagingDER: "Processing",
1067
+ CopySwiftLibs: "Copying",
1068
+ CpResource: "Copying",
1069
+ GenerateDSYMFile: "Generating dSYM",
1070
+ Strip: "Stripping",
1071
+ Validate: "Validating"
1072
+ };
1073
+ const COMPILES = /* @__PURE__ */ new Set([
1074
+ "CompileC",
1075
+ "SwiftCompile",
1076
+ "CompileSwift"
1077
+ ]);
1078
+ const PRECOMPILES = /* @__PURE__ */ new Set([
1079
+ "PrecompileModule",
1080
+ "ScanDependencies",
1081
+ "SwiftExplicitDependencyGeneratePcm",
1082
+ "SwiftExplicitDependencyCompileModuleFromInterface"
1083
+ ]);
1084
+ const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
1085
+ /**
1086
+ * Reads the phase of an `xcodebuild` build, archive, or export from its raw
1087
+ * output: `Compiling React-Core · 412 files`, `Linking CodexRelay`,
1088
+ * `Signing CodexRelay`, `Bundling JavaScript`, with the target each
1089
+ * action's line names.
1090
+ */
1091
+ const createXcodebuildPhaseReader = () => {
1092
+ let compiled = 0;
1093
+ let precompiled = 0;
1094
+ return (line) => {
1095
+ const space = line.indexOf(" ");
1096
+ const action = space === -1 ? line : line.slice(0, space);
1097
+ const target = TARGET.exec(line)?.[1];
1098
+ const withTarget = (phase) => target ? `${phase} ${target}` : phase;
1099
+ if (COMPILES.has(action)) {
1100
+ const batch = SWIFT_BATCH.exec(line)?.[1];
1101
+ compiled += batch ? unescape(batch).split(",").length : 1;
1102
+ return `${withTarget("Compiling")} · ${plural(compiled, "file")}`;
1103
+ }
1104
+ if (PRECOMPILES.has(action)) {
1105
+ precompiled += 1;
1106
+ return `Precompiling modules · ${precompiled}`;
1107
+ }
1108
+ if (action === "PhaseScriptExecution") {
1109
+ const script = unescape(SCRIPT.exec(line)?.[1] ?? "");
1110
+ if (script === "Bundle React Native code and images") return "Bundling JavaScript";
1111
+ return script ? `Running ${script}` : withTarget("Running a script");
1112
+ }
1113
+ const phase = ACTIONS[action];
1114
+ if (phase) return withTarget(phase);
1115
+ if (line === "Prepare packages" || line.startsWith("Resolve Package")) return "Resolving packages";
1116
+ if (line === "ComputeTargetDependencyGraph") return "Planning the build";
1117
+ };
1118
+ };
1119
+ //#endregion
1049
1120
  //#region src/utils/createXcodebuildLogger.ts
1050
- const createXcodebuildLogger = ({ logPrefix }) => new _hot_updater_cli_tools.BuildLogger({
1121
+ const createXcodebuildLogger = ({ logPrefix, logFilePath }) => new _hot_updater_cli_tools.BuildLogger({
1051
1122
  logPrefix,
1052
- importantLogPatterns: [
1053
- "error:",
1054
- "** BUILD FAILED **",
1055
- "** ARCHIVE FAILED **",
1056
- "The following build commands failed:",
1057
- "note: Building targets in dependency order",
1058
- "note: Target dependency graph",
1059
- "Running script",
1060
- "[CP-User]",
1061
- "[CP] Check Pods Manifest.lock",
1062
- "Build Succeeded"
1063
- ],
1064
- progressStages: [
1065
- ["Building targets in dependency order", "Target dependency graph"],
1066
- ["Write Auxiliary File", "[CP] Check Pods Manifest.lock"],
1067
- [
1068
- "Running script",
1069
- "[CP-User] [RN]Check rncore",
1070
- "[CP-User] [Hermes] Replace Hermes",
1071
- "[CP-User] [RN]Check FBReactNativeSpec"
1072
- ],
1073
- [
1074
- "Processing",
1075
- "ProcessProductPackaging",
1076
- "ProcessProductPackagingDER"
1077
- ],
1078
- ["Compiling"],
1079
- ["RegisterExecutionPolicyException"],
1080
- ["Touching"],
1081
- ["Building library", "Create Universal Binary"],
1082
- ["Copying"],
1083
- [
1084
- "Build Succeeded",
1085
- "BUILD SUCCEEDED",
1086
- "ARCHIVE SUCCEEDED"
1087
- ]
1088
- ],
1123
+ logFilePath,
1124
+ createPhaseReader: createXcodebuildPhaseReader,
1089
1125
  failureSummary: {
1090
1126
  lines: [/(^|:\s)error: /, /^\*\* [A-Z ]+ FAILED \*\*$/],
1091
1127
  blocks: [{
1092
1128
  start: "The following build commands failed:",
1093
1129
  end: /^\(\d+ failures?\)$/,
1094
1130
  includeEnd: true
1131
+ }, {
1132
+ start: /^error: exportArchive /,
1133
+ end: /^$/,
1134
+ includeEnd: false
1095
1135
  }]
1096
1136
  }
1097
1137
  });
1098
1138
  //#endregion
1099
- //#region src/utils/runXcodebuildWithLogging.ts
1100
- let cachedXcbeautifyAvailability = null;
1101
- const TERMINATION_SIGNALS$1 = ["SIGINT", "SIGTERM"];
1102
- const isXcbeautifyAvailable = async () => {
1103
- if (cachedXcbeautifyAvailability !== null) return cachedXcbeautifyAvailability;
1104
- try {
1105
- await (0, execa.execa)("which", ["xcbeautify"]);
1106
- cachedXcbeautifyAvailability = true;
1107
- return true;
1108
- } catch {
1109
- cachedXcbeautifyAvailability = false;
1110
- return false;
1139
+ //#region src/utils/formatCommand.ts
1140
+ /**
1141
+ * A command to print, broken at spaces with ` \` once a line would pass
1142
+ * `width`, each continuation indented two spaces, so a terminal never wraps
1143
+ * it mid-word and it still pastes as one command.
1144
+ */
1145
+ const formatCommand = (words, width = 74) => {
1146
+ const lines = [];
1147
+ let line = "";
1148
+ for (const word of words) {
1149
+ const next = line ? `${line} ${word}` : word;
1150
+ if (line && next.length > width - 2) {
1151
+ lines.push(`${line} \\`);
1152
+ line = ` ${word}`;
1153
+ } else line = next;
1111
1154
  }
1155
+ lines.push(line);
1156
+ return lines.join("\n");
1112
1157
  };
1158
+ //#endregion
1159
+ //#region src/utils/runXcodebuildWithLogging.ts
1160
+ const TERMINATION_SIGNALS$1 = ["SIGINT", "SIGTERM"];
1113
1161
  const createRawXcodebuildProcess = ({ args, env, sourceDir }) => {
1114
1162
  return (0, execa.execa)("xcodebuild", args, {
1115
1163
  cwd: sourceDir,
@@ -1122,13 +1170,6 @@ const createRawXcodebuildProcess = ({ args, env, sourceDir }) => {
1122
1170
  }
1123
1171
  });
1124
1172
  };
1125
- const createXcbeautifyProcess = ({ cwd }) => {
1126
- return (0, execa.execa)("xcbeautify", [], {
1127
- cwd,
1128
- all: true,
1129
- buffer: false
1130
- });
1131
- };
1132
1173
  /** Why xcodebuild failed, without its command line. */
1133
1174
  const describeFailure = (error) => {
1134
1175
  if (error instanceof execa.ExecaError) {
@@ -1137,10 +1178,24 @@ const describeFailure = (error) => {
1137
1178
  }
1138
1179
  return error instanceof Error ? error.message : String(error);
1139
1180
  };
1140
- const runXcodebuildWithLogging = async ({ args, env, sourceDir, successMessage, failureMessage, logPrefix }) => {
1141
- const useXcbeautify = await isXcbeautifyAvailable();
1142
- const logger = createXcodebuildLogger({ logPrefix });
1143
- await logger.start();
1181
+ /**
1182
+ * Runs xcodebuild as a step titled `title`: its live line shows the build's
1183
+ * phase, the log file keeps the command and the whole output, and a failure
1184
+ * prints the output's errors and the log's path on stderr.
1185
+ */
1186
+ const runXcodebuildWithLogging = async ({ args, env, sourceDir, title, successMessage, failureMessage, logPrefix, logFilePath, header = [] }) => {
1187
+ const logger = createXcodebuildLogger({
1188
+ logPrefix,
1189
+ logFilePath
1190
+ });
1191
+ await logger.start({
1192
+ title,
1193
+ header: [
1194
+ ...header,
1195
+ `$ ${formatCommand(["xcodebuild", ...args])}`,
1196
+ ""
1197
+ ]
1198
+ });
1144
1199
  const activeProcesses = [];
1145
1200
  const terminateActiveProcesses = () => {
1146
1201
  for (const activeProcess of activeProcesses) activeProcess.kill();
@@ -1154,31 +1209,17 @@ const runXcodebuildWithLogging = async ({ args, env, sourceDir, successMessage,
1154
1209
  });
1155
1210
  activeProcesses.push(xcodebuildProcess);
1156
1211
  const xcodebuildOutput = xcodebuildProcess.all ?? xcodebuildProcess.stdout;
1157
- let xcbeautifyProcess = null;
1158
- if (useXcbeautify && xcodebuildOutput) {
1159
- xcbeautifyProcess = createXcbeautifyProcess({ cwd: sourceDir });
1160
- activeProcesses.push(xcbeautifyProcess);
1161
- }
1162
- const logging = xcodebuildOutput ? logger.processStream(xcodebuildOutput, { display: xcbeautifyProcess ? xcbeautifyProcess.all ?? xcbeautifyProcess.stdout ?? void 0 : void 0 }) : Promise.resolve();
1163
- const xcbeautifyInput = xcbeautifyProcess?.stdin;
1164
- if (xcodebuildOutput && xcbeautifyInput) {
1165
- xcbeautifyInput.on("error", () => {
1166
- xcodebuildOutput.unpipe(xcbeautifyInput);
1167
- });
1168
- xcodebuildOutput.pipe(xcbeautifyInput);
1169
- }
1170
- const [xcodebuildResult, loggingResult] = await Promise.allSettled([
1171
- xcodebuildProcess,
1172
- logging,
1173
- xcbeautifyProcess
1174
- ]);
1212
+ const logging = xcodebuildOutput ? logger.processStream(xcodebuildOutput) : Promise.resolve();
1213
+ const [xcodebuildResult, loggingResult] = await Promise.allSettled([xcodebuildProcess, logging]);
1175
1214
  if (xcodebuildResult.status === "rejected") throw xcodebuildResult.reason;
1176
1215
  if (loggingResult.status === "rejected") throw loggingResult.reason;
1177
1216
  logger.stop(successMessage);
1217
+ return { logFilePath: logger.logFilePath };
1178
1218
  } catch (error) {
1179
1219
  logger.writeError(error);
1180
- logger.stop(failureMessage, false);
1181
- throw new _hot_updater_cli_tools.BuildFailedError(`${failureMessage}: ${describeFailure(error)}`, {
1220
+ const reason = describeFailure(error);
1221
+ logger.stop(`${failureMessage} · ${reason.replace(/\.$/, "")}`, false);
1222
+ throw new _hot_updater_cli_tools.BuildFailedError(`${failureMessage}: ${reason}`, {
1182
1223
  logPath: logger.logPath,
1183
1224
  summary: logger.failureSummary,
1184
1225
  cause: error
@@ -1270,7 +1311,7 @@ const buildSettingsOverScheme = ({ minBundleId, authenticationArgs }) => ({
1270
1311
  * written to a temporary directory, removed whether the archive succeeds or
1271
1312
  * not.
1272
1313
  */
1273
- const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, minBundleId, authenticationArgs }) => {
1314
+ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, header = [], minBundleId, authenticationArgs }) => {
1274
1315
  if (installPods) await installPodsIfNeeded(sourceDir);
1275
1316
  const xcodeProject = await parseXcodeProjectInfo(sourceDir);
1276
1317
  const archiveName = `${xcodeProject.name.replace(".xcworkspace", "").replace(".xcodeproj", "")}.xcarchive`;
@@ -1300,24 +1341,26 @@ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, inst
1300
1341
  xcconfig: build.xcconfig,
1301
1342
  xcodeScheme
1302
1343
  });
1303
- _hot_updater_cli_tools.p.log.info([
1304
- "Xcode archive",
1305
- `Project ${xcodeProject.name}`,
1306
- `Scheme ${xcodeScheme}`,
1307
- `Platform ${platform}`,
1308
- "Command",
1309
- formatCommand(["xcodebuild", ...archiveArgs])
1310
- ].join("\n"));
1311
1344
  try {
1312
- await runXcodebuildWithLogging({
1345
+ const { logFilePath } = await runXcodebuildWithLogging({
1313
1346
  args: archiveArgs,
1314
1347
  env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
1348
+ title: `Archiving ${xcodeScheme}`,
1349
+ successMessage: `Archived ${xcodeScheme}`,
1315
1350
  failureMessage: "Archive failed",
1316
1351
  logPrefix,
1317
- sourceDir,
1318
- successMessage: "Archive completed successfully"
1352
+ header: [
1353
+ ...header,
1354
+ `Project ${xcodeProject.name}`,
1355
+ `Scheme ${xcodeScheme}`,
1356
+ `Platform ${platform}`
1357
+ ],
1358
+ sourceDir
1319
1359
  });
1320
- return { archivePath };
1360
+ return {
1361
+ archivePath,
1362
+ logFilePath
1363
+ };
1321
1364
  } catch (error) {
1322
1365
  throw prettifyXcodebuildError(error);
1323
1366
  }
@@ -1360,7 +1403,7 @@ const prepareArchiveArgs = ({ xcodeScheme, archivePath, platform, sourceDir, xco
1360
1403
  * `.app` to `outputDirectory`, and removes DerivedData, which takes 10 GB or
1361
1404
  * more, whether the build succeeds or not.
1362
1405
  */
1363
- const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs }) => {
1406
+ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs, header = [] }) => {
1364
1407
  if (installPods) await installPodsIfNeeded(sourceDir);
1365
1408
  const xcodeProject = await parseXcodeProjectInfo(sourceDir);
1366
1409
  const tmpDir = await createRandomTmpDir();
@@ -1398,26 +1441,26 @@ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeSc
1398
1441
  sourceDir,
1399
1442
  xcodeProject
1400
1443
  });
1401
- _hot_updater_cli_tools.p.log.info([
1402
- "Xcode build",
1403
- `Project ${xcodeProject.name}`,
1404
- `Scheme ${xcodeScheme}`,
1405
- `Configuration ${configuration}`,
1406
- `Platform ${platform}`,
1407
- `Device type ${deviceType}`,
1408
- "Command",
1409
- formatCommand(["xcodebuild", ...buildArgs])
1410
- ].join("\n"));
1411
1444
  let builtAppPath;
1445
+ let logFilePath;
1412
1446
  try {
1413
- await runXcodebuildWithLogging({
1447
+ ({logFilePath} = await runXcodebuildWithLogging({
1414
1448
  args: buildArgs,
1415
1449
  env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
1416
1450
  sourceDir,
1417
1451
  logPrefix,
1418
- successMessage: "Build completed successfully",
1452
+ header: [
1453
+ ...header,
1454
+ `Project ${xcodeProject.name}`,
1455
+ `Scheme ${xcodeScheme}`,
1456
+ `Configuration ${configuration}`,
1457
+ `Platform ${platform}`,
1458
+ `Device type ${deviceType}`
1459
+ ],
1460
+ title: `Building ${xcodeScheme}`,
1461
+ successMessage: `Built ${xcodeScheme}`,
1419
1462
  failureMessage: "Build failed"
1420
- });
1463
+ }));
1421
1464
  ({appPath: builtAppPath} = await getBuildSettings({
1422
1465
  configuration,
1423
1466
  derivedDataPath,
@@ -1431,7 +1474,10 @@ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeSc
1431
1474
  }
1432
1475
  const appPath = path.default.join(outputDirectory, path.default.basename(builtAppPath));
1433
1476
  await fs.default.promises.cp(builtAppPath, appPath, { recursive: true });
1434
- return { appPath };
1477
+ return {
1478
+ appPath,
1479
+ logFilePath
1480
+ };
1435
1481
  } finally {
1436
1482
  await removeTmpDir(tmpDir);
1437
1483
  }
@@ -1491,7 +1537,7 @@ const getBuildSettings = async ({ xcodeProject, sourceDir, xcodeScheme, configur
1491
1537
  //#endregion
1492
1538
  //#region src/builder/exportXcodeArchive.ts
1493
1539
  /** Exports the archive into `exportPath`. */
1494
- const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, authenticationArgs }) => {
1540
+ const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, logFilePath, title, authenticationArgs }) => {
1495
1541
  const exportArgs = prepareExportArgs({
1496
1542
  authenticationArgs,
1497
1543
  archivePath,
@@ -1503,9 +1549,11 @@ const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportEx
1503
1549
  await runXcodebuildWithLogging({
1504
1550
  args: exportArgs,
1505
1551
  sourceDir,
1506
- successMessage: "Archive exported successfully",
1552
+ title: `Exporting ${title}`,
1553
+ successMessage: `Exported ${title}`,
1507
1554
  failureMessage: "Export failed",
1508
- logPrefix
1555
+ logPrefix,
1556
+ logFilePath
1509
1557
  });
1510
1558
  return { exportPath };
1511
1559
  } catch (error) {
@@ -2587,7 +2635,7 @@ const readBuiltApp = async (appPath) => {
2587
2635
  * once the caller has copied it out. A failed build removes it, and every
2588
2636
  * other directory the build used, itself.
2589
2637
  */
2590
- const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools.createMinBundleId)(), credentials }) => {
2638
+ const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools.createMinBundleId)(), credentials, logHeader = [] }) => {
2591
2639
  await assertXcodebuildExist();
2592
2640
  if (schemeConfig.simulator && schemeConfig.destination.length) {
2593
2641
  _hot_updater_cli_tools.p.log.error("The scheme option 'simulator' is not compatible with the 'destination' option.");
@@ -2600,7 +2648,8 @@ const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools
2600
2648
  schemeConfig,
2601
2649
  minBundleId,
2602
2650
  credentials,
2603
- buildDirectory
2651
+ buildDirectory,
2652
+ logHeader
2604
2653
  }),
2605
2654
  dispose: () => removeTmpDir(buildDirectory)
2606
2655
  };
@@ -2609,15 +2658,16 @@ const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools
2609
2658
  throw error;
2610
2659
  }
2611
2660
  };
2612
- const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirectory }) => {
2661
+ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirectory, logHeader }) => {
2613
2662
  const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
2614
2663
  if (schemeConfig.simulator) {
2615
2664
  const appDir = path.default.join(buildDirectory, "app");
2616
2665
  await fs.default.promises.mkdir(appDir, { recursive: true });
2617
- const { appPath } = await buildXcodeProject({
2666
+ const { appPath, logFilePath } = await buildXcodeProject({
2618
2667
  sourceDir: iosProjectRoot,
2619
2668
  outputDirectory: appDir,
2620
2669
  logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
2670
+ header: logHeader,
2621
2671
  platform: schemeConfig.platform,
2622
2672
  xcodeScheme: schemeConfig.scheme,
2623
2673
  configuration: schemeConfig.configuration,
@@ -2630,6 +2680,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
2630
2680
  return {
2631
2681
  buildDirectory,
2632
2682
  buildArtifactPath: appPath,
2683
+ logFilePath,
2633
2684
  builtApp: await readBuiltApp(appPath)
2634
2685
  };
2635
2686
  } else {
@@ -2640,7 +2691,8 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
2640
2691
  if (!signing) return archiveAndExport({
2641
2692
  schemeConfig,
2642
2693
  minBundleId,
2643
- buildDirectory
2694
+ buildDirectory,
2695
+ logHeader
2644
2696
  });
2645
2697
  const appStoreConnect = await exportsForAppStoreConnect(schemeConfig);
2646
2698
  if ((0, _hot_updater_cli_tools.isCI)() && !appStoreConnect) _hot_updater_cli_tools.p.log.info("Signing in the runner's own keychains (not an App Store Connect export).");
@@ -2651,6 +2703,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
2651
2703
  schemeConfig,
2652
2704
  minBundleId,
2653
2705
  buildDirectory,
2706
+ logHeader,
2654
2707
  authenticationArgs: signing.authenticationArgs
2655
2708
  })
2656
2709
  });
@@ -2660,12 +2713,12 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
2660
2713
  * Archives a device build and exports its `.ipa`, or returns the
2661
2714
  * `.xcarchive` of a scheme it cannot export.
2662
2715
  */
2663
- const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, authenticationArgs }) => {
2716
+ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, logHeader, authenticationArgs }) => {
2664
2717
  const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
2665
2718
  const archiveDir = path.default.join(buildDirectory, "archive");
2666
2719
  const exportDir = path.default.join(buildDirectory, "export");
2667
2720
  await fs.default.promises.mkdir(archiveDir, { recursive: true });
2668
- const { archivePath } = await archiveXcodeProject({
2721
+ const { archivePath, logFilePath } = await archiveXcodeProject({
2669
2722
  archiveDirectory: archiveDir,
2670
2723
  authenticationArgs,
2671
2724
  minBundleId,
@@ -2677,7 +2730,8 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2677
2730
  extraParams: schemeConfig.extraParams,
2678
2731
  configuration: schemeConfig.configuration,
2679
2732
  installPods: schemeConfig.installPods,
2680
- logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build-archive`
2733
+ logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
2734
+ header: logHeader
2681
2735
  });
2682
2736
  const appFromArchive = extractAppFromXcarchive(archivePath);
2683
2737
  const archivedAppPath = appFromArchive ? path.default.join(archiveDir, path.default.basename(appFromArchive)) : void 0;
@@ -2685,6 +2739,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2685
2739
  if (!schemeConfig.exportOptionsPlist && !authenticationArgs) return {
2686
2740
  buildDirectory,
2687
2741
  buildArtifactPath: archivePath,
2742
+ logFilePath,
2688
2743
  builtApp: archivedAppPath ? await readBuiltApp(archivedAppPath) : void 0
2689
2744
  };
2690
2745
  await fs.default.promises.mkdir(exportDir, { recursive: true });
@@ -2695,7 +2750,9 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2695
2750
  exportExtraParams: schemeConfig.exportExtraParams,
2696
2751
  exportOptionsPlist,
2697
2752
  sourceDir: iosProjectRoot,
2698
- logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build-export`
2753
+ logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
2754
+ logFilePath: logFilePath ?? void 0,
2755
+ title: schemeConfig.scheme
2699
2756
  });
2700
2757
  if (schemeConfig.exportOptionsPlist) await exportArchive(schemeConfig.exportOptionsPlist);
2701
2758
  else {
@@ -2711,6 +2768,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
2711
2768
  return {
2712
2769
  buildDirectory,
2713
2770
  buildArtifactPath: path.default.join(exportDir, ipaFile),
2771
+ logFilePath,
2714
2772
  builtApp: archivedAppPath ? await readBuiltApp(archivedAppPath) : void 0
2715
2773
  };
2716
2774
  };
package/dist/index.d.cts CHANGED
@@ -81,15 +81,19 @@ declare const enrichNativeBuildIosScheme: ({ scheme, hotUpdaterSchemeName }: {
81
81
  * once the caller has copied it out. A failed build removes it, and every
82
82
  * other directory the build used, itself.
83
83
  */
84
- declare const buildIos: ({ schemeConfig, minBundleId, credentials }: {
84
+ declare const buildIos: ({ schemeConfig, minBundleId, credentials, logHeader }: {
85
85
  schemeConfig: EnrichedNativeBuildIosScheme;
86
86
  /** The MIN_BUNDLE_ID the build compiles in. */
87
87
  minBundleId?: string;
88
88
  /** Resolves the scheme's `signing` key, when a device build signs with one. */
89
89
  credentials?: Pick<CredentialResolver, "resolve">;
90
+ /** Lines the build log starts with, such as the native build key. */
91
+ logHeader?: string[];
90
92
  }) => Promise<{
91
93
  buildDirectory: string;
92
94
  buildArtifactPath: string;
95
+ /** The build log, which an archive's export appends to, or null. */
96
+ logFilePath: string | null;
93
97
  /** What the built app reports, from its Info.plist. */
94
98
  builtApp?: {
95
99
  appVersion: string | null;
@@ -191,7 +195,14 @@ declare const uploadToTestFlight: ({ ipaPath, apiKey, fetch, pollIntervalMs, pro
191
195
  type AppleDeviceType = "device" | "simulator";
192
196
  //#endregion
193
197
  //#region src/utils/cocoapods.d.ts
194
- declare const installPodsIfNeeded: (sourceDir: string) => Promise<void>;
198
+ /** What `installPodsIfNeeded` ran. */
199
+ interface PodInstallSteps {
200
+ /** React Native codegen, before `pod install`. */
201
+ readonly codegen: boolean;
202
+ /** `pod install`, which a project without a Podfile skips. */
203
+ readonly pods: boolean;
204
+ }
205
+ declare const installPodsIfNeeded: (sourceDir: string) => Promise<PodInstallSteps>;
195
206
  //#endregion
196
207
  //#region src/utils/extractBuiltInBundle.d.ts
197
208
  /**