Skip to content

Commit e4349d2

Browse files
authored
feat: filter xcodebuild errors for readability (#673)
* feat: filter xcodebuild errors for readability * adjust error handling and propagate runXcodebuild to other places * feat: break early when script for first destination fails * feat: output full log to tmp directory * changeset * test: strip ansi
1 parent ac41e4b commit e4349d2

13 files changed

Lines changed: 4273 additions & 195 deletions

File tree

.changeset/ten-bugs-warn.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@rock-js/platform-apple-helpers': patch
3+
'@rock-js/plugin-brownfield-ios': patch
4+
'@rock-js/platform-android': patch
5+
---
6+
7+
feat: filter xcodebuild errors for readability

.github/workflows/pr-labeler.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ jobs:
2222
configuration-path: .github/pr-labeler.yml
2323
enable-versioned-regex: 0
2424
include-title: 1
25-
sync-labels: 1
25+
sync-labels: 1

packages/platform-android/src/lib/commands/runAndroid/__tests__/tryLaunchAppOnDevice.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const args: Flags = {
4141
};
4242

4343
const androidProject: AndroidProjectConfig = {
44-
sourceDir: '/Users/thymikee/Developer/tmp/App73/android',
44+
sourceDir: '/Users/developer/tmp/App73/android',
4545
appName: 'app',
4646
packageName: 'com.myapp',
4747
applicationId: 'com.myapp.custom',
Lines changed: 10 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,12 @@
11
import path from 'node:path';
2-
import type { SubprocessError } from '@rock-js/tools';
3-
import { color, logger, RockError, spawn, spinner } from '@rock-js/tools';
2+
import { color, logger, RockError } from '@rock-js/tools';
43
import type { ApplePlatform, XcodeProjectInfo } from '../../types/index.js';
54
import { getBuildPaths } from '../../utils/getBuildPaths.js';
5+
import { runXcodebuild } from '../../utils/runXcodebuild.js';
66
import { supportedPlatforms } from '../../utils/supportedPlatforms.js';
77
import type { RunFlags } from '../run/runOptions.js';
88
import type { BuildFlags } from './buildOptions.js';
99

10-
let lastProgress = 0;
11-
/**
12-
* Creates an ASCII progress bar
13-
* @param percent - Percentage of completion (0-100)
14-
* @param length - Length of the progress bar in characters
15-
* @returns ASCII progress bar string
16-
*/
17-
function createProgressBar(percent: number, length = 20): string {
18-
const latestPercent = percent > lastProgress ? percent : lastProgress;
19-
lastProgress = latestPercent;
20-
const filledLength = Math.round(length * (latestPercent / 100));
21-
const emptyLength = length - filledLength;
22-
23-
const filled = '█'.repeat(filledLength);
24-
const empty = '░'.repeat(emptyLength);
25-
26-
return `[${filled}${empty}]`;
27-
}
28-
29-
function reportProgress(
30-
chunk: string,
31-
loader: ReturnType<typeof spinner>,
32-
message: string,
33-
) {
34-
if (chunk.includes('PhaseScriptExecution')) {
35-
if (chunk.includes('[CP-User]\\ [Hermes]\\ Replace\\ Hermes\\')) {
36-
const progressBar = createProgressBar(10);
37-
loader.message(`${message} ${progressBar}`);
38-
}
39-
if (
40-
chunk.includes('[CP-User]\\ [RN]Check\\ rncore') &&
41-
chunk.includes('React-Fabric')
42-
) {
43-
const progressBar = createProgressBar(35);
44-
loader.message(`${message} ${progressBar}`);
45-
}
46-
if (chunk.includes('[CP-User]\\ [RN]Check\\ FBReactNativeSpec')) {
47-
const progressBar = createProgressBar(53);
48-
loader.message(`${message} ${progressBar}`);
49-
}
50-
if (
51-
chunk.includes('[CP-User]\\ [RN]Check\\ rncore') &&
52-
chunk.includes('React-FabricComponents')
53-
) {
54-
const progressBar = createProgressBar(66);
55-
loader.message(`${message} ${progressBar}`);
56-
}
57-
if (chunk.includes('[CP]\\ Check\\ Pods\\ Manifest.lock')) {
58-
const progressBar = createProgressBar(90);
59-
loader.message(`${message} ${progressBar}`);
60-
}
61-
} else if (chunk.includes('BUILD SUCCEEDED')) {
62-
const progressBar = createProgressBar(100);
63-
loader.message(`${message} ${progressBar}`);
64-
}
65-
}
66-
6710
export const buildProject = async ({
6811
xcodeProject,
6912
sourceDir,
@@ -80,7 +23,7 @@ export const buildProject = async ({
8023
configuration: string;
8124
destinations: string[];
8225
args: RunFlags | BuildFlags;
83-
}) => {
26+
}): Promise<void> => {
8427
if (!supportedPlatforms[platformName]) {
8528
throw new RockError(
8629
`Unknown platform: ${platformName}. Please, use one of: ${Object.values(
@@ -122,45 +65,19 @@ export const buildProject = async ({
12265
Scheme ${color.bold(scheme)}
12366
Configuration ${color.bold(configuration)}`);
12467

125-
const loader = spinner({ indicator: 'timer' });
68+
const { errorSummary } = await runXcodebuild(xcodebuildArgs, {
69+
cwd: sourceDir,
70+
});
12671

127-
const message = `${args.archive ? 'Archiving' : 'Building'} the app`;
128-
129-
let commandOutput = '';
130-
131-
loader.start(message);
132-
try {
133-
const process = spawn('xcodebuild', xcodebuildArgs, {
134-
cwd: sourceDir,
135-
});
136-
137-
if (!logger.isVerbose()) {
138-
// Process the output from the AsyncIterable
139-
for await (const chunk of process) {
140-
commandOutput += chunk + '\n';
141-
reportProgress(chunk, loader, message);
142-
}
143-
}
144-
145-
await process;
146-
loader.stop(`${args.archive ? 'Archived' : 'Built'} the app.`);
147-
} catch (error) {
148-
loader.stop(`Failed: ${message}.`, 1);
72+
if (errorSummary) {
14973
if (!xcodeProject.isWorkspace) {
15074
logger.error(
15175
`If your project uses CocoaPods, make sure to install pods with "pod install" in ${sourceDir} directory.`,
15276
);
15377
}
154-
if (commandOutput) {
155-
// Use lightweight console.error instead of logger.error to avoid stack overflow issues when Xcode logs go crazy
156-
console.error(color.red(`xcodebuild output: ${commandOutput}`));
157-
throw new RockError(
158-
'Running xcodebuild failed. See error details above.',
159-
);
160-
}
161-
throw new RockError('Running xcodebuild failed', {
162-
cause:
163-
(error as SubprocessError).stderr || (error as SubprocessError).command,
78+
79+
throw new RockError('Failed to build the project', {
80+
cause: errorSummary,
16481
});
16582
}
16683
};

packages/platform-apple-helpers/src/lib/commands/build/createBuild.ts

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@ import {
99
logger,
1010
promptSelect,
1111
relativeToCwd,
12-
RockError,
1312
saveLocalBuildCache,
1413
} from '@rock-js/tools';
15-
import type {
16-
BuilderCommand,
17-
ProjectConfig,
18-
XcodeProjectInfo,
19-
} from '../../types/index.js';
14+
import type { BuilderCommand, ProjectConfig } from '../../types/index.js';
2015
import { buildApp } from '../../utils/buildApp.js';
2116
import { getBuildPaths } from '../../utils/getBuildPaths.js';
2217
import type { BuildFlags } from './buildOptions.js';
@@ -43,9 +38,6 @@ export const createBuild = async ({
4338
}) => {
4439
await validateArgs(args);
4540

46-
let xcodeProject: XcodeProjectInfo;
47-
let sourceDir: string;
48-
let scheme = args.scheme;
4941
const deviceOrSimulator = args.destination
5042
? // there can be multiple destinations, so we'll pick the first one
5143
args.destination[0].match(/simulator/i)
@@ -81,30 +73,21 @@ export const createBuild = async ({
8173
}
8274
}
8375

84-
return { scheme };
76+
return { scheme: args.scheme };
8577
}
8678

87-
try {
88-
const { appPath, ...buildAppResult } = await buildApp({
89-
projectRoot,
90-
projectConfig,
91-
platformName,
92-
args,
93-
reactNativePath,
94-
artifactName,
95-
deviceOrSimulator,
96-
fingerprintOptions,
97-
usePrebuiltRNCore,
98-
});
99-
logger.log(`Build available at: ${colorLink(relativeToCwd(appPath))}`);
100-
101-
xcodeProject = buildAppResult.xcodeProject;
102-
sourceDir = buildAppResult.sourceDir;
103-
scheme = buildAppResult.scheme;
104-
} catch (error) {
105-
const message = `Failed to create ${args.archive ? 'archive' : 'build'}`;
106-
throw new RockError(message, { cause: error });
107-
}
79+
const { appPath, xcodeProject, sourceDir, scheme } = await buildApp({
80+
projectRoot,
81+
projectConfig,
82+
platformName,
83+
args,
84+
reactNativePath,
85+
artifactName,
86+
deviceOrSimulator,
87+
fingerprintOptions,
88+
usePrebuiltRNCore,
89+
});
90+
logger.log(`Build available at: ${colorLink(relativeToCwd(appPath))}`);
10891

10992
if (args.archive) {
11093
const { archiveDir } = getBuildPaths(platformName);
Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import type { SubprocessError } from '@rock-js/tools';
2-
import {
3-
colorLink,
4-
relativeToCwd,
5-
RockError,
6-
spawn,
7-
spinner,
8-
} from '@rock-js/tools';
1+
import { colorLink, logger, relativeToCwd, RockError } from '@rock-js/tools';
92
import { existsSync, readdirSync } from 'fs';
103
import path from 'path';
114
import { getBuildPaths } from '../../utils/getBuildPaths.js';
5+
import { runXcodebuild } from '../../utils/runXcodebuild.js';
126

137
export const exportArchive = async ({
148
sourceDir,
@@ -23,16 +17,12 @@ export const exportArchive = async ({
2317
exportExtraParams: string[];
2418
exportOptionsPlist?: string;
2519
}): Promise<{ ipaPath: string }> => {
26-
const loader = spinner();
27-
28-
loader.start('Exporting the archive...');
2920
const exportOptionsPlistPath = path.join(
3021
sourceDir,
3122
exportOptionsPlist ?? 'ExportOptions.plist',
3223
);
3324

3425
if (!existsSync(exportOptionsPlistPath)) {
35-
loader.stop('Failed to export the archive.', 1);
3626
throw new RockError(
3727
`ExportOptions.plist not found, please create ${colorLink(
3828
relativeToCwd(exportOptionsPlistPath),
@@ -52,29 +42,28 @@ export const exportArchive = async ({
5242
...exportExtraParams,
5343
];
5444

45+
let ipaFiles: string[] = [];
46+
47+
const { errorSummary } = await runXcodebuild(xcodebuildArgs, {
48+
cwd: sourceDir,
49+
});
5550
try {
56-
let ipaFiles: string[] = [];
51+
ipaFiles = readdirSync(exportDir).filter((file) => file.endsWith('.ipa'));
52+
} catch {
53+
ipaFiles = [];
54+
}
5755

58-
await spawn('xcodebuild', xcodebuildArgs, {
59-
cwd: sourceDir,
60-
stdio: 'pipe',
56+
if (errorSummary) {
57+
throw new Error('Running xcodebuild failed', {
58+
cause: errorSummary,
6159
});
62-
try {
63-
ipaFiles = readdirSync(exportDir).filter((file) => file.endsWith('.ipa'));
64-
} catch {
65-
ipaFiles = [];
66-
}
67-
68-
loader.stop(
60+
} else {
61+
logger.success(
6962
`Archive available at: ${colorLink(
7063
path.join(exportDir, ipaFiles[0]) ?? exportDir,
7164
)}`,
7265
);
73-
return { ipaPath: path.join(exportDir, ipaFiles[0]) };
74-
} catch (error) {
75-
loader.stop('Running xcodebuild failed.', 1);
76-
throw new Error('Running xcodebuild failed', {
77-
cause: (error as SubprocessError).stderr,
78-
});
7966
}
67+
68+
return { ipaPath: path.join(exportDir, ipaFiles[0]) };
8069
};

packages/platform-apple-helpers/src/lib/commands/run/createRun.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,6 @@ ${devices
247247
await Promise.all([
248248
launchSimulator(bootedDevice),
249249
buildApp({
250-
brownfield: false,
251250
args,
252251
projectConfig,
253252
platformName,

0 commit comments

Comments
 (0)