#build-script #package-manager #cmake #build-dependencies #cc #integrating #io

build conan

A Rust wrapper of the conan C/C++ package manager (conan.io) to simplify usage in build scripts

8 releases

0.4.2 Nov 2, 2023
0.4.1 Nov 1, 2023
0.3.0 Oct 17, 2022
0.2.0 Jun 2, 2020
0.1.2 May 25, 2019

#36 in Build Utils

Download history 788/week @ 2023-12-11 510/week @ 2023-12-18 64/week @ 2023-12-25 355/week @ 2024-01-01 625/week @ 2024-01-08 358/week @ 2024-01-15 767/week @ 2024-01-22 707/week @ 2024-01-29 836/week @ 2024-02-05 947/week @ 2024-02-12 1510/week @ 2024-02-19 1112/week @ 2024-02-26 754/week @ 2024-03-04 718/week @ 2024-03-11 1005/week @ 2024-03-18 285/week @ 2024-03-25

2,815 downloads per month

MIT/Apache

52KB
1K SLoC

conan-rs

A Rust wrapper of the conan C/C++ package manager (conan.io) to simplify usage in build scripts

TLDR

Add conan to the build-dependencies section:

cargo add conan --build

Modify the project build.rs script to invoke cargo and emit the conan build information automatically. Using conan profiles with names derived from the cargo target information is recommended:

NOTE: The conan executable is assumed to be conan unless the CONAN environment variable is set.

use std::path::Path;
use std::env;

use conan::*;

fn main() {
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
    let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
    let conan_profile = format!("{}-{}", target_os, target_arch);

    let command = InstallCommandBuilder::new()
        .with_profile(&conan_profile)
        .build_policy(BuildPolicy::Missing)
        .with_option("sign", "True")
        .recipe_path(Path::new("conanfile.txt"))
        .build();

    if let Some(build_info) = command.generate() {
        println!("using conan build info");
        build_info.cargo_emit();
    }

    let build_comman = BuildCommandBuilder::new()
        .with_recipe_path(PathBuf::from("../../../conanfile.py"))
        .with_build_path(PathBuf::from("../../../build/"))
        .build();

    if let Some(exit_status) = build_comman.run() {
        println!("conan build exited with {}", exit_status);
    }
}

The simplest approach is to add a conanfile.txt file alongside build.rs:

[requires]
openssl/1.1.1l@devolutions/stable

To test if the conan packages are properly imported, run cargo -vv build, and look for output similar to this:

[conan-test 0.1.0] using conan build info
[conan-test 0.1.0] cargo:rustc-link-search=native=/Users/mamoreau/.conan/data/openssl/1.1.1l/devolutions/stable/package/ce597277d61571523403b5b500bda70acd77cd8a/lib
[conan-test 0.1.0] cargo:rustc-link-lib=crypto
[conan-test 0.1.0] cargo:rustc-link-lib=ssl
[conan-test 0.1.0] cargo:include=/Users/mamoreau/.conan/data/openssl/1.1.1l/devolutions/stable/package/ce597277d61571523403b5b500bda70acd77cd8a/include
[conan-test 0.1.0] cargo:rerun-if-env-changed=CONAN

This sample conan recipe is available here, even if it is not available in a public conan repository.

Documentation

Conan Install

The InstallCommand struct represents the "conan install" command, facilitating package installation and dependency management in Rust projects. InstallCommandBuilder provides a fluent API for constructing an InstallCommand.

Example

use conan::{InstallCommandBuilder, BuildPolicy};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let install_command = InstallCommandBuilder::new()
        .with_profile("default")
        .build_policy(BuildPolicy::Missing)
        .recipe_path(Path::new("conanfile.txt"))
        .output_dir(Path::new("output_directory"))
        .build();

    if install_command.generate().is_some() {
        println!("Packages installed successfully!");
    } else {
        println!("Failed to install packages.");
    }

    Ok(())
}

In this example, InstallCommandBuilder configures the Conan install command with a profile, build policy, recipe file path, and output directory. generate() executes the command, returning Some(BuildInfo) on success or None on failure.

Conan Build

The BuildCommand struct represents the "conan build" command, facilitating the build process of Conan packages in Rust projects. BuildCommandBuilder provides a fluent API to construct a BuildCommand.

Example

use conan::BuildCommandBuilder;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let build_command = BuildCommandBuilder::new()
        .with_recipe_path(PathBuf::from("conanfile.py"))
        .with_build_path(PathBuf::from("build"))
        .should_configure(true)
        .should_build(true)
        .should_install(true)
        .build();

    match build_command.run() {
        Some(status) if status.success() => println!("Build succeeded!"),
        _ => println!("Build failed."),
    }

    Ok(())
}

In this example, BuildCommandBuilder is used to configure the Conan build command with paths and options. run() executes the command, returning Some(ExitStatus) on success or None on failure.

Conan Package

The PackageCommand struct represents the "conan package" command and is used for creating packages. The PackageCommandBuilder provides a fluent API for constructing a PackageCommand.

The ConanPackage struct provides functionality for managing Conan packages that generate C++ libraries, and it aids in linking these libraries with Rust.

Example Usage:

use conan::{PackageCommandBuilder, PackageComman, ConanPackage};
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let package_command = PackageCommandBuilder::new()
        .with_recipe_path(PathBuf::from("conanfile.py"))
        .build();

    if let Some(status) = package_command.run() {
        if !status.success() {
            println!("Package command failed.");
            return Ok(());
        }
    }

    let conan_package = ConanPackage::new(PathBuf::from("./package/"));
    conan_package.emit_cargo_libs_linkage(PathBuf::from("lib"))?;

    Ok(())
}

