@invarn/cibuild 2.8.4 → 2.8.6
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 +11 -11
- package/dist/src/yaml/steps/android.d.ts +8 -0
- package/dist/src/yaml/steps/android.d.ts.map +1 -1
- package/dist/src/yaml/steps/android.js +5 -0
- package/dist/src/yaml/steps/cocoapods-locked-bundler.test.d.ts +17 -0
- package/dist/src/yaml/steps/cocoapods-locked-bundler.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cocoapods-locked-bundler.test.js +145 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +1 -1
- package/dist/src/yaml/steps/gradle-memory.d.ts +46 -0
- package/dist/src/yaml/steps/gradle-memory.d.ts.map +1 -0
- package/dist/src/yaml/steps/gradle-memory.js +268 -0
- package/dist/src/yaml/steps/gradle-memory.test.d.ts +2 -0
- package/dist/src/yaml/steps/gradle-memory.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/gradle-memory.test.js +197 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
- package/dist/src/yaml/steps/ios-deps.js +62 -10
- package/dist/src/yaml/steps/xcode-derived-data.test.js +70 -2
- package/dist/src/yaml/steps/xcode-new.test.js +1 -1
- package/dist/src/yaml/steps/xcode-resolve-retry.test.js +2 -2
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +40 -11
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradle's daemon heaps, fitted to the build machine; and a killed daemon,
|
|
3
|
+
* named as one.
|
|
4
|
+
*
|
|
5
|
+
* The fixtures are the `gradle.properties` of IacobIonut01/ReFra
|
|
6
|
+
* (`-Xmx8G`, inherited by the Kotlin daemon) and vitorpamplona/amethyst
|
|
7
|
+
* (`-Xmx6g` beside `kotlin.daemon.jvmargs=-Xmx8g`). Both died on a 10 GiB
|
|
8
|
+
* build machine with nothing in the log but "Gradle build daemon disappeared
|
|
9
|
+
* unexpectedly".
|
|
10
|
+
*
|
|
11
|
+
* The shell under test is the one that ships: it is executed here, not
|
|
12
|
+
* re-implemented.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
15
|
+
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { AndroidBuildForUITestingStepExecutor, AndroidLintStepExecutor, AndroidUnitTestStepExecutor, GradleBuildStepExecutor, } from "./android.js";
|
|
20
|
+
import { GRADLE_MEMORY_FUNCTIONS } from "./gradle-memory.js";
|
|
21
|
+
import { testConfig } from "./test-config.js";
|
|
22
|
+
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../test/fixtures/gradle-memory");
|
|
23
|
+
let dir;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
dir = mkdtempSync(join(tmpdir(), "cibuild-gradle-memory-"));
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
/** Run the shipped plan in `dir`, the way the step does, with a given memory. */
|
|
31
|
+
function plan(memMib, preferred = "", userHome = join(dir, "no-user-home")) {
|
|
32
|
+
const script = `${GRADLE_MEMORY_FUNCTIONS}
|
|
33
|
+
cibuild_gradle_memory_plan "$1" gradle.properties "$2/gradle.properties" "$3"
|
|
34
|
+
for a in "\${CIBUILD_GRADLE_MEMORY_ARGS[@]}"; do printf 'ARG %s\\n' "$a"; done
|
|
35
|
+
printf 'LINE %s\\n' "$CIBUILD_GRADLE_MEMORY_LINE"
|
|
36
|
+
printf 'SENTENCE %s\\n' "$CIBUILD_GRADLE_MEMORY_SENTENCE"`;
|
|
37
|
+
const out = execFileSync("bash", ["-c", script, "plan", memMib, userHome, preferred], {
|
|
38
|
+
cwd: dir,
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
});
|
|
41
|
+
const lines = out.split("\n");
|
|
42
|
+
return {
|
|
43
|
+
args: lines.filter((l) => l.startsWith("ARG ")).map((l) => l.slice(4)),
|
|
44
|
+
line: (lines.find((l) => l.startsWith("LINE ")) ?? "").slice(5),
|
|
45
|
+
sentence: (lines.find((l) => l.startsWith("SENTENCE ")) ?? "").slice(9),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const TEN_GIB = "10240";
|
|
49
|
+
describe("cibuild_gradle_memory_plan", () => {
|
|
50
|
+
test("ReFra: -Xmx8G, inherited by the Kotlin daemon, is capped on a 10 GiB machine", () => {
|
|
51
|
+
copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
|
|
52
|
+
const p = plan(TEN_GIB);
|
|
53
|
+
// 75% of 10 GiB, halved because the Kotlin daemon inherits it. Their own
|
|
54
|
+
// CI runs 4g.
|
|
55
|
+
expect(p.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3840m -Dfile.encoding=UTF-8"]);
|
|
56
|
+
expect(p.line).toContain("org.gradle.jvmargs=-Xmx8G -Dfile.encoding=UTF-8 at gradle.properties:9");
|
|
57
|
+
expect(p.line).toContain("16.0 GiB");
|
|
58
|
+
expect(p.line).toContain("-Xmx3840m");
|
|
59
|
+
expect(p.sentence).toMatch(/^The build machine has 10\.0 GiB; this build's Gradle and Kotlin daemons asked for 16\.0 GiB/);
|
|
60
|
+
});
|
|
61
|
+
test("amethyst: both heaps are scaled, and the Kotlin one keeps its other flags", () => {
|
|
62
|
+
copyFileSync(join(FIXTURES, "amethyst.gradle.properties"), join(dir, "gradle.properties"));
|
|
63
|
+
const p = plan(TEN_GIB);
|
|
64
|
+
expect(p.args).toEqual([
|
|
65
|
+
"-Dorg.gradle.jvmargs=-Xmx3264m -Dfile.encoding=UTF-8",
|
|
66
|
+
"-Pkotlin.daemon.jvmargs=-Xmx4352m -XX:MaxMetaspaceSize=2g",
|
|
67
|
+
]);
|
|
68
|
+
expect(p.line).toContain("kotlin.daemon.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=2g at gradle.properties:27");
|
|
69
|
+
expect(p.line).toContain("14.0 GiB");
|
|
70
|
+
});
|
|
71
|
+
test("heaps that fit are left alone, and nothing is printed", () => {
|
|
72
|
+
copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
|
|
73
|
+
const p = plan("65536");
|
|
74
|
+
expect(p.args).toEqual([]);
|
|
75
|
+
expect(p.line).toBe("");
|
|
76
|
+
expect(p.sentence).toContain("asked for 16.0 GiB");
|
|
77
|
+
});
|
|
78
|
+
test("a project that declares nothing gets Gradle's default and no cap", () => {
|
|
79
|
+
const p = plan(TEN_GIB);
|
|
80
|
+
expect(p.args).toEqual([]);
|
|
81
|
+
expect(p.sentence).toContain("Gradle's default -Xmx512m");
|
|
82
|
+
});
|
|
83
|
+
test("in-process Kotlin compilation has no Kotlin daemon to count", () => {
|
|
84
|
+
writeFileSync(join(dir, "gradle.properties"), "org.gradle.jvmargs=-Xmx4g\nkotlin.compiler.execution.strategy=in-process\n");
|
|
85
|
+
expect(plan("6144").args).toEqual([]);
|
|
86
|
+
expect(plan("4096").args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3072m"]);
|
|
87
|
+
});
|
|
88
|
+
test("the Gradle user home's value wins over the project's, as in Gradle", () => {
|
|
89
|
+
writeFileSync(join(dir, "gradle.properties"), "org.gradle.jvmargs=-Xmx1g\n");
|
|
90
|
+
const home = join(dir, "home");
|
|
91
|
+
mkdirSync(home);
|
|
92
|
+
writeFileSync(join(home, "gradle.properties"), "org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC\n");
|
|
93
|
+
const p = plan(TEN_GIB, "", home);
|
|
94
|
+
expect(p.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3840m -XX:+UseParallelGC"]);
|
|
95
|
+
expect(p.line).toContain(`at ${home}/gradle.properties:1`);
|
|
96
|
+
});
|
|
97
|
+
test("JVM arguments the pipeline asks for replace the project's, and are still capped", () => {
|
|
98
|
+
copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
|
|
99
|
+
const fits = plan(TEN_GIB, "-Xmx2g -Dfile.encoding=UTF-8");
|
|
100
|
+
expect(fits.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8"]);
|
|
101
|
+
expect(fits.line).toContain("as the pipeline asks");
|
|
102
|
+
const capped = plan("4096", "-Xmx4g -Dfile.encoding=UTF-8");
|
|
103
|
+
expect(capped.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8"]);
|
|
104
|
+
expect(capped.line).toContain("at the pipeline");
|
|
105
|
+
});
|
|
106
|
+
test("a value continued onto another line is left as it is, and says so", () => {
|
|
107
|
+
writeFileSync(join(dir, "gradle.properties"), "org.gradle.jvmargs=-Xmx12g \\\n -Dfile.encoding=UTF-8\n");
|
|
108
|
+
const p = plan(TEN_GIB);
|
|
109
|
+
expect(p.args).toEqual([]);
|
|
110
|
+
expect(p.line).toContain("continues onto another line");
|
|
111
|
+
});
|
|
112
|
+
test("an unknown machine size caps nothing", () => {
|
|
113
|
+
copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
|
|
114
|
+
const p = plan("");
|
|
115
|
+
expect(p.args).toEqual([]);
|
|
116
|
+
expect(p.sentence).toContain("could not be read");
|
|
117
|
+
});
|
|
118
|
+
test("-Xmx in every unit, and a comment is not an assignment", () => {
|
|
119
|
+
writeFileSync(join(dir, "gradle.properties"), "# org.gradle.jvmargs=-Xmx64g\norg.gradle.jvmargs = -Xms1g -Xmx6144m\nkotlin.daemon.jvmargs:-Xmx4194304k\n");
|
|
120
|
+
const p = plan("8192");
|
|
121
|
+
expect(p.args).toEqual([
|
|
122
|
+
"-Dorg.gradle.jvmargs=-Xms1g -Xmx3648m",
|
|
123
|
+
"-Pkotlin.daemon.jvmargs=-Xmx2432m",
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
/** A directory standing in for PATH, with a fake `dmesg` that prints `oom`. */
|
|
128
|
+
function fakeBin(oom) {
|
|
129
|
+
const bin = join(dir, "bin");
|
|
130
|
+
mkdirSync(bin, { recursive: true });
|
|
131
|
+
writeFileSync(join(bin, "dmesg"), `#!/bin/bash\nprintf '%s\\n' ${JSON.stringify(oom)}\n`);
|
|
132
|
+
chmodSync(join(bin, "dmesg"), 0o755);
|
|
133
|
+
return bin;
|
|
134
|
+
}
|
|
135
|
+
/** A `./gradlew` that records its arguments and fails the way it is told. */
|
|
136
|
+
function fakeGradlew(body) {
|
|
137
|
+
writeFileSync(join(dir, "gradlew"), `#!/bin/bash\nprintf '%s\\n' "$@" > "$(dirname "$0")/gradlew-args"\n${body}\n`);
|
|
138
|
+
chmodSync(join(dir, "gradlew"), 0o755);
|
|
139
|
+
}
|
|
140
|
+
function runStep(script, bin) {
|
|
141
|
+
writeFileSync(join(dir, "step.sh"), script);
|
|
142
|
+
return spawnSync("bash", [join(dir, "step.sh")], {
|
|
143
|
+
cwd: dir,
|
|
144
|
+
encoding: "utf-8",
|
|
145
|
+
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, JAVA_HOME: "", CIBUILD_SOURCE_DIR: "" },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
describe("the Gradle steps", () => {
|
|
149
|
+
const OOM_LINE = "[ 412.9] Out of memory: Killed process 4242 (java) total-vm:14680064kB, anon-rss:9437184kB";
|
|
150
|
+
test("a killed daemon prints the kernel's out-of-memory lines and the sentence, and still fails", async () => {
|
|
151
|
+
copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
|
|
152
|
+
fakeGradlew('echo "FAILURE: Build failed with an exception."\necho "Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)"\nexit 1');
|
|
153
|
+
const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug" }, {}, testConfig);
|
|
154
|
+
const run = runStep(step.script, fakeBin(OOM_LINE));
|
|
155
|
+
expect(run.status).toBe(1);
|
|
156
|
+
expect(run.stdout).toContain("Gradle memory: a Gradle daemon was killed rather than failing the build (exit 1).");
|
|
157
|
+
expect(run.stdout).toContain(` ${OOM_LINE}`);
|
|
158
|
+
expect(run.stdout).toMatch(/The build machine has [0-9.]+ GiB; this build's Gradle and Kotlin daemons asked for 16\.0 GiB/);
|
|
159
|
+
expect(run.stdout).not.toContain("Gradle build completed successfully");
|
|
160
|
+
});
|
|
161
|
+
test("an ordinary failure says nothing about memory", async () => {
|
|
162
|
+
fakeGradlew('echo "Compilation error"\nexit 1');
|
|
163
|
+
const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug" }, {}, testConfig);
|
|
164
|
+
const run = runStep(step.script, fakeBin(OOM_LINE));
|
|
165
|
+
expect(run.status).toBe(1);
|
|
166
|
+
expect(run.stdout).not.toContain("Gradle memory:");
|
|
167
|
+
});
|
|
168
|
+
test("the pipeline's jvmargs reach Gradle as one argument, before the task", async () => {
|
|
169
|
+
fakeGradlew("exit 0");
|
|
170
|
+
const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug", gradle_options: "--stacktrace", jvmargs: "-Xmx256m -Dfile.encoding=UTF-8" }, {}, testConfig);
|
|
171
|
+
const run = runStep(step.script, fakeBin(""));
|
|
172
|
+
expect(run.status).toBe(0);
|
|
173
|
+
const args = execFileSync("cat", [join(dir, "gradlew-args")], { encoding: "utf-8" }).trim().split("\n");
|
|
174
|
+
expect(args).toEqual(["-Dorg.gradle.jvmargs=-Xmx256m -Dfile.encoding=UTF-8", "assembleDebug", "--stacktrace"]);
|
|
175
|
+
});
|
|
176
|
+
test("every Gradle step runs through the plan, and every one is valid bash", async () => {
|
|
177
|
+
const steps = [
|
|
178
|
+
await new GradleBuildStepExecutor().execute({}, {}, testConfig),
|
|
179
|
+
await new AndroidLintStepExecutor().execute({}, {}, testConfig),
|
|
180
|
+
await new AndroidUnitTestStepExecutor().execute({}, {}, testConfig),
|
|
181
|
+
await new AndroidBuildForUITestingStepExecutor().execute({}, {}, testConfig),
|
|
182
|
+
];
|
|
183
|
+
for (const step of steps) {
|
|
184
|
+
const script = step.script;
|
|
185
|
+
expect(script).toContain("cibuild_gradle_memory_cap ''");
|
|
186
|
+
execFileSync("bash", ["-n"], { input: script });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// cibuild's log is parsed for step-progress markers; nothing here may look
|
|
190
|
+
// like one.
|
|
191
|
+
test("no line the functions print can be taken for a step-progress marker", () => {
|
|
192
|
+
for (const line of GRADLE_MEMORY_FUNCTIONS.split("\n")) {
|
|
193
|
+
expect(line).not.toMatch(/INVARN_(STEP|PHASE)|▸/);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=gradle-memory.test.js.map
|
|
@@ -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;;;;;;;;;;;;;;;;;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;
|
|
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;CAoJpB;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"}
|
|
@@ -65,17 +65,69 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
65
65
|
commands.push('# Detect Gemfile with cocoapods gem → use bundle exec');
|
|
66
66
|
commands.push('USE_BUNDLE_EXEC="false"');
|
|
67
67
|
commands.push('if [ -f "Gemfile.lock" ] && grep -q "cocoapods" "Gemfile.lock" 2>/dev/null; then');
|
|
68
|
-
commands.push('
|
|
69
|
-
commands.push('
|
|
70
|
-
commands.push(' echo "Detected cocoapods in Gemfile.lock — using bundle exec"');
|
|
71
|
-
commands.push(' bundle install --quiet');
|
|
72
|
-
commands.push(' fi');
|
|
68
|
+
commands.push(' USE_BUNDLE_EXEC="true"');
|
|
69
|
+
commands.push(' echo "Detected cocoapods in Gemfile.lock — using bundle exec"');
|
|
73
70
|
commands.push('elif [ -f "Gemfile" ] && grep -q "cocoapods" "Gemfile" 2>/dev/null; then');
|
|
74
|
-
commands.push('
|
|
75
|
-
commands.push('
|
|
76
|
-
commands.push('
|
|
77
|
-
commands.push('
|
|
71
|
+
commands.push(' USE_BUNDLE_EXEC="true"');
|
|
72
|
+
commands.push(' echo "Detected cocoapods in Gemfile — using bundle exec"');
|
|
73
|
+
commands.push('fi');
|
|
74
|
+
commands.push('');
|
|
75
|
+
// Run the bundler the lockfile names, from a Ruby that can install it.
|
|
76
|
+
//
|
|
77
|
+
// A Gemfile.lock ends in `BUNDLED WITH <version>`, and RubyGems refuses to
|
|
78
|
+
// start any other bundler for it: "Could not find 'bundler' (2.3.21)
|
|
79
|
+
// required by your Gemfile.lock". The step used to run whatever `bundle`
|
|
80
|
+
// was on PATH, which on a mac guest is /usr/bin/bundle, system Ruby 2.6 —
|
|
81
|
+
// and system Ruby cannot `gem install` without sudo, so nothing in the step
|
|
82
|
+
// could recover.
|
|
83
|
+
//
|
|
84
|
+
// Three moves, each conditional, so a machine missing a piece keeps the
|
|
85
|
+
// old behaviour rather than failing on it:
|
|
86
|
+
// - prefer an installed Homebrew Ruby. It is keg-only, so it is not on
|
|
87
|
+
// PATH even where it is installed (Homebrew's cocoapods and fastlane
|
|
88
|
+
// formulae depend on it), and the system 2.6 wins by default;
|
|
89
|
+
// - point GEM_HOME at a writable directory outside the checkout, so both
|
|
90
|
+
// `gem install bundler` and `bundle install` can write without sudo,
|
|
91
|
+
// and put its bin directory first;
|
|
92
|
+
// - install the locked bundler if it is absent and invoke it by version,
|
|
93
|
+
// `bundle _<version>_`, which is the form RubyGems accepts.
|
|
94
|
+
commands.push('BUNDLE_CMD="bundle"');
|
|
95
|
+
commands.push('if [ "$USE_BUNDLE_EXEC" = "true" ]; then');
|
|
96
|
+
commands.push(' if command -v brew &>/dev/null; then');
|
|
97
|
+
commands.push(' BREW_RUBY="$(brew --prefix --installed ruby 2>/dev/null || true)"');
|
|
98
|
+
commands.push(' if [ -n "$BREW_RUBY" ] && [ -x "$BREW_RUBY/bin/ruby" ]; then');
|
|
99
|
+
commands.push(' export PATH="$BREW_RUBY/bin:$PATH"');
|
|
100
|
+
commands.push(' fi');
|
|
101
|
+
commands.push(' fi');
|
|
102
|
+
commands.push(' if ! command -v bundle &>/dev/null && ! command -v gem &>/dev/null; then');
|
|
103
|
+
commands.push(' USE_BUNDLE_EXEC="false"');
|
|
104
|
+
commands.push(' echo "No bundle or gem on PATH — running pod directly"');
|
|
105
|
+
commands.push(' fi');
|
|
106
|
+
commands.push('fi');
|
|
107
|
+
commands.push('if [ "$USE_BUNDLE_EXEC" = "true" ]; then');
|
|
108
|
+
commands.push(' export GEM_HOME="${CIBUILD_SCRATCH:-${TMPDIR:-/tmp}}/cibuild-gems"');
|
|
109
|
+
commands.push(' mkdir -p "$GEM_HOME"');
|
|
110
|
+
commands.push(' export PATH="$GEM_HOME/bin:$PATH"');
|
|
111
|
+
commands.push(' RUBY_VERSION_USED="$(ruby -e \'print RUBY_VERSION\' 2>/dev/null || echo unknown)"');
|
|
112
|
+
commands.push(' echo "Ruby: $(command -v ruby || echo none) ($RUBY_VERSION_USED), GEM_HOME: $GEM_HOME"');
|
|
113
|
+
commands.push(' LOCKED_BUNDLER=""');
|
|
114
|
+
commands.push(' if [ -f "Gemfile.lock" ]; then');
|
|
115
|
+
commands.push(' LOCKED_BUNDLER="$(awk \'/^BUNDLED WITH/ { getline; gsub(/[[:space:]]/, ""); print; exit }\' Gemfile.lock)"');
|
|
116
|
+
commands.push(' fi');
|
|
117
|
+
commands.push(' if [ -n "$LOCKED_BUNDLER" ]; then');
|
|
118
|
+
commands.push(' if ! gem list -i bundler -v "$LOCKED_BUNDLER" &>/dev/null; then');
|
|
119
|
+
commands.push(' echo "Installing bundler $LOCKED_BUNDLER, which Gemfile.lock names under BUNDLED WITH"');
|
|
120
|
+
commands.push(' if ! gem install bundler -v "$LOCKED_BUNDLER" --no-document; then');
|
|
121
|
+
commands.push(' echo "❌ Error: could not install bundler $LOCKED_BUNDLER (Gemfile.lock BUNDLED WITH) under Ruby $RUBY_VERSION_USED"');
|
|
122
|
+
commands.push(' exit 1');
|
|
123
|
+
commands.push(' fi');
|
|
124
|
+
commands.push(' fi');
|
|
125
|
+
commands.push(' BUNDLE_CMD="bundle _${LOCKED_BUNDLER}_"');
|
|
126
|
+
commands.push(' elif ! command -v bundle &>/dev/null; then');
|
|
127
|
+
commands.push(' echo "Installing bundler — none on PATH and Gemfile.lock names no version"');
|
|
128
|
+
commands.push(' gem install bundler --no-document');
|
|
78
129
|
commands.push(' fi');
|
|
130
|
+
commands.push(' $BUNDLE_CMD install --quiet');
|
|
79
131
|
commands.push('fi');
|
|
80
132
|
commands.push('');
|
|
81
133
|
// Build pod command
|
|
@@ -87,7 +139,7 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
87
139
|
// refused anonymous clone there fails the step exactly as it does during a
|
|
88
140
|
// Swift package resolve — same signature, same fix.
|
|
89
141
|
commands.push('if [ "$USE_BUNDLE_EXEC" = "true" ]; then');
|
|
90
|
-
commands.push(' POD_INVOCATION="
|
|
142
|
+
commands.push(' POD_INVOCATION="$BUNDLE_CMD exec pod"');
|
|
91
143
|
commands.push('else');
|
|
92
144
|
commands.push(' POD_INVOCATION="pod"');
|
|
93
145
|
commands.push('fi');
|
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
* left both `Debug` and `Debug-iphonesimulator` under Products.
|
|
28
28
|
*/
|
|
29
29
|
import { describe, test, expect } from '@jest/globals';
|
|
30
|
+
import { execFileSync } from 'child_process';
|
|
31
|
+
import { mkdtempSync, realpathSync } from 'fs';
|
|
32
|
+
import { tmpdir } from 'os';
|
|
33
|
+
import { join } from 'path';
|
|
30
34
|
import { XcodeBuildStepExecutor, XcodeBuildForTestStepExecutor, XcodeBuildForSimulatorStepExecutor, } from './xcode.js';
|
|
31
35
|
import { APP_PRODUCT_RESOLVER_SOURCE } from './xcode-app-product.js';
|
|
32
36
|
import { testConfig } from './test-config.js';
|
|
33
|
-
const DERIVED = '-derivedDataPath "$
|
|
37
|
+
const DERIVED = '-derivedDataPath "$DERIVED_DATA_PATH"';
|
|
34
38
|
const scriptFor = async (executor, inputs) => {
|
|
35
39
|
const result = await executor.execute({ project_path: 'MyApp.xcodeproj', scheme: 'MyApp', ...inputs }, {}, testConfig);
|
|
36
40
|
return result.script;
|
|
@@ -71,7 +75,7 @@ describe('xcode-build-for-simulator finds the .app where it now lands', () => {
|
|
|
71
75
|
const build = (inputs = {}) => scriptFor(new XcodeBuildForSimulatorStepExecutor(), inputs);
|
|
72
76
|
test('searches under the per-platform Products directory', async () => {
|
|
73
77
|
const script = await build();
|
|
74
|
-
expect(script).toContain('PRODUCTS_DIR="$
|
|
78
|
+
expect(script).toContain('PRODUCTS_DIR="$DERIVED_DATA_PATH/Build/Products"');
|
|
75
79
|
expect(script).toContain('find "$PRODUCTS_DIR"');
|
|
76
80
|
});
|
|
77
81
|
test('searches deep enough to reach <config>-<platform>/Foo.app', async () => {
|
|
@@ -165,4 +169,68 @@ describe('the output_dir input still steers where everything goes', () => {
|
|
|
165
169
|
expect(script).toContain(DERIVED);
|
|
166
170
|
});
|
|
167
171
|
});
|
|
172
|
+
/**
|
|
173
|
+
* Derived data lives outside the source tree.
|
|
174
|
+
*
|
|
175
|
+
* With `-derivedDataPath "$OUTPUT_DIR/DerivedData"` and an output_dir of
|
|
176
|
+
* `build/app`, every resolved SPM checkout sat at
|
|
177
|
+
* `<repo>/build/app/DerivedData/SourcePackages/checkouts/` — inside the
|
|
178
|
+
* working tree an app's own SwiftLint build phase lints. vinhnx/Clendar went red
|
|
179
|
+
* on 145 serious violations, every one of them in a third-party package, none
|
|
180
|
+
* in the app. A developer's Xcode keeps derived data under ~/Library, so no
|
|
181
|
+
* repository has a reason to exclude it.
|
|
182
|
+
*/
|
|
183
|
+
describe('derived data is outside the source tree', () => {
|
|
184
|
+
const definitionOf = (script) => {
|
|
185
|
+
const line = script.split('\n').find((l) => l.startsWith('DERIVED_DATA_PATH='));
|
|
186
|
+
expect(line).toBeDefined();
|
|
187
|
+
return line;
|
|
188
|
+
};
|
|
189
|
+
// Runs the step's own definition line, the way the step would.
|
|
190
|
+
const resolve = (script, env) => execFileSync('bash', ['-c', `${definitionOf(script)}\nprintf '%s' "$DERIVED_DATA_PATH"`], {
|
|
191
|
+
env: { PATH: process.env.PATH ?? '', ...env },
|
|
192
|
+
}).toString();
|
|
193
|
+
test.each(buildingExecutors())('%s never builds into the output dir', async (_name, executor) => {
|
|
194
|
+
expect(await scriptFor(executor, {})).not.toContain('$OUTPUT_DIR/DerivedData');
|
|
195
|
+
});
|
|
196
|
+
test.each(buildingExecutors())('%s puts derived data under the scratch root, not under the checkout', async (_name, executor) => {
|
|
197
|
+
const scratch = realpathSync(mkdtempSync(join(tmpdir(), 'scratch-')));
|
|
198
|
+
const repo = join(scratch, 'workspace', 'repo');
|
|
199
|
+
const path = resolve(await scriptFor(executor, { output_dir: 'build/app' }), {
|
|
200
|
+
CIBUILD_SCRATCH: scratch,
|
|
201
|
+
OUTPUT_DIR: join(repo, 'build', 'app'),
|
|
202
|
+
});
|
|
203
|
+
expect(path.startsWith(scratch + '/')).toBe(true);
|
|
204
|
+
expect(path.startsWith(repo + '/')).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
test('falls back to TMPDIR outside a sandbox', async () => {
|
|
207
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'tmp-')));
|
|
208
|
+
const path = resolve(await scriptFor(new XcodeBuildStepExecutor(), {}), {
|
|
209
|
+
TMPDIR: tmp,
|
|
210
|
+
OUTPUT_DIR: '/work/repo/build',
|
|
211
|
+
});
|
|
212
|
+
expect(path.startsWith(tmp + '/')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
// The iOS template names the same output_dir on Build App and Build for
|
|
215
|
+
// Testing so the second step reads what the first compiled. That sharing has
|
|
216
|
+
// to survive the move: same output dir, same derived data.
|
|
217
|
+
test('two steps naming the same output_dir share derived data', async () => {
|
|
218
|
+
const env = { CIBUILD_SCRATCH: '/scratch', OUTPUT_DIR: '/work/repo/build/app' };
|
|
219
|
+
const app = resolve(await scriptFor(new XcodeBuildForSimulatorStepExecutor(), { output_dir: 'build/app' }), env);
|
|
220
|
+
const tests = resolve(await scriptFor(new XcodeBuildForTestStepExecutor(), { output_dir: 'build/app' }), env);
|
|
221
|
+
expect(tests).toBe(app);
|
|
222
|
+
});
|
|
223
|
+
test('two different output dirs stay apart', async () => {
|
|
224
|
+
const script = await scriptFor(new XcodeBuildStepExecutor(), {});
|
|
225
|
+
const a = resolve(script, { CIBUILD_SCRATCH: '/scratch', OUTPUT_DIR: '/work/repo/build/app' });
|
|
226
|
+
const b = resolve(script, { CIBUILD_SCRATCH: '/scratch', OUTPUT_DIR: '/work/repo/build' });
|
|
227
|
+
expect(a).not.toBe(b);
|
|
228
|
+
});
|
|
229
|
+
test('the product listing and the xctestrun search follow it', async () => {
|
|
230
|
+
const build = await scriptFor(new XcodeBuildStepExecutor(), {});
|
|
231
|
+
expect(build).toContain('ls -lhR "$DERIVED_DATA_PATH/Build/Products"');
|
|
232
|
+
const forTest = await scriptFor(new XcodeBuildForTestStepExecutor(), {});
|
|
233
|
+
expect(forTest).toContain('find "$DERIVED_DATA_PATH/Build/Products" -name "*.xctestrun"');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
168
236
|
//# sourceMappingURL=xcode-derived-data.test.js.map
|
|
@@ -52,7 +52,7 @@ describe('XcodeBuildForTestStepExecutor', () => {
|
|
|
52
52
|
const executor = new XcodeBuildForTestStepExecutor();
|
|
53
53
|
const result = await executor.execute({ project_path: 'MyApp.xcodeproj', scheme: 'MyApp' }, {}, testConfig);
|
|
54
54
|
expect(result.script).toContain('-derivedDataPath');
|
|
55
|
-
expect(result.script).toContain('
|
|
55
|
+
expect(result.script).toContain('-derivedDataPath "$DERIVED_DATA_PATH"');
|
|
56
56
|
});
|
|
57
57
|
test('should locate .xctestrun file', async () => {
|
|
58
58
|
const executor = new XcodeBuildForTestStepExecutor();
|
|
@@ -29,7 +29,7 @@ describe('xcode-build-for-test resolves before it builds', () => {
|
|
|
29
29
|
const resolveLine = script
|
|
30
30
|
.split('\n')
|
|
31
31
|
.find((l) => l.includes('-resolvePackageDependencies'));
|
|
32
|
-
expect(resolveLine).toContain('-derivedDataPath "$
|
|
32
|
+
expect(resolveLine).toContain('-derivedDataPath "$DERIVED_DATA_PATH"');
|
|
33
33
|
});
|
|
34
34
|
test('passes the same project type and scheme as the build', async () => {
|
|
35
35
|
const resolveLine = (await build())
|
|
@@ -131,7 +131,7 @@ describe('cocoapods-install retries a refused pod source fetch', () => {
|
|
|
131
131
|
});
|
|
132
132
|
test('still chooses between bundle exec and bare pod', async () => {
|
|
133
133
|
const script = await build();
|
|
134
|
-
expect(script).toContain('
|
|
134
|
+
expect(script).toContain('$BUNDLE_CMD exec pod');
|
|
135
135
|
expect(script).toContain('USE_BUNDLE_EXEC');
|
|
136
136
|
});
|
|
137
137
|
test('runs the pod command exactly once in the script', async () => {
|
|
@@ -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;AAI7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,32 @@ import { BaseStepExecutor } from './base.js';
|
|
|
6
6
|
import { resolveDestinationInput } from './xcode-destination.js';
|
|
7
7
|
import { APP_PRODUCT_RESOLVER_SOURCE } from './xcode-app-product.js';
|
|
8
8
|
import { refusedFetchHelpers, retryOnRefusedFetch } from './git-fetch-retry.js';
|
|
9
|
+
/**
|
|
10
|
+
* Where derived data goes: outside the source tree, keyed by the output dir.
|
|
11
|
+
*
|
|
12
|
+
* It used to be "$DERIVED_DATA_PATH", and OUTPUT_DIR is inside the
|
|
13
|
+
* checkout — so every resolved SPM package sat in the working tree, where an
|
|
14
|
+
* app's own lint or format build phase walks it. vinhnx/Clendar's SwiftLint
|
|
15
|
+
* phase failed the build on 145 serious violations, all of them in
|
|
16
|
+
* third-party packages under build/app/DerivedData/SourcePackages/checkouts.
|
|
17
|
+
* A developer's Xcode keeps derived data under ~/Library, so no repository has
|
|
18
|
+
* a reason to exclude that directory, and it should not have to.
|
|
19
|
+
*
|
|
20
|
+
* CIBUILD_SCRATCH is the runner's per-build local root, which holds the
|
|
21
|
+
* checkout rather than sitting inside it; outside a sandbox it falls back to
|
|
22
|
+
* TMPDIR. The key is a checksum of the absolute output dir, so two steps that
|
|
23
|
+
* name the same output_dir still share derived data (the iOS template relies
|
|
24
|
+
* on that to avoid compiling the app twice) and two that name different ones
|
|
25
|
+
* stay apart, as they did before.
|
|
26
|
+
*/
|
|
27
|
+
function derivedDataPathLines() {
|
|
28
|
+
return [
|
|
29
|
+
'# Derived data outside the source tree, keyed by the output dir',
|
|
30
|
+
'DERIVED_DATA_PATH="${CIBUILD_SCRATCH:-${TMPDIR:-/tmp}}/cibuild-derived-data/$(printf \'%s\' "$OUTPUT_DIR" | cksum | cut -d\' \' -f1)"',
|
|
31
|
+
'mkdir -p "$DERIVED_DATA_PATH"',
|
|
32
|
+
'echo "Derived data: $DERIVED_DATA_PATH"',
|
|
33
|
+
];
|
|
34
|
+
}
|
|
9
35
|
/**
|
|
10
36
|
* Xcodebuild step executor
|
|
11
37
|
* Builds iOS/macOS projects using xcodebuild
|
|
@@ -64,6 +90,7 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
64
90
|
commands.push('# Create output directory and resolve to absolute path');
|
|
65
91
|
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
66
92
|
commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
|
|
93
|
+
commands.push(...derivedDataPathLines());
|
|
67
94
|
// Create xcconfig file if content provided
|
|
68
95
|
if (xcconfigContent && xcconfigContent.trim() !== '') {
|
|
69
96
|
commands.push('');
|
|
@@ -97,7 +124,7 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
97
124
|
command: 'xcodebuild -resolvePackageDependencies' +
|
|
98
125
|
` "$PROJECT_TYPE" '${escapedPath}'` +
|
|
99
126
|
` -scheme '${this.escapeBash(scheme)}'` +
|
|
100
|
-
' -derivedDataPath "$
|
|
127
|
+
' -derivedDataPath "$DERIVED_DATA_PATH"',
|
|
101
128
|
}));
|
|
102
129
|
// Build xcodebuild command
|
|
103
130
|
commands.push('');
|
|
@@ -112,14 +139,14 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
112
139
|
xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
|
|
113
140
|
xcodebuildCmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
114
141
|
xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
115
|
-
//
|
|
142
|
+
// A derived data path (see derivedDataPathLines), rather than CONFIGURATION_BUILD_DIR.
|
|
116
143
|
// Same reasoning as XcodeBuildForSimulatorStepExecutor below, where the
|
|
117
144
|
// full mechanism and the control are written up: the flat directory
|
|
118
145
|
// collapses xcodebuild's per-configuration/per-platform product separation
|
|
119
146
|
// and any graph with two tasks producing the same output name is refused
|
|
120
147
|
// with "Multiple commands produce". It also desynchronises CocoaPods, whose
|
|
121
148
|
// PODS_CONFIGURATION_BUILD_DIR follows BUILD_DIR and not this setting.
|
|
122
|
-
xcodebuildCmd += ` -derivedDataPath "$
|
|
149
|
+
xcodebuildCmd += ` -derivedDataPath "$DERIVED_DATA_PATH"`;
|
|
123
150
|
// Add xcconfig if provided
|
|
124
151
|
if (xcconfigContent && xcconfigContent.trim() !== '') {
|
|
125
152
|
xcodebuildCmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
@@ -132,8 +159,8 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
|
132
159
|
// Products live under the per-platform Products directory now, not flat in
|
|
133
160
|
// $OUTPUT_DIR, so point the listing at where they actually are — otherwise
|
|
134
161
|
// the log says "artifacts in build/" above an empty directory.
|
|
135
|
-
commands.push('echo "Build artifacts in: $
|
|
136
|
-
commands.push('ls -lhR "$
|
|
162
|
+
commands.push('echo "Build artifacts in: $DERIVED_DATA_PATH/Build/Products"');
|
|
163
|
+
commands.push('ls -lhR "$DERIVED_DATA_PATH/Build/Products" 2>/dev/null || true');
|
|
137
164
|
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
138
165
|
return this.createScriptStep(script, stepName);
|
|
139
166
|
}
|
|
@@ -806,6 +833,7 @@ export class XcodeBuildForTestStepExecutor extends BaseStepExecutor {
|
|
|
806
833
|
// Output directory
|
|
807
834
|
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
808
835
|
commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
|
|
836
|
+
commands.push(...derivedDataPathLines());
|
|
809
837
|
commands.push('');
|
|
810
838
|
// xcconfig
|
|
811
839
|
if (xcconfigContent.trim()) {
|
|
@@ -833,7 +861,7 @@ export class XcodeBuildForTestStepExecutor extends BaseStepExecutor {
|
|
|
833
861
|
command: 'xcodebuild -resolvePackageDependencies' +
|
|
834
862
|
` "$PROJECT_TYPE" '${escapedPath}'` +
|
|
835
863
|
` -scheme '${escapedScheme}'` +
|
|
836
|
-
' -derivedDataPath "$
|
|
864
|
+
' -derivedDataPath "$DERIVED_DATA_PATH"',
|
|
837
865
|
}));
|
|
838
866
|
// Build command
|
|
839
867
|
let cmd = 'xcodebuild build-for-testing';
|
|
@@ -841,7 +869,7 @@ export class XcodeBuildForTestStepExecutor extends BaseStepExecutor {
|
|
|
841
869
|
cmd += ` -scheme '${escapedScheme}'`;
|
|
842
870
|
cmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
843
871
|
cmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
844
|
-
cmd += ` -derivedDataPath "$
|
|
872
|
+
cmd += ` -derivedDataPath "$DERIVED_DATA_PATH"`;
|
|
845
873
|
if (testPlan) {
|
|
846
874
|
cmd += ` -testPlan '${this.escapeBash(testPlan)}'`;
|
|
847
875
|
}
|
|
@@ -855,7 +883,7 @@ export class XcodeBuildForTestStepExecutor extends BaseStepExecutor {
|
|
|
855
883
|
commands.push('');
|
|
856
884
|
// Locate .xctestrun file
|
|
857
885
|
commands.push('# Locate generated .xctestrun file');
|
|
858
|
-
commands.push('XCTESTRUN_PATH=$(find "$
|
|
886
|
+
commands.push('XCTESTRUN_PATH=$(find "$DERIVED_DATA_PATH/Build/Products" -name "*.xctestrun" | head -1)');
|
|
859
887
|
commands.push('if [ -z "$XCTESTRUN_PATH" ]; then');
|
|
860
888
|
commands.push(' echo "❌ Error: .xctestrun file not found"');
|
|
861
889
|
commands.push(' exit 1');
|
|
@@ -1007,6 +1035,7 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
|
1007
1035
|
// Output dir
|
|
1008
1036
|
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
1009
1037
|
commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
|
|
1038
|
+
commands.push(...derivedDataPathLines());
|
|
1010
1039
|
commands.push('');
|
|
1011
1040
|
// xcconfig
|
|
1012
1041
|
commands.push('XCCONFIG_FILE="$(mktemp -t cibuild-xcconfig).xcconfig"');
|
|
@@ -1040,7 +1069,7 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
|
1040
1069
|
command: 'xcodebuild -resolvePackageDependencies' +
|
|
1041
1070
|
` "$PROJECT_TYPE" '${escapedPath}'` +
|
|
1042
1071
|
` -scheme '${escapedScheme}'` +
|
|
1043
|
-
' -derivedDataPath "$
|
|
1072
|
+
' -derivedDataPath "$DERIVED_DATA_PATH"',
|
|
1044
1073
|
}));
|
|
1045
1074
|
// Build command
|
|
1046
1075
|
let cmd = 'xcodebuild';
|
|
@@ -1087,7 +1116,7 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
|
1087
1116
|
// with no .app; -derivedDataPath reached BUILD SUCCEEDED in 2m06s and left
|
|
1088
1117
|
// both Debug and Debug-iphonesimulator under Products — the separation the
|
|
1089
1118
|
// flat directory destroys.
|
|
1090
|
-
cmd += ` -derivedDataPath "$
|
|
1119
|
+
cmd += ` -derivedDataPath "$DERIVED_DATA_PATH"`;
|
|
1091
1120
|
cmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
1092
1121
|
if (xcodebuildOptions) {
|
|
1093
1122
|
cmd += ` ${xcodebuildOptions}`;
|
|
@@ -1106,7 +1135,7 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
|
1106
1135
|
// answer — a project with no shared scheme is ordinary and builds fine, so
|
|
1107
1136
|
// this resolves the same bundle it always did there.
|
|
1108
1137
|
commands.push('# Locate generated .app — the one this scheme builds');
|
|
1109
|
-
commands.push('PRODUCTS_DIR="$
|
|
1138
|
+
commands.push('PRODUCTS_DIR="$DERIVED_DATA_PATH/Build/Products"');
|
|
1110
1139
|
commands.push('APP_RESOLVER="$(mktemp -t cibuild-app-product).cjs"');
|
|
1111
1140
|
commands.push(`cat > "$APP_RESOLVER" << 'CIBUILD_APP_PRODUCT_EOF'`);
|
|
1112
1141
|
commands.push(APP_PRODUCT_RESOLVER_SOURCE.trim());
|