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/Error.h"
19#include "llvm/Support/FileSystem.h"
20#include "llvm/Support/LLVMDriver.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_STR_TABLE_CODE
49#include "TapiOpts.inc"
50#undef OPTTABLE_STR_TABLE_CODE
51
52#define OPTTABLE_PREFIXES_TABLE_CODE
53#include "TapiOpts.inc"
54#undef OPTTABLE_PREFIXES_TABLE_CODE
55
56static constexpr opt::OptTable::Info InfoTable[] = {
57#define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__),
58#include "TapiOpts.inc"
59#undef OPTION
60};
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 from
91// TextAPI operations. This is used for wrapping `compare` operations in
92// automation and scripting.
93const int NON_TAPI_EXIT_CODE = 2;
94const std::string TOOLNAME = "llvm-readtapi";
95ExitOnError ExitOnErr;
96} // anonymous namespace
97
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(status: 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(Out&: Storage);
120 SmallString<PATH_MAX> Result;
121 ssize_t Len;
122 if ((Len = ::readlink(path: P.data(), buf: Result.data(), PATH_MAX)) == -1)
123 reportError(Message: "unable to read symlink: " + Path);
124 Result.resize_for_overwrite(N: Len);
125 Output.swap(RHS&: Result);
126#else
127 reportError("unable to read symlink on windows: " + Path);
128#endif
129}
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(EC: BufferOrErr.getError()));
138 auto Buffer = std::move(*BufferOrErr);
139
140 std::unique_ptr<InterfaceFile> IF;
141 switch (identify_magic(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: Buffer->getMemBufferRef()));
146 break;
147 case file_magic::tapi_file:
148 IF = ExitOnErr(TextAPIReader::get(InputBuffer: Buffer->getMemBufferRef()));
149 break;
150 default:
151 reportError(Message: 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(Message: "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(Filename: Ctx.Inputs.front());
168 auto RightIF = getInterfaceFile(Filename: Ctx.Inputs.at(n: 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(Arch: 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(Message: "write only supports one input file");
201 Out = getInterfaceFile(Filename: Ctx.Inputs.front());
202 }
203 raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs();
204 ExitOnErr(TextAPIWriter::writeToStream(OS, File: *Out, FileKind: Ctx.WriteFT, Compact: Ctx.Compact));
205 return EXIT_SUCCESS;
206}
207
208static bool handleMergeAction(const Context &Ctx) {
209 if (Ctx.Inputs.size() < 2)
210 reportError(Message: "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: 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(O: IF.get()));
221 }
222 return handleWriteAction(Ctx, Out: 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(Path&: OutputLoc, Extension: ".tbd");
232 Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(args&: OutputLoc, args&: EC);
233 if (EC)
234 reportError(Message: "opening file '" + OutputLoc + ": " + EC.message());
235 }
236
237 handleWriteAction(Ctx, Out: std::move(IF));
238 // Clear out output stream after file has been written incase more files are
239 // 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&: EC)) {
253 if (EC == std::errc::no_such_file_or_directory) {
254 reportWarning(Message: IT->path() + ": " + EC.message());
255 continue;
256 }
257 if (EC)
258 reportError(Message: 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: 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(Suffix: ".map") || Path.ends_with(Suffix: ".modulemap"))
273 continue;
274
275 // Check if the entry is a symlink. We don't follow symlinks but we record
276 // their content.
277 bool IsSymLink;
278 if (auto EC = sys::fs::is_symlink_file(path: Path, result&: IsSymLink))
279 reportError(Message: Path + ": " + EC.message());
280
281 if (IsSymLink) {
282 IT.no_push();
283
284 bool ShouldSkip;
285 auto SymLinkEC = shouldSkipSymLink(Path, Result&: ShouldSkip);
286
287 // If symlink is broken, for some reason, we should continue
288 // trying to repair it before quitting.
289 if (!SymLinkEC && ShouldSkip)
290 continue;
291
292 if (Ctx.StubOpt.DeletePrivate &&
293 isPrivateLibrary(Path: Path.drop_front(N: InputPath.size()), IsSymLink: true)) {
294 LibsToDelete.emplace(args&: Path, args: false);
295 continue;
296 }
297
298 SmallString<PATH_MAX> SymPath;
299 read_link(Path, Output&: SymPath);
300 // Sometimes there are broken symlinks that are absolute paths, which are
301 // invalid during build time, but would be correct during runtime. In the
302 // case of an absolute path we should check first if the path exists with
303 // the known locations as prefix.
304 SmallString<PATH_MAX> LinkSrc = Path;
305 SmallString<PATH_MAX> LinkTarget;
306 if (sys::path::is_absolute(path: SymPath)) {
307 LinkTarget = InputPath;
308 sys::path::append(path&: LinkTarget, a: SymPath);
309
310 // TODO: Investigate supporting a file manager for file system accesses.
311 if (sys::fs::exists(Path: LinkTarget)) {
312 // Convert the absolute path to an relative path.
313 if (auto ec = MachO::make_relative(From: LinkSrc, To: LinkTarget, RelativePath&: SymPath))
314 reportError(Message: LinkTarget + ": " + EC.message());
315 } else if (!sys::fs::exists(Path: SymPath)) {
316 reportWarning(Message: "ignoring broken symlink: " + Path);
317 continue;
318 } else {
319 LinkTarget = SymPath;
320 }
321 } else {
322 LinkTarget = LinkSrc;
323 sys::path::remove_filename(path&: LinkTarget);
324 sys::path::append(path&: LinkTarget, a: SymPath);
325 }
326
327 // For Apple SDKs, the symlink src is guaranteed to be a canonical path
328 // because we don't follow symlinks when scanning. The symlink target is
329 // constructed from the symlink path and needs to be canonicalized.
330 if (auto ec = sys::fs::real_path(path: Twine(LinkTarget), output&: LinkTarget)) {
331 reportWarning(Message: LinkTarget + ": " + ec.message());
332 continue;
333 }
334
335 SymLinks[LinkTarget.c_str()].emplace_back(args: LinkSrc.str(),
336 args: std::string(SymPath.str()));
337
338 continue;
339 }
340
341 bool IsDirectory = false;
342 if (auto EC = sys::fs::is_directory(path: Path, result&: IsDirectory))
343 reportError(Message: Path + ": " + EC.message());
344 if (IsDirectory)
345 continue;
346
347 if (Ctx.StubOpt.DeletePrivate &&
348 isPrivateLibrary(Path: Path.drop_front(N: InputPath.size()))) {
349 IT.no_push();
350 LibsToDelete.emplace(args&: Path, args: false);
351 continue;
352 }
353 auto IF = getInterfaceFile(Filename: 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(Path&: NormalizedPath, Extension: "");
360
361 auto [It, Inserted] = Dylibs.try_emplace(Key: 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 a
368 // 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(IF: std::move(Dylib), Ctx);
381
382 replace_extension(Path&: NormalizedPath, Extension: "");
383 auto Found = OriginalNames.find(Key: NormalizedPath.c_str());
384 if (Found == OriginalNames.end())
385 continue;
386
387 if (Ctx.StubOpt.DeleteInput)
388 LibsToDelete.emplace(args&: Found->second, args: true);
389
390 // Don't allow for more than 20 levels of symlinks when searching for
391 // libraries to stubify.
392 StringRef LibToCheck = Found->second;
393 for (int i = 0; i < 20; ++i) {
394 auto LinkIt = SymLinks.find(Key: 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(Path&: LinkSrc, Extension: "tbd");
400 replace_extension(Path&: LinkTarget, Extension: "tbd");
401
402 if (auto EC = sys::fs::remove(path: LinkSrc))
403 reportError(Message: LinkSrc + " : " + EC.message());
404
405 if (auto EC = sys::fs::create_link(to: LinkTarget, from: LinkSrc))
406 reportError(Message: LinkTarget + " : " + EC.message());
407
408 if (Ctx.StubOpt.DeleteInput)
409 LibsToDelete.emplace(args&: SymInfo.SrcPath, args: true);
410
411 LibToCheck = SymInfo.SrcPath;
412 }
413 } else
414 break;
415 }
416 }
417
418 // Recursively delete the directories. This will abort when they are not empty
419 // or we reach the root of the SDK.
420 for (const auto &[LibPath, IsInput] : LibsToDelete) {
421 if (!IsInput && SymLinks.count(Key: LibPath))
422 continue;
423
424 if (auto EC = sys::fs::remove(path: LibPath))
425 reportError(Message: LibPath + " : " + EC.message());
426
427 std::error_code EC;
428 auto Dir = sys::path::parent_path(path: LibPath);
429 do {
430 EC = sys::fs::remove(path: Dir);
431 Dir = sys::path::parent_path(path: Dir);
432 if (!Dir.starts_with(Prefix: InputPath))
433 break;
434 } while (!EC);
435 }
436}
437
438static bool handleStubifyAction(Context &Ctx) {
439 if (Ctx.Inputs.empty())
440 reportError(Message: "stubify requires at least one input file");
441
442 if ((Ctx.Inputs.size() > 1) && (Ctx.OutStream != nullptr))
443 reportError(Message: "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(path: PathName, result&: IsDirectory))
448 reportError(Message: PathName + ": " + EC.message());
449
450 if (IsDirectory) {
451 if (Ctx.OutStream != nullptr)
452 reportError(Message: "cannot stubify directory'" + PathName +
453 "' into single output file");
454 stubifyDirectory(InputPath: PathName, Ctx);
455 continue;
456 }
457
458 stubifyImpl(IF: getInterfaceFile(Filename: PathName), Ctx);
459 if (Ctx.StubOpt.DeleteInput)
460 if (auto ec = sys::fs::remove(path: PathName))
461 reportError(Message: "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(Message: Action + " only supports one input file");
473 if (Ctx.Arch == AK_unknown)
474 reportError(Message: Action + " requires -arch <arch>");
475
476 auto IF = getInterfaceFile(Filename: Ctx.Inputs.front(), /*ResetBanner=*/false);
477 auto OutIF = act(*IF, Ctx.Arch);
478 if (!OutIF)
479 ExitOnErr(OutIF.takeError());
480
481 return handleWriteAction(Ctx, Out: std::move(*OutIF));
482}
483
484static void setStubOptions(opt::InputArgList &Args, StubOptions &Opt) {
485 Opt.DeleteInput = Args.hasArg(Ids: OPT_delete_input);
486 Opt.DeletePrivate = Args.hasArg(Ids: OPT_delete_private_libraries);
487 Opt.TraceLibs = Args.hasArg(Ids: OPT_t);
488}
489
490int llvm_readtapi_main(int Argc, char **Argv, const llvm::ToolContext &) {
491 BumpPtrAllocator A;
492 StringSaver Saver(A);
493 TAPIOptTable Tbl;
494 Context Ctx;
495 ExitOnErr.setBanner(TOOLNAME + ": error:");
496 opt::InputArgList Args = Tbl.parseArgs(
497 Argc, Argv, Unknown: OPT_UNKNOWN, Saver, ErrorFn: [&](StringRef Msg) { reportError(Message: Msg); });
498 if (Args.hasArg(Ids: OPT_help)) {
499 Tbl.printHelp(OS&: outs(),
500 Usage: "USAGE: llvm-readtapi <command> [-arch <architecture> "
501 "<options>]* <inputs> [-o "
502 "<output>]*",
503 Title: "LLVM TAPI file reader and transformer");
504 return EXIT_SUCCESS;
505 }
506
507 if (Args.hasArg(Ids: OPT_version)) {
508 cl::PrintVersionMessage();
509 return EXIT_SUCCESS;
510 }
511
512 for (opt::Arg *A : Args.filtered(Ids: OPT_INPUT))
513 Ctx.Inputs.push_back(x: A->getValue());
514
515 if (opt::Arg *A = Args.getLastArg(Ids: OPT_output_EQ)) {
516 std::string OutputLoc = std::move(A->getValue());
517 std::error_code EC;
518 Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(args&: OutputLoc, args&: EC);
519 if (EC)
520 reportError(Message: "error opening the file '" + OutputLoc + EC.message(),
521 ExitCode: NON_TAPI_EXIT_CODE);
522 }
523
524 Ctx.Compact = Args.hasArg(Ids: OPT_compact);
525
526 if (opt::Arg *A = Args.getLastArg(Ids: OPT_filetype_EQ)) {
527 StringRef FT = A->getValue();
528 Ctx.WriteFT = TextAPIWriter::parseFileType(FT);
529 if (Ctx.WriteFT < FileType::TBD_V3)
530 reportError(Message: "deprecated filetype '" + FT + "' is not supported to write");
531 if (Ctx.WriteFT == FileType::Invalid)
532 reportError(Message: "unsupported filetype '" + FT + "'");
533 }
534
535 auto SanitizeArch = [&](opt::Arg *A) {
536 StringRef ArchStr = A->getValue();
537 auto Arch = getArchitectureFromName(Name: ArchStr);
538 if (Arch == AK_unknown)
539 reportError(Message: "unsupported architecture '" + ArchStr);
540 return Arch;
541 };
542
543 if (opt::Arg *A = Args.getLastArg(Ids: OPT_arch_EQ))
544 Ctx.Arch = SanitizeArch(A);
545
546 for (opt::Arg *A : Args.filtered(Ids: OPT_ignore_arch_EQ))
547 Ctx.CmpOpt.ArchsToIgnore.set(SanitizeArch(A));
548
549 // Handle top level and exclusive operation.
550 SmallVector<opt::Arg *, 1> ActionArgs(Args.filtered(Ids: OPT_action_group));
551
552 if (ActionArgs.empty())
553 // If no action specified, write out tapi file in requested format.
554 return handleWriteAction(Ctx);
555
556 if (ActionArgs.size() > 1) {
557 std::string Buf;
558 raw_string_ostream OS(Buf);
559 OS << "only one of the following actions can be specified:";
560 for (auto *Arg : ActionArgs)
561 OS << " " << Arg->getSpelling();
562 reportError(Message: OS.str());
563 }
564
565 switch (ActionArgs.front()->getOption().getID()) {
566 case OPT_compare:
567 return handleCompareAction(Ctx);
568 case OPT_merge:
569 return handleMergeAction(Ctx);
570 case OPT_extract:
571 return handleSingleFileAction(Ctx, Action: "extract", act: &InterfaceFile::extract);
572 case OPT_remove:
573 return handleSingleFileAction(Ctx, Action: "remove", act: &InterfaceFile::remove);
574 case OPT_stubify:
575 setStubOptions(Args, Opt&: Ctx.StubOpt);
576 return handleStubifyAction(Ctx);
577 }
578
579 return EXIT_SUCCESS;
580}
581