Skip to content

Commit 9ee3729

Browse files
authored
Add support of '--min-api' d8 argument (#1000)
1 parent df6a828 commit 9ee3729

7 files changed

Lines changed: 118 additions & 0 deletions

File tree

server/src/main/java/com/defold/extender/Extender.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,6 +2199,9 @@ private File[] buildClassesDex(List<String> jars, File mainDexList) throws Exten
21992199
context.put("jars", jars);
22002200
context.put("engineJars", empty_list);
22012201
context.put("mainDexList", mainDexList.getAbsolutePath());
2202+
// Always bound, also for engines that don't send it, so that a build.yml referencing
2203+
// '--min-api {{minAndroidSdkVersion}}' can never render the flag without its argument.
2204+
context.put("minAndroidSdkVersion", buildState.getMinAndroidSdkVersion());
22022205

22032206
// replace parameter name because '--main-dex-list' is deprecated and reported as error
22042207
// we can't change command format for older version of engine so replace parameter here.

server/src/main/java/com/defold/extender/ExtenderBuildState.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public class ExtenderBuildState {
88
static final String APPMANIFEST_BUILD_ARTIFACTS_KEYWORD = "buildArtifacts";
99
static final String APPMANIFEST_JETIFIER_KEYWORD = "jetifier";
1010
static final String APPMANIFEST_DEBUG_SOURCE_PATH = "debugSourcePath";
11+
static final String APPMANIFEST_MIN_ANDROID_SDK_VERSION_KEYWORD = "minAndroidSdkVersion";
12+
13+
static final int DEFAULT_MIN_ANDROID_SDK_VERSION = 21;
1114

1215
File jobDir;
1316
File uploadDir;
@@ -19,6 +22,7 @@ public class ExtenderBuildState {
1922
String hostPlatform;
2023
private final String buildArtifacts;
2124
private final String debugSourcePath;
25+
private final int minAndroidSdkVersion;
2226

2327
private final Boolean withSymbols;
2428
private final Boolean useJetifier;
@@ -37,6 +41,7 @@ public class ExtenderBuildState {
3741
this.withSymbols = ExtenderUtil.getAppManifestContextBoolean(appManifest, APPMANIFEST_WITH_SYMBOLS_KEYWORD, true);
3842
this.buildArtifacts = ExtenderUtil.getAppManifestContextString(appManifest, APPMANIFEST_BUILD_ARTIFACTS_KEYWORD, "");
3943
this.debugSourcePath = ExtenderUtil.getAppManifestContextString(appManifest, APPMANIFEST_DEBUG_SOURCE_PATH, null);
44+
this.minAndroidSdkVersion = ExtenderUtil.getAppManifestContextInteger(appManifest, APPMANIFEST_MIN_ANDROID_SDK_VERSION_KEYWORD, DEFAULT_MIN_ANDROID_SDK_VERSION);
4045
// assign configuration names started with upper letter because it used for cocoapods
4146
if (baseVariant != null && (baseVariant.equals("release") || baseVariant.equals("headless"))) {
4247
this.buildConfiguration = "Release";
@@ -101,6 +106,10 @@ public String getDebugSourcePath() {
101106
return debugSourcePath;
102107
}
103108

109+
public int getMinAndroidSdkVersion() {
110+
return minAndroidSdkVersion;
111+
}
112+
104113
public Boolean isNeedSymbols() {
105114
return withSymbols;
106115
}

server/src/main/java/com/defold/extender/ExtenderUtil.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,25 @@ static String getAppManifestContextString(AppManifestConfiguration manifest, Str
411411
return default_value;
412412
}
413413

414+
// The app manifest context is generated by bob without quoting, so an integer written there is
415+
// parsed by snakeyaml as an Integer, while a quoted one arrives as a String. Accept both, and
416+
// fall back to the default for anything else (including older clients that omit the key).
417+
static Integer getAppManifestContextInteger(AppManifestConfiguration manifest, String name, Integer default_value) throws ExtenderException {
418+
Object o = getAppManifestContextObject(manifest, name);
419+
if (o instanceof Integer) {
420+
return (Integer)o;
421+
}
422+
if (o instanceof String) {
423+
try {
424+
return Integer.valueOf(((String)o).trim());
425+
} catch (NumberFormatException e) {
426+
throw new ExtenderException(String.format(
427+
"Error in app.manifest: '%s' must be an integer, got '%s'.", name, o));
428+
}
429+
}
430+
return default_value;
431+
}
432+
414433
static public boolean isListOfStrings(List<Object> list) {
415434
return list != null && list.stream().allMatch(o -> o instanceof String);
416435
}

server/src/main/java/com/defold/extender/ExtensionManifestValidator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ExtensionManifestValidator {
2121

2222
private static final Pattern VALID_INCLUDE_PATH = Pattern.compile("^[A-Za-z0-9._+\\-/]+$");
2323
private static final Pattern VALID_SYMBOL_IDENTIFIER = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
24+
private static final Pattern VALID_API_LEVEL = Pattern.compile("^[0-9]+$");
2425

2526
ExtensionManifestValidator(WhitelistConfig whitelistConfig, List<String> allowedFlags, List<String> allowedSymbols) {
2627
this.allowedDefines.add(WhitelistConfig.compile(whitelistConfig.defineRe));
@@ -57,6 +58,16 @@ void validateAppManifestContext(Map<String, Object> appContext) throws ExtenderE
5758
ExtenderBuildState.APPMANIFEST_DEBUG_SOURCE_PATH, s));
5859
}
5960
}
61+
62+
// Reaches the command line as the argument of d8 --min-api, so it must be a bare number.
63+
Object minAndroidSdkVersion = appContext.get(ExtenderBuildState.APPMANIFEST_MIN_ANDROID_SDK_VERSION_KEYWORD);
64+
if (minAndroidSdkVersion != null && !(minAndroidSdkVersion instanceof Integer)) {
65+
if (!(minAndroidSdkVersion instanceof String) || !VALID_API_LEVEL.matcher((String) minAndroidSdkVersion).matches()) {
66+
throw new ExtenderException(String.format(
67+
"Error in app.manifest: '%s' must be an integer, got '%s'.",
68+
ExtenderBuildState.APPMANIFEST_MIN_ANDROID_SDK_VERSION_KEYWORD, minAndroidSdkVersion));
69+
}
70+
}
6071
}
6172

6273
private void validateIncludePaths(String extensionName, File extensionFolder, List<String> includes) throws ExtenderException {

server/src/test/java/com/defold/extender/ExtenderUtilTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,4 +376,28 @@ public void testSanitizeJavacCmdFlagElsewhere() {
376376
String cmd = "javac -source 11 -proc:none Foo.java";
377377
assertEquals(cmd, ExtenderUtil.sanitizeJavacCmd(cmd));
378378
}
379+
380+
@Test
381+
public void testGetAppManifestContextInteger() throws ExtenderException {
382+
// Exercised with the minAndroidSdkVersion key, the d8 --min-api value
383+
AppManifestConfiguration manifest = new AppManifestConfiguration();
384+
385+
// Engines older than the --min-api change send no context at all, or a context without the
386+
// key. Both must fall back to the default instead of failing the build.
387+
assertEquals(Integer.valueOf(21), ExtenderUtil.getAppManifestContextInteger(manifest, "minAndroidSdkVersion", 21));
388+
manifest.context = new HashMap<>();
389+
assertEquals(Integer.valueOf(21), ExtenderUtil.getAppManifestContextInteger(manifest, "minAndroidSdkVersion", 21));
390+
391+
// bob writes the value unquoted, so snakeyaml parses it as an Integer
392+
manifest.context.put("minAndroidSdkVersion", 24);
393+
assertEquals(Integer.valueOf(24), ExtenderUtil.getAppManifestContextInteger(manifest, "minAndroidSdkVersion", 21));
394+
395+
// A quoted value arrives as a String
396+
manifest.context.put("minAndroidSdkVersion", "24");
397+
assertEquals(Integer.valueOf(24), ExtenderUtil.getAppManifestContextInteger(manifest, "minAndroidSdkVersion", 21));
398+
399+
manifest.context.put("minAndroidSdkVersion", "not-a-number");
400+
assertThrows(ExtenderException.class,
401+
() -> ExtenderUtil.getAppManifestContextInteger(manifest, "minAndroidSdkVersion", 21));
402+
}
379403
}

server/src/test/java/com/defold/extender/ExtensionManifestValidatorTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,44 @@ public void testValidateAppManifestContextDebugSourcePath() throws ExtenderExcep
274274
}
275275
}
276276

