ruyi 0.39.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. ruyi/__init__.py +21 -0
  2. ruyi/__main__.py +98 -0
  3. ruyi/cli/__init__.py +5 -0
  4. ruyi/cli/builtin_commands.py +14 -0
  5. ruyi/cli/cmd.py +224 -0
  6. ruyi/cli/completer.py +50 -0
  7. ruyi/cli/completion.py +26 -0
  8. ruyi/cli/config_cli.py +153 -0
  9. ruyi/cli/main.py +111 -0
  10. ruyi/cli/self_cli.py +295 -0
  11. ruyi/cli/user_input.py +127 -0
  12. ruyi/cli/version_cli.py +45 -0
  13. ruyi/config/__init__.py +401 -0
  14. ruyi/config/editor.py +92 -0
  15. ruyi/config/errors.py +76 -0
  16. ruyi/config/news.py +39 -0
  17. ruyi/config/schema.py +197 -0
  18. ruyi/device/__init__.py +0 -0
  19. ruyi/device/provision.py +591 -0
  20. ruyi/device/provision_cli.py +40 -0
  21. ruyi/log/__init__.py +272 -0
  22. ruyi/mux/.gitignore +1 -0
  23. ruyi/mux/__init__.py +0 -0
  24. ruyi/mux/runtime.py +213 -0
  25. ruyi/mux/venv/__init__.py +12 -0
  26. ruyi/mux/venv/emulator_cfg.py +41 -0
  27. ruyi/mux/venv/maker.py +782 -0
  28. ruyi/mux/venv/venv_cli.py +92 -0
  29. ruyi/mux/venv_cfg.py +214 -0
  30. ruyi/pluginhost/__init__.py +0 -0
  31. ruyi/pluginhost/api.py +206 -0
  32. ruyi/pluginhost/ctx.py +222 -0
  33. ruyi/pluginhost/paths.py +135 -0
  34. ruyi/pluginhost/plugin_cli.py +37 -0
  35. ruyi/pluginhost/unsandboxed.py +246 -0
  36. ruyi/py.typed +0 -0
  37. ruyi/resource_bundle/__init__.py +20 -0
  38. ruyi/resource_bundle/__main__.py +55 -0
  39. ruyi/resource_bundle/data.py +26 -0
  40. ruyi/ruyipkg/__init__.py +0 -0
  41. ruyi/ruyipkg/admin_checksum.py +88 -0
  42. ruyi/ruyipkg/admin_cli.py +83 -0
  43. ruyi/ruyipkg/atom.py +184 -0
  44. ruyi/ruyipkg/augmented_pkg.py +212 -0
  45. ruyi/ruyipkg/canonical_dump.py +320 -0
  46. ruyi/ruyipkg/checksum.py +39 -0
  47. ruyi/ruyipkg/cli_completion.py +42 -0
  48. ruyi/ruyipkg/distfile.py +208 -0
  49. ruyi/ruyipkg/entity.py +387 -0
  50. ruyi/ruyipkg/entity_cli.py +123 -0
  51. ruyi/ruyipkg/entity_provider.py +273 -0
  52. ruyi/ruyipkg/fetch.py +271 -0
  53. ruyi/ruyipkg/host.py +55 -0
  54. ruyi/ruyipkg/install.py +554 -0
  55. ruyi/ruyipkg/install_cli.py +150 -0
  56. ruyi/ruyipkg/list.py +126 -0
  57. ruyi/ruyipkg/list_cli.py +79 -0
  58. ruyi/ruyipkg/list_filter.py +173 -0
  59. ruyi/ruyipkg/msg.py +99 -0
  60. ruyi/ruyipkg/news.py +123 -0
  61. ruyi/ruyipkg/news_cli.py +78 -0
  62. ruyi/ruyipkg/news_store.py +183 -0
  63. ruyi/ruyipkg/pkg_manifest.py +657 -0
  64. ruyi/ruyipkg/profile.py +208 -0
  65. ruyi/ruyipkg/profile_cli.py +33 -0
  66. ruyi/ruyipkg/protocols.py +55 -0
  67. ruyi/ruyipkg/repo.py +763 -0
  68. ruyi/ruyipkg/state.py +345 -0
  69. ruyi/ruyipkg/unpack.py +369 -0
  70. ruyi/ruyipkg/unpack_method.py +91 -0
  71. ruyi/ruyipkg/update_cli.py +54 -0
  72. ruyi/telemetry/__init__.py +0 -0
  73. ruyi/telemetry/aggregate.py +72 -0
  74. ruyi/telemetry/event.py +41 -0
  75. ruyi/telemetry/node_info.py +192 -0
  76. ruyi/telemetry/provider.py +411 -0
  77. ruyi/telemetry/scope.py +43 -0
  78. ruyi/telemetry/store.py +238 -0
  79. ruyi/telemetry/telemetry_cli.py +127 -0
  80. ruyi/utils/__init__.py +0 -0
  81. ruyi/utils/ar.py +74 -0
  82. ruyi/utils/ci.py +63 -0
  83. ruyi/utils/frontmatter.py +38 -0
  84. ruyi/utils/git.py +169 -0
  85. ruyi/utils/global_mode.py +204 -0
  86. ruyi/utils/l10n.py +83 -0
  87. ruyi/utils/markdown.py +73 -0
  88. ruyi/utils/nuitka.py +33 -0
  89. ruyi/utils/porcelain.py +51 -0
  90. ruyi/utils/prereqs.py +77 -0
  91. ruyi/utils/ssl_patch.py +170 -0
  92. ruyi/utils/templating.py +34 -0
  93. ruyi/utils/toml.py +115 -0
  94. ruyi/utils/url.py +7 -0
  95. ruyi/utils/xdg_basedir.py +80 -0
  96. ruyi/version.py +67 -0
  97. ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
  98. ruyi-0.39.0.dist-info/METADATA +403 -0
  99. ruyi-0.39.0.dist-info/RECORD +101 -0
  100. ruyi-0.39.0.dist-info/WHEEL +4 -0
  101. ruyi-0.39.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,657 @@
