AAPT2: Expose split support to command line

Bug:30445078
Change-Id: If4b8530dba71b9059b8e62c04757da99c1119d22
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index acb0f38..c1c5ba2 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -59,11 +59,14 @@
     std::string manifestPath;
     std::vector<std::string> includePaths;
     std::vector<std::string> overlayFiles;
+
+    // Java/Proguard options.
     Maybe<std::string> generateJavaClassPath;
     Maybe<std::string> customJavaPackage;
     std::set<std::string> extraJavaPackages;
     Maybe<std::string> generateProguardRulesPath;
     Maybe<std::string> generateMainDexProguardRulesPath;
+
     bool noAutoVersion = false;
     bool noVersionVectors = false;
     bool staticLib = false;
@@ -77,7 +80,13 @@
     Maybe<std::string> privateSymbols;
     ManifestFixerOptions manifestFixerOptions;
     std::unordered_set<std::string> products;
+
+    // Split APK options.
     TableSplitterOptions tableSplitterOptions;
+    std::vector<SplitConstraints> splitConstraints;
+    std::vector<std::string> splitPaths;
+
+    // Stable ID options.
     std::unordered_map<ResourceName, ResourceId> stableIdMap;
     Maybe<std::string> resourceIdMapPath;
 };
@@ -585,7 +594,7 @@
         const size_t resIdStrLen = line.size() - resIdStartIdx;
         StringPiece resIdStr = util::trimWhitespace(line.substr(resIdStartIdx, resIdStrLen));
 
-        Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(resIdStr);
+        Maybe<ResourceId> maybeId = ResourceUtils::parseResourceId(resIdStr);
         if (!maybeId) {
             diag->error(DiagMessage(Source(path, lineNo)) << "invalid resource ID '"
                         << resIdStr << "'");
@@ -597,6 +606,28 @@
     return true;
 }
 
