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