diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa17bed..cca1566 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,8 @@ jobs: test: name: Test runs-on: ubuntu-latest + permissions: + contents: write strategy: matrix: go: [1.21.x, 1.22.x, 1.23.x, 1.24.x] @@ -105,18 +107,37 @@ jobs: - name: Build binaries run: | - # Build for different platforms - OS = ["darwin", "freebsd", "linux", "windows"] - ARCH = ["amd64", "arm64"] + # Set the build time environment variable + BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') - for os in OS: - for arch in ARCH: - GOOS=$os GOARCH=$arch go build -o articulate-parser-$os-$arch main.go + # Add run permissions to the build script + chmod +x ./scripts/build.sh + + # Display help information for the build script + ./scripts/build.sh --help + + # Build for all platforms + ./scripts/build.sh \ + --verbose \ + -ldflags "-s -w -X github.com/kjanat/articulate-parser/internal/version.Version=${{ github.ref_name }} -X github.com/kjanat/articulate-parser/internal/version.BuildTime=$BUILD_TIME -X github.com/kjanat/articulate-parser/internal/version.GitCommit=${{ github.sha }}" + + - name: Upload a Build Artifact + uses: actions/upload-artifact@v4.6.2 + with: + # Artifact name + name: build-artifacts # optional, default is artifact + # A file, directory or wildcard pattern that describes what to upload + path: build/ + if-no-files-found: ignore + retention-days: 1 + compression-level: 9 + overwrite: true + include-hidden-files: true - name: Create Release uses: softprops/action-gh-release@v2 with: - files: articulate-parser-* + files: build/* generate_release_notes: true draft: false prerelease: ${{ startsWith(github.ref, 'refs/tags/v0.') }} diff --git a/.gitignore b/.gitignore index c1f20e8..d7f4a13 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ go.work output/ articulate-sample.json test-output.* +go-os-arch-matrix.csv + +# Build artifacts +build/ diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..81470f4 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,15 @@ +// Package version provides version information for the Articulate Parser. +// It includes the current version, build time, and Git commit hash. +package version + +// Version information. +var ( + // Version is the current version of the application. + Version = "0.1.0" + + // BuildTime is the time the binary was built. + BuildTime = "unknown" + + // GitCommit is the git commit hash. + GitCommit = "unknown" +) diff --git a/main.go b/main.go index 833a0bd..4c57c77 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,8 @@ import ( "time" "github.com/unidoc/unioffice/document" + + "github.com/kjanat/articulate-parser/internal/version" ) // Core data structures based on the Articulate Rise JSON format @@ -547,8 +549,17 @@ func (p *ArticulateParser) processItemToDocx(doc *document.Document, item Item) } func main() { + // Handle version flag + if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "--version") { + fmt.Printf("articulate-parser %s\n", version.Version) + fmt.Printf("Build time: %s\n", version.BuildTime) + fmt.Printf("Commit: %s\n", version.GitCommit) + os.Exit(0) + } + if len(os.Args) < 3 { fmt.Println("Usage: articulate-parser [output_path]") + fmt.Println(" articulate-parser -v|--version") fmt.Println(" input_uri_or_file: Articulate Rise URI or local JSON file path") fmt.Println(" output_format: md (Markdown) or docx (Word Document)") fmt.Println(" output_path: Optional output file path") diff --git a/scripts/_build_tools_available.ps1 b/scripts/_build_tools_available.ps1 new file mode 100644 index 0000000..f6a072f --- /dev/null +++ b/scripts/_build_tools_available.ps1 @@ -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 diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..08302a2 --- /dev/null +++ b/scripts/build.ps1 @@ -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 +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..f30c9ad --- /dev/null +++ b/scripts/build.sh @@ -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 of parallel jobs (default: 4) + -o Output directory for binaries (default: build) + -e 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