diff options
author | 2016-06-01 15:31:50 -0700 | |
---|---|---|
committer | 2016-07-25 16:02:18 -0700 | |
commit | bf0bd0f9ac1ffa0231cff0f6591dede48b3c6d52 (patch) | |
tree | fa0def8a1f754872ce6b912e30b7badc6470e208 | |
parent | ef556916aa75ae5e992b7c92345dc1011899a660 (diff) |
AAPT2: Add support to specify stable IDs
The --stable-ids flag allows the user to specify a file containing
a list of resource name and resource ID pairs in the form of:
package:type/name = 0xPPTTEEEE
This assigns the given resource the specified ID. It helps ensure
that when adding or removing resources, IDs are assigned in a stable
fashion.
If a package, type, or name is not found, no error or warning is
raised.
Change-Id: Ibc2f4e05cc924be255fedd862d835cb5b18d7584
-rw-r--r-- | tools/aapt2/Resource.h | 17 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser.cpp | 26 | ||||
-rw-r--r-- | tools/aapt2/ResourceUtils.cpp | 16 | ||||
-rw-r--r-- | tools/aapt2/ResourceUtils.h | 5 | ||||
-rw-r--r-- | tools/aapt2/compile/IdAssigner.cpp | 207 | ||||
-rw-r--r-- | tools/aapt2/compile/IdAssigner.h | 15 | ||||
-rw-r--r-- | tools/aapt2/compile/IdAssigner_test.cpp | 64 | ||||
-rw-r--r-- | tools/aapt2/link/Link.cpp | 111 |
8 files changed, 380 insertions, 81 deletions
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index 22d75a2fb315..0ba034520563 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -19,9 +19,10 @@ #include "ConfigDescription.h" #include "Source.h" - #include "util/StringPiece.h" +#include <utils/JenkinsHash.h> + #include <iomanip> #include <limits> #include <sstream> @@ -353,4 +354,18 @@ inline bool operator==(const SourcedResourceName& lhs, const SourcedResourceName } // namespace aapt +namespace std { + +template <> struct hash<aapt::ResourceName> { + size_t operator()(const aapt::ResourceName& name) const { + android::hash_t h = 0; + h = android::JenkinsHashMix(h, hash<string>()(name.package)); + h = android::JenkinsHashMix(h, static_cast<uint32_t>(name.type)); + h = android::JenkinsHashMix(h, hash<string>()(name.entry)); + return static_cast<size_t>(h); + } +}; + +} // namespace std + #endif // AAPT_RESOURCE_H diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 45d3db9c3c4a..a144c6ab2be7 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -589,17 +589,14 @@ bool ResourceParser::parsePublic(xml::XmlPullParser* parser, ParsedResource* out outResource->name.type = *parsedType; - if (Maybe<StringPiece> maybeId = xml::findNonEmptyAttribute(parser, "id")) { - android::Res_value val; - std::u16string idStr16 = util::utf8ToUtf16(maybeId.value()); - bool result = android::ResTable::stringToInt(idStr16.data(), idStr16.size(), &val); - ResourceId resourceId(val.data); - if (!result || !resourceId.isValid()) { + if (Maybe<StringPiece> maybeIdStr = xml::findNonEmptyAttribute(parser, "id")) { + Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(maybeIdStr.value()); + if (!maybeId) { mDiag->error(DiagMessage(outResource->source) << "invalid resource ID '" << maybeId.value() << "' in <public>"); return false; } - outResource->id = resourceId; + outResource->id = maybeId.value(); } if (*parsedType == ResourceType::kId) { @@ -626,23 +623,22 @@ bool ResourceParser::parsePublicGroup(xml::XmlPullParser* parser, ParsedResource return false; } - Maybe<StringPiece> maybeId = xml::findNonEmptyAttribute(parser, "first-id"); - if (!maybeId) { + Maybe<StringPiece> maybeIdStr = xml::findNonEmptyAttribute(parser, "first-id"); + if (!maybeIdStr) { mDiag->error(DiagMessage(outResource->source) << "<public-group> must have a 'first-id' attribute"); return false; } - android::Res_value val; - std::u16string idStr16 = util::utf8ToUtf16(maybeId.value()); - bool result = android::ResTable::stringToInt(idStr16.data(), idStr16.size(), &val); - ResourceId nextId(val.data); - if (!result || !nextId.isValid()) { + Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(maybeIdStr.value()); + if (!maybeId) { mDiag->error(DiagMessage(outResource->source) - << "invalid resource ID '" << maybeId.value() << "' in <public-group>"); + << "invalid resource ID '" << maybeIdStr.value() << "' in <public-group>"); return false; } + ResourceId nextId = maybeId.value(); + std::string comment; bool error = false; const size_t depth = parser->getDepth(); diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index 31d6435a6184..7dc88dedc96e 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -436,6 +436,22 @@ bool tryParseBool(const StringPiece& str, bool* outValue) { return false; } +Maybe<ResourceId> tryParseResourceId(const StringPiece& str) { + StringPiece trimmedStr(util::trimWhitespace(str)); + + std::u16string str16 = util::utf8ToUtf16(trimmedStr); + android::Res_value value; + if (android::ResTable::stringToInt(str16.data(), str16.size(), &value)) { + if (value.dataType == android::Res_value::TYPE_INT_HEX) { + ResourceId id(value.data); + if (id.isValid()) { + return id; + } + } + } + return {}; +} + Maybe<int> tryParseSdkVersion(const StringPiece& str) { StringPiece trimmedStr(util::trimWhitespace(str)); diff --git a/tools/aapt2/ResourceUtils.h b/tools/aapt2/ResourceUtils.h index 871ed7ca1e3b..31b8e89850e5 100644 --- a/tools/aapt2/ResourceUtils.h +++ b/tools/aapt2/ResourceUtils.h @@ -85,6 +85,11 @@ bool isAttributeReference(const StringPiece& str); bool tryParseBool(const StringPiece& str, bool* outValue); /** + * Returns an ID if it the string represented a valid ID. + */ +Maybe<ResourceId> tryParseResourceId(const StringPiece& str); + +/** * Parses an SDK version, which can be an integer, or a letter from A-Z. */ Maybe<int> tryParseSdkVersion(const StringPiece& str); diff --git a/tools/aapt2/compile/IdAssigner.cpp b/tools/aapt2/compile/IdAssigner.cpp index 341c9b3d5107..501ae9d08c08 100644 --- a/tools/aapt2/compile/IdAssigner.cpp +++ b/tools/aapt2/compile/IdAssigner.cpp @@ -19,87 +19,180 @@ #include "process/IResourceTableConsumer.h" #include "util/Util.h" -#include <bitset> #include <cassert> -#include <set> +#include <map> namespace aapt { +/** + * Assigns the intended ID to the ResourceTablePackage, ResourceTableType, and ResourceEntry, + * as long as there is no existing ID or the ID is the same. + */ +static bool assignId(IDiagnostics* diag, const ResourceId id, const ResourceName& name, + ResourceTablePackage* pkg, ResourceTableType* type, ResourceEntry* entry) { + if (pkg->id.value() == id.packageId()) { + if (!type->id || type->id.value() == id.typeId()) { + type->id = id.typeId(); + + if (!entry->id || entry->id.value() == id.entryId()) { + entry->id = id.entryId(); + return true; + } + } + } + + const ResourceId existingId(pkg->id.value(), + type->id ? type->id.value() : 0, + entry->id ? entry->id.value() : 0); + diag->error(DiagMessage() << "can't assign ID " << id + << " to resource " << name + << " with conflicting ID " << existingId); + return false; +} + bool IdAssigner::consume(IAaptContext* context, ResourceTable* table) { - std::bitset<256> usedTypeIds; - std::set<uint16_t> usedEntryIds; + std::map<ResourceId, ResourceName> assignedIds; for (auto& package : table->packages) { assert(package->id && "packages must have manually assigned IDs"); - usedTypeIds.reset(); + for (auto& type : package->types) { + for (auto& entry : type->entries) { + const ResourceName name(package->name, type->type, entry->name); - // Type ID 0 is invalid, reserve it. - usedTypeIds.set(0); + if (mAssignedIdMap) { + // Assign the pre-assigned stable ID meant for this resource. + const auto iter = mAssignedIdMap->find(name); + if (iter != mAssignedIdMap->end()) { + const ResourceId assignedId = iter->second; + const bool result = assignId(context->getDiagnostics(), assignedId, name, + package.get(), type.get(), entry.get()); + if (!result) { + return false; + } + } + } - // Collect used type IDs. - for (auto& type : package->types) { - if (type->id) { - usedEntryIds.clear(); - - if (usedTypeIds[type->id.value()]) { - // This ID is already taken! - context->getDiagnostics()->error(DiagMessage() - << "type '" << type->type << "' in " - << "package '" << package->name << "' has " - << "duplicate ID " - << std::hex << (int) type->id.value() - << std::dec); - return false; + if (package->id && type->id && entry->id) { + // If the ID is set for this resource, then reserve it. + ResourceId resourceId(package->id.value(), type->id.value(), entry->id.value()); + auto result = assignedIds.insert({ resourceId, name }); + const ResourceName& existingName = result.first->second; + if (!result.second) { + context->getDiagnostics()->error(DiagMessage() << "resource " << name + << " has same ID " + << resourceId + << " as " << existingName); + return false; + } } + } + } + } - // Mark the type ID as taken. - usedTypeIds.set(type->id.value()); + if (mAssignedIdMap) { + // Reserve all the IDs mentioned in the stable ID map. That way we won't assign + // IDs that were listed in the map if they don't exist in the table. + for (const auto& stableIdEntry : *mAssignedIdMap) { + const ResourceName& preAssignedName = stableIdEntry.first; + const ResourceId& preAssignedId = stableIdEntry.second; + auto result = assignedIds.insert({ preAssignedId, preAssignedName }); + const ResourceName& existingName = result.first->second; + if (!result.second && existingName != preAssignedName) { + context->getDiagnostics()->error(DiagMessage() << "stable ID " << preAssignedId + << " for resource " << preAssignedName + << " is already taken by resource " + << existingName); + return false; } + } + } - // Collect used entry IDs. - for (auto& entry : type->entries) { - if (entry->id) { - // Mark entry ID as taken. - if (!usedEntryIds.insert(entry->id.value()).second) { - // This ID existed before! - ResourceNameRef nameRef(package->name, type->type, entry->name); - context->getDiagnostics()->error(DiagMessage() - << "resource '" << nameRef << "' " - << "has duplicate entry ID " - << std::hex << (int) entry->id.value() - << std::dec); - return false; + // Assign any resources without IDs the next available ID. Gaps will be filled if possible, + // unless those IDs have been reserved. + + const auto assignedIdsIterEnd = assignedIds.end(); + for (auto& package : table->packages) { + assert(package->id && "packages must have manually assigned IDs"); + + // Build a half filled ResourceId object, which will be used to find the closest matching + // reserved ID in the assignedId map. From that point the next available type ID can be + // found. + ResourceId resourceId(package->id.value(), 0, 0); + uint8_t nextExpectedTypeId = 1; + + // Find the closest matching ResourceId that is <= the one with only the package set. + auto nextTypeIter = assignedIds.lower_bound(resourceId); + for (auto& type : package->types) { + if (!type->id) { + // We need to assign a type ID. Iterate over the reserved IDs until we find + // some type ID that is a distance of 2 greater than the last one we've seen. + // That means there is an available type ID between these reserved IDs. + while (nextTypeIter != assignedIdsIterEnd) { + if (nextTypeIter->first.packageId() != package->id.value()) { + break; } + + const uint8_t typeId = nextTypeIter->first.typeId(); + if (typeId > nextExpectedTypeId) { + // There is a gap in the type IDs, so use the missing one. + type->id = nextExpectedTypeId++; + break; + } + + // Set our expectation to be the next type ID after the reserved one we + // just saw. + nextExpectedTypeId = typeId + 1; + + // Move to the next reserved ID. + ++nextTypeIter; + } + + if (!type->id) { + // We must have hit the end of the reserved IDs and not found a gap. + // That means the next ID is available. + type->id = nextExpectedTypeId++; } } - // Assign unused entry IDs. - const auto endUsedEntryIter = usedEntryIds.end(); - auto nextUsedEntryIter = usedEntryIds.begin(); - uint16_t nextId = 0; + resourceId = ResourceId(package->id.value(), type->id.value(), 0); + uint16_t nextExpectedEntryId = 0; + + // Find the closest matching ResourceId that is <= the one with only the package + // and type set. + auto nextEntryIter = assignedIds.lower_bound(resourceId); for (auto& entry : type->entries) { if (!entry->id) { - // Assign the next available entryID. - while (nextUsedEntryIter != endUsedEntryIter && - nextId == *nextUsedEntryIter) { - nextId++; - ++nextUsedEntryIter; + // We need to assign an entry ID. Iterate over the reserved IDs until we find + // some entry ID that is a distance of 2 greater than the last one we've seen. + // That means there is an available entry ID between these reserved IDs. + while (nextEntryIter != assignedIdsIterEnd) { + if (nextEntryIter->first.packageId() != package->id.value() || + nextEntryIter->first.typeId() != type->id.value()) { + break; + } + + const uint16_t entryId = nextEntryIter->first.entryId(); + if (entryId > nextExpectedEntryId) { + // There is a gap in the entry IDs, so use the missing one. + entry->id = nextExpectedEntryId++; + break; + } + + // Set our expectation to be the next type ID after the reserved one we + // just saw. + nextExpectedEntryId = entryId + 1; + + // Move to the next reserved entry ID. + ++nextEntryIter; } - entry->id = nextId++; - } - } - } - // Assign unused type IDs. - size_t nextTypeId = 0; - for (auto& type : package->types) { - if (!type->id) { - while (nextTypeId < usedTypeIds.size() && usedTypeIds[nextTypeId]) { - nextTypeId++; + if (!entry->id) { + // We must have hit the end of the reserved IDs and not found a gap. + // That means the next ID is available. + entry->id = nextExpectedEntryId++; + } } - type->id = static_cast<uint8_t>(nextTypeId); - nextTypeId++; } } } diff --git a/tools/aapt2/compile/IdAssigner.h b/tools/aapt2/compile/IdAssigner.h index 514df3ad3861..06cd5e3f6473 100644 --- a/tools/aapt2/compile/IdAssigner.h +++ b/tools/aapt2/compile/IdAssigner.h @@ -17,16 +17,29 @@ #ifndef AAPT_COMPILE_IDASSIGNER_H #define AAPT_COMPILE_IDASSIGNER_H +#include "Resource.h" #include "process/IResourceTableConsumer.h" +#include <android-base/macros.h> +#include <unordered_map> + namespace aapt { /** * Assigns IDs to each resource in the table, respecting existing IDs and filling in gaps * in between fixed ID assignments. */ -struct IdAssigner : public IResourceTableConsumer { +class IdAssigner : public IResourceTableConsumer { +public: + IdAssigner() = default; + explicit IdAssigner(const std::unordered_map<ResourceName, ResourceId>* map) : + mAssignedIdMap(map) { + } + bool consume(IAaptContext* context, ResourceTable* table) override; + +private: + const std::unordered_map<ResourceName, ResourceId>* mAssignedIdMap = nullptr; }; } // namespace aapt diff --git a/tools/aapt2/compile/IdAssigner_test.cpp b/tools/aapt2/compile/IdAssigner_test.cpp index 802e99a4640b..4f43c4069edc 100644 --- a/tools/aapt2/compile/IdAssigner_test.cpp +++ b/tools/aapt2/compile/IdAssigner_test.cpp @@ -15,11 +15,7 @@ */ #include "compile/IdAssigner.h" - -#include "test/Context.h" -#include "test/Builders.h" - -#include <gtest/gtest.h> +#include "test/Test.h" namespace aapt { @@ -42,9 +38,14 @@ TEST(IdAssignerTest, AssignIds) { TEST(IdAssignerTest, AssignIdsWithReservedIds) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple("@android:id/foo", ResourceId(0x01010000)) + .addSimple("@android:dimen/two") + .addSimple("@android:integer/three") + .addSimple("@android:string/five") + .addSimple("@android:attr/fun", ResourceId(0x01040000)) .addSimple("@android:attr/foo", ResourceId(0x01040006)) .addSimple("@android:attr/bar") - .addSimple("@android:id/foo") + .addSimple("@android:attr/baz") .addSimple("@app:id/biz") .setPackageId("android", 0x01) .setPackageId("app", 0x7f) @@ -55,6 +56,34 @@ TEST(IdAssignerTest, AssignIdsWithReservedIds) { ASSERT_TRUE(assigner.consume(context.get(), table.get())); ASSERT_TRUE(verifyIds(table.get())); + + Maybe<ResourceTable::SearchResult> maybeResult; + + // Expect to fill in the gaps between 0x0101XXXX and 0x0104XXXX. + + maybeResult = table->findResource(test::parseNameOrDie("@android:dimen/two")); + AAPT_ASSERT_TRUE(maybeResult); + EXPECT_EQ(make_value<uint8_t>(2), maybeResult.value().type->id); + + maybeResult = table->findResource(test::parseNameOrDie("@android:integer/three")); + AAPT_ASSERT_TRUE(maybeResult); + EXPECT_EQ(make_value<uint8_t>(3), maybeResult.value().type->id); + + // Expect to bypass the reserved 0x0104XXXX IDs and use the next 0x0105XXXX IDs. + + maybeResult = table->findResource(test::parseNameOrDie("@android:string/five")); + AAPT_ASSERT_TRUE(maybeResult); + EXPECT_EQ(make_value<uint8_t>(5), maybeResult.value().type->id); + + // Expect to fill in the gaps between 0x01040000 and 0x01040006. + + maybeResult = table->findResource(test::parseNameOrDie("@android:attr/bar")); + AAPT_ASSERT_TRUE(maybeResult); + EXPECT_EQ(make_value<uint16_t>(1), maybeResult.value().entry->id); + + maybeResult = table->findResource(test::parseNameOrDie("@android:attr/baz")); + AAPT_ASSERT_TRUE(maybeResult); + EXPECT_EQ(make_value<uint16_t>(2), maybeResult.value().entry->id); } TEST(IdAssignerTest, FailWhenNonUniqueIdsAssigned) { @@ -71,6 +100,29 @@ TEST(IdAssignerTest, FailWhenNonUniqueIdsAssigned) { ASSERT_FALSE(assigner.consume(context.get(), table.get())); } +TEST(IdAssignerTest, AssignIdsWithIdMap) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple("@android:attr/foo") + .addSimple("@android:attr/bar") + .setPackageId("android", 0x01) + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + std::unordered_map<ResourceName, ResourceId> idMap = { + { test::parseNameOrDie("@android:attr/foo"), ResourceId(0x01010002) } }; + IdAssigner assigner(&idMap); + ASSERT_TRUE(assigner.consume(context.get(), table.get())); + ASSERT_TRUE(verifyIds(table.get())); + Maybe<ResourceTable::SearchResult> result = table->findResource( + test::parseNameOrDie("@android:attr/foo")); + AAPT_ASSERT_TRUE(result); + + const ResourceTable::SearchResult& searchResult = result.value(); + EXPECT_EQ(make_value<uint8_t>(0x01), searchResult.package->id); + EXPECT_EQ(make_value<uint8_t>(0x01), searchResult.type->id); + EXPECT_EQ(make_value<uint16_t>(0x0002), searchResult.entry->id); +} + ::testing::AssertionResult verifyIds(ResourceTable* table) { std::set<uint8_t> packageIds; for (auto& package : table->packages) { diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp index 8093e6a035d9..ded661e6d049 100644 --- a/tools/aapt2/link/Link.cpp +++ b/tools/aapt2/link/Link.cpp @@ -44,10 +44,12 @@ #include "util/StringPiece.h" #include "xml/XmlDom.h" +#include <android-base/file.h> #include <google/protobuf/io/coded_stream.h> #include <fstream> #include <sys/stat.h> +#include <unordered_map> #include <vector> namespace aapt { @@ -76,6 +78,8 @@ struct LinkOptions { ManifestFixerOptions manifestFixerOptions; std::unordered_set<std::string> products; TableSplitterOptions tableSplitterOptions; + std::unordered_map<ResourceName, ResourceId> stableIdMap; + Maybe<std::string> resourceIdMapPath; }; class LinkContext : public IAaptContext { @@ -517,6 +521,77 @@ bool ResourceFileFlattener::flatten(ResourceTable* table, IArchiveWriter* archiv return !error; } +static bool writeStableIdMapToPath(IDiagnostics* diag, + const std::unordered_map<ResourceName, ResourceId>& idMap, + const std::string idMapPath) { + std::ofstream fout(idMapPath, std::ofstream::binary); + if (!fout) { + diag->error(DiagMessage(idMapPath) << strerror(errno)); + return false; + } + + for (const auto& entry : idMap) { + const ResourceName& name = entry.first; + const ResourceId& id = entry.second; + fout << name << " = " << id << "\n"; + } + + if (!fout) { + diag->error(DiagMessage(idMapPath) << "failed writing to file: " << strerror(errno)); + return false; + } + + return true; +} + +static bool loadStableIdMap(IDiagnostics* diag, const std::string& path, + std::unordered_map<ResourceName, ResourceId>* outIdMap) { + std::string content; + if (!android::base::ReadFileToString(path, &content)) { + diag->error(DiagMessage(path) << "failed reading stable ID file"); + return false; + } + + outIdMap->clear(); + size_t lineNo = 0; + for (StringPiece line : util::tokenize(content, '\n')) { + lineNo++; + line = util::trimWhitespace(line); + if (line.empty()) { + continue; + } + + auto iter = std::find(line.begin(), line.end(), '='); + if (iter == line.end()) { + diag->error(DiagMessage(Source(path, lineNo)) << "missing '='"); + return false; + } + + ResourceNameRef name; + StringPiece resNameStr = util::trimWhitespace( + line.substr(0, std::distance(line.begin(), iter))); + if (!ResourceUtils::parseResourceName(resNameStr, &name)) { + diag->error(DiagMessage(Source(path, lineNo)) + << "invalid resource name '" << resNameStr << "'"); + return false; + } + + const size_t resIdStartIdx = std::distance(line.begin(), iter) + 1; + const size_t resIdStrLen = line.size() - resIdStartIdx; + StringPiece resIdStr = util::trimWhitespace(line.substr(resIdStartIdx, resIdStrLen)); + + Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(resIdStr); + if (!maybeId) { + diag->error(DiagMessage(Source(path, lineNo)) << "invalid resource ID '" + << resIdStr << "'"); + return false; + } + + (*outIdMap)[name.toResourceName()] = maybeId.value(); + } + return true; +} + class LinkCommand { public: LinkCommand(LinkContext* context, const LinkOptions& options) : @@ -1176,11 +1251,32 @@ public: if (!mOptions.staticLib) { // Assign IDs if we are building a regular app. - IdAssigner idAssigner; + IdAssigner idAssigner(&mOptions.stableIdMap); if (!idAssigner.consume(mContext, &mFinalTable)) { mContext->getDiagnostics()->error(DiagMessage() << "failed assigning IDs"); return 1; } + + // Now grab each ID and emit it as a file. + if (mOptions.resourceIdMapPath) { + for (auto& package : mFinalTable.packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + ResourceName name(package->name, type->type, entry->name); + // The IDs are guaranteed to exist. + mOptions.stableIdMap[std::move(name)] = ResourceId(package->id.value(), + type->id.value(), + entry->id.value()); + } + } + } + + if (!writeStableIdMapToPath(mContext->getDiagnostics(), + mOptions.stableIdMap, + mOptions.resourceIdMapPath.value())) { + return 1; + } + } } else { // Static libs are merged with other apps, and ID collisions are bad, so verify that // no IDs have been set. @@ -1437,6 +1533,7 @@ int link(const std::vector<StringPiece>& args) { bool legacyXFlag = false; bool requireLocalization = false; bool verbose = false; + Maybe<std::string> stableIdFilePath; Flags flags = Flags() .requiredFlag("-o", "Output path", &options.outputPath) .requiredFlag("--manifest", "Path to the Android manifest to build", @@ -1493,6 +1590,11 @@ int link(const std::vector<StringPiece>& args) { .optionalSwitch("--non-final-ids", "Generates R.java without the final modifier.\n" "This is implied when --static-lib is specified.", &options.generateNonFinalIds) + .optionalFlag("--stable-ids", "File containing a list of name to ID mapping.", + &stableIdFilePath) + .optionalFlag("--emit-ids", "Emit a file at the given path with a list of name to ID\n" + "mappings, suitable for use with --stable-ids.", + &options.resourceIdMapPath) .optionalFlag("--private-symbols", "Package name to use when generating R.java for " "private symbols.\n" "If not specified, public and private symbols will use the application's " @@ -1619,6 +1721,13 @@ int link(const std::vector<StringPiece>& args) { options.tableSplitterOptions.preferredDensity = preferredDensityConfig.density; } + if (!options.staticLib && stableIdFilePath) { + if (!loadStableIdMap(context.getDiagnostics(), stableIdFilePath.value(), + &options.stableIdMap)) { + return 1; + } + } + // Turn off auto versioning for static-libs. if (options.staticLib) { options.noAutoVersion = true; |