diff --git a/Cargo.lock b/Cargo.lock index 9ea1a8291e9..0444eec8f68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,7 @@ dependencies = [ "os_info", "pasetors", "pathdiff", + "pkg-config", "rand", "regex", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index ca281d1c1d0..49f09487dda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,6 +191,7 @@ opener.workspace = true os_info.workspace = true pasetors.workspace = true pathdiff.workspace = true +pkg-config.workspace = true rand.workspace = true regex.workspace = true rusqlite.workspace = true diff --git a/crates/cargo-util-schemas/manifest.schema.json b/crates/cargo-util-schemas/manifest.schema.json index ec63f68ee0c..bdd2a42c106 100644 --- a/crates/cargo-util-schemas/manifest.schema.json +++ b/crates/cargo-util-schemas/manifest.schema.json @@ -148,6 +148,15 @@ "$ref": "#/$defs/InheritableDependency" } }, + "pkgconfig-dependencies": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/TomlPkgConfigDependency" + } + }, "target": { "type": [ "object", @@ -922,6 +931,103 @@ "workspace" ] }, + "TomlPkgConfigDependency": { + "description": "A pkg-config dependency specification", + "anyOf": [ + { + "description": "In the simple format, only a version is specified, eg.\n`libfoo = \"1.2\"`", + "type": "string" + }, + { + "description": "The simple format is equivalent to a detailed dependency\nspecifying only a version, eg.\n`libfoo = { version = \"1.2\" }`", + "$ref": "#/$defs/TomlPkgConfigDetailedDependency" + } + ] + }, + "TomlPkgConfigDetailedDependency": { + "type": "object", + "properties": { + "version": { + "type": [ + "string", + "null" + ] + }, + "names": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "optional": { + "type": [ + "boolean", + "null" + ] + }, + "feature": { + "type": [ + "string", + "null" + ] + }, + "link": { + "type": [ + "string", + "null" + ] + }, + "fallback": { + "anyOf": [ + { + "$ref": "#/$defs/TomlPkgConfigFallback" + }, + { + "type": "null" + } + ] + } + } + }, + "TomlPkgConfigFallback": { + "description": "Fallback specification for pkg-config dependencies\nUsed when pkg-config query fails", + "type": "object", + "properties": { + "libs": { + "description": "Library names to link (-l flags)", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "lib-paths": { + "description": "Library search paths (-L flags)", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "include-paths": { + "description": "Include search paths (-I flags)", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, "TomlPlatform": { "description": "Corresponds to a `target` entry, but `TomlTarget` is already used.", "type": "object", @@ -970,6 +1076,15 @@ "additionalProperties": { "$ref": "#/$defs/InheritableDependency" } + }, + "pkgconfig-dependencies": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/TomlPkgConfigDependency" + } } } }, diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index bca5e2362c4..0e61d11fdbe 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -54,6 +54,7 @@ pub struct TomlManifest { pub build_dependencies: Option>, #[serde(rename = "build_dependencies")] pub build_dependencies2: Option>, + pub pkgconfig_dependencies: Option>, pub target: Option>, pub lints: Option, pub hints: Option, @@ -84,6 +85,9 @@ impl TomlManifest { self.build_dependencies() .as_ref() .map(|_| "build-dependencies"), + self.pkgconfig_dependencies + .as_ref() + .map(|_| "pkgconfig-dependencies"), self.target.as_ref().map(|_| "target"), self.lints.as_ref().map(|_| "lints"), self.hints.as_ref().map(|_| "hints"), @@ -899,6 +903,135 @@ impl Default for TomlDetailedDependency

