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/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
- expected_type = get_expected_type_for_config_key(parsed_key)
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
- _ensure_value_type(key, check_val, val, expected_type)
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 _ensure_value_type(
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 check_val and not isinstance(val, expected):
130
- raise InvalidConfigValueTypeError(key, val, expected)
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] | type,
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
- expected_type = key
175
- else:
176
- expected_type = get_expected_type_for_config_key(key)
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"invalid type for config value: {expected_type}")
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 the flashing begins.
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
@@ -44,7 +44,7 @@ class RuyiHostAPI:
44
44
 
45
45
  @property
46
46
  def ruyi_version(self) -> str:
47
- return str(RUYI_SEMVER)
47
+ return RUYI_SEMVER
48
48
 
49
49
  @property
50
50
  def ruyi_plugin_api_rev(self) -> int:
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 .fetch import BaseFetcher
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(self, root: str | None, logger: RuyiLogger) -> None:
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(self, root: str | None, logger: RuyiLogger) -> None:
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.path
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
- bm = pm.binary_metadata
53
- sm = pm.source_metadata
54
- if bm is None and sm is None:
55
- logger.F(f"don't know how to extract package [green]{pkg_name}[/]")
56
- return 2
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
- if bm is not None and sm is not None:
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
- if not distfiles_for_host:
71
- logger.F(
72
- f"package [green]{pkg_name}[/] declares no distfile for host {canonicalized_host}"
73
- )
74
- return 2
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
- dfs = pm.distfiles
80
+ pkg_name = pm.name_for_installation
77
81
 
78
- for df_name in distfiles_for_host:
79
- df_decl = dfs[df_name]
80
- ensure_unpack_cmd_for_method(logger, df_decl.unpack_method)
81
- df = Distfile(df_decl, mr)
82
- df.ensure(logger)
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
- logger.I(f"extracting [green]{df_name}[/] for package [green]{pkg_name}[/]")
85
- # unpack into CWD
86
- df.unpack(None, logger)
90
+ logger.D(f"about to extract {pm} to {dest_dir}")
87
91
 
88
- logger.I(f"package [green]{pkg_name}[/] extracted to current working directory")
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
 
@@ -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 Any, Iterable, TypeGuard, cast
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(f"the package repository does not exist at [yellow]{self.root}[/]")
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.override_repo_dir is None
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: