| //===-- 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/Driver.h" | |
| #include "llvm/Support/Error.h" | |
| #include "llvm/Support/FileSystem.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 <cstdlib> | |
| #if !defined(_MSC_VER) && !defined(__MINGW32__) | |
| #include <unistd.h> | |
| #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_CODE | |
| #include "TapiOpts.inc" | |
| class TAPIOptTable : public opt::OptTable { | |
| public: | |
| TAPIOptTable() : opt::OptTable(optionTables()) { | |
| setGroupedShortOptions(true); | |
| } | |
| }; | |
| struct StubOptions { | |
| bool DeleteInput = false; | |
| bool DeletePrivate = false; | |
| bool TraceLibs = false; | |
| }; | |
| struct CompareOptions { | |
| ArchitectureSet ArchsToIgnore; | |
| }; | |
| struct Context { | |
| std::vector<std::string> Inputs; | |
| StubOptions StubOpt; | |
| CompareOptions CmpOpt; | |
| std::unique_ptr<llvm::raw_fd_stream> 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<char> &Output) { | |
| #if !defined(_MSC_VER) && !defined(__MINGW32__) | |
| Output.clear(); | |
| if (Path.isTriviallyEmpty()) | |
| return; | |
| SmallString<PATH_MAX> Storage; | |
| auto P = Path.toNullTerminatedStringRef(Storage); | |
| SmallString<PATH_MAX> 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<InterfaceFile> | |
| getInterfaceFile(const StringRef Filename, bool ResetBanner = true) { | |
| ExitOnErr.setBanner(TOOLNAME + ": error: '" + Filename.str() + "' "); | |
| ErrorOr<std::unique_ptr<MemoryBuffer>> BufferOrErr = | |
| MemoryBuffer::getFile(Filename, /*IsText=*/true); | |
| if (BufferOrErr.getError()) | |
| ExitOnErr(errorCodeToError(BufferOrErr.getError())); | |
| auto Buffer = std::move(*BufferOrErr); | |
| std::unique_ptr<InterfaceFile> 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<InterfaceFile> 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<InterfaceFile> 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<InterfaceFile> 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<PATH_MAX> OutputLoc = IF->getPath(); | |
| replace_extension(OutputLoc, ".tbd"); | |
| Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(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<std::vector<SymLink>> SymLinks; | |
| StringMap<std::unique_ptr<InterfaceFile>> Dylibs; | |
| StringMap<std::string> OriginalNames; | |
| std::set<std::pair<std::string, bool>> 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<PATH_MAX> 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<PATH_MAX> LinkSrc = Path; | |
| SmallString<PATH_MAX> 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<PATH_MAX> 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<PATH_MAX> 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<PATH_MAX> LinkSrc(SymInfo.SrcPath); | |
| SmallString<PATH_MAX> 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<llvm::Expected<std::unique_ptr<InterfaceFile>>( | |
| 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 <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 llvm_readtapi_main(int Argc, char **Argv, const llvm::ToolContext &) { | |
| 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 <command> [-arch <architecture> " | |
| "<options>]* <inputs> [-o " | |
| "<output>]*", | |
| "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<llvm::raw_fd_stream>(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<opt::Arg *, 1> 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; | |
| } |