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,91 @@
1
+ import enum
2
+ import re
3
+ import sys
4
+ from typing import Final
5
+
6
+ RE_TARBALL: Final = re.compile(r"\.tar(?:\.gz|\.bz2|\.lz4|\.xz|\.zst)?$")
7
+
8
+
9
+ if sys.version_info >= (3, 11):
10
+
11
+ class UnpackMethod(enum.StrEnum):
12
+ UNKNOWN = ""
13
+ AUTO = "auto"
14
+ TAR_AUTO = "tar.auto"
15
+
16
+ RAW = "raw"
17
+ GZ = "gz"
18
+ BZ2 = "bz2"
19
+ LZ4 = "lz4"
20
+ XZ = "xz"
21
+ ZST = "zst"
22
+
23
+ TAR = "tar"
24
+ TAR_GZ = "tar.gz"
25
+ TAR_BZ2 = "tar.bz2"
26
+ TAR_LZ4 = "tar.lz4"
27
+ TAR_XZ = "tar.xz"
28
+ TAR_ZST = "tar.zst"
29
+
30
+ ZIP = "zip"
31
+ DEB = "deb"
32
+
33
+ else:
34
+
35
+ class UnpackMethod(str, enum.Enum):
36
+ UNKNOWN = ""
37
+ AUTO = "auto"
38
+ TAR_AUTO = "tar.auto"
39
+
40
+ RAW = "raw"
41
+ GZ = "gz"
42
+ BZ2 = "bz2"
43
+ LZ4 = "lz4"
44
+ XZ = "xz"
45
+ ZST = "zst"
46
+
47
+ TAR = "tar"
48
+ TAR_GZ = "tar.gz"
49
+ TAR_BZ2 = "tar.bz2"
50
+ TAR_LZ4 = "tar.lz4"
51
+ TAR_XZ = "tar.xz"
52
+ TAR_ZST = "tar.zst"
53
+
54
+ ZIP = "zip"
55
+ DEB = "deb"
56
+
57
+
58
+ class UnrecognizedPackFormatError(Exception):
59
+ def __init__(self, filename: str) -> None:
60
+ self.filename = filename
61
+
62
+ def __str__(self) -> str:
63
+ return f"don't know how to unpack file {self.filename}"
64
+
65
+
66
+ def determine_unpack_method(
67
+ filename: str,
68
+ ) -> UnpackMethod:
69
+ filename_lower = filename.lower()
70
+ if m := RE_TARBALL.search(filename_lower):
71
+ return UnpackMethod(m.group(0)[1:])
72
+ if filename_lower.endswith(".deb"):
73
+ return UnpackMethod.DEB
74
+ if filename_lower.endswith(".zip"):
75
+ return UnpackMethod.ZIP
76
+ if filename_lower.endswith(".gz"):
77
+ # bare gzip file
78
+ return UnpackMethod.GZ
79
+ if filename_lower.endswith(".bz2"):
80
+ # bare bzip2 file
81
+ return UnpackMethod.BZ2
82
+ if filename_lower.endswith(".lz4"):
83
+ # bare lz4 file
84
+ return UnpackMethod.LZ4
85
+ if filename_lower.endswith(".xz"):
86
+ # bare xz file
87
+ return UnpackMethod.XZ
88
+ if filename_lower.endswith(".zst"):
89
+ # bare zstd file
90
+ return UnpackMethod.ZST
91
+ return UnpackMethod.UNKNOWN
@@ -0,0 +1,54 @@
1
+ import argparse
2
+ from typing import TYPE_CHECKING
3
+
4
+ from ..cli.cmd import RootCommand
5
+
6
+ if TYPE_CHECKING:
7
+ from ..cli.completion import ArgumentParser
8
+ from ..config import GlobalConfig
9
+
10
+
11
+ class UpdateCommand(
12
+ RootCommand,
13
+ cmd="update",
14
+ help="Update RuyiSDK repo and packages",
15
+ ):
16
+ @classmethod
17
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
18
+ pass
19
+
20
+ @classmethod
21
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
22
+ from . import news
23
+ from .state import BoundInstallationStateStore
24
+
25
+ logger = cfg.logger
26
+ mr = cfg.repo
27
+ mr.sync()
28
+
29
+ # check for upgradable packages
30
+ bis = BoundInstallationStateStore(cfg.ruyipkg_global_state, mr)
31
+ upgradable = list(bis.iter_upgradable_pkgs(cfg.include_prereleases))
32
+
33
+ if upgradable:
34
+ logger.stdout(
35
+ "\nNewer versions are available for some of your installed packages:\n"
36
+ )
37
+ for pm, new_ver in upgradable:
38
+ logger.stdout(
39
+ f" - [bold]{pm.category}/{pm.name}[/]: [yellow]{pm.ver}[/] -> [green]{new_ver}[/]"
40
+ )
41
+ logger.stdout(
42
+ """
43
+ Re-run [yellow]ruyi install[/] to upgrade, and don't forget to re-create any affected
44
+ virtual environments."""
45
+ )
46
+
47
+ # check if there are new newsitems
48
+ unread_newsitems = mr.news_store().list(True)
49
+ if unread_newsitems:
50
+ logger.stdout(f"\nThere are {len(unread_newsitems)} new news item(s):\n")
51
+ news.print_news_item_titles(logger, unread_newsitems, cfg.lang_code)
52
+ logger.stdout("\nYou can read them with [yellow]ruyi news read[/].")
53
+
54
+ return 0
File without changes
@@ -0,0 +1,72 @@
1
+ from typing import Iterable, TypeAlias, TypedDict, TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from typing_extensions import NotRequired
5
+
6
+ from .event import TelemetryEvent
7
+ from .node_info import NodeInfo
8
+
9
+
10
+ class AggregatedTelemetryEvent(TypedDict):
11
+ time_bucket: str
12
+ kind: str
13
+ params: list[tuple[str, str]]
14
+ count: int
15
+
16
+
17
+ class UploadPayload(TypedDict):
18
+ fmt: int
19
+ nonce: str
20
+ ruyi_version: str
21
+ installation: "NotRequired[NodeInfo | None]"
22
+ events: list[AggregatedTelemetryEvent]
23
+
24
+
25
+ def stringify_param_val(v: object) -> str:
26
+ if v is None:
27
+ return "null"
28
+ if isinstance(v, bool):
29
+ return "1" if v else "0"
30
+ if isinstance(v, bytes):
31
+ return v.decode("utf-8")
32
+ if isinstance(v, str):
33
+ return v
34
+ return str(v)
35
+
36
+
37
+ AggregateKey: TypeAlias = tuple[tuple[str, str], ...]
38
+
39
+
40
+ def _make_aggregate_key(ev: TelemetryEvent) -> AggregateKey:
41
+ param_list = [(k, stringify_param_val(v)) for k, v in ev["params"].items()]
42
+ param_list.sort()
43
+ return tuple([("", ev["kind"])] + param_list)
44
+
45
+
46
+ def aggregate_events(
47
+ events: Iterable[TelemetryEvent],
48
+ ) -> Iterable[AggregatedTelemetryEvent]:
49
+ # dict[time_bucket, dict[AggregateKey, count]]
50
+ buf: dict[str, dict[AggregateKey, int]] = {}
51
+ for raw_ev in events:
52
+ time_bucket = raw_ev.get("time_bucket")
53
+ if time_bucket is None:
54
+ continue
55
+ if time_bucket not in buf:
56
+ buf[time_bucket] = {}
57
+
58
+ agg_key = _make_aggregate_key(raw_ev)
59
+ if agg_key not in buf[time_bucket]:
60
+ buf[time_bucket][agg_key] = 1
61
+ else:
62
+ buf[time_bucket][agg_key] += 1
63
+
64
+ for time_bucket in sorted(buf.keys()):
65
+ bucket_events = buf[time_bucket]
66
+ for agg_key in sorted(bucket_events.keys()):
67
+ yield {
68
+ "time_bucket": time_bucket,
69
+ "kind": agg_key[0][1],
70
+ "params": list(agg_key[1:]),
71
+ "count": bucket_events[agg_key],
72
+ }
@@ -0,0 +1,41 @@
1
+ from typing import TypedDict, TypeGuard, TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from typing_extensions import NotRequired
5
+
6
+
7
+ class TelemetryEvent(TypedDict):
8
+ fmt: int
9
+ time_bucket: "NotRequired[str]" # canonically "YYYYMMDDHHMM"
10
+ kind: str
11
+ params: dict[str, object]
12
+
13
+
14
+ def is_telemetry_event(x: object) -> TypeGuard[TelemetryEvent]:
15
+ if not isinstance(x, dict):
16
+ return False
17
+
18
+ if not 3 <= len(x.keys()) <= 4:
19
+ return False
20
+
21
+ try:
22
+ if not isinstance(x["fmt"], int):
23
+ return False
24
+ if not isinstance(x["kind"], str):
25
+ return False
26
+ if not isinstance(x["params"], dict):
27
+ return False
28
+ except KeyError:
29
+ return False
30
+
31
+ try:
32
+ if not isinstance(x["time_bucket"], str):
33
+ return False
34
+ if len(x["time_bucket"]) != 12:
35
+ return False
36
+ if not x["time_bucket"].isdigit():
37
+ return False
38
+ except KeyError:
39
+ pass
40
+
41
+ return True
@@ -0,0 +1,192 @@
1
+ import glob
2
+ import os
3
+ import platform
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ from typing import Final, Mapping, TypedDict, TYPE_CHECKING
8
+ import uuid
9
+
10
+ if TYPE_CHECKING:
11
+ from typing_extensions import NotRequired
12
+
13
+ from ..utils.ci import probe_for_ci
14
+
15
+
16
+ class NodeInfo(TypedDict):
17
+ v: int
18
+ report_uuid: str
19
+
20
+ arch: str
21
+ ci: str
22
+ libc_name: str
23
+ libc_ver: str
24
+ os: str
25
+ os_release_id: str
26
+ os_release_version_id: str
27
+ shell: str
28
+
29
+ riscv_machine: "NotRequired[RISCVMachineInfo]"
30
+
31
+
32
+ class RISCVMachineInfo(TypedDict):
33
+ model_name: str
34
+ cpu_count: int
35
+ isa: str
36
+ uarch: str
37
+ uarch_csr: str
38
+ mmu: str
39
+
40
+
41
+ def probe_for_libc() -> tuple[str, str]:
42
+ r = platform.libc_ver()
43
+ if r[0] and r[1]:
44
+ return r
45
+
46
+ # check for musl ld.so at the upstream standard paths, because
47
+ # platform.libc_ver() as of Python 3.12 does not know how to handle musl
48
+ #
49
+ # see https://wiki.musl-libc.org/guidelines-for-distributions
50
+ musl_lds = glob.glob("/lib/ld-musl-*.so.1")
51
+ if musl_lds:
52
+ # run it and check for "Version *.*.*"
53
+ # in case of multiple hits (hybrid-architecture sysroot?), hope the
54
+ # first one that successfully returns something is the native one
55
+ for p in musl_lds:
56
+ if ver := _try_get_musl_ver(p):
57
+ return ("musl", ver)
58
+
59
+ return ("unknown", "unknown")
60
+
61
+
62
+ _MUSL_VERSION_RE: Final = re.compile(rb"(?m)^Version ([0-9.]+)$")
63
+
64
+
65
+ def _try_get_musl_ver(ldso_path: str) -> str | None:
66
+ res = subprocess.run([ldso_path], stderr=subprocess.PIPE)
67
+ if m := _MUSL_VERSION_RE.search(res.stderr):
68
+ return m.group(1).decode("ascii", "ignore")
69
+ return None
70
+
71
+
72
+ def _try_parse_hex(v: str) -> int | None:
73
+ if not v.startswith("0x"):
74
+ return None
75
+ try:
76
+ return int(v[2:], 16)
77
+ except ValueError:
78
+ return None
79
+
80
+
81
+ def probe_for_riscv_machine_info(
82
+ model_name: str | None = None,
83
+ cpuinfo_data: str | None = None,
84
+ ) -> RISCVMachineInfo | None:
85
+ if model_name is None:
86
+ try:
87
+ with open(
88
+ "/sys/firmware/devicetree/base/model",
89
+ "r",
90
+ encoding="utf-8",
91
+ ) as fp:
92
+ model_name = fp.read().strip(" \n\t\x00")
93
+ except Exception:
94
+ pass
95
+
96
+ if not model_name:
97
+ model_name = "unknown"
98
+
99
+ if cpuinfo_data is None:
100
+ try:
101
+ with open("/proc/cpuinfo", "r", encoding="utf-8") as fp:
102
+ cpuinfo_data = fp.read()
103
+ except Exception:
104
+ pass
105
+
106
+ cpu_count = 0
107
+ isa, mmu, uarch = "unknown", "unknown", "unknown"
108
+ mvendorid: int | None = None
109
+ marchid: int | None = None
110
+ mimpid: int | None = None
111
+ if cpuinfo_data is not None:
112
+ for line in cpuinfo_data.split("\n"):
113
+ if not line:
114
+ continue
115
+
116
+ try:
117
+ k, v = line.split(": ", 1)
118
+ except ValueError:
119
+ # malformed line: non-empty but no ": "
120
+ continue
121
+
122
+ k = k.strip(" \t")
123
+ v = v.strip()
124
+
125
+ match k:
126
+ case "processor":
127
+ cpu_count += 1
128
+ case "isa":
129
+ isa = v
130
+ case "mmu":
131
+ mmu = v
132
+ case "uarch":
133
+ uarch = v
134
+ case "mvendorid":
135
+ mvendorid = _try_parse_hex(v)
136
+ case "marchid":
137
+ marchid = _try_parse_hex(v)
138
+ case "mimpid":
139
+ mimpid = _try_parse_hex(v)
140
+ case _:
141
+ continue
142
+
143
+ if mvendorid is not None and marchid is not None and mimpid is not None:
144
+ uarch_csr = f"{mvendorid:x}:{marchid:x}:{mimpid:x}"
145
+ else:
146
+ uarch_csr = "unknown"
147
+
148
+ return {
149
+ "model_name": model_name,
150
+ "cpu_count": cpu_count,
151
+ "isa": isa,
152
+ "mmu": mmu,
153
+ "uarch": uarch,
154
+ "uarch_csr": uarch_csr,
155
+ }
156
+
157
+
158
+ def probe_for_shell(os_environ: Mapping[str, str]) -> str:
159
+ if x := os_environ.get("SHELL"):
160
+ return os.path.basename(x)
161
+ return "unknown"
162
+
163
+
164
+ def gather_node_info(report_uuid: uuid.UUID | None = None) -> NodeInfo:
165
+ arch = platform.machine()
166
+ libc = probe_for_libc()
167
+ os_release = platform.freedesktop_os_release()
168
+
169
+ os_version = os_release.get("VERSION_CODENAME") # works on e.g. Debian
170
+ if not os_version:
171
+ os_version = os_release.get("VERSION_ID") # works on e.g. openEuler, Gentoo
172
+ if not os_version:
173
+ os_version = "unknown"
174
+
175
+ data: NodeInfo = {
176
+ "v": 1,
177
+ "report_uuid": report_uuid.hex if report_uuid is not None else uuid.uuid4().hex,
178
+ "arch": arch,
179
+ "ci": probe_for_ci(os.environ) or "maybe-not",
180
+ "libc_name": libc[0],
181
+ "libc_ver": libc[1],
182
+ "os": sys.platform,
183
+ "os_release_id": os_release.get("ID", "unknown"),
184
+ "os_release_version_id": os_version,
185
+ "shell": probe_for_shell(os.environ),
186
+ }
187
+
188
+ if arch.startswith("riscv"):
189
+ if riscv_machine := probe_for_riscv_machine_info():
190
+ data["riscv_machine"] = riscv_machine
191
+
192
+ return data