ruyi 0.42.0a20251005__py3-none-any.whl → 0.42.0a20251013__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/cli/config_cli.py +20 -4
- ruyi/cli/main.py +12 -7
- ruyi/config/__init__.py +127 -38
- ruyi/config/editor.py +34 -3
- ruyi/config/errors.py +12 -0
- ruyi/config/schema.py +5 -0
- ruyi/device/provision.py +1 -1
- ruyi/ruyipkg/distfile.py +12 -4
- ruyi/ruyipkg/install.py +99 -32
- ruyi/ruyipkg/install_cli.py +29 -1
- ruyi/ruyipkg/repo.py +1 -1
- ruyi/ruyipkg/unpack.py +14 -16
- ruyi/telemetry/provider.py +65 -22
- ruyi/utils/git.py +4 -2
- ruyi/utils/toml.py +11 -0
- ruyi/utils/xdg_basedir.py +20 -13
- {ruyi-0.42.0a20251005.dist-info → ruyi-0.42.0a20251013.dist-info}/METADATA +17 -10
- {ruyi-0.42.0a20251005.dist-info → ruyi-0.42.0a20251013.dist-info}/RECORD +22 -22
- /ruyi/ruyipkg/{fetch.py → fetcher.py} +0 -0
- {ruyi-0.42.0a20251005.dist-info → ruyi-0.42.0a20251013.dist-info}/WHEEL +0 -0
- {ruyi-0.42.0a20251005.dist-info → ruyi-0.42.0a20251013.dist-info}/entry_points.txt +0 -0
- {ruyi-0.42.0a20251005.dist-info → ruyi-0.42.0a20251013.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/cli/config_cli.py
CHANGED
|
@@ -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
|
ruyi/cli/main.py
CHANGED
|
@@ -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):
|
ruyi/config/__init__.py
CHANGED
|
@@ -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
|
ruyi/config/editor.py
CHANGED
|
@@ -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]
|
ruyi/config/errors.py
CHANGED
|
@@ -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})"
|
ruyi/config/schema.py
CHANGED
|
@@ -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):
|
ruyi/device/provision.py
CHANGED
|
@@ -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(
|
ruyi/ruyipkg/distfile.py
CHANGED
|
@@ -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,
|
ruyi/ruyipkg/install.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import os
|
|
1
|
+
import os
|
|
2
2
|
import pathlib
|
|
3
3
|
import shutil
|
|
4
4
|
import tempfile
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from ruyi.ruyipkg.state import BoundInstallationStateStore
|
|
7
8
|
|
|
@@ -29,6 +30,9 @@ def do_extract_atoms(
|
|
|
29
30
|
atom_strs: set[str],
|
|
30
31
|
*,
|
|
31
32
|
canonicalized_host: str | RuyiHost,
|
|
33
|
+
dest_dir: os.PathLike[Any] | None, # None for CWD
|
|
34
|
+
extract_without_subdir: bool,
|
|
35
|
+
fetch_only: bool,
|
|
32
36
|
) -> int:
|
|
33
37
|
logger = cfg.logger
|
|
34
38
|
logger.D(f"about to extract for host {canonicalized_host}: {atom_strs}")
|
|
@@ -41,7 +45,6 @@ def do_extract_atoms(
|
|
|
41
45
|
if pm is None:
|
|
42
46
|
logger.F(f"atom {a_str} matches no package in the repository")
|
|
43
47
|
return 1
|
|
44
|
-
pkg_name = pm.name_for_installation
|
|
45
48
|
|
|
46
49
|
sv = pm.service_level
|
|
47
50
|
if sv.has_known_issues:
|
|
@@ -49,43 +52,92 @@ def do_extract_atoms(
|
|
|
49
52
|
for s in sv.render_known_issues(pm.repo.messages, cfg.lang_code):
|
|
50
53
|
logger.I(s)
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
ret = _do_extract_pkg(
|
|
56
|
+
cfg,
|
|
57
|
+
pm,
|
|
58
|
+
canonicalized_host=canonicalized_host,
|
|
59
|
+
fetch_only=fetch_only,
|
|
60
|
+
dest_dir=dest_dir,
|
|
61
|
+
extract_without_subdir=extract_without_subdir,
|
|
62
|
+
)
|
|
63
|
+
if ret != 0:
|
|
64
|
+
return ret
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
logger.F(
|
|
60
|
-
f"cannot handle package [green]{pkg_name}[/]: package is both binary and source"
|
|
61
|
-
)
|
|
62
|
-
return 2
|
|
66
|
+
return 0
|
|
63
67
|
|
|
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
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
def _do_extract_pkg(
|
|
70
|
+
cfg: GlobalConfig,
|
|
71
|
+
pm: BoundPackageManifest,
|
|
72
|
+
*,
|
|
73
|
+
canonicalized_host: str | RuyiHost,
|
|
74
|
+
dest_dir: os.PathLike[Any] | None, # None for CWD
|
|
75
|
+
extract_without_subdir: bool,
|
|
76
|
+
fetch_only: bool,
|
|
77
|
+
) -> int:
|
|
78
|
+
logger = cfg.logger
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
pkg_name = pm.name_for_installation
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
if not extract_without_subdir:
|
|
83
|
+
# extract into a subdirectory named <pkg_name>-<version>
|
|
84
|
+
subdir_name = pm.name_for_installation
|
|
85
|
+
if dest_dir is None:
|
|
86
|
+
dest_dir = pathlib.Path(subdir_name)
|
|
87
|
+
else:
|
|
88
|
+
dest_dir = pathlib.Path(dest_dir) / subdir_name
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
# unpack into CWD
|
|
86
|
-
df.unpack(None, logger)
|
|
90
|
+
logger.D(f"about to extract {pm} to {dest_dir}")
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
# Make sure destination directory exists
|
|
93
|
+
if dest_dir is not None:
|
|
94
|
+
dest_dir = pathlib.Path(dest_dir)
|
|
95
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
bm = pm.binary_metadata
|
|
98
|
+
sm = pm.source_metadata
|
|
99
|
+
if bm is None and sm is None:
|
|
100
|
+
logger.F(f"don't know how to extract package [green]{pkg_name}[/]")
|
|
101
|
+
return 2
|
|
102
|
+
|
|
103
|
+
if bm is not None and sm is not None:
|
|
104
|
+
logger.F(
|
|
105
|
+
f"cannot handle package [green]{pkg_name}[/]: package is both binary and source"
|
|
106
|
+
)
|
|
107
|
+
return 2
|
|
108
|
+
|
|
109
|
+
distfiles_for_host: list[str] | None = None
|
|
110
|
+
if bm is not None:
|
|
111
|
+
distfiles_for_host = bm.get_distfile_names_for_host(canonicalized_host)
|
|
112
|
+
elif sm is not None:
|
|
113
|
+
distfiles_for_host = sm.get_distfile_names_for_host(canonicalized_host)
|
|
114
|
+
|
|
115
|
+
if not distfiles_for_host:
|
|
116
|
+
logger.F(
|
|
117
|
+
f"package [green]{pkg_name}[/] declares no distfile for host {canonicalized_host}"
|
|
118
|
+
)
|
|
119
|
+
return 2
|
|
120
|
+
|
|
121
|
+
dfs = pm.distfiles
|
|
122
|
+
|
|
123
|
+
for df_name in distfiles_for_host:
|
|
124
|
+
df_decl = dfs[df_name]
|
|
125
|
+
ensure_unpack_cmd_for_method(logger, df_decl.unpack_method)
|
|
126
|
+
df = Distfile(df_decl, pm.repo)
|
|
127
|
+
df.ensure(logger)
|
|
128
|
+
|
|
129
|
+
if fetch_only:
|
|
130
|
+
logger.D("skipping extraction because [yellow]--fetch-only[/] is given")
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
logger.I(f"extracting [green]{df_name}[/] for package [green]{pkg_name}[/]")
|
|
134
|
+
# unpack into destination
|
|
135
|
+
df.unpack(dest_dir, logger)
|
|
136
|
+
|
|
137
|
+
if not fetch_only:
|
|
138
|
+
logger.I(
|
|
139
|
+
f"package [green]{pkg_name}[/] has been extracted to {dest_dir}",
|
|
140
|
+
)
|
|
89
141
|
|
|
90
142
|
return 0
|
|
91
143
|
|
|
@@ -147,6 +199,21 @@ def do_install_atoms(
|
|
|
147
199
|
return ret
|
|
148
200
|
continue
|
|
149
201
|
|
|
202
|
+
# the user may be trying to fetch a source-only package with `ruyi install --fetch-only`,
|
|
203
|
+
# so try that too for better UX
|
|
204
|
+
if fetch_only and pm.source_metadata is not None:
|
|
205
|
+
ret = _do_extract_pkg(
|
|
206
|
+
config,
|
|
207
|
+
pm,
|
|
208
|
+
canonicalized_host=canonicalized_host,
|
|
209
|
+
dest_dir=None, # unused in this case
|
|
210
|
+
extract_without_subdir=False, # unused in this case
|
|
211
|
+
fetch_only=fetch_only,
|
|
212
|
+
)
|
|
213
|
+
if ret != 0:
|
|
214
|
+
return ret
|
|
215
|
+
continue
|
|
216
|
+
|
|
150
217
|
logger.F(f"don't know how to handle non-binary package [green]{pkg_name}[/]")
|
|
151
218
|
return 2
|
|
152
219
|
|
ruyi/ruyipkg/install_cli.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import pathlib
|
|
2
3
|
from typing import TYPE_CHECKING
|
|
3
4
|
|
|
4
5
|
from ..cli.cmd import RootCommand
|
|
@@ -26,6 +27,25 @@ class ExtractCommand(
|
|
|
26
27
|
if gc.is_cli_autocomplete:
|
|
27
28
|
a.completer = package_completer_builder(gc)
|
|
28
29
|
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"-d",
|
|
32
|
+
"--dest-dir",
|
|
33
|
+
type=str,
|
|
34
|
+
metavar="DESTDIR",
|
|
35
|
+
default=".",
|
|
36
|
+
help="Destination directory to extract to (default: current directory)",
|
|
37
|
+
)
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"--extract-without-subdir",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Extract files directly into DESTDIR instead of package-named subdirectories",
|
|
42
|
+
)
|
|
43
|
+
p.add_argument(
|
|
44
|
+
"-f",
|
|
45
|
+
"--fetch-only",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Fetch distribution files only without installing",
|
|
48
|
+
)
|
|
29
49
|
p.add_argument(
|
|
30
50
|
"--host",
|
|
31
51
|
type=str,
|
|
@@ -38,14 +58,22 @@ class ExtractCommand(
|
|
|
38
58
|
from .host import canonicalize_host_str
|
|
39
59
|
from .install import do_extract_atoms
|
|
40
60
|
|
|
41
|
-
host: str = args.host
|
|
42
61
|
atom_strs: set[str] = set(args.atom)
|
|
62
|
+
dest_dir_arg: str = args.dest_dir
|
|
63
|
+
extract_without_subdir: bool = args.extract_without_subdir
|
|
64
|
+
host: str = args.host
|
|
65
|
+
fetch_only: bool = args.fetch_only
|
|
66
|
+
|
|
67
|
+
dest_dir = None if dest_dir_arg == "." else pathlib.Path(dest_dir_arg)
|
|
43
68
|
|
|
44
69
|
return do_extract_atoms(
|
|
45
70
|
cfg,
|
|
46
71
|
cfg.repo,
|
|
47
72
|
atom_strs,
|
|
48
73
|
canonicalized_host=canonicalize_host_str(host),
|
|
74
|
+
dest_dir=dest_dir,
|
|
75
|
+
extract_without_subdir=extract_without_subdir,
|
|
76
|
+
fetch_only=fetch_only,
|
|
49
77
|
)
|
|
50
78
|
|
|
51
79
|
|
ruyi/ruyipkg/repo.py
CHANGED
|
@@ -315,7 +315,7 @@ class MetadataRepo(ProvidesPackageManifests):
|
|
|
315
315
|
|
|
316
316
|
# only manage the repo settings on the user's behalf if the user
|
|
317
317
|
# has not overridden the repo directory themselves
|
|
318
|
-
allow_auto_management = self._gc.
|
|
318
|
+
allow_auto_management = not self._gc.have_overridden_repo_dir
|
|
319
319
|
|
|
320
320
|
pull_ff_or_die(
|
|
321
321
|
self.logger,
|
ruyi/ruyipkg/unpack.py
CHANGED
|
@@ -2,7 +2,7 @@ import mmap
|
|
|
2
2
|
import os
|
|
3
3
|
import shutil
|
|
4
4
|
import subprocess
|
|
5
|
-
from typing import BinaryIO, NoReturn, Protocol
|
|
5
|
+
from typing import Any, BinaryIO, NoReturn, Protocol
|
|
6
6
|
|
|
7
7
|
from ..log import RuyiLogger
|
|
8
8
|
from ..utils import ar, prereqs
|
|
@@ -20,7 +20,7 @@ class SupportsRead(Protocol):
|
|
|
20
20
|
def do_unpack(
|
|
21
21
|
logger: RuyiLogger,
|
|
22
22
|
filename: str,
|
|
23
|
-
dest: str | None,
|
|
23
|
+
dest: str | os.PathLike[Any] | None,
|
|
24
24
|
strip_components: int,
|
|
25
25
|
unpack_method: UnpackMethod,
|
|
26
26
|
stream: BinaryIO | SupportsRead | None = None,
|
|
@@ -77,7 +77,7 @@ def do_unpack(
|
|
|
77
77
|
def do_unpack_or_symlink(
|
|
78
78
|
logger: RuyiLogger,
|
|
79
79
|
filename: str,
|
|
80
|
-
dest: str | None,
|
|
80
|
+
dest: str | os.PathLike[Any] | None,
|
|
81
81
|
strip_components: int,
|
|
82
82
|
unpack_method: UnpackMethod,
|
|
83
83
|
stream: BinaryIO | SupportsRead | None = None,
|
|
@@ -100,7 +100,7 @@ def do_unpack_or_symlink(
|
|
|
100
100
|
|
|
101
101
|
def _do_copy_raw(
|
|
102
102
|
src_path: str,
|
|
103
|
-
destdir: str | None,
|
|
103
|
+
destdir: str | os.PathLike[Any] | None,
|
|
104
104
|
) -> None:
|
|
105
105
|
src_filename = os.path.basename(src_path)
|
|
106
106
|
if destdir is None:
|
|
@@ -114,7 +114,7 @@ def _do_copy_raw(
|
|
|
114
114
|
|
|
115
115
|
def do_symlink(
|
|
116
116
|
src_path: str,
|
|
117
|
-
destdir: str | None,
|
|
117
|
+
destdir: str | os.PathLike[Any] | None,
|
|
118
118
|
) -> None:
|
|
119
119
|
src_filename = os.path.basename(src_path)
|
|
120
120
|
if destdir is None:
|
|
@@ -132,7 +132,7 @@ def do_symlink(
|
|
|
132
132
|
def _do_unpack_tar(
|
|
133
133
|
logger: RuyiLogger,
|
|
134
134
|
filename: str,
|
|
135
|
-
dest: str | None,
|
|
135
|
+
dest: str | os.PathLike[Any] | None,
|
|
136
136
|
strip_components: int,
|
|
137
137
|
unpack_method: UnpackMethod,
|
|
138
138
|
stream: SupportsRead | None,
|
|
@@ -164,8 +164,6 @@ def _do_unpack_tar(
|
|
|
164
164
|
stdin = subprocess.PIPE
|
|
165
165
|
|
|
166
166
|
argv.extend(("-f", filename, f"--strip-components={strip_components}"))
|
|
167
|
-
if dest is not None:
|
|
168
|
-
argv.extend(("-C", dest))
|
|
169
167
|
if prefixes_to_unpack:
|
|
170
168
|
if any(p.startswith("-") for p in prefixes_to_unpack):
|
|
171
169
|
raise ValueError(
|
|
@@ -200,11 +198,11 @@ def _do_unpack_tar(
|
|
|
200
198
|
def _do_unpack_zip(
|
|
201
199
|
logger: RuyiLogger,
|
|
202
200
|
filename: str,
|
|
203
|
-
dest: str | None,
|
|
201
|
+
dest: str | os.PathLike[Any] | None,
|
|
204
202
|
) -> None:
|
|
205
203
|
argv = ["unzip", filename]
|
|
206
204
|
if dest is not None:
|
|
207
|
-
argv.extend(("-d", dest))
|
|
205
|
+
argv.extend(("-d", str(dest)))
|
|
208
206
|
logger.D(f"about to call unzip: argv={argv}")
|
|
209
207
|
retcode = subprocess.call(argv, cwd=dest)
|
|
210
208
|
if retcode != 0:
|
|
@@ -214,7 +212,7 @@ def _do_unpack_zip(
|
|
|
214
212
|
def _do_unpack_bare_gz(
|
|
215
213
|
logger: RuyiLogger,
|
|
216
214
|
filename: str,
|
|
217
|
-
destdir: str | None,
|
|
215
|
+
destdir: str | os.PathLike[Any] | None,
|
|
218
216
|
) -> None:
|
|
219
217
|
# the suffix may not be ".gz" so do this generically
|
|
220
218
|
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
@@ -235,7 +233,7 @@ def _do_unpack_bare_gz(
|
|
|
235
233
|
def _do_unpack_bare_bzip2(
|
|
236
234
|
logger: RuyiLogger,
|
|
237
235
|
filename: str,
|
|
238
|
-
destdir: str | None,
|
|
236
|
+
destdir: str | os.PathLike[Any] | None,
|
|
239
237
|
) -> None:
|
|
240
238
|
# the suffix may not be ".bz2" so do this generically
|
|
241
239
|
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
@@ -256,7 +254,7 @@ def _do_unpack_bare_bzip2(
|
|
|
256
254
|
def _do_unpack_bare_lz4(
|
|
257
255
|
logger: RuyiLogger,
|
|
258
256
|
filename: str,
|
|
259
|
-
destdir: str | None,
|
|
257
|
+
destdir: str | os.PathLike[Any] | None,
|
|
260
258
|
) -> None:
|
|
261
259
|
# the suffix may not be ".lz4" so do this generically
|
|
262
260
|
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
@@ -271,7 +269,7 @@ def _do_unpack_bare_lz4(
|
|
|
271
269
|
def _do_unpack_bare_xz(
|
|
272
270
|
logger: RuyiLogger,
|
|
273
271
|
filename: str,
|
|
274
|
-
destdir: str | None,
|
|
272
|
+
destdir: str | os.PathLike[Any] | None,
|
|
275
273
|
) -> None:
|
|
276
274
|
# the suffix may not be ".xz" so do this generically
|
|
277
275
|
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
@@ -292,7 +290,7 @@ def _do_unpack_bare_xz(
|
|
|
292
290
|
def _do_unpack_bare_zstd(
|
|
293
291
|
logger: RuyiLogger,
|
|
294
292
|
filename: str,
|
|
295
|
-
destdir: str | None,
|
|
293
|
+
destdir: str | os.PathLike[Any] | None,
|
|
296
294
|
) -> None:
|
|
297
295
|
# the suffix may not be ".zst" so do this generically
|
|
298
296
|
dest_filename = os.path.splitext(os.path.basename(filename))[0]
|
|
@@ -307,7 +305,7 @@ def _do_unpack_bare_zstd(
|
|
|
307
305
|
def _do_unpack_deb(
|
|
308
306
|
logger: RuyiLogger,
|
|
309
307
|
filename: str,
|
|
310
|
-
destdir: str | None,
|
|
308
|
+
destdir: str | os.PathLike[Any] | None,
|
|
311
309
|
) -> None:
|
|
312
310
|
with ar.ArpyArchiveWrapper(filename) as a:
|
|
313
311
|
for f in a.infolist():
|
ruyi/telemetry/provider.py
CHANGED
|
@@ -18,19 +18,23 @@ if TYPE_CHECKING:
|
|
|
18
18
|
|
|
19
19
|
FALLBACK_PM_TELEMETRY_ENDPOINT = "https://api.ruyisdk.cn/telemetry/pm/"
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
TELEMETRY_CONSENT_AND_UPLOAD_DESC = """
|
|
22
|
+
RuyiSDK collects anonymized usage data locally to help us improve the product.
|
|
23
|
+
|
|
24
|
+
[green]By default, nothing leaves your machine[/], and you can also turn off usage data
|
|
25
|
+
collection completely. Only with your explicit permission can [yellow]ruyi[/] upload
|
|
26
|
+
collected telemetry, periodically to RuyiSDK team-managed servers located in
|
|
27
|
+
the Chinese mainland. You can change this setting at any time by running
|
|
28
|
+
[yellow]ruyi telemetry consent[/], [yellow]ruyi telemetry local[/], or [yellow]ruyi telemetry optout[/].
|
|
29
|
+
|
|
30
|
+
If you enable uploads now, we'll also send a one-time report from this [yellow]ruyi[/]
|
|
31
|
+
installation so the RuyiSDK team can better understand adoption. Thank you for
|
|
32
|
+
helping us build a better experience!
|
|
33
33
|
"""
|
|
34
|
+
TELEMETRY_CONSENT_AND_UPLOAD_PROMPT = (
|
|
35
|
+
"Enable telemetry uploads and send a one-time report now?"
|
|
36
|
+
)
|
|
37
|
+
TELEMETRY_OPTOUT_PROMPT = "\nDo you want to disable telemetry entirely?"
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
def next_utc_weekday(wday: int, now: float | None = None) -> int:
|
|
@@ -66,12 +70,17 @@ def set_telemetry_mode(
|
|
|
66
70
|
|
|
67
71
|
logger = gc.logger
|
|
68
72
|
|
|
73
|
+
if mode == "on":
|
|
74
|
+
if consent_time is None:
|
|
75
|
+
consent_time = datetime.datetime.now().astimezone()
|
|
76
|
+
else:
|
|
77
|
+
# clear any previously recorded consent time
|
|
78
|
+
consent_time = None
|
|
79
|
+
|
|
80
|
+
# First, persist the changes to user config
|
|
69
81
|
with ConfigEditor.work_on_user_local_config(gc) as ed:
|
|
70
82
|
ed.set_value((schema.SECTION_TELEMETRY, schema.KEY_TELEMETRY_MODE), mode)
|
|
71
|
-
|
|
72
|
-
if mode == "on":
|
|
73
|
-
if consent_time is None:
|
|
74
|
-
consent_time = datetime.datetime.now().astimezone()
|
|
83
|
+
if consent_time is not None:
|
|
75
84
|
ed.set_value(
|
|
76
85
|
(schema.SECTION_TELEMETRY, schema.KEY_TELEMETRY_UPLOAD_CONSENT),
|
|
77
86
|
consent_time,
|
|
@@ -83,6 +92,18 @@ def set_telemetry_mode(
|
|
|
83
92
|
|
|
84
93
|
ed.stage()
|
|
85
94
|
|
|
95
|
+
# Then, apply the changes to the running instance's GlobalConfig
|
|
96
|
+
# TelemetryProvider instance (if any) will pick them up automatically
|
|
97
|
+
# because the properties are backed by GlobalConfig.
|
|
98
|
+
gc.set_by_key(
|
|
99
|
+
(schema.SECTION_TELEMETRY, schema.KEY_TELEMETRY_MODE),
|
|
100
|
+
mode,
|
|
101
|
+
)
|
|
102
|
+
gc.set_by_key(
|
|
103
|
+
(schema.SECTION_TELEMETRY, schema.KEY_TELEMETRY_UPLOAD_CONSENT),
|
|
104
|
+
consent_time,
|
|
105
|
+
)
|
|
106
|
+
|
|
86
107
|
if not show_cli_feedback:
|
|
87
108
|
return
|
|
88
109
|
match mode:
|
|
@@ -111,8 +132,6 @@ def set_telemetry_mode(
|
|
|
111
132
|
class TelemetryProvider:
|
|
112
133
|
def __init__(self, gc: "GlobalConfig") -> None:
|
|
113
134
|
self.state_root = pathlib.Path(gc.telemetry_root)
|
|
114
|
-
self.local_mode = gc.telemetry_mode == "local"
|
|
115
|
-
self.upload_consent_time = gc.telemetry_upload_consent_time
|
|
116
135
|
|
|
117
136
|
self._discard_events = False
|
|
118
137
|
self._gc = gc
|
|
@@ -129,6 +148,14 @@ class TelemetryProvider:
|
|
|
129
148
|
def logger(self) -> RuyiLogger:
|
|
130
149
|
return self._gc.logger
|
|
131
150
|
|
|
151
|
+
@property
|
|
152
|
+
def local_mode(self) -> bool:
|
|
153
|
+
return self._gc.telemetry_mode == "local"
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def upload_consent_time(self) -> datetime.datetime | None:
|
|
157
|
+
return self._gc.telemetry_upload_consent_time
|
|
158
|
+
|
|
132
159
|
def store(self, scope: TelemetryScope) -> TelemetryStore | None:
|
|
133
160
|
return self._stores.get(scope)
|
|
134
161
|
|
|
@@ -386,16 +413,32 @@ class TelemetryProvider:
|
|
|
386
413
|
return store.upload_staged_payloads()
|
|
387
414
|
|
|
388
415
|
def oobe_prompt(self) -> None:
|
|
389
|
-
"""Ask whether the user consents to a first-run telemetry upload
|
|
416
|
+
"""Ask whether the user consents to a first-run telemetry upload, and
|
|
417
|
+
persist the user's exact telemetry choice."""
|
|
390
418
|
|
|
391
419
|
from ..cli import user_input
|
|
392
420
|
|
|
393
|
-
self.logger.stdout(
|
|
421
|
+
self.logger.stdout(TELEMETRY_CONSENT_AND_UPLOAD_DESC)
|
|
394
422
|
if not user_input.ask_for_yesno_confirmation(
|
|
395
423
|
self.logger,
|
|
396
|
-
|
|
397
|
-
|
|
424
|
+
TELEMETRY_CONSENT_AND_UPLOAD_PROMPT,
|
|
425
|
+
False,
|
|
398
426
|
):
|
|
427
|
+
# ask if the user wants to opt out entirely
|
|
428
|
+
if user_input.ask_for_yesno_confirmation(
|
|
429
|
+
self.logger,
|
|
430
|
+
TELEMETRY_OPTOUT_PROMPT,
|
|
431
|
+
False,
|
|
432
|
+
):
|
|
433
|
+
set_telemetry_mode(self._gc, "off")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
# user wants to stay in local mode
|
|
437
|
+
# explicitly record the preference, so we don't have to worry about
|
|
438
|
+
# us potentially changing defaults yet another time
|
|
439
|
+
set_telemetry_mode(self._gc, "local")
|
|
399
440
|
return
|
|
400
441
|
|
|
442
|
+
consent_time = datetime.datetime.now().astimezone()
|
|
443
|
+
set_telemetry_mode(self._gc, "on", consent_time)
|
|
401
444
|
self._upload_on_exit = True
|
ruyi/utils/git.py
CHANGED
|
@@ -119,13 +119,15 @@ def pull_ff_or_die(
|
|
|
119
119
|
logger.I(f"repository: [yellow]{repo_path}[/]")
|
|
120
120
|
logger.I(f"expected remote URL: [yellow]{remote_url}[/]")
|
|
121
121
|
logger.I(f"actual remote URL: [yellow]{remote.url}[/]")
|
|
122
|
-
logger.I("please fix the repo settings manually")
|
|
122
|
+
logger.I("please [bold red]fix the repo settings manually[/]")
|
|
123
123
|
raise SystemExit(1)
|
|
124
124
|
|
|
125
125
|
logger.D(
|
|
126
126
|
f"updating url of remote {remote_name} from {remote.url} to {remote_url}"
|
|
127
127
|
)
|
|
128
|
-
repo.remotes.set_url(
|
|
128
|
+
repo.remotes.set_url(remote_name, remote_url)
|
|
129
|
+
# this needs manual refreshing
|
|
130
|
+
remote = repo.remotes[remote_name]
|
|
129
131
|
|
|
130
132
|
logger.D("fetching")
|
|
131
133
|
try:
|
ruyi/utils/toml.py
CHANGED
|
@@ -7,6 +7,17 @@ from tomlkit.container import Container
|
|
|
7
7
|
from tomlkit.items import Array, Comment, InlineTable, Item, Table, Trivia, Whitespace
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class NoneValue(Exception):
|
|
11
|
+
"""Used to indicate that a None value is to be dumped in TOML. Because TOML
|
|
12
|
+
does not support None natively, this means special handling is needed."""
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
return "NoneValue()"
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return "NoneValue()"
|
|
19
|
+
|
|
20
|
+
|
|
10
21
|
def with_indent(item: Item, spaces: int = 2) -> Item:
|
|
11
22
|
item.indent(spaces)
|
|
12
23
|
return item
|
ruyi/utils/xdg_basedir.py
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import pathlib
|
|
7
|
-
from typing import Iterable
|
|
7
|
+
from typing import Iterable, NamedTuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class XDGPathEntry(NamedTuple):
|
|
11
|
+
path: pathlib.Path
|
|
12
|
+
is_global: bool
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
def _paths_from_env(env: str, default: str) -> Iterable[pathlib.Path]:
|
|
@@ -38,14 +43,16 @@ class XDGBaseDir:
|
|
|
38
43
|
return pathlib.Path(v) if v else pathlib.Path.home() / ".local" / "state"
|
|
39
44
|
|
|
40
45
|
@property
|
|
41
|
-
def config_dirs(self) -> Iterable[
|
|
46
|
+
def config_dirs(self) -> Iterable[XDGPathEntry]:
|
|
42
47
|
# from highest precedence to lowest
|
|
43
|
-
|
|
48
|
+
for p in _paths_from_env("XDG_CONFIG_DIRS", "/etc/xdg"):
|
|
49
|
+
yield XDGPathEntry(p, True)
|
|
44
50
|
|
|
45
51
|
@property
|
|
46
|
-
def data_dirs(self) -> Iterable[
|
|
52
|
+
def data_dirs(self) -> Iterable[XDGPathEntry]:
|
|
47
53
|
# from highest precedence to lowest
|
|
48
|
-
|
|
54
|
+
for p in _paths_from_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/"):
|
|
55
|
+
yield XDGPathEntry(p, True)
|
|
49
56
|
|
|
50
57
|
# derived info
|
|
51
58
|
|
|
@@ -66,15 +73,15 @@ class XDGBaseDir:
|
|
|
66
73
|
return self.state_home / self.app_name
|
|
67
74
|
|
|
68
75
|
@property
|
|
69
|
-
def app_config_dirs(self) -> Iterable[
|
|
76
|
+
def app_config_dirs(self) -> Iterable[XDGPathEntry]:
|
|
70
77
|
# from highest precedence to lowest
|
|
71
|
-
yield self.app_config
|
|
72
|
-
for
|
|
73
|
-
yield
|
|
78
|
+
yield XDGPathEntry(self.app_config, False)
|
|
79
|
+
for e in self.config_dirs:
|
|
80
|
+
yield XDGPathEntry(e.path / self.app_name, e.is_global)
|
|
74
81
|
|
|
75
82
|
@property
|
|
76
|
-
def app_data_dirs(self) -> Iterable[
|
|
83
|
+
def app_data_dirs(self) -> Iterable[XDGPathEntry]:
|
|
77
84
|
# from highest precedence to lowest
|
|
78
|
-
yield self.app_data
|
|
79
|
-
for
|
|
80
|
-
yield
|
|
85
|
+
yield XDGPathEntry(self.app_data, False)
|
|
86
|
+
for e in self.data_dirs:
|
|
87
|
+
yield XDGPathEntry(e.path / self.app_name, e.is_global)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruyi
|
|
3
|
-
Version: 0.42.
|
|
3
|
+
Version: 0.42.0a20251013
|
|
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
|
|
@@ -5,19 +5,19 @@ ruyi/cli/builtin_commands.py,sha256=cYyPSF00DSBH1WMv6mHcMygbFRBGXObMWhbXHs5K1Mc,
|
|
|
5
5
|
ruyi/cli/cmd.py,sha256=kR3aEiDE3AfPoP0Zr7MO-09CKoExbkLLmPvve9oKaUg,6725
|
|
6
6
|
ruyi/cli/completer.py,sha256=cnOkU7veDe-jP8ROXZL2uBop2HgWfaAZl6dromnPLx8,1426
|
|
7
7
|
ruyi/cli/completion.py,sha256=ffLs3Dv7pY_uinwH98wkBPohRvAjpUOGqy01OTA_Bgo,841
|
|
8
|
-
ruyi/cli/config_cli.py,sha256=
|
|
9
|
-
ruyi/cli/main.py,sha256=
|
|
8
|
+
ruyi/cli/config_cli.py,sha256=9kq5W3Ir_lfwImhvrUmQ1KTKy1aRCv_UU1CmL5eGyJs,4038
|
|
9
|
+
ruyi/cli/main.py,sha256=X1xyD7mYcCdQAWxGU7F27XPI77LzG0oifJx_el_UOBs,4124
|
|
10
10
|
ruyi/cli/oobe.py,sha256=fpzNukwpT16CSqAobClC615Fii7GFDYc5g5ArZMur1A,2463
|
|
11
11
|
ruyi/cli/self_cli.py,sha256=opDHVpvgg0AgZdzi7kEY73_jwV6LRjtIpdgsFkRL1hI,8668
|
|
12
12
|
ruyi/cli/user_input.py,sha256=ZJPyCAD7Aizkt37f_KDtW683aKvGZ2pL85Rg_y1LLfg,3711
|
|
13
13
|
ruyi/cli/version_cli.py,sha256=L2pejZ7LIPYLUTb9NIz4t51KB8ai8igPBuE64tyqUuI,1275
|
|
14
|
-
ruyi/config/__init__.py,sha256=
|
|
15
|
-
ruyi/config/editor.py,sha256=
|
|
16
|
-
ruyi/config/errors.py,sha256=
|
|
14
|
+
ruyi/config/__init__.py,sha256=IeRfS51U4f-d---jKVNrBZshJrlrK06u25LssMgd8sI,17075
|
|
15
|
+
ruyi/config/editor.py,sha256=piAJ-a68iX6IFWXy1g9OXoSs_3DardZwoaSPdStKKL0,4243
|
|
16
|
+
ruyi/config/errors.py,sha256=pSu9bzVmQLh5ov36i1T6SkZOYwdus1v2DoMGWV7caKY,2518
|
|
17
17
|
ruyi/config/news.py,sha256=83LjQjJHsqOPdRrytG7VBFubG6pyDwJ-Mg37gpBRU20,1061
|
|
18
|
-
ruyi/config/schema.py,sha256=
|
|
18
|
+
ruyi/config/schema.py,sha256=btw3dwuTfNRIEVWa9JwIqrdfnxhbQ72uX_PV5Q7kCvM,5951
|
|
19
19
|
ruyi/device/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
ruyi/device/provision.py,sha256=
|
|
20
|
+
ruyi/device/provision.py,sha256=9rUbLtybBQ8x9qRXaMwz_yAMf51vb3wmYE0qm54P5Bk,20906
|
|
21
21
|
ruyi/device/provision_cli.py,sha256=sc6AF8ohWrXA-kIAYdZcD6sl1HHbj_dH2cCi-pjjOQg,1031
|
|
22
22
|
ruyi/log/__init__.py,sha256=ehgUl8iY1oRfP_nJKrln5lnvJFSPPU1J-3g-PK28YWk,6516
|
|
23
23
|
ruyi/mux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -45,14 +45,14 @@ ruyi/ruyipkg/augmented_pkg.py,sha256=QbzYNHCRanFx9xwYRosN_dAEzOSxmOBxyBVli8SC9FE
|
|
|
45
45
|
ruyi/ruyipkg/canonical_dump.py,sha256=Qu25YXwJpjWUBte0C3bmmaJxe7zyqfN-2-__u2_dJDM,9363
|
|
46
46
|
ruyi/ruyipkg/checksum.py,sha256=ChYFPXl7-Y8p_bDerkXOGroRz4k6ejjWY9ViaVWuEgk,1274
|
|
47
47
|
ruyi/ruyipkg/cli_completion.py,sha256=kJf7vN5rXi5zAwTI3dr4J8HdUzR3-ZXnsdaNQV6kqzo,1151
|
|
48
|
-
ruyi/ruyipkg/distfile.py,sha256=
|
|
48
|
+
ruyi/ruyipkg/distfile.py,sha256=wkyYVcSM2b3vD462TtoltdLcUZElCwWvcZBiPG3lTZ0,7002
|
|
49
49
|
ruyi/ruyipkg/entity.py,sha256=s8h5kNaR2vsP6v_QUFIO6SZiUrt0jrb8GU11bNT6fn4,14498
|
|
50
50
|
ruyi/ruyipkg/entity_cli.py,sha256=hF66i3sJX8XVLIWq376GA9vzgegfQy-6s0x0L831Irk,3802
|
|
51
51
|
ruyi/ruyipkg/entity_provider.py,sha256=jDfS2Jh01PVHo0kb1XyI5WkZgL4fv21AooXLLwqK-1I,8741
|
|
52
|
-
ruyi/ruyipkg/
|
|
52
|
+
ruyi/ruyipkg/fetcher.py,sha256=_btz2hkTz0uEUCCSAhOK7tlhAuvzCu42ZTUCP-RpU7U,9047
|
|
53
53
|
ruyi/ruyipkg/host.py,sha256=pmqgggi7koDCWgzFexwHpycv4SZ07VF6xUbi4s8FSKA,1399
|
|
54
|
-
ruyi/ruyipkg/install.py,sha256=
|
|
55
|
-
ruyi/ruyipkg/install_cli.py,sha256=
|
|
54
|
+
ruyi/ruyipkg/install.py,sha256=RhJwIqGxMveWrGmpP-2uWpZBRnqkA75HwyHwrNTdqow,18176
|
|
55
|
+
ruyi/ruyipkg/install_cli.py,sha256=joIV5iY4iblDinoH2pgbHNYkMVWqMxtVNj89T0IpJuk,5267
|
|
56
56
|
ruyi/ruyipkg/list.py,sha256=iO7666xFEWKTDNtJqVLaaQKsp0BfLTzgXBwFFHrqCGc,4172
|
|
57
57
|
ruyi/ruyipkg/list_cli.py,sha256=9tRWWRcJv3yJqzup6Oc89E3xahGp5jIfybfEvwwd55I,2243
|
|
58
58
|
ruyi/ruyipkg/list_filter.py,sha256=F64_UhwUEiaUR73EkLu91qoUBA-Yz9mEVWj8XY46MXQ,5467
|
|
@@ -64,16 +64,16 @@ ruyi/ruyipkg/pkg_manifest.py,sha256=FmksKjQyBnU4zA3MFXiHdB2EjNmhs-J9km9vE-zBQlk,
|
|
|
64
64
|
ruyi/ruyipkg/profile.py,sha256=FVppmA6x12m3JfeAWd3Vl9osk4aBprZL4ff7akO018Q,6467
|
|
65
65
|
ruyi/ruyipkg/profile_cli.py,sha256=ud9MS9JQLOtec_1tFRu669I-imVfi1DHU3AuBJym9mw,848
|
|
66
66
|
ruyi/ruyipkg/protocols.py,sha256=lPKRaAcK3DY3wmkyO2tKpFvPQ_4QA4aSNNsNLvi78O8,1833
|
|
67
|
-
ruyi/ruyipkg/repo.py,sha256=
|
|
67
|
+
ruyi/ruyipkg/repo.py,sha256=3D_R9JjztY2IbyeQsfnrwJHNJ_Bcx3o_QZZEOk02YNo,25167
|
|
68
68
|
ruyi/ruyipkg/state.py,sha256=3DRYyHvm_GOvKFVWZUV0iFVnXdK2vn6_RKBXKxFzK64,11235
|
|
69
|
-
ruyi/ruyipkg/unpack.py,sha256=
|
|
69
|
+
ruyi/ruyipkg/unpack.py,sha256=hPd-lOnDXp1lEb4mdbLUEckT4QXblSFxz-dGAdwaAjc,11207
|
|
70
70
|
ruyi/ruyipkg/unpack_method.py,sha256=MonFFvcDb7MVsi2w4yitnCeZkmWmS7nRMMY-wSt9AMs,2106
|
|
71
71
|
ruyi/ruyipkg/update_cli.py,sha256=ywsAAUPctJ3_2qPDvFL2__ql9m8tGZ6ZfrwbSk30Xh4,1425
|
|
72
72
|
ruyi/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
73
|
ruyi/telemetry/aggregate.py,sha256=Ybt25xrg0n62A9Ipslv18EBC-kLkyYsPeEzgguTtChg,1981
|
|
74
74
|
ruyi/telemetry/event.py,sha256=GZnFj6E59Q7mjp-2VRApAZH3rT_bu4_cWb5QMCPm-Zc,982
|
|
75
75
|
ruyi/telemetry/node_info.py,sha256=ix-udhoKQI-HV7qVeEbd-jy3ZyhXL62sVA9uX4fowEg,5178
|
|
76
|
-
ruyi/telemetry/provider.py,sha256=
|
|
76
|
+
ruyi/telemetry/provider.py,sha256=U88ydGga3dwYAi7mfNKmEEyfhAZ4xelF2L-jNAk29IY,16486
|
|
77
77
|
ruyi/telemetry/scope.py,sha256=e45VPAvRAqSxrL0ESorN9SCnR_I6Bwi2CMPJDDshJEE,1133
|
|
78
78
|
ruyi/telemetry/store.py,sha256=7ThCzKF2txT_xU3qgjotOcwRqwNhozIkIyYQ0GSUxBg,8015
|
|
79
79
|
ruyi/telemetry/telemetry_cli.py,sha256=PBVMUSE3P6IBKQVMji_bueVenCdbchbOlowySXy0468,3364
|
|
@@ -81,7 +81,7 @@ ruyi/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
81
81
|
ruyi/utils/ar.py,sha256=w9wiYYdbInLewr5IRTcP0TiOw2ibVDQEmnV0hHm9WlA,2271
|
|
82
82
|
ruyi/utils/ci.py,sha256=66DBm4ooA7yozDtXCJFd1n2jJXTsEnxPSpkNzLfE28M,2970
|
|
83
83
|
ruyi/utils/frontmatter.py,sha256=4EOohEYCZ_q6ncpDv7ktJYf9PN4WEdgFfdE9hZBV3Zg,1052
|
|
84
|
-
ruyi/utils/git.py,sha256=
|
|
84
|
+
ruyi/utils/git.py,sha256=YspRRkfxLXluCv4LNx6q_mjkPdoX7WSM9aR7EfudqlM,5991
|
|
85
85
|
ruyi/utils/global_mode.py,sha256=A9ehY4z1ckUSMHqiDDlXmJpjPzTajCgsDvDqyTwZZj4,4979
|
|
86
86
|
ruyi/utils/l10n.py,sha256=l003oQ5M8fWIKQHbYTVSc6oHzFFGU2sbKac7Hh6FNFU,2530
|
|
87
87
|
ruyi/utils/markdown.py,sha256=Mpq--ClM4j9lm_-5zO53ptYePUTLI4rg0V1YshOwsf8,2654
|
|
@@ -91,12 +91,12 @@ ruyi/utils/porcelain.py,sha256=pF6ieSE2xlnC0HBADFY0m-uuwVNNME3wlbHo2jWdLFA,1403
|
|
|
91
91
|
ruyi/utils/prereqs.py,sha256=oWAaH-smpTMQxpHt782YBxqHxrTaheataslN988-a78,2076
|
|
92
92
|
ruyi/utils/ssl_patch.py,sha256=a5gf4br6nC39wTHsqiFtcJ-mGzqB-YzK6DHSeERLaHQ,5661
|
|
93
93
|
ruyi/utils/templating.py,sha256=94xBJTkIfDqmUBTc9hnLO54zQoC7hwGWONGF3YbaqHk,966
|
|
94
|
-
ruyi/utils/toml.py,sha256=
|
|
94
|
+
ruyi/utils/toml.py,sha256=aniIF3SGfR69_s3GWWwlnoKxW4B5IDVY2CM0eUI55_c,3501
|
|
95
95
|
ruyi/utils/url.py,sha256=Wyct6syS4GmZC6mY7SK-YgBWxKl3cOOBXtp9UtvGkto,186
|
|
96
|
-
ruyi/utils/xdg_basedir.py,sha256=
|
|
96
|
+
ruyi/utils/xdg_basedir.py,sha256=RwVH199jPcLVsg5ngR62RaNS5hqnMpkdt31LqkCfa1g,2751
|
|
97
97
|
ruyi/version.py,sha256=KLJkvKexU07mu-GVDbYKsQvReRvwlVFYkRmcvnyfQNY,2142
|
|
98
|
-
ruyi-0.42.
|
|
99
|
-
ruyi-0.42.
|
|
100
|
-
ruyi-0.42.
|
|
101
|
-
ruyi-0.42.
|
|
102
|
-
ruyi-0.42.
|
|
98
|
+
ruyi-0.42.0a20251013.dist-info/METADATA,sha256=S9n6IZt93cQ9AXUc1vAdp6XAgOAtFTLOkKFjMvnhhog,24282
|
|
99
|
+
ruyi-0.42.0a20251013.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
100
|
+
ruyi-0.42.0a20251013.dist-info/entry_points.txt,sha256=GXSNSy7OgFrnlU5xm5dE3l3PGO92Qf6VDIUCdvQNm8E,49
|
|
101
|
+
ruyi-0.42.0a20251013.dist-info/licenses/LICENSE-Apache.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
102
|
+
ruyi-0.42.0a20251013.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ruyi-0.42.0a20251005.dist-info → ruyi-0.42.0a20251013.dist-info}/licenses/LICENSE-Apache.txt
RENAMED
|
File without changes
|