@invarn/cibuild 2.8.5 → 2.8.7

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.
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Gradle's daemon heaps, fitted to the machine the build runs on.
3
+ *
4
+ * A project's `gradle.properties` sizes its heaps for a developer laptop. On a
5
+ * 10 GiB build guest, `org.gradle.jvmargs=-Xmx8G` (which the Kotlin daemon
6
+ * inherits) or `-Xmx6g` beside `kotlin.daemon.jvmargs=-Xmx8g` asks for more
7
+ * than the machine has, and the kernel kills a daemon mid-build. All the log
8
+ * then says is "Gradle build daemon disappeared unexpectedly". IacobIonut01/ReFra
9
+ * and vitorpamplona/amethyst are the two measured cases; ReFra's own CI lowers
10
+ * its heap to 4g before building.
11
+ *
12
+ * Two things, both at run time, in the Gradle project directory:
13
+ *
14
+ * 1. **Cap.** Read the machine's memory and the declared `-Xmx` of the Gradle
15
+ * daemon and the Kotlin daemon. When their sum is past
16
+ * `GRADLE_HEAP_SHARE_PERCENT` of memory, scale both down to fit, and pass
17
+ * them on the command line (`-Dorg.gradle.jvmargs=`,
18
+ * `-Pkotlin.daemon.jvmargs=`). One line names the old values and the new.
19
+ * 2. **Say so.** When a Gradle run exits 137 or its output says the daemon
20
+ * disappeared, print the kernel's out-of-memory lines and one sentence
21
+ * with the machine's memory and what the build asked for.
22
+ * 3. **Workers.** A heap cap does not bound what runs beside the heaps:
23
+ * off-heap memory, and native compilers run in parallel. IacobIonut01/ReFra
24
+ * was killed again at a capped -Xmx3712m, one JVM holding ~5.1 GiB
25
+ * resident; its own CI also sets `org.gradle.workers.max=2`. A pipeline
26
+ * may ask for a worker count, passed as `--max-workers=<n>`.
27
+ *
28
+ * Precedence, measured with Gradle 8.14.3 and 9.5.0 (daemon and --no-daemon):
29
+ * `-Dorg.gradle.jvmargs` on the command line beats both the Gradle user
30
+ * home's and the project's `gradle.properties` for the daemon's heap, and
31
+ * `-P` beats `gradle.properties` for a Gradle property, which is how the
32
+ * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`. `--max-workers` beats
33
+ * `org.gradle.workers.max` in both files (`gradle.startParameter.maxWorkerCount`).
34
+ * `GRADLE_OPTS` is not used: it sizes the client JVM, not the daemon.
35
+ *
36
+ * Written as shell functions so what ships is what the tests run. Its lines
37
+ * start with "Gradle memory:", never with a step-progress marker.
38
+ */
39
+ /** The share of the machine's memory the two daemon heaps may take together. */
40
+ export const GRADLE_HEAP_SHARE_PERCENT = 75;
41
+ export const GRADLE_MEMORY_FUNCTIONS = `# This machine's memory in MiB, or nothing when it cannot be read.
42
+ cibuild_mem_mib() {
43
+ if [ -r /proc/meminfo ]; then
44
+ awk '/^MemTotal:/ { print int($2 / 1024); exit }' /proc/meminfo
45
+ else
46
+ local bytes
47
+ bytes=$(sysctl -n hw.memsize 2>/dev/null || true)
48
+ if [ -n "$bytes" ]; then echo $(( bytes / 1048576 )); fi
49
+ fi
50
+ }
51
+
52
+ # $1 a .properties file, $2 a key. Prints "<line number><TAB><value>" for the
53
+ # last assignment of the key, or nothing.
54
+ cibuild_prop_line() {
55
+ [ -f "$1" ] || return 0
56
+ awk -v key="$2" '
57
+ { line = $0; sub(/^[ \\t]+/, "", line) }
58
+ line ~ /^[#!]/ { next }
59
+ {
60
+ k = line; sub(/[ \\t]*[=:].*$/, "", k)
61
+ if (k == key) { v = line; sub(/^[^=:]*[=:][ \\t]*/, "", v); sub(/[ \\t\\r]+$/, "", v); found = NR "\\t" v }
62
+ }
63
+ END { if (found != "") print found }
64
+ ' "$1"
65
+ }
66
+
67
+ # The -Xmx of a JVM argument string in MiB (the last one wins, as in the JVM),
68
+ # or nothing when it has none.
69
+ cibuild_xmx_mib() {
70
+ printf '%s\\n' "$1" | tr ' \\t' '\\n\\n' | awk '
71
+ /^-Xmx[0-9]+[kKmMgGtT]?$/ {
72
+ n = substr($0, 5); u = substr(n, length(n))
73
+ if (u ~ /[0-9]/) { v = n / 1048576 }
74
+ else {
75
+ n = substr(n, 1, length(n) - 1)
76
+ if (u ~ /[kK]/) v = n / 1024
77
+ else if (u ~ /[mM]/) v = n
78
+ else if (u ~ /[gG]/) v = n * 1024
79
+ else v = n * 1048576
80
+ }
81
+ r = int(v)
82
+ }
83
+ END { if (r != "") print r }'
84
+ }
85
+
86
+ # $1 a JVM argument string, $2 a heap in MiB: the same arguments with every
87
+ # -Xmx replaced by that one (added in front when there was none).
88
+ cibuild_with_xmx() (
89
+ set -f
90
+ out=""
91
+ replaced=0
92
+ for arg in $1; do
93
+ case "$arg" in
94
+ -Xmx*) if [ "$replaced" -eq 0 ]; then out="$out -Xmx\${2}m"; replaced=1; fi ;;
95
+ *) out="$out $arg" ;;
96
+ esac
97
+ done
98
+ [ "$replaced" -eq 1 ] || out=" -Xmx\${2}m$out"
99
+ printf '%s' "\${out# }"
100
+ )
101
+
102
+ # MiB as GiB with one decimal.
103
+ cibuild_gib() {
104
+ awk -v m="$1" 'BEGIN { printf "%.1f", m / 1024 }'
105
+ }
106
+
107
+ # A heap scaled by budget/total, rounded down to 64 MiB, never below 512 MiB.
108
+ cibuild_scaled_heap() {
109
+ local v=$(( $1 * $2 / $3 / 64 * 64 ))
110
+ [ "$v" -ge 512 ] || v=512
111
+ echo "$v"
112
+ }
113
+
114
+ # The plan, from its inputs only, so a test can run it:
115
+ # $1 this machine's memory in MiB (empty when unknown)
116
+ # $2 the project's gradle.properties $3 the Gradle user home's
117
+ # $4 JVM arguments the pipeline asks for instead (may be empty)
118
+ # Sets CIBUILD_GRADLE_MEMORY_ARGS (extra Gradle arguments),
119
+ # CIBUILD_GRADLE_MEMORY_LINE (what to print before Gradle runs; empty when
120
+ # nothing changes) and CIBUILD_GRADLE_MEMORY_SENTENCE (what to print if a
121
+ # daemon is killed).
122
+ #
123
+ # Precedence is Gradle's own: the command line beats GRADLE_USER_HOME's
124
+ # gradle.properties, which beats the project's. A Kotlin daemon with no -Xmx of
125
+ # its own inherits the Gradle daemon's, and in-process compilation has no
126
+ # Kotlin daemon at all.
127
+ cibuild_gradle_memory_plan() {
128
+ local mem="$1" project="$2" user="$3" preferred="$4" l
129
+ local jvm="" jvm_at="" kd="" kd_at="" strategy=""
130
+ CIBUILD_GRADLE_MEMORY_ARGS=()
131
+ CIBUILD_GRADLE_MEMORY_LINE=""
132
+ CIBUILD_GRADLE_MEMORY_SENTENCE=""
133
+
134
+ if [ -n "$preferred" ]; then jvm="$preferred"; jvm_at="the pipeline"; fi
135
+ for f in "$user" "$project"; do
136
+ if [ -z "$jvm_at" ]; then
137
+ l=$(cibuild_prop_line "$f" org.gradle.jvmargs)
138
+ if [ -n "$l" ]; then jvm="\${l#* }"; jvm_at="$f:\${l%% *}"; fi
139
+ fi
140
+ if [ -z "$kd_at" ]; then
141
+ l=$(cibuild_prop_line "$f" kotlin.daemon.jvmargs)
142
+ if [ -n "$l" ]; then kd="\${l#* }"; kd_at="$f:\${l%% *}"; fi
143
+ fi
144
+ if [ -z "$strategy" ]; then
145
+ l=$(cibuild_prop_line "$f" kotlin.compiler.execution.strategy)
146
+ strategy="\${l#* }"
147
+ fi
148
+ done
149
+
150
+ local g k kd_own=""
151
+ g=$(cibuild_xmx_mib "$jvm")
152
+ [ -n "$g" ] || g=512
153
+ if [ "$strategy" = "in-process" ]; then
154
+ k=0
155
+ else
156
+ kd_own=$(cibuild_xmx_mib "$kd")
157
+ k="\${kd_own:-$g}"
158
+ fi
159
+ local total=$(( g + k ))
160
+
161
+ local asked
162
+ if [ -n "$jvm_at" ]; then asked="org.gradle.jvmargs=$jvm at \${jvm_at}"; else asked="Gradle's default -Xmx512m"; fi
163
+ if [ "$k" -eq 0 ]; then
164
+ asked="$asked; Kotlin compiles in-process"
165
+ elif [ -n "$kd_own" ]; then
166
+ asked="$asked, kotlin.daemon.jvmargs=$kd at \${kd_at}"
167
+ else
168
+ asked="$asked; the Kotlin daemon inherits its -Xmx"
169
+ fi
170
+
171
+ if [ -z "$mem" ]; then
172
+ CIBUILD_GRADLE_MEMORY_SENTENCE="This build's Gradle and Kotlin daemons asked for $(cibuild_gib "$total") GiB ($asked); this machine's memory could not be read."
173
+ if [ -n "$preferred" ]; then CIBUILD_GRADLE_MEMORY_ARGS=("-Dorg.gradle.jvmargs=$preferred"); fi
174
+ return 0
175
+ fi
176
+
177
+ local budget=$(( mem * ${GRADLE_HEAP_SHARE_PERCENT} / 100 ))
178
+ CIBUILD_GRADLE_MEMORY_SENTENCE="The build machine has $(cibuild_gib "$mem") GiB; this build's Gradle and Kotlin daemons asked for $(cibuild_gib "$total") GiB ($asked)."
179
+
180
+ if [ "$total" -le "$budget" ]; then
181
+ if [ -n "$preferred" ]; then
182
+ CIBUILD_GRADLE_MEMORY_ARGS=("-Dorg.gradle.jvmargs=$preferred")
183
+ CIBUILD_GRADLE_MEMORY_LINE="Gradle memory: org.gradle.jvmargs=$preferred, as the pipeline asks (-Dorg.gradle.jvmargs on the command line)."
184
+ fi
185
+ return 0
186
+ fi
187
+
188
+ case "$jvm" in
189
+ *\\\\)
190
+ CIBUILD_GRADLE_MEMORY_LINE="Gradle memory: the declared heaps ask for $(cibuild_gib "$total") GiB of this machine's $(cibuild_gib "$mem") GiB, but org.gradle.jvmargs at \${jvm_at} continues onto another line, so it is left as it is."
191
+ return 0
192
+ ;;
193
+ esac
194
+
195
+ local ng nk new_jvm
196
+ ng=$(cibuild_scaled_heap "$g" "$budget" "$total")
197
+ new_jvm=$(cibuild_with_xmx "$jvm" "$ng")
198
+ CIBUILD_GRADLE_MEMORY_ARGS=("-Dorg.gradle.jvmargs=$new_jvm")
199
+ local now="Gradle -Xmx\${ng}m"
200
+ if [ "$k" -eq 0 ]; then
201
+ :
202
+ elif [ -n "$kd_own" ]; then
203
+ nk=$(cibuild_scaled_heap "$k" "$budget" "$total")
204
+ CIBUILD_GRADLE_MEMORY_ARGS+=("-Pkotlin.daemon.jvmargs=$(cibuild_with_xmx "$kd" "$nk")")
205
+ now="$now and the Kotlin daemon -Xmx\${nk}m"
206
+ else
207
+ now="$now, which the Kotlin daemon inherits"
208
+ fi
209
+ CIBUILD_GRADLE_MEMORY_LINE="Gradle memory: this machine has $(cibuild_gib "$mem") GiB and the declared heaps ask for $(cibuild_gib "$total") GiB ($asked), past the $(cibuild_gib "$budget") GiB this build allows them. Running with $now, set on the command line."
210
+ CIBUILD_GRADLE_MEMORY_SENTENCE="$CIBUILD_GRADLE_MEMORY_SENTENCE This build ran them capped: $now."
211
+ }
212
+
213
+ # $1 the number of Gradle workers the pipeline asks for (may be empty). Adds
214
+ # --max-workers to the plan's arguments; run after cibuild_gradle_memory_plan.
215
+ # Sets CIBUILD_GRADLE_WORKERS_LINE (what to print; empty when nothing asked).
216
+ cibuild_gradle_workers_plan() {
217
+ CIBUILD_GRADLE_WORKERS_LINE=""
218
+ [ -n "$1" ] || return 0
219
+ case "$1" in
220
+ *[!0-9]*|0*)
221
+ CIBUILD_GRADLE_WORKERS_LINE="Gradle memory: max_workers=$1 is not a positive whole number, so it is not passed and Gradle picks its own worker count."
222
+ return 0
223
+ ;;
224
+ esac
225
+ CIBUILD_GRADLE_MEMORY_ARGS+=("--max-workers=$1")
226
+ CIBUILD_GRADLE_WORKERS_LINE="Gradle memory: running with --max-workers=$1, as the pipeline asks (set on the command line, over any org.gradle.workers.max)."
227
+ CIBUILD_GRADLE_MEMORY_SENTENCE="$CIBUILD_GRADLE_MEMORY_SENTENCE Gradle ran with --max-workers=$1."
228
+ }
229
+
230
+ # The kernel's out-of-memory lines, if this user may read the kernel log.
231
+ # Linux guests restrict dmesg to root by default; their build user has
232
+ # non-interactive sudo, so that is the second try.
233
+ cibuild_kernel_oom_lines() {
234
+ { dmesg 2>/dev/null || sudo -n dmesg 2>/dev/null || true; } |
235
+ grep -i -E 'out of memory|oom-kill|oom_reaper|killed process' | tail -n 20 || true
236
+ }
237
+
238
+ # $1 Gradle's exit status, $2 a file holding its output. Says so when the
239
+ # failure is a killed daemon rather than a failed build.
240
+ cibuild_gradle_memory_report() {
241
+ if [ "$1" -ne 137 ] && ! grep -q "daemon disappeared unexpectedly" "$2" 2>/dev/null; then
242
+ return 0
243
+ fi
244
+ local oom
245
+ oom=$(cibuild_kernel_oom_lines)
246
+ echo ""
247
+ echo "Gradle memory: a Gradle daemon was killed rather than failing the build (exit $1)."
248
+ if [ -n "$oom" ]; then
249
+ echo "The kernel's out-of-memory lines:"
250
+ printf '%s\\n' "$oom" | sed 's/^/ /'
251
+ else
252
+ echo "The kernel log shows no out-of-memory kill, or this machine does not let the build read it."
253
+ fi
254
+ if [ -n "$CIBUILD_GRADLE_MEMORY_SENTENCE" ]; then echo "$CIBUILD_GRADLE_MEMORY_SENTENCE"; fi
255
+ }
256
+
257
+ # Runs Gradle with the plan's arguments, and reports a killed daemon.
258
+ cibuild_gradle() {
259
+ local log rc
260
+ log=$(mktemp)
261
+ set +e
262
+ "$CIBUILD_GRADLE_BIN" "\${CIBUILD_GRADLE_MEMORY_ARGS[@]}" "$@" 2>&1 | tee "$log"
263
+ rc=\${PIPESTATUS[0]}
264
+ set -e
265
+ if [ "$rc" -ne 0 ]; then cibuild_gradle_memory_report "$rc" "$log" || true; fi
266
+ rm -f "$log"
267
+ return "$rc"
268
+ }
269
+
270
+ # $1 JVM arguments the pipeline asks for, $2 the Gradle workers it asks for
271
+ # (either may be empty). Run in the Gradle project directory, after GRADLE_CMD
272
+ # is chosen.
273
+ cibuild_gradle_memory_cap() {
274
+ cibuild_gradle_memory_plan "$(cibuild_mem_mib || true)" "gradle.properties" \\
275
+ "\${GRADLE_USER_HOME:-$HOME/.gradle}/gradle.properties" "$1"
276
+ cibuild_gradle_workers_plan "$2"
277
+ if [ -n "$CIBUILD_GRADLE_MEMORY_LINE" ]; then echo "$CIBUILD_GRADLE_MEMORY_LINE"; fi
278
+ if [ -n "$CIBUILD_GRADLE_WORKERS_LINE" ]; then echo "$CIBUILD_GRADLE_WORKERS_LINE"; fi
279
+ CIBUILD_GRADLE_BIN="$GRADLE_CMD"
280
+ GRADLE_CMD=cibuild_gradle
281
+ }`;
282
+ /**
283
+ * The commands a Gradle step runs once it has chosen `GRADLE_CMD` and is in
284
+ * the project directory. Afterwards `$GRADLE_CMD` runs Gradle with the plan's
285
+ * arguments and reports a killed daemon.
286
+ *
287
+ * `jvmargs` is what the pipeline asks for instead of the project's
288
+ * `org.gradle.jvmargs` (a value the repository's own CI sets, say); the cap
289
+ * still applies to it. `maxWorkers` is the worker count it asks for (the
290
+ * repository CI's `org.gradle.workers.max`), passed as `--max-workers`.
291
+ */
292
+ export function gradleMemoryCommands(jvmargs, escape, maxWorkers = '') {
293
+ return [
294
+ '',
295
+ GRADLE_MEMORY_FUNCTIONS,
296
+ '',
297
+ `cibuild_gradle_memory_cap '${escape(jvmargs)}' '${escape(maxWorkers)}'`,
298
+ ];
299
+ }
300
+ //# sourceMappingURL=gradle-memory.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gradle-memory.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradle-memory.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/gradle-memory.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,275 @@
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
+ /** The plan, then the workers plan, as the step runs them. */
128
+ function workersPlan(memMib, maxWorkers) {
129
+ const script = `${GRADLE_MEMORY_FUNCTIONS}
130
+ cibuild_gradle_memory_plan "$1" gradle.properties "$2/gradle.properties" ""
131
+ cibuild_gradle_workers_plan "$3"
132
+ for a in "\${CIBUILD_GRADLE_MEMORY_ARGS[@]}"; do printf 'ARG %s\\n' "$a"; done
133
+ printf 'LINE %s\\n' "$CIBUILD_GRADLE_WORKERS_LINE"
134
+ printf 'SENTENCE %s\\n' "$CIBUILD_GRADLE_MEMORY_SENTENCE"`;
135
+ const out = execFileSync("bash", ["-c", script, "plan", memMib, join(dir, "no-user-home"), maxWorkers], {
136
+ cwd: dir,
137
+ encoding: "utf-8",
138
+ });
139
+ const lines = out.split("\n");
140
+ return {
141
+ args: lines.filter((l) => l.startsWith("ARG ")).map((l) => l.slice(4)),
142
+ line: (lines.find((l) => l.startsWith("LINE ")) ?? "").slice(5),
143
+ sentence: (lines.find((l) => l.startsWith("SENTENCE ")) ?? "").slice(9),
144
+ };
145
+ }
146
+ // ReFra was killed again with its heap capped: off-heap memory and parallel
147
+ // native compilers are past what a heap cap bounds. Its CI also sets
148
+ // org.gradle.workers.max=2, which the pipeline carries as max_workers.
149
+ describe("cibuild_gradle_workers_plan", () => {
150
+ test("ReFra: the worker count goes after the heap cap, and the sentence names it", () => {
151
+ copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
152
+ const p = workersPlan(TEN_GIB, "2");
153
+ expect(p.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3840m -Dfile.encoding=UTF-8", "--max-workers=2"]);
154
+ expect(p.line).toBe("Gradle memory: running with --max-workers=2, as the pipeline asks (set on the command line, over any org.gradle.workers.max).");
155
+ expect(p.sentence).toMatch(/This build ran them capped: Gradle -Xmx3840m, .* Gradle ran with --max-workers=2\.$/);
156
+ });
157
+ test("heaps that fit still get the worker count", () => {
158
+ const p = workersPlan(TEN_GIB, "4");
159
+ expect(p.args).toEqual(["--max-workers=4"]);
160
+ });
161
+ test("nothing asked, nothing passed or printed", () => {
162
+ const p = workersPlan(TEN_GIB, "");
163
+ expect(p.args).toEqual([]);
164
+ expect(p.line).toBe("");
165
+ expect(p.sentence).not.toContain("max-workers");
166
+ });
167
+ test.each(["0", "02", "-1", "two", "2 --offline"])("%s is not a worker count, and says so", (value) => {
168
+ const p = workersPlan(TEN_GIB, value);
169
+ expect(p.args).toEqual([]);
170
+ expect(p.line).toBe(`Gradle memory: max_workers=${value} is not a positive whole number, so it is not passed and Gradle picks its own worker count.`);
171
+ });
172
+ });
173
+ /** A directory standing in for PATH, with a fake `dmesg` that prints `oom`. */
174
+ function fakeBin(oom) {
175
+ const bin = join(dir, "bin");
176
+ mkdirSync(bin, { recursive: true });
177
+ writeFileSync(join(bin, "dmesg"), `#!/bin/bash\nprintf '%s\\n' ${JSON.stringify(oom)}\n`);
178
+ chmodSync(join(bin, "dmesg"), 0o755);
179
+ return bin;
180
+ }
181
+ /** A `./gradlew` that records its arguments and fails the way it is told. */
182
+ function fakeGradlew(body) {
183
+ writeFileSync(join(dir, "gradlew"), `#!/bin/bash\nprintf '%s\\n' "$@" > "$(dirname "$0")/gradlew-args"\n${body}\n`);
184
+ chmodSync(join(dir, "gradlew"), 0o755);
185
+ }
186
+ function runStep(script, bin) {
187
+ writeFileSync(join(dir, "step.sh"), script);
188
+ return spawnSync("bash", [join(dir, "step.sh")], {
189
+ cwd: dir,
190
+ encoding: "utf-8",
191
+ env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, JAVA_HOME: "", CIBUILD_SOURCE_DIR: "" },
192
+ });
193
+ }
194
+ describe("the Gradle steps", () => {
195
+ const OOM_LINE = "[ 412.9] Out of memory: Killed process 4242 (java) total-vm:14680064kB, anon-rss:9437184kB";
196
+ test("a killed daemon prints the kernel's out-of-memory lines and the sentence, and still fails", async () => {
197
+ copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
198
+ 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');
199
+ const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug" }, {}, testConfig);
200
+ const run = runStep(step.script, fakeBin(OOM_LINE));
201
+ expect(run.status).toBe(1);
202
+ expect(run.stdout).toContain("Gradle memory: a Gradle daemon was killed rather than failing the build (exit 1).");
203
+ expect(run.stdout).toContain(` ${OOM_LINE}`);
204
+ expect(run.stdout).toMatch(/The build machine has [0-9.]+ GiB; this build's Gradle and Kotlin daemons asked for 16\.0 GiB/);
205
+ expect(run.stdout).not.toContain("Gradle build completed successfully");
206
+ });
207
+ test("an ordinary failure says nothing about memory", async () => {
208
+ fakeGradlew('echo "Compilation error"\nexit 1');
209
+ const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug" }, {}, testConfig);
210
+ const run = runStep(step.script, fakeBin(OOM_LINE));
211
+ expect(run.status).toBe(1);
212
+ expect(run.stdout).not.toContain("Gradle memory:");
213
+ });
214
+ test("the pipeline's jvmargs reach Gradle as one argument, before the task", async () => {
215
+ fakeGradlew("exit 0");
216
+ const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug", gradle_options: "--stacktrace", jvmargs: "-Xmx256m -Dfile.encoding=UTF-8" }, {}, testConfig);
217
+ const run = runStep(step.script, fakeBin(""));
218
+ expect(run.status).toBe(0);
219
+ const args = execFileSync("cat", [join(dir, "gradlew-args")], { encoding: "utf-8" }).trim().split("\n");
220
+ expect(args).toEqual(["-Dorg.gradle.jvmargs=-Xmx256m -Dfile.encoding=UTF-8", "assembleDebug", "--stacktrace"]);
221
+ });
222
+ test("the pipeline's max_workers reach Gradle after the heap arguments, before the task", async () => {
223
+ fakeGradlew("exit 0");
224
+ const step = await new GradleBuildStepExecutor().execute(
225
+ // YAML reads `max_workers: 2` as a number.
226
+ { gradle_task: "assembleDebug", gradle_options: "--stacktrace", jvmargs: "-Xmx256m", max_workers: 2 }, {}, testConfig);
227
+ const run = runStep(step.script, fakeBin(""));
228
+ expect(run.status).toBe(0);
229
+ expect(run.stdout).toContain("Gradle memory: running with --max-workers=2, as the pipeline asks");
230
+ const args = execFileSync("cat", [join(dir, "gradlew-args")], { encoding: "utf-8" }).trim().split("\n");
231
+ expect(args).toEqual(["-Dorg.gradle.jvmargs=-Xmx256m", "--max-workers=2", "assembleDebug", "--stacktrace"]);
232
+ });
233
+ test("a killed daemon's sentence names the worker count it ran with", async () => {
234
+ copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
235
+ fakeGradlew('echo "Gradle build daemon disappeared unexpectedly"\nexit 1');
236
+ const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug", max_workers: "2" }, {}, testConfig);
237
+ const run = runStep(step.script, fakeBin(OOM_LINE));
238
+ expect(run.status).toBe(1);
239
+ // The test machine's own memory decides whether a cap applied, so only the
240
+ // sentence's end is asserted.
241
+ expect(run.stdout).toMatch(/^The build machine has .*asked for 16\.0 GiB .* Gradle ran with --max-workers=2\.$/m);
242
+ });
243
+ test("every Gradle step passes max_workers to the plan", async () => {
244
+ const steps = [
245
+ await new GradleBuildStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
246
+ await new AndroidLintStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
247
+ await new AndroidUnitTestStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
248
+ await new AndroidBuildForUITestingStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
249
+ ];
250
+ for (const step of steps) {
251
+ expect(step.script).toContain("cibuild_gradle_memory_cap '' '3'");
252
+ }
253
+ });
254
+ test("every Gradle step runs through the plan, and every one is valid bash", async () => {
255
+ const steps = [
256
+ await new GradleBuildStepExecutor().execute({}, {}, testConfig),
257
+ await new AndroidLintStepExecutor().execute({}, {}, testConfig),
258
+ await new AndroidUnitTestStepExecutor().execute({}, {}, testConfig),
259
+ await new AndroidBuildForUITestingStepExecutor().execute({}, {}, testConfig),
260
+ ];
261
+ for (const step of steps) {
262
+ const script = step.script;
263
+ expect(script).toContain("cibuild_gradle_memory_cap '' ''");
264
+ execFileSync("bash", ["-n"], { input: script });
265
+ }
266
+ });
267
+ // cibuild's log is parsed for step-progress markers; nothing here may look
268
+ // like one.
269
+ test("no line the functions print can be taken for a step-progress marker", () => {
270
+ for (const line of GRADLE_MEMORY_FUNCTIONS.split("\n")) {
271
+ expect(line).not.toMatch(/INVARN_(STEP|PHASE)|▸/);
272
+ }
273
+ });
274
+ });
275
+ //# sourceMappingURL=gradle-memory.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.8.5",
3
+ "version": "2.8.7",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",