use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
#[derive(Debug, Clone)]
pub struct BuildResult {
pub build_system: BuildSystem,
pub success: bool,
pub targets_built: Vec<String>,
pub targets_failed: Vec<String>,
pub total_files: usize,
pub compile_time_ms: u64,
pub link_time_ms: u64,
pub total_time_ms: u64,
pub warnings: Vec<BuildWarning>,
pub errors: Vec<BuildError>,
pub output_files: Vec<PathBuf>,
}
impl Default for BuildResult {
fn default() -> Self {
Self {
build_system: BuildSystem::CMake,
success: true,
targets_built: Vec::new(),
targets_failed: Vec::new(),
total_files: 0,
compile_time_ms: 0,
link_time_ms: 0,
total_time_ms: 0,
warnings: Vec::new(),
errors: Vec::new(),
output_files: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuildSystem {
CMake,
Make,
Ninja,
Meson,
Bazel,
Buck2,
SCons,
Premake5,
Xmake,
Gradle,
MSBuild,
Xcode,
Qbs,
Autotools,
}
impl BuildSystem {
pub fn name(&self) -> &'static str {
match self {
Self::CMake => "CMake",
Self::Make => "GNU Make",
Self::Ninja => "Ninja",
Self::Meson => "Meson",
Self::Bazel => "Bazel",
Self::Buck2 => "Buck2",
Self::SCons => "SCons",
Self::Premake5 => "Premake5",
Self::Xmake => "xmake",
Self::Gradle => "Gradle",
Self::MSBuild => "MSBuild",
Self::Xcode => "Xcode",
Self::Qbs => "Qbs",
Self::Autotools => "Autotools",
}
}
pub fn config_file(&self) -> &'static str {
match self {
Self::CMake => "CMakeLists.txt",
Self::Make => "Makefile",
Self::Ninja => "build.ninja",
Self::Meson => "meson.build",
Self::Bazel => "BUILD",
Self::Buck2 => "BUCK",
Self::SCons => "SConstruct",
Self::Premake5 => "premake5.lua",
Self::Xmake => "xmake.lua",
Self::Gradle => "build.gradle",
Self::MSBuild => "*.vcxproj",
Self::Xcode => "*.xcodeproj",
Self::Qbs => "*.qbs",
Self::Autotools => "configure.ac",
}
}
}
#[derive(Debug, Clone)]
pub struct BuildWarning {
pub file: String,
pub line: u32,
pub message: String,
pub warning_flag: String,
pub category: BuildWarningCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuildWarningCategory {
DeprecatedDeclaration,
UnusedVariable,
UnusedParameter,
SignCompare,
Shadow,
MissingFieldInitializer,
UnreachableCode,
Narrowing,
Other(String),
}
#[derive(Debug, Clone)]
pub struct BuildError {
pub file: String,
pub line: u32,
pub column: u32,
pub message: String,
pub error_code: String,
pub is_fatal: bool,
}
#[derive(Debug, Clone)]
pub struct CMakeContext {
pub source_dir: PathBuf,
pub build_dir: PathBuf,
pub generator: CMakeGenerator,
pub build_type: CMakeBuildType,
pub toolchain_file: Option<PathBuf>,
pub install_prefix: PathBuf,
pub defines: HashMap<String, String>,
pub cmake_args: Vec<String>,
pub parallel_jobs: u32,
pub target: Option<String>,
pub verbose: bool,
pub clean_first: bool,
}
impl Default for CMakeContext {
fn default() -> Self {
Self {
source_dir: PathBuf::from("."),
build_dir: PathBuf::from("build"),
generator: CMakeGenerator::Ninja,
build_type: CMakeBuildType::Release,
toolchain_file: None,
install_prefix: PathBuf::from("/usr/local"),
defines: HashMap::new(),
cmake_args: Vec::new(),
parallel_jobs: 8,
target: None,
verbose: false,
clean_first: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CMakeGenerator {
Ninja,
NinjaMultiConfig,
UnixMakefiles,
Xcode,
VisualStudio17,
VisualStudio16,
MSYS,
MinGW,
GreenHillsMulti,
CodeBlocks,
CodeLite,
Kate,
SublimeText2,
}
impl CMakeGenerator {
pub fn as_arg(&self) -> &'static str {
match self {
Self::Ninja => "Ninja",
Self::NinjaMultiConfig => "Ninja Multi-Config",
Self::UnixMakefiles => "Unix Makefiles",
Self::Xcode => "Xcode",
Self::VisualStudio17 => "Visual Studio 17 2022",
Self::VisualStudio16 => "Visual Studio 16 2019",
Self::MSYS => "MSYS Makefiles",
Self::MinGW => "MinGW Makefiles",
Self::GreenHillsMulti => "Green Hills MULTI",
Self::CodeBlocks => "CodeBlocks",
Self::CodeLite => "CodeLite",
Self::Kate => "Kate",
Self::SublimeText2 => "Sublime Text 2",
}
}
pub fn is_multi_config(&self) -> bool {
matches!(
self,
Self::Xcode
| Self::VisualStudio17
| Self::VisualStudio16
| Self::NinjaMultiConfig
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CMakeBuildType {
Debug,
Release,
RelWithDebInfo,
MinSizeRel,
Coverage,
ASan,
TSan,
UBSan,
Custom(String),
}
impl CMakeBuildType {
pub fn as_str(&self) -> &str {
match self {
Self::Debug => "Debug",
Self::Release => "Release",
Self::RelWithDebInfo => "RelWithDebInfo",
Self::MinSizeRel => "MinSizeRel",
Self::Coverage => "Coverage",
Self::ASan => "ASan",
Self::TSan => "TSan",
Self::UBSan => "UBSan",
Self::Custom(s) => s.as_str(),
}
}
pub fn compiler_flags(&self) -> Vec<&'static str> {
match self {
Self::Debug => vec!["-O0", "-g", "-DDEBUG"],
Self::Release => vec!["-O3", "-DNDEBUG"],
Self::RelWithDebInfo => vec!["-O2", "-g", "-DNDEBUG"],
Self::MinSizeRel => vec!["-Os", "-DNDEBUG"],
Self::Coverage => vec!["-O0", "-g", "--coverage"],
Self::ASan => vec!["-O1", "-g", "-fsanitize=address"],
Self::TSan => vec!["-O1", "-g", "-fsanitize=thread"],
Self::UBSan => vec!["-O1", "-g", "-fsanitize=undefined"],
Self::Custom(_) => Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct CMakeProject {
pub name: String,
pub version: String,
pub description: String,
pub languages: Vec<String>,
pub cmake_minimum: String,
pub targets: Vec<CMakeTarget>,
pub options: Vec<CMakeOption>,
pub dependencies: Vec<CMakeDependency>,
pub subdirectories: Vec<String>,
pub install_rules: Vec<CMakeInstallRule>,
pub tests: Vec<CMakeTest>,
pub variables: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct CMakeTarget {
pub name: String,
pub target_type: CMakeTargetType,
pub sources: Vec<String>,
pub headers: Vec<String>,
pub private_includes: Vec<String>,
pub public_includes: Vec<String>,
pub interface_includes: Vec<String>,
pub link_libraries: Vec<String>,
pub compile_definitions: Vec<String>,
pub compile_features: Vec<String>,
pub compile_options: Vec<String>,
pub link_options: Vec<String>,
pub properties: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CMakeTargetType {
Executable,
StaticLibrary,
SharedLibrary,
ModuleLibrary,
ObjectLibrary,
InterfaceLibrary,
Custom,
}
impl CMakeTargetType {
pub fn as_cmake(&self) -> &'static str {
match self {
Self::Executable => "EXECUTABLE",
Self::StaticLibrary => "STATIC",
Self::SharedLibrary => "SHARED",
Self::ModuleLibrary => "MODULE",
Self::ObjectLibrary => "OBJECT",
Self::InterfaceLibrary => "INTERFACE",
Self::Custom => "CUSTOM",
}
}
}
#[derive(Debug, Clone)]
pub struct CMakeOption {
pub name: String,
pub description: String,
pub default_value: String,
pub option_type: CMakeOptionType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CMakeOptionType {
Bool,
String,
FilePath,
Path,
}
#[derive(Debug, Clone)]
pub struct CMakeDependency {
pub name: String,
pub version: Option<String>,
pub required: bool,
pub components: Vec<String>,
pub config_mode: bool,
}
#[derive(Debug, Clone)]
pub struct CMakeInstallRule {
pub targets: Vec<String>,
pub destination: String,
pub component: Option<String>,
pub permissions: Option<String>,
pub configs: Vec<String>,
pub export_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CMakeTest {
pub name: String,
pub command: String,
pub working_directory: Option<String>,
pub configs: Vec<String>,
}
pub fn parse_cmake_file(path: &Path) -> Result<CMakeProject, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let mut project = CMakeProject {
name: String::new(),
version: String::new(),
description: String::new(),
languages: Vec::new(),
cmake_minimum: String::new(),
targets: Vec::new(),
options: Vec::new(),
dependencies: Vec::new(),
subdirectories: Vec::new(),
install_rules: Vec::new(),
tests: Vec::new(),
variables: HashMap::new(),
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("cmake_minimum_required") {
if let Some(ver) = extract_cmake_string(trimmed, "VERSION") {
project.cmake_minimum = ver;
}
} else if trimmed.starts_with("project(") {
if let Some(name) = extract_first_arg(trimmed, "project") {
project.name = name;
}
if let Some(ver) = extract_cmake_string(trimmed, "VERSION") {
project.version = ver;
}
if let Some(desc) = extract_cmake_string(trimmed, "DESCRIPTION") {
project.description = desc;
}
if let Some(langs) = extract_cmake_string(trimmed, "LANGUAGES") {
project.languages = langs
.split_whitespace()
.map(String::from)
.collect();
}
} else if trimmed.starts_with("option(") {
if let Some(name) = extract_first_arg(trimmed, "option") {
project.options.push(CMakeOption {
name,
description: String::new(),
default_value: "OFF".to_string(),
option_type: CMakeOptionType::Bool,
});
}
} else if trimmed.starts_with("add_executable(") {
project.targets.push(CMakeTarget {
name: extract_first_arg(trimmed, "add_executable").unwrap_or_default(),
target_type: CMakeTargetType::Executable,
sources: Vec::new(),
headers: Vec::new(),
private_includes: Vec::new(),
public_includes: Vec::new(),
interface_includes: Vec::new(),
link_libraries: Vec::new(),
compile_definitions: Vec::new(),
compile_features: Vec::new(),
compile_options: Vec::new(),
link_options: Vec::new(),
properties: HashMap::new(),
});
} else if trimmed.starts_with("add_library(") {
project.targets.push(CMakeTarget {
name: extract_first_arg(trimmed, "add_library").unwrap_or_default(),
target_type: CMakeTargetType::StaticLibrary,
sources: Vec::new(),
headers: Vec::new(),
private_includes: Vec::new(),
public_includes: Vec::new(),
interface_includes: Vec::new(),
link_libraries: Vec::new(),
compile_definitions: Vec::new(),
compile_features: Vec::new(),
compile_options: Vec::new(),
link_options: Vec::new(),
properties: HashMap::new(),
});
} else if trimmed.starts_with("find_package(") {
let name = extract_first_arg(trimmed, "find_package").unwrap_or_default();
project.dependencies.push(CMakeDependency {
name,
version: None,
required: trimmed.contains("REQUIRED"),
components: Vec::new(),
config_mode: trimmed.contains("CONFIG"),
});
} else if trimmed.starts_with("add_subdirectory(") {
if let Some(subdir) = extract_first_arg(trimmed, "add_subdirectory") {
project.subdirectories.push(subdir);
}
} else if trimmed.starts_with("add_test(") {
project.tests.push(CMakeTest {
name: extract_cmake_string(trimmed, "NAME").unwrap_or_default(),
command: extract_cmake_string(trimmed, "COMMAND").unwrap_or_default(),
working_directory: None,
configs: Vec::new(),
});
}
}
Ok(project)
}
fn extract_first_arg(line: &str, function: &str) -> Option<String> {
let prefix = format!("{}(", function);
if let Some(rest) = line.strip_prefix(&prefix) {
let first = rest.trim_start();
if first.starts_with('"') {
if let Some(end) = first[1..].find('"') {
return Some(first[1..=end].to_string());
}
}
first
.split(|c: char| c.is_whitespace() || c == ')')
.next()
.map(String::from)
} else {
None
}
}
fn extract_cmake_string(line: &str, keyword: &str) -> Option<String> {
let pattern = format!("{} ", keyword);
if let Some(pos) = line.find(&pattern) {
let rest = &line[pos + pattern.len()..];
if let Some(start) = rest.find('"') {
let quoted = &rest[start + 1..];
if let Some(end) = quoted.find('"') {
return Some(quoted[..end].to_string());
}
}
rest.split(|c: char| c.is_whitespace() || c == ')')
.next()
.map(String::from)
} else {
None
}
}
pub fn cmake_configure(ctx: &CMakeContext) -> Result<BuildResult, String> {
let mut cmd = ProcessCommand::new("cmake");
cmd.arg("-S").arg(&ctx.source_dir);
cmd.arg("-B").arg(&ctx.build_dir);
cmd.arg("-G").arg(ctx.generator.as_arg());
cmd.arg(format!("-DCMAKE_BUILD_TYPE={}", ctx.build_type.as_str()));
cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", ctx.install_prefix.display()));
if let Some(ref toolchain) = ctx.toolchain_file {
cmd.arg(format!("-DCMAKE_TOOLCHAIN_FILE={}", toolchain.display()));
}
for (key, value) in &ctx.defines {
cmd.arg(format!("-D{}={}", key, value));
}
for arg in &ctx.cmake_args {
cmd.arg(arg);
}
let output = cmd.output().map_err(|e| format!("cmake configure failed: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("cmake configure failed:\n{}", stderr));
}
Ok(BuildResult {
build_system: BuildSystem::CMake,
success: true,
..Default::default()
})
}
pub fn cmake_build(ctx: &CMakeContext) -> Result<BuildResult, String> {
let mut cmd = ProcessCommand::new("cmake");
cmd.arg("--build").arg(&ctx.build_dir);
if let Some(ref target) = ctx.target {
cmd.arg("--target").arg(target);
}
cmd.arg("--config").arg(ctx.build_type.as_str());
cmd.arg(format!("-j{}", ctx.parallel_jobs));
if ctx.verbose {
cmd.arg("--verbose");
}
if ctx.clean_first {
cmd.arg("--clean-first");
}
let output = cmd.output().map_err(|e| format!("cmake build failed: {}", e))?;
Ok(BuildResult {
build_system: BuildSystem::CMake,
success: output.status.success(),
..Default::default()
})
}
pub fn cmake_configure_and_build(ctx: &CMakeContext) -> Result<BuildResult, String> {
cmake_configure(ctx)?;
cmake_build(ctx)
}
#[derive(Debug, Clone)]
pub struct NinjaContext {
pub build_dir: PathBuf,
pub targets: Vec<String>,
pub parallel_jobs: u32,
pub verbose: bool,
pub dry_run: bool,
pub keep_going: bool,
}
impl Default for NinjaContext {
fn default() -> Self {
Self {
build_dir: PathBuf::from("build"),
targets: Vec::new(),
parallel_jobs: 8,
verbose: false,
dry_run: false,
keep_going: false,
}
}
}
#[derive(Debug, Clone)]
pub struct NinjaBuildFile {
pub rules: HashMap<String, NinjaRule>,
pub build_edges: Vec<NinjaBuildEdge>,
pub variables: HashMap<String, String>,
pub default_targets: Vec<String>,
pub pools: HashMap<String, NinjaPool>,
}
#[derive(Debug, Clone)]
pub struct NinjaRule {
pub name: String,
pub command: String,
pub description: Option<String>,
pub depfile: Option<String>,
pub deps: Option<String>,
pub generator: bool,
pub pool: Option<String>,
pub restat: bool,
pub rspfile: Option<String>,
pub rspfile_content: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NinjaBuildEdge {
pub outputs: Vec<String>,
pub rule: String,
pub inputs: Vec<String>,
pub implicit_inputs: Vec<String>,
pub order_only_inputs: Vec<String>,
pub variables: HashMap<String, String>,
pub implicit_outputs: Vec<String>,
pub validations: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct NinjaPool {
pub name: String,
pub depth: u32,
}
pub fn parse_ninja_file(path: &Path) -> Result<NinjaBuildFile, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let mut ninja = NinjaBuildFile {
rules: HashMap::new(),
build_edges: Vec::new(),
variables: HashMap::new(),
default_targets: Vec::new(),
pools: HashMap::new(),
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with("rule ") {
let name = trimmed.strip_prefix("rule ").unwrap_or("").trim().to_string();
ninja.rules.insert(
name.clone(),
NinjaRule {
name,
command: String::new(),
description: None,
depfile: None,
deps: None,
generator: false,
pool: None,
restat: false,
rspfile: None,
rspfile_content: None,
},
);
} else if trimmed.starts_with("build ") {
let outputs_part = trimmed.strip_prefix("build ").unwrap_or("");
let parts: Vec<&str> = outputs_part.splitn(2, ':').collect();
let outputs: Vec<String> = if parts.len() > 1 {
parts[0].split_whitespace().map(String::from).collect()
} else {
Vec::new()
};
let rule_part = if parts.len() > 1 { parts[1] } else { "" };
let rule_name = rule_part.split_whitespace().next().unwrap_or("");
ninja.build_edges.push(NinjaBuildEdge {
outputs,
rule: rule_name.to_string(),
inputs: Vec::new(),
implicit_inputs: Vec::new(),
order_only_inputs: Vec::new(),
variables: HashMap::new(),
implicit_outputs: Vec::new(),
validations: Vec::new(),
});
} else if trimmed.starts_with("default ") {
let defaults = trimmed.strip_prefix("default ").unwrap_or("");
ninja.default_targets = defaults.split_whitespace().map(String::from).collect();
} else if let Some(eq) = trimmed.find('=') {
let key = trimmed[..eq].trim().to_string();
let value = trimmed[eq + 1..].trim().to_string();
ninja.variables.insert(key, value);
}
}
Ok(ninja)
}
pub fn ninja_build(ctx: &NinjaContext) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::Ninja,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("ninja");
cmd.arg("-C").arg(&ctx.build_dir);
if ctx.parallel_jobs > 0 {
cmd.arg(format!("-j{}", ctx.parallel_jobs));
}
if ctx.verbose {
cmd.arg("-v");
}
if ctx.dry_run {
cmd.arg("-n");
}
if ctx.keep_going {
cmd.arg("-k").arg("0");
}
for target in &ctx.targets {
cmd.arg(target);
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
}
result
}
pub fn generate_ninja_file(ninja: &NinjaBuildFile) -> String {
let mut content = String::new();
content.push_str("# Generated by clang_build_systems\n\n");
for (key, value) in &ninja.variables {
content.push_str(&format!("{} = {}\n", key, value));
}
content.push('\n');
for rule in ninja.rules.values() {
content.push_str(&format!("rule {}\n", rule.name));
content.push_str(&format!(" command = {}\n", rule.command));
if let Some(ref desc) = rule.description {
content.push_str(&format!(" description = {}\n", desc));
}
if let Some(ref depfile) = rule.depfile {
content.push_str(&format!(" depfile = {}\n", depfile));
}
if let Some(ref deps) = rule.deps {
content.push_str(&format!(" deps = {}\n", deps));
}
if rule.generator {
content.push_str(" generator = 1\n");
}
if let Some(ref pool) = rule.pool {
content.push_str(&format!(" pool = {}\n", pool));
}
content.push('\n');
}
for edge in &ninja.build_edges {
content.push_str(&format!(
"build {}: {}",
edge.outputs.join(" "),
edge.rule
));
if !edge.inputs.is_empty() {
content.push_str(&format!(" {}", edge.inputs.join(" ")));
}
if !edge.implicit_inputs.is_empty() {
content.push_str(&format!(" | {}", edge.implicit_inputs.join(" ")));
}
if !edge.order_only_inputs.is_empty() {
content.push_str(&format!(" || {}", edge.order_only_inputs.join(" ")));
}
if !edge.implicit_outputs.is_empty() {
content.push_str(&format!(" | {}", edge.implicit_outputs.join(" ")));
}
content.push('\n');
for (key, value) in &edge.variables {
content.push_str(&format!(" {} = {}\n", key, value));
}
content.push('\n');
}
if !ninja.default_targets.is_empty() {
content.push_str(&format!(
"default {}\n",
ninja.default_targets.join(" ")
));
}
content
}
#[derive(Debug, Clone)]
pub struct MesonContext {
pub source_dir: PathBuf,
pub build_dir: PathBuf,
pub build_type: MesonBuildType,
pub cross_file: Option<PathBuf>,
pub native_file: Option<PathBuf>,
pub options: HashMap<String, String>,
pub setup_options: Vec<String>,
pub parallel_jobs: u32,
}
impl Default for MesonContext {
fn default() -> Self {
Self {
source_dir: PathBuf::from("."),
build_dir: PathBuf::from("build"),
build_type: MesonBuildType::Release,
cross_file: None,
native_file: None,
options: HashMap::new(),
setup_options: Vec::new(),
parallel_jobs: 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MesonBuildType {
Plain,
Debug,
DebugOptimized,
Release,
Minsize,
Custom,
}
impl MesonBuildType {
pub fn as_arg(&self) -> &'static str {
match self {
Self::Plain => "plain",
Self::Debug => "debug",
Self::DebugOptimized => "debugoptimized",
Self::Release => "release",
Self::Minsize => "minsize",
Self::Custom => "custom",
}
}
pub fn optimization_flag(&self) -> &'static str {
match self {
Self::Plain => "0",
Self::Debug => "0",
Self::DebugOptimized => "2",
Self::Release => "3",
Self::Minsize => "s",
Self::Custom => "0",
}
}
pub fn debug_flag(&self) -> bool {
match self {
Self::Plain => false,
Self::Debug | Self::DebugOptimized => true,
Self::Release | Self::Minsize => false,
Self::Custom => false,
}
}
}
#[derive(Debug, Clone)]
pub struct MesonProject {
pub name: String,
pub version: String,
pub license: Option<String>,
pub meson_version: String,
pub default_options: HashMap<String, String>,
pub subprojects: Vec<String>,
pub dependencies: Vec<MesonDependency>,
pub executables: Vec<MesonExecutable>,
pub libraries: Vec<MesonLibrary>,
pub tests: Vec<MesonTest>,
}
#[derive(Debug, Clone)]
pub struct MesonDependency {
pub name: String,
pub version: Option<String>,
pub required: bool,
pub fallback: Option<String>,
pub static_linking: bool,
}
#[derive(Debug, Clone)]
pub struct MesonExecutable {
pub name: String,
pub sources: Vec<String>,
pub dependencies: Vec<String>,
pub include_directories: Vec<String>,
pub cpp_args: Vec<String>,
pub link_args: Vec<String>,
pub install: bool,
pub gui_app: bool,
pub win_subsystem: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MesonLibrary {
pub name: String,
pub sources: Vec<String>,
pub version: Option<String>,
pub soversion: Option<String>,
pub install: bool,
}
#[derive(Debug, Clone)]
pub struct MesonTest {
pub name: String,
pub executable: String,
pub args: Vec<String>,
pub is_parallel: bool,
pub timeout: u64,
pub protocol: String,
}
pub fn meson_setup_and_compile(ctx: &MesonContext) -> Result<BuildResult, String> {
let mut setup_cmd = ProcessCommand::new("meson");
setup_cmd.arg("setup");
setup_cmd.arg(&ctx.build_dir);
setup_cmd.arg(&ctx.source_dir);
setup_cmd.arg(format!("--buildtype={}", ctx.build_type.as_arg()));
if let Some(ref cross) = ctx.cross_file {
setup_cmd.arg(format!("--cross-file={}", cross.display()));
}
if let Some(ref native) = ctx.native_file {
setup_cmd.arg(format!("--native-file={}", native.display()));
}
for (key, value) in &ctx.options {
setup_cmd.arg(format!("-D{}={}", key, value));
}
for opt in &ctx.setup_options {
setup_cmd.arg(opt);
}
let setup_output = setup_cmd.output().map_err(|e| format!("meson setup failed: {}", e))?;
if !setup_output.status.success() {
return Err(String::from_utf8_lossy(&setup_output.stderr).to_string());
}
let mut compile_cmd = ProcessCommand::new("meson");
compile_cmd.arg("compile");
compile_cmd.arg("-C").arg(&ctx.build_dir);
compile_cmd.arg(format!("-j{}", ctx.parallel_jobs));
let compile_output = compile_cmd.output().map_err(|e| format!("meson compile failed: {}", e))?;
Ok(BuildResult {
build_system: BuildSystem::Meson,
success: compile_output.status.success(),
..Default::default()
})
}
#[derive(Debug, Clone)]
pub struct BazelContext {
pub workspace_dir: PathBuf,
pub target: String,
pub configuration: Option<String>,
pub cpu: Option<String>,
pub compilation_mode: BazelCompilationMode,
pub linkopt: Vec<String>,
pub copts: Vec<String>,
pub crosstool_top: Option<String>,
pub action_env: HashMap<String, String>,
pub remote_cache: Option<String>,
pub disk_cache: Option<PathBuf>,
}
impl Default for BazelContext {
fn default() -> Self {
Self {
workspace_dir: PathBuf::from("."),
target: String::new(),
configuration: None,
cpu: None,
compilation_mode: BazelCompilationMode::Opt,
linkopt: Vec::new(),
copts: Vec::new(),
crosstool_top: None,
action_env: HashMap::new(),
remote_cache: None,
disk_cache: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BazelCompilationMode {
Fastbuild,
Dbg,
Opt,
}
impl BazelCompilationMode {
pub fn as_flag(&self) -> &'static str {
match self {
Self::Fastbuild => "fastbuild",
Self::Dbg => "dbg",
Self::Opt => "opt",
}
}
}
#[derive(Debug, Clone)]
pub struct BazelBuildFile {
pub package_name: String,
pub load_statements: Vec<BazelLoadStatement>,
pub rules: Vec<BazelRule>,
pub filegroup: Option<BazelFilegroup>,
}
#[derive(Debug, Clone)]
pub struct BazelLoadStatement {
pub path: String,
pub symbols: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct BazelRule {
pub rule_type: String,
pub name: String,
pub srcs: Vec<String>,
pub hdrs: Vec<String>,
pub deps: Vec<String>,
pub copts: Vec<String>,
pub linkopts: Vec<String>,
pub defines: Vec<String>,
pub includes: Vec<String>,
pub visibility: Vec<String>,
pub tags: Vec<String>,
pub testonly: bool,
pub alwayslink: bool,
pub linkstatic: Option<bool>,
pub features: Vec<String>,
pub data: Vec<String>,
pub compatible_with: Vec<String>,
pub restricted_to: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct BazelFilegroup {
pub name: String,
pub srcs: Vec<String>,
pub visibility: Vec<String>,
}
pub fn bazel_build(ctx: &BazelContext) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::Bazel,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("bazel");
cmd.arg("build");
cmd.arg(&ctx.target);
if let Some(ref cfg) = ctx.configuration {
cmd.arg(format!("--config={}", cfg));
}
if let Some(ref cpu) = ctx.cpu {
cmd.arg(format!("--cpu={}", cpu));
}
cmd.arg(format!("--compilation_mode={}", ctx.compilation_mode.as_flag()));
for opt in &ctx.linkopt {
cmd.arg(format!("--linkopt={}", opt));
}
for opt in &ctx.copts {
cmd.arg(format!("--copt={}", opt));
}
if let Some(ref cache) = ctx.remote_cache {
cmd.arg(format!("--remote_cache={}", cache));
}
if let Some(ref cache) = ctx.disk_cache {
cmd.arg(format!("--disk_cache={}", cache.display()));
}
for (key, val) in &ctx.action_env {
cmd.arg(format!("--action_env={}={}", key, val));
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
#[derive(Debug, Clone)]
pub struct Buck2Context {
pub project_root: PathBuf,
pub target: String,
pub mode: Buck2Mode,
pub out: Option<PathBuf>,
pub config_flags: HashMap<String, String>,
pub isolation_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Buck2Mode {
Dev,
Opt,
Release,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Buck2RuleType {
CxxBinary,
CxxLibrary,
CxxTest,
CxxGenrule,
PrebuiltCxxLibrary,
}
impl Buck2RuleType {
pub fn as_str(&self) -> &'static str {
match self {
Self::CxxBinary => "cxx_binary",
Self::CxxLibrary => "cxx_library",
Self::CxxTest => "cxx_test",
Self::CxxGenrule => "cxx_genrule",
Self::PrebuiltCxxLibrary => "prebuilt_cxx_library",
}
}
}
pub fn buck2_build(ctx: &Buck2Context) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::Buck2,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("buck2");
cmd.arg("build");
cmd.arg(&ctx.target);
if let Some(ref out) = ctx.out {
cmd.arg(format!("--out={}", out.display()));
}
for (key, value) in &ctx.config_flags {
cmd.arg(format!("--config={}={}", key, value));
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
#[derive(Debug, Clone)]
pub struct SConsContext {
pub project_dir: PathBuf,
pub sconstruct_file: Option<PathBuf>,
pub targets: Vec<String>,
pub parallel_jobs: u32,
pub debug: bool,
pub variables: HashMap<String, String>,
pub site_dir: Option<PathBuf>,
}
impl Default for SConsContext {
fn default() -> Self {
Self {
project_dir: PathBuf::from("."),
sconstruct_file: None,
targets: Vec::new(),
parallel_jobs: 4,
debug: false,
variables: HashMap::new(),
site_dir: None,
}
}
}
#[derive(Debug, Clone)]
pub struct SConsProject {
pub environment_vars: HashMap<String, String>,
pub source_files: Vec<String>,
pub programs: Vec<SConsProgram>,
pub libraries: Vec<SConsLibrary>,
pub parse_flags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SConsProgram {
pub target: String,
pub source: Vec<String>,
pub libs: Vec<String>,
pub libpath: Vec<String>,
pub cpppath: Vec<String>,
pub cppdefines: Vec<String>,
pub cxxflags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SConsLibrary {
pub target: String,
pub source: Vec<String>,
pub shared: bool,
}
pub fn scons_build(ctx: &SConsContext) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::SCons,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("scons");
if let Some(ref sconstruct) = ctx.sconstruct_file {
cmd.arg(format!("-f={}", sconstruct.display()));
}
cmd.arg(format!("-j{}", ctx.parallel_jobs));
if ctx.debug {
cmd.arg("--debug=explain");
}
for (key, value) in &ctx.variables {
cmd.arg(format!("{}={}", key, value));
}
for target in &ctx.targets {
cmd.arg(target);
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
#[derive(Debug, Clone)]
pub struct Premake5Context {
pub project_dir: PathBuf,
pub action: String,
pub file: Option<PathBuf>,
pub os: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Premake5Project {
pub workspace_name: String,
pub projects: Vec<Premake5ProjectEntry>,
pub configurations: Vec<String>,
pub platforms: Vec<String>,
pub flags: Vec<String>,
pub defines: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Premake5ProjectEntry {
pub name: String,
pub kind: String,
pub language: String,
pub files: Vec<String>,
pub includedirs: Vec<String>,
pub libdirs: Vec<String>,
pub links: Vec<String>,
pub defines: Vec<String>,
pub flags: Vec<String>,
pub dependson: Vec<String>,
pub targetdir: Option<String>,
pub objdir: Option<String>,
pub pchheader: Option<String>,
pub pchsource: Option<String>,
}
pub fn premake5_generate(ctx: &Premake5Context) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::Premake5,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("premake5");
cmd.arg(&ctx.action);
if let Some(ref file) = ctx.file {
cmd.arg(format!("--file={}", file.display()));
}
if let Some(ref os) = ctx.os {
cmd.arg(format!("--os={}", os));
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
#[derive(Debug, Clone)]
pub struct XmakeContext {
pub project_dir: PathBuf,
pub mode: String,
pub plat: Option<String>,
pub arch: Option<String>,
pub buildir: Option<PathBuf>,
pub target: Option<String>,
pub jobs: u32,
pub verbose: bool,
}
#[derive(Debug, Clone)]
pub struct XmakeProject {
pub name: String,
pub version: String,
pub description: Option<String>,
pub targets: Vec<XmakeTarget>,
pub packages: Vec<XmakePackage>,
pub languages: Vec<String>,
pub set_rules: Vec<String>,
pub includes: Vec<XmakeInclude>,
}
#[derive(Debug, Clone)]
pub struct XmakeTarget {
pub name: String,
pub kind: String,
pub files: Vec<String>,
pub add_deps: Vec<String>,
pub add_packages: Vec<String>,
pub add_defines: Vec<String>,
pub add_includedirs: Vec<String>,
pub add_linkdirs: Vec<String>,
pub add_links: Vec<String>,
pub add_cxflags: Vec<String>,
pub add_ldflags: Vec<String>,
pub set_languages: Vec<String>,
pub set_kind: Option<String>,
}
#[derive(Debug, Clone)]
pub struct XmakePackage {
pub name: String,
pub version: Option<String>,
pub description: Option<String>,
pub links: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct XmakeInclude {
pub path: String,
}
pub fn xmake_build(ctx: &XmakeContext) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::Xmake,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("xmake");
cmd.arg("build");
if let Some(ref target) = ctx.target {
cmd.arg(target);
}
cmd.arg(format!("-m={}", ctx.mode));
cmd.arg(format!("-j{}", ctx.jobs));
if let Some(ref plat) = ctx.plat {
cmd.arg(format!("-p={}", plat));
}
if let Some(ref arch) = ctx.arch {
cmd.arg(format!("-a={}", arch));
}
if ctx.verbose {
cmd.arg("-v");
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
#[derive(Debug, Clone)]
pub struct GradleNDKContext {
pub project_dir: PathBuf,
pub build_type: String,
pub flavor: Option<String>,
pub ndk_version: String,
pub cmake_version: String,
pub abi_filters: Vec<String>,
pub arguments: Vec<String>,
pub native_build_system: GradleNativeBuildSystem,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GradleNativeBuildSystem {
CMake,
NdkBuild,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AndroidABI {
ArmeabiV7a,
Arm64V8a,
X86,
X8664,
}
impl AndroidABI {
pub fn as_str(&self) -> &'static str {
match self {
Self::ArmeabiV7a => "armeabi-v7a",
Self::Arm64V8a => "arm64-v8a",
Self::X86 => "x86",
Self::X8664 => "x86_64",
}
}
pub fn clang_target(&self) -> &'static str {
match self {
Self::ArmeabiV7a => "armv7a-linux-androideabi",
Self::Arm64V8a => "aarch64-linux-android",
Self::X86 => "i686-linux-android",
Self::X8664 => "x86_64-linux-android",
}
}
pub fn api_level(&self) -> u32 {
match self {
Self::ArmeabiV7a => 21,
Self::Arm64V8a => 21,
Self::X86 => 21,
Self::X8664 => 21,
}
}
}
pub fn gradle_ndk_cmake_config(
ctx: &GradleNDKContext,
) -> String {
let mut gradle = String::new();
gradle.push_str("android {\n");
gradle.push_str(" defaultConfig {\n");
gradle.push_str(" externalNativeBuild {\n");
gradle.push_str(" cmake {\n");
if !ctx.arguments.is_empty() {
gradle.push_str(" arguments ");
for arg in &ctx.arguments {
gradle.push_str(&format!("\"{}\", ", arg));
}
gradle.push('\n');
}
for abi in &ctx.abi_filters {
gradle.push_str(&format!(" abiFilters \"{}\"\n", abi));
}
gradle.push_str(" }\n");
gradle.push_str(" }\n");
gradle.push_str(" }\n");
gradle.push_str(" externalNativeBuild {\n");
gradle.push_str(" cmake {\n");
gradle.push_str(" path \"src/main/cpp/CMakeLists.txt\"\n");
gradle.push_str(&format!(" version \"{}\"\n", ctx.cmake_version));
gradle.push_str(" }\n");
gradle.push_str(" }\n");
gradle.push_str(&format!(" ndkVersion \"{}\"\n", ctx.ndk_version));
gradle.push_str("}\n");
gradle
}
pub fn gradle_build(ctx: &GradleNDKContext) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::Gradle,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("./gradlew");
if let Some(ref flavor) = ctx.flavor {
cmd.arg(format!("assemble{}{}", flavor, ctx.build_type));
} else {
cmd.arg(format!("assemble{}", ctx.build_type));
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
#[derive(Debug, Clone)]
pub struct MSBuildContext {
pub project_file: PathBuf,
pub configuration: String,
pub platform: String,
pub targets: Vec<String>,
pub properties: HashMap<String, String>,
pub max_cpu_count: u32,
pub verbosity: MSBuildVerbosity,
pub restore_first: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MSBuildVerbosity {
Quiet,
Minimal,
Normal,
Detailed,
Diagnostic,
}
impl MSBuildVerbosity {
pub fn as_arg(&self) -> &'static str {
match self {
Self::Quiet => "q",
Self::Minimal => "m",
Self::Normal => "n",
Self::Detailed => "d",
Self::Diagnostic => "diag",
}
}
}
#[derive(Debug, Clone)]
pub struct VCXProject {
pub name: String,
pub configuration_type: VCXConfigurationType,
pub configurations: Vec<VCXConfiguration>,
pub cl_compile_items: Vec<VCXCompileItem>,
pub cl_include_items: Vec<String>,
pub link_items: Vec<VCXLinkItem>,
pub project_references: Vec<VCXProjectReference>,
pub import_groups: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VCXConfigurationType {
Application,
DynamicLibrary,
StaticLibrary,
Utility,
Makefile,
}
impl VCXConfigurationType {
pub fn as_xml(&self) -> &'static str {
match self {
Self::Application => "Application",
Self::DynamicLibrary => "DynamicLibrary",
Self::StaticLibrary => "StaticLibrary",
Self::Utility => "Utility",
Self::Makefile => "Makefile",
}
}
}
#[derive(Debug, Clone)]
pub struct VCXConfiguration {
pub name: String,
pub platform: String,
pub configuration_type: VCXConfigurationType,
pub use_debug_libraries: bool,
pub whole_program_optimization: bool,
pub character_set: String,
pub platform_toolset: String,
pub windows_target_platform_version: Option<String>,
}
#[derive(Debug, Clone)]
pub struct VCXCompileItem {
pub include: String,
pub precompiled_header: Option<String>,
pub additional_include_directories: Vec<String>,
pub preprocessor_definitions: Vec<String>,
pub warning_level: String,
pub optimization: String,
pub runtime_library: String,
pub language_standard: Option<String>,
pub exception_handling: Option<String>,
pub compile_as: Option<String>,
}
#[derive(Debug, Clone)]
pub struct VCXLinkItem {
pub additional_dependencies: Vec<String>,
pub additional_library_directories: Vec<String>,
pub subsystem: String,
pub generate_debug_information: bool,
pub enable_comdat_folding: bool,
pub optimize_references: bool,
pub incremental: bool,
pub large_address_aware: bool,
}
#[derive(Debug, Clone)]
pub struct VCXProjectReference {
pub project_path: String,
pub copy_local: bool,
}
pub fn msbuild_build(ctx: &MSBuildContext) -> BuildResult {
let mut result = BuildResult {
build_system: BuildSystem::MSBuild,
success: true,
..Default::default()
};
let mut cmd = ProcessCommand::new("msbuild");
cmd.arg(&ctx.project_file);
cmd.arg(format!("/p:Configuration={}", ctx.configuration));
cmd.arg(format!("/p:Platform={}", ctx.platform));
if ctx.max_cpu_count > 0 {
cmd.arg(format!("/m:{}", ctx.max_cpu_count));
}
cmd.arg(format!("/v:{}", ctx.verbosity.as_arg()));
for (key, value) in &ctx.properties {
cmd.arg(format!("/p:{}={}", key, value));
}
if ctx.restore_first {
cmd.arg("/restore");
}
for target in &ctx.targets {
cmd.arg(format!("/t:{}", target));
}
if let Ok(output) = cmd.output() {
result.success = output.status.success();
} else {
result.success = false;
}
result
}
pub fn cmake_generate_vcxproj(ctx: &CMakeContext) -> Result<PathBuf, String> {
let mut cmd = ProcessCommand::new("cmake");
cmd.arg("-S").arg(&ctx.source_dir);
cmd.arg("-B").arg(&ctx.build_dir);
cmd.arg("-G").arg(CMakeGenerator::VisualStudio17.as_arg());
let output = cmd.output().map_err(|e| format!("cmake failed: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
Ok(ctx.build_dir.clone())
}
#[derive(Debug, Clone)]
pub struct XcodeContext {
pub cmake_ctx: CMakeContext,
pub scheme: Option<String>,
pub destination: Option<String>,
pub xcodebuild_args: Vec<String>,
}
impl Default for XcodeContext {
fn default() -> Self {
Self {
cmake_ctx: CMakeContext {
generator: CMakeGenerator::Xcode,
..Default::default()
},
scheme: None,
destination: None,
xcodebuild_args: Vec::new(),
}
}
}
pub fn xcode_generate_and_build(ctx: &XcodeContext) -> Result<BuildResult, String> {
cmake_configure(&ctx.cmake_ctx)?;
let mut cmd = ProcessCommand::new("xcodebuild");
cmd.arg("-project");
let xcodeproj = find_xcodeproj(&ctx.cmake_ctx.build_dir)?;
cmd.arg(&xcodeproj);
if let Some(ref scheme) = ctx.scheme {
cmd.arg("-scheme").arg(scheme);
}
if let Some(ref dest) = ctx.destination {
cmd.arg("-destination").arg(dest);
}
cmd.arg("build");
for arg in &ctx.xcodebuild_args {
cmd.arg(arg);
}
let output = cmd.output().map_err(|e| format!("xcodebuild failed: {}", e))?;
Ok(BuildResult {
build_system: BuildSystem::Xcode,
success: output.status.success(),
..Default::default()
})
}
fn find_xcodeproj(build_dir: &Path) -> Result<PathBuf, String> {
if let Ok(entries) = std::fs::read_dir(build_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map_or(false, |e| e == "xcodeproj") {
return Ok(path);
}
}
}
Err("No .xcodeproj found in build directory".to_string())
}
#[derive(Debug, Clone)]
pub struct XcodeBuildSettings {
pub sdkroot: Option<String>,
pub sdk_name: Option<String>,
pub target: Option<String>,
pub archs: Vec<String>,
pub valid_archs: Vec<String>,
pub deployment_target: Option<String>,
pub clang_cxx_language_standard: Option<String>,
pub clang_cxx_library: Option<String>,
pub other_cflags: Vec<String>,
pub other_cxxflags: Vec<String>,
pub other_ldflags: Vec<String>,
pub header_search_paths: Vec<String>,
pub library_search_paths: Vec<String>,
pub framework_search_paths: Vec<String>,
pub preprocessor_definitions: Vec<String>,
pub enable_bitcode: Option<bool>,
pub code_sign_identity: Option<String>,
pub code_sign_entitlements: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_system_names() {
assert_eq!(BuildSystem::CMake.name(), "CMake");
assert_eq!(BuildSystem::Ninja.name(), "Ninja");
assert_eq!(BuildSystem::Meson.name(), "Meson");
assert_eq!(BuildSystem::Bazel.name(), "Bazel");
}
#[test]
fn test_build_system_config_files() {
assert_eq!(BuildSystem::CMake.config_file(), "CMakeLists.txt");
assert_eq!(BuildSystem::Meson.config_file(), "meson.build");
assert_eq!(BuildSystem::Make.config_file(), "Makefile");
}
#[test]
fn test_cmake_generators() {
assert_eq!(CMakeGenerator::Ninja.as_arg(), "Ninja");
assert_eq!(CMakeGenerator::UnixMakefiles.as_arg(), "Unix Makefiles");
assert!(CMakeGenerator::Xcode.is_multi_config());
assert!(!CMakeGenerator::Ninja.is_multi_config());
}
#[test]
fn test_cmake_build_types() {
assert_eq!(CMakeBuildType::Debug.as_str(), "Debug");
assert_eq!(CMakeBuildType::Release.as_str(), "Release");
assert_eq!(
CMakeBuildType::Custom("Profile".to_string()).as_str(),
"Profile"
);
}
#[test]
fn test_cmake_parse_project() {
let cmake_content = r#"
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
add_executable(myapp main.cpp)
add_library(mylib STATIC lib.cpp)
find_package(OpenSSL REQUIRED)
add_subdirectory(tests)
"#;
let tmp = std::env::temp_dir().join("test_CMakeLists.txt");
std::fs::write(&tmp, cmake_content).unwrap();
let project = parse_cmake_file(&tmp).unwrap();
assert_eq!(project.name, "MyProject");
assert_eq!(project.version, "1.0.0");
assert_eq!(project.languages, vec!["CXX"]);
assert_eq!(project.cmake_minimum, "3.20");
assert_eq!(project.targets.len(), 2);
assert_eq!(project.dependencies.len(), 1);
assert_eq!(project.subdirectories.len(), 1);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_cmake_target_types() {
assert_eq!(CMakeTargetType::Executable.as_cmake(), "EXECUTABLE");
assert_eq!(CMakeTargetType::StaticLibrary.as_cmake(), "STATIC");
assert_eq!(CMakeTargetType::SharedLibrary.as_cmake(), "SHARED");
}
#[test]
fn test_parse_ninja_file() {
let ninja_content = r#"
ninja_required_version = 1.10
rule C_COMPILER
command = clang $FLAGS -c $in -o $out
description = Compiling C $in
rule CXX_COMPILER
command = clang++ $FLAGS -c $in -o $out
description = Compiling C++ $in
build main.o: CXX_COMPILER main.cpp
FLAGS = -std=c++17 -O2
build myapp: LINK main.o
command = clang++ $in -o $out
default myapp
"#;
let tmp = std::env::temp_dir().join("test_build.ninja");
std::fs::write(&tmp, ninja_content).unwrap();
let ninja = parse_ninja_file(&tmp).unwrap();
assert_eq!(ninja.rules.len(), 2);
assert_eq!(ninja.build_edges.len(), 2);
assert_eq!(ninja.default_targets, vec!["myapp"]);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_generate_ninja_file() {
let mut ninja = NinjaBuildFile {
rules: HashMap::new(),
build_edges: Vec::new(),
variables: HashMap::new(),
default_targets: vec!["app".to_string()],
pools: HashMap::new(),
};
ninja.rules.insert(
"cxx".to_string(),
NinjaRule {
name: "cxx".to_string(),
command: "clang++ -c $in -o $out".to_string(),
description: Some("CXX $out".to_string()),
depfile: None,
deps: None,
generator: false,
pool: None,
restat: false,
rspfile: None,
rspfile_content: None,
},
);
ninja.build_edges.push(NinjaBuildEdge {
outputs: vec!["main.o".to_string()],
rule: "cxx".to_string(),
inputs: vec!["main.cpp".to_string()],
implicit_inputs: Vec::new(),
order_only_inputs: Vec::new(),
variables: HashMap::new(),
implicit_outputs: Vec::new(),
validations: Vec::new(),
});
let generated = generate_ninja_file(&ninja);
assert!(generated.contains("rule cxx"));
assert!(generated.contains("build main.o: cxx"));
assert!(generated.contains("default app"));
}
#[test]
fn test_meson_build_types() {
assert_eq!(MesonBuildType::Debug.as_arg(), "debug");
assert_eq!(MesonBuildType::Release.as_arg(), "release");
assert_eq!(MesonBuildType::DebugOptimized.optimization_flag(), "2");
assert!(MesonBuildType::Debug.debug_flag());
assert!(!MesonBuildType::Release.debug_flag());
}
#[test]
fn test_bazel_compilation_modes() {
assert_eq!(BazelCompilationMode::Opt.as_flag(), "opt");
assert_eq!(BazelCompilationMode::Dbg.as_flag(), "dbg");
}
#[test]
fn test_buck2_rule_types() {
assert_eq!(Buck2RuleType::CxxBinary.as_str(), "cxx_binary");
assert_eq!(Buck2RuleType::CxxLibrary.as_str(), "cxx_library");
}
#[test]
fn test_android_abi() {
assert_eq!(AndroidABI::Arm64V8a.as_str(), "arm64-v8a");
assert_eq!(
AndroidABI::Arm64V8a.clang_target(),
"aarch64-linux-android"
);
assert!(AndroidABI::Arm64V8a.api_level() >= 21);
}
#[test]
fn test_gradle_ndk_cmake_config() {
let ctx = GradleNDKContext {
project_dir: PathBuf::from("."),
build_type: "Release".to_string(),
flavor: None,
ndk_version: "25.2.9519653".to_string(),
cmake_version: "3.22.1".to_string(),
abi_filters: vec!["arm64-v8a".to_string(), "x86_64".to_string()],
arguments: vec!["-DANDROID_STL=c++_shared".to_string()],
native_build_system: GradleNativeBuildSystem::CMake,
};
let gradle = gradle_ndk_cmake_config(&ctx);
assert!(gradle.contains("externalNativeBuild"));
assert!(gradle.contains("cmake"));
assert!(gradle.contains("arm64-v8a"));
assert!(gradle.contains("ndkVersion"));
}
#[test]
fn test_msbuild_verbosity() {
assert_eq!(MSBuildVerbosity::Quiet.as_arg(), "q");
assert_eq!(MSBuildVerbosity::Diagnostic.as_arg(), "diag");
}
#[test]
fn test_vcx_configuration_type() {
assert_eq!(VCXConfigurationType::Application.as_xml(), "Application");
assert_eq!(VCXConfigurationType::StaticLibrary.as_xml(), "StaticLibrary");
}
#[test]
fn test_xcode_build_context_default() {
let ctx = XcodeContext::default();
assert_eq!(
ctx.cmake_ctx.generator,
CMakeGenerator::Xcode
);
}
#[test]
fn test_cmake_context_default() {
let ctx = CMakeContext::default();
assert_eq!(ctx.parallel_jobs, 8);
assert_eq!(ctx.build_type, CMakeBuildType::Release);
assert_eq!(ctx.generator, CMakeGenerator::Ninja);
}
#[test]
fn test_ninja_context_default() {
let ctx = NinjaContext::default();
assert_eq!(ctx.parallel_jobs, 8);
assert!(!ctx.verbose);
}
#[test]
fn test_meson_context_default() {
let ctx = MesonContext::default();
assert_eq!(ctx.parallel_jobs, 8);
assert_eq!(ctx.build_type, MesonBuildType::Release);
}
#[test]
fn test_bazel_context_default() {
let ctx = BazelContext::default();
assert_eq!(ctx.compilation_mode, BazelCompilationMode::Opt);
}
#[test]
fn test_buck2_context_default() {
let ctx = Buck2Context {
project_root: PathBuf::from("."),
target: "//myapp:myapp".to_string(),
mode: Buck2Mode::Dev,
out: None,
config_flags: HashMap::new(),
isolation_dir: None,
};
assert_eq!(ctx.mode, Buck2Mode::Dev);
}
#[test]
fn test_scons_context_default() {
let ctx = SConsContext::default();
assert_eq!(ctx.parallel_jobs, 4);
assert!(!ctx.debug);
}
}