@@ -77,60 +77,246 @@ 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 (oci:// or bare registry URL)."""
115+ if path .startswith ("oci://" ):
116+ return True
117+ # Reject other schemes (docker://, http://, etc.)
118+ if "://" in path :
119+ return False
120+ # Bare registry URL: has colon (for tag) and slash (for path), not absolute path
121+ return ":" in path and "/" in path and not path .startswith ("/" )
122+
123+ def _resolve_flash_input (
81124 self ,
82125 path : str | Dict [str , str ],
83- * ,
84- target : str | None = None ,
85- operator : Operator | Dict [ str , Operator ] | None = None ,
86- compression = None ,
87- ):
126+ target : str | None ,
127+ operator : Operator | Dict [ str , Operator ] | None ,
128+ oci_url_from_argv : str | None ,
129+ ) -> tuple [ Dict [ str , str ] | None , Dict [ str , Operator ] | None , str | None ]:
130+ """Resolve path/target/operator into (partitions, operators, oci_url)."""
88131 if isinstance (path , dict ):
89- partitions = path
90132 operators = operator if isinstance (operator , dict ) else None
133+ oci_url = oci_url_from_argv if oci_url_from_argv is not None else None
134+ return path , operators , oci_url
135+
136+ if self ._is_oci_path (path ):
137+ oci_url = path
138+ if target is None :
139+ self .logger .info ("Auto-detecting partitions from OCI image annotations..." )
140+ return None , None , oci_url
141+ if ":" not in target :
142+ raise ValueError (f"Target must be in format 'partition:filename' for OCI flashing, got: { target } " )
143+ partition_name , filename = target .split (":" , 1 )
144+ partitions = {partition_name : filename }
91145 else :
92146 if target is None :
93147 raise ValueError (
94148 "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"
149+ "Usage: j storage flash --target <partition>:<file> [path]\n "
150+ "For OCI images: j storage flash [oci://image] (auto-detects partitions)\n "
151+ "For OCI with explicit mapping: j storage flash -t boot_a:boot.simg oci://image\n "
152+ "For local files: j storage flash -t boot_a:/path/to/boot.img"
97153 )
98154 partitions = {target : path }
99- operators = {target : operator } if isinstance (operator , Operator ) else None
155+ oci_url = oci_url_from_argv
156+
157+ operators = {list (partitions .keys ())[0 ]: operator } if partitions and isinstance (operator , Operator ) else None
158+ return partitions , operators , oci_url
100159
160+ def _validate_partition_mappings (self , partitions : Dict [str , str ] | None ) -> None :
161+ """Validate partition mappings; raise ValueError if any path is empty."""
162+ if partitions is None :
163+ return
101164 for partition_name , file_path in partitions .items ():
102165 if not file_path or not file_path .strip ():
103166 raise ValueError (
104167 f"Partition '{ partition_name } ' has an empty file path. "
105168 f"Please provide a valid file path (e.g., -t { partition_name } :/path/to/image)"
106169 )
107170
108- self .logger .info ("Starting RideSX flash operation" )
171+ def _power_off_if_available (self ) -> None :
172+ """Power off device if power child is present."""
173+ if "power" in self .children :
174+ self .power .off ()
175+ self .logger .info ("device powered off" )
176+ else :
177+ self .logger .info ("device left running" )
109178
179+ def flash (
180+ self ,
181+ path : str | Dict [str , str ],
182+ * ,
183+ target : str | None = None ,
184+ operator : Operator | Dict [str , Operator ] | None = None ,
185+ compression = None ,
186+ fls_version : str | None = None ,
187+ fls_binary_url : str | None = None ,
188+ ):
189+ oci_url_from_argv = self ._oci_url_from_argv ()
190+ if oci_url_from_argv :
191+ self .logger .info (f"Detected OCI URL from command line: { oci_url_from_argv } " )
192+
193+ partitions , operators , oci_url = self ._resolve_flash_input (path , target , operator , oci_url_from_argv )
194+ if oci_url and partitions is None :
195+ argv_targets = self ._target_mappings_from_argv ()
196+ if argv_targets :
197+ self .logger .info ("Using target mappings from command line arguments" )
198+ partitions = argv_targets
199+ self ._validate_partition_mappings (partitions )
200+
201+ self .logger .info ("Starting RideSX flash operation" )
110202 self .boot_to_fastboot ()
111203
112- result = self .flash_images (partitions , operators )
204+ if oci_url :
205+ if partitions is None :
206+ self .logger .info (f"Using FLS auto-detection for OCI image: { oci_url } " )
207+ else :
208+ self .logger .info (f"Using FLS OCI flash with explicit mapping for image: { oci_url } " )
209+ result = self .flash_oci_auto (oci_url , partitions , fls_version = fls_version , fls_binary_url = fls_binary_url )
210+ else :
211+ self .logger .info ("Using traditional file-based flashing" )
212+ result = self .flash_images (partitions , operators )
113213
114214 self .logger .info ("flash operation completed successfully" )
215+ self ._power_off_if_available ()
216+ return result
115217
116- if "power" in self .children :
117- self .power .off ()
118- self .logger .info ("device powered off" )
218+ def flash_oci_auto (
219+ self ,
220+ oci_url : str ,
221+ partitions : Dict [str , str ] | None = None ,
222+ fls_version : str | None = None ,
223+ fls_binary_url : str | None = None ,
224+ ):
225+ """Flash OCI image using auto-detection or explicit partition mapping
226+
227+ Args:
228+ oci_url: OCI image reference (e.g., "oci://registry.com/image:latest")
229+ partitions: Optional mapping of partition -> filename inside OCI image
230+ fls_version: Optional FLS version to download from GitHub releases
231+ fls_binary_url: Optional custom URL to download FLS binary from
232+ """
233+ # Normalize OCI URL
234+ if not oci_url .startswith ("oci://" ):
235+ if "://" in oci_url :
236+ raise ValueError (f"Only oci:// URLs are supported, got: { oci_url } " )
237+ if ":" in oci_url and "/" in oci_url :
238+ oci_url = f"oci://{ oci_url } "
239+ else :
240+ raise ValueError (f"Invalid OCI URL format: { oci_url } " )
241+
242+ if partitions :
243+ self .logger .info (f"Flashing OCI image with explicit mapping: { list (partitions .keys ())} " )
119244 else :
120- self .logger .info ("device left running " )
245+ self .logger .info (f"Auto-detecting partitions for OCI image: { oci_url } " )
121246
122- return result
247+ self .logger .info ("Checking for fastboot devices on Exporter..." )
248+ detection_result = self .call ("detect_fastboot_device" , 5 , 2.0 )
249+
250+ if detection_result ["status" ] != "device_found" :
251+ raise RuntimeError ("No fastboot devices found. Make sure device is in fastboot mode." )
252+
253+ device_id = detection_result ["device_id" ]
254+ self .logger .info (f"Found fastboot device: { device_id } " )
255+
256+ flash_result = self .call ("flash_oci_image" , oci_url , partitions , fls_version , fls_binary_url )
257+
258+ return flash_result
123259
124260 def cli (self ):
261+ import click
262+
125263 generic_cli = FlasherClient .cli (self )
126264
127265 @driver_click_group (self )
128266 def base ():
129267 """RideSX storage operations"""
130268 pass
131269
270+ # Add all generic commands except 'flash' (we override it)
132271 for name , cmd in generic_cli .commands .items ():
133- base .add_command (cmd , name = name )
272+ if name != "flash" :
273+ base .add_command (cmd , name = name )
274+
275+ @base .command ()
276+ @click .argument ("file" )
277+ @click .option (
278+ "-t" ,
279+ "--target" ,
280+ "target_specs" ,
281+ multiple = True ,
282+ help = "Partition mapping as partition:filename (e.g., boot_a:boot.img)" ,
283+ )
284+ @click .option (
285+ "--fls-version" ,
286+ type = str ,
287+ default = None ,
288+ help = "FLS version to download from GitHub releases (e.g., 0.1.9)" ,
289+ )
290+ @click .option (
291+ "--fls-binary-url" ,
292+ type = str ,
293+ default = None ,
294+ help = "Custom URL to download FLS binary from (overrides --fls-version)" ,
295+ )
296+ def flash (file , target_specs , fls_version , fls_binary_url ):
297+ """Flash image to device
298+
299+ For OCI images with auto-detection:
300+ j storage flash oci://registry.com/image:tag
301+
302+ For OCI images with explicit partition mapping:
303+ j storage flash -t boot_a:boot.img oci://registry.com/image:tag
304+
305+ For local files:
306+ j storage flash -t boot_a:/path/to/boot.img .
307+ """
308+ if target_specs :
309+ mapping : dict [str , str ] = {}
310+ for spec in target_specs :
311+ if ":" not in spec :
312+ raise click .ClickException (f"Invalid target spec '{ spec } '. Expected format: partition:filename" )
313+ name , img = spec .split (":" , 1 )
314+ mapping [name ] = img
315+
316+ self .flash (mapping , fls_version = fls_version , fls_binary_url = fls_binary_url )
317+ return
318+
319+ self .flash (file , target = None , fls_version = fls_version , fls_binary_url = fls_binary_url )
134320
135321 @base .command ()
136322 def boot_to_fastboot ():
0 commit comments