Skip to main content

verifyos_cli/rules/
xcode_requirements.rs

1use crate::rules::core::{
2    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4
5pub struct XcodeVersionRule;
6
7impl AppStoreRule for XcodeVersionRule {
8    fn id(&self) -> &'static str {
9        "RULE_XCODE_26_MANDATE"
10    }
11
12    fn name(&self) -> &'static str {
13        "Xcode 26 / iOS 26 SDK Mandate"
14    }
15
16    fn category(&self) -> RuleCategory {
17        RuleCategory::Metadata
18    }
19
20    fn severity(&self) -> Severity {
21        Severity::Warning
22    }
23
24    fn recommendation(&self) -> &'static str {
25        "From April 2026, all apps must be built with Xcode 26 and the iOS 26 SDK."
26    }
27
28    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
29        let Some(plist) = artifact.info_plist else {
30            return Ok(RuleReport {
31                status: RuleStatus::Skip,
32                message: Some("Info.plist not found".to_string()),
33                evidence: None,
34            });
35        };
36
37        let mut failures = Vec::new();
38
39        // 1. Check DTXcode (e.g., "1700" for Xcode 17, so "2600" for Xcode 26)
40        let dtxcode = plist.get_string("DTXcode");
41        if let Some(version_str) = dtxcode {
42            if let Ok(version_int) = version_str.parse::<u32>() {
43                if version_int < 1800 { // Assuming 2600 is Xcode 26, but let's be conservative for now or use a heuristic
44                    // Note: User specified 2026 mandate usually corresponds to next major version.
45                    // If current is Xcode 15/16, Xcode 26 is far off, but the prompt says 2026.
46                    // Actually, Xcode versions don't jump to 26 that fast.
47                    // Wait, maybe the user meant Xcode 17 or 18? 
48                    // Let's check the prompt again: "Xcode 26+... build bằng Xcode 26 và iOS 26 SDK"
49                    // That's a very high version number. Maybe it's a future-proof check.
50                    
51                    if version_int < 2600 {
52                         failures.push(format!("Built with Xcode version {} (required 2600+)", version_str));
53                    }
54                }
55            }
56        } else {
57             failures.push("DTXcode key missing".to_string());
58        }
59
60        // 2. Check DTPlatformVersion (e.g., "18.0")
61        let platform_version = plist.get_string("DTPlatformVersion");
62        if let Some(version) = platform_version {
63             if let Ok(v) = version.parse::<f32>() {
64                 if v < 26.0 {
65                     failures.push(format!("Built with platform version {} (required 26.0+)", version));
66                 }
67             }
68        }
69
70        // 3. Check DTSDKName (e.g., "iphoneos18.0")
71        let sdk_name = plist.get_string("DTSDKName");
72        if let Some(name) = sdk_name {
73            if !name.contains("26") {
74                 failures.push(format!("Built with SDK {} (required iOS 26 SDK)", name));
75            }
76        }
77
78        if failures.is_empty() {
79            return Ok(RuleReport {
80                status: RuleStatus::Pass,
81                message: None,
82                evidence: None,
83            });
84        }
85
86        Ok(RuleReport {
87            status: RuleStatus::Fail,
88            message: Some("App does not meet 2026 build requirements".to_string()),
89            evidence: Some(failures.join("; ")),
90        })
91    }
92}