277+
@Test
278+
public void testValidateAppManifestContextMinAndroidSdkVersion() throws ExtenderException {
279+
List<String> empty = new ArrayList<>();
280+
ExtensionManifestValidator validator = new ExtensionManifestValidator(new WhitelistConfig(), empty, empty);
281+
282+
// Missing key is accepted: engines older than the --min-api change never send it
283+
assertDoesNotThrow(() -> validator.validateAppManifestContext(new HashMap<>()));
284+
285+
// bob writes the value unquoted, so snakeyaml hands us an Integer; a quoted one is a String
286+
for (Object v : new Object[] { 21, 24, "21", "24" }) {
287+
Map<String, Object> ctx = new HashMap<>();
288+
ctx.put("minAndroidSdkVersion", v);
289+
assertDoesNotThrow(() -> validator.validateAppManifestContext(ctx),
290+
"expected to accept minAndroidSdkVersion: " + v);
291+
}
292+
293+
// The value ends up as the argument of d8 --min-api, and the rendered command is split on
294+
// spaces, so anything that is not a bare number is argv injection
295+
Object[] bad = new Object[] {
296+
"24 --output /tmp/evil",
297+
"24;rm -rf /",
298+
"24$(whoami)",
299+
"-1",
300+
"",
301+
"twentyfour",
302+
true,
303+
};
304+
for (Object v : bad) {
305+
Map<String, Object> ctx = new HashMap<>();
306+
ctx.put("minAndroidSdkVersion", v);
307+
ExtenderException exc = assertThrows(ExtenderException.class,
308+
() -> validator.validateAppManifestContext(ctx),
309+
"expected rejection of minAndroidSdkVersion: " + v);
310+
assertTrue(exc.getMessage().contains("minAndroidSdkVersion"),
311+
"message should mention minAndroidSdkVersion, got: " + exc.getMessage());
312+
}
313+
}
314+
277315
@Test
278316
public void testValidateSymbols() throws ExtenderException {
279317
List<String> empty = new ArrayList<>();

server/src/test/java/com/defold/extender/TemplateExecutorTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,18 @@ public void templateVariablesShouldBeReplacedByContext() {
1919
assertThat(result).isEqualTo("Hello James!");
2020
}
2121

22+
@Test
23+
public void minAndroidSdkVersionShouldRenderAsASingleArgumentToMinApi() {
24+
// The rendered command is split on spaces, so an unbound or multi-token
25+
// minAndroidSdkVersion would make d8 read the next flag as the value of --min-api.
26+
TemplateExecutor templateExecutor = new TemplateExecutor();
27+
String template = "d8 --min-api {{minAndroidSdkVersion}} --main-dex-rules {{mainDexList}}";
28+
Map<String, Object> context = new HashMap<>();
29+
context.put("minAndroidSdkVersion", 24);
30+
context.put("mainDexList", "/tmp/main.rules");
31+
String result = templateExecutor.execute(template, context);
32+
assertThat(result).isEqualTo("d8 --min-api 24 --main-dex-rules /tmp/main.rules");
33+
assertThat(result.split(" ")).hasSize(5);
34+
}
35+
2236
}

0 commit comments

Comments
 (0)