build: diagnose Intel Darwin ghc-boot Setup failures with LLDB

This commit is contained in:
Moritz Angermann committed 2026-10-16 21:19:24 +09:00
1 parent 17554a9d17
commit 75f95375f8
2 files changed
+269

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
# Failure-only diagnostic for the pinned Intel Darwin compiler's ghc-boot Setup.
# Resolve lldbPath with xcrun on the runner before evaluation. Only this
# diagnostic build may use `--option sandbox false` to reach Apple's debugger.
{ original, lldbPath
, expectedDrv ? "/nix/store/vghjgkr64qjigddh3gnf3c103wbvpxmz-ghc-boot-lib-ghc-boot-9.14.drv"
}:
let
shellQuote = value: "'" + builtins.replaceStrings [ "'" ] [ "'\\''" ] value + "'";
needle = " printf '#!/bin/sh\\nexec %s \"$@\"\\n' \"$g\" > \"$wrapBin/$gName\"";
replacement = ''
printf '#!/bin/sh\nrealGhc="%s"\n' "$g" > "$wrapBin/$gName"
cat >> "$wrapBin/$gName" <<'SIMPLEX_SETUP_COMPILER'
setupOutput=
previous=
setupSource=
setupMake=
for arg do
[ "$previous" != -o ] || setupOutput=$arg
case "$arg" in
--make) setupMake=1 ;;
*/ghc-boot-9.14/setup/setup.hs) setupSource=1 ;;
esac
previous=$arg
done
"$realGhc" "$@"
status=$?
[ "$status" -eq 0 ] || exit "$status"
case "$setupOutput" in
*/ghc-boot-9.14/setup/setup)
if [ -n "$setupMake" ] && [ -n "$setupSource" ]; then
realSetup=$setupOutput.lldb-real
cp -p "$setupOutput" "$realSetup" || exit 1
cmp "$setupOutput" "$realSetup" || exit 1
echo 'Intel Setup diagnostic: preserving compiler-produced executable'
sha256sum "$realSetup"
printf '#!/bin/sh\nset -eu\nrealSetup="%s"\noriginalSetup="%s"\n' \
"$realSetup" "$setupOutput" > "$setupOutput"
cat >> "$setupOutput" <<'SIMPLEX_SETUP_EXECUTION'
configure=
for arg do
[ "$arg" != configure ] || configure=1
done
if [ -z "$configure" ]; then
exec "$realSetup" "$@"
fi
argvFile=$originalSetup.lldb-argv
printf '%s\0' "$originalSetup" "$@" > "$argvFile"
env -0 > "$argvFile.env"
: "''${NIX_BUILD_TOP:?Intel Setup diagnostic requires NIX_BUILD_TOP}"
: > "$NIX_BUILD_TOP/intel-setup-lldb-attempted"
# LLDB rejects a Python module whose basename contains Nix's hash prefix.
driverDir=$originalSetup.lldb-driver
mkdir -p "$driverDir"
cp -f ${./trace_intel_setup_lldb.py} "$driverDir/trace_intel_setup_lldb.py"
echo 'Intel Setup diagnostic: actual configure under host LLDB (diagnostic permits sandbox=false)'
${shellQuote lldbPath} --version
exec timeout --kill-after=10s 300s ${shellQuote lldbPath} --batch --no-lldbinit \
-o "command script import \"$driverDir/trace_intel_setup_lldb.py\"" \
-o "intel-setup-run \"$argvFile\"" -- "$realSetup"
SIMPLEX_SETUP_EXECUTION
chmod +x "$setupOutput"
fi
;;
esac
exit 0
SIMPLEX_SETUP_COMPILER
'';
occurrences = phase: builtins.div
(builtins.stringLength phase - builtins.stringLength
(builtins.replaceStrings [ needle ] [ "" ] phase))
(builtins.stringLength needle);
in
assert original.system == "x86_64-darwin";
assert original.identifier.name == "ghc-boot";
assert original.drvPath == expectedDrv;
assert builtins.isString lldbPath && builtins.substring 0 1 lldbPath == "/";
original.overrideAttrs (old:
assert occurrences old.buildPhase == 1; {
buildPhase = builtins.replaceStrings [ needle ] [ replacement ] old.buildPhase + ''
if [ ! -f "$NIX_BUILD_TOP/intel-setup-lldb-attempted" ]; then
echo 'ERROR: ghc-boot Setup configure was not intercepted by LLDB' >&2
exit 1
fi
'';
})
+184
View File
@@ -0,0 +1,184 @@
"""LLDB command for the unchanged compiler-produced ghc-boot Setup binary."""
import hashlib
import json
import os
from pathlib import Path
import shlex
import lldb
def nul_entries(path):
data = path.read_bytes()
if not data.endswith(b"\0"):
raise ValueError("incomplete NUL-delimited capture")
return [os.fsdecode(item) for item in data[:-1].split(b"\0")]
def drain(process):
for reader in (process.GetSTDOUT, process.GetSTDERR):
while True:
text = reader(65536)
if not text:
break
print(text, end="", flush=True)
def describe_stop(process):
stops = []
for thread in process:
data = [thread.GetStopReasonDataAtIndex(i)
for i in range(thread.GetStopReasonDataCount())]
stops.append({"thread": thread.GetThreadID(),
"reason": thread.GetStopReason(), "data": data,
"description": thread.GetStopDescription(1024)})
print("Intel Setup stop: " + json.dumps(stops), flush=True)
return stops
def dump_stop(target, process):
# Use SB objects directly: a Python command's nested CLI execution context
# can still refer to the pre-launch process even after SBTarget.Launch.
for thread in process:
print("Thread " + str(thread.GetThreadID()), flush=True)
for index in range(min(thread.GetNumFrames(), 80)):
print(str(thread.GetFrameAtIndex(index)), flush=True)
if thread.GetNumFrames() > 80:
print("Backtrace truncated after 80 frames", flush=True)
stopped = next((thread for thread in process
if thread.GetStopReason() not in (lldb.eStopReasonNone,
lldb.eStopReasonInvalid)), None)
if stopped is not None and stopped.GetNumFrames():
frame = stopped.GetFrameAtIndex(0)
for group in frame.GetRegisters():
print("Registers: " + str(group.GetName()), flush=True)
for register in group:
print(str(register.GetName()) + " = " + str(register.GetValue()), flush=True)
print("Instructions at stopped PC:", flush=True)
for instruction in target.ReadInstructions(frame.GetPCAddress(), 12):
print(str(instruction), flush=True)
print("Loaded images:", flush=True)
for index in range(target.GetNumModules()):
module = target.GetModuleAtIndex(index)
print(str(module.GetFileSpec()) + " UUID=" + str(module.GetUUIDString()), flush=True)
def stopped_signals(process):
reasons = [(thread.GetStopReason(),
thread.GetStopReasonDataAtIndex(0) if thread.GetStopReasonDataCount() else None)
for thread in process
if thread.GetStopReason() not in (lldb.eStopReasonNone, lldb.eStopReasonInvalid)]
return [number for reason, number in reasons
if reason == lldb.eStopReasonSignal and number is not None], reasons
def run(debugger, command, _result, _internal_dict):
process = None
code = 1
try:
arguments = shlex.split(command)
if len(arguments) != 1:
raise ValueError("expected one argv-capture pathname")
capture = Path(arguments[0])
argv = nul_entries(capture)
environment_file = Path(str(capture) + ".env")
environment_bytes = environment_file.read_bytes()
environment = nul_entries(environment_file)
environment_file.unlink()
target = debugger.GetSelectedTarget()
executable = target.GetExecutable()
executable_path = Path(executable.fullpath)
print("Intel Setup launch: " + json.dumps({
"executable": str(executable_path), "argv": argv,
"cwd": os.getcwd(),
"executable_sha256": hashlib.sha256(executable_path.read_bytes()).hexdigest(),
"environment_sha256": hashlib.sha256(environment_bytes).hexdigest(),
"environment_entries": len(environment),
}), flush=True)
# Supplying the executable separately leaves argv[0] equal to Cabal's
# original Setup pathname, rather than the retained sibling's name.
launch = lldb.SBLaunchInfo(argv)
launch.SetExecutableFile(executable, False)
launch.SetArguments(argv, False)
launch.SetWorkingDirectory(os.getcwd())
launch.SetEnvironmentEntries(environment, False)
launch.SetLaunchFlags(launch.GetLaunchFlags()
& ~lldb.eLaunchFlagDisableASLR
| lldb.eLaunchFlagStopAtEntry)
# Synchronous launch waits past LLDB's transient initial loader stop.
# The shell wrapper bounds this call and the debugger at 300 seconds.
debugger.SetAsync(False)
error = lldb.SBError()
process = target.Launch(launch, error)
if error.Fail() or not process.IsValid():
raise RuntimeError("LLDB launch failed: " + str(error))
unix_signals = process.GetUnixSignals()
benign = set()
for name in ("SIGPIPE", "SIGALRM", "SIGVTALRM", "SIGPROF"):
number = unix_signals.GetSignalNumberFromName(name)
if number < 0 or not all((unix_signals.SetShouldStop(number, False),
unix_signals.SetShouldNotify(number, False),
unix_signals.SetShouldSuppress(number, False))):
raise RuntimeError("cannot configure LLDB signal policy for " + name)
benign.add(number)
print("Intel Setup signals pass/nostop/nonotify: SIGPIPE SIGALRM SIGVTALRM SIGPROF", flush=True)
for name in ("SIGABRT", "SIGSEGV"):
number = unix_signals.GetSignalNumberFromName(name)
if number < 0 or not all((unix_signals.SetShouldStop(number, True),
unix_signals.SetShouldNotify(number, True),
unix_signals.SetShouldSuppress(number, False))):
raise RuntimeError("cannot preserve LLDB fault policy for " + name)
_, first_reasons = stopped_signals(process)
print("Intel Setup entry stop: " + json.dumps(first_reasons), flush=True)
initial_stop = unix_signals.GetSignalNumberFromName("SIGSTOP")
entry_reasons = (lldb.eStopReasonExec, lldb.eStopReasonTrace,
lldb.eStopReasonBreakpoint, lldb.eStopReasonPlanComplete)
# Continue the deliberate entry stop, but preserve any genuine fault
# occurring before entry. Apply policy before GHC starts its ticker.
if process.GetState() == lldb.eStateStopped and first_reasons and all(
reason in entry_reasons
or (reason == lldb.eStopReasonSignal and (number in benign or number == initial_stop))
for reason, number in first_reasons):
error = process.Continue()
if error.Fail():
raise RuntimeError("LLDB continue failed: " + str(error))
# A benign signal may already have been queued when policy changed.
while process.GetState() == lldb.eStateStopped:
numbers, reasons = stopped_signals(process)
if not numbers or len(numbers) != len(reasons) or not set(numbers) <= benign:
break
print("Intel Setup resuming already-stopped benign signal: " + str(numbers), flush=True)
error = process.Continue()
if error.Fail():
raise RuntimeError("LLDB continue failed: " + str(error))
drain(process)
state = process.GetState()
if state == lldb.eStateExited:
code = process.GetExitStatus()
print("Intel Setup exited: " + str(code), flush=True)
elif state in (lldb.eStateStopped, lldb.eStateCrashed, lldb.eStateSuspended):
stops = describe_stop(process)
dump_stop(target, process)
signals = [stop["data"][0] for stop in stops
if stop["reason"] == lldb.eStopReasonSignal and stop["data"]]
code = 128 + int(signals[0]) if signals else 1
else:
raise RuntimeError("unexpected LLDB process state: " + str(state))
except Exception as error:
code = 1
print("Intel Setup diagnostic error: " + str(error), flush=True)
finally:
if process is not None and process.IsValid():
if process.GetState() not in (lldb.eStateExited, lldb.eStateDetached):
process.Kill()
drain(process)
# LLDB's own successful batch execution must not mask a failed child.
debugger.HandleCommand("quit " + str(code if 0 <= code <= 255 else 1))
def __lldb_init_module(debugger, _internal_dict):
debugger.HandleCommand("command script add -f trace_intel_setup_lldb.run intel-setup-run")