582 lines · cpp
1//===-- llvm-readtapi.cpp - tapi file reader and transformer -----*- C++-*-===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8//9// This file defines the command-line driver for llvm-readtapi.10//11//===----------------------------------------------------------------------===//12#include "DiffEngine.h"13#include "llvm/BinaryFormat/Magic.h"14#include "llvm/Option/Arg.h"15#include "llvm/Option/ArgList.h"16#include "llvm/Option/Option.h"17#include "llvm/Support/CommandLine.h"18#include "llvm/Support/Error.h"19#include "llvm/Support/FileSystem.h"20#include "llvm/Support/InitLLVM.h"21#include "llvm/Support/MemoryBuffer.h"22#include "llvm/Support/Path.h"23#include "llvm/Support/raw_ostream.h"24#include "llvm/TextAPI/DylibReader.h"25#include "llvm/TextAPI/TextAPIError.h"26#include "llvm/TextAPI/TextAPIReader.h"27#include "llvm/TextAPI/TextAPIWriter.h"28#include "llvm/TextAPI/Utils.h"29#include <cstdlib>30 31#if !defined(_MSC_VER) && !defined(__MINGW32__)32#include <unistd.h>33#endif34 35using namespace llvm;36using namespace MachO;37using namespace object;38 39namespace {40using namespace llvm::opt;41enum ID {42 OPT_INVALID = 0, // This is not an option ID.43#define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__),44#include "TapiOpts.inc"45#undef OPTION46};47 48#define OPTTABLE_STR_TABLE_CODE49#include "TapiOpts.inc"50#undef OPTTABLE_STR_TABLE_CODE51 52#define OPTTABLE_PREFIXES_TABLE_CODE53#include "TapiOpts.inc"54#undef OPTTABLE_PREFIXES_TABLE_CODE55 56static constexpr opt::OptTable::Info InfoTable[] = {57#define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__),58#include "TapiOpts.inc"59#undef OPTION60};61 62class TAPIOptTable : public opt::GenericOptTable {63public:64 TAPIOptTable()65 : opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable) {66 setGroupedShortOptions(true);67 }68};69 70struct StubOptions {71 bool DeleteInput = false;72 bool DeletePrivate = false;73 bool TraceLibs = false;74};75 76struct CompareOptions {77 ArchitectureSet ArchsToIgnore;78};79 80struct Context {81 std::vector<std::string> Inputs;82 StubOptions StubOpt;83 CompareOptions CmpOpt;84 std::unique_ptr<llvm::raw_fd_stream> OutStream;85 FileType WriteFT = FileType::TBD_V5;86 bool Compact = false;87 Architecture Arch = AK_unknown;88};89 90// Use unique exit code to differentiate failures not directly caused from91// TextAPI operations. This is used for wrapping `compare` operations in92// automation and scripting.93const int NON_TAPI_EXIT_CODE = 2;94const std::string TOOLNAME = "llvm-readtapi";95ExitOnError ExitOnErr;96} // anonymous namespace97 98// Handle error reporting in cases where `ExitOnError` is not used.99static void reportError(Twine Message, int ExitCode = EXIT_FAILURE) {100 errs() << TOOLNAME << ": error: " << Message << "\n";101 errs().flush();102 exit(ExitCode);103}104 105// Handle warnings.106static void reportWarning(Twine Message) {107 errs() << TOOLNAME << ": warning: " << Message << "\n";108}109 110/// Get what the symlink points to.111/// This is a no-op on windows as it references POSIX level apis.112static void read_link(const Twine &Path, SmallVectorImpl<char> &Output) {113#if !defined(_MSC_VER) && !defined(__MINGW32__)114 Output.clear();115 if (Path.isTriviallyEmpty())116 return;117 118 SmallString<PATH_MAX> Storage;119 auto P = Path.toNullTerminatedStringRef(Storage);120 SmallString<PATH_MAX> Result;121 ssize_t Len;122 if ((Len = ::readlink(P.data(), Result.data(), PATH_MAX)) == -1)123 reportError("unable to read symlink: " + Path);124 Result.resize_for_overwrite(Len);125 Output.swap(Result);126#else127 reportError("unable to read symlink on windows: " + Path);128#endif129}130 131static std::unique_ptr<InterfaceFile>132getInterfaceFile(const StringRef Filename, bool ResetBanner = true) {133 ExitOnErr.setBanner(TOOLNAME + ": error: '" + Filename.str() + "' ");134 ErrorOr<std::unique_ptr<MemoryBuffer>> BufferOrErr =135 MemoryBuffer::getFile(Filename, /*IsText=*/true);136 if (BufferOrErr.getError())137 ExitOnErr(errorCodeToError(BufferOrErr.getError()));138 auto Buffer = std::move(*BufferOrErr);139 140 std::unique_ptr<InterfaceFile> IF;141 switch (identify_magic(Buffer->getBuffer())) {142 case file_magic::macho_dynamically_linked_shared_lib:143 case file_magic::macho_dynamically_linked_shared_lib_stub:144 case file_magic::macho_universal_binary:145 IF = ExitOnErr(DylibReader::get(Buffer->getMemBufferRef()));146 break;147 case file_magic::tapi_file:148 IF = ExitOnErr(TextAPIReader::get(Buffer->getMemBufferRef()));149 break;150 default:151 reportError(Filename + ": unsupported file type");152 }153 154 if (ResetBanner)155 ExitOnErr.setBanner(TOOLNAME + ": error: ");156 return IF;157}158 159static bool handleCompareAction(const Context &Ctx) {160 if (Ctx.Inputs.size() != 2)161 reportError("compare only supports two input files",162 /*ExitCode=*/NON_TAPI_EXIT_CODE);163 164 // Override default exit code.165 ExitOnErr = ExitOnError(TOOLNAME + ": error: ",166 /*DefaultErrorExitCode=*/NON_TAPI_EXIT_CODE);167 auto LeftIF = getInterfaceFile(Ctx.Inputs.front());168 auto RightIF = getInterfaceFile(Ctx.Inputs.at(1));169 170 // Remove all architectures to ignore before running comparison.171 auto removeArchFromIF = [](auto &IF, const ArchitectureSet &ArchSet,172 const Architecture ArchToRemove) {173 if (!ArchSet.has(ArchToRemove))174 return;175 if (ArchSet.count() == 1)176 return;177 auto OutIF = IF->remove(ArchToRemove);178 if (!OutIF)179 ExitOnErr(OutIF.takeError());180 IF = std::move(*OutIF);181 };182 183 if (!Ctx.CmpOpt.ArchsToIgnore.empty()) {184 const ArchitectureSet LeftArchs = LeftIF->getArchitectures();185 const ArchitectureSet RightArchs = RightIF->getArchitectures();186 for (const auto Arch : Ctx.CmpOpt.ArchsToIgnore) {187 removeArchFromIF(LeftIF, LeftArchs, Arch);188 removeArchFromIF(RightIF, RightArchs, Arch);189 }190 }191 192 raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs();193 return DiffEngine(LeftIF.get(), RightIF.get()).compareFiles(OS);194}195 196static bool handleWriteAction(const Context &Ctx,197 std::unique_ptr<InterfaceFile> Out = nullptr) {198 if (!Out) {199 if (Ctx.Inputs.size() != 1)200 reportError("write only supports one input file");201 Out = getInterfaceFile(Ctx.Inputs.front());202 }203 raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs();204 ExitOnErr(TextAPIWriter::writeToStream(OS, *Out, Ctx.WriteFT, Ctx.Compact));205 return EXIT_SUCCESS;206}207 208static bool handleMergeAction(const Context &Ctx) {209 if (Ctx.Inputs.size() < 2)210 reportError("merge requires at least two input files");211 212 std::unique_ptr<InterfaceFile> Out;213 for (StringRef FileName : Ctx.Inputs) {214 auto IF = getInterfaceFile(FileName);215 // On the first iteration copy the input file and skip merge.216 if (!Out) {217 Out = std::move(IF);218 continue;219 }220 Out = ExitOnErr(Out->merge(IF.get()));221 }222 return handleWriteAction(Ctx, std::move(Out));223}224 225static void stubifyImpl(std::unique_ptr<InterfaceFile> IF, Context &Ctx) {226 // TODO: Add inlining and magic merge support.227 if (Ctx.OutStream == nullptr) {228 std::error_code EC;229 assert(!IF->getPath().empty() && "Unknown output location");230 SmallString<PATH_MAX> OutputLoc = IF->getPath();231 replace_extension(OutputLoc, ".tbd");232 Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(OutputLoc, EC);233 if (EC)234 reportError("opening file '" + OutputLoc + ": " + EC.message());235 }236 237 handleWriteAction(Ctx, std::move(IF));238 // Clear out output stream after file has been written incase more files are239 // stubifed.240 Ctx.OutStream = nullptr;241}242 243static void stubifyDirectory(const StringRef InputPath, Context &Ctx) {244 assert(InputPath.back() != '/' && "Unexpected / at end of input path.");245 StringMap<std::vector<SymLink>> SymLinks;246 StringMap<std::unique_ptr<InterfaceFile>> Dylibs;247 StringMap<std::string> OriginalNames;248 std::set<std::pair<std::string, bool>> LibsToDelete;249 250 std::error_code EC;251 for (sys::fs::recursive_directory_iterator IT(InputPath, EC), IE; IT != IE;252 IT.increment(EC)) {253 if (EC == std::errc::no_such_file_or_directory) {254 reportWarning(IT->path() + ": " + EC.message());255 continue;256 }257 if (EC)258 reportError(IT->path() + ": " + EC.message());259 260 // Skip header directories (include/Headers/PrivateHeaders).261 StringRef Path = IT->path();262 if (sys::fs::is_directory(Path)) {263 const StringRef Stem = sys::path::stem(Path);264 if ((Stem == "include") || (Stem == "Headers") ||265 (Stem == "PrivateHeaders") || (Stem == "Modules")) {266 IT.no_push();267 continue;268 }269 }270 271 // Skip module files too.272 if (Path.ends_with(".map") || Path.ends_with(".modulemap"))273 continue;274 275 // Check if the entry is a symlink. We don't follow symlinks but we record276 // their content.277 bool IsSymLink;278 if (auto EC = sys::fs::is_symlink_file(Path, IsSymLink))279 reportError(Path + ": " + EC.message());280 281 if (IsSymLink) {282 IT.no_push();283 284 bool ShouldSkip;285 auto SymLinkEC = shouldSkipSymLink(Path, ShouldSkip);286 287 // If symlink is broken, for some reason, we should continue288 // trying to repair it before quitting.289 if (!SymLinkEC && ShouldSkip)290 continue;291 292 if (Ctx.StubOpt.DeletePrivate &&293 isPrivateLibrary(Path.drop_front(InputPath.size()), true)) {294 LibsToDelete.emplace(Path, false);295 continue;296 }297 298 SmallString<PATH_MAX> SymPath;299 read_link(Path, SymPath);300 // Sometimes there are broken symlinks that are absolute paths, which are301 // invalid during build time, but would be correct during runtime. In the302 // case of an absolute path we should check first if the path exists with303 // the known locations as prefix.304 SmallString<PATH_MAX> LinkSrc = Path;305 SmallString<PATH_MAX> LinkTarget;306 if (sys::path::is_absolute(SymPath)) {307 LinkTarget = InputPath;308 sys::path::append(LinkTarget, SymPath);309 310 // TODO: Investigate supporting a file manager for file system accesses.311 if (sys::fs::exists(LinkTarget)) {312 // Convert the absolute path to an relative path.313 if (auto ec = MachO::make_relative(LinkSrc, LinkTarget, SymPath))314 reportError(LinkTarget + ": " + EC.message());315 } else if (!sys::fs::exists(SymPath)) {316 reportWarning("ignoring broken symlink: " + Path);317 continue;318 } else {319 LinkTarget = SymPath;320 }321 } else {322 LinkTarget = LinkSrc;323 sys::path::remove_filename(LinkTarget);324 sys::path::append(LinkTarget, SymPath);325 }326 327 // For Apple SDKs, the symlink src is guaranteed to be a canonical path328 // because we don't follow symlinks when scanning. The symlink target is329 // constructed from the symlink path and needs to be canonicalized.330 if (auto ec = sys::fs::real_path(Twine(LinkTarget), LinkTarget)) {331 reportWarning(LinkTarget + ": " + ec.message());332 continue;333 }334 335 SymLinks[LinkTarget.c_str()].emplace_back(LinkSrc.str(),336 std::string(SymPath.str()));337 338 continue;339 }340 341 bool IsDirectory = false;342 if (auto EC = sys::fs::is_directory(Path, IsDirectory))343 reportError(Path + ": " + EC.message());344 if (IsDirectory)345 continue;346 347 if (Ctx.StubOpt.DeletePrivate &&348 isPrivateLibrary(Path.drop_front(InputPath.size()))) {349 IT.no_push();350 LibsToDelete.emplace(Path, false);351 continue;352 }353 auto IF = getInterfaceFile(Path);354 if (Ctx.StubOpt.TraceLibs)355 errs() << Path << "\n";356 357 // Normalize path for map lookup by removing the extension.358 SmallString<PATH_MAX> NormalizedPath(Path);359 replace_extension(NormalizedPath, "");360 361 auto [It, Inserted] = Dylibs.try_emplace(NormalizedPath.str());362 363 if ((IF->getFileType() == FileType::MachO_DynamicLibrary) ||364 (IF->getFileType() == FileType::MachO_DynamicLibrary_Stub)) {365 OriginalNames[NormalizedPath.c_str()] = IF->getPath();366 367 // Don't add this MachO dynamic library because we already have a368 // text-based stub recorded for this path.369 if (!Inserted)370 continue;371 }372 373 It->second = std::move(IF);374 }375 376 for (auto &Lib : Dylibs) {377 auto &Dylib = Lib.second;378 // Get the original file name.379 SmallString<PATH_MAX> NormalizedPath(Dylib->getPath());380 stubifyImpl(std::move(Dylib), Ctx);381 382 replace_extension(NormalizedPath, "");383 auto Found = OriginalNames.find(NormalizedPath.c_str());384 if (Found == OriginalNames.end())385 continue;386 387 if (Ctx.StubOpt.DeleteInput)388 LibsToDelete.emplace(Found->second, true);389 390 // Don't allow for more than 20 levels of symlinks when searching for391 // libraries to stubify.392 StringRef LibToCheck = Found->second;393 for (int i = 0; i < 20; ++i) {394 auto LinkIt = SymLinks.find(LibToCheck);395 if (LinkIt != SymLinks.end()) {396 for (auto &SymInfo : LinkIt->second) {397 SmallString<PATH_MAX> LinkSrc(SymInfo.SrcPath);398 SmallString<PATH_MAX> LinkTarget(SymInfo.LinkContent);399 replace_extension(LinkSrc, "tbd");400 replace_extension(LinkTarget, "tbd");401 402 if (auto EC = sys::fs::remove(LinkSrc))403 reportError(LinkSrc + " : " + EC.message());404 405 if (auto EC = sys::fs::create_link(LinkTarget, LinkSrc))406 reportError(LinkTarget + " : " + EC.message());407 408 if (Ctx.StubOpt.DeleteInput)409 LibsToDelete.emplace(SymInfo.SrcPath, true);410 411 LibToCheck = SymInfo.SrcPath;412 }413 } else414 break;415 }416 }417 418 // Recursively delete the directories. This will abort when they are not empty419 // or we reach the root of the SDK.420 for (const auto &[LibPath, IsInput] : LibsToDelete) {421 if (!IsInput && SymLinks.count(LibPath))422 continue;423 424 if (auto EC = sys::fs::remove(LibPath))425 reportError(LibPath + " : " + EC.message());426 427 std::error_code EC;428 auto Dir = sys::path::parent_path(LibPath);429 do {430 EC = sys::fs::remove(Dir);431 Dir = sys::path::parent_path(Dir);432 if (!Dir.starts_with(InputPath))433 break;434 } while (!EC);435 }436}437 438static bool handleStubifyAction(Context &Ctx) {439 if (Ctx.Inputs.empty())440 reportError("stubify requires at least one input file");441 442 if ((Ctx.Inputs.size() > 1) && (Ctx.OutStream != nullptr))443 reportError("cannot write multiple inputs into single output file");444 445 for (StringRef PathName : Ctx.Inputs) {446 bool IsDirectory = false;447 if (auto EC = sys::fs::is_directory(PathName, IsDirectory))448 reportError(PathName + ": " + EC.message());449 450 if (IsDirectory) {451 if (Ctx.OutStream != nullptr)452 reportError("cannot stubify directory'" + PathName +453 "' into single output file");454 stubifyDirectory(PathName, Ctx);455 continue;456 }457 458 stubifyImpl(getInterfaceFile(PathName), Ctx);459 if (Ctx.StubOpt.DeleteInput)460 if (auto ec = sys::fs::remove(PathName))461 reportError("deleting file '" + PathName + ": " + ec.message());462 }463 return EXIT_SUCCESS;464}465 466using IFOperation =467 std::function<llvm::Expected<std::unique_ptr<InterfaceFile>>(468 const llvm::MachO::InterfaceFile &, Architecture)>;469static bool handleSingleFileAction(const Context &Ctx, const StringRef Action,470 IFOperation act) {471 if (Ctx.Inputs.size() != 1)472 reportError(Action + " only supports one input file");473 if (Ctx.Arch == AK_unknown)474 reportError(Action + " requires -arch <arch>");475 476 auto IF = getInterfaceFile(Ctx.Inputs.front(), /*ResetBanner=*/false);477 auto OutIF = act(*IF, Ctx.Arch);478 if (!OutIF)479 ExitOnErr(OutIF.takeError());480 481 return handleWriteAction(Ctx, std::move(*OutIF));482}483 484static void setStubOptions(opt::InputArgList &Args, StubOptions &Opt) {485 Opt.DeleteInput = Args.hasArg(OPT_delete_input);486 Opt.DeletePrivate = Args.hasArg(OPT_delete_private_libraries);487 Opt.TraceLibs = Args.hasArg(OPT_t);488}489 490int main(int Argc, char **Argv) {491 InitLLVM X(Argc, Argv);492 BumpPtrAllocator A;493 StringSaver Saver(A);494 TAPIOptTable Tbl;495 Context Ctx;496 ExitOnErr.setBanner(TOOLNAME + ": error:");497 opt::InputArgList Args = Tbl.parseArgs(498 Argc, Argv, OPT_UNKNOWN, Saver, [&](StringRef Msg) { reportError(Msg); });499 if (Args.hasArg(OPT_help)) {500 Tbl.printHelp(outs(),501 "USAGE: llvm-readtapi <command> [-arch <architecture> "502 "<options>]* <inputs> [-o "503 "<output>]*",504 "LLVM TAPI file reader and transformer");505 return EXIT_SUCCESS;506 }507 508 if (Args.hasArg(OPT_version)) {509 cl::PrintVersionMessage();510 return EXIT_SUCCESS;511 }512 513 for (opt::Arg *A : Args.filtered(OPT_INPUT))514 Ctx.Inputs.push_back(A->getValue());515 516 if (opt::Arg *A = Args.getLastArg(OPT_output_EQ)) {517 std::string OutputLoc = std::move(A->getValue());518 std::error_code EC;519 Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(OutputLoc, EC);520 if (EC)521 reportError("error opening the file '" + OutputLoc + EC.message(),522 NON_TAPI_EXIT_CODE);523 }524 525 Ctx.Compact = Args.hasArg(OPT_compact);526 527 if (opt::Arg *A = Args.getLastArg(OPT_filetype_EQ)) {528 StringRef FT = A->getValue();529 Ctx.WriteFT = TextAPIWriter::parseFileType(FT);530 if (Ctx.WriteFT < FileType::TBD_V3)531 reportError("deprecated filetype '" + FT + "' is not supported to write");532 if (Ctx.WriteFT == FileType::Invalid)533 reportError("unsupported filetype '" + FT + "'");534 }535 536 auto SanitizeArch = [&](opt::Arg *A) {537 StringRef ArchStr = A->getValue();538 auto Arch = getArchitectureFromName(ArchStr);539 if (Arch == AK_unknown)540 reportError("unsupported architecture '" + ArchStr);541 return Arch;542 };543 544 if (opt::Arg *A = Args.getLastArg(OPT_arch_EQ))545 Ctx.Arch = SanitizeArch(A);546 547 for (opt::Arg *A : Args.filtered(OPT_ignore_arch_EQ))548 Ctx.CmpOpt.ArchsToIgnore.set(SanitizeArch(A));549 550 // Handle top level and exclusive operation.551 SmallVector<opt::Arg *, 1> ActionArgs(Args.filtered(OPT_action_group));552 553 if (ActionArgs.empty())554 // If no action specified, write out tapi file in requested format.555 return handleWriteAction(Ctx);556 557 if (ActionArgs.size() > 1) {558 std::string Buf;559 raw_string_ostream OS(Buf);560 OS << "only one of the following actions can be specified:";561 for (auto *Arg : ActionArgs)562 OS << " " << Arg->getSpelling();563 reportError(OS.str());564 }565 566 switch (ActionArgs.front()->getOption().getID()) {567 case OPT_compare:568 return handleCompareAction(Ctx);569 case OPT_merge:570 return handleMergeAction(Ctx);571 case OPT_extract:572 return handleSingleFileAction(Ctx, "extract", &InterfaceFile::extract);573 case OPT_remove:574 return handleSingleFileAction(Ctx, "remove", &InterfaceFile::remove);575 case OPT_stubify:576 setStubOptions(Args, Ctx.StubOpt);577 return handleStubifyAction(Ctx);578 }579 580 return EXIT_SUCCESS;581}582