1111- Offline rootfs / NFS rootfs validation (--target-root /srv/nfs/spider-rootfs)
1212- Remote SSH target validation (--ssh root@spider)
1313- Mismatch detection against historical SOF releases
14+ - Displays version of installed files in summary table
1415- Fix mode (--fix, --dry-run) to automatically sync/repair installed files
1516- Platform filtering (-p tgl, -p mtl, -p ptl, etc.)
1617- Component filtering (-c fw, -c llext, -c tplg, -c tools)
@@ -51,6 +52,8 @@ class ValidationRecord:
5152 component : str # fw, llext, tplg, tools
5253 platform : str # tgl, mtl, ptl, generic, etc.
5354 rel_path : str # relative to fw_dest (or tools_dest)
55+ expected_version : str = "" # target version requested e.g. v2.14.1
56+ installed_version : Optional [str ] = None # version found on host e.g. v2.12, v2.14.1, unknown
5457 source_path : Optional [str ] = None # in sof-bin
5558 target_path : Optional [str ] = None # on host
5659 is_symlink : bool = False
@@ -71,6 +74,8 @@ def to_dict(self) -> Dict[str, Any]:
7174 "component" : self .component ,
7275 "platform" : self .platform ,
7376 "rel_path" : self .rel_path ,
77+ "expected_version" : self .expected_version ,
78+ "installed_version" : self .installed_version ,
7479 "source_path" : self .source_path ,
7580 "target_path" : self .target_path ,
7681 "is_symlink" : self .is_symlink ,
@@ -298,7 +303,6 @@ def lookup_md5(self, md5_hash: Optional[str], target_filename: Optional[str] = N
298303 if not matches :
299304 return None
300305
301- # Prioritize matching filename if provided
302306 filtered = matches
303307 if target_filename :
304308 same_name = [m for m in matches if m ["filename" ] == target_filename ]
@@ -307,13 +311,12 @@ def lookup_md5(self, md5_hash: Optional[str], target_filename: Optional[str] = N
307311
308312 descriptions = []
309313 for m in filtered :
310- # Extract clean release name e.g. v2.12 or v2.11.1
311314 comp = m ["component" ]
312315 ver_match = re .search (r"v\d+(\.\d+)*(-[a-zA-Z0-9]+)?" , comp )
313316 ver_label = ver_match .group (0 ) if ver_match else m ["version_dir" ]
314317 descriptions .append (f"{ ver_label } ({ comp } )" )
315318
316- return ", " .join (dict .fromkeys (descriptions )) # unique preserved
319+ return ", " .join (dict .fromkeys (descriptions ))
317320
318321
319322class HostTarget :
@@ -333,7 +336,6 @@ def fix_file(
333336 symlink_target : Optional [str ],
334337 dry_run : bool = False ,
335338 ) -> Tuple [bool , str ]:
336- """Fix a file or symlink on the target host."""
337339 raise NotImplementedError
338340
339341
@@ -403,7 +405,6 @@ def fix_file(
403405
404406 try :
405407 real_tgt .parent .mkdir (parents = True , exist_ok = True )
406- # Remove existing file or broken link if it exists
407408 if real_tgt .exists () or real_tgt .is_symlink ():
408409 real_tgt .unlink ()
409410
@@ -653,14 +654,16 @@ def should_include(self, component: str, platform: str, rel_path: str) -> bool:
653654
654655 return True
655656
656- def scan_source_components (self , comp_dirs : Dict [str , Path ]) -> List [ValidationRecord ]:
657+ def scan_source_components (self , comp_dirs : Dict [str , Path ], target_version : str ) -> List [ValidationRecord ]:
657658 records : List [ValidationRecord ] = []
658659
659660 # 1. Firmware
660661 if "fw" in comp_dirs and "fw" in self .component_filter :
661662 fw_dir = comp_dirs ["fw" ]
662663 is_ipc4 = "ipc4" in fw_dir .name
663664 radix = "sof-ipc4" if is_ipc4 else "sof"
665+ ver_match = re .search (r"v\d+(\.\d+)*(-[a-zA-Z0-9]+)?" , fw_dir .name )
666+ fw_ver = ver_match .group (0 ) if ver_match else target_version
664667
665668 for root , _ , files in os .walk (fw_dir ):
666669 for f in files :
@@ -682,6 +685,7 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
682685 component = "fw" ,
683686 platform = platform ,
684687 rel_path = rel_install ,
688+ expected_version = fw_ver ,
685689 source_path = str (src_p ),
686690 is_symlink = is_sym ,
687691 symlink_target_expected = sym_tgt ,
@@ -692,6 +696,9 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
692696 # 2. LLEXT
693697 if "llext" in comp_dirs and "llext" in self .component_filter :
694698 lib_dir = comp_dirs ["llext" ]
699+ ver_match = re .search (r"v\d+(\.\d+)*(-[a-zA-Z0-9]+)?" , lib_dir .name )
700+ lib_ver = ver_match .group (0 ) if ver_match else target_version
701+
695702 for root , _ , files in os .walk (lib_dir ):
696703 for f in files :
697704 src_p = Path (root ) / f
@@ -712,6 +719,7 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
712719 component = "llext" ,
713720 platform = platform ,
714721 rel_path = rel_install ,
722+ expected_version = lib_ver ,
715723 source_path = str (src_p ),
716724 is_symlink = is_sym ,
717725 symlink_target_expected = sym_tgt ,
@@ -724,6 +732,8 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
724732 tplg_dir = comp_dirs ["tplg" ]
725733 is_ipc4 = "ipc4" in tplg_dir .name or "ace" in tplg_dir .name
726734 radix = "sof-ipc4-tplg" if is_ipc4 else "sof-tplg"
735+ ver_match = re .search (r"v\d+(\.\d+)*(-[a-zA-Z0-9]+)?" , tplg_dir .name )
736+ tplg_ver = ver_match .group (0 ) if ver_match else target_version
727737
728738 for root , _ , files in os .walk (tplg_dir ):
729739 for f in files :
@@ -745,6 +755,7 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
745755 component = "tplg" ,
746756 platform = platform ,
747757 rel_path = rel_install ,
758+ expected_version = tplg_ver ,
748759 source_path = str (src_p ),
749760 is_symlink = is_sym ,
750761 symlink_target_expected = sym_tgt ,
@@ -755,6 +766,9 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
755766 # 4. Tools
756767 if "tools" in comp_dirs and "tools" in self .component_filter :
757768 tools_dir = comp_dirs ["tools" ]
769+ ver_match = re .search (r"v\d+(\.\d+)*(-[a-zA-Z0-9]+)?" , tools_dir .name )
770+ tools_ver = ver_match .group (0 ) if ver_match else target_version
771+
758772 for root , _ , files in os .walk (tools_dir ):
759773 for f in files :
760774 src_p = Path (root ) / f
@@ -775,6 +789,7 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
775789 component = "tools" ,
776790 platform = platform ,
777791 rel_path = rel_install ,
792+ expected_version = tools_ver ,
778793 source_path = str (src_p ),
779794 is_symlink = is_sym ,
780795 symlink_target_expected = sym_tgt ,
@@ -786,7 +801,7 @@ def scan_source_components(self, comp_dirs: Dict[str, Path]) -> List[ValidationR
786801
787802 def validate_installation (self , version : str ) -> List [ValidationRecord ]:
788803 comp_dirs = self .repo .resolve_version_components (version )
789- records = self .scan_source_components (comp_dirs )
804+ records = self .scan_source_components (comp_dirs , target_version = version )
790805
791806 target_path_map : Dict [str , str ] = {}
792807 for rec in records :
@@ -812,19 +827,21 @@ def validate_installation(self, version: str) -> List[ValidationRecord]:
812827
813828 if not exists :
814829 rec .status = Status .MISSING
830+ rec .installed_version = "-"
815831 rec .message = f"File not found on target: { tgt_p } "
816832 continue
817833
818834 if rec .is_symlink :
819835 if not is_sym :
820836 if actual_md5 and rec .expected_md5 and actual_md5 .lower () == rec .expected_md5 .lower ():
821837 rec .status = Status .MATCH
838+ rec .installed_version = rec .expected_version
822839 rec .message = f"Installed as regular file matching MD5 (expected symlink -> { rec .symlink_target_expected } )"
823840 else :
824841 rec .status = Status .MISMATCH
825- # Match against historical releases
826842 matched_rel = self .repo .lookup_md5 (actual_md5 , Path (rec .rel_path ).name )
827843 rec .matched_release = matched_rel
844+ rec .installed_version = matched_rel .split ()[0 ] if matched_rel else "unknown"
828845 match_info = f" (installed file matches { matched_rel } )" if matched_rel else ""
829846 rec .message = f"Expected symlink -> { rec .symlink_target_expected } , found regular file with MD5 mismatch{ match_info } "
830847 else :
@@ -833,43 +850,54 @@ def validate_installation(self, version: str) -> List[ValidationRecord]:
833850 if norm_exp and norm_act and (norm_exp == norm_act or norm_exp .endswith (norm_act ) or norm_act .endswith (norm_exp )):
834851 if actual_md5 and rec .expected_md5 and actual_md5 .lower () == rec .expected_md5 .lower ():
835852 rec .status = Status .SYMLINK_MATCH
853+ rec .installed_version = rec .expected_version
836854 rec .message = f"Symlink matches -> { sym_target } (resolved MD5 OK)"
837855 elif actual_md5 is None :
838856 rec .status = Status .BROKEN_SYMLINK
857+ rec .installed_version = "broken"
839858 rec .message = f"Broken symlink on target: { tgt_p } -> { sym_target } (target missing)"
840859 else :
841860 rec .status = Status .MISMATCH
842861 matched_rel = self .repo .lookup_md5 (actual_md5 , Path (rec .rel_path ).name )
843862 rec .matched_release = matched_rel
863+ rec .installed_version = matched_rel .split ()[0 ] if matched_rel else "unknown"
844864 match_info = f" (target matches { matched_rel } )" if matched_rel else ""
845865 rec .message = f"Symlink target { sym_target } has MD5 mismatch: expected { rec .expected_md5 } , got { actual_md5 } { match_info } "
846866 else :
847867 rec .status = Status .SYMLINK_MISMATCH
868+ matched_rel = self .repo .lookup_md5 (actual_md5 , Path (rec .rel_path ).name )
869+ rec .matched_release = matched_rel
870+ rec .installed_version = matched_rel .split ()[0 ] if matched_rel else "target mismatch"
848871 rec .message = f"Symlink target mismatch: expected '{ rec .symlink_target_expected } ', got '{ sym_target } '"
849872 else :
850873 if is_sym :
851874 if actual_md5 and rec .expected_md5 and actual_md5 .lower () == rec .expected_md5 .lower ():
852875 rec .status = Status .MATCH
876+ rec .installed_version = rec .expected_version
853877 rec .message = f"Symlink resolved to valid file (MD5 match: { actual_md5 } )"
854878 else :
855879 rec .status = Status .MISMATCH
856880 matched_rel = self .repo .lookup_md5 (actual_md5 , Path (rec .rel_path ).name )
857881 rec .matched_release = matched_rel
882+ rec .installed_version = matched_rel .split ()[0 ] if matched_rel else "unknown"
858883 match_info = f" (target matches { matched_rel } )" if matched_rel else ""
859884 rec .message = f"Symlink resolved to mismatched file: expected { rec .expected_md5 } , got { actual_md5 } { match_info } "
860885 else :
861886 if actual_md5 and rec .expected_md5 :
862887 if actual_md5 .lower () == rec .expected_md5 .lower ():
863888 rec .status = Status .MATCH
889+ rec .installed_version = rec .expected_version
864890 rec .message = "MD5 checksum matched"
865891 else :
866892 rec .status = Status .MISMATCH
867893 matched_rel = self .repo .lookup_md5 (actual_md5 , Path (rec .rel_path ).name )
868894 rec .matched_release = matched_rel
895+ rec .installed_version = matched_rel .split ()[0 ] if matched_rel else "unknown"
869896 match_info = f" (installed file matches { matched_rel } )" if matched_rel else ""
870897 rec .message = f"MD5 mismatch: expected { rec .expected_md5 } , got { actual_md5 } { match_info } "
871898 else :
872899 rec .status = Status .ERROR
900+ rec .installed_version = "error"
873901 rec .message = "Could not compute MD5 checksum on target"
874902
875903 return records
@@ -904,38 +932,43 @@ def format_table(records: List[ValidationRecord], colorize: bool = True) -> str:
904932 RESET = "\033 [0m" if colorize else ""
905933
906934 lines = []
907- lines .append (f"{ BOLD } { 'COMPONENT' :<8} { 'PLATFORM' :<12 } { 'STATUS' :<16} { 'MD5 / MATCHED VERSION ' :<38 } { 'FILE' :<40} { RESET } " )
908- lines .append ("-" * 120 )
935+ lines .append (f"{ BOLD } { 'COMPONENT' :<8} { 'PLATFORM' :<10 } { 'STATUS' :<16} { 'VERSION' :<14 } { ' MD5 CHECKSUM ' :<34 } { 'FILE' :<40} { RESET } " )
936+ lines .append ("-" * 125 )
909937
910938 status_counts : Dict [str , int ] = {}
911939
912940 for r in records :
913941 status_name = r .status .value
914942 status_counts [status_name ] = status_counts .get (status_name , 0 ) + 1
915943
944+ ver_display = r .installed_version or "-"
945+
916946 if r .status in (Status .MATCH , Status .SYMLINK_MATCH ):
917947 c_status = f"{ GREEN } { status_name :<16} { RESET } "
948+ c_ver = f"{ GREEN } { ver_display :<14} { RESET } "
918949 md5_display = (r .actual_md5 or "--------------------------------" )[:32 ]
919- elif r .status in (Status .MISMATCH , Status .SYMLINK_MISMATCH , Status . BROKEN_SYMLINK ):
950+ elif r .status in (Status .MISMATCH , Status .SYMLINK_MISMATCH ):
920951 c_status = f"{ RED } { status_name :<16} { RESET } "
921- if r . matched_release :
922- md5_display = f" { MAGENTA } matches { r . matched_release [: 26 ] } { RESET } "
923- elif r .expected_md5 and r . actual_md5 :
924- md5_display = f"{ r . actual_md5 [: 8 ] } != { r . expected_md5 [: 8 ] } "
925- else :
926- md5_display = (r .actual_md5 or "--------------------------------" )[:32 ]
952+ c_ver = f" { MAGENTA } { ver_display :<14 } { RESET } "
953+ md5_display = ( r . actual_md5 or "--------------------------------" )[: 32 ]
954+ elif r .status == Status . BROKEN_SYMLINK :
955+ c_status = f"{ RED } { status_name :<16 } { RESET } "
956+ c_ver = f" { RED } { ver_display :<14 } { RESET } "
957+ md5_display = (r .actual_md5 or "--------------------------------" )[:32 ]
927958 elif r .status == Status .MISSING :
928959 c_status = f"{ YELLOW } { status_name :<16} { RESET } "
960+ c_ver = f"{ YELLOW } { '-' :<14} { RESET } "
929961 md5_display = f"{ YELLOW } MISSING{ RESET } "
930962 else :
931963 c_status = f"{ CYAN } { status_name :<16} { RESET } "
964+ c_ver = f"{ CYAN } { ver_display :<14} { RESET } "
932965 md5_display = (r .actual_md5 or "--------------------------------" )[:32 ]
933966
934967 lines .append (
935- f"{ r .component :<8} { r .platform :<12 } { c_status } { md5_display :<38 } { r .rel_path } "
968+ f"{ r .component :<8} { r .platform :<10 } { c_status } { c_ver } { md5_display :<34 } { r .rel_path } "
936969 )
937970
938- lines .append ("=" * 120 )
971+ lines .append ("=" * 125 )
939972 total = len (records )
940973 passed = status_counts .get (Status .MATCH .value , 0 ) + status_counts .get (Status .SYMLINK_MATCH .value , 0 )
941974 failed = status_counts .get (Status .MISMATCH .value , 0 ) + status_counts .get (Status .SYMLINK_MISMATCH .value , 0 ) + status_counts .get (Status .BROKEN_SYMLINK .value , 0 )
0 commit comments