+static bool parseSplitParameter(const StringPiece& arg, IDiagnostics* diag,
+                                std::string* outPath, SplitConstraints* outSplit) {
+    std::vector<std::string> parts = util::split(arg, ':');
+    if (parts.size() != 2) {
+        diag->error(DiagMessage() << "invalid split parameter '" << arg << "'");
+        diag->note(DiagMessage() << "should be --split path/to/output.apk:<config>[,<config>...]");
+        return false;
+    }
+    *outPath = parts[0];
+    std::vector<ConfigDescription> configs;
+    for (const StringPiece& configStr : util::tokenize(parts[1], ',')) {
+        configs.push_back({});
+        if (!ConfigDescription::parse(configStr, &configs.back())) {
+            diag->error(DiagMessage() << "invalid config '" << configStr
+                        << "' in split parameter '" << arg << "'");
+            return false;
+        }
+    }
+    outSplit->configs.insert(configs.begin(), configs.end());
+    return true;
+}
+
 class LinkCommand {
 public:
     LinkCommand(LinkContext* context, const LinkOptions& options) :
@@ -676,6 +707,30 @@
 
             appInfo.package = packageAttr->value;
 
+            if (xml::Attribute* versionCodeAttr =
+                    manifestEl->findAttribute(xml::kSchemaAndroid, "versionCode")) {
+                Maybe<uint32_t> maybeCode = ResourceUtils::parseInt(versionCodeAttr->value);
+                if (!maybeCode) {
+                    diag->error(DiagMessage(xmlRes->file.source.withLine(manifestEl->lineNumber))
+                                << "invalid android:versionCode '"
+                                << versionCodeAttr->value << "'");
+                    return {};
+                }
+                appInfo.versionCode = maybeCode.value();
+            }
+
+            if (xml::Attribute* revisionCodeAttr =
+                    manifestEl->findAttribute(xml::kSchemaAndroid, "revisionCode")) {
+                Maybe<uint32_t> maybeCode = ResourceUtils::parseInt(revisionCodeAttr->value);
+                if (!maybeCode) {
+                    diag->error(DiagMessage(xmlRes->file.source.withLine(manifestEl->lineNumber))
+                                << "invalid android:revisionCode '"
+                                << revisionCodeAttr->value << "'");
+                    return {};
+                }
+                appInfo.revisionCode = maybeCode.value();
+            }
+
             if (xml::Element* usesSdkEl = manifestEl->findChild({}, "uses-sdk")) {
                 if (xml::Attribute* minSdk =
                         usesSdkEl->findAttribute(xml::kSchemaAndroid, "minSdkVersion")) {
@@ -767,11 +822,11 @@
         return true;
     }
 
-    std::unique_ptr<IArchiveWriter> makeArchiveWriter() {
+    std::unique_ptr<IArchiveWriter> makeArchiveWriter(const StringPiece& out) {
         if (mOptions.outputToDirectory) {
-            return createDirectoryArchiveWriter(mContext->getDiagnostics(), mOptions.outputPath);
+            return createDirectoryArchiveWriter(mContext->getDiagnostics(), out);
         } else {
-            return createZipFileArchiveWriter(mContext->getDiagnostics(), mOptions.outputPath);
+            return createZipFileArchiveWriter(mContext->getDiagnostics(), out);
         }
     }
 
@@ -1179,6 +1234,94 @@
         return true;
     }
 
+    std::unique_ptr<xml::XmlResource> generateSplitManifest(const AppInfo& appInfo,
+                                                            const SplitConstraints& constraints) {
+        std::unique_ptr<xml::XmlResource> doc = util::make_unique<xml::XmlResource>();
+
+        std::unique_ptr<xml::Namespace> namespaceAndroid = util::make_unique<xml::Namespace>();
+        namespaceAndroid->namespaceUri = xml::kSchemaAndroid;
+        namespaceAndroid->namespacePrefix = "android";
+
+        std::unique_ptr<xml::Element> manifestEl = util::make_unique<xml::Element>();
+        manifestEl->name = "manifest";
+        manifestEl->attributes.push_back(
+                xml::Attribute{ "", "package", appInfo.package });
+
+        if (appInfo.versionCode) {
+            manifestEl->attributes.push_back(xml::Attribute{
+                    xml::kSchemaAndroid,
+                    "versionCode",
+                    std::to_string(appInfo.versionCode.value()) });
+        }
+
+        if (appInfo.revisionCode) {
+            manifestEl->attributes.push_back(xml::Attribute{
+                    xml::kSchemaAndroid,
+                    "revisionCode", std::to_string(appInfo.revisionCode.value()) });
+        }
+
+        std::stringstream splitName;
+        splitName << "config." << util::joiner(constraints.configs, "_");
+
+        manifestEl->attributes.push_back(
+                xml::Attribute{ "", "split", splitName.str() });
+
+        std::unique_ptr<xml::Element> applicationEl = util::make_unique<xml::Element>();
+        applicationEl->name = "application";
+        applicationEl->attributes.push_back(
+                xml::Attribute{ xml::kSchemaAndroid, "hasCode", "false" });
+
+        manifestEl->addChild(std::move(applicationEl));
+        namespaceAndroid->addChild(std::move(manifestEl));
+        doc->root = std::move(namespaceAndroid);
+        return doc;
+    }
+
+    /**
+     * Writes the AndroidManifest, ResourceTable, and all XML files referenced by the ResourceTable
+     * to the IArchiveWriter.
+     */
+    bool writeApk(IArchiveWriter* writer, proguard::KeepSet* keepSet, xml::XmlResource* manifest,
+                  ResourceTable* table) {
+        const bool keepRawValues = mOptions.staticLib;
+        bool result = flattenXml(manifest, "AndroidManifest.xml", {}, keepRawValues, writer,
+                                 mContext);
+        if (!result) {
+            return false;
+        }
+
+        ResourceFileFlattenerOptions fileFlattenerOptions;
+        fileFlattenerOptions.keepRawValues = keepRawValues;
+        fileFlattenerOptions.doNotCompressAnything = mOptions.doNotCompressAnything;
+        fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress;
+        fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion;
+        fileFlattenerOptions.noVersionVectors = mOptions.noVersionVectors;
+        fileFlattenerOptions.updateProguardSpec =
+                static_cast<bool>(mOptions.generateProguardRulesPath);
+
+        ResourceFileFlattener fileFlattener(fileFlattenerOptions, mContext, keepSet);
+
+        if (!fileFlattener.flatten(table, writer)) {
+            mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources");
+            return false;
+        }
+
+        if (mOptions.staticLib) {
+            if (!flattenTableToPb(table, writer)) {
+                mContext->getDiagnostics()->error(DiagMessage()
+                                                  << "failed to write resources.arsc.flat");
+                return false;
+            }
+        } else {
+            if (!flattenTable(table, writer)) {
+                mContext->getDiagnostics()->error(DiagMessage()
+                                                  << "failed to write resources.arsc");
+                return false;
+            }
+        }
+        return true;
+    }
+
     int run(const std::vector<std::string>& inputFiles) {
         // Load the AndroidManifest.xml
         std::unique_ptr<xml::XmlResource> manifestXml = loadXml(mOptions.manifestPath,
@@ -1187,30 +1330,33 @@
             return 1;
         }
 
+        // First extract the Package name without modifying it (via --rename-manifest-package).
         if (Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get(),
                                                                      mContext->getDiagnostics())) {
-            AppInfo& appInfo = maybeAppInfo.value();
+            const AppInfo& appInfo = maybeAppInfo.value();
             mContext->setCompilationPackage(appInfo.package);
-            if (appInfo.minSdkVersion) {
-                if (Maybe<int> maybeMinSdkVersion =
-                        ResourceUtils::tryParseSdkVersion(appInfo.minSdkVersion.value())) {
-                    mContext->setMinSdkVersion(maybeMinSdkVersion.value());
-                }
-            }
-        } else {
+        }
+
+        ManifestFixer manifestFixer(mOptions.manifestFixerOptions);
+        if (!manifestFixer.consume(mContext, manifestXml.get())) {
             return 1;
         }
 
-        if (!util::isJavaPackageName(mContext->getCompilationPackage())) {
-            mContext->getDiagnostics()->error(DiagMessage(mOptions.manifestPath)
-                                             << "invalid package name '"
-                                             << mContext->getCompilationPackage()
-                                             << "'");
+        Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get(),
+                                                                 mContext->getDiagnostics());
+        if (!maybeAppInfo) {
             return 1;
         }
 
+        const AppInfo& appInfo = maybeAppInfo.value();
+        if (appInfo.minSdkVersion) {
+            if (Maybe<int> maybeMinSdkVersion =
+                    ResourceUtils::parseSdkVersion(appInfo.minSdkVersion.value())) {
+                mContext->setMinSdkVersion(maybeMinSdkVersion.value());
+            }
+        }
+
         mContext->setNameManglerPolicy(NameManglerPolicy{ mContext->getCompilationPackage() });
-
         if (mContext->getCompilationPackage() == "android") {
             mContext->setPackageId(0x01);
         } else {
@@ -1258,9 +1404,7 @@
                         DiagMessage() << "failed moving private attributes");
                 return 1;
             }
-        }
 
