Linux only: the PowerShell scripts from the boilerplate go

The eight tools/*.ps1 scripts came in with 4df5c68 from
ps5-native-app-boilerplate and nothing in the build, the gates or the
docs runs them: the project builds and deploys from Linux only.
tools/prepare-assets.sh only wrapped prepare-assets.ps1, so it goes
too. .gitattributes loses the CRLF rule for *.ps1 and tools/lint.sh
no longer lists .ps1 among the files whose headers it checks.

Evidence: git grep finds no reference to any removed script;
tools/verify.sh format PASS; tools/verify.sh unit PASS (283 s,
klog/unit-gate.log).
This commit is contained in:
Mihawk committed 2026-10-17 19:57:40 -04:00
1 parent 247336399d
commit 8f14452e5d
11 files changed
+1 -1108

No files matched your search

-1
View File
@@ -14,7 +14,6 @@ Makefile text eol=lf
.clang-format text eol=lf
.clang-tidy text eol=lf
tooling/prospero-clang18 text eol=lf
*.ps1 text eol=crlf
*.png binary
*.dds binary
*.at9 binary
-56
View File
@@ -1,56 +0,0 @@
<#
ps5-native-app-boilerplate - Source attribution header audit.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Verifies attribution in every tracked, comment-capable code and tooling file.
#>
#requires -Version 5.1
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$patterns = @("*.c", "*.cc", "*.cpp", "*.h", "*.hpp", "*.ld", "*.py", "*.ps1", "*.yml", "*.yaml", "*.sh")
$files = @(& git -C $root ls-files --cached --others --exclude-standard -- @patterns |
Where-Object { Test-Path -LiteralPath (Join-Path $root $_) -PathType Leaf })
$files += @(
".editorconfig",
".clang-format",
".clang-tidy",
".gitattributes",
".gitignore",
".env.example",
"Makefile",
"tooling/prospero-clang18",
"tooling/native/runtime/api-surface.txt",
"tooling/native/runtime/imports.txt"
)
$files = @($files | Sort-Object -Unique)
$markers = @(
"ps5-native-app-boilerplate",
"Copyright (C) 2026 BlackBearReloaded",
"SPDX-License-Identifier: GPL-3.0-or-later"
)
$missing = @()
foreach ($file in $files) {
$path = Join-Path $root $file
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
$missing += "$file (missing file)"
continue
}
$header = (Get-Content -LiteralPath $path -TotalCount 20) -join "`n"
foreach ($marker in $markers) {
if (-not $header.Contains($marker)) {
$missing += "$file (missing '$marker')"
}
}
}
if ($missing.Count -ne 0) {
$missing | ForEach-Object { Write-Error $_ }
throw "One or more source attribution headers are incomplete."
}
Write-Host "Verified attribution headers in $($files.Count) tracked code and tooling files."
-34
View File
@@ -1,34 +0,0 @@
<#
ps5-native-app-boilerplate - Windows prerequisite-check frontend.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Runs the canonical read-only Linux/WSL doctor so both host entry points report
the same required and optional tools.
#>
#requires -Version 5.1
$ErrorActionPreference = "Stop"
function Fail([string]$Message) {
throw "ps5-native-app-boilerplate: $Message"
}
function Convert-ToWslPath([string]$Path) {
$absolute = [IO.Path]::GetFullPath($Path)
if ($absolute -notmatch '^([A-Za-z]):\\(.*)$') {
Fail "The repository must be on a Windows drive visible to WSL."
}
return "/mnt/$($Matches[1].ToLowerInvariant())/$($Matches[2].Replace('\', '/'))"
}
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
Fail "WSL was not found. Install WSL and a Linux distribution first."
}
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$doctor = Convert-ToWslPath (Join-Path $root "tools/doctor.sh")
& wsl.exe --exec bash $doctor
if ($LASTEXITCODE -ne 0) {
Fail "One or more Linux/WSL prerequisites are missing."
}
-157
View File
@@ -1,157 +0,0 @@
<#
ps5-native-app-boilerplate - Static ELF/FSELF validator.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Checks the loader-visible structure of a generated native application.
#>
#requires -Version 5.1
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$File
)
$ErrorActionPreference = "Stop"
$path = (Resolve-Path -LiteralPath $File).Path
$data = [IO.File]::ReadAllBytes($path)
function U16([int]$offset) { [BitConverter]::ToUInt16($data, $offset) }
function U32([int]$offset) { [BitConverter]::ToUInt32($data, $offset) }
function U64([int]$offset) { [BitConverter]::ToUInt64($data, $offset) }
function Hex([uint64]$value) { "0x{0:x}" -f $value }
function Align16([uint64]$value) { ($value + 15) -band ([uint64]::MaxValue - 15) }
$container = $false
$elf = 0
$fselfCompatible = $null
$authInfoEmbedded = $null
if ($data.Length -ge 0x20 -and (U32 0) -in @([uint32]0x1D3D154F, [uint32]4009038932)) {
$container = $true
$entries = U16 0x18
$headerSize = U16 0x0c
$metaSize = U16 0x0e
$elf = 0x20 + 0x20 * $entries
if ($elf + 0x40 -gt $data.Length -or (U32 $elf) -ne 0x464c457f) {
throw "FSELF does not contain an ELF header at the segment-table boundary."
}
$phoff = U64 ($elf + 0x20)
$phnum = U16 ($elf + 0x38)
$ext = Align16 ($elf + $phoff + 0x38 * $phnum)
$extMarker = if ($ext + 16 -le $data.Length) { U64 ([int]$ext + 8) } else { 0 }
$fselfCompatible = $extMarker -eq 1
# This is where current kstuff-lite looks for an optional 0x88-byte auth record.
$authOffset = $ext + 0x40 + 0x30 + 0x50 * $entries + 0x50
$authInfoEmbedded = $authOffset + 8 -le $data.Length -and (U64 ([int]$authOffset)) -eq 0x88
[pscustomobject]@{
Layer = "FSELF"
SegmentEntries = $entries
HeaderSize = Hex $headerSize
MetaSize = Hex $metaSize
ElfOffset = Hex $elf
ExtendedInfoOffset = Hex $ext
ExtendedInfoMarker = Hex $extMarker
KstuffRecognized = $fselfCompatible
EmbeddedAuthInfo = $authInfoEmbedded
} | Format-List
}
if ($elf + 0x40 -gt $data.Length -or (U32 $elf) -ne 0x464c457f) {
throw "Input is neither a raw ELF nor a supported FSELF."
}
$osAbi = $data[$elf + 7]
$abiVersion = $data[$elf + 8]
$type = U16 ($elf + 0x10)
$entry = U64 ($elf + 0x18)
$phoff = U64 ($elf + 0x20)
$phentsize = U16 ($elf + 0x36)
$phnum = U16 ($elf + 0x38)
$types = @()
$mappedLoads = @()
$procParam = $null
$linkingLoad = $null
$programHeaders = @()
for ($i = 0; $i -lt $phnum; $i++) {
$p = $elf + [int]$phoff + $i * $phentsize
$pt = U32 $p
$flags = U32 ($p + 4)
$offset = U64 ($p + 8)
$vaddr = U64 ($p + 16)
$filesz = U64 ($p + 32)
$memsz = U64 ($p + 40)
$align = U64 ($p + 48)
$programHeaders += [pscustomobject]@{
Index=$i; Type=$pt; Flags=$flags; Offset=$offset; Address=$vaddr
FileSize=$filesz; MemorySize=$memsz; Align=$align
}
$types += $pt
if ($pt -eq 1) {
if ($flags -eq 0) { $linkingLoad = $i }
else { $mappedLoads += [pscustomobject]@{ Index=$i; Flags=$flags; Offset=$offset; Address=$vaddr; FileSize=$filesz; MemorySize=$memsz; Align=$align } }
}
if ($pt -eq 0x61000001) { $procParam = $offset }
}
$errors = [Collections.Generic.List[string]]::new()
if ($osAbi -ne 9) { $errors.Add("OS/ABI is not FreeBSD (9).") }
if ($abiVersion -ne 2) { $errors.Add("ABI version is not 2.") }
if ($type -ne 0xfe10) { $errors.Add("ELF type is not ET_SCE_EXEC_ASLR (0xfe10).") }
if ($phentsize -ne 0x38) { $errors.Add("Program-header size is not 0x38.") }
if ($null -eq $procParam) { $errors.Add("PT_SCE_PROCPARAM is missing.") }
if ($null -eq $linkingLoad) { $errors.Add("The flags-zero linking LOAD is missing.") }
foreach ($load in $mappedLoads) {
if ($load.Align -ne 0x4000) { $errors.Add("Mapped LOAD $($load.Index) is not 0x4000 aligned.") }
if ($load.MemorySize -lt $load.FileSize) { $errors.Add("LOAD $($load.Index) has memsz smaller than filesz.") }
if ($load.Flags -notin @(1, 4, 6)) { $errors.Add("LOAD $($load.Index) has unsupported flags $($load.Flags).") }
}
foreach ($header in $programHeaders) {
if ($header.Type -eq 0x6fffff00) {
if ($header.MemorySize -ne 0) {
$errors.Add("Comment header $($header.Index) must have zero memsz.")
}
if ($header.Align -ne 0x10) {
$errors.Add("Comment header $($header.Index) must use 0x10 alignment.")
}
}
if ($header.Type -eq 4 -and $header.Address -eq 0 -and
$header.MemorySize -ne 0) {
$errors.Add("Unmapped NOTE header $($header.Index) must have zero memsz.")
}
}
$companion = $null
$sdk = $null
$procMagic = $null
if (-not $container -and $null -ne $procParam -and $elf + $procParam + 0x60 -le $data.Length) {
$procMagic = [Text.Encoding]::ASCII.GetString($data, $elf + [int]$procParam + 8, 4)
$companion = U32 ($elf + [int]$procParam + 0x10)
$sdk = U32 ($elf + [int]$procParam + 0x14)
if ($procMagic -ne "ORBI") { $errors.Add("Process-parameter magic is not ORBI.") }
}
[pscustomobject]@{
Layer = "ELF"
Container = $container
OsAbi = $osAbi
AbiVersion = $abiVersion
Type = Hex $type
Entry = Hex $entry
ProgramHeaders = $phnum
MappedLoads = $mappedLoads.Count
LinkingLoadIndex = $linkingLoad
ProcessParamMagic = $procMagic
CompanionVersion = if ($null -ne $companion) { Hex $companion } else { $null }
SdkVersion = if ($null -ne $sdk) { Hex $sdk } else { $null }
StaticErrors = $errors.Count
} | Format-List
if ($errors.Count) {
$errors | ForEach-Object { Write-Error $_ }
exit 1
}
+1 -1
View File
@@ -18,7 +18,7 @@ checked=0
for file in "${repository_files[@]}"; do
[[ -f $file ]] || continue
case "$file" in
*.c|*.cc|*.cpp|*.h|*.hpp|*.ld|*.py|*.ps1|*.sh|*.yml|*.yaml|Makefile|.clang-format|.clang-tidy|.env.example)
*.c|*.cc|*.cpp|*.h|*.hpp|*.ld|*.py|*.sh|*.yml|*.yaml|Makefile|.clang-format|.clang-tidy|.env.example)
header=$(head -n 20 "$file")
grep -Fq ps5-native-app-boilerplate <<<"$header"
grep -Fq 'Copyright (C) 2026 BlackBearReloaded' <<<"$header"
-375
View File
@@ -1,375 +0,0 @@
<#
ps5-native-app-boilerplate - Presentation asset preparation.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Converts developer-owned artwork and audio into the supported launcher
asset profile, or validates the current sce_sys presentation files.
#>
#requires -Version 5.1
[CmdletBinding()]
param(
[switch]$ValidateOnly,
[string]$Icon = "",
[string]$Background = "",
[string]$SelectionBackground = "",
[string]$LaunchBackground = "",
[string]$Audio = "",
[string]$Texconv = "",
[string]$Ffmpeg = "",
[string]$At9Tool = "",
[ValidateRange(0, 86400)]
[double]$AudioStart = 0,
[ValidateRange(0.1, 87.3)]
[double]$AudioDuration = 15,
[string]$OutputDirectory = ""
)
$ErrorActionPreference = "Stop"
$repo = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
if (-not $OutputDirectory) {
$OutputDirectory = Join-Path $repo "sce_sys"
}
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
function Fail([string]$Message) {
throw "prepare-assets: $Message"
}
function Read-U16([byte[]]$Bytes, [int]$Offset) {
if ($Offset -lt 0 -or $Offset + 2 -gt $Bytes.Length) {
Fail "Unexpected end of file."
}
return [BitConverter]::ToUInt16($Bytes, $Offset)
}
function Read-U32([byte[]]$Bytes, [int]$Offset) {
if ($Offset -lt 0 -or $Offset + 4 -gt $Bytes.Length) {
Fail "Unexpected end of file."
}
return [BitConverter]::ToUInt32($Bytes, $Offset)
}
function Read-U32BigEndian([byte[]]$Bytes, [int]$Offset) {
if ($Offset -lt 0 -or $Offset + 4 -gt $Bytes.Length) {
Fail "Unexpected end of file."
}
return ([uint32]$Bytes[$Offset] -shl 24) -bor
([uint32]$Bytes[$Offset + 1] -shl 16) -bor
([uint32]$Bytes[$Offset + 2] -shl 8) -bor
[uint32]$Bytes[$Offset + 3]
}
function Read-Ascii([byte[]]$Bytes, [int]$Offset, [int]$Count) {
if ($Offset -lt 0 -or $Offset + $Count -gt $Bytes.Length) {
Fail "Unexpected end of file."
}
return [Text.Encoding]::ASCII.GetString($Bytes, $Offset, $Count)
}
function Resolve-Input([string]$Path, [string]$Label) {
if (-not $Path -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {
Fail "$Label file not found: $Path"
}
return (Resolve-Path -LiteralPath $Path).Path
}
function Resolve-Executable([string]$Path, [string[]]$Names, [string]$Help) {
if ($Path) {
if (Test-Path -LiteralPath $Path -PathType Leaf) {
return (Resolve-Path -LiteralPath $Path).Path
}
$command = Get-Command $Path -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
Fail "Executable not found: $Path"
}
foreach ($name in $Names) {
$command = Get-Command $name -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
}
Fail $Help
}
function Invoke-Checked([string]$Executable, [string[]]$Arguments, [string]$Label) {
& $Executable @Arguments
if ($LASTEXITCODE -ne 0) {
Fail "$Label failed with exit code $LASTEXITCODE."
}
}
function Convert-BackgroundAsset(
[string]$Source,
[string]$Label,
[string]$Stem,
[string]$WorkDirectory,
[string]$Destination,
[string]$TexconvPath
) {
$resolvedSource = Resolve-Input $Source $Label
$assetWork = Join-Path $WorkDirectory $Stem
New-Item -ItemType Directory -Path $assetWork | Out-Null
Invoke-Checked $TexconvPath @("-nologo", "-y", "-w", "3840", "-h", "2160", "-m", "1",
"-f", "R8G8B8A8_UNORM", "-ft", "png", "-o", $assetWork, $resolvedSource) `
"$Label preview conversion"
$preview = Get-ChildItem -LiteralPath $assetWork -File |
Where-Object { $_.Extension -ieq ".png" } | Select-Object -First 1
if (-not $preview) {
Fail "texconv did not produce the $Label preview."
}
Invoke-Checked $TexconvPath @("-nologo", "-y", "-w", "3840", "-h", "2160", "-m", "1",
"-f", "BC7_UNORM", "-dx10", "-o", $assetWork, $preview.FullName) "$Label DDS conversion"
$dds = Get-ChildItem -LiteralPath $assetWork -File |
Where-Object { $_.Extension -ieq ".dds" } | Select-Object -First 1
if (-not $dds) {
Fail "texconv did not produce the $Label BC7 DDS."
}
[IO.File]::Copy($dds.FullName, (Join-Path $Destination "$Stem.dds"), $true)
return [PSCustomObject]@{ Preview = $preview.FullName; Dds = $dds.FullName }
}
function Test-Png([string]$Path, [int]$ExpectedWidth, [int]$ExpectedHeight) {
$bytes = [IO.File]::ReadAllBytes($Path)
$signature = [byte[]](137, 80, 78, 71, 13, 10, 26, 10)
if ($bytes.Length -lt 24) {
Fail "$Path is too small to be a PNG."
}
for ($i = 0; $i -lt $signature.Length; $i++) {
if ($bytes[$i] -ne $signature[$i]) {
Fail "$Path is not a PNG file."
}
}
$width = Read-U32BigEndian $bytes 16
$height = Read-U32BigEndian $bytes 20
if ($width -ne $ExpectedWidth -or $height -ne $ExpectedHeight) {
Fail "$Path must be ${ExpectedWidth}x${ExpectedHeight}; found ${width}x${height}."
}
}
function Test-Dds([string]$Path) {
$bytes = [IO.File]::ReadAllBytes($Path)
if ($bytes.Length -lt 148 -or (Read-Ascii $bytes 0 4) -ne "DDS ") {
Fail "$Path is not a DDS file."
}
$headerSize = Read-U32 $bytes 4
$height = Read-U32 $bytes 12
$width = Read-U32 $bytes 16
$mipCount = Read-U32 $bytes 28
$fourCc = Read-Ascii $bytes 84 4
$format = Read-U32 $bytes 128
$dimension = Read-U32 $bytes 132
$arraySize = Read-U32 $bytes 140
$expectedSize = [long]148 + ([long]$width / 4) * ([long]$height / 4) * 16
if ($headerSize -ne 124 -or $width -ne 3840 -or $height -ne 2160 -or
$mipCount -ne 1 -or $fourCc -ne "DX10" -or $format -ne 98 -or
$dimension -ne 3 -or $arraySize -ne 1 -or $bytes.Length -ne $expectedSize) {
Fail "$Path must be a single 3840x2160 BC7_UNORM (98) DX10 2D DDS without mipmaps."
}
}
function Test-At9([string]$Path) {
$bytes = [IO.File]::ReadAllBytes($Path)
if ($bytes.Length -lt 128 -or (Read-Ascii $bytes 0 4) -ne "RIFF" -or
(Read-Ascii $bytes 8 4) -ne "WAVE" -or (Read-U32 $bytes 4) + 8 -ne $bytes.Length) {
Fail "$Path is not a complete RIFF/WAVE file."
}
$chunks = @{}
$position = 12
while ($position + 8 -le $bytes.Length) {
$id = Read-Ascii $bytes $position 4
$size = [int](Read-U32 $bytes ($position + 4))
$payload = $position + 8
if ($size -lt 0 -or $payload + $size -gt $bytes.Length) {
Fail "$Path contains a truncated RIFF chunk."
}
$chunks[$id] = @($payload, $size)
$position = $payload + $size + ($size % 2)
}
foreach ($required in @("fmt ", "fact", "smpl", "data")) {
if (-not $chunks.ContainsKey($required)) {
Fail "$Path is missing the required RIFF '$required' chunk."
}
}
$fmt = [int]$chunks["fmt "][0]
$smpl = [int]$chunks["smpl"][0]
if ([int]$chunks["fmt "][1] -lt 40 -or [int]$chunks["smpl"][1] -lt 32) {
Fail "$Path contains undersized ATRAC9 format or loop metadata."
}
$formatGuid = "D242E147BA368D4D88FC61654F8C836C"
$actualGuid = -join ($bytes[($fmt + 24)..($fmt + 39)] | ForEach-Object { $_.ToString("X2") })
$channels = Read-U16 $bytes ($fmt + 2)
$byteRate = Read-U32 $bytes ($fmt + 8)
$loops = Read-U32 $bytes ($smpl + 28)
if ((Read-U16 $bytes $fmt) -ne 0xFFFE -or $channels -notin @(1, 2) -or
(Read-U32 $bytes ($fmt + 4)) -ne 48000 -or $byteRate -le 0 -or
$byteRate -gt ($channels * 12000) -or $actualGuid -ne $formatGuid -or
$loops -lt 1 -or $bytes.Length -gt 2097152) {
Fail "$Path must be looped 48 kHz ATRAC9 at no more than 96 kb/s mono or 192 kb/s stereo, and no larger than 2 MiB."
}
}
function Test-AssetSet([string]$Directory) {
$iconPath = Join-Path $Directory "icon0.png"
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
Fail "Required launcher icon not found: $iconPath"
}
Test-Png $iconPath 512 512
$pic0 = Join-Path $Directory "pic0.dds"
$pic1 = Join-Path $Directory "pic1.dds"
$hasPic0 = Test-Path -LiteralPath $pic0 -PathType Leaf
$hasPic1 = Test-Path -LiteralPath $pic1 -PathType Leaf
if ($hasPic0 -xor $hasPic1) {
Fail "Supply both pic0.dds and pic1.dds, or neither."
}
if ($hasPic0) {
Test-Dds $pic0
Test-Dds $pic1
}
$sound = Join-Path $Directory "snd0.at9"
if (Test-Path -LiteralPath $sound -PathType Leaf) {
Test-At9 $sound
}
Write-Host "Presentation assets validated: $Directory"
}
if ($ValidateOnly) {
Test-AssetSet $OutputDirectory
return
}
if ($Background -and ($SelectionBackground -or $LaunchBackground)) {
Fail "Use -Background by itself, or use -SelectionBackground and -LaunchBackground for distinct artwork."
}
if (-not $Icon -and -not $Background -and -not $SelectionBackground -and
-not $LaunchBackground -and -not $Audio) {
Fail "Supply an icon, background, or audio input, or use -ValidateOnly."
}
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
Fail "Conversion uses Windows texconv and an optional Windows ATRAC9 encoder; run this command from Windows PowerShell."
}
$root = [IO.Path]::GetPathRoot($OutputDirectory)
if ($OutputDirectory.TrimEnd('\', '/') -eq $root.TrimEnd('\', '/')) {
Fail "Refusing to use a filesystem root as the output directory."
}
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
$work = Join-Path ([IO.Path]::GetTempPath()) ("ps5-native-assets-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $work | Out-Null
try {
if ($Icon -or $Background -or $SelectionBackground -or $LaunchBackground) {
$texconvPath = Resolve-Executable $Texconv @("texconv.exe", "texconv") `
"texconv was not found. Install it with: winget install Microsoft.DirectXTex.Texconv"
}
if ($Icon) {
$iconSource = Resolve-Input $Icon "Icon"
$iconWork = Join-Path $work "icon"
New-Item -ItemType Directory -Path $iconWork | Out-Null
Invoke-Checked $texconvPath @("-nologo", "-y", "-w", "512", "-h", "512", "-m", "1",
"-f", "R8G8B8A8_UNORM", "-ft", "png", "-o", $iconWork, $iconSource) "Icon conversion"
$converted = Get-ChildItem -LiteralPath $iconWork -File |
Where-Object { $_.Extension -ieq ".png" } | Select-Object -First 1
if (-not $converted) {
Fail "texconv did not produce the launcher PNG."
}
[IO.File]::Copy($converted.FullName, (Join-Path $OutputDirectory "icon0.png"), $true)
}
if ($Background) {
$convertedBackground = Convert-BackgroundAsset $Background "Background" "pic0" $work `
$OutputDirectory $texconvPath
[IO.File]::Copy($convertedBackground.Dds, (Join-Path $OutputDirectory "pic1.dds"), $true)
[IO.File]::Copy($convertedBackground.Preview,
(Join-Path $OutputDirectory "background-source.png"), $true)
[IO.File]::Copy($convertedBackground.Preview,
(Join-Path $OutputDirectory "launch-background-source.png"), $true)
} else {
if ($SelectionBackground) {
$convertedSelection = Convert-BackgroundAsset $SelectionBackground `
"Selection background" "pic0" $work $OutputDirectory $texconvPath
[IO.File]::Copy($convertedSelection.Preview,
(Join-Path $OutputDirectory "background-source.png"), $true)
}
if ($LaunchBackground) {
$convertedLaunch = Convert-BackgroundAsset $LaunchBackground `
"Launch background" "pic1" $work $OutputDirectory $texconvPath
[IO.File]::Copy($convertedLaunch.Preview,
(Join-Path $OutputDirectory "launch-background-source.png"), $true)
}
}
if ($Audio) {
$audioSource = Resolve-Input $Audio "Audio"
$soundOutput = Join-Path $OutputDirectory "snd0.at9"
if ([IO.Path]::GetExtension($audioSource) -ieq ".at9") {
if ($audioSource -ne $soundOutput) {
[IO.File]::Copy($audioSource, $soundOutput, $true)
}
} else {
$at9ToolPath = Resolve-Executable $At9Tool @("ps4_at9tool.exe") `
"A compatible ATRAC9 encoder was not found. Supply your legally obtained ps4_at9tool.exe with -At9Tool."
$wav = Join-Path $work "selection.wav"
$startText = $AudioStart.ToString([Globalization.CultureInfo]::InvariantCulture)
$durationText = $AudioDuration.ToString([Globalization.CultureInfo]::InvariantCulture)
$ffmpegArgs = @("-hide_banner", "-loglevel", "error", "-y", "-ss", $startText, "-i", $audioSource,
"-t", $durationText, "-map_metadata", "-1", "-af", "loudnorm=I=-28:LRA=11:TP=-2",
"-ar", "48000", "-ac", "2", "-c:a", "pcm_s16le", $wav)
$ffmpegCommand = if ($Ffmpeg) {
Resolve-Executable $Ffmpeg @() "FFmpeg was not found."
} else {
$command = Get-Command ffmpeg.exe, ffmpeg -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) { $command.Source } else { "" }
}
if ($ffmpegCommand) {
Invoke-Checked $ffmpegCommand $ffmpegArgs "Audio preparation"
} elseif (Get-Command wsl.exe -ErrorAction SilentlyContinue) {
& wsl.exe --exec sh -lc "command -v ffmpeg >/dev/null 2>&1"
if ($LASTEXITCODE -ne 0) {
Fail "FFmpeg was not found on Windows or in WSL. Install it in WSL with: sudo apt install ffmpeg"
}
function To-WslPath([string]$Path) {
if ($Path -notmatch '^([A-Za-z]):\\(.*)$') {
Fail "WSL FFmpeg requires source files on a Windows drive: $Path"
}
return "/mnt/$($Matches[1].ToLowerInvariant())/$($Matches[2].Replace('\', '/'))"
}
$wslArgs = @($ffmpegArgs)
$wslArgs[7] = To-WslPath $audioSource
$wslArgs[$wslArgs.Count - 1] = To-WslPath $wav
Invoke-Checked "wsl.exe" (@("--exec", "ffmpeg") + $wslArgs) "WSL audio preparation"
} else {
Fail "FFmpeg was not found. Install it or pass its executable with -Ffmpeg."
}
$encoded = Join-Path $work "snd0.at9"
Invoke-Checked $at9ToolPath @("-e", "-br", "192", "-wholeloop", $wav, $encoded) `
"ATRAC9 encoding"
[IO.File]::Copy($encoded, $soundOutput, $true)
}
$paramPath = Join-Path $OutputDirectory "param.json"
if (Test-Path -LiteralPath $paramPath -PathType Leaf) {
$param = Get-Content -LiteralPath $paramPath -Raw | ConvertFrom-Json
$param.pubtools | Add-Member -NotePropertyName loudnessSnd0 -NotePropertyValue "-28.00" -Force
$json = $param | ConvertTo-Json -Depth 16
[IO.File]::WriteAllText($paramPath, $json + [Environment]::NewLine,
[Text.UTF8Encoding]::new($false))
}
}
Test-AssetSet $OutputDirectory
} finally {
$resolvedWork = [IO.Path]::GetFullPath($work)
$resolvedTemp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
if ($resolvedWork.StartsWith($resolvedTemp, [StringComparison]::OrdinalIgnoreCase) -and
(Test-Path -LiteralPath $resolvedWork -PathType Container)) {
Remove-Item -LiteralPath $resolvedWork -Recurse -Force
}
}
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/env bash
# ps5-native-app-boilerplate - Bash presentation asset preparation frontend.
# Copyright (C) 2026 BlackBearReloaded
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Provides Linux/WSL-style arguments for the Windows DirectXTex and ATRAC9
# conversion workflow while keeping validation available on native Linux.
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
output_directory="$root/sce_sys"
validate_only=0
conversion_inputs=0
declare -a forwarded=()
usage() {
cat <<'EOF'
usage: tools/prepare-assets.sh [options]
--icon PATH Prepare sce_sys/icon0.png
--background PATH Use one image for pic0.dds and pic1.dds
--selection-background PATH Prepare pic0.dds from distinct artwork
--launch-background PATH Prepare pic1.dds from distinct artwork
--audio PATH Copy ready AT9 or prepare supported source audio
--audio-start SECONDS Start of the audio excerpt (default: 0)
--audio-duration SECONDS Excerpt duration (default: 15; maximum: 87.3)
--texconv COMMAND texconv.exe name or path
--ffmpeg COMMAND Windows FFmpeg name or path
--at9-tool COMMAND Legally obtained compatible ATRAC9 encoder
--output-directory PATH Destination (default: sce_sys)
--validate-only Validate current files on Linux or WSL
-h, --help Show this help
Conversion runs through Windows PowerShell and therefore requires WSL or a
Windows Bash environment. Validation is portable and requires only Python 3.
EOF
}
need_value() {
[[ $# -ge 2 && -n $2 ]] || {
echo "missing value for $1" >&2
exit 2
}
}
while (($#)); do
case "$1" in
--icon|--background|--selection-background|--launch-background|--audio)
need_value "$@"
forwarded+=("$1" "$2")
((conversion_inputs += 1))
shift 2
;;
--texconv|--ffmpeg|--at9-tool|--audio-start|--audio-duration)
need_value "$@"
forwarded+=("$1" "$2")
shift 2
;;
--output-directory)
need_value "$@"
output_directory=$2
shift 2
;;
--validate-only)
validate_only=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if ((validate_only)); then
((${#forwarded[@]} == 0)) || {
echo "--validate-only cannot be combined with conversion options" >&2
exit 2
}
exec bash "$root/tools/validate-assets.sh" "$output_directory"
fi
((conversion_inputs > 0)) || {
echo "supply an icon, background, or audio input, or use --validate-only" >&2
exit 2
}
if command -v powershell.exe >/dev/null 2>&1; then
powershell_command=$(command -v powershell.exe)
elif command -v powershell >/dev/null 2>&1; then
powershell_command=$(command -v powershell)
else
echo "conversion requires Windows PowerShell through WSL or Windows Bash" >&2
exit 2
fi
to_windows_path() {
local path=$1
case "$path" in
[A-Za-z]:\\*) printf '%s\n' "$path"; return ;;
esac
if command -v wslpath >/dev/null 2>&1; then
wslpath -w -- "$(realpath -m -- "$path")"
elif command -v cygpath >/dev/null 2>&1; then
cygpath -w -- "$path"
else
echo "cannot translate path for Windows: $path" >&2
exit 2
fi
}
to_windows_command() {
local command=$1
if [[ -e $command || $command == */* ]]; then
to_windows_path "$command"
else
printf '%s\n' "$command"
fi
}
script_path=$(to_windows_path "$root/tools/prepare-assets.ps1")
declare -a powershell_args=(-NoProfile -ExecutionPolicy Bypass -File "$script_path")
for ((index = 0; index < ${#forwarded[@]}; index += 2)); do
option=${forwarded[index]}
value=${forwarded[index + 1]}
case "$option" in
--icon) powershell_args+=(-Icon "$(to_windows_path "$value")") ;;
--background) powershell_args+=(-Background "$(to_windows_path "$value")") ;;
--selection-background) powershell_args+=(-SelectionBackground "$(to_windows_path "$value")") ;;
--launch-background) powershell_args+=(-LaunchBackground "$(to_windows_path "$value")") ;;
--audio) powershell_args+=(-Audio "$(to_windows_path "$value")") ;;
--texconv) powershell_args+=(-Texconv "$(to_windows_command "$value")") ;;
--ffmpeg) powershell_args+=(-Ffmpeg "$(to_windows_command "$value")") ;;
--at9-tool) powershell_args+=(-At9Tool "$(to_windows_command "$value")") ;;
--audio-start) powershell_args+=(-AudioStart "$value") ;;
--audio-duration) powershell_args+=(-AudioDuration "$value") ;;
esac
done
powershell_args+=(-OutputDirectory "$(to_windows_path "$output_directory")")
"$powershell_command" "${powershell_args[@]}"
-126
View File
@@ -1,126 +0,0 @@
<#
ps5-native-app-boilerplate - Clean-room runtime-shim reproducer.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Rebuilds twice with the native C++ tools and installs the shim only after
deterministic hash and attribution checks pass.
#>
#requires -Version 5.1
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$native = Join-Path $root "tooling/native"
$setupNativeDependencies = Join-Path $root "tools/setup-native-dependencies.ps1"
$apiManifest = Join-Path $root "tooling/native/runtime/api-surface.txt"
$importManifest = Join-Path $root "tooling/native/runtime/imports.txt"
$work = Join-Path $root "build/runtime-shim"
$rawA = Join-Path $work "libc-a.raw.elf"
$rawB = Join-Path $work "libc-b.raw.elf"
$signedA = Join-Path $work "libc-a.prx"
$signedB = Join-Path $work "libc-b.prx"
$builder = Join-Path $work "libc-builder"
$tool = Join-Path $work "ps5-native-tool"
$output = Join-Path $root "runtime/libc.prx"
$manifest = Join-Path $root "runtime/libc.prx.sha256"
$expectedRaw = "8EE6E124993E1AF26420CB455890FD002F5D6C7E78883C860CE45734E7D002BB"
$expectedSigned = "E6FF45D16ADF687855CC3B33B0C8A4132B6504360B221E0A34C7E99FB3BA0036"
function Fail([string]$Message) {
throw "ps5-native-app-boilerplate: $Message"
}
function WslPath([string]$Path) {
$absolute = [IO.Path]::GetFullPath($Path)
if ($absolute -notmatch '^([A-Za-z]):\\(.*)$') {
Fail "Path is not on a Windows drive visible to WSL: $absolute"
}
return "/mnt/$($Matches[1].ToLowerInvariant())/$($Matches[2].Replace('\', '/'))"
}
function Invoke-Wsl([string[]]$Arguments) {
& wsl.exe --exec @Arguments
if ($LASTEXITCODE -ne 0) { Fail "Native runtime build failed." }
}
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
Fail "WSL was not found."
}
foreach ($required in @($apiManifest, $importManifest,
$manifest,
$setupNativeDependencies,
(Join-Path $native "libc_builder.cpp"),
(Join-Path $native "native_app_builder.cpp"))) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
Fail "Required source file not found: $required"
}
}
$recordedSigned = ((Get-Content -LiteralPath $manifest -Raw).Trim() -split '\s+')[0]
if ($recordedSigned -ne $expectedSigned) {
Fail "The runtime checksum manifest does not match the release digest."
}
New-Item -ItemType Directory -Path $work -Force | Out-Null
$dependencyJson = & $setupNativeDependencies -SkipSdk
$nativeDependencies = ($dependencyJson -join "`n") | ConvertFrom-Json
$zlibInclude = [string]$nativeDependencies.zlibInclude
$zlibArchive = [string]$nativeDependencies.zlibArchive
if (-not $zlibInclude -or -not $zlibArchive) {
Fail "Native dependency bootstrap returned incomplete zlib paths."
}
Invoke-Wsl @("clang++", "-std=c++20", "-O2", "-Wall", "-Wextra", "-Werror",
(WslPath (Join-Path $native "libc_builder.cpp")), "-o", (WslPath $builder))
Invoke-Wsl @("clang++", "-std=c++20", "-O2", "-Wall", "-Wextra", "-Werror",
"-I", $zlibInclude,
(WslPath (Join-Path $native "native_app_builder.cpp")),
(WslPath (Join-Path $native "self_container.cpp")),
(WslPath (Join-Path $native "elf_object.cpp")),
(WslPath (Join-Path $native "sce_module_writer.cpp")),
$zlibArchive, "-o", (WslPath $tool))
Invoke-Wsl @((WslPath $builder), (WslPath $apiManifest),
(WslPath $importManifest), (WslPath $rawA))
Invoke-Wsl @((WslPath $builder), (WslPath $apiManifest),
(WslPath $importManifest), (WslPath $rawB))
$rawHashA = (Get-FileHash -LiteralPath $rawA -Algorithm SHA256).Hash
$rawHashB = (Get-FileHash -LiteralPath $rawB -Algorithm SHA256).Hash
if ($rawHashA -ne $rawHashB -or $rawHashA -ne $expectedRaw) {
Fail "The raw module does not match the deterministic release artifact."
}
Invoke-Wsl @((WslPath $tool), "self", "--sign", "--in", (WslPath $rawA),
"--out", (WslPath $signedA))
Invoke-Wsl @((WslPath $tool), "self", "--sign", "--in", (WslPath $rawB),
"--out", (WslPath $signedB))
$signedHashA = (Get-FileHash -LiteralPath $signedA -Algorithm SHA256).Hash
$signedHashB = (Get-FileHash -LiteralPath $signedB -Algorithm SHA256).Hash
if ($signedHashA -ne $signedHashB -or $signedHashA -ne $expectedSigned) {
Fail "The signed module does not match the deterministic release artifact."
}
if ((Get-Item -LiteralPath $signedA).Length -ne 1284674) {
Fail "The signed module is not 1,284,674 bytes."
}
foreach ($artifact in @($rawA, $signedA)) {
$text = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($artifact))
if (-not $text.Contains("BlackBearReloaded")) {
Fail "Generated output is missing the attribution marker."
}
foreach ($forbidden in @("W:/Build", "J013", "Prospero_Release", "sys/internal")) {
if ($text.Contains($forbidden)) {
Fail "Generated output contains forbidden historical text: $forbidden"
}
}
}
[IO.File]::Copy($signedA, $output, $true)
$installedHash = (Get-FileHash -LiteralPath $output -Algorithm SHA256).Hash
if ($installedHash -ne $expectedSigned) {
Fail "The installed runtime does not match the release digest."
}
Write-Host "Rebuilt deterministic clean-room runtime module with native C++ tools."
Write-Host "Raw SHA-256: $rawHashA"
Write-Host "Signed SHA-256: $signedHashA"
Write-Host "Output: $output"
-43
View File
@@ -1,43 +0,0 @@
<#
ps5-native-app-boilerplate - Optional UFS2Tool bootstrapper.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Delegates to the shared WSL bootstrapper, which fetches and builds a pinned
UFS2Tool checkout inside the ignored dependency cache.
#>
#requires -Version 5.1
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$setup = Join-Path $root "tools/setup-packaging-dependencies.sh"
function Fail([string]$Message) {
throw "ps5-native-app-boilerplate: $Message"
}
function WslPath([string]$Path) {
$absolute = [IO.Path]::GetFullPath($Path)
if ($absolute -notmatch '^([A-Za-z]):\\(.*)$') {
Fail "Path is not on a Windows drive visible to WSL: $absolute"
}
return "/mnt/$($Matches[1].ToLowerInvariant())/$($Matches[2].Replace('\', '/'))"
}
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
Fail "WSL was not found."
}
if (-not (Test-Path -LiteralPath $setup -PathType Leaf)) {
Fail "Shared packaging bootstrapper was not found: $setup"
}
$binary = & wsl.exe --exec bash (WslPath $setup) ffpkg
if ($LASTEXITCODE -ne 0) {
Fail "Unable to fetch and build UFS2Tool."
}
if ($binary -is [array]) { $binary = $binary[-1] }
if (-not $binary) {
Fail "UFS2Tool bootstrapper did not return its runner path."
}
Write-Output $binary.Trim()
-106
View File
@@ -1,106 +0,0 @@
<#
ps5-native-app-boilerplate - Optional MkPFS packaging bootstrapper.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Fetches a pinned MkPFS checkout and installs it in an ignored virtual env.
#>
#requires -Version 5.1
param(
[string]$Python = ""
)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$dependencyRoot = Join-Path $root ".deps"
$checkout = Join-Path $dependencyRoot "MkPFS"
$repository = "https://github.com/PSBrew/MkPFS.git"
$revision = "6cb8313dfe0c988ac52617794553f343243d3a56"
function Fail([string]$Message) {
throw "ps5-native-app-boilerplate: $Message"
}
function Invoke-Git([string[]]$Arguments) {
& git @Arguments
if ($LASTEXITCODE -ne 0) {
Fail "Git command failed: git $($Arguments -join ' ')"
}
}
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
Fail "Git was not found. Install Git for Windows and retry."
}
if (-not $Python) {
$pythonCommand = Get-Command python -ErrorAction SilentlyContinue
} else {
$pythonCommand = Get-Command $Python -ErrorAction SilentlyContinue
}
if (-not $pythonCommand) {
Fail "Python 3.9 or newer was not found. Install Python or pass -Python C:\path\to\python.exe."
}
$Python = $pythonCommand.Source
$pythonVersion = (& $Python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')").Trim()
if ($LASTEXITCODE -ne 0 -or [version]$pythonVersion -lt [version]"3.9") {
Fail "MkPFS requires Python 3.9 or newer; found $pythonVersion."
}
if (-not (Test-Path -LiteralPath $checkout)) {
New-Item -ItemType Directory -Path $dependencyRoot -Force | Out-Null
New-Item -ItemType Directory -Path $checkout | Out-Null
Invoke-Git @("-C", $checkout, "init", "--quiet")
Invoke-Git @("-C", $checkout, "config", "core.autocrlf", "false")
Invoke-Git @("-C", $checkout, "remote", "add", "origin", $repository)
Invoke-Git @("-C", $checkout, "fetch", "--quiet", "--depth", "1", "origin", $revision)
Invoke-Git @("-C", $checkout, "checkout", "--quiet", "--detach", "FETCH_HEAD")
} elseif (-not (Test-Path -LiteralPath (Join-Path $checkout ".git") -PathType Container)) {
Fail "$checkout exists but is not a managed Git checkout."
}
$actualRevision = (& git -C $checkout rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or $actualRevision -ne $revision) {
Fail "MkPFS cache is at $actualRevision; expected $revision. Remove .deps/MkPFS and retry."
}
& git -C $checkout diff --quiet
if ($LASTEXITCODE -ne 0) {
Fail "MkPFS cache has local changes. Remove .deps/MkPFS and retry."
}
foreach ($required in @("pyproject.toml", "LICENSE", "mkpfs/__main__.py")) {
$requiredPath = Join-Path $checkout $required
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
Fail "Required MkPFS file not found: $requiredPath"
}
}
$venv = Join-Path $checkout ".venv"
$venvPython = Join-Path $venv "Scripts/python.exe"
$stamp = Join-Path $venv ".boilerplate-revision"
if (-not (Test-Path -LiteralPath $venvPython -PathType Leaf)) {
& $Python -m venv $venv
if ($LASTEXITCODE -ne 0) {
Fail "Unable to create the MkPFS virtual environment."
}
}
$installedRevision = if (Test-Path -LiteralPath $stamp -PathType Leaf) {
(Get-Content -LiteralPath $stamp -Raw).Trim()
} else {
""
}
if ($installedRevision -ne $revision) {
& $venvPython -m pip install --disable-pip-version-check $checkout | Out-Host
if ($LASTEXITCODE -ne 0) {
Fail "MkPFS dependency installation failed."
}
Set-Content -LiteralPath $stamp -Value $revision -Encoding ascii
}
& $venvPython -m mkpfs --help *> $null
if ($LASTEXITCODE -ne 0) {
Fail "The cached MkPFS command failed its startup check."
}
Write-Host "MkPFS ready at revision $revision."
Write-Output $venvPython
-63
View File
@@ -1,63 +0,0 @@
<#
ps5-native-app-boilerplate - Native dependency bootstrapper.
Copyright (C) 2026 BlackBearReloaded
SPDX-License-Identifier: GPL-3.0-or-later
Runs the canonical Linux/WSL dependency bootstrap and returns its paths as
JSON for PowerShell callers.
#>
#requires -Version 5.1
param(
[switch]$SkipSdk
)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
function Fail([string]$Message) {
throw "ps5-native-app-boilerplate: $Message"
}
function Convert-ToWslPath([string]$Path) {
$absolute = [IO.Path]::GetFullPath($Path)
if ($absolute -notmatch '^([A-Za-z]):\\(.*)$') {
Fail "The repository must be on a Windows drive visible to WSL."
}
return "/mnt/$($Matches[1].ToLowerInvariant())/$($Matches[2].Replace('\', '/'))"
}
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
Fail "WSL was not found."
}
$script = Convert-ToWslPath (Join-Path $root "tools/setup-native-dependencies.sh")
$arguments = @("--exec", "bash", $script)
if ($SkipSdk) {
$arguments += "--skip-sdk"
}
$lines = @(& wsl.exe @arguments)
if ($LASTEXITCODE -ne 0) {
Fail "Native dependency bootstrap failed."
}
$values = @{}
foreach ($line in $lines) {
if ($line -match '^([A-Z_]+)=(.*)$') {
$values[$Matches[1]] = $Matches[2]
}
}
foreach ($name in @("ZLIB_INCLUDE", "ZLIB_ARCHIVE")) {
if (-not $values.ContainsKey($name)) {
Fail "Native dependency bootstrap did not report $name."
}
}
if (-not $SkipSdk -and -not $values.ContainsKey("SDK_ROOT")) {
Fail "Native dependency bootstrap did not report SDK_ROOT."
}
[pscustomobject]@{
sdkRoot = if ($SkipSdk) { "" } else { $values["SDK_ROOT"] }
zlibInclude = $values["ZLIB_INCLUDE"]
zlibArchive = $values["ZLIB_ARCHIVE"]
} | ConvertTo-Json -Compress