{ } } +/// A pkg-config dependency specification +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] +pub enum TomlPkgConfigDependency { + /// In the simple format, only a version is specified, eg. + /// `libfoo = "1.2"` + Simple(String), + /// The simple format is equivalent to a detailed dependency + /// specifying only a version, eg. + /// `libfoo = { version = "1.2" }` + Detailed(TomlPkgConfigDetailedDependency), +} + +impl TomlPkgConfigDependency { + pub fn version_constraint(&self) -> Option<&str> { + match self { + TomlPkgConfigDependency::Simple(v) => Some(v), + TomlPkgConfigDependency::Detailed(d) => d.version.as_deref(), + } + } + + pub fn names(&self) -> Option<&[String]> { + match self { + TomlPkgConfigDependency::Simple(_) => None, + TomlPkgConfigDependency::Detailed(d) => d.names.as_deref(), + } + } + + pub fn is_optional(&self) -> bool { + match self { + TomlPkgConfigDependency::Simple(_) => false, + TomlPkgConfigDependency::Detailed(d) => d.optional.unwrap_or(false), + } + } + + pub fn feature(&self) -> Option<&str> { + match self { + TomlPkgConfigDependency::Simple(_) => None, + TomlPkgConfigDependency::Detailed(d) => d.feature.as_deref(), + } + } + + pub fn link(&self) -> Option<&str> { + match self { + TomlPkgConfigDependency::Simple(_) => None, + TomlPkgConfigDependency::Detailed(d) => d.link.as_deref(), + } + } + + pub fn fallback(&self) -> Option<&TomlPkgConfigFallback> { + match self { + TomlPkgConfigDependency::Simple(_) => None, + TomlPkgConfigDependency::Detailed(d) => d.fallback.as_ref(), + } + } + + pub fn unused_keys(&self) -> Vec { + match self { + TomlPkgConfigDependency::Simple(_) => vec![], + TomlPkgConfigDependency::Detailed(detailed) => { + detailed._unused_keys.keys().cloned().collect() + } + } + } +} + +impl<'de> de::Deserialize<'de> for TomlPkgConfigDependency { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let expected = "a version string like \"1.2\" or a \ + detailed dependency like { version = \"1.2\" }"; + UntaggedEnumVisitor::new() + .expecting(expected) + .string(|value| Ok(TomlPkgConfigDependency::Simple(value.to_owned()))) + .map(|value| value.deserialize().map(TomlPkgConfigDependency::Detailed)) + .deserialize(deserializer) + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] +pub struct TomlPkgConfigDetailedDependency { + pub version: Option, + pub names: Option>, + pub optional: Option, + pub feature: Option, + pub link: Option, + pub fallback: Option, + + /// This is here to provide a way to see the "unused manifest keys" when deserializing + #[serde(skip_serializing)] + #[serde(flatten)] + #[cfg_attr(feature = "unstable-schema", schemars(skip))] + pub _unused_keys: BTreeMap, +} + +// Explicit implementation so we avoid pulling in default from flatten +impl Default for TomlPkgConfigDetailedDependency { + fn default() -> Self { + Self { + version: Default::default(), + names: Default::default(), + optional: Default::default(), + feature: Default::default(), + link: Default::default(), + fallback: Default::default(), + _unused_keys: Default::default(), + } + } +} + +/// Fallback specification for pkg-config dependencies +/// Used when pkg-config query fails +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] +pub struct TomlPkgConfigFallback { + /// Library names to link (-l flags) + pub libs: Option>, + /// Library search paths (-L flags) + pub lib_paths: Option>, + /// Include search paths (-I flags) + pub include_paths: Option>, +} + #[derive(Deserialize, Serialize, Clone, Debug, Default)] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] pub struct TomlProfiles(pub BTreeMap); @@ -1517,6 +1650,7 @@ pub struct TomlPlatform { pub dev_dependencies: Option>, #[serde(rename = "dev_dependencies")] pub dev_dependencies2: Option>, + pub pkgconfig_dependencies: Option>, } impl TomlPlatform { diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 84093ff0a96..122c9e5b087 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -44,6 +44,7 @@ mod links; mod lto; mod output_depinfo; mod output_sbom; +pub mod pkgconfig; pub mod rustdoc; pub mod standard_lib; mod timings; diff --git a/src/cargo/core/compiler/pkgconfig.rs b/src/cargo/core/compiler/pkgconfig.rs new file mode 100644 index 00000000000..00a299c28a1 --- /dev/null +++ b/src/cargo/core/compiler/pkgconfig.rs @@ -0,0 +1,967 @@ +//! Support for pkg-config dependencies declared in Cargo.toml +//! +//! This module handles querying system pkg-config for dependencies declared +//! in the `[pkgconfig-dependencies]` section and generating metadata. +//! +//! # Usage +//! +//! Declare pkgconfig dependencies in Cargo.toml under an unstable feature flag: +//! +//! ```toml +//! # Requires: cargo build -Z pkgconfig-dependencies +//! +//! [pkgconfig-dependencies] +//! # Simple form: just version constraint +//! openssl = "1.1" +//! +//! # Detailed form with additional options +//! [pkgconfig-dependencies.sqlite3] +//! version = "3.0" +//! # Try alternative pkg-config names +//! names = ["sqlite3", "sqlite"] +//! # Mark as optional - doesn't fail build if not found +//! optional = true +//! # Fallback specification if pkg-config fails +//! [pkgconfig-dependencies.sqlite3.fallback] +//! libs = ["sqlite3"] +//! lib-paths = ["/usr/local/lib"] +//! include-paths = ["/usr/local/include"] +//! ``` +//! +//! # Generated Metadata +//! +//! The module generates `OUT_DIR/pkgconfig_meta.rs` with compile-time constants: +//! +//! ```ignore +//! pub mod pkgconfig { +//! pub mod openssl { +//! pub const VERSION: &str = "1.1.1"; +//! pub const FOUND: bool = true; +//! pub const RESOLVED_VIA: &str = "pkg-config"; +//! pub const INCLUDE_PATHS: &[&str] = &["/usr/include"]; +//! pub const LIB_PATHS: &[&str] = &["/usr/lib"]; +//! pub const LIBS: &[&str] = &["ssl", "crypto"]; +//! // ... and more fields +//! } +//! } +//! ``` +//! +//! # Accessing Metadata +//! +//! In your build script or build-time code: +//! +//! ```ignore +//! include!(concat!(env!("OUT_DIR"), "/pkgconfig_meta.rs")); +//! +//! fn main() { +//! let version = pkgconfig::openssl::VERSION; +//! let is_found = pkgconfig::openssl::FOUND; +//! +//! if is_found { +//! for lib in pkgconfig::openssl::LIBS { +//! println!("cargo:rustc-link-lib={}", lib); +//! } +//! } +//! } +//! ``` + +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use anyhow::{Context as _, bail}; +use cargo_util_schemas::manifest::{TomlPkgConfigDependency, TomlPkgConfigFallback}; +use tracing::warn; + +use crate::util::errors::CargoResult; + +/// Information about a resolved pkg-config dependency +#[derive(Debug, Clone)] +pub struct PkgConfigLibrary { + /// The name used to query pkg-config + pub name: String, + /// Version string from pkg-config, or empty if not available + pub version: String, + /// How the dependency was resolved (pkg-config, fallback, not-found, etc.) + pub resolved_via: ResolutionMethod, + /// Include paths (-I flags) + pub include_paths: Vec, + /// Library search paths (-L flags) + pub lib_paths: Vec, + /// Library names to link (-l flags) + pub libs: Vec, + /// Other compiler flags (excluding -I and -D) + pub cflags: Vec, + /// Preprocessor defines (-D flags) + pub defines: Vec, + /// Other linker flags (excluding -L and -l) + pub ldflags: Vec, + /// Raw output from pkg-config --cflags + pub raw_cflags: String, + /// Raw output from pkg-config --libs + pub raw_ldflags: String, + /// How to link this library (e.g., "static", "dynamic") + pub link_type: Option, +} + +/// How a pkg-config dependency was resolved +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolutionMethod { + /// Successfully found via pkg-config + PkgConfig, + /// Used fallback specification + Fallback, + /// Not found and optional=true + NotFound, + /// Not probed because feature was not enabled + NotProbed, +} + +impl ResolutionMethod { + pub fn as_str(&self) -> &'static str { + match self { + ResolutionMethod::PkgConfig => "pkg-config", + ResolutionMethod::Fallback => "fallback", + ResolutionMethod::NotFound => "not-found", + ResolutionMethod::NotProbed => "not-probed", + } + } +} + +/// Sanitize a package name to be a valid Rust module name +pub fn sanitize_module_name(name: &str) -> String { + let mut result = String::new(); + + // If starts with digit, prepend "lib_" + if name.chars().next().map_or(false, |c| c.is_ascii_digit()) { + result.push_str("lib_"); + } + + for ch in name.chars() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => result.push(ch), + '-' | '.' | '+' => result.push('_'), + _ => { + // Skip other characters or convert to underscore + result.push('_'); + } + } + } + + result.to_lowercase() +} + +/// Generate Rust code for a single dependency's metadata +fn generate_dep_module(name: &str, lib: &PkgConfigLibrary) -> String { + let module_name = sanitize_module_name(name); + + // Format arrays for Rust code (helper for potential future use) + let _format_str_array = |items: &[String]| -> String { + let strs: Vec<_> = items.iter().map(|s| format!("\"{}\"", s)).collect(); + format!("&[{}]", strs.join(", ")) + }; + + format!( + r#" /// Metadata for pkgconfig dependency: {} + pub mod {} {{ + /// Version from pkg-config --modversion + pub const VERSION: &str = "{}"; + + /// Successfully resolved + pub const FOUND: bool = {}; + + /// Resolution method + pub const RESOLVED_VIA: &str = "{}"; + + /// Include paths from --cflags-only-I + pub const INCLUDE_PATHS: &[&str] = &{:?}; + + /// Library paths from --libs-only-L + pub const LIB_PATHS: &[&str] = &{:?}; + + /// Libraries from --libs-only-l + pub const LIBS: &[&str] = &{:?}; + + /// Other compiler flags from --cflags-only-other + pub const CFLAGS: &[&str] = &{:?}; + + /// Defines from -D flags + pub const DEFINES: &[&str] = &{:?}; + + /// Other linker flags from --libs-only-other + pub const LDFLAGS: &[&str] = &{:?}; + + /// Raw pkg-config --cflags output + pub const RAW_CFLAGS: &str = "{}"; + + /// Raw pkg-config --libs output + pub const RAW_LDFLAGS: &str = "{}"; + + /// How to link this library (e.g., "static", "dynamic") + pub const LINK_TYPE: Option<&str> = {}; + }} +"#, + name, + module_name, + lib.version, + lib.resolved_via != ResolutionMethod::NotFound + && lib.resolved_via != ResolutionMethod::NotProbed, + lib.resolved_via.as_str(), + lib.include_paths + .iter() + .map(|s| s.as_str()) + .collect::>(), + lib.lib_paths.iter().map(|s| s.as_str()).collect::>(), + lib.libs.iter().map(|s| s.as_str()).collect::>(), + lib.cflags.iter().map(|s| s.as_str()).collect::>(), + lib.defines.iter().map(|s| s.as_str()).collect::>(), + lib.ldflags.iter().map(|s| s.as_str()).collect::>(), + lib.raw_cflags.replace('\\', "\\\\").replace('"', "\\\""), + lib.raw_ldflags.replace('\\', "\\\\").replace('"', "\\\""), + match &lib.link_type { + Some(lt) => format!("Some(\"{}\")", lt), + None => "None".to_string(), + }, + ) +} + +/// Generate the pkgconfig_meta.rs file content +/// +/// Creates a Rust module with compile-time constants for each dependency. +/// +/// # Output Structure +/// +/// For each dependency, creates a module like: +/// ```ignore +/// pub mod pkgconfig { +/// pub mod dependency_name { +/// pub const VERSION: &str = "1.0.0"; +/// pub const FOUND: bool = true; +/// pub const RESOLVED_VIA: &str = "pkg-config"; +/// pub const INCLUDE_PATHS: &[&str] = &["/usr/include", ...]; +/// pub const LIB_PATHS: &[&str] = &["/usr/lib", ...]; +/// pub const LIBS: &[&str] = &["lib1", "lib2"]; +/// pub const CFLAGS: &[&str] = &[...]; +/// pub const DEFINES: &[&str] = &[...]; +/// pub const LDFLAGS: &[&str] = &[...]; +/// pub const RAW_CFLAGS: &str = "-I/usr/include ..."; +/// pub const RAW_LDFLAGS: &str = "-L/usr/lib -llib1 -llib2"; +/// } +/// } +/// ``` +/// +/// Module names are sanitized for Rust identifier rules (special chars become underscores). +pub fn generate_metadata_file(libraries: &BTreeMap) -> String { + let mut modules = String::new(); + + for (name, lib) in libraries.iter() { + modules.push_str(&generate_dep_module(name, lib)); + } + + format!( + r#"// Auto-generated by Cargo from [pkgconfig-dependencies] +// DO NOT EDIT - regenerated on each build + +#![allow(dead_code, non_upper_case_globals)] + +/// Package metadata for pkgconfig-dependencies +pub mod pkgconfig {{ +{}}} +"#, + modules + ) +} + +/// Apply fallback specification as a PkgConfigLibrary +fn apply_fallback( + name: &str, + fallback: &TomlPkgConfigFallback, + link_type: Option<&str>, +) -> PkgConfigLibrary { + let libs = fallback + .libs + .as_ref() + .map(|v| v.clone()) + .unwrap_or_default(); + let include_paths = fallback + .include_paths + .as_ref() + .map(|v| v.clone()) + .unwrap_or_default(); + let lib_paths = fallback + .lib_paths + .as_ref() + .map(|v| v.clone()) + .unwrap_or_default(); + + // Reconstruct raw flags from fallback + let raw_cflags = { + let mut parts = Vec::new(); + for path in &include_paths { + parts.push(format!("-I{}", path)); + } + parts.join(" ") + }; + + let raw_ldflags = { + let mut parts = Vec::new(); + for path in &lib_paths { + parts.push(format!("-L{}", path)); + } + for lib in &libs { + parts.push(format!("-l{}", lib)); + } + parts.join(" ") + }; + + PkgConfigLibrary { + name: name.to_string(), + version: String::new(), + resolved_via: ResolutionMethod::Fallback, + include_paths, + lib_paths, + libs, + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags, + raw_ldflags, + link_type: link_type.map(|s| s.to_string()), + } +} + +/// Parse and apply version constraints to pkg-config config +fn apply_version_constraint(config: &mut pkg_config::Config, constraint: &str) -> CargoResult<()> { + let constraint = constraint.trim(); + + // Exact version: "= 3.0" + if let Some(version) = constraint.strip_prefix('=') { + config.exactly_version(version.trim()); + } + // Range: "3.0 .. 4.0" or "3.0..4.0" + else if constraint.contains("..") { + let parts: Vec<&str> = constraint.split("..").collect(); + if parts.len() == 2 { + let min = parts[0].trim(); + let max = parts[1].trim(); + config.range_version(min..max); + } else { + bail!("invalid version range constraint: {}", constraint); + } + } + // At least version: ">= 3.0" or just "3.0" (default) + else if let Some(version) = constraint.strip_prefix(">=") { + config.atleast_version(version.trim()); + } else { + // Default: treat as minimum version + config.atleast_version(constraint); + } + + Ok(()) +} + +/// Query pkg-config for a single dependency by name +fn query_pkg_config_by_name( + name: &str, + version_constraint: &str, +) -> CargoResult { + let mut config = pkg_config::Config::new(); + apply_version_constraint(&mut config, version_constraint)?; + config.probe(name).map_err(|e| anyhow::anyhow!("{}", e)) +} + +/// Query pkg-config for a single dependency +/// +/// # Resolution Strategy +/// +/// 1. First tries the primary package name +/// 2. If that fails, tries each alternative name in order (if provided) +/// 3. If all pkg-config queries fail and a fallback is provided, uses the fallback spec +/// 4. Returns the first successful match +/// +/// # Arguments +/// +/// * `name` - Primary package name for this dependency +/// * `version_constraint` - Minimum version constraint (passed to pkg-config) +/// * `alternative_names` - Optional list of alternative pkg-config names to try +/// * `fallback` - Optional fallback specification for manual configuration +/// +/// # Returns +/// +/// Returns `PkgConfigLibrary` with resolved metadata if found via pkg-config or fallback. +/// Returns `Err` with detailed error message if all resolution methods fail. +/// +/// # Errors +/// +/// Returns error if: +/// - All pkg-config names fail AND no fallback is provided +/// - The fallback spec itself is invalid +pub fn query_pkg_config( + name: &str, + version_constraint: &str, + alternative_names: Option<&[String]>, + fallback: Option<&TomlPkgConfigFallback>, + link_type: Option<&str>, +) -> CargoResult { + // Collect all names to try, with the primary name first + let mut names_to_try = vec![name.to_string()]; + if let Some(alts) = alternative_names { + names_to_try.extend(alts.iter().cloned()); + } + + // Try each name in order + let mut last_error = None; + for try_name in names_to_try.iter() { + match query_pkg_config_by_name(try_name, version_constraint) { + Ok(lib) => { + let include_paths = lib + .include_paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect::>(); + + let lib_paths = lib + .link_paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect::>(); + + let libs = lib.libs.clone(); + + // Reconstruct raw cflags and ldflags for debugging output + let raw_cflags = { + let mut parts = Vec::new(); + for path in &include_paths { + parts.push(format!("-I{}", path)); + } + parts.join(" ") + }; + + let raw_ldflags = { + let mut parts = Vec::new(); + for path in &lib_paths { + parts.push(format!("-L{}", path)); + } + for lib in &libs { + parts.push(format!("-l{}", lib)); + } + parts.join(" ") + }; + + return Ok(PkgConfigLibrary { + name: name.to_string(), + version: lib.version.clone(), + resolved_via: ResolutionMethod::PkgConfig, + include_paths, + lib_paths, + libs, + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags, + raw_ldflags, + link_type: link_type.map(|s| s.to_string()), + }); + } + Err(e) => { + last_error = Some(e); + continue; + } + } + } + + // All names failed - try fallback if available + if let Some(fb) = fallback { + return Ok(apply_fallback(name, fb, link_type)); + } + + // No fallback available - report error with helpful suggestions + let mut error_msg = format!( + "pkg-config dependency `{}` (version {}) not found", + name, version_constraint + ); + + if names_to_try.len() > 1 { + error_msg.push_str(&format!( + "\n tried pkg-config names: {}", + names_to_try.join(", ") + )); + } + + if let Some(e) = last_error { + error_msg.push_str(&format!("\n error: {}", e)); + } + + error_msg.push_str(&format!( + "\n\nTo fix this, you can:\n\ + 1. Install the system library (e.g., libfoo-dev on Debian, libfoo-devel on Fedora)\n\ + 2. Set the PKG_CONFIG_PATH environment variable to include the directory with the .pc file\n\ + 3. Add a [fallback] specification in Cargo.toml to manually specify library paths\n\ + 4. Use alternative names via the `names` field if the package has multiple names" + )); + + bail!("{}", error_msg) +} + +/// Probe all pkgconfig dependencies for a package +/// +/// # Behavior +/// +/// - Skips dependencies that have a `feature` requirement that isn't enabled +/// - Probes remaining dependencies using pkg-config +/// - Tracks how each was resolved (pkg-config, fallback, not-found) +/// - Records the `link` field for each dependency's metadata +/// - For optional dependencies, records "not-found" state instead of erroring +pub fn probe_all_dependencies( + deps: &BTreeMap, + enabled_features: &[&str], +) -> CargoResult> { + let mut results = BTreeMap::new(); + + for (name, dep) in deps.iter() { + // Skip if this dependency is gated by a feature that's not enabled + if let Some(feature_name) = dep.feature() { + if !enabled_features.contains(&feature_name) { + // Record as not-probed since the feature isn't enabled + results.insert( + name.clone(), + PkgConfigLibrary { + name: name.clone(), + version: String::new(), + resolved_via: ResolutionMethod::NotProbed, + include_paths: Vec::new(), + lib_paths: Vec::new(), + libs: Vec::new(), + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags: String::new(), + raw_ldflags: String::new(), + link_type: None, + }, + ); + continue; + } + } + + let version_constraint = dep.version_constraint().unwrap_or("0"); + let alternative_names = dep.names(); + let is_optional = dep.is_optional(); + let fallback = dep.fallback(); + let link_type = dep.link(); + + match query_pkg_config( + name, + version_constraint, + alternative_names, + fallback, + link_type, + ) { + Ok(lib) => { + results.insert(name.clone(), lib); + } + Err(e) => { + if is_optional { + // For optional dependencies, insert a "not found" entry + // Log a warning so users know which optional dependency wasn't found + warn!( + "optional pkg-config dependency `{}` not found (ignoring): {}", + name, e + ); + + results.insert( + name.clone(), + PkgConfigLibrary { + name: name.clone(), + version: String::new(), + resolved_via: ResolutionMethod::NotFound, + include_paths: Vec::new(), + lib_paths: Vec::new(), + libs: Vec::new(), + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags: String::new(), + raw_ldflags: String::new(), + link_type: None, + }, + ); + } else { + // Required dependency not found - error + return Err(e); + } + } + } + } + + Ok(results) +} +/// Write the pkgconfig metadata file to OUT_DIR +pub fn write_metadata_file( + out_dir: &Path, + libraries: &BTreeMap, +) -> CargoResult<()> { + let content = generate_metadata_file(libraries); + let metadata_path = out_dir.join("pkgconfig_meta.rs"); + + fs::write(&metadata_path, content).with_context(|| { + format!( + "failed to write pkgconfig metadata to {}", + metadata_path.display() + ) + })?; + + Ok(()) +} + +/// Probe and write pkgconfig metadata for all dependencies +/// +/// This is the main entry point for integrating pkgconfig dependencies into the build. +/// It probes pkg-config for all declared dependencies and writes the metadata file. +/// +/// # Arguments +/// +/// * `deps` - Map of pkgconfig dependencies from the manifest +/// * `out_dir` - Output directory where `pkgconfig_meta.rs` will be written +/// * `enabled_features` - List of enabled Cargo features (for feature-gated dependencies) +/// +/// # Process +/// +/// 1. Skips dependencies gated by disabled features +/// 2. Probes each enabled dependency using pkg-config +/// 3. For each dependency, tries alternative names if specified +/// 4. Uses fallback specification if pkg-config fails +/// 5. Handles optional dependencies (doesn't fail build if not found) +/// 6. Generates Rust code with compile-time constants +/// 7. Writes `OUT_DIR/pkgconfig_meta.rs` for inclusion in build scripts +/// +/// # Returns +/// +/// Returns `Ok(())` if all required dependencies are found. +/// Returns `Err` if any required (non-optional) dependency is not found. +/// +/// # Panics +/// +/// Does not panic. All errors are returned as `CargoResult`. +pub fn probe_and_generate_metadata( + deps: &BTreeMap, + out_dir: &Path, + enabled_features: &[&str], +) -> CargoResult<()> { + if deps.is_empty() { + // No pkgconfig dependencies, write an empty module + let empty_metadata = r#"// Auto-generated by Cargo from [pkgconfig-dependencies] +// This package has no pkgconfig-dependencies + +#![allow(dead_code, non_upper_case_globals)] + +/// Package metadata for pkgconfig-dependencies +pub mod pkgconfig {} +"#; + let metadata_path = out_dir.join("pkgconfig_meta.rs"); + fs::write(&metadata_path, empty_metadata).with_context(|| { + format!( + "failed to write pkgconfig metadata to {}", + metadata_path.display() + ) + })?; + return Ok(()); + } + + let libraries = probe_all_dependencies(deps, enabled_features)?; + write_metadata_file(out_dir, &libraries)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_constraint_parsing() { + // Exact version + { + let mut config = pkg_config::Config::new(); + assert!(apply_version_constraint(&mut config, "= 3.0").is_ok()); + } + + // At least version with >= + { + let mut config = pkg_config::Config::new(); + assert!(apply_version_constraint(&mut config, ">= 3.0").is_ok()); + } + + // At least version (default) + { + let mut config = pkg_config::Config::new(); + assert!(apply_version_constraint(&mut config, "3.0").is_ok()); + } + + // Range + { + let mut config = pkg_config::Config::new(); + assert!(apply_version_constraint(&mut config, "3.0 .. 4.0").is_ok()); + } + + // Range without spaces + { + let mut config = pkg_config::Config::new(); + assert!(apply_version_constraint(&mut config, "3.0..4.0").is_ok()); + } + + // Invalid range + { + let mut config = pkg_config::Config::new(); + assert!(apply_version_constraint(&mut config, "3.0 .. 4.0 .. 5.0").is_err()); + } + } + + #[test] + fn test_sanitize_module_name() { + assert_eq!(sanitize_module_name("libfoo"), "libfoo"); + assert_eq!(sanitize_module_name("lib-foo"), "lib_foo"); + assert_eq!(sanitize_module_name("lib.foo"), "lib_foo"); + assert_eq!(sanitize_module_name("gtk+-3.0"), "gtk__3_0"); + assert_eq!(sanitize_module_name("3dlib"), "lib_3dlib"); + } + + #[test] + fn test_generate_metadata_file_empty() { + let deps = BTreeMap::new(); + let content = generate_metadata_file(&deps); + + assert!(content.contains("pub mod pkgconfig")); + assert!(content.contains("DO NOT EDIT")); + assert!(content.contains("Auto-generated")); + } + + #[test] + fn test_generate_metadata_file_with_libs() { + let mut deps = BTreeMap::new(); + deps.insert( + "test-lib".to_string(), + PkgConfigLibrary { + name: "test-lib".to_string(), + version: "1.0.0".to_string(), + resolved_via: ResolutionMethod::PkgConfig, + include_paths: vec!["/usr/include".to_string()], + lib_paths: vec!["/usr/lib".to_string()], + libs: vec!["testlib".to_string()], + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags: "-I/usr/include".to_string(), + raw_ldflags: "-L/usr/lib -ltestlib".to_string(), + link_type: None, + }, + ); + + let content = generate_metadata_file(&deps); + + assert!(content.contains("pub mod test_lib")); + assert!(content.contains("VERSION")); + assert!(content.contains("INCLUDE_PATHS")); + assert!(content.contains("pub const VERSION: &str = \"1.0.0\"")); + assert!(content.contains("1.0.0")); + } + + #[test] + fn test_resolution_method_as_str() { + assert_eq!(ResolutionMethod::PkgConfig.as_str(), "pkg-config"); + assert_eq!(ResolutionMethod::Fallback.as_str(), "fallback"); + assert_eq!(ResolutionMethod::NotFound.as_str(), "not-found"); + assert_eq!(ResolutionMethod::NotProbed.as_str(), "not-probed"); + } + + #[test] + fn test_sanitize_module_name_edge_cases() { + // Test with special characters + assert_eq!(sanitize_module_name("lib++"), "lib__"); + assert_eq!(sanitize_module_name("lib-3.0"), "lib_3_0"); + assert_eq!(sanitize_module_name("PKG_CONFIG"), "pkg_config"); + + // Test starting with digit + assert_eq!(sanitize_module_name("123"), "lib_123"); + assert_eq!(sanitize_module_name("2to3"), "lib_2to3"); + } + + #[test] + fn test_generate_metadata_file_module_naming() { + let mut deps = BTreeMap::new(); + + // Test various names that need sanitization + deps.insert( + "gtk+-3.0".to_string(), + PkgConfigLibrary { + name: "gtk+-3.0".to_string(), + version: "3.0".to_string(), + resolved_via: ResolutionMethod::PkgConfig, + include_paths: vec![], + lib_paths: vec![], + libs: vec![], + cflags: vec![], + defines: vec![], + ldflags: vec![], + raw_cflags: String::new(), + raw_ldflags: String::new(), + link_type: None, + }, + ); + + let content = generate_metadata_file(&deps); + + // Should sanitize the module name (+ becomes _, - becomes _, . becomes _) + assert!(content.contains("pub mod gtk__3_0")); + // But keep the original name in comments + assert!(content.contains("gtk+-3.0")); + } + + #[test] + fn test_pkgconfig_library_with_multiple_items() { + let mut libs = BTreeMap::new(); + + libs.insert( + "lib1".to_string(), + PkgConfigLibrary { + name: "lib1".to_string(), + version: "1.0".to_string(), + resolved_via: ResolutionMethod::PkgConfig, + include_paths: vec!["/usr/include/lib1".to_string()], + lib_paths: vec!["/usr/lib".to_string()], + libs: vec!["lib1".to_string()], + cflags: vec![], + defines: vec!["LIB1_ENABLED".to_string()], + ldflags: vec![], + raw_cflags: "-I/usr/include/lib1 -DLIB1_ENABLED".to_string(), + raw_ldflags: "-L/usr/lib -llib1".to_string(), + link_type: None, + }, + ); + + libs.insert( + "lib2".to_string(), + PkgConfigLibrary { + name: "lib2".to_string(), + version: "2.0".to_string(), + resolved_via: ResolutionMethod::PkgConfig, + include_paths: vec!["/usr/include/lib2".to_string()], + lib_paths: vec!["/usr/lib".to_string()], + libs: vec!["lib2".to_string()], + cflags: vec![], + defines: vec![], + ldflags: vec![], + raw_cflags: "-I/usr/include/lib2".to_string(), + raw_ldflags: "-L/usr/lib -llib2".to_string(), + link_type: None, + }, + ); + + let content = generate_metadata_file(&libs); + + // Should have both modules + assert!(content.contains("pub mod lib1")); + assert!(content.contains("pub mod lib2")); + // Each should have their own version + assert!(content.contains("pub const VERSION: &str = \"1.0\"")); + assert!(content.contains("pub const VERSION: &str = \"2.0\"")); + } + + #[test] + fn test_apply_fallback_creates_library() { + use cargo_util_schemas::manifest::TomlPkgConfigFallback; + + let fallback = TomlPkgConfigFallback { + libs: Some(vec!["mylib".to_string()]), + lib_paths: Some(vec!["/usr/local/lib".to_string()]), + include_paths: Some(vec!["/usr/local/include".to_string()]), + }; + + let lib = apply_fallback("mylib", &fallback, None); + + assert_eq!(lib.name, "mylib"); + assert_eq!(lib.libs, vec!["mylib"]); + assert_eq!(lib.lib_paths, vec!["/usr/local/lib"]); + assert_eq!(lib.include_paths, vec!["/usr/local/include"]); + assert_eq!(lib.resolved_via.as_str(), "fallback"); + assert!(lib.version.is_empty()); + assert_eq!(lib.link_type, None); + } + + #[test] + fn test_apply_fallback_empty_values() { + use cargo_util_schemas::manifest::TomlPkgConfigFallback; + + let fallback = TomlPkgConfigFallback { + libs: None, + lib_paths: None, + include_paths: None, + }; + + let lib = apply_fallback("test", &fallback, Some("static")); + + assert_eq!(lib.name, "test"); + assert!(lib.libs.is_empty()); + assert!(lib.lib_paths.is_empty()); + assert!(lib.include_paths.is_empty()); + assert_eq!(lib.resolved_via.as_str(), "fallback"); + assert_eq!(lib.link_type, Some("static".to_string())); + } + + #[test] + fn test_generate_metadata_with_fallback_resolution() { + let mut deps = BTreeMap::new(); + deps.insert( + "fallback-lib".to_string(), + PkgConfigLibrary { + name: "fallback-lib".to_string(), + version: String::new(), + resolved_via: ResolutionMethod::Fallback, + include_paths: vec!["/custom/include".to_string()], + lib_paths: vec!["/custom/lib".to_string()], + libs: vec!["customlib".to_string()], + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags: "-I/custom/include".to_string(), + raw_ldflags: "-L/custom/lib -lcustomlib".to_string(), + link_type: None, + }, + ); + + let content = generate_metadata_file(&deps); + + assert!(content.contains("pub mod fallback_lib")); + assert!(content.contains("\"fallback\"")); + assert!(content.contains("pub const RESOLVED_VIA: &str = \"fallback\"")); + assert!(content.contains("pub const FOUND: bool = true")); + } + + #[test] + fn test_generate_metadata_with_not_found_resolution() { + let mut deps = BTreeMap::new(); + deps.insert( + "missing-lib".to_string(), + PkgConfigLibrary { + name: "missing-lib".to_string(), + version: String::new(), + resolved_via: ResolutionMethod::NotFound, + include_paths: Vec::new(), + lib_paths: Vec::new(), + libs: Vec::new(), + cflags: Vec::new(), + defines: Vec::new(), + ldflags: Vec::new(), + raw_cflags: String::new(), + raw_ldflags: String::new(), + link_type: None, + }, + ); + + let content = generate_metadata_file(&deps); + + assert!(content.contains("pub mod missing_lib")); + assert!(content.contains("pub const FOUND: bool = false")); + assert!(content.contains("\"not-found\"")); + } +} diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index f9f13dc2951..2c509baec07 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -582,6 +582,9 @@ features! { /// Allows use of panic="immediate-abort". (unstable, panic_immediate_abort, "", "reference/unstable.html#panic-immediate-abort"), + + /// Allows use of [pkgconfig-dependencies] in Cargo.toml. + (unstable, pkgconfig_dependencies, "", "reference/unstable.html#pkgconfig-dependencies"), } /// Status and metadata for a single unstable feature. @@ -876,6 +879,7 @@ unstable_cli_options!( no_index_update: bool = ("Do not update the registry index even if the cache is outdated"), panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"), panic_immediate_abort: bool = ("Enable setting `panic = \"immediate-abort\"` in profiles"), + pkgconfig_dependencies: bool = ("Enable the `[pkgconfig-dependencies]` table in Cargo.toml"), profile_hint_mostly_unused: bool = ("Enable the `hint-mostly-unused` setting in profiles to mark a crate as mostly unused."), profile_rustflags: bool = ("Enable the `rustflags` option in profiles in .cargo/config.toml file"), public_dependency: bool = ("Respect a dependency's `public` field in Cargo.toml to control public/private dependencies"), diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index eb24127775e..53ec9d8501e 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -73,6 +73,8 @@ pub struct Manifest { default_kind: Option, forced_kind: Option, links: Option, + pkgconfig_dependencies: + Option>, warnings: Warnings, exclude: Vec, include: Vec, @@ -508,6 +510,9 @@ impl Manifest { exclude: Vec, include: Vec, links: Option, + pkgconfig_dependencies: Option< + BTreeMap, + >, metadata: ManifestMetadata, custom_metadata: Option, publish: Option>, @@ -539,6 +544,7 @@ impl Manifest { exclude, include, links, + pkgconfig_dependencies, metadata, custom_metadata, publish, @@ -646,6 +652,13 @@ impl Manifest { pub fn links(&self) -> Option<&str> { self.links.as_deref() } + + pub fn pkgconfig_dependencies( + &self, + ) -> Option<&BTreeMap> { + self.pkgconfig_dependencies.as_ref() + } + pub fn is_embedded(&self) -> bool { self.embedded } diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 2777b3fdd7b..1408abd8308 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -14,7 +14,7 @@ use cargo_platform::Platform; use cargo_util::paths; use cargo_util_schemas::manifest::{ self, PackageName, PathBaseName, TomlDependency, TomlDetailedDependency, TomlManifest, - TomlPackageBuild, TomlWorkspace, + TomlPackageBuild, TomlPkgConfigDependency, TomlWorkspace, }; use cargo_util_schemas::manifest::{RustVersion, StringOrBool}; use itertools::Itertools; @@ -325,6 +325,7 @@ fn normalize_toml( dev_dependencies2: None, build_dependencies: None, build_dependencies2: None, + pkgconfig_dependencies: None, target: None, lints: None, hints: None, @@ -493,6 +494,17 @@ fn normalize_toml( package_root, warnings, )?; + + normalized_toml.pkgconfig_dependencies = normalize_pkgconfig_dependencies( + original_toml.pkgconfig_dependencies.as_ref(), + warnings, + )?; + + // Check if pkgconfig-dependencies is used and require unstable feature + if normalized_toml.pkgconfig_dependencies.is_some() { + features.require(Feature::pkgconfig_dependencies())?; + } + let mut normalized_target = BTreeMap::new(); for (name, platform) in original_toml.target.iter().flatten() { let normalized_dependencies = normalize_dependencies( @@ -546,6 +558,10 @@ fn normalize_toml( package_root, warnings, )?; + let normalized_pkgconfig_dependencies = normalize_pkgconfig_dependencies( + platform.pkgconfig_dependencies.as_ref(), + warnings, + )?; normalized_target.insert( name.clone(), manifest::TomlPlatform { @@ -554,6 +570,7 @@ fn normalize_toml( build_dependencies2: None, dev_dependencies: normalized_dev_dependencies, dev_dependencies2: None, + pkgconfig_dependencies: normalized_pkgconfig_dependencies, }, ); } @@ -959,6 +976,38 @@ fn normalize_path_dependency<'a>( Ok(()) } +fn normalize_pkgconfig_dependencies( + orig_deps: Option<&BTreeMap>, + warnings: &mut Vec, +) -> CargoResult>> { + let Some(dependencies) = orig_deps else { + return Ok(None); + }; + + let mut deps = BTreeMap::new(); + for (name, dep) in dependencies.iter() { + // Validate that a version constraint is present + if dep.version_constraint().is_none() { + bail!( + "pkgconfig dependency `{}` must have a version constraint", + name + ); + } + + // Check for unused keys + for unused_key in dep.unused_keys() { + warnings.push(format!( + "unused manifest key in pkgconfig-dependencies.{}: `{}`", + name, unused_key + )); + } + + deps.insert(name.clone(), dep.clone()); + } + + Ok(Some(deps)) +} + fn load_inheritable_fields( gctx: &GlobalContext, normalized_path: &Path, @@ -1400,6 +1449,7 @@ pub fn to_real_manifest( profile: _, patch: _, replace: _, + pkgconfig_dependencies: _, _unused_keys: _, } = &original_toml; let mut invalid_fields = vec![ @@ -1809,6 +1859,7 @@ note: only a feature named `default` will be enabled by default" .cloned() .unwrap_or_default(); let links = normalized_package.links.clone(); + let pkgconfig_dependencies = normalized_toml.pkgconfig_dependencies.clone(); let custom_metadata = normalized_package.metadata.clone(); let im_a_teapot = normalized_package.im_a_teapot; let default_run = normalized_package.default_run.clone(); @@ -1825,6 +1876,7 @@ note: only a feature named `default` will be enabled by default" exclude, include, links, + pkgconfig_dependencies, metadata, custom_metadata, publish, @@ -3139,6 +3191,7 @@ fn prepare_toml_for_publish( dev_dependencies2: None, build_dependencies: map_deps(gctx, v.build_dependencies(), all)?, build_dependencies2: None, + pkgconfig_dependencies: None, }, )) }) @@ -3154,6 +3207,7 @@ fn prepare_toml_for_publish( profile: me.profile.clone(), patch: None, replace: None, + pkgconfig_dependencies: None, _unused_keys: Default::default(), }; strip_features(&mut manifest); diff --git a/src/doc/src/reference/manifest.md b/src/doc/src/reference/manifest.md index 5f1074f8ea5..a125aeea467 100644 --- a/src/doc/src/reference/manifest.md +++ b/src/doc/src/reference/manifest.md @@ -627,6 +627,138 @@ See the [specifying dependencies page](specifying-dependencies.md) for information on the `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`, and target-specific `[target.*.dependencies]` sections. +## The `[pkgconfig-dependencies]` section + +The `[pkgconfig-dependencies]` table (currently unstable) allows declaring +system library dependencies managed by pkg-config. This enables Cargo to +automatically query system libraries for compiler flags and link information. + +**Note:** This feature requires the `-Z pkgconfig-dependencies` unstable flag. +```toml +cargo build -Z pkgconfig-dependencies +``` + +### Simple form + +The simplest form specifies only a version constraint: + +```toml +[pkgconfig-dependencies] +openssl = "1.1" # At least version 1.1 +sqlite3 = ">= 3.0" # Explicit minimum version +zlib = "= 1.2.11" # Exact version +curl = "7.0 .. 8.0" # Version range +``` + +### Detailed form + +For more control, use the detailed form: + +```toml +[pkgconfig-dependencies.openssl] +version = "1.1" +# Try multiple pkg-config names (if library has different names on different systems) +names = ["openssl", "libssl"] +# Mark as optional - build continues if not found +optional = true +# Fallback specification for when pkg-config fails +[pkgconfig-dependencies.openssl.fallback] +libs = ["ssl", "crypto"] +lib-paths = ["/usr/local/lib"] +include-paths = ["/usr/local/include"] +``` + +### Version Constraints + +The `version` field supports three constraint types: + +```toml +[pkgconfig-dependencies] +openssl = "1.1" # Minimum version (at least 1.1) +sqlite = ">= 3.0" # Explicit minimum (same as above) +zlib = "= 1.2.11" # Exact version +curl = "7.0 .. 8.0" # Version range (7.x to 8.x) +``` + +**Constraint types:** +- `1.1` or `>= 1.1` — Require at least this version +- `= 1.1` — Require exactly this version +- `3.0 .. 4.0` — Require version between 3.0 and 4.0 (inclusive) + +### Fields + +* `version` — Version constraint for the pkg-config library. + Supports exact version (`= 3.0`), minimum version (`>= 3.0` or `3.0`), + and version ranges (`3.0 .. 4.0`). If not specified, any version is accepted. + +* `names` — Alternative pkg-config package names to try. If the primary name + (from the table key) fails, these names are tried in order. Useful when + a library has different names on different systems (e.g., `sqlite3` vs + `sqlite`). + +* `optional` — If `true`, the build continues even if the library is not + found. Users can check the `FOUND` constant in the generated metadata. + Default is `false` (required). + +* `feature` — (Reserved for future use) Can be used to conditionally include + the dependency based on a Cargo feature. + +* `link` — (Reserved for future use) Specifies how to link the library. + +* `fallback` — Manual specification of compiler and linker flags for when + pkg-config fails. Contains: + * `libs` — Library names to link (e.g., `["ssl", "crypto"]`) + * `lib-paths` — Directories to search for libraries (e.g., `["/usr/lib"]`) + * `include-paths` — Directories to search for headers (e.g., `["/usr/include"]`) + +### Generated metadata + +Cargo generates a Rust module at `OUT_DIR/pkgconfig_meta.rs` with compile-time +constants for each dependency. Include it in your build script: + +```rust +include!(concat!(env!("OUT_DIR"), "/pkgconfig_meta.rs")); + +fn main() { + // Access metadata for openssl dependency + if pkgconfig::openssl::FOUND { + for lib in pkgconfig::openssl::LIBS { + println!("cargo:rustc-link-lib={}", lib); + } + for path in pkgconfig::openssl::LIB_PATHS { + println!("cargo:rustc-link-search=native={}", path); + } + } +} +``` + +Each dependency module contains constants for: +* `VERSION` — Version string from pkg-config +* `FOUND` — Boolean indicating if library was found +* `RESOLVED_VIA` — How it was resolved: `"pkg-config"`, `"fallback"`, or `"not-found"` +* `INCLUDE_PATHS` — Array of include directories +* `LIB_PATHS` — Array of library directories +* `LIBS` — Array of library names +* `CFLAGS` — Compiler flags +* `DEFINES` — Preprocessor defines +* `LDFLAGS` — Linker flags +* `RAW_CFLAGS` and `RAW_LDFLAGS` — Raw flag strings from pkg-config + +### Error handling + +If a required dependency is not found: +* Cargo will error with a helpful message listing attempted pkg-config names +* The message suggests solutions: + 1. Installing the system library + 2. Setting PKG_CONFIG_PATH environment variable + 3. Using a fallback specification + 4. Using alternative names if the package has different names + +Optional dependencies that are not found will: +* Log a warning message +* Generate metadata with `FOUND = false` +* Allow the build to continue + ## The `[profile.*]` sections The `[profile]` tables provide a way to customize compiler settings such as diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 9872e709d04..bb7760eafd5 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -67,6 +67,7 @@ Each new feature described below should explain how to use it. * Build scripts and linking * [Metabuild](#metabuild) --- Provides declarative build scripts. * [Multiple Build Scripts](#multiple-build-scripts) --- Allows use of multiple build scripts. + * [pkgconfig-dependencies](#pkgconfig-dependencies) --- Declarative system library dependencies via pkg-config. * Resolver and features * [no-index-update](#no-index-update) --- Prevents cargo from updating the index cache. * [avoid-dev-deps](#avoid-dev-deps) --- Prevents the resolver from including dev-dependencies during resolution. @@ -2323,6 +2324,133 @@ Multi-package publishing has been stabilized in Rust 1.90.0. Support for `build.build-dir` was stabilized in the 1.91 release. See the [config documentation](config.md#buildbuild-dir) for information about changing the build-dir +## pkgconfig-dependencies + +The `pkgconfig-dependencies` feature (enabled with `-Z pkgconfig-dependencies`) +allows you to declaratively specify system library dependencies in your `Cargo.toml` +that are managed by pkg-config. Cargo automatically queries pkg-config for +compiler and linker flags needed to use these libraries. + +### Motivation + +Traditionally, when a Rust crate needs to use a system library, you either: +1. Manually specify compiler and linker flags in your build script +2. Use a build-dependency that probes pkg-config and generates code + +This feature streamlines the process by making pkg-config dependency declarations +first-class citizens in `Cargo.toml`, similar to regular Rust dependencies. + +### Usage + +Enable the feature with `-Z pkgconfig-dependencies`: + +``` +cargo build -Z pkgconfig-dependencies +``` + +Or in `.cargo/config.toml`: + +```toml +[unstable] +pkgconfig-dependencies = true +``` + +### Declaring dependencies + +In your `Cargo.toml`, add a `[pkgconfig-dependencies]` section: + +```toml +[pkgconfig-dependencies] +# Simple form: just version constraint +openssl = "1.1" # Minimum version +sqlite3 = ">= 3.0" # Explicit minimum +zlib = "= 1.2.11" # Exact version +curl = "7.0 .. 8.0" # Version range + +# Detailed form for more control +[pkgconfig-dependencies.libfoo] +version = "2.0" +names = ["libfoo", "foo"] # Try alternative pkg-config names +optional = true # Don't fail build if not found +link = "static" # Prefer static linking +[pkgconfig-dependencies.libfoo.fallback] +libs = ["foo"] +lib-paths = ["/usr/local/lib"] +include-paths = ["/usr/local/include"] +``` + +See the [manifest reference](manifest.md#the-pkgconfig-dependencies-section) +for complete documentation of the `[pkgconfig-dependencies]` table. + +### Generated metadata + +Cargo generates Rust code in `OUT_DIR/pkgconfig_meta.rs` containing compile-time +constants for each dependency. Use it in your build script: + +```rust +include!(concat!(env!("OUT_DIR"), "/pkgconfig_meta.rs")); + +fn main() { + if pkgconfig::openssl::FOUND { + println!("cargo:rustc-link-search=native={}", + pkgconfig::openssl::LIB_PATHS[0]); + for lib in pkgconfig::openssl::LIBS { + println!("cargo:rustc-link-lib={}", lib); + } + } +} +``` + +Each dependency has a module with these constants: +- `VERSION`: Version string +- `FOUND`: Boolean indicating if found +- `RESOLVED_VIA`: How resolved (`"pkg-config"`, `"fallback"`, or `"not-found"`) +- `INCLUDE_PATHS`: Include directories +- `LIB_PATHS`: Library directories +- `LIBS`: Library names to link +- `CFLAGS`, `DEFINES`, `LDFLAGS`: Compiler and linker flags + +### Resolution strategy + +For each dependency, Cargo: + +1. Tries the primary pkg-config name +2. If that fails, tries alternative names (from the `names` field) in order +3. If all pkg-config queries fail, uses the fallback specification (if provided) +4. If nothing succeeds and the dependency is not optional, errors with helpful suggestions + +### Error messages + +When a required dependency is not found, Cargo provides clear error messages with +suggestions: + +``` +error: pkg-config dependency `openssl` (version 1.1) not found + tried pkg-config names: openssl + error: pkg-config failed + +To fix this, you can: +1. Install the system library (e.g., libssl-dev on Debian, openssl-devel on Fedora) +2. Set the PKG_CONFIG_PATH environment variable to include the directory with the .pc file +3. Add a [fallback] specification in Cargo.toml to manually specify library paths +4. Use alternative names via the `names` field if the package has multiple names +``` + +### Optional dependencies + +If a dependency is marked `optional = true`: +- Cargo will emit a warning if not found but continue the build +- The generated metadata will have `FOUND = false` +- Build scripts can check the `FOUND` constant to handle missing libraries + +```rust +if pkgconfig::libfoo::FOUND { + // Use libfoo +} else { + // Provide alternative implementation or feature gate +} +``` + ## Build-plan The `--build-plan` argument for the `build` command has been removed in 1.93.0-nightly.