Files
brass-and-sigil/scripts/Build-Pack.ps1
T
Matt 4ee4a2bb43 scripts: make Build-Pack and Deploy-Brass cross-platform
Two small fixes that surface when running the deploy pipeline on a
non-Windows host (we now do this on glados via the Telegram bridge):

- Build-Pack.ps1: fall back to launcher/ModpackLauncher.csproj <Version>
  when Get-Item.VersionInfo.FileVersion returns empty. Linux PowerShell
  can't parse PE FileVersion from a Windows .exe, so the previous code
  threw "set <Version> in ModpackLauncher.csproj and republish" -- but
  the csproj <Version> *was* set, just not readable from a foreign PE.
  Appends ".0" so the embedded value still matches the 4-part form the
  Windows compile bakes in.

- Deploy-Brass.ps1: prefer rsync where available, fall back to robocopy
  otherwise. robocopy is Windows-only and bails on Linux/macOS hosts.
2026-05-22 14:23:09 +00:00

208 lines
9.0 KiB
PowerShell

#requires -Version 5
<#
.SYNOPSIS
Generates manifest.json deterministically from pack.lock.json.
.DESCRIPTION
The lockfile (pack.lock.json) is the source of truth for every mod's exact
version, URL, SHA-1, and size. Running this script does not change versions.
To intentionally bump versions, use Update-Pack.ps1 with the -Refresh flag.
To see what new versions are available, use Check-Updates.ps1.
Workflow:
1. Edit pack.lock.json (manually or via Update-Pack.ps1 -Refresh)
2. Run Build-Pack.ps1 -OutputPath ...\manifest.json
3. Update the server to match
4. Deploy manifest to your hosted URL
.PARAMETER OutputPath
Where to write manifest.json.
.PARAMETER LocalPackSource
Optional folder containing local override files (configs, resourcepacks, etc.).
.PARAMETER SelfHostBaseUrl
Public URL prefix for self-hosted files (only used when LocalPackSource is set).
#>
[CmdletBinding()]
param(
[string]$OutputPath = ".\manifest.json",
[string]$LocalPackSource = "",
[string]$SelfHostBaseUrl = "https://bns.sijbers.uk/pack/files",
# Optional -- point at a published launcher .exe to embed launcherVersion + launcherUrl
# in the manifest. The launcher displays a "newer version available" banner when
# its embedded version is lower than this. Skip entirely to leave both fields out.
[string]$LauncherExePath = "",
[string]$LauncherPublicUrl = "https://bns.sijbers.uk/launcher/BrassAndSigil-Launcher.exe",
# Skip the auto-invoke of Build-Tweaks.ps1 (only touch this if you're debugging).
[switch]$SkipTweaks
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$lockPath = Join-Path $here '..\pack\pack.lock.json'
# Build any data-only tweak jars first, then fold the resulting overrides
# folder into the manifest. Set -SkipTweaks to bypass. The "overrides" name
# matches CurseForge/Modrinth modpack conventions: files that override or
# augment the standard mod set, hosted by us.
$tweaksRoot = Join-Path $here '..\pack\tweaks'
$overridesRoot = Join-Path $here '..\pack\overrides'
if (-not $SkipTweaks -and (Test-Path $tweaksRoot)) {
& (Join-Path $here 'Build-Tweaks.ps1') -TweaksRoot $tweaksRoot -OutputRoot (Join-Path $overridesRoot 'mods')
}
if (-not $LocalPackSource -and (Test-Path $overridesRoot)) {
$LocalPackSource = $overridesRoot
}
if (-not (Test-Path $lockPath)) {
throw "pack.lock.json not found at $lockPath. Run Update-Pack.ps1 -Refresh first to bootstrap it."
}
$lock = Get-Content $lockPath -Raw | ConvertFrom-Json
Write-Host ""
Write-Host "Building manifest from pack.lock.json:"
Write-Host (" Pack: {0} v{1}" -f $lock.name, $lock.version)
Write-Host (" MC: {0} ({1} {2})" -f $lock.minecraft, $lock.loader.type, $lock.loader.version)
Write-Host (" Locked: {0}" -f $lock.lockedAt)
Write-Host (" Mods: {0}" -f $lock.mods.Count)
Write-Host ""
$files = @()
$totalBytes = 0L
foreach ($mod in $lock.mods) {
# `side` is emitted only when explicitly set in the lockfile -- a missing or
# null lockfile side means "both", and we keep the manifest clean by omitting
# the field rather than serialising "both" everywhere.
$entry = [ordered]@{
path = $mod.path
url = $mod.url
sha1 = $mod.sha1
size = $mod.size
}
$hasSide = ($mod.PSObject.Properties.Name -contains 'side') -and $mod.side -and $mod.side -ne 'both'
if ($hasSide) { $entry.side = $mod.side }
$files += [pscustomobject]$entry
$totalBytes += $mod.size
$sideTag = if ($hasSide) { " [$($mod.side)]" } else { '' }
Write-Host (" [{0}] {1,-26} {2,-22} {3,8:N0} KB{4}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $mod.version, ($mod.size/1KB), $sideTag)
}
# Local overrides (configs, custom files not on Modrinth/CurseForge)
if ($LocalPackSource -and (Test-Path $LocalPackSource)) {
Write-Host ""
Write-Host "Adding local overrides from $LocalPackSource..."
$managedRoots = @('mods', 'config', 'resourcepacks', 'shaderpacks', 'kubejs', 'defaultconfigs')
$base = $SelfHostBaseUrl.TrimEnd('/')
$sourceFull = (Resolve-Path $LocalPackSource).Path.TrimEnd('\','/')
foreach ($root in $managedRoots) {
$rootDir = Join-Path $LocalPackSource $root
if (-not (Test-Path $rootDir)) { continue }
Get-ChildItem -Path $rootDir -Recurse -File | ForEach-Object {
$rel = $_.FullName.Substring($sourceFull.Length).TrimStart('\','/') -replace '\\','/'
$sha1 = (Get-FileHash -Algorithm SHA1 -Path $_.FullName).Hash.ToLowerInvariant()
$files += [pscustomobject]@{
path = $rel
url = "$base/$rel"
sha1 = $sha1
size = $_.Length
}
$totalBytes += $_.Length
Write-Host (" [local] {0}" -f $rel)
}
}
}
$manifest = [ordered]@{
name = $lock.name
version = $lock.version
minecraft = [ordered]@{ version = $lock.minecraft }
loader = [ordered]@{ type = $lock.loader.type; version = $lock.loader.version }
files = $files
}
# Optional: copy a defaultServer block through from the lockfile so the launcher
# can pre-populate friends' multiplayer list. Lockfile schema:
# "defaultServer": { "name": "Brass & Sigil", "ip": "bns.sijbers.uk" }
if ($lock.PSObject.Properties.Name -contains "defaultServer" -and $lock.defaultServer) {
$manifest.defaultServer = [ordered]@{
name = $lock.defaultServer.name
ip = $lock.defaultServer.ip
}
Write-Host (" Default server: {0} ({1})" -f $lock.defaultServer.name, $lock.defaultServer.ip)
}
# Optional: shader pack to enable by default on fresh installs. Lockfile schema:
# "defaultShader": "ComplementaryReimagined_r5.7.1.zip"
if ($lock.PSObject.Properties.Name -contains "defaultShader" -and $lock.defaultShader) {
$manifest.defaultShader = $lock.defaultShader
Write-Host (" Default shader: {0}" -f $lock.defaultShader)
}
# Optional: public URL of the brass-sigil-server panel. Used by the launcher
# to send friend-side whitelist requests. Lockfile schema:
# "panelUrl": "https://bns-admin.sijbers.uk"
if ($lock.PSObject.Properties.Name -contains "panelUrl" -and $lock.panelUrl) {
$manifest.panelUrl = $lock.panelUrl
Write-Host (" Panel URL: {0}" -f $lock.panelUrl)
}
# Optional launcher metadata: when -LauncherExePath is supplied, read the exe's
# FileVersion (set via <Version> in ModpackLauncher.csproj) and embed it. The
# launcher compares its assembly version to this value and shows an upgrade
# banner pointing at LauncherPublicUrl when older.
if ($LauncherExePath) {
if (-not (Test-Path $LauncherExePath)) {
throw "LauncherExePath '$LauncherExePath' does not exist."
}
$launcherFile = Get-Item $LauncherExePath
$launcherVersion = $launcherFile.VersionInfo.FileVersion
$versionSource = 'PE FileVersion'
# Linux PowerShell can't read PE FileVersion from a Windows .exe. When that
# happens, fall back to launcher/ModpackLauncher.csproj's <Version> (the
# source of truth that the Windows compile bakes into FileVersion anyway).
# Append ".0" so callers get the same 4-part form as the PE metadata.
if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
$csprojPath = Join-Path $here '..\launcher\ModpackLauncher.csproj'
if (Test-Path $csprojPath) {
[xml]$csproj = Get-Content $csprojPath
$csprojVersion = ($csproj.Project.PropertyGroup.Version | Where-Object { $_ }) | Select-Object -First 1
if ($csprojVersion) {
$launcherVersion = if ($csprojVersion -match '^\d+\.\d+\.\d+$') { "$csprojVersion.0" } else { $csprojVersion }
$versionSource = "csproj <Version> (fallback)"
}
}
}
if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
throw "Launcher exe has no FileVersion and csproj <Version> couldn't be read either -- set <Version> in ModpackLauncher.csproj and republish."
}
# FileVersion is the four-component form (e.g. "0.1.0.0" for csproj <Version>0.1.0</Version>).
# Embed as-is -- the launcher's Version.Parse handles it directly and avoids ambiguous
# comparisons between trimmed and untrimmed forms.
$manifest.launcherVersion = $launcherVersion
$manifest.launcherUrl = $LauncherPublicUrl
Write-Host ""
Write-Host ("Launcher metadata embedded:")
Write-Host (" Version: {0} ({1})" -f $launcherVersion, $versionSource)
Write-Host (" Url: {0}" -f $LauncherPublicUrl)
Write-Host (" Source: {0}" -f $launcherFile.FullName)
}
$json = $manifest | ConvertTo-Json -Depth 10
$outDir = Split-Path -Parent $OutputPath
if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
[System.IO.File]::WriteAllText($OutputPath, $json, [System.Text.UTF8Encoding]::new($false))
Write-Host ""
Write-Host "Manifest written: $OutputPath"
Write-Host (" Files: {0}" -f $files.Count)
Write-Host (" Total: {0:N1} MB (download size)" -f ($totalBytes / 1MB))