@invarn/cibuild 2.8.1 → 2.8.2
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,140 @@ 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
|
+
});
|
|
218
354
|
//# 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,6 +316,118 @@ 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.
|
|
@@ -360,12 +472,49 @@ function findSourceFiles(dir, budget, depth = 0) {
|
|
|
360
472
|
function outermost(dirs) {
|
|
361
473
|
return dirs.filter((dir) => !dirs.some((other) => other !== dir && dir.startsWith(other + "/")));
|
|
362
474
|
}
|
|
475
|
+
/** Manifests under `dir`, up to the shared file budget. */
|
|
476
|
+
function findManifests(dir, budget, depth = 0) {
|
|
477
|
+
if (depth > SOURCE_DEPTH_CAP || budget.left <= 0)
|
|
478
|
+
return [];
|
|
479
|
+
let entries;
|
|
480
|
+
try {
|
|
481
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
const found = [];
|
|
487
|
+
for (const entry of entries) {
|
|
488
|
+
if (entry.name.startsWith("."))
|
|
489
|
+
continue;
|
|
490
|
+
if (entry.isDirectory()) {
|
|
491
|
+
if (!SOURCE_SKIP_DIRS.has(entry.name)) {
|
|
492
|
+
found.push(...findManifests(join(dir, entry.name), budget, depth + 1));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else if (entry.name === "AndroidManifest.xml") {
|
|
496
|
+
if (budget.left <= 0)
|
|
497
|
+
break;
|
|
498
|
+
budget.left--;
|
|
499
|
+
found.push(join(dir, entry.name));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return found;
|
|
503
|
+
}
|
|
363
504
|
/**
|
|
364
|
-
* The property names the build reads out of the
|
|
365
|
-
*
|
|
505
|
+
* The property names the build reads out of the properties file — see
|
|
506
|
+
* `ScanResult.missingPropertyKeys`. Four sources, because a repository may use
|
|
507
|
+
* any of them and most use exactly one:
|
|
508
|
+
*
|
|
509
|
+
* - `${KEY}` in an `AndroidManifest.xml`, which the Secrets plugin fills. The
|
|
510
|
+
* commonest of the four by a distance, and the one its documentation shows.
|
|
511
|
+
* - `manifestPlaceholders` declared in a build file.
|
|
512
|
+
* - `BuildConfig.<FIELD>` in source, for a key read as a generated constant.
|
|
513
|
+
* - `getProperty("key")` in a build file that loads the properties file
|
|
514
|
+
* itself, for a build that does not use the plugin at all.
|
|
366
515
|
*
|
|
367
|
-
* Bounded to the modules that apply the plugin
|
|
368
|
-
*
|
|
516
|
+
* Bounded to the modules that either apply the plugin or read the file
|
|
517
|
+
* directly. A module that does neither cannot be reading a key out of it.
|
|
369
518
|
*/
|
|
370
519
|
function collectPropertyKeys(gradleFiles) {
|
|
371
520
|
// A module with both a `build.gradle` and a `build.gradle.kts` yields its
|
|
@@ -374,11 +523,16 @@ function collectPropertyKeys(gradleFiles) {
|
|
|
374
523
|
const keys = new Set();
|
|
375
524
|
for (const filePath of gradleFiles) {
|
|
376
525
|
const content = safeRead(filePath);
|
|
377
|
-
|
|
526
|
+
const plugin = appliesSecretsPlugin(content);
|
|
527
|
+
const direct = readsPropertiesFileDirectly(content);
|
|
528
|
+
if (!plugin && !direct)
|
|
378
529
|
continue;
|
|
379
530
|
moduleDirs.add(dirname(filePath));
|
|
380
531
|
for (const key of extractManifestPlaceholderKeys(content))
|
|
381
532
|
keys.add(key);
|
|
533
|
+
if (direct)
|
|
534
|
+
for (const key of extractDirectPropertyReads(content))
|
|
535
|
+
keys.add(key);
|
|
382
536
|
}
|
|
383
537
|
if (moduleDirs.size === 0)
|
|
384
538
|
return [];
|
|
@@ -388,7 +542,13 @@ function collectPropertyKeys(gradleFiles) {
|
|
|
388
542
|
for (const name of extractBuildConfigFieldRefs(safeRead(file)))
|
|
389
543
|
keys.add(name);
|
|
390
544
|
}
|
|
545
|
+
for (const manifest of findManifests(dir, budget)) {
|
|
546
|
+
for (const name of extractManifestXmlPlaceholders(safeRead(manifest)))
|
|
547
|
+
keys.add(name);
|
|
548
|
+
}
|
|
391
549
|
}
|
|
550
|
+
for (const key of MACHINE_LOCAL_KEYS)
|
|
551
|
+
keys.delete(key);
|
|
392
552
|
return [...keys].sort();
|
|
393
553
|
}
|
|
394
554
|
/** Parse property keys from a standard .properties file, skipping comments and blank lines. */
|