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 {
44                    // Assuming 2600 is Xcode 26, but let's be conservative for now or use a heuristic
45                    // Note: User specified 2026 mandate usually corresponds to next major version.
46                    // If current is Xcode 15/16, Xcode 26 is far off, but the prompt says 2026.
47                    // Actually, Xcode versions don't jump to 26 that fast.
48                    // Wait, maybe the user meant Xcode 17 or 18?
49                    // Let's check the prompt again: "Xcode 26+... build bằng Xcode 26 và iOS 26 SDK"
50                    // That's a very high version number. Maybe it's a future-proof check.
51
52                    if version_int < 2600 {
53                        failures.push(format!(
54                            "Built with Xcode version {} (required 2600+)",
55                            version_str
56                        ));
57                    }
58                }
59            }
60        } else {
61            failures.push("DTXcode key missing".to_string());
62        }
63
64        // 2. Check DTPlatformVersion (e.g., "18.0")
65        let platform_version = plist.get_string("DTPlatformVersion");
66        if let Some(version) = platform_version {
67            if let Ok(v) = version.parse::<f32>() {
68                if v < 26.0 {
69                    failures.push(format!(
70                        "Built with platform version {} (required 26.0+)",
71                        version
72                    ));
73                }
74            }
75        }
76
77        // 3. Check DTSDKName (e.g., "iphoneos18.0")
78        let sdk_name = plist.get_string("DTSDKName");
79        if let Some(name) = sdk_name {
80            if !name.contains("26") {
81                failures.push(format!("Built with SDK {} (required iOS 26 SDK)", name));
82            }
83        }
84
85        if failures.is_empty() {
86            return Ok(RuleReport {
87                status: RuleStatus::Pass,
88                message: None,
89                evidence: None,
90            });
91        }
92
93        Ok(RuleReport {
94            status: RuleStatus::Fail,
95            message: Some("App does not meet 2026 build requirements".to_string()),
96            evidence: Some(failures.join("; ")),
97        })
98    }
99}