@invarn/cibuild 2.7.9 → 2.8.1

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.
Files changed (33) hide show
  1. package/dist/cli.cjs +143 -10
  2. package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts +2 -0
  3. package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts.map +1 -0
  4. package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.js +218 -0
  5. package/dist/src/commands/android-java-version.test.js +93 -10
  6. package/dist/src/commands/android-scanner.d.ts +109 -8
  7. package/dist/src/commands/android-scanner.d.ts.map +1 -1
  8. package/dist/src/commands/android-scanner.js +397 -27
  9. package/dist/src/commands/build.d.ts +8 -0
  10. package/dist/src/commands/build.d.ts.map +1 -1
  11. package/dist/src/commands/build.js +17 -3
  12. package/dist/src/commands/index.d.ts +1 -0
  13. package/dist/src/commands/index.d.ts.map +1 -1
  14. package/dist/src/commands/index.js +3 -0
  15. package/dist/src/commands/ios-scanner.d.ts.map +1 -1
  16. package/dist/src/commands/ios-scanner.js +15 -24
  17. package/dist/src/commands/ios-scheme-ranking.test.js +5 -0
  18. package/dist/src/shared/detect-project.d.ts +32 -0
  19. package/dist/src/shared/detect-project.d.ts.map +1 -1
  20. package/dist/src/shared/detect-project.js +62 -10
  21. package/dist/src/shared/xcode-container.test.d.ts +2 -0
  22. package/dist/src/shared/xcode-container.test.d.ts.map +1 -0
  23. package/dist/src/shared/xcode-container.test.js +123 -0
  24. package/dist/src/yaml/steps/xcode-app-product.d.ts +90 -0
  25. package/dist/src/yaml/steps/xcode-app-product.d.ts.map +1 -0
  26. package/dist/src/yaml/steps/xcode-app-product.js +247 -0
  27. package/dist/src/yaml/steps/xcode-app-product.test.d.ts +2 -0
  28. package/dist/src/yaml/steps/xcode-app-product.test.d.ts.map +1 -0
  29. package/dist/src/yaml/steps/xcode-app-product.test.js +202 -0
  30. package/dist/src/yaml/steps/xcode-derived-data.test.js +66 -1
  31. package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
  32. package/dist/src/yaml/steps/xcode.js +50 -3
  33. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts","sourceRoot":"","sources":["../../../src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * An empty properties file fixes "file missing", not "field missing".
