Skip to content

Commit 03f8b49

Browse files
authored
Merge pull request #12 from ind4skylivey/main
[FIX] Release 1.1.1: prefix manager, msix support, config fixes
2 parents 464a78f + 48199e3 commit 03f8b49

9 files changed

Lines changed: 189 additions & 22 deletions

File tree

affinity_cli/commands/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Command implementations used by the CLI entrypoint."""
2+
3+
__all__ = ["status", "install", "list_installers"]

affinity_cli/config.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
# Project metadata ---------------------------------------------------------
99

10-
VERSION = "1.1.0"
10+
VERSION = "1.1.1"
1111
APP_NAME = "Affinity CLI"
1212

1313
# Paths --------------------------------------------------------------------
@@ -31,7 +31,7 @@
3131

3232
# Installer discovery -------------------------------------------------------
3333

34-
INSTALLER_SUFFIXES = (".exe", ".msi")
34+
INSTALLER_SUFFIXES = (".exe", ".msi", ".msix")
3535
INSTALLER_NAME_PREFIX = "affinity"
3636

3737
# Affinity Products --------------------------------------------------------
@@ -94,7 +94,41 @@
9494
"flex",
9595
]
9696

97-
# Ensure config directories exist -----------------------------------------
98-
99-
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
100-
CACHE_DIR.mkdir(parents=True, exist_ok=True)
97+
# Ensure config directories exist (best-effort; ignore permission errors) --
98+
for path in (CONFIG_DIR, CACHE_DIR):
99+
try:
100+
path.mkdir(parents=True, exist_ok=True)
101+
except PermissionError:
102+
# In restricted environments we still want imports to succeed.
103+
pass
104+
105+
# Convenience re-exports ----------------------------------------------------
106+
# Imported lazily to avoid circular imports during module initialization.
107+
from affinity_cli.core.config_loader import ConfigLoader, ConfigError, ResolvedConfig, UserConfig # noqa: E402,F401
108+
109+
__all__ = [
110+
"ConfigLoader",
111+
"ConfigError",
112+
"ResolvedConfig",
113+
"UserConfig",
114+
"VERSION",
115+
"APP_NAME",
116+
"HOME_DIR",
117+
"CONFIG_DIR",
118+
"CACHE_DIR",
119+
"DEFAULT_INSTALLERS_PATH",
120+
"DEFAULT_WINE_PREFIX",
121+
"DEFAULT_WINE_INSTALL",
122+
"DEFAULT_INSTALLER_VERSION",
123+
"SUPPORTED_INSTALLER_VERSIONS",
124+
"WINE_VERSION_DEFAULT",
125+
"ELEMENTALWARRIOR_REPO",
126+
"INSTALLER_SUFFIXES",
127+
"INSTALLER_NAME_PREFIX",
128+
"AFFINITY_PRODUCTS",
129+
"CORE_WINE_DEPS",
130+
"MULTIARCH_32BIT_DEPS",
131+
"GRAPHICS_DEPS",
132+
"FONT_DEPS",
133+
"BUILD_DEPS",
134+
]

affinity_cli/core/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Subpackage containing core building blocks (distro detection, installer scanning, wine helpers).
3+
"""
4+
5+
__all__ = [
6+
"config_loader",
7+
"distro_detector",
8+
"installer_scanner",
9+
"wine_manager",
10+
"wine_executor",
11+
"prefix_manager",
12+
]

affinity_cli/core/config_loader.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from pathlib import Path
88
from typing import Any, Dict, Optional
99

10+
import os
11+
1012
from affinity_cli import config
1113

1214
try: # Python 3.11+
@@ -49,7 +51,7 @@ def to_display_dict(self) -> Dict[str, str]:
4951

5052

5153
class ConfigLoader:
52-
"""Loads configuration from ~/.config/affinity-cli or an explicit path."""
54+
"""Loads configuration from ~/.config/affinity-cli, an explicit path, or environment."""
5355

5456
CONFIG_FILES = (
5557
"config.toml",
@@ -58,31 +60,60 @@ class ConfigLoader:
5860
"config.json",
5961
)
6062

