Skip to content

Instantly share code, notes, and snippets.

@fralalonde
Created November 17, 2025 15:40
Show Gist options
  • Select an option

  • Save fralalonde/424d19aad55cf53f6fe4f8f7fa63a077 to your computer and use it in GitHub Desktop.

Select an option

Save fralalonde/424d19aad55cf53f6fe4f8f7fa63a077 to your computer and use it in GitHub Desktop.
wine-select
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
SCAN_PATHS = [
"~/.steam/steam/steamapps/common",
"~/.local/share/Steam/steamapps/common",
"~/.var/app/com.valvesoftware.Steam/.steam/steam/steamapps/common",
]
SCRIPT_DIR = Path(__file__).resolve().parent
LINK_WINE = SCRIPT_DIR / "wine"
LINK_WINE64 = SCRIPT_DIR / "wine64"
# ---------------------------------------------------------------------------
def normalize(name: str) -> str:
return name.replace(" ", "_").replace("-", "_").lower()
def find_versions():
"""Return list of (shorthand, full_name, real_path)."""
seen = set()
out = []
for base in SCAN_PATHS:
base = Path(base).expanduser()
if not base.is_dir():
continue
for d in base.iterdir():
if not d.is_dir():
continue
lname = d.name.lower()
if "proton" not in lname and "wine" not in lname:
continue
real = d.resolve()
if real in seen:
continue
seen.add(real)
out.append((normalize(d.name), d.name, real))
out.sort(key=lambda x: x[0])
return out
def get_active():
if LINK_WINE64.is_symlink():
try:
return LINK_WINE64.resolve()
except FileNotFoundError:
return None
return None
# ---------------------------------------------------------------------------
def find_binaries(base: Path):
"""Return dict of available binaries: wine, wine64."""
candidates = {
"wine": [
base / "files/bin/wine",
base / "dist/bin/wine",
base / "bin/wine",
],
"wine64": [
base / "files/bin/wine64",
base / "dist/bin/wine64",
base / "bin/wine64",
],
}
found = {}
for name, paths in candidates.items():
for c in paths:
if c.is_file() and os.access(c, os.X_OK):
found[name] = c
break
return found
def is_runnable(bin_path: Path) -> bool:
"""Check whether a binary can actually run (avoids fake Proton wine32 stubs)."""
try:
# We don't want to *run* Wine, just check interpreter
out = subprocess.run(
["readelf", "-l", str(bin_path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
return "Requesting program interpreter" in out.stdout
except Exception:
return False
# ---------------------------------------------------------------------------
def cmd_list(json_mode=False):
active = get_active()
versions = find_versions()
items = []
for sh, full, path in versions:
bins = find_binaries(path)
runnable_wine = None
if "wine" in bins and is_runnable(bins["wine"]):
runnable_wine = bins["wine"]
items.append({
"shorthand": sh,
"full": full,
"path": str(path),
"wine": str(runnable_wine) if runnable_wine else None,
"wine64": str(bins.get("wine64")) if "wine64" in bins else None,
"active": active == bins.get("wine64"),
})
if json_mode:
print(json.dumps(items, indent=2))
return
if not items:
print("No Proton/Wine versions found.")
return
for i in items:
prefix = "*" if i["active"] else " "
print(f"{prefix} {i['shorthand']:25} {i['full']}")
# ---------------------------------------------------------------------------
def cmd_use(shorthand):
versions = find_versions()
match = None
for sh, full, path in versions:
if sh == shorthand:
match = (full, path)
break
if not match:
print(f"No such version: {shorthand}")
return
full, path = match
bins = find_binaries(path)
if "wine64" not in bins:
print("Error: No wine64 binary found — cannot activate this version.")
return
# Always link wine64
if LINK_WINE64.exists() or LINK_WINE64.is_symlink():
LINK_WINE64.unlink()
LINK_WINE64.symlink_to(bins["wine64"])
# Link wine only if runnable
if "wine" in bins and is_runnable(bins["wine"]):
if LINK_WINE.exists() or LINK_WINE.is_symlink():
LINK_WINE.unlink()
LINK_WINE.symlink_to(bins["wine"])
else:
# Remove old 'wine' link if present
if LINK_WINE.exists() or LINK_WINE.is_symlink():
LINK_WINE.unlink()
print(f"Activated: {full}")
print(f"wine64 -> {bins['wine64']}")
if "wine" in bins and is_runnable(bins["wine"]):
print(f"wine -> {bins['wine']}")
else:
print(f"wine (skipped; 32-bit loader not runnable)")
# ---------------------------------------------------------------------------
def cmd_help():
print("Usage: wineman <command> [args]\n")
print("Commands:")
print(" help Show this help (default)")
print(" list Show available versions")
print(" list --json Output in JSON")
print(" use <version> Activate version (create wine + wine64 links)")
print(f"\nLinks created in: {SCRIPT_DIR}\n")
# ---------------------------------------------------------------------------
def main():
if len(sys.argv) < 2:
return cmd_help()
cmd = sys.argv[1]
if cmd in ("help", "-h", "--help"):
return cmd_help()
if cmd == "list":
json_mode = (len(sys.argv) > 2 and sys.argv[2] == "--json")
return cmd_list(json_mode)
if cmd == "use":
if len(sys.argv) < 3:
print("Missing version.\n")
return cmd_help()
return cmd_use(sys.argv[2])
print(f"Unknown command: {cmd}")
cmd_help()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment