cmake/CMakeCargo.cmake (358 lines of code) (raw):

# ccommon - a cache common library. # Copyright (C) 2019 Twitter, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Hack to ensure we have the directory of this cmake file when # the functions contained within are called. set(FILE_LIST_DIR ${CMAKE_CURRENT_LIST_DIR} CACHE INTERNAL "") # Get the target that we want to pass to cargo function(cargo_build_private_get_target TARGET_VAR) 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") elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin) set(LIB_TARGET "x86_64-apple-darwin") else() if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(LIB_TARGET "x86_64-unknown-linux-gnu") else() set(LIB_TARGET "i686-unknown-linux-gnu") endif() endif() set(${TARGET_VAR} ${LIB_TARGET} PARENT_SCOPE) endfunction() # Get whether to build in release or debug mode function(cargo_build_private_get_build_type TARGET_VAR) if(NOT CMAKE_BUILD_TYPE) set(${TARGET_VAR} "debug" PARENT_SCOPE) elseif(${CMAKE_BUILD_TYPE} STREQUAL "Release") set(${TARGET_VAR} "release" PARENT_SCOPE) else() set(${TARGET_VAR} "debug" PARENT_SCOPE) endif() endfunction() # Create a set of targets to invoke cargo and pass in # the required linker flags. This is meant to be run # in the same directory as the Cargo.toml file for the # library. # # In all cases, the target can be used as if it was defined # through the add_executable or add_binary. In addition, # library dependencies will be properly passed through. # # Arguments: # NAME <lib-name> # The library package name as specified in Cargo.toml. # This argument is required. # TARGET_DIR <dir> # Overrides the default directory for the cargo target # directory which is ${CMAKE_BINARY_DIR}/target. # BIN # Indicates that this crate generated a binary and to # expose that binary file as a target. # STATIC # Indicates this target generates a static library that # can be consumed by other cmake targets. # NO_TEST # Disables generation of the test target. # # Notes: # - If neither BIN or STATIC is defined then it is assumed # that the target exports no artifacts (i.e. it is only used # by other rust targets within the build) # - Build and exported are currently mutually exclusive. If you # want to have multiple targets like this then call cargo_build # multiple times. # # Additional Things: # - This also sets up the required cmake properties to delete the # target directory when `make clean` or equivalent is run. # # How it Works: # In general, cmake has a hard time integrating with external build # systems within the same directory. However, cmake has extensive # support for custom linkers and also allows you to define interface # libraries when no artifact is created. Together, we can use these # to trick cmake into thinking it is building the executable while # actually using cargo to build them. This means that things like # linker flags and dependencies should mostly "just work" (hopefully). # # What we have to do depends on what type of library we are compiling: # - For pure rust libraries that don't export any C API, we define # an interface library. This means that any linker flags are # properly passed on to downstream dependencies. In addition, we # define a custom target so the library is built. # - For static libraries, we define a library that's built using a # custom linker language. The custom linker command is really just # a `cargo build` in disguise, but one that passes the correct # flags in. # - For binaries, we define an executable target also using a custom # linker language similar to a static library. # # To pass the proper linker flags to the rust process, we have a special # target <NAME>.linkflags.txt that echoes the linker flags into a known # file. These are then picked up by the import-link-flags crate which # uses a build script to pass them to rustc. function(cargo_build) cmake_parse_arguments( CARGO "BIN;STATIC;NO_TEST" "NAME;TARGET_DIR;COPY_TO" "" ${ARGN} ) string(REPLACE "-" "_" LIB_NAME ${CARGO_NAME}) if(NOT (DEFINED CARGO_TARGET_DIR)) if ($ENV{CI}) set(CARGO_TARGET_DIR ${CMAKE_BINARY_DIR}/target) else() set(CARGO_TARGET_DIR ${CMAKE_CURRENT_BINARY_DIR}/target) endif() endif() if(CARGO_BIN AND CARGO_STATIC) message( FATAL_ERROR "Cannot create a cargo target that has " "both a binary and a static library. Use multiple " "targets instead." ) endif() cargo_build_private_get_target(CRATE_TARGET) cargo_build_private_get_build_type(CRATE_BUILD_TYPE) # The CONFIGURE_DEPENDS flag will rerun the glob at build time if the # the build system supports it. file( GLOB_RECURSE CRATE_SOURCES CONFIGURE_DEPENDS "*.rs" ) list(APPEND CRATE_SOURCES Cargo.toml) # Clean the target directory when make clean is run set_directory_properties(PROPERTIES ADDITIONAL_CLEAN_FILES ${CARGO_TARGET_DIR} ) if(CARGO_BIN OR CARGO_STATIC) set(LINK_FLAGS_FILE $<TARGET_FILE:${CARGO_NAME}>.linkflags.txt) else() set(LINK_FLAGS_FILE $<TARGET_FILE:${CARGO_NAME}-link-export>) endif() set(FORWARDED_VARS # So that internal invocations of cmake are consistent "CMAKE=${CMAKE_COMMAND}" # So that build scripts can configure themselves based # on whether cmake is driving the build or not "CCOMMON_CMAKE_IS_DRIVING_BUILD=1" # Needed to configure the correct target directory "CARGO_TARGET_DIR=${CARGO_TARGET_DIR}" # Needed for build scripts "CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR}" ) if(CARGO_BIN) set(OUTPUT_FILE_NAME ${LIB_NAME}${CMAKE_EXECUTABLE_SUFFIX}) set(OUTPUT_FILE ${CARGO_TARGET_DIR}/${CRATE_TARGET}/${CRATE_BUILD_TYPE}/${OUTPUT_FILE_NAME}) elseif(CARGO_STATIC) set(OUTPUT_FILE_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${LIB_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}) set(OUTPUT_FILE ${CARGO_TARGET_DIR}/${CRATE_TARGET}/${CRATE_BUILD_TYPE}/${OUTPUT_FILE_NAME}) endif() if(IOS) # Since we're going through cargo rustc to pass linker flags # this won't work. The previous build script used cargo lipo # here. However, the likelyhood of someone wanting to use # a library for cache servers on IOS is low at this time so # this is OK. message(FATAL_ERROR "Compiling for IOS is not supported") endif() # Arguments to cargo set(CRATE_ARGS "") list(APPEND CRATE_ARGS "--target" ${CRATE_TARGET}) if(${CRATE_BUILD_TYPE} STREQUAL "release") list(APPEND CRATE_ARGS "--release") endif() # Convert CRATE_ARGS from a list to a string string(REPLACE ";" " " CRATE_ARGS_STR "${CRATE_ARGS}") set(LINK_FLAGS_FILE "${CMAKE_CURRENT_BINARY_DIR}/${CARGO_NAME}.linkflags.txt") # The following is a hack. It takes advantage of the fact that # cmake allows us to define arbitrary linker languages in order # to invoke cargo as our linker command. To do this, we define # a custom language specific to only our target that also takes # in the required environment variables we want to set and the # output file generated by cargo. # # This works by running a cmake script during build-time which # parses the link flags and sends them off to cargo as -Clink-flag=... # For the full details in what's done during build see CargoLink.cmake. # # The upside of this hack is that it allows libraries and binaries # generated by cargo to be used as normal cmake targets. This # means that stuff like target_link_libraries works properly. # # Extra Note: The variables that look like <VAR_NAME> in the string # below are called expansion variables by the community wiki. They # don't seem to be documented anywhere in the official docs but # are hopefully stable. # # Another Note: Since these are required to be global variables we # push them in the cache as hidden variables. # # Note 3: There is a <FLAGS> expansion rule in the docs which I would # expect to expand to what was passed in to target_compile_flags. # Unfortunately, it doesn't, so I've instead just shoved the # compile flags into linker command template directory. # # The inspiration for this hack came from this SO answer # https://stackoverflow.com/questions/34165365/retrieve-all-link-flags-in-cmake # # "Docs" for the expansion rules can be found here # https://gitlab.kitware.com/cmake/community/wikis/doc/cmake/Build-Rules set(LINK_COMMAND "<CMAKE_COMMAND> -P ${FILE_LIST_DIR}/CargoLink.cmake 'TARGET=<TARGET>' 'LINK_FLAGS=<LINK_FLAGS>' 'LINK_LIBRARIES=<LINK_LIBRARIES>' 'FLAGS=${CRATE_ARGS_STR}' 'OUTPUT=${OUTPUT_FILE}' 'CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}' 'LINK_FLAGS_FILE=${LINK_FLAGS_FILE}' -- ${FORWARDED_VARS}") foreach(VAR ${FORWARDED_VARS}) string(APPEND LINK_COMMAND " ${VAR}") endforeach() # TODO(sean): disambiguate this based on bin/lib so that multiple targets in # the same directory don't clash. set(CMAKE_${CARGO_NAME}_LINK_EXECUTABLE ${LINK_COMMAND} CACHE INTERNAL "") set(CMAKE_${CARGO_NAME}_CREATE_STATIC_LIBRARY ${LINK_COMMAND} CACHE INTERNAL "") set(CMAKE_${CARGO_NAME}_CREATE_SHARED_LIBRARY ${LINK_COMMAND} CACHE INTERNAL "") set(CMAKE_${CARGO_NAME}_LINK_FLAGS ${CMAKE_C_LINK_FLAGS} CACHE INTERNAL "") set(CMAKE_${CARGO_NAME}_LINK_DIRECTORIES ${CMAKE_C_LINK_DIRECTORIES} CACHE INTERNAL "") # Needed to build when we have test targets set( CMAKE_ECHO_LINK_EXECUTABLE "bash -c \"<CMAKE_COMMAND> -E echo_append <LINK_FLAGS> <LINK_LIBRARIES> > <TARGET>\"" CACHE INTERNAL "" ) set(CMAKE_ECHO_LINK_FLAGS ${CMAKE_C_LINK_FLAGS} CACHE INTERNAL "") set(CMAKE_ECHO_LINK_DIRECTORIES ${CMAKE_C_LINK_DIRECTORIES} CACHE INTERNAL "") set_source_files_properties( ${CRATE_SOURCES} PROPERTY HEADER_FILE_ONLY ON ) # CMake doesn't seem to add dependencies on the crate # sources. This means that making changes to a rust # target is rather painful. # # We work around this by creating a stamp file that depends # on all the source files in the crate. When a source file # is modified, the stamp file is updated, and this is picked # up since we set the stamp file as a link dependency of # library/binary. add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/up-to-date.stamp DEPENDS ${CRATE_SOURCES} COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/up-to-date.stamp COMMENT "checking whether crate sources are up-to-date" ) # Targets if(CARGO_BIN) # We are building a binary executable add_executable( ${CARGO_NAME} ${CRATE_SOURCES} # Needed so that this file is usable in LINK_DEPENDENCY ${CMAKE_CURRENT_BINARY_DIR}/up-to-date.stamp ) # Ensure that we use our custom "linker" set_target_properties( ${CARGO_NAME} PROPERTIES LINKER_LANGUAGE ${CARGO_NAME} LINK_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/up-to-date.stamp ) target_compile_options(${CARGO_NAME} PRIVATE ${CRATE_ARGS}) elseif(CARGO_STATIC) # We are building a static library that will be used from C code add_library( ${CARGO_NAME} STATIC ${CRATE_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/up-to-date.stamp ) # Ensure that we use our custom "linker" set_target_properties( ${CARGO_NAME} PROPERTIES LINKER_LANGUAGE ${CARGO_NAME} # Needed so that this file is usable in LINK_DEPENDENCY LINK_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/up-to-date.stamp ) target_compile_options(${CARGO_NAME} PRIVATE ${CRATE_ARGS}) else() set(CARGO_ENV_COMMAND ${CMAKE_COMMAND} -E env ${FORWARDED_VARS}) # We are building a rust-only library. Define it as a interface library add_library( ${CARGO_NAME} INTERFACE ) if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.13.0") # This is required for dependency detection to work correctly. # However, nothing breaks on a clean build if you don't have it # so we don't make it required. set_target_properties( ${CARGO_NAME} PROPERTIES INTERFACE_LINK_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/build.stamp ) endif() # However, we still want to build the library, so define a custom command for that add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/build.stamp COMMAND ${CARGO_ENV_COMMAND} cargo build ${CRATE_ARGS} COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/build.stamp DEPENDS ${CRATE_SOURCES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "running cargo for target ${CARGO_NAME}" ) add_custom_target( ${CARGO_NAME}-build ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/build.stamp ) add_dependencies(${CARGO_NAME} ${CARGO_NAME}-build) add_dependencies(${CARGO_NAME} ${CARGO_NAME}-link-export) add_executable(${CARGO_NAME}-link-export Cargo.toml) set_target_properties( ${CARGO_NAME}-link-export PROPERTIES LINKER_LANGUAGE ECHO SUFFIX ".txt" TARGET_MESSAGES OFF ) target_link_libraries(${CARGO_NAME}-link-export $<TARGET_PROPERTY:${CARGO_NAME},INTERFACE_LINK_LIBRARIES>) set(LINK_FLAGS_FILE $<TARGET_FILE:${CARGO_NAME}-link-export>) endif() if(NOT CARGO_NO_TEST) add_test( NAME test-${CARGO_NAME} COMMAND ${CMAKE_COMMAND} -P "${FILE_LIST_DIR}/CargoTest.cmake" "LINK_FLAGS_FILE=${LINK_FLAGS_FILE}" "FLAGS=${CRATE_ARGS_STR}" "CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" "CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" -- ${FORWARDED_VARS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() endfunction()