Rewrite run.ps1; it was five kinds of stale

run.ps1 is a .ps1 that does not live under tools/, so every sweep that
fixed the other launchers went straight past it:

  - hdb defaulted to build/fs.img, which nothing builds since SchFS
    became the root, so it pointed at a file that does not exist. QEMU
    came up with no usable disk on device 1 and the kernel panicked with
    SCHFS_ROOT -- "no SchFS volume on the root device". That is exactly
    the failure this script exists to prevent.
  - a -SchFS switch built around the old arrangement, where SchFS was a
    secondary volume and needed `make ramroot` to free device 1. There
    is no such arrangement now; every boot is a SchFS boot.
  - `cd /d/DEV/SchOS` hardcoded into the build invocation. It survived
    the Schunix -> SchOS rename by luck and would break on any clone.
    Derived from $PSScriptRoot now.
  - a hardcoded scoop QEMU path, and build\ backslash literals.
  - only -vga std, where the Makefile also attaches virtio-gpu-pci.

The magic check is SchFS-only now and prints the bytes it actually read
when it fails.

SchFS itself is fine: it mounts on 1, 2 and 4 CPUs, and all seven
bootcheck suites pass.
This commit is contained in:
Nexoniarz 2026-08-09 14:49:47 +02:00
commit 66340e720e
2 changed files with 106 additions and 46 deletions

View file

