Created
November 17, 2025 15:40
-
-
Save fralalonde/424d19aad55cf53f6fe4f8f7fa63a077 to your computer and use it in GitHub Desktop.
wine-select
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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