2626import copy
2727import hashlib
2828import io
29+ import os
2930import re
31+ import stat
3032import sys
33+ import tempfile
3134from itertools import product
3235from pathlib import Path
33- from typing import Any , Dict , List , NamedTuple , Optional , Set
36+ from typing import Any , Dict , List , NamedTuple , Optional , Set , Tuple
3437
3538import yaml
3639from yaml .constructor import ConstructorError
@@ -523,23 +526,51 @@ def registry_path() -> Path:
523526 return BACKEND_ROOT / "runtime/WebGPUShaderRegistry.cpp"
524527
525528
526- def registry_entries () -> List [RegistryEntry ]:
527- """Return one registry entry for every concrete generated shader."""
528- entries = []
529+ def _registry_entry (header : Path ) -> RegistryEntry :
530+ suffix = "_wgsl.h"
531+ if not header .name .endswith (suffix ):
532+ raise ValueError (f"unexpected generated header name: { header .name } " )
533+ name = header .name [: - len (suffix )]
534+ return RegistryEntry (
535+ name = name ,
536+ include = header .relative_to (BACKEND_ROOT ).as_posix (),
537+ symbol = symbol_base (name ),
538+ )
539+
540+
541+ def _collect_header_outputs () -> Tuple [Dict [Path , str ], List [RegistryEntry ]]:
542+ """Render every concrete header once and reject global collisions."""
543+ outputs : Dict [Path , str ] = {}
544+ entries : List [RegistryEntry ] = []
545+ registry_names : Set [str ] = set ()
546+ registry_symbols : Set [str ] = set ()
529547 for wgsl in discover ():
530- for header , _ in headers_for_shader (wgsl ):
531- suffix = "_wgsl.h"
532- if not header .name .endswith (suffix ):
533- raise ValueError (f"unexpected generated header name: { header .name } " )
534- name = header .name [: - len (suffix )]
535- entries .append (
536- RegistryEntry (
537- name = name ,
538- include = header .relative_to (BACKEND_ROOT ).as_posix (),
539- symbol = symbol_base (name ),
548+ try :
549+ rendered_headers = list (headers_for_shader (wgsl ))
550+ except Exception as error :
551+ raise ValueError (f"{ wgsl .relative_to (BACKEND_ROOT )} : { error } " ) from error
552+ for header , rendered in rendered_headers :
553+ if header in outputs :
554+ raise ValueError (
555+ "duplicate generated header path: "
556+ f"{ header .relative_to (BACKEND_ROOT )} "
540557 )
541- )
542- return sorted (entries )
558+ entry = _registry_entry (header )
559+ if entry .name in registry_names :
560+ raise ValueError (f"duplicate shader registry name: { entry .name } " )
561+ if entry .symbol in registry_symbols :
562+ raise ValueError (f"duplicate shader registry symbol: { entry .symbol } " )
563+ outputs [header ] = rendered
564+ entries .append (entry )
565+ registry_names .add (entry .name )
566+ registry_symbols .add (entry .symbol )
567+ return outputs , sorted (entries )
568+
569+
570+ def registry_entries () -> List [RegistryEntry ]:
571+ """Return one registry entry for every concrete generated shader."""
572+ _ , entries = _collect_header_outputs ()
573+ return entries
543574
544575
545576def render_registry (entries : List [RegistryEntry ]) -> str :
@@ -630,7 +661,143 @@ def headers_for_shader(wgsl):
630661 yield header , render_header (stem , text , stem )
631662
632663
633- def _report_drift (missing , stale ) -> None :
664+ def collect_outputs () -> Tuple [Dict [Path , bytes ], List [Path ]]:
665+ """Render the complete output tree and report unexpected old headers."""
666+ header_outputs , entries = _collect_header_outputs ()
667+ outputs = {
668+ path : rendered .encode ("utf-8" ) for path , rendered in header_outputs .items ()
669+ }
670+ registry = registry_path ()
671+ if registry in outputs :
672+ raise ValueError (f"duplicate generated output path: { registry } " )
673+ outputs [registry ] = render_registry (entries ).encode ("utf-8" )
674+
675+ expected_headers = set (header_outputs )
676+ actual_headers = set ((BACKEND_ROOT / "runtime/ops" ).glob ("**/*_wgsl.h" ))
677+ return outputs , sorted (actual_headers - expected_headers )
678+
679+
680+ class _OriginalOutput (NamedTuple ):
681+ existed : bool
682+ contents : bytes
683+ mode : int
684+
685+
686+ def _stage_bytes (destination : Path , contents : bytes , mode : int ) -> Path :
687+ """Write one same-directory candidate without changing its destination."""
688+ fd , name = tempfile .mkstemp (
689+ prefix = f".{ destination .name } .wgsl-gen-" ,
690+ suffix = ".tmp" ,
691+ dir = destination .parent ,
692+ )
693+ temporary = Path (name )
694+ try :
695+ with os .fdopen (fd , "wb" ) as output :
696+ output .write (contents )
697+ temporary .chmod (mode )
698+ except BaseException :
699+ try :
700+ temporary .unlink (missing_ok = True )
701+ except OSError :
702+ pass
703+ raise
704+ return temporary
705+
706+
707+ def _cleanup_temporaries (temporaries ) -> List [str ]:
708+ errors = []
709+ for temporary in temporaries :
710+ try :
711+ temporary .unlink (missing_ok = True )
712+ except OSError as error :
713+ errors .append (f"cannot remove temporary { temporary } : { error } " )
714+ return errors
715+
716+
717+ def _stage_outputs (
718+ outputs : Dict [Path , bytes ], changed : List [Path ]
719+ ) -> Tuple [Dict [Path , _OriginalOutput ], Dict [Path , Path ], List [str ]]:
720+ originals : Dict [Path , _OriginalOutput ] = {}
721+ staged : Dict [Path , Path ] = {}
722+ try :
723+ for destination in sorted (changed ):
724+ if destination .exists ():
725+ original = _OriginalOutput (
726+ existed = True ,
727+ contents = destination .read_bytes (),
728+ mode = stat .S_IMODE (destination .stat ().st_mode ),
729+ )
730+ else :
731+ original = _OriginalOutput (False , b"" , 0o644 )
732+ originals [destination ] = original
733+ staged [destination ] = _stage_bytes (
734+ destination , outputs [destination ], original .mode
735+ )
736+ except BaseException as error :
737+ cleanup_errors = _cleanup_temporaries (staged .values ())
738+ if isinstance (error , OSError ):
739+ errors = [f"cannot stage generated output: { error } " ] + cleanup_errors
740+ return originals , staged , errors
741+ raise
742+ return originals , staged , []
743+
744+
745+ def _rollback_outputs (
746+ originals : Dict [Path , _OriginalOutput ],
747+ replaced : List [Path ],
748+ staged : Dict [Path , Path ],
749+ ) -> List [str ]:
750+ errors = []
751+ for destination in reversed (replaced ):
752+ original = originals [destination ]
753+ restore_temporary : Optional [Path ] = None
754+ try :
755+ if original .existed :
756+ restore_temporary = _stage_bytes (
757+ destination , original .contents , original .mode
758+ )
759+ os .replace (restore_temporary , destination )
760+ else :
761+ destination .unlink (missing_ok = True )
762+ except OSError as error :
763+ errors .append (f"cannot roll back { destination } : { error } " )
764+ finally :
765+ if restore_temporary is not None :
766+ errors .extend (_cleanup_temporaries ([restore_temporary ]))
767+ errors .extend (_cleanup_temporaries (staged .values ()))
768+ return errors
769+
770+
771+ def _publish_outputs (outputs : Dict [Path , bytes ], changed : List [Path ]) -> List [str ]:
772+ """Stage and publish changed outputs, rolling back reported failures."""
773+ originals , staged , stage_errors = _stage_outputs (outputs , changed )
774+ if stage_errors :
775+ return stage_errors
776+
777+ replaced : List [Path ] = []
778+ try :
779+ for destination in sorted (changed ):
780+ try :
781+ os .replace (staged [destination ], destination )
782+ except OSError :
783+ raise
784+ except BaseException :
785+ replaced .append (destination )
786+ raise
787+ else :
788+ replaced .append (destination )
789+ except OSError as commit_error :
790+ return [f"cannot publish generated output: { commit_error } " ] + _rollback_outputs (
791+ originals , replaced , staged
792+ )
793+ except BaseException :
794+ _rollback_outputs (originals , replaced , staged )
795+ raise
796+
797+ return _cleanup_temporaries (staged .values ())
798+
799+
800+ def _report_drift (missing , stale , orphans ) -> None :
634801 """Print the --check report for missing/stale committed headers."""
635802 if missing :
636803 print ("Missing embedded WGSL headers (run scripts/gen_wgsl_headers.py):" )
@@ -640,16 +807,10 @@ def _report_drift(missing, stale) -> None:
640807 print ("Stale embedded WGSL headers (run scripts/gen_wgsl_headers.py):" )
641808 for h in stale :
642809 print (f" { h .relative_to (BACKEND_ROOT )} " )
643-
644-
645- def _sync_generated_output (output , want , check , missing , stale ) -> None :
646- """Write one generated file, or record its --check drift."""
647- if output .exists () and output .read_text () == want :
648- return
649- if check :
650- (missing if not output .exists () else stale ).append (output )
651- else :
652- output .write_text (want )
810+ if orphans :
811+ print ("Orphan embedded WGSL headers (remove or restore their sources):" )
812+ for h in orphans :
813+ print (f" { h .relative_to (BACKEND_ROOT )} " )
653814
654815
655816def main (argv = None ) -> int :
@@ -661,38 +822,35 @@ def main(argv=None) -> int:
661822 )
662823 args = parser .parse_args (argv )
663824
664- stale = []
665- missing = []
666- errors = []
667- for wgsl in discover ():
668- try :
669- rendered = list (headers_for_shader (wgsl ))
670- # A malformed spec raises yaml.YAMLError (incl. UniqueKeyLoader's
671- # ConstructorError) / ValueError / KeyError from parse_template_spec, and
672- # a malformed template raises AssertionError from preprocess; catch them
673- # all so a bad shader is a clean --check report, not a traceback.
674- except (ValueError , KeyError , AssertionError , yaml .YAMLError ) as e :
675- errors .append (f"{ wgsl .relative_to (BACKEND_ROOT )} : { e } " )
676- continue
677- for header , want in rendered :
678- # Full-content compare (not just the sha) catches generator-logic drift too.
679- _sync_generated_output (header , want , args .check , missing , stale )
825+ try :
826+ outputs , orphans = collect_outputs ()
827+ missing = []
828+ stale = []
829+ for output , want in sorted (outputs .items ()):
830+ if not output .exists ():
831+ missing .append (output )
832+ elif output .read_bytes () != want :
833+ stale .append (output )
834+ except Exception as error :
835+ print ("Cannot generate WGSL outputs:" )
836+ print (f" { error } " )
837+ return 1
680838
681- if not errors :
682- try :
683- registry = render_registry (registry_entries ())
684- output = registry_path ()
685- _sync_generated_output (output , registry , args .check , missing , stale )
686- except ValueError as e :
687- errors .append (f"shader registry: { e } " )
839+ if orphans :
840+ _report_drift ([], [], orphans )
841+ return 1
842+
843+ if args .check :
844+ if stale or missing :
845+ _report_drift (missing , stale , [])
846+ return 1
847+ return 0
688848
849+ errors = _publish_outputs (outputs , missing + stale )
689850 if errors :
690- print ("Cannot generate header (malformed shader):" )
691- for e in errors :
692- print (f" { e } " )
693- return 1
694- if args .check and (stale or missing ):
695- _report_drift (missing , stale )
851+ print ("Cannot publish WGSL outputs:" )
852+ for error in errors :
853+ print (f" { error } " )
696854 return 1
697855 return 0
698856
0 commit comments