@@ -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 = (
@@ -132,19 +144,10 @@ def _has_platform_source(self) -> bool:
132144 source_url = self ._get_nix_attribute (f"{ self .attribute_path } .src.url" )
133145 return "targetPlatform=" in source_url
134146
135- def _get_nix_vscode_extension_src_hash (self , system : str ) -> str :
136- url = self .execute_command ([
137- "nix" ,
138- "--extra-experimental-features" ,
139- "nix-command" ,
140- "eval" ,
141- "--raw" ,
142- "-f" ,
143- "." ,
144- f"{ self .attribute_path } .src.url" ,
145- "--system" ,
146- system ,
147- ])
147+ def _prefetch_url_sri (self , url : str ) -> str :
148+ """
149+ Fetches a URL and returns its SRI hash.
150+ """
148151 sha256 = self .execute_command (["nix-prefetch-url" , url ])
149152 return self .execute_command ([
150153 "nix" ,
@@ -159,6 +162,40 @@ def _get_nix_vscode_extension_src_hash(self, system: str) -> str:
159162 sha256 ,
160163 ])
161164
165+ def _get_nix_vscode_extension_src_hash (self , system : str ) -> str :
166+ url = self .execute_command ([
167+ "nix" ,
168+ "--extra-experimental-features" ,
169+ "nix-command" ,
170+ "eval" ,
171+ "--raw" ,
172+ "-f" ,
173+ "." ,
174+ f"{ self .attribute_path } .src.url" ,
175+ "--system" ,
176+ system ,
177+ ])
178+ return self ._prefetch_url_sri (url )
179+
180+ def _fetch_signature_hash (self , version : str , target_platform : Optional [str ] = None ) -> Optional [str ]:
181+ """
182+ Fetches the signature hash for an extension version from the VS Code Marketplace.
183+ Returns the SRI hash or None if fetch fails.
184+ """
185+ platform_suffix = f"?targetPlatform={ target_platform } " if target_platform else ""
186+ url = (
187+ f"https://{ self .extension_publisher } .gallery.vsassets.io/_apis/public/gallery/"
188+ f"publisher/{ self .extension_publisher } /extension/{ self .extension_name } /{ version } /"
189+ f"assetbyname/Microsoft.VisualStudio.Services.VsixSignature{ platform_suffix } "
190+ )
191+ try :
192+ sri_hash = self ._prefetch_url_sri (url )
193+ logger .info (f"Fetched signature hash: { sri_hash } " )
194+ return sri_hash
195+ except subprocess .CalledProcessError :
196+ logger .warning ("Failed to fetch signature hash" )
197+ return None
198+
162199 def get_target_platform (self , nix_system : str ) -> str :
163200 """
164201 Retrieves the VS Code targetPlatform variable based on the Nix system.
@@ -339,6 +376,93 @@ def repl(m):
339376 )
340377 Path (self .override_filename ).write_text (updated_content , encoding = "utf-8" )
341378
379+ def update_signature_hash (self , signature_hash : str ) -> None :
380+ """
381+ Adds or updates signatureHash in the mktplcRef block for this extension.
382+ Handles both inline extensions in default.nix and standalone extension files.
383+ """
384+ if not self .override_filename :
385+ logger .warning ("No override filename set, cannot update signature hash" )
386+ return
387+
388+ content = Path (self .override_filename ).read_text (encoding = "utf-8" )
389+ target_name = self .attribute_path .removeprefix ("vscode-extensions." )
390+
391+ # Try to find the extension block by name (inline in default.nix)
392+ pattern = re .compile (
393+ rf"{ re .escape (target_name )} \s*=\s*buildVscodeMarketplaceExtension\s*\{{" ,
394+ re .MULTILINE ,
395+ )
396+ ext_match = pattern .search (content )
397+ search_start = ext_match .end () if ext_match else 0
398+
399+ # If not found as inline, the file is likely a standalone extension file
400+ # In that case, search from the beginning for mktplcRef
401+ if not ext_match :
402+ # Verify this looks like a standalone extension file
403+ standalone_pattern = re .compile (
404+ r'(vscode-utils\.)?buildVscodeMarketplaceExtension\s*\{' ,
405+ re .MULTILINE ,
406+ )
407+ if not standalone_pattern .search (content ):
408+ logger .warning (f"Could not find extension block for { target_name } " )
409+ return
410+
411+ # Find the mktplcRef block
412+ mktplc_pattern = re .compile (r'mktplcRef\s*=\s*\{' , re .MULTILINE )
413+ mktplc_match = mktplc_pattern .search (content , search_start )
414+ if not mktplc_match :
415+ logger .warning ("Could not find mktplcRef block" )
416+ return
417+
418+ # Find the end of mktplcRef block
419+ brace_start = mktplc_match .end () - 1
420+ count = 0
421+ pos = brace_start
422+ while pos < len (content ):
423+ if content [pos ] == "{" :
424+ count += 1
425+ elif content [pos ] == "}" :
426+ count -= 1
427+ if count == 0 :
428+ break
429+ pos += 1
430+
431+ if count != 0 :
432+ logger .warning ("Braces mismatch in mktplcRef block" )
433+ return
434+
435+ block_end = pos
436+ block_text = content [brace_start :block_end + 1 ]
437+
438+ # Check if signatureHash already exists
439+ sig_pattern = re .compile (r'(\s*)(signatureHash\s*=\s*")([^"]+)(";)' )
440+ sig_match = sig_pattern .search (block_text )
441+
442+ if sig_match :
443+ # Update existing signatureHash
444+ new_block_text = sig_pattern .sub (
445+ rf'\g<1>signatureHash = "{ signature_hash } ";' ,
446+ block_text
447+ )
448+ logger .info (f"Updated existing signatureHash" )
449+ else :
450+ # Find the hash line to insert after
451+ hash_pattern = re .compile (r'([ \t]*)(hash|sha256)\s*=\s*"[^"]+";' )
452+ hash_match = hash_pattern .search (block_text )
453+ if hash_match :
454+ indent = hash_match .group (1 )
455+ insert_pos = hash_match .end ()
456+ new_line = f'\n { indent } signatureHash = "{ signature_hash } ";'
457+ new_block_text = block_text [:insert_pos ] + new_line + block_text [insert_pos :]
458+ logger .info (f"Added signatureHash after hash line" )
459+ else :
460+ logger .warning ("Could not find hash line in mktplcRef block" )
461+ return
462+
463+ updated_content = content [:brace_start ] + new_block_text + content [block_end + 1 :]
464+ Path (self .override_filename ).write_text (updated_content , encoding = "utf-8" )
465+
342466 def run_nix_update (self , new_version : str , system : str ) -> None :
343467 """
344468 Builds and executes the nix-update command.
@@ -420,11 +544,25 @@ def run(self):
420544 f"<{ self .new_version } " ,
421545 ])
422546 except subprocess .CalledProcessError :
423- logger .info ("Already up to date or new version is older!" )
424- sys .exit (0 )
547+ if not self .force :
548+ logger .info ("Already up to date or new version is older!" )
549+ sys .exit (0 )
550+ logger .info (f"Force mode: re-fetching even though version unchanged ({ self .current_version } )" )
425551 for i , system in enumerate (self .nix_vscode_extension_platforms ):
426552 version = self .new_version if i == 0 else "skip"
427553 self .run_nix_update (version , system )
554+ # Fetch and add signature hash if requested
555+ if self .with_signature :
556+ logger .info ("Fetching signature hash..." )
557+ # Use the first platform's target platform if extension is platform-specific
558+ target_platform = None
559+ if self ._has_platform_source ():
560+ target_platform = self .get_target_platform (self .nix_vscode_extension_platforms [0 ])
561+ signature_hash = self ._fetch_signature_hash (self .new_version , target_platform )
562+ if signature_hash :
563+ self .update_signature_hash (signature_hash )
564+ else :
565+ logger .warning ("Could not fetch signature hash, skipping" )
428566 if self .commit :
429567 self .execute_command (["git" , "add" , self .override_filename ])
430568 self .execute_command ([
0 commit comments