AAPT2: Separate out the various steps
An early refactor. Some ideas became clearer as
development continued. Now the various phases are much
clearer and more easily reusable.
Also added a ton of tests!
Change-Id: Ic8f0a70c8222370352e63533b329c40457c0903e
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
new file mode 100644
index 0000000..2b4c4d2
--- /dev/null
+++ b/tools/aapt2/link/Link.cpp
@@ -0,0 +1,702 @@
+/*
+ * Copyright (C) 2015 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 "AppInfo.h"
+#include "Debug.h"
+#include "Flags.h"
+#include "JavaClassGenerator.h"
+#include "NameMangler.h"
+#include "ProguardRules.h"
+#include "XmlDom.h"
+
+#include "compile/IdAssigner.h"
+#include "flatten/Archive.h"
+#include "flatten/TableFlattener.h"
+#include "flatten/XmlFlattener.h"
+#include "link/Linkers.h"
+#include "link/TableMerger.h"
+#include "process/IResourceTableConsumer.h"
+#include "process/SymbolTable.h"
+#include "unflatten/BinaryResourceParser.h"
+#include "unflatten/FileExportHeaderReader.h"
+#include "util/Files.h"
+#include "util/StringPiece.h"
+
+#include <fstream>
+#include <sys/stat.h>
+#include <utils/FileMap.h>
+#include <vector>
+
+namespace aapt {
+
+struct LinkOptions {
+ std::string outputPath;
+ std::string manifestPath;
+ std::vector<std::string> includePaths;
+ Maybe<std::string> generateJavaClassPath;
+ Maybe<std::string> generateProguardRulesPath;
+ bool noAutoVersion = false;
+ bool staticLib = false;
+ bool verbose = false;
+ bool outputToDirectory = false;
+};
+
+struct LinkContext : public IAaptContext {
+ StdErrDiagnostics mDiagnostics;
+ std::unique_ptr<NameMangler> mNameMangler;
+ std::u16string mCompilationPackage;
+ uint8_t mPackageId;
+ std::unique_ptr<ISymbolTable> mSymbols;
+
+ IDiagnostics* getDiagnostics() override {
+ return &mDiagnostics;
+ }
+
+ NameMangler* getNameMangler() override {
+ return mNameMangler.get();
+ }
+
+ StringPiece16 getCompilationPackage() override {
+ return mCompilationPackage;
+ }
+
+ uint8_t getPackageId() override {
+ return mPackageId;
+ }
+
+ ISymbolTable* getExternalSymbols() override {
+ return mSymbols.get();
+ }
+};
+
+struct LinkCommand {
+ LinkOptions mOptions;
+ LinkContext mContext;
+
+ std::string buildResourceFileName(const ResourceFile& resFile) {
+ std::stringstream out;
+ out << "res/" << resFile.name.type;
+ if (resFile.config != ConfigDescription{}) {
+ out << "-" << resFile.config;
+ }
+ out << "/";
+
+ if (mContext.getNameMangler()->shouldMangle(resFile.name.package)) {
+ out << NameMangler::mangleEntry(resFile.name.package, resFile.name.entry);
+ } else {
+ out << resFile.name.entry;
+ }
+ out << file::getExtension(resFile.source.path);
+ return out.str();
+ }
+
+ /**
+ * 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(Source{ 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(Source{ path }) << "failed to load include path");
+ return {};
+ }
+ builder.add(std::move(assetManager));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Loads the resource table (not inside an apk) at the given path.
+ */
+ std::unique_ptr<ResourceTable> loadTable(const std::string& input) {
+ std::string errorStr;
+ Maybe<android::FileMap> map = file::mmapPath(input, &errorStr);
+ if (!map) {
+ mContext.getDiagnostics()->error(DiagMessage(Source{ input }) << errorStr);
+ return {};
+ }
+
+ std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
+ BinaryResourceParser parser(&mContext, table.get(), Source{ input },
+ map.value().getDataPtr(), map.value().getDataLength());
+ if (!parser.parse()) {
+ return {};
+ }
+ return table;
+ }
+
+ /**
+ * Inflates an XML file from the source path.
+ */
+ std::unique_ptr<XmlResource> loadXml(const std::string& path) {
+ std::ifstream fin(path, std::ifstream::binary);
+ if (!fin) {
+ mContext.getDiagnostics()->error(DiagMessage(Source{ path }) << strerror(errno));
+ return {};
+ }
+
+ return xml::inflate(&fin, mContext.getDiagnostics(), Source{ path });
+ }
+
+ /**
+ * Inflates a binary XML file from the source path.
+ */
+ std::unique_ptr<XmlResource> loadBinaryXmlSkipFileExport(const std::string& path) {
+ // Read header for symbol info and export info.
+ std::string errorStr;
+ Maybe<android::FileMap> maybeF = file::mmapPath(path, &errorStr);
+ if (!maybeF) {
+ mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
+ return {};
+ }
+
+ ssize_t offset = getWrappedDataOffset(maybeF.value().getDataPtr(),
+ maybeF.value().getDataLength(), &errorStr);
+ if (offset < 0) {
+ mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
+ return {};
+ }
+
+ std::unique_ptr<XmlResource> xmlRes = xml::inflate(
+ (const uint8_t*) maybeF.value().getDataPtr() + (size_t) offset,
+ maybeF.value().getDataLength() - offset,
+ mContext.getDiagnostics(), Source(path));
+ if (!xmlRes) {
+ return {};
+ }
+ return xmlRes;
+ }
+
+ Maybe<ResourceFile> loadFileExportHeader(const std::string& path) {
+ // Read header for symbol info and export info.
+ std::string errorStr;
+ Maybe<android::FileMap> maybeF = file::mmapPath(path, &errorStr);
+ if (!maybeF) {
+ mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
+ return {};
+ }
+
+ ResourceFile resFile;
+ ssize_t offset = unwrapFileExportHeader(maybeF.value().getDataPtr(),
+ maybeF.value().getDataLength(),
+ &resFile, &errorStr);
+ if (offset < 0) {
+ mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
+ return {};
+ }
+ return std::move(resFile);
+ }
+
+ bool copyFileToArchive(const std::string& path, const std::string& outPath, uint32_t flags,
+ IArchiveWriter* writer) {
+ std::string errorStr;
+ Maybe<android::FileMap> maybeF = file::mmapPath(path, &errorStr);
+ if (!maybeF) {
+ mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
+ return false;
+ }
+
+ ssize_t offset = getWrappedDataOffset(maybeF.value().getDataPtr(),
+ maybeF.value().getDataLength(),
+ &errorStr);
+ if (offset < 0) {
+ mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
+ return false;
+ }
+
+ ArchiveEntry* entry = writer->writeEntry(outPath, flags, &maybeF.value(),
+ offset, maybeF.value().getDataLength() - offset);
+ if (!entry) {
+ mContext.getDiagnostics()->error(
+ DiagMessage(mOptions.outputPath) << "failed to write file " << outPath);
+ return false;
+ }
+ return true;
+ }
+
+ Maybe<AppInfo> extractAppInfoFromManifest(XmlResource* xmlRes) {
+ xml::Node* node = xmlRes->root.get();
+
+ // Find the first xml::Element.
+ while (node && !xml::nodeCast<xml::Element>(node)) {
+ node = !node->children.empty() ? node->children.front().get() : nullptr;
+ }
+
+ // Make sure the first element is <manifest> with package attribute.
+ if (xml::Element* manifestEl = xml::nodeCast<xml::Element>(node)) {
+ if (manifestEl->namespaceUri.empty() && manifestEl->name == u"manifest") {
+ if (xml::Attribute* packageAttr = manifestEl->findAttribute({}, u"package")) {
+ return AppInfo{ packageAttr->value };
+ }
+ }
+ }
+ return {};
+ }
+
+ bool verifyNoExternalPackages(ResourceTable* table) {
+ bool error = false;
+ for (const auto& package : table->packages) {
+ if (mContext.getCompilationPackage() != package->name ||
+ !package->id || package->id.value() != mContext.getPackageId()) {
+ // We have a package that is not related to the one we're building!
+ for (const auto& type : package->types) {
+ for (const auto& entry : type->entries) {
+ for (const auto& configValue : entry->values) {
+ mContext.getDiagnostics()->error(DiagMessage(configValue.source)
+ << "defined resource '"
+ << ResourceNameRef(package->name,
+ type->type,
+ entry->name)
+ << "' for external package '"
+ << package->name << "'");
+ error = true;
+ }
+ }
+ }
+ }
+ }
+ return !error;
+ }
+
+ std::unique_ptr<IArchiveWriter> makeArchiveWriter() {
+ if (mOptions.outputToDirectory) {
+ return createDirectoryArchiveWriter(mOptions.outputPath);
+ } else {
+ return createZipFileArchiveWriter(mOptions.outputPath);
+ }
+ }
+
+ bool flattenTable(ResourceTable* table, IArchiveWriter* writer) {
+ BigBuffer buffer(1024);
+ TableFlattenerOptions options = {};
+ options.useExtendedChunks = mOptions.staticLib;
+ TableFlattener flattener(&buffer, options);
+ if (!flattener.consume(&mContext, table)) {
+ return false;
+ }
+
+ ArchiveEntry* entry = writer->writeEntry("resources.arsc", ArchiveEntry::kAlign, buffer);
+ if (!entry) {
+ mContext.getDiagnostics()->error(
+ DiagMessage() << "failed to write resources.arsc to archive");
+ return false;
+ }
+ return true;
+ }
+
+ bool flattenXml(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;
+ }
+
+ ArchiveEntry* entry = writer->writeEntry(path, ArchiveEntry::kCompress, buffer);
+ if (!entry) {
+ mContext.getDiagnostics()->error(
+ DiagMessage() << "failed to write " << path << " to archive");
+ return false;
+ }
+ return true;
+ }
+
+ bool writeJavaFile(ResourceTable* table, const StringPiece16& package) {
+ if (!mOptions.generateJavaClassPath) {
+ return true;
+ }
+
+ std::string outPath = mOptions.generateJavaClassPath.value();
+ file::appendPath(&outPath, file::packageToPath(util::utf16ToUtf8(package)));
+ file::mkdirs(outPath);
+ file::appendPath(&outPath, "R.java");
+
+ std::ofstream fout(outPath, std::ofstream::binary);
+ if (!fout) {
+ mContext.getDiagnostics()->error(DiagMessage() << strerror(errno));
+ return false;
+ }
+
+ JavaClassGeneratorOptions javaOptions;
+ if (mOptions.staticLib) {
+ javaOptions.useFinal = false;
+ }
+
+ JavaClassGenerator generator(table, javaOptions);
+ if (!generator.generate(mContext.getCompilationPackage(), &fout)) {
+ mContext.getDiagnostics()->error(DiagMessage(outPath) << generator.getError());
+ return false;
+ }
+ return true;
+ }
+
+ bool writeProguardFile(const proguard::KeepSet& keepSet) {
+ if (!mOptions.generateProguardRulesPath) {
+ return true;
+ }
+
+ std::ofstream fout(mOptions.generateProguardRulesPath.value(), std::ofstream::binary);
+ if (!fout) {
+ mContext.getDiagnostics()->error(DiagMessage() << strerror(errno));
+ return false;
+ }
+
+ proguard::writeKeepSet(&fout, keepSet);
+ if (!fout) {
+ mContext.getDiagnostics()->error(DiagMessage() << strerror(errno));
+ return false;
+ }
+ return true;
+ }
+
+ int run(const std::vector<std::string>& inputFiles) {
+ // Load the AndroidManifest.xml
+ std::unique_ptr<XmlResource> manifestXml = loadXml(mOptions.manifestPath);
+ if (!manifestXml) {
+ return 1;
+ }
+
+ if (Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get())) {
+ mContext.mCompilationPackage = maybeAppInfo.value().package;
+ } else {
+ mContext.getDiagnostics()->error(DiagMessage(mOptions.manifestPath)
+ << "no package specified in <manifest> tag");
+ return 1;
+ }
+
+ if (!util::isJavaPackageName(mContext.mCompilationPackage)) {
+ mContext.getDiagnostics()->error(DiagMessage(mOptions.manifestPath)
+ << "invalid package name '"
+ << mContext.mCompilationPackage
+ << "'");
+ return 1;
+ }
+
+ mContext.mNameMangler = util::make_unique<NameMangler>(
+ NameManglerPolicy{ mContext.mCompilationPackage });
+ mContext.mPackageId = 0x7f;
+ mContext.mSymbols = createSymbolTableFromIncludePaths();
+ if (!mContext.mSymbols) {
+ return 1;
+ }
+
+ if (mOptions.verbose) {
+ mContext.getDiagnostics()->note(
+ DiagMessage() << "linking package '" << mContext.mCompilationPackage << "' "
+ << "with package ID " << std::hex << (int) mContext.mPackageId);
+ }
+
+ ResourceTable mergedTable;
+ TableMerger tableMerger(&mContext, &mergedTable);
+
+ struct FilesToProcess {
+ Source source;
+ ResourceFile file;
+ };
+
+ bool error = false;
+ std::queue<FilesToProcess> filesToProcess;
+ for (const std::string& input : inputFiles) {
+ if (util::stringEndsWith<char>(input, ".apk")) {
+ // TODO(adamlesinski): Load resources from a static library APK
+ // Merge the table into TableMerger.
+
+ } else if (util::stringEndsWith<char>(input, ".arsc.flat")) {
+ if (mOptions.verbose) {
+ mContext.getDiagnostics()->note(DiagMessage() << "linking " << input);
+ }
+
+ std::unique_ptr<ResourceTable> table = loadTable(input);
+ if (!table) {
+ return 1;
+ }
+
+ if (!tableMerger.merge(Source(input), table.get())) {
+ return 1;
+ }
+
+ } else {
+ // Extract the exported IDs here so we can build the resource table.
+ if (Maybe<ResourceFile> maybeF = loadFileExportHeader(input)) {
+ ResourceFile& f = maybeF.value();
+
+ if (f.name.package.empty()) {
+ f.name.package = mContext.getCompilationPackage().toString();
+ }
+
+ Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(f.name);
+
+ // Add this file to the table.
+ if (!mergedTable.addFileReference(mangledName ? mangledName.value() : f.name,
+ f.config, f.source,
+ util::utf8ToUtf16(buildResourceFileName(f)),
+ mContext.getDiagnostics())) {
+ error = true;
+ }
+
+ // Add the exports of this file to the table.
+ for (SourcedResourceName& exportedSymbol : f.exportedSymbols) {
+ if (exportedSymbol.name.package.empty()) {
+ exportedSymbol.name.package = mContext.getCompilationPackage()
+ .toString();
+ }
+
+ Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(
+ exportedSymbol.name);
+ if (!mergedTable.addResource(
+ mangledName ? mangledName.value() : exportedSymbol.name,
+ {}, {}, f.source.withLine(exportedSymbol.line),
+ util::make_unique<Id>(), mContext.getDiagnostics())) {
+ error = true;
+ }
+ }
+
+ filesToProcess.push(FilesToProcess{ Source(input), std::move(f) });
+ } else {
+ return 1;
+ }
+ }
+ }
+
+ if (error) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed parsing input");
+ return 1;
+ }
+
+ if (!verifyNoExternalPackages(&mergedTable)) {
+ return 1;
+ }
+
+ if (!mOptions.staticLib) {
+ PrivateAttributeMover mover;
+ if (!mover.consume(&mContext, &mergedTable)) {
+ mContext.getDiagnostics()->error(
+ DiagMessage() << "failed moving private attributes");
+ return 1;
+ }
+ }
+
+ {
+ IdAssigner idAssigner;
+ if (!idAssigner.consume(&mContext, &mergedTable)) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed assigning IDs");
+ return 1;
+ }
+ }
+
+ mContext.mNameMangler = util::make_unique<NameMangler>(
+ NameManglerPolicy{ mContext.mCompilationPackage, tableMerger.getMergedPackages() });
+ mContext.mSymbols = JoinedSymbolTableBuilder()
+ .addSymbolTable(util::make_unique<SymbolTableWrapper>(&mergedTable))
+ .addSymbolTable(std::move(mContext.mSymbols))
+ .build();
+
+ {
+ ReferenceLinker linker;
+ if (!linker.consume(&mContext, &mergedTable)) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed linking references");
+ return 1;
+ }
+ }
+
+ proguard::KeepSet proguardKeepSet;
+
+ std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter();
+ if (!archiveWriter) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed to create archive");
+ return 1;
+ }
+
+ {
+ XmlReferenceLinker manifestLinker;
+ if (manifestLinker.consume(&mContext, manifestXml.get())) {
+
+ if (!proguard::collectProguardRulesForManifest(Source(mOptions.manifestPath),
+ manifestXml.get(),
+ &proguardKeepSet)) {
+ error = true;
+ }
+
+ if (!flattenXml(manifestXml.get(), "AndroidManifest.xml", {},
+ archiveWriter.get())) {
+ error = true;
+ }
+ } else {
+ error = true;
+ }
+ }
+
+ for (; !filesToProcess.empty(); filesToProcess.pop()) {
+ FilesToProcess& f = filesToProcess.front();
+ if (f.file.name.type != ResourceType::kRaw &&
+ util::stringEndsWith<char>(f.source.path, ".xml.flat")) {
+ if (mOptions.verbose) {
+ mContext.getDiagnostics()->note(DiagMessage() << "linking " << f.source.path);
+ }
+
+ std::unique_ptr<XmlResource> xmlRes = loadBinaryXmlSkipFileExport(f.source.path);
+ if (!xmlRes) {
+ return 1;
+ }
+
+ xmlRes->file = std::move(f.file);
+
+ 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(), buildResourceFileName(xmlRes->file), maxSdkLevel,
+ archiveWriter.get())) {
+ error = true;
+ }
+
+ if (!mOptions.noAutoVersion) {
+ Maybe<ResourceTable::SearchResult> result = mergedTable.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;
+ if (!mergedTable.addFileReference(xmlRes->file.name,
+ xmlRes->file.config,
+ xmlRes->file.source,
+ util::utf8ToUtf16(
+ buildResourceFileName(xmlRes->file)),
+ mContext.getDiagnostics())) {
+ error = true;
+ continue;
+ }
+
+ if (!flattenXml(xmlRes.get(), buildResourceFileName(xmlRes->file),
+ sdkLevel, archiveWriter.get())) {
+ error = true;
+ }
+ }
+ }
+ }
+
+ } else {
+ error = true;
+ }
+ } else {
+ if (mOptions.verbose) {
+ mContext.getDiagnostics()->note(DiagMessage() << "copying " << f.source.path);
+ }
+
+ if (!copyFileToArchive(f.source.path, buildResourceFileName(f.file), 0,
+ archiveWriter.get())) {
+ error = true;
+ }
+ }
+ }
+
+ if (error) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed linking file resources");
+ return 1;
+ }
+
+ if (!mOptions.noAutoVersion) {
+ AutoVersioner versioner;
+ if (!versioner.consume(&mContext, &mergedTable)) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed versioning styles");
+ return 1;
+ }
+ }
+
+ if (!flattenTable(&mergedTable, archiveWriter.get())) {
+ mContext.getDiagnostics()->error(DiagMessage() << "failed to write resources.arsc");
+ return 1;
+ }
+
+ if (mOptions.generateJavaClassPath) {
+ if (!writeJavaFile(&mergedTable, mContext.getCompilationPackage())) {
+ return 1;
+ }
+ }
+
+ if (mOptions.generateProguardRulesPath) {
+ if (!writeProguardFile(proguardKeepSet)) {
+ return 1;
+ }
+ }
+
+ if (mOptions.verbose) {
+ Debug::printTable(&mergedTable);
+ for (; !tableMerger.getFileMergeQueue()->empty();
+ tableMerger.getFileMergeQueue()->pop()) {
+ const FileToMerge& f = tableMerger.getFileMergeQueue()->front();
+ mContext.getDiagnostics()->note(
+ DiagMessage() << f.srcPath << " -> " << f.dstPath << " from (0x"
+ << std::hex << (uintptr_t) f.srcTable << std::dec);
+ }
+ }
+
+ return 0;
+ }
+};
+
+int link(const std::vector<StringPiece>& args) {
+ LinkOptions options;
+ Flags flags = Flags()
+ .requiredFlag("-o", "Output path", &options.outputPath)
+ .requiredFlag("--manifest", "Path to the Android manifest to build",
+ &options.manifestPath)
+ .optionalFlagList("-I", "Adds an Android APK to link against", &options.includePaths)
+ .optionalFlag("--java", "Directory in which to generate R.java",
+ &options.generateJavaClassPath)
+ .optionalFlag("--proguard", "Output file for generated Proguard rules",
+ &options.generateProguardRulesPath)
+ .optionalSwitch("--no-auto-version",
+ "Disables automatic style and layout SDK versioning",
+ &options.noAutoVersion)
+ .optionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified "
+ "by -o",
+ &options.outputToDirectory)
+ .optionalSwitch("--static-lib", "Generate a static Android library", &options.staticLib)
+ .optionalSwitch("-v", "Enables verbose logging", &options.verbose);
+
+ if (!flags.parse("aapt2 link", args, &std::cerr)) {
+ return 1;
+ }
+
+ LinkCommand cmd = { options };
+ return cmd.run(flags.getArgs());
+}
+
+} // namespace aapt