Add native Windows compatibility for config, bootstrap, and CLI.

Support profile-based config resolution with auto-selection of a sole profile,
native IPA.exe -n bootstrap on Windows, platform-aware process cleanup, and
Windows-friendly userdata backup path inference.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pleb
2026-07-01 22:33:24 -07:00
parent 6af1b03c35
commit c67878cac0
7 changed files with 313 additions and 47 deletions
+61 -18
View File
@@ -9,6 +9,7 @@ from typing import Any, Callable
from urllib.request import Request, urlopen
from .bsipa import BSIPA_PLUGIN_ID, check_bsipa_health
from .config import is_windows
from .fsutil import sha256_file
from .installer import apply_plan
from .models import LockedPlugin, Lockfile, Registry
@@ -130,31 +131,63 @@ def fetch_locked_bsipa_archive(lockfile: Lockfile, state_root: Path) -> dict[str
return result
def build_bootstrap_command(
ipa: Path,
*,
proton: Path | None = None,
native: bool | None = None,
) -> list[str]:
use_native = is_windows() if native is None else native
if use_native:
return [str(ipa), "-n"]
proton_path = proton or _default_proton()
return [str(proton_path), "run", str(ipa), "-n"]
def _terminate_process(process: subprocess.Popen[str]) -> None:
if is_windows():
process.terminate()
else:
os.killpg(process.pid, signal.SIGTERM)
def _kill_process(process: subprocess.Popen[str]) -> None:
if is_windows():
process.kill()
else:
os.killpg(process.pid, signal.SIGKILL)
def _run_ipa(
*,
command: list[str],
instance_path: Path,
timeout_seconds: int,
native: bool = False,
) -> dict[str, Any]:
process = subprocess.Popen(
command,
cwd=instance_path,
env=_proton_env(instance_path),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
popen_kwargs: dict[str, Any] = {
"cwd": instance_path,
"text": True,
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
}
if native:
popen_kwargs["env"] = os.environ.copy()
else:
popen_kwargs["env"] = _proton_env(instance_path)
popen_kwargs["start_new_session"] = True
process = subprocess.Popen(command, **popen_kwargs)
try:
stdout, stderr = process.communicate(timeout=timeout_seconds)
timed_out = False
except subprocess.TimeoutExpired:
timed_out = True
os.killpg(process.pid, signal.SIGTERM)
_terminate_process(process)
try:
stdout, stderr = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
_kill_process(process)
stdout, stderr = process.communicate()
return {
"returncode": process.returncode,
@@ -175,10 +208,14 @@ def run_bootstrap(
state_root: Path,
repo_root: Path,
proton: Path | None = None,
native: bool | None = None,
progress: Callable[[str], None] | None = None,
ipa_timeout_seconds: int = 120,
) -> dict[str, Any]:
tell = progress or (lambda _message: None)
use_native = is_windows() if native is None else native
bootstrap_mode = "native" if use_native else "proton"
tell("Fetching locked BSIPA archive")
fetched = fetch_locked_bsipa_archive(lockfile, state_root)
if fetched.get("cached"):
@@ -210,30 +247,34 @@ def run_bootstrap(
if not ipa.is_file():
raise FileNotFoundError(f"BSIPA archive did not install IPA.exe: {ipa}")
proton_path = proton or _default_proton()
if not proton_path.is_file():
raise FileNotFoundError(f"Proton executable not found: {proton_path}")
command = build_bootstrap_command(ipa, proton=proton, native=use_native)
if use_native:
tell(f"Running IPA.exe -n natively; timeout {ipa_timeout_seconds}s")
else:
proton_path = proton or _default_proton()
if not proton_path.is_file():
raise FileNotFoundError(f"Proton executable not found: {proton_path}")
tell(f"Running IPA.exe -n through Proton; timeout {ipa_timeout_seconds}s")
command = [str(proton_path), "run", str(ipa), "-n"]
tell(f"Running IPA.exe -n through Proton; timeout {ipa_timeout_seconds}s")
completed = _run_ipa(
command=command,
instance_path=instance_path,
timeout_seconds=ipa_timeout_seconds,
native=use_native,
)
tell("Scanning bootstrap files after IPA.exe -n")
after = scan_bootstrap_files(instance_path)
delta = _files_delta(before, after)
state = {
state: dict[str, Any] = {
"schemaVersion": 1,
"instance": instance,
"beatSaberVersion": beat_saber_version,
"bootstrappedAt": _now_iso(),
"plugin": BSIPA_PLUGIN_ID,
"bootstrapMode": bootstrap_mode,
"archive": fetched,
"planPath": str(plan_path),
"applied": apply_result["applied"],
"proton": str(proton_path),
"command": command,
"ipaExitCode": completed["returncode"],
"ipaTimedOut": completed["timedOut"],
@@ -244,6 +285,8 @@ def run_bootstrap(
"delta": delta,
"health": {},
}
if not use_native:
state["proton"] = str(proton or _default_proton())
save_bootstrap_state(state_root, instance, state)
state["health"] = check_bsipa_health(instance_path, state_root, instance)
save_bootstrap_state(state_root, instance, state)