A Rust wrapper of the conan C/C++ package manager (conan.io) to simplify usage in build scripts
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_command = BuildCommandBuilder::new()
.with_recipe_path(PathBuf::from("../../../conanfile.py"))
.with_build_path(PathBuf::from("../../../build/"))
.build();
if let Some(exit_status) = build_command.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.
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
.
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.
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
.
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.
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.
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(())
}
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
.
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
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
Install conan-rs
and autocxx
:
cargo add conan-rs autocxx --build
cargo add autocxx
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");
}
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
}