@invarn/cibuild 2.7.4 → 2.7.5
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/cli.cjs +10 -10
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +38 -0
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +1 -0
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +219 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +41 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +1 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +291 -0
- package/dist/src/commands/build.d.ts +124 -1
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +251 -33
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +26 -0
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +1 -0
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +196 -0
- package/dist/src/commands/file-secret-collector.d.ts +13 -0
- package/dist/src/commands/file-secret-collector.d.ts.map +1 -1
- package/dist/src/commands/file-secret-collector.js +3 -2
- package/dist/src/commands/local-properties-reaches-every-workflow.test.js +2 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +28 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +1 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +223 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +37 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +1 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +255 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +34 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +1 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +218 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +27 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +1 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +126 -0
- package/dist/src/yaml/converter.d.ts +48 -0
- package/dist/src/yaml/converter.d.ts.map +1 -1
- package/dist/src/yaml/converter.js +117 -0
- package/dist/src/yaml/env-resolver.d.ts +52 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -1
- package/dist/src/yaml/env-resolver.js +99 -1
- package/dist/src/yaml/secrets-manager.d.ts +13 -3
- package/dist/src/yaml/secrets-manager.d.ts.map +1 -1
- package/dist/src/yaml/secrets-manager.js +25 -14
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +17 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +132 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +9 -0
- package/dist/src/yaml/steps/ios-deps.d.ts +35 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
- package/dist/src/yaml/steps/ios-deps.js +18 -3
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +30 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.js +103 -0
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +73 -20
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cocoapods-install` can be asked to skip a project that has no Podfile.
|
|
3
|
+
*
|
|
4
|
+
* The default stays a hard failure, because a pipeline that names this step is
|
|
5
|
+
* asserting the project uses CocoaPods and a missing Podfile is a real error.
|
|
6
|
+
*
|
|
7
|
+
* The iOS template needs the other behaviour: it serves every iOS project and a
|
|
8
|
+
* large share of them resolve dependencies through Swift Package Manager, where
|
|
9
|
+
* "Podfile not found" would be a CocoaPods error in a project that does not use
|
|
10
|
+
* CocoaPods. Until this input existed the template could not use this step at
|
|
11
|
+
* all, so it ran `pod install` from an inline `script@1.0.0` — which put it out
|
|
12
|
+
* of reach of the refused-fetch retry this step carries. That is the gap this
|
|
13
|
+
* closes, and it is why the release order matters: this must ship before the
|
|
14
|
+
* template is pointed at the step, or every SPM-only iOS repo breaks.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=cocoapods-skip-if-absent.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cocoapods-skip-if-absent.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cocoapods-skip-if-absent.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cocoapods-install` can be asked to skip a project that has no Podfile.
|
|
3
|
+
*
|
|
4
|
+
* The default stays a hard failure, because a pipeline that names this step is
|
|
5
|
+
* asserting the project uses CocoaPods and a missing Podfile is a real error.
|
|
6
|
+
*
|
|
7
|
+
* The iOS template needs the other behaviour: it serves every iOS project and a
|
|
8
|
+
* large share of them resolve dependencies through Swift Package Manager, where
|
|
9
|
+
* "Podfile not found" would be a CocoaPods error in a project that does not use
|
|
10
|
+
* CocoaPods. Until this input existed the template could not use this step at
|
|
11
|
+
* all, so it ran `pod install` from an inline `script@1.0.0` — which put it out
|
|
12
|
+
* of reach of the refused-fetch retry this step carries. That is the gap this
|
|
13
|
+
* closes, and it is why the release order matters: this must ship before the
|
|
14
|
+
* template is pointed at the step, or every SPM-only iOS repo breaks.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, test, expect } from '@jest/globals';
|
|
17
|
+
import { CocoapodsInstallStepExecutor } from './ios-deps.js';
|
|
18
|
+
import { testConfig } from './test-config.js';
|
|
19
|
+
const build = async (inputs = {}) => {
|
|
20
|
+
const executor = new CocoapodsInstallStepExecutor();
|
|
21
|
+
const result = await executor.execute(inputs, {}, testConfig);
|
|
22
|
+
return result.script;
|
|
23
|
+
};
|
|
24
|
+
/** The body of the `if [ ! -f "Podfile" ]` branch, which is what changes. */
|
|
25
|
+
const absentBranch = (script) => {
|
|
26
|
+
const lines = script.split('\n');
|
|
27
|
+
const start = lines.findIndex((l) => l.includes('if [ ! -f "Podfile" ]'));
|
|
28
|
+
expect(start).toBeGreaterThanOrEqual(0);
|
|
29
|
+
const end = lines.findIndex((l, i) => i > start && l.trim() === 'fi');
|
|
30
|
+
return lines.slice(start, end + 1).join('\n');
|
|
31
|
+
};
|
|
32
|
+
describe('by default a missing Podfile is still an error', () => {
|
|
33
|
+
test('exits non-zero', async () => {
|
|
34
|
+
expect(absentBranch(await build())).toContain('exit 1');
|
|
35
|
+
});
|
|
36
|
+
test('says what was not found', async () => {
|
|
37
|
+
expect(absentBranch(await build())).toContain('Podfile not found');
|
|
38
|
+
});
|
|
39
|
+
test('an explicit false is the same as omitting it', async () => {
|
|
40
|
+
expect(absentBranch(await build({ skip_if_absent: false }))).toContain('exit 1');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('skip_if_absent turns it into a clean no-op', () => {
|
|
44
|
+
test('exits zero instead of one', async () => {
|
|
45
|
+
const branch = absentBranch(await build({ skip_if_absent: true }));
|
|
46
|
+
expect(branch).toContain('exit 0');
|
|
47
|
+
expect(branch).not.toContain('exit 1');
|
|
48
|
+
});
|
|
49
|
+
test('says it skipped rather than that something failed', async () => {
|
|
50
|
+
const branch = absentBranch(await build({ skip_if_absent: true }));
|
|
51
|
+
expect(branch).toContain('skipping CocoaPods install');
|
|
52
|
+
expect(branch).not.toContain('Error');
|
|
53
|
+
});
|
|
54
|
+
test('still runs pod install when a Podfile IS present', async () => {
|
|
55
|
+
// The skip is a branch, not a mode — the rest of the step is untouched.
|
|
56
|
+
const script = await build({ skip_if_absent: true });
|
|
57
|
+
expect(script).toContain('POD_INVOCATION');
|
|
58
|
+
expect(script).toContain('$POD_INVOCATION $POD_CMD');
|
|
59
|
+
});
|
|
60
|
+
test('still wraps the pod fetch in the refused-fetch retry', async () => {
|
|
61
|
+
// This is the entire point of letting the template use the step.
|
|
62
|
+
const script = await build({ skip_if_absent: true });
|
|
63
|
+
expect(script).toContain('cibuild_fetch_was_refused');
|
|
64
|
+
expect(script).toContain('CIBUILD_FETCH_MAX_ATTEMPTS');
|
|
65
|
+
});
|
|
66
|
+
test('still chooses between bundle exec and bare pod', async () => {
|
|
67
|
+
const script = await build({ skip_if_absent: true });
|
|
68
|
+
expect(script).toContain('USE_BUNDLE_EXEC');
|
|
69
|
+
expect(script).toContain('bundle exec pod');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* The inline script the template used did two things, not one: it skipped a
|
|
74
|
+
* project with no Podfile, AND it looked for that Podfile beside the Xcode
|
|
75
|
+
* project rather than at the repository root. Both have to exist as inputs or
|
|
76
|
+
* the template cannot be switched — about a quarter of iOS repositories keep
|
|
77
|
+
* the app one directory down, and template inputs interpolate variables, not
|
|
78
|
+
* `$(dirname …)`.
|
|
79
|
+
*/
|
|
80
|
+
describe('project_path finds the Podfile beside the project', () => {
|
|
81
|
+
test('changes into the project directory, not the repo root', async () => {
|
|
82
|
+
const script = await build({ project_path: 'ios/MyApp.xcworkspace' });
|
|
83
|
+
expect(script).toContain(`WORK_DIR="$(dirname 'ios/MyApp.xcworkspace')"`);
|
|
84
|
+
expect(script).toContain('cd "$WORK_DIR"');
|
|
85
|
+
});
|
|
86
|
+
test('does not fall back to CIBUILD_SOURCE_DIR when it is given', async () => {
|
|
87
|
+
const script = await build({ project_path: 'ios/MyApp.xcworkspace' });
|
|
88
|
+
expect(script).not.toContain('cd "$CIBUILD_SOURCE_DIR"');
|
|
89
|
+
});
|
|
90
|
+
test('still falls back to CIBUILD_SOURCE_DIR when nothing is given', async () => {
|
|
91
|
+
expect(await build()).toContain('cd "$CIBUILD_SOURCE_DIR"');
|
|
92
|
+
});
|
|
93
|
+
test('an explicit source_root_path still wins over project_path', async () => {
|
|
94
|
+
const script = await build({
|
|
95
|
+
project_path: 'ios/MyApp.xcworkspace',
|
|
96
|
+
source_root_path: 'somewhere/else',
|
|
97
|
+
});
|
|
98
|
+
expect(script).toContain(`cd 'somewhere/else'`);
|
|
99
|
+
expect(script).not.toContain('dirname');
|
|
100
|
+
});
|
|
101
|
+
test('composes with skip_if_absent, which is how the template uses it', async () => {
|
|
102
|
+
const script = await build({ project_path: 'ios/MyApp.xcworkspace', skip_if_absent: true });
|
|
103
|
+
expect(script).toContain(`WORK_DIR="$(dirname 'ios/MyApp.xcworkspace')"`);
|
|
104
|
+
expect(absentBranch(script)).toContain('exit 0');
|
|
105
|
+
expect(script).toContain('cibuild_fetch_was_refused');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* The exact input combination the iOS template sends. Pinned here because the
|
|
110
|
+
* template lives in another repository and deploys on a different cadence, so
|
|
111
|
+
* nothing else would notice this step drifting away from what it sends.
|
|
112
|
+
*/
|
|
113
|
+
describe('the iOS template combination', () => {
|
|
114
|
+
const templateInputs = {
|
|
115
|
+
project_path: 'MyApp.xcworkspace',
|
|
116
|
+
skip_if_absent: true,
|
|
117
|
+
command: 'install --repo-update',
|
|
118
|
+
};
|
|
119
|
+
test('produces pod install --repo-update, flags intact', async () => {
|
|
120
|
+
const script = await build(templateInputs);
|
|
121
|
+
expect(script).toContain('POD_CMD="install --repo-update"');
|
|
122
|
+
});
|
|
123
|
+
test('runs that under the refused-fetch retry, exactly once', async () => {
|
|
124
|
+
const script = await build(templateInputs);
|
|
125
|
+
expect(script).toContain('$POD_INVOCATION $POD_CMD');
|
|
126
|
+
expect(script.split('CIBUILD_FETCH_MAX_ATTEMPTS=').length - 1).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
test('skips instead of failing when the repo has no Podfile', async () => {
|
|
129
|
+
expect(absentBranch(await build(templateInputs))).toContain('exit 0');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=cocoapods-skip-if-absent.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAq8C7C"}
|
|
@@ -451,11 +451,20 @@ export function initializeStepRegistry() {
|
|
|
451
451
|
description: 'Explicit path to Podfile',
|
|
452
452
|
required: false,
|
|
453
453
|
},
|
|
454
|
+
project_path: {
|
|
455
|
+
description: 'A .xcodeproj/.xcworkspace whose directory holds the Podfile. For repositories that keep the app one directory down from the root.',
|
|
456
|
+
required: false,
|
|
457
|
+
},
|
|
454
458
|
verbose: {
|
|
455
459
|
description: 'Enable verbose CocoaPods logging',
|
|
456
460
|
required: false,
|
|
457
461
|
default: false,
|
|
458
462
|
},
|
|
463
|
+
skip_if_absent: {
|
|
464
|
+
description: 'Succeed and do nothing when there is no Podfile, instead of failing. For pipelines that serve every iOS project, where plenty use Swift Package Manager and have no Podfile.',
|
|
465
|
+
required: false,
|
|
466
|
+
default: false,
|
|
467
|
+
},
|
|
459
468
|
},
|
|
460
469
|
});
|
|
461
470
|
registerStep('carthage', new CarthageStepExecutor(), {
|
|
@@ -11,8 +11,43 @@ export interface CocoapodsInstallInputs {
|
|
|
11
11
|
source_root_path?: string;
|
|
12
12
|
/** Explicit path to Podfile (overrides source_root_path) */
|
|
13
13
|
podfile_path?: string;
|
|
14
|
+
/**
|
|
15
|
+
* A .xcodeproj/.xcworkspace whose DIRECTORY holds the Podfile.
|
|
16
|
+
*
|
|
17
|
+
* About a quarter of iOS repositories keep the app one directory down from
|
|
18
|
+
* the repository root, and the Podfile is down there with it — so "run pod
|
|
19
|
+
* install at the root" is wrong for them. A caller that already knows where
|
|
20
|
+
* the project is should not have to compute its directory separately, and a
|
|
21
|
+
* pipeline template cannot: template inputs interpolate variables, not
|
|
22
|
+
* `$(dirname …)`, so `source_root_path` alone could not express it and the
|
|
23
|
+
* iOS template had to run `pod install` from an inline script to get at
|
|
24
|
+
* `dirname`. That inline script is what put it out of reach of the
|
|
25
|
+
* refused-fetch retry.
|
|
26
|
+
*
|
|
27
|
+
* Only the directory is used; the file itself need not exist.
|
|
28
|
+
*
|
|
29
|
+
* Precedence, most specific first: podfile_path, source_root_path,
|
|
30
|
+
* project_path, then $CIBUILD_SOURCE_DIR.
|
|
31
|
+
*/
|
|
32
|
+
project_path?: string;
|
|
14
33
|
/** Enable verbose CocoaPods logging */
|
|
15
34
|
verbose?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Succeed and do nothing when there is no Podfile, instead of failing.
|
|
37
|
+
*
|
|
38
|
+
* Defaults to false, which keeps the contract every existing caller was
|
|
39
|
+
* written against: a pipeline that names this step is asserting the project
|
|
40
|
+
* uses CocoaPods, and a missing Podfile is a real error worth stopping for.
|
|
41
|
+
*
|
|
42
|
+
* The iOS template is the caller that needs the other behaviour. It serves
|
|
43
|
+
* every iOS project, and a large share of them resolve dependencies through
|
|
44
|
+
* Swift Package Manager and have no Podfile at all — for those, "Podfile not
|
|
45
|
+
* found" is a CocoaPods error in a project that does not use CocoaPods.
|
|
46
|
+
* Until this existed the template could not use this step, and so ran
|
|
47
|
+
* `pod install` from an inline script instead, out of reach of the
|
|
48
|
+
* refused-fetch retry that this step carries.
|
|
49
|
+
*/
|
|
50
|
+
skip_if_absent?: boolean;
|
|
16
51
|
}
|
|
17
52
|
/**
|
|
18
53
|
* Runs `pod install` or `pod update`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CA+FpB;AAMD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,gBAAgB;IACxD,yBAAyB,CACvB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAyDpB"}
|
|
@@ -19,7 +19,9 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
19
19
|
const command = this.getInput(inputs, 'command', 'install');
|
|
20
20
|
const sourceRootPath = this.getInput(inputs, 'source_root_path', '');
|
|
21
21
|
const podfilePath = this.getInput(inputs, 'podfile_path', '');
|
|
22
|
+
const projectPath = this.getInput(inputs, 'project_path', '');
|
|
22
23
|
const verbose = this.getInput(inputs, 'verbose', false);
|
|
24
|
+
const skipIfAbsent = this.getInput(inputs, 'skip_if_absent', false);
|
|
23
25
|
const commands = [];
|
|
24
26
|
commands.push('# cocoapods-install');
|
|
25
27
|
commands.push('echo "🔧 Running CocoaPods..."');
|
|
@@ -33,6 +35,11 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
33
35
|
else if (sourceRootPath) {
|
|
34
36
|
commands.push(`cd '${this.escapeBash(sourceRootPath)}'`);
|
|
35
37
|
}
|
|
38
|
+
else if (projectPath) {
|
|
39
|
+
commands.push('# Podfile sits beside the Xcode project, not at the repo root');
|
|
40
|
+
commands.push(`WORK_DIR="$(dirname '${this.escapeBash(projectPath)}')"`);
|
|
41
|
+
commands.push('cd "$WORK_DIR"');
|
|
42
|
+
}
|
|
36
43
|
else {
|
|
37
44
|
commands.push('# Use CIBUILD_SOURCE_DIR if set, otherwise current directory');
|
|
38
45
|
commands.push('if [ -n "$CIBUILD_SOURCE_DIR" ]; then');
|
|
@@ -40,10 +47,18 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
40
47
|
commands.push('fi');
|
|
41
48
|
}
|
|
42
49
|
commands.push('');
|
|
43
|
-
// Check for Podfile
|
|
50
|
+
// Check for Podfile. Absent is either a hard error or a clean no-op,
|
|
51
|
+
// depending on whether the caller is asserting "this project uses
|
|
52
|
+
// CocoaPods" or asking "use CocoaPods if this project does".
|
|
44
53
|
commands.push('if [ ! -f "Podfile" ]; then');
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
if (skipIfAbsent) {
|
|
55
|
+
commands.push(' echo "No Podfile in $(pwd) — skipping CocoaPods install."');
|
|
56
|
+
commands.push(' exit 0');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
commands.push(' echo "❌ Error: Podfile not found in $(pwd)"');
|
|
60
|
+
commands.push(' exit 1');
|
|
61
|
+
}
|
|
47
62
|
commands.push('fi');
|
|
48
63
|
commands.push('');
|
|
49
64
|
// Determine whether to use bundle exec
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products go under a derived data path, never into one flat directory.
|
|
3
|
+
*
|
|
4
|
+
* `CONFIGURATION_BUILD_DIR` overrides the per-configuration/per-platform
|
|
5
|
+
* product separation xcodebuild provides under
|
|
6
|
+
* `DerivedData/Build/Products/<config>-<platform>/`, flattening every target in
|
|
7
|
+
* the graph into a single directory. Two build tasks for the same target then
|
|
8
|
+
* declare the same absolute output path and xcodebuild refuses the entire build
|
|
9
|
+
* with "Multiple commands produce". The notes it prints name the SAME target
|
|
10
|
+
* twice, which is the tell: one target built twice into one directory, not two
|
|
11
|
+
* packages colliding on a name.
|
|
12
|
+
*
|
|
13
|
+
* Measured 2026-09-09 on the iOS corpus rows, all on the template's must-pass
|
|
14
|
+
* "Build App" step: 16 collisions on jellyfin/Swiftfin, 266 on
|
|
15
|
+
* Automattic/pocket-casts-ios, 425 on home-assistant/iOS.
|
|
16
|
+
*
|
|
17
|
+
* A fourth row failed differently for the same reason. CocoaPods derives
|
|
18
|
+
* `PODS_CONFIGURATION_BUILD_DIR` from `BUILD_DIR`, which `CONFIGURATION_BUILD_DIR`
|
|
19
|
+
* does NOT override — so pod products land in the flat directory while every
|
|
20
|
+
* generated copy-resources script and linker search path still points into
|
|
21
|
+
* DerivedData. shenruisi/Stay reported "Resource SDWebImage.bundle not found.
|
|
22
|
+
* Run pod install" for a bundle that had been built two directories away.
|
|
23
|
+
*
|
|
24
|
+
* Proved by control on a runner guest, same repo and image and flags with only
|
|
25
|
+
* this setting differing: `CONFIGURATION_BUILD_DIR` reproduced the collision and
|
|
26
|
+
* exit 65 with no .app, `-derivedDataPath` reached BUILD SUCCEEDED in 2m06s and
|
|
27
|
+
* left both `Debug` and `Debug-iphonesimulator` under Products.
|
|
28
|
+
*/
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=xcode-derived-data.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xcode-derived-data.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode-derived-data.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products go under a derived data path, never into one flat directory.
|
|
3
|
+
*
|
|
4
|
+
* `CONFIGURATION_BUILD_DIR` overrides the per-configuration/per-platform
|
|
5
|
+
* product separation xcodebuild provides under
|
|
6
|
+
* `DerivedData/Build/Products/<config>-<platform>/`, flattening every target in
|
|
7
|
+
* the graph into a single directory. Two build tasks for the same target then
|
|
8
|
+
* declare the same absolute output path and xcodebuild refuses the entire build
|
|
9
|
+
* with "Multiple commands produce". The notes it prints name the SAME target
|
|
10
|
+
* twice, which is the tell: one target built twice into one directory, not two
|
|
11
|
+
* packages colliding on a name.
|
|
12
|
+
*
|
|
13
|
+
* Measured 2026-09-09 on the iOS corpus rows, all on the template's must-pass
|
|
14
|
+
* "Build App" step: 16 collisions on jellyfin/Swiftfin, 266 on
|
|
15
|
+
* Automattic/pocket-casts-ios, 425 on home-assistant/iOS.
|
|
16
|
+
*
|
|
17
|
+
* A fourth row failed differently for the same reason. CocoaPods derives
|
|
18
|
+
* `PODS_CONFIGURATION_BUILD_DIR` from `BUILD_DIR`, which `CONFIGURATION_BUILD_DIR`
|
|
19
|
+
* does NOT override — so pod products land in the flat directory while every
|
|
20
|
+
* generated copy-resources script and linker search path still points into
|
|
21
|
+
* DerivedData. shenruisi/Stay reported "Resource SDWebImage.bundle not found.
|
|
22
|
+
* Run pod install" for a bundle that had been built two directories away.
|
|
23
|
+
*
|
|
24
|
+
* Proved by control on a runner guest, same repo and image and flags with only
|
|
25
|
+
* this setting differing: `CONFIGURATION_BUILD_DIR` reproduced the collision and
|
|
26
|
+
* exit 65 with no .app, `-derivedDataPath` reached BUILD SUCCEEDED in 2m06s and
|
|
27
|
+
* left both `Debug` and `Debug-iphonesimulator` under Products.
|
|
28
|
+
*/
|
|
29
|
+
import { describe, test, expect } from '@jest/globals';
|
|
30
|
+
import { XcodeBuildStepExecutor, XcodeBuildForTestStepExecutor, XcodeBuildForSimulatorStepExecutor, } from './xcode.js';
|
|
31
|
+
import { testConfig } from './test-config.js';
|
|
32
|
+
const DERIVED = '-derivedDataPath "$OUTPUT_DIR/DerivedData"';
|
|
33
|
+
const scriptFor = async (executor, inputs) => {
|
|
34
|
+
const result = await executor.execute({ project_path: 'MyApp.xcodeproj', scheme: 'MyApp', ...inputs }, {}, testConfig);
|
|
35
|
+
return result.script;
|
|
36
|
+
};
|
|
37
|
+
const buildingExecutors = () => [
|
|
38
|
+
['xcodebuild', new XcodeBuildStepExecutor()],
|
|
39
|
+
['xcode-build-for-test', new XcodeBuildForTestStepExecutor()],
|
|
40
|
+
['xcode-build-for-simulator', new XcodeBuildForSimulatorStepExecutor()],
|
|
41
|
+
];
|
|
42
|
+
describe('no xcode step flattens the product tree', () => {
|
|
43
|
+
test.each(buildingExecutors())('%s never passes CONFIGURATION_BUILD_DIR', async (_name, executor) => {
|
|
44
|
+
expect(await scriptFor(executor, {})).not.toContain('CONFIGURATION_BUILD_DIR');
|
|
45
|
+
});
|
|
46
|
+
test.each(buildingExecutors())('%s builds into a derived data path', async (_name, executor) => {
|
|
47
|
+
expect(await scriptFor(executor, {})).toContain(DERIVED);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('the resolve and the build read the same derived data', () => {
|
|
51
|
+
// If they diverge, the retried resolve populates one location and the build
|
|
52
|
+
// re-resolves into another — the retry is spent on work that is thrown away,
|
|
53
|
+
// and a refused fetch fails the expensive half anyway.
|
|
54
|
+
test.each(buildingExecutors())('%s', async (_name, executor) => {
|
|
55
|
+
const script = await scriptFor(executor, {});
|
|
56
|
+
const lines = script.split('\n');
|
|
57
|
+
const resolveLine = lines.find((l) => l.includes('-resolvePackageDependencies'));
|
|
58
|
+
const buildLine = lines.find((l) => /^xcodebuild (build|clean build|build-for-testing)/.test(l.trim()));
|
|
59
|
+
expect(resolveLine).toBeDefined();
|
|
60
|
+
expect(buildLine).toBeDefined();
|
|
61
|
+
expect(resolveLine).toContain(DERIVED);
|
|
62
|
+
expect(buildLine).toContain(DERIVED);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('xcode-build-for-simulator finds the .app where it now lands', () => {
|
|
66
|
+
// The guard is the step's strongest statement that the app compiled, so it
|
|
67
|
+
// has to follow the products. Left at `find "$OUTPUT_DIR" -maxdepth 1` it
|
|
68
|
+
// matches nothing and fails every build with ".app not found" while the app
|
|
69
|
+
// has in fact built perfectly.
|
|
70
|
+
const build = (inputs = {}) => scriptFor(new XcodeBuildForSimulatorStepExecutor(), inputs);
|
|
71
|
+
test('searches under the per-platform Products directory', async () => {
|
|
72
|
+
const script = await build();
|
|
73
|
+
expect(script).toContain('find "$OUTPUT_DIR/DerivedData/Build/Products"');
|
|
74
|
+
});
|
|
75
|
+
test('searches deep enough to reach <config>-<platform>/Foo.app', async () => {
|
|
76
|
+
// Products/Debug-iphonesimulator/MyApp.app is two levels down. maxdepth 1
|
|
77
|
+
// would find the configuration directory and no .app at all.
|
|
78
|
+
const findLine = (await build())
|
|
79
|
+
.split('\n')
|
|
80
|
+
.find((l) => l.includes('-name "*.app"'));
|
|
81
|
+
expect(findLine).toContain('-maxdepth 2');
|
|
82
|
+
});
|
|
83
|
+
test('still fails the step when nothing landed', async () => {
|
|
84
|
+
const script = await build();
|
|
85
|
+
expect(script).toContain('if [ -z "$APP_PATH" ]; then');
|
|
86
|
+
expect(script).toContain('exit 1');
|
|
87
|
+
});
|
|
88
|
+
test('still exports the app path for later steps', async () => {
|
|
89
|
+
expect(await build()).toContain('envman add --key CIBUILD_APP_DIR_PATH');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('the output_dir input still steers where everything goes', () => {
|
|
93
|
+
// The template gives "Build App" its own output_dir, so derived data has to
|
|
94
|
+
// follow that input rather than a hardcoded path.
|
|
95
|
+
test('a custom output_dir is what OUTPUT_DIR resolves to', async () => {
|
|
96
|
+
const script = await scriptFor(new XcodeBuildForSimulatorStepExecutor(), {
|
|
97
|
+
output_dir: 'build/app',
|
|
98
|
+
});
|
|
99
|
+
expect(script).toContain(`OUTPUT_DIR="$(cd 'build/app' && pwd)"`);
|
|
100
|
+
expect(script).toContain(DERIVED);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=xcode-derived-data.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEhF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,gBAAgB;IAC1D,yBAAyB,CACvB,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAmDpB,OAAO,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
|
|
@@ -52,10 +52,13 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
52
52
|
commands.push(`echo "Building project: ${this.escapeBash(projectPath)}"`);
|
|
53
53
|
commands.push(`echo "Scheme: ${this.escapeBash(scheme)}"`);
|
|
54
54
|
commands.push(`echo "Configuration: ${this.escapeBash(configuration)}"`);
|
|
55
|
-
// Create output directory and resolve to absolute path
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
55
|
+
// Create output directory and resolve to absolute path. The absolute path
|
|
56
|
+
// is the load-bearing part: a relative one let xcodebuild create a `build`
|
|
57
|
+
// directory inside each SPM package checkout in DerivedData, which fails
|
|
58
|
+
// with "File exists but is not a directory" on repeated runs. That was
|
|
59
|
+
// originally fixed by making CONFIGURATION_BUILD_DIR absolute; it is now
|
|
60
|
+
// -derivedDataPath that is absolute instead, which fixes the same thing
|
|
61
|
+
// without flattening the product tree.
|
|
59
62
|
commands.push('');
|
|
60
63
|
commands.push('# Create output directory and resolve to absolute path');
|
|
61
64
|
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
@@ -81,9 +84,10 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
81
84
|
commands.push('fi');
|
|
82
85
|
// Resolve the package graph before building, and retry that on a refused
|
|
83
86
|
// fetch. Resolution is seconds and the build is minutes, so the retry is
|
|
84
|
-
// spent on the cheap half.
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
+
// spent on the cheap half. The two -derivedDataPath arguments move
|
|
88
|
+
// together: the build below reads the same derived data and finds the
|
|
89
|
+
// graph already resolved. Split them and the retry is spent on work the
|
|
90
|
+
// build discards.
|
|
87
91
|
commands.push('');
|
|
88
92
|
commands.push('# Resolve package dependencies first — see git-fetch-retry');
|
|
89
93
|
commands.push(...refusedFetchHelpers());
|
|
@@ -91,7 +95,8 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
91
95
|
label: 'Package resolution',
|
|
92
96
|
command: 'xcodebuild -resolvePackageDependencies' +
|
|
93
97
|
` "$PROJECT_TYPE" '${escapedPath}'` +
|
|
94
|
-
` -scheme '${this.escapeBash(scheme)}'
|
|
98
|
+
` -scheme '${this.escapeBash(scheme)}'` +
|
|
99
|
+
' -derivedDataPath "$OUTPUT_DIR/DerivedData"',
|
|
95
100
|
}));
|
|
96
101
|
// Build xcodebuild command
|
|
97
102
|
commands.push('');
|
|
@@ -106,8 +111,14 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
106
111
|
xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
|
|
107
112
|
xcodebuildCmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
108
113
|
xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
109
|
-
//
|
|
110
|
-
|
|
114
|
+
// Derived data under the output dir, rather than CONFIGURATION_BUILD_DIR.
|
|
115
|
+
// Same reasoning as XcodeBuildForSimulatorStepExecutor below, where the
|
|
116
|
+
// full mechanism and the control are written up: the flat directory
|
|
117
|
+
// collapses xcodebuild's per-configuration/per-platform product separation
|
|
118
|
+
// and any graph with two tasks producing the same output name is refused
|
|
119
|
+
// with "Multiple commands produce". It also desynchronises CocoaPods, whose
|
|
120
|
+
// PODS_CONFIGURATION_BUILD_DIR follows BUILD_DIR and not this setting.
|
|
121
|
+
xcodebuildCmd += ` -derivedDataPath "$OUTPUT_DIR/DerivedData"`;
|
|
111
122
|
// Add xcconfig if provided
|
|
112
123
|
if (xcconfigContent && xcconfigContent.trim() !== '') {
|
|
113
124
|
xcodebuildCmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
@@ -117,8 +128,11 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
117
128
|
commands.push('');
|
|
118
129
|
commands.push('# Build completed');
|
|
119
130
|
commands.push('echo "Build completed successfully"');
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
// Products live under the per-platform Products directory now, not flat in
|
|
132
|
+
// $OUTPUT_DIR, so point the listing at where they actually are — otherwise
|
|
133
|
+
// the log says "artifacts in build/" above an empty directory.
|
|
134
|
+
commands.push('echo "Build artifacts in: $OUTPUT_DIR/DerivedData/Build/Products"');
|
|
135
|
+
commands.push('ls -lhR "$OUTPUT_DIR/DerivedData/Build/Products" 2>/dev/null || true');
|
|
122
136
|
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
123
137
|
return this.createScriptStep(script, stepName);
|
|
124
138
|
}
|
|
@@ -1013,16 +1027,19 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
|
1013
1027
|
commands.push('');
|
|
1014
1028
|
// Resolve the package graph before building, under the refused-fetch
|
|
1015
1029
|
// retry. This is the step an iOS app is normally built with, so it is the
|
|
1016
|
-
// one a refused dependency fetch lands on first.
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1030
|
+
// one a refused dependency fetch lands on first. The build below reads the
|
|
1031
|
+
// same derived data and finds the graph already resolved — the two
|
|
1032
|
+
// -derivedDataPath arguments have to move together, or the resolve
|
|
1033
|
+
// populates one location and the build re-resolves into another, which
|
|
1034
|
+
// silently spends the retry on work the build then throws away.
|
|
1019
1035
|
commands.push('# Resolve package dependencies first — see git-fetch-retry');
|
|
1020
1036
|
commands.push(...refusedFetchHelpers());
|
|
1021
1037
|
commands.push(...retryOnRefusedFetch({
|
|
1022
1038
|
label: 'Package resolution',
|
|
1023
1039
|
command: 'xcodebuild -resolvePackageDependencies' +
|
|
1024
1040
|
` "$PROJECT_TYPE" '${escapedPath}'` +
|
|
1025
|
-
` -scheme '${escapedScheme}'
|
|
1041
|
+
` -scheme '${escapedScheme}'` +
|
|
1042
|
+
' -derivedDataPath "$OUTPUT_DIR/DerivedData"',
|
|
1026
1043
|
}));
|
|
1027
1044
|
// Build command
|
|
1028
1045
|
let cmd = 'xcodebuild';
|
|
@@ -1036,18 +1053,54 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
|
1036
1053
|
cmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
1037
1054
|
}
|
|
1038
1055
|
cmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
1039
|
-
|
|
1056
|
+
// -derivedDataPath rather than CONFIGURATION_BUILD_DIR. The latter was set
|
|
1057
|
+
// to keep xcodebuild from writing a `build` directory inside each SPM
|
|
1058
|
+
// package checkout in DerivedData ("File exists but is not a directory" on
|
|
1059
|
+
// repeated runs) — but note what that fix actually was: making the path
|
|
1060
|
+
// ABSOLUTE. Pointing derived data somewhere writable achieves the same
|
|
1061
|
+
// thing without the cost below, and matches xcode-build-for-test.
|
|
1062
|
+
//
|
|
1063
|
+
// The cost, measured 2026-09-09 on four of the six iOS corpus rows:
|
|
1064
|
+
// CONFIGURATION_BUILD_DIR overrides the per-configuration/per-platform
|
|
1065
|
+
// product separation xcodebuild normally provides under
|
|
1066
|
+
// DerivedData/Build/Products/<config>-<platform>/, flattening every target
|
|
1067
|
+
// in the graph into ONE directory. Two build tasks for the same target then
|
|
1068
|
+
// declare the same absolute output path and xcodebuild refuses the whole
|
|
1069
|
+
// build with "Multiple commands produce". Read the notes it prints: they
|
|
1070
|
+
// name the SAME target twice. It is not two packages colliding on a name,
|
|
1071
|
+
// it is one target built twice into one directory. 16 collisions on
|
|
1072
|
+
// jellyfin/Swiftfin, 266 on Automattic/pocket-casts-ios, 425 on
|
|
1073
|
+
// home-assistant/iOS.
|
|
1074
|
+
//
|
|
1075
|
+
// It also breaks CocoaPods, which is the same defect wearing a disguise.
|
|
1076
|
+
// CocoaPods derives PODS_CONFIGURATION_BUILD_DIR from BUILD_DIR, and
|
|
1077
|
+
// BUILD_DIR is NOT what CONFIGURATION_BUILD_DIR overrides — so pod products
|
|
1078
|
+
// are written to the flat directory while every generated copy-resources
|
|
1079
|
+
// script and linker search path still points into DerivedData. On
|
|
1080
|
+
// shenruisi/Stay that surfaced as "Resource SDWebImage.bundle not found.
|
|
1081
|
+
// Run pod install", for a bundle that had in fact been built, two
|
|
1082
|
+
// directories away.
|
|
1083
|
+
//
|
|
1084
|
+
// Proved by control, same repo and image and flags, only this line
|
|
1085
|
+
// differing: CONFIGURATION_BUILD_DIR reproduced the collision and exit 65
|
|
1086
|
+
// with no .app; -derivedDataPath reached BUILD SUCCEEDED in 2m06s and left
|
|
1087
|
+
// both Debug and Debug-iphonesimulator under Products — the separation the
|
|
1088
|
+
// flat directory destroys.
|
|
1089
|
+
cmd += ` -derivedDataPath "$OUTPUT_DIR/DerivedData"`;
|
|
1040
1090
|
cmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
1041
1091
|
if (xcodebuildOptions) {
|
|
1042
1092
|
cmd += ` ${xcodebuildOptions}`;
|
|
1043
1093
|
}
|
|
1044
1094
|
commands.push(cmd);
|
|
1045
1095
|
commands.push('');
|
|
1046
|
-
// Locate .app
|
|
1096
|
+
// Locate .app. It now lands under the per-platform Products directory
|
|
1097
|
+
// rather than flat in $OUTPUT_DIR, so the guard has to follow it — at
|
|
1098
|
+
// maxdepth 1 this found nothing and the step would fail every build with
|
|
1099
|
+
// ".app not found" while the app had compiled perfectly.
|
|
1047
1100
|
commands.push('# Locate generated .app');
|
|
1048
|
-
commands.push('APP_PATH=$(find "$OUTPUT_DIR" -maxdepth
|
|
1101
|
+
commands.push('APP_PATH=$(find "$OUTPUT_DIR/DerivedData/Build/Products" -maxdepth 2 -name "*.app" -type d 2>/dev/null | head -1)');
|
|
1049
1102
|
commands.push('if [ -z "$APP_PATH" ]; then');
|
|
1050
|
-
commands.push(' echo "❌ Error: .app not found
|
|
1103
|
+
commands.push(' echo "❌ Error: .app not found under $OUTPUT_DIR/DerivedData/Build/Products"');
|
|
1051
1104
|
commands.push(' exit 1');
|
|
1052
1105
|
commands.push('fi');
|
|
1053
1106
|
commands.push('');
|