mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 10:58:29 +00:00
399 lines
12 KiB
Rust
399 lines
12 KiB
Rust
// DCR — Cargo-like C/C++ project manager.
|
|
//
|
|
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
use crate::core::build::engine::ToolchainExecs;
|
|
use crate::core::build_config::Config;
|
|
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
|
|
use glob::glob;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct BuildStep {
|
|
name: String,
|
|
input: String,
|
|
output: String,
|
|
cmd: String,
|
|
}
|
|
|
|
pub(crate) struct StepVars<'a> {
|
|
pub(crate) profile: &'a str,
|
|
pub(crate) version: &'a str,
|
|
pub(crate) version_major: &'a str,
|
|
pub(crate) version_minor: &'a str,
|
|
pub(crate) version_patch: &'a str,
|
|
pub(crate) version_suffix: &'a str,
|
|
pub(crate) version_suffix_dash: &'a str,
|
|
}
|
|
|
|
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
|
|
let arr = value
|
|
.as_array()
|
|
.ok_or_else(|| format!("{key} must be an array"))?;
|
|
let mut out = Vec::new();
|
|
for item in arr {
|
|
let tbl = item
|
|
.as_table()
|
|
.ok_or_else(|| format!("{key} entries must be tables"))?;
|
|
let name = tbl
|
|
.get("name")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.trim()
|
|
.to_string();
|
|
let input = tbl
|
|
.get("in")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.trim()
|
|
.to_string();
|
|
let output = tbl
|
|
.get("out")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.trim()
|
|
.to_string();
|
|
let cmd = tbl
|
|
.get("cmd")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.trim()
|
|
.to_string();
|
|
if name.is_empty() || input.is_empty() || output.is_empty() || cmd.is_empty() {
|
|
return Err(format!("{key} entries must include name, in, out, cmd"));
|
|
}
|
|
out.push(BuildStep {
|
|
name,
|
|
input,
|
|
output,
|
|
cmd,
|
|
});
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
pub(crate) fn get_build_steps_with_profile(
|
|
config: &Config,
|
|
field: &str,
|
|
profile: &str,
|
|
) -> Result<Vec<BuildStep>, String> {
|
|
if let Some(table) = profile_table(config, profile)
|
|
&& let Some(value) = table.get(field)
|
|
{
|
|
return get_build_steps_from_value(value, &format!("build.{profile}.{field}"));
|
|
}
|
|
get_build_steps(config, &format!("build.{field}"))
|
|
}
|
|
|
|
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
|
|
let value = match config.get(key) {
|
|
Some(v) => v,
|
|
None => return Ok(Vec::new()),
|
|
};
|
|
get_build_steps_from_value(value, key)
|
|
}
|
|
|
|
use crate::core::build::report::{BuildEvent, BuildReporter};
|
|
use std::sync::{Arc, atomic::AtomicBool};
|
|
|
|
pub(crate) fn run_build_steps(
|
|
steps: &[BuildStep],
|
|
tools: &ToolchainExecs,
|
|
step_flags: &str,
|
|
vars: &StepVars,
|
|
cancel: &Arc<AtomicBool>,
|
|
rep: &mut dyn BuildReporter,
|
|
) -> Result<(), String> {
|
|
for step in steps {
|
|
if cancel.load(std::sync::atomic::Ordering::SeqCst) {
|
|
return Err("Build interrupted".to_string());
|
|
}
|
|
run_build_step(step, tools, step_flags, vars, cancel, rep)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run_build_step(
|
|
step: &BuildStep,
|
|
tools: &ToolchainExecs,
|
|
step_flags: &str,
|
|
vars: &StepVars,
|
|
cancel: &Arc<AtomicBool>,
|
|
rep: &mut dyn BuildReporter,
|
|
) -> Result<(), String> {
|
|
let input_pattern = expand_step_value(&step.input, "", vars);
|
|
let inputs = expand_glob(&input_pattern)?;
|
|
if inputs.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let needs_stem = step.output.contains("{stem}");
|
|
if inputs.len() > 1 && !needs_stem {
|
|
return Err(format!(
|
|
"build.steps '{}' output must include {{stem}} for multiple inputs",
|
|
step.name
|
|
));
|
|
}
|
|
for input in inputs {
|
|
if !input.is_file() {
|
|
continue;
|
|
}
|
|
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
|
|
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
|
|
if !should_run_step(&input, &out_path) {
|
|
continue;
|
|
}
|
|
if let Some(parent) = out_path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.map_err(|err| format!("Failed to create step output dir: {err}"))?;
|
|
}
|
|
let cmd = substitute_step_cmd(&step.cmd, &input, &out_path, tools, step_flags, stem, vars);
|
|
let status = run_shell_command(&cmd, cancel, rep)
|
|
.map_err(|err| format!("Failed to run step '{}': {err}", step.name))?;
|
|
if !status.success() {
|
|
return Err(format!("Step '{}' failed", step.name));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
|
for pattern in patterns {
|
|
for path in expand_glob(pattern)? {
|
|
if path.is_file() {
|
|
let _ = fs::remove_file(&path);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
|
let mut out = Vec::new();
|
|
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
|
|
for entry in entries {
|
|
let path = entry.map_err(|err| format!("glob error: {err}"))?;
|
|
out.push(path);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
|
|
for pattern in patterns {
|
|
let expanded = expand_step_value(pattern, "", vars);
|
|
let matches = expand_glob(&expanded)?;
|
|
if matches.is_empty() {
|
|
return Err(format!("Expected artifact not found: {expanded}"));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn should_run_step(input: &Path, output: &Path) -> bool {
|
|
let in_time = fs::metadata(input).and_then(|m| m.modified());
|
|
let out_time = fs::metadata(output).and_then(|m| m.modified());
|
|
match (in_time, out_time) {
|
|
(Ok(i), Ok(o)) => i > o,
|
|
(Ok(_), Err(_)) => true,
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
fn substitute_step_cmd(
|
|
template: &str,
|
|
input: &Path,
|
|
output: &Path,
|
|
tools: &ToolchainExecs,
|
|
step_flags: &str,
|
|
stem: &str,
|
|
vars: &StepVars,
|
|
) -> String {
|
|
let info = make_version_info(vars);
|
|
let s = substitute_vars(template, &info, vars.profile, "");
|
|
s.replace("{in}", &input.to_string_lossy())
|
|
.replace("{out}", &output.to_string_lossy())
|
|
.replace("{uic}", &tools.uic)
|
|
.replace("{moc}", &tools.moc)
|
|
.replace("{rcc}", &tools.rcc)
|
|
.replace("{cflags}", step_flags)
|
|
.replace("{stem}", stem)
|
|
}
|
|
|
|
pub(crate) fn build_step_flags(
|
|
cflags: &[String],
|
|
include_dirs: &[String],
|
|
compiler: &str,
|
|
) -> String {
|
|
let mut out = Vec::new();
|
|
let msvc_style = is_msvc_compiler(compiler) || cflags.iter().any(|f| f.starts_with('/'));
|
|
for flag in cflags {
|
|
if flag.starts_with("-I") || flag.starts_with("-D") {
|
|
out.push(flag.clone());
|
|
}
|
|
if flag.starts_with("/I") || flag.starts_with("/D") {
|
|
out.push(flag.clone());
|
|
}
|
|
if msvc_style && flag.starts_with("-D") {
|
|
out.push(format!("/D{}", flag.trim_start_matches("-D")));
|
|
}
|
|
}
|
|
for dir in include_dirs {
|
|
out.push(format!("-I{dir}"));
|
|
if msvc_style {
|
|
out.push(format!("/I{dir}"));
|
|
}
|
|
}
|
|
out.sort();
|
|
out.dedup();
|
|
out.into_iter()
|
|
.map(quote_step_arg)
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
}
|
|
|
|
fn quote_step_arg(arg: String) -> String {
|
|
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
|
|
return arg;
|
|
}
|
|
let escaped = arg.replace('"', "\\\"");
|
|
format!("\"{escaped}\"")
|
|
}
|
|
|
|
fn is_msvc_compiler(compiler: &str) -> bool {
|
|
let lower = compiler.to_lowercase();
|
|
lower.contains("cl.exe")
|
|
|| lower == "cl"
|
|
|| lower.contains("clang-cl")
|
|
|| lower.contains("msvc")
|
|
}
|
|
|
|
fn run_shell_command(
|
|
cmd: &str,
|
|
cancel: &Arc<AtomicBool>,
|
|
rep: &mut dyn BuildReporter,
|
|
) -> Result<std::process::ExitStatus, std::io::Error> {
|
|
let mut child = if cfg!(target_os = "windows") {
|
|
std::process::Command::new("cmd")
|
|
.arg("/C")
|
|
.arg(cmd)
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.spawn()
|
|
} else {
|
|
std::process::Command::new("sh")
|
|
.arg("-c")
|
|
.arg(cmd)
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.spawn()
|
|
}?;
|
|
|
|
let stdout = child.stdout.take().unwrap();
|
|
let stderr = child.stderr.take().unwrap();
|
|
|
|
use std::io::{BufRead, BufReader};
|
|
use std::sync::mpsc;
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
let cancel_clone = cancel.clone();
|
|
let tx_stdout = tx.clone();
|
|
let stdout_thread = std::thread::spawn(move || {
|
|
let reader = BufReader::new(stdout);
|
|
for line in reader.lines() {
|
|
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
|
break;
|
|
}
|
|
if let Ok(line) = line {
|
|
let _ = tx_stdout.send(("stdout", line));
|
|
}
|
|
}
|
|
});
|
|
|
|
let cancel_clone = cancel.clone();
|
|
let tx_stderr = tx.clone();
|
|
let stderr_thread = std::thread::spawn(move || {
|
|
let reader = BufReader::new(stderr);
|
|
for line in reader.lines() {
|
|
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
|
break;
|
|
}
|
|
if let Ok(line) = line {
|
|
let _ = tx_stderr.send(("stderr", line));
|
|
}
|
|
}
|
|
});
|
|
|
|
let status = child.wait()?;
|
|
let _ = stdout_thread.join();
|
|
let _ = stderr_thread.join();
|
|
|
|
drop(tx);
|
|
while let Ok((stream, text)) = rx.recv() {
|
|
rep.on_event(BuildEvent::CompilerOutput {
|
|
stream,
|
|
text: &text,
|
|
});
|
|
}
|
|
|
|
Ok(status)
|
|
}
|
|
|
|
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
|
|
for step in steps {
|
|
let input_pattern = expand_step_value(&step.input, "", vars);
|
|
let inputs = expand_glob(&input_pattern)?;
|
|
if inputs.is_empty() {
|
|
continue;
|
|
}
|
|
let needs_stem = step.output.contains("{stem}");
|
|
if inputs.len() > 1 && !needs_stem {
|
|
return Err(format!(
|
|
"build.steps '{}' output must include {{stem}} for multiple inputs",
|
|
step.name
|
|
));
|
|
}
|
|
for input in inputs {
|
|
if !input.is_file() {
|
|
continue;
|
|
}
|
|
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
|
|
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
|
|
if should_run_step(&input, &out_path) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
|
|
fn make_version_info(vars: &StepVars) -> VersionInfo {
|
|
VersionInfo {
|
|
full: vars.version.to_string(),
|
|
major: vars.version_major.to_string(),
|
|
minor: vars.version_minor.to_string(),
|
|
patch: vars.version_patch.to_string(),
|
|
suffix: vars.version_suffix.to_string(),
|
|
suffix_dash: vars.version_suffix_dash.to_string(),
|
|
}
|
|
}
|
|
|
|
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
|
|
let info = make_version_info(vars);
|
|
let s = substitute_vars(template, &info, vars.profile, "");
|
|
s.replace("{stem}", stem)
|
|
}
|