# Find the Rust toolchain and add the `add_rust_library()` API to build Rust # libraries. # # Copyright (C) 2021-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved. # # Author: Micah Snyder # To see this in a sample project, visit: https://github.com/micahsnyder/cmake-rust-demo # # Code to set the Cargo arguments was lifted from: # https://github.com/Devolutions/CMakeRust # # This Module defines the following variables: # - _FOUND - True if the program was found # - _EXECUTABLE - path of the program # - _VERSION - version number of the program # # ... for the following Rust toolchain programs: # - cargo # - rustc # - rustup # - rust-gdb # - rust-lldb # - rustdoc # - rustfmt # - bindgen # # Callers can make any program mandatory by setting `_REQUIRED` before # the call to `find_package(Rust)` # # Eg: # # if(MAINTAINER_MODE) # set(bindgen_REQUIRED 1) # endif() # find_package(Rust REQUIRED) # # This module also provides: # # - `add_rust_library()` - This allows a caller to create a Rust static library # target which you can link to with `target_link_libraries()`. # # Your Rust static library target will itself depend on the native static libs # you get from `rustc --crate-type staticlib --print=native-static-libs /dev/null` # # The CARGO_CMD environment variable will be set to "BUILD" so you can tell # it's not building the unit tests inside your (optional) `build.rs` file. # # Example `add_rust_library()` usage: # # add_rust_library(TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") # add_library(YourProject::yourlib ALIAS yourlib) # # add_executable(yourexe) # target_link_libraries(yourexe YourProject::yourlib) # # - `add_rust_test()` - This allows a caller to run `cargo test` for a specific # Rust target as a CTest test. # # The CARGO_CMD environment variable will be set to "TEST" so you can tell # it's not building the unit tests inside your (optional) `build.rs` file. # # Example `add_rust_library()` usage: # # add_rust_test(NAME yourlib WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/yourlib") # set_property(TEST yourlib PROPERTY ENVIRONMENT ${ENVIRONMENT}) # if(NOT DEFINED CARGO_HOME) if(WIN32) set(CARGO_HOME "$ENV{USERPROFILE}/.cargo") else() set(CARGO_HOME "$ENV{HOME}/.cargo") endif() endif() include(FindPackageHandleStandardArgs) function(find_rust_program RUST_PROGRAM) find_program(${RUST_PROGRAM}_EXECUTABLE ${RUST_PROGRAM} HINTS "${CARGO_HOME}" PATH_SUFFIXES "bin" ) if(${RUST_PROGRAM}_EXECUTABLE) execute_process(COMMAND "${${RUST_PROGRAM}_EXECUTABLE}" --version OUTPUT_VARIABLE ${RUST_PROGRAM}_VERSION_OUTPUT ERROR_VARIABLE ${RUST_PROGRAM}_VERSION_ERROR RESULT_VARIABLE ${RUST_PROGRAM}_VERSION_RESULT ) if(NOT ${${RUST_PROGRAM}_VERSION_RESULT} EQUAL 0) message(STATUS "Rust tool `${RUST_PROGRAM}` not found: Failed to determine version.") unset(${RUST_PROGRAM}_EXECUTABLE) else() string(REGEX MATCH "[0-9]+\\.[0-9]+(\\.[0-9]+)?(-nightly)?" ${RUST_PROGRAM}_VERSION "${${RUST_PROGRAM}_VERSION_OUTPUT}" ) set(${RUST_PROGRAM}_VERSION "${${RUST_PROGRAM}_VERSION}" PARENT_SCOPE) message(STATUS "Rust tool `${RUST_PROGRAM}` found: ${${RUST_PROGRAM}_EXECUTABLE}, ${${RUST_PROGRAM}_VERSION}") endif() mark_as_advanced(${RUST_PROGRAM}_EXECUTABLE ${RUST_PROGRAM}_VERSION) else() if(${${RUST_PROGRAM}_REQUIRED}) message(FATAL_ERROR "Rust tool `${RUST_PROGRAM}` not found.") else() message(STATUS "Rust tool `${RUST_PROGRAM}` not found.") endif() endif() endfunction() function(cargo_vendor) set(options) set(oneValueArgs TARGET WORKING_DIRECTORY) cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT EXISTS ${ARGS_WORKING_DIRECTORY}}/.cargo/config.toml) # Vendor the dependencies and create .cargo/config.toml # Vendored dependencies will be used during the build. # This will allow us to package vendored dependencies in source tarballs # for online builds when we run `cpack --config CPackSourceConfig.cmake` message(STATUS "Running `cargo vendor` to collect dependencies for ${ARGS_TARGET}. This may take a while if the local crates.io index needs to be updated ...") make_directory(${ARGS_WORKING_DIRECTORY}/.cargo) execute_process( COMMAND ${CMAKE_COMMAND} -E env "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${cargo_EXECUTABLE} vendor ".cargo/vendor" WORKING_DIRECTORY "${ARGS_WORKING_DIRECTORY}" OUTPUT_VARIABLE CARGO_VENDOR_OUTPUT ERROR_VARIABLE CARGO_VENDOR_ERROR RESULT_VARIABLE CARGO_VENDOR_RESULT ) if(NOT ${CARGO_VENDOR_RESULT} EQUAL 0) message(FATAL_ERROR "Failed!\n${CARGO_VENDOR_ERROR}") else() message("Success!") endif() write_file(${ARGS_WORKING_DIRECTORY}/.cargo/config.toml " [source.crates-io] replace-with = \"vendored-sources\" [source.vendored-sources] directory = \".cargo/vendor\" " ) endif() endfunction() function(add_rust_library) set(options) set(oneValueArgs TARGET WORKING_DIRECTORY) cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(WIN32) set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${RUST_COMPILER_TARGET}/${LIB_BUILD_TYPE}/${ARGS_TARGET}.lib") else() set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${RUST_COMPILER_TARGET}/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a") endif() file(GLOB_RECURSE LIB_SOURCES "${ARGS_WORKING_DIRECTORY}/*.rs") set(MY_CARGO_ARGS ${CARGO_ARGS}) list(APPEND MY_CARGO_ARGS "--target-dir" ${CMAKE_CURRENT_BINARY_DIR}) list(JOIN MY_CARGO_ARGS " " MY_CARGO_ARGS_STRING) # Build the library and generate the c-binding if ("${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") add_custom_command( OUTPUT "${OUTPUT}" COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=build" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" "MAINTAINER_MODE=${MAINTAINER_MODE}" "RUSTFLAGS=\"${RUSTFLAGS}\"" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} --target=x86_64-apple-darwin COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=build" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" "MAINTAINER_MODE=${MAINTAINER_MODE}" "RUSTFLAGS=\"${RUSTFLAGS}\"" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} --target=aarch64-apple-darwin COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/${RUST_COMPILER_TARGET}/${LIB_BUILD_TYPE}" COMMAND lipo ARGS -create ${CMAKE_CURRENT_BINARY_DIR}/x86_64-apple-darwin/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a ${CMAKE_CURRENT_BINARY_DIR}/aarch64-apple-darwin/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a -output "${OUTPUT}" WORKING_DIRECTORY "${ARGS_WORKING_DIRECTORY}" DEPENDS ${LIB_SOURCES} COMMENT "Building ${ARGS_TARGET} in ${ARGS_WORKING_DIRECTORY} with: ${cargo_EXECUTABLE} ${MY_CARGO_ARGS_STRING}") else() add_custom_command( OUTPUT "${OUTPUT}" COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=build" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" "MAINTAINER_MODE=${MAINTAINER_MODE}" "RUSTFLAGS=\"${RUSTFLAGS}\"" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} WORKING_DIRECTORY "${ARGS_WORKING_DIRECTORY}" DEPENDS ${LIB_SOURCES} COMMENT "Building ${ARGS_TARGET} in ${ARGS_WORKING_DIRECTORY} with: ${cargo_EXECUTABLE} ${MY_CARGO_ARGS_STRING}") endif() # Create a target from the build output add_custom_target(${ARGS_TARGET}_target DEPENDS ${OUTPUT}) # Create a static imported library target from library target add_library(${ARGS_TARGET} STATIC IMPORTED GLOBAL) add_dependencies(${ARGS_TARGET} ${ARGS_TARGET}_target) target_link_libraries(${ARGS_TARGET} INTERFACE ${RUST_NATIVE_STATIC_LIBS}) # Specify where the library is and where to find the headers set_target_properties(${ARGS_TARGET} PROPERTIES IMPORTED_LOCATION "${OUTPUT}" INTERFACE_INCLUDE_DIRECTORIES "${ARGS_WORKING_DIRECTORY};${CMAKE_CURRENT_BINARY_DIR}" ) # Vendor the dependencies, if desired if(VENDOR_DEPENDENCIES) cargo_vendor(TARGET "${ARGS_TARGET}" WORKING_DIRECTORY "${ARGS_WORKING_DIRECTORY}") endif() endfunction() function(add_rust_test) set(options) set(oneValueArgs NAME WORKING_DIRECTORY) cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) set(MY_CARGO_ARGS "test") if (NOT "${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") # Don't specify the target for universal, we'll do that manually for each build. list(APPEND MY_CARGO_ARGS "--target" ${RUST_COMPILER_TARGET}) endif() if("${CMAKE_BUILD_TYPE}" STREQUAL "Release") list(APPEND MY_CARGO_ARGS "--release") endif() list(APPEND MY_CARGO_ARGS "--target-dir" ${CMAKE_CURRENT_BINARY_DIR}) list(JOIN MY_CARGO_ARGS " " MY_CARGO_ARGS_STRING) add_test( NAME ${ARGS_NAME} COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=test" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${cargo_EXECUTABLE} ${MY_CARGO_ARGS} --color always WORKING_DIRECTORY ${ARGS_WORKING_DIRECTORY} ) endfunction() # # Cargo is the primary tool for using the Rust Toolchain to to build static # libs that can include other crate dependencies. # find_rust_program(cargo) # These other programs may also be useful... find_rust_program(rustc) find_rust_program(rustup) find_rust_program(rust-gdb) find_rust_program(rust-lldb) find_rust_program(rustdoc) find_rust_program(rustfmt) find_rust_program(bindgen) # Determine the native libs required to link w/ rust static libs # message(STATUS "Detecting native static libs for rust: ${rustc_EXECUTABLE} --crate-type staticlib --print=native-static-libs /dev/null") execute_process( COMMAND ${CMAKE_COMMAND} -E env "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${rustc_EXECUTABLE} --crate-type staticlib --print=native-static-libs /dev/null OUTPUT_VARIABLE RUST_NATIVE_STATIC_LIBS_OUTPUT ERROR_VARIABLE RUST_NATIVE_STATIC_LIBS_ERROR RESULT_VARIABLE RUST_NATIVE_STATIC_LIBS_RESULT ) string(REGEX REPLACE "\r?\n" ";" LINE_LIST "${RUST_NATIVE_STATIC_LIBS_ERROR}") foreach(LINE ${LINE_LIST}) # do the match on each line string(REGEX MATCH "native-static-libs: .*" LINE "${LINE}") if(NOT LINE) continue() endif() string(REPLACE "native-static-libs: " "" LINE "${LINE}") string(REGEX REPLACE " " "" LINE "${LINE}") string(REGEX REPLACE " " ";" LINE "${LINE}") if(LINE) message(STATUS "Rust's native static libs: ${LINE}") set(RUST_NATIVE_STATIC_LIBS "${LINE}") break() endif() endforeach() if(NOT RUST_COMPILER_TARGET) # Automatically determine the Rust Target Triple. # Note: Users may override automatic target detection by specifying their own. Most likely needed for cross-compiling. # For reference determining target platform: https://doc.rust-lang.org/nightly/rustc/platform-support.html if(WIN32) # For windows x86/x64, it's easy enough to guess the target. if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(RUST_COMPILER_TARGET "x86_64-pc-windows-msvc") else() set(RUST_COMPILER_TARGET "i686-pc-windows-msvc") endif() elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin AND "${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") # Special case for Darwin because we may want to build universal binaries. set(RUST_COMPILER_TARGET "universal-apple-darwin") else() # Determine default LLVM target triple. execute_process(COMMAND ${rustc_EXECUTABLE} -vV OUTPUT_VARIABLE RUSTC_VV_OUT ERROR_QUIET) string(REGEX REPLACE "^.*host: ([a-zA-Z0-9_\\-]+).*" "\\1" DEFAULT_RUST_COMPILER_TARGET1 "${RUSTC_VV_OUT}") string(STRIP ${DEFAULT_RUST_COMPILER_TARGET1} DEFAULT_RUST_COMPILER_TARGET) set(RUST_COMPILER_TARGET "${DEFAULT_RUST_COMPILER_TARGET}") endif() endif() set(CARGO_ARGS "build") if (NOT "${RUST_COMPILER_TARGET}" MATCHES "^universal-apple-darwin$") # Don't specify the target for macOS universal builds, we'll do that manually for each build. list(APPEND CARGO_ARGS "--target" ${RUST_COMPILER_TARGET}) endif() set(RUSTFLAGS "") if(NOT CMAKE_BUILD_TYPE) set(LIB_BUILD_TYPE "debug") elseif(${CMAKE_BUILD_TYPE} STREQUAL "Release" OR ${CMAKE_BUILD_TYPE} STREQUAL "MinSizeRel") set(LIB_BUILD_TYPE "release") list(APPEND CARGO_ARGS "--release") elseif(${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo") set(LIB_BUILD_TYPE "release") list(APPEND CARGO_ARGS "--release") set(RUSTFLAGS "-g") else() set(LIB_BUILD_TYPE "debug") endif() find_package_handle_standard_args( Rust REQUIRED_VARS cargo_EXECUTABLE VERSION_VAR cargo_VERSION )