61-
def __init__(self, explicit_path: Optional[str] = None) -> None:
62-
self.explicit_path = Path(explicit_path).expanduser() if explicit_path else None
63+
ENV_INSTALLERS = "AFFINITY_INSTALLERS_PATH"
64+
ENV_PREFIX = "AFFINITY_WINE_PREFIX"
65+
ENV_VERSION = "AFFINITY_DEFAULT_VERSION"
66+
67+
def __init__(self, explicit_path: Optional[str] = None, config_file: Optional[str] = None) -> None:
68+
"""
69+
Args:
70+
explicit_path: Backwards-compatible path argument (kept for callers)
71+
config_file: Preferred keyword accepted by tests/CLI
72+
"""
73+
chosen = config_file or explicit_path
74+
self.explicit_path = Path(chosen).expanduser() if chosen else None
6375
self.config_path: Optional[Path] = None
6476
self._raw_data: Dict[str, Any] = {}
6577
self.user_config = UserConfig()
6678
self._load()
6779

80+
def load(self) -> ResolvedConfig:
81+
"""
82+
Public helper used by tests and CLI entrypoint.
83+
Mirrors `derive` with no overrides.
84+
"""
85+
return self.derive()
86+
6887
def derive(
6988
self,
7089
*,
7190
installers_path: Optional[str] = None,
7291
prefix_path: Optional[str] = None,
7392
version: Optional[str] = None,
7493
) -> ResolvedConfig:
94+
"""
95+
Resolve configuration using precedence:
96+
explicit args > environment > user config file > defaults
97+
"""
98+
env_installers = os.getenv(self.ENV_INSTALLERS)
99+
env_prefix = os.getenv(self.ENV_PREFIX)
100+
env_version = os.getenv(self.ENV_VERSION)
101+
75102
installers = self._normalize_path(
76103
installers_path
104+
or env_installers
77105
or (self.user_config.installers_path and str(self.user_config.installers_path))
78106
or str(config.DEFAULT_INSTALLERS_PATH)
79107
)
80108
prefix = self._normalize_path(
81109
prefix_path
110+
or env_prefix
82111
or (self.user_config.wine_prefix and str(self.user_config.wine_prefix))
83112
or str(config.DEFAULT_WINE_PREFIX)
84113
)
85-
version_choice = (version or self.user_config.default_version or config.DEFAULT_INSTALLER_VERSION)
114+
version_choice = (
115+
(version or env_version or self.user_config.default_version or config.DEFAULT_INSTALLER_VERSION)
116+
)
86117
version_choice = version_choice.lower()
87118
if version_choice not in config.SUPPORTED_INSTALLER_VERSIONS:
88119
raise ConfigError(
@@ -93,12 +124,17 @@ def derive(
93124

94125
def _load(self) -> None:
95126
if self.explicit_path:
96-
if not self.explicit_path.exists():
97-
raise ConfigError(f"Config file not found: {self.explicit_path}")
98-
self.config_path = self.explicit_path
99-
self._raw_data = self._read_file(self.explicit_path)
100-
self.user_config = self._parse_user_config(self._raw_data)
101-
return
127+
# If the caller asked for a specific path but it does not exist,
128+
# fall back to defaults instead of crashing (friendlier UX/tests).
129+
if self.explicit_path.exists():
130+
self.config_path = self.explicit_path
131+
self._raw_data = self._read_file(self.explicit_path)
132+
self.user_config = self._parse_user_config(self._raw_data)
133+
return
134+
else:
135+
self._raw_data = {}
136+
self.user_config = UserConfig()
137+
return
102138

103139
for candidate in self.CONFIG_FILES:
104140
path = config.CONFIG_DIR / candidate
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Wine prefix management helpers."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import subprocess
7+
from pathlib import Path
8+
from typing import Optional, Tuple
9+
10+
from affinity_cli.core.wine_manager import WineManager
11+
12+
13+
class PrefixManager:
14+
"""
15+
Minimal manager for creating and checking a Wine prefix.
16+
17+
This implementation intentionally keeps side effects small; it is enough for
18+
status checks and install flows while avoiding fragile assumptions.
19+
"""
20+
21+
def __init__(self, prefix_path: Path, wine_manager: Optional[WineManager] = None) -> None:
22+
self.prefix_path = Path(prefix_path).expanduser()
23+
self.wine_manager = wine_manager or WineManager()
24+
25+
# ------------------------------------------------------------------
26+
# Basic introspection helpers
27+
# ------------------------------------------------------------------
28+
def prefix_exists(self) -> bool:
29+
"""Return True when the prefix directory looks initialized."""
30+
return (self.prefix_path / "drive_c").exists()
31+
32+
# ------------------------------------------------------------------
33+
# Creation helpers
34+
# ------------------------------------------------------------------
35+
def create_prefix(self) -> Tuple[bool, str]:
36+
"""
37+
Initialize the Wine prefix using wineboot.
38+
39+
Returns:
40+
(success flag, human-readable message)
41+
"""
42+
wine_bin = self.wine_manager.get_wine_path()
43+
if not wine_bin:
44+
return False, "Wine binary not found; install Wine first."
45+
46+
try:
47+
self.prefix_path.mkdir(parents=True, exist_ok=True)
48+
except OSError as exc: # pragma: no cover - filesystem issues
49+
return False, f"Unable to create prefix directory: {exc}"
50+
51+
env = os.environ.copy()
52+
env["WINEPREFIX"] = str(self.prefix_path)
53+
env.setdefault("WINEARCH", "win64")
54+
55+
try:
56+
result = subprocess.run(
57+
[str(wine_bin), "wineboot", "-u"],
58+
env=env,
59+
capture_output=True,
60+
text=True,
61+
timeout=120,
62+
)
63+
except subprocess.TimeoutExpired: # pragma: no cover - rare
64+
return False, "wineboot timed out while initializing the prefix."
65+
except Exception as exc: # pragma: no cover - defensive
66+
return False, f"Failed to initialize prefix: {exc}"
67+
68+
if result.returncode != 0:
69+
return False, f"wineboot failed: {result.stderr.strip() or result.stdout.strip()}"
70+
71+
return True, f"Prefix initialized at {self.prefix_path}"
72+
73+
74+
__all__ = ["PrefixManager"]

affinity_cli/installer_discovery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
}
1717

1818
INSTALLER_PATTERN = re.compile(
19-
r"^affinity-(photo|designer|publisher)(-msi)?-([0-9]+\.[0-9]+\.[0-9]+)\.exe$",
19+
r"^affinity-(photo|designer|publisher)(-msi|-msix)?-([0-9]+\.[0-9]+\.[0-9]+)\.(exe|msix)$",
2020
re.IGNORECASE,
2121
)
2222

@@ -51,7 +51,7 @@ def scan(self) -> List[InstallerInfo]:
5151
match = INSTALLER_PATTERN.match(file.name)
5252
if not match:
5353
continue
54-
product, msi_marker, file_version = match.groups()
54+
product, msi_marker, file_version, _ext = match.groups()
5555
version: VersionLiteral = "v2" if msi_marker else "v1"
5656
installers.append(
5757
InstallerInfo(

affinity_cli/main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ def cli(ctx: click.Context, config_file: Optional[str], verbose: bool) -> None:
4040

4141
@cli.command(name="list-installers")
4242
@click.option("--path", "installers_path", type=click.Path(file_okay=False), help="Directory to scan")
43+
@click.option(
44+
"--installers",
45+
"installers_path_alias",
46+
type=click.Path(file_okay=False),
47+
help="Alias for --path (matches other commands)",
48+
)
4349
@click.option(
4450
"--version",
4551
"version_filter",
@@ -50,12 +56,14 @@ def cli(ctx: click.Context, config_file: Optional[str], verbose: bool) -> None:
5056
def list_installers_cmd(
5157
ctx: click.Context,
5258
installers_path: Optional[str],
59+
installers_path_alias: Optional[str],
5360
version_filter: Optional[str],
5461
) -> None:
5562
"""List every installer detected in the configured directory."""
5663

5764
loader: ConfigLoader = ctx.obj["config_loader"]
58-
settings = loader.derive(installers_path=installers_path)
65+
path_choice = installers_path_alias or installers_path
66+
settings = loader.derive(installers_path=path_choice)
5967

6068
from affinity_cli.commands.list_installers import run_list_installers
6169

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "affinity-cli"
7-
version = "1.1.0"
7+
version = "1.1.1"
88
description = "Universal CLI installer for Affinity products on Linux"
99
readme = "README.md"
1010
requires-python = ">=3.8"
@@ -34,7 +34,7 @@ dependencies = [
3434
dev = [
3535
"pytest>=7.0",
3636
"pytest-cov>=4.0",
37-
"black>=22.0",
37+
"black>=22.0,<24.0",
3838
"flake8>=5.0",
3939
"mypy>=0.990",
4040
]

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setup(
1313
name="affinity-cli",
14-
version="1.1.0",
14+
version="1.1.1",
1515
author="ind4skylivey",
1616
description="Universal CLI installer for Affinity products on Linux",
1717
long_description=long_description,

0 commit comments

Comments
 (0)