From 8a9840d6a8c109f4867277fb53a41ef4257140b3 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 6 Apr 2026 03:41:47 +0000 Subject: [PATCH] tool: replace go.cmd with a 19KB Rust go.exe wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/test.yml | 8 +- tool/go-win.ps1 | 64 ---- tool/go.cmd | 36 -- tool/go.exe | Bin 0 -> 18944 bytes tool/go.exe.README.txt | 20 + tool/goexe/.cargo/config.toml | 2 + tool/goexe/.gitignore | 2 + tool/goexe/Cargo.lock | 7 + tool/goexe/Cargo.toml | 11 + tool/goexe/Makefile | 28 ++ tool/goexe/src/main.rs | 686 ++++++++++++++++++++++++++++++++++ 11 files changed, 757 insertions(+), 107 deletions(-) delete mode 100644 tool/go-win.ps1 delete mode 100644 tool/go.cmd create mode 100755 tool/go.exe create mode 100644 tool/go.exe.README.txt create mode 100644 tool/goexe/.cargo/config.toml create mode 100644 tool/goexe/.gitignore create mode 100644 tool/goexe/Cargo.lock create mode 100644 tool/goexe/Cargo.toml create mode 100644 tool/goexe/Makefile create mode 100644 tool/goexe/src/main.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f73e8178d..38ebd1291 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/tool/go-win.ps1 b/tool/go-win.ps1 deleted file mode 100644 index 49313ffba..000000000 --- a/tool/go-win.ps1 +++ /dev/null @@ -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 - diff --git a/tool/go.cmd b/tool/go.cmd deleted file mode 100644 index b7b5d0483..000000000 --- a/tool/go.cmd +++ /dev/null @@ -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" %* diff --git a/tool/go.exe b/tool/go.exe new file mode 100755 index 0000000000000000000000000000000000000000..39e90fb9a1d5ccd3422bc58ed671dec77b7279e5 GIT binary patch literal 18944 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 O%R5;7 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 { + buf: [u16; N], + len: usize, +} + +impl WBuf { + 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(&mut self, other: &WBuf) -> &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 "\". + 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(name: &[u8], dst: &mut WBuf) -> 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\. + // 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: .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-/windows-.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 + 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 + // 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::() 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 "\tool\gocross\gocross-wrapper.ps1" +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, + 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) } +}