Skip to main content

null_e/plugins/
swift.rs

1//! Swift/Xcode plugin
2
3use crate::core::{Artifact, ArtifactKind, ArtifactMetadata, MarkerKind, ProjectKind, ProjectMarker};
4use crate::error::Result;
5use crate::plugins::Plugin;
6use std::path::Path;
7
8/// Plugin for Swift/Xcode projects
9pub struct SwiftPlugin;
10
11impl Plugin for SwiftPlugin {
12    fn id(&self) -> &'static str {
13        "swift"
14    }
15
16    fn name(&self) -> &'static str {
17        "Swift/Xcode"
18    }
19
20    fn supported_kinds(&self) -> &[ProjectKind] {
21        &[ProjectKind::SwiftSpm, ProjectKind::SwiftXcode]
22    }
23
24    fn markers(&self) -> Vec<ProjectMarker> {
25        vec![
26            ProjectMarker {
27                indicator: MarkerKind::File("Package.swift"),
28                kind: ProjectKind::SwiftSpm,
29                priority: 60,
30            },
31            ProjectMarker {
32                indicator: MarkerKind::Extension("xcodeproj"),
33                kind: ProjectKind::SwiftXcode,
34                priority: 55,
35            },
36            ProjectMarker {
37                indicator: MarkerKind::Extension("xcworkspace"),
38                kind: ProjectKind::SwiftXcode,
39                priority: 55,
40            },
41        ]
42    }
43
44    fn detect(&self, path: &Path) -> Option<ProjectKind> {
45        // Check for SPM first (higher priority)
46        if path.join("Package.swift").is_file() {
47            return Some(ProjectKind::SwiftSpm);
48        }
49
50        // Check for Xcode project
51        if let Ok(entries) = std::fs::read_dir(path) {
52            for entry in entries.filter_map(|e| e.ok()) {
53                if let Some(name) = entry.file_name().to_str() {
54                    if name.ends_with(".xcodeproj") || name.ends_with(".xcworkspace") {
55                        return Some(ProjectKind::SwiftXcode);
56                    }
57                }
58            }
59        }
60
61        None
62    }
63
64    fn find_artifacts(&self, project_root: &Path) -> Result<Vec<Artifact>> {
65        let mut artifacts = Vec::new();
66
67        // .build directory (Swift Package Manager)
68        let build = project_root.join(".build");
69        if build.exists() {
70            artifacts.push(Artifact {
71                path: build,
72                kind: ArtifactKind::BuildOutput,
73                size: 0,
74                file_count: 0,
75                age: None,
76                metadata: ArtifactMetadata::restorable("swift build"),
77            });
78        }
79
80        // .swiftpm directory
81        let swiftpm = project_root.join(".swiftpm");
82        if swiftpm.exists() {
83            artifacts.push(Artifact {
84                path: swiftpm,
85                kind: ArtifactKind::Cache,
86                size: 0,
87                file_count: 0,
88                age: None,
89                metadata: ArtifactMetadata::default(),
90            });
91        }
92
93        // Pods directory (CocoaPods)
94        let pods = project_root.join("Pods");
95        if pods.exists() {
96            artifacts.push(Artifact {
97                path: pods,
98                kind: ArtifactKind::Dependencies,
99                size: 0,
100                file_count: 0,
101                age: None,
102                metadata: ArtifactMetadata {
103                    restorable: true,
104                    restore_command: Some("pod install".into()),
105                    lockfile: Some(project_root.join("Podfile.lock")),
106                    ..Default::default()
107                },
108            });
109        }
110
111        // DerivedData (if in project - usually in ~/Library)
112        let derived_data = project_root.join("DerivedData");
113        if derived_data.exists() {
114            artifacts.push(Artifact {
115                path: derived_data,
116                kind: ArtifactKind::BuildOutput,
117                size: 0,
118                file_count: 0,
119                age: None,
120                metadata: ArtifactMetadata::restorable("xcodebuild"),
121            });
122        }
123
124        // build directory
125        let build_dir = project_root.join("build");
126        if build_dir.exists() {
127            artifacts.push(Artifact {
128                path: build_dir,
129                kind: ArtifactKind::BuildOutput,
130                size: 0,
131                file_count: 0,
132                age: None,
133                metadata: ArtifactMetadata::restorable("xcodebuild"),
134            });
135        }
136
137        Ok(artifacts)
138    }
139
140    fn cleanable_dirs(&self) -> &[&'static str] {
141        &[".build", ".swiftpm", "Pods", "DerivedData", "build"]
142    }
143
144    fn priority(&self) -> u8 {
145        55
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use tempfile::TempDir;
153
154    #[test]
155    fn test_detect_spm() {
156        let temp = TempDir::new().unwrap();
157        std::fs::write(
158            temp.path().join("Package.swift"),
159            "// swift-tools-version:5.5\nimport PackageDescription\n",
160        )
161        .unwrap();
162
163        let plugin = SwiftPlugin;
164        assert_eq!(plugin.detect(temp.path()), Some(ProjectKind::SwiftSpm));
165    }
166
167    #[test]
168    fn test_detect_xcode() {
169        let temp = TempDir::new().unwrap();
170        std::fs::create_dir(temp.path().join("MyApp.xcodeproj")).unwrap();
171
172        let plugin = SwiftPlugin;
173        assert_eq!(plugin.detect(temp.path()), Some(ProjectKind::SwiftXcode));
174    }
175
176    #[test]
177    fn test_find_artifacts() {
178        let temp = TempDir::new().unwrap();
179        std::fs::write(temp.path().join("Package.swift"), "").unwrap();
180        std::fs::create_dir(temp.path().join(".build")).unwrap();
181        std::fs::create_dir(temp.path().join("Pods")).unwrap();
182
183        let plugin = SwiftPlugin;
184        let artifacts = plugin.find_artifacts(temp.path()).unwrap();
185
186        assert_eq!(artifacts.len(), 2);
187    }
188}