ruyi 0.44.0a20251118__py3-none-any.whl → 0.45.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.
- ruyi/__main__.py +16 -4
- ruyi/cli/cmd.py +6 -5
- ruyi/cli/config_cli.py +14 -11
- ruyi/cli/main.py +34 -17
- ruyi/cli/oobe.py +10 -10
- ruyi/cli/self_cli.py +49 -36
- ruyi/cli/user_input.py +42 -12
- ruyi/cli/version_cli.py +11 -5
- ruyi/config/__init__.py +30 -10
- ruyi/config/errors.py +19 -7
- ruyi/device/provision.py +116 -55
- ruyi/device/provision_cli.py +6 -3
- ruyi/i18n/__init__.py +129 -0
- ruyi/log/__init__.py +6 -5
- ruyi/mux/runtime.py +19 -6
- ruyi/mux/venv/maker.py +93 -35
- ruyi/mux/venv/venv_cli.py +13 -10
- ruyi/pluginhost/plugin_cli.py +4 -3
- ruyi/resource_bundle/__init__.py +22 -8
- ruyi/resource_bundle/__main__.py +6 -5
- ruyi/resource_bundle/data.py +13 -9
- ruyi/ruyipkg/admin_checksum.py +4 -1
- ruyi/ruyipkg/admin_cli.py +9 -6
- ruyi/ruyipkg/augmented_pkg.py +15 -14
- ruyi/ruyipkg/checksum.py +8 -2
- ruyi/ruyipkg/distfile.py +33 -9
- ruyi/ruyipkg/entity.py +12 -2
- ruyi/ruyipkg/entity_cli.py +20 -12
- ruyi/ruyipkg/entity_provider.py +11 -2
- ruyi/ruyipkg/fetcher.py +38 -9
- ruyi/ruyipkg/install.py +163 -64
- ruyi/ruyipkg/install_cli.py +18 -15
- ruyi/ruyipkg/list.py +27 -20
- ruyi/ruyipkg/list_cli.py +12 -7
- ruyi/ruyipkg/news.py +23 -11
- ruyi/ruyipkg/news_cli.py +10 -7
- ruyi/ruyipkg/profile_cli.py +8 -2
- ruyi/ruyipkg/repo.py +22 -8
- ruyi/ruyipkg/unpack.py +42 -8
- ruyi/ruyipkg/unpack_method.py +5 -1
- ruyi/ruyipkg/update_cli.py +8 -3
- ruyi/telemetry/aggregate.py +5 -0
- ruyi/telemetry/provider.py +292 -105
- ruyi/telemetry/store.py +68 -15
- ruyi/telemetry/telemetry_cli.py +32 -13
- ruyi/utils/git.py +18 -11
- ruyi/utils/prereqs.py +10 -5
- ruyi/utils/ssl_patch.py +2 -1
- ruyi/version.py +9 -3
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/METADATA +4 -2
- ruyi-0.45.0.dist-info/RECORD +103 -0
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/WHEEL +1 -1
- ruyi-0.44.0a20251118.dist-info/RECORD +0 -102
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/entry_points.txt +0 -0
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/device/provision.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, TypedDict, TypeGuard, cast
|
|
|
4
4
|
|
|
5
5
|
from ..cli import user_input
|
|
6
6
|
from ..config import GlobalConfig
|
|
7
|
+
from ..i18n import _
|
|
7
8
|
from ..log import RuyiLogger
|
|
8
9
|
from ..ruyipkg.atom import Atom, ExprAtom, SlugAtom
|
|
9
10
|
from ..ruyipkg.entity_provider import BaseEntity
|
|
@@ -36,7 +37,8 @@ def do_provision_interactive(config: GlobalConfig) -> int:
|
|
|
36
37
|
mr.ensure_git_repo()
|
|
37
38
|
|
|
38
39
|
log.stdout(
|
|
39
|
-
|
|
40
|
+
_(
|
|
41
|
+
"""
|
|
40
42
|
[bold green]RuyiSDK Device Provisioning Wizard[/]
|
|
41
43
|
|
|
42
44
|
This is a wizard intended to help you install a system on your device for your
|
|
@@ -52,11 +54,12 @@ required to flash images, you should arrange to allow your user account [yellow]
|
|
|
52
54
|
access to necessary commands such as [yellow]dd[/]. Flashing will fail if the [yellow]sudo[/]
|
|
53
55
|
configuration does not allow so.
|
|
54
56
|
"""
|
|
57
|
+
)
|
|
55
58
|
)
|
|
56
59
|
|
|
57
|
-
if not user_input.ask_for_yesno_confirmation(log, "Continue?"):
|
|
60
|
+
if not user_input.ask_for_yesno_confirmation(log, _("Continue?")):
|
|
58
61
|
log.stdout(
|
|
59
|
-
"\nExiting. You can restart the wizard whenever prepared.",
|
|
62
|
+
_("\nExiting. You can restart the wizard whenever prepared."),
|
|
60
63
|
end="\n\n",
|
|
61
64
|
)
|
|
62
65
|
return 1
|
|
@@ -68,7 +71,9 @@ configuration does not allow so.
|
|
|
68
71
|
dev_choices = {k: v.display_name or "" for k, v in devices_by_id.items()}
|
|
69
72
|
dev_id = user_input.ask_for_kv_choice(
|
|
70
73
|
log,
|
|
71
|
-
|
|
74
|
+
_(
|
|
75
|
+
"\nThe following devices are currently supported by the wizard. Please pick your device:"
|
|
76
|
+
),
|
|
72
77
|
dev_choices,
|
|
73
78
|
)
|
|
74
79
|
dev = devices_by_id[dev_id]
|
|
@@ -84,7 +89,9 @@ configuration does not allow so.
|
|
|
84
89
|
variant_choices = [get_variant_display_name(dev, i) for i in variants]
|
|
85
90
|
variant_idx = user_input.ask_for_choice(
|
|
86
91
|
log,
|
|
87
|
-
|
|
92
|
+
_(
|
|
93
|
+
"\nThe device has the following variants. Please choose the one corresponding to your hardware at hand:"
|
|
94
|
+
),
|
|
88
95
|
variant_choices,
|
|
89
96
|
)
|
|
90
97
|
variant = variants[variant_idx]
|
|
@@ -101,7 +108,9 @@ configuration does not allow so.
|
|
|
101
108
|
combo_choices = [combo.display_name or "" for combo in supported_combos]
|
|
102
109
|
combo_idx = user_input.ask_for_choice(
|
|
103
110
|
log,
|
|
104
|
-
|
|
111
|
+
_(
|
|
112
|
+
"\nThe following system configurations are supported by the device variant you have chosen. Please pick the one you want to put on the device:"
|
|
113
|
+
),
|
|
105
114
|
combo_choices,
|
|
106
115
|
)
|
|
107
116
|
combo = supported_combos[combo_idx]
|
|
@@ -132,7 +141,8 @@ def do_provision_combo_interactive(
|
|
|
132
141
|
combo: BaseEntity,
|
|
133
142
|
) -> int:
|
|
134
143
|
logger = config.logger
|
|
135
|
-
|
|
144
|
+
devid = f"{dev_decl.id}@{variant_decl.id}"
|
|
145
|
+
logger.D(f"provisioning device variant '{devid}'")
|
|
136
146
|
|
|
137
147
|
# download packages
|
|
138
148
|
pkg_atoms = combo.data.get("package_atoms", [])
|
|
@@ -141,28 +151,36 @@ def do_provision_combo_interactive(
|
|
|
141
151
|
return 0
|
|
142
152
|
|
|
143
153
|
logger.F(
|
|
144
|
-
|
|
154
|
+
_(
|
|
155
|
+
"malformed config: device variant '{devid}' asks for no packages but provides no messages either"
|
|
156
|
+
).format(
|
|
157
|
+
devid=devid,
|
|
158
|
+
)
|
|
145
159
|
)
|
|
146
160
|
return 1
|
|
147
161
|
|
|
148
162
|
new_pkg_atoms = customize_package_versions(config, mr, pkg_atoms)
|
|
149
163
|
if new_pkg_atoms is None:
|
|
150
|
-
logger.stdout(
|
|
164
|
+
logger.stdout(
|
|
165
|
+
_("\nExiting. You may restart the wizard at any time."), end="\n\n"
|
|
166
|
+
)
|
|
151
167
|
return 1
|
|
152
168
|
else:
|
|
153
169
|
pkg_atoms = new_pkg_atoms
|
|
154
170
|
|
|
155
171
|
pkg_names_for_display = "\n".join(f" * [green]{i}[/]" for i in pkg_atoms)
|
|
156
172
|
logger.stdout(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"""
|
|
173
|
+
_(
|
|
174
|
+
"\nWe are about to download and install the following packages for your device:"
|
|
175
|
+
),
|
|
176
|
+
end="\n\n",
|
|
162
177
|
)
|
|
178
|
+
logger.stdout(pkg_names_for_display, end="\n\n")
|
|
163
179
|
|
|
164
|
-
if not user_input.ask_for_yesno_confirmation(logger, "Proceed?"):
|
|
165
|
-
logger.stdout(
|
|
180
|
+
if not user_input.ask_for_yesno_confirmation(logger, _("Proceed?")):
|
|
181
|
+
logger.stdout(
|
|
182
|
+
_("\nExiting. You may restart the wizard at any time."), end="\n\n"
|
|
183
|
+
)
|
|
166
184
|
return 1
|
|
167
185
|
|
|
168
186
|
ret = do_install_atoms(
|
|
@@ -174,8 +192,8 @@ We are about to download and install the following packages for your device:
|
|
|
174
192
|
reinstall=False,
|
|
175
193
|
)
|
|
176
194
|
if ret != 0:
|
|
177
|
-
logger.F("failed to download and install packages")
|
|
178
|
-
logger.I("your device was not touched")
|
|
195
|
+
logger.F(_("failed to download and install packages"))
|
|
196
|
+
logger.I(_("your device was not touched"))
|
|
179
197
|
return 2
|
|
180
198
|
|
|
181
199
|
strat_provider = ProvisionStrategyProvider(mr)
|
|
@@ -198,7 +216,8 @@ We are about to download and install the following packages for your device:
|
|
|
198
216
|
host_blkdev_map: PartitionMapDecl = {}
|
|
199
217
|
if requested_host_blkdevs:
|
|
200
218
|
logger.stdout(
|
|
201
|
-
|
|
219
|
+
_(
|
|
220
|
+
"""
|
|
202
221
|
For initializing this target device, you should plug into this host system the
|
|
203
222
|
device's storage (e.g. SD card or NVMe SSD), or a removable disk to be
|
|
204
223
|
reformatted as a live medium, and note down the corresponding device file
|
|
@@ -206,6 +225,7 @@ path(s), e.g. /dev/sdX, /dev/nvmeXnY for whole disks; /dev/sdXY, /dev/nvmeXnYpZ
|
|
|
206
225
|
for partitions. You may consult e.g. [yellow]sudo blkid[/] output for the
|
|
207
226
|
information you will need later.
|
|
208
227
|
"""
|
|
228
|
+
)
|
|
209
229
|
)
|
|
210
230
|
for part in requested_host_blkdevs:
|
|
211
231
|
part_desc = get_part_desc(part)
|
|
@@ -213,7 +233,9 @@ information you will need later.
|
|
|
213
233
|
while True:
|
|
214
234
|
path = user_input.ask_for_file(
|
|
215
235
|
logger,
|
|
216
|
-
|
|
236
|
+
_("Please give the path for the {part_desc}:").format(
|
|
237
|
+
part_desc=part_desc,
|
|
238
|
+
),
|
|
217
239
|
)
|
|
218
240
|
|
|
219
241
|
# Retrieve the latest mount info in case the user un-mounts
|
|
@@ -224,10 +246,17 @@ information you will need later.
|
|
|
224
246
|
for m in blkdev_mounts:
|
|
225
247
|
if m.source_path.samefile(path):
|
|
226
248
|
logger.W(
|
|
227
|
-
|
|
249
|
+
_(
|
|
250
|
+
"path [cyan]'{path}'[/] is currently mounted at [yellow]'{target}'[/]"
|
|
251
|
+
).format(
|
|
252
|
+
path=path,
|
|
253
|
+
target=m.target,
|
|
254
|
+
)
|
|
228
255
|
)
|
|
229
256
|
logger.I(
|
|
230
|
-
|
|
257
|
+
_(
|
|
258
|
+
"rejecting the path for safety; please double-check and retry"
|
|
259
|
+
)
|
|
231
260
|
)
|
|
232
261
|
path_valid = False
|
|
233
262
|
break
|
|
@@ -238,12 +267,14 @@ information you will need later.
|
|
|
238
267
|
|
|
239
268
|
# final confirmation
|
|
240
269
|
logger.stdout(
|
|
241
|
-
|
|
270
|
+
_(
|
|
271
|
+
"""
|
|
242
272
|
We have collected enough information for the actual flashing. Now is the last
|
|
243
273
|
chance to re-check and confirm everything is fine.
|
|
244
274
|
|
|
245
275
|
We are about to:
|
|
246
276
|
"""
|
|
277
|
+
)
|
|
247
278
|
)
|
|
248
279
|
|
|
249
280
|
pretend_steps = "\n".join(
|
|
@@ -257,9 +288,11 @@ We are about to:
|
|
|
257
288
|
)
|
|
258
289
|
logger.stdout(pretend_steps, end="\n\n")
|
|
259
290
|
|
|
260
|
-
if not user_input.ask_for_yesno_confirmation(logger, "Proceed with flashing?"):
|
|
291
|
+
if not user_input.ask_for_yesno_confirmation(logger, _("Proceed with flashing?")):
|
|
261
292
|
logger.stdout(
|
|
262
|
-
|
|
293
|
+
_(
|
|
294
|
+
"\nExiting. The device is not touched and you may re-start the wizard at will."
|
|
295
|
+
),
|
|
263
296
|
end="\n\n",
|
|
264
297
|
)
|
|
265
298
|
return 1
|
|
@@ -273,18 +306,20 @@ We are about to:
|
|
|
273
306
|
# ask the user to ensure the device shows up
|
|
274
307
|
# TODO: automate doing so
|
|
275
308
|
logger.stdout(
|
|
276
|
-
"""
|
|
309
|
+
_("""
|
|
277
310
|
Some flashing steps require the use of fastboot, in which case you should
|
|
278
311
|
ensure the target device is showing up in [yellow]fastboot devices[/] output.
|
|
279
312
|
Please [bold red]confirm it yourself before continuing[/].
|
|
280
|
-
"""
|
|
313
|
+
""")
|
|
281
314
|
)
|
|
282
315
|
if not user_input.ask_for_yesno_confirmation(
|
|
283
316
|
logger,
|
|
284
|
-
"Is the device identified by fastboot now?",
|
|
317
|
+
_("Is the device identified by fastboot now?"),
|
|
285
318
|
):
|
|
286
319
|
logger.stdout(
|
|
287
|
-
|
|
320
|
+
_(
|
|
321
|
+
"\nAborting. The device is not touched. You may re-start the wizard after [yellow]fastboot[/] is fixed for the device."
|
|
322
|
+
),
|
|
288
323
|
end="\n\n",
|
|
289
324
|
)
|
|
290
325
|
return 1
|
|
@@ -294,16 +329,16 @@ Please [bold red]confirm it yourself before continuing[/].
|
|
|
294
329
|
logger.D(f"flashing {pkg} with strategy {strat}")
|
|
295
330
|
ret = strat.flash(pkg_part_maps[pkg], host_blkdev_map)
|
|
296
331
|
if ret != 0:
|
|
297
|
-
logger.F("flashing failed, check your device right now")
|
|
332
|
+
logger.F(_("flashing failed, check your device right now"))
|
|
298
333
|
return ret
|
|
299
334
|
|
|
300
335
|
# parting words
|
|
301
336
|
logger.stdout(
|
|
302
|
-
"""
|
|
337
|
+
_("""
|
|
303
338
|
It seems the flashing has finished without errors.
|
|
304
339
|
|
|
305
340
|
[bold green]Happy hacking![/]
|
|
306
|
-
"""
|
|
341
|
+
""")
|
|
307
342
|
)
|
|
308
343
|
|
|
309
344
|
maybe_render_postinst_msg(logger, mr, combo, config.lang_code)
|
|
@@ -314,11 +349,11 @@ It seems the flashing has finished without errors.
|
|
|
314
349
|
def get_part_desc(part: PartitionKind) -> str:
|
|
315
350
|
match part:
|
|
316
351
|
case "disk":
|
|
317
|
-
return "target's whole disk"
|
|
352
|
+
return _("target's whole disk")
|
|
318
353
|
case "live":
|
|
319
|
-
return "removable disk to use as live medium"
|
|
354
|
+
return _("removable disk to use as live medium")
|
|
320
355
|
case _:
|
|
321
|
-
return
|
|
356
|
+
return _("target's '{part}' partition").format(part=part)
|
|
322
357
|
|
|
323
358
|
|
|
324
359
|
class PackageProvisionStrategyDecl(TypedDict):
|
|
@@ -489,17 +524,19 @@ def customize_package_versions(
|
|
|
489
524
|
|
|
490
525
|
# Ask if the user wants to customize package versions
|
|
491
526
|
logger.stdout(
|
|
492
|
-
|
|
527
|
+
_(
|
|
528
|
+
"By default, we'll install the latest version of each package, but in this case, other choices are possible."
|
|
529
|
+
)
|
|
493
530
|
)
|
|
494
531
|
if not user_input.ask_for_yesno_confirmation(
|
|
495
532
|
logger,
|
|
496
|
-
"Would you like to customize package versions?",
|
|
533
|
+
_("Would you like to customize package versions?"),
|
|
497
534
|
):
|
|
498
535
|
return pkg_atoms
|
|
499
536
|
|
|
500
537
|
while True: # Loop to allow restarting the selection process
|
|
501
538
|
result: list[str] = []
|
|
502
|
-
logger.stdout("\n[bold]Package Version Selection[/]")
|
|
539
|
+
logger.stdout(_("\n[bold]Package Version Selection[/]"))
|
|
503
540
|
|
|
504
541
|
for atom_str in pkg_atoms:
|
|
505
542
|
# Parse the atom to get package name
|
|
@@ -507,18 +544,26 @@ def customize_package_versions(
|
|
|
507
544
|
if isinstance(a, ExprAtom):
|
|
508
545
|
# If it's already an expression with version constraints, show the constraints
|
|
509
546
|
logger.stdout(
|
|
510
|
-
|
|
547
|
+
_(
|
|
548
|
+
"\nPackage [green]{atom}[/] already has version constraints."
|
|
549
|
+
).format(
|
|
550
|
+
atom=atom_str,
|
|
551
|
+
)
|
|
511
552
|
)
|
|
512
553
|
if not user_input.ask_for_yesno_confirmation(
|
|
513
554
|
logger,
|
|
514
|
-
"Would you like to change them?",
|
|
555
|
+
_("Would you like to change them?"),
|
|
515
556
|
):
|
|
516
557
|
result.append(atom_str)
|
|
517
558
|
continue
|
|
518
559
|
elif isinstance(a, SlugAtom):
|
|
519
560
|
# Slugs already fix the version, so we can't change them
|
|
520
561
|
logger.W(
|
|
521
|
-
|
|
562
|
+
_(
|
|
563
|
+
"version cannot be overridden for slug atom [green]{atom}[/]"
|
|
564
|
+
).format(
|
|
565
|
+
atom=atom_str,
|
|
566
|
+
)
|
|
522
567
|
)
|
|
523
568
|
result.append(atom_str)
|
|
524
569
|
continue
|
|
@@ -526,19 +571,24 @@ def customize_package_versions(
|
|
|
526
571
|
# Get all available versions for this package
|
|
527
572
|
package_name = a.name
|
|
528
573
|
category = a.category
|
|
574
|
+
pkg_fullname = f"{category}/{package_name}" if category else package_name
|
|
529
575
|
|
|
530
576
|
available_versions: "list[BoundPackageManifest]" = []
|
|
531
577
|
try:
|
|
532
578
|
available_versions = list(mr.iter_pkg_vers(package_name, category))
|
|
533
579
|
except KeyError:
|
|
534
580
|
logger.W(
|
|
535
|
-
|
|
581
|
+
_("could not find package [yellow]{pkg}[/] in repository").format(
|
|
582
|
+
pkg=pkg_fullname
|
|
583
|
+
)
|
|
536
584
|
)
|
|
537
585
|
result.append(atom_str)
|
|
538
586
|
|
|
539
587
|
if not available_versions:
|
|
540
588
|
logger.W(
|
|
541
|
-
|
|
589
|
+
_("no versions found for package [yellow]{pkg}[/]").format(
|
|
590
|
+
pkg=pkg_fullname
|
|
591
|
+
)
|
|
542
592
|
)
|
|
543
593
|
result.append(atom_str)
|
|
544
594
|
continue
|
|
@@ -547,7 +597,12 @@ def customize_package_versions(
|
|
|
547
597
|
# If there's only one version available, use it
|
|
548
598
|
selected_version = available_versions[0]
|
|
549
599
|
logger.stdout(
|
|
550
|
-
|
|
600
|
+
_(
|
|
601
|
+
"Only one version available for [green]{pkg}[/]: [blue]{ver}[/], using it."
|
|
602
|
+
).format(
|
|
603
|
+
pkg=pkg_fullname,
|
|
604
|
+
ver=selected_version.ver,
|
|
605
|
+
)
|
|
551
606
|
)
|
|
552
607
|
result.append(atom_str)
|
|
553
608
|
continue
|
|
@@ -562,11 +617,15 @@ def customize_package_versions(
|
|
|
562
617
|
remarks = []
|
|
563
618
|
|
|
564
619
|
if pm.is_prerelease:
|
|
565
|
-
remarks.append("prerelease")
|
|
620
|
+
remarks.append(_("prerelease"))
|
|
566
621
|
if pm.service_level.has_known_issues:
|
|
567
|
-
remarks.append("has known issues")
|
|
622
|
+
remarks.append(_("has known issues"))
|
|
568
623
|
if pm.upstream_version:
|
|
569
|
-
remarks.append(
|
|
624
|
+
remarks.append(
|
|
625
|
+
_("upstream: {upstream_ver}").format(
|
|
626
|
+
upstream_ver=pm.upstream_version
|
|
627
|
+
)
|
|
628
|
+
)
|
|
570
629
|
|
|
571
630
|
remark_str = f" ({', '.join(remarks)})" if remarks else ""
|
|
572
631
|
version_choices.append(f"{version_str}{remark_str}")
|
|
@@ -574,7 +633,9 @@ def customize_package_versions(
|
|
|
574
633
|
# Ask the user to select a version
|
|
575
634
|
version_idx = user_input.ask_for_choice(
|
|
576
635
|
logger,
|
|
577
|
-
|
|
636
|
+
_("\nSelect a version for package [green]{pkg}[/]:").format(
|
|
637
|
+
pkg=pkg_fullname,
|
|
638
|
+
),
|
|
578
639
|
version_choices,
|
|
579
640
|
)
|
|
580
641
|
|
|
@@ -586,27 +647,27 @@ def customize_package_versions(
|
|
|
586
647
|
else:
|
|
587
648
|
new_atom = f"{package_name}(=={selected_version.ver})"
|
|
588
649
|
|
|
589
|
-
logger.stdout(
|
|
650
|
+
logger.stdout(_("Selected: [blue]{new_atom}[/]").format(new_atom=new_atom))
|
|
590
651
|
result.append(new_atom)
|
|
591
652
|
|
|
592
|
-
logger.stdout("\nPackage versions to be installed:")
|
|
653
|
+
logger.stdout(_("\nPackage versions to be installed:"))
|
|
593
654
|
for atom in result:
|
|
594
655
|
logger.stdout(f" * [green]{atom}[/]")
|
|
595
656
|
|
|
596
657
|
confirmation = user_input.ask_for_choice(
|
|
597
658
|
logger,
|
|
598
|
-
"\nHow would you like to proceed?",
|
|
659
|
+
_("\nHow would you like to proceed?"),
|
|
599
660
|
[
|
|
600
|
-
"Continue with these versions",
|
|
601
|
-
"Restart version selection",
|
|
602
|
-
"Abort device provisioning",
|
|
661
|
+
_("Continue with these versions"),
|
|
662
|
+
_("Restart version selection"),
|
|
663
|
+
_("Abort device provisioning"),
|
|
603
664
|
],
|
|
604
665
|
)
|
|
605
666
|
|
|
606
667
|
if confirmation == 0: # Continue with these versions
|
|
607
668
|
return result
|
|
608
669
|
elif confirmation == 1: # Restart version selection
|
|
609
|
-
logger.stdout("\nRestarting package version selection...")
|
|
670
|
+
logger.stdout(_("\nRestarting package version selection..."))
|
|
610
671
|
continue
|
|
611
672
|
else: # Abort installation
|
|
612
673
|
return None
|
ruyi/device/provision_cli.py
CHANGED
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
from ..cli.cmd import RootCommand
|
|
5
|
+
from ..i18n import _
|
|
5
6
|
|
|
6
7
|
if TYPE_CHECKING:
|
|
7
8
|
from ..cli.completion import ArgumentParser
|
|
@@ -12,7 +13,7 @@ class DeviceCommand(
|
|
|
12
13
|
RootCommand,
|
|
13
14
|
cmd="device",
|
|
14
15
|
has_subcommands=True,
|
|
15
|
-
help="Manage devices",
|
|
16
|
+
help=_("Manage devices"),
|
|
16
17
|
):
|
|
17
18
|
@classmethod
|
|
18
19
|
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
@@ -23,7 +24,7 @@ class DeviceProvisionCommand(
|
|
|
23
24
|
DeviceCommand,
|
|
24
25
|
cmd="provision",
|
|
25
26
|
aliases=["flash"],
|
|
26
|
-
help="Interactively initialize a device for development",
|
|
27
|
+
help=_("Interactively initialize a device for development"),
|
|
27
28
|
):
|
|
28
29
|
@classmethod
|
|
29
30
|
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
@@ -36,5 +37,7 @@ class DeviceProvisionCommand(
|
|
|
36
37
|
try:
|
|
37
38
|
return do_provision_interactive(cfg)
|
|
38
39
|
except KeyboardInterrupt:
|
|
39
|
-
cfg.logger.stdout(
|
|
40
|
+
cfg.logger.stdout(
|
|
41
|
+
_("\n\nKeyboard interrupt received, exiting."), end="\n\n"
|
|
42
|
+
)
|
|
40
43
|
return 1
|
ruyi/i18n/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
import gettext
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Final, Mapping, NewType
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 11):
|
|
8
|
+
from typing import LiteralString
|
|
9
|
+
else:
|
|
10
|
+
# It may happen that Python and typing_extensions are both too old, which
|
|
11
|
+
# is unfortunately the case with Ubuntu 22.04 LTS system packages, meaning
|
|
12
|
+
# typing_extensions cannot guarantee us LiteralString either.
|
|
13
|
+
#
|
|
14
|
+
# We don't expect development work within such an environment, so just
|
|
15
|
+
# alias to str to avoid importing typing_extensions altogether. This also
|
|
16
|
+
# helps CLI startup performance.
|
|
17
|
+
#
|
|
18
|
+
# Unfortunately, simply assigning str to LiteralString would not work either,
|
|
19
|
+
# due to mypy/pyright not wanting us to re-assign types; we have to
|
|
20
|
+
# resort to providing different function signatures for Python 3.10, which
|
|
21
|
+
# is done below.
|
|
22
|
+
#
|
|
23
|
+
# LiteralString = str # type: ignore[misc]
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
from ..resource_bundle import get_resource_blob
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _probe_lang(environ: Mapping[str, str]) -> list[str]:
|
|
30
|
+
"""Probe the environment variables the gettext way, to determine the list
|
|
31
|
+
of preferred languages."""
|
|
32
|
+
languages: list[str] = []
|
|
33
|
+
# check the variables in this order
|
|
34
|
+
for envar in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):
|
|
35
|
+
if val := environ.get(envar):
|
|
36
|
+
languages = val.split(":")
|
|
37
|
+
break
|
|
38
|
+
if "C" not in languages:
|
|
39
|
+
languages.append("C")
|
|
40
|
+
|
|
41
|
+
for i, lang in enumerate(languages):
|
|
42
|
+
# normalize things like en_US.UTF-8 to en_US
|
|
43
|
+
if "." in lang:
|
|
44
|
+
languages[i] = lang.split(".", 1)[0]
|
|
45
|
+
|
|
46
|
+
return languages
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_DOMAINS = (
|
|
50
|
+
"argparse",
|
|
51
|
+
"ruyi",
|
|
52
|
+
)
|
|
53
|
+
"""gettext domains we supply and use ourselves"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class I18nAdapter:
|
|
57
|
+
"""Adapter for gettext translation functions."""
|
|
58
|
+
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
self._t = gettext.NullTranslations()
|
|
61
|
+
|
|
62
|
+
def hook(self) -> None:
|
|
63
|
+
# monkey-patch the global gettext functions
|
|
64
|
+
# the type ignore comments are necessary because mypy doesn't see
|
|
65
|
+
# the bounded methods as compatible with the unbound functions
|
|
66
|
+
# (it doesn't remove self from the unbound method signature)
|
|
67
|
+
gettext.gettext = self.gettext # type: ignore[assignment]
|
|
68
|
+
gettext.ngettext = self.ngettext # type: ignore[assignment]
|
|
69
|
+
|
|
70
|
+
def init_from_env(self, environ: Mapping[str, str] | None = None) -> None:
|
|
71
|
+
if environ is None:
|
|
72
|
+
environ = os.environ
|
|
73
|
+
|
|
74
|
+
langs = _probe_lang(environ)
|
|
75
|
+
for domain in _DOMAINS:
|
|
76
|
+
for lang in langs:
|
|
77
|
+
if self.set_locale(domain, lang):
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
def _get_mo(self, domain: str, locale: str) -> BytesIO | None:
|
|
81
|
+
# this is always forward-slash-separated, because this is not a concrete
|
|
82
|
+
# filesystem path, rather a resource bundle key
|
|
83
|
+
path = f"locale/{locale}/LC_MESSAGES/{domain}.mo"
|
|
84
|
+
blob = get_resource_blob(path)
|
|
85
|
+
if blob:
|
|
86
|
+
return BytesIO(blob)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def set_locale(self, domain: str, locale: str | None = None) -> bool:
|
|
90
|
+
if locale is not None:
|
|
91
|
+
if mo_file := self._get_mo(domain, locale):
|
|
92
|
+
self._t.add_fallback(gettext.GNUTranslations(mo_file))
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def gettext(self, x: str) -> str:
|
|
97
|
+
return self._t.gettext(x)
|
|
98
|
+
|
|
99
|
+
def ngettext(self, singular: str, plural: str, n: int) -> str:
|
|
100
|
+
return self._t.ngettext(singular, plural, n)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
ADAPTER: Final = I18nAdapter()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
DeferredI18nString = NewType("DeferredI18nString", str)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if sys.version_info >= (3, 11):
|
|
110
|
+
|
|
111
|
+
def _(x: LiteralString | DeferredI18nString) -> str:
|
|
112
|
+
"""``gettext`` alias that ensures its input is string literal via type
|
|
113
|
+
signature."""
|
|
114
|
+
return ADAPTER.gettext(x)
|
|
115
|
+
|
|
116
|
+
def d_(x: LiteralString) -> DeferredI18nString:
|
|
117
|
+
"""Mark a string literal for deferred translation: call ``_`` at use sites."""
|
|
118
|
+
return DeferredI18nString(x)
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
|
|
122
|
+
def _(x: str | DeferredI18nString) -> str:
|
|
123
|
+
"""``gettext`` alias that ensures its input is string literal via type
|
|
124
|
+
signature."""
|
|
125
|
+
return ADAPTER.gettext(x)
|
|
126
|
+
|
|
127
|
+
def d_(x: str) -> DeferredI18nString:
|
|
128
|
+
"""Mark a string literal for deferred translation: call ``_`` at use sites."""
|
|
129
|
+
return DeferredI18nString(x)
|
ruyi/log/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from rich.console import Console, RenderableType
|
|
12
12
|
from rich.text import Text
|
|
13
13
|
|
|
14
|
+
from ..i18n import _
|
|
14
15
|
from ..utils.global_mode import ProvidesGlobalMode
|
|
15
16
|
from ..utils.porcelain import PorcelainEntity, PorcelainEntityType, PorcelainOutput
|
|
16
17
|
|
|
@@ -217,7 +218,7 @@ class RuyiConsoleLogger(RuyiLogger):
|
|
|
217
218
|
return self._emit_porcelain_log("F", message, sep, *objects)
|
|
218
219
|
|
|
219
220
|
return self.log_console.print(
|
|
220
|
-
|
|
221
|
+
_("[bold red]fatal error:[/] {message}").format(message=message),
|
|
221
222
|
*objects,
|
|
222
223
|
sep=sep,
|
|
223
224
|
end=end,
|
|
@@ -234,7 +235,7 @@ class RuyiConsoleLogger(RuyiLogger):
|
|
|
234
235
|
return self._emit_porcelain_log("I", message, sep, *objects)
|
|
235
236
|
|
|
236
237
|
return self.log_console.print(
|
|
237
|
-
|
|
238
|
+
_("[bold green]info:[/] {message}").format(message=message),
|
|
238
239
|
*objects,
|
|
239
240
|
sep=sep,
|
|
240
241
|
end=end,
|
|
@@ -251,7 +252,7 @@ class RuyiConsoleLogger(RuyiLogger):
|
|
|
251
252
|
return self._emit_porcelain_log("W", message, sep, *objects)
|
|
252
253
|
|
|
253
254
|
return self.log_console.print(
|
|
254
|
-
|
|
255
|
+
_("[bold yellow]warn:[/] {message}").format(message=message),
|
|
255
256
|
*objects,
|
|
256
257
|
sep=sep,
|
|
257
258
|
end=end,
|
|
@@ -263,10 +264,10 @@ def humanize_list(
|
|
|
263
264
|
*,
|
|
264
265
|
sep: str = ", ",
|
|
265
266
|
item_color: str | None = None,
|
|
266
|
-
empty_prompt: str =
|
|
267
|
+
empty_prompt: str | None = None,
|
|
267
268
|
) -> str:
|
|
268
269
|
if not obj:
|
|
269
|
-
return empty_prompt
|
|
270
|
+
return empty_prompt if empty_prompt is not None else _("(none)")
|
|
270
271
|
if item_color is None:
|
|
271
272
|
return sep.join(obj)
|
|
272
273
|
return sep.join(f"[{item_color}]{x}[/]" for x in obj)
|