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