build(cmake): link migrated Rust code into native libraries

Build a configuration-matched Cargo static library during CMake builds and
join its object members into the generated static libzstd archive.  A single
flattened archive avoids C-to-Rust and Rust-to-C archive-order failures for
external consumers.

Shared builds whole-archive the Cargo output so Rust-only public ABI exports
remain visible.  The configuration also follows partial compression and
decompression builds, forced HUF modes, and supported 32-bit Linux targets.
Dedicated CTest smoke consumers cover both linkage forms.

Test Plan:
- configure and build fresh static and shared CMake smoke targets
- ctest -R 'rustLibSmoke(Static|Shared)' --output-on-failure
- cargo clippy, cargo clippy --benches, cargo clippy --tests, and nightly fmt
- validate partial, forced-HUF, i686, install, and incremental build paths

Refs: build/cmake/lib/merge_rust_archive.cmake
This commit is contained in:
2026-07-10 21:55:40 +02:00
parent 0a6a5f5267
commit 22ec52d5ec
3 changed files with 275 additions and 1 deletions
+176 -1
View File
@@ -16,6 +16,10 @@ option(ZSTD_BUILD_COMPRESSION "BUILD COMPRESSION MODULE" ON)
option(ZSTD_BUILD_DECOMPRESSION "BUILD DECOMPRESSION MODULE" ON)
option(ZSTD_BUILD_DICTBUILDER "BUILD DICTBUILDER MODULE" ON)
option(ZSTD_BUILD_DEPRECATED "BUILD DEPRECATED MODULE" OFF)
option(ZSTD_HUF_FORCE_DECOMPRESS_X1 "FORCE THE HUF X1 DECODER" OFF)
option(ZSTD_HUF_FORCE_DECOMPRESS_X2 "FORCE THE HUF X2 DECODER" OFF)
set(ZSTD_RUST_TARGET "" CACHE STRING
"Cargo target triple (required when CMake cross-compiles)")
set(ZSTDLIB_VISIBLE "" CACHE STRING "Visibility for ZSTDLIB API")
set(ZSTDERRORLIB_VISIBLE "" CACHE STRING "Visibility for ZSTDERRORLIB_VISIBLE API")
@@ -39,7 +43,8 @@ file(GLOB DecompressSources ${LIBRARY_DIR}/decompress/*.c)
if (MSVC)
add_compile_options(-DZSTD_DISABLE_ASM)
else ()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64.*|AMD64.*|x86_64.*|X86_64.*" AND ${ZSTD_HAS_NOEXECSTACK})
if(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64.*|AMD64.*|x86_64.*|X86_64.*"
AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND ${ZSTD_HAS_NOEXECSTACK})
set(DecompressSources ${DecompressSources} ${LIBRARY_DIR}/decompress/huf_decompress_amd64.S)
else()
add_compile_options(-DZSTD_DISABLE_ASM)
@@ -97,6 +102,135 @@ if (ZSTD_LEGACY_SUPPORT)
${LIBRARY_LEGACY_DIR}/zstd_v07.h)
endif ()
# The C compatibility shims are backed by a Cargo static library. Keep its
# feature set in lockstep with CMake's partial-library switches, otherwise a
# Rust object can retain references to a C module that was intentionally left
# out of this build.
find_program(ZSTD_CARGO_EXECUTABLE NAMES cargo)
if(NOT ZSTD_CARGO_EXECUTABLE)
message(FATAL_ERROR "Cargo is required to build the migrated Rust modules")
endif()
mark_as_advanced(ZSTD_CARGO_EXECUTABLE ZSTD_RUST_TARGET)
set(_zstd_huf_c_flags "${CMAKE_C_FLAGS}")
foreach(_zstd_build_type DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
set(_zstd_c_flag_var "CMAKE_C_FLAGS_${_zstd_build_type}")
set(_zstd_huf_c_flags "${_zstd_huf_c_flags} ${${_zstd_c_flag_var}}")
endforeach()
set(_zstd_huf_force_x1 ${ZSTD_HUF_FORCE_DECOMPRESS_X1})
set(_zstd_huf_force_x2 ${ZSTD_HUF_FORCE_DECOMPRESS_X2})
if(_zstd_huf_c_flags MATCHES "(^|[ \t])(-D|/D)HUF_FORCE_DECOMPRESS_X1(=[^ \t]+)?([ \t]|$)")
set(_zstd_huf_force_x1 ON)
endif()
if(_zstd_huf_c_flags MATCHES "(^|[ \t])(-D|/D)HUF_FORCE_DECOMPRESS_X2(=[^ \t]+)?([ \t]|$)")
set(_zstd_huf_force_x2 ON)
endif()
if(_zstd_huf_force_x1 AND _zstd_huf_force_x2)
message(FATAL_ERROR
"HUF_FORCE_DECOMPRESS_X1 and HUF_FORCE_DECOMPRESS_X2 are mutually exclusive")
endif()
set(_zstd_rust_c_definitions)
if(ZSTD_HUF_FORCE_DECOMPRESS_X1)
list(APPEND _zstd_rust_c_definitions HUF_FORCE_DECOMPRESS_X1)
endif()
if(ZSTD_HUF_FORCE_DECOMPRESS_X2)
list(APPEND _zstd_rust_c_definitions HUF_FORCE_DECOMPRESS_X2)
endif()
set(_zstd_rust_features)
set(_zstd_rust_compression 0)
set(_zstd_rust_decompression 0)
if(ZSTD_BUILD_COMPRESSION)
list(APPEND _zstd_rust_features compression)
set(_zstd_rust_compression 1)
endif()
if(ZSTD_BUILD_DECOMPRESSION)
list(APPEND _zstd_rust_features decompression)
set(_zstd_rust_decompression 1)
endif()
set(_zstd_rust_huf_mode default)
if(_zstd_huf_force_x1)
set(_zstd_rust_huf_mode huf-force-decompress-x1)
if(ZSTD_BUILD_DECOMPRESSION)
list(APPEND _zstd_rust_features huf-force-decompress-x1)
endif()
elseif(_zstd_huf_force_x2)
set(_zstd_rust_huf_mode huf-force-decompress-x2)
if(ZSTD_BUILD_DECOMPRESSION)
list(APPEND _zstd_rust_features huf-force-decompress-x2)
endif()
endif()
set(_zstd_rust_target "${ZSTD_RUST_TARGET}")
if(NOT _zstd_rust_target AND CMAKE_SYSTEM_NAME STREQUAL "Linux"
AND CMAKE_SIZEOF_VOID_P EQUAL 4)
set(_zstd_rust_x86_32 OFF)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i[3-6]86|x86)$")
set(_zstd_rust_x86_32 ON)
elseif(CMAKE_C_COMPILER_TARGET MATCHES "^(i[3-6]86|x86)-")
set(_zstd_rust_x86_32 ON)
elseif(_zstd_huf_c_flags MATCHES "(^|[ \t])-m32([ \t]|$)")
set(_zstd_rust_x86_32 ON)
endif()
if(_zstd_rust_x86_32)
set(_zstd_rust_target i686-unknown-linux-gnu)
endif()
endif()
if(NOT _zstd_rust_target AND CMAKE_CROSSCOMPILING)
message(FATAL_ERROR
"Set ZSTD_RUST_TARGET to the Cargo target triple for this cross build")
endif()
if(_zstd_rust_features)
string(REPLACE ";" "," _zstd_rust_feature_arg "${_zstd_rust_features}")
set(_zstd_rust_feature_args --features "${_zstd_rust_feature_arg}")
endif()
set(_zstd_rust_build_config
"c${_zstd_rust_compression}-d${_zstd_rust_decompression}-${_zstd_rust_huf_mode}")
set(ZSTD_RUST_MANIFEST "${ZSTD_SOURCE_DIR}/rust/Cargo.toml")
set(ZSTD_RUST_TARGET_DIR
"${CMAKE_CURRENT_BINARY_DIR}/rust-target/${_zstd_rust_build_config}")
set(_zstd_rust_target_args)
set(_zstd_rust_target_component)
if(_zstd_rust_target)
list(APPEND _zstd_rust_target_args --target "${_zstd_rust_target}")
set(_zstd_rust_target_component "/${_zstd_rust_target}")
endif()
if(MSVC)
set(_zstd_rust_staticlib_name zstd_rs.lib)
else()
set(_zstd_rust_staticlib_name libzstd_rs.a)
endif()
set(ZSTD_RUST_STATICLIB
"${ZSTD_RUST_TARGET_DIR}${_zstd_rust_target_component}/release/${_zstd_rust_staticlib_name}")
# This is deliberately an always-checked target instead of a configure-time
# Rust source glob. The project supports CMake 3.10, predating reliable
# CONFIGURE_DEPENDS handling here, while Cargo already tracks new modules and
# manifest changes precisely. LINK_DEPENDS below still avoids relinking the
# C library when Cargo leaves its archive unchanged.
add_custom_target(zstd_rust_archive
BYPRODUCTS "${ZSTD_RUST_STATICLIB}"
COMMAND "${ZSTD_CARGO_EXECUTABLE}" build
--manifest-path "${ZSTD_RUST_MANIFEST}"
--release
--target-dir "${ZSTD_RUST_TARGET_DIR}"
--no-default-features
${_zstd_rust_feature_args}
${_zstd_rust_target_args}
WORKING_DIRECTORY "${ZSTD_SOURCE_DIR}"
COMMENT "Building Rust compatibility archive (${_zstd_rust_build_config})"
VERBATIM)
function(zstd_attach_rust_archive target)
add_dependencies(${target} zstd_rust_archive)
set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS "${ZSTD_RUST_STATICLIB}")
endfunction()
if (MSVC)
set(MSVC_RESOURCE_DIR ${ZSTD_SOURCE_DIR}/build/VS2010/libzstd-dll)
set(PlatformDependResources ${MSVC_RESOURCE_DIR}/libzstd-dll.rc)
@@ -124,6 +258,25 @@ set(library_targets)
if (ZSTD_BUILD_SHARED)
add_library(libzstd_shared SHARED ${Sources} ${Headers} ${PlatformDependResources})
target_include_directories(libzstd_shared INTERFACE $<BUILD_INTERFACE:${PUBLIC_INCLUDE_DIRS}>)
if(_zstd_rust_c_definitions)
target_compile_definitions(libzstd_shared PRIVATE ${_zstd_rust_c_definitions})
endif()
zstd_attach_rust_archive(libzstd_shared)
# Link the complete Cargo archive, rather than only the members reached by
# C code. Several migrated functions are public ABI entry points and are
# otherwise invisible to the shared-library linker's archive scan.
if(APPLE)
target_link_libraries(libzstd_shared PRIVATE
"-Wl,-force_load,${ZSTD_RUST_STATICLIB}")
elseif(MSVC)
target_link_libraries(libzstd_shared PRIVATE
"/WHOLEARCHIVE:${ZSTD_RUST_STATICLIB}")
else()
target_link_libraries(libzstd_shared PRIVATE
"-Wl,--whole-archive"
"${ZSTD_RUST_STATICLIB}"
"-Wl,--no-whole-archive")
endif()
list(APPEND library_targets libzstd_shared)
if (ZSTD_MULTITHREAD_SUPPORT)
set_property(TARGET libzstd_shared APPEND PROPERTY COMPILE_DEFINITIONS "ZSTD_MULTITHREAD")
@@ -138,6 +291,28 @@ endif ()
if (ZSTD_BUILD_STATIC)
add_library(libzstd_static STATIC ${Sources} ${Headers})
target_include_directories(libzstd_static INTERFACE $<BUILD_INTERFACE:${PUBLIC_INCLUDE_DIRS}>)
if(_zstd_rust_c_definitions)
target_compile_definitions(libzstd_static PRIVATE ${_zstd_rust_c_definitions})
endif()
zstd_attach_rust_archive(libzstd_static)
if(MSVC)
set(_zstd_archive_style MSVC)
else()
set(_zstd_archive_style AR)
endif()
# CMake builds the C archive first. Merge Cargo's object members after
# that step so external C consumers need only one libzstd archive and do
# not depend on a particular C/Rust archive ordering.
add_custom_command(TARGET libzstd_static POST_BUILD
COMMAND "${CMAKE_COMMAND}"
"-DARCHIVER:FILEPATH=${CMAKE_AR}"
"-DARCHIVER_STYLE:STRING=${_zstd_archive_style}"
"-DRANLIB:FILEPATH=${CMAKE_RANLIB}"
"-DRUST_ARCHIVE:FILEPATH=${ZSTD_RUST_STATICLIB}"
"-DTARGET_ARCHIVE:FILEPATH=$<TARGET_FILE:libzstd_static>"
"-DWORK_DIR:PATH=${CMAKE_CURRENT_BINARY_DIR}/rust-archive-members"
-P "${CMAKE_CURRENT_LIST_DIR}/merge_rust_archive.cmake"
VERBATIM)
list(APPEND library_targets libzstd_static)
if (ZSTD_MULTITHREAD_SUPPORT)
set_property(TARGET libzstd_static APPEND PROPERTY COMPILE_DEFINITIONS "ZSTD_MULTITHREAD")
+82
View File
@@ -0,0 +1,82 @@
# ################################################################
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under both the BSD-style license (found in the
# LICENSE file in the root directory of this source tree) and the GPLv2 (found
# in the COPYING file in the root directory of this source tree).
# ################################################################
# Merge the Cargo static library into CMake's libzstd static archive. Keeping
# the object members in one archive matters: C objects call migrated Rust
# functions and Rust objects call C helpers, so two separate archives are
# sensitive to the consumer linker's archive scan order.
foreach(_zstd_required ARCHIVER RUST_ARCHIVE TARGET_ARCHIVE WORK_DIR)
if(NOT DEFINED ${_zstd_required})
message(FATAL_ERROR "${_zstd_required} is required")
endif()
endforeach()
file(REMOVE_RECURSE "${WORK_DIR}")
file(MAKE_DIRECTORY "${WORK_DIR}")
if(ARCHIVER_STYLE STREQUAL "MSVC")
# lib.exe accepts libraries as inputs and writes their object members to a
# new archive. Keep the original C archive separate while it is read,
# because /OUT must not overwrite an input archive.
set(_zstd_c_archive "${WORK_DIR}/libzstd-c.lib")
execute_process(
COMMAND "${CMAKE_COMMAND}" -E copy "${TARGET_ARCHIVE}" "${_zstd_c_archive}"
RESULT_VARIABLE _zstd_copy_result)
if(NOT _zstd_copy_result EQUAL 0)
message(FATAL_ERROR "could not copy ${TARGET_ARCHIVE} for Rust archive merge")
endif()
execute_process(
COMMAND "${ARCHIVER}" "/OUT:${TARGET_ARCHIVE}" "${_zstd_c_archive}" "${RUST_ARCHIVE}"
RESULT_VARIABLE _zstd_merge_result
OUTPUT_VARIABLE _zstd_merge_output
ERROR_VARIABLE _zstd_merge_error)
else()
execute_process(
COMMAND "${ARCHIVER}" x "${RUST_ARCHIVE}"
WORKING_DIRECTORY "${WORK_DIR}"
RESULT_VARIABLE _zstd_extract_result
OUTPUT_VARIABLE _zstd_extract_output
ERROR_VARIABLE _zstd_extract_error)
if(NOT _zstd_extract_result EQUAL 0)
message(FATAL_ERROR
"could not extract Rust archive ${RUST_ARCHIVE}: ${_zstd_extract_output}${_zstd_extract_error}")
endif()
file(GLOB _zstd_rust_objects "${WORK_DIR}/*.o" "${WORK_DIR}/*.obj")
if(NOT _zstd_rust_objects)
message(FATAL_ERROR "Rust archive ${RUST_ARCHIVE} did not contain object files")
endif()
execute_process(
COMMAND "${ARCHIVER}" q "${TARGET_ARCHIVE}" ${_zstd_rust_objects}
RESULT_VARIABLE _zstd_merge_result
OUTPUT_VARIABLE _zstd_merge_output
ERROR_VARIABLE _zstd_merge_error)
endif()
if(NOT _zstd_merge_result EQUAL 0)
message(FATAL_ERROR
"could not merge Rust archive ${RUST_ARCHIVE}: ${_zstd_merge_output}${_zstd_merge_error}")
endif()
if(DEFINED RANLIB AND NOT "${RANLIB}" STREQUAL "" AND NOT ARCHIVER_STYLE STREQUAL "MSVC")
execute_process(
COMMAND "${RANLIB}" "${TARGET_ARCHIVE}"
RESULT_VARIABLE _zstd_ranlib_result
OUTPUT_VARIABLE _zstd_ranlib_output
ERROR_VARIABLE _zstd_ranlib_error)
if(NOT _zstd_ranlib_result EQUAL 0)
message(FATAL_ERROR
"could not index ${TARGET_ARCHIVE}: ${_zstd_ranlib_output}${_zstd_ranlib_error}")
endif()
endif()
file(REMOVE_RECURSE "${WORK_DIR}")
+17
View File
@@ -90,6 +90,23 @@ AddTestFlagsOption(ZSTD_ZSTREAM_FLAGS "$ENV{ZSTREAM_TESTTIME} $ENV{FUZZER_FLAGS}
"Semicolon-separated list of flags to pass to the zstreamtest test (see `zstreamtest -h` for usage)")
add_test(NAME zstreamtest COMMAND "$<TARGET_FILE:zstreamtest>" ${ZSTD_ZSTREAM_FLAGS})
# Verify that a consumer can link the completed static archive alone: migrated
# Rust objects and the C code they call must not remain separate archives. The
# shared variant also checks that whole-archive linking retains Rust-only ABI
# exports such as the DDict lifecycle functions.
add_executable(rustLibSmokeStatic ${TESTS_DIR}/rustLibSmoke.c)
target_link_libraries(rustLibSmokeStatic libzstd_static)
add_test(NAME rustLibSmokeStatic COMMAND "$<TARGET_FILE:rustLibSmokeStatic>")
if(ZSTD_BUILD_SHARED)
add_executable(rustLibSmokeShared ${TESTS_DIR}/rustLibSmoke.c)
target_link_libraries(rustLibSmokeShared libzstd_shared)
if(UNIX)
set_property(TARGET rustLibSmokeShared APPEND PROPERTY
BUILD_RPATH "$<TARGET_FILE_DIR:libzstd_shared>")
add_test(NAME rustLibSmokeShared COMMAND "$<TARGET_FILE:rustLibSmokeShared>")
endif()
endif()
#
# playTests.sh
#