@@ -77,60 +77,236 @@ def flash_images(self, partitions: Dict[str, str], operators: Optional[Dict[str,
7777
7878 return flash_result
7979
80- def flash (
80+ def _oci_url_from_argv (self ) -> str | None :
81+ """Return first OCI-like URL from sys.argv, or None."""
82+ import sys
83+
84+ for arg in sys .argv [1 :]:
85+ if self ._is_oci_path (arg ):
86+ return arg
87+ return None
88+
89+ def _target_mappings_from_argv (self ) -> Dict [str , str ] | None :
90+ """Return target mappings from sys.argv, if present."""
91+ import sys
92+
93+ mapping : Dict [str , str ] = {}
94+ args = sys .argv [1 :]
95+ i = 0
96+ while i < len (args ):
97+ arg = args [i ]
98+ spec = None
99+ if arg in ("-t" , "--target" ):
100+ if i + 1 < len (args ):
101+ spec = args [i + 1 ]
102+ i += 1
103+ elif arg .startswith ("--target=" ):
104+ spec = arg .split ("=" , 1 )[1 ]
105+
106+ if spec and ":" in spec :
107+ name , img = spec .split (":" , 1 )
108+ mapping [name ] = img
109+ i += 1
110+
111+ return mapping or None
112+
113+ def _is_oci_path (self , path : str ) -> bool :
114+ """Return True if path looks like an OCI image reference."""
115+ return path .startswith (("oci://" , "docker://" )) or (":" in path and "/" in path and not path .startswith ("/" ))
116+
117+ def _resolve_flash_input (
81118 self ,
82119 path : str | Dict [str , str ],
83- * ,
84- target : str | None = None ,
85- operator : Operator | Dict [ str , Operator ] | None = None ,
86- compression = None ,
87- ):
120+ target : str | None ,
121+ operator : Operator | Dict [ str , Operator ] | None ,
122+ oci_url_from_argv : str | None ,
123+ ) -> tuple [ Dict [ str , str ] | None , Dict [ str , Operator ] | None , str | None ]:
124+ """Resolve path/target/operator into (partitions, operators, oci_url)."""
88125 if isinstance (path , dict ):
89- partitions = path
90- operators = operator if isinstance (operator , dict ) else None
126+ return path , operator if isinstance (operator , dict ) else None , oci_url_from_argv
127+
128+ if self ._is_oci_path (path ):
129+ oci_url = path
130+ if target is None :
131+ print ("Auto-detecting partitions from OCI image annotations..." )
132+ return None , None , oci_url
133+ if ":" not in target :
134+ raise ValueError (f"Target must be in format 'partition:filename' for OCI flashing, got: { target } " )
135+ partition_name , filename = target .split (":" , 1 )
136+ partitions = {partition_name : filename }
91137 else :
92138 if target is None :
93139 raise ValueError (
94140 "This driver requires a target partition.\n "
95- "Usage: j storage flash --target <partition>:<file>\n "
96- "Example: j storage flash -t boot_a:aboot.img -t system_a:rootfs.simg -t system_b:qm_var.simg"
141+ "Usage: j storage flash --target <partition>:<file> [path]\n "
142+ "For OCI images: j storage flash [oci://image] (auto-detects partitions)\n "
143+ "For OCI with explicit mapping: j storage flash -t boot_a:boot.simg oci://image\n "
144+ "For local files: j storage flash -t boot_a:/path/to/boot.img"
97145 )
98146 partitions = {target : path }
99- operators = {target : operator } if isinstance (operator , Operator ) else None
147+ oci_url = oci_url_from_argv
148+
149+ operators = {list (partitions .keys ())[0 ]: operator } if partitions and isinstance (operator , Operator ) else None
150+ return partitions , operators , oci_url
100151
152+ def _validate_partition_mappings (self , partitions : Dict [str , str ] | None ) -> None :
153+ """Validate partition mappings; raise ValueError if any path is empty."""
154+ if partitions is None :
155+ return
101156 for partition_name , file_path in partitions .items ():
102157 if not file_path or not file_path .strip ():
103158 raise ValueError (
104159 f"Partition '{ partition_name } ' has an empty file path. "
105160 f"Please provide a valid file path (e.g., -t { partition_name } :/path/to/image)"
106161 )
107162
108- self .logger .info ("Starting RideSX flash operation" )
163+ def _power_off_if_available (self ) -> None :
164+ """Power off device if power child is present."""
165+ if "power" in self .children :
166+ self .power .off ()
167+ self .logger .info ("device powered off" )
168+ else :
169+ self .logger .info ("device left running" )
109170
171+ def flash (
172+ self ,
173+ path : str | Dict [str , str ],
174+ * ,
175+ target : str | None = None ,
176+ operator : Operator | Dict [str , Operator ] | None = None ,
177+ compression = None ,
178+ fls_version : str | None = None ,
179+ fls_binary_url : str | None = None ,
180+ ):
181+ oci_url_from_argv = self ._oci_url_from_argv ()
182+ if oci_url_from_argv :
183+ self .logger .info (f"Detected OCI URL from command line: { oci_url_from_argv } " )
184+
185+ partitions , operators , oci_url = self ._resolve_flash_input (path , target , operator , oci_url_from_argv )
186+ if oci_url and partitions is None :
187+ argv_targets = self ._target_mappings_from_argv ()
188+ if argv_targets :
189+ self .logger .info ("Using target mappings from command line arguments" )
190+ partitions = argv_targets
191+ self ._validate_partition_mappings (partitions )
192+
193+ self .logger .info ("Starting RideSX flash operation" )
110194 self .boot_to_fastboot ()
111195
112- result = self .flash_images (partitions , operators )
196+ if oci_url :
197+ if partitions is None :
198+ self .logger .info (f"Using FLS auto-detection for OCI image: { oci_url } " )
199+ else :
200+ self .logger .info (f"Using FLS OCI flash with explicit mapping for image: { oci_url } " )
201+ result = self .flash_oci_auto (oci_url , partitions , fls_version = fls_version , fls_binary_url = fls_binary_url )
202+ else :
203+ self .logger .info ("Using traditional file-based flashing" )
204+ result = self .flash_images (partitions , operators )
113205
114206 self .logger .info ("flash operation completed successfully" )
207+ self ._power_off_if_available ()
208+ return result
115209
116- if "power" in self .children :
117- self .power .off ()
118- self .logger .info ("device powered off" )
210+ def flash_oci_auto (
211+ self ,
212+ oci_url : str ,
213+ partitions : Dict [str , str ] | None = None ,
214+ fls_version : str | None = None ,
215+ fls_binary_url : str | None = None ,
216+ ):
217+ """Flash OCI image using auto-detection or explicit partition mapping
218+
219+ Args:
220+ oci_url: OCI image reference (e.g., "oci://registry.com/image:latest")
221+ partitions: Optional mapping of partition -> filename inside OCI image
222+ fls_version: Optional FLS version to download from GitHub releases
223+ fls_binary_url: Optional custom URL to download FLS binary from
224+ """
225+ # Normalize OCI URL
226+ if not oci_url .startswith (("oci://" , "docker://" )):
227+ if ":" in oci_url and "/" in oci_url :
228+ oci_url = f"oci://{ oci_url } "
229+ else :
230+ raise ValueError (f"Invalid OCI URL format: { oci_url } " )
231+
232+ if partitions :
233+ self .logger .info (f"Flashing OCI image with explicit mapping: { list (partitions .keys ())} " )
119234 else :
120- self .logger .info ("device left running " )
235+ self .logger .info (f"Auto-detecting partitions for OCI image: { oci_url } " )
121236
122- return result
237+ self .logger .info ("Checking for fastboot devices on Exporter..." )
238+ detection_result = self .call ("detect_fastboot_device" , 5 , 2.0 )
239+
240+ if detection_result ["status" ] != "device_found" :
241+ raise RuntimeError ("No fastboot devices found. Make sure device is in fastboot mode." )
242+
243+ device_id = detection_result ["device_id" ]
244+ self .logger .info (f"Found fastboot device: { device_id } " )
245+
246+ flash_result = self .call ("flash_oci_image" , oci_url , partitions , fls_version , fls_binary_url )
247+
248+ return flash_result
123249
124250 def cli (self ):
251+ import click
252+
125253 generic_cli = FlasherClient .cli (self )
126254
127255 @driver_click_group (self )
128256 def base ():
129257 """RideSX storage operations"""
130258 pass
131259
260+ # Add all generic commands except 'flash' (we override it)
132261 for name , cmd in generic_cli .commands .items ():
133- base .add_command (cmd , name = name )
262+ if name != "flash" :
263+ base .add_command (cmd , name = name )
264+
265+ @base .command ()
266+ @click .argument ("file" )
267+ @click .option (
268+ "-t" ,
269+ "--target" ,
270+ "target_specs" ,
271+ multiple = True ,
272+ help = "Partition mapping as partition:filename (e.g., boot_a:boot.img)" ,
273+ )
274+ @click .option (
275+ "--fls-version" ,
276+ type = str ,
277+ default = None ,
278+ help = "FLS version to download from GitHub releases (e.g., 0.1.9)" ,
279+ )
280+ @click .option (
281+ "--fls-binary-url" ,
282+ type = str ,
283+ default = None ,
284+ help = "Custom URL to download FLS binary from (overrides --fls-version)" ,
285+ )
286+ def flash (file , target_specs , fls_version , fls_binary_url ):
287+ """Flash image to device
288+
289+ For OCI images with auto-detection:
290+ j storage flash oci://registry.com/image:tag
291+
292+ For OCI images with explicit partition mapping:
293+ j storage flash -t boot_a:boot.img oci://registry.com/image:tag
294+
295+ For local files:
296+ j storage flash -t boot_a:/path/to/boot.img .
297+ """
298+ if target_specs :
299+ mapping : dict [str , str ] = {}
300+ for spec in target_specs :
301+ if ":" not in spec :
302+ raise click .ClickException (f"Invalid target spec '{ spec } '. Expected format: partition:filename" )
303+ name , img = spec .split (":" , 1 )
304+ mapping [name ] = img
305+
306+ self .flash (mapping , fls_version = fls_version , fls_binary_url = fls_binary_url )
307+ return
308+
309+ self .flash (file , target = None , fls_version = fls_version , fls_binary_url = fls_binary_url )
134310
135311 @base .command ()
136312 def boot_to_fastboot ():
0 commit comments