Add command for generating the RuntimeEnabledSdkConfig message. This is used in an app bundle to reference the sandboxed SDK dependencies and how they are signed. This is later read by app stores to properly install SDK dependencies on devices (or create split APKs with SDK dependencies). Note that the config message needs to have a `resource id` for every SDK, which represents the first byte of the package IDs for the SDK. Low values for this ID can cause problems on API <26 (P and below) so we need to start from a base value (0x7F) and decrease from there, enforcing a low limit (50 SDKs). On Android O and above we can use the full byte starting from the base (0x7F). Since this logic depends on the minSdkVersion, the command receives the AAPT manifest dump of the app. PiperOrigin-RevId: 573756689 Change-Id: I4830042f8356f9a774822f2efedf9fc3011f8221
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD index d9a43c3..edc4a11 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD
@@ -15,6 +15,7 @@ deps = [ "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors", "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/clientsources", + "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig", "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest", "@rules_android_maven//:info_picocli_picocli", ],
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java index ee9607e..3a778f9 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java
@@ -18,6 +18,7 @@ import com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors.ExtractApiDescriptorsCommand; import com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors.ExtractApiDescriptorsFromAsarCommand; import com.google.devtools.build.android.sandboxedsdktoolbox.clientsources.GenerateClientSourcesCommand; +import com.google.devtools.build.android.sandboxedsdktoolbox.runtimeenabledsdkconfig.GenerateRuntimeEnabledSdkConfigCommand; import com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest.GenerateSdkDependenciesManifestCommand; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -28,8 +29,9 @@ subcommands = { ExtractApiDescriptorsCommand.class, ExtractApiDescriptorsFromAsarCommand.class, - GenerateSdkDependenciesManifestCommand.class, GenerateClientSourcesCommand.class, + GenerateRuntimeEnabledSdkConfigCommand.class, + GenerateSdkDependenciesManifestCommand.class, }) public final class SandboxedSdkToolbox {
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfo.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfo.java index 867e55d..9eacadd 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfo.java +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfo.java
@@ -15,7 +15,10 @@ */ package com.google.devtools.build.android.sandboxedsdktoolbox.info; +import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; +import com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder; import java.util.Objects; +import java.util.Optional; /** * Information about a Sandboxed SDK. Used to define an SDK dependency and read from an SDK archive @@ -24,11 +27,22 @@ public final class SdkInfo { private final String packageName; - private final long versionMajor; + private final RuntimeEnabledSdkVersion version; + private final Optional<String> certificateDigest; - SdkInfo(String packageName, long versionMajor) { + SdkInfo(String packageName, RuntimeEnabledSdkVersion version) { + this(packageName, version, Optional.empty()); + } + + SdkInfo(String packageName, RuntimeEnabledSdkVersion version, String certificateDigest) { + this(packageName, version, Optional.of(certificateDigest)); + } + + private SdkInfo( + String packageName, RuntimeEnabledSdkVersion version, Optional<String> certificateDigest) { this.packageName = packageName; - this.versionMajor = versionMajor; + this.version = version; + this.certificateDigest = certificateDigest; } /** The SDK unique package name. */ @@ -36,26 +50,45 @@ return packageName; } + public RuntimeEnabledSdkVersion getVersion() { + return version; + } + /** - * The SDK versionMajor. This value is constructed from the full SDK version description and it - * represents the actual version of the SDK as used by the package manager later. The major and - * minor versions are merged and the patch version is ignored. + * The SDK encoded version major-minor. + * + * <p>This value is constructed from the full SDK version description and it represents the actual + * version of the SDK as used by the package manager later. The major and minor versions are + * merged and the patch version is ignored. */ - public long getVersionMajor() { - return versionMajor; + public long getEncodedVersionMajorMinor() { + return RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion( + version.getMajor(), version.getMinor()); + } + + /** + * Digest of certificate used to sign this SDK's APKs. + * + * <p>Might be missing if the certificate is not known at the time, for example for SDKs signed + * with debug keys for local deployment. + */ + public Optional<String> getCertificateDigest() { + return certificateDigest; } @Override public boolean equals(Object object) { if (object instanceof SdkInfo) { SdkInfo that = (SdkInfo) object; - return this.packageName.equals(that.packageName) && this.versionMajor == that.versionMajor; + return this.packageName.equals(that.packageName) + && this.version.equals(that.version) + && this.certificateDigest.equals(that.certificateDigest); } return false; } @Override public int hashCode() { - return Objects.hash(packageName, versionMajor); + return Objects.hash(packageName, version, certificateDigest); } }
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfoReader.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfoReader.java index 7efef66..43a81b2 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfoReader.java +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info/SdkInfoReader.java
@@ -16,9 +16,7 @@ package com.google.devtools.build.android.sandboxedsdktoolbox.info; import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; -import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; -import com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.util.JsonFormat; import java.io.IOException; @@ -40,8 +38,7 @@ SdkModulesConfig.Builder modulesConfig = SdkModulesConfig.newBuilder(); try { JsonFormat.parser().merge(Files.newBufferedReader(sdkModulesConfigPath), modulesConfig); - return new SdkInfo( - modulesConfig.getSdkPackageName(), getVersionMajor(modulesConfig.getSdkVersion())); + return new SdkInfo(modulesConfig.getSdkPackageName(), modulesConfig.getSdkVersion()); } catch (IOException e) { throw new UncheckedIOException("Failed to parse SDK Module Config.", e); } @@ -58,16 +55,12 @@ SdkMetadata metadata = SdkMetadata.parseFrom( Files.readAllBytes(metadataInAsar), ExtensionRegistry.getEmptyRegistry()); - return new SdkInfo(metadata.getPackageName(), getVersionMajor(metadata.getSdkVersion())); + return new SdkInfo( + metadata.getPackageName(), metadata.getSdkVersion(), metadata.getCertificateDigest()); } catch (IOException e) { - throw new UncheckedIOException("Failed to extract SDK API descriptors.", e); + throw new UncheckedIOException("Failed to read SDK archive.", e); } } - private static long getVersionMajor(RuntimeEnabledSdkVersion version) { - return RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion( - version.getMajor(), version.getMinor()); - } - private SdkInfoReader() {} }
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/BUILD new file mode 100644 index 0000000..cade12f --- /dev/null +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/BUILD
@@ -0,0 +1,18 @@ +# Package for mixins with shared logic between commands. + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//src:__subpackages__"], +) + +licenses(["notice"]) + +java_library( + name = "mixin", + srcs = glob(["*.java"]), + deps = [ + "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info", + "@rules_android_maven//:com_google_guava_guava", + "@rules_android_maven//:info_picocli_picocli", + ], +)
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/CertificateDigestGenerator.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/CertificateDigestGenerator.java similarity index 96% rename from src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/CertificateDigestGenerator.java rename to src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/CertificateDigestGenerator.java index f8ad00b..77a7d14 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/CertificateDigestGenerator.java +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/CertificateDigestGenerator.java
@@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest; +package com.google.devtools.build.android.sandboxedsdktoolbox.mixin; import static java.util.stream.Collectors.joining;
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/SdkDependenciesCommandMixin.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/SdkDependenciesCommandMixin.java new file mode 100644 index 0000000..38d1c75 --- /dev/null +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin/SdkDependenciesCommandMixin.java
@@ -0,0 +1,87 @@ +/* + * Copyright 2023 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.devtools.build.android.sandboxedsdktoolbox.mixin; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.devtools.build.android.sandboxedsdktoolbox.mixin.CertificateDigestGenerator.generateCertificateDigest; + +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.android.sandboxedsdktoolbox.info.SdkInfo; +import com.google.devtools.build.android.sandboxedsdktoolbox.info.SdkInfoReader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import picocli.CommandLine.Option; + +/** + * Parses command line options that describe SDK dependencies, coming from SDK archives or bundles. + */ +public final class SdkDependenciesCommandMixin { + + private List<Path> sdkModuleConfigPaths = new ArrayList<>(); + private List<Path> sdkArchivePaths = new ArrayList<>(); + private Path debugKeystorePath; + private String debugKeystorePassword; + private String debugKeystoreAlias; + + public ImmutableSet<SdkInfo> getSdkDependencies() { + checkValid(); + + return Stream.concat( + sdkModuleConfigPaths.stream().map(SdkInfoReader::readFromSdkModuleJsonFile), + sdkArchivePaths.stream().map(SdkInfoReader::readFromSdkArchive)) + .collect(toImmutableSet()); + } + + public String getDebugCertificateDigest() { + return generateCertificateDigest(debugKeystorePath, debugKeystorePassword, debugKeystoreAlias); + } + + @Option(names = "--sdk-module-configs", split = ",", required = false) + void setSdkModuleConfigPaths(List<Path> sdkModuleConfigPaths) { + this.sdkModuleConfigPaths = sdkModuleConfigPaths; + } + + @Option(names = "--sdk-archives", split = ",", required = false) + void setSdkArchivePaths(List<Path> sdkArchivePaths) { + this.sdkArchivePaths = sdkArchivePaths; + } + + @Option(names = "--debug-keystore", required = true) + void setDebugKeystorePath(Path debugKeystorePath) { + this.debugKeystorePath = debugKeystorePath; + } + + @Option(names = "--debug-keystore-pass", required = true) + void setDebugKeystorePassword(String debugKeystorePassword) { + this.debugKeystorePassword = debugKeystorePassword; + } + + @Option(names = "--debug-keystore-alias", required = true) + void setDebugKeystoreAlias(String debugKeystoreAlias) { + this.debugKeystoreAlias = debugKeystoreAlias; + } + + private void checkValid() { + if (sdkModuleConfigPaths.isEmpty() && sdkArchivePaths.isEmpty()) { + throw new IllegalArgumentException( + "At least one of --sdk-module-configs or --sdk-archives must be specified."); + } + } + + private SdkDependenciesCommandMixin() {} +}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/BUILD new file mode 100644 index 0000000..93c3a18 --- /dev/null +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/BUILD
@@ -0,0 +1,20 @@ +# Command to generate the RuntimeEnabledSdkConfig file for app bundles. + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:__subpackages__"], +) + +licenses(["notice"]) + +java_library( + name = "runtimeenabledsdkconfig", + srcs = glob(["*.java"]), + deps = [ + "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info", + "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin", + "@rules_android_maven//:com_android_tools_build_bundletool", + "@rules_android_maven//:com_google_guava_guava", + "@rules_android_maven//:info_picocli_picocli", + ], +)
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/GenerateRuntimeEnabledSdkConfigCommand.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/GenerateRuntimeEnabledSdkConfigCommand.java new file mode 100644 index 0000000..91b87b0 --- /dev/null +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/GenerateRuntimeEnabledSdkConfigCommand.java
@@ -0,0 +1,107 @@ +/* + * Copyright 2023 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.devtools.build.android.sandboxedsdktoolbox.runtimeenabledsdkconfig; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; +import com.google.devtools.build.android.sandboxedsdktoolbox.info.SdkInfo; +import com.google.devtools.build.android.sandboxedsdktoolbox.mixin.SdkDependenciesCommandMixin; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** Command for extracting API descriptors from a sandboxed SDK Archive. */ +@Command( + name = "generate-runtime-enabled-sdk-config", + description = + "Generate the RuntimeEnabledSdkConfig file for app bundles that depend on sandboxed SDKs.") +public final class GenerateRuntimeEnabledSdkConfigCommand implements Runnable { + + private static final Pattern MIN_SDK_VERSION_PATTERN = + Pattern.compile( + "http:\\/\\/schemas.android.com\\/apk\\/res\\/android:minSdkVersion\\(.*\\)=(\\d*)"); + + @Option(names = "--output-config", required = true) + Path outputConfigPath; + + @Option( + names = "--manifest-xml-tree", + description = "Path to the manifest xml tree file, as generated by AAPT2 dump command.", + required = true) + Path manifestXmlTreePath; + + @Mixin SdkDependenciesCommandMixin sdkDependenciesMixin; + + @Override + public void run() { + Optional<Integer> maybeMinSdkVersion = extractMinSdkVersion(); + checkArgument( + maybeMinSdkVersion.isPresent(), "Min SDK version missing from manifest xml tree file."); + int minSdkVersion = maybeMinSdkVersion.get(); + + ResourceIdGenerator generator = new ResourceIdGenerator(minSdkVersion); + checkArgument( + sdkDependenciesMixin.getSdkDependencies().size() <= generator.maxResourceIds(), + "Too many SDK dependencies (%s). For apps with min SDK 26 and above the maximum is 127. " + + "For older versions it's 50.", + sdkDependenciesMixin.getSdkDependencies().size()); + + String debugCertificateDigest = sdkDependenciesMixin.getDebugCertificateDigest(); + + RuntimeEnabledSdkConfig.Builder builder = RuntimeEnabledSdkConfig.newBuilder(); + for (SdkInfo sdkInfo : sdkDependenciesMixin.getSdkDependencies()) { + builder.addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName(sdkInfo.getPackageName()) + .setVersionMajor(sdkInfo.getVersion().getMajor()) + .setVersionMinor(sdkInfo.getVersion().getMinor()) + .setBuildTimeVersionPatch(sdkInfo.getVersion().getPatch()) + .setCertificateDigest(sdkInfo.getCertificateDigest().orElse(debugCertificateDigest)) + .setResourcesPackageId(generator.nextResourceId())); + } + + try { + Files.write(outputConfigPath, builder.build().toByteArray()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to write runtime-enabled SDK config file.", e); + } + } + + private Optional<Integer> extractMinSdkVersion() { + try (BufferedReader reader = Files.newBufferedReader(manifestXmlTreePath)) { + return reader + .lines() + .map(MIN_SDK_VERSION_PATTERN::matcher) + .filter(Matcher::find) + .map(matcher -> Integer.parseInt(matcher.group(1))) + .findFirst(); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid manifest xml tree file.", e); + } + } + + private GenerateRuntimeEnabledSdkConfigCommand() {} +}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/ResourceIdGenerator.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/ResourceIdGenerator.java new file mode 100644 index 0000000..cf0df9b --- /dev/null +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/ResourceIdGenerator.java
@@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.devtools.build.android.sandboxedsdktoolbox.runtimeenabledsdkconfig; + +/** + * Generator for resource package IDs. This is a prefix byte added to resource IDs. + * + * <p>This ID needs to be generated slightly differently based on the minSdkVersion used by the app. + */ +final class ResourceIdGenerator { + private static final int BASE_RESOURCE_ID = 0x7F; + private static final int ANDROID_O_SDK_VERSION = 26; + + private final int minSdkVersion; + private int currentResourceId; + + ResourceIdGenerator(int minSdkVersion) { + this.minSdkVersion = minSdkVersion; + if (isOlderThanAndroidO()) { + currentResourceId = BASE_RESOURCE_ID - 1; + } else { + currentResourceId = BASE_RESOURCE_ID + 1; + } + } + + int maxResourceIds() { + // Mirrors Android Gradle Plugin implementation of resource IDs for splits: + // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/featuresplit/FeatureSetMetadata.kt;drc=b1afd3d7dfa38875ff7950b65bd58f6a79e74374 + return isOlderThanAndroidO() ? 50 : 127; + } + + int nextResourceId() { + if (isOlderThanAndroidO()) { + return currentResourceId--; + } + return currentResourceId++; + } + + private boolean isOlderThanAndroidO() { + return minSdkVersion < ANDROID_O_SDK_VERSION; + } +}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java index 67ce730..87e8717 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java
@@ -64,7 +64,7 @@ Element sdkDependencyElement = root.createElement(SDK_DEPENDENCY_ELEMENT_NAME); sdkDependencyElement.setAttribute(ANDROID_NAME_ATTRIBUTE, sdkInfo.getPackageName()); sdkDependencyElement.setAttribute( - ANDROID_VERSION_MAJOR_ATTRIBUTE, Long.toString(sdkInfo.getVersionMajor())); + ANDROID_VERSION_MAJOR_ATTRIBUTE, Long.toString(sdkInfo.getEncodedVersionMajorMinor())); sdkDependencyElement.setAttribute(ANDROID_CERTIFICATE_DIGEST_ATTRIBUTE, certificateDigest); applicationNode.appendChild(sdkDependencyElement); }
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD index 12a7a1b..70c6b5f 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD
@@ -12,7 +12,7 @@ srcs = glob(["*.java"]), deps = [ "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/info", - "@rules_android_maven//:com_android_tools_build_bundletool", + "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/mixin", "@rules_android_maven//:com_google_guava_guava", "@rules_android_maven//:info_picocli_picocli", ],
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java index 354f8fe..3dc015e 100644 --- a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java +++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java
@@ -15,18 +15,12 @@ */ package com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest; -import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest.AndroidManifestWriter.writeManifest; -import static com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest.CertificateDigestGenerator.generateCertificateDigest; -import com.google.common.collect.ImmutableSet; -import com.google.devtools.build.android.sandboxedsdktoolbox.info.SdkInfo; -import com.google.devtools.build.android.sandboxedsdktoolbox.info.SdkInfoReader; +import com.google.devtools.build.android.sandboxedsdktoolbox.mixin.SdkDependenciesCommandMixin; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; /** Command for generating SDK dependencies manifest. */ @@ -40,41 +34,18 @@ @Option(names = "--manifest-package", required = true) String manifestPackage; - @Option(names = "--sdk-module-configs", split = ",", required = false) - List<Path> sdkModuleConfigPaths = new ArrayList<>(); - - @Option(names = "--sdk-archives", split = ",", required = false) - List<Path> sdkArchivePaths = new ArrayList<>(); - - @Option(names = "--debug-keystore", required = true) - Path debugKeystorePath; - - @Option(names = "--debug-keystore-pass", required = true) - String debugKeystorePassword; - - @Option(names = "--debug-keystore-alias", required = true) - String debugKeystoreAlias; - @Option(names = "--output-manifest", required = true) Path outputManifestPath; + @Mixin SdkDependenciesCommandMixin sdkDependenciesMixin; + @Override public void run() { - if (sdkModuleConfigPaths.isEmpty() && sdkArchivePaths.isEmpty()) { - throw new IllegalArgumentException( - "At least one of --sdk-module-configs or --sdk-archives must be specified."); - } - - ImmutableSet<SdkInfo> configSet = - Stream.concat( - sdkModuleConfigPaths.stream().map(SdkInfoReader::readFromSdkModuleJsonFile), - sdkArchivePaths.stream().map(SdkInfoReader::readFromSdkArchive)) - .collect(toImmutableSet()); - - String certificateDigest = - generateCertificateDigest(debugKeystorePath, debugKeystorePassword, debugKeystoreAlias); - - writeManifest(manifestPackage, certificateDigest, configSet, outputManifestPath); + writeManifest( + manifestPackage, + sdkDependenciesMixin.getDebugCertificateDigest(), + sdkDependenciesMixin.getSdkDependencies(), + outputManifestPath); } private GenerateSdkDependenciesManifestCommand() {}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/BUILD b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/BUILD new file mode 100644 index 0000000..5abe44a --- /dev/null +++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/BUILD
@@ -0,0 +1,23 @@ +# Tests for generate-runtime-enabled-sdk-config command. + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:__subpackages__"], +) + +licenses(["notice"]) + +java_test( + name = "GenerateRuntimeEnabledSdkConfigCommandTest", + size = "small", + srcs = ["GenerateRuntimeEnabledSdkConfigCommandTest.java"], + data = glob(["testdata/*"]), + deps = [ + "//src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils", + "@rules_android_maven//:com_android_tools_build_bundletool", + "@rules_android_maven//:com_google_guava_guava", + "@rules_android_maven//:com_google_protobuf_protobuf_java", + "@rules_android_maven//:com_google_truth_truth", + "@rules_android_maven//:junit_junit", + ], +)
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/GenerateRuntimeEnabledSdkConfigCommandTest.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/GenerateRuntimeEnabledSdkConfigCommandTest.java new file mode 100644 index 0000000..454aa06 --- /dev/null +++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/GenerateRuntimeEnabledSdkConfigCommandTest.java
@@ -0,0 +1,349 @@ +/* + * Copyright 2023 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.devtools.build.android.sandboxedsdktoolbox.runtimeenabledsdkconfig; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.Runner.runCommand; +import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.TestData.JAVATESTS_DIR; +import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.Zip.createZipWithSingleEntry; +import static java.util.stream.Collectors.joining; + +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; +import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.sandboxedsdktoolbox.utils.CommandResult; +import com.google.protobuf.ExtensionRegistry; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class GenerateRuntimeEnabledSdkConfigCommandTest { + + @Rule public final TemporaryFolder testFolder = new TemporaryFolder(); + + private static final Path TEST_DATA_DIR = + JAVATESTS_DIR.resolve( + Path.of( + "com/google/devtools/build/android/sandboxedsdktoolbox", + "runtimeenabledsdkconfig/testdata")); + private static final Path SDK_CONFIG_JSON_PATH = + TEST_DATA_DIR.resolve("com.example.sdkconfig.json"); + // Fake manifest XML tree file content, with extra data around the important bits. + private static final String MANIFEST_TREE_XML_MIN_VERSION_21 = + "should\nignore http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=21\n" + + "gibberish"; + private static final String MANIFEST_TREE_XML_MIN_VERSION_26 = + "should\nignore http://schemas.android.com/apk/res/android:minSdkVersion(0x0a01030e)=26\n" + + "gibberish"; + /* + The test key was generated with this command, its password is "android" + keytool -genkeypair \ + -alias androiddebugkey \ + -dname "CN=Android Debug, O=Android, C=US" \ + -keystore test_key \ + -sigalg SHA256withDSA \ + -validity 10950 + */ + private static final Path TEST_KEY_PATH = TEST_DATA_DIR.resolve("test_key"); + private static final String TEST_KEY_CERTIFICATE_DIGEST = + "91:8E:A3:7D:7D:D0:E0:A0:14:9F:21:28:83:95:8A:F0:80:E6:F9:7B:4D:5A:39:01:76:02:E8:" + + "2D:7D:FF:A9:10"; + private static final RuntimeEnabledSdkVersion SDK_VERSION = + RuntimeEnabledSdkVersion.newBuilder().setMajor(3).setMinor(2).setPatch(1).build(); + + @Test + public void generateConfig_onApi26_returnsIncreasingResourceIds() throws Exception { + String firstPackageName = "com.example.archivedsdk1"; + String secondPackageName = "com.example.archivedsdk2"; + String digest = "example:fake:digest"; + + RuntimeEnabledSdkConfig result = + runCommandAndReturnConfig( + MANIFEST_TREE_XML_MIN_VERSION_26, + ImmutableList.of( + SdkMetadata.newBuilder() + .setPackageName(firstPackageName) + .setSdkVersion(SDK_VERSION) + .setCertificateDigest(digest) + .build(), + SdkMetadata.newBuilder() + .setPackageName(secondPackageName) + .setSdkVersion(SDK_VERSION) + .setCertificateDigest(digest) + .build()), + ImmutableList.of()); + + RuntimeEnabledSdk expectedCommonSdk = + RuntimeEnabledSdk.newBuilder() + .setVersionMajor(SDK_VERSION.getMajor()) + .setVersionMinor(SDK_VERSION.getMinor()) + .setBuildTimeVersionPatch(SDK_VERSION.getPatch()) + .setCertificateDigest(digest) + .build(); + assertThat(result) + .isEqualTo( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + expectedCommonSdk.toBuilder() + .setPackageName(firstPackageName) + .setResourcesPackageId(128)) + .addRuntimeEnabledSdk( + expectedCommonSdk.toBuilder() + .setPackageName(secondPackageName) + .setResourcesPackageId(129)) + .build()); + } + + @Test + public void generateConfig_onApi21_returnsDecreasingResourceIds() throws Exception { + String firstPackageName = "com.example.archivedsdk1"; + String secondPackageName = "com.example.archivedsdk2"; + String digest = "example:fake:digest"; + + RuntimeEnabledSdkConfig result = + runCommandAndReturnConfig( + MANIFEST_TREE_XML_MIN_VERSION_21, + ImmutableList.of( + SdkMetadata.newBuilder() + .setPackageName(firstPackageName) + .setSdkVersion(SDK_VERSION) + .setCertificateDigest(digest) + .build(), + SdkMetadata.newBuilder() + .setPackageName(secondPackageName) + .setSdkVersion(SDK_VERSION) + .setCertificateDigest(digest) + .build()), + ImmutableList.of()); + + RuntimeEnabledSdk expectedCommonSdk = + RuntimeEnabledSdk.newBuilder() + .setVersionMajor(SDK_VERSION.getMajor()) + .setVersionMinor(SDK_VERSION.getMinor()) + .setBuildTimeVersionPatch(SDK_VERSION.getPatch()) + .setCertificateDigest(digest) + .build(); + assertThat(result) + .isEqualTo( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + expectedCommonSdk.toBuilder() + .setPackageName(firstPackageName) + .setResourcesPackageId(126)) + .addRuntimeEnabledSdk( + expectedCommonSdk.toBuilder() + .setPackageName(secondPackageName) + .setResourcesPackageId(125)) + .build()); + } + + @Test + public void generateConfig_forModuleConfig_usesDebugKeyDigest() throws Exception { + String sdkArchivePackageName = "com.example.sdkarchive"; + String productionDigest = "fake:prod:digest"; + + RuntimeEnabledSdkConfig result = + runCommandAndReturnConfig( + MANIFEST_TREE_XML_MIN_VERSION_26, + ImmutableList.of( + SdkMetadata.newBuilder() + .setPackageName(sdkArchivePackageName) + .setSdkVersion(SDK_VERSION) + .setCertificateDigest(productionDigest) + .build()), + ImmutableList.of(SDK_CONFIG_JSON_PATH)); + + assertThat(result) + .isEqualTo( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.example.sdkfrombundle") + .setVersionMajor(42) + .setVersionMinor(25) + .setBuildTimeVersionPatch(2) + .setCertificateDigest(TEST_KEY_CERTIFICATE_DIGEST) + .setResourcesPackageId(128)) + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName(sdkArchivePackageName) + .setCertificateDigest(productionDigest) + .setVersionMajor(SDK_VERSION.getMajor()) + .setVersionMinor(SDK_VERSION.getMinor()) + .setBuildTimeVersionPatch(SDK_VERSION.getPatch()) + .setResourcesPackageId(129)) + .build()); + } + + @Test + public void generateConfig_withoutSdks_fails() throws Exception { + Path outputConfig = testFolder.newFile().toPath(); + ImmutableList<String> args = + buildArgs( + outputConfig, MANIFEST_TREE_XML_MIN_VERSION_21, ImmutableList.of(), ImmutableList.of()); + CommandResult result = runCommand(args.toArray(String[]::new)); + + assertThat(result.getStatusCode()).isEqualTo(1); + assertThat(result.getOutput()) + .contains("At least one of --sdk-module-configs or --sdk-archives must be specified."); + } + + @Test + public void generateConfig_for51Sdks_onApi21_fails() throws Exception { + Path outputConfig = testFolder.newFile().toPath(); + + ImmutableList<String> args = + buildArgs( + outputConfig, + MANIFEST_TREE_XML_MIN_VERSION_21, + IntStream.range(0, 51) + .mapToObj(i -> SdkMetadata.newBuilder().setPackageName("com.example." + i).build()) + .collect(toImmutableList()), + ImmutableList.of()); + CommandResult result = runCommand(args.toArray(String[]::new)); + + assertThat(result.getStatusCode()).isEqualTo(1); + assertThat(result.getOutput()).contains("Too many SDK dependencies (51)."); + } + + @Test + public void generateConfig_for51Sdks_onApi26_success() throws Exception { + Path outputConfig = testFolder.newFile().toPath(); + + ImmutableList<String> args = + buildArgs( + outputConfig, + MANIFEST_TREE_XML_MIN_VERSION_26, + IntStream.range(0, 51) + .mapToObj(i -> SdkMetadata.newBuilder().setPackageName("com.example." + i).build()) + .collect(toImmutableList()), + ImmutableList.of()); + CommandResult result = runCommand(args.toArray(String[]::new)); + + assertThat(result.getStatusCode()).isEqualTo(0); + assertThat(result.getOutput()).isEmpty(); + } + + @Test + public void generateConfig_for129Sdks_onApi26_fails() throws Exception { + Path outputConfig = testFolder.newFile().toPath(); + + ImmutableList<String> args = + buildArgs( + outputConfig, + MANIFEST_TREE_XML_MIN_VERSION_26, + IntStream.range(0, 129) + .mapToObj(i -> SdkMetadata.newBuilder().setPackageName("com.example." + i).build()) + .collect(toImmutableList()), + ImmutableList.of()); + CommandResult result = runCommand(args.toArray(String[]::new)); + + assertThat(result.getStatusCode()).isEqualTo(1); + assertThat(result.getOutput()).contains("Too many SDK dependencies (129)."); + } + + @Test + public void generateConfig_withMissingMinSdkVersion_fails() throws Exception { + Path outputConfig = testFolder.newFile().toPath(); + + ImmutableList<String> args = + buildArgs(outputConfig, "", ImmutableList.of(), ImmutableList.of(SDK_CONFIG_JSON_PATH)); + CommandResult result = runCommand(args.toArray(String[]::new)); + + assertThat(result.getStatusCode()).isEqualTo(1); + assertThat(result.getOutput()).contains("Min SDK version missing from manifest xml tree file."); + } + + /** + * Runs the generate-runtime-enabled-sdk-config command with the given android manifest, sdk + * archives and SDK module configs. + * + * <p>The archives are represented as a single zip with the given SDK Metadata messages inside. + */ + private RuntimeEnabledSdkConfig runCommandAndReturnConfig( + String manifestXmlTree, + ImmutableList<SdkMetadata> sdkArchives, + ImmutableList<Path> sdkModuleConfigPaths) + throws Exception { + Path outputConfig = testFolder.newFile().toPath(); + ImmutableList<String> args = + buildArgs(outputConfig, manifestXmlTree, sdkArchives, sdkModuleConfigPaths); + CommandResult result = runCommand(args.toArray(String[]::new)); + + assertThat(result.getStatusCode()).isEqualTo(0); + assertThat(result.getOutput()).isEmpty(); + return readConfig(outputConfig); + } + + private ImmutableList<String> buildArgs( + Path outputConfig, + String manifestXmlTree, + ImmutableList<SdkMetadata> sdkArchives, + ImmutableList<Path> sdkModuleConfigPaths) + throws Exception { + Path manifestXmlTreePath = testFolder.newFile("manifest_xml_tree.txt").toPath(); + Files.writeString(manifestXmlTreePath, manifestXmlTree); + + ImmutableList.Builder<String> args = ImmutableList.builder(); + args.add( + "generate-runtime-enabled-sdk-config", + "--debug-keystore", + TEST_KEY_PATH.toString(), + "--debug-keystore-pass", + "android", + "--debug-keystore-alias", + "androiddebugkey", + "--output-config", + outputConfig.toString(), + "--manifest-xml-tree", + manifestXmlTreePath.toString()); + + if (!sdkModuleConfigPaths.isEmpty()) { + args.add( + "--sdk-module-configs", + sdkModuleConfigPaths.stream().map(Path::toString).collect(joining(","))); + } + + if (!sdkArchives.isEmpty()) { + ArrayList<String> sdkArchivePaths = new ArrayList<>(); + for (SdkMetadata sdkMetadata : sdkArchives) { + Path sdkArchivePath = + testFolder.getRoot().toPath().resolve(sdkMetadata.hashCode() + ".asar"); + createZipWithSingleEntry(sdkArchivePath, "SdkMetadata.pb", sdkMetadata.toByteArray()); + sdkArchivePaths.add(sdkArchivePath.toString()); + } + args.add("--sdk-archives", String.join(",", sdkArchivePaths)); + } + + return args.build(); + } + + private static RuntimeEnabledSdkConfig readConfig(Path configPath) throws IOException { + return RuntimeEnabledSdkConfig.parseFrom( + Files.readAllBytes(configPath), ExtensionRegistry.getEmptyRegistry()); + } +}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/testdata/com.example.sdkconfig.json b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/testdata/com.example.sdkconfig.json new file mode 100644 index 0000000..a552cde --- /dev/null +++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/testdata/com.example.sdkconfig.json
@@ -0,0 +1,9 @@ +{ + "sdk_package_name": "com.example.sdkfrombundle", + "sdk_provider_class_name": "com.testsdk.lib.FakeSdkProvider", + "sdk_version": { + "major": 42, + "minor": 25, + "patch": 2 + } +} \ No newline at end of file
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/testdata/test_key b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/testdata/test_key new file mode 100644 index 0000000..e0061a5 --- /dev/null +++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/runtimeenabledsdkconfig/testdata/test_key Binary files differ
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java index bfd3ec9..1010698 100644 --- a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java +++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java
@@ -24,13 +24,15 @@ public final class Runner { public static CommandResult runCommand(String... parameters) { CommandLine command = SandboxedSdkToolbox.create(); - StringWriter stringWriter = new StringWriter(); + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); - command.setOut(new PrintWriter(stringWriter)); + command.setOut(new PrintWriter(out)); + command.setErr(new PrintWriter(err)); int statusCode = command.execute(parameters); - String output = stringWriter.toString(); + out.append(err.toString()); - return new CommandResult(statusCode, output); + return new CommandResult(statusCode, out.toString()); } private Runner() {}