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