From 3a7d7de0d332092a93d6c38a1474e19bf307d594 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 21:43:49 +0900 Subject: [PATCH 1/6] test: add unified DOSBox-X milestone runner --- .gitignore | 3 + AGENTS.md | 7 +- pyproject.toml | 2 + src/ferrolang_vm/dosboxx.py | 236 +++++++++++++++++++++++++ src/ferrolang_vm/registry_m1_m3.py | 69 ++++++++ src/ferrolang_vm/registry_m4_m6.py | 49 +++++ src/ferrolang_vm/suite.py | 22 +++ src/ferrolang_vm/test_cli.py | 72 ++++++++ tools/README.md | 32 +++- tools/tests/test_milestones_dosboxx.py | 54 ++++++ tools/toolchains/dosboxx.lock.json | 23 +++ uv.lock | 45 +++++ 12 files changed, 610 insertions(+), 4 deletions(-) create mode 100644 src/ferrolang_vm/dosboxx.py create mode 100644 src/ferrolang_vm/registry_m1_m3.py create mode 100644 src/ferrolang_vm/registry_m4_m6.py create mode 100644 src/ferrolang_vm/suite.py create mode 100644 src/ferrolang_vm/test_cli.py create mode 100644 tools/tests/test_milestones_dosboxx.py create mode 100644 tools/toolchains/dosboxx.lock.json diff --git a/.gitignore b/.gitignore index ac1e67a..e699b19 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ !.qemu/share/*.c .qemu/share/fec/ +# Reproducible DOSBox-X/Open Watcom development cache and ephemeral runs +.dosboxx/ + .qemu/*.qcow2 .qemu/*.img .qemu/*.iso diff --git a/AGENTS.md b/AGENTS.md index 629b919..c9590c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,11 +18,16 @@ VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범** ```powershell uv run ferro-vm --help uv run ferro-vm --help +uv run ferro-test --help ``` ## 검증 규칙 -- **실행 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox는 쓰지 않는다. +- 개발 중 빠른 회귀 검사는 `uv run ferro-test run --through `로 + DOSBox-X에서 수행한다. + 이 경로도 컴파일러 A를 DOS 내부 Open Watcom으로 매번 새로 빌드한다. +- **마일스톤의 최종 공식 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox-X 결과는 + 개발용 smoke test이며 완료 게이트를 대체하지 않는다. - 컴파일러 A와 생성 C 모두 VM 안의 Open Watcom으로 컴파일한다. 컴파일러 A와 bits16은 `WCL`, bits32 생성 C는 `WCL386`. - **호스트에서 컴파일하지 않는다.** WSL이나 호스트 C 컴파일러 결과는 정식 검증으로 diff --git a/pyproject.toml b/pyproject.toml index 2660113..9b0a084 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,13 @@ description = "Ferro language compiler and QEMU development automation" requires-python = ">=3.12" dependencies = [ "onnxruntime>=1.28.0", + "pytest>=9.0.0", "rapidocr>=3.9.2", ] [project.scripts] ferro-vm = "ferrolang_vm.cli:main" +ferro-test = "ferrolang_vm.test_cli:main" [build-system] requires = ["hatchling"] diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py new file mode 100644 index 0000000..1deb9c6 --- /dev/null +++ b/src/ferrolang_vm/dosboxx.py @@ -0,0 +1,236 @@ +"""Reproducible DOSBox-X/Open Watcom development test backend.""" +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +import tempfile +import urllib.request +import zipfile +from dataclasses import dataclass +from pathlib import Path + +from .daemon import ROOT +from .suite import Case + + +CACHE = ROOT / ".dosboxx" +LOCK_PATH = ROOT / "tools" / "toolchains" / "dosboxx.lock.json" +RUNS = CACHE / "runs" + + +class DosboxError(RuntimeError): + pass + + +def _lock() -> dict[str, object]: + return json.loads(LOCK_PATH.read_text(encoding="utf-8")) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + while chunk := source.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _download(name: str, spec: dict[str, object]) -> Path: + downloads = CACHE / "downloads" + downloads.mkdir(parents=True, exist_ok=True) + target = downloads / Path(str(spec["url"])).name + expected = str(spec["sha256"]).lower() + if target.is_file() and _sha256(target) == expected: + return target + target.unlink(missing_ok=True) + partial = target.with_suffix(target.suffix + ".part") + partial.unlink(missing_ok=True) + print(f"ferro-test: downloading {name} {spec['version']}...") + try: + with urllib.request.urlopen(str(spec["url"])) as response, partial.open("wb") as output: + shutil.copyfileobj(response, output, length=1024 * 1024) + except Exception: + partial.unlink(missing_ok=True) + raise + actual = _sha256(partial) + if actual != expected: + partial.unlink(missing_ok=True) + raise DosboxError(f"{name} SHA-256 mismatch: expected {expected}, got {actual}") + partial.replace(target) + return target + + +def _safe_extract(archive: Path, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}-", dir=destination.parent)) + try: + with zipfile.ZipFile(archive) as bundle: + root = temporary.resolve() + for member in bundle.infolist(): + target = (temporary / member.filename).resolve() + if target != root and root not in target.parents: + raise DosboxError(f"unsafe archive member: {member.filename}") + bundle.extractall(temporary) + if destination.exists(): + shutil.rmtree(destination) + temporary.replace(destination) + except Exception: + shutil.rmtree(temporary, ignore_errors=True) + raise + + +def _required_paths(lock: dict[str, object]) -> tuple[Path, Path, list[Path]]: + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + dosbox = CACHE / "dosbox-x" / str(dosbox_spec["executable"]) + watcom = CACHE / "watcom" + required = [watcom / str(item) for item in watcom_spec["required"]] + return dosbox, watcom, required + + +def setup(*, accept_watcom_license: bool = False) -> tuple[Path, Path]: + if os.name != "nt": + raise DosboxError("the DOSBox-X development backend currently supports Windows only") + lock = _lock() + dosbox, watcom, watcom_required = _required_paths(lock) + marker = CACHE / "SETUP.OK" + lock_hash = _sha256(LOCK_PATH) + if (marker.is_file() and marker.read_text(encoding="ascii").strip() == lock_hash + and dosbox.is_file() and all(path.is_file() for path in watcom_required)): + return dosbox, watcom + if not accept_watcom_license: + raise DosboxError( + "Open Watcom is distributed under the Sybase Open Watcom Public License. " + "Review tools/toolchains/dosboxx.lock.json and rerun " + "`uv run ferro-test setup --accept-watcom-license`." + ) + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") + _safe_extract(_download("Open Watcom", watcom_spec), watcom) + dosbox, watcom, watcom_required = _required_paths(lock) + missing = [str(path.relative_to(CACHE)) for path in [dosbox, *watcom_required] + if not path.is_file()] + if missing: + raise DosboxError("toolchain archive is missing: " + ", ".join(missing)) + marker.write_text(lock_hash + "\n", encoding="ascii") + return dosbox, watcom + + +def resolve_tools() -> tuple[Path, Path]: + lock = _lock() + dosbox, watcom, required = _required_paths(lock) + if not dosbox.is_file() or not all(path.is_file() for path in required): + raise DosboxError("toolchain is not installed; run `uv run ferro-test setup`") + return dosbox, watcom + + +def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: + lines = [ + "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", + "set WATCOM=W:", "set INCLUDE=W:\\H", + "set LIB=W:\\LIB286\\DOS;W:\\LIB286;W:\\LIB386\\DOS;W:\\LIB386", + "call BUILD.BAT", "if not exist BUILD.OK goto BUILDFAIL", + "echo PASS>RESULTS\\BUILD.RES", + ] + for index, case in enumerate(cases): + key = f"C{index:03d}" + command = case.command + if not trace_dos: + command += f" > RESULTS\\{key}.LOG" + lines.append(command) + if case.expect_success: + lines.extend([ + f"if errorlevel 1 goto {key}F", f"echo PASS>RESULTS\\{key}.RES", + f"goto {key}D", f":{key}F", f"echo FAIL>RESULTS\\{key}.RES", f":{key}D", + ]) + else: + lines.extend([ + f"if errorlevel 1 goto {key}P", f"echo FAIL>RESULTS\\{key}.RES", + f"goto {key}D", f":{key}P", f"echo PASS>RESULTS\\{key}.RES", f":{key}D", + ]) + lines.extend([ + "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", + "echo DONE>RUN.OK", *(["pause"] if show_dos else []), "exit", "", + ]) + return "\r\n".join(lines) + + +@dataclass +class SuiteRun: + root: Path + cases: list[Case] + keep: bool = False + + @property + def fec(self) -> Path: + return self.root / "FEC" + + def _key(self, case: Case) -> str: + return f"C{self.cases.index(case):03d}" + + def result(self, case: Case | None = None) -> str: + name = "BUILD" if case is None else self._key(case) + path = self.fec / "RESULTS" / f"{name}.RES" + return path.read_text(encoding="ascii").strip() if path.is_file() else "MISSING" + + def log(self, case: Case | None = None) -> str: + name = "BUILD" if case is None else self._key(case) + path = self.fec / "RESULTS" / f"{name}.LOG" + content = path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" + if content: + return content + errors = sorted(self.fec.glob("*.ERR")) + if errors: + return "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in errors) + console = self.root / "CONSOLE.LOG" + return console.read_text(encoding="utf-8", errors="replace") if console.is_file() else "" + + def cleanup(self) -> None: + if not self.keep: + shutil.rmtree(self.root, ignore_errors=True) + + +def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, + trace_dos: bool = False) -> SuiteRun: + dosbox, watcom = resolve_tools() + RUNS.mkdir(parents=True, exist_ok=True) + run_root = Path(tempfile.mkdtemp(prefix="suite-", dir=RUNS)) + result = SuiteRun(run_root, cases, keep) + fec = result.fec + try: + shutil.copytree(ROOT / "fec" / "src", fec / "SRC") + shutil.copytree(ROOT / "fec" / "std", fec / "STD") + shutil.copytree(ROOT / "fec" / "tests", fec / "TESTS") + shutil.copy2(ROOT / "fec" / "build-dos.bat", fec / "BUILD.BAT") + console = run_root / "CONSOLE.LOG" + config = run_root / "DOSBOX.CON" + config.write_text( + f"[log]\nlogfile={console}\n[dosbox]\nlog console=quiet\n", + encoding="ascii", + ) + (fec / "RUN.BAT").write_text( + _batch(cases, show_dos=show_dos, trace_dos=trace_dos), + encoding="ascii", newline="", + ) + command = [str(dosbox)] + if not show_dos: + command.append("-silent") + command.extend([ + "-fastlaunch", "-conf", str(config), + "-c", f'mount C "{run_root}"', "-c", f'mount W "{watcom}" -ro', + "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", + ]) + completed = subprocess.run(command, check=False, timeout=300) + if completed.returncode != 0: + raise DosboxError(f"DOSBox-X exited with status {completed.returncode}") + if not (fec / "RUN.OK").is_file(): + raise DosboxError("DOSBox-X did not complete the test batch") + return result + except Exception: + result.keep = True + raise diff --git a/src/ferrolang_vm/registry_m1_m3.py b/src/ferrolang_vm/registry_m1_m3.py new file mode 100644 index 0000000..a9623ab --- /dev/null +++ b/src/ferrolang_vm/registry_m1_m3.py @@ -0,0 +1,69 @@ +"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" +from __future__ import annotations + +from .suite import Case + + +def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +M1_M3_CASES: list[Case] = [] +for _name in ("basic", "literals", "keybuilt", "v012form"): + M1_M3_CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) +for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): + M1_M3_CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) +for _name in ("misssemi", "unclcomm", "logical"): + M1_M3_CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) + +for _name in ("hello", "scopes"): + _upper = _name.upper() + M1_M3_CASES.extend([ + _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), + ]) +M1_M3_CASES.extend([ + _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), +]) +_m2_outputs = { + "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", + "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", + "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", +} +for _name, _output in _m2_outputs.items(): + M1_M3_CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) + +def _m3_runtime(name: str) -> list[Case]: + upper = name.upper() + return [ + _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), + ] + + +for _name in ("struct", "enum", "array", "mutable"): + M1_M3_CASES.extend(_m3_runtime(_name)) +for _name in ("bad-mlet", "bad-shwr"): + M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) +for _name in ("str", "for", "nested", "char", "arrayctx"): + M1_M3_CASES.extend(_m3_runtime(_name)) +for _name in ("bounds", "slcbound"): + _upper = _name.upper() + M1_M3_CASES.extend([ + _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), + ]) +M1_M3_CASES.extend([ + _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), + _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), +]) +for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): + M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) diff --git a/src/ferrolang_vm/registry_m4_m6.py b/src/ferrolang_vm/registry_m4_m6.py new file mode 100644 index 0000000..48b23e0 --- /dev/null +++ b/src/ferrolang_vm/registry_m4_m6.py @@ -0,0 +1,49 @@ +"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. + +Only commands and their expected status live here; the ``.fe`` fixtures stay +under ``fec/tests`` and are copied by the runner. +""" +from __future__ import annotations + +from .suite import Case + + +def _c(milestone: int, name: str, command: str, ok: bool) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +M4_M6_CASES: list[Case] = [ + _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), + _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), + _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), + _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), + _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), + _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), + _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), +] + +_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} +for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): + _output = _m4_outputs.get(_name, _name.upper()) + M4_M6_CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) + +for _name in ("defer", "owned"): + M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) +for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): + _output = "BAD-DES" if _name == "bad-dest" else _name.upper() + M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) +M4_M6_CASES += [ + _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), + _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), + _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), +] + +for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): + M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) +for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): + M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py new file mode 100644 index 0000000..7072f51 --- /dev/null +++ b/src/ferrolang_vm/suite.py @@ -0,0 +1,22 @@ +"""Shared types and selection for the explicit milestone registries.""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Case: + id: str + milestone: int + command: str + expect_success: bool + + +def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: + from .registry_m1_m3 import M1_M3_CASES + from .registry_m4_m6 import M4_M6_CASES + + cases = [*M1_M3_CASES, *M4_M6_CASES] + if only is not None: + return [case for case in cases if case.milestone == only] + return [case for case in cases if case.milestone <= through] diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py new file mode 100644 index 0000000..8a9d1e3 --- /dev/null +++ b/src/ferrolang_vm/test_cli.py @@ -0,0 +1,72 @@ +"""Developer test entry point backed by a disposable DOSBox-X run.""" +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +from .dosboxx import DosboxError, setup + + +MILESTONES = tuple(f"m{number}" for number in range(1, 7)) + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="ferro-test", + description="Fast Ferro development tests in DOSBox-X/Open Watcom.", + ) + commands = parser.add_subparsers(dest="command", required=True) + prepare = commands.add_parser("setup", help="download and verify pinned development tools") + prepare.add_argument("--accept-watcom-license", action="store_true", + help="confirm acceptance of the Sybase Open Watcom Public License") + run = commands.add_parser("run", help="build once and run milestone pytest cases") + selection = run.add_mutually_exclusive_group() + selection.add_argument("--through", choices=MILESTONES, default="m6", + help="run cumulatively through this milestone (default: m6)") + selection.add_argument("--only", choices=MILESTONES, + help="run only this milestone's cases") + run.add_argument("-v", "--verbose", action="store_true", help="show every pytest case") + run.add_argument("--keep-failed", action="store_true", + help="keep the disposable DOS filesystem after failures") + run.add_argument("--show-dos", action="store_true", + help="show DOSBox-X and wait for a key before closing") + run.add_argument("--dos-log", action="store_true", + help="print the captured DOS console after the run") + run.add_argument("--trace-dos", action="store_true", + help="do not redirect case command output") + args = parser.parse_args() + try: + if args.command == "setup": + dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) + print(f"DOSBox-X: {dosbox}") + print(f"Open Watcom: {watcom}") + return 0 + if args.only: + os.environ["FERRO_TEST_ONLY"] = args.only + else: + os.environ["FERRO_TEST_THROUGH"] = args.through + for enabled, name in ( + (args.keep_failed, "FERRO_TEST_KEEP_FAILED"), + (args.show_dos, "FERRO_TEST_SHOW_DOS"), + (args.trace_dos, "FERRO_TEST_TRACE_DOS"), + (args.dos_log, "FERRO_TEST_DOS_LOG"), + ): + if enabled: + os.environ[name] = "1" + import pytest + test_file = os.fspath( + Path(__file__).resolve().parents[2] / "tools" / "tests" / "test_milestones_dosboxx.py" + ) + pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] + if args.dos_log: + pytest_args.append("-s") + return int(pytest.main(pytest_args)) + except (DosboxError, ValueError) as exc: + print(f"ferro-test: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/README.md b/tools/README.md index 3d909a5..2e3429e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,12 +2,37 @@ ## Host support -Automation currently supports **Windows 10/11 only**. It requires `uv`, QEMU -with WHPX support, and `ffmpeg.exe` on `PATH`. The Python implementation uses -portable APIs where possible, but other hosts are not supported yet. +Automation currently supports **Windows 10/11 only**. The fast development loop +requires only `uv`; it downloads pinned DOSBox-X and Open Watcom DOS releases. +The final milestone gate additionally requires QEMU with WHPX support and +`ffmpeg.exe` on `PATH`. Other hosts are not supported yet. ## Getting started +```powershell +uv run ferro-test setup --accept-watcom-license +uv run ferro-test run --through m6 -v +``` + +`setup` reads `tools/toolchains/dosboxx.lock.json`, downloads the exact official +archives, verifies their SHA-256 hashes, and extracts them under ignored +`.dosboxx/`. Review the Open Watcom license referenced by the lock file before +accepting it. Archives and installed tools are deliberately not committed. + +Each `run` creates a disposable DOS drive, copies the current compiler, standard +library, and fixtures, then builds `FEC.EXE` once inside DOS with Open Watcom. +All selected milestone commands execute sequentially in that same DOSBox-X +instance, while pytest reports every emit, Watcom build, runtime, and rejection +check separately. Thus stale QEMU binaries cannot make the test pass. + +`--through m6` runs cumulatively from M1; `--only m6` selects one milestone. +Use `--keep-failed` to preserve a failed drive under `.dosboxx/runs/`, +`--dos-log` to print the captured DOS console, `--trace-dos` to disable command +output redirection, and `--show-dos` to keep the GUI open until a key is pressed. + +This is the quick development smoke test. Run the QEMU/FreeDOS workflow below +for the authoritative milestone completion gate. + ```powershell uv run ferro-vm start uv run ferro-vm status @@ -18,6 +43,7 @@ The command list lives in the CLI itself, not in this file: ```powershell uv run ferro-vm --help uv run ferro-vm --help +uv run ferro-test --help ``` Working rules, verification gates, and DOS build traps are in `AGENTS.md`. diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py new file mode 100644 index 0000000..c6bc2fd --- /dev/null +++ b/tools/tests/test_milestones_dosboxx.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import os + +import pytest + +from ferrolang_vm.dosboxx import SuiteRun, run_suite +from ferrolang_vm.suite import Case, all_cases + + +def _number(name: str) -> int: + if not name.startswith("m") or not name[1:].isdigit(): + raise ValueError(f"invalid milestone: {name}") + value = int(name[1:]) + if value not in range(1, 7): + raise ValueError(f"unsupported milestone: {name}") + return value + + +ONLY = os.environ.get("FERRO_TEST_ONLY") +CASES = all_cases( + through=_number(os.environ.get("FERRO_TEST_THROUGH", "m6")), + only=_number(ONLY) if ONLY else None, +) + + +@pytest.fixture(scope="session") +def suite_run() -> SuiteRun: + run = run_suite( + CASES, + keep=os.environ.get("FERRO_TEST_KEEP_FAILED") == "1", + show_dos=os.environ.get("FERRO_TEST_SHOW_DOS") == "1", + trace_dos=os.environ.get("FERRO_TEST_TRACE_DOS") == "1", + ) + yield run + if os.environ.get("FERRO_TEST_DOS_LOG") == "1": + console = run.root / "CONSOLE.LOG" + if console.is_file(): + print(console.read_text(encoding="utf-8", errors="replace")) + run.cleanup() + + +def test_compiler_build(suite_run: SuiteRun) -> None: + assert suite_run.result() == "PASS", suite_run.log() + + +@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id) +def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: + if suite_run.result() != "PASS": + pytest.skip("compiler build failed") + assert suite_run.result(case) == "PASS", ( + f"DOS command: {case.command}\nExpected success: {case.expect_success}\n" + f"{suite_run.log(case)}" + ) diff --git a/tools/toolchains/dosboxx.lock.json b/tools/toolchains/dosboxx.lock.json new file mode 100644 index 0000000..08b1123 --- /dev/null +++ b/tools/toolchains/dosboxx.lock.json @@ -0,0 +1,23 @@ +{ + "schema": 1, + "dosboxx": { + "version": "2026.08.02", + "url": "https://github.com/joncampbell123/dosbox-x/releases/download/dosbox-x-v2026.08.02/dosbox-x-vsbuild-win64-2026.08.02-portable.zip", + "sha256": "ca28208f5fee25a74caf3a02cc0189c7f7943a42ce37c02848f5fc03450a96cf", + "executable": "bin/x64/Release/dosbox-x.exe" + }, + "open_watcom": { + "version": "2026-08-01-Build", + "url": "https://github.com/open-watcom/open-watcom-v2/releases/download/2026-08-01-Build/open-watcom-2_0-c-dos.exe", + "sha256": "80db4ab340f382e59bf3d396280576ec837964c2ef00e8ddd3b2b3724ab63edf", + "required": [ + "binw/wcl.exe", + "binw/wcl386.exe", + "binp/wlink.exe", + "h/stdio.h", + "lib286/dos/clibl.lib", + "lib386/dos/clib3r.lib", + "license.txt" + ] + } +} diff --git a/uv.lock b/uv.lock index c8f60c6..c7b989e 100644 --- a/uv.lock +++ b/uv.lock @@ -175,12 +175,14 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "onnxruntime" }, + { name = "pytest" }, { name = "rapidocr" }, ] [package.metadata] requires-dist = [ { name = "onnxruntime", specifier = ">=1.28.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "rapidocr", specifier = ">=3.9.2" }, ] @@ -201,6 +203,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + [[package]] name = "numpy" version = "2.5.2" @@ -418,6 +429,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + [[package]] name = "protobuf" version = "7.35.1" @@ -463,6 +483,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608 }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, +] + [[package]] name = "pyyaml" version = "6.0.3" From 0dada80e65cd08a95a718d926c31569803f1bedf Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 21:54:22 +0900 Subject: [PATCH 2/6] test: unify registry and capture Watcom diagnostics --- src/ferrolang_vm/dosboxx.py | 22 ++++- src/ferrolang_vm/registry.py | 120 +++++++++++++++++++++++++ src/ferrolang_vm/registry_m1_m3.py | 69 -------------- src/ferrolang_vm/registry_m4_m6.py | 49 ---------- src/ferrolang_vm/suite.py | 9 +- tools/tests/test_milestones_dosboxx.py | 11 ++- 6 files changed, 149 insertions(+), 131 deletions(-) create mode 100644 src/ferrolang_vm/registry.py delete mode 100644 src/ferrolang_vm/registry_m1_m3.py delete mode 100644 src/ferrolang_vm/registry_m4_m6.py diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 1deb9c6..581f14c 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -140,19 +140,29 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: for index, case in enumerate(cases): key = f"C{index:03d}" command = case.command + # Watcom writes diagnostics into the current directory. Isolate each + # case so a later pytest item never sees stale diagnostics. + lines.extend([ + "if exist *.ERR del *.ERR > NUL", + f"if exist RESULTS\\{key}.ERR del RESULTS\\{key}.ERR > NUL", + ]) if not trace_dos: command += f" > RESULTS\\{key}.LOG" lines.append(command) if case.expect_success: lines.extend([ - f"if errorlevel 1 goto {key}F", f"echo PASS>RESULTS\\{key}.RES", - f"goto {key}D", f":{key}F", f"echo FAIL>RESULTS\\{key}.RES", f":{key}D", + f"if errorlevel 1 goto {key}E", f"echo PASS>RESULTS\\{key}.RES", + f"goto {key}C", f":{key}E", f"echo FAIL>RESULTS\\{key}.RES", ]) else: lines.extend([ - f"if errorlevel 1 goto {key}P", f"echo FAIL>RESULTS\\{key}.RES", - f"goto {key}D", f":{key}P", f"echo PASS>RESULTS\\{key}.RES", f":{key}D", + f"if errorlevel 1 goto {key}E", f"echo FAIL>RESULTS\\{key}.RES", + f"goto {key}C", f":{key}E", f"echo PASS>RESULTS\\{key}.RES", ]) + lines.extend([ + f":{key}C", f"if exist *.ERR type *.ERR > RESULTS\\{key}.ERR", + f"if not exist RESULTS\\{key}.ERR type NUL > RESULTS\\{key}.ERR", + ]) lines.extend([ "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", "echo DONE>RUN.OK", *(["pause"] if show_dos else []), "exit", "", @@ -190,6 +200,10 @@ class SuiteRun: console = self.root / "CONSOLE.LOG" return console.read_text(encoding="utf-8", errors="replace") if console.is_file() else "" + def err(self, case: Case) -> str: + path = self.fec / "RESULTS" / f"{self._key(case)}.ERR" + return path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" + def cleanup(self) -> None: if not self.keep: shutil.rmtree(self.root, ignore_errors=True) diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py new file mode 100644 index 0000000..5ef627e --- /dev/null +++ b/src/ferrolang_vm/registry.py @@ -0,0 +1,120 @@ +"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" +from __future__ import annotations + +from .suite import Case + + +def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +CASES: list[Case] = [] +for _name in ("basic", "literals", "keybuilt", "v012form"): + CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) +for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): + CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) +for _name in ("misssemi", "unclcomm", "logical"): + CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) + +for _name in ("hello", "scopes"): + _upper = _name.upper() + CASES.extend([ + _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), + ]) +CASES.extend([ + _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), +]) +_m2_outputs = { + "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", + "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", + "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", +} +for _name, _output in _m2_outputs.items(): + CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) + +def _m3_runtime(name: str) -> list[Case]: + upper = name.upper() + return [ + _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), + ] + + +for _name in ("struct", "enum", "array", "mutable"): + CASES.extend(_m3_runtime(_name)) +for _name in ("bad-mlet", "bad-shwr"): + CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) +for _name in ("str", "for", "nested", "char", "arrayctx"): + CASES.extend(_m3_runtime(_name)) +for _name in ("bounds", "slcbound"): + _upper = _name.upper() + CASES.extend([ + _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), + ]) +CASES.extend([ + _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), + _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), +]) +for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): + CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) + +"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. + +Only commands and their expected status live here; the ``.fe`` fixtures stay +under ``fec/tests`` and are copied by the runner. +""" +def _c(milestone: int, name: str, command: str, ok: bool) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +CASES.extend([ + _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), + _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), + _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), + _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), + _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), + _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), + _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), +]) + +_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} +for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): + _output = _m4_outputs.get(_name, _name.upper()) + CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) + +for _name in ("defer", "owned"): + CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) +for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): + _output = "BAD-DES" if _name == "bad-dest" else _name.upper() + CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) +CASES += [ + _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), + _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), + _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), +] + +for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): + CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) +for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): + CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) + + +def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: + if only is not None: + return [case for case in CASES if case.milestone == only] + return [case for case in CASES if case.milestone <= through] diff --git a/src/ferrolang_vm/registry_m1_m3.py b/src/ferrolang_vm/registry_m1_m3.py deleted file mode 100644 index a9623ab..0000000 --- a/src/ferrolang_vm/registry_m1_m3.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" -from __future__ import annotations - -from .suite import Case - - -def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) - - -M1_M3_CASES: list[Case] = [] -for _name in ("basic", "literals", "keybuilt", "v012form"): - M1_M3_CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) -for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): - M1_M3_CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) -for _name in ("misssemi", "unclcomm", "logical"): - M1_M3_CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) - -for _name in ("hello", "scopes"): - _upper = _name.upper() - M1_M3_CASES.extend([ - _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), - ]) -M1_M3_CASES.extend([ - _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), -]) -_m2_outputs = { - "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", - "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", - "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", -} -for _name, _output in _m2_outputs.items(): - M1_M3_CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) - -def _m3_runtime(name: str) -> list[Case]: - upper = name.upper() - return [ - _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), - ] - - -for _name in ("struct", "enum", "array", "mutable"): - M1_M3_CASES.extend(_m3_runtime(_name)) -for _name in ("bad-mlet", "bad-shwr"): - M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) -for _name in ("str", "for", "nested", "char", "arrayctx"): - M1_M3_CASES.extend(_m3_runtime(_name)) -for _name in ("bounds", "slcbound"): - _upper = _name.upper() - M1_M3_CASES.extend([ - _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), - ]) -M1_M3_CASES.extend([ - _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), - _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), -]) -for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): - M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) diff --git a/src/ferrolang_vm/registry_m4_m6.py b/src/ferrolang_vm/registry_m4_m6.py deleted file mode 100644 index 48b23e0..0000000 --- a/src/ferrolang_vm/registry_m4_m6.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. - -Only commands and their expected status live here; the ``.fe`` fixtures stay -under ``fec/tests`` and are copied by the runner. -""" -from __future__ import annotations - -from .suite import Case - - -def _c(milestone: int, name: str, command: str, ok: bool) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) - - -M4_M6_CASES: list[Case] = [ - _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), - _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), - _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), - _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), - _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), - _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), - _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), -] - -_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} -for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): - _output = _m4_outputs.get(_name, _name.upper()) - M4_M6_CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) - -for _name in ("defer", "owned"): - M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) -for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): - _output = "BAD-DES" if _name == "bad-dest" else _name.upper() - M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) -M4_M6_CASES += [ - _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), - _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), - _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), -] - -for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): - M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) -for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): - M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py index 7072f51..b402d4e 100644 --- a/src/ferrolang_vm/suite.py +++ b/src/ferrolang_vm/suite.py @@ -13,10 +13,5 @@ class Case: def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: - from .registry_m1_m3 import M1_M3_CASES - from .registry_m4_m6 import M4_M6_CASES - - cases = [*M1_M3_CASES, *M4_M6_CASES] - if only is not None: - return [case for case in cases if case.milestone == only] - return [case for case in cases if case.milestone <= through] + from .registry import all_cases as _all_cases + return _all_cases(through=through, only=only) diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index c6bc2fd..a1c0ea1 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import warnings import pytest @@ -48,7 +49,13 @@ def test_compiler_build(suite_run: SuiteRun) -> None: def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: if suite_run.result() != "PASS": pytest.skip("compiler build failed") - assert suite_run.result(case) == "PASS", ( + result = suite_run.result(case) + err = suite_run.err(case) + if result == "PASS" and err: + warning_lines = [line for line in err.splitlines() if "warning" in line.lower()] + if warning_lines: + warnings.warn("\n".join(warning_lines), stacklevel=1) + assert result == "PASS", ( f"DOS command: {case.command}\nExpected success: {case.expect_success}\n" - f"{suite_run.log(case)}" + f"{suite_run.log(case)}\n{err}" ) From c25312135d13830695e0d74b5e3f6c8006726681 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:10:46 +0900 Subject: [PATCH 3/6] refactor: rebuild the milestone registry as a table registry.py was literally `cat registry_m1_m3.py registry_m4_m6.py`, which left a duplicate `_c` definition and a stray module-level docstring at the seam. Replace the hand-rolled append loops with five builders -- _emit, _wcl, _triple, _rejects, _dump_ast -- and express the cases as one ordered list. The irregularities are now parameters instead of one-off code, each with the reason recorded: - _triple(stem=) for M2 castwhil emitting CAST16 - _triple(build_source=) for M4 prop, which emits PROP.C but compiles PROPTEST.C because that file #includes it - _triple(run_suffix="trap", run_ok=False) for the M3 bounds cases - _triple(emit_suffix=None) for M4, whose ids lack the -emit suffix - _emit(output_first=True) for M6, which passes -o before the input The three scattered 8.3 output-name mappings collapse into one _OUT83 table keyed by (milestone, name). The key needs both: bad-type is BAD-TY in M2 but BAD-TYP in M4, and bad-cond is BAD-CO in M2 but unshortened in M5. Values are carried over verbatim -- the shortenings are inconsistent and several were never required, but that is a separate decision. suite.py keeps only Case and drops the all_cases forwarder, so the registry -> suite -> registry cycle is gone along with the function-scoped import that worked around it. dosboxx.py still imports Case from suite and is untouched. Verified behaviour-preserving by snapshotting (id, milestone, command, expect_success) for all 150 cases in order before and after: diff is empty. pytest collects the same 151 items. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- src/ferrolang_vm/registry.py | 265 ++++++++++++++++--------- src/ferrolang_vm/suite.py | 11 +- tools/tests/test_milestones_dosboxx.py | 3 +- 3 files changed, 176 insertions(+), 103 deletions(-) diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 5ef627e..99eecc4 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -1,119 +1,192 @@ -"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" +"""Milestone case registry: DOS commands and their expected exit status. + +Only commands live here; the ``.fe`` fixtures stay under ``fec/tests`` and are +copied into the disposable DOS filesystem by the runner. Case order is load +bearing -- ``emit`` must precede ``build`` must precede ``run`` for the same +fixture, because each step consumes the previous step's output. +""" from __future__ import annotations from .suite import Case +PASS = "TESTS\\PASS" +FAIL = "TESTS\\FAIL" +STD = "STD" +M2 = "TESTS\\M2" +M3 = "TESTS\\M3" +M4 = "TESTS\\M4" +M5 = "TESTS\\M5" +M6 = "TESTS\\M6" +OUT = "OUT" -def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: +# Emitted-C basenames that were hand-shortened for DOS 8.3. Keyed by milestone +# because the same fixture name maps to different outputs across milestones +# (``bad-type`` is BAD-TY in M2 but BAD-TYP in M4). The shortenings are not +# consistent -- M2 cut to six characters, M4 to seven, and several were never +# required at all since BAD-COND is already a legal 8.3 name. Preserved verbatim; +# changing one renames a file inside the DOS run, so re-verify if you touch it. +_OUT83 = { + (2, "bad-cond"): "BAD-CO", + (2, "bad-cast"): "BAD-CA", + (2, "bad-asgn"): "BAD-AS", + (2, "bad-unk"): "BAD-UN", + (2, "bad-ari"): "BAD-AR", + (2, "bad-type"): "BAD-TY", + (2, "bad-ret"): "BAD-RE", + (2, "bad-unit"): "BAD-UI", + (2, "bad-void"): "BAD-VO", + (4, "bad-type"): "BAD-TYP", + (4, "bad-writ"): "BAD-WRI", + (5, "bad-dest"): "BAD-DES", +} + + +def _case(milestone: int, name: str, command: str, ok: bool = True) -> Case: return Case(f"m{milestone}-{name}", milestone, command, ok) -CASES: list[Case] = [] -for _name in ("basic", "literals", "keybuilt", "v012form"): - CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) -for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): - CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) -for _name in ("misssemi", "unclcomm", "logical"): - CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) +def _fe(directory: str, name: str) -> str: + return f"{directory}\\{name.upper()}.FE" -for _name in ("hello", "scopes"): - _upper = _name.upper() - CASES.extend([ - _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), - ]) -CASES.extend([ - _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), -]) -_m2_outputs = { - "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", - "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", - "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", -} -for _name, _output in _m2_outputs.items(): - CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) -def _m3_runtime(name: str) -> list[Case]: - upper = name.upper() +def _emit(source: str, output: str, *, target: str = "bits32", + flags: tuple[str, ...] = (), output_first: bool = False) -> str: + """``fec`` invocation that translates ``source`` to C at ``output``. + + ``output_first`` reproduces the M6 cases, which pass ``-o`` before the input + file while every other milestone passes it after. + """ + parts = ["FEC.EXE", f"--target={target}", *flags, "--emit-c"] + parts += ["-o", output, source] if output_first else [source, "-o", output] + return " ".join(parts) + + +def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False, + defines: tuple[str, ...] = ()) -> str: + """Open Watcom invocation. ``strict`` is the M4 ``-wx -wcd=202`` pairing: + warnings are errors except W202, which the generated C trips on unused + helpers (see AGENTS.md).""" + parts = ["WCL386" if bits == 32 else "WCL", "-q", "-za"] + if strict: + parts += ["-wx", "-wcd=202"] + parts += ["-bt=dos", *defines, f"-fe={exe}", *sources] + return " ".join(parts) + + +def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *, + suffix: str, ok: bool = True, prefix: str = "") -> list[Case]: return [ - _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), + _case(milestone, f"{prefix}{name}-{suffix}", + f"FEC.EXE --dump-ast {_fe(directory, name)}", ok) + for name in names ] -for _name in ("struct", "enum", "array", "mutable"): - CASES.extend(_m3_runtime(_name)) -for _name in ("bad-mlet", "bad-shwr"): - CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) -for _name in ("str", "for", "nested", "char", "arrayctx"): - CASES.extend(_m3_runtime(_name)) -for _name in ("bounds", "slcbound"): - _upper = _name.upper() - CASES.extend([ - _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), - ]) -CASES.extend([ - _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), - _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), -]) -for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): - CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) - -"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. - -Only commands and their expected status live here; the ``.fe`` fixtures stay -under ``fec/tests`` and are copied by the runner. -""" -def _c(milestone: int, name: str, command: str, ok: bool) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) +def _rejects(milestone: int, directory: str, names: tuple[str, ...], *, + suffix: str = "") -> list[Case]: + """Fixtures that must fail to compile. The emitted-C path is still spelled + out because ``fec`` needs an ``-o`` even when it is expected to bail.""" + return [ + _case(milestone, f"{name}-{suffix}" if suffix else name, + _emit(_fe(directory, name), + f"{directory}\\{_OUT83.get((milestone, name), name.upper())}.C"), + False) + for name in names + ] -CASES.extend([ - _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), - _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), - _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), - _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), - _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), - _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), - _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), -]) +def _triple(milestone: int, name: str, directory: str, *, stem: str | None = None, + target: str = "bits32", bits: int = 32, strict: bool = False, + build_source: str | None = None, emit_suffix: str | None = "emit", + run_suffix: str = "run", run_ok: bool = True) -> list[Case]: + """emit -> build -> run for one fixture. -_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} -for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): - _output = _m4_outputs.get(_name, _name.upper()) - CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) + ``stem`` renames the C/EXE pair when the fixture name does not fit 8.3 or + collides (M2 castwhil emits CAST16). ``build_source`` compiles a different + file than the one emitted (M4 prop emits PROP.C but builds PROPTEST.C, which + ``#include``s it). + """ + stem = stem or name.upper() + cfile = f"{directory}\\{stem}.C" + exe = f"{directory}\\{stem}.EXE" + emit_id = f"{name}-{emit_suffix}" if emit_suffix else name + return [ + _case(milestone, emit_id, _emit(_fe(directory, name), cfile, target=target)), + _case(milestone, f"{name}-build", + _wcl(exe, build_source or cfile, bits=bits, strict=strict)), + _case(milestone, f"{name}-{run_suffix}", exe, run_ok), + ] -for _name in ("defer", "owned"): - CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) -for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): - _output = "BAD-DES" if _name == "bad-dest" else _name.upper() - CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) -CASES += [ - _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), - _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), - _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), + +CASES: list[Case] = [ + # -- M1: parse only ------------------------------------------------------- + *_dump_ast(1, PASS, ("basic", "literals", "keybuilt", "v012form"), suffix="parse"), + *_dump_ast(1, STD, ("core", "fmt", "io", "list", "map", "mem", "str", "sys"), + suffix="parse", prefix="std-"), + *_dump_ast(1, FAIL, ("misssemi", "unclcomm", "logical"), suffix="reject", ok=False), + + # -- M2: first generated C ------------------------------------------------ + *_triple(2, "hello", M2), + *_triple(2, "scopes", M2), + *_triple(2, "castwhil", M2, stem="CAST16", target="bits16", bits=16), + *_rejects(2, M2, ("bad-cond", "bad-cast", "bad-asgn", "bad-unk", "bad-ari", + "bad-type", "bad-ret", "bad-unit", "bad-void"), suffix="reject"), + + # -- M3: aggregates, strings, bounds checks ------------------------------- + *_triple(3, "struct", M3), + *_triple(3, "enum", M3), + *_triple(3, "array", M3), + *_triple(3, "mutable", M3), + *_rejects(3, M3, ("bad-mlet", "bad-shwr"), suffix="reject"), + *_triple(3, "str", M3), + *_triple(3, "for", M3), + *_triple(3, "nested", M3), + *_triple(3, "char", M3), + *_triple(3, "arrayctx", M3), + # These two must trap at runtime: the bounds check is the feature under test. + *_triple(3, "bounds", M3, run_suffix="trap", run_ok=False), + *_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False), + _case(3, "bounds-no-checks-emit", + _emit(_fe(M3, "bounds"), f"{M3}\\BOUNDS-N.C", flags=("--no-checks",))), + _case(3, "bounds-no-checks-build", + _wcl(f"{M3}\\BOUNDS-N.EXE", f"{M3}\\BOUNDS-N.C")), + *_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", + "badfield", "badindex"), suffix="reject"), + + # -- M4: formatting and error propagation --------------------------------- + *_triple(4, "format", M4, strict=True, emit_suffix=None), + *_triple(4, "try-fpr", M4, strict=True, emit_suffix=None), + *_triple(4, "prop", M4, strict=True, emit_suffix=None, + build_source=f"{M4}\\PROPTEST.C"), + *_rejects(4, M4, ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", + "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")), + + # -- M5: defer and ownership ---------------------------------------------- + _case(5, "defer", _emit(_fe(M5, "defer"), f"{M5}\\DEFER.C")), + _case(5, "owned", _emit(_fe(M5, "owned"), f"{M5}\\OWNED.C")), + *_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", + "bad-proj", "bad-clos", "bad-loop")), + # The runtime case links the generated C against a hand-written allocator + # shim, so malloc/free are redirected at compile time. + _case(5, "runtime", _emit(_fe(M5, "runtime"), f"{M5}\\RUNT-G.C")), + _case(5, "runtime-build", + _wcl(f"{M5}\\RUNTIME.EXE", f"{M5}\\RUNT-G.C", f"{M5}\\RUNTIME.C", + defines=("-dmalloc=m5_malloc", "-dfree=m5_free"))), + _case(5, "runtime-run", f"{M5}\\RUNTIME.EXE"), + + # -- M6: borrow checking (R1--R8) ----------------------------------------- + *[_case(6, name, f"FEC.EXE --check {_fe(M6, name)}", False) for name in ( + "badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", + "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", + "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", + "badtwo", "badup", "badweak")], + *[_case(6, name, _emit(_fe(M6, name), f"{OUT}\\{name.upper()}.C", + output_first=True)) for name in ( + "okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", + "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", + "okstatic", "oktemp", "oktrim", "okwcall")], ] -for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): - CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) -for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): - CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) - - def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: if only is not None: return [case for case in CASES if case.milestone == only] diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py index b402d4e..b412298 100644 --- a/src/ferrolang_vm/suite.py +++ b/src/ferrolang_vm/suite.py @@ -1,4 +1,8 @@ -"""Shared types and selection for the explicit milestone registries.""" +"""The one type shared by the case registry and the DOSBox-X runner. + +Kept separate from ``registry`` so that ``dosboxx`` can depend on the type +without importing the case data. +""" from __future__ import annotations from dataclasses import dataclass @@ -10,8 +14,3 @@ class Case: milestone: int command: str expect_success: bool - - -def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: - from .registry import all_cases as _all_cases - return _all_cases(through=through, only=only) diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index a1c0ea1..32d2a01 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -6,7 +6,8 @@ import warnings import pytest from ferrolang_vm.dosboxx import SuiteRun, run_suite -from ferrolang_vm.suite import Case, all_cases +from ferrolang_vm.registry import all_cases +from ferrolang_vm.suite import Case def _number(name: str) -> int: From 4ad3e3097bd60d37f2fdbe193e1d6c47a9a02947 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:11:36 +0900 Subject: [PATCH 4/6] refactor: derive the milestone bounds from the registry The highest supported milestone was spelled out in six places across four files: range(1, 7) and default="m6" in test_cli.py, the same pair in test_milestones_dosboxx.py, and through=6 in both registry.py and suite.py. Registering M7 meant finding all six, and missing one failed silently. Derive MAX_MILESTONE and MILESTONES from CASES instead, and move the mN selector parser to registry.milestone_number so the pytest module stops carrying its own copy. Adding cases for a new milestone is now enough for ferro-test to accept --through/--only for it. No behaviour change: MAX_MILESTONE evaluates to 6, ferro-test still advertises {m1..m6} with default m6, and the case snapshot is unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- src/ferrolang_vm/registry.py | 17 ++++++++++++++++- src/ferrolang_vm/test_cli.py | 9 ++++----- tools/tests/test_milestones_dosboxx.py | 16 +++------------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 99eecc4..5478bbc 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -187,7 +187,22 @@ CASES: list[Case] = [ "okstatic", "oktemp", "oktrim", "okwcall")], ] -def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: +MAX_MILESTONE: int = max(case.milestone for case in CASES) +MILESTONES: tuple[str, ...] = tuple(f"m{number}" + for number in range(1, MAX_MILESTONE + 1)) + + +def milestone_number(name: str) -> int: + """Parse an ``mN`` selector against the milestones the registry knows about.""" + if not name.startswith("m") or not name[1:].isdigit(): + raise ValueError(f"invalid milestone: {name}") + value = int(name[1:]) + if value not in range(1, MAX_MILESTONE + 1): + raise ValueError(f"unsupported milestone: {name}") + return value + + +def all_cases(*, through: int = MAX_MILESTONE, only: int | None = None) -> list[Case]: if only is not None: return [case for case in CASES if case.milestone == only] return [case for case in CASES if case.milestone <= through] diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py index 8a9d1e3..e20f744 100644 --- a/src/ferrolang_vm/test_cli.py +++ b/src/ferrolang_vm/test_cli.py @@ -7,9 +7,7 @@ import sys from pathlib import Path from .dosboxx import DosboxError, setup - - -MILESTONES = tuple(f"m{number}" for number in range(1, 7)) +from .registry import MAX_MILESTONE, MILESTONES def main() -> int: @@ -23,8 +21,9 @@ def main() -> int: help="confirm acceptance of the Sybase Open Watcom Public License") run = commands.add_parser("run", help="build once and run milestone pytest cases") selection = run.add_mutually_exclusive_group() - selection.add_argument("--through", choices=MILESTONES, default="m6", - help="run cumulatively through this milestone (default: m6)") + selection.add_argument("--through", choices=MILESTONES, default=f"m{MAX_MILESTONE}", + help="run cumulatively through this milestone " + f"(default: m{MAX_MILESTONE})") selection.add_argument("--only", choices=MILESTONES, help="run only this milestone's cases") run.add_argument("-v", "--verbose", action="store_true", help="show every pytest case") diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index 32d2a01..c386b62 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -6,23 +6,13 @@ import warnings import pytest from ferrolang_vm.dosboxx import SuiteRun, run_suite -from ferrolang_vm.registry import all_cases +from ferrolang_vm.registry import MAX_MILESTONE, all_cases, milestone_number from ferrolang_vm.suite import Case - -def _number(name: str) -> int: - if not name.startswith("m") or not name[1:].isdigit(): - raise ValueError(f"invalid milestone: {name}") - value = int(name[1:]) - if value not in range(1, 7): - raise ValueError(f"unsupported milestone: {name}") - return value - - ONLY = os.environ.get("FERRO_TEST_ONLY") CASES = all_cases( - through=_number(os.environ.get("FERRO_TEST_THROUGH", "m6")), - only=_number(ONLY) if ONLY else None, + through=milestone_number(os.environ.get("FERRO_TEST_THROUGH", f"m{MAX_MILESTONE}")), + only=milestone_number(ONLY) if ONLY else None, ) From 594704a07d2757941a3d84047ab3db82d5dc711b Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:14:50 +0900 Subject: [PATCH 5/6] dev: promote DOSBox-X and remove QEMU support --- .gitignore | 21 +- .qemu/install.ps1 | 28 -- .qemu/readme.txt | 144 --------- .qemu/send-keys.ps1 | 60 ---- .qemu/setup.ps1 | 26 -- AGENTS.md | 65 ++-- TODO.md | 27 -- fec/vm-m1.bat | 201 ------------ pyproject.toml | 6 +- src/ferrolang_vm/__init__.py | 2 +- src/ferrolang_vm/cli.py | 195 ------------ src/ferrolang_vm/daemon.py | 467 --------------------------- src/ferrolang_vm/dos_cli.py | 80 +++++ src/ferrolang_vm/dosboxx.py | 33 +- src/ferrolang_vm/paths.py | 5 + tools/README.md | 111 +++---- tools/qemu_ocr.py | 79 ----- tools/tcpagent/BUILD.BAT | 6 - tools/tcpagent/INSTALL.BAT | 15 - tools/tcpagent/Makefile | 38 --- tools/tcpagent/README.md | 101 ------ tools/tcpagent/REBUILD.BAT | 22 -- tools/tcpagent/WPP.RSP | 11 - tools/tcpagent/tcpagent.cfg | 12 - tools/tcpagent/tcpagent.cpp | 303 ------------------ uv.lock | 599 +---------------------------------- 26 files changed, 176 insertions(+), 2481 deletions(-) delete mode 100644 .qemu/install.ps1 delete mode 100644 .qemu/readme.txt delete mode 100644 .qemu/send-keys.ps1 delete mode 100644 .qemu/setup.ps1 delete mode 100644 TODO.md delete mode 100644 fec/vm-m1.bat delete mode 100644 src/ferrolang_vm/cli.py delete mode 100644 src/ferrolang_vm/daemon.py create mode 100644 src/ferrolang_vm/dos_cli.py create mode 100644 src/ferrolang_vm/paths.py delete mode 100644 tools/qemu_ocr.py delete mode 100644 tools/tcpagent/BUILD.BAT delete mode 100644 tools/tcpagent/INSTALL.BAT delete mode 100644 tools/tcpagent/Makefile delete mode 100644 tools/tcpagent/README.md delete mode 100644 tools/tcpagent/REBUILD.BAT delete mode 100644 tools/tcpagent/WPP.RSP delete mode 100644 tools/tcpagent/tcpagent.cfg delete mode 100644 tools/tcpagent/tcpagent.cpp diff --git a/.gitignore b/.gitignore index e699b19..6979fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,9 @@ -# QEMU runtime state, downloaded media, and generated captures -.qemu/* -!.qemu/*.ps1 -!.qemu/*.mjs -!.qemu/*.txt -!.qemu/share/ -!.qemu/share/*.c -.qemu/share/fec/ +# Legacy local QEMU state is no longer used, but may contain large user-owned images. +.qemu/ # Reproducible DOSBox-X/Open Watcom development cache and ephemeral runs .dosboxx/ -.qemu/*.qcow2 -.qemu/*.img -.qemu/*.iso -.qemu/*.zip -.qemu/*.png -.qemu/*.ppm -.qemu/*.tmp - # Windows reserved-device artifact: `> nul` under Git Bash creates a real file. # Committing it breaks checkout on Windows. nul @@ -39,6 +25,3 @@ __pycache__/ node_modules/ .npm/ .cache/ - -# Temporary DOS build sources -.qemu/share/dosag*.c diff --git a/.qemu/install.ps1 b/.qemu/install.ps1 deleted file mode 100644 index 2d3ade9..0000000 --- a/.qemu/install.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' - -if (-not (Test-Path -LiteralPath $disk)) { - throw "Missing $disk. Create it first with: qemu-img create -f qcow2 .qemu\\freedos.qcow2 2G" -} -if (-not (Test-Path -LiteralPath $iso)) { - throw "Missing $iso. Run .qemu\\setup.ps1 first." -} -if (Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue) { - throw 'QEMU monitor port 4444 is already in use. Stop the existing VM first.' -} - -& $qemu.Source ` - -machine pc,usb=on ` - -cpu pentium3 ` - -m 64 ` - -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` - -drive "file=$iso,media=cdrom,readonly=on" ` - -nic user,model=ne2k_isa ` - -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` - -serial null ` - -boot order=d ` - -display default diff --git a/.qemu/readme.txt b/.qemu/readme.txt deleted file mode 100644 index bd4facd..0000000 --- a/.qemu/readme.txt +++ /dev/null @@ -1,144 +0,0 @@ -############################################################################### - FreeDOS 1.4 ("FreeDOS 1.4") -############################################################################### - - -------------------------------------------------------------------------------- - General system requirements: -------------------------------------------------------------------------------- - - * DOS-compatible system (Intel + BIOS, or UEFI with Legacy support) - - * At least 20MB free disk space: - - 20MB Plain DOS system - 30MB Plain DOS system, with sources - - 275MB Full installation including applications and games - 450MB Full installation with sources - - -------------------------------------------------------------------------------- - What's in all those zip files? -------------------------------------------------------------------------------- - -FD14-LiveCD.zip - - * FD14BOOT.IMG - Basic FreeDOS installation boot floppy image. - If your computer has a CD-ROM drive, but you cannot boot from the Live CD - or Legacy CD. Use this diskette image to boot the system. Then insert the - install CD. The FreeDOS installer should do the rest. This diskette - image is for installation purposes only and does not provide a Live - Environment. - - * FD14LIVE.ISO - The FreeDOS 1.4 installer. Most users should - use this image to install FreeDOS. - - Depending on your computer system and hardware configuration, you - can also use the LiveCD to boot and run FreeDOS directly from the - CD-ROM without installation to your hard drive. - -FD14-LegacyCD.zip - - * FD14BOOT.IMG - This zip archive also contains a copy of the basic - CD-ROM installation boot floppy. - - * FD14LGCY.ISO - A bootable CD image designed for older hardware. If - you cannot boot the LiveCD to install FreeDOS, try this disc image. - - This disc image uses the older El Torito boot CD format. Some newer - computers and virtual machines cannot use this older format. Unless - you have a computer that requires this type of bootable CD, we - recommend using the LiveCD instead. - -FD14-BonusCD.zip - - * FD14BNS.ISO - A non-bootable CD image that contains some FreeDOS - packages that are not installed as part of either the LiveCD or - the Legacy CD. - -FD14-LiteUSB.zip - - * FD14LITE.IMG - A minimal FreeDOS installer, as a USB fob drive - image. This does not contain all of the packages from either the - LiveCD or the LegacyCD, and instead only contains a basic set of - FreeDOS packages. - - * FD14LITE.VMDK - A virtual machine disk file, compatible with a - variety of virtual machine software including VirtualBox, VMware, - and other systems. - - Using a VMDK file can simplify installing FreeDOS. Just attach the - VMDK image to your virtual machine software as a hard drive, and - boot it. (Please note that you will still need to create a virtual - hard disk to install FreeDOS) - -FD14-FullUSB.zip - - * FD14FULL.IMG - Plain DOS system and Full install USB stick image. - - * FD14FULL.VMDK - A virtual machine disk file, compatible with a - variety of virtual machine software. Just attach the VMDK image to - your virtual machine as a hard drive, and boot it. - -VERIFY.TXT - - * Contains MD5, SHA256 and SHA512 hashes for all of the different - release files. You can verify your copy of FreeDOS with these. - -README.TXT - - * The "before you choose and install" document. (All of the zip - files listed above also have a copy of the README file.) - - -------------------------------------------------------------------------------- - FreeDOS Floppy-Only Edition (FD14-x86) -------------------------------------------------------------------------------- - -FreeDOS 1.4 includes a Floppy-Only Edition! This edition should run on -any hardware that can run FreeDOS and has EGA or better graphics: - - * Are you running a '286 or another classic system without a CD-ROM - drive? Install from these floppies to install FreeDOS. - - * Do you have just one hard drive and no CD or floppy drive? Just - copy the contents of the floppies to a temporary directory and run - the installer from there. - - * Want to perform a "headless" install to a different DOS directory? - It's easy with the command line options. - -The Floppy-Only Edition uses a completely different installer than -the CD-ROM or USB installers. The Floppy-Only Edition does not use -any of those other media to install. - -The Floppy-Only Edition contains a limited set of FreeDOS programs -that are more useful on classic PC hardware. - -The FreeDOS Floppy-Only Edition is distributed as single zip archive that -contains several pre-made floppy diskette images: - - These zip archives contain image files for several common floppy - diskette media under separate directories: - - * 720k - 3.5" 720k diskette images - - * 144m - 3.5" 1.44mb diskette images - - * 120m - 5.25" 1.2mb diskette images - - Each of those sets contain a number of pre-made disk images: - - * x86BOOT.img - A floppy boot disk image with the x86 installer. - - * x86DSK??.img - Several floppy diskette images that contain the - core FreeDOS operating system files. The number of floppy images - and amount of files on each varies depending on the diskette - capacity. - -To conserve space, the FreeDOS Floppy-Only Edition does not contain -the source code for the FreeDOS packages. You can find the source code -via the FreeDOS website (https://www.freedos.org/download/) or from -the other release media, like the USB or CD-ROM installer. - diff --git a/.qemu/send-keys.ps1 b/.qemu/send-keys.ps1 deleted file mode 100644 index 3431978..0000000 --- a/.qemu/send-keys.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -param( - [Parameter(Mandatory)] - [string] $Text, - - [ValidateRange(0, 1000)] - [int] $DelayMilliseconds = 25, - - [switch] $NoEnter -) - -$ErrorActionPreference = 'Stop' - -$map = @{ - ' ' = 'spc'; ':' = 'shift-semicolon'; ';' = 'semicolon' - '\' = 'backslash'; '|' = 'shift-backslash'; '/' = 'slash'; '?' = 'shift-slash' - '.' = 'dot'; '>' = 'shift-dot'; ',' = 'comma'; '<' = 'shift-comma' - '-' = 'minus'; '_' = 'shift-minus'; '=' = 'equal'; '+' = 'shift-equal' - '[' = 'bracket_left'; '{' = 'shift-bracket_left' - ']' = 'bracket_right'; '}' = 'shift-bracket_right' - "'" = 'apostrophe'; '"' = 'shift-apostrophe'; '`' = 'grave_accent'; '~' = 'shift-grave_accent' - '!' = 'shift-1'; '@' = 'shift-2'; '#' = 'shift-3'; '$' = 'shift-4'; '%' = 'shift-5' - '^' = 'shift-6'; '&' = 'shift-7'; '*' = 'shift-8'; '(' = 'shift-9'; ')' = 'shift-0' -} - -function ConvertTo-QemuKey([char] $Character) { - $text = [string] $Character - if ($map.ContainsKey($text)) { return $map[$text] } - if ([char]::IsLetter($Character)) { - $letter = [char]::ToLowerInvariant($Character) - if ([char]::IsUpper($Character)) { return "shift-$letter" } - return [string] $letter - } - if ([char]::IsDigit($Character)) { return $text } - throw "Unsupported QEMU key character: '$Character'" -} - -$client = [System.Net.Sockets.TcpClient]::new([System.Net.Sockets.AddressFamily]::InterNetwork) -try { - $client.Connect([System.Net.IPAddress]::Parse('127.0.0.1'), 4444) - $stream = $client.GetStream() - $writer = [System.IO.StreamWriter]::new($stream, [System.Text.Encoding]::ASCII, 1024, $true) - try { - $writer.NewLine = "`r`n" - $writer.AutoFlush = $true - Start-Sleep -Milliseconds 100 - - foreach ($char in $Text.ToCharArray()) { - $writer.WriteLine("sendkey $(ConvertTo-QemuKey $char)") - Start-Sleep -Milliseconds $DelayMilliseconds - } - if (-not $NoEnter) { $writer.WriteLine('sendkey ret') } - Start-Sleep -Milliseconds $DelayMilliseconds - } - finally { - $writer.Dispose() - } -} -finally { - $client.Dispose() -} diff --git a/.qemu/setup.ps1 b/.qemu/setup.ps1 deleted file mode 100644 index 58b944e..0000000 --- a/.qemu/setup.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemuImg = Get-Command qemu-img.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$archive = Join-Path $PSScriptRoot 'FD14-LiveCD.zip' -$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' -$url = 'https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.4/FD14-LiveCD.zip' - -if (-not (Test-Path -LiteralPath $disk)) { - & $qemuImg.Source create -f qcow2 $disk 2G - if ($LASTEXITCODE -ne 0) { throw "qemu-img failed with exit code $LASTEXITCODE." } -} - -if (-not (Test-Path -LiteralPath $iso)) { - if (-not (Test-Path -LiteralPath $archive)) { - Invoke-WebRequest -Uri $url -OutFile $archive - } - Expand-Archive -LiteralPath $archive -DestinationPath $PSScriptRoot -Force -} - -if (-not (Test-Path -LiteralPath $iso)) { - throw 'FreeDOS ISO extraction failed.' -} - -Get-Item -LiteralPath $disk,$iso | Select-Object Name,Length,LastWriteTime diff --git a/AGENTS.md b/AGENTS.md index c9590c2..ad78332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,53 +10,39 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | | `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | -| `tools/tcpagent/README.md` | DOS 내부 TCP 에이전트 프로토콜과 빌드 | -VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범**이다. 문서에 복제하면 -반드시 드리프트하므로 아래로 확인한다. +개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. ```powershell -uv run ferro-vm --help -uv run ferro-vm --help +uv run ferro-dos --help +uv run ferro-dos --help uv run ferro-test --help ``` ## 검증 규칙 -- 개발 중 빠른 회귀 검사는 `uv run ferro-test run --through `로 - DOSBox-X에서 수행한다. - 이 경로도 컴파일러 A를 DOS 내부 Open Watcom으로 매번 새로 빌드한다. -- **마일스톤의 최종 공식 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox-X 결과는 - 개발용 smoke test이며 완료 게이트를 대체하지 않는다. -- 컴파일러 A와 생성 C 모두 VM 안의 Open Watcom으로 컴파일한다. - 컴파일러 A와 bits16은 `WCL`, bits32 생성 C는 `WCL386`. -- **호스트에서 컴파일하지 않는다.** WSL이나 호스트 C 컴파일러 결과는 정식 검증으로 - 인정하지 않는다. 호스트는 편집, diff, Git, 파일 전송에만 쓴다. -- authoritative workspace는 VM의 `C:\FEC`다. -- 마일스톤 완료 기준은 `C:\FEC\BUILD.OK`, `C:\FEC\TEST.OK`, `TEST-DOS.BAT` exit 0 - 세 가지를 모두 확인하는 것이다. 테스트가 증명하지 않는 기능은 완료로 처리하지 - 않는다. -- VGA 데모처럼 멀티모달 수동 검증이 필요한 항목은 완료 게이트에서 제외한다. +- 컴파일러와 생성 C는 DOSBox-X 내부의 고정된 Open Watcom으로 컴파일한다. + 컴파일러와 bits16은 `WCL`, bits32 생성 C는 `WCL386`을 쓴다. +- 호스트 C 컴파일러 결과는 검증으로 인정하지 않는다. 호스트는 편집, Git, 다운로드, + 격리 작업공간 준비에만 쓴다. +- 실행마다 만들어지는 authoritative workspace는 `C:\FEC`다. 호스트에서는 + `.dosboxx/runs//FEC`에 대응한다. +- 완료하려는 기능을 직접 검사하는 pytest case가 통과해야 한다. 테스트가 증명하지 + 않는 기능은 완료로 처리하지 않는다. +- VGA 데모처럼 수동 검증이 필요한 항목은 자동 완료 게이트에서 제외한다. ## 빌드 함정 -VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 실패가 난다. - -- **컴파일러 A는 16비트 large model로 빌드한다.** small model은 메모리 부족으로 - 실패한다. -- **링크는 `*.obj` 와일드카드로 한다.** DOS 명령줄 길이 제한 때문에 오브젝트를 - 나열할 수 없다. 그래서 `fec/build-dos.bat`는 먼저 `C:\FEC\*.obj`를 지워 - stale 32비트 test object가 섞이지 않게 한다. -- **M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다.** 생성 C의 보수적 미사용 helper 때문에 +- 컴파일러는 16비트 large model로 빌드한다. small model은 메모리 부족으로 실패한다. +- 링크는 `*.obj` 와일드카드로 한다. DOS 명령줄 길이 제한 때문에 오브젝트를 나열할 + 수 없다. `fec/build-dos.bat`는 먼저 stale object를 지운다. +- M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다. 생성 C의 보수적 미사용 helper 때문에 W202만 끄고 나머지 경고는 오류로 유지한다. -- **fixture는 짧은 이름으로 전송한다.** DOS 8.3 파일명 때문에 긴 이름은 - `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 줄여야 한다. -- **`D:`에서 빌드하지 않는다.** QEMU의 vvfat 뷰는 교환용이며, 과거 `D:`에서 빌드하다 - rename 처리 오류로 QEMU가 종료된 적이 있다. 호스트에서 편집한 파일은 `put`으로 - `C:`에 올린 뒤 컴파일한다. -- 긴 DOS 배치가 멈추면 QEMU를 재시작하기 전에 Ctrl+C 주입을 먼저 시도한다. - `Terminate batch file ... (Yes/No/All)?`가 뜨면 `y`, `ret`을 보내고 `ping` 복구를 - 확인한다. +- fixture는 DOS 8.3 이름으로 실행한다. 긴 이름은 registry에서 명시적으로 줄인다. +- `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. 빌드 산출물은 반드시 + 임시 `C:\FEC`에 쓴다. +- 실패 분석이 필요하면 `ferro-test --keep-failed` 또는 `ferro-dos --keep`으로 + 임시 작업공간을 보존한다. ## 작업 흐름 @@ -64,12 +50,9 @@ VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 구현이 명세와 다르면 둘 중 하나가 틀린 것이므로 그 자리에서 결론을 낸다. - 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. - primary 브랜치는 `master`다. -- `.qemu/*.png`, `.qemu/*.ppm`은 진단용이며 커밋하지 않는다. +- `.dosboxx/`의 다운로드, 실행 작업공간, 로그는 커밋하지 않는다. ## 현재 상태 -- M1~M5 완료 및 QEMU/Open Watcom 검증됨. -- 다음은 M6(R1~R8 대여 검사)다. 착수할 때 소유권 로직을 `check.c`/`emit_c.c`에서 - `own.c/h`로 분리한다 (`SPEC.md` §11.3). -- v0.1.6에서 R8(파생 반환), R6(마지막 사용까지 대여), R10(전역 대여 금지)이 - 바뀌었다. 셋 다 own.c의 상태 기계를 건드리므로 분리 이후에 함께 구현한다. +현재 구현 상태와 다음 마일스톤은 `SPEC.md`와 테스트 registry를 기준으로 판단한다. +과거 VM 이미지나 호스트에 남은 바이너리를 근거로 완료 처리하지 않는다. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index d6fcdd1..0000000 --- a/TODO.md +++ /dev/null @@ -1,27 +0,0 @@ -# TODO - -## Done - -- [x] Add a non-reboot abort path for a hung DOS command: inject `Ctrl+C` through - QEMU's monitor and wait for the agent to recover. -- [x] Apply a configurable timeout to `exec` and invoke the non-reboot abort path - on timeout. -- [x] Expose `abort` for an immediate user-requested command interruption. - -The three above were first built for the COM1 serial agent, lost in the rewrite -to the resident TCP agent, and rebuilt on the QEMU monitor in `ferro-vm exec`. -The TCP version supervises with guest disk liveness (`info blockstats`) rather -than a fixed stopwatch, so a slow compile is no longer mistaken for a hang. - -## Open - -- [ ] Consider `BREAK=ON` in `C:\FDCONFIG.SYS`. Ctrl+C only takes effect at a DOS - break check, and with the FreeDOS default of `BREAK=OFF` a compute-bound - child whose output is redirected to a file may never reach one, so `abort` - cannot always stop it. `BREAK=ON` checks on every DOS call and makes the - abort reliable, at a small cost to every DOS call. Needs a VM reboot; back - up `FDCONFIG.SYS` first. -- [ ] `TCPAGENT.EXE` connects from a fixed source port (`LOCAL_PORT 2058`). After - the host end closes, a reconnect reuses the same 4-tuple and can flap until - the old state ages out. Observed as a ~10s connect/disconnect cycle after - the daemon is killed mid-connection. diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat deleted file mode 100644 index e6e76a3..0000000 --- a/fec/vm-m1.bat +++ /dev/null @@ -1,201 +0,0 @@ -@echo off -rem D: is the read-only exchange volume. Stage everything before running DOS tools. -if not exist C:\FEC md C:\FEC -if not exist C:\FEC\SRC md C:\FEC\SRC -if not exist C:\FEC\STD md C:\FEC\STD -if not exist C:\FEC\TESTS md C:\FEC\TESTS -if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS -if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL -if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 -if not exist C:\FEC\TESTS\M3 md C:\FEC\TESTS\M3 -if not exist C:\FEC\TESTS\M4 md C:\FEC\TESTS\M4 -if not exist C:\FEC\TESTS\M5 md C:\FEC\TESTS\M5 -if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL -if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL - -copy D:\FEC\BUILD-~1.BAT C:\FEC\BUILD.BAT > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TEST-DOS.BAT C:\FEC\TEST-DOS.BAT > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\SRC\ARENA.C C:\FEC\SRC\ARENA.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\ARENA.H C:\FEC\SRC\ARENA.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DIAG.C C:\FEC\SRC\DIAG.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DIAG.H C:\FEC\SRC\DIAG.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\LEXER.C C:\FEC\SRC\LEXER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\LEXER.H C:\FEC\SRC\LEXER.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\AST.C C:\FEC\SRC\AST.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\AST.H C:\FEC\SRC\AST.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\PARSER.C C:\FEC\SRC\PARSER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\PARSER.H C:\FEC\SRC\PARSER.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DRIVER.C C:\FEC\SRC\DRIVER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\TYPES.C C:\FEC\SRC\TYPES.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\TYPES.H C:\FEC\SRC\TYPES.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\CHECK.C C:\FEC\SRC\CHECK.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\CHECK.H C:\FEC\SRC\CHECK.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\EMIT_C.C C:\FEC\SRC\EMIT_C.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\EMIT_C.H C:\FEC\SRC\EMIT_C.H > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\STD\CORE.FE C:\FEC\STD\CORE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\FMT.FE C:\FEC\STD\FMT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\IO.FE C:\FEC\STD\IO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\LIST.FE C:\FEC\STD\LIST.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\MAP.FE C:\FEC\STD\MAP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\MEM.FE C:\FEC\STD\MEM.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\STR.FE C:\FEC\STD\STR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\SYS.FE C:\FEC\STD\SYS.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\PASS\BASIC.FE C:\FEC\TESTS\PASS\BASIC.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\LITERALS.FE C:\FEC\TESTS\PASS\LITERALS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\KEYWOR~1.FE C:\FEC\TESTS\PASS\KEYWOR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\V012-F~1.FE C:\FEC\TESTS\PASS\V012-F.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\MISSIN~1.FE C:\FEC\TESTS\FAIL\MISSIN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\UNCLOS~1.FE C:\FEC\TESTS\FAIL\UNCLOS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\LOGICA~1.FE C:\FEC\TESTS\FAIL\LOGICA.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\HELLO.FE C:\FEC\TESTS\M2\HELLO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\SCOPES.FE C:\FEC\TESTS\M2\SCOPES.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-CO~1.FE C:\FEC\TESTS\M2\BAD-CO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-CAST.FE C:\FEC\TESTS\M2\BAD-CA.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-ASSI~1.FE C:\FEC\TESTS\M2\BAD-AS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-UN~1.FE C:\FEC\TESTS\M2\BAD-UN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-UN~2.FE C:\FEC\TESTS\M2\BAD-UI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-AR~1.FE C:\FEC\TESTS\M2\BAD-AR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-TY~1.FE C:\FEC\TESTS\M2\BAD-TY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-RE~1.FE C:\FEC\TESTS\M2\BAD-RE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-VOID.FE C:\FEC\TESTS\M2\BAD-VO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\CAST-W~1.FE C:\FEC\TESTS\M2\CAST-W.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\M3\STRUCT.FE C:\FEC\TESTS\M3\STRUCT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ENUM.FE C:\FEC\TESTS\M3\ENUM.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ARRAY.FE C:\FEC\TESTS\M3\ARRAY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\STR.FE C:\FEC\TESTS\M3\STR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\FOR.FE C:\FEC\TESTS\M3\FOR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\NESTED.FE C:\FEC\TESTS\M3\NESTED.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\CHAR.FE C:\FEC\TESTS\M3\CHAR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ARRAYCTX.FE C:\FEC\TESTS\M3\ARRAYCTX.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BOUNDS.FE C:\FEC\TESTS\M3\BOUNDS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADFLD.FE C:\FEC\TESTS\M3\BADFLD.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADMAT.FE C:\FEC\TESTS\M3\BADMAT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADARR.FE C:\FEC\TESTS\M3\BADARR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADCYCLE.FE C:\FEC\TESTS\M3\BADCYCLE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADSTR.FE C:\FEC\TESTS\M3\BADSTR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADCHAR.FE C:\FEC\TESTS\M3\BADCHAR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADFIELD.FE C:\FEC\TESTS\M3\BADFIELD.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADINDEX.FE C:\FEC\TESTS\M3\BADINDEX.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\M4\FORMAT.FE C:\FEC\TESTS\M4\FORMAT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-ARI~1.FE C:\FEC\TESTS\M4\BAD-ARI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-VERB.FE C:\FEC\TESTS\M4\BAD-VERB.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-RUN~1.FE C:\FEC\TESTS\M4\BAD-RUN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-TYP~1.FE C:\FEC\TESTS\M4\BAD-TYP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-TRY.FE C:\FEC\TESTS\M4\BAD-TRY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\TRY-FPR~1.FE C:\FEC\TESTS\M4\TRY-FPR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-WRI~1.FE C:\FEC\TESTS\M4\BAD-WRI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\PROP.FE C:\FEC\TESTS\M4\PROP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\PROPTEST.C C:\FEC\TESTS\M4\PROPTEST.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-MANY.FE C:\FEC\TESTS\M4\BAD-MANY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-OPEN.FE C:\FEC\TESTS\M4\BAD-OPEN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-CLS.FE C:\FEC\TESTS\M4\BAD-CLS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\DEFER.FE C:\FEC\TESTS\M5\DEFER.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\OWNED.FE C:\FEC\TESTS\M5\OWNED.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\BAD-MOVE.FE C:\FEC\TESTS\M5\BAD-MOVE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\BAD-DES~1.FE C:\FEC\TESTS\M5\BAD-DES.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\RUNTIME.FE C:\FEC\TESTS\M5\RUNTIME.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\RUNTIME.C C:\FEC\TESTS\M5\RUNTIME.C > nul -if errorlevel 1 goto stage_fail - -call C:\FEC\TEST-DOS.BAT -if exist C:\FEC\TEST.OK goto vm_success -echo FAIL>C:\FEC\VM.FAIL -verify other 2>nul -goto stage_done - -:vm_success -cd C:\FEC -goto stage_done - -:stage_fail -echo FAIL>C:\FEC\STAGE.FAIL -echo FAIL>C:\FEC\VM.FAIL -verify other 2>nul - -:stage_done diff --git a/pyproject.toml b/pyproject.toml index 9b0a084..635c397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,14 @@ [project] name = "ferrolang" version = "0.1.0" -description = "Ferro language compiler and QEMU development automation" +description = "Ferro language compiler and reproducible DOS development tools" requires-python = ">=3.12" dependencies = [ - "onnxruntime>=1.28.0", "pytest>=9.0.0", - "rapidocr>=3.9.2", ] [project.scripts] -ferro-vm = "ferrolang_vm.cli:main" +ferro-dos = "ferrolang_vm.dos_cli:main" ferro-test = "ferrolang_vm.test_cli:main" [build-system] diff --git a/src/ferrolang_vm/__init__.py b/src/ferrolang_vm/__init__.py index 039a606..a63f890 100644 --- a/src/ferrolang_vm/__init__.py +++ b/src/ferrolang_vm/__init__.py @@ -1 +1 @@ -"""Windows-only QEMU and FreeDOS TCP-agent automation.""" +"""Reproducible DOS development tools for the Ferro compiler.""" diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py deleted file mode 100644 index ff40ddb..0000000 --- a/src/ferrolang_vm/cli.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Command line client for the Windows-only ferro-vm daemon.""" -from __future__ import annotations - -import argparse -import json -import shutil -import subprocess -import sys -import time -from multiprocessing.connection import Client -from pathlib import Path - -from .daemon import PIPE, ROOT - - -def rpc(payload: dict[str, object], start_daemon: bool = False) -> object: - try: - conn = Client(PIPE, family="AF_PIPE") - except (FileNotFoundError, OSError): - if not start_daemon: - raise RuntimeError("ferro-vm daemon is not running; run `uv run ferro-vm start`") - flags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0) - subprocess.Popen([sys.executable, "-m", "ferrolang_vm.daemon"], cwd=ROOT, creationflags=flags, close_fds=True) - deadline = time.monotonic() + 5 - while True: - try: - conn = Client(PIPE, family="AF_PIPE") - break - except (FileNotFoundError, OSError): - if time.monotonic() >= deadline: - raise RuntimeError("ferro-vm daemon did not create its control pipe") - time.sleep(.1) - with conn: - conn.send(payload) - response = conn.recv() - if not response["ok"]: - raise RuntimeError(response["error"]) - return response["result"] - - -def wait_ready(timeout: int) -> bool: - """Wait quietly; do not turn an expected boot gap into error-log spam.""" - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - try: - status = rpc({"op": "status"}) - if status["agent_connected"] and str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): - return True - except RuntimeError: - pass - time.sleep(.5) - return False - - -def follow_logs() -> int: - log_path = ROOT / ".qemu" / "ferro-vm.log" - lnav = shutil.which("lnav.exe") or shutil.which("lnav") - if lnav: - return subprocess.run([lnav, str(log_path)]).returncode - print("lnav was not found; following the log with PowerShell.", file=sys.stderr) - return subprocess.run([ - "powershell", "-NoProfile", "-Command", - f"Get-Content -LiteralPath '{log_path}' -Wait", - ]).returncode - - -EPILOG = r"""examples: - uv run ferro-vm start boot the VM and start the daemon - uv run ferro-vm wait-ready block until TCPAGENT answers PING - uv run ferro-vm exec 'dir C:\FEC' run a DOS command, print exit code and output - uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' - uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK - uv run ferro-vm exec --idle-timeout 180 'C:\FEC\BUILD-DOS.BAT' - uv run ferro-vm abort Ctrl+C the command running right now - uv run ferro-vm logs follow the structured daemon log - -The authoritative workspace is C:\FEC inside the VM. Never build on D: (the vvfat -view is for exchange only). See AGENTS.md for verification rules and build traps. -""" - -SIMPLE_COMMANDS = { - "start": "Start the daemon and boot QEMU. Safe to run when already up.", - "stop": "Quit QEMU cleanly and stop the daemon.", - "status": "Print daemon, QEMU, and TCPAGENT connection state as JSON.", - "ping": "Send PING to TCPAGENT. Expects 'OK 504F4E47' (PONG).", - "screenshot": "Capture the VGA console to a PPM/PNG under .qemu/.", - "ocr": "Capture the console and print recognized text (RapidOCR).", - "logs": "Follow the append-only daemon log. Uses lnav when available.", - "abort": "Interrupt the DOS command currently running (Ctrl+C via QEMU).", -} - - -def main() -> int: - parser = argparse.ArgumentParser( - prog="ferro-vm", - description="Windows-only QEMU/FreeDOS automation for the Ferro compiler.", - epilog=EPILOG, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - commands = parser.add_subparsers(dest="op", required=True, metavar="COMMAND") - for name, blurb in SIMPLE_COMMANDS.items(): - commands.add_parser(name, help=blurb, description=blurb) - - wait_help = "Block until TCPAGENT is connected and answers PING." - wait = commands.add_parser("wait-ready", help=wait_help, description=wait_help) - wait.add_argument("--timeout", type=int, default=45, metavar="SECONDS", - help="give up after this many seconds (default: %(default)s)") - - reset_help = "Quit QEMU cleanly, reboot it, and wait for TCPAGENT." - reset = commands.add_parser("reset", help=reset_help, description=reset_help) - reset.add_argument("--timeout", type=int, default=45, metavar="SECONDS", - help="give up after this many seconds (default: %(default)s)") - - exec_help = "Run a DOS command inside the VM and print its exit code and output." - execute = commands.add_parser( - "exec", help=exec_help, - description=exec_help + " Quote the command so the host shell does not eat" - r" backslashes: exec 'wcl386 -q HELLO.C'." - " A slow command is not a failed one: the wait ends" - " when the guest stops touching its disk, not when a" - " stopwatch expires.") - execute.add_argument("command", metavar="DOS_COMMAND", - help=r"command line to hand to COMMAND.COM, e.g. 'dir C:\FEC'") - execute.add_argument("--idle-timeout", type=float, default=60, metavar="SECONDS", - help="interrupt once the guest has made no disk access for" - " this long (default: %(default)s)") - execute.add_argument("--hard-timeout", type=float, default=900, metavar="SECONDS", - help="interrupt after this much total time regardless of" - " activity; the backstop for a CPU-bound hang" - " (default: %(default)s)") - - put_help = "Copy a host file into the VM." - put = commands.add_parser("put", help=put_help, description=put_help) - put.add_argument("source", type=Path, metavar="HOST_PATH", - help="file on this machine") - put.add_argument("destination", metavar="DOS_PATH", - help=r"target inside the VM, e.g. 'C:\FEC\SRC\CHECK.C'." - " DOS uses 8.3 names, so long fixtures must be shortened" - " explicitly (BAD-ARI.FE, TRY-FPR.FE)") - - get_help = "Copy a file out of the VM onto the host." - get = commands.add_parser("get", help=get_help, description=get_help) - get.add_argument("source", metavar="DOS_PATH", - help=r"file inside the VM, e.g. 'C:\FEC\TEST.OK'") - get.add_argument("destination", type=Path, metavar="HOST_PATH", - help="target on this machine") - - args = parser.parse_args() - - try: - if args.op == "logs": - return follow_logs() - if args.op == "wait-ready": - if wait_ready(args.timeout): - print(json.dumps({"agent": "PONG"})) - return 0 - raise RuntimeError("TCPAGENT did not become ready") - if args.op == "ocr": - result = rpc({"op": "screenshot"}) - import logging - logging.disable(logging.INFO) - from rapidocr import RapidOCR - recognized = RapidOCR()(result["path"]) - print("\n".join(recognized.txts or ())) - return 0 - if args.op == "reset": - rpc({"op": "stop"}, start_daemon=True) - time.sleep(.5) - rpc({"op": "start"}, start_daemon=True) - time.sleep(2) - rpc({"op": "monitor", "command": "sendkey ret"}) - if wait_ready(args.timeout): - print(json.dumps({"reset": "complete", "agent": "PONG"})) - return 0 - raise RuntimeError("TCPAGENT did not become ready") - payload: dict[str, object] = {"op": args.op} - if args.op == "exec": - payload["command"] = args.command - payload["idle_timeout"] = args.idle_timeout - payload["hard_timeout"] = args.hard_timeout - if args.op == "put": - payload["source"] = str(args.source.resolve()) - payload["destination"] = args.destination - if args.op == "get": - payload["source"] = args.source - payload["destination"] = str(args.destination.resolve()) - print(json.dumps(rpc(payload, start_daemon=args.op == "start"), ensure_ascii=False, indent=2)) - return 0 - except RuntimeError as exc: - print(f"ferro-vm: {exc}", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py deleted file mode 100644 index 20b3e9d..0000000 --- a/src/ferrolang_vm/daemon.py +++ /dev/null @@ -1,467 +0,0 @@ -"""Long-lived Windows host for the FreeDOS TCP agent. - -The only automation TCP listener is 127.0.0.1:5558, used exclusively by -TCPAGENT.EXE. Local commands use a Windows named pipe. -""" -from __future__ import annotations - -import json -import logging -import os -import re -import select -import shutil -import socket -import subprocess -import sys -import threading -import time -from datetime import datetime, timezone -from multiprocessing.connection import Listener -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -QEMU = ROOT / ".qemu" -PIPE = r"\\.\pipe\ferrolang-vm" -AGENT_ADDRESS = ("127.0.0.1", 5558) -MONITOR_ADDRESS = ("127.0.0.1", 4444) -LOG_PATH = QEMU / "ferro-vm.log" - -# Short commands answer promptly, so a plain socket timeout is the right guard. -REQUEST_TIMEOUT = 30 -# EXEC is different: TCPAGENT is frozen inside system() for the whole command -# and cannot answer, so silence proves nothing. Wake up often, decide with -# guest liveness instead of a stopwatch, and never discard the connection just -# because a compile is slow. -EXEC_POLL_SECONDS = 2 -DEFAULT_IDLE_TIMEOUT = 60 -DEFAULT_HARD_TIMEOUT = 900 -# Budget for collecting the result after Ctrl+C, before giving up on the stream. -INTERRUPT_GRACE_SECONDS = 15 - - -class ExecInterrupted(RuntimeError): - """The supervisor decided to stop the running DOS command.""" - - -def configure_logging() -> None: - QEMU.mkdir(exist_ok=True) - handler = logging.FileHandler(LOG_PATH, encoding="utf-8") - handler.setFormatter(logging.Formatter("%(asctime)s.%(msecs)03dZ %(levelname)-7s %(message)s", "%Y-%m-%dT%H:%M:%S")) - logging.basicConfig(level=logging.INFO, handlers=[handler]) - logging.Formatter.converter = time.gmtime - - -def log_event(level: int, event: str, **fields: object) -> None: - suffix = " ".join(f"{key}={json.dumps(value, ensure_ascii=False)}" for key, value in fields.items()) - logging.log(level, "%s%s", event, f" {suffix}" if suffix else "") - - -class Host: - def __init__(self) -> None: - self.agent: socket.socket | None = None - self.agent_lock = threading.Lock() - self.agent_ready = threading.Event() - self.qemu: subprocess.Popen[bytes] | None = None - # QEMU accepts one monitor connection at a time, and the EXEC - # supervisor polls it while other control requests run concurrently. - self.monitor_lock = threading.Lock() - self.abort_requested = threading.Event() - self.exec_active = threading.Event() - - @staticmethod - def bind_agent_listener() -> socket.socket: - """Bind 5558 exclusively so a second daemon fails loudly. - - SO_REUSEADDR means something different on Windows than on Unix: it lets - another process bind an already-bound port and quietly take over new - connections, so a duplicate daemon would be silently half-working - instead of refusing to start. SO_EXCLUSIVEADDRUSE is the Windows way to - say "only me". - """ - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - exclusive = getattr(socket, "SO_EXCLUSIVEADDRUSE", None) - if exclusive is not None: - server.setsockopt(socket.SOL_SOCKET, exclusive, 1) - else: - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(AGENT_ADDRESS) - server.listen(1) - return server - - def accept_agents(self, server: socket.socket) -> None: - log_event(logging.INFO, "agent listener ready", address="127.0.0.1:5558") - while True: - sock, peer = server.accept() - sock.settimeout(REQUEST_TIMEOUT) - with self.agent_lock: - if self.agent is not None: - sock.close() - log_event(logging.WARNING, "agent rejected", peer=str(peer), reason="already connected") - continue - self.agent = sock - try: - banner = self._read_line(sock).decode("ascii", "replace") - if banner != "TCPAGENT READY": - raise ConnectionError(f"invalid TCPAGENT banner: {banner!r}") - self.agent_ready.set() - log_event(logging.INFO, "agent connected", peer=f"{peer[0]}:{peer[1]}") - # Request() owns protocol reads. Between requests, peek only - # for EOF so TCPAGENT can reconnect without being rejected. - while self.agent is sock: - if self.agent_lock.acquire(blocking=False): - try: - readable, _, _ = select.select([sock], [], [], .25) - if readable and not sock.recv(1, socket.MSG_PEEK): - break - finally: - self.agent_lock.release() - else: - time.sleep(.05) - except (ConnectionError, OSError) as exc: - # Reset can close a connection during its banner. This is a - # per-connection event, never a reason to kill the listener. - log_event(logging.WARNING, "agent handshake/connection failed", - peer=f"{peer[0]}:{peer[1]}", error=str(exc)) - finally: - with self.agent_lock: - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - sock.close() - log_event(logging.INFO, "agent disconnected", peer=f"{peer[0]}:{peer[1]}") - - @staticmethod - def _read_line(sock: socket.socket, supervise=None) -> bytes: - # Partial input is kept across timeouts, so supervise() may fire in the - # middle of a line without losing what has already arrived. - out = bytearray() - while not out.endswith(b"\n"): - try: - part = sock.recv(1) - except TimeoutError: - if supervise is None: - raise - supervise() - continue - if not part: - raise ConnectionError("TCP agent closed connection") - out.extend(part) - return bytes(out).rstrip(b"\r\n") - - @staticmethod - def _read_exactly(sock: socket.socket, count: int, supervise=None) -> bytes: - chunks: list[bytes] = [] - while count: - try: - chunk = sock.recv(min(65536, count)) - except TimeoutError: - if supervise is None: - raise - supervise() - continue - if not chunk: - raise ConnectionError("TCP agent closed connection") - chunks.append(chunk) - count -= len(chunk) - return b"".join(chunks) - - def guest_idle_seconds(self) -> float | None: - """Seconds since the guest last touched a disk, or None if unknown. - - QEMU keeps counting while TCPAGENT is frozen inside system(), so this - is the one progress signal available during a long DOS command. A - purely CPU-bound command looks idle here, which is what the hard - timeout is for. - """ - try: - text = self.monitor("info blockstats") - except OSError: - return None - idle: float | None = None - for line in text.splitlines(): - operations = re.search(r"rd_operations=(\d+)", line) - elapsed = re.search(r"idle_time_ns=(\d+)", line) - if not operations or not elapsed or int(operations.group(1)) == 0: - continue - seconds = int(elapsed.group(1)) / 1e9 - idle = seconds if idle is None else min(idle, seconds) - return idle - - def request(self, command: str, payload: bytes = b"") -> str: - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - started = time.monotonic() - try: - sock.sendall(command.encode("ascii") + b"\n" + payload) - response = self._read_line(sock).decode("ascii", "replace") - except OSError as exc: - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - raise RuntimeError(f"TCPAGENT request failed: {exc}") from exc - log_event(logging.INFO, "agent request", command=command.split(" ", 1)[0], response=response[:200], elapsed_ms=round((time.monotonic()-started)*1000)) - if response.startswith("ERR "): - raise RuntimeError(bytes.fromhex(response[4:]).decode("utf-8", "replace")) - return response - - def ping(self) -> dict[str, object]: - return {"response": self.request("PING")} - - def _read_exec_result(self, sock: socket.socket, supervise=None) -> tuple[int, int, bytes]: - header = self._read_line(sock, supervise).decode("ascii", "replace") - fields = header.split() - if len(fields) == 4 and fields[0] == "RESULT": - code, length, flags = int(fields[1]), int(fields[2]), int(fields[3]) - return code, flags, self._read_exactly(sock, length, supervise) - if fields and fields[0] == "OK": - # Compatibility with an installed pre-RESULT agent. - return int(fields[1]), 1, bytes.fromhex(fields[2]) if len(fields) > 2 else b"" - raise RuntimeError("malformed EXEC response: " + header) - - def exec(self, command: str, idle_timeout: float = DEFAULT_IDLE_TIMEOUT, - hard_timeout: float = DEFAULT_HARD_TIMEOUT) -> dict[str, object]: - log_event(logging.INFO, "exec start", command=command, - idle_timeout=idle_timeout, hard_timeout=hard_timeout) - started = time.monotonic() - self.abort_requested.clear() - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - self.exec_active.set() - reported = started - interrupt_at: float | None = None - interrupted = "" - - def supervise() -> None: - """Called every EXEC_POLL_SECONDS while the agent stays silent.""" - nonlocal reported, interrupt_at, interrupted - now = time.monotonic() - idle = self.guest_idle_seconds() - if now - reported >= 15: - reported = now - log_event(logging.INFO, "exec running", elapsed_s=round(now-started, 1), - guest_idle_s=None if idle is None else round(idle, 1)) - if interrupt_at is None: - if self.abort_requested.is_set(): - interrupted = "aborted by request" - elif now - started > hard_timeout: - interrupted = f"hard timeout after {hard_timeout:.0f}s" - elif idle is not None and idle > idle_timeout: - interrupted = f"guest idle {idle:.0f}s exceeds {idle_timeout:.0f}s" - if interrupted: - interrupt_at = now - log_event(logging.WARNING, "exec interrupting", reason=interrupted) - self.monitor("sendkey ctrl-c") - return - waited = now - interrupt_at - # DOS answers Ctrl+C with "Terminate batch file (Y/N/A)?" and - # waits there. The prompt only appears once COMMAND.COM reaches - # the next batch line, which can be many seconds into a slow - # command, so answer on every poll rather than once: a single - # early 'y' is swallowed by whatever is still running. Send only - # 'y' -- the prompt takes one keystroke, and a trailing Enter - # gets read as "keep going". - self.monitor("sendkey y") - # Even answered, Ctrl+C is a request. It lands only at a DOS - # break check, and a DOS/4GW child (wcc386, wmake) runs in - # protected mode where it may never reach one, so the command - # can still run to completion. Keep collecting its result rather - # than abandoning a stream that still owes us one -- give up - # only once the guest has gone quiet too. - if waited > INTERRUPT_GRACE_SECONDS and (idle is None or idle > 5): - raise ExecInterrupted(interrupted + "; command did not stop") - - try: - encoded = command.encode("ascii", "replace").hex().upper() - sock.settimeout(EXEC_POLL_SECONDS) - sock.sendall(f"EXEC {encoded}\n".encode("ascii")) - code, flags, raw = self._read_exec_result(sock, supervise) - except (OSError, ExecInterrupted) as exc: - # Only now is the stream beyond repair; drop it so the agent - # reconnects with a clean protocol state. - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - sock.close() - raise RuntimeError(f"TCPAGENT EXEC failed: {exc}") from exc - finally: - self.exec_active.clear() - self.abort_requested.clear() - try: - sock.settimeout(REQUEST_TIMEOUT) - except OSError: - pass - output = raw.decode("cp437", "replace") - for line in output.splitlines(): - log_event(logging.INFO, "dos output", line=line) - log_event(logging.INFO, "exec finish", exit=code, bytes=len(raw), flags=flags, - interrupted=interrupted or None, - elapsed_ms=round((time.monotonic()-started)*1000)) - result = {"exit": code, "output": output, "bytes": len(raw), "flags": flags} - if interrupted: - result["interrupted"] = interrupted - return result - - def abort(self) -> dict[str, object]: - if not self.exec_active.is_set(): - return {"aborted": False, "reason": "no command is running"} - self.abort_requested.set() - log_event(logging.INFO, "abort requested") - return {"aborted": True} - - def put(self, source: str, destination: str) -> dict[str, object]: - data = Path(source).read_bytes() - encoded = destination.encode("ascii").hex().upper() - self.request(f"PUT {encoded} {len(data)}", data) - stat = self.request(f"HASH {encoded}") - log_event(logging.INFO, "put", path=destination, bytes=len(data), stat=stat) - return {"path": destination, "bytes": len(data), "stat": stat} - - def get(self, source: str, destination: str) -> dict[str, object]: - encoded = source.encode("ascii").hex().upper() - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - sock.sendall(f"GET {encoded}\n".encode("ascii")) - header = self._read_line(sock).decode("ascii", "strict").split() - if len(header) != 2 or header[0] != "DATA": - raise RuntimeError("GET failed: " + " ".join(header)) - remaining = int(header[1]) - chunks: list[bytes] = [] - while remaining: - chunk = sock.recv(min(65536, remaining)) - if not chunk: - raise RuntimeError("TCPAGENT closed during GET") - chunks.append(chunk) - remaining -= len(chunk) - target = Path(destination) - target.parent.mkdir(parents=True, exist_ok=True) - data = b"".join(chunks) - target.write_bytes(data) - log_event(logging.INFO, "get", path=source, bytes=len(data), destination=str(target)) - return {"path": source, "destination": str(target), "bytes": len(data)} - - def monitor(self, command: str) -> str: - with self.monitor_lock, socket.create_connection(MONITOR_ADDRESS, timeout=3) as sock: - sock.settimeout(1) - time.sleep(.1) - try: - sock.recv(4096) - except TimeoutError: - pass - sock.sendall(command.encode("ascii") + b"\n") - time.sleep(.2) - chunks: list[bytes] = [] - while True: - try: - chunk = sock.recv(4096) - except TimeoutError: - break - if not chunk: - break - chunks.append(chunk) - return b"".join(chunks).decode("ascii", "replace").strip() - - def start(self) -> dict[str, object]: - if self.qemu is not None and self.qemu.poll() is None: - return {"started": False, "reason": "already running"} - try: - self.monitor("info status") - return {"started": False, "reason": "already running (external)"} - except OSError: - pass - executable = shutil.which("qemu-system-i386.exe") - disk = QEMU / "freedos.qcow2" - if not executable: - raise RuntimeError("qemu-system-i386.exe is not on PATH") - if not disk.exists(): - raise RuntimeError(f"missing {disk}; run .qemu/setup.ps1 and .qemu/install.ps1") - self.qemu = subprocess.Popen([executable, "-machine", "pc,accel=whpx,kernel-irqchip=off,usb=on", "-smp", "1", "-m", "64", "-drive", f"file={disk},format=qcow2,if=ide,index=0,media=disk", "-nic", "user,model=ne2k_isa", "-monitor", "tcp:127.0.0.1:4444,server=on,wait=off", "-boot", "order=c", "-display", "default"], cwd=QEMU) - log_event(logging.INFO, "qemu started", pid=self.qemu.pid) - return {"started": True, "pid": self.qemu.pid} - - def stop(self) -> dict[str, object]: - try: - self.monitor("quit") - log_event(logging.INFO, "qemu stop requested") - except OSError: - pass - return {"stopped": True} - - def screenshot(self) -> dict[str, object]: - ppm, png = QEMU / "qemu-screen.ppm", QEMU / "qemu-screen.png" - self.monitor("screendump " + str(ppm).replace("\\", "/")) - ffmpeg = shutil.which("ffmpeg.exe") - if not ppm.exists() or not ffmpeg: - raise RuntimeError("screenshot failed or ffmpeg.exe is not on PATH") - subprocess.run([ffmpeg, "-y", "-loglevel", "error", "-i", str(ppm), str(png)], check=True) - ppm.unlink(missing_ok=True) - log_event(logging.INFO, "screenshot", path=str(png)) - return {"path": str(png)} - - def dispatch(self, request: dict[str, object]) -> object: - op = request["op"] - if op == "status": - try: - self.monitor("info status") - running = True - except OSError: - running = False - return {"agent_connected": self.agent_ready.is_set(), "qemu_running": running, "log": str(LOG_PATH)} - if op == "start": return self.start() - if op == "stop": return self.stop() - if op == "ping": return self.ping() - if op == "abort": return self.abort() - if op == "exec": - return self.exec(str(request["command"]), - float(request.get("idle_timeout", DEFAULT_IDLE_TIMEOUT)), - float(request.get("hard_timeout", DEFAULT_HARD_TIMEOUT))) - if op == "put": return self.put(str(request["source"]), str(request["destination"])) - if op == "get": return self.get(str(request["source"]), str(request["destination"])) - if op == "screenshot": return self.screenshot() - if op == "monitor": return {"output": self.monitor(str(request["command"]))} - raise ValueError(f"unknown operation: {op}") - - -def serve_pipe(host: Host) -> None: - listener = Listener(PIPE, family="AF_PIPE") - log_event(logging.INFO, "control pipe ready", pipe=PIPE) - - def handle(conn) -> None: - try: - request = conn.recv() - try: - conn.send({"ok": True, "result": host.dispatch(request)}) - except Exception as exc: - log_event(logging.ERROR, "control failed", error=str(exc)) - conn.send({"ok": False, "error": str(exc)}) - finally: - conn.close() - - while True: - # One thread per request: `abort` has to be answerable while a long - # `exec` is still holding the agent. - threading.Thread(target=handle, args=(listener.accept(),), daemon=True).start() - - -def main() -> None: - if os.name != "nt": - raise SystemExit("ferro-vm currently supports Windows only") - configure_logging() - host = Host() - try: - server = host.bind_agent_listener() - except OSError as exc: - log_event(logging.ERROR, "agent listener bind failed", address="127.0.0.1:5558", error=str(exc)) - raise SystemExit(f"another ferro-vm daemon already owns 127.0.0.1:5558 ({exc})") - threading.Thread(target=host.accept_agents, args=(server,), daemon=True).start() - serve_pipe(host) - - -if __name__ == "__main__": - main() diff --git a/src/ferrolang_vm/dos_cli.py b/src/ferrolang_vm/dos_cli.py new file mode 100644 index 0000000..02a0567 --- /dev/null +++ b/src/ferrolang_vm/dos_cli.py @@ -0,0 +1,80 @@ +"""General-purpose disposable DOSBox-X/Open Watcom environment CLI.""" +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +from .dosboxx import DosboxError, run_suite, setup +from .paths import ROOT +from .suite import Case + + +def _run(command: str | None, *, keep: bool, show_dos: bool) -> int: + cases = [] if command is None else [Case("command", 0, command, True)] + run = run_suite(cases, keep=keep, show_dos=show_dos, trace_dos=show_dos) + try: + if run.result() != "PASS": + print(run.log(), file=sys.stderr) + return 1 + if cases and run.result(cases[0]) != "PASS": + print(run.log(cases[0]), file=sys.stderr) + return 1 + if cases: + output = run.log(cases[0]) + if output: + print(output, end="" if output.endswith("\n") else "\n") + if keep: + print(f"DOS workspace: {run.root}") + return 0 + finally: + run.cleanup() + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="ferro-dos", + description="Disposable directory-backed DOSBox-X/Open Watcom environment.", + ) + commands = parser.add_subparsers(dest="action", required=True) + prepare = commands.add_parser("setup", help="install the pinned DOSBox-X and Open Watcom tools") + prepare.add_argument("--accept-watcom-license", action="store_true") + for name, help_text in ( + ("build", "build the current FEC source inside DOS"), + ("exec", "build FEC and execute one DOS command"), + ("batch", "build FEC and call a repository DOS batch"), + ("shell", "build FEC and open an interactive DOS shell"), + ): + command = commands.add_parser(name, help=help_text) + command.add_argument("--keep", action="store_true", help="preserve the temporary DOS workspace") + command.add_argument("--show-dos", action="store_true", help="show and pause the DOS window") + if name == "exec": + command.add_argument("dos_command") + elif name == "batch": + command.add_argument("path", type=Path) + args = parser.parse_args() + try: + if args.action == "setup": + dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) + print(f"DOSBox-X: {dosbox}") + print(f"Open Watcom: {watcom}") + return 0 + if args.action == "build": + return _run(None, keep=args.keep, show_dos=args.show_dos) + if args.action == "exec": + return _run(args.dos_command, keep=args.keep, show_dos=args.show_dos) + if args.action == "shell": + return _run("COMMAND.COM", keep=args.keep, show_dos=True) + path = (ROOT / args.path).resolve() + if (path != ROOT and ROOT not in path.parents) or not path.is_file(): + raise DosboxError("batch path must be an existing file inside the repository") + relative = path.relative_to(ROOT).as_posix().replace("/", "\\").upper() + return _run(f"CALL R:\\{relative}", keep=args.keep, show_dos=args.show_dos) + except (DosboxError, subprocess.SubprocessError) as exc: + print(f"ferro-dos: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 581f14c..58b5ca8 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -12,7 +12,7 @@ import zipfile from dataclasses import dataclass from pathlib import Path -from .daemon import ROOT +from .paths import ROOT from .suite import Case @@ -96,28 +96,25 @@ def setup(*, accept_watcom_license: bool = False) -> tuple[Path, Path]: raise DosboxError("the DOSBox-X development backend currently supports Windows only") lock = _lock() dosbox, watcom, watcom_required = _required_paths(lock) - marker = CACHE / "SETUP.OK" - lock_hash = _sha256(LOCK_PATH) - if (marker.is_file() and marker.read_text(encoding="ascii").strip() == lock_hash - and dosbox.is_file() and all(path.is_file() for path in watcom_required)): - return dosbox, watcom - if not accept_watcom_license: + tools_ready = dosbox.is_file() and all(path.is_file() for path in watcom_required) + if not tools_ready and not accept_watcom_license: raise DosboxError( "Open Watcom is distributed under the Sybase Open Watcom Public License. " "Review tools/toolchains/dosboxx.lock.json and rerun " - "`uv run ferro-test setup --accept-watcom-license`." + "`uv run ferro-dos setup --accept-watcom-license`." ) - dosbox_spec = lock["dosboxx"] - watcom_spec = lock["open_watcom"] - assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) - _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") - _safe_extract(_download("Open Watcom", watcom_spec), watcom) + if not tools_ready: + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") + _safe_extract(_download("Open Watcom", watcom_spec), watcom) dosbox, watcom, watcom_required = _required_paths(lock) missing = [str(path.relative_to(CACHE)) for path in [dosbox, *watcom_required] if not path.is_file()] if missing: raise DosboxError("toolchain archive is missing: " + ", ".join(missing)) - marker.write_text(lock_hash + "\n", encoding="ascii") + (CACHE / "SETUP.OK").write_text(_sha256(LOCK_PATH) + "\n", encoding="ascii") return dosbox, watcom @@ -125,7 +122,7 @@ def resolve_tools() -> tuple[Path, Path]: lock = _lock() dosbox, watcom, required = _required_paths(lock) if not dosbox.is_file() or not all(path.is_file() for path in required): - raise DosboxError("toolchain is not installed; run `uv run ferro-test setup`") + raise DosboxError("toolchain is not installed; run `uv run ferro-dos setup`") return dosbox, watcom @@ -165,7 +162,8 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: ]) lines.extend([ "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", - "echo DONE>RUN.OK", *(["pause"] if show_dos else []), "exit", "", + "echo DONE>RUN.OK", + *(["pause"] if show_dos else []), "exit", "", ]) return "\r\n".join(lines) @@ -236,7 +234,8 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, command.append("-silent") command.extend([ "-fastlaunch", "-conf", str(config), - "-c", f'mount C "{run_root}"', "-c", f'mount W "{watcom}" -ro', + "-c", f'mount C "{run_root}"', "-c", f'mount R "{ROOT}" -ro', + "-c", f'mount W "{watcom}" -ro', "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", ]) completed = subprocess.run(command, check=False, timeout=300) diff --git a/src/ferrolang_vm/paths.py b/src/ferrolang_vm/paths.py new file mode 100644 index 0000000..87ffbbd --- /dev/null +++ b/src/ferrolang_vm/paths.py @@ -0,0 +1,5 @@ +"""Repository and local cache paths shared by Ferro developer tools.""" +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] diff --git a/tools/README.md b/tools/README.md index 2e3429e..97b141e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,75 +2,54 @@ ## Host support -Automation currently supports **Windows 10/11 only**. The fast development loop -requires only `uv`; it downloads pinned DOSBox-X and Open Watcom DOS releases. -The final milestone gate additionally requires QEMU with WHPX support and -`ffmpeg.exe` on `PATH`. Other hosts are not supported yet. - -## Getting started +The automated DOS development environment currently supports Windows 10/11. +The host only needs `uv`. The setup command downloads the pinned DOSBox-X and +Open Watcom DOS archives, verifies their SHA-256 hashes, and installs them in the +ignored `.dosboxx/` cache. + +```powershell +uv run ferro-dos setup --accept-watcom-license +``` + +Review the Open Watcom license referenced by +`tools/toolchains/dosboxx.lock.json` before accepting it. Neither downloaded +archives nor installed tools are committed. + +## General DOS environment + +`ferro-dos` provides the development entry points: + +```powershell +uv run ferro-dos build +uv run ferro-dos exec "FEC.EXE --check TESTS\M6\OKLAST.FE" +uv run ferro-dos batch fec\test-dos.bat +uv run ferro-dos shell +uv run ferro-dos --help +``` + +Every invocation creates an isolated host directory under `.dosboxx/runs/` and +mounts it as writable `C:`. The repository is mounted read-only as `R:` and the +pinned Open Watcom installation as read-only `W:`. Current compiler sources, +the standard library, and fixtures are copied to `C:\FEC`; all compilation and +execution happen there inside DOSBox-X. Successful runs are removed by default. +Use `--keep` to retain a workspace and `--show-dos` to display the DOS window. + +This directory-backed layout deliberately has no QEMU, disk-image, TCP-agent, +or OCR dependency. A future disk-image backend can be added without changing +the command interface. + +## Pytest regression suite + +`ferro-test` uses the same isolated DOSBox-X/Open Watcom environment, builds +`FEC.EXE` once, and executes all selected cases sequentially in that one DOS +instance. Pytest still reports each registered case separately. ```powershell -uv run ferro-test setup --accept-watcom-license uv run ferro-test run --through m6 -v -``` - -`setup` reads `tools/toolchains/dosboxx.lock.json`, downloads the exact official -archives, verifies their SHA-256 hashes, and extracts them under ignored -`.dosboxx/`. Review the Open Watcom license referenced by the lock file before -accepting it. Archives and installed tools are deliberately not committed. - -Each `run` creates a disposable DOS drive, copies the current compiler, standard -library, and fixtures, then builds `FEC.EXE` once inside DOS with Open Watcom. -All selected milestone commands execute sequentially in that same DOSBox-X -instance, while pytest reports every emit, Watcom build, runtime, and rejection -check separately. Thus stale QEMU binaries cannot make the test pass. - -`--through m6` runs cumulatively from M1; `--only m6` selects one milestone. -Use `--keep-failed` to preserve a failed drive under `.dosboxx/runs/`, -`--dos-log` to print the captured DOS console, `--trace-dos` to disable command -output redirection, and `--show-dos` to keep the GUI open until a key is pressed. - -This is the quick development smoke test. Run the QEMU/FreeDOS workflow below -for the authoritative milestone completion gate. - -```powershell -uv run ferro-vm start -uv run ferro-vm status -``` - -The command list lives in the CLI itself, not in this file: - -```powershell -uv run ferro-vm --help -uv run ferro-vm --help +uv run ferro-test run --only m6 --dos-log uv run ferro-test --help ``` -Working rules, verification gates, and DOS build traps are in `AGENTS.md`. - -## How it fits together - -`TCPAGENT.EXE` runs inside FreeDOS and dials out to `127.0.0.1:5558`; its wire -protocol is documented in `tcpagent/README.md`. The `ferro-vm` daemon owns that -connection and the QEMU monitor. Local commands reach the daemon over the -Windows named pipe `\\.\pipe\ferrolang-vm` — there is no controller or observer -TCP port. - -The daemon writes an append-only structured log (`uv run ferro-vm logs`, which -uses `lnav` when installed and otherwise falls back to PowerShell `Get-Content --Wait`). It records command metadata, DOS output, exit status, transfers, and -agent lifecycle events as UTF-8 lines, and deliberately never logs raw binary -payloads or protocol hex. - -`reset` quits QEMU cleanly, restarts it, waits for FreeDOS to boot, submits the -default boot-menu Enter, and requires a TCPAGENT `PING`/`PONG` before returning. -QEMU `system_reset` is intentionally unsupported: repeated soft resets leave the -FreeDOS NE2000 packet driver stuck during initialization. - -## Standalone OCR - -`tools/qemu_ocr.py` remains available for OCRing an existing image: - -```powershell -uv run python tools/qemu_ocr.py --image .qemu/qemu-screen.png -``` +`--keep-failed` preserves a failed workspace, `--dos-log` prints captured DOS +output, `--trace-dos` disables per-command redirection, and `--show-dos` displays +the GUI. Working rules and DOS/Open Watcom build traps are in `AGENTS.md`. diff --git a/tools/qemu_ocr.py b/tools/qemu_ocr.py deleted file mode 100644 index 949380f..0000000 --- a/tools/qemu_ocr.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -"""Capture the QEMU VGA console and print its text with RapidOCR.""" - -from __future__ import annotations - -import argparse -import json -import logging -import subprocess -import sys -from pathlib import Path - -from rapidocr import RapidOCR - -ROOT = Path(__file__).resolve().parent.parent -DEFAULT_IMAGE = ROOT / ".qemu" / "qemu-screen.png" - - -def capture() -> Path: - subprocess.run( - ["ferro-vm", "screenshot"], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - ) - if not DEFAULT_IMAGE.is_file(): - raise RuntimeError(f"QEMU screenshot was not created: {DEFAULT_IMAGE}") - return DEFAULT_IMAGE - - -def recognize(image: Path, min_score: float) -> list[dict[str, object]]: - logging.disable(logging.INFO) - result = RapidOCR()(image) - rows: list[dict[str, object]] = [] - if result.txts is None or result.boxes is None or result.scores is None: - return rows - for box, text, score in zip(result.boxes, result.txts, result.scores): - if float(score) < min_score: - continue - rows.append( - { - "x": int(min(point[0] for point in box)), - "y": int(min(point[1] for point in box)), - "text": text, - "score": round(float(score), 5), - } - ) - rows.sort(key=lambda row: (int(row["y"]), int(row["x"]))) - return rows - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--image", type=Path, help="OCR an existing image instead of capturing QEMU") - parser.add_argument("--min-score", type=float, default=0.5) - parser.add_argument("--json", action="store_true", help="emit locations and scores as JSON") - parser.add_argument("-o", "--output", type=Path, help="also save output as UTF-8 text") - args = parser.parse_args() - - image = args.image.resolve() if args.image else capture() - rows = recognize(image, args.min_score) - if args.json: - rendered = json.dumps({"image": str(image), "lines": rows}, ensure_ascii=False, indent=2) - else: - rendered = "\n".join(str(row["text"]) for row in rows) - if args.output: - args.output.write_text(rendered + ("\n" if rendered else ""), encoding="utf-8") - if rendered: - print(rendered) - return 0 if rows else 1 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except (OSError, RuntimeError, subprocess.CalledProcessError) as exc: - print(f"qemu-ocr: {exc}", file=sys.stderr) - raise SystemExit(2) diff --git a/tools/tcpagent/BUILD.BAT b/tools/tcpagent/BUILD.BAT deleted file mode 100644 index fa13ec9..0000000 --- a/tools/tcpagent/BUILD.BAT +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -set WATCOM=C:\DEVEL\WATCOMC -set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN -set INCLUDE=C:\DEVEL\WATCOMC\H -set EDPATH= -wmake diff --git a/tools/tcpagent/INSTALL.BAT b/tools/tcpagent/INSTALL.BAT deleted file mode 100644 index 5532775..0000000 --- a/tools/tcpagent/INSTALL.BAT +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -if not exist TCPAGENT.EXE goto fail -copy TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE > nul -if errorlevel 1 goto fail -find "TCPAGENT.EXE" C:\FDAUTO.BAT > nul -if not errorlevel 1 goto done -echo SET MTCPCFG=C:\FREEDOS\MTCP.CFG>>C:\FDAUTO.BAT -echo IF EXIST C:\FREEDOS\BIN\TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE>>C:\FDAUTO.BAT -:done -echo TCPAGENT installed. -goto end -:fail -echo TCPAGENT install failed. -verify other 2>nul -:end diff --git a/tools/tcpagent/Makefile b/tools/tcpagent/Makefile deleted file mode 100644 index ceb4f0f..0000000 --- a/tools/tcpagent/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -# Place this directory at MTCP\APPS\TCPAGENT in an mTCP checkout. -tcp_c_dir = ..\..\TCPLIB -memory_model = -mc -compile_options = @WPP.RSP -tcpobjs = packet.obj arp.obj eth.obj ip.obj tcp.obj tcpsockm.obj udp.obj utils.obj timer.obj ipasm.obj trace.obj -all : tcpagent.exe -.cpp.obj : - wpp $[* $(compile_options) -tcpagent.obj: tcpagent.cpp - wpp $(compile_options) tcpagent.cpp -packet.obj: - wpp $(tcp_c_dir)\packet.cpp $(compile_options) -arp.obj: - wpp $(tcp_c_dir)\arp.cpp $(compile_options) -eth.obj: - wpp $(tcp_c_dir)\eth.cpp $(compile_options) -ip.obj: - wpp $(tcp_c_dir)\ip.cpp $(compile_options) -tcp.obj: - wpp $(tcp_c_dir)\tcp.cpp $(compile_options) -tcpsockm.obj: - wpp $(tcp_c_dir)\tcpsockm.cpp $(compile_options) -udp.obj: - wpp $(tcp_c_dir)\udp.cpp $(compile_options) -utils.obj: - wpp $(tcp_c_dir)\utils.cpp $(compile_options) -timer.obj: - wpp $(tcp_c_dir)\timer.cpp $(compile_options) -trace.obj: - wpp $(tcp_c_dir)\trace.cpp $(compile_options) -ipasm.obj: - wasm -0 -mc $(tcp_c_dir)\ipasm.asm -tcpagent.exe: tcpagent.obj $(tcpobjs) - wlink system dos option map option eliminate option stack=16384 name $@ file *.obj -clean : .symbolic - del *.obj - del *.map - del tcpagent.exe diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md deleted file mode 100644 index 249b8c8..0000000 --- a/tools/tcpagent/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# FreeDOS resident TCP agent - -`TCPAGENT.EXE` is a foreground resident automation process. It uses the mTCP -packet-driver stack and maintains an outbound connection to the QEMU host at -`10.0.2.2:5558`. - -The Windows-only Python `ferro-vm` daemon owns that listener. It logs metadata -and decoded command output to `.qemu/ferro-vm.log`; it does not expose an -observer/controller TCP port or emit binary payloads to the log. Local host -control uses a Windows named pipe. - -## Build in FreeDOS - -1. Obtain the GPLv3 mTCP source tree (tested with the jhpyle/mTCP 2022 fork). -2. Copy this directory to `MTCP\APPS\TCPAGENT` inside that tree. -3. Set `WATCOM` for Open Watcom and run `BUILD.BAT`. -4. Run `INSTALL.BAT`; it installs the executable and adds startup lines after - the existing packet-driver setup in `C:\FDAUTO.BAT`. - -The build uses mTCP's compact memory model and Open Watcom C++16. The agent is -therefore distributed under GPLv3 when linked with mTCP. - -## Protocol - -`PING`, `READ`, `WRITE`, and `LIST` use text commands. `EXEC` captures both -stdout and stderr at the DOS handle level and returns an untruncated raw body: - -- `EXEC \n` -> `RESULT \r\n` - -Fast transfer commands are: - -- `PUT \n` -> `OK\r\n` -- `GET \n` -> `DATA \r\n` -- `HASH \n` -> `STAT \r\n` - -The host invokes them through: - -```powershell -uv run ferro-vm put host-file 'C:\DOS\FILE' -uv run ferro-vm get 'C:\DOS\FILE' host-file -``` - -## Agent-side logging - -The foreground agent prints one timestamped line per event on the VGA console -and keeps the same text in `C:\TCPAGENT.LOG`, rotating files larger than 256 KiB -to `C:\TCPAGENT.OLD`. Payloads and command output are never written to that -metadata log. - -Every command is logged with a request line and a result line carrying byte -counts and elapsed time — `EXEC`, `PUT`, `GET`, `HASH`, `LIST`, `READ`, and -`WRITE`. `PING` is deliberately excluded because `wait-ready` polls it twice a -second. Connection events (`connecting`, `connected`, `connect failed; retry N`, -`link lost`) are logged too; those are invisible to the host by definition, -since they happen when the socket is down. - -Lines are colored by writing VGA attribute bytes after `cprintf` lays out the -line: gray timestamps, cyan requests, yellow `EXEC` command text, green success, -red failure. Open Watcom's DOS `conio.h` has no `textattr()`, and ANSI escapes -are not interpreted on this FreeDOS console, so neither of the usual routes -works. Elapsed times come from the BIOS tick counter at 18.2065 Hz (~55 ms -resolution). - -## Long commands - -mTCP is only driven when the agent calls it, and `system()` freezes the agent -for the entire child command. So during a long `EXEC` the DOS side is mute: it -cannot answer, cannot acknowledge, cannot report progress. Silence therefore -proves nothing about whether the command is healthy. - -The host must not read that silence as failure. `ferro-vm exec` waits on -QEMU's own view of the guest instead: `info blockstats` keeps counting while -the agent is frozen, and `idle_time_ns` distinguishes a slow command from a -stuck one. See `--idle-timeout` and `--hard-timeout` in `ferro-vm exec --help`. - -When the host does decide to stop a command it injects Ctrl+C through the QEMU -monitor, then answers COMMAND.COM's `Terminate batch file (Y/N/A)?` prompt. -That is a request, not a guarantee: Ctrl+C only lands at a DOS break check, and -with `BREAK=OFF` (the FreeDOS default in `C:\FDCONFIG.SYS`) a compute-bound -child whose output we redirected to a file may never reach one. The host keeps -collecting the result either way rather than abandoning a stream that still -owes it a `RESULT`. - -Adding `BREAK=ON` to `C:\FDCONFIG.SYS` would make DOS check on every system -call and so make Ctrl+C reliable, at a small cost to every DOS call. - -## Rebuilding inside the VM - -`REBUILD.BAT` compiles and installs the agent in a single `exec`. `BUILD.BAT` -only runs `wmake` in the current directory, which is not where a host-driven -`ferro-vm exec` starts. - -```powershell -uv run ferro-vm put tools/tcpagent/tcpagent.cpp 'C:\MTSRC\MTCP\APPS\TCPAGENT\TCPAGENT.CPP' -uv run ferro-vm put tools/tcpagent/REBUILD.BAT 'C:\REBUILD.BAT' -uv run ferro-vm exec 'C:\REBUILD.BAT' -uv run ferro-vm reset -``` - -The reset is required: the running agent holds the old image in memory, and -`C:\FDAUTO.BAT` starts it at boot. diff --git a/tools/tcpagent/REBUILD.BAT b/tools/tcpagent/REBUILD.BAT deleted file mode 100644 index 989d172..0000000 --- a/tools/tcpagent/REBUILD.BAT +++ /dev/null @@ -1,22 +0,0 @@ -@echo off -rem Rebuild TCPAGENT.EXE from C:\MTSRC and install it, in one EXEC. -rem BUILD.BAT only runs wmake in the current directory, which is not where a -rem host-driven `ferro-vm exec` starts. Copy this to C:\ and run it by path. -rem The running agent keeps the old image in memory, so reset the VM afterwards: -rem uv run ferro-vm reset -C: -cd C:\MTSRC\MTCP\APPS\TCPAGENT -set WATCOM=C:\DEVEL\WATCOMC -set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN -set INCLUDE=C:\DEVEL\WATCOMC\H -set EDPATH= -if exist TCPAGENT.OBJ del TCPAGENT.OBJ -if exist TCPAGENT.EXE del TCPAGENT.EXE -wmake -if not exist TCPAGENT.EXE goto fail -copy /Y TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE -echo BUILD-OK -goto end -:fail -echo BUILD-FAILED -:end diff --git a/tools/tcpagent/WPP.RSP b/tools/tcpagent/WPP.RSP deleted file mode 100644 index 08000f4..0000000 --- a/tools/tcpagent/WPP.RSP +++ /dev/null @@ -1,11 +0,0 @@ --0 --mc --DCFG_H="tcpagent.cfg" --oh --os --s --zp2 --zpw --we --i=..\..\TCPINC --i=..\..\INCLUDE diff --git a/tools/tcpagent/tcpagent.cfg b/tools/tcpagent/tcpagent.cfg deleted file mode 100644 index b962291..0000000 --- a/tools/tcpagent/tcpagent.cfg +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H -#define MTCP_PROGRAM_NAME "tcpagent" -#include "Global.Cfg" -#define COMPILE_ARP -#define IP_FRAGMENTS_ON -#define COMPILE_UDP -#define COMPILE_TCP -#define COMPILE_ICMP -#undef TCP_MAX_SOCKETS -#define TCP_MAX_SOCKETS (1) -#endif diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp deleted file mode 100644 index 574316d..0000000 --- a/tools/tcpagent/tcpagent.cpp +++ /dev/null @@ -1,303 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "types.h" -#include "trace.h" -#include "utils.h" -#include "packet.h" -#include "arp.h" -#include "udp.h" -#include "tcp.h" -#include "tcpsockm.h" - -#define LINE_SIZE 9000 -#define CHUNK_SIZE 4096 -#define SERVER_PORT 5558 -#define LOCAL_PORT 2058 -#define RECV_SIZE 12288 - -static char linebuf[LINE_SIZE]; -static unsigned char data[CHUNK_SIZE+1]; -static TcpSocket *socketp; -static volatile uint8_t stop_requested; -static FILE *put_file; -static unsigned long put_remaining; -static unsigned long put_started; -static char put_path[260]; - -static unsigned long ticks(void) { - long value=0; - _bios_timeofday(_TIME_GETCLOCK,&value); - return (unsigned long)value; -} -/* The BIOS tick is 18.2065 Hz, so one tick is 5.49254 hundredths of a second. - 549/100 keeps the error under 0.05% and cannot overflow 32 bits for a delta - up to a full day (1573040 ticks * 549 fits). Plain delta/18 ran 1.1% fast. */ -static void elapsed_text(unsigned long started,char *out) { - unsigned long now=ticks(),delta=now>=started?now-started:now+(1573040UL-started); - unsigned long hundredths=delta*549UL/100UL; - sprintf(out,"%lu.%02lus",hundredths/100UL,hundredths%100UL); -} - -/* Open Watcom's DOS conio has no textattr()/textcolor() -- it offers only - cprintf/cputs/getch and friends -- and ANSI escapes are not interpreted on - the installed FreeDOS console. So let cprintf lay the line out (it scrolls - correctly) and then repaint the attribute bytes of the cells it just wrote. - The cursor sits at column 0 of the row after the line, which is what lets us - find those cells without tracking scrolling ourselves. */ -#define TIMESTAMP_WIDTH 9 -static void colorize(unsigned total,int attr) { - unsigned char __far *vram; unsigned cols,used,row,col,start,k; - if(*(unsigned char __far *)MK_FP(0x0040,0x0049)==7) return; /* MDA: no color */ - cols=*(unsigned __far *)MK_FP(0x0040,0x004A); - if(cols<40||cols>132) cols=80; - used=(total+cols-1)/cols; if(!used) used=1; - row=*(unsigned char __far *)MK_FP(0x0040,0x0051); - if(row262144L){remove("C:\\TCPAGENT.OLD");rename("C:\\TCPAGENT.LOG","C:\\TCPAGENT.OLD");} - f=fopen("C:\\TCPAGENT.LOG","a"); - if(f){fputs("--- TCPAGENT start ---\n",f);fclose(f);} -} -/* PUT completes either here in command_put (zero length) or in the receive loop - once the body arrives, so keep the one line both paths emit in one place. */ -static void put_finished(void) { - char elapsed[24]; elapsed_text(put_started,elapsed); - log_line(0x0A,"< PUT %s OK %s",put_path,elapsed); -} - -void __interrupt __far ctrl_break(void) { stop_requested=1; } -void __interrupt __far ctrl_c(void) { stop_requested=1; } - -static void drive(void) { - PACKET_PROCESS_MULT(5); - Arp::driveArp(); - Tcp::drivePackets(); -} - -static int send_all(const void *buffer, unsigned length) { - const uint8_t *p=(const uint8_t *)buffer; - unsigned sent=0; - int rc; - while(sentsend((uint8_t *)(p+sent),length-sent); - if(rc>0) sent+=(unsigned)rc; - else if(rc<0 || socketp->isRemoteClosed()) return -1; - } - return sent==length ? 0 : -1; -} - -static void write_text(const char *s) { send_all(s,strlen(s)); } -static int hexval(int c) { - if(c>='0'&&c<='9') return c-'0'; - if(c>='A'&&c<='F') return c-'A'+10; - if(c>='a'&&c<='f') return c-'a'+10; - return -1; -} -static int decode_hex(const char *src,unsigned char *dst,int cap) { - int n=0,hi,lo; - while(*src&&src[1]) { - if(n>=cap) return -1; - hi=hexval((unsigned char)src[0]); lo=hexval((unsigned char)src[1]); - if(hi<0||lo<0) return -1; - dst[n++]=(unsigned char)((hi<<4)|lo); src+=2; - } - return *src ? -1 : n; -} -static void write_hex(const unsigned char *src,unsigned count) { - static const char digits[]="0123456789ABCDEF"; - char out[256]; unsigned i,n; - while(count) { - n=count>sizeof(out)/2 ? sizeof(out)/2 : count; - for(i=0;i>4]; out[i*2+1]=digits[src[i]&15]; } - send_all(out,n*2); src+=n; count-=n; - } -} -static void ok_data(const unsigned char *src,unsigned count) { - write_text("OK "); write_hex(src,count); write_text("\r\n"); -} -static void error_text(const char *s) { - write_text("ERR "); write_hex((const unsigned char *)s,strlen(s)); write_text("\r\n"); -} -static int decode_path(const char *hex,char *path,int cap) { - int n=decode_hex(hex,(unsigned char *)path,cap-1); - if(n<0) return 0; path[n]='\0'; return 1; -} -static void command_read(char *args) { - char path[260],*off=strchr(args,' '); FILE *f; long pos; size_t count; int eof; - if(!off){log_line(0x0C,"< READ ERR missing offset");error_text("READ requires path and offset");return;} *off++='\0'; - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< READ ERR bad path encoding");error_text("Invalid path encoding");return;} - pos=atol(off); log_line(0x0B,"> READ %s @%ld",path,pos); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< READ ERR cannot open");error_text("Cannot open file");return;} - if(fseek(f,pos,SEEK_SET)){fclose(f);log_line(0x0C,"< READ ERR cannot seek");error_text("Cannot seek file");return;} - count=fread(data,1,CHUNK_SIZE,f); eof=count WRITE %s %c %dB",path,mode[0]=='A'?'A':'T',count); - f=fopen(path,mode[0]=='A'?"ab":"wb"); if(!f){log_line(0x0C,"< WRITE ERR cannot open");error_text("Cannot write file");return;} - if(count&&fwrite(data,1,count,f)!=(size_t)count){fclose(f);log_line(0x0C,"< WRITE ERR short write");error_text("Short write");return;} - fclose(f); log_line(0x0A,"< WRITE OK"); ok_data((const unsigned char *)"",0); -} -static void command_put(char *args) { - char path[260],*length_text=strchr(args,' '); - if(!length_text){log_line(0x0C,"< PUT ERR missing length");error_text("PUT requires path and length");return;} - *length_text++='\0'; - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< PUT ERR bad path encoding");error_text("Invalid path encoding");return;} - put_remaining=strtoul(length_text,0,10); strcpy(put_path,path); put_started=ticks(); - log_line(0x0B,"> PUT %s %luB",put_path,put_remaining); - put_file=fopen(path,"wb"); - if(!put_file){put_remaining=0;log_line(0x0C,"< PUT %s ERR cannot open",put_path);error_text("Cannot write file");return;} - if(!put_remaining){fclose(put_file);put_file=0;put_finished();ok_data((const unsigned char *)"",0);} -} -static void command_get(char *args) { - char path[260],elapsed[24]; FILE *f; long length; size_t count; unsigned long started=ticks(); - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< GET ERR bad path encoding");error_text("Invalid path encoding");return;} - log_line(0x0B,"> GET %s",path); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< GET %s ERR cannot open",path);error_text("Cannot open file");return;} - fseek(f,0,SEEK_END); length=ftell(f); fseek(f,0,SEEK_SET); - sprintf(linebuf,"DATA %ld\r\n",length); write_text(linebuf); - while((count=fread(data,1,CHUNK_SIZE,f))>0) if(send_all(data,count)<0)break; - fclose(f); elapsed_text(started,elapsed); - log_line(0x0A,"< GET OK %ldB %s",length,elapsed); -} -static void command_hash(char *args) { - char path[260],elapsed[24]; FILE *f; size_t count; unsigned i; - unsigned long length=0,hash=2166136261UL,started=ticks(); - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< HASH ERR bad path encoding");error_text("Invalid path encoding");return;} - log_line(0x0B,"> HASH %s",path); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< HASH %s ERR cannot open",path);error_text("Cannot open file");return;} - while((count=fread(data,1,CHUNK_SIZE,f))>0){length+=(unsigned long)count;for(i=0;i LIST %s",path); - strcpy(pattern,path); if(pattern[0]&&pattern[strlen(pattern)-1]!='\\') strcat(pattern,"\\"); strcat(pattern,"*.*"); - rc=_dos_findfirst(pattern,_A_NORMAL|_A_RDONLY|_A_HIDDEN|_A_SYSTEM|_A_SUBDIR|_A_ARCH,&found); - while(rc==0) { char entry[100]; int len; - if(strcmp(found.name,".")&&strcmp(found.name,"..")) { sprintf(entry,"%s\t%lu\t%s\n",found.name,found.size,(found.attrib&_A_SUBDIR)?"DIR":"FILE"); len=strlen(entry); if(used+(unsigned)len>=sizeof(output)) {truncated=1;break;} memcpy(output+used,entry,len); used+=(unsigned)len; ++entries; } - rc=_dos_findnext(&found); - } - log_line(truncated?0x0E:0x0A,"< LIST %u entries%s",entries,truncated?" (truncated)":""); - ok_data((unsigned char *)output,used); -} -static void command_exec(char *args) { - char command[700],elapsed[24]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; - int n,code=-1,fd=-1,save1=-1,save2=-1; long length=0; size_t count; unsigned long started; - n=decode_hex(args,(unsigned char *)command,sizeof(command)-1); if(n<0){error_text("Invalid command encoding");return;} command[n]='\0'; - started=ticks(); log_line(0x0E,"> EXEC %.640s",command); - fflush(stdout); fflush(stderr); - save1=dup(1); save2=dup(2); - fd=open(tmp,O_CREAT|O_TRUNC|O_WRONLY|O_BINARY,S_IREAD|S_IWRITE); - if(save1<0||save2<0||fd<0||dup2(fd,1)<0||dup2(fd,2)<0) { - if(fd>=0)close(fd); - if(save1>=0){dup2(save1,1);close(save1);} - if(save2>=0){dup2(save2,2);close(save2);} - log_line(0x0C,"< EXEC ERR redirect failed"); error_text("Cannot capture command output"); return; - } - close(fd); code=system(command); fflush(stdout); fflush(stderr); - dup2(save1,1); dup2(save2,2); close(save1); close(save2); - f=fopen(tmp,"rb"); - if(f){fseek(f,0,SEEK_END);length=ftell(f);fseek(f,0,SEEK_SET);} - elapsed_text(started,elapsed); - log_line(code?0x0C:0x0A,"< EXEC exit=%d %ldB %s",code,length,elapsed); - sprintf(linebuf,"RESULT %d %ld 0\r\n",code,length); write_text(linebuf); - while(f&&(count=fread(data,1,CHUNK_SIZE,f))>0)if(send_all(data,count)<0)break; - if(f)fclose(f); remove(tmp); -} -static void process_line(char *line) { - char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd); - /* PING is deliberately not logged: wait-ready polls it twice a second. */ - if(!strcmp(cmd,"PING"))ok_data((const unsigned char *)"PONG",4); - else if(!strcmp(cmd,"READ"))command_read(args); - else if(!strcmp(cmd,"WRITE"))command_write(args); - else if(!strcmp(cmd,"PUT"))command_put(args); - else if(!strcmp(cmd,"GET"))command_get(args); - else if(!strcmp(cmd,"HASH"))command_hash(args); - else if(!strcmp(cmd,"LIST"))command_list(args); - else if(!strcmp(cmd,"EXEC"))command_exec(args); - else if(!strcmp(cmd,"QUIT")){log_line(0x07,"* QUIT received");ok_data((const unsigned char *)"BYE",3);stop_requested=1;} - else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); log_line(0x0C,"< ERR %s",message); error_text(message); } -} -static int connect_host(void) { - IpAddr_t host={10,0,2,2}; int8_t rc; - socketp=TcpSocketMgr::getSocket(); if(!socketp)return -1; - socketp->setRecvBuffer(RECV_SIZE); - rc=socketp->connect(LOCAL_PORT,host,SERVER_PORT,10000); - while(rc==0&&!socketp->isConnectComplete()&&!socketp->isRemoteClosed()&&!stop_requested)drive(); - if(socketp->isRemoteClosed()){TcpSocketMgr::freeSocket(socketp);socketp=0;return -1;} - return 0; -} -int main(void) { - int used,rc; uint16_t key; unsigned attempts=0; - init_log(); - log_line(0x0B,"* TCPAGENT starting; Alt-X returns to DOS"); - if(Utils::parseEnv()!=0){log_line(0x0C,"* MTCP configuration error");return 2;} - if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c)){log_line(0x0C,"* TCP stack initialization error");return 3;} - while(!stop_requested) { - if(!attempts)log_line(0x07,"* connecting 10.0.2.2:%u",SERVER_PORT); - if(connect_host()!=0){unsigned long spins=0;++attempts;if(attempts==1||attempts%10==0)log_line(0x0C,"* connect failed; retry %u",attempts);while(spins++<60000UL&&!stop_requested)drive();continue;} - attempts=0; log_line(0x0A,"* connected; host automation owns console"); - write_text("TCPAGENT READY\r\n"); used=0; - while(!stop_requested&&!socketp->isRemoteClosed()) { - drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE); - if(rc<0)break; - for(int i=0;i>8)==45)stop_requested=1;} - } - socketp->close(); TcpSocketMgr::freeSocket(socketp); socketp=0; - if(!stop_requested)log_line(0x0C,"* link lost; retrying"); - } - log_line(0x07,"* stopped; returning to DOS"); - Utils::endStack(); return 0; -} diff --git a/uv.lock b/uv.lock index c7b989e..500d600 100644 --- a/uv.lock +++ b/uv.lock @@ -2,152 +2,6 @@ version = 1 revision = 1 requires-python = ">=3.12" -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } - -[[package]] -name = "certifi" -version = "2026.7.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456 }, - { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530 }, - { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200 }, - { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222 }, - { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951 }, - { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801 }, - { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070 }, - { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110 }, - { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836 }, - { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712 }, - { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977 }, - { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207 }, - { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562 }, - { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507 }, - { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551 }, - { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700 }, - { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584 }, - { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359 }, - { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464 }, - { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676 }, - { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473 }, - { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156 }, - { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246 }, - { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660 }, - { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354 }, - { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638 }, - { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583 }, - { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038 }, - { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677 }, - { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491 }, - { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196 }, - { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660 }, - { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618 }, - { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362 }, - { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755 }, - { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295 }, - { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856 }, - { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318 }, - { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897 }, - { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848 }, - { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163 }, - { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823 }, - { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458 }, - { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717 }, - { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111 }, - { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128 }, - { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240 }, - { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282 }, - { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597 }, - { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376 }, - { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715 }, - { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848 }, - { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521 }, - { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054 }, - { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580 }, - { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325 }, - { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175 }, - { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123 }, - { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682 }, - { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826 }, - { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861 }, - { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758 }, - { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950 }, - { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329 }, - { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137 }, - { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820 }, - { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504 }, - { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087 }, - { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269 }, - { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766 }, - { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814 }, - { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074 }, - { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476 }, - { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115 }, - { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048 }, - { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997 }, - { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014 }, - { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174 }, - { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361 }, - { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143 }, - { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086 }, - { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231 }, - { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546 }, - { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033 }, - { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045 }, - { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866 }, - { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932 }, - { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320 }, - { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174 }, - { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126 }, - { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441 }, - { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742 }, - { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298 }, - { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500 }, - { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888 }, - { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243 }, - { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871 }, - { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580 }, - { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807 }, - { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083 }, - { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317 }, - { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173 }, - { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960 }, - { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186 }, - { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947 }, - { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909 }, - { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467 }, - { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057 }, - { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930 }, - { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822 }, - { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037 }, - { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097 }, - { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166 }, - { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821 }, - { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529 }, - { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348 }, - { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234 }, - { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917 }, - { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846 }, - { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216 }, - { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764 }, - { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318 }, - { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658 }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -157,51 +11,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "colorlog" -version = "6.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239 }, -] - [[package]] name = "ferrolang" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "onnxruntime" }, { name = "pytest" }, - { name = "rapidocr" }, ] [package.metadata] -requires-dist = [ - { name = "onnxruntime", specifier = ">=1.28.0" }, - { name = "pytest", specifier = ">=9.0.0" }, - { name = "rapidocr", specifier = ">=3.9.2" }, -] - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, -] - -[[package]] -name = "idna" -version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, -] +requires-dist = [{ name = "pytest", specifier = ">=9.0.0" }] [[package]] name = "iniconfig" @@ -212,143 +31,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] -[[package]] -name = "numpy" -version = "2.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693 }, - { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109 }, - { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202 }, - { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736 }, - { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696 }, - { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264 }, - { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396 }, - { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044 }, - { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817 }, - { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674 }, - { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131 }, - { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595 }, - { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845 }, - { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880 }, - { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264 }, - { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566 }, - { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995 }, - { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511 }, - { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609 }, - { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204 }, - { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532 }, - { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725 }, - { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180 }, - { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878 }, - { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922 }, - { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168 }, - { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501 }, - { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701 }, - { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065 }, - { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031 }, - { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028 }, - { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627 }, - { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414 }, - { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967 }, - { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874 }, - { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276 }, - { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154 }, - { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909 }, - { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685 }, - { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181 }, - { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085 }, - { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971 }, - { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306 }, - { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274 }, - { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846 }, - { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892 }, - { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309 }, - { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850 }, - { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664 }, - { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749 }, - { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495 }, - { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696 }, - { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324 }, - { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466 }, - { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947 }, - { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331 }, - { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336 }, - { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387 }, - { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096 }, - { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730 }, - { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686 }, - { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727 }, - { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775 }, - { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559 }, - { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103 }, -] - -[[package]] -name = "omegaconf" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502 }, -] - -[[package]] -name = "onnxruntime" -version = "1.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flatbuffers" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "protobuf" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362 }, - { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628 }, - { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257 }, - { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036 }, - { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462 }, - { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759 }, - { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339 }, - { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329 }, - { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033 }, - { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175 }, - { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307 }, - { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954 }, - { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748 }, - { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950 }, - { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924 }, - { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738 }, - { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117 }, - { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518 }, - { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976 }, -] - -[[package]] -name = "opencv-python" -version = "5.0.0.93" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443 }, - { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755 }, - { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064 }, - { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711 }, - { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576 }, - { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032 }, - { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734 }, - { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345 }, -] - [[package]] name = "packaging" version = "26.3" @@ -358,77 +40,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956 }, ] -[[package]] -name = "pillow" -version = "12.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969 }, - { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323 }, - { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838 }, - { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830 }, - { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383 }, - { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934 }, - { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684 }, - { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137 }, - { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267 }, - { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684 }, - { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487 }, - { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433 }, - { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889 }, - { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109 }, - { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736 }, - { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129 }, - { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562 }, - { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439 }, - { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691 }, - { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185 }, - { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736 }, - { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435 }, - { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262 }, - { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344 }, - { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131 }, - { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757 }, - { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962 }, - { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171 }, - { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116 }, - { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209 }, - { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707 }, - { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995 }, - { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503 }, - { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956 }, - { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855 }, - { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642 }, - { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281 }, - { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716 }, - { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125 }, - { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939 }, - { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506 }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063 }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549 }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331 }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370 }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147 }, - { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659 }, - { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439 }, - { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577 }, - { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394 }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375 }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048 }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006 }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509 }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167 }, - { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237 }, - { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047 }, - { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440 }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895 }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384 }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537 }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -438,51 +49,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] -[[package]] -name = "protobuf" -version = "7.35.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226 }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847 }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030 }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130 }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945 }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996 }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659 }, -] - -[[package]] -name = "pyclipper" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676 }, - { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458 }, - { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235 }, - { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388 }, - { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169 }, - { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619 }, - { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342 }, - { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839 }, - { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142 }, - { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789 }, - { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817 }, - { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007 }, - { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167 }, - { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966 }, - { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216 }, - { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198 }, - { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951 }, - { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782 }, - { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880 }, - { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308 }, - { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608 }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -507,166 +73,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, ] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, -] - -[[package]] -name = "rapidocr" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorlog" }, - { name = "numpy" }, - { name = "omegaconf" }, - { name = "opencv-python" }, - { name = "pillow" }, - { name = "pyclipper" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "shapely" }, - { name = "six" }, - { name = "tqdm" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/ed/0ee9b9281986974be9d2406ae0134c8d7c91d2fc613f16ffda9701eeda6f/rapidocr-3.9.2-py3-none-any.whl", hash = "sha256:04d6b8d151f823d930bd91910555f57bea897c0c44fa6794267b94cf9c1ef9a0", size = 27275208 }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, -] - -[[package]] -name = "shapely" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 }, - { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 }, - { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 }, - { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 }, - { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 }, - { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 }, - { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 }, - { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 }, - { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 }, - { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 }, - { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 }, - { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 }, - { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 }, - { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 }, - { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 }, - { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 }, - { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 }, - { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 }, - { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 }, - { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 }, - { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 }, - { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 }, - { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 }, - { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 }, - { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 }, - { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 }, - { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 }, - { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 }, - { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 }, - { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 }, - { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 }, - { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 }, - { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 }, - { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 }, - { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 }, - { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 }, - { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 }, - { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "tqdm" -version = "4.70.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184 }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, -] From 5529e3dc14322672bbc78e2c455677808d003162 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:23:41 +0900 Subject: [PATCH 6/6] test: prove --no-checks removes the bounds check The bounds-no-checks cases emitted and built but never ran, so they only proved that --no-checks produces compilable C -- not that it removes the check, which is the entire point of the flag. A run case could not simply be appended. BOUNDS.FE returns the out-of-bounds element directly, so with checks removed its exit code is whatever sits past the array on the stack and there is no correct status to assert. Asserting on the generated C instead does not work either: emit_c.c defines fe_trap_bounds unconditionally and --no-checks only suppresses the call sites. Add NOCHK.FE, which reads one element past a [2]i32 and returns x - x. That is 0 for whatever garbage the unchecked read produced, so the same source has a defined outcome both ways: compiled with checks it must trap, compiled with --no-checks it must run to completion and exit 0. Register both halves and drop the two BOUNDS-N cases they supersede. Verified in DOSBox-X: 155 passed, including m3-nochk-trap failing as expected and m3-nochk-off-run succeeding. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- fec/tests/m3/nochk.fe | 7 +++++++ src/ferrolang_vm/registry.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 fec/tests/m3/nochk.fe diff --git a/fec/tests/m3/nochk.fe b/fec/tests/m3/nochk.fe new file mode 100644 index 0000000..e8e62f3 --- /dev/null +++ b/fec/tests/m3/nochk.fe @@ -0,0 +1,7 @@ +unit m3_no_checks; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let x: i32 = a[2]; + return x - x; +} diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 5478bbc..74d129a 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -146,10 +146,18 @@ CASES: list[Case] = [ # These two must trap at runtime: the bounds check is the feature under test. *_triple(3, "bounds", M3, run_suffix="trap", run_ok=False), *_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False), - _case(3, "bounds-no-checks-emit", - _emit(_fe(M3, "bounds"), f"{M3}\\BOUNDS-N.C", flags=("--no-checks",))), - _case(3, "bounds-no-checks-build", - _wcl(f"{M3}\\BOUNDS-N.EXE", f"{M3}\\BOUNDS-N.C")), + # --no-checks is proved by a differential on one source. NOCHK.FE reads one + # element past a [2]i32 and returns x - x, which is 0 whatever garbage the + # unchecked read produced: compiled with checks it must trap, compiled with + # --no-checks it must run to completion. BOUNDS.FE cannot serve as the + # unchecked half because it returns the out-of-bounds value directly, so its + # exit code would be whatever happens to sit past the array on the stack. + *_triple(3, "nochk", M3, run_suffix="trap", run_ok=False), + _case(3, "nochk-off-emit", + _emit(_fe(M3, "nochk"), f"{M3}\\NOCHK-N.C", flags=("--no-checks",))), + _case(3, "nochk-off-build", + _wcl(f"{M3}\\NOCHK-N.EXE", f"{M3}\\NOCHK-N.C")), + _case(3, "nochk-off-run", f"{M3}\\NOCHK-N.EXE"), *_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"), suffix="reject"),