diff options
author | 2023-09-15 16:38:50 +0900 | |
---|---|---|
committer | 2023-10-31 13:03:39 +0900 | |
commit | 5fe521e5b3bd5e7ef17dc50425060680cadb4e0e (patch) | |
tree | 267201cbbb59d2fb9ea76620887f92b22249c4a9 /tools/aapt2/process | |
parent | fa8c3a142a791cfc176c09e80d3ab3acb3391abe (diff) |
Implement filter-product
filter-product option is added to aapt2 compile, which filters values
with a given product and removes all other values (including default).
The motivation of this change is to generate product-specific RRO
automatically, making a main resource APK (e.g. framework-res.apk)
identical in all targets.
Bug: 294799593
Test: build
Change-Id: I42eb3e134c7aa120f6bbe2d26d311bd46b586595
Diffstat (limited to 'tools/aapt2/process')
-rw-r--r-- | tools/aapt2/process/ProductFilter.cpp | 155 | ||||
-rw-r--r-- | tools/aapt2/process/ProductFilter.h | 65 | ||||
-rw-r--r-- | tools/aapt2/process/ProductFilter_test.cpp | 208 |
3 files changed, 428 insertions, 0 deletions
diff --git a/tools/aapt2/process/ProductFilter.cpp b/tools/aapt2/process/ProductFilter.cpp new file mode 100644 index 000000000000..0b1c0a6adb51 --- /dev/null +++ b/tools/aapt2/process/ProductFilter.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 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 "process/ProductFilter.h" + +#include <algorithm> + +#include "ResourceTable.h" +#include "trace/TraceBuffer.h" + +namespace aapt { + +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; + + for (ResourceConfigValueIter iter = begin; iter != end; ++iter) { + ResourceConfigValue* config_value = iter->get(); + if (products_.find(config_value->product) != products_.end()) { + if (selected_product_iter != end) { + // We have two possible values for this product! + diag->Error(android::DiagMessage(config_value->value->GetSource()) + << "selection of product '" << config_value->product << "' for resource " + << name << " is ambiguous"); + + 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 std::nullopt; + } + + // Select this product. + selected_product_iter = iter; + } + + if (config_value->product.empty() || config_value->product == "default") { + if (default_product_iter != end) { + // We have two possible default values. + diag->Error(android::DiagMessage(config_value->value->GetSource()) + << "multiple default products defined for resource " << name); + + 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 std::nullopt; + } + + // Mark the default. + default_product_iter = iter; + } + } + + 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 std::nullopt; + } + + if (selected_product_iter == end) { + selected_product_iter = default_product_iter; + } + return selected_product_iter; +} + +bool ProductFilter::Consume(IAaptContext* context, ResourceTable* table) { + TRACE_NAME("ProductFilter::Consume"); + bool error = false; + for (auto& pkg : table->packages) { + for (auto& type : pkg->types) { + for (auto& entry : type->entries) { + std::vector<std::unique_ptr<ResourceConfigValue>> new_values; + + ResourceConfigValueIter iter = entry->values.begin(); + ResourceConfigValueIter start_range_iter = iter; + while (iter != entry->values.end()) { + ++iter; + 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.has_value()) { + // An error occurred, we could not pick a product. + error = true; + } else if (auto val = value_to_keep.value(); val != iter) { + // We selected a product to keep. Move it to the new array. + 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. + start_range_iter = iter; + } + } + + // Now move the new values in to place. + entry->values = std::move(new_values); + } + } + } + + 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 000000000000..0ec2f00863fc --- /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/process/ProductFilter_test.cpp b/tools/aapt2/process/ProductFilter_test.cpp new file mode 100644 index 000000000000..27a82dcc3453 --- /dev/null +++ b/tools/aapt2/process/ProductFilter_test.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2016 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 "process/ProductFilter.h" + +#include "test/Test.h" + +using ::android::ConfigDescription; + +namespace aapt { + +TEST(ProductFilterTest, SelectTwoProducts) { + 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/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())); + + 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")); +} + +TEST(ProductFilterTest, SelectDefaultProduct) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + ResourceTable table; + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build()) + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("tablet.xml")).Build(), {}, + "tablet") + .Build(), + context->GetDiagnostics())); + ; + + 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")); +} + +TEST(ProductFilterTest, FailOnAmbiguousProduct) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + ResourceTable table; + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build()) + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("tablet.xml")).Build(), {}, + "tablet") + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("no-sdcard.xml")).Build(), + {}, "no-sdcard") + .Build(), + context->GetDiagnostics())); + + ProductFilter filter({"tablet", "no-sdcard"}, /* remove_default_config_values = */ false); + ASSERT_FALSE(filter.Consume(context.get(), &table)); +} + +TEST(ProductFilterTest, FailOnMultipleDefaults) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + ResourceTable table; + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source(".xml")).Build()) + .Build(), + context->GetDiagnostics())); + + ASSERT_TRUE(table.AddResource( + NewResourceBuilder(test::ParseNameOrDie("android:string/one")) + .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build(), {}, + "default") + .Build(), + context->GetDiagnostics())); + + 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 |