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/install.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import pathlib
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
from ruyi.ruyipkg.state import BoundInstallationStateStore
|
|
7
|
+
|
|
8
|
+
from ..cli.user_input import ask_for_yesno_confirmation
|
|
9
|
+
from ..config import GlobalConfig
|
|
10
|
+
from ..telemetry.scope import TelemetryScope
|
|
11
|
+
from .atom import Atom
|
|
12
|
+
from .distfile import Distfile
|
|
13
|
+
from .host import RuyiHost
|
|
14
|
+
from .pkg_manifest import BoundPackageManifest
|
|
15
|
+
from .repo import MetadataRepo
|
|
16
|
+
from .unpack import ensure_unpack_cmd_for_method
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_root_likely_populated(root: str) -> bool:
|
|
20
|
+
try:
|
|
21
|
+
return any(os.scandir(root))
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def do_extract_atoms(
|
|
27
|
+
cfg: GlobalConfig,
|
|
28
|
+
mr: MetadataRepo,
|
|
29
|
+
atom_strs: set[str],
|
|
30
|
+
*,
|
|
31
|
+
canonicalized_host: str | RuyiHost,
|
|
32
|
+
) -> int:
|
|
33
|
+
logger = cfg.logger
|
|
34
|
+
logger.D(f"about to extract for host {canonicalized_host}: {atom_strs}")
|
|
35
|
+
|
|
36
|
+
mr = cfg.repo
|
|
37
|
+
|
|
38
|
+
for a_str in atom_strs:
|
|
39
|
+
a = Atom.parse(a_str)
|
|
40
|
+
pm = a.match_in_repo(mr, cfg.include_prereleases)
|
|
41
|
+
if pm is None:
|
|
42
|
+
logger.F(f"atom {a_str} matches no package in the repository")
|
|
43
|
+
return 1
|
|
44
|
+
pkg_name = pm.name_for_installation
|
|
45
|
+
|
|
46
|
+
sv = pm.service_level
|
|
47
|
+
if sv.has_known_issues:
|
|
48
|
+
logger.W("package has known issue(s)")
|
|
49
|
+
for s in sv.render_known_issues(pm.repo.messages, cfg.lang_code):
|
|
50
|
+
logger.I(s)
|
|
51
|
+
|
|
52
|
+
bm = pm.binary_metadata
|
|
53
|
+
sm = pm.source_metadata
|
|
54
|
+
if bm is None and sm is None:
|
|
55
|
+
logger.F(f"don't know how to extract package [green]{pkg_name}[/]")
|
|
56
|
+
return 2
|
|
57
|
+
|
|
58
|
+
if bm is not None and sm is not None:
|
|
59
|
+
logger.F(
|
|
60
|
+
f"cannot handle package [green]{pkg_name}[/]: package is both binary and source"
|
|
61
|
+
)
|
|
62
|
+
return 2
|
|
63
|
+
|
|
64
|
+
distfiles_for_host: list[str] | None = None
|
|
65
|
+
if bm is not None:
|
|
66
|
+
distfiles_for_host = bm.get_distfile_names_for_host(canonicalized_host)
|
|
67
|
+
elif sm is not None:
|
|
68
|
+
distfiles_for_host = sm.get_distfile_names_for_host(canonicalized_host)
|
|
69
|
+
|
|
70
|
+
if not distfiles_for_host:
|
|
71
|
+
logger.F(
|
|
72
|
+
f"package [green]{pkg_name}[/] declares no distfile for host {canonicalized_host}"
|
|
73
|
+
)
|
|
74
|
+
return 2
|
|
75
|
+
|
|
76
|
+
dfs = pm.distfiles
|
|
77
|
+
|
|
78
|
+
for df_name in distfiles_for_host:
|
|
79
|
+
df_decl = dfs[df_name]
|
|
80
|
+
ensure_unpack_cmd_for_method(logger, df_decl.unpack_method)
|
|
81
|
+
df = Distfile(df_decl, mr)
|
|
82
|
+
df.ensure(logger)
|
|
83
|
+
|
|
84
|
+
logger.I(f"extracting [green]{df_name}[/] for package [green]{pkg_name}[/]")
|
|
85
|
+
# unpack into CWD
|
|
86
|
+
df.unpack(None, logger)
|
|
87
|
+
|
|
88
|
+
logger.I(f"package [green]{pkg_name}[/] extracted to current working directory")
|
|
89
|
+
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def do_install_atoms(
|
|
94
|
+
config: GlobalConfig,
|
|
95
|
+
mr: MetadataRepo,
|
|
96
|
+
atom_strs: set[str],
|
|
97
|
+
*,
|
|
98
|
+
canonicalized_host: str | RuyiHost,
|
|
99
|
+
fetch_only: bool,
|
|
100
|
+
reinstall: bool,
|
|
101
|
+
) -> int:
|
|
102
|
+
logger = config.logger
|
|
103
|
+
logger.D(f"about to install for host {canonicalized_host}: {atom_strs}")
|
|
104
|
+
|
|
105
|
+
for a_str in atom_strs:
|
|
106
|
+
a = Atom.parse(a_str)
|
|
107
|
+
pm = a.match_in_repo(mr, config.include_prereleases)
|
|
108
|
+
if pm is None:
|
|
109
|
+
logger.F(f"atom {a_str} matches no package in the repository")
|
|
110
|
+
return 1
|
|
111
|
+
pkg_name = pm.name_for_installation
|
|
112
|
+
|
|
113
|
+
sv = pm.service_level
|
|
114
|
+
if sv.has_known_issues:
|
|
115
|
+
logger.W("package has known issue(s)")
|
|
116
|
+
for s in sv.render_known_issues(pm.repo.messages, config.lang_code):
|
|
117
|
+
logger.I(s)
|
|
118
|
+
|
|
119
|
+
if tm := config.telemetry:
|
|
120
|
+
tm.record(
|
|
121
|
+
TelemetryScope(mr.repo_id),
|
|
122
|
+
"repo:package-install-v1",
|
|
123
|
+
atom=a_str,
|
|
124
|
+
host=canonicalized_host,
|
|
125
|
+
pkg_category=pm.category,
|
|
126
|
+
pkg_kinds=pm.kind,
|
|
127
|
+
pkg_name=pm.name,
|
|
128
|
+
pkg_version=pm.ver,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if pm.binary_metadata is not None:
|
|
132
|
+
ret = _do_install_binary_pkg(
|
|
133
|
+
config,
|
|
134
|
+
mr,
|
|
135
|
+
pm,
|
|
136
|
+
canonicalized_host,
|
|
137
|
+
fetch_only,
|
|
138
|
+
reinstall,
|
|
139
|
+
)
|
|
140
|
+
if ret != 0:
|
|
141
|
+
return ret
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if pm.blob_metadata is not None:
|
|
145
|
+
ret = _do_install_blob_pkg(config, mr, pm, fetch_only, reinstall)
|
|
146
|
+
if ret != 0:
|
|
147
|
+
return ret
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
logger.F(f"don't know how to handle non-binary package [green]{pkg_name}[/]")
|
|
151
|
+
return 2
|
|
152
|
+
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _do_install_binary_pkg(
|
|
157
|
+
config: GlobalConfig,
|
|
158
|
+
mr: MetadataRepo,
|
|
159
|
+
pm: BoundPackageManifest,
|
|
160
|
+
canonicalized_host: str | RuyiHost,
|
|
161
|
+
fetch_only: bool,
|
|
162
|
+
reinstall: bool,
|
|
163
|
+
) -> int:
|
|
164
|
+
logger = config.logger
|
|
165
|
+
bm = pm.binary_metadata
|
|
166
|
+
assert bm is not None
|
|
167
|
+
|
|
168
|
+
pkg_name = pm.name_for_installation
|
|
169
|
+
install_root = config.global_binary_install_root(str(canonicalized_host), pkg_name)
|
|
170
|
+
|
|
171
|
+
rgs = config.ruyipkg_global_state
|
|
172
|
+
is_installed = rgs.is_package_installed(
|
|
173
|
+
pm.repo_id,
|
|
174
|
+
pm.category,
|
|
175
|
+
pm.name,
|
|
176
|
+
pm.ver,
|
|
177
|
+
str(canonicalized_host),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Fallback to directory check if not tracked in state
|
|
181
|
+
if not is_installed and is_root_likely_populated(install_root):
|
|
182
|
+
is_installed = True
|
|
183
|
+
|
|
184
|
+
if is_installed:
|
|
185
|
+
if not reinstall:
|
|
186
|
+
logger.I(f"skipping already installed package [green]{pkg_name}[/]")
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
logger.W(
|
|
190
|
+
f"package [green]{pkg_name}[/] seems already installed; purging and re-installing due to [yellow]--reinstall[/]"
|
|
191
|
+
)
|
|
192
|
+
# Remove from state tracking before purging
|
|
193
|
+
rgs.remove_installation(
|
|
194
|
+
pm.repo_id,
|
|
195
|
+
pm.category,
|
|
196
|
+
pm.name,
|
|
197
|
+
pm.ver,
|
|
198
|
+
str(canonicalized_host),
|
|
199
|
+
)
|
|
200
|
+
shutil.rmtree(install_root)
|
|
201
|
+
|
|
202
|
+
ir_parent = pathlib.Path(install_root).resolve().parent
|
|
203
|
+
ir_parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
with tempfile.TemporaryDirectory(prefix=".ruyi-tmp", dir=ir_parent) as tmp_root:
|
|
205
|
+
ret = _do_install_binary_pkg_to(
|
|
206
|
+
config,
|
|
207
|
+
mr,
|
|
208
|
+
pm,
|
|
209
|
+
canonicalized_host,
|
|
210
|
+
fetch_only,
|
|
211
|
+
tmp_root,
|
|
212
|
+
)
|
|
213
|
+
if ret != 0:
|
|
214
|
+
return ret
|
|
215
|
+
os.rename(tmp_root, install_root)
|
|
216
|
+
|
|
217
|
+
if not fetch_only:
|
|
218
|
+
rgs.record_installation(
|
|
219
|
+
repo_id=pm.repo_id,
|
|
220
|
+
category=pm.category,
|
|
221
|
+
name=pm.name,
|
|
222
|
+
version=pm.ver,
|
|
223
|
+
host=str(canonicalized_host),
|
|
224
|
+
install_path=install_root,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
logger.I(f"package [green]{pkg_name}[/] installed to [yellow]{install_root}[/]")
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _do_install_binary_pkg_to(
|
|
232
|
+
config: GlobalConfig,
|
|
233
|
+
mr: MetadataRepo,
|
|
234
|
+
pm: BoundPackageManifest,
|
|
235
|
+
canonicalized_host: str | RuyiHost,
|
|
236
|
+
fetch_only: bool,
|
|
237
|
+
install_root: str,
|
|
238
|
+
) -> int:
|
|
239
|
+
logger = config.logger
|
|
240
|
+
bm = pm.binary_metadata
|
|
241
|
+
assert bm is not None
|
|
242
|
+
|
|
243
|
+
dfs = pm.distfiles
|
|
244
|
+
|
|
245
|
+
pkg_name = pm.name_for_installation
|
|
246
|
+
distfiles_for_host = bm.get_distfile_names_for_host(str(canonicalized_host))
|
|
247
|
+
if not distfiles_for_host:
|
|
248
|
+
logger.F(
|
|
249
|
+
f"package [green]{pkg_name}[/] declares no binary for host {canonicalized_host}"
|
|
250
|
+
)
|
|
251
|
+
return 2
|
|
252
|
+
|
|
253
|
+
for df_name in distfiles_for_host:
|
|
254
|
+
df_decl = dfs[df_name]
|
|
255
|
+
ensure_unpack_cmd_for_method(logger, df_decl.unpack_method)
|
|
256
|
+
df = Distfile(df_decl, mr)
|
|
257
|
+
df.ensure(logger)
|
|
258
|
+
|
|
259
|
+
if fetch_only:
|
|
260
|
+
logger.D("skipping installation because [yellow]--fetch-only[/] is given")
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
logger.I(f"extracting [green]{df_name}[/] for package [green]{pkg_name}[/]")
|
|
264
|
+
df.unpack(install_root, logger)
|
|
265
|
+
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _do_install_blob_pkg(
|
|
270
|
+
config: GlobalConfig,
|
|
271
|
+
mr: MetadataRepo,
|
|
272
|
+
pm: BoundPackageManifest,
|
|
273
|
+
fetch_only: bool,
|
|
274
|
+
reinstall: bool,
|
|
275
|
+
) -> int:
|
|
276
|
+
logger = config.logger
|
|
277
|
+
bm = pm.blob_metadata
|
|
278
|
+
assert bm is not None
|
|
279
|
+
|
|
280
|
+
pkg_name = pm.name_for_installation
|
|
281
|
+
install_root = config.global_blob_install_root(pkg_name)
|
|
282
|
+
|
|
283
|
+
rgs = config.ruyipkg_global_state
|
|
284
|
+
is_installed = rgs.is_package_installed(
|
|
285
|
+
pm.repo_id,
|
|
286
|
+
pm.category,
|
|
287
|
+
pm.name,
|
|
288
|
+
pm.ver,
|
|
289
|
+
"", # host is "" for blob packages
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Fallback to directory check if not tracked in state
|
|
293
|
+
if not is_installed and is_root_likely_populated(install_root):
|
|
294
|
+
is_installed = True
|
|
295
|
+
|
|
296
|
+
if is_installed:
|
|
297
|
+
if not reinstall:
|
|
298
|
+
logger.I(f"skipping already installed package [green]{pkg_name}[/]")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
logger.W(
|
|
302
|
+
f"package [green]{pkg_name}[/] seems already installed; purging and re-installing due to [yellow]--reinstall[/]"
|
|
303
|
+
)
|
|
304
|
+
# Remove from state tracking before purging
|
|
305
|
+
rgs.remove_installation(
|
|
306
|
+
pm.repo_id,
|
|
307
|
+
pm.category,
|
|
308
|
+
pm.name,
|
|
309
|
+
pm.ver,
|
|
310
|
+
"",
|
|
311
|
+
)
|
|
312
|
+
shutil.rmtree(install_root)
|
|
313
|
+
|
|
314
|
+
ir_parent = pathlib.Path(install_root).resolve().parent
|
|
315
|
+
ir_parent.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
with tempfile.TemporaryDirectory(prefix=".ruyi-tmp", dir=ir_parent) as tmp_root:
|
|
317
|
+
ret = _do_install_blob_pkg_to(
|
|
318
|
+
config,
|
|
319
|
+
mr,
|
|
320
|
+
pm,
|
|
321
|
+
fetch_only,
|
|
322
|
+
tmp_root,
|
|
323
|
+
)
|
|
324
|
+
if ret != 0:
|
|
325
|
+
return ret
|
|
326
|
+
os.rename(tmp_root, install_root)
|
|
327
|
+
|
|
328
|
+
if not fetch_only:
|
|
329
|
+
rgs.record_installation(
|
|
330
|
+
repo_id=pm.repo_id,
|
|
331
|
+
category=pm.category,
|
|
332
|
+
name=pm.name,
|
|
333
|
+
version=pm.ver,
|
|
334
|
+
host="", # Empty for blob packages
|
|
335
|
+
install_path=install_root,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
logger.I(f"package [green]{pkg_name}[/] installed to [yellow]{install_root}[/]")
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _do_install_blob_pkg_to(
|
|
343
|
+
config: GlobalConfig,
|
|
344
|
+
mr: MetadataRepo,
|
|
345
|
+
pm: BoundPackageManifest,
|
|
346
|
+
fetch_only: bool,
|
|
347
|
+
install_root: str,
|
|
348
|
+
) -> int:
|
|
349
|
+
logger = config.logger
|
|
350
|
+
bm = pm.blob_metadata
|
|
351
|
+
assert bm is not None
|
|
352
|
+
|
|
353
|
+
pkg_name = pm.name_for_installation
|
|
354
|
+
dfs = pm.distfiles
|
|
355
|
+
distfile_names = bm.get_distfile_names()
|
|
356
|
+
if not distfile_names:
|
|
357
|
+
logger.F(f"package [green]{pkg_name}[/] declares no blob distfile")
|
|
358
|
+
return 2
|
|
359
|
+
|
|
360
|
+
for df_name in distfile_names:
|
|
361
|
+
df_decl = dfs[df_name]
|
|
362
|
+
ensure_unpack_cmd_for_method(logger, df_decl.unpack_method)
|
|
363
|
+
df = Distfile(df_decl, mr)
|
|
364
|
+
df.ensure(logger)
|
|
365
|
+
|
|
366
|
+
if fetch_only:
|
|
367
|
+
logger.D("skipping installation because [yellow]--fetch-only[/] is given")
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
logger.I(f"extracting [green]{df_name}[/] for package [green]{pkg_name}[/]")
|
|
371
|
+
df.unpack_or_symlink(install_root, logger)
|
|
372
|
+
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def do_uninstall_atoms(
|
|
377
|
+
config: GlobalConfig,
|
|
378
|
+
mr: MetadataRepo,
|
|
379
|
+
atom_strs: set[str],
|
|
380
|
+
*,
|
|
381
|
+
canonicalized_host: str | RuyiHost,
|
|
382
|
+
assume_yes: bool,
|
|
383
|
+
) -> int:
|
|
384
|
+
logger = config.logger
|
|
385
|
+
logger.D(f"about to uninstall for host {canonicalized_host}: {atom_strs}")
|
|
386
|
+
|
|
387
|
+
bis = BoundInstallationStateStore(config.ruyipkg_global_state, mr)
|
|
388
|
+
|
|
389
|
+
pms_to_uninstall: list[tuple[str, BoundPackageManifest]] = []
|
|
390
|
+
for a_str in atom_strs:
|
|
391
|
+
a = Atom.parse(a_str)
|
|
392
|
+
pm = a.match_in_repo(bis, config.include_prereleases)
|
|
393
|
+
if pm is None:
|
|
394
|
+
logger.F(f"atom [yellow]{a_str}[/] is non-existent or not installed")
|
|
395
|
+
return 1
|
|
396
|
+
pms_to_uninstall.append((a_str, pm))
|
|
397
|
+
|
|
398
|
+
if not pms_to_uninstall:
|
|
399
|
+
logger.I("no packages to uninstall")
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
logger.I("the following packages will be uninstalled:")
|
|
403
|
+
for _, pm in pms_to_uninstall:
|
|
404
|
+
logger.I(f" - [green]{pm.category}/{pm.name}[/] ({pm.ver})")
|
|
405
|
+
|
|
406
|
+
if not assume_yes:
|
|
407
|
+
if not ask_for_yesno_confirmation(logger, "Proceed?", default=False):
|
|
408
|
+
logger.I("uninstallation aborted")
|
|
409
|
+
return 0
|
|
410
|
+
|
|
411
|
+
for a_str, pm in pms_to_uninstall:
|
|
412
|
+
pkg_name = pm.name_for_installation
|
|
413
|
+
|
|
414
|
+
if tm := config.telemetry:
|
|
415
|
+
tm.record(
|
|
416
|
+
TelemetryScope(mr.repo_id),
|
|
417
|
+
"repo:package-uninstall-v1",
|
|
418
|
+
atom=a_str,
|
|
419
|
+
host=canonicalized_host,
|
|
420
|
+
pkg_category=pm.category,
|
|
421
|
+
pkg_kinds=pm.kind,
|
|
422
|
+
pkg_name=pm.name,
|
|
423
|
+
pkg_version=pm.ver,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if pm.binary_metadata is not None:
|
|
427
|
+
ret = _do_uninstall_binary_pkg(
|
|
428
|
+
config,
|
|
429
|
+
pm,
|
|
430
|
+
canonicalized_host,
|
|
431
|
+
)
|
|
432
|
+
if ret != 0:
|
|
433
|
+
return ret
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
if pm.blob_metadata is not None:
|
|
437
|
+
ret = _do_uninstall_blob_pkg(config, pm)
|
|
438
|
+
if ret != 0:
|
|
439
|
+
return ret
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
logger.F(f"don't know how to handle non-binary package [green]{pkg_name}[/]")
|
|
443
|
+
return 2
|
|
444
|
+
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _do_uninstall_binary_pkg(
|
|
449
|
+
config: GlobalConfig,
|
|
450
|
+
pm: BoundPackageManifest,
|
|
451
|
+
canonicalized_host: str | RuyiHost,
|
|
452
|
+
) -> int:
|
|
453
|
+
logger = config.logger
|
|
454
|
+
bm = pm.binary_metadata
|
|
455
|
+
assert bm is not None
|
|
456
|
+
|
|
457
|
+
pkg_name = pm.name_for_installation
|
|
458
|
+
install_root = config.global_binary_install_root(str(canonicalized_host), pkg_name)
|
|
459
|
+
|
|
460
|
+
rgs = config.ruyipkg_global_state
|
|
461
|
+
is_installed = rgs.is_package_installed(
|
|
462
|
+
pm.repo_id,
|
|
463
|
+
pm.category,
|
|
464
|
+
pm.name,
|
|
465
|
+
pm.ver,
|
|
466
|
+
str(canonicalized_host),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Check directory existence if the PM state says the package is not installed
|
|
470
|
+
if not is_installed:
|
|
471
|
+
if not os.path.exists(install_root):
|
|
472
|
+
logger.I(f"skipping not-installed package [green]{pkg_name}[/]")
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
# There may be potentially user-generated data in the directory,
|
|
476
|
+
# let's be safe and fail the process.
|
|
477
|
+
logger.F(
|
|
478
|
+
f"package [green]{pkg_name}[/] is not tracked as installed, but its directory [yellow]{install_root}[/] exists."
|
|
479
|
+
)
|
|
480
|
+
logger.I("Please remove it manually if you are sure it's safe to do so.")
|
|
481
|
+
logger.I(
|
|
482
|
+
"If you believe this is a bug, please file an issue at [yellow]https://github.com/ruyisdk/ruyi/issues[/]."
|
|
483
|
+
)
|
|
484
|
+
return 1
|
|
485
|
+
|
|
486
|
+
logger.I(f"uninstalling package [green]{pkg_name}[/]")
|
|
487
|
+
if is_installed:
|
|
488
|
+
rgs.remove_installation(
|
|
489
|
+
pm.repo_id,
|
|
490
|
+
pm.category,
|
|
491
|
+
pm.name,
|
|
492
|
+
pm.ver,
|
|
493
|
+
str(canonicalized_host),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if os.path.exists(install_root):
|
|
497
|
+
shutil.rmtree(install_root)
|
|
498
|
+
|
|
499
|
+
logger.I(f"package [green]{pkg_name}[/] uninstalled")
|
|
500
|
+
return 0
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _do_uninstall_blob_pkg(
|
|
504
|
+
config: GlobalConfig,
|
|
505
|
+
pm: BoundPackageManifest,
|
|
506
|
+
) -> int:
|
|
507
|
+
logger = config.logger
|
|
508
|
+
bm = pm.blob_metadata
|
|
509
|
+
assert bm is not None
|
|
510
|
+
|
|
511
|
+
pkg_name = pm.name_for_installation
|
|
512
|
+
install_root = config.global_blob_install_root(pkg_name)
|
|
513
|
+
|
|
514
|
+
rgs = config.ruyipkg_global_state
|
|
515
|
+
is_installed = rgs.is_package_installed(
|
|
516
|
+
pm.repo_id,
|
|
517
|
+
pm.category,
|
|
518
|
+
pm.name,
|
|
519
|
+
pm.ver,
|
|
520
|
+
"", # host is "" for blob packages
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Check directory existence if the PM state says the package is not installed
|
|
524
|
+
if not is_installed:
|
|
525
|
+
if not os.path.exists(install_root):
|
|
526
|
+
logger.I(f"skipping not-installed package [green]{pkg_name}[/]")
|
|
527
|
+
return 0
|
|
528
|
+
|
|
529
|
+
# There may be potentially user-generated data in the directory,
|
|
530
|
+
# let's be safe and fail the process.
|
|
531
|
+
logger.F(
|
|
532
|
+
f"package [green]{pkg_name}[/] is not tracked as installed, but its directory [yellow]{install_root}[/] exists."
|
|
533
|
+
)
|
|
534
|
+
logger.I("Please remove it manually if you are sure it's safe to do so.")
|
|
535
|
+
logger.I(
|
|
536
|
+
"If you believe this is a bug, please file an issue at [yellow]https://github.com/ruyisdk/ruyi/issues[/]."
|
|
537
|
+
)
|
|
538
|
+
return 1
|
|
539
|
+
|
|
540
|
+
logger.I(f"uninstalling package [green]{pkg_name}[/]")
|
|
541
|
+
if is_installed:
|
|
542
|
+
rgs.remove_installation(
|
|
543
|
+
pm.repo_id,
|
|
544
|
+
pm.category,
|
|
545
|
+
pm.name,
|
|
546
|
+
pm.ver,
|
|
547
|
+
"",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if os.path.exists(install_root):
|
|
551
|
+
shutil.rmtree(install_root)
|
|
552
|
+
|
|
553
|
+
logger.I(f"package [green]{pkg_name}[/] uninstalled")
|
|
554
|
+
return 0
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..cli.cmd import RootCommand
|
|
5
|
+
from .cli_completion import package_completer_builder
|
|
6
|
+
from .host import get_native_host
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..cli.completion import ArgumentParser
|
|
10
|
+
from ..config import GlobalConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExtractCommand(
|
|
14
|
+
RootCommand,
|
|
15
|
+
cmd="extract",
|
|
16
|
+
help="Fetch package(s) then extract to current directory",
|
|
17
|
+
):
|
|
18
|
+
@classmethod
|
|
19
|
+
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
20
|
+
a = p.add_argument(
|
|
21
|
+
"atom",
|
|
22
|
+
type=str,
|
|
23
|
+
nargs="+",
|
|
24
|
+
help="Specifier (atom) of the package(s) to extract",
|
|
25
|
+
)
|
|
26
|
+
if gc.is_cli_autocomplete:
|
|
27
|
+
a.completer = package_completer_builder(gc)
|
|
28
|
+
|
|
29
|
+
p.add_argument(
|
|
30
|
+
"--host",
|
|
31
|
+
type=str,
|
|
32
|
+
default=get_native_host(),
|
|
33
|
+
help="Override the host architecture (normally not needed)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
|
|
38
|
+
from .host import canonicalize_host_str
|
|
39
|
+
from .install import do_extract_atoms
|
|
40
|
+
|
|
41
|
+
host: str = args.host
|
|
42
|
+
atom_strs: set[str] = set(args.atom)
|
|
43
|
+
|
|
44
|
+
return do_extract_atoms(
|
|
45
|
+
cfg,
|
|
46
|
+
cfg.repo,
|
|
47
|
+
atom_strs,
|
|
48
|
+
canonicalized_host=canonicalize_host_str(host),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InstallCommand(
|
|
53
|
+
RootCommand,
|
|
54
|
+
cmd="install",
|
|
55
|
+
aliases=["i"],
|
|
56
|
+
help="Install package from configured repository",
|
|
57
|
+
):
|
|
58
|
+
@classmethod
|
|
59
|
+
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
60
|
+
a = p.add_argument(
|
|
61
|
+
"atom",
|
|
62
|
+
type=str,
|
|
63
|
+
nargs="+",
|
|
64
|
+
help="Specifier (atom) of the package to install",
|
|
65
|
+
)
|
|
66
|
+
if gc.is_cli_autocomplete:
|
|
67
|
+
a.completer = package_completer_builder(gc)
|
|
68
|
+
|
|
69
|
+
p.add_argument(
|
|
70
|
+
"-f",
|
|
71
|
+
"--fetch-only",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Fetch distribution files only without installing",
|
|
74
|
+
)
|
|
75
|
+
p.add_argument(
|
|
76
|
+
"--host",
|
|
77
|
+
type=str,
|
|
78
|
+
default=get_native_host(),
|
|
79
|
+
help="Override the host architecture (normally not needed)",
|
|
80
|
+
)
|
|
81
|
+
p.add_argument(
|
|
82
|
+
"--reinstall",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Force re-installation of already installed packages",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
|
|
89
|
+
from .host import canonicalize_host_str
|
|
90
|
+
from .install import do_install_atoms
|
|
91
|
+
|
|
92
|
+
host: str = args.host
|
|
93
|
+
atom_strs: set[str] = set(args.atom)
|
|
94
|
+
fetch_only: bool = args.fetch_only
|
|
95
|
+
reinstall: bool = args.reinstall
|
|
96
|
+
|
|
97
|
+
return do_install_atoms(
|
|
98
|
+
cfg,
|
|
99
|
+
cfg.repo,
|
|
100
|
+
atom_strs,
|
|
101
|
+
canonicalized_host=canonicalize_host_str(host),
|
|
102
|
+
fetch_only=fetch_only,
|
|
103
|
+
reinstall=reinstall,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UninstallCommand(
|
|
108
|
+
RootCommand,
|
|
109
|
+
cmd="uninstall",
|
|
110
|
+
aliases=["remove", "rm"],
|
|
111
|
+
help="Uninstall installed packages",
|
|
112
|
+
):
|
|
113
|
+
@classmethod
|
|
114
|
+
def configure_args(cls, gc: "GlobalConfig", p: argparse.ArgumentParser) -> None:
|
|
115
|
+
p.add_argument(
|
|
116
|
+
"atom",
|
|
117
|
+
type=str,
|
|
118
|
+
nargs="+",
|
|
119
|
+
help="Specifier (atom) of the package to uninstall",
|
|
120
|
+
)
|
|
121
|
+
p.add_argument(
|
|
122
|
+
"--host",
|
|
123
|
+
type=str,
|
|
124
|
+
default=get_native_host(),
|
|
125
|
+
help="Override the host architecture (normally not needed)",
|
|
126
|
+
)
|
|
127
|
+
p.add_argument(
|
|
128
|
+
"-y",
|
|
129
|
+
"--yes",
|
|
130
|
+
action="store_true",
|
|
131
|
+
dest="assume_yes",
|
|
132
|
+
help="Assume yes to all prompts",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
|
|
137
|
+
from .host import canonicalize_host_str
|
|
138
|
+
from .install import do_uninstall_atoms
|
|
139
|
+
|
|
140
|
+
host: str = args.host
|
|
141
|
+
atom_strs: set[str] = set(args.atom)
|
|
142
|
+
assume_yes: bool = args.assume_yes
|
|
143
|
+
|
|
144
|
+
return do_uninstall_atoms(
|
|
145
|
+
cfg,
|
|
146
|
+
cfg.repo,
|
|
147
|
+
atom_strs,
|
|
148
|
+
canonicalized_host=canonicalize_host_str(host),
|
|
149
|
+
assume_yes=assume_yes,
|
|
150
|
+
)
|