Sort output artifacts so that the updated versionCode manifest entry will allow correct handling of updates from Play Store. The most important dimension is Android SDK version. It is important that a split based on min SDK version will allow a user to get a new APK if they upgrade the OS on their device to support a new split. ABI splits need to also be taken into consideration as it is possible for a device to run in ARM emulation mode and installing an ARM APK over a x86 APK could cause performance regressions. The XML file format was updated to give each of the configuration groups have their own section of the XML file. This allows the sort order to be determined by a groups ordering. Artifacts can now be added to the configuration file in an arbitrary order. Since this will be the common case for developers, it will help reduce errors from inserting a new artifact in the wrong spot. The implementation follows the rules outlined at: https://developer.android.com/google/play/publishing/multiple-apks.html Test: Unit tests Test: Manual process XML configuration Change-Id: I0face862c6d6b9d3cd2d99088afe5b9491be0120
344 lines
11 KiB
C++
344 lines
11 KiB
C++
/*
|
|
* Copyright (C) 2017 The Android Open Source Project
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include "MultiApkGenerator.h"
|
|
|
|
#include <algorithm>
|
|
#include <regex>
|
|
#include <string>
|
|
|
|
#include "androidfw/StringPiece.h"
|
|
|
|
#include "LoadedApk.h"
|
|
#include "ResourceUtils.h"
|
|
#include "ValueVisitor.h"
|
|
#include "configuration/ConfigurationParser.h"
|
|
#include "filter/AbiFilter.h"
|
|
#include "filter/Filter.h"
|
|
#include "format/Archive.h"
|
|
#include "format/binary/XmlFlattener.h"
|
|
#include "optimize/VersionCollapser.h"
|
|
#include "process/IResourceTableConsumer.h"
|
|
#include "split/TableSplitter.h"
|
|
#include "util/Files.h"
|
|
#include "xml/XmlDom.h"
|
|
#include "xml/XmlUtil.h"
|
|
|
|
namespace aapt {
|
|
|
|
using ::aapt::configuration::AndroidSdk;
|
|
using ::aapt::configuration::OutputArtifact;
|
|
using ::aapt::xml::kSchemaAndroid;
|
|
using ::aapt::xml::XmlResource;
|
|
using ::android::StringPiece;
|
|
|
|
/**
|
|
* Context wrapper that allows the min Android SDK value to be overridden.
|
|
*/
|
|
class ContextWrapper : public IAaptContext {
|
|
public:
|
|
explicit ContextWrapper(IAaptContext* context)
|
|
: context_(context), min_sdk_(context_->GetMinSdkVersion()) {
|
|
}
|
|
|
|
PackageType GetPackageType() override {
|
|
return context_->GetPackageType();
|
|
}
|
|
|
|
SymbolTable* GetExternalSymbols() override {
|
|
return context_->GetExternalSymbols();
|
|
}
|
|
|
|
IDiagnostics* GetDiagnostics() override {
|
|
if (source_diag_) {
|
|
return source_diag_.get();
|
|
}
|
|
return context_->GetDiagnostics();
|
|
}
|
|
|
|
const std::string& GetCompilationPackage() override {
|
|
return context_->GetCompilationPackage();
|
|
}
|
|
|
|
uint8_t GetPackageId() override {
|
|
return context_->GetPackageId();
|
|
}
|
|
|
|
NameMangler* GetNameMangler() override {
|
|
return context_->GetNameMangler();
|
|
}
|
|
|
|
bool IsVerbose() override {
|
|
return context_->IsVerbose();
|
|
}
|
|
|
|
int GetMinSdkVersion() override {
|
|
return min_sdk_;
|
|
}
|
|
|
|
void SetMinSdkVersion(int min_sdk) {
|
|
min_sdk_ = min_sdk;
|
|
}
|
|
|
|
void SetSource(const std::string& source) {
|
|
source_diag_ =
|
|
util::make_unique<SourcePathDiagnostics>(Source{source}, context_->GetDiagnostics());
|
|
}
|
|
|
|
private:
|
|
IAaptContext* context_;
|
|
std::unique_ptr<SourcePathDiagnostics> source_diag_;
|
|
|
|
int min_sdk_ = -1;
|
|
};
|
|
|
|
class SignatureFilter : public IPathFilter {
|
|
bool Keep(const std::string& path) override {
|
|
static std::regex signature_regex(R"regex(^META-INF/.*\.(RSA|DSA|EC|SF)$)regex");
|
|
if (std::regex_search(path, signature_regex)) {
|
|
return false;
|
|
}
|
|
return !(path == "META-INF/MANIFEST.MF");
|
|
}
|
|
};
|
|
|
|
MultiApkGenerator::MultiApkGenerator(LoadedApk* apk, IAaptContext* context)
|
|
: apk_(apk), context_(context) {
|
|
}
|
|
|
|
bool MultiApkGenerator::FromBaseApk(const MultiApkGeneratorOptions& options) {
|
|
std::unordered_set<std::string> artifacts_to_keep = options.kept_artifacts;
|
|
std::unordered_set<std::string> filtered_artifacts;
|
|
std::unordered_set<std::string> kept_artifacts;
|
|
|
|
// For now, just write out the stripped APK since ABI splitting doesn't modify anything else.
|
|
for (const OutputArtifact& artifact : options.apk_artifacts) {
|
|
FilterChain filters;
|
|
|
|
ContextWrapper wrapped_context{context_};
|
|
wrapped_context.SetSource(artifact.name);
|
|
|
|
if (!options.kept_artifacts.empty()) {
|
|
const auto& it = artifacts_to_keep.find(artifact.name);
|
|
if (it == artifacts_to_keep.end()) {
|
|
filtered_artifacts.insert(artifact.name);
|
|
if (context_->IsVerbose()) {
|
|
context_->GetDiagnostics()->Note(DiagMessage(artifact.name) << "skipping artifact");
|
|
}
|
|
continue;
|
|
} else {
|
|
artifacts_to_keep.erase(it);
|
|
kept_artifacts.insert(artifact.name);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<ResourceTable> table =
|
|
FilterTable(context_, artifact, *apk_->GetResourceTable(), &filters);
|
|
if (!table) {
|
|
return false;
|
|
}
|
|
|
|
IDiagnostics* diag = wrapped_context.GetDiagnostics();
|
|
|
|
std::unique_ptr<XmlResource> manifest;
|
|
if (!UpdateManifest(artifact, &manifest, diag)) {
|
|
diag->Error(DiagMessage() << "could not update AndroidManifest.xml for output artifact");
|
|
return false;
|
|
}
|
|
|
|
std::string out = options.out_dir;
|
|
if (!file::mkdirs(out)) {
|
|
diag->Warn(DiagMessage() << "could not create out dir: " << out);
|
|
}
|
|
file::AppendPath(&out, artifact.name);
|
|
|
|
if (context_->IsVerbose()) {
|
|
diag->Note(DiagMessage() << "Generating split: " << out);
|
|
}
|
|
|
|
std::unique_ptr<IArchiveWriter> writer = CreateZipFileArchiveWriter(diag, out);
|
|
|
|
if (context_->IsVerbose()) {
|
|
diag->Note(DiagMessage() << "Writing output: " << out);
|
|
}
|
|
|
|
filters.AddFilter(util::make_unique<SignatureFilter>());
|
|
if (!apk_->WriteToArchive(&wrapped_context, table.get(), options.table_flattener_options,
|
|
&filters, writer.get(), manifest.get())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Make sure all of the requested artifacts were valid. If there are any kept artifacts left,
|
|
// either the config or the command line was wrong.
|
|
if (!artifacts_to_keep.empty()) {
|
|
context_->GetDiagnostics()->Error(
|
|
DiagMessage() << "The configuration and command line to filter artifacts do not match");
|
|
|
|
context_->GetDiagnostics()->Error(DiagMessage() << kept_artifacts.size() << " kept:");
|
|
for (const auto& artifact : kept_artifacts) {
|
|
context_->GetDiagnostics()->Error(DiagMessage() << " " << artifact);
|
|
}
|
|
|
|
context_->GetDiagnostics()->Error(DiagMessage() << filtered_artifacts.size() << " filtered:");
|
|
for (const auto& artifact : filtered_artifacts) {
|
|
context_->GetDiagnostics()->Error(DiagMessage() << " " << artifact);
|
|
}
|
|
|
|
context_->GetDiagnostics()->Error(DiagMessage() << artifacts_to_keep.size() << " missing:");
|
|
for (const auto& artifact : artifacts_to_keep) {
|
|
context_->GetDiagnostics()->Error(DiagMessage() << " " << artifact);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::unique_ptr<ResourceTable> MultiApkGenerator::FilterTable(IAaptContext* context,
|
|
const OutputArtifact& artifact,
|
|
const ResourceTable& old_table,
|
|
FilterChain* filters) {
|
|
TableSplitterOptions splits;
|
|
AxisConfigFilter axis_filter;
|
|
ContextWrapper wrapped_context{context};
|
|
wrapped_context.SetSource(artifact.name);
|
|
|
|
if (!artifact.abis.empty()) {
|
|
filters->AddFilter(AbiFilter::FromAbiList(artifact.abis));
|
|
}
|
|
|
|
if (!artifact.screen_densities.empty()) {
|
|
for (const auto& density_config : artifact.screen_densities) {
|
|
splits.preferred_densities.push_back(density_config.density);
|
|
}
|
|
}
|
|
|
|
if (!artifact.locales.empty()) {
|
|
for (const auto& locale : artifact.locales) {
|
|
axis_filter.AddConfig(locale);
|
|
}
|
|
splits.config_filter = &axis_filter;
|
|
}
|
|
|
|
if (artifact.android_sdk) {
|
|
wrapped_context.SetMinSdkVersion(artifact.android_sdk.value().min_sdk_version);
|
|
}
|
|
|
|
std::unique_ptr<ResourceTable> table = old_table.Clone();
|
|
|
|
VersionCollapser collapser;
|
|
if (!collapser.Consume(&wrapped_context, table.get())) {
|
|
context->GetDiagnostics()->Error(DiagMessage() << "Failed to strip versioned resources");
|
|
return {};
|
|
}
|
|
|
|
TableSplitter splitter{{}, splits};
|
|
splitter.SplitTable(table.get());
|
|
return table;
|
|
}
|
|
|
|
bool MultiApkGenerator::UpdateManifest(const OutputArtifact& artifact,
|
|
std::unique_ptr<XmlResource>* updated_manifest,
|
|
IDiagnostics* diag) {
|
|
const xml::XmlResource* apk_manifest = apk_->GetManifest();
|
|
if (apk_manifest == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
*updated_manifest = apk_manifest->Clone();
|
|
XmlResource* manifest = updated_manifest->get();
|
|
|
|
// Make sure the first element is <manifest> with package attribute.
|
|
xml::Element* manifest_el = manifest->root.get();
|
|
if (manifest_el == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
if (!manifest_el->namespace_uri.empty() || manifest_el->name != "manifest") {
|
|
diag->Error(DiagMessage(manifest->file.source) << "root tag must be <manifest>");
|
|
return false;
|
|
}
|
|
|
|
// Update the versionCode attribute.
|
|
xml::Attribute* versionCode = manifest_el->FindAttribute(kSchemaAndroid, "versionCode");
|
|
if (versionCode == nullptr) {
|
|
diag->Error(DiagMessage(manifest->file.source) << "manifest must have a versionCode attribute");
|
|
return false;
|
|
}
|
|
|
|
auto* compiled_version = ValueCast<BinaryPrimitive>(versionCode->compiled_value.get());
|
|
if (compiled_version == nullptr) {
|
|
diag->Error(DiagMessage(manifest->file.source) << "versionCode is invalid");
|
|
return false;
|
|
}
|
|
|
|
int new_version = compiled_version->value.data + artifact.version;
|
|
versionCode->compiled_value = ResourceUtils::TryParseInt(std::to_string(new_version));
|
|
|
|
// Check to see if the minSdkVersion needs to be updated.
|
|
if (artifact.android_sdk) {
|
|
// TODO(safarmer): Handle the rest of the Android SDK.
|
|
const AndroidSdk& android_sdk = artifact.android_sdk.value();
|
|
|
|
if (xml::Element* uses_sdk_el = manifest_el->FindChild({}, "uses-sdk")) {
|
|
if (xml::Attribute* min_sdk_attr =
|
|
uses_sdk_el->FindAttribute(xml::kSchemaAndroid, "minSdkVersion")) {
|
|
// Populate with a pre-compiles attribute to we don't need to relink etc.
|
|
const std::string& min_sdk_str = std::to_string(android_sdk.min_sdk_version);
|
|
min_sdk_attr->compiled_value = ResourceUtils::TryParseInt(min_sdk_str);
|
|
} else {
|
|
// There was no minSdkVersion. This is strange since at this point we should have been
|
|
// through the manifest fixer which sets the default minSdkVersion.
|
|
diag->Error(DiagMessage(manifest->file.source) << "missing minSdkVersion from <uses-sdk>");
|
|
return false;
|
|
}
|
|
} else {
|
|
// No uses-sdk present. This is strange since at this point we should have been
|
|
// through the manifest fixer which should have added it.
|
|
diag->Error(DiagMessage(manifest->file.source) << "missing <uses-sdk> from <manifest>");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!artifact.screen_densities.empty()) {
|
|
xml::Element* screens_el = manifest_el->FindChild({}, "compatible-screens");
|
|
if (!screens_el) {
|
|
// create a new element.
|
|
std::unique_ptr<xml::Element> new_screens_el = util::make_unique<xml::Element>();
|
|
new_screens_el->name = "compatible-screens";
|
|
screens_el = new_screens_el.get();
|
|
manifest_el->InsertChild(0, std::move(new_screens_el));
|
|
} else {
|
|
// clear out the old element.
|
|
screens_el->GetChildElements().clear();
|
|
}
|
|
|
|
for (const auto& density : artifact.screen_densities) {
|
|
std::unique_ptr<xml::Element> screen_el = util::make_unique<xml::Element>();
|
|
screen_el->name = "screen";
|
|
const char* density_str = density.toString().string();
|
|
screen_el->attributes.push_back(xml::Attribute{kSchemaAndroid, "screenDensity", density_str});
|
|
screens_el->AppendChild(std::move(screen_el));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace aapt
|