From 9d11a4f7f6e302b505757cb7176b6f18b3542c18 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Mon, 21 Jul 2025 14:14:02 -0600 Subject: [PATCH] gocross for Windows Signed-off-by: Aaron Klotz --- tool/go.cmd | 2 +- tool/gocross/exec_other.go | 2 +- tool/gocross/gocross-wrapper.ps1 | 213 +++++++++++++++++++++++++++++++ tool/gocross/gocross-wrapper.sh | 5 + tool/gocross/gocross.go | 32 ++--- tool/gocross/toolchain.go | 7 +- 6 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 tool/gocross/gocross-wrapper.ps1 diff --git a/tool/go.cmd b/tool/go.cmd index 04172a28d..e3274d9fd 100644 --- a/tool/go.cmd +++ b/tool/go.cmd @@ -1,2 +1,2 @@ @echo off -powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0go-win.ps1" %* +pwsh -NoProfile -ExecutionPolicy Bypass "%~dp0..\tool\gocross\gocross-wrapper.ps1" %* diff --git a/tool/gocross/exec_other.go b/tool/gocross/exec_other.go index 8d4df0db3..7bce0c099 100644 --- a/tool/gocross/exec_other.go +++ b/tool/gocross/exec_other.go @@ -11,7 +11,7 @@ import ( ) func doExec(cmd string, args []string, env []string) error { - c := exec.Command(cmd, args...) + c := exec.Command(cmd, args[1:]...) c.Env = env c.Stdin = os.Stdin c.Stdout = os.Stdout diff --git a/tool/gocross/gocross-wrapper.ps1 b/tool/gocross/gocross-wrapper.ps1 new file mode 100644 index 000000000..0e7700fd0 --- /dev/null +++ b/tool/gocross/gocross-wrapper.ps1 @@ -0,0 +1,213 @@ +#Requires -Version 7.4 + +$ErrorActionPreference = 'Stop' + +<# + .DESCRIPTION + Copies the global $args variable into an array, which is easier to work with + when preparing to start child processes. +#> +function Copy-Args { + $list = [System.Collections.Generic.List[string]]::new($Global:args.Count) + foreach ($arg in $Global:args) { + $list.Add($arg) + } + return $list.ToArray() +} + +<# + .DESCRIPTION + Copies the current environment into a hashtable, which is easier to work with + when preparing to start child processes. +#> +function Copy-Environment { + $result = @{} + foreach ($pair in (Get-Item -Path Env:)) { + $result[$pair.Key] = $pair.Value + } + return $result +} + +<# + .DESCRIPTION + Outputs the fully-qualified path to the repository's root directory. This + function expects to be run from somewhere within a git repository. + The directory containing the git executable must be somewhere in the PATH. +#> +function Get-RepoRoot { + Get-Command -Name 'git' | Out-Null + $repoRoot = & git rev-parse --show-toplevel + if ($LASTEXITCODE -ne 0) { + throw "failed obtaining repo root: git failed with code $LASTEXITCODE" + } + + # Git outputs a path containing forward slashes. Canonicalize. + return [System.IO.Path]::GetFullPath($repoRoot) +} + +<# + .DESCRIPTION + Runs the provided ScriptBlock in a child scope, restoring any changes to the + current working directory once the script block completes. +#> +function Start-ChildScope { + param ( + [Parameter(Mandatory = $true)] + [ScriptBlock]$ScriptBlock + ) + + $initialLocation = Get-Location + try { + Invoke-Command -ScriptBlock $ScriptBlock + } + finally { + Set-Location -Path $initialLocation + } +} + +<# + .SYNOPSIS + Write-Output with timestamps prepended to each line. +#> +function Write-Log { + param ($message) + $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + Write-Output "$timestamp - $message" +} + +$bootstrapScriptBlock = { + + $repoRoot = Get-RepoRoot + + Set-Location -LiteralPath $repoRoot + + switch -Wildcard -File .\go.toolchain.rev { + "/*" { $toolchain = $_ } + default { + $rev = $_ + $tsgo = Join-Path $Env:USERPROFILE '.cache' 'tsgo' + $toolchain = Join-Path $tsgo $rev + if (-not (Test-Path -LiteralPath "$toolchain.extracted" -PathType Leaf -ErrorAction SilentlyContinue)) { + New-Item -Force -Path $tsgo -ItemType Directory + Remove-Item -Force -Recurse -LiteralPath $toolchain -ErrorAction Continue + Remove-Item -Force -LiteralPath "$toolchain.extracted" -ErrorAction Continue + Write-Log "Downloading Go toolchain $rev" + + # Values from https://web.archive.org/web/20250227081443/https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.architecture?view=net-9.0 + $cpuArch = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture | Out-String -NoNewline) + # Comparison in switch is case-insensitive by default. + switch ($cpuArch) { + 'x86' { $goArch = '386' } + 'x64' { $goArch = 'amd64' } + default { $goArch = $cpuArch } + } + + Invoke-WebRequest -Uri "https://github.com/tailscale/go/releases/download/build-$rev/windows-$goArch.tar.gz" -OutFile "$toolchain.tar.gz" + try { + New-Item -Force -Path $toolchain -ItemType Directory + Start-ChildScope -ScriptBlock { + Set-Location -LiteralPath $toolchain + tar --strip-components=1 -xf "$toolchain.tar.gz" + if ($LASTEXITCODE -ne 0) { + throw "tar failed with exit code $LASTEXITCODE" + } + } + $rev | Out-File -FilePath "$toolchain.extracted" + } + finally { + Remove-Item -Force "$toolchain.tar.gz" -ErrorAction Continue + } + + # Cleanup old toolchains. + $maxDays = 90 + $oldFiles = Get-ChildItem -Path $tsgo -Filter '*.extracted' -File -Recurse -Depth 1 | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$maxDays) } + foreach ($file in $oldFiles) { + Write-Log "Cleaning up old Go toolchain $($file.Basename)" + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Continue + $dirName = Join-Path $file.DirectoryName $file.Basename -Resolve -ErrorAction Continue + if ($dirName -and (Test-Path -LiteralPath $dirName -PathType Container -ErrorAction Continue)) { + Remove-Item -LiteralPath $dirName -Recurse -Force -ErrorAction Continue + } + } + } + } + } + + if ($Env:TS_USE_GOCROSS -ne '1') { + return + } + + if (Test-Path -LiteralPath $toolchain -PathType Container -ErrorAction SilentlyContinue) { + $goMod = Join-Path $repoRoot 'go.mod' -Resolve + $goLine = Get-Content -LiteralPath $goMod | Select-String -Pattern '^go (.*)$' -List + $wantGoMinor = $goLine.Matches.Groups[1].Value.split('.')[1] + $versionFile = Join-Path $toolchain 'VERSION' + if (Test-Path -LiteralPath $versionFile -PathType Leaf -ErrorAction SilentlyContinue) { + try { + $haveGoMinor = ((Get-Content -LiteralPath $versionFile -TotalCount 1).split('.')[1]) -replace 'rc.*', '' + } + catch { + } + } + + if ([string]::IsNullOrEmpty($haveGoMinor) -or ($haveGoMinor -lt $wantGoMinor)) { + Remove-Item -Force -Recurse -LiteralPath $toolchain -ErrorAction Continue + Remove-Item -Force -LiteralPath "$toolchain.extracted" -ErrorAction Continue + } + } + + $wantVer = & git rev-parse HEAD + $gocrossOk = $false + $gocrossPath = '.\gocross.exe' + if (Get-Command -Name $gocrossPath -CommandType Application -ErrorAction SilentlyContinue) { + $gotVer = & $gocrossPath gocross-version 2> $null + if ($gotVer -eq $wantVer) { + $gocrossOk = $true + } + } + + if (-not $gocrossOk) { + $goBuildEnv = Copy-Environment + $goBuildEnv['CGO_ENABLED'] = '0' + $goBuildEnv.Remove('GOOS') + $goBuildEnv.Remove('GOARCH') + $goBuildEnv.Remove('GO111MODULE') + $goBuildEnv.Remove('GOROOT') + + $procExe = Join-Path $toolchain 'bin' 'go.exe' -Resolve + $proc = Start-Process -FilePath $procExe -WorkingDirectory $repoRoot -Environment $goBuildEnv -ArgumentList 'build', '-o', $gocrossPath, "-ldflags=-X=tailscale.com/version.gitCommitStamp=$wantVer", 'tailscale.com/tool/gocross' -NoNewWindow -Wait -PassThru + if ($proc.ExitCode -ne 0) { + throw 'error building gocross' + } + } + +} # bootstrapScriptBlock + +Start-ChildScope -ScriptBlock $bootstrapScriptBlock + +$repoRoot = Get-RepoRoot + +$execEnv = Copy-Environment +$execEnv.Remove('GOROOT') + +$argList = Copy-Args + +if ($Env:TS_USE_GOCROSS -ne '1') { + $revFile = Join-Path $repoRoot 'go.toolchain.rev' -Resolve + switch -Wildcard -File $revFile { + "/*" { $toolchain = $_ } + default { + $rev = $_ + $tsgo = Join-Path $Env:USERPROFILE '.cache' 'tsgo' + $toolchain = Join-Path $tsgo $rev -Resolve + } + } + + $procExe = Join-Path $toolchain 'bin' 'go.exe' -Resolve + $proc = Start-Process -FilePath $procExe -WorkingDirectory $repoRoot -Environment $execEnv -ArgumentList $argList -NoNewWindow -Wait -PassThru + exit $proc.ExitCode +} + +$procExe = Join-Path $repoRoot 'gocross.exe' -Resolve +$proc = Start-Process -FilePath $procExe -WorkingDirectory $repoRoot -Environment $execEnv -ArgumentList $argList -NoNewWindow -Wait -PassThru +exit $proc.ExitCode diff --git a/tool/gocross/gocross-wrapper.sh b/tool/gocross/gocross-wrapper.sh index 90485d31b..22130a049 100755 --- a/tool/gocross/gocross-wrapper.sh +++ b/tool/gocross/gocross-wrapper.sh @@ -15,6 +15,11 @@ if [[ "${CI:-}" == "true" && "${NOBASHDEBUG:-}" != "true" ]]; then set -x fi +if [[ "${OSTYPE:-}" == "cygwin" || "${OSTYPE:-}" == "msys" ]]; then + echo "You're running on Windows: use go.cmd instead." >&2 + exit 1 +fi + # Locate a bootstrap toolchain and (re)build gocross if necessary. We run all of # this in a subshell because posix shell semantics make it very easy to # accidentally mutate the input environment that will get passed to gocross at diff --git a/tool/gocross/gocross.go b/tool/gocross/gocross.go index d14ea0388..f5131505e 100644 --- a/tool/gocross/gocross.go +++ b/tool/gocross/gocross.go @@ -15,9 +15,9 @@ import ( "fmt" "os" "path/filepath" - "runtime/debug" "tailscale.com/atomicfile" + "tailscale.com/version" ) func main() { @@ -28,19 +28,13 @@ func main() { // any time. switch os.Args[1] { case "gocross-version": - bi, ok := debug.ReadBuildInfo() - if !ok { - fmt.Fprintln(os.Stderr, "failed getting build info") + commit := version.GetMeta().GitCommit + if commit == "" { + fmt.Fprintln(os.Stderr, "did not find VCS revision in build info") os.Exit(1) } - for _, s := range bi.Settings { - if s.Key == "vcs.revision" { - fmt.Println(s.Value) - os.Exit(0) - } - } - fmt.Fprintln(os.Stderr, "did not find vcs.revision in build info") - os.Exit(1) + fmt.Println(commit) + os.Exit(0) case "is-gocross": // This subcommand exits with an error code when called on a // regular go binary, so it can be used to detect when `go` is @@ -68,8 +62,13 @@ func main() { fmt.Fprintf(os.Stderr, "usage: gocross write-wrapper-script \n") os.Exit(1) } - if err := atomicfile.WriteFile(os.Args[2], wrapperScript, 0755); err != nil { - fmt.Fprintf(os.Stderr, "writing wrapper script: %v\n", err) + if err := atomicfile.WriteFile(os.Args[2], wrapperScriptBash, 0755); err != nil { + fmt.Fprintf(os.Stderr, "writing bash wrapper script: %v\n", err) + os.Exit(1) + } + psFileName := strings.TrimSuffix(os.Args[2], filepath.Ext(os.Args[2])) + ".ps1" + if err := atomicfile.WriteFile(psFileName, wrapperScriptPowerShell, 0755); err != nil { + fmt.Fprintf(os.Stderr, "writing PowerShell wrapper script: %v\n", err) os.Exit(1) } os.Exit(0) @@ -112,7 +111,10 @@ func main() { } //go:embed gocross-wrapper.sh -var wrapperScript []byte +var wrapperScriptBash []byte + +//go:embed gocross-wrapper.ps1 +var wrapperScriptPowerShell []byte func debugf(format string, args ...any) { debug := os.Getenv("GOCROSS_DEBUG") diff --git a/tool/gocross/toolchain.go b/tool/gocross/toolchain.go index f422e289e..a05a814ea 100644 --- a/tool/gocross/toolchain.go +++ b/tool/gocross/toolchain.go @@ -60,7 +60,12 @@ func getToolchain() (toolchainDir, gorootDir string, err error) { return "", "", err } - cache := filepath.Join(os.Getenv("HOME"), ".cache") + homeDir, err := os.UserHomeDir() + if err != nil { + return "", "", err + } + + cache := filepath.Join(homeDir, ".cache") toolchainDir = filepath.Join(cache, "tsgo", rev) gorootDir = filepath.Join(cache, "tsgoroot", rev)