[clang] [InstallAPI] Pick up input headers by directory traversal (PR #94508)

Cyndy Ishida via cfe-commits cfe-commits at lists.llvm.org
Wed Jun 5 10:43:04 PDT 2024


https://github.com/cyndyishida created https://github.com/llvm/llvm-project/pull/94508

Match TAPI behavior and allow input headers to be resolved via a passed directory, which is expected to be a library sitting in a build directory.

>From c547d990aca29ecfe6f51d37c5c3f8712dfc5e44 Mon Sep 17 00:00:00 2001
From: Cyndy Ishida <cyndy_ishida at apple.com>
Date: Fri, 10 May 2024 09:19:22 -0700
Subject: [PATCH] [InstallAPI] Pick up input headers by directory traversal

Match TAPI behavior and allow input headers to be resolved via a passed
directory, which is expected to be a library sitting in a build
directory.
---
 .../clang/Basic/DiagnosticInstallAPIKinds.td  |   2 +
 .../clang/InstallAPI/DirectoryScanner.h       |  81 +++++
 clang/include/clang/InstallAPI/HeaderFile.h   |   8 +
 clang/include/clang/InstallAPI/Library.h      |  65 ++++
 clang/include/clang/InstallAPI/MachO.h        |   1 +
 clang/lib/InstallAPI/CMakeLists.txt           |   2 +
 clang/lib/InstallAPI/DirectoryScanner.cpp     | 300 ++++++++++++++++++
 clang/lib/InstallAPI/Library.cpp              |  40 +++
 clang/test/InstallAPI/asm.test                |   2 +-
 clang/test/InstallAPI/basic.test              |   4 +-
 clang/test/InstallAPI/binary-attributes.test  |   6 +-
 clang/test/InstallAPI/cpp.test                |   4 +-
 clang/test/InstallAPI/diagnostics-dsym.test   |   4 +-
 .../InstallAPI/directory-scanning-dylib.test  |  57 ++++
 .../directory-scanning-frameworks.test        |  89 ++++++
 clang/test/InstallAPI/functions.test          |   2 +-
 clang/test/InstallAPI/variables.test          |   2 +-
 clang/tools/clang-installapi/Options.cpp      |  51 ++-
 clang/tools/clang-installapi/Options.h        |   3 +
 19 files changed, 699 insertions(+), 24 deletions(-)
 create mode 100644 clang/include/clang/InstallAPI/DirectoryScanner.h
 create mode 100644 clang/include/clang/InstallAPI/Library.h
 create mode 100644 clang/lib/InstallAPI/DirectoryScanner.cpp
 create mode 100644 clang/lib/InstallAPI/Library.cpp
 create mode 100644 clang/test/InstallAPI/directory-scanning-dylib.test
 create mode 100644 clang/test/InstallAPI/directory-scanning-frameworks.test

diff --git a/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td b/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
index cdf27247602f2..e10fa71011f30 100644
--- a/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
+++ b/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
@@ -26,6 +26,8 @@ def err_unsupported_environment : Error<"environment '%0' is not supported: '%1'
 def err_unsupported_os : Error<"os '%0' is not supported: '%1'">;
 def err_cannot_read_input_list : Error<"could not read %0 input list '%1': %2">;
 def err_invalid_label: Error<"label '%0' is reserved: use a different label name for -X<label>">;
+def err_directory_scanning: Error<"could not read directory '%0': %1">;
+def err_more_than_one_library: Error<"more than one framework/dynamic library found">;
 } // end of command line category.
 
 let CategoryName = "Verification" in {
diff --git a/clang/include/clang/InstallAPI/DirectoryScanner.h b/clang/include/clang/InstallAPI/DirectoryScanner.h
new file mode 100644
index 0000000000000..803328982ec87
--- /dev/null
+++ b/clang/include/clang/InstallAPI/DirectoryScanner.h
@@ -0,0 +1,81 @@
+//===- InstallAPI/DirectoryScanner.h ----------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// The DirectoryScanner for collecting library files on the file system.
+///
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_INSTALLAPI_DIRECTORYSCANNER_H
+#define LLVM_CLANG_INSTALLAPI_DIRECTORYSCANNER_H
+
+#include "clang/Basic/FileManager.h"
+#include "clang/InstallAPI/Library.h"
+
+namespace clang::installapi {
+
+enum ScanMode {
+  /// Scanning Framework directory.
+  ScanFrameworks,
+  /// Scanning Dylib directory.
+  ScanDylibs,
+};
+
+class DirectoryScanner {
+public:
+  DirectoryScanner(FileManager &FM, ScanMode Mode = ScanMode::ScanFrameworks)
+      : FM(FM), Mode(Mode) {}
+
+  /// Scan for all input files throughout directory.
+  ///
+  /// \param Directory Path of input directory.
+  llvm::Error scan(StringRef Directory);
+
+  /// Take over ownership of stored libraries.
+  std::vector<Library> takeLibraries() { return std::move(Libraries); };
+
+  /// Get all the header files in libraries.
+  ///
+  /// \param Libraries Reference of collection of libraries.
+  static HeaderSeq getHeaders(ArrayRef<Library> Libraries);
+
+private:
+  /// Collect files for dylibs in usr/(local)/lib within directory.
+  llvm::Error scanForUnwrappedLibraries(StringRef Directory);
+
+  /// Collect files for any frameworks within directory.
+  llvm::Error scanForFrameworks(StringRef Directory);
+
+  /// Get a library from the libraries collection.
+  Library &getOrCreateLibrary(StringRef Path, std::vector<Library> &Libs) const;
+
+  /// Collect multiple frameworks from directory.
+  llvm::Error scanMultipleFrameworks(StringRef Directory,
+                                     std::vector<Library> &Libs) const;
+  /// Collect files from nested frameworks.
+  llvm::Error scanSubFrameworksDirectory(StringRef Directory,
+                                         std::vector<Library> &Libs) const;
+
+  /// Collect files from framework path.
+  llvm::Error scanFrameworkDirectory(StringRef Path, Library &Framework) const;
+
+  /// Collect header files from path.
+  llvm::Error scanHeaders(StringRef Path, Library &Lib, HeaderType Type,
+                          StringRef BasePath,
+                          StringRef ParentPath = StringRef()) const;
+
+  /// Collect files from Version directories inside Framework directories.
+  llvm::Error scanFrameworkVersionsDirectory(StringRef Path,
+                                             Library &Lib) const;
+  FileManager &FM;
+  ScanMode Mode;
+  StringRef RootPath;
+  std::vector<Library> Libraries;
+};
+
+} // namespace clang::installapi
+
+#endif // LLVM_CLANG_INSTALLAPI_DIRECTORYSCANNER_H
diff --git a/clang/include/clang/InstallAPI/HeaderFile.h b/clang/include/clang/InstallAPI/HeaderFile.h
index c67503d4ad49e..12a87c01ad1c4 100644
--- a/clang/include/clang/InstallAPI/HeaderFile.h
+++ b/clang/include/clang/InstallAPI/HeaderFile.h
@@ -97,6 +97,14 @@ class HeaderFile {
                                           Other.Excluded, Other.Extra,
                                           Other.Umbrella);
   }
