Skip to content

Conversation

@domenkozar
Copy link

This PR explores a new unstable feature that allows developers to
declaratively specify system library dependencies in Cargo.toml, eliminating
the need for custom build scripts to probe pkg-config.

With pkgconfig-dependencies, you can declare system libraries just like
regular Rust dependencies, with support for version constraints, alternative
names, fallback specifications, feature gating, and link type control.

The Problem

Currently, using system libraries in Rust requires manual build.rs scripts:

fn main() {
    pkg_config::probe_library("openssl")
        .or_else(|_| { /* fallback */ })
        .unwrap();
}

This is repetitive boilerplate that every project needs to write and maintain.

pkgconfig-dependencies

Simply declare your system dependencies in Cargo.toml:

  [pkgconfig-dependencies]
  openssl = "1.1"
  sqlite3 = "3.0"

Enable the feature and build:

  $ cargo build -Z pkgconfig-dependencies

Cargo automatically probes pkg-config, generates metadata, and makes it
available to your code.

Usage

Simple Dependencies

Just specify version constraints:

  [pkgconfig-dependencies]
  openssl = "1.1"
  libcurl = "7.0"
  zlib = "1.2"

Advanced Configuration

For more control, use the detailed form:

[pkgconfig-dependencies.sqlite3]
version = "3.0"
names = ["sqlite3", "sqlite"]      # Try alternative pkg-config names
optional = true                     # Don't fail if not found
link = "static"                     # How to link (static/dynamic)
feature = "with-sqlite"             # Only required if feature enabled

[pkgconfig-dependencies.sqlite3.fallback]
libs = ["sqlite3"]
lib-paths = ["/usr/local/lib"]
include-paths = ["/usr/local/include"]

Feature-Gated Dependencies

Build with or without system libraries:

  [features]
  system-deps = []

  [pkgconfig-dependencies.openssl]
  version = "1.1"
  feature = "system-deps"
  $ cargo build                          # Uses bundled dependencies
  $ cargo build --features system-deps   # Uses system OpenSSL

Accessing Library Information

Cargo generates a Rust module with compile-time constants:

  include!(concat!(env!("OUT_DIR"), "/pkgconfig_meta.rs"));

  fn main() {
      if pkgconfig::openssl::FOUND {
          println!("OpenSSL {} found", pkgconfig::openssl::VERSION);
          for lib in pkgconfig::openssl::LIBS {
              println!("cargo:rustc-link-lib={}", lib);
          }
      }
  }

Example: Portable OpenSSL

  [package]
  name = "cryptolib"

  [features]
  default = ["vendored"]
  vendored = []
  system = []

  [pkgconfig-dependencies.openssl]
  version = "1.1"
  names = ["openssl", "libssl"]
  feature = "system"
  optional = true
  link = "dynamic"
  fallback = { lib-paths = ["/opt/openssl/lib"], include-paths =
  ["/opt/openssl/include"] }

Works everywhere:

  • Default: Uses vendored OpenSSL
  • Linux with pkg-config: cargo build --features system
  • Custom install: Fallback paths handle non-standard locations
  • macOS/Windows: Alternative names handle platform naming differences

Unstable Feature Flag

This feature is behind -Z pkgconfig-dependencies and requires nightly:

  cargo +nightly build -Z pkgconfig-dependencies

domenkozar and others added 7 commits November 20, 2025 13:19
Add declarative pkg-config dependency support to Cargo.toml with automatic
metadata generation and compile-time constants. This implements the core
foundation for the [pkgconfig-dependencies] table specification.

## Changes

### Schema & Parsing (cargo-util-schemas)
- Add TomlPkgConfigDependency enum supporting simple and detailed forms
- Support version constraints, alternative package names, optional flag
- Integrate into TomlManifest and TomlPlatform for target-specific deps

### TOML Processing (src/cargo/util/toml/mod.rs)
- Implement normalize_pkgconfig_dependencies() with validation
- Validate version constraints at parse time
- Detect unused manifest keys for better error messages

### Manifest Integration (src/cargo/core/manifest.rs)
- Add pkgconfig_dependencies field to Manifest struct
- Extract from normalized TOML during manifest construction
- Expose public accessor for build system integration

### Pkg-Config Module (src/cargo/core/compiler/pkgconfig.rs)
- query_pkg_config(): Query system pkg-config with version constraints
- probe_all_dependencies(): Batch probe for all declared dependencies
- generate_metadata_file(): Generate Rust code with compile-time constants
- sanitize_module_name(): Convert package names to valid Rust identifiers
- probe_and_generate_metadata(): Main entry point for build integration

