SchOS/tools/loc.ps1
Nexoniarz cda5908660 Build and tools work on Linux, macOS and BSD as well as Windows
Developed from both Windows and Linux, so nothing in the build should
care which host it runs on. It did, in four places.

Toolchain: one path instead of two. clang is a cross-compiler by
construction, so a single LLVM install targets bare-metal ELF, 16/32-bit
code below 1 MB, and Linux/musl userland from any host -- which is how
the Windows build already worked. The GNU branch beside it had drifted
into passing --no-warn-rwx-segments, a hard error on lld, which is the
default linker on both FreeBSD and macOS; and it required a brewed
x86_64-elf-gcc before anything built on a Mac at all. GNU is kept as a
fallback (USE_GNU=1) but clang+lld is chosen whenever both are present.
Host tools use the host's own cc; only mingw needs naming explicitly.

tools/mkschfs.c included <sys/sysmacros.h> unconditionally on non-mingw
hosts. That header is glibc/musl only -- macOS and the BSDs have neither
the header nor a need for it, since major()/minor() come from
<sys/types.h> there. Now guarded on __linux__.

Ports verified tarballs with `sha256sum -c -`, which does not exist on
macOS (shasum), FreeBSD/NetBSD (sha256), or OpenBSD (cksum -a sha256),
and each spells its check mode differently. tools/sha256check.sh picks
whichever of the five the host has and compares digests itself.

nproc is coreutils, so absent on macOS and BSD; falls through to sysctl
-n hw.ncpu before defaulting to 4.

The PowerShell tools now run under pwsh on any OS: QEMU is found on PATH
(the scoop location is a Windows-only fallback), and path literals use
forward slashes -- Join-Path $root 'build\x' produces a *filename
containing a backslash* on a Unix host. The backslash sweep is limited
to known directory prefixes on purpose: bootcheck sends
printf 'caf\303\251' to the guest to test UTF-8, and a blanket
substitution would have quietly changed what that asserts.

Also fixes six tools left booting build/fs.img, which stopped being
bootable when SchFS became the root filesystem: crashshot, dispshot,
fbshot, splashshot, textshot and ttyshot all take build/schfs.img now,
and the -SchFS switches that used to select it are gone.

Verified on Windows: full build, and bootcheck green. The Linux, macOS
and BSD paths are written from their documented behaviour and are NOT
executed here -- this machine has no WSL and no second host.
2026-08-09 14:24:53 +02:00

200 lines
8.8 KiB
PowerShell