3
+ *
4
+ * The Secrets Gradle plugin turns each key of the properties file it loads
5
+ * into a `BuildConfig` constant and a manifest placeholder. A checkout without
6
+ * that file gets neither, so handing the build an empty stand-in clears the
7
+ * plugin's own throw and then fails one step later, inside Gradle, at the first
8
+ * reference to a constant that was never generated.
9
+ *
10
+ * `missingPropertyKeys` is the list a stand-in has to declare for that
11
+ * reference to resolve: what the build reads, read off the build files and the
12
+ * source under the modules that apply the plugin.
13
+ */
14
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+ import { scanAndroidProject } from "./android-scanner.js";
18
+ let root;
19
+ beforeEach(() => {
20
+ root = mkdtempSync(join(tmpdir(), "cibuild-props-"));
21
+ });
22
+ afterEach(() => {
23
+ rmSync(root, { recursive: true, force: true });
24
+ });
25
+ function write(relative, content) {
26
+ const path = join(root, relative);
27
+ mkdirSync(join(path, ".."), { recursive: true });
28
+ writeFileSync(path, content);
29
+ }
30
+ async function keys() {
31
+ return (await scanAndroidProject(root)).missingPropertyKeys;
32
+ }
33
+ /**
34
+ * The shape the whole thing is for: one module applying the plugin, no
35
+ * properties file anywhere, and the root declaring the plugin without
36
+ * applying it — which is where a version catalog alias is nearly always named.
37
+ */
38
+ function projectApplyingTheSecretsPlugin(appBuildFileExtra = "") {
39
+ write("settings.gradle.kts", 'include(":app")\n');
40
+ write("build.gradle.kts", "plugins {\n alias(libs.plugins.secrets) apply false\n}\n");
41
+ write("app/build.gradle.kts", `plugins {\n id("com.android.application")\n alias(libs.plugins.secrets)\n}\n\nandroid {\n namespace = "com.example.app"\n${appBuildFileExtra}}\n`);
42
+ write(".gitignore", "local.properties\n");
43
+ }
44
+ describe("the keys a generated properties file has to declare", () => {
45
+ test("names a BuildConfig field referenced from Kotlin source", async () => {
46
+ projectApplyingTheSecretsPlugin();
47
+ write("app/src/main/java/com/example/app/Maps.kt", "package com.example.app\n\nval key: String = BuildConfig.MAPS_API_KEY\n");
48
+ expect(await keys()).toEqual(["MAPS_API_KEY"]);
49
+ });
50
+ test("names one referenced from Java source", async () => {
51
+ projectApplyingTheSecretsPlugin();
52
+ write("app/src/main/java/com/example/app/Maps.java", "package com.example.app;\n\nclass Maps { String k = BuildConfig.MAPS_API_KEY; }\n");
53
+ expect(await keys()).toEqual(["MAPS_API_KEY"]);
54
+ });
55
+ test("names a manifestPlaceholders key — Kotlin DSL, indexed", async () => {
56
+ projectApplyingTheSecretsPlugin(' defaultConfig {\n manifestPlaceholders["mapsApiKey"] = "unused"\n }\n');
57
+ expect(await keys()).toEqual(["mapsApiKey"]);
58
+ });
59
+ test("names a manifestPlaceholders key — Kotlin DSL, whole map", async () => {
60
+ projectApplyingTheSecretsPlugin(' defaultConfig {\n manifestPlaceholders += mapOf("mapsApiKey" to "u", "adsAppId" to "u")\n }\n');
61
+ expect(await keys()).toEqual(["adsAppId", "mapsApiKey"]);
62
+ });
63
+ test("names a manifestPlaceholders key — Groovy map literal", async () => {
64
+ write("settings.gradle", "include ':app'\n");
65
+ write("build.gradle", "// root\n");
66
+ write("app/build.gradle", "plugins {\n id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'\n}\n" +
67
+ "android {\n defaultConfig {\n" +
68
+ " manifestPlaceholders = [mapsApiKey: \"unused\", 'adsAppId': \"unused\"]\n" +
69
+ " }\n}\n");
70
+ expect(await keys()).toEqual(["adsAppId", "mapsApiKey"]);
71
+ });
72
+ test("names a manifestPlaceholders key — Groovy put", async () => {
73
+ write("settings.gradle", "include ':app'\n");
74
+ write("build.gradle", "// root\n");
75
+ write("app/build.gradle", "plugins {\n id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'\n}\n" +
76
+ "android {\n defaultConfig {\n" +
77
+ " manifestPlaceholders.put('mapsApiKey', 'unused')\n" +
78
+ " }\n}\n");
79
+ expect(await keys()).toEqual(["mapsApiKey"]);
80
+ });
81
+ test("merges both sources, sorted and deduplicated", async () => {
82
+ projectApplyingTheSecretsPlugin(' defaultConfig {\n manifestPlaceholders["MAPS_API_KEY"] = "u"\n }\n');
83
+ write("app/src/main/java/com/example/app/Keys.kt", "val a = BuildConfig.MAPS_API_KEY\nval b = BuildConfig.ADS_APP_ID\nval c = BuildConfig.MAPS_API_KEY\n");
84
+ expect(await keys()).toEqual(["ADS_APP_ID", "MAPS_API_KEY"]);
85
+ });
86
+ });
87
+ describe("what it deliberately leaves out", () => {
88
+ /**
89
+ * `BuildConfig.DEBUG` is the most-referenced constant in Android source, and
90
+ * AGP generates it whether or not a properties file exists. Declaring it
91
+ * would make the Secrets plugin generate a `String DEBUG` beside AGP's
92
+ * `boolean DEBUG` — the file meant to rescue the build breaking it instead.
93
+ */
94
+ test("the fields AGP generates by itself", async () => {
95
+ projectApplyingTheSecretsPlugin();
96
+ write("app/src/main/java/com/example/app/Env.kt", [
97
+ "val d = BuildConfig.DEBUG",
98
+ "val a = BuildConfig.APPLICATION_ID",
99
+ "val t = BuildConfig.BUILD_TYPE",
100
+ "val f = BuildConfig.FLAVOR",
101
+ "val fd = BuildConfig.FLAVOR_environment",
102
+ "val vc = BuildConfig.VERSION_CODE",
103
+ "val vn = BuildConfig.VERSION_NAME",
104
+ "val lp = BuildConfig.LIBRARY_PACKAGE_NAME",
105
+ "val mine = BuildConfig.MAPS_API_KEY",
106
+ ].join("\n"));
107
+ expect(await keys()).toEqual(["MAPS_API_KEY"]);
108
+ });
109
+ test("a project that applies no Secrets plugin", async () => {
110
+ write("settings.gradle.kts", 'include(":app")\n');
111
+ write("build.gradle.kts", "// root\n");
112
+ write("app/build.gradle.kts", 'plugins {\n id("com.android.application")\n}\n');
113
+ write("app/src/main/java/com/example/app/Maps.kt", "val k = BuildConfig.MAPS_API_KEY\n");
114
+ expect(await keys()).toEqual([]);
115
+ });
116
+ /**
117
+ * `apply false` names the plugin's version for the modules that will apply
118
+ * it; the root project loads no properties file of its own. Reading it as an
119
+ * application would aim the source walk at the whole repository — every
120
+ * module's source, on the strength of a line that applies nothing.
121
+ */
122
+ test("a root that declares the plugin with apply false", async () => {
123
+ write("settings.gradle.kts", 'include(":app", ":other")\n');
124
+ write("build.gradle.kts", "plugins {\n alias(libs.plugins.secrets) apply false\n}\n");
125
+ write("app/build.gradle.kts", 'plugins {\n id("com.android.application")\n}\n');
126
+ write("other/build.gradle.kts", "// nothing\n");
127
+ write("other/src/main/java/Other.kt", "val k = BuildConfig.ELSEWHERE\n");
128
+ expect(await keys()).toEqual([]);
129
+ });
130
+ test("generated sources under build/", async () => {
131
+ projectApplyingTheSecretsPlugin();
132
+ write("app/build/generated/source/buildConfig/debug/com/example/app/BuildConfig.java", "public final class BuildConfig { public static final String STALE = BuildConfig.STALE; }\n");
133
+ write("app/src/main/java/com/example/app/Maps.kt", "val k = BuildConfig.MAPS_API_KEY\n");
134
+ expect(await keys()).toEqual(["MAPS_API_KEY"]);
135
+ });
136
+ });
137
+ describe("the module boundary", () => {
138
+ test("reads source of the module that applies the plugin, not its siblings", async () => {
139
+ write("settings.gradle.kts", 'include(":app", ":other")\n');
140
+ write("build.gradle.kts", "// root\n");
141
+ write("app/build.gradle.kts", 'plugins {\n alias(libs.plugins.secrets)\n}\n');
142
+ write("app/src/main/java/App.kt", "val k = BuildConfig.MAPS_API_KEY\n");
143
+ write("other/build.gradle.kts", "// nothing\n");
144
+ write("other/src/main/java/Other.kt", "val k = BuildConfig.ELSEWHERE\n");
145
+ expect(await keys()).toEqual(["MAPS_API_KEY"]);
146
+ });
147
+ test("a root module that really applies the plugin covers the whole tree", async () => {
148
+ write("settings.gradle.kts", 'include(":app")\n');
149
+ write("build.gradle.kts", 'plugins {\n id("com.android.application")\n alias(libs.plugins.secrets)\n}\n');
150
+ write("app/build.gradle.kts", "// nothing\n");
151
+ write("app/src/main/java/App.kt", "val k = BuildConfig.MAPS_API_KEY\n");
152
+ expect(await keys()).toEqual(["MAPS_API_KEY"]);
153
+ });
154
+ });
155
+ /**
156
+ * The warnings a customer already sees. This scan adds a field and reads more
157
+ * files; it must not add, drop or reword a single warning, so the two paths
158
+ * that emit a `secrets-plugin` warning are asserted whole.
159
+ */
160
+ describe("the Secrets-plugin warning path is unchanged", () => {
161
+ test("with no companion defaults file, the one file-absent warning", async () => {
162
+ projectApplyingTheSecretsPlugin();
163
+ write("app/src/main/java/com/example/app/Maps.kt", "val k = BuildConfig.MAPS_API_KEY\n");
164
+ const result = await scanAndroidProject(root);
165
+ expect(result.warnings.filter((w) => w.category === "secrets-plugin")).toEqual([
166
+ {
167
+ category: "secrets-plugin",
168
+ severity: "warning",
169
+ message: "Google Secrets Gradle Plugin reads 'local.properties' which is absent in CI",
170
+ hint: "Inject 'local.properties' as a file step using a secret variable",
171
+ // The root build file, which is where the version-catalog alias is
172
+ // named with `apply false`. The warning path counts that as an
173
+ // application and always has; `missingPropertyKeys` does not, which is
174
+ // why the two ask the question through different functions.
175
+ location: "build.gradle.kts",
176
+ },
177
+ ]);
178
+ // And the new field is populated in the same scan, from the same files.
179
+ expect(result.missingPropertyKeys).toEqual(["MAPS_API_KEY"]);
180
+ });
181
+ test("with a companion defaults file, one warning per expected key", async () => {
182
+ projectApplyingTheSecretsPlugin();
183
+ write("secrets.defaults.properties", "MAPS_API_KEY=\nADS_APP_ID=\n");
184
+ write("app/src/main/java/com/example/app/Maps.kt", "val k = BuildConfig.MAPS_API_KEY\n");
185
+ const result = await scanAndroidProject(root);
186
+ expect(result.warnings.filter((w) => w.category === "secrets-plugin")).toEqual([
187
+ {
188
+ category: "secrets-plugin",
189
+ severity: "warning",
190
+ message: "Secrets plugin key: MAPS_API_KEY",
191
+ hint: "Add via: ci secrets add MAPS_API_KEY",
192
+ location: "local.properties",
193
+ },
194
+ {
195
+ category: "secrets-plugin",
196
+ severity: "warning",
197
+ message: "Secrets plugin key: ADS_APP_ID",
198
+ hint: "Add via: ci secrets add ADS_APP_ID",
199
+ location: "local.properties",
200
+ },
201
+ ]);
202
+ });
203
+ test("a properties file that is present but not gitignored warns about neither", async () => {
204
+ write("settings.gradle.kts", 'include(":app")\n');
205
+ write("build.gradle.kts", "// root\n");
206
+ write("app/build.gradle.kts", "plugins {\n alias(libs.plugins.secrets)\n}\n");
207
+ write("local.properties", "sdk.dir=/opt/android\n");
208
+ write("app/src/main/java/App.kt", "val k = BuildConfig.MAPS_API_KEY\n");
209
+ const result = await scanAndroidProject(root);
210
+ expect(result.warnings.filter((w) => w.category === "secrets-plugin")).toEqual([]);
211
+ // The keys are still reported. `invarn`-style callers run this scan on a
212
+ // developer's own checkout, where the file is nearly always present and
213
+ // holds nothing but `sdk.dir` — gating the keys on its absence would
214
+ // return nothing in exactly the case they are needed for.
215
+ expect(result.missingPropertyKeys).toEqual(["MAPS_API_KEY"]);
216
+ });
217
+ });
218
+ //# sourceMappingURL=a-properties-stand-in-declares-the-keys-the-build-reads.test.js.map
@@ -63,8 +63,14 @@ describe('runnableJavaVersion', () => {
63
63
  expect(runnableJavaVersion(17)).toBe(25);
64
64
  expect(runnableJavaVersion(25)).toBe(25);
65
65
  });
