mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-23 11:27:29 +00:00
tool, tool/gocross: update gocross to support building natively on Windows and add a PowerShell Core wrapper script
gocross-wrapper.ps1 is a PowerShell core script that is essentially a straight port of gocross-wrapper.sh. It requires PowerShell 7.4, which is the latest LTS release of PSCore. Why use PowerShell Core instead of Windows PowerShell? Essentially because the former is much better to script with and is the edition that is currently maintained. Because we're using PowerShell Core, but many people will be running scripts from a machine that only has Windows PowerShell, go.cmd has been updated to prompt the user for PowerShell core installation if necessary. gocross-wrapper.sh has also been updated to utilize the PSCore script when running under cygwin or msys. gocross itself required a couple of updates: We update gocross to output the PowerShell Core wrapper alongside the bash wrapper, which will propagate the revised scripts to other repos as necessary. We also fix a couple of things in gocross that didn't work on Windows: we change the toolchain resolution code to use os.UserHomeDir instead of directly referencing the HOME environment variable, and we fix a bug in the way arguments were being passed into exec.Command on non-Unix systems. Updates https://github.com/tailscale/corp/issues/29940 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
This commit is contained in:
36
tool/go.cmd
36
tool/go.cmd
@@ -1,2 +1,36 @@
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0go-win.ps1" %*
|
||||
rem Checking for PowerShell Core using PowerShell for Windows...
|
||||
powershell -NoProfile -NonInteractive -Command "& {Get-Command -Name pwsh -ErrorAction Stop}" > NUL
|
||||
if ERRORLEVEL 1 (
|
||||
rem Ask the user whether they should install the dependencies. Note that this
|
||||
rem code path never runs in CI because pwsh is always explicitly installed.
|
||||
|
||||
rem Time out after 5 minutes, defaulting to 'N'
|
||||
choice /c yn /t 300 /d n /m "PowerShell Core is required. Install now"
|
||||
if ERRORLEVEL 2 (
|
||||
echo Aborting due to unmet dependencies.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rem Check for a .NET Core runtime using PowerShell for Windows...
|
||||
powershell -NoProfile -NonInteractive -Command "& {if (-not (dotnet --list-runtimes | Select-String 'Microsoft\.NETCore\.App' -Quiet)) {exit 1}}" > NUL
|
||||
rem Install .NET Core if missing to provide PowerShell Core's runtime library.
|
||||
if ERRORLEVEL 1 (
|
||||
rem Time out after 5 minutes, defaulting to 'N'
|
||||
choice /c yn /t 300 /d n /m "PowerShell Core requires .NET Core for its runtime library. Install now"
|
||||
if ERRORLEVEL 2 (
|
||||
echo Aborting due to unmet dependencies.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
winget install --accept-package-agreements --id Microsoft.DotNet.Runtime.8 -e --source winget
|
||||
)
|
||||
|
||||
rem Now install PowerShell Core.
|
||||
winget install --accept-package-agreements --id Microsoft.PowerShell -e --source winget
|
||||
if ERRORLEVEL 0 echo Please re-run this script within a new console session to pick up PATH changes.
|
||||
rem Either way we didn't build, so return 1.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass "%~dp0..\tool\gocross\gocross-wrapper.ps1" %*
|
||||
|
@@ -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
|
||||
|
220
tool/gocross/gocross-wrapper.ps1
Normal file
220
tool/gocross/gocross-wrapper.ps1
Normal file
@@ -0,0 +1,220 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#Requires -Version 7.4
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
if (($Env:CI -eq 'true') -and ($Env:NOPWSHDEBUG -ne 'true')) {
|
||||
Set-PSDebug -Trace 1
|
||||
}
|
||||
|
||||
<#
|
||||
.DESCRIPTION
|
||||
Copies the script's $args variable into an array, which is easier to work with
|
||||
when preparing to start child processes.
|
||||
#>
|
||||
function Copy-ScriptArgs {
|
||||
$list = [System.Collections.Generic.List[string]]::new($Script:args.Count)
|
||||
foreach ($arg in $Script: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 | Out-Null
|
||||
Remove-Item -Force -Recurse -LiteralPath $toolchain -ErrorAction SilentlyContinue
|
||||
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 | Out-Null
|
||||
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-ScriptArgs
|
||||
|
||||
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
|
@@ -15,6 +15,12 @@ if [[ "${CI:-}" == "true" && "${NOBASHDEBUG:-}" != "true" ]]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
if [[ "${OSTYPE:-}" == "cygwin" || "${OSTYPE:-}" == "msys" ]]; then
|
||||
hash pwsh 2>/dev/null || { echo >&2 "This operation requires PowerShell Core."; exit 1; }
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass "${BASH_SOURCE%/*}/gocross-wrapper.ps1" "$@"
|
||||
exit
|
||||
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
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
)
|
||||
@@ -68,8 +69,13 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "usage: gocross write-wrapper-script <path>\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 +118,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")
|
||||
|
@@ -21,7 +21,7 @@ func TestGocrossWrapper(t *testing.T) {
|
||||
t.Fatalf("gocross-wrapper.sh failed: %v\n%s", err, out)
|
||||
}
|
||||
if i > 0 && !strings.Contains(string(out), "gocross_ok=1\n") {
|
||||
t.Errorf("expected to find 'gocross-ok=1'; got output:\n%s", out)
|
||||
t.Errorf("expected to find 'gocross_ok=1'; got output:\n%s", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
tool/gocross/gocross_wrapper_windows_test.go
Normal file
25
tool/gocross/gocross_wrapper_windows_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGocrossWrapper(t *testing.T) {
|
||||
for i := range 2 { // once to build gocross; second to test it's cached
|
||||
cmd := exec.Command("pwsh", "-NoProfile", "-ExecutionPolicy", "Bypass", ".\\gocross-wrapper.ps1", "version")
|
||||
cmd.Env = append(os.Environ(), "CI=true", "NOPWSHDEBUG=false", "TS_USE_GOCROSS=1") // for Set-PSDebug verbosity
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("gocross-wrapper.ps1 failed: %v\n%s", err, out)
|
||||
}
|
||||
if i > 0 && !strings.Contains(string(out), "$gocrossOk = $true\r\n") {
|
||||
t.Errorf("expected to find '$gocrossOk = $true'; got output:\n%s", out)
|
||||
}
|
||||
}
|
||||
}
|
@@ -60,7 +60,15 @@ 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
|
||||
}
|
||||
|
||||
// We use ".cache" instead of os.UserCacheDir for legacy reasons and we
|
||||
// don't want to break that on platforms where the latter returns a different
|
||||
// result.
|
||||
cache := filepath.Join(homeDir, ".cache")
|
||||
toolchainDir = filepath.Join(cache, "tsgo", rev)
|
||||
gorootDir = filepath.Join(cache, "tsgoroot", rev)
|
||||
|
||||
|
Reference in New Issue
Block a user