Merge changes from topic "fcm_version"

* changes:
  Add AssembleVintfTest
  assemble_vintf: warn for missing env var for fwk_cm.empty.xml
  Add Named<T> in place of std::pair<std::string, T>
  assemble_vintf: move logic to CompatibilityMatrix::combine
  Use base::Split to replace tokenize() function.
  assemble_vintf: refactor to prepare for test
diff --git a/Android.bp b/Android.bp
index 2fd9eef..fea4d38 100644
--- a/Android.bp
+++ b/Android.bp
@@ -105,6 +105,20 @@
     ],
 }
 
+cc_library_static {
+    name: "libassemblevintf",
+    host_supported: true,
+    defaults: ["libvintf-defaults"],
+    shared_libs: [
+        "libvintf",
+        "libbase",
+    ],
+    srcs: [
+        "AssembleVintf.cpp",
+    ],
+    export_include_dirs: ["include-test"],
+}
+
 cc_binary_host {
     name: "assemble_vintf",
     defaults: ["libvintf-defaults"],
@@ -112,8 +126,11 @@
         "libvintf",
         "libbase",
     ],
+    static_libs: [
+        "libassemblevintf",
+    ],
     srcs: [
-        "assemble_vintf.cpp"
+        "assemble_vintf_main.cpp"
     ],
 }
 
diff --git a/assemble_vintf.cpp b/AssembleVintf.cpp
similarity index 61%
rename from assemble_vintf.cpp
rename to AssembleVintf.cpp
index 0aa79fd..d2a0b8f 100644
--- a/assemble_vintf.cpp
+++ b/AssembleVintf.cpp
@@ -14,22 +14,24 @@
  * limitations under the License.
  */
 
-#include <getopt.h>
 #include <stdlib.h>
 #include <unistd.h>
 
 #include <fstream>
 #include <iostream>
-#include <unordered_map>
 #include <sstream>
 #include <string>
+#include <unordered_map>
 
 #include <android-base/file.h>
 #include <android-base/parseint.h>
+#include <android-base/strings.h>
 
+#include <vintf/AssembleVintf.h>
 #include <vintf/KernelConfigParser.h>
 #include <vintf/parse_string.h>
 #include <vintf/parse_xml.h>
+#include "utils.h"
 
 #define BUFFER_SIZE sysconf(_SC_PAGESIZE)
 
@@ -58,16 +60,27 @@
 /**
  * Slurps the device manifest file and add build time flag to it.
  */
