Skip to content

Commit 67d9f9e

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 67d9f9e

File tree

3 files changed

+171
-19
lines changed

3 files changed

+171
-19
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ edition = "2018"
1919

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

2324
[features]
2425
parallel = ["jobserver"]

src/json_compilation_database.rs

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::collections::HashMap;
2+
use std::path::{Path, PathBuf};
3+
use std::process::Command;
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+
pub fn store_json_compilation_database<'a, C, P>(commands: C, path: P)
70+
where
71+
C: IntoIterator<Item = &'a CompileCommand>,
72+
P: AsRef<Path>,
73+
{
74+
let db = JsonValue::Array(
75+
commands
76+
.into_iter()
77+
.map(|command| command.into())
78+
.collect::<Vec<JsonValue>>(),
79+
);
80+
81+
std::fs::write(path, db.stringify().unwrap()).unwrap();
82+
}
83+
84+
impl<'a> std::convert::From<&CompileCommand> for JsonValue {
85+
fn from(compile_command: &CompileCommand) -> Self {
86+
JsonValue::Object(HashMap::from([
87+
(
88+
String::from("directory"),
89+
JsonValue::String(compile_command.directory.to_string_lossy().to_string()),
90+
),
91+
(
92+
String::from("file"),
93+
JsonValue::String(compile_command.file.to_string_lossy().to_string()),
94+
),
95+
(
96+
String::from("output"),
97+
JsonValue::String(compile_command.output.to_string_lossy().to_string()),
98+
),
99+
(
100+
String::from("arguments"),
101+
JsonValue::Array(
102+
compile_command
103+
.arguments
104+
.iter()
105+
.map(|arg| JsonValue::String(arg.to_string()))
106+
.collect::<Vec<_>>(),
107+
),
108+
),
109+
]))
110+
}
111+
}

src/lib.rs

+59-19
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,6 +1132,26 @@ 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")]
11241156
fn compile_objects<'me>(&'me self, objs: &[Object]) -> Result<(), Error> {
11251157
use std::sync::atomic::{AtomicBool, Ordering::SeqCst};
@@ -1272,14 +1304,15 @@ impl Build {
12721304
}
12731305

12741306
#[cfg(not(feature = "parallel"))]
1275-
fn compile_objects(&self, objs: &[Object]) -> Result<(), Error> {
1307+
fn compile_objects(&self, objs: &[Object]) -> Result<Vec<CompileCommand>, Error> {
1308+
let mut entries = Vec::new();
12761309
for obj in objs {
1277-
self.compile_object(obj)?;
1310+
entries.push(self.compile_object(obj)?);
12781311
}
1279-
Ok(())
1312+
Ok(entries)
12801313
}
12811314

1282-
fn compile_object(&self, obj: &Object) -> Result<(), Error> {
1315+
fn compile_object(&self, obj: &Object) -> Result<CompileCommand, Error> {
12831316
let is_asm = obj.src.extension().and_then(|s| s.to_str()) == Some("asm");
12841317
let target = self.get_target()?;
12851318
let msvc = target.contains("msvc");
@@ -1324,7 +1357,7 @@ impl Build {
13241357
}
13251358

13261359
run(&mut cmd, &name)?;
1327-
Ok(())
1360+
Ok(CompileCommand::new(&cmd, obj.src.clone(), obj.dst.clone()))
13281361
}
13291362

13301363
/// This will return a result instead of panicing; see expand() for the complete description.
@@ -3287,22 +3320,29 @@ fn map_darwin_target_from_rust_to_compiler_architecture(target: &str) -> Option<
32873320
}
32883321
}
32893322

3290-
fn which(tool: &Path) -> Option<PathBuf> {
3323+
pub(crate) fn which<P>(tool: P) -> Option<PathBuf>
3324+
where
3325+
P: AsRef<Path>,
3326+
{
32913327
fn check_exe(exe: &mut PathBuf) -> bool {
32923328
let exe_ext = std::env::consts::EXE_EXTENSION;
32933329
exe.exists() || (!exe_ext.is_empty() && exe.set_extension(exe_ext) && exe.exists())
32943330
}
32953331

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

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-
})
3347+
non_generic_which(tool.as_ref())
33083348
}

0 commit comments

Comments
 (0)