diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 1dc47f88ab927..d39a8a8237b7d 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -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", diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp index 8a8f8be205e7e..7e5efa15f61b8 100644 --- a/tools/aapt2/LoadedApk.cpp +++ b/tools/aapt2/LoadedApk.cpp @@ -57,6 +57,12 @@ std::unique_ptr 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 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); diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h index 59eb8161a868b..8aa9674aa2ed9 100644 --- a/tools/aapt2/LoadedApk.h +++ b/tools/aapt2/LoadedApk.h @@ -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 LoadApkFromPath(IAaptContext* context, const android::StringPiece& path); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 194c0c80c2b23..0d69c892c103f 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -17,6 +17,7 @@ #include #include +#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 output_path; + // Path to the output APK directory for splits. + Maybe output_dir; // Details of the app extracted from the AndroidManifest.xml AppInfo app_info; @@ -55,6 +65,9 @@ struct OptimizeOptions { std::vector split_constraints; TableFlattenerOptions table_flattener_options; + + // TODO: Come up with a better name for the Configuration struct. + Maybe configuration; }; class OptimizeContext : public IAaptContext { @@ -175,10 +188,52 @@ class OptimizeCommand { ++split_constraints_iter; } - std::unique_ptr 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 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 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& args) { OptimizeContext context; OptimizeOptions options; + Maybe config_path; Maybe target_densities; std::vector configs; std::vector 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& 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 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; } diff --git a/tools/aapt2/configuration/ConfigurationParser.cpp b/tools/aapt2/configuration/ConfigurationParser.cpp index 303a809fbaa9e..555cb35c0bb9a 100644 --- a/tools/aapt2/configuration/ConfigurationParser.cpp +++ b/tools/aapt2/configuration/ConfigurationParser.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -56,15 +57,15 @@ using ::aapt::xml::XmlActionExecutorPolicy; using ::aapt::xml::XmlNodeAction; using ::android::base::ReadFileToString; -const std::unordered_map 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 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 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::ForPath(const std::string& path) { @@ -175,6 +182,9 @@ Maybe 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(node.get())) != nullptr) { - group.push_back(kAbiMap.at(TrimWhitespace(t->text).to_string())); + group.push_back(kStringToAbiMap.at(TrimWhitespace(t->text).to_string())); break; } } diff --git a/tools/aapt2/configuration/ConfigurationParser.h b/tools/aapt2/configuration/ConfigurationParser.h index 8b9c0853773bf..0435cbf72a229 100644 --- a/tools/aapt2/configuration/ConfigurationParser.h +++ b/tools/aapt2/configuration/ConfigurationParser.h @@ -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 artifacts; + // TODO: Support named artifacts? + std::vector artifacts; Maybe artifact_format; Group abi_groups; diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp index 8421ee3fcff0d..a8c385823fa90 100644 --- a/tools/aapt2/configuration/ConfigurationParser_test.cpp +++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp @@ -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( + )xml"; + doc = test::BuildXmlDom(second); + + ok = artifact_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + EXPECT_EQ(2ul, config.artifacts.size()); } TEST_F(ConfigurationParserTest, ArtifactFormatAction) { diff --git a/tools/aapt2/filter/AbiFilter.cpp b/tools/aapt2/filter/AbiFilter.cpp new file mode 100644 index 0000000000000..cb96235f98f9b --- /dev/null +++ b/tools/aapt2/filter/AbiFilter.cpp @@ -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 + +#include "io/Util.h" + +namespace aapt { + +std::unique_ptr AbiFilter::FromAbiList(const std::vector& abi_list) { + std::unordered_set 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(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 diff --git a/tools/aapt2/filter/AbiFilter.h b/tools/aapt2/filter/AbiFilter.h new file mode 100644 index 0000000000000..d875cb2b127b8 --- /dev/null +++ b/tools/aapt2/filter/AbiFilter.h @@ -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 +#include +#include +#include + +#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 FromAbiList(const std::vector& 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 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 abis_; +}; + +} // namespace aapt + +#endif // AAPT2_ABISPLITTER_H diff --git a/tools/aapt2/filter/AbiFilter_test.cpp b/tools/aapt2/filter/AbiFilter_test.cpp new file mode 100644 index 0000000000000..0c8ea3575a295 --- /dev/null +++ b/tools/aapt2/filter/AbiFilter_test.cpp @@ -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 + +#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 {}; + +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 diff --git a/tools/aapt2/filter/Filter.h b/tools/aapt2/filter/Filter.h new file mode 100644 index 0000000000000..d737dc92e87b4 --- /dev/null +++ b/tools/aapt2/filter/Filter.h @@ -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 +#include + +#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 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> filters_; +}; + +} // namespace aapt + +#endif // AAPT2_FILTER_H diff --git a/tools/aapt2/filter/Filter_test.cpp b/tools/aapt2/filter/Filter_test.cpp new file mode 100644 index 0000000000000..fb75a4b4d7c19 --- /dev/null +++ b/tools/aapt2/filter/Filter_test.cpp @@ -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 + +#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("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("keep/")); + chain.AddFilter(util::make_unique("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