ruyi 0.42.0a20251005__tar.gz → 0.42.0b20251014__tar.gz
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-0.42.0a20251005 → ruyi-0.42.0b20251014}/PKG-INFO +17 -10
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/README.md +16 -9
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/pyproject.toml +1 -1
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/config_cli.py +20 -4
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/main.py +12 -7
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/config/__init__.py +127 -38
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/config/editor.py +34 -3
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/config/errors.py +12 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/config/schema.py +5 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/device/provision.py +1 -1
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/distfile.py +12 -4
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/install.py +99 -32
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/install_cli.py +29 -1
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/profile.py +148 -1
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/repo.py +3 -2
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/unpack.py +14 -16
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/provider.py +65 -22
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/git.py +4 -2
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/toml.py +11 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/xdg_basedir.py +20 -13
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/LICENSE-Apache.txt +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/__main__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/builtin_commands.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/cmd.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/completer.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/completion.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/oobe.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/self_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/user_input.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/cli/version_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/config/news.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/device/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/device/provision_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/log/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/runtime.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/venv/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/venv/emulator_cfg.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/venv/maker.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/venv/venv_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/mux/venv_cfg.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/pluginhost/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/pluginhost/api.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/pluginhost/ctx.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/pluginhost/paths.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/pluginhost/plugin_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/pluginhost/unsandboxed.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/py.typed +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/resource_bundle/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/resource_bundle/__main__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/resource_bundle/data.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/admin_checksum.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/admin_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/atom.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/augmented_pkg.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/canonical_dump.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/checksum.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/cli_completion.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/entity.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/entity_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/entity_provider.py +0 -0
- /ruyi-0.42.0a20251005/ruyi/ruyipkg/fetch.py → /ruyi-0.42.0b20251014/ruyi/ruyipkg/fetcher.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/host.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/list.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/list_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/list_filter.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/msg.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/news.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/news_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/news_store.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/pkg_manifest.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/profile_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/protocols.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/state.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/unpack_method.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/ruyipkg/update_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/aggregate.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/event.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/node_info.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/scope.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/store.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/telemetry/telemetry_cli.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/__init__.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/ar.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/ci.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/frontmatter.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/global_mode.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/l10n.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/markdown.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/mounts.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/nuitka.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/porcelain.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/prereqs.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/ssl_patch.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/templating.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/utils/url.py +0 -0
- {ruyi-0.42.0a20251005 → ruyi-0.42.0b20251014}/ruyi/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruyi
|
|
3
|
-
Version: 0.42.
|
|
3
|
+
Version: 0.42.0b20251014
|
|
4
4
|
Summary: Package manager for RuyiSDK
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -332,6 +332,10 @@ Various aspects of `ruyi` can be configured with files or environment variables.
|
|
|
332
332
|
look up its config accordingly. If these are not explicitly set though, as in
|
|
333
333
|
typical use cases, the default config directory is most likely `~/.config/ruyi`.
|
|
334
334
|
|
|
335
|
+
GNU/Linux distribution packagers and system administrators will find that the
|
|
336
|
+
directories `/usr/share/ruyi` and `/usr/local/share/ruyi` are searched for the
|
|
337
|
+
config file on such systems.
|
|
338
|
+
|
|
335
339
|
### Config file
|
|
336
340
|
|
|
337
341
|
Currently `ruyi` will look for an optional `config.toml` in its XDG config
|
|
@@ -361,9 +365,8 @@ branch = "main"
|
|
|
361
365
|
# details.
|
|
362
366
|
#
|
|
363
367
|
# If unset or empty, this default value is used: data will be collected and
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
mode = "on"
|
|
368
|
+
# stored locally; nothing will be uploaded automatically.
|
|
369
|
+
mode = "local"
|
|
367
370
|
# The time the user's consent is given to telemetry data uploading. If the
|
|
368
371
|
# system time is later than the time given here, telemetry consent banner will
|
|
369
372
|
# not be displayed any more each time `ruyi` is executed. The exact consent
|
|
@@ -407,12 +410,16 @@ There are 3 telemetry modes available:
|
|
|
407
410
|
* `off`: data will not be collected nor uploaded.
|
|
408
411
|
* `on`: data will be collected and periodically uploaded.
|
|
409
412
|
|
|
410
|
-
By default the `
|
|
411
|
-
will record some non-sensitive information locally
|
|
412
|
-
states of `ruyi`,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
By default the `local` mode is active from `ruyi` 0.42.0 (inclusive) on, which
|
|
414
|
+
means every `ruyi` invocation will record some non-sensitive information locally
|
|
415
|
+
alongside various other states of `ruyi`, but collected data will not be
|
|
416
|
+
uploaded automatically unless you explicitly request so (for example by
|
|
417
|
+
switching to the `on` mode, or by executing `ruyi telemetry upload`).
|
|
418
|
+
|
|
419
|
+
In case the `on` mode is active, collected data will be periodically uploaded
|
|
420
|
+
to servers managed by the RuyiSDK team in the People's Republic of China, in a
|
|
421
|
+
weekly fashion. The upload will happen on a random weekday which is determined
|
|
422
|
+
by the installation's anonymous ID alone.
|
|
416
423
|
|
|
417
424
|
You can change the telemetry mode by editing `ruyi`'s config file, or simply
|
|
418
425
|
disable telemetry altogether by setting the `RUYI_TELEMETRY_OPTOUT` environment
|
|
@@ -88,6 +88,10 @@ Various aspects of `ruyi` can be configured with files or environment variables.
|
|
|
88
88
|
look up its config accordingly. If these are not explicitly set though, as in
|
|
89
89
|
typical use cases, the default config directory is most likely `~/.config/ruyi`.
|
|
90
90
|
|
|
91
|
+
GNU/Linux distribution packagers and system administrators will find that the
|
|
92
|
+
directories `/usr/share/ruyi` and `/usr/local/share/ruyi` are searched for the
|
|
93
|
+
config file on such systems.
|
|
94
|
+
|
|
91
95
|
### Config file
|
|
92
96
|
|
|
93
97
|
Currently `ruyi` will look for an optional `config.toml` in its XDG config
|
|
@@ -117,9 +121,8 @@ branch = "main"
|
|
|
117
121
|
# details.
|
|
118
122
|
#
|
|
119
123
|
# If unset or empty, this default value is used: data will be collected and
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
mode = "on"
|
|
124
|
+
# stored locally; nothing will be uploaded automatically.
|
|
125
|
+
mode = "local"
|
|
123
126
|
# The time the user's consent is given to telemetry data uploading. If the
|
|
124
127
|
# system time is later than the time given here, telemetry consent banner will
|
|
125
128
|
# not be displayed any more each time `ruyi` is executed. The exact consent
|
|
@@ -163,12 +166,16 @@ There are 3 telemetry modes available:
|
|
|
163
166
|
* `off`: data will not be collected nor uploaded.
|
|
164
167
|
* `on`: data will be collected and periodically uploaded.
|
|
165
168
|
|
|
166
|
-
By default the `
|
|
167
|
-
will record some non-sensitive information locally
|
|
168
|
-
states of `ruyi`,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
By default the `local` mode is active from `ruyi` 0.42.0 (inclusive) on, which
|
|
170
|
+
means every `ruyi` invocation will record some non-sensitive information locally
|
|
171
|
+
alongside various other states of `ruyi`, but collected data will not be
|
|
172
|
+
uploaded automatically unless you explicitly request so (for example by
|
|
173
|
+
switching to the `on` mode, or by executing `ruyi telemetry upload`).
|
|
174
|
+
|
|
175
|
+
In case the `on` mode is active, collected data will be periodically uploaded
|
|
176
|
+
to servers managed by the RuyiSDK team in the People's Republic of China, in a
|
|
177
|
+
weekly fashion. The upload will happen on a random weekday which is determined
|
|
178
|
+
by the installation's anonymous ID alone.
|
|
172
179
|
|
|
173
180
|
You can change the telemetry mode by editing `ruyi`'s config file, or simply
|
|
174
181
|
disable telemetry altogether by setting the `RUYI_TELEMETRY_OPTOUT` environment
|
|
@@ -43,15 +43,23 @@ class ConfigGetCommand(
|
|
|
43
43
|
|
|
44
44
|
@classmethod
|
|
45
45
|
def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
|
|
46
|
+
from ..config.errors import InvalidConfigKeyError
|
|
46
47
|
from ..config.schema import encode_value
|
|
48
|
+
from ..utils.toml import NoneValue
|
|
47
49
|
|
|
48
50
|
key: str = args.key
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
try:
|
|
53
|
+
val = cfg.get_by_key(key)
|
|
54
|
+
except InvalidConfigKeyError:
|
|
52
55
|
return 1
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
try:
|
|
58
|
+
encoded_val = encode_value(val)
|
|
59
|
+
except NoneValue:
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
cfg.logger.stdout(encoded_val)
|
|
55
63
|
return 0
|
|
56
64
|
|
|
57
65
|
|
|
@@ -80,6 +88,7 @@ class ConfigSetCommand(
|
|
|
80
88
|
@classmethod
|
|
81
89
|
def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
|
|
82
90
|
from ..config.editor import ConfigEditor
|
|
91
|
+
from ..config.errors import ProtectedGlobalConfigError
|
|
83
92
|
from ..config.schema import decode_value
|
|
84
93
|
|
|
85
94
|
key: str = args.key
|
|
@@ -87,7 +96,14 @@ class ConfigSetCommand(
|
|
|
87
96
|
|
|
88
97
|
pyval = decode_value(key, val)
|
|
89
98
|
with ConfigEditor.work_on_user_local_config(cfg) as ed:
|
|
90
|
-
|
|
99
|
+
try:
|
|
100
|
+
ed.set_value(key, pyval)
|
|
101
|
+
except ProtectedGlobalConfigError:
|
|
102
|
+
cfg.logger.F(
|
|
103
|
+
f"the config [yellow]{key}[/] is protected and not meant to be overridden by users",
|
|
104
|
+
)
|
|
105
|
+
return 2
|
|
106
|
+
|
|
91
107
|
ed.stage()
|
|
92
108
|
|
|
93
109
|
return 0
|
|
@@ -27,15 +27,20 @@ def should_prompt_for_renaming(argv0: str) -> bool:
|
|
|
27
27
|
|
|
28
28
|
def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
|
|
29
29
|
logger = gc.logger
|
|
30
|
-
oobe = OOBE(gc)
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
oobe
|
|
31
|
+
# do not init telemetry or OOBE on CLI auto-completion invocations, because
|
|
32
|
+
# our output isn't meant for humans in that case, and a "real" invocation
|
|
33
|
+
# will likely follow shortly after
|
|
34
|
+
if not gm.is_cli_autocomplete:
|
|
35
|
+
oobe = OOBE(gc)
|
|
36
|
+
|
|
37
|
+
if tm := gc.telemetry:
|
|
38
|
+
tm.check_first_run_status()
|
|
39
|
+
tm.init_installation(False)
|
|
40
|
+
atexit.register(tm.flush)
|
|
41
|
+
oobe.handlers.append(tm.oobe_prompt)
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
oobe.maybe_prompt()
|
|
39
44
|
|
|
40
45
|
if not is_called_as_ruyi(gm.argv0):
|
|
41
46
|
if should_prompt_for_renaming(gm.argv0):
|
|
@@ -15,8 +15,10 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from ..ruyipkg.state import RuyipkgGlobalStateStore
|
|
16
16
|
from ..telemetry.provider import TelemetryProvider
|
|
17
17
|
from ..utils.global_mode import ProvidesGlobalMode
|
|
18
|
+
from ..utils.xdg_basedir import XDGPathEntry
|
|
18
19
|
from .news import NewsReadStatusStore
|
|
19
20
|
|
|
21
|
+
from . import errors
|
|
20
22
|
from . import schema
|
|
21
23
|
|
|
22
24
|
|
|
@@ -33,6 +35,7 @@ else:
|
|
|
33
35
|
DEFAULT_APP_NAME: Final = "ruyi"
|
|
34
36
|
DEFAULT_REPO_URL: Final = "https://github.com/ruyisdk/packages-index.git"
|
|
35
37
|
DEFAULT_REPO_BRANCH: Final = "main"
|
|
38
|
+
DEFAULT_TELEMETRY_MODE: Final = "local" # "off", "local", "on"
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def get_host_path_fragment_for_binary_install_dir(canonicalized_host: str) -> str:
|
|
@@ -101,12 +104,21 @@ class GlobalConfig:
|
|
|
101
104
|
self._telemetry_upload_consent: datetime.datetime | None = None
|
|
102
105
|
self._telemetry_pm_telemetry_url: str | None = None
|
|
103
106
|
|
|
104
|
-
def
|
|
107
|
+
def _apply_config(
|
|
108
|
+
self,
|
|
109
|
+
config_data: GlobalConfigRootType,
|
|
110
|
+
*,
|
|
111
|
+
is_global_scope: bool,
|
|
112
|
+
) -> None:
|
|
105
113
|
if ins_cfg := config_data.get(schema.SECTION_INSTALLATION):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
iem = ins_cfg.get(schema.KEY_INSTALLATION_EXTERNALLY_MANAGED, None)
|
|
115
|
+
if iem is not None and not is_global_scope:
|
|
116
|
+
iem_cfg_key = f"{schema.SECTION_INSTALLATION}.{schema.KEY_INSTALLATION_EXTERNALLY_MANAGED}"
|
|
117
|
+
self.logger.W(
|
|
118
|
+
f"the config key [yellow]{iem_cfg_key}[/] cannot be set from user config; ignoring",
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
self.is_installation_externally_managed = bool(iem)
|
|
110
122
|
|
|
111
123
|
if pkgs_cfg := config_data.get(schema.SECTION_PACKAGES):
|
|
112
124
|
self.include_prereleases = pkgs_cfg.get(
|
|
@@ -137,61 +149,90 @@ class GlobalConfig:
|
|
|
137
149
|
if isinstance(consent, datetime.datetime):
|
|
138
150
|
self._telemetry_upload_consent = consent
|
|
139
151
|
|
|
140
|
-
def get_by_key(self, key: str | Sequence[str]) -> object
|
|
152
|
+
def get_by_key(self, key: str | Sequence[str]) -> object:
|
|
153
|
+
parsed_key = schema.parse_config_key(key)
|
|
154
|
+
section, sel = parsed_key[0], parsed_key[1:]
|
|
155
|
+
attr_name = self._get_attr_name_by_key(section, sel)
|
|
156
|
+
if attr_name is None:
|
|
157
|
+
raise errors.InvalidConfigKeyError(key)
|
|
158
|
+
return getattr(self, attr_name)
|
|
159
|
+
|
|
160
|
+
def set_by_key(self, key: str | Sequence[str], value: object) -> None:
|
|
161
|
+
# We don't have to check for global-only keys here because this
|
|
162
|
+
# method is only used for programmatic changes to the in-memory
|
|
163
|
+
# config, not for loading from config files.
|
|
141
164
|
parsed_key = schema.parse_config_key(key)
|
|
142
165
|
section, sel = parsed_key[0], parsed_key[1:]
|
|
166
|
+
attr_name = self._get_attr_name_by_key(section, sel)
|
|
167
|
+
if attr_name is None:
|
|
168
|
+
raise errors.InvalidConfigKeyError(key)
|
|
169
|
+
|
|
170
|
+
expected_type = schema.get_expected_type_for_config_key(key)
|
|
171
|
+
if not isinstance(value, expected_type):
|
|
172
|
+
raise TypeError(
|
|
173
|
+
f"expected type {expected_type.__name__} for config key '{key}', got {type(value).__name__}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
setattr(self, attr_name, value)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def _get_attr_name_by_key(cls, section: str, sel: list[str]) -> str | None:
|
|
143
180
|
if section == schema.SECTION_INSTALLATION:
|
|
144
|
-
return
|
|
181
|
+
return cls._get_section_installation(sel)
|
|
145
182
|
elif section == schema.SECTION_PACKAGES:
|
|
146
|
-
return
|
|
183
|
+
return cls._get_section_packages(sel)
|
|
147
184
|
elif section == schema.SECTION_REPO:
|
|
148
|
-
return
|
|
185
|
+
return cls._get_section_repo(sel)
|
|
149
186
|
elif section == schema.SECTION_TELEMETRY:
|
|
150
|
-
return
|
|
187
|
+
return cls._get_section_telemetry(sel)
|
|
151
188
|
else:
|
|
152
189
|
return None
|
|
153
190
|
|
|
154
|
-
|
|
191
|
+
@classmethod
|
|
192
|
+
def _get_section_installation(cls, selector: list[str]) -> str | None:
|
|
155
193
|
if len(selector) != 1:
|
|
156
194
|
return None
|
|
157
195
|
leaf = selector[0]
|
|
158
196
|
if leaf == schema.KEY_INSTALLATION_EXTERNALLY_MANAGED:
|
|
159
|
-
return
|
|
197
|
+
return "is_installation_externally_managed"
|
|
160
198
|
else:
|
|
161
199
|
return None
|
|
162
200
|
|
|
163
|
-
|
|
201
|
+
@classmethod
|
|
202
|
+
def _get_section_packages(cls, selector: list[str]) -> str | None:
|
|
164
203
|
if len(selector) != 1:
|
|
165
204
|
return None
|
|
166
205
|
leaf = selector[0]
|
|
167
206
|
if leaf == schema.KEY_PACKAGES_PRERELEASES:
|
|
168
|
-
return
|
|
207
|
+
return "include_prereleases"
|
|
169
208
|
else:
|
|
170
209
|
return None
|
|
171
210
|
|
|
172
|
-
|
|
211
|
+
@classmethod
|
|
212
|
+
def _get_section_repo(cls, selector: list[str]) -> str | None:
|
|
173
213
|
if len(selector) != 1:
|
|
174
214
|
return None
|
|
175
215
|
leaf = selector[0]
|
|
176
216
|
if leaf == schema.KEY_REPO_BRANCH:
|
|
177
|
-
return
|
|
217
|
+
return "override_repo_branch"
|
|
178
218
|
elif leaf == schema.KEY_REPO_LOCAL:
|
|
179
|
-
return
|
|
219
|
+
return "override_repo_dir"
|
|
180
220
|
elif leaf == schema.KEY_REPO_REMOTE:
|
|
181
|
-
return
|
|
221
|
+
return "override_repo_url"
|
|
182
222
|
else:
|
|
183
223
|
return None
|
|
184
224
|
|
|
185
|
-
|
|
225
|
+
@classmethod
|
|
226
|
+
def _get_section_telemetry(cls, selector: list[str]) -> str | None:
|
|
186
227
|
if len(selector) != 1:
|
|
187
228
|
return None
|
|
188
229
|
leaf = selector[0]
|
|
189
230
|
if leaf == schema.KEY_TELEMETRY_MODE:
|
|
190
|
-
return
|
|
231
|
+
return "telemetry_mode"
|
|
191
232
|
elif leaf == schema.KEY_TELEMETRY_PM_TELEMETRY_URL:
|
|
192
|
-
return
|
|
233
|
+
return "override_pm_telemetry_url"
|
|
193
234
|
elif leaf == schema.KEY_TELEMETRY_UPLOAD_CONSENT:
|
|
194
|
-
return
|
|
235
|
+
return "telemetry_upload_consent_time"
|
|
195
236
|
else:
|
|
196
237
|
return None
|
|
197
238
|
|
|
@@ -262,26 +303,63 @@ class GlobalConfig:
|
|
|
262
303
|
def telemetry_root(self) -> os.PathLike[Any]:
|
|
263
304
|
return pathlib.Path(self.ensure_state_dir()) / "telemetry"
|
|
264
305
|
|
|
265
|
-
@
|
|
306
|
+
@property
|
|
266
307
|
def telemetry(self) -> "TelemetryProvider | None":
|
|
308
|
+
return None if self.telemetry_mode == "off" else self._telemetry_provider
|
|
309
|
+
|
|
310
|
+
@cached_property
|
|
311
|
+
def _telemetry_provider(self) -> "TelemetryProvider | None":
|
|
312
|
+
"""Do not access directly; use the ``telemetry`` property instead."""
|
|
313
|
+
|
|
267
314
|
from ..telemetry.provider import TelemetryProvider
|
|
268
315
|
|
|
269
316
|
return None if self.telemetry_mode == "off" else TelemetryProvider(self)
|
|
270
317
|
|
|
271
318
|
@property
|
|
272
319
|
def telemetry_mode(self) -> str:
|
|
273
|
-
return self._telemetry_mode or
|
|
320
|
+
return self._telemetry_mode or DEFAULT_TELEMETRY_MODE
|
|
321
|
+
|
|
322
|
+
@telemetry_mode.setter
|
|
323
|
+
def telemetry_mode(self, mode: str) -> None:
|
|
324
|
+
if mode not in ("off", "local", "on"):
|
|
325
|
+
raise ValueError("telemetry mode must be one of: off, local, on")
|
|
326
|
+
if self._gm.is_telemetry_optout and mode != "off":
|
|
327
|
+
raise ValueError(
|
|
328
|
+
"cannot enable telemetry when the environment variable opt-out is set"
|
|
329
|
+
)
|
|
330
|
+
self._telemetry_mode = mode
|
|
274
331
|
|
|
275
332
|
@property
|
|
276
333
|
def telemetry_upload_consent_time(self) -> datetime.datetime | None:
|
|
277
334
|
return self._telemetry_upload_consent
|
|
278
335
|
|
|
336
|
+
@telemetry_upload_consent_time.setter
|
|
337
|
+
def telemetry_upload_consent_time(self, t: datetime.datetime | None) -> None:
|
|
338
|
+
self._telemetry_upload_consent = t
|
|
339
|
+
|
|
279
340
|
@property
|
|
280
341
|
def override_pm_telemetry_url(self) -> str | None:
|
|
281
342
|
return self._telemetry_pm_telemetry_url
|
|
282
343
|
|
|
344
|
+
@override_pm_telemetry_url.setter
|
|
345
|
+
def override_pm_telemetry_url(self, url: str | None) -> None:
|
|
346
|
+
self._telemetry_pm_telemetry_url = url
|
|
347
|
+
|
|
348
|
+
@cached_property
|
|
349
|
+
def default_repo_dir(self) -> str:
|
|
350
|
+
return os.path.join(self.cache_root, "packages-index")
|
|
351
|
+
|
|
283
352
|
def get_repo_dir(self) -> str:
|
|
284
|
-
return self.override_repo_dir or
|
|
353
|
+
return self.override_repo_dir or self.default_repo_dir
|
|
354
|
+
|
|
355
|
+
@cached_property
|
|
356
|
+
def have_overridden_repo_dir(self) -> bool:
|
|
357
|
+
if not self.override_repo_dir:
|
|
358
|
+
return False
|
|
359
|
+
override_path = pathlib.Path(self.override_repo_dir)
|
|
360
|
+
default_path = pathlib.Path(self.default_repo_dir)
|
|
361
|
+
# we don't use samefile() here because the path may not exist
|
|
362
|
+
return override_path.resolve() != default_path.resolve()
|
|
285
363
|
|
|
286
364
|
def get_repo_url(self) -> str:
|
|
287
365
|
return self.override_repo_url or DEFAULT_REPO_URL
|
|
@@ -312,7 +390,7 @@ class GlobalConfig:
|
|
|
312
390
|
def lookup_binary_install_dir(self, host: str, slug: str) -> PathLike[Any] | None:
|
|
313
391
|
host_path = get_host_path_fragment_for_binary_install_dir(host)
|
|
314
392
|
for data_dir in self._dirs.app_data_dirs:
|
|
315
|
-
p = data_dir / "binaries" / host_path / slug
|
|
393
|
+
p = data_dir.path / "binaries" / host_path / slug
|
|
316
394
|
if p.exists():
|
|
317
395
|
return p
|
|
318
396
|
return None
|
|
@@ -347,30 +425,40 @@ class GlobalConfig:
|
|
|
347
425
|
p.mkdir(parents=True, exist_ok=True)
|
|
348
426
|
return p
|
|
349
427
|
|
|
350
|
-
def iter_preset_configs(self) -> Iterable[
|
|
428
|
+
def iter_preset_configs(self) -> "Iterable[XDGPathEntry]":
|
|
351
429
|
"""
|
|
352
430
|
Yields possible Ruyi config files in all preset config path locations,
|
|
353
431
|
sorted by precedence from lowest to highest (so that each file may be
|
|
354
432
|
simply applied consecutively).
|
|
355
433
|
"""
|
|
356
434
|
|
|
435
|
+
from ..utils.xdg_basedir import XDGPathEntry
|
|
436
|
+
|
|
357
437
|
for path in PRESET_GLOBAL_CONFIG_LOCATIONS:
|
|
358
|
-
yield pathlib.Path(path)
|
|
438
|
+
yield XDGPathEntry(pathlib.Path(path), True)
|
|
359
439
|
|
|
360
|
-
def iter_xdg_configs(self) -> Iterable[
|
|
440
|
+
def iter_xdg_configs(self) -> "Iterable[XDGPathEntry]":
|
|
361
441
|
"""
|
|
362
442
|
Yields possible Ruyi config files in all XDG config paths, sorted by precedence
|
|
363
443
|
from lowest to highest (so that each file may be simply applied consecutively).
|
|
364
444
|
"""
|
|
365
445
|
|
|
366
|
-
|
|
367
|
-
|
|
446
|
+
from ..utils.xdg_basedir import XDGPathEntry
|
|
447
|
+
|
|
448
|
+
entries = list(self._dirs.app_config_dirs)
|
|
449
|
+
for e in reversed(entries):
|
|
450
|
+
yield XDGPathEntry(e.path / "config.toml", e.is_global)
|
|
368
451
|
|
|
369
452
|
@property
|
|
370
453
|
def local_user_config_file(self) -> pathlib.Path:
|
|
371
454
|
return self._dirs.app_config / "config.toml"
|
|
372
455
|
|
|
373
|
-
def
|
|
456
|
+
def _try_apply_config_file(
|
|
457
|
+
self,
|
|
458
|
+
path: os.PathLike[Any],
|
|
459
|
+
*,
|
|
460
|
+
is_global_scope: bool,
|
|
461
|
+
) -> None:
|
|
374
462
|
import tomlkit
|
|
375
463
|
|
|
376
464
|
try:
|
|
@@ -379,23 +467,24 @@ class GlobalConfig:
|
|
|
379
467
|
except FileNotFoundError:
|
|
380
468
|
return
|
|
381
469
|
|
|
382
|
-
self.logger.D(f"applying config: {data}")
|
|
383
|
-
self.
|
|
470
|
+
self.logger.D(f"applying config: {data}, is_global_scope={is_global_scope}")
|
|
471
|
+
self._apply_config(data, is_global_scope=is_global_scope)
|
|
384
472
|
|
|
385
473
|
@classmethod
|
|
386
474
|
def load_from_config(cls, gm: "ProvidesGlobalMode", logger: "RuyiLogger") -> "Self":
|
|
387
475
|
obj = cls(gm, logger)
|
|
388
476
|
|
|
389
|
-
for config_path in obj.iter_preset_configs():
|
|
477
|
+
for config_path, is_global in obj.iter_preset_configs():
|
|
390
478
|
obj.logger.D(f"trying config file from preset location: {config_path}")
|
|
391
|
-
obj.
|
|
479
|
+
obj._try_apply_config_file(config_path, is_global_scope=is_global)
|
|
392
480
|
|
|
393
|
-
for config_path in obj.iter_xdg_configs():
|
|
481
|
+
for config_path, is_global in obj.iter_xdg_configs():
|
|
394
482
|
obj.logger.D(f"trying config file from XDG path: {config_path}")
|
|
395
|
-
obj.
|
|
483
|
+
obj._try_apply_config_file(config_path, is_global_scope=is_global)
|
|
396
484
|
|
|
397
485
|
# let environment variable take precedence
|
|
398
486
|
if gm.is_telemetry_optout:
|
|
399
487
|
obj._telemetry_mode = "off"
|
|
488
|
+
obj._telemetry_upload_consent = None
|
|
400
489
|
|
|
401
490
|
return obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from contextlib import AbstractContextManager
|
|
2
2
|
import pathlib
|
|
3
|
-
from typing import Sequence, TYPE_CHECKING, cast
|
|
3
|
+
from typing import Final, Sequence, TYPE_CHECKING, cast
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from types import TracebackType
|
|
@@ -9,13 +9,35 @@ if TYPE_CHECKING:
|
|
|
9
9
|
import tomlkit
|
|
10
10
|
from tomlkit.items import Table
|
|
11
11
|
|
|
12
|
-
from .errors import MalformedConfigFileError
|
|
13
|
-
from .schema import
|
|
12
|
+
from .errors import MalformedConfigFileError, ProtectedGlobalConfigError
|
|
13
|
+
from .schema import (
|
|
14
|
+
SECTION_INSTALLATION,
|
|
15
|
+
KEY_INSTALLATION_EXTERNALLY_MANAGED,
|
|
16
|
+
ensure_valid_config_kv,
|
|
17
|
+
parse_config_key,
|
|
18
|
+
validate_section,
|
|
19
|
+
)
|
|
14
20
|
|
|
15
21
|
if TYPE_CHECKING:
|
|
16
22
|
from . import GlobalConfig
|
|
17
23
|
|
|
18
24
|
|
|
25
|
+
GLOBAL_ONLY_CONFIG_KEYS: Final[set[tuple[str, str]]] = {
|
|
26
|
+
(SECTION_INSTALLATION, KEY_INSTALLATION_EXTERNALLY_MANAGED),
|
|
27
|
+
}
|
|
28
|
+
"""Settings that can only be set in global-scope config files.
|
|
29
|
+
|
|
30
|
+
Changes should be reflected in ``GlobalConfig._apply_config`` too."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_config_key_global_only(key: str | Sequence[str]) -> bool:
|
|
34
|
+
parsed_key = parse_config_key(key)
|
|
35
|
+
if len(parsed_key) != 2:
|
|
36
|
+
return False
|
|
37
|
+
section, leaf = parsed_key
|
|
38
|
+
return (section, leaf) in GLOBAL_ONLY_CONFIG_KEYS
|
|
39
|
+
|
|
40
|
+
|
|
19
41
|
class ConfigEditor(AbstractContextManager["ConfigEditor"]):
|
|
20
42
|
def __init__(self, path: pathlib.Path) -> None:
|
|
21
43
|
self._path = path
|
|
@@ -63,6 +85,15 @@ class ConfigEditor(AbstractContextManager["ConfigEditor"]):
|
|
|
63
85
|
parsed_key = parse_config_key(key)
|
|
64
86
|
ensure_valid_config_kv(parsed_key, check_val=True, val=val)
|
|
65
87
|
|
|
88
|
+
# Gate protected settings: user-local config is not allowed to override
|
|
89
|
+
# global-only settings.
|
|
90
|
+
if _is_config_key_global_only(parsed_key):
|
|
91
|
+
# This mechanism is only meant for modifying user-local configs,
|
|
92
|
+
# because global configs are assumed to be maintained by packagers
|
|
93
|
+
# and/or sysadmins, who are expected to write the config file by
|
|
94
|
+
# hand.
|
|
95
|
+
raise ProtectedGlobalConfigError(parsed_key)
|
|
96
|
+
|
|
66
97
|
section, sel = parsed_key[0], parsed_key[1:]
|
|
67
98
|
if section in self._stage:
|
|
68
99
|
existing_section = self._stage[section]
|
|
@@ -74,3 +74,15 @@ class MalformedConfigFileError(Exception):
|
|
|
74
74
|
|
|
75
75
|
def __repr__(self) -> str:
|
|
76
76
|
return f"MalformedConfigFileError({self._path:!r})"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ProtectedGlobalConfigError(Exception):
|
|
80
|
+
def __init__(self, key: str | Sequence[str]) -> None:
|
|
81
|
+
super().__init__()
|
|
82
|
+
self._key = key
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
return f"attempt to modify protected global config key: {self._key}"
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
return f"ProtectedGlobalConfigError({self._key!r})"
|
|
@@ -145,6 +145,11 @@ def encode_value(v: object) -> str:
|
|
|
145
145
|
"""Encodes the given config value into a string representation suitable for
|
|
146
146
|
display or storage into TOML config files."""
|
|
147
147
|
|
|
148
|
+
if v is None:
|
|
149
|
+
from ..utils.toml import NoneValue
|
|
150
|
+
|
|
151
|
+
raise NoneValue()
|
|
152
|
+
|
|
148
153
|
if isinstance(v, bool):
|
|
149
154
|
return "true" if v else "false"
|
|
150
155
|
elif isinstance(v, int):
|
|
@@ -276,7 +276,7 @@ We are about to:
|
|
|
276
276
|
"""
|
|
277
277
|
Some flashing steps require the use of fastboot, in which case you should
|
|
278
278
|
ensure the target device is showing up in [yellow]fastboot devices[/] output.
|
|
279
|
-
Please confirm it yourself before
|
|
279
|
+
Please [bold red]confirm it yourself before continuing[/].
|
|
280
280
|
"""
|
|
281
281
|
)
|
|
282
282
|
if not user_input.ask_for_yesno_confirmation(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from functools import cached_property
|
|
2
2
|
import os
|
|
3
|
-
from typing import Final
|
|
3
|
+
from typing import Any, Final
|
|
4
4
|
|
|
5
5
|
from ..log import RuyiLogger
|
|
6
6
|
from .checksum import Checksummer
|
|
7
|
-
from .
|
|
7
|
+
from .fetcher import BaseFetcher
|
|
8
8
|
from .pkg_manifest import DistfileDecl
|
|
9
9
|
from .repo import MetadataRepo
|
|
10
10
|
from .unpack import do_unpack, do_unpack_or_symlink
|
|
@@ -187,7 +187,11 @@ class Distfile:
|
|
|
187
187
|
f"failed to fetch distfile: {self.dest} failed integrity checks"
|
|
188
188
|
)
|
|
189
189
|
|
|
190
|
-
def unpack(
|
|
190
|
+
def unpack(
|
|
191
|
+
self,
|
|
192
|
+
root: str | os.PathLike[Any] | None,
|
|
193
|
+
logger: RuyiLogger,
|
|
194
|
+
) -> None:
|
|
191
195
|
return do_unpack(
|
|
192
196
|
logger,
|
|
193
197
|
self.dest,
|
|
@@ -197,7 +201,11 @@ class Distfile:
|
|
|
197
201
|
prefixes_to_unpack=self.prefixes_to_unpack,
|
|
198
202
|
)
|
|
199
203
|
|
|
200
|
-
def unpack_or_symlink(
|
|
204
|
+
def unpack_or_symlink(
|
|
205
|
+
self,
|
|
206
|
+
root: str | os.PathLike[Any] | None,
|
|
207
|
+
logger: RuyiLogger,
|
|
208
|
+
) -> None:
|
|
201
209
|
return do_unpack_or_symlink(
|
|
202
210
|
logger,
|
|
203
211
|
self.dest,
|