use anyhow::{Context, Result};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::processors::{check_command_output, run_command_capture};
static SHELL_COMMAND_CACHE: Mutex<Option<HashMap<String, Vec<String>>>> = Mutex::new(None);
fn cached_shell_command(
cmd_line: &str,
runner: impl FnOnce(&crate::build_context::BuildContext, &str) -> Result<Vec<String>>,
ctx: &crate::build_context::BuildContext,
) -> Result<Vec<String>> {
let mut guard = SHELL_COMMAND_CACHE.lock();
let cache = guard.get_or_insert_with(HashMap::new);
if let Some(cached) = cache.get(cmd_line) {
return Ok(cached.clone());
}
let result = runner(ctx, cmd_line)?;
cache.insert(cmd_line.to_string(), result.clone());
Ok(result)
}
#[derive(Default)]
struct SourceFlags {
compile_args_before: Vec<String>,
compile_args_after: Vec<String>,
link_args_before: Vec<String>,
link_args_after: Vec<String>,
}
fn extract_comment_value(line: &str) -> Option<&str> {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("//") {
return Some(rest.trim());
}
if let Some(rest) = trimmed.strip_prefix("/*") {
return rest.strip_suffix("*/").map(str::trim);
}
if let Some(rest) = trimmed.strip_prefix('*') {
let rest = rest.trim();
if rest.is_empty() || rest == "/" {
return None;
}
return Some(rest.strip_suffix("*/").map_or(rest, str::trim));
}
None
}
fn should_exclude_for_profile(source: &Path, profile_name: &str) -> bool {
if profile_name.is_empty() {
return false;
}
let Ok(content) = fs::read_to_string(source) else {
return false;
};
for line in content.lines() {
let Some(value_part) = extract_comment_value(line) else {
continue;
};
if let Some(rest) = value_part.strip_prefix("EXCLUDE_PROFILE")
&& let Some(profiles_str) = rest.strip_prefix('=')
&& profiles_str.split_whitespace().any(|p| p == profile_name)
{
return true;
}
}
false
}
fn parse_source_flags(
ctx: &crate::build_context::BuildContext,
source: &Path,
profile_name: &str,
) -> Result<SourceFlags> {
let content = fs::read_to_string(source)
.with_context(|| format!("Failed to read source file: {}", source.display()))?;
let mut flags = SourceFlags::default();
let args_var_names = [
"EXTRA_COMPILE_FLAGS_BEFORE",
"EXTRA_COMPILE_FLAGS_AFTER",
"EXTRA_LINK_FLAGS_BEFORE",
"EXTRA_LINK_FLAGS_AFTER",
];
let cmd_var_names = ["EXTRA_COMPILE_CMD", "EXTRA_LINK_CMD"];
let shell_var_names = ["EXTRA_COMPILE_SHELL", "EXTRA_LINK_SHELL"];
for line in content.lines() {
let Some(value_part) = extract_comment_value(line) else {
continue;
};
for var_name in &args_var_names {
if let Some((rest, applies)) =
match_directive_with_profile(value_part, var_name, profile_name)
&& applies
&& let Some(raw_value) = rest.strip_prefix('=')
{
let expanded = expand_backticks(ctx, raw_value.trim())?;
let args: Vec<String> = expanded.split_whitespace().map(String::from).collect();
match *var_name {
"EXTRA_COMPILE_FLAGS_BEFORE" => flags.compile_args_before.extend(args),
"EXTRA_COMPILE_FLAGS_AFTER" => flags.compile_args_after.extend(args),
"EXTRA_LINK_FLAGS_BEFORE" => flags.link_args_before.extend(args),
"EXTRA_LINK_FLAGS_AFTER" => flags.link_args_after.extend(args),
_ => {}
}
}
}
for var_name in &cmd_var_names {
if let Some((rest, applies)) =
match_directive_with_profile(value_part, var_name, profile_name)
&& applies
&& let Some(raw_value) = rest.strip_prefix('=')
{
let cmd = raw_value.trim();
let args = cached_shell_command(cmd, run_command_for_flags, ctx)?;
match *var_name {
"EXTRA_COMPILE_CMD" => flags.compile_args_after.extend(args),
"EXTRA_LINK_CMD" => flags.link_args_after.extend(args),
_ => {}
}
}
}
for var_name in &shell_var_names {
if let Some((rest, applies)) =
match_directive_with_profile(value_part, var_name, profile_name)
&& applies
&& let Some(raw_value) = rest.strip_prefix('=')
{
let cmd = raw_value.trim();
let args = cached_shell_command(cmd, run_shell_for_flags, ctx)?;
match *var_name {
"EXTRA_COMPILE_SHELL" => flags.compile_args_after.extend(args),
"EXTRA_LINK_SHELL" => flags.link_args_after.extend(args),
_ => {}
}
}
}
}
Ok(flags)
}
fn match_directive_with_profile<'a>(
line: &'a str,
directive: &str,
profile_name: &str,
) -> Option<(&'a str, bool)> {
if let Some(rest) = line.strip_prefix(directive) {
if let Some(rest_after_bracket) = rest.strip_prefix('[') {
if let Some(bracket_end) = rest_after_bracket.find(']') {
let specified_profile = &rest_after_bracket[..bracket_end];
let rest_after_suffix = &rest_after_bracket[bracket_end + 1..];
let applies = specified_profile == profile_name;
return Some((rest_after_suffix, applies));
}
None
} else {
Some((rest, true))
}
} else {
None
}
}
fn run_command_for_flags(
ctx: &crate::build_context::BuildContext,
cmd_line: &str,
) -> Result<Vec<String>> {
let parts: Vec<&str> = cmd_line.split_whitespace().collect();
if parts.is_empty() {
return Ok(Vec::new());
}
let program = parts[0];
let args = &parts[1..];
let mut cmd = Command::new(program);
cmd.args(args);
let _guard = crate::processors::suspend_tool_check();
let output = run_command_capture(ctx, &cmd)?;
check_command_output(&output, cmd_line)?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
Ok(stdout.split_whitespace().map(String::from).collect())
}
fn run_shell_for_flags(
ctx: &crate::build_context::BuildContext,
cmd_line: &str,
) -> Result<Vec<String>> {
if cmd_line.is_empty() {
return Ok(Vec::new());
}
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(cmd_line);
let _guard = crate::processors::suspend_tool_check();
let output = run_command_capture(ctx, &cmd)?;
check_command_output(&output, cmd_line)?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
Ok(stdout.split_whitespace().map(String::from).collect())
}
fn run_backtick_command(
ctx: &crate::build_context::BuildContext,
cmd_str: &str,
) -> Result<Vec<String>> {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(cmd_str);
let _guard = crate::processors::suspend_tool_check();
let output = run_command_capture(ctx, &cmd)?;
check_command_output(&output, cmd_str)?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
Ok(vec![stdout])
}
fn expand_backticks(ctx: &crate::build_context::BuildContext, value: &str) -> Result<String> {
if !value.contains('`') {
return Ok(value.to_string());
}
let mut result = String::new();
let mut rest = value;
while let Some(start) = rest.find('`') {
result.push_str(&rest[..start]);
let after_start = &rest[start + 1..];
let end = after_start
.find('`')
.ok_or_else(|| anyhow::anyhow!("Unmatched backtick in value: {value}"))?;
let cmd_str = &after_start[..end];
let cached = cached_shell_command(cmd_str, run_backtick_command, ctx)?;
let stdout = cached.first().map_or("", std::string::String::as_str);
result.push_str(stdout);
rest = &after_start[end + 1..];
}
result.push_str(rest);
Ok(result)
}
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::config::{StandardConfig, output_config_hash, resolve_extra_inputs};
use crate::file_index::FileIndex;
use crate::graph::{BuildGraph, Product};
use crate::processors::{Processor, format_command, run_command};
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum IncludeScanner {
#[default]
Native,
Compiler,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct CompilerProfile {
pub name: String,
#[serde(default = "crate::config::default_true")]
pub enabled: bool,
#[serde(default = "crate::config::default_cc_compiler")]
pub cc: String,
#[serde(default = "crate::config::default_cxx_compiler")]
pub cxx: String,
#[serde(default)]
pub cflags: Vec<String>,
#[serde(default)]
pub cxxflags: Vec<String>,
#[serde(default)]
pub ldflags: Vec<String>,
#[serde(default = "crate::config::default_output_suffix")]
pub output_suffix: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CcSingleFileConfig {
#[serde(default = "crate::config::default_cc_compiler")]
pub cc: String,
#[serde(default = "crate::config::default_cxx_compiler")]
pub cxx: String,
#[serde(default)]
pub cflags: Vec<String>,
#[serde(default)]
pub cxxflags: Vec<String>,
#[serde(default)]
pub ldflags: Vec<String>,
#[serde(default = "crate::config::default_output_suffix")]
pub output_suffix: String,
#[serde(default)]
pub compilers: Vec<CompilerProfile>,
#[serde(default)]
pub include_paths: Vec<String>,
#[serde(default)]
pub include_scanner: IncludeScanner,
#[serde(flatten)]
pub standard: StandardConfig,
}
impl CcSingleFileConfig {
pub(crate) fn get_compiler_profiles(&self) -> Vec<CompilerProfile> {
if self.compilers.is_empty() {
vec![CompilerProfile {
name: String::new(), enabled: true,
cc: self.cc.clone(),
cxx: self.cxx.clone(),
cflags: self.cflags.clone(),
cxxflags: self.cxxflags.clone(),
ldflags: self.ldflags.clone(),
output_suffix: self.output_suffix.clone(),
}]
} else {
self.compilers
.iter()
.filter(|p| p.enabled)
.cloned()
.collect()
}
}
}
impl Default for CcSingleFileConfig {
fn default() -> Self {
Self {
cc: "gcc".into(),
cxx: "g++".into(),
cflags: Vec::new(),
cxxflags: Vec::new(),
ldflags: Vec::new(),
output_suffix: ".elf".into(),
compilers: Vec::new(),
include_paths: Vec::new(),
include_scanner: IncludeScanner::default(),
standard: StandardConfig::default(),
}
}
}
pub struct CcSingleFileProcessor {
config: CcSingleFileConfig,
profiles: Vec<CompilerProfile>,
output_dir: PathBuf,
}
impl CcSingleFileProcessor {
pub fn new(config: CcSingleFileConfig) -> Self {
let profiles = config.get_compiler_profiles();
let output_dir = PathBuf::from(&config.standard.output_dir);
Self {
config,
profiles,
output_dir,
}
}
fn source_dir(&self) -> PathBuf {
PathBuf::from(
self.config
.standard
.src_dirs()
.first()
.map_or("", std::string::String::as_str),
)
}
fn should_process(&self) -> bool {
let src = self.source_dir();
src.as_os_str().is_empty() || src.exists()
}
fn find_source_files(&self, file_index: &FileIndex) -> Vec<(PathBuf, bool)> {
file_index
.scan(&self.config.standard, true)
.into_iter()
.map(|p| {
let is_cpp = p.extension().and_then(|s| s.to_str()) == Some("cc");
(p, is_cpp)
})
.collect()
}
fn get_executable_path(&self, source: &Path, profile: &CompilerProfile) -> PathBuf {
let stem = source.with_extension("");
let name = format!("{}{}", stem.display(), profile.output_suffix);
if profile.name.is_empty() {
self.output_dir.join(name)
} else {
self.output_dir.join(&profile.name).join(name)
}
}
fn find_profile(&self, name: &str) -> Option<&CompilerProfile> {
self.profiles.iter().find(|p| p.name == name)
}
fn add_compile_flags(
&self,
cmd: &mut Command,
profile: &CompilerProfile,
is_cpp: bool,
source_flags: &SourceFlags,
) {
let flags = if is_cpp {
&profile.cxxflags
} else {
&profile.cflags
};
for inc in &self.config.include_paths {
cmd.arg(format!("-I{inc}"));
}
for arg in &source_flags.compile_args_before {
cmd.arg(arg);
}
for flag in flags {
cmd.arg(flag);
}
for arg in &source_flags.compile_args_after {
cmd.arg(arg);
}
}
fn compile_source(
&self,
ctx: &crate::build_context::BuildContext,
source: &Path,
executable: &Path,
profile: &CompilerProfile,
is_cpp: bool,
) -> Result<()> {
let compiler = if is_cpp { &profile.cxx } else { &profile.cc };
let source_flags = parse_source_flags(ctx, source, &profile.name)?;
crate::processors::ensure_output_dir(executable)?;
let mut cmd = Command::new(compiler);
self.add_compile_flags(&mut cmd, profile, is_cpp, &source_flags);
cmd.arg("-o").arg(executable).arg(source);
for arg in &source_flags.link_args_before {
cmd.arg(arg);
}
for flag in &profile.ldflags {
cmd.arg(flag);
}
for arg in &source_flags.link_args_after {
cmd.arg(arg);
}
if crate::runtime_flags::show_child_processes() {
let profile_tag = if profile.name.is_empty() {
String::new()
} else {
format!(":{}", profile.name)
};
println!(
"[{}{}] {}",
crate::processors::names::CC_SINGLE_FILE,
profile_tag,
format_command(&cmd)
);
}
let output = run_command(ctx, &cmd)?;
check_command_output(&output, format_args!("Compilation of {}", source.display()))
}
fn get_profile_from_product(&self, product: &Product) -> Result<&CompilerProfile> {
if let Some(output) = product.outputs.first()
&& let Ok(relative) = output.strip_prefix(&self.output_dir)
{
if let Some(first) = relative.components().next() {
let first_str = first.as_os_str().to_string_lossy();
if let Some(profile) = self.find_profile(&first_str) {
return Ok(profile);
}
}
}
self.profiles
.first()
.ok_or_else(|| anyhow::anyhow!("no compiler profiles configured"))
}
fn discover_impl(
&self,
graph: &mut BuildGraph,
file_index: &FileIndex,
for_clean: bool,
instance_name: &str,
) -> Result<()> {
if !self.should_process() {
return Ok(());
}
let source_files = self.find_source_files(file_index);
if source_files.is_empty() {
return Ok(());
}
let cfg_hash = if for_clean {
None
} else {
Some(output_config_hash(
&self.config,
&crate::config::checksum_fields_of(instance_name),
))
};
let extra = if for_clean {
Vec::new()
} else {
resolve_extra_inputs(&self.config.standard.dep_inputs)?
};
for profile in &self.profiles {
let variant = if profile.name.is_empty() {
None
} else {
Some(profile.name.as_str())
};
for (source, _is_cpp) in &source_files {
if should_exclude_for_profile(source, &profile.name) {
continue;
}
let executable = self.get_executable_path(source, profile);
let mut inputs = Vec::with_capacity(1 + extra.len());
inputs.push(source.clone());
inputs.extend_from_slice(&extra);
graph.add_product_with_variant(
inputs,
vec![executable],
instance_name,
cfg_hash.clone(),
variant,
)?;
}
}
Ok(())
}
}
impl Processor for CcSingleFileProcessor {
fn scan_config(&self) -> &crate::config::StandardConfig {
&self.config.standard
}
fn config_json(&self) -> Option<String> {
crate::processors::ProcessorBase::config_json(&self.config)
}
fn clean(&self, product: &crate::graph::Product, verbose: bool) -> anyhow::Result<usize> {
crate::processors::ProcessorBase::clean(product, &product.processor, verbose)
}
fn auto_detect(&self, file_index: &FileIndex) -> bool {
self.should_process() && !self.find_source_files(file_index).is_empty()
}
fn required_tools(&self) -> Vec<String> {
let mut tools: Vec<String> = self
.profiles
.iter()
.flat_map(|p| vec![p.cc.clone(), p.cxx.clone()])
.collect();
tools.sort();
tools.dedup();
tools
}
fn discover(
&self,
graph: &mut BuildGraph,
file_index: &FileIndex,
instance_name: &str,
) -> Result<()> {
self.discover_impl(graph, file_index, false, instance_name)
}
fn discover_for_clean(
&self,
graph: &mut BuildGraph,
file_index: &FileIndex,
instance_name: &str,
) -> Result<()> {
self.discover_impl(graph, file_index, true, instance_name)
}
fn execute(&self, ctx: &crate::build_context::BuildContext, product: &Product) -> Result<()> {
let source = product.primary_input();
let executable = product.primary_output();
let is_cpp = source.extension().and_then(|s| s.to_str()) == Some("cc");
let profile = self.get_profile_from_product(product)?;
self.compile_source(ctx, source, executable, profile, is_cpp)
}
}
fn plugin_create(toml: &toml::Value) -> anyhow::Result<Box<dyn crate::processors::Processor>> {
crate::registries::deserialize_and_create(toml, |cfg| Box::new(CcSingleFileProcessor::new(cfg)))
}
inventory::submit! {
crate::registries::ProcessorPlugin {
version: 1,
name: "cc_single_file",
processor_type: crate::processors::ProcessorType::Generator,
create: plugin_create,
fields: &[
crate::config::FieldSpec { name: "cc", ty: crate::config::FieldType::String,
affects_output: true, required: false,
doc: "C compiler executable" },
crate::config::FieldSpec { name: "cxx", ty: crate::config::FieldType::String,
affects_output: true, required: false,
doc: "C++ compiler executable" },
crate::config::FieldSpec { name: "cflags", ty: crate::config::FieldType::StringArray,
affects_output: true, required: false,
doc: "Flags passed when compiling C sources" },
crate::config::FieldSpec { name: "cxxflags", ty: crate::config::FieldType::StringArray,
affects_output: true, required: false,
doc: "Flags passed when compiling C++ sources" },
crate::config::FieldSpec { name: "ldflags", ty: crate::config::FieldType::StringArray,
affects_output: true, required: false,
doc: "Flags passed to the linker" },
crate::config::FieldSpec { name: "output_suffix", ty: crate::config::FieldType::String,
affects_output: true, required: false,
doc: "Suffix appended to output binary names" },
crate::config::FieldSpec { name: "compilers", ty: crate::config::FieldType::TableArray,
affects_output: true, required: false,
doc: "Named compiler profiles (overrides cc/cxx when set)" },
crate::config::FieldSpec { name: "include_paths", ty: crate::config::FieldType::StringArray,
affects_output: true, required: false,
doc: "Additional header search directories" },
crate::config::FieldSpec { name: "include_scanner", ty: crate::config::FieldType::String,
affects_output: true, required: false,
doc: "Header dependency scanner: native (fast) or compiler (accurate)" },
],
omit_standard_fields: &["command", "formats", "args"],
scan_defaults: Some(crate::config::ScanDefaultsData { src_dirs: &[], src_extensions: &[".c", ".cc"], src_exclude_dirs: &[] }),
defaults: Some(crate::config::ProcessorDefaults { output_dir: "out/cc_single_file", ..crate::config::ProcessorDefaults::EMPTY }),
defconfig_json: crate::registries::default_config_json::<CcSingleFileConfig>,
keywords: &["c", "cpp", "compiler", "gcc", "clang", "binary", "executable"],
description: "Compile C/C++ source files into executables (single-file)",
is_native: false,
can_fix: false,
supports_batch: false,
max_jobs_cap: None,
}
}