#![allow(dead_code)]
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct CcgoConfig {
#[serde(alias = "project")]
pub package: Option<PackageConfig>,
pub workspace: Option<WorkspaceConfig>,
#[serde(default)]
pub dependencies: Vec<DependencyConfig>,
#[serde(default, rename = "deps")]
pub simplified_deps: SimplifiedDependencies,
#[serde(default)]
pub registries: RegistriesConfig,
#[serde(default)]
pub features: FeaturesConfig,
pub build: Option<BuildConfig>,
pub platforms: Option<PlatformConfigs>,
#[serde(default, rename = "bin")]
pub bins: Vec<BinConfig>,
#[serde(default, rename = "example")]
pub examples: Vec<ExampleConfig>,
#[serde(default)]
pub patch: PatchConfig,
pub include: Option<IncludeConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct IncludeConfig {
pub src: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RegistriesConfig {
#[serde(flatten)]
pub registries: HashMap<String, String>,
}
impl RegistriesConfig {
pub fn is_empty(&self) -> bool {
self.registries.is_empty()
}
pub fn get(&self, name: &str) -> Option<&String> {
self.registries.get(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
self.registries.iter()
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SimplifiedDependencies {
#[serde(flatten)]
pub deps: HashMap<String, SimplifiedDep>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum SimplifiedDep {
Version(String),
Full(SimplifiedDepSpec),
}
#[derive(Debug, Clone, Deserialize)]
pub struct SimplifiedDepSpec {
pub version: String,
pub registry: Option<String>,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub default_features: Option<bool>,
#[serde(default)]
pub optional: bool,
}
impl SimplifiedDep {
pub fn version(&self) -> &str {
match self {
SimplifiedDep::Version(v) => v,
SimplifiedDep::Full(spec) => &spec.version,
}
}
pub fn registry(&self) -> Option<&str> {
match self {
SimplifiedDep::Version(_) => None,
SimplifiedDep::Full(spec) => spec.registry.as_deref(),
}
}
pub fn features(&self) -> &[String] {
match self {
SimplifiedDep::Version(_) => &[],
SimplifiedDep::Full(spec) => &spec.features,
}
}
pub fn is_optional(&self) -> bool {
match self {
SimplifiedDep::Version(_) => false,
SimplifiedDep::Full(spec) => spec.optional,
}
}
pub fn to_dependency_config(&self, name: &str) -> DependencyConfig {
DependencyConfig {
name: name.to_string(),
version: self.version().to_string(),
git: None,
branch: None,
path: None,
zip: None,
optional: self.is_optional(),
features: self.features().to_vec(),
default_features: match self {
SimplifiedDep::Version(_) => None,
SimplifiedDep::Full(spec) => spec.default_features,
},
workspace: false,
registry: match self {
SimplifiedDep::Version(_) => None,
SimplifiedDep::Full(spec) => spec.registry.clone(),
},
linkage: None,
linkage_on_shared: None,
linkage_on_static: None,
android: None,
ios: None,
macos: None,
ohos: None,
linux: None,
windows: None,
}
}
}
impl SimplifiedDependencies {
pub fn is_empty(&self) -> bool {
self.deps.is_empty()
}
pub fn to_dependency_configs(&self) -> Vec<DependencyConfig> {
self.deps
.iter()
.map(|(name, dep)| dep.to_dependency_config(name))
.collect()
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PatchConfig {
#[serde(default, rename = "crates-io")]
pub crates_io: HashMap<String, PatchDependency>,
#[serde(flatten)]
pub sources: HashMap<String, HashMap<String, PatchDependency>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PatchDependency {
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub path: Option<String>,
#[serde(default)]
pub version: String,
}
impl PatchConfig {
pub fn find_patch(&self, dep_name: &str, dep_source: Option<&str>) -> Option<&PatchDependency> {
if let Some(source) = dep_source {
if let Some(source_patches) = self.sources.get(source) {
if let Some(patch) = source_patches.get(dep_name) {
return Some(patch);
}
}
}
self.crates_io.get(dep_name)
}
pub fn has_patches(&self) -> bool {
!self.crates_io.is_empty() || !self.sources.is_empty()
}
pub fn patched_dependencies(&self) -> Vec<&str> {
let mut deps = Vec::new();
deps.extend(self.crates_io.keys().map(|s| s.as_str()));
for source_patches in self.sources.values() {
deps.extend(source_patches.keys().map(|s| s.as_str()));
}
deps.sort();
deps.dedup();
deps
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct WorkspaceConfig {
#[serde(default)]
pub members: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default = "default_resolver")]
pub resolver: String,
#[serde(default)]
pub dependencies: Vec<WorkspaceDependency>,
#[serde(default)]
pub default_members: Vec<String>,
}
fn default_resolver() -> String {
"1".to_string()
}
impl Default for WorkspaceConfig {
fn default() -> Self {
Self {
members: Vec::new(),
exclude: Vec::new(),
resolver: default_resolver(),
dependencies: Vec::new(),
default_members: Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct WorkspaceDependency {
pub name: String,
pub version: String,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub path: Option<String>,
pub zip: Option<String>,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub default_features: Option<bool>,
}
impl WorkspaceDependency {
pub fn to_dependency_config(&self) -> DependencyConfig {
DependencyConfig {
name: self.name.clone(),
version: self.version.clone(),
git: self.git.clone(),
branch: self.branch.clone(),
path: self.path.clone(),
zip: self.zip.clone(),
optional: false,
features: self.features.clone(),
default_features: self.default_features,
workspace: false,
registry: None,
linkage: None,
linkage_on_shared: None,
linkage_on_static: None,
android: None,
ios: None,
macos: None,
ohos: None,
linux: None,
windows: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub version: String,
pub description: Option<String>,
pub authors: Option<Vec<String>>,
pub license: Option<String>,
pub repository: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BinConfig {
pub name: String,
pub path: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ExampleConfig {
pub name: String,
pub path: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PlatformLinkageConfig {
pub linkage: Option<Linkage>,
pub linkage_on_shared: Option<Linkage>,
pub linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub enum Linkage {
SharedExternal,
StaticEmbedded,
StaticExternal,
}
impl std::fmt::Display for Linkage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Linkage::SharedExternal => "shared-external",
Linkage::StaticEmbedded => "static-embedded",
Linkage::StaticExternal => "static-external",
};
f.write_str(s)
}
}
impl TryFrom<String> for Linkage {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.as_str() {
"shared-external" => Ok(Linkage::SharedExternal),
"static-embedded" => Ok(Linkage::StaticEmbedded),
"static-external" => Ok(Linkage::StaticExternal),
"shared-embedded" => Err("linkage = \"shared-embedded\" is invalid: a .so cannot be \
archived into another .so. Use shared-external (DT_NEEDED) \
or static-embedded (merge dep's .a into consumer)."
.to_string()),
other => Err(format!(
"linkage = \"{other}\" is not recognized. Valid values: \
shared-external, static-embedded, static-external."
)),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DependencyConfig {
pub name: String,
#[serde(default)]
pub version: String,
pub git: Option<String>,
pub branch: Option<String>,
pub path: Option<String>,
pub zip: Option<String>,
pub linkage: Option<Linkage>,
pub linkage_on_shared: Option<Linkage>,
pub linkage_on_static: Option<Linkage>,
#[serde(default)]
pub android: Option<PlatformLinkageConfig>,
#[serde(default)]
pub ios: Option<PlatformLinkageConfig>,
#[serde(default)]
pub macos: Option<PlatformLinkageConfig>,
#[serde(default)]
pub ohos: Option<PlatformLinkageConfig>,
#[serde(default)]
pub linux: Option<PlatformLinkageConfig>,
#[serde(default)]
pub windows: Option<PlatformLinkageConfig>,
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub default_features: Option<bool>,
#[serde(default)]
pub workspace: bool,
#[serde(default)]
pub registry: Option<String>,
}
impl DependencyConfig {
pub fn platform_config(&self, platform: &str) -> Option<&PlatformLinkageConfig> {
match platform {
"android" => self.android.as_ref(),
"ios" => self.ios.as_ref(),
"macos" => self.macos.as_ref(),
"ohos" => self.ohos.as_ref(),
"linux" => self.linux.as_ref(),
"windows" => self.windows.as_ref(),
_ => None,
}
}
pub fn merge_with_workspace(&mut self, ws_dep: &WorkspaceDependency) {
if !self.workspace {
return;
}
if self.version.is_empty() {
self.version = ws_dep.version.clone();
}
if self.git.is_none() {
self.git = ws_dep.git.clone();
}
if self.branch.is_none() {
self.branch = ws_dep.branch.clone();
}
if self.path.is_none() {
self.path = ws_dep.path.clone();
}
let mut merged_features = ws_dep.features.clone();
for feat in &self.features {
if !merged_features.contains(feat) {
merged_features.push(feat.clone());
}
}
self.features = merged_features;
if self.default_features.is_none() {
self.default_features = ws_dep.default_features;
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct FeaturesConfig {
#[serde(default)]
pub default: Vec<String>,
#[serde(flatten)]
pub features: HashMap<String, Vec<String>>,
}
impl FeaturesConfig {
pub fn has_feature(&self, name: &str) -> bool {
name == "default" || self.features.contains_key(name)
}
pub fn feature_names(&self) -> Vec<&str> {
self.features.keys().map(|s| s.as_str()).collect()
}
pub fn resolve_feature(&self, name: &str, resolved: &mut HashSet<String>) -> Result<()> {
if resolved.contains(name) {
return Ok(());
}
if name == "default" {
for default_feature in &self.default {
self.resolve_feature(default_feature, resolved)?;
}
return Ok(());
}
resolved.insert(name.to_string());
if let Some(deps) = self.features.get(name) {
for dep in deps {
if dep.contains('/') {
resolved.insert(dep.clone());
} else if self.has_feature(dep) {
self.resolve_feature(dep, resolved)?;
} else {
resolved.insert(dep.clone());
}
}
}
Ok(())
}
pub fn resolve_features(
&self,
requested: &[String],
use_defaults: bool,
) -> Result<HashSet<String>> {
let mut resolved = HashSet::new();
if use_defaults {
self.resolve_feature("default", &mut resolved)?;
}
for feature in requested {
if !self.has_feature(feature) && !feature.contains('/') {
bail!(
"Unknown feature: '{}'. Available features: {:?}",
feature,
self.feature_names()
);
}
self.resolve_feature(feature, &mut resolved)?;
}
Ok(resolved)
}
pub fn get_enabled_optional_deps<'a>(
&self,
resolved_features: &HashSet<String>,
dependencies: &'a [DependencyConfig],
) -> Vec<&'a DependencyConfig> {
dependencies
.iter()
.filter(|dep| {
if !dep.optional {
return true; }
resolved_features.contains(&dep.name)
})
.collect()
}
}
fn reject_static_external_for_shared(
value: Option<Linkage>,
field: &str,
) -> Result<()> {
if value == Some(Linkage::StaticExternal) {
bail!(
"`{field}` cannot be \"static-external\": a shared consumer cannot have \
unresolved external static references. Use \"shared-external\" or \
\"static-embedded\" instead."
);
}
Ok(())
}
impl DependencyConfig {
pub fn validate(&self) -> Result<()> {
crate::version::VersionReq::parse(&self.version).with_context(|| {
format!(
"Invalid version requirement '{}' for dependency '{}'",
self.version, self.name
)
})?;
let dep = &self.name;
reject_static_external_for_shared(
self.linkage_on_shared,
&format!("[[dependencies]].linkage_on_shared (dep '{dep}')"),
)?;
for (plat, cfg) in [
("android", &self.android),
("ios", &self.ios),
("macos", &self.macos),
("ohos", &self.ohos),
("linux", &self.linux),
("windows", &self.windows),
] {
if let Some(cfg) = cfg {
reject_static_external_for_shared(
cfg.linkage_on_shared,
&format!("[[dependencies]].{plat}.linkage_on_shared (dep '{dep}')"),
)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct BuildConfig {
#[serde(default)]
pub parallel: bool,
pub jobs: Option<usize>,
#[serde(default)]
pub symbol_visibility: bool,
#[serde(default)]
pub submodule_deps: std::collections::HashMap<String, Vec<String>>,
pub verinfo_path: Option<String>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PlatformConfigs {
pub android: Option<AndroidConfig>,
pub ios: Option<IosConfig>,
pub macos: Option<MacosConfig>,
pub windows: Option<WindowsConfig>,
pub linux: Option<LinuxConfig>,
pub ohos: Option<OhosConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AndroidConfig {
pub min_sdk: Option<u32>,
pub architectures: Option<Vec<String>>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct IosConfig {
pub min_version: Option<String>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MacosConfig {
pub min_version: Option<String>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WindowsConfig {
pub toolchain: Option<String>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LinuxConfig {
pub architectures: Option<Vec<String>>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OhosConfig {
pub min_api: Option<u32>,
pub architectures: Option<Vec<String>>,
pub default_dep_linkage: Option<Linkage>,
pub dep_linkage_on_shared: Option<Linkage>,
pub dep_linkage_on_static: Option<Linkage>,
}
impl CcgoConfig {
pub fn load() -> Result<Self> {
Self::load_from_path("CCGO.toml")
}
pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read configuration from {}", path.display()))?;
Self::parse(&content)
}
pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::load_from_path(path)
}
pub fn parse(content: &str) -> Result<Self> {
let mut config: Self = toml::from_str(content).context("Failed to parse CCGO.toml")?;
if config.package.is_none() && config.workspace.is_none() {
bail!("CCGO.toml must contain either [package] or [workspace] section");
}
config.merge_simplified_dependencies();
for dep in &config.dependencies {
if !dep.workspace {
dep.validate()
.with_context(|| format!("Invalid dependency: {}", dep.name))?;
}
}
if let Some(build) = &config.build {
reject_static_external_for_shared(
build.dep_linkage_on_shared,
"[build].dep_linkage_on_shared",
)?;
}
if let Some(platforms) = &config.platforms {
macro_rules! check_plat {
($cfg:expr, $name:literal) => {
if let Some(cfg) = $cfg {
reject_static_external_for_shared(
cfg.dep_linkage_on_shared,
concat!("[platforms.", $name, "].dep_linkage_on_shared"),
)?;
}
};
}
check_plat!(platforms.android.as_ref(), "android");
check_plat!(platforms.ios.as_ref(), "ios");
check_plat!(platforms.macos.as_ref(), "macos");
check_plat!(platforms.ohos.as_ref(), "ohos");
check_plat!(platforms.linux.as_ref(), "linux");
check_plat!(platforms.windows.as_ref(), "windows");
}
Ok(config)
}
fn merge_simplified_dependencies(&mut self) {
if self.simplified_deps.is_empty() {
return;
}
let simplified_configs = self.simplified_deps.to_dependency_configs();
let existing_names: HashSet<String> =
self.dependencies.iter().map(|d| d.name.clone()).collect();
for config in simplified_configs {
if !existing_names.contains(&config.name) {
self.dependencies.push(config);
}
}
}
pub fn registry_dependencies(&self) -> Vec<(&str, &SimplifiedDep)> {
self.simplified_deps
.deps
.iter()
.map(|(name, dep)| (name.as_str(), dep))
.collect()
}
pub fn find_config() -> Result<PathBuf> {
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
let mut dir = current_dir.as_path();
loop {
let config_path = dir.join("CCGO.toml");
if config_path.exists() {
return Ok(config_path);
}
match dir.parent() {
Some(parent) => dir = parent,
None => {
anyhow::bail!(
"Could not find CCGO.toml in current directory or any parent directory"
)
}
}
}
}
pub fn is_workspace(&self) -> bool {
self.workspace.is_some()
}
pub fn is_package(&self) -> bool {
self.package.is_some()
}
pub fn require_package(&self) -> Result<&PackageConfig> {
self.package.as_ref().context(
"This operation requires a [package] section in CCGO.toml.\n\
Workspace-only configurations cannot be used for this operation.",
)
}
pub fn require_workspace(&self) -> Result<&WorkspaceConfig> {
self.workspace
.as_ref()
.context("This operation requires a [workspace] section in CCGO.toml.")
}
pub fn find_workspace_root(start_dir: &Path) -> Result<Option<(PathBuf, Self)>> {
let mut dir = start_dir;
loop {
let config_path = dir.join("CCGO.toml");
if config_path.exists() {
let config = Self::load_from_path(&config_path)?;
if config.is_workspace() {
return Ok(Some((dir.to_path_buf(), config)));
}
}
match dir.parent() {
Some(parent) => dir = parent,
None => return Ok(None),
}
}
}
pub fn get_workspace_members(&self, workspace_root: &Path) -> Result<Vec<PathBuf>> {
let workspace = self.require_workspace()?;
let mut members = Vec::new();
for pattern in &workspace.members {
let full_pattern = workspace_root.join(pattern);
let pattern_str = full_pattern.to_string_lossy();
let paths = glob::glob(&pattern_str)
.with_context(|| format!("Invalid glob pattern: {}", pattern))?;
for path_result in paths {
let path = path_result
.with_context(|| format!("Failed to resolve glob pattern: {}", pattern))?;
if path.is_dir() && path.join("CCGO.toml").exists() {
let relative = path
.strip_prefix(workspace_root)
.unwrap_or(&path)
.to_string_lossy();
let is_excluded = workspace.exclude.iter().any(|exc| {
relative.starts_with(exc) || relative.as_ref() == exc
});
if !is_excluded {
members.push(path);
}
}
}
}
members.sort();
Ok(members)
}
pub fn load_workspace_members(&self, workspace_root: &Path) -> Result<Vec<(PathBuf, Self)>> {
let member_paths = self.get_workspace_members(workspace_root)?;
let mut members = Vec::new();
for member_path in member_paths {
let config_path = member_path.join("CCGO.toml");
let config = Self::load_from_path(&config_path).with_context(|| {
format!("Failed to load member config: {}", config_path.display())
})?;
members.push((member_path, config));
}
Ok(members)
}
pub fn resolve_workspace_dependencies(&mut self, workspace_config: &Self) -> Result<()> {
let workspace = workspace_config.require_workspace()?;
let ws_deps: HashMap<&str, &WorkspaceDependency> = workspace
.dependencies
.iter()
.map(|d| (d.name.as_str(), d))
.collect();
for dep in &mut self.dependencies {
if dep.workspace {
if let Some(ws_dep) = ws_deps.get(dep.name.as_str()) {
dep.merge_with_workspace(ws_dep);
} else {
bail!(
"Dependency '{}' is marked as workspace = true, but not found in \
[workspace.dependencies]",
dep.name
);
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_config() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
"#;
let config = CcgoConfig::parse(toml).unwrap();
let package = config.package.as_ref().unwrap();
assert_eq!(package.name, "mylib");
assert_eq!(package.version, "1.0.0");
}
#[test]
fn test_parse_full_config() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
description = "My C++ library"
authors = ["Test Author"]
license = "MIT"
[[dependencies]]
name = "fmt"
version = "^10.0"
git = "https://github.com/fmtlib/fmt.git"
[build]
parallel = true
jobs = 4
[platforms.android]
min_sdk = 21
architectures = ["arm64-v8a", "armeabi-v7a"]
[platforms.ios]
min_version = "12.0"
"#;
let config = CcgoConfig::parse(toml).unwrap();
let package = config.package.as_ref().unwrap();
assert_eq!(package.name, "mylib");
assert_eq!(config.dependencies.len(), 1);
assert_eq!(config.dependencies[0].name, "fmt");
assert!(config.build.is_some());
assert!(config.platforms.is_some());
}
#[test]
fn test_parse_features_config() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[features]
default = ["std"]
std = []
networking = ["http-client"]
advanced = ["networking", "async"]
full = ["networking", "advanced"]
[[dependencies]]
name = "http-client"
version = "^1.0"
optional = true
[[dependencies]]
name = "async"
version = "^2.0"
optional = true
"#;
let config = CcgoConfig::parse(toml).unwrap();
assert_eq!(config.features.default, vec!["std"]);
assert!(config.features.has_feature("std"));
assert!(config.features.has_feature("networking"));
assert!(config.features.has_feature("advanced"));
assert!(config.features.has_feature("full"));
assert_eq!(config.dependencies.len(), 2);
assert!(config.dependencies[0].optional);
assert!(config.dependencies[1].optional);
}
#[test]
fn test_features_resolution() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[features]
default = ["std"]
std = []
networking = ["http-client"]
advanced = ["networking"]
full = ["advanced", "logging"]
logging = []
"#;
let config = CcgoConfig::parse(toml).unwrap();
let resolved = config
.features
.resolve_features(&["networking".to_string()], false)
.unwrap();
assert!(resolved.contains("networking"));
assert!(resolved.contains("http-client"));
assert!(!resolved.contains("std"));
let resolved = config
.features
.resolve_features(&["networking".to_string()], true)
.unwrap();
assert!(resolved.contains("networking"));
assert!(resolved.contains("std"));
let resolved = config
.features
.resolve_features(&["advanced".to_string()], false)
.unwrap();
assert!(resolved.contains("advanced"));
assert!(resolved.contains("networking"));
assert!(resolved.contains("http-client"));
let resolved = config
.features
.resolve_features(&["full".to_string()], false)
.unwrap();
assert!(resolved.contains("full"));
assert!(resolved.contains("advanced"));
assert!(resolved.contains("networking"));
assert!(resolved.contains("logging"));
assert!(resolved.contains("http-client"));
}
#[test]
fn test_features_unknown_feature_error() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[features]
std = []
"#;
let config = CcgoConfig::parse(toml).unwrap();
let result = config
.features
.resolve_features(&["unknown".to_string()], false);
assert!(result.is_err());
}
#[test]
fn test_optional_dependency_filtering() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[features]
networking = ["http-client"]
[[dependencies]]
name = "fmt"
version = "^10.0"
[[dependencies]]
name = "http-client"
version = "^1.0"
optional = true
[[dependencies]]
name = "unused-optional"
version = "^1.0"
optional = true
"#;
let config = CcgoConfig::parse(toml).unwrap();
let resolved = config.features.resolve_features(&[], false).unwrap();
let enabled_deps = config
.features
.get_enabled_optional_deps(&resolved, &config.dependencies);
assert_eq!(enabled_deps.len(), 1);
assert_eq!(enabled_deps[0].name, "fmt");
let resolved = config
.features
.resolve_features(&["networking".to_string()], false)
.unwrap();
let enabled_deps = config
.features
.get_enabled_optional_deps(&resolved, &config.dependencies);
assert_eq!(enabled_deps.len(), 2);
let names: Vec<_> = enabled_deps.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"fmt"));
assert!(names.contains(&"http-client"));
assert!(!names.contains(&"unused-optional"));
}
#[test]
fn test_dependency_features() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[[dependencies]]
name = "serde"
version = "^1.0"
features = ["derive", "std"]
default_features = false
"#;
let config = CcgoConfig::parse(toml).unwrap();
assert_eq!(config.dependencies[0].features, vec!["derive", "std"]);
assert_eq!(config.dependencies[0].default_features, Some(false));
}
#[test]
fn test_dependency_feature_syntax() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[features]
derive = ["serde/derive"]
"#;
let config = CcgoConfig::parse(toml).unwrap();
let resolved = config
.features
.resolve_features(&["derive".to_string()], false)
.unwrap();
assert!(resolved.contains("derive"));
assert!(resolved.contains("serde/derive"));
}
#[test]
fn test_parse_workspace_config() {
let toml = r#"
[workspace]
members = ["core", "utils", "examples/*"]
exclude = ["examples/deprecated"]
resolver = "2"
[[workspace.dependencies]]
name = "fmt"
version = "^10.0"
git = "https://github.com/fmtlib/fmt.git"
[[workspace.dependencies]]
name = "spdlog"
version = "^1.12"
"#;
let config = CcgoConfig::parse(toml).unwrap();
assert!(config.is_workspace());
assert!(!config.is_package());
let workspace = config.workspace.as_ref().unwrap();
assert_eq!(workspace.members, vec!["core", "utils", "examples/*"]);
assert_eq!(workspace.exclude, vec!["examples/deprecated"]);
assert_eq!(workspace.resolver, "2");
assert_eq!(workspace.dependencies.len(), 2);
assert_eq!(workspace.dependencies[0].name, "fmt");
assert_eq!(workspace.dependencies[1].name, "spdlog");
}
#[test]
fn test_parse_workspace_with_package() {
let toml = r#"
[workspace]
members = ["crates/*"]
[package]
name = "my-workspace"
version = "1.0.0"
"#;
let config = CcgoConfig::parse(toml).unwrap();
assert!(config.is_workspace());
assert!(config.is_package());
let package = config.package.as_ref().unwrap();
assert_eq!(package.name, "my-workspace");
}
#[test]
fn test_workspace_dependency_inheritance() {
let ws_toml = r#"
[workspace]
members = ["core"]
[[workspace.dependencies]]
name = "fmt"
version = "^10.0"
git = "https://github.com/fmtlib/fmt.git"
features = ["std"]
"#;
let member_toml = r#"
[package]
name = "my-core"
version = "1.0.0"
[[dependencies]]
name = "fmt"
workspace = true
features = ["extra"]
"#;
let ws_config = CcgoConfig::parse(ws_toml).unwrap();
let mut member_config = CcgoConfig::parse(member_toml).unwrap();
assert!(member_config.dependencies[0].workspace);
assert!(member_config.dependencies[0].version.is_empty());
member_config
.resolve_workspace_dependencies(&ws_config)
.unwrap();
let dep = &member_config.dependencies[0];
assert_eq!(dep.version, "^10.0");
assert_eq!(
dep.git.as_ref().unwrap(),
"https://github.com/fmtlib/fmt.git"
);
assert!(dep.features.contains(&"std".to_string()));
assert!(dep.features.contains(&"extra".to_string()));
}
#[test]
fn test_workspace_dependency_not_found() {
let ws_toml = r#"
[workspace]
members = ["core"]
[[workspace.dependencies]]
name = "fmt"
version = "^10.0"
"#;
let member_toml = r#"
[package]
name = "my-core"
version = "1.0.0"
[[dependencies]]
name = "nonexistent"
workspace = true
"#;
let ws_config = CcgoConfig::parse(ws_toml).unwrap();
let mut member_config = CcgoConfig::parse(member_toml).unwrap();
let result = member_config.resolve_workspace_dependencies(&ws_config);
assert!(result.is_err());
}
#[test]
fn test_config_requires_package_or_workspace() {
let toml = r#"
[build]
parallel = true
"#;
let result = CcgoConfig::parse(toml);
assert!(result.is_err());
}
#[test]
fn test_parse_patch_config() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[[dependencies]]
name = "fmt"
version = "^10.0"
git = "https://github.com/fmtlib/fmt.git"
[patch.crates-io]
fmt = { git = "https://github.com/myorg/fmt.git", branch = "custom-fix" }
[patch."https://github.com/spdlog/spdlog"]
spdlog = { path = "../spdlog-local" }
"#;
let config = CcgoConfig::parse(toml).unwrap();
assert!(config.patch.has_patches());
assert_eq!(config.patch.patched_dependencies(), vec!["fmt", "spdlog"]);
let fmt_patch = config.patch.find_patch("fmt", None).unwrap();
assert_eq!(
fmt_patch.git.as_ref().unwrap(),
"https://github.com/myorg/fmt.git"
);
assert_eq!(fmt_patch.branch.as_ref().unwrap(), "custom-fix");
let spdlog_patch = config
.patch
.find_patch("spdlog", Some("https://github.com/spdlog/spdlog"))
.unwrap();
assert_eq!(spdlog_patch.path.as_ref().unwrap(), "../spdlog-local");
}
#[test]
fn test_patch_priority() {
let toml = r#"
[package]
name = "mylib"
version = "1.0.0"
[patch.crates-io]
fmt = { git = "https://github.com/fallback/fmt.git" }
[patch."https://github.com/fmtlib/fmt.git"]
fmt = { path = "../fmt-local" }
"#;
let config = CcgoConfig::parse(toml).unwrap();
let fmt_with_source = config
.patch
.find_patch("fmt", Some("https://github.com/fmtlib/fmt.git"))
.unwrap();
assert!(fmt_with_source.path.is_some());
let fmt_without_source = config.patch.find_patch("fmt", None).unwrap();
assert!(fmt_without_source.git.is_some());
}
#[test]
fn test_dependency_zip_field() {
let toml_str = r#"
[package]
name = "myproject"
version = "1.0.0"
[[dependencies]]
name = "foundrycomm"
version = "1.0.0"
zip = "https://cdn.example.com/foundrycomm_CCGO_PACKAGE-1.0.0.zip"
"#;
let config: CcgoConfig = toml::from_str(toml_str).unwrap();
let dep = &config.dependencies[0];
assert_eq!(dep.name, "foundrycomm");
assert_eq!(
dep.zip.as_deref(),
Some("https://cdn.example.com/foundrycomm_CCGO_PACKAGE-1.0.0.zip")
);
assert!(dep.git.is_none());
assert!(dep.path.is_none());
}
#[test]
fn test_dependency_zip_field_absent() {
let toml_str = r#"
[package]
name = "myproject"
version = "1.0.0"
[[dependencies]]
name = "somelib"
version = "1.0.0"
git = "https://github.com/example/somelib.git"
"#;
let config: CcgoConfig = toml::from_str(toml_str).unwrap();
let dep = &config.dependencies[0];
assert!(dep.zip.is_none());
}
#[test]
fn parses_dependency_linkage_shared_external() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[[dependencies]]
name = "stdcomm"
version = "1.0.0"
linkage = "shared-external"
"#;
let cfg: CcgoConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.dependencies[0].linkage, Some(Linkage::SharedExternal));
}
#[test]
fn parses_dependency_linkage_static_embedded() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[[dependencies]]
name = "stdcomm"
version = "1.0.0"
linkage = "static-embedded"
"#;
let cfg: CcgoConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.dependencies[0].linkage, Some(Linkage::StaticEmbedded));
}
#[test]
fn parses_dependency_linkage_static_external() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[[dependencies]]
name = "stdcomm"
version = "1.0.0"
linkage = "static-external"
"#;
let cfg: CcgoConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.dependencies[0].linkage, Some(Linkage::StaticExternal));
}
#[test]
fn rejects_dependency_linkage_shared_embedded() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[[dependencies]]
name = "stdcomm"
version = "1.0.0"
linkage = "shared-embedded"
"#;
let err = toml::from_str::<CcgoConfig>(toml).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("shared-embedded") && msg.contains("invalid"),
"expected error mentioning shared-embedded as invalid, got: {msg}"
);
}
#[test]
fn rejects_dependency_linkage_garbage() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[[dependencies]]
name = "stdcomm"
version = "1.0.0"
linkage = "wibble"
"#;
let err = toml::from_str::<CcgoConfig>(toml).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("wibble") && msg.contains("not recognized"),
"expected error mentioning 'wibble' as unrecognized, got: {msg}"
);
}
#[test]
fn parses_build_default_dep_linkage() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[build]
default_dep_linkage = "shared-external"
"#;
let cfg: CcgoConfig = toml::from_str(toml).unwrap();
let build = cfg.build.expect("build section missing");
assert_eq!(build.default_dep_linkage, Some(Linkage::SharedExternal));
}
#[test]
fn dependency_linkage_defaults_to_none() {
let toml = r#"
[package]
name = "x"
version = "0.1.0"
[[dependencies]]
name = "stdcomm"
version = "1.0.0"
"#;
let cfg: CcgoConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.dependencies[0].linkage, None);
}
}