//===-- llvm-readtapi.cpp - tapi file reader and transformer -----*- 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 // //===----------------------------------------------------------------------===// // // This file defines the command-line driver for llvm-readtapi. // //===----------------------------------------------------------------------===// #include "DiffEngine.h" #include "llvm/BinaryFormat/Magic.h" #include "llvm/Option/Arg.h" #include "llvm/Option/ArgList.h" #include "llvm/Option/Option.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/InitLLVM.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/raw_ostream.h" #include "llvm/TextAPI/DylibReader.h" #include "llvm/TextAPI/TextAPIError.h" #include "llvm/TextAPI/TextAPIReader.h" #include "llvm/TextAPI/TextAPIWriter.h" #include "llvm/TextAPI/Utils.h" #include #if !defined(_MSC_VER) && !defined(__MINGW32__) #include #endif using namespace llvm; using namespace MachO; using namespace object; namespace { using namespace llvm::opt; enum ID { OPT_INVALID = 0, // This is not an option ID. #define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__), #include "TapiOpts.inc" #undef OPTION }; #define OPTTABLE_STR_TABLE_CODE #include "TapiOpts.inc" #undef OPTTABLE_STR_TABLE_CODE #define OPTTABLE_PREFIXES_TABLE_CODE #include "TapiOpts.inc" #undef OPTTABLE_PREFIXES_TABLE_CODE static constexpr opt::OptTable::Info InfoTable[] = { #define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__), #include "TapiOpts.inc" #undef OPTION }; class TAPIOptTable : public opt::GenericOptTable { public: TAPIOptTable() : opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable) { setGroupedShortOptions(true); } }; struct StubOptions { bool DeleteInput = false; bool DeletePrivate = false; bool TraceLibs = false; }; struct CompareOptions { ArchitectureSet ArchsToIgnore; }; struct Context { std::vector Inputs; StubOptions StubOpt; CompareOptions CmpOpt; std::unique_ptr OutStream; FileType WriteFT = FileType::TBD_V5; bool Compact = false; Architecture Arch = AK_unknown; }; // Use unique exit code to differentiate failures not directly caused from // TextAPI operations. This is used for wrapping `compare` operations in // automation and scripting. const int NON_TAPI_EXIT_CODE = 2; const std::string TOOLNAME = "llvm-readtapi"; ExitOnError ExitOnErr; } // anonymous namespace // Handle error reporting in cases where `ExitOnError` is not used. static void reportError(Twine Message, int ExitCode = EXIT_FAILURE) { errs() << TOOLNAME << ": error: " << Message << "\n"; errs().flush(); exit(ExitCode); } // Handle warnings. static void reportWarning(Twine Message) { errs() << TOOLNAME << ": warning: " << Message << "\n"; } /// Get what the symlink points to. /// This is a no-op on windows as it references POSIX level apis. static void read_link(const Twine &Path, SmallVectorImpl &Output) { #if !defined(_MSC_VER) && !defined(__MINGW32__) Output.clear(); if (Path.isTriviallyEmpty()) return; SmallString Storage; auto P = Path.toNullTerminatedStringRef(Storage); SmallString Result; ssize_t Len; if ((Len = ::readlink(P.data(), Result.data(), PATH_MAX)) == -1) reportError("unable to read symlink: " + Path); Result.resize_for_overwrite(Len); Output.swap(Result); #else reportError("unable to read symlink on windows: " + Path); #endif } static std::unique_ptr getInterfaceFile(const StringRef Filename, bool ResetBanner = true) { ExitOnErr.setBanner(TOOLNAME + ": error: '" + Filename.str() + "' "); ErrorOr> BufferOrErr = MemoryBuffer::getFile(Filename, /*IsText=*/true); if (BufferOrErr.getError()) ExitOnErr(errorCodeToError(BufferOrErr.getError())); auto Buffer = std::move(*BufferOrErr); std::unique_ptr IF; switch (identify_magic(Buffer->getBuffer())) { case file_magic::macho_dynamically_linked_shared_lib: case file_magic::macho_dynamically_linked_shared_lib_stub: case file_magic::macho_universal_binary: IF = ExitOnErr(DylibReader::get(Buffer->getMemBufferRef())); break; case file_magic::tapi_file: IF = ExitOnErr(TextAPIReader::get(Buffer->getMemBufferRef())); break; default: reportError(Filename + ": unsupported file type"); } if (ResetBanner) ExitOnErr.setBanner(TOOLNAME + ": error: "); return IF; } static bool handleCompareAction(const Context &Ctx) { if (Ctx.Inputs.size() != 2) reportError("compare only supports two input files", /*ExitCode=*/NON_TAPI_EXIT_CODE); // Override default exit code. ExitOnErr = ExitOnError(TOOLNAME + ": error: ", /*DefaultErrorExitCode=*/NON_TAPI_EXIT_CODE); auto LeftIF = getInterfaceFile(Ctx.Inputs.front()); auto RightIF = getInterfaceFile(Ctx.Inputs.at(1)); // Remove all architectures to ignore before running comparison. auto removeArchFromIF = [](auto &IF, const ArchitectureSet &ArchSet, const Architecture ArchToRemove) { if (!ArchSet.has(ArchToRemove)) return; if (ArchSet.count() == 1) return; auto OutIF = IF->remove(ArchToRemove); if (!OutIF) ExitOnErr(OutIF.takeError()); IF = std::move(*OutIF); }; if (!Ctx.CmpOpt.ArchsToIgnore.empty()) { const ArchitectureSet LeftArchs = LeftIF->getArchitectures(); const ArchitectureSet RightArchs = RightIF->getArchitectures(); for (const auto Arch : Ctx.CmpOpt.ArchsToIgnore) { removeArchFromIF(LeftIF, LeftArchs, Arch); removeArchFromIF(RightIF, RightArchs, Arch); } } raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs(); return DiffEngine(LeftIF.get(), RightIF.get()).compareFiles(OS); } static bool handleWriteAction(const Context &Ctx, std::unique_ptr Out = nullptr) { if (!Out) { if (Ctx.Inputs.size() != 1) reportError("write only supports one input file"); Out = getInterfaceFile(Ctx.Inputs.front()); } raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs(); ExitOnErr(TextAPIWriter::writeToStream(OS, *Out, Ctx.WriteFT, Ctx.Compact)); return EXIT_SUCCESS; } static bool handleMergeAction(const Context &Ctx) { if (Ctx.Inputs.size() < 2) reportError("merge requires at least two input files"); std::unique_ptr Out; for (StringRef FileName : Ctx.Inputs) { auto IF = getInterfaceFile(FileName); // On the first iteration copy the input file and skip merge. if (!Out) { Out = std::move(IF); continue; } Out = ExitOnErr(Out->merge(IF.get())); } return handleWriteAction(Ctx, std::move(Out)); } static void stubifyImpl(std::unique_ptr IF, Context &Ctx) { // TODO: Add inlining and magic merge support. if (Ctx.OutStream == nullptr) { std::error_code EC; assert(!IF->getPath().empty() && "Unknown output location"); SmallString OutputLoc = IF->getPath(); replace_extension(OutputLoc, ".tbd"); Ctx.OutStream = std::make_unique(OutputLoc, EC); if (EC) reportError("opening file '" + OutputLoc + ": " + EC.message()); } handleWriteAction(Ctx, std::move(IF)); // Clear out output stream after file has been written incase more files are // stubifed. Ctx.OutStream = nullptr; } static void stubifyDirectory(const StringRef InputPath, Context &Ctx) { assert(InputPath.back() != '/' && "Unexpected / at end of input path."); StringMap> SymLinks; StringMap> Dylibs; StringMap OriginalNames; std::set> LibsToDelete; std::error_code EC; for (sys::fs::recursive_directory_iterator IT(InputPath, EC), IE; IT != IE; IT.increment(EC)) { if (EC == std::errc::no_such_file_or_directory) { reportWarning(IT->path() + ": " + EC.message()); continue; } if (EC) reportError(IT->path() + ": " + EC.message()); // Skip header directories (include/Headers/PrivateHeaders). StringRef Path = IT->path(); if (sys::fs::is_directory(Path)) { const StringRef Stem = sys::path::stem(Path); if ((Stem == "include") || (Stem == "Headers") || (Stem == "PrivateHeaders") || (Stem == "Modules")) { IT.no_push(); continue; } } // Skip module files too. if (Path.ends_with(".map") || Path.ends_with(".modulemap")) continue; // Check if the entry is a symlink. We don't follow symlinks but we record // their content. bool IsSymLink; if (auto EC = sys::fs::is_symlink_file(Path, IsSymLink)) reportError(Path + ": " + EC.message()); if (IsSymLink) { IT.no_push(); bool ShouldSkip; auto SymLinkEC = shouldSkipSymLink(Path, ShouldSkip); // If symlink is broken, for some reason, we should continue // trying to repair it before quitting. if (!SymLinkEC && ShouldSkip) continue; if (Ctx.StubOpt.DeletePrivate && isPrivateLibrary(Path.drop_front(InputPath.size()), true)) { LibsToDelete.emplace(Path, false); continue; } SmallString SymPath; read_link(Path, SymPath); // Sometimes there are broken symlinks that are absolute paths, which are // invalid during build time, but would be correct during runtime. In the // case of an absolute path we should check first if the path exists with // the known locations as prefix. SmallString LinkSrc = Path; SmallString LinkTarget; if (sys::path::is_absolute(SymPath)) { LinkTarget = InputPath; sys::path::append(LinkTarget, SymPath); // TODO: Investigate supporting a file manager for file system accesses. if (sys::fs::exists(LinkTarget)) { // Convert the absolute path to an relative path. if (auto ec = MachO::make_relative(LinkSrc, LinkTarget, SymPath)) reportError(LinkTarget + ": " + EC.message()); } else if (!sys::fs::exists(SymPath)) { reportWarning("ignoring broken symlink: " + Path); continue; } else { LinkTarget = SymPath; } } else { LinkTarget = LinkSrc; sys::path::remove_filename(LinkTarget); sys::path::append(LinkTarget, SymPath); } // For Apple SDKs, the symlink src is guaranteed to be a canonical path // because we don't follow symlinks when scanning. The symlink target is // constructed from the symlink path and needs to be canonicalized. if (auto ec = sys::fs::real_path(Twine(LinkTarget), LinkTarget)) { reportWarning(LinkTarget + ": " + ec.message()); continue; } SymLinks[LinkTarget.c_str()].emplace_back(LinkSrc.str(), std::string(SymPath.str())); continue; } bool IsDirectory = false; if (auto EC = sys::fs::is_directory(Path, IsDirectory)) reportError(Path + ": " + EC.message()); if (IsDirectory) continue; if (Ctx.StubOpt.DeletePrivate && isPrivateLibrary(Path.drop_front(InputPath.size()))) { IT.no_push(); LibsToDelete.emplace(Path, false); continue; } auto IF = getInterfaceFile(Path); if (Ctx.StubOpt.TraceLibs) errs() << Path << "\n"; // Normalize path for map lookup by removing the extension. SmallString NormalizedPath(Path); replace_extension(NormalizedPath, ""); auto [It, Inserted] = Dylibs.try_emplace(NormalizedPath.str()); if ((IF->getFileType() == FileType::MachO_DynamicLibrary) || (IF->getFileType() == FileType::MachO_DynamicLibrary_Stub)) { OriginalNames[NormalizedPath.c_str()] = IF->getPath(); // Don't add this MachO dynamic library because we already have a // text-based stub recorded for this path. if (!Inserted) continue; } It->second = std::move(IF); } for (auto &Lib : Dylibs) { auto &Dylib = Lib.second; // Get the original file name. SmallString NormalizedPath(Dylib->getPath()); stubifyImpl(std::move(Dylib), Ctx); replace_extension(NormalizedPath, ""); auto Found = OriginalNames.find(NormalizedPath.c_str()); if (Found == OriginalNames.end()) continue; if (Ctx.StubOpt.DeleteInput) LibsToDelete.emplace(Found->second, true); // Don't allow for more than 20 levels of symlinks when searching for // libraries to stubify. StringRef LibToCheck = Found->second; for (int i = 0; i < 20; ++i) { auto LinkIt = SymLinks.find(LibToCheck); if (LinkIt != SymLinks.end()) { for (auto &SymInfo : LinkIt->second) { SmallString LinkSrc(SymInfo.SrcPath); SmallString LinkTarget(SymInfo.LinkContent); replace_extension(LinkSrc, "tbd"); replace_extension(LinkTarget, "tbd"); if (auto EC = sys::fs::remove(LinkSrc)) reportError(LinkSrc + " : " + EC.message()); if (auto EC = sys::fs::create_link(LinkTarget, LinkSrc)) reportError(LinkTarget + " : " + EC.message()); if (Ctx.StubOpt.DeleteInput) LibsToDelete.emplace(SymInfo.SrcPath, true); LibToCheck = SymInfo.SrcPath; } } else break; } } // Recursively delete the directories. This will abort when they are not empty // or we reach the root of the SDK. for (const auto &[LibPath, IsInput] : LibsToDelete) { if (!IsInput && SymLinks.count(LibPath)) continue; if (auto EC = sys::fs::remove(LibPath)) reportError(LibPath + " : " + EC.message()); std::error_code EC; auto Dir = sys::path::parent_path(LibPath); do { EC = sys::fs::remove(Dir); Dir = sys::path::parent_path(Dir); if (!Dir.starts_with(InputPath)) break; } while (!EC); } } static bool handleStubifyAction(Context &Ctx) { if (Ctx.Inputs.empty()) reportError("stubify requires at least one input file"); if ((Ctx.Inputs.size() > 1) && (Ctx.OutStream != nullptr)) reportError("cannot write multiple inputs into single output file"); for (StringRef PathName : Ctx.Inputs) { bool IsDirectory = false; if (auto EC = sys::fs::is_directory(PathName, IsDirectory)) reportError(PathName + ": " + EC.message()); if (IsDirectory) { if (Ctx.OutStream != nullptr) reportError("cannot stubify directory'" + PathName + "' into single output file"); stubifyDirectory(PathName, Ctx); continue; } stubifyImpl(getInterfaceFile(PathName), Ctx); if (Ctx.StubOpt.DeleteInput) if (auto ec = sys::fs::remove(PathName)) reportError("deleting file '" + PathName + ": " + ec.message()); } return EXIT_SUCCESS; } using IFOperation = std::function>( const llvm::MachO::InterfaceFile &, Architecture)>; static bool handleSingleFileAction(const Context &Ctx, const StringRef Action, IFOperation act) { if (Ctx.Inputs.size() != 1) reportError(Action + " only supports one input file"); if (Ctx.Arch == AK_unknown) reportError(Action + " requires -arch "); auto IF = getInterfaceFile(Ctx.Inputs.front(), /*ResetBanner=*/false); auto OutIF = act(*IF, Ctx.Arch); if (!OutIF) ExitOnErr(OutIF.takeError()); return handleWriteAction(Ctx, std::move(*OutIF)); } static void setStubOptions(opt::InputArgList &Args, StubOptions &Opt) { Opt.DeleteInput = Args.hasArg(OPT_delete_input); Opt.DeletePrivate = Args.hasArg(OPT_delete_private_libraries); Opt.TraceLibs = Args.hasArg(OPT_t); } int main(int Argc, char **Argv) { InitLLVM X(Argc, Argv); BumpPtrAllocator A; StringSaver Saver(A); TAPIOptTable Tbl; Context Ctx; ExitOnErr.setBanner(TOOLNAME + ": error:"); opt::InputArgList Args = Tbl.parseArgs( Argc, Argv, OPT_UNKNOWN, Saver, [&](StringRef Msg) { reportError(Msg); }); if (Args.hasArg(OPT_help)) { Tbl.printHelp(outs(), "USAGE: llvm-readtapi [-arch " "]* [-o " "]*", "LLVM TAPI file reader and transformer"); return EXIT_SUCCESS; } if (Args.hasArg(OPT_version)) { cl::PrintVersionMessage(); return EXIT_SUCCESS; } for (opt::Arg *A : Args.filtered(OPT_INPUT)) Ctx.Inputs.push_back(A->getValue()); if (opt::Arg *A = Args.getLastArg(OPT_output_EQ)) { std::string OutputLoc = std::move(A->getValue()); std::error_code EC; Ctx.OutStream = std::make_unique(OutputLoc, EC); if (EC) reportError("error opening the file '" + OutputLoc + EC.message(), NON_TAPI_EXIT_CODE); } Ctx.Compact = Args.hasArg(OPT_compact); if (opt::Arg *A = Args.getLastArg(OPT_filetype_EQ)) { StringRef FT = A->getValue(); Ctx.WriteFT = TextAPIWriter::parseFileType(FT); if (Ctx.WriteFT < FileType::TBD_V3) reportError("deprecated filetype '" + FT + "' is not supported to write"); if (Ctx.WriteFT == FileType::Invalid) reportError("unsupported filetype '" + FT + "'"); } auto SanitizeArch = [&](opt::Arg *A) { StringRef ArchStr = A->getValue(); auto Arch = getArchitectureFromName(ArchStr); if (Arch == AK_unknown) reportError("unsupported architecture '" + ArchStr); return Arch; }; if (opt::Arg *A = Args.getLastArg(OPT_arch_EQ)) Ctx.Arch = SanitizeArch(A); for (opt::Arg *A : Args.filtered(OPT_ignore_arch_EQ)) Ctx.CmpOpt.ArchsToIgnore.set(SanitizeArch(A)); // Handle top level and exclusive operation. SmallVector ActionArgs(Args.filtered(OPT_action_group)); if (ActionArgs.empty()) // If no action specified, write out tapi file in requested format. return handleWriteAction(Ctx); if (ActionArgs.size() > 1) { std::string Buf; raw_string_ostream OS(Buf); OS << "only one of the following actions can be specified:"; for (auto *Arg : ActionArgs) OS << " " << Arg->getSpelling(); reportError(OS.str()); } switch (ActionArgs.front()->getOption().getID()) { case OPT_compare: return handleCompareAction(Ctx); case OPT_merge: return handleMergeAction(Ctx); case OPT_extract: return handleSingleFileAction(Ctx, "extract", &InterfaceFile::extract); case OPT_remove: return handleSingleFileAction(Ctx, "remove", &InterfaceFile::remove); case OPT_stubify: setStubOptions(Args, Ctx.StubOpt); return handleStubifyAction(Ctx); } return EXIT_SUCCESS; }