xref: /llvm-project/llvm/tools/llvm-readtapi/llvm-readtapi.cpp (revision dd647e3e608ed0b2bac7c588d5859b80ef4a5976)
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 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 
56 static constexpr opt::OptTable::Info InfoTable[] = {
57 #define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__),
58 #include "TapiOpts.inc"
59 #undef OPTION
60 };
61 
62 class TAPIOptTable : public opt::GenericOptTable {
63 public:
64   TAPIOptTable()
65       : opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable) {
66     setGroupedShortOptions(true);
67   }
68 };
69 
70 struct StubOptions {
71   bool DeleteInput = false;
72   bool DeletePrivate = false;
73   bool TraceLibs = false;
74 };
75 
76 struct CompareOptions {
77   ArchitectureSet ArchsToIgnore;
78 };
79 
80 struct 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.
93 const int NON_TAPI_EXIT_CODE = 2;
94 const std::string TOOLNAME = "llvm-readtapi";
95 ExitOnError ExitOnErr;
96 } // anonymous namespace
97 
98 // Handle error reporting in cases where `ExitOnError` is not used.
99 static 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.
106 static 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.
112 static 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 #else
127   reportError("unable to read symlink on windows: " + Path);
128 #endif
129 }
130 
131 static std::unique_ptr<InterfaceFile>
132 getInterfaceFile(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 
159 static 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 
196 static 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 
208 static 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 
225 static 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 are
239   // stubifed.
240   Ctx.OutStream = nullptr;
241 }
242 
243 static 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 record
276     // 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 continue
288       // 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 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(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 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(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     if ((IF->getFileType() == FileType::MachO_DynamicLibrary) ||
362         (IF->getFileType() == FileType::MachO_DynamicLibrary_Stub)) {
363       OriginalNames[NormalizedPath.c_str()] = IF->getPath();
364 
365       // Don't add this MachO dynamic library because we already have a
366       // text-based stub recorded for this path.
367       if (Dylibs.count(NormalizedPath.c_str()))
368         continue;
369     }
370 
371     Dylibs[NormalizedPath.c_str()] = std::move(IF);
372   }
373 
374   for (auto &Lib : Dylibs) {
375     auto &Dylib = Lib.second;
376     // Get the original file name.
377     SmallString<PATH_MAX> NormalizedPath(Dylib->getPath());
378     stubifyImpl(std::move(Dylib), Ctx);
379 
380     replace_extension(NormalizedPath, "");
381     auto Found = OriginalNames.find(NormalizedPath.c_str());
382     if (Found == OriginalNames.end())
383       continue;
384 
385     if (Ctx.StubOpt.DeleteInput)
386       LibsToDelete.emplace(Found->second, true);
387 
388     // Don't allow for more than 20 levels of symlinks when searching for
389     // libraries to stubify.
390     StringRef LibToCheck = Found->second;
391     for (int i = 0; i < 20; ++i) {
392       auto LinkIt = SymLinks.find(LibToCheck);
393       if (LinkIt != SymLinks.end()) {
394         for (auto &SymInfo : LinkIt->second) {
395           SmallString<PATH_MAX> LinkSrc(SymInfo.SrcPath);
396           SmallString<PATH_MAX> LinkTarget(SymInfo.LinkContent);
397           replace_extension(LinkSrc, "tbd");
398           replace_extension(LinkTarget, "tbd");
399 
400           if (auto EC = sys::fs::remove(LinkSrc))
401             reportError(LinkSrc + " : " + EC.message());
402 
403           if (auto EC = sys::fs::create_link(LinkTarget, LinkSrc))
404             reportError(LinkTarget + " : " + EC.message());
405 
406           if (Ctx.StubOpt.DeleteInput)
407             LibsToDelete.emplace(SymInfo.SrcPath, true);
408 
409           LibToCheck = SymInfo.SrcPath;
410         }
411       } else
412         break;
413     }
414   }
415 
416   // Recursively delete the directories. This will abort when they are not empty
417   // or we reach the root of the SDK.
418   for (const auto &[LibPath, IsInput] : LibsToDelete) {
419     if (!IsInput && SymLinks.count(LibPath))
420       continue;
421 
422     if (auto EC = sys::fs::remove(LibPath))
423       reportError(LibPath + " : " + EC.message());
424 
425     std::error_code EC;
426     auto Dir = sys::path::parent_path(LibPath);
427     do {
428       EC = sys::fs::remove(Dir);
429       Dir = sys::path::parent_path(Dir);
430       if (!Dir.starts_with(InputPath))
431         break;
432     } while (!EC);
433   }
434 }
435 
436 static bool handleStubifyAction(Context &Ctx) {
437   if (Ctx.Inputs.empty())
438     reportError("stubify requires at least one input file");
439 
440   if ((Ctx.Inputs.size() > 1) && (Ctx.OutStream != nullptr))
441     reportError("cannot write multiple inputs into single output file");
442 
443   for (StringRef PathName : Ctx.Inputs) {
444     bool IsDirectory = false;
445     if (auto EC = sys::fs::is_directory(PathName, IsDirectory))
446       reportError(PathName + ": " + EC.message());
447 
448     if (IsDirectory) {
449       if (Ctx.OutStream != nullptr)
450         reportError("cannot stubify directory'" + PathName +
451                     "' into single output file");
452       stubifyDirectory(PathName, Ctx);
453       continue;
454     }
455 
456     stubifyImpl(getInterfaceFile(PathName), Ctx);
457     if (Ctx.StubOpt.DeleteInput)
458       if (auto ec = sys::fs::remove(PathName))
459         reportError("deleting file '" + PathName + ": " + ec.message());
460   }
461   return EXIT_SUCCESS;
462 }
463 
464 using IFOperation =
465     std::function<llvm::Expected<std::unique_ptr<InterfaceFile>>(
466         const llvm::MachO::InterfaceFile &, Architecture)>;
467 static bool handleSingleFileAction(const Context &Ctx, const StringRef Action,
468                                    IFOperation act) {
469   if (Ctx.Inputs.size() != 1)
470     reportError(Action + " only supports one input file");
471   if (Ctx.Arch == AK_unknown)
472     reportError(Action + " requires -arch <arch>");
473 
474   auto IF = getInterfaceFile(Ctx.Inputs.front(), /*ResetBanner=*/false);
475   auto OutIF = act(*IF, Ctx.Arch);
476   if (!OutIF)
477     ExitOnErr(OutIF.takeError());
478 
479   return handleWriteAction(Ctx, std::move(*OutIF));
480 }
481 
482 static void setStubOptions(opt::InputArgList &Args, StubOptions &Opt) {
483   Opt.DeleteInput = Args.hasArg(OPT_delete_input);
484   Opt.DeletePrivate = Args.hasArg(OPT_delete_private_libraries);
485   Opt.TraceLibs = Args.hasArg(OPT_t);
486 }
487 
488 int main(int Argc, char **Argv) {
489   InitLLVM X(Argc, Argv);
490   BumpPtrAllocator A;
491   StringSaver Saver(A);
492   TAPIOptTable Tbl;
493   Context Ctx;
494   ExitOnErr.setBanner(TOOLNAME + ": error:");
495   opt::InputArgList Args = Tbl.parseArgs(
496       Argc, Argv, OPT_UNKNOWN, Saver, [&](StringRef Msg) { reportError(Msg); });
497   if (Args.hasArg(OPT_help)) {
498     Tbl.printHelp(outs(),
499                   "USAGE: llvm-readtapi <command> [-arch <architecture> "
500                   "<options>]* <inputs> [-o "
501                   "<output>]*",
502                   "LLVM TAPI file reader and transformer");
503     return EXIT_SUCCESS;
504   }
505 
506   if (Args.hasArg(OPT_version)) {
507     cl::PrintVersionMessage();
508     return EXIT_SUCCESS;
509   }
510 
511   for (opt::Arg *A : Args.filtered(OPT_INPUT))
512     Ctx.Inputs.push_back(A->getValue());
513 
514   if (opt::Arg *A = Args.getLastArg(OPT_output_EQ)) {
515     std::string OutputLoc = std::move(A->getValue());
516     std::error_code EC;
517     Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(OutputLoc, EC);
518     if (EC)
519       reportError("error opening the file '" + OutputLoc + EC.message(),
520                   NON_TAPI_EXIT_CODE);
521   }
522 
523   Ctx.Compact = Args.hasArg(OPT_compact);
524 
525   if (opt::Arg *A = Args.getLastArg(OPT_filetype_EQ)) {
526     StringRef FT = A->getValue();
527     Ctx.WriteFT = TextAPIWriter::parseFileType(FT);
528     if (Ctx.WriteFT < FileType::TBD_V3)
529       reportError("deprecated filetype '" + FT + "' is not supported to write");
530     if (Ctx.WriteFT == FileType::Invalid)
531       reportError("unsupported filetype '" + FT + "'");
532   }
533 
534   auto SanitizeArch = [&](opt::Arg *A) {
535     StringRef ArchStr = A->getValue();
536     auto Arch = getArchitectureFromName(ArchStr);
537     if (Arch == AK_unknown)
538       reportError("unsupported architecture '" + ArchStr);
539     return Arch;
540   };
541 
542   if (opt::Arg *A = Args.getLastArg(OPT_arch_EQ))
543     Ctx.Arch = SanitizeArch(A);
544 
545   for (opt::Arg *A : Args.filtered(OPT_ignore_arch_EQ))
546     Ctx.CmpOpt.ArchsToIgnore.set(SanitizeArch(A));
547 
548   // Handle top level and exclusive operation.
549   SmallVector<opt::Arg *, 1> ActionArgs(Args.filtered(OPT_action_group));
550 
551   if (ActionArgs.empty())
552     // If no action specified, write out tapi file in requested format.
553     return handleWriteAction(Ctx);
554 
555   if (ActionArgs.size() > 1) {
556     std::string Buf;
557     raw_string_ostream OS(Buf);
558     OS << "only one of the following actions can be specified:";
559     for (auto *Arg : ActionArgs)
560       OS << " " << Arg->getSpelling();
561     reportError(OS.str());
562   }
563 
564   switch (ActionArgs.front()->getOption().getID()) {
565   case OPT_compare:
566     return handleCompareAction(Ctx);
567   case OPT_merge:
568     return handleMergeAction(Ctx);
569   case OPT_extract:
570     return handleSingleFileAction(Ctx, "extract", &InterfaceFile::extract);
571   case OPT_remove:
572     return handleSingleFileAction(Ctx, "remove", &InterfaceFile::remove);
573   case OPT_stubify:
574     setStubOptions(Args, Ctx.StubOpt);
575     return handleStubifyAction(Ctx);
576   }
577 
578   return EXIT_SUCCESS;
579 }
580