Files
systemd/tools/fix-rust-rpath.py
Christian Brauner 8d0c48fe98 meson: support programs written in Rust
Programs written in Rust are declared just like the C ones, in the
same executables list. The crate root comes first and that's how the
loop tells them apart. 'conditions' and 'type' work the same way. Names
starting with 'test-' become unit tests and are installed next to the C
ones with -Dinstall-tests. Installed programs take part in the --help
and --version checks of the dist suite and go into the mkosi images
like the C ones. There are no .standalone variants as Rust programs
always link libsystemd-shared dynamically. They take part in
test-link-abi like every other program.

Nothing from the crates is exported and the programs link with
--exclude-libs ALL. The linker exports a symbol of the program when a
DSO in the link defines it too and a stub of the crates would then
preempt the real one for every library the program loads later.

meson keeps rustc's own library directory in the runpath of Rust
programs that link a shared library. It's only needed for Rust dylibs
which we don't use. meson can't grow a runpath at install time so with
a long pkglibdir such as Debian's multiarch one the install fails. So
give the build-time runpath a padding entry of the right length so the
install always succeeds and drop the leftover entry afterwards with
tools/fix-rust-rpath.py which rewrites the runpath string in place.
Both become no-ops once meson stops adding the directory. The fix is
proposed upstream (cf. [1]).

A rustfmt check over every program source runs in the dist suite and
ninja clippy covers everything.

[1]: https://github.com/mesonbuild/meson/pull/16287

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-23 16:34:47 +02:00

102 lines
3.2 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
"""Drop rustc's own library directory from the runpath of installed programs written in Rust.
meson adds rustc's own library directory to the runpath of every Rust program that links a shared
library (a rustup toolchain needs that to find a dynamically linked libstd) and keeps it when installing.
Our programs do not link libstd at all. The given directory, the one libsystemd-shared lives in, comes
first, other entries stay, like the runtime directory of clang's sanitizers that the C programs get too.
meson 1.12.2 will have the fix (https://github.com/mesonbuild/meson/pull/16287), this is a no-op then.
TODO: drop once the meson baseline is >= 1.12.2.
Usage: fix-rust-rpath.py RPATH FILE...
FILE is the installed path, $DESTDIR is prepended.
"""
import os
import struct
import sys
DT_NULL = 0
DT_RPATH = 15
DT_RUNPATH = 29
SHT_DYNAMIC = 6
def read_cstring(f, offset: int) -> bytes:
f.seek(offset)
s = b''
while True:
c = f.read(1)
if c in (b'', b'\0'):
return s
s += c
def fix_rpath(path: str, rpath: bytes) -> None:
with open(path, 'r+b') as f:
ident = f.read(16)
if ident[:4] != b'\x7fELF':
sys.exit(f'{path}: not an ELF file')
is64 = ident[4] == 2
end = '<' if ident[5] == 1 else '>'
f.seek(0)
if is64:
ehdr = struct.unpack(end + '16sHHIQQQIHHHHHH', f.read(64))
else:
ehdr = struct.unpack(end + '16sHHIIIIIHHHHHH', f.read(52))
shoff, shentsize, shnum = ehdr[6], ehdr[11], ehdr[12]
sections = []
for i in range(shnum):
f.seek(shoff + i * shentsize)
if is64:
sections.append(struct.unpack(end + 'IIQQQQIIQQ', f.read(64)))
else:
sections.append(struct.unpack(end + 'IIIIIIIIII', f.read(40)))
dynamic = next((s for s in sections if s[1] == SHT_DYNAMIC), None)
if dynamic is None:
return
dynstr = sections[dynamic[6]]
entsize = 16 if is64 else 8
entfmt = end + ('qQ' if is64 else 'iI')
for off in range(dynamic[4], dynamic[4] + dynamic[5], entsize):
f.seek(off)
tag, val = struct.unpack(entfmt, f.read(entsize))
if tag == DT_NULL:
break
if tag not in (DT_RPATH, DT_RUNPATH):
continue
str_off = dynstr[4] + val
old = read_cstring(f, str_off)
keep = [e for e in old.split(b':') if e and e != rpath and b'/lib/rustlib/' not in e]
new = b':'.join([rpath] + keep)
if old == new:
continue
if len(new) > len(old):
sys.exit(f'{path}: runpath {new!r} does not fit into {old!r}')
# Overwrite in place, the string table cannot grow.
f.seek(str_off)
f.write(new + b'\0' * (len(old) - len(new) + 1))
print(f'Setting runpath of {path} to {new.decode()}')
def main() -> None:
rpath = sys.argv[1].encode()
destdir = os.environ.get('DESTDIR', '')
for path in sys.argv[2:]:
fix_rpath(destdir + path, rpath)
if __name__ == '__main__':
main()