Files
scuc-qt-course/tools/gen_sources.py
T
张宗平 13b3ebbe14 课程代码仓库初始化:Part1 (144) + Part3 (29) 全量生成与验证
把川大 wiki「02.Qt方向」课程代码整理为 CMake 管理的可编译运行项目。

Part1 C/C++ 强化 (p01/, 144 目标, host gcc/g++ 构建, 全绿):
- 修复全部编译失败:补 <cstring>/<fstream>/<iomanip>/<vector> 等缺失头
  (common_fix + infer_stl_includes 自动检测注入)、修正 pause() 壳注入
  (改检测已转换后的 pause 调用)、namespace 提升出函数体、const 成员函数
  正确性、dedupe 不再破坏函数重载集、K&R 隐式 int / 动态异常规范等
  C++17 移除特性的可编译等价改写。
- 目录采用深缩写 p01/ch04/s03/ (原 part1_cpp/ch04_class_object/03_ctor_dtor)。

Part3 Qt 桌面 (p03/, 29 目标, 隔离 Qt 5.14.2 构建, 全绿):
- gen_part3.py: Qt 外壳模板 (main+QApplication+show, QTimer 自动退出便于
  offscreen 验证)、AUTOMOC + .moc include、rewrite_* 处理散文混入/重载/
  资源依赖、生成占位 png/gif + .qrc + rundata 样例数据。
- 修正 QLable 笔误、全角分号、.→->、Windows 路径、QApplication 阻塞事件
  循环改为 QCoreApplication 等。

任务1 概念/版本验证:
- tools/std_probe.py: 跨 -std= 编译探针 (--pedantic-errors 让已废除特性硬失败)。
- tools/demo_versions/: K&R 隐式 int、三目左值、void* 转换、enum 赋整、
  const 经指针、动态异常规范 共 6 个跨版本演示。
- docs/VERSION_NOTES.md: 每条「原文说法→C17/C++17 是否成立→何版本成立→已验证」。

文档: 顶层 README、docs/SOURCE_PAGES.md (页面→目录映射)、
docs/ERRATA.md (Part1 修正)、docs/ERRATA_part3.md (Part3 修正)。
另修复 tools/run.sh / run_qt.sh 的 ${1:?...} 引号语法 bug。

隔离 Qt 验证: ldd 0 系统 Qt 引用; 173/173 目标全绿; p03 offscreen 运行通过。
2026-06-30 14:16:55 +08:00

