diff --git a/.claude/memory/schos-cross-platform-build.md b/.claude/memory/schos-cross-platform-build.md index e33fddb..12bd590 100644 --- a/.claude/memory/schos-cross-platform-build.md +++ b/.claude/memory/schos-cross-platform-build.md @@ -56,3 +56,36 @@ not a theoretical one. 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 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.** diff --git a/run.ps1 b/run.ps1 index 38ac724..8469442 100644 --- a/run.ps1 +++ b/run.ps1 @@ -1,43 +1,60 @@ # SchOS launcher (Windows side). # -# Why this exists: without it you have to hand-type a QEMU command line, and a -# hand-typed one drifts -- notably onto a stale fs.img, which boots to -# "fsinit: invalid file system" because the wrong disk is attached as hdb. +# Why this exists: without it you hand-type a QEMU command line, and a +# hand-typed one drifts -- notably onto the wrong disk as hdb, which boots +# 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 -# Hyper-V VM, so it hard-requires virtualisation, and enabling that puts the -# Hyper-V hypervisor underneath the whole machine -- which makes VMware run -# nested on top of it and slows the box down permanently. Not a trade worth -# making for a build environment. +# The build runs natively rather than through WSL. WSL2 runs a real Linux +# kernel inside a Hyper-V VM, so it hard-requires virtualisation, and enabling +# that puts the Hyper-V hypervisor underneath the whole machine -- which makes +# VMware run nested on top of it and slows the box down permanently. Not a +# trade worth making for a build environment. # -# Instead it builds natively: clang/lld cross-compile the kernel to bare-metal -# ELF, mingw-w64 builds the host tools, and Git Bash supplies make, perl, sed -# and dd. See the Toolchain section of the Makefile. +# Instead: clang/lld cross-compile the kernel to bare-metal ELF, mingw-w64 +# builds the host tools, and Git Bash supplies make, perl, sed and dd. See the +# 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 -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 -SchFS boot the ramdisk-root kernel with a SchFS volume -# attached as hdb; then run cat /proc/schfs +# +# There is no -SchFS switch. SchFS is the root filesystem now, not an extra +# volume to attach, so every boot is a SchFS boot. param( [switch]$NoBuild, [switch]$Headless, [switch]$Gdb, - [switch]$SchFS, - [string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe" + [string]$Qemu ) $ErrorActionPreference = 'Stop' $root = $PSScriptRoot -# The two images QEMU needs. These paths are the single source of truth -- -# keep them in sync with the 'qemu' and 'qemu-schfs' targets in the Makefile. +if (-not $Qemu) { + $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 )" } + +# 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' -$hdb = if ($SchFS) { Join-Path $root 'build\schfs.img' } - else { Join-Path $root 'build\fs.img' } +$hdb = Join-Path $root 'build/schfs.img' if (-not $NoBuild) { 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 # 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). - $bash = Join-Path $env:ProgramFiles 'Git\bin\bash.exe' - if (-not (Test-Path $bash)) { throw "Git Bash not found at $bash" } + $bashCandidates = @( + (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 = @( "$env:USERPROFILE\scoop\shims" "$env:USERPROFILE\scoop\apps\mingw\current\bin" @@ -54,38 +78,37 @@ if (-not $NoBuild) { ) -join ';' $env:PATH = "$toolPath;$env:PATH" - # -SchFS needs the ramdisk-root kernel: the root filesystem then comes out - # of memory, which leaves device 1 free for the SchFS volume. Booting the - # normal kernel with a SchFS disk as hdb would leave it with no root. - $target = if ($SchFS) { 'make ramroot && make build/schfs.img' } else { 'make' } - & $bash -lc "cd /d/DEV/SchOS && $target" + # The repo's own path, not a hardcoded one. This used to say + # `cd /d/DEV/SchOS`, which survived the Schunix -> SchOS rename only by + # luck and would break the moment the tree is cloned anywhere else. + $posixRoot = ($root -replace '\\', '/') -replace '^([A-Za-z]):', '/$1' + $posixRoot = $posixRoot.Substring(0, 2).ToLower() + $posixRoot.Substring(2) + + & $bash -lc "cd '$posixRoot' && make" if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" } } 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 -# a stale or empty hdb. Each format has its own magic in its own place. +# Guard against the exact failure this script exists to prevent: booting +# 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) try { $buf = New-Object byte[] 4 - # v1: superblock in block 1 with BSIZE 1024, FSMAGIC 0x10203040. - # SchFS: first uberblock in block 1 with SCHFS_BLKSIZE 4096, magic "SchF". - $null = $fs.Seek($(if ($SchFS) { 4096 } else { 1024 }), 'Begin') + $null = $fs.Seek(4096, 'Begin') $null = $fs.Read($buf, 0, 4) } finally { $fs.Close() } -if ($SchFS) { - # 0x53636846 little-endian on disk: 46 68 63 53 ("FhcS" byte order). - if ($buf[0] -ne 0x46 -or $buf[1] -ne 0x68 -or $buf[2] -ne 0x63 -or $buf[3] -ne 0x53) { - throw "$hdb is not a valid SchFS image (bad uberblock magic). Rebuild it: make build/schfs.img" - } -} 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" - } +if ($buf[0] -ne 0x46 -or $buf[1] -ne 0x68 -or $buf[2] -ne 0x63 -or $buf[3] -ne 0x53) { + throw ("$hdb is not a SchFS volume (bad uberblock magic: " + + ('{0:x2} {1:x2} {2:x2} {3:x2}' -f $buf[0], $buf[1], $buf[2], $buf[3]) + + ").`n Rebuild it: rm -f build/schfs.img && make") } $qemuArgs = @( @@ -98,11 +121,15 @@ $qemuArgs = @( ) if ($Headless) { - $log = Join-Path $root 'build\serial.log' + $log = Join-Path $root 'build/serial.log' $qemuArgs += @('-display', 'none', '-serial', "file:$log") Write-Host "==> headless; serial -> $log" -ForegroundColor Cyan } 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) {