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
ruyi/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+
5
+ class NuitkaVersion(typing.NamedTuple):
6
+ major: int
7
+ minor: int
8
+ micro: int
9
+ releaselevel: str
10
+ containing_dir: str
11
+ standalone: bool
12
+ onefile: bool
13
+ macos_bundle_mode: bool
14
+ no_asserts: bool
15
+ no_docstrings: bool
16
+ no_annotations: bool
17
+ module: bool
18
+ main: str
19
+ original_argv0: str | None
20
+
21
+ __compiled__: NuitkaVersion
ruyi/__main__.py ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import sys
5
+
6
+ import ruyi
7
+ from ruyi.utils.ci import is_running_in_ci
8
+ from ruyi.utils.global_mode import (
9
+ EnvGlobalModeProvider,
10
+ ENV_FORCE_ALLOW_ROOT,
11
+ TRUTHY_ENV_VAR_VALUES,
12
+ is_env_var_truthy,
13
+ )
14
+
15
+ # NOTE: no imports that directly or indirectly pull in pygit2 should go here,
16
+ # because import of pygit2 will fail if done before ssl_patch. Notably this
17
+ # means no GlobalConfig here because it depends on ruyi.ruyipkg.repo.
18
+
19
+
20
+ def _is_running_as_root() -> bool:
21
+ # this is way too simplistic but works on *nix systems which is all we
22
+ # support currently
23
+ if hasattr(os, "getuid"):
24
+ return os.getuid() == 0
25
+ return False
26
+
27
+
28
+ def _is_allowed_to_run_as_root() -> bool:
29
+ if is_env_var_truthy(os.environ, ENV_FORCE_ALLOW_ROOT):
30
+ return True
31
+ if is_running_in_ci(os.environ):
32
+ # CI environments are usually considered to be controlled, and safe
33
+ # for root usage.
34
+ return True
35
+ return False
36
+
37
+
38
+ def entrypoint() -> None:
39
+ gm = EnvGlobalModeProvider(os.environ, sys.argv)
40
+
41
+ # NOTE: import of `ruyi.log` takes ~90ms on my machine, so initialization
42
+ # of logging is deferred as late as possible
43
+
44
+ if _is_running_as_root() and not _is_allowed_to_run_as_root():
45
+ from ruyi.log import RuyiConsoleLogger
46
+
47
+ logger = RuyiConsoleLogger(gm)
48
+
49
+ logger.F("refusing to run as super user outside CI without explicit consent")
50
+
51
+ choices = ", ".join(f"'{x}'" for x in TRUTHY_ENV_VAR_VALUES)
52
+ logger.I(
53
+ f"re-run with environment variable [yellow]{ENV_FORCE_ALLOW_ROOT}[/] set to one of [yellow]{choices}[/] to signify consent"
54
+ )
55
+ sys.exit(1)
56
+
57
+ if not sys.argv:
58
+ from ruyi.log import RuyiConsoleLogger
59
+
60
+ logger = RuyiConsoleLogger(gm)
61
+
62
+ logger.F("no argv?")
63
+ sys.exit(1)
64
+
65
+ if gm.is_packaged and ruyi.__compiled__.standalone:
66
+ # If we're running from a bundle, our bundled libssl may remember a
67
+ # different path for loading certificates than appropriate for the
68
+ # current system, in which case the pygit2 import will fail. To avoid
69
+ # this we have to patch ssl.get_default_verify_paths with additional
70
+ # logic.
71
+ #
72
+ # this must happen before pygit2 is imported
73
+ from ruyi.utils import ssl_patch
74
+
75
+ del ssl_patch
76
+
77
+ from ruyi.utils.nuitka import get_nuitka_self_exe, get_argv0
78
+
79
+ # note down our own executable path, for identity-checking in mux, if not
80
+ # we're not already Nuitka-compiled
81
+ #
82
+ # we assume the one-file build if Nuitka is detected; sys.argv[0] does NOT
83
+ # work if it's just `ruyi` so we have to check our parent process in that case
84
+ self_exe = get_nuitka_self_exe() if gm.is_packaged else __file__
85
+ sys.argv[0] = get_argv0()
86
+ gm.record_self_exe(sys.argv[0], __file__, self_exe)
87
+
88
+ from ruyi.config import GlobalConfig
89
+ from ruyi.cli.main import main
90
+ from ruyi.log import RuyiConsoleLogger
91
+
92
+ logger = RuyiConsoleLogger(gm)
93
+ gc = GlobalConfig.load_from_config(gm, logger)
94
+ sys.exit(main(gm, gc, sys.argv))
95
+
96
+
97
+ if __name__ == "__main__":
98
+ entrypoint()
ruyi/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from typing import Final
2
+
3
+
4
+ # Should be all-lower for is_called_as_ruyi to work
5
+ RUYI_ENTRYPOINT_NAME: Final = "ruyi"
@@ -0,0 +1,14 @@
1
+ from ..device import provision_cli as provision_cli
2
+ from ..mux.venv import venv_cli as venv_cli
3
+ from ..pluginhost import plugin_cli as plugin_cli
4
+ from ..ruyipkg import admin_cli as admin_cli
5
+ from ..ruyipkg import entity_cli as entity_cli
6
+ from ..ruyipkg import install_cli as install_cli
7
+ from ..ruyipkg import list_cli as list_cli
8
+ from ..ruyipkg import news_cli as news_cli
9
+ from ..ruyipkg import profile_cli as profile_cli
10
+ from ..ruyipkg import update_cli as update_cli
11
+ from ..telemetry import telemetry_cli as telemetry_cli
12
+ from . import self_cli as self_cli
13
+ from . import config_cli as config_cli
14
+ from . import version_cli as version_cli
ruyi/cli/cmd.py ADDED
@@ -0,0 +1,224 @@
1
+ import argparse
2
+ from typing import Callable, IO, Protocol, TYPE_CHECKING
3
+
4
+ from . import RUYI_ENTRYPOINT_NAME
5
+
6
+ if TYPE_CHECKING:
7
+ from ..config import GlobalConfig
8
+ from .completion import ArgumentParser
9
+
10
+ CLIEntrypoint = Callable[["GlobalConfig", argparse.Namespace], int]
11
+
12
+
13
+ class _PrintHelp(Protocol):
14
+ def print_help(self, file: IO[str] | None = None) -> None: ...
15
+
16
+
17
+ def _wrap_help(x: _PrintHelp) -> "CLIEntrypoint":
18
+ def _wrapped_(gc: "GlobalConfig", args: argparse.Namespace) -> int:
19
+ x.print_help()
20
+ return 0
21
+
22
+ return _wrapped_
23
+
24
+
25
+ class BaseCommand:
26
+ parsers: "list[type[BaseCommand]]" = []
27
+
28
+ cmd: str | None
29
+ _tele_key: str | None
30
+ has_subcommands: bool
31
+ is_experimental: bool
32
+ is_subcommand_required: bool
33
+ has_main: bool
34
+ aliases: list[str]
35
+ description: str | None
36
+ prog: str | None
37
+ help: str | None
38
+
39
+ def __init_subclass__(
40
+ cls,
41
+ cmd: str | None,
42
+ has_subcommands: bool = False,
43
+ is_subcommand_required: bool = False,
44
+ is_experimental: bool = False,
45
+ has_main: bool | None = None,
46
+ aliases: list[str] | None = None,
47
+ description: str | None = None,
48
+ prog: str | None = None,
49
+ help: str | None = None,
50
+ **kwargs: object,
51
+ ) -> None:
52
+ cls.cmd = cmd
53
+
54
+ if cmd is None:
55
+ cls._tele_key = None
56
+ else:
57
+ parent_cls = cls.mro()[1]
58
+ parent_raw_tele_key = getattr(parent_cls, "_tele_key", None)
59
+ if parent_raw_tele_key is None:
60
+ cls._tele_key = cmd
61
+ else:
62
+ cls._tele_key = f"{parent_raw_tele_key} {cmd}"
63
+
64
+ cls.has_subcommands = has_subcommands
65
+ cls.is_subcommand_required = is_subcommand_required
66
+ cls.is_experimental = is_experimental
67
+ cls.has_main = has_main if has_main is not None else not has_subcommands
68
+
69
+ # argparse params
70
+ cls.aliases = aliases or []
71
+ cls.description = description
72
+ cls.prog = prog
73
+ cls.help = help
74
+
75
+ cls.parsers.append(cls)
76
+
77
+ super().__init_subclass__(**kwargs)
78
+
79
+ @classmethod
80
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
81
+ """Configure arguments for this parser."""
82
+ pass
83
+
84
+ @classmethod
85
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
86
+ """Entrypoint of this command."""
87
+ raise NotImplementedError
88
+
89
+ @classmethod
90
+ def is_root(cls) -> bool:
91
+ return cls.cmd is None
92
+
93
+ @classmethod
94
+ def _build_tele_key(cls) -> str:
95
+ return "<bare>" if cls._tele_key is None else cls._tele_key
96
+
97
+ @classmethod
98
+ def build_argparse(cls, gc: "GlobalConfig") -> "ArgumentParser":
99
+ from .completion import ArgumentParser
100
+
101
+ p = ArgumentParser(prog=cls.prog, description=cls.description)
102
+ cls.configure_args(gc, p)
103
+ cls._populate_defaults(p)
104
+ cls._maybe_build_subcommands(gc, p)
105
+ return p
106
+
107
+ @classmethod
108
+ def _maybe_build_subcommands(
109
+ cls,
110
+ gc: "GlobalConfig",
111
+ p: "ArgumentParser",
112
+ ) -> None:
113
+ if not cls.has_subcommands:
114
+ return
115
+
116
+ sp = p.add_subparsers(
117
+ title="subcommands",
118
+ required=cls.is_subcommand_required,
119
+ )
120
+ for subcmd_cls in cls.parsers:
121
+ if subcmd_cls.mro()[1] is not cls:
122
+ # do not recurse onto self or non-direct subclasses
123
+ continue
124
+ if subcmd_cls.is_experimental and not gc.is_experimental:
125
+ # skip configuring experimental commands if not enabled in
126
+ # the environment
127
+ continue
128
+ subcmd_cls._configure_subcommand(gc, sp)
129
+
130
+ @classmethod
131
+ def _configure_subcommand(
132
+ cls,
133
+ gc: "GlobalConfig",
134
+ sp: "argparse._SubParsersAction[ArgumentParser]",
135
+ ) -> argparse.ArgumentParser:
136
+ assert cls.cmd is not None
137
+ p = sp.add_parser(
138
+ cls.cmd,
139
+ aliases=cls.aliases,
140
+ help=cls.help,
141
+ )
142
+ cls.configure_args(gc, p)
143
+ cls._populate_defaults(p)
144
+ cls._maybe_build_subcommands(gc, p)
145
+ return p
146
+
147
+ @classmethod
148
+ def _populate_defaults(cls, p: "ArgumentParser") -> None:
149
+ if cls.has_main:
150
+ p.set_defaults(func=cls.main, tele_key=cls._build_tele_key())
151
+ else:
152
+ p.set_defaults(func=_wrap_help(p), tele_key=cls._build_tele_key())
153
+
154
+
155
+ class RootCommand(
156
+ BaseCommand,
157
+ cmd=None,
158
+ has_subcommands=True,
159
+ has_main=True,
160
+ prog=RUYI_ENTRYPOINT_NAME,
161
+ description="RuyiSDK Package Manager",
162
+ ):
163
+ @classmethod
164
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
165
+ from .version_cli import cli_version
166
+
167
+ p.add_argument(
168
+ "-V",
169
+ "--version",
170
+ action="store_const",
171
+ dest="func",
172
+ const=cli_version,
173
+ help="Print version information",
174
+ )
175
+ p.add_argument(
176
+ "--porcelain",
177
+ action="store_true",
178
+ help="Give the output in a machine-friendly format if applicable",
179
+ )
180
+
181
+ # https://github.com/python/cpython/issues/67037 prevents the registration
182
+ # of undocumented subcommands, so a preferred usage of
183
+ # "ruyi completion-script --shell=bash" is not possible right now.
184
+ p.add_argument(
185
+ "--output-completion-script",
186
+ action="store",
187
+ type=str,
188
+ dest="completion_script",
189
+ default=None,
190
+ help=argparse.SUPPRESS,
191
+ )
192
+
193
+ @classmethod
194
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
195
+ sh: str | None = args.completion_script
196
+ if not sh:
197
+ args._parser.print_help() # pylint: disable=protected-access
198
+ return 0
199
+ # the rest are implementation of "--output-completion-script"
200
+
201
+ if sh not in {"bash", "zsh"}:
202
+ raise ValueError(f"Unsupported shell: {sh}")
203
+
204
+ import sys
205
+ from ..resource_bundle import get_resource_str
206
+
207
+ script = get_resource_str("_ruyi_completion")
208
+ assert script is not None, "should never happen; completion script not found"
209
+ sys.stdout.write(script)
210
+ return 0
211
+
212
+
213
+ # Repo admin commands
214
+ class AdminCommand(
215
+ RootCommand,
216
+ cmd="admin",
217
+ has_subcommands=True,
218
+ # https://github.com/python/cpython/issues/67037
219
+ # help=argparse.SUPPRESS,
220
+ help="(NOT FOR REGULAR USERS) Subcommands for managing Ruyi repos",
221
+ ):
222
+ @classmethod
223
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
224
+ pass
ruyi/cli/completer.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ helper functions for CLI completions
3
+ """
4
+
5
+ import argparse
6
+ from typing import Protocol, Any, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ # A "lie" for type checking purposes. This is a known and wont fix issue for mypy.
10
+ # Mypy would think the fallback import needs to be the same type as the first import.
11
+ # See: https://github.com/python/mypy/issues/1153
12
+ from argcomplete.completers import BaseCompleter
13
+ else:
14
+ try:
15
+ from argcomplete.completers import BaseCompleter
16
+ except ImportError:
17
+ # Fallback for environments where argcomplete is less than 2.0.0
18
+ class BaseCompleter(object):
19
+ def __call__(
20
+ self,
21
+ *,
22
+ prefix: str,
23
+ action: argparse.Action,
24
+ parser: argparse.ArgumentParser,
25
+ parsed_args: argparse.Namespace,
26
+ ) -> None:
27
+ raise NotImplementedError(
28
+ "This method should be implemented by a subclass."
29
+ )
30
+
31
+
32
+ class NoneCompleter(BaseCompleter):
33
+ def __call__(
34
+ self,
35
+ *,
36
+ prefix: str,
37
+ action: argparse.Action,
38
+ parser: argparse.ArgumentParser,
39
+ parsed_args: argparse.Namespace,
40
+ ) -> None:
41
+ return None
42
+
43
+
44
+ class DynamicCompleter(Protocol):
45
+ def __call__(
46
+ self,
47
+ prefix: str,
48
+ parsed_args: object,
49
+ **kwargs: Any,
50
+ ) -> list[str]: ...
ruyi/cli/completion.py ADDED
@@ -0,0 +1,26 @@
1
+ """
2
+ helper functions for CLI completions
3
+
4
+ see https://github.com/kislyuk/argcomplete/issues/443 for why this is needed
5
+ """
6
+
7
+ import argparse
8
+ from typing import Any, Callable, Optional, Sequence, cast
9
+
10
+
11
+ class ArgcompleteAction(argparse.Action):
12
+ completer: Optional[Callable[[str, object], list[str]]]
13
+
14
+ def __call__(
15
+ self,
16
+ parser: argparse.ArgumentParser,
17
+ namespace: argparse.Namespace,
18
+ values: str | Sequence[Any] | None,
19
+ option_string: str | None = None,
20
+ ) -> None:
21
+ raise NotImplementedError(".__call__() not defined")
22
+
23
+
24
+ class ArgumentParser(argparse.ArgumentParser):
25
+ def add_argument(self, *args: Any, **kwargs: Any) -> ArgcompleteAction:
26
+ return cast(ArgcompleteAction, super().add_argument(*args, **kwargs))
ruyi/cli/config_cli.py ADDED
@@ -0,0 +1,153 @@
1
+ import argparse
2
+ from typing import TYPE_CHECKING
3
+
4
+ from .cmd import RootCommand
5
+
6
+ if TYPE_CHECKING:
7
+ from .completion import ArgumentParser
8
+ from .. import config
9
+
10
+
11
+ # Config management commands
12
+ class ConfigCommand(
13
+ RootCommand,
14
+ cmd="config",
15
+ has_subcommands=True,
16
+ help="Manage Ruyi's config options",
17
+ ):
18
+ @classmethod
19
+ def configure_args(
20
+ cls,
21
+ gc: "config.GlobalConfig",
22
+ p: "ArgumentParser",
23
+ ) -> None:
24
+ pass
25
+
26
+
27
+ class ConfigGetCommand(
28
+ ConfigCommand,
29
+ cmd="get",
30
+ help="Query the value of a Ruyi config option",
31
+ ):
32
+ @classmethod
33
+ def configure_args(
34
+ cls,
35
+ gc: "config.GlobalConfig",
36
+ p: "ArgumentParser",
37
+ ) -> None:
38
+ p.add_argument(
39
+ "key",
40
+ type=str,
41
+ help="The Ruyi config option to query",
42
+ )
43
+
44
+ @classmethod
45
+ def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
46
+ from ..config.schema import encode_value
47
+
48
+ key: str = args.key
49
+
50
+ val = cfg.get_by_key(key)
51
+ if val is None:
52
+ return 1
53
+
54
+ cfg.logger.stdout(encode_value(val))
55
+ return 0
56
+
57
+
58
+ class ConfigSetCommand(
59
+ ConfigCommand,
60
+ cmd="set",
61
+ help="Set the value of a Ruyi config option",
62
+ ):
63
+ @classmethod
64
+ def configure_args(
65
+ cls,
66
+ gc: "config.GlobalConfig",
67
+ p: "ArgumentParser",
68
+ ) -> None:
69
+ p.add_argument(
70
+ "key",
71
+ type=str,
72
+ help="The Ruyi config option to set",
73
+ )
74
+ p.add_argument(
75
+ "value",
76
+ type=str,
77
+ help="The value to set the option to",
78
+ )
79
+
80
+ @classmethod
81
+ def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
82
+ from ..config.editor import ConfigEditor
83
+ from ..config.schema import decode_value
84
+
85
+ key: str = args.key
86
+ val: str = args.value
87
+
88
+ pyval = decode_value(key, val)
89
+ with ConfigEditor.work_on_user_local_config(cfg) as ed:
90
+ ed.set_value(key, pyval)
91
+ ed.stage()
92
+
93
+ return 0
94
+
95
+
96
+ class ConfigUnsetCommand(
97
+ ConfigCommand,
98
+ cmd="unset",
99
+ help="Unset a Ruyi config option",
100
+ ):
101
+ @classmethod
102
+ def configure_args(
103
+ cls,
104
+ gc: "config.GlobalConfig",
105
+ p: "ArgumentParser",
106
+ ) -> None:
107
+ p.add_argument(
108
+ "key",
109
+ type=str,
110
+ help="The Ruyi config option to unset",
111
+ )
112
+
113
+ @classmethod
114
+ def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
115
+ from ..config.editor import ConfigEditor
116
+
117
+ key: str = args.key
118
+
119
+ with ConfigEditor.work_on_user_local_config(cfg) as ed:
120
+ ed.unset_value(key)
121
+ ed.stage()
122
+
123
+ return 0
124
+
125
+
126
+ class ConfigRemoveSectionCommand(
127
+ ConfigCommand,
128
+ cmd="remove-section",
129
+ help="Remove a section from the Ruyi config",
130
+ ):
131
+ @classmethod
132
+ def configure_args(
133
+ cls,
134
+ gc: "config.GlobalConfig",
135
+ p: "ArgumentParser",
136
+ ) -> None:
137
+ p.add_argument(
138
+ "section",
139
+ type=str,
140
+ help="The section to remove",
141
+ )
142
+
143
+ @classmethod
144
+ def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
145
+ from ..config.editor import ConfigEditor
146
+
147
+ section: str = args.section
148
+
149
+ with ConfigEditor.work_on_user_local_config(cfg) as ed:
150
+ ed.remove_section(section)
151
+ ed.stage()
152
+
153
+ return 0
ruyi/cli/main.py ADDED
@@ -0,0 +1,111 @@
1
+ import atexit
2
+ import os
3
+ import sys
4
+ from typing import Final, TYPE_CHECKING
5
+
6
+ from ..config import GlobalConfig
7
+ from ..telemetry.scope import TelemetryScope
8
+ from ..utils.global_mode import GlobalModeProvider
9
+ from . import RUYI_ENTRYPOINT_NAME
10
+
11
+ ALLOWED_RUYI_ENTRYPOINT_NAMES: Final = (
12
+ RUYI_ENTRYPOINT_NAME,
13
+ f"{RUYI_ENTRYPOINT_NAME}.exe",
14
+ f"{RUYI_ENTRYPOINT_NAME}.bin", # Nuitka one-file program cache
15
+ "__main__.py",
16
+ )
17
+
18
+
19
+ def is_called_as_ruyi(argv0: str) -> bool:
20
+ return os.path.basename(argv0).lower() in ALLOWED_RUYI_ENTRYPOINT_NAMES
21
+
22
+
23
+ def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
24
+ if tm := gc.telemetry:
25
+ tm.check_first_run_status()
26
+ tm.init_installation(False)
27
+ atexit.register(tm.flush)
28
+ tm.maybe_prompt_for_first_run_upload()
29
+
30
+ if not is_called_as_ruyi(gm.argv0):
31
+ from ..mux.runtime import mux_main
32
+
33
+ # record an invocation and the command name being proxied to
34
+ if tm := gc.telemetry:
35
+ tm.record(
36
+ TelemetryScope(None),
37
+ "cli:mux-invocation-v1",
38
+ target=os.path.basename(gm.argv0),
39
+ )
40
+
41
+ return mux_main(gm, gc, argv)
42
+
43
+ import ruyi
44
+ from .cmd import RootCommand
45
+ from . import builtin_commands
46
+
47
+ del builtin_commands
48
+
49
+ if TYPE_CHECKING:
50
+ from .cmd import CLIEntrypoint
51
+
52
+ logger = gc.logger
53
+ p = RootCommand.build_argparse(gc)
54
+
55
+ # We have to ensure argcomplete is only requested when it's supposed to,
56
+ # as the argcomplete import is very costly in terms of startup time, and
57
+ # that the package name completer requires the whole repo to be synced
58
+ # (which may not be the case for an out-of-the-box experience).
59
+ if gm.is_cli_autocomplete:
60
+ import argcomplete
61
+ from .completer import NoneCompleter
62
+
63
+ argcomplete.autocomplete(
64
+ p,
65
+ always_complete_options=True,
66
+ default_completer=NoneCompleter(),
67
+ )
68
+
69
+ args = p.parse_args(argv[1:])
70
+ # for getting access to the argparse parser in the CLI entrypoint
71
+ args._parser = p # pylint: disable=protected-access
72
+
73
+ gm.is_porcelain = args.porcelain
74
+
75
+ nuitka_info = "not compiled"
76
+ if hasattr(ruyi, "__compiled__"):
77
+ nuitka_info = f"__compiled__ = {ruyi.__compiled__}"
78
+
79
+ logger.D(
80
+ f"__main__.__file__ = {gm.main_file}, sys.executable = {sys.executable}, {nuitka_info}"
81
+ )
82
+ logger.D(f"argv[0] = {gm.argv0}, self_exe = {gm.self_exe}")
83
+ logger.D(f"args={args}")
84
+
85
+ func: "CLIEntrypoint" = args.func
86
+
87
+ # record every invocation's subcommand for better insight into usage
88
+ # frequencies
89
+ try:
90
+ telemetry_key: str = args.tele_key
91
+ except AttributeError:
92
+ logger.F("internal error: CLI entrypoint was added without a telemetry key")
93
+ return 1
94
+
95
+ # Special-case the `--output-completion-script` argument; treat it as if
96
+ # "ruyi completion-script" were called.
97
+ try:
98
+ if args.completion_script:
99
+ telemetry_key = "completion-script"
100
+ except AttributeError:
101
+ pass
102
+
103
+ if tm := gc.telemetry:
104
+ tm.print_telemetry_notice()
105
+ tm.record(
106
+ TelemetryScope(None),
107
+ "cli:invocation-v1",
108
+ key=telemetry_key,
109
+ )
110
+
111
+ return func(gc, args)