Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions src/bootstrap/src/core/build_steps/dist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,9 @@ impl Step for DebuggerScripts {
cp_debugger_script("gdb_load_rust_pretty_printers.py");
cp_debugger_script("gdb_lookup.py");
cp_debugger_script("gdb_providers.py");
if builder.build.unstable_features() {
cp_debugger_script("gdb_trim_paths.py");
}

// lldb debugger scripts
builder.install(
Expand Down
105 changes: 105 additions & 0 deletions src/etc/gdb_trim_paths.py

@weihanglo weihanglo Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How should we test this? Do you have any manual steps to follow?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Side note: I am adding tests in rust-lang/rust exercising the unremap file itself.

@Urgau Urgau Aug 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I tested it by manually modifying the rust-gdb script to point to my local gdb_trim_paths.py ("$GDB_PYTHON_MODULE_DIRECTORY/gdb_trim_paths.py" to "$YOUR_RUST_LOCAL_CHECKOUT/src/etc/gdb_trim_paths.py"), as otherwise you need to do x.py dist which annoying.

Then I created a dummy hello-world Cargo project, cargo build-it and manually added the hello-world.trim-paths.jsonl file (since the Cargo bump PR is not yet merged).

After that you can just execute rust-gdb as always:

  1. rust-gdb target/debug/hello-world & run
  2. or rust-gdb & file target/debug/hello-world & run

Tip: show substitute-path shows all the substitutions.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# GDB Python script to handle Cargo `<exe>.trim-paths.jsonl` files from the trim-paths feature
# - https://github.com/rust-lang/cargo/issues/12137
# - https://github.com/rust-lang/rust/issues/111540

import json
import os
import gdb
import sys


# https://doc.guix.gnu.org/gdb/16.3/en/html_node/Source-Path.html#index-set-substitute_002dpath
def _process_v1_trim_paths(lines, trim_paths_path):
for idx, line in enumerate(lines[2:], start=3):
try:
entry = json.loads(line)
if "from" in entry and "to" in entry:
cmd = f'set substitute-path "{entry["from"]}" "{entry["to"]}"'
gdb.execute(cmd)
except json.JSONDecodeError:
print(
f"(rust-gdb) warning: invalid JSON on line {idx} of {trim_paths_path}",
file=sys.stderr,
)


def _load_trim_paths(filepath):
trim_paths_path = f"{filepath}.trim-paths.jsonl"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not blocking (consider it just a thought out of curiosity) -- should this be looking up in some gdb facility for finding these? E.g., so that you can fetch this from debuginfod servers or look next to the debuginfo on disk rather than the actual binary?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a interesting idea.

I haven't looked it up much, but it seems like debuginfod works with some kind of "BuildID" to retrieve the debuginfo files. gdb automatically reads the "BuildID" and fetches from debuginfod.

What I haven't figure-out is how to access the fetched content, there doesn't seems to be an easy way for that. We could invoke debuginfod-find debuginfo to "force" the fetch and look inside the fetched content, but that seems a bit hacky.

Leaving a comment inside the script to look at it.


# FIXME: It might be worth looking into the debuginfod fetch content if the local file
# doesn't exists (maybe with `debuginfod-find debuginfo`).
if not os.path.isfile(trim_paths_path):
return

try:
# Load all the lines of the trim-paths file
with open(trim_paths_path, "r", encoding="utf-8") as f:
lines = [line.strip() for line in f]

# Abort if we have less than 3 lines as that means that we cannot have any
# substitutions (header + metadata is already 2 lines)
if not lines or len(lines) < 3:
return

# Try loading the header line, which contains the version (v) field
try:
header = json.loads(lines[0])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not blocking, but it seems a little odd to me to include a version like this as a JSON blob. Maybe we should have magic starting bytes instead? E.g., specifically rust-trim-paths-v1\n?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think header follow what Cargo also does for SBOM, @weihanglo you know more I think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess my feeling is that if we're going to have a versioned header, I'd much rather treat it as a magic byte sequence than a JSON blob. That seems more reliable against future iteration (e.g., allows us to move away from JSON for it).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good point, though I think fi we ever change the format, tools should just give up parsing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sure, but it's generally noisy and more expensive to attempt to parse JSON than to look for a fixed string. Here it's not terrible, we have Python anyway, but if we were doing this in (say) C then that becomes much more gnarly. Of course, if the actual format is JSON then that's no big deal :)

@weihanglo weihanglo Aug 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah. I meant we can always change the format in a new toolchain version. People anyway need to align with the parsing logic, so it doesn't matter if this version it is JSON and next becomes ad-hoc binary format.

ver = header["v"]
except json.JSONDecodeError:
print(
f"(rust-gdb) warning: header line 1 of {trim_paths_path} is not valid JSON",
file=sys.stderr,
)
Comment thread
Urgau marked this conversation as resolved.
return

# We only handle version 1
if ver == 1:
_process_v1_trim_paths(lines, trim_paths_path)
Comment thread
Urgau marked this conversation as resolved.
else:
print(
f"(rust-gdb) warning: unsupported trim-paths version {ver}: {trim_paths_path}",
file=sys.stderr,
)
except Exception as e:
print(
f"(rust-gdb) warning: failed to process trim-paths mappings: {e} of {trim_paths_path}",
file=sys.stderr,
)


def _on_objfile(objfile):
filepath = objfile.filename

if not filepath or not objfile.is_valid() or not os.path.isfile(filepath):
return

_load_trim_paths(filepath)


def _on_progspace(progspace):
filepath = progspace.filename

if not filepath or not os.path.isfile(filepath):
return

_load_trim_paths(filepath)


def _on_new_objfile(event):
_on_objfile(event.new_objfile)


def _on_executable_changed(event):
_on_progspace(event.progspace)


# Setup the events for new objfile (so) and new executable
# https://doc.guix.gnu.org/gdb/16.3/en/html_node/Events-In-Python.html
#
# FIXME: should we handle clear/free events?
gdb.events.new_objfile.connect(_on_new_objfile)
gdb.events.executable_changed.connect(_on_executable_changed)

# Load the trim-paths files for the already loaded objfiles
for objfile in gdb.objfiles():
_on_objfile(objfile)
17 changes: 14 additions & 3 deletions src/etc/rust-gdb
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ GDB_PYTHON_MODULE_DIRECTORY="$RUSTC_SYSROOT/lib/rustlib/etc"
# Get the commit hash for path remapping
RUSTC_COMMIT_HASH="$("$RUSTC" -vV | sed -n 's/commit-hash: \([a-zA-Z0-9_]*\)/\1/p')"

# Run GDB with the additional arguments that load the pretty printers
# Set the environment variable `RUST_GDB` to overwrite the call to a
# different/specific command (defaults to `gdb`).
RUST_GDB="${RUST_GDB:-gdb}"
PYTHONPATH="$PYTHONPATH:$GDB_PYTHON_MODULE_DIRECTORY" exec ${RUST_GDB} \

# Run GDB with the additional arguments that load the pretty printers
# Prepend base GDB flags ahead of any arguments passed by the user
set -- \
--directory="$GDB_PYTHON_MODULE_DIRECTORY" \
-iex "add-auto-load-safe-path $GDB_PYTHON_MODULE_DIRECTORY" \
-iex "set substitute-path /rustc/$RUSTC_COMMIT_HASH $RUSTC_SYSROOT/lib/rustlib/src/rust" \
"$@"


# Unstable (nightly-only) trim-paths feature, activated only if
# - RUST_GDB_TRIM_PATHS=unstable is set
# - and if the python file is present (only shipped on nightly)
GDB_TRIM_PATHS="$GDB_PYTHON_MODULE_DIRECTORY/gdb_trim_paths.py"
if [ "${RUST_GDB_TRIM_PATHS:-}" = "unstable" ] && [ -f "$GDB_TRIM_PATHS" ]; then
set -- -x "$GDB_TRIM_PATHS" "$@"
fi

PYTHONPATH="$PYTHONPATH:$GDB_PYTHON_MODULE_DIRECTORY" exec ${RUST_GDB} "$@"
Loading