+
+  bool operator<(const HeaderFile &Other) const {
+    if (isExtra() && Other.isExtra())
+      return std::tie(Type, Umbrella) < std::tie(Other.Type, Other.Umbrella);
+
+    return std::tie(Type, Umbrella, Extra, FullPath) <
+           std::tie(Other.Type, Other.Umbrella, Other.Extra, Other.FullPath);
+  }
 };
 
 /// Glob that represents a pattern of header files to retreive.
diff --git a/clang/include/clang/InstallAPI/Library.h b/clang/include/clang/InstallAPI/Library.h
new file mode 100644
index 0000000000000..8373d424dd364
--- /dev/null
+++ b/clang/include/clang/InstallAPI/Library.h
@@ -0,0 +1,65 @@
+//===- InstallAPI/Library.h -------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// Defines the content of a library, such as public and private
+/// header files, and whether it is a framework.
+///
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_INSTALLAPI_LIBRARY_H
+#define LLVM_CLANG_INSTALLAPI_LIBRARY_H
+
+#include "clang/InstallAPI/HeaderFile.h"
+#include "clang/InstallAPI/MachO.h"
+
+namespace clang::installapi {
+
+class Library {
+public:
+  Library(StringRef Directory) : BaseDirectory(Directory) {}
+
+  /// Capture the name of the framework by the install name.
+  ///
+  /// \param InstallName The install name of the library encoded in a dynamic
+  /// library.
+  static StringRef getFrameworkNameFromInstallName(StringRef InstallName);
+
+  /// Get name of library by the discovered file path.
+  StringRef getName() const;
+
+  /// Get discovered path of library.
+  StringRef getPath() const { return BaseDirectory; }
+
+  /// Add a header file that belongs to the library.
+  ///
+  /// \param FullPath Path to header file.
+  /// \param Type Access level of header.
+  /// \param IncludePath The way the header should be included.
+  void addHeaderFile(StringRef FullPath, HeaderType Type,
+                     StringRef IncludePath = StringRef()) {
+    Headers.emplace_back(FullPath, Type, IncludePath);
+  }
+
+  /// Determine if library is empty.
+  bool empty() {
+    return SubFrameworks.empty() && Headers.empty() &&
+           FrameworkVersions.empty();
+  }
+
+private:
+  std::string BaseDirectory;
+  HeaderSeq Headers;
+  std::vector<Library> SubFrameworks;
+  std::vector<Library> FrameworkVersions;
+  bool IsUnwrappedDylib{false};
+
+  friend class DirectoryScanner;
+};
+
+} // namespace clang::installapi
+
+#endif // LLVM_CLANG_INSTALLAPI_LIBRARY_H
diff --git a/clang/include/clang/InstallAPI/MachO.h b/clang/include/clang/InstallAPI/MachO.h
index 1ea544412f4cd..6036a7e5397cb 100644
--- a/clang/include/clang/InstallAPI/MachO.h
+++ b/clang/include/clang/InstallAPI/MachO.h
@@ -31,6 +31,7 @@ using RecordLinkage = llvm::MachO::RecordLinkage;
 using Record = llvm::MachO::Record;
 using EncodeKind = llvm::MachO::EncodeKind;
 using GlobalRecord = llvm::MachO::GlobalRecord;
+using InterfaceFile = llvm::MachO::InterfaceFile;
 using ObjCContainerRecord = llvm::MachO::ObjCContainerRecord;
 using ObjCInterfaceRecord = llvm::MachO::ObjCInterfaceRecord;
 using ObjCCategoryRecord = llvm::MachO::ObjCCategoryRecord;
