mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 08:58:35 +00:00
1266 lines
40 KiB
Rust
1266 lines
40 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::builder::BuildContext;
|
|
use crate::core::build::builder::collect_sources;
|
|
use crate::core::build::common;
|
|
use crate::core::build_config::Config;
|
|
use crate::core::workspace::parse_workspace;
|
|
use crate::utils::build::{
|
|
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
|
|
get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform,
|
|
normalize_target, resolve_compiler, resolve_pkg_config_flags_lossy,
|
|
};
|
|
use crate::utils::fs::find_project_root;
|
|
use crate::utils::log::error;
|
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
|
|
/// Used by `gen` commands where the project may not have been built yet.
|
|
struct GenDeps {
|
|
include_dirs: Vec<String>,
|
|
lib_dirs: Vec<String>,
|
|
libs: Vec<String>,
|
|
}
|
|
|
|
fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps {
|
|
let deps_val = match config.get("dependencies") {
|
|
Some(v) => v,
|
|
None => {
|
|
return GenDeps {
|
|
include_dirs: vec![],
|
|
lib_dirs: vec![],
|
|
libs: vec![],
|
|
};
|
|
}
|
|
};
|
|
let deps_table = match deps_val.as_table() {
|
|
Some(t) => t,
|
|
None => {
|
|
return GenDeps {
|
|
include_dirs: vec![],
|
|
lib_dirs: vec![],
|
|
libs: vec![],
|
|
};
|
|
}
|
|
};
|
|
|
|
let mut include_dirs = Vec::new();
|
|
let mut lib_dirs = Vec::new();
|
|
let mut libs = Vec::new();
|
|
|
|
for (name, value) in deps_table {
|
|
let tbl = match value.as_table() {
|
|
Some(t) => t,
|
|
None => continue,
|
|
};
|
|
// skip system deps
|
|
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
|
|
continue;
|
|
}
|
|
let path_raw = match tbl.get("path").and_then(|v| v.as_str()) {
|
|
Some(p) => p.replace("{profile}", profile),
|
|
None => continue,
|
|
};
|
|
let dep_path = {
|
|
let p = Path::new(&path_raw);
|
|
if p.is_absolute() {
|
|
p.to_path_buf()
|
|
} else {
|
|
project_root.join(p)
|
|
}
|
|
};
|
|
|
|
// include dirs — use explicit list or fall back to <dep>/include if it exists
|
|
let include_raws: Option<Vec<String>> =
|
|
tbl.get("include").and_then(|v| v.as_array()).map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.replace("{profile}", profile))
|
|
.collect()
|
|
});
|
|
|
|
if let Some(raws) = include_raws {
|
|
for r in raws {
|
|
let p = Path::new(&r);
|
|
let full = if p.is_absolute() {
|
|
p.to_path_buf()
|
|
} else {
|
|
dep_path.join(p)
|
|
};
|
|
include_dirs.push(full.to_string_lossy().to_string());
|
|
}
|
|
} else {
|
|
let candidate = dep_path.join("include");
|
|
if candidate.exists() {
|
|
include_dirs.push(candidate.to_string_lossy().to_string());
|
|
}
|
|
}
|
|
|
|
// lib dirs — use explicit list or fall back to <dep>/lib (best-effort, may not exist yet)
|
|
let lib_raws: Option<Vec<String>> = tbl.get("lib").and_then(|v| v.as_array()).map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.replace("{profile}", profile))
|
|
.collect()
|
|
});
|
|
|
|
if let Some(raws) = lib_raws {
|
|
for r in raws {
|
|
let p = Path::new(&r);
|
|
let full = if p.is_absolute() {
|
|
p.to_path_buf()
|
|
} else {
|
|
dep_path.join(p)
|
|
};
|
|
// Include even if it doesn't exist yet — for IntelliSense purposes
|
|
lib_dirs.push(full.to_string_lossy().to_string());
|
|
}
|
|
} else {
|
|
for default in &["lib", "lib64"] {
|
|
let candidate = dep_path.join(default);
|
|
if candidate.exists() {
|
|
lib_dirs.push(candidate.to_string_lossy().to_string());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// libs
|
|
let libs_raws: Option<Vec<String>> =
|
|
tbl.get("libs").and_then(|v| v.as_array()).map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.to_string())
|
|
.collect()
|
|
});
|
|
match libs_raws {
|
|
Some(ls) if !ls.is_empty() => libs.extend(ls),
|
|
_ => libs.push(name.clone()),
|
|
}
|
|
}
|
|
|
|
GenDeps {
|
|
include_dirs,
|
|
lib_dirs,
|
|
libs,
|
|
}
|
|
}
|
|
|
|
// ── public API ───────────────────────────────────────────────────────────────
|
|
|
|
/// Everything needed to generate output for one project member.
|
|
pub struct ProjectInfo {
|
|
pub name: String,
|
|
pub version: String,
|
|
pub root: PathBuf,
|
|
pub profile: String,
|
|
pub language: String,
|
|
pub standard: String,
|
|
pub cxx_standard: String,
|
|
pub compiler: String,
|
|
pub kind: String,
|
|
pub sources: Vec<String>,
|
|
pub include_dirs: Vec<String>,
|
|
pub lib_dirs: Vec<String>,
|
|
pub libs: Vec<String>,
|
|
pub cflags: Vec<String>,
|
|
pub ldflags: Vec<String>,
|
|
}
|
|
|
|
// ── entry-point for `dcr gen` ────────────────────────────────────────────────
|
|
|
|
pub fn r#gen(args: &[String]) -> i32 {
|
|
let subcommand = match args.first() {
|
|
Some(s) if s == "--help" => {
|
|
printc("USAGE:", BOLD_GREEN);
|
|
printc(" dcr gen <subcommand>", BOLD_CYAN);
|
|
println!();
|
|
printc("DESCRIPTION:", BOLD_GREEN);
|
|
println!(" Generates IDE and tooling integration files.");
|
|
println!();
|
|
printc("SUBCOMMANDS:", BOLD_GREEN);
|
|
println!(" project-info Print project metadata as JSON");
|
|
println!(" compile-commands Generate compile_commands.json");
|
|
println!(" vscode Generate .vscode/ integration files");
|
|
println!(" clion Generate .idea/ integration files");
|
|
return 0;
|
|
}
|
|
Some(s) => s.as_str(),
|
|
None => {
|
|
printc("USAGE:", BOLD_GREEN);
|
|
printc(" dcr gen <subcommand>", BOLD_CYAN);
|
|
println!();
|
|
printc("SUBCOMMANDS:", BOLD_GREEN);
|
|
println!(" project-info Print project metadata as JSON");
|
|
println!(" compile-commands Generate compile_commands.json");
|
|
println!(" vscode Generate .vscode/ integration files");
|
|
println!(" clion Generate .idea/ integration files");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let rest = &args[1..];
|
|
|
|
match subcommand {
|
|
"project-info" => gen_project_info(rest),
|
|
"compile-commands" => gen_compile_commands(rest),
|
|
"vscode" => gen_vscode(rest),
|
|
"clion" => gen_clion(rest),
|
|
_ => {
|
|
error(&format!("Unknown gen subcommand: {subcommand}"));
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── shared: collect per-project data ─────────────────────────────────────────
|
|
|
|
fn collect_project_info(root: &Path, profile: &str) -> Result<ProjectInfo, String> {
|
|
// Run from the project root so relative paths in dcr.toml resolve correctly.
|
|
let prev = std::env::current_dir().map_err(|e| e.to_string())?;
|
|
std::env::set_current_dir(root).map_err(|e| e.to_string())?;
|
|
let result = collect_project_info_inner(root, profile);
|
|
let _ = std::env::set_current_dir(prev);
|
|
result
|
|
}
|
|
|
|
fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo, String> {
|
|
let config = Config::open("./dcr.toml").map_err(|e| e.to_string())?;
|
|
|
|
let name = get_config_str(&config, "package.name");
|
|
let version = get_config_str(&config, "package.version");
|
|
let language = get_language_with_profile_or_default(&config, profile);
|
|
let standard = get_string_with_profile(&config, "standard", profile);
|
|
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
|
|
let compiler_s = get_string_with_profile(&config, "compiler", profile);
|
|
let kind = get_string_with_profile(&config, "kind", profile);
|
|
let build_target = get_string_with_profile(&config, "target", profile);
|
|
let platform = get_string_with_profile(&config, "platform", profile);
|
|
|
|
let tc_cc = get_config_opt(&config, "toolchain.cc");
|
|
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
|
|
let tc_as = get_config_opt(&config, "toolchain.as");
|
|
let tc_ar = get_config_opt(&config, "toolchain.ar");
|
|
let tc_ld = get_config_opt(&config, "toolchain.ld");
|
|
|
|
let base_cflags = get_list_with_profile(&config, "cflags", profile);
|
|
let base_ldflags = get_list_with_profile(&config, "ldflags", profile);
|
|
let build_excludes = get_list_with_profile(&config, "exclude", profile);
|
|
let build_includes = get_list_with_profile(&config, "include", profile);
|
|
let build_roots = get_list_with_profile(&config, "roots", profile);
|
|
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
|
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
|
|
|
|
let resolved_compiler = resolve_compiler(
|
|
&language,
|
|
&compiler_s,
|
|
tc_cc.as_deref(),
|
|
tc_cxx.as_deref(),
|
|
tc_as.as_deref(),
|
|
);
|
|
|
|
let resolved_linker = tc_ld.or_else(|| {
|
|
std::env::var("DCR_LD")
|
|
.ok()
|
|
.filter(|v| !v.trim().is_empty())
|
|
});
|
|
let resolved_archiver = tc_ar.or_else(|| {
|
|
std::env::var("DCR_AR")
|
|
.ok()
|
|
.filter(|v| !v.trim().is_empty())
|
|
});
|
|
|
|
let resolved = resolve_deps_for_gen(&config, profile, root);
|
|
let (resolved_cflags, resolved_ldflags) =
|
|
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
|
|
|
|
// Build exclude/include pattern lists (same logic as cli::build)
|
|
let mut combined_excludes: Vec<PathBuf> = Vec::new();
|
|
let mut exclude_patterns: Vec<String> = Vec::new();
|
|
for raw in &build_excludes {
|
|
let t = raw.trim();
|
|
if t.is_empty() {
|
|
continue;
|
|
}
|
|
let norm = t.replace('\\', "/");
|
|
let p = Path::new(t);
|
|
if p.is_absolute() {
|
|
combined_excludes.push(p.to_path_buf());
|
|
} else {
|
|
combined_excludes.push(root.join(p));
|
|
}
|
|
exclude_patterns.push(norm);
|
|
}
|
|
|
|
let mut combined_includes: Vec<String> = Vec::new();
|
|
combined_includes.extend(exclude_patterns.iter().map(|v| format!("!{v}")));
|
|
combined_includes.extend(build_includes.iter().map(|v| v.replace('\\', "/")));
|
|
|
|
// Source roots
|
|
let mut source_roots: Vec<PathBuf> = Vec::new();
|
|
for raw in &build_roots {
|
|
let t = raw.trim();
|
|
if t.is_empty() {
|
|
continue;
|
|
}
|
|
let p = Path::new(t);
|
|
source_roots.push(if p.is_absolute() {
|
|
p.to_path_buf()
|
|
} else {
|
|
root.join(p)
|
|
});
|
|
}
|
|
if !src_disable && source_roots.is_empty() {
|
|
source_roots.push(root.join("src"));
|
|
}
|
|
|
|
// Merge include dirs (dep include dirs + any include globs that are directories)
|
|
let mut merged_include_dirs = resolved.include_dirs.clone();
|
|
for raw in &build_includes {
|
|
let t = raw.trim();
|
|
if t.is_empty() {
|
|
continue;
|
|
}
|
|
let norm = t.replace('\\', "/");
|
|
if common::has_glob_magic(&norm) {
|
|
continue;
|
|
}
|
|
let p = Path::new(t);
|
|
let dir = if p.is_absolute() {
|
|
p.to_path_buf()
|
|
} else {
|
|
root.join(p)
|
|
};
|
|
if dir.is_dir() {
|
|
merged_include_dirs.push(dir.to_string_lossy().to_string());
|
|
}
|
|
}
|
|
|
|
let target_dir_binding = normalize_target(&build_target, profile);
|
|
let ctx = BuildContext {
|
|
profile,
|
|
project_name: &name,
|
|
compiler: &resolved_compiler,
|
|
language: &language,
|
|
standard: &standard,
|
|
cxx_standard: &cxx_standard,
|
|
target: Some(build_target.as_str()),
|
|
target_dir: target_dir_binding.as_deref(),
|
|
kind: normalize_kind(&kind),
|
|
platform: normalize_platform(&platform),
|
|
linker: resolved_linker.as_deref(),
|
|
archiver: resolved_archiver.as_deref(),
|
|
moc: None,
|
|
uic: None,
|
|
rcc: None,
|
|
package_type: None,
|
|
freestanding: false,
|
|
panic_abort: false,
|
|
codegen_units: 0,
|
|
source_roots: &source_roots,
|
|
exclude_dirs: &combined_excludes,
|
|
include_paths: &combined_includes,
|
|
include_dirs: &merged_include_dirs,
|
|
lib_dirs: &resolved.lib_dirs,
|
|
libs: &resolved.libs,
|
|
cflags: &resolved_cflags,
|
|
ldflags: &resolved_ldflags,
|
|
output_filename: None,
|
|
output_extension: None,
|
|
verbose: false,
|
|
qt: false,
|
|
};
|
|
|
|
let sources = collect_sources(&ctx).map_err(|e| format!("Failed to collect sources: {e}"))?;
|
|
|
|
// Convert relative source paths to absolute
|
|
let abs_sources: Vec<String> = sources
|
|
.iter()
|
|
.map(|s| {
|
|
let p = Path::new(s);
|
|
if p.is_absolute() {
|
|
s.clone()
|
|
} else {
|
|
root.join(s).to_string_lossy().to_string()
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(ProjectInfo {
|
|
name,
|
|
version,
|
|
root: root.to_path_buf(),
|
|
profile: profile.to_string(),
|
|
language,
|
|
standard,
|
|
cxx_standard,
|
|
compiler: resolved_compiler,
|
|
kind: normalize_kind(&kind).to_string(),
|
|
sources: abs_sources,
|
|
include_dirs: merged_include_dirs,
|
|
lib_dirs: resolved.lib_dirs,
|
|
libs: resolved.libs,
|
|
cflags: resolved_cflags,
|
|
ldflags: resolved_ldflags,
|
|
})
|
|
}
|
|
|
|
/// Collect info for root project + all workspace members.
|
|
fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
|
// Check for workspace
|
|
let config = {
|
|
let prev = std::env::current_dir().map_err(|e| e.to_string())?;
|
|
std::env::set_current_dir(root).map_err(|e| e.to_string())?;
|
|
let cfg = Config::open("./dcr.toml").map_err(|e| e.to_string());
|
|
let _ = std::env::set_current_dir(prev);
|
|
cfg?
|
|
};
|
|
|
|
let mut all = Vec::new();
|
|
|
|
if let Ok(Some(ws)) = parse_workspace(&config, profile, None, root) {
|
|
for member in &ws.members {
|
|
match collect_project_info(&member.path, profile) {
|
|
Ok(info) => all.push(info),
|
|
Err(e) => eprintln!(
|
|
"Warning: skipping workspace member {}: {e}",
|
|
member.path.display()
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Root project itself
|
|
let root_info = collect_project_info(root, profile)?;
|
|
all.push(root_info);
|
|
|
|
Ok(all)
|
|
}
|
|
|
|
// ── dcr gen project-info ─────────────────────────────────────────────────────
|
|
|
|
fn gen_project_info(args: &[String]) -> i32 {
|
|
let (root, profile) = match parse_gen_args(args) {
|
|
Ok(v) => v,
|
|
Err(code) => return code,
|
|
};
|
|
|
|
let all = match collect_all(&root, &profile) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
error(&e);
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
print!("[");
|
|
for (i, info) in all.iter().enumerate() {
|
|
if i > 0 {
|
|
print!(",");
|
|
}
|
|
println!();
|
|
print!("{}", project_info_to_json(info));
|
|
}
|
|
println!();
|
|
println!("]");
|
|
0
|
|
}
|
|
|
|
fn project_info_to_json(info: &ProjectInfo) -> String {
|
|
let mut out = String::new();
|
|
out.push_str(" {\n");
|
|
out.push_str(&format!(" \"name\": {},\n", json_str(&info.name)));
|
|
out.push_str(&format!(" \"version\": {},\n", json_str(&info.version)));
|
|
out.push_str(&format!(
|
|
" \"root\": {},\n",
|
|
json_str(&info.root.to_string_lossy())
|
|
));
|
|
out.push_str(&format!(" \"profile\": {},\n", json_str(&info.profile)));
|
|
out.push_str(&format!(
|
|
" \"language\": {},\n",
|
|
json_str(&info.language)
|
|
));
|
|
out.push_str(&format!(
|
|
" \"standard\": {},\n",
|
|
json_str(&info.standard)
|
|
));
|
|
out.push_str(&format!(
|
|
" \"cxx_standard\": {},\n",
|
|
json_str(&info.cxx_standard)
|
|
));
|
|
out.push_str(&format!(
|
|
" \"compiler\": {},\n",
|
|
json_str(&info.compiler)
|
|
));
|
|
out.push_str(&format!(" \"kind\": {},\n", json_str(&info.kind)));
|
|
out.push_str(&format!(
|
|
" \"sources\": {},\n",
|
|
json_str_array(&info.sources)
|
|
));
|
|
out.push_str(&format!(
|
|
" \"include_dirs\": {},\n",
|
|
json_str_array(&info.include_dirs)
|
|
));
|
|
out.push_str(&format!(
|
|
" \"lib_dirs\": {},\n",
|
|
json_str_array(&info.lib_dirs)
|
|
));
|
|
out.push_str(&format!(" \"libs\": {},\n", json_str_array(&info.libs)));
|
|
out.push_str(&format!(
|
|
" \"cflags\": {},\n",
|
|
json_str_array(&info.cflags)
|
|
));
|
|
out.push_str(&format!(
|
|
" \"ldflags\": {}\n",
|
|
json_str_array(&info.ldflags)
|
|
));
|
|
out.push_str(" }");
|
|
out
|
|
}
|
|
|
|
// ── dcr gen compile-commands ─────────────────────────────────────────────────
|
|
|
|
fn gen_compile_commands(args: &[String]) -> i32 {
|
|
let (root, profile) = match parse_gen_args(args) {
|
|
Ok(v) => v,
|
|
Err(code) => return code,
|
|
};
|
|
|
|
let all = match collect_all(&root, &profile) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
error(&e);
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
gen_compile_commands_inner(&root, &profile, &all)
|
|
}
|
|
|
|
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo]) -> i32 {
|
|
let entries = build_compile_commands(all, profile);
|
|
|
|
let out_path = root.join("compile_commands.json");
|
|
match std::fs::write(&out_path, &entries) {
|
|
Ok(_) => {
|
|
println!("Generated {}", out_path.display());
|
|
0
|
|
}
|
|
Err(e) => {
|
|
error(&format!("Failed to write compile_commands.json: {e}"));
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String {
|
|
let mut out = String::from("[\n");
|
|
let mut first = true;
|
|
|
|
for info in projects {
|
|
for source in &info.sources {
|
|
if !first {
|
|
out.push_str(",\n");
|
|
}
|
|
first = false;
|
|
|
|
let command = build_compile_command(info, source, profile);
|
|
out.push_str(" {\n");
|
|
out.push_str(&format!(
|
|
" \"directory\": {},\n",
|
|
json_str(&info.root.to_string_lossy())
|
|
));
|
|
out.push_str(&format!(" \"file\": {},\n", json_str(source)));
|
|
out.push_str(&format!(
|
|
" \"arguments\": {}\n",
|
|
json_str_array(&command)
|
|
));
|
|
out.push_str(" }");
|
|
}
|
|
}
|
|
|
|
out.push_str("\n]\n");
|
|
out
|
|
}
|
|
|
|
fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec<String> {
|
|
let mut cmd: Vec<String> = Vec::new();
|
|
let compiler = if info.compiler.is_empty() {
|
|
"cc"
|
|
} else {
|
|
&info.compiler
|
|
};
|
|
cmd.push(compiler.to_string());
|
|
cmd.push("-c".to_string());
|
|
|
|
// ASM x flag — must be before source file
|
|
if let Some(flag) = common::asm_lang_flag(source) {
|
|
cmd.push("-x".to_string());
|
|
cmd.push(flag.to_string());
|
|
}
|
|
|
|
cmd.push(source.to_string());
|
|
|
|
// Object path (for -o, approximate — not critical for IntelliSense)
|
|
let obj_dir = info.root.join("target").join(profile).join("obj");
|
|
let obj_path = {
|
|
let p = Path::new(source);
|
|
let rel = strip_src_prefix(p);
|
|
obj_dir
|
|
.join(rel)
|
|
.with_extension("o")
|
|
.to_string_lossy()
|
|
.to_string()
|
|
};
|
|
cmd.push("-o".to_string());
|
|
cmd.push(obj_path);
|
|
|
|
if info.kind == "sharedlib" {
|
|
cmd.push("-fPIC".to_string());
|
|
}
|
|
|
|
// -std=
|
|
if !info.standard.is_empty() && info.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 && !info.cxx_standard.is_empty() {
|
|
cmd.push(format!("-std={}", info.cxx_standard));
|
|
} else if !is_cpp {
|
|
cmd.push(format!("-std={}", info.standard));
|
|
}
|
|
}
|
|
|
|
// Default profile flags (mirrors unix_cc.rs defaults)
|
|
match profile {
|
|
"release" => {
|
|
cmd.push("-O3".to_string());
|
|
cmd.push("-DNDEBUG".to_string());
|
|
}
|
|
"debug" => {
|
|
cmd.push("-O0".to_string());
|
|
cmd.push("-g".to_string());
|
|
cmd.push("-Wall".to_string());
|
|
cmd.push("-Wextra".to_string());
|
|
cmd.push("-fno-omit-frame-pointer".to_string());
|
|
cmd.push("-DDCR_DEBUG".to_string());
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
for flag in &info.cflags {
|
|
// Expand relative -I paths to absolute so clangd/cpptools work
|
|
// regardless of their working directory.
|
|
if let Some(rel) = flag.strip_prefix("-I") {
|
|
let p = Path::new(rel);
|
|
let abs = if p.is_absolute() {
|
|
p.to_path_buf()
|
|
} else {
|
|
info.root.join(p)
|
|
};
|
|
cmd.push(format!("-I{}", abs.to_string_lossy()));
|
|
} else {
|
|
cmd.push(flag.clone());
|
|
}
|
|
}
|
|
for dir in &info.include_dirs {
|
|
cmd.push(format!("-I{dir}"));
|
|
}
|
|
|
|
cmd
|
|
}
|
|
|
|
fn strip_src_prefix(p: &Path) -> PathBuf {
|
|
// Try to strip leading ./src or src
|
|
let s = p.to_string_lossy();
|
|
let trimmed = s.trim_start_matches("./");
|
|
let without_src = trimmed
|
|
.strip_prefix("src/")
|
|
.unwrap_or(trimmed)
|
|
.strip_prefix("src\\")
|
|
.unwrap_or(trimmed);
|
|
PathBuf::from(without_src)
|
|
}
|
|
|
|
// ── dcr gen vscode ───────────────────────────────────────────────────────────
|
|
|
|
fn gen_vscode(args: &[String]) -> i32 {
|
|
let (root, profile) = match parse_gen_args(args) {
|
|
Ok(v) => v,
|
|
Err(code) => return code,
|
|
};
|
|
|
|
// Collect project info once for tasks/launch and compile-commands
|
|
let all = match collect_all(&root, &profile) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
error(&e);
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
// 1. Generate compile_commands.json
|
|
let cc_code = gen_compile_commands_inner(&root, &profile, &all);
|
|
if cc_code != 0 {
|
|
return cc_code;
|
|
}
|
|
|
|
let vscode_dir = root.join(".vscode");
|
|
if let Err(e) = std::fs::create_dir_all(&vscode_dir) {
|
|
error(&format!("Failed to create .vscode/: {e}"));
|
|
return 1;
|
|
}
|
|
|
|
// tasks.json
|
|
if let Err(e) = std::fs::write(vscode_dir.join("tasks.json"), gen_tasks_json()) {
|
|
error(&format!("Failed to write tasks.json: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", vscode_dir.join("tasks.json").display());
|
|
|
|
// launch.json — one entry per binary target
|
|
let launch = gen_launch_json(&all, &root);
|
|
if let Err(e) = std::fs::write(vscode_dir.join("launch.json"), launch) {
|
|
error(&format!("Failed to write launch.json: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", vscode_dir.join("launch.json").display());
|
|
|
|
// settings.json (clangd compile-commands-dir)
|
|
let settings = gen_settings_json(&root);
|
|
if let Err(e) = std::fs::write(vscode_dir.join("settings.json"), settings) {
|
|
error(&format!("Failed to write settings.json: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", vscode_dir.join("settings.json").display());
|
|
|
|
// extensions.json — disable cpptools, recommend clangd
|
|
if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) {
|
|
error(&format!("Failed to write extensions.json: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", vscode_dir.join("extensions.json").display());
|
|
|
|
0
|
|
}
|
|
|
|
fn gen_extensions_json() -> String {
|
|
r#"{
|
|
"recommendations": [
|
|
"llvm-vs-code-extensions.vscode-clangd",
|
|
"vadimcn.vscode-lldb"
|
|
],
|
|
"unwantedRecommendations": [
|
|
"ms-vscode.cpptools",
|
|
"ms-vscode.cpptools-extension-pack",
|
|
"ms-vscode.cpptools-themes"
|
|
]
|
|
}
|
|
"#
|
|
.to_string()
|
|
}
|
|
|
|
fn gen_tasks_json() -> String {
|
|
r#"{
|
|
"version": "2.0.0",
|
|
"tasks": [
|
|
{
|
|
"label": "dcr: build (debug)",
|
|
"type": "shell",
|
|
"command": "dcr build --debug",
|
|
"group": {
|
|
"kind": "build",
|
|
"isDefault": true
|
|
},
|
|
"problemMatcher": ["$gcc"],
|
|
"presentation": { "reveal": "always", "panel": "shared" }
|
|
},
|
|
{
|
|
"label": "dcr: build (release)",
|
|
"type": "shell",
|
|
"command": "dcr build --release",
|
|
"group": "build",
|
|
"problemMatcher": ["$gcc"],
|
|
"presentation": { "reveal": "always", "panel": "shared" }
|
|
},
|
|
{
|
|
"label": "dcr: run (debug)",
|
|
"type": "shell",
|
|
"command": "dcr run --debug",
|
|
"group": {
|
|
"kind": "test",
|
|
"isDefault": true
|
|
},
|
|
"problemMatcher": ["$gcc"],
|
|
"presentation": { "reveal": "always", "panel": "shared" }
|
|
},
|
|
{
|
|
"label": "dcr: run (release)",
|
|
"type": "shell",
|
|
"command": "dcr run --release",
|
|
"group": "test",
|
|
"problemMatcher": ["$gcc"],
|
|
"presentation": { "reveal": "always", "panel": "shared" }
|
|
},
|
|
{
|
|
"label": "dcr: clean",
|
|
"type": "shell",
|
|
"command": "dcr clean --all",
|
|
"group": "none",
|
|
"problemMatcher": [],
|
|
"presentation": { "reveal": "always", "panel": "shared" }
|
|
},
|
|
{
|
|
"label": "dcr: gen compile-commands",
|
|
"type": "shell",
|
|
"command": "dcr gen compile-commands",
|
|
"group": "none",
|
|
"problemMatcher": [],
|
|
"presentation": { "reveal": "always", "panel": "shared" }
|
|
}
|
|
]
|
|
}
|
|
"#
|
|
.to_string()
|
|
}
|
|
|
|
fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|
let mut configs = Vec::new();
|
|
|
|
for info in projects {
|
|
if info.kind != "bin" {
|
|
continue;
|
|
}
|
|
|
|
// binary expected at info.root/target/<profile>/<name> (account for member projects)
|
|
let debug_bin = info
|
|
.root
|
|
.join("target")
|
|
.join("debug")
|
|
.join(&info.name)
|
|
.to_string_lossy()
|
|
.to_string();
|
|
let release_bin = info
|
|
.root
|
|
.join("target")
|
|
.join("release")
|
|
.join(&info.name)
|
|
.to_string_lossy()
|
|
.to_string();
|
|
|
|
let debug_entry = format!(
|
|
r#" {{
|
|
"name": {name},
|
|
"type": "lldb",
|
|
"request": "launch",
|
|
"program": {prog},
|
|
"args": [],
|
|
"stopOnEntry": false,
|
|
"cwd": {cwd},
|
|
"terminal": "integrated",
|
|
"preLaunchTask": "dcr: build (debug)"
|
|
}}"#,
|
|
name = json_str(&format!("{} (debug)", info.name)),
|
|
prog = json_str(&debug_bin),
|
|
cwd = json_str(&info.root.to_string_lossy()),
|
|
);
|
|
|
|
let release_entry = format!(
|
|
r#" {{
|
|
"name": {name},
|
|
"type": "lldb",
|
|
"request": "launch",
|
|
"program": {prog},
|
|
"args": [],
|
|
"stopOnEntry": false,
|
|
"cwd": {cwd},
|
|
"terminal": "integrated",
|
|
"preLaunchTask": "dcr: build (release)"
|
|
}}"#,
|
|
name = json_str(&format!("{} (release)", info.name)),
|
|
prog = json_str(&release_bin),
|
|
cwd = json_str(&info.root.to_string_lossy()),
|
|
);
|
|
|
|
configs.push(debug_entry);
|
|
configs.push(release_entry);
|
|
}
|
|
|
|
if configs.is_empty() {
|
|
// no binary targets — emit a placeholder
|
|
configs.push(
|
|
r#" {
|
|
"name": "(placeholder — no binary targets found)",
|
|
"type": "lldb",
|
|
"request": "launch",
|
|
"program": "",
|
|
"cwd": "${workspaceFolder}"
|
|
}"#
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
format!(
|
|
"{{\n \"version\": \"0.2.0\",\n \"configurations\": [\n{}\n ]\n}}\n",
|
|
configs.join(",\n")
|
|
)
|
|
}
|
|
|
|
fn gen_settings_json(root: &Path) -> String {
|
|
let cc_dir = root.to_string_lossy();
|
|
format!(
|
|
r#"{{
|
|
"clangd.arguments": [
|
|
"--compile-commands-dir={cc_dir}",
|
|
"--header-insertion=never",
|
|
"--clang-tidy=false"
|
|
],
|
|
"C_Cpp.intelliSenseEngine": "disabled",
|
|
"C_Cpp.autocomplete": "disabled",
|
|
"C_Cpp.errorSquiggles": "disabled",
|
|
"C_Cpp.hover": "disabled"
|
|
}}
|
|
"#
|
|
)
|
|
}
|
|
|
|
// ── dcr gen clion ─────────────────────────────────────────────────────────────
|
|
|
|
fn gen_clion(args: &[String]) -> i32 {
|
|
let (root, profile) = match parse_gen_args(args) {
|
|
Ok(v) => v,
|
|
Err(code) => return code,
|
|
};
|
|
|
|
// collect project info
|
|
let all = match collect_all(&root, &profile) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
error(&e);
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
// 1. compile_commands.json
|
|
let cc_code = gen_compile_commands_inner(&root, &profile, &all);
|
|
if cc_code != 0 {
|
|
return cc_code;
|
|
}
|
|
|
|
let idea_dir = root.join(".idea");
|
|
if let Err(e) = std::fs::create_dir_all(&idea_dir) {
|
|
error(&format!("Failed to create .idea/: {e}"));
|
|
return 1;
|
|
}
|
|
let run_configs_dir = idea_dir.join("runConfigurations");
|
|
if let Err(e) = std::fs::create_dir_all(&run_configs_dir) {
|
|
error(&format!("Failed to create .idea/runConfigurations/: {e}"));
|
|
return 1;
|
|
}
|
|
|
|
// externalTools.xml
|
|
let ext_tools = gen_clion_external_tools();
|
|
if let Err(e) = std::fs::write(idea_dir.join("externalTools.xml"), ext_tools) {
|
|
error(&format!("Failed to write externalTools.xml: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", idea_dir.join("externalTools.xml").display());
|
|
|
|
// customTargets.xml
|
|
let targets = gen_clion_custom_targets();
|
|
if let Err(e) = std::fs::write(idea_dir.join("customTargets.xml"), targets) {
|
|
error(&format!("Failed to write customTargets.xml: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", idea_dir.join("customTargets.xml").display());
|
|
|
|
// misc.xml — point CLion at compile_commands.json
|
|
let misc = gen_clion_misc_xml(&root);
|
|
if let Err(e) = std::fs::write(idea_dir.join("misc.xml"), misc) {
|
|
error(&format!("Failed to write misc.xml: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", idea_dir.join("misc.xml").display());
|
|
|
|
// .idea/.gitignore
|
|
let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n";
|
|
if let Err(e) = std::fs::write(idea_dir.join(".gitignore"), gitignore) {
|
|
error(&format!("Failed to write .idea/.gitignore: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", idea_dir.join(".gitignore").display());
|
|
|
|
// runConfigurations/<name>.xml — one per binary
|
|
for info in &all {
|
|
if info.kind != "bin" {
|
|
continue;
|
|
}
|
|
let xml = gen_clion_run_config(info, &root, &profile);
|
|
let fname = format!("{}.xml", sanitize_filename(&info.name));
|
|
let path = run_configs_dir.join(&fname);
|
|
if let Err(e) = std::fs::write(&path, xml) {
|
|
error(&format!("Failed to write runConfigurations/{fname}: {e}"));
|
|
return 1;
|
|
}
|
|
println!("Generated {}", path.display());
|
|
}
|
|
|
|
0
|
|
}
|
|
|
|
fn gen_clion_external_tools() -> String {
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<project version="4">
|
|
<component name="ExternalToolsComponent">
|
|
<tools name="DCR">
|
|
<tool name="Build Debug"
|
|
description="dcr build --debug"
|
|
showInMainMenu="true"
|
|
showInEditor="false"
|
|
showInProject="false"
|
|
showInSearchPopup="false"
|
|
disabled="false"
|
|
useConsole="true"
|
|
showConsoleOnStdOut="false"
|
|
showConsoleOnStdErr="true"
|
|
synchronizeAfterRun="true">
|
|
<exec>
|
|
<option name="COMMAND" value="dcr" />
|
|
<option name="PARAMETERS" value="build --debug" />
|
|
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
|
|
</exec>
|
|
</tool>
|
|
<tool name="Build Release"
|
|
description="dcr build --release"
|
|
showInMainMenu="true"
|
|
showInEditor="false"
|
|
showInProject="false"
|
|
showInSearchPopup="false"
|
|
disabled="false"
|
|
useConsole="true"
|
|
showConsoleOnStdOut="false"
|
|
showConsoleOnStdErr="true"
|
|
synchronizeAfterRun="true">
|
|
<exec>
|
|
<option name="COMMAND" value="dcr" />
|
|
<option name="PARAMETERS" value="build --release" />
|
|
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
|
|
</exec>
|
|
</tool>
|
|
<tool name="Clean"
|
|
description="dcr clean"
|
|
showInMainMenu="true"
|
|
showInEditor="false"
|
|
showInProject="false"
|
|
showInSearchPopup="false"
|
|
disabled="false"
|
|
useConsole="true"
|
|
showConsoleOnStdOut="false"
|
|
showConsoleOnStdErr="true"
|
|
synchronizeAfterRun="true">
|
|
<exec>
|
|
<option name="COMMAND" value="dcr" />
|
|
<option name="PARAMETERS" value="clean" />
|
|
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
|
|
</exec>
|
|
</tool>
|
|
<tool name="Gen Compile Commands"
|
|
description="dcr gen compile-commands"
|
|
showInMainMenu="true"
|
|
showInEditor="false"
|
|
showInProject="false"
|
|
showInSearchPopup="false"
|
|
disabled="false"
|
|
useConsole="true"
|
|
showConsoleOnStdOut="false"
|
|
showConsoleOnStdErr="true"
|
|
synchronizeAfterRun="true">
|
|
<exec>
|
|
<option name="COMMAND" value="dcr" />
|
|
<option name="PARAMETERS" value="gen compile-commands" />
|
|
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
|
|
</exec>
|
|
</tool>
|
|
</tools>
|
|
</component>
|
|
</project>
|
|
"#
|
|
.to_string()
|
|
}
|
|
|
|
fn gen_clion_custom_targets() -> String {
|
|
// Fixed UUIDs for CLion custom targets
|
|
let uuid = "dcr00000-0000-0000-0000-000000000001";
|
|
format!(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<project version="4">
|
|
<component name="CLionExternalBuildManager">
|
|
<target id="{uuid}"
|
|
name="dcr: build (debug)"
|
|
defaultType="TOOL">
|
|
<build type="TOOL">
|
|
<tool actionId="Tool_DCR_Build Debug" />
|
|
</build>
|
|
<clean type="TOOL">
|
|
<tool actionId="Tool_DCR_Clean" />
|
|
</clean>
|
|
</target>
|
|
<target id="dcr00000-0000-0000-0000-000000000002"
|
|
name="dcr: build (release)"
|
|
defaultType="TOOL">
|
|
<build type="TOOL">
|
|
<tool actionId="Tool_DCR_Build Release" />
|
|
</build>
|
|
<clean type="TOOL">
|
|
<tool actionId="Tool_DCR_Clean" />
|
|
</clean>
|
|
</target>
|
|
</component>
|
|
</project>
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn gen_clion_misc_xml(root: &Path) -> String {
|
|
let cc_path = root.join("compile_commands.json");
|
|
let cc = xml_escape(&cc_path.to_string_lossy());
|
|
format!(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<project version="4">
|
|
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
|
|
<component name="CompDBWorkspace" projectDir="$PROJECT_DIR$">
|
|
<customCompileCommandsPath>{cc}</customCompileCommandsPath>
|
|
</component>
|
|
</project>
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn gen_clion_run_config(info: &ProjectInfo, root: &Path, profile: &str) -> String {
|
|
let bin_path = root.join("target").join(profile).join(&info.name);
|
|
let bin = xml_escape(&bin_path.to_string_lossy());
|
|
let target = if profile == "release" {
|
|
"dcr: build (release)"
|
|
} else {
|
|
"dcr: build (debug)"
|
|
};
|
|
let target_esc = xml_escape(target);
|
|
let name_esc = xml_escape(&format!("{} ({})", info.name, profile));
|
|
format!(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<component name="ProjectRunConfigurationManager">
|
|
<configuration default="false"
|
|
name="{name_esc}"
|
|
type="CLionExternalRunConfiguration"
|
|
factoryName="Application">
|
|
<build target="{target_esc}" />
|
|
<executable path="{bin}" />
|
|
<workingDirectory value="$PROJECT_DIR$" />
|
|
<envs />
|
|
<method v="2">
|
|
<option name="CLionExternalBuildTargetBeforeRunTask" enabled="true" />
|
|
</method>
|
|
</configuration>
|
|
</component>
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn sanitize_filename(s: &str) -> String {
|
|
s.chars()
|
|
.map(|c| {
|
|
if c.is_alphanumeric() || c == '-' || c == '_' {
|
|
c
|
|
} else {
|
|
'_'
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn xml_escape(s: &str) -> String {
|
|
s.replace('&', "&")
|
|
.replace('<', "<")
|
|
.replace('>', ">")
|
|
.replace('"', """)
|
|
.replace('\'', "'")
|
|
}
|
|
|
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
|
|
let mut profile = "debug".to_string();
|
|
for arg in args {
|
|
match arg.as_str() {
|
|
"--debug" => profile = "debug".to_string(),
|
|
"--release" => profile = "release".to_string(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let start = std::env::current_dir().map_err(|_| {
|
|
error("Failed to determine current directory");
|
|
1i32
|
|
})?;
|
|
|
|
let root = match find_project_root(&start) {
|
|
Ok(Some(r)) => r,
|
|
Ok(None) => {
|
|
error("dcr.toml not found");
|
|
return Err(1);
|
|
}
|
|
Err(_) => {
|
|
error("Failed to find project root");
|
|
return Err(1);
|
|
}
|
|
};
|
|
|
|
Ok((root, profile))
|
|
}
|
|
|
|
fn json_str(s: &str) -> String {
|
|
let mut result = String::with_capacity(s.len() + 2);
|
|
result.push('"');
|
|
for c in s.chars() {
|
|
match c {
|
|
'\\' => result.push_str("\\\\"),
|
|
'"' => result.push_str("\\\""),
|
|
'\n' => result.push_str("\\n"),
|
|
'\r' => result.push_str("\\r"),
|
|
'\t' => result.push_str("\\t"),
|
|
'\x08' => result.push_str("\\b"),
|
|
'\x0c' => result.push_str("\\f"),
|
|
c if c.is_control() => {
|
|
// Escape other control characters as unicode escapes
|
|
result.push_str(&format!("\\u{:04x}", c as u32));
|
|
}
|
|
c => result.push(c),
|
|
}
|
|
}
|
|
result.push('"');
|
|
result
|
|
}
|
|
|
|
fn json_str_array(items: &[String]) -> String {
|
|
let inner: Vec<String> = items.iter().map(|s| json_str(s)).collect();
|
|
format!("[{}]", inner.join(", "))
|
|
}
|