Skip to content

Commit 4f67fe1

Browse files
Add API to Record Compile Invocation
This commit adds API to record the compilation invocations in order to emit compile_commands.json a.k.a. JSON compilation database. Fixes #497
1 parent f2e1b1c commit 4f67fe1

File tree

3 files changed

+181
-23
lines changed

3 files changed

+181
-23
lines changed

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ edition = "2018"
1919

2020
[dependencies]
2121
jobserver = { version = "0.1.16", optional = true }
22+
tinyjson = { version = "2.3.0", optional = true }
2223

2324
[features]
2425
parallel = ["jobserver"]
26+
compile_commands = ["tinyjson"]
2527

2628
[dev-dependencies]
2729
tempfile = "3"

src/json_compilation_database.rs

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use std::path::{Path, PathBuf};
2+
use std::process::Command;
3+
#[cfg(feature = "compile_commands")]
4+
use tinyjson::JsonValue;
5+
6+
/// An entry for creating a [JSON Compilation Database](https://clang.llvm.org/docs/JSONCompilationDatabase.html).
7+
pub struct CompileCommand {
8+
directory: PathBuf,
9+
arguments: Vec<String>,
10+
file: PathBuf,
11+
output: PathBuf,
12+
}
13+
14+
impl CompileCommand {
15+
pub(crate) fn new(cmd: &Command, src: PathBuf, output: PathBuf) -> Self {
16+
let mut arguments = Vec::with_capacity(cmd.get_args().len() + 1);
17+
18+
let program = String::from(cmd.get_program().to_str().unwrap());
19+
arguments.push(
20+
crate::which(&program)
21+
.map(|p| p.to_string_lossy().into_owned())
22+
.map(|p| p.to_string())
23+
.unwrap_or(program),
24+
);
25+
arguments.extend(
26+
cmd.get_args()
27+
.flat_map(std::ffi::OsStr::to_str)
28+
.map(String::from),
29+
);
30+
31+
Self {
32+
// TODO: is the assumption correct?
33+
directory: std::env::current_dir().unwrap(),
34+
arguments,
35+
file: src,
36+
output,
37+
}
38+
}
39+
40+
/// The working directory of the compilation. All paths specified in the command or file fields
41+
/// must be either absolute or relative to this directory.
42+
pub fn directory(&self) -> &PathBuf {
43+
&self.directory
44+
}
45+
46+
/// The name of the output created by this compilation step. This field is optional. It can be
47+
/// used to distinguish different processing modes of the same input file.
48+
pub fn output(&self) -> &PathBuf {
49+
&self.output
50+
}
51+
52+
/// The main translation unit source processed by this compilation step. This is used by tools
53+
/// as the key into the compilation database. There can be multiple command objects for the
54+
/// same file, for example if the same source file is compiled with different configurations.
55+
pub fn file(&self) -> &PathBuf {
56+
&self.file
57+
}
58+
59+
/// The compile command argv as list of strings. This should run the compilation step for the
60+
/// translation unit file. arguments[0] should be the executable name, such as clang++.
61+
/// Arguments should not be escaped, but ready to pass to execvp().
62+
pub fn arguments(&self) -> &Vec<String> {
63+
&self.arguments
64+
}
65+
}
66+
67+
/// Stores the provided list of [compile commands](crate::CompileCommand) as [JSON
68+
/// Compilation Database](https://clang.llvm.org/docs/JSONCompilationDatabase.html).
69+
#[cfg(feature = "compile_commands")]
70+
pub fn store_json_compilation_database<'a, C, P>(commands: C, path: P)
71+
where
72+
C: IntoIterator<Item = &'a CompileCommand>,
73+
P: AsRef<Path>,
74+
{
75+
let db = JsonValue::Array(
76+
commands
77+
.into_iter()
78+
.map(|command| command.into())
79+
.collect::<Vec<JsonValue>>(),
80+
);
81+
82+
std::fs::write(path, db.stringify().unwrap()).unwrap();
83+
}
84+
85+
#[cfg(feature = "compile_commands")]
86+
impl<'a> std::convert::From<&CompileCommand> for JsonValue {
87+
fn from(compile_command: &CompileCommand) -> Self {
88+
use std::collections::HashMap;
89+
JsonValue::Object(HashMap::from([
90+
(
91+
String::from("directory"),
92+
JsonValue::String(compile_command.directory.to_string_lossy().to_string()),
93+
),
94+
(
95+
String::from("file"),
96+
JsonValue::String(compile_command.file.to_string_lossy().to_string()),
97+
),
98+
(
99+
String::from("output"),
100+
JsonValue::String(compile_command.output.to_string_lossy().to_string()),
101+
),
102+
(
103+
String::from("arguments"),
104+
JsonValue::Array(
105+
compile_command
106+
.arguments
107+
.iter()
108+
.map(|arg| JsonValue::String(arg.to_string()))
109+
.collect::<Vec<_>>(),
110+
),
111+
),
112+
]))
113+
}
114+
}