diff --git a/clang/lib/InstallAPI/CMakeLists.txt b/clang/lib/InstallAPI/CMakeLists.txt
index b36493942300b..b63173bc1be3e 100644
--- a/clang/lib/InstallAPI/CMakeLists.txt
+++ b/clang/lib/InstallAPI/CMakeLists.txt
@@ -8,10 +8,12 @@ set(LLVM_LINK_COMPONENTS
 
 add_clang_library(clangInstallAPI
   DiagnosticBuilderWrappers.cpp
+  DirectoryScanner.cpp
   DylibVerifier.cpp
   FileList.cpp
   Frontend.cpp
   HeaderFile.cpp
+  Library.cpp
   Visitor.cpp
 
   LINK_LIBS
diff --git a/clang/lib/InstallAPI/DirectoryScanner.cpp b/clang/lib/InstallAPI/DirectoryScanner.cpp
new file mode 100644
index 0000000000000..ae4ba52de6ba9
--- /dev/null
+++ b/clang/lib/InstallAPI/DirectoryScanner.cpp
@@ -0,0 +1,300 @@
+//===- DirectoryScanner.cpp -----------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/InstallAPI/DirectoryScanner.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSwitch.h"
+#include "llvm/TextAPI/DylibReader.h"
+
+using namespace llvm;
+using namespace llvm::MachO;
+
+namespace clang::installapi {
+
+HeaderSeq DirectoryScanner::getHeaders(ArrayRef<Library> Libraries) {
+  HeaderSeq Headers;
+  for (const Library &Lib : Libraries)
+    llvm::append_range(Headers, Lib.Headers);
+  return Headers;
+}
+
+llvm::Error DirectoryScanner::scan(StringRef Directory) {
+  if (Mode == ScanMode::ScanFrameworks)
+    return scanForFrameworks(Directory);
+
+  return scanForUnwrappedLibraries(Directory);
+}
+
+llvm::Error DirectoryScanner::scanForUnwrappedLibraries(StringRef Directory) {
+  // Check some known sub-directory locations.
+  auto GetDirectory = [&](const char *Sub) -> OptionalDirectoryEntryRef {
+    SmallString<PATH_MAX> Path(Directory);
+    sys::path::append(Path, Sub);
+    return FM.getOptionalDirectoryRef(Path);
+  };
+
+  auto DirPublic = GetDirectory("usr/include");
+  auto DirPrivate = GetDirectory("usr/local/include");
+  if (!DirPublic && !DirPrivate) {
+    std::error_code ec = std::make_error_code(std::errc::not_a_directory);
+    return createStringError(ec,
+                             "cannot find any public (usr/include) or private "
+                             "(usr/local/include) header directory");
+  }
+
+  Library &Lib = getOrCreateLibrary(Directory, Libraries);
+  Lib.IsUnwrappedDylib = true;
+
+  if (DirPublic)
+    if (Error Err = scanHeaders(DirPublic->getName(), Lib, HeaderType::Public,
+                                Directory))
+      return Err;
+
+  if (DirPrivate)
+    if (Error Err = scanHeaders(DirPrivate->getName(), Lib, HeaderType::Private,
+                                Directory))
+      return Err;
+
+  return Error::success();
+}
+
+static bool isFramework(StringRef Path) {
+  while (Path.back() == '/')
+    Path = Path.slice(0, Path.size() - 1);
+
+  return llvm::StringSwitch<bool>(llvm::sys::path::extension(Path))
+      .Case(".framework", true)
+      .Default(false);
+}
+
+Library &
+DirectoryScanner::getOrCreateLibrary(StringRef Path,
+                                     std::vector<Library> &Libs) const {
+  if (Path.consume_front(RootPath) && Path.empty())
+    Path = "/";
+
+  auto LibIt =
+      find_if(Libs, [Path](const Library &L) { return L.getPath() == Path; });
+  if (LibIt != Libs.end())
+    return *LibIt;
+
+  Libs.emplace_back(Path);
+  return Libs.back();
+}
+
+Error DirectoryScanner::scanHeaders(StringRef Path, Library &Lib,
+                                    HeaderType Type, StringRef BasePath,
+                                    StringRef ParentPath) const {
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+  PathSeq SubDirectories;
+  for (vfs::directory_iterator i = FS.dir_begin(Path, ec), ie; i != ie;
+       i.increment(ec)) {
+    StringRef HeaderPath = i->path();
+    if (ec)
+      return createStringError(ec, "unable to read: " + HeaderPath);
+
+    if (sys::fs::is_symlink_file(HeaderPath))
+      continue;
+
+    // Ignore tmp files from unifdef.
+    const StringRef Filename = sys::path::filename(HeaderPath);
+    if (Filename.starts_with("."))
+      continue;
+
+    // If it is a directory, remember the subdirectory.
+    if (FM.getOptionalDirectoryRef(HeaderPath))
+      SubDirectories.push_back(HeaderPath.str());
+
+    if (!isHeaderFile(HeaderPath))
+      continue;
+
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (FS.status(HeaderPath) == std::errc::no_such_file_or_directory)
+      continue;
+
+    auto IncludeName = createIncludeHeaderName(HeaderPath);
+    Lib.addHeaderFile(HeaderPath, Type,
+                      IncludeName.has_value() ? IncludeName.value() : "");
+  }
+
+  // Go through the subdirectories.
+  // Sort the sub-directory first since different file systems might have
+  // different traverse order.
+  llvm::sort(SubDirectories);
+  if (ParentPath.empty())
+    ParentPath = Path;
+  for (const StringRef Dir : SubDirectories)
+    return scanHeaders(Dir, Lib, Type, BasePath, ParentPath);
+
+  return Error::success();
+}
+
+llvm::Error
+DirectoryScanner::scanMultipleFrameworks(StringRef Directory,
+                                         std::vector<Library> &Libs) const {
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+  for (vfs::directory_iterator i = FS.dir_begin(Directory, ec), ie; i != ie;
+       i.increment(ec)) {
+    StringRef Curr = i->path();
+
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (ec == std::errc::no_such_file_or_directory) {
+      ec.clear();
+      continue;
+    }
+    if (ec)
+      return createStringError(ec, Curr);
+
+    if (sys::fs::is_symlink_file(Curr))
+      continue;
+
+    if (isFramework(Curr)) {
+      if (FM.getOptionalDirectoryRef(Curr))
+        continue;
+      Library &Framework = getOrCreateLibrary(Curr, Libs);
+      if (Error Err = scanFrameworkDirectory(Curr, Framework))
+        return Err;
+    }
+  }
+
+  return Error::success();
+}
+
+llvm::Error
+DirectoryScanner::scanSubFrameworksDirectory(StringRef Directory,
+                                             std::vector<Library> &Libs) const {
+  if (FM.getOptionalDirectoryRef(Directory))
+    return scanMultipleFrameworks(Directory, Libs);
+
+  std::error_code ec = std::make_error_code(std::errc::not_a_directory);
+  return createStringError(ec, Directory);
+}
+
+/// FIXME: How to handle versions? For now scan them separately as independent
+/// frameworks.
+llvm::Error
+DirectoryScanner::scanFrameworkVersionsDirectory(StringRef Path,
+                                                 Library &Lib) const {
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+  for (vfs::directory_iterator i = FS.dir_begin(Path, ec), ie; i != ie;
+       i.increment(ec)) {
+    const StringRef Curr = i->path();
+
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (ec == std::errc::no_such_file_or_directory) {
+      ec.clear();
+      continue;
+    }
+    if (ec)
+      return createStringError(ec, Curr);
+
+    if (sys::fs::is_symlink_file(Curr))
+      continue;
+
+    // Each version should be a framework directory.
+    if (!FM.getOptionalDirectoryRef(Curr))
+      continue;
+
+    Library &VersionedFramework =
+        getOrCreateLibrary(Curr, Lib.FrameworkVersions);
+    if (Error Err = scanFrameworkDirectory(Curr, VersionedFramework))
+      return Err;
+  }
+
+  return Error::success();
+}
+
+llvm::Error DirectoryScanner::scanFrameworkDirectory(StringRef Path,
+                                                     Library &Framework) const {
+  // If the framework is inside Kernel or IOKit, scan headers in the different
+  // directories separately.
+  Framework.IsUnwrappedDylib =
+      Path.contains("Kernel.framework") || Path.contains("IOKit.framework");
+
+  // Unfortunately we cannot identify symlinks in the VFS. We assume that if
+  // there is a Versions directory, then we have symlinks and directly proceed
+  // to the Versions folder.
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+
+  for (vfs::directory_iterator i = FS.dir_begin(Path, ec), ie; i != ie;
+       i.increment(ec)) {
+    StringRef Curr = i->path();
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (ec == std::errc::no_such_file_or_directory) {
+      ec.clear();
+      continue;
+    }
+
+    if (ec)
+      return createStringError(ec, Curr);
+
+    if (sys::fs::is_symlink_file(Curr))
+      continue;
+
+    StringRef FileName = sys::path::filename(Curr);
+    // Scan all "public" headers.
+    if (FileName.contains("Headers")) {
+      if (Error Err = scanHeaders(Curr, Framework, HeaderType::Public, Curr))
+        return Err;
+      continue;
+    }
+    // Scan all "private" headers.
+    if (FileName.contains("PrivateHeaders")) {
+      if (Error Err = scanHeaders(Curr, Framework, HeaderType::Private, Curr))
+        return Err;
+      continue;
+    }
+    // Scan sub frameworks.
+    if (FileName.contains("Frameworks")) {
+      if (Error Err = scanSubFrameworksDirectory(Curr, Framework.SubFrameworks))
+        return Err;
+      continue;
+    }
+    // Check for versioned frameworks.
+    if (FileName.contains("Versions")) {
+      if (Error Err = scanFrameworkVersionsDirectory(Curr, Framework))
+        return Err;
+      continue;
+    }
+  }
+
+  return Error::success();
+}
+
+llvm::Error DirectoryScanner::scanForFrameworks(StringRef Directory) {
+  RootPath = "";
+
+  // Expect a certain directory structure and naming convention to find
+  // frameworks.
+  static const char *SubDirectories[] = {"System/Library/Frameworks/",
+                                         "System/Library/PrivateFrameworks/"};
+
+  // Check if the directory is already a framework.
+  if (isFramework(Directory)) {
+    Library &Framework = getOrCreateLibrary(Directory, Libraries);
+    if (Error Err = scanFrameworkDirectory(Directory, Framework))
+      return Err;
+    return Error::success();
+  }
+
+  // Check known sub-directory locations.
+  for (const auto *SubDir : SubDirectories) {
+    SmallString<PATH_MAX> Path(Directory);
+    sys::path::append(Path, SubDir);
+
+    if (Error Err = scanMultipleFrameworks(Path, Libraries))
+      return Err;
+  }
+
+  return Error::success();
+}
+} // namespace clang::installapi
diff --git a/clang/lib/InstallAPI/Library.cpp b/clang/lib/InstallAPI/Library.cpp
new file mode 100644
index 0000000000000..bdfa3535273e1
--- /dev/null
+++ b/clang/lib/InstallAPI/Library.cpp
@@ -0,0 +1,40 @@
+//===- Library.cpp --------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/InstallAPI/Library.h"
+
+using namespace llvm;
+namespace clang::installapi {
+
+const Regex Rule("(.+)/(.+)\\.framework/");
+StringRef Library::getFrameworkNameFromInstallName(StringRef InstallName) {
+  assert(InstallName.contains(".framework") && "expected a framework");
+  SmallVector<StringRef, 3> Match;
+  Rule.match(InstallName, &Match);
+  if (Match.empty())
+    return "";
+  return Match.back();
+}
+
+StringRef Library::getName() const {
+  assert(!IsUnwrappedDylib && "expected a framework");
+  StringRef Path = BaseDirectory;
+
+  // Return the framework name extracted from path.
+  while (!Path.empty()) {
+    if (Path.ends_with(".framework"))
+      return sys::path::filename(Path);
+    Path = sys::path::parent_path(Path);
+  }
+
+  // Otherwise, return the name of the BaseDirectory.
+  Path = BaseDirectory;
+  return sys::path::filename(Path.rtrim("/"));
+}
+
+} // namespace clang::installapi
diff --git a/clang/test/InstallAPI/asm.test b/clang/test/InstallAPI/asm.test
index b6af7f643d72f..9df644a823909 100644
--- a/clang/test/InstallAPI/asm.test
+++ b/clang/test/InstallAPI/asm.test
@@ -3,7 +3,7 @@
 // RUN: sed -e "s|DSTROOT|%/t|g" %t/inputs.json.in > %t/inputs.json
 
 // RUN: clang-installapi -target arm64-apple-macos13.1 \
-// RUN: -I%t/usr/include \
+// RUN: -I%t/usr/include -dynamiclib \
 // RUN: -install_name @rpath/lib/libasm.dylib \
 // RUN: %t/inputs.json -o %t/output.tbd 2>&1 | FileCheck %s --allow-empty
 // RUN: llvm-readtapi -compare %t/output.tbd %t/expected.tbd 2>&1 | FileCheck %s --allow-empty
diff --git a/clang/test/InstallAPI/basic.test b/clang/test/InstallAPI/basic.test
index 096911039d114..d86948fd22289 100644
--- a/clang/test/InstallAPI/basic.test
+++ b/clang/test/InstallAPI/basic.test
@@ -3,13 +3,13 @@
 /// Check basic arguments are captured.
 // RUN: clang-installapi -x objective-c -target arm64-apple-ios13.0.0 \
 // RUN: -fapplication-extension -current_version 1 -compatibility_version 1 \
-// RUN: -install_name /usr/lib/basic.dylib \
+// RUN: -install_name /usr/lib/basic.dylib -dynamiclib \
 // RUN: %t/basic_inputs.json -o %t/basic.tbd 2>&1 | FileCheck %s --allow-empty
 // RUN: llvm-readtapi -compare %t/basic.tbd %t/expected.tbd 2>&1 | FileCheck %s --allow-empty
 
 /// Check multiple targets are captured.
 // RUN: clang-installapi -x objective-c -target arm64-apple-ios14.1 -target arm64e-apple-ios14.1 \
-// RUN: -fapplication-extension -install_name /usr/lib/basic.dylib \
+// RUN: -fapplication-extension -install_name /usr/lib/basic.dylib -dynamiclib \
 // RUN: %t/basic_inputs.json -o %t/multi-targets.tbd 2>&1 | FileCheck %s --allow-empty
 // RUN: llvm-readtapi -compare %t/multi-targets.tbd %t/expected-multi.tbd 2>&1 | FileCheck %s --allow-empty
 
diff --git a/clang/test/InstallAPI/binary-attributes.test b/clang/test/InstallAPI/binary-attributes.test
index fd9ff12998a34..4c56c01c30aaa 100644
--- a/clang/test/InstallAPI/binary-attributes.test
+++ b/clang/test/InstallAPI/binary-attributes.test
@@ -5,12 +5,14 @@
 ; RUN: yaml2obj %S/Inputs/Simple/Simple.yaml -o %t/Simple 
 
 ; RUN: not clang-installapi -target x86_64h-apple-macos10.12 \
-; RUN: -install_name Simple -current_version 3 -compatibility_version 2 \
+; RUN: -install_name /System/Library/Frameworks/Simple.framework/Versions/A/Simple \
+; RUN: -current_version 3 -compatibility_version 2 \
 ; RUN: -o tmp.tbd --verify-against=%t/Simple 2>&1 | FileCheck -check-prefix=ARCHITECTURE %s
 ; ARCHITECTURE: error: architectures do not match: 'x86_64h' (provided) vs 'x86_64' (found)
 
 ; RUN: not clang-installapi -target x86_64-apple-macos10.12 \
-; RUN: -install_name Simple -current_version 3 -compatibility_version 2 \
+; RUN: -install_name Simple -dynamiclib \
+; RUN: -current_version 3 -compatibility_version 2 \
 ; RUN: -o tmp.tbd --verify-against=%t/Simple 2>&1 | FileCheck -check-prefix=INSTALL_NAME %s
 ; INSTALL_NAME: error: install_name does not match: 'Simple' (provided) vs '/System/Library/Frameworks/Simple.framework/Versions/A/Simple' (found)
 
diff --git a/clang/test/InstallAPI/cpp.test b/clang/test/InstallAPI/cpp.test
index 4817899095302..e29fb0c7fdb68 100644
--- a/clang/test/InstallAPI/cpp.test
+++ b/clang/test/InstallAPI/cpp.test
@@ -4,7 +4,7 @@
 
 // Invoke C++ with no-rtti.
 // RUN: clang-installapi -target arm64-apple-macos13.1 \
-// RUN: -I%t/usr/include -I%t/usr/local/include -x c++ \
+// RUN: -I%t/usr/include -I%t/usr/local/include -x c++ -dynamiclib \
 // RUN: -install_name @rpath/lib/libcpp.dylib -fno-rtti \
 // RUN: %t/inputs.json -o %t/no-rtti.tbd 2>&1 | FileCheck %s --allow-empty
 
@@ -14,7 +14,7 @@
 // Invoke C++ with rtti.
 // RUN: clang-installapi -target arm64-apple-macos13.1 \
 // RUN: -I%t/usr/include -I%t/usr/local/include -x c++ \
-// RUN: -install_name @rpath/lib/libcpp.dylib -frtti \
+// RUN: -install_name @rpath/lib/libcpp.dylib -frtti -dynamiclib \
 // RUN: %t/inputs.json -o %t/rtti.tbd 2>&1 | FileCheck %s --allow-empty
 // RUN: llvm-readtapi -compare %t/rtti.tbd \
 // RUN: %t/expected-rtti.tbd 2>&1 | FileCheck %s --allow-empty
diff --git a/clang/test/InstallAPI/diagnostics-dsym.test b/clang/test/InstallAPI/diagnostics-dsym.test
index c9cbeffef7bac..42fa67a1f9b1e 100644
--- a/clang/test/InstallAPI/diagnostics-dsym.test
+++ b/clang/test/InstallAPI/diagnostics-dsym.test
@@ -7,14 +7,14 @@
 // Build a simple dylib with debug info.
 ; RUN: %clang --target=arm64-apple-macos11 -g -dynamiclib %t/foo.c \
 ; RUN: -current_version 1 -compatibility_version 1 -L%t/usr/lib \
-; RUN: -save-temps \
+; RUN: -save-temps -dynamiclib \
 ; RUN: -o %t/foo.dylib -install_name %t/foo.dylib
 ; RUN: dsymutil %t/foo.dylib -o %t/foo.dSYM
 
 ; RUN: not clang-installapi -x c++ --target=arm64-apple-macos11 \
 ; RUN: -install_name %t/foo.dylib  \
 ; RUN: -current_version 1 -compatibility_version 1 \
-; RUN: -o %t/output.tbd \
+; RUN: -o %t/output.tbd -dynamiclib \
 ; RUN: --verify-against=%t/foo.dylib --dsym=%t/foo.dSYM \
 ; RUN: --verify-mode=Pedantic 2>&1 | FileCheck %s
 
diff --git a/clang/test/InstallAPI/directory-scanning-dylib.test b/clang/test/InstallAPI/directory-scanning-dylib.test
new file mode 100644
index 0000000000000..ace8745877872
--- /dev/null
+++ b/clang/test/InstallAPI/directory-scanning-dylib.test
@@ -0,0 +1,57 @@
+; RUN: rm -rf %t
+; RUN: split-file %s %t
+; RUN: mkdir -p %t/DstRoot/
+; RUN: cp -r %S/Inputs/LibFoo/ %t/DstRoot/
+
+; RUN: clang-installapi \
+; RUN: -target arm64-apple-macos12 -install_name @rpath/libfoo.dylib \
+; RUN: -current_version 1 -compatibility_version 1 \
+; RUN: -I%t/DstRoot/usr/include -dynamiclib \
+; RUN: -exclude-public-header %t/DstRoot/usr/include/public.h \
+; RUN: %t/DstRoot -o %t/output.tbd 2>&1 | FileCheck %s --allow-empty \
+; RUN: --implicit-check-not=error --implicit-check-not=warning 
+; RUN: llvm-readtapi --compare %t/output.tbd %t/expected.tbd 
+
+# Test expected error by empty directory.
+; RUN: mkdir -p %t/EmptyRoot
+; RUN: not clang-installapi \
+; RUN: -target arm64-apple-macos12 -install_name @rpath/libfoo.dylib \
+; RUN: -current_version 1 -compatibility_version 1 \
+; RUN: %t/DstRoot/usr/include -dynamiclib \
+; RUN: %t/EmptyRoot -o %t/output.tbd 2>&1 | FileCheck %s --check-prefix=EMPTY 
+
+; EMPTY: could not read directory {{.*}} cannot find any public (usr/include) or private (usr/local/include) header directory
+
+;--- expected.tbd
+{
+  "main_library": {
+    "exported_symbols": [
+      {
+        "text": {
+          "global": [
+            "_foo"
+          ]
+        }
+      }
+    ],
+    "flags": [
+      {
+        "attributes": [
+          "not_app_extension_safe"
+        ]
+      }
+    ],
+    "install_names": [
+      {
+        "name": "@rpath/libfoo.dylib"
+      }
+    ],
+    "target_info": [
+      {
+        "min_deployment": "12",
+        "target": "arm64-macos"
+      }
+    ]
+  },
+  "tapi_tbd_version": 5
+}
diff --git a/clang/test/InstallAPI/directory-scanning-frameworks.test b/clang/test/InstallAPI/directory-scanning-frameworks.test
new file mode 100644
index 0000000000000..78f9fe0a92997
--- /dev/null
+++ b/clang/test/InstallAPI/directory-scanning-frameworks.test
@@ -0,0 +1,89 @@
+; RUN: rm -rf %t
+; RUN: split-file %s %t
+; RUN: mkdir -p %t/Frameworks 
+; RUN: cp -r %S/Inputs/Simple/Simple.framework %t/Frameworks/
+; RUN: cp -r %S/Inputs/Foundation/Foundation.framework %t/Frameworks/
+
+; RUN: clang-installapi -target x86_64-apple-macosx10.12 \
+; RUN: -install_name /System/Library/Frameworks/Simple.framework/Versions/A/Simple \
+; RUN: -current_version 1.2.3 -compatibility_version 1 -o %t/Simple.tbd \
+; RUN: -exclude-public-header **/Simple.h \
+; RUN: -F %t/Frameworks --verify-mode=ErrorsOnly \
+; RUN: %t/Frameworks/Simple.framework 2>&1 | FileCheck -allow-empty %s \
+; RUN: --implicit-check-not=error --implicit-check-not=warning 
+; RUN: llvm-readtapi -compare %t/expected.tbd %t/Simple.tbd
+
+# Test expected error by collecting too many frameworks.
+; RUN: mkdir -p %t/FakeSDKRoot/System/Library/
+; RUN: ln -s %t/Frameworks %t/FakeSDKRoot/System/Library/Frameworks
+; RUN: not clang-installapi -target x86_64-apple-macosx10.12 \
+; RUN: -install_name /System/Library/Frameworks/Simple.framework/Versions/A/Simple \
+; RUN: -current_version 1.2.3 -compatibility_version 1 -o %t/Simple.tbd \
+; RUN: -F %t/Frameworks --verify-mode=ErrorsOnly \
+; RUN: %t/FakeSDKRoot  2>&1 | FileCheck %s --check-prefix=TOO_MANY
+
+; TOO_MANY: error: more than one framework/dynamic library found
+
+;--- expected.tbd
+{
+  "main_library": {
+    "current_versions": [
+      {
+        "version": "1.2.3"
+      }
+    ],
+    "exported_symbols": [
+      {
+        "data": {
+          "global": [
+            "_otherFrameworkAPI",
+            "_otherFrameworkSPI",
+            "_privateGlobalVariable"
+          ],
+          "objc_class": [
+            "Basic6",
+            "Basic1",
+            "Basic3",
+            "Basic4_2",
+            "Basic5",
+            "Basic9",
+            "Basic8",
+            "Basic2",
+            "Basic4",
+            "A",
+            "ExternalManagedObject"
+          ],
+          "objc_ivar": [
+            "Basic4.ivar2",
+            "Basic4_2.ivar1",
+            "Basic6.ivar1",
+            "Basic4.ivar1",
+            "Basic4_2.ivar2"
+          ],
+          "weak": [
+            "_weakPrivateGlobalVariable"
+          ]
+        }
+      }
+    ],
+    "flags": [
+      {
+        "attributes": [
+          "not_app_extension_safe"
+        ]
+      }
+    ],
+    "install_names": [
+      {
+        "name": "/System/Library/Frameworks/Simple.framework/Versions/A/Simple"
+      }
+    ],
+    "target_info": [
+      {
+        "min_deployment": "10.12",
+        "target": "x86_64-macos"
+      }
+    ]
+  },
+  "tapi_tbd_version": 5
+}
diff --git a/clang/test/InstallAPI/functions.test b/clang/test/InstallAPI/functions.test
index 5b5fd1308842e..a50a6a53e1001 100644
--- a/clang/test/InstallAPI/functions.test
+++ b/clang/test/InstallAPI/functions.test
@@ -3,7 +3,7 @@
 // RUN: sed -e "s|DSTROOT|%/t|g" %t/inputs.json.in > %t/inputs.json
 
 // RUN: clang-installapi -target arm64-apple-macos13.1 \
-// RUN: -I%t/usr/include -I%t/usr/local/include \
+// RUN: -I%t/usr/include -I%t/usr/local/include -dynamiclib \
 // RUN: -install_name @rpath/lib/libfunctions.dylib --filetype=tbd-v4 \
 // RUN: %t/inputs.json -o %t/outputs.tbd 2>&1 | FileCheck %s --allow-empty
 // RUN: llvm-readtapi -compare %t/outputs.tbd %t/expected.tbd 2>&1 | FileCheck %s --allow-empty
diff --git a/clang/test/InstallAPI/variables.test b/clang/test/InstallAPI/variables.test
index 6272867911f18..159158a6b91ec 100644
--- a/clang/test/InstallAPI/variables.test
+++ b/clang/test/InstallAPI/variables.test
@@ -4,7 +4,7 @@
 
 /// Check multiple targets are captured.
 // RUN: clang-installapi -target arm64-apple-macos13.1 -target arm64e-apple-macos13.1 \
-// RUN: -fapplication-extension -install_name /usr/lib/vars.dylib \
+// RUN: -fapplication-extension -install_name /usr/lib/vars.dylib -dynamiclib \
 // RUN: %t/vars_inputs.json -o %t/vars.tbd 2>&1 | FileCheck %s --allow-empty
 // RUN: llvm-readtapi -compare %t/vars.tbd %t/expected.tbd 2>&1 | FileCheck %s --allow-empty
 
diff --git a/clang/tools/clang-installapi/Options.cpp b/clang/tools/clang-installapi/Options.cpp
index 95d28b7b040d7..1ca1d583d5ccd 100644
--- a/clang/tools/clang-installapi/Options.cpp
+++ b/clang/tools/clang-installapi/Options.cpp
@@ -9,6 +9,7 @@
 #include "Options.h"
 #include "clang/Basic/DiagnosticIDs.h"
 #include "clang/Driver/Driver.h"
+#include "clang/InstallAPI/DirectoryScanner.h"
 #include "clang/InstallAPI/FileList.h"
 #include "clang/InstallAPI/HeaderFile.h"
 #include "clang/InstallAPI/InstallAPIDiagnostic.h"
@@ -126,8 +127,14 @@ getArgListFromJSON(const StringRef Input, llvm::opt::OptTable *Table,
 
 bool Options::processDriverOptions(InputArgList &Args) {
   // Handle inputs.
-  llvm::append_range(DriverOpts.FileLists,
-                     Args.getAllArgValues(drv::OPT_INPUT));
+  for (const StringRef Path : Args.getAllArgValues(drv::OPT_INPUT)) {
+    // Assume any input that is not a directory is a filelist.
+    // InstallAPI does not accept multiple directories, so retain the last one.
+    if (FM->getOptionalDirectoryRef(Path))
+      DriverOpts.InputDirectory = Path.str();
+    else
+      DriverOpts.FileLists.emplace_back(Path.str());
+  }
 
   // Handle output.
   SmallString<PATH_MAX> OutputPath;
@@ -760,15 +767,6 @@ Options::Options(DiagnosticsEngine &Diag, FileManager *FM,
   }
 }
 
-static const Regex Rule("(.+)/(.+)\\.framework/");
-static StringRef getFrameworkNameFromInstallName(StringRef InstallName) {
-  SmallVector<StringRef, 3> Match;
-  Rule.match(InstallName, &Match);
-  if (Match.empty())
-    return "";
-  return Match.back();
-}
-
 static Expected<std::unique_ptr<InterfaceFile>>
 getInterfaceFile(const StringRef Filename) {
   ErrorOr<std::unique_ptr<MemoryBuffer>> BufferOrErr =
@@ -897,9 +895,36 @@ InstallAPIContext Options::createContext() {
   // Attempt to find umbrella headers by capturing framework name.
   StringRef FrameworkName;
   if (!LinkerOpts.IsDylib)
-    FrameworkName = getFrameworkNameFromInstallName(LinkerOpts.InstallName);
+    FrameworkName =
+        Library::getFrameworkNameFromInstallName(LinkerOpts.InstallName);
+
+  /// Process inputs headers.
+  // 1. For headers discovered by directory scanning, sort them.
+  // 2. For headers discovered by filelist, respect ordering.
+  // 3. Append extra headers and mark any excluded headers.
+  // 4. Finally, surface up umbrella headers to top of the list.
+  if (!DriverOpts.InputDirectory.empty()) {
+    DirectoryScanner Scanner(*FM, LinkerOpts.IsDylib
+                                      ? ScanMode::ScanDylibs
+                                      : ScanMode::ScanFrameworks);
+    SmallString<PATH_MAX> NormalizedPath(DriverOpts.InputDirectory);
+    FM->getVirtualFileSystem().makeAbsolute(NormalizedPath);
+    sys::path::remove_dots(NormalizedPath, /*remove_dot_dot=*/true);
+    if (llvm::Error Err = Scanner.scan(NormalizedPath)) {
+      Diags->Report(diag::err_directory_scanning)
+          << DriverOpts.InputDirectory << std::move(Err);
+      return Ctx;
+    }
+    std::vector<Library> InputLibraries = Scanner.takeLibraries();
+    if (InputLibraries.size() > 1) {
+      Diags->Report(diag::err_more_than_one_library);
+      return Ctx;
+    }
+    llvm::append_range(Ctx.InputHeaders,
+                       DirectoryScanner::getHeaders(InputLibraries));
+    llvm::stable_sort(Ctx.InputHeaders);
+  }
 
-  // Process inputs.
   for (const StringRef ListPath : DriverOpts.FileLists) {
     auto Buffer = FM->getBufferForFile(ListPath);
     if (auto Err = Buffer.getError()) {
diff --git a/clang/tools/clang-installapi/Options.h b/clang/tools/clang-installapi/Options.h
index b37f91efbda72..d62f2efd3141a 100644
--- a/clang/tools/clang-installapi/Options.h
+++ b/clang/tools/clang-installapi/Options.h
@@ -30,6 +30,9 @@ struct DriverOptions {
   /// \brief Path to input file lists (JSON).
   llvm::MachO::PathSeq FileLists;
 
+  /// \brief Path to input directory.
+  std::string InputDirectory;
+
   /// \brief Path to public umbrella header.
   std::string PublicUmbrellaHeader;
 



More information about the cfe-commits mailing list