Implement versioning and build enhancements with cross-platform support

This commit is contained in:
2025-05-25 05:06:21 +02:00
parent 8ba48d2248
commit 48cad7144f
7 changed files with 990 additions and 7 deletions

View File

@ -0,0 +1,38 @@
param(
[switch]$AsCheckmark
)
# Get the list from 'go tool dist list'
$dists = & go tool dist list
# Parse into OS/ARCH pairs
$parsed = $dists | ForEach-Object {
$split = $_ -split '/'
[PSCustomObject]@{ OS = $split[0]; ARCH = $split[1] }
}
# Find all unique OSes and arches, sorted
$oses = $parsed | Select-Object -ExpandProperty OS -Unique | Sort-Object
$arches = $parsed | Select-Object -ExpandProperty ARCH -Unique | Sort-Object
# Group by OS, and build custom objects
$results = foreach ($os in $oses) {
$props = @{}
$props.OS = $os
foreach ($arch in $arches) {
$hasArch = $parsed | Where-Object { $_.OS -eq $os -and $_.ARCH -eq $arch }
if ($hasArch) {
if ($AsCheckmark) {
$props[$arch] = '✅'
} else {
$props[$arch] = $true
}
} else {
$props[$arch] = $false
}
}
[PSCustomObject]$props
}
# Output
$results | Format-Table -AutoSize

536
scripts/build.ps1 Normal file
View File