1
+ from copy import deepcopy
2
+ from functools import cached_property
3
+ import os
4
+ import pathlib
5
+ import re
6
+ from typing import (
7
+ Any,
8
+ BinaryIO,
9
+ Final,
10
+ Iterable,
11
+ Iterator,
12
+ Literal,
13
+ TypedDict,
14
+ TYPE_CHECKING,
15
+ cast,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from typing_extensions import NotRequired, Self
20
+
21
+ # pyright only works with semver 3.x
22
+ from semver.version import Version
23
+ else:
24
+ try:
25
+ from semver.version import Version # type: ignore[import-untyped,unused-ignore]
26
+ except ModuleNotFoundError:
27
+ # semver 2.x
28
+ from semver import VersionInfo as Version # type: ignore[import-untyped,unused-ignore]
29
+
30
+ import tomlkit
31
+
32
+ from .host import RuyiHost, canonicalize_host_str, get_native_host
33
+ from .msg import RepoMessageStore
34
+ from .unpack_method import UnpackMethod, determine_unpack_method
35
+
36
+ if TYPE_CHECKING:
37
+ # avoid circular import at runtime
38
+ from .repo import MetadataRepo
39
+
40
+
41
+ class VendorDeclType(TypedDict):
42
+ name: str
43
+ eula: str | None
44
+
45
+
46
+ RestrictKind = Literal["fetch"] | Literal["mirror"]
47
+
48
+
49
+ class FetchRestrictionDeclType(TypedDict):
50
+ msgid: str
51
+ params: "NotRequired[dict[str, str]]"
52
+
53
+
54
+ class DistfileDeclType(TypedDict):
55
+ name: str
56
+ urls: "NotRequired[list[str]]"
57
+ restrict: "NotRequired[list[RestrictKind]]"
58
+ size: int
59
+ checksums: dict[str, str]
60
+ strip_components: "NotRequired[int]"
61
+ unpack: "NotRequired[UnpackMethod]"
62
+ fetch_restriction: "NotRequired[FetchRestrictionDeclType]"
63
+ prefixes_to_unpack: "NotRequired[list[str]]"
64
+
65
+
66
+ class BinaryFileDeclType(TypedDict):
67
+ host: str
68
+ distfiles: list[str]
69
+ commands: "NotRequired[dict[str, str]]"
70
+
71
+
72
+ BinaryDeclType = list[BinaryFileDeclType]
73
+
74
+
75
+ class BlobDeclType(TypedDict):
76
+ distfiles: list[str]
77
+
78
+
79
+ class SourceDeclType(TypedDict):
80
+ distfiles: list[str]
81
+
82
+
83
+ class ToolchainComponentDeclType(TypedDict):
84
+ name: str
85
+ version: str
86
+
87
+
88
+ class ToolchainDeclType(TypedDict):
89
+ target: str
90
+ quirks: "NotRequired[list[str]]"
91
+ flavors: "NotRequired[list[str]]" # Backward compatibility alias
92
+ components: list[ToolchainComponentDeclType]
93
+ included_sysroot: "NotRequired[str]"
94
+
95
+
96
+ EmulatorFlavor = Literal["qemu-linux-user"]
97
+
98
+
99
+ class EmulatorProgramDeclType(TypedDict):
100
+ path: str
101
+ flavor: EmulatorFlavor
102
+ supported_arches: list[str]
103
+ binfmt_misc: "NotRequired[str]"
104
+
105
+
106
+ class EmulatorDeclType(TypedDict):
107
+ quirks: "NotRequired[list[str]]"
108
+ flavors: "NotRequired[list[str]]" # Backward compatibility alias
109
+ programs: list[EmulatorProgramDeclType]
110
+
111
+
112
+ PartitionKind = (
113
+ Literal["boot"]
114
+ | Literal["disk"]
115
+ | Literal["live"]
116
+ | Literal["root"]
117
+ | Literal["uboot"]
118
+ )
119
+
120
+ # error: "<typing special form>" has no attribute "__args__"
121
+ # KNOWN_PARTITION_KINDS = frozenset(kind.__args__[0] for kind in PartitionKind.__args__)
122
+ KNOWN_PARTITION_KINDS: Final = frozenset(("boot", "disk", "live", "root", "uboot"))
123
+
124
+ PartitionMapDecl = dict[PartitionKind, str]
125
+
126
+
127
+ class ProvisionableDeclType(TypedDict):
128
+ partition_map: PartitionMapDecl
129
+ strategy: str
130
+
131
+
132
+ PackageKind = (
133
+ Literal["binary"]
134
+ | Literal["blob"]
135
+ | Literal["source"]
136
+ | Literal["toolchain"]
137
+ | Literal["emulator"]
138
+ | Literal["provisionable"]
139
+ )
140
+
141
+ ALL_PACKAGE_KINDS: Final[list[PackageKind]] = [
142
+ "binary",
143
+ "blob",
144
+ "source",
145
+ "toolchain",
146
+ "emulator",
147
+ "provisionable",
148
+ ]
149
+
150
+ RuyiPkgFormat = Literal["v1"]
151
+
152
+ ServiceLevelKind = Literal["known_issue"] | Literal["untested"]
153
+
154
+ ALL_SERVICE_LEVEL_KINDS: Final[list[ServiceLevelKind]] = ["known_issue", "untested"]
155
+
156
+
157
+ class ServiceLevelDeclType(TypedDict):
158
+ level: ServiceLevelKind
159
+ msgid: "NotRequired[str]"
160
+ params: "NotRequired[dict[str, str]]"
161
+
162
+
163
+ class PackageMetadataDeclType(TypedDict):
164
+ slug: "NotRequired[str]" # deprecated for v1+
165
+ desc: str
166
+ doc_uri: "NotRequired[str]"
167
+ vendor: VendorDeclType
168
+ service_level: "NotRequired[list[ServiceLevelDeclType]]"
169
+ upstream_version: "NotRequired[str]"
170
+
171
+
172
+ class InputPackageManifestType(TypedDict):
173
+ format: "NotRequired[RuyiPkgFormat]"
174
+
175
+ # v0 fields
176
+ slug: "NotRequired[str]"
177
+ kind: "NotRequired[list[PackageKind]]" # mandatory in v0
178
+ desc: "NotRequired[str]" # mandatory in v0
179
+ doc_uri: "NotRequired[str]"
180
+ vendor: "NotRequired[VendorDeclType]" # mandatory in v0
181
+
182
+ # v1+ fields
183
+ metadata: "NotRequired[PackageMetadataDeclType]"
184
+
185
+ # common fields
186
+ distfiles: list[DistfileDeclType]
187
+ binary: "NotRequired[BinaryDeclType]"
188
+ blob: "NotRequired[BlobDeclType]"
189
+ source: "NotRequired[SourceDeclType]"
190
+ toolchain: "NotRequired[ToolchainDeclType]"
191
+ emulator: "NotRequired[EmulatorDeclType]"
192
+ provisionable: "NotRequired[ProvisionableDeclType]"
193
+
194
+
195
+ class PackageManifestType(TypedDict):
196
+ format: RuyiPkgFormat
197
+ kind: list[PackageKind]
198
+ metadata: PackageMetadataDeclType
199
+ distfiles: list[DistfileDeclType]
200
+ binary: "NotRequired[BinaryDeclType]"
201
+ blob: "NotRequired[BlobDeclType]"
202
+ source: "NotRequired[SourceDeclType]"
203
+ toolchain: "NotRequired[ToolchainDeclType]"
204
+ emulator: "NotRequired[EmulatorDeclType]"
205
+ provisionable: "NotRequired[ProvisionableDeclType]"
206
+
207
+
208
+ class DistfileDecl:
209
+ def __init__(self, data: DistfileDeclType) -> None:
210
+ self._data = data
211
+
212
+ @property
213
+ def name(self) -> str:
214
+ return self._data["name"]
215
+
216
+ @property
217
+ def urls(self) -> list[str] | None:
218
+ return self._data.get("urls")
219
+
220
+ def is_restricted(self, kind: RestrictKind) -> bool:
221
+ if restricts := self._data.get("restrict"):
222
+ # account for a common oversight in some existing manifests where
223
+ # the field was specified as a string instead of a list, in case the
224
+ # user has not yet synced their repo
225
+ if isinstance(restricts, str):
226
+ return kind == restricts
227
+ return kind in restricts
228
+ return False
229
+
230
+ @property
231
+ def size(self) -> int:
232
+ return self._data["size"]
233
+
234
+ @property
235
+ def checksums(self) -> dict[str, str]:
236
+ return self._data["checksums"]
237
+
238
+ def get_checksum(self, kind: str) -> str | None:
239
+ return self._data["checksums"].get(kind)
240
+
241
+ @property
242
+ def prefixes_to_unpack(self) -> list[str] | None:
243
+ return self._data.get("prefixes_to_unpack")
244
+
245
+ @property
246
+ def strip_components(self) -> int:
247
+ return self._data.get("strip_components", 1)
248
+
249
+ @property
250
+ def unpack_method(self) -> UnpackMethod:
251
+ x = self._data.get("unpack", UnpackMethod.AUTO)
252
+ if x == UnpackMethod.AUTO:
253
+ return determine_unpack_method(self.name)
254
+ return x
255
+
256
+ @property
257
+ def fetch_restriction(self) -> FetchRestrictionDeclType | None:
258
+ return self._data.get("fetch_restriction")
259
+
260
+
261
+ class BinaryDecl:
262
+ def __init__(self, data: BinaryDeclType) -> None:
263
+ self._data = {canonicalize_host_str(d["host"]): d for d in data}
264
+
265
+ @property
266
+ def data(self) -> dict[str, BinaryFileDeclType]:
267
+ return self._data
268
+
269
+ def get_distfile_names_for_host(self, host: str | RuyiHost) -> list[str] | None:
270
+ if data := self._data.get(canonicalize_host_str(host)):
271
+ return data.get("distfiles")
272
+ return None
273
+
274
+ @property
275
+ def is_available_for_current_host(self) -> bool:
276
+ return str(get_native_host()) in self._data
277
+
278
+ def get_commands_for_host(self, host: str) -> dict[str, str]:
279
+ if data := self._data.get(canonicalize_host_str(host)):
280
+ return data.get("commands", {})
281
+ return {}
282
+
283
+
284
+ class BlobDecl:
285
+ def __init__(self, data: BlobDeclType) -> None:
286
+ self._data = data
287
+
288
+ def get_distfile_names(self) -> list[str] | None:
289
+ return self._data["distfiles"]
290
+
291
+
292
+ class SourceDecl:
293
+ def __init__(self, data: SourceDeclType) -> None:
294
+ self._data = data
295
+
296
+ def get_distfile_names_for_host(self, host: str | RuyiHost) -> list[str] | None:
297
+ # currently the host parameter is ignored
298
+ return self._data["distfiles"]
299
+
300
+
301
+ class ToolchainDecl:
302
+ def __init__(self, data: ToolchainDeclType) -> None:
303
+ self._data = data
304
+ self._component_vers_cache: dict[str, str] | None = None
305
+
306
+ # rename "flavors" to "quirks" for compatibility with old data
307
+ if "quirks" not in self._data and "flavors" in self._data:
308
+ self._data["quirks"] = self._data["flavors"]
309
+ del self._data["flavors"]
310
+
311
+ @property
312
+ def _component_vers(self) -> dict[str, str]:
313
+ if self._component_vers_cache is None:
314
+ self._component_vers_cache = {
315
+ x["name"]: x["version"] for x in self.components
316
+ }
317
+ return self._component_vers_cache
318
+
319
+ @property
320
+ def target(self) -> str:
321
+ return self._data["target"]
322
+
323
+ @property
324
+ def target_arch(self) -> str:
325
+ # TODO: switch to proper mapping later; for now this suffices
326
+ return self.target.split("-", 1)[0]
327
+
328
+ @property
329
+ def quirks(self) -> list[str]:
330
+ return self._data.get("quirks", [])
331
+
332
+ def has_quirk(self, q: str) -> bool:
333
+ return q in self.quirks
334
+
335
+ def satisfies_quirk_set(self, req: set[str]) -> bool:
336
+ # req - my_quirks must be the empty set so that my_quirks >= req
337
+ return len(req.difference(self.quirks)) == 0
338
+
339
+ @property
340
+ def components(self) -> Iterable[ToolchainComponentDeclType]:
341
+ return self._data["components"]
342
+
343
+ def get_component_version(self, name: str) -> str | None:
344
+ return self._component_vers.get(name)
345
+
346
+ @property
347
+ def has_binutils(self) -> bool:
348
+ return self.get_component_version("binutils") is not None
349
+
350
+ @property
351
+ def has_clang(self) -> bool:
352
+ return self.get_component_version("clang") is not None
353
+
354
+ @property
355
+ def has_gcc(self) -> bool:
356
+ return self.get_component_version("gcc") is not None
357
+
358
+ @property
359
+ def has_llvm(self) -> bool:
360
+ return self.get_component_version("llvm") is not None
361
+
362
+ @property
363
+ def included_sysroot(self) -> str | None:
364
+ return self._data.get("included_sysroot")
365
+
366
+
367
+ class EmulatorProgDecl:
368
+ def __init__(self, data: EmulatorProgramDeclType) -> None:
369
+ self.relative_path = data["path"]
370
+ # have to explicitly annotate the type to please the type checker...
371
+ self.flavor: EmulatorFlavor = data["flavor"]
372
+ self.supported_arches = set(data["supported_arches"])
373
+ self.binfmt_misc = data.get("binfmt_misc")
374
+
375
+ def get_binfmt_misc_str(self, install_root: os.PathLike[Any]) -> str | None:
376
+ if self.binfmt_misc is None:
377
+ return None
378
+ binpath = os.path.join(install_root, self.relative_path)
379
+ return self.binfmt_misc.replace("$BIN", binpath)
380
+
381
+ @property
382
+ def is_qemu(self) -> bool:
383
+ return self.flavor == "qemu-linux-user"
384
+
385
+
386
+ class EmulatorDecl:
387
+ def __init__(self, data: EmulatorDeclType) -> None:
388
+ self._data = data
389
+ self.programs = [EmulatorProgDecl(x) for x in data["programs"]]
390
+
391
+ # rename "flavors" to "quirks" for compatibility with old data
392
+ if "quirks" not in self._data and "flavors" in self._data:
393
+ self._data["quirks"] = self._data["flavors"]
394
+ del self._data["flavors"]
395
+
396
+ @property
397
+ def quirks(self) -> list[str] | None:
398
+ return self._data.get("quirks")
399
+
400
+ def list_for_arch(self, arch: str) -> Iterable[EmulatorProgDecl]:
401
+ for p in self.programs:
402
+ if arch in p.supported_arches:
403
+ yield p
404
+
405
+
406
+ class ProvisionableDecl:
407
+ def __init__(self, data: ProvisionableDeclType) -> None:
408
+ self._data = data
409
+
410
+ @property
411
+ def partition_map(self) -> PartitionMapDecl:
412
+ return self._data["partition_map"]
413
+
414
+ @property
415
+ def strategy(self) -> str:
416
+ return self._data["strategy"]
417
+
418
+
419
+ class PackageMetadataDecl:
420
+ def __init__(self, data: PackageMetadataDeclType) -> None:
421
+ self._data = data
422
+
423
+
424
+ def _translate_to_manifest_v1(obj: InputPackageManifestType) -> PackageManifestType:
425
+ fmt = obj.get("format", "")
426
+ if fmt == "v1":
427
+ return cast(PackageManifestType, obj)
428
+ if fmt != "":
429
+ # unrecognized package format
430
+ raise RuntimeError(f"unrecognized Ruyi package format: {fmt}")
431
+
432
+ # translate v0 to v1
433
+ result = deepcopy(obj)
434
+ result["format"] = "v1"
435
+
436
+ md: PackageMetadataDeclType = {"desc": "", "vendor": {"name": "", "eula": None}}
437
+ if "slug" in result:
438
+ md["slug"] = result["slug"]
439
+ del result["slug"]
440
+ if "desc" in result:
441
+ md["desc"] = result["desc"]
442
+ del result["desc"]
443
+ if "vendor" in result:
444
+ md["vendor"] = result["vendor"]
445
+ del result["vendor"]
446
+ if "doc_uri" in result:
447
+ md["doc_uri"] = result["doc_uri"]
448
+ del result["doc_uri"]
449
+ result["metadata"] = md
450
+
451
+ return cast(PackageManifestType, result)
452
+
453
+
454
+ class PackageServiceLevel:
455
+ def __init__(self, data: list[ServiceLevelDeclType]) -> None:
456
+ self._data = data
457
+
458
+ @property
459
+ def level(self) -> ServiceLevelKind:
460
+ for r in self._data:
461
+ if r["level"] == "untested":
462
+ continue
463
+ return r["level"]
464
+ return "untested"
465
+
466
+ @property
467
+ def has_known_issues(self) -> bool:
468
+ for r in self._data:
469
+ if r["level"] == "known_issue":
470
+ return True
471
+ return False
472
+
473
+ @property
474
+ def known_issues(self) -> Iterator[ServiceLevelDeclType]:
475
+ for r in self._data:
476
+ if r["level"] == "known_issue":
477
+ yield r
478
+
479
+ def render_known_issues(
480
+ self,
481
+ msg_store: RepoMessageStore,
482
+ lang_code: str,
483
+ ) -> Iterator[str]:
484
+ for x in self.known_issues:
485
+ if "msgid" not in x:
486
+ # malformed known issue declaration, but let's not panic
487
+ yield ""
488
+ continue
489
+ yield msg_store.render_message(x["msgid"], lang_code, x.get("params", {}))
490
+
491
+
492
+ class PackageManifest:
493
+ def __init__(
494
+ self,
495
+ doc: tomlkit.TOMLDocument | InputPackageManifestType,
496
+ ) -> None:
497
+ self._raw_doc = doc if isinstance(doc, tomlkit.TOMLDocument) else None
498
+ self._data = _translate_to_manifest_v1(cast(InputPackageManifestType, doc))
499
+ if "kind" not in self._data:
500
+ self._data["kind"] = [k for k in ALL_PACKAGE_KINDS if k in self._data]
501
+
502
+ @classmethod
503
+ def load_toml(cls, stream: BinaryIO) -> "Self":
504
+ return cls(tomlkit.load(stream))
505
+
506
+ @classmethod
507
+ def load_from_path(cls, p: pathlib.Path) -> "Self":
508
+ suffix = p.suffix.lower()
509
+ match suffix:
510
+ case ".toml":
511
+ with open(p, "rb") as fp:
512
+ return cls.load_toml(fp)
513
+ case _:
514
+ raise RuntimeError(
515
+ f"unrecognized package manifest file extension: '{p.suffix}'"
516
+ )
517
+
518
+ def to_raw(self) -> PackageManifestType:
519
+ return deepcopy(self._data)
520
+
521
+ @property
522
+ def raw_doc(self) -> tomlkit.TOMLDocument | None:
523
+ return self._raw_doc
524
+
525
+ @property
526
+ def slug(self) -> str | None:
527
+ return self._data["metadata"].get("slug")
528
+
529
+ @property
530
+ def kind(self) -> list[PackageKind]:
531
+ return self._data["kind"]
532
+
533
+ def has_kind(self, k: PackageKind) -> bool:
534
+ return k in self._data["kind"]
535
+
536
+ @property
537
+ def desc(self) -> str:
538
+ return self._data["metadata"]["desc"]
539
+
540
+ @property
541
+ def doc_uri(self) -> str | None:
542
+ return self._data["metadata"].get("doc_uri")
543
+
544
+ @property
545
+ def vendor_name(self) -> str:
546
+ return self._data["metadata"]["vendor"]["name"]
547
+
548
+ @property
549
+ def upstream_version(self) -> str | None:
550
+ return self._data["metadata"].get("upstream_version")
551
+
552
+ # TODO: vendor_eula
553
+
554
+ @property
555
+ def service_level(self) -> PackageServiceLevel:
556
+ return PackageServiceLevel(self._data["metadata"].get("service_level", []))
557
+
558
+ @cached_property
559
+ def distfiles(self) -> dict[str, DistfileDecl]:
560
+ return {x["name"]: DistfileDecl(x) for x in self._data["distfiles"]}
561
+
562
+ @cached_property
563
+ def binary_metadata(self) -> BinaryDecl | None:
564
+ if not self.has_kind("binary"):
565
+ return None
566
+ if "binary" not in self._data:
567
+ return None
568
+ return BinaryDecl(self._data["binary"])
569
+
570
+ @cached_property
571
+ def blob_metadata(self) -> BlobDecl | None:
572
+ if not self.has_kind("blob"):
573
+ return None
574
+ if "blob" not in self._data:
575
+ return None
576
+ return BlobDecl(self._data["blob"])
577
+
578
+ @cached_property
579
+ def source_metadata(self) -> SourceDecl | None:
580
+ if not self.has_kind("source"):
581
+ return None
582
+ if "source" not in self._data:
583
+ return None
584
+ return SourceDecl(self._data["source"])
585
+
586
+ @cached_property
587
+ def toolchain_metadata(self) -> ToolchainDecl | None:
588
+ if not self.has_kind("toolchain"):
589
+ return None
590
+ if "toolchain" not in self._data:
591
+ return None
592
+ return ToolchainDecl(self._data["toolchain"])
593
+
594
+ @cached_property
595
+ def emulator_metadata(self) -> EmulatorDecl | None:
596
+ if not self.has_kind("emulator"):
597
+ return None
598
+ if "emulator" not in self._data:
599
+ return None
600
+ return EmulatorDecl(self._data["emulator"])
601
+
602
+ @cached_property
603
+ def provisionable_metadata(self) -> ProvisionableDecl | None:
604
+ if not self.has_kind("provisionable"):
605
+ return None
606
+ if "provisionable" not in self._data:
607
+ return None
608
+ return ProvisionableDecl(self._data["provisionable"])
609
+
610
+
611
+ class BoundPackageManifest(PackageManifest):
612
+ def __init__(
613
+ self,
614
+ category: str,
615
+ name: str,
616
+ ver: str,
617
+ data: InputPackageManifestType,
618
+ repo: "MetadataRepo",
619
+ ) -> None:
620
+ super().__init__(data)
621
+
622
+ self.category = category
623
+ self.name = name
624
+ self.ver = ver
625
+ self._semver = Version.parse(ver)
626
+ self.repo = repo
627
+
628
+ @property
629
+ def repo_id(self) -> str:
630
+ return self.repo.repo_id
631
+
632
+ @property
633
+ def semver(self) -> Version:
634
+ return self._semver
635
+
636
+ @property
637
+ def is_prerelease(self) -> bool:
638
+ return is_prerelease(self._semver)
639
+
640
+ @property
641
+ def name_for_installation(self) -> str:
642
+ return f"{self.name}-{self.ver}"
643
+
644
+
645
+ PRERELEASE_TAGS_RE: Final = re.compile(r"^(?:alpha|beta|pre|rc)")
646
+
647
+
648
+ def is_prerelease(sv: Version) -> bool:
649
+ if sv.prerelease is None:
650
+ return False
651
+
652
+ # only consider "(alpha|beta|pre|rc).*" versions as prerelease, to accommodate
653
+ # various semver "hacks" as incorporated by upstream(s), and ourselves
654
+ # ("ruyi.YYYYMMDD" are used as ordinary datestamps that affects sorting
655
+ # order, in contrast to build tags).
656
+ # see https://github.com/ruyisdk/ruyi/issues/156
657
+ return PRERELEASE_TAGS_RE.match(sv.prerelease) is not None