Files
DockMV/scripts/build-release.ps1
kawaandClaude Sonnet 5 34e18987a0 Rename product to dockmv (container, image, module, env vars)
Container/image/service name, Go module path, CLI binary name, and
DOCKER_MIGRATE_* env vars still used the old working name; the project
is branded DockMV everywhere else (README, logo, Gitea repo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 13:56:26 +02:00

233 lines
8.6 KiB
PowerShell

#requires -Version 5.1
<#
.SYNOPSIS
Build the dockmv web UI and cross-compile release binaries.
.DESCRIPTION
Produces dist/dockmv-<os>-<arch>[.exe] for every target, one archive
per binary (.zip for Windows, .tar.gz elsewhere) and dist/SHA256SUMS.txt.
The PowerShell equivalent of `make release`, so a Windows host without make
can cut the same artifacts. The web UI is built first: vite writes into
internal/webui/dist, which the Go binary embeds, so the UI must be current
before the compile step — that directory is committed, so -SkipUi is safe for
a backend-only iteration.
Runs anywhere: the Go toolchain cross-compiles with CGO_ENABLED=0, and no
target needs a platform-specific linker.
.EXAMPLE
./scripts/build-release.ps1
Build every default target, tagging archives with `git describe`.
.EXAMPLE
./scripts/build-release.ps1 -Tag v1.2.0 -Targets windows/amd64, linux/amd64
Build two targets only, named v1.2.0.
.EXAMPLE
./scripts/build-release.ps1 -SkipUi
Reuse the committed internal/webui/dist instead of running npm.
#>
[CmdletBinding()]
param(
# os/arch pairs to build. Anything `go tool dist list` accepts works.
[string[]]$Targets = @("linux/amd64", "linux/arm64", "darwin/arm64", "darwin/amd64", "windows/amd64"),
# Version stamped into the binary and used in archive names. Defaults to
# `git describe --tags --always --dirty`, matching the Makefile.
[string]$Tag,
# Go toolchain to build with.
[string]$Go = "go",
# Skip the `npm ci && npm run build` UI step.
[switch]$SkipUi,
# Remove dist/ before building.
[switch]$Clean,
# Build the binaries but skip archives and checksums.
[switch]$NoArchive
)
$ErrorActionPreference = "Stop"
function Invoke-Checked {
param([Parameter(Mandatory)][string]$Exe, [Parameter(Mandatory)][string[]]$Args)
Write-Host " > $Exe $($Args -join ' ')" -ForegroundColor DarkGray
& $Exe @Args
if ($LASTEXITCODE -ne 0) {
throw "'$Exe $($Args -join ' ')' failed with exit code $LASTEXITCODE."
}
}
function Invoke-GoBuild {
<#
One cross-compile. GOOS/GOARCH/CGO_ENABLED are process-wide env vars, so
they are saved and restored: leaving GOOS=windows set would silently
poison every later `go` call in the same session.
#>
param(
[Parameter(Mandatory)][string]$Go,
[Parameter(Mandatory)][string]$Goos,
[Parameter(Mandatory)][string]$Goarch,
[Parameter(Mandatory)][string]$Output,
[Parameter(Mandatory)][string]$Ldflags
)
$saved = @{
GOOS = $env:GOOS
GOARCH = $env:GOARCH
CGO_ENABLED = $env:CGO_ENABLED
}
try {
$env:GOOS = $Goos
$env:GOARCH = $Goarch
$env:CGO_ENABLED = "0"
Invoke-Checked $Go @("build", "-trimpath", "-ldflags", $Ldflags, "-o", $Output, ".")
}
finally {
foreach ($k in $saved.Keys) {
if ($null -eq $saved[$k]) { Remove-Item "env:$k" -ErrorAction SilentlyContinue }
else { Set-Item "env:$k" $saved[$k] }
}
}
}
function New-Archive {
<#
Archive one binary. .zip for Windows consumers, .tar.gz for the unix
targets — a tar preserves the executable bit, which a zip made by
Compress-Archive does not, so a downloaded linux binary stays runnable
without a chmod.
#>
param(
[Parameter(Mandatory)][string]$BinaryPath,
[Parameter(Mandatory)][string]$ArchivePath
)
if (Test-Path $ArchivePath) { Remove-Item -Force $ArchivePath }
$dir = Split-Path -Parent $BinaryPath
$name = Split-Path -Leaf $BinaryPath
if ($ArchivePath.EndsWith(".zip")) {
Compress-Archive -Path $BinaryPath -DestinationPath $ArchivePath
}
else {
# bsdtar ships with Windows 10 1803+; -C keeps the archive flat so it
# extracts as a bare binary rather than a dist/ tree.
$tar = (Get-Command tar -ErrorAction SilentlyContinue).Source
if (-not $tar) { throw "tar not found — needed for .tar.gz archives (pass -NoArchive to skip)." }
Invoke-Checked $tar @("-czf", $ArchivePath, "-C", $dir, $name)
}
}
$repoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $repoRoot
try {
if (-not $Tag) {
try { $Tag = (git describe --tags --always --dirty 2>$null).Trim() } catch { }
if (-not $Tag) { $Tag = "dev" }
}
$bin = "dockmv"
$distDir = Join-Path $repoRoot "dist"
$ldflags = "-s -w -X main.version=$Tag"
Write-Host "dockmv release build" -ForegroundColor Cyan
Write-Host " go : $Go"
Write-Host " tag : $Tag"
Write-Host " targets : $($Targets -join ', ')"
Write-Host " output : $distDir"
Write-Host ""
if ($Clean) {
Write-Host "Cleaning dist/..." -ForegroundColor Cyan
if (Test-Path $distDir) { Remove-Item -Recurse -Force $distDir }
}
New-Item -ItemType Directory -Force -Path $distDir | Out-Null
# --- Web UI ---------------------------------------------------------------
# vite's outDir is internal/webui/dist, which embed.go embeds, so this has to
# run before the compile — not after, and not in parallel with it.
if (-not $SkipUi) {
Write-Host "Building web UI..." -ForegroundColor Cyan
# npm.cmd, not npm(.ps1): the ps1 shim reparses $MyInvocation.Line as text
# when called via `& $Exe @Args`, which mangles splatted args into a bogus
# single token and makes npm print usage instead of running the command.
$npmCmd = (Get-Command npm.cmd -ErrorAction SilentlyContinue).Source
if (-not $npmCmd) {
throw "npm not found — install Node 22+, or pass -SkipUi to reuse the committed internal/webui/dist."
}
Push-Location (Join-Path $repoRoot "web")
try {
Invoke-Checked $npmCmd @("ci", "--no-audit", "--no-fund")
Invoke-Checked $npmCmd @("run", "build")
}
finally { Pop-Location }
Write-Host ""
}
$embedded = Join-Path $repoRoot "internal/webui/dist/index.html"
if (-not (Test-Path $embedded)) {
throw "internal/webui/dist/index.html is missing — the binary would serve an empty UI. Run without -SkipUi."
}
# --- Compile --------------------------------------------------------------
Write-Host "Cross-compiling..." -ForegroundColor Cyan
$built = @()
foreach ($target in $Targets) {
$parts = $target -split "/"
if ($parts.Count -ne 2) { throw "target '$target' is not in os/arch form." }
$goos, $goarch = $parts
$ext = if ($goos -eq "windows") { ".exe" } else { "" }
$binaryPath = Join-Path $distDir "$bin-$goos-$goarch$ext"
Write-Host " $goos/$goarch" -ForegroundColor DarkGray
Invoke-GoBuild -Go $Go -Goos $goos -Goarch $goarch -Output $binaryPath -Ldflags $ldflags
if (-not (Test-Path $binaryPath)) { throw "go build reported success but $binaryPath is missing." }
$archivePath = $null
if (-not $NoArchive) {
$archiveExt = if ($goos -eq "windows") { "zip" } else { "tar.gz" }
$archivePath = Join-Path $distDir "$bin-$Tag-$goos-$goarch.$archiveExt"
New-Archive -BinaryPath $binaryPath -ArchivePath $archivePath
}
$built += [pscustomobject]@{
Target = $target
Binary = $binaryPath
Archive = $archivePath
SizeMb = [math]::Round((Get-Item $binaryPath).Length / 1MB, 1)
}
}
Write-Host ""
# --- Checksums ------------------------------------------------------------
# Over the archives, since those are what a release serves. Written in the
# `sha256sum -c` format so it verifies on a linux host as-is.
$sumsPath = $null
if (-not $NoArchive) {
Write-Host "Writing checksums..." -ForegroundColor Cyan
$sumsPath = Join-Path $distDir "SHA256SUMS.txt"
$lines = foreach ($b in $built) {
$hash = (Get-FileHash -Algorithm SHA256 $b.Archive).Hash.ToLower()
"$hash $(Split-Path -Leaf $b.Archive)"
}
# ASCII with LF: a BOM or CRLF makes `sha256sum -c` reject the first line.
[System.IO.File]::WriteAllText($sumsPath, ($lines -join "`n") + "`n", [System.Text.ASCIIEncoding]::new())
Write-Host ""
}
Write-Host "Done. Built:" -ForegroundColor Green
foreach ($b in $built) {
Write-Host " $($b.Binary) ($($b.SizeMb) MB)" -ForegroundColor Green
if ($b.Archive) { Write-Host " $($b.Archive)" -ForegroundColor Green }
}
if ($sumsPath) { Write-Host " $sumsPath" -ForegroundColor Green }
}
finally {
Pop-Location
}