diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 0d6dc3522d24e0b91595af125b821cdd09515986..ed3e1ac5c39fba06d5c59d1de37bda6760d6898d 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -121,7 +121,6 @@ cc_library_host_static { "link/AutoVersioner.cpp", "link/ManifestFixer.cpp", "link/NoDefaultResourceRemover.cpp", - "link/ProductFilter.cpp", "link/PrivateAttributeMover.cpp", "link/ReferenceLinker.cpp", "link/ResourceExcluder.cpp", @@ -134,6 +133,7 @@ cc_library_host_static { "optimize/ResourceFilter.cpp", "optimize/Obfuscator.cpp", "optimize/VersionCollapser.cpp", + "process/ProductFilter.cpp", "process/SymbolTable.cpp", "split/TableSplitter.cpp", "text/Printer.cpp", diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 03f9715fb265d8f9b94f99a51fb378f80c6796f1..bb7b13a71412e3cd70183e7e2e05c85e17e44273 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -45,6 +45,7 @@ #include "io/StringStream.h" #include "io/Util.h" #include "io/ZipArchive.h" +#include "process/ProductFilter.h" #include "trace/TraceBuffer.h" #include "util/Files.h" #include "util/Util.h" @@ -179,6 +180,15 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, if (!res_parser.Parse(&xml_parser)) { return false; } + + if (options.product_.has_value()) { + if (!ProductFilter({*options.product_}, /* remove_default_config_values = */ true) + .Consume(context, &table)) { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) + << "failed to filter product"); + return false; + } + } } if (options.pseudolocalize && translatable_file) { diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index 14a730a1b1a04d9d9288c1328cdb2bb8992dcf8b..984d8901ac5633d36f541dad42011fcac9fe4b5f 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -42,6 +42,7 @@ struct CompileOptions { // See comments on aapt::ResourceParserOptions. bool preserve_visibility_of_styleables = false; bool verbose = false; + std::optional<std::string> product_; }; /** Parses flags and compiles resources to be used in linking. */ @@ -76,6 +77,10 @@ class CompileCommand : public Command { AddOptionalFlag("--source-path", "Sets the compiled resource file source file path to the given string.", &options_.source_path); + AddOptionalFlag("--filter-product", + "Leave only resources specific to the given product. All " + "other resources (including defaults) are removed.", + &options_.product_); } int Action(const std::vector<std::string>& args) override; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 97404fc69af25f711b27b9afce15e6f2f49e40c5..f00be3688597e1f388e5b56639952383f5198db9 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -66,6 +66,7 @@ #include "optimize/ResourceDeduper.h" #include "optimize/VersionCollapser.h" #include "process/IResourceTableConsumer.h" +#include "process/ProductFilter.h" #include "process/SymbolTable.h" #include "split/TableSplitter.h" #include "trace/TraceBuffer.h" @@ -2127,7 +2128,7 @@ class Linker { << "can't select products when building static library"); } } else { - ProductFilter product_filter(options_.products); + ProductFilter product_filter(options_.products, /* remove_default_config_values = */ false); if (!product_filter.Consume(context_, &final_table_)) { context_->GetDiagnostics()->Error(android::DiagMessage() << "failed stripping products"); return 1; diff --git a/tools/aapt2/link/Linkers.h b/tools/aapt2/link/Linkers.h index 44cd276f77a256fac194ef07eb8086be9976d4d1..18165f7d489fbbbce581df74386a9754862bdab6 100644 --- a/tools/aapt2/link/Linkers.h +++ b/tools/aapt2/link/Linkers.h @@ -20,12 +20,12 @@ #include <set> #include <unordered_set> +#include "Resource.h" +#include "SdkConstants.h" #include "android-base/macros.h" +#include "android-base/result.h" #include "androidfw/ConfigDescription.h" #include "androidfw/StringPiece.h" - -#include "Resource.h" -#include "SdkConstants.h" #include "process/IResourceTableConsumer.h" #include "xml/XmlDom.h" @@ -92,28 +92,6 @@ class PrivateAttributeMover : public IResourceTableConsumer { DISALLOW_COPY_AND_ASSIGN(PrivateAttributeMover); }; -class ResourceConfigValue; - -class ProductFilter : public IResourceTableConsumer { - public: - using ResourceConfigValueIter = std::vector<std::unique_ptr<ResourceConfigValue>>::iterator; - - explicit ProductFilter(std::unordered_set<std::string> products) : products_(products) { - } - - ResourceConfigValueIter SelectProductToKeep(const ResourceNameRef& name, - const ResourceConfigValueIter begin, - const ResourceConfigValueIter end, - android::IDiagnostics* diag); - - bool Consume(IAaptContext* context, ResourceTable* table) override; - - private: - DISALLOW_COPY_AND_ASSIGN(ProductFilter); - - std::unordered_set<std::string> products_; -}; - // Removes namespace nodes and URI information from the XmlResource. // // Once an XmlResource is processed by this consumer, it is no longer able to have its attributes diff --git a/tools/aapt2/link/ProductFilter.cpp b/tools/aapt2/process/ProductFilter.cpp similarity index 60% rename from tools/aapt2/link/ProductFilter.cpp rename to tools/aapt2/process/ProductFilter.cpp index 9544986fda76a77e13941fe1e2029b5fd14cad03..0b1c0a6adb51f9d814eba1ff77794d5ac094e8c5 100644 --- a/tools/aapt2/link/ProductFilter.cpp +++ b/tools/aapt2/process/ProductFilter.cpp @@ -14,16 +14,18 @@ * limitations under the License. */ -#include "link/Linkers.h" +#include "process/ProductFilter.h" + +#include <algorithm> #include "ResourceTable.h" #include "trace/TraceBuffer.h" namespace aapt { -ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep( - const ResourceNameRef& name, const ResourceConfigValueIter begin, - const ResourceConfigValueIter end, android::IDiagnostics* diag) { +std::optional<ProductFilter::ResourceConfigValueIter> ProductFilter::SelectProductToKeep( + const ResourceNameRef& name, ResourceConfigValueIter begin, ResourceConfigValueIter end, + android::IDiagnostics* diag) { ResourceConfigValueIter default_product_iter = end; ResourceConfigValueIter selected_product_iter = end; @@ -36,12 +38,11 @@ ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep( << "selection of product '" << config_value->product << "' for resource " << name << " is ambiguous"); - ResourceConfigValue* previously_selected_config_value = - selected_product_iter->get(); + ResourceConfigValue* previously_selected_config_value = selected_product_iter->get(); diag->Note(android::DiagMessage(previously_selected_config_value->value->GetSource()) << "product '" << previously_selected_config_value->product << "' is also a candidate"); - return end; + return std::nullopt; } // Select this product. @@ -54,11 +55,10 @@ ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep( diag->Error(android::DiagMessage(config_value->value->GetSource()) << "multiple default products defined for resource " << name); - ResourceConfigValue* previously_default_config_value = - default_product_iter->get(); + ResourceConfigValue* previously_default_config_value = default_product_iter->get(); diag->Note(android::DiagMessage(previously_default_config_value->value->GetSource()) << "default product also defined here"); - return end; + return std::nullopt; } // Mark the default. @@ -66,9 +66,16 @@ ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep( } } + if (remove_default_config_values_) { + // If we are leaving only a specific product, return early here instead of selecting the default + // value. Returning end here will cause this value set to be skipped, and will be removed with + // ClearEmptyValues method. + return selected_product_iter; + } + if (default_product_iter == end) { diag->Error(android::DiagMessage() << "no default product defined for resource " << name); - return end; + return std::nullopt; } if (selected_product_iter == end) { @@ -89,20 +96,27 @@ bool ProductFilter::Consume(IAaptContext* context, ResourceTable* table) { ResourceConfigValueIter start_range_iter = iter; while (iter != entry->values.end()) { ++iter; - if (iter == entry->values.end() || - (*iter)->config != (*start_range_iter)->config) { + if (iter == entry->values.end() || (*iter)->config != (*start_range_iter)->config) { // End of the array, or we saw a different config, // so this must be the end of a range of products. // Select the product to keep from the set of products defined. ResourceNameRef name(pkg->name, type->named_type, entry->name); - auto value_to_keep = SelectProductToKeep( - name, start_range_iter, iter, context->GetDiagnostics()); - if (value_to_keep == iter) { + auto value_to_keep = + SelectProductToKeep(name, start_range_iter, iter, context->GetDiagnostics()); + if (!value_to_keep.has_value()) { // An error occurred, we could not pick a product. error = true; - } else { + } else if (auto val = value_to_keep.value(); val != iter) { // We selected a product to keep. Move it to the new array. - new_values.push_back(std::move(*value_to_keep)); + if (remove_default_config_values_) { + // We are filtering values with the given product. The selected value here will be + // a new default value, and all other values will be removed. + new_values.push_back( + std::make_unique<ResourceConfigValue>((*val)->config, android::StringPiece{})); + new_values.back()->value = std::move((*val)->value); + } else { + new_values.push_back(std::move(*val)); + } } // Start the next range of products. @@ -115,7 +129,27 @@ bool ProductFilter::Consume(IAaptContext* context, ResourceTable* table) { } } } + + if (remove_default_config_values_) { + ClearEmptyValues(table); + } + return !error; } +void ProductFilter::ClearEmptyValues(ResourceTable* table) { + // Clear any empty packages/types/entries, as remove_default_config_values_ may remove an entire + // value set. + CHECK(remove_default_config_values_) + << __func__ << " should only be called when remove_default_config_values_ is set"; + + for (auto& pkg : table->packages) { + for (auto& type : pkg->types) { + std::erase_if(type->entries, [](auto& entry) { return entry->values.empty(); }); + } + std::erase_if(pkg->types, [](auto& type) { return type->entries.empty(); }); + } + std::erase_if(table->packages, [](auto& package) { return package->types.empty(); }); +} + } // namespace aapt diff --git a/tools/aapt2/process/ProductFilter.h b/tools/aapt2/process/ProductFilter.h new file mode 100644 index 0000000000000000000000000000000000000000..0ec2f00863fcf3baf9f8ef571708bc4a42a24ada --- /dev/null +++ b/tools/aapt2/process/ProductFilter.h @@ -0,0 +1,65 @@ +#pragma once + +#include <memory> +#include <optional> +#include <string> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "Resource.h" +#include "android-base/macros.h" +#include "androidfw/ConfigDescription.h" +#include "androidfw/IDiagnostics.h" +#include "process/IResourceTableConsumer.h" + +namespace aapt { + +class ResourceConfigValue; + +class ProductFilter : public IResourceTableConsumer { + public: + using ResourceConfigValueIter = std::vector<std::unique_ptr<ResourceConfigValue>>::iterator; + + // Setting remove_default_config_values will remove all values other than + // specified product, including default. For example, if the following table + // + // <string name="foo" product="default">foo_default</string> + // <string name="foo" product="tablet">foo_tablet</string> + // <string name="bar">bar</string> + // + // is consumed with tablet, it will result in + // + // <string name="foo">foo_tablet</string> + // + // removing foo_default and bar. This option is to generate an RRO package + // with given product. + explicit ProductFilter(std::unordered_set<std::string> products, + bool remove_default_config_values) + : products_(std::move(products)), + remove_default_config_values_(remove_default_config_values) { + } + + bool Consume(IAaptContext* context, ResourceTable* table) override; + + private: + DISALLOW_COPY_AND_ASSIGN(ProductFilter); + + // SelectProductToKeep returns an iterator for the selected value. + // + // Returns std::nullopt in case of failure (e.g. ambiguous values, missing or duplicated default + // values). + // Returns `end` if keep_as_default_product is set and no value for the specified product was + // found. + std::optional<ResourceConfigValueIter> SelectProductToKeep(const ResourceNameRef& name, + ResourceConfigValueIter begin, + ResourceConfigValueIter end, + android::IDiagnostics* diag); + + void ClearEmptyValues(ResourceTable* table); + + std::unordered_set<std::string> products_; + bool remove_default_config_values_; +}; + +} // namespace aapt diff --git a/tools/aapt2/link/ProductFilter_test.cpp b/tools/aapt2/process/ProductFilter_test.cpp similarity index 55% rename from tools/aapt2/link/ProductFilter_test.cpp rename to tools/aapt2/process/ProductFilter_test.cpp index 2cb9afa05cad6cd1c1fa3785266e9d0922fd6292..27a82dcc34531c73e6bccb103b851c2ffb689cd0 100644 --- a/tools/aapt2/link/ProductFilter_test.cpp +++ b/tools/aapt2/process/ProductFilter_test.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "link/Linkers.h" +#include "process/ProductFilter.h" #include "test/Test.h" @@ -57,17 +57,15 @@ TEST(ProductFilterTest, SelectTwoProducts) { .Build(), context->GetDiagnostics())); - ProductFilter filter({"tablet"}); + ProductFilter filter({"tablet"}, /* remove_default_config_values = */ false); ASSERT_TRUE(filter.Consume(context.get(), &table)); - EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>( - &table, "android:string/one", land, "")); - EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>( - &table, "android:string/one", land, "tablet")); - EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>( - &table, "android:string/one", port, "")); - EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>( - &table, "android:string/one", port, "tablet")); + EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, "")); + EXPECT_NE(nullptr, + test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, "tablet")); + EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, "")); + EXPECT_NE(nullptr, + test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, "tablet")); } TEST(ProductFilterTest, SelectDefaultProduct) { @@ -88,15 +86,15 @@ TEST(ProductFilterTest, SelectDefaultProduct) { context->GetDiagnostics())); ; - ProductFilter filter(std::unordered_set<std::string>{}); + ProductFilter filter(std::unordered_set<std::string>{}, + /* remove_default_config_values = */ false); ASSERT_TRUE(filter.Consume(context.get(), &table)); - EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>( - &table, "android:string/one", - ConfigDescription::DefaultConfig(), "")); - EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>( - &table, "android:string/one", - ConfigDescription::DefaultConfig(), "tablet")); + EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", + ConfigDescription::DefaultConfig(), "")); + EXPECT_EQ(nullptr, + test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", + ConfigDescription::DefaultConfig(), "tablet")); } TEST(ProductFilterTest, FailOnAmbiguousProduct) { @@ -123,7 +121,7 @@ TEST(ProductFilterTest, FailOnAmbiguousProduct) { .Build(), context->GetDiagnostics())); - ProductFilter filter({"tablet", "no-sdcard"}); + ProductFilter filter({"tablet", "no-sdcard"}, /* remove_default_config_values = */ false); ASSERT_FALSE(filter.Consume(context.get(), &table)); } @@ -144,8 +142,67 @@ TEST(ProductFilterTest, FailOnMultipleDefaults) { .Build(), context->GetDiagnostics())); - ProductFilter filter(std::unordered_set<std::string>{}); + ProductFilter filter(std::unordered_set<std::string>{}, + /* remove_default_config_values = */ false); ASSERT_FALSE(filter.Consume(context.get(), &table)); } +TEST(ProductFilterTest, RemoveDefaultConfigValues) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + const ConfigDescription land = test::ParseConfigOrDie("land"); + const ConfigDescription port = test::ParseConfigOrDie("port"); + + ResourceTable table; + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(), + land) + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/tablet.xml")).Build(), + land, "tablet") + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/two")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(), + land) + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(), + port) + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/tablet.xml")).Build(), + port, "tablet") + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/two")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(), + port) + .Build(), + context->GetDiagnostics())); + + ProductFilter filter({"tablet"}, /* remove_default_config_values = */ true); + ASSERT_TRUE(filter.Consume(context.get(), &table)); + + EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, "")); + EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/two", land, "")); + EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, "")); + EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/two", port, "")); +} + } // namespace aapt