Skip to content

Commit c3c46ce

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 rust-lang#497
1 parent 53fb72c commit c3c46ce

File tree

3 files changed

+199
-23
lines changed

3 files changed

+199
-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

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

src/lib.rs

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

59+
#[cfg(feature = "compile_commands")]
60+
pub use crate::json_compilation_database::{store_json_compilation_database, CompileCommand};
61+
#[cfg(not(feature = "compile_commands"))]
62+
use crate::json_compilation_database::CompileCommand;
5963
use std::collections::HashMap;
6064
use std::env;
6165
use std::ffi::{OsStr, OsString};
@@ -81,6 +85,7 @@ mod setup_config;
8185
#[cfg(windows)]
8286
mod vs_instances;
8387

88+
mod json_compilation_database;
8489
pub mod windows_registry;
8590

8691
/// A builder for compilation of a native library.
@@ -943,8 +948,17 @@ impl Build {
943948

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

9911005
objects.push(Object::new(file.to_path_buf(), obj));
9921006
}
993-
self.compile_objects(&objects)?;
1007+
let entries = self.compile_objects(&objects)?;
9941008
self.assemble(lib_name, &dst.join(gnu_lib_name), &objects)?;
9951009

9961010
if self.get_target()?.contains("msvc") {
@@ -1074,7 +1088,7 @@ impl Build {
10741088
}
10751089
}
10761090

1077-
Ok(())
1091+
Ok(entries)
10781092
}
10791093

10801094
/// Run the compiler, generating the file `output`
@@ -1120,8 +1134,30 @@ impl Build {
11201134
}
11211135
}
11221136

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

@@ -1191,9 +1227,11 @@ impl Build {
11911227
threads.push(JoinOnDrop(Some(thread)));
11921228
}
11931229

1230+
let mut entries = Vec::new();
1231+
11941232
for mut thread in threads {
11951233
if let Some(thread) = thread.0.take() {
1196-
thread.join().expect("thread should not panic")?;
1234+
entries.push(thread.join().expect("thread should not panic")?);
11971235
}
11981236
}
11991237

@@ -1203,7 +1241,7 @@ impl Build {
12031241
server.acquire_raw()?;
12041242
}
12051243

1206-
return Ok(());
1244+
return Ok(entries);
12071245

12081246
/// Shared state from the parent thread to the child thread. This
12091247
/// package of pointers is temporarily transmuted to a `'static`
@@ -1260,7 +1298,7 @@ impl Build {
12601298
return client;
12611299
}
12621300

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

12651303
impl Drop for JoinOnDrop {
12661304
fn drop(&mut self) {
@@ -1272,14 +1310,15 @@ impl Build {
12721310
}
12731311

12741312
#[cfg(not(feature = "parallel"))]
1275-
fn compile_objects(&self, objs: &[Object]) -> Result<(), Error> {
1313+
fn compile_objects(&self, objs: &[Object]) -> Result<Vec<CompileCommand>, Error> {
1314+
let mut entries = Vec::new();
12761315
for obj in objs {
1277-
self.compile_object(obj)?;
1316+
entries.push(self.compile_object(obj)?);
12781317
}
1279-
Ok(())
1318+
Ok(entries)
12801319
}
12811320

1282-
fn compile_object(&self, obj: &Object) -> Result<(), Error> {
1321+
fn compile_object(&self, obj: &Object) -> Result<CompileCommand, Error> {
12831322
let is_asm = obj.src.extension().and_then(|s| s.to_str()) == Some("asm");
12841323
let target = self.get_target()?;
12851324
let msvc = target.contains("msvc");
@@ -1324,7 +1363,7 @@ impl Build {
13241363
}
13251364

13261365
run(&mut cmd, &name)?;
1327-
Ok(())
1366+
Ok(CompileCommand::new(&cmd, obj.src.clone(), obj.dst.clone()))
13281367
}
13291368

13301369
/// This will return a result instead of panicing; see expand() for the complete description.
@@ -3335,22 +3374,29 @@ fn map_darwin_target_from_rust_to_compiler_architecture(target: &str) -> Option<
33353374
}
33363375
}
33373376

3338-
fn which(tool: &Path) -> Option<PathBuf> {
3377+
pub(crate) fn which<P>(tool: P) -> Option<PathBuf>
3378+
where
3379+
P: AsRef<Path>,
3380+
{
33393381
fn check_exe(exe: &mut PathBuf) -> bool {
33403382
let exe_ext = std::env::consts::EXE_EXTENSION;
33413383
exe.exists() || (!exe_ext.is_empty() && exe.set_extension(exe_ext) && exe.exists())
33423384
}
33433385

3344-
// If |tool| is not just one "word," assume it's an actual path...
3345-
if tool.components().count() > 1 {
3346-
let mut exe = PathBuf::from(tool);
3347-
return if check_exe(&mut exe) { Some(exe) } else { None };
3386+
fn non_generic_which(tool: &Path) -> Option<PathBuf> {
3387+
// If |tool| is not just one "word," assume it's an actual path...
3388+
if tool.components().count() > 1 {
3389+
let mut exe = PathBuf::from(tool);
3390+
return if check_exe(&mut exe) { Some(exe) } else { None };
3391+
}
3392+
3393+
// Loop through PATH entries searching for the |tool|.
3394+
let path_entries = env::var_os("PATH")?;
3395+
env::split_paths(&path_entries).find_map(|path_entry| {
3396+
let mut exe = path_entry.join(tool);
3397+
return if check_exe(&mut exe) { Some(exe) } else { None };
3398+
})
33483399
}
33493400

3350-
// Loop through PATH entries searching for the |tool|.
3351-
let path_entries = env::var_os("PATH")?;
3352-
env::split_paths(&path_entries).find_map(|path_entry| {
3353-
let mut exe = path_entry.join(tool);
3354-
return if check_exe(&mut exe) { Some(exe) } else { None };
3355-
})
3401+
non_generic_which(tool.as_ref())
33563402
}

0 commit comments

Comments
 (0)