ruyi 0.39.0a20250731__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/__init__.py +21 -0
- ruyi/__main__.py +98 -0
- ruyi/cli/__init__.py +5 -0
- ruyi/cli/builtin_commands.py +14 -0
- ruyi/cli/cmd.py +224 -0
- ruyi/cli/completer.py +50 -0
- ruyi/cli/completion.py +26 -0
- ruyi/cli/config_cli.py +153 -0
- ruyi/cli/main.py +111 -0
- ruyi/cli/self_cli.py +295 -0
- ruyi/cli/user_input.py +127 -0
- ruyi/cli/version_cli.py +45 -0
- ruyi/config/__init__.py +401 -0
- ruyi/config/editor.py +92 -0
- ruyi/config/errors.py +76 -0
- ruyi/config/news.py +39 -0
- ruyi/config/schema.py +197 -0
- ruyi/device/__init__.py +0 -0
- ruyi/device/provision.py +591 -0
- ruyi/device/provision_cli.py +40 -0
- ruyi/log/__init__.py +272 -0
- ruyi/mux/.gitignore +1 -0
- ruyi/mux/__init__.py +0 -0
- ruyi/mux/runtime.py +213 -0
- ruyi/mux/venv/__init__.py +12 -0
- ruyi/mux/venv/emulator_cfg.py +41 -0
- ruyi/mux/venv/maker.py +782 -0
- ruyi/mux/venv/venv_cli.py +92 -0
- ruyi/mux/venv_cfg.py +214 -0
- ruyi/pluginhost/__init__.py +0 -0
- ruyi/pluginhost/api.py +206 -0
- ruyi/pluginhost/ctx.py +222 -0
- ruyi/pluginhost/paths.py +135 -0
- ruyi/pluginhost/plugin_cli.py +37 -0
- ruyi/pluginhost/unsandboxed.py +246 -0
- ruyi/py.typed +0 -0
- ruyi/resource_bundle/__init__.py +20 -0
- ruyi/resource_bundle/__main__.py +55 -0
- ruyi/resource_bundle/data.py +26 -0
- ruyi/ruyipkg/__init__.py +0 -0
- ruyi/ruyipkg/admin_checksum.py +88 -0
- ruyi/ruyipkg/admin_cli.py +83 -0
- ruyi/ruyipkg/atom.py +184 -0
- ruyi/ruyipkg/augmented_pkg.py +212 -0
- ruyi/ruyipkg/canonical_dump.py +320 -0
- ruyi/ruyipkg/checksum.py +39 -0
- ruyi/ruyipkg/cli_completion.py +42 -0
- ruyi/ruyipkg/distfile.py +208 -0
- ruyi/ruyipkg/entity.py +387 -0
- ruyi/ruyipkg/entity_cli.py +123 -0
- ruyi/ruyipkg/entity_provider.py +273 -0
- ruyi/ruyipkg/fetch.py +271 -0
- ruyi/ruyipkg/host.py +55 -0
- ruyi/ruyipkg/install.py +554 -0
- ruyi/ruyipkg/install_cli.py +150 -0
- ruyi/ruyipkg/list.py +126 -0
- ruyi/ruyipkg/list_cli.py +79 -0
- ruyi/ruyipkg/list_filter.py +173 -0
- ruyi/ruyipkg/msg.py +99 -0
- ruyi/ruyipkg/news.py +123 -0
- ruyi/ruyipkg/news_cli.py +78 -0
- ruyi/ruyipkg/news_store.py +183 -0
- ruyi/ruyipkg/pkg_manifest.py +657 -0
- ruyi/ruyipkg/profile.py +208 -0
- ruyi/ruyipkg/profile_cli.py +33 -0
- ruyi/ruyipkg/protocols.py +55 -0
- ruyi/ruyipkg/repo.py +763 -0
- ruyi/ruyipkg/state.py +345 -0
- ruyi/ruyipkg/unpack.py +369 -0
- ruyi/ruyipkg/unpack_method.py +91 -0
- ruyi/ruyipkg/update_cli.py +54 -0
- ruyi/telemetry/__init__.py +0 -0
- ruyi/telemetry/aggregate.py +72 -0
- ruyi/telemetry/event.py +41 -0
- ruyi/telemetry/node_info.py +192 -0
- ruyi/telemetry/provider.py +411 -0
- ruyi/telemetry/scope.py +43 -0
- ruyi/telemetry/store.py +238 -0
- ruyi/telemetry/telemetry_cli.py +127 -0
- ruyi/utils/__init__.py +0 -0
- ruyi/utils/ar.py +74 -0
- ruyi/utils/ci.py +63 -0
- ruyi/utils/frontmatter.py +38 -0
- ruyi/utils/git.py +169 -0
- ruyi/utils/global_mode.py +204 -0
- ruyi/utils/l10n.py +83 -0
- ruyi/utils/markdown.py +73 -0
- ruyi/utils/nuitka.py +33 -0
- ruyi/utils/porcelain.py +51 -0
- ruyi/utils/prereqs.py +77 -0
- ruyi/utils/ssl_patch.py +170 -0
- ruyi/utils/templating.py +34 -0
- ruyi/utils/toml.py +115 -0
- ruyi/utils/url.py +7 -0
- ruyi/utils/xdg_basedir.py +80 -0
- ruyi/version.py +67 -0
- ruyi-0.39.0a20250731.dist-info/LICENSE-Apache.txt +201 -0
- ruyi-0.39.0a20250731.dist-info/METADATA +403 -0
- ruyi-0.39.0a20250731.dist-info/RECORD +101 -0
- ruyi-0.39.0a20250731.dist-info/WHEEL +4 -0
- ruyi-0.39.0a20250731.dist-info/entry_points.txt +3 -0
ruyi/device/provision.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import os.path
|
|
3
|
+
from typing import TYPE_CHECKING, TypedDict, TypeGuard, cast
|
|
4
|
+
|
|
5
|
+
from ..cli import user_input
|
|
6
|
+
from ..config import GlobalConfig
|
|
7
|
+
from ..log import RuyiLogger
|
|
8
|
+
from ..ruyipkg.atom import Atom, ExprAtom, SlugAtom
|
|
9
|
+
from ..ruyipkg.entity_provider import BaseEntity
|
|
10
|
+
from ..ruyipkg.host import get_native_host
|
|
11
|
+
from ..ruyipkg.install import do_install_atoms
|
|
12
|
+
from ..ruyipkg.pkg_manifest import (
|
|
13
|
+
KNOWN_PARTITION_KINDS,
|
|
14
|
+
PartitionKind,
|
|
15
|
+
PartitionMapDecl,
|
|
16
|
+
)
|
|
17
|
+
from ..ruyipkg.repo import MetadataRepo
|
|
18
|
+
from ..utils import prereqs
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..ruyipkg.pkg_manifest import BoundPackageManifest
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_variant_display_name(dev: BaseEntity, variant: BaseEntity) -> str:
|
|
25
|
+
"""Get the display name of a device variant."""
|
|
26
|
+
if n := variant.display_name:
|
|
27
|
+
return n
|
|
28
|
+
return f"{dev.display_name} ({variant.data['variant_name']})"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def do_provision_interactive(config: GlobalConfig) -> int:
|
|
32
|
+
log = config.logger
|
|
33
|
+
|
|
34
|
+
# ensure ruyi repo is present, for good out-of-the-box experience
|
|
35
|
+
mr = config.repo
|
|
36
|
+
mr.ensure_git_repo()
|
|
37
|
+
|
|
38
|
+
log.stdout(
|
|
39
|
+
"""
|
|
40
|
+
[bold green]RuyiSDK Device Provisioning Wizard[/]
|
|
41
|
+
|
|
42
|
+
This is a wizard intended to help you install a system on your device for your
|
|
43
|
+
development pleasure, all with ease.
|
|
44
|
+
|
|
45
|
+
You will be asked some questions that help RuyiSDK understand your device and
|
|
46
|
+
your intended configuration, then packages will be downloaded and flashed onto
|
|
47
|
+
the device's storage, that you should somehow make available on this host
|
|
48
|
+
system beforehand.
|
|
49
|
+
|
|
50
|
+
Note that, as Ruyi does not run as [yellow]root[/], but raw disk access is most likely
|
|
51
|
+
required to flash images, you should arrange to allow your user account [yellow]sudo[/]
|
|
52
|
+
access to necessary commands such as [yellow]dd[/]. Flashing will fail if the [yellow]sudo[/]
|
|
53
|
+
configuration does not allow so.
|
|
54
|
+
"""
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if not user_input.ask_for_yesno_confirmation(log, "Continue?"):
|
|
58
|
+
log.stdout(
|
|
59
|
+
"\nExiting. You can restart the wizard whenever prepared.",
|
|
60
|
+
end="\n\n",
|
|
61
|
+
)
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
device_entities = list(mr.entity_store.iter_entities("device"))
|
|
65
|
+
device_entities.sort(key=lambda x: x.display_name or "")
|
|
66
|
+
devices_by_id = {x.id: x for x in device_entities}
|
|
67
|
+
|
|
68
|
+
dev_choices = {k: v.display_name or "" for k, v in devices_by_id.items()}
|
|
69
|
+
dev_id = user_input.ask_for_kv_choice(
|
|
70
|
+
log,
|
|
71
|
+
"\nThe following devices are currently supported by the wizard. Please pick your device:",
|
|
72
|
+
dev_choices,
|
|
73
|
+
)
|
|
74
|
+
dev = devices_by_id[dev_id]
|
|
75
|
+
|
|
76
|
+
variants = list(
|
|
77
|
+
mr.entity_store.traverse_related_entities(
|
|
78
|
+
dev,
|
|
79
|
+
entity_types=["device-variant"],
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
variants.sort(key=lambda x: x.data.get("variant_name", ""))
|
|
83
|
+
|
|
84
|
+
variant_choices = [get_variant_display_name(dev, i) for i in variants]
|
|
85
|
+
variant_idx = user_input.ask_for_choice(
|
|
86
|
+
log,
|
|
87
|
+
"\nThe device has the following variants. Please choose the one corresponding to your hardware at hand:",
|
|
88
|
+
variant_choices,
|
|
89
|
+
)
|
|
90
|
+
variant = variants[variant_idx]
|
|
91
|
+
|
|
92
|
+
supported_combos = list(
|
|
93
|
+
mr.entity_store.traverse_related_entities(
|
|
94
|
+
variant,
|
|
95
|
+
forward_refs=False,
|
|
96
|
+
reverse_refs=True,
|
|
97
|
+
entity_types=["image-combo"],
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
supported_combos.sort(key=lambda x: x.display_name or "")
|
|
101
|
+
combo_choices = [combo.display_name or "" for combo in supported_combos]
|
|
102
|
+
combo_idx = user_input.ask_for_choice(
|
|
103
|
+
log,
|
|
104
|
+
"\nThe following system configurations are supported by the device variant you have chosen. Please pick the one you want to put on the device:",
|
|
105
|
+
combo_choices,
|
|
106
|
+
)
|
|
107
|
+
combo = supported_combos[combo_idx]
|
|
108
|
+
|
|
109
|
+
return do_provision_combo_interactive(config, mr, dev, variant, combo)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def maybe_render_postinst_msg(
|
|
113
|
+
logger: RuyiLogger,
|
|
114
|
+
mr: MetadataRepo,
|
|
115
|
+
combo: BaseEntity,
|
|
116
|
+
lang_code: str,
|
|
117
|
+
) -> bool:
|
|
118
|
+
if postinst_msgid := combo.data.get("postinst_msgid"):
|
|
119
|
+
# This field is named just "msgid" so no variables to render for
|
|
120
|
+
# the retrieved text
|
|
121
|
+
if msg := mr.messages.get_message_template(postinst_msgid, lang_code):
|
|
122
|
+
logger.stdout(f"\n{msg}")
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def do_provision_combo_interactive(
|
|
128
|
+
config: GlobalConfig,
|
|
129
|
+
mr: MetadataRepo,
|
|
130
|
+
dev_decl: BaseEntity,
|
|
131
|
+
variant_decl: BaseEntity,
|
|
132
|
+
combo: BaseEntity,
|
|
133
|
+
) -> int:
|
|
134
|
+
logger = config.logger
|
|
135
|
+
logger.D(f"provisioning device variant '{dev_decl.id}@{variant_decl.id}'")
|
|
136
|
+
|
|
137
|
+
# download packages
|
|
138
|
+
pkg_atoms = combo.data.get("package_atoms", [])
|
|
139
|
+
if not pkg_atoms:
|
|
140
|
+
if maybe_render_postinst_msg(logger, mr, combo, config.lang_code):
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
logger.F(
|
|
144
|
+
f"malformed config: device variant '{dev_decl.id}@{variant_decl.id}' asks for no packages but provides no messages either"
|
|
145
|
+
)
|
|
146
|
+
return 1
|
|
147
|
+
|
|
148
|
+
new_pkg_atoms = customize_package_versions(config, mr, pkg_atoms)
|
|
149
|
+
if new_pkg_atoms is None:
|
|
150
|
+
logger.stdout("\nExiting. You may restart the wizard at any time.", end="\n\n")
|
|
151
|
+
return 1
|
|
152
|
+
else:
|
|
153
|
+
pkg_atoms = new_pkg_atoms
|
|
154
|
+
|
|
155
|
+
pkg_names_for_display = "\n".join(f" * [green]{i}[/]" for i in pkg_atoms)
|
|
156
|
+
logger.stdout(
|
|
157
|
+
f"""
|
|
158
|
+
We are about to download and install the following packages for your device:
|
|
159
|
+
|
|
160
|
+
{pkg_names_for_display}
|
|
161
|
+
"""
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if not user_input.ask_for_yesno_confirmation(logger, "Proceed?"):
|
|
165
|
+
logger.stdout("\nExiting. You may restart the wizard at any time.", end="\n\n")
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
ret = do_install_atoms(
|
|
169
|
+
config,
|
|
170
|
+
mr,
|
|
171
|
+
set(pkg_atoms),
|
|
172
|
+
canonicalized_host=get_native_host(),
|
|
173
|
+
fetch_only=False,
|
|
174
|
+
reinstall=False,
|
|
175
|
+
)
|
|
176
|
+
if ret != 0:
|
|
177
|
+
logger.F("failed to download and install packages")
|
|
178
|
+
logger.I("your device was not touched")
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
strat_provider = ProvisionStrategyProvider(mr)
|
|
182
|
+
strategies = [
|
|
183
|
+
(pkg, get_pkg_provision_strategy(strat_provider, mr, pkg)) for pkg in pkg_atoms
|
|
184
|
+
]
|
|
185
|
+
strategies.sort(key=lambda x: x[1].priority, reverse=True)
|
|
186
|
+
|
|
187
|
+
# compose a partition map for each image pkg installed
|
|
188
|
+
pkg_part_maps = {pkg: make_pkg_part_map(config, mr, pkg) for pkg in pkg_atoms}
|
|
189
|
+
all_parts: list[PartitionKind] = []
|
|
190
|
+
for pkg_part_map in pkg_part_maps.values():
|
|
191
|
+
all_parts.extend(pkg_part_map.keys())
|
|
192
|
+
|
|
193
|
+
# prompt user to give paths to target block device(s)
|
|
194
|
+
requested_host_blkdevs: list[PartitionKind] = []
|
|
195
|
+
for pkg, strat in strategies:
|
|
196
|
+
requested_host_blkdevs.extend(strat.need_host_blkdevs(all_parts))
|
|
197
|
+
|
|
198
|
+
host_blkdev_map: PartitionMapDecl = {}
|
|
199
|
+
if requested_host_blkdevs:
|
|
200
|
+
logger.stdout(
|
|
201
|
+
"""
|
|
202
|
+
For initializing this target device, you should plug into this host system the
|
|
203
|
+
device's storage (e.g. SD card or NVMe SSD), or a removable disk to be
|
|
204
|
+
reformatted as a live medium, and note down the corresponding device file
|
|
205
|
+
path(s), e.g. /dev/sdX, /dev/nvmeXnY for whole disks; /dev/sdXY, /dev/nvmeXnYpZ
|
|
206
|
+
for partitions. You may consult e.g. [yellow]sudo blkid[/] output for the
|
|
207
|
+
information you will need later.
|
|
208
|
+
"""
|
|
209
|
+
)
|
|
210
|
+
for part in requested_host_blkdevs:
|
|
211
|
+
part_desc = get_part_desc(part)
|
|
212
|
+
path = user_input.ask_for_file(
|
|
213
|
+
logger,
|
|
214
|
+
f"Please give the path for the {part_desc}:",
|
|
215
|
+
)
|
|
216
|
+
host_blkdev_map[part] = path
|
|
217
|
+
|
|
218
|
+
# final confirmation
|
|
219
|
+
logger.stdout(
|
|
220
|
+
"""
|
|
221
|
+
We have collected enough information for the actual flashing. Now is the last
|
|
222
|
+
chance to re-check and confirm everything is fine.
|
|
223
|
+
|
|
224
|
+
We are about to:
|
|
225
|
+
"""
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
pretend_steps = "\n".join(
|
|
229
|
+
f" * {step_str}"
|
|
230
|
+
for step_str in itertools.chain(
|
|
231
|
+
*(
|
|
232
|
+
strat[1].pretend(pkg_part_maps[strat[0]], host_blkdev_map)
|
|
233
|
+
for strat in strategies
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
logger.stdout(pretend_steps, end="\n\n")
|
|
238
|
+
|
|
239
|
+
if not user_input.ask_for_yesno_confirmation(logger, "Proceed with flashing?"):
|
|
240
|
+
logger.stdout(
|
|
241
|
+
"\nExiting. The device is not touched and you may re-start the wizard at will.",
|
|
242
|
+
end="\n\n",
|
|
243
|
+
)
|
|
244
|
+
return 1
|
|
245
|
+
|
|
246
|
+
# ensure commands
|
|
247
|
+
all_needed_cmds = set(itertools.chain(*(strat.need_cmd for _, strat in strategies)))
|
|
248
|
+
if all_needed_cmds:
|
|
249
|
+
prereqs.ensure_cmds(logger, all_needed_cmds, interactive_retry=True)
|
|
250
|
+
|
|
251
|
+
if "fastboot" in all_needed_cmds:
|
|
252
|
+
# ask the user to ensure the device shows up
|
|
253
|
+
# TODO: automate doing so
|
|
254
|
+
logger.stdout(
|
|
255
|
+
"""
|
|
256
|
+
Some flashing steps require the use of fastboot, in which case you should
|
|
257
|
+
ensure the target device is showing up in [yellow]fastboot devices[/] output.
|
|
258
|
+
Please confirm it yourself before the flashing begins.
|
|
259
|
+
"""
|
|
260
|
+
)
|
|
261
|
+
if not user_input.ask_for_yesno_confirmation(
|
|
262
|
+
logger,
|
|
263
|
+
"Is the device identified by fastboot now?",
|
|
264
|
+
):
|
|
265
|
+
logger.stdout(
|
|
266
|
+
"\nAborting. The device is not touched. You may re-start the wizard after [yellow]fastboot[/] is fixed for the device.",
|
|
267
|
+
end="\n\n",
|
|
268
|
+
)
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
# flash
|
|
272
|
+
for pkg, strat in strategies:
|
|
273
|
+
logger.D(f"flashing {pkg} with strategy {strat}")
|
|
274
|
+
ret = strat.flash(pkg_part_maps[pkg], host_blkdev_map)
|
|
275
|
+
if ret != 0:
|
|
276
|
+
logger.F("flashing failed, check your device right now")
|
|
277
|
+
return ret
|
|
278
|
+
|
|
279
|
+
# parting words
|
|
280
|
+
logger.stdout(
|
|
281
|
+
"""
|
|
282
|
+
It seems the flashing has finished without errors.
|
|
283
|
+
|
|
284
|
+
[bold green]Happy hacking![/]
|
|
285
|
+
"""
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
maybe_render_postinst_msg(logger, mr, combo, config.lang_code)
|
|
289
|
+
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_part_desc(part: PartitionKind) -> str:
|
|
294
|
+
match part:
|
|
295
|
+
case "disk":
|
|
296
|
+
return "target's whole disk"
|
|
297
|
+
case "live":
|
|
298
|
+
return "removable disk to use as live medium"
|
|
299
|
+
case _:
|
|
300
|
+
return f"target's '{part}' partition"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class PackageProvisionStrategyDecl(TypedDict):
|
|
304
|
+
priority: int # higher number means earlier
|
|
305
|
+
need_host_blkdevs_fn: object # Callable[[list[PartitionKind]], list[PartitionKind]]
|
|
306
|
+
need_cmd: list[str]
|
|
307
|
+
pretend_fn: object # Callable[[PartitionMapDecl, PartitionMapDecl], list[str]]
|
|
308
|
+
flash_fn: object # Callable[[PartitionMapDecl, PartitionMapDecl], int]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def validate_list_str(x: object) -> TypeGuard[list[str]]:
|
|
312
|
+
if not isinstance(x, list):
|
|
313
|
+
return False
|
|
314
|
+
x = cast(list[object], x)
|
|
315
|
+
return all(isinstance(y, str) for y in x)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def validate_list_partition_kinds(x: object) -> TypeGuard[list[PartitionKind]]:
|
|
319
|
+
if not isinstance(x, list):
|
|
320
|
+
return False
|
|
321
|
+
x = cast(list[object], x)
|
|
322
|
+
for item in x:
|
|
323
|
+
if not isinstance(item, str) or item not in KNOWN_PARTITION_KINDS:
|
|
324
|
+
return False
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class PackageProvisionStrategy:
|
|
329
|
+
def __init__(
|
|
330
|
+
self,
|
|
331
|
+
decl: PackageProvisionStrategyDecl,
|
|
332
|
+
mr: MetadataRepo,
|
|
333
|
+
) -> None:
|
|
334
|
+
self._d = decl
|
|
335
|
+
self._mr = mr
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def priority(self) -> int:
|
|
339
|
+
return self._d["priority"]
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def need_cmd(self) -> list[str]:
|
|
343
|
+
return self._d["need_cmd"]
|
|
344
|
+
|
|
345
|
+
def need_host_blkdevs(self, x: list[PartitionKind]) -> list[PartitionKind]:
|
|
346
|
+
result = self._mr.eval_plugin_fn(self._d["need_host_blkdevs_fn"], x)
|
|
347
|
+
if not validate_list_partition_kinds(result):
|
|
348
|
+
raise TypeError("need_host_blkdevs_fn must return list[PartitionKind]")
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
def pretend(
|
|
352
|
+
self,
|
|
353
|
+
img_paths: PartitionMapDecl,
|
|
354
|
+
blkdev_paths: PartitionMapDecl,
|
|
355
|
+
) -> list[str]:
|
|
356
|
+
result = self._mr.eval_plugin_fn(self._d["pretend_fn"], img_paths, blkdev_paths)
|
|
357
|
+
if not validate_list_str(result):
|
|
358
|
+
raise TypeError("pretend_fn must return list[str]")
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
def flash(
|
|
362
|
+
self,
|
|
363
|
+
img_paths: PartitionMapDecl,
|
|
364
|
+
blkdev_paths: PartitionMapDecl,
|
|
365
|
+
) -> int:
|
|
366
|
+
result = self._mr.eval_plugin_fn(self._d["flash_fn"], img_paths, blkdev_paths)
|
|
367
|
+
if not isinstance(result, int):
|
|
368
|
+
raise TypeError("flash_fn must return int")
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class ProvisionStrategyProvider:
|
|
373
|
+
def __init__(self, mr: MetadataRepo) -> None:
|
|
374
|
+
self._mr = mr
|
|
375
|
+
self._strats: dict[str, PackageProvisionStrategy] = {}
|
|
376
|
+
|
|
377
|
+
# import the "standard library" of strategies
|
|
378
|
+
self._import_strategy_plugin("std")
|
|
379
|
+
|
|
380
|
+
def _import_strategy_plugin(self, plugin_pkg_name: str) -> None:
|
|
381
|
+
plugin_id = f"ruyi-device-provision-strategy-{plugin_pkg_name}"
|
|
382
|
+
provided_strats = self._mr.get_from_plugin(
|
|
383
|
+
plugin_id,
|
|
384
|
+
"PROVIDED_DEVICE_PROVISION_STRATEGIES_V1",
|
|
385
|
+
)
|
|
386
|
+
if not isinstance(provided_strats, dict):
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
f"malformed device provisioner strategy plugin '{plugin_id}'"
|
|
389
|
+
)
|
|
390
|
+
for name, decl in provided_strats.items():
|
|
391
|
+
self._strats[name] = PackageProvisionStrategy(decl, self._mr)
|
|
392
|
+
|
|
393
|
+
def __getitem__(self, name: str) -> PackageProvisionStrategy:
|
|
394
|
+
try:
|
|
395
|
+
return self._strats[name]
|
|
396
|
+
except KeyError:
|
|
397
|
+
# for now it's "ruyi-device-provision-strategy-STRATEGY-NAME"
|
|
398
|
+
# we may have to revise before Ruyi v1.0 though
|
|
399
|
+
self._import_strategy_plugin(name)
|
|
400
|
+
return self._strats[name]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def get_pkg_provision_strategy(
|
|
404
|
+
strat_provider: ProvisionStrategyProvider,
|
|
405
|
+
mr: MetadataRepo,
|
|
406
|
+
atom: str,
|
|
407
|
+
) -> PackageProvisionStrategy:
|
|
408
|
+
a = Atom.parse(atom)
|
|
409
|
+
pm = a.match_in_repo(mr, True)
|
|
410
|
+
assert pm is not None
|
|
411
|
+
|
|
412
|
+
pmd = pm.provisionable_metadata
|
|
413
|
+
assert pmd is not None
|
|
414
|
+
return strat_provider[pmd.strategy]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def make_pkg_part_map(
|
|
418
|
+
config: GlobalConfig,
|
|
419
|
+
mr: MetadataRepo,
|
|
420
|
+
atom: str,
|
|
421
|
+
) -> PartitionMapDecl:
|
|
422
|
+
a = Atom.parse(atom)
|
|
423
|
+
pm = a.match_in_repo(mr, True)
|
|
424
|
+
assert pm is not None
|
|
425
|
+
pkg_root = config.global_blob_install_root(pm.name_for_installation)
|
|
426
|
+
|
|
427
|
+
pmd = pm.provisionable_metadata
|
|
428
|
+
assert pmd is not None
|
|
429
|
+
return {p: os.path.join(pkg_root, f) for p, f in pmd.partition_map.items()}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def is_package_version_customization_possible(
|
|
433
|
+
gc: GlobalConfig,
|
|
434
|
+
mr: MetadataRepo,
|
|
435
|
+
pkg_atoms: list[str],
|
|
436
|
+
) -> bool:
|
|
437
|
+
"""
|
|
438
|
+
Check if package version customization is possible, which means there
|
|
439
|
+
are at least one package atom specified that matches more than one versions.
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
for atom_str in pkg_atoms:
|
|
443
|
+
# Get all available versions for this package
|
|
444
|
+
a = Atom.parse(atom_str)
|
|
445
|
+
try:
|
|
446
|
+
if len(list(a.iter_in_repo(mr, gc.include_prereleases))) > 1:
|
|
447
|
+
return True
|
|
448
|
+
except KeyError:
|
|
449
|
+
continue
|
|
450
|
+
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def customize_package_versions(
|
|
455
|
+
config: GlobalConfig,
|
|
456
|
+
mr: MetadataRepo,
|
|
457
|
+
pkg_atoms: list[str],
|
|
458
|
+
) -> list[str] | None:
|
|
459
|
+
"""
|
|
460
|
+
Allow the user to customize the versions of packages to be installed.
|
|
461
|
+
Returns a new list of package atoms with user-selected versions.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
if not is_package_version_customization_possible(config, mr, pkg_atoms):
|
|
465
|
+
return pkg_atoms
|
|
466
|
+
|
|
467
|
+
logger = config.logger
|
|
468
|
+
|
|
469
|
+
# Ask if the user wants to customize package versions
|
|
470
|
+
logger.stdout(
|
|
471
|
+
"By default, we'll install the latest version of each package, but in this case, other choices are possible."
|
|
472
|
+
)
|
|
473
|
+
if not user_input.ask_for_yesno_confirmation(
|
|
474
|
+
logger,
|
|
475
|
+
"Would you like to customize package versions?",
|
|
476
|
+
):
|
|
477
|
+
return pkg_atoms
|
|
478
|
+
|
|
479
|
+
while True: # Loop to allow restarting the selection process
|
|
480
|
+
result: list[str] = []
|
|
481
|
+
logger.stdout("\n[bold]Package Version Selection[/]")
|
|
482
|
+
|
|
483
|
+
for atom_str in pkg_atoms:
|
|
484
|
+
# Parse the atom to get package name
|
|
485
|
+
a = Atom.parse(atom_str)
|
|
486
|
+
if isinstance(a, ExprAtom):
|
|
487
|
+
# If it's already an expression with version constraints, show the constraints
|
|
488
|
+
logger.stdout(
|
|
489
|
+
f"\nPackage [green]{atom_str}[/] already has version constraints."
|
|
490
|
+
)
|
|
491
|
+
if not user_input.ask_for_yesno_confirmation(
|
|
492
|
+
logger,
|
|
493
|
+
"Would you like to change them?",
|
|
494
|
+
):
|
|
495
|
+
result.append(atom_str)
|
|
496
|
+
continue
|
|
497
|
+
elif isinstance(a, SlugAtom):
|
|
498
|
+
# Slugs already fix the version, so we can't change them
|
|
499
|
+
logger.W(
|
|
500
|
+
f"version cannot be overridden for slug atom [green]{atom_str}[/]"
|
|
501
|
+
)
|
|
502
|
+
result.append(atom_str)
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
# Get all available versions for this package
|
|
506
|
+
package_name = a.name
|
|
507
|
+
category = a.category
|
|
508
|
+
|
|
509
|
+
available_versions: "list[BoundPackageManifest]" = []
|
|
510
|
+
try:
|
|
511
|
+
available_versions = list(mr.iter_pkg_vers(package_name, category))
|
|
512
|
+
except KeyError:
|
|
513
|
+
logger.W(
|
|
514
|
+
f"could not find package [yellow]{category}/{package_name}[/] in repository"
|
|
515
|
+
)
|
|
516
|
+
result.append(atom_str)
|
|
517
|
+
|
|
518
|
+
if not available_versions:
|
|
519
|
+
logger.W(
|
|
520
|
+
f"no versions found for package [yellow]{category}/{package_name}[/]"
|
|
521
|
+
)
|
|
522
|
+
result.append(atom_str)
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
if len(available_versions) == 1:
|
|
526
|
+
# If there's only one version available, use it
|
|
527
|
+
selected_version = available_versions[0]
|
|
528
|
+
logger.stdout(
|
|
529
|
+
f"Only one version available for [green]{category}/{package_name}[/]: [blue]{selected_version.ver}[/], using it."
|
|
530
|
+
)
|
|
531
|
+
result.append(atom_str)
|
|
532
|
+
continue
|
|
533
|
+
|
|
534
|
+
# Sort versions with newest first
|
|
535
|
+
available_versions.sort(key=lambda pm: pm.semver, reverse=True)
|
|
536
|
+
|
|
537
|
+
# Create a list of version choices for display
|
|
538
|
+
version_choices = []
|
|
539
|
+
for pm in available_versions:
|
|
540
|
+
version_str = str(pm.semver)
|
|
541
|
+
remarks = []
|
|
542
|
+
|
|
543
|
+
if pm.is_prerelease:
|
|
544
|
+
remarks.append("prerelease")
|
|
545
|
+
if pm.service_level.has_known_issues:
|
|
546
|
+
remarks.append("has known issues")
|
|
547
|
+
if pm.upstream_version:
|
|
548
|
+
remarks.append(f"upstream: {pm.upstream_version}")
|
|
549
|
+
|
|
550
|
+
remark_str = f" ({', '.join(remarks)})" if remarks else ""
|
|
551
|
+
version_choices.append(f"{version_str}{remark_str}")
|
|
552
|
+
|
|
553
|
+
# Ask the user to select a version
|
|
554
|
+
version_idx = user_input.ask_for_choice(
|
|
555
|
+
logger,
|
|
556
|
+
f"\nSelect a version for package [green]{category or ''}{('/' + package_name) if category else package_name}[/]:",
|
|
557
|
+
version_choices,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
selected_version = available_versions[version_idx]
|
|
561
|
+
|
|
562
|
+
# Create the new atom string with the selected version
|
|
563
|
+
if category:
|
|
564
|
+
new_atom = f"{category}/{package_name}(=={selected_version.ver})"
|
|
565
|
+
else:
|
|
566
|
+
new_atom = f"{package_name}(=={selected_version.ver})"
|
|
567
|
+
|
|
568
|
+
logger.stdout(f"Selected: [blue]{new_atom}[/]")
|
|
569
|
+
result.append(new_atom)
|
|
570
|
+
|
|
571
|
+
logger.stdout("\nPackage versions to be installed:")
|
|
572
|
+
for atom in result:
|
|
573
|
+
logger.stdout(f" * [green]{atom}[/]")
|
|
574
|
+
|
|
575
|
+
confirmation = user_input.ask_for_choice(
|
|
576
|
+
logger,
|
|
577
|
+
"\nHow would you like to proceed?",
|
|
578
|
+
[
|
|
579
|
+
"Continue with these versions",
|
|
580
|
+
"Restart version selection",
|
|
581
|
+
"Abort device provisioning",
|
|
582
|
+
],
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if confirmation == 0: # Continue with these versions
|
|
586
|
+
return result
|
|
587
|
+
elif confirmation == 1: # Restart version selection
|
|
588
|
+
logger.stdout("\nRestarting package version selection...")
|
|
589
|
+
continue
|
|
590
|
+
else: # Abort installation
|
|
591
|
+
return None
|
|
@@ -0,0 +1,40 @@
|
|
|
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 DeviceCommand(
|
|
12
|
+
RootCommand,
|
|
13
|
+
cmd="device",
|
|
14
|
+
has_subcommands=True,
|
|
15
|
+
help="Manage devices",
|
|
16
|
+
):
|
|
17
|
+
@classmethod
|
|
18
|
+
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeviceProvisionCommand(
|
|
23
|
+
DeviceCommand,
|
|
24
|
+
cmd="provision",
|
|
25
|
+
aliases=["flash"],
|
|
26
|
+
help="Interactively initialize a device for development",
|
|
27
|
+
):
|
|
28
|
+
@classmethod
|
|
29
|
+
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
|
|
34
|
+
from .provision import do_provision_interactive
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
return do_provision_interactive(cfg)
|
|
38
|
+
except KeyboardInterrupt:
|
|
39
|
+
cfg.logger.stdout("\n\nKeyboard interrupt received, exiting.", end="\n\n")
|
|
40
|
+
return 1
|