@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.
- package/dist/cli.cjs +143 -10
- package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts +2 -0
- package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts.map +1 -0
- package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.js +218 -0
- package/dist/src/commands/android-java-version.test.js +93 -10
- package/dist/src/commands/android-scanner.d.ts +109 -8
- package/dist/src/commands/android-scanner.d.ts.map +1 -1
- package/dist/src/commands/android-scanner.js +397 -27
- package/dist/src/commands/build.d.ts +8 -0
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +17 -3
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +3 -0
- package/dist/src/commands/ios-scanner.d.ts.map +1 -1
- package/dist/src/commands/ios-scanner.js +15 -24
- package/dist/src/commands/ios-scheme-ranking.test.js +5 -0
- package/dist/src/shared/detect-project.d.ts +32 -0
- package/dist/src/shared/detect-project.d.ts.map +1 -1
- package/dist/src/shared/detect-project.js +62 -10
- package/dist/src/shared/xcode-container.test.d.ts +2 -0
- package/dist/src/shared/xcode-container.test.d.ts.map +1 -0
- package/dist/src/shared/xcode-container.test.js +123 -0
- package/dist/src/yaml/steps/xcode-app-product.d.ts +90 -0
- package/dist/src/yaml/steps/xcode-app-product.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-app-product.js +247 -0
- package/dist/src/yaml/steps/xcode-app-product.test.d.ts +2 -0
- package/dist/src/yaml/steps/xcode-app-product.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-app-product.test.js +202 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.js +66 -1
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +50 -3
- package/package.json +1 -1
package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts.map
ADDED
|
@@ -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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
|
131
|
-
*
|
|
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;
|
|
1
|
+
{"version":3,"file":"android-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/android-scanner.ts"],"names":[],"mappings":"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"}
|