@invarn/cibuild 2.8.8 → 2.9.0

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.
@@ -70,11 +70,24 @@ fi`);
70
70
  executable(join(bin, 'bundle'), `echo "bundle $* (GEM_HOME=$GEM_HOME)" >> '${log}'`);
71
71
  executable(join(bin, 'pod'), `echo "pod $*" >> '${log}'`);
72
72
  executable(join(bin, 'ruby'), `echo "ruby $*" >> '${log}'; printf '2.6.10'`);
73
- if (opts.brewRuby) {
73
+ if (opts.brewRuby || opts.ruby33) {
74
+ const brewVersion = opts.brewRubyVersion ?? '3.4.1';
74
75
  const keg = join(root, 'homebrew-ruby');
75
- mkdirSync(join(keg, 'bin'), { recursive: true });
76
- executable(join(keg, 'bin', 'ruby'), `echo "brew-ruby $*" >> '${log}'; printf '3.4.1'`);
77
- executable(join(bin, 'brew'), `if [ "$*" = "--prefix --installed ruby" ]; then echo '${keg}'; exit 0; fi; exit 1`);
76
+ const keg33 = join(root, 'homebrew-ruby@3.3');
77
+ if (opts.brewRuby) {
78
+ mkdirSync(join(keg, 'bin'), { recursive: true });
79
+ executable(join(keg, 'bin', 'ruby'), `echo "brew-ruby $*" >> '${log}'; printf '${brewVersion}'`);
80
+ }
81
+ if (opts.ruby33) {
82
+ mkdirSync(join(keg33, 'bin'), { recursive: true });
83
+ executable(join(keg33, 'bin', 'ruby'), `echo "ruby33 $*" >> '${log}'; printf '3.3.10'`);
84
+ }
85
+ executable(join(bin, 'brew'), [
86
+ `if [ "$*" = "--prefix --installed ruby" ]; then ${opts.brewRuby ? `echo '${keg}'; exit 0` : 'exit 1'}; fi`,
87
+ `if [ "$*" = "list --versions ruby" ]; then ${opts.brewRuby ? `echo 'ruby ${brewVersion}'; exit 0` : 'exit 1'}; fi`,
88
+ `if [ "$*" = "--prefix --installed ruby@3.3" ]; then ${opts.ruby33 ? `echo '${keg33}'; exit 0` : 'exit 1'}; fi`,
89
+ 'exit 1',
90
+ ].join('\n'));
78
91
  }
79
92
  const script = (await new CocoapodsInstallStepExecutor().execute({ source_root_path: project }, {}, testConfig)).script;
80
93
  const scriptPath = join(root, 'step.sh');
@@ -154,7 +167,7 @@ describe('cocoapods-install runs the Ruby the project names', () => {
154
167
  expect(usedBrew(calls)).toBe(false);
155
168
  expect(calls).toContain('gem install bundler -v 2.3.21 --no-document');
156
169
  expect(calls.find((c) => c.startsWith('bundle _2.3.21_ install'))).toContain(`GEM_HOME=${gemHome}`);
157
- expect(output).toMatch(/^Ruby: .*\(2\.6\.10\), GEM_HOME: .* — system: lockfile names no Ruby$/m);
170
+ expect(output).toMatch(/^Ruby: .*\(2\.6\.10\), GEM_HOME: .* — system: lockfile names no Ruby, and no Ruby 3\.3 is installed$/m);
158
171
  });
159
172
  test('a lock whose RUBY VERSION is 3 or newer runs Homebrew Ruby', async () => {
160
173
  const { calls, output } = await run({ lock: lockWithRuby('3.3.4p94'), brewRuby: true });
@@ -204,4 +217,66 @@ describe('cocoapods-install runs the Ruby the project names', () => {
204
217
  expect(usedSystem(calls)).toBe(false);
205
218
  });
206
219
  });
220
+ // With the lock naming no Ruby, the step chose system Ruby 2.6 as above, and the build still died:
221
+ // the Xcode 26.4 SDK's Ruby 2.6 headers have no `ruby/config.h`, so no native extension (json, ffi)
222
+ // compiles. Homebrew's Ruby is 4, where the lock's rake 13.0.6 cannot load. So a Ruby 3.3 runs when
223
+ // the lock names no Ruby or one below 3; a named 3.x runs 3.3 unless Homebrew's matches it better; a
224
+ // named 4.x runs Homebrew's. A machine without 3.3 behaves as before.
225
+ describe('cocoapods-install runs Ruby 3.3 where system Ruby cannot build native gems', () => {
226
+ const used33 = (calls) => calls.some((c) => c.startsWith('ruby33'));
227
+ const usedBrew = (calls) => calls.some((c) => c.startsWith('brew-ruby'));
228
+ const usedSystem = (calls) => calls.some((c) => c.startsWith('ruby '));
229
+ const lockWithRuby = (v) => LOCK_WITH_BUNDLER.replace('BUNDLED WITH', `RUBY VERSION\n ruby ${v}\n\nBUNDLED WITH`);
230
+ const image = { brewRuby: true, brewRubyVersion: '4.0.2', ruby33: true };
231
+ test('a lock that names no Ruby runs 3.3, with the locked bundler', async () => {
232
+ const { calls, output, gemHome } = await run({ lock: LOCK_WITH_BUNDLER, ...image });
233
+ expect(used33(calls)).toBe(true);
234
+ expect(usedSystem(calls)).toBe(false);
235
+ expect(usedBrew(calls)).toBe(false);
236
+ expect(calls).toContain('gem install bundler -v 2.3.21 --no-document');
237
+ expect(calls.find((c) => c.startsWith('bundle _2.3.21_ install'))).toContain(`GEM_HOME=${gemHome}`);
238
+ expect(output).toMatch(/^Ruby: .*homebrew-ruby@3\.3\/bin\/ruby \(3\.3\.10\), GEM_HOME: .* — Ruby 3\.3: lockfile names no Ruby, and system Ruby 2\.6 cannot build native gems$/m);
239
+ });
240
+ test('a lock whose RUBY VERSION is 2.x runs 3.3', async () => {
241
+ const { calls, output } = await run({ lock: lockWithRuby('2.7.2p137'), ...image });
242
+ expect(used33(calls)).toBe(true);
243
+ expect(output).toContain('— from Gemfile.lock RUBY VERSION 2.7.2, run on Ruby 3.3: system Ruby 2.6 cannot build native gems');
244
+ });
245
+ test('a 2.x .ruby-version runs 3.3', async () => {
246
+ const { calls } = await run({ lock: LOCK_WITH_BUNDLER, rubyVersionFile: '2.7.6\n', ...image });
247
+ expect(used33(calls)).toBe(true);
248
+ });
249
+ test('a named 3.x that Homebrew does not match runs 3.3', async () => {
250
+ const { calls, output } = await run({ lock: lockWithRuby('3.2.2'), ...image });
251
+ expect(used33(calls)).toBe(true);
252
+ expect(usedBrew(calls)).toBe(false);
253
+ expect(output).toContain('— from Gemfile.lock RUBY VERSION 3.2.2, run on Ruby 3.3');
254
+ });
255
+ test('a named 3.3 runs 3.3 and needs no aside', async () => {
256
+ const { calls, output } = await run({ lock: lockWithRuby('3.3.4p94'), ...image });
257
+ expect(used33(calls)).toBe(true);
258
+ expect(output).toMatch(/— from Gemfile\.lock RUBY VERSION 3\.3\.4$/m);
259
+ });
260
+ test('a named 3.x that Homebrew matches runs Homebrew Ruby', async () => {
261
+ const { calls, output } = await run({ lock: lockWithRuby('3.4.1'), brewRuby: true, brewRubyVersion: '3.4.7', ruby33: true });
262
+ expect(usedBrew(calls)).toBe(true);
263
+ expect(used33(calls)).toBe(false);
264
+ expect(output).toMatch(/— from Gemfile\.lock RUBY VERSION 3\.4\.1$/m);
265
+ });
266
+ test('a named 4.x runs Homebrew Ruby', async () => {
267
+ const { calls } = await run({ lock: lockWithRuby('4.0.1'), ...image });
268
+ expect(usedBrew(calls)).toBe(true);
269
+ expect(used33(calls)).toBe(false);
270
+ });
271
+ test('without 3.3, a lock that names no Ruby runs system Ruby, and says 3.3 is missing', async () => {
272
+ const { calls, output } = await run({ lock: LOCK_WITH_BUNDLER, brewRuby: true, brewRubyVersion: '4.0.2' });
273
+ expect(usedSystem(calls)).toBe(true);
274
+ expect(output).toContain('— system: lockfile names no Ruby, and no Ruby 3.3 is installed');
275
+ });
276
+ test('without 3.3, a 2.x lock runs system Ruby, and says 3.3 is missing', async () => {
277
+ const { calls, output } = await run({ lock: lockWithRuby('2.7.2p137'), brewRuby: true });
278
+ expect(usedSystem(calls)).toBe(true);
279
+ expect(output).toContain('— from Gemfile.lock RUBY VERSION 2.7.2, and no Ruby 3.3 is installed');
280
+ });
281
+ });
207
282
  //# sourceMappingURL=cocoapods-locked-bundler.test.js.map
@@ -16,6 +16,9 @@
16
16
  * `GRADLE_HEAP_SHARE_PERCENT` of memory, scale both down to fit, and pass
17
17
  * them on the command line (`-Dorg.gradle.jvmargs=`,
18
18
  * `-Pkotlin.daemon.jvmargs=`). One line names the old values and the new.
19
+ * A Kotlin daemon that would inherit Gradle's -Xmx is given its own
20
+ * instead, the budget split Kotlin 60 / Gradle 40: at an even split
21
+ * (-Xmx3712m each) ReFra's Kotlin compile ran out of heap.
19
22
  * 2. **Say so.** When a Gradle run exits 137 or its output says the daemon
20
23
  * disappeared, print the kernel's out-of-memory lines and one sentence
21
24
  * with the machine's memory and what the build asked for.
@@ -29,7 +32,9 @@
29
32
  * `-Dorg.gradle.jvmargs` on the command line beats both the Gradle user
30
33
  * home's and the project's `gradle.properties` for the daemon's heap, and
31
34
  * `-P` beats `gradle.properties` for a Gradle property, which is how the
32
- * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`. `--max-workers` beats
35
+ * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`; where the project
36
+ * declares none, `-Pkotlin.daemon.jvmargs=-Xmx<n>` gives the Kotlin daemon
37
+ * that heap instead of the Gradle daemon's. `--max-workers` beats
33
38
  * `org.gradle.workers.max` in both files (`gradle.startParameter.maxWorkerCount`).
34
39
  * `GRADLE_OPTS` is not used: it sizes the client JVM, not the daemon.
35
40
  *
@@ -38,7 +43,13 @@
38
43
  */
39
44
  /** The share of the machine's memory the two daemon heaps may take together. */
40
45
  export declare const GRADLE_HEAP_SHARE_PERCENT = 75;
41
- export declare const GRADLE_MEMORY_FUNCTIONS = "# This machine's memory in MiB, or nothing when it cannot be read.\ncibuild_mem_mib() {\n if [ -r /proc/meminfo ]; then\n awk '/^MemTotal:/ { print int($2 / 1024); exit }' /proc/meminfo\n else\n local bytes\n bytes=$(sysctl -n hw.memsize 2>/dev/null || true)\n if [ -n \"$bytes\" ]; then echo $(( bytes / 1048576 )); fi\n fi\n}\n\n# $1 a .properties file, $2 a key. Prints \"<line number><TAB><value>\" for the\n# last assignment of the key, or nothing.\ncibuild_prop_line() {\n [ -f \"$1\" ] || return 0\n awk -v key=\"$2\" '\n { line = $0; sub(/^[ \\t]+/, \"\", line) }\n line ~ /^[#!]/ { next }\n {\n k = line; sub(/[ \\t]*[=:].*$/, \"\", k)\n if (k == key) { v = line; sub(/^[^=:]*[=:][ \\t]*/, \"\", v); sub(/[ \\t\\r]+$/, \"\", v); found = NR \"\\t\" v }\n }\n END { if (found != \"\") print found }\n ' \"$1\"\n}\n\n# The -Xmx of a JVM argument string in MiB (the last one wins, as in the JVM),\n# or nothing when it has none.\ncibuild_xmx_mib() {\n printf '%s\\n' \"$1\" | tr ' \\t' '\\n\\n' | awk '\n /^-Xmx[0-9]+[kKmMgGtT]?$/ {\n n = substr($0, 5); u = substr(n, length(n))\n if (u ~ /[0-9]/) { v = n / 1048576 }\n else {\n n = substr(n, 1, length(n) - 1)\n if (u ~ /[kK]/) v = n / 1024\n else if (u ~ /[mM]/) v = n\n else if (u ~ /[gG]/) v = n * 1024\n else v = n * 1048576\n }\n r = int(v)\n }\n END { if (r != \"\") print r }'\n}\n\n# $1 a JVM argument string, $2 a heap in MiB: the same arguments with every\n# -Xmx replaced by that one (added in front when there was none).\ncibuild_with_xmx() (\n set -f\n out=\"\"\n replaced=0\n for arg in $1; do\n case \"$arg\" in\n -Xmx*) if [ \"$replaced\" -eq 0 ]; then out=\"$out -Xmx${2}m\"; replaced=1; fi ;;\n *) out=\"$out $arg\" ;;\n esac\n done\n [ \"$replaced\" -eq 1 ] || out=\" -Xmx${2}m$out\"\n printf '%s' \"${out# }\"\n)\n\n# MiB as GiB with one decimal.\ncibuild_gib() {\n awk -v m=\"$1\" 'BEGIN { printf \"%.1f\", m / 1024 }'\n}\n\n# A heap scaled by budget/total, rounded down to 64 MiB, never below 512 MiB.\ncibuild_scaled_heap() {\n local v=$(( $1 * $2 / $3 / 64 * 64 ))\n [ \"$v\" -ge 512 ] || v=512\n echo \"$v\"\n}\n\n# The plan, from its inputs only, so a test can run it:\n# $1 this machine's memory in MiB (empty when unknown)\n# $2 the project's gradle.properties $3 the Gradle user home's\n# $4 JVM arguments the pipeline asks for instead (may be empty)\n# Sets CIBUILD_GRADLE_MEMORY_ARGS (extra Gradle arguments),\n# CIBUILD_GRADLE_MEMORY_LINE (what to print before Gradle runs; empty when\n# nothing changes) and CIBUILD_GRADLE_MEMORY_SENTENCE (what to print if a\n# daemon is killed).\n#\n# Precedence is Gradle's own: the command line beats GRADLE_USER_HOME's\n# gradle.properties, which beats the project's. A Kotlin daemon with no -Xmx of\n# its own inherits the Gradle daemon's, and in-process compilation has no\n# Kotlin daemon at all.\ncibuild_gradle_memory_plan() {\n local mem=\"$1\" project=\"$2\" user=\"$3\" preferred=\"$4\" l\n local jvm=\"\" jvm_at=\"\" kd=\"\" kd_at=\"\" strategy=\"\"\n CIBUILD_GRADLE_MEMORY_ARGS=()\n CIBUILD_GRADLE_MEMORY_LINE=\"\"\n CIBUILD_GRADLE_MEMORY_SENTENCE=\"\"\n\n if [ -n \"$preferred\" ]; then jvm=\"$preferred\"; jvm_at=\"the pipeline\"; fi\n for f in \"$user\" \"$project\"; do\n if [ -z \"$jvm_at\" ]; then\n l=$(cibuild_prop_line \"$f\" org.gradle.jvmargs)\n if [ -n \"$l\" ]; then jvm=\"${l#*\t}\"; jvm_at=\"$f:${l%%\t*}\"; fi\n fi\n if [ -z \"$kd_at\" ]; then\n l=$(cibuild_prop_line \"$f\" kotlin.daemon.jvmargs)\n if [ -n \"$l\" ]; then kd=\"${l#*\t}\"; kd_at=\"$f:${l%%\t*}\"; fi\n fi\n if [ -z \"$strategy\" ]; then\n l=$(cibuild_prop_line \"$f\" kotlin.compiler.execution.strategy)\n strategy=\"${l#*\t}\"\n fi\n done\n\n local g k kd_own=\"\"\n g=$(cibuild_xmx_mib \"$jvm\")\n [ -n \"$g\" ] || g=512\n if [ \"$strategy\" = \"in-process\" ]; then\n k=0\n else\n kd_own=$(cibuild_xmx_mib \"$kd\")\n k=\"${kd_own:-$g}\"\n fi\n local total=$(( g + k ))\n\n local asked\n if [ -n \"$jvm_at\" ]; then asked=\"org.gradle.jvmargs=$jvm at ${jvm_at}\"; else asked=\"Gradle's default -Xmx512m\"; fi\n if [ \"$k\" -eq 0 ]; then\n asked=\"$asked; Kotlin compiles in-process\"\n elif [ -n \"$kd_own\" ]; then\n asked=\"$asked, kotlin.daemon.jvmargs=$kd at ${kd_at}\"\n else\n asked=\"$asked; the Kotlin daemon inherits its -Xmx\"\n fi\n\n if [ -z \"$mem\" ]; then\n 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.\"\n if [ -n \"$preferred\" ]; then CIBUILD_GRADLE_MEMORY_ARGS=(\"-Dorg.gradle.jvmargs=$preferred\"); fi\n return 0\n fi\n\n local budget=$(( mem * 75 / 100 ))\n 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).\"\n\n if [ \"$total\" -le \"$budget\" ]; then\n if [ -n \"$preferred\" ]; then\n CIBUILD_GRADLE_MEMORY_ARGS=(\"-Dorg.gradle.jvmargs=$preferred\")\n CIBUILD_GRADLE_MEMORY_LINE=\"Gradle memory: org.gradle.jvmargs=$preferred, as the pipeline asks (-Dorg.gradle.jvmargs on the command line).\"\n fi\n return 0\n fi\n\n case \"$jvm\" in\n *\\\\)\n 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.\"\n return 0\n ;;\n esac\n\n local ng nk new_jvm\n ng=$(cibuild_scaled_heap \"$g\" \"$budget\" \"$total\")\n new_jvm=$(cibuild_with_xmx \"$jvm\" \"$ng\")\n CIBUILD_GRADLE_MEMORY_ARGS=(\"-Dorg.gradle.jvmargs=$new_jvm\")\n local now=\"Gradle -Xmx${ng}m\"\n if [ \"$k\" -eq 0 ]; then\n :\n elif [ -n \"$kd_own\" ]; then\n nk=$(cibuild_scaled_heap \"$k\" \"$budget\" \"$total\")\n CIBUILD_GRADLE_MEMORY_ARGS+=(\"-Pkotlin.daemon.jvmargs=$(cibuild_with_xmx \"$kd\" \"$nk\")\")\n now=\"$now and the Kotlin daemon -Xmx${nk}m\"\n else\n now=\"$now, which the Kotlin daemon inherits\"\n fi\n 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.\"\n CIBUILD_GRADLE_MEMORY_SENTENCE=\"$CIBUILD_GRADLE_MEMORY_SENTENCE This build ran them capped: $now.\"\n}\n\n# $1 the number of Gradle workers the pipeline asks for (may be empty). Adds\n# --max-workers to the plan's arguments; run after cibuild_gradle_memory_plan.\n# Sets CIBUILD_GRADLE_WORKERS_LINE (what to print; empty when nothing asked).\ncibuild_gradle_workers_plan() {\n CIBUILD_GRADLE_WORKERS_LINE=\"\"\n [ -n \"$1\" ] || return 0\n case \"$1\" in\n *[!0-9]*|0*)\n 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.\"\n return 0\n ;;\n esac\n CIBUILD_GRADLE_MEMORY_ARGS+=(\"--max-workers=$1\")\n 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).\"\n CIBUILD_GRADLE_MEMORY_SENTENCE=\"$CIBUILD_GRADLE_MEMORY_SENTENCE Gradle ran with --max-workers=$1.\"\n}\n\n# The kernel's out-of-memory lines, if this user may read the kernel log.\n# Linux guests restrict dmesg to root by default; their build user has\n# non-interactive sudo, so that is the second try.\ncibuild_kernel_oom_lines() {\n { dmesg 2>/dev/null || sudo -n dmesg 2>/dev/null || true; } |\n grep -i -E 'out of memory|oom-kill|oom_reaper|killed process' | tail -n 20 || true\n}\n\n# $1 Gradle's exit status, $2 a file holding its output. Says so when the\n# failure is a killed daemon rather than a failed build.\ncibuild_gradle_memory_report() {\n if [ \"$1\" -ne 137 ] && ! grep -q \"daemon disappeared unexpectedly\" \"$2\" 2>/dev/null; then\n return 0\n fi\n local oom\n oom=$(cibuild_kernel_oom_lines)\n echo \"\"\n echo \"Gradle memory: a Gradle daemon was killed rather than failing the build (exit $1).\"\n if [ -n \"$oom\" ]; then\n echo \"The kernel's out-of-memory lines:\"\n printf '%s\\n' \"$oom\" | sed 's/^/ /'\n else\n echo \"The kernel log shows no out-of-memory kill, or this machine does not let the build read it.\"\n fi\n if [ -n \"$CIBUILD_GRADLE_MEMORY_SENTENCE\" ]; then echo \"$CIBUILD_GRADLE_MEMORY_SENTENCE\"; fi\n}\n\n# Runs Gradle with the plan's arguments, and reports a killed daemon.\ncibuild_gradle() {\n local log rc\n log=$(mktemp)\n set +e\n \"$CIBUILD_GRADLE_BIN\" \"${CIBUILD_GRADLE_MEMORY_ARGS[@]}\" \"$@\" 2>&1 | tee \"$log\"\n rc=${PIPESTATUS[0]}\n set -e\n if [ \"$rc\" -ne 0 ]; then cibuild_gradle_memory_report \"$rc\" \"$log\" || true; fi\n rm -f \"$log\"\n return \"$rc\"\n}\n\n# $1 JVM arguments the pipeline asks for, $2 the Gradle workers it asks for\n# (either may be empty). Run in the Gradle project directory, after GRADLE_CMD\n# is chosen.\ncibuild_gradle_memory_cap() {\n cibuild_gradle_memory_plan \"$(cibuild_mem_mib || true)\" \"gradle.properties\" \\\n \"${GRADLE_USER_HOME:-$HOME/.gradle}/gradle.properties\" \"$1\"\n cibuild_gradle_workers_plan \"$2\"\n if [ -n \"$CIBUILD_GRADLE_MEMORY_LINE\" ]; then echo \"$CIBUILD_GRADLE_MEMORY_LINE\"; fi\n if [ -n \"$CIBUILD_GRADLE_WORKERS_LINE\" ]; then echo \"$CIBUILD_GRADLE_WORKERS_LINE\"; fi\n CIBUILD_GRADLE_BIN=\"$GRADLE_CMD\"\n GRADLE_CMD=cibuild_gradle\n}";
46
+ /**
47
+ * Gradle's share of that budget when the Kotlin daemon would inherit its
48
+ * -Xmx; the Kotlin daemon gets the rest as its own. The Kotlin compile is the
49
+ * larger heap: ReFra's ran out at an even split.
50
+ */
51
+ export declare const GRADLE_HEAP_SPLIT_PERCENT = 40;
52
+ export declare const GRADLE_MEMORY_FUNCTIONS = "# This machine's memory in MiB, or nothing when it cannot be read.\ncibuild_mem_mib() {\n if [ -r /proc/meminfo ]; then\n awk '/^MemTotal:/ { print int($2 / 1024); exit }' /proc/meminfo\n else\n local bytes\n bytes=$(sysctl -n hw.memsize 2>/dev/null || true)\n if [ -n \"$bytes\" ]; then echo $(( bytes / 1048576 )); fi\n fi\n}\n\n# $1 a .properties file, $2 a key. Prints \"<line number><TAB><value>\" for the\n# last assignment of the key, or nothing.\ncibuild_prop_line() {\n [ -f \"$1\" ] || return 0\n awk -v key=\"$2\" '\n { line = $0; sub(/^[ \\t]+/, \"\", line) }\n line ~ /^[#!]/ { next }\n {\n k = line; sub(/[ \\t]*[=:].*$/, \"\", k)\n if (k == key) { v = line; sub(/^[^=:]*[=:][ \\t]*/, \"\", v); sub(/[ \\t\\r]+$/, \"\", v); found = NR \"\\t\" v }\n }\n END { if (found != \"\") print found }\n ' \"$1\"\n}\n\n# The -Xmx of a JVM argument string in MiB (the last one wins, as in the JVM),\n# or nothing when it has none.\ncibuild_xmx_mib() {\n printf '%s\\n' \"$1\" | tr ' \\t' '\\n\\n' | awk '\n /^-Xmx[0-9]+[kKmMgGtT]?$/ {\n n = substr($0, 5); u = substr(n, length(n))\n if (u ~ /[0-9]/) { v = n / 1048576 }\n else {\n n = substr(n, 1, length(n) - 1)\n if (u ~ /[kK]/) v = n / 1024\n else if (u ~ /[mM]/) v = n\n else if (u ~ /[gG]/) v = n * 1024\n else v = n * 1048576\n }\n r = int(v)\n }\n END { if (r != \"\") print r }'\n}\n\n# $1 a JVM argument string, $2 a heap in MiB: the same arguments with every\n# -Xmx replaced by that one (added in front when there was none).\ncibuild_with_xmx() (\n set -f\n out=\"\"\n replaced=0\n for arg in $1; do\n case \"$arg\" in\n -Xmx*) if [ \"$replaced\" -eq 0 ]; then out=\"$out -Xmx${2}m\"; replaced=1; fi ;;\n *) out=\"$out $arg\" ;;\n esac\n done\n [ \"$replaced\" -eq 1 ] || out=\" -Xmx${2}m$out\"\n printf '%s' \"${out# }\"\n)\n\n# MiB as GiB with one decimal.\ncibuild_gib() {\n awk -v m=\"$1\" 'BEGIN { printf \"%.1f\", m / 1024 }'\n}\n\n# A heap scaled by budget/total, rounded down to 64 MiB, never below 512 MiB.\ncibuild_scaled_heap() {\n local v=$(( $1 * $2 / $3 / 64 * 64 ))\n [ \"$v\" -ge 512 ] || v=512\n echo \"$v\"\n}\n\n# The plan, from its inputs only, so a test can run it:\n# $1 this machine's memory in MiB (empty when unknown)\n# $2 the project's gradle.properties $3 the Gradle user home's\n# $4 JVM arguments the pipeline asks for instead (may be empty)\n# Sets CIBUILD_GRADLE_MEMORY_ARGS (extra Gradle arguments),\n# CIBUILD_GRADLE_MEMORY_LINE (what to print before Gradle runs; empty when\n# nothing changes) and CIBUILD_GRADLE_MEMORY_SENTENCE (what to print if a\n# daemon is killed).\n#\n# Precedence is Gradle's own: the command line beats GRADLE_USER_HOME's\n# gradle.properties, which beats the project's. A Kotlin daemon with no -Xmx of\n# its own inherits the Gradle daemon's, and in-process compilation has no\n# Kotlin daemon at all.\ncibuild_gradle_memory_plan() {\n local mem=\"$1\" project=\"$2\" user=\"$3\" preferred=\"$4\" l\n local jvm=\"\" jvm_at=\"\" kd=\"\" kd_at=\"\" strategy=\"\"\n CIBUILD_GRADLE_MEMORY_ARGS=()\n CIBUILD_GRADLE_MEMORY_LINE=\"\"\n CIBUILD_GRADLE_MEMORY_SENTENCE=\"\"\n\n if [ -n \"$preferred\" ]; then jvm=\"$preferred\"; jvm_at=\"the pipeline\"; fi\n for f in \"$user\" \"$project\"; do\n if [ -z \"$jvm_at\" ]; then\n l=$(cibuild_prop_line \"$f\" org.gradle.jvmargs)\n if [ -n \"$l\" ]; then jvm=\"${l#*\t}\"; jvm_at=\"$f:${l%%\t*}\"; fi\n fi\n if [ -z \"$kd_at\" ]; then\n l=$(cibuild_prop_line \"$f\" kotlin.daemon.jvmargs)\n if [ -n \"$l\" ]; then kd=\"${l#*\t}\"; kd_at=\"$f:${l%%\t*}\"; fi\n fi\n if [ -z \"$strategy\" ]; then\n l=$(cibuild_prop_line \"$f\" kotlin.compiler.execution.strategy)\n strategy=\"${l#*\t}\"\n fi\n done\n\n local g k kd_own=\"\"\n g=$(cibuild_xmx_mib \"$jvm\")\n [ -n \"$g\" ] || g=512\n if [ \"$strategy\" = \"in-process\" ]; then\n k=0\n else\n kd_own=$(cibuild_xmx_mib \"$kd\")\n k=\"${kd_own:-$g}\"\n fi\n local total=$(( g + k ))\n\n local asked\n if [ -n \"$jvm_at\" ]; then asked=\"org.gradle.jvmargs=$jvm at ${jvm_at}\"; else asked=\"Gradle's default -Xmx512m\"; fi\n if [ \"$k\" -eq 0 ]; then\n asked=\"$asked; Kotlin compiles in-process\"\n elif [ -n \"$kd_own\" ]; then\n asked=\"$asked, kotlin.daemon.jvmargs=$kd at ${kd_at}\"\n else\n asked=\"$asked; the Kotlin daemon inherits its -Xmx\"\n fi\n\n if [ -z \"$mem\" ]; then\n 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.\"\n if [ -n \"$preferred\" ]; then CIBUILD_GRADLE_MEMORY_ARGS=(\"-Dorg.gradle.jvmargs=$preferred\"); fi\n return 0\n fi\n\n local budget=$(( mem * 75 / 100 ))\n 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).\"\n\n if [ \"$total\" -le \"$budget\" ]; then\n if [ -n \"$preferred\" ]; then\n CIBUILD_GRADLE_MEMORY_ARGS=(\"-Dorg.gradle.jvmargs=$preferred\")\n CIBUILD_GRADLE_MEMORY_LINE=\"Gradle memory: org.gradle.jvmargs=$preferred, as the pipeline asks (-Dorg.gradle.jvmargs on the command line).\"\n fi\n return 0\n fi\n\n case \"$jvm\" in\n *\\\\)\n 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.\"\n return 0\n ;;\n esac\n\n local ng nk now\n if [ \"$k\" -gt 0 ] && [ -z \"$kd_own\" ]; then\n # A Kotlin daemon that would inherit Gradle's -Xmx gets its own, from a\n # split budget: halved, ReFra's Kotlin compile ran out of heap.\n ng=$(cibuild_scaled_heap \"$budget\" 40 100)\n nk=$(cibuild_scaled_heap \"$budget\" 60 100)\n else\n ng=$(cibuild_scaled_heap \"$g\" \"$budget\" \"$total\")\n fi\n CIBUILD_GRADLE_MEMORY_ARGS=(\"-Dorg.gradle.jvmargs=$(cibuild_with_xmx \"$jvm\" \"$ng\")\")\n now=\"Gradle -Xmx${ng}m\"\n if [ \"$k\" -eq 0 ]; then\n :\n elif [ -n \"$kd_own\" ]; then\n nk=$(cibuild_scaled_heap \"$k\" \"$budget\" \"$total\")\n CIBUILD_GRADLE_MEMORY_ARGS+=(\"-Pkotlin.daemon.jvmargs=$(cibuild_with_xmx \"$kd\" \"$nk\")\")\n now=\"$now and the Kotlin daemon -Xmx${nk}m\"\n else\n CIBUILD_GRADLE_MEMORY_ARGS+=(\"-Pkotlin.daemon.jvmargs=-Xmx${nk}m\")\n now=\"$now and the Kotlin daemon -Xmx${nk}m of its own (it would inherit Gradle's, so the heaps are split Kotlin 60 / Gradle 40)\"\n fi\n 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.\"\n CIBUILD_GRADLE_MEMORY_SENTENCE=\"$CIBUILD_GRADLE_MEMORY_SENTENCE This build ran them capped: $now.\"\n}\n\n# $1 the number of Gradle workers the pipeline asks for (may be empty). Adds\n# --max-workers to the plan's arguments; run after cibuild_gradle_memory_plan.\n# Sets CIBUILD_GRADLE_WORKERS_LINE (what to print; empty when nothing asked).\ncibuild_gradle_workers_plan() {\n CIBUILD_GRADLE_WORKERS_LINE=\"\"\n [ -n \"$1\" ] || return 0\n case \"$1\" in\n *[!0-9]*|0*)\n 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.\"\n return 0\n ;;\n esac\n CIBUILD_GRADLE_MEMORY_ARGS+=(\"--max-workers=$1\")\n 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).\"\n CIBUILD_GRADLE_MEMORY_SENTENCE=\"$CIBUILD_GRADLE_MEMORY_SENTENCE Gradle ran with --max-workers=$1.\"\n}\n\n# The kernel's out-of-memory lines, if this user may read the kernel log.\n# Linux guests restrict dmesg to root by default; their build user has\n# non-interactive sudo, so that is the second try.\ncibuild_kernel_oom_lines() {\n { dmesg 2>/dev/null || sudo -n dmesg 2>/dev/null || true; } |\n grep -i -E 'out of memory|oom-kill|oom_reaper|killed process' | tail -n 20 || true\n}\n\n# $1 Gradle's exit status, $2 a file holding its output. Says so when the\n# failure is a killed daemon rather than a failed build.\ncibuild_gradle_memory_report() {\n if [ \"$1\" -ne 137 ] && ! grep -q \"daemon disappeared unexpectedly\" \"$2\" 2>/dev/null; then\n return 0\n fi\n local oom\n oom=$(cibuild_kernel_oom_lines)\n echo \"\"\n echo \"Gradle memory: a Gradle daemon was killed rather than failing the build (exit $1).\"\n if [ -n \"$oom\" ]; then\n echo \"The kernel's out-of-memory lines:\"\n printf '%s\\n' \"$oom\" | sed 's/^/ /'\n else\n echo \"The kernel log shows no out-of-memory kill, or this machine does not let the build read it.\"\n fi\n if [ -n \"$CIBUILD_GRADLE_MEMORY_SENTENCE\" ]; then echo \"$CIBUILD_GRADLE_MEMORY_SENTENCE\"; fi\n}\n\n# Runs Gradle with the plan's arguments, and reports a killed daemon.\ncibuild_gradle() {\n local log rc\n log=$(mktemp)\n set +e\n \"$CIBUILD_GRADLE_BIN\" \"${CIBUILD_GRADLE_MEMORY_ARGS[@]}\" \"$@\" 2>&1 | tee \"$log\"\n rc=${PIPESTATUS[0]}\n set -e\n if [ \"$rc\" -ne 0 ]; then cibuild_gradle_memory_report \"$rc\" \"$log\" || true; fi\n rm -f \"$log\"\n return \"$rc\"\n}\n\n# $1 JVM arguments the pipeline asks for, $2 the Gradle workers it asks for\n# (either may be empty). Run in the Gradle project directory, after GRADLE_CMD\n# is chosen.\ncibuild_gradle_memory_cap() {\n cibuild_gradle_memory_plan \"$(cibuild_mem_mib || true)\" \"gradle.properties\" \\\n \"${GRADLE_USER_HOME:-$HOME/.gradle}/gradle.properties\" \"$1\"\n cibuild_gradle_workers_plan \"$2\"\n if [ -n \"$CIBUILD_GRADLE_MEMORY_LINE\" ]; then echo \"$CIBUILD_GRADLE_MEMORY_LINE\"; fi\n if [ -n \"$CIBUILD_GRADLE_WORKERS_LINE\" ]; then echo \"$CIBUILD_GRADLE_WORKERS_LINE\"; fi\n CIBUILD_GRADLE_BIN=\"$GRADLE_CMD\"\n GRADLE_CMD=cibuild_gradle\n}";
42
53
  /**
43
54
  * The commands a Gradle step runs once it has chosen `GRADLE_CMD` and is in
44
55
  * the project directory. Afterwards `$GRADLE_CMD` runs Gradle with the plan's
@@ -1 +1 @@
1
- {"version":3,"file":"gradle-memory.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/gradle-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,eAAO,MAAM,uBAAuB,4hTAgPlC,CAAC;AAEH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACjC,UAAU,SAAK,GACd,MAAM,EAAE,CAOV"}
1
+ {"version":3,"file":"gradle-memory.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/gradle-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,eAAO,MAAM,uBAAuB,q9TAuPlC,CAAC;AAEH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACjC,UAAU,SAAK,GACd,MAAM,EAAE,CAOV"}
@@ -16,6 +16,9 @@
16
16
  * `GRADLE_HEAP_SHARE_PERCENT` of memory, scale both down to fit, and pass
17
17
  * them on the command line (`-Dorg.gradle.jvmargs=`,
18
18
  * `-Pkotlin.daemon.jvmargs=`). One line names the old values and the new.
19
+ * A Kotlin daemon that would inherit Gradle's -Xmx is given its own
20
+ * instead, the budget split Kotlin 60 / Gradle 40: at an even split
21
+ * (-Xmx3712m each) ReFra's Kotlin compile ran out of heap.
19
22
  * 2. **Say so.** When a Gradle run exits 137 or its output says the daemon
20
23
  * disappeared, print the kernel's out-of-memory lines and one sentence
21
24
  * with the machine's memory and what the build asked for.
@@ -29,7 +32,9 @@
29
32
  * `-Dorg.gradle.jvmargs` on the command line beats both the Gradle user
30
33
  * home's and the project's `gradle.properties` for the daemon's heap, and
31
34
  * `-P` beats `gradle.properties` for a Gradle property, which is how the
32
- * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`. `--max-workers` beats
35
+ * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`; where the project
36
+ * declares none, `-Pkotlin.daemon.jvmargs=-Xmx<n>` gives the Kotlin daemon
37
+ * that heap instead of the Gradle daemon's. `--max-workers` beats
33
38
  * `org.gradle.workers.max` in both files (`gradle.startParameter.maxWorkerCount`).
34
39
  * `GRADLE_OPTS` is not used: it sizes the client JVM, not the daemon.
35
40
  *
@@ -38,6 +43,12 @@
38
43
  */
39
44
  /** The share of the machine's memory the two daemon heaps may take together. */
40
45
  export const GRADLE_HEAP_SHARE_PERCENT = 75;
46
+ /**
47
+ * Gradle's share of that budget when the Kotlin daemon would inherit its
48
+ * -Xmx; the Kotlin daemon gets the rest as its own. The Kotlin compile is the
49
+ * larger heap: ReFra's ran out at an even split.
50
+ */
51
+ export const GRADLE_HEAP_SPLIT_PERCENT = 40;
41
52
  export const GRADLE_MEMORY_FUNCTIONS = `# This machine's memory in MiB, or nothing when it cannot be read.
