GoWinBridge

February 13, 2026 · View on GitHub

Go License: GPL

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.txt don't resolve on the Windows side
  • Encoding handling — Windows tools output CP1252/UTF-16LE, Go assumes UTF-8
  • Interactive support — REPLs like python.exe and 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

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

FlagDefaultDescription
--concurrency NNumCPUMax concurrent Windows process executions
--convert-pathsfalseAuto-detect and convert file path arguments to Windows format
--encoding ENC"" (UTF-8)Output encoding: utf8, cp1252, utf16le, utf16be, auto
--interactivefalseRun in interactive/PTY mode (bypasses output capture)
--env KEY=VALSet environment variable (repeatable)
--tunnel-envfalseEnable WSLENV tunneling for --env vars
--timeout DURATION0 (none)Max execution time (e.g., 30s, 5m)
--versionPrint 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

DecisionRationale
Pure Go path resolverParses /proc/mounts once; eliminates wslpath subprocess overhead (~20ms → <1µs)
Value-based WSLENV heuristicsinferWSLEnvFlag inspects values (not just key names) to auto-select /p, /l, /u
Dual execution modesBuffered (Scanner) for output capture; interactive (io.Copy) for REPLs and TUI apps
Encoding middlewaretransform.Reader wraps stdio pipes to decode CP1252/UTF-16 transparently before Scanner
sync.Once for WSL detectionAvoids repeated /proc/version reads; cached after first call
sync.Map for path cacheMemoizes resolved paths; concurrent-safe without locks
exec.CommandContextEnsures context cancellation (timeout / SIGINT) kills the Windows process
.exe auto-resolutionAppends .exe and checks PATH if the user passes cmd instead of cmd.exe
Worker pool with injectable executorTestable concurrency engine; mock executor eliminates WSL dependency in tests
Shim scripts with marker commentsSafe identification and removal; prevents accidental deletion of non-shim files

Known Gotchas

  • Binary names: Always use .exe suffix (e.g., cmd.exe, not cmd). 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/SIGTERM handlers 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 auto for BOM-based detection, or --encoding cp1252 for legacy Western European tools.
  • Interactive mode: Auto-detected for python, node, mysql, psql, irb, bash. Use --interactive explicitly for other REPLs.
  • Shim PATH: Ensure ~/.local/bin is in your $PATH (add export PATH="$HOME/.local/bin:$PATH" to your shell profile).

License

GNU GENERAL PUBLIC LICENSE