Simplify plugin-helper state configuration
This commit is contained in:
@@ -11,55 +11,67 @@ The first implementation focuses on safe local workflows:
|
||||
- apply exactly that plan and record install state
|
||||
- uninstall only files recorded in install state
|
||||
|
||||
Default BSManager instance roots:
|
||||
Default BSManager instance root:
|
||||
|
||||
```text
|
||||
/home/pleb/Windows/Users/pleb/BSManager/BSInstances
|
||||
/home/pleb/.local/share/BSManager/BSInstances
|
||||
```
|
||||
|
||||
Override with `--instances-root` or `PLUGIN_HELPER_INSTANCES_ROOT`. To search
|
||||
Default plugin-helper state directory:
|
||||
|
||||
```text
|
||||
$XDG_STATE_HOME/plugin-helper
|
||||
```
|
||||
|
||||
If `XDG_STATE_HOME` is not set, the state directory defaults to:
|
||||
|
||||
```text
|
||||
~/.local/state/plugin-helper
|
||||
```
|
||||
|
||||
Override the instance root with `--instances-root`,
|
||||
`PLUGIN_HELPER_INSTANCES_ROOT`, or `plugin-helper.local.toml`. To search
|
||||
multiple explicit roots, separate them with `:`.
|
||||
|
||||
## Managing Multiple Installs
|
||||
Override the state directory with `--state-dir`, `PLUGIN_HELPER_STATE_DIR`, or
|
||||
`plugin-helper.local.toml`.
|
||||
|
||||
The helper is intended to manage both the local Linux BSManager install and the
|
||||
mounted Windows install. Lockfiles and registry entries are shared by Beat Saber
|
||||
version, but install state is target-specific. When the same instance name
|
||||
exists under both roots, such as `1.44.1`, give each install profile its own
|
||||
state directory.
|
||||
## Local Configuration
|
||||
|
||||
This checkout is intended to manage the local Linux BSManager install. If you
|
||||
also manage a Windows install, use a separate clone on that partition and point
|
||||
both clones at the same state directory only when you intentionally want one
|
||||
shared source of truth.
|
||||
|
||||
Copy the example config and adjust paths if needed:
|
||||
|
||||
Copy the example profile config and adjust paths if needed:
|
||||
|
||||
```sh
|
||||
cp plugin-helper.toml.example plugin-helper.local.toml
|
||||
```
|
||||
|
||||
`plugin-helper.local.toml` is ignored by git. The default example uses this
|
||||
repo-local convention:
|
||||
`plugin-helper.local.toml` is ignored by git and uses top-level fields:
|
||||
|
||||
```text
|
||||
.state/ local Linux BSManager state
|
||||
.state-windows/ mounted Windows BSManager state
|
||||
```toml
|
||||
instances_root = "~/.local/share/BSManager/BSInstances"
|
||||
state_dir = "~/.local/state/plugin-helper"
|
||||
```
|
||||
|
||||
Examples:
|
||||
For repo-local state, set:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m plugin_helper \
|
||||
--profile linux \
|
||||
installed --instance 1.44.1
|
||||
|
||||
PYTHONPATH=src python -m plugin_helper \
|
||||
--profile windows \
|
||||
installed --instance 1.44.1
|
||||
```toml
|
||||
state_dir = ".state"
|
||||
```
|
||||
|
||||
Explicit `--instances-root` and `--state-dir` still work and override profile
|
||||
values. Do not reuse the same state directory for both targets when their
|
||||
instance names match. The current state layout is keyed by instance name, so
|
||||
sharing one state directory would mix bootstrap records, generated plans,
|
||||
backups, and installed file records for different game trees.
|
||||
For a shared Windows-partition state directory, set the same `state_dir` in both
|
||||
clones, for example:
|
||||
|
||||
```toml
|
||||
state_dir = "~/Windows/Users/pleb/ops/plugin-helper/.state"
|
||||
```
|
||||
|
||||
CLI flags override environment variables, environment variables override local
|
||||
config, and local config overrides built-in defaults.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -79,21 +91,13 @@ disable all currently enabled managed plugins and `e` to enable all currently
|
||||
disabled managed plugins.
|
||||
|
||||
The individual subcommands are mostly for automation and debugging. If you use
|
||||
them, prefer `--profile linux` or `--profile windows`. Pass `--state-dir`
|
||||
directly only when you intentionally want to override profile state or use the
|
||||
default live state outside this repo.
|
||||
them, pass `--state-dir` directly only when you intentionally want to override
|
||||
the configured state directory for one command.
|
||||
|
||||
Install assets are currently expected to already exist locally, usually under:
|
||||
|
||||
```text
|
||||
.state/instances/<instance>/downloads/<plugin-id>/
|
||||
```
|
||||
|
||||
For a second target-specific state directory, copy or re-download the same
|
||||
locked assets under that state root before planning. For example:
|
||||
|
||||
```text
|
||||
.state-windows/instances/<instance>/downloads/<plugin-id>/
|
||||
<state-dir>/instances/<instance>/downloads/<plugin-id>/
|
||||
```
|
||||
|
||||
## Beat Saber Data Backups
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[[profiles]]
|
||||
id = "linux"
|
||||
label = "Local Linux BSManager"
|
||||
instances_root = "~/.local/share/BSManager/BSInstances"
|
||||
state_dir = ".state"
|
||||
state_dir = "~/.local/state/plugin-helper"
|
||||
|
||||
[[profiles]]
|
||||
id = "windows"
|
||||
label = "Mounted Windows BSManager"
|
||||
instances_root = "~/Windows/Users/pleb/BSManager/BSInstances"
|
||||
state_dir = ".state-windows"
|
||||
# To keep state inside this checkout instead:
|
||||
# state_dir = ".state"
|
||||
#
|
||||
# To share one state directory between Linux and a Windows-partition checkout:
|
||||
# state_dir = "~/Windows/Users/pleb/ops/plugin-helper/.state"
|
||||
|
||||
+10
-51
@@ -6,7 +6,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .config import Profile, repo_root, resolve_runtime_config
|
||||
from .config import repo_root, resolve_runtime_config
|
||||
from .bootstrap import run_bootstrap
|
||||
from .bsipa import check_bsipa_health
|
||||
from .checker import check_lock
|
||||
@@ -63,8 +63,6 @@ def _add_common(parser: argparse.ArgumentParser, *, suppress_default: bool = Fal
|
||||
default = argparse.SUPPRESS if suppress_default else None
|
||||
parser.add_argument("--instances-root", default=default, help="BSManager instances root")
|
||||
parser.add_argument("--state-dir", default=default, help="plugin-helper state directory")
|
||||
parser.add_argument("--profile", default=default, help="Configured profile id from plugin-helper.local.toml")
|
||||
parser.add_argument("--config", default=default, help="Path to plugin-helper TOML config")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
@@ -225,37 +223,6 @@ def _common_parent() -> argparse.ArgumentParser:
|
||||
return parent
|
||||
|
||||
|
||||
def _menu_profiles(
|
||||
*,
|
||||
profiles: tuple[Profile, ...],
|
||||
selected_profile: Profile | None,
|
||||
instances_roots: list[Path],
|
||||
state_root: Path,
|
||||
use_config_profiles: bool,
|
||||
) -> list[Profile]:
|
||||
if selected_profile:
|
||||
return [
|
||||
Profile(
|
||||
id=selected_profile.id if len(instances_roots) == 1 else f"{selected_profile.id}-{index}",
|
||||
label=selected_profile.label,
|
||||
instances_root=root,
|
||||
state_dir=state_root,
|
||||
)
|
||||
for index, root in enumerate(instances_roots, start=1)
|
||||
]
|
||||
if use_config_profiles and profiles:
|
||||
return list(profiles)
|
||||
return [
|
||||
Profile(
|
||||
id=f"root-{index}",
|
||||
label=str(root),
|
||||
instances_root=root,
|
||||
state_dir=state_root,
|
||||
)
|
||||
for index, root in enumerate(instances_roots, start=1)
|
||||
]
|
||||
|
||||
|
||||
def _run_menu(
|
||||
*,
|
||||
runtime: Any,
|
||||
@@ -269,35 +236,29 @@ def _run_menu(
|
||||
print("Install project dependencies, for example: python -m pip install -e .")
|
||||
return 1
|
||||
|
||||
profiles = _menu_profiles(
|
||||
profiles=runtime.profiles,
|
||||
selected_profile=runtime.selected_profile,
|
||||
instances_roots=runtime.instances_roots,
|
||||
state_root=runtime.state_root,
|
||||
use_config_profiles=runtime.config_loaded and not explicit_instances_root and not explicit_state_dir,
|
||||
)
|
||||
choices: list[InstallationChoice] = []
|
||||
for profile in profiles:
|
||||
for instance in list_instances(profile.instances_root):
|
||||
for index, root in enumerate(runtime.instances_roots, start=1):
|
||||
install_label = str(root) if len(runtime.instances_roots) > 1 else "Default"
|
||||
for instance in list_instances(root):
|
||||
choices.append(
|
||||
InstallationChoice(
|
||||
profile_id=profile.id,
|
||||
profile_label=profile.label,
|
||||
install_id=f"root-{index}",
|
||||
install_label=install_label,
|
||||
instance_name=instance.name,
|
||||
instance_path=instance.path,
|
||||
state_root=profile.state_dir,
|
||||
state_root=runtime.state_root,
|
||||
)
|
||||
)
|
||||
if not choices:
|
||||
searched = ", ".join(str(profile.instances_root) for profile in profiles)
|
||||
searched = ", ".join(str(root) for root in runtime.instances_roots)
|
||||
print(f"No Beat Saber instances found under {searched}")
|
||||
return 1
|
||||
|
||||
setup_hint = None
|
||||
if not runtime.config_loaded and not runtime.selected_profile and not explicit_instances_root and not explicit_state_dir:
|
||||
if not runtime.config_loaded and not explicit_instances_root and not explicit_state_dir:
|
||||
setup_hint = (
|
||||
f"No {runtime.config_path.name} found; using default roots and default state. "
|
||||
f"Copy plugin-helper.toml.example to {runtime.config_path.name} to map each install to a state dir."
|
||||
f"Copy plugin-helper.toml.example to {runtime.config_path.name} to set a custom state dir."
|
||||
)
|
||||
app = PluginHelperTui(choices=choices, repo_root=repo_root(), setup_hint=setup_hint)
|
||||
result = app.run()
|
||||
@@ -321,8 +282,6 @@ def run(argv: list[str] | None = None) -> int:
|
||||
runtime = resolve_runtime_config(
|
||||
instances_root_value=getattr(args, "instances_root", None),
|
||||
state_dir_value=getattr(args, "state_dir", None),
|
||||
profile_id=getattr(args, "profile", None),
|
||||
config_path=getattr(args, "config", None),
|
||||
)
|
||||
inst_roots = runtime.instances_roots
|
||||
st_root = runtime.state_root
|
||||
|
||||
+58
-55
@@ -7,27 +7,21 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
WINDOWS_INSTANCES_ROOT = Path("/home/pleb/Windows/Users/pleb/BSManager/BSInstances")
|
||||
LOCAL_INSTANCES_ROOT = Path.home() / ".local/share/BSManager/BSInstances"
|
||||
DEFAULT_INSTANCES_ROOTS = (WINDOWS_INSTANCES_ROOT, LOCAL_INSTANCES_ROOT)
|
||||
DEFAULT_INSTANCES_ROOT = WINDOWS_INSTANCES_ROOT
|
||||
DEFAULT_INSTANCES_ROOT = LOCAL_INSTANCES_ROOT
|
||||
LOCAL_CONFIG_NAME = "plugin-helper.local.toml"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Profile:
|
||||
id: str
|
||||
label: str
|
||||
instances_root: Path
|
||||
state_dir: Path
|
||||
class LocalConfig:
|
||||
instances_roots: list[Path] | None
|
||||
state_root: Path | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeConfig:
|
||||
instances_roots: list[Path]
|
||||
state_root: Path
|
||||
profiles: tuple[Profile, ...]
|
||||
selected_profile: Profile | None
|
||||
config_path: Path
|
||||
config_loaded: bool
|
||||
|
||||
@@ -40,16 +34,19 @@ def instances_root(value: str | None = None) -> Path:
|
||||
return instances_roots(value)[0]
|
||||
|
||||
|
||||
def instances_roots(value: str | None = None) -> list[Path]:
|
||||
def instances_roots(value: str | None = None, *, base: Path | None = None) -> list[Path]:
|
||||
raw = value or os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
|
||||
if raw:
|
||||
return [Path(item).expanduser() for item in raw.split(os.pathsep) if item]
|
||||
return list(DEFAULT_INSTANCES_ROOTS)
|
||||
return _resolve_path_list(raw, base or repo_root())
|
||||
return [DEFAULT_INSTANCES_ROOT]
|
||||
|
||||
|
||||
def state_root(value: str | None = None) -> Path:
|
||||
if value:
|
||||
return _resolve_path(value, repo_root())
|
||||
env_state = os.environ.get("PLUGIN_HELPER_STATE_DIR")
|
||||
if env_state:
|
||||
return _resolve_path(env_state, repo_root())
|
||||
xdg_state = os.environ.get("XDG_STATE_HOME")
|
||||
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
|
||||
return base / "plugin-helper"
|
||||
@@ -66,75 +63,81 @@ def _resolve_path(value: str | Path, base: Path) -> Path:
|
||||
return (base / path).resolve()
|
||||
|
||||
|
||||
def load_profiles(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[tuple[Profile, ...], Path, bool]:
|
||||
def _resolve_path_list(value: str, base: Path) -> list[Path]:
|
||||
return [_resolve_path(item, base) for item in value.split(os.pathsep) if item]
|
||||
|
||||
|
||||
def load_local_config(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[LocalConfig, Path, bool]:
|
||||
repo = root or repo_root()
|
||||
path = _resolve_path(config_path, repo) if config_path else default_config_path(repo)
|
||||
if not path.exists():
|
||||
if config_path:
|
||||
raise FileNotFoundError(f"plugin-helper config not found: {path}")
|
||||
return (), path, False
|
||||
return LocalConfig(instances_roots=None, state_root=None), path, False
|
||||
|
||||
with path.open("rb") as handle:
|
||||
data: dict[str, Any] = tomllib.load(handle)
|
||||
|
||||
profiles: list[Profile] = []
|
||||
seen: set[str] = set()
|
||||
base = path.parent
|
||||
for item in data.get("profiles", []):
|
||||
profile_id = item["id"]
|
||||
if profile_id in seen:
|
||||
raise ValueError(f"{path}: duplicate profile id: {profile_id}")
|
||||
seen.add(profile_id)
|
||||
profiles.append(
|
||||
Profile(
|
||||
id=profile_id,
|
||||
label=item.get("label", profile_id),
|
||||
instances_root=_resolve_path(item["instances_root"], base),
|
||||
state_dir=_resolve_path(item["state_dir"], base),
|
||||
instances_value = data.get("instances_root")
|
||||
state_value = data.get("state_dir")
|
||||
return (
|
||||
LocalConfig(
|
||||
instances_roots=_resolve_path_list(instances_value, base) if instances_value else None,
|
||||
state_root=_resolve_path(state_value, base) if state_value else None,
|
||||
),
|
||||
path,
|
||||
True,
|
||||
)
|
||||
)
|
||||
return tuple(profiles), path, True
|
||||
|
||||
|
||||
def profile_by_id(profiles: tuple[Profile, ...], profile_id: str) -> Profile:
|
||||
for profile in profiles:
|
||||
if profile.id == profile_id:
|
||||
return profile
|
||||
available = ", ".join(profile.id for profile in profiles) or "(none)"
|
||||
raise KeyError(f"unknown profile: {profile_id}; available profiles: {available}")
|
||||
def _env_instances_roots(root: Path) -> list[Path] | None:
|
||||
value = os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
|
||||
return _resolve_path_list(value, root) if value else None
|
||||
|
||||
|
||||
def _env_state_root(root: Path) -> Path | None:
|
||||
value = os.environ.get("PLUGIN_HELPER_STATE_DIR")
|
||||
return _resolve_path(value, root) if value else None
|
||||
|
||||
|
||||
def _default_state_root() -> Path:
|
||||
xdg_state = os.environ.get("XDG_STATE_HOME")
|
||||
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
|
||||
return base / "plugin-helper"
|
||||
|
||||
|
||||
def _default_instances_roots() -> list[Path]:
|
||||
return [DEFAULT_INSTANCES_ROOT]
|
||||
|
||||
|
||||
def resolve_runtime_config(
|
||||
*,
|
||||
instances_root_value: str | None = None,
|
||||
state_dir_value: str | None = None,
|
||||
profile_id: str | None = None,
|
||||
config_path: str | Path | None = None,
|
||||
root: Path | None = None,
|
||||
) -> RuntimeConfig:
|
||||
repo = root or repo_root()
|
||||
profiles, loaded_path, loaded = load_profiles(config_path, root=repo)
|
||||
selected = profile_by_id(profiles, profile_id) if profile_id else None
|
||||
local_config, loaded_path, loaded = load_local_config(root=repo)
|
||||
|
||||
if instances_root_value:
|
||||
resolved_instances = instances_roots(instances_root_value)
|
||||
elif selected:
|
||||
resolved_instances = [selected.instances_root]
|
||||
else:
|
||||
resolved_instances = instances_roots(None)
|
||||
|
||||
if state_dir_value:
|
||||
resolved_state = _resolve_path(state_dir_value, repo)
|
||||
elif selected:
|
||||
resolved_state = selected.state_dir
|
||||
else:
|
||||
resolved_state = state_root(None)
|
||||
resolved_instances = (
|
||||
_resolve_path_list(instances_root_value, repo)
|
||||
if instances_root_value
|
||||
else _env_instances_roots(repo)
|
||||
or local_config.instances_roots
|
||||
or _default_instances_roots()
|
||||
)
|
||||
resolved_state = (
|
||||
_resolve_path(state_dir_value, repo)
|
||||
if state_dir_value
|
||||
else _env_state_root(repo)
|
||||
or local_config.state_root
|
||||
or _default_state_root()
|
||||
)
|
||||
|
||||
return RuntimeConfig(
|
||||
instances_roots=resolved_instances,
|
||||
state_root=resolved_state,
|
||||
profiles=profiles,
|
||||
selected_profile=selected,
|
||||
config_path=loaded_path,
|
||||
config_loaded=loaded,
|
||||
)
|
||||
|
||||
@@ -18,8 +18,8 @@ from .state import load_installed_state
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallationChoice:
|
||||
profile_id: str
|
||||
profile_label: str
|
||||
install_id: str
|
||||
install_label: str
|
||||
instance_name: str
|
||||
instance_path: Path
|
||||
state_root: Path
|
||||
@@ -189,10 +189,10 @@ class PluginHelperTui(App[int]):
|
||||
self._set_title("Choose Beat Saber Installation")
|
||||
table = self.query_one(DataTable)
|
||||
table.clear(columns=True)
|
||||
table.add_columns("Profile", "Version", "Instance Path", "State Dir")
|
||||
table.add_columns("Install", "Version", "Instance Path", "State Dir")
|
||||
for choice in self.choices:
|
||||
table.add_row(
|
||||
choice.profile_label,
|
||||
choice.install_label,
|
||||
choice.instance_name,
|
||||
str(choice.instance_path),
|
||||
str(choice.state_root),
|
||||
@@ -208,7 +208,7 @@ class PluginHelperTui(App[int]):
|
||||
return
|
||||
target = self.selected_installation
|
||||
self.mode = "plugins"
|
||||
self._set_title(f"{target.profile_label} / {target.instance_name}")
|
||||
self._set_title(f"{target.install_label} / {target.instance_name}")
|
||||
table = self.query_one(DataTable)
|
||||
table.clear(columns=True)
|
||||
table.add_columns("Status", "Name", "ID", "Version", "Files", "Asset")
|
||||
|
||||
+84
-33
@@ -17,7 +17,7 @@ from plugin_helper.bootstrap import _run_ipa
|
||||
from plugin_helper.beatmods import by_version_id, normalize_mods
|
||||
from plugin_helper.checker import check_lock
|
||||
from plugin_helper.cli import installed_plugins_report, run
|
||||
from plugin_helper.config import load_profiles, profile_by_id, resolve_runtime_config
|
||||
from plugin_helper.config import load_local_config, resolve_runtime_config
|
||||
from plugin_helper.fsutil import sha256_file
|
||||
from plugin_helper.installer import apply_plan, disable_plugin, uninstall_plugin
|
||||
from plugin_helper.instances import get_instance, list_instances
|
||||
@@ -125,61 +125,112 @@ class PluginHelperTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "ambiguous"):
|
||||
get_instance([windows, local], "1.44.1")
|
||||
|
||||
def test_profile_config_loads_and_resolves_paths(self) -> None:
|
||||
def test_local_config_loads_top_level_paths(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
config = root / "plugin-helper.local.toml"
|
||||
config.write_text(
|
||||
"""
|
||||
[[profiles]]
|
||||
id = "linux"
|
||||
label = "Linux"
|
||||
instances_root = "~/BSInstances"
|
||||
state_dir = ".state"
|
||||
|
||||
[[profiles]]
|
||||
id = "windows"
|
||||
label = "Windows"
|
||||
instances_root = "mounted/BSInstances"
|
||||
state_dir = ".state-windows"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
profiles, loaded_path, loaded = load_profiles(config, root=root)
|
||||
local_config, loaded_path, loaded = load_local_config(config, root=root)
|
||||
|
||||
self.assertTrue(loaded)
|
||||
self.assertEqual(loaded_path, config)
|
||||
self.assertEqual(profile_by_id(profiles, "linux").instances_root, Path("~/BSInstances").expanduser())
|
||||
self.assertEqual(profile_by_id(profiles, "windows").instances_root, root / "mounted" / "BSInstances")
|
||||
self.assertEqual(profile_by_id(profiles, "windows").state_dir, root / ".state-windows")
|
||||
self.assertEqual(local_config.instances_roots, [Path("~/BSInstances").expanduser()])
|
||||
self.assertEqual(local_config.state_root, root / ".state")
|
||||
|
||||
def test_profile_runtime_explicit_overrides(self) -> None:
|
||||
def test_runtime_explicit_overrides_env_and_config(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
config = root / "plugin-helper.local.toml"
|
||||
config.write_text(
|
||||
(root / "plugin-helper.local.toml").write_text(
|
||||
"""
|
||||
[[profiles]]
|
||||
id = "linux"
|
||||
label = "Linux"
|
||||
instances_root = "profile-root"
|
||||
state_dir = ".state"
|
||||
instances_root = "config-root"
|
||||
state_dir = "config-state"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PLUGIN_HELPER_INSTANCES_ROOT": str(root / "env-root"),
|
||||
"PLUGIN_HELPER_STATE_DIR": str(root / "env-state"),
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
runtime = resolve_runtime_config(
|
||||
instances_root_value=str(root / "explicit-root"),
|
||||
state_dir_value="explicit-state",
|
||||
profile_id="linux",
|
||||
config_path=config,
|
||||
root=root,
|
||||
)
|
||||
|
||||
self.assertEqual(runtime.instances_roots, [root / "explicit-root"])
|
||||
self.assertEqual(runtime.state_root, root / "explicit-state")
|
||||
self.assertEqual(runtime.selected_profile.id, "linux")
|
||||
|
||||
def test_runtime_env_overrides_config(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
(root / "plugin-helper.local.toml").write_text(
|
||||
"""
|
||||
instances_root = "config-root"
|
||||
state_dir = "config-state"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PLUGIN_HELPER_INSTANCES_ROOT": f"{root / 'env-root-a'}{os.pathsep}{root / 'env-root-b'}",
|
||||
"PLUGIN_HELPER_STATE_DIR": str(root / "env-state"),
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
runtime = resolve_runtime_config(root=root)
|
||||
|
||||
self.assertEqual(runtime.instances_roots, [root / "env-root-a", root / "env-root-b"])
|
||||
self.assertEqual(runtime.state_root, root / "env-state")
|
||||
|
||||
def test_runtime_uses_local_config_before_defaults(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
(root / "plugin-helper.local.toml").write_text(
|
||||
"""
|
||||
instances_root = "config-root"
|
||||
state_dir = "config-state"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
runtime = resolve_runtime_config(root=root)
|
||||
|
||||
self.assertEqual(runtime.instances_roots, [root / "config-root"])
|
||||
self.assertEqual(runtime.state_root, root / "config-state")
|
||||
|
||||
def test_runtime_default_state_uses_xdg_state_home(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
xdg = root / "xdg-state"
|
||||
|
||||
with patch.dict(os.environ, {"XDG_STATE_HOME": str(xdg)}, clear=True):
|
||||
runtime = resolve_runtime_config(root=root)
|
||||
|
||||
self.assertEqual(runtime.state_root, xdg / "plugin-helper")
|
||||
|
||||
def test_runtime_default_state_uses_home_local_state(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
runtime = resolve_runtime_config(root=root)
|
||||
|
||||
self.assertEqual(runtime.state_root, Path.home() / ".local" / "state" / "plugin-helper")
|
||||
|
||||
def test_no_args_prints_help_when_not_interactive(self) -> None:
|
||||
output = StringIO()
|
||||
@@ -196,7 +247,7 @@ state_dir = ".state"
|
||||
patch("sys.stdout.isatty", return_value=True),
|
||||
patch("plugin_helper.cli._run_menu", return_value=0) as run_menu,
|
||||
):
|
||||
status = run(["--profile", "linux"])
|
||||
status = run([])
|
||||
|
||||
self.assertEqual(status, 0)
|
||||
run_menu.assert_called_once()
|
||||
@@ -1063,8 +1114,8 @@ sha256 = "{sha256_file(asset)}"
|
||||
{"instance": "1.40.8", "plugins": plugins, "disabledPlugins": disabled_plugins},
|
||||
)
|
||||
choice = InstallationChoice(
|
||||
profile_id="test",
|
||||
profile_label="Test Profile",
|
||||
install_id="test",
|
||||
install_label="Test Install",
|
||||
instance_name="1.40.8",
|
||||
instance_path=instance,
|
||||
state_root=state,
|
||||
@@ -1076,15 +1127,15 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_installation_picker_shows_duplicate_instances_with_state_dirs(self) -> None:
|
||||
choices = [
|
||||
InstallationChoice(
|
||||
profile_id="linux",
|
||||
profile_label="Linux",
|
||||
install_id="linux",
|
||||
install_label="Linux",
|
||||
instance_name="1.44.1",
|
||||
instance_path=Path("/tmp/linux/1.44.1"),
|
||||
state_root=Path("/tmp/state-linux"),
|
||||
),
|
||||
InstallationChoice(
|
||||
profile_id="windows",
|
||||
profile_label="Windows",
|
||||
install_id="windows",
|
||||
install_label="Windows",
|
||||
instance_name="1.44.1",
|
||||
instance_path=Path("/tmp/windows/1.44.1"),
|
||||
state_root=Path("/tmp/state-windows"),
|
||||
|
||||
Reference in New Issue
Block a user