@invarn/cibuild 2.8.1 → 2.8.3
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.
|
@@ -215,4 +215,168 @@ describe("the Secrets-plugin warning path is unchanged", () => {
|
|
|
215
215
|
expect(result.missingPropertyKeys).toEqual(["MAPS_API_KEY"]);
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
|
+
/**
|
|
219
|
+
* The commonest shape of all, and the one the first three sources missed.
|
|
220
|
+
*
|
|
221
|
+
* The Secrets plugin's own documentation, and every Google Maps setup guide,
|
|
222
|
+
* puts the key in the manifest and nowhere else. There is no `BuildConfig`
|
|
223
|
+
* reference and no `manifestPlaceholders` block — the plugin substitutes the
|
|
224
|
+
* value during manifest merging. A scan reading only build files and source
|
|
225
|
+
* returns nothing for such a repository, which is the majority of them.
|
|
226
|
+
*/
|
|
227
|
+
describe("a key referenced only in AndroidManifest.xml", () => {
|
|
228
|
+
test("names a placeholder the plugin fills", async () => {
|
|
229
|
+
projectApplyingTheSecretsPlugin();
|
|
230
|
+
write("app/src/main/AndroidManifest.xml", '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n' +
|
|
231
|
+
" <application>\n" +
|
|
232
|
+
' <meta-data android:name="com.google.android.geo.API_KEY"\n' +
|
|
233
|
+
' android:value="${MAPS_API_KEY}" />\n' +
|
|
234
|
+
" </application>\n</manifest>\n");
|
|
235
|
+
expect(await keys()).toEqual(["MAPS_API_KEY"]);
|
|
236
|
+
});
|
|
237
|
+
// AGP supplies this one from the variant, so it is not a value anybody owes.
|
|
238
|
+
test("ignores ${applicationId}", async () => {
|
|
239
|
+
projectApplyingTheSecretsPlugin();
|
|
240
|
+
write("app/src/main/AndroidManifest.xml", '<manifest>\n <provider android:authorities="${applicationId}.provider" />\n</manifest>\n');
|
|
241
|
+
expect(await keys()).toEqual([]);
|
|
242
|
+
});
|
|
243
|
+
test("finds them in flavor source sets too", async () => {
|
|
244
|
+
projectApplyingTheSecretsPlugin();
|
|
245
|
+
write("app/src/main/AndroidManifest.xml", '<manifest a="${BASE_KEY}" />\n');
|
|
246
|
+
write("app/src/prod/AndroidManifest.xml", '<manifest a="${PROD_KEY}" />\n');
|
|
247
|
+
expect(await keys()).toEqual(["BASE_KEY", "PROD_KEY"]);
|
|
248
|
+
});
|
|
249
|
+
test("skips a merged manifest under build/", async () => {
|
|
250
|
+
projectApplyingTheSecretsPlugin();
|
|
251
|
+
write("app/src/main/AndroidManifest.xml", '<manifest a="${REAL_KEY}" />\n');
|
|
252
|
+
write("app/build/intermediates/merged_manifest/AndroidManifest.xml", '<manifest a="${STALE}" />\n');
|
|
253
|
+
expect(await keys()).toEqual(["REAL_KEY"]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
/**
|
|
257
|
+
* A build that loads the properties file itself, with no Secrets plugin
|
|
258
|
+
* anywhere. The file existing is not the fix for these — only the file
|
|
259
|
+
* existing *with the key in it* is, because `getProperty` on an absent key
|
|
260
|
+
* returns null whether or not the file is there.
|
|
261
|
+
*/
|
|
262
|
+
describe("a build that reads the properties file directly", () => {
|
|
263
|
+
test("names a getProperty key, with no plugin applied", async () => {
|
|
264
|
+
write("settings.gradle.kts", 'include(":app")\n');
|
|
265
|
+
write("build.gradle.kts", "// root\n");
|
|
266
|
+
write("app/build.gradle.kts", 'import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties\n' +
|
|
267
|
+
'val url: String = gradleLocalProperties(rootDir).getProperty("api.url.release")\n');
|
|
268
|
+
expect(await keys()).toEqual(["api.url.release"]);
|
|
269
|
+
});
|
|
270
|
+
test("keeps dotted keys, which a direct reader may use freely", async () => {
|
|
271
|
+
write("settings.gradle", "include ':app'\n");
|
|
272
|
+
write("build.gradle", "// root\n");
|
|
273
|
+
write("app/build.gradle", "def props = new Properties()\n" +
|
|
274
|
+
"props.load(rootProject.file('local.properties').newDataInputStream())\n" +
|
|
275
|
+
"def a = props.getProperty('api.url.debug')\n" +
|
|
276
|
+
"def b = props.getProperty('tv.isDebug')\n");
|
|
277
|
+
expect(await keys()).toEqual(["api.url.debug", "tv.isDebug"]);
|
|
278
|
+
});
|
|
279
|
+
test("honours a default argument without treating it as a key", async () => {
|
|
280
|
+
write("settings.gradle.kts", 'include(":app")\n');
|
|
281
|
+
write("build.gradle.kts", "// root\n");
|
|
282
|
+
write("app/build.gradle.kts", 'val p = java.util.Properties()\n' +
|
|
283
|
+
'p.load(rootProject.file("local.properties").reader())\n' +
|
|
284
|
+
'val u = p.getProperty("api.url.release", "")\n');
|
|
285
|
+
expect(await keys()).toEqual(["api.url.release"]);
|
|
286
|
+
});
|
|
287
|
+
/**
|
|
288
|
+
* The idiom that hides every key behind a name the repository chose. Without
|
|
289
|
+
* following it, a repository like this reads as having no keys at all.
|
|
290
|
+
*/
|
|
291
|
+
test("follows a hand-rolled accessor to its call sites", async () => {
|
|
292
|
+
write("settings.gradle", "include ':app'\n");
|
|
293
|
+
write("build.gradle", "// root\n");
|
|
294
|
+
write("app/build.gradle", "def getProps(String propName) {\n" +
|
|
295
|
+
" def propsFile = rootProject.file('local.properties')\n" +
|
|
296
|
+
" if (propsFile.exists()) {\n" +
|
|
297
|
+
" def props = new Properties()\n" +
|
|
298
|
+
" props.load(new FileInputStream(propsFile))\n" +
|
|
299
|
+
" return props[propName]\n" +
|
|
300
|
+
" } else {\n" +
|
|
301
|
+
" return \"\";\n" +
|
|
302
|
+
" }\n" +
|
|
303
|
+
"}\n" +
|
|
304
|
+
"\n" +
|
|
305
|
+
"String signFilePath = getProps(\"sign.file\")\n" +
|
|
306
|
+
"Boolean noObfuscate = getProps(\"app.dontobfuscate\")?.toBoolean() ?: false\n");
|
|
307
|
+
expect(await keys()).toEqual(["app.dontobfuscate", "sign.file"]);
|
|
308
|
+
});
|
|
309
|
+
test("does not follow a function that reads something else", async () => {
|
|
310
|
+
write("settings.gradle", "include ':app'\n");
|
|
311
|
+
write("build.gradle", "// root\n");
|
|
312
|
+
write("app/build.gradle", "def getProps(String n) {\n" +
|
|
313
|
+
" def p = new Properties()\n" +
|
|
314
|
+
" p.load(rootProject.file('local.properties').newDataInputStream())\n" +
|
|
315
|
+
" return p[n]\n" +
|
|
316
|
+
"}\n" +
|
|
317
|
+
"def version(String n) {\n" +
|
|
318
|
+
" def p = new Properties()\n" +
|
|
319
|
+
" p.load(rootProject.file('version.properties').newDataInputStream())\n" +
|
|
320
|
+
" return p[n]\n" +
|
|
321
|
+
"}\n" +
|
|
322
|
+
"def a = getProps('wanted')\n" +
|
|
323
|
+
"def b = version('unwanted')\n");
|
|
324
|
+
expect(await keys()).toEqual(["wanted"]);
|
|
325
|
+
});
|
|
326
|
+
/**
|
|
327
|
+
* `properties["x"]` is the Gradle *project* property channel —
|
|
328
|
+
* `gradle.properties` and `-P` — which the scan already reports separately.
|
|
329
|
+
* Declaring those in a properties file would be the wrong file entirely.
|
|
330
|
+
*/
|
|
331
|
+
test("does not confuse Gradle project properties for properties-file keys", async () => {
|
|
332
|
+
write("settings.gradle.kts", 'include(":app")\n');
|
|
333
|
+
write("build.gradle.kts", "// root\n");
|
|
334
|
+
write("app/build.gradle.kts", 'val p = java.util.Properties()\n' +
|
|
335
|
+
'p.load(rootProject.file("local.properties").reader())\n' +
|
|
336
|
+
'val a = p.getProperty("wanted")\n' +
|
|
337
|
+
'val b = properties["projectScoped"]\n' +
|
|
338
|
+
'val c = findProperty("alsoProjectScoped")\n');
|
|
339
|
+
expect(await keys()).toEqual(["wanted"]);
|
|
340
|
+
});
|
|
341
|
+
/** A developer's own SDK path is not a value anybody owes the build. */
|
|
342
|
+
test("never names a machine-local key", async () => {
|
|
343
|
+
write("settings.gradle.kts", 'include(":app")\n');
|
|
344
|
+
write("build.gradle.kts", "// root\n");
|
|
345
|
+
write("app/build.gradle.kts", 'val p = java.util.Properties()\n' +
|
|
346
|
+
'p.load(rootProject.file("local.properties").reader())\n' +
|
|
347
|
+
'val sdk = p.getProperty("sdk.dir")\n' +
|
|
348
|
+
'val ndk = p.getProperty("ndk.dir")\n' +
|
|
349
|
+
'val cmake = p.getProperty("cmake.dir")\n' +
|
|
350
|
+
'val real = p.getProperty("api.key")\n');
|
|
351
|
+
expect(await keys()).toEqual(["api.key"]);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
/**
|
|
355
|
+
* A scan that answers well on a small repository and thins out on a large one
|
|
356
|
+
* is the worse of the two failures, because the large one is the one someone
|
|
357
|
+
* is paid to build.
|
|
358
|
+
*
|
|
359
|
+
* Manifests and source shared a file budget at first, and source was walked
|
|
360
|
+
* first. Measured on a real application — 1743 source files, 47 manifests —
|
|
361
|
+
* the source walk spent the whole budget and the manifests were never opened,
|
|
362
|
+
* so every key referenced only as `${KEY}` in a manifest came back missing.
|
|
363
|
+
* Nine of that application's twenty declared keys were lost that way, and the
|
|
364
|
+
* scan reported the other eleven with no sign anything was wrong.
|
|
365
|
+
*
|
|
366
|
+
* This is the only test here that builds a tree big enough to exhaust a
|
|
367
|
+
* budget, because that is the only way to express the defect. It costs about
|
|
368
|
+
* half a second.
|
|
369
|
+
*/
|
|
370
|
+
describe("a repository larger than the source budget", () => {
|
|
371
|
+
test("still reads its manifests", async () => {
|
|
372
|
+
projectApplyingTheSecretsPlugin();
|
|
373
|
+
write("app/src/main/AndroidManifest.xml", '<manifest android:value="${MANIFEST_ONLY_KEY}" />\n');
|
|
374
|
+
// Past SOURCE_FILE_CAP, spread over directories so no single readdir is
|
|
375
|
+
// unrepresentative.
|
|
376
|
+
for (let i = 0; i < 8100; i++) {
|
|
377
|
+
write(`app/src/main/java/p${i % 40}/F${i}.kt`, `val x${i} = 1\n`);
|
|
378
|
+
}
|
|
379
|
+
expect(await keys()).toContain("MANIFEST_ONLY_KEY");
|
|
380
|
+
}, 60_000);
|
|
381
|
+
});
|
|
218
382
|
//# sourceMappingURL=a-properties-stand-in-declares-the-keys-the-build-reads.test.js.map
|
|
@@ -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;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;
|
|
1
|
+
{"version":3,"file":"android-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/android-scanner.ts"],"names":[],"mappings":"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"}
|
|
@@ -316,12 +316,140 @@ function extractManifestPlaceholderKeys(content) {
|
|
|
316
316
|
}
|
|
317
317
|
return names;
|
|
318
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Placeholder names referenced in an `AndroidManifest.xml`, other than the ones
|
|
321
|
+
* the Android Gradle Plugin injects itself.
|
|
322
|
+
*
|
|
323
|
+
* **This is what the Secrets plugin is mostly for.** Its own documentation, and
|
|
324
|
+
* every Google Maps setup guide, puts the key in the manifest and nowhere else:
|
|
325
|
+
*
|
|
326
|
+
* <meta-data android:name="com.google.android.geo.API_KEY"
|
|
327
|
+
* android:value="${MAPS_API_KEY}" />
|
|
328
|
+
*
|
|
329
|
+
* There is no `BuildConfig` reference and no `manifestPlaceholders` block —
|
|
330
|
+
* the plugin reads `MAPS_API_KEY` from the properties file and substitutes it
|
|
331
|
+
* during manifest merging. A scan that reads only build files and source sees
|
|
332
|
+
* nothing at all in such a repository.
|
|
333
|
+
*
|
|
334
|
+
* `applicationId` is excluded because AGP supplies it from the variant. A name
|
|
335
|
+
* a build file *declares* in `manifestPlaceholders` is not excluded: the build
|
|
336
|
+
* supplies that one, so naming it costs the usual unused line, and telling the
|
|
337
|
+
* two apart would mean resolving which module's manifest merges with which
|
|
338
|
+
* module's build file.
|
|
339
|
+
*/
|
|
340
|
+
const AGP_INJECTED_PLACEHOLDERS = new Set(["applicationId"]);
|
|
341
|
+
function extractManifestXmlPlaceholders(content) {
|
|
342
|
+
const names = [];
|
|
343
|
+
const re = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
344
|
+
let m;
|
|
345
|
+
while ((m = re.exec(content)) !== null) {
|
|
346
|
+
if (!AGP_INJECTED_PLACEHOLDERS.has(m[1]))
|
|
347
|
+
names.push(m[1]);
|
|
348
|
+
}
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* True when this build file loads the properties file itself, rather than
|
|
353
|
+
* leaving it to the Secrets plugin.
|
|
354
|
+
*
|
|
355
|
+
* Every spelling seen in the wild reduces to naming the file or calling a
|
|
356
|
+
* helper that names it: `rootProject.file("local.properties")`, AGP's own
|
|
357
|
+
* `gradleLocalProperties(rootDir)`, and Compose's `localPropertiesFile`.
|
|
358
|
+
*/
|
|
359
|
+
function readsPropertiesFileDirectly(content) {
|
|
360
|
+
return (/\blocal\.properties\b/.test(content) ||
|
|
361
|
+
/\bgradleLocalProperties\s*\(/.test(content) ||
|
|
362
|
+
/\blocalPropertiesFile\b/.test(content));
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Keys a build file reads out of a properties file it loaded itself.
|
|
366
|
+
*
|
|
367
|
+
* `getProperty("x")` is the direct form, with or without a default. The rest is
|
|
368
|
+
* for the idiom that hides it: a repository defines its own one-line accessor —
|
|
369
|
+
*
|
|
370
|
+
* def getProps(String name) {
|
|
371
|
+
* def f = rootProject.file('local.properties')
|
|
372
|
+
* if (f.exists()) { def p = new Properties(); p.load(...); return p[name] }
|
|
373
|
+
* return ""
|
|
374
|
+
* }
|
|
375
|
+
*
|
|
376
|
+
* — and every key then arrives as `getProps("sign.file")`, under a name chosen
|
|
377
|
+
* by that repository. So a function whose *body* reads the properties file is
|
|
378
|
+
* treated as an accessor for it, and the string literals at its call sites are
|
|
379
|
+
* keys. Without that, a repository like this reads as having none.
|
|
380
|
+
*
|
|
381
|
+
* `properties["x"]` is deliberately not matched: that is the Gradle *project*
|
|
382
|
+
* property channel — `gradle.properties` and `-P` — which `extractPropertyRefs`
|
|
383
|
+
* already reports, and which is not this file.
|
|
384
|
+
*/
|
|
385
|
+
function extractDirectPropertyReads(content) {
|
|
386
|
+
const names = [];
|
|
387
|
+
const direct = /\.\s*getProperty\s*\(\s*["']([^"']+)["']/g;
|
|
388
|
+
let m;
|
|
389
|
+
while ((m = direct.exec(content)) !== null)
|
|
390
|
+
names.push(m[1]);
|
|
391
|
+
for (const accessor of propertiesAccessorNames(content)) {
|
|
392
|
+
const call = new RegExp(`\\b${escapeForRegExp(accessor)}\\s*\\(\\s*["']([^"']+)["']`, "g");
|
|
393
|
+
let c;
|
|
394
|
+
while ((c = call.exec(content)) !== null)
|
|
395
|
+
names.push(c[1]);
|
|
396
|
+
}
|
|
397
|
+
return names;
|
|
398
|
+
}
|
|
399
|
+
function escapeForRegExp(value) {
|
|
400
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
401
|
+
}
|
|
402
|
+
/** Functions declared in this file whose body reads the properties file. */
|
|
403
|
+
function propertiesAccessorNames(content) {
|
|
404
|
+
const names = [];
|
|
405
|
+
// Groovy `def name(...) {` and Kotlin `fun name(...) {`, which is how a
|
|
406
|
+
// build script declares one.
|
|
407
|
+
const decl = /\b(?:def|fun)\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*(?::[^{]*)?\{/g;
|
|
408
|
+
let m;
|
|
409
|
+
while ((m = decl.exec(content)) !== null) {
|
|
410
|
+
// `matchingBrace` counts from inside the block, so hand it the index after
|
|
411
|
+
// the opening brace the declaration regex ended on.
|
|
412
|
+
const body = m.index + m[0].length;
|
|
413
|
+
const close = matchingBrace(content, body);
|
|
414
|
+
if (close === -1)
|
|
415
|
+
continue;
|
|
416
|
+
if (readsPropertiesFileDirectly(content.slice(body, close)))
|
|
417
|
+
names.push(m[1]);
|
|
418
|
+
}
|
|
419
|
+
return names;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Keys that name a location on the machine running the build, not a value the
|
|
423
|
+
* build needs supplied.
|
|
424
|
+
*
|
|
425
|
+
* Writing one into a generated file puts a developer's own SDK path onto a
|
|
426
|
+
* runner, where it is wrong — and Gradle finds the SDK from `ANDROID_HOME`
|
|
427
|
+
* when the key is absent, which is how every build on a runner resolves it.
|
|
428
|
+
* The Secrets plugin ignores `sdk.dir` for the same reason.
|
|
429
|
+
*/
|
|
430
|
+
const MACHINE_LOCAL_KEYS = new Set(["sdk.dir", "ndk.dir", "cmake.dir"]);
|
|
319
431
|
// Bounds on the source walk, the way `findGradleFiles` bounds itself to the
|
|
320
432
|
// files a build actually declares. A repository is an unbounded tree and this
|
|
321
433
|
// runs on every scan of one.
|
|
322
434
|
const SOURCE_EXTENSIONS = [".kt", ".kts", ".java"];
|
|
323
435
|
const SOURCE_SKIP_DIRS = new Set(["build", "node_modules"]);
|
|
324
|
-
|
|
436
|
+
/**
|
|
437
|
+
* **Manifests get their own budget, and a source walk cannot spend it.**
|
|
438
|
+
*
|
|
439
|
+
* They shared one at first, with source walked first, and on a real application
|
|
440
|
+
* that meant the manifests were never reached at all: 1743 source files against
|
|
441
|
+
* a 1500-file cap left nothing, and the keys referenced only as `${KEY}` in a
|
|
442
|
+
* manifest — which is most of them — came back missing. A scan that answers
|
|
443
|
+
* well on a small repository and silently thins out on a large one is the worse
|
|
444
|
+
* of the two failures, because the large one is the one someone is paid to
|
|
445
|
+
* build.
|
|
446
|
+
*
|
|
447
|
+
* There are few manifests and many source files — 47 against 1743 in that same
|
|
448
|
+
* application — so the two are not competing for a scarce resource, they were
|
|
449
|
+
* just sharing the wrong one.
|
|
450
|
+
*/
|
|
451
|
+
const SOURCE_FILE_CAP = 8000;
|
|
452
|
+
const MANIFEST_FILE_CAP = 500;
|
|
325
453
|
const SOURCE_DEPTH_CAP = 12;
|
|
326
454
|
/** Source files under `dir`, up to the shared file budget. */
|
|
327
455
|
function findSourceFiles(dir, budget, depth = 0) {
|
|
@@ -360,12 +488,49 @@ function findSourceFiles(dir, budget, depth = 0) {
|
|
|
360
488
|
function outermost(dirs) {
|
|
361
489
|
return dirs.filter((dir) => !dirs.some((other) => other !== dir && dir.startsWith(other + "/")));
|
|
362
490
|
}
|
|
491
|
+
/** Manifests under `dir`, up to the shared file budget. */
|
|
492
|
+
function findManifests(dir, budget, depth = 0) {
|
|
493
|
+
if (depth > SOURCE_DEPTH_CAP || budget.left <= 0)
|
|
494
|
+
return [];
|
|
495
|
+
let entries;
|
|
496
|
+
try {
|
|
497
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
const found = [];
|
|
503
|
+
for (const entry of entries) {
|
|
504
|
+
if (entry.name.startsWith("."))
|
|
505
|
+
continue;
|
|
506
|
+
if (entry.isDirectory()) {
|
|
507
|
+
if (!SOURCE_SKIP_DIRS.has(entry.name)) {
|
|
508
|
+
found.push(...findManifests(join(dir, entry.name), budget, depth + 1));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else if (entry.name === "AndroidManifest.xml") {
|
|
512
|
+
if (budget.left <= 0)
|
|
513
|
+
break;
|
|
514
|
+
budget.left--;
|
|
515
|
+
found.push(join(dir, entry.name));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return found;
|
|
519
|
+
}
|
|
363
520
|
/**
|
|
364
|
-
* The property names the build reads out of the
|
|
365
|
-
*
|
|
521
|
+
* The property names the build reads out of the properties file — see
|
|
522
|
+
* `ScanResult.missingPropertyKeys`. Four sources, because a repository may use
|
|
523
|
+
* any of them and most use exactly one:
|
|
524
|
+
*
|
|
525
|
+
* - `${KEY}` in an `AndroidManifest.xml`, which the Secrets plugin fills. The
|
|
526
|
+
* commonest of the four by a distance, and the one its documentation shows.
|
|
527
|
+
* - `manifestPlaceholders` declared in a build file.
|
|
528
|
+
* - `BuildConfig.<FIELD>` in source, for a key read as a generated constant.
|
|
529
|
+
* - `getProperty("key")` in a build file that loads the properties file
|
|
530
|
+
* itself, for a build that does not use the plugin at all.
|
|
366
531
|
*
|
|
367
|
-
* Bounded to the modules that apply the plugin
|
|
368
|
-
*
|
|
532
|
+
* Bounded to the modules that either apply the plugin or read the file
|
|
533
|
+
* directly. A module that does neither cannot be reading a key out of it.
|
|
369
534
|
*/
|
|
370
535
|
function collectPropertyKeys(gradleFiles) {
|
|
371
536
|
// A module with both a `build.gradle` and a `build.gradle.kts` yields its
|
|
@@ -374,21 +539,35 @@ function collectPropertyKeys(gradleFiles) {
|
|
|
374
539
|
const keys = new Set();
|
|
375
540
|
for (const filePath of gradleFiles) {
|
|
376
541
|
const content = safeRead(filePath);
|
|
377
|
-
|
|
542
|
+
const plugin = appliesSecretsPlugin(content);
|
|
543
|
+
const direct = readsPropertiesFileDirectly(content);
|
|
544
|
+
if (!plugin && !direct)
|
|
378
545
|
continue;
|
|
379
546
|
moduleDirs.add(dirname(filePath));
|
|
380
547
|
for (const key of extractManifestPlaceholderKeys(content))
|
|
381
548
|
keys.add(key);
|
|
549
|
+
if (direct)
|
|
550
|
+
for (const key of extractDirectPropertyReads(content))
|
|
551
|
+
keys.add(key);
|
|
382
552
|
}
|
|
383
553
|
if (moduleDirs.size === 0)
|
|
384
554
|
return [];
|
|
385
|
-
const
|
|
555
|
+
const sourceBudget = { left: SOURCE_FILE_CAP };
|
|
556
|
+
const manifestBudget = { left: MANIFEST_FILE_CAP };
|
|
386
557
|
for (const dir of outermost([...moduleDirs])) {
|
|
387
|
-
for
|
|
558
|
+
// Manifests first. They are the likeliest place for a key and the cheapest
|
|
559
|
+
// to read, so if anything is ever going to run short it must not be these.
|
|
560
|
+
for (const manifest of findManifests(dir, manifestBudget)) {
|
|
561
|
+
for (const name of extractManifestXmlPlaceholders(safeRead(manifest)))
|
|
562
|
+
keys.add(name);
|
|
563
|
+
}
|
|
564
|
+
for (const file of findSourceFiles(dir, sourceBudget)) {
|
|
388
565
|
for (const name of extractBuildConfigFieldRefs(safeRead(file)))
|
|
389
566
|
keys.add(name);
|
|
390
567
|
}
|
|
391
568
|
}
|
|
569
|
+
for (const key of MACHINE_LOCAL_KEYS)
|
|
570
|
+
keys.delete(key);
|
|
392
571
|
return [...keys].sort();
|
|
393
572
|
}
|
|
394
573
|
/** Parse property keys from a standard .properties file, skipping comments and blank lines. */
|