zccache_depgraph/
compile_commands.rs1use zccache_core::NormalizedPath;
7
8use serde::Deserialize;
9
10use crate::args::{parse_compile_args, split_command, ParsedArgs};
11
12#[derive(Debug, Clone, Deserialize)]
14pub struct CompileCommand {
15 pub directory: NormalizedPath,
17 pub file: NormalizedPath,
19 pub command: Option<String>,
21 pub arguments: Option<Vec<String>>,
23 pub output: Option<NormalizedPath>,
25}
26
27impl CompileCommand {
28 pub fn args_without_compiler(&self) -> Vec<String> {
31 if let Some(ref args) = self.arguments {
32 if args.len() > 1 {
33 args[1..].to_vec()
34 } else {
35 Vec::new()
36 }
37 } else if let Some(ref cmd) = self.command {
38 let parts = split_command(cmd);
39 if parts.len() > 1 {
40 parts[1..].to_vec()
41 } else {
42 Vec::new()
43 }
44 } else {
45 Vec::new()
46 }
47 }
48
49 pub fn compiler(&self) -> Option<NormalizedPath> {
51 if let Some(ref args) = self.arguments {
52 args.first().map(|s| s.as_str().into())
53 } else if let Some(ref cmd) = self.command {
54 split_command(cmd)
55 .into_iter()
56 .next()
57 .map(|s| s.as_str().into())
58 } else {
59 None
60 }
61 }
62
63 pub fn parse(&self) -> ParsedArgs {
65 let args = self.args_without_compiler();
66 let mut parsed = parse_compile_args(&args, &self.directory);
67 parsed.compiler = self.compiler();
68
69 if parsed.source_file.as_os_str().is_empty() {
71 if self.file.is_absolute() {
72 parsed.source_file = self.file.clone();
73 } else {
74 parsed.source_file = self.directory.join(&self.file);
75 }
76 }
77
78 parsed
79 }
80}
81
82pub fn parse_compile_commands_json(json: &str) -> Result<Vec<CompileCommand>, serde_json::Error> {
88 serde_json::from_str(json)
89}
90
91#[cfg(test)]
92mod tests {
93 use std::path::Path;
94 use zccache_core::NormalizedPath;
95
96 use super::*;
97
98 #[test]
99 fn parse_with_command_string() {
100 let json = r#"[
101 {
102 "directory": "/home/user/project/build",
103 "command": "cc -I../src -DNDEBUG -std=c17 -c ../src/foo.c -o foo.o",
104 "file": "../src/foo.c"
105 }
106 ]"#;
107
108 let commands = parse_compile_commands_json(json).unwrap();
109 assert_eq!(commands.len(), 1);
110
111 let parsed = commands[0].parse();
112 assert_eq!(
113 parsed.source_file,
114 Path::new("/home/user/project/build/../src/foo.c")
115 );
116 assert_eq!(
117 parsed.output_file.as_deref(),
118 Some(Path::new("/home/user/project/build/foo.o"))
119 );
120 assert_eq!(
121 parsed.include_search.user,
122 vec![Path::new("/home/user/project/build/../src")]
123 );
124 assert_eq!(parsed.defines, vec!["NDEBUG"]);
125 assert!(parsed.flags.contains(&"-std=c17".to_string()));
126 assert_eq!(parsed.compiler, Some("cc".into()));
127 }
128
129 #[test]
130 fn parse_with_arguments_array() {
131 let json = r#"[
132 {
133 "directory": "/build",
134 "arguments": ["clang++", "-std=c++17", "-I", "/include", "-c", "main.cpp", "-o", "main.o"],
135 "file": "main.cpp"
136 }
137 ]"#;
138
139 let commands = parse_compile_commands_json(json).unwrap();
140 let parsed = commands[0].parse();
141 assert_eq!(parsed.source_file, Path::new("/build/main.cpp"));
142 assert_eq!(parsed.include_search.user, vec![Path::new("/include")]);
143 assert!(parsed.flags.contains(&"-std=c++17".to_string()));
144 assert_eq!(parsed.compiler, Some("clang++".into()));
145 }
146
147 #[test]
148 fn parse_multiple_entries() {
149 let json = r#"[
150 {
151 "directory": "/build",
152 "command": "cc -c a.c",
153 "file": "a.c"
154 },
155 {
156 "directory": "/build",
157 "command": "cc -c b.c",
158 "file": "b.c"
159 }
160 ]"#;
161
162 let commands = parse_compile_commands_json(json).unwrap();
163 assert_eq!(commands.len(), 2);
164 assert_eq!(commands[0].parse().source_file, Path::new("/build/a.c"));
165 assert_eq!(commands[1].parse().source_file, Path::new("/build/b.c"));
166 }
167
168 #[test]
169 fn source_file_fallback_to_file_field() {
170 let json = r#"[
171 {
172 "directory": "/build",
173 "command": "cc -c",
174 "file": "src/main.c"
175 }
176 ]"#;
177
178 let commands = parse_compile_commands_json(json).unwrap();
179 let parsed = commands[0].parse();
180 assert_eq!(parsed.source_file, Path::new("/build/src/main.c"));
182 }
183
184 #[test]
185 fn absolute_file_field() {
186 let json = r#"[
187 {
188 "directory": "/build",
189 "command": "cc -c",
190 "file": "/src/main.c"
191 }
192 ]"#;
193
194 let commands = parse_compile_commands_json(json).unwrap();
195 let parsed = commands[0].parse();
196 assert_eq!(parsed.source_file, Path::new("/src/main.c"));
197 }
198
199 #[test]
200 fn empty_json() {
201 let commands = parse_compile_commands_json("[]").unwrap();
202 assert!(commands.is_empty());
203 }
204
205 #[test]
206 fn malformed_json_returns_error() {
207 let result = parse_compile_commands_json("not json");
208 assert!(result.is_err());
209 }
210
211 #[test]
212 fn with_output_field() {
213 let json = r#"[
214 {
215 "directory": "/build",
216 "command": "cc -c foo.c -o foo.o",
217 "file": "foo.c",
218 "output": "foo.o"
219 }
220 ]"#;
221
222 let commands = parse_compile_commands_json(json).unwrap();
223 assert_eq!(commands[0].output, Some("foo.o".into()));
224 }
225
226 #[test]
227 fn complex_cmake_style() {
228 let json = r#"[
229 {
230 "directory": "/home/user/project/build",
231 "command": "/usr/bin/g++ -DPROJECT_VERSION=\"1.0\" -I/home/user/project/src -I/home/user/project/include -isystem /usr/local/include/boost -std=c++20 -O2 -Wall -Wextra -fPIC -pthread -o CMakeFiles/app.dir/src/main.cpp.o -c /home/user/project/src/main.cpp",
232 "file": "/home/user/project/src/main.cpp"
233 }
234 ]"#;
235
236 let commands = parse_compile_commands_json(json).unwrap();
237 let parsed = commands[0].parse();
238 assert_eq!(
239 parsed.source_file,
240 Path::new("/home/user/project/src/main.cpp")
241 );
242 assert_eq!(parsed.include_search.user.len(), 2);
243 assert_eq!(parsed.include_search.system.len(), 1);
244 assert!(parsed.defines.contains(&"PROJECT_VERSION=1.0".to_string()));
245 assert!(parsed.flags.contains(&"-std=c++20".to_string()));
246 assert!(parsed.flags.contains(&"-O2".to_string()));
247 assert!(parsed.flags.contains(&"-fPIC".to_string()));
248 assert!(parsed.flags.contains(&"-pthread".to_string()));
249 assert_eq!(parsed.compiler, Some(NormalizedPath::from("/usr/bin/g++")));
250 }
251}