src/lib.rs

+65-23
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
#![allow(deprecated)]
5757
#![deny(missing_docs)]
5858

59+
pub use crate::json_compilation_database::store_json_compilation_database;
60+
pub use crate::json_compilation_database::CompileCommand;
5961
use std::collections::HashMap;
6062
use std::env;
6163
use std::ffi::{OsStr, OsString};
@@ -81,6 +83,7 @@ mod setup_config;
8183
#[cfg(windows)]
8284
mod vs_instances;
8385

86+
mod json_compilation_database;
8487
pub mod windows_registry;
8588

8689
/// A builder for compilation of a native library.
@@ -943,8 +946,17 @@ impl Build {
943946

944947
/// Run the compiler, generating the file `output`
945948
///
946-
/// This will return a result instead of panicing; see compile() for the complete description.
949+
/// This will return a result instead of panicing; see [compile()](Build::compile) for the complete description.
947950
pub fn try_compile(&self, output: &str) -> Result<(), Error> {
951+
self.try_recorded_compile(output)?;
952+
Ok(())
953+
}
954+
955+
/// Run the compiler, generating the file `output` and provides compile commands for creating
956+
/// [JSON Compilation Database](https://clang.llvm.org/docs/JSONCompilationDatabase.html).
957+
///
958+
/// This will return a result instead of panicing; see [recorded_compile()](Build::recorded_compile) for the complete description.
959+
pub fn try_recorded_compile(&self, output: &str) -> Result<Vec<CompileCommand>, Error> {
948960
let mut output_components = Path::new(output).components();
949961
match (output_components.next(), output_components.next()) {
950962
(Some(Component::Normal(_)), None) => {}
@@ -990,7 +1002,7 @@ impl Build {
9901002

9911003
objects.push(Object::new(file.to_path_buf(), obj));
9921004
}
993-
self.compile_objects(&objects)?;
1005+
let entries = self.compile_objects(&objects)?;
9941006
self.assemble(lib_name, &dst.join(gnu_lib_name), &objects)?;
9951007

9961008
if self.get_target()?.contains("msvc") {
@@ -1074,7 +1086,7 @@ impl Build {
10741086
}
10751087
}
10761088

1077-
Ok(())
1089+
Ok(entries)
10781090
}
10791091

10801092
/// Run the compiler, generating the file `output`
@@ -1120,8 +1132,28 @@ impl Build {
11201132
}
11211133
}
11221134

1135+
/// Run the compiler, generating the file `output` and provides compile commands for creating
1136+
/// [JSON Compilation Database](https://clang.llvm.org/docs/JSONCompilationDatabase.html),
1137+
///
1138+
/// ```no_run
1139+
/// let compile_commands = cc::Build::new().file("blobstore.c")
1140+
/// .recorded_compile("blobstore");
1141+
///
1142+
/// cc::store_json_compilation_database(&compile_commands, "target/compilation_database.json");
1143+
/// ```
1144+
///
1145+
/// See [compile()](Build::compile) for the further description.
1146+
pub fn recorded_compile(&self, output: &str) -> Vec<CompileCommand> {
1147+
match self.try_recorded_compile(output) {
1148+
Ok(entries) => entries,
1149+
Err(e) => {
1150+
fail(&e.message);
1151+
}
1152+
}
1153+
}
1154+
11231155
#[cfg(feature = "parallel")]
1124-
fn compile_objects<'me>(&'me self, objs: &[Object]) -> Result<(), Error> {
1156+
fn compile_objects<'me>(&'me self, objs: &[Object]) -> Result<Vec<CompileCommand>, Error> {
11251157
use std::sync::atomic::{AtomicBool, Ordering::SeqCst};
11261158
use std::sync::Once;
11271159

@@ -1191,9 +1223,11 @@ impl Build {
11911223
threads.push(JoinOnDrop(Some(thread)));
11921224
}
11931225

1226+
let mut entries = Vec::new();
1227+
11941228
for mut thread in threads {
11951229
if let Some(thread) = thread.0.take() {
1196-
thread.join().expect("thread should not panic")?;
1230+
entries.push(thread.join().expect("thread should not panic")?);
11971231
}
11981232
}
11991233

@@ -1203,7 +1237,7 @@ impl Build {
12031237
server.acquire_raw()?;
12041238
}
12051239

1206-
return Ok(());
1240+
return Ok(entries);
12071241

12081242
/// Shared state from the parent thread to the child thread. This
12091243
/// package of pointers is temporarily transmuted to a `'static`
@@ -1260,7 +1294,7 @@ impl Build {
12601294
return client;
12611295
}
12621296

1263-
struct JoinOnDrop(Option<thread::JoinHandle<Result<(), Error>>>);
1297+
struct JoinOnDrop(Option<thread::JoinHandle<Result<CompileCommand, Error>>>);
12641298

12651299
impl Drop for JoinOnDrop {
12661300
fn drop(&mut self) {
@@ -1272,14 +1306,15 @@ impl Build {
12721306
}
12731307

12741308
#[cfg(not(feature = "parallel"))]
1275-
fn compile_objects(&self, objs: &[Object]) -> Result<(), Error> {
1309+
fn compile_objects(&self, objs: &[Object]) -> Result<Vec<CompileCommand>, Error> {
1310+
let mut entries = Vec::new();
12761311
for obj in objs {
1277-
self.compile_object(obj)?;
1312+
entries.push(self.compile_object(obj)?);
12781313
}
1279-
Ok(())
1314+
Ok(entries)
12801315
}
12811316

1282-
fn compile_object(&self, obj: &Object) -> Result<(), Error> {
1317+
fn compile_object(&self, obj: &Object) -> Result<CompileCommand, Error> {
12831318
let is_asm = obj.src.extension().and_then(|s| s.to_str()) == Some("asm");
12841319
let target = self.get_target()?;
12851320
let msvc = target.contains("msvc");
@@ -1324,7 +1359,7 @@ impl Build {
13241359
}
13251360

13261361
run(&mut cmd, &name)?;
1327-
Ok(())
1362+
Ok(CompileCommand::new(&cmd, obj.src.clone(), obj.dst.clone()))
13281363
}
13291364

13301365
/// This will return a result instead of panicing; see expand() for the complete description.
@@ -3287,22 +3322,29 @@ fn map_darwin_target_from_rust_to_compiler_architecture(target: &str) -> Option<
32873322
}
32883323
}
32893324

3290-
fn which(tool: &Path) -> Option<PathBuf> {
3325+
pub(crate) fn which<P>(tool: P) -> Option<PathBuf>
3326+
where
3327+
P: AsRef<Path>,
3328+
{
32913329
fn check_exe(exe: &mut PathBuf) -> bool {
32923330
let exe_ext = std::env::consts::EXE_EXTENSION;
32933331
exe.exists() || (!exe_ext.is_empty() && exe.set_extension(exe_ext) && exe.exists())
32943332
}
32953333

3296-
// If |tool| is not just one "word," assume it's an actual path...
3297-
if tool.components().count() > 1 {
3298-
let mut exe = PathBuf::from(tool);
3299-
return if check_exe(&mut exe) { Some(exe) } else { None };
3334+
fn non_generic_which(tool: &Path) -> Option<PathBuf> {
3335+
// If |tool| is not just one "word," assume it's an actual path...
3336+
if tool.components().count() > 1 {
3337+
let mut exe = PathBuf::from(tool);
3338+
return if check_exe(&mut exe) { Some(exe) } else { None };
3339+
}
3340+
3341+
// Loop through PATH entries searching for the |tool|.
3342+
let path_entries = env::var_os("PATH")?;
3343+
env::split_paths(&path_entries).find_map(|path_entry| {
3344+
let mut exe = path_entry.join(tool);
3345+
return if check_exe(&mut exe) { Some(exe) } else { None };
3346+
})
33003347
}
33013348

3302-
// Loop through PATH entries searching for the |tool|.
3303-
let path_entries = env::var_os("PATH")?;
3304-
env::split_paths(&path_entries).find_map(|path_entry| {
3305-
let mut exe = path_entry.join(tool);
3306-
return if check_exe(&mut exe) { Some(exe) } else { None };
3307-
})
3349+
non_generic_which(tool.as_ref())
33083350
}

0 commit comments

Comments
 (0)