42
53
  cibuild_mem_mib() {
43
54
  if [ -r /proc/meminfo ]; then
@@ -192,11 +203,17 @@ cibuild_gradle_memory_plan() {
192
203
  ;;
193
204
  esac
194
205
 
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"
206
+ local ng nk now
207
+ if [ "$k" -gt 0 ] && [ -z "$kd_own" ]; then
208
+ # A Kotlin daemon that would inherit Gradle's -Xmx gets its own, from a
209
+ # split budget: halved, ReFra's Kotlin compile ran out of heap.
210
+ ng=$(cibuild_scaled_heap "$budget" ${GRADLE_HEAP_SPLIT_PERCENT} 100)
211
+ nk=$(cibuild_scaled_heap "$budget" ${100 - GRADLE_HEAP_SPLIT_PERCENT} 100)
212
+ else
213
+ ng=$(cibuild_scaled_heap "$g" "$budget" "$total")
214
+ fi
215
+ CIBUILD_GRADLE_MEMORY_ARGS=("-Dorg.gradle.jvmargs=$(cibuild_with_xmx "$jvm" "$ng")")
216
+ now="Gradle -Xmx\${ng}m"
200
217
  if [ "$k" -eq 0 ]; then
201
218
  :
202
219
  elif [ -n "$kd_own" ]; then
@@ -204,7 +221,8 @@ cibuild_gradle_memory_plan() {
204
221
  CIBUILD_GRADLE_MEMORY_ARGS+=("-Pkotlin.daemon.jvmargs=$(cibuild_with_xmx "$kd" "$nk")")
205
222
  now="$now and the Kotlin daemon -Xmx\${nk}m"
206
223
  else
207
- now="$now, which the Kotlin daemon inherits"
224
+ CIBUILD_GRADLE_MEMORY_ARGS+=("-Pkotlin.daemon.jvmargs=-Xmx\${nk}m")
225
+ now="$now and the Kotlin daemon -Xmx\${nk}m of its own (it would inherit Gradle's, so the heaps are split Kotlin ${100 - GRADLE_HEAP_SPLIT_PERCENT} / Gradle ${GRADLE_HEAP_SPLIT_PERCENT})"
208
226
  fi
209
227
  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
228
  CIBUILD_GRADLE_MEMORY_SENTENCE="$CIBUILD_GRADLE_MEMORY_SENTENCE This build ran them capped: $now."
@@ -50,13 +50,28 @@ describe("cibuild_gradle_memory_plan", () => {
50
50
  test("ReFra: -Xmx8G, inherited by the Kotlin daemon, is capped on a 10 GiB machine", () => {
51
51
  copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
52
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"]);
53
+ // 75% of 10 GiB, split Kotlin 60 / Gradle 40: halved, the inherited
54
+ // -Xmx3712m ran out of heap in ReFra's Kotlin compile.
55
+ expect(p.args).toEqual([
56
+ "-Dorg.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8",
57
+ "-Pkotlin.daemon.jvmargs=-Xmx4608m",
58
+ ]);
56
59
  expect(p.line).toContain("org.gradle.jvmargs=-Xmx8G -Dfile.encoding=UTF-8 at gradle.properties:9");
57
60
  expect(p.line).toContain("16.0 GiB");
58
- expect(p.line).toContain("-Xmx3840m");
61
+ expect(p.line).toContain("Running with Gradle -Xmx3072m and the Kotlin daemon -Xmx4608m of its own (it would inherit Gradle's, so the heaps are split Kotlin 60 / Gradle 40), set on the command line.");
59
62
  expect(p.sentence).toMatch(/^The build machine has 10\.0 GiB; this build's Gradle and Kotlin daemons asked for 16\.0 GiB/);
63
+ expect(p.sentence).toMatch(/This build ran them capped: Gradle -Xmx3072m and the Kotlin daemon -Xmx4608m of its own .*\.$/);
64
+ });
65
+ test("ReFra on its 9.7 GiB build guest", () => {
66
+ copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
67
+ expect(plan("9933").args).toEqual([
68
+ "-Dorg.gradle.jvmargs=-Xmx2944m -Dfile.encoding=UTF-8",
69
+ "-Pkotlin.daemon.jvmargs=-Xmx4416m",
70
+ ]);
71
+ });
72
+ test("the split never goes below 512 MiB either heap", () => {
73
+ writeFileSync(join(dir, "gradle.properties"), "org.gradle.jvmargs=-Xmx4g\n");
74
+ expect(plan("1024").args).toEqual(["-Dorg.gradle.jvmargs=-Xmx512m", "-Pkotlin.daemon.jvmargs=-Xmx512m"]);
60
75
  });
61
76
  test("amethyst: both heaps are scaled, and the Kotlin one keeps its other flags", () => {
62
77
  copyFileSync(join(FIXTURES, "amethyst.gradle.properties"), join(dir, "gradle.properties"));
@@ -91,7 +106,10 @@ describe("cibuild_gradle_memory_plan", () => {
91
106
  mkdirSync(home);
92
107
  writeFileSync(join(home, "gradle.properties"), "org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC\n");
93
108
  const p = plan(TEN_GIB, "", home);
94
- expect(p.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3840m -XX:+UseParallelGC"]);
109
+ expect(p.args).toEqual([
110
+ "-Dorg.gradle.jvmargs=-Xmx3072m -XX:+UseParallelGC",
111
+ "-Pkotlin.daemon.jvmargs=-Xmx4608m",
112
+ ]);
95
113
  expect(p.line).toContain(`at ${home}/gradle.properties:1`);
96
114
  });
97
115
  test("JVM arguments the pipeline asks for replace the project's, and are still capped", () => {
@@ -100,7 +118,10 @@ describe("cibuild_gradle_memory_plan", () => {
100
118
  expect(fits.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8"]);
101
119
  expect(fits.line).toContain("as the pipeline asks");
102
120
  const capped = plan("4096", "-Xmx4g -Dfile.encoding=UTF-8");
103
- expect(capped.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8"]);
121
+ expect(capped.args).toEqual([
122
+ "-Dorg.gradle.jvmargs=-Xmx1216m -Dfile.encoding=UTF-8",
123
+ "-Pkotlin.daemon.jvmargs=-Xmx1792m",
124
+ ]);
104
125
  expect(capped.line).toContain("at the pipeline");
105
126
  });
106
127
  test("a value continued onto another line is left as it is, and says so", () => {
@@ -150,9 +171,13 @@ describe("cibuild_gradle_workers_plan", () => {
150
171
  test("ReFra: the worker count goes after the heap cap, and the sentence names it", () => {
151
172
  copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
152
173
  const p = workersPlan(TEN_GIB, "2");
153
- expect(p.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3840m -Dfile.encoding=UTF-8", "--max-workers=2"]);
174
+ expect(p.args).toEqual([
175
+ "-Dorg.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8",
176
+ "-Pkotlin.daemon.jvmargs=-Xmx4608m",
177
+ "--max-workers=2",
178
+ ]);
154
179
  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\.$/);
180
+ expect(p.sentence).toMatch(/This build ran them capped: Gradle -Xmx3072m and the Kotlin daemon -Xmx4608m .* Gradle ran with --max-workers=2\.$/);
156
181
  });
157
182
  test("heaps that fit still get the worker count", () => {
158
183
  const p = workersPlan(TEN_GIB, "4");
@@ -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;CAiLpB;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"}
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+MpB;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"}
@@ -83,14 +83,18 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
83
83
  //
84
84
  // Three moves, each conditional, so a machine missing a piece keeps the
85
85
  // old behaviour rather than failing on it:
86
- // - run the Ruby the project names: Gemfile.lock's `RUBY VERSION`, then
87
- // `.ruby-version`. Only a named Ruby 3 or newer moves to Homebrew's
88
- // Ruby, which is keg-only, so not on PATH even where it is installed
89
- // (Homebrew's cocoapods and fastlane formulae depend on it). Anything
90
- // else, including a project that names none, stays on system Ruby 2.6.
91
- // Homebrew's is Ruby 4 on the mac image, and an old lockfile's gems do
92
- // not load there — rake 13.0.6 requires `ostruct`, no longer a default
93
- // gem — so "newest installed" is the wrong default;
86
+ // - run a Ruby that fits the one the project names: Gemfile.lock's
87
+ // `RUBY VERSION`, then `.ruby-version`. Homebrew's Ruby is 4 on the mac
88
+ // image, and an old lockfile's gems do not load there — rake 13.0.6
89
+ // requires `ostruct`, no longer a default gem — so "newest installed"
90
+ // is the wrong default. System Ruby 2.6 is no answer either: the Xcode
91
+ // 26.4 SDK's Ruby 2.6 headers have no `ruby/config.h`, so no native
92
+ // extension (json, ffi) compiles under it. So a Ruby named below 3, or
93
+ // none, runs a Homebrew `ruby@3.3`; a named 3.x runs 3.3 unless
94
+ // Homebrew's `ruby` has its major.minor; a named 4.x runs Homebrew's.
95
+ // Both are keg-only, so neither is on PATH where it is installed. With
96
+ // no 3.3 on the machine, below 3 stays on system Ruby and 3+ runs
97
+ // Homebrew's, as before. The `Ruby:` line says which, and why;
94
98
  // - point GEM_HOME at a writable directory outside the checkout, so both
95
99
  // `gem install bundler` and `bundle install` can write without sudo,
96
100
  // and put its bin directory first;
@@ -112,20 +116,46 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
112
116
  commands.push(' case "${WANTED_RUBY%%.*}" in');
113
117
  commands.push(' \'\'|*[!0-9]*) WANTED_RUBY="" ;;');
114
118
  commands.push(' esac');
115
- commands.push(' if [ -z "$WANTED_RUBY" ]; then');
116
- commands.push(' RUBY_REASON="system: lockfile names no Ruby"');
117
- commands.push(' else');
118
- commands.push(' RUBY_REASON="from $WANTED_FROM $WANTED_RUBY"');
119
- commands.push(' if [ "${WANTED_RUBY%%.*}" -ge 3 ]; then');
119
+ commands.push(' # The Rubies this machine has: a Homebrew `ruby@3.3` keg, and Homebrew\'s');
120
+ commands.push(' # `ruby` with its major.minor. Both keg-only, so neither is on PATH.');
121
+ commands.push(' RUBY33=""');
122
+ commands.push(' BREW_RUBY=""');
123
+ commands.push(' BREW_RUBY_MM=""');
124
+ commands.push(' if command -v brew &>/dev/null; then');
125
+ commands.push(' RUBY33="$(brew --prefix --installed ruby@3.3 2>/dev/null || true)"');
126
+ commands.push(' if [ -n "$RUBY33" ] && [ ! -x "$RUBY33/bin/ruby" ]; then RUBY33=""; fi');
127
+ commands.push(' BREW_RUBY="$(brew --prefix --installed ruby 2>/dev/null || true)"');
128
+ commands.push(' if [ -n "$BREW_RUBY" ] && [ -x "$BREW_RUBY/bin/ruby" ]; then');
129
+ commands.push(' BREW_RUBY_MM="$(brew list --versions ruby 2>/dev/null | awk \'{ split($2, v, "."); print v[1] "." v[2]; exit }\')"');
130
+ commands.push(' else');
120
131
  commands.push(' BREW_RUBY=""');
121
- commands.push(' if command -v brew &>/dev/null; then');
122
- commands.push(' BREW_RUBY="$(brew --prefix --installed ruby 2>/dev/null || true)"');
123
- commands.push(' fi');
124
- commands.push(' if [ -n "$BREW_RUBY" ] && [ -x "$BREW_RUBY/bin/ruby" ]; then');
125
- commands.push(' export PATH="$BREW_RUBY/bin:$PATH"');
132
+ commands.push(' fi');
133
+ commands.push(' fi');
134
+ commands.push(' WANTED_MAJOR="${WANTED_RUBY%%.*}"');
135
+ commands.push(' WANTED_MM="$(printf \'%s\' "$WANTED_RUBY" | awk -F. \'{ print $1 "." $2 }\')"');
136
+ commands.push(' if [ -z "$WANTED_RUBY" ] || [ "$WANTED_MAJOR" -lt 3 ]; then');
137
+ commands.push(' if [ -z "$WANTED_RUBY" ]; then');
138
+ commands.push(' if [ -n "$RUBY33" ]; then');
139
+ commands.push(' export PATH="$RUBY33/bin:$PATH"');
140
+ commands.push(' RUBY_REASON="Ruby 3.3: lockfile names no Ruby, and system Ruby 2.6 cannot build native gems"');
126
141
  commands.push(' else');
127
- commands.push(' RUBY_REASON="$RUBY_REASON, but no Homebrew Ruby is installed"');
142
+ commands.push(' RUBY_REASON="system: lockfile names no Ruby, and no Ruby 3.3 is installed"');
128
143
  commands.push(' fi');
144
+ commands.push(' elif [ -n "$RUBY33" ]; then');
145
+ commands.push(' export PATH="$RUBY33/bin:$PATH"');
146
+ commands.push(' RUBY_REASON="from $WANTED_FROM $WANTED_RUBY, run on Ruby 3.3: system Ruby 2.6 cannot build native gems"');
147
+ commands.push(' else');
148
+ commands.push(' RUBY_REASON="from $WANTED_FROM $WANTED_RUBY, and no Ruby 3.3 is installed"');
149
+ commands.push(' fi');
150
+ commands.push(' else');
151
+ commands.push(' RUBY_REASON="from $WANTED_FROM $WANTED_RUBY"');
152
+ commands.push(' if [ "$WANTED_MAJOR" -eq 3 ] && [ -n "$RUBY33" ] && [ "$BREW_RUBY_MM" != "$WANTED_MM" ]; then');
153
+ commands.push(' export PATH="$RUBY33/bin:$PATH"');
154
+ commands.push(' [ "$WANTED_MM" != "3.3" ] && RUBY_REASON="$RUBY_REASON, run on Ruby 3.3"');
155
+ commands.push(' elif [ -n "$BREW_RUBY" ]; then');
156
+ commands.push(' export PATH="$BREW_RUBY/bin:$PATH"');
157
+ commands.push(' else');
158
+ commands.push(' RUBY_REASON="$RUBY_REASON, but no Homebrew Ruby is installed"');
129
159
  commands.push(' fi');
130
160
  commands.push(' fi');
131
161
  commands.push(' if ! command -v bundle &>/dev/null && ! command -v gem &>/dev/null; then');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.8.8",
3
+ "version": "2.9.0",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",