@ -0,0 +1,536 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Build articulate-parser for multiple platforms
.DESCRIPTION
This script builds the articulate-parser application for multiple operating systems and architectures.
It can build using either native Go (preferred) or WSL if Go is not available on Windows.
.PARAMETER Jobs
Number of parallel build jobs to run (default: 4)
.PARAMETER Platforms
Comma-separated list of platforms to build for (e.g., "windows,linux")
Available: windows, linux, darwin, freebsd
.PARAMETER Architectures
Comma-separated list of architectures to build for (e.g., "amd64,arm64")
Available: amd64, arm64
.PARAMETER BuildDir
Directory to place built binaries (default: 'build')
.PARAMETER EntryPoint
Entry point Go file (default: 'main.go')
Note: This script assumes the entry point is in the project root.
.PARAMETER UseWSL
Force use of WSL even if Go is available on Windows
.PARAMETER Clean
Clean build directory before building
.PARAMETER VerboseOutput
Enable verbose output
.PARAMETER LdFlags
Linker flags to pass to go build (default: "-s -w" for smaller binaries)
Use empty string ("") to disable default ldflags
.PARAMETER SkipTests
Skip running tests before building
.PARAMETER Version
Version string to embed in binaries (auto-detected from git if not provided)
.PARAMETER ShowTargets
Show available build targets and exit
.EXAMPLE
.\build.ps1
Build for all platforms and architectures
.EXAMPLE
.\build.ps1 -Platforms "windows,linux" -Architectures "amd64"
Build only for Windows and Linux on amd64
.EXAMPLE
.\build.ps1 -Jobs 8 -VerboseOutput
Build with 8 parallel jobs and verbose output
.EXAMPLE
.\build.ps1 -Version "v1.2.3" -SkipTests
Build with specific version and skip tests
.EXAMPLE
.\build.ps1 -LdFlags "-X main.version=1.0.0"
Build with custom ldflags (overrides default -s -w)
.EXAMPLE
.\build.ps1 -LdFlags ""
Build without any ldflags (disable defaults)
.EXAMPLE
.\build.ps1 -ShowTargets
Show all available build targets
.FUNCTIONALITY
Build automation, Cross-platform compilation, Go builds, Multi-architecture, Parallel builds, Windows, Linux, macOS, FreeBSD, Release management, Go applications
.NOTES
This script requires Go to be installed and available in the PATH.
It also requires git if auto-detecting version from tags.
If Go is not available on Windows, it will use WSL to perform the build.
Ensure you have the necessary permissions to create directories and files in the specified BuildDir.
For WSL builds, ensure you have a compatible Linux distribution installed and configured.
.OUTPUTS
Outputs built binaries to the specified BuildDir.
Displays build summary including successful and failed builds.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false, Position = 0, HelpMessage = 'Number of parallel build jobs', ValueFromPipeline, ValueFromPipelineByPropertyName)]
[int]$Jobs = 4,
[Parameter(Mandatory = $false, Position = 1, HelpMessage = 'Comma-separated list of platforms to build for', ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Platforms = 'windows,linux,darwin,freebsd',
[Parameter(Mandatory = $false, Position = 2, HelpMessage = 'Comma-separated list of architectures to build for', ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Architectures = 'amd64,arm64',
[Parameter(Mandatory = $false, Position = 3, HelpMessage = 'Directory to place built binaries', ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$BuildDir = 'build',
[Parameter(Mandatory = $false, Position = 4, HelpMessage = 'Entry point Go file', ValueFromPipeline, ValueFromPipelineByPropertyName, ValueFromRemainingArguments)]
[string]$EntryPoint = 'main.go',
[Parameter(Mandatory = $false, Position = 5, HelpMessage = 'Force use of WSL even if Go is available on Windows')]
[switch]$UseWSL,
[Parameter(Mandatory = $false, Position = 6, HelpMessage = 'Clean build directory before building')]
[switch]$Clean,
[Parameter(Mandatory = $false, Position = 7, HelpMessage = 'Enable verbose output')]
[switch]$VerboseOutput,
[Parameter(Mandatory = $false, Position = 8, HelpMessage = 'Linker flags to pass to go build')]
[string]$LdFlags = '-s -w',
[Parameter(Mandatory = $false, Position = 9, HelpMessage = 'Skip running tests before building')]
[switch]$SkipTests,
[Parameter(Mandatory = $false, Position = 10, HelpMessage = 'Version string to embed in binaries')]
[string]$Version = '',
[Parameter(Mandatory = $false, Position = 11, HelpMessage = 'Show available build targets and exit')]
[switch]$ShowTargets
)
# Set error action preference
$ErrorActionPreference = 'Stop'
# Get script directory and project root
$ScriptDir = $PSScriptRoot
$ProjectRoot = Split-Path $ScriptDir -Parent
$BuildDir = Join-Path $ProjectRoot $BuildDir
# Ensure we're in the project root
Push-Location $ProjectRoot
try {
# Show targets and exit if requested
if ($ShowTargets) {
Write-Host 'Available build targets:' -ForegroundColor Cyan
# Get available platforms and architectures from Go toolchain
try {
$GoTargets = @(go tool dist list 2>$null)
if ($LASTEXITCODE -ne 0 -or $GoTargets.Count -eq 0) {
throw 'Failed to get target list from Go toolchain'
}
} catch {
Write-Host '⚠️ Could not retrieve targets from Go. Using default targets.' -ForegroundColor Yellow
$PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() }
$ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() }
foreach ($platform in $PlatformList) {
foreach ($arch in $ArchList) {
$BinaryName = "articulate-parser-$platform-$arch"
if ($platform -eq 'windows') { $BinaryName += '.exe' }
Write-Host " $platform/$arch -> $BinaryName" -ForegroundColor Gray
}
}
return
}
# Filter targets from go tool dist list
$SelectedTargets = @()
$PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() }
$ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() }
foreach ($target in $GoTargets) {
$parts = $target.Split('/')
$platform = $parts[0]
$arch = $parts[1]
if ($PlatformList -contains $platform -and $ArchList -contains $arch) {
$SelectedTargets += @{
Platform = $platform
Arch = $arch
Original = $target
}
}
}
# Display filtered targets
foreach ($target in $SelectedTargets) {
$BinaryName = "articulate-parser-$($target.Platform)-$($target.Arch)"
if ($target.Platform -eq 'windows') { $BinaryName += '.exe' }
Write-Host " $($target.Original) -> $BinaryName" -ForegroundColor Gray
}
# Show all available targets if verbose
if ($VerboseOutput) {
Write-Host "`nAll Go targets available on this system:" -ForegroundColor Cyan
foreach ($target in $GoTargets) {
Write-Host " $target" -ForegroundColor DarkGray
}
}
return
}
# Validate required files exist
$RequiredFiles = @('go.mod', 'main.go')
foreach ($file in $RequiredFiles) {
if (-not (Test-Path $file)) {
Write-Error "Required file not found: $file. Make sure you're in the project root."
exit 1
}
}
# Auto-detect version from git if not provided
if (-not $Version) {
try {
$gitTag = git describe --tags --always --dirty 2>$null
if ($LASTEXITCODE -eq 0 -and $gitTag) {
$Version = $gitTag.Trim()
if ($VerboseOutput) { Write-Host "✓ Auto-detected version: $Version" -ForegroundColor Green }
} else {
$Version = 'dev'
if ($VerboseOutput) { Write-Host "⚠ Using default version: $Version" -ForegroundColor Yellow }
}
} catch {
$Version = 'dev'
if ($VerboseOutput) { Write-Host "⚠ Git not available, using default version: $Version" -ForegroundColor Yellow }
}
}
# Get build timestamp
$BuildTime = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'
# Get commit hash if available
$CommitHash = 'unknown'
try {
$gitCommit = git rev-parse --short HEAD 2>$null
if ($LASTEXITCODE -eq 0 -and $gitCommit) {
$CommitHash = $gitCommit.Trim()
}
} catch {
# Git not available or not in a git repo
}
# Prepare enhanced ldflags with version info
$VersionLdFlags = @(
"-X main.Version=$Version",
"-X main.BuildTime=$BuildTime",
"-X main.CommitHash=$CommitHash"
)
# Combine base ldflags with version ldflags
$AllLdFlags = @()
if ($LdFlags) {
# Remove quotes if present and split by space
$BaseLdFlags = $LdFlags.Trim('"', "'").Split(' ', [StringSplitOptions]::RemoveEmptyEntries)
$AllLdFlags += $BaseLdFlags
}
$AllLdFlags += $VersionLdFlags
$EnhancedLdFlags = $AllLdFlags -join ' '
if ($VerboseOutput) {
Write-Host "🔍 Enhanced ldflags: '$EnhancedLdFlags'" -ForegroundColor Magenta
}
# Validate Go installation
$GoAvailable = $false
try {
$goVersion = go version 2>$null
if ($LASTEXITCODE -eq 0) {
$GoAvailable = $true
if ($VerboseOutput) { Write-Host "✓ Go is available: $goVersion" -ForegroundColor Green }
}
} catch {
# Go not available
}
# Check if we should use WSL
$UseWSLBuild = $UseWSL -or (-not $GoAvailable)
if ($UseWSLBuild) {
# Check WSL availability
try {
wsl.exe --status >$null 2>&1
if ($LASTEXITCODE -ne 0) {
throw 'WSL is not available'
}
} catch {
Write-Error 'Neither Go nor WSL is available. Please install Go or WSL to build the project.'
exit 1
}
Write-Host '🔄 Using WSL for build...' -ForegroundColor Yellow
# Build script path
$bashScript = Join-Path $ScriptDir 'build.sh'
if (-not (Test-Path $bashScript)) {
Write-Error "Build script not found at $bashScript"
exit 1
}
# Prepare arguments for bash script
$bashArgs = @()
if ($Jobs -ne 4) {
$bashArgs += '-j', $Jobs
}
if ($EnhancedLdFlags) {
$bashArgs += '-ldflags', $EnhancedLdFlags
}
# Pass build directory and entry point
$bashArgs += '-o', $BuildDir
$bashArgs += '-e', $EntryPoint
# Execute WSL build
wsl.exe bash "$bashScript" @bashArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "WSL build script failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
return
}
# Run tests before building (unless skipped)
if (-not $SkipTests) {
Write-Host '🧪 Running tests...' -ForegroundColor Cyan
$TestResult = go test -v ./... 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host '❌ Tests failed:' -ForegroundColor Red
Write-Host $TestResult -ForegroundColor Red
Write-Error 'Tests failed. Use -SkipTests to build anyway.'
exit 1
}
Write-Host '✅ All tests passed' -ForegroundColor Green
}
# Native PowerShell build
Write-Host '🔨 Building articulate-parser natively...' -ForegroundColor Cyan
# Clean build directory if requested
if ($Clean -and (Test-Path $BuildDir)) {
Write-Host '🧹 Cleaning build directory...' -ForegroundColor Yellow
Remove-Item $BuildDir -Recurse -Force
}
# Create build directory
if (-not (Test-Path $BuildDir)) {
New-Item -ItemType Directory -Path $BuildDir | Out-Null
}
# Parse platforms and architectures
$PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() }
$ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() }
# Validate platforms and architectures
$ValidPlatforms = @('windows', 'linux', 'darwin', 'freebsd')
$ValidArchs = @('amd64', 'arm64')
foreach ($platform in $PlatformList) {
if ($platform -notin $ValidPlatforms) {
Write-Error "Invalid platform: $platform. Valid platforms: $($ValidPlatforms -join ', ')"
exit 1
}
}
foreach ($arch in $ArchList) {
if ($arch -notin $ValidArchs) {
Write-Error "Invalid architecture: $arch. Valid architectures: $($ValidArchs -join ', ')"
exit 1
}
}
# Generate build targets
$Targets = @()
foreach ($platform in $PlatformList) {
foreach ($arch in $ArchList) {
$BinaryName = "articulate-parser-$platform-$arch"
if ($platform -eq 'windows') {
$BinaryName += '.exe'
}
$Targets += @{
Platform = $platform
Arch = $arch
Binary = $BinaryName
Path = Join-Path $BuildDir $BinaryName
}
}
}
Write-Host "📋 Building $($Targets.Count) targets with $Jobs parallel jobs" -ForegroundColor Cyan
# Display targets
if ($VerboseOutput) {
foreach ($target in $Targets) {
Write-Host " - $($target.Platform)/$($target.Arch) -> $($target.Binary)" -ForegroundColor Gray
}
}
# Build function
$BuildTarget = {
param($Target, $EnhancedLdFlags, $VerboseOutput, $BuildDir, $EntryPoint, $ProjectRoot)
$env:GOOS = $Target.Platform
$env:GOARCH = $Target.Arch
$env:CGO_ENABLED = '0'
# Construct build arguments
$BuildArgs = @('build')
if ($EnhancedLdFlags) {
$BuildArgs += '-ldflags'
$BuildArgs += "`"$EnhancedLdFlags`""
}
$BuildArgs += '-o'
$BuildArgs += $Target.Path
# If using custom entry point that's not main.go
# we need to use the file explicitly to avoid duplicate declarations
$EntryPointPath = Join-Path $ProjectRoot $EntryPoint
$EntryPointFile = Split-Path $EntryPointPath -Leaf
$IsCustomEntryPoint = ($EntryPointFile -ne 'main.go')
if ($IsCustomEntryPoint) {
# When using custom entry point, compile only that file
$BuildArgs += $EntryPointPath
} else {
# For standard main.go, let Go find and compile all package files
$PackagePath = Split-Path $EntryPointPath -Parent
$BuildArgs += $PackagePath
}
# For verbose output, show the command that will be executed
if ($VerboseOutput) {
Write-Host "Command: go $($BuildArgs -join ' ')" -ForegroundColor DarkCyan
}
$LogFile = "$($Target.Path).log"
try {
if ($VerboseOutput) {
Write-Host "🔨 Building $($Target.Binary)..." -ForegroundColor Yellow
}
$Process = Start-Process -FilePath 'go' -ArgumentList $BuildArgs -Wait -PassThru -NoNewWindow -RedirectStandardError $LogFile
if ($Process.ExitCode -eq 0) {
# Remove log file on success
if (Test-Path $LogFile) {
Remove-Item $LogFile -Force
}
return @{ Success = $true; Target = $Target.Binary }
} else {
return @{ Success = $false; Target = $Target.Binary; LogFile = $LogFile }
}
} catch {
return @{ Success = $false; Target = $Target.Binary; Error = $_.Exception.Message }
}
}
# Execute builds with throttling
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Jobs)
$RunspacePool.Open()
$BuildJobs = @()
foreach ($target in $Targets) {
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript($BuildTarget).AddParameters(@{
Target = $target
EnhancedLdFlags = $EnhancedLdFlags
VerboseOutput = $VerboseOutput
BuildDir = $BuildDir
EntryPoint = $EntryPoint
ProjectRoot = $ProjectRoot
}) | Out-Null
$BuildJobs += @{
PowerShell = $PowerShell
AsyncResult = $PowerShell.BeginInvoke()
Target = $target.Binary
}
}
# Wait for results and display progress
$Completed = 0
$Successful = 0
$Failed = 0
Write-Host ''
while ($Completed -lt $BuildJobs.Count) {
foreach ($job in $BuildJobs | Where-Object { $_.AsyncResult.IsCompleted -and -not $_.Processed }) {
$job.Processed = $true
$Result = $job.PowerShell.EndInvoke($job.AsyncResult)
$job.PowerShell.Dispose()
$Completed++
if ($Result.Success) {
$Successful++
Write-Host "$($Result.Target)" -ForegroundColor Green
} else {
$Failed++
if ($Result.LogFile) {
Write-Host "$($Result.Target) (see $($Result.LogFile))" -ForegroundColor Red
} else {
Write-Host "$($Result.Target): $($Result.Error)" -ForegroundColor Red
}
}
}
Start-Sleep -Milliseconds 100
}
$RunspacePool.Close()
$RunspacePool.Dispose()
# Summary
Write-Host ''
Write-Host '📊 Build Summary:' -ForegroundColor Cyan
Write-Host " 🏷️ Version: $Version" -ForegroundColor Gray
Write-Host " 🔨 Commit: $CommitHash" -ForegroundColor Gray
Write-Host " ⏰ Build Time: $BuildTime" -ForegroundColor Gray
Write-Host " ✅ Successful: $Successful" -ForegroundColor Green
Write-Host " ❌ Failed: $Failed" -ForegroundColor Red
Write-Host " 📁 Output: $BuildDir" -ForegroundColor Yellow
if ($Successful -gt 0) {
Write-Host ''
Write-Host '📦 Built binaries:' -ForegroundColor Cyan
Get-ChildItem $BuildDir -File | Where-Object { $_.Name -notlike '*.log' } | Sort-Object Name | ForEach-Object {
$Size = [math]::Round($_.Length / 1MB, 2)
$LastWrite = $_.LastWriteTime.ToString('HH:mm:ss')
Write-Host " $($_.Name.PadRight(35)) $($Size.ToString().PadLeft(6)) MB ($LastWrite)" -ForegroundColor Gray
}
# Calculate total size
$TotalSize = (Get-ChildItem $BuildDir -File | Where-Object { $_.Name -notlike '*.log' } | Measure-Object -Property Length -Sum).Sum
$TotalSizeMB = [math]::Round($TotalSize / 1MB, 2)
Write-Host " $('Total:'.PadRight(35)) $($TotalSizeMB.ToString().PadLeft(6)) MB" -ForegroundColor Cyan
}
if ($Failed -gt 0) {
exit 1
}
} finally {
Pop-Location
}

358
scripts/build.sh Normal file
View File

@ -0,0 +1,358 @@
#!/usr/bin/env bash
set -euo pipefail
# Get the *real* path to the script, even if called via symlink
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PARENT_DIR"
# Default values
OS=("darwin" "freebsd" "linux" "windows")
ARCH=("amd64" "arm64")
OUTDIR="build"
ENTRYPOINT="main.go"
JOBS=4
SHOW_TARGETS=false
SHOW_HELP=false
VERBOSE=false
DEFAULT_LDFLAGS="-s -w"
# Function to show help
show_help() {
cat <<'EOF'
articulate-parser Build Script (Bash)
=====================================
SYNOPSIS:
build.sh [OPTIONS] [GO_BUILD_FLAGS...]
DESCRIPTION:
Cross-platform build script for articulate-parser. Builds binaries for multiple
OS/architecture combinations in parallel with embedded version information.
OPTIONS:
-h, --help Show this help message and exit
-j <number> Number of parallel jobs (default: 4)
-o <directory> Output directory for binaries (default: build)
-e <file> Entry point Go file (default: main.go)
-v, --verbose Enable verbose output for debugging
--show-targets Show available Go build targets and exit
EXAMPLES:
# Basic build with default settings
./scripts/build.sh
# Build with 8 parallel jobs
./scripts/build.sh -j 8
# Build to custom directory
./scripts/build.sh -o my_builds
# Build with custom entry point
./scripts/build.sh -e test_entry.go
# Build with verbose output
./scripts/build.sh -v
# Build with Go build flags and version info
./scripts/build.sh -ldflags "-s -w -X main.version=1.0.0 -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Show available targets
./scripts/build.sh --show-targets
# Build with Go build flags and version info
./scripts/build.sh -ldflags "-s -w -X github.com/kjanat/articulate-parser/internal/version.Version=1.0.0 -X github.com/kjanat/articulate-parser/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Build with custom ldflags (overrides default -s -w)
./scripts/build.sh -ldflags "-X github.com/kjanat/articulate-parser/internal/version.Version=1.0.0"
# Build without any ldflags (disable defaults)
./scripts/build.sh -ldflags ""
DEFAULT TARGETS:
Operating Systems: darwin, freebsd, linux, windows
Architectures: amd64, arm64
This creates 8 binaries total (4 OS × 2 ARCH)
GO BUILD FLAGS:
Any additional arguments are passed directly to 'go build'.
Default: -ldflags "-s -w" (strip debug info for smaller binaries)
Common flags include:
-ldflags Link flags (e.g., version info, optimization)
-tags Build tags
-v Verbose Go build output
-race Enable race detector
-trimpath Remove file system paths from executable
To override default ldflags, specify your own -ldflags argument.
To disable ldflags entirely, use: -ldflags ""
OUTPUT:
Binaries are named: articulate-parser-{OS}-{ARCH}[.exe]
Build logs for failed builds: {BINARY_NAME}.log
NOTES:
- Requires Go to be installed and in PATH
- Removes and recreates the output directory
- Failed builds create .log files with error details
- Uses colored output with real-time status updates
- Entry point validation ensures file exists before building
- Supports custom entry points (compiles single file to avoid conflicts)
EOF
}
# Function to show available Go build targets
show_targets() {
echo "Available Go Build Targets:"
echo "=========================="
echo
if command -v go >/dev/null 2>&1; then
echo "Getting targets from 'go tool dist list'..."
echo
# Get all targets and format them nicely
local targets
targets=$(go tool dist list 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$targets" ]; then
# Show formatted output
printf "%-15s %-10s %s\n" "OS" "ARCH" "STATUS"
printf "%-15s %-10s %s\n" "---------------" "----------" "------"
# Track our default targets
local default_targets=()
for os in "${OS[@]}"; do
for arch in "${ARCH[@]}"; do
default_targets+=("$os/$arch")
done
done
# Display all targets with status
echo "$targets" | sort | while IFS='/' read -r os arch; do
local status="available"
if printf '%s\n' "${default_targets[@]}" | grep -q "^$os/$arch$"; then
status="default"
fi
printf "%-15s %-10s %s\n" "$os" "$arch" "$status"
done
echo
echo "Summary:"
echo " Total available targets: $(echo "$targets" | wc -l)"
echo " Default targets used by this script: ${#default_targets[@]}"
echo
echo "Default script targets:"
for target in "${default_targets[@]}"; do
echo " - $target"
done
else
echo "Error: Failed to get target list from 'go tool dist list'"
exit 1
fi
else
echo "Error: Go is not installed or not in PATH"
exit 1
fi
}
# Parse parameters
while (("$#")); do
case $1 in
-h | --help)
SHOW_HELP=true
shift
;;
--show-targets)
SHOW_TARGETS=true
shift
;;
-v | --verbose)
VERBOSE=true
shift
;;
-j)
if [[ ${2-} =~ ^[0-9]+$ ]]; then
JOBS=$2
shift 2
else
echo "Error: Missing number of jobs after -j"
exit 1
fi
;;
-o)
if [ -n "${2-}" ]; then
OUTDIR=$2
shift 2
else
echo "Error: Missing output directory after -o"
exit 1
fi
;;
-e)
if [ -n "${2-}" ]; then
ENTRYPOINT=$2
shift 2
else
echo "Error: Missing entry point file after -e"
exit 1
fi
;;
*)
break
;;
esac
done
# Handle help and show-targets early
if [ "$SHOW_HELP" = true ]; then
show_help
exit 0
fi
if [ "$SHOW_TARGETS" = true ]; then
show_targets
exit 0
fi
# Validate entry point exists
if [ ! -f "$ENTRYPOINT" ]; then
echo "Error: Entry point file '$ENTRYPOINT' does not exist"
exit 1
fi
# Store remaining arguments as an array to preserve argument boundaries
GO_BUILD_FLAGS_ARRAY=("$@")
# Apply default ldflags if no custom ldflags were provided
HAS_CUSTOM_LDFLAGS=false
for arg in "${GO_BUILD_FLAGS_ARRAY[@]}"; do
if [[ "$arg" == "-ldflags" ]]; then
HAS_CUSTOM_LDFLAGS=true
break
fi
done
if [[ "$HAS_CUSTOM_LDFLAGS" == false ]] && [[ -n "$DEFAULT_LDFLAGS" ]]; then
# Add default ldflags at the beginning
GO_BUILD_FLAGS_ARRAY=("-ldflags" "$DEFAULT_LDFLAGS" "${GO_BUILD_FLAGS_ARRAY[@]}")
fi
# Verbose output
if [ "$VERBOSE" = true ]; then
echo "Build Configuration:"
echo " Entry Point: $ENTRYPOINT"
echo " Output Dir: $OUTDIR"
echo " Parallel Jobs: $JOBS"
if [ ${#GO_BUILD_FLAGS_ARRAY[@]} -gt 0 ]; then
echo " Go Build Flags: ${GO_BUILD_FLAGS_ARRAY[*]}"
else
echo " Go Build Flags: none"
fi
echo " Targets: ${#OS[@]}×${#ARCH[@]} = $((${#OS[@]} * ${#ARCH[@]})) total"
echo
fi
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
# Get build start time
BUILD_START=$(date +%s)
# Compose all targets in an array
TARGETS=()
for os in "${OS[@]}"; do
for arch in "${ARCH[@]}"; do
BIN="articulate-parser-$os-$arch"
[[ "$os" == "windows" ]] && BIN="$BIN.exe"
TARGETS+=("$BIN|$os|$arch")
done
done
# Show targets info if verbose
if [ "$VERBOSE" = true ]; then
echo "Building targets:"
for target in "${TARGETS[@]}"; do
BIN="${target%%|*}"
echo " - $BIN"
done
echo
fi
# Print pending statuses and save line numbers
for idx in "${!TARGETS[@]}"; do
BIN="${TARGETS[$idx]%%|*}"
printf "[ ] %-35s ... pending\n" "$BIN"
done
# Make sure output isn't buffered
export PYTHONUNBUFFERED=1
# Function to update a line in-place (1-based index)
update_status() {
local idx=$1
local symbol=$2
local msg=$3
# Move cursor up to the correct line
printf "\0337" # Save cursor position
printf "\033[%dA" $((${#TARGETS[@]} - idx + 1)) # Move up
printf "\r\033[K[%s] %-35s\n" "$symbol" "$msg" # Clear & update line
printf "\0338" # Restore cursor position
}
for idx in "${!TARGETS[@]}"; do
while (($(jobs -rp | wc -l) >= JOBS)); do sleep 0.2; done
(
IFS='|' read -r BIN os arch <<<"${TARGETS[$idx]}"
update_status $((idx + 1)) '>' "$BIN ... building"
# Prepare build command as an array to properly handle arguments with spaces
build_cmd=(go build)
if [ "$VERBOSE" = true ]; then
build_cmd+=(-v)
fi
build_cmd+=("${GO_BUILD_FLAGS_ARRAY[@]}" -o "$OUTDIR/$BIN" "$ENTRYPOINT")
if GOOS="$os" GOARCH="$arch" "${build_cmd[@]}" 2>"$OUTDIR/$BIN.log"; then
update_status $((idx + 1)) '✔' "$BIN done"
rm -f "$OUTDIR/$BIN.log"
else
update_status $((idx + 1)) '✖' "$BIN FAILED (see $OUTDIR/$BIN.log)"
fi
) &
done
wait
# Calculate build time
BUILD_END=$(date +%s)
BUILD_DURATION=$((BUILD_END - BUILD_START))
echo -e "\nAll builds completed in ${BUILD_DURATION}s. Find them in $OUTDIR/"
# Show build summary if verbose
if [ "$VERBOSE" = true ]; then
echo
echo "Build Summary:"
echo "=============="
success_count=0
total_size=0
for target in "${TARGETS[@]}"; do
BIN="${target%%|*}"
if [ -f "$OUTDIR/$BIN" ]; then
success_count=$((success_count + 1))
size=$(stat -f%z "$OUTDIR/$BIN" 2>/dev/null || stat -c%s "$OUTDIR/$BIN" 2>/dev/null || echo "0")
total_size=$((total_size + size))
rm -f "$OUTDIR/$BIN.log"
printf " ✔ %-42s %s\n" "$OUTDIR/$BIN" "$(numfmt --to=iec-i --suffix=B $size 2>/dev/null || echo "${size} bytes")"
else
printf " ✖ %-42s %s\n" "$OUTDIR/$BIN" "FAILED"
fi
done
echo " ────────────────────────────────────────────────"
printf " Total: %d/%d successful, %s total size\n" "$success_count" "${#TARGETS[@]}" "$(numfmt --to=iec-i --suffix=B $total_size 2>/dev/null || echo "${total_size} bytes")"
fi