mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 22:09:35 +00:00
- Added `cxx_standard` field to the build configuration for specifying C++ standards. - Updated documentation to reflect new `cxx_standard` and variable substitution capabilities in `cflags`, `ldflags`, and `include`. - Implemented logic to handle C++ standard flags in the build process. - Modified object file naming to include source file extensions. - Updated various builder modules to support new language parsing logic. - Introduced issue and feature request templates for better community engagement.
394 lines
12 KiB
Rust
394 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::config::Config;
|
|
use crate::utils::log::warn;
|
|
|
|
pub struct VersionInfo {
|
|
pub full: String,
|
|
pub major: String,
|
|
pub minor: String,
|
|
pub patch: String,
|
|
pub suffix: String,
|
|
pub suffix_dash: String,
|
|
}
|
|
|
|
pub fn parse_version_info(version: &str) -> VersionInfo {
|
|
let mut full = version.trim().to_string();
|
|
if full.is_empty() {
|
|
full = "0.0.0".to_string();
|
|
}
|
|
let full_clone = full.clone();
|
|
let (base, suffix) = match full_clone.split_once('-') {
|
|
Some((head, tail)) => (head, tail),
|
|
None => (full_clone.as_str(), ""),
|
|
};
|
|
let mut parts = base.split('.');
|
|
let major = parts.next().unwrap_or("0").to_string();
|
|
let minor = parts.next().unwrap_or("0").to_string();
|
|
let patch = parts.next().unwrap_or("0").to_string();
|
|
let suffix_dash = if suffix.is_empty() {
|
|
"".to_string()
|
|
} else {
|
|
format!("-{suffix}")
|
|
};
|
|
VersionInfo {
|
|
full,
|
|
major,
|
|
minor,
|
|
patch,
|
|
suffix: suffix.to_string(),
|
|
suffix_dash,
|
|
}
|
|
}
|
|
|
|
pub fn normalize_target_os(target: &str) -> &str {
|
|
match target {
|
|
"linux" => "x86_64-unknown-linux-gnu",
|
|
"macos" => "x86_64-apple-darwin",
|
|
"windows" => "x86_64-pc-windows-msvc",
|
|
_ if target.contains('-') => target,
|
|
_ => {
|
|
warn(&format!(
|
|
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
|
|
target
|
|
));
|
|
target
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_config_str(config: &Config, key: &str) -> String {
|
|
config
|
|
.get(key)
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string()
|
|
}
|
|
|
|
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
|
|
config
|
|
.get("build")
|
|
.and_then(|v| v.as_table())
|
|
.and_then(|b| b.get(profile))
|
|
.and_then(|v| v.as_table())
|
|
}
|
|
|
|
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
|
let value = config.get(key)?.as_str()?;
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
}
|
|
|
|
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
|
let base = get_config_str(config, &format!("build.{field}"));
|
|
let Some(table) = profile_table(config, profile) else {
|
|
return base;
|
|
};
|
|
let value = table.get(field).and_then(|v| v.as_str()).unwrap_or("");
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
base
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
|
config
|
|
.get(key)
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.to_string())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
|
|
let mut out = get_config_list(config, &format!("build.{field}"));
|
|
if let Some(table) = profile_table(config, profile)
|
|
&& let Some(extra) = table.get(field).and_then(|v| v.as_array())
|
|
{
|
|
out.extend(
|
|
extra
|
|
.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.to_string()),
|
|
);
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
|
let base = config
|
|
.get(&format!("build.{field}"))
|
|
.and_then(|v| v.as_bool());
|
|
let profile_val =
|
|
profile_table(config, profile).and_then(|t| t.get(field).and_then(|v| v.as_bool()));
|
|
profile_val.or(base).unwrap_or(default)
|
|
}
|
|
|
|
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
|
|
if let Some(table) = profile_table(config, profile)
|
|
&& let Some(value) = table.get("language")
|
|
{
|
|
return parse_language_value(value, "build.language");
|
|
}
|
|
match config.get("build.language") {
|
|
Some(v) => parse_language_value(v, "build.language"),
|
|
None => Err("build.language is missing".to_string()),
|
|
}
|
|
}
|
|
|
|
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
|
|
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
|
|
}
|
|
|
|
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
|
|
if let Some(s) = value.as_str() {
|
|
let trimmed = s.trim();
|
|
if trimmed.is_empty() {
|
|
return Err(format!("{key} is empty"));
|
|
}
|
|
return Ok(trimmed.to_string());
|
|
}
|
|
let arr = value
|
|
.as_array()
|
|
.ok_or_else(|| format!("{key} must be string or array of strings"))?;
|
|
let mut parts = Vec::new();
|
|
for item in arr {
|
|
let s = item
|
|
.as_str()
|
|
.ok_or_else(|| format!("{key} must be string or array of strings"))?;
|
|
let trimmed = s.trim();
|
|
if trimmed.is_empty() {
|
|
return Err(format!("{key} contains empty value"));
|
|
}
|
|
parts.push(trimmed.to_string());
|
|
}
|
|
if parts.is_empty() {
|
|
return Err(format!("{key} is empty"));
|
|
}
|
|
Ok(parts.join(","))
|
|
}
|
|
|
|
pub fn resolve_compiler(
|
|
language: &str,
|
|
compiler: &str,
|
|
tc_cc: Option<&str>,
|
|
tc_cxx: Option<&str>,
|
|
tc_as: Option<&str>,
|
|
) -> String {
|
|
let lang = primary_language(language);
|
|
env_override_compiler(&lang)
|
|
.or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as))
|
|
.unwrap_or_else(|| compiler.to_string())
|
|
}
|
|
|
|
fn env_override_compiler(lang: &str) -> Option<String> {
|
|
if let Ok(value) = std::env::var("DCR_COMPILER") {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
if lang == "asm" {
|
|
if let Ok(value) = std::env::var("DCR_AS") {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
if (lang == "c++" || lang == "cpp" || lang == "cxx")
|
|
&& let Ok(value) = std::env::var("DCR_CXX")
|
|
{
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
if let Ok(value) = std::env::var("DCR_CC") {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn toolchain_override_compiler(
|
|
lang: &str,
|
|
tc_cc: Option<&str>,
|
|
tc_cxx: Option<&str>,
|
|
tc_as: Option<&str>,
|
|
) -> Option<String> {
|
|
if lang == "asm" {
|
|
return tc_as.map(|v| v.to_string());
|
|
}
|
|
if (lang == "c++" || lang == "cpp" || lang == "cxx")
|
|
&& let Some(v) = tc_cxx
|
|
{
|
|
return Some(v.to_string());
|
|
}
|
|
tc_cc.map(|v| v.to_string())
|
|
}
|
|
|
|
pub fn primary_language(language: &str) -> String {
|
|
let parts: Vec<String> = language
|
|
.split(',')
|
|
.map(|p| p.trim().to_lowercase())
|
|
.filter(|p| !p.is_empty())
|
|
.collect();
|
|
for p in &parts {
|
|
if p == "c++" || p == "cpp" || p == "cxx" {
|
|
return p.clone();
|
|
}
|
|
}
|
|
if parts.iter().any(|p| p == "c") {
|
|
return "c".to_string();
|
|
}
|
|
if parts.iter().any(|p| p == "asm") {
|
|
return "asm".to_string();
|
|
}
|
|
language.to_lowercase()
|
|
}
|
|
|
|
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
|
if let Ok(value) = std::env::var(env_key) {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
fallback.map(|v| v.to_string())
|
|
}
|
|
|
|
pub fn resolve_pkg_config_flags(
|
|
pkgs: &[String],
|
|
base_cflags: &[String],
|
|
base_ldflags: &[String],
|
|
) -> Result<(Vec<String>, Vec<String>), String> {
|
|
let mut cflags = base_cflags.to_vec();
|
|
let mut ldflags = base_ldflags.to_vec();
|
|
for pkg in pkgs {
|
|
let c_out = run_pkg_config(pkg, "--cflags")?;
|
|
let l_out = run_pkg_config(pkg, "--libs")?;
|
|
cflags.extend(split_flags(&c_out));
|
|
ldflags.extend(split_flags(&l_out));
|
|
}
|
|
Ok((cflags, ldflags))
|
|
}
|
|
|
|
pub fn resolve_pkg_config_flags_lossy(
|
|
pkgs: &[String],
|
|
base_cflags: &[String],
|
|
base_ldflags: &[String],
|
|
) -> (Vec<String>, Vec<String>) {
|
|
match resolve_pkg_config_flags(pkgs, base_cflags, base_ldflags) {
|
|
Ok(flags) => flags,
|
|
Err(err) => {
|
|
eprintln!("Warning: {err}");
|
|
(base_cflags.to_vec(), base_ldflags.to_vec())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
|
let output = std::process::Command::new("pkg-config")
|
|
.arg(arg)
|
|
.arg(pkg)
|
|
.output()
|
|
.map_err(|err| format!("Failed to run pkg-config: {err}"))?;
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!("pkg-config failed for {pkg}: {stderr}"));
|
|
}
|
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
}
|
|
|
|
fn split_flags(value: &str) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
let mut current = String::new();
|
|
let mut chars = value.chars().peekable();
|
|
let mut quote: Option<char> = None;
|
|
while let Some(ch) = chars.next() {
|
|
if let Some(q) = quote {
|
|
if ch == q {
|
|
quote = None;
|
|
} else {
|
|
current.push(ch);
|
|
}
|
|
continue;
|
|
}
|
|
match ch {
|
|
'\'' | '"' => quote = Some(ch),
|
|
'\\' => {
|
|
if let Some(next) = chars.next() {
|
|
current.push(next);
|
|
}
|
|
}
|
|
c if c.is_whitespace() => {
|
|
if !current.is_empty() {
|
|
out.push(std::mem::take(&mut current));
|
|
}
|
|
}
|
|
c => current.push(c),
|
|
}
|
|
}
|
|
if !current.is_empty() {
|
|
out.push(current);
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn normalize_target_short_names() {
|
|
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
|
|
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
|
|
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
|
|
assert_eq!(
|
|
normalize_target_os("x86_64-unknown-linux-gnu"),
|
|
"x86_64-unknown-linux-gnu"
|
|
);
|
|
assert_eq!(normalize_target_os("unknown"), "unknown");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_version_parts() {
|
|
let info = parse_version_info("1.2.3-beta");
|
|
assert_eq!(info.full, "1.2.3-beta");
|
|
assert_eq!(info.major, "1");
|
|
assert_eq!(info.minor, "2");
|
|
assert_eq!(info.patch, "3");
|
|
assert_eq!(info.suffix, "beta");
|
|
assert_eq!(info.suffix_dash, "-beta");
|
|
}
|
|
}
|