ruyi 0.41.0b20250926__py3-none-any.whl → 0.42.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/cli/config_cli.py +20 -4
- ruyi/cli/main.py +30 -8
- ruyi/cli/oobe.py +7 -1
- ruyi/cli/self_cli.py +7 -2
- ruyi/config/__init__.py +121 -38
- ruyi/config/editor.py +34 -3
- ruyi/config/errors.py +22 -6
- ruyi/config/schema.py +72 -17
- ruyi/device/provision.py +1 -1
- ruyi/pluginhost/api.py +1 -1
- ruyi/ruyipkg/distfile.py +12 -4
- ruyi/ruyipkg/install.py +99 -32
- ruyi/ruyipkg/install_cli.py +29 -1
- ruyi/ruyipkg/profile.py +148 -1
- ruyi/ruyipkg/repo.py +6 -3
- ruyi/ruyipkg/state.py +10 -0
- ruyi/ruyipkg/unpack.py +14 -16
- ruyi/telemetry/provider.py +65 -22
- ruyi/telemetry/store.py +1 -1
- ruyi/utils/git.py +4 -2
- ruyi/utils/global_mode.py +19 -1
- ruyi/utils/toml.py +11 -0
- ruyi/utils/xdg_basedir.py +20 -13
- ruyi/version.py +2 -49
- {ruyi-0.41.0b20250926.dist-info → ruyi-0.42.0.dist-info}/METADATA +18 -11
- {ruyi-0.41.0b20250926.dist-info → ruyi-0.42.0.dist-info}/RECORD +30 -30
- /ruyi/ruyipkg/{fetch.py → fetcher.py} +0 -0
- {ruyi-0.41.0b20250926.dist-info → ruyi-0.42.0.dist-info}/WHEEL +0 -0
- {ruyi-0.41.0b20250926.dist-info → ruyi-0.42.0.dist-info}/entry_points.txt +0 -0
- {ruyi-0.41.0b20250926.dist-info → ruyi-0.42.0.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/config/schema.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import sys
|
|
3
|
-
from typing import Final, Sequence
|
|
3
|
+
from typing import Final, Sequence, TypeGuard
|
|
4
4
|
|
|
5
5
|
from .errors import (
|
|
6
6
|
InvalidConfigKeyError,
|
|
@@ -43,7 +43,7 @@ def validate_section(section: str) -> None:
|
|
|
43
43
|
raise InvalidConfigSectionError(section)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def get_expected_type_for_config_key(key: str | Sequence[str]) -> type:
|
|
46
|
+
def get_expected_type_for_config_key(key: str | Sequence[str]) -> type | Sequence[type]:
|
|
47
47
|
parsed_key = parse_config_key(key)
|
|
48
48
|
if len(parsed_key) != 2:
|
|
49
49
|
# for now there's no nested config option
|
|
@@ -87,17 +87,29 @@ def _get_expected_type_for_section_repo(sel: str) -> type:
|
|
|
87
87
|
raise InvalidConfigKeyError(sel)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def _get_expected_type_for_section_telemetry(sel: str) -> type:
|
|
90
|
+
def _get_expected_type_for_section_telemetry(sel: str) -> type | tuple[type, ...]:
|
|
91
91
|
if sel == KEY_TELEMETRY_MODE:
|
|
92
92
|
return str
|
|
93
93
|
elif sel == KEY_TELEMETRY_PM_TELEMETRY_URL:
|
|
94
94
|
return str
|
|
95
95
|
elif sel == KEY_TELEMETRY_UPLOAD_CONSENT:
|
|
96
|
-
return datetime.datetime
|
|
96
|
+
return (type(None), datetime.datetime)
|
|
97
97
|
else:
|
|
98
98
|
raise InvalidConfigKeyError(sel)
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def _is_all_str(obj: object) -> TypeGuard[Sequence[str]]:
|
|
102
|
+
if not isinstance(obj, Sequence):
|
|
103
|
+
return False
|
|
104
|
+
return all(isinstance(i, str) for i in obj)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_all_type(obj: object) -> TypeGuard[Sequence[type]]:
|
|
108
|
+
if not isinstance(obj, Sequence):
|
|
109
|
+
return False
|
|
110
|
+
return all(isinstance(i, type) for i in obj)
|
|
111
|
+
|
|
112
|
+
|
|
101
113
|
def ensure_valid_config_kv(
|
|
102
114
|
key: str | Sequence[str],
|
|
103
115
|
check_val: bool = False,
|
|
@@ -108,9 +120,9 @@ def ensure_valid_config_kv(
|
|
|
108
120
|
# for now there's no nested config option
|
|
109
121
|
raise InvalidConfigKeyError(key)
|
|
110
122
|
|
|
111
|
-
|
|
123
|
+
expected_types = get_expected_type_for_config_key(parsed_key)
|
|
112
124
|
# validity of config key is already checked by get_expected_type_for_config_key
|
|
113
|
-
|
|
125
|
+
ensure_value_type(key, check_val, val, expected_types)
|
|
114
126
|
|
|
115
127
|
if not check_val:
|
|
116
128
|
return
|
|
@@ -120,14 +132,26 @@ def ensure_valid_config_kv(
|
|
|
120
132
|
return _extra_validate_section_telemetry_kv(key, sel, val)
|
|
121
133
|
|
|
122
134
|
|
|
123
|
-
def
|
|
135
|
+
def ensure_value_type(
|
|
124
136
|
key: str | Sequence[str],
|
|
125
137
|
check_val: bool,
|
|
126
138
|
val: object | None,
|
|
127
|
-
expected: type,
|
|
139
|
+
expected: type | Sequence[type],
|
|
128
140
|
) -> None:
|
|
129
|
-
if
|
|
130
|
-
|
|
141
|
+
if not check_val:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
expected_types: tuple[type, ...]
|
|
145
|
+
if isinstance(expected, type):
|
|
146
|
+
expected_types = (expected,)
|
|
147
|
+
else:
|
|
148
|
+
expected_types = tuple(expected)
|
|
149
|
+
|
|
150
|
+
for ty in expected_types:
|
|
151
|
+
if isinstance(val, ty):
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
raise InvalidConfigValueTypeError(key, val, expected)
|
|
131
155
|
|
|
132
156
|
|
|
133
157
|
def _extra_validate_section_telemetry_kv(
|
|
@@ -138,13 +162,18 @@ def _extra_validate_section_telemetry_kv(
|
|
|
138
162
|
if sel == KEY_TELEMETRY_MODE:
|
|
139
163
|
# value type is already ensured earlier
|
|
140
164
|
if val not in ("local", "off", "on"):
|
|
141
|
-
raise InvalidConfigValueError(key, val)
|
|
165
|
+
raise InvalidConfigValueError(key, val, str)
|
|
142
166
|
|
|
143
167
|
|
|
144
168
|
def encode_value(v: object) -> str:
|
|
145
169
|
"""Encodes the given config value into a string representation suitable for
|
|
146
170
|
display or storage into TOML config files."""
|
|
147
171
|
|
|
172
|
+
if v is None:
|
|
173
|
+
from ..utils.toml import NoneValue
|
|
174
|
+
|
|
175
|
+
raise NoneValue()
|
|
176
|
+
|
|
148
177
|
if isinstance(v, bool):
|
|
149
178
|
return "true" if v else "false"
|
|
150
179
|
elif isinstance(v, int):
|
|
@@ -164,24 +193,50 @@ def encode_value(v: object) -> str:
|
|
|
164
193
|
|
|
165
194
|
|
|
166
195
|
def decode_value(
|
|
167
|
-
key: str | Sequence[str]
|
|
196
|
+
key: str | Sequence[str],
|
|
168
197
|
val: str,
|
|
169
198
|
) -> object:
|
|
170
199
|
"""Decodes the given string representation of a config value into a Python
|
|
171
200
|
value, directed by type information implied by the config key."""
|
|
172
201
|
|
|
173
202
|
if isinstance(key, type):
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
203
|
+
return _decode_single_type_value(None, val, key)
|
|
204
|
+
elif _is_all_type(key):
|
|
205
|
+
return _decode_typed_value(None, val, key)
|
|
206
|
+
|
|
207
|
+
assert isinstance(key, str) or _is_all_str(key)
|
|
208
|
+
expected_types = get_expected_type_for_config_key(key)
|
|
177
209
|
|
|
210
|
+
if isinstance(expected_types, type):
|
|
211
|
+
return _decode_single_type_value(key, val, expected_types)
|
|
212
|
+
return _decode_typed_value(key, val, expected_types)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _decode_typed_value(
|
|
216
|
+
key: str | Sequence[str] | None,
|
|
217
|
+
val: str,
|
|
218
|
+
expected_types: Sequence[type],
|
|
219
|
+
) -> object:
|
|
220
|
+
for ty in expected_types:
|
|
221
|
+
try:
|
|
222
|
+
return _decode_single_type_value(key, val, ty)
|
|
223
|
+
except (RuntimeError, TypeError, ValueError):
|
|
224
|
+
continue
|
|
225
|
+
raise InvalidConfigValueError(key, val, expected_types)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _decode_single_type_value(
|
|
229
|
+
key: str | Sequence[str] | None,
|
|
230
|
+
val: str,
|
|
231
|
+
expected_type: type,
|
|
232
|
+
) -> object:
|
|
178
233
|
if expected_type is bool:
|
|
179
234
|
if val in ("true", "yes", "1"):
|
|
180
235
|
return True
|
|
181
236
|
elif val in ("false", "no", "0"):
|
|
182
237
|
return False
|
|
183
238
|
else:
|
|
184
|
-
raise InvalidConfigValueError(key, val)
|
|
239
|
+
raise InvalidConfigValueError(key, val, expected_type)
|
|
185
240
|
elif expected_type is int:
|
|
186
241
|
return int(val, 10)
|
|
187
242
|
elif expected_type is str:
|
|
@@ -194,4 +249,4 @@ def decode_value(
|
|
|
194
249
|
v = datetime.datetime.fromisoformat(val)
|
|
195
250
|
return v.astimezone() if v.tzinfo is None else v
|
|
196
251
|
else:
|
|
197
|
-
raise NotImplementedError(f"
|
|
252
|
+
raise NotImplementedError(f"unhandled type for config value: {expected_type}")
|
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/pluginhost/api.py
CHANGED
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/profile.py
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
from os import PathLike
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Iterable,
|
|
5
|
+
Mapping,
|
|
6
|
+
Protocol,
|
|
7
|
+
Sequence,
|
|
8
|
+
TypedDict,
|
|
9
|
+
TypeGuard,
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
cast,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing_extensions import NotRequired
|
|
3
16
|
|
|
4
17
|
from ..pluginhost.ctx import PluginHostContext, SupportsEvalFunction
|
|
18
|
+
from .entity_provider import BaseEntityProvider
|
|
5
19
|
from .pkg_manifest import EmulatorFlavor
|
|
6
20
|
|
|
7
21
|
|
|
@@ -206,3 +220,136 @@ class ProfileProxy:
|
|
|
206
220
|
sysroot: PathLike[Any] | None,
|
|
207
221
|
) -> dict[str, str] | None:
|
|
208
222
|
return self._provider.get_env_config_for_emu_flavor(self._id, flavor, sysroot)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
#
|
|
226
|
+
# Protocols
|
|
227
|
+
#
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# MetadataRepo is defined in repo.py, but we don't want to import repo.py here
|
|
231
|
+
# to avoid circular import. Instead, we just describe the methods and properties
|
|
232
|
+
# that we need from MetadataRepo with a Protocol.
|
|
233
|
+
class ProvidesProfiles(Protocol):
|
|
234
|
+
def get_supported_arches(self) -> list[str]: ...
|
|
235
|
+
def get_profile_for_arch(self, arch: str, name: str) -> ProfileProxy | None: ...
|
|
236
|
+
def iter_profiles_for_arch(self, arch: str) -> Iterable[ProfileProxy]: ...
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
#
|
|
240
|
+
# Entity type and schema for profile entities
|
|
241
|
+
#
|
|
242
|
+
|
|
243
|
+
PROFILE_V1_ENTITY_TYPE = "profile-v1"
|
|
244
|
+
PROFILE_V1_ENTITY_TYPE_SCHEMA = {
|
|
245
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
246
|
+
"required": ["profile-v1"],
|
|
247
|
+
"properties": {
|
|
248
|
+
"profile-v1": {
|
|
249
|
+
"type": "object",
|
|
250
|
+
"properties": {
|
|
251
|
+
"id": {"type": "string"},
|
|
252
|
+
"display_name": {"type": "string"},
|
|
253
|
+
"name": {"type": "string"},
|
|
254
|
+
"arch": {"type": "string"},
|
|
255
|
+
"needed_toolchain_quirks": {
|
|
256
|
+
"type": "array",
|
|
257
|
+
"items": {"type": "string"},
|
|
258
|
+
},
|
|
259
|
+
"toolchain_common_flags_str": {"type": "string"},
|
|
260
|
+
},
|
|
261
|
+
"required": [
|
|
262
|
+
"id",
|
|
263
|
+
"display_name",
|
|
264
|
+
"name",
|
|
265
|
+
"arch",
|
|
266
|
+
"needed_toolchain_quirks",
|
|
267
|
+
"toolchain_common_flags_str",
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
"related": {
|
|
271
|
+
"type": "array",
|
|
272
|
+
"description": "List of related entity references",
|
|
273
|
+
"items": {"type": "string", "pattern": "^.+:.+"},
|
|
274
|
+
},
|
|
275
|
+
"unique_among_type_during_traversal": {
|
|
276
|
+
"type": "boolean",
|
|
277
|
+
"description": "Whether this entity should be unique among all entities of the same type during traversal",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ProfileV1EntityData(TypedDict):
|
|
284
|
+
id: str
|
|
285
|
+
display_name: str
|
|
286
|
+
name: str
|
|
287
|
+
arch: str
|
|
288
|
+
needed_toolchain_quirks: list[str]
|
|
289
|
+
toolchain_common_flags_str: str
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
ProfileV1Entity = TypedDict(
|
|
293
|
+
"ProfileV1Entity",
|
|
294
|
+
{
|
|
295
|
+
"profile-v1": ProfileV1EntityData,
|
|
296
|
+
"related": "NotRequired[list[str]]",
|
|
297
|
+
"unique_among_type_during_traversal": "NotRequired[bool]",
|
|
298
|
+
},
|
|
299
|
+
total=False,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ProfileEntityProvider(BaseEntityProvider):
|
|
304
|
+
def __init__(self, provider: ProvidesProfiles) -> None:
|
|
305
|
+
super().__init__()
|
|
306
|
+
self._provider = provider
|
|
307
|
+
|
|
308
|
+
def discover_schemas(self) -> dict[str, object]:
|
|
309
|
+
return {
|
|
310
|
+
PROFILE_V1_ENTITY_TYPE: PROFILE_V1_ENTITY_TYPE_SCHEMA,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def load_entities(
|
|
314
|
+
self,
|
|
315
|
+
entity_types: Sequence[str],
|
|
316
|
+
) -> Mapping[str, Mapping[str, Mapping[str, Any]]]:
|
|
317
|
+
result: dict[str, Mapping[str, Mapping[str, Any]]] = {}
|
|
318
|
+
for ty in entity_types:
|
|
319
|
+
if ty == PROFILE_V1_ENTITY_TYPE:
|
|
320
|
+
result[ty] = _load_profile_v1_entities(self._provider)
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _load_profile_v1_entities(provider: ProvidesProfiles) -> dict[str, ProfileV1Entity]:
|
|
325
|
+
result: dict[str, ProfileV1Entity] = {}
|
|
326
|
+
for arch in provider.get_supported_arches():
|
|
327
|
+
result.update(_load_profile_v1_entities_for_arch(provider, arch))
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _load_profile_v1_entities_for_arch(
|
|
332
|
+
provider: ProvidesProfiles,
|
|
333
|
+
arch: str,
|
|
334
|
+
) -> dict[str, ProfileV1Entity]:
|
|
335
|
+
result: dict[str, ProfileV1Entity] = {}
|
|
336
|
+
for profile in provider.iter_profiles_for_arch(arch):
|
|
337
|
+
full_name = profile.id
|
|
338
|
+
relations = [f"arch:{arch}"]
|
|
339
|
+
|
|
340
|
+
needed_toolchain_quirks = sorted(profile.need_quirks)
|
|
341
|
+
|
|
342
|
+
result[profile.id] = {
|
|
343
|
+
"profile-v1": {
|
|
344
|
+
"id": profile.id,
|
|
345
|
+
"display_name": full_name,
|
|
346
|
+
"name": profile.id,
|
|
347
|
+
"arch": profile.arch,
|
|
348
|
+
"needed_toolchain_quirks": needed_toolchain_quirks,
|
|
349
|
+
"toolchain_common_flags_str": profile.get_common_flags(
|
|
350
|
+
needed_toolchain_quirks,
|
|
351
|
+
),
|
|
352
|
+
},
|
|
353
|
+
"related": relations,
|
|
354
|
+
}
|
|
355
|
+
return result
|
ruyi/ruyipkg/repo.py
CHANGED
|
@@ -34,7 +34,7 @@ from .pkg_manifest import (
|
|
|
34
34
|
InputPackageManifestType,
|
|
35
35
|
is_prerelease,
|
|
36
36
|
)
|
|
37
|
-
from .profile import PluginProfileProvider, ProfileProxy
|
|
37
|
+
from .profile import PluginProfileProvider, ProfileEntityProvider, ProfileProxy
|
|
38
38
|
from .protocols import ProvidesPackageManifests
|
|
39
39
|
|
|
40
40
|
if sys.version_info >= (3, 11):
|
|
@@ -234,6 +234,7 @@ class MetadataRepo(ProvidesPackageManifests):
|
|
|
234
234
|
gc.logger,
|
|
235
235
|
FSEntityProvider(gc.logger, pathlib.Path(self.root) / "entities"),
|
|
236
236
|
MetadataRepoEntityProvider(self),
|
|
237
|
+
ProfileEntityProvider(self),
|
|
237
238
|
)
|
|
238
239
|
self._plugin_host_ctx = PluginHostContext.new(gc.logger, self.plugin_root)
|
|
239
240
|
self._plugin_fn_evaluator = self._plugin_host_ctx.make_evaluator()
|
|
@@ -284,7 +285,9 @@ class MetadataRepo(ProvidesPackageManifests):
|
|
|
284
285
|
self.repo = Repository(self.root)
|
|
285
286
|
return self.repo
|
|
286
287
|
|
|
287
|
-
self.logger.I(
|
|
288
|
+
self.logger.I(
|
|
289
|
+
f"the package repository does not exist at [yellow]{self.root}[/]"
|
|
290
|
+
)
|
|
288
291
|
self.logger.I(f"cloning from [cyan link={self.remote}]{self.remote}[/]")
|
|
289
292
|
|
|
290
293
|
with RemoteGitProgressIndicator() as pr:
|
|
@@ -313,7 +316,7 @@ class MetadataRepo(ProvidesPackageManifests):
|
|
|
313
316
|
|
|
314
317
|
# only manage the repo settings on the user's behalf if the user
|
|
315
318
|
# has not overridden the repo directory themselves
|
|
316
|
-
allow_auto_management = self._gc.
|
|
319
|
+
allow_auto_management = not self._gc.have_overridden_repo_dir
|
|
317
320
|
|
|
318
321
|
pull_ff_or_die(
|
|
319
322
|
self.logger,
|
ruyi/ruyipkg/state.py
CHANGED
|
@@ -76,6 +76,16 @@ class RuyipkgGlobalStateStore:
|
|
|
76
76
|
"""Ensure the state directory exists."""
|
|
77
77
|
self.root.mkdir(parents=True, exist_ok=True)
|
|
78
78
|
|
|
79
|
+
def purge_installation_info(self) -> None:
|
|
80
|
+
"""Purge installation records."""
|
|
81
|
+
self._installs_file.unlink(missing_ok=True)
|
|
82
|
+
self._installs_cache = None
|
|
83
|
+
# if the state dir is empty, remove it
|
|
84
|
+
try:
|
|
85
|
+
self.root.rmdir()
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|
|
88
|
+
|
|
79
89
|
def _load_installs(self) -> dict[str, PackageInstallationInfo]:
|
|
80
90
|
"""Load installation records from disk."""
|
|
81
91
|
if self._installs_cache is not None:
|