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/__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,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)
|