ruyi 0.39.0__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.
- ruyi/__init__.py +21 -0
- ruyi/__main__.py +98 -0
- ruyi/cli/__init__.py +5 -0
- ruyi/cli/builtin_commands.py +14 -0
- ruyi/cli/cmd.py +224 -0
- ruyi/cli/completer.py +50 -0
- ruyi/cli/completion.py +26 -0
- ruyi/cli/config_cli.py +153 -0
- ruyi/cli/main.py +111 -0
- ruyi/cli/self_cli.py +295 -0
- ruyi/cli/user_input.py +127 -0
- ruyi/cli/version_cli.py +45 -0
- ruyi/config/__init__.py +401 -0
- ruyi/config/editor.py +92 -0
- ruyi/config/errors.py +76 -0
- ruyi/config/news.py +39 -0
- ruyi/config/schema.py +197 -0
- ruyi/device/__init__.py +0 -0
- ruyi/device/provision.py +591 -0
- ruyi/device/provision_cli.py +40 -0
- ruyi/log/__init__.py +272 -0
- ruyi/mux/.gitignore +1 -0
- ruyi/mux/__init__.py +0 -0
- ruyi/mux/runtime.py +213 -0
- ruyi/mux/venv/__init__.py +12 -0
- ruyi/mux/venv/emulator_cfg.py +41 -0
- ruyi/mux/venv/maker.py +782 -0
- ruyi/mux/venv/venv_cli.py +92 -0
- ruyi/mux/venv_cfg.py +214 -0
- ruyi/pluginhost/__init__.py +0 -0
- ruyi/pluginhost/api.py +206 -0
- ruyi/pluginhost/ctx.py +222 -0
- ruyi/pluginhost/paths.py +135 -0
- ruyi/pluginhost/plugin_cli.py +37 -0
- ruyi/pluginhost/unsandboxed.py +246 -0
- ruyi/py.typed +0 -0
- ruyi/resource_bundle/__init__.py +20 -0
- ruyi/resource_bundle/__main__.py +55 -0
- ruyi/resource_bundle/data.py +26 -0
- ruyi/ruyipkg/__init__.py +0 -0
- ruyi/ruyipkg/admin_checksum.py +88 -0
- ruyi/ruyipkg/admin_cli.py +83 -0
- ruyi/ruyipkg/atom.py +184 -0
- ruyi/ruyipkg/augmented_pkg.py +212 -0
- ruyi/ruyipkg/canonical_dump.py +320 -0
- ruyi/ruyipkg/checksum.py +39 -0
- ruyi/ruyipkg/cli_completion.py +42 -0
- ruyi/ruyipkg/distfile.py +208 -0
- ruyi/ruyipkg/entity.py +387 -0
- ruyi/ruyipkg/entity_cli.py +123 -0
- ruyi/ruyipkg/entity_provider.py +273 -0
- ruyi/ruyipkg/fetch.py +271 -0
- ruyi/ruyipkg/host.py +55 -0
- ruyi/ruyipkg/install.py +554 -0
- ruyi/ruyipkg/install_cli.py +150 -0
- ruyi/ruyipkg/list.py +126 -0
- ruyi/ruyipkg/list_cli.py +79 -0
- ruyi/ruyipkg/list_filter.py +173 -0
- ruyi/ruyipkg/msg.py +99 -0
- ruyi/ruyipkg/news.py +123 -0
- ruyi/ruyipkg/news_cli.py +78 -0
- ruyi/ruyipkg/news_store.py +183 -0
- ruyi/ruyipkg/pkg_manifest.py +657 -0
- ruyi/ruyipkg/profile.py +208 -0
- ruyi/ruyipkg/profile_cli.py +33 -0
- ruyi/ruyipkg/protocols.py +55 -0
- ruyi/ruyipkg/repo.py +763 -0
- ruyi/ruyipkg/state.py +345 -0
- ruyi/ruyipkg/unpack.py +369 -0
- ruyi/ruyipkg/unpack_method.py +91 -0
- ruyi/ruyipkg/update_cli.py +54 -0
- ruyi/telemetry/__init__.py +0 -0
- ruyi/telemetry/aggregate.py +72 -0
- ruyi/telemetry/event.py +41 -0
- ruyi/telemetry/node_info.py +192 -0
- ruyi/telemetry/provider.py +411 -0
- ruyi/telemetry/scope.py +43 -0
- ruyi/telemetry/store.py +238 -0
- ruyi/telemetry/telemetry_cli.py +127 -0
- ruyi/utils/__init__.py +0 -0
- ruyi/utils/ar.py +74 -0
- ruyi/utils/ci.py +63 -0
- ruyi/utils/frontmatter.py +38 -0
- ruyi/utils/git.py +169 -0
- ruyi/utils/global_mode.py +204 -0
- ruyi/utils/l10n.py +83 -0
- ruyi/utils/markdown.py +73 -0
- ruyi/utils/nuitka.py +33 -0
- ruyi/utils/porcelain.py +51 -0
- ruyi/utils/prereqs.py +77 -0
- ruyi/utils/ssl_patch.py +170 -0
- ruyi/utils/templating.py +34 -0
- ruyi/utils/toml.py +115 -0
- ruyi/utils/url.py +7 -0
- ruyi/utils/xdg_basedir.py +80 -0
- ruyi/version.py +67 -0
- ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
- ruyi-0.39.0.dist-info/METADATA +403 -0
- ruyi-0.39.0.dist-info/RECORD +101 -0
- ruyi-0.39.0.dist-info/WHEEL +4 -0
- ruyi-0.39.0.dist-info/entry_points.txt +3 -0
ruyi/ruyipkg/state.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
from typing import Any, Iterable, Iterator, TypedDict, TYPE_CHECKING
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .pkg_manifest import BoundPackageManifest
|
|
9
|
+
from .protocols import ProvidesPackageManifests
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
# for avoiding heavy import
|
|
13
|
+
from .repo import MetadataRepo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PackageInstallationRecord(TypedDict):
|
|
17
|
+
"""Record of a package installation."""
|
|
18
|
+
|
|
19
|
+
repo_id: str
|
|
20
|
+
category: str
|
|
21
|
+
name: str
|
|
22
|
+
version: str
|
|
23
|
+
host: str # For binary packages, empty for blobs
|
|
24
|
+
install_path: str
|
|
25
|
+
install_time: str # ISO format datetime
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class PackageInstallationInfo:
|
|
30
|
+
"""Information about an installed package."""
|
|
31
|
+
|
|
32
|
+
repo_id: str
|
|
33
|
+
category: str
|
|
34
|
+
name: str
|
|
35
|
+
version: str
|
|
36
|
+
host: str # For binary packages, empty for blobs
|
|
37
|
+
install_path: str
|
|
38
|
+
install_time: datetime.datetime
|
|
39
|
+
|
|
40
|
+
def to_record(self) -> PackageInstallationRecord:
|
|
41
|
+
"""Convert to a record for JSON serialization."""
|
|
42
|
+
return PackageInstallationRecord(
|
|
43
|
+
repo_id=self.repo_id,
|
|
44
|
+
category=self.category,
|
|
45
|
+
name=self.name,
|
|
46
|
+
version=self.version,
|
|
47
|
+
host=self.host,
|
|
48
|
+
install_path=self.install_path,
|
|
49
|
+
install_time=self.install_time.isoformat(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_record(
|
|
54
|
+
cls,
|
|
55
|
+
record: PackageInstallationRecord,
|
|
56
|
+
) -> "PackageInstallationInfo":
|
|
57
|
+
"""Create from a record."""
|
|
58
|
+
return cls(
|
|
59
|
+
repo_id=record["repo_id"],
|
|
60
|
+
category=record["category"],
|
|
61
|
+
name=record["name"],
|
|
62
|
+
version=record["version"],
|
|
63
|
+
host=record["host"],
|
|
64
|
+
install_path=record["install_path"],
|
|
65
|
+
install_time=datetime.datetime.fromisoformat(record["install_time"]),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RuyipkgGlobalStateStore:
|
|
70
|
+
def __init__(self, root: os.PathLike[Any]) -> None:
|
|
71
|
+
self.root = pathlib.Path(root)
|
|
72
|
+
self._installs_file = self.root / "installs.json"
|
|
73
|
+
self._installs_cache: dict[str, PackageInstallationInfo] | None = None
|
|
74
|
+
|
|
75
|
+
def ensure_state_dir(self) -> None:
|
|
76
|
+
"""Ensure the state directory exists."""
|
|
77
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
def _load_installs(self) -> dict[str, PackageInstallationInfo]:
|
|
80
|
+
"""Load installation records from disk."""
|
|
81
|
+
if self._installs_cache is not None:
|
|
82
|
+
return self._installs_cache
|
|
83
|
+
|
|
84
|
+
self.ensure_state_dir()
|
|
85
|
+
|
|
86
|
+
if not self._installs_file.exists():
|
|
87
|
+
self._installs_cache = {}
|
|
88
|
+
return self._installs_cache
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
with open(self._installs_file, "r", encoding="utf-8") as f:
|
|
92
|
+
data = json.load(f)
|
|
93
|
+
|
|
94
|
+
installs = {}
|
|
95
|
+
for key, record in data.items():
|
|
96
|
+
installs[key] = PackageInstallationInfo.from_record(record)
|
|
97
|
+
|
|
98
|
+
self._installs_cache = installs
|
|
99
|
+
return self._installs_cache
|
|
100
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
101
|
+
# If file is corrupted, start fresh
|
|
102
|
+
self._installs_cache = {}
|
|
103
|
+
return self._installs_cache
|
|
104
|
+
|
|
105
|
+
def _save_installs(self) -> None:
|
|
106
|
+
"""Save installation records to disk."""
|
|
107
|
+
if self._installs_cache is None:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self.ensure_state_dir()
|
|
111
|
+
|
|
112
|
+
data = {}
|
|
113
|
+
for key, info in self._installs_cache.items():
|
|
114
|
+
data[key] = info.to_record()
|
|
115
|
+
|
|
116
|
+
# Write atomically by writing to temp file then renaming
|
|
117
|
+
temp_file = self._installs_file.with_suffix(".tmp")
|
|
118
|
+
try:
|
|
119
|
+
with open(temp_file, "w", encoding="utf-8") as f:
|
|
120
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
121
|
+
temp_file.replace(self._installs_file)
|
|
122
|
+
except Exception:
|
|
123
|
+
# Clean up temp file if something went wrong
|
|
124
|
+
if temp_file.exists():
|
|
125
|
+
temp_file.unlink()
|
|
126
|
+
raise
|
|
127
|
+
|
|
128
|
+
def _get_installation_key(
|
|
129
|
+
self,
|
|
130
|
+
repo_id: str,
|
|
131
|
+
category: str,
|
|
132
|
+
name: str,
|
|
133
|
+
version: str,
|
|
134
|
+
host: str = "",
|
|
135
|
+
) -> str:
|
|
136
|
+
"""Get the key used to store installation info."""
|
|
137
|
+
if host:
|
|
138
|
+
# Use a format that includes host for binary packages
|
|
139
|
+
return f"{repo_id}:{category}/{name} {version} host={host}"
|
|
140
|
+
return f"{repo_id}:{category}/{name} {version}"
|
|
141
|
+
|
|
142
|
+
def record_installation(
|
|
143
|
+
self,
|
|
144
|
+
repo_id: str,
|
|
145
|
+
category: str,
|
|
146
|
+
name: str,
|
|
147
|
+
version: str,
|
|
148
|
+
host: str,
|
|
149
|
+
install_path: str,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Record a successful package installation."""
|
|
152
|
+
installs = self._load_installs()
|
|
153
|
+
|
|
154
|
+
key = self._get_installation_key(repo_id, category, name, version, host)
|
|
155
|
+
info = PackageInstallationInfo(
|
|
156
|
+
repo_id=repo_id,
|
|
157
|
+
category=category,
|
|
158
|
+
name=name,
|
|
159
|
+
version=version,
|
|
160
|
+
host=host,
|
|
161
|
+
install_path=install_path,
|
|
162
|
+
install_time=datetime.datetime.now(),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
installs[key] = info
|
|
166
|
+
self._save_installs()
|
|
167
|
+
|
|
168
|
+
def remove_installation(
|
|
169
|
+
self,
|
|
170
|
+
repo_id: str,
|
|
171
|
+
category: str,
|
|
172
|
+
name: str,
|
|
173
|
+
version: str,
|
|
174
|
+
host: str = "",
|
|
175
|
+
) -> bool:
|
|
176
|
+
"""Remove an installation record."""
|
|
177
|
+
installs = self._load_installs()
|
|
178
|
+
key = self._get_installation_key(repo_id, category, name, version, host)
|
|
179
|
+
|
|
180
|
+
if key in installs:
|
|
181
|
+
del installs[key]
|
|
182
|
+
self._save_installs()
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def get_installation(
|
|
187
|
+
self,
|
|
188
|
+
repo_id: str,
|
|
189
|
+
category: str,
|
|
190
|
+
name: str,
|
|
191
|
+
version: str,
|
|
192
|
+
host: str = "",
|
|
193
|
+
) -> PackageInstallationInfo | None:
|
|
194
|
+
"""Get information about a specific installation."""
|
|
195
|
+
installs = self._load_installs()
|
|
196
|
+
key = self._get_installation_key(repo_id, category, name, version, host)
|
|
197
|
+
return installs.get(key)
|
|
198
|
+
|
|
199
|
+
def is_package_installed(
|
|
200
|
+
self,
|
|
201
|
+
repo_id: str,
|
|
202
|
+
category: str,
|
|
203
|
+
name: str,
|
|
204
|
+
version: str,
|
|
205
|
+
host: str = "",
|
|
206
|
+
) -> bool:
|
|
207
|
+
"""Check if a package is installed."""
|
|
208
|
+
return self.get_installation(repo_id, category, name, version, host) is not None
|
|
209
|
+
|
|
210
|
+
def list_installed_packages(self) -> list[PackageInstallationInfo]:
|
|
211
|
+
"""List all installed packages."""
|
|
212
|
+
installs = self._load_installs()
|
|
213
|
+
return list(installs.values())
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class BoundInstallationStateStore(ProvidesPackageManifests):
|
|
217
|
+
def __init__(self, rgs: RuyipkgGlobalStateStore, mr: "MetadataRepo") -> None:
|
|
218
|
+
self._rgs = rgs
|
|
219
|
+
self._mr = mr
|
|
220
|
+
|
|
221
|
+
def _get_installed_manifest(
|
|
222
|
+
self,
|
|
223
|
+
info: PackageInstallationInfo,
|
|
224
|
+
) -> BoundPackageManifest | None:
|
|
225
|
+
"""Get the bound manifest for an installed package, or None if not found in repo."""
|
|
226
|
+
|
|
227
|
+
return self._mr.get_pkg(info.name, info.category, info.version)
|
|
228
|
+
|
|
229
|
+
def iter_pkg_manifests(self) -> Iterable[BoundPackageManifest]:
|
|
230
|
+
"""Iterate over all installed package manifests."""
|
|
231
|
+
|
|
232
|
+
installed_pkgs = self._rgs.list_installed_packages()
|
|
233
|
+
for info in installed_pkgs:
|
|
234
|
+
if m := self._get_installed_manifest(info):
|
|
235
|
+
yield m
|
|
236
|
+
|
|
237
|
+
def iter_pkgs(
|
|
238
|
+
self,
|
|
239
|
+
) -> Iterable[tuple[str, str, dict[str, BoundPackageManifest]]]:
|
|
240
|
+
"""Iterate over installed packages grouped by category and name."""
|
|
241
|
+
|
|
242
|
+
installed_pkgs = self._rgs.list_installed_packages()
|
|
243
|
+
|
|
244
|
+
# Group by category and name
|
|
245
|
+
result: dict[str, dict[str, dict[str, BoundPackageManifest]]] = {}
|
|
246
|
+
for info in installed_pkgs:
|
|
247
|
+
if m := self._get_installed_manifest(info):
|
|
248
|
+
if info.category not in result:
|
|
249
|
+
result[info.category] = {}
|
|
250
|
+
if info.name not in result[info.category]:
|
|
251
|
+
result[info.category][info.name] = {}
|
|
252
|
+
result[info.category][info.name][info.version] = m
|
|
253
|
+
|
|
254
|
+
for category, cat_pkgs in result.items():
|
|
255
|
+
for pkg_name, pkg_vers in cat_pkgs.items():
|
|
256
|
+
yield (category, pkg_name, pkg_vers)
|
|
257
|
+
|
|
258
|
+
def iter_pkg_vers(
|
|
259
|
+
self,
|
|
260
|
+
name: str,
|
|
261
|
+
category: str | None = None,
|
|
262
|
+
) -> Iterable[BoundPackageManifest]:
|
|
263
|
+
"""Iterate over installed versions of a specific package."""
|
|
264
|
+
|
|
265
|
+
installed_pkgs = self._rgs.list_installed_packages()
|
|
266
|
+
for info in installed_pkgs:
|
|
267
|
+
if info.name == name and (category is None or info.category == category):
|
|
268
|
+
if m := self._get_installed_manifest(info):
|
|
269
|
+
yield m
|
|
270
|
+
|
|
271
|
+
def get_pkg(
|
|
272
|
+
self,
|
|
273
|
+
name: str,
|
|
274
|
+
category: str,
|
|
275
|
+
ver: str,
|
|
276
|
+
) -> BoundPackageManifest | None:
|
|
277
|
+
"""Returns the package manifest by exact match, or None if not found."""
|
|
278
|
+
installed_pkgs = self._rgs.list_installed_packages()
|
|
279
|
+
for info in installed_pkgs:
|
|
280
|
+
if info.name == name and info.category == category and info.version == ver:
|
|
281
|
+
if m := self._get_installed_manifest(info):
|
|
282
|
+
return m
|
|
283
|
+
# Package is installed but not found in current repo
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def get_pkg_latest_ver(
|
|
289
|
+
self,
|
|
290
|
+
name: str,
|
|
291
|
+
category: str | None = None,
|
|
292
|
+
include_prerelease_vers: bool = False,
|
|
293
|
+
) -> BoundPackageManifest:
|
|
294
|
+
"""Get the latest installed version of a package."""
|
|
295
|
+
|
|
296
|
+
from .pkg_manifest import is_prerelease
|
|
297
|
+
|
|
298
|
+
installed_vers = list(self.iter_pkg_vers(name, category))
|
|
299
|
+
if not installed_vers:
|
|
300
|
+
raise KeyError(f"No installed versions found for package '{name}'")
|
|
301
|
+
|
|
302
|
+
if not include_prerelease_vers:
|
|
303
|
+
installed_vers = [
|
|
304
|
+
pm for pm in installed_vers if not is_prerelease(pm.semver)
|
|
305
|
+
]
|
|
306
|
+
if not installed_vers:
|
|
307
|
+
raise KeyError(
|
|
308
|
+
f"No non-prerelease installed versions found for package '{name}'"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Find the latest version
|
|
312
|
+
latest = max(installed_vers, key=lambda pm: pm.semver)
|
|
313
|
+
return latest
|
|
314
|
+
|
|
315
|
+
# To be removed later along with slug support
|
|
316
|
+
def get_pkg_by_slug(self, slug: str) -> BoundPackageManifest | None:
|
|
317
|
+
"""Get an installed package by its slug."""
|
|
318
|
+
|
|
319
|
+
installed_pkgs = self._rgs.list_installed_packages()
|
|
320
|
+
for info in installed_pkgs:
|
|
321
|
+
if m := self._get_installed_manifest(info):
|
|
322
|
+
if m.slug == slug:
|
|
323
|
+
return m
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
# Useful helpers
|
|
327
|
+
|
|
328
|
+
def iter_upgradable_pkgs(
|
|
329
|
+
self,
|
|
330
|
+
include_prereleases: bool = False,
|
|
331
|
+
) -> Iterator[tuple[BoundPackageManifest, str]]:
|
|
332
|
+
for installed_pm in self.iter_pkg_manifests():
|
|
333
|
+
latest_pm: BoundPackageManifest
|
|
334
|
+
try:
|
|
335
|
+
latest_pm = self._mr.get_pkg_latest_ver(
|
|
336
|
+
installed_pm.name,
|
|
337
|
+
installed_pm.category,
|
|
338
|
+
include_prereleases,
|
|
339
|
+
)
|
|
340
|
+
except KeyError:
|
|
341
|
+
# package not found in the repo, skip it
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
if latest_pm.semver > installed_pm.semver:
|
|
345
|
+
yield (installed_pm, str(latest_pm.semver))
|
ruyi/ruyipkg/unpack.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import mmap
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import BinaryIO, NoReturn, Protocol
|
|
6
|
+
|
|
7
|
+
from ..log import RuyiLogger
|
|
8
|
+
from ..utils import ar, prereqs
|
|
9
|
+
from .unpack_method import (
|
|
10
|
+
UnpackMethod,
|
|
11
|
+
UnrecognizedPackFormatError,
|
|
12
|
+
determine_unpack_method,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SupportsRead(Protocol):
|
|
17
|
+
def read(self, n: int = -1, /) -> bytes: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def do_unpack(
|
|
21
|
+
logger: RuyiLogger,
|
|
22
|
+
filename: str,
|
|
23
|
+
dest: str | None,
|
|
24
|
+
strip_components: int,
|
|
25
|
+
unpack_method: UnpackMethod,
|
|
26
|
+
stream: BinaryIO | SupportsRead | None = None,
|
|
27
|
+
prefixes_to_unpack: list[str] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
match unpack_method:
|
|
30
|
+
case UnpackMethod.AUTO:
|
|
31
|
+
raise ValueError("the auto unpack method must be resolved prior to use")
|
|
32
|
+
case UnpackMethod.RAW:
|
|
33
|
+
return _do_copy_raw(filename, dest)
|
|
34
|
+
case (
|
|
35
|
+
UnpackMethod.TAR_AUTO
|
|
36
|
+
| UnpackMethod.TAR
|
|
37
|
+
| UnpackMethod.TAR_BZ2
|
|
38
|
+
| UnpackMethod.TAR_GZ
|
|
39
|
+
| UnpackMethod.TAR_LZ4
|
|
40
|
+
| UnpackMethod.TAR_XZ
|
|
41
|
+
| UnpackMethod.TAR_ZST
|
|
42
|
+
):
|
|
43
|
+
return _do_unpack_tar(
|
|
44
|
+
logger,
|
|
45
|
+
filename,
|
|
46
|
+
dest,
|
|
47
|
+
strip_components,
|
|
48
|
+
unpack_method,
|
|
49
|
+
stream,
|
|
50
|
+
prefixes_to_unpack,
|
|
51
|
+
)
|
|
52
|
+
case UnpackMethod.ZIP:
|
|
53
|
+
# TODO: handle strip_components somehow; the unzip(1) command currently
|
|
54
|
+
# does not have such support.
|
|
55
|
+
return _do_unpack_zip(logger, filename, dest)
|
|
56
|
+
case UnpackMethod.DEB:
|
|
57
|
+
return _do_unpack_deb(logger, filename, dest)
|
|
58
|
+
case UnpackMethod.GZ:
|
|
59
|
+
# bare gzip file
|
|
60
|
+
return _do_unpack_bare_gz(logger, filename, dest)
|
|
61
|
+
case UnpackMethod.BZ2:
|
|
62
|
+
# bare bzip2 file
|
|
63
|
+
return _do_unpack_bare_bzip2(logger, filename, dest)
|
|
64
|
+
case UnpackMethod.LZ4:
|
|
65
|
+
# bare lz4 file
|
|
66
|
+
return _do_unpack_bare_lz4(logger, filename, dest)
|
|
67
|
+
case UnpackMethod.XZ:
|
|
68
|
+
# bare xz file
|
|
69
|
+
return _do_unpack_bare_xz(logger, filename, dest)
|
|
70
|
+
case UnpackMethod.ZST:
|
|
71
|
+
# bare zstd file
|
|
72
|
+
return _do_unpack_bare_zstd(logger, filename, dest)
|
|
73
|
+
case _:
|
|
74
|
+
raise UnrecognizedPackFormatError(filename)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def do_unpack_or_symlink(
|
|
78
|
+
logger: RuyiLogger,
|
|
79
|
+
filename: str,
|
|
80
|
+
dest: str | None,
|
|
81
|
+
strip_components: int,
|
|
82
|
+
unpack_method: UnpackMethod,
|
|
83
|
+
stream: BinaryIO | SupportsRead | None = None,
|
|
84
|
+
prefixes_to_unpack: list[str] | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
try:
|
|
87
|
+
return do_unpack(
|
|
88
|
+
logger,
|
|
89
|
+
filename,
|
|
90
|
+
dest,
|
|
91
|
+
strip_components,
|
|
92
|
+
unpack_method,
|
|
93
|
+
stream,
|
|
94
|
+
prefixes_to_unpack,
|
|
95
|
+
)
|
|
96
|
+
except UnrecognizedPackFormatError:
|
|
97
|
+
# just symlink into destination
|
|
98
|
+
return do_symlink(filename, dest)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _do_copy_raw(
|
|
102
|
+
src_path: str,
|
|
103
|
+
destdir: str | None,
|
|
104
|
+
) -> None:
|
|
105
|
+
src_filename = os.path.basename(src_path)
|
|
106
|
+
if destdir is None:
|
|
107
|
+
# symlink into CWD
|
|
108
|
+
dest = src_filename
|
|
109
|
+
else:
|
|
110
|
+
dest = os.path.join(destdir, src_filename)
|
|
111
|
+
|
|
112
|
+
shutil.copy(src_path, dest)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def do_symlink(
|
|
116
|
+
src_path: str,
|
|
117
|
+
destdir: str | None,
|
|
118
|
+
) -> None:
|
|
119
|
+
src_filename = os.path.basename(src_path)
|
|
120
|
+
if destdir is None:
|
|
121
|
+
# symlink into CWD
|
|
122
|
+
dest = src_filename
|
|
123
|
+
else:
|
|
124
|
+
dest = os.path.join(destdir, src_filename)
|
|
125
|
+
|
|
126
|
+
# avoid the hassle and pitfalls around relative paths and symlinks, and
|
|
127
|
+
# just point to the target using absolute path
|
|
128
|
+
symlink_target = os.path.abspath(src_path)
|
|
129
|
+
os.symlink(symlink_target, dest)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _do_unpack_tar(
|
|
133
|
+
logger: RuyiLogger,
|
|
134
|
+
filename: str,
|
|
135
|
+
dest: str | None,
|
|
136
|
+
strip_components: int,
|
|
137
|
+
unpack_method: UnpackMethod,
|
|
138
|
+
stream: SupportsRead | None,
|
|
139
|
+
prefixes_to_unpack: list[str] | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
argv = ["tar", "-x"]
|
|
142
|
+
|
|
143
|
+
match unpack_method:
|
|
144
|
+
case UnpackMethod.TAR | UnpackMethod.TAR_AUTO:
|
|
145
|
+
pass
|
|
146
|
+
case UnpackMethod.TAR_GZ:
|
|
147
|
+
argv.append("-z")
|
|
148
|
+
case UnpackMethod.TAR_BZ2:
|
|
149
|
+
argv.append("-j")
|
|
150
|
+
case UnpackMethod.TAR_LZ4:
|
|
151
|
+
argv.append("--use-compress-program=lz4")
|
|
152
|
+
case UnpackMethod.TAR_XZ:
|
|
153
|
+
argv.append("-J")
|
|
154
|
+
case UnpackMethod.TAR_ZST:
|
|
155
|
+
argv.append("--zstd")
|
|
156
|
+
case _:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"do_unpack_tar cannot handle non-tar unpack method {unpack_method}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
stdin: int | None = None
|
|
162
|
+
if stream is not None:
|
|
163
|
+
filename = "-"
|
|
164
|
+
stdin = subprocess.PIPE
|
|
165
|
+
|
|
166
|
+
argv.extend(("-f", filename, f"--strip-components={strip_components}"))
|
|
167
|
+
if dest is not None:
|
|
168
|
+
argv.extend(("-C", dest))
|
|
169
|
+
if prefixes_to_unpack:
|
|
170
|
+
if any(p.startswith("-") for p in prefixes_to_unpack):
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"prefixes_to_unpack must not contain any item starting with '-'"
|
|
173
|
+
)
|
|
174
|
+
argv.extend(prefixes_to_unpack)
|
|
175
|
+
logger.D(f"about to call tar: argv={argv}")
|
|
176
|
+
p = subprocess.Popen(argv, cwd=dest, stdin=stdin)
|
|
177
|
+
|
|
178
|
+
retcode: int
|
|
179
|
+
if stream is None:
|
|
180
|
+
retcode = p.wait()
|
|
181
|
+
else:
|
|
182
|
+
# this is only for pleasing the type-checker; it's statically true
|
|
183
|
+
# because the assignment always happens due to the earlier
|
|
184
|
+
# "stream is not None" branch.
|
|
185
|
+
assert p.stdin is not None
|
|
186
|
+
|
|
187
|
+
bufsize = 4 * mmap.PAGESIZE
|
|
188
|
+
while True:
|
|
189
|
+
buf = stream.read(bufsize)
|
|
190
|
+
if not buf:
|
|
191
|
+
break
|
|
192
|
+
p.stdin.write(buf)
|
|
193
|
+
p.stdin.close()
|
|
194
|
+
retcode = p.wait()
|
|
195
|
+
|
|
196
|
+
if retcode != 0:
|
|
197
|
+
raise RuntimeError(f"untar failed: command {' '.join(argv)} returned {retcode}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _do_unpack_zip(
|
|
201
|
+
logger: RuyiLogger,
|
|
202
|
+
filename: str,
|
|
203
|
+
dest: str | None,
|
|
204
|
+
) -> None:
|
|
205
|
+
argv = ["unzip", filename]
|
|
206
|
+
if dest is not None:
|
|
207
|
+
argv.extend(("-d", dest))
|
|
208
|
+
logger.D(f"about to call unzip: argv={argv}")
|
|
209
|
+
retcode = subprocess.call(argv, cwd=dest)
|
|
210
|
+
if retcode != 0:
|
|
211
|
+
raise RuntimeError(f"unzip failed: command {' '.join(argv)} returned {retcode}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _do_unpack_bare_gz(
|
|
215
|
+
logger: RuyiLogger,
|
|
216
|
+
filename: str,
|
|
217
|
+
destdir: str | None,
|
|
218
|
+
) -> None:
|
|
219
|
+
# the suffix may not be ".gz" so do this generically
|
|
220
|
+
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
221
|
+
|
|
222
|
+
argv = ["gunzip", "-c", filename]
|
|
223
|
+
if destdir is not None:
|
|
224
|
+
os.chdir(destdir)
|
|
225
|
+
|
|
226
|
+
logger.D(f"about to call gunzip: argv={argv}")
|
|
227
|
+
with open(dest_filename, "wb") as out:
|
|
228
|
+
retcode = subprocess.call(argv, stdout=out)
|
|
229
|
+
if retcode != 0:
|
|
230
|
+
raise RuntimeError(
|
|
231
|
+
f"gunzip failed: command {' '.join(argv)} returned {retcode}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _do_unpack_bare_bzip2(
|
|
236
|
+
logger: RuyiLogger,
|
|
237
|
+
filename: str,
|
|
238
|
+
destdir: str | None,
|
|
239
|
+
) -> None:
|
|
240
|
+
# the suffix may not be ".bz2" so do this generically
|
|
241
|
+
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
242
|
+
|
|
243
|
+
argv = ["bzip2", "-dc", filename]
|
|
244
|
+
if destdir is not None:
|
|
245
|
+
os.chdir(destdir)
|
|
246
|
+
|
|
247
|
+
logger.D(f"about to call bzip2: argv={argv}")
|
|
248
|
+
with open(dest_filename, "wb") as out:
|
|
249
|
+
retcode = subprocess.call(argv, stdout=out)
|
|
250
|
+
if retcode != 0:
|
|
251
|
+
raise RuntimeError(
|
|
252
|
+
f"bzip2 failed: command {' '.join(argv)} returned {retcode}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _do_unpack_bare_lz4(
|
|
257
|
+
logger: RuyiLogger,
|
|
258
|
+
filename: str,
|
|
259
|
+
destdir: str | None,
|
|
260
|
+
) -> None:
|
|
261
|
+
# the suffix may not be ".lz4" so do this generically
|
|
262
|
+
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
263
|
+
|
|
264
|
+
argv = ["lz4", "-dk", filename, f"./{dest_filename}"]
|
|
265
|
+
logger.D(f"about to call lz4: argv={argv}")
|
|
266
|
+
retcode = subprocess.call(argv, cwd=destdir)
|
|
267
|
+
if retcode != 0:
|
|
268
|
+
raise RuntimeError(f"lz4 failed: command {' '.join(argv)} returned {retcode}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _do_unpack_bare_xz(
|
|
272
|
+
logger: RuyiLogger,
|
|
273
|
+
filename: str,
|
|
274
|
+
destdir: str | None,
|
|
275
|
+
) -> None:
|
|
276
|
+
# the suffix may not be ".xz" so do this generically
|
|
277
|
+
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
278
|
+
|
|
279
|
+
argv = ["xz", "-d", "-c", filename]
|
|
280
|
+
if destdir is not None:
|
|
281
|
+
os.chdir(destdir)
|
|
282
|
+
|
|
283
|
+
logger.D(f"about to call xz: argv={argv}")
|
|
284
|
+
with open(dest_filename, "wb") as out:
|
|
285
|
+
retcode = subprocess.call(argv, stdout=out)
|
|
286
|
+
if retcode != 0:
|
|
287
|
+
raise RuntimeError(
|
|
288
|
+
f"xz failed: command {' '.join(argv)} returned {retcode}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _do_unpack_bare_zstd(
|
|
293
|
+
logger: RuyiLogger,
|
|
294
|
+
filename: str,
|
|
295
|
+
destdir: str | None,
|
|
296
|
+
) -> None:
|
|
297
|
+
# the suffix may not be ".zst" so do this generically
|
|
298
|
+
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
299
|
+
|
|
300
|
+
argv = ["zstd", "-d", filename, "-o", f"./{dest_filename}"]
|
|
301
|
+
logger.D(f"about to call zstd: argv={argv}")
|
|
302
|
+
retcode = subprocess.call(argv, cwd=destdir)
|
|
303
|
+
if retcode != 0:
|
|
304
|
+
raise RuntimeError(f"zstd failed: command {' '.join(argv)} returned {retcode}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _do_unpack_deb(
|
|
308
|
+
logger: RuyiLogger,
|
|
309
|
+
filename: str,
|
|
310
|
+
destdir: str | None,
|
|
311
|
+
) -> None:
|
|
312
|
+
with ar.ArpyArchiveWrapper(filename) as a:
|
|
313
|
+
for f in a.infolist():
|
|
314
|
+
name = f.name.decode("utf-8")
|
|
315
|
+
if name.startswith("data.tar"):
|
|
316
|
+
inner_unpack_method = determine_unpack_method(name)
|
|
317
|
+
return _do_unpack_tar(
|
|
318
|
+
logger,
|
|
319
|
+
name,
|
|
320
|
+
destdir,
|
|
321
|
+
0,
|
|
322
|
+
inner_unpack_method,
|
|
323
|
+
a.open(f),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
raise RuntimeError(f"file '{filename}' does not appear to be a deb")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _get_unpack_cmds_for_method(m: UnpackMethod) -> list[str]:
|
|
330
|
+
match m:
|
|
331
|
+
case UnpackMethod.UNKNOWN | UnpackMethod.RAW | UnpackMethod.DEB:
|
|
332
|
+
return []
|
|
333
|
+
case UnpackMethod.GZ:
|
|
334
|
+
return ["gunzip"]
|
|
335
|
+
case UnpackMethod.BZ2:
|
|
336
|
+
return ["bzip2"]
|
|
337
|
+
case UnpackMethod.LZ4:
|
|
338
|
+
return ["lz4"]
|
|
339
|
+
case UnpackMethod.XZ:
|
|
340
|
+
return ["xz"]
|
|
341
|
+
case UnpackMethod.ZST:
|
|
342
|
+
return ["zstd"]
|
|
343
|
+
case UnpackMethod.TAR | UnpackMethod.TAR_AUTO:
|
|
344
|
+
return ["tar"]
|
|
345
|
+
case UnpackMethod.TAR_GZ:
|
|
346
|
+
return ["tar", "gunzip"]
|
|
347
|
+
case UnpackMethod.TAR_BZ2:
|
|
348
|
+
return ["tar", "bzip2"]
|
|
349
|
+
case UnpackMethod.TAR_LZ4:
|
|
350
|
+
return ["tar", "lz4"]
|
|
351
|
+
case UnpackMethod.TAR_XZ:
|
|
352
|
+
return ["tar", "xz"]
|
|
353
|
+
case UnpackMethod.TAR_ZST:
|
|
354
|
+
return ["tar", "zstd"]
|
|
355
|
+
case UnpackMethod.ZIP:
|
|
356
|
+
return ["unzip"]
|
|
357
|
+
case UnpackMethod.AUTO:
|
|
358
|
+
raise ValueError(f"the unpack method {m} must be resolved prior to use")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def ensure_unpack_cmd_for_method(
|
|
362
|
+
logger: RuyiLogger,
|
|
363
|
+
m: UnpackMethod,
|
|
364
|
+
) -> None | NoReturn:
|
|
365
|
+
required_cmds = _get_unpack_cmds_for_method(m)
|
|
366
|
+
if not required_cmds:
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
return prereqs.ensure_cmds(logger, required_cmds, interactive_retry=True)
|