@@ -39,6 +39,16 @@ def __init__(self):
3939 self .parser .add_argument (
4040 "--commit" , action = "store_true" , help = "commit the updated package"
4141 )
42+ self .parser .add_argument (
43+ "--with-signature" ,
44+ action = "store_true" ,
45+ help = "fetch and add signatureHash for cryptographic verification" ,
46+ )
47+ self .parser .add_argument (
48+ "--force" ,
49+ action = "store_true" ,
50+ help = "force update even if version is the same (useful for hash-only updates)" ,
51+ )
4252 self .args = self .parser .parse_args ()
4353 self .attribute_path = self .args .attribute
4454 if not self .attribute_path :
@@ -49,6 +59,8 @@ def __init__(self):
4959 self .override_filename = self .args .override_filename
5060 self .allow_pre_release = self .args .pre_release
5161 self .commit = self .args .commit
62+ self .with_signature = self .args .with_signature
63+ self .force = self .args .force
5264 self .extension_publisher = self ._get_nix_vscode_extension_publisher ()
5365 self .extension_name = self ._get_nix_vscode_extension_name ()
5466 self .extension_marketplace_id = (
@@ -159,6 +171,37 @@ def _get_nix_vscode_extension_src_hash(self, system: str) -> str:
159171 sha256 ,
160172 ])
161173
174+ def _fetch_signature_hash (self , version : str , target_platform : Optional [str ] = None ) -> Optional [str ]:
175+ """
176+ Fetches the signature hash for an extension version from the VS Code Marketplace.
177+ Returns the SRI hash or None if fetch fails.
178+ """
179+ platform_suffix = f"?targetPlatform={ target_platform } " if target_platform else ""
180+ url = (
181+ f"https://{ self .extension_publisher } .gallery.vsassets.io/_apis/public/gallery/"
182+ f"publisher/{ self .extension_publisher } /extension/{ self .extension_name } /{ version } /"
183+ f"assetbyname/Microsoft.VisualStudio.Services.VsixSignature{ platform_suffix } "
184+ )
185+ try :
186+ sha256 = self .execute_command (["nix-prefetch-url" , url ])
187+ sri_hash = self .execute_command ([
188+ "nix" ,
189+ "--extra-experimental-features" ,
190+ "nix-command" ,
191+ "hash" ,
192+ "convert" ,
193+ "--to" ,
194+ "sri" ,
195+ "--hash-algo" ,
196+ "sha256" ,
197+ sha256 ,
198+ ])
199+ logger .info (f"Fetched signature hash: { sri_hash } " )
200+ return sri_hash
201+ except subprocess .CalledProcessError :
202+ logger .warning ("Failed to fetch signature hash" )
203+ return None
204+
162205 def get_target_platform (self , nix_system : str ) -> str :
163206 """
164207 Retrieves the VS Code targetPlatform variable based on the Nix system.
@@ -339,6 +382,93 @@ def repl(m):
339382 )
340383 Path (self .override_filename ).write_text (updated_content , encoding = "utf-8" )
341384
385+ def update_signature_hash (self , signature_hash : str ) -> None :
386+ """
387+ Adds or updates signatureHash in the mktplcRef block for this extension.
388+ Handles both inline extensions in default.nix and standalone extension files.
389+ """
390+ if not self .override_filename :
391+ logger .warning ("No override filename set, cannot update signature hash" )
392+ return
393+
394+ content = Path (self .override_filename ).read_text (encoding = "utf-8" )
395+ target_name = self .attribute_path .removeprefix ("vscode-extensions." )
396+
397+ # Try to find the extension block by name (inline in default.nix)
398+ pattern = re .compile (
399+ rf"{ re .escape (target_name )} \s*=\s*buildVscodeMarketplaceExtension\s*\{{" ,
400+ re .MULTILINE ,
401+ )
402+ ext_match = pattern .search (content )
403+ search_start = ext_match .end () if ext_match else 0
404+
405+ # If not found as inline, the file is likely a standalone extension file
406+ # In that case, search from the beginning for mktplcRef
407+ if not ext_match :
408+ # Verify this looks like a standalone extension file
409+ standalone_pattern = re .compile (
410+ r'(vscode-utils\.)?buildVscodeMarketplaceExtension\s*\{' ,
411+ re .MULTILINE ,
412+ )
413+ if not standalone_pattern .search (content ):
414+ logger .warning (f"Could not find extension block for { target_name } " )
415+ return
416+
417+ # Find the mktplcRef block
418+ mktplc_pattern = re .compile (r'mktplcRef\s*=\s*\{' , re .MULTILINE )
419+ mktplc_match = mktplc_pattern .search (content , search_start )
420+ if not mktplc_match :
421+ logger .warning ("Could not find mktplcRef block" )
422+ return
423+
424+ # Find the end of mktplcRef block
425+ brace_start = mktplc_match .end () - 1
426+ count = 0
427+ pos = brace_start
428+ while pos < len (content ):
429+ if content [pos ] == "{" :
430+ count += 1
431+ elif content [pos ] == "}" :
432+ count -= 1
433+ if count == 0 :
434+ break
435+ pos += 1
436+
437+ if count != 0 :
438+ logger .warning ("Braces mismatch in mktplcRef block" )
439+ return
440+
441+ block_end = pos
442+ block_text = content [brace_start :block_end + 1 ]
443+
444+ # Check if signatureHash already exists
445+ sig_pattern = re .compile (r'(\s*)(signatureHash\s*=\s*")([^"]+)(";)' )
446+ sig_match = sig_pattern .search (block_text )
447+
448+ if sig_match :
449+ # Update existing signatureHash
450+ new_block_text = sig_pattern .sub (
451+ rf'\g<1>signatureHash = "{ signature_hash } ";' ,
452+ block_text
453+ )
454+ logger .info (f"Updated existing signatureHash" )
455+ else :
456+ # Find the hash line to insert after
457+ hash_pattern = re .compile (r'([ \t]*)(hash|sha256)\s*=\s*"[^"]+";' )
458+ hash_match = hash_pattern .search (block_text )
459+ if hash_match :
460+ indent = hash_match .group (1 )
461+ insert_pos = hash_match .end ()
462+ new_line = f'\n { indent } signatureHash = "{ signature_hash } ";'
463+ new_block_text = block_text [:insert_pos ] + new_line + block_text [insert_pos :]
464+ logger .info (f"Added signatureHash after hash line" )
465+ else :
466+ logger .warning ("Could not find hash line in mktplcRef block" )
467+ return
468+
469+ updated_content = content [:brace_start ] + new_block_text + content [block_end + 1 :]
470+ Path (self .override_filename ).write_text (updated_content , encoding = "utf-8" )
471+
342472 def run_nix_update (self , new_version : str , system : str ) -> None :
343473 """
344474 Builds and executes the nix-update command.
@@ -420,11 +550,25 @@ def run(self):
420550 f"<{ self .new_version } " ,
421551 ])
422552 except subprocess .CalledProcessError :
423- logger .info ("Already up to date or new version is older!" )
424- sys .exit (0 )
553+ if not self .force :
554+ logger .info ("Already up to date or new version is older!" )
555+ sys .exit (0 )
556+ logger .info (f"Force mode: re-fetching even though version unchanged ({ self .current_version } )" )
425557 for i , system in enumerate (self .nix_vscode_extension_platforms ):
426558 version = self .new_version if i == 0 else "skip"
427559 self .run_nix_update (version , system )
560+ # Fetch and add signature hash if requested
561+ if self .with_signature :
562+ logger .info ("Fetching signature hash..." )
563+ # Use the first platform's target platform if extension is platform-specific
564+ target_platform = None
565+ if self ._has_platform_source ():
566+ target_platform = self .get_target_platform (self .nix_vscode_extension_platforms [0 ])
567+ signature_hash = self ._fetch_signature_hash (self .new_version , target_platform )
568+ if signature_hash :
569+ self .update_signature_hash (signature_hash )
570+ else :
571+ logger .warning ("Could not fetch signature hash, skipping" )
428572 if self .commit :
429573 self .execute_command (["git" , "add" , self .override_filename ])
430574 self .execute_command ([
0 commit comments