@ -56,3 +56,36 @@ not a theoretical one.
written from documented behaviour and have **never been executed** — this written from documented behaviour and have **never been executed** — this
machine has no WSL and no second host. Windows is the only host where the build machine has no WSL and no second host. Windows is the only host where the build
and `tools/bootcheck.ps1` have actually been run green. and `tools/bootcheck.ps1` have actually been run green.
## run.ps1 is at the repo root, so tool sweeps miss it
It is a `.ps1` but it does **not** live under `tools/`, and it went stale in
five ways at once while every tool beside it was fixed: hdb defaulting to
`build/fs.img` (which nothing builds any more, so the file simply does not
exist), a `-SchFS` switch built around the old arrangement where SchFS was a
secondary volume, a hardcoded QEMU path, `build\` backslash literals, and
`cd /d/DEV/SchOS` hardcoded into the build invocation -- which survived the
Schunix -> SchOS rename by luck and would break on any clone. Rewritten
2026-08-09. **When sweeping the tools, check the repo root too.**
## The stale-image trap bit again, and it looks exactly like a filesystem bug
A leftover headless QEMU holds `schos.img` and `build/schfs.img` open. On
Windows that makes `rm` fail with *Device or resource busy*, so `make` stops
**after** the kernel has relinked -- leaving a stale `schos.img` that boots the
previous kernel. The symptom this time was a PSOGM reading
`schfs root: no SchFS volume on the root device`, which reads as SchFS being
broken and is not: SchFS mounts cleanly on 1, 2 and 4 CPUs.
`kill %1` from Git Bash does not reliably kill a Windows QEMU. Use:
```sh
pwsh -NoProfile -Command "Get-Process qemu-system-x86_64 -ErrorAction SilentlyContinue | Stop-Process -Force"
```
A tell: `ls -l schos.img` showing anything other than **5,120,000 bytes**
(`dd count=10000`) means the image was not rewritten and is stale.
**Rule: before believing a boot failure, confirm no QEMU is running and that
schos.img was written by the build you think it was.**

119
run.ps1
View file

@ -1,43 +1,60 @@
# SchOS launcher (Windows side). # SchOS launcher (Windows side).
# #
# Why this exists: without it you have to hand-type a QEMU command line, and a # Why this exists: without it you hand-type a QEMU command line, and a
# hand-typed one drifts -- notably onto a stale fs.img, which boots to # hand-typed one drifts -- notably onto the wrong disk as hdb, which boots
# "fsinit: invalid file system" because the wrong disk is attached as hdb. # straight into a PSOGM reading "no SchFS volume on the root device".
# #
# The build no longer goes through WSL. WSL2 runs a real Linux kernel inside a # The build runs natively rather than through WSL. WSL2 runs a real Linux
# Hyper-V VM, so it hard-requires virtualisation, and enabling that puts the # kernel inside a Hyper-V VM, so it hard-requires virtualisation, and enabling
# Hyper-V hypervisor underneath the whole machine -- which makes VMware run # that puts the Hyper-V hypervisor underneath the whole machine -- which makes
# nested on top of it and slows the box down permanently. Not a trade worth # VMware run nested on top of it and slows the box down permanently. Not a
# making for a build environment. # trade worth making for a build environment.
# #
# Instead it builds natively: clang/lld cross-compile the kernel to bare-metal # Instead: clang/lld cross-compile the kernel to bare-metal ELF, mingw-w64
# ELF, mingw-w64 builds the host tools, and Git Bash supplies make, perl, sed # builds the host tools, and Git Bash supplies make, perl, sed and dd. See the
# and dd. See the Toolchain section of the Makefile. # Toolchain section of the Makefile, and docs/building.md.
# #
# .\run.ps1 build (native) then boot, windowed # .\run.ps1 build then boot, windowed
# .\run.ps1 -NoBuild boot whatever is already built # .\run.ps1 -NoBuild boot whatever is already built
# .\run.ps1 -Headless no window; serial captured to build\serial.log # .\run.ps1 -Headless no window; serial captured to build/serial.log
# .\run.ps1 -Gdb wait for a debugger on :26000 before executing # .\run.ps1 -Gdb wait for a debugger on :26000 before executing
#
# .\run.ps1 -SchFS boot the ramdisk-root kernel with a SchFS volume # There is no -SchFS switch. SchFS is the root filesystem now, not an extra
# attached as hdb; then run cat /proc/schfs # volume to attach, so every boot is a SchFS boot.
param( param(
[switch]$NoBuild, [switch]$NoBuild,
[switch]$Headless, [switch]$Headless,
[switch]$Gdb, [switch]$Gdb,
[switch]$SchFS, [string]$Qemu
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = $PSScriptRoot $root = $PSScriptRoot
# The two images QEMU needs. These paths are the single source of truth -- if (-not $Qemu) {
# keep them in sync with the 'qemu' and 'qemu-schfs' targets in the Makefile. $found = Get-Command 'qemu-system-x86_64.exe' -ErrorAction SilentlyContinue
if ($found) {
$Qemu = $found.Source
} else {
$Qemu = Join-Path $env:USERPROFILE 'scoop/apps/qemu/current/qemu-system-x86_64.exe'
}
}
if (-not (Test-Path $Qemu)) { throw "QEMU not found: $Qemu (install it, or pass -Qemu <path>)" }
# The two images QEMU needs. Keep in sync with the 'qemu' target in the
# Makefile.
#
# hda the boot disk: bootblock, stage 2 and the kernel
# hdb the root filesystem, mounted as / by forkret()
#
# hdb was build/fs.img until SchFS became the root. Nothing builds fs.img on
# the default path any more, so the old default pointed at a file that no
# longer exists -- QEMU came up with no usable disk on device 1 and the kernel
# panicked with SCHFS_ROOT. That is the failure this script is supposed to
# prevent, so it is worth naming.
$hda = Join-Path $root 'schos.img' $hda = Join-Path $root 'schos.img'
$hdb = if ($SchFS) { Join-Path $root 'build\schfs.img' } $hdb = Join-Path $root 'build/schfs.img'
else { Join-Path $root 'build\fs.img' }
if (-not $NoBuild) { if (-not $NoBuild) {
Write-Host '==> building (native, no WSL)' -ForegroundColor Cyan Write-Host '==> building (native, no WSL)' -ForegroundColor Cyan
@ -45,8 +62,15 @@ if (-not $NoBuild) {
# The three toolchain directories, prepended so the build sees them whether # The three toolchain directories, prepended so the build sees them whether
# or not the current shell happened to inherit them: scoop's shims (make), # or not the current shell happened to inherit them: scoop's shims (make),
# mingw-w64 (host gcc), and LLVM (clang, ld.lld, llvm-objcopy/objdump). # mingw-w64 (host gcc), and LLVM (clang, ld.lld, llvm-objcopy/objdump).
$bash = Join-Path $env:ProgramFiles 'Git\bin\bash.exe' $bashCandidates = @(
if (-not (Test-Path $bash)) { throw "Git Bash not found at $bash" } (Join-Path $env:ProgramFiles 'Git/bin/bash.exe')
(Join-Path ${env:ProgramFiles(x86)} 'Git/bin/bash.exe')
(Join-Path $env:USERPROFILE 'scoop/apps/git/current/bin/bash.exe')
(Join-Path $env:LOCALAPPDATA 'Programs/Git/bin/bash.exe')
)
$bash = $bashCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
if (-not $bash) { throw "Git Bash not found; looked in:`n $($bashCandidates -join "`n ")" }
$toolPath = @( $toolPath = @(
"$env:USERPROFILE\scoop\shims" "$env:USERPROFILE\scoop\shims"
"$env:USERPROFILE\scoop\apps\mingw\current\bin" "$env:USERPROFILE\scoop\apps\mingw\current\bin"
@ -54,38 +78,37 @@ if (-not $NoBuild) {
) -join ';' ) -join ';'
$env:PATH = "$toolPath;$env:PATH" $env:PATH = "$toolPath;$env:PATH"
# -SchFS needs the ramdisk-root kernel: the root filesystem then comes out # The repo's own path, not a hardcoded one. This used to say
# of memory, which leaves device 1 free for the SchFS volume. Booting the # `cd /d/DEV/SchOS`, which survived the Schunix -> SchOS rename only by
# normal kernel with a SchFS disk as hdb would leave it with no root. # luck and would break the moment the tree is cloned anywhere else.
$target = if ($SchFS) { 'make ramroot && make build/schfs.img' } else { 'make' } $posixRoot = ($root -replace '\\', '/') -replace '^([A-Za-z]):', '/$1'
& $bash -lc "cd /d/DEV/SchOS && $target" $posixRoot = $posixRoot.Substring(0, 2).ToLower() + $posixRoot.Substring(2)
& $bash -lc "cd '$posixRoot' && make"
if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" } if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" }
} }
foreach ($img in @($hda, $hdb)) { foreach ($img in @($hda, $hdb)) {
if (-not (Test-Path $img)) { throw "missing image: $img (run without -NoBuild)" } if (-not (Test-Path $img)) {
throw "missing image: $img (run without -NoBuild, or: make)"
}
} }
# Guard against the exact failure this script exists to prevent: booting against # Guard against the exact failure this script exists to prevent: booting
# a stale or empty hdb. Each format has its own magic in its own place. # against a stale, empty or wrong-format root disk. The first uberblock sits in
# block 1 at SCHFS_BLKSIZE 4096, and starts with the magic "SchF" -- stored
# little-endian, so the bytes on disk read 46 68 63 53.
$fs = [System.IO.File]::OpenRead($hdb) $fs = [System.IO.File]::OpenRead($hdb)
try { try {
$buf = New-Object byte[] 4 $buf = New-Object byte[] 4
# v1: superblock in block 1 with BSIZE 1024, FSMAGIC 0x10203040. $null = $fs.Seek(4096, 'Begin')
# SchFS: first uberblock in block 1 with SCHFS_BLKSIZE 4096, magic "SchF".
$null = $fs.Seek($(if ($SchFS) { 4096 } else { 1024 }), 'Begin')
$null = $fs.Read($buf, 0, 4) $null = $fs.Read($buf, 0, 4)
} finally { $fs.Close() } } finally { $fs.Close() }
if ($SchFS) { if ($buf[0] -ne 0x46 -or $buf[1] -ne 0x68 -or $buf[2] -ne 0x63 -or $buf[3] -ne 0x53) {
# 0x53636846 little-endian on disk: 46 68 63 53 ("FhcS" byte order). throw ("$hdb is not a SchFS volume (bad uberblock magic: " +
if ($buf[0] -ne 0x46 -or $buf[1] -ne 0x68 -or $buf[2] -ne 0x63 -or $buf[3] -ne 0x53) { ('{0:x2} {1:x2} {2:x2} {3:x2}' -f $buf[0], $buf[1], $buf[2], $buf[3]) +
throw "$hdb is not a valid SchFS image (bad uberblock magic). Rebuild it: make build/schfs.img" ").`n Rebuild it: rm -f build/schfs.img && make")
}
} else {
if ($buf[0] -ne 0x40 -or $buf[1] -ne 0x30 -or $buf[2] -ne 0x20 -or $buf[3] -ne 0x10) {
throw "$hdb is not a valid SCHFS image (bad superblock magic). Rebuild it: rm -f build/fs.img && make"
}
} }
$qemuArgs = @( $qemuArgs = @(
@ -98,11 +121,15 @@ $qemuArgs = @(
) )
if ($Headless) { if ($Headless) {
$log = Join-Path $root 'build\serial.log' $log = Join-Path $root 'build/serial.log'
$qemuArgs += @('-display', 'none', '-serial', "file:$log") $qemuArgs += @('-display', 'none', '-serial', "file:$log")
Write-Host "==> headless; serial -> $log" -ForegroundColor Cyan Write-Host "==> headless; serial -> $log" -ForegroundColor Cyan
} else { } else {
$qemuArgs += @('-vga', 'std', '-serial', 'stdio') # Both display devices, matching QEMU_DISPLAY in the Makefile: the
# bootloader draws its splash through VBE in 16-bit real mode and there is
# no virtio path that early, so stdvga carries the splash and the text
# console while virtio-gpu is what the display class binds.
$qemuArgs += @('-vga', 'std', '-device', 'virtio-gpu-pci', '-serial', 'stdio')
} }
if ($Gdb) { if ($Gdb) {