Use Case: Integrating Rust into a legacy c++/conan1 codebase

Integrating Rust into a legacy C++ codebase can be a strategic move to leverage Rust's memory safety features while maintaining existing C++ functionality. In this guide, we will explore how to integrate Rust into a legacy C++/Conan codebase using conan-rs and autocxx.

Existing C++ Conan Codebase Structure

Your existing C++ codebase with Conan and CMake might look like this:

.
├── build
│   ├── bin
│   │   └── target_bin
│   ├── lib
│   │   ├── lib1.a
│   │   ├── lib2.a
│   │   ├── lib3.so
│   │   ├── lib4.so
│   │   ├── ...
│   │   └── libn.a
├── CMakeLists.txt
├── conanfile.py
├── include
│   └── ...
├── profiles
│   ├── ...
├── src
│   ├── target_bin
│   │   ├── ...
│   ├── lib1
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   │   └── ...
│   │   ├── src
│   │   │   └── ...
│   ├── ...
│   ├── libn
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   │   └── ...
│   │   ├── src
│   │   │   └── ...

Make sure that after a build the build dir look like this(Your configuration may vary):

├── build
│   ├── bin
│   │   └── target_bin
│   ├── lib
│   │   ├── lib1.a
│   │   ├── lib2.a
│   │   ├── lib3.so
│   │   ├── lib4.so
│   │   ├── ...
│   │   └── libn.a

Also, the package() method in your conanfile should organize your libs and associated includes in a config akin to:

package
├── conaninfo.txt
├── conanmanifest.txt
├── include
└── lib
├── lib1.a
├── ...
└── libn.so

Creating the Rust "Bridge" Crate

Create a Rust library crate within the codebase to act as the "bridge" between the C++ and Rust code:

.
├── build.rs
├── Cargo.lock
├── Cargo.toml
└── src
├── lib.rs
└── main.rs

Setting Up Dependencies

Install conan-rs and autocxx:

cargo add conan-rs autocxx --build
cargo add autocxx

Setting the build script:

In your crate's build script (build.rs), configure the integration:

use conan::{
    BuildCommandBuilder, BuildPolicy, ConanPackage, InstallCommandBuilder, PackageCommandBuilder,
};
use std::env;
use std::path::{Path, PathBuf};
use std::process;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=../../path/to/your/conanfile.py");
    println!("cargo:rerun-if-changed=../../path/to/your/build/directory");

    let out_dir = env::var("OUT_DIR").map(PathBuf::from).unwrap_or_else(|_| {
        eprintln!("Error: OUT_DIR environment variable is not set");
        process::exit(1);
    });

    println!("OUT_DIR: {:?}", out_dir);

    let conan_profile = env::var("CONAN_PROFILE").unwrap_or_else(|_| "default".to_string());
    let install_command = InstallCommandBuilder::new()
        .with_profile(&conan_profile)
        .with_remote("your_remote")
        .build_policy(BuildPolicy::Missing)
        .with_profile("../../path/to/your/conan/profile")
        .recipe_path(Path::new("../../path/to/your/conanfile.py"))
        .output_dir(Path::new("../../path/to/your/build/directory"))
        .with_options(&["option1=True", "option2=True"])
        .update_check()
        .build();

    if let Some(build_info) = install_command.generate() {
        println!("using conan build info");
        build_info.cargo_emit();
    } else {
        eprintln!("Error: failed to run conan install");
        process::exit(1);
    }

    BuildCommandBuilder::new()
        .with_recipe_path(PathBuf::from("../../path/to/your/conanfile.py"))
        .with_build_path(PathBuf::from("../../path/to/your/build/directory"))
        .build()
        .run()
        .unwrap_or_else(|| {
            eprintln!("Error: Unable to run conan build");
            process::exit(1);
        });

    let package_command = PackageCommandBuilder::new()
        .with_recipe_path(PathBuf::from("../../path/to/your/conanfile.py"))
        .with_build_path(PathBuf::from("../../path/to/your/build/directory"))
        .with_package_path(out_dir.clone())
        .build();

    if let Some(exit_status) = package_command.run() {
        println!("conan package exited with {}", exit_status);
    }

    let conan_package = ConanPackage::new(out_dir.clone());
    if let Err(err) = conan_package.emit_cargo_libs_linkage("lib".into()) {
        eprintln!("Error: Unable to emit cargo linkage: {:?}", err);
        process::exit(1);
    }

    let include_path = out_dir.join("include");
    let mut builder = autocxx_build::Builder::new("src/lib.rs", &[include_path])
        .build()
        .unwrap_or_else(|err| {
            eprintln!("Error: Unable to generate bindings: {:?}", err);
            process::exit(1);
        });

    builder.flag_if_supported("-std=c++14").compile("foo_bar");
    println!("cargo:rerun-if-changed=src/main.rs");
}

Using C++ Libraries in Rust

Finally, use the C++ libraries in lib.rs:

use autocxx::prelude::*;

include_cpp! {
    #include "path/to/header.h"
    safety!(unsafe_ffi)
    generate!("FunctionFromCpp")
}

pub fn use_cpp_function() {
    let result = ffi::FunctionFromCpp();
    // Use result as needed
}

Dependencies

~3.5–5.5MB
~104K SLoC