param( [string]$Destination, [string]$NodeVersion = '22.18.0', [switch]$Force ) $ErrorActionPreference = 'Stop' function Get-Sha256Hex([string]$Path) { $stream = [IO.File]::OpenRead($Path) $algorithm = [Security.Cryptography.SHA256]::Create() try { return ([BitConverter]::ToString($algorithm.ComputeHash($stream))).Replace('-', '') } finally { $algorithm.Dispose() $stream.Dispose() } } $RepoRoot = Split-Path -Parent $PSScriptRoot $SourceRoot = Join-Path $RepoRoot 'third_party\MoeKoeMusic' $ApiSource = Join-Path $SourceRoot 'api' if ([string]::IsNullOrWhiteSpace($Destination)) { $Destination = Join-Path $RepoRoot 'build\music-api' } $Destination = [IO.Path]::GetFullPath($Destination) $BuildRoot = [IO.Path]::GetFullPath((Join-Path $RepoRoot 'build')) if (-not $Destination.StartsWith($BuildRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw "Music API destination must be inside $BuildRoot" } $requiredSource = @( (Join-Path $SourceRoot 'UPSTREAM_COMMIT'), (Join-Path $SourceRoot 'API_SUBMODULE_COMMIT'), (Join-Path $SourceRoot 'MOEKOE_LICENSE'), (Join-Path $SourceRoot 'MOEKOE_README.md'), (Join-Path $ApiSource 'package.json'), (Join-Path $ApiSource 'pnpm-lock.yaml'), (Join-Path $ApiSource 'LICENSE'), (Join-Path $ApiSource 'README.md'), (Join-Path $ApiSource 'ymhut-sidecar.js') ) foreach ($path in $requiredSource) { if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Required music API source is missing: $path" } } $fingerprintLines = Get-ChildItem -LiteralPath $SourceRoot -Recurse -File | Where-Object { $_.FullName -notlike '*\node_modules\*' } | Sort-Object FullName | ForEach-Object { $relative = $_.FullName.Substring($SourceRoot.TrimEnd('\').Length + 1).Replace('\', '/') "$relative|$(Get-Sha256Hex $_.FullName)" } $fingerprintBytes = [Text.Encoding]::UTF8.GetBytes(($fingerprintLines -join "`n")) $sha256 = [Security.Cryptography.SHA256]::Create() try { $fingerprint = ([BitConverter]::ToString($sha256.ComputeHash($fingerprintBytes))).Replace('-', '') } finally { $sha256.Dispose() } $markerPath = Join-Path $Destination 'payload.json' if (-not $Force -and (Test-Path -LiteralPath $markerPath)) { try { $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json if ($marker.nodeVersion -eq $NodeVersion -and $marker.sourceFingerprint -eq $fingerprint -and (Test-Path -LiteralPath (Join-Path $Destination 'node.exe')) -and (Test-Path -LiteralPath (Join-Path $Destination 'api\node_modules\express\package.json') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Destination 'api\pnpm-lock.yaml')) -and (Test-Path -LiteralPath (Join-Path $Destination 'api\LICENSE'))) { return } } catch { } } $CacheRoot = Join-Path $RepoRoot '.cache\node-runtime' New-Item -ItemType Directory -Force -Path $CacheRoot | Out-Null $archiveName = "node-v$NodeVersion-win-x64.zip" $archivePath = Join-Path $CacheRoot $archiveName $checksumsPath = Join-Path $CacheRoot "SHASUMS256-$NodeVersion.txt" $distRoot = "https://nodejs.org/dist/v$NodeVersion" if (-not (Test-Path -LiteralPath $archivePath)) { Invoke-WebRequest -UseBasicParsing "$distRoot/$archiveName" -OutFile $archivePath } if (-not (Test-Path -LiteralPath $checksumsPath)) { Invoke-WebRequest -UseBasicParsing "$distRoot/SHASUMS256.txt" -OutFile $checksumsPath } $checksumLine = Get-Content -LiteralPath $checksumsPath | Where-Object { $_ -match "\s$([regex]::Escape($archiveName))$" } | Select-Object -First 1 if (-not $checksumLine) { throw "Node checksum was not found for $archiveName" } $expectedHash = ($checksumLine -split '\s+')[0].ToUpperInvariant() $actualHash = (Get-Sha256Hex $archivePath).ToUpperInvariant() if ($actualHash -ne $expectedHash) { throw "Node runtime checksum mismatch for $archiveName" } $staging = Join-Path $BuildRoot ("music-api.staging." + $PID) if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force } New-Item -ItemType Directory -Force -Path $staging | Out-Null $extractRoot = Join-Path $staging '_node' Expand-Archive -LiteralPath $archivePath -DestinationPath $extractRoot -Force $nodeRoot = Join-Path $extractRoot "node-v$NodeVersion-win-x64" Copy-Item -LiteralPath (Join-Path $nodeRoot 'node.exe') -Destination (Join-Path $staging 'node.exe') Copy-Item -LiteralPath (Join-Path $nodeRoot 'LICENSE') -Destination (Join-Path $staging 'NODE_LICENSE') Copy-Item -LiteralPath (Join-Path $nodeRoot 'README.md') -Destination (Join-Path $staging 'NODE_README.md') Remove-Item -LiteralPath $extractRoot -Recurse -Force $apiDestination = Join-Path $staging 'api' robocopy $ApiSource $apiDestination /E /XD node_modules .git /NFL /NDL /NJH /NJS /NP | Out-Null if ($LASTEXITCODE -ge 8) { throw "Failed to copy music API source (robocopy exit $LASTEXITCODE)." } Copy-Item -LiteralPath (Join-Path $SourceRoot 'MOEKOE_LICENSE') -Destination (Join-Path $staging 'MOEKOE_LICENSE') Copy-Item -LiteralPath (Join-Path $SourceRoot 'MOEKOE_README.md') -Destination (Join-Path $staging 'MOEKOE_README.md') Copy-Item -LiteralPath (Join-Path $SourceRoot 'UPSTREAM_COMMIT') -Destination (Join-Path $staging 'UPSTREAM_COMMIT') Copy-Item -LiteralPath (Join-Path $SourceRoot 'API_SUBMODULE_COMMIT') -Destination (Join-Path $staging 'API_SUBMODULE_COMMIT') Copy-Item -LiteralPath (Join-Path $SourceRoot 'YMhut_PATCHES.md') -Destination (Join-Path $staging 'YMhut_PATCHES.md') Push-Location $apiDestination try { & corepack pnpm install --prod --frozen-lockfile --ignore-scripts --package-import-method=copy --config.node-linker=hoisted --store-dir (Join-Path $RepoRoot '.cache\pnpm-store-music') if ($LASTEXITCODE -ne 0) { throw "pnpm failed to install the locked music API dependencies." } } finally { Pop-Location } [ordered]@{ protocolVersion = 1 moekoeCommit = '4581660209c9cdd07669e8bd5bcbe136b7d91e02' apiCommit = '06560e3e053bda1ab830750db6f645bab703f824' nodeVersion = $NodeVersion sourceFingerprint = $fingerprint } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $staging 'payload.json') -Encoding utf8 if (Test-Path -LiteralPath $Destination) { Remove-Item -LiteralPath $Destination -Recurse -Force } Move-Item -LiteralPath $staging -Destination $Destination