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 PackageDependency {
pub name: String,
pub version: String,
pub description: String,
pub homepage: Option<String>,
pub license: Option<String>,
pub include_dirs: Vec<PathBuf>,
pub library_dirs: Vec<PathBuf>,
pub libraries: Vec<String>,
pub compile_flags: Vec<String>,
pub link_flags: Vec<String>,
pub defines: HashMap<String, String>,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PackageResolution {
pub packages: Vec<PackageDependency>,
pub transitive_packages: Vec<PackageDependency>,
pub missing: Vec<String>,
pub conflicts: Vec<PackageConflict>,
pub total_time_ms: u64,
pub cache_hits: usize,
}
impl Default for PackageResolution {
fn default() -> Self {
Self {
packages: Vec::new(),
transitive_packages: Vec::new(),
missing: Vec::new(),
conflicts: Vec::new(),
total_time_ms: 0,
cache_hits: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct PackageConflict {
pub name: String,
pub version_a: String,
pub version_b: String,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PackageManager {
Conan,
Vcpkg,
FetchContent,
PkgConfig,
Homebrew,
Apt,
Rpm,
Pacman,
Nix,
Conda,
Spack,
Hunter,
CpmCmake,
}
impl PackageManager {
pub fn name(&self) -> &'static str {
match self {
Self::Conan => "conan",
Self::Vcpkg => "vcpkg",
Self::FetchContent => "FetchContent",
Self::PkgConfig => "pkg-config",
Self::Homebrew => "homebrew",
Self::Apt => "apt",
Self::Rpm => "rpm",
Self::Pacman => "pacman",
Self::Nix => "nix",
Self::Conda => "conda",
Self::Spack => "spack",
Self::Hunter => "Hunter",
Self::CpmCmake => "CPM.cmake",
}
}
}
#[derive(Debug, Clone)]
pub struct ConanContext {
pub conanfile_path: PathBuf,
pub profile: String,
pub build_type: String,
pub settings: HashMap<String, String>,
pub options: HashMap<String, String>,
pub requires: Vec<ConanRequirement>,
pub generators: Vec<ConanGenerator>,
pub build_policy: ConanBuildPolicy,
pub remote: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ConanRequirement {
pub name: String,
pub version_range: String,
pub override_version: bool,
pub private: bool,
pub transitive_headers: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConanGenerator {
CMake,
CMakeDeps,
CMakeToolchain,
VisualStudio,
Xcode,
Premake,
QMake,
Meson,
SCons,
Bazel,
PkgConfig,
Json,
TextFiles,
CompileCommands,
MSBuildDeps,
MSBuildToolchain,
Markdown,
VCVars,
}
impl ConanGenerator {
pub fn command_arg(&self) -> &'static str {
match self {
Self::CMake => "cmake",
Self::CMakeDeps => "CMakeDeps",
Self::CMakeToolchain => "CMakeToolchain",
Self::VisualStudio => "visual_studio",
Self::Xcode => "xcode",
Self::Premake => "premake",
Self::QMake => "qmake",
Self::Meson => "meson",
Self::SCons => "scons",
Self::Bazel => "bazel",
Self::PkgConfig => "pkg_config",
Self::Json => "json",
Self::TextFiles => "txt",
Self::CompileCommands => "compile_commands",
Self::MSBuildDeps => "MSBuildDeps",
Self::MSBuildToolchain => "MSBuildToolchain",
Self::Markdown => "markdown",
Self::VCVars => "VCVars",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConanBuildPolicy {
Missing,
Never,
Outdated,
Cascade,
}
impl ConanBuildPolicy {
pub fn as_flag(&self) -> &'static str {
match self {
Self::Missing => "--build=missing",
Self::Never => "--build=never",
Self::Outdated => "--build=outdated",
Self::Cascade => "--build=cascade",
}
}
}
pub fn conan_install_and_compile(ctx: &ConanContext) -> PackageResolution {
let start = std::time::Instant::now();
let mut resolution = PackageResolution::default();
let requires: Vec<String> = ctx
.requires
.iter()
.map(|r| format!("{}/{}", r.name, r.version_range))
.collect();
resolution.packages.push(PackageDependency {
name: "conanfile".to_string(),
version: "1.0".to_string(),
description: format!("Conan package with {} dependencies", requires.len()),
homepage: None,
license: Some("MIT".to_string()),
include_dirs: vec![PathBuf::from(".")],
library_dirs: vec![PathBuf::from("build/lib")],
libraries: Vec::new(),
compile_flags: Vec::new(),
link_flags: Vec::new(),
defines: HashMap::new(),
dependencies: requires,
});
resolution.total_time_ms = start.elapsed().as_millis() as u64;
resolution
}
pub fn parse_conanfile(path: &Path) -> Result<Vec<ConanRequirement>, io::Error> {
let content = std::fs::read_to_string(path)?;
let mut requirements = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("self.requires(") || trimmed.contains("requires =") {
if let Some(req_str) = trimmed
.split('"')
.nth(1)
.or_else(|| trimmed.split('\'').nth(1))
{
let parts: Vec<&str> = req_str.split('/').collect();
if parts.len() >= 2 {
requirements.push(ConanRequirement {
name: parts[0].to_string(),
version_range: parts[1].to_string(),
override_version: false,
private: false,
transitive_headers: true,
});
}
}
}
}
Ok(requirements)
}
#[derive(Debug, Clone)]
pub struct VcpkgContext {
pub manifest_path: PathBuf,
pub triplet: String,
pub features: Vec<String>,
pub overlays: Vec<PathBuf>,
pub registries: Vec<VcpkgRegistry>,
pub vcpkg_root: PathBuf,
}
#[derive(Debug, Clone)]
pub struct VcpkgManifest {
pub name: String,
pub version_string: String,
pub description: Option<String>,
pub homepage: Option<String>,
pub license: Option<String>,
pub dependencies: Vec<VcpkgDependency>,
pub features: HashMap<String, VcpkgFeature>,
pub default_features: Vec<String>,
pub supports: Option<String>,
pub vcpkg_configuration: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct VcpkgDependency {
pub name: String,
pub version_gt: Option<String>,
pub features: Vec<String>,
pub platform: Option<String>,
pub default_features: Option<bool>,
pub host: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct VcpkgFeature {
pub description: String,
pub dependencies: Vec<VcpkgDependency>,
}
#[derive(Debug, Clone)]
pub struct VcpkgRegistry {
pub kind: String,
pub baseline: String,
pub repository: String,
pub packages: Vec<String>,
}
pub fn vcpkg_install_and_compile(ctx: &VcpkgContext) -> PackageResolution {
let start = std::time::Instant::now();
let mut resolution = PackageResolution::default();
let triplet = ctx.triplet.clone();
resolution.packages.push(PackageDependency {
name: "vcpkg-manifest".to_string(),
version: "1.0".to_string(),
description: format!("vcpkg manifest ({})", triplet),
homepage: None,
license: Some("MIT".to_string()),
include_dirs: vec![ctx
.vcpkg_root
.join("installed")
.join(&triplet)
.join("include")],
library_dirs: vec![ctx.vcpkg_root.join("installed").join(&triplet).join("lib")],
libraries: Vec::new(),
compile_flags: Vec::new(),
link_flags: Vec::new(),
defines: HashMap::new(),
dependencies: Vec::new(),
});
resolution.total_time_ms = start.elapsed().as_millis() as u64;
resolution
}
pub fn parse_vcpkg_manifest(path: &Path) -> Result<VcpkgManifest, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let json: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
let name = json["name"].as_str().unwrap_or("unknown").to_string();
let version_string = json["version-string"]
.as_str()
.or_else(|| json["version"].as_str())
.unwrap_or("0.0.0")
.to_string();
let mut dependencies = Vec::new();
if let Some(deps) = json["dependencies"].as_array() {
for dep in deps {
let dep_name = dep.as_str().unwrap_or("");
if !dep_name.is_empty() {
dependencies.push(VcpkgDependency {
name: dep_name.to_string(),
version_gt: None,
features: Vec::new(),
platform: None,
default_features: None,
host: None,
});
} else if let Some(obj) = dep.as_object() {
let dep_name = obj
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let features: Vec<String> = obj
.get("features")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|i| i.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let version_gt = obj
.get("version>=")
.and_then(|v| v.as_str())
.map(String::from);
let platform = obj
.get("platform")
.and_then(|v| v.as_str())
.map(String::from);
dependencies.push(VcpkgDependency {
name: dep_name.to_string(),
version_gt,
features,
platform,
default_features: obj.get("default-features").and_then(|v| v.as_bool()),
host: obj.get("host").and_then(|v| v.as_bool()),
});
}
}
}
let mut features_map = HashMap::new();
if let Some(features_json) = json["features"].as_object() {
for (feat_name, feat_val) in features_json {
let description = feat_val["description"].as_str().unwrap_or("").to_string();
let mut feat_deps = Vec::new();
if let Some(feat_deps_array) = feat_val["dependencies"].as_array() {
for dep in feat_deps_array {
if let Some(dep_name) = dep.as_str() {
feat_deps.push(VcpkgDependency {
name: dep_name.to_string(),
version_gt: None,
features: Vec::new(),
platform: None,
default_features: None,
host: None,
});
}
}
}
features_map.insert(
feat_name.clone(),
VcpkgFeature {
description,
dependencies: feat_deps,
},
);
}
}
let default_features: Vec<String> = json["default-features"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Ok(VcpkgManifest {
name,
version_string,
description: json["description"].as_str().map(String::from),
homepage: json["homepage"].as_str().map(String::from),
license: json["license"].as_str().map(String::from),
dependencies,
features: features_map,
default_features,
supports: json["supports"].as_str().map(String::from),
vcpkg_configuration: json.get("vcpkg-configuration").cloned(),
})
}
pub fn vcpkg_common_triplets() -> Vec<&'static str> {
vec![
"x64-windows",
"x86-windows",
"x64-windows-static",
"x64-uwp",
"x64-linux",
"x64-linux-dynamic",
"x64-osx",
"arm64-osx",
"arm64-windows",
"x64-android",
"arm64-android",
"arm-neon-android",
"wasm32-emscripten",
"x64-freebsd",
"x64-openbsd",
]
}
#[derive(Debug, Clone)]
pub struct FetchContentDependency {
pub name: String,
pub git_repository: Option<String>,
pub git_tag: Option<String>,
pub url: Option<String>,
pub url_hash: Option<String>,
pub source_dir: Option<PathBuf>,
pub binary_dir: Option<PathBuf>,
pub cmake_args: Vec<String>,
pub system: bool,
pub find_package_args: Vec<String>,
}
pub fn generate_fetchcontent_cmake(deps: &[FetchContentDependency]) -> String {
let mut cmake = String::new();
cmake.push_str("include(FetchContent)\n\n");
for dep in deps {
cmake.push_str(&format!("# Dependency: {}\n", dep.name));
cmake.push_str(&format!("FetchContent_Declare(\n"));
cmake.push_str(&format!(" {}\n", dep.name));
if let Some(ref repo) = dep.git_repository {
cmake.push_str(&format!(" GIT_REPOSITORY {}\n", repo));
}
if let Some(ref tag) = dep.git_tag {
cmake.push_str(&format!(" GIT_TAG {}\n", tag));
}
if let Some(ref url) = dep.url {
cmake.push_str(&format!(" URL {}\n", url));
}
if let Some(ref hash) = dep.url_hash {
cmake.push_str(&format!(" URL_HASH {}\n", hash));
}
if let Some(ref src_dir) = dep.source_dir {
cmake.push_str(&format!(" SOURCE_DIR {}\n", src_dir.display()));
}
if dep.system {
cmake.push_str(" SYSTEM\n");
}
cmake.push_str(")\n");
cmake.push_str(&format!("FetchContent_MakeAvailable({})\n\n", dep.name));
}
cmake
}
pub fn resolve_fetchcontent(deps: &[FetchContentDependency]) -> PackageResolution {
let mut resolution = PackageResolution::default();
for dep in deps {
resolution.packages.push(PackageDependency {
name: dep.name.clone(),
version: dep.git_tag.clone().unwrap_or_else(|| "latest".to_string()),
description: format!("FetchContent dependency {}", dep.name),
homepage: dep.git_repository.clone(),
license: None,
include_dirs: Vec::new(),
library_dirs: Vec::new(),
libraries: vec![dep.name.clone()],
compile_flags: Vec::new(),
link_flags: Vec::new(),
defines: HashMap::new(),
dependencies: Vec::new(),
});
}
resolution
}
#[derive(Debug, Clone)]
pub struct PkgConfigContext {
pub pc_path: Option<Vec<PathBuf>>,
pub static_linking: bool,
pub max_depth: u32,
}
impl Default for PkgConfigContext {
fn default() -> Self {
Self {
pc_path: None,
static_linking: false,
max_depth: 10,
}
}
}
#[derive(Debug, Clone)]
pub struct PcFile {
pub name: String,
pub description: String,
pub version: String,
pub requires: Vec<String>,
pub requires_private: Vec<String>,
pub conflicts: Vec<String>,
pub cflags: Vec<String>,
pub libs: Vec<String>,
pub libs_private: Vec<String>,
pub include_dirs: Vec<PathBuf>,
pub lib_dirs: Vec<PathBuf>,
pub url: Option<String>,
pub variables: HashMap<String, String>,
}
pub fn parse_pc_file(path: &Path) -> Result<PcFile, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let mut pc = PcFile {
name: String::new(),
description: String::new(),
version: String::new(),
requires: Vec::new(),
requires_private: Vec::new(),
conflicts: Vec::new(),
cflags: Vec::new(),
libs: Vec::new(),
libs_private: Vec::new(),
include_dirs: Vec::new(),
lib_dirs: Vec::new(),
url: None,
variables: HashMap::new(),
};
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(eq) = line.find('=') {
let key = line[..eq].trim().to_string();
let value = line[eq + 1..].trim().to_string();
pc.variables.insert(key, value);
continue;
}
if let Some(colon) = line.find(':') {
let key = line[..colon].trim();
let value = line[colon + 1..].trim();
match key {
"Name" => pc.name = value.to_string(),
"Description" => pc.description = value.to_string(),
"Version" => pc.version = value.to_string(),
"Requires" => pc.requires = value.split_whitespace().map(String::from).collect(),
"Requires.private" => {
pc.requires_private = value.split_whitespace().map(String::from).collect()
}
"Conflicts" => pc.conflicts = value.split_whitespace().map(String::from).collect(),
"Cflags" => pc.cflags = value.split_whitespace().map(String::from).collect(),
"Libs" => pc.libs = value.split_whitespace().map(String::from).collect(),
"Libs.private" => {
pc.libs_private = value.split_whitespace().map(String::from).collect()
}
"URL" => pc.url = Some(value.to_string()),
_ => {}
}
for flag in &pc.cflags {
if flag.starts_with("-I") {
pc.include_dirs.push(PathBuf::from(&flag[2..]));
}
}
for flag in pc.libs.iter().chain(pc.libs_private.iter()) {
if flag.starts_with("-L") {
pc.lib_dirs.push(PathBuf::from(&flag[2..]));
}
}
}
}
Ok(pc)
}
pub fn pkg_config_resolve(
package_name: &str,
ctx: &PkgConfigContext,
) -> Result<PackageDependency, String> {
let pc_paths = ctx.pc_path.clone().unwrap_or_else(|| {
vec![
PathBuf::from("/usr/lib/pkgconfig"),
PathBuf::from("/usr/share/pkgconfig"),
PathBuf::from("/usr/local/lib/pkgconfig"),
]
});
for dir in &pc_paths {
let pc_file = dir.join(format!("{}.pc", package_name));
if pc_file.exists() {
let pc = parse_pc_file(&pc_file)?;
return Ok(PackageDependency {
name: pc.name,
version: pc.version,
description: pc.description,
homepage: pc.url,
license: None,
include_dirs: pc.include_dirs,
library_dirs: pc.lib_dirs,
libraries: pc
.libs
.iter()
.filter(|l| l.starts_with("-l"))
.map(|l| l[2..].to_string())
.collect(),
compile_flags: pc.cflags,
link_flags: pc.libs,
defines: HashMap::new(),
dependencies: pc.requires,
});
}
}
Err(format!(
"Package '{}' not found in pkg-config search path",
package_name
))
}
#[derive(Debug, Clone)]
pub struct HomebrewContext {
pub prefix: PathBuf,
pub cellar: PathBuf,
pub opt: PathBuf,
pub formulas: Vec<HomebrewFormula>,
}
impl Default for HomebrewContext {
fn default() -> Self {
Self {
prefix: PathBuf::from("/usr/local"),
cellar: PathBuf::from("/usr/local/Cellar"),
opt: PathBuf::from("/usr/local/opt"),
formulas: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct HomebrewFormula {
pub name: String,
pub full_name: String,
pub version: String,
pub description: String,
pub homepage: String,
pub license: String,
pub keg_only: bool,
pub dependencies: Vec<String>,
pub build_dependencies: Vec<String>,
pub install_dir: PathBuf,
pub include_dir: PathBuf,
pub lib_dir: PathBuf,
pub bin_dir: PathBuf,
}
pub fn brew_prefix() -> Result<PathBuf, String> {
let candidates = vec![
PathBuf::from("/opt/homebrew"),
PathBuf::from("/usr/local"),
PathBuf::from("/home/linuxbrew/.linuxbrew"),
];
for candidate in &candidates {
if candidate.join("bin/brew").exists() {
return Ok(candidate.clone());
}
}
Err("Homebrew not found".to_string())
}
pub fn brew_resolve(
formula_name: &str,
ctx: &HomebrewContext,
) -> Result<PackageDependency, String> {
let formula = ctx
.formulas
.iter()
.find(|f| f.name == formula_name || f.full_name == formula_name)
.ok_or_else(|| format!("Formula '{}' not found", formula_name))?;
Ok(PackageDependency {
name: formula.name.clone(),
version: formula.version.clone(),
description: formula.description.clone(),
homepage: Some(formula.homepage.clone()),
license: Some(formula.license.clone()),
include_dirs: vec![formula.include_dir.clone()],
library_dirs: vec![formula.lib_dir.clone()],
libraries: Vec::new(),
compile_flags: vec![format!("-I{}", formula.include_dir.display())],
link_flags: vec![format!("-L{}", formula.lib_dir.display())],
defines: HashMap::new(),
dependencies: formula.dependencies.clone(),
})
}
pub fn brew_formula_list() -> Vec<&'static str> {
vec![
"boost",
"openssl",
"cmake",
"llvm",
"gcc",
"pkg-config",
"zlib",
"libpng",
"libjpeg",
"freetype",
"sdl2",
"sfml",
"glfw",
"glew",
"glm",
"libxml2",
"libxslt",
"curl",
"protobuf",
"grpc",
"eigen",
"openblas",
"tbb",
"openmpi",
"hdf5",
"fftw",
"gsl",
"nlopt",
"ceres-solver",
"glog",
"gflags",
"abseil",
"rapidjson",
"nlohmann-json",
"spdlog",
]
}
#[derive(Debug, Clone)]
pub struct AptContext {
pub dpkg_root: PathBuf,
pub apt_cache_dir: PathBuf,
pub architectures: Vec<String>,
pub sources_list: Vec<String>,
}
impl Default for AptContext {
fn default() -> Self {
Self {
dpkg_root: PathBuf::from("/"),
apt_cache_dir: PathBuf::from("/var/cache/apt"),
architectures: vec!["amd64".to_string(), "i386".to_string()],
sources_list: vec!["/etc/apt/sources.list".to_string()],
}
}
}
pub fn apt_resolve(package_name: &str, ctx: &AptContext) -> Result<PackageDependency, String> {
let dev_package = if package_name.starts_with("lib") {
format!("{}-dev", package_name)
} else {
format!("lib{}-dev", package_name)
};
let include_dir = ctx.dpkg_root.join("usr/include");
let lib_dir = ctx.dpkg_root.join("usr/lib/x86_64-linux-gnu");
Ok(PackageDependency {
name: package_name.to_string(),
version: "unknown".to_string(),
description: format!("APT package: {}", dev_package),
homepage: None,
license: None,
include_dirs: vec![include_dir],
library_dirs: vec![lib_dir],
libraries: vec![package_name.to_string()],
compile_flags: Vec::new(),
link_flags: vec![format!("-l{}", package_name)],
defines: HashMap::new(),
dependencies: Vec::new(),
})
}
pub fn debian_common_dev_packages() -> Vec<&'static str> {
vec![
"build-essential",
"g++",
"gcc",
"clang",
"llvm-dev",
"libc6-dev",
"libstdc++-dev",
"libssl-dev",
"zlib1g-dev",
"libpng-dev",
"libjpeg-dev",
"libfreetype6-dev",
"libfontconfig1-dev",
"libx11-dev",
"libxcb1-dev",
"libxrandr-dev",
"libxi-dev",
"libgl1-mesa-dev",
"libglu1-mesa-dev",
"libvulkan-dev",
"libsdl2-dev",
"libboost-all-dev",
"libeigen3-dev",
"libopencv-dev",
"libcurl4-openssl-dev",
"libxml2-dev",
"libprotobuf-dev",
"libgtest-dev",
"libgmock-dev",
"libbenchmark-dev",
"catch2",
"nlohmann-json3-dev",
"libspdlog-dev",
"libfmt-dev",
"libabsl-dev",
"libgflags-dev",
"libglog-dev",
]
}
#[derive(Debug, Clone)]
pub struct RpmContext {
pub rpm_root: PathBuf,
pub rpm_db_path: PathBuf,
pub release: Option<String>,
}
impl Default for RpmContext {
fn default() -> Self {
Self {
rpm_root: PathBuf::from("/"),
rpm_db_path: PathBuf::from("/var/lib/rpm"),
release: None,
}
}
}
pub fn rpm_resolve(package_name: &str, ctx: &RpmContext) -> Result<PackageDependency, String> {
let include_dir = ctx.rpm_root.join("usr/include");
let lib_dir = if cfg!(target_arch = "x86_64") {
ctx.rpm_root.join("usr/lib64")
} else {
ctx.rpm_root.join("usr/lib")
};
Ok(PackageDependency {
name: package_name.to_string(),
version: "unknown".to_string(),
description: format!("RPM package: {}", package_name),
homepage: None,
license: None,
include_dirs: vec![include_dir],
library_dirs: vec![lib_dir],
libraries: vec![package_name.to_string()],
compile_flags: Vec::new(),
link_flags: vec![format!("-l{}", package_name)],
defines: HashMap::new(),
dependencies: Vec::new(),
})
}
pub fn rpm_common_dev_packages() -> Vec<&'static str> {
vec![
"gcc",
"gcc-c++",
"clang",
"llvm-devel",
"glibc-devel",
"libstdc++-devel",
"openssl-devel",
"zlib-devel",
"libpng-devel",
"libjpeg-turbo-devel",
"freetype-devel",
"fontconfig-devel",
"libX11-devel",
"libxcb-devel",
"libXrandr-devel",
"libXi-devel",
"mesa-libGL-devel",
"mesa-libGLU-devel",
"vulkan-devel",
"SDL2-devel",
"boost-devel",
"eigen3-devel",
"opencv-devel",
"libcurl-devel",
"libxml2-devel",
"protobuf-devel",
"gtest-devel",
"gmock-devel",
"benchmark-devel",
"json-devel",
"spdlog-devel",
"fmt-devel",
"abseil-cpp-devel",
"gflags-devel",
"glog-devel",
]
}
#[derive(Debug, Clone)]
pub struct PacmanContext {
pub root: PathBuf,
pub db_path: PathBuf,
pub cache_dir: PathBuf,
}
impl Default for PacmanContext {
fn default() -> Self {
Self {
root: PathBuf::from("/"),
db_path: PathBuf::from("/var/lib/pacman"),
cache_dir: PathBuf::from("/var/cache/pacman/pkg"),
}
}
}
pub fn pacman_resolve(
package_name: &str,
ctx: &PacmanContext,
) -> Result<PackageDependency, String> {
let include_dir = ctx.root.join("usr/include");
let lib_dir = ctx.root.join("usr/lib");
Ok(PackageDependency {
name: package_name.to_string(),
version: "unknown".to_string(),
description: format!("Pacman package: {}", package_name),
homepage: None,
license: None,
include_dirs: vec![include_dir],
library_dirs: vec![lib_dir],
libraries: vec![package_name.to_string()],
compile_flags: Vec::new(),
link_flags: vec![format!("-l{}", package_name)],
defines: HashMap::new(),
dependencies: Vec::new(),
})
}
#[derive(Debug, Clone)]
pub struct NixContext {
pub nix_store: PathBuf,
pub nix_profile: PathBuf,
pub shell_nix_path: Option<PathBuf>,
}
impl Default for NixContext {
fn default() -> Self {
Self {
nix_store: PathBuf::from("/nix/store"),
nix_profile: PathBuf::from(
std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()) + "/.nix-profile",
),
shell_nix_path: None,
}
}
}
pub fn nix_resolve(package_name: &str, ctx: &NixContext) -> Result<PackageDependency, String> {
let hash = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
package_name.hash(&mut h);
format!("{:x}", h.finish())
};
let store_path = ctx.nix_store.join(format!("{}-{}", hash, package_name));
Ok(PackageDependency {
name: package_name.to_string(),
version: "nixpkgs".to_string(),
description: format!("Nix package: {}", package_name),
homepage: None,
license: None,
include_dirs: vec![store_path.join("include")],
library_dirs: vec![store_path.join("lib")],
libraries: vec![package_name.to_string()],
compile_flags: vec![format!("-I{}", store_path.join("include").display())],
link_flags: vec![format!("-L{}", store_path.join("lib").display())],
defines: HashMap::new(),
dependencies: Vec::new(),
})
}
pub fn generate_shell_nix(packages: &[&str]) -> String {
let mut nix = String::new();
nix.push_str("{ pkgs ? import <nixpkgs> {} }:\n\n");
nix.push_str("pkgs.mkShell {\n");
nix.push_str(" buildInputs = with pkgs; [\n");
for pkg in packages {
nix.push_str(&format!(" {}\n", pkg));
}
nix.push_str(" ];\n");
nix.push_str("\n");
nix.push_str(" shellHook = ''\n");
nix.push_str(" echo \"Nix C/C++ development environment activated\"\n");
nix.push_str(" '';\n");
nix.push_str("}\n");
nix
}
pub fn nix_cpp_packages() -> Vec<&'static str> {
vec![
"gcc",
"clang",
"llvm",
"cmake",
"ninja",
"pkg-config",
"boost",
"eigen",
"opencv",
"protobuf",
"grpc",
"sdl2",
"sfml",
"glfw",
"glew",
"glm",
"openssl",
"curl",
"zlib",
"libpng",
"freetype",
"fmt",
"spdlog",
"nlohmann_json",
"gtest",
"benchmark",
]
}
#[derive(Debug, Clone)]
pub struct CondaContext {
pub environment_path: PathBuf,
pub channels: Vec<String>,
pub platform: String,
}
impl Default for CondaContext {
fn default() -> Self {
Self {
environment_path: PathBuf::from(
std::env::var("CONDA_PREFIX").unwrap_or_else(|_| "/opt/conda".to_string()),
),
channels: vec!["conda-forge".to_string(), "defaults".to_string()],
platform: "linux-64".to_string(),
}
}
}
pub fn conda_resolve(package_name: &str, ctx: &CondaContext) -> Result<PackageDependency, String> {
let include_dir = ctx.environment_path.join("include");
let lib_dir = ctx.environment_path.join("lib");
Ok(PackageDependency {
name: package_name.to_string(),
version: "conda".to_string(),
description: format!("Conda package: {}", package_name),
homepage: None,
license: None,
include_dirs: vec![include_dir],
library_dirs: vec![lib_dir],
libraries: vec![package_name.to_string()],
compile_flags: vec![format!(
"-I{}",
ctx.environment_path.join("include").display()
)],
link_flags: vec![
format!("-L{}", ctx.environment_path.join("lib").display()),
format!("-Wl,-rpath,{}", ctx.environment_path.join("lib").display()),
],
defines: HashMap::new(),
dependencies: Vec::new(),
})
}
pub fn generate_conda_env(name: &str, packages: &[&str]) -> String {
let mut yml = String::new();
yml.push_str(&format!("name: {}\n", name));
yml.push_str("channels:\n");
yml.push_str(" - conda-forge\n");
yml.push_str(" - defaults\n");
yml.push_str("dependencies:\n");
for pkg in packages {
yml.push_str(&format!(" - {}\n", pkg));
}
yml
}
#[derive(Debug, Clone)]
pub struct SpackContext {
pub spack_root: PathBuf,
pub opt_dir: PathBuf,
pub compilers: Vec<SpackCompiler>,
pub spec: String,
pub variants: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct SpackCompiler {
pub name: String,
pub version: String,
pub cc_path: PathBuf,
pub cxx_path: PathBuf,
pub f77_path: Option<PathBuf>,
pub f90_path: Option<PathBuf>,
pub target: String,
}
impl Default for SpackContext {
fn default() -> Self {
Self {
spack_root: PathBuf::from("/opt/spack"),
opt_dir: PathBuf::from("/opt/spack/opt"),
compilers: Vec::new(),
spec: String::new(),
variants: HashMap::new(),
}
}
}
pub fn spack_resolve(package_name: &str, ctx: &SpackContext) -> Result<PackageDependency, String> {
let hash = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
package_name.hash(&mut h);
format!("{:x}", h.finish())
};
let install_prefix = ctx.opt_dir.join(format!("spack-{}/{}", package_name, hash));
Ok(PackageDependency {
name: package_name.to_string(),
version: ctx.spec.clone(),
description: format!("Spack package: {}", package_name),
homepage: None,
license: None,
include_dirs: vec![install_prefix.join("include")],
library_dirs: vec![install_prefix.join("lib")],
libraries: vec![package_name.to_string()],
compile_flags: vec![format!("-I{}", install_prefix.join("include").display())],
link_flags: vec![
format!("-L{}", install_prefix.join("lib").display()),
format!("-Wl,-rpath,{}", install_prefix.join("lib").display()),
],
defines: HashMap::new(),
dependencies: Vec::new(),
})
}
pub fn spack_hpc_packages() -> Vec<&'static str> {
vec![
"openmpi",
"mpich",
"hdf5",
"netcdf-c",
"netcdf-cxx4",
"petsc",
"trilinos",
"hypre",
"superlu",
"scalapack",
"fftw",
"boost",
"eigen",
"gsl",
"openblas",
"blas",
"lapack",
"cgal",
"vtk",
"paraview",
"cmake",
"ninja",
"gcc",
"llvm",
"intel-mkl",
"cuda",
"kokkos",
"raja",
]
}
#[derive(Debug, Clone)]
pub struct HunterContext {
pub hunter_root: PathBuf,
pub gate_url: String,
pub sha1: String,
pub packages: Vec<HunterPackage>,
pub cmake_args: Vec<String>,
}
impl Default for HunterContext {
fn default() -> Self {
Self {
hunter_root: PathBuf::from("_builds/Hunter"),
gate_url: "https://github.com/cpp-pm/gate/archive/v0.12.0.tar.gz".to_string(),
sha1: "default".to_string(),
packages: Vec::new(),
cmake_args: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct HunterPackage {
pub name: String,
pub version: String,
pub url: Option<String>,
pub sha1: Option<String>,
pub cmake_args: Vec<String>,
pub find_package_name: Option<String>,
}
pub fn generate_hunter_cmake(ctx: &HunterContext) -> String {
let mut cmake = String::new();
cmake.push_str("include(\"cmake/HunterGate.cmake\")\n");
cmake.push_str(&format!(
"HunterGate(\n URL \"{}\"\n SHA1 \"{}\"\n",
ctx.gate_url, ctx.sha1
));
cmake.push_str(")\n\n");
for pkg in &ctx.packages {
cmake.push_str(&format!("hunter_add_package({})\n", pkg.name));
cmake.push_str(&format!(
"find_package({} CONFIG REQUIRED)\n",
pkg.find_package_name.as_deref().unwrap_or(&pkg.name)
));
}
cmake
}
pub fn hunter_resolve(ctx: &HunterContext) -> PackageResolution {
let mut resolution = PackageResolution::default();
for pkg in &ctx.packages {
let install_dir = ctx
.hunter_root
.join("_Base")
.join(format!("{}/{}", pkg.name, pkg.version));
resolution.packages.push(PackageDependency {
name: pkg.name.clone(),
version: pkg.version.clone(),
description: format!("Hunter package: {}", pkg.name),
homepage: pkg.url.clone(),
license: None,
include_dirs: vec![install_dir.join("include")],
library_dirs: vec![install_dir.join("lib")],
libraries: vec![pkg.name.clone()],
compile_flags: Vec::new(),
link_flags: Vec::new(),
defines: HashMap::new(),
dependencies: Vec::new(),
});
}
resolution
}
#[derive(Debug, Clone)]
pub struct CpmContext {
pub cpm_version: String,
pub packages: Vec<CpmPackage>,
pub cmake_minimum: String,
}
impl Default for CpmContext {
fn default() -> Self {
Self {
cpm_version: "0.38.7".to_string(),
packages: Vec::new(),
cmake_minimum: "3.14".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct CpmPackage {
pub name: String,
pub version: Option<String>,
pub github_repository: Option<String>,
pub git_tag: Option<String>,
pub url: Option<String>,
pub options: HashMap<String, String>,
pub find_package_args: Option<String>,
}
pub fn generate_cpm_cmake(ctx: &CpmContext) -> String {
let mut cmake = String::new();
cmake.push_str(&format!("# CPM.cmake v{}\n", ctx.cpm_version));
cmake.push_str(&format!(
"cmake_minimum_required(VERSION {})\n\n",
ctx.cmake_minimum
));
cmake.push_str(
r#"
# Download CPM.cmake
file(DOWNLOAD
https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_VERSION}/CPM.cmake
${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake
)
include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake)
"#,
);
for pkg in &ctx.packages {
cmake.push_str(&format!("CPMAddPackage(\n NAME {}\n", pkg.name));
if let Some(ref ver) = pkg.version {
cmake.push_str(&format!(" VERSION {}\n", ver));
}
if let Some(ref repo) = pkg.github_repository {
cmake.push_str(&format!(" GITHUB_REPOSITORY {}\n", repo));
}
if let Some(ref tag) = pkg.git_tag {
cmake.push_str(&format!(" GIT_TAG {}\n", tag));
}
if let Some(ref url) = pkg.url {
cmake.push_str(&format!(" URL {}\n", url));
}
for (opt_key, opt_val) in &pkg.options {
cmake.push_str(&format!(" {} {}\n", opt_key, opt_val));
}
if let Some(ref find_args) = pkg.find_package_args {
cmake.push_str(&format!(" FIND_PACKAGE_ARGUMENTS {}\n", find_args));
}
cmake.push_str(")\n\n");
}
cmake
}
pub fn cpm_resolve(ctx: &CpmContext) -> PackageResolution {
let mut resolution = PackageResolution::default();
for pkg in &ctx.packages {
resolution.packages.push(PackageDependency {
name: pkg.name.clone(),
version: pkg
.git_tag
.clone()
.or_else(|| pkg.version.clone())
.unwrap_or_else(|| "latest".to_string()),
description: format!("CPM.cmake package: {}", pkg.name),
homepage: pkg.github_repository.clone(),
license: None,
include_dirs: Vec::new(),
library_dirs: Vec::new(),
libraries: vec![pkg.name.clone()],
compile_flags: Vec::new(),
link_flags: Vec::new(),
defines: HashMap::new(),
dependencies: Vec::new(),
});
}
resolution
}
pub fn cpm_common_packages() -> Vec<CpmPackage> {
vec![
CpmPackage {
name: "fmt".to_string(),
version: Some("10.1.1".to_string()),
github_repository: Some("fmtlib/fmt".to_string()),
git_tag: Some("10.1.1".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
},
CpmPackage {
name: "spdlog".to_string(),
version: Some("1.12.0".to_string()),
github_repository: Some("gabime/spdlog".to_string()),
git_tag: Some("v1.12.0".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
},
CpmPackage {
name: "catch2".to_string(),
version: Some("3.5.2".to_string()),
github_repository: Some("catchorg/Catch2".to_string()),
git_tag: Some("v3.5.2".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
},
CpmPackage {
name: "nlohmann_json".to_string(),
version: Some("3.11.3".to_string()),
github_repository: Some("nlohmann/json".to_string()),
git_tag: Some("v3.11.3".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
},
CpmPackage {
name: "doctest".to_string(),
version: Some("2.4.11".to_string()),
github_repository: Some("doctest/doctest".to_string()),
git_tag: Some("v2.4.11".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
},
CpmPackage {
name: "glm".to_string(),
version: Some("0.9.9.8".to_string()),
github_repository: Some("g-truc/glm".to_string()),
git_tag: Some("0.9.9.8".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
},
]
}
#[derive(Debug, Clone)]
pub struct PackageManagerRegistry {
pub managers: HashMap<PackageManager, String>,
pub cache: HashMap<String, PackageDependency>,
pub fallback_order: Vec<PackageManager>,
}
impl PackageManagerRegistry {
pub fn new() -> Self {
Self {
managers: HashMap::new(),
cache: HashMap::new(),
fallback_order: vec![
PackageManager::PkgConfig,
PackageManager::CpmCmake,
PackageManager::FetchContent,
PackageManager::Vcpkg,
PackageManager::Conan,
PackageManager::Apt,
PackageManager::Homebrew,
],
}
}
pub fn resolve(&mut self, package_name: &str) -> Result<PackageDependency, String> {
if let Some(dep) = self.cache.get(package_name) {
return Ok(dep.clone());
}
for pm in &self.fallback_order.clone() {
match self.try_resolve(pm, package_name) {
Ok(dep) => {
self.cache.insert(package_name.to_string(), dep.clone());
return Ok(dep);
}
Err(_) => continue,
}
}
Err(format!(
"Package '{}' not found in any registered package manager",
package_name
))
}
fn try_resolve(&self, pm: &PackageManager, name: &str) -> Result<PackageDependency, String> {
match pm {
PackageManager::Conan => Err("Conan resolution not available in registry".to_string()),
PackageManager::PkgConfig => {
let ctx = PkgConfigContext::default();
pkg_config_resolve(name, &ctx)
}
_ => Err(format!("{:?} resolution not attempted", pm)),
}
}
pub fn register_manager(&mut self, pm: PackageManager, command: &str) {
self.managers.insert(pm, command.to_string());
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn cache_size(&self) -> usize {
self.cache.len()
}
}
impl Default for PackageManagerRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn resolve_unified(
packages: &[&str],
registry: &mut PackageManagerRegistry,
) -> PackageResolution {
let mut resolution = PackageResolution::default();
let start = std::time::Instant::now();
for package in packages {
match registry.resolve(package) {
Ok(dep) => {
resolution.packages.push(dep);
resolution.cache_hits += 1;
}
Err(e) => {
resolution.missing.push(package.to_string());
eprintln!("Warning: {}", e);
}
}
}
resolution.total_time_ms = start.elapsed().as_millis() as u64;
resolution
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conan_requirement() {
let req = ConanRequirement {
name: "boost".to_string(),
version_range: "1.84.0".to_string(),
override_version: false,
private: false,
transitive_headers: true,
};
assert_eq!(req.name, "boost");
assert_eq!(req.version_range, "1.84.0");
}
#[test]
fn test_conan_generators() {
assert_eq!(ConanGenerator::CMake.command_arg(), "cmake");
assert_eq!(ConanGenerator::CMakeDeps.command_arg(), "CMakeDeps");
assert_eq!(ConanGenerator::PkgConfig.command_arg(), "pkg_config");
}
#[test]
fn test_conan_build_policy() {
assert_eq!(ConanBuildPolicy::Missing.as_flag(), "--build=missing");
assert_eq!(ConanBuildPolicy::Outdated.as_flag(), "--build=outdated");
}
#[test]
fn test_vcpkg_manifest_parse() {
let manifest_json = r#"{
"name": "myproject",
"version-string": "1.0.0",
"dependencies": [
"boost",
{"name": "fmt", "version>=": "10.0.0"},
{"name": "spdlog", "features": ["async"]}
],
"default-features": ["core"]
}"#;
let tmp = std::env::temp_dir().join("test_vcpkg.json");
std::fs::write(&tmp, manifest_json).unwrap();
let manifest = parse_vcpkg_manifest(&tmp).unwrap();
assert_eq!(manifest.name, "myproject");
assert_eq!(manifest.version_string, "1.0.0");
assert_eq!(manifest.dependencies.len(), 3);
assert_eq!(manifest.default_features, vec!["core"]);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_vcpkg_triplets() {
let triplets = vcpkg_common_triplets();
assert!(triplets.contains(&"x64-linux"));
assert!(triplets.contains(&"x64-windows"));
assert!(triplets.contains(&"arm64-osx"));
}
#[test]
fn test_fetchcontent_dependency() {
let dep = FetchContentDependency {
name: "nlohmann_json".to_string(),
git_repository: Some("https://github.com/nlohmann/json".to_string()),
git_tag: Some("v3.11.3".to_string()),
url: None,
url_hash: None,
source_dir: None,
binary_dir: None,
cmake_args: Vec::new(),
system: true,
find_package_args: Vec::new(),
};
assert!(dep.git_repository.is_some());
assert_eq!(dep.name, "nlohmann_json");
}
#[test]
fn test_generate_fetchcontent_cmake() {
let deps = vec![FetchContentDependency {
name: "googletest".to_string(),
git_repository: Some("https://github.com/google/googletest".to_string()),
git_tag: Some("v1.14.0".to_string()),
url: None,
url_hash: None,
source_dir: None,
binary_dir: None,
cmake_args: Vec::new(),
system: false,
find_package_args: Vec::new(),
}];
let cmake = generate_fetchcontent_cmake(&deps);
assert!(cmake.contains("FetchContent_Declare"));
assert!(cmake.contains("googletest"));
assert!(cmake.contains("GIT_REPOSITORY"));
assert!(cmake.contains("v1.14.0"));
}
#[test]
fn test_parse_pc_file() {
let pc_content = r#"
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: zlib
Description: zlib compression library
Version: 1.2.13
Requires:
Libs: -L${libdir} -lz
Cflags: -I${includedir}
"#;
let tmp = std::env::temp_dir().join("zlib.pc");
std::fs::write(&tmp, pc_content).unwrap();
let pc = parse_pc_file(&tmp).unwrap();
assert_eq!(pc.name, "zlib");
assert_eq!(pc.version, "1.2.13");
assert_eq!(pc.description, "zlib compression library");
assert!(!pc.include_dirs.is_empty());
assert!(!pc.lib_dirs.is_empty());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_brew_prefix_detection() {
let result = brew_prefix();
match result {
Ok(prefix) => {
assert!(!prefix.as_os_str().is_empty());
}
Err(e) => {
assert!(e.contains("not found"));
}
}
}
#[test]
fn test_brew_formula_list() {
let formulas = brew_formula_list();
assert!(!formulas.is_empty());
assert!(formulas.contains(&"cmake"));
assert!(formulas.contains(&"boost"));
}
#[test]
fn test_apt_default_context() {
let ctx = AptContext::default();
assert_eq!(ctx.dpkg_root, PathBuf::from("/"));
assert!(ctx.architectures.contains(&"amd64".to_string()));
}
#[test]
fn test_debian_dev_packages() {
let pkgs = debian_common_dev_packages();
assert!(!pkgs.is_empty());
assert!(pkgs.contains(&"build-essential"));
assert!(pkgs.contains(&"clang"));
}
#[test]
fn test_rpm_default_context() {
let ctx = RpmContext::default();
assert_eq!(ctx.rpm_root, PathBuf::from("/"));
}
#[test]
fn test_rpm_dev_packages() {
let pkgs = rpm_common_dev_packages();
assert!(!pkgs.is_empty());
assert!(pkgs.contains(&"gcc-c++"));
}
#[test]
fn test_pacman_default_context() {
let ctx = PacmanContext::default();
assert_eq!(ctx.root, PathBuf::from("/"));
}
#[test]
fn test_nix_default_context() {
let ctx = NixContext::default();
assert!(!ctx.nix_store.as_os_str().is_empty());
assert!(!ctx.nix_profile.as_os_str().is_empty());
}
#[test]
fn test_generate_shell_nix() {
let nix = generate_shell_nix(&["gcc", "cmake", "boost"]);
assert!(nix.contains("mkShell"));
assert!(nix.contains("gcc"));
assert!(nix.contains("cmake"));
assert!(nix.contains("boost"));
}
#[test]
fn test_nix_cpp_packages() {
let pkgs = nix_cpp_packages();
assert!(pkgs.contains(&"gcc"));
assert!(pkgs.contains(&"llvm"));
}
#[test]
fn test_conda_default_context() {
let ctx = CondaContext::default();
assert!(!ctx.environment_path.as_os_str().is_empty());
}
#[test]
fn test_generate_conda_env() {
let yml = generate_conda_env("myenv", &["gcc", "cmake", "boost"]);
assert!(yml.contains("name: myenv"));
assert!(yml.contains("conda-forge"));
assert!(yml.contains("gcc"));
}
#[test]
fn test_spack_hpc_packages() {
let pkgs = spack_hpc_packages();
assert!(pkgs.contains(&"openmpi"));
assert!(pkgs.contains(&"hdf5"));
assert!(pkgs.contains(&"cuda"));
}
#[test]
fn test_hunter_context_default() {
let ctx = HunterContext::default();
assert!(!ctx.gate_url.is_empty());
assert_eq!(ctx.hunter_root, PathBuf::from("_builds/Hunter"));
}
#[test]
fn test_generate_hunter_cmake() {
let ctx = HunterContext {
packages: vec![HunterPackage {
name: "GTest".to_string(),
version: "1.14.0".to_string(),
url: None,
sha1: None,
cmake_args: Vec::new(),
find_package_name: Some("GTest".to_string()),
}],
..Default::default()
};
let cmake = generate_hunter_cmake(&ctx);
assert!(cmake.contains("HunterGate"));
assert!(cmake.contains("hunter_add_package(GTest)"));
}
#[test]
fn test_cpm_common_packages() {
let pkgs = cpm_common_packages();
assert!(pkgs.iter().any(|p| p.name == "fmt"));
assert!(pkgs.iter().any(|p| p.name == "catch2"));
}
#[test]
fn test_generate_cpm_cmake() {
let ctx = CpmContext {
cpm_version: "0.38.7".to_string(),
packages: vec![CpmPackage {
name: "fmt".to_string(),
version: Some("10.1.1".to_string()),
github_repository: Some("fmtlib/fmt".to_string()),
git_tag: Some("10.1.1".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
}],
cmake_minimum: "3.14".to_string(),
};
let cmake = generate_cpm_cmake(&ctx);
assert!(cmake.contains("CPMAddPackage"));
assert!(cmake.contains("fmtlib/fmt"));
assert!(cmake.contains("10.1.1"));
}
#[test]
fn test_package_manager_registry() {
let mut registry = PackageManagerRegistry::new();
assert_eq!(registry.cache_size(), 0);
registry.register_manager(PackageManager::Apt, "apt-get");
assert!(registry.managers.contains_key(&PackageManager::Apt));
}
#[test]
fn test_resolve_unified() {
let mut registry = PackageManagerRegistry::new();
let packages = vec!["zlib", "libpng"];
let resolution = resolve_unified(&packages, &mut registry);
assert!(!resolution.missing.is_empty() || !resolution.packages.is_empty());
}
#[test]
fn test_package_resolution_default() {
let res = PackageResolution::default();
assert!(res.packages.is_empty());
assert!(res.missing.is_empty());
assert_eq!(res.total_time_ms, 0);
}
#[test]
fn test_package_manager_names() {
assert_eq!(PackageManager::Conan.name(), "conan");
assert_eq!(PackageManager::PkgConfig.name(), "pkg-config");
assert_eq!(PackageManager::Nix.name(), "nix");
assert_eq!(PackageManager::Vcpkg.name(), "vcpkg");
assert_eq!(PackageManager::Apt.name(), "apt");
assert_eq!(PackageManager::Spack.name(), "spack");
}
#[test]
fn test_package_dependency_fields() {
let dep = PackageDependency {
name: "test-lib".to_string(),
version: "2.0.0".to_string(),
description: "A test library".to_string(),
homepage: Some("https://example.com".to_string()),
license: Some("MIT".to_string()),
include_dirs: vec![PathBuf::from("/usr/include")],
library_dirs: vec![PathBuf::from("/usr/lib")],
libraries: vec!["test".to_string()],
compile_flags: vec!["-DTEST=1".to_string()],
link_flags: vec!["-ltest".to_string()],
defines: HashMap::new(),
dependencies: vec!["zlib".to_string()],
};
assert_eq!(dep.name, "test-lib");
assert_eq!(dep.version, "2.0.0");
assert!(!dep.include_dirs.is_empty());
assert!(!dep.dependencies.is_empty());
}
#[test]
fn test_registry_clear_cache() {
let mut registry = PackageManagerRegistry::new();
registry.clear_cache();
assert_eq!(registry.cache_size(), 0);
}
#[test]
fn test_conan_install_and_compile() {
let ctx = ConanContext {
conanfile_path: PathBuf::from("conanfile.py"),
profile: "default".to_string(),
build_type: "Release".to_string(),
settings: HashMap::new(),
options: HashMap::new(),
requires: vec![ConanRequirement {
name: "openssl".to_string(),
version_range: "3.2.0".to_string(),
override_version: false,
private: false,
transitive_headers: true,
}],
generators: vec![ConanGenerator::CMakeDeps],
build_policy: ConanBuildPolicy::Missing,
remote: None,
};
let resolution = conan_install_and_compile(&ctx);
assert_eq!(resolution.packages.len(), 1);
}
#[test]
fn test_vcpkg_install_and_compile() {
let ctx = VcpkgContext {
manifest_path: PathBuf::from("vcpkg.json"),
triplet: "x64-linux".to_string(),
features: vec!["async".to_string()],
overlays: Vec::new(),
registries: Vec::new(),
vcpkg_root: PathBuf::from("/opt/vcpkg"),
};
let resolution = vcpkg_install_and_compile(&ctx);
assert_eq!(resolution.packages.len(), 1);
}
#[test]
fn test_fetchcontent_resolve() {
let deps = vec![FetchContentDependency {
name: "fmt".to_string(),
git_repository: Some("https://github.com/fmtlib/fmt".to_string()),
git_tag: Some("10.0.0".to_string()),
url: None,
url_hash: None,
source_dir: None,
binary_dir: None,
cmake_args: Vec::new(),
system: false,
find_package_args: Vec::new(),
}];
let resolution = resolve_fetchcontent(&deps);
assert_eq!(resolution.packages.len(), 1);
}
#[test]
fn test_hunter_resolve() {
let ctx = HunterContext {
packages: vec![HunterPackage {
name: "GTest".to_string(),
version: "1.14.0".to_string(),
url: None,
sha1: None,
cmake_args: Vec::new(),
find_package_name: None,
}],
..Default::default()
};
let resolution = hunter_resolve(&ctx);
assert_eq!(resolution.packages.len(), 1);
}
#[test]
fn test_cpm_resolve() {
let ctx = CpmContext {
packages: vec![CpmPackage {
name: "spdlog".to_string(),
version: None,
github_repository: Some("gabime/spdlog".to_string()),
git_tag: Some("v1.12.0".to_string()),
url: None,
options: HashMap::new(),
find_package_args: None,
}],
..Default::default()
};
let resolution = cpm_resolve(&ctx);
assert_eq!(resolution.packages.len(), 1);
}
#[test]
fn test_apt_resolve() {
let ctx = AptContext::default();
let result = apt_resolve("ssl", &ctx);
assert!(result.is_ok());
let dep = result.unwrap();
assert_eq!(dep.name, "ssl");
}
#[test]
fn test_rpm_resolve() {
let ctx = RpmContext::default();
let result = rpm_resolve("openssl", &ctx);
assert!(result.is_ok());
}
#[test]
fn test_pacman_resolve() {
let ctx = PacmanContext::default();
let result = pacman_resolve("zlib", &ctx);
assert!(result.is_ok());
}
#[test]
fn test_nix_resolve() {
let ctx = NixContext::default();
let result = nix_resolve("boost", &ctx);
assert!(result.is_ok());
let dep = result.unwrap();
assert_eq!(dep.name, "boost");
assert!(!dep.include_dirs.is_empty());
}
#[test]
fn test_conda_resolve() {
let ctx = CondaContext::default();
let result = conda_resolve("gcc", &ctx);
assert!(result.is_ok());
}
#[test]
fn test_spack_resolve() {
let ctx = SpackContext::default();
let result = spack_resolve("openmpi", &ctx);
assert!(result.is_ok());
}
#[test]
fn test_parse_conanfile_nonexistent() {
let result = parse_conanfile(&PathBuf::from("/nonexistent/conanfile.py"));
assert!(result.is_err());
}
#[test]
fn test_parse_vcpkg_manifest_nonexistent() {
let result = parse_vcpkg_manifest(&PathBuf::from("/nonexistent/vcpkg.json"));
assert!(result.is_err());
}
}