Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bootstrap.py: patch RPATH on NixOS to handle the new zlib dependency. #74441

Merged
merged 2 commits into from
Jul 18, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 66 additions & 38 deletions src/bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ def __init__(self):
self.use_vendored_sources = ''
self.verbose = False
self.git_version = None
self.nix_deps_dir = None

def download_stage0(self):
"""Fetch the build system for Rust, written in Rust
Expand Down Expand Up @@ -388,8 +389,12 @@ def support_xz():
filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
tarball_suffix)
self._download_stage0_helper(filename, "rustc", tarball_suffix)
self.fix_executable("{}/bin/rustc".format(self.bin_root()))
self.fix_executable("{}/bin/rustdoc".format(self.bin_root()))
self.fix_bin_or_dylib("{}/bin/rustc".format(self.bin_root()))
self.fix_bin_or_dylib("{}/bin/rustdoc".format(self.bin_root()))
lib_dir = "{}/lib".format(self.bin_root())
for lib in os.listdir(lib_dir):
if lib.endswith(".so"):
self.fix_bin_or_dylib("{}/{}".format(lib_dir, lib))
with output(self.rustc_stamp()) as rust_stamp:
rust_stamp.write(self.date)

Expand All @@ -408,7 +413,7 @@ def support_xz():
filename = "cargo-{}-{}{}".format(cargo_channel, self.build,
tarball_suffix)
self._download_stage0_helper(filename, "cargo", tarball_suffix)
self.fix_executable("{}/bin/cargo".format(self.bin_root()))
self.fix_bin_or_dylib("{}/bin/cargo".format(self.bin_root()))
with output(self.cargo_stamp()) as cargo_stamp:
cargo_stamp.write(self.date)

Expand All @@ -421,8 +426,8 @@ def support_xz():
[channel, date] = rustfmt_channel.split('-', 1)
filename = "rustfmt-{}-{}{}".format(channel, self.build, tarball_suffix)
self._download_stage0_helper(filename, "rustfmt-preview", tarball_suffix, date)
self.fix_executable("{}/bin/rustfmt".format(self.bin_root()))
self.fix_executable("{}/bin/cargo-fmt".format(self.bin_root()))
self.fix_bin_or_dylib("{}/bin/rustfmt".format(self.bin_root()))
self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(self.bin_root()))
with output(self.rustfmt_stamp()) as rustfmt_stamp:
rustfmt_stamp.write(self.date + self.rustfmt_channel)

Expand All @@ -440,12 +445,12 @@ def _download_stage0_helper(self, filename, pattern, tarball_suffix, date=None):
get("{}/{}".format(url, filename), tarball, verbose=self.verbose)
unpack(tarball, tarball_suffix, self.bin_root(), match=pattern, verbose=self.verbose)

@staticmethod
def fix_executable(fname):
"""Modifies the interpreter section of 'fname' to fix the dynamic linker
def fix_bin_or_dylib(self, fname):
"""Modifies the interpreter section of 'fname' to fix the dynamic linker,
or the RPATH section, to fix the dynamic library search path

This method is only required on NixOS and uses the PatchELF utility to
change the dynamic linker of ELF executables.
change the interpreter/RPATH of ELF executables.

Please see https://nixos.org/patchelf.html for more information
"""
Expand All @@ -472,38 +477,61 @@ def fix_executable(fname):
nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
print(nix_os_msg, fname)

try:
interpreter = subprocess.check_output(
["patchelf", "--print-interpreter", fname])
interpreter = interpreter.strip().decode(default_encoding)
except subprocess.CalledProcessError as reason:
print("warning: failed to call patchelf:", reason)
return

loader = interpreter.split("/")[-1]

try:
ldd_output = subprocess.check_output(
['ldd', '/run/current-system/sw/bin/sh'])
ldd_output = ldd_output.strip().decode(default_encoding)
except subprocess.CalledProcessError as reason:
print("warning: unable to call ldd:", reason)
return

for line in ldd_output.splitlines():
libname = line.split()[0]
if libname.endswith(loader):
loader_path = libname[:len(libname) - len(loader)]
break
# Only build `stage0/.nix-deps` once.
nix_deps_dir = self.nix_deps_dir
if not nix_deps_dir:
nix_deps_dir = "{}/.nix-deps".format(self.bin_root())
if not os.path.exists(nix_deps_dir):
os.makedirs(nix_deps_dir)

nix_deps = [
# Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
"stdenv.cc.bintools",

# Needed as a system dependency of `libLLVM-*.so`.
"zlib",

# Needed for patching ELF binaries (see doc comment above).
"patchelf",
]

# Run `nix-build` to "build" each dependency (which will likely reuse
# the existing `/nix/store` copy, or at most download a pre-built copy).
# Importantly, we don't rely on `nix-build` printing the `/nix/store`
# path on stdout, but use `-o` to symlink it into `stage0/.nix-deps/$dep`,
# ensuring garbage collection will never remove the `/nix/store` path
# (which would break our patched binaries that hardcode those paths).
for dep in nix_deps:
try:
subprocess.check_output([
"nix-build", "<nixpkgs>",
"-A", dep,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to try and build all of the deps with a single command?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to do that and keep the symlinks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The symlinks won’t be named as well as they are right now, but:

$ nix-build '<nixpkgs>' -A zlib -A stdenv.cc.bintools -o foo/.nixdeps
$ ls -alh foo/
total 0
drwxr-xr-x  2 nagisa users  80 Jul 17 17:43 .
drwxrwxrwt 21 root   root  560 Jul 17 17:43 ..
lrwxrwxrwx  1 nagisa users  55 Jul 17 17:43 .nixdeps -> /nix/store/ml4ipdnvc7pr07dr6i35831f7ffxny0k-zlib-1.2.11
lrwxrwxrwx  1 nagisa users  67 Jul 17 17:43 .nixdeps-2 -> /nix/store/wy6v1s2y8rvxcy98l2yvxqj280cq9wgc-binutils-wrapper-2.31.1

"-o", "{}/{}".format(nix_deps_dir, dep),
])
except subprocess.CalledProcessError as reason:
print("warning: failed to call nix-build:", reason)
return

self.nix_deps_dir = nix_deps_dir

patchelf = "{}/patchelf/bin/patchelf".format(nix_deps_dir)

if fname.endswith(".so"):
# Dynamic library, patch RPATH to point to system dependencies.
dylib_deps = ["zlib"]
rpath_entries = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm… shouldn't we just append the rpath?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require a command to read it, which... I can do it if you want, it's just uglier (and I've gotten used to hardcoding the $ORIGIN/../lib part in my patchelf commands).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it is just not clear to me that $ORIGIN/../lib is sufficient and that there aren’t libraries/binaries that implicitly gain other rpath entries as part of their compilation process. I’m fine with it if it is, I guess.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, NixOS rustup also hardcodes $ORIGIN/../lib:, so I think we're fine.

# Relative default, all binary and dynamic libraries we ship
# appear to have this (even when `../lib` is redundant).
"$ORIGIN/../lib",
] + ["{}/{}/lib".format(nix_deps_dir, dep) for dep in dylib_deps]
patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
else:
print("warning: unable to find the path to the dynamic linker")
return

correct_interpreter = loader_path + loader
bintools_dir = "{}/stdenv.cc.bintools".format(nix_deps_dir)
with open("{}/nix-support/dynamic-linker".format(bintools_dir)) as dynamic_linker:
patchelf_args = ["--set-interpreter", dynamic_linker.read().rstrip()]

try:
subprocess.check_output(
["patchelf", "--set-interpreter", correct_interpreter, fname])
subprocess.check_output([patchelf] + patchelf_args + [fname])
except subprocess.CalledProcessError as reason:
print("warning: failed to call patchelf:", reason)
return
Expand Down