# build.ps1 - build a Power C program for the C64 on a fresh work disk, then # optionally leave it back in BASIC, loaded and running. # # powershell -NoProfile -File build.ps1 # powershell -NoProfile -File build.ps1 -Run -Keys 'vim.c\njjj' # powershell -NoProfile -File build.ps1 -Shell 'l\n' (poke at the disk) # # Everything happens in ONE emulator session: compile each unit, link, then # "bye" out of the Power C shell, load the program from BASIC and run it. One # session is not just faster, it is required: two emulators both attached to # work.d81 write the image back over each other on exit and the files made by # the other one disappear. # # Power C is interactive - cc asks which disk to insert three times, the linker # wants one object file per ">" line, "^" for the libraries, an empty RETURN # and then a name - so every prompt is answered from the -keybuf string. VICE # paces those keystrokes in wall-clock milliseconds while the machine runs at # warp, so the delay has to be long enough that a step's answers arrive after # its prompt: cc, for one, clears the keyboard buffer when it starts. param( [string]$Prog = "vim", [string]$Keys = "", [string]$Shell = "", [int]$Delay = 900, # ms per keystroke; wall clock, machine is at warp [int]$Pad = 20, # spare RETURNs while a step is working [int]$Tail = 8, # spare RETURNs while the linker writes [int]$Settle = 0, # extra seconds to run after the last key (screen redraw) [switch]$Full, # ignore the stamps: fresh disk, recompile everything [switch]$Run ) $ErrorActionPreference = "Stop" $vice = "C:\Program Files\GTK3VICE-3.10-win64\bin" $x64 = Join-Path $vice "x64sc.exe" $c1541 = Join-Path $vice "c1541.exe" $here = $PSScriptRoot $base = Join-Path $here "..\reference\POWERc.d81" $shots = Join-Path $here "shots" # VICE splits "image:file" on a colon, so keep the image name bare and run from # this folder. Set-Location $here $img = "work.d81" $units = @{ vim = @("vim", "vscreen", "vview", "vedit", "vdir", "vml") mltest = @("mltest", "vml") } if (-not $units.ContainsKey($Prog)) { throw "unknown program '$Prog'" } $unitList = $units[$Prog] if (-not (Test-Path $x64)) { throw "x64sc.exe not found in $vice" } if (-not (Test-Path $base)) { throw "$base not found - is the repository complete?" } New-Item -ItemType Directory -Force -Path $shots | Out-Null # ---------------------------------------------------------------- petscii # Power C sources live on the disk as PETSCII SEQ files with CR line endings: # a-z -> $41-$5A, A-Z -> $C1-$DA, CRLF -> CR, and the four characters the manual # says are typed with shift/logo keys: { | } ~ -> $DB $DC $DD $DE. function ConvertTo-Petscii([string]$in, [string]$out) { $bytes = [IO.File]::ReadAllBytes($in) $start = 0 if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } $o = New-Object System.Collections.Generic.List[byte] for ($i = $start; $i -lt $bytes.Length; $i++) { $b = $bytes[$i] if ($b -eq 13) { continue } if ($b -eq 10) { $o.Add([byte]13); continue } if ($b -ge 0x61 -and $b -le 0x7A) { $o.Add([byte]($b - 0x20)); continue } if ($b -ge 0x41 -and $b -le 0x5A) { $o.Add([byte]($b + 0x80)); continue } if ($b -ge 0x7B -and $b -le 0x7F) { $o.Add([byte]($b + 0x60)); continue } $o.Add([byte]$b) } [IO.File]::WriteAllBytes($out, $o.ToArray()) return $o.Count } # ---------------------------------------------------------------- emulator # Quote one argument the way the Windows command line wants it. Start-Process's # -ArgumentList joins with spaces and does not quote anything, which silently # split "-keybuf cc vim.c\n" into two arguments, so the command line is built # here and handed to .NET as a single string. function Quote-Arg([string]$a) { if ($a -ne "" -and $a -notmatch '[\s"]') { return $a } $e = $a -replace '(\\*)"', '$1$1\"' $e = $e -replace '(\\+)$', '$1$1' return '"' + $e + '"' } # Exactly one emulator, waited for, so nothing else touches the image. function Invoke-C64 { param( [string]$KeyBuf, [string]$Shot, [string]$Target = "shell", [int]$Cycles = 0, [int]$KeyDelay = 900 ) if (Test-Path $Shot) { Remove-Item $Shot -Force } # VICE won't overwrite if ($Cycles -le 0) { $wall = ($KeyBuf.Length * $KeyDelay / 1000.0) + 15 + $Settle $c = [long]($wall * 13000000) if ($c -lt 150000000) { $c = 150000000 } if ($c -gt 2000000000) { $c = 2000000000 } $Cycles = [int]$c } $argv = @( "-8", $img, "-autostart", ($img + ":" + $Target), "-autostart-warp", "-warp", "-sounddev", "dummy", "-keybuf-delay", "$KeyDelay", "-keybuf", $KeyBuf, "-limitcycles", "$Cycles", "-exitscreenshot", $Shot ) $cmdline = ($argv | ForEach-Object { Quote-Arg $_ }) -join ' ' $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $x64 $psi.Arguments = $cmdline $psi.UseShellExecute = $false $pi = [System.Diagnostics.Process]::Start($psi) $pi.WaitForExit() Start-Sleep -Milliseconds 900 # let the image write settle } function Get-DiskDir { return (& $c1541 $img -list 2>$null) -join "`n" } # ---------------------------------------------------------------- raw shell if ($Shell -ne "") { if (-not (Test-Path $img)) { throw "$img not found - build something first" } Invoke-C64 -KeyBuf $Shell -Shot (Join-Path $shots "shell.png") -KeyDelay $Delay Write-Host "shots\shell.png" exit 0 } # ---------------------------------------------------------------- fresh disk # work.stamp remembers the hash of each .c as it was last compiled onto the work # disk. Only units whose hash differs are converted, copied over and recompiled; # the others keep the .o already on the disk. The linker then runs again. $stampFile = Join-Path $here "work.stamp" $stamps = @{} if (Test-Path $stampFile) { foreach ($ln in (Get-Content $stampFile)) { $p = $ln -split '=' if ($p.Count -eq 2) { $stamps[$p[0]] = $p[1] } } } if ($Full -or -not (Test-Path $img) -or -not (Test-Path $stampFile)) { Write-Host "== fresh work disk" Copy-Item $base $img -Force $stamps = @{} } else { Write-Host "== incremental build on $img" } $changed = @() foreach ($u in $unitList) { $src = Join-Path $here "$u.c" if (-not (Test-Path $src)) { throw "$src not found" } $hash = (Get-FileHash $src -Algorithm MD5).Hash if ($stamps[$u] -eq $hash) { continue } $changed += $u $pet = Join-Path $here "$u.pet" $n = ConvertTo-Petscii $src $pet Write-Host ("== {0}.c changed -> petscii ({1} bytes)" -f $u, $n) # cc and the linker refuse to overwrite, so scratch the stale copies first $null = & $c1541 $img -delete "$u.c" "$u.o" 2>$null $null = & $c1541 $img -write $pet "$u.c,s" 2>$null $stamps.Remove($u) # only re-stamped once its .o really exists } $haveProg = ((& $c1541 $img -list 2>$null) -join "`n") -match ('"' + [regex]::Escape($Prog) + '"\s+prg') $needLink = ($changed.Count -gt 0) -or (-not $haveProg) if ($needLink -and $haveProg) { $null = & $c1541 $img -delete $Prog 2>$null } if ($changed.Count -eq 0) { Write-Host "== no source changed" } # ---------------------------------------------------------------- one session # cc and RETURNs for its disk prompts, over and over, then the linker: # ">" object files, ">" up-arrow for the libraries, ">" empty to finish, the # output name. With -Run the session goes on to leave the shell with bye, load # the program from BASIC and type -Keys at whatever it puts on the screen. $kb = "" foreach ($u in $changed) { $kb += "cc $u.c\n" + ("\n" * 3) + ("\n" * $Pad) } if ($needLink) { $kb += "link -s\n" foreach ($u in $unitList) { $kb += "$u.o\n" } $kb += "^\n\n" + $Prog + "\n" + ("\n" * $Tail) } if ((-not $Run) -and ($kb -eq "")) { Write-Host "build ok - nothing to do" exit 0 } if ($Run) { $kb += "bye\n" + ("\n" * 6) # let the shell hand back to BASIC $kb += 'load"' + $Prog + '",8,1' + "\n" + ("\n" * 10) # let the load finish $kb += "run\n" $kb += $Keys } $shot = if ($Run) { Join-Path $shots "run.png" } else { Join-Path $shots "build.png" } Write-Host ("== one session: cc [{0}], {1}{2}" -f ($changed -join ' '), $(if ($needLink) { 'link' } else { 'no link' }), $(if ($Run) { ', bye, load, run' } else { '' })) Invoke-C64 -KeyBuf $kb -Shot $shot -KeyDelay $Delay $dir = Get-DiskDir $found = $false foreach ($line in ($dir -split "`n")) { if ($line -match ('"' + [regex]::Escape($Prog) + '"\s+prg')) { $found = $true } if ($line -match ('"(' + ($unitList -join '|') + ')\.[co]"')) { Write-Host " $line" } } # stamp a unit only once its object file is really on the disk foreach ($u in $changed) { if ($dir -match ('"' + [regex]::Escape($u) + '\.o"')) { $stamps[$u] = (Get-FileHash (Join-Path $here "$u.c") -Algorithm MD5).Hash } else { Write-Host " $u.c produced no object - read $shot" } } ($stamps.GetEnumerator() | ForEach-Object { "{0}={1}" -f $_.Key, $_.Value }) | Set-Content $stampFile if (-not $found) { throw "'$Prog' was not created - read $shot" } if ($Run) { Write-Host (" {0}" -f (Split-Path $shot -Leaf)) } else { Write-Host "build ok - 'build.ps1 -Run -Keys ...' to try it" }