Generated pkgconfig_meta.rs constants:
- VERSION: Package version from pkg-config
- FOUND: Whether dependency was successfully resolved
- RESOLVED_VIA: Resolution method (pkg-config, fallback, not-found, etc.)
- INCLUDE_PATHS, LIB_PATHS, LIBS: System paths and libraries
- CFLAGS, DEFINES, LDFLAGS: Compiler and linker flags
- RAW_CFLAGS, RAW_LDFLAGS: Raw pkg-config output for debugging

### Feature Gating
- Add pkgconfig_dependencies to -Z unstable options
- Require feature activation when [pkgconfig-dependencies] is used
- Provides clear error message with feature documentation

### Testing
- Unit tests for module name sanitization
- Tests for pkg-config output parsing
- Tests for metadata file generation
- Tests for resolution method conversions

Users can now declare pkg-config dependencies:

  [pkgconfig-dependencies]
  libfoo = "1.2"
  libbar = ">= 2.0, < 3.0"

And access metadata via generated constants:

  include!(concat!(env!("OUT_DIR"), "/pkgconfig_meta.rs"));

  pub fn get_version() -> &'static str {
      pkgconfig::libfoo::VERSION
  }

Enabled with: cargo build -Z pkgconfig-dependencies

🤖 Generated with Claude Code

Co-Authored-By: Claude <[email protected]>
…, and fallbacks

This adds support for:
- Alternative package names (tries multiple pkg-config names in order)
- Optional dependencies that don't fail the build if not found
- Fallback specifications for manual pkg-config configuration

New features:
- query_pkg_config() now accepts alternative_names and fallback parameters
- apply_fallback() converts fallback specs to PkgConfigLibrary
- probe_all_dependencies() handles optional deps by recording NotFound state
- Updated tests to verify fallback and metadata generation behavior

Also added:
- TomlPkgConfigFallback schema struct with libs, lib_paths, include_paths
- fallback() accessor on TomlPkgConfigDependency enum
- Comprehensive unit tests for fallback scenarios and metadata generation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Enhanced user experience with:
- Detailed error messages when pkg-config dependencies fail
- Actionable suggestions for fixing missing dependencies
- Instructions for using fallbacks and alternative names
- Warnings when optional dependencies are not found

Documentation improvements:
- Module-level documentation with complete usage examples
- Detailed docstrings for public functions
- Examples of Cargo.toml syntax (simple and detailed forms)
- Instructions for accessing generated metadata
- Process documentation for probe_and_generate_metadata()

Error messages now include:
1. Clear statement of which dependency and version was not found
2. List of pkg-config names that were tried (if multiple)
3. Helpful suggestions:
   - How to install the library for different distros
   - Using PKG_CONFIG_PATH environment variable
   - Adding fallback specifications in Cargo.toml
   - Using alternative names for packages with multiple names

