From a0482ace67e6b67f70e0febb4df3fc61d76e6883 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:42:22 +0800 Subject: [PATCH 1/5] ci: reproduce pinned Qt C++ and ConPTY build inputs (#117) --- .github/workflows/qt-cpp.yml | 49 ++++++++++ third_party/conpty/README.md | 2 + third_party/qt/README.md | 22 ++++- third_party/qt/manifest.json | 8 +- tools/prepare_qt_build.py | 160 +++++++++++++++++++++++++++++++++ tools/test_prepare_qt_build.py | 81 +++++++++++++++++ 6 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/qt-cpp.yml create mode 100644 tools/prepare_qt_build.py create mode 100644 tools/test_prepare_qt_build.py diff --git a/.github/workflows/qt-cpp.yml b/.github/workflows/qt-cpp.yml new file mode 100644 index 00000000..6dee06bb --- /dev/null +++ b/.github/workflows/qt-cpp.yml @@ -0,0 +1,49 @@ +name: Qt C++ fixed ConPTY build and smoke + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + qt-cpp-windows: + runs-on: windows-2025 + timeout-minutes: 20 + env: + PYTHONUTF8: '1' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12' + - name: Verify preparation failure contracts + run: python -m unittest tools.test_prepare_qt_build -v + - name: Prepare manifest-pinned Qt qtbase and Microsoft ConPTY + run: python -m tools.prepare_qt_build + - name: Preserve path-free dependency preparation identity + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: qt-cpp-dependency-preparation + retention-days: 14 + if-no-files-found: warn + path: local/qt-build-preparation.json + - name: Configure explicit Qt product and test entries + run: | + $dependencyRoot = Join-Path $env:GITHUB_WORKSPACE 'local/qt-build-deps' + cmake -S . -B build/qt-cpp -G 'Visual Studio 17 2022' -A x64 ` + -DTERMFORGE_BUILD_QT_SHELL=ON -DBUILD_TESTING=ON ` + -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF ` + "-DPython3_EXECUTABLE=$((Get-Command python).Source)" ` + "-DQt6_DIR=$dependencyRoot/qt/lib/cmake/Qt6" ` + "-DTERMFORGE_CONPTY_BACKEND_DIR=$dependencyRoot/conpty" + - name: Build Qt product and independent smoke + run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke --parallel 2 + - name: Run Qt offscreen smoke in its owned Job + run: ctest --test-dir build/qt-cpp -C Release -R '^qt_cpp_shell$' --no-tests=error --verbose + # Keep this configuration independent from the default/system-backend and + # Alacritty reference jobs. SDKs, runtime folders and raw logs are not + # publication artifacts; this job does not produce a release package. diff --git a/third_party/conpty/README.md b/third_party/conpty/README.md index 282155ec..bd7b456e 100644 --- a/third_party/conpty/README.md +++ b/third_party/conpty/README.md @@ -6,6 +6,8 @@ Windows 本地 Shell 的产品路线已按 [ADR-0007](../../docs/adr/0007-bundle 首次取得组件时,按 manifest 的官方 URL 获取包并核对包大小及 SHA-256,仅提取列出的 `package_members`,保持 `conpty.dll` / `OpenConsole.exe` 名称。不得混合其他包或系统目录的文件。现有同哈希组件可直接复用,构建不隐式下载或修改 Windows 组件。 +Qt 正式入口的本地/CI 依赖准备可使用 `python -m tools.prepare_qt_build`,详见 [Qt 复建说明](../qt/README.md)。该独立准备命令先验证完整 nupkg 身份,再提取并验证文件对;CMake 构建本身仍不下载、不修改 Windows 组件。归档与文件对都只放在被忽略的本地目录,CI 不把它们发布为产品包。 + 根构建将 `TERMFORGE_CONPTY_BACKEND_DIR` 设为该文件对的绝对目录,对正式入口调用 `termforge_use_bundled_conpty(target)`。该函数不依赖 Rust 候选开关;配置和每次构建均核对双文件及本目录许可的身份,并把两文件、manifest 和 LICENSE 复制到目标旁的 `conpty-backend/`。缺包或身份错误直接失败,不退回系统版本。 运行时 `Backend::configured()` 再核对固定双文件,从可执行文件旁的绝对目录加载;create/resize/close 与模块生命周期由同一个 Backend 拥有。当前复用已验证接口的 Console 字符宽度策略 `0x18`,不是旧 passthrough flag。该策略与原版 Unicode、原生控制台和末端行为的完整匹配仍需分别验证。 diff --git a/third_party/qt/README.md b/third_party/qt/README.md index 8ea5511e..38e79f72 100644 --- a/third_party/qt/README.md +++ b/third_party/qt/README.md @@ -8,23 +8,37 @@ manifest 固定了 Qt 官方 Updates.xml 和原始 qtbase 7z 的 URL、大小与 对应源码为 [qtbase 6.10.3 源码归档](https://download.qt.io/archive/qt/6.10/6.10.3/submodules/qtbase-everywhere-src-6.10.3.tar.xz)及 Qt 官方仓库的 `v6.10.3`。这里只标识源码获取位置,没有把源码归档下载、哈希复核或完整对应源码交付算作已完成。 -本地 aqtinstall 3.3.0 会修补 qmake 和 `.prl` 的安装前缀。**原始归档、修补后的 SDK 文件、最终部署目录是不同身份**,不能用归档哈希冒充 SDK 或产品目录的完整哈希;具体构建结果按各次证据绑定实际输入和输出。 +最初本地准备使用 aqtinstall 3.3.0,会修补 qmake 和 `.prl` 的安装前缀。下面的自动复建直接解压同一官方归档,仅写相对的 `bin/qt.conf`(`Prefix=..`),保留原始 qmake 与 `.prl` 字节。**原始归档、两种准备方式得到的 SDK、最终部署目录是不同身份**,不能用归档哈希冒充 SDK 或产品目录的完整哈希;具体构建结果按各次证据绑定实际输入和输出。 ## 本地构建 +在 Windows x64、Python 3.12、CMake 3.31 和 Visual Studio 2022 C++ 工具环境中,无须全局安装 Qt 或修改 PATH: + +```powershell +python -m tools.prepare_qt_build +``` + +命令读取本目录和 ConPTY 的 manifest,下载固定官方归档至忽略的 `local/qt-downloads/`,核对每个归档的大小及 SHA-256,再用 CMake 的归档读取器生成全新的 `local/qt-build-deps/qt/` SDK 与 `local/qt-build-deps/conpty/` 文件对。它核对 Qt 构建/部署工具、四个模块、两个平台插件、SDK SBOM 和实际 qmake 版本;ConPTY 只提取 manifest 命名成员并再次核对双文件。SDK 只用于构建,没有全局注册或环境修改。 + +`--cache ` 可复用已经取得的原始归档;`--offline` 要求两个归档均已缓存,不允许下载。缓存身份不符直接失败,不覆盖坏文件;`--out ` 必须尚不存在,避免覆盖或混用已有 SDK。准备失败不发布半成 SDK;原始下载和临时提取目录不会进入源码。命令输出及 `local/qt-build-preparation.json` 仅包含公开版本、内容哈希、文件数和固定错误码,不含本地路径或原始日志。SDK 的树哈希绑定写入相对配置后的所有文件,不等于归档哈希。 + 使用已核对的 Qt SDK 和[固定微软 ConPTY 文件对](../conpty/README.md),只设置本次构建参数,无须修改全局 PATH: ```powershell cmake -S . -B build/qt-cpp -G "Visual Studio 17 2022" -A x64 ` -DTERMFORGE_BUILD_QT_SHELL=ON ` -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF ` - -DQt6_DIR="/lib/cmake/Qt6" ` - -DTERMFORGE_CONPTY_BACKEND_DIR="" -cmake --build build/qt-cpp --config Release --target termforge_qt_shell + -DBUILD_TESTING=ON ` + -DQt6_DIR="$PWD/local/qt-build-deps/qt/lib/cmake/Qt6" ` + -DTERMFORGE_CONPTY_BACKEND_DIR="$PWD/local/qt-build-deps/conpty" +cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke +ctest --test-dir build/qt-cpp -C Release -R '^qt_cpp_shell$' --no-tests=error --verbose ``` `Qt6_DIR` 应包含该版本的 `Qt6Config.cmake`;正式入口缺少或不匹配固定 ConPTY 时明确失败,不回退系统版本。最小 SDK 未安装翻译,当前构建使用 `windeployqt --no-translations --no-compiler-runtime`,只形成本地构建目录,尚不是可发布的独立产品包。 +专用 [Qt C++ CI](../../.github/workflows/qt-cpp.yml) 在 `windows-2025` 中执行同一准备、两个显式 Qt target 构建,以及 `qt_cpp_shell` 的 offscreen / OwnedJob smoke。它不继承默认/系统后端或 Alacritty 作业的通过结果;runner 验证不扩大 Windows 11 x64 或其他平台的产品支持声明。自动产物仅保留路径无关的准备摘要,不上传 SDK、部署目录或原始日志;Qt 原生窗口、真实输入/DPI 和干净机器运行仍须另验。 + ## 许可与分发边界 Qt 模块保留自己的许可,不由项目 MIT 许可覆盖。[Qt 6.10 许可说明](https://doc.qt.io/qt-6.10/licensing.html)与 SDK SBOM/头文件显示,当前 Core、Gui、Widgets、Test 的模块许可选项包含 LGPL-3.0-only,以及相应 GPL/商业许可选项;manifest 保留精确 SPDX 表达式及实际 SBOM 身份。当前应用计划采用**动态链接的 LGPLv3 路径**,这不是对完整产品分发条件已经满足的声明。 diff --git a/third_party/qt/manifest.json b/third_party/qt/manifest.json index 6f2cd5ca..ad2b6736 100644 --- a/third_party/qt/manifest.json +++ b/third_party/qt/manifest.json @@ -5,7 +5,7 @@ "host": "windows", "target": "desktop", "architecture": "win64_msvc2022_64", - "scope": "Local build dependency for the first C++/Qt migration entry; no released product package or complete redistribution acceptance.", + "scope": "Local and CI build dependency for the C++/Qt migration entry; no released product package or complete redistribution acceptance.", "required_application_modules": [ "Core", "Gui", @@ -97,6 +97,12 @@ "transformations": "aqtinstall patches qmake.exe and .prl installation prefixes; archive hash and installed selected-file hashes are distinct identities.", "hash_scope": "The archive SHA-256 identifies the untouched official 7z. It is not the hash of an aqt-patched SDK directory, qmake.exe, .prl file or deployed product." }, + "reproducible_preparation": { + "entry": "python -m tools.prepare_qt_build", + "extractor": "cmake -E tar xf", + "transformations": "Write bin/qt.conf as [Paths]\\nPrefix=..\\n; preserve official qmake.exe and .prl bytes.", + "output_identity": "The preparation summary records all post-relocation SDK files in a sorted relative-path/SHA-256 tree digest. No absolute paths, SDK files or raw logs are published." + }, "deployment_status": { "published_product_package": false, "clean_machine_runtime_validation": "pending", diff --git a/tools/prepare_qt_build.py b/tools/prepare_qt_build.py new file mode 100644 index 00000000..3ebcff31 --- /dev/null +++ b/tools/prepare_qt_build.py @@ -0,0 +1,160 @@ +"""Prepare the manifest-pinned Windows Qt/ConPTY build inputs locally. + +Only untouched archives are cached. Each run extracts a fresh SDK, adds a +relative qt.conf, verifies the required files, and publishes a path-free summary. +No global installation, PATH modification, or product redistribution occurs. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import tempfile +import urllib.request +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = "termforge-qt-build-preparation-v1" + + +class PreparationError(Exception): + """A fixed public code, never a raw process, network, or filesystem message.""" + + +def sha256(path): + with path.open("rb") as source: + return hashlib.file_digest(source, "sha256").hexdigest() + + +def acquire(cache, name, url, size, digest, offline): + target = cache / name + if target.exists(): + if not target.is_file() or target.stat().st_size != size or sha256(target) != digest: + raise PreparationError("archive_identity") + return target + if offline: + raise PreparationError("archive_missing") + cache.mkdir(parents=True, exist_ok=True) + # Never leave an interrupted or unverified response under a cache filename. + with tempfile.TemporaryDirectory(prefix="download-", dir=cache) as temporary: + candidate = Path(temporary) / "archive" + received = 0 + try: + with urllib.request.urlopen(url, timeout=45) as response, candidate.open("wb") as output: + while chunk := response.read(1024 * 1024): + received += len(chunk) + if received > size: + raise PreparationError("archive_identity") + output.write(chunk) + except OSError: + raise PreparationError("download_failed") from None + if received != size or sha256(candidate) != digest: + raise PreparationError("archive_identity") + candidate.replace(target) + return target + + +def run_tool(arguments, cwd=None): + try: + result = subprocess.run(arguments, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=180, check=False) + except (OSError, subprocess.TimeoutExpired): + raise PreparationError("sdk_tool_failed") from None + if result.returncode: + raise PreparationError("sdk_tool_failed") + return result.stdout.decode("utf-8", "replace").strip() + + +def tree_identity(directory): + """Bind every SDK file after relocation without publishing a path inventory.""" + identity = hashlib.sha256() + count = 0 + for path in sorted(directory.rglob("*")): + if path.is_file(): + identity.update(path.relative_to(directory).as_posix().encode("utf-8") + b"\0") + identity.update(sha256(path).encode("ascii") + b"\n") + count += 1 + return {"files": count, "tree_sha256": identity.hexdigest()} + + +def prepare(cache, out, offline): + if out.exists(): + raise PreparationError("output_exists") + qt = json.loads((ROOT / "third_party/qt/manifest.json").read_text(encoding="utf-8")) + conpty = json.loads((ROOT / "third_party/conpty/manifest.json").read_text(encoding="utf-8")) + archive = qt["official_binary_archive"] + qt_archive = acquire(cache, archive["archive"], archive["url"], archive["bytes"], archive["sha256"], offline) + package = acquire(cache, conpty["package_url"].rsplit("/", 1)[1], conpty["package_url"], + conpty["package_bytes"], conpty["package_sha256"], offline) + license_path = ROOT / "third_party/conpty/LICENSE" + if sha256(license_path) != conpty["bundled_license_sha256"]: + raise PreparationError("conpty_license_identity") + out.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="qt-build-", dir=out.parent) as temporary: + staging = Path(temporary) / "dependencies" + sdk, backend = staging / "qt", staging / "conpty" + sdk.mkdir(parents=True) + backend.mkdir() + # CMake's libarchive reader handles the fixed 7z directly. The verified + # archive contains the SDK prefix itself, not an outer version folder. + run_tool(["cmake", "-E", "tar", "xf", str(qt_archive)], cwd=sdk) + required = ["bin/qmake.exe", "bin/windeployqt.exe", "bin/Qt6Core.dll", + "bin/Qt6Gui.dll", "bin/Qt6Widgets.dll", "bin/Qt6Test.dll", + "plugins/platforms/qwindows.dll", "plugins/platforms/qoffscreen.dll", + "lib/cmake/Qt6/Qt6Config.cmake", "lib/cmake/Qt6Widgets/Qt6WidgetsConfig.cmake", + "lib/cmake/Qt6Test/Qt6TestConfig.cmake", "sbom/qtbase-6.10.3.spdx.json"] + if any(not (sdk / name).is_file() for name in required): + raise PreparationError("sdk_missing_file") + if sha256(sdk / qt["license"]["evidence"]["sdk_sbom_relative_path"]) != qt["license"]["evidence"]["sdk_sbom_sha256"]: + raise PreparationError("sdk_sbom_identity") + # Relative relocation leaves official qmake/.prl bytes untouched. CMake + # imports resolve their own prefix; Qt tools read this adjacent qt.conf. + (sdk / "bin/qt.conf").write_text("[Paths]\nPrefix=..\n", encoding="utf-8", newline="\n") + if run_tool([str(sdk / "bin/qmake.exe"), "-query", "QT_VERSION"]) != qt["version"]: + raise PreparationError("sdk_version") + with zipfile.ZipFile(package) as source: + for name, member in conpty["package_members"].items(): + try: + contents = source.read(member) + except KeyError: + raise PreparationError("conpty_missing_member") from None + if hashlib.sha256(contents).hexdigest() != conpty["files"][name]: + raise PreparationError("conpty_file_identity") + (backend / name).write_bytes(contents) + sdk_identity = tree_identity(sdk) + staging.replace(out) + return {"schema": SCHEMA, "status": "passed", "qt_version": qt["version"], + "qt_architecture": qt["architecture"], "qt_archive_sha256": archive["sha256"], + "input_manifests": {name: sha256(ROOT / "third_party" / name / "manifest.json") + for name in ("qt", "conpty")}, + "preparation_script_sha256": sha256(Path(__file__)), + "sdk_transformation": "bin/qt.conf contains [Paths] and Prefix=..; no binary or .prl patch", + "sdk": sdk_identity, "conpty_version": conpty["version"], + "conpty_package_sha256": conpty["package_sha256"], "conpty_files": conpty["files"], + "scope": "Windows local/CI build inputs only; no product package or platform acceptance"} + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cache", type=Path, default=ROOT / "local/qt-downloads") + parser.add_argument("--out", type=Path, default=ROOT / "local/qt-build-deps") + parser.add_argument("--summary", type=Path, default=ROOT / "local/qt-build-preparation.json") + parser.add_argument("--offline", action="store_true", help="Require both pinned archives in cache") + arguments = parser.parse_args(argv) + try: + result = prepare(arguments.cache.resolve(), arguments.out.resolve(), arguments.offline) + except PreparationError as error: + result = {"schema": SCHEMA, "status": "failed", "error": str(error)} + except (OSError, ValueError, KeyError, zipfile.BadZipFile): + result = {"schema": SCHEMA, "status": "failed", "error": "dependency_input_failed"} + arguments.summary.parent.mkdir(parents=True, exist_ok=True) + report = json.dumps(result, indent=2) + "\n" + arguments.summary.write_text(report, encoding="utf-8", newline="\n") + print(report, end="") + return 0 if result["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_prepare_qt_build.py b/tools/test_prepare_qt_build.py new file mode 100644 index 00000000..7b3e6840 --- /dev/null +++ b/tools/test_prepare_qt_build.py @@ -0,0 +1,81 @@ +"""Dependency preparation CLI contracts; no GUI or personal desktop access.""" +from __future__ import annotations + +from contextlib import redirect_stdout +import io +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from tools import prepare_qt_build + + +class PreparationCommandTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.cache = self.root / "cache" + self.cache.mkdir() + self.out = self.root / "dependencies" + self.summary = self.root / "summary.json" + self.arguments = ["--cache", str(self.cache), "--out", str(self.out), + "--summary", str(self.summary)] + + def run_command(self, *extra): + stdout = io.StringIO() + with redirect_stdout(stdout): + code = prepare_qt_build.main([*self.arguments, *extra]) + result = json.loads(stdout.getvalue()) + self.assertEqual(result, json.loads(self.summary.read_text(encoding="utf-8"))) + self.assertNotIn(str(self.root), stdout.getvalue()) + return code, result + + def test_offline_missing_archive_fails_without_creating_sdk(self): + code, result = self.run_command("--offline") + self.assertEqual(code, 1) + self.assertEqual(result["error"], "archive_missing") + self.assertFalse(self.out.exists()) + + def test_corrupt_cache_is_rejected_and_never_replaced(self): + archive_name = (prepare_qt_build.ROOT / "third_party/qt/manifest.json") + archive_name = json.loads(archive_name.read_text(encoding="utf-8"))["official_binary_archive"]["archive"] + archive = self.cache / archive_name + archive.write_bytes(b"corrupt cached bytes") + with patch("urllib.request.urlopen", side_effect=AssertionError("network must not be called")): + code, result = self.run_command() + self.assertEqual(code, 1) + self.assertEqual(result["error"], "archive_identity") + self.assertEqual(archive.read_bytes(), b"corrupt cached bytes") + self.assertFalse(self.out.exists()) + + def test_download_with_wrong_content_is_not_cached(self): + with patch("urllib.request.urlopen", return_value=io.BytesIO(b"wrong response")): + code, result = self.run_command() + self.assertEqual(code, 1) + self.assertEqual(result["error"], "archive_identity") + self.assertEqual(list(self.cache.iterdir()), []) + self.assertFalse(self.out.exists()) + + def test_network_failure_does_not_publish_a_partial_archive(self): + with patch("urllib.request.urlopen", side_effect=OSError("private network detail")): + code, result = self.run_command() + self.assertEqual(code, 1) + self.assertEqual(result["error"], "download_failed") + self.assertNotIn("private", json.dumps(result)) + self.assertEqual(list(self.cache.iterdir()), []) + + def test_existing_output_is_not_overlaid_or_deleted(self): + self.out.mkdir() + marker = self.out / "keep.txt" + marker.write_text("existing files", encoding="utf-8") + code, result = self.run_command("--offline") + self.assertEqual(code, 1) + self.assertEqual(result["error"], "output_exists") + self.assertEqual(marker.read_text(encoding="utf-8"), "existing files") + + +if __name__ == "__main__": + unittest.main() From c59899e0a61c1b1ca3f855aa0db407cfdb3673af Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:40:56 +0800 Subject: [PATCH 2/5] docs: align R2 next steps with merged Qt entry (#115) --- README.md | 2 +- docs/FEATURES.md | 4 ++-- docs/TRACKING.md | 3 +++ docs/milestones/R2-local-terminal.md | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 85f6c2b8..cc08f9fe 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ An independent, fully open-source terminal project targeting WindTerm 2.7.0 feature, behavior, UI and performance parity. Windows 11 x64 first; Windows, Linux and macOS ultimately. Not affiliated with WindTerm or KingToolbox. -当前是有限 Windows 本地终端原型,**R1 基础文字交互已按限定合同验收,正在推进 R2 可靠本地终端与开发版**。已有 Win32 原型、有限多会话与配置恢复,以及独立 Alacritty 参考实验。正式路线已确定为自主 C++ 核心、当前采用 Qt Widgets GUI 与随包固定微软 ConPTY;新配置的最小整合、核心能力缺口、完整 TUI/输入、真实不同 DPI、性能预算和开发包仍需闭合。现状见[能力矩阵](docs/FEATURES.md),接续顺序见 [R2 交付路线与验收](docs/milestones/R2-local-terminal.md)。SSH/SFTP 属于 R3,完整 UI 与 WindTerm 性能等价均未验收。`TermForge` 是暂名。 +当前是有限 Windows 本地终端原型,**R1 基础文字交互已按限定合同验收,正在推进 R2 可靠本地终端与开发版**。已有 Win32 原型、有限多会话与配置恢复,以及独立 Alacritty 参考实验。正式路线已确定为自主 C++ 核心、当前采用 Qt Widgets GUI 与随包固定微软 ConPTY;[最小单会话整合](https://github.com/JavaZeroo/termforge/issues/115)已合入 main,专用 CI、核心能力缺口、完整 TUI/输入、真实不同 DPI、性能预算和开发包仍需闭合。现状见[能力矩阵](docs/FEATURES.md),接续顺序见 [R2 交付路线与验收](docs/milestones/R2-local-terminal.md)。SSH/SFTP 属于 R3,完整 UI 与 WindTerm 性能等价均未验收。`TermForge` 是暂名。 方向已经用户确认,依据见 [ADR-0006](docs/adr/0006-confirmed-project-direction.md):原版行为优先,内部结构不强求一致,但认真研究、借鉴原版;用户已确认[自主 C++ 核心与当前 Qt Widgets GUI 路线](docs/adr/0008-cpp-core-qt-widgets.md),Windows 本地 Shell 后端采用[随包固定微软 ConPTY](docs/adr/0007-bundled-microsoft-conpty.md)。Alacritty 保留为参考,路线确认与实现验收分开。关键性能指标分别验收,具体允许波动范围后续与用户明确。 diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 3e1fea77..10edd94e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -12,12 +12,12 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| -| 正式目标:自主 C++ + Qt Widgets + 固定微软 ConPTY | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 已确认路线;先建立最小整合入口 | 新组合尚无完整工作流验收;核心能力与 Qt 交互按实际实现逐项验证,旧默认或 Alacritty 成果不自动继承 | +| 正式目标:自主 C++ + Qt Widgets + 固定微软 ConPTY | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 已确认路线;[最小单会话入口 #115](https://github.com/JavaZeroo/termforge/issues/115)已合入 main,专用 CI 由 [#117](https://github.com/JavaZeroo/termforge/issues/117)接续 | 新组合尚无完整工作流验收;核心能力与 Qt 交互按实际实现逐项验证,旧默认或 Alacritty 成果不自动继承 | | 既有默认/Alpha:自主 C++ 核心 + 系统 ConPTY + Win32 | 现有原型与已发布体验包的固定路线,不需要 Rust;不等于新的正式组合已完成 | 已有 R1 限定验收;VT/Unicode 仍为有限子集,不能引用 Alacritty 参考的字素或 TUI 结果 | | Alacritty 参考构建:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;保留[历史核心比较](adr/0003-core-reference-and-reuse.md)与差分,不继续作为正式产品核心扩展 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md)均只证明其固定源码/配置 | | 历史 Alacritty/固定后端整合分支:`termforge.5` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),早期 `termforge.4` 联合源码 `3e5b96d` 的[原基线](research/joint-conpty-baseline.md)保留;后续限定修复已整合,尚未合入 main 或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize #51](https://github.com/JavaZeroo/termforge/issues/51) 与[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63) 已在实验父分支限定闭合,完整 resize/历史由 [#29](https://github.com/JavaZeroo/termforge/issues/29) 接续;[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52) 仍开放。[#53](https://github.com/JavaZeroo/termforge/issues/53) 的原 3MB/768B/16KiB 未完成粘贴取消/关闭、负例及八会话 data-only 两轮已限定验证并整合到实验父分支;同次真实 CF/IME 与完整官方矩阵未通过。fence 默认关闭;[ADR-0007](adr/0007-bundled-microsoft-conpty.md)已记录用户正式后端选择,接入成果须在自主 C++/Qt 路线重新整合并验证 | -正式目标组合为自主 C++ 核心 + Qt Widgets + 随包固定微软 ConPTY;Qt 客户端尚无完整产品能力或联合验收,当前先建立最小整合入口。以下保留既有实现与各固定证据;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 +正式目标组合为自主 C++ 核心 + Qt Widgets + 随包固定微软 ConPTY;Qt 最小单会话入口已有[限定本地验证](../experiments/shell/results/2026-10-28-qt-cpp-entry/README.md),尚无完整产品能力或联合验收。以下保留既有实现与各固定证据;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 ## 基础文字交互与本地终端 diff --git a/docs/TRACKING.md b/docs/TRACKING.md index c2bad468..9c753a56 100644 --- a/docs/TRACKING.md +++ b/docs/TRACKING.md @@ -20,6 +20,9 @@ | 工作 | 责任任务 | 硬前置或联合条件 | |---|---|---| +| 正式路线最小单会话与专用 CI | [单会话 #115](https://github.com/JavaZeroo/termforge/issues/115)、[Qt CI #117](https://github.com/JavaZeroo/termforge/issues/117) | #115 已合入 main;#117 在固定依赖的干净 Windows 环境单独验证 Qt 入口,不由默认/参考 CI 代替 | +| 原版备用屏可观察合同 | [1049 取证 #118](https://github.com/JavaZeroo/termforge/issues/118) | 可独立调查;为 #27 提供原版证据,末端、local-shell transport 和未观察项分别记录 | +| 原版本地输出正确性自动化 | [小负载完整文本 #119](https://github.com/JavaZeroo/termforge/issues/119) | #3 的独立切片;生产事件、完整文本与回收绑定,尚不验收消费/呈现端点、扰动或正式 #4 基线 | | 历史候选/官方后端联合基线 | [联合配置基线 #55](https://github.com/JavaZeroo/termforge/issues/55) | 保留原固定源码与红/绿结果;可复用输入和接入经验,不替代自主 C++/Qt/固定 ConPTY 的新配置验收 | | 原生 API / VT 末端 margin 的处理路径 | [平台语义与方案 #56](https://github.com/JavaZeroo/termforge/issues/56) | 可用固定探针独立推进;不能以策略说明豁免 #41 原合同 | | 实验后端连续 resize | [残尾与 Unicode 重放 #51](https://github.com/JavaZeroo/termforge/issues/51) | 原生 viewport 与原 GUI/Unicode 合同已由 PR #89 在实验父分支限定闭合;完整 resize 由 #29、最终官方配置由 #47 接续 | diff --git a/docs/milestones/R2-local-terminal.md b/docs/milestones/R2-local-terminal.md index d09ff319..de23a325 100644 --- a/docs/milestones/R2-local-terminal.md +++ b/docs/milestones/R2-local-terminal.md @@ -8,7 +8,7 @@ ## 当前站位 -R2 的平台调查、有限会话与恢复及 Alacritty 参考实验已有各自限定交付。当前正式路线为**自主 C++ 核心、Qt Widgets 与随包固定微软 ConPTY**,下一步先建立最小可构建整合,再补 C++ 核心能力和完整用户工作流。现有 C++ 核心仍是有限子集;候选的首批字素、TUI 与重排成功不转记为 C++ 能力。已有 Alpha、Win32 原型、Alacritty 和 Draft 后端保留固定身份,新组合需要自己的证据。 +R2 的平台调查、有限会话与恢复及 Alacritty 参考实验已有各自限定交付。当前正式路线为**自主 C++ 核心、Qt Widgets 与随包固定微软 ConPTY**;[最小可构建单会话入口 #115](https://github.com/JavaZeroo/termforge/issues/115)已合入 main,并完成固定 80×24、本地 cmd 输入输出、错误和关闭回收的限定验证。接续补专用 CI、C++ 核心能力和完整用户工作流。现有 C++ 核心仍是有限子集;候选的首批字素、TUI 与重排成功不转记为 C++ 能力。已有 Alpha、Win32 原型、Alacritty 和 Draft 后端保留固定身份,新组合需要自己的证据。 历史联合源码 `3e5b96d` 的三配置[真实基线](../research/joint-conpty-baseline.md)及其后续限定修复继续用于复用输入、接入经验和复验原失败;它们不证明新的 C++/Qt/固定后端组合已通过。Draft #50 的后端成果接入当前路线后仍须验证原合同,路线确认不解除失败或跳过项。 @@ -43,7 +43,7 @@ flowchart LR ## 下一批的任务边界 -1. **先建立正式 C++/Qt/固定后端最小整合,再复验后端成果。** 新组合先验证可构建、实际输出、尺寸、退出与回收,保留尚未支持的 C++ 核心模式和 Qt 交互缺口。 #55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 +1. **从正式路线最小整合接续专用 CI 和后端复验。** #115 已限定验证固定 80×24 单会话;[#117](https://github.com/JavaZeroo/termforge/issues/117)接续固定依赖的干净 Windows CI。动态尺寸、C++ 核心模式和 Qt 交互缺口继续保留。#55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 2. **并行处理原版对照、平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#3/#4 优先建立原版 Windows 本地终端基线;#54 现在冻结重复方法与完成端点并定位吞吐成本,数值预算作为提案交用户确认,不等到 #34 才补性能。 3. **按原版合同补自主 C++ 能力。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 提供的是参考实现与固定场景;自主 C++ 是否具备同等字素能力必须独立实现和验证。 4. **补终端模式和应用矩阵。** #27 明确 DECOM/DECAWM、pending-wrap、保存恢复、异常序列与 margin×resize;#28 先固定应用版本/入口/动作及协议字节预期,再逐应用完成进入、操作、resize、退出、主屏恢复。Vim 的一个修复不替代完整 TUI 验收。 From d174a7be0260cda1743d0c2c708087c8d874ee69 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:49:18 +0800 Subject: [PATCH 3/5] fix: preserve pinned license bytes and safe preparation errors (#117) --- .gitattributes | 1 + tools/prepare_qt_build.py | 9 +++++++-- tools/test_prepare_qt_build.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index d80940da..6410ef0b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,4 +4,5 @@ *.yml text eol=lf *.py text eol=lf *.ps1 text eol=crlf +third_party/conpty/LICENSE text eol=lf experiments/core/fixtures/*.bin binary diff --git a/tools/prepare_qt_build.py b/tools/prepare_qt_build.py index 3ebcff31..2df64be1 100644 --- a/tools/prepare_qt_build.py +++ b/tools/prepare_qt_build.py @@ -149,9 +149,14 @@ def main(argv=None): result = {"schema": SCHEMA, "status": "failed", "error": str(error)} except (OSError, ValueError, KeyError, zipfile.BadZipFile): result = {"schema": SCHEMA, "status": "failed", "error": "dependency_input_failed"} - arguments.summary.parent.mkdir(parents=True, exist_ok=True) report = json.dumps(result, indent=2) + "\n" - arguments.summary.write_text(report, encoding="utf-8", newline="\n") + try: + arguments.summary.parent.mkdir(parents=True, exist_ok=True) + arguments.summary.write_text(report, encoding="utf-8", newline="\n") + except (OSError, ValueError): + result = {"schema": SCHEMA, "status": "failed", "error": "summary_write_failed", + "preparation_status": result["status"], "preparation_error": result.get("error")} + report = json.dumps(result, indent=2) + "\n" print(report, end="") return 0 if result["status"] == "passed" else 1 diff --git a/tools/test_prepare_qt_build.py b/tools/test_prepare_qt_build.py index 7b3e6840..2f72e946 100644 --- a/tools/test_prepare_qt_build.py +++ b/tools/test_prepare_qt_build.py @@ -76,6 +76,18 @@ def test_existing_output_is_not_overlaid_or_deleted(self): self.assertEqual(result["error"], "output_exists") self.assertEqual(marker.read_text(encoding="utf-8"), "existing files") + def test_unwritable_summary_still_reports_path_free_failure(self): + self.summary.mkdir() + stdout = io.StringIO() + with redirect_stdout(stdout): + code = prepare_qt_build.main([*self.arguments, "--offline"]) + result = json.loads(stdout.getvalue()) + self.assertEqual(code, 1) + self.assertEqual(result["error"], "summary_write_failed") + self.assertEqual(result["preparation_error"], "archive_missing") + self.assertNotIn(str(self.root), stdout.getvalue()) + self.assertEqual(list(self.summary.iterdir()), []) + if __name__ == "__main__": unittest.main() From ecb559800e8146126117360ca24c519e08698ae6 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:52:25 +0800 Subject: [PATCH 4/5] fix: match Qt CI generator to the hosted VS 2026 image (#117) --- .github/workflows/qt-cpp.yml | 2 +- third_party/qt/README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/qt-cpp.yml b/.github/workflows/qt-cpp.yml index 6dee06bb..1030a2e6 100644 --- a/.github/workflows/qt-cpp.yml +++ b/.github/workflows/qt-cpp.yml @@ -34,7 +34,7 @@ jobs: - name: Configure explicit Qt product and test entries run: | $dependencyRoot = Join-Path $env:GITHUB_WORKSPACE 'local/qt-build-deps' - cmake -S . -B build/qt-cpp -G 'Visual Studio 17 2022' -A x64 ` + cmake -S . -B build/qt-cpp -G 'Visual Studio 18 2026' -A x64 ` -DTERMFORGE_BUILD_QT_SHELL=ON -DBUILD_TESTING=ON ` -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF ` "-DPython3_EXECUTABLE=$((Get-Command python).Source)" ` diff --git a/third_party/qt/README.md b/third_party/qt/README.md index 38e79f72..7b2eddc1 100644 --- a/third_party/qt/README.md +++ b/third_party/qt/README.md @@ -39,6 +39,8 @@ ctest --test-dir build/qt-cpp -C Release -R '^qt_cpp_shell$' --no-tests=error -- 专用 [Qt C++ CI](../../.github/workflows/qt-cpp.yml) 在 `windows-2025` 中执行同一准备、两个显式 Qt target 构建,以及 `qt_cpp_shell` 的 offscreen / OwnedJob smoke。它不继承默认/系统后端或 Alacritty 作业的通过结果;runner 验证不扩大 Windows 11 x64 或其他平台的产品支持声明。自动产物仅保留路径无关的准备摘要,不上传 SDK、部署目录或原始日志;Qt 原生窗口、真实输入/DPI 和干净机器运行仍须另验。 +CI 的 `windows-2025` 已实际解析到 `windows-2025-vs2026`,该映像没有 VS 2022 实例;因此工作流显式使用 [CMake 的 VS 2026 generator](https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2018%202026.html),要求 CMake 4.2 或更新。Qt SDK 仍是固定的 MSVC 2022 x64 包,不把 SDK 名称改称 VS 2026。[微软的二进制兼容说明](https://learn.microsoft.com/en-us/cpp/porting/binary-compat-2015-2017?view=msvc-170)允许较新链接器消费较早工具链构建的动态库,但实际兼容性仍由这次独立构建和会话检查证明;本地 VS 2022 验证与 hosted VS 2026 验证分别记录。 + ## 许可与分发边界 Qt 模块保留自己的许可,不由项目 MIT 许可覆盖。[Qt 6.10 许可说明](https://doc.qt.io/qt-6.10/licensing.html)与 SDK SBOM/头文件显示,当前 Core、Gui、Widgets、Test 的模块许可选项包含 LGPL-3.0-only,以及相应 GPL/商业许可选项;manifest 保留精确 SPDX 表达式及实际 SBOM 身份。当前应用计划采用**动态链接的 LGPLv3 路径**,这不是对完整产品分发条件已经满足的声明。 From 1d9b673746d3070824bd92aa234a2bed4f0cd4fa Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:01:03 +0800 Subject: [PATCH 5/5] docs: record separate Qt hosted validation and original failures (#117) --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 +- .../results/2026-10-28-qt-cpp-ci/README.md | 22 +++ .../2026-10-28-qt-cpp-ci/validation.json | 139 ++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 experiments/shell/results/2026-10-28-qt-cpp-ci/README.md create mode 100644 experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 885a4eb0..b1fc5afb 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -8,6 +8,7 @@ | 配置 | 证据定位 | |---|---| +| Qt 正式入口的独立 hosted 构建 | [固定结果](../experiments/shell/results/2026-10-28-qt-cpp-ci/README.md)、[验证身份](../experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json):固定 SDK、VS 2026 hosted 与真实 cmd 的 offscreen / OwnedJob 检查;原依赖换行和 generator 失败保留。没有原生上屏、完整部署哈希或产品包验收 | | 自主 C++ + Qt Widgets + 固定微软 ConPTY 正式目标 | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 记录用户确认;新组合的实现与验证另行留证,不引用旧默认或 Alacritty 通过结果代替 | | 既有默认/Alpha:自主 C++ + 系统 ConPTY + Win32 | R1 限定总验收和有限 VT/Unicode 子集;与当前 Qt/固定后端目标组合分列 | | Alacritty 历史参考:`termforge.4` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);结果仅证明其固定配置,不转为自主 C++ 能力 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 10edd94e..4cb58b5e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -12,7 +12,7 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| -| 正式目标:自主 C++ + Qt Widgets + 固定微软 ConPTY | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 已确认路线;[最小单会话入口 #115](https://github.com/JavaZeroo/termforge/issues/115)已合入 main,专用 CI 由 [#117](https://github.com/JavaZeroo/termforge/issues/117)接续 | 新组合尚无完整工作流验收;核心能力与 Qt 交互按实际实现逐项验证,旧默认或 Alacritty 成果不自动继承 | +| 正式目标:自主 C++ + Qt Widgets + 固定微软 ConPTY | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 已确认路线;[最小单会话入口 #115](https://github.com/JavaZeroo/termforge/issues/115)已合入 main,另有[固定依赖的专用 hosted 构建](../experiments/shell/results/2026-10-28-qt-cpp-ci/README.md) | 新组合尚无完整工作流验收;核心能力与 Qt 交互按实际实现逐项验证,旧默认或 Alacritty 成果不自动继承 | | 既有默认/Alpha:自主 C++ 核心 + 系统 ConPTY + Win32 | 现有原型与已发布体验包的固定路线,不需要 Rust;不等于新的正式组合已完成 | 已有 R1 限定验收;VT/Unicode 仍为有限子集,不能引用 Alacritty 参考的字素或 TUI 结果 | | Alacritty 参考构建:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;保留[历史核心比较](adr/0003-core-reference-and-reuse.md)与差分,不继续作为正式产品核心扩展 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md)均只证明其固定源码/配置 | | 历史 Alacritty/固定后端整合分支:`termforge.5` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),早期 `termforge.4` 联合源码 `3e5b96d` 的[原基线](research/joint-conpty-baseline.md)保留;后续限定修复已整合,尚未合入 main 或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize #51](https://github.com/JavaZeroo/termforge/issues/51) 与[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63) 已在实验父分支限定闭合,完整 resize/历史由 [#29](https://github.com/JavaZeroo/termforge/issues/29) 接续;[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52) 仍开放。[#53](https://github.com/JavaZeroo/termforge/issues/53) 的原 3MB/768B/16KiB 未完成粘贴取消/关闭、负例及八会话 data-only 两轮已限定验证并整合到实验父分支;同次真实 CF/IME 与完整官方矩阵未通过。fence 默认关闭;[ADR-0007](adr/0007-bundled-microsoft-conpty.md)已记录用户正式后端选择,接入成果须在自主 C++/Qt 路线重新整合并验证 | diff --git a/experiments/shell/results/2026-10-28-qt-cpp-ci/README.md b/experiments/shell/results/2026-10-28-qt-cpp-ci/README.md new file mode 100644 index 00000000..80781ba6 --- /dev/null +++ b/experiments/shell/results/2026-10-28-qt-cpp-ci/README.md @@ -0,0 +1,22 @@ +# 正式 C++ / Qt / 固定 ConPTY 的独立 hosted 构建 + +[#117](https://github.com/JavaZeroo/termforge/issues/117)在固定源码 `ecb559800e8146126117360ca24c519e08698ae6` 上完成专用 Windows CI;[验证身份](validation.json)分列输入、运行、原失败及限制。完整 R2 尚未完成。 + +## 实际结果 + +- [PR run 40700440118](https://github.com/JavaZeroo/termforge/actions/runs/40700440118)和[push run 40700431496](https://github.com/JavaZeroo/termforge/actions/runs/40700431496)均为 attempt 1、success。PR 实际检出 `5c3bea1ae05e5515d4cf7d6c36b1f098776be233`,与上述来源提交的 Git tree 完全一致。 +- `windows-2025` 实际为 `windows-2025-vs2026/20261021.278.1`,编译器自报 MSVC 19.51.36260.0;固定 Qt SDK 仍是 Qt 6.10.3 的 MSVC 2022 x64 包。两个 Qt target 构建成功,候选核心关闭,后端固定为微软 ConPTY 1.25.260930003。 +- 独立下载并审查 PR 的准备摘要:4358 项 SDK 文件的树哈希 `a35ab6fe76b62e4d9bd6d3c6b0d476c63583070cea5c9118ef5bb02042ffe266` 与本地两次不同目录的干净提取一致;归档、清单、SBOM 和 ConPTY 双文件另有固定哈希。仅新增相对 `qt.conf`,不修补 Qt 二进制或 `.prl`。 +- PR 的 `qt_cpp_shell` 为 1/1,通过;真实 cmd 经程序化按键得到 750,子进程自然退出37,启动错误可见、窗口关闭回收均通过。外层 Job active=0、total=7、terminated=0。CTest 单项0.62秒、总0.68秒仅作诊断记录。 + +## 保留的失败与修复 + +首次 PR/push 的依赖准备失败于许可文件身份。真实 `git -c core.autocrlf=true checkout-index` 将该 LICENSE 转为 CRLF,复现了错误哈希;`.gitattributes` 明确该文件为 LF 后恢复清单哈希,许可内容和清单哈希未改。下一源码依赖准备通过,在 configure 处因 hosted 没有 VS 2022 实例失败;按实际映像改用 VS 2026 generator 后通过。两对失败 run 与各源码均保留,不重跑覆盖。 + +独立审查发现的摘要写入失败会泄露路径问题,已用公共 CLI 反例复现并修正:无法写摘要时仍输出固定错误码 JSON、退出1,保留准备阶段状态。最终准备测试6/6;Standards 与 Spec 复核均无剩余发现。 + +## 证据边界 + +详细回读的是 PR 的准备产物和 smoke 输出;push 的 success 只引用提供者元数据。现有默认/系统后端 CI、Alacritty 参考、原本地 VS 2022 验证与这次 hosted VS 2026 验证分别保留,不能合并为完整功能验收。 + +本工作流不导出运行二进制或完整部署目录哈希,不生成可分发包。offscreen 及程序化按键不证明原生上屏、物理输入、IME、真实不同 DPI、完整 TUI 或性能等价;`presented_done=null`,性能容差未评估。原始提供者日志、SDK 和部署目录不提交。 diff --git a/experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json b/experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json new file mode 100644 index 00000000..fe3c21d3 --- /dev/null +++ b/experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json @@ -0,0 +1,139 @@ +{ + "schema": "termforge-qt-cpp-hosted-validation-v1", + "status": "limited_build_and_offscreen_session_verified_r2_incomplete", + "issue": 117, + "source_commit": "ecb559800e8146126117360ca24c519e08698ae6", + "base_commit": "29032905af719c2502cd92774b93d78a3ce005d9", + "pr_checkout_commit": "5c3bea1ae05e5515d4cf7d6c36b1f098776be233", + "source_tree_sha": "58cac4972d64dc3d6b17a738ec9cabc5492f6e46", + "pr_checkout_tree_equals_source": true, + "configuration": { + "candidate_adapter": false, + "qt_shell": true, + "build_testing": true, + "backend": "fixed-microsoft-conpty", + "platform_plugin": "offscreen", + "build_type": "Release", + "architecture": "x64", + "generator": "Visual Studio 18 2026" + }, + "environment": { + "runner_label": "windows-2025", + "runner_image": "windows-2025-vs2026", + "runner_image_version": "20261021.278.1", + "python": "3.12.10", + "compiler_identification": "MSVC 19.51.36260.0", + "msvc_tools_directory_version": "14.51.36231", + "qt_sdk_architecture": "win64_msvc2022_64" + }, + "preparation": { + "schema": "termforge-qt-build-preparation-v1", + "status": "passed", + "qt_version": "6.10.3", + "qt_architecture": "win64_msvc2022_64", + "qt_archive_sha256": "4db84dee7fe3c558f242bef0a88852613af76580dc6d2b24596479f47004dad7", + "input_manifests": { + "qt": "2796294f254f7fe0370be99d2de8f7745734c7352ac2648b1f8d9a8fab5ad2cd", + "conpty": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439" + }, + "preparation_script_sha256": "475ee564cf5aac4c23444d24d5faf38bb5506324df09b668ca6eabd0b85d0089", + "sdk_transformation": "bin/qt.conf contains [Paths] and Prefix=..; no binary or .prl patch", + "sdk": { + "files": 4358, + "tree_sha256": "a35ab6fe76b62e4d9bd6d3c6b0d476c63583070cea5c9118ef5bb02042ffe266" + }, + "conpty_version": "1.25.260930003", + "conpty_package_sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "conpty_files": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + "scope": "Windows local/CI build inputs only; no product package or platform acceptance" + }, + "downloaded_preparation_json_sha256": "58b614cc14417743375810bc22469acfbc644c2d4827c2dfcfaf1f1cfa33e22d", + "runs": [ + { + "id": 40700440118, + "event": "pull_request", + "attempt": 1, + "status": "completed", + "conclusion": "success", + "url": "https://github.com/JavaZeroo/termforge/actions/runs/40700440118" + }, + { + "id": 40700431496, + "event": "push", + "attempt": 1, + "status": "completed", + "conclusion": "success", + "url": "https://github.com/JavaZeroo/termforge/actions/runs/40700431496" + } + ], + "pr_smoke": { + "test": "qt_cpp_shell", + "passed": 1, + "total": 1, + "ctest_reported_test_seconds": 0.62, + "ctest_total_seconds": 0.68, + "supervisor": { + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 7, + "terminated": 0 + } + }, + "sample": { + "qt": "6.10.3", + "core": "cpp", + "backend": "fixed-microsoft-conpty", + "arithmetic": 750, + "child_exit": 37, + "cleanup_verified": true, + "image_changed": true, + "startup_error_visible": true, + "manual_close_reclaimed": true, + "presented_done": null + } + }, + "preserved_failures": [ + { + "head": "c59899e0a61c1b1ca3f855aa0db407cfdb3673af", + "pr_run": 40699187960, + "push_run": 40699135492, + "step": "dependency preparation", + "error": "conpty_license_identity", + "cause_evidence": "Git core.autocrlf=true checkout-index reproduced CRLF hash 5d177f23ecfeb0ea8e050b6a5a16355e1ae9a0b286436ca8f83ed08b3795be6b; explicit eol=lf yields manifest hash ad0cf28f3381ca9bb0bf101d127402d44c17bfa0991e1a00bff7ae6679e9dada." + }, + { + "head": "d174a7be0260cda1743d0c2c708087c8d874ee69", + "pr_run": 40699955889, + "push_run": 40699946013, + "step": "configure", + "error": "VS 2022 generator found no Visual Studio instance", + "cause_evidence": "Provider image is windows-2025-vs2026. New source selects VS 18 2026; Qt SDK version and bytes are unchanged." + } + ], + "review": { + "standards_remaining": 0, + "spec_remaining": 0, + "fixed": [ + "path-bearing summary write exception", + "license checkout line endings", + "hosted generator mismatch" + ] + }, + "runtime_binary_sha256": null, + "presented_done": null, + "performance_gate": "not_evaluated", + "limitations": [ + "Only the PR job's downloaded preparation artifact and detailed smoke sample were independently inspected; the push job success is provider metadata.", + "Runtime binaries and complete deployed directory hashes were not exported by these hosted jobs; this is no redistribution or clean-machine package audit.", + "Offscreen/programmatic keys do not prove native screen presentation, physical input, IME, different DPI, complete TUI or full R2.", + "The SDK archive, relocated SDK, local VS 2022 build, and hosted VS 2026 build are separate identities.", + "CTest wall times are diagnostic records, not product throughput or latency measurements.", + "Existing default/system and reference configurations have independent tests and evidence; their successes are not combined with this Qt result." + ] +}