-class AssembleVintf {
+class AssembleVintfImpl : public AssembleVintf {
     using Condition = std::unique_ptr<KernelConfig>;
     using ConditionedConfig = std::pair<Condition, std::vector<KernelConfig> /* configs */>;
 
    public:
-    template<typename T>
-    static bool getFlag(const std::string& key, T* value) {
-        const char *envValue = getenv(key.c_str());
-        if (envValue == NULL) {
-            std::cerr << "Warning: " << key << " is missing, defaulted to " << (*value)
+    void setFakeEnv(const std::string& key, const std::string& value) { mFakeEnv[key] = value; }
+
+    std::string getEnv(const std::string& key) const {
+        auto it = mFakeEnv.find(key);
+        if (it != mFakeEnv.end()) {
+            return it->second;
+        }
+        const char* envValue = getenv(key.c_str());
+        return envValue != nullptr ? std::string(envValue) : std::string();
+    }
+
+    template <typename T>
+    bool getFlag(const std::string& key, T* value) const {
+        std::string envValue = getEnv(key);
+        if (envValue.empty()) {
+            std::cerr << "Warning: " << key << " is missing, defaulted to " << (*value) << "."
                       << std::endl;
             return true;
         }
@@ -83,15 +96,15 @@
      * Set *out to environment variable if *out is not a dummy value (i.e. default constructed).
      */
     template <typename T>
-    bool getFlagIfUnset(const std::string& envKey, T* out) {
+    bool getFlagIfUnset(const std::string& envKey, T* out) const {
         bool hasExistingValue = !(*out == T{});
 
         bool hasEnvValue = false;
         T envValue;
-        const char* envCValue = getenv(envKey.c_str());
-        if (envCValue != nullptr) {
-            if (!parse(envCValue, &envValue)) {
-                std::cerr << "Cannot parse " << envCValue << "." << std::endl;
+        std::string envStrValue = getEnv(envKey);
+        if (!envStrValue.empty()) {
+            if (!parse(envStrValue, &envValue)) {
+                std::cerr << "Cannot parse " << envValue << "." << std::endl;
                 return false;
             }
             hasEnvValue = true;
@@ -113,13 +126,10 @@
         return true;
     }
 
-    static bool getBooleanFlag(const char* key) {
-        const char* envValue = getenv(key);
-        return envValue != nullptr && strcmp(envValue, "true") == 0;
-    }
+    bool getBooleanFlag(const std::string& key) const { return getEnv(key) == std::string("true"); }
 
-    static size_t getIntegerFlag(const char* key, size_t defaultValue = 0) {
-        std::string envValue = getenv(key);
+    size_t getIntegerFlag(const std::string& key, size_t defaultValue = 0) const {
+        std::string envValue = getEnv(key);
         if (envValue.empty()) {
             return defaultValue;
         }
@@ -185,14 +195,10 @@
         return std::make_unique<KernelConfig>(std::move(sub), Tristate::YES);
     }
 
-    static bool parseFileForKernelConfigs(const std::string& path, std::vector<KernelConfig>* out) {
-        std::ifstream ifs{path};
-        if (!ifs.is_open()) {
-            std::cerr << "File '" << path << "' does not exist or cannot be read." << std::endl;
-            return false;
-        }
+    static bool parseFileForKernelConfigs(std::basic_istream<char>& stream,
+                                          std::vector<KernelConfig>* out) {
         KernelConfigParser parser(true /* processComments */, true /* relaxedFormat */);
-        std::string content = read(ifs);
+        std::string content = read(stream);
         status_t err = parser.process(content.c_str(), content.size());
         if (err != OK) {
             std::cerr << parser.error();
@@ -217,35 +223,32 @@
         return true;
     }
 
-    static bool parseFilesForKernelConfigs(const std::string& path,
+    static bool parseFilesForKernelConfigs(std::vector<NamedIstream>* streams,
                                            std::vector<ConditionedConfig>* out) {
         out->clear();
         ConditionedConfig commonConfig;
         bool foundCommonConfig = false;
         bool ret = true;
-        char *pathIter;
-        char *modPath = new char[path.length() + 1];
-        strcpy(modPath, path.c_str());
-        pathIter = strtok(modPath, ":");
-        while (ret && pathIter != NULL) {
-            if (isCommonConfig(pathIter)) {
-                ret &= parseFileForKernelConfigs(pathIter, &commonConfig.second);
+
+        for (auto& namedStream : *streams) {
+            if (isCommonConfig(namedStream.name())) {
+                ret &= parseFileForKernelConfigs(namedStream.stream(), &commonConfig.second);
                 foundCommonConfig = true;
             } else {
-                Condition condition = generateCondition(pathIter);
+                Condition condition = generateCondition(namedStream.name());
                 ret &= (condition != nullptr);
 
                 std::vector<KernelConfig> kernelConfigs;
-                if ((ret &= parseFileForKernelConfigs(pathIter, &kernelConfigs)))
+                if ((ret &= parseFileForKernelConfigs(namedStream.stream(), &kernelConfigs)))
                     out->emplace_back(std::move(condition), std::move(kernelConfigs));
             }
-            pathIter = strtok(NULL, ":");
         }
-        delete[] modPath;
 
         if (!foundCommonConfig) {
-            std::cerr << "No android-base.cfg is found in these paths: '" << path << "'"
-                      << std::endl;
+            std::cerr << "No android-base.cfg is found in these paths:" << std::endl;
+            for (auto& namedStream : *streams) {
+                std::cerr << "    " << namedStream.name() << std::endl;
+            }
         }
         ret &= foundCommonConfig;
         // first element is always common configs (no conditions).
@@ -261,28 +264,26 @@
         return path;
     }
 
-    std::basic_ostream<char>& out() const {
-        return mOutFileRef == nullptr ? std::cout : *mOutFileRef;
-    }
+    std::basic_ostream<char>& out() const { return mOutRef == nullptr ? std::cout : *mOutRef; }
 
     template <typename S>
-    using Schemas = std::vector<std::pair<std::string, S>>;
+    using Schemas = std::vector<Named<S>>;
     using HalManifests = Schemas<HalManifest>;
     using CompatibilityMatrices = Schemas<CompatibilityMatrix>;
 
     bool assembleHalManifest(HalManifests* halManifests) {
         std::string error;
-        HalManifest* halManifest = &halManifests->front().second;
+        HalManifest* halManifest = &halManifests->front().object;
         for (auto it = halManifests->begin() + 1; it != halManifests->end(); ++it) {
-            const std::string& path = it->first;
-            HalManifest& halToAdd = it->second;
+            const std::string& path = it->name;
+            HalManifest& halToAdd = it->object;
 
             if (halToAdd.level() != Level::UNSPECIFIED) {
                 if (halManifest->level() == Level::UNSPECIFIED) {
                     halManifest->mLevel = halToAdd.level();
                 } else if (halManifest->level() != halToAdd.level()) {
                     std::cerr << "Inconsistent FCM Version in HAL manifests:" << std::endl
-                              << "    File '" << halManifests->front().first << "' has level "
+                              << "    File '" << halManifests->front().name << "' has level "
                               << halManifest->level() << std::endl
                               << "    File '" << path << "' has level " << halToAdd.level()
                               << std::endl;
@@ -327,9 +328,9 @@
         }
         out().flush();
 
-        if (mCheckFile.is_open()) {
+        if (mCheckFile != nullptr) {
             CompatibilityMatrix checkMatrix;
-            if (!gCompatibilityMatrixConverter(&checkMatrix, read(mCheckFile))) {
+            if (!gCompatibilityMatrixConverter(&checkMatrix, read(*mCheckFile))) {
                 std::cerr << "Cannot parse check file as a compatibility matrix: "
                           << gCompatibilityMatrixConverter.lastError() << std::endl;
                 return false;
@@ -344,9 +345,9 @@
     }
 
     bool assembleFrameworkCompatibilityMatrixKernels(CompatibilityMatrix* matrix) {
-        for (const auto& pair : mKernels) {
+        for (auto& pair : mKernels) {
             std::vector<ConditionedConfig> conditionedConfigs;
-            if (!parseFilesForKernelConfigs(pair.second, &conditionedConfigs)) {
+            if (!parseFilesForKernelConfigs(&pair.second, &conditionedConfigs)) {
                 return false;
             }
             for (ConditionedConfig& conditionedConfig : conditionedConfigs) {
@@ -400,8 +401,8 @@
     Level getLowestFcmVersion(const CompatibilityMatrices& matrices) {
         Level ret = Level::UNSPECIFIED;
         for (const auto& e : matrices) {
-            if (ret == Level::UNSPECIFIED || ret > e.second.level()) {
-                ret = e.second.level();
+            if (ret == Level::UNSPECIFIED || ret > e.object.level()) {
+                ret = e.object.level();
             }
         }
         return ret;
@@ -410,19 +411,16 @@
     bool assembleCompatibilityMatrix(CompatibilityMatrices* matrices) {
         std::string error;
         CompatibilityMatrix* matrix = nullptr;
-        KernelSepolicyVersion kernelSepolicyVers;
-        Version sepolicyVers;
         std::unique_ptr<HalManifest> checkManifest;
-        if (matrices->front().second.mType == SchemaType::DEVICE) {
-            matrix = &matrices->front().second;
+        if (matrices->front().object.mType == SchemaType::DEVICE) {
+            matrix = &matrices->front().object;
         }
 
-        if (matrices->front().second.mType == SchemaType::FRAMEWORK) {
+        if (matrices->front().object.mType == SchemaType::FRAMEWORK) {
             Level deviceLevel = Level::UNSPECIFIED;
-            std::vector<std::string> fileList;
-            if (mCheckFile.is_open()) {
+            if (mCheckFile != nullptr) {
                 checkManifest = std::make_unique<HalManifest>();
-                if (!gHalManifestConverter(checkManifest.get(), read(mCheckFile))) {
+                if (!gHalManifestConverter(checkManifest.get(), read(*mCheckFile))) {
                     std::cerr << "Cannot parse check file as a HAL manifest: "
                               << gHalManifestConverter.lastError() << std::endl;
                     return false;
@@ -436,55 +434,39 @@
                 deviceLevel = getLowestFcmVersion(*matrices);
             }
 
-            for (auto& e : *matrices) {
-                if (e.second.level() == deviceLevel) {
-                    fileList.push_back(e.first);
-                    matrix = &e.second;
-                }
-            }
-            if (matrix == nullptr) {
-                std::cerr << "FATAL ERROR: cannot find matrix with level '" << deviceLevel << "'"
-                          << std::endl;
-                return false;
-            }
-            for (auto& e : *matrices) {
-                if (e.second.level() <= deviceLevel) {
-                    continue;
-                }
-                fileList.push_back(e.first);
-                if (!matrix->addAllHalsAsOptional(&e.second, &error)) {
-                    std::cerr << "File \"" << e.first << "\" cannot be added: " << error
-                              << ". See <hal> with the same name "
-                              << "in previously parsed files or previously declared in this file."
-                              << std::endl;
+            if (deviceLevel == Level::UNSPECIFIED) {
+                // building empty.xml
+                matrix = &matrices->front().object;
+            } else {
+                matrix = CompatibilityMatrix::combine(deviceLevel, matrices, &error);
+                if (matrix == nullptr) {
+                    std::cerr << error << std::endl;
                     return false;
                 }
             }
 
-            if (!getFlag("BOARD_SEPOLICY_VERS", &sepolicyVers)) {
-                return false;
-            }
-            if (!getFlag("POLICYVERS", &kernelSepolicyVers)) {
-                return false;
-            }
-
             if (!assembleFrameworkCompatibilityMatrixKernels(matrix)) {
                 return false;
             }
 
-            matrix->framework.mSepolicy =
-                Sepolicy(kernelSepolicyVers, {{sepolicyVers.majorVer, sepolicyVers.minorVer}});
-
-            Version avbMetaVersion;
-            if (!getFlag("FRAMEWORK_VBMETA_VERSION", &avbMetaVersion)) {
-                return false;
+            // set sepolicy.sepolicy-version to BOARD_SEPOLICY_VERS when none is specified.
+            std::vector<VersionRange>* sepolicyVrs =
+                &matrix->framework.mSepolicy.mSepolicyVersionRanges;
+            VersionRange sepolicyVr;
+            if (!sepolicyVrs->empty()) sepolicyVr = sepolicyVrs->front();
+            if (getFlagIfUnset("BOARD_SEPOLICY_VERS", &sepolicyVr)) {
+                *sepolicyVrs = {{sepolicyVr}};
             }
-            matrix->framework.mAvbMetaVersion = avbMetaVersion;
+
+            getFlagIfUnset("POLICYVERS", &matrix->framework.mSepolicy.mKernelSepolicyVersion);
+            getFlagIfUnset("FRAMEWORK_VBMETA_VERSION", &matrix->framework.mAvbMetaVersion);
 
             out() << "<!--" << std::endl;
             out() << "    Input:" << std::endl;
-            for (const auto& path : fileList) {
-                out() << "        " << getFileNameFromPath(path) << std::endl;
+            for (const auto& e : *matrices) {
+                if (!e.name.empty()) {
+                    out() << "        " << getFileNameFromPath(e.name) << std::endl;
+                }
             }
             out() << "-->" << std::endl;
         }
@@ -533,7 +515,7 @@
         return assemble(&schemas) ? SUCCESS : FAIL_AND_EXIT;
     }
 
-    bool assemble() {
+    bool assemble() override {
         using std::placeholders::_1;
         if (mInFiles.empty()) {
             std::cerr << "Missing input file." << std::endl;
@@ -541,14 +523,14 @@
         }
 
         auto status = tryAssemble(gHalManifestConverter, "manifest",
-                                  std::bind(&AssembleVintf::assembleHalManifest, this, _1));
+                                  std::bind(&AssembleVintfImpl::assembleHalManifest, this, _1));
         if (status == SUCCESS) return true;
         if (status == FAIL_AND_EXIT) return false;
 
         resetInFiles();
 
         status = tryAssemble(gCompatibilityMatrixConverter, "compatibility matrix",
-                             std::bind(&AssembleVintf::assembleCompatibilityMatrix, this, _1));
+                             std::bind(&AssembleVintfImpl::assembleCompatibilityMatrix, this, _1));
         if (status == SUCCESS) return true;
         if (status == FAIL_AND_EXIT) return false;
 
@@ -560,22 +542,30 @@
         return false;
     }
 
-    bool openOutFile(const char* path) {
-        mOutFileRef = std::make_unique<std::ofstream>();
-        mOutFileRef->open(path);
-        return mOutFileRef->is_open();
+    std::ostream& setOutputStream(Ostream&& out) override {
+        mOutRef = std::move(out);
+        return *mOutRef;
     }
 
-    bool openInFile(const char* path) {
-        auto s = std::make_unique<std::ifstream>(path);
-        if (!s->is_open()) return false;
-        mInFiles.emplace(mInFiles.end(), std::string{path}, std::move(s));
-        return true;
+    std::istream& addInputStream(const std::string& name, Istream&& in) override {
+        auto it = mInFiles.emplace(mInFiles.end(), name, std::move(in));
+        return it->stream();
     }
 
-    bool openCheckFile(const char* path) {
-        mCheckFile.open(path);
-        return mCheckFile.is_open();
+    std::istream& setCheckInputStream(Istream&& in) override {
+        mCheckFile = std::move(in);
+        return *mCheckFile;
+    }
+
+    bool hasKernelVersion(const Version& kernelVer) const override {
+        return mKernels.find(kernelVer) != mKernels.end();
+    }
+
+    std::istream& addKernelConfigInputStream(const Version& kernelVer, const std::string& name,
+                                             Istream&& in) override {
+        auto&& kernel = mKernels[kernelVer];
+        auto it = kernel.emplace(kernel.end(), name, std::move(in));
+        return it->stream();
     }
 
     void resetInFiles() {
@@ -585,169 +575,76 @@
         }
     }
 
-    void setOutputMatrix() { mOutputMatrix = true; }
+    void setOutputMatrix() override { mOutputMatrix = true; }
 
-    bool setHalsOnly() {
+    bool setHalsOnly() override {
         if (mSerializeFlags) return false;
         mSerializeFlags |= SerializeFlag::HALS_ONLY;
         return true;
     }
 
-    bool setNoHals() {
+    bool setNoHals() override {
         if (mSerializeFlags) return false;
         mSerializeFlags |= SerializeFlag::NO_HALS;
         return true;
     }
 
-    bool addKernel(const std::string& kernelArg) {
-        auto ind = kernelArg.find(':');
-        if (ind == std::string::npos) {
-            std::cerr << "Unrecognized --kernel option '" << kernelArg << "'" << std::endl;
-            return false;
-        }
-        std::string kernelVerStr{kernelArg.begin(), kernelArg.begin() + ind};
-        std::string kernelConfigPath{kernelArg.begin() + ind + 1, kernelArg.end()};
-        Version kernelVer;
-        if (!parse(kernelVerStr, &kernelVer)) {
-            std::cerr << "Unrecognized kernel version '" << kernelVerStr << "'" << std::endl;
-            return false;
-        }
-        if (mKernels.find(kernelVer) != mKernels.end()) {
-            std::cerr << "Multiple --kernel for " << kernelVer << " is specified." << std::endl;
-            return false;
-        }
-        mKernels[kernelVer] = kernelConfigPath;
-        return true;
-    }
-
    private:
     std::vector<NamedIstream> mInFiles;
-    std::unique_ptr<std::ofstream> mOutFileRef;
-    std::ifstream mCheckFile;
+    Ostream mOutRef;
+    Istream mCheckFile;
     bool mOutputMatrix = false;
     SerializeFlags mSerializeFlags = SerializeFlag::EVERYTHING;
-    std::map<Version, std::string> mKernels;
+    std::map<Version, std::vector<NamedIstream>> mKernels;
+    std::map<std::string, std::string> mFakeEnv;
 };
 
+bool AssembleVintf::openOutFile(const std::string& path) {
+    return static_cast<std::ofstream&>(setOutputStream(std::make_unique<std::ofstream>(path)))
+        .is_open();
+}
+
+bool AssembleVintf::openInFile(const std::string& path) {
+    return static_cast<std::ifstream&>(addInputStream(path, std::make_unique<std::ifstream>(path)))
+        .is_open();
+}
+
+bool AssembleVintf::openCheckFile(const std::string& path) {
+    return static_cast<std::ifstream&>(setCheckInputStream(std::make_unique<std::ifstream>(path)))
+        .is_open();
+}
+
+bool AssembleVintf::addKernel(const std::string& kernelArg) {
+    auto tokens = base::Split(kernelArg, ":");
+    if (tokens.size() <= 1) {
+        std::cerr << "Unrecognized --kernel option '" << kernelArg << "'" << std::endl;
+        return false;
+    }
+    Version kernelVer;
+    if (!parse(tokens.front(), &kernelVer)) {
+        std::cerr << "Unrecognized kernel version '" << tokens.front() << "'" << std::endl;
+        return false;
+    }
+    if (hasKernelVersion(kernelVer)) {
+        std::cerr << "Multiple --kernel for " << kernelVer << " is specified." << std::endl;
+        return false;
+    }
+    for (auto it = tokens.begin() + 1; it != tokens.end(); ++it) {
+        bool opened =
+            static_cast<std::ifstream&>(
+                addKernelConfigInputStream(kernelVer, *it, std::make_unique<std::ifstream>(*it)))
+                .is_open();
+        if (!opened) {
+            std::cerr << "Cannot open file '" << *it << "'." << std::endl;
+            return false;
+        }
+    }
+    return true;
+}
+
+std::unique_ptr<AssembleVintf> AssembleVintf::newInstance() {
+    return std::make_unique<AssembleVintfImpl>();
+}
+
 }  // namespace vintf
 }  // namespace android
-
-void help() {
-    std::cerr << "assemble_vintf: Checks if a given manifest / matrix file is valid and \n"
-                 "    fill in build-time flags into the given file.\n"
-                 "assemble_vintf -h\n"
-                 "               Display this help text.\n"
-                 "assemble_vintf -i <input file>[:<input file>[...]] [-o <output file>] [-m]\n"
-                 "               [-c [<check file>]]\n"
-                 "               Fill in build-time flags into the given file.\n"
-                 "    -i <input file>[:<input file>[...]]\n"
-                 "               A list of input files. Format is automatically detected for the\n"
-                 "               first file, and the remaining files must have the same format.\n"
-                 "               Files other than the first file should only have <hal> defined;\n"
-                 "               other entries are ignored.\n"
-                 "    -o <output file>\n"
-                 "               Optional output file. If not specified, write to stdout.\n"
-                 "    -m\n"
-                 "               a compatible compatibility matrix is\n"
-                 "               generated instead; for example, given a device manifest,\n"
-                 "               a framework compatibility matrix is generated. This flag\n"
-                 "               is ignored when input is a compatibility matrix.\n"
-                 "    -c [<check file>]\n"
-                 "               After writing the output file, check compatibility between\n"
-                 "               output file and check file.\n"
-                 "               If -c is set but the check file is not specified, a warning\n"
-                 "               message is written to stderr. Return 0.\n"
-                 "               If the check file is specified but is not compatible, an error\n"
-                 "               message is written to stderr. Return 1.\n"
-                 "    --kernel=<version>:<android-base.cfg>[:<android-base-arch.cfg>[...]]\n"
-                 "               Add a kernel entry to framework compatibility matrix.\n"
-                 "               Ignored for other input format.\n"
-                 "               <version> has format: 3.18\n"
-                 "               <android-base.cfg> is the location of android-base.cfg\n"
-                 "               <android-base-arch.cfg> is the location of an optional\n"
-                 "               arch-specific config fragment, more than one may be specified\n"
-                 "    -l, --hals-only\n"
-                 "               Output has only <hal> entries. Cannot be used with -n.\n"
-                 "    -n, --no-hals\n"
-                 "               Output has no <hal> entries (but all other entries).\n"
-                 "               Cannot be used with -l.\n";
-}
-
-int main(int argc, char **argv) {
-    const struct option longopts[] = {{"kernel", required_argument, NULL, 'k'},
-                                      {"hals-only", no_argument, NULL, 'l'},
-                                      {"no-hals", no_argument, NULL, 'n'},
-                                      {0, 0, 0, 0}};
-
-    std::string outFilePath;
-    ::android::vintf::AssembleVintf assembleVintf;
-    int res;
-    int optind;
-    while ((res = getopt_long(argc, argv, "hi:o:mc:nl", longopts, &optind)) >= 0) {
-        switch (res) {
-            case 'i': {
-                char* inFilePath = strtok(optarg, ":");
-                while (inFilePath != NULL) {
-                    if (!assembleVintf.openInFile(inFilePath)) {
-                        std::cerr << "Failed to open " << optarg << std::endl;
-                        return 1;
-                    }
-                    inFilePath = strtok(NULL, ":");
-                }
-            } break;
-
-            case 'o': {
-                outFilePath = optarg;
-                if (!assembleVintf.openOutFile(optarg)) {
-                    std::cerr << "Failed to open " << optarg << std::endl;
-                    return 1;
-                }
-            } break;
-
-            case 'm': {
-                assembleVintf.setOutputMatrix();
-            } break;
-
-            case 'c': {
-                if (strlen(optarg) != 0) {
-                    if (!assembleVintf.openCheckFile(optarg)) {
-                        std::cerr << "Failed to open " << optarg << std::endl;
-                        return 1;
-                    }
-                } else {
-                    std::cerr << "WARNING: no compatibility check is done on "
-                              << (outFilePath.empty() ? "output" : outFilePath) << std::endl;
-                }
-            } break;
-
-            case 'k': {
-                if (!assembleVintf.addKernel(optarg)) {
-                    std::cerr << "ERROR: Unrecognized --kernel argument." << std::endl;
-                    return 1;
-                }
-            } break;
-
-            case 'l': {
-                if (!assembleVintf.setHalsOnly()) {
-                    return 1;
-                }
-            } break;
-
-            case 'n': {
-                if (!assembleVintf.setNoHals()) {
-                    return 1;
-                }
-            } break;
-
-            case 'h':
-            default: {
-                help();
-                return 1;
-            } break;
-        }
-    }
-
-    bool success = assembleVintf.assemble();
-
-    return success ? 0 : 1;
-}
diff --git a/CompatibilityMatrix.cpp b/CompatibilityMatrix.cpp
index 1e7d542..076055c 100644
--- a/CompatibilityMatrix.cpp
+++ b/CompatibilityMatrix.cpp
@@ -21,6 +21,9 @@
 #include "parse_string.h"
 #include "utils.h"
 
+#include <iostream>
+#include "parse_xml.h"
+
 namespace android {
 namespace vintf {
 
@@ -166,5 +169,122 @@
              lft.framework.mAvbMetaVersion == rgt.framework.mAvbMetaVersion));
 }
 
+// Find compatibility_matrix.empty.xml (which has unspecified level) and use it
+// as a base matrix.
+CompatibilityMatrix* CompatibilityMatrix::findOrInsertBaseMatrix(
+    std::vector<Named<CompatibilityMatrix>>* matrices, std::string* error) {
+    bool multipleFound = false;
+    CompatibilityMatrix* matrix = nullptr;
+    for (auto& e : *matrices) {
+        if (e.object.level() == Level::UNSPECIFIED) {
+            if (!e.object.mHals.empty()) {
+                if (error) {
+                    *error = "Error: File \"" + e.name + "\" should not contain " + "HAL elements.";
+                }
+                return nullptr;
+            }
+
+            if (!e.object.mXmlFiles.empty()) {
+                if (error) {
+                    *error =
+                        "Error: File \"" + e.name + "\" should not contain " + "XML File elements.";
+                }
+                return nullptr;
+            }
+
+            if (matrix != nullptr) {
+                multipleFound = true;
+            }
+
+            matrix = &e.object;
+            // continue to detect multiple files with "unspecified" levels
+        }
+    }
+
+    if (multipleFound) {
+        if (error) {
+            *error =
+                "Error: multiple framework compatibility matrix files have "
+                "unspecified level; there should only be one such file.\n";
+            for (auto& e : *matrices) {
+                if (e.object.level() == Level::UNSPECIFIED) {
+                    *error += "    " + e.name + "\n";
+                }
+            }
+        }
+        return nullptr;
+    }
+
+    if (matrix == nullptr) {
+        matrix = &matrices->emplace(matrices->end())->object;
+        matrix->mType = SchemaType::FRAMEWORK;
+        matrix->mLevel = Level::UNSPECIFIED;
+    }
+
+    return matrix;
+}
+
+CompatibilityMatrix* CompatibilityMatrix::combine(Level deviceLevel,
+                                                  std::vector<Named<CompatibilityMatrix>>* matrices,
+                                                  std::string* error) {
+    if (deviceLevel == Level::UNSPECIFIED) {
+        if (error) {
+            *error = "Error: device level is unspecified.";
+        }
+        return nullptr;
+    }
+
+    CompatibilityMatrix* matrix = findOrInsertBaseMatrix(matrices, error);
+    if (matrix == nullptr) {
+        return nullptr;
+    }
+
+    matrix->mLevel = deviceLevel;
+
+    for (auto& e : *matrices) {
+        if (&e.object != matrix && e.object.level() == deviceLevel) {
+            if (!matrix->addAllHals(&e.object, error)) {
+                if (error) {
+                    *error = "File \"" + e.name + "\" cannot be added: HAL " + *error +
+                             " has a conflict.";
+                }
+                return nullptr;
+            }
+
+            if (!matrix->addAllXmlFiles(&e.object, error)) {
+                if (error) {
+                    *error = "File \"" + e.name + "\" cannot be added: XML File entry " + *error +
+                             " has a conflict.";
+                }
+                return nullptr;
+            }
+        }
+    }
+
+    for (auto& e : *matrices) {
+        if (&e.object != matrix && e.object.level() != Level::UNSPECIFIED &&
+            e.object.level() > deviceLevel) {
+            if (!matrix->addAllHalsAsOptional(&e.object, error)) {
+                if (error) {
+                    *error = "File \"" + e.name + "\" cannot be added: " + *error +
+                             ". See <hal> with the same name " +
+                             "in previously parsed files or previously declared in this file.";
+                }
+                return nullptr;
+            }
+
+            if (!matrix->addAllXmlFilesAsOptional(&e.object, error)) {
+                if (error) {
+                    *error = "File \"" + e.name + "\" cannot be added: XML File entry " + *error +
+                             " has a conflict.";
+                }
+                return nullptr;
+            }
+        }
+    }
+
+    return matrix;
+}
+
 } // namespace vintf
 } // namespace android
diff --git a/assemble_vintf_main.cpp b/assemble_vintf_main.cpp
new file mode 100644
index 0000000..45e6dc2
--- /dev/null
+++ b/assemble_vintf_main.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2017 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 <getopt.h>
+
+#include <iostream>
+
+#include <android-base/strings.h>
+#include <vintf/AssembleVintf.h>
+#include "utils.h"
+
+void help() {
+    std::cerr << "assemble_vintf: Checks if a given manifest / matrix file is valid and \n"
+                 "    fill in build-time flags into the given file.\n"
+                 "assemble_vintf -h\n"
+                 "               Display this help text.\n"
+                 "assemble_vintf -i <input file>[:<input file>[...]] [-o <output file>] [-m]\n"
+                 "               [-c [<check file>]]\n"
+                 "               Fill in build-time flags into the given file.\n"
+                 "    -i <input file>[:<input file>[...]]\n"
+                 "               A list of input files. Format is automatically detected for the\n"
+                 "               first file, and the remaining files must have the same format.\n"
+                 "               Files other than the first file should only have <hal> defined;\n"
+                 "               other entries are ignored.\n"
+                 "    -o <output file>\n"
+                 "               Optional output file. If not specified, write to stdout.\n"
+                 "    -m\n"
+                 "               a compatible compatibility matrix is\n"
+                 "               generated instead; for example, given a device manifest,\n"
+                 "               a framework compatibility matrix is generated. This flag\n"
+                 "               is ignored when input is a compatibility matrix.\n"
+                 "    -c [<check file>]\n"
+                 "               After writing the output file, check compatibility between\n"
+                 "               output file and check file.\n"
+                 "               If -c is set but the check file is not specified, a warning\n"
+                 "               message is written to stderr. Return 0.\n"
+                 "               If the check file is specified but is not compatible, an error\n"
+                 "               message is written to stderr. Return 1.\n"
+                 "    --kernel=<version>:<android-base.cfg>[:<android-base-arch.cfg>[...]]\n"
+                 "               Add a kernel entry to framework compatibility matrix.\n"
+                 "               Ignored for other input format.\n"
+                 "               <version> has format: 3.18\n"
+                 "               <android-base.cfg> is the location of android-base.cfg\n"
+                 "               <android-base-arch.cfg> is the location of an optional\n"
+                 "               arch-specific config fragment, more than one may be specified\n"
+                 "    -l, --hals-only\n"
+                 "               Output has only <hal> entries. Cannot be used with -n.\n"
+                 "    -n, --no-hals\n"
+                 "               Output has no <hal> entries (but all other entries).\n"
+                 "               Cannot be used with -l.\n";
+}
+
+int main(int argc, char** argv) {
+    using namespace ::android::vintf;
+    const struct option longopts[] = {{"kernel", required_argument, NULL, 'k'},
+                                      {"hals-only", no_argument, NULL, 'l'},
+                                      {"no-hals", no_argument, NULL, 'n'},
+                                      {0, 0, 0, 0}};
+
+    std::string outFilePath;
+    auto assembleVintf = AssembleVintf::newInstance();
+    int res;
+    int optind;
+    while ((res = getopt_long(argc, argv, "hi:o:mc:nl", longopts, &optind)) >= 0) {
+        switch (res) {
+            case 'i': {
+                for (const auto& inFilePath : ::android::base::Split(optarg, ":")) {
+                    if (!assembleVintf->openInFile(inFilePath.c_str())) {
+                        std::cerr << "Failed to open " << inFilePath << std::endl;
+                        return 1;
+                    }
+                }
+            } break;
+
+            case 'o': {
+                outFilePath = optarg;
+                if (!assembleVintf->openOutFile(optarg)) {
+                    std::cerr << "Failed to open " << optarg << std::endl;
+                    return 1;
+                }
+            } break;
+
+            case 'm': {
+                assembleVintf->setOutputMatrix();
+            } break;
+
+            case 'c': {
+                if (!assembleVintf->openCheckFile(optarg)) {
+                    std::cerr << "Failed to open " << optarg << std::endl;
+                    return 1;
+                }
+            } break;
+
+            case 'k': {
+                if (!assembleVintf->addKernel(optarg)) {
+                    std::cerr << "ERROR: Unrecognized --kernel argument." << std::endl;
+                    return 1;
+                }
+            } break;
+
+            case 'l': {
+                if (!assembleVintf->setHalsOnly()) {
+                    return 1;
+                }
+            } break;
+
+            case 'n': {
+                if (!assembleVintf->setNoHals()) {
+                    return 1;
+                }
+            } break;
+
+            case 'h':
+            default: {
+                help();
+                return 1;
+            } break;
+        }
+    }
+
+    bool success = assembleVintf->assemble();
+
+    return success ? 0 : 1;
+}
diff --git a/include-test/vintf/AssembleVintf.h b/include-test/vintf/AssembleVintf.h
new file mode 100644
index 0000000..1b457eb
--- /dev/null
+++ b/include-test/vintf/AssembleVintf.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 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 ANDROID_VINTF_ASSEMBLE_VINTF_H
+#define ANDROID_VINTF_ASSEMBLE_VINTF_H
+
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <vintf/Version.h>
+
+namespace android {
+namespace vintf {
+
+class AssembleVintf {
+   public:
+    using Ostream = std::unique_ptr<std::ostream>;
+    using Istream = std::unique_ptr<std::istream>;
+
+    static std::unique_ptr<AssembleVintf> newInstance();
+
+    virtual ~AssembleVintf() = default;
+    virtual bool setHalsOnly() = 0;
+    virtual bool setNoHals() = 0;
+    virtual void setOutputMatrix() = 0;
+    virtual bool assemble() = 0;
+
+    bool openOutFile(const std::string& path);
+    bool openInFile(const std::string& path);
+    bool openCheckFile(const std::string& path);
+    bool addKernel(const std::string& kernelArg);
+
+    virtual std::ostream& setOutputStream(Ostream&&) = 0;
+    virtual std::istream& addInputStream(const std::string& name, Istream&&) = 0;
+    virtual std::istream& setCheckInputStream(Istream&&) = 0;
+    virtual std::istream& addKernelConfigInputStream(const Version& kernelVer,
+                                                     const std::string& name, Istream&& in) = 0;
+    virtual void setFakeEnv(const std::string& key, const std::string& value) = 0;
+
+   protected:
+    virtual bool hasKernelVersion(const Version&) const = 0;
+    virtual std::string getEnv(const std::string& key) const = 0;
+};
+
+}  // namespace vintf
+}  // namespace android
+#endif  // ANDROID_VINTF_ASSEMBLE_VINTF_H
diff --git a/include/vintf/CompatibilityMatrix.h b/include/vintf/CompatibilityMatrix.h
index bfb4dbf..62b505a 100644
--- a/include/vintf/CompatibilityMatrix.h
+++ b/include/vintf/CompatibilityMatrix.h
@@ -27,6 +27,7 @@
 #include "MapValueIterator.h"
 #include "MatrixHal.h"
 #include "MatrixKernel.h"
+#include "Named.h"
 #include "SchemaType.h"
 #include "Sepolicy.h"
 #include "Vndk.h"
@@ -73,12 +74,25 @@
 
     status_t fetchAllInformation(const std::string& path, std::string* error = nullptr);
 
+    // Combine a subset of "matrices". For each CompatibilityMatrix in matrices,
+    // - If level() == UNSPECIFIED, use it as the base matrix (for non-HAL, non-XML-file
+    //   requirements).
+    // - If level() < deviceLevel, ignore
+    // - If level() == deviceLevel, all HAL versions and XML files are added as is
+    //   (optionality is kept)
+    // - If level() > deviceLevel, all HAL versions and XML files are added as optional.
+    static CompatibilityMatrix* combine(Level deviceLevel,
+                                        std::vector<Named<CompatibilityMatrix>>* matrices,
+                                        std::string* error);
+    static CompatibilityMatrix* findOrInsertBaseMatrix(
+        std::vector<Named<CompatibilityMatrix>>* matrices, std::string* error);
+
     friend struct HalManifest;
     friend struct RuntimeInfo;
     friend struct CompatibilityMatrixConverter;
     friend struct LibVintfTest;
     friend class VintfObject;
-    friend class AssembleVintf;
+    friend class AssembleVintfImpl;
     friend bool operator==(const CompatibilityMatrix &, const CompatibilityMatrix &);
 
     SchemaType mType;
diff --git a/include/vintf/HalManifest.h b/include/vintf/HalManifest.h
index cc64201..b9cd504 100644
--- a/include/vintf/HalManifest.h
+++ b/include/vintf/HalManifest.h
@@ -122,7 +122,7 @@
    private:
     friend struct HalManifestConverter;
     friend class VintfObject;
-    friend class AssembleVintf;
+    friend class AssembleVintfImpl;
     friend struct LibVintfTest;
     friend std::string dump(const HalManifest &vm);
     friend bool operator==(const HalManifest &lft, const HalManifest &rgt);
diff --git a/include/vintf/MatrixKernel.h b/include/vintf/MatrixKernel.h
index f0990a8..cee7e32 100644
--- a/include/vintf/MatrixKernel.h
+++ b/include/vintf/MatrixKernel.h
@@ -58,7 +58,7 @@
    private:
     friend struct MatrixKernelConverter;
     friend struct MatrixKernelConditionsConverter;
-    friend class AssembleVintf;
+    friend class AssembleVintfImpl;
 
     KernelVersion mMinLts;
     std::vector<KernelConfig> mConfigs;
diff --git a/include/vintf/Named.h b/include/vintf/Named.h
new file mode 100644
index 0000000..eacb70b
--- /dev/null
+++ b/include/vintf/Named.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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 ANDROID_VINTF_NAMED_H
+#define ANDROID_VINTF_NAMED_H
+
+#include <string>
+
+template <typename T>
+struct Named {
+    std::string name;
+    T object;
+
+    Named() = default;
+    Named(const std::string& n, const T& o) : name(n), object(o) {}
+    Named(std::string&& n, T&& o) : name(std::move(n)), object(std::move(o)) {}
+};
+
+#endif  // ANDROID_VINTF_NAMED_H
diff --git a/include/vintf/Sepolicy.h b/include/vintf/Sepolicy.h
index 698cab0..09b9c97 100644
--- a/include/vintf/Sepolicy.h
+++ b/include/vintf/Sepolicy.h
@@ -43,7 +43,9 @@
     inline const std::vector<VersionRange> &sepolicyVersions() const {
         return mSepolicyVersionRanges;
     }
-private:
+
+   private:
+    friend class AssembleVintfImpl;
     friend struct SepolicyConverter;
     KernelSepolicyVersion mKernelSepolicyVersion;
     std::vector<VersionRange> mSepolicyVersionRanges;
diff --git a/test/Android.bp b/test/Android.bp
index 5c9fc43..40d8fbf 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -18,6 +18,7 @@
     host_supported: true,
     gtest: false,
     srcs: [
+        "AssembleVintfTest.cpp",
         "LibVintfTest.cpp",
     ],
 
@@ -27,7 +28,10 @@
         "liblog",
         "libvintf",
     ],
-    static_libs: ["libgtest"],
+    static_libs: [
+        "libgtest",
+        "libassemblevintf",
+    ],
 
     cflags: [
         "-O0",
diff --git a/test/AssembleVintfTest.cpp b/test/AssembleVintfTest.cpp
new file mode 100644
index 0000000..b1d146c
--- /dev/null
+++ b/test/AssembleVintfTest.cpp
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#define LOG_TAG "AssembleVintfTest"
+
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
+#include <vintf/AssembleVintf.h>
+#include <vintf/parse_string.h>
+
+namespace android {
+namespace vintf {
+
+static bool In(const std::string& sub, const std::string& str) {
+    return str.find(sub) != std::string::npos;
+}
+#define EXPECT_IN(sub, str) EXPECT_TRUE(In((sub), (str))) << (str);
+
+class AssembleVintfTest : public ::testing::Test {
+   public:
+    virtual void SetUp() override {
+        mInstance = AssembleVintf::newInstance();
+        auto s = std::make_unique<std::stringstream>();
+        mOutputStream = s.get();
+        mInstance->setOutputStream(std::move(s));
+    }
+    virtual void TearDown() override { mInstance = nullptr; }
+
+    const std::unique_ptr<AssembleVintf>& getInstance() { return mInstance; }
+
+    std::string getOutput() { return mOutputStream->str(); }
+
+    void resetOutput() { mOutputStream->str(""); }
+
+    void setFakeEnvs(const std::map<std::string, std::string>& envs) {
+        for (const auto& pair : envs) {
+            getInstance()->setFakeEnv(pair.first, pair.second);
+        }
+    }
+
+    void addInput(const std::string& name, const std::string& s) {
+        getInstance()->addInputStream(name, std::make_unique<std::stringstream>(s));
+    }
+
+    std::unique_ptr<AssembleVintf> mInstance;
+    // do not own this object.
+    std::stringstream* mOutputStream;
+};
+
+TEST_F(AssembleVintfTest, FrameworkMatrixEmpty) {
+    std::string xmlEmpty = "<compatibility-matrix version=\"1.0\" type=\"framework\" />";
+    std::string kernel318 = "CONFIG_FOO=y\n";
+    std::string kernel318_64 = "CONFIG_BAR=y\n";
+    std::string kernel44 = "# CONFIG_FOO is not set\n";
+    std::string kernel44_64 = "CONFIG_BAR=y\n";
+
+    addInput("compatibility_matrix.empty.xml", xmlEmpty);
+    setFakeEnvs({
+        {"POLICYVERS", "30"},
+        {"BOARD_SEPOLICY_VERS", "10000.0"},
+        {"FRAMEWORK_VBMETA_VERSION", "1.0"},
+    });
+    getInstance()->addKernelConfigInputStream({3, 18}, "android-base.cfg",
+                                              std::make_unique<std::stringstream>(kernel318));
+    getInstance()->addKernelConfigInputStream({3, 18}, "android-base-arm64.cfg",
+                                              std::make_unique<std::stringstream>(kernel318_64));
+    getInstance()->addKernelConfigInputStream({4, 4}, "android-base.cfg",
+                                              std::make_unique<std::stringstream>(kernel44));
+    getInstance()->addKernelConfigInputStream({4, 4}, "android-base-arm64.cfg",
+                                              std::make_unique<std::stringstream>(kernel44_64));
+
+    EXPECT_TRUE(getInstance()->assemble());
+
+    EXPECT_IN(
+        "<compatibility-matrix version=\"1.0\" type=\"framework\">\n"
+        "    <kernel version=\"3.18.0\">\n"
+        "        <config>\n"
+        "            <key>CONFIG_FOO</key>\n"
+        "            <value type=\"tristate\">y</value>\n"
+        "        </config>\n"
+        "    </kernel>\n"
+        "    <kernel version=\"3.18.0\">\n"
+        "        <conditions>\n"
+        "            <config>\n"
+        "                <key>CONFIG_ARM64</key>\n"
+        "                <value type=\"tristate\">y</value>\n"
+        "            </config>\n"
+        "        </conditions>\n"
+        "        <config>\n"
+        "            <key>CONFIG_BAR</key>\n"
+        "            <value type=\"tristate\">y</value>\n"
+        "        </config>\n"
+        "    </kernel>\n"
+        "    <kernel version=\"4.4.0\">\n"
+        "        <config>\n"
+        "            <key>CONFIG_FOO</key>\n"
+        "            <value type=\"tristate\">n</value>\n"
+        "        </config>\n"
+        "    </kernel>\n"
+        "    <kernel version=\"4.4.0\">\n"
+        "        <conditions>\n"
+        "            <config>\n"
+        "                <key>CONFIG_ARM64</key>\n"
+        "                <value type=\"tristate\">y</value>\n"
+        "            </config>\n"
+        "        </conditions>\n"
+        "        <config>\n"
+        "            <key>CONFIG_BAR</key>\n"
+        "            <value type=\"tristate\">y</value>\n"
+        "        </config>\n"
+        "    </kernel>\n"
+        "    <sepolicy>\n"
+        "        <kernel-sepolicy-version>30</kernel-sepolicy-version>\n"
+        "        <sepolicy-version>10000.0</sepolicy-version>\n"
+        "    </sepolicy>\n"
+        "    <avb>\n"
+        "        <vbmeta-version>1.0</vbmeta-version>\n"
+        "    </avb>\n"
+        "</compatibility-matrix>\n",
+        getOutput());
+}
+
+TEST_F(AssembleVintfTest, FrameworkMatrix) {
+    std::string tail =
+        "    <kernel version=\"3.18.0\">\n"
+        "        <config>\n"
+        "            <key>CONFIG_FOO</key>\n"
+        "            <value type=\"tristate\">y</value>\n"
+        "        </config>\n"
+        "    </kernel>\n"
+        "    <sepolicy>\n"
+        "        <kernel-sepolicy-version>30</kernel-sepolicy-version>\n"
+        "        <sepolicy-version>10000.0</sepolicy-version>\n"
+        "    </sepolicy>\n"
+        "    <avb>\n"
+        "        <vbmeta-version>1.0</vbmeta-version>\n"
+        "    </avb>\n"
+        "</compatibility-matrix>\n";
+
+    std::string xmlEmpty = "<compatibility-matrix version=\"1.0\" type=\"framework\">\n" + tail;
+
+    std::string xml1 =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+        "    <hal format=\"hidl\" optional=\"true\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.0</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+
+    std::string xml2 =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"2\">\n"
+        "    <hal format=\"hidl\" optional=\"true\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.0-1</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+
+    std::string xml3 =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"3\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>2.0</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+
+    auto manifest = [](size_t level) {
+        return "<manifest version=\"1.0\" type=\"device\" target-level=\"" + std::to_string(level) +
+               "\">\n" +
+               "    <hal format=\"hidl\">\n"
+               "        <name>android.hardware.foo</name>\n"
+               "        <version>1.1</version>\n"
+               "        <transport>hwbinder</transport>\n"
+               "        <interface>\n"
+               "            <name>IFoo</name>\n"
+               "            <instance>default</instance>\n"
+               "        </interface>\n"
+               "    </hal>\n"
+               "    <hal format=\"hidl\">\n"
+               "        <name>android.hardware.foo</name>\n"
+               "        <version>2.0</version>\n"
+               "        <transport>hwbinder</transport>\n"
+               "        <interface>\n"
+               "            <name>IFoo</name>\n"
+               "            <instance>default</instance>\n"
+               "        </interface>\n"
+               "    </hal>\n"
+               "    <sepolicy>\n"
+               "        <version>10000.0</version>\n"
+               "    </sepolicy>\n"
+               "</manifest>\n";
+    };
+
+    addInput("compatibility_matrix.1.xml", xml1);
+    addInput("compatibility_matrix.2.xml", xml2);
+    addInput("compatibility_matrix.3.xml", xml3);
+    addInput("compatibility_matrix.empty.xml", xmlEmpty);
+    getInstance()->setFakeEnv("PRODUCT_ENFORCE_VINTF_MANIFEST", "true");
+
+    resetOutput();
+    getInstance()->setCheckInputStream(std::make_unique<std::stringstream>(manifest(1)));
+    getInstance()->assemble();
+    EXPECT_IN(
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+        "    <hal format=\"hidl\" optional=\"true\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.0-1</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "    <hal format=\"hidl\" optional=\"true\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>2.0</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n" +
+            tail,
+        getOutput());
+
+    resetOutput();
+    getInstance()->setCheckInputStream(std::make_unique<std::stringstream>(manifest(2)));
+    getInstance()->assemble();
+    EXPECT_IN(
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"2\">\n"
+        "    <hal format=\"hidl\" optional=\"true\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.0-1</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "    <hal format=\"hidl\" optional=\"true\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>2.0</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n" +
+            tail,
+        getOutput());
+
+    resetOutput();
+    getInstance()->setCheckInputStream(std::make_unique<std::stringstream>(manifest(3)));
+    getInstance()->assemble();
+    EXPECT_IN(
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"3\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>2.0</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n" +
+            tail,
+        getOutput());
+}
+
+}  // namespace vintf
+}  // namespace android