From 5b12b5205291bd9f467c98a9a13a3a605f6de8fb Mon Sep 17 00:00:00 2001 From: Dragohn Date: Mon, 3 Aug 2026 14:18:43 +0200 Subject: [PATCH 1/2] build: add Windows build support via zig cc and GNU objcopy Three scripts port \make\ to Windows: ensure-toolchain.ps1 provisions zig, nasm, qemu and MSYS2 binutils; build.ps1 compiles with zig cc (clang) and re-labels the ELF64 kernel as an ELF32 container with \objcopy -O elf32-i386\, matching the Makefile; run.ps1 boots it in QEMU headless or with graphics. README gains the On Windows section and a fork/NO_OS attribution. --- .gitignore | 3 + README.md | 37 +++++ scripts/build.ps1 | 299 +++++++++++++++++++++++++++++++++++ scripts/ensure-toolchain.ps1 | 137 ++++++++++++++++ scripts/run.ps1 | 141 +++++++++++++++++ 5 files changed, 617 insertions(+) create mode 100644 scripts/build.ps1 create mode 100644 scripts/ensure-toolchain.ps1 create mode 100644 scripts/run.ps1 diff --git a/.gitignore b/.gitignore index d01f9a8..d25934c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ build/ # Scratch idea notes ideas/ +# Windows toolchain paths (scripts/ensure-toolchain.ps1 output; machine-specific) +scripts/.toolchain.json + # macOS .DS_Store diff --git a/README.md b/README.md index 2b96b99..0978bda 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,35 @@ make toolchain # once: nasm, x86_64-elf-gcc, qemu make run ``` +### On Windows + +There is no x86_64-elf toolchain here, so the same steps run through three +scripts that build with Zig's bundled clang instead. The Windows port follows +the approach proven by [NO_OS](https://github.com/coff33ninja/NO_OS) — a +from-scratch x86-64 kernel of a similar nature — whose +[ELF-reframing ADR](https://github.com/coff33ninja/NO_OS/blob/main/docs/ADR/0003-elf-reframe-qemu.md) +is the one trick this build was missing: + +```powershell +.\scripts\ensure-toolchain.ps1 # once: zig, nasm, qemu, python, MSYS2 binutils +.\scripts\build.ps1 # 211 objects -> kernel.bin +.\scripts\run.ps1 # headless: .\scripts\run.ps1 -NoGraphics +``` + +Build flags and API-key handling mirror the Makefile exactly (same CFLAGS minus +the GCC-only ones, same `.env` two-name rule, same `opt/fableos/apikey` fw_cfg +channel). Two Windows-only wrinkles, both contained in the scripts: + +- `-fno-sanitize=undefined` is added, or zig's compiler_rt drags in the + `__ubsan_handle_*` helpers and the link fails. +- QEMU's multiboot loader accepts ELF32 only, so after linking the kernel is + re-labeled as an ELF32 *container* with `objcopy -O elf32-i386` — the exact + command the Makefile runs. On Windows that objcopy comes from + `mingw-w64-x86_64-binutils` (installed by `ensure-toolchain.ps1` through + MSYS2's pacman), because zig's bundled llvm-objcopy only emits `binary`. + `run.ps1` prints a warning that "multiboot knows VBE. we don't" — that one + is a benign stderr line from QEMU, not the kernel. + That boots with no key: the kernel still completes a real TLS handshake to `api.anthropic.com` and gets an honest `401` back, which is itself proof the HTTPS path works. It will tell you it has no key. @@ -203,3 +232,11 @@ build, and design notes per subsystem. `AGENTS.md` has the architectural constraints anyone — or anything — working on this code needs to know first. Built and run on macOS via QEMU. + +--- + +*This tree is a fork of [robiot/fable-os](https://github.com/robiot/fable-os), +kept on my profile as [coff33ninja/fable-os](https://github.com/coff33ninja/fable-os) +to carry the Windows build. My own from-scratch OS of a similar nature — an +x86-64 kernel whose only interface is a bytecode VM — is +[NO_OS](https://github.com/coff33ninja/NO_OS).* diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..a07451a --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,299 @@ +<# +.SYNOPSIS + Build the fable-os kernel on Windows with zig cc (clang), NASM and QEMU. + +.DESCRIPTION + Windows port of `make` for this tree. The macOS Makefile uses a brew + x86_64-elf-gcc toolchain; here the kernel is compiled with Zig's bundled + clang (zig cc) targeting x86_64-freestanding, assembled with NASM, linked + with lld, and converted to an ELF32 container for QEMU's multiboot loader + with GNU objcopy from MSYS2 (`-O elf32-i386`), exactly as the Makefile's + `$(OBJCOPY) -O elf32-i386` does. zig's bundled llvm-objcopy cannot emit ELF. + + The object list below MUST stay in lock-step with the Makefile's + KERNEL_SRCS (the comment marks it as such). The script refuses to run if a + listed source is missing, so a rename in one place fails loudly here too. + + Traps encoded here, each paid for in an actual Windows build: + * PowerShell 5.1 splatting unwraps a single-element or scalar array, so + `@flags @w` where $w = '-w' silently passes a bare '-' and zig cc fails + with "unrecognized file extension". Every compile passes ONE array and + splats it exactly once. + * zig cc enables UBSan by default in freestanding builds; the kernel must + not call __ubsan_handle_*, so -fno-sanitize=undefined is mandatory or + the link dies on unresolved symbols. + * Linking with -nostdlib drops zig's compiler_rt, leaving __udivti3 / + __divti3 (mbedTLS bignum, expr.c, gui_demo.c 128-bit division) + undefined. Link WITHOUT -nostdlib; a freestanding target pulls no libc. + * The clang driver overrides the linker script's ENTRY(start) with its + own `-e _start`, producing e_entry=0 and a QEMU that jumps to address + zero. -Wl,-e,start restores the real entry. + * -fno-tree-loop-distribute-patterns (a GCC flag in the Makefile) is + rejected by zig's clang; it is unnecessary here because -fno-builtin + already prevents the memset/memcpy loops it targets. + * lwIP and mbedTLS are vendored and get -w, exactly as the Makefile does + (lwip/%.o mbedtls/%.o: CFLAGS += -w). + +.PARAMETER Clean + Wipe the object cache and rebuild everything. + +.PARAMETER ExtCFlags + Extra compiler flags, e.g. -ExtCFlags '-DFABLEOS_DIAGNOSTIC'. Mirrors the + Makefile's EXTRA_CFLAGS. Changing it discards cached objects (stamped in + build\.build-flags, same guard as the Makefile's .build-flags). + +.PARAMETER SkipInstall + Do not offer to install missing tools via winget; fail instead. + +.EXAMPLE + .\scripts\build.ps1 +.EXAMPLE + .\scripts\build.ps1 -ExtCFlags '-DFABLEOS_FAULT_TEST=FAULT_INJECT_PF_WRITE' +#> +[CmdletBinding()] +param( + [switch]$Clean, + [string]$ExtCFlags = '', + [switch]$SkipInstall +) + +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +Set-Location $repo + +# --------------------------------------------------------------------------- +# Tools +# --------------------------------------------------------------------------- +$toolchainJson = Join-Path $PSScriptRoot '.toolchain.json' +function Get-Tools { + if (-not (Test-Path -LiteralPath $toolchainJson)) { + if ($SkipInstall) { throw '.toolchain.json missing; run scripts\ensure-toolchain.ps1' } + & (Join-Path $PSScriptRoot 'ensure-toolchain.ps1') -Install | Out-Null + } + $t = Get-Content -LiteralPath $toolchainJson -Raw | ConvertFrom-Json + foreach ($name in 'zig', 'nasm', 'objcopy') { + if (-not $t.$name -or -not (Test-Path -LiteralPath $t.$name)) { + if ($SkipInstall) { throw "toolchain entry '$name' is stale; run scripts\ensure-toolchain.ps1" } + Remove-Item -LiteralPath $toolchainJson -Force + & (Join-Path $PSScriptRoot 'ensure-toolchain.ps1') -Install | Out-Null + $t = Get-Content -LiteralPath $toolchainJson -Raw | ConvertFrom-Json + } + } + $t +} +$T = Get-Tools + +# --------------------------------------------------------------------------- +# Build configuration +# --------------------------------------------------------------------------- +$objDir = Join-Path $repo 'build\obj' +$stamp = Join-Path $repo 'build\.build-flags' +$kernelElf = Join-Path $repo 'kernel.elf' +$kernelBin = Join-Path $repo 'kernel.bin' +New-Item -ItemType Directory -Force -Path $objDir | Out-Null + +# EXTRA_CFLAGS guard: a build that silently disagrees with its own flags is +# worse than a failing one (the Makefile's whole .build-flags machinery says +# so). Store the flag string; any change wipes the object cache. +$want = "EXTRA_CFLAGS=$ExtCFlags" +$have = '' +if (Test-Path -LiteralPath $stamp) { $have = (Get-Content -LiteralPath $stamp -Raw).Trim() } +if ($Clean -or ($have -ne $want)) { + if ($have -ne $want -and $have) { Write-Host "EXTRA_CFLAGS changed: rebuilding everything" } + Remove-Item -Recurse -Force (Join-Path $repo 'build\obj'), $kernelElf, $kernelBin -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $objDir | Out-Null + Set-Content -LiteralPath $stamp -Value $want -NoNewline +} + +# --------------------------------------------------------------------------- +# Source tables -- mirror of the Makefile's KERNEL_SRCS, LWIP_SRCS, MBEDTLS_SRCS +# --------------------------------------------------------------------------- +$kernelSources = @( + 'kernel/main.c', 'kernel/drivers.c', + 'arch/x86_64/idt.c', 'arch/x86_64/fault.c', + 'core/kobject.c', 'core/tool.c', 'core/audio.c', 'core/fiber.c', + 'core/capability.c', 'core/agenda.c', + 'mm/heap.c', 'device/device.c', + 'fs/vfs/vfs.c', 'fs/native/ramfs.c', 'fs/fs.c', + 'fs/fat/fat_vol.c', 'fs/fat/fat_dir.c', 'fs/fat/fat.c', + 'drivers/block/block.c', 'drivers/block/ata.c', + 'lib/base.c', 'lib/libc_shim.c', 'lib/kfmt.c', 'lib/trace.c', + 'lib/fb.c', 'lib/font.c', 'lib/font_spleen8x16.c', + 'gui/wm.c', 'gui/widgets.c', 'gui/gui_demo.c', + 'apps/runtime.c', 'apps/expr.c', 'apps/cap.c', 'apps/app_selftest.c', + 'apps/app_format_selftest.c', 'apps/app_audio_selftest.c', + 'vm/dvm.c', + 'compiler/cc.c', 'compiler/cc_lex.c', 'compiler/cc_parse.c', + 'compiler/cc_x64.c', 'compiler/cc_sym.c', 'compiler/cc_store.c', + 'drivers/serial/serial.c', 'drivers/fwcfg/fwcfg.c', 'drivers/pci/pci.c', + 'drivers/acpi/acpi.c', 'drivers/acpi/power.c', + 'drivers/acpi/power_selftest.c', + 'drivers/input/input.c', 'drivers/input/kbd.c', + 'drivers/input/serial_input.c', 'drivers/input/script.c', + 'drivers/input/mouse.c', + 'drivers/net/e1000.c', 'drivers/rtc/rtc.c', + 'net/json.c', 'net/model.c', 'net/chat.c', 'net/sse.c', 'net/net.c', + 'net/fetch.c', 'net/faultchat.c', 'net/tls_ca.c' +) +# tools/ is wildcarded on purpose: a tool self-registers through a linker +# section (REGISTER_TOOL), so this must never be a fixed list. +$toolSources = @(Get-ChildItem -LiteralPath (Join-Path $repo 'tools') -Filter '*.c' | + ForEach-Object { "tools/$($_.Name)" } | Sort-Object) + +$lwipSources = @( + @(Get-ChildItem -LiteralPath (Join-Path $repo 'lwip\src\core') -Filter '*.c' | + ForEach-Object { $_.FullName.Substring($repo.Length + 1) }) + @(Get-ChildItem -LiteralPath (Join-Path $repo 'lwip\src\core\ipv4') -Filter '*.c' | + ForEach-Object { $_.FullName.Substring($repo.Length + 1) }) + 'lwip/src/netif/ethernet.c' + 'lwip/src/apps/altcp_tls/altcp_tls_mbedtls.c' + 'lwip/src/apps/altcp_tls/altcp_tls_mbedtls_mem.c' +) +$mbedtlsSources = @(Get-ChildItem -LiteralPath (Join-Path $repo 'mbedtls\library') -Filter '*.c' | + ForEach-Object { $_.FullName.Substring($repo.Length + 1) }) + +$allSources = @($kernelSources + $toolSources + $lwipSources + $mbedtlsSources) + +# Drift check: every listed source must exist, and every tools/*.c must be in +# the list. A source that vanished (renamed, deleted) would otherwise link +# silently into an image missing a driver or a tool. +$missing = @($allSources | Where-Object { -not (Test-Path -LiteralPath (Join-Path $repo $_)) }) +if ($missing.Count -gt 0) { + throw ("source table drift: these files do not exist:`n" + + ($missing | ForEach-Object { " $_" }) + + "`nSync scripts/build.ps1 with the Makefile's source lists.") +} + +# --------------------------------------------------------------------------- +# Compile +# --------------------------------------------------------------------------- +$includeDirs = @('-Iinclude', '-Iport', '-Ilwip/src/include', '-Imbedtls/include') +# The Makefile passes -DMBEDTLS_CONFIG_FILE='"mbedtls_config.h"'. GCC strips the +# single quotes when the macro lands in `#include MBEDTLS_CONFIG_FILE`; clang +# (zig) keeps them and errors with 'expected "FILENAME"'. The backslash-quote +# form makes clang expand it to a proper string literal instead. +$mbedtlsDefine = '-DMBEDTLS_CONFIG_FILE=\"mbedtls_config.h\"' +$commonFlags = @( + '-target', 'x86_64-freestanding', + '-ffreestanding', '-m64', '-mno-red-zone', '-mno-mmx', '-mno-sse', '-mno-sse2', + '-fno-stack-protector', '-fno-pic', '-fno-builtin', + '-std=gnu11', '-fno-sanitize=undefined', + $mbedtlsDefine, '-MMD', '-MP', '-c' +) +if ($ExtCFlags) { $commonFlags += $ExtCFlags } + +# Run a native command, treating a non-zero exit as a fatal build error. +# Windows PowerShell 5.1 gotchas encoded here: +# * With $ErrorActionPreference='Stop', ANY native stderr redirection +# (`2>&1` or `2>file`) routes the process's stderr through PowerShell's +# error stream, and the first compiler warning -- which legitimately goes +# to stderr -- becomes a TERMINATING error. So the native call runs with +# EAP suspended; cmdlet errors elsewhere still stop the script. +# * A single-element/scalar array splat is unwrapped, which turned one '-w' +# into a bare '-' (the caller passes full arrays built once, and splats +# exactly once). +function Invoke-Native { + param([string]$File, [string[]]$ArgList, [string]$What) + $errTmp = Join-Path $env:TEMP ([IO.Path]::GetRandomFileName() + '.err') + $oldEAP = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & $File @ArgList 1>$null 2>$errTmp + $code = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $oldEAP + } + $err = Get-Content -LiteralPath $errTmp -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $errTmp -Force -ErrorAction SilentlyContinue + if ($code -ne 0) { + Write-Host "FAIL: $What" + $err | Select-Object -First 8 + exit 1 + } + if ($err) { $err } +} + +function Invoke-Compile { + param([string]$Source, [string]$Obj, [string[]]$ExtraFlags) + $argv = @('cc') + $argv += $commonFlags + $argv += $ExtraFlags + $argv += $includeDirs + $argv += '-o', $Obj, $Source + # ONE splat. PowerShell 5.1 unwraps a scalar/single-element array on splat, + # which silently turned a single '-w' into a bare '-' (see header comment). + Invoke-Native -File $T.zig -ArgList $argv -What $Source +} + +# Rebuild an object when its source or any header its .d file names is newer. +# This is the Windows counterpart of the Makefile's `-include $(OBJS:.o=.d)`. +function Test-Stale { + param([string]$Source, [string]$Obj) + if (-not (Test-Path -LiteralPath $Obj)) { return $true } + $newest = (Get-Item -LiteralPath $Source).LastWriteTimeUtc + $depFile = [IO.Path]::ChangeExtension($Obj, '.d') + if (Test-Path -LiteralPath $depFile) { + $raw = (Get-Content -LiteralPath $depFile -Raw) -replace '\\\r?\n', ' ' + $parts = $raw -split ':' , 2 + foreach ($dep in ($parts[1] -split '\s+' | Where-Object { $_ -and -not $_.EndsWith(':') })) { + if (Test-Path -LiteralPath $dep) { + $t = (Get-Item -LiteralPath $dep).LastWriteTimeUtc + if ($t -gt $newest) { $newest = $t } + } + } + } + return $newest -gt (Get-Item -LiteralPath $Obj).LastWriteTimeUtc +} + +$objOf = { param($s) Join-Path $objDir (($s -replace '[\\/]', '_') + '.o') } + +# Assembler objects (NASM, elf64), the same three as the Makefile. +$asmPairs = @( + @{ src = 'boot/boot.asm'; obj = 'boot_boot.asm.o' }, + @{ src = 'arch/x86_64/isr.asm'; obj = 'arch_x86_64_isr.asm.o' }, + @{ src = 'arch/x86_64/switch.asm'; obj = 'arch_x86_64_switch.asm.o' } +) +foreach ($a in $asmPairs) { + $dst = Join-Path $objDir $a.obj + if (-not (Test-Path -LiteralPath $dst) -or + (Get-Item -LiteralPath (Join-Path $repo $a.src)).LastWriteTimeUtc -gt (Get-Item -LiteralPath $dst).LastWriteTimeUtc) { + Write-Host "nasm $($a.src)" + Invoke-Native -File $T.nasm -ArgList @('-f', 'elf64', '-o', $dst, (Join-Path $repo $a.src)) -What $a.src + } +} + +$n = 0 +foreach ($src in $allSources) { + $n++ + $obj = & $objOf $src + $vendored = $src -like 'lwip\*' -or $src -like 'lwip/*' -or + $src -like 'mbedtls\*' -or $src -like 'mbedtls/*' + if (-not (Test-Stale -Source (Join-Path $repo $src) -Obj $obj)) { continue } + Write-Host ("[{0}/{1}] {2}" -f $n, $allSources.Count, $src) + $extra = if ($vendored) { @('-w') } else { @('-Wall', '-Wextra') } + Invoke-Compile -Source (Join-Path $repo $src) -Obj $obj -ExtraFlags $extra +} +Write-Host "objects: $((Get-ChildItem -LiteralPath $objDir -Filter '*.o').Count) up to date" + +# --------------------------------------------------------------------------- +# Link -- no -nostdlib (drops compiler_rt), -Wl,-e,start (restores the entry +# the clang driver would otherwise zero out). +# --------------------------------------------------------------------------- +$linkArgs = @('cc', '-target', 'x86_64-freestanding', '-Wl,-e,start', '-T', 'linker.ld', '-o', $kernelElf) +$linkArgs += (Get-ChildItem -LiteralPath $objDir -Filter '*.o' | ForEach-Object { $_.FullName }) +Write-Host "link kernel.elf" +Invoke-Native -File $T.zig -ArgList $linkArgs -What 'kernel link' + +# --------------------------------------------------------------------------- +# ELF32 container (QEMU's multiboot loader rejects ELFCLASS64). GNU objcopy +# re-labels the ELF64 image as ELF32; the 64-bit code, entry point and program +# headers are untouched. This is the Makefile's own `objcopy -O elf32-i386` +# step -- zig's llvm-objcopy cannot do it (only -O binary), hence the MSYS2 +# binutils provisioned by ensure-toolchain.ps1. +# --------------------------------------------------------------------------- +Write-Host "convert kernel.bin (ELF32 container)" +Invoke-Native -File $T.objcopy -ArgList @('-O', 'elf32-i386', $kernelElf, $kernelBin) -What 'elf32 container conversion' + +$sz = (Get-Item -LiteralPath $kernelBin).Length +Write-Host ("kernel.bin: {0:N0} bytes - boot it with .\scripts\run.ps1" -f $sz) diff --git a/scripts/ensure-toolchain.ps1 b/scripts/ensure-toolchain.ps1 new file mode 100644 index 0000000..4143e68 --- /dev/null +++ b/scripts/ensure-toolchain.ps1 @@ -0,0 +1,137 @@ +<# +.SYNOPSIS + Ensure the Windows toolchain fable-os needs is installed and record where. + +.DESCRIPTION + The macOS Makefile builds with x86_64-elf-gcc from a brew tap. On Windows + the same kernel is built with Zig's bundled clang (zig cc), NASM and QEMU. + This script finds each tool where it is known to live, offers to winget + install anything that is missing (winget installs per-user, no admin + needed), and writes scripts/.toolchain.json with the resolved paths that + build.ps1 and run.ps1 read. + + Known homes: + zig on PATH via winget's shim, else winget install zig.zig + nasm %LOCALAPPDATA%\bin\NASM\nasm.exe (winget's NASM.NASM) + qemu C:\Program Files\qemu\qemu-system-x86_64.exe (winget's QEMU) + objcopy C:\msys64\mingw64\bin\objcopy.exe (MSYS2 + mingw-w64-x86_64-binutils) + + scripts/.toolchain.json holds the resolved absolute paths and is gitignored + -- they are machine-specific and regenerated on every run. +#> +[CmdletBinding()] +param( + [switch]$Install +) + +$ErrorActionPreference = 'Stop' + +function Resolve-Tool { + param( + [string]$Name, + [scriptblock]$Locator, + [string]$WingetId + ) + $found = & $Locator + if ($found -and (Test-Path -LiteralPath $found)) { + Write-Verbose "${Name}: $found" + return $found + } + if (-not $Install) { + Write-Host "${Name}: NOT FOUND. Rerun with -Install to install via winget." -ForegroundColor Yellow + return $null + } + Write-Host "${Name}: missing - installing $WingetId via winget (per-user, no admin)..." -ForegroundColor Cyan + & winget install --id $WingetId --exact --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw "winget install $WingetId failed" } + $found = & $Locator + if (-not $found -or -not (Test-Path -LiteralPath $found)) { + throw "$Name still not found after winget install. Open a new terminal so the PATH refresh applies." + } + Write-Verbose "$Name (just installed): $found" + return $found +} + +$tools = @{} + +$tools.zig = Resolve-Tool -Name 'zig' -WingetId 'zig.zig' -Locator { + (Get-Command zig -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source) +} + +$tools.nasm = Resolve-Tool -Name 'nasm' -WingetId 'NASM.NASM' -Locator { + $p = Join-Path $env:LOCALAPPDATA 'bin\NASM\nasm.exe' + if (Test-Path -LiteralPath $p) { $p } else { $null } +} + +$tools.qemu = Resolve-Tool -Name 'qemu-system-x86_64' -WingetId 'SoftwareFreedomConservancy.QEMU' -Locator { + $p = 'C:\Program Files\qemu\qemu-system-x86_64.exe' + if (Test-Path -LiteralPath $p) { $p } else { $null } +} + +# qemu-img sits next to qemu-system-x86_64. +if ($tools.qemu) { + $tools['qemu-img'] = Join-Path (Split-Path $tools.qemu) 'qemu-img.exe' + if (-not (Test-Path -LiteralPath $tools['qemu-img'])) { + throw "qemu-img.exe missing next to $($tools.qemu)" + } +} else { + $tools['qemu-img'] = $null +} + +# python is used by the qemu test suites and the vm/ header generators. +$tools.python = Resolve-Tool -Name 'python' -WingetId 'Python.Python.3.13' -Locator { + (Get-Command python -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source) +} + +# objcopy (GNU binutils) makes the ELF32 container for QEMU's multiboot loader, +# mirroring the Makefile's `$(OBJCOPY) -O elf32-i386`. zig's bundled llvm-objcopy +# cannot emit ELF, so this is the one tool that comes from MSYS2's pacman rather +# than winget. Install = winget MSYS2, then pacman the mingw binutils package. +$tools.objcopy = $null +$cmd = Get-Command objcopy -ErrorAction SilentlyContinue | Select-Object -First 1 +if ($cmd -and (Test-Path -LiteralPath $cmd.Source)) { + if ((& $cmd.Source --version 2>$null | Select-Object -First 1) -like 'GNU objcopy*') { + $tools.objcopy = $cmd.Source + } +} +if (-not $tools.objcopy) { + foreach ($root in @((Join-Path $env:SystemDrive 'msys64\mingw64\bin'), 'C:\msys64\mingw64\bin')) { + $p = Join-Path $root 'objcopy.exe' + if (-not (Test-Path -LiteralPath $p)) { continue } + $ver = & $p --version 2>$null | Select-Object -First 1 + if ($ver -like 'GNU objcopy*') { + $tools.objcopy = $p + break + } + } +} +if (-not $tools.objcopy -and $Install) { + Write-Host "objcopy: missing - installing MSYS2 via winget, then mingw-w64-x86_64-binutils via pacman..." -ForegroundColor Cyan + & winget install --id MSYS2.MSYS2 --exact --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw 'winget install MSYS2.MSYS2 failed' } + $pacman = Join-Path $env:SystemDrive 'msys64\usr\bin\pacman.exe' + if (-not (Test-Path -LiteralPath $pacman)) { throw 'MSYS2 installed but pacman.exe was not found' } + & $pacman -Sy --noconfirm + if ($LASTEXITCODE -ne 0) { throw 'pacman -Sy failed' } + & $pacman -S --noconfirm mingw-w64-x86_64-binutils + if ($LASTEXITCODE -ne 0) { throw 'pacman install of mingw-w64-x86_64-binutils failed' } + $tools.objcopy = Join-Path $env:SystemDrive 'msys64\mingw64\bin\objcopy.exe' + if (-not (Test-Path -LiteralPath $tools.objcopy)) { throw 'objcopy still not found after the MSYS2 install' } +} +if (-not $tools.objcopy) { + Write-Host 'objcopy: NOT FOUND. Rerun with -Install to install MSYS2 binutils.' -ForegroundColor Yellow +} + +$missing = @($tools.GetEnumerator() | Where-Object { -not $_.Value }) +if ($missing.Count -gt 0) { + Write-Host "Still missing: $($missing.Name -join ', ')" -ForegroundColor Red + exit 1 +} + +$out = Join-Path $PSScriptRoot '.toolchain.json' +$tools | ConvertTo-Json -Depth 2 | Set-Content -LiteralPath $out -Encoding utf8 + +Write-Host "toolchain recorded in $out" +$tools.GetEnumerator() | Sort-Object Key | ForEach-Object { + Write-Host (" {0,-12} {1}" -f $_.Key, $_.Value) +} diff --git a/scripts/run.ps1 b/scripts/run.ps1 new file mode 100644 index 0000000..05893f6 --- /dev/null +++ b/scripts/run.ps1 @@ -0,0 +1,141 @@ +<# +.SYNOPSIS + Boot the built fable-os kernel in QEMU on Windows. + +.DESCRIPTION + Windows counterpart of `make run` / `make run-nox`. Requires kernel.bin + (see scripts/build.ps1). Reads the API key from .env exactly the way the + Makefile does -- ANTHROPIC_API_KEY wins over KEY, and the environment's + $ANTHROPIC_API_KEY is used only when .env has neither -- writes it to a + temp file OUTSIDE the repository, passes it to the guest over fw_cfg, and + deletes it however the run ends (normal exit, QEMU failure, Ctrl-C). + + Deliberately NOT read: $env:KEY. The Makefile's rule says KEY in the + environment is a maximally generic name that may hold any licence key, and + sending a stranger's secret to api.anthropic.com is not acceptable. + + The API key is never a build input and never a file in this tree, matching + the README's "The API key never enters the build". + +.PARAMETER NoGraphics + Headless boot (-display none), serial on the console. `make run-nox`. + +.PARAMETER Extra + Extra QEMU arguments, appended verbatim after the network and disk flags. + Same hook as the Makefile's QEMU_EXTRA: hardware the kernel has no driver + for is the whole point of vm/. + +.PARAMETER Disk + Disk image path, default disk.img (created as 128 MiB of zeros if absent; + the kernel formats it FAT32 on first boot). Pass -Disk '' for no disk. + +.EXAMPLE + .\scripts\run.ps1 +.EXAMPLE + .\scripts\run.ps1 -NoGraphics +.EXAMPLE + .\scripts\run.ps1 -Extra '-device AC97 -audiodev dsound,id=a0' +#> +[CmdletBinding()] +param( + [switch]$NoGraphics, + [string]$Extra = '', + [string]$Disk = 'disk.img' +) + +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +Set-Location $repo + +if (-not (Test-Path -LiteralPath (Join-Path $repo 'kernel.bin'))) { + throw "kernel.bin not found. Run .\scripts\build.ps1 first." +} + +$toolchainJson = Join-Path $PSScriptRoot '.toolchain.json' +if (-not (Test-Path -LiteralPath $toolchainJson)) { + & (Join-Path $PSScriptRoot 'ensure-toolchain.ps1') -Install | Out-Null +} +$T = Get-Content -LiteralPath $toolchainJson -Raw | ConvertFrom-Json +if (-not $T.qemu -or -not (Test-Path -LiteralPath $T.qemu)) { + throw "QEMU not recorded; run .\scripts\ensure-toolchain.ps1" +} + +# --------------------------------------------------------------------------- +# The API key, read exactly like the Makefile's READ_KEY. +# --------------------------------------------------------------------------- +$keyFile = $null +$key = $null +$envPath = Join-Path $repo '.env' +if (Test-Path -LiteralPath $envPath) { + $lines = Get-Content -LiteralPath $envPath + foreach ($name in 'ANTHROPIC_API_KEY', 'KEY') { + foreach ($line in $lines) { + $m = [regex]::Match($line, "^\s*(?:export\s+)?$name\s*=\s*(?:'([^']*)'|`"([^`"]*)`"|([^#\r\n]*))") + if ($m.Success) { + $key = ($m.Groups[1].Value + $m.Groups[2].Value + $m.Groups[3].Value).Trim() + break + } + } + if ($key) { break } + } + $others = $lines | + ForEach-Object { + if ($_ -match "^\s*(?:export\s+)?([A-Z_]*KEY)\s*=") { $matches[1] } + } | + Where-Object { $_ -ne 'KEY' -and $_ -ne 'ANTHROPIC_API_KEY' } | + Sort-Object -Unique + if ($others) { + Write-Host "note: .env also defines $($others -join ', ') - IGNORED. Only KEY and ANTHROPIC_API_KEY are read as the Anthropic key." + } +} +if (-not $key) { $key = $env:ANTHROPIC_API_KEY } + +$fwCfgArgs = @() +if ($key) { + $keyFile = Join-Path $env:TEMP ("fableos-fwcfg." + [IO.Path]::GetRandomFileName()) + [IO.File]::WriteAllText($keyFile, $key) # no trailing newline, like printf '%s' + $fwCfgArgs = @('-fw_cfg', "name=opt/fableos/apikey,file=$keyFile") + Write-Host "note: api key passed to the guest over fw_cfg as opt/fableos/apikey" +} else { + Write-Host "note: no api key in .env and no ANTHROPIC_API_KEY - the API will answer 401" +} + +# --------------------------------------------------------------------------- +# Disk: 128 MiB of zeros; the kernel formats it FAT32 on first boot. +# --------------------------------------------------------------------------- +$diskArgs = @() +if ($Disk) { + $diskPath = Join-Path $repo $Disk + if (-not (Test-Path -LiteralPath $diskPath)) { + if (-not $T.'qemu-img') { throw "qemu-img not found; run scripts\ensure-toolchain.ps1" } + Write-Host "disk: creating $Disk (128 MiB of zeros - the kernel formats it FAT32)" + & $T.'qemu-img' create -f raw $diskPath 128M | Out-Null + if ($LASTEXITCODE -ne 0) { throw "qemu-img create failed for $Disk" } + } + $diskArgs = @('-drive', "file=$diskPath,format=raw,if=ide,index=0,media=disk") +} + +# --------------------------------------------------------------------------- +# QEMU +# --------------------------------------------------------------------------- +$qemuArgs = @('-kernel', (Join-Path $repo 'kernel.bin')) +$qemuArgs += '-netdev', 'user,id=n0', '-device', 'e1000,netdev=n0' +$qemuArgs += $diskArgs +if ($Extra) { $qemuArgs += ($Extra -split ' ') } +$qemuArgs += '-display', ($(if ($NoGraphics) { 'none' } else { 'gtk' })) +$qemuArgs += '-serial', 'stdio' +$qemuArgs += $fwCfgArgs + +Write-Host "qemu: $($T.qemu)" +Write-Host " $($qemuArgs -join ' ')" + +try { + & $T.qemu @qemuArgs + $code = $LASTEXITCODE + if ($code -ne 0) { Write-Host "qemu exited with $code" } +} +finally { + if ($keyFile -and (Test-Path -LiteralPath $keyFile)) { + Remove-Item -LiteralPath $keyFile -Force + } +} From 7702b9d93ea3ffe1fe6c69cd67201af522026db9 Mon Sep 17 00:00:00 2001 From: Dragohn Date: Mon, 3 Aug 2026 16:18:18 +0200 Subject: [PATCH 2/2] test: run the QEMU boot-assertion suite on Windows --- README.md | 15 ++++++- scripts/build.ps1 | 17 ++++++- scripts/test-qemu.ps1 | 78 ++++++++++++++++++++++++++++++++ tests/qemu/harness.py | 100 ++++++++++++++++++++++++++++++++---------- 4 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 scripts/test-qemu.ps1 diff --git a/README.md b/README.md index 0978bda..966475e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ make run ### On Windows -There is no x86_64-elf toolchain here, so the same steps run through three +There is no x86_64-elf toolchain here, so the same steps run through four scripts that build with Zig's bundled clang instead. The Windows port follows the approach proven by [NO_OS](https://github.com/coff33ninja/NO_OS) — a from-scratch x86-64 kernel of a similar nature — whose @@ -55,11 +55,12 @@ is the one trick this build was missing: .\scripts\ensure-toolchain.ps1 # once: zig, nasm, qemu, python, MSYS2 binutils .\scripts\build.ps1 # 211 objects -> kernel.bin .\scripts\run.ps1 # headless: .\scripts\run.ps1 -NoGraphics +.\scripts\test-qemu.ps1 # the 6 QEMU boot-assertion cases, plus the formatter lint ``` Build flags and API-key handling mirror the Makefile exactly (same CFLAGS minus the GCC-only ones, same `.env` two-name rule, same `opt/fableos/apikey` fw_cfg -channel). Two Windows-only wrinkles, both contained in the scripts: +channel). Windows-only wrinkles, all contained in the scripts: - `-fno-sanitize=undefined` is added, or zig's compiler_rt drags in the `__ubsan_handle_*` helpers and the link fails. @@ -70,6 +71,16 @@ channel). Two Windows-only wrinkles, both contained in the scripts: MSYS2's pacman), because zig's bundled llvm-objcopy only emits `binary`. `run.ps1` prints a warning that "multiboot knows VBE. we don't" — that one is a benign stderr line from QEMU, not the kernel. +- Objects are linked in the Makefile's source order, not directory order: the + self-registering driver table initializes drivers in link order, and the + boot-assertion cases pin that order. +- `-Wl,-e,start` restores the kernel entry point the clang driver would + otherwise zero out. +- The QEMU suite has no `make`, so `test-qemu.ps1` points the harness's + per-case build hook (`FABLEOS_TEST_MAKE`) at `build.ps1` — the one + `build-cflags:` case (the AC'97 reference bring-up) builds and restores the + tree through it — and the interactive REPL case speaks to the VM over a + loopback TCP serial port instead of the POSIX AF_UNIX socket. That boots with no key: the kernel still completes a real TLS handshake to `api.anthropic.com` and gets an honest `401` back, which is itself proof the HTTPS diff --git a/scripts/build.ps1 b/scripts/build.ps1 index a07451a..57ffd7a 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -135,6 +135,15 @@ $kernelSources = @( 'net/json.c', 'net/model.c', 'net/chat.c', 'net/sse.c', 'net/net.c', 'net/fetch.c', 'net/faultchat.c', 'net/tls_ca.c' ) +# The Makefile links the AC'97 reference fixture only into the bring-up build +# that tests/qemu/cases/ac97.case asks for with `build-cflags:`: +# ifneq ($(findstring FABLEOS_AC97_REFERENCE,$(EXTRA_CFLAGS)),) +# KERNEL_SRCS += tests/qemu/fixtures/ac97_boot.c +# The fixture #errors without the define, so this condition must stay in +# lock-step with the Makefile's or that build cannot exist here at all. +if ($ExtCFlags -like '*FABLEOS_AC97_REFERENCE*') { + $kernelSources += 'tests/qemu/fixtures/ac97_boot.c' +} # tools/ is wildcarded on purpose: a tool self-registers through a linker # section (REGISTER_TOOL), so this must never be a fixed list. $toolSources = @(Get-ChildItem -LiteralPath (Join-Path $repo 'tools') -Filter '*.c' | @@ -281,7 +290,13 @@ Write-Host "objects: $((Get-ChildItem -LiteralPath $objDir -Filter '*.o').Count) # the clang driver would otherwise zero out). # --------------------------------------------------------------------------- $linkArgs = @('cc', '-target', 'x86_64-freestanding', '-Wl,-e,start', '-T', 'linker.ld', '-o', $kernelElf) -$linkArgs += (Get-ChildItem -LiteralPath $objDir -Filter '*.o' | ForEach-Object { $_.FullName }) +# Objects on the link line in the Makefile's OBJS order (boot, isr/switch, then +# KERNEL_SRCS order), NOT the alphabet Get-ChildItem returns. The driver_table +# and tool_table sections self-register in the order their objects appear on +# the link line, so boot-time init order would silently follow directory +# sorting otherwise -- and the QEMU boot-assertion suite pins that order. +$linkArgs += ($asmPairs | ForEach-Object { Join-Path $objDir $_.obj }) +$linkArgs += ($allSources | ForEach-Object { & $objOf $_ }) Write-Host "link kernel.elf" Invoke-Native -File $T.zig -ArgList $linkArgs -What 'kernel link' diff --git a/scripts/test-qemu.ps1 b/scripts/test-qemu.ps1 new file mode 100644 index 0000000..48d2276 --- /dev/null +++ b/scripts/test-qemu.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + Run the QEMU boot-assertion suite on Windows. + +.DESCRIPTION + Windows counterpart of tests/qemu/run.sh (`make test-qemu`). Resolves the + Python interpreter and QEMU binary from scripts/.toolchain.json (the same + file ensure-toolchain.ps1 writes, and run.ps1/build.ps1 read), builds + kernel.bin if it is missing, then runs tests/qemu/harness.py with every + argument passed through. + + Two Windows facts are encoded here, both load-bearing: + + * QEMU is not on PATH (it lives under Program Files), so FABLEOS_TEST_QEMU + is set to the resolved binary. The harness only checks PATH otherwise. + * Windows has no make. Cases with a `build-cflags:` line need their own + kernel, and the harness's build hook defaults to `make -C `. That + is pointed at scripts/build.ps1 here through FABLEOS_TEST_MAKE: a \x1f- + separated argv prefix ending in the script path, so the harness invokes + `powershell -File build.ps1 -ExtCFlags:`. The case-file character + filter (harness.py `_setting`) guarantees the flags are bare -D/-U/-f/-W + words, and the colon form is the one PowerShell -File binds + unambiguously, so no quoting can leak a command through. + +.PARAMETER Args + Passed straight to harness.py: case-name filters (e.g. "boot"), -v to keep + passing logs, -h for harness help. + +.EXAMPLE + .\scripts\test-qemu.ps1 + .\scripts\test-qemu.ps1 repl-turn -v +#> + +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +Set-Location $repo + +$toolchainJson = Join-Path $PSScriptRoot '.toolchain.json' +if (-not (Test-Path -LiteralPath $toolchainJson)) { + & (Join-Path $PSScriptRoot 'ensure-toolchain.ps1') -Install | Out-Null +} +$T = Get-Content -LiteralPath $toolchainJson -Raw | ConvertFrom-Json + +$python = if ($env:FABLEOS_TEST_PYTHON) { $env:FABLEOS_TEST_PYTHON } else { $T.python } +if (-not $python -or -not (Test-Path -LiteralPath $python)) { + throw "python not found; set FABLEOS_TEST_PYTHON or run .\scripts\ensure-toolchain.ps1" +} +$qemu = if ($env:FABLEOS_TEST_QEMU) { $env:FABLEOS_TEST_QEMU } else { $T.qemu } +if (-not $qemu -or -not (Test-Path -LiteralPath $qemu)) { + throw "QEMU not found; run .\scripts\ensure-toolchain.ps1" +} + +# Build the kernel if it isn't there, exactly like run.sh does for `make`. +if (-not (Test-Path -LiteralPath (Join-Path $repo 'kernel.bin'))) { + Write-Host 'kernel.bin missing — building...' + & (Join-Path $PSScriptRoot 'build.ps1') -SkipInstall + if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $repo 'kernel.bin'))) { + throw 'build failed' + } +} + +$env:FABLEOS_TEST_QEMU = $qemu + +# Point the harness's variant-build hook at build.ps1 (Windows has no make). +$powershell = (Get-Command powershell.exe -ErrorAction SilentlyContinue).Source +if (-not $powershell) { $powershell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" } +$sep = [string][char]0x1f +$env:FABLEOS_TEST_MAKE = ($powershell + $sep + '-NoProfile' + $sep + + '-ExecutionPolicy' + $sep + 'Bypass' + $sep + '-File' + $sep + + (Join-Path $PSScriptRoot 'build.ps1') + $sep + '-SkipInstall') + +# The Makefile's `test-qemu` recipe runs the formatter lint before the boot +# suite (lib/kfmt.c is a reduced printf the host tests cannot see). +& $python (Join-Path $repo 'tests\qemu\lint_printf.py') +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +& $python (Join-Path $repo 'tests\qemu\harness.py') @args +exit $LASTEXITCODE diff --git a/tests/qemu/harness.py b/tests/qemu/harness.py index c00bd1b..99dabdc 100644 --- a/tests/qemu/harness.py +++ b/tests/qemu/harness.py @@ -22,6 +22,10 @@ user,model=e1000,restrict=on to fake no internet) FABLEOS_TEST_QEMU_EXTRA extra QEMU arguments appended to every case, split like a shell word list (see qemu_args) + FABLEOS_TEST_MAKE how a `build-cflags:` case gets its kernel; a + \\x1f-separated argv prefix that replaces + `make -C -j8`. scripts/test-qemu.ps1 sets it + to build.ps1, because Windows has no make. FABLEOS_TEST_KEEPLOGS=1 keep the scratch dir even when everything passes FABLEOS_TEST_PYTHON=path interpreter run.sh uses to start this file """ @@ -80,7 +84,16 @@ def _reap(proc, grace=2.0): _children.discard(proc) if proc.poll() is not None: return - for sig in (signal.SIGTERM, signal.SIGKILL): + # Windows Python has neither SIGKILL nor process groups; proc.kill() + # (TerminateProcess) is the only move and needs no grace loop. + if not hasattr(os, "killpg"): + try: + proc.kill() + except ProcessLookupError: + pass + proc.wait() + return + for sig in (signal.SIGTERM, getattr(signal, "SIGKILL", signal.SIGTERM)): try: os.killpg(proc.pid, sig) except (ProcessLookupError, PermissionError): @@ -119,8 +132,12 @@ def _on_signal(signum, _frame): os.kill(os.getpid(), signum) -for _s in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP): - signal.signal(_s, _on_signal) +# SIGHUP does not exist on Windows Python; the getattr keeps the registration +# valid on both platforms (SIGINT/SIGTERM are what actually fire on a console +# Ctrl-C, and on Windows there is no hangup to catch at all). +for _s in (signal.SIGINT, signal.SIGTERM, getattr(signal, "SIGHUP", None)): + if _s is not None: + signal.signal(_s, _on_signal) # -------------------------------------------------------------------------- @@ -647,16 +664,40 @@ def read_log(path): return raw.decode("utf-8", "replace").replace("\r\n", "\n").replace("\r", "\n") +def interactive_serial(): + """A (-serial spec, client address, socket family) triple that agrees. + + The interactive cases need to type into the VM, so the harness must know + where QEMU's serial chardev will listen. POSIX uses an AF_UNIX socket + (QEMU's `unix:` chardev); Windows Python has no AF_UNIX and the Windows + QEMU build has no unix chardev, so a loopback TCP port is used there + instead. The port is probed for availability first; the small race before + QEMU binds it is covered by the connect() retry loop. + """ + if not hasattr(socket, "AF_UNIX"): + probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + probe.bind(("127.0.0.1", 0)) + port = probe.getsockname()[1] + probe.close() + return ("tcp:127.0.0.1:%d,server=on,wait=off" % port, + ("127.0.0.1", port), socket.AF_INET) + # Short path: macOS caps AF_UNIX paths at ~104 bytes, and $TMPDIR is long. + sockdir = tempfile.mkdtemp(prefix="tqs", dir="/tmp") + sockpath = os.path.join(sockdir, "com1") + return ("unix:%s,server=on,wait=off" % sockpath, sockpath, socket.AF_UNIX) + + class SerialSocket: - """Bidirectional COM1 over a unix socket, tee'd into the log file. + """Bidirectional COM1 over a socket, tee'd into the log file. Only used by cases with `send:` steps; the plain read-only cases keep the simpler `-serial file:` path. The kernel registers COM1 as an input source (drivers/input/serial_input.c), so writing here is the same as typing. """ - def __init__(self, sockpath, logpath): - self.path = sockpath + def __init__(self, addr, family, logpath): + self.addr = addr + self.family = family self.fh = open(logpath, "wb") self.sock = None self.buf = bytearray() @@ -668,9 +709,9 @@ def connect(self, deadline): # Closed on every failed attempt: the retry loop runs up to ~200 # times in the 10 s window, and leaking a descriptor per attempt (per # interactive case) left reclamation to the garbage collector. - s = socket.socket(socket.AF_UNIX) + s = socket.socket(self.family) try: - s.connect(self.path) + s.connect(self.addr) except (FileNotFoundError, ConnectionRefusedError, OSError): s.close() time.sleep(0.05) @@ -848,15 +889,25 @@ def base_cflags(): def _make(cflags, what): - """make -C with an exact EXTRA_CFLAGS. No shell: the flags come from a - case file, and `subprocess` with a list argv gives them no way to become - commands even if the character filter in _setting() were wrong.""" - proc = subprocess.run(["make", "-C", OSDIR, "-j8", "EXTRA_CFLAGS=" + cflags], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + """Build the tree's kernel with an exact EXTRA_CFLAGS. No shell: the flags + come from a case file, and `subprocess` with a list argv gives them no way + to become commands even if the character filter in _setting() were wrong. + + POSIX uses `make`; Windows has no make, so scripts/test-qemu.ps1 points + FABLEOS_TEST_MAKE at `build.ps1`. The override is a \\x1f-separated argv + prefix (a NUL would be cleaner but env vars cannot carry one), and the + flags ride as ONE argument in the colon form PowerShell -File binds + unambiguously (`-ExtCFlags:`), never as tokens a shell could split.""" + prefix = os.environ.get("FABLEOS_TEST_MAKE") + if prefix: + argv = prefix.split("\x1f") + ["-ExtCFlags:" + cflags] + else: + argv = ["make", "-C", OSDIR, "-j8", "EXTRA_CFLAGS=" + cflags] + proc = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if proc.returncode != 0: tail = proc.stdout.decode("utf-8", "replace").strip().splitlines()[-25:] - raise BuildError("%s failed: make EXTRA_CFLAGS='%s' exited %d\n%s" - % (what, cflags, proc.returncode, + raise BuildError("%s failed: EXTRA_CFLAGS='%s' (via %s) exited %d\n%s" + % (what, cflags, argv[0], proc.returncode, "\n".join(" " + t for t in tail))) @@ -930,18 +981,18 @@ def qemu_args(case, serial): def boot_interactive(case, logpath, timeout, offline): """Same as boot(), but drives the REPL: wait for a prompt, type a line, - wait for the next one. Serial goes over a unix socket instead of a file.""" + wait for the next one. Serial goes over a socket instead of a file; the + transport (unix on POSIX, loopback TCP on Windows) comes from + interactive_serial() so the -serial spec and this client always agree.""" errpath = logpath + ".qemu-stderr" - # Short path: macOS caps AF_UNIX paths at ~104 bytes, and $TMPDIR is long. - sockdir = tempfile.mkdtemp(prefix="tqs", dir="/tmp") - sockpath = os.path.join(sockdir, "com1") - cmd = qemu_args(case, "unix:%s,server=on,wait=off" % sockpath) + serial_spec, ser_addr, ser_family = interactive_serial() + cmd = qemu_args(case, serial_spec) errfh = open(errpath, "wb") proc = subprocess.Popen(cmd, stdout=errfh, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True) _children.add(proc) - ser = SerialSocket(sockpath, logpath) + ser = SerialSocket(ser_addr, ser_family, logpath) readies = case.ready_res(offline) sends = [s for s in case.sends if s.active(offline)] start = time.time() @@ -950,7 +1001,7 @@ def boot_interactive(case, logpath, timeout, offline): note = "" try: if not ser.connect(start + min(10.0, timeout)): - note = "could not connect to the QEMU serial socket %s" % sockpath + note = "could not connect to the QEMU serial socket %s" % ser_addr else: step = 0 scan = 0 # only look for a trigger after the @@ -983,7 +1034,10 @@ def boot_interactive(case, logpath, timeout, offline): ser.close() _reap(proc) errfh.close() - shutil.rmtree(sockdir, ignore_errors=True) + # POSIX: interactive_serial() parked the unix socket in a temp dir. + # On Windows the socket is loopback TCP and there is no dir to remove. + if getattr(socket, "AF_UNIX", None) == ser_family: + shutil.rmtree(os.path.dirname(ser_addr), ignore_errors=True) qerr = qemu_diagnosis(errpath, early, hit) if note: qerr = (qerr + "\n" + note).strip()