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()