lumen-app 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lumen_app/__init__.py +7 -0
- lumen_app/core/__init__.py +0 -0
- lumen_app/core/config.py +661 -0
- lumen_app/core/installer.py +274 -0
- lumen_app/core/loader.py +45 -0
- lumen_app/core/router.py +87 -0
- lumen_app/core/server.py +389 -0
- lumen_app/core/service.py +49 -0
- lumen_app/core/tests/__init__.py +1 -0
- lumen_app/core/tests/test_core_integration.py +561 -0
- lumen_app/core/tests/test_env_checker.py +487 -0
- lumen_app/proto/README.md +12 -0
- lumen_app/proto/ml_service.proto +88 -0
- lumen_app/proto/ml_service_pb2.py +66 -0
- lumen_app/proto/ml_service_pb2.pyi +136 -0
- lumen_app/proto/ml_service_pb2_grpc.py +251 -0
- lumen_app/server.py +362 -0
- lumen_app/utils/env_checker.py +752 -0
- lumen_app/utils/installation/__init__.py +25 -0
- lumen_app/utils/installation/env_manager.py +152 -0
- lumen_app/utils/installation/micromamba_installer.py +459 -0
- lumen_app/utils/installation/package_installer.py +149 -0
- lumen_app/utils/installation/verifier.py +95 -0
- lumen_app/utils/logger.py +181 -0
- lumen_app/utils/mamba/cuda.yaml +12 -0
- lumen_app/utils/mamba/default.yaml +6 -0
- lumen_app/utils/mamba/openvino.yaml +7 -0
- lumen_app/utils/mamba/tensorrt.yaml +13 -0
- lumen_app/utils/package_resolver.py +309 -0
- lumen_app/utils/preset_registry.py +219 -0
- lumen_app/web/__init__.py +3 -0
- lumen_app/web/api/__init__.py +1 -0
- lumen_app/web/api/config.py +229 -0
- lumen_app/web/api/hardware.py +201 -0
- lumen_app/web/api/install.py +608 -0
- lumen_app/web/api/server.py +253 -0
- lumen_app/web/core/__init__.py +1 -0
- lumen_app/web/core/server_manager.py +348 -0
- lumen_app/web/core/state.py +264 -0
- lumen_app/web/main.py +145 -0
- lumen_app/web/models/__init__.py +28 -0
- lumen_app/web/models/config.py +63 -0
- lumen_app/web/models/hardware.py +64 -0
- lumen_app/web/models/install.py +134 -0
- lumen_app/web/models/server.py +95 -0
- lumen_app/web/static/assets/index-CGuhGHC9.css +1 -0
- lumen_app/web/static/assets/index-DN6HmxWS.js +56 -0
- lumen_app/web/static/index.html +14 -0
- lumen_app/web/static/vite.svg +1 -0
- lumen_app/web/websockets/__init__.py +1 -0
- lumen_app/web/websockets/logs.py +159 -0
- lumen_app-0.4.2.dist-info/METADATA +23 -0
- lumen_app-0.4.2.dist-info/RECORD +56 -0
- lumen_app-0.4.2.dist-info/WHEEL +5 -0
- lumen_app-0.4.2.dist-info/entry_points.txt +3 -0
- lumen_app-0.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Installation utilities for Lumen.
|
|
2
|
+
|
|
3
|
+
This module provides classes for managing micromamba installation,
|
|
4
|
+
Python environment creation, package installation, and verification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .env_manager import PythonEnvManager
|
|
8
|
+
from .micromamba_installer import (
|
|
9
|
+
MicromambaCheckResult,
|
|
10
|
+
MicromambaInstaller,
|
|
11
|
+
MicromambaStatus,
|
|
12
|
+
MirrorSelector,
|
|
13
|
+
)
|
|
14
|
+
from .package_installer import LumenPackageInstaller
|
|
15
|
+
from .verifier import InstallationVerifier
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MicromambaInstaller",
|
|
19
|
+
"MicromambaCheckResult",
|
|
20
|
+
"MicromambaStatus",
|
|
21
|
+
"MirrorSelector",
|
|
22
|
+
"PythonEnvManager",
|
|
23
|
+
"LumenPackageInstaller",
|
|
24
|
+
"InstallationVerifier",
|
|
25
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Python environment manager for creating and managing micromamba environments."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PythonEnvManager:
|
|
11
|
+
"""Manages Python environment creation and pip operations."""
|
|
12
|
+
|
|
13
|
+
ENV_NAME = "lumen_env"
|
|
14
|
+
|
|
15
|
+
def __init__(self, cache_dir: Path | str, micromamba_exe: Path | str):
|
|
16
|
+
"""Initialize environment manager.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
cache_dir: Cache directory
|
|
20
|
+
micromamba_exe: Path to micromamba executable
|
|
21
|
+
"""
|
|
22
|
+
self.cache_dir = Path(cache_dir).expanduser()
|
|
23
|
+
self.micromamba_exe = Path(micromamba_exe)
|
|
24
|
+
self.mamba_configs_dir = Path(__file__).parent.parent / "mamba"
|
|
25
|
+
self.target_name = "micromamba"
|
|
26
|
+
|
|
27
|
+
logger.debug(
|
|
28
|
+
f"[PythonEnvManager] Initialized with cache_dir={self.cache_dir}, "
|
|
29
|
+
f"micromamba_exe={self.micromamba_exe}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def create_env(self, yaml_config: str = "default") -> Path:
|
|
33
|
+
"""Create Python environment using micromamba.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
yaml_config: Mamba yaml config identifier (e.g., "cuda", "openvino", "default")
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Path to created environment
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
Exception: If environment creation fails
|
|
43
|
+
"""
|
|
44
|
+
logger.info(
|
|
45
|
+
f"[PythonEnvManager] Creating environment '{self.ENV_NAME}' "
|
|
46
|
+
f"with yaml_config={yaml_config}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
env_path = self.get_env_path()
|
|
50
|
+
|
|
51
|
+
# Check if environment already exists
|
|
52
|
+
if env_path.exists():
|
|
53
|
+
logger.info(f"[PythonEnvManager] Environment already exists: {env_path}")
|
|
54
|
+
return env_path
|
|
55
|
+
|
|
56
|
+
# Ensure parent directory exists
|
|
57
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# Determine which yaml file to use
|
|
60
|
+
yaml_file = self.mamba_configs_dir / f"{yaml_config}.yaml"
|
|
61
|
+
|
|
62
|
+
if not yaml_file.exists():
|
|
63
|
+
raise Exception(f"YAML config file not found: {yaml_file}")
|
|
64
|
+
|
|
65
|
+
# Create environment from yaml config file
|
|
66
|
+
cmd = [
|
|
67
|
+
str(self.micromamba_exe),
|
|
68
|
+
"create",
|
|
69
|
+
"-y",
|
|
70
|
+
"-p", # Use path instead of name
|
|
71
|
+
str(env_path),
|
|
72
|
+
"-f",
|
|
73
|
+
str(yaml_file),
|
|
74
|
+
]
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"[PythonEnvManager] Creating environment from yaml: {' '.join(cmd)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
cmd,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=600, # 10 minutes
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if result.returncode != 0:
|
|
88
|
+
error_msg = result.stderr or result.stdout
|
|
89
|
+
logger.error(
|
|
90
|
+
f"[PythonEnvManager] Environment creation failed: {error_msg}"
|
|
91
|
+
)
|
|
92
|
+
raise Exception(f"Failed to create environment: {error_msg}")
|
|
93
|
+
|
|
94
|
+
logger.info(f"[PythonEnvManager] Environment created: {env_path}")
|
|
95
|
+
return env_path
|
|
96
|
+
|
|
97
|
+
except subprocess.TimeoutExpired:
|
|
98
|
+
logger.error("[PythonEnvManager] Environment creation timed out")
|
|
99
|
+
raise Exception("Environment creation timed out")
|
|
100
|
+
|
|
101
|
+
def get_env_path(self) -> Path:
|
|
102
|
+
"""Get the path to the lumen_env environment.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Path to environment directory
|
|
106
|
+
"""
|
|
107
|
+
env_path = self.cache_dir / self.target_name / "envs" / self.ENV_NAME
|
|
108
|
+
return env_path
|
|
109
|
+
|
|
110
|
+
def run_pip(self, *args: str) -> subprocess.CompletedProcess:
|
|
111
|
+
"""Run pip command in the lumen_env environment.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
*args: Pip command arguments (e.g., "install", "package")
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Completed process result
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
Exception: If command fails
|
|
121
|
+
"""
|
|
122
|
+
env_path = self.get_env_path()
|
|
123
|
+
cmd = [
|
|
124
|
+
str(self.micromamba_exe),
|
|
125
|
+
"run",
|
|
126
|
+
"-p", # Use path instead of name
|
|
127
|
+
str(env_path),
|
|
128
|
+
"pip",
|
|
129
|
+
*args,
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
logger.debug(f"[PythonEnvManager] Running pip: {' '.join(cmd)}")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
cmd,
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
timeout=600, # 10 minutes
|
|
140
|
+
)
|
|
141
|
+
return result
|
|
142
|
+
except subprocess.TimeoutExpired:
|
|
143
|
+
logger.error("[PythonEnvManager] Pip command timed out")
|
|
144
|
+
raise Exception("Pip command timed out")
|
|
145
|
+
|
|
146
|
+
def env_exists(self) -> bool:
|
|
147
|
+
"""Check if lumen_env exists.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if environment exists
|
|
151
|
+
"""
|
|
152
|
+
return self.get_env_path().exists()
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Micromamba installer for managing micromamba installation and checking.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to:
|
|
4
|
+
1. Check if micromamba is installed and accessible
|
|
5
|
+
2. Download and install micromamba with region-based mirror support
|
|
6
|
+
3. Get the path to micromamba executable
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from lumen_resources.lumen_config import Region
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MicromambaStatus(Enum):
|
|
23
|
+
"""Micromamba installation status."""
|
|
24
|
+
|
|
25
|
+
INSTALLED = "installed"
|
|
26
|
+
NOT_INSTALLED = "not_installed"
|
|
27
|
+
INCOMPATIBLE = "incompatible"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MicromambaCheckResult:
|
|
32
|
+
"""Result of micromamba availability check.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
status: Installation status
|
|
36
|
+
version: Version string if installed
|
|
37
|
+
executable_path: Path to micromamba executable
|
|
38
|
+
details: Additional details
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
status: MicromambaStatus
|
|
42
|
+
version: str | None = None
|
|
43
|
+
executable_path: str | None = None
|
|
44
|
+
details: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MirrorSelector:
|
|
48
|
+
"""Selects mirror URLs based on region for micromamba downloads."""
|
|
49
|
+
|
|
50
|
+
GITHUB_MIRROR_CN = "https://gh-proxy.org/https://github.com"
|
|
51
|
+
|
|
52
|
+
def get_micromamba_urls(self, base_url: str, region: Region) -> list[str]:
|
|
53
|
+
"""Get micromamba download URLs with mirror fallback.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
base_url: Original GitHub URL
|
|
57
|
+
region: Region.cn or Region.other
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of URLs to try (mirror first if cn, then original)
|
|
61
|
+
"""
|
|
62
|
+
urls = []
|
|
63
|
+
if region == Region.cn:
|
|
64
|
+
# Apply ghproxy mirror for Chinese users
|
|
65
|
+
mirror_url = base_url.replace("https://github.com", self.GITHUB_MIRROR_CN)
|
|
66
|
+
urls.append(mirror_url)
|
|
67
|
+
urls.append(base_url)
|
|
68
|
+
return urls
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MicromambaInstaller:
|
|
72
|
+
"""Manages micromamba installation and retrieval.
|
|
73
|
+
|
|
74
|
+
This class provides a complete solution for:
|
|
75
|
+
- Checking if micromamba is installed
|
|
76
|
+
- Downloading and installing micromamba with mirror support
|
|
77
|
+
- Getting the executable path
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, cache_dir: Path | str, region: Region = Region.other):
|
|
81
|
+
"""Initialize installer.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
cache_dir: Cache directory for micromamba installation
|
|
85
|
+
region: Region for mirror selection
|
|
86
|
+
"""
|
|
87
|
+
self.cache_dir = Path(cache_dir).expanduser()
|
|
88
|
+
self.target_name = "micromamba"
|
|
89
|
+
self.region = region
|
|
90
|
+
self.mirror_selector = MirrorSelector()
|
|
91
|
+
|
|
92
|
+
logger.debug(
|
|
93
|
+
f"[MicromambaInstaller] Initialized: cache_dir={self.cache_dir}, region={region.value}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def check(self, custom_path: str | None = None) -> MicromambaCheckResult:
|
|
97
|
+
"""Check if micromamba is installed and accessible.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
custom_path: Optional path to micromamba executable.
|
|
101
|
+
If None, checks PATH and install directory.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
MicromambaCheckResult with status and details
|
|
105
|
+
"""
|
|
106
|
+
logger.debug("[MicromambaInstaller] Checking micromamba availability")
|
|
107
|
+
|
|
108
|
+
# Determine executable path
|
|
109
|
+
if custom_path:
|
|
110
|
+
exe_path = Path(custom_path)
|
|
111
|
+
logger.debug(f"[MicromambaInstaller] Using custom path: {custom_path}")
|
|
112
|
+
else:
|
|
113
|
+
# First check if it's in our install directory
|
|
114
|
+
exe_path = self.get_executable()
|
|
115
|
+
if not exe_path.exists():
|
|
116
|
+
# Fall back to PATH
|
|
117
|
+
exe_path = Path("micromamba")
|
|
118
|
+
logger.debug("[MicromambaInstaller] Checking PATH for micromamba")
|
|
119
|
+
|
|
120
|
+
# Try to run micromamba --version
|
|
121
|
+
try:
|
|
122
|
+
result = subprocess.run(
|
|
123
|
+
[str(exe_path), "--version"],
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
timeout=5,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if result.returncode == 0:
|
|
130
|
+
version = result.stdout.strip().split()[-1]
|
|
131
|
+
logger.info(
|
|
132
|
+
f"[MicromambaInstaller] Micromamba found: version {version}"
|
|
133
|
+
)
|
|
134
|
+
return MicromambaCheckResult(
|
|
135
|
+
status=MicromambaStatus.INSTALLED,
|
|
136
|
+
version=version,
|
|
137
|
+
executable_path=str(exe_path),
|
|
138
|
+
details=f"version {version}",
|
|
139
|
+
)
|
|
140
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
141
|
+
logger.debug(f"[MicromambaInstaller] Check failed: {e}")
|
|
142
|
+
|
|
143
|
+
logger.info("[MicromambaInstaller] Micromamba not available")
|
|
144
|
+
return MicromambaCheckResult(
|
|
145
|
+
status=MicromambaStatus.NOT_INSTALLED,
|
|
146
|
+
details=f"micromamba not found at {exe_path}",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def install(self, dry_run: bool = False) -> Path:
|
|
150
|
+
"""Download and install micromamba to cache_dir.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
dry_run: If True, only print commands without executing
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Path to micromamba executable
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
Exception: If installation fails
|
|
160
|
+
"""
|
|
161
|
+
logger.info(f"[MicromambaInstaller] Installing micromamba (dry_run={dry_run})")
|
|
162
|
+
|
|
163
|
+
install_dir = self.cache_dir / self.target_name
|
|
164
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
# Determine executable path based on platform
|
|
167
|
+
exe_path = self._get_executable_path(install_dir)
|
|
168
|
+
|
|
169
|
+
# Check if already installed AND executable
|
|
170
|
+
if exe_path.exists():
|
|
171
|
+
# Verify it's executable
|
|
172
|
+
if platform.system() != "Windows" and not os.access(exe_path, os.X_OK):
|
|
173
|
+
logger.warning(
|
|
174
|
+
f"[MicromambaInstaller] File exists but not executable: {exe_path}"
|
|
175
|
+
)
|
|
176
|
+
logger.info("[MicromambaInstaller] Fixing permissions...")
|
|
177
|
+
try:
|
|
178
|
+
subprocess.run(
|
|
179
|
+
["chmod", "+x", str(exe_path)],
|
|
180
|
+
capture_output=True,
|
|
181
|
+
text=True,
|
|
182
|
+
timeout=10,
|
|
183
|
+
)
|
|
184
|
+
logger.info(f"[MicromambaInstaller] Fixed permissions: {exe_path}")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(
|
|
187
|
+
f"[MicromambaInstaller] Failed to fix permissions: {e}"
|
|
188
|
+
)
|
|
189
|
+
# Fall through to re-download
|
|
190
|
+
|
|
191
|
+
# Test if it works
|
|
192
|
+
try:
|
|
193
|
+
result = subprocess.run(
|
|
194
|
+
[str(exe_path), "--version"],
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
timeout=5,
|
|
198
|
+
)
|
|
199
|
+
if result.returncode == 0:
|
|
200
|
+
logger.info(
|
|
201
|
+
f"[MicromambaInstaller] Already installed and working: {exe_path}"
|
|
202
|
+
)
|
|
203
|
+
return exe_path
|
|
204
|
+
else:
|
|
205
|
+
logger.warning(
|
|
206
|
+
f"[MicromambaInstaller] File exists but not working: {result.stderr}"
|
|
207
|
+
)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.warning(
|
|
210
|
+
f"[MicromambaInstaller] Executable test failed: {e}, re-downloading..."
|
|
211
|
+
)
|
|
212
|
+
# Remove broken file and re-download
|
|
213
|
+
exe_path.unlink()
|
|
214
|
+
|
|
215
|
+
# Build installation command based on platform
|
|
216
|
+
system = platform.system()
|
|
217
|
+
|
|
218
|
+
if system == "Windows":
|
|
219
|
+
success, message = self._install_windows(install_dir, dry_run)
|
|
220
|
+
else:
|
|
221
|
+
success, message = self._install_unix(install_dir, exe_path, dry_run)
|
|
222
|
+
|
|
223
|
+
if not success:
|
|
224
|
+
raise Exception(f"Failed to install micromamba: {message}")
|
|
225
|
+
|
|
226
|
+
# Verify installation
|
|
227
|
+
if not exe_path.exists():
|
|
228
|
+
raise Exception(
|
|
229
|
+
f"Installation succeeded but executable not found: {exe_path}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
logger.info(f"[MicromambaInstaller] Successfully installed: {exe_path}")
|
|
233
|
+
return exe_path
|
|
234
|
+
|
|
235
|
+
def ensure_installed(self) -> Path:
|
|
236
|
+
"""Ensure micromamba is installed, installing if necessary.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Path to micromamba executable
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
Exception: If installation check or install fails
|
|
243
|
+
"""
|
|
244
|
+
result = self.check()
|
|
245
|
+
|
|
246
|
+
if result.status == MicromambaStatus.INSTALLED:
|
|
247
|
+
logger.debug("[MicromambaInstaller] Already installed, skipping")
|
|
248
|
+
if result.executable_path is None:
|
|
249
|
+
raise Exception("Executable path is None despite being installed")
|
|
250
|
+
return Path(result.executable_path)
|
|
251
|
+
|
|
252
|
+
logger.info("[MicromambaInstaller] Not installed, installing now")
|
|
253
|
+
return self.install()
|
|
254
|
+
|
|
255
|
+
def get_executable(self) -> Path:
|
|
256
|
+
"""Get micromamba executable path in install directory.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Path to micromamba executable
|
|
260
|
+
"""
|
|
261
|
+
install_dir = self.cache_dir / self.target_name
|
|
262
|
+
return self._get_executable_path(install_dir)
|
|
263
|
+
|
|
264
|
+
def _get_executable_path(self, install_dir: Path) -> Path:
|
|
265
|
+
"""Get executable path for current platform.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
install_dir: Installation directory
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Path to micromamba executable
|
|
272
|
+
"""
|
|
273
|
+
if platform.system() == "Windows":
|
|
274
|
+
return install_dir / "bin" / "micromamba.exe"
|
|
275
|
+
else:
|
|
276
|
+
return install_dir / "bin" / "micromamba"
|
|
277
|
+
|
|
278
|
+
def _install_windows(self, install_dir: Path, dry_run: bool) -> tuple[bool, str]:
|
|
279
|
+
"""Install micromamba on Windows.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
install_dir: Installation directory
|
|
283
|
+
dry_run: If True, only print command without executing
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Tuple of (success, message)
|
|
287
|
+
"""
|
|
288
|
+
logger.debug("[MicromambaInstaller] Installing on Windows")
|
|
289
|
+
|
|
290
|
+
install_script = install_dir / "install.ps1"
|
|
291
|
+
cmd = [
|
|
292
|
+
"powershell",
|
|
293
|
+
"-Command",
|
|
294
|
+
f"Invoke-WebRequest -Uri https://micro.mamba.pm/install.ps1 -OutFile {install_script}; "
|
|
295
|
+
f"& {install_script} -prefix {install_dir} -batch",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
if dry_run:
|
|
299
|
+
logger.info(f"[MicromambaInstaller] Would run: {' '.join(cmd)}")
|
|
300
|
+
return True, f"Would run: {' '.join(cmd)}"
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=400)
|
|
304
|
+
|
|
305
|
+
if result.returncode == 0:
|
|
306
|
+
return True, f"Installed to {self._get_executable_path(install_dir)}"
|
|
307
|
+
else:
|
|
308
|
+
return False, f"Installation failed: {result.stderr}"
|
|
309
|
+
|
|
310
|
+
except subprocess.TimeoutExpired:
|
|
311
|
+
return False, "Installation timed out"
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return False, f"Installation error: {str(e)}"
|
|
314
|
+
|
|
315
|
+
def _install_unix(
|
|
316
|
+
self, install_dir: Path, exe_path: Path, dry_run: bool
|
|
317
|
+
) -> tuple[bool, str]:
|
|
318
|
+
"""Install micromamba on Unix (Linux/macOS).
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
install_dir: Installation directory
|
|
322
|
+
exe_path: Expected executable path
|
|
323
|
+
dry_run: If True, only print command without executing
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Tuple of (success, message)
|
|
327
|
+
"""
|
|
328
|
+
logger.debug("[MicromambaInstaller] Installing on Unix")
|
|
329
|
+
|
|
330
|
+
# Detect platform and architecture
|
|
331
|
+
platform_name, arch = self._detect_platform()
|
|
332
|
+
|
|
333
|
+
# Build download URL
|
|
334
|
+
base_url = f"https://github.com/mamba-org/micromamba-releases/releases/latest/download/micromamba-{platform_name}-{arch}"
|
|
335
|
+
|
|
336
|
+
# Get URLs with mirror fallback
|
|
337
|
+
download_urls = self.mirror_selector.get_micromamba_urls(base_url, self.region)
|
|
338
|
+
|
|
339
|
+
logger.debug(
|
|
340
|
+
f"[MicromambaInstaller] Platform: {platform_name}-{arch}, URLs: {download_urls}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Create bin directory
|
|
344
|
+
bin_dir = install_dir / "bin"
|
|
345
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
|
|
347
|
+
# Try each URL
|
|
348
|
+
for download_url in download_urls:
|
|
349
|
+
logger.debug(f"[MicromambaInstaller] Trying URL: {download_url}")
|
|
350
|
+
|
|
351
|
+
download_cmd = ["curl", "-fsSL", download_url, "-o", str(exe_path)]
|
|
352
|
+
chmod_cmd = ["chmod", "+x", str(exe_path)]
|
|
353
|
+
|
|
354
|
+
if dry_run:
|
|
355
|
+
logger.info(
|
|
356
|
+
f"[MicromambaInstaller] Would run: {' '.join(download_cmd)}"
|
|
357
|
+
)
|
|
358
|
+
return True, f"Would download from {download_url}"
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
# Download
|
|
362
|
+
logger.info("[MicromambaInstaller] Downloading micromamba...")
|
|
363
|
+
download_result = subprocess.run(
|
|
364
|
+
download_cmd, capture_output=True, text=True, timeout=120
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if download_result.returncode != 0:
|
|
368
|
+
logger.warning(
|
|
369
|
+
f"[MicromambaInstaller] Download failed: {download_result.stderr}"
|
|
370
|
+
)
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# Make executable
|
|
374
|
+
subprocess.run(chmod_cmd, capture_output=True, text=True, timeout=10)
|
|
375
|
+
|
|
376
|
+
# Configure conda-forge channels
|
|
377
|
+
self._configure_channels(exe_path)
|
|
378
|
+
|
|
379
|
+
logger.info(f"[MicromambaInstaller] Successfully installed: {exe_path}")
|
|
380
|
+
return True, f"Installed to {exe_path}"
|
|
381
|
+
|
|
382
|
+
except subprocess.TimeoutExpired:
|
|
383
|
+
logger.warning("[MicromambaInstaller] Download timed out")
|
|
384
|
+
continue
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.warning(
|
|
387
|
+
f"[MicromambaInstaller] Install error: {type(e).__name__}: {e}"
|
|
388
|
+
)
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
return False, "Failed to download from all sources"
|
|
392
|
+
|
|
393
|
+
def _detect_platform(self) -> tuple[str, str]:
|
|
394
|
+
"""Detect platform and architecture for micromamba download.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Tuple of (platform_name, arch)
|
|
398
|
+
- platform_name: "linux" or "osx"
|
|
399
|
+
- arch: "64", "aarch64", "arm64", or "ppc64le"
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
Exception: If platform detection fails
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
# Get OS platform
|
|
406
|
+
uname_result = subprocess.run(
|
|
407
|
+
["uname", "-s"], capture_output=True, text=True, timeout=5
|
|
408
|
+
)
|
|
409
|
+
uname_s = uname_result.stdout.strip()
|
|
410
|
+
|
|
411
|
+
if uname_s == "Linux":
|
|
412
|
+
platform_name = "linux"
|
|
413
|
+
elif uname_s == "Darwin":
|
|
414
|
+
platform_name = "osx"
|
|
415
|
+
else:
|
|
416
|
+
raise Exception(f"Unsupported platform: {uname_s}")
|
|
417
|
+
|
|
418
|
+
# Get architecture
|
|
419
|
+
uname_result = subprocess.run(
|
|
420
|
+
["uname", "-m"], capture_output=True, text=True, timeout=5
|
|
421
|
+
)
|
|
422
|
+
uname_m = uname_result.stdout.strip()
|
|
423
|
+
|
|
424
|
+
# Map architecture names (same logic as install.sh)
|
|
425
|
+
if uname_m in ("aarch64", "ppc64le", "arm64"):
|
|
426
|
+
arch = uname_m
|
|
427
|
+
else:
|
|
428
|
+
arch = "64"
|
|
429
|
+
|
|
430
|
+
logger.debug(f"[MicromambaInstaller] Detected: {platform_name}-{arch}")
|
|
431
|
+
return platform_name, arch
|
|
432
|
+
|
|
433
|
+
except Exception as e:
|
|
434
|
+
logger.error(f"[MicromambaInstaller] Platform detection failed: {e}")
|
|
435
|
+
raise Exception(f"Failed to detect platform: {e}")
|
|
436
|
+
|
|
437
|
+
def _configure_channels(self, exe_path: Path) -> None:
|
|
438
|
+
"""Configure conda-forge channels for micromamba.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
exe_path: Path to micromamba executable
|
|
442
|
+
"""
|
|
443
|
+
logger.debug("[MicromambaInstaller] Configuring conda-forge channels")
|
|
444
|
+
|
|
445
|
+
config_commands = [
|
|
446
|
+
[str(exe_path), "config", "append", "channels", "conda-forge"],
|
|
447
|
+
[str(exe_path), "config", "append", "channels", "nodefaults"],
|
|
448
|
+
[str(exe_path), "config", "set", "channel_priority", "strict"],
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
for cmd in config_commands:
|
|
452
|
+
try:
|
|
453
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
454
|
+
if result.returncode != 0:
|
|
455
|
+
logger.warning(
|
|
456
|
+
f"[MicromambaInstaller] Config failed: {result.stderr}"
|
|
457
|
+
)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.warning(f"[MicromambaInstaller] Config error: {e}")
|