├── .gitignore ├── tests ├── conanfile.txt └── integration_test.rs ├── example-build-script ├── conanfile.txt ├── Cargo.toml ├── test_pkg_conanfile.py ├── build.rs └── src │ └── main.rs ├── .github └── workflows │ ├── publish.yml │ └── rust.yml ├── Cargo.toml ├── .gitlab-ci.yml ├── LICENSE ├── CHANGELOG.md ├── README.md ├── cliff.toml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust stuff 2 | /target/ 3 | /Cargo.lock 4 | 5 | # GitLab CI stuff 6 | /.cargo/ 7 | /cargo/ 8 | -------------------------------------------------------------------------------- /tests/conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | zlib/1.3.1 3 | libxml2/2.13.8 4 | openssl/3.4.1 5 | lightgbm/4.3.0 6 | -------------------------------------------------------------------------------- /example-build-script/conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | zlib/1.3.1 3 | libxml2/2.13.8 4 | openssl/3.4.1 5 | system_libs_test/0.1.0 6 | -------------------------------------------------------------------------------- /example-build-script/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-build-script" 3 | description = "A working example of using Conan dependencies with conan2-rs" 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [build-dependencies] 8 | conan2 = { path = "../" } 9 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish crates 2 | 3 | on: 4 | push: 5 | tags: [ v* ] 6 | 7 | env: 8 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 9 | 10 | jobs: 11 | publish-crate: 12 | name: Publish to crates.io 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Run cargo publish 17 | run: cargo publish 18 | -------------------------------------------------------------------------------- /example-build-script/test_pkg_conanfile.py: -------------------------------------------------------------------------------- 1 | from conan import ConanFile 2 | 3 | 4 | class TestSystemLibsConan(ConanFile): 5 | """ 6 | Defines a Conan package for the cpp_info.system_libs attribute tests. 7 | """ 8 | 9 | name = "system_libs_test" 10 | version = "0.1.0" 11 | 12 | package_type = "static-library" 13 | build_policy = "missing" 14 | 15 | settings = "os", "compiler", "build_type", "arch" 16 | 17 | def package_info(self): 18 | self.cpp_info.system_libs = ["libyaml.so", "libsqlite3.a"] 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "conan2" 3 | version = "0.1.8" 4 | description = "Pulls the C/C++ library linking flags from Conan dependencies" 5 | authors = ["Sergey Kvachonok "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/ravenexp/conan2-rs" 9 | keywords = ["ffi", "conan", "build"] 10 | categories = ["development-tools::build-utils", "development-tools::ffi"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | serde_json = "1.0" 15 | 16 | [workspace] 17 | members = [ "example-build-script" ] 18 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: "devops/support" 3 | ref: master 4 | file: 5 | - "includes/build/rust/crates-io-mirror.yml" 6 | - "includes/docs/rustdoc.yml" 7 | 8 | default: 9 | tags: 10 | - linux-docker 11 | image: ${PLG_CI_DOCKER_TAG}/rusty-python:latest 12 | 13 | stages: 14 | - check 15 | - build 16 | - test 17 | - deploy 18 | 19 | fmt: 20 | stage: check 21 | script: 22 | - cargo fmt -- --check 23 | 24 | clippy: 25 | stage: check 26 | script: 27 | - cargo clippy -- --deny warnings 28 | - cargo clippy --tests -- --deny warnings 29 | 30 | compile: 31 | stage: build 32 | script: 33 | - cargo build --verbose 34 | 35 | package: 36 | stage: build 37 | script: 38 | - cargo package 39 | 40 | runtests: 41 | stage: test 42 | script: 43 | - cargo test --verbose -- --test-threads=1 44 | - cargo build -p example-build-script -vv 45 | - cargo build -p example-build-script -vv --release 46 | - cargo run -p example-build-script 47 | -------------------------------------------------------------------------------- /example-build-script/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use conan2::{ConanInstall, ConanScope, ConanVerbosity}; 4 | 5 | fn main() { 6 | let status = Command::new("conan") 7 | .arg("create") 8 | .arg("test_pkg_conanfile.py") 9 | .status() 10 | .expect("failed to create test package"); 11 | 12 | assert!(status.success(), "creating test package failed"); 13 | 14 | ConanInstall::new() 15 | .host_profile("cargo-host") 16 | .build_profile("default") 17 | // Auto-detect "cargo-host" and "default" profiles if none exist 18 | .detect_profile() 19 | .build("missing") 20 | .verbosity(ConanVerbosity::Error) // Silence Conan warnings 21 | .option(ConanScope::Global, "shared", "False") 22 | .option(ConanScope::Local, "sanitizers", "True") 23 | .option(ConanScope::Package("openssl"), "no_deprecated", "True") 24 | .option(ConanScope::Package("libxml2/2.13.8"), "ftp", "False") 25 | .config("tools.build:skip_test", "True") 26 | .run() 27 | .parse() 28 | .emit(); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sergey Kvachonok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example-build-script/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_int, c_uint, c_ulong, c_void}; 2 | 3 | extern "C" { 4 | /// zlib 5 | fn adler32(adler: c_ulong, buf: *const c_void, len: c_uint) -> c_ulong; 6 | 7 | /// libxml2 8 | fn xmlCheckVersion(version: c_int); 9 | 10 | /// libiconv 11 | fn iconv_open(tocode: *const u8, fromcode: *const u8) -> *const c_void; 12 | 13 | /// libcharset 14 | fn locale_charset() -> *const u8; 15 | 16 | /// libcrypto 17 | fn EVP_MD_CTX_new() -> *const c_void; 18 | 19 | /// libssl 20 | fn OPENSSL_init_ssl(opts: u64, buf: *const c_void) -> c_int; 21 | 22 | /// libsqlite3 23 | fn sqlite3_libversion() -> *const u8; 24 | 25 | /// libyaml 26 | fn yaml_get_version_string() -> *const u8; 27 | } 28 | 29 | fn main() { 30 | unsafe { 31 | // zlib 32 | adler32(0, std::ptr::null(), 0); 33 | 34 | // libxml2 35 | xmlCheckVersion(2_00_00); 36 | 37 | // libiconv 38 | iconv_open(b"a".as_ptr(), b"b".as_ptr()); 39 | 40 | // libcharset 41 | locale_charset(); 42 | 43 | // libcrypto 44 | EVP_MD_CTX_new(); 45 | 46 | // libssl 47 | OPENSSL_init_ssl(0, std::ptr::null()); 48 | 49 | // libsqlite3 50 | sqlite3_libversion(); 51 | 52 | // libyaml 53 | yaml_get_version_string(); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust build checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | name: Build and test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Build 19 | run: cargo build --verbose 20 | - name: Install Conan 21 | id: conan 22 | uses: turtlebrowser/get-conan@v1.2 23 | - name: Conan version 24 | run: echo "${{ steps.conan.outputs.version }}" 25 | - name: Conan profile init 26 | run: conan profile detect 27 | - name: Run tests 28 | run: cargo test --verbose -- --test-threads=1 29 | - name: Test build the example crates 30 | run: | 31 | cargo build -p example-build-script -vv 32 | cargo build -p example-build-script -vv --release 33 | - name: Test run the build script example 34 | run: cargo run -p example-build-script 35 | fmt: 36 | name: Check code formatting 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Run cargo fmt 41 | run: cargo fmt -- --check 42 | clippy: 43 | name: Clippy lints 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | - name: Run cargo clippy 48 | run: cargo clippy --tests -- --deny warnings 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.1.8] - 2025-08-08 6 | 7 | ### 🐛 Bug Fixes 8 | 9 | - Solve issue [#26](https://github.com/ravenexp/conan2-rs/issues/26) by collecting all library search paths in BTreeMap 10 | 11 | ### ⚙️ Miscellaneous Tasks 12 | 13 | - Update git-cliff config file for v2.10 14 | ## [0.1.7] - 2025-05-31 15 | 16 | ### 🚀 Features 17 | 18 | - Add "build_type" setting configuration method 19 | - Add support for setting package build options 20 | - Add support for setting Conan config options 21 | 22 | ### 🐛 Bug Fixes 23 | 24 | - Ignore bogus `libdirs` in non-library packages 25 | ## [0.1.6] - 2025-05-20 26 | 27 | ### 🚀 Features 28 | 29 | - Support library file names in `cpp_info.libs` 30 | 31 | ### 🐛 Bug Fixes 32 | 33 | - Process header-only dependencies normally 34 | 35 | ### 🧪 Testing 36 | 37 | - Update dependencies in test Conanfiles 38 | - Add a test case for `cpp_info.system_libs` 39 | ## [0.1.5] - 2024-12-21 40 | 41 | ### 🚀 Features 42 | 43 | - Support for `sharedlinkflags` 44 | 45 | ### 🧪 Testing 46 | 47 | - Add `sharedlinkflags` to integration test 48 | ## [0.1.4] - 2024-11-28 49 | 50 | ### 🚀 Features 51 | 52 | - Support for `exelinkflags` 53 | 54 | ### 🧪 Testing 55 | 56 | - Add integration test for `exelinkflags` 57 | ## [0.1.3] - 2024-11-27 58 | 59 | ### 🚀 Features 60 | 61 | - Add `ConanInstall::build_profile()` method 62 | 63 | ### 🚜 Refactor 64 | 65 | - Show better errors for `conan profile detect` 66 | 67 | ### 📚 Documentation 68 | 69 | - Add examples for `ConanInstall::build_profile()` 70 | 71 | ### 🧪 Testing 72 | 73 | - Add integration tests for `build_profile()` 74 | ## [0.1.2] - 2024-08-28 75 | 76 | ### 🚀 Features 77 | 78 | - Add Conan command verbosity control option 79 | - Add profile auto-detection option 80 | 81 | ### ⚙️ Miscellaneous Tasks 82 | 83 | - Update `git-cliff` config file 84 | ## [0.1.1] - 2024-02-15 85 | 86 | ### 🚀 Features 87 | 88 | - Collect C/C++ include directories from deps 89 | 90 | ### 📚 Documentation 91 | 92 | - Add C/C++ include paths use examples 93 | ## [0.1.0] - 2023-10-13 94 | 95 | ### 🚀 Features 96 | 97 | - Auto-detect and define `build_type` setting 98 | 99 | ### 📚 Documentation 100 | 101 | - Add README.md 102 | 103 | ### 🧪 Testing 104 | 105 | - Add crate-level integration tests 106 | - Add an example build script usage crate 107 | 108 | ### 🦊 CI 109 | 110 | - Add a basic GitLab CI config file 111 | - Add GitHub workflows for building and publishing 112 | 113 | ### ⚙️ Miscellaneous Tasks 114 | 115 | - Add MIT license 116 | - Add `git-cliff` config file 117 | - Add a change log file 118 | 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conan2-rs 2 | 3 | ## Introduction 4 | 5 | `conan2-rs` is a Cargo build script wrapper of the Conan C/C++ package manager 6 | (version 2.0 only). 7 | 8 | It automatically pulls the C/C++ library linking flags from Conan dependencies 9 | and passes them to `rustc`. 10 | 11 | ## Adding C/C++ dependencies using Conan 12 | 13 | The simplest way to add C/C++ dependencies to a Rust project using Conan 14 | is to add a plain `conanfile.txt` file as follows: 15 | 16 | ```text 17 | [requires] 18 | libxml2/2.13.8 19 | openssl/3.4.1 20 | zlib/1.3.1 21 | ``` 22 | 23 | ## Example usage 24 | 25 | Add `conan2` to the `Cargo.toml` build dependencies section: 26 | 27 | ```toml 28 | [build-dependencies] 29 | conan2 = "0.1" 30 | ``` 31 | 32 | Add the following lines to the project `build.rs` script to invoke `conan install` 33 | and pass the Conan dependency information to Cargo automatically: 34 | 35 | ```rust 36 | use conan2::ConanInstall; 37 | 38 | fn main() { 39 | ConanInstall::new().run().parse().emit(); 40 | } 41 | ``` 42 | 43 | The most commonly used `build_type` Conan setting will be defined automatically 44 | depending on the current Cargo build profile: `debug` or `release`. 45 | 46 | The Conan executable is assumed to be named `conan` unless 47 | the `CONAN` environment variable is set to override. 48 | 49 | An example Rust crate using `conan2-rs` to link Conan dependencies 50 | can also be found in the project repository. 51 | 52 | ## Advanced usage 53 | 54 | ### Automatic Conan profile inference from Cargo target 55 | 56 | Using custom Conan profiles with names derived from the Cargo target information 57 | and a reduced output verbosity level: 58 | 59 | ```rust 60 | use conan2::{ConanInstall, ConanScope, ConanVerbosity}; 61 | 62 | fn main() { 63 | let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); 64 | let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 65 | let conan_profile = format!("{}-{}", target_os, target_arch); 66 | 67 | ConanInstall::new() 68 | .profile(&conan_profile) 69 | .build_type("RelWithDebInfo") // Override the Cargo build profile 70 | .build("missing") 71 | .verbosity(ConanVerbosity::Error) // Silence Conan warnings 72 | .option(ConanScope::Global, "shared", "True") // Add some package options 73 | .option(ConanScope::Local, "power", "10") 74 | .option(ConanScope::Package("foolib"), "frob", "max") 75 | .option(ConanScope::Package("barlib/1.0"), "zoom", "True") 76 | .config("tools.build:skip_test", "True") // Add some Conan configs 77 | .run() 78 | .parse() 79 | .emit(); 80 | } 81 | ``` 82 | 83 | ### Automatic Conan profile creation 84 | 85 | Creating a custom default Conan profile on the fly with zero configuration: 86 | 87 | ```rust 88 | use conan2::{ConanInstall, ConanVerbosity}; 89 | 90 | ConanInstall::new() 91 | .profile("cargo") 92 | .detect_profile() // Run `conan profile detect --exist-ok` for the above 93 | .run() 94 | .parse() 95 | .emit(); 96 | ``` 97 | 98 | ### Using separate host and build profiles 99 | 100 | Using different values for `--profile:host` and `--profile:build` 101 | arguments of `conan install` command: 102 | 103 | ```rust 104 | use conan2::{ConanInstall, ConanVerbosity}; 105 | 106 | ConanInstall::new() 107 | .host_profile("cargo-host") 108 | .build_profile("cargo-build") 109 | .run() 110 | .parse() 111 | .emit(); 112 | ``` 113 | 114 | ### Getting C/C++ include paths from Conan dependencies 115 | 116 | To use the list of include paths, do the following after 117 | parsing the `conan install` output: 118 | 119 | ```rust 120 | use conan2::ConanInstall; 121 | 122 | let metadata = ConanInstall::new().run().parse(); 123 | 124 | for path in metadata.include_paths() { 125 | // Add "-I{path}" to CXXFLAGS or something. 126 | } 127 | 128 | metadata.emit(); 129 | ``` 130 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | //! conan2-rs integration tests 2 | 3 | use std::{io::Write, path::Path}; 4 | 5 | use conan2::{ConanInstall, ConanScope, ConanVerbosity}; 6 | 7 | #[test] 8 | fn run_conan_install() { 9 | let output = ConanInstall::with_recipe(Path::new("tests/conanfile.txt")) 10 | .output_folder(Path::new(env!("CARGO_TARGET_TMPDIR"))) 11 | .detect_profile() // Auto-detect "default" profile if not exists 12 | .build_type("Release") 13 | .build("missing") 14 | .verbosity(ConanVerbosity::Verbose) 15 | .option(ConanScope::Global, "shared", "True") 16 | .option(ConanScope::Local, "sanitizers", "True") 17 | .option(ConanScope::Package("openssl"), "no_deprecated", "True") 18 | .option(ConanScope::Package("libxml2/2.13.8"), "ftp", "False") 19 | .config("tools.build:skip_test", "True") 20 | .run(); 21 | 22 | // Fallback for test debugging 23 | if !output.is_success() { 24 | std::io::stderr().write_all(output.stderr()).unwrap(); 25 | } 26 | 27 | assert!(output.is_success()); 28 | assert_eq!(output.status_code(), 0); 29 | 30 | let cargo = output.parse(); 31 | let includes = cargo.include_paths(); 32 | 33 | assert!(includes.len() > 3); 34 | 35 | cargo.emit(); 36 | } 37 | 38 | #[test] 39 | fn fail_no_conanfile() { 40 | let output = ConanInstall::new() 41 | .output_folder(Path::new(env!("CARGO_TARGET_TMPDIR"))) 42 | .build_type("Debug") 43 | .verbosity(ConanVerbosity::Status) 44 | .run(); 45 | 46 | std::io::stderr().write_all(output.stderr()).unwrap(); 47 | 48 | assert!(!output.is_success()); 49 | assert_eq!(output.status_code(), 1); 50 | assert_eq!(output.stdout().len(), 0); 51 | assert!(output 52 | .stderr() 53 | .starts_with(b"ERROR: Conanfile not found at")); 54 | } 55 | 56 | #[test] 57 | fn fail_no_profile() { 58 | let output = ConanInstall::with_recipe(Path::new("tests/conanfile.txt")) 59 | .output_folder(Path::new(env!("CARGO_TARGET_TMPDIR"))) 60 | .profile("no-such-profile") 61 | .verbosity(ConanVerbosity::Debug) 62 | .run(); 63 | 64 | std::io::stderr().write_all(output.stderr()).unwrap(); 65 | 66 | assert!(!output.is_success()); 67 | assert_eq!(output.status_code(), 1); 68 | assert_eq!(output.stdout().len(), 0); 69 | assert!(output.stderr().starts_with(b"ERROR: Profile not found: ")); 70 | } 71 | 72 | #[test] 73 | fn detect_custom_profile() { 74 | let output = ConanInstall::with_recipe(Path::new("tests/conanfile.txt")) 75 | .output_folder(Path::new(env!("CARGO_TARGET_TMPDIR"))) 76 | .profile(&format!("{}-dynamic-profile", env!("CARGO_PKG_NAME"))) 77 | .detect_profile() 78 | .build_type("RelWithDebInfo") 79 | .build("missing") 80 | .verbosity(ConanVerbosity::Debug) 81 | .run(); 82 | 83 | std::io::stderr().write_all(output.stderr()).unwrap(); 84 | assert!(output.is_success()); 85 | } 86 | 87 | #[test] 88 | fn host_and_build_profiles() { 89 | let output = ConanInstall::with_recipe(Path::new("tests/conanfile.txt")) 90 | .output_folder(Path::new(env!("CARGO_TARGET_TMPDIR"))) 91 | .host_profile(&format!("{}-dynamic-host-profile", env!("CARGO_PKG_NAME"))) 92 | .build_profile(&format!("{}-dynamic-build-profile", env!("CARGO_PKG_NAME"))) 93 | .detect_profile() 94 | .build("missing") 95 | .verbosity(ConanVerbosity::Debug) 96 | .run(); 97 | 98 | std::io::stderr().write_all(output.stderr()).unwrap(); 99 | assert!(output.is_success()); 100 | } 101 | 102 | #[test] 103 | fn test_shared_and_exe_link_flags() { 104 | let output = ConanInstall::with_recipe(Path::new("tests/conanfile.txt")) 105 | .output_folder(Path::new(env!("CARGO_TARGET_TMPDIR"))) 106 | .detect_profile() 107 | .build("missing") 108 | .verbosity(ConanVerbosity::Debug) 109 | .run(); 110 | 111 | assert!(output.is_success()); 112 | let cargo = output.parse(); 113 | let emitted_instructions = String::from_utf8(cargo.as_bytes().to_vec()).expect("Invalid UTF-8"); 114 | assert!(emitted_instructions.contains("cargo:rustc-cdylib-link-arg=-fopenmp")); 115 | assert!(emitted_instructions.contains("cargo:rustc-link-arg-bins=-fopenmp")); 116 | } 117 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | 5 | [changelog] 6 | # Template for the changelog header 7 | header = """ 8 | # Changelog\n 9 | All notable changes to this project will be documented in this file.\n 10 | """ 11 | 12 | # A Tera template to be rendered for each release in the changelog. 13 | # See https://keats.github.io/tera/docs/#introduction 14 | body = """ 15 | {% if version %}\ 16 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 17 | {% else %}\ 18 | ## [unreleased] 19 | {% endif %}\ 20 | {% for group, commits in commits | group_by(attribute="group") %} 21 | ### {{ group | striptags | trim | upper_first }} 22 | {% for commit in commits %} 23 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 24 | {% if commit.breaking %}[**breaking**] {% endif %}\ 25 | {{ commit.message | upper_first }}\ 26 | {% endfor %} 27 | {% endfor %} 28 | """ 29 | 30 | # Template for the changelog footer 31 | footer = """ 32 | 33 | """ 34 | 35 | # Remove leading and trailing whitespaces from the changelog's body. 36 | trim = true 37 | # Render body even when there are no releases to process. 38 | render_always = true 39 | # An array of regex based postprocessors to modify the changelog. 40 | postprocessors = [ 41 | # Replace the placeholder with a URL. 42 | { pattern = '', replace = "https://github.com/ravenexp/conan2-rs" }, 43 | ] 44 | # render body even when there are no releases to process 45 | # render_always = true 46 | # output file path 47 | # output = "test.md" 48 | 49 | [git] 50 | # Parse commits according to the conventional commits specification. 51 | # See https://www.conventionalcommits.org 52 | conventional_commits = true 53 | # Exclude commits that do not match the conventional commits specification. 54 | filter_unconventional = true 55 | # Require all commits to be conventional. 56 | # Takes precedence over filter_unconventional. 57 | require_conventional = false 58 | # Split commits on newlines, treating each line as an individual commit. 59 | split_commits = false 60 | # An array of regex based parsers to modify commit messages prior to further processing. 61 | commit_preprocessors = [ 62 | # Replace issue numbers with link templates to be updated in `changelog.postprocessors`. 63 | { pattern = '#([0-9]+)', replace = "[#${1}](/issues/${1})"}, 64 | # Check spelling of the commit message using https://github.com/crate-ci/typos. 65 | # If the spelling is incorrect, it will be fixed automatically. 66 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 67 | ] 68 | # Prevent commits that are breaking from being excluded by commit parsers. 69 | protect_breaking_commits = false 70 | # An array of regex based parsers for extracting data from the commit message. 71 | # Assigns commits to groups. 72 | # Optionally sets the commit's scope and can decide to exclude commits from further processing. 73 | commit_parsers = [ 74 | { message = "^feat", group = "🚀 Features" }, 75 | { message = "^fix", group = "🐛 Bug Fixes" }, 76 | { message = "^doc", group = "📚 Documentation" }, 77 | { message = "^perf", group = "⚡ Performance" }, 78 | { message = "^refactor", group = "🚜 Refactor" }, 79 | { message = "^style", group = "🎨 Styling" }, 80 | { message = "^test", group = "🧪 Testing" }, 81 | { message = "^build", group = "🛠️ Build" }, 82 | { message = "^ci", group = "🦊 CI" }, 83 | { message = "^chore\\(release\\): prepare for", skip = true }, 84 | { message = "^chore\\(deps.*\\)", skip = true }, 85 | { message = "^chore\\(pr\\)", skip = true }, 86 | { message = "^chore\\(pull\\)", skip = true }, 87 | { message = "^chore", group = "⚙️ Miscellaneous Tasks" }, 88 | { body = ".*security", group = "🛡️ Security" }, 89 | { message = "^revert", group = "◀️ Revert" }, 90 | { message = ".*", group = "💼 Other" }, 91 | ] 92 | # Exclude commits that are not matched by any commit parser. 93 | filter_commits = false 94 | # An array of link parsers for extracting external references, and turning them into URLs, using regex. 95 | link_parsers = [] 96 | # Include only the tags that belong to the current branch. 97 | use_branch_tags = false 98 | # Order releases topologically instead of chronologically. 99 | topo_order = false 100 | # Order releases topologically instead of chronologically. 101 | topo_order_commits = true 102 | # Order of commits in each group/release within the changelog. 103 | # Allowed values: newest, oldest 104 | sort_commits = "oldest" 105 | # Process submodules commits 106 | recurse_submodules = false 107 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # conan2-rs 2 | //! 3 | //! ## Introduction 4 | //! 5 | //! `conan2-rs` is a Cargo build script wrapper of the Conan C/C++ package manager 6 | //! (version 2.0 only). 7 | //! 8 | //! It automatically pulls the C/C++ library linking flags from Conan dependencies 9 | //! and passes them to `rustc`. 10 | //! 11 | //! ## Adding C/C++ dependencies using Conan 12 | //! 13 | //! The simplest way to add C/C++ dependencies to a Rust project using Conan 14 | //! is to add a plain `conanfile.txt` file as follows: 15 | //! 16 | //! ```text 17 | //! [requires] 18 | //! libxml2/2.13.8 19 | //! openssl/3.4.1 20 | //! zlib/1.3.1 21 | //! ``` 22 | //! 23 | //! ## Example usage 24 | //! 25 | //! Add `conan2` to the `Cargo.toml` build dependencies section: 26 | //! 27 | //! ```toml 28 | //! [build-dependencies] 29 | //! conan2 = "0.1" 30 | //! ``` 31 | //! 32 | //! Add the following lines to the project `build.rs` script to invoke `conan install` 33 | //! and pass the Conan dependency information to Cargo automatically: 34 | //! 35 | //! ```no_run 36 | //! use conan2::ConanInstall; 37 | //! 38 | //! ConanInstall::new().run().parse().emit(); 39 | //! ``` 40 | //! 41 | //! The most commonly used `build_type` Conan setting will be defined automatically 42 | //! depending on the current Cargo build profile: `debug` or `release`. 43 | //! 44 | //! The Conan executable is assumed to be named `conan` unless 45 | //! the `CONAN` environment variable is set to override. 46 | //! 47 | //! ## Advanced usage 48 | //! 49 | //! ### Automatic Conan profile inference from Cargo target 50 | //! 51 | //! Using custom Conan profiles with names derived from the Cargo target information 52 | //! and a reduced output verbosity level: 53 | //! 54 | //! ```no_run 55 | //! use conan2::{ConanInstall, ConanScope, ConanVerbosity}; 56 | //! 57 | //! let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); 58 | //! let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 59 | //! let conan_profile = format!("{}-{}", target_os, target_arch); 60 | //! 61 | //! ConanInstall::new() 62 | //! .profile(&conan_profile) 63 | //! .build_type("RelWithDebInfo") // Override the Cargo build profile 64 | //! .build("missing") 65 | //! .verbosity(ConanVerbosity::Error) // Silence Conan warnings 66 | //! .option(ConanScope::Global, "shared", "True") // Add some package options 67 | //! .option(ConanScope::Local, "power", "10") 68 | //! .option(ConanScope::Package("foolib"), "frob", "max") 69 | //! .option(ConanScope::Package("barlib/1.0"), "zoom", "True") 70 | //! .config("tools.build:skip_test", "True") // Add some Conan configs 71 | //! .run() 72 | //! .parse() 73 | //! .emit(); 74 | //! ``` 75 | //! 76 | //! ### Automatic Conan profile creation 77 | //! 78 | //! Creating a custom default Conan profile on the fly with zero configuration: 79 | //! 80 | //! ```no_run 81 | //! use conan2::{ConanInstall, ConanVerbosity}; 82 | //! 83 | //! ConanInstall::new() 84 | //! .profile("cargo") 85 | //! .detect_profile() // Run `conan profile detect --exist-ok` for the above 86 | //! .run() 87 | //! .parse() 88 | //! .emit(); 89 | //! ``` 90 | //! 91 | //! ### Using separate host and build profiles 92 | //! 93 | //! Using different values for `--profile:host` and `--profile:build` 94 | //! arguments of `conan install` command: 95 | //! 96 | //! ```no_run 97 | //! use conan2::{ConanInstall, ConanVerbosity}; 98 | //! 99 | //! ConanInstall::new() 100 | //! .host_profile("cargo-host") 101 | //! .build_profile("cargo-build") 102 | //! .run() 103 | //! .parse() 104 | //! .emit(); 105 | //! ``` 106 | //! 107 | //! ### Getting C/C++ include paths from Conan dependencies 108 | //! 109 | //! To use the list of include paths, do the following after 110 | //! parsing the `conan install` output: 111 | //! 112 | //! ```no_run 113 | //! use conan2::ConanInstall; 114 | //! 115 | //! let metadata = ConanInstall::new().run().parse(); 116 | //! 117 | //! for path in metadata.include_paths() { 118 | //! // Add "-I{path}" to CXXFLAGS or something. 119 | //! } 120 | //! 121 | //! metadata.emit(); 122 | //! ``` 123 | 124 | #![deny(missing_docs)] 125 | 126 | use std::collections::BTreeSet; 127 | use std::ffi::OsStr; 128 | use std::io::{BufRead, Cursor, Write}; 129 | use std::path::{Path, PathBuf}; 130 | use std::process::{Command, Output}; 131 | 132 | use serde_json::{Map, Value}; 133 | 134 | /// Conan binary override environment variable 135 | const CONAN_ENV: &str = "CONAN"; 136 | 137 | /// Default Conan binary name 138 | const DEFAULT_CONAN: &str = "conan"; 139 | 140 | /// `conan` command verbosity level 141 | /// 142 | /// Defines the level of detail of the Conan command output. 143 | /// 144 | /// Enum variants correspond to the following options: 145 | /// `-vquiet`, `-verror`, `-vwarning`, `-vnotice`, `-vstatus`, 146 | /// `-v` or `-vverbose`, `-vv` or `-vdebug`, `-vvv` or `-vtrace`. 147 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 148 | pub enum ConanVerbosity { 149 | /// `-vquiet` 150 | Quiet, 151 | /// `-verror` 152 | Error, 153 | /// `-vwarning` 154 | #[default] 155 | Warning, 156 | /// `-vnotice` 157 | Notice, 158 | /// `-vstatus` 159 | Status, 160 | /// `-vverbose` 161 | Verbose, 162 | /// `-vdebug` 163 | Debug, 164 | /// `-vtrace` 165 | Trace, 166 | } 167 | 168 | /// `conan install` command option scope kind 169 | /// 170 | /// Defines the Conan install command option scope variant: 171 | /// local, global or per-package. 172 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 173 | pub enum ConanScope<'a> { 174 | /// `--options *:key=value` 175 | #[default] 176 | Global, 177 | /// `--options &:key=value` 178 | Local, 179 | /// `--options package:key=value` 180 | Package(&'a str), 181 | } 182 | 183 | /// `conan install` command builder 184 | /// 185 | /// This opaque type implements a command line builder for 186 | /// the `conan install` command invocation. 187 | #[derive(Default)] 188 | pub struct ConanInstall { 189 | /// Conan generators output directory 190 | output_folder: Option, 191 | /// Conan recipe file path 192 | recipe_path: Option, 193 | /// Conan host profile name 194 | profile: Option, 195 | /// Conan build profile name 196 | build_profile: Option, 197 | /// Conan profile auto-detection flag 198 | new_profile: bool, 199 | /// Conan build policy 200 | build: Option, 201 | /// Conan build type setting: 202 | /// one of "Debug", "Release", "RelWithDebInfo" and "MinSizeRel" 203 | build_type: Option, 204 | /// Conan conf options stored as `{key}={value}` 205 | confs: Vec<(String, String)>, 206 | /// Conan package build options stored as `{scope}:{key}={value}` 207 | options: Vec<(String, String, String)>, 208 | /// Conan output verbosity level 209 | verbosity: ConanVerbosity, 210 | } 211 | 212 | /// `conan install` command output data 213 | pub struct ConanOutput(Output); 214 | 215 | /// Build script instructions for Cargo 216 | pub struct CargoInstructions { 217 | /// Raw build script output 218 | out: Vec, 219 | /// C include paths collected from the packages 220 | includes: BTreeSet, 221 | /// C library search paths collected from the packages 222 | lib_dirs: BTreeSet, 223 | } 224 | 225 | /// Conan dependency graph as a JSON-based tree structure 226 | struct ConanDependencyGraph(Value); 227 | 228 | impl std::fmt::Display for ConanVerbosity { 229 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 230 | match self { 231 | ConanVerbosity::Quiet => f.write_str("quiet"), 232 | ConanVerbosity::Error => f.write_str("error"), 233 | ConanVerbosity::Warning => f.write_str("warning"), 234 | ConanVerbosity::Notice => f.write_str("notice"), 235 | ConanVerbosity::Status => f.write_str("status"), 236 | ConanVerbosity::Verbose => f.write_str("verbose"), 237 | ConanVerbosity::Debug => f.write_str("debug"), 238 | ConanVerbosity::Trace => f.write_str("trace"), 239 | } 240 | } 241 | } 242 | 243 | impl std::fmt::Display for ConanScope<'_> { 244 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 245 | match self { 246 | ConanScope::Global => f.write_str("*"), 247 | ConanScope::Local => f.write_str("&"), 248 | ConanScope::Package(name) => { 249 | if name.contains('/') { 250 | // "name/version" combination works as it is. 251 | f.write_str(name) 252 | } else { 253 | // Bare package names are not accepted by Conan for some reason. 254 | write!(f, "{name}/*") 255 | } 256 | } 257 | } 258 | } 259 | } 260 | 261 | impl ConanInstall { 262 | /// Creates a new `conan install` command with the default recipe path (`.`). 263 | #[must_use] 264 | pub fn new() -> ConanInstall { 265 | ConanInstall::default() 266 | } 267 | 268 | /// Creates a new `conan install` command with the user-provided recipe path. 269 | #[must_use] 270 | pub fn with_recipe(recipe_path: &Path) -> ConanInstall { 271 | ConanInstall { 272 | recipe_path: Some(recipe_path.to_owned()), 273 | ..Default::default() 274 | } 275 | } 276 | 277 | /// Sets a custom Conan generator output folder path. 278 | /// 279 | /// Matches `--output-folder` Conan executable option. 280 | /// 281 | /// This method should not be used in most cases: 282 | /// 283 | /// The Cargo-provided `OUT_DIR` environment variable value is used 284 | /// as the default when the command is invoked from a build script. 285 | pub fn output_folder(&mut self, output_folder: &Path) -> &mut ConanInstall { 286 | self.output_folder = Some(output_folder.to_owned()); 287 | self 288 | } 289 | 290 | /// Sets the Conan profile name to use for installing dependencies. 291 | /// 292 | /// Matches `--profile` Conan executable option. 293 | pub fn profile(&mut self, profile: &str) -> &mut ConanInstall { 294 | self.profile = Some(profile.to_owned()); 295 | self 296 | } 297 | 298 | /// Sets the Conan host profile name to use for installing dependencies. 299 | /// 300 | /// Matches `--profile:host` Conan executable option. 301 | pub fn host_profile(&mut self, profile: &str) -> &mut ConanInstall { 302 | self.profile(profile) 303 | } 304 | 305 | /// Sets the Conan build profile name to use for installing dependencies. 306 | /// 307 | /// Matches `--profile:build` Conan executable option. 308 | pub fn build_profile(&mut self, profile: &str) -> &mut ConanInstall { 309 | self.build_profile = Some(profile.to_owned()); 310 | self 311 | } 312 | 313 | /// Auto-detects and creates the Conan profile to use for installing dependencies. 314 | /// 315 | /// Schedules `conan profile detect --exist-ok` to run before running `conan install`. 316 | pub fn detect_profile(&mut self) -> &mut ConanInstall { 317 | self.new_profile = true; 318 | self 319 | } 320 | 321 | /// Overrides the default Conan build type setting value for `conan install`. 322 | /// 323 | /// Matches `--settings build_type={value}` Conan executable option. 324 | /// 325 | /// NOTE: The default value for this setting will be inferred automatically 326 | /// depending on the current Cargo build profile as either 327 | /// `"Debug"` or `"Release"`. 328 | pub fn build_type(&mut self, build_type: &str) -> &mut ConanInstall { 329 | self.build_type = Some(build_type.to_owned()); 330 | self 331 | } 332 | 333 | /// Adds the Conan configuration options (confs). 334 | /// 335 | /// Matches `--conf {key}={value}` Conan executable option. 336 | /// Can be called multiple times per Conan invocation. 337 | pub fn config(&mut self, key: &str, value: &str) -> &mut ConanInstall { 338 | self.confs.push((key.to_owned(), value.to_owned())); 339 | 340 | self 341 | } 342 | 343 | /// Adds the Conan package options to use for installing dependencies. 344 | /// 345 | /// Matches `--options {scope}:{key}={value}` Conan executable option. 346 | /// Can be called multiple times per Conan invocation. 347 | pub fn option(&mut self, scope: ConanScope, key: &str, value: &str) -> &mut ConanInstall { 348 | self.options 349 | .push((scope.to_string(), key.to_owned(), value.to_owned())); 350 | 351 | self 352 | } 353 | 354 | /// Sets the Conan dependency build policy for `conan install`. 355 | /// 356 | /// Matches `--build` Conan executable option. 357 | pub fn build(&mut self, build: &str) -> &mut ConanInstall { 358 | self.build = Some(build.to_owned()); 359 | self 360 | } 361 | 362 | /// Sets the Conan command verbosity level. 363 | /// 364 | /// Matches `-v` Conan executable option. 365 | pub fn verbosity(&mut self, verbosity: ConanVerbosity) -> &mut ConanInstall { 366 | self.verbosity = verbosity; 367 | self 368 | } 369 | 370 | /// Runs the `conan install` command and captures its JSON-formatted output. 371 | /// 372 | /// # Panics 373 | /// 374 | /// Panics if the Conan executable cannot be found. 375 | #[must_use] 376 | pub fn run(&self) -> ConanOutput { 377 | let conan = std::env::var_os(CONAN_ENV).unwrap_or_else(|| DEFAULT_CONAN.into()); 378 | let recipe = self.recipe_path.as_deref().unwrap_or(Path::new(".")); 379 | 380 | let output_folder = match &self.output_folder { 381 | Some(s) => s.clone(), 382 | None => std::env::var_os("OUT_DIR") 383 | .expect("OUT_DIR environment variable must be set") 384 | .into(), 385 | }; 386 | 387 | if self.new_profile { 388 | Self::run_profile_detect(&conan, self.profile.as_deref()); 389 | 390 | if self.build_profile != self.profile { 391 | Self::run_profile_detect(&conan, self.build_profile.as_deref()); 392 | }; 393 | } 394 | 395 | let mut command = Command::new(conan); 396 | command 397 | .arg("install") 398 | .arg(recipe) 399 | .arg(format!("-v{}", self.verbosity)) 400 | .arg("--format") 401 | .arg("json") 402 | .arg("--output-folder") 403 | .arg(output_folder); 404 | 405 | if let Some(profile) = self.profile.as_deref() { 406 | command.arg("--profile:host").arg(profile); 407 | } 408 | 409 | if let Some(build_profile) = self.build_profile.as_deref() { 410 | command.arg("--profile:build").arg(build_profile); 411 | } 412 | 413 | if let Some(build) = self.build.as_deref() { 414 | command.arg("--build"); 415 | command.arg(build); 416 | } 417 | 418 | if let Some(build_type) = self.build_type.as_deref() { 419 | // Prefer the user-provided build setting values. 420 | command.arg("--settings"); 421 | command.arg(format!("build_type={build_type}")); 422 | } else { 423 | // Otherwise, use additional environment variables set by Cargo. 424 | Self::add_settings_from_env(&mut command); 425 | } 426 | 427 | for (scope, key, value) in &self.options { 428 | command.arg("--options"); 429 | command.arg(format!("{scope}:{key}={value}")); 430 | } 431 | 432 | for (key, value) in &self.confs { 433 | command.arg("--conf"); 434 | command.arg(format!("{key}={value}")); 435 | } 436 | 437 | let output = command 438 | .output() 439 | .expect("failed to run the Conan executable"); 440 | 441 | ConanOutput(output) 442 | } 443 | 444 | /// Creates a new profile with `conan profile detect` if required. 445 | fn run_profile_detect(conan: &OsStr, profile: Option<&str>) { 446 | let mut command = Command::new(conan); 447 | command.arg("profile").arg("detect").arg("--exist-ok"); 448 | 449 | if let Some(profile) = profile { 450 | println!("running 'conan profile detect' for profile '{profile}'"); 451 | 452 | command.arg("--name").arg(profile); 453 | } else { 454 | println!("running 'conan profile detect' for the default profile"); 455 | } 456 | 457 | let status = command 458 | .status() 459 | .expect("failed to run the Conan executable"); 460 | 461 | #[allow(clippy::manual_assert)] 462 | if !status.success() { 463 | panic!("'conan profile detect' command failed: {status}"); 464 | } 465 | } 466 | 467 | /// Adds automatic Conan settings arguments derived 468 | /// from the environment variables set by Cargo. 469 | /// 470 | /// The following Conan settings are auto-detected and set: 471 | /// 472 | /// - `build_type` 473 | fn add_settings_from_env(command: &mut Command) { 474 | match std::env::var("PROFILE").as_deref() { 475 | Ok("debug") => { 476 | command.arg("-s"); 477 | command.arg("build_type=Debug"); 478 | } 479 | Ok("release") => { 480 | command.arg("-s"); 481 | command.arg("build_type=Release"); 482 | } 483 | _ => (), 484 | } 485 | } 486 | } 487 | 488 | impl ConanOutput { 489 | /// Parses `conan install` command output and generates build script 490 | /// instructions for Cargo. 491 | /// 492 | /// # Panics 493 | /// 494 | /// Panics if the Conan command invocation failed or 495 | /// the JSON-formatted Conan output could not be parsed. 496 | #[must_use] 497 | pub fn parse(self) -> CargoInstructions { 498 | // Panic if the `conan install` command has failed. 499 | self.ensure_success(); 500 | 501 | let mut cargo = CargoInstructions::new(); 502 | 503 | // Re-run the build script if `CONAN` environment variable changes. 504 | cargo.rerun_if_env_changed(CONAN_ENV); 505 | 506 | // Pass Conan warnings through to Cargo using build script instructions. 507 | for line in Cursor::new(self.stderr()).lines() { 508 | if let Some(msg) = line.unwrap().strip_prefix("WARN: ") { 509 | cargo.warning(msg); 510 | } 511 | } 512 | 513 | // Parse the JSON-formatted `conan install` command output. 514 | let metadata: Value = 515 | serde_json::from_slice(self.stdout()).expect("failed to parse JSON output"); 516 | 517 | // Walk the dependency graph and collect the C/C++ libraries. 518 | ConanDependencyGraph(metadata).traverse(&mut cargo); 519 | 520 | cargo 521 | } 522 | 523 | /// Ensures that the Conan command has been executed successfully. 524 | /// 525 | /// # Panics 526 | /// 527 | /// Panics with an error message if the Conan command invocation failed. 528 | pub fn ensure_success(&self) { 529 | if self.is_success() { 530 | return; 531 | } 532 | 533 | let code = self.status_code(); 534 | let msg = String::from_utf8_lossy(self.stderr()); 535 | 536 | panic!("Conan failed with status {code}: {msg}"); 537 | } 538 | 539 | /// Checks the Conan install command execution status. 540 | #[must_use] 541 | pub fn is_success(&self) -> bool { 542 | self.0.status.success() 543 | } 544 | 545 | /// Gets the Conan install command execution status code. 546 | #[must_use] 547 | pub fn status_code(&self) -> i32 { 548 | self.0.status.code().unwrap_or_default() 549 | } 550 | 551 | /// Gets the Conan JSON-formatted output as bytes. 552 | #[must_use] 553 | pub fn stdout(&self) -> &[u8] { 554 | &self.0.stdout 555 | } 556 | 557 | /// Gets the Conan command error message as bytes. 558 | #[must_use] 559 | pub fn stderr(&self) -> &[u8] { 560 | &self.0.stderr 561 | } 562 | } 563 | 564 | impl CargoInstructions { 565 | /// Emits build script instructions for Cargo into `stdout`. 566 | /// 567 | /// # Panics 568 | /// 569 | /// Panics if the Cargo build instructions can not be written to `stdout`. 570 | pub fn emit(&self) { 571 | std::io::stdout().write_all(self.as_bytes()).unwrap(); 572 | } 573 | 574 | /// Gets the Cargo instruction lines as bytes. 575 | #[must_use] 576 | pub fn as_bytes(&self) -> &[u8] { 577 | &self.out 578 | } 579 | 580 | /// Gets the C/C++ include directory paths for all dependencies. 581 | #[must_use] 582 | pub fn include_paths(&self) -> Vec { 583 | self.includes.iter().cloned().collect() 584 | } 585 | 586 | /// Gets the C/C++ library search directory paths for all dependencies. 587 | #[must_use] 588 | pub fn library_paths(&self) -> Vec { 589 | self.lib_dirs.iter().cloned().collect() 590 | } 591 | 592 | /// Creates a new empty Cargo instructions list. 593 | fn new() -> CargoInstructions { 594 | CargoInstructions { 595 | out: Vec::with_capacity(1024), 596 | includes: BTreeSet::new(), 597 | lib_dirs: BTreeSet::new(), 598 | } 599 | } 600 | 601 | /// Adds `cargo:warning={message}` instruction. 602 | fn warning(&mut self, message: &str) { 603 | writeln!(self.out, "cargo:warning={message}").unwrap(); 604 | } 605 | 606 | /// Adds `cargo:rerun-if-env-changed={val}` instruction. 607 | fn rerun_if_env_changed(&mut self, val: &str) { 608 | writeln!(self.out, "cargo:rerun-if-env-changed={val}").unwrap(); 609 | } 610 | 611 | /// Adds `cargo:rustc-cdylib-link-arg={val}` instruction. 612 | fn rustc_cdylib_link_arg(&mut self, val: &str) { 613 | writeln!(self.out, "cargo:rustc-cdylib-link-arg={val}").unwrap(); 614 | } 615 | 616 | /// Adds `cargo:rustc-link-arg-bins={val}` instruction. 617 | fn rustc_link_arg_bins(&mut self, val: &str) { 618 | writeln!(self.out, "cargo:rustc-link-arg-bins={val}").unwrap(); 619 | } 620 | 621 | /// Adds `cargo:rustc-link-lib=[(dylib|static)=]{lib}` instruction. 622 | /// 623 | /// The library linking type (dynamic or static) may be inferred 624 | /// from the file name pattern on Linux-like platforms. 625 | fn rustc_link_lib(&mut self, lib: &str) { 626 | // When the full library file name is supplied, 627 | // convert `libfoo.a` and `libfoo.so` into `foo` automatically. 628 | if let Some(lib) = lib.strip_prefix("lib") { 629 | if let Some(lib) = lib.strip_suffix(".a") { 630 | self.rustc_link_lib_kind(lib, Some("static")); 631 | return; 632 | } else if let Some(lib) = lib.strip_suffix(".so") { 633 | self.rustc_link_lib_kind(lib, Some("dylib")); 634 | return; 635 | } 636 | } 637 | 638 | self.rustc_link_lib_kind(lib, None); 639 | } 640 | 641 | /// Adds `cargo:rustc-link-lib=[{kind}=]{lib}` instruction. 642 | fn rustc_link_lib_kind(&mut self, lib: &str, kind: Option<&str>) { 643 | match kind { 644 | Some(kind) => { 645 | writeln!(self.out, "cargo:rustc-link-lib={kind}={lib}").unwrap(); 646 | } 647 | None => { 648 | writeln!(self.out, "cargo:rustc-link-lib={lib}").unwrap(); 649 | } 650 | } 651 | } 652 | 653 | /// Adds `cargo:rustc-link-search={path}` instruction. 654 | fn rustc_link_search(&mut self, path: &str) { 655 | let lib_dir = path.into(); 656 | if !self.lib_dirs.contains(&lib_dir) { 657 | writeln!(self.out, "cargo:rustc-link-search={path}").unwrap(); 658 | self.lib_dirs.insert(lib_dir); 659 | } 660 | } 661 | 662 | /// Adds `cargo:include={path}` instruction. 663 | fn include(&mut self, path: &str) { 664 | let include_dir = path.into(); 665 | if !self.includes.contains(&include_dir) { 666 | writeln!(self.out, "cargo:include={path}").unwrap(); 667 | self.includes.insert(include_dir); 668 | } 669 | } 670 | } 671 | 672 | impl ConanDependencyGraph { 673 | /// Traverses the dependency graph and emits the `rustc` link instructions 674 | /// in the correct linking order. 675 | fn traverse(self, cargo: &mut CargoInstructions) { 676 | // Consumer package node id: the root of the graph 677 | let root_node_id = "0"; 678 | 679 | self.visit_dependency(cargo, root_node_id); 680 | } 681 | 682 | /// Visits the dependencies recursively starting from node `node_id` 683 | /// and emits `rustc` link instructions. 684 | fn visit_dependency(&self, cargo: &mut CargoInstructions, node_id: &str) { 685 | let Some(node) = self.find_node(node_id) else { 686 | return; 687 | }; 688 | 689 | if let Some(Value::Object(cpp_info)) = node.get("cpp_info") { 690 | for cpp_comp_name in cpp_info.keys() { 691 | Self::visit_cpp_component(cargo, cpp_info, cpp_comp_name); 692 | } 693 | }; 694 | 695 | // Recursively visit transitive dependencies. 696 | if let Some(Value::Object(dependencies)) = node.get("dependencies") { 697 | for dependency_id in dependencies.keys() { 698 | self.visit_dependency(cargo, dependency_id); 699 | } 700 | }; 701 | } 702 | 703 | /// Visits the dependency package components recursively starting from 704 | /// the component named `comp_name` and emits `rustc` link instructions. 705 | fn visit_cpp_component( 706 | cargo: &mut CargoInstructions, 707 | cpp_info: &Map, 708 | comp_name: &str, 709 | ) { 710 | let Some(component) = Self::find_cpp_component(cpp_info, comp_name) else { 711 | return; 712 | }; 713 | 714 | // 1. Emit packaged library link instructions for `rustc`. 715 | if let Some(Value::Array(libs)) = component.get("libs") { 716 | // FIXME: Many non-library Conan packages in the wild have 717 | // non-empty "libdirs" attribute arrays. 718 | // Ignore them and do not emit bogus library search paths. 719 | if !libs.is_empty() { 720 | // 1.1. Emit linker search directory instructions for `rustc`. 721 | if let Some(Value::Array(libdirs)) = component.get("libdirs") { 722 | for libdir in libdirs { 723 | if let Value::String(libdir) = libdir { 724 | cargo.rustc_link_search(libdir); 725 | } 726 | } 727 | } 728 | } 729 | 730 | // 1.2. Emit library link by name (`-lfoo`) instructions for `rustc`. 731 | for lib in libs { 732 | if let Value::String(lib) = lib { 733 | cargo.rustc_link_lib(lib); 734 | } 735 | } 736 | } 737 | 738 | // 2. Emit system library link by name (`-lbar`) instructions for `rustc`. 739 | if let Some(Value::Array(system_libs)) = component.get("system_libs") { 740 | for system_lib in system_libs { 741 | if let Value::String(system_lib) = system_lib { 742 | cargo.rustc_link_lib(system_lib); 743 | } 744 | } 745 | }; 746 | 747 | // 3. Emit `cargo:include=DIR` metadata for Rust dependencies. 748 | if let Some(Value::Array(includedirs)) = component.get("includedirs") { 749 | for include in includedirs { 750 | if let Value::String(include) = include { 751 | cargo.include(include); 752 | } 753 | } 754 | }; 755 | 756 | // 4. Emit `cargo:rustc-cdylib-link-arg=FLAGS` metadata for `rustc`. 757 | if let Some(Value::Array(flags)) = component.get("sharedlinkflags") { 758 | for flag in flags { 759 | if let Value::String(flag) = flag { 760 | cargo.rustc_cdylib_link_arg(flag); 761 | } 762 | } 763 | } 764 | 765 | // 5. Emit `cargo:rustc-link-arg-bins=FLAGS` metadata for `rustc`. 766 | if let Some(Value::Array(flags)) = component.get("exelinkflags") { 767 | for flag in flags { 768 | if let Value::String(flag) = flag { 769 | cargo.rustc_link_arg_bins(flag); 770 | } 771 | } 772 | } 773 | 774 | // 6. Recursively visit dependency component requirements. 775 | if let Some(Value::Array(requires)) = component.get("requires") { 776 | for requirement in requires { 777 | if let Value::String(req_comp_name) = requirement { 778 | Self::visit_cpp_component(cargo, cpp_info, req_comp_name); 779 | } 780 | } 781 | }; 782 | } 783 | 784 | /// Gets the dependency node field map by the node `id` key. 785 | fn find_node(&self, id: &str) -> Option<&Map> { 786 | let Value::Object(root) = &self.0 else { 787 | panic!("root JSON object expected"); 788 | }; 789 | 790 | let Some(Value::Object(graph)) = root.get("graph") else { 791 | panic!("root 'graph' object expected"); 792 | }; 793 | 794 | let Some(Value::Object(nodes)) = graph.get("nodes") else { 795 | panic!("root 'nodes' object expected"); 796 | }; 797 | 798 | if let Some(Value::Object(node)) = nodes.get(id) { 799 | Some(node) 800 | } else { 801 | None 802 | } 803 | } 804 | 805 | /// Gets the dependency component field map by its name. 806 | fn find_cpp_component<'a>( 807 | cpp_info: &'a Map, 808 | name: &str, 809 | ) -> Option<&'a Map> { 810 | if let Some(Value::Object(component)) = cpp_info.get(name) { 811 | Some(component) 812 | } else { 813 | None 814 | } 815 | } 816 | } 817 | --------------------------------------------------------------------------------