mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-04 19:56:35 +02:00
tool: replace go.cmd with a 19KB Rust go.exe wrapper
go.cmd used cmd.exe to invoke PowerShell, which mangled arguments: cmd.exe treats ^ as an escape character (so -run "^$" became -run "$", running all tests instead of none) and = signs also caused issues in the PowerShell→cmd.exe argument passing layer. Replace it with a tiny no_std Rust binary (19KB, 32-bit x86 for universal Windows compat: x86/x64/ARM64) that directly invokes the Tailscale Go toolchain via CreateProcessW. The raw command line from GetCommandLineW is passed through to CreateProcessW with only argv[0] replaced, so arguments are never parsed or re-escaped. The binary also handles first-run toolchain download natively using curl.exe and tar.exe (both ship with Windows 10+), so PowerShell is no longer required for normal operation. The PowerShell fallback is only used for the rare TS_USE_GOCROSS=1 path. PowerShell prefers go.exe over go.cmd when resolving ./tool/go, so this is a drop-in replacement. With go.exe in place, the CI can use the natural -bench=. -benchtime=1x -run="^$" flags directly. Also removes tool/go-win.ps1 which is now unused. Updates #19255 Change-Id: I80da23285b74796e7694b89cff29a9fa0eaa6281 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
d0cd0906d5
commit
cccd5e93e9
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@ -270,13 +270,7 @@ jobs:
|
||||
- name: bench all
|
||||
if: matrix.key == 'win-bench'
|
||||
working-directory: src
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
# Don't use -run "^$" either; the ^ is cmd.exe's escape
|
||||
# character, so go.cmd's cmd.exe layer eats it, turning
|
||||
# -run "^$" into -run "$" which matches all test names.
|
||||
run: ./tool/go test ./... -bench . -benchtime 1x -run XXXXNothingXXXX
|
||||
run: ./tool/go test ./... -bench=. -benchtime=1x -run="^$"
|
||||
env:
|
||||
NOPWSHDEBUG: "true" # to quiet tool/gocross/gocross-wrapper.ps1 in CI
|
||||
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
<#
|
||||
go.ps1 – Tailscale Go toolchain fetching wrapper for Windows/PowerShell
|
||||
• Reads go.toolchain.rev one dir above this script
|
||||
• If the requested commit hash isn't cached, downloads and unpacks
|
||||
https://github.com/tailscale/go/releases/download/build-${REV}/${OS}-${ARCH}.tar.gz
|
||||
• Finally execs the toolchain's "go" binary, forwarding all args & exit-code
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]] $Args
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($env:CI -eq 'true' -and $env:NODEBUG -ne 'true') {
|
||||
$VerbosePreference = 'Continue'
|
||||
}
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..')
|
||||
$REV = (Get-Content (Join-Path $repoRoot 'go.toolchain.rev') -Raw).Trim()
|
||||
|
||||
if ([IO.Path]::IsPathRooted($REV)) {
|
||||
$toolchain = $REV
|
||||
} else {
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:TSGO_CACHE_ROOT)) {
|
||||
$cacheRoot = $env:TSGO_CACHE_ROOT
|
||||
} else {
|
||||
$cacheRoot = Join-Path $env:USERPROFILE '.cache\tsgo'
|
||||
}
|
||||
|
||||
$toolchain = Join-Path $cacheRoot $REV
|
||||
$marker = "$toolchain.extracted"
|
||||
|
||||
if (-not (Test-Path $marker)) {
|
||||
Write-Host "# Downloading Go toolchain $REV" -ForegroundColor Cyan
|
||||
if (Test-Path $toolchain) { Remove-Item -Recurse -Force $toolchain }
|
||||
|
||||
# Removing the marker file again (even though it shouldn't still exist)
|
||||
# because the equivalent Bash script also does so (to guard against
|
||||
# concurrent cache fills?).
|
||||
# TODO(bradfitz): remove this and add some proper locking instead?
|
||||
if (Test-Path $marker ) { Remove-Item -Force $marker }
|
||||
|
||||
New-Item -ItemType Directory -Path $cacheRoot -Force | Out-Null
|
||||
|
||||
$url = "https://github.com/tailscale/go/releases/download/build-$REV/windows-amd64.tar.gz"
|
||||
$tgz = "$toolchain.tar.gz"
|
||||
Invoke-WebRequest -Uri $url -OutFile $tgz -UseBasicParsing -ErrorAction Stop
|
||||
|
||||
New-Item -ItemType Directory -Path $toolchain -Force | Out-Null
|
||||
tar --strip-components=1 -xzf $tgz -C $toolchain
|
||||
Remove-Item $tgz
|
||||
Set-Content -Path $marker -Value $REV
|
||||
}
|
||||
}
|
||||
|
||||
$goExe = Join-Path $toolchain 'bin\go.exe'
|
||||
if (-not (Test-Path $goExe)) { throw "go executable not found at $goExe" }
|
||||
|
||||
& $goExe @Args
|
||||
exit $LASTEXITCODE
|
||||
|
||||
36
tool/go.cmd
36
tool/go.cmd
@ -1,36 +0,0 @@
|
||||
@echo off
|
||||
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" %*
|
||||
BIN
tool/go.exe
Executable file
BIN
tool/go.exe
Executable file
Binary file not shown.
2
tool/goexe/.cargo/config.toml
Normal file
2
tool/goexe/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[target.i686-pc-windows-gnu]
|
||||
rustflags = ["-C", "link-args=-nostartfiles -lkernel32"]
|
||||
2
tool/goexe/.gitignore
vendored
Normal file
2
tool/goexe/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target/
|
||||
/go.exe
|
||||
7
tool/goexe/Cargo.lock
generated
Normal file
7
tool/goexe/Cargo.lock
generated
Normal file
@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "go"
|
||||
version = "0.1.0"
|
||||
11
tool/goexe/Cargo.toml
Normal file
11
tool/goexe/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "go"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
28
tool/goexe/Makefile
Normal file
28
tool/goexe/Makefile
Normal file
@ -0,0 +1,28 @@
|
||||
# Builds tool/go.exe, a thin wrapper that execs the Tailscale Go
|
||||
# toolchain without going through cmd.exe (which mangles ^ and other
|
||||
# special characters in arguments).
|
||||
# See https://github.com/tailscale/tailscale/issues/19255
|
||||
#
|
||||
# Built as no_std Rust with raw Win32 API calls for minimal size (~17KB).
|
||||
# The resulting go.exe is checked into the repo at tool/go.exe.
|
||||
#
|
||||
# Built as 32-bit x86 so one binary runs on x86, x64 (via WoW64),
|
||||
# and ARM64 (via Windows x86 emulation).
|
||||
#
|
||||
# Requirements:
|
||||
# rustup target add i686-pc-windows-gnu
|
||||
# apt install gcc-mingw-w64-i686 (or equivalent)
|
||||
|
||||
RUST_TARGET = i686-pc-windows-gnu
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: go.exe
|
||||
|
||||
go.exe: src/main.rs Cargo.toml
|
||||
cargo build --release --target $(RUST_TARGET)
|
||||
cp target/$(RUST_TARGET)/release/go.exe $@
|
||||
|
||||
clean:
|
||||
rm -f go.exe
|
||||
rm -rf target
|
||||
686
tool/goexe/src/main.rs
Normal file
686
tool/goexe/src/main.rs
Normal file
@ -0,0 +1,686 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//! A thin wrapper that finds and execs the Tailscale Go toolchain without
|
||||
//! going through cmd.exe, avoiding its argument mangling (cmd.exe treats ^
|
||||
//! as an escape character, breaking -run "^$" and similar, and = signs
|
||||
//! also cause issues in PowerShell→cmd.exe argument passing).
|
||||
//! See https://github.com/tailscale/tailscale/issues/19255.
|
||||
//!
|
||||
//! This replaces tool/go.cmd. When PowerShell resolves `./tool/go`, it
|
||||
//! prefers go.exe over go.cmd, so this binary is used automatically.
|
||||
//!
|
||||
//! Built as no_std with raw Win32 API calls for minimal binary size (~17KB).
|
||||
//! Built as 32-bit x86 so one binary runs on x86, x64 (via WoW64), and
|
||||
//! ARM64 (via Windows x86 emulation).
|
||||
//!
|
||||
//! The raw command line from GetCommandLineW is passed through directly to
|
||||
//! CreateProcessW (after swapping out argv[0]), so arguments are never
|
||||
//! parsed or re-escaped, preserving them exactly as the caller specified.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![windows_subsystem = "console"]
|
||||
// Every function in this program calls raw Win32 FFI; requiring unsafe
|
||||
// blocks inside each unsafe fn would be pure noise.
|
||||
#![allow(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
use core::ptr;
|
||||
|
||||
// Win32 constants.
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights
|
||||
const GENERIC_READ: u32 = 0x80000000;
|
||||
const GENERIC_WRITE: u32 = 0x40000000;
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew (dwCreationDisposition)
|
||||
const OPEN_EXISTING: u32 = 3;
|
||||
const CREATE_ALWAYS: u32 = 2;
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew (dwShareMode)
|
||||
const FILE_SHARE_READ: u32 = 1;
|
||||
/// Returned by CreateFileW on failure.
|
||||
const INVALID_HANDLE_VALUE: isize = -1;
|
||||
/// Returned by GetFileAttributesW when the file does not exist.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesw
|
||||
const INVALID_FILE_ATTRIBUTES: u32 = 0xFFFFFFFF;
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject
|
||||
const INFINITE: u32 = 0xFFFFFFFF;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/console/getstdhandle
|
||||
const STD_INPUT_HANDLE: u32 = (-10i32) as u32;
|
||||
const STD_OUTPUT_HANDLE: u32 = (-11i32) as u32;
|
||||
const STD_ERROR_HANDLE: u32 = (-12i32) as u32;
|
||||
|
||||
/// Indicates that the hStdInput/hStdOutput/hStdError fields in STARTUPINFOW
|
||||
/// contain valid handles.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow
|
||||
const STARTF_USESTDHANDLES: u32 = 0x00000100;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info
|
||||
const PROCESSOR_ARCHITECTURE_INTEL: u16 = 0;
|
||||
const PROCESSOR_ARCHITECTURE_AMD64: u16 = 9;
|
||||
const PROCESSOR_ARCHITECTURE_ARM64: u16 = 12;
|
||||
|
||||
/// Exit code used when this wrapper panics, to distinguish from child
|
||||
/// process failures.
|
||||
const EXIT_CODE_PANIC: u32 = 0xFE;
|
||||
|
||||
// Win32 struct definitions.
|
||||
|
||||
/// STARTUPINFOW — passed to CreateProcessW to configure the child process.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow
|
||||
#[repr(C)]
|
||||
struct StartupInfoW {
|
||||
cb: u32, // Size of this struct in bytes.
|
||||
reserved: usize, // lpReserved (must be NULL).
|
||||
desktop: usize, // lpDesktop
|
||||
title: usize, // lpTitle
|
||||
x: u32, // dwX
|
||||
y: u32, // dwY
|
||||
x_size: u32, // dwXSize
|
||||
y_size: u32, // dwYSize
|
||||
x_count_chars: u32, // dwXCountChars
|
||||
y_count_chars: u32, // dwYCountChars
|
||||
fill_attribute: u32,// dwFillAttribute
|
||||
flags: u32, // dwFlags (e.g. STARTF_USESTDHANDLES)
|
||||
show_window: u16, // wShowWindow
|
||||
cb_reserved2: u16, // cbReserved2
|
||||
reserved2: usize, // lpReserved2
|
||||
std_input: isize, // hStdInput (HANDLE)
|
||||
std_output: isize, // hStdOutput (HANDLE)
|
||||
std_error: isize, // hStdError (HANDLE)
|
||||
}
|
||||
|
||||
/// PROCESS_INFORMATION — filled by CreateProcessW with handles to the new process/thread.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_information
|
||||
#[repr(C)]
|
||||
struct ProcessInformation {
|
||||
process: isize, // hProcess (HANDLE)
|
||||
thread: isize, // hThread (HANDLE)
|
||||
process_id: u32, // dwProcessId
|
||||
thread_id: u32, // dwThreadId
|
||||
}
|
||||
|
||||
/// SYSTEM_INFO — returned by GetNativeSystemInfo.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info
|
||||
#[repr(C)]
|
||||
struct SystemInfo {
|
||||
processor_architecture: u16, // wProcessorArchitecture
|
||||
_reserved: u16,
|
||||
_page_size: u32,
|
||||
_min_app_addr: usize,
|
||||
_max_app_addr: usize,
|
||||
_active_processor_mask: usize,
|
||||
_number_of_processors: u32,
|
||||
_processor_type: u32,
|
||||
_allocation_granularity: u32,
|
||||
_processor_level: u16,
|
||||
_processor_revision: u16,
|
||||
}
|
||||
|
||||
// Win32 API declarations (all from kernel32.dll unless noted).
|
||||
|
||||
unsafe extern "system" {
|
||||
/// Returns the fully qualified path of the running executable.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew
|
||||
fn GetModuleFileNameW(module: isize, filename: *mut u16, size: u32) -> u32;
|
||||
|
||||
/// Opens or creates a file, returning a HANDLE.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
|
||||
fn CreateFileW(
|
||||
name: *const u16,
|
||||
access: u32,
|
||||
share: u32,
|
||||
security: usize,
|
||||
disposition: u32,
|
||||
flags: u32,
|
||||
template: usize,
|
||||
) -> isize;
|
||||
|
||||
/// Reads bytes from a file handle.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
|
||||
fn ReadFile(
|
||||
file: isize,
|
||||
buffer: *mut u8,
|
||||
to_read: u32,
|
||||
read: *mut u32,
|
||||
overlapped: usize,
|
||||
) -> i32;
|
||||
|
||||
/// Closes a kernel object handle.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle
|
||||
fn CloseHandle(handle: isize) -> i32;
|
||||
|
||||
/// Returns file attributes, or INVALID_FILE_ATTRIBUTES if not found.
|
||||
/// Used here as a lightweight file-existence check.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesw
|
||||
fn GetFileAttributesW(name: *const u16) -> u32;
|
||||
|
||||
/// Retrieves the value of an environment variable.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getenvironmentvariablew
|
||||
fn GetEnvironmentVariableW(name: *const u16, buffer: *mut u16, size: u32) -> u32;
|
||||
|
||||
/// Sets or deletes an environment variable (pass null value to delete).
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew
|
||||
fn SetEnvironmentVariableW(name: *const u16, value: *const u16) -> i32;
|
||||
|
||||
/// Creates a new process and its primary thread.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
|
||||
fn CreateProcessW(
|
||||
app: *const u16,
|
||||
cmd: *mut u16,
|
||||
proc_attr: usize,
|
||||
thread_attr: usize,
|
||||
inherit: i32,
|
||||
flags: u32,
|
||||
env: usize,
|
||||
dir: usize,
|
||||
startup: *const StartupInfoW,
|
||||
info: *mut ProcessInformation,
|
||||
) -> i32;
|
||||
|
||||
/// Waits until a handle is signaled (process exits) or timeout elapses.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject
|
||||
fn WaitForSingleObject(handle: isize, ms: u32) -> u32;
|
||||
|
||||
/// Retrieves the exit code of a process.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess
|
||||
fn GetExitCodeProcess(process: isize, code: *mut u32) -> i32;
|
||||
|
||||
/// Terminates the calling process with the given exit code.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess
|
||||
fn ExitProcess(code: u32) -> !;
|
||||
|
||||
/// Returns a handle to stdin, stdout, or stderr.
|
||||
/// https://learn.microsoft.com/en-us/windows/console/getstdhandle
|
||||
fn GetStdHandle(id: u32) -> isize;
|
||||
|
||||
/// Returns a pointer to the command-line string for the current process.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getcommandlinew
|
||||
fn GetCommandLineW() -> *const u16;
|
||||
|
||||
/// Writes bytes to a file handle (used here for stderr output).
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
|
||||
fn WriteFile(
|
||||
file: isize,
|
||||
buffer: *const u8,
|
||||
to_write: u32,
|
||||
written: *mut u32,
|
||||
overlapped: usize,
|
||||
) -> i32;
|
||||
|
||||
/// Creates a directory.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectoryw
|
||||
fn CreateDirectoryW(path: *const u16, security: usize) -> i32;
|
||||
|
||||
/// Deletes a file.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew
|
||||
fn DeleteFileW(path: *const u16) -> i32;
|
||||
|
||||
/// Returns system info including processor architecture, using the
|
||||
/// native architecture even when called from a WoW64 process.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getnativesysteminfo
|
||||
fn GetNativeSystemInfo(info: *mut SystemInfo);
|
||||
}
|
||||
|
||||
// A fixed-capacity UTF-16 buffer for building null-terminated wide strings
|
||||
// to pass to Win32 APIs. All Win32-facing methods automatically null-terminate.
|
||||
//
|
||||
// Callers push ASCII (&[u8]) or wide (&WBuf) content; the buffer handles
|
||||
// the ASCII-to-UTF-16 widening internally, keeping encoding concerns in
|
||||
// one place.
|
||||
|
||||
struct WBuf<const N: usize> {
|
||||
buf: [u16; N],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> WBuf<N> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
buf: [0; N],
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Null-terminated pointer for Win32 APIs.
|
||||
fn as_ptr(&mut self) -> *const u16 {
|
||||
self.buf[self.len] = 0;
|
||||
self.buf.as_ptr()
|
||||
}
|
||||
|
||||
/// Mutable null-terminated pointer (for CreateProcessW's lpCommandLine).
|
||||
fn as_mut_ptr(&mut self) -> *mut u16 {
|
||||
self.buf[self.len] = 0;
|
||||
self.buf.as_mut_ptr()
|
||||
}
|
||||
|
||||
/// Append ASCII bytes, widening each byte to UTF-16.
|
||||
fn push_ascii(&mut self, s: &[u8]) -> &mut Self {
|
||||
for &b in s {
|
||||
self.buf[self.len] = b as u16;
|
||||
self.len += 1;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Append the contents of another WBuf.
|
||||
fn push_wbuf<const M: usize>(&mut self, other: &WBuf<M>) -> &mut Self {
|
||||
self.buf[self.len..self.len + other.len].copy_from_slice(&other.buf[..other.len]);
|
||||
self.len += other.len;
|
||||
self
|
||||
}
|
||||
|
||||
/// Append raw UTF-16 content from a pointer until null terminator.
|
||||
/// Used for appending the tail of GetCommandLineW.
|
||||
unsafe fn push_ptr(&mut self, mut p: *const u16) -> &mut Self {
|
||||
loop {
|
||||
let c = *p;
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
self.buf[self.len] = c;
|
||||
self.len += 1;
|
||||
p = p.add(1);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Find the last path separator (\ or /) and truncate to it,
|
||||
/// effectively navigating to the parent directory.
|
||||
fn pop_path_component(&mut self) -> bool {
|
||||
let mut i = self.len;
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
if self.buf[i] == b'\\' as u16 || self.buf[i] == b'/' as u16 {
|
||||
self.len = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether a file exists at "<self>\<suffix>".
|
||||
unsafe fn file_exists_with(&mut self, suffix: &[u8]) -> bool {
|
||||
let saved = self.len;
|
||||
self.push_ascii(suffix);
|
||||
let result = GetFileAttributesW(self.as_ptr()) != INVALID_FILE_ATTRIBUTES;
|
||||
self.len = saved;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an environment variable equals an expected ASCII value.
|
||||
/// Neither name nor val should include a null terminator.
|
||||
unsafe fn env_eq(name: &[u8], val: &[u8]) -> bool {
|
||||
let mut name_w = WBuf::<64>::new();
|
||||
name_w.push_ascii(name);
|
||||
let mut buf = [0u16; 64];
|
||||
let n = GetEnvironmentVariableW(name_w.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) as usize;
|
||||
if n != val.len() {
|
||||
return false;
|
||||
}
|
||||
for (i, &b) in val.iter().enumerate() {
|
||||
if buf[i] != b as u16 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Get an environment variable's value into a WBuf.
|
||||
/// Returns the number of characters written (0 if not set).
|
||||
unsafe fn get_env<const N: usize>(name: &[u8], dst: &mut WBuf<N>) -> usize {
|
||||
let mut name_w = WBuf::<64>::new();
|
||||
name_w.push_ascii(name);
|
||||
let n = GetEnvironmentVariableW(
|
||||
name_w.as_ptr(),
|
||||
dst.buf.as_mut_ptr(),
|
||||
dst.buf.len() as u32,
|
||||
) as usize;
|
||||
dst.len = n;
|
||||
n
|
||||
}
|
||||
|
||||
/// Unset an environment variable.
|
||||
unsafe fn unset_env(name: &[u8]) {
|
||||
let mut name_w = WBuf::<64>::new();
|
||||
name_w.push_ascii(name);
|
||||
SetEnvironmentVariableW(name_w.as_ptr(), ptr::null());
|
||||
}
|
||||
|
||||
/// C runtime entry point for MinGW/MSVC. Called before main() would be.
|
||||
/// We use #[no_main] so we define this directly.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn mainCRTStartup() -> ! {
|
||||
unsafe { main_impl() }
|
||||
}
|
||||
|
||||
unsafe fn main_impl() -> ! {
|
||||
// Get our own exe path, e.g. "C:\Users\...\tailscale\tool\go.exe".
|
||||
let mut exe = WBuf::<4096>::new();
|
||||
exe.len = GetModuleFileNameW(0, exe.buf.as_mut_ptr(), exe.buf.len() as u32) as usize;
|
||||
if exe.len == 0 {
|
||||
die(b"GetModuleFileNameW failed\n");
|
||||
}
|
||||
|
||||
// Walk up directories from our exe location to find the repo root,
|
||||
// identified by the presence of "go.toolchain.rev".
|
||||
exe.pop_path_component(); // strip filename, e.g. "...\tool"
|
||||
let repo_root = loop {
|
||||
if !exe.file_exists_with(b"\\go.toolchain.rev") {
|
||||
if !exe.pop_path_component() {
|
||||
die(b"could not find go.toolchain.rev\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break WBuf::<4096> {
|
||||
buf: exe.buf,
|
||||
len: exe.len,
|
||||
};
|
||||
};
|
||||
|
||||
// Read the toolchain revision hash from go.toolchain.rev (or
|
||||
// go.toolchain.next.rev if TS_GO_NEXT=1).
|
||||
let mut rev_path = WBuf::<4096>::new();
|
||||
rev_path.push_wbuf(&repo_root);
|
||||
if env_eq(b"TS_GO_NEXT", b"1") {
|
||||
rev_path.push_ascii(b"\\go.toolchain.next.rev");
|
||||
} else {
|
||||
rev_path.push_ascii(b"\\go.toolchain.rev");
|
||||
}
|
||||
|
||||
let mut rev_buf = [0u8; 256];
|
||||
let rev = read_file_trimmed(&mut rev_path, &mut rev_buf);
|
||||
|
||||
// Build the toolchain path. The rev is normally a git hash, and
|
||||
// the toolchain lives at %USERPROFILE%\.cache\tsgo\<hash>.
|
||||
// If the rev starts with "/" or "\" it's an absolute path to a
|
||||
// local toolchain (used for testing).
|
||||
let mut toolchain = WBuf::<4096>::new();
|
||||
if rev.first() == Some(&b'/') || rev.first() == Some(&b'\\') {
|
||||
toolchain.push_ascii(rev);
|
||||
} else {
|
||||
if get_env(b"USERPROFILE", &mut toolchain) == 0 {
|
||||
die(b"USERPROFILE not set\n");
|
||||
}
|
||||
toolchain.push_ascii(b"\\.cache\\tsgo\\");
|
||||
toolchain.push_ascii(rev);
|
||||
}
|
||||
|
||||
// If the toolchain hasn't been downloaded yet (no ".extracted" marker),
|
||||
// download it. For TS_USE_GOCROSS=1, fall back to PowerShell since
|
||||
// that path also needs to build gocross.
|
||||
if !toolchain.file_exists_with(b".extracted") {
|
||||
if env_eq(b"TS_USE_GOCROSS", b"1") {
|
||||
fallback_pwsh(&repo_root);
|
||||
}
|
||||
download_toolchain(&toolchain, rev);
|
||||
}
|
||||
|
||||
// Build the path to the real go.exe binary inside the toolchain,
|
||||
// or to gocross.exe if TS_USE_GOCROSS=1.
|
||||
let mut go_exe = WBuf::<4096>::new();
|
||||
if env_eq(b"TS_USE_GOCROSS", b"1") {
|
||||
go_exe.push_wbuf(&repo_root).push_ascii(b"\\gocross.exe");
|
||||
} else {
|
||||
go_exe.push_wbuf(&toolchain).push_ascii(b"\\bin\\go.exe");
|
||||
}
|
||||
|
||||
// Unset GOROOT to avoid breaking builds that depend on our Go
|
||||
// fork's patches (e.g. net/). The Go toolchain sets GOROOT
|
||||
// internally from its own location.
|
||||
unset_env(b"GOROOT");
|
||||
|
||||
// Build the new command line by replacing argv[0] with the real
|
||||
// go.exe path. We take the raw command line from GetCommandLineW
|
||||
// and pass the args portion through untouched — no parsing or
|
||||
// re-escaping — so special characters like ^ and = survive intact.
|
||||
let raw_cmd = GetCommandLineW();
|
||||
let args_tail = skip_argv0(raw_cmd);
|
||||
|
||||
let mut cmd = WBuf::<32768>::new();
|
||||
cmd.push_ascii(b"\"");
|
||||
cmd.push_wbuf(&go_exe);
|
||||
cmd.push_ascii(b"\"");
|
||||
cmd.push_ptr(args_tail);
|
||||
|
||||
// Exec: create the child process, wait for it, and exit with its code.
|
||||
let code = run_and_wait(go_exe.as_ptr(), &mut cmd, ptr::null());
|
||||
ExitProcess(code);
|
||||
}
|
||||
|
||||
/// Download the Go toolchain tarball from GitHub and extract it.
|
||||
/// Uses curl.exe and tar.exe which ship with Windows 10+.
|
||||
unsafe fn download_toolchain(toolchain: &WBuf<4096>, rev: &[u8]) {
|
||||
stderr(b"# Downloading Go toolchain ");
|
||||
stderr(rev);
|
||||
stderr(b"\n");
|
||||
|
||||
// Create parent directories (%USERPROFILE%\.cache\tsgo).
|
||||
// CreateDirectoryW is fine if the dir already exists.
|
||||
let mut dir = WBuf::<4096>::new();
|
||||
get_env(b"USERPROFILE", &mut dir);
|
||||
dir.push_ascii(b"\\.cache");
|
||||
CreateDirectoryW(dir.as_ptr(), 0);
|
||||
dir.push_ascii(b"\\tsgo");
|
||||
CreateDirectoryW(dir.as_ptr(), 0);
|
||||
|
||||
// Create the toolchain directory itself.
|
||||
let mut tc_dir = WBuf::<4096>::new();
|
||||
tc_dir.push_wbuf(toolchain);
|
||||
CreateDirectoryW(tc_dir.as_ptr(), 0);
|
||||
|
||||
// Detect host architecture via GetNativeSystemInfo (gives real arch
|
||||
// even from a WoW64 32-bit process).
|
||||
let mut si: SystemInfo = core::mem::zeroed();
|
||||
GetNativeSystemInfo(&mut si);
|
||||
let arch: &[u8] = match si.processor_architecture {
|
||||
PROCESSOR_ARCHITECTURE_AMD64 => b"amd64",
|
||||
PROCESSOR_ARCHITECTURE_ARM64 => b"arm64",
|
||||
PROCESSOR_ARCHITECTURE_INTEL => b"386",
|
||||
_ => die(b"unsupported architecture\n"),
|
||||
};
|
||||
|
||||
// Build tarball path: <toolchain>.tar.gz
|
||||
let mut tgz = WBuf::<4096>::new();
|
||||
tgz.push_wbuf(toolchain).push_ascii(b".tar.gz");
|
||||
|
||||
// Build URL:
|
||||
// https://github.com/tailscale/go/releases/download/build-<rev>/windows-<arch>.tar.gz
|
||||
let mut url = [0u8; 512];
|
||||
let mut u = 0;
|
||||
for part in [
|
||||
b"https://github.com/tailscale/go/releases/download/build-" as &[u8],
|
||||
rev,
|
||||
b"/windows-",
|
||||
arch,
|
||||
b".tar.gz",
|
||||
] {
|
||||
url[u..u + part.len()].copy_from_slice(part);
|
||||
u += part.len();
|
||||
}
|
||||
|
||||
// Run: curl.exe -fsSL -o <tgz> <url>
|
||||
let mut cmd = WBuf::<32768>::new();
|
||||
cmd.push_ascii(b"curl.exe -fsSL -o \"");
|
||||
cmd.push_wbuf(&tgz);
|
||||
cmd.push_ascii(b"\" ");
|
||||
cmd.push_ascii(&url[..u]);
|
||||
|
||||
let code = run_and_wait(ptr::null(), &mut cmd, ptr::null());
|
||||
if code != 0 {
|
||||
die(b"curl failed to download Go toolchain\n");
|
||||
}
|
||||
|
||||
// Run: tar.exe --strip-components=1 -xf <tgz>
|
||||
// with working directory set to the toolchain dir.
|
||||
let mut cmd = WBuf::<32768>::new();
|
||||
cmd.push_ascii(b"tar.exe --strip-components=1 -xf \"");
|
||||
cmd.push_wbuf(&tgz);
|
||||
cmd.push_ascii(b"\"");
|
||||
|
||||
let code = run_and_wait(ptr::null(), &mut cmd, tc_dir.as_ptr());
|
||||
if code != 0 {
|
||||
die(b"tar failed to extract Go toolchain\n");
|
||||
}
|
||||
|
||||
// Write the .extracted marker file.
|
||||
let mut marker = WBuf::<4096>::new();
|
||||
marker.push_wbuf(toolchain).push_ascii(b".extracted");
|
||||
let fh = CreateFileW(marker.as_ptr(), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);
|
||||
if fh != INVALID_HANDLE_VALUE {
|
||||
let mut written: u32 = 0;
|
||||
WriteFile(fh, rev.as_ptr(), rev.len() as u32, &mut written, 0);
|
||||
CloseHandle(fh);
|
||||
}
|
||||
|
||||
// Clean up the tarball.
|
||||
DeleteFileW(tgz.as_ptr());
|
||||
}
|
||||
|
||||
/// Spawn a child process, wait for it, and return its exit code.
|
||||
/// If app is null, CreateProcessW searches PATH using the command line.
|
||||
/// If dir is null, the child inherits the current directory.
|
||||
unsafe fn run_and_wait(app: *const u16, cmd: &mut WBuf<32768>, dir: *const u16) -> u32 {
|
||||
let si = StartupInfoW {
|
||||
cb: core::mem::size_of::<StartupInfoW>() as u32,
|
||||
reserved: 0,
|
||||
desktop: 0,
|
||||
title: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
x_size: 0,
|
||||
y_size: 0,
|
||||
x_count_chars: 0,
|
||||
y_count_chars: 0,
|
||||
fill_attribute: 0,
|
||||
flags: STARTF_USESTDHANDLES,
|
||||
show_window: 0,
|
||||
cb_reserved2: 0,
|
||||
reserved2: 0,
|
||||
std_input: GetStdHandle(STD_INPUT_HANDLE),
|
||||
std_output: GetStdHandle(STD_OUTPUT_HANDLE),
|
||||
std_error: GetStdHandle(STD_ERROR_HANDLE),
|
||||
};
|
||||
let mut pi = ProcessInformation {
|
||||
process: 0,
|
||||
thread: 0,
|
||||
process_id: 0,
|
||||
thread_id: 0,
|
||||
};
|
||||
|
||||
if CreateProcessW(
|
||||
app,
|
||||
cmd.as_mut_ptr(),
|
||||
0,
|
||||
0,
|
||||
1, // bInheritHandles = TRUE
|
||||
0,
|
||||
0,
|
||||
dir as usize,
|
||||
&si,
|
||||
&mut pi,
|
||||
) == 0
|
||||
{
|
||||
die(b"CreateProcess failed\n");
|
||||
}
|
||||
|
||||
WaitForSingleObject(pi.process, INFINITE);
|
||||
let mut code: u32 = 1;
|
||||
GetExitCodeProcess(pi.process, &mut code);
|
||||
CloseHandle(pi.process);
|
||||
CloseHandle(pi.thread);
|
||||
code
|
||||
}
|
||||
|
||||
/// Fall back to PowerShell for the full bootstrap flow (downloading the
|
||||
/// toolchain, optionally building gocross, and then running go):
|
||||
/// pwsh -NoProfile -ExecutionPolicy Bypass "<repo>\tool\gocross\gocross-wrapper.ps1" <args...>
|
||||
unsafe fn fallback_pwsh(repo_root: &WBuf<4096>) -> ! {
|
||||
let raw_cmd = GetCommandLineW();
|
||||
let args_tail = skip_argv0(raw_cmd);
|
||||
|
||||
let mut cmd = WBuf::<32768>::new();
|
||||
cmd.push_ascii(b"pwsh -NoProfile -ExecutionPolicy Bypass \"");
|
||||
cmd.push_wbuf(repo_root);
|
||||
cmd.push_ascii(b"\\tool\\gocross\\gocross-wrapper.ps1\"");
|
||||
cmd.push_ptr(args_tail);
|
||||
|
||||
// Pass null for lpApplicationName so CreateProcessW searches PATH for "pwsh".
|
||||
let code = run_and_wait(ptr::null(), &mut cmd, ptr::null());
|
||||
ExitProcess(code);
|
||||
}
|
||||
|
||||
/// Read an entire file (expected to be small ASCII, e.g. a git hash) into buf,
|
||||
/// and return the trimmed content as a byte slice.
|
||||
unsafe fn read_file_trimmed<'a, const N: usize>(
|
||||
path: &mut WBuf<N>,
|
||||
buf: &'a mut [u8],
|
||||
) -> &'a [u8] {
|
||||
let h = CreateFileW(
|
||||
path.as_ptr(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if h == INVALID_HANDLE_VALUE {
|
||||
die(b"cannot open go.toolchain.rev\n");
|
||||
}
|
||||
let mut n: u32 = 0;
|
||||
ReadFile(h, buf.as_mut_ptr(), buf.len() as u32, &mut n, 0);
|
||||
CloseHandle(h);
|
||||
|
||||
let s = &buf[..n as usize];
|
||||
let start = s.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(s.len());
|
||||
let end = s.iter().rposition(|b| !b.is_ascii_whitespace()).map_or(start, |i| i + 1);
|
||||
&s[start..end]
|
||||
}
|
||||
|
||||
/// Advance past argv[0] in a raw Windows command line string.
|
||||
///
|
||||
/// Windows command lines are a single string; argv[0] may be quoted
|
||||
/// (if the path contains spaces) or unquoted.
|
||||
/// See https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments
|
||||
unsafe fn skip_argv0(cmd: *const u16) -> *const u16 {
|
||||
let mut p = cmd;
|
||||
if *p == b'"' as u16 {
|
||||
// Quoted argv[0]: advance past closing quote.
|
||||
p = p.add(1);
|
||||
while *p != 0 && *p != b'"' as u16 {
|
||||
p = p.add(1);
|
||||
}
|
||||
if *p == b'"' as u16 {
|
||||
p = p.add(1);
|
||||
}
|
||||
} else {
|
||||
// Unquoted argv[0]: advance to first whitespace.
|
||||
while *p != 0 && *p != b' ' as u16 && *p != b'\t' as u16 {
|
||||
p = p.add(1);
|
||||
}
|
||||
}
|
||||
// Return pointer to the rest (typically starts with a space before
|
||||
// the first real argument, or is empty if there are no arguments).
|
||||
p
|
||||
}
|
||||
|
||||
/// Write bytes to stderr.
|
||||
unsafe fn stderr(msg: &[u8]) {
|
||||
let h = GetStdHandle(STD_ERROR_HANDLE);
|
||||
let mut n: u32 = 0;
|
||||
WriteFile(h, msg.as_ptr(), msg.len() as u32, &mut n, 0);
|
||||
}
|
||||
|
||||
/// Write an error message to stderr and terminate with exit code 1.
|
||||
unsafe fn die(msg: &[u8]) -> ! {
|
||||
stderr(b"tool/go: ");
|
||||
stderr(msg);
|
||||
ExitProcess(1);
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_: &core::panic::PanicInfo) -> ! {
|
||||
unsafe { ExitProcess(EXIT_CODE_PANIC) }
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user