blob: 79c72a78a91e04478cfbbda3f584f7cf60fcd538 [file] [edit]
#!/usr/bin/env python3
import errno
import glob
import os
import re
import shutil
import subprocess
import sys
import tempfile
from urllib.request import urlretrieve
from urllib.error import HTTPError, ContentTooShortError
# Usage: rr-collect-symbols.py <trace-dir> [<url> | <path>]
#
# Given a <url>, downloads the zip/.tar.zst file at <url>, uncompresses it,
# runs "gunzip" on any .gz files, and for any ELF files found whose build-ids
# match the build-id of an ELF file in the trace, moves it into the trace.
#
# Given a <path>, which must contain a .build-id directory with the usual
# structure (e.g. as Ubuntu and Fedora create under /usr/lib/debug), searches
# the directory tree for any ELF files whose build-ids match the build-id of
# an ELF file in the trace and copies them into the trace. <path> defaults to
# "/usr/lib/debug", which will grab any available system debuginfo files
# in Ubuntu and Fedora at least.
#
# This script assumes that the trace-dir has been packed via `rr pack` so all
# relevant files actually appear in the trace-dir.
# It also assumes rr is on the PATH.
#
# The debuginfo files are placed in the trace under a "debug" subdirectory,
# in a ".build-id" subdirectory with the usual structure.
#
# If a debuginfo file contains a .gnu_debugaltlink section then we also
# attempt to find the referenced file and copy it into the trace with the
# same file name as the .debug file, but with a .sup suffix.
if len(sys.argv) < 2:
print("Usage: rr-collect-symbols.py <trace-dir> [<url> | <path>]", file=sys.stderr)
sys.exit(1)
trace_dir = sys.argv[1]
if len(sys.argv) < 3:
source = "/usr/lib/debug"
else:
source = sys.argv[2]
rr_buildid = subprocess.Popen(["rr", "buildid"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
def build_id_for(file):
global rr_buildid
rr_buildid.stdin.write(("%s\n"%file).encode('utf-8'))
try:
rr_buildid.stdin.flush()
except BrokenPipeError:
print("Can't write to rr, termination code %s"%rr_buildid.returncode, file=sys.stderr)
sys.exit(2)
return rr_buildid.stdout.readline().rstrip().decode('utf-8')
altref_regex = re.compile(rb"^\s+\[\s*0\]\s+(.*)");
def find_altref(file):
proc = subprocess.Popen(["readelf", "-p", ".gnu_debugaltlink", file], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
for line in proc.stdout:
m = altref_regex.match(line)
if m:
return m.group(1).rstrip()
finally:
proc.wait()
return None
def find_altref_for_trace_file(trace_file, altref):
proc = subprocess.Popen(["rr", "filename", trace_file], stdout=subprocess.PIPE)
try:
for line in proc.stdout:
file = line.rstrip()
altref_file = os.path.join(os.path.dirname(file), altref)
if os.path.isfile(altref_file):
return altref_file
finally:
proc.wait()
return None
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else:
raise
# 'dst' must be a complete file name, not a directory.
def copy_file(src, dst):
try:
# Remove the destination file in case it's a hard link
# or owned by someone else.
os.remove(dst)
except:
pass
shutil.copy(src, dst)
# 'dst' must be a complete file name, not a directory
def create_link(src, dst):
try:
# Remove the destination file in case it's wrong.
os.remove(dst)
except:
pass
os.symlink(src, dst)
def collect_trace_build_ids():
ret = {}
for file in glob.iglob("%s/mmap_*"%trace_dir):
build_id = build_id_for(file)
if build_id:
ret[build_id] = True
altref = find_altref(file)
if altref:
altref_file = find_altref_for_trace_file(file, altref)
if not altref_file:
print("WARNING: Can't find alt file %s for %s"%(altref, file))
continue
dir = "%s/debug/.build-id/%s"%(trace_dir, build_id[:2])
mkdir_p(dir)
copy_file(altref_file, "%s/%s.sup"%(dir, build_id[2:]))
return ret
trace_build_ids = collect_trace_build_ids()
def collect_archive(url):
is_tar_zst = url.endswith(".tar.zst")
tmp_dir = tempfile.mkdtemp(dir=trace_dir)
if is_tar_zst:
tmp_file_name = "%s/archive.tar.zst"%tmp_dir
else:
# Assume its a ZIP
tmp_file_name = "%s/archive.zip"%tmp_dir
try:
(file, headers) = urlretrieve(url, tmp_file_name)
except (HTTPError, ContentTooShortError) as exc:
print("Failed to load archive %s: %s"%(url, exc), file=sys.stderr)
sys.exit(2)
if is_tar_zst:
subprocess.check_call(["tar", "-C", tmp_dir, "-I", "zstd", "-xvf", file])
else:
subprocess.check_call(["unzip", "-d", tmp_dir, file])
os.remove(file)
for root, dirs, files in os.walk(tmp_dir):
for name in files:
file = os.path.join(root, name)
if file.endswith(".gz"):
subprocess.check_call(["gunzip", file])
file = file[:-3]
build_id = build_id_for(file)
if build_id and build_id in trace_build_ids:
dir = "%s/debug/.build-id/%s"%(trace_dir, build_id[:2])
mkdir_p(dir)
dst = "%s/%s.debug"%(dir, build_id[2:])
os.rename(file, dst)
else:
os.remove(file)
shutil.rmtree(tmp_dir)
def collect_filesystem(path):
for root, dirs, files in os.walk(path):
for name in files:
file = os.path.join(root, name)
if not os.path.islink(file):
build_id = build_id_for(file)
if build_id and build_id in trace_build_ids:
dir = "%s/debug/.build-id/%s"%(trace_dir, build_id[:2])
mkdir_p(dir)
copy_file(file, "%s/%s.debug"%(dir, build_id[2:]))
altref = find_altref(file)
if altref:
altref = altref.decode('utf-8')
altref_file = os.path.join(os.path.dirname(file), altref)
copy_file(altref_file, "%s/%s.sup"%(dir, build_id[2:]))
if altref.startswith("../../../.dwz/"):
mkdir_p("%s/.dwz"%trace_dir)
src = "../debug/.build-id/%s/%s.sup"%(build_id[:2], build_id[2:])
create_link(src, "%s/.dwz/%s"%(trace_dir, altref[14:]))
elif altref.startswith("../../.dwz/"):
mkdir_p("%s/debug/.dwz"%trace_dir)
src = "../.build-id/%s/%s.sup"%(build_id[:2], build_id[2:])
create_link(src, "%s/debug/.dwz/%s"%(trace_dir, altref[11:]))
elif altref.startswith("../.dwz/"):
mkdir_p("%s/debug/.build-id/.dwz"%trace_dir)
src = "../%s/%s.sup"%(build_id[:2], build_id[2:])
create_link(src, "%s/debug/.build-id/.dwz/%s"%(trace_dir, altref[8:]))
if re.search("^[^:/]+:", source):
collect_archive(source)
else:
collect_filesystem(source)
rr_buildid.terminate()