fix: tools/bugreport launcher finds a Python 3.8+ itself and never starts a Store alias (#166)
CI / verify (push) Successful in 5m20s
CI / pwsh (push) Successful in 1m53s
Release / release (push) Successful in 36s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2

Files changed:
- .gitea/workflows/ci.yml
- CHANGES.md
- INSTALL.md
- VERSION
- instructions/bug-report.md
- tools/README.md
- tools/bugreport
- tools/bugreport.ps1
- tools/bugreport.py
- tools/chemenu/tests/test_bugreport_launcher.py
- tools/chemenu/tests/test_instructions_shell.py
- tools/chemenu/tests/test_portability.py
This commit is contained in:
torben committed 2026-10-02 13:12:19 +02:00
1 parent c77bda2004
commit 5d937233b1
12 files changed
+537 -19

No files matched your search

+4 -3
View File
@@ -307,12 +307,13 @@ jobs:
- name: The bug-report collector works in the distribution - name: The bug-report collector works in the distribution
# tools/bugreport.py is the one part of the stack that must run when # tools/bugreport.py is the one part of the stack that must run when
# nothing else does, so it is exercised as shipped: from the export, # nothing else does, so it is exercised as shipped: from the export,
# on the runner's plain python3, in the instance the step above set up. # through the tools/bugreport launcher the instructions name, which picks
# the runner's plain python3, in the instance the step above set up.
# The unit tests cover what it collects; this covers that it arrives. # The unit tests cover what it collects; this covers that it arrives.
run: | run: |
set -eu set -eu
cd "$INSTANCE_DIR" cd "$INSTANCE_DIR"
python3 tools/bugreport.py --no-trace tools/bugreport --no-trace
bundle=$(ls -d reports/bugreport-*/ | head -n 1) bundle=$(ls -d reports/bugreport-*/ | head -n 1)
test -f "$bundle/MANIFEST.md" test -f "$bundle/MANIFEST.md"
test -f "$bundle/environment.json" test -f "$bundle/environment.json"
@@ -321,7 +322,7 @@ jobs:
# The same collector with --pseudonymise: the mapping lies beside the # The same collector with --pseudonymise: the mapping lies beside the
# bundle, not in it or its zip, and this container's hostname is gone. # bundle, not in it or its zip, and this container's hostname is gone.
rm -rf reports/bugreport-* rm -rf reports/bugreport-*
python3 tools/bugreport.py --no-trace --pseudonymise tools/bugreport --no-trace --pseudonymise
bundle=$(ls -d reports/bugreport-*/ | head -n 1) bundle=$(ls -d reports/bugreport-*/ | head -n 1)
bundle=${bundle%/} bundle=${bundle%/}
test -f "$bundle.pseudonyms.json" test -f "$bundle.pseudonyms.json"
+26 -1
View File
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
--- ---
## 8.0.0-beta.21 - 2026-10-02 - INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft ## 8.0.0-beta.22 - 2026-10-02 - tools/bugreport: Starter für den Bugreport-Sammler, überspringt die Store-Aliase (#166)
**Author:** Torben Nehmer **Author:** Torben Nehmer
@@ -96,6 +96,7 @@ concern - readable here, never shipped as something to parse.
- trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog - trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
- Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks - Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
- INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft - INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
- tools/bugreport: Starter für den Bugreport-Sammler, überspringt die Store-Aliase (#166)
**Low impact** **Low impact**
- version bump no longer points at version release in its output - version bump no longer points at version release in its output
@@ -133,6 +134,30 @@ concern - readable here, never shipped as something to parse.
- preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes - preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
<!-- /wikitool:bumps --> <!-- /wikitool:bumps -->
### `tools/bugreport`: the collector finds its own Python (#166)
The bug-report collector is the one part of the stack that has to run when nothing else does,
and it was started as `python3 tools/bugreport.py`. On the Windows target that name is the
Microsoft Store's alias in Git Bash, and in PowerShell `python` can be one too - so the report
failed exactly where it was needed.
It now has a launcher pair after the pattern of `tools/wikitool`: `tools/bugreport` (sh, for
Linux, macOS and Git Bash) and `tools/bugreport.ps1`, which PowerShell resolves the same string
to first. They look for a Python the way the preflight does - `python3`, `python` on `PATH`; on
Windows `python`, `py -3`, `python3`, with the registry's `PATH` added under PowerShell - and then
try the venv's. Each candidate is probed for 3.8 or newer before it runs anything, and nothing
under `WindowsApps` is ever started. One that is too old, or that the machine refuses (a venv
`python.exe` blocked by Defender), is passed over. Finding none, the launcher exits 1 and says
why, naming any Python it found too old and the direct call with a full path as the way out.
Neither launcher assumes the preflight, `.wikitool-tools.json` or the venv, and the PowerShell
one keeps to what Windows PowerShell 5.1 understands, so a report never fails on the shell's
version first.
`instructions/bug-report.md`, `INSTALL.md` § Troubleshooting, the stage-2 line the collector
prints and the CI step that runs it in the distribution all use `tools/bugreport`. The shell test
for shipped instructions now refuses a bare `python3`/`python`/`py` call in a command block.
Starting `bugreport.py` directly still works.
### INSTALL.md held to the installation instructions (#154) ### INSTALL.md held to the installation instructions (#154)
The installation procedure has one source, the instructions under `instructions/`; `INSTALL.md` The installation procedure has one source, the instructions under `instructions/`; `INSTALL.md`
+5 -3
View File
@@ -490,9 +490,11 @@ tools/wikitool instructions verify
anfasst, um an der Verweigerung vorbeizukommen, öffnet ein Gate aus eigenem Antrieb. anfasst, um an der Verweigerung vorbeizukommen, öffnet ein Gate aus eigenem Antrieb.
- **Setup oder Update schlägt fehl und ich will es melden** - den Agenten - **Setup oder Update schlägt fehl und ich will es melden** - den Agenten
[instructions/bug-report.md](instructions/bug-report.md) ausführen lassen, oder direkt [instructions/bug-report.md](instructions/bug-report.md) ausführen lassen, oder direkt
`python3 tools/bugreport.py`. Das Skript braucht nur Python 3.8 oder neuer, läuft auch ohne `tools/bugreport` aufrufen (aus Bash, Git Bash und PowerShell gleich). Der Aufruf sucht sich
funktionierendes `wikitool` und schreibt ein Bündel nach `reports/bugreport-<Zeitstempel>/` selbst ein Python 3.8 oder neuer und überspringt dabei die Store-Aliase, auf die `python3`
samt Zip. Geheimnisse werden entfernt, und aus allem, was das Skript selbst erzeugt, bleiben unter Windows zeigt. Er braucht weder den Preflight noch ein funktionierendes `wikitool`, und
findet er kein Python, sagt er, warum. Das Skript schreibt ein Bündel nach
`reports/bugreport-<Zeitstempel>/` samt Zip. Geheimnisse werden entfernt, und aus allem, was das Skript selbst erzeugt, bleiben
Seitentitel draußen (`--titles` nimmt sie mit). Der Sitzungs-Trace ist standardmäßig dabei Seitentitel draußen (`--titles` nimmt sie mit). Der Sitzungs-Trace ist standardmäßig dabei
(`--no-trace` lässt ihn weg) und kann wie Chronologie und Transkripte Seiteninhalt und Titel (`--no-trace` lässt ihn weg) und kann wie Chronologie und Transkripte Seiteninhalt und Titel
enthalten; das Bündel enthält außerdem Maschinen-, Benutzer- und Pfadnamen. Mit `--pseudonymise` enthalten; das Bündel enthält außerdem Maschinen-, Benutzer- und Pfadnamen. Mit `--pseudonymise`
+1 -1
View File
@@ -1 +1 @@
8.0.0-beta.21 8.0.0-beta.22
+13 -7
View File
@@ -1,7 +1,7 @@
--- ---
type: types/instruction.md type: types/instruction.md
name: bug-report name: bug-report
description: How to collect a bug-report bundle when the stack misbehaves on this machine - run tools/bugreport.py, write a fact-only chronology, tell the user what the bundle contains, and stop short of sending it anywhere. description: How to collect a bug-report bundle when the stack misbehaves on this machine - run tools/bugreport, write a fact-only chronology, tell the user what the bundle contains, and stop short of sending it anywhere.
manual: true manual: true
--- ---
# Collect a bug report # Collect a bug report
@@ -32,7 +32,9 @@ the pointers at the failure decision points of [setup-instance.md](setup-instanc
`tools/bugreport.py` is a standalone script. It uses the standard library only and imports nothing `tools/bugreport.py` is a standalone script. It uses the standard library only and imports nothing
from the stack, so it still runs when `wikitool` does not start - no venv, a broken package, a Python from the stack, so it still runs when `wikitool` does not start - no venv, a broken package, a Python
that is too old for the stack. It writes `reports/bugreport-<UTC stamp>/` and a zip beside it, in that is too old for the stack. Its launcher `tools/bugreport` assumes no more than that: it looks for
a Python 3.8 or later on `PATH` itself, skipping the Microsoft Store's aliases, and needs neither
the preflight nor `.wikitool-tools.json`. It writes `reports/bugreport-<UTC stamp>/` and a zip beside it, in
four layers: four layers:
| Layer | Files | Holds | | Layer | Files | Holds |
@@ -94,16 +96,20 @@ session. The bundle does not leave that place because of it.
[template](#chronology-template) below. Facts only - no diagnosis. If the session's own history is [template](#chronology-template) below. Facts only - no diagnosis. If the session's own history is
too long to reconstruct, say what is missing instead of filling the gap. too long to reconstruct, say what is missing instead of filling the gap.
3. **Run the collector**, with whichever Python 3.8 or later the machine has: 3. **Run the collector** through its launcher, which finds a Python 3.8 or later itself:
```bash ```bash
python3 tools/bugreport.py --chronology reports/chronology.md tools/bugreport --chronology reports/chronology.md
``` ```
Add `--no-trace` if the user declined the trace, `--titles` if titles may stay, Add `--no-trace` if the user declined the trace, `--titles` if titles may stay,
`--transcript <file>` (repeatable) for a transcript the user pointed at, `--session <id>` to take a `--transcript <file>` (repeatable) for a transcript the user pointed at, `--session <id>` to take a
trace other than the caller's, `--pseudonymise` if the user chose it (stage 1). `python3` may be `python` or `py -3` on Windows. If no Python starts trace other than the caller's, `--pseudonymise` if the user chose it (stage 1). Do not call a Python yourself: on Windows,
at all, that is the report: give the user the exact error text, verbatim. `python3` - and in PowerShell also `python` - can be the Microsoft Store's alias, which the
launcher skips. If the launcher finds no Python, it exits 1 and says why; that is the report -
give the user its text verbatim. If the user then names a working Python by its full path, run
the collector with it directly (`<full path to python> tools/bugreport.py` and the same
options).
4. **Stage 2, only if the bundle was pseudonymised.** Read, in the bundle: `CHRONOLOGY.md` and 4. **Stage 2, only if the bundle was pseudonymised.** Read, in the bundle: `CHRONOLOGY.md` and
`MANIFEST.md` completely; the review list `reports/bugreport-<stamp>.review.txt` completely; the `MANIFEST.md` completely; the review list `reports/bugreport-<stamp>.review.txt` completely; the
@@ -112,7 +118,7 @@ session. The bundle does not leave that place because of it.
domain, or project - one per line in `reports/candidates.txt`; `#` starts a comment. Then run: domain, or project - one per line in `reports/candidates.txt`; `#` starts a comment. Then run:
```bash ```bash
python3 tools/bugreport.py --bundle reports/bugreport-<stamp> --candidates reports/candidates.txt tools/bugreport --bundle reports/bugreport-<stamp> --candidates reports/candidates.txt
``` ```
The script applies the candidates with the machinery of stage 1, reports which it did not apply The script applies the candidates with the machinery of stage 1, reports which it did not apply
+10
View File
@@ -76,6 +76,9 @@ tools/
prerequisites.txt what the machine needs, one `|`-separated line per tool - read by the preflight and `doctor` prerequisites.txt what the machine needs, one `|`-separated line per tool - read by the preflight and `doctor`
trace-hook what the harness hooks call: trace_ingest.py under the venv's Python trace-hook what the harness hooks call: trace_ingest.py under the venv's Python
trace-hook.ps1 the same for PowerShell, which resolves `./tools/trace-hook` to this file first - without it Windows asks which app opens the sh script trace-hook.ps1 the same for PowerShell, which resolves `./tools/trace-hook` to this file first - without it Windows asks which app opens the sh script
bugreport entry point for the bug-report collector (POSIX sh): finds a Python 3.8+ on PATH itself, skipping the Microsoft Store aliases, and runs bugreport.py - no preflight needed
bugreport.ps1 the same for PowerShell, which resolves `tools/bugreport` to this file first; keeps to what Windows PowerShell 5.1 understands
bugreport.py the bug-report collector itself - see below
chemenu/ chemenu/
cli.py Typer app: registers every command, runs the budget gate, renders `-h`/`--help` from cli_contract cli.py Typer app: registers every command, runs the budget gate, renders `-h`/`--help` from cli_contract
cli_contract.py one data record per command (name, synopsis, properties, exit status) - the source `-h`, the index and CONTRACT.md's generated region render from cli_contract.py one data record per command (name, synopsis, properties, exit status) - the source `-h`, the index and CONTRACT.md's generated region render from
@@ -119,6 +122,13 @@ survive. The mapping, the review list and the candidate file stay beside the bun
`dist export` ships it with the rest of `tools/`; `dist export` ships it with the rest of `tools/`;
its tests (`tests/test_bugreport.py`) run it on a bare interpreter (`-I -S`) to keep that promise. its tests (`tests/test_bugreport.py`) run it on a bare interpreter (`-I -S`) to keep that promise.
It is started through `tools/bugreport` (`bugreport.ps1` under PowerShell), which assumes no more
than the collector does: no preflight, no `.wikitool-tools.json`, no venv. It looks for the
interpreter the way the preflight does - `python3`, `python` on `PATH`; on Windows `python`,
`py -3`, `python3` - then tries the venv's, probes each for 3.8 or newer, and never starts one under
`WindowsApps`, where `python3` in Git Bash is the Microsoft Store's alias. Finding none, it exits 1
and says why.
**Two consumers, one core.** The CLI is not the only caller any more. The cores **Two consumers, one core.** The CLI is not the only caller any more. The cores
(`search/service.py`, `lint_core.py`, `types_core.py`, `catalog.py`) hold what (`search/service.py`, `lint_core.py`, `types_core.py`, `catalog.py`) hold what
decides an answer and import no `typer` and no `rich`; the modules under `commands/` turn decides an answer and import no `typer` and no `rich`; the modules under `commands/` turn
+129
View File
@@ -0,0 +1,129 @@
#!/bin/sh
# Entry point for the bug-report collector, so agents and people start it through one
# stable path on every platform:
#
# tools/bugreport [options]
#
# This is the POSIX half - Linux, macOS and Git Bash on Windows. PowerShell resolves
# the same string to tools/bugreport.ps1 first.
#
# tools/bugreport.py is the one part of the stack that has to run when nothing else
# does, so this script assumes nothing the preflight sets up: no .wikitool-tools.json,
# no venv. It looks for a Python itself, the way the preflight does - on Windows
# python, py -3, python3; elsewhere python3, python; every match on PATH in PATH
# order - and only then tries the venv's. It never starts anything under WindowsApps:
# there, python.exe and python3.exe are the Microsoft Store's aliases, and in Git Bash
# `python3` is exactly that. Every candidate is probed first, so one that is too old or
# that the machine refuses to run (a venv python.exe blocked by Defender) is passed over
# rather than ending the report.
#
# Without any Python 3.8 or newer it says so and exits 1, never 42: the collector opens
# no gate.
#
# CHEMENU_PREFLIGHT_PLATFORM stands in for the platform, as it does for the preflight;
# the test suite is its only user.
set -u
DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P) || exit 1
MIN=3.8
# Exit 3 marks "a Python, but too old" apart from "did not start at all".
PROBE='import sys; sys.exit(0 if sys.version_info >= (3, 8) else 3)'
platform=${CHEMENU_PREFLIGHT_PLATFORM:-}
if [ -z "$platform" ]; then
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*) platform=windows ;;
*) platform=other ;;
esac
fi
if [ "$platform" = windows ]; then
order="python: py:-3 python3:"
looked="python, py -3, python3"
else
order="python3: python:"
looked="python3, python"
fi
is_store_alias() {
case "$1" in *[Ww]indows[Aa]pps/*|*[Ww]indows[Aa]pps\\*) return 0 ;; esac
return 1
}
# Every executable called <name> on PATH, in PATH order, store aliases dropped.
on_path() {
set -f
old_ifs=$IFS
IFS=:
for d in $PATH; do
[ -n "$d" ] || d=.
for f in "$d/$1" "$d/$1.exe"; do
if [ -f "$f" ] && [ -x "$f" ] && ! is_store_alias "$f"; then
printf '%s\n' "$f"
fi
done
done
IFS=$old_ifs
set +f
}
PY="" EXTRA="" TOO_OLD=""
probe() { # <path> [extra argument, e.g. -3 for py] -> sets PY/EXTRA on success
"$@" -c "$PROBE" >/dev/null 2>&1
status=$?
if [ "$status" -eq 0 ]; then
PY=$1 EXTRA=${2:-}
return 0
fi
if [ "$status" -eq 3 ]; then
TOO_OLD="$TOO_OLD
$1"
fi
return 1
}
for candidate in $order; do
name=${candidate%%:*}
extra=${candidate#*:}
found=$(on_path "$name")
[ -n "$found" ] || continue
# A here-document, not a pipe: `break 2` has to leave the outer loop of this
# shell, and a path may contain spaces ("Program Files").
while IFS= read -r path; do
if [ -n "$extra" ]; then
probe "$path" "$extra" && break 2
else
probe "$path" && break 2
fi
done <<EOF
$found
EOF
done
if [ -z "$PY" ]; then
for venv in "$DIR/.venv/bin/python" "$DIR/.venv/Scripts/python.exe"; do
[ -f "$venv" ] && probe "$venv" && break
done
fi
if [ -n "$PY" ]; then
if [ -n "$EXTRA" ]; then
exec "$PY" "$EXTRA" "$DIR/bugreport.py" "$@"
fi
exec "$PY" "$DIR/bugreport.py" "$@"
fi
{
echo "No Python $MIN or newer could be started, so the bug-report collector did not run."
echo "Looked for $looked on PATH, then the one in tools/.venv. Microsoft Store aliases"
echo "under WindowsApps are skipped on purpose - they do not run Python."
if [ -n "$TOO_OLD" ]; then
echo "Found, but older than $MIN:$TOO_OLD"
fi
echo
echo "Install Python $MIN or newer, or start the collector directly with the full path"
echo "of one that works:"
echo
echo " <full path to python> tools/bugreport.py <the same options>"
} >&2
exit 1
+151
View File
@@ -0,0 +1,151 @@
# Entry point for the bug-report collector under PowerShell, so agents and people start it
# through the same string on every platform:
#
# tools/bugreport [options]
#
# PowerShell resolves that to this file first. Without it, `tools/bugreport` is the sh
# launcher next to it, which PowerShell does not run. The POSIX half - Linux, macOS and
# Git Bash - is tools/bugreport.
#
# Same rules as the sh twin: tools/bugreport.py has to run when nothing else does, so
# nothing the preflight sets up is assumed. The Python is looked for the way the preflight
# looks - on Windows python, py -3, python3; elsewhere python3, python; every match on PATH
# in PATH order, on Windows with the registry's PATH added for a session that started
# before the install - and only then in the venv. Nothing under WindowsApps is ever started:
# python.exe and python3.exe there are the Microsoft Store's aliases. Every candidate is
# probed first, so one that is too old or refused (a venv python.exe blocked by Defender) is
# passed over rather than ending the report. Without any Python 3.8 or newer it says so and
# exits 1, never 42: the collector opens no gate.
#
# There is no `#Requires -Version 7`: someone who needs a bug report should not fail at the
# shell's version first, so this file keeps to what Windows PowerShell 5.1 understands too.
#
# CHEMENU_PREFLIGHT_PLATFORM stands in for the platform, as it does for the preflight; the
# test suite is its only user.
$ErrorActionPreference = 'Stop'
$PSNativeCommandArgumentPassing = 'Standard'
$Dir = $PSScriptRoot
$Collector = Join-Path $Dir 'bugreport.py'
$Min = '3.8'
# Exit 3 marks "a Python, but too old" apart from "did not start at all".
$Probe = 'import sys; sys.exit(0 if sys.version_info >= (3, 8) else 3)'
$Platform = $env:CHEMENU_PREFLIGHT_PLATFORM
if (-not $Platform) {
if ($env:OS -eq 'Windows_NT') { $Platform = 'windows' } else { $Platform = 'other' }
}
if ($Platform -eq 'windows') {
$Order = @(@('python', @()), @('py', @('-3')), @('python3', @()))
$Looked = 'python, py -3, python3'
} else {
$Order = @(@('python3', @()), @('python', @()))
$Looked = 'python3, python'
}
function Test-StoreAlias {
param([string]$Path)
return $Path -match '(?i)[\\/]windowsapps[\\/]'
}
# PATH as this process has it, plus - on Windows - what the registry holds that a
# long-running session has not picked up yet.
function Get-SearchDirectory {
$directories = New-Object System.Collections.Generic.List[string]
$sources = @($env:PATH)
if ($env:OS -eq 'Windows_NT') {
foreach ($scope in 'Machine', 'User') {
$sources += [Environment]::GetEnvironmentVariable('Path', $scope)
}
}
foreach ($source in $sources) {
if (-not $source) { continue }
foreach ($entry in $source.Split([IO.Path]::PathSeparator)) {
$expanded = [Environment]::ExpandEnvironmentVariables($entry.Trim())
if ($expanded -and -not $directories.Contains($expanded)) {
$directories.Add($expanded)
}
}
}
return $directories
}
# Every file called <name> on PATH, in PATH order, store aliases dropped.
function Find-OnPath {
param([string]$Name)
$found = @()
foreach ($directory in (Get-SearchDirectory)) {
foreach ($file in @($Name, "$Name.exe")) {
$candidate = Join-Path $directory $file
if ((Test-Path -LiteralPath $candidate -PathType Leaf) -and -not (Test-StoreAlias $candidate)) {
$found += $candidate
}
}
}
return $found
}
$script:TooOld = @()
# 0 when this Python is 3.8 or newer; anything else when it is not, or did not start.
function Test-Python {
param([string]$Path, [string[]]$Extra)
try {
& $Path @Extra -c $Probe *> $null
$status = $LASTEXITCODE
} catch {
return $false
}
if ($status -eq 3) {
$script:TooOld += $Path
}
return $status -eq 0
}
$Python = ''
$PythonExtra = @()
foreach ($candidate in $Order) {
foreach ($path in (Find-OnPath $candidate[0])) {
if (Test-Python $path $candidate[1]) {
$Python = $path
$PythonExtra = $candidate[1]
break
}
}
if ($Python) { break }
}
if (-not $Python) {
foreach ($venv in @((Join-Path $Dir '.venv/Scripts/python.exe'), (Join-Path $Dir '.venv/bin/python'))) {
if ((Test-Path -LiteralPath $venv -PathType Leaf) -and (Test-Python $venv @())) {
$Python = $venv
break
}
}
}
if ($Python) {
& $Python @PythonExtra $Collector @args
exit $LASTEXITCODE
}
$message = @(
"No Python $Min or newer could be started, so the bug-report collector did not run."
"Looked for $Looked on PATH, then the one in tools/.venv. Microsoft Store aliases"
'under WindowsApps are skipped on purpose - they do not run Python.'
)
if ($script:TooOld.Count -gt 0) {
$message += "Found, but older than ${Min}:"
$message += ($script:TooOld | ForEach-Object { " $_" })
}
$message += @(
''
"Install Python $Min or newer, or start the collector directly with the full path"
'of one that works:'
''
' <full path to python> tools/bugreport.py <the same options>'
)
[Console]::Error.WriteLine(($message -join [Environment]::NewLine))
exit 1
+9 -4
View File
@@ -6,10 +6,15 @@ Runs on the base Python with the standard library only, and imports nothing from
- no venv, a broken package, a Python that is too old. Syntax stays at Python - no venv, a broken package, a Python that is too old. Syntax stays at Python
3.8 so that even an old interpreter can still produce a report. 3.8 so that even an old interpreter can still produce a report.
python tools/bugreport.py [--chronology FILE] [--transcript FILE]... tools/bugreport [--chronology FILE] [--transcript FILE]...
[--session ID | --no-trace] [--titles] [--session ID | --no-trace] [--titles]
[--pseudonymise] [--root DIR] [--out DIR] [--pseudonymise] [--root DIR] [--out DIR]
python tools/bugreport.py --bundle DIR --candidates FILE tools/bugreport --bundle DIR --candidates FILE
`tools/bugreport` (and `tools/bugreport.ps1` under PowerShell) finds a Python 3.8
or newer and runs this file with it, skipping the Microsoft Store's aliases that
`python3` resolves to on Windows. Started directly - `<python> tools/bugreport.py`
- it works the same, with whichever interpreter was named.
The bundle is `<out>/bugreport-<UTC stamp>/` plus a zip beside it, in four The bundle is `<out>/bugreport-<UTC stamp>/` plus a zip beside it, in four
layers: the environment, the stack, what `wikitool` prints (if it starts), and layers: the environment, the stack, what `wikitool` prints (if it starts), and
@@ -1188,7 +1193,7 @@ def make_zip(bundle_dir: Path, archive: Path) -> None:
def parse_args(argv): def parse_args(argv):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="bugreport.py", prog="tools/bugreport",
description="Collect a bug-report bundle. Removes secrets, keeps page titles out " description="Collect a bug-report bundle. Removes secrets, keeps page titles out "
"unless --titles is given, uploads nothing.", "unless --titles is given, uploads nothing.",
) )
@@ -1330,7 +1335,7 @@ def _collect(args, root: Path, out: Path, stamp: str, bundle_dir: Path) -> int:
print("Mapping: %s" % _sibling(bundle_dir, ".pseudonyms.json")) print("Mapping: %s" % _sibling(bundle_dir, ".pseudonyms.json"))
print("Review: %s" % _sibling(bundle_dir, ".review.txt")) print("Review: %s" % _sibling(bundle_dir, ".review.txt"))
print("Both hold originals and stay on this machine: never share them.") print("Both hold originals and stay on this machine: never share them.")
print("Stage 2: python3 tools/bugreport.py --bundle %s --candidates <file>" % bundle_dir) print("Stage 2: tools/bugreport --bundle %s --candidates <file>" % bundle_dir)
print() print()
print(STAGE1_NOTICE if pz is not None else PRIVACY_NOTICE) print(STAGE1_NOTICE if pz is not None else PRIVACY_NOTICE)
return 0 return 0
@@ -0,0 +1,184 @@
"""`tools/bugreport` and `tools/bugreport.ps1`, the collector's launchers (Gitea #166).
The collector has to run when nothing else does, and on Windows `python3` - in PowerShell
also `python` - can be the Microsoft Store's alias. So the launchers find a Python 3.8+
themselves, never start anything under WindowsApps, and assume nothing the preflight sets
up. These tests drive both against the stub machine from `test_preflight.py`: a `PATH` the
test builds, stub Pythons in it, and no `.wikitool-tools.json` anywhere.
A stub Python answers the launcher's probe (`-c ...`) with 0, or 3 for "too old", and
otherwise prints the arguments it was started with - which is how a test sees that the
collector was run, and with what.
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from chemenu.tests.test_preflight import SHELLS, TOOLS, Machine
PWSH = shutil.which("pwsh")
# Answers the probe as a current Python, otherwise echoes its arguments.
PYTHON = "#!/bin/sh\n[ \"$1\" = -c ] && exit 0\nprintf '%s\\n' \"$@\"\n"
# The same for the `py` launcher, which needs -3 in front of the probe.
PY_LAUNCHER = "#!/bin/sh\n[ \"$1\" = -3 ] && [ \"$2\" = -c ] && exit 0\nprintf '%s\\n' \"$@\"\n"
TOO_OLD = "#!/bin/sh\n[ \"$1\" = -c ] && exit 3\necho 'collector run by a Python that is too old'\n"
REFUSED = "#!/bin/sh\necho 'Access is denied.' >&2\nexit 1\n"
@pytest.fixture
def machine(tmp_path: Path) -> Machine:
machine = Machine(tmp_path.resolve())
for name in ("bugreport", "bugreport.ps1", "bugreport.py"):
shutil.copy2(TOOLS / name, machine.tools / name)
return machine
def _env(machine: Machine) -> dict[str, str]:
return {
"PATH": os.pathsep.join(str(d) for d in machine.path_dirs),
"HOME": str(machine.base),
**machine.extra_env,
}
def _sh(machine: Machine, *args: str, shell: str) -> subprocess.CompletedProcess:
return subprocess.run([shell, str(machine.tools / "bugreport"), *args],
capture_output=True, text=True, env=_env(machine), timeout=30)
def _pwsh(machine: Machine, *args: str) -> subprocess.CompletedProcess:
env = {**_env(machine), "DOTNET_CLI_TELEMETRY_OPTOUT": "1", "POWERSHELL_TELEMETRY_OPTOUT": "1"}
return subprocess.run(
[PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(machine.tools / "bugreport.ps1"), *args],
capture_output=True, text=True, env=env, timeout=120,
)
def _runners():
runners = [pytest.param(lambda m, *a, shell=shell: _sh(m, *a, shell=shell), id=Path(shell).name)
for shell in SHELLS]
runners.append(pytest.param(
_pwsh, id="pwsh", marks=pytest.mark.skipif(PWSH is None, reason="PowerShell 7 (pwsh) is not installed"),
))
return runners
@pytest.fixture(params=_runners())
def launch(request):
return request.param
def _store_alias(machine: Machine, name: str) -> Path:
"""A Store alias first on PATH that leaves a marker if anything ever starts it."""
marker = machine.base / f"{name}-alias-was-run"
apps = machine.base / "Users" / "u" / "AppData" / "Local" / "Microsoft" / "WindowsApps"
machine.stub(name, f"#!/bin/sh\necho ran > '{marker}'\nexit 9009\n", apps)
machine.path_dirs.insert(0, apps)
return marker
def test_runs_the_collector_with_every_argument(machine, launch):
machine.stub("python3", PYTHON)
result = launch(machine, "--chronology", "reports/a b.md", "--no-trace")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines() == [
str(machine.tools / "bugreport.py"), "--chronology", "reports/a b.md", "--no-trace",
]
def test_needs_nothing_the_preflight_sets_up(machine, launch):
machine.stub("python3", PYTHON)
assert launch(machine, "--no-trace").returncode == 0
assert not (machine.root / ".wikitool-tools.json").exists()
assert not (machine.tools / ".venv").exists()
@pytest.mark.parametrize("platform, alias", [("linux", "python3"), ("windows", "python")])
def test_a_store_alias_is_never_started(machine, launch, platform, alias):
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = platform
marker = _store_alias(machine, alias)
machine.stub(alias, PYTHON)
result = launch(machine, "--no-trace")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
assert not marker.exists()
def test_on_windows_python_comes_before_python3(machine, launch):
"""In Git Bash on the Windows target, `python` is the real one and `python3` the alias
(T4) - the order alone must not depend on the alias being recognised."""
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = "windows"
machine.stub("python3", "#!/bin/sh\necho 'wrong python'\n")
machine.stub("python", PYTHON)
result = launch(machine, "--no-trace")
assert result.stdout.splitlines() == [str(machine.tools / "bugreport.py"), "--no-trace"]
def test_on_windows_the_py_launcher_gets_minus_3(machine, launch):
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = "windows"
machine.stub("py", PY_LAUNCHER)
result = launch(machine, "--no-trace")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines() == ["-3", str(machine.tools / "bugreport.py"), "--no-trace"]
def test_a_python_too_old_is_passed_over(machine, launch):
old = machine.stub("python3", TOO_OLD)
machine.stub("python", PYTHON)
result = launch(machine, "--no-trace")
assert result.returncode == 0, result.stderr
assert "too old" not in result.stdout
assert str(old) not in result.stdout
def test_a_python_that_refuses_to_start_is_passed_over(machine, launch):
machine.stub("python3", REFUSED)
machine.stub("python", PYTHON)
result = launch(machine, "--no-trace")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
@pytest.mark.parametrize("layout", [("bin", "python"), ("Scripts", "python.exe")])
def test_the_venv_python_is_the_last_resort(machine, launch, layout):
machine.stub(layout[1], PYTHON, machine.tools / ".venv" / layout[0])
result = launch(machine, "--no-trace")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
def test_without_a_python_it_says_why_and_exits_1(machine, launch):
old = machine.stub("python3", TOO_OLD)
marker = _store_alias(machine, "python")
result = launch(machine, "--no-trace")
assert result.returncode == 1
assert "No Python 3.8 or newer" in result.stderr
assert "WindowsApps" in result.stderr
assert str(old) in result.stderr
assert "<full path to python> tools/bugreport.py" in result.stderr
assert not marker.exists()
def test_the_collectors_exit_code_passes_through(machine, launch):
machine.stub("python3", "#!/bin/sh\n[ \"$1\" = -c ] && exit 0\nexit 1\n")
assert launch(machine, "--bundle", "b", "--candidates", "c").returncode == 1
def test_both_launchers_ship_and_the_sh_one_is_executable():
assert (TOOLS / "bugreport").is_file() and (TOOLS / "bugreport.ps1").is_file()
assert os.access(TOOLS / "bugreport", os.X_OK)
def test_the_real_collector_runs_through_the_launcher(tmp_path):
"""End to end on this machine's own Python: the launcher finds it and the collector's
usage comes back, naming the launcher as the program."""
result = subprocess.run([str(TOOLS / "bugreport"), "--help"], capture_output=True, text=True,
timeout=60, cwd=tmp_path)
assert result.returncode == 0, result.stderr
assert result.stdout.startswith("usage: tools/bugreport")
@@ -35,6 +35,10 @@ FORBIDDEN = (
("sha256sum", re.compile(r"\bsha256sum\b")), ("sha256sum", re.compile(r"\bsha256sum\b")),
("curl", re.compile(r"\bcurl\b")), ("curl", re.compile(r"\bcurl\b")),
("tar", re.compile(r"(^|[;&|]\s*)tar\s")), ("tar", re.compile(r"(^|[;&|]\s*)tar\s")),
# Which name starts a real Python differs per shell and platform: on Windows `python3` - in
# PowerShell also `python` - can be the Microsoft Store's alias. A command goes through a
# launcher that finds the interpreter itself (`tools/wikitool`, `tools/bugreport`).
("bare interpreter", re.compile(r"(^|[;&|]\s*)(python3?|py)(\.exe)?\s")),
) )
# The two sanctioned exceptions, each one line per shell (instructions/CONTRACT.md): the session # The two sanctioned exceptions, each one line per shell (instructions/CONTRACT.md): the session
+1
View File
@@ -396,6 +396,7 @@ def test_output_into_a_pipe_is_utf8_even_under_a_cp1252_locale():
@pytest.mark.parametrize("path, eol", [ @pytest.mark.parametrize("path, eol", [
("tools/wikitool", "lf"), ("tools/wikitool", "lf"),
("tools/trace-hook", "lf"), ("tools/trace-hook", "lf"),
("tools/bugreport", "lf"),
("tools/preflight.sh", "lf"), ("tools/preflight.sh", "lf"),
("AGENTS.md", "lf"), ("AGENTS.md", "lf"),
]) ])