@@ -1735,6 +1735,39 @@ def _sha256_file(path: Path) -> str:
17351735 return h .hexdigest ()
17361736
17371737
1738+ def _ordered_sftp_uploads (upload_map : dict ) -> list :
1739+ """Return payload files first and project.json last as the commit marker."""
1740+ config_path = upload_map .get (".cf/project.json" )
1741+ if not config_path :
1742+ raise ValueError ("SFTP push requires .cf/project.json" )
1743+ payloads = [
1744+ (rel_path , local_path )
1745+ for rel_path , local_path in upload_map .items ()
1746+ if rel_path != ".cf/project.json" and local_path
1747+ ]
1748+ payloads .append ((".cf/project.json" , config_path ))
1749+ return payloads
1750+
1751+
1752+ def _prepared_wrapper_hash (project_json_path : str ) -> str :
1753+ with open (project_json_path , "r" ) as f :
1754+ data = json .load (f )
1755+ value = str ((data .get ("project" ) or {}).get ("user_project_wrapper_hash" ) or "" ).strip ()
1756+ if not value :
1757+ raise ValueError ("project.json is missing project.user_project_wrapper_hash" )
1758+ return value
1759+
1760+
1761+ def _assert_wrapper_hash_unchanged (wrapper_path : str , expected_hash : str ) -> None :
1762+ current_hash = _sha256_file (Path (wrapper_path ))
1763+ if current_hash != expected_hash :
1764+ raise RuntimeError (
1765+ "Wrapper GDS changed while it was being uploaded. "
1766+ "project.json was not uploaded; stop any process modifying the GDS "
1767+ "and run cf push --force-overwrite again."
1768+ )
1769+
1770+
17381771def _collect_push_candidates (project_root : Path ) -> List [Tuple [str , Path , int , str ]]:
17391772 """Return [(rel_path, abs_path, size, kind)] for files the platform
17401773 accepts via --https.
@@ -2111,17 +2144,22 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
21112144 cf_dir = ensure_cf_directory (project_root )
21122145
21132146 # Find the GDS file path for hash calculation
2114- gds_path = None
2115- for gds_key , gds_path in collected .items ():
2147+ wrapper_path = None
2148+ for gds_key , local_path in collected .items ():
21162149 if gds_key .startswith ("gds/" ):
2150+ wrapper_path = local_path
21172151 break
2152+ if not wrapper_path :
2153+ console .print ("[red]No wrapper GDS found in collected project files.[/red]" )
2154+ raise click .Abort ()
21182155
21192156 project_json_path = update_or_create_project_json (
21202157 cf_dir = str (cf_dir ),
2121- gds_path = gds_path ,
2158+ gds_path = wrapper_path ,
21222159 cli_overrides = cli_overrides ,
21232160 existing_json_path = collected .get (".cf/project.json" )
21242161 )
2162+ prepared_wrapper_hash = _prepared_wrapper_hash (project_json_path )
21252163
21262164 # SFTP upload or dry-run
21272165 final_project_name = project_name or (
@@ -2135,16 +2173,14 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
21352173 upload_map ["verilog/rtl/user_defines.v" ] = collected .get ("verilog/rtl/user_defines.v" )
21362174
21372175 # Add the appropriate GDS file based on what was collected
2138- for gds_key , gds_path in collected .items ():
2176+ for gds_key , local_path in collected .items ():
21392177 if gds_key .startswith ("gds/" ):
2140- upload_map [gds_key ] = gds_path
2178+ upload_map [gds_key ] = local_path
21412179
21422180 if dry_run :
21432181 console .print ("[bold]Files to upload:[/bold]" )
2144- for rel_path , local_path in upload_map .items ():
2145- if local_path :
2146- remote_path = os .path .join (sftp_base , rel_path )
2147- console .print (f" { os .path .basename (local_path )} → { rel_path } " )
2182+ for rel_path , local_path in _ordered_sftp_uploads (upload_map ):
2183+ console .print (f" { os .path .basename (local_path )} → { rel_path } " )
21482184 return
21492185
21502186 console .print (f"Connecting to { sftp_host } ..." )
@@ -2163,15 +2199,16 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
21632199 raise click .Abort ()
21642200
21652201 try :
2166- for rel_path , local_path in upload_map .items ():
2167- if local_path :
2168- remote_path = os .path .join (sftp_base , rel_path )
2169- upload_with_progress (
2170- sftp ,
2171- local_path = local_path ,
2172- remote_path = remote_path ,
2173- force_overwrite = force_overwrite
2174- )
2202+ for rel_path , local_path in _ordered_sftp_uploads (upload_map ):
2203+ if rel_path == ".cf/project.json" :
2204+ _assert_wrapper_hash_unchanged (wrapper_path , prepared_wrapper_hash )
2205+ remote_path = os .path .join (sftp_base , rel_path )
2206+ upload_with_progress (
2207+ sftp ,
2208+ local_path = local_path ,
2209+ remote_path = remote_path ,
2210+ force_overwrite = force_overwrite
2211+ )
21752212 console .print (f"[green]✓ Uploaded to { sftp_base } [/green]" )
21762213
21772214 except Exception as e :
0 commit comments