GoWinBridge
February 13, 2026 · View on GitHub
A Go library and CLI for executing Windows binaries from WSL with pure Go path translation, codepage sanitization, interactive PTY support, environment bridging, shim generation, and bounded concurrency.
Why?
Running Windows binaries from WSL via os/exec works, but lacks:
- Path translation — Linux paths like
./file.txtdon't resolve on the Windows side - Encoding handling — Windows tools output CP1252/UTF-16LE, Go assumes UTF-8
- Interactive support — REPLs like
python.exeand TUI apps break with buffered I/O - Environment bridging — Windows processes don't inherit Linux env vars without
WSLENV - Signal propagation — Ctrl+C in your terminal won't kill the Windows child process
- Concurrency control — Spawning dozens of Windows processes simultaneously is expensive
GoWinBridge solves all of these behind a clean Go API and a ready-to-use CLI.
Architecture
cmd/winrun/ CLI entry point (flags, signal handling, shim subcommand)
│
├── pkg/workerpool/ Bounded worker pool (configurable concurrency)
│ │
│ └── pkg/bridge/ Core executor
│ ├── exec.go CommandContext, .exe resolution, buffered + interactive modes
│ ├── encoding.go CP1252/UTF-16LE/BE → UTF-8 decoder middleware
│ ├── env.go WSLENV formatting with value-based heuristics
│ └── config.go CommandConfig / Output types
│
└── internal/wsl/ WSL detection & path translation
├── detect.go WSL1 vs WSL2 detection (cached singleton)
└── path.go Pure Go path resolver (/proc/mounts + string manipulation)
Installation
One-line Installer (Recommended)
curl -sL https://raw.githubusercontent.com/sibikrish3000/gowinbridge/main/install.sh | sh
From Source
# Clone
git clone https://github.com/sibikrish3000/gowinbridge.git
cd gowinbridge
# Build the CLI
go build -o winrun ./cmd/winrun
# Or install via Go
go install github.com/sibikrish3000/gowinbridge/cmd/winrun@latest
CLI Usage
# Basic execution
winrun -- cmd.exe /c echo hello
# Auto-convert Linux paths to Windows paths
winrun --convert-paths -- cmd.exe /c type ./go.mod
# Handle Windows codepage output (legacy tools outputting CP1252)
winrun --encoding cp1252 -- cmd.exe /c chcp
# Interactive mode for REPLs (auto-detected for python, node, mysql, etc.)
winrun --interactive -- python.exe
# Environment variable tunneling (flags auto-detected from values)
winrun --env MY_VAR=hello --env MY_PATH=/home/user --tunnel-env -- cmd.exe /c echo %MY_VAR%
# Concurrent execution with timeout
winrun --concurrency 4 --timeout 30s -- powershell.exe -Command Get-Process
# Version info
winrun --version
Flags
| Flag | Default | Description |
|---|---|---|
--concurrency N | NumCPU | Max concurrent Windows process executions |
--convert-paths | false | Auto-detect and convert file path arguments to Windows format |
--encoding ENC | "" (UTF-8) | Output encoding: utf8, cp1252, utf16le, utf16be, auto |
--interactive | false | Run in interactive/PTY mode (bypasses output capture) |
--env KEY=VAL | — | Set environment variable (repeatable) |
--tunnel-env | false | Enable WSLENV tunneling for --env vars |
--timeout DURATION | 0 (none) | Max execution time (e.g., 30s, 5m) |
--version | — | Print version information and exit |
Shim Generator
Create transparent wrapper scripts so Windows tools behave like native Linux binaries:
# Install a shim — creates ~/.local/bin/docker that proxies to docker.exe
winrun shim install docker.exe --as docker
# Install with custom bin directory
winrun shim install code.exe --as code --bin-dir /usr/local/bin
# Auto-derive name from binary (docker.exe → docker)
winrun shim install docker.exe
# List installed shims
winrun shim list
# Remove a shim
winrun shim remove docker
Each shim is a shell script:
#!/bin/sh
# Generated by winrun shim install
# Binary: docker.exe
exec winrun --convert-paths -- docker.exe "$@"
Library Usage
Basic Execution
package main
import (
"context"
"fmt"
"log"
"github.com/sibikrish3000/gowinbridge/pkg/bridge"
)
func main() {
output, err := bridge.Execute(context.Background(), bridge.CommandConfig{
Command: "cmd.exe",
Args: []string{"/c", "echo", "hello from Windows"},
ConvertPaths: false,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(output.Stdout)
// Output: hello from Windows
}
With Path Translation & Environment Tunneling
output, err := bridge.Execute(ctx, bridge.CommandConfig{
Command: "cmd.exe",
Args: []string{"/c", "type", "./myfile.txt"},
ConvertPaths: true, // ./myfile.txt → C:\Users\...\myfile.txt
Env: map[string]string{
"MY_VAR": "value", // auto-detected as /u (plain string)
"MY_PATH": "/home/user", // auto-detected as /p (path)
},
EnvTunneling: true, // Generates WSLENV=MY_PATH/p:MY_VAR/u
Timeout: 30 * time.Second,
})
With Encoding (Legacy Windows Tools)
output, err := bridge.Execute(ctx, bridge.CommandConfig{
Command: "legacy_tool.exe",
Args: []string{"/report"},
Encoding: "cp1252", // Decode CP1252 output to UTF-8
})
// output.Stdout is now valid UTF-8, even if the tool outputs "café"
Interactive Mode (REPLs & TUI)
output, err := bridge.Execute(ctx, bridge.CommandConfig{
Command: "python.exe",
Interactive: true, // Direct stdin/stdout/stderr copy
Stdin: os.Stdin, // Pass through terminal input
})
Concurrent Execution via Worker Pool
import "github.com/sibikrish3000/gowinbridge/pkg/workerpool"
pool := workerpool.NewPool(4, bridge.Execute)
pool.Submit(bridge.CommandConfig{Command: "build1.exe"})
pool.Submit(bridge.CommandConfig{Command: "build2.exe"})
pool.Submit(bridge.CommandConfig{Command: "build3.exe"})
go pool.Shutdown()
for result := range pool.Results() {
if result.Err != nil {
log.Printf("Error: %v", result.Err)
continue
}
fmt.Printf("[%s] %s\n", result.Config.Command, result.Output.Stdout)
}
WSL Detection
import "github.com/sibikrish3000/gowinbridge/internal/wsl"
if !wsl.IsWSL() {
log.Fatal("This tool requires WSL")
}
fmt.Printf("Running on WSL%d\n", wsl.DetectWSLVersion())
Pure Go Path Translation
import "github.com/sibikrish3000/gowinbridge/internal/wsl"
// Linux → Windows (no subprocess, <1µs)
winPath, _ := wsl.ToWindowsPath("/mnt/c/Users/test")
// winPath = "C:\Users\test"
// Non-mount paths get UNC notation
uncPath, _ := wsl.ToWindowsPath("/home/user/project")
// uncPath = "\\wsl.localhost\Ubuntu\home\user\project"
// Windows → Linux
linPath, _ := wsl.ToLinuxPath(`C:\Users\test`)
// linPath = "/mnt/c/Users/test"
Development
Prerequisites
- Go 1.21+
- WSL (for integration tests; unit tests run anywhere)
Project Structure
.
├── cmd/winrun/ CLI tool
│ ├── main.go Entry point, flag parsing, shim dispatch
│ ├── shim.go Shim install/list/remove subcommands
│ └── shim_test.go
├── internal/wsl/ WSL detection & path translation (private)
│ ├── detect.go
│ ├── detect_test.go
│ ├── path.go Pure Go resolver (/proc/mounts parsing)
│ └── path_test.go
├── pkg/bridge/ Core executor (public API)
│ ├── config.go CommandConfig / Output types
│ ├── encoding.go CP1252/UTF-16LE/BE decoder middleware
│ ├── encoding_test.go
│ ├── env.go WSLENV formatting with value-based heuristics
│ ├── env_test.go
│ ├── exec.go Buffered + interactive execution modes
│ └── exec_test.go
├── pkg/workerpool/ Bounded concurrency pool (public API)
│ ├── pool.go
│ └── pool_test.go
├── go.mod
├── go.sum
└── README.md
Running Tests
# All unit tests (works on any OS — WSL integration tests auto-skip)
go test ./... -v
# With race detector
go test ./... -race
# Short / unit tests only
go test ./... -short
Key Design Decisions
| Decision | Rationale |
|---|---|
| Pure Go path resolver | Parses /proc/mounts once; eliminates wslpath subprocess overhead (~20ms → <1µs) |
| Value-based WSLENV heuristics | inferWSLEnvFlag inspects values (not just key names) to auto-select /p, /l, /u |
| Dual execution modes | Buffered (Scanner) for output capture; interactive (io.Copy) for REPLs and TUI apps |
| Encoding middleware | transform.Reader wraps stdio pipes to decode CP1252/UTF-16 transparently before Scanner |
sync.Once for WSL detection | Avoids repeated /proc/version reads; cached after first call |
sync.Map for path cache | Memoizes resolved paths; concurrent-safe without locks |
exec.CommandContext | Ensures context cancellation (timeout / SIGINT) kills the Windows process |
.exe auto-resolution | Appends .exe and checks PATH if the user passes cmd instead of cmd.exe |
| Worker pool with injectable executor | Testable concurrency engine; mock executor eliminates WSL dependency in tests |
| Shim scripts with marker comments | Safe identification and removal; prevents accidental deletion of non-shim files |
Known Gotchas
- Binary names: Always use
.exesuffix (e.g.,cmd.exe, notcmd). The library attempts auto-resolution but explicit is better. - Path separators: Windows uses
\. The library handles this via the pure Go resolver, but be careful with manual string building. - Zombie processes: The CLI registers
SIGINT/SIGTERMhandlers to cancel all in-flight Windows processes on exit. - WSLENV: Only works for environment variables you explicitly pass — it does not auto-export your entire shell environment.
- Encoding: If unsure about the encoding, use
--encoding autofor BOM-based detection, or--encoding cp1252for legacy Western European tools. - Interactive mode: Auto-detected for
python,node,mysql,psql,irb,bash. Use--interactiveexplicitly for other REPLs. - Shim PATH: Ensure
~/.local/binis in your$PATH(addexport PATH="$HOME/.local/bin:$PATH"to your shell profile).
License
GNU GENERAL PUBLIC LICENSE