-        if (!mOptions.staticLib) {
             // Assign IDs if we are building a regular app.
             IdAssigner idAssigner(&mOptions.stableIdMap);
             if (!idAssigner.consume(mContext, &mFinalTable)) {
@@ -1304,45 +1448,118 @@
         mContext->getExternalSymbols()->prependSource(
                         util::make_unique<ResourceTableSymbolSource>(&mFinalTable));
 
-        {
-            ReferenceLinker linker;
-            if (!linker.consume(mContext, &mFinalTable)) {
-                mContext->getDiagnostics()->error(DiagMessage() << "failed linking references");
+        ReferenceLinker linker;
+        if (!linker.consume(mContext, &mFinalTable)) {
+            mContext->getDiagnostics()->error(DiagMessage() << "failed linking references");
+            return 1;
+        }
+
+        if (mOptions.staticLib) {
+            if (!mOptions.products.empty()) {
+                mContext->getDiagnostics()->warn(
+                        DiagMessage() << "can't select products when building static library");
+            }
+        } else {
+            ProductFilter productFilter(mOptions.products);
+            if (!productFilter.consume(mContext, &mFinalTable)) {
+                mContext->getDiagnostics()->error(DiagMessage() << "failed stripping products");
                 return 1;
             }
+        }
 
-            if (mOptions.staticLib) {
-                if (!mOptions.products.empty()) {
-                    mContext->getDiagnostics()->warn(
-                            DiagMessage() << "can't select products when building static library");
-                }
+        if (!mOptions.noAutoVersion) {
+            AutoVersioner versioner;
+            if (!versioner.consume(mContext, &mFinalTable)) {
+                mContext->getDiagnostics()->error(DiagMessage() << "failed versioning styles");
+                return 1;
+            }
+        }
 
-                if (mOptions.tableSplitterOptions.configFilter != nullptr ||
-                        mOptions.tableSplitterOptions.preferredDensity) {
-                    mContext->getDiagnostics()->warn(
-                            DiagMessage() << "can't strip resources when building static library");
-                }
-            } else {
-                ProductFilter productFilter(mOptions.products);
-                if (!productFilter.consume(mContext, &mFinalTable)) {
-                    mContext->getDiagnostics()->error(DiagMessage() << "failed stripping products");
-                    return 1;
-                }
+        if (!mOptions.staticLib && mContext->getMinSdkVersion() > 0) {
+            if (mContext->verbose()) {
+                mContext->getDiagnostics()->note(
+                        DiagMessage() << "collapsing resource versions for minimum SDK "
+                        << mContext->getMinSdkVersion());
+            }
 
-                // 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);
+            VersionCollapser collapser;
+            if (!collapser.consume(mContext, &mFinalTable)) {
+                return 1;
             }
         }
 
         proguard::KeepSet proguardKeepSet;
         proguard::KeepSet proguardMainDexKeepSet;
 
-        std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter();
+        if (mOptions.staticLib) {
+            if (mOptions.tableSplitterOptions.configFilter != nullptr ||
+                    mOptions.tableSplitterOptions.preferredDensity) {
+                mContext->getDiagnostics()->warn(
+                        DiagMessage() << "can't strip resources when building static library");
+            }
+        } else {
+            // Adjust the SplitConstraints so that their SDK version is stripped if it is less
+            // than or equal to the minSdk. Otherwise the resources that have had their SDK version
+            // stripped due to minSdk won't ever match.
+            std::vector<SplitConstraints> adjustedConstraintsList;
+            adjustedConstraintsList.reserve(mOptions.splitConstraints.size());
+            for (const SplitConstraints& constraints : mOptions.splitConstraints) {
+                SplitConstraints adjustedConstraints;
+                for (const ConfigDescription& config : constraints.configs) {
+                    if (config.sdkVersion <= mContext->getMinSdkVersion()) {
+                        adjustedConstraints.configs.insert(config.copyWithoutSdkVersion());
+                    } else {
+                        adjustedConstraints.configs.insert(config);
+                    }
+                }
+                adjustedConstraintsList.push_back(std::move(adjustedConstraints));
+            }
+
+            TableSplitter tableSplitter(adjustedConstraintsList, mOptions.tableSplitterOptions);
+            if (!tableSplitter.verifySplitConstraints(mContext)) {
+                return 1;
+            }
+            tableSplitter.splitTable(&mFinalTable);
+
+            // Now we need to write out the Split APKs.
+            auto pathIter = mOptions.splitPaths.begin();
+            auto splitConstraintsIter = adjustedConstraintsList.begin();
+            for (std::unique_ptr<ResourceTable>& splitTable : tableSplitter.getSplits()) {
+                if (mContext->verbose()) {
+                    mContext->getDiagnostics()->note(
+                            DiagMessage(*pathIter) << "generating split with configurations '"
+                            << util::joiner(splitConstraintsIter->configs, ", ") << "'");
+                }
+
+                std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter(*pathIter);
+                if (!archiveWriter) {
+                    mContext->getDiagnostics()->error(DiagMessage() << "failed to create archive");
+                    return 1;
+                }
+
+                // Generate an AndroidManifest.xml for each split.
+                std::unique_ptr<xml::XmlResource> splitManifest =
+                        generateSplitManifest(appInfo, *splitConstraintsIter);
+
+                XmlReferenceLinker linker;
+                if (!linker.consume(mContext, splitManifest.get())) {
+                    mContext->getDiagnostics()->error(
+                            DiagMessage() << "failed to create Split AndroidManifest.xml");
+                    return 1;
+                }
+
+                if (!writeApk(archiveWriter.get(), &proguardKeepSet, splitManifest.get(),
+                              splitTable.get())) {
+                    return 1;
+                }
+
+                ++pathIter;
+                ++splitConstraintsIter;
+            }
+        }
+
+        // Start writing the base APK.
+        std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter(mOptions.outputPath);
         if (!archiveWriter) {
             mContext->getDiagnostics()->error(DiagMessage() << "failed to create archive");
             return 1;
@@ -1350,11 +1567,6 @@
 
         bool error = false;
         {
-            ManifestFixer manifestFixer(mOptions.manifestFixerOptions);
-            if (!manifestFixer.consume(mContext, manifestXml.get())) {
-                error = true;
-            }
-
             // AndroidManifest.xml has no resource name, but the CallSite is built from the name
             // (aka, which package the AndroidManifest.xml is coming from).
             // So we give it a package name so it can see local resources.
@@ -1382,13 +1594,6 @@
                         error = true;
                     }
                 }
-
-                const bool keepRawValues = mOptions.staticLib;
-                bool result = flattenXml(manifestXml.get(), "AndroidManifest.xml", {},
-                                         keepRawValues, archiveWriter.get(), mContext);
-                if (!result) {
-                    error = true;
-                }
             } else {
                 error = true;
             }
@@ -1399,58 +1604,10 @@
             return 1;
         }
 
-        if (!mOptions.noAutoVersion) {
-            AutoVersioner versioner;
-            if (!versioner.consume(mContext, &mFinalTable)) {
-                mContext->getDiagnostics()->error(DiagMessage() << "failed versioning styles");
-                return 1;
-            }
-        }
-
-        if (!mOptions.staticLib && mContext->getMinSdkVersion() > 0) {
-            if (mContext->verbose()) {
-                mContext->getDiagnostics()->note(
-                        DiagMessage() << "collapsing resource versions for minimum SDK "
-                        << mContext->getMinSdkVersion());
-            }
-
-            VersionCollapser collapser;
-            if (!collapser.consume(mContext, &mFinalTable)) {
-                return 1;
-            }
-        }
-
-        // Write out the table to an archive. Optimizations to the table should come before this
-        // step.
-        ResourceFileFlattenerOptions fileFlattenerOptions;
-        fileFlattenerOptions.keepRawValues = mOptions.staticLib;
-        fileFlattenerOptions.doNotCompressAnything = mOptions.doNotCompressAnything;
-        fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress;
-        fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion;
-        fileFlattenerOptions.noVersionVectors = mOptions.noVersionVectors;
-        fileFlattenerOptions.updateProguardSpec =
-                static_cast<bool>(mOptions.generateProguardRulesPath);
-        ResourceFileFlattener fileFlattener(fileFlattenerOptions, mContext, &proguardKeepSet);
-
-        if (!fileFlattener.flatten(&mFinalTable, archiveWriter.get())) {
-            mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources");
+        if (!writeApk(archiveWriter.get(), &proguardKeepSet, manifestXml.get(), &mFinalTable)) {
             return 1;
         }
 
-        if (mOptions.staticLib) {
-            if (!flattenTableToPb(&mFinalTable, archiveWriter.get())) {
-                mContext->getDiagnostics()->error(DiagMessage()
-                                                  << "failed to write resources.arsc.flat");
-                return 1;
-            }
-        } else {
-            if (!flattenTable(&mFinalTable, archiveWriter.get())) {
-                mContext->getDiagnostics()->error(DiagMessage()
-                                                  << "failed to write resources.arsc");
-                return 1;
-            }
-        }
-
         if (mOptions.generateJavaClassPath) {
             JavaClassGeneratorOptions options;
             options.types = JavaClassGeneratorOptions::SymbolTypes::kAll;
@@ -1538,6 +1695,7 @@
     bool requireLocalization = false;
     bool verbose = false;
     Maybe<std::string> stableIdFilePath;
+    std::vector<std::string> splitArgs;
     Flags flags = Flags()
             .requiredFlag("-o", "Output path", &options.outputPath)
             .requiredFlag("--manifest", "Path to the Android manifest to build",
@@ -1623,6 +1781,9 @@
                           &options.manifestFixerOptions.renameInstrumentationTargetPackage)
             .optionalFlagList("-0", "File extensions not to compress",
                               &options.extensionsToNotCompress)
+            .optionalFlagList("--split", "Split resources matching a set of configs out to a "
+                              "Split APK.\nSyntax: path/to/output.apk:<config>[,<config>[...]]",
+                              &splitArgs)
             .optionalSwitch("-v", "Enables verbose logging",
                             &verbose);
 
@@ -1741,6 +1902,16 @@
             ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
             ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"});
 
+    // Parse the split parameters.
+    for (const std::string& splitArg : splitArgs) {
+        options.splitPaths.push_back({});
+        options.splitConstraints.push_back({});
+        if (!parseSplitParameter(splitArg, context.getDiagnostics(), &options.splitPaths.back(),
+                                 &options.splitConstraints.back())) {
+            return 1;
+        }
+    }
+
     // Turn off auto versioning for static-libs.
     if (options.staticLib) {
         options.noAutoVersion = true;