Skip to content

Commit

Permalink
[InstallAPI] Add *umbrella-header options (#86587)
Browse files Browse the repository at this point in the history
Umbrella headers are a concept for Darwin-based libraries. They allow
framework authors to control the order in which their headers should be
parsed and allow clients to access available headers by including a
single header.

InstallAPI will attempt to find the umbrella based on the name of the
framework. Users can also specify this explicitly by using command line
options specifying the umbrella header by file path. There can be an
umbrella header per access level.
  • Loading branch information
cyndyishida committed Mar 27, 2024
1 parent 9669aba commit 6ad1cf3
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 7 deletions.
1 change: 1 addition & 0 deletions clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def err_no_output_file: Error<"no output file specified">;
def err_no_such_header_file : Error<"no such %select{public|private|project}1 header file: '%0'">;
def warn_no_such_excluded_header_file : Warning<"no such excluded %select{public|private}0 header file: '%1'">, InGroup<InstallAPIViolation>;
def warn_glob_did_not_match: Warning<"glob '%0' did not match any header file">, InGroup<InstallAPIViolation>;
def err_no_such_umbrella_header_file : Error<"%select{public|private|project}1 umbrella header file not found in input: '%0'">;
} // end of command line category.

let CategoryName = "Verification" in {
Expand Down
16 changes: 11 additions & 5 deletions clang/include/clang/InstallAPI/HeaderFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@

namespace clang::installapi {
enum class HeaderType {
/// Unset or unknown type.
Unknown,
/// Represents declarations accessible to all clients.
Public,
/// Represents declarations accessible to a disclosed set of clients.
Private,
/// Represents declarations only accessible as implementation details to the
/// input library.
Project,
/// Unset or unknown type.
Unknown,
};

inline StringRef getName(const HeaderType T) {
Expand Down Expand Up @@ -62,6 +62,8 @@ class HeaderFile {
bool Excluded{false};
/// Add header file to processing.
bool Extra{false};
/// Specify that header file is the umbrella header for library.
bool Umbrella{false};

public:
HeaderFile() = delete;
Expand All @@ -79,17 +81,21 @@ class HeaderFile {

void setExtra(bool V = true) { Extra = V; }
void setExcluded(bool V = true) { Excluded = V; }
void setUmbrellaHeader(bool V = true) { Umbrella = V; }
bool isExtra() const { return Extra; }
bool isExcluded() const { return Excluded; }
bool isUmbrellaHeader() const { return Umbrella; }

bool useIncludeName() const {
return Type != HeaderType::Project && !IncludeName.empty();
}

bool operator==(const HeaderFile &Other) const {
return std::tie(Type, FullPath, IncludeName, Language, Excluded, Extra) ==
std::tie(Other.Type, Other.FullPath, Other.IncludeName,
Other.Language, Other.Excluded, Other.Extra);
return std::tie(Type, FullPath, IncludeName, Language, Excluded, Extra,
Umbrella) == std::tie(Other.Type, Other.FullPath,
Other.IncludeName, Other.Language,
Other.Excluded, Other.Extra,
Other.Umbrella);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#ifndef PUBLIC_UMBRELLA_HEADER_FIRST
#error "Public umbrella header was not included first!"
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#define PUBLIC_UMBRELLA_HEADER_FIRST
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#ifndef PRIVATE_UMBRELLA_HEADER_FIRST
#error "Private umbrella header was not included first!"
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#define PRIVATE_UMBRELLA_HEADER_FIRST
40 changes: 40 additions & 0 deletions clang/test/InstallAPI/umbrella-headers-unix.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// UNSUPPORTED: system-windows

; RUN: rm -rf %t
; RUN: split-file %s %t
; RUN: sed -e "s|DSTROOT|%/t|g" %t/inputs.json.in > %t/inputs.json
; RUN: mkdir %t/Frameworks/
; RUN: cp -r %S/Inputs/Umbrella/Umbrella.framework %t/Frameworks/

// Only validate path based input that rely on regex matching on unix based file systems.
; RUN: clang-installapi --target=arm64-apple-macosx13 \
; RUN: -install_name /System/Library/Frameworks/Umbrella2.framework/Versions/A/Umbrella \
; RUN: -ObjC -F%t/Frameworks/ %t/inputs.json \
; RUN: --public-umbrella-header=%t/Frameworks/Umbrella.framework/Headers/SpecialUmbrella.h \
; RUN: -private-umbrella-header \
; RUN: %t/Frameworks/Umbrella.framework/PrivateHeaders/SpecialPrivateUmbrella.h \
; RUN: -o %t/output.tbd 2>&1 | FileCheck -allow-empty %s

; CHECK-NOT: error
; CHECK-NOT: warning

;--- inputs.json.in
{
"headers": [ {
"path" : "DSTROOT/Frameworks/Umbrella.framework/Headers/AAA.h",
"type" : "public"
},
{
"path" : "DSTROOT/Frameworks/Umbrella.framework/Headers/SpecialUmbrella.h",
"type" : "public"
},
{
"path" : "DSTROOT/Frameworks/Umbrella.framework/PrivateHeaders/AAA_Private.h",
"type" : "private"
},
{
"path" : "DSTROOT/Frameworks/Umbrella.framework/PrivateHeaders/SpecialPrivateUmbrella.h",
"type" : "private"
}],
"version": "3"
}
48 changes: 48 additions & 0 deletions clang/test/InstallAPI/umbrella-headers.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
; RUN: rm -rf %t
; RUN: split-file %s %t
; RUN: sed -e "s|DSTROOT|%/t|g" %t/inputs.json.in > %t/inputs.json
; RUN: cp -r %S/Inputs/Umbrella/Umbrella.framework %t/Frameworks/

// Check base filename matches.
; RUN: clang-installapi --target=arm64-apple-macosx13 \
; RUN: -install_name /System/Library/Frameworks/Umbrella.framework/Versions/A/Umbrella \
; RUN: -ObjC -F%t/Frameworks/ %t/inputs.json \
; RUN: --public-umbrella-header=SpecialUmbrella.h \
; RUN: --private-umbrella-header=SpecialPrivateUmbrella.h \
; RUN: -o %t/output.tbd 2>&1 | FileCheck -allow-empty %s

// Try missing umbrella header argument.
; RUN: not clang-installapi --target=arm64-apple-macosx13 \
; RUN: -install_name /System/Library/Frameworks/Umbrella.framework/Versions/A/Umbrella \
; RUN: -ObjC -F%t/Frameworks/ %t/inputs.json \
; RUN: --public-umbrella-header=Ignore.h \
; RUN: -o %t/output.tbd 2>&1 | FileCheck %s -check-prefix=ERR

; ERR: error: public umbrella header file not found in input: 'Ignore.h'

; CHECK-NOT: error
; CHECK-NOT: warning

;--- Frameworks/Umbrella.framework/Headers/Ignore.h
#error "This header should be ignored"

;--- inputs.json.in
{
"headers": [ {
"path" : "DSTROOT/Frameworks/Umbrella.framework/Headers/AAA.h",
"type" : "public"
},
{
"path" : "DSTROOT/Frameworks/Umbrella.framework/Headers/SpecialUmbrella.h",
"type" : "public"
},
{
"path" : "DSTROOT/Frameworks/Umbrella.framework/PrivateHeaders/AAA_Private.h",
"type" : "private"
},
{
"path" : "DSTROOT/Frameworks/Umbrella.framework/PrivateHeaders/SpecialPrivateUmbrella.h",
"type" : "private"
}],
"version": "3"
}
12 changes: 12 additions & 0 deletions clang/tools/clang-installapi/InstallAPIOpts.td
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,15 @@ def exclude_private_header : Separate<["-"], "exclude-private-header">,
HelpText<"Exclude private header from parsing">;
def exclude_private_header_EQ : Joined<["--"], "exclude-private-header=">,
Alias<exclude_private_header>;
def public_umbrella_header : Separate<["-"], "public-umbrella-header">,
MetaVarName<"<path>">, HelpText<"Specify the public umbrella header location">;
def public_umbrella_header_EQ : Joined<["--"], "public-umbrella-header=">,
Alias<public_umbrella_header>;
def private_umbrella_header : Separate<["-"], "private-umbrella-header">,
MetaVarName<"<path>">, HelpText<"Specify the private umbrella header location">;
def private_umbrella_header_EQ : Joined<["--"], "private-umbrella-header=">,
Alias<private_umbrella_header>;
def project_umbrella_header : Separate<["-"], "project-umbrella-header">,
MetaVarName<"<path>">, HelpText<"Specify the project umbrella header location">;
def project_umbrella_header_EQ : Joined<["--"], "project-umbrella-header=">,
Alias<project_umbrella_header>;
79 changes: 77 additions & 2 deletions clang/tools/clang-installapi/Options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,16 @@ Options::processAndFilterOutInstallAPIOptions(ArrayRef<const char *> Args) {
OPT_exclude_project_header))
return {};

// Handle umbrella headers.
if (const Arg *A = ParsedArgs.getLastArg(OPT_public_umbrella_header))
DriverOpts.PublicUmbrellaHeader = A->getValue();

if (const Arg *A = ParsedArgs.getLastArg(OPT_private_umbrella_header))
DriverOpts.PrivateUmbrellaHeader = A->getValue();

if (const Arg *A = ParsedArgs.getLastArg(OPT_project_umbrella_header))
DriverOpts.ProjectUmbrellaHeader = A->getValue();

/// Any unclaimed arguments should be forwarded to the clang driver.
std::vector<const char *> ClangDriverArgs(ParsedArgs.size());
for (const Arg *A : ParsedArgs) {
Expand Down Expand Up @@ -323,6 +333,15 @@ 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();
}

InstallAPIContext Options::createContext() {
InstallAPIContext Ctx;
Ctx.FM = FM;
Expand All @@ -339,6 +358,11 @@ InstallAPIContext Options::createContext() {
Ctx.OutputLoc = DriverOpts.OutputPath;
Ctx.LangMode = FEOpts.LangMode;

// Attempt to find umbrella headers by capturing framework name.
StringRef FrameworkName;
if (!LinkerOpts.IsDylib)
FrameworkName = getFrameworkNameFromInstallName(LinkerOpts.InstallName);

// Process inputs.
for (const std::string &ListPath : DriverOpts.FileLists) {
auto Buffer = FM->getBufferForFile(ListPath);
Expand All @@ -357,8 +381,7 @@ InstallAPIContext Options::createContext() {
assert(Type != HeaderType::Unknown && "Missing header type.");
for (const StringRef Path : Headers) {
if (!FM->getOptionalFileRef(Path)) {
Diags->Report(diag::err_no_such_header_file)
<< Path << (unsigned)Type - 1;
Diags->Report(diag::err_no_such_header_file) << Path << (unsigned)Type;
return false;
}
SmallString<PATH_MAX> FullPath(Path);
Expand All @@ -382,6 +405,7 @@ InstallAPIContext Options::createContext() {
std::vector<std::unique_ptr<HeaderGlob>> ExcludedHeaderGlobs;
std::set<FileEntryRef> ExcludedHeaderFiles;
auto ParseGlobs = [&](const PathSeq &Paths, HeaderType Type) {
assert(Type != HeaderType::Unknown && "Missing header type.");
for (const StringRef Path : Paths) {
auto Glob = HeaderGlob::create(Path, Type);
if (Glob)
Expand Down Expand Up @@ -424,6 +448,57 @@ InstallAPIContext Options::createContext() {
if (!Glob->didMatch())
Diags->Report(diag::warn_glob_did_not_match) << Glob->str();

// Mark any explicit or inferred umbrella headers. If one exists, move
// that to the beginning of the input headers.
auto MarkandMoveUmbrellaInHeaders = [&](llvm::Regex &Regex,
HeaderType Type) -> bool {
auto It = find_if(Ctx.InputHeaders, [&Regex, Type](const HeaderFile &H) {
return (H.getType() == Type) && Regex.match(H.getPath());
});

if (It == Ctx.InputHeaders.end())
return false;
It->setUmbrellaHeader();

// Because there can be an umbrella header per header type,
// find the first non umbrella header to swap position with.
auto BeginPos = find_if(Ctx.InputHeaders, [](const HeaderFile &H) {
return !H.isUmbrellaHeader();
});
if (BeginPos != Ctx.InputHeaders.end() && BeginPos < It)
std::swap(*BeginPos, *It);
return true;
};

auto FindUmbrellaHeader = [&](StringRef HeaderPath, HeaderType Type) -> bool {
assert(Type != HeaderType::Unknown && "Missing header type.");
if (!HeaderPath.empty()) {
auto EscapedString = Regex::escape(HeaderPath);
Regex UmbrellaRegex(EscapedString);
if (!MarkandMoveUmbrellaInHeaders(UmbrellaRegex, Type)) {
Diags->Report(diag::err_no_such_umbrella_header_file)
<< HeaderPath << (unsigned)Type;
return false;
}
} else if (!FrameworkName.empty() && (Type != HeaderType::Project)) {
auto UmbrellaName = "/" + Regex::escape(FrameworkName);
if (Type == HeaderType::Public)
UmbrellaName += "\\.h";
else
UmbrellaName += "[_]?Private\\.h";
Regex UmbrellaRegex(UmbrellaName);
MarkandMoveUmbrellaInHeaders(UmbrellaRegex, Type);
}
return true;
};
if (!FindUmbrellaHeader(DriverOpts.PublicUmbrellaHeader,
HeaderType::Public) ||
!FindUmbrellaHeader(DriverOpts.PrivateUmbrellaHeader,
HeaderType::Private) ||
!FindUmbrellaHeader(DriverOpts.ProjectUmbrellaHeader,
HeaderType::Project))
return Ctx;

// Parse binary dylib and initialize verifier.
if (DriverOpts.DylibToVerify.empty()) {
Ctx.Verifier = std::make_unique<DylibVerifier>();
Expand Down
9 changes: 9 additions & 0 deletions clang/tools/clang-installapi/Options.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ struct DriverOptions {
/// \brief Path to input file lists (JSON).
llvm::MachO::PathSeq FileLists;

/// \brief Path to public umbrella header.
std::string PublicUmbrellaHeader;

/// \brief Path to private umbrella header.
std::string PrivateUmbrellaHeader;

/// \brief Path to project umbrella header.
std::string ProjectUmbrellaHeader;

/// \brief Paths of extra public headers.
PathSeq ExtraPublicHeaders;

Expand Down

0 comments on commit 6ad1cf3

Please sign in to comment.