66
- test('takes the newest available JDK when nothing is declared', () => {
67
- expect(runnableJavaVersion(undefined)).toBe(25);
66
+ // Nothing declared and no wrapper to read is nothing to go on. It used to
67
+ // answer 25 anyway, which is a guess wearing a number; the generator's own
68
+ // default stands instead, and that is not news worth a warning.
69
+ test('abstains when there is nothing to go on', () => {
70
+ expect(runnableJavaVersion(undefined)).toBeUndefined();
71
+ });
72
+ test('still answers from the wrapper alone, with nothing declared', () => {
73
+ expect(runnableJavaVersion(undefined, '8.9')).toBe(21);
68
74
  });
69
75
  // 8 joined the images on 2026-09-07 and must stay unreachable except where
70
76
  // a wrapper ceiling forces it: a project declaring `sourceCompatibility 1.8`
@@ -84,17 +90,58 @@ describe('runnableJavaVersion', () => {
84
90
  expect(runnableJavaVersion(11, '5.2.1')).toBe(11);
85
91
  expect(runnableJavaVersion(undefined, '6.9')).toBe(11);
86
92
  });
93
+ // No answer is ever a JDK the build machines lack.
87
94
  test('never names a JDK no build machine has', () => {
88
- expect(AVAILABLE_JAVA_VERSIONS).toContain(runnableJavaVersion(23));
89
- expect(AVAILABLE_JAVA_VERSIONS).toContain(runnableJavaVersion(25, '8.9'));
90
- });
91
- test('lets an old wrapper cap a newer declaration', () => {
92
- // Gradle 7.x tops out at 17 even though newer JDKs are installed. There
93
- // is no usable JDK at all, so the pipeline names the newest and the
94
- // caller warns — `javaVersionProblem` is what says so.
95
- expect(runnableJavaVersion(21, '7.6.1')).toBe(25);
95
+ for (const answer of [runnableJavaVersion(23), runnableJavaVersion(25, '8.9')]) {
96
+ if (answer !== undefined)
97
+ expect(AVAILABLE_JAVA_VERSIONS).toContain(answer);
98
+ }
99
+ });
100
+ /**
101
+ * And no answer is ever above the wrapper's own ceiling.
102
+ *
103
+ * This is the defect. Nothing satisfied both the declaration and the
104
+ * ceiling, so the chooser fell back to the newest installed JDK — which is
105
+ * *above* the ceiling by construction, because the ceiling is why nothing
106
+ * fitted. The build then died selecting the JDK, exactly as a too-new JDK
107
+ * always does, and `javaVersionProblem` had no branch for the shape so
108
+ * nothing anywhere said a choice had been made.
109
+ *
110
+ * The ceiling is hard: Gradle starts before it reads a single
111
+ * `sourceCompatibility`. The declaration is a floor. So where they conflict
112
+ * the ceiling wins, and the caller says so.
113
+ */
114
+ test('never answers above the wrapper ceiling', () => {
115
+ // Gradle 7.x tops out at 17. Declaring 21 cannot raise that.
116
+ expect(runnableJavaVersion(21, '7.6.1')).toBe(17);
96
117
  expect(runnableJavaVersion(17, '7.6.1')).toBe(17);
97
118
  });
119
+ // Gradle 6.5 runs on Java 11 at most, and a workflow pins java-version 12.
120
+ // The old chooser answered 25 and said nothing.
121
+ test('caps a modern declaration under an old wrapper', () => {
122
+ expect(runnableJavaVersion(12, '6.5')).toBe(11);
123
+ });
124
+ // Every shape the corpus has, held to both properties at once: the answer
125
+ // is installed, and it is not above the ceiling.
126
+ test.each([
127
+ [8, '4.10.2'],
128
+ [11, '5.2.1'],
129
+ [12, '6.5'],
130
+ [17, '7.6.1'],
131
+ [21, '7.6.1'],
132
+ [8, '9.0'],
133
+ [30, '8.9'],
134
+ [25, '9.1'],
135
+ [undefined, '6.9'],
136
+ ])('declared %s on Gradle %s answers within both bounds', (declared, gradle) => {
137
+ const answer = runnableJavaVersion(declared, gradle);
138
+ if (answer === undefined)
139
+ return; // abstention is always allowed
140
+ expect(AVAILABLE_JAVA_VERSIONS).toContain(answer);
141
+ const ceiling = gradleJavaCeiling(gradle);
142
+ if (ceiling !== undefined)
143
+ expect(answer).toBeLessThanOrEqual(ceiling);
144
+ });
98
145
  test('honours the Gradle 9 floor of 17', () => {
99
146
  // The floor rules 11 out; the newest above it is what gets written.
100
147
  expect(runnableJavaVersion(8, '9.0')).toBe(21);
@@ -122,5 +169,41 @@ describe('javaVersionProblem', () => {
122
169
  // Nor is 4.10.2, since the images carry 8.
123
170
  expect(javaVersionProblem(undefined, '4.10.2')).toBeUndefined();
124
171
  });
172
+ /**
173
+ * The shape that used to fall through every branch, and the reason this
174
+ * function is resolved from the same place as the answer.
175
+ *
176
+ * A wrapper on Gradle 6.5 under a workflow pinning java-version 12: the
177
+ * ceiling is not below every installed JDK and the declaration is not above
178
+ * every installed JDK, so neither old branch fired. The chooser answered 25
179
+ * — above the ceiling — and nothing said the project had asked for something
180
+ * else and been overruled.
181
+ */
182
+ test('speaks when an old wrapper overrules a newer declaration', () => {
183
+ const problem = javaVersionProblem(12, '6.5');
184
+ expect(problem).toContain('Java 12');
185
+ expect(problem).toContain('Gradle 6.5');
186
+ expect(problem).toContain('Java 11 at most');
187
+ expect(problem).toContain('Building on 11');
188
+ });
189
+ // An answer that is not what was asked for always speaks, whichever
190
+ // constraint did the overruling.
191
+ test.each([
192
+ [12, '6.5'],
193
+ [21, '7.6.1'],
194
+ [30, '8.9'],
195
+ ])('declared %s on Gradle %s never passes silently', (declared, gradle) => {
196
+ expect(javaVersionProblem(declared, gradle)).toBeDefined();
197
+ });
198
+ // Naming a version with no provenance sends a reader through every Gradle
199
+ // file looking for a number that is in none of them. The scan knows which
200
+ // file asked, so the sentence can say.
201
+ test('names the file that asked, when it is known', () => {
202
+ const problem = javaVersionProblem(12, '6.5', '.github/workflows/Build.yml');
203
+ expect(problem).toContain('Java 12 (.github/workflows/Build.yml)');
204
+ });
205
+ test('reads correctly without a source', () => {
206
+ expect(javaVersionProblem(12, '6.5')).not.toContain('(');
207
+ });
125
208
  });
126
209
  //# sourceMappingURL=android-java-version.test.js.map
@@ -11,10 +11,42 @@ export interface ScanResult {
11
11
  warnings: BuildWarning[];
12
12
  /** Highest Java version requirement detected in Gradle files (e.g. 17, 21). */
13
13
  detectedJavaVersion?: number;
14
+ /**
15
+ * The file that asked for it, when the level came from outside Gradle —
16
+ * `.github/workflows/Build.yml`, `.java-version`, `.tool-versions`.
17
+ *
18
+ * The scan computed this and dropped it, so a warning could name a version
19
+ * with no provenance and send a reader looking through every Gradle file for
20
+ * a number that is not in any of them. Undefined when the level came from a
21
+ * Gradle file, or when a Gradle file asked for at least as much.
22
+ */
23
+ detectedJavaVersionSource?: string;
14
24
  /** Gradle version the wrapper pins, e.g. "8.9". Undefined when there is no wrapper. */
15
25
  detectedGradleVersion?: string;
16
26
  /** Build types, product flavors, and all variant combinations detected in Gradle files. */
17
27
  buildVariants: BuildVariants;
28
+ /**
29
+ * The property names the build reads out of the properties file the Secrets
30
+ * Gradle plugin loads — sorted, deduplicated, empty when no module applies
31
+ * the plugin.
32
+ *
33
+ * **An empty properties file fixes "file missing", not "field missing".**
34
+ * The plugin turns each key of that file into a `BuildConfig` constant and a
35
+ * manifest placeholder, so a checkout without the file compiles neither.
36
+ * Supplying an empty stand-in gets the build past the plugin's own throw and
37
+ * then fails one step later, inside Gradle, at the first reference to a field
38
+ * that was never generated. These are those names: what a stand-in has to
39
+ * declare for the reference to resolve.
40
+ *
41
+ * Data, not prose — deliberately not more `warnings`. A warning is a sentence
42
+ * for a human to read; this is a list for a generator to consume, and sharing
43
+ * one channel between them means the generator has to parse English.
44
+ *
45
+ * Coarse on purpose. A name here that some other plugin turns out to
46
+ * generate costs one unused line in a properties file; a name missing from
47
+ * here costs the build.
48
+ */
49
+ missingPropertyKeys: string[];
18
50
  }
19
51
  export interface BuildVariants {
20
52
  /** Build types found, e.g. ["debug", "release", "staging"]. Always includes debug + release. */
@@ -102,12 +134,71 @@ export declare function gradleJavaFloor(gradleVersion: string | undefined): numb
102
134
  */
103
135
  export declare const AVAILABLE_JAVA_VERSIONS: number[];
104
136
  /**
105
- * The newest JDK a build machine carries. Named because it is what a
106
- * generated pipeline falls back to when no JDK can satisfy the constraints —
107
- * a pipeline has to write something, and `javaVersionProblem` says what was
108
- * given up.
137
+ * The newest JDK a build machine carries.
138
+ *
139
+ * It is no longer what the chooser falls back to. It used to be: with no JDK
140
+ * satisfying both the sources and the wrapper, `runnableJavaVersion` returned
141
+ * this — which on a project pinning a modern Java level under an old Gradle
142
+ * wrapper is a JDK **above** that wrapper's ceiling, and the build dies at
143
+ * `:app:processDebugMainManifest` with "module java.base does not opens
144
+ * java.io to unnamed module". A number above the ceiling is not a fallback;
145
+ * it is the failure, chosen deliberately and reported as success.
109
146
  */
110
147
  export declare const NEWEST_AVAILABLE_JAVA_VERSION: number;
148
+ /** Why the chooser answered as it did. See {@link chooseJavaVersion}. */
149
+ export type JavaVersionKind = "unknown" | "satisfies" | "capped-by-wrapper" | "above-runners" | "wrapper-below-runners";
150
+ /** The JDK choice and the reason for it, resolved together. */
151
+ export interface JavaVersionChoice {
152
+ /** The JDK to write, or undefined where no answer exists. */
153
+ version: number | undefined;
154
+ kind: JavaVersionKind;
155
+ /** The highest level the project asked for, or 0 if it asked for none. */
156
+ declared: number;
157
+ /** The newest JDK the wrapper can start on, or undefined if unreadable. */
158
+ ceiling: number | undefined;
159
+ installed: number[];
160
+ }
161
+ /**
162
+ * The JDK to build on, with the reason attached — one place where the
163
+ * constraints are resolved, so the answer and the explanation cannot disagree.
164
+ *
165
+ * They used to. The chooser fell back to the newest installed JDK whenever
166
+ * nothing satisfied both constraints, and the sentence beside it had two
167
+ * branches — a ceiling below every installed JDK, and a declaration above
168
+ * every installed JDK — so the commonest shape of all fell through both and
169
+ * said nothing at all. A project pinning Java 12 against a Gradle wrapper that
170
+ * runs on 11 at most got Java 25 and silence.
171
+ *
172
+ * Three facts decide it:
173
+ *
174
+ * - **What the build machine has.** Only `AVAILABLE_JAVA_VERSIONS` exist, so
175
+ * a project declaring 30 cannot simply be given 30.
176
+ * - **What Gradle can run on.** The wrapper sets a ceiling that source
177
+ * compatibility cannot raise: Gradle starts before it reads a single
178
+ * `sourceCompatibility`.
179
+ * - **What the sources ask for.** A declaration is a floor, not an exact
180
+ * request — a project declaring 11 compiles fine on 21.
181
+ *
182
+ * `kind` distinguishes the five outcomes:
183
+ *
184
+ * - `unknown` — nothing declared and no readable wrapper. Nothing to go on
185
+ * and nothing to say.
186
+ * - `satisfies` — an installed JDK meets both the floor and the ceiling.
187
+ * - `capped-by-wrapper` — nothing meets both, and the wrapper is why. The
188
+ * ceiling is hard and the declaration is a preference, so the newest JDK
189
+ * the wrapper *can* run is the answer, and the caller says so.
190
+ * - `above-runners` — the declaration is above every installed JDK, and that
191
+ * rather than the wrapper is what binds. **No answer**: asking for it would
192
+ * fail when the JDK is selected, and quietly picking a lower one would be
193
+ * the guess this exists to avoid.
194
+ * - `wrapper-below-runners` — the ceiling is below every installed JDK. No
195
+ * answer exists.
196
+ *
197
+ * `above-runners` is checked before `capped-by-wrapper` so a project declaring
198
+ * 99 against a modern wrapper is told about the machines rather than about its
199
+ * wrapper: both bind, and the one it can act on is the one worth naming.
200
+ */
201
+ export declare function chooseJavaVersion(detectedJavaVersion: number | undefined, gradleVersion?: string): JavaVersionChoice;
111
202
  /**
112
203
  * The Java version to write into a generated pipeline: the **newest**
113
204
  * available JDK that satisfies both what the sources declare and what the
@@ -125,12 +216,22 @@ export declare const NEWEST_AVAILABLE_JAVA_VERSION: number;
125
216
  * caps the JDK no matter what the sources say — which is why the ceiling
126
217
  * above had to learn minor versions before this could ship.
127
218
  */
128
- export declare function runnableJavaVersion(detectedJavaVersion: number | undefined, gradleVersion?: string): number;
219
+ export declare function runnableJavaVersion(detectedJavaVersion: number | undefined, gradleVersion?: string): number | undefined;
129
220
  /**
130
- * What `runnableJavaVersion` could not honour, as a sentence, or undefined
131
- * when it honoured everything.
221
+ * What the choice could not honour, as a sentence, or undefined when it
222
+ * honoured everything.
223
+ *
224
+ * Silence is the failure this exists to prevent: a pipeline that quietly
225
+ * overruled what a project asked for reads as the project's own problem, and
226
+ * the commonest shape of all — an old wrapper under a modern declaration — was
227
+ * the one that used to fall through every branch.
228
+ *
229
+ * `source` names the file that asked, when the caller knows it. The scan now
230
+ * returns it, so "asks for Java 12" can become "asks for Java 12
231
+ * (.github/workflows/Build.yml)" — a version with no provenance sends a reader
232
+ * looking through every Gradle file for a number that is not in any of them.
132
233
  */
133
- export declare function javaVersionProblem(detectedJavaVersion: number | undefined, gradleVersion?: string): string | undefined;
234
+ export declare function javaVersionProblem(detectedJavaVersion: number | undefined, gradleVersion?: string, source?: string): string | undefined;
134
235
  /**
135
236
  * Detects build types and product flavors from Gradle files by static parsing,
136
237
  * falling back to the module's source sets for the flavors when no Gradle file
@@ -1 +1 @@
1
- {"version":3,"file":"android-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/android-scanner.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,SAAS,GACT,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,2FAA2F;IAC3F,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wFAAwF;IACxF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAwKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcnE;AAyKD,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAO7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,GACX;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CA8CjD;AA2BD;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG7E;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAsBvF;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIrF;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,uBAAuB,UAAsB,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,QAAuC,CAAC;AAElF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAQR;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAkBpB;AAwLD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,aAAa,CAsCxE;AAMD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAoQjF;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAyD3D"}
1
+ {"version":3,"file":"android-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/android-scanner.ts"],"names":[],"mappings":"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"}