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.
Files changed (101) hide show
  1. ruyi/__init__.py +21 -0
  2. ruyi/__main__.py +98 -0
  3. ruyi/cli/__init__.py +5 -0
  4. ruyi/cli/builtin_commands.py +14 -0
  5. ruyi/cli/cmd.py +224 -0
  6. ruyi/cli/completer.py +50 -0
  7. ruyi/cli/completion.py +26 -0
  8. ruyi/cli/config_cli.py +153 -0
  9. ruyi/cli/main.py +111 -0
  10. ruyi/cli/self_cli.py +295 -0
  11. ruyi/cli/user_input.py +127 -0
  12. ruyi/cli/version_cli.py +45 -0
  13. ruyi/config/__init__.py +401 -0
  14. ruyi/config/editor.py +92 -0
  15. ruyi/config/errors.py +76 -0
  16. ruyi/config/news.py +39 -0
  17. ruyi/config/schema.py +197 -0
  18. ruyi/device/__init__.py +0 -0
  19. ruyi/device/provision.py +591 -0
  20. ruyi/device/provision_cli.py +40 -0
  21. ruyi/log/__init__.py +272 -0
  22. ruyi/mux/.gitignore +1 -0
  23. ruyi/mux/__init__.py +0 -0
  24. ruyi/mux/runtime.py +213 -0
  25. ruyi/mux/venv/__init__.py +12 -0
  26. ruyi/mux/venv/emulator_cfg.py +41 -0
  27. ruyi/mux/venv/maker.py +782 -0
  28. ruyi/mux/venv/venv_cli.py +92 -0
  29. ruyi/mux/venv_cfg.py +214 -0
  30. ruyi/pluginhost/__init__.py +0 -0
  31. ruyi/pluginhost/api.py +206 -0
  32. ruyi/pluginhost/ctx.py +222 -0
  33. ruyi/pluginhost/paths.py +135 -0
  34. ruyi/pluginhost/plugin_cli.py +37 -0
  35. ruyi/pluginhost/unsandboxed.py +246 -0
  36. ruyi/py.typed +0 -0
  37. ruyi/resource_bundle/__init__.py +20 -0
  38. ruyi/resource_bundle/__main__.py +55 -0
  39. ruyi/resource_bundle/data.py +26 -0
  40. ruyi/ruyipkg/__init__.py +0 -0
  41. ruyi/ruyipkg/admin_checksum.py +88 -0
  42. ruyi/ruyipkg/admin_cli.py +83 -0
  43. ruyi/ruyipkg/atom.py +184 -0
  44. ruyi/ruyipkg/augmented_pkg.py +212 -0
  45. ruyi/ruyipkg/canonical_dump.py +320 -0
  46. ruyi/ruyipkg/checksum.py +39 -0
  47. ruyi/ruyipkg/cli_completion.py +42 -0
  48. ruyi/ruyipkg/distfile.py +208 -0
  49. ruyi/ruyipkg/entity.py +387 -0
  50. ruyi/ruyipkg/entity_cli.py +123 -0
  51. ruyi/ruyipkg/entity_provider.py +273 -0
  52. ruyi/ruyipkg/fetch.py +271 -0
  53. ruyi/ruyipkg/host.py +55 -0
  54. ruyi/ruyipkg/install.py +554 -0
  55. ruyi/ruyipkg/install_cli.py +150 -0
  56. ruyi/ruyipkg/list.py +126 -0
  57. ruyi/ruyipkg/list_cli.py +79 -0
  58. ruyi/ruyipkg/list_filter.py +173 -0
  59. ruyi/ruyipkg/msg.py +99 -0
  60. ruyi/ruyipkg/news.py +123 -0
  61. ruyi/ruyipkg/news_cli.py +78 -0
  62. ruyi/ruyipkg/news_store.py +183 -0
  63. ruyi/ruyipkg/pkg_manifest.py +657 -0
  64. ruyi/ruyipkg/profile.py +208 -0
  65. ruyi/ruyipkg/profile_cli.py +33 -0
  66. ruyi/ruyipkg/protocols.py +55 -0
  67. ruyi/ruyipkg/repo.py +763 -0
  68. ruyi/ruyipkg/state.py +345 -0
  69. ruyi/ruyipkg/unpack.py +369 -0
  70. ruyi/ruyipkg/unpack_method.py +91 -0
  71. ruyi/ruyipkg/update_cli.py +54 -0
  72. ruyi/telemetry/__init__.py +0 -0
  73. ruyi/telemetry/aggregate.py +72 -0
  74. ruyi/telemetry/event.py +41 -0
  75. ruyi/telemetry/node_info.py +192 -0
  76. ruyi/telemetry/provider.py +411 -0
  77. ruyi/telemetry/scope.py +43 -0
  78. ruyi/telemetry/store.py +238 -0
  79. ruyi/telemetry/telemetry_cli.py +127 -0
  80. ruyi/utils/__init__.py +0 -0
  81. ruyi/utils/ar.py +74 -0
  82. ruyi/utils/ci.py +63 -0
  83. ruyi/utils/frontmatter.py +38 -0
  84. ruyi/utils/git.py +169 -0
  85. ruyi/utils/global_mode.py +204 -0
  86. ruyi/utils/l10n.py +83 -0
  87. ruyi/utils/markdown.py +73 -0
  88. ruyi/utils/nuitka.py +33 -0
  89. ruyi/utils/porcelain.py +51 -0
  90. ruyi/utils/prereqs.py +77 -0
  91. ruyi/utils/ssl_patch.py +170 -0
  92. ruyi/utils/templating.py +34 -0
  93. ruyi/utils/toml.py +115 -0
  94. ruyi/utils/url.py +7 -0
  95. ruyi/utils/xdg_basedir.py +80 -0
  96. ruyi/version.py +67 -0
  97. ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
  98. ruyi-0.39.0.dist-info/METADATA +403 -0
  99. ruyi-0.39.0.dist-info/RECORD +101 -0
  100. ruyi-0.39.0.dist-info/WHEEL +4 -0
  101. ruyi-0.39.0.dist-info/entry_points.txt +3 -0
@@ -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
+ )