refactor: modularize build core engine, update workspaces, cache, and test suite

This commit is contained in:
2026-07-18 20:02:12 +08:00
parent 9326e23a53
commit 9eaa4635d5
49 changed files with 3385 additions and 2312 deletions

View File

@@ -0,0 +1,126 @@
// 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::builder::BuildContext;
use crate::core::build::common;
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::process::Command;
use std::time::Instant;
pub(crate) fn archive_static(
ctx: &BuildContext,
objects: &[String],
start_time: Instant,
) -> Result<f64, String> {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(objects, &lib_path) {
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in objects {
cmd.arg(obj);
}
run(ctx, cmd, start_time)
}
pub(crate) fn link_binary(
ctx: &BuildContext,
objects: &[String],
default_linker: &str,
start_time: Instant,
) -> Result<f64, String> {
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
cmd.arg("-nostdlib");
cmd.arg("-static");
}
for obj in objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
cmd.arg("-o").arg(out_path);
run(ctx, cmd, start_time)
}
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
if output.status.success() {
Ok(common::elapsed_secs(start_time))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
}
}

View File

@@ -0,0 +1,209 @@
// 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::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::utils::build::is_bare_metal_target;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let qt_include_path = if ctx.qt {
crate::core::build::language::cxx::qt::process_qt(ctx, &obj_dir)?
} else {
None
};
let mut all_source_roots = ctx.source_roots.to_vec();
if let Some(qt_path) = &qt_include_path {
all_source_roots.push(qt_path.clone());
}
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
&all_source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?;
if ctx.kind == "staticlib" {
return artifact::archive_static(ctx, &objects, start_time);
}
artifact::link_binary(ctx, &objects, compiler, start_time)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::build::common::source_extensions(language)
}
fn default_flags(profile: &str) -> &'static [&'static str] {
crate::utils::build::default_profile_flags(profile)
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
qt_include_dir: Option<PathBuf>,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(
sources.len(),
|i| {
build_object(
compiler,
&sources[i],
&objects[i],
ctx,
qt_include_dir.as_deref(),
)
},
ctx.codegen_units,
)?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
qt_include_dir: Option<&Path>,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c");
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
cmd.arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if (ctx.freestanding || is_bare_metal_target(ctx.target))
&& ctx.language.to_lowercase() != "asm"
{
cmd.arg("-ffreestanding");
}
if ctx.panic_abort && ctx.language.to_lowercase() != "asm" {
if ctx.language.contains("++")
|| ctx.language.contains("cpp")
|| ctx.language.contains("cxx")
{
cmd.arg("-fno-exceptions");
}
cmd.arg("-fno-unwind-tables");
cmd.arg("-fno-asynchronous-unwind-tables");
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp && !ctx.cxx_standard.is_empty() {
cmd.arg(format!("-std={}", ctx.cxx_standard));
} else if !is_cpp {
cmd.arg(format!("-std={}", ctx.standard));
}
}
let use_dcr_defaults =
ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target));
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
if let Some(path) = qt_include_dir {
cmd.arg(format!("-I{}", path.display()));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
crate::core::build::common::asm_lang_flag(source)
}

View File

@@ -0,0 +1,234 @@
// 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/>.
pub mod artifact;
pub mod cc_common;
pub mod msvc;
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
use crate::core::build::language::llvm_ir;
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub cxx_standard: &'a str,
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub moc: Option<&'a str>,
pub uic: Option<&'a str>,
pub rcc: Option<&'a str>,
pub package_type: Option<&'a str>,
pub freestanding: bool,
pub panic_abort: bool,
pub codegen_units: usize,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
pub qt: bool,
}
pub trait Builder {
#[allow(dead_code)]
fn id(&self) -> &'static str;
fn build(&self, ctx: &BuildContext) -> Result<f64, String>;
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
}
struct Cc;
struct Msvc;
struct Nasm;
struct Gas;
struct Masm;
struct Fasm;
struct Llc;
impl Builder for Cc {
fn id(&self) -> &'static str {
"cc"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
cc_common::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
cc_common::collect_sources(ctx)
}
}
impl Builder for Msvc {
fn id(&self) -> &'static str {
"msvc"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
msvc::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
msvc::collect_sources(ctx)
}
}
impl Builder for Nasm {
fn id(&self) -> &'static str {
"nasm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
nasm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
nasm::collect_sources(ctx)
}
}
impl Builder for Gas {
fn id(&self) -> &'static str {
"gas"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
gas::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
gas::collect_sources(ctx)
}
}
impl Builder for Masm {
fn id(&self) -> &'static str {
"masm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
masm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
masm::collect_sources(ctx)
}
}
impl Builder for Fasm {
fn id(&self) -> &'static str {
"fasm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
fasm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
fasm::collect_sources(ctx)
}
}
impl Builder for Llc {
fn id(&self) -> &'static str {
"llvm_ir"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
llvm_ir::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
llvm_ir::collect_sources(ctx)
}
}
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
let c = compiler.to_lowercase();
if c.contains("clang-cl") {
return &Msvc;
}
if c == "as" || c.contains("gas") {
return &Gas;
}
if c.contains("llc") {
return &Llc;
}
if c.contains("fasm") {
return &Fasm;
}
if c.contains("nasm") {
return &Nasm;
}
if c == "ml" || c == "ml64" || c.contains("masm") {
return &Masm;
}
if c == "cl" || c.contains("msvc") {
return &Msvc;
}
&Cc
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if !check_compiler_exists(ctx.compiler) {
return Err(format!(
"Compiler not found: {}. Make sure it is installed and available in PATH.",
ctx.compiler
));
}
builder_for(ctx.compiler).build(ctx)
}
fn check_compiler_exists(compiler: &str) -> bool {
let name = if compiler.is_empty() {
"cc"
} else {
match compiler.to_lowercase().as_str() {
"gas" | "gnu-as" => "as",
"nasm" => "nasm",
"fasm" | "fasm64" => "fasm",
"masm" | "ml" | "ml64" => "ml",
_ => compiler,
}
};
std::process::Command::new(name)
.arg("--version")
.output()
.is_ok()
}
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
builder_for(ctx.compiler).collect_sources(ctx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_resolves_by_compiler_name() {
assert_eq!(builder_for("gcc").id(), "cc");
assert_eq!(builder_for("clang").id(), "cc");
assert_eq!(builder_for("clang++").id(), "cc");
assert_eq!(builder_for("").id(), "cc");
assert_eq!(builder_for("nasm").id(), "nasm");
assert_eq!(builder_for("as").id(), "gas");
assert_eq!(builder_for("llc-18").id(), "llvm_ir");
assert_eq!(builder_for("fasm").id(), "fasm");
assert_eq!(builder_for("ml64").id(), "masm");
assert_eq!(builder_for("cl").id(), "msvc");
assert_eq!(builder_for("clang-cl").id(), "msvc");
}
}

