Reland "[TextAPI] Implement TBDv5 Writer"
authorCyndy Ishida <cyndy_ishida@apple.com>
Wed, 22 Feb 2023 18:00:07 +0000 (10:00 -0800)
committerCyndy Ishida <cyndy_ishida@apple.com>
Wed, 22 Feb 2023 20:01:37 +0000 (12:01 -0800)
Create writer for new JSON format.
The new JSON format allows practically all attributes to be defined per
target in a universal library however the internal representation only
allows one for the time being. For now the write will always write those
attributes as default available for all targets (install name,
compatability & current version, swift abi, flags e.g. flatnamepace &
app exenstion safety)

rdar://102076911

Reviewed By: ributzka

Differential Revision: https://reviews.llvm.org/D144339

llvm/include/llvm/TextAPI/InterfaceFile.h
llvm/include/llvm/TextAPI/PackedVersion.h
llvm/include/llvm/TextAPI/TextAPIWriter.h
llvm/lib/TextAPI/PackedVersion.cpp
llvm/lib/TextAPI/TextStub.cpp
llvm/lib/TextAPI/TextStubCommon.h
llvm/lib/TextAPI/TextStubV5.cpp
llvm/unittests/TextAPI/TextStubV5Tests.cpp

index c32917d..2ee22ee 100644 (file)
@@ -396,7 +396,16 @@ public:
 
   const_filtered_symbol_range exports() const {
     std::function<bool(const Symbol *)> fn = [](const Symbol *Symbol) {
-      return !Symbol->isUndefined();
+      return !Symbol->isUndefined() && !Symbol->isReexported();
+    };
+    return make_filter_range(
+        make_range<const_symbol_iterator>({Symbols.begin()}, {Symbols.end()}),
+        fn);
+  }
+
+  const_filtered_symbol_range reexports() const {
+    std::function<bool(const Symbol *)> fn = [](const Symbol *Symbol) {
+      return Symbol->isReexported();
     };
     return make_filter_range(
         make_range<const_symbol_iterator>({Symbols.begin()}, {Symbols.end()}),
index 24bec2e..eafa508 100644 (file)
@@ -14,6 +14,7 @@
 #define LLVM_TEXTAPI_PACKEDVERSION_H
 
 #include <cstdint>
+#include <string>
 #include <utility>
 
 namespace llvm {
@@ -53,6 +54,8 @@ public:
 
   uint32_t rawValue() const { return Version; }
 
+  operator std::string() const;
+
   void print(raw_ostream &OS) const;
 };
 
index f9857a8..9bdaaf5 100644 (file)
@@ -22,7 +22,8 @@ class TextAPIWriter {
 public:
   TextAPIWriter() = delete;
 
-  static Error writeToStream(raw_ostream &os, const InterfaceFile &);
+  static Error writeToStream(raw_ostream &OS, const InterfaceFile &File,
+                             bool Compact = false);
 };
 
 } // end namespace MachO.
index 67fb30a..22960c3 100644 (file)
@@ -100,6 +100,13 @@ std::pair<bool, bool> PackedVersion::parse64(StringRef Str) {
   return std::make_pair(true, Truncated);
 }
 
+PackedVersion::operator std::string() const {
+  SmallString<32> Str;
+  raw_svector_ostream OS(Str);
+  print(OS);
+  return std::string(Str);
+}
+
 void PackedVersion::print(raw_ostream &OS) const {
   OS << format("%d", getMajor());
   if (getMinor() || getSubminor())
index 73cb614..c51edc1 100644 (file)
@@ -1159,10 +1159,17 @@ TextAPIReader::get(MemoryBufferRef InputBuffer) {
   return std::move(File);
 }
 
-Error TextAPIWriter::writeToStream(raw_ostream &OS, const InterfaceFile &File) {
+Error TextAPIWriter::writeToStream(raw_ostream &OS, const InterfaceFile &File,
+                                   bool Compact) {
   TextAPIContext Ctx;
   Ctx.Path = std::string(File.getPath());
   Ctx.FileKind = File.getFileType();
+
+  // Write out in JSON format.
+  if (Ctx.FileKind >= FileType::TBD_V5) {
+    return serializeInterfaceFileToJSON(OS, File, Compact);
+  }
+
   llvm::yaml::Output YAMLOut(OS, &Ctx, /*WrapColumn=*/80);
 
   std::vector<const InterfaceFile *> Files;
index 51b4231..9558bd9 100644 (file)
@@ -45,6 +45,9 @@ class PackedVersion;
 
 Expected<std::unique_ptr<InterfaceFile>>
 getInterfaceFileFromJSON(StringRef JSON);
+
+Error serializeInterfaceFileToJSON(raw_ostream &OS, const InterfaceFile &File,
+                                   bool Compact);
 } // namespace MachO
 
 namespace yaml {
index f0ef10d..43519c0 100644 (file)
@@ -161,6 +161,10 @@ static llvm::SmallString<128> getParseErrorMsg(TBDKey Key) {
   return {"invalid ", Keys[Key], " section"};
 }
 
+static llvm::SmallString<128> getSerializeErrorMsg(TBDKey Key) {
+  return {"missing ", Keys[Key], " information"};
+}
+
 class JSONStubError : public llvm::ErrorInfo<llvm::json::ParseError> {
 public:
   JSONStubError(Twine ErrMsg) : Message(ErrMsg.str()) {}
@@ -716,3 +720,294 @@ MachO::getInterfaceFileFromJSON(StringRef JSON) {
   }
   return std::move(IF);
 }
+
+namespace {
+
+template <typename ContainerT = Array>
+bool insertNonEmptyValues(Object &Obj, TBDKey Key, ContainerT &&Contents) {
+  if (Contents.empty())
+    return false;
+  Obj[Keys[Key]] = std::move(Contents);
+  return true;
+}
+
+std::string getFormattedStr(const MachO::Target &Targ) {
+  std::string PlatformStr = Targ.Platform == PLATFORM_MACCATALYST
+                                ? "maccatalyst"
+                                : getOSAndEnvironmentName(Targ.Platform);
+  return (getArchitectureName(Targ.Arch) + "-" + PlatformStr).str();
+}
+
+template <typename AggregateT>
+std::vector<std::string> serializeTargets(const AggregateT Targets,
+                                          const TargetList &ActiveTargets) {
+  std::vector<std::string> TargetsStr;
+  if (Targets.size() == ActiveTargets.size())
+    return TargetsStr;
+
+  llvm::for_each(Targets, [&TargetsStr](const MachO::Target &Target) {
+    TargetsStr.emplace_back(getFormattedStr(Target));
+  });
+  return TargetsStr;
+}
+
+Array serializeTargetInfo(const TargetList &ActiveTargets) {
+  Array Targets;
+  for (const auto Targ : ActiveTargets) {
+    Object TargetInfo;
+    TargetInfo[Keys[TBDKey::Deployment]] = Targ.MinDeployment.getAsString();
+    TargetInfo[Keys[TBDKey::Target]] = getFormattedStr(Targ);
+    Targets.emplace_back(std::move(TargetInfo));
+  }
+  return Targets;
+}
+
+template <typename ValueT, typename EntryT = ValueT>
+Array serializeScalar(TBDKey Key, ValueT Value, ValueT Default = ValueT()) {
+  if (Value == Default)
+    return {};
+  Array Container;
+  Object ScalarObj({Object::KV({Keys[Key], EntryT(Value)})});
+
+  Container.emplace_back(std::move(ScalarObj));
+  return Container;
+}
+
+using TargetsToValuesMap =
+    std::map<std::vector<std::string>, std::vector<std::string>>;
+
+template <typename AggregateT = TargetsToValuesMap>
+Array serializeAttrToTargets(AggregateT &Entries, TBDKey Key) {
+  Array Container;
+  for (const auto &[Targets, Values] : Entries) {
+    Object Obj;
+    insertNonEmptyValues(Obj, TBDKey::Targets, std::move(Targets));
+    Obj[Keys[Key]] = Values;
+    Container.emplace_back(std::move(Obj));
+  }
+  return Container;
+}
+
+template <typename ValueT = std::string,
+          typename AggregateT = std::vector<std::pair<MachO::Target, ValueT>>>
+Array serializeField(TBDKey Key, const AggregateT &Values,
+                     const TargetList &ActiveTargets, bool IsArray = true) {
+  std::map<ValueT, std::set<MachO::Target>> Entries;
+  for (const auto &[Target, Val] : Values)
+    Entries[Val].insert(Target);
+
+  if (!IsArray) {
+    std::map<std::vector<std::string>, std::string> FinalEntries;
+    for (const auto &[Val, Targets] : Entries)
+      FinalEntries[serializeTargets(Targets, ActiveTargets)] = Val;
+    return serializeAttrToTargets(FinalEntries, Key);
+  }
+
+  TargetsToValuesMap FinalEntries;
+  for (const auto &[Val, Targets] : Entries)
+    FinalEntries[serializeTargets(Targets, ActiveTargets)].emplace_back(Val);
+  return serializeAttrToTargets(FinalEntries, Key);
+}
+
+Array serializeField(TBDKey Key, const std::vector<InterfaceFileRef> &Values,
+                     const TargetList &ActiveTargets) {
+  TargetsToValuesMap FinalEntries;
+  for (const auto &Ref : Values) {
+    TargetList Targets{Ref.targets().begin(), Ref.targets().end()};
+    FinalEntries[serializeTargets(Targets, ActiveTargets)].emplace_back(
+        Ref.getInstallName());
+  }
+  return serializeAttrToTargets(FinalEntries, Key);
+}
+
+struct SymbolFields {
+  struct SymbolTypes {
+    std::vector<StringRef> Weaks;
+    std::vector<StringRef> Globals;
+    std::vector<StringRef> TLV;
+    std::vector<StringRef> ObjCClasses;
+    std::vector<StringRef> IVars;
+    std::vector<StringRef> EHTypes;
+
+    bool empty() const {
+      return Weaks.empty() && Globals.empty() && TLV.empty() &&
+             ObjCClasses.empty() && IVars.empty() && EHTypes.empty();
+    }
+  };
+  SymbolTypes Data;
+  SymbolTypes Text;
+};
+
+Array serializeSymbols(InterfaceFile::const_filtered_symbol_range Symbols,
+                       const TargetList &ActiveTargets) {
+  auto AssignForSymbolType = [](SymbolFields::SymbolTypes &Assignment,
+                                const Symbol *Sym) {
+    switch (Sym->getKind()) {
+    case SymbolKind::ObjectiveCClass:
+      Assignment.ObjCClasses.emplace_back(Sym->getName());
+      return;
+    case SymbolKind::ObjectiveCClassEHType:
+      Assignment.EHTypes.emplace_back(Sym->getName());
+      return;
+    case SymbolKind::ObjectiveCInstanceVariable:
+      Assignment.IVars.emplace_back(Sym->getName());
+      return;
+    case SymbolKind::GlobalSymbol: {
+      if (Sym->isWeakReferenced() || Sym->isWeakDefined())
+        Assignment.Weaks.emplace_back(Sym->getName());
+      else if (Sym->isThreadLocalValue())
+        Assignment.TLV.emplace_back(Sym->getName());
+      else
+        Assignment.Globals.emplace_back(Sym->getName());
+      return;
+    }
+    }
+  };
+
+  std::map<std::vector<std::string>, SymbolFields> Entries;
+  for (const auto *Sym : Symbols) {
+    std::set<MachO::Target> Targets{Sym->targets().begin(),
+                                    Sym->targets().end()};
+    auto JSONTargets = serializeTargets(Targets, ActiveTargets);
+    if (Sym->isData())
+      AssignForSymbolType(Entries[std::move(JSONTargets)].Data, Sym);
+    else if (Sym->isText())
+      AssignForSymbolType(Entries[std::move(JSONTargets)].Text, Sym);
+    else
+      llvm_unreachable("unexpected symbol type");
+  }
+
+  auto InsertSymbolsToJSON = [](Object &SymSection, TBDKey SegmentKey,
+                                SymbolFields::SymbolTypes &SymField) {
+    if (SymField.empty())
+      return;
+    Object Segment;
+    insertNonEmptyValues(Segment, TBDKey::Globals, std::move(SymField.Globals));
+    insertNonEmptyValues(Segment, TBDKey::ThreadLocal, std::move(SymField.TLV));
+    insertNonEmptyValues(Segment, TBDKey::Weak, std::move(SymField.Weaks));
+    insertNonEmptyValues(Segment, TBDKey::ObjCClass,
+                         std::move(SymField.ObjCClasses));
+    insertNonEmptyValues(Segment, TBDKey::ObjCEHType,
+                         std::move(SymField.EHTypes));
+    insertNonEmptyValues(Segment, TBDKey::ObjCIvar, std::move(SymField.IVars));
+    insertNonEmptyValues(SymSection, SegmentKey, std::move(Segment));
+  };
+
+  Array SymbolSection;
+  for (auto &[Targets, Fields] : Entries) {
+    Object AllSyms;
+    insertNonEmptyValues(AllSyms, TBDKey::Targets, std::move(Targets));
+    InsertSymbolsToJSON(AllSyms, TBDKey::Data, Fields.Data);
+    InsertSymbolsToJSON(AllSyms, TBDKey::Text, Fields.Text);
+    SymbolSection.emplace_back(std::move(AllSyms));
+  }
+
+  return SymbolSection;
+}
+
+Array serializeFlags(const InterfaceFile *File) {
+  // TODO: Give all Targets the same flags for now.
+  Array Flags;
+  if (!File->isTwoLevelNamespace())
+    Flags.emplace_back("flat_namespace");
+  if (!File->isApplicationExtensionSafe())
+    Flags.emplace_back("not_app_extension_safe");
+  return serializeScalar(TBDKey::Attributes, std::move(Flags));
+}
+
+Expected<Object> serializeIF(const InterfaceFile *File) {
+  Object Library;
+
+  // Handle required keys.
+  TargetList ActiveTargets{File->targets().begin(), File->targets().end()};
+  if (!insertNonEmptyValues(Library, TBDKey::TargetInfo,
+                            serializeTargetInfo(ActiveTargets)))
+    return make_error<JSONStubError>(getSerializeErrorMsg(TBDKey::TargetInfo));
+
+  Array Name = serializeScalar<StringRef>(TBDKey::Name, File->getInstallName());
+  if (!insertNonEmptyValues(Library, TBDKey::InstallName, std::move(Name)))
+    return make_error<JSONStubError>(getSerializeErrorMsg(TBDKey::InstallName));
+
+  // Handle optional keys.
+  Array Flags = serializeFlags(File);
+  insertNonEmptyValues(Library, TBDKey::Flags, std::move(Flags));
+
+  Array CurrentV = serializeScalar<PackedVersion, std::string>(
+      TBDKey::Version, File->getCurrentVersion(), PackedVersion(1, 0, 0));
+  insertNonEmptyValues(Library, TBDKey::CurrentVersion, std::move(CurrentV));
+
+  Array CompatV = serializeScalar<PackedVersion, std::string>(
+      TBDKey::Version, File->getCompatibilityVersion(), PackedVersion(1, 0, 0));
+  insertNonEmptyValues(Library, TBDKey::CompatibilityVersion,
+                       std::move(CompatV));
+
+  Array SwiftABI = serializeScalar<uint8_t, int64_t>(
+      TBDKey::ABI, File->getSwiftABIVersion(), 0u);
+  insertNonEmptyValues(Library, TBDKey::SwiftABI, std::move(SwiftABI));
+
+  Array RPaths = serializeField(TBDKey::Paths, File->rpaths(), ActiveTargets);
+  insertNonEmptyValues(Library, TBDKey::RPath, std::move(RPaths));
+
+  Array Umbrellas = serializeField(TBDKey::Umbrella, File->umbrellas(),
+                                   ActiveTargets, /*IsArray=*/false);
+  insertNonEmptyValues(Library, TBDKey::ParentUmbrella, std::move(Umbrellas));
+
+  Array Clients =
+      serializeField(TBDKey::Clients, File->allowableClients(), ActiveTargets);
+  insertNonEmptyValues(Library, TBDKey::AllowableClients, std::move(Clients));
+
+  Array ReexportLibs =
+      serializeField(TBDKey::Names, File->reexportedLibraries(), ActiveTargets);
+  insertNonEmptyValues(Library, TBDKey::ReexportLibs, std::move(ReexportLibs));
+
+  // Handle symbols.
+  Array Exports = serializeSymbols(File->exports(), ActiveTargets);
+  insertNonEmptyValues(Library, TBDKey::Exports, std::move(Exports));
+
+  Array Reexports = serializeSymbols(File->reexports(), ActiveTargets);
+  insertNonEmptyValues(Library, TBDKey::Reexports, std::move(Reexports));
+
+  if (!File->isTwoLevelNamespace()) {
+    Array Undefineds = serializeSymbols(File->undefineds(), ActiveTargets);
+    insertNonEmptyValues(Library, TBDKey::Undefineds, std::move(Undefineds));
+  }
+
+  return std::move(Library);
+}
+
+Expected<Object> getJSON(const InterfaceFile *File) {
+  assert(File->getFileType() == FileType::TBD_V5 &&
+         "unexpected json file format version");
+  Object Root;
+
+  auto MainLibOrErr = serializeIF(File);
+  if (!MainLibOrErr)
+    return MainLibOrErr;
+  Root[Keys[TBDKey::MainLibrary]] = std::move(*MainLibOrErr);
+  Array Documents;
+  for (const auto &Doc : File->documents()) {
+    auto LibOrErr = serializeIF(Doc.get());
+    if (!LibOrErr)
+      return LibOrErr;
+    Documents.emplace_back(std::move(*LibOrErr));
+  }
+
+  Root[Keys[TBDKey::TBDVersion]] = 5;
+  insertNonEmptyValues(Root, TBDKey::Documents, std::move(Documents));
+  return std::move(Root);
+}
+
+} // namespace
+
+Error MachO::serializeInterfaceFileToJSON(raw_ostream &OS,
+                                          const InterfaceFile &File,
+                                          bool Compact) {
+  auto TextFile = getJSON(&File);
+  if (!TextFile)
+    return TextFile.takeError();
+  if (Compact)
+    OS << formatv("{0}", Value(std::move(*TextFile))) << "\n";
+  else
+    OS << formatv("{0:2}", Value(std::move(*TextFile))) << "\n";
+  return Error::success();
+}
index c9668bb..2536a5a 100644 (file)
@@ -520,4 +520,524 @@ TEST(TBDv5, ReadMultipleDocuments) {
       std::equal(Exports.begin(), Exports.end(), std::begin(ExpectedExports)));
 }
 
+TEST(TBDv5, WriteFile) {
+  static const char TBDv5File[] = R"({
+"tapi_tbd_version": 5,
+"main_library": {
+  "target_info": [
+    {
+      "target": "x86_64-macos",
+      "min_deployment": "10.14"
+    },
+    {
+      "target": "arm64-macos",
+      "min_deployment": "10.14"
+    },
+    {
+      "target": "arm64-maccatalyst",
+      "min_deployment": "12.1"
+    }
+  ],
+  "install_names": [
+    {
+        "name": "@rpath/S/L/F/Foo.framework/Foo"
+    }
+  ],
+  "current_versions": [
+    {
+        "version": "1.2"
+    }
+  ],
+  "compatibility_versions": [
+    { "version": "1.1" }
+  ],
+  "flags": [
+    {
+      "attributes": [
+            "flat_namespace"
+        ]
+    }
+  ],
+  "rpaths": [
+    {
+      "targets": [
+          "x86_64-macos"
+      ],
+      "paths": [
+          "@executable_path/.../Frameworks"
+      ]
+    }
+  ],
+  "parent_umbrellas": [
+    {
+      "umbrella": "System"
+    }
+  ],
+  "allowable_clients": [
+    {
+        "clients": [
+            "ClientA",
+            "ClientB"
+        ]
+    }
+  ],
+  "reexported_libraries": [
+    {
+        "names": [
+            "/u/l/l/libfoo.dylib",
+            "/u/l/l/libbar.dylib"
+        ]
+    }
+  ],
+  "exported_symbols": [
+    {
+        "targets": [
+            "x86_64-macos",
+            "arm64-macos"
+        ],
+        "data": {
+            "global": [
+                "_global"
+            ],
+            "objc_class": [
+                "ClassA"
+            ],
+            "weak": [],
+            "thread_local": []
+        },
+        "text": {
+            "global": [
+                "_func"
+            ],
+            "weak": [],
+            "thread_local": []
+        }
+    },
+    {
+      "targets": [
+          "x86_64-macos"
+      ],
+      "data": {
+          "global": [
+              "_globalVar"
+          ],
+          "objc_class": [
+              "ClassData"
+          ],
+          "objc_eh_type": [
+              "ClassA",
+              "ClassB"
+          ],
+          "objc_ivar": [
+              "ClassA.ivar1",
+              "ClassA.ivar2",
+              "ClassC.ivar1"
+          ]
+      },
+      "text": {
+          "global": [
+              "_funcFoo"
+          ]
+      }
+    }
+  ],
+  "reexported_symbols": [
+    {
+        "data": {
+            "global": [
+                "_globalRe"
+            ],
+            "objc_class": [
+                "ClassRexport"
+            ]
+        },
+        "text": {
+            "global": [
+                "_funcA"
+            ]
+        }
+    }
+  ],
+  "undefined_symbols": [
+    {
+        "targets": [
+            "x86_64-macos"
+        ],
+        "data": {
+            "global": [
+                "_globalBind"
+            ],
+            "weak": [
+                "referenced_sym"
+            ]
+        }
+    }
+  ]
+}})";
+
+  InterfaceFile File;
+  File.setFileType(FileType::TBD_V5);
+
+  TargetList AllTargets = {
+      Target(AK_x86_64, PLATFORM_MACOS, VersionTuple(10, 14)),
+      Target(AK_arm64, PLATFORM_MACOS, VersionTuple(10, 14)),
+      Target(AK_arm64, PLATFORM_MACCATALYST, VersionTuple(12, 1)),
+  };
+  File.addTargets(AllTargets);
+  File.setInstallName("@rpath/S/L/F/Foo.framework/Foo");
+  File.setCurrentVersion(PackedVersion(1, 2, 0));
+  File.setCompatibilityVersion(PackedVersion(1, 1, 0));
+  File.addRPath(AllTargets[0], "@executable_path/.../Frameworks");
+
+  for (const auto &Targ : AllTargets) {
+    File.addParentUmbrella(Targ, "System");
+    File.addAllowableClient("ClientA", Targ);
+    File.addAllowableClient("ClientB", Targ);
+    File.addReexportedLibrary("/u/l/l/libfoo.dylib", Targ);
+    File.addReexportedLibrary("/u/l/l/libbar.dylib", Targ);
+  }
+
+  SymbolFlags Flags = SymbolFlags::None;
+  // Exports.
+  File.addSymbol(SymbolKind::GlobalSymbol, "_global",
+                 {AllTargets[0], AllTargets[1]}, Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::GlobalSymbol, "_func",
+                 {AllTargets[0], AllTargets[1]}, Flags | SymbolFlags::Text);
+  File.addSymbol(SymbolKind::ObjectiveCClass, "ClassA",
+                 {AllTargets[0], AllTargets[1]}, Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::GlobalSymbol, "_funcFoo", {AllTargets[0]},
+                 Flags | SymbolFlags::Text);
+  File.addSymbol(SymbolKind::GlobalSymbol, "_globalVar", {AllTargets[0]},
+                 Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::ObjectiveCClass, "ClassData", {AllTargets[0]},
+                 Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::ObjectiveCClassEHType, "ClassA", {AllTargets[0]},
+                 Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::ObjectiveCClassEHType, "ClassB", {AllTargets[0]},
+                 Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::ObjectiveCInstanceVariable, "ClassA.ivar1",
+                 {AllTargets[0]}, Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::ObjectiveCInstanceVariable, "ClassA.ivar2",
+                 {AllTargets[0]}, Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::ObjectiveCInstanceVariable, "ClassC.ivar1",
+                 {AllTargets[0]}, Flags | SymbolFlags::Data);
+
+  // Reexports.
+  Flags = SymbolFlags::Rexported;
+  File.addSymbol(SymbolKind::GlobalSymbol, "_globalRe", AllTargets,
+                 Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::GlobalSymbol, "_funcA", AllTargets,
+                 Flags | SymbolFlags::Text);
+  File.addSymbol(SymbolKind::ObjectiveCClass, "ClassRexport", AllTargets,
+                 Flags | SymbolFlags::Data);
+
+  // Undefineds.
+  Flags = SymbolFlags::Undefined;
+  File.addSymbol(SymbolKind::GlobalSymbol, "_globalBind", {AllTargets[0]},
+                 Flags | SymbolFlags::Data);
+  File.addSymbol(SymbolKind::GlobalSymbol, "referenced_sym", {AllTargets[0]},
+                 Flags | SymbolFlags::Data | SymbolFlags::WeakReferenced);
+
+  File.setTwoLevelNamespace(false);
+  File.setApplicationExtensionSafe(true);
+
+  // Write out file then process it back into IF and compare equality
+  // against TBDv5File.
+  SmallString<4096> Buffer;
+  raw_svector_ostream OS(Buffer);
+  Error Result = TextAPIWriter::writeToStream(OS, File);
+  EXPECT_FALSE(Result);
+
+  Expected<TBDFile> Input =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Input.tbd"));
+  EXPECT_TRUE(!!Input);
+  TBDFile InputFile = std::move(Input.get());
+
+  Expected<TBDFile> Output =
+      TextAPIReader::get(MemoryBufferRef(Buffer, "Output.tbd"));
+  EXPECT_TRUE(!!Output);
+  TBDFile OutputFile = std::move(Output.get());
+  EXPECT_EQ(*InputFile, *OutputFile);
+}
+
+TEST(TBDv5, WriteMultipleDocuments) {
+  static const char TBDv5File[] = R"({ 
+"tapi_tbd_version": 5,
+"main_library": {
+  "target_info": [
+    {
+      "target": "armv7-ios",
+      "min_deployment": "11.0" 
+    }
+  ],
+  "install_names":[
+    { "name":"/S/L/F/Foo.framework/Foo" }
+  ],
+  "reexported_libraries": [
+    { "names": ["/u/l/l/libfoo.dylib"] 
+    }
+  ]
+},
+"libraries": [
+  {
+    "target_info": [
+      {
+        "target": "armv7-ios",
+        "min_deployment": "11.0" 
+      },
+      {
+        "target": "armv7s-ios",
+        "min_deployment": "11.0" 
+      }
+    ],
+    "install_names":[
+      { "name":"/u/l/l/libfoo.dylib" }
+    ],
+    "current_versions": [
+      {
+          "version": "2.1.1"
+      }
+    ],
+    "rpaths": [
+      {
+        "targets": [
+            "armv7-ios"
+        ],
+        "paths": [
+            "@executable_path/.../Frameworks"
+        ]
+      }],
+    "reexported_libraries": [ { "names": ["@rpath/libfoo.dylib"] } ],
+    "flags":[ 
+      { "attributes": ["not_app_extension_safe"] }
+    ], 
+    "exported_symbols": [
+      {
+        "text": {
+          "global": [ "_funcFoo" ]
+        }
+      }
+    ]
+  },
+  {
+    "target_info": [
+      {
+        "target": "armv7-ios",
+        "min_deployment": "11.0" 
+      }
+    ],
+    "install_names":[
+      { "name":"@rpath/libfoo.dylib" }
+    ],
+    "exported_symbols": [
+      {
+        "data": {
+          "global": [ "_varFooBaz" ]
+        }
+      }
+    ]
+  }
+]})";
+
+  InterfaceFile File;
+  File.setFileType(FileType::TBD_V5);
+
+  TargetList AllTargets = {
+      Target(AK_armv7, PLATFORM_IOS, VersionTuple(11, 0)),
+      Target(AK_armv7s, PLATFORM_IOS, VersionTuple(11, 0)),
+  };
+  File.setInstallName("/S/L/F/Foo.framework/Foo");
+  File.addTarget(AllTargets[0]);
+  File.setCurrentVersion(PackedVersion(1, 0, 0));
+  File.setCompatibilityVersion(PackedVersion(1, 0, 0));
+  File.addReexportedLibrary("/u/l/l/libfoo.dylib", AllTargets[0]);
+  File.setTwoLevelNamespace();
+  File.setApplicationExtensionSafe(true);
+
+  InterfaceFile NestedFile;
+  NestedFile.setFileType(FileType::TBD_V5);
+  NestedFile.setInstallName("/u/l/l/libfoo.dylib");
+  NestedFile.addTargets(AllTargets);
+  NestedFile.setCompatibilityVersion(PackedVersion(1, 0, 0));
+  NestedFile.setTwoLevelNamespace();
+  NestedFile.setApplicationExtensionSafe(false);
+  NestedFile.setCurrentVersion(PackedVersion(2, 1, 1));
+  NestedFile.addRPath(AllTargets[0], "@executable_path/.../Frameworks");
+  for (const auto &Targ : AllTargets)
+    NestedFile.addReexportedLibrary("@rpath/libfoo.dylib", Targ);
+  NestedFile.addSymbol(SymbolKind::GlobalSymbol, "_funcFoo", AllTargets,
+                       SymbolFlags::Text);
+  File.addDocument(std::make_shared<InterfaceFile>(std::move(NestedFile)));
+
+  InterfaceFile NestedFileB;
+  NestedFileB.setFileType(FileType::TBD_V5);
+  NestedFileB.setInstallName("@rpath/libfoo.dylib");
+  NestedFileB.addTarget(AllTargets[0]);
+  NestedFileB.setCompatibilityVersion(PackedVersion(1, 0, 0));
+  NestedFileB.setCurrentVersion(PackedVersion(1, 0, 0));
+  NestedFileB.setTwoLevelNamespace();
+  NestedFileB.setApplicationExtensionSafe(true);
+  NestedFileB.addSymbol(SymbolKind::GlobalSymbol, "_varFooBaz", AllTargets,
+                        SymbolFlags::Data);
+  File.addDocument(std::make_shared<InterfaceFile>(std::move(NestedFileB)));
+
+  // Write out file then process it back into IF and compare equality
+  // against TBDv5File.
+  SmallString<4096> Buffer;
+  raw_svector_ostream OS(Buffer);
+  Error Result = TextAPIWriter::writeToStream(OS, File, /*Compact=*/true);
+  EXPECT_FALSE(Result);
+
+  Expected<TBDFile> Input =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Input.tbd"));
+  EXPECT_TRUE(!!Input);
+  TBDFile InputFile = std::move(Input.get());
+
+  Expected<TBDFile> Output =
+      TextAPIReader::get(MemoryBufferRef(Buffer, "Output.tbd"));
+  EXPECT_TRUE(!!Output);
+  TBDFile OutputFile = std::move(Output.get());
+  EXPECT_EQ(*InputFile, *OutputFile);
+}
+
+TEST(TBDv5, Target_Simulator) {
+  static const char TBDv5File[] = R"({ 
+"tapi_tbd_version": 5,
+"main_library": {
+  "target_info": [
+    {
+      "target": "arm64-ios-simulator",
+      "min_deployment": "11.0"
+    },
+    {
+      "target": "x86_64-ios-simulator",
+      "min_deployment": "11.3" 
+    }
+  ],
+  "install_names":[
+    { "name":"/S/L/F/Foo.framework/Foo" }
+  ]
+}})";
+
+  Expected<TBDFile> Result =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Test.tbd"));
+  EXPECT_TRUE(!!Result);
+  TBDFile File = std::move(Result.get());
+  EXPECT_EQ(FileType::TBD_V5, File->getFileType());
+  TargetList ExpectedTargets = {
+      Target(AK_x86_64, PLATFORM_IOSSIMULATOR, VersionTuple(11, 3)),
+      Target(AK_arm64, PLATFORM_IOSSIMULATOR, VersionTuple(11, 0)),
+  };
+  TargetList Targets{File->targets().begin(), File->targets().end()};
+  llvm::sort(Targets);
+  EXPECT_EQ(Targets, ExpectedTargets);
+
+  SmallString<4096> Buffer;
+  raw_svector_ostream OS(Buffer);
+  Error WriteResult = TextAPIWriter::writeToStream(OS, *File);
+  EXPECT_TRUE(!WriteResult);
+
+  Expected<TBDFile> Output =
+      TextAPIReader::get(MemoryBufferRef(Buffer, "Output.tbd"));
+  EXPECT_TRUE(!!Output);
+  TBDFile WriteResultFile = std::move(Output.get());
+  EXPECT_EQ(*File, *WriteResultFile);
+}
+
+TEST(TBDv5, MisspelledKey) {
+  static const char TBDv5File[] = R"({ 
+"tapi_tbd_version": 5,
+"main_library": {
+  "target_info": [
+    {
+      "target": "arm64-ios-simulator",
+      "min_deployment": "11.0"
+    }
+  ],
+  "intall_names":[
+    { "name":"/S/L/F/Foo.framework/Foo" }
+  ]
+}})";
+
+  Expected<TBDFile> Result =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Test.tbd"));
+  EXPECT_FALSE(!!Result);
+  std::string ErrorMessage = toString(Result.takeError());
+  EXPECT_EQ("invalid install_names section\n", ErrorMessage);
+}
+
+TEST(TBDv5, InvalidVersion) {
+  static const char TBDv5File[] = R"({ 
+"tapi_tbd_version": 11,
+"main_library": {
+  "target_info": [
+    {
+      "target": "arm64-ios-simulator",
+      "min_deployment": "11.0"
+    }
+  ],
+  "install_names":[
+    { "name":"/S/L/F/Foo.framework/Foo" }
+  ]
+}})";
+
+  Expected<TBDFile> Result =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Test.tbd"));
+  EXPECT_FALSE(!!Result);
+  std::string ErrorMessage = toString(Result.takeError());
+  EXPECT_EQ("invalid tapi_tbd_version section\n", ErrorMessage);
+}
+
+TEST(TBDv5, MissingRequiredKey) {
+  static const char TBDv5File[] = R"({ 
+"main_library": {
+  "target_info": [
+    {
+      "target": "arm64-ios-simulator",
+      "min_deployment": "11.0"
+    }
+  ],
+  "install_names":[
+    { "name":"/S/L/F/Foo.framework/Foo" }
+  ]
+}})";
+
+  Expected<TBDFile> Result =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Test.tbd"));
+  EXPECT_FALSE(!!Result);
+  std::string ErrorMessage = toString(Result.takeError());
+  EXPECT_EQ("invalid tapi_tbd_version section\n", ErrorMessage);
+}
+
+TEST(TBDv5, InvalidSymbols) {
+  static const char TBDv5File[] = R"({ 
+"tapi_tbd_version": 5,
+"main_library": {
+  "target_info": [
+    {
+      "target": "arm64-driverkit",
+      "min_deployment": "11.0"
+    }
+  ],
+  "install_names":[
+    { "name":"/S/L/F/Foo.framework/Foo" }
+  ],
+  "exported_symbols": [
+    {
+      "daa": {
+        "global": {
+            "weak": []
+          }
+      }
+    }
+  ]
+}})";
+
+  Expected<TBDFile> Result =
+      TextAPIReader::get(MemoryBufferRef(TBDv5File, "Test.tbd"));
+  EXPECT_FALSE(!!Result);
+  std::string ErrorMessage = toString(Result.takeError());
+  EXPECT_EQ("invalid exported_symbols section\n", ErrorMessage);
+}
+
 } // end namespace TBDv5