Skip to content

Commit 2710c3a

Browse files
committed
tools: add script to sync -O opt-level in rustc manpage with rustc --help
1 parent 5b150d2 commit 2710c3a

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

src/doc/man/rustc.1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Comma separated list of compiler information to print on stdout.
6262
Equivalent to \fI\-C\ debuginfo=2\fR.
6363
.TP
6464
\fB\-O\fR
65-
Equivalent to \fI\-C\ opt\-level=2\fR.
65+
Equivalent to \fI\-C\ opt\-level=3\fR.
6666
.TP
6767
\fB\-o\fR \fIFILENAME\fR
6868
Write output to \fIFILENAME\fR. Ignored if multiple \fI\-\-emit\fR outputs are specified which
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
r"""
4+
Update the rustc manpage "-O" description to match `rustc --help`.
5+
6+
Usage (dry-run by default):
7+
./src/tools/update-rustc-man-opt-level.py --man-file src/doc/man/rustc.1
8+
9+
Apply changes (creates a timestamped backup):
10+
./src/tools/update-rustc-man-opt-level.py --man-file src/doc/man/rustc.1 --apply
11+
12+
Force a level instead of querying rustc:
13+
./src/tools/update-rustc-man-opt-level.py --man-file ... --expected-level 3 --apply
14+
"""
15+
from __future__ import annotations
16+
17+
import argparse
18+
import datetime
19+
import difflib
20+
import shutil
21+
import subprocess
22+
import sys
23+
import re
24+
from pathlib import Path
25+
from typing import Tuple
26+
27+
DEFAULT_RUSTC = "rustc"
28+
29+
# ANSI color codes
30+
_CLR = {
31+
"reset": "\033[0m",
32+
"red": "\033[31m",
33+
"green": "\033[32m",
34+
"yellow": "\033[33m",
35+
"bold": "\033[1m",
36+
}
37+
38+
39+
def colorize(line: str, color: str, enabled: bool) -> str:
40+
if not enabled or color not in _CLR:
41+
return line
42+
return f"{_CLR[color]}{line}{_CLR['reset']}"
43+
44+
45+
def get_rustc_opt_level(rustc_cmd: str = DEFAULT_RUSTC) -> int:
46+
"""Query `rustc --help` and parse the opt-level mapped to -O."""
47+
try:
48+
proc = subprocess.run([rustc_cmd, "--help"], capture_output=True, text=True, check=True)
49+
except FileNotFoundError:
50+
raise RuntimeError(f"rustc not found at '{rustc_cmd}'")
51+
except subprocess.CalledProcessError as e:
52+
stderr = (e.stderr or "").strip()
53+
raise RuntimeError(f"rustc --help failed: {stderr or e}") from e
54+
55+
help_text = (proc.stdout or "") + "\n" + (proc.stderr or "")
56+
m = re.search(r'-O[^\n]*opt(?:\\-)?level\s*=\s*(\d+)', help_text, flags=re.IGNORECASE)
57+
if not m:
58+
m2 = re.search(r'Equivalent to\s+-C\s+opt(?:-)?level\s*=\s*(\d+)', help_text, flags=re.IGNORECASE)
59+
if not m2:
60+
raise RuntimeError("Could not find '-O' opt-level mapping in rustc --help output")
61+
return int(m2.group(1))
62+
return int(m.group(1))
63+
64+
65+
def find_and_replace_manpage_content(content: str, new_level: int) -> Tuple[str, int]:
66+
"""
67+
Replace opt-level numbers in 'Equivalent to ... opt-level=N.' sentences tied to -O.
68+
69+
Conservative heuristic:
70+
- Locate sentences starting with 'Equivalent to' up to the next period.
71+
- Ensure the sentence mentions opt-level (accepting escaped '\-').
72+
- Confirm a -O header appears within a lookback window before the sentence.
73+
"""
74+
replacements = 0
75+
out_parts = []
76+
last_index = 0
77+
78+
sentence_pattern = re.compile(r'Equivalent to([^\n\.]{0,800}?)\.', flags=re.IGNORECASE)
79+
80+
for m in sentence_pattern.finditer(content):
81+
start, end = m.span()
82+
sentence = m.group(0)
83+
84+
if not re.search(r'opt(?:\\-)?level', sentence, flags=re.IGNORECASE):
85+
continue
86+
87+
num_match = re.search(r'(\d+)', sentence)
88+
if not num_match:
89+
continue
90+
old_level = int(num_match.group(1))
91+
if old_level == new_level:
92+
continue
93+
94+
window_start = max(0, start - 1200)
95+
window = content[window_start:start]
96+
97+
if not (
98+
re.search(r'(^|\n)\s*-O\b', window)
99+
or re.search(r'\\fB\\-?O\\fR', window)
100+
or re.search(r'\\-O\b', window)
101+
or re.search(r'\.B\s+-O\b', window)
102+
or re.search(r'\\fB-?O\\fP', window)
103+
):
104+
continue
105+
106+
new_sentence = re.sub(r'(\d+)', str(new_level), sentence, count=1)
107+
out_parts.append(content[last_index:start])
108+
out_parts.append(new_sentence)
109+
last_index = end
110+
replacements += 1
111+
112+
out_parts.append(content[last_index:])
113+
return "".join(out_parts), replacements
114+
115+
116+
def show_colored_diff(old: str, new: str, filename: str, color: bool) -> None:
117+
old_lines = old.splitlines(keepends=True)
118+
new_lines = new.splitlines(keepends=True)
119+
diff_iter = difflib.unified_diff(old_lines, new_lines, fromfile=filename, tofile=filename + " (updated)", lineterm="")
120+
for line in diff_iter:
121+
if line.startswith("---") or line.startswith("+++"):
122+
print(colorize(line.rstrip("\n"), "bold", color))
123+
elif line.startswith("@@"):
124+
print(colorize(line.rstrip("\n"), "yellow", color))
125+
elif line.startswith("+"):
126+
# avoid coloring the file header lines that also start with +++
127+
print(colorize(line.rstrip("\n"), "green", color))
128+
elif line.startswith("-"):
129+
print(colorize(line.rstrip("\n"), "red", color))
130+
else:
131+
print(line.rstrip("\n"))
132+
133+
134+
def backup_file(path: Path) -> Path:
135+
ts = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
136+
backup = path.with_name(path.name + f".bak.{ts}")
137+
shutil.copy2(path, backup)
138+
return backup
139+
140+
141+
def parse_args() -> argparse.Namespace:
142+
p = argparse.ArgumentParser(description="Update rustc man page -O opt-level to match rustc --help")
143+
p.add_argument("--man-file", "-m", required=True, help="Path to rustc man page file to update (e.g. src/doc/man/rustc.1)")
144+
p.add_argument("--rustc-cmd", default=DEFAULT_RUSTC, help="rustc binary to query (default: rustc)")
145+
p.add_argument("--expected-level", "-e", type=int, help="Use this level instead of querying rustc")
146+
p.add_argument("--apply", action="store_true", help="Write changes to the man file (creates a backup). Without this flag runs in dry-run mode and prints a diff.")
147+
p.add_argument("--no-color", action="store_true", help="Disable colored output")
148+
return p.parse_args()
149+
150+
151+
def main() -> int:
152+
args = parse_args()
153+
color = (not args.no_color) and sys.stdout.isatty()
154+
155+
man_path = Path(args.man_file)
156+
if not man_path.exists():
157+
print(f"Error: man file not found: {man_path}", file=sys.stderr)
158+
return 2
159+
160+
try:
161+
new_level = args.expected_level if args.expected_level is not None else get_rustc_opt_level(args.rustc_cmd)
162+
except RuntimeError as e:
163+
print(f"Error determining rustc opt-level: {e}", file=sys.stderr)
164+
return 3
165+
166+
try:
167+
content = man_path.read_text(encoding="utf-8")
168+
except Exception as e:
169+
print(f"Error reading man file {man_path}: {e}", file=sys.stderr)
170+
return 4
171+
172+
new_content, replacements = find_and_replace_manpage_content(content, new_level)
173+
174+
if replacements == 0:
175+
print("No replacements necessary (either already up-to-date or -O entry not found near an 'Equivalent to -C opt-level=' line).")
176+
return 0
177+
178+
header = f"Found {replacements} replacement(s). Proposed changes:"
179+
print(colorize(header, "bold", color))
180+
show_colored_diff(content, new_content, str(man_path), color)
181+
182+
if args.apply:
183+
try:
184+
backup = backup_file(man_path)
185+
man_path.write_text(new_content, encoding="utf-8")
186+
print(colorize(f"\nApplied changes to {man_path}. Backup saved to {backup}", "green", color))
187+
except Exception as e:
188+
print(f"Error writing updated man file: {e}", file=sys.stderr)
189+
return 5
190+
else:
191+
print("\nDry-run only. Use --apply to write changes to disk.")
192+
193+
return 0
194+
195+
196+
if __name__ == "__main__":
197+
raise SystemExit(main())

0 commit comments

Comments
 (0)