diff options
author | 2016-02-13 20:26:45 -0800 | |
---|---|---|
committer | 2016-02-17 18:17:25 -0800 | |
commit | 355f285ffd000f6cfe76680eb22d010546d124bb (patch) | |
tree | 94d86559ba73ed2f482af1f296ef56374776a8f6 /tools | |
parent | e4735a99598bf29847a9f12dd7fae6d7df880bc4 (diff) |
AAPT2: Implement density stripping and initial Split support
When a preferred density is supplied, the closest matching densities
will be selected, the rest stripped from the APK.
Split support will be enabled in a later CL. Command line support is still
needed, but the foundation is ready.
Bug:25958912
Change-Id: I56d599806b4ec4ffa24e17aad48d47130ca05c08
Diffstat (limited to 'tools')
-rw-r--r-- | tools/aapt2/Android.mk | 3 | ||||
-rw-r--r-- | tools/aapt2/Debug.cpp | 35 | ||||
-rw-r--r-- | tools/aapt2/Debug.h | 6 | ||||
-rw-r--r-- | tools/aapt2/ResourceTable.cpp | 27 | ||||
-rw-r--r-- | tools/aapt2/ResourceTable.h | 31 | ||||
-rw-r--r-- | tools/aapt2/ResourceValues.cpp | 2 | ||||
-rw-r--r-- | tools/aapt2/ResourceValues.h | 6 | ||||
-rw-r--r-- | tools/aapt2/SdkConstants_test.cpp | 38 | ||||
-rw-r--r-- | tools/aapt2/compile/Compile.cpp | 16 | ||||
-rw-r--r-- | tools/aapt2/dump/Dump.cpp | 15 | ||||
-rw-r--r-- | tools/aapt2/link/Link.cpp | 689 | ||||
-rw-r--r-- | tools/aapt2/link/TableMerger.cpp | 21 | ||||
-rw-r--r-- | tools/aapt2/link/TableMerger.h | 30 | ||||
-rw-r--r-- | tools/aapt2/link/TableMerger_test.cpp | 59 | ||||
-rw-r--r-- | tools/aapt2/process/IResourceTableConsumer.h | 1 | ||||
-rw-r--r-- | tools/aapt2/split/TableSplitter.cpp | 264 | ||||
-rw-r--r-- | tools/aapt2/split/TableSplitter.h | 78 | ||||
-rw-r--r-- | tools/aapt2/split/TableSplitter_test.cpp | 107 | ||||
-rw-r--r-- | tools/aapt2/test/Context.h | 4 |
19 files changed, 1035 insertions, 397 deletions
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index cb82ac37a3b2..f9d35aba86d2 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -47,6 +47,7 @@ sources := \ proto/ProtoHelpers.cpp \ proto/TableProtoDeserializer.cpp \ proto/TableProtoSerializer.cpp \ + split/TableSplitter.cpp \ unflatten/BinaryResourceParser.cpp \ unflatten/ResChunkPullParser.cpp \ util/BigBuffer.cpp \ @@ -90,6 +91,7 @@ testSources := \ link/XmlReferenceLinker_test.cpp \ process/SymbolTable_test.cpp \ proto/TableProtoSerializer_test.cpp \ + split/TableSplitter_test.cpp \ util/BigBuffer_test.cpp \ util/Maybe_test.cpp \ util/StringPiece_test.cpp \ @@ -103,6 +105,7 @@ testSources := \ ResourceParser_test.cpp \ ResourceTable_test.cpp \ ResourceUtils_test.cpp \ + SdkConstants_test.cpp \ StringPool_test.cpp \ ValueVisitor_test.cpp \ xml/XmlDom_test.cpp \ diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 4bea12973692..19bd5210c840 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -30,7 +30,8 @@ namespace aapt { -struct PrintVisitor : public ValueVisitor { +class PrintVisitor : public ValueVisitor { +public: using ValueVisitor::visit; void visit(Attribute* attr) override { @@ -69,7 +70,11 @@ struct PrintVisitor : public ValueVisitor { for (const auto& entry : style->entries) { std::cout << "\n "; if (entry.key.name) { - std::cout << entry.key.name.value().package << ":" << entry.key.name.value().entry; + const ResourceName& name = entry.key.name.value(); + if (!name.package.empty()) { + std::cout << name.package << ":"; + } + std::cout << name.entry; } if (entry.key.id) { @@ -89,7 +94,21 @@ struct PrintVisitor : public ValueVisitor { } void visit(Styleable* styleable) override { - styleable->print(&std::cout); + std::cout << "(styleable)"; + for (const auto& attr : styleable->entries) { + std::cout << "\n "; + if (attr.name) { + const ResourceName& name = attr.name.value(); + if (!name.package.empty()) { + std::cout << name.package << ":"; + } + std::cout << name.entry; + } + + if (attr.id) { + std::cout << "(" << attr.id.value() << ")"; + } + } } void visitItem(Item* item) override { @@ -97,7 +116,9 @@ struct PrintVisitor : public ValueVisitor { } }; -void Debug::printTable(ResourceTable* table) { +void Debug::printTable(ResourceTable* table, const DebugPrintTableOptions& options) { + PrintVisitor visitor; + for (auto& package : table->packages) { std::cout << "Package name=" << package->name; if (package->id) { @@ -106,7 +127,7 @@ void Debug::printTable(ResourceTable* table) { std::cout << std::endl; for (const auto& type : package->types) { - std::cout << " type " << type->type; + std::cout << "\n type " << type->type; if (type->id) { std::cout << " id=" << std::hex << (int) type->id.value() << std::dec; } @@ -142,10 +163,12 @@ void Debug::printTable(ResourceTable* table) { std::cout << std::endl; - PrintVisitor visitor; for (const auto& value : entry->values) { std::cout << " (" << value->config << ") "; value->value->accept(&visitor); + if (options.showSources && !value->value->getSource().path.empty()) { + std::cout << " src=" << value->value->getSource(); + } std::cout << std::endl; } } diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h index ba05be91e91d..fbe64773d4ed 100644 --- a/tools/aapt2/Debug.h +++ b/tools/aapt2/Debug.h @@ -25,8 +25,12 @@ namespace aapt { +struct DebugPrintTableOptions { + bool showSources = false; +}; + struct Debug { - static void printTable(ResourceTable* table); + static void printTable(ResourceTable* table, const DebugPrintTableOptions& options = {}); static void printStyleGraph(ResourceTable* table, const ResourceName& targetStyle); static void dumpHex(const void* data, size_t len); diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 3e73be4dbc54..8d734f3fc36d 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -277,20 +277,31 @@ bool ResourceTable::addFileReference(const ResourceNameRef& name, const Source& source, const StringPiece16& path, IDiagnostics* diag) { - return addFileReference(name, config, source, path, resolveValueCollision, diag); + return addFileReferenceImpl(name, config, source, path, nullptr, kValidNameChars, diag); } -bool ResourceTable::addFileReference(const ResourceNameRef& name, - const ConfigDescription& config, - const Source& source, - const StringPiece16& path, - std::function<int(Value*,Value*)> conflictResolver, - IDiagnostics* diag) { +bool ResourceTable::addFileReferenceAllowMangled(const ResourceNameRef& name, + const ConfigDescription& config, + const Source& source, + const StringPiece16& path, + io::IFile* file, + IDiagnostics* diag) { + return addFileReferenceImpl(name, config, source, path, file, kValidNameMangledChars, diag); +} + +bool ResourceTable::addFileReferenceImpl(const ResourceNameRef& name, + const ConfigDescription& config, + const Source& source, + const StringPiece16& path, + io::IFile* file, + const char16_t* validChars, + IDiagnostics* diag) { std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>( stringPool.makeRef(path)); fileRef->setSource(source); + fileRef->file = file; return addResourceImpl(name, ResourceId{}, config, StringPiece{}, std::move(fileRef), - kValidNameChars, conflictResolver, diag); + kValidNameChars, resolveValueCollision, diag); } bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name, diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index 8ffff1f70710..7f5c2b8c0f37 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -23,6 +23,7 @@ #include "ResourceValues.h" #include "Source.h" #include "StringPool.h" +#include "io/File.h" #include <android-base/macros.h> #include <map> @@ -202,17 +203,17 @@ public: IDiagnostics* diag); bool addFileReference(const ResourceNameRef& name, - const ConfigDescription& config, - const Source& source, - const StringPiece16& path, - IDiagnostics* diag); - - bool addFileReference(const ResourceNameRef& name, - const ConfigDescription& config, - const Source& source, - const StringPiece16& path, - std::function<int(Value*,Value*)> conflictResolver, - IDiagnostics* diag); + const ConfigDescription& config, + const Source& source, + const StringPiece16& path, + IDiagnostics* diag); + + bool addFileReferenceAllowMangled(const ResourceNameRef& name, + const ConfigDescription& config, + const Source& source, + const StringPiece16& path, + io::IFile* file, + IDiagnostics* diag); /** * Same as addResource, but doesn't verify the validity of the name. This is used @@ -280,6 +281,14 @@ public: private: ResourceTablePackage* findOrCreatePackage(const StringPiece16& name); + bool addFileReferenceImpl(const ResourceNameRef& name, + const ConfigDescription& config, + const Source& source, + const StringPiece16& path, + io::IFile* file, + const char16_t* validChars, + IDiagnostics* diag); + bool addResourceImpl(const ResourceNameRef& name, ResourceId resId, const ConfigDescription& config, diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp index ab9c792876b3..dd7ff013e524 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -18,6 +18,7 @@ #include "ResourceUtils.h" #include "ResourceValues.h" #include "ValueVisitor.h" +#include "io/File.h" #include "util/Util.h" #include <androidfw/ResourceTypes.h> @@ -190,6 +191,7 @@ bool FileReference::flatten(android::Res_value* outValue) const { FileReference* FileReference::clone(StringPool* newPool) const { FileReference* fr = new FileReference(newPool->makeRef(*path)); + fr->file = file; fr->mComment = mComment; fr->mSource = mSource; return fr; diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h index dc2e28ee3abd..43354acf1d0b 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -20,6 +20,7 @@ #include "Diagnostics.h" #include "Resource.h" #include "StringPool.h" +#include "io/File.h" #include "util/Maybe.h" #include <array> @@ -226,6 +227,11 @@ private: struct FileReference : public BaseItem<FileReference> { StringPool::Ref path; + /** + * A handle to the file object from which this file can be read. + */ + io::IFile* file = nullptr; + FileReference() = default; FileReference(const StringPool::Ref& path); diff --git a/tools/aapt2/SdkConstants_test.cpp b/tools/aapt2/SdkConstants_test.cpp new file mode 100644 index 000000000000..e81f412dda15 --- /dev/null +++ b/tools/aapt2/SdkConstants_test.cpp @@ -0,0 +1,38 @@ +/* + * 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 "SdkConstants.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(SdkConstantsTest, FirstAttributeIsSdk1) { + EXPECT_EQ(1u, findAttributeSdkLevel(ResourceId(0x01010000))); +} + +TEST(SdkConstantsTest, AllAttributesAfterLollipopAreLollipopMR1) { + EXPECT_EQ(SDK_LOLLIPOP, findAttributeSdkLevel(ResourceId(0x010103f7))); + EXPECT_EQ(SDK_LOLLIPOP, findAttributeSdkLevel(ResourceId(0x010104ce))); + + EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x010104cf))); + EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x010104d8))); + + EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x010104d9))); + EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x0101ffff))); +} + +} // namespace aapt diff --git a/tools/aapt2/compile/Compile.cpp b/tools/aapt2/compile/Compile.cpp index 0dd8e1859661..5f9719e61a9a 100644 --- a/tools/aapt2/compile/Compile.cpp +++ b/tools/aapt2/compile/Compile.cpp @@ -424,8 +424,17 @@ static bool compileFile(IAaptContext* context, const CompileOptions& options, class CompileContext : public IAaptContext { private: StdErrDiagnostics mDiagnostics; + bool mVerbose = false; public: + void setVerbose(bool val) { + mVerbose = val; + } + + bool verbose() override { + return mVerbose; + } + IDiagnostics* getDiagnostics() override { return &mDiagnostics; } @@ -453,8 +462,10 @@ public: * Entry point for compilation phase. Parses arguments and dispatches to the correct steps. */ int compile(const std::vector<StringPiece>& args) { + CompileContext context; CompileOptions options; + bool verbose = false; Flags flags = Flags() .requiredFlag("-o", "Output path", &options.outputPath) .optionalFlag("--dir", "Directory to scan for resources", &options.resDir) @@ -462,12 +473,13 @@ int compile(const std::vector<StringPiece>& args) { "(en-XA and ar-XB)", &options.pseudolocalize) .optionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", &options.legacyMode) - .optionalSwitch("-v", "Enables verbose logging", &options.verbose); + .optionalSwitch("-v", "Enables verbose logging", &verbose); if (!flags.parse("aapt2 compile", args, &std::cerr)) { return 1; } - CompileContext context; + context.setVerbose(verbose); + std::unique_ptr<IArchiveWriter> archiveWriter; std::vector<ResourcePathData> inputData; diff --git a/tools/aapt2/dump/Dump.cpp b/tools/aapt2/dump/Dump.cpp index 915fae80fcbb..ad7de0ab2fbd 100644 --- a/tools/aapt2/dump/Dump.cpp +++ b/tools/aapt2/dump/Dump.cpp @@ -103,21 +103,32 @@ public: return nullptr; } + bool verbose() override { + return mVerbose; + } + + void setVerbose(bool val) { + mVerbose = val; + } + private: StdErrDiagnostics mDiagnostics; + bool mVerbose = false; }; /** * Entry point for dump command. */ int dump(const std::vector<StringPiece>& args) { - //DumpOptions options; - Flags flags = Flags(); + bool verbose = false; + Flags flags = Flags() + .optionalSwitch("-v", "increase verbosity of output", &verbose); if (!flags.parse("aapt2 dump", args, &std::cerr)) { return 1; } DumpContext context; + context.setVerbose(verbose); for (const std::string& arg : flags.getArgs()) { tryDumpFile(&context, arg); diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp index 3437ac0d9a7f..d83f6def890c 100644 --- a/tools/aapt2/link/Link.cpp +++ b/tools/aapt2/link/Link.cpp @@ -38,6 +38,7 @@ #include "process/IResourceTableConsumer.h" #include "process/SymbolTable.h" #include "proto/ProtoSerialize.h" +#include "split/TableSplitter.h" #include "unflatten/BinaryResourceParser.h" #include "util/Files.h" #include "util/StringPiece.h" @@ -63,15 +64,14 @@ struct LinkOptions { bool noAutoVersion = false; bool staticLib = false; bool generateNonFinalIds = false; - bool verbose = false; bool outputToDirectory = false; bool autoAddOverlay = false; bool doNotCompressAnything = false; std::vector<std::string> extensionsToNotCompress; Maybe<std::u16string> privateSymbols; ManifestFixerOptions manifestFixerOptions; - IConfigFilter* configFilter = nullptr; std::unordered_set<std::string> products; + TableSplitterOptions tableSplitterOptions; }; struct LinkContext : public IAaptContext { @@ -80,6 +80,7 @@ struct LinkContext : public IAaptContext { std::u16string mCompilationPackage; uint8_t mPackageId; std::unique_ptr<ISymbolTable> mSymbols; + bool mVerbose = false; IDiagnostics* getDiagnostics() override { return &mDiagnostics; @@ -100,163 +101,412 @@ struct LinkContext : public IAaptContext { ISymbolTable* getExternalSymbols() override { return mSymbols.get(); } + + bool verbose() override { + return mVerbose; + } }; -class LinkCommand { -public: - LinkCommand(LinkContext* context, const LinkOptions& options) : - mOptions(options), mContext(context), mFinalTable(), mFileCollection(nullptr) { - std::unique_ptr<io::FileCollection> fileCollection = - util::make_unique<io::FileCollection>(); +static bool copyFileToArchive(io::IFile* file, const std::string& outPath, + uint32_t compressionFlags, + IArchiveWriter* writer, IAaptContext* context) { + std::unique_ptr<io::IData> data = file->openAsData(); + if (!data) { + context->getDiagnostics()->error(DiagMessage(file->getSource()) + << "failed to open file"); + return false; + } - // Get a pointer to the FileCollection for convenience, but it will be owned by the vector. - mFileCollection = fileCollection.get(); + CompiledFileInputStream inputStream(data->data(), data->size()); + if (!inputStream.CompiledFile()) { + context->getDiagnostics()->error(DiagMessage(file->getSource()) + << "invalid compiled file header"); + return false; + } - // Move it to the collection. - mCollections.push_back(std::move(fileCollection)); + if (context->verbose()) { + context->getDiagnostics()->note(DiagMessage() << "writing " << outPath << " to archive"); } - /** - * Creates a SymbolTable that loads symbols from the various APKs and caches the - * results for faster lookup. - */ - std::unique_ptr<ISymbolTable> createSymbolTableFromIncludePaths() { - AssetManagerSymbolTableBuilder builder; - for (const std::string& path : mOptions.includePaths) { - if (mOptions.verbose) { - mContext->getDiagnostics()->note(DiagMessage(path) << "loading include path"); + if (writer->startEntry(outPath, compressionFlags)) { + if (writer->writeEntry(reinterpret_cast<const uint8_t*>(inputStream.data()), + inputStream.size())) { + if (writer->finishEntry()) { + return true; } - - std::unique_ptr<android::AssetManager> assetManager = - util::make_unique<android::AssetManager>(); - int32_t cookie = 0; - if (!assetManager->addAssetPath(android::String8(path.data(), path.size()), &cookie)) { - mContext->getDiagnostics()->error( - DiagMessage(path) << "failed to load include path"); - return {}; - } - builder.add(std::move(assetManager)); } - return builder.build(); } - std::unique_ptr<ResourceTable> loadTable(const Source& source, const void* data, size_t len) { - std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>(); - BinaryResourceParser parser(mContext, table.get(), source, data, len); - if (!parser.parse()) { - return {}; - } - return table; + context->getDiagnostics()->error(DiagMessage() << "failed to write file " << outPath); + return false; +} + +static bool flattenXml(xml::XmlResource* xmlRes, const StringPiece& path, Maybe<size_t> maxSdkLevel, + bool keepRawValues, IArchiveWriter* writer, IAaptContext* context) { + BigBuffer buffer(1024); + XmlFlattenerOptions options = {}; + options.keepRawValues = keepRawValues; + options.maxSdkLevel = maxSdkLevel; + XmlFlattener flattener(&buffer, options); + if (!flattener.consume(context, xmlRes)) { + return false; } - std::unique_ptr<ResourceTable> loadTableFromPb(const Source& source, - const void* data, size_t len) { - pb::ResourceTable pbTable; - if (!pbTable.ParseFromArray(data, len)) { - mContext->getDiagnostics()->error(DiagMessage(source) << "invalid compiled table"); - return {}; + if (context->verbose()) { + DiagMessage msg; + msg << "writing " << path << " to archive"; + if (maxSdkLevel) { + msg << " maxSdkLevel=" << maxSdkLevel.value(); } + context->getDiagnostics()->note(msg); + } - std::unique_ptr<ResourceTable> table = deserializeTableFromPb(pbTable, source, - mContext->getDiagnostics()); - if (!table) { - return {}; + if (writer->startEntry(path, ArchiveEntry::kCompress)) { + if (writer->writeEntry(buffer)) { + if (writer->finishEntry()) { + return true; + } } - return table; } + context->getDiagnostics()->error(DiagMessage() << "failed to write " << path << " to archive"); + return false; +} - /** - * Inflates an XML file from the source path. - */ - static std::unique_ptr<xml::XmlResource> loadXml(const std::string& path, IDiagnostics* diag) { - std::ifstream fin(path, std::ifstream::binary); - if (!fin) { - diag->error(DiagMessage(path) << strerror(errno)); - return {}; - } +/*static std::unique_ptr<ResourceTable> loadTable(const Source& source, const void* data, size_t len, + IDiagnostics* diag) { + std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>(); + BinaryResourceParser parser(diag, table.get(), source, data, len); + if (!parser.parse()) { + return {}; + } + return table; +}*/ + +static std::unique_ptr<ResourceTable> loadTableFromPb(const Source& source, + const void* data, size_t len, + IDiagnostics* diag) { + pb::ResourceTable pbTable; + if (!pbTable.ParseFromArray(data, len)) { + diag->error(DiagMessage(source) << "invalid compiled table"); + return {}; + } - return xml::inflate(&fin, diag, Source(path)); + std::unique_ptr<ResourceTable> table = deserializeTableFromPb(pbTable, source, diag); + if (!table) { + return {}; } + return table; +} - static std::unique_ptr<xml::XmlResource> loadBinaryXmlSkipFileExport( - const Source& source, - const void* data, size_t len, - IDiagnostics* diag) { - CompiledFileInputStream inputStream(data, len); - if (!inputStream.CompiledFile()) { - diag->error(DiagMessage(source) << "invalid compiled file header"); - return {}; - } +/** + * Inflates an XML file from the source path. + */ +static std::unique_ptr<xml::XmlResource> loadXml(const std::string& path, IDiagnostics* diag) { + std::ifstream fin(path, std::ifstream::binary); + if (!fin) { + diag->error(DiagMessage(path) << strerror(errno)); + return {}; + } + return xml::inflate(&fin, diag, Source(path)); +} - const uint8_t* xmlData = reinterpret_cast<const uint8_t*>(inputStream.data()); - const size_t xmlDataLen = inputStream.size(); +static std::unique_ptr<xml::XmlResource> loadBinaryXmlSkipFileExport(const Source& source, + const void* data, size_t len, + IDiagnostics* diag) { + CompiledFileInputStream inputStream(data, len); + if (!inputStream.CompiledFile()) { + diag->error(DiagMessage(source) << "invalid compiled file header"); + return {}; + } - std::unique_ptr<xml::XmlResource> xmlRes = xml::inflate(xmlData, xmlDataLen, diag, source); - if (!xmlRes) { - return {}; - } - return xmlRes; + const uint8_t* xmlData = reinterpret_cast<const uint8_t*>(inputStream.data()); + const size_t xmlDataLen = inputStream.size(); + + std::unique_ptr<xml::XmlResource> xmlRes = xml::inflate(xmlData, xmlDataLen, diag, source); + if (!xmlRes) { + return {}; } + return xmlRes; +} - static std::unique_ptr<ResourceFile> loadFileExportHeader(const Source& source, - const void* data, size_t len, - IDiagnostics* diag) { - CompiledFileInputStream inputStream(data, len); - const pb::CompiledFile* pbFile = inputStream.CompiledFile(); - if (!pbFile) { - diag->error(DiagMessage(source) << "invalid compiled file header"); - return {}; - } +static std::unique_ptr<ResourceFile> loadFileExportHeader(const Source& source, + const void* data, size_t len, + IDiagnostics* diag) { + CompiledFileInputStream inputStream(data, len); + const pb::CompiledFile* pbFile = inputStream.CompiledFile(); + if (!pbFile) { + diag->error(DiagMessage(source) << "invalid compiled file header"); + return {}; + } - std::unique_ptr<ResourceFile> resFile = deserializeCompiledFileFromPb(*pbFile, source, - diag); - if (!resFile) { - return {}; - } - return resFile; + std::unique_ptr<ResourceFile> resFile = deserializeCompiledFileFromPb(*pbFile, source, diag); + if (!resFile) { + return {}; + } + return resFile; +} + +struct ResourceFileFlattenerOptions { + bool noAutoVersion = false; + bool keepRawValues = false; + bool doNotCompressAnything = false; + std::vector<std::string> extensionsToNotCompress; +}; + +class ResourceFileFlattener { +public: + ResourceFileFlattener(const ResourceFileFlattenerOptions& options, + IAaptContext* context, proguard::KeepSet* keepSet) : + mOptions(options), mContext(context), mKeepSet(keepSet) { } - uint32_t getCompressionFlags(const StringPiece& str) { - if (mOptions.doNotCompressAnything) { + bool flatten(ResourceTable* table, IArchiveWriter* archiveWriter); + +private: + struct FileOperation { + io::IFile* fileToCopy; + std::unique_ptr<xml::XmlResource> xmlToFlatten; + std::string dstPath; + }; + + uint32_t getCompressionFlags(const StringPiece& str); + + std::unique_ptr<xml::XmlResource> linkAndVersionXmlFile(const ResourceEntry* entry, + const ResourceFile& fileDesc, + io::IFile* file, + ResourceTable* table); + + ResourceFileFlattenerOptions mOptions; + IAaptContext* mContext; + proguard::KeepSet* mKeepSet; +}; + +uint32_t ResourceFileFlattener::getCompressionFlags(const StringPiece& str) { + if (mOptions.doNotCompressAnything) { + return 0; + } + + for (const std::string& extension : mOptions.extensionsToNotCompress) { + if (util::stringEndsWith<char>(str, extension)) { return 0; } + } + return ArchiveEntry::kCompress; +} + +std::unique_ptr<xml::XmlResource> ResourceFileFlattener::linkAndVersionXmlFile( + const ResourceEntry* entry, + const ResourceFile& fileDesc, + io::IFile* file, + ResourceTable* table) { + const StringPiece srcPath = file->getSource().path; + if (mContext->verbose()) { + mContext->getDiagnostics()->note(DiagMessage() << "linking " << srcPath); + } + + std::unique_ptr<io::IData> data = file->openAsData(); + if (!data) { + mContext->getDiagnostics()->error(DiagMessage(file->getSource()) << "failed to open file"); + return {}; + } + + std::unique_ptr<xml::XmlResource> xmlRes; + if (util::stringEndsWith<char>(srcPath, ".flat")) { + xmlRes = loadBinaryXmlSkipFileExport(file->getSource(), data->data(), data->size(), + mContext->getDiagnostics()); + } else { + xmlRes = xml::inflate(data->data(), data->size(), mContext->getDiagnostics(), + file->getSource()); + } + + if (!xmlRes) { + return {}; + } + + // Copy the the file description header. + xmlRes->file = fileDesc; + + XmlReferenceLinker xmlLinker; + if (!xmlLinker.consume(mContext, xmlRes.get())) { + return {}; + } + + if (!proguard::collectProguardRules(xmlRes->file.source, xmlRes.get(), mKeepSet)) { + return {}; + } + + if (!mOptions.noAutoVersion) { + // Find the first SDK level used that is higher than this defined config and + // not superseded by a lower or equal SDK level resource. + for (int sdkLevel : xmlLinker.getSdkLevels()) { + if (sdkLevel > xmlRes->file.config.sdkVersion) { + if (!shouldGenerateVersionedResource(entry, xmlRes->file.config, sdkLevel)) { + // If we shouldn't generate a versioned resource, stop checking. + break; + } + + ResourceFile versionedFileDesc = xmlRes->file; + versionedFileDesc.config.sdkVersion = sdkLevel; - for (const std::string& extension : mOptions.extensionsToNotCompress) { - if (util::stringEndsWith<char>(str, extension)) { - return 0; + if (mContext->verbose()) { + mContext->getDiagnostics()->note(DiagMessage(versionedFileDesc.source) + << "auto-versioning resource from config '" + << xmlRes->file.config << "' -> '" + << versionedFileDesc.config << "'"); + } + + std::u16string genPath = util::utf8ToUtf16(ResourceUtils::buildResourceFileName( + versionedFileDesc, mContext->getNameMangler())); + + bool added = table->addFileReferenceAllowMangled(versionedFileDesc.name, + versionedFileDesc.config, + versionedFileDesc.source, + genPath, + file, + mContext->getDiagnostics()); + if (!added) { + return {}; + } + break; } } - return ArchiveEntry::kCompress; } + return xmlRes; +} - bool copyFileToArchive(io::IFile* file, const std::string& outPath, - IArchiveWriter* writer) { - std::unique_ptr<io::IData> data = file->openAsData(); - if (!data) { - mContext->getDiagnostics()->error(DiagMessage(file->getSource()) - << "failed to open file"); - return false; - } +/** + * Do not insert or remove any resources while executing in this function. It will + * corrupt the iteration order. + */ +bool ResourceFileFlattener::flatten(ResourceTable* table, IArchiveWriter* archiveWriter) { + bool error = false; + std::map<std::pair<ConfigDescription, StringPiece16>, FileOperation> configSortedFiles; + + for (auto& pkg : table->packages) { + for (auto& type : pkg->types) { + // Sort by config and name, so that we get better locality in the zip file. + configSortedFiles.clear(); + for (auto& entry : type->entries) { + // Iterate via indices because auto generated values can be inserted ahead of + // the value being processed. + for (size_t i = 0; i < entry->values.size(); i++) { + ResourceConfigValue* configValue = entry->values[i].get(); + + FileReference* fileRef = valueCast<FileReference>(configValue->value.get()); + if (!fileRef) { + continue; + } - CompiledFileInputStream inputStream(data->data(), data->size()); - if (!inputStream.CompiledFile()) { - mContext->getDiagnostics()->error(DiagMessage(file->getSource()) - << "invalid compiled file header"); - return false; - } + io::IFile* file = fileRef->file; + if (!file) { + mContext->getDiagnostics()->error(DiagMessage(fileRef->getSource()) + << "file not found"); + return false; + } - if (writer->startEntry(outPath, getCompressionFlags(outPath))) { - if (writer->writeEntry(reinterpret_cast<const uint8_t*>(inputStream.data()), - inputStream.size())) { - if (writer->finishEntry()) { - return true; + FileOperation fileOp; + fileOp.dstPath = util::utf16ToUtf8(*fileRef->path); + + const StringPiece srcPath = file->getSource().path; + if (type->type != ResourceType::kRaw && + (util::stringEndsWith<char>(srcPath, ".xml.flat") || + util::stringEndsWith<char>(srcPath, ".xml"))) { + ResourceFile fileDesc; + fileDesc.config = configValue->config; + fileDesc.name = ResourceName(pkg->name, type->type, entry->name); + fileDesc.source = fileRef->getSource(); + fileOp.xmlToFlatten = linkAndVersionXmlFile(entry.get(), fileDesc, + file, table); + if (!fileOp.xmlToFlatten) { + error = true; + continue; + } + + } else { + fileOp.fileToCopy = file; + } + + // NOTE(adamlesinski): Explicitly construct a StringPiece16 here, or else + // we end up copying the string in the std::make_pair() method, then creating + // a StringPiece16 from the copy, which would cause us to end up referencing + // garbage in the map. + const StringPiece16 entryName(entry->name); + configSortedFiles[std::make_pair(configValue->config, entryName)] = + std::move(fileOp); + } + } + + if (error) { + return false; + } + + // Now flatten the sorted values. + for (auto& mapEntry : configSortedFiles) { + const ConfigDescription& config = mapEntry.first.first; + const FileOperation& fileOp = mapEntry.second; + + if (fileOp.xmlToFlatten) { + Maybe<size_t> maxSdkLevel; + if (!mOptions.noAutoVersion) { + maxSdkLevel = std::max<size_t>(config.sdkVersion, 1u); + } + + bool result = flattenXml(fileOp.xmlToFlatten.get(), fileOp.dstPath, maxSdkLevel, + mOptions.keepRawValues, + archiveWriter, mContext); + if (!result) { + error = true; + } + } else { + bool result = copyFileToArchive(fileOp.fileToCopy, fileOp.dstPath, + getCompressionFlags(fileOp.dstPath), + archiveWriter, mContext); + if (!result) { + error = true; + } } } } + } + return !error; +} - mContext->getDiagnostics()->error( - DiagMessage(mOptions.outputPath) << "failed to write file " << outPath); - return false; +class LinkCommand { +public: + LinkCommand(LinkContext* context, const LinkOptions& options) : + mOptions(options), mContext(context), mFinalTable(), mFileCollection(nullptr) { + std::unique_ptr<io::FileCollection> fileCollection = + util::make_unique<io::FileCollection>(); + + // Get a pointer to the FileCollection for convenience, but it will be owned by the vector. + mFileCollection = fileCollection.get(); + + // Move it to the collection. + mCollections.push_back(std::move(fileCollection)); + } + + /** + * Creates a SymbolTable that loads symbols from the various APKs and caches the + * results for faster lookup. + */ + std::unique_ptr<ISymbolTable> createSymbolTableFromIncludePaths() { + AssetManagerSymbolTableBuilder builder; + for (const std::string& path : mOptions.includePaths) { + if (mContext->verbose()) { + mContext->getDiagnostics()->note(DiagMessage(path) << "loading include path"); + } + + std::unique_ptr<android::AssetManager> assetManager = + util::make_unique<android::AssetManager>(); + int32_t cookie = 0; + if (!assetManager->addAssetPath(android::String8(path.data(), path.size()), &cookie)) { + mContext->getDiagnostics()->error( + DiagMessage(path) << "failed to load include path"); + return {}; + } + builder.add(std::move(assetManager)); + } + return builder.build(); } Maybe<AppInfo> extractAppInfoFromManifest(xml::XmlResource* xmlRes) { @@ -349,28 +599,6 @@ public: return false; } - bool flattenXml(xml::XmlResource* xmlRes, const StringPiece& path, Maybe<size_t> maxSdkLevel, - IArchiveWriter* writer) { - BigBuffer buffer(1024); - XmlFlattenerOptions options = {}; - options.keepRawValues = mOptions.staticLib; - options.maxSdkLevel = maxSdkLevel; - XmlFlattener flattener(&buffer, options); - if (!flattener.consume(mContext, xmlRes)) { - return false; - } - - if (writer->startEntry(path, ArchiveEntry::kCompress)) { - if (writer->writeEntry(buffer)) { - if (writer->finishEntry()) { - return true; - } - } - } - mContext->getDiagnostics()->error( - DiagMessage() << "failed to write " << path << " to archive"); - return false; - } bool writeJavaFile(ResourceTable* table, const StringPiece16& packageNameToGenerate, const StringPiece16& outPackage, JavaClassGeneratorOptions javaOptions) { @@ -456,7 +684,7 @@ public: } bool mergeResourceTable(io::IFile* file, bool override) { - if (mOptions.verbose) { + if (mContext->verbose()) { mContext->getDiagnostics()->note(DiagMessage() << "linking " << file->getSource()); } @@ -467,8 +695,9 @@ public: return false; } - std::unique_ptr<ResourceTable> table = loadTableFromPb(file->getSource(), data->data(), - data->size()); + std::unique_ptr<ResourceTable> table = loadTableFromPb(file->getSource(), + data->data(), data->size(), + mContext->getDiagnostics()); if (!table) { return false; } @@ -483,7 +712,7 @@ public: } bool mergeCompiledFile(io::IFile* file, std::unique_ptr<ResourceFile> fileDesc, bool overlay) { - if (mOptions.verbose) { + if (mContext->verbose()) { mContext->getDiagnostics()->note(DiagMessage() << "adding " << file->getSource()); } @@ -628,10 +857,9 @@ public: TableMergerOptions tableMergerOptions; tableMergerOptions.autoAddOverlay = mOptions.autoAddOverlay; - tableMergerOptions.filter = mOptions.configFilter; mTableMerger = util::make_unique<TableMerger>(mContext, &mFinalTable, tableMergerOptions); - if (mOptions.verbose) { + if (mContext->verbose()) { mContext->getDiagnostics()->note( DiagMessage() << "linking package '" << mContext->mCompilationPackage << "' " << "with package ID " << std::hex << (int) mContext->mPackageId); @@ -692,6 +920,15 @@ public: mContext->getDiagnostics()->error(DiagMessage() << "failed stripping products"); return 1; } + + // TODO(adamlesinski): Actually pass in split constraints and handle splits at the file + // level. + TableSplitter tableSplitter({}, mOptions.tableSplitterOptions); + if (!tableSplitter.verifySplitConstraints(mContext)) { + return 1; + } + + tableSplitter.splitTable(&mFinalTable); } proguard::KeepSet proguardKeepSet; @@ -728,8 +965,10 @@ public: } } - if (!flattenXml(manifestXml.get(), "AndroidManifest.xml", {}, - archiveWriter.get())) { + const bool keepRawValues = mOptions.staticLib; + bool result = flattenXml(manifestXml.get(), "AndroidManifest.xml", {}, + keepRawValues, archiveWriter.get(), mContext); + if (!result) { error = true; } } else { @@ -742,113 +981,14 @@ public: return 1; } - for (auto& mergeEntry : mTableMerger->getFilesToMerge()) { - const ResourceKeyRef& key = mergeEntry.first; - const FileToMerge& fileToMerge = mergeEntry.second; - - const StringPiece path = fileToMerge.file->getSource().path; - - if (key.name.type != ResourceType::kRaw && - (util::stringEndsWith<char>(path, ".xml.flat") || - util::stringEndsWith<char>(path, ".xml"))) { - if (mOptions.verbose) { - mContext->getDiagnostics()->note(DiagMessage() << "linking " << path); - } - - io::IFile* file = fileToMerge.file; - std::unique_ptr<io::IData> data = file->openAsData(); - if (!data) { - mContext->getDiagnostics()->error(DiagMessage(file->getSource()) - << "failed to open file"); - return 1; - } - - std::unique_ptr<xml::XmlResource> xmlRes; - if (util::stringEndsWith<char>(path, ".flat")) { - xmlRes = loadBinaryXmlSkipFileExport(file->getSource(), - data->data(), data->size(), - mContext->getDiagnostics()); - } else { - xmlRes = xml::inflate(data->data(), data->size(), mContext->getDiagnostics(), - file->getSource()); - } - - if (!xmlRes) { - return 1; - } - - // Create the file description header. - xmlRes->file = ResourceFile{ - key.name.toResourceName(), - key.config, - fileToMerge.originalSource, - }; - - XmlReferenceLinker xmlLinker; - if (xmlLinker.consume(mContext, xmlRes.get())) { - if (!proguard::collectProguardRules(xmlRes->file.source, xmlRes.get(), - &proguardKeepSet)) { - error = true; - } - - Maybe<size_t> maxSdkLevel; - if (!mOptions.noAutoVersion) { - maxSdkLevel = std::max<size_t>(xmlRes->file.config.sdkVersion, 1u); - } - - if (!flattenXml(xmlRes.get(), fileToMerge.dstPath, maxSdkLevel, - archiveWriter.get())) { - error = true; - } - - if (!mOptions.noAutoVersion) { - Maybe<ResourceTable::SearchResult> result = mFinalTable.findResource( - xmlRes->file.name); - for (int sdkLevel : xmlLinker.getSdkLevels()) { - if (sdkLevel > xmlRes->file.config.sdkVersion && - shouldGenerateVersionedResource(result.value().entry, - xmlRes->file.config, - sdkLevel)) { - xmlRes->file.config.sdkVersion = sdkLevel; - - std::string genResourcePath = ResourceUtils::buildResourceFileName( - xmlRes->file, mContext->getNameMangler()); - - bool added = mFinalTable.addFileReference( - xmlRes->file.name, - xmlRes->file.config, - xmlRes->file.source, - util::utf8ToUtf16(genResourcePath), - mContext->getDiagnostics()); - if (!added) { - error = true; - continue; - } - - if (!flattenXml(xmlRes.get(), genResourcePath, sdkLevel, - archiveWriter.get())) { - error = true; - } - } - } - } - - } else { - error = true; - } - } else { - if (mOptions.verbose) { - mContext->getDiagnostics()->note(DiagMessage() << "copying " << path); - } + ResourceFileFlattenerOptions fileFlattenerOptions; + fileFlattenerOptions.keepRawValues = mOptions.staticLib; + fileFlattenerOptions.doNotCompressAnything = mOptions.doNotCompressAnything; + fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress; + fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion; + ResourceFileFlattener fileFlattener(fileFlattenerOptions, mContext, &proguardKeepSet); - if (!copyFileToArchive(fileToMerge.file, fileToMerge.dstPath, - archiveWriter.get())) { - error = true; - } - } - } - - if (error) { + if (!fileFlattener.flatten(&mFinalTable, archiveWriter.get())) { mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources"); return 1; } @@ -912,8 +1052,10 @@ public: } } - if (mOptions.verbose) { - Debug::printTable(&mFinalTable); + if (mContext->verbose()) { + DebugPrintTableOptions debugPrintTableOptions; + debugPrintTableOptions.showSources = true; + Debug::printTable(&mFinalTable, debugPrintTableOptions); } return 0; } @@ -934,6 +1076,7 @@ private: }; int link(const std::vector<StringPiece>& args) { + LinkContext context; LinkOptions options; Maybe<std::string> privateSymbolsPackage; Maybe<std::string> minSdkVersion, targetSdkVersion; @@ -942,6 +1085,7 @@ int link(const std::vector<StringPiece>& args) { Maybe<std::string> customJavaPackage; std::vector<std::string> extraJavaPackages; Maybe<std::string> configs; + Maybe<std::string> preferredDensity; Maybe<std::string> productList; bool legacyXFlag = false; bool requireLocalization = false; @@ -966,6 +1110,9 @@ int link(const std::vector<StringPiece>& args) { &requireLocalization) .optionalFlag("-c", "Comma separated list of configurations to include. The default\n" "is all configurations", &configs) + .optionalFlag("--preferred-density", + "Selects the closest matching density and strips out all others.", + &preferredDensity) .optionalFlag("--product", "Comma separated list of product names to keep", &productList) .optionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified " @@ -1001,14 +1148,12 @@ int link(const std::vector<StringPiece>& args) { &renameInstrumentationTargetPackage) .optionalFlagList("-0", "File extensions not to compress", &options.extensionsToNotCompress) - .optionalSwitch("-v", "Enables verbose logging", &options.verbose); + .optionalSwitch("-v", "Enables verbose logging", &context.mVerbose); if (!flags.parse("aapt2 link", args, &std::cerr)) { return 1; } - LinkContext context; - if (privateSymbolsPackage) { options.privateSymbols = util::utf8ToUtf16(privateSymbolsPackage.value()); } @@ -1082,7 +1227,29 @@ int link(const std::vector<StringPiece>& args) { } } - options.configFilter = &filter; + options.tableSplitterOptions.configFilter = &filter; + } + + if (preferredDensity) { + ConfigDescription preferredDensityConfig; + if (!ConfigDescription::parse(preferredDensity.value(), &preferredDensityConfig)) { + context.getDiagnostics()->error(DiagMessage() << "invalid density '" + << preferredDensity.value() + << "' for --preferred-density option"); + return 1; + } + + // Clear the version that can be automatically added. + preferredDensityConfig.sdkVersion = 0; + + if (preferredDensityConfig.diff(ConfigDescription::defaultConfig()) + != ConfigDescription::CONFIG_DENSITY) { + context.getDiagnostics()->error(DiagMessage() << "invalid preferred density '" + << preferredDensity.value() << "'. " + << "Preferred density must only be a density value"); + return 1; + } + options.tableSplitterOptions.preferredDensity = preferredDensityConfig.density; } LinkCommand cmd(&context, options); diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 2ecd5b018691..5f1174588438 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -98,8 +98,7 @@ bool TableMerger::mergeAndMangle(const Source& src, const StringPiece16& package return false; } - mFilesToMerge[ResourceKeyRef(name, config)] = FileToMerge{ - f, oldFile->getSource(), util::utf16ToUtf8(*newFile->path) }; + newFile->file = f; return true; }; @@ -198,10 +197,6 @@ bool TableMerger::doMerge(const Source& src, for (auto& srcValue : srcEntry->values) { ResourceConfigValue* dstValue = dstEntry->findValue(srcValue->config, srcValue->product); - - const bool stripConfig = mOptions.filter ? - !mOptions.filter->match(srcValue->config) : false; - if (dstValue) { const int collisionResult = ResourceTable::resolveValueCollision( dstValue->value.get(), srcValue->value.get()); @@ -227,10 +222,6 @@ bool TableMerger::doMerge(const Source& src, } - if (stripConfig) { - continue; - } - if (!dstValue) { // Force create the entry if we didn't have it. dstValue = dstEntry->findOrCreateValue(srcValue->config, srcValue->product); @@ -286,6 +277,7 @@ bool TableMerger::mergeFileImpl(const ResourceFile& fileDesc, io::IFile* file, b std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>( table.stringPool.makeRef(path)); fileRef->setSource(fileDesc.source); + fileRef->file = file; ResourceTablePackage* pkg = table.createPackage(fileDesc.name.package, 0x0); pkg->findOrCreateType(fileDesc.name.type) @@ -293,15 +285,8 @@ bool TableMerger::mergeFileImpl(const ResourceFile& fileDesc, io::IFile* file, b ->findOrCreateValue(fileDesc.config, {}) ->value = std::move(fileRef); - auto callback = [&](const ResourceNameRef& name, const ConfigDescription& config, - FileReference* newFile, FileReference* oldFile) -> bool { - mFilesToMerge[ResourceKeyRef(name, config)] = FileToMerge{ - file, oldFile->getSource(), util::utf16ToUtf8(*newFile->path) }; - return true; - }; - return doMerge(file->getSource(), &table, pkg, - false /* mangle */, overlay /* overlay */, true /* allow new */, callback); + false /* mangle */, overlay /* overlay */, true /* allow new */, {}); } bool TableMerger::mergeFile(const ResourceFile& fileDesc, io::IFile* file) { diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h index 4539679fa769..b3c22dd736f4 100644 --- a/tools/aapt2/link/TableMerger.h +++ b/tools/aapt2/link/TableMerger.h @@ -30,33 +30,11 @@ namespace aapt { -struct FileToMerge { - /** - * The compiled file from which to read the data. - */ - io::IFile* file; - - /** - * Where the original, uncompiled file came from. - */ - Source originalSource; - - /** - * The destination path within the APK/archive. - */ - std::string dstPath; -}; - struct TableMergerOptions { /** * If true, resources in overlays can be added without previously having existed. */ bool autoAddOverlay = false; - - /** - * A filter that removes resources whose configurations don't match. - */ - IConfigFilter* filter = nullptr; }; /** @@ -81,10 +59,6 @@ public: */ TableMerger(IAaptContext* context, ResourceTable* outTable, const TableMergerOptions& options); - const std::map<ResourceKeyRef, FileToMerge>& getFilesToMerge() { - return mFilesToMerge; - } - const std::set<std::u16string>& getMergedPackages() const { return mMergedPackages; } @@ -119,8 +93,7 @@ public: private: using FileMergeCallback = std::function<bool(const ResourceNameRef&, const ConfigDescription& config, - FileReference*, - FileReference*)>; + FileReference*, FileReference*)>; IAaptContext* mContext; ResourceTable* mMasterTable; @@ -128,7 +101,6 @@ private: ResourceTablePackage* mMasterPackage; std::set<std::u16string> mMergedPackages; - std::map<ResourceKeyRef, FileToMerge> mFilesToMerge; bool mergeFileImpl(const ResourceFile& fileDesc, io::IFile* file, bool overlay); diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp index 45c8c98780b8..4a80d3f48777 100644 --- a/tools/aapt2/link/TableMerger_test.cpp +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -97,16 +97,6 @@ TEST_F(TableMergerTest, MergeFile) { test::parseConfigOrDie("hdpi-v4")); ASSERT_NE(nullptr, file); EXPECT_EQ(std::u16string(u"res/layout-hdpi-v4/main.xml"), *file->path); - - ResourceName name = test::parseNameOrDie(u"@com.app.a:layout/main"); - ResourceKeyRef key = { name, test::parseConfigOrDie("hdpi-v4") }; - - auto iter = merger.getFilesToMerge().find(key); - ASSERT_NE(merger.getFilesToMerge().end(), iter); - - const FileToMerge& actualFileToMerge = iter->second; - EXPECT_EQ(&testFile, actualFileToMerge.file); - EXPECT_EQ(std::string("res/layout-hdpi-v4/main.xml"), actualFileToMerge.dstPath); } TEST_F(TableMergerTest, MergeFileOverlay) { @@ -122,14 +112,6 @@ TEST_F(TableMergerTest, MergeFileOverlay) { ASSERT_TRUE(merger.mergeFile(fileDesc, &fileA)); ASSERT_TRUE(merger.mergeFileOverlay(fileDesc, &fileB)); - - ResourceName name = test::parseNameOrDie(u"@com.app.a:xml/foo"); - ResourceKeyRef key = { name, ConfigDescription{} }; - auto iter = merger.getFilesToMerge().find(key); - ASSERT_NE(merger.getFilesToMerge().end(), iter); - - const FileToMerge& actualFileToMerge = iter->second; - EXPECT_EQ(&fileB, actualFileToMerge.file); } TEST_F(TableMergerTest, MergeFileReferences) { @@ -157,15 +139,6 @@ TEST_F(TableMergerTest, MergeFileReferences) { f = test::getValue<FileReference>(&finalTable, u"@com.app.a:xml/com.app.b$file"); ASSERT_NE(f, nullptr); EXPECT_EQ(std::u16string(u"res/xml/com.app.b$file.xml"), *f->path); - - ResourceName name = test::parseNameOrDie(u"@com.app.a:xml/com.app.b$file"); - ResourceKeyRef key = { name, ConfigDescription{} }; - auto iter = merger.getFilesToMerge().find(key); - ASSERT_NE(merger.getFilesToMerge().end(), iter); - - const FileToMerge& actualFileToMerge = iter->second; - EXPECT_EQ(Source("res/xml/file.xml"), actualFileToMerge.file->getSource()); - EXPECT_EQ(std::string("res/xml/com.app.b$file.xml"), actualFileToMerge.dstPath); } TEST_F(TableMergerTest, OverrideResourceWithOverlay) { @@ -244,36 +217,4 @@ TEST_F(TableMergerTest, FailToMergeNewResourceWithoutAutoAddOverlay) { ASSERT_FALSE(merger.mergeOverlay({}, tableB.get())); } -TEST_F(TableMergerTest, MergeAndStripResourcesNotMatchingFilter) { - ResourceTable finalTable; - TableMergerOptions options; - options.autoAddOverlay = false; - - AxisConfigFilter filter; - filter.addConfig(test::parseConfigOrDie("en")); - options.filter = &filter; - - test::TestFile fileA("res/layout-en/main.xml"), fileB("res/layout-fr-rFR/main.xml"); - const ResourceName name = test::parseNameOrDie(u"@com.app.a:layout/main"); - const ConfigDescription configEn = test::parseConfigOrDie("en"); - const ConfigDescription configFr = test::parseConfigOrDie("fr-rFR"); - - TableMerger merger(mContext.get(), &finalTable, options); - ASSERT_TRUE(merger.mergeFile(ResourceFile{ name, configEn }, &fileA)); - ASSERT_TRUE(merger.mergeFile(ResourceFile{ name, configFr }, &fileB)); - - EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(&finalTable, - u"@com.app.a:layout/main", - configEn)); - EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(&finalTable, - u"@com.app.a:layout/main", - configFr)); - - EXPECT_NE(merger.getFilesToMerge().end(), - merger.getFilesToMerge().find(ResourceKeyRef(name, configEn))); - - EXPECT_EQ(merger.getFilesToMerge().end(), - merger.getFilesToMerge().find(ResourceKeyRef(name, configFr))); -} - } // namespace aapt diff --git a/tools/aapt2/process/IResourceTableConsumer.h b/tools/aapt2/process/IResourceTableConsumer.h index a2528d2ac195..3a880449b759 100644 --- a/tools/aapt2/process/IResourceTableConsumer.h +++ b/tools/aapt2/process/IResourceTableConsumer.h @@ -40,6 +40,7 @@ struct IAaptContext { virtual StringPiece16 getCompilationPackage() = 0; virtual uint8_t getPackageId() = 0; virtual NameMangler* getNameMangler() = 0; + virtual bool verbose() = 0; }; struct IResourceTableConsumer { diff --git a/tools/aapt2/split/TableSplitter.cpp b/tools/aapt2/split/TableSplitter.cpp new file mode 100644 index 000000000000..0f7649bac157 --- /dev/null +++ b/tools/aapt2/split/TableSplitter.cpp @@ -0,0 +1,264 @@ +/* + * 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 "ConfigDescription.h" +#include "ResourceTable.h" +#include "split/TableSplitter.h" + +#include <map> +#include <set> +#include <unordered_map> +#include <vector> + +namespace aapt { + +using ConfigClaimedMap = std::unordered_map<ResourceConfigValue*, bool>; +using ConfigDensityGroups = std::map<ConfigDescription, std::vector<ResourceConfigValue*>>; + +static ConfigDescription copyWithoutDensity(const ConfigDescription& config) { + ConfigDescription withoutDensity = config; + withoutDensity.density = 0; + return withoutDensity; +} + +/** + * Selects values that match exactly the constraints given. + */ +class SplitValueSelector { +public: + SplitValueSelector(const SplitConstraints& constraints) { + for (const ConfigDescription& config : constraints.configs) { + if (config.density == 0) { + mDensityIndependentConfigs.insert(config); + } else { + mDensityDependentConfigToDensityMap[copyWithoutDensity(config)] = config.density; + } + } + } + + std::vector<ResourceConfigValue*> selectValues(const ConfigDensityGroups& densityGroups, + ConfigClaimedMap* claimedValues) { + std::vector<ResourceConfigValue*> selected; + + // Select the regular values. + for (auto& entry : *claimedValues) { + // Check if the entry has a density. + ResourceConfigValue* configValue = entry.first; + if (configValue->config.density == 0 && !entry.second) { + // This is still available. + if (mDensityIndependentConfigs.find(configValue->config) != + mDensityIndependentConfigs.end()) { + selected.push_back(configValue); + + // Mark the entry as taken. + entry.second = true; + } + } + } + + // Now examine the densities + for (auto& entry : densityGroups) { + // We do not care if the value is claimed, since density values can be + // in multiple splits. + const ConfigDescription& config = entry.first; + const std::vector<ResourceConfigValue*>& relatedValues = entry.second; + + auto densityValueIter = mDensityDependentConfigToDensityMap.find(config); + if (densityValueIter != mDensityDependentConfigToDensityMap.end()) { + // Select the best one! + ConfigDescription targetDensity = config; + targetDensity.density = densityValueIter->second; + + ResourceConfigValue* bestValue = nullptr; + for (ResourceConfigValue* thisValue : relatedValues) { + if (!bestValue || + thisValue->config.isBetterThan(bestValue->config, &targetDensity)) { + bestValue = thisValue; + } + + // When we select one of these, they are all claimed such that the base + // doesn't include any anymore. + (*claimedValues)[thisValue] = true; + } + assert(bestValue); + selected.push_back(bestValue); + } + } + return selected; + } + +private: + std::set<ConfigDescription> mDensityIndependentConfigs; + std::map<ConfigDescription, uint16_t> mDensityDependentConfigToDensityMap; +}; + +/** + * Marking non-preferred densities as claimed will make sure the base doesn't include them, + * leaving only the preferred density behind. + */ +static void markNonPreferredDensitiesAsClaimed(uint16_t preferredDensity, + const ConfigDensityGroups& densityGroups, + ConfigClaimedMap* configClaimedMap) { + for (auto& entry : densityGroups) { + const ConfigDescription& config = entry.first; + const std::vector<ResourceConfigValue*>& relatedValues = entry.second; + + ConfigDescription targetDensity = config; + targetDensity.density = preferredDensity; + ResourceConfigValue* bestValue = nullptr; + for (ResourceConfigValue* thisValue : relatedValues) { + if (!bestValue) { + bestValue = thisValue; + } else if (thisValue->config.isBetterThan(bestValue->config, &targetDensity)) { + // Claim the previous value so that it is not included in the base. + (*configClaimedMap)[bestValue] = true; + bestValue = thisValue; + } else { + // Claim this value so that it is not included in the base. + (*configClaimedMap)[thisValue] = true; + } + } + assert(bestValue); + } +} + +bool TableSplitter::verifySplitConstraints(IAaptContext* context) { + bool error = false; + for (size_t i = 0; i < mSplitConstraints.size(); i++) { + for (size_t j = i + 1; j < mSplitConstraints.size(); j++) { + for (const ConfigDescription& config : mSplitConstraints[i].configs) { + if (mSplitConstraints[j].configs.find(config) != + mSplitConstraints[j].configs.end()) { + context->getDiagnostics()->error(DiagMessage() << "config '" << config + << "' appears in multiple splits, " + << "target split ambiguous"); + error = true; + } + } + } + } + return !error; +} + +void TableSplitter::splitTable(ResourceTable* originalTable) { + const size_t splitCount = mSplitConstraints.size(); + for (auto& pkg : originalTable->packages) { + // Initialize all packages for splits. + for (size_t idx = 0; idx < splitCount; idx++) { + ResourceTable* splitTable = mSplits[idx].get(); + splitTable->createPackage(pkg->name, pkg->id); + } + + for (auto& type : pkg->types) { + if (type->type == ResourceType::kMipmap) { + // Always keep mipmaps. + continue; + } + + for (auto& entry : type->entries) { + if (mConfigFilter) { + // First eliminate any resource that we definitely don't want. + for (std::unique_ptr<ResourceConfigValue>& configValue : entry->values) { + if (!mConfigFilter->match(configValue->config)) { + // null out the entry. We will clean up and remove nulls at the end + // for performance reasons. + configValue.reset(); + } + } + } + + // Organize the values into two separate buckets. Those that are density-dependent + // and those that are density-independent. + // One density technically matches all density, it's just that some densities + // match better. So we need to be aware of the full set of densities to make this + // decision. + ConfigDensityGroups densityGroups; + ConfigClaimedMap configClaimedMap; + for (const std::unique_ptr<ResourceConfigValue>& configValue : entry->values) { + if (configValue) { + configClaimedMap[configValue.get()] = false; + + if (configValue->config.density != 0) { + // Create a bucket for this density-dependent config. + densityGroups[copyWithoutDensity(configValue->config)] + .push_back(configValue.get()); + } + } + } + + // First we check all the splits. If it doesn't match one of the splits, we + // leave it in the base. + for (size_t idx = 0; idx < splitCount; idx++) { + const SplitConstraints& splitConstraint = mSplitConstraints[idx]; + ResourceTable* splitTable = mSplits[idx].get(); + + // Select the values we want from this entry for this split. + SplitValueSelector selector(splitConstraint); + std::vector<ResourceConfigValue*> selectedValues = + selector.selectValues(densityGroups, &configClaimedMap); + + // No need to do any work if we selected nothing. + if (!selectedValues.empty()) { + // Create the same resource structure in the split. We do this lazily + // because we might not have actual values for each type/entry. + ResourceTablePackage* splitPkg = splitTable->findPackage(pkg->name); + ResourceTableType* splitType = splitPkg->findOrCreateType(type->type); + if (!splitType->id) { + splitType->id = type->id; + splitType->symbolStatus = type->symbolStatus; + } + + ResourceEntry* splitEntry = splitType->findOrCreateEntry(entry->name); + if (!splitEntry->id) { + splitEntry->id = entry->id; + splitEntry->symbolStatus = entry->symbolStatus; + } + + // Copy the selected values into the new Split Entry. + for (ResourceConfigValue* configValue : selectedValues) { + ResourceConfigValue* newConfigValue = splitEntry->findOrCreateValue( + configValue->config, configValue->product); + newConfigValue->value = std::unique_ptr<Value>( + configValue->value->clone(&splitTable->stringPool)); + } + } + } + + if (mPreferredDensity) { + markNonPreferredDensitiesAsClaimed(mPreferredDensity.value(), + densityGroups, + &configClaimedMap); + } + + // All splits are handled, now check to see what wasn't claimed and remove + // whatever exists in other splits. + for (std::unique_ptr<ResourceConfigValue>& configValue : entry->values) { + if (configValue && configClaimedMap[configValue.get()]) { + // Claimed, remove from base. + configValue.reset(); + } + } + + // Now erase all nullptrs. + entry->values.erase( + std::remove(entry->values.begin(), entry->values.end(), nullptr), + entry->values.end()); + } + } + } +} + +} // namespace aapt diff --git a/tools/aapt2/split/TableSplitter.h b/tools/aapt2/split/TableSplitter.h new file mode 100644 index 000000000000..15e0764c4259 --- /dev/null +++ b/tools/aapt2/split/TableSplitter.h @@ -0,0 +1,78 @@ +/* + * 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. + */ + +#ifndef AAPT_SPLIT_TABLESPLITTER_H +#define AAPT_SPLIT_TABLESPLITTER_H + +#include "ConfigDescription.h" +#include "ResourceTable.h" +#include "filter/ConfigFilter.h" +#include "process/IResourceTableConsumer.h" + +#include <android-base/macros.h> +#include <set> +#include <vector> + +namespace aapt { + +struct SplitConstraints { + std::set<ConfigDescription> configs; +}; + +struct TableSplitterOptions { + /** + * The preferred density to keep in the table, stripping out all others. + */ + Maybe<uint16_t> preferredDensity; + + /** + * Configuration filter that determines which resource configuration values end up in + * the final table. + */ + IConfigFilter* configFilter = nullptr; +}; + +class TableSplitter { +public: + TableSplitter(const std::vector<SplitConstraints>& splits, + const TableSplitterOptions& options) : + mSplitConstraints(splits), mPreferredDensity(options.preferredDensity), + mConfigFilter(options.configFilter) { + for (size_t i = 0; i < mSplitConstraints.size(); i++) { + mSplits.push_back(util::make_unique<ResourceTable>()); + } + } + + bool verifySplitConstraints(IAaptContext* context); + + void splitTable(ResourceTable* originalTable); + + const std::vector<std::unique_ptr<ResourceTable>>& getSplits() { + return mSplits; + } + +private: + std::vector<SplitConstraints> mSplitConstraints; + std::vector<std::unique_ptr<ResourceTable>> mSplits; + Maybe<uint16_t> mPreferredDensity; + IConfigFilter* mConfigFilter; + + DISALLOW_COPY_AND_ASSIGN(TableSplitter); +}; + +} + +#endif /* AAPT_SPLIT_TABLESPLITTER_H */ diff --git a/tools/aapt2/split/TableSplitter_test.cpp b/tools/aapt2/split/TableSplitter_test.cpp new file mode 100644 index 000000000000..74ca32e04a30 --- /dev/null +++ b/tools/aapt2/split/TableSplitter_test.cpp @@ -0,0 +1,107 @@ +/* + * 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 "split/TableSplitter.h" +#include "test/Builders.h" +#include "test/Common.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(TableSplitterTest, NoSplitPreferredDensity) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addFileReference(u"@android:drawable/icon", u"res/drawable-mdpi/icon.png", + test::parseConfigOrDie("mdpi")) + .addFileReference(u"@android:drawable/icon", u"res/drawable-hdpi/icon.png", + test::parseConfigOrDie("hdpi")) + .addFileReference(u"@android:drawable/icon", u"res/drawable-xhdpi/icon.png", + test::parseConfigOrDie("xhdpi")) + .addFileReference(u"@android:drawable/icon", u"res/drawable-xxhdpi/icon.png", + test::parseConfigOrDie("xxhdpi")) + .addSimple(u"@android:string/one", {}) + .build(); + + TableSplitterOptions options; + options.preferredDensity = ConfigDescription::DENSITY_XHIGH; + TableSplitter splitter({}, options); + splitter.splitTable(table.get()); + + EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(), + u"@android:drawable/icon", + test::parseConfigOrDie("mdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(), + u"@android:drawable/icon", + test::parseConfigOrDie("hdpi"))); + EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(table.get(), + u"@android:drawable/icon", + test::parseConfigOrDie("xhdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(), + u"@android:drawable/icon", + test::parseConfigOrDie("xxhdpi"))); + EXPECT_NE(nullptr, test::getValue<Id>(table.get(), u"@android:string/one")); +} + +TEST(TableSplitterTest, SplitTableByConfigAndDensity) { + ResourceTable table; + + const ResourceName foo = test::parseNameOrDie(u"@android:string/foo"); + ASSERT_TRUE(table.addResource(foo, test::parseConfigOrDie("land-hdpi"), {}, + util::make_unique<Id>(), + test::getDiagnostics())); + ASSERT_TRUE(table.addResource(foo, test::parseConfigOrDie("land-xhdpi"), {}, + util::make_unique<Id>(), + test::getDiagnostics())); + ASSERT_TRUE(table.addResource(foo, test::parseConfigOrDie("land-xxhdpi"), {}, + util::make_unique<Id>(), + test::getDiagnostics())); + + std::vector<SplitConstraints> constraints; + constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("land-mdpi") } }); + constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("land-xhdpi") } }); + + TableSplitter splitter(constraints, TableSplitterOptions{}); + splitter.splitTable(&table); + + ASSERT_EQ(2u, splitter.getSplits().size()); + + ResourceTable* splitOne = splitter.getSplits()[0].get(); + ResourceTable* splitTwo = splitter.getSplits()[1].get(); + + // Since a split was defined, all densities should be gone from base. + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, u"@android:string/foo", + test::parseConfigOrDie("land-hdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, u"@android:string/foo", + test::parseConfigOrDie("land-xhdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, u"@android:string/foo", + test::parseConfigOrDie("land-xxhdpi"))); + + EXPECT_NE(nullptr, test::getValueForConfig<Id>(splitOne, u"@android:string/foo", + test::parseConfigOrDie("land-hdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitOne, u"@android:string/foo", + test::parseConfigOrDie("land-xhdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitOne, u"@android:string/foo", + test::parseConfigOrDie("land-xxhdpi"))); + + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitTwo, u"@android:string/foo", + test::parseConfigOrDie("land-hdpi"))); + EXPECT_NE(nullptr, test::getValueForConfig<Id>(splitTwo, u"@android:string/foo", + test::parseConfigOrDie("land-xhdpi"))); + EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitTwo, u"@android:string/foo", + test::parseConfigOrDie("land-xxhdpi"))); +} + +} // namespace aapt diff --git a/tools/aapt2/test/Context.h b/tools/aapt2/test/Context.h index 555a53959737..e540cd7ace90 100644 --- a/tools/aapt2/test/Context.h +++ b/tools/aapt2/test/Context.h @@ -71,6 +71,10 @@ public: assert(mNameMangler && "test name mangler not set"); return mNameMangler.get(); } + + bool verbose() override { + return false; + } }; class ContextBuilder { |