Skip to content

Commit

Permalink
[lld/mac] Add --reproduce option
Browse files Browse the repository at this point in the history
This adds support for ld.lld's --reproduce / lld-link's /reproduce:
flag to the MachO port. This flag can be added to a link command
to make the link write a tar file containing all inputs to the link
and a response file containing the link command. This can be used
to reproduce the link on another machine, which is useful for sharing
bug report inputs or performance test loads.

Since the linker is usually called through the clang driver and
adding linker flags can be a bit cumbersome, setting the env var
`LLD_REPRODUCE=foo.tar` triggers the feature as well.

The file response.txt in the archive can be used with
`ld64.lld.darwinnew $(cat response.txt)` as long as the contents are
smaller than the command-line limit, or with `ld64.lld.darwinnew
@response.txt` once D92149 is in.

The support in this patch is sufficient to create a tar file for
Chromium's base_unittests that can link after unpacking on a different
machine.

Differential Revision: https://reviews.llvm.org/D92274
  • Loading branch information
nico committed Nov 30, 2020
1 parent 25d54ab commit 83e60f5
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 3 deletions.
7 changes: 6 additions & 1 deletion lld/Common/Reproduce.cpp
Expand Up @@ -54,7 +54,12 @@ std::string lld::toString(const opt::Arg &arg) {
std::string k = std::string(arg.getSpelling());
if (arg.getNumValues() == 0)
return k;
std::string v = quote(arg.getValue());
std::string v;
for (size_t i = 0; i < arg.getNumValues(); ++i) {
if (i > 0)
v.push_back(' ');
v += quote(arg.getValue(i));
}
if (arg.getOption().getRenderStyle() == opt::Option::RenderJoinedStyle)
return k + v;
return k + " " + v;
Expand Down
2 changes: 1 addition & 1 deletion lld/ELF/DriverUtils.cpp
Expand Up @@ -184,7 +184,7 @@ std::string elf::createResponseFile(const opt::InputArgList &args) {
// fail because the archive we are creating doesn't contain empty
// directories for the output path (-o doesn't create directories).
// Strip directories to prevent the issue.
os << "-o " << quote(sys::path::filename(arg->getValue())) << "\n";
os << "-o " << quote(path::filename(arg->getValue())) << "\n";
break;
case OPT_dynamic_list:
case OPT_library_path:
Expand Down
21 changes: 21 additions & 0 deletions lld/MachO/Driver.cpp
Expand Up @@ -37,6 +37,7 @@
#include "llvm/Support/Host.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/TarWriter.h"
#include "llvm/Support/TargetSelect.h"

#include <algorithm>
Expand Down Expand Up @@ -548,6 +549,12 @@ static void warnIfUnimplementedOption(const opt::Option &opt) {
}
}

static const char *getReproduceOption(opt::InputArgList &args) {
if (auto *arg = args.getLastArg(OPT_reproduce))
return arg->getValue();
return getenv("LLD_REPRODUCE");
}

bool macho::link(llvm::ArrayRef<const char *> argsArr, bool canExitEarly,
raw_ostream &stdoutOS, raw_ostream &stderrOS) {
lld::stdoutOS = &stdoutOS;
Expand All @@ -569,6 +576,20 @@ bool macho::link(llvm::ArrayRef<const char *> argsArr, bool canExitEarly,
return true;
}

if (const char *path = getReproduceOption(args)) {
// Note that --reproduce is a debug option so you can ignore it
// if you are trying to understand the whole picture of the code.
Expected<std::unique_ptr<TarWriter>> errOrWriter =
TarWriter::create(path, path::stem(path));
if (errOrWriter) {
tar = std::move(*errOrWriter);
tar->append("response.txt", createResponseFile(args));
tar->append("version.txt", getLLDVersion() + "\n");
} else {
error("--reproduce: " + toString(errOrWriter.takeError()));
}
}

config = make<Configuration>();
symtab = make<SymbolTable>();
target = createTargetInfo(args);
Expand Down
2 changes: 2 additions & 0 deletions lld/MachO/Driver.h
Expand Up @@ -35,6 +35,8 @@ enum {
#undef OPTION
};

std::string createResponseFile(const llvm::opt::InputArgList &args);

// Check for both libfoo.dylib and libfoo.tbd (in that order).
llvm::Optional<std::string> resolveDylibPath(llvm::StringRef path);

Expand Down
51 changes: 51 additions & 0 deletions lld/MachO/DriverUtils.cpp
Expand Up @@ -9,8 +9,10 @@
#include "Driver.h"
#include "InputFiles.h"

#include "lld/Common/Args.h"
#include "lld/Common/ErrorHandler.h"
#include "lld/Common/Memory.h"
#include "lld/Common/Reproduce.h"
#include "llvm/Option/Arg.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Option/Option.h"
Expand Down Expand Up @@ -96,6 +98,55 @@ void MachOOptTable::printHelp(const char *argv0, bool showHidden) const {
lld::outs() << "\n";
}

static std::string rewritePath(StringRef s) {
if (fs::exists(s))
return relativeToRoot(s);
return std::string(s);
}

// Reconstructs command line arguments so that so that you can re-run
// the same command with the same inputs. This is for --reproduce.
std::string macho::createResponseFile(const opt::InputArgList &args) {
SmallString<0> data;
raw_svector_ostream os(data);

// Copy the command line to the output while rewriting paths.
for (auto *arg : args) {
switch (arg->getOption().getID()) {
case OPT_reproduce:
break;
case OPT_INPUT:
os << quote(rewritePath(arg->getValue())) << "\n";
break;
case OPT_o:
os << "-o " << quote(path::filename(arg->getValue())) << "\n";
break;
case OPT_filelist:
if (Optional<MemoryBufferRef> buffer = readFile(arg->getValue()))
for (StringRef path : args::getLines(*buffer))
os << quote(rewritePath(path)) << "\n";
break;
case OPT_force_load:
case OPT_rpath:
case OPT_syslibroot:
case OPT_F:
case OPT_L:
case OPT_order_file:
os << arg->getSpelling() << " " << quote(rewritePath(arg->getValue()))
<< "\n";
break;
case OPT_sectcreate:
os << arg->getSpelling() << " " << quote(arg->getValue(0)) << " "
<< quote(arg->getValue(1)) << " "
<< quote(rewritePath(arg->getValue(2))) << "\n";
break;
default:
os << toString(*arg) << "\n";
}
}
return std::string(data.str());
}

Optional<std::string> macho::resolveDylibPath(StringRef path) {
// TODO: if a tbd and dylib are both present, we should check to make sure
// they are consistent.
Expand Down
10 changes: 9 additions & 1 deletion lld/MachO/InputFiles.cpp
Expand Up @@ -56,12 +56,14 @@

#include "lld/Common/ErrorHandler.h"
#include "lld/Common/Memory.h"
#include "lld/Common/Reproduce.h"
#include "llvm/ADT/iterator.h"
#include "llvm/BinaryFormat/MachO.h"
#include "llvm/LTO/LTO.h"
#include "llvm/Support/Endian.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/TarWriter.h"

using namespace llvm;
using namespace llvm::MachO;
Expand All @@ -71,6 +73,7 @@ using namespace lld;
using namespace lld::macho;

std::vector<InputFile *> macho::inputFiles;
std::unique_ptr<TarWriter> macho::tar;

// Open a given file path and return it as a memory-mapped file.
Optional<MemoryBufferRef> macho::readFile(StringRef path) {
Expand All @@ -88,8 +91,11 @@ Optional<MemoryBufferRef> macho::readFile(StringRef path) {
// If this is a regular non-fat file, return it.
const char *buf = mbref.getBufferStart();
auto *hdr = reinterpret_cast<const MachO::fat_header *>(buf);
if (read32be(&hdr->magic) != MachO::FAT_MAGIC)
if (read32be(&hdr->magic) != MachO::FAT_MAGIC) {
if (tar)
tar->append(relativeToRoot(path), mbref.getBuffer());
return mbref;
}

// Object files and archive files may be fat files, which contains
// multiple real files for different CPU ISAs. Here, we search for a
Expand All @@ -112,6 +118,8 @@ Optional<MemoryBufferRef> macho::readFile(StringRef path) {
uint32_t size = read32be(&arch[i].size);
if (offset + size > mbref.getBufferSize())
error(path + ": slice extends beyond end of file");
if (tar)
tar->append(relativeToRoot(path), mbref.getBuffer());
return MemoryBufferRef(StringRef(buf + offset, size), path.copy(bAlloc));
}

Expand Down
5 changes: 5 additions & 0 deletions lld/MachO/InputFiles.h
Expand Up @@ -27,6 +27,7 @@ namespace llvm {
namespace lto {
class InputFile;
} // namespace lto
class TarWriter;
} // namespace llvm

namespace lld {
Expand All @@ -36,6 +37,10 @@ class InputSection;
class Symbol;
struct Reloc;

// If --reproduce option is given, all input files are written
// to this tar archive.
extern std::unique_ptr<llvm::TarWriter> tar;

// If .subsections_via_symbols is set, each InputSection will be split along
// symbol boundaries. The keys of a SubsectionMap represent the offsets of
// each subsection from the start of the original pre-split InputSection.
Expand Down
4 changes: 4 additions & 0 deletions lld/MachO/Options.td
Expand Up @@ -14,6 +14,10 @@ def no_color_diagnostics: Flag<["--"], "no-color-diagnostics">,
def color_diagnostics_eq: Joined<["--"], "color-diagnostics=">,
HelpText<"Use colors in diagnostics (default: auto)">,
MetaVarName<"[auto,always,never]">;
def reproduce: Separate<["--"], "reproduce">;
def reproduce_eq: Joined<["--"], "reproduce=">,
Alias<!cast<Separate>(reproduce)>,
HelpText<"Write tar file containing inputs and command to reproduce link">;


// This is a complete Options.td compiled from Apple's ld(1) manpage
Expand Down
37 changes: 37 additions & 0 deletions lld/test/MachO/reproduce.s
@@ -0,0 +1,37 @@
# REQUIRES: x86

# RUN: rm -rf %t.dir
# RUN: mkdir -p %t.dir/build1
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-macos %s -o %t.dir/build1/foo.o
# RUN: cd %t.dir
# RUN: %lld -platform_version macos 10.10.0 11.0 build1/foo.o -o bar --reproduce repro1.tar
# RUN: tar xOf repro1.tar repro1/%:t.dir/build1/foo.o > build1-foo.o
# RUN: cmp build1/foo.o build1-foo.o

# RUN: tar xf repro1.tar repro1/response.txt repro1/version.txt
# RUN: FileCheck %s --check-prefix=RSP1 < repro1/response.txt
# RSP1: {{^}}-platform_version macos 10.10.0 11.0{{$}}
# RSP1-NOT: {{^}}repro1{{[/\\]}}
# RSP1-NEXT: {{[/\\]}}foo.o
# RSP1-NEXT: -o bar
# RSP1-NOT: --reproduce

# RUN: FileCheck %s --check-prefix=VERSION < repro1/version.txt
# VERSION: LLD

# RUN: mkdir -p %t.dir/build2/a/b/c
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-macos %s -o %t.dir/build2/foo.o
# RUN: cd %t.dir/build2/a/b/c
# RUN: echo ./../../../foo.o > %t.dir/build2/filelist
# RUN: env LLD_REPRODUCE=repro2.tar %lld -filelist %t.dir/build2/filelist -o /dev/null
# RUN: tar xOf repro2.tar repro2/%:t.dir/build2/foo.o > build2-foo.o
# RUN: cmp %t.dir/build2/foo.o build2-foo.o

# RUN: tar xf repro2.tar repro2/response.txt repro2/version.txt
# RUN: FileCheck %s --check-prefix=RSP2 < repro2/response.txt
# RSP2-NOT: {{^}}repro2{{[/\\]}}
# RSP2: {{[/\\]}}foo.o

.globl _main
_main:
ret

0 comments on commit 83e60f5

Please sign in to comment.