# Find the Rust toolchain and add the `add_rust_library()` API to build Rust # libraries. # # Copyright (C) 2022 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}/${LIB_TARGET}/${LIB_BUILD_TYPE}/${ARGS_TARGET}.lib") else() set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${LIB_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}/${LIB_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" ${LIB_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} -vv --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() # 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_LIB_TARGET1 "${RUSTC_VV_OUT}") string(STRIP ${DEFAULT_LIB_TARGET1} DEFAULT_LIB_TARGET) if(WIN32) if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(LIB_TARGET "x86_64-pc-windows-msvc") else() set(LIB_TARGET "i686-pc-windows-msvc") endif() elseif(ANDROID) if(ANDROID_SYSROOT_ABI STREQUAL "x86") set(LIB_TARGET "i686-linux-android") elseif(ANDROID_SYSROOT_ABI STREQUAL "x86_64") set(LIB_TARGET "x86_64-linux-android") elseif(ANDROID_SYSROOT_ABI STREQUAL "arm") set(LIB_TARGET "arm-linux-androideabi") elseif(ANDROID_SYSROOT_ABI STREQUAL "arm64") set(LIB_TARGET "aarch64-linux-android") endif() elseif(IOS) set(LIB_TARGET "universal") # For reference determining target platform: # CMake Systems: https://github.com/Kitware/CMake/blob/master/Modules/CMakeDetermineSystem.cmake # Rust Targets: https://doc.rust-lang.org/nightly/rustc/platform-support.html elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin) if ("${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") set(LIB_TARGET "universal-apple-darwin") else() if(CMAKE_SYSTEM_PROCESSOR STREQUAL arm64) set(LIB_TARGET "aarch64-apple-darwin") else() set(LIB_TARGET "x86_64-apple-darwin") endif() endif() elseif(CMAKE_SYSTEM_NAME STREQUAL FreeBSD) if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(LIB_TARGET "x86_64-unknown-freebsd") else() set(LIB_TARGET "i686-unknown-freebsd") endif() elseif(CMAKE_SYSTEM_NAME STREQUAL OpenBSD) if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(LIB_TARGET "x86_64-unknown-openbsd") else() set(LIB_TARGET "i686-unknown-openbsd") endif() else() # Probably Linux if(EXISTS "/lib/libc.musl-x86_64.so.1") # Just use the default target, no cross-compiling on libc.musl today :( set(LIB_TARGET "${DEFAULT_LIB_TARGET}") else() if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64) set(LIB_TARGET "aarch64-unknown-linux-gnu") elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) set(LIB_TARGET "x86_64-unknown-linux-gnu") else() set(LIB_TARGET "i686-unknown-linux-gnu") endif() endif() endif() if(IOS) set(CARGO_ARGS "lipo") else() set(CARGO_ARGS "build") 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 CARGO_ARGS "--target" ${LIB_TARGET}) endif() 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 )