# How much of SchOS is actually SchOS.
#
# `wc -l` over the whole tree answers a question nobody asked: it counts musl,
# toybox, mksh and a NetBSD checkout, which between them are
# two orders of magnitude larger than this project and none of them ours. This
# counts only first-party code, and separates the code from the comments so the
# number means something.
#
# tools/loc.ps1 the summary table
# tools/loc.ps1 -PerFile every file, largest first
# tools/loc.ps1 -All include third-party trees, for comparison
#
# Comment detection is deliberately simple: it handles // and /* */ for C and
# # for scripts, and does not try to understand a // inside a string literal.
# That is worth being honest about -- the comment ratio is indicative, not
# exact. The line and byte counts are exact.
param(
[switch]$PerFile,
[switch]$All,
[int]$Top = 30
)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
# What counts as ours. Everything else in the tree is either somebody else's
# source or generated output, and mixing either in makes the total meaningless.
$areas = [ordered]@{
'kernel' = 'kernel'
'include' = 'include'
'boot' = 'boot'
'lib' = 'lib'
'cmd' = 'cmd'
'tools' = 'tools'
'docs' = 'docs'
'projects' = 'projects'
}
if ($All) {
$areas['ports (3rd party)'] = 'ports'
$areas['src-trunk (NetBSD)'] = 'src-trunk'
}
# Source only. Images, fonts-as-data and upstream tarballs are not code.
$exts = @('.c', '.h', '.S', '.s', '.ld', '.inc', '.ps1', '.sh', '.pl', '.py', '.md', '.mk')
function Measure-Area([string]$rel) {
$dir = Join-Path $root $rel
$res = [PSCustomObject]@{
Area = $rel; Files = 0; Lines = 0; Code = 0; Comment = 0; Blank = 0
Bytes = 0; NonWs = 0; Alpha = 0; Digit = 0; Files_ = @()
}
if (-not (Test-Path $dir)) { return $res }
Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
$f = $_
if ($exts -notcontains $f.Extension.ToLower()) { return }
# Makefiles have no extension; picked up separately below.
$text = [System.IO.File]::ReadAllText($f.FullName)
$lines = $text -split "`r?`n"
# A trailing newline produces one empty element; do not count it.
if ($lines.Length -gt 0 -and $lines[-1] -eq '') { $lines = $lines[0..($lines.Length-2)] }
$code = 0; $comment = 0; $blank = 0
$inBlock = $false
$isC = @('.c','.h','.s','.ld','.inc') -contains $f.Extension.ToLower()
foreach ($l in $lines) {
$t = $l.Trim()
if ($t -eq '') { $blank++; continue }
if ($inBlock) {
$comment++
if ($t -match '\*/') { $inBlock = $false }
continue
}
if ($isC) {
if ($t.StartsWith('//')) { $comment++; continue }
if ($t.StartsWith('/*')) {
$comment++
if ($t -notmatch '\*/') { $inBlock = $true }
continue
}
} elseif ($t.StartsWith('#') -and $f.Extension -ne '.md') {
$comment++; continue
}
$code++
}
$nonWs = ($text.ToCharArray() | Where-Object { -not [char]::IsWhiteSpace($_) }).Count
$alpha = ($text.ToCharArray() | Where-Object { [char]::IsLetter($_) }).Count
$digit = ($text.ToCharArray() | Where-Object { [char]::IsDigit($_) }).Count
$res.Files++
$res.Lines += $lines.Length
$res.Code += $code
$res.Comment += $comment
$res.Blank += $blank
$res.Bytes += $f.Length
$res.NonWs += $nonWs
$res.Alpha += $alpha
$res.Digit += $digit
$res.Files_ += [PSCustomObject]@{
Path = $f.FullName.Substring($root.Length + 1)
Lines = $lines.Length; Code = $code; Comment = $comment
}
}
return $res
}
$results = @()
foreach ($k in $areas.Keys) { $results += (Measure-Area $areas[$k]) }
# The top-level Makefile has no extension, so it is added by hand rather than
# widening the extension list and dragging in every stray file in the tree.
$mk = Join-Path $root 'Makefile'
if (Test-Path $mk) {
$mkLines = ([System.IO.File]::ReadAllText($mk) -split "`r?`n")
$r = $results | Where-Object { $_.Area -eq 'tools' } | Select-Object -First 1
if ($r) {
$r.Files++; $r.Lines += $mkLines.Length; $r.Bytes += (Get-Item $mk).Length
$r.Code += ($mkLines | Where-Object { $_.Trim() -ne '' -and -not $_.Trim().StartsWith('#') }).Count
$r.Comment += ($mkLines | Where-Object { $_.Trim().StartsWith('#') }).Count
$r.Blank += ($mkLines | Where-Object { $_.Trim() -eq '' }).Count
}
}
$fmt = @(
@{ N='Area'; E={$_.Area} }
@{ N='Files'; E={$_.Files}; A='right' }
@{ N='Lines'; E={'{0:N0}' -f $_.Lines}; A='right' }
@{ N='Code'; E={'{0:N0}' -f $_.Code}; A='right' }
@{ N='Comment'; E={'{0:N0}' -f $_.Comment}; A='right' }
@{ N='Blank'; E={'{0:N0}' -f $_.Blank}; A='right' }
@{ N='Cmt%'; E={ if ($_.Code + $_.Comment) { '{0:N0}%' -f (100 * $_.Comment / ($_.Code + $_.Comment)) } else { '-' } }; A='right' }
@{ N='Ln/File'; E={ if ($_.Files) { '{0:N0}' -f ($_.Lines / $_.Files) } else { '-' } }; A='right' }
@{ N='KiB'; E={'{0:N0}' -f ($_.Bytes / 1KB)}; A='right' }
)
Write-Host ''
Write-Host ' SchOS — first-party source' -ForegroundColor Cyan
Write-Host ''
Write-Host (($results | Where-Object { $_.Files -gt 0 } | Sort-Object Lines -Descending | Format-Table $fmt -AutoSize | Out-String))
# Two totals, because they answer different questions and conflating them
# flatters the number. "The OS" is what boots and runs on the machine; docs,
# host-side tools and the projects/ stubs are first-party but are not SchOS
# running on hardware. Linux 0.01's 10,239 lines were kernel, headers and boot
# -- no documentation and no build tooling -- so only the first total is a fair
# comparison.
$osAreas = @('kernel','include','boot','lib','cmd')
$own = $results | Where-Object { $_.Area -notin @('ports','src-trunk') }
$os = $results | Where-Object { $_.Area -in $osAreas }
$T = ($own | Measure-Object -Property Lines -Sum).Sum
$C = ($own | Measure-Object -Property Code -Sum).Sum
$M = ($own | Measure-Object -Property Comment -Sum).Sum
$B = ($own | Measure-Object -Property Blank -Sum).Sum
$F = ($own | Measure-Object -Property Files -Sum).Sum
$By = ($own | Measure-Object -Property Bytes -Sum).Sum
$NW = ($own | Measure-Object -Property NonWs -Sum).Sum
$AL = ($own | Measure-Object -Property Alpha -Sum).Sum
$DG = ($own | Measure-Object -Property Digit -Sum).Sum
Write-Host (' {0,-22} {1,12:N0}' -f 'Files', $F)
Write-Host (' {0,-22} {1,12:N0}' -f 'Lines total', $T)
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which code', $C, (100 * $C / [Math]::Max($T,1)))
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which comment', $M, (100 * $M / [Math]::Max($T,1)))
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which blank', $B, (100 * $B / [Math]::Max($T,1)))
Write-Host (' {0,-22} {1,12:N0}' -f 'Lines per file', ($T / [Math]::Max($F,1)))
Write-Host ''
$osT = ($os | Measure-Object -Property Lines -Sum).Sum
$osC = ($os | Measure-Object -Property Code -Sum).Sum
$osF = ($os | Measure-Object -Property Files -Sum).Sum
Write-Host (' {0,-22} {1,12:N0} ({2:N0} code, {3} files)' -f 'THE OS ITSELF', $osT, $osC, $osF) -ForegroundColor Green
Write-Host (' {0,-22}' -f ' kernel+include+boot+lib+cmd') -ForegroundColor DarkGray
Write-Host ''
Write-Host (' {0,-22} {1,12:N0}' -f 'Bytes', $By)
Write-Host (' {0,-22} {1,12:N0}' -f 'Non-whitespace chars', $NW)
Write-Host (' {0,-22} {1,12:N0}' -f ' letters', $AL)
Write-Host (' {0,-22} {1,12:N0}' -f ' digits', $DG)
Write-Host ''
# For scale. Linux 0.01 (September 1991) is 10,239 lines by the same crude
# measure -- every line of every file in the tarball, comments included.
$lin = 10239
Write-Host (' For scale: Linux 0.01 was {0:N0} lines (kernel, headers, boot -- the' -f $lin) -ForegroundColor DarkGray
Write-Host (' same things counted above as THE OS ITSELF). SchOS is {0:N1}x that.' -f ($osT / $lin)) -ForegroundColor DarkGray
Write-Host ''
if ($PerFile) {
Write-Host ' Largest files' -ForegroundColor Cyan
# Out-String, not a bare Format-Table: two Format-Table calls writing to
# the same output stream interleave their grouping records and PowerShell
# rejects the second one outright.
$rows = ($own | ForEach-Object { $_.Files_ }) |
Sort-Object Lines -Descending | Select-Object -First $Top |
Format-Table @{N='Lines';E={'{0:N0}' -f $_.Lines};A='right'},
@{N='Code'; E={'{0:N0}' -f $_.Code};A='right'},
@{N='Cmt'; E={'{0:N0}' -f $_.Comment};A='right'},
Path -AutoSize | Out-String
Write-Host $rows
}