provide-foundation 0.0.0.dev2__py3-none-any.whl → 0.0.0.dev3__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.
- provide/foundation/__init__.py +20 -20
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +90 -91
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +4 -4
- provide/foundation/cli/commands/logs/__init__.py +2 -2
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +3 -3
- provide/foundation/cli/commands/logs/send.py +2 -2
- provide/foundation/cli/commands/logs/tail.py +2 -2
- provide/foundation/cli/decorators.py +0 -1
- provide/foundation/cli/testing.py +0 -5
- provide/foundation/cli/utils.py +1 -2
- provide/foundation/config/__init__.py +19 -19
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +81 -83
- provide/foundation/config/defaults.py +1 -1
- provide/foundation/config/env.py +2 -1
- provide/foundation/config/loader.py +1 -1
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/output.py +7 -7
- provide/foundation/context/core.py +19 -17
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -2
- provide/foundation/errors/decorators.py +0 -3
- provide/foundation/errors/types.py +0 -1
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +1 -1
- provide/foundation/file/lock.py +2 -3
- provide/foundation/hub/components.py +19 -21
- provide/foundation/hub/config.py +25 -19
- provide/foundation/hub/discovery.py +5 -4
- provide/foundation/hub/handlers.py +13 -5
- provide/foundation/hub/lifecycle.py +10 -9
- provide/foundation/hub/manager.py +3 -0
- provide/foundation/hub/processors.py +8 -3
- provide/foundation/integrations/__init__.py +1 -1
- provide/foundation/integrations/openobserve/client.py +2 -2
- provide/foundation/integrations/openobserve/commands.py +9 -9
- provide/foundation/integrations/openobserve/config.py +2 -2
- provide/foundation/integrations/openobserve/otlp.py +2 -2
- provide/foundation/integrations/openobserve/search.py +1 -2
- provide/foundation/integrations/openobserve/streaming.py +1 -1
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +19 -19
- provide/foundation/logger/config/telemetry.py +11 -13
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +38 -24
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +1 -1
- provide/foundation/process/__init__.py +1 -1
- provide/foundation/process/exit.py +6 -5
- provide/foundation/process/lifecycle.py +41 -18
- provide/foundation/resilience/__init__.py +6 -5
- provide/foundation/resilience/circuit.py +32 -30
- provide/foundation/resilience/decorators.py +58 -42
- provide/foundation/resilience/fallback.py +55 -40
- provide/foundation/resilience/retry.py +67 -65
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +8 -9
- provide/foundation/streams/console.py +3 -3
- provide/foundation/streams/core.py +2 -2
- provide/foundation/streams/file.py +1 -1
- provide/foundation/testing/__init__.py +22 -7
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +3 -6
- provide/foundation/testing/common/__init__.py +13 -13
- provide/foundation/testing/common/fixtures.py +27 -30
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +65 -92
- provide/foundation/testing/file/directory_fixtures.py +19 -19
- provide/foundation/testing/file/fixtures.py +14 -17
- provide/foundation/testing/file/special_fixtures.py +34 -42
- provide/foundation/testing/logger.py +28 -23
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +89 -80
- provide/foundation/testing/process/fixtures.py +11 -13
- provide/foundation/testing/process/subprocess_fixtures.py +41 -40
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +21 -17
- provide/foundation/testing/threading/data_fixtures.py +18 -16
- provide/foundation/testing/threading/execution_fixtures.py +67 -52
- provide/foundation/testing/threading/fixtures.py +10 -14
- provide/foundation/testing/threading/sync_fixtures.py +21 -18
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +91 -79
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +62 -69
- provide/foundation/tools/downloader.py +51 -56
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +1 -13
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +11 -13
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +86 -81
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +3 -2
- provide/foundation/utils/parsing.py +7 -7
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -5,14 +5,14 @@ Provides capabilities for downloading tools with progress tracking,
|
|
5
5
|
parallel downloads, and mirror support.
|
6
6
|
"""
|
7
7
|
|
8
|
-
import hashlib
|
9
8
|
from concurrent.futures import ThreadPoolExecutor
|
9
|
+
import hashlib
|
10
10
|
from pathlib import Path
|
11
11
|
from typing import Callable
|
12
12
|
|
13
13
|
from provide.foundation.errors import FoundationError
|
14
14
|
from provide.foundation.logger import get_logger
|
15
|
-
from provide.foundation.resilience import retry
|
15
|
+
from provide.foundation.resilience import retry
|
16
16
|
from provide.foundation.transport import UniversalClient
|
17
17
|
|
18
18
|
log = get_logger(__name__)
|
@@ -20,48 +20,48 @@ log = get_logger(__name__)
|
|
20
20
|
|
21
21
|
class DownloadError(FoundationError):
|
22
22
|
"""Raised when download fails."""
|
23
|
-
|
23
|
+
|
24
24
|
pass
|
25
25
|
|
26
26
|
|
27
27
|
class ToolDownloader:
|
28
28
|
"""
|
29
29
|
Advanced download capabilities for tools.
|
30
|
-
|
30
|
+
|
31
31
|
Features:
|
32
32
|
- Progress reporting with callbacks
|
33
33
|
- Parallel downloads for multiple files
|
34
34
|
- Mirror fallback support
|
35
35
|
- Checksum verification
|
36
|
-
|
36
|
+
|
37
37
|
Attributes:
|
38
38
|
client: Transport client for HTTP requests.
|
39
39
|
progress_callbacks: List of progress callback functions.
|
40
40
|
"""
|
41
|
-
|
41
|
+
|
42
42
|
def __init__(self, client: UniversalClient):
|
43
43
|
"""
|
44
44
|
Initialize the downloader.
|
45
|
-
|
45
|
+
|
46
46
|
Args:
|
47
47
|
client: Universal client for making HTTP requests.
|
48
48
|
"""
|
49
49
|
self.client = client
|
50
50
|
self.progress_callbacks: list[Callable[[int, int], None]] = []
|
51
|
-
|
51
|
+
|
52
52
|
def add_progress_callback(self, callback: Callable[[int, int], None]) -> None:
|
53
53
|
"""
|
54
54
|
Add a progress callback.
|
55
|
-
|
55
|
+
|
56
56
|
Args:
|
57
57
|
callback: Function that receives (downloaded_bytes, total_bytes).
|
58
58
|
"""
|
59
59
|
self.progress_callbacks.append(callback)
|
60
|
-
|
60
|
+
|
61
61
|
def _report_progress(self, downloaded: int, total: int) -> None:
|
62
62
|
"""
|
63
63
|
Report progress to all callbacks.
|
64
|
-
|
64
|
+
|
65
65
|
Args:
|
66
66
|
downloaded: Bytes downloaded so far.
|
67
67
|
total: Total bytes to download (0 if unknown).
|
@@ -71,101 +71,95 @@ class ToolDownloader:
|
|
71
71
|
callback(downloaded, total)
|
72
72
|
except Exception as e:
|
73
73
|
log.warning(f"Progress callback failed: {e}")
|
74
|
-
|
74
|
+
|
75
75
|
@retry(max_attempts=3, base_delay=1.0)
|
76
76
|
def download_with_progress(
|
77
|
-
self,
|
78
|
-
url: str,
|
79
|
-
dest: Path,
|
80
|
-
checksum: str | None = None
|
77
|
+
self, url: str, dest: Path, checksum: str | None = None
|
81
78
|
) -> Path:
|
82
79
|
"""
|
83
80
|
Download a file with progress reporting.
|
84
|
-
|
81
|
+
|
85
82
|
Args:
|
86
83
|
url: URL to download from.
|
87
84
|
dest: Destination file path.
|
88
85
|
checksum: Optional checksum for verification.
|
89
|
-
|
86
|
+
|
90
87
|
Returns:
|
91
88
|
Path to the downloaded file.
|
92
|
-
|
89
|
+
|
93
90
|
Raises:
|
94
91
|
DownloadError: If download or verification fails.
|
95
92
|
"""
|
96
93
|
log.debug(f"Downloading {url} to {dest}")
|
97
|
-
|
94
|
+
|
98
95
|
# Ensure parent directory exists
|
99
96
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
100
|
-
|
97
|
+
|
101
98
|
# Stream download with progress
|
102
99
|
with self.client.stream("GET", url) as response:
|
103
100
|
# Get total size if available
|
104
101
|
total_size = int(response.headers.get("content-length", 0))
|
105
102
|
downloaded = 0
|
106
|
-
|
103
|
+
|
107
104
|
# Write to file and report progress
|
108
105
|
with dest.open("wb") as f:
|
109
106
|
for chunk in response.iter_bytes(8192):
|
110
107
|
f.write(chunk)
|
111
108
|
downloaded += len(chunk)
|
112
109
|
self._report_progress(downloaded, total_size)
|
113
|
-
|
110
|
+
|
114
111
|
# Verify checksum if provided
|
115
112
|
if checksum:
|
116
113
|
if not self.verify_checksum(dest, checksum):
|
117
114
|
dest.unlink()
|
118
115
|
raise DownloadError(f"Checksum mismatch for {url}")
|
119
|
-
|
116
|
+
|
120
117
|
log.info(f"Downloaded {url} successfully")
|
121
118
|
return dest
|
122
|
-
|
119
|
+
|
123
120
|
def verify_checksum(self, file_path: Path, expected: str) -> bool:
|
124
121
|
"""
|
125
122
|
Verify file checksum.
|
126
|
-
|
123
|
+
|
127
124
|
Args:
|
128
125
|
file_path: Path to file to verify.
|
129
126
|
expected: Expected checksum (hex string).
|
130
|
-
|
127
|
+
|
131
128
|
Returns:
|
132
129
|
True if checksum matches, False otherwise.
|
133
130
|
"""
|
134
131
|
# Default to SHA256
|
135
132
|
hasher = hashlib.sha256()
|
136
|
-
|
133
|
+
|
137
134
|
with file_path.open("rb") as f:
|
138
135
|
for chunk in iter(lambda: f.read(8192), b""):
|
139
136
|
hasher.update(chunk)
|
140
|
-
|
137
|
+
|
141
138
|
actual = hasher.hexdigest()
|
142
139
|
return actual == expected
|
143
|
-
|
144
|
-
def download_parallel(
|
145
|
-
self,
|
146
|
-
urls: list[tuple[str, Path]]
|
147
|
-
) -> list[Path]:
|
140
|
+
|
141
|
+
def download_parallel(self, urls: list[tuple[str, Path]]) -> list[Path]:
|
148
142
|
"""
|
149
143
|
Download multiple files in parallel.
|
150
|
-
|
144
|
+
|
151
145
|
Args:
|
152
146
|
urls: List of (url, destination) tuples.
|
153
|
-
|
147
|
+
|
154
148
|
Returns:
|
155
149
|
List of downloaded file paths in the same order as input.
|
156
|
-
|
150
|
+
|
157
151
|
Raises:
|
158
152
|
DownloadError: If any download fails.
|
159
153
|
"""
|
160
154
|
errors = []
|
161
|
-
|
155
|
+
|
162
156
|
with ThreadPoolExecutor(max_workers=4) as executor:
|
163
157
|
# Submit all downloads, maintaining order with index
|
164
158
|
futures = [
|
165
159
|
executor.submit(self.download_with_progress, url, dest)
|
166
160
|
for url, dest in urls
|
167
161
|
]
|
168
|
-
|
162
|
+
|
169
163
|
# Collect results in order
|
170
164
|
results = []
|
171
165
|
for i, future in enumerate(futures):
|
@@ -176,49 +170,50 @@ class ToolDownloader:
|
|
176
170
|
except Exception as e:
|
177
171
|
errors.append((url, e))
|
178
172
|
log.error(f"Failed to download {url}: {e}")
|
179
|
-
|
173
|
+
|
180
174
|
if errors:
|
181
175
|
raise DownloadError(f"Some downloads failed: {errors}")
|
182
|
-
|
176
|
+
|
183
177
|
return results
|
184
|
-
|
185
|
-
def download_with_mirrors(
|
186
|
-
self,
|
187
|
-
mirrors: list[str],
|
188
|
-
dest: Path
|
189
|
-
) -> Path:
|
178
|
+
|
179
|
+
def download_with_mirrors(self, mirrors: list[str], dest: Path) -> Path:
|
190
180
|
"""
|
191
181
|
Try multiple mirrors until one succeeds using fallback pattern.
|
192
|
-
|
182
|
+
|
193
183
|
Args:
|
194
184
|
mirrors: List of mirror URLs to try.
|
195
185
|
dest: Destination file path.
|
196
|
-
|
186
|
+
|
197
187
|
Returns:
|
198
188
|
Path to downloaded file.
|
199
|
-
|
189
|
+
|
200
190
|
Raises:
|
201
191
|
DownloadError: If all mirrors fail.
|
202
192
|
"""
|
203
193
|
from provide.foundation.resilience.fallback import FallbackChain
|
204
|
-
|
194
|
+
|
205
195
|
if not mirrors:
|
206
196
|
raise DownloadError("No mirrors provided")
|
207
|
-
|
197
|
+
|
208
198
|
# Create fallback functions for each mirror
|
209
199
|
fallback_funcs = []
|
210
200
|
for mirror_url in mirrors:
|
201
|
+
|
211
202
|
def create_mirror_func(url):
|
212
203
|
def mirror_download():
|
213
204
|
log.debug(f"Trying mirror: {url}")
|
214
205
|
return self.download_with_progress(url, dest)
|
206
|
+
|
215
207
|
return mirror_download
|
208
|
+
|
216
209
|
fallback_funcs.append(create_mirror_func(mirror_url))
|
217
|
-
|
210
|
+
|
218
211
|
# Use FallbackChain to try mirrors in order
|
219
|
-
chain = FallbackChain(
|
220
|
-
|
212
|
+
chain = FallbackChain(
|
213
|
+
fallbacks=fallback_funcs[1:]
|
214
|
+
) # All but first are fallbacks
|
215
|
+
|
221
216
|
try:
|
222
217
|
return chain.execute(fallback_funcs[0]) # First is primary
|
223
218
|
except Exception as e:
|
224
|
-
raise DownloadError(f"All mirrors failed: {e}")
|
219
|
+
raise DownloadError(f"All mirrors failed: {e}")
|
@@ -5,11 +5,10 @@ Handles extraction and installation of tools from different
|
|
5
5
|
archive formats (zip, tar, gz, etc.) and binary files.
|
6
6
|
"""
|
7
7
|
|
8
|
+
from pathlib import Path
|
8
9
|
import shutil
|
9
10
|
import tarfile
|
10
|
-
import tempfile
|
11
11
|
import zipfile
|
12
|
-
from pathlib import Path
|
13
12
|
|
14
13
|
from provide.foundation.errors import FoundationError
|
15
14
|
from provide.foundation.logger import get_logger
|
@@ -20,43 +19,43 @@ log = get_logger(__name__)
|
|
20
19
|
|
21
20
|
class InstallError(FoundationError):
|
22
21
|
"""Raised when installation fails."""
|
23
|
-
|
22
|
+
|
24
23
|
pass
|
25
24
|
|
26
25
|
|
27
26
|
class ToolInstaller:
|
28
27
|
"""
|
29
28
|
Handle tool installation from various artifact formats.
|
30
|
-
|
29
|
+
|
31
30
|
Supports:
|
32
31
|
- ZIP archives
|
33
32
|
- TAR archives (with compression)
|
34
33
|
- Single binary files
|
35
34
|
- Platform-specific installation patterns
|
36
35
|
"""
|
37
|
-
|
36
|
+
|
38
37
|
def install(self, artifact: Path, metadata: ToolMetadata) -> Path:
|
39
38
|
"""
|
40
39
|
Install tool from artifact.
|
41
|
-
|
40
|
+
|
42
41
|
Args:
|
43
42
|
artifact: Path to downloaded artifact.
|
44
43
|
metadata: Tool metadata with installation info.
|
45
|
-
|
44
|
+
|
46
45
|
Returns:
|
47
46
|
Path to installed tool directory.
|
48
|
-
|
47
|
+
|
49
48
|
Raises:
|
50
49
|
InstallError: If installation fails.
|
51
50
|
"""
|
52
51
|
if not artifact.exists():
|
53
52
|
raise InstallError(f"Artifact not found: {artifact}")
|
54
|
-
|
53
|
+
|
55
54
|
# Determine install directory
|
56
55
|
install_dir = self.get_install_dir(metadata)
|
57
|
-
|
56
|
+
|
58
57
|
log.info(f"Installing {metadata.name} {metadata.version} to {install_dir}")
|
59
|
-
|
58
|
+
|
60
59
|
# Extract based on file type
|
61
60
|
suffix = artifact.suffix.lower()
|
62
61
|
if suffix == ".zip":
|
@@ -67,65 +66,65 @@ class ToolInstaller:
|
|
67
66
|
self.install_binary(artifact, install_dir, metadata)
|
68
67
|
else:
|
69
68
|
raise InstallError(f"Unknown artifact type: {suffix}")
|
70
|
-
|
69
|
+
|
71
70
|
# Set permissions
|
72
71
|
self.set_permissions(install_dir, metadata)
|
73
|
-
|
72
|
+
|
74
73
|
# Create symlinks if needed
|
75
74
|
self.create_symlinks(install_dir, metadata)
|
76
|
-
|
75
|
+
|
77
76
|
log.info(f"Successfully installed {metadata.name} to {install_dir}")
|
78
77
|
return install_dir
|
79
|
-
|
78
|
+
|
80
79
|
def get_install_dir(self, metadata: ToolMetadata) -> Path:
|
81
80
|
"""
|
82
81
|
Get installation directory for tool.
|
83
|
-
|
82
|
+
|
84
83
|
Args:
|
85
84
|
metadata: Tool metadata.
|
86
|
-
|
85
|
+
|
87
86
|
Returns:
|
88
87
|
Installation directory path.
|
89
88
|
"""
|
90
89
|
if metadata.install_path:
|
91
90
|
return metadata.install_path
|
92
|
-
|
91
|
+
|
93
92
|
# Default to ~/.wrknv/tools/<name>/<version>
|
94
93
|
base = Path.home() / ".wrknv" / "tools"
|
95
94
|
return base / metadata.name / metadata.version
|
96
|
-
|
95
|
+
|
97
96
|
def extract_zip(self, archive: Path, dest: Path) -> None:
|
98
97
|
"""
|
99
98
|
Extract ZIP archive.
|
100
|
-
|
99
|
+
|
101
100
|
Args:
|
102
101
|
archive: Path to ZIP file.
|
103
102
|
dest: Destination directory.
|
104
103
|
"""
|
105
104
|
log.debug(f"Extracting ZIP {archive} to {dest}")
|
106
|
-
|
105
|
+
|
107
106
|
dest.mkdir(parents=True, exist_ok=True)
|
108
|
-
|
107
|
+
|
109
108
|
with zipfile.ZipFile(archive, "r") as zf:
|
110
109
|
# Check for unsafe paths
|
111
110
|
for member in zf.namelist():
|
112
111
|
if member.startswith("/") or ".." in member:
|
113
112
|
raise InstallError(f"Unsafe path in archive: {member}")
|
114
|
-
|
113
|
+
|
115
114
|
zf.extractall(dest)
|
116
|
-
|
115
|
+
|
117
116
|
def extract_tar(self, archive: Path, dest: Path) -> None:
|
118
117
|
"""
|
119
118
|
Extract tar archive (with optional compression).
|
120
|
-
|
119
|
+
|
121
120
|
Args:
|
122
121
|
archive: Path to tar file.
|
123
122
|
dest: Destination directory.
|
124
123
|
"""
|
125
124
|
log.debug(f"Extracting tar {archive} to {dest}")
|
126
|
-
|
125
|
+
|
127
126
|
dest.mkdir(parents=True, exist_ok=True)
|
128
|
-
|
127
|
+
|
129
128
|
# Determine mode based on extension
|
130
129
|
mode = "r"
|
131
130
|
if archive.suffix in [".gz", ".tgz"]:
|
@@ -134,22 +133,22 @@ class ToolInstaller:
|
|
134
133
|
mode = "r:bz2"
|
135
134
|
elif archive.suffix == ".xz":
|
136
135
|
mode = "r:xz"
|
137
|
-
|
136
|
+
|
138
137
|
with tarfile.open(archive, mode) as tf:
|
139
138
|
# Check for unsafe paths
|
140
139
|
for member in tf.getmembers():
|
141
140
|
if member.name.startswith("/") or ".." in member.name:
|
142
141
|
raise InstallError(f"Unsafe path in archive: {member.name}")
|
143
|
-
|
142
|
+
|
144
143
|
tf.extractall(dest)
|
145
|
-
|
144
|
+
|
146
145
|
def is_binary(self, file_path: Path) -> bool:
|
147
146
|
"""
|
148
147
|
Check if file is a binary executable.
|
149
|
-
|
148
|
+
|
150
149
|
Args:
|
151
150
|
file_path: Path to check.
|
152
|
-
|
151
|
+
|
153
152
|
Returns:
|
154
153
|
True if file appears to be binary.
|
155
154
|
"""
|
@@ -170,85 +169,80 @@ class ToolInstaller:
|
|
170
169
|
return True
|
171
170
|
except Exception:
|
172
171
|
pass
|
173
|
-
|
172
|
+
|
174
173
|
return False
|
175
|
-
|
176
|
-
def install_binary(
|
177
|
-
self,
|
178
|
-
binary: Path,
|
179
|
-
dest: Path,
|
180
|
-
metadata: ToolMetadata
|
181
|
-
) -> None:
|
174
|
+
|
175
|
+
def install_binary(self, binary: Path, dest: Path, metadata: ToolMetadata) -> None:
|
182
176
|
"""
|
183
177
|
Install single binary file.
|
184
|
-
|
178
|
+
|
185
179
|
Args:
|
186
180
|
binary: Path to binary file.
|
187
181
|
dest: Destination directory.
|
188
182
|
metadata: Tool metadata.
|
189
183
|
"""
|
190
184
|
log.debug(f"Installing binary {binary} to {dest}")
|
191
|
-
|
185
|
+
|
192
186
|
dest.mkdir(parents=True, exist_ok=True)
|
193
187
|
bin_dir = dest / "bin"
|
194
188
|
bin_dir.mkdir(exist_ok=True)
|
195
|
-
|
189
|
+
|
196
190
|
# Determine target name
|
197
191
|
target_name = metadata.executable_name or binary.name
|
198
192
|
target = bin_dir / target_name
|
199
|
-
|
193
|
+
|
200
194
|
# Copy binary
|
201
195
|
shutil.copy2(binary, target)
|
202
|
-
|
196
|
+
|
203
197
|
# Make executable
|
204
198
|
target.chmod(0o755)
|
205
|
-
|
199
|
+
|
206
200
|
def set_permissions(self, install_dir: Path, metadata: ToolMetadata) -> None:
|
207
201
|
"""
|
208
202
|
Set appropriate permissions on installed files.
|
209
|
-
|
203
|
+
|
210
204
|
Args:
|
211
205
|
install_dir: Installation directory.
|
212
206
|
metadata: Tool metadata.
|
213
207
|
"""
|
214
208
|
import platform
|
215
|
-
|
209
|
+
|
216
210
|
if platform.system() == "Windows":
|
217
211
|
return # Windows handles permissions differently
|
218
|
-
|
212
|
+
|
219
213
|
# Find executables and make them executable
|
220
214
|
bin_dir = install_dir / "bin"
|
221
215
|
if bin_dir.exists():
|
222
216
|
for file in bin_dir.iterdir():
|
223
217
|
if file.is_file():
|
224
218
|
file.chmod(0o755)
|
225
|
-
|
219
|
+
|
226
220
|
# Check for executable name in root
|
227
221
|
if metadata.executable_name:
|
228
222
|
exe_path = install_dir / metadata.executable_name
|
229
223
|
if exe_path.exists():
|
230
224
|
exe_path.chmod(0o755)
|
231
|
-
|
225
|
+
|
232
226
|
def create_symlinks(self, install_dir: Path, metadata: ToolMetadata) -> None:
|
233
227
|
"""
|
234
228
|
Create symlinks for easier access.
|
235
|
-
|
229
|
+
|
236
230
|
Args:
|
237
231
|
install_dir: Installation directory.
|
238
232
|
metadata: Tool metadata.
|
239
233
|
"""
|
240
234
|
import platform
|
241
|
-
|
235
|
+
|
242
236
|
if platform.system() == "Windows":
|
243
237
|
return # Windows doesn't support symlinks easily
|
244
|
-
|
238
|
+
|
245
239
|
# Create version-less symlink
|
246
240
|
if metadata.name and metadata.version:
|
247
241
|
parent = install_dir.parent
|
248
242
|
latest_link = parent / "latest"
|
249
|
-
|
243
|
+
|
250
244
|
if latest_link.exists() or latest_link.is_symlink():
|
251
245
|
latest_link.unlink()
|
252
|
-
|
246
|
+
|
253
247
|
latest_link.symlink_to(install_dir)
|
254
|
-
log.debug(f"Created symlink {latest_link} -> {install_dir}")
|
248
|
+
log.debug(f"Created symlink {latest_link} -> {install_dir}")
|