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
ruyi/log/__init__.py ADDED
@@ -0,0 +1,272 @@
1
+ import abc
2
+ import datetime
3
+ from functools import cached_property
4
+ import io
5
+ import sys
6
+ import time
7
+ from typing import Any, TextIO, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ # too heavy at package import time
11
+ from rich.console import Console, RenderableType
12
+ from rich.text import Text
13
+
14
+ from ..utils.global_mode import ProvidesGlobalMode
15
+ from ..utils.porcelain import PorcelainEntity, PorcelainEntityType, PorcelainOutput
16
+
17
+
18
+ class PorcelainLog(PorcelainEntity):
19
+ t: int
20
+ """Timestamp of the message line in microseconds"""
21
+
22
+ lvl: str
23
+ """Log level of the message line (one of D, F, I, W)"""
24
+
25
+ msg: str
26
+ """Message content"""
27
+
28
+
29
+ def log_time_formatter(x: datetime.datetime) -> "Text":
30
+ from rich.text import Text
31
+
32
+ return Text(f"debug: [{x.isoformat()}]")
33
+
34
+
35
+ def _make_porcelain_log(
36
+ t: int,
37
+ lvl: str,
38
+ message: "RenderableType",
39
+ sep: str,
40
+ *objects: Any,
41
+ ) -> PorcelainLog:
42
+ from rich.console import Console
43
+
44
+ with io.StringIO() as buf:
45
+ tmp_console = Console(file=buf)
46
+ tmp_console.print(message, *objects, sep=sep, end="")
47
+ return {
48
+ "ty": PorcelainEntityType.LogV1,
49
+ "t": t,
50
+ "lvl": lvl,
51
+ "msg": buf.getvalue(),
52
+ }
53
+
54
+
55
+ class RuyiLogger(metaclass=abc.ABCMeta):
56
+ def __init__(self) -> None:
57
+ pass
58
+
59
+ @property
60
+ @abc.abstractmethod
61
+ def log_console(self) -> "Console":
62
+ raise NotImplementedError
63
+
64
+ @abc.abstractmethod
65
+ def stdout(
66
+ self,
67
+ message: "RenderableType",
68
+ *objects: Any,
69
+ sep: str = " ",
70
+ end: str = "\n",
71
+ ) -> None:
72
+ raise NotImplementedError
73
+
74
+ @abc.abstractmethod
75
+ def D(
76
+ self,
77
+ message: "RenderableType",
78
+ *objects: Any,
79
+ sep: str = " ",
80
+ end: str = "\n",
81
+ _stack_offset_delta: int = 0,
82
+ ) -> None:
83
+ raise NotImplementedError
84
+
85
+ @abc.abstractmethod
86
+ def F(
87
+ self,
88
+ message: "RenderableType",
89
+ *objects: Any,
90
+ sep: str = " ",
91
+ end: str = "\n",
92
+ ) -> None:
93
+ raise NotImplementedError
94
+
95
+ @abc.abstractmethod
96
+ def I( # noqa: E743 # the name intentionally mimics Android logging for brevity
97
+ self,
98
+ message: "RenderableType",
99
+ *objects: Any,
100
+ sep: str = " ",
101
+ end: str = "\n",
102
+ ) -> None:
103
+ raise NotImplementedError
104
+
105
+ @abc.abstractmethod
106
+ def W(
107
+ self,
108
+ message: "RenderableType",
109
+ *objects: Any,
110
+ sep: str = " ",
111
+ end: str = "\n",
112
+ ) -> None:
113
+ raise NotImplementedError
114
+
115
+
116
+ class RuyiConsoleLogger(RuyiLogger):
117
+ def __init__(
118
+ self,
119
+ gm: ProvidesGlobalMode,
120
+ stdout: TextIO = sys.stdout,
121
+ stderr: TextIO = sys.stderr,
122
+ ) -> None:
123
+ super().__init__()
124
+
125
+ self._gm = gm
126
+ self._stdout = stdout
127
+ self._stderr = stderr
128
+
129
+ @cached_property
130
+ def _stdout_console(self) -> "Console":
131
+ from rich.console import Console
132
+
133
+ return Console(
134
+ file=self._stdout,
135
+ highlight=False,
136
+ soft_wrap=True,
137
+ )
138
+
139
+ @cached_property
140
+ def _debug_console(self) -> "Console":
141
+ from rich.console import Console
142
+
143
+ return Console(
144
+ file=self._stderr,
145
+ log_time_format=log_time_formatter,
146
+ soft_wrap=True,
147
+ )
148
+
149
+ @cached_property
150
+ def _log_console(self) -> "Console":
151
+ from rich.console import Console
152
+
153
+ return Console(
154
+ file=self._stderr,
155
+ highlight=False,
156
+ soft_wrap=True,
157
+ )
158
+
159
+ @cached_property
160
+ def _porcelain_sink(self) -> PorcelainOutput:
161
+ return PorcelainOutput(self._stderr.buffer)
162
+
163
+ @property
164
+ def log_console(self) -> "Console":
165
+ return self._log_console
166
+
167
+ def _emit_porcelain_log(
168
+ self,
169
+ lvl: str,
170
+ message: "RenderableType",
171
+ sep: str = " ",
172
+ *objects: Any,
173
+ ) -> None:
174
+ t = int(time.time() * 1000000)
175
+ obj = _make_porcelain_log(t, lvl, message, sep, *objects)
176
+ self._porcelain_sink.emit(obj)
177
+
178
+ def stdout(
179
+ self,
180
+ message: "RenderableType",
181
+ *objects: Any,
182
+ sep: str = " ",
183
+ end: str = "\n",
184
+ ) -> None:
185
+ return self._stdout_console.print(message, *objects, sep=sep, end=end)
186
+
187
+ def D(
188
+ self,
189
+ message: "RenderableType",
190
+ *objects: Any,
191
+ sep: str = " ",
192
+ end: str = "\n",
193
+ _stack_offset_delta: int = 0,
194
+ ) -> None:
195
+ if not self._gm.is_debug:
196
+ return
197
+
198
+ if self._gm.is_porcelain:
199
+ return self._emit_porcelain_log("D", message, sep, *objects)
200
+
201
+ return self._debug_console.log(
202
+ message,
203
+ *objects,
204
+ sep=sep,
205
+ end=end,
206
+ _stack_offset=2 + _stack_offset_delta,
207
+ )
208
+
209
+ def F(
210
+ self,
211
+ message: "RenderableType",
212
+ *objects: Any,
213
+ sep: str = " ",
214
+ end: str = "\n",
215
+ ) -> None:
216
+ if self._gm.is_porcelain:
217
+ return self._emit_porcelain_log("F", message, sep, *objects)
218
+
219
+ return self.log_console.print(
220
+ f"[bold red]fatal error:[/] {message}",
221
+ *objects,
222
+ sep=sep,
223
+ end=end,
224
+ )
225
+
226
+ def I( # noqa: E743 # the name intentionally mimics Android logging for brevity
227
+ self,
228
+ message: "RenderableType",
229
+ *objects: Any,
230
+ sep: str = " ",
231
+ end: str = "\n",
232
+ ) -> None:
233
+ if self._gm.is_porcelain:
234
+ return self._emit_porcelain_log("I", message, sep, *objects)
235
+
236
+ return self.log_console.print(
237
+ f"[bold green]info:[/] {message}",
238
+ *objects,
239
+ sep=sep,
240
+ end=end,
241
+ )
242
+
243
+ def W(
244
+ self,
245
+ message: "RenderableType",
246
+ *objects: Any,
247
+ sep: str = " ",
248
+ end: str = "\n",
249
+ ) -> None:
250
+ if self._gm.is_porcelain:
251
+ return self._emit_porcelain_log("W", message, sep, *objects)
252
+
253
+ return self.log_console.print(
254
+ f"[bold yellow]warn:[/] {message}",
255
+ *objects,
256
+ sep=sep,
257
+ end=end,
258
+ )
259
+
260
+
261
+ def humanize_list(
262
+ obj: list[str] | set[str],
263
+ *,
264
+ sep: str = ", ",
265
+ item_color: str | None = None,
266
+ empty_prompt: str = "(none)",
267
+ ) -> str:
268
+ if not obj:
269
+ return empty_prompt
270
+ if item_color is None:
271
+ return sep.join(obj)
272
+ return sep.join(f"[{item_color}]{x}[/]" for x in obj)
ruyi/mux/.gitignore ADDED
@@ -0,0 +1 @@
1
+ !venv
ruyi/mux/__init__.py ADDED
File without changes
ruyi/mux/runtime.py ADDED
@@ -0,0 +1,213 @@
1
+ import atexit
2
+ import os
3
+ import re
4
+ import shlex
5
+ from typing import Final, List, NoReturn
6
+
7
+ from ..config import GlobalConfig
8
+ from ..utils.global_mode import ProvidesGlobalMode
9
+ from .venv_cfg import RuyiVenvConfig
10
+
11
+
12
+ def _run_exit_handlers_and_execv(
13
+ path: str,
14
+ argv: list[str],
15
+ ) -> NoReturn:
16
+ # run all exit handlers before execv
17
+ # crucially this includes our telemetry handler so we don't lose telemetry
18
+ # events in mux mode
19
+ atexit._run_exitfuncs()
20
+ os.execv(path, argv)
21
+
22
+
23
+ def mux_main(
24
+ gm: ProvidesGlobalMode,
25
+ gc: GlobalConfig,
26
+ argv: List[str],
27
+ ) -> int | NoReturn:
28
+ basename = os.path.basename(gm.argv0)
29
+ logger = gc.logger
30
+ logger.D(f"mux mode: argv = {argv}, basename = {basename}")
31
+
32
+ vcfg = RuyiVenvConfig.load_from_venv(gm, logger)
33
+ if vcfg is None:
34
+ logger.F("the Ruyi toolchain mux is not configured")
35
+ logger.I("check out `ruyi venv` for making a virtual environment")
36
+ return 1
37
+
38
+ direct_symlink_target = resolve_direct_symlink_target(gm.argv0, vcfg)
39
+ if direct_symlink_target is not None:
40
+ logger.D(
41
+ f"detected direct symlink target: {direct_symlink_target}, overriding basename"
42
+ )
43
+ basename = direct_symlink_target
44
+
45
+ if basename == "ruyi-qemu":
46
+ return mux_qemu_main(gc, vcfg, argv)
47
+
48
+ # match the basename with one of the configured target tuples
49
+ target_tuple: str | None = None
50
+ binpath: str | None = None
51
+ toolchain_sysroot: str | None = None
52
+ toolchain_flags: str | None = None
53
+ gcc_install_dir: str | None = None
54
+
55
+ # prefer v1+ cached info which is lossless
56
+ if md := vcfg.resolve_cmd_metadata_with_cache(basename):
57
+ target_tuple = md["target_tuple"]
58
+ binpath = md["dest"]
59
+ if target_tuple:
60
+ tgt_data = vcfg.targets.get(target_tuple)
61
+ if tgt_data is None:
62
+ logger.F(
63
+ f"internal error: no target data for tuple [yellow]{target_tuple}[/]"
64
+ )
65
+ return 1
66
+ toolchain_sysroot = tgt_data.get("toolchain_sysroot")
67
+ toolchain_flags = tgt_data.get("toolchain_flags")
68
+ gcc_install_dir = tgt_data.get("gcc_install_dir")
69
+ else:
70
+ toolchain_bindir: str | None = None
71
+ for tgt_tuple, tgt_data in vcfg.targets.items():
72
+ if not basename.startswith(f"{tgt_tuple}-"):
73
+ continue
74
+
75
+ logger.D(f"matched target '{tgt_tuple}', data {tgt_data}")
76
+ target_tuple = tgt_tuple
77
+ toolchain_bindir = tgt_data["toolchain_bindir"]
78
+ toolchain_sysroot = tgt_data.get("toolchain_sysroot")
79
+ toolchain_flags = tgt_data.get("toolchain_flags")
80
+ gcc_install_dir = tgt_data.get("gcc_install_dir")
81
+ break
82
+
83
+ if toolchain_bindir is None:
84
+ # should not happen
85
+ logger.F(
86
+ f"internal error: no bindir configured for target [yellow]{target_tuple}[/]"
87
+ )
88
+ return 1
89
+
90
+ binpath = os.path.join(toolchain_bindir, basename)
91
+
92
+ if target_tuple is None:
93
+ logger.F(f"no configured target found for command [yellow]{basename}[/]")
94
+ return 1
95
+
96
+ logger.D(f"binary to exec: {binpath}")
97
+
98
+ argv_to_insert: list[str] | None = None
99
+ if is_proxying_to_cc(basename):
100
+ logger.D(f"{basename} is considered a CC")
101
+
102
+ argv_to_insert = []
103
+
104
+ if is_proxying_to_clang(basename):
105
+ logger.D(f"adding target for clang: {target_tuple}")
106
+ argv_to_insert.append(f"--target={target_tuple}")
107
+ if gcc_install_dir is not None:
108
+ logger.D(f"informing clang of GCC install dir: {gcc_install_dir}")
109
+ argv_to_insert.append(f"--gcc-install-dir={gcc_install_dir}")
110
+
111
+ if toolchain_flags is not None:
112
+ argv_to_insert.extend(shlex.split(toolchain_flags))
113
+ logger.D(f"parsed toolchain flags: {argv_to_insert}")
114
+
115
+ if toolchain_sysroot is not None:
116
+ logger.D(f"adding sysroot: {toolchain_sysroot}")
117
+ argv_to_insert.extend(("--sysroot", toolchain_sysroot))
118
+
119
+ new_argv = [binpath]
120
+ if argv_to_insert:
121
+ new_argv.extend(argv_to_insert)
122
+ if len(argv) > 1:
123
+ new_argv.extend(argv[1:])
124
+
125
+ ensure_venv_in_path(vcfg)
126
+
127
+ logger.D(f"exec-ing with argv {new_argv}")
128
+ return _run_exit_handlers_and_execv(binpath, new_argv)
129
+
130
+
131
+ # TODO: dedup with venv provision logic (into a command name parser)
132
+ CC_ARGV0_RE: Final = re.compile(
133
+ r"(?:^|-)(?:g?cc|c\+\+|g\+\+|cpp|clang|clang\+\+|clang-cl|clang-cpp)(?:-[0-9.]+)?$"
134
+ )
135
+
136
+
137
+ def resolve_direct_symlink_target(argv0: str, vcfg: RuyiVenvConfig) -> str | None:
138
+ direct_symlink_target = resolve_argv0_symlink(argv0, vcfg)
139
+ if direct_symlink_target is not None and os.path.sep in direct_symlink_target:
140
+ # we're not designed to handle such indirections
141
+ return None
142
+ return direct_symlink_target
143
+
144
+
145
+ def resolve_argv0_symlink(argv0: str, vcfg: RuyiVenvConfig) -> str | None:
146
+ if os.path.sep in argv0:
147
+ # argv[0] contains path information that we can just use
148
+ try:
149
+ return os.readlink(argv0)
150
+ except OSError:
151
+ # argv[0] is not a symlink
152
+ return None
153
+
154
+ # argv[0] is bare command name, in which case we expect venv root to
155
+ # be available, so we can just check f'{venv_root}/bin/{argv[0]}'.
156
+ # we're guaranteed a venv_root because of the vcfg init logic.
157
+ try:
158
+ return os.readlink(vcfg.venv_root / "bin" / argv0)
159
+ except OSError:
160
+ return None
161
+
162
+
163
+ def is_proxying_to_cc(argv0: str) -> bool:
164
+ return CC_ARGV0_RE.search(argv0) is not None
165
+
166
+
167
+ def is_proxying_to_clang(basename: str) -> bool:
168
+ return "clang" in basename
169
+
170
+
171
+ def mux_qemu_main(
172
+ gc: GlobalConfig,
173
+ vcfg: RuyiVenvConfig,
174
+ argv: List[str],
175
+ ) -> int | NoReturn:
176
+ logger = gc.logger
177
+ binpath = vcfg.qemu_bin
178
+ if binpath is None:
179
+ logger.F("this virtual environment has no QEMU-like emulator configured")
180
+ return 1
181
+
182
+ if vcfg.profile_emu_env is not None:
183
+ logger.D(f"seeding QEMU environment with {vcfg.profile_emu_env}")
184
+ for k, v in vcfg.profile_emu_env.items():
185
+ os.environ[k] = v
186
+
187
+ logger.D(f"QEMU binary to exec: {binpath}")
188
+
189
+ new_argv = [binpath]
190
+ if len(argv) > 1:
191
+ new_argv.extend(argv[1:])
192
+
193
+ logger.D(f"exec-ing with argv {new_argv}")
194
+ return _run_exit_handlers_and_execv(binpath, new_argv)
195
+
196
+
197
+ def ensure_venv_in_path(vcfg: RuyiVenvConfig) -> None:
198
+ venv_root = vcfg.venv_root
199
+ venv_bindir = venv_root / "bin"
200
+ venv_bindir = venv_bindir.resolve()
201
+
202
+ orig_path = os.environ.get("PATH", "")
203
+ for p in orig_path.split(os.pathsep):
204
+ try:
205
+ if os.path.samefile(p, venv_bindir):
206
+ # TODO: what if our bindir actually comes after the system ones?
207
+ return
208
+ except FileNotFoundError:
209
+ # maybe the PATH entry is stale
210
+ continue
211
+
212
+ # we're not in PATH, so prepend the bindir to PATH
213
+ os.environ["PATH"] = f"{venv_bindir}:{orig_path}" if orig_path else str(venv_bindir)
@@ -0,0 +1,12 @@
1
+ from os import PathLike
2
+ from typing import Any, TypedDict
3
+
4
+
5
+ class ConfiguredTargetTuple(TypedDict):
6
+ target: str
7
+ toolchain_root: PathLike[Any]
8
+ toolchain_sysroot: PathLike[Any] | None
9
+ toolchain_flags: str
10
+ binutils_flavor: str
11
+ cc_flavor: str
12
+ gcc_install_dir: PathLike[Any] | None
@@ -0,0 +1,41 @@
1
+ import os
2
+ from typing import Any, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from typing_extensions import Self
6
+
7
+ from ...ruyipkg.pkg_manifest import EmulatorProgDecl
8
+ from ...ruyipkg.profile import ProfileProxy
9
+
10
+
11
+ class ResolvedEmulatorProg:
12
+ def __init__(
13
+ self,
14
+ display_name: str,
15
+ binfmt_misc_str: str | None,
16
+ env: dict[str, str] | None,
17
+ ) -> None:
18
+ self.display_name = display_name
19
+ self.binfmt_misc_str = binfmt_misc_str
20
+ self.env = env
21
+
22
+ @classmethod
23
+ def new(
24
+ cls,
25
+ prog: EmulatorProgDecl,
26
+ prog_install_root: os.PathLike[Any],
27
+ profile: ProfileProxy,
28
+ sysroot: os.PathLike[Any] | None,
29
+ ) -> "Self":
30
+ return cls(
31
+ get_display_name_for_emulator(prog, prog_install_root),
32
+ prog.get_binfmt_misc_str(prog_install_root),
33
+ profile.get_env_config_for_emu_flavor(prog.flavor, sysroot),
34
+ )
35
+
36
+
37
+ def get_display_name_for_emulator(
38
+ prog: EmulatorProgDecl,
39
+ prog_install_root: os.PathLike[Any],
40
+ ) -> str:
41
+ return f"{os.path.basename(prog.relative_path)} from {prog_install_root}"