提交 24e8e6ef 编写于 作者: I Iurii Makhno

Prepare for release 0.13.4.

上级 a325c37e
......@@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool
## Releases
Latest release: [0.13.3](https://github.com/google/bundletool/releases)
Latest release: [0.13.4](https://github.com/google/bundletool/releases)
......@@ -21,8 +21,16 @@ apply plugin: "maven"
repositories {
jcenter()
google()
maven {
// We need to add this custom format repo because we use r8 version which is
// not available in gMaven yet. This repo can be removed once new version is
// uploaded to gMaven.
// Shrunk version of r8 is available there by the following path:
// https://storage.googleapis.com/r8-releases/raw/[revision]/r8lib.jar
ivy {
url "https://storage.googleapis.com/r8-releases/raw"
patternLayout {
artifact "[revision]/[artifact]lib.[ext]"
}
}
}
......@@ -66,6 +74,9 @@ dependencies {
testCompile "org.junit.vintage:junit-vintage-engine:5.2.0"
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.2.0"
testCompile "org.junit.platform:junit-platform-runner:1.2.0"
testCompile("org.smali:dexlib2:2.3.4") {
exclude group: "com.google.guava", module: "guava"
}
}
def osName = System.getProperty("os.name").toLowerCase()
......
release_version = 0.13.3
release_version = 0.13.4
......@@ -124,6 +124,13 @@ public abstract class BuildApksCommand {
private static final Flag<Password> KEYSTORE_PASSWORD_FLAG = Flag.password("ks-pass");
private static final Flag<Password> KEY_PASSWORD_FLAG = Flag.password("key-pass");
// SourceStamp-related flags.
private static final Flag<Boolean> CREATE_STAMP_FLAG = Flag.booleanFlag("create-stamp");
private static final Flag<Path> STAMP_KEYSTORE_FLAG = Flag.path("stamp-ks");
private static final Flag<Password> STAMP_KEYSTORE_PASSWORD_FLAG = Flag.password("stamp-ks-pass");
private static final Flag<String> STAMP_KEY_ALIAS_FLAG = Flag.string("stamp-key-alias");
private static final Flag<Password> STAMP_KEY_PASSWORD_FLAG = Flag.password("stamp-key-pass");
private static final Flag<String> STAMP_SOURCE_FLAG = Flag.string("stamp-source");
private static final String APK_SET_ARCHIVE_EXTENSION = "apks";
......@@ -181,6 +188,7 @@ public abstract class BuildApksCommand {
public abstract Optional<PrintStream> getOutputPrintStream();
public abstract Optional<SourceStamp> getSourceStamp();
public static Builder builder() {
return new AutoValue_BuildApksCommand.Builder()
......@@ -331,6 +339,11 @@ public abstract class BuildApksCommand {
/** For command line, sets the {@link PrintStream} to use for outputting the warnings. */
public abstract Builder setOutputPrintStream(PrintStream outputPrintStream);
/**
* Provides a {@link SourceStamp} to be included in the
* generated APKs.
*/
public abstract Builder setSourceStamp(SourceStamp sourceStamp);
abstract BuildApksCommand autoBuild();
......@@ -464,6 +477,7 @@ public abstract class BuildApksCommand {
OPTIMIZE_FOR_FLAG.getValue(flags).ifPresent(buildApksCommand::setOptimizationDimensions);
populateSigningConfigurationFromFlags(buildApksCommand, flags, out, systemEnvironmentProvider);
populateSourceStampFromFlags(buildApksCommand, flags, out, systemEnvironmentProvider);
boolean connectedDeviceMode = CONNECTED_DEVICE_FLAG.getValue(flags).orElse(false);
CONNECTED_DEVICE_FLAG
......@@ -732,6 +746,71 @@ public abstract class BuildApksCommand {
+ " accessed by the Play Core API.",
InstallApksCommand.COMMAND_NAME)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(CREATE_STAMP_FLAG.getName())
.setOptional(true)
.setDescription(
"If set, a stamp will be generated and embedded in the generated APKs.")
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(STAMP_KEYSTORE_FLAG.getName())
.setExampleValue("path/to/keystore")
.setOptional(true)
.setDescription(
"Path to the stamp keystore that should be used to sign the APK contents hash."
+ " If not set, the '%s' keystore will be tried if present. Otherwise, the"
+ " default debug keystore will be used if it exists. If a default debug"
+ " keystore is not found, the stamp will fail to get generated. If"
+ " set, the flag '%s' must also be set.",
KEYSTORE_FLAG, STAMP_KEY_ALIAS_FLAG)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(STAMP_KEYSTORE_PASSWORD_FLAG.getName())
.setExampleValue("[pass|file]:value")
.setOptional(true)
.setDescription(
"Password of the stamp keystore to use to sign the APK contents hash. If"
+ " provided, must be prefixed with either 'pass:' (if the password is"
+ " passed in clear text, e.g. 'pass:qwerty') or 'file:' (if the password"
+ " is the first line of a file, e.g. 'file:/tmp/myPassword.txt'). If this"
+ " flag is not set, the password will be requested on the prompt.")
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(STAMP_KEY_ALIAS_FLAG.getName())
.setExampleValue("stamp-key-alias")
.setOptional(true)
.setDescription(
"Alias of the stamp key to use in the keystore to sign the APK contents hash."
+ " If not set, the '%s' key alias will be tried if present.",
KEY_ALIAS_FLAG)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(STAMP_KEY_PASSWORD_FLAG.getName())
.setExampleValue("stamp-key-password")
.setOptional(true)
.setDescription(
"Password of the stamp key in the keystore to use to sign the APK contents"
+ " hash. if provided, must be prefixed with either 'pass:' (if the"
+ " password is passed in clear text, e.g. 'pass:qwerty') or 'file:' (if"
+ " the password is the first line of a file, e.g."
+ " 'file:/tmp/myPassword.txt'). If this flag is not set, the keystore"
+ " password will be tried. If that fails, the password will be requested"
+ " on the prompt.")
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(STAMP_SOURCE_FLAG.getName())
.setExampleValue("stamp-source")
.setOptional(true)
.setDescription(
"Name of source generating the stamp. For stores, it is their package names."
+ " For locally generated stamp, it is 'local'.")
.build())
.build();
}
......@@ -778,4 +857,81 @@ public abstract class BuildApksCommand {
}
}
private static void populateSourceStampFromFlags(
Builder buildApksCommand,
ParsedFlags flags,
PrintStream out,
SystemEnvironmentProvider systemEnvironmentProvider) {
boolean createStamp = CREATE_STAMP_FLAG.getValue(flags).orElse(false);
Optional<String> stampSource = STAMP_SOURCE_FLAG.getValue(flags);
if (!createStamp) {
return;
}
SourceStamp.Builder sourceStamp = SourceStamp.builder();
sourceStamp.setSigningConfiguration(
getStampSigningConfiguration(flags, out, systemEnvironmentProvider));
stampSource.ifPresent(sourceStamp::setSource);
buildApksCommand.setSourceStamp(sourceStamp.build());
}
private static SigningConfiguration getStampSigningConfiguration(
ParsedFlags flags, PrintStream out, SystemEnvironmentProvider systemEnvironmentProvider) {
// Signing-related flags.
Optional<Path> signingKeystorePath = KEYSTORE_FLAG.getValue(flags);
Optional<Password> signingKeystorePassword = KEYSTORE_PASSWORD_FLAG.getValue(flags);
Optional<String> signingKeyAlias = KEY_ALIAS_FLAG.getValue(flags);
Optional<Password> signingKeyPassword = KEY_PASSWORD_FLAG.getValue(flags);
// Stamp-related flags.
Optional<Path> stampKeystorePath = STAMP_KEYSTORE_FLAG.getValue(flags);
Optional<Password> stampKeystorePassword = STAMP_KEYSTORE_PASSWORD_FLAG.getValue(flags);
Optional<String> stampKeyAlias = STAMP_KEY_ALIAS_FLAG.getValue(flags);
Optional<Password> stampKeyPassword = STAMP_KEY_PASSWORD_FLAG.getValue(flags);
Path keystorePath = null;
Optional<Password> keystorePassword = Optional.empty();
if (stampKeystorePath.isPresent()) {
keystorePath = stampKeystorePath.get();
keystorePassword = stampKeystorePassword;
} else if (signingKeystorePath.isPresent()) {
keystorePath = signingKeystorePath.get();
keystorePassword = signingKeystorePassword;
}
if (keystorePath == null) {
// Try to use debug keystore if present.
Optional<SigningConfiguration> debugConfig =
DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider);
if (debugConfig.isPresent()) {
out.printf(
"INFO: The stamp will be signed with the debug keystore found at '%s'.%n",
DebugKeystoreUtils.DEBUG_KEYSTORE_CACHE.getUnchecked(systemEnvironmentProvider).get());
return debugConfig.get();
} else {
throw new CommandExecutionException("No key was found to sign the stamp.");
}
}
String keyAlias = null;
Optional<Password> keyPassword = Optional.empty();
if (stampKeyAlias.isPresent()) {
keyAlias = stampKeyAlias.get();
keyPassword = stampKeyPassword;
} else if (signingKeyAlias.isPresent()) {
keyAlias = signingKeyAlias.get();
keyPassword = signingKeyPassword;
}
if (keyAlias == null) {
throw new CommandExecutionException(
"Flag --stamp-key-alias or --ks-key-alias are required when --stamp-ks or --ks are set.");
}
return SigningConfiguration.extractFromKeystore(
keystorePath, keyAlias, keystorePassword, keyPassword);
}
}
......@@ -53,6 +53,7 @@ import com.android.tools.build.bundletool.model.GeneratedAssetSlices;
import com.android.tools.build.bundletool.model.ModuleSplit;
import com.android.tools.build.bundletool.model.OptimizationDimension;
import com.android.tools.build.bundletool.model.SigningConfiguration;
import com.android.tools.build.bundletool.model.SourceStamp;
import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException;
import com.android.tools.build.bundletool.model.targeting.AlternativeVariantTargetingPopulator;
import com.android.tools.build.bundletool.model.utils.SplitsXmlInjector;
......@@ -118,7 +119,7 @@ final class BuildApksManager {
}
try (ZipFile bundleZip = new ZipFile(command.getBundlePath().toFile())) {
executeWithZip(bundleZip, deviceSpec);
executeWithZip(bundleZip, deviceSpec, command.getSourceStamp());
} catch (IOException e) {
throw new UncheckedIOException(
String.format(
......@@ -133,8 +134,10 @@ final class BuildApksManager {
return command.getOutputFile();
}
private void executeWithZip(ZipFile bundleZip, Optional<DeviceSpec> deviceSpec)
private void executeWithZip(
ZipFile bundleZip, Optional<DeviceSpec> deviceSpec, Optional<SourceStamp> sourceStamp)
throws IOException {
Optional<String> stampSource = sourceStamp.map(SourceStamp::getSource);
AppBundleValidator bundleValidator = AppBundleValidator.create(command.getExtraValidators());
bundleValidator.validateFile(bundleZip);
......@@ -162,17 +165,18 @@ final class BuildApksManager {
// Split APKs
if (apksToGenerate.generateSplitApks()) {
generatedApksBuilder.setSplitApks(generateSplitApks(appBundle));
generatedApksBuilder.setSplitApks(generateSplitApks(appBundle, stampSource));
}
// Instant APKs
if (apksToGenerate.generateInstantApks()) {
generatedApksBuilder.setInstantApks(generateInstantApks(appBundle));
generatedApksBuilder.setInstantApks(generateInstantApks(appBundle, stampSource));
}
// Standalone APKs
if (apksToGenerate.generateStandaloneApks()) {
generatedApksBuilder.setStandaloneApks(generateStandaloneApks(tempDir, appBundle));
generatedApksBuilder.setStandaloneApks(
generateStandaloneApks(tempDir, appBundle, stampSource));
}
// Universal APK
......@@ -183,7 +187,12 @@ final class BuildApksManager {
? modulesToFuse(appBundle.getFeatureModules().values().asList())
: requestedModules.asList();
generatedApksBuilder.setStandaloneApks(
new ShardedApksGenerator(tempDir, bundleVersion, getSuffixStrippings(bundleConfig))
new ShardedApksGenerator(
tempDir,
bundleVersion,
/* strip64BitLibrariesFromShards= */ false,
getSuffixStrippings(bundleConfig),
stampSource)
.generateSplits(
modulesToFuse,
appBundle.getBundleMetadata(),
......@@ -217,10 +226,13 @@ final class BuildApksManager {
checkDeviceCompatibilityWithBundle(generatedApks, deviceSpec.get());
}
Optional<SigningConfiguration> stampSigningConfiguration =
sourceStamp.map(SourceStamp::getSigningConfiguration);
ApkSetBuilder apkSetBuilder =
createApkSetBuilder(
aapt2Command,
command.getSigningConfiguration(),
stampSigningConfiguration,
bundleVersion,
bundleConfig.getCompression(),
tempDir);
......@@ -248,7 +260,8 @@ final class BuildApksManager {
apkSetBuilder.writeTo(command.getOutputFile());
}
private ImmutableList<ModuleSplit> generateStandaloneApks(Path tempDir, AppBundle appBundle) {
private ImmutableList<ModuleSplit> generateStandaloneApks(
Path tempDir, AppBundle appBundle, Optional<String> stampSource) {
ImmutableList<BundleModule> allModules = getModulesForStandaloneApks(appBundle);
Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
ShardedApksGenerator shardedApksGenerator =
......@@ -256,7 +269,8 @@ final class BuildApksManager {
tempDir,
bundleVersion,
shouldStrip64BitLibrariesFromShards(appBundle),
getSuffixStrippings(appBundle.getBundleConfig()));
getSuffixStrippings(appBundle.getBundleConfig()),
stampSource);
return appBundle.isApex()
? shardedApksGenerator.generateApexSplits(modulesToFuse(allModules))
: shardedApksGenerator.generateSplits(
......@@ -273,7 +287,8 @@ final class BuildApksManager {
return assetSlicesGenerator.generateAssetSlices();
}
private ImmutableList<ModuleSplit> generateInstantApks(AppBundle appBundle) {
private ImmutableList<ModuleSplit> generateInstantApks(
AppBundle appBundle, Optional<String> stampSource) {
Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
ImmutableList<BundleModule> allFeatureModules = appBundle.getFeatureModules().values().asList();
ImmutableList<BundleModule> instantModules =
......@@ -285,11 +300,13 @@ final class BuildApksManager {
// only support one variant.
.setEnableDexCompressionSplitter(false)
.build();
return new SplitApksGenerator(instantModules, bundleVersion, instantApkGenerationConfiguration)
return new SplitApksGenerator(
instantModules, bundleVersion, instantApkGenerationConfiguration, stampSource)
.generateSplits();
}
private ImmutableList<ModuleSplit> generateSplitApks(AppBundle appBundle) throws IOException {
private ImmutableList<ModuleSplit> generateSplitApks(
AppBundle appBundle, Optional<String> stampSource) throws IOException {
Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
ApkGenerationConfiguration.Builder apkGenerationConfiguration =
getCommonSplitApkGenerationConfiguration(appBundle);
......@@ -303,7 +320,8 @@ final class BuildApksManager {
}
ImmutableList<BundleModule> featureModules = appBundle.getFeatureModules().values().asList();
return new SplitApksGenerator(featureModules, bundleVersion, apkGenerationConfiguration.build())
return new SplitApksGenerator(
featureModules, bundleVersion, apkGenerationConfiguration.build(), stampSource)
.generateSplits();
}
......@@ -346,16 +364,27 @@ final class BuildApksManager {
private ApkSetBuilder createApkSetBuilder(
Aapt2Command aapt2Command,
Optional<SigningConfiguration> signingConfiguration,
Optional<SigningConfiguration> stampSigningConfiguration,
Version bundleVersion,
Compression compression,
Path tempDir) {
ApkPathManager apkPathmanager = new ApkPathManager();
SplitApkSerializer splitApkSerializer =
new SplitApkSerializer(
apkPathmanager, aapt2Command, signingConfiguration, bundleVersion, compression);
apkPathmanager,
aapt2Command,
signingConfiguration,
stampSigningConfiguration,
bundleVersion,
compression);
StandaloneApkSerializer standaloneApkSerializer =
new StandaloneApkSerializer(
apkPathmanager, aapt2Command, signingConfiguration, bundleVersion, compression);
apkPathmanager,
aapt2Command,
signingConfiguration,
stampSigningConfiguration,
bundleVersion,
compression);
if (!command.getCreateApkSetArchive()) {
return ApkSetBuilderFactory.createApkSetWithoutArchiveBuilder(
......
......@@ -33,7 +33,6 @@ import com.android.apksig.ApkSigner;
import com.android.apksig.ApkSigner.SignerConfig;
import com.android.apksig.apk.ApkFormatException;
import com.android.bundle.Config.Compression;
import com.android.tools.build.apkzlib.sign.SigningOptions;
import com.android.tools.build.apkzlib.zfile.ZFiles;
import com.android.tools.build.apkzlib.zip.AlignmentRule;
import com.android.tools.build.apkzlib.zip.AlignmentRules;
......@@ -117,16 +116,19 @@ final class ApkSerializerHelper {
private final Aapt2Command aapt2Command;
private final Version bundleVersion;
private final Optional<SigningConfiguration> signingConfig;
private final Optional<SigningConfiguration> stampSigningConfig;
private final ImmutableList<PathMatcher> uncompressedPathMatchers;
ApkSerializerHelper(
Aapt2Command aapt2Command,
Optional<SigningConfiguration> signingConfig,
Optional<SigningConfiguration> stampSigningConfig,
Version bundleVersion,
Compression compression) {
this.aapt2Command = aapt2Command;
this.bundleVersion = bundleVersion;
this.signingConfig = signingConfig;
this.stampSigningConfig = stampSigningConfig;
this.uncompressedPathMatchers =
compression.getUncompressedGlobList().stream()
......@@ -154,34 +156,17 @@ final class ApkSerializerHelper {
aapt2Command.convertApkProtoToBinary(partialProtoApk, binaryApk);
checkState(Files.exists(binaryApk), "No APK created by aapt2 convert command.");
boolean signWithV1 =
split.getAndroidManifest().getEffectiveMinSdkVersion() < Versions.ANDROID_N_API_VERSION
|| !VersionGuardedFeature.NO_V1_SIGNING_WHEN_POSSIBLE.enabledForVersion(bundleVersion);
// Create a new APK that includes files processed by aapt2 and the other ones.
int minSdkVersion = split.getAndroidManifest().getEffectiveMinSdkVersion();
com.google.common.base.Optional<SigningOptions> signingOptions =
signingConfig
.map(
config ->
com.google.common.base.Optional.of(
SigningOptions.builder()
.setKey(config.getPrivateKey())
.setCertificates(config.getCertificates())
.setV1SigningEnabled(signWithV1)
.setV2SigningEnabled(true)
.setMinSdkVersion(minSdkVersion)
.build()))
.orElse(com.google.common.base.Optional.absent());
Path unsignedApkPath = tempDir.resolve("apk-unsigned.apk");
try (ZFile zOutputApk =
ZFiles.apk(
outputPath.toFile(),
unsignedApkPath.toFile(),
createZFileOptions(tempDir)
.setAlignmentRule(APK_ALIGNMENT_RULE)
.setCoverEmptySpaceUsingExtraField(true)
// Clear timestamps on zip entries to minimize diffs between APKs.
.setNoTimestamps(true),
signingOptions,
/* signingOptions= */ com.google.common.base.Optional.absent(),
BUILT_BY,
CREATED_BY);
ZFile zAapt2Files =
......@@ -195,7 +180,29 @@ final class ApkSerializerHelper {
zOutputApk.sortZipContents();
} catch (IOException e) {
throw new UncheckedIOException(
String.format("Failed to write APK file '%s'.", outputPath), e);
String.format("Failed to write APK file '%s'.", unsignedApkPath), e);
}
// Sign APK.
boolean signWithV1 =
split.getAndroidManifest().getEffectiveMinSdkVersion() < Versions.ANDROID_N_API_VERSION
|| !VersionGuardedFeature.NO_V1_SIGNING_WHEN_POSSIBLE.enabledForVersion(bundleVersion);
int minSdkVersion = split.getAndroidManifest().getEffectiveMinSdkVersion();
if (signingConfig.isPresent()) {
signApk(
unsignedApkPath,
outputPath,
signingConfig.get(),
stampSigningConfig,
signWithV1,
minSdkVersion);
} else {
try {
Files.move(unsignedApkPath, outputPath);
} catch (IOException e) {
throw new UncheckedIOException(
String.format("Failed to write APK file '%s'.", outputPath), e);
}
}
}
......@@ -426,4 +433,36 @@ final class ApkSerializerHelper {
ZFileOptions options = new ZFileOptions();
return options;
}
private static void signApk(
Path inputApkPath,
Path outputApkPath,
SigningConfiguration signingConfiguration,
Optional<SigningConfiguration> stampSigningConfiguration,
boolean signWithV1,
int minSdkVersion) {
try {
SignerConfig signerConfig =
new SignerConfig.Builder(
SIGNER_CONFIG_NAME,
signingConfiguration.getPrivateKey(),
signingConfiguration.getCertificates())
.build();
ApkSigner.Builder apkSigner =
new ApkSigner.Builder(ImmutableList.of(signerConfig))
.setInputApk(inputApkPath.toFile())
.setV1SigningEnabled(signWithV1)
.setV2SigningEnabled(true)
.setOtherSignersSignaturesPreserved(false)
.setMinSdkVersion(minSdkVersion)
.setOutputApk(outputApkPath.toFile());
apkSigner.build().sign();
} catch (IOException
| ApkFormatException
| NoSuchAlgorithmException
| InvalidKeyException
| SignatureException e) {
throw new ValidationException("Unable to sign APK.", e);
}
}
}
......@@ -70,11 +70,7 @@ public class AppBundleSerializer {
for (ModuleEntry entry : module.getEntries()) {
ZipPath entryPath = moduleDir.resolve(entry.getPath());
if (entry.isDirectory()) {
zipBuilder.addDirectory(entryPath);
} else {
zipBuilder.addFile(entryPath, entry.getContentSupplier(), compression);
}
zipBuilder.addFile(entryPath, entry.getContentSupplier(), compression);
}
// Special module files are not represented as module entries (above).
......
......@@ -41,11 +41,13 @@ public class SplitApkSerializer {
ApkPathManager apkPathManager,
Aapt2Command aapt2Command,
Optional<SigningConfiguration> signingConfig,
Optional<SigningConfiguration> stampSigningConfig,
Version bundleVersion,
Compression compression) {
this.apkPathManager = apkPathManager;
this.apkSerializerHelper =
new ApkSerializerHelper(aapt2Command, signingConfig, bundleVersion, compression);
new ApkSerializerHelper(
aapt2Command, signingConfig, stampSigningConfig, bundleVersion, compression);
}
/** Writes the installable split to disk. */
......
......@@ -45,11 +45,13 @@ public class StandaloneApkSerializer {
ApkPathManager apkPathManager,
Aapt2Command aapt2Command,
Optional<SigningConfiguration> signingConfig,
Optional<SigningConfiguration> stampSigningConfig,
Version bundleVersion,
Compression compression) {
this.apkPathManager = apkPathManager;
this.apkSerializerHelper =
new ApkSerializerHelper(aapt2Command, signingConfig, bundleVersion, compression);
new ApkSerializerHelper(
aapt2Command, signingConfig, stampSigningConfig, bundleVersion, compression);
}
public ApkDescription writeToDisk(ModuleSplit standaloneSplit, Path outputDirectory) {
......