@invarn/cibuild 2.8.0 → 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 +11 -11
- 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-scanner.d.ts +22 -0
- package/dist/src/commands/android-scanner.d.ts.map +1 -1
- package/dist/src/commands/android-scanner.js +264 -0
- 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/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
|
|
@@ -25,6 +25,28 @@ export interface ScanResult {
|
|
|
25
25
|
detectedGradleVersion?: string;
|
|
26
26
|
/** Build types, product flavors, and all variant combinations detected in Gradle files. */
|
|
27
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[];
|
|
28
50
|
}
|
|
29
51
|
export interface BuildVariants {
|
|
30
52
|
/** Build types found, e.g. ["debug", "release", "staging"]. Always includes debug + release. */
|
|
@@ -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;;;;;;;;OAQG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,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":"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;;;;;;;;OAQG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,2FAA2F;IAC3F,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;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;AAkaD;;;;;;;;;;;;;;;;;;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;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6BAA6B,QAAuC,CAAC;AAElF,yEAAyE;AACzE,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,WAAW,GACX,mBAAmB,GACnB,eAAe,GACf,uBAAuB,CAAC;AAE5B,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,iBAAiB,CAC/B,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,iBAAiB,CAkCnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAEpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,GAAG,SAAS,CAwBpB;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,CA8RjF;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAqE3D"}
|
|
@@ -138,6 +138,259 @@ function extractSecretsPropertiesFile(content) {
|
|
|
138
138
|
const m = /propertiesFileName\s*=\s*["']([^"']+)["']/.exec(block);
|
|
139
139
|
return m ? m[1] : "local.properties";
|
|
140
140
|
}
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Property keys the build reads out of the Secrets plugin's properties file
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* True when this build file *applies* the Secrets plugin, as opposed to merely
|
|
146
|
+
* declaring its version for other modules.
|
|
147
|
+
*
|
|
148
|
+
* A root build file almost always carries
|
|
149
|
+
* `alias(libs.plugins.secrets) apply false` — the plugin is named there so a
|
|
150
|
+
* module can apply it without repeating the version, and the root project
|
|
151
|
+
* itself never loads a properties file. Reading that as "the root module
|
|
152
|
+
* applies it" would aim the source walk below at the whole repository on the
|
|
153
|
+
* strength of a line that applies nothing.
|
|
154
|
+
*
|
|
155
|
+
* Separate from `detectSecretsPlugin` on purpose: that one decides which
|
|
156
|
+
* warnings are emitted, and this must not change them.
|
|
157
|
+
*/
|
|
158
|
+
function appliesSecretsPlugin(content) {
|
|
159
|
+
const applied = content
|
|
160
|
+
.split("\n")
|
|
161
|
+
.filter((line) => !/\bapply\s+false\b/.test(line))
|
|
162
|
+
.join("\n");
|
|
163
|
+
return detectSecretsPlugin(applied);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The `BuildConfig` fields the Android Gradle Plugin generates by itself.
|
|
167
|
+
*
|
|
168
|
+
* `BuildConfig.DEBUG` is the most-referenced constant in Android source, and
|
|
169
|
+
* every one of these is generated whether or not a properties file exists — so
|
|
170
|
+
* naming them costs more than the unused line the rest of this scan tolerates.
|
|
171
|
+
* A property named `DEBUG` would make the Secrets plugin generate a `String
|
|
172
|
+
* DEBUG` beside AGP's `boolean DEBUG`, and the build that was going to compile
|
|
173
|
+
* stops compiling because of the file meant to rescue it.
|
|
174
|
+
*
|
|
175
|
+
* `FLAVOR_<dimension>` is generated once per flavor dimension, so it is matched
|
|
176
|
+
* by prefix rather than listed.
|
|
177
|
+
*/
|
|
178
|
+
const AGP_GENERATED_BUILD_CONFIG_FIELDS = new Set([
|
|
179
|
+
"DEBUG",
|
|
180
|
+
"APPLICATION_ID",
|
|
181
|
+
"BUILD_TYPE",
|
|
182
|
+
"FLAVOR",
|
|
183
|
+
"VERSION_CODE",
|
|
184
|
+
"VERSION_NAME",
|
|
185
|
+
"LIBRARY_PACKAGE_NAME",
|
|
186
|
+
]);
|
|
187
|
+
function isAgpGeneratedField(name) {
|
|
188
|
+
return AGP_GENERATED_BUILD_CONFIG_FIELDS.has(name) || name.startsWith("FLAVOR_");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* `BuildConfig.SOME_FIELD` references in a source file.
|
|
192
|
+
*
|
|
193
|
+
* Screaming snake case only: that is the convention for a generated constant,
|
|
194
|
+
* and widening it to any identifier would pull in `BuildConfig.javaClass` and
|
|
195
|
+
* every other member access on the class itself.
|
|
196
|
+
*/
|
|
197
|
+
function extractBuildConfigFieldRefs(content) {
|
|
198
|
+
const names = [];
|
|
199
|
+
const re = /\bBuildConfig\.([A-Z][A-Z0-9_]*)\b/g;
|
|
200
|
+
let m;
|
|
201
|
+
while ((m = re.exec(content)) !== null) {
|
|
202
|
+
if (!isAgpGeneratedField(m[1]))
|
|
203
|
+
names.push(m[1]);
|
|
204
|
+
}
|
|
205
|
+
return names;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The index of the character closing the bracket or parenthesis at `open`,
|
|
209
|
+
* ignoring anything inside a string literal. -1 when it is never closed.
|
|
210
|
+
*/
|
|
211
|
+
function matchingClose(content, open) {
|
|
212
|
+
const closing = content[open] === "[" ? "]" : ")";
|
|
213
|
+
let depth = 0;
|
|
214
|
+
let quote = "";
|
|
215
|
+
for (let i = open; i < content.length; i++) {
|
|
216
|
+
const ch = content[i];
|
|
217
|
+
if (quote) {
|
|
218
|
+
if (ch === "\\")
|
|
219
|
+
i++;
|
|
220
|
+
else if (ch === quote)
|
|
221
|
+
quote = "";
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ch === '"' || ch === "'")
|
|
225
|
+
quote = ch;
|
|
226
|
+
else if (ch === "[" || ch === "(")
|
|
227
|
+
depth++;
|
|
228
|
+
else if (ch === "]" || ch === ")") {
|
|
229
|
+
depth--;
|
|
230
|
+
if (depth === 0)
|
|
231
|
+
return content[i] === closing ? i : -1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return -1;
|
|
235
|
+
}
|
|
236
|
+
/** Split a map-literal body on its top-level commas — not ones nested or quoted. */
|
|
237
|
+
function splitTopLevel(body) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
let depth = 0;
|
|
240
|
+
let quote = "";
|
|
241
|
+
let start = 0;
|
|
242
|
+
for (let i = 0; i < body.length; i++) {
|
|
243
|
+
const ch = body[i];
|
|
244
|
+
if (quote) {
|
|
245
|
+
if (ch === "\\")
|
|
246
|
+
i++;
|
|
247
|
+
else if (ch === quote)
|
|
248
|
+
quote = "";
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === '"' || ch === "'")
|
|
252
|
+
quote = ch;
|
|
253
|
+
else if (ch === "[" || ch === "(" || ch === "{")
|
|
254
|
+
depth++;
|
|
255
|
+
else if (ch === "]" || ch === ")" || ch === "}")
|
|
256
|
+
depth--;
|
|
257
|
+
else if (ch === "," && depth === 0) {
|
|
258
|
+
parts.push(body.slice(start, i));
|
|
259
|
+
start = i + 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
parts.push(body.slice(start));
|
|
263
|
+
return parts;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The keys of a map literal, in either language's spelling:
|
|
267
|
+
* Groovy `[key: v, "key": v]`, Kotlin `mapOf("key" to v)`.
|
|
268
|
+
*/
|
|
269
|
+
function mapLiteralKeys(body) {
|
|
270
|
+
const names = [];
|
|
271
|
+
for (const entry of splitTopLevel(body)) {
|
|
272
|
+
const kotlin = /^\s*["']([^"']+)["']\s+to\s/.exec(entry);
|
|
273
|
+
if (kotlin) {
|
|
274
|
+
names.push(kotlin[1]);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const groovy = /^\s*(?:["']([^"']+)["']|([A-Za-z_]\w*))\s*:/.exec(entry);
|
|
278
|
+
if (groovy)
|
|
279
|
+
names.push(groovy[1] ?? groovy[2]);
|
|
280
|
+
}
|
|
281
|
+
return names;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Keys named in a `manifestPlaceholders` map in a build file.
|
|
285
|
+
*
|
|
286
|
+
* The Secrets plugin fills a placeholder from the property of the same name,
|
|
287
|
+
* so a placeholder the build declares is a property the build reads. Every
|
|
288
|
+
* spelling the two DSLs offer for the same map:
|
|
289
|
+
*
|
|
290
|
+
* manifestPlaceholders["mapsApiKey"] = … // both
|
|
291
|
+
* manifestPlaceholders.put("mapsApiKey", …) // Groovy
|
|
292
|
+
* manifestPlaceholders = [mapsApiKey: …] // Groovy
|
|
293
|
+
* manifestPlaceholders += mapOf("mapsApiKey" to …) // Kotlin
|
|
294
|
+
*/
|
|
295
|
+
function extractManifestPlaceholderKeys(content) {
|
|
296
|
+
const names = [];
|
|
297
|
+
for (const re of [
|
|
298
|
+
/manifestPlaceholders\s*\[\s*["']([^"']+)["']\s*\]/g,
|
|
299
|
+
/manifestPlaceholders\s*\.\s*put\s*\(\s*["']([^"']+)["']/g,
|
|
300
|
+
]) {
|
|
301
|
+
let m;
|
|
302
|
+
while ((m = re.exec(content)) !== null)
|
|
303
|
+
names.push(m[1]);
|
|
304
|
+
}
|
|
305
|
+
// A whole map assigned, added or put at once. The opening bracket the regex
|
|
306
|
+
// ends on is where the literal starts; its match is where it ends.
|
|
307
|
+
const assigned = /manifestPlaceholders\s*(?:\.\s*putAll\s*\(\s*)?(?:\+?=\s*)?(?:mapOf\s*|mutableMapOf\s*)?[[(]/g;
|
|
308
|
+
let m;
|
|
309
|
+
while ((m = assigned.exec(content)) !== null) {
|
|
310
|
+
const open = m.index + m[0].length - 1;
|
|
311
|
+
const close = matchingClose(content, open);
|
|
312
|
+
if (close === -1)
|
|
313
|
+
continue;
|
|
314
|
+
names.push(...mapLiteralKeys(content.slice(open + 1, close)));
|
|
315
|
+
assigned.lastIndex = close;
|
|
316
|
+
}
|
|
317
|
+
return names;
|
|
318
|
+
}
|
|
319
|
+
// Bounds on the source walk, the way `findGradleFiles` bounds itself to the
|
|
320
|
+
// files a build actually declares. A repository is an unbounded tree and this
|
|
321
|
+
// runs on every scan of one.
|
|
322
|
+
const SOURCE_EXTENSIONS = [".kt", ".kts", ".java"];
|
|
323
|
+
const SOURCE_SKIP_DIRS = new Set(["build", "node_modules"]);
|
|
324
|
+
const SOURCE_FILE_CAP = 1500;
|
|
325
|
+
const SOURCE_DEPTH_CAP = 12;
|
|
326
|
+
/** Source files under `dir`, up to the shared file budget. */
|
|
327
|
+
function findSourceFiles(dir, budget, depth = 0) {
|
|
328
|
+
if (depth > SOURCE_DEPTH_CAP || budget.left <= 0)
|
|
329
|
+
return [];
|
|
330
|
+
let entries;
|
|
331
|
+
try {
|
|
332
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const files = [];
|
|
338
|
+
const subdirs = [];
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
// Dot directories hold caches and VCS state, never source: `.git`,
|
|
341
|
+
// `.gradle`, `.idea`, `.kotlin`.
|
|
342
|
+
if (entry.name.startsWith("."))
|
|
343
|
+
continue;
|
|
344
|
+
if (entry.isDirectory()) {
|
|
345
|
+
if (!SOURCE_SKIP_DIRS.has(entry.name))
|
|
346
|
+
subdirs.push(join(dir, entry.name));
|
|
347
|
+
}
|
|
348
|
+
else if (SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
349
|
+
if (budget.left <= 0)
|
|
350
|
+
break;
|
|
351
|
+
budget.left--;
|
|
352
|
+
files.push(join(dir, entry.name));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const sub of subdirs)
|
|
356
|
+
files.push(...findSourceFiles(sub, budget, depth + 1));
|
|
357
|
+
return files;
|
|
358
|
+
}
|
|
359
|
+
/** The directories of `dirs` that no other member of `dirs` contains. */
|
|
360
|
+
function outermost(dirs) {
|
|
361
|
+
return dirs.filter((dir) => !dirs.some((other) => other !== dir && dir.startsWith(other + "/")));
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The property names the build reads out of the Secrets plugin's properties
|
|
365
|
+
* file — see `ScanResult.missingPropertyKeys`.
|
|
366
|
+
*
|
|
367
|
+
* Bounded to the modules that apply the plugin, because that is where its
|
|
368
|
+
* generated fields are visible and where the references to them live.
|
|
369
|
+
*/
|
|
370
|
+
function collectPropertyKeys(gradleFiles) {
|
|
371
|
+
// A module with both a `build.gradle` and a `build.gradle.kts` yields its
|
|
372
|
+
// directory twice, and walking it twice would spend the file budget twice.
|
|
373
|
+
const moduleDirs = new Set();
|
|
374
|
+
const keys = new Set();
|
|
375
|
+
for (const filePath of gradleFiles) {
|
|
376
|
+
const content = safeRead(filePath);
|
|
377
|
+
if (!appliesSecretsPlugin(content))
|
|
378
|
+
continue;
|
|
379
|
+
moduleDirs.add(dirname(filePath));
|
|
380
|
+
for (const key of extractManifestPlaceholderKeys(content))
|
|
381
|
+
keys.add(key);
|
|
382
|
+
}
|
|
383
|
+
if (moduleDirs.size === 0)
|
|
384
|
+
return [];
|
|
385
|
+
const budget = { left: SOURCE_FILE_CAP };
|
|
386
|
+
for (const dir of outermost([...moduleDirs])) {
|
|
387
|
+
for (const file of findSourceFiles(dir, budget)) {
|
|
388
|
+
for (const name of extractBuildConfigFieldRefs(safeRead(file)))
|
|
389
|
+
keys.add(name);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return [...keys].sort();
|
|
393
|
+
}
|
|
141
394
|
/** Parse property keys from a standard .properties file, skipping comments and blank lines. */
|
|
142
395
|
function parsePropertyKeys(propertiesContent) {
|
|
143
396
|
return propertiesContent
|
|
@@ -1070,6 +1323,16 @@ export async function scanAndroidProject(projectRoot) {
|
|
|
1070
1323
|
}
|
|
1071
1324
|
}
|
|
1072
1325
|
// ------------------------------------------------------------------
|
|
1326
|
+
// 4b. Which keys that properties file has to declare
|
|
1327
|
+
// ------------------------------------------------------------------
|
|
1328
|
+
//
|
|
1329
|
+
// Section 4 says the file is absent; this says what is in it. The two are
|
|
1330
|
+
// separate because they answer different questions for different readers —
|
|
1331
|
+
// the warnings above tell a person a file is missing, and this tells a
|
|
1332
|
+
// generator what a stand-in for it has to contain.
|
|
1333
|
+
// ------------------------------------------------------------------
|
|
1334
|
+
const missingPropertyKeys = collectPropertyKeys(gradleFiles);
|
|
1335
|
+
// ------------------------------------------------------------------
|
|
1073
1336
|
// 5. Firebase / GMS — check google-services.json after scanning all files
|
|
1074
1337
|
// ------------------------------------------------------------------
|
|
1075
1338
|
if (gmsDetected) {
|
|
@@ -1121,6 +1384,7 @@ export async function scanAndroidProject(projectRoot) {
|
|
|
1121
1384
|
detectedJavaVersionSource,
|
|
1122
1385
|
detectedGradleVersion,
|
|
1123
1386
|
buildVariants,
|
|
1387
|
+
missingPropertyKeys,
|
|
1124
1388
|
};
|
|
1125
1389
|
}
|
|
1126
1390
|
// ---------------------------------------------------------------------------
|
|
@@ -23,6 +23,7 @@ export { handleBuildCommand } from "./build.js";
|
|
|
23
23
|
export { handleResetCommand } from "./reset.js";
|
|
24
24
|
export { detectMobileProjectRoot } from "../shared/detect-project.js";
|
|
25
25
|
export type { MobileProjectType } from "../shared/detect-project.js";
|
|
26
|
+
export { isXcodeContainer, xcodeContainersIn } from "../shared/detect-project.js";
|
|
26
27
|
export { scanIosProject, formatIosScanResult } from "./ios-scanner.js";
|
|
27
28
|
export type { IosScanResult, IosWarning, IosWarningCategory, IosWarningSeverity, } from "./ios-scanner.js";
|
|
28
29
|
export { scanAndroidProject, formatScanResult, detectBuildVariants, } from "./android-scanner.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGrE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAOlF,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EACV,aAAa,EACb,UAAU,EACV,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,UAAU,IAAI,iBAAiB,EAC/B,aAAa,EACb,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACjF,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,aAAa,GACd,MAAM,4BAA4B,CAAC"}
|
|
@@ -19,6 +19,9 @@ export { handleBuildCommand } from "./build.js";
|
|
|
19
19
|
export { handleResetCommand } from "./reset.js";
|
|
20
20
|
// Helpers useful for callers that implement their own variations.
|
|
21
21
|
export { detectMobileProjectRoot } from "../shared/detect-project.js";
|
|
22
|
+
// The one answer to "is this `.xcodeproj` a project", exported so the Invarn
|
|
23
|
+
// CLI's own scans cannot drift from cibuild's.
|
|
24
|
+
export { isXcodeContainer, xcodeContainersIn } from "../shared/detect-project.js";
|
|
22
25
|
// Project scanners — pure, toolchain-free file-content detectors. Exposed
|
|
23
26
|
// so external callers (the Invarn CLI) can detect build variables and
|
|
24
27
|
// secret-bearing files from a checkout and push them to the backend,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/ios-scanner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ios-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/ios-scanner.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,kBAAkB,GAAG,cAAc,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC;AAE7F,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB;;;OAGG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,YAAY,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAC;CACzB;AAiFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,EAAE,CAEV;AAED,iEAAiE;AACjE,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EAAE,GAChB;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CA8CxC;AAkLD,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgGhF;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAkDjE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve, relative } from "node:path";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { xcodeContainersIn } from "../shared/detect-project.js";
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// File discovery
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -18,17 +19,16 @@ function relPath(root, filePath) {
|
|
|
18
19
|
* Finds the primary Xcode project path.
|
|
19
20
|
* Prefers .xcworkspace over .xcodeproj (CocoaPods projects use workspace).
|
|
20
21
|
* Returns the relative path from root, or empty string if not found.
|
|
22
|
+
*
|
|
23
|
+
* A candidate must be a real container — `isXcodeContainer` — and not merely
|
|
24
|
+
* a directory with the right suffix. `Uwi0/Oakane` ships `iosApp.xcodeproj`
|
|
25
|
+
* with no `project.pbxproj` beside the `oakane.xcodeproj` that has one, and
|
|
26
|
+
* this function returned the husk because it was first.
|
|
21
27
|
*/
|
|
22
28
|
function findXcodeProjectPath(root) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return "";
|
|
29
|
-
}
|
|
30
|
-
const workspaces = entries.filter((e) => e.endsWith(".xcworkspace"));
|
|
31
|
-
const projects = entries.filter((e) => e.endsWith(".xcodeproj"));
|
|
29
|
+
const containers = xcodeContainersIn(root);
|
|
30
|
+
const workspaces = containers.filter((e) => e.endsWith(".xcworkspace"));
|
|
31
|
+
const projects = containers.filter((e) => e.endsWith(".xcodeproj"));
|
|
32
32
|
// Prefer workspace (CocoaPods / multi-package setups use these)
|
|
33
33
|
if (workspaces.length > 0)
|
|
34
34
|
return workspaces[0];
|
|
@@ -43,14 +43,7 @@ function findXcodeProjectPath(root) {
|
|
|
43
43
|
*/
|
|
44
44
|
const COMPANION_SCHEME = /(extension|widget|clip|intents?|notification|tvos|macos|watchos|visionos|tests?|screenshots?|staging|prototype|codegen)/i;
|
|
45
45
|
function readSchemeFacts(root, name) {
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
entries = readdirSync(root);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
for (const entry of entries) {
|
|
46
|
+
for (const entry of xcodeContainersIn(root)) {
|
|
54
47
|
if (!entry.endsWith(".xcodeproj"))
|
|
55
48
|
continue;
|
|
56
49
|
const path = resolve(root, entry, "xcshareddata", "xcschemes", `${name}.xcscheme`);
|
|
@@ -194,13 +187,11 @@ export function rankSchemesWithReason(root, projectPath, schemes) {
|
|
|
194
187
|
*/
|
|
195
188
|
function detectSchemes(root) {
|
|
196
189
|
const schemes = new Set();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return [];
|
|
203
|
-
}
|
|
190
|
+
// Real projects only. The fallback below invents a scheme from the
|
|
191
|
+
// directory's name, so a husk with no shared schemes contributes a scheme
|
|
192
|
+
// named after a project that cannot be opened — which is how `Uwi0/Oakane`
|
|
193
|
+
// got `IOS_SCHEME: iosApp` as well as `IOS_PROJECT_PATH: iosApp.xcodeproj`.
|
|
194
|
+
const entries = xcodeContainersIn(root);
|
|
204
195
|
for (const entry of entries) {
|
|
205
196
|
if (!entry.endsWith(".xcodeproj"))
|
|
206
197
|
continue;
|