You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tsfile.apache.org by co...@apache.org on 2026/09/17 06:31:04 UTC
(tsfile) 21/24: fix(packaging): complete native runtime and install contracts
This is an automated email from the ASF dual-hosted git repository.
ColinLeeo pushed a commit to branch feat/native-package-artifacts
in repository https://gitbox.apache.org/repos/asf/tsfile.git
commit 4758a900480c25e5145699cf2a8bae4c12cc8065
Author: ColinLee <sh...@163.com>
AuthorDate: Thu Sep 10 22:08:17 2026 +0800
fix(packaging): complete native runtime and install contracts
---
.github/workflows/native-packages.yml | 85 +++------
cpp/CMakeLists.txt | 27 +++
cpp/cmake/TsFileConfig.cmake.in | 14 ++
cpp/cmake/TsFileStaticDependencies.cmake | 104 +++++++++++
....cmake.in => TsFileStaticDependencies.cmake.in} | 11 +-
cpp/cmake/libtsfile.pc.in | 2 +-
cpp/cmake/tests/CheckStaticMSVCRuntime.cmake | 38 ++++
.../projects/InstalledConsumer/CMakeLists.txt | 3 +
cpp/cmake/tests/projects/InstalledConsumer/main.cc | 5 +-
cpp/cmake/tests/projects/PkgConfigConsumer/main.c | 5 +-
cpp/cmake/tests/projects/PkgConfigConsumer/main.cc | 6 +-
cpp/src/CMakeLists.txt | 13 +-
packaging/README.md | 41 ++++
packaging/homebrew/tsfile-dev.rb.in | 3 +-
packaging/homebrew/tsfile.rb | 3 +-
packaging/scripts/native_package_versions.py | 11 +-
packaging/scripts/verify_windows_runtime.py | 101 ++++++++++
packaging/tests/check_native_workflow.rb | 53 ++++++
packaging/tests/test_native_install.py | 206 +++++++++++++++++++++
packaging/tests/test_native_package_versions.py | 41 ++++
packaging/tests/test_windows_runtime.py | 73 ++++++++
21 files changed, 769 insertions(+), 76 deletions(-)
diff --git a/.github/workflows/native-packages.yml b/.github/workflows/native-packages.yml
index 3c326d2e2..f6f349315 100644
--- a/.github/workflows/native-packages.yml
+++ b/.github/workflows/native-packages.yml
@@ -42,6 +42,11 @@ jobs:
with:
fetch-depth: 0
+ - name: Check packaging helpers and workflow contracts
+ run: |
+ python3 -m unittest discover -s packaging/tests -p 'test_*.py' -v
+ ruby packaging/tests/check_native_workflow.rb
+
- name: Generate package versions
id: versions
run: |
@@ -152,6 +157,9 @@ jobs:
- ubuntu:24.04
container: ${{ matrix.container }}
steps:
+ - name: Checkout installed-consumer fixture
+ uses: actions/checkout@v7
+
- name: Download DEB packages
uses: actions/download-artifact@v8
with:
@@ -181,25 +189,9 @@ jobs:
- name: Build and run an installed-package consumer
shell: bash
run: |
- mkdir -p consumer
- cat > consumer/CMakeLists.txt <<'CMAKE'
- cmake_minimum_required(VERSION 3.11)
- project(TsFileInstalledConsumer LANGUAGES CXX)
-
- find_package(TsFile CONFIG REQUIRED)
- add_executable(installed_consumer main.cpp)
- target_link_libraries(installed_consumer PRIVATE TsFile::tsfile)
- CMAKE
- cat > consumer/main.cpp <<'CPP'
- #include <tsfile/cwrapper/tsfile_cwrapper.h>
-
- int main() {
- return TS_DATATYPE_INT32 == 1 ? 0 : 1;
- }
- CPP
- cmake -S consumer -B consumer/build
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build
cmake --build consumer/build --parallel
- ./consumer/build/installed_consumer
+ ./consumer/build/tsfile_installed_consumer
build-rpm:
name: Build AlmaLinux 9 RPM packages
@@ -217,7 +209,7 @@ jobs:
pkgconf-pkg-config \
rpm-build \
git \
- curl \
+ curl-minimal \
tar \
gzip \
libuuid-devel
@@ -299,6 +291,9 @@ jobs:
runs-on: ubuntu-24.04
container: almalinux:9
steps:
+ - name: Checkout installed-consumer fixture
+ uses: actions/checkout@v7
+
- name: Download RPM packages
uses: actions/download-artifact@v8
with:
@@ -324,25 +319,9 @@ jobs:
- name: Build and run an installed-package consumer
shell: bash
run: |
- mkdir -p consumer
- cat > consumer/CMakeLists.txt <<'CMAKE'
- cmake_minimum_required(VERSION 3.11)
- project(TsFileInstalledConsumer LANGUAGES CXX)
-
- find_package(TsFile CONFIG REQUIRED)
- add_executable(installed_consumer main.cpp)
- target_link_libraries(installed_consumer PRIVATE TsFile::tsfile)
- CMAKE
- cat > consumer/main.cpp <<'CPP'
- #include <tsfile/cwrapper/tsfile_cwrapper.h>
-
- int main() {
- return TS_DATATYPE_INT32 == 1 ? 0 : 1;
- }
- CPP
- cmake -S consumer -B consumer/build
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build
cmake --build consumer/build --parallel
- ./consumer/build/installed_consumer
+ ./consumer/build/tsfile_installed_consumer
build-windows:
name: Build Windows MSVC x86_64 ZIP
@@ -370,6 +349,8 @@ jobs:
-DBUILD_TOOLS=ON `
-DTSFILE_ENABLE_CPACK=ON `
-DTSFILE_DEPENDENCY_SOURCE=BUNDLED `
+ -DTSFILE_MSVC_STATIC_RUNTIME=ON `
+ -DCMAKE_PROJECT_TsFile_CPP_INCLUDE="$((Resolve-Path cpp/cmake/tests/CheckStaticMSVCRuntime.cmake).Path)" `
-DTSFILE_ENABLE_NATIVE_ARCH=OFF `
-DTSFILE_ARCHIVE_VERSION="$env:TSFILE_ARCHIVE_VERSION"
cmake --build build/windows --config Release --parallel
@@ -396,29 +377,16 @@ jobs:
}
}
+ python packaging/scripts/verify_windows_runtime.py "$stage"
$env:PATH = "$stage/bin;$env:PATH"
tsfile-cli.exe --version
- New-Item -ItemType Directory -Path consumer -Force | Out-Null
- @'
- cmake_minimum_required(VERSION 3.11)
- project(TsFileInstalledConsumer LANGUAGES CXX)
-
- find_package(TsFile CONFIG REQUIRED)
- add_executable(installed_consumer main.cpp)
- target_link_libraries(installed_consumer PRIVATE TsFile::tsfile)
- '@ | Set-Content -Path consumer/CMakeLists.txt
- @'
- #include <tsfile/cwrapper/tsfile_cwrapper.h>
-
- int main() {
- return TS_DATATYPE_INT32 == 1 ? 0 : 1;
- }
- '@ | Set-Content -Path consumer/main.cpp
- cmake -S consumer -B consumer/build -G "Visual Studio 17 2022" -A x64 `
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build `
+ -G "Visual Studio 17 2022" -A x64 `
+ -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded `
-DCMAKE_PREFIX_PATH="$stage"
cmake --build consumer/build --config Release --parallel
- & consumer/build/Release/installed_consumer.exe
+ & consumer/build/Release/tsfile_installed_consumer.exe
- name: Build and inspect combined ZIP
env:
@@ -469,6 +437,13 @@ jobs:
$archive.Dispose()
}
+ Expand-Archive -LiteralPath $archives[0].FullName -DestinationPath extracted/windows
+ python packaging/scripts/verify_windows_runtime.py extracted/windows
+ $zipCli = @(Get-ChildItem extracted/windows -Recurse -Filter tsfile-cli.exe -File)
+ if ($zipCli.Count -ne 1) { throw 'Expected exactly one extracted CLI' }
+ $env:PATH = "$($zipCli[0].DirectoryName);$env:SystemRoot/System32"
+ & $zipCli[0].FullName --version
+
- name: Upload Windows ZIP
uses: actions/upload-artifact@v7
with:
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index eb0a2aea6..247b5c383 100755
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -17,6 +17,18 @@ specific language governing permissions and limitations
under the License.
]]
cmake_minimum_required(VERSION 3.11)
+option(TSFILE_MSVC_STATIC_RUNTIME
+ "Use the static MSVC runtime throughout a portable bundled build" OFF)
+if (TSFILE_MSVC_STATIC_RUNTIME)
+ if (NOT POLICY CMP0091)
+ message(FATAL_ERROR "TSFILE_MSVC_STATIC_RUNTIME requires CMake 3.15+")
+ endif ()
+ # The policy must be selected before the first project(), and inherited by
+ # upstream projects that reset their policy version in add_subdirectory().
+ cmake_policy(SET CMP0091 NEW)
+ set(CMAKE_POLICY_DEFAULT_CMP0091 NEW)
+ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
+endif ()
project(TsFile_CPP)
if (DEFINED ToolChain)
@@ -62,6 +74,12 @@ if (NOT TSFILE_ABI_VERSION MATCHES "^[0-9]+$")
endif ()
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DependencySource.cmake)
+if (TSFILE_MSVC_STATIC_RUNTIME AND
+ NOT TSFILE_DEPENDENCY_SOURCE STREQUAL "BUNDLED")
+ message(FATAL_ERROR
+ "TSFILE_MSVC_STATIC_RUNTIME requires TSFILE_DEPENDENCY_SOURCE=BUNDLED "
+ "so prebuilt dependencies cannot introduce a different MSVC runtime")
+endif ()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
option(TSFILE_BUILD_SHARED "Build libtsfile as a shared library" ON)
@@ -502,6 +520,13 @@ install(FILES
COMPONENT development)
set(TSFILE_PKGCONFIG_CFLAGS "")
+if (NOT TSFILE_BUILD_SHARED)
+ string(APPEND TSFILE_PKGCONFIG_CFLAGS " -DTSFILE_STATIC")
+ if (ENABLE_ANTLR4 AND (TSFILE_ANTLR4_SOURCE STREQUAL "BUNDLED" OR
+ TSFILE_ANTLR4_SYSTEM_STATIC))
+ string(APPEND TSFILE_PKGCONFIG_CFLAGS " -DANTLR4CPP_STATIC")
+ endif ()
+endif ()
foreach (_TSFILE_PUBLIC_FEATURE
ENABLE_ANTLR4
ENABLE_MEM_STAT
@@ -519,6 +544,8 @@ foreach (_TSFILE_PUBLIC_FEATURE
" -D${_TSFILE_PUBLIC_FEATURE}")
endif ()
endforeach ()
+file(RELATIVE_PATH TSFILE_PKGCONFIG_PREFIX_RELATIVE
+ "${CMAKE_INSTALL_FULL_LIBDIR}/pkgconfig" "${CMAKE_INSTALL_PREFIX}")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/libtsfile.pc.in"
"${CMAKE_CURRENT_BINARY_DIR}/libtsfile.pc"
diff --git a/cpp/cmake/TsFileConfig.cmake.in b/cpp/cmake/TsFileConfig.cmake.in
index eea711a9e..1357f4898 100644
--- a/cpp/cmake/TsFileConfig.cmake.in
+++ b/cpp/cmake/TsFileConfig.cmake.in
@@ -24,6 +24,20 @@ if (@ENABLE_THREADS@)
find_dependency(Threads)
endif()
+if (NOT @TSFILE_BUILD_SHARED@)
+ set(_TSFILE_SAVED_MODULE_PATH "${CMAKE_MODULE_PATH}")
+ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")
+ file(GLOB _TSFILE_STATIC_CONFIGS
+ "${CMAKE_CURRENT_LIST_DIR}/TsFileStaticDependencies-*.cmake")
+ foreach (_TSFILE_STATIC_CONFIG IN LISTS _TSFILE_STATIC_CONFIGS)
+ include("${_TSFILE_STATIC_CONFIG}")
+ endforeach ()
+ set(CMAKE_MODULE_PATH "${_TSFILE_SAVED_MODULE_PATH}")
+ unset(_TSFILE_STATIC_CONFIG)
+ unset(_TSFILE_STATIC_CONFIGS)
+ unset(_TSFILE_SAVED_MODULE_PATH)
+endif ()
+
include("${CMAKE_CURRENT_LIST_DIR}/TsFileTargets.cmake")
check_required_components(TsFile)
diff --git a/cpp/cmake/TsFileStaticDependencies.cmake b/cpp/cmake/TsFileStaticDependencies.cmake
new file mode 100644
index 000000000..a4942ca77
--- /dev/null
+++ b/cpp/cmake/TsFileStaticDependencies.cmake
@@ -0,0 +1,104 @@
+#[[
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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
+
+ https://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.
+]]
+
+# Static archives do not absorb their link dependencies. Keep the private
+# build wrappers out of the export: install bundled archives and describe
+# imported link-only targets, or rediscover system packages on the consumer.
+# install(FILES) also works on CMake 3.11 for targets in another directory.
+function(tsfile_install_static_dependency NAME SOURCE SYSTEM_TARGET FIND_CODE)
+ if (NOT TARGET TsFile::${NAME})
+ return()
+ endif ()
+ target_link_libraries(tsfile INTERFACE
+ $<INSTALL_INTERFACE:$<LINK_ONLY:TsFile::Static_${NAME}>>)
+ set(_CODE "${TSFILE_STATIC_DEPENDENCY_CODE}")
+ if (SOURCE STREQUAL "BUNDLED")
+ get_target_property(_TARGET TsFile::${NAME} INTERFACE_LINK_LIBRARIES)
+ install(FILES $<TARGET_FILE:${_TARGET}>
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/tsfile/$<CONFIG>"
+ COMPONENT development)
+ string(APPEND _CODE
+ "if (NOT TARGET TsFile::Static_${NAME})\n"
+ " add_library(TsFile::Static_${NAME} STATIC IMPORTED)\n"
+ "endif ()\n"
+ "set_property(TARGET TsFile::Static_${NAME} APPEND PROPERTY IMPORTED_CONFIGURATIONS $<UPPER_CASE:$<CONFIG>>)\n"
+ "set_target_properties(TsFile::Static_${NAME} PROPERTIES IMPORTED_LOCATION_$<UPPER_CASE:$<CONFIG>> \"\${CMAKE_CURRENT_LIST_DIR}/../../tsfile/$<CONFIG>/$<TARGET_FILE_NAME:${_TARGET}>\")\n")
+ else ()
+ string(APPEND _CODE "${FIND_CODE}\n"
+ "if (NOT TARGET TsFile::Static_${NAME})\n"
+ " add_library(TsFile::Static_${NAME} INTERFACE IMPORTED)\n"
+ " set_target_properties(TsFile::Static_${NAME} PROPERTIES INTERFACE_LINK_LIBRARIES ${SYSTEM_TARGET})\n"
+ "endif ()\n")
+ endif ()
+ set(TSFILE_STATIC_DEPENDENCY_CODE "${_CODE}" PARENT_SCOPE)
+endfunction()
+
+set(TSFILE_STATIC_DEPENDENCY_CODE "")
+tsfile_install_static_dependency(ANTLR4 "${TSFILE_ANTLR4_SOURCE}"
+ "${TSFILE_ANTLR4_SYSTEM_TARGET}"
+ "find_package(utf8cpp CONFIG QUIET)\nfind_dependency(antlr4-runtime CONFIG)")
+if (ENABLE_ANTLR4 AND TSFILE_ANTLR4_SOURCE STREQUAL "BUNDLED")
+ if (WIN32)
+ string(APPEND TSFILE_STATIC_DEPENDENCY_CODE
+ "set_property(TARGET TsFile::Static_ANTLR4 PROPERTY INTERFACE_LINK_LIBRARIES ole32)\n")
+ else ()
+ if (APPLE)
+ set(_PLATFORM_LIBRARY CoreFoundation)
+ else ()
+ set(_PLATFORM_LIBRARY uuid)
+ endif ()
+ string(APPEND TSFILE_STATIC_DEPENDENCY_CODE
+ "find_library(_TSFILE_ANTLR4_PLATFORM_LIBRARY ${_PLATFORM_LIBRARY})\n"
+ "if (NOT _TSFILE_ANTLR4_PLATFORM_LIBRARY)\n"
+ " set(TsFile_FOUND FALSE)\n"
+ " set(TsFile_NOT_FOUND_MESSAGE \"Static TsFile needs ${_PLATFORM_LIBRARY}\")\n"
+ " return()\n"
+ "endif ()\n"
+ "set_property(TARGET TsFile::Static_ANTLR4 PROPERTY INTERFACE_LINK_LIBRARIES \"\${_TSFILE_ANTLR4_PLATFORM_LIBRARY}\")\n")
+ unset(_PLATFORM_LIBRARY)
+ endif ()
+endif ()
+tsfile_install_static_dependency(Snappy "${TSFILE_SNAPPY_SOURCE}" Snappy::snappy
+ "find_dependency(Snappy ${TSFILE_SNAPPY_MIN_VERSION} CONFIG)")
+tsfile_install_static_dependency(LZ4 "${TSFILE_LZ4_SOURCE}" LZ4::LZ4
+ "find_dependency(LZ4 ${TSFILE_LZ4_MIN_VERSION})")
+if (ENABLE_LZ4 AND TSFILE_LZ4_SOURCE STREQUAL "SYSTEM")
+ install(FILES "${CMAKE_SOURCE_DIR}/cmake/FindLZ4.cmake"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/TsFile"
+ COMPONENT development)
+endif ()
+tsfile_install_static_dependency(LZOKAY "${TSFILE_LZOKAY_SOURCE}" lzokay::lzokay
+ "find_dependency(lzokay ${TSFILE_LZOKAY_MIN_VERSION} CONFIG)")
+tsfile_install_static_dependency(ZLIB "${TSFILE_ZLIB_SOURCE}" ZLIB::ZLIB
+ "find_dependency(ZLIB ${TSFILE_ZLIB_MIN_VERSION})")
+tsfile_install_static_dependency(ZSTD "${TSFILE_ZSTD_SOURCE}"
+ "${TSFILE_ZSTD_SYSTEM_TARGET}"
+ "find_dependency(zstd ${TSFILE_ZSTD_MIN_VERSION} CONFIG)")
+tsfile_install_static_dependency(LibLZMA "${TSFILE_LIBLZMA_SOURCE}" LibLZMA::LibLZMA
+ "find_dependency(LibLZMA ${TSFILE_LIBLZMA_MIN_VERSION})\nif (NOT TARGET LibLZMA::LibLZMA)\n add_library(LibLZMA::LibLZMA INTERFACE IMPORTED)\n set_target_properties(LibLZMA::LibLZMA PROPERTIES INTERFACE_LINK_LIBRARIES \"\${LIBLZMA_LIBRARIES}\")\nendif ()")
+
+configure_file("${CMAKE_SOURCE_DIR}/cmake/TsFileStaticDependencies.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/TsFileStaticDependencies.cmake.in" @ONLY)
+file(GENERATE
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/TsFileStaticDependencies-$<CONFIG>.cmake"
+ INPUT "${CMAKE_CURRENT_BINARY_DIR}/TsFileStaticDependencies.cmake.in")
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/TsFileStaticDependencies-$<CONFIG>.cmake"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/TsFile"
+ COMPONENT development)
+unset(TSFILE_STATIC_DEPENDENCY_CODE)
diff --git a/cpp/cmake/TsFileConfig.cmake.in b/cpp/cmake/TsFileStaticDependencies.cmake.in
similarity index 79%
copy from cpp/cmake/TsFileConfig.cmake.in
copy to cpp/cmake/TsFileStaticDependencies.cmake.in
index eea711a9e..877bc21df 100644
--- a/cpp/cmake/TsFileConfig.cmake.in
+++ b/cpp/cmake/TsFileStaticDependencies.cmake.in
@@ -17,13 +17,4 @@ specific language governing permissions and limitations
under the License.
]]
-@PACKAGE_INIT@
-
-include(CMakeFindDependencyMacro)
-if (@ENABLE_THREADS@)
- find_dependency(Threads)
-endif()
-
-include("${CMAKE_CURRENT_LIST_DIR}/TsFileTargets.cmake")
-
-check_required_components(TsFile)
+@TSFILE_STATIC_DEPENDENCY_CODE@
diff --git a/cpp/cmake/libtsfile.pc.in b/cpp/cmake/libtsfile.pc.in
index ff820d0d8..f7b3db9bf 100644
--- a/cpp/cmake/libtsfile.pc.in
+++ b/cpp/cmake/libtsfile.pc.in
@@ -18,7 +18,7 @@
# The .pc file is installed below ${prefix}/@CMAKE_INSTALL_LIBDIR@/pkgconfig.
# Deriving the prefix from its own location keeps DESTDIR and relocations
# working without embedding the build-time install prefix.
-prefix=${pcfiledir}/../..
+prefix=${pcfiledir}/@TSFILE_PKGCONFIG_PREFIX_RELATIVE@
exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@
includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
diff --git a/cpp/cmake/tests/CheckStaticMSVCRuntime.cmake b/cpp/cmake/tests/CheckStaticMSVCRuntime.cmake
new file mode 100644
index 000000000..a30ddfcab
--- /dev/null
+++ b/cpp/cmake/tests/CheckStaticMSVCRuntime.cmake
@@ -0,0 +1,38 @@
+#[[
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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
+
+ https://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.
+]]
+
+# A configure-time contract test also runnable on non-MSVC hosts. Runtime
+# properties initialize on every platform; Windows CI checks actual PE imports.
+function(tsfile_check_static_runtime DIRECTORY)
+ get_property(_TARGETS DIRECTORY "${DIRECTORY}" PROPERTY BUILDSYSTEM_TARGETS)
+ foreach (_TARGET IN LISTS _TARGETS)
+ get_target_property(_TYPE ${_TARGET} TYPE)
+ if (_TYPE MATCHES "^(STATIC_LIBRARY|SHARED_LIBRARY|OBJECT_LIBRARY|EXECUTABLE)$")
+ get_target_property(_RUNTIME ${_TARGET} MSVC_RUNTIME_LIBRARY)
+ if (NOT _RUNTIME STREQUAL "MultiThreaded$<$<CONFIG:Debug>:Debug>")
+ message(FATAL_ERROR "${_TARGET} has inconsistent MSVC runtime: ${_RUNTIME}")
+ endif ()
+ endif ()
+ endforeach ()
+ get_property(_SUBDIRECTORIES DIRECTORY "${DIRECTORY}" PROPERTY SUBDIRECTORIES)
+ foreach (_SUBDIRECTORY IN LISTS _SUBDIRECTORIES)
+ tsfile_check_static_runtime("${_SUBDIRECTORY}")
+ endforeach ()
+endfunction()
+cmake_language(DEFER CALL tsfile_check_static_runtime "${CMAKE_SOURCE_DIR}")
diff --git a/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt b/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt
index bf0f83e34..40ea9481d 100644
--- a/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt
+++ b/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt
@@ -17,6 +17,9 @@ specific language governing permissions and limitations
under the License.
]]
cmake_minimum_required(VERSION 3.11)
+if (POLICY CMP0091)
+ cmake_policy(SET CMP0091 NEW)
+endif ()
project(TsFileInstalledConsumer LANGUAGES CXX)
find_package(TsFile CONFIG REQUIRED)
diff --git a/cpp/cmake/tests/projects/InstalledConsumer/main.cc b/cpp/cmake/tests/projects/InstalledConsumer/main.cc
index 8d242f678..2d265d429 100644
--- a/cpp/cmake/tests/projects/InstalledConsumer/main.cc
+++ b/cpp/cmake/tests/projects/InstalledConsumer/main.cc
@@ -21,4 +21,7 @@
#include <tsfile/writer/tsfile_table_writer.h>
#include <tsfile/writer/tsfile_tree_writer.h>
-int main() { return TS_DATATYPE_INT32 == 1 ? 0 : 1; }
+int main() {
+ if (set_global_time_encoding(TS_ENCODING_TS_2DIFF) != 0) return 1;
+ return get_global_time_encoding() == TS_ENCODING_TS_2DIFF ? 0 : 1;
+}
diff --git a/cpp/cmake/tests/projects/PkgConfigConsumer/main.c b/cpp/cmake/tests/projects/PkgConfigConsumer/main.c
index f32e5a550..05a1566f7 100644
--- a/cpp/cmake/tests/projects/PkgConfigConsumer/main.c
+++ b/cpp/cmake/tests/projects/PkgConfigConsumer/main.c
@@ -18,4 +18,7 @@
*/
#include <tsfile/cwrapper/tsfile_cwrapper.h>
-int main(void) { return TS_DATATYPE_INT32 == 1 ? 0 : 1; }
+int main(void) {
+ if (set_global_time_encoding(TS_ENCODING_TS_2DIFF) != 0) return 1;
+ return get_global_time_encoding() == TS_ENCODING_TS_2DIFF ? 0 : 1;
+}
diff --git a/cpp/cmake/tests/projects/PkgConfigConsumer/main.cc b/cpp/cmake/tests/projects/PkgConfigConsumer/main.cc
index be94b8543..daa807236 100644
--- a/cpp/cmake/tests/projects/PkgConfigConsumer/main.cc
+++ b/cpp/cmake/tests/projects/PkgConfigConsumer/main.cc
@@ -16,6 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
+#include <tsfile/cwrapper/tsfile_cwrapper.h>
#include <tsfile/reader/tsfile_reader.h>
-int main() { return 0; }
+int main() {
+ if (set_global_time_encoding(TS_ENCODING_TS_2DIFF) != 0) return 1;
+ return get_global_time_encoding() == TS_ENCODING_TS_2DIFF ? 0 : 1;
+}
diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt
index 8854e5d6c..96c7d9f47 100644
--- a/cpp/src/CMakeLists.txt
+++ b/cpp/src/CMakeLists.txt
@@ -126,6 +126,10 @@ else()
# Consumers of the CMake target must see TSFILE_API without DLL import
# decoration when linking the static library on MSVC.
target_compile_definitions(tsfile INTERFACE TSFILE_STATIC)
+ if (ENABLE_ANTLR4 AND (TSFILE_ANTLR4_SOURCE STREQUAL "BUNDLED" OR
+ TSFILE_ANTLR4_SYSTEM_STATIC))
+ target_compile_definitions(tsfile INTERFACE ANTLR4CPP_STATIC)
+ endif ()
endif()
if (${COV_ENABLED})
@@ -151,9 +155,10 @@ foreach (_TSFILE_OBJECT_TARGET IN LISTS _TSFILE_OBJECT_TARGETS)
$<TARGET_OBJECTS:${_TSFILE_OBJECT_TARGET}>)
endforeach ()
-if (NOT "${_TSFILE_PROJECT_DEPENDENCIES}" STREQUAL "")
- target_link_libraries(tsfile PRIVATE ${_TSFILE_PROJECT_DEPENDENCIES})
-endif ()
+foreach (_TSFILE_DEPENDENCY IN LISTS _TSFILE_PROJECT_DEPENDENCIES)
+ target_link_libraries(tsfile PRIVATE $<BUILD_INTERFACE:${_TSFILE_DEPENDENCY}>)
+endforeach ()
+unset(_TSFILE_DEPENDENCY)
unset(_TSFILE_OBJECT_SOURCES)
unset(_TSFILE_OBJECT_TARGET)
@@ -230,6 +235,8 @@ if (TSFILE_BUILD_SHARED)
# dependencies at link time. Do not export those build-only targets as
# consumer link requirements; they are not installed with the package.
set_property(TARGET tsfile PROPERTY INTERFACE_LINK_LIBRARIES "")
+else ()
+ include(${CMAKE_SOURCE_DIR}/cmake/TsFileStaticDependencies.cmake)
endif ()
# A shared library is a RUNTIME plus an import ARCHIVE on Windows and a LIBRARY
diff --git a/packaging/README.md b/packaging/README.md
index 910026236..feffc2cb2 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -147,11 +147,52 @@ runtime, development files, and tools under a relocatable prefix. In
particular, the MSVC outputs are installed as `bin/tsfile.dll`,
`lib/tsfile.lib`, and `bin/tsfile-cli.exe`.
+The portable ZIP targets Windows 10 / Windows Server 2022 or newer and uses
+the static MSVC runtime (`/MT`) in Release. `TSFILE_MSVC_STATIC_RUNTIME=ON`
+requires CMake 3.15+ and `TSFILE_DEPENDENCY_SOURCE=BUNDLED`: all codec archives,
+TsFile, and the CLI are compiled with the same runtime selection. The workflow
+checks the runtime property on every native target and inspects every shipped
+EXE/DLL with `dumpbin`, rejecting dynamic Visual C++ runtime imports and missing
+non-system DLLs in both the staged tree and the extracted ZIP. Running the CLI
+does not require a separate Visual C++ Redistributable installation. Microsoft
+redistributable DLLs are not copied from the runner or included in the ZIP;
+runtime security updates require rebuilding the static-runtime package.
+
+C++ SDK consumers should use the matching MSVC v143 Release toolset and `/MT`
+(`CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded`, with CMake policy CMP0091 set to
+NEW before `project()`). Keep allocation and release paired through the public
+APIs; CRT-owned objects such as `FILE*` must not cross DLL boundaries. The
+installed-consumer fixture demonstrates a public setting/getter round trip
+that requires real symbols from `tsfile.dll`.
+
The workflow uploads that ZIP as the intermediate artifact
`native-windows-msvc-x86_64` for 14 days. Like the Linux jobs, it validates the
license files, CMake package config, staged executable, library, import library,
and public headers without publishing the artifact.
+## Static SDK and install regressions
+
+`TSFILE_BUILD_SHARED=OFF` remains installable. Its CMake package rediscovers
+system codec packages on the consuming machine and installs bundled codec
+archives under `<libdir>/tsfile/<configuration>`. Link with `TsFile::tsfile`
+to receive the complete static dependency list. Bundled archives and CMake
+metadata move with the install prefix; system dependency development packages
+must be available on the consuming machine. The shared package retains its
+existing runtime/development/tools layout.
+
+The pkg-config prefix is derived from the configured pkg-config installation
+directory, including multiarch library directories. The optional Unix native
+integration suite builds, installs, relocates, and links CMake and pkg-config
+consumers. It requires CMake 3.19+ for the runtime-property test, pkg-config,
+system LZ4, and a verified dependency cache containing ANTLR4, utf8cpp, zlib,
+and LZOKAY archives:
+
+```bash
+TSFILE_RUN_INSTALL_TESTS=1 \
+TSFILE_TEST_DEPENDENCY_CACHE=/path/to/dependency-cache \
+python3 -m unittest discover -s packaging/tests -p 'test_*.py' -v
+```
+
## Final native package bundle
Only after the Ubuntu 22.04 and 24.04 DEB installation tests, AlmaLinux 9 RPM
diff --git a/packaging/homebrew/tsfile-dev.rb.in b/packaging/homebrew/tsfile-dev.rb.in
index cc5b60bb2..567a2fcfc 100644
--- a/packaging/homebrew/tsfile-dev.rb.in
+++ b/packaging/homebrew/tsfile-dev.rb.in
@@ -54,7 +54,8 @@ class TsfileDev < Formula
#include <tsfile/cwrapper/tsfile_cwrapper.h>
int main() {
- return TS_DATATYPE_INT32 == 1 ? 0 : 1;
+ if (set_global_time_encoding(TS_ENCODING_TS_2DIFF) != 0) return 1;
+ return get_global_time_encoding() == TS_ENCODING_TS_2DIFF ? 0 : 1;
}
CPP
diff --git a/packaging/homebrew/tsfile.rb b/packaging/homebrew/tsfile.rb
index 3e96f3dca..1beaac75b 100644
--- a/packaging/homebrew/tsfile.rb
+++ b/packaging/homebrew/tsfile.rb
@@ -59,7 +59,8 @@ class Tsfile < Formula
#include <tsfile/cwrapper/tsfile_cwrapper.h>
int main() {
- return TS_DATATYPE_INT32 == 1 ? 0 : 1;
+ if (set_global_time_encoding(TS_ENCODING_TS_2DIFF) != 0) return 1;
+ return get_global_time_encoding() == TS_ENCODING_TS_2DIFF ? 0 : 1;
}
CPP
diff --git a/packaging/scripts/native_package_versions.py b/packaging/scripts/native_package_versions.py
index d4bd653eb..19981e377 100644
--- a/packaging/scripts/native_package_versions.py
+++ b/packaging/scripts/native_package_versions.py
@@ -119,9 +119,16 @@ def main(argv: Sequence[str] | None = None) -> int:
)
arguments.json_out.write_text(json.dumps(versions, indent=2) + "\n", encoding="utf-8")
if arguments.github_output is not None:
- with arguments.github_output.open("a", encoding="utf-8") as github_output:
+ with arguments.github_output.open("a+b") as github_output:
+ if github_output.tell():
+ github_output.seek(-1, 2)
+ if github_output.read(1) != b"\n":
+ github_output.write(b"\n")
github_output.write(
- "\n".join(f"{key}={value}" for key, value in versions.items()) + "\n"
+ (
+ "\n".join(f"{key}={value}" for key, value in versions.items())
+ + "\n"
+ ).encode("utf-8")
)
return 0
diff --git a/packaging/scripts/verify_windows_runtime.py b/packaging/scripts/verify_windows_runtime.py
new file mode 100644
index 000000000..79a7c90b4
--- /dev/null
+++ b/packaging/scripts/verify_windows_runtime.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""Reject portable Windows binaries that need an unshipped DLL or dynamic CRT."""
+
+import argparse
+import re
+import subprocess
+from pathlib import Path
+
+# Windows 10 / Server 2022 inbox DLLs used by TsFile and the static CRT. This
+# explicit list deliberately does not trust whatever happens to be on PATH or
+# in the development runner's System32 (which also contains the VC redist).
+SYSTEM_DLLS = {
+ "advapi32.dll",
+ "bcrypt.dll",
+ "crypt32.dll",
+ "kernel32.dll",
+ "ntdll.dll",
+ "ole32.dll",
+ "oleaut32.dll",
+ "rpcrt4.dll",
+ "secur32.dll",
+ "shell32.dll",
+ "shlwapi.dll",
+ "user32.dll",
+ "version.dll",
+ "ws2_32.dll",
+}
+DYNAMIC_CRT = re.compile(r"^(msvcp|msvcr|vcruntime|ucrtbase|api-ms-win-crt-)", re.I)
+
+
+def parse_dependents(output: str) -> set[str]:
+ imports = set(re.findall(r"^\s+([A-Za-z0-9_.-]+\.dll)\s*$", output, re.M | re.I))
+ if not imports:
+ raise ValueError("dumpbin did not report any DLL imports")
+ return {name.lower() for name in imports}
+
+
+def validate_dependents(binary: str, imports: set[str], colocated: set[str]) -> None:
+ for name in sorted(imports):
+ if DYNAMIC_CRT.match(name):
+ raise ValueError(
+ f"{binary} imports {name}; the ZIP requires a static MSVC runtime"
+ )
+ if (
+ name not in SYSTEM_DLLS
+ and not name.startswith("api-ms-win-")
+ and name not in colocated
+ ):
+ raise ValueError(f"{binary} needs an unshipped non-system DLL: {name}")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("prefix", type=Path)
+ args = parser.parse_args()
+ binaries = sorted(
+ path
+ for path in args.prefix.rglob("*")
+ if path.suffix.lower() in (".exe", ".dll")
+ )
+ if not binaries:
+ parser.error("the prefix contains no Windows binaries")
+ for binary in binaries:
+ output = subprocess.run(
+ ["dumpbin", "/nologo", "/dependents", str(binary)],
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ ).stdout
+ imports = parse_dependents(output)
+ colocated = {
+ path.name.lower()
+ for path in binary.parent.iterdir()
+ if path.suffix.lower() == ".dll"
+ }
+ validate_dependents(binary.name, imports, colocated)
+ print(
+ f"{binary.name}: static CRT; all {len(imports)} DLL dependencies are inbox or colocated"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/check_native_workflow.rb b/packaging/tests/check_native_workflow.rb
new file mode 100644
index 000000000..82e5e506f
--- /dev/null
+++ b/packaging/tests/check_native_workflow.rb
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+require "yaml"
+require "shellwords"
+require "open3"
+
+path = ARGV.fetch(0, File.expand_path("../../.github/workflows/native-packages.yml", __dir__))
+workflow = YAML.load_file(path)
+triggers = workflow["on"] || workflow[true] # Psych's YAML 1.1 boolean parsing
+raise "Workflow must remain manual-only" unless triggers.keys == ["workflow_dispatch"]
+raise "Workflow permissions must remain read-only" unless workflow["permissions"] == { "contents" => "read" }
+
+jobs = workflow.fetch("jobs")
+bootstrap = jobs.fetch("build-rpm").fetch("steps").find { |step| step["name"] == "Install RPM build prerequisites" }
+packages = Shellwords.split(bootstrap.fetch("run").gsub("\\\n", " "))
+raise "AlmaLinux 9 curl-minimal conflicts with full curl" if packages.include?("curl")
+raise "RPM bootstrap must retain curl-minimal" unless packages.include?("curl-minimal")
+
+windows = jobs.fetch("build-windows").fetch("steps").filter_map { |step| step["run"] }.join("\n")
+raise "Windows ZIP needs a consistent static CRT" unless windows.include?("-DTSFILE_MSVC_STATIC_RUNTIME=ON") && windows.include?("-DTSFILE_DEPENDENCY_SOURCE=BUNDLED")
+raise "All native target runtimes must be checked" unless windows.include?("CheckStaticMSVCRuntime.cmake")
+raise "Both staged and extracted PE imports must be checked" unless windows.scan("python packaging/scripts/verify_windows_runtime.py").size == 2
+
+%w[test-deb test-rpm build-windows].each do |name|
+ steps = jobs.fetch(name).fetch("steps")
+ raise "#{name} must check out the consumer fixture" unless steps.any? { |step| step["uses"].to_s.start_with?("actions/checkout@") }
+ runs = steps.filter_map { |step| step["run"] }.join("\n")
+ raise "#{name} must use the installed CMake consumer" unless runs.include?("-S cpp/cmake/tests/projects/InstalledConsumer")
+end
+
+jobs.each_value do |job|
+ job.fetch("steps").each do |step|
+ next unless step["run"] && step.fetch("shell", "bash") == "bash"
+ output, status = Open3.capture2e("bash", "-n", stdin_data: step["run"])
+ raise "Invalid shell in #{step['name']}: #{output}" unless status.success?
+ end
+end
+puts "PASS: manual/read-only workflow, AlmaLinux bootstrap, Windows CRT checks, shared consumers, and Bash syntax"
diff --git a/packaging/tests/test_native_install.py b/packaging/tests/test_native_install.py
new file mode 100644
index 000000000..ea0574834
--- /dev/null
+++ b/packaging/tests/test_native_install.py
@@ -0,0 +1,206 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""Opt-in native configure/install/link regressions (requires CMake and LZ4)."""
+
+import os
+import re
+import shlex
+import subprocess
+import tempfile
+import textwrap
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+FIXTURES = ROOT / "cpp/cmake/tests/projects"
+
+
+@unittest.skipUnless(
+ os.environ.get("TSFILE_RUN_INSTALL_TESTS") == "1", "opt-in native builds"
+)
+class NativeInstallTest(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix="tsfile-install-")
+ self.addCleanup(self.temporary.cleanup)
+ self.directory = Path(self.temporary.name)
+ self.build = self.directory / "build"
+ self.stage = self.directory / "stage"
+
+ def run_command(self, *arguments, env=None):
+ result = subprocess.run(
+ [str(argument) for argument in arguments],
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env=env,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout[-14000:])
+ return result.stdout.strip()
+
+ def configure(self, *options):
+ arguments = [
+ "cmake",
+ "-S",
+ ROOT / "cpp",
+ "-B",
+ self.build,
+ "-DCMAKE_BUILD_TYPE=Release",
+ "-DBUILD_TEST=OFF",
+ "-DBUILD_TOOLS=OFF",
+ "-DTSFILE_DEPENDENCY_SOURCE=SYSTEM",
+ "-DTSFILE_ENABLE_NATIVE_ARCH=OFF",
+ f"-DCMAKE_INSTALL_PREFIX={self.stage}",
+ ]
+ arguments.extend(
+ f"-DENABLE_{name}=OFF"
+ for name in (
+ "ANTLR4",
+ "SNAPPY",
+ "LZ4",
+ "LZOKAY",
+ "ZLIB",
+ "ZSTD",
+ "LZMA2",
+ "SIMD",
+ )
+ )
+ self.run_command(*arguments, *options)
+
+ def install(self):
+ self.run_command("cmake", "--build", self.build, "--parallel", "8")
+ self.run_command("cmake", "--install", self.build, "--config", "Release")
+ relocated = self.directory / "relocated"
+ self.stage.rename(relocated)
+ self.stage = relocated
+ for path in self.stage.rglob("*.cmake"):
+ self.assertNotIn(str(self.build), path.read_text(), str(path))
+ self.assertNotIn(str(ROOT / "cpp"), path.read_text(), str(path))
+
+ def consumer(self, fixture, target, env=None, *options):
+ build = self.directory / fixture
+ self.run_command(
+ "cmake",
+ "-S",
+ FIXTURES / fixture,
+ "-B",
+ build,
+ f"-DCMAKE_PREFIX_PATH={self.stage}",
+ *options,
+ env=env,
+ )
+ self.run_command("cmake", "--build", build, "--parallel", "8", env=env)
+ self.run_command(build / target, env=env)
+
+ def test_static_system_lz4_install_and_relocated_consumer(self):
+ self.configure("-DTSFILE_BUILD_SHARED=OFF", "-DENABLE_LZ4=ON")
+ self.install()
+ self.consumer("InstalledConsumer", "tsfile_installed_consumer")
+
+ def test_static_bundled_install_and_relocated_consumer(self):
+ self.configure(
+ "-DTSFILE_BUILD_SHARED=OFF",
+ "-DTSFILE_DEPENDENCY_SOURCE=BUNDLED",
+ "-DENABLE_ANTLR4=ON",
+ "-DENABLE_ZLIB=ON",
+ "-DENABLE_LZOKAY=ON",
+ "-DTSFILE_DEPENDENCY_OFFLINE=ON",
+ f"-DTSFILE_DEPENDENCY_CACHE={os.environ['TSFILE_TEST_DEPENDENCY_CACHE']}",
+ )
+ self.install()
+ self.consumer("InstalledConsumer", "tsfile_installed_consumer")
+
+ def test_multiarch_pkgconfig_install_and_relocated_consumers(self):
+ self.configure("-DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu")
+ self.install()
+ env = dict(
+ os.environ,
+ PKG_CONFIG_PATH=str(self.stage / "lib/x86_64-linux-gnu/pkgconfig"),
+ )
+ prefix = self.run_command(
+ "pkg-config", "--variable=prefix", "libtsfile", env=env
+ )
+ self.assertEqual(Path(prefix).resolve(), self.stage.resolve())
+ self.consumer("PkgConfigConsumer", "tsfile_pkgconfig_consumer", env)
+ self.run_command(
+ self.directory / "PkgConfigConsumer/tsfile_pkgconfig_cpp_consumer", env=env
+ )
+
+ def test_installed_smoke_requires_a_library_symbol(self):
+ self.configure()
+ self.install()
+ result = subprocess.run(
+ [
+ *shlex.split(os.environ.get("CXX", "c++")),
+ "-std=c++11",
+ f"-I{self.stage / 'include'}",
+ f"-I{self.stage / 'include/tsfile'}",
+ str(FIXTURES / "InstalledConsumer/main.cc"),
+ "-o",
+ str(self.directory / "unlinked"),
+ ],
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ self.assertNotEqual(
+ result.returncode, 0, "Smoke consumer linked successfully without libtsfile"
+ )
+ self.assertIn("get_global_time_encoding", result.stdout)
+ self.consumer("InstalledConsumer", "tsfile_installed_consumer")
+ for formula in ("tsfile.rb", "tsfile-dev.rb.in"):
+ source = re.search(
+ r"write <<~CPP\n(.*?)\n\s+CPP",
+ (ROOT / "packaging/homebrew" / formula).read_text(),
+ re.S,
+ ).group(1)
+ source_path = self.directory / f"{formula}.cc"
+ source_path.write_text(textwrap.dedent(source))
+ command = [
+ *shlex.split(os.environ.get("CXX", "c++")),
+ "-std=c++11",
+ f"-I{self.stage / 'include'}",
+ str(source_path),
+ "-o",
+ str(self.directory / "brew-consumer"),
+ ]
+ unlinked = subprocess.run(
+ command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
+ self.assertNotEqual(unlinked.returncode, 0, formula)
+ self.assertIn("get_global_time_encoding", unlinked.stdout)
+ self.run_command(
+ *command,
+ f"-L{self.stage / 'lib'}",
+ f"-Wl,-rpath,{self.stage / 'lib'}",
+ "-ltsfile",
+ )
+ self.run_command(self.directory / "brew-consumer")
+
+ def test_static_msvc_runtime_reaches_dependency_and_project_targets(self):
+ self.configure(
+ "-DTSFILE_MSVC_STATIC_RUNTIME=ON",
+ "-DTSFILE_DEPENDENCY_SOURCE=BUNDLED",
+ "-DENABLE_ZLIB=ON",
+ "-DTSFILE_DEPENDENCY_OFFLINE=ON",
+ f"-DTSFILE_DEPENDENCY_CACHE={os.environ['TSFILE_TEST_DEPENDENCY_CACHE']}",
+ f"-DCMAKE_PROJECT_TsFile_CPP_INCLUDE={ROOT / 'cpp/cmake/tests/CheckStaticMSVCRuntime.cmake'}",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_native_package_versions.py b/packaging/tests/test_native_package_versions.py
index a2cc05221..b5a6b80c9 100644
--- a/packaging/tests/test_native_package_versions.py
+++ b/packaging/tests/test_native_package_versions.py
@@ -141,6 +141,47 @@ class NativePackageVersionsTest(unittest.TestCase):
["existing=value", *[f"{key}={value}" for key, value in expected.items()]],
)
+ def test_cli_separates_unterminated_github_output_without_changing_bytes(self):
+ for existing in (
+ b"",
+ b"existing=value",
+ b"existing=value\n",
+ b"existing=value\r\n",
+ ):
+ with self.subTest(
+ existing=existing
+ ), tempfile.TemporaryDirectory() as directory:
+ output = Path(directory) / "github-output.txt"
+ output.write_bytes(existing)
+ subprocess.run(
+ [
+ sys.executable,
+ str(MODULE_PATH),
+ "--cmake-file",
+ str(CPP_CMAKE_FILE),
+ "--build-date",
+ "20260910",
+ "--run-number",
+ "123",
+ "--run-attempt",
+ "1",
+ "--git-sha",
+ "abcdef123456",
+ "--json-out",
+ str(Path(directory) / "versions.json"),
+ "--github-output",
+ str(output),
+ ],
+ check=True,
+ )
+ separator = b"\n" if existing and not existing.endswith(b"\n") else b""
+ self.assertTrue(
+ output.read_bytes().startswith(
+ existing + separator + b"base_version=2.5.0\n"
+ ),
+ output.read_bytes(),
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/packaging/tests/test_windows_runtime.py b/packaging/tests/test_windows_runtime.py
new file mode 100644
index 000000000..8258f00a0
--- /dev/null
+++ b/packaging/tests/test_windows_runtime.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+import importlib.util
+import unittest
+from pathlib import Path
+
+MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts/verify_windows_runtime.py"
+
+
+class WindowsRuntimeTest(unittest.TestCase):
+ def checker(self):
+ self.assertTrue(
+ MODULE_PATH.is_file(), "Windows artifacts need a runtime dependency check"
+ )
+ spec = importlib.util.spec_from_file_location(
+ "verify_windows_runtime", MODULE_PATH
+ )
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+ def test_allows_only_inbox_or_colocated_package_dlls(self):
+ module = self.checker()
+ imports = module.parse_dependents(
+ "File Type: EXECUTABLE IMAGE\n\n Image has the following dependencies:\n\n"
+ " tsfile.dll\n KERNEL32.dll\n api-ms-win-core-synch-l1-2-0.dll\n\n"
+ " Summary\n 1000 .data\n"
+ )
+ module.validate_dependents("tsfile-cli.exe", imports, {"tsfile.dll"})
+ self.assertEqual(len(imports), 3)
+
+ def test_rejects_dynamic_crt_even_when_runner_or_package_provides_it(self):
+ module = self.checker()
+ for dll in (
+ "VCRUNTIME140.dll",
+ "MSVCP140.dll",
+ "ucrtbase.dll",
+ "api-ms-win-crt-runtime-l1-1-0.dll",
+ ):
+ with self.subTest(dll=dll), self.assertRaisesRegex(
+ ValueError, "static MSVC runtime"
+ ):
+ module.validate_dependents("tsfile.dll", {dll.lower()}, {dll.lower()})
+
+ def test_rejects_unshipped_codec_dll(self):
+ module = self.checker()
+ with self.assertRaisesRegex(ValueError, "snappy.dll"):
+ module.validate_dependents("tsfile.dll", {"snappy.dll"}, {"tsfile.dll"})
+
+ def test_rejects_unrecognized_dumpbin_output(self):
+ with self.assertRaisesRegex(ValueError, "DLL imports"):
+ self.checker().parse_dependents(
+ "fatal error LNK1107: invalid or corrupt file"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()