std 는 예약된 이름이고 프로그램이 아니라 컴파일러와 함께 있으므로 자기 루트를 갖는다 (--std=). 본문 없는 선언은 링커가 찾을 것 -- 런타임이나 C 라이브러리 -- 이므로 IR 에 extern 으로 나간다. 런타임에 write/alloc/free/exit 를 넣었다. 이것이 표준 라이브러리가 스스로 말할 수 없는 전부이고 나머지는 Ferro 로 쓴다. @trap @unreachable @size_of @align_of @line 을 내린다. 링크 이름에서 점과 괄호를 걸렀다. 유닛 경로에는 점이 있고 제네릭 인스턴스에는 괄호가 있는데 어셈블러가 받지 않는다.
106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
"""Compile a Ferro program to a Windows executable and run it.
|
|
|
|
fec --emit-asm -> wasm -> wlink (+ the runtime, + kernel32) -> .exe
|
|
|
|
The toolchain is the pinned Open Watcom under `.dosboxx/watcom`, hosted: the
|
|
assembler and linker there produce PE binaries as happily as they produce DOS
|
|
ones. Nothing about this step needs a virtual machine.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
WATCOM = ROOT / ".dosboxx" / "watcom"
|
|
RUNTIME = ROOT / "fec" / "rt" / "start.asm"
|
|
|
|
|
|
def _env() -> dict:
|
|
env = dict(os.environ)
|
|
env.update(WATCOM=str(WATCOM),
|
|
INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}",
|
|
PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}")
|
|
return env
|
|
|
|
|
|
def _run(cmd, cwd) -> subprocess.CompletedProcess:
|
|
return subprocess.run([str(c) for c in cmd], cwd=cwd, env=_env(),
|
|
capture_output=True, text=True, timeout=120)
|
|
|
|
|
|
def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False):
|
|
"""Returns (exe_path, log). exe_path is None when a step failed."""
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
stem = source.stem
|
|
asm = out_dir / f"{stem}.asm"
|
|
log = []
|
|
|
|
# `std` ships with the compiler, so it is looked for beside it rather
|
|
# than beside the program.
|
|
cmd = [fec, "--emit-asm", source, "-o", asm, f"--std={ROOT / 'fec'}"]
|
|
if no_checks:
|
|
cmd.append("--no-checks")
|
|
step = _run(cmd, out_dir)
|
|
log.append(("fec", step.returncode, step.stdout + step.stderr))
|
|
if step.returncode != 0 or not asm.is_file():
|
|
return None, log
|
|
|
|
wasm = WATCOM / "binnt" / "wasm.exe"
|
|
for src, obj in ((asm, out_dir / f"{stem}.obj"),
|
|
(RUNTIME, out_dir / "start.obj")):
|
|
step = _run([wasm, "-q", "-zq", src, f"-fo={obj}"], out_dir)
|
|
log.append(("wasm " + src.name, step.returncode,
|
|
step.stdout + step.stderr))
|
|
if step.returncode != 0:
|
|
return None, log
|
|
|
|
exe = out_dir / f"{stem}.exe"
|
|
step = _run([WATCOM / "binnt" / "wlink.exe",
|
|
"system", "nt",
|
|
"file", out_dir / f"{stem}.obj",
|
|
"file", out_dir / "start.obj",
|
|
"library", WATCOM / "lib386" / "nt" / "kernel32.lib",
|
|
"name", exe,
|
|
"option", "quiet"], out_dir)
|
|
log.append(("wlink", step.returncode, step.stdout + step.stderr))
|
|
if step.returncode != 0 or not exe.is_file():
|
|
return None, log
|
|
return exe, log
|
|
|
|
|
|
def run(exe: Path):
|
|
done = subprocess.run([str(exe)], capture_output=True, text=True,
|
|
timeout=30)
|
|
return done.returncode, done.stdout + done.stderr
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 2:
|
|
print("usage: build.py <program.fe> [--no-checks]")
|
|
return 2
|
|
source = Path(sys.argv[1]).resolve()
|
|
fec = ROOT / ".build" / "fec.exe"
|
|
if not fec.is_file():
|
|
print("build the front end first: uv run python tests/run.py")
|
|
return 2
|
|
exe, log = build(fec, source, ROOT / ".build" / "out",
|
|
"--no-checks" in sys.argv)
|
|
for name, code, text in log:
|
|
if code != 0 or text.strip():
|
|
print(f"--- {name} (exit {code})")
|
|
print(text.rstrip())
|
|
if not exe:
|
|
return 1
|
|
code, text = run(exe)
|
|
if text:
|
|
print(text, end="")
|
|
print(f"{exe.name} exited {code}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|