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,55 @@
1
+ #!/usr/bin/env python3
2
+ # Regenerates data.py from fresh contents.
3
+
4
+ import base64
5
+ import pathlib
6
+ from typing import Any
7
+ import zlib
8
+
9
+
10
+ def make_payload_from_file(path: pathlib.Path) -> str:
11
+ with open(path, "rb") as fp:
12
+ content = fp.read()
13
+
14
+ return base64.b64encode(zlib.compress(content, 9)).decode("ascii")
15
+
16
+
17
+ def main() -> None:
18
+ self_path = pathlib.Path(__file__).parent.resolve()
19
+ bundled_resource_root = self_path / ".." / ".." / "resources" / "bundled"
20
+
21
+ resources: dict[str, str] = {}
22
+ template_names: dict[str, str] = {}
23
+ for f in bundled_resource_root.iterdir():
24
+ if not f.is_file():
25
+ continue
26
+
27
+ resources[f.name] = make_payload_from_file(f)
28
+
29
+ if f.suffix.lower() == ".jinja":
30
+ # strip the .jinja suffix for the template name
31
+ template_names[f.stem] = f.name
32
+
33
+ with open(self_path / "data.py", "w", encoding="utf-8") as fp:
34
+
35
+ def p(*args: Any) -> None:
36
+ return print(*args, file=fp)
37
+
38
+ p("# NOTE: This file is auto-generated. DO NOT EDIT!")
39
+ p("# Update by running the __main__.py alongside this file\n")
40
+
41
+ p("from typing import Final\n\n")
42
+
43
+ p("RESOURCES: Final = {")
44
+ for filename, payload in sorted(resources.items()):
45
+ p(f' "{filename}": b"{payload}", # fmt: skip')
46
+ p("}\n")
47
+
48
+ p("TEMPLATES: Final = {")
49
+ for stem, full_filename in sorted(template_names.items()):
50
+ p(f' "{stem}": RESOURCES["{full_filename}"],')
51
+ p("}")
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,26 @@
1
+ # NOTE: This file is auto-generated. DO NOT EDIT!
2
+ # Update by running the __main__.py alongside this file
3
+
4
+ from typing import Final
5
+
6
+
7
+ RESOURCES: Final = {
8
+ "_ruyi_completion": b"eNqlVntP40YQ/z+fYmosCC0h5KpKd7Tmjoc5ogYSxUlPd4FaTryJV9i7kXdNLofoZ++M7TgmmOiutVDYncdvZuexOzsTGc19NoU4WfJabQc6/J6FS/BAxzyKmA++XAh4YLHiUoCcQqD1XB03mzOug2R8iPrNe67CZXLf9OIZwYVMs+Y4lONm5HHxnOqpwM13iHfoN935UgdSNEpS6MUVC+doEgLvgYHPleZiomEesyn/CtNYRqADBjLmMy68ENQk5nMNWoL3ILkPwosYTGQYcvJa0bn6iQAlI6YDLmYHECUIOQOZ6HmiEQhi5vOYTVIqT6HIgs/GyQyUjpkXIYjP5kz4qaJI+Q9emDCKinvaP3cv7LPhx0OUa0/htP/xvHvd69gD2x06tjuwr3uX7Y7tAFegmD6ARDHQLJpPecgUTNGHdu/8sObmEXFLEXEpO/gj6vvwWAP8+BRGI2h8A8N8fNVU48mAu7vfyVORatG3Fd/lQrAYQT8YhULMdBJn+lOe/gvlBIOuM9ctw6xH93SQ/UzHLfvjDC66w4FL7tycXtsonKsZP+ZIZnIifWaZ71PKxNOwRktJcfSCkvoOJinWnmq177BZGeJ1eitjSm7C25PdFrw72X0DrZOmzx6aIglDeEPUP4p9qsJCxV7X3X1XqYSxpwO84n/hdRao9qVjmXu3R61f90rUrE0swyifT9D5vjhX7l9232l3byoPuAr/qnFVwSnRrLpZJ8OGib8G3BYy9FFFuJ32DZXA2fDy0u5XSvS67ZsBipwP+073hUi5tKzWFqbrXNmdjmV8U4GxVWzY6/Vtx3Gd3ul5BeS2cgHzMQvoccN8XMjYV6PW3dPT/v5G1IRUMtZWvYI+9yaszMCUcNXwdCNkntLw2+HbjTzQt8JryHy51s/7c51dTG0pQZRa6x+oj/6+vb3bP4bdXRKIPD0JyPWUObKax3cvCyA3nDnccGBvr9Kq6zOKyZgRcBETo1wlxMn8Hn0gVrYl4HT/skGyaG1k6qh8UEKXeP//Ilc+YgdB0T4VR9nMe9VZqEj6dq/z+bvrulhvL+1iUyk2+NwrsGi9tX5TqU/d/sVZ3z7901nprSn/u4Poxf6RFjKf7/9rQ7XKfZTVsvkefrLgqLo2E4Fv6jplBY+FufKGW5YFLUSiFsjapFBtlPrArDa2qrdGUW+b9ZNe1uUHZNsFuwpECY+WOJV5SZhaoSQU20t4LYjZEFc0zw58QhP4UIYhznHjZTqxfFFBqRtBLRU+3QfIwrFkwfHBwgEHFzrIIYxQer6XaDlNxMSABSFywTVHVBwTkUECiO+hnqGCTCz0NL7eUhyARKPxgit2kAOSE/m8tvDwNmA4QxkNBFAyHYmMfCI1aAaL2QyHP4TCkYycIu0c58UhUvrKofI9WnrtTHwQXLKIQ6jQ7KumQviZfK4YlF6LcjGUPLumVoP09uTkxYF//wJg/FlP", # fmt: skip
9
+ "binfmt.conf.jinja": b"eNpNUrlu20AQ7f0VAwgCbECk+wQp06Zwl0oakUNyoD2UvQRC1r/7LUkhrkjuzjuHOzqrG2w6Wo0ddd4NOlLMmvhshAYfKE0a6SPPSkVDymxIHN68s+LS4WVH6ijOMYntm5XrSQO05fSTogidLLtNqu1PC3EvidXEFhR/faY4+Wx6CjIqyAJ5J+QHyFcbxvibuhGYznDgpN69xrdD1Wa68QwOvl6DvwbltPqefYYGuIKecwXQO5yEop0QzPAIjXeS1B0I0+qKvwhoqp7YbDjhFIY5zAtVFDOgAV4GTgF9NP8wd6JbgLKE9uW+X3ThYqy+gkRvivTHehBp/wD57yfx/b7MtfB3NTwfHVuhxwMcDemw3qHmBQXcH49QaeJUrRCb6GnigiNfR4v29VaifF8NFUYZ2GIk4EJ2Sa1s+/KhR3qguZtUQNT5EKRLW/JaVhSUlLSLP+Bgi3Y5UKnwp71WsfX4+gabO2pqqAtC/MKz0CcWuiaCqb6i92s8fCFhU7+2Er79gUes69nDf9gX9HfteA==", # fmt: skip
10
+ "meson-cross.ini.jinja": b"eNptkMFqwzAQRO/6ioUQ0tJazaU9FPoLvfUUglDkNV4iS0IrB4eQf69k2ZRCbmI083Z3NvDDCJbO+Ck2YgMDsnfAmMYATWOiZ246sgi3G1zQXVT0PsH9/jYbq0GSI5BSzoBvxJYXzF6+f8j9ixTicCKnIyEfhYEv2GWaMRmzEyaEVZimWbltG+h8hDNeXyHo1AO5ClS6bSmRd9qqFSgp4cBPz7C9iwzJoQxZiHN4RaJrC7XYtkAd8JXnW7JwOI1kU5PH+FDwecsQsaNp4azWiiqknC+5EH3AmOpdgz6jSt5b02tyam5tuezR17rX/1Xys1b8aHLzN1oces9JDdr05PBYcrmHkrLkxqn0OqpOD2SvaxnRG2TOFdTax4d6GaBd5aRkcSd+ATx1t64=", # fmt: skip
11
+ "prompt.venv-created.txt.jinja": b"eNp1kTFvwkAMhff7FV7YStjbqWLqwMZSISSOO4dYJOfIvoRGiP9eXwR0KTedbOv5e8/bBmEkyYNvAZP9OHWYMpBC4gsEQZ8xVs598wCdn8CHTKPVgDIcJ1AeJFA6QTYh3/fCvVBp3+eIE2gQ6k0ylSG3Owli2h8p7VZ7iCQYMsv0Bj5FiPjUN3FKI5+L+EGGiZZ/zUPlroslUA06qTBnWNyc+4RaUJtHbdUL1vRTrPhWGYxtJDUgjHeW/5xX7mt270dPrT+25iTPszW3LV8KTe9z8+4c2LubuV6fILeb2ZrpMEUDLGDbFyHPWIE7VLhQbsDCjtOy8xFhvfFnhMzchsYbbU0tupLQBtUiDcKqc7GCdYPh/MoOzFA1iw2w4oezdaWujwiKhhpEymXNoP5kp02aZQjleFq5Xz21xdY=", # fmt: skip
12
+ "ruyi-activate.bash.jinja": b"eNp9lN1u2kAQhe/3KSYGqiSVRUnuqIhEGqRESgLCBClKI2uxx/JKZh2t126o6yfqI/QuT9bxH2CHhBvEzpn1d2YO7sDCFxF4IkBYx5GGFUIcoQu/hPbBiMJYOQgrIfsq3giTO1okXKMBp54K17DikX/KOrAJY3C4lKEGFUsQGlyh0NHBhjEmPHgCo5tejq1r25o+zH9MzMyAEZ19M+D5O2gfJQP6oOOHYDzSZQVL9XSdE0aOEi96CD+79XHefPHlrGx8pUeenzNPMFaAulijwvEJpIWoAwoj1BAGLqBMhArlGqWGhCvBVwFGleqIcM3f0E2X4/lXO4Nn6tOxkhFoFSOQHSoAMbnoBFzRsLgGHgRFO1WrfnJszx8eb+zp7ZU9Gy+uh3QZ+YU9w/knL42MblNrbMv4+hIqXai2Z7HMfTQ7imLuvzQhZMID4eYDoKcV7bQhx8ei7tPewFRwdtF3MenLmOg/xbcGH8Bbgya7NXiPbg0+Jq9qNXhZL8rLyf3y0Jk9m0/vZosa94mAiXVQRUqG0sWIFpWvH1vEHbAw8KAWHLWwTA9a4anZMkYhL0VCKQww4Y3gtDPXpGBsCz9K050T+AO0hixj1aB2tllztzTiMhTVj62wT//MYVXbDwr7LAAHl5/28mHuhnw/vpuA2ctYe+4NB6WstpH2TMAgwsN9RveYXhco+Rphz4JxYpSN0iWA3vth1Psu3yNlMnfFqxtrfHk7qURmO6ONpBFCSl8kYtvsvv2d0+5g/8ryquztHzTke2mmRPwHax2J0Q==", # fmt: skip
13
+ "ruyi-cache.toml.jinja": b"eNptUs1uhCAYvPsUX2nMtknjoccmve0e9tK9eDOGsIJK1r8CmhjjuxcUF6uexG+GmWHgFX5u4eULwpxLSHnBQH9LUpGMUbj3INqeB3C+GRpcztfwxfOihCQ5o7j7jL3BB57CLytbfOcV+ONz+Q1oGBwyjkhzWUU13R/tvkbUxhMbEqs6AzjxYINOZmkt4PEBHWjJDR5wxUr59q5VtPFDO9oMnTH3ZnezX+P/k6xFFREZU3Iltkq0gFY/9lRdF0lOeGXOSLlYHKPTFjnFUwo3TguSySP+BFj6XFMXIQfLXoq6VijW0XbTIz0LzYr7O9DiWZJgXklFigLrqJP0ZuaEN8Ch7LPobbtJSXHJFKFEEVyS5rjmHQvZwlHsUSZXpzR/S7PT3WDVNvoRuxpWU9epy/cHatoHPQ==", # fmt: skip
14
+ "ruyi-venv.toml.jinja": b"eNqLTs7PS8tMj+UqKMpPy8xJVbBVUKquVoDxamuVuKpVFTLTFIori4vy80sUdFVruWBsiFoYD6JWVyE1LwWoHqgMAMZMHuE=", # fmt: skip
15
+ "toolchain.cmake.jinja": b"eNqFkVFrwjAUhd/7Ky6IMGF274M9dG2cZWlT0joUhJDVaIuajKQTR+l/X6zOWccwb+F+5+Scmx5MjIBNuRaPTs/pQb7lawGu68Lc3g5nGPiR94pYRgj2x14Ys1GI0VNdw07IHdNKVdA0D5VSm7zgpXSPFnOn7g+hXIL5Mi3Tb64MwzjNPIxZQtEonB4Mf9CmOcmFXFiHs9LGchwjqrujQTpLMxSx2IsQ4FJ+7gd/hwklPkpTQsH6f2iVC2OUti9csj7zSZTYVi2V59fj6bQL7PctUfcv+w1tzF/NKIwDRgnJWOJlY+iUa5UX1YzgOi9gaXMVgi+ENsDlwn7Ku+a6FAZKCVUhoOJ6JSqr3JVaya2Q1X1XbfutNN+eBYUyHfzffCwiATrs6oV6EcToDdHBDRiHz9SjMyAxnt1iw9jHkwCd2G+4PsQk", # fmt: skip
16
+ }
17
+
18
+ TEMPLATES: Final = {
19
+ "binfmt.conf": RESOURCES["binfmt.conf.jinja"],
20
+ "meson-cross.ini": RESOURCES["meson-cross.ini.jinja"],
21
+ "prompt.venv-created.txt": RESOURCES["prompt.venv-created.txt.jinja"],
22
+ "ruyi-activate.bash": RESOURCES["ruyi-activate.bash.jinja"],
23
+ "ruyi-cache.toml": RESOURCES["ruyi-cache.toml.jinja"],
24
+ "ruyi-venv.toml": RESOURCES["ruyi-venv.toml.jinja"],
25
+ "toolchain.cmake": RESOURCES["toolchain.cmake.jinja"],
26
+ }
File without changes
@@ -0,0 +1,88 @@
1
+ import os
2
+ import sys
3
+ from typing import Any, TypeGuard
4
+
5
+ from tomlkit import document, table
6
+ from tomlkit.items import AoT, Table
7
+ from tomlkit.toml_document import TOMLDocument
8
+
9
+ from ..log import RuyiLogger
10
+ from . import checksum
11
+ from .pkg_manifest import DistfileDeclType, RestrictKind
12
+
13
+
14
+ def do_admin_checksum(
15
+ logger: RuyiLogger,
16
+ files: list[os.PathLike[Any]],
17
+ format: str,
18
+ restrict: list[str],
19
+ ) -> int:
20
+ if not validate_restrict_kinds(restrict):
21
+ logger.F(f"invalid restrict kinds given: {restrict}")
22
+ return 1
23
+
24
+ entries = [gen_distfile_entry(logger, f, restrict) for f in files]
25
+ if format == "toml":
26
+ doc = emit_toml_distfiles_section(entries)
27
+ logger.D(f"{doc}")
28
+ sys.stdout.write(doc.as_string())
29
+ return 0
30
+
31
+ raise RuntimeError("unrecognized output format; should never happen")
32
+
33
+
34
+ def validate_restrict_kinds(input: list[str]) -> TypeGuard[list[RestrictKind]]:
35
+ for x in input:
36
+ match x:
37
+ case "fetch" | "mirror":
38
+ pass
39
+ case _:
40
+ return False
41
+ return True
42
+
43
+
44
+ def gen_distfile_entry(
45
+ logger: RuyiLogger,
46
+ path: os.PathLike[Any],
47
+ restrict: list[RestrictKind],
48
+ ) -> DistfileDeclType:
49
+ logger.D(f"generating distfile entry for {path}")
50
+ with open(path, "rb") as fp:
51
+ filesize = os.stat(fp.fileno()).st_size
52
+ c = checksum.Checksummer(fp, {})
53
+ checksums = c.compute(kinds=checksum.SUPPORTED_CHECKSUM_KINDS)
54
+
55
+ obj: DistfileDeclType = {
56
+ "name": os.path.basename(path),
57
+ "size": filesize,
58
+ "checksums": checksums,
59
+ }
60
+
61
+ if restrict:
62
+ obj["restrict"] = restrict
63
+
64
+ return obj
65
+
66
+
67
+ def emit_toml_distfiles_section(x: list[DistfileDeclType]) -> TOMLDocument:
68
+ doc = document()
69
+
70
+ arr: list[Table] = []
71
+ for dd in x:
72
+ t = table()
73
+ t.add("name", dd["name"])
74
+ t.add("size", dd["size"])
75
+ if r := dd.get("restrict"):
76
+ t.add("restrict", r)
77
+ t.add("checksums", emit_toml_checksums(dd["checksums"]))
78
+ arr.append(t)
79
+
80
+ doc.add("distfiles", AoT(arr))
81
+ return doc
82
+
83
+
84
+ def emit_toml_checksums(x: dict[str, str]) -> Table:
85
+ t = table()
86
+ for k in sorted(x.keys()):
87
+ t.add(k, x[k])
88
+ return t
@@ -0,0 +1,83 @@
1
+ import argparse
2
+ import pathlib
3
+ from typing import TYPE_CHECKING, cast
4
+
5
+ from ..cli.cmd import AdminCommand
6
+
7
+ if TYPE_CHECKING:
8
+ from ..cli.completion import ArgumentParser
9
+ from ..config import GlobalConfig
10
+
11
+
12
+ class AdminChecksumCommand(
13
+ AdminCommand,
14
+ cmd="checksum",
15
+ help="Generate a checksum section for a manifest file for the distfiles given",
16
+ ):
17
+ @classmethod
18
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
19
+ p.add_argument(
20
+ "--format",
21
+ "-f",
22
+ type=str,
23
+ choices=["toml"],
24
+ default="toml",
25
+ help="Format of checksum section to generate in",
26
+ )
27
+ p.add_argument(
28
+ "--restrict",
29
+ type=str,
30
+ default="",
31
+ help="the 'restrict' field to use for all mentioned distfiles, separated with comma",
32
+ )
33
+ p.add_argument(
34
+ "file",
35
+ type=str,
36
+ nargs="+",
37
+ help="Path to the distfile(s) to checksum",
38
+ )
39
+
40
+ @classmethod
41
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
42
+ from .admin_checksum import do_admin_checksum
43
+
44
+ logger = cfg.logger
45
+ files = args.file
46
+ format = args.format
47
+ restrict_str = cast(str, args.restrict)
48
+ restrict = restrict_str.split(",") if restrict_str else []
49
+
50
+ return do_admin_checksum(logger, files, format, restrict)
51
+
52
+
53
+ class AdminFormatManifestCommand(
54
+ AdminCommand,
55
+ cmd="format-manifest",
56
+ help="Format the given package manifests into canonical TOML representation",
57
+ ):
58
+ @classmethod
59
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
60
+ p.add_argument(
61
+ "file",
62
+ type=str,
63
+ nargs="+",
64
+ help="Path to the distfile(s) to generate manifest for",
65
+ )
66
+
67
+ @classmethod
68
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
69
+ from .canonical_dump import dumps_canonical_package_manifest_toml
70
+ from .pkg_manifest import PackageManifest
71
+
72
+ files = args.file
73
+
74
+ for f in files:
75
+ p = pathlib.Path(f)
76
+ pm = PackageManifest.load_from_path(p)
77
+ d = dumps_canonical_package_manifest_toml(pm)
78
+
79
+ dest_path = p.with_suffix(".toml")
80
+ with open(dest_path, "w", encoding="utf-8") as fp:
81
+ fp.write(d)
82
+
83
+ return 0
ruyi/ruyipkg/atom.py ADDED
@@ -0,0 +1,184 @@
1
+ import abc
2
+ import re
3
+ from typing import Final, Iterator, Literal, Tuple
4
+
5
+ import semver
6
+
7
+ from .pkg_manifest import BoundPackageManifest, is_prerelease
8
+ from .protocols import ProvidesPackageManifests
9
+
10
+
11
+ AtomKind = Literal["name"] | Literal["expr"] | Literal["slug"]
12
+
13
+ RE_ATOM_EXPR: Final = re.compile(r"^([^:(]+)\((.+)\)$")
14
+ RE_ATOM_NAME: Final = re.compile(r"^[^:()]+$")
15
+
16
+
17
+ class Atom(abc.ABC):
18
+ def __init__(self, s: str, kind: AtomKind) -> None:
19
+ self._s = s
20
+ self._kind: AtomKind = kind
21
+
22
+ @property
23
+ def input_str(self) -> str:
24
+ return self._s
25
+
26
+ @property
27
+ def kind(self) -> AtomKind:
28
+ return self._kind
29
+
30
+ @classmethod
31
+ def parse(cls, s: str) -> "SlugAtom | NameAtom | ExprAtom":
32
+ if s.startswith("slug:"):
33
+ return SlugAtom(s)
34
+
35
+ if s.startswith("name:"):
36
+ return NameAtom(s, s[5:]) # strip the "name:" prefix
37
+
38
+ if match := RE_ATOM_EXPR.match(s):
39
+ return ExprAtom(s, match[1], match[2])
40
+
41
+ # fallback
42
+ if match := RE_ATOM_NAME.match(s):
43
+ return NameAtom(s, s)
44
+
45
+ raise ValueError(f"invalid atom: '{s}'")
46
+
47
+ @abc.abstractmethod
48
+ def match_in_repo(
49
+ self,
50
+ repo: ProvidesPackageManifests,
51
+ include_prerelease_vers: bool,
52
+ ) -> BoundPackageManifest | None:
53
+ raise NotImplementedError
54
+
55
+ @abc.abstractmethod
56
+ def iter_in_repo(
57
+ self,
58
+ repo: ProvidesPackageManifests,
59
+ include_prerelease_vers: bool,
60
+ ) -> Iterator[BoundPackageManifest]:
61
+ raise NotImplementedError
62
+
63
+
64
+ def split_category(name: str) -> Tuple[str | None, str]:
65
+ fragments = name.split("/", 1)
66
+ if len(fragments) == 2:
67
+ return (fragments[0], fragments[1])
68
+ return (None, name)
69
+
70
+
71
+ class NameAtom(Atom):
72
+ def __init__(self, s: str, name: str) -> None:
73
+ super().__init__(s, "name")
74
+
75
+ self.category, self.name = split_category(name)
76
+
77
+ def match_in_repo(
78
+ self,
79
+ repo: ProvidesPackageManifests,
80
+ include_prerelease_vers: bool,
81
+ ) -> BoundPackageManifest | None:
82
+ # return the latest version of the package named self.name in the given repo
83
+ try:
84
+ return repo.get_pkg_latest_ver(
85
+ self.name,
86
+ self.category,
87
+ include_prerelease_vers,
88
+ )
89
+ except KeyError:
90
+ return None
91
+
92
+ def iter_in_repo(
93
+ self,
94
+ repo: ProvidesPackageManifests,
95
+ include_prerelease_vers: bool,
96
+ ) -> Iterator[BoundPackageManifest]:
97
+ # return all versions of the package named self.name in the given repo
98
+ for pm in repo.iter_pkg_vers(self.name, self.category):
99
+ if not is_prerelease(pm.semver) or include_prerelease_vers:
100
+ yield pm
101
+
102
+
103
+ def fix_version_matcher_for_semver2(match_expr: str) -> str:
104
+ # equivalent of https://github.com/python-semver/python-semver/pull/362
105
+ # for semver 2.x
106
+ if match_expr and match_expr[0] in "0123456789":
107
+ return f"=={match_expr}"
108
+ return match_expr
109
+
110
+
111
+ class ExprAtom(Atom):
112
+ def __init__(self, s: str, name: str, expr: str) -> None:
113
+ super().__init__(s, "expr")
114
+ self.exprs = expr.split(",")
115
+
116
+ if semver.__version__ < "3":
117
+ self.exprs = list(map(fix_version_matcher_for_semver2, self.exprs))
118
+
119
+ self.category, self.name = split_category(name)
120
+
121
+ def _is_pm_matching_my_exprs(self, pm: BoundPackageManifest) -> bool:
122
+ for e in self.exprs:
123
+ if not pm.semver.match(e):
124
+ return False
125
+ return True
126
+
127
+ def match_in_repo(
128
+ self,
129
+ repo: ProvidesPackageManifests,
130
+ include_prerelease_vers: bool,
131
+ ) -> BoundPackageManifest | None:
132
+ matching_pms = {
133
+ pm.ver: pm
134
+ for pm in repo.iter_pkg_vers(self.name, self.category)
135
+ if self._is_pm_matching_my_exprs(pm)
136
+ }
137
+ if not matching_pms:
138
+ return None
139
+
140
+ semvers = [pm.semver for pm in matching_pms.values()]
141
+ if not include_prerelease_vers:
142
+ semvers = [sv for sv in semvers if not is_prerelease(sv)]
143
+ if not semvers:
144
+ return None
145
+ latest_ver = max(semvers)
146
+ return matching_pms[str(latest_ver)]
147
+
148
+ def iter_in_repo(
149
+ self,
150
+ repo: ProvidesPackageManifests,
151
+ include_prerelease_vers: bool,
152
+ ) -> Iterator[BoundPackageManifest]:
153
+ for pm in repo.iter_pkg_vers(self.name, self.category):
154
+ if self._is_pm_matching_my_exprs(pm):
155
+ if not is_prerelease(pm.semver) or include_prerelease_vers:
156
+ yield pm
157
+
158
+
159
+ class SlugAtom(Atom):
160
+ def __init__(self, s: str) -> None:
161
+ super().__init__(s, "slug")
162
+ self.slug = s[5:] # strip the "slug:" prefix
163
+
164
+ def match_in_repo(
165
+ self,
166
+ repo: ProvidesPackageManifests,
167
+ include_prerelease_vers: bool,
168
+ ) -> BoundPackageManifest | None:
169
+ pm = repo.get_pkg_by_slug(self.slug)
170
+ if pm and is_prerelease(pm.semver):
171
+ return pm if include_prerelease_vers else None
172
+ return pm
173
+
174
+ def iter_in_repo(
175
+ self,
176
+ repo: ProvidesPackageManifests,
177
+ include_prerelease_vers: bool,
178
+ ) -> Iterator[BoundPackageManifest]:
179
+ pm = repo.get_pkg_by_slug(self.slug)
180
+ if pm is None:
181
+ return None
182
+
183
+ if is_prerelease(pm.semver) and include_prerelease_vers:
184
+ yield pm
@@ -0,0 +1,212 @@
1
+ import enum
2
+ import sys
3
+ from typing import Iterable, TypedDict, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from typing_extensions import Self
7
+
8
+ from ..config import GlobalConfig
9
+ from ..utils.porcelain import PorcelainEntity, PorcelainEntityType
10
+ from .distfile import Distfile
11
+ from .host import get_native_host
12
+ from .list_filter import ListFilter
13
+ from .pkg_manifest import BoundPackageManifest, PackageManifestType
14
+ from .repo import MetadataRepo
15
+
16
+
17
+ if sys.version_info >= (3, 11):
18
+
19
+ class PkgRemark(enum.StrEnum):
20
+ Latest = "latest"
21
+ LatestPreRelease = "latest-prerelease"
22
+ NoBinaryForCurrentHost = "no-binary-for-current-host"
23
+ PreRelease = "prerelease"
24
+ HasKnownIssue = "known-issue"
25
+ Downloaded = "downloaded"
26
+ Installed = "installed"
27
+
28
+ def as_rich_markup(self) -> str:
29
+ match self:
30
+ case self.Latest:
31
+ return "latest"
32
+ case self.LatestPreRelease:
33
+ return "latest-prerelease"
34
+ case self.NoBinaryForCurrentHost:
35
+ return "[red]no binary for current host[/]"
36
+ case self.PreRelease:
37
+ return "prerelease"
38
+ case self.HasKnownIssue:
39
+ return "[yellow]has known issue[/]"
40
+ case self.Downloaded:
41
+ return "[green]downloaded[/]"
42
+ case self.Installed:
43
+ return "[green]installed[/]"
44
+ return ""
45
+
46
+ else:
47
+
48
+ class PkgRemark(str, enum.Enum):
49
+ Latest = "latest"
50
+ LatestPreRelease = "latest-prerelease"
51
+ NoBinaryForCurrentHost = "no-binary-for-current-host"
52
+ PreRelease = "prerelease"
53
+ HasKnownIssue = "known-issue"
54
+ Downloaded = "downloaded"
55
+ Installed = "installed"
56
+
57
+ def as_rich_markup(self) -> str:
58
+ match self:
59
+ case self.Latest:
60
+ return "latest"
61
+ case self.LatestPreRelease:
62
+ return "latest-prerelease"
63
+ case self.NoBinaryForCurrentHost:
64
+ return "[red]no binary for current host[/]"
65
+ case self.PreRelease:
66
+ return "prerelease"
67
+ case self.HasKnownIssue:
68
+ return "[yellow]has known issue[/]"
69
+ case self.Downloaded:
70
+ return "[green]downloaded[/]"
71
+ case self.Installed:
72
+ return "[green]installed[/]"
73
+ return ""
74
+
75
+
76
+ class AugmentedPkgManifest:
77
+ def __init__(
78
+ self,
79
+ pm: BoundPackageManifest,
80
+ remarks: list[PkgRemark],
81
+ ) -> None:
82
+ self.pm = pm
83
+ self.remarks = remarks
84
+ self._is_downloaded = PkgRemark.Downloaded in remarks
85
+ self._is_installed = PkgRemark.Installed in remarks
86
+
87
+ def to_porcelain(self) -> "PorcelainPkgVersionV1":
88
+ return {
89
+ "semver": str(self.pm.semver),
90
+ "pm": self.pm.to_raw(),
91
+ "remarks": self.remarks,
92
+ "is_downloaded": self._is_downloaded,
93
+ "is_installed": self._is_installed,
94
+ }
95
+
96
+
97
+ class AugmentedPkg:
98
+ def __init__(self) -> None:
99
+ self.versions: list[AugmentedPkgManifest] = []
100
+
101
+ def add_version(self, v: AugmentedPkgManifest) -> None:
102
+ if self.versions:
103
+ if v.pm.category != self.category or v.pm.name != self.name:
104
+ raise ValueError("cannot add a version of a different pkg")
105
+ self.versions.append(v)
106
+
107
+ @property
108
+ def category(self) -> str | None:
109
+ return self.versions[0].pm.category if self.versions else None
110
+
111
+ @property
112
+ def name(self) -> str | None:
113
+ return self.versions[0].pm.name if self.versions else None
114
+
115
+ @classmethod
116
+ def yield_from_repo(
117
+ cls,
118
+ cfg: GlobalConfig,
119
+ mr: MetadataRepo,
120
+ filters: ListFilter,
121
+ *,
122
+ ensure_repo: bool = True,
123
+ ) -> "Iterable[Self]":
124
+ rgs = cfg.ruyipkg_global_state
125
+ native_host = str(get_native_host())
126
+
127
+ for category, pkg_name, pkg_vers in mr.iter_pkgs(ensure_repo=ensure_repo):
128
+ if not filters.check_pkg_name(cfg, mr, category, pkg_name):
129
+ continue
130
+
131
+ pkg = cls()
132
+
133
+ semvers = [pm.semver for pm in pkg_vers.values()]
134
+ semvers.sort(reverse=True)
135
+ found_latest = False
136
+ for i, sv in enumerate(semvers):
137
+ # TODO: support filter ops against individual versions
138
+
139
+ pm = pkg_vers[str(sv)]
140
+
141
+ latest = False
142
+ latest_prerelease = i == 0
143
+ prerelease = pm.is_prerelease
144
+ if not found_latest and not prerelease:
145
+ latest = True
146
+ found_latest = True
147
+
148
+ remarks: list[PkgRemark] = []
149
+ if latest or latest_prerelease or prerelease:
150
+ if prerelease:
151
+ remarks.append(PkgRemark.PreRelease)
152
+ if latest:
153
+ remarks.append(PkgRemark.Latest)
154
+ if latest_prerelease and not latest:
155
+ remarks.append(PkgRemark.LatestPreRelease)
156
+ if pm.service_level.has_known_issues:
157
+ remarks.append(PkgRemark.HasKnownIssue)
158
+ if bm := pm.binary_metadata:
159
+ if not bm.is_available_for_current_host:
160
+ remarks.append(PkgRemark.NoBinaryForCurrentHost)
161
+ if _is_pkg_fully_downloaded(pm):
162
+ remarks.append(PkgRemark.Downloaded)
163
+
164
+ host = native_host if bm is not None else ""
165
+ is_installed = rgs.is_package_installed(
166
+ pm.repo_id,
167
+ pm.category,
168
+ pm.name,
169
+ str(sv),
170
+ host,
171
+ )
172
+ if is_installed:
173
+ remarks.append(PkgRemark.Installed)
174
+
175
+ pkg.add_version(AugmentedPkgManifest(pm, remarks))
176
+
177
+ yield pkg
178
+
179
+ def to_porcelain(self) -> "PorcelainPkgListOutputV1":
180
+ return {
181
+ "ty": PorcelainEntityType.PkgListOutputV1,
182
+ "category": self.category or "",
183
+ "name": self.name or "",
184
+ "vers": [x.to_porcelain() for x in self.versions],
185
+ }
186
+
187
+
188
+ class PorcelainPkgVersionV1(TypedDict):
189
+ semver: str
190
+ pm: PackageManifestType
191
+ remarks: list[PkgRemark]
192
+ is_downloaded: bool
193
+ is_installed: bool
194
+
195
+
196
+ class PorcelainPkgListOutputV1(PorcelainEntity):
197
+ category: str
198
+ name: str
199
+ vers: list[PorcelainPkgVersionV1]
200
+
201
+
202
+ def _is_pkg_fully_downloaded(pm: BoundPackageManifest) -> bool:
203
+ dfs = pm.distfiles
204
+ if not dfs:
205
+ return True
206
+
207
+ for df_decl in dfs.values():
208
+ df = Distfile(df_decl, pm.repo)
209
+ if not df.is_downloaded():
210
+ return False
211
+
212
+ return True