@@ -90,6 +90,7 @@ def flash( # noqa: C901
9090 path : PathBuf ,
9191 * ,
9292 partition : str | None = None ,
93+ block_device : str | None = None ,
9394 operator : Operator | None = None ,
9495 os_image_checksum : str | None = None ,
9596 force_exporter_http : bool = False ,
@@ -177,6 +178,7 @@ def flash( # noqa: C901
177178 try :
178179 self ._perform_flash_operation (
179180 partition ,
181+ block_device ,
180182 path ,
181183 image_url ,
182184 should_download_to_httpd ,
@@ -299,9 +301,86 @@ def _find_exception_in_chain(self, exception: Exception, target_type: type) -> E
299301 current = getattr (current , "__cause__" , None )
300302 return None
301303
304+ def _resolve_and_prepare_target (
305+ self ,
306+ partition : str | None ,
307+ block_device : str | None ,
308+ manifest ,
309+ console ,
310+ ) -> str :
311+ """Resolve partition/target device and prepare flash target path.
312+
313+ Resolves the target for flashing, with priority to manifest targets when there
314+ is ambiguity. Supports two modes:
315+ 1. partition matches a manifest target name - use it as target (if collision with
316+ partition_label suspected, log warning mentioning both interpretations)
317+ 2. partition is a partition label - resolve block_device or use default target,
318+ then construct the /dev/disk/by-partlabel path
319+
320+ Args:
321+ partition: Either a target device name (from manifest.spec.targets) or a
322+ partition label. If it matches a manifest target, it takes priority.
323+ block_device: Optional block device name (e.g., 'usd', 'emmc'). Ignored if
324+ partition matches a manifest target. If partition is a partition
325+ label, this specifies which block_device to target; if omitted,
326+ uses default target from call("get_default_target") or
327+ manifest.spec.default_target.
328+ manifest: Flasher manifest containing targets and default_target
329+ console: Console object for device interaction
330+
331+ Returns:
332+ The flash target path (either device path from _get_target_device() or
333+ /dev/disk/by-partlabel/<partition_label> path)
334+
335+ Raises:
336+ ArgumentError: If no valid target can be determined
337+ """
338+ # Check if partition is a valid target device name in the manifest
339+ is_target_name = partition and partition in manifest .spec .targets
340+
341+ if is_target_name :
342+ # Collision detection: partition matches a manifest target
343+ if block_device :
344+ self .logger .warning (
345+ f"Ambiguous partition argument '{ partition } ': matches both a "
346+ f"manifest target name and potentially a partition label. "
347+ f"Treating as manifest target (ignoring block_device='{ block_device } '). "
348+ f"To use '{ partition } ' as a partition label on block_device '{ block_device } ', "
349+ f"rename the partition label or use only the -t option with partition:file pairs."
350+ )
351+ target = partition
352+ target_device = self ._get_target_device (target , manifest , console )
353+ self .logger .info (f"Using manifest target: { target } -> { target_device } " )
354+ return target_device
355+
356+ # partition is a partition label (or None), not a manifest target
357+ partition_label = partition
358+ if block_device :
359+ target = block_device
360+ self .logger .debug (f"Using block_device '{ block_device } ' for partition label '{ partition_label } '" )
361+ else :
362+ target = self .call ("get_default_target" ) or manifest .spec .default_target
363+ if partition_label :
364+ self .logger .debug (f"Using default target for partition label '{ partition_label } '" )
365+
366+ if not target :
367+ raise ArgumentError ("No partition or default target specified" )
368+
369+ target_device = self ._get_target_device (target , manifest , console )
370+
371+ if partition_label :
372+ flash_target = f"/dev/disk/by-partlabel/{ partition_label } "
373+ self .logger .info (f"Using partition label: { partition_label } on { target } -> { flash_target } " )
374+ else :
375+ flash_target = target_device
376+ self .logger .info (f"Using target block device: { target_device } " )
377+
378+ return flash_target
379+
302380 def _perform_flash_operation (
303381 self ,
304382 partition : str | None ,
383+ block_device : str | None ,
305384 path : PathBuf ,
306385 image_url : str ,
307386 should_download_to_httpd : bool ,
@@ -321,13 +400,8 @@ def _perform_flash_operation(
321400 """
322401 with self ._busybox () as console :
323402 manifest = self .manifest
324- target = partition or self .call ("get_default_target" ) or manifest .spec .default_target
325- if not target :
326- raise ArgumentError ("No partition or default target specified" )
327-
328- target_device = self ._get_target_device (target , manifest , console )
403+ flash_target = self ._resolve_and_prepare_target (partition , block_device , manifest , console )
329404
330- self .logger .info (f"Using target block device: { target_device } " )
331405 console .sendline (f"export dhcp_addr={ self ._dhcp_details .ip_address } " )
332406 console .expect (manifest .spec .login .prompt , timeout = EXPECT_TIMEOUT_DEFAULT )
333407 console .sendline (f"export gw_addr={ self ._dhcp_details .gateway } " )
@@ -372,7 +446,7 @@ def _perform_flash_operation(
372446 manifest ,
373447 path ,
374448 image_url ,
375- target_device ,
449+ flash_target ,
376450 insecure_tls ,
377451 stored_cacert ,
378452 header_args ,
@@ -385,7 +459,7 @@ def _perform_flash_operation(
385459 manifest ,
386460 path ,
387461 image_url ,
388- target_device ,
462+ flash_target ,
389463 insecure_tls ,
390464 stored_cacert ,
391465 header_args ,
@@ -1162,15 +1236,90 @@ def _validate_bearer_token(self, token: str | None) -> str | None:
11621236
11631237 return token
11641238
1239+ def _resolve_flash_parameters (
1240+ self , file : str | None , partitions : tuple [str , ...] | None , block_device : str | None
1241+ ) -> list [tuple [str , str | None , str | None ]]:
1242+ """Resolve and validate flash parameters from CLI options.
1243+
1244+ Supports multiple modes:
1245+ 1. Single file to default block device:
1246+ flash image.img
1247+ 2. Single file to specific block device:
1248+ flash image.img --target usd
1249+ 3. Multiple file-partition pairs:
1250+ flash -t rootfs:rootfs.img -t boot:boot.img
1251+ 4. Multiple file-partition pairs to specific block device:
1252+ flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1253+
1254+ Args:
1255+ file: The image file argument (positional, optional). When provided alone,
1256+ flashes to the default block device.
1257+ partitions: The -t options in 'partition:file' format (repeatable).
1258+ Use this to specify partition and file together.
1259+ block_device: The --target option (block device name like 'usd', 'emmc').
1260+ Can be used with both file and -t options.
1261+
1262+ Returns:
1263+ list[tuple]: List of (image_file, target_partition, block_device) tuples
1264+
1265+ Raises:
1266+ click.UsageError: If parameters are invalid or conflicting
1267+ """
1268+ flash_ops : list [tuple [str , str | None , str | None ]] = []
1269+
1270+ # Mode 1 & 2: Single file with optional block device
1271+ if file :
1272+ if partitions :
1273+ raise click .UsageError (
1274+ "Cannot specify FILE argument with -t options. "
1275+ "Use either 'flash image.img' or 'flash -t partition:file'"
1276+ )
1277+ flash_ops .append ((file , None , block_device ))
1278+
1279+ # Mode 3 & 4: Multiple file-partition pairs with optional block device
1280+ elif partitions :
1281+ for spec in partitions :
1282+ if ':' not in spec :
1283+ raise click .UsageError (
1284+ f"Invalid flash spec format: '{ spec } '. "
1285+ "Expected 'partition:filename'"
1286+ )
1287+ partition_label , filename = spec .split (':' , 1 )
1288+ if not partition_label or not filename :
1289+ raise click .UsageError (
1290+ f"Invalid flash spec format: '{ spec } '. "
1291+ "Both partition label and filename are required"
1292+ )
1293+ flash_ops .append ((filename , partition_label , block_device ))
1294+
1295+ # No input provided
1296+ else :
1297+ raise click .UsageError (
1298+ "Must provide either FILE argument or -t options. "
1299+ "Use 'j storage flash --help' for usage examples"
1300+ )
1301+
1302+ return flash_ops
1303+
11651304 def cli (self ):
11661305 @driver_click_group (self )
11671306 def base ():
11681307 """Software-defined flasher interface"""
11691308 pass
11701309
11711310 @base .command ()
1172- @click .argument ("file" )
1173- @click .option ("--target" , type = str )
1311+ @click .argument ("file" , required = False )
1312+ @click .option (
1313+ "--target" ,
1314+ type = str ,
1315+ help = "Block device to flash to (e.g., 'usd', 'emmc'). If not provided, uses default target."
1316+ )
1317+ @click .option (
1318+ "-t" ,
1319+ "partitions" ,
1320+ multiple = True ,
1321+ help = "Flash file to partition: 'partition:filename'. Can be repeated for multiple partitions." ,
1322+ )
11741323 @click .option ("--os-image-checksum" , help = "SHA256 checksum of OS image (direct value)" )
11751324 @click .option (
11761325 "--os-image-checksum-file" ,
@@ -1219,6 +1368,7 @@ def base():
12191368 def flash (
12201369 file ,
12211370 target ,
1371+ partitions ,
12221372 os_image_checksum ,
12231373 os_image_checksum_file ,
12241374 console_debug ,
@@ -1233,31 +1383,67 @@ def flash(
12331383 fls_version ,
12341384 fls_binary_url ,
12351385 ):
1236- """Flash image to DUT from file"""
1237- if os_image_checksum_file and os .path .exists (os_image_checksum_file ):
1238- with open (os_image_checksum_file ) as f :
1239- os_image_checksum = f .read ().strip ().split ()[0 ]
1240- self .logger .info (f"Read checksum from file: { os_image_checksum } " )
1386+ """Flash image(s) to DUT
12411387
1242- self .set_console_debug (console_debug )
1388+ Usage examples:
1389+
1390+ - Flash to default block device and target
1391+
1392+ j storage flash image.img
1393+
1394+ - Flash to specific block device (e.g., 'emmc')
1395+
1396+ j storage flash image.img --target emmc
1397+
1398+ - Flash to partition(s) on default block device
1399+
1400+ j storage flash -t rootfs:rootfs.img
12431401
1244- headers = self ._parse_headers (header ) if header else None
1245-
1246- self .flash (
1247- file ,
1248- partition = target ,
1249- force_exporter_http = force_exporter_http ,
1250- force_flash_bundle = force_flash_bundle ,
1251- cacert_file = cacert ,
1252- insecure_tls = insecure_tls ,
1253- headers = headers ,
1254- bearer_token = bearer ,
1255- retries = retries ,
1256- method = method ,
1257- fls_version = fls_version ,
1258- fls_binary_url = fls_binary_url ,
1402+ - Flash to partition(s) on specific block device
1403+
1404+ j storage flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1405+ """
1406+ # Validate and resolve flash parameters
1407+ flash_operations = self ._resolve_flash_parameters (
1408+ file , partitions , target
12591409 )
12601410
1411+ # Setup common options
1412+ self .set_console_debug (console_debug )
1413+ headers_dict = self ._parse_headers (header ) if header else None
1414+
1415+ # Load checksum from file if provided (used for all operations)
1416+ checksum = os_image_checksum
1417+ if os_image_checksum_file and os .path .exists (os_image_checksum_file ):
1418+ with open (os_image_checksum_file ) as f :
1419+ checksum = f .read ().strip ().split ()[0 ]
1420+ self .logger .info (f"Read checksum from file: { checksum } " )
1421+
1422+ # Execute each flash operation
1423+ for idx , (image_file , target_partition , block_device ) in enumerate (flash_operations ):
1424+ op_num = f"{ idx + 1 } /{ len (flash_operations )} " if len (flash_operations ) > 1 else ""
1425+ op_desc = f"partition '{ target_partition } '" if target_partition else "default target"
1426+
1427+ self .logger .info (f"Flashing { op_num } { op_desc } with '{ image_file } '" .strip ())
1428+
1429+ # Perform the flash operation
1430+ self .flash (
1431+ image_file ,
1432+ partition = target_partition ,
1433+ block_device = block_device ,
1434+ os_image_checksum = checksum ,
1435+ force_exporter_http = force_exporter_http ,
1436+ force_flash_bundle = force_flash_bundle ,
1437+ cacert_file = cacert ,
1438+ insecure_tls = insecure_tls ,
1439+ headers = headers_dict ,
1440+ bearer_token = bearer ,
1441+ retries = retries ,
1442+ method = method ,
1443+ fls_version = fls_version ,
1444+ fls_binary_url = fls_binary_url ,
1445+ )
1446+
12611447 @base .command ()
12621448 @debug_console_option
12631449 def bootloader_shell (console_debug ):
0 commit comments