Optional dependencies now log warnings when not found, so users are
aware of what's being skipped.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Bug fixes:
- Remove unused PathBuf import from pkgconfig.rs
- Remove unused parse_pkg_config_output function (we use pkg-config crate's parsing)
- Remove corresponding unit test for parse_pkg_config_output
- Prefix unused _format_str_array closure with underscore
- Remove unused Error import in schema deserialization
- Add missing pkgconfig_dependencies field to TomlManifest destructuring
- Add missing pkgconfig_dependencies field to TomlPlatform and TomlManifest constructors

Documentation:
- Add comprehensive [pkgconfig-dependencies] section to manifest.md
  * Simple and detailed TOML syntax examples
  * Complete field documentation
  * Generated metadata module structure
  * Error handling and optional dependency behavior

- Add pkgconfig-dependencies to unstable.md feature list
  * Detailed feature documentation with motivation
  * Usage examples (command line and config.toml)
  * Resolution strategy explanation
  * Error message examples
  * Optional dependency handling

All documentation follows Cargo's existing documentation patterns and includes
practical examples for build scripts and manifest configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The test_generate_metadata_file_module_naming test was expecting 'gtk_plus_3_0'
but the sanitize_module_name function correctly converts '+', '-', and '.' to
underscores, resulting in 'gtk__3_0'. Updated the test to match the actual
correct behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…pendencies

Implements feature gating:
- Dependencies can now be gated by Cargo features via 'feature' field
- Dependencies with unmet features are recorded as NotProbed
- Simplifies conditional compilation of optional system dependencies

Implements link type support:
- Dependencies can specify how to link ('static', 'dynamic', etc.) via 'link' field
- Link type is tracked through resolution and included in generated metadata
- Generated Rust code includes LINK_TYPE constant for each dependency

Improves logging:
- Replace eprintln! with proper tracing::warn! for optional deps
- Integrates with Cargo's logging infrastructure

Changes:
- Add link_type field to PkgConfigLibrary struct
- Update probe_all_dependencies() to accept enabled_features list
- Feature-gated deps are skipped without probing
- Link type passed through query_pkg_config() and apply_fallback()
- Generated metadata includes LINK_TYPE: Option<&str>
- Update all test fixtures to include link_type field
- Update probe_and_generate_metadata() signature to accept enabled_features

Tests updated:
- test_apply_fallback_creates_library - verify link_type=None
- test_apply_fallback_empty_values - verify link_type is preserved
- All test PkgConfigLibrary instances now include link_type field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implements version constraint parsing and validation:
- Exact version: '= 3.0'
- Minimum version: '>= 3.0' or '3.0' (default)
- Version range: '3.0 .. 4.0' or '3.0..4.0'

Users can now write:
  [pkgconfig-dependencies]
  openssl = '= 1.1.1'      # Require exact version
  sqlite3 = '>= 3.0'       # At least version 3.0
  zlib = '1.2 .. 1.3'      # Range between versions

Added apply_version_constraint() helper that parses the constraint
syntax and applies the appropriate pkg-config configuration method.

Constraints are validated with helpful error messages for invalid ranges.

Tests added for all constraint types including edge cases.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@rustbot rustbot added the A-build-execution Area: anything dealing with executing the compiler label Nov 20, 2025
@rustbot
Copy link
Collaborator

rustbot commented Nov 20, 2025

r? @ehuss

rustbot has assigned @ehuss.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added A-documenting-cargo-itself Area: Cargo's documentation A-manifest Area: Cargo.toml issues A-unstable Area: nightly unstable support S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 20, 2025
Add examples and documentation for all version constraint types:
- Minimum version: '1.1' or '>= 1.1'
- Exact version: '= 1.1'
- Version range: '3.0 .. 4.0'

Updated examples in both manifest.md and unstable.md to show the
different constraint syntaxes with helpful comments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@domenkozar domenkozar marked this pull request as draft November 20, 2025 19:17
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Nov 20, 2025
@bonzini
Copy link

bonzini commented Nov 21, 2025

Cargo generates a Rust module with compile-time constants:

If you still need to do work in build.rs, you might as well use system-deps. I just don't see the point.

I'm not sure this is great advertisement for Claude Code...

@ssokolow
Copy link

ssokolow commented Nov 21, 2025

Cargo generates a Rust module with compile-time constants:

If you still need to do work in build.rs, you might as well use system-deps. I just don't see the point.

I think the idea is that, given how much functional overlap there would be between the code for the case which does need that and the code for the case without, it makes sense to not encourage staying in the direction of "this codebase contains both lazy-static and once-cell" as soon as one of the transitive dependencies needs a build.rs.

Better to have a single source of truth for interfacing with pkg-config but, at the same time, not force everyone to "subsidize" the subset of packages which will need build.rs either way by forcing the common-case uses with no intrinsic build.rs dependency to depend on system-deps.

EDIT: In essence, it's extending Cargo's existing "Most crates don't need build.rs" to interfacing with pkg-config. Right now, interfacing with system libraries is akin to using Makefile to orchestrate your build when 99% of projects don't need that power and flexibility, but all have to pay for it.

@bonzini
Copy link

bonzini commented Nov 21, 2025

EDIT: In essence, it's extending Cargo's existing "Most crates don't need build.rs" to interfacing with pkg-config. Right now, interfacing with system libraries is akin to using Makefile to orchestrate your build when 99% of projects don't need that power and flexibility, but all have to pay for it.

The way to go for that would be to stabilize metabuild and have system-deps support it. There's no need to have Cargo do it.

@ssokolow
Copy link

I I'm reading that correctly, having a metabuild() function wouldn't address the other issue of making the dependencies declarative in a way that's beneficial to higher-level build orchestration.

(eg. distro build tooling extracting dependencies without running third-party code to extend the cargo fetch/cargo build --offline split to the system dependencies.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-build-execution Area: anything dealing with executing the compiler A-documenting-cargo-itself Area: Cargo's documentation A-manifest Area: Cargo.toml issues A-unstable Area: nightly unstable support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants