Skip to content

Commit 9ad1d26

Browse files
authored
Rollup merge of rust-lang#74441 - eddyb:zlib-on-nixos, r=nagisa
bootstrap.py: patch RPATH on NixOS to handle the new zlib dependency. This is a stop-gap until rust-lang#74420 is resolved (assuming we'll patch beta to statically link zlib). However, I've been meaning to rewrite the NixOS support we have in `bootstrap.py` for a while now, and had to in order to cleanly add zlib as a dependency (the second commit is a relatively small delta in functionality, compared to the first). Previously, we would extract the `ld-linux.so` path from the output of `ldd /run/current-system/sw/bin/sh`, which assumes a lot. On top of that we didn't use any symlinks, which meant if the user ran GC (`nix-collect-garbage`), e.g. after updating their system, their `stage0` binaries would suddenly be broken (i.e. referring to files that no longer exist). We were also using `patchelf` directly, assuming it can be found in `$PATH` (which is not necessarily true). My new approach relies on using `nix-build` to get the following "derivations" (packages, more or less): * `stdenv.cc.bintools`, which has a `nix-support/dynamic-linker` file containing the path to `ld-linux.so` * reading this file is [the canonical way to run `patchelf --set-interpreter`](https://github.com/NixOS/nixpkgs/search?l=Nix&q=%22--set-interpreter+%24%28cat+%24NIX_CC%2Fnix-support%2Fdynamic-linker%29%22) * `patchelf` (so that the user doesn't need to have it installed) * `zlib`, for the `libz.so` dependency of `libLLVM-*.so` (until rust-lang#74420 is resolved, presumably) This is closer to how software is built on Nix, but I've tried to keep it as simple as possible (and not add e.g. a `stage0.nix` file). Symlinks to each of those dependencies are kept in `stage0/.nix-deps`, which prevents GC from invalidating `stage0` binaries. r? @nagisa cc @Mark-Simulacrum @oli-obk @davidtwco
2 parents e5009f7 + b5076fb commit 9ad1d26

File tree

1 file changed

+66
-38
lines changed

1 file changed

+66
-38
lines changed

src/bootstrap/bootstrap.py

+66-38
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def __init__(self):
349349
self.use_vendored_sources = ''
350350
self.verbose = False
351351
self.git_version = None
352+
self.nix_deps_dir = None
352353

353354
def download_stage0(self):
354355
"""Fetch the build system for Rust, written in Rust
@@ -388,8 +389,12 @@ def support_xz():
388389
filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
389390
tarball_suffix)
390391
self._download_stage0_helper(filename, "rustc", tarball_suffix)
391-
self.fix_executable("{}/bin/rustc".format(self.bin_root()))
392-
self.fix_executable("{}/bin/rustdoc".format(self.bin_root()))
392+
self.fix_bin_or_dylib("{}/bin/rustc".format(self.bin_root()))
393+
self.fix_bin_or_dylib("{}/bin/rustdoc".format(self.bin_root()))
394+
lib_dir = "{}/lib".format(self.bin_root())
395+
for lib in os.listdir(lib_dir):
396+
if lib.endswith(".so"):
397+
self.fix_bin_or_dylib("{}/{}".format(lib_dir, lib))
393398
with output(self.rustc_stamp()) as rust_stamp:
394399
rust_stamp.write(self.date)
395400

@@ -408,7 +413,7 @@ def support_xz():
408413
filename = "cargo-{}-{}{}".format(cargo_channel, self.build,
409414
tarball_suffix)
410415
self._download_stage0_helper(filename, "cargo", tarball_suffix)
411-
self.fix_executable("{}/bin/cargo".format(self.bin_root()))
416+
self.fix_bin_or_dylib("{}/bin/cargo".format(self.bin_root()))
412417
with output(self.cargo_stamp()) as cargo_stamp:
413418
cargo_stamp.write(self.date)
414419

@@ -421,8 +426,8 @@ def support_xz():
421426
[channel, date] = rustfmt_channel.split('-', 1)
422427
filename = "rustfmt-{}-{}{}".format(channel, self.build, tarball_suffix)
423428
self._download_stage0_helper(filename, "rustfmt-preview", tarball_suffix, date)
424-
self.fix_executable("{}/bin/rustfmt".format(self.bin_root()))
425-
self.fix_executable("{}/bin/cargo-fmt".format(self.bin_root()))
429+
self.fix_bin_or_dylib("{}/bin/rustfmt".format(self.bin_root()))
430+
self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(self.bin_root()))
426431
with output(self.rustfmt_stamp()) as rustfmt_stamp:
427432
rustfmt_stamp.write(self.date + self.rustfmt_channel)
428433

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

443-
@staticmethod
444-
def fix_executable(fname):
445-
"""Modifies the interpreter section of 'fname' to fix the dynamic linker
448+
def fix_bin_or_dylib(self, fname):
449+
"""Modifies the interpreter section of 'fname' to fix the dynamic linker,
450+
or the RPATH section, to fix the dynamic library search path
446451
447452
This method is only required on NixOS and uses the PatchELF utility to
448-
change the dynamic linker of ELF executables.
453+
change the interpreter/RPATH of ELF executables.
449454
450455
Please see https://nixos.org/patchelf.html for more information
451456
"""
@@ -472,38 +477,61 @@ def fix_executable(fname):
472477
nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
473478
print(nix_os_msg, fname)
474479

475-
try:
476-
interpreter = subprocess.check_output(
477-
["patchelf", "--print-interpreter", fname])
478-
interpreter = interpreter.strip().decode(default_encoding)
479-
except subprocess.CalledProcessError as reason:
480-
print("warning: failed to call patchelf:", reason)
481-
return
482-
483-
loader = interpreter.split("/")[-1]
484-
485-
try:
486-
ldd_output = subprocess.check_output(
487-
['ldd', '/run/current-system/sw/bin/sh'])
488-
ldd_output = ldd_output.strip().decode(default_encoding)
489-
except subprocess.CalledProcessError as reason:
490-
print("warning: unable to call ldd:", reason)
491-
return
492-
493-
for line in ldd_output.splitlines():
494-
libname = line.split()[0]
495-
if libname.endswith(loader):
496-
loader_path = libname[:len(libname) - len(loader)]
497-
break
480+
# Only build `stage0/.nix-deps` once.
481+
nix_deps_dir = self.nix_deps_dir
482+
if not nix_deps_dir:
483+
nix_deps_dir = "{}/.nix-deps".format(self.bin_root())
484+
if not os.path.exists(nix_deps_dir):
485+
os.makedirs(nix_deps_dir)
486+
487+
nix_deps = [
488+
# Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
489+
"stdenv.cc.bintools",
490+
491+
# Needed as a system dependency of `libLLVM-*.so`.
492+
"zlib",
493+
494+
# Needed for patching ELF binaries (see doc comment above).
495+
"patchelf",
496+
]
497+
498+
# Run `nix-build` to "build" each dependency (which will likely reuse
499+
# the existing `/nix/store` copy, or at most download a pre-built copy).
500+
# Importantly, we don't rely on `nix-build` printing the `/nix/store`
501+
# path on stdout, but use `-o` to symlink it into `stage0/.nix-deps/$dep`,
502+
# ensuring garbage collection will never remove the `/nix/store` path
503+
# (which would break our patched binaries that hardcode those paths).
504+
for dep in nix_deps:
505+
try:
506+
subprocess.check_output([
507+
"nix-build", "<nixpkgs>",
508+
"-A", dep,
509+
"-o", "{}/{}".format(nix_deps_dir, dep),
510+
])
511+
except subprocess.CalledProcessError as reason:
512+
print("warning: failed to call nix-build:", reason)
513+
return
514+
515+
self.nix_deps_dir = nix_deps_dir
516+
517+
patchelf = "{}/patchelf/bin/patchelf".format(nix_deps_dir)
518+
519+
if fname.endswith(".so"):
520+
# Dynamic library, patch RPATH to point to system dependencies.
521+
dylib_deps = ["zlib"]
522+
rpath_entries = [
523+
# Relative default, all binary and dynamic libraries we ship
524+
# appear to have this (even when `../lib` is redundant).
525+
"$ORIGIN/../lib",
526+
] + ["{}/{}/lib".format(nix_deps_dir, dep) for dep in dylib_deps]
527+
patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
498528
else:
499-
print("warning: unable to find the path to the dynamic linker")
500-
return
501-
502-
correct_interpreter = loader_path + loader
529+
bintools_dir = "{}/stdenv.cc.bintools".format(nix_deps_dir)
530+
with open("{}/nix-support/dynamic-linker".format(bintools_dir)) as dynamic_linker:
531+
patchelf_args = ["--set-interpreter", dynamic_linker.read().rstrip()]
503532

504533
try:
505-
subprocess.check_output(
506-
["patchelf", "--set-interpreter", correct_interpreter, fname])
534+
subprocess.check_output([patchelf] + patchelf_args + [fname])
507535
except subprocess.CalledProcessError as reason:
508536
print("warning: failed to call patchelf:", reason)
509537
return

0 commit comments

Comments
 (0)