tools: group scripts by what runs them

tools/windows/   .ps1   (pwsh, so usable from Linux and macOS too)
  tools/posix/     .sh
  tools/python/    .py
  tools/           .c and .pl

The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.

Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.

Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:

  - schfs_fsck.c did not compile under mingw at all: --extract uses
    POSIX mkdir(path,mode), symlink() and link(), none of which mingw
    has. That took the whole tool with it, and `make schfs-check` with
    it. mkdir maps to _mkdir; symlink and link fail loudly rather than
    writing a regular file in their place, because an extracted tree
    that differs from the image while reporting success defeats the
    point of --extract.
  - the round-trip test then failed at `ln -s`, since Git Bash cannot
    create symlinks without developer mode. It skips with a message now.
    Not a fudge: mkschfs's mingw build cannot see a symlink either, so
    the test would have compared a tree with none against an image with
    none and called it a pass -- coverage reporting itself as present
    while being absent.

Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.
This commit is contained in:
Nexoniarz 2026-08-09 14:38:02 +02:00
commit 754d96ebc5
26 changed files with 127 additions and 39 deletions

201
tools/windows/loc.ps1 Normal file
View file

@ -0,0 +1,201 @@
# 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'
# Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -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
}