CultLib/scripts/build-unity-package.ps1

259 lines
14 KiB
PowerShell
Raw Normal View History

2026-07-10 03:34:49 +01:00
param(
[string] $Configuration = "Release",
[string] $OutputDirectory = "artifacts\unity\org.gamecult.cultlib",
[string] $NuGetConfig = "",
2026-08-18 23:44:51 +02:00
[switch] $NoRestore,
[switch] $UpdateTemplate
2026-07-10 03:34:49 +01:00
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$projectPath = Join-Path $repoRoot "src\GameCult.Mesh\GameCult.Mesh.csproj"
$webSocketProjectPath = Join-Path $repoRoot "src\GameCult.Networking.WebSockets\GameCult.Networking.WebSockets.csproj"
$quicProjectPath = Join-Path $repoRoot "src\GameCult.Mesh.Quic.Native\GameCult.Mesh.Quic.Native.csproj"
2026-07-10 03:34:49 +01:00
$templateRoot = Join-Path $repoRoot "unity\org.gamecult.cultlib"
$publishRoot = Join-Path $repoRoot "artifacts\unity-publish"
$webSocketPublishRoot = Join-Path $repoRoot "artifacts\unity-websocket-publish"
$quicPublishRoot = Join-Path $repoRoot "artifacts\unity-quic-publish"
$quicNativeRoot = Join-Path $repoRoot "artifacts\unity-quic-native"
2026-07-10 03:34:49 +01:00
$outputRoot = if ([System.IO.Path]::IsPathRooted($OutputDirectory)) {
$OutputDirectory
} else {
Join-Path $repoRoot $OutputDirectory
}
$pluginRoot = Join-Path $outputRoot "Runtime\Plugins"
if (Test-Path -LiteralPath $publishRoot) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if (Test-Path -LiteralPath $quicPublishRoot) {
Remove-Item -LiteralPath $quicPublishRoot -Recurse -Force
}
if (Test-Path -LiteralPath $webSocketPublishRoot) {
Remove-Item -LiteralPath $webSocketPublishRoot -Recurse -Force
}
2026-07-10 03:34:49 +01:00
if (Test-Path -LiteralPath $outputRoot) {
Remove-Item -LiteralPath $outputRoot -Recurse -Force
}
$unityPackageVersion = (Get-Content -LiteralPath (Join-Path $templateRoot "package.json") -Raw | ConvertFrom-Json).version
Write and enforce the semver policy, per operator ruling on 1.0.60 1.0.60 removed public API (CultInspectorAssetPath) under a patch bump; its own changelog says "Breaking" and the version number lied. Operator ruling: semver is policy, strictly, for every published package family. docs/semver-policy.md states what counts as breaking per surface (managed API, Unity inspector attributes, CultCache wire format/store compatibility, CultNet protocol, native bridge exports), the 0.y.z pre-1.0 convention, tag naming, and required changelog shape. Records the 1.0.60 scar as history; does not renumber it. scripts/check-changelog-semver.mjs is the mechanical check: refuses a changelog "### Breaking" section under too small a bump, and refuses a version that skips, reverses, or repeats the previous published tag. Skips re-litigating a version that is already tagged (rebuilding immutable history is not a release action). Wired into publish-packages.yml for the four npm/PyPI packages, and into build-unity-package.ps1 (cultlib), packages/cultmath/scripts/build-unity-package.ps1 (cultmath), and a new scripts/verify-caching-unity-release.ps1 (caching.unity has no build script of its own to hook into). 18 node:test cases cover the required table (breaking+major/minor/patch, non-breaking, 0.y.z, skip, downgrade) plus CLI-level git-tag resolution. Hand-mutated each rule (bump classification, pre-1.0 lane requirement, breaking-heading detection, breaking early-return, already-published skip) and confirmed each kill, then restored byte-exact. No mutation harness is committed; CultLib has none today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:03:22 +02:00
# docs/semver-policy.md: refuse to build a release whose CHANGELOG.md entry
# claims a breaking change under too small a bump, or whose version skips or
# reverses the previous published tag. Runs before the (slow) build below so
# a bad version number fails fast.
& node (Join-Path $repoRoot "scripts\check-changelog-semver.mjs") `
--package "org.gamecult.cultlib" `
--changelog (Join-Path $templateRoot "CHANGELOG.md") `
--version $unityPackageVersion `
--tag-prefix "cultlib-unity" `
--cwd $repoRoot
if ($LASTEXITCODE -ne 0) {
throw "CultLib Unity package failed the semver policy check (see docs/semver-policy.md)."
}
# Release gate: cultlib's Unity package resolves CultMath through the org.gamecult.cultmath
# dependency, so a cultlib release must not run ahead of the CultMath package it names.
# (1) The declared dependency version must not exceed the CultMath package's own version.
# (2) The CultMath.dll that package tracks must define every type CultMathResolver formats.
# The names come from the resolver source (its Shape<T> entries); scripts\check-cultmath-types.cs
# looks them up in the assembly's type-definition table, which Windows PowerShell cannot load
# the assembly to do. It checks type names, not signatures.
$cultMathPackageRoot = Join-Path $repoRoot "packages\cultmath\unity\org.gamecult.cultmath"
$cultMathDeclared = (Get-Content -LiteralPath (Join-Path $templateRoot "package.json") -Raw | ConvertFrom-Json).dependencies.'org.gamecult.cultmath'
$cultMathAvailable = (Get-Content -LiteralPath (Join-Path $cultMathPackageRoot "package.json") -Raw | ConvertFrom-Json).version
if ([string]::IsNullOrWhiteSpace($cultMathDeclared)) {
throw "package.json must declare org.gamecult.cultmath, which supplies CultMath.dll."
}
foreach ($declared in @(@("org.gamecult.cultlib's org.gamecult.cultmath dependency", $cultMathDeclared), @("the CultMath package version", $cultMathAvailable))) {
if ($declared[1] -notmatch '^\d+\.\d+\.\d+$') {
throw "$($declared[0]) is '$($declared[1])'. The release gate compares exact release versions (major.minor.patch); prerelease and range versions are not supported."
}
}
if ([version]$cultMathDeclared -gt [version]$cultMathAvailable) {
throw "Release order: org.gamecult.cultlib depends on org.gamecult.cultmath $cultMathDeclared, but the CultMath package is at $cultMathAvailable. Release CultMath first."
}
$resolverSource = Get-Content -LiteralPath (Join-Path $repoRoot "src\GameCult.Caching.MessagePack\CultMathResolver.cs") -Raw
$resolverTypes = @([regex]::Matches($resolverSource, '\bShape<(?!T>)(\w+)>\(') | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
if ($resolverTypes.Count -eq 0) {
throw "Found no CultMath types in CultMathResolver.cs; the release gate cannot check the CultMath package."
}
$missingTypes = & dotnet run (Join-Path $PSScriptRoot "check-cultmath-types.cs") -- (Join-Path $cultMathPackageRoot "Runtime\Plugins\CultMath.dll") @resolverTypes
if ($LASTEXITCODE -eq 1) {
throw "The CultMath.dll in org.gamecult.cultmath $cultMathAvailable lacks types CultMathResolver formats: $missingTypes. Release CultMath first."
}
if ($LASTEXITCODE -ne 0) {
throw "scripts\check-cultmath-types.cs failed with exit code $LASTEXITCODE."
}
# The tracked DLLs and pdbs are committed beside their source, so none may name a commit or a worktree:
# Source Link writes the commit SHA into the pdb, the informational version carries it too, and each DLL
# carries its pdb's content id. ContinuousIntegrationBuild maps the local source path to /_/.
# dotnet publish has no --no-incremental, so clear the configuration's intermediates instead: a stale obj
# directory must not decide what the byte check compares.
Get-ChildItem -LiteralPath (Join-Path $repoRoot "obj\src") -Directory -ErrorAction SilentlyContinue |
ForEach-Object { Join-Path $_.FullName $Configuration } |
Where-Object { Test-Path -LiteralPath $_ } |
ForEach-Object { Remove-Item -LiteralPath $_ -Recurse -Force }
$deterministicArguments = @("--disable-build-servers", "-p:UseSharedCompilation=false",
"-p:ContinuousIntegrationBuild=true", "-p:EnableSourceLink=false",
"-p:IncludeSourceRevisionInInformationalVersion=false", "-m:1")
$publishArguments = @("publish", $projectPath, "-c", $Configuration, "-o", $publishRoot, "-p:CultLibPackageVersion=$unityPackageVersion") + $deterministicArguments
2026-08-18 23:44:51 +02:00
if ($NoRestore) { $publishArguments += "--no-restore" }
if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) {
$publishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))"
}
& dotnet @publishArguments
2026-07-10 03:34:49 +01:00
if ($LASTEXITCODE -ne 0) {
throw "CultLib publish failed with exit code $LASTEXITCODE"
}
$webSocketPublishArguments = @("publish", $webSocketProjectPath, "-c", $Configuration, "-f", "netstandard2.1", "-o", $webSocketPublishRoot, "-p:CultLibPackageVersion=$unityPackageVersion") + $deterministicArguments
2026-08-18 23:44:51 +02:00
if ($NoRestore) { $webSocketPublishArguments += "--no-restore" }
if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) {
$webSocketPublishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))"
}
& dotnet @webSocketPublishArguments
if ($LASTEXITCODE -ne 0) {
throw "CultLib WebSocket publish failed with exit code $LASTEXITCODE"
}
$quicPublishArguments = @("publish", $quicProjectPath, "-c", $Configuration, "-o", $quicPublishRoot, "-p:CultLibPackageVersion=$unityPackageVersion") + $deterministicArguments
2026-08-18 23:44:51 +02:00
if ($NoRestore) { $quicPublishArguments += "--no-restore" }
if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) {
$quicPublishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))"
}
& dotnet @quicPublishArguments
if ($LASTEXITCODE -ne 0) {
throw "CultLib native QUIC managed publish failed with exit code $LASTEXITCODE"
}
& (Join-Path $PSScriptRoot "build-quic-native.ps1") `
-Configuration $Configuration `
-OutputDirectory $quicNativeRoot
2026-07-10 03:34:49 +01:00
New-Item -ItemType Directory -Force -Path $pluginRoot | Out-Null
Copy-Item -LiteralPath (Join-Path $templateRoot "package.json") -Destination $outputRoot
Copy-Item -LiteralPath (Join-Path $templateRoot "README.md") -Destination $outputRoot
Copy-Item -LiteralPath (Join-Path $templateRoot "CHANGELOG.md") -Destination $outputRoot
2026-07-10 03:34:49 +01:00
$expectedAssemblies = @(
"ConcurrentCollections.dll",
"GameCult.Caching.dll",
"GameCult.Caching.MessagePack.dll",
"GameCult.Logging.dll",
"GameCult.Mesh.dll",
"GameCult.Mesh.Quic.Native.dll",
2026-07-10 03:34:49 +01:00
"GameCult.Networking.dll",
"GameCult.Networking.WebSockets.dll",
2026-07-10 03:34:49 +01:00
"Isopoh.Cryptography.Argon2.dll",
"Isopoh.Cryptography.Blake2b.dll",
"Isopoh.Cryptography.SecureArray.dll",
"LiteNetLib.dll",
"MessagePack.Annotations.dll",
"MessagePack.dll",
"Microsoft.Bcl.AsyncInterfaces.dll",
"Microsoft.Bcl.TimeProvider.dll",
"Microsoft.NET.StringTools.dll",
"R3.dll",
"System.Collections.Immutable.dll",
"System.ComponentModel.Annotations.dll",
"System.IO.Pipelines.dll",
"System.Runtime.CompilerServices.Unsafe.dll",
"System.Text.Encodings.Web.dll",
"System.Text.Json.dll",
"System.Threading.Channels.dll"
)
# Assemblies the package references but never ships: org.gamecult.cultmath owns CultMath.dll, and the
# package.json dependency delivers it. A second copy in Runtime\Plugins would be a duplicate assembly.
$externalAssemblies = @("CultMath.dll")
$asmdef = Get-Content -LiteralPath (Join-Path $templateRoot "Runtime\GameCult.CultLib.asmdef") -Raw | ConvertFrom-Json
$referencedAssemblies = @($asmdef.precompiledReferences | Sort-Object)
$declaredAssemblies = @($expectedAssemblies + $externalAssemblies | Sort-Object)
if (($referencedAssemblies -join "|") -ne ($declaredAssemblies -join "|")) {
throw "GameCult.CultLib.asmdef precompiledReferences differ from the shipped plus external assembly lists."
}
2026-07-10 03:34:49 +01:00
$publishedByName = @{}
foreach ($assembly in Get-ChildItem -LiteralPath $publishRoot -Filter "*.dll") {
$publishedByName[$assembly.Name] = $assembly
}
foreach ($assembly in Get-ChildItem -LiteralPath $quicPublishRoot -Filter "*.dll") {
$publishedByName[$assembly.Name] = $assembly
}
foreach ($assembly in Get-ChildItem -LiteralPath $webSocketPublishRoot -Filter "*.dll") {
$publishedByName[$assembly.Name] = $assembly
}
2026-07-10 03:34:49 +01:00
foreach ($assemblyName in $expectedAssemblies) {
if (-not $publishedByName.ContainsKey($assemblyName)) {
throw "CultLib Unity package is missing expected assembly: $assemblyName"
}
}
Copy-Item -LiteralPath (Join-Path $templateRoot "Runtime\GameCult.CultLib.asmdef") `
-Destination (Join-Path $outputRoot "Runtime")
foreach ($assemblyName in $expectedAssemblies) {
$assembly = $publishedByName[$assemblyName]
Copy-Item -LiteralPath $assembly.FullName -Destination $pluginRoot
$pdb = [System.IO.Path]::ChangeExtension($assembly.FullName, ".pdb")
if (Test-Path -LiteralPath $pdb) {
Copy-Item -LiteralPath $pdb -Destination $pluginRoot
}
}
$nativePluginRoot = Join-Path $pluginRoot "x86_64"
New-Item -ItemType Directory -Force -Path $nativePluginRoot | Out-Null
Copy-Item -LiteralPath (Join-Path $quicNativeRoot "gamecult_mesh_quic_native.dll") -Destination $nativePluginRoot
Copy-Item -LiteralPath (Join-Path $quicNativeRoot "msquic.dll") -Destination $nativePluginRoot
New-Item -ItemType Directory -Force -Path (Join-Path $outputRoot "Third Party Notices") | Out-Null
Copy-Item -LiteralPath (Join-Path $quicNativeRoot "MSQUIC-LICENSE.txt") `
-Destination (Join-Path $outputRoot "Third Party Notices\MSQUIC-LICENSE.txt")
Copy-Item -LiteralPath (Join-Path $quicNativeRoot "OPENSSL-NOTICE.txt") `
-Destination (Join-Path $outputRoot "Third Party Notices\OPENSSL-NOTICE.txt")
2026-07-10 03:34:49 +01:00
$meshPackagePath = Join-Path $pluginRoot "GameCult.Mesh.dll"
$meshPackageVersion = [Reflection.AssemblyName]::GetAssemblyName($meshPackagePath).Version.ToString()
if ($meshPackageVersion -ne "$unityPackageVersion.0") {
throw "Packaged GameCult.Mesh.dll version '$meshPackageVersion' does not match package '$unityPackageVersion'."
}
$meshPackageMetadata = [Text.Encoding]::UTF8.GetString([IO.File]::ReadAllBytes($meshPackagePath))
foreach ($requiredType in @(
"GameCult.Mesh.CultMeshAuthorityTrustPolicy",
"GameCult.Mesh.CultMeshRouteCertificate",
"GameCult.Mesh.CultMeshHttpsContentTransportConnector")) {
if (-not $meshPackageMetadata.Contains($requiredType.Split('.')[-1])) {
throw "Packaged GameCult.Mesh.dll is missing required API '$requiredType'."
}
}
$webSocketPackageMetadata = [Text.Encoding]::UTF8.GetString(
[IO.File]::ReadAllBytes((Join-Path $pluginRoot "GameCult.Networking.WebSockets.dll")))
if (-not $webSocketPackageMetadata.Contains("CultNetWebSocketSchemaClient")) {
throw "Packaged WebSocket assembly is missing the Unity-compatible CultNet client."
}
if ($UpdateTemplate) {
$templatePluginRoot = Join-Path $templateRoot "Runtime\Plugins"
foreach ($assemblyName in $expectedAssemblies) {
Copy-Item -LiteralPath (Join-Path $pluginRoot $assemblyName) -Destination $templatePluginRoot -Force
$pdbName = [System.IO.Path]::ChangeExtension($assemblyName, ".pdb")
$pdb = Join-Path $pluginRoot $pdbName
if (Test-Path -LiteralPath $pdb) {
Copy-Item -LiteralPath $pdb -Destination $templatePluginRoot -Force
}
}
$templateNativePluginRoot = Join-Path $templatePluginRoot "x86_64"
New-Item -ItemType Directory -Force -Path $templateNativePluginRoot | Out-Null
Copy-Item -LiteralPath (Join-Path $nativePluginRoot "gamecult_mesh_quic_native.dll") `
-Destination $templateNativePluginRoot -Force
Copy-Item -LiteralPath (Join-Path $nativePluginRoot "msquic.dll") `
-Destination $templateNativePluginRoot -Force
$templateNoticesRoot = Join-Path $templateRoot "Third Party Notices"
New-Item -ItemType Directory -Force -Path $templateNoticesRoot | Out-Null
Copy-Item -LiteralPath (Join-Path $outputRoot "Third Party Notices\MSQUIC-LICENSE.txt") `
-Destination $templateNoticesRoot -Force
Copy-Item -LiteralPath (Join-Path $outputRoot "Third Party Notices\OPENSSL-NOTICE.txt") `
-Destination $templateNoticesRoot -Force
Write-Host "Updated tracked Unity package assemblies: $templatePluginRoot"
}
2026-07-10 03:34:49 +01:00
$manifest = Get-Content -LiteralPath (Join-Path $outputRoot "package.json") -Raw | ConvertFrom-Json
Write-Host "CultLib Unity package: $outputRoot"
Write-Host "Package: $($manifest.name)@$($manifest.version)"
Write-Host "Managed assemblies: $($expectedAssemblies.Count)"
Pin the Windows MsQuic to the OpenSSL build, and both of its digests Q9 ruled A. The Schannel MsQuic refuses QUIC_CREDENTIAL_TYPE_CERTIFICATE_PKCS12 with QUIC_STATUS_NOT_SUPPORTED, and PKCS12 is the only credential type a provider has on both platforms, so a Windows provider could not open a listener against the runtime this repo was shipping. The same bridge binary passes all twenty smoke checks and the v1 gate against the OpenSSL build, which is what Soul verified and what this now downloads. Two digests, not one. The zip is pinned by FEE9A664 and 19,179,817 bytes; the msquic.dll inside it by C17C6581 and 4,181,856 bytes. The earlier report and the cut map's first record put the zip's digest on the DLL, which is a pin that checks the archive and then ships a file nobody looked at. Sizes ride along with both, because a digest that fails tells you less than a digest and a length that fail together. The CRT is linked statically. A Node or Unity host without the Visual C++ redistributable cannot load a DLL that imports vcruntime140.dll, and Windows reports that as "the specified module could not be found" while naming the module that was found. Confirmed on the rebuild: msquic.dll, CRYPT32, ntdll, KERNEL32, and no CRT import at all. The DLL goes from 83,456 to 302,080 bytes, which is the price of the host not needing anything installed. The Unity plugin's committed msquic.dll is still the Schannel one and this does not touch it; the next build-unity-package.ps1 run is what replaces it, and that is not in this batch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:04:27 +02:00
Write-Host "Native realtime: MsQuic OpenSSL 2.5.9 (Windows x64)"