View File

@@ -0,0 +1,343 @@
// 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::builder::BuildContext;
use crate::core::build::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cl"
} else {
ctx.compiler
};
let lang = ctx.language.to_lowercase();
if lang.contains("asm") {
return Err("MSVC backend does not support build.language with asm".to_string());
}
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
if ctx.kind == "sharedlib" {
cmd.arg("/LD");
}
match ctx.language.to_lowercase().as_str() {
"c" => {
cmd.arg("/TC");
}
"c++" | "cpp" | "cxx" => {
cmd.arg("/TP");
}
_ => {
return Err("Unsupported language".to_string());
}
}
if !ctx.standard.is_empty() {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("/LIBPATH:{dir}"));
}
for lib in ctx.libs {
if lib.to_lowercase().ends_with(".lib") {
cmd.arg(lib);
} else {
cmd.arg(format!("{lib}.lib"));
}
}
if !ctx.ldflags.is_empty() {
cmd.arg("/link");
for flag in ctx.ldflags {
cmd.arg(flag);
}
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = common::elapsed_secs(start_time);
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::build::common::source_extensions(language)
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
let lang = language.to_lowercase();
let std = standard.to_lowercase();
if lang == "c" {
return match std.as_str() {
"c11" => Ok("/std:c11".to_string()),
"c17" => Ok("/std:c17".to_string()),
_ => Err("Unsupported C standard for MSVC".to_string()),
};
}
if lang == "c++" || lang == "cpp" || lang == "cxx" {
return match std.as_str() {
"c++11" => Ok("/std:c++11".to_string()),
"c++14" => Ok("/std:c++14".to_string()),
"c++17" => Ok("/std:c++17".to_string()),
"c++20" => Ok("/std:c++20".to_string()),
"c++23" => Ok("/std:c++latest".to_string()),
_ => Err("Unsupported C++ standard for MSVC".to_string()),
};
}
Err("Unsupported language".to_string())
}
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
let raw = platform?.trim();
if raw.is_empty() {
return None;
}
let p = raw.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
return Some("/arch:IA32");
}
match p.as_str() {
"sse2" => Some("/arch:SSE2"),
"avx" => Some("/arch:AVX"),
"avx2" => Some("/arch:AVX2"),
_ => None,
}
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["/O2", "/DNDEBUG"],
"debug" => &["/Od", "/Zi", "/W4", "/DDCR_DEBUG", "/Oy-"],
_ => &[],
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(compiler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp {
cmd.arg("/TP");
} else {
cmd.arg("/TC");
}
let std_val = if is_cpp && !ctx.cxx_standard.is_empty() {
ctx.cxx_standard
} else if !is_cpp && !ctx.standard.is_empty() {
ctx.standard
} else {
""
};
if !std_val.is_empty() {
let std_flag = msvc_standard_flag(if is_cpp { "c++" } else { "c" }, std_val)?;
cmd.arg(std_flag);
}
if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag);
}
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("/I{dir}"));
}
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
cmd.arg("/showIncludes");
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut headers = Vec::new();
let mut clean_stdout = String::new();
for line in stdout.lines() {
if let Some(stripped) = line.strip_prefix("Note: including file:") {
headers.push(stripped.trim().to_string());
} else if let Some(stripped) = line.strip_prefix("Примечание: включение файла:")
{
headers.push(stripped.trim().to_string());
} else {
clean_stdout.push_str(line);
clean_stdout.push('\n');
}
}
let _lock = common::get_output_lock().lock().unwrap();
if !output.status.success() {
eprint!("{}", clean_stdout);
eprint!("{}", stderr);
return Err("Build failed".to_string());
}
let trimmed_out = clean_stdout.trim();
let trimmed_err = stderr.trim();
let src_filename = Path::new(source)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("");
if !trimmed_out.is_empty() && trimmed_out != src_filename {
print!("{}", clean_stdout);
}
if !trimmed_err.is_empty() {
eprintln!("{}", trimmed_err);
}
let d_path = Path::new(obj_path).with_extension("d");
let mut d_content = format!("{}: \\\n", obj_path.replace('\\', "/"));
for h in headers {
let escaped = h.replace('\\', "/").replace(" ", "\\ ");
d_content.push_str(&format!(" {} \\\n", escaped));
}
fs::write(&d_path, d_content).map_err(|err| format!("d file error: {err}"))?;
Ok(())
}