321 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# ==============================================================================
# gen_sources.py — 把抓取的 wiki 数据生成为课程源码 + CMakeLists(可复现)
#
# 输入 : tools/wiki_data/{extracted_code.json, all_page_ids.json,
# qt_full_tree.json, bodies/*.json}
# 输出 : part1_cpp/**, part3_qt_desktop/** 的 .c/.cpp/.h/.qrc 与 CMakeLists.txt
#
# 设计要点(与计划一致):
# * 按页面标题层级(h1..h6)解析子话题;同一子话题内的代码块(多为 test()
# 片段)合并为「一个完整小程序」,把各片段按教学顺序嵌入,并标注出处。
# * C/C++ 按内容上下文判定:用 gcc -std=c17 或 g++ -std=c++17 构建。
# * 每个完整小程序 = 一个独立 add_executable 目标,可单独构建运行。
#
# 本文件是「骨架/分类/合并」的通用引擎;针对每个页面需要的人工修正
# (笔误、system("pause")、缺 include、伪代码等)通过 OVERRIDES 字典注入,
# 并自动记录到 docs/ERRATA.md,使生成过程透明可审计。
# ------------------------------------------------------------------------------
import json, os, re, glob, sys
from html.parser import HTMLParser
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WIKI = os.path.join(REPO, "tools", "wiki_data")
# ------------------------------------------------------------------ 标题解析 ----
class OutlineParser(HTMLParser):
"""顺序产出 ('h', level, text) 与 ('code', idx) 事件,定位每个代码块所属标题。"""
def __init__(self):
super().__init__()
self.events = []
self.code_depth = 0
self.code_idx = 0
self.in_code_body = False
self.capture_h = False
self.cur_lvl = None
self.cur_buf = []
def handle_starttag(self, tag, attrs):
ad = dict(attrs)
if tag in ("h1","h2","h3","h4","h5","h6"):
self.capture_h = True; self.cur_lvl = int(tag[1]); self.cur_buf = []
elif tag in ("ac:structured-macro","structured-macro") and (
ad.get("ac:name") == "code" or ad.get("name") == "code"):
self.code_depth += 1
def handle_endtag(self, tag):
if tag in ("h1","h2","h3","h4","h5","h6") and self.capture_h:
txt = re.sub(r"\s+", " ", "".join(self.cur_buf)).strip()
self.events.append(("h", self.cur_lvl, txt))
self.capture_h = False
elif tag in ("ac:structured-macro","structured-macro") and self.code_depth >= 1:
if self.code_depth == 1:
self.events.append(("code", self.code_idx))
self.code_idx += 1
self.code_depth -= 1
def unknown_decl(self, decl):
pass # CDATA handled by the code extractor separately
def handle_data(self, data):
if self.capture_h:
self.cur_buf.append(data)
def handle_entityref(self, name):
if self.capture_h:
self.cur_buf.append({"nbsp": " "}.get(name, f"&{name};"))
def page_outline(body):
"""Return list of {level, title, code_indices} for the deepest headings that
own at least one code block, in document order. Each code block is attached
to the heading under which it physically appears (last heading seen above it
at any level). Headings with no code blocks are dropped from this list but
their text is folded into the preceding owning heading's context."""
if not body:
return []
p = OutlineParser()
p.feed(body)
owning = [] # stack of (level, title) currently open
sections = [] # list of dict
cur_section = None
for ev in p.events:
if ev[0] == "h":
_, lvl, txt = ev
# pop closed headings
while owning and owning[-1][0] >= lvl:
owning.pop()
owning.append((lvl, txt))
cur_section = None # a new heading starts a new (potential) section
else: # code
if cur_section is None:
# build a context title from the heading path
path_titles = [t for (_, t) in owning]
title = path_titles[-1] if path_titles else "(导言)"
cur_section = {"title": title, "path": path_titles, "code_indices": []}
sections.append(cur_section)
cur_section["code_indices"].append(ev[1])
return sections
# ------------------------------------------------------------------ 语言判定 ----
CPP_TOKENS = ["iostream", "std::", "#include <iostream>", "#include<iostream>",
"class ", "template", "namespace", "using namespace", "new ",
"delete ", "std::string", "cout", "cin", "cerr", "vector<",
"string ", "->", "const_cast", "static_cast", "dynamic_cast",
"reinterpret_cast", "operator ", "public:", "private:", "protected:",
"signals:", "slots:", "virtual ", "throw ", "catch ", "try {",
"Q_OBJECT", "QApplication", "QWidget", "emit ", "qDebug", "QFile",
"#include <Q", "#include<Q"]
C_TOKENS = ["printf", "scanf", "puts(", "getchar", "malloc", "free(",
"#include <stdio.h>", "#include<stdio.h>", "#include <stdlib.h>",
"#include <string.h>", "#include <math.h>", "strcpy", "strcat",
"strcmp", "sprintf", "EXIT_SUCCESS"]
def classify_lang(code):
"""Return 'c' | 'cpp' | 'qt' based on content."""
is_qt = any(t in code for t in
["Q_OBJECT","QApplication","QWidget","QMainWindow","QDialog","QPushButton",
"QLabel","#include <Q","#include<Q","emit ","qDebug","QFile","QPainter",
"QMessageBox","QHBoxLayout","QVBoxLayout","QGridLayout","QLineEdit",
"QTextEdit","QComboBox","QEvent","QPaintEvent","QTimer","QAction",
"QMenuBar","QStatusBar","QToolBar","QFileDialog","QTextStream",
"QDataStream","QDir","QFileInfo","QSpinBox","QSlider","QDockWidget",
"QTextEdit","QPixmap","QImage","QPicture","QBitmap","QMovie"])
if is_qt:
return "qt"
is_cpp = any(t in code for t in CPP_TOKENS)
is_c = any(t in code for t in C_TOKENS)
if is_cpp:
return "cpp"
if is_c:
return "c"
return "cpp" # default to C++ when ambiguous (most wiki blocks are C++)
def is_complete(code):
return bool(re.search(r"\bint\s+main\s*\(", code))
def has_windows_pause(code):
return 'system("pause")' in code or 'system( "pause" )' in code
# C 字符串/内存函数(<cstring>/<string.h> 提供)。wiki 的 C++ 片段常直接用 strcpy/
# strlen 等,却未 include 对应头文件(原意依赖教学环境的传递包含)。检测到即补头。
CSTRING_RE = re.compile(
r"\b(strcpy|strncpy|strcat|strncat|strlen|strcmp|strncmp|strchr|strrchr|"
r"strstr|strtok|strerror|strcoll|memcpy|memmove|memset|memcmp|memchr|"
r"sprintf|snprintf)\s*\(")
def needs_cstring(code):
"""True if the fragment calls any <cstring> function and so needs the header."""
return bool(CSTRING_RE.search(code))
def has_pause_call(code):
"""True if the fragment calls pause() (after common_fix converted system("pause"))."""
return bool(re.search(r"\bpause\s*\(", code))
# Map of (header) → list of tokens that, when used unqualified (relying on
# `using namespace std;`), require that header. wiki STL/IO snippets routinely
# omit these includes (they rely on the teaching toolchain's transitive pulls).
# Detection is on bare symbol usage; includes are injected by the emit template.
_STL_HEADER_TOKENS = {
"<vector>": [r"\bvector\b"],
"<list>": [r"\blist\b"],
"<deque>": [r"\bdeque\b"],
"<set>": [r"\bset\s*<", r"\bmultiset\s*<"],
"<map>": [r"\bmap\s*<", r"\bmultimap\s*<"],
"<unordered_map>": [r"\bunordered_map\s*<"],
"<unordered_set>": [r"\bunordered_set\s*<"],
"<stack>": [r"\bstack\s*<"],
"<queue>": [r"\bqueue\s*<", r"\bpriority_queue\s*<"],
"<algorithm>":[r"\bfor_each\b", r"\btransform\b", r"\bfind\b", r"\bfind_if\b",
r"\bsort\b", r"\bcopy\b", r"\bremove\b", r"\bcount\b",
r"\bcount_if\b", r"\baccumulate\b", r"\bfill\b", r"\breplace\b",
r"\bswap\b", r"\breverse\b", r"\bmerge\b", r"\bunique\b",
r"\bbind\b"],
"<functional>":[r"\bfunction\s*<", r"\bplus\b", r"\bminus\b", r"\bmultiplies\b",
r"\bnegate\b", r"\bequal_to\b", r"\bless\b", r"\bgreater\b",
r"\blogical_and\b", r"\bplaceholders::_"],
"<numeric>": [r"\baccumulate\b"],
"<fstream>": [r"\bifstream\b", r"\bofstream\b", r"\bfstream\b"],
"<iomanip>": [r"\bsetw\b", r"\bsetfill\b", r"\bsetiosflags\b", r"\bresetiosflags\b",
r"\bsetbase\b", r"\bsetprecision\b"],
"<string>": [r"\bstd::string\b", r"\bgetline\b"],
"<cmath>": [r"\bsqrt\b", r"\bpow\b", r"\bsin\b", r"\bcos\b", r"\babs\b",
r"\bfabs\b", r"\bfloor\b", r"\bceil\b", r"\blog\b", r"\bexp\b"],
}
def infer_stl_includes(code):
"""Return the sorted list of C++ standard-library headers the fragment needs
but the wiki omitted (detected by symbol usage). Returns [] for C code."""
needed = []
for hdr, pats in _STL_HEADER_TOKENS.items():
if any(re.search(p, code) for p in pats):
needed.append(hdr)
# de-dup & sort for stable output
return sorted(set(needed))
# ------------------------------------------------------------------ 目标命名 ----
def slug(s):
"""Make a filesystem-friendly slug from a heading (keeps CJK for readability
of source filenames)."""
s = re.sub(r"^\d+(\.\d+)*\s*", "", s) # drop leading "3.10.2 "
s = re.sub(r"[(].*?[)]", "", s) # drop parenthetical
s = re.sub(r"[^\w\u4e00-\u9fff]+", "_", s).strip("_")
s = re.sub(r"_+", "_", s)
return s[:40] or "topic"
def ascii_slug(s):
"""ASCII-only slug for CMake target names (CMake forbids non-ASCII targets).
CJK chars are dropped; the result is guaranteed [A-Za-z0-9_]."""
s = re.sub(r"^\d+(\.\d+)*\s*", "", s)
s = re.sub(r"[(].*?[)]", "", s)
# drop anything that's not ASCII word char
s = re.sub(r"[^A-Za-z0-9_]+", "_", s).strip("_")
s = re.sub(r"_+", "_", s)
return s.lower()[:30] or "topic"
# ------------------------------------------------------------------ 输出助手 ----
def write(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
ERRATA = [] # list of (pageId, blockIdx, kind, before, after, reason)
def record_erratum(pid, idx, kind, before, after, reason):
ERRATA.append((pid, idx, kind, before, after, reason))
# ==============================================================================
# 公共修正:对「所有」part1 代码块都适用的轻量修补(保留原意、可移植)
# ==============================================================================
def common_fix(code, pid, idx):
orig = code
# 0) Normalize non-breaking spaces (U+00A0 NBSP) that the wiki editor injected
# into indented code — these are 'stray \302' compile errors otherwise.
if "\u00a0" in code:
code = code.replace("\u00a0", " ")
# 1) system("pause") → 便携 pause() 壳(仅当出现时;壳函数由模板注入)
if has_windows_pause(code):
code = re.sub(r'system\(\s*"pause"\s*\)', 'pause()', code)
code = re.sub(r'system\(\s*" pause"\s*\)', 'pause()', code)
record_erratum(pid, idx, "platform",
'system("pause")', 'pause()',
"system(\"pause\") 仅 Windows 可用;改用仓库提供的便携 pause() 壳函数 "
"(读一行 stdin 等待回车),行为一致,Linux 可编译运行。")
# 2) 全角分号 → 半角
if "" in code:
code = code.replace("", ";")
record_erratum(pid, idx, "typo", "(全角分号)", ";(半角)",
"全角分号在 C/C++ 中非法,统一替换为半角。")
# 3) 全角引号 → 半角(教学代码常见误输)
for fq, hq in [("", '"'), ("", '"'), ("", "'"), ("", "'")]:
if fq in code:
code = code.replace(fq, hq)
record_erratum(pid, idx, "typo", f"{fq}(全角引号)", f"{hq}(半角)",
"字符串/字符字面量须用半角引号。")
# 4) C 字符串/内存函数缺头:wiki 的 C++ 片段常直接调用 strcpy/strlen/strcmp 等
# 却未 include(依赖教学环境的传递包含)。检测到即补 <cstring>C++)。
# include 由 emit 模板注入;此处只记录 erratum,避免与模板重复插入。
if needs_cstring(code):
record_erratum(pid, idx, "missing-include",
"调用 strcpy/strlen/strcmp 等 C 字符串函数但未 #include <cstring>",
"由生成模板统一补 #include <cstring>",
"C 字符串/内存函数声明在 <cstring>C++/ <string.h>C);"
"原 wiki 代码依赖教学环境传递包含,隔离编译时须显式 include。")
# 5) STL 容器/算法/IO 头缺失:wiki 片段依赖 `using namespace std;` 且假定教学
# 工具链传递包含了 <vector>/<algorithm>/<fstream>/<iomanip> 等。隔离编译时这些
# 符号未声明。检测到的头由 emit 模板注入;此处只记录 erratum。
missing = infer_stl_includes(code)
if missing:
record_erratum(pid, idx, "missing-include",
f"使用 STL/IO 符号(如 vector/for_each/ifstream/setw 等)但未 #include {', '.join(missing)}",
f"由生成模板统一补 #include {' '.join(missing)}",
"原 wiki 代码假定教学环境已传递包含相应标准库头;按「示例相对隔离」独立编译"
"时须显式 include。检测依据为符号使用(见 gen_sources.infer_stl_includes)。")
return code
# ---------------------------------------------------------------- 页面配置表 ----
# 每个页面的目录映射、C/C++ 判定覆盖、特殊修正。键=pageId。
# 目录采用「深缩写」:part 缩写 pNN(顶层不编号)/ 章缩写 chNN / 节缩写 sNN。
# 例:part1_cpp/ch04_class_object/03_ctor_dtor → p01/ch04/s03
# 例:part3_qt_desktop/ch04_signal_slot → p03/ch04
# 单章多节的页:dir 含 sNN 子目录;单章无子节的页:dir 仅到 chNN。
PAGE_CONFIG = {
# ---- Part 1: C++ 强化 ---- (p01)
"58954438": {"part": "p01", "dir": "ch02", "prefix": "p1c02"},
"58954439": {"part": "p01", "dir": "ch03", "prefix": "p1c03"},
"58954448": {"part": "p01", "dir": "ch04/s01", "prefix": "p1c04_01"},
"58954449": {"part": "p01", "dir": "ch04/s02", "prefix": "p1c04_02"},
"58954451": {"part": "p01", "dir": "ch04/s03", "prefix": "p1c04_03"},
"58954456": {"part": "p01", "dir": "ch04/s04", "prefix": "p1c04_04"},
"58954457": {"part": "p01", "dir": "ch04/s05", "prefix": "p1c04_05"},
"58954458": {"part": "p01", "dir": "ch04/s06", "prefix": "p1c04_06"},
"58954461": {"part": "p01", "dir": "ch04/s07", "prefix": "p1c04_07"},
"58954469": {"part": "p01", "dir": "ch04/s08", "prefix": "p1c04_08"},
"58954472": {"part": "p01", "dir": "ch05", "prefix": "p1c05"},
"58954473": {"part": "p01", "dir": "ch06", "prefix": "p1c06"},
"58954474": {"part": "p01", "dir": "ch07", "prefix": "p1c07"},
"58954476": {"part": "p01", "dir": "ch08", "prefix": "p1c08"},
"58954487": {"part": "p01", "dir": "ch09/s02", "prefix": "p1c09_02"},
"58954488": {"part": "p01", "dir": "ch09/s03", "prefix": "p1c09_03"},
"58954498": {"part": "p01", "dir": "ch09/s04", "prefix": "p1c09_04"},
# ---- Part 3: Qt 桌面 ---- (p03)
"58954515": {"part": "p03", "dir": "ch02", "prefix": "p3c02"},
"58954525": {"part": "p03", "dir": "ch03", "prefix": "p3c03"},
"58954527": {"part": "p03", "dir": "ch04", "prefix": "p3c04"},
"58954529": {"part": "p03", "dir": "ch05", "prefix": "p3c05"},
"58954535": {"part": "p03", "dir": "ch06", "prefix": "p3c06"},
"58954540": {"part": "p03", "dir": "ch08", "prefix": "p3c08"},
"58954546": {"part": "p03", "dir": "ch09", "prefix": "p3c09"},
"58954548": {"part": "p03", "dir": "ch10", "prefix": "p3c10"},
"58954552": {"part": "p03", "dir": "ch11", "prefix": "p3c11"},
}
# Per-block language overrides (after manual review of ambiguous cases).
LANG_OVERRIDE = {
("58954439", 13): "c", # tentative def + printf + EXIT_SUCCESS
("58954439", 14): "c", # K&R implicit-int (demo of removed C89 feature)
("58954439", 15): "c", # enum=int + malloc w/o cast (C-only)
("58954439", 18): "c", # ternary rvalue (C side)
("58954439", 21): "c", # const modified via cast pointer
("58954439", 44): "c", # extern "C" guarded header (valid C)
("58954439", 45): "c", # C impl file
("58954448", 0): "c", # C structs + strcpy + Person* to Animal*
}
if __name__ == "__main__":
print("gen_sources: this is the engine module; run gen_part1.py / gen_part3.py.")