@@ -24,6 +24,7 @@ pub struct EngineResult {
2424 pub rule_name : & ' static str ,
2525 pub category : RuleCategory ,
2626 pub severity : Severity ,
27+ pub target : String ,
2728 pub recommendation : & ' static str ,
2829 pub report : Result < RuleReport , RuleError > ,
2930 pub duration_ms : u128 ,
@@ -61,52 +62,72 @@ impl Engine {
6162 }
6263
6364 let extracted_ipa = extract_ipa ( path) ?;
64-
65- // Attempt to discover project metadata during extraction
66- let discovered_project_path = extracted_ipa
67- . get_project_path ( )
65+ let targets = extracted_ipa
66+ . discover_targets ( )
6867 . map_err ( |e| OrchestratorError :: Extraction ( ExtractionError :: Io ( e) ) ) ?;
6968
70- let discovered_project = discovered_project_path. as_ref ( ) . and_then ( |p| {
71- if p. extension ( ) . is_some_and ( |e| e == "xcworkspace" ) {
72- crate :: parsers:: xcworkspace_parser:: Xcworkspace :: from_path ( p)
69+ let mut all_results = Vec :: new ( ) ;
70+ let mut total_cache_stats = ArtifactCacheStats :: default ( ) ;
71+
72+ if targets. is_empty ( ) {
73+ // Fallback to original logic if no specific targets found
74+ let res = self . run_on_bundle_internal ( & extracted_ipa. payload_dir , run_started, None ) ?;
75+ return Ok ( res) ;
76+ }
77+
78+ for ( target_path, target_type) in targets {
79+ let project_context = if target_type == "app" {
80+ // Try to find a project context for this app if it's in a larger folder
81+ extracted_ipa. get_project_path ( ) . ok ( ) . flatten ( ) . and_then ( |p| {
82+ if p. extension ( ) . is_some_and ( |e| e == "xcworkspace" ) {
83+ crate :: parsers:: xcworkspace_parser:: Xcworkspace :: from_path ( & p)
84+ . ok ( )
85+ . and_then ( |ws| ws. project_paths . first ( ) . cloned ( ) )
86+ . and_then ( |proj_path| {
87+ crate :: parsers:: xcode_parser:: XcodeProject :: from_path ( proj_path) . ok ( )
88+ } )
89+ } else {
90+ crate :: parsers:: xcode_parser:: XcodeProject :: from_path ( & p) . ok ( )
91+ }
92+ } )
93+ } else if target_type == "project" {
94+ crate :: parsers:: xcode_parser:: XcodeProject :: from_path ( & target_path) . ok ( )
95+ } else if target_type == "workspace" {
96+ crate :: parsers:: xcworkspace_parser:: Xcworkspace :: from_path ( & target_path)
7397 . ok ( )
7498 . and_then ( |ws| ws. project_paths . first ( ) . cloned ( ) )
7599 . and_then ( |proj_path| {
76100 crate :: parsers:: xcode_parser:: XcodeProject :: from_path ( proj_path) . ok ( )
77101 } )
78102 } else {
79- crate :: parsers:: xcode_parser:: XcodeProject :: from_path ( p) . ok ( )
103+ None
104+ } ;
105+
106+ let app_results =
107+ self . run_on_bundle_internal ( & target_path, run_started, project_context) ?;
108+
109+ // Tag results with target name
110+ let target_name = target_path
111+ . file_name ( )
112+ . map ( |n| n. to_string_lossy ( ) . into_owned ( ) )
113+ . unwrap_or_else ( || "Unknown" . to_string ( ) ) ;
114+
115+ for mut res in app_results. results {
116+ res. target = target_name. clone ( ) ;
117+ all_results. push ( res) ;
80118 }
81- } ) ;
82-
83- let app_bundle_path = extracted_ipa
84- . get_app_bundle_path ( )
85- . map_err ( |e| OrchestratorError :: Extraction ( ExtractionError :: Io ( e) ) ) ?;
86119
87- let app_bundle_path = match app_bundle_path {
88- Some ( p) => p,
89- None => {
90- // If we found a project but no .app, we can still proceed with the extraction root
91- // as a context and let rules that check project metadata run.
92- if discovered_project_path. is_some ( ) {
93- extracted_ipa. payload_dir . clone ( )
94- } else {
95- let mut entries = Vec :: new ( ) ;
96- if let Ok ( rd) = std:: fs:: read_dir ( & extracted_ipa. payload_dir ) {
97- for entry in rd. flatten ( ) . take ( 10 ) {
98- entries. push ( entry. file_name ( ) . to_string_lossy ( ) . into_owned ( ) ) ;
99- }
100- }
101- return Err ( OrchestratorError :: AppBundleNotFoundWithContext (
102- extracted_ipa. payload_dir . display ( ) . to_string ( ) ,
103- entries. join ( ", " ) ,
104- ) ) ;
105- }
106- }
107- } ;
120+ // Merge stats (simplified)
121+ total_cache_stats. nested_bundles . hits += app_results. cache_stats . nested_bundles . hits ;
122+ total_cache_stats. nested_bundles . misses += app_results. cache_stats . nested_bundles . misses ;
123+ // ... other stats could be merged if needed, but for now we focus on results
124+ }
108125
109- self . run_on_bundle_internal ( & app_bundle_path, run_started, discovered_project)
126+ Ok ( EngineRun {
127+ results : all_results,
128+ total_duration_ms : run_started. elapsed ( ) . as_millis ( ) ,
129+ cache_stats : total_cache_stats,
130+ } )
110131 }
111132
112133 pub fn run_on_bundle < P : AsRef < Path > > (
@@ -147,6 +168,10 @@ impl Engine {
147168 rule_name : rule. name ( ) ,
148169 category : rule. category ( ) ,
149170 severity : rule. severity ( ) ,
171+ target : app_bundle_path
172+ . file_name ( )
173+ . map ( |n| n. to_string_lossy ( ) . into_owned ( ) )
174+ . unwrap_or_else ( || "Bundle" . to_string ( ) ) ,
150175 recommendation : rule. recommendation ( ) ,
151176 report : res,
152177 duration_ms : rule_started. elapsed ( ) . as_millis ( ) ,
0 commit comments