Merge "AAPT2: Split APK by ABI."

This commit is contained in:
TreeHugger Robot
2017-06-21 23:03:04 +00:00
committed by Android (Google) Code Review
12 changed files with 450 additions and 22 deletions

View File

@@ -85,6 +85,7 @@ cc_library_host_static {
"compile/Pseudolocalizer.cpp",
"compile/XmlIdCollector.cpp",
"configuration/ConfigurationParser.cpp",
"filter/AbiFilter.cpp",
"filter/ConfigFilter.cpp",
"flatten/Archive.cpp",
"flatten/TableFlattener.cpp",

View File

@@ -57,6 +57,12 @@ std::unique_ptr<LoadedApk> LoadedApk::LoadApkFromPath(IAaptContext* context,
bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
IArchiveWriter* writer) {
FilterChain empty;
return WriteToArchive(context, options, &empty, writer);
}
bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
FilterChain* filters, IArchiveWriter* writer) {
std::set<std::string> referenced_resources;
// List the files being referenced in the resource table.
for (auto& pkg : table_->packages) {
@@ -89,6 +95,13 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOption
continue;
}
if (!filters->Keep(path)) {
if (context->IsVerbose()) {
context->GetDiagnostics()->Note(DiagMessage() << "Filtered '" << path << "' from APK.");
}
continue;
}
// The resource table needs to be re-serialized since it might have changed.
if (path == "resources.arsc") {
BigBuffer buffer(4096);

View File

@@ -20,6 +20,7 @@
#include "androidfw/StringPiece.h"
#include "ResourceTable.h"
#include "filter/Filter.h"
#include "flatten/Archive.h"
#include "flatten/TableFlattener.h"
#include "io/ZipArchive.h"
@@ -49,6 +50,14 @@ class LoadedApk {
bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
IArchiveWriter* writer);
/**
* Writes the APK on disk at the given path, while also removing the resource
* files that are not referenced in the resource table. The provided filter
* chain is applied to each entry in the APK file.
*/
bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
FilterChain* filters, IArchiveWriter* writer);
static std::unique_ptr<LoadedApk> LoadApkFromPath(IAaptContext* context,
const android::StringPiece& path);

View File

@@ -17,6 +17,7 @@
#include <memory>
#include <vector>
#include "android-base/stringprintf.h"
#include "androidfw/StringPiece.h"
#include "Diagnostics.h"
@@ -26,6 +27,8 @@
#include "SdkConstants.h"
#include "ValueVisitor.h"
#include "cmd/Util.h"
#include "configuration/ConfigurationParser.h"
#include "filter/AbiFilter.h"
#include "flatten/TableFlattener.h"
#include "flatten/XmlFlattener.h"
#include "io/BigBufferInputStream.h"
@@ -33,14 +36,21 @@
#include "optimize/ResourceDeduper.h"
#include "optimize/VersionCollapser.h"
#include "split/TableSplitter.h"
#include "util/Files.h"
using android::StringPiece;
using ::aapt::configuration::Abi;
using ::aapt::configuration::Artifact;
using ::aapt::configuration::Configuration;
using ::android::StringPiece;
using ::android::base::StringPrintf;
namespace aapt {
struct OptimizeOptions {
// Path to the output APK.
std::string output_path;
Maybe<std::string> output_path;
// Path to the output APK directory for splits.
Maybe<std::string> output_dir;
// Details of the app extracted from the AndroidManifest.xml
AppInfo app_info;
@@ -55,6 +65,9 @@ struct OptimizeOptions {
std::vector<SplitConstraints> split_constraints;
TableFlattenerOptions table_flattener_options;
// TODO: Come up with a better name for the Configuration struct.
Maybe<Configuration> configuration;
};
class OptimizeContext : public IAaptContext {
@@ -175,10 +188,52 @@ class OptimizeCommand {
++split_constraints_iter;
}
std::unique_ptr<IArchiveWriter> writer =
CreateZipFileArchiveWriter(context_->GetDiagnostics(), options_.output_path);
if (!apk->WriteToArchive(context_, options_.table_flattener_options, writer.get())) {
return 1;
if (options_.configuration && options_.output_dir) {
Configuration& config = options_.configuration.value();
// For now, just write out the stripped APK since ABI splitting doesn't modify anything else.
for (const Artifact& artifact : config.artifacts) {
if (artifact.abi_group) {
const std::string& group = artifact.abi_group.value();
auto abi_group = config.abi_groups.find(group);
// TODO: Remove validation when configuration parser ensures referential integrity.
if (abi_group == config.abi_groups.end()) {
context_->GetDiagnostics()->Note(
DiagMessage() << "could not find referenced ABI group '" << group << "'");
return 1;
}
FilterChain filters;
filters.AddFilter(AbiFilter::FromAbiList(abi_group->second));
const std::string& path = apk->GetSource().path;
const StringPiece ext = file::GetExtension(path);
const std::string name = path.substr(0, path.rfind(ext.to_string()));
// Name is hard coded for now since only one split dimension is supported.
// TODO: Incorporate name generation into the configuration objects.
const std::string file_name =
StringPrintf("%s.%s%s", name.c_str(), group.c_str(), ext.data());
std::string out = options_.output_dir.value();
file::AppendPath(&out, file_name);
std::unique_ptr<IArchiveWriter> writer =
CreateZipFileArchiveWriter(context_->GetDiagnostics(), out);
if (!apk->WriteToArchive(context_, options_.table_flattener_options, &filters,
writer.get())) {
return 1;
}
}
}
}
if (options_.output_path) {
std::unique_ptr<IArchiveWriter> writer =
CreateZipFileArchiveWriter(context_->GetDiagnostics(), options_.output_path.value());
if (!apk->WriteToArchive(context_, options_.table_flattener_options, writer.get())) {
return 1;
}
}
return 0;
@@ -214,8 +269,8 @@ class OptimizeCommand {
if (file_ref->file == nullptr) {
ResourceNameRef name(pkg->name, type->type, entry->name);
context_->GetDiagnostics()->Warn(DiagMessage(file_ref->GetSource())
<< "file for resource " << name << " with config '"
<< config_value->config << "' not found");
<< "file for resource " << name << " with config '"
<< config_value->config << "' not found");
continue;
}
@@ -293,13 +348,16 @@ bool ExtractAppDataFromManifest(OptimizeContext* context, LoadedApk* apk,
int Optimize(const std::vector<StringPiece>& args) {
OptimizeContext context;
OptimizeOptions options;
Maybe<std::string> config_path;
Maybe<std::string> target_densities;
std::vector<std::string> configs;
std::vector<std::string> split_args;
bool verbose = false;
Flags flags =
Flags()
.RequiredFlag("-o", "Path to the output APK.", &options.output_path)
.OptionalFlag("-o", "Path to the output APK.", &options.output_path)
.OptionalFlag("-d", "Path to the output directory (for splits).", &options.output_dir)
.OptionalFlag("-x", "Path to XML configuration file.", &config_path)
.OptionalFlag(
"--target-densities",
"Comma separated list of the screen densities that the APK will be optimized for.\n"
@@ -369,6 +427,22 @@ int Optimize(const std::vector<StringPiece>& args) {
}
}
if (config_path) {
if (!options.output_dir) {
context.GetDiagnostics()->Error(
DiagMessage() << "Output directory is required when using a configuration file");
return 1;
}
std::string& path = config_path.value();
Maybe<ConfigurationParser> for_path = ConfigurationParser::ForPath(path);
if (for_path) {
options.configuration = for_path.value().WithDiagnostics(context.GetDiagnostics()).Parse();
} else {
context.GetDiagnostics()->Error(DiagMessage() << "Could not parse config file " << path);
return 1;
}
}
if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) {
return 1;
}

View File

@@ -18,6 +18,7 @@
#include <algorithm>
#include <functional>
#include <map>
#include <memory>
#include <utility>
@@ -56,15 +57,15 @@ using ::aapt::xml::XmlActionExecutorPolicy;
using ::aapt::xml::XmlNodeAction;
using ::android::base::ReadFileToString;
const std::unordered_map<std::string, Abi> kAbiMap = {
{"armeabi", Abi::kArmeV6},
{"armeabi-v7a", Abi::kArmV7a},
{"arm64-v8a", Abi::kArm64V8a},
{"x86", Abi::kX86},
{"x86_64", Abi::kX86_64},
{"mips", Abi::kMips},
{"mips64", Abi::kMips64},
{"universal", Abi::kUniversal},
const std::unordered_map<std::string, Abi> kStringToAbiMap = {
{"armeabi", Abi::kArmeV6}, {"armeabi-v7a", Abi::kArmV7a}, {"arm64-v8a", Abi::kArm64V8a},
{"x86", Abi::kX86}, {"x86_64", Abi::kX86_64}, {"mips", Abi::kMips},
{"mips64", Abi::kMips64}, {"universal", Abi::kUniversal},
};
const std::map<Abi, std::string> kAbiToStringMap = {
{Abi::kArmeV6, "armeabi"}, {Abi::kArmV7a, "armeabi-v7a"}, {Abi::kArm64V8a, "arm64-v8a"},
{Abi::kX86, "x86"}, {Abi::kX86_64, "x86_64"}, {Abi::kMips, "mips"},
{Abi::kMips64, "mips64"}, {Abi::kUniversal, "universal"},
};
constexpr const char* kAaptXmlNs = "http://schemas.android.com/tools/aapt";
@@ -102,7 +103,13 @@ class NamespaceVisitor : public xml::Visitor {
} // namespace
namespace configuration {
const std::string& AbiToString(Abi abi) {
return kAbiToStringMap.find(abi)->second;
}
} // namespace configuration
/** Returns a ConfigurationParser for the file located at the provided path. */
Maybe<ConfigurationParser> ConfigurationParser::ForPath(const std::string& path) {
@@ -175,6 +182,9 @@ Maybe<Configuration> ConfigurationParser::Parse() {
return {};
}
// TODO: Validate all references in the configuration are valid. It should be safe to assume from
// this point on that any references from one section to another will be present.
return {config};
}
@@ -201,7 +211,7 @@ ConfigurationParser::ActionHandler ConfigurationParser::artifact_handler_ =
DiagMessage() << "Unknown artifact attribute: " << attr.name << " = " << attr.value);
}
}
config->artifacts[artifact.name] = artifact;
config->artifacts.push_back(artifact);
return true;
};
@@ -236,7 +246,7 @@ ConfigurationParser::ActionHandler ConfigurationParser::abi_group_handler_ =
for (auto& node : child->children) {
xml::Text* t;
if ((t = NodeCast<xml::Text>(node.get())) != nullptr) {
group.push_back(kAbiMap.at(TrimWhitespace(t->text).to_string()));
group.push_back(kStringToAbiMap.at(TrimWhitespace(t->text).to_string()));
break;
}
}

View File

@@ -62,6 +62,9 @@ enum class Abi {
kUniversal
};
/** Helper method to convert an ABI to a string representing the path within the APK. */
const std::string& AbiToString(Abi abi);
/**
* Represents an individual locale. When a locale is included, it must be
* declared from least specific to most specific, as a region does not make
@@ -118,7 +121,8 @@ struct GlTexture {
* AAPT2 XML configuration binary representation.
*/
struct Configuration {
std::unordered_map<std::string, Artifact> artifacts;
// TODO: Support named artifacts?
std::vector<Artifact> artifacts;
Maybe<std::string> artifact_format;
Group<Abi> abi_groups;

View File

@@ -196,7 +196,7 @@ TEST_F(ConfigurationParserTest, ArtifactAction) {
EXPECT_EQ(1ul, config.artifacts.size());
auto& artifact = config.artifacts.begin()->second;
auto& artifact = config.artifacts.front();
EXPECT_EQ("", artifact.name); // TODO: make this fail.
EXPECT_EQ("arm", artifact.abi_group.value());
EXPECT_EQ("large", artifact.screen_density_group.value());
@@ -204,6 +204,21 @@ TEST_F(ConfigurationParserTest, ArtifactAction) {
EXPECT_EQ("19", artifact.android_sdk_group.value());
EXPECT_EQ("dxt1", artifact.gl_texture_group.value());
EXPECT_EQ("low-latency", artifact.device_feature_group.value());
// Perform a second action to ensure we get 2 artifacts.
static constexpr const char* second = R"xml(
<artifact
abi-group="other"
screen-density-group="large"
locale-group="europe"
android-sdk-group="19"
gl-texture-group="dxt1"
device-feature-group="low-latency"/>)xml";
doc = test::BuildXmlDom(second);
ok = artifact_handler_(&config, NodeCast<Element>(doc.get()->root.get()), &diag_);
ASSERT_TRUE(ok);
EXPECT_EQ(2ul, config.artifacts.size());
}
TEST_F(ConfigurationParserTest, ArtifactFormatAction) {

View File

@@ -0,0 +1,51 @@
/*
* 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 "AbiFilter.h"
#include <memory>
#include "io/Util.h"
namespace aapt {
std::unique_ptr<AbiFilter> AbiFilter::FromAbiList(const std::vector<configuration::Abi>& abi_list) {
std::unordered_set<std::string> abi_set;
for (auto& abi : abi_list) {
abi_set.insert(configuration::AbiToString(abi));
}
// Make unique by hand as the constructor is private.
return std::unique_ptr<AbiFilter>(new AbiFilter(abi_set));
}
bool AbiFilter::Keep(const std::string& path) {
// We only care about libraries.
if (!util::StartsWith(path, kLibPrefix)) {
return true;
}
auto abi_end = path.find('/', kLibPrefixLen);
if (abi_end == std::string::npos) {
// Ignore any files in the top level lib directory.
return true;
}
// Strip the lib/ prefix.
const std::string& path_abi = path.substr(kLibPrefixLen, abi_end - kLibPrefixLen);
return (abis_.find(path_abi) != abis_.end());
}
} // namespace aapt

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
#ifndef AAPT2_ABISPLITTER_H
#define AAPT2_ABISPLITTER_H
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include "configuration/ConfigurationParser.h"
#include "filter/Filter.h"
namespace aapt {
/**
* Filters native library paths by ABI. ABIs present in the filter list are kept and all over
* libraries are removed. The filter is only applied to native library paths (this under lib/).
*/
class AbiFilter : public IPathFilter {
public:
/** Factory method to create a filter from a list of configuration::Abi. */
static std::unique_ptr<AbiFilter> FromAbiList(const std::vector<configuration::Abi>& abi_list);
/** Returns true if the path is for a native library in the list of desired ABIs. */
bool Keep(const std::string& path) override;
private:
explicit AbiFilter(std::unordered_set<std::string> abis) : abis_(std::move(abis)) {
}
/** The path prefix to where all native libs end up inside an APK file. */
static constexpr const char* kLibPrefix = "lib/";
static constexpr size_t kLibPrefixLen = 4;
const std::unordered_set<std::string> abis_;
};
} // namespace aapt
#endif // AAPT2_ABISPLITTER_H

View File

@@ -0,0 +1,66 @@
/*
* 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 "filter/AbiFilter.h"
#include <string>
#include "gtest/gtest.h"
namespace aapt {
namespace {
using ::aapt::configuration::Abi;
struct TestData {
std::string path;
bool kept;
};
const TestData kTestData[] = {
/* Keep. */
{"lib/mips/libnative.so", true},
{"not/native/file.txt", true},
// Not sure if this is a valid use case.
{"lib/listing.txt", true},
{"lib/mips/foo/bar/baz.so", true},
{"lib/mips/x86/foo.so", true},
/* Discard. */
{"lib/mips_horse/foo.so", false},
{"lib/horse_mips/foo.so", false},
{"lib/mips64/armeabi-v7a/foo.so", false},
{"lib/mips64/x86_64/x86.so", false},
{"lib/x86/libnative.so", false},
{"lib/x86/foo/bar/baz.so", false},
{"lib/x86/x86/foo.so", false},
{"lib/x86_horse/foo.so", false},
{"lib/horse_x86/foo.so", false},
{"lib/x86/armeabi-v7a/foo.so", false},
{"lib/x86_64/x86_64/x86.so", false},
};
class AbiFilterTest : public ::testing::TestWithParam<TestData> {};
TEST_P(AbiFilterTest, Keep) {
auto mips = AbiFilter::FromAbiList({Abi::kMips});
const TestData& data = GetParam();
EXPECT_EQ(mips->Keep(data.path), data.kept);
}
INSTANTIATE_TEST_CASE_P(NativePaths, AbiFilterTest, ::testing::ValuesIn(kTestData));
} // namespace
} // namespace aapt

View File

@@ -0,0 +1,77 @@
/*
* 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.
*/
#ifndef AAPT2_FILTER_H
#define AAPT2_FILTER_H
#include <string>
#include <vector>
#include "util/Util.h"
namespace aapt {
/** A filter to be applied to a path segment. */
class IPathFilter {
public:
~IPathFilter() = default;
/** Returns true if the path should be kept. */
virtual bool Keep(const std::string& path) = 0;
};
/**
* Path filter that keeps anything that matches the provided prefix.
*/
class PrefixFilter : public IPathFilter {
public:
explicit PrefixFilter(std::string prefix) : prefix_(std::move(prefix)) {
}
/** Returns true if the provided path matches the prefix. */
bool Keep(const std::string& path) override {
return util::StartsWith(path, prefix_);
}
private:
const std::string prefix_;
};
/** Applies a set of IPathFilters to a path and returns true iif all filters keep the path. */
class FilterChain : public IPathFilter {
public:
/** Adds a filter to the list to be applied to each path. */
void AddFilter(std::unique_ptr<IPathFilter> filter) {
filters_.push_back(std::move(filter));
}
/** Returns true if all filters keep the path. */
bool Keep(const std::string& path) override {
for (auto& filter : filters_) {
if (!filter->Keep(path)) {
return false;
}
}
return true;
}
private:
std::vector<std::unique_ptr<IPathFilter>> filters_;
};
} // namespace aapt
#endif // AAPT2_FILTER_H

View File

@@ -0,0 +1,54 @@
/*
* 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 "filter/Filter.h"
#include <string>
#include "io/Util.h"
#include "gtest/gtest.h"
namespace aapt {
namespace {
TEST(FilterChainTest, EmptyChain) {
FilterChain chain;
ASSERT_TRUE(chain.Keep("some/random/path"));
}
TEST(FilterChainTest, SingleFilter) {
FilterChain chain;
chain.AddFilter(util::make_unique<PrefixFilter>("keep/"));
ASSERT_FALSE(chain.Keep("removed/path"));
ASSERT_TRUE(chain.Keep("keep/path/1"));
ASSERT_TRUE(chain.Keep("keep/path/2"));
}
TEST(FilterChainTest, MultipleFilters) {
FilterChain chain;
chain.AddFilter(util::make_unique<PrefixFilter>("keep/"));
chain.AddFilter(util::make_unique<PrefixFilter>("keep/really/"));
ASSERT_FALSE(chain.Keep("removed/path"));
ASSERT_FALSE(chain.Keep("/keep/really/wrong/prefix"));
ASSERT_FALSE(chain.Keep("keep/maybe/1"));
ASSERT_TRUE(chain.Keep("keep/really/1"));
}
} // namespace
} // namespace aapt