You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tsfile.apache.org by ho...@apache.org on 2026/10/16 11:10:21 UTC
(tsfile) branch develop updated: Add bundled native release packages and installed artifact tests (#1016)
This is an automated email from the ASF dual-hosted git repository.
hongzhi-gao pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/tsfile.git
The following commit(s) were added to refs/heads/develop by this push:
new 12e39fe9d Add bundled native release packages and installed artifact tests (#1016)
12e39fe9d is described below
commit 12e39fe9d9dace63cc0ad39664aa0fbf5e29f52f
Author: Colin Lee <10...@users.noreply.github.com>
AuthorDate: Fri Oct 16 19:10:15 2026 +0800
Add bundled native release packages and installed artifact tests (#1016)
* Fix portable C++ builds and installed SDK consumption
Fix MSVC language feature detection, runtime selection, native ARM64 builds, dependency downloads and relocatable library paths. Export headers, CMake and pkg-config metadata, static dependencies and native package components. Record the resolved CRT and runtime filename in build-tree and installed SDK metadata so independent consumers can select matching compiler options. Allow existing API tests to consume an installed SDK and include dependency-download regression tests. Inherit th [...]
* Add complete multi-platform native release workflow
Build native SDKs, DEB/RPM packages, Windows ZIPs, Homebrew bottles and Python 3.9-3.14 wheels. Reuse SDKs and automatically read their MSVC runtime configuration. Ship runtime-only wheels with one native library, stripped Unix binaries and license texts; reject build-only files, duplicate libraries and ELF debug sections. Test every installed package, wheel and relocated SDK in fresh jobs using existing C++ and Go tests, CLI operations on Python resources and the full pytest suite. G [...]
* Bundle third-party dependencies in every native release package
* Build native release artifacts with release publication metadata
* Make release-version tests independent of the checkout version
* Fix native SDK linkage and CLI runtime paths
Include bundled and system link dependencies in static pkg-config metadata, with per-configuration archive paths and C++ runtime flags. Preserve CMake's build-tree CLI RPATH while keeping relocated installations working in lib64 and multiarch layouts.
Align exported path-parser macros with the library's object targets to resolve Linux Debug UTF-8 failures. Exercise installed C and C++ consumers with real codec round trips and run the installation regressions in CI.
---
.github/workflows/cpp-dependency-source.yml | 28 +
.github/workflows/native-packages.yml | 2331 ++++++++++++++++++++
.github/workflows/wheels.yml | 314 ---
.gitignore | 3 +
cpp/CMakeLists.txt | 222 +-
cpp/README.md | 7 +-
cpp/VersionUpdater.groovy | 11 +
cpp/cmake/ANTLR4Source.cmake | 11 +-
cpp/cmake/DependencySource.cmake | 2 +-
cpp/cmake/DownloadWithRetry.cmake | 63 +
cpp/cmake/TsFileBuildConfig.cmake | 45 +
cpp/cmake/TsFileConfig.cmake.in | 43 +
cpp/cmake/TsFilePublicHeaders.cmake | 47 +
cpp/cmake/TsFileStaticDependencies.cmake | 185 ++
cpp/cmake/TsFileStaticDependencies.cmake.in | 20 +
cpp/cmake/libtsfile.pc.in | 32 +
cpp/cmake/tests/ANTLR4DependencyTest.cmake | 9 +
cpp/cmake/tests/CheckStaticMSVCRuntime.cmake | 38 +
.../tests/projects/ANTLR4Dependency/CMakeLists.txt | 2 +-
.../projects/InstalledConsumer/CMakeLists.txt | 45 +
cpp/cmake/tests/projects/InstalledConsumer/main.cc | 27 +
.../projects/PkgConfigConsumer/CMakeLists.txt | 48 +
cpp/cmake/tests/projects/PkgConfigConsumer/main.c | 24 +
cpp/cmake/tests/projects/PkgConfigConsumer/main.cc | 106 +
cpp/pom.xml | 59 +-
cpp/src/CMakeLists.txt | 127 +-
cpp/src/utils/util_define.h | 8 +-
cpp/test/CMakeLists.txt | 8 +
cpp/test/InstalledSDKTests.cmake | 63 +
cpp/third_party/CMakeLists.txt | 23 +
cpp/third_party/README.md | 4 +-
cpp/tools/CMakeLists.txt | 17 +-
packaging/README.md | 428 ++++
packaging/cmake/InstalledGTest/CMakeLists.txt | 36 +
packaging/cmake/PythonSDK.cmake | 24 +
packaging/homebrew/tsfile.rb.in | 61 +
packaging/scripts/assemble_native_packages.py | 542 +++++
packaging/scripts/build_python_sdk.sh | 72 +
packaging/scripts/check_python_wheel.py | 118 +
packaging/scripts/native_package_versions.py | 95 +
packaging/scripts/render_homebrew_formula.py | 85 +
packaging/scripts/run_maven_with_retry.py | 72 +
packaging/scripts/verify_macos_runtime.py | 124 ++
packaging/scripts/verify_windows_runtime.py | 101 +
packaging/tests/check_native_workflow.rb | 295 +++
packaging/tests/cli_package_smoke.py | 278 +++
packaging/tests/cpp_installed_tests.py | 141 ++
packaging/tests/go_installed_tests.py | 168 ++
packaging/tests/python_installed_tests.py | 149 ++
packaging/tests/python_wheel_smoke.py | 98 +
packaging/tests/sdk_archive_tests.py | 157 ++
packaging/tests/test_assemble_native_packages.py | 591 +++++
packaging/tests/test_dependency_download.py | 105 +
packaging/tests/test_installed_test_runners.py | 111 +
packaging/tests/test_macos_runtime.py | 120 +
packaging/tests/test_maven_retry.py | 92 +
packaging/tests/test_native_install.py | 515 +++++
packaging/tests/test_native_package_versions.py | 182 ++
packaging/tests/test_python_wheel_contents.py | 86 +
packaging/tests/test_render_homebrew_formula.py | 182 ++
packaging/tests/test_sdk_archive_tests.py | 122 +
packaging/tests/test_sdk_build_config.py | 163 ++
packaging/tests/test_windows_runtime.py | 73 +
pom.xml | 43 +
python/MANIFEST.in | 22 +
python/VersionUpdater.groovy | 11 +
python/_build_support.py | 101 +
python/check_cpp_build.py | 70 +
python/pom.xml | 36 +
python/pyproject.toml | 22 +-
python/setup.py | 66 +-
71 files changed, 9355 insertions(+), 374 deletions(-)
diff --git a/.github/workflows/cpp-dependency-source.yml b/.github/workflows/cpp-dependency-source.yml
index 72d977ba8..80fa184d0 100644
--- a/.github/workflows/cpp-dependency-source.yml
+++ b/.github/workflows/cpp-dependency-source.yml
@@ -26,6 +26,7 @@ on:
paths:
- '.github/workflows/cpp-dependency-source.yml'
- 'cpp/**'
+ - 'packaging/tests/test_native_install.py'
- 'pom.xml'
pull_request:
branches:
@@ -36,6 +37,7 @@ on:
paths:
- '.github/workflows/cpp-dependency-source.yml'
- 'cpp/**'
+ - 'packaging/tests/test_native_install.py'
- 'pom.xml'
workflow_dispatch:
@@ -256,6 +258,32 @@ jobs:
- name: Build TsFile offline
run: cmake --build "$TSFILE_BUILD_DIR" --target tsfile --parallel 2
+ installed-sdk:
+ name: Relocated static SDK and CLI install layouts
+ needs: bundled
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Download verified dependency archives
+ uses: actions/download-artifact@v8
+ with:
+ name: tsfile-cpp-dependency-archives
+ path: ${{ env.TSFILE_DEPENDENCY_CACHE }}
+
+ - name: Install SDK test prerequisites
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y uuid-dev liblz4-dev pkg-config ninja-build
+
+ - name: Test installed CMake and pkg-config consumers and CLI RPATH
+ env:
+ TSFILE_RUN_INSTALL_TESTS: '1'
+ TSFILE_TEST_DEPENDENCY_CACHE: ${{ github.workspace }}/${{ env.TSFILE_DEPENDENCY_CACHE }}
+ run: python3 -m unittest discover -s packaging/tests -p test_native_install.py -v
+
system:
name: SYSTEM from an external package prefix
runs-on: ubuntu-24.04
diff --git a/.github/workflows/native-packages.yml b/.github/workflows/native-packages.yml
new file mode 100644
index 000000000..5bf2f5fbc
--- /dev/null
+++ b/.github/workflows/native-packages.yml
@@ -0,0 +1,2331 @@
+#
+# 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.
+
+name: Build native release artifacts
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ prepare:
+ name: Prepare package versions
+ runs-on: ubuntu-24.04
+ outputs:
+ base_version: ${{ steps.versions.outputs.base_version }}
+ python_version: ${{ steps.versions.outputs.python_version }}
+ logical_version: ${{ steps.versions.outputs.logical_version }}
+ deb_version: ${{ steps.versions.outputs.deb_version }}
+ rpm_version: ${{ steps.versions.outputs.rpm_version }}
+ rpm_release: ${{ steps.versions.outputs.rpm_release }}
+ archive_version: ${{ steps.versions.outputs.archive_version }}
+ homebrew_version: ${{ steps.versions.outputs.homebrew_version }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+ 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: Upload CLI data from the source checkout
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cli-test-data
+ path: python/tests/resources/*.tsfile
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Generate package versions
+ id: versions
+ run: |
+ python3 packaging/scripts/native_package_versions.py \
+ --cmake-file cpp/CMakeLists.txt \
+ --json-out versions.json \
+ --github-output "$GITHUB_OUTPUT"
+
+ - name: Upload package versions
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-package-versions
+ path: versions.json
+ if-no-files-found: error
+ retention-days: 14
+
+ build-deb:
+ name: Build Ubuntu 22.04 C++ SDK and DEB packages
+ needs: prepare
+ runs-on: ubuntu-22.04
+ timeout-minutes: 45
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up Java 17
+ uses: actions/setup-java@v6.0.0
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Install DEB build prerequisites
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ cmake \
+ pkg-config \
+ dpkg-dev \
+ uuid-dev
+
+ - name: Build C++ core and CPack metadata with Maven
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ DEBIAN_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: bash
+ run: |
+ ./mvnw -Pwith-cpp package \
+ -Dbuild.type=Release \
+ -Dbuild.test=OFF \
+ -DskipTests \
+ -Dspotless.skip=true \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Dtsfile.version.sync.skip=true \
+ -Denable.cpack=ON \
+ -Dtsfile.archive.version="$ARCHIVE_VERSION" \
+ -Dtsfile.debian.package.version="$DEBIAN_VERSION" \
+ -Dtsfile.rpm.package.version="$RPM_VERSION" \
+ -Dtsfile.rpm.package.release="$RPM_RELEASE"
+ test -f cpp/target/build/CPackConfig.cmake
+
+ - name: Stage and verify the Ubuntu SDK
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: bash
+ run: |
+ sdk_name="tsfile-sdk-ubuntu22.04-amd64-$ARCHIVE_VERSION"
+ sdk_root="$PWD/sdk/$sdk_name"
+ cmake --install cpp/target/build --prefix "$sdk_root"
+ test -f "$sdk_root/bin/tsfile-cli"
+ test -d "$sdk_root/include"
+ test -n "$(find "$sdk_root" -maxdepth 2 -name 'libtsfile.so*' -print -quit)"
+ sdk_pkgconfig="$(find "$sdk_root" -type f -name libtsfile.pc -print -quit)"
+ test -n "$sdk_pkgconfig"
+ "$sdk_root/bin/tsfile-cli" --version
+ PKG_CONFIG_PATH="$(dirname "$sdk_pkgconfig")" pkg-config --cflags --libs libtsfile
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/deb \
+ -DCMAKE_PREFIX_PATH="$sdk_root"
+ cmake --build consumer/deb --parallel
+ ./consumer/deb/tsfile_installed_consumer
+ tar -C sdk -czf "$sdk_root.tar.gz" "$sdk_name"
+ rm -rf "$sdk_root"
+
+ - name: Build and verify DEB packages
+ env:
+ EXPECTED_DEB_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ shell: bash
+ run: |
+ mkdir -p packages
+ cpack -G DEB --config cpp/target/build/CPackConfig.cmake -B packages
+
+ mapfile -t packages < <(find packages -maxdepth 1 -type f -name '*.deb' -print | sort)
+ if [[ ${#packages[@]} -ne 3 ]]; then
+ printf 'Expected exactly 3 DEB packages, found %s\n' "${#packages[@]}"
+ exit 1
+ fi
+
+ actual_names="$({
+ for package in "${packages[@]}"; do
+ dpkg-deb --field "$package" Package
+ done
+ } | sort)"
+ expected_names="$(printf '%s\n' tsfile tsfile-dev tsfile-tools | sort)"
+ if [[ "$actual_names" != "$expected_names" ]]; then
+ printf 'Unexpected DEB package names:\n%s\n' "$actual_names"
+ exit 1
+ fi
+
+ for package in "${packages[@]}"; do
+ package_version="$(dpkg-deb --field "$package" Version)"
+ if [[ "$package_version" != "$EXPECTED_DEB_VERSION" ]]; then
+ printf '%s has version %s, expected %s\n' \
+ "$package" "$package_version" "$EXPECTED_DEB_VERSION"
+ exit 1
+ fi
+ done
+
+ - name: Upload Ubuntu SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-ubuntu22.04-amd64
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload DEB packages
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-deb-ubuntu22.04-amd64
+ path: packages/*.deb
+ if-no-files-found: error
+ retention-days: 14
+
+ test-deb:
+ name: Test DEB packages on ${{ matrix.container }}
+ needs: build-deb
+ runs-on: ubuntu-24.04
+ strategy:
+ fail-fast: false
+ matrix:
+ container:
+ - ubuntu:22.04
+ - ubuntu:24.04
+ container: ${{ matrix.container }}
+ timeout-minutes: 45
+ steps:
+ - name: Checkout installation test sources
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ cpp/test
+ cpp/cmake/tests/projects/InstalledConsumer
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Download DEB packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-deb-ubuntu22.04-amd64
+ path: packages
+
+ - name: Install DEB packages
+ shell: bash
+ run: |
+ apt-get update
+ apt-get install -y ./packages/*.deb cmake build-essential python3 pkg-config git curl ca-certificates
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Verify installed DEB packages
+ shell: bash
+ run: |
+ cli_path="$(command -v tsfile-cli)"
+ cli_path="$(readlink -f -- "$cli_path")"
+ cli_owner="$(dpkg-query -S "$cli_path")"
+ cli_owner="${cli_owner%%:*}"
+ if [[ "$cli_owner" != "tsfile-tools" ]]; then
+ printf '%s is owned by %s, expected tsfile-tools\n' "$cli_path" "$cli_owner"
+ exit 1
+ fi
+ tsfile-cli --version
+ dpkg-query -W tsfile tsfile-dev tsfile-tools
+
+ - name: Build and run an installed-package consumer
+ shell: bash
+ run: |
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build
+ cmake --build consumer/build --parallel
+ ./consumer/build/tsfile_installed_consumer
+
+ - name: Test the installed CLI with Python test data
+ shell: bash
+ run: |
+ python3 packaging/tests/cli_package_smoke.py \
+ --cli "$(command -v tsfile-cli)" --resources cli-test-data
+
+ - name: Compile and run C++ regressions against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/cpp_installed_tests.py \
+ --prefix /usr --build-dir installed-tests
+
+ - name: Run Go tests against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/go_installed_tests.py \
+ --prefix /usr --goarch amd64
+
+ - name: Download the standalone SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-ubuntu22.04-amd64
+ path: sdk-download
+
+ - name: Test the downloaded and relocated SDK archive
+ shell: bash
+ run: |
+ python3 packaging/tests/sdk_archive_tests.py \
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests \
+ --resources cli-test-data --goarch amd64 \
+ --gtest-prefix installed-tests/dependencies
+
+ - name: Upload C++ installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cpp-tests-${{ github.job }}-${{ runner.arch }}-${{ strategy.job-index }}
+ path: |
+ installed-tests/cpp-results.xml
+ installed-tests/tests/Testing/Temporary/LastTest.log
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ build-deb-arm64:
+ name: Build Ubuntu 22.04 ARM64 C++ SDK and DEB packages
+ needs: prepare
+ runs-on: ubuntu-22.04-arm
+ timeout-minutes: 45
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up Java 17
+ uses: actions/setup-java@v6.0.0
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Install DEB build prerequisites
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ cmake \
+ pkg-config \
+ dpkg-dev \
+ uuid-dev
+
+ - name: Verify native ARM64 runner
+ run: test "$(dpkg --print-architecture)" = arm64
+
+ - name: Build C++ core and CPack metadata with Maven
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ DEBIAN_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: bash
+ run: |
+ ./mvnw -Pwith-cpp package \
+ -Dbuild.type=Release \
+ -Dbuild.test=OFF \
+ -DskipTests \
+ -Dspotless.skip=true \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Dtsfile.version.sync.skip=true \
+ -Denable.cpack=ON \
+ -Dtsfile.archive.version="$ARCHIVE_VERSION" \
+ -Dtsfile.debian.package.version="$DEBIAN_VERSION" \
+ -Dtsfile.rpm.package.version="$RPM_VERSION" \
+ -Dtsfile.rpm.package.release="$RPM_RELEASE"
+ test -f cpp/target/build/CPackConfig.cmake
+
+ - name: Stage and verify the Ubuntu ARM64 SDK
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: bash
+ run: |
+ sdk_name="tsfile-sdk-ubuntu22.04-arm64-$ARCHIVE_VERSION"
+ sdk_root="$PWD/sdk/$sdk_name"
+ cmake --install cpp/target/build --prefix "$sdk_root"
+ test -f "$sdk_root/bin/tsfile-cli"
+ test -d "$sdk_root/include"
+ test -n "$(find "$sdk_root" -maxdepth 2 -name 'libtsfile.so*' -print -quit)"
+ sdk_pkgconfig="$(find "$sdk_root" -type f -name libtsfile.pc -print -quit)"
+ test -n "$sdk_pkgconfig"
+ "$sdk_root/bin/tsfile-cli" --version
+ PKG_CONFIG_PATH="$(dirname "$sdk_pkgconfig")" pkg-config --cflags --libs libtsfile
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/deb-arm64 \
+ -DCMAKE_PREFIX_PATH="$sdk_root"
+ cmake --build consumer/deb-arm64 --parallel
+ ./consumer/deb-arm64/tsfile_installed_consumer
+ tar -C sdk -czf "$sdk_root.tar.gz" "$sdk_name"
+ rm -rf "$sdk_root"
+
+ - name: Build and verify ARM64 DEB packages
+ env:
+ EXPECTED_DEB_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ shell: bash
+ run: |
+ mkdir -p packages
+ cpack -G DEB --config cpp/target/build/CPackConfig.cmake -B packages
+ mapfile -t packages < <(find packages -maxdepth 1 -type f -name '*.deb' -print | sort)
+ [[ ${#packages[@]} -eq 3 ]]
+ actual_names="$({ for package in "${packages[@]}"; do dpkg-deb --field "$package" Package; done; } | sort)"
+ expected_names="$(printf '%s\n' tsfile tsfile-dev tsfile-tools | sort)"
+ [[ "$actual_names" == "$expected_names" ]]
+ for package in "${packages[@]}"; do
+ [[ "$(dpkg-deb --field "$package" Architecture)" == arm64 ]]
+ [[ "$(dpkg-deb --field "$package" Version)" == "$EXPECTED_DEB_VERSION" ]]
+ done
+
+ - name: Upload Ubuntu ARM64 SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-ubuntu22.04-arm64
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload ARM64 DEB packages
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-deb-ubuntu22.04-arm64
+ path: packages/*.deb
+ if-no-files-found: error
+ retention-days: 14
+
+ test-deb-arm64:
+ name: Test ARM64 DEB packages on ${{ matrix.container }}
+ needs: build-deb-arm64
+ runs-on: ubuntu-24.04-arm
+ strategy:
+ fail-fast: false
+ matrix:
+ container:
+ - ubuntu:22.04
+ - ubuntu:24.04
+ container: ${{ matrix.container }}
+ timeout-minutes: 45
+ steps:
+ - name: Checkout installation test sources
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ cpp/test
+ cpp/cmake/tests/projects/InstalledConsumer
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Download ARM64 DEB packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-deb-ubuntu22.04-arm64
+ path: packages
+
+ - name: Install ARM64 DEB packages
+ shell: bash
+ run: |
+ test "$(dpkg --print-architecture)" = arm64
+ apt-get update
+ apt-get install -y ./packages/*.deb cmake build-essential python3 pkg-config git curl ca-certificates
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Verify installed ARM64 DEB packages
+ shell: bash
+ run: |
+ cli_path="$(readlink -f -- "$(command -v tsfile-cli)")"
+ cli_owner="$(dpkg-query -S "$cli_path")"
+ cli_owner="${cli_owner%%:*}"
+ [[ "$cli_owner" == tsfile-tools ]]
+ tsfile-cli --version
+ dpkg-query -W tsfile tsfile-dev tsfile-tools
+
+ - name: Build and run an installed-package consumer
+ shell: bash
+ run: |
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build
+ cmake --build consumer/build --parallel
+ ./consumer/build/tsfile_installed_consumer
+
+ - name: Test the installed CLI with Python test data
+ shell: bash
+ run: |
+ python3 packaging/tests/cli_package_smoke.py \
+ --cli "$(command -v tsfile-cli)" --resources cli-test-data
+
+ - name: Compile and run C++ regressions against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/cpp_installed_tests.py \
+ --prefix /usr --build-dir installed-tests
+
+ - name: Run Go tests against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/go_installed_tests.py \
+ --prefix /usr --goarch arm64
+
+ - name: Download the standalone SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-ubuntu22.04-arm64
+ path: sdk-download
+
+ - name: Test the downloaded and relocated SDK archive
+ shell: bash
+ run: |
+ python3 packaging/tests/sdk_archive_tests.py \
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests \
+ --resources cli-test-data --goarch arm64 \
+ --gtest-prefix installed-tests/dependencies
+
+ - name: Upload C++ installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cpp-tests-${{ github.job }}-${{ runner.arch }}-${{ strategy.job-index }}
+ path: |
+ installed-tests/cpp-results.xml
+ installed-tests/tests/Testing/Temporary/LastTest.log
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ build-rpm:
+ name: Build AlmaLinux 9 C++ SDK and RPM packages
+ needs: prepare
+ runs-on: ubuntu-24.04
+ container: almalinux:9
+ timeout-minutes: 45
+ steps:
+ - name: Install RPM build prerequisites
+ run: |
+ dnf install -y \
+ java-17-openjdk-devel \
+ cmake \
+ gcc-c++ \
+ make \
+ pkgconf-pkg-config \
+ rpm-build \
+ git \
+ curl-minimal \
+ tar \
+ gzip \
+ libuuid-devel
+
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Build C++ core and CPack metadata with Maven
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ DEBIAN_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: bash
+ run: |
+ ./mvnw -Pwith-cpp package \
+ -Dbuild.type=Release \
+ -Dbuild.test=OFF \
+ -DskipTests \
+ -Dspotless.skip=true \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Dtsfile.version.sync.skip=true \
+ -Denable.cpack=ON \
+ -Dtsfile.archive.version="$ARCHIVE_VERSION" \
+ -Dtsfile.debian.package.version="$DEBIAN_VERSION" \
+ -Dtsfile.rpm.package.version="$RPM_VERSION" \
+ -Dtsfile.rpm.package.release="$RPM_RELEASE"
+ test -f cpp/target/build/CPackConfig.cmake
+
+ - name: Stage and verify the AlmaLinux SDK
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: bash
+ run: |
+ sdk_name="tsfile-sdk-almalinux9-x86_64-$ARCHIVE_VERSION"
+ sdk_root="$PWD/sdk/$sdk_name"
+ cmake --install cpp/target/build --prefix "$sdk_root"
+ test -f "$sdk_root/bin/tsfile-cli"
+ test -d "$sdk_root/include"
+ test -n "$(find "$sdk_root" -maxdepth 2 -name 'libtsfile.so*' -print -quit)"
+ sdk_pkgconfig="$(find "$sdk_root" -type f -name libtsfile.pc -print -quit)"
+ test -n "$sdk_pkgconfig"
+ "$sdk_root/bin/tsfile-cli" --version
+ PKG_CONFIG_PATH="$(dirname "$sdk_pkgconfig")" pkg-config --cflags --libs libtsfile
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/rpm \
+ -DCMAKE_PREFIX_PATH="$sdk_root"
+ cmake --build consumer/rpm --parallel
+ ./consumer/rpm/tsfile_installed_consumer
+ tar -C sdk -czf "$sdk_root.tar.gz" "$sdk_name"
+ rm -rf "$sdk_root"
+
+ - name: Build and verify RPM packages
+ env:
+ EXPECTED_RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ EXPECTED_RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: bash
+ run: |
+ mkdir -p packages
+ cpack -G RPM --config cpp/target/build/CPackConfig.cmake -B packages
+
+ mapfile -t packages < <(find packages -maxdepth 1 -type f -name '*.rpm' -print | sort)
+ if [[ ${#packages[@]} -ne 3 ]]; then
+ printf 'Expected exactly 3 RPM packages, found %s\n' "${#packages[@]}"
+ exit 1
+ fi
+
+ actual_names="$({
+ for package in "${packages[@]}"; do
+ rpm -qp --queryformat '%{NAME}\n' "$package"
+ done
+ } | sort)"
+ expected_names="$(printf '%s\n' tsfile tsfile-devel tsfile-tools | sort)"
+ if [[ "$actual_names" != "$expected_names" ]]; then
+ printf 'Unexpected RPM package names:\n%s\n' "$actual_names"
+ exit 1
+ fi
+
+ for package in "${packages[@]}"; do
+ package_version="$(rpm -qp --queryformat '%{VERSION}' "$package")"
+ package_release="$(rpm -qp --queryformat '%{RELEASE}' "$package")"
+ if [[ "$package_version" != "$EXPECTED_RPM_VERSION" ]]; then
+ printf '%s has version %s, expected %s\n' \
+ "$package" "$package_version" "$EXPECTED_RPM_VERSION"
+ exit 1
+ fi
+ if [[ "$package_release" != "$EXPECTED_RPM_RELEASE" ]]; then
+ printf '%s has release %s, expected %s\n' \
+ "$package" "$package_release" "$EXPECTED_RPM_RELEASE"
+ exit 1
+ fi
+ done
+
+ - name: Upload AlmaLinux SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-almalinux9-x86_64
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload RPM packages
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-rpm-almalinux9-x86_64
+ path: packages/*.rpm
+ if-no-files-found: error
+ retention-days: 14
+
+ test-rpm:
+ name: Test RPM packages on AlmaLinux 9
+ needs: build-rpm
+ runs-on: ubuntu-24.04
+ container: almalinux:9
+ timeout-minutes: 45
+ steps:
+ - name: Checkout installation test sources
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ cpp/test
+ cpp/cmake/tests/projects/InstalledConsumer
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Download RPM packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-rpm-almalinux9-x86_64
+ path: packages
+
+ - name: Install RPM packages
+ run: dnf install -y packages/*.rpm cmake gcc-c++ python3 pkgconf-pkg-config git curl-minimal tar gzip
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Verify installed RPM packages
+ shell: bash
+ run: |
+ cli_path="$(command -v tsfile-cli)"
+ cli_path="$(readlink -f -- "$cli_path")"
+ cli_owner="$(rpm -qf --queryformat '%{NAME}' "$cli_path")"
+ if [[ "$cli_owner" != "tsfile-tools" ]]; then
+ printf '%s is owned by %s, expected tsfile-tools\n' "$cli_path" "$cli_owner"
+ exit 1
+ fi
+ tsfile-cli --version
+ rpm -q tsfile tsfile-devel tsfile-tools
+
+ - name: Build and run an installed-package consumer
+ shell: bash
+ run: |
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build
+ cmake --build consumer/build --parallel
+ ./consumer/build/tsfile_installed_consumer
+
+ - name: Test the installed CLI with Python test data
+ shell: bash
+ run: |
+ python3 packaging/tests/cli_package_smoke.py \
+ --cli "$(command -v tsfile-cli)" --resources cli-test-data
+
+ - name: Compile and run C++ regressions against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/cpp_installed_tests.py \
+ --prefix /usr --build-dir installed-tests
+
+ - name: Run Go tests against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/go_installed_tests.py \
+ --prefix /usr --goarch amd64
+
+ - name: Download the standalone SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-almalinux9-x86_64
+ path: sdk-download
+
+ - name: Test the downloaded and relocated SDK archive
+ shell: bash
+ run: |
+ python3 packaging/tests/sdk_archive_tests.py \
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests \
+ --resources cli-test-data --goarch amd64 \
+ --gtest-prefix installed-tests/dependencies
+
+ - name: Upload C++ installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cpp-tests-${{ github.job }}-${{ runner.arch }}-${{ strategy.job-index }}
+ path: |
+ installed-tests/cpp-results.xml
+ installed-tests/tests/Testing/Temporary/LastTest.log
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ build-rpm-aarch64:
+ name: Build AlmaLinux 9 aarch64 C++ SDK and RPM packages
+ needs: prepare
+ runs-on: ubuntu-24.04-arm
+ container: almalinux:9
+ timeout-minutes: 45
+ steps:
+ - name: Install RPM build prerequisites
+ run: |
+ test "$(uname -m)" = aarch64
+ dnf install -y \
+ java-17-openjdk-devel \
+ cmake \
+ gcc-c++ \
+ make \
+ pkgconf-pkg-config \
+ rpm-build \
+ git \
+ curl-minimal \
+ tar \
+ gzip \
+ libuuid-devel
+
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Build C++ core and CPack metadata with Maven
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ DEBIAN_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: bash
+ run: |
+ ./mvnw -Pwith-cpp package \
+ -Dbuild.type=Release \
+ -Dbuild.test=OFF \
+ -DskipTests \
+ -Dspotless.skip=true \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Dtsfile.version.sync.skip=true \
+ -Denable.cpack=ON \
+ -Dtsfile.archive.version="$ARCHIVE_VERSION" \
+ -Dtsfile.debian.package.version="$DEBIAN_VERSION" \
+ -Dtsfile.rpm.package.version="$RPM_VERSION" \
+ -Dtsfile.rpm.package.release="$RPM_RELEASE"
+ test -f cpp/target/build/CPackConfig.cmake
+
+ - name: Stage and verify the AlmaLinux aarch64 SDK
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: bash
+ run: |
+ sdk_name="tsfile-sdk-almalinux9-aarch64-$ARCHIVE_VERSION"
+ sdk_root="$PWD/sdk/$sdk_name"
+ cmake --install cpp/target/build --prefix "$sdk_root"
+ test -f "$sdk_root/bin/tsfile-cli"
+ test -d "$sdk_root/include"
+ test -n "$(find "$sdk_root" -maxdepth 2 -name 'libtsfile.so*' -print -quit)"
+ sdk_pkgconfig="$(find "$sdk_root" -type f -name libtsfile.pc -print -quit)"
+ test -n "$sdk_pkgconfig"
+ "$sdk_root/bin/tsfile-cli" --version
+ PKG_CONFIG_PATH="$(dirname "$sdk_pkgconfig")" pkg-config --cflags --libs libtsfile
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/rpm-aarch64 \
+ -DCMAKE_PREFIX_PATH="$sdk_root"
+ cmake --build consumer/rpm-aarch64 --parallel
+ ./consumer/rpm-aarch64/tsfile_installed_consumer
+ tar -C sdk -czf "$sdk_root.tar.gz" "$sdk_name"
+ rm -rf "$sdk_root"
+
+ - name: Build and verify aarch64 RPM packages
+ env:
+ EXPECTED_RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ EXPECTED_RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: bash
+ run: |
+ mkdir -p packages
+ cpack -G RPM --config cpp/target/build/CPackConfig.cmake -B packages
+ mapfile -t packages < <(find packages -maxdepth 1 -type f -name '*.rpm' -print | sort)
+ [[ ${#packages[@]} -eq 3 ]]
+ actual_names="$({ for package in "${packages[@]}"; do rpm -qp --queryformat '%{NAME}\n' "$package"; done; } | sort)"
+ expected_names="$(printf '%s\n' tsfile tsfile-devel tsfile-tools | sort)"
+ [[ "$actual_names" == "$expected_names" ]]
+ for package in "${packages[@]}"; do
+ [[ "$(rpm -qp --queryformat '%{ARCH}' "$package")" == aarch64 ]]
+ [[ "$(rpm -qp --queryformat '%{VERSION}' "$package")" == "$EXPECTED_RPM_VERSION" ]]
+ [[ "$(rpm -qp --queryformat '%{RELEASE}' "$package")" == "$EXPECTED_RPM_RELEASE" ]]
+ done
+
+ - name: Upload AlmaLinux aarch64 SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-almalinux9-aarch64
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload aarch64 RPM packages
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-rpm-almalinux9-aarch64
+ path: packages/*.rpm
+ if-no-files-found: error
+ retention-days: 14
+
+ test-rpm-aarch64:
+ name: Test RPM packages on AlmaLinux 9 aarch64
+ needs: build-rpm-aarch64
+ runs-on: ubuntu-24.04-arm
+ container: almalinux:9
+ timeout-minutes: 45
+ steps:
+ - name: Checkout installation test sources
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ cpp/test
+ cpp/cmake/tests/projects/InstalledConsumer
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Download aarch64 RPM packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-rpm-almalinux9-aarch64
+ path: packages
+
+ - name: Install aarch64 RPM packages
+ run: |
+ test "$(uname -m)" = aarch64
+ dnf install -y packages/*.rpm cmake gcc-c++ python3 pkgconf-pkg-config git curl-minimal tar gzip
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Verify installed aarch64 RPM packages
+ shell: bash
+ run: |
+ cli_path="$(readlink -f -- "$(command -v tsfile-cli)")"
+ cli_owner="$(rpm -qf --queryformat '%{NAME}' "$cli_path")"
+ [[ "$cli_owner" == tsfile-tools ]]
+ tsfile-cli --version
+ rpm -q tsfile tsfile-devel tsfile-tools
+
+ - name: Build and run an installed-package consumer
+ shell: bash
+ run: |
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/build
+ cmake --build consumer/build --parallel
+ ./consumer/build/tsfile_installed_consumer
+
+ - name: Test the installed CLI with Python test data
+ shell: bash
+ run: |
+ python3 packaging/tests/cli_package_smoke.py \
+ --cli "$(command -v tsfile-cli)" --resources cli-test-data
+
+ - name: Compile and run C++ regressions against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/cpp_installed_tests.py \
+ --prefix /usr --build-dir installed-tests
+
+ - name: Run Go tests against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/go_installed_tests.py \
+ --prefix /usr --goarch arm64
+
+ - name: Download the standalone SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-almalinux9-aarch64
+ path: sdk-download
+
+ - name: Test the downloaded and relocated SDK archive
+ shell: bash
+ run: |
+ python3 packaging/tests/sdk_archive_tests.py \
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests \
+ --resources cli-test-data --goarch arm64 \
+ --gtest-prefix installed-tests/dependencies
+
+ - name: Upload C++ installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cpp-tests-${{ github.job }}-${{ runner.arch }}-${{ strategy.job-index }}
+ path: |
+ installed-tests/cpp-results.xml
+ installed-tests/tests/Testing/Temporary/LastTest.log
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ build-windows:
+ name: Build Windows MSVC C++ SDK and ZIP
+ needs: prepare
+ runs-on: windows-2022
+ timeout-minutes: 50
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up Java 17
+ uses: actions/setup-java@v6.0.0
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Configure the MSVC environment
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: x64
+
+ - name: Build C++ core and CPack metadata with Maven
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ DEBIAN_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $projectInclude = (Resolve-Path cpp/cmake/tests/CheckStaticMSVCRuntime.cmake).Path
+ .\mvnw.cmd -Pwith-cpp package `
+ "-Dcpp.toolchain=msvc" `
+ "-Dbuild.type=Release" `
+ "-Dbuild.test=OFF" `
+ "-DskipTests" `
+ "-Dspotless.skip=true" `
+ "-Dtsfile.dependency.source=BUNDLED" `
+ "-Dtsfile.msvc.static.runtime=ON" `
+ "-Denable.lzma2=ON" `
+ "-Dtsfile.project.include=$projectInclude" `
+ "-Dtsfile.version.sync.skip=true" `
+ "-Denable.cpack=ON" `
+ "-Dtsfile.archive.version=$env:ARCHIVE_VERSION" `
+ "-Dtsfile.debian.package.version=$env:DEBIAN_VERSION" `
+ "-Dtsfile.rpm.package.version=$env:RPM_VERSION" `
+ "-Dtsfile.rpm.package.release=$env:RPM_RELEASE"
+ if (-not (Test-Path -LiteralPath cpp/target/build/CPackConfig.cmake -PathType Leaf)) {
+ throw 'Maven did not generate cpp/target/build/CPackConfig.cmake'
+ }
+
+ - name: Stage and verify the Windows SDK
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $sdkName = "tsfile-sdk-windows-msvc-x86_64-$env:ARCHIVE_VERSION"
+ $sdkRoot = Join-Path $PWD "sdk/$sdkName"
+ cmake --install cpp/target/build --config Release --prefix $sdkRoot
+ $requiredPaths = @(
+ "$sdkRoot/bin/tsfile-cli.exe"
+ "$sdkRoot/bin/tsfile.dll"
+ "$sdkRoot/lib/tsfile.lib"
+ "$sdkRoot/include/tsfile/cwrapper/tsfile_cwrapper.h"
+ "$sdkRoot/lib/cmake/TsFile/TsFileConfig.cmake"
+ "$sdkRoot/share/doc/tsfile/LICENSE"
+ "$sdkRoot/share/doc/tsfile/NOTICE"
+ )
+ foreach ($path in $requiredPaths) {
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
+ throw "Required SDK file is absent: $path"
+ }
+ }
+ python packaging/scripts/verify_windows_runtime.py $sdkRoot
+ $env:PATH = "$sdkRoot/bin;$env:PATH"
+ & "$sdkRoot/bin/tsfile-cli.exe" --version
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/windows `
+ -G "Visual Studio 17 2022" -A x64 `
+ -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded `
+ "-DCMAKE_PREFIX_PATH=$sdkRoot"
+ cmake --build consumer/windows --config Release --parallel
+ & consumer/windows/Release/tsfile_installed_consumer.exe
+ tar.exe -czf "$sdkRoot.tar.gz" -C sdk $sdkName
+ Remove-Item -LiteralPath $sdkRoot -Recurse -Force
+
+ - name: Build and inspect combined ZIP
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $cpackConfig = Get-Content cpp/target/build/CPackConfig.cmake -Raw
+ if ($cpackConfig -notmatch 'set\(CPACK_ARCHIVE_COMPONENT_INSTALL "OFF"\)') {
+ throw 'CPACK_ARCHIVE_COMPONENT_INSTALL must remain disabled'
+ }
+
+ New-Item -ItemType Directory -Path packages -Force | Out-Null
+ cpack -G ZIP -C Release --config cpp/target/build/CPackConfig.cmake -B packages `
+ -D "CPACK_PACKAGE_VERSION=$env:ARCHIVE_VERSION"
+
+ $archives = @(Get-ChildItem -Path packages -Filter *.zip -File)
+ if ($archives.Count -ne 1) {
+ throw "Expected exactly one combined ZIP, found $($archives.Count)"
+ }
+ $expectedName = "tsfile-$env:ARCHIVE_VERSION-windows-x86_64.zip"
+ if ($archives[0].Name -cne $expectedName) {
+ throw "Unexpected ZIP name '$($archives[0].Name)'; expected '$expectedName'"
+ }
+
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $archive = [System.IO.Compression.ZipFile]::OpenRead($archives[0].FullName)
+ try {
+ $entryPaths = @($archive.Entries | ForEach-Object {
+ $_.FullName.Replace('\', '/')
+ })
+ $requiredSuffixes = @(
+ 'bin/tsfile-cli.exe'
+ 'bin/tsfile.dll'
+ 'lib/tsfile.lib'
+ 'include/tsfile/cwrapper/tsfile_cwrapper.h'
+ )
+ foreach ($suffix in $requiredSuffixes) {
+ $match = $entryPaths | Where-Object {
+ $_.EndsWith($suffix, [System.StringComparison]::Ordinal)
+ }
+ if (-not $match) {
+ throw "ZIP is missing a path ending in '$suffix'"
+ }
+ }
+ } finally {
+ $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 SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-windows-msvc-x86_64
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload Windows ZIP
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-windows-msvc-x86_64
+ path: packages/*.zip
+ if-no-files-found: error
+ retention-days: 14
+
+ build-windows-arm64:
+ name: Build Windows MSVC ARM64 C++ SDK and ZIP
+ needs: prepare
+ runs-on: windows-11-vs2026-arm
+ timeout-minutes: 50
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Select the native ARM64 Java runtime
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ if (-not $env:JAVA_HOME_21_AARCH64) {
+ throw 'The native ARM64 Java runtime is missing from the runner image'
+ }
+ "JAVA_HOME=$env:JAVA_HOME_21_AARCH64" >> $env:GITHUB_ENV
+ "$env:JAVA_HOME_21_AARCH64\bin" >> $env:GITHUB_PATH
+ java -version
+
+ - name: Configure the native ARM64 MSVC environment
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: arm64
+
+ - name: Verify native ARM64 MSVC compiler
+ shell: pwsh
+ run: |
+ $compiler = (Get-Command cl.exe).Source
+ Write-Host "Native compiler: $compiler"
+ if ($compiler -notmatch 'HostARM64\\ARM64\\cl\.exe$') {
+ throw "Expected the ARM64-hosted ARM64 compiler, got $compiler"
+ }
+
+ - name: Build C++ core and CPack metadata with Maven
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ DEBIAN_VERSION: ${{ needs.prepare.outputs.deb_version }}
+ RPM_VERSION: ${{ needs.prepare.outputs.rpm_version }}
+ RPM_RELEASE: ${{ needs.prepare.outputs.rpm_release }}
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $projectInclude = (Resolve-Path cpp/cmake/tests/CheckStaticMSVCRuntime.cmake).Path
+ .\mvnw.cmd -Pwith-cpp package `
+ "-Dcpp.toolchain=msvc-arm64-native" `
+ "-Dbuild.type=Release" `
+ "-Dbuild.test=OFF" `
+ "-DskipTests" `
+ "-Dspotless.skip=true" `
+ "-Dtsfile.dependency.source=BUNDLED" `
+ "-Dtsfile.msvc.static.runtime=ON" `
+ "-Denable.lzma2=ON" `
+ "-Dtsfile.project.include=$projectInclude" `
+ "-Dtsfile.version.sync.skip=true" `
+ "-Denable.cpack=ON" `
+ "-Dtsfile.archive.version=$env:ARCHIVE_VERSION" `
+ "-Dtsfile.debian.package.version=$env:DEBIAN_VERSION" `
+ "-Dtsfile.rpm.package.version=$env:RPM_VERSION" `
+ "-Dtsfile.rpm.package.release=$env:RPM_RELEASE"
+ if (-not (Test-Path -LiteralPath cpp/target/build/CPackConfig.cmake -PathType Leaf)) {
+ throw 'Maven did not generate cpp/target/build/CPackConfig.cmake'
+ }
+
+ - name: Stage and verify the Windows ARM64 SDK
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $sdkName = "tsfile-sdk-windows-msvc-arm64-$env:ARCHIVE_VERSION"
+ $sdkRoot = Join-Path $PWD "sdk/$sdkName"
+ cmake --install cpp/target/build --config Release --prefix $sdkRoot
+ $requiredPaths = @(
+ "$sdkRoot/bin/tsfile-cli.exe"
+ "$sdkRoot/bin/tsfile.dll"
+ "$sdkRoot/lib/tsfile.lib"
+ "$sdkRoot/include/tsfile/cwrapper/tsfile_cwrapper.h"
+ "$sdkRoot/lib/cmake/TsFile/TsFileConfig.cmake"
+ "$sdkRoot/share/doc/tsfile/LICENSE"
+ "$sdkRoot/share/doc/tsfile/NOTICE"
+ )
+ foreach ($path in $requiredPaths) {
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
+ throw "Required ARM64 SDK file is absent: $path"
+ }
+ }
+ python packaging/scripts/verify_windows_runtime.py $sdkRoot
+ $env:PATH = "$sdkRoot/bin;$env:PATH"
+ & "$sdkRoot/bin/tsfile-cli.exe" --version
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/windows-arm64 `
+ -G "Ninja Multi-Config" `
+ -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded `
+ "-DCMAKE_PREFIX_PATH=$sdkRoot"
+ cmake --build consumer/windows-arm64 --config Release --parallel
+ & consumer/windows-arm64/Release/tsfile_installed_consumer.exe
+ tar.exe -czf "$sdkRoot.tar.gz" -C sdk $sdkName
+ Remove-Item -LiteralPath $sdkRoot -Recurse -Force
+
+ - name: Build and inspect the Windows ARM64 ZIP
+ env:
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $cpackConfig = Get-Content cpp/target/build/CPackConfig.cmake -Raw
+ if ($cpackConfig -notmatch 'set\(CPACK_ARCHIVE_COMPONENT_INSTALL "OFF"\)') {
+ throw 'CPACK_ARCHIVE_COMPONENT_INSTALL must remain disabled'
+ }
+ New-Item -ItemType Directory -Path packages -Force | Out-Null
+ cpack -G ZIP -C Release --config cpp/target/build/CPackConfig.cmake -B packages `
+ -D "CPACK_PACKAGE_VERSION=$env:ARCHIVE_VERSION" `
+ -D "CPACK_PACKAGE_FILE_NAME=tsfile-$env:ARCHIVE_VERSION-windows-arm64"
+ $archives = @(Get-ChildItem -Path packages -Filter *.zip -File)
+ if ($archives.Count -ne 1) { throw "Expected exactly one ARM64 ZIP, found $($archives.Count)" }
+ $expectedName = "tsfile-$env:ARCHIVE_VERSION-windows-arm64.zip"
+ if ($archives[0].Name -cne $expectedName) {
+ throw "Unexpected ZIP name '$($archives[0].Name)'; expected '$expectedName'"
+ }
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $archive = [System.IO.Compression.ZipFile]::OpenRead($archives[0].FullName)
+ try {
+ $entryPaths = @($archive.Entries | ForEach-Object { $_.FullName.Replace('\', '/') })
+ foreach ($suffix in @(
+ 'bin/tsfile-cli.exe'
+ 'bin/tsfile.dll'
+ 'lib/tsfile.lib'
+ 'include/tsfile/cwrapper/tsfile_cwrapper.h'
+ )) {
+ if (-not ($entryPaths | Where-Object { $_.EndsWith($suffix, [System.StringComparison]::Ordinal) })) {
+ throw "ARM64 ZIP is missing a path ending in '$suffix'"
+ }
+ }
+ } finally { $archive.Dispose() }
+ Expand-Archive -LiteralPath $archives[0].FullName -DestinationPath extracted/windows-arm64
+ python packaging/scripts/verify_windows_runtime.py extracted/windows-arm64
+ $zipCli = @(Get-ChildItem extracted/windows-arm64 -Recurse -Filter tsfile-cli.exe -File)
+ if ($zipCli.Count -ne 1) { throw 'Expected exactly one ARM64 CLI' }
+ & $zipCli[0].FullName --version
+
+ - name: Upload Windows ARM64 SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-windows-msvc-arm64
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload Windows ARM64 ZIP
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-windows-msvc-arm64
+ path: packages/*.zip
+ if-no-files-found: error
+ retention-days: 14
+
+ build-python-unix:
+ name: Build Python wheels on ${{ matrix.name }}
+ needs: prepare
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 90
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: manylinux_2_28-x86_64
+ os: ubuntu-22.04
+ arch: x86_64
+ - name: manylinux_2_28-aarch64
+ os: ubuntu-22.04-arm
+ arch: aarch64
+ - name: macos12-x86_64
+ os: macos-15-intel
+ arch: x86_64
+ - name: macos12-arm64
+ os: macos-latest
+ arch: arm64
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v7
+ with:
+ python-version: "3.11"
+
+ - name: Set up Java 17 for macOS
+ if: runner.os == 'macOS'
+ uses: actions/setup-java@v6.0.0
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Install macOS build prerequisites
+ if: runner.os == 'macOS'
+ run: |
+ command -v cmake > /dev/null || brew install cmake
+ command -v pkg-config > /dev/null || brew install pkgconf
+
+ - name: Install wheel build tools
+ run: python -m pip install cibuildwheel==3.4.1
+
+ - name: Build wheels from one SDK per compatibility baseline
+ env:
+ CIBW_ARCHS: ${{ matrix.arch }}
+ CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*"
+ CIBW_SKIP: "pp* *-musllinux*"
+ CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
+ CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
+ CIBW_BEFORE_ALL_LINUX: >-
+ bash packaging/scripts/build_python_sdk.sh
+ ${{ matrix.name }} ${{ needs.prepare.outputs.archive_version }}
+ CIBW_BEFORE_ALL_MACOS: >-
+ bash packaging/scripts/build_python_sdk.sh
+ ${{ matrix.name }} ${{ needs.prepare.outputs.archive_version }}
+ CIBW_ENVIRONMENT_LINUX: >-
+ TSFILE_CPP_BUILD=/project/python-sdk
+ TSFILE_PYTHON_SDK_EXPORT="/host${{ github.workspace }}/python-sdk-output"
+ CIBW_ENVIRONMENT_MACOS: >-
+ TSFILE_CPP_BUILD="${{ github.workspace }}/python-sdk"
+ MACOSX_DEPLOYMENT_TARGET=12.0
+ MACOSX_DEPLOYMENT_TARGET: "12.0"
+ CIBW_TEST_SKIP: "*"
+ CIBW_BUILD_VERBOSITY: "1"
+ run: python -m cibuildwheel --output-dir wheelhouse python
+
+ - name: Upload the Python compatibility SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-${{ matrix.name }}
+ path: python-sdk-output/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Check runtime-only wheel contents
+ run: python packaging/scripts/check_python_wheel.py --wheelhouse wheelhouse
+
+ - name: Upload the Python wheels
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-python-wheel-${{ matrix.name }}
+ path: wheelhouse/*.whl
+ if-no-files-found: error
+ retention-days: 14
+
+ build-python-windows:
+ name: Build Python wheels from the Windows MSVC SDK
+ needs: build-windows
+ runs-on: windows-2022
+ timeout-minutes: 60
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up Python
+ uses: actions/setup-python@v7
+ with:
+ python-version: "3.11"
+
+ - name: Configure the MSVC environment
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: x64
+
+ - name: Download the Windows SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-windows-msvc-x86_64
+ path: sdk-download
+
+ - name: Prepare the SDK for the Python build
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $archives = @(Get-ChildItem sdk-download -Filter *.tar.gz -File)
+ if ($archives.Count -ne 1) { throw 'Expected one Windows SDK archive' }
+ New-Item -ItemType Directory -Path sdk | Out-Null
+ tar.exe -xzf $archives[0].FullName -C sdk
+ $roots = @(Get-ChildItem sdk -Directory)
+ if ($roots.Count -ne 1) { throw 'Expected one Windows SDK prefix' }
+ $sdkRoot = $roots[0].FullName
+ # setup.py consumes libraries from lib/; the installed SDK stores DLLs in bin/.
+ Copy-Item "$sdkRoot/bin/*.dll" "$sdkRoot/lib/"
+ if (-not (Test-Path "$sdkRoot/lib/tsfile.lib")) { throw 'Missing MSVC import library' }
+ if (-not (Test-Path "$sdkRoot/lib/tsfile.dll")) { throw 'Missing TsFile DLL' }
+ "TSFILE_CPP_BUILD=$sdkRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+
+ - name: Install wheel build tools
+ run: python -m pip install cibuildwheel==3.4.1
+
+ - name: Build wheels using the installed SDK
+ env:
+ CIBW_ARCHS_WINDOWS: AMD64
+ CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*"
+ CIBW_TEST_SKIP: "*"
+ CIBW_BUILD_VERBOSITY: "1"
+ run: python -m cibuildwheel --output-dir wheelhouse python
+
+ - name: Check runtime-only wheel contents
+ run: python packaging/scripts/check_python_wheel.py --wheelhouse wheelhouse
+
+ - name: Upload the Python wheels
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-python-wheel-windows-msvc-x86_64
+ path: wheelhouse/*.whl
+ if-no-files-found: error
+ retention-days: 14
+
+ build-homebrew:
+ name: Build Homebrew bottle on ${{ matrix.name }}
+ needs: prepare
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: macos-arm64
+ os: macos-latest
+ - name: macos-x86_64
+ os: macos-15-intel
+ env:
+ HOMEBREW_NO_AUTO_UPDATE: "1"
+ HOMEBREW_NO_INSTALL_CLEANUP: "1"
+ ARCHIVE_VERSION: ${{ needs.prepare.outputs.archive_version }}
+ TSFILE_HOMEBREW_VERSION: ${{ needs.prepare.outputs.homebrew_version }}
+ SOURCE_REPOSITORY: ${{ github.repository }}
+ SOURCE_GIT_SHA: ${{ github.sha }}
+ BOTTLE_ROOT_URL: https://packages.apache.org/artifactory/tsfile/homebrew/release/versions/${{ needs.prepare.outputs.homebrew_version }}/bottles
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Render immutable source Formula in a temporary local tap
+ shell: bash
+ run: |
+ mkdir -p homebrew/Formula homebrew/bottles
+ curl --fail --location --retry 3 \
+ "https://github.com/$SOURCE_REPOSITORY/archive/$SOURCE_GIT_SHA.tar.gz" \
+ --output "$RUNNER_TEMP/tsfile-source.tar.gz"
+ source_sha256="$(shasum -a 256 "$RUNNER_TEMP/tsfile-source.tar.gz" | awk '{print $1}')"
+ python3 packaging/scripts/render_homebrew_formula.py \
+ --template packaging/homebrew/tsfile.rb.in \
+ --repository "$SOURCE_REPOSITORY" \
+ --git-sha "$SOURCE_GIT_SHA" \
+ --version "$TSFILE_HOMEBREW_VERSION" \
+ --source-sha256 "$source_sha256" \
+ --output homebrew/Formula/tsfile.rb
+ ruby -c homebrew/Formula/tsfile.rb
+ brew tap-new apache/tsfile
+ tap_path="$(brew --repository apache/tsfile)"
+ cp homebrew/Formula/tsfile.rb "$tap_path/Formula/tsfile.rb"
+
+ - name: Build, test, and bottle the release Formula
+ shell: bash
+ run: |
+ brew install --build-bottle apache/tsfile/tsfile
+ python3 packaging/scripts/verify_macos_runtime.py "$(brew --prefix apache/tsfile/tsfile | tail -n 1)"
+ brew test apache/tsfile/tsfile
+ cd homebrew/bottles
+ brew bottle --json --root-url "$BOTTLE_ROOT_URL" apache/tsfile/tsfile
+ shopt -s nullglob
+ bottles=(*.bottle*.tar.gz)
+ metadata=(*.bottle.json)
+ [[ ${#bottles[@]} -eq 1 && ${#metadata[@]} -eq 1 ]]
+
+ - name: Stage and verify the macOS SDK
+ shell: bash
+ run: |
+ # The runner's /bin/bash is 3.2, where "set -u" rejects empty arrays.
+ set -eo pipefail
+ fail() { printf '::error::%s\n' "$1"; exit 1; }
+ sdk_name="tsfile-sdk-${{ matrix.name }}-$ARCHIVE_VERSION"
+ # Homebrew may print advisory lines before the path, so keep the last line.
+ formula_prefix="$(brew --prefix apache/tsfile/tsfile | tail -n 1)"
+ [ -n "$formula_prefix" ] || fail "brew --prefix returned no formula path"
+ printf 'formula prefix: <%s>\n' "$formula_prefix"
+ keg_root="$(cd "$formula_prefix" && pwd -P)"
+ printf 'resolved keg: <%s>\n' "$keg_root"
+ command -v pkg-config > /dev/null 2>&1 || brew install pkgconf
+ command -v pkg-config > /dev/null 2>&1 || fail "pkg-config is unavailable on the runner"
+ shopt -s nullglob
+ dylibs=("$keg_root"/lib/libtsfile*.dylib)
+ pkgconfigs=("$keg_root"/lib/pkgconfig/libtsfile.pc)
+ [ -d "$keg_root/include" ] || fail "missing include/ under $keg_root"
+ [ ${#dylibs[@]} -gt 0 ] || fail "missing libtsfile*.dylib under $keg_root/lib"
+ [ -x "$keg_root/bin/tsfile-cli" ] || fail "missing bin/tsfile-cli under $keg_root"
+ [ -f "$keg_root/lib/cmake/TsFile/TsFileConfig.cmake" ] ||
+ fail "missing TsFileConfig.cmake under $keg_root/lib/cmake/TsFile"
+ [ ${#pkgconfigs[@]} -gt 0 ] || fail "missing libtsfile.pc under $keg_root/lib/pkgconfig"
+
+ # Stage a real tree: the keg lives at a symlinked opt path and must not
+ # be archived as a single link.
+ sdk_root="$PWD/sdk/$sdk_name"
+ rm -rf "$sdk_root"
+ mkdir -p "$sdk_root"
+ ditto "$keg_root" "$sdk_root"
+
+ # Drop Homebrew keg bookkeeping so the archive holds the same SDK
+ # layout as the Linux and Windows archives.
+ rm -rf "$sdk_root/.brew" "$sdk_root/INSTALL_RECEIPT.json" "$sdk_root/sbom.spdx.json"
+
+ # A keg records its own absolute path as the dylib identity, which
+ # would pin every downstream consumer to this runner's keg.
+ real_dylib=""
+ for candidate in "$sdk_root"/lib/libtsfile*.dylib; do
+ [ -L "$candidate" ] || real_dylib="$candidate"
+ done
+ [ -n "$real_dylib" ] || fail "missing real libtsfile dylib under $sdk_root/lib"
+ dylib_id="$(basename "$(otool -D "$real_dylib" | sed -n '2p')")"
+ [ -n "$dylib_id" ] || fail "cannot read the dylib identity from $real_dylib"
+ install_name_tool -id "@rpath/$dylib_id" "$real_dylib"
+ codesign --force --sign - "$real_dylib"
+ [ "$(otool -D "$real_dylib" | sed -n '2p')" = "@rpath/$dylib_id" ] ||
+ fail "dylib identity was not normalized to @rpath"
+
+ python3 packaging/scripts/verify_macos_runtime.py "$sdk_root"
+ "$sdk_root/bin/tsfile-cli" --version
+ PKG_CONFIG_PATH="$sdk_root/lib/pkgconfig" pkg-config --cflags --libs libtsfile
+ cmake -S cpp/cmake/tests/projects/InstalledConsumer -B consumer/macos \
+ -DCMAKE_PREFIX_PATH="$sdk_root"
+ cmake --build consumer/macos --parallel
+ ./consumer/macos/tsfile_installed_consumer
+ tar -C sdk -czf "sdk/$sdk_name.tar.gz" "$sdk_name"
+ rm -rf "$sdk_root"
+
+ - name: Upload platform Homebrew bottle and source Formula
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-homebrew-${{ matrix.name }}
+ path: homebrew/
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload macOS SDK
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-${{ matrix.name }}
+ path: sdk/*.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ merge-homebrew:
+ name: Merge Homebrew release bottles
+ needs: [prepare, build-homebrew]
+ runs-on: macos-latest
+ timeout-minutes: 15
+ env:
+ HOMEBREW_NO_AUTO_UPDATE: "1"
+ TSFILE_HOMEBREW_VERSION: ${{ needs.prepare.outputs.homebrew_version }}
+ BOTTLE_ROOT_URL: https://packages.apache.org/artifactory/tsfile/homebrew/release/versions/${{ needs.prepare.outputs.homebrew_version }}/bottles
+ steps:
+ - name: Download both platform Homebrew artifacts separately
+ uses: actions/download-artifact@v8
+ with:
+ pattern: native-homebrew-macos-*
+ path: homebrew-intermediate
+ merge-multiple: false
+
+ - name: Recreate the same pre-bottle Formula and collect metadata
+ shell: bash
+ run: |
+ arm=homebrew-intermediate/native-homebrew-macos-arm64
+ intel=homebrew-intermediate/native-homebrew-macos-x86_64
+ cmp "$arm/Formula/tsfile.rb" "$intel/Formula/tsfile.rb"
+ brew tap-new apache/tsfile
+ tap_path="$(brew --repository apache/tsfile)"
+ cp "$arm/Formula/tsfile.rb" "$tap_path/Formula/tsfile.rb"
+ mkdir -p homebrew/Formula homebrew/bottles
+ python3 - <<'PY'
+ import hashlib
+ import json
+ import os
+ import shutil
+ from pathlib import Path
+
+ destination = Path("homebrew/bottles")
+ tags = set()
+ for platform in ("macos-arm64", "macos-x86_64"):
+ directory = Path("homebrew-intermediate") / f"native-homebrew-{platform}" / "bottles"
+ metadata = list(directory.glob("*.bottle.json"))
+ assert len(metadata) == 1, f"Expected one JSON for {platform}"
+ data = json.loads(metadata[0].read_text())
+ assert list(data) == ["apache/tsfile/tsfile"]
+ entry = data["apache/tsfile/tsfile"]
+ assert entry["formula"]["pkg_version"] == os.environ["TSFILE_HOMEBREW_VERSION"]
+ assert entry["formula"]["path"] == "Library/Taps/apache/homebrew-tsfile/Formula/tsfile.rb"
+ assert entry["bottle"]["root_url"] == os.environ["BOTTLE_ROOT_URL"]
+ assert len(entry["bottle"]["tags"]) == 1
+ tag, bottle = next(iter(entry["bottle"]["tags"].items()))
+ assert (platform == "macos-arm64") == tag.startswith("arm64_")
+ assert tag not in tags, f"Duplicate bottle tag: {tag}"
+ tags.add(tag)
+ local_name = bottle["local_filename"]
+ assert Path(local_name).name == local_name
+ source = directory / local_name
+ assert hashlib.sha256(source.read_bytes()).hexdigest() == bottle["sha256"]
+ for source in (source, metadata[0]):
+ target = destination / source.name
+ assert not target.exists(), f"Duplicate artifact: {target}"
+ shutil.copy2(source, target)
+ assert len(tags) == 2
+ PY
+
+ - name: Merge both bottle tags into the Formula
+ shell: bash
+ run: |
+ brew trust apache/tsfile
+ brew bottle --merge --write --no-commit homebrew/bottles/*.json
+ tap_path="$(brew --repository apache/tsfile)"
+ cp "$tap_path/Formula/tsfile.rb" homebrew/Formula/tsfile.rb
+ ruby -c homebrew/Formula/tsfile.rb
+
+ - name: Verify merged Formula and normalize HTTP bottle filenames
+ shell: bash
+ run: |
+ python3 - <<'PY'
+ import hashlib
+ import json
+ import os
+ import re
+ from pathlib import Path
+ from urllib.parse import unquote
+
+ formula = Path("homebrew/Formula/tsfile.rb").read_text()
+ assert not re.search(r"@[A-Z0-9_]+@", formula), "Unresolved Formula token"
+ assert len(re.findall(r"^\s*bottle\s+do\b", formula, re.M)) == 1
+ assert f'root_url "{os.environ["BOTTLE_ROOT_URL"]}"' in formula
+ directory = Path("homebrew/bottles")
+ metadata = list(directory.glob("*.bottle.json"))
+ assert len(metadata) == 2
+ tags = set()
+ for path in metadata:
+ data = json.loads(path.read_text())
+ entry = data["apache/tsfile/tsfile"]
+ for tag, bottle in entry["bottle"]["tags"].items():
+ assert tag not in tags, f"Duplicate bottle tag: {tag}"
+ tags.add(tag)
+ assert re.search(rf'\b{re.escape(tag)}:\s*"{bottle["sha256"]}"', formula)
+ # Homebrew emits a double-dash local name but serves a single-dash URL.
+ filename = unquote(bottle["filename"])
+ assert Path(filename).name == filename and filename.endswith(".tar.gz")
+ source = directory / bottle["local_filename"]
+ target = directory / filename
+ assert hashlib.sha256(source.read_bytes()).hexdigest() == bottle["sha256"]
+ if source != target:
+ assert not target.exists(), f"Duplicate HTTP bottle name: {filename}"
+ source.rename(target)
+ bottle["local_filename"] = filename
+ path.write_text(json.dumps(data, indent=2) + "\n")
+ assert len(tags) == 2
+ assert len(list(directory.glob("*.tar.gz"))) == 2
+ PY
+
+ - name: Upload merged Homebrew Formula and bottles
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-homebrew
+ path: homebrew/
+ if-no-files-found: error
+ retention-days: 14
+
+ test-windows:
+ name: Test installed Windows C++, CLI and Go on ${{ matrix.name }}
+ needs: [build-windows, build-windows-arm64]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: windows-msvc-x86_64
+ os: windows-2022
+ msvc-arch: x64
+ goarch: amd64
+ llvm-arch: x86_64
+ llvm-sha256: 317492c456aa27ee607a5919f1d2d38dcdc1112516a24d0bf4b00d078f52d17a
+ - name: windows-msvc-arm64
+ os: windows-11-vs2026-arm
+ msvc-arch: arm64
+ goarch: arm64
+ llvm-arch: aarch64
+ llvm-sha256: 9a835d5179c9f3a5a783c11a7f14062a249b4765dd11add64a66786864e82ab2
+ steps:
+ - name: Checkout installation test sources
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ cpp/test
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Set up native MSVC for C++ tests
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: ${{ matrix.msvc-arch }}
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Download Windows ZIP
+ uses: actions/download-artifact@v8
+ with:
+ name: native-${{ matrix.name }}
+ path: packages
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Install ZIP and test the CLI with Python test data
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $archives = @(Get-ChildItem packages -Filter *.zip -File)
+ if ($archives.Count -ne 1) { throw 'Expected one Windows ZIP' }
+ Expand-Archive -LiteralPath $archives[0].FullName -DestinationPath installed
+ $executables = @(Get-ChildItem installed -Recurse -Filter tsfile-cli.exe -File)
+ if ($executables.Count -ne 1) { throw 'Expected one installed CLI' }
+ $prefix = $executables[0].Directory.Parent.FullName
+ "TSFILE_INSTALLED_PREFIX=$prefix" >> $env:GITHUB_ENV
+ $python = (Get-Command python).Source
+ $env:PATH = "$($executables[0].DirectoryName);$env:SystemRoot/System32;$env:SystemRoot"
+ & $python packaging/tests/cli_package_smoke.py `
+ --cli $executables[0].FullName --resources cli-test-data
+
+ - name: Compile and run C++ regressions against the installed SDK
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ python packaging/tests/cpp_installed_tests.py `
+ --prefix "$env:TSFILE_INSTALLED_PREFIX" --build-dir installed-tests
+
+ - name: Install pinned LLVM-MinGW for Go cgo
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ $archive = Join-Path $env:RUNNER_TEMP 'llvm-mingw.zip'
+ $url = 'https://github.com/mstorsjo/llvm-mingw/releases/download/20261006/llvm-mingw-20261006-ucrt-${{ matrix.llvm-arch }}.zip'
+ curl.exe --fail --location --retry 3 --output $archive $url
+ $hash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
+ if ($hash -ne '${{ matrix.llvm-sha256 }}') { throw 'LLVM-MinGW checksum mismatch' }
+ $destination = Join-Path $env:RUNNER_TEMP 'llvm-mingw'
+ Expand-Archive -LiteralPath $archive -DestinationPath $destination
+ $tools = Join-Path $destination 'llvm-mingw-20261006-ucrt-${{ matrix.llvm-arch }}/bin'
+ if (-not (Test-Path "$tools/llvm-dlltool.exe")) { throw 'LLVM-MinGW tools missing' }
+ "TSFILE_LLVM_BIN=$tools" >> $env:GITHUB_ENV
+
+ - name: Run Go tests against the installed MSVC DLL
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ python packaging/tests/go_installed_tests.py `
+ --prefix "$env:TSFILE_INSTALLED_PREFIX" --goarch ${{ matrix.goarch }} `
+ --llvm-bin "$env:TSFILE_LLVM_BIN"
+
+ - name: Download the standalone SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-${{ matrix.name }}
+ path: sdk-download
+
+ - name: Test the downloaded and relocated SDK archive
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ python packaging/tests/sdk_archive_tests.py `
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests `
+ --resources cli-test-data --goarch ${{ matrix.goarch }} `
+ --gtest-prefix installed-tests/dependencies --llvm-bin "$env:TSFILE_LLVM_BIN"
+
+ - name: Upload C++ installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cpp-tests-${{ matrix.name }}
+ path: |
+ installed-tests/cpp-results.xml
+ installed-tests/tests/Testing/Temporary/LastTest.log
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ test-homebrew:
+ name: Test installed Homebrew C++, CLI and Go on ${{ matrix.name }}
+ needs: build-homebrew
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: macos-arm64
+ os: macos-latest
+ goarch: arm64
+ - name: macos-x86_64
+ os: macos-15-intel
+ goarch: amd64
+ env:
+ HOMEBREW_NO_AUTO_UPDATE: "1"
+ HOMEBREW_NO_INSTALL_CLEANUP: "1"
+ steps:
+ - name: Checkout installation test sources
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ packaging/scripts/verify_macos_runtime.py
+ cpp/test
+ cpp/cmake/tests/projects/InstalledConsumer
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Install test build tools
+ run: |
+ command -v cmake >/dev/null || brew install cmake
+ command -v pkg-config >/dev/null || brew install pkgconf
+
+ - name: Download Homebrew bottle and Formula
+ uses: actions/download-artifact@v8
+ with:
+ name: native-homebrew-${{ matrix.name }}
+ path: homebrew
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Install the local bottle and test the CLI with Python test data
+ shell: bash
+ run: |
+ brew tap-new apache/tsfile
+ tap_dir="$(brew --repository apache/tsfile)"
+ cp homebrew/Formula/tsfile.rb "$tap_dir/Formula/tsfile.rb"
+ brew trust apache/tsfile
+ shopt -s nullglob
+ bottles=("$PWD"/homebrew/bottles/*.bottle*.tar.gz)
+ [[ ${#bottles[@]} -eq 1 ]]
+ # Load bottle checksums into the trusted tap, then seed the exact
+ # download cache path. Install by formula name: current Homebrew
+ # rejects direct package paths outside developer mode.
+ brew bottle --merge --write --no-commit homebrew/bottles/*.json
+ cached_bottle="$(brew --cache --force-bottle apache/tsfile/tsfile | tail -n 1)"
+ [[ "$cached_bottle" == *.bottle*.tar.gz ]]
+ mkdir -p "$(dirname "$cached_bottle")"
+ cp "${bottles[0]}" "$cached_bottle"
+ brew install --force-bottle apache/tsfile/tsfile
+ formula_prefix="$(brew --prefix apache/tsfile/tsfile | tail -n 1)"
+ echo "TSFILE_INSTALLED_PREFIX=$formula_prefix" >> "$GITHUB_ENV"
+ python3 - "$formula_prefix" <<'PY'
+ import json
+ import sys
+ from pathlib import Path
+
+ receipt = json.loads((Path(sys.argv[1]) / "INSTALL_RECEIPT.json").read_text())
+ if receipt.get("poured_from_bottle") is not True:
+ raise SystemExit("CLI acceptance must install the built bottle")
+ PY
+ python3 packaging/scripts/verify_macos_runtime.py "$formula_prefix"
+ python3 packaging/tests/cli_package_smoke.py \
+ --cli "$formula_prefix/bin/tsfile-cli" --resources cli-test-data
+
+ - name: Compile and run C++ regressions against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/cpp_installed_tests.py \
+ --prefix "$TSFILE_INSTALLED_PREFIX" --build-dir installed-tests
+
+ - name: Run Go tests against the installed SDK
+ shell: bash
+ run: |
+ python3 packaging/tests/go_installed_tests.py \
+ --prefix "$TSFILE_INSTALLED_PREFIX" --goarch ${{ matrix.goarch }}
+
+ - name: Download the standalone SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-${{ matrix.name }}
+ path: sdk-download
+
+ - name: Test the downloaded and relocated SDK archive
+ shell: bash
+ run: |
+ python3 packaging/tests/sdk_archive_tests.py \
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests \
+ --resources cli-test-data --goarch ${{ matrix.goarch }} \
+ --gtest-prefix installed-tests/dependencies
+
+ - name: Upload C++ installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-cpp-tests-${{ github.job }}-${{ runner.arch }}-${{ strategy.job-index }}
+ path: |
+ installed-tests/cpp-results.xml
+ installed-tests/tests/Testing/Temporary/LastTest.log
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ test-compatibility-sdk:
+ name: Test downloaded compatibility SDK on ${{ matrix.name }}
+ needs: [prepare, build-python-unix]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 40
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: manylinux_2_28-x86_64
+ os: ubuntu-22.04
+ goarch: amd64
+ - name: manylinux_2_28-aarch64
+ os: ubuntu-22.04-arm
+ goarch: arm64
+ - name: macos12-x86_64
+ os: macos-15-intel
+ goarch: amd64
+ - name: macos12-arm64
+ os: macos-latest
+ goarch: arm64
+ steps:
+ - name: Checkout SDK consumer tests only
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests
+ packaging/cmake/InstalledGTest
+ packaging/scripts/verify_macos_runtime.py
+ cpp/test
+ go
+ sparse-checkout-cone-mode: false
+
+ - name: Set up Go for installed-package tests
+ uses: actions/setup-go@v7
+ with:
+ go-version: "1.26.x"
+ cache-dependency-path: go/go.sum
+
+ - name: Install Linux consumer prerequisites
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y cmake build-essential pkg-config python3 libuuid1
+
+ - name: Install macOS consumer prerequisites
+ if: runner.os == 'macOS'
+ run: |
+ command -v cmake >/dev/null || brew install cmake
+ command -v pkg-config >/dev/null || brew install pkgconf
+
+ - name: Download the compatibility SDK archive
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-${{ matrix.name }}
+ path: sdk-download
+
+ - name: Download CLI test data
+ uses: actions/download-artifact@v8
+ with:
+ name: native-cli-test-data
+ path: cli-test-data
+
+ - name: Test the downloaded and relocated SDK archive
+ run: |
+ python3 packaging/tests/sdk_archive_tests.py \
+ --archives sdk-download --destination downloaded-sdk --build-dir sdk-tests \
+ --resources cli-test-data --goarch ${{ matrix.goarch }}
+
+ - name: Upload SDK archive test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-sdk-tests-${{ matrix.name }}
+ path: |
+ sdk-tests/cpp-results.xml
+ sdk-tests/tests/Testing/Temporary/LastTest.log
+ if-no-files-found: ignore
+ retention-days: 14
+
+ test-python-unix:
+ name: Test installed Python ${{ matrix.python-version }} wheel on ${{ matrix.platform.name }}
+ needs: [prepare, build-python-unix]
+ runs-on: ${{ matrix.platform.os }}
+ timeout-minutes: 40
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ platform:
+ - name: manylinux_2_28-x86_64
+ os: ubuntu-22.04
+ - name: manylinux_2_28-aarch64
+ os: ubuntu-22.04-arm
+ - name: macos12-x86_64
+ os: macos-15-intel
+ - name: macos12-arm64
+ os: macos-latest
+ steps:
+ - name: Checkout Python tests only
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests/python_installed_tests.py
+ packaging/scripts/check_python_wheel.py
+ python/tests
+ sparse-checkout-cone-mode: false
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v7
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Download the platform wheels
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-${{ matrix.platform.name }}
+ path: wheelhouse
+
+ - name: Install wheel and run the existing pytest suite
+ run: |
+ python packaging/tests/python_installed_tests.py \
+ --wheelhouse wheelhouse --version "${{ needs.prepare.outputs.python_version }}" \
+ --results python-test-results.xml
+
+ - name: Upload Python installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-pytest-${{ matrix.platform.name }}-py${{ matrix.python-version }}
+ path: python-test-results.xml
+ if-no-files-found: error
+ retention-days: 14
+
+ test-python-windows:
+ name: Test installed Windows Python ${{ matrix.python-version }} wheel
+ needs: [prepare, build-python-windows]
+ runs-on: windows-2022
+ timeout-minutes: 40
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ steps:
+ - name: Checkout Python tests only
+ uses: actions/checkout@v7
+ with:
+ sparse-checkout: |
+ packaging/tests/python_installed_tests.py
+ packaging/scripts/check_python_wheel.py
+ python/tests
+ sparse-checkout-cone-mode: false
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v7
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Download the Windows wheels
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-windows-msvc-x86_64
+ path: wheelhouse
+
+ - name: Install wheel and run the existing pytest suite
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+ python packaging/tests/python_installed_tests.py `
+ --wheelhouse wheelhouse --version "${{ needs.prepare.outputs.python_version }}" `
+ --results python-test-results.xml
+
+ - name: Upload Python installation test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-pytest-windows-msvc-x86_64-py${{ matrix.python-version }}
+ path: python-test-results.xml
+ if-no-files-found: error
+ retention-days: 14
+
+ assemble:
+ name: Assemble checksummed native package bundle
+ needs:
+ - prepare
+ - test-deb
+ - test-deb-arm64
+ - test-rpm
+ - test-rpm-aarch64
+ - test-windows
+ - test-homebrew
+ - test-compatibility-sdk
+ - test-python-unix
+ - test-python-windows
+ - build-python-unix
+ - build-python-windows
+ - merge-homebrew
+ - build-windows
+ - build-windows-arm64
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Download package versions
+ uses: actions/download-artifact@v8
+ with:
+ name: native-package-versions
+ path: versions
+
+ - name: Download DEB packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-deb-ubuntu22.04-amd64
+ path: input/deb/amd64
+
+ - name: Download ARM64 DEB packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-deb-ubuntu22.04-arm64
+ path: input/deb/arm64
+
+ - name: Download RPM packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-rpm-almalinux9-x86_64
+ path: input/rpm/x86_64
+
+ - name: Download aarch64 RPM packages
+ uses: actions/download-artifact@v8
+ with:
+ name: native-rpm-almalinux9-aarch64
+ path: input/rpm/aarch64
+
+ - name: Download Homebrew Formula and bottles
+ uses: actions/download-artifact@v8
+ with:
+ name: native-homebrew
+ path: input/homebrew
+
+ - name: Download Windows ZIP
+ uses: actions/download-artifact@v8
+ with:
+ name: native-windows-msvc-x86_64
+ path: input/windows/x86_64
+
+ - name: Download Windows ARM64 ZIP
+ uses: actions/download-artifact@v8
+ with:
+ name: native-windows-msvc-arm64
+ path: input/windows/arm64
+
+ - name: Download Ubuntu SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-ubuntu22.04-amd64
+ path: input/sdk/ubuntu22.04-amd64
+
+ - name: Download Ubuntu ARM64 SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-ubuntu22.04-arm64
+ path: input/sdk/ubuntu22.04-arm64
+
+ - name: Download AlmaLinux SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-almalinux9-x86_64
+ path: input/sdk/almalinux9-x86_64
+
+ - name: Download AlmaLinux aarch64 SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-almalinux9-aarch64
+ path: input/sdk/almalinux9-aarch64
+
+ - name: Download Windows SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-windows-msvc-x86_64
+ path: input/sdk/windows-msvc-x86_64
+
+ - name: Download Windows ARM64 SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-windows-msvc-arm64
+ path: input/sdk/windows-msvc-arm64
+
+ - name: Download macOS arm64 SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-macos-arm64
+ path: input/sdk/macos-arm64
+
+ - name: Download macOS x86_64 SDK
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-macos-x86_64
+ path: input/sdk/macos-x86_64
+
+ - name: Download the Python SDK for manylinux_2_28-x86_64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-manylinux_2_28-x86_64
+ path: input/sdk/manylinux_2_28-x86_64
+
+ - name: Download Python wheels for manylinux_2_28-x86_64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-manylinux_2_28-x86_64
+ path: input/python/manylinux_2_28-x86_64
+
+ - name: Download the Python SDK for manylinux_2_28-aarch64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-manylinux_2_28-aarch64
+ path: input/sdk/manylinux_2_28-aarch64
+
+ - name: Download Python wheels for manylinux_2_28-aarch64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-manylinux_2_28-aarch64
+ path: input/python/manylinux_2_28-aarch64
+
+ - name: Download the Python SDK for macos12-x86_64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-macos12-x86_64
+ path: input/sdk/macos12-x86_64
+
+ - name: Download Python wheels for macos12-x86_64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-macos12-x86_64
+ path: input/python/macos12-x86_64
+
+ - name: Download the Python SDK for macos12-arm64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-sdk-macos12-arm64
+ path: input/sdk/macos12-arm64
+
+ - name: Download Python wheels for macos12-arm64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-macos12-arm64
+ path: input/python/macos12-arm64
+
+ - name: Download Python wheels for windows-msvc-x86_64
+ uses: actions/download-artifact@v8
+ with:
+ name: native-python-wheel-windows-msvc-x86_64
+ path: input/python/windows-msvc-x86_64
+
+ - name: Assemble the publishable bundle
+ env:
+ LOGICAL_VERSION: ${{ needs.prepare.outputs.logical_version }}
+ shell: bash
+ run: |
+ python3 packaging/scripts/assemble_native_packages.py \
+ --input-dir input \
+ --output-dir "tsfile-native-packages-$LOGICAL_VERSION" \
+ --versions-json versions/versions.json \
+ --source-repository "${{ github.repository }}" \
+ --source-commit "${{ github.sha }}"
+
+ - name: Verify assembled manifest checksums
+ env:
+ LOGICAL_VERSION: ${{ needs.prepare.outputs.logical_version }}
+ shell: bash
+ run: |
+ python3 packaging/scripts/assemble_native_packages.py \
+ --verify-bundle "tsfile-native-packages-$LOGICAL_VERSION"
+
+ - name: Upload checksummed native package bundle
+ uses: actions/upload-artifact@v7
+ with:
+ name: tsfile-native-packages-${{ needs.prepare.outputs.archive_version }}
+ path: tsfile-native-packages-${{ needs.prepare.outputs.logical_version }}/
+ if-no-files-found: error
+ retention-days: 14
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
deleted file mode 100644
index c0ca24ec2..000000000
--- a/.github/workflows/wheels.yml
+++ /dev/null
@@ -1,314 +0,0 @@
-name: Build TsFile wheels(multi-platform)
-
-on:
- push:
- branches:
- - "rc/**"
- workflow_dispatch:
-
-jobs:
- build:
- name: Build wheels on ${{ matrix.name }}
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - name: linux-x86_64
- os: ubuntu-22.04
- platform: linux
- cibw_archs_linux: "x86_64"
-
- - name: linux-aarch64
- os: ubuntu-22.04-arm
- platform: linux
- cibw_archs_linux: "aarch64"
-
- - name: macos-x86_64
- os: macos-15-intel
- platform: macos
- cibw_archs_macos: "x86_64"
-
- - name: macos-arm64
- os: macos-latest
- platform: macos
- cibw_archs_macos: "arm64"
-# Windows is handled by the build-windows job below
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
- with:
- submodules: false
- fetch-depth: 0
-
- - name: Set up Python
- uses: actions/setup-python@v7
- with:
- python-version: "3.11"
-
- - name: Set up Java 17
- uses: actions/setup-java@v6.0.1
- with:
- distribution: temurin
- java-version: "17"
-
- - name: Install system deps (macOS)
- if: matrix.platform == 'macos'
- run: |
- set -eux
- brew update
- brew install pkg-config || true
-
- - name: Install build tools
- run: |
- python -m pip install -U pip wheel
- python -m pip install cibuildwheel==3.4.1
-
- - name: Pre-download virtualenv for cibuildwheel
- run: |
- if [ "$(uname)" = "Darwin" ]; then
- CACHE_DIR="$HOME/Library/Caches/cibuildwheel"
- else
- CACHE_DIR="$HOME/.cache/cibuildwheel"
- fi
- mkdir -p "$CACHE_DIR"
- TARGET="$CACHE_DIR/virtualenv-20.26.6.pyz"
- if [ ! -f "$TARGET" ]; then
- curl -sSL --retry 5 --retry-delay 15 --retry-all-errors \
- -o "$TARGET" \
- "https://github.com/pypa/get-virtualenv/raw/20.26.6/public/virtualenv.pyz"
- fi
- ls -la "$TARGET"
-
-
- - name: Build C++ core via Maven (macOS)
- if: matrix.platform == 'macos'
- shell: bash
- env:
- MACOSX_DEPLOYMENT_TARGET: "12.0"
- CFLAGS: "-mmacosx-version-min=12.0"
- CXXFLAGS: "-mmacosx-version-min=12.0"
- LDFLAGS: "-mmacosx-version-min=12.0"
- run: |
- set -euxo pipefail
- chmod +x mvnw || true
- ./mvnw -Pwith-cpp clean package \
- -DskipTests -Dspotless.check.skip=true -Dspotless.apply.skip=true \
- -Dbuild.test=OFF \
- -Dtsfile.dependency.source=BUNDLED \
- -Denable.lzma2=ON \
- -Dcmake.args="-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0"
- otool -l cpp/target/build/lib/libtsfile*.dylib | grep -A2 LC_VERSION_MIN_MACOSX || true
-
- - name: Build wheels via cibuildwheel
- if: matrix.platform != 'macos'
- env:
- CIBW_ARCHS_LINUX: ${{ matrix.cibw_archs_linux }}
-# CIBW_ARCHS_WINDOWS: ${{ matrix.cibw_archs_windows }}
-
- CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*"
- CIBW_SKIP: "pp* *-musllinux*"
-
- CIBW_MANYLINUX_X86_64_IMAGE: "manylinux_2_28"
- CIBW_MANYLINUX_AARCH64_IMAGE: "manylinux_2_28"
-
- MACOSX_DEPLOYMENT_TARGET: "12.0"
-
- CIBW_BEFORE_ALL_LINUX: |
- set -euxo pipefail
- if command -v yum >/dev/null 2>&1; then
- yum install -y wget tar gzip pkgconfig libuuid-devel libblkid-devel
- else
- echo "Not a yum-based image?" ; exit 1
- fi
- ARCH="$(uname -m)"
- mkdir -p /opt/java
- if [ "$ARCH" = "x86_64" ]; then
- JDK_URL="https://download.oracle.com/java/17/archive/jdk-17.0.12_linux-x64_bin.tar.gz"
- else
- # aarch64
- JDK_URL="https://download.oracle.com/java/17/archive/jdk-17.0.12_linux-aarch64_bin.tar.gz"
- fi
- curl -L -o /tmp/jdk17.tar.gz "$JDK_URL"
- tar -xzf /tmp/jdk17.tar.gz -C /opt/java
- export JAVA_HOME=$(echo /opt/java/jdk-17.0.12*)
- export PATH="$JAVA_HOME/bin:$PATH"
- java -version
-
- chmod +x mvnw || true
- ./mvnw -Pwith-cpp clean package \
- -DskipTests -Dbuild.test=OFF \
- -Dtsfile.dependency.source=BUNDLED \
- -Denable.lzma2=ON \
- -Dspotless.check.skip=true -Dspotless.apply.skip=true
- test -d cpp/target/build/lib && test -d cpp/target/build/include
-
- CIBW_TEST_COMMAND: >
- python -c "import tsfile, tsfile.tsfile_reader as r; print('import-ok:')"
- CIBW_BUILD_VERBOSITY: "1"
- run: cibuildwheel --output-dir wheelhouse python
-
- - name: Build wheels via cibuildwheel (macOS)
- if: matrix.platform == 'macos'
- env:
- CIBW_ARCHS_MACOS: ${{ matrix.cibw_archs_macos }}
- CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*"
-# CIBW_BUILD: "cp313-*"
- CIBW_SKIP: "pp*"
- CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=12.0"
- MACOSX_DEPLOYMENT_TARGET: "12.0"
- CIBW_TEST_COMMAND: >
- python -c "import tsfile, tsfile.tsfile_reader as r; print('import-ok:')"
- CIBW_BUILD_VERBOSITY: "1"
- run: cibuildwheel --output-dir wheelhouse python
-
- - name: Upload wheels as artifact
- uses: actions/upload-artifact@v7
- with:
- name: tsfile-wheels-${{ matrix.name }}
- path: wheelhouse/*.whl
-
- # ── Windows: build C++ once, then build wheels for each Python version ──
- build-windows-cpp:
- name: Build C++ core (Windows)
- runs-on: windows-2022
- steps:
- - name: Checkout
- uses: actions/checkout@v7
- with:
- submodules: false
- fetch-depth: 0
-
- - name: Set up Java 17
- uses: actions/setup-java@v6.0.1
- with:
- distribution: temurin
- java-version: "17"
-
- - name: Set up MSYS2 / MinGW
- uses: msys2/setup-msys2@v2
- with:
- msystem: MINGW64
- update: true
- install: >-
- mingw-w64-x86_64-gcc
- mingw-w64-x86_64-cmake
- mingw-w64-x86_64-make
- make
-
- - name: Build C++ core via Maven
- shell: msys2 {0}
- run: |
- set -euxo pipefail
- export JAVA_HOME="$(cygpath "$JAVA_HOME")"
- export PATH="$JAVA_HOME/bin:$PATH"
- java -version
- chmod +x mvnw || true
- ./mvnw -Pwith-cpp clean package \
- -DskipTests -Dbuild.test=OFF \
- -Dtsfile.dependency.source=BUNDLED \
- -Denable.lzma2=ON \
- -Dspotless.check.skip=true -Dspotless.apply.skip=true
- test -d cpp/target/build/lib
- test -d cpp/target/build/include
- cp /mingw64/bin/libstdc++-6.dll cpp/target/build/lib/
- cp /mingw64/bin/libgcc_s_seh-1.dll cpp/target/build/lib/
- cp /mingw64/bin/libwinpthread-1.dll cpp/target/build/lib/
-
- - name: Upload C++ build output
- uses: actions/upload-artifact@v7
- with:
- name: tsfile-cpp-windows
- path: |
- cpp/target/build/lib/
- cpp/target/build/include/
-
- build-windows-wheels:
- name: Build wheel (Windows, Python ${{ matrix.python-version }})
- needs: build-windows-cpp
- runs-on: windows-2022
- strategy:
- fail-fast: false
- matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
- steps:
- - name: Checkout
- uses: actions/checkout@v7
- with:
- submodules: false
- fetch-depth: 0
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v7
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Set up Java 17
- uses: actions/setup-java@v6.0.1
- with:
- distribution: temurin
- java-version: "17"
-
- - name: Set up MSYS2 / MinGW
- uses: msys2/setup-msys2@v2
- with:
- msystem: MINGW64
- update: false
- install: >-
- mingw-w64-x86_64-gcc
- mingw-w64-x86_64-make
- make
-
- - name: Download C++ build output
- uses: actions/download-artifact@v8
- with:
- name: tsfile-cpp-windows
- path: cpp/target/build/
-
- - name: Verify C++ artifacts exist
- shell: msys2 {0}
- run: |
- set -euxo pipefail
- ls -laR cpp/target/build/lib/
- ls -laR cpp/target/build/include/
- test -f cpp/target/build/lib/libtsfile.dll
- test -f cpp/target/build/lib/libtsfile.dll.a
- test -f cpp/target/build/lib/libstdc++-6.dll
- test -f cpp/target/build/lib/libgcc_s_seh-1.dll
- test -f cpp/target/build/lib/libwinpthread-1.dll
-
- - name: Build wheel
- shell: msys2 {0}
- run: |
- set -euxo pipefail
- export JAVA_HOME="$(cygpath "$JAVA_HOME")"
- export PYTHON_HOME="$(cygpath "$pythonLocation")"
- export PATH="/mingw64/bin:$PYTHON_HOME:$PYTHON_HOME/Scripts:$JAVA_HOME/bin:$PATH"
-
- # Build wheel via Maven (no clean — keep C++ artifacts from previous job)
- chmod +x mvnw || true
- cd python
- BLACK_VER_PROP=""
- case "${{ matrix.python-version }}" in
- 3.9) BLACK_VER_PROP="-Dblack.version=25.11.0" ;;
- esac
- ../mvnw package -DskipTests -Dmaven.test.skip=true \
- -Dspotless.check.skip=true -Dspotless.apply.skip=true \
- ${BLACK_VER_PROP}
- ls -la dist/
-
- - name: Verify wheel
- shell: msys2 {0}
- run: |
- set -euxo pipefail
- export PYTHON_HOME="$(cygpath "$pythonLocation")"
- export PATH="$PYTHON_HOME:$PYTHON_HOME/Scripts:$PATH"
- python -m pip install python/dist/*.whl
- python -c "import tsfile, tsfile.tsfile_reader as r; print('import-ok')"
-
- - name: Upload wheel
- uses: actions/upload-artifact@v7
- with:
- name: tsfile-wheels-windows-py${{ matrix.python-version }}
- path: python/dist/*.whl
diff --git a/.gitignore b/.gitignore
index cf943dae7..dc16352bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,8 +40,11 @@ python/tsfile/**/*so*
python/data
python/venv/*
python/tests/__pycache__/*
+packaging/**/__pycache__/
+packaging/**/*.py[cod]
python/tests/*.tsfile
python/tsfile/include
+python/tsfile/licenses/
cpp/cmake-build-debug-mingw/
cpp/third_party/googletest-release-1.12.1.zip
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 0e498e3e0..f9cb5741b 100755
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -17,6 +17,21 @@ 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 (POLICY CMP0091)
+ # Use the runtime property for both /MD and /MT builds so exported SDK
+ # metadata describes the actual compiler selection. Select this before
+ # project() and inherit it in dependencies with older policy versions.
+ cmake_policy(SET CMP0091 NEW)
+ set(CMAKE_POLICY_DEFAULT_CMP0091 NEW)
+endif ()
+if (TSFILE_MSVC_STATIC_RUNTIME)
+ if (NOT POLICY CMP0091)
+ message(FATAL_ERROR "TSFILE_MSVC_STATIC_RUNTIME requires CMake 3.15+")
+ endif ()
+ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
+endif ()
project(TsFile_CPP)
if (DEFINED ToolChain)
@@ -34,7 +49,40 @@ if (POLICY CMP0074)
endif ()
set(TsFile_CPP_VERSION 2.5.1.dev)
+include(GNUInstallDirs)
+include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/TsFilePublicHeaders.cmake)
+
+# The package version identifies the source release. Development suffixes are
+# intentionally removed from generated package metadata and never enter the
+# SONAME.
+string(REGEX MATCH "^[0-9]+\\.[0-9]+\\.[0-9]+" TSFILE_PACKAGE_VERSION
+ "${TsFile_CPP_VERSION}")
+if ("${TSFILE_PACKAGE_VERSION}" STREQUAL "")
+ message(FATAL_ERROR
+ "TsFile_CPP_VERSION must start with a semantic version: "
+ "${TsFile_CPP_VERSION}")
+endif ()
+set(TSFILE_ARCHIVE_VERSION "${TSFILE_PACKAGE_VERSION}" CACHE STRING
+ "Version used in portable archive names")
+set(TSFILE_DEBIAN_PACKAGE_VERSION "${TSFILE_PACKAGE_VERSION}" CACHE STRING
+ "Complete Debian package version")
+set(TSFILE_RPM_PACKAGE_VERSION "${TSFILE_PACKAGE_VERSION}" CACHE STRING
+ "RPM Version value")
+set(TSFILE_RPM_PACKAGE_RELEASE "1" CACHE STRING
+ "RPM Release value")
+set(TSFILE_ABI_VERSION "1" CACHE STRING
+ "TsFile shared-library ABI epoch (changes only on ABI breaks)")
+if (NOT TSFILE_ABI_VERSION MATCHES "^[0-9]+$")
+ message(FATAL_ERROR "TSFILE_ABI_VERSION must be a positive integer")
+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)
@@ -64,10 +112,18 @@ endif ()
message("cmake using: USE_CPP11=${USE_CPP11}")
# MSVC has no /std:c++11; CMake maps this to the closest supported standard
# (C++14 default on MSVC), which compiles the C++11 codebase fine.
-set(CMAKE_CXX_STANDARD 11)
+set(TSFILE_CXX_STANDARD "11" CACHE STRING
+ "C++ language standard used to build TsFile (11, 14, or 17)")
+set_property(CACHE TSFILE_CXX_STANDARD PROPERTY STRINGS 11 14 17)
+if (NOT TSFILE_CXX_STANDARD MATCHES "^(11|14|17)$")
+ message(FATAL_ERROR
+ "TSFILE_CXX_STANDARD must be one of 11, 14, or 17")
+endif ()
+set(CMAKE_CXX_STANDARD ${TSFILE_CXX_STANDARD})
set(CMAKE_CXX_STANDARD_REQUIRED OFF)
if (NOT MSVC)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+ set(CMAKE_CXX_FLAGS
+ "${CMAKE_CXX_FLAGS} -std=c++${TSFILE_CXX_STANDARD}")
endif ()
if (DEFINED ENV{CXX})
@@ -336,7 +392,8 @@ if (MSVC)
# C++17 extensions), so we pin it explicitly for reproducibility.
set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} /W3 /utf-8 /EHsc /bigobj /Zc:__cplusplus /std:c++14")
else ()
- set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -Wall -std=c++11")
+ set(CMAKE_CXX_FLAGS
+ "$ENV{CXXFLAGS} -Wall -std=c++${TSFILE_CXX_STANDARD}")
endif ()
add_subdirectory(third_party)
@@ -418,3 +475,162 @@ endif ()
unset(_TSFILE_PROJECT_DEPENDENCIES)
add_subdirectory(examples)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/TsFileBuildConfig.cmake)
+
+# Install the compatibility header closure from the generated staging tree. The
+# staging tree is populated by the existing copy_* targets and contains only
+# headers selected by the reviewed closure manifest. The closure is retained
+# for source compatibility; it is not the final public API boundary.
+foreach (header_dir IN LISTS TSFILE_PUBLIC_HEADER_CLOSURE)
+ install(DIRECTORY "${LIBRARY_INCLUDE_DIR}/${header_dir}/"
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/tsfile/${header_dir}"
+ COMPONENT development
+ FILES_MATCHING PATTERN "*.h")
+endforeach()
+if (ENABLE_SIMD AND TSFILE_SIMDE_INCLUDE_ROOT)
+ install(DIRECTORY "${TSFILE_SIMDE_INCLUDE_ROOT}/simde/"
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/simde"
+ COMPONENT development
+ FILES_MATCHING PATTERN "*.h")
+endif ()
+if (ENABLE_ANTLR4 AND TSFILE_ANTLR4_INCLUDE_ROOT)
+ install(DIRECTORY "${TSFILE_ANTLR4_INCLUDE_ROOT}/"
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+ COMPONENT development
+ FILES_MATCHING PATTERN "*.h")
+endif ()
+if (ENABLE_ANTLR4 AND TSFILE_UTF8CPP_INCLUDE_ROOT)
+ install(DIRECTORY "${TSFILE_UTF8CPP_INCLUDE_ROOT}/"
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+ COMPONENT development
+ FILES_MATCHING PATTERN "*.h")
+endif ()
+
+set(TSFILE_LICENSE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE")
+set(TSFILE_NOTICE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../NOTICE")
+if (EXISTS "${TSFILE_LICENSE_FILE}")
+ install(FILES "${TSFILE_LICENSE_FILE}"
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/tsfile"
+ COMPONENT runtime)
+endif()
+if (EXISTS "${TSFILE_NOTICE_FILE}")
+ install(FILES "${TSFILE_NOTICE_FILE}"
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/tsfile"
+ COMPONENT runtime)
+endif()
+
+include(CMakePackageConfigHelpers)
+set(TSFILE_CMAKE_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/TsFile")
+configure_package_config_file(
+ "${CMAKE_CURRENT_SOURCE_DIR}/cmake/TsFileConfig.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfig.cmake"
+ INSTALL_DESTINATION "${TSFILE_CMAKE_INSTALL_DIR}")
+write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfigVersion.cmake"
+ VERSION "${TSFILE_PACKAGE_VERSION}"
+ COMPATIBILITY SameMajorVersion)
+install(FILES
+ "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfig.cmake"
+ "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfigVersion.cmake"
+ DESTINATION "${TSFILE_CMAKE_INSTALL_DIR}"
+ 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_MEM_STAT
+ ENABLE_SNAPPY
+ ENABLE_LZ4
+ ENABLE_LZOKAY
+ ENABLE_ZLIB
+ ENABLE_GZIP
+ ENABLE_ZSTD
+ ENABLE_LZMA2
+ ENABLE_THREADS
+ ENABLE_SIMD)
+ if (${_TSFILE_PUBLIC_FEATURE})
+ string(APPEND TSFILE_PKGCONFIG_CFLAGS
+ " -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.in"
+ @ONLY)
+file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libtsfile-$<CONFIG>.pc"
+ INPUT "${CMAKE_CURRENT_BINARY_DIR}/libtsfile.pc.in")
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libtsfile-$<CONFIG>.pc"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig"
+ RENAME libtsfile.pc
+ COMPONENT development)
+unset(_TSFILE_PUBLIC_FEATURE)
+
+# CPack is an opt-in local/package-builder integration. Native release jobs
+# use BUNDLED dependencies so packages do not depend on third-party shared
+# libraries installed on the build host.
+option(TSFILE_ENABLE_CPACK
+ "Enable CPack native package metadata generation" OFF)
+if (TSFILE_ENABLE_CPACK)
+ set(CPACK_PACKAGE_NAME "tsfile")
+ set(CPACK_PACKAGE_VENDOR "Apache Software Foundation")
+ set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Apache TsFile C++ library")
+ set(CPACK_PACKAGE_DESCRIPTION
+ "Columnar storage library and command-line tools for time series data")
+ set(CPACK_PACKAGE_HOMEPAGE_URL "https://tsfile.apache.org/")
+ set(CPACK_PACKAGE_CONTACT "dev@tsfile.apache.org")
+ set(CPACK_RESOURCE_FILE_LICENSE "${TSFILE_LICENSE_FILE}")
+ set(CPACK_PACKAGE_VERSION "${TSFILE_ARCHIVE_VERSION}")
+ string(REPLACE "." ";" _TSFILE_PACKAGE_VERSION_PARTS
+ "${TSFILE_PACKAGE_VERSION}")
+ list(GET _TSFILE_PACKAGE_VERSION_PARTS 0 CPACK_PACKAGE_VERSION_MAJOR)
+ list(GET _TSFILE_PACKAGE_VERSION_PARTS 1 CPACK_PACKAGE_VERSION_MINOR)
+ list(GET _TSFILE_PACKAGE_VERSION_PARTS 2 CPACK_PACKAGE_VERSION_PATCH)
+
+ set(CPACK_COMPONENTS_ALL runtime development tools)
+ set(CPACK_COMPONENT_DEVELOPMENT_DEPENDS runtime)
+ set(CPACK_COMPONENT_TOOLS_DEPENDS runtime)
+ # Portable archives contain the complete runtime, SDK, and tools install
+ # tree in one file. Native DEB and RPM generators remain componentized.
+ set(CPACK_ARCHIVE_COMPONENT_INSTALL OFF)
+ if (WIN32)
+ set(CPACK_PACKAGE_FILE_NAME
+ "tsfile-${TSFILE_ARCHIVE_VERSION}-windows-x86_64")
+ endif ()
+ set(CPACK_DEB_COMPONENT_INSTALL ON)
+ set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
+ set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
+ set(CPACK_DEBIAN_PACKAGE_VERSION "${TSFILE_DEBIAN_PACKAGE_VERSION}")
+ set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "tsfile")
+ set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_NAME "tsfile-dev")
+ set(CPACK_DEBIAN_TOOLS_PACKAGE_NAME "tsfile-tools")
+ set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_DEPENDS
+ "tsfile (= ${TSFILE_DEBIAN_PACKAGE_VERSION})")
+ set(CPACK_DEBIAN_TOOLS_PACKAGE_DEPENDS
+ "tsfile (= ${TSFILE_DEBIAN_PACKAGE_VERSION})")
+
+ set(CPACK_RPM_COMPONENT_INSTALL ON)
+ set(CPACK_RPM_FILE_NAME RPM-DEFAULT)
+ set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
+ set(CPACK_RPM_PACKAGE_VERSION "${TSFILE_RPM_PACKAGE_VERSION}")
+ set(CPACK_RPM_PACKAGE_RELEASE "${TSFILE_RPM_PACKAGE_RELEASE}")
+ set(CPACK_RPM_PACKAGE_AUTOREQPROV ON)
+ set(CPACK_RPM_RUNTIME_PACKAGE_NAME "tsfile")
+ set(CPACK_RPM_DEVELOPMENT_PACKAGE_NAME "tsfile-devel")
+ set(CPACK_RPM_TOOLS_PACKAGE_NAME "tsfile-tools")
+ set(CPACK_RPM_DEVELOPMENT_PACKAGE_REQUIRES
+ "tsfile = ${TSFILE_RPM_PACKAGE_VERSION}-${TSFILE_RPM_PACKAGE_RELEASE}")
+ set(CPACK_RPM_TOOLS_PACKAGE_REQUIRES
+ "tsfile = ${TSFILE_RPM_PACKAGE_VERSION}-${TSFILE_RPM_PACKAGE_RELEASE}")
+
+ include(CPack)
+ unset(_TSFILE_PACKAGE_VERSION_PARTS)
+endif ()
diff --git a/cpp/README.md b/cpp/README.md
index b1a69b0a7..8089ac1cd 100644
--- a/cpp/README.md
+++ b/cpp/README.md
@@ -161,12 +161,13 @@ cmake --build cpp/build/static --target tsfile
The global `TSFILE_DEPENDENCY_SOURCE` CMake option defines how migrated C++
dependencies are resolved:
-- `AUTO` (default): prefer a compatible system package and fall back to the
+- `AUTO`: prefer a compatible system package and fall back to the
verified source archive managed by the build.
- `SYSTEM`: require compatible system packages and fail configuration with a
clear error when one is unavailable.
-- `BUNDLED`: download and build pinned dependency source archives managed by
- the TsFile build.
+- `BUNDLED` (default): download and build pinned dependency source archives
+ managed by the TsFile build. Native release packages use this mode to avoid
+ runtime dependencies on third-party libraries installed on the build host.
ANTLR4, Snappy, LZ4, lzokay, SIMDe, zlib, Zstandard, and liblzma are currently
resolved through this policy. A compatible system ANTLR4 must be version 4.9.3
diff --git a/cpp/VersionUpdater.groovy b/cpp/VersionUpdater.groovy
index 49cbc6504..cde2f109b 100644
--- a/cpp/VersionUpdater.groovy
+++ b/cpp/VersionUpdater.groovy
@@ -19,6 +19,17 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Synchronize the version in CMakeLists.txt and the one used in the maven pom.
+
+// Native package jobs derive their identity from native_package_versions.py.
+// Keep that identity stable by leaving source versions untouched in those jobs.
+def skipVersionSync = project.properties.getProperty("tsfile.version.sync.skip")
+if (skipVersionSync == null) {
+ skipVersionSync = System.getProperty("tsfile.version.sync.skip")
+}
+if (skipVersionSync != null && skipVersionSync.equalsIgnoreCase("true")) {
+ println "Skipping source version synchronization for native packaging"
+ return
+}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
def cppProjectFile = new File(project.basedir, "CMakeLists.txt")
diff --git a/cpp/cmake/ANTLR4Source.cmake b/cpp/cmake/ANTLR4Source.cmake
index 9094ab09f..81d1a595f 100644
--- a/cpp/cmake/ANTLR4Source.cmake
+++ b/cpp/cmake/ANTLR4Source.cmake
@@ -17,6 +17,8 @@ specific language governing permissions and limitations
under the License.
]]
+include("${CMAKE_CURRENT_LIST_DIR}/DownloadWithRetry.cmake")
+
set(_TSFILE_ANTLR4_ARCHIVE_NAME
"antlr4-${TSFILE_ANTLR4_BUNDLED_VERSION}.tar.gz")
set(_TSFILE_ANTLR4_URL
@@ -89,13 +91,8 @@ function(_tsfile_prepare_antlr4_archive
set(_TSFILE_PARTIAL_ARCHIVE "${_TSFILE_ARCHIVE}.part")
file(REMOVE "${_TSFILE_PARTIAL_ARCHIVE}")
message(STATUS "Downloading ${DEPENDENCY}")
- file(DOWNLOAD
- "${URL}"
- "${_TSFILE_PARTIAL_ARCHIVE}"
- STATUS _TSFILE_DOWNLOAD_STATUS
- TLS_VERIFY ON
- TIMEOUT 120
- INACTIVITY_TIMEOUT 30)
+ tsfile_download_with_retry(
+ "${URL}" "${_TSFILE_PARTIAL_ARCHIVE}" _TSFILE_DOWNLOAD_STATUS)
list(GET _TSFILE_DOWNLOAD_STATUS 0 _TSFILE_DOWNLOAD_RESULT)
list(GET _TSFILE_DOWNLOAD_STATUS 1 _TSFILE_DOWNLOAD_MESSAGE)
if (NOT _TSFILE_DOWNLOAD_RESULT EQUAL 0)
diff --git a/cpp/cmake/DependencySource.cmake b/cpp/cmake/DependencySource.cmake
index 5f4bc25a6..7573e41f7 100644
--- a/cpp/cmake/DependencySource.cmake
+++ b/cpp/cmake/DependencySource.cmake
@@ -26,7 +26,7 @@ set(TSFILE_DEPENDENCY_OFFLINE OFF
CACHE BOOL
"Disallow network downloads and require cached dependency archives")
-set(TSFILE_DEPENDENCY_SOURCE "AUTO"
+set(TSFILE_DEPENDENCY_SOURCE "BUNDLED"
CACHE STRING
"Dependency source policy: AUTO, SYSTEM, or BUNDLED")
set_property(CACHE TSFILE_DEPENDENCY_SOURCE
diff --git a/cpp/cmake/DownloadWithRetry.cmake b/cpp/cmake/DownloadWithRetry.cmake
new file mode 100644
index 000000000..632e64b72
--- /dev/null
+++ b/cpp/cmake/DownloadWithRetry.cmake
@@ -0,0 +1,63 @@
+#[[
+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.
+]]
+
+include_guard(GLOBAL)
+
+# Retry only transient transport/server failures. Archive identity is still
+# checked by the caller before the downloaded file enters the dependency cache.
+function(tsfile_download_with_retry URL DESTINATION OUTPUT_STATUS)
+ set(_TSFILE_TRANSIENT_CURL_CODES 6 7 18 28 35 52 55 56)
+ set(_TSFILE_TRANSIENT_HTTP_CODES 408 429 500 502 503 504)
+ foreach (_TSFILE_ATTEMPT RANGE 1 3)
+ file(REMOVE "${DESTINATION}")
+ file(DOWNLOAD "${URL}" "${DESTINATION}"
+ STATUS _TSFILE_STATUS
+ LOG _TSFILE_LOG
+ TLS_VERIFY ON
+ TIMEOUT 120
+ INACTIVITY_TIMEOUT 30)
+ list(GET _TSFILE_STATUS 0 _TSFILE_RESULT)
+ if (_TSFILE_RESULT EQUAL 0)
+ break()
+ endif ()
+ file(REMOVE "${DESTINATION}")
+ list(FIND _TSFILE_TRANSIENT_CURL_CODES "${_TSFILE_RESULT}"
+ _TSFILE_TRANSIENT_INDEX)
+ if (_TSFILE_RESULT EQUAL 22)
+ # Use the final HTTP response after redirects/proxy negotiation.
+ string(REGEX MATCHALL "HTTP/[0-9.]+ [0-9][0-9][0-9]"
+ _TSFILE_RESPONSES "${_TSFILE_LOG}")
+ if (_TSFILE_RESPONSES)
+ list(GET _TSFILE_RESPONSES -1 _TSFILE_RESPONSE)
+ string(REGEX REPLACE "^HTTP/[0-9.]+ " ""
+ _TSFILE_HTTP_CODE "${_TSFILE_RESPONSE}")
+ list(FIND _TSFILE_TRANSIENT_HTTP_CODES "${_TSFILE_HTTP_CODE}"
+ _TSFILE_TRANSIENT_INDEX)
+ endif ()
+ endif ()
+ if (_TSFILE_TRANSIENT_INDEX EQUAL -1 OR _TSFILE_ATTEMPT EQUAL 3)
+ break()
+ endif ()
+ list(GET _TSFILE_STATUS 1 _TSFILE_MESSAGE)
+ message(STATUS "Dependency download attempt ${_TSFILE_ATTEMPT}/3 failed: "
+ "${_TSFILE_MESSAGE}; retrying ${URL}")
+ execute_process(COMMAND "${CMAKE_COMMAND}" -E sleep "${_TSFILE_ATTEMPT}")
+ endforeach ()
+ set(${OUTPUT_STATUS} "${_TSFILE_STATUS}" PARENT_SCOPE)
+endfunction()
diff --git a/cpp/cmake/TsFileBuildConfig.cmake b/cpp/cmake/TsFileBuildConfig.cmake
new file mode 100644
index 000000000..bbd215935
--- /dev/null
+++ b/cpp/cmake/TsFileBuildConfig.cmake
@@ -0,0 +1,45 @@
+#[[
+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.
+]]
+
+# Record the resolved runtime on both the build tree and the installed SDK.
+# file(GENERATE) evaluates Debug suffixes for each configuration separately.
+set(_TSFILE_MSVC_RUNTIME "")
+if (MSVC)
+ get_target_property(_TSFILE_MSVC_RUNTIME tsfile MSVC_RUNTIME_LIBRARY)
+ if (NOT _TSFILE_MSVC_RUNTIME)
+ set(_TSFILE_MSVC_RUNTIME "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
+ endif ()
+endif ()
+set(_TSFILE_RUNTIME_LIBRARY "")
+if (TSFILE_BUILD_SHARED)
+ if (WIN32)
+ set(_TSFILE_RUNTIME_LIBRARY "$<TARGET_FILE_NAME:tsfile>")
+ else ()
+ set(_TSFILE_RUNTIME_LIBRARY "$<TARGET_SONAME_FILE_NAME:tsfile>")
+ endif ()
+endif ()
+set(_TSFILE_BUILD_CONFIG "$<TARGET_FILE_DIR:tsfile>/tsfile-build-config.json")
+file(GENERATE OUTPUT "${_TSFILE_BUILD_CONFIG}" CONTENT
+"{\n \"schema_version\": 1,\n \"version\": \"${TSFILE_PACKAGE_VERSION}\",\n \"compiler\": \"${CMAKE_CXX_COMPILER_ID}\",\n \"configuration\": \"$<CONFIG>\",\n \"msvc_runtime\": \"${_TSFILE_MSVC_RUNTIME}\",\n \"runtime_library\": \"${_TSFILE_RUNTIME_LIBRARY}\"\n}\n")
+install(FILES "${_TSFILE_BUILD_CONFIG}"
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/tsfile"
+ COMPONENT development)
+unset(_TSFILE_BUILD_CONFIG)
+unset(_TSFILE_RUNTIME_LIBRARY)
+unset(_TSFILE_MSVC_RUNTIME)
diff --git a/cpp/cmake/TsFileConfig.cmake.in b/cpp/cmake/TsFileConfig.cmake.in
new file mode 100644
index 000000000..1357f4898
--- /dev/null
+++ b/cpp/cmake/TsFileConfig.cmake.in
@@ -0,0 +1,43 @@
+#[[
+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.
+]]
+
+@PACKAGE_INIT@
+
+include(CMakeFindDependencyMacro)
+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/TsFilePublicHeaders.cmake b/cpp/cmake/TsFilePublicHeaders.cmake
new file mode 100644
index 000000000..a517d3a9c
--- /dev/null
+++ b/cpp/cmake/TsFilePublicHeaders.cmake
@@ -0,0 +1,47 @@
+#[[
+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.
+]]
+
+# Public entry points are intentionally listed separately from the transitive
+# header closure. The current public headers include implementation-level
+# declarations, so the closure is installed until those dependencies can be
+# hidden behind a stable facade. Adding a new public entry point requires an
+# explicit review of this list.
+set(TSFILE_PUBLIC_ENTRYPOINT_HEADERS
+ cwrapper/tsfile_cwrapper.h
+ cwrapper/tsfile_cwrapper_expression.h
+ reader/tsfile_reader.h
+ writer/tsfile_writer.h
+ writer/tsfile_table_writer.h
+ writer/tsfile_tree_writer.h)
+
+# These directories form the reviewed transitive closure of the entry points
+# above. This is an installation compatibility closure, not a promise that
+# every header is a stable public API. Compression, encoding, parser, and
+# utility headers are implementation dependencies today, but are required for
+# consumers to parse the installed public declarations.
+set(TSFILE_PUBLIC_HEADER_CLOSURE
+ common
+ compress
+ cwrapper
+ encoding
+ file
+ parser
+ reader
+ utils
+ writer)
diff --git a/cpp/cmake/TsFileStaticDependencies.cmake b/cpp/cmake/TsFileStaticDependencies.cmake
new file mode 100644
index 000000000..db6acfbb2
--- /dev/null
+++ b/cpp/cmake/TsFileStaticDependencies.cmake
@@ -0,0 +1,185 @@
+#[[
+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.
+# Resolve system interface wrappers as well as imported archive/shared targets.
+function(tsfile_pkgconfig_system_libraries OUTPUT)
+ set(_FLAGS "")
+ foreach (_LIBRARY IN LISTS ARGN)
+ if (TARGET "${_LIBRARY}")
+ get_target_property(_TYPE "${_LIBRARY}" TYPE)
+ if (NOT _TYPE STREQUAL "INTERFACE_LIBRARY")
+ string(APPEND _FLAGS " \"$<TARGET_FILE:${_LIBRARY}>\"")
+ endif ()
+ get_target_property(_LINKS "${_LIBRARY}" INTERFACE_LINK_LIBRARIES)
+ if (_LINKS)
+ tsfile_pkgconfig_system_libraries(_TRANSITIVE ${_LINKS})
+ string(APPEND _FLAGS "${_TRANSITIVE}")
+ endif ()
+ elseif (IS_ABSOLUTE "${_LIBRARY}" AND _LIBRARY MATCHES "\\.framework$")
+ get_filename_component(_FRAMEWORK_DIR "${_LIBRARY}" DIRECTORY)
+ get_filename_component(_FRAMEWORK_NAME "${_LIBRARY}" NAME_WE)
+ string(APPEND _FLAGS " -F\"${_FRAMEWORK_DIR}\" -framework ${_FRAMEWORK_NAME}")
+ elseif (IS_ABSOLUTE "${_LIBRARY}")
+ string(APPEND _FLAGS " \"${_LIBRARY}\"")
+ elseif (_LIBRARY MATCHES "^-")
+ string(APPEND _FLAGS " ${_LIBRARY}")
+ elseif (_LIBRARY MATCHES "^\\$<LINK_ONLY:(.*)>$")
+ tsfile_pkgconfig_system_libraries(_TRANSITIVE "${CMAKE_MATCH_1}")
+ string(APPEND _FLAGS "${_TRANSITIVE}")
+ else ()
+ string(APPEND _FLAGS " -l${_LIBRARY}")
+ endif ()
+ endforeach ()
+ set(${OUTPUT} "${_FLAGS}" PARENT_SCOPE)
+endfunction()
+
+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}")
+ set(_PKGCONFIG "${TSFILE_PKGCONFIG_LIBS_PRIVATE}")
+ 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 _PKGCONFIG
+ " \"\${libdir}/tsfile/$<CONFIG>/$<TARGET_FILE_NAME:${_TARGET}>\"")
+ 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 ()
+ tsfile_pkgconfig_system_libraries(_SYSTEM_FLAGS TsFile::${NAME})
+ string(APPEND _PKGCONFIG "${_SYSTEM_FLAGS}")
+ 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)
+ set(TSFILE_PKGCONFIG_LIBS_PRIVATE "${_PKGCONFIG}" PARENT_SCOPE)
+endfunction()
+
+set(TSFILE_STATIC_DEPENDENCY_CODE "")
+set(TSFILE_PKGCONFIG_LIBS_PRIVATE "")
+# Keep the build-side compatibility interval when a static consumer rediscovers
+# ANTLR4. Explicit comparisons also support CMake 3.11 and packages that report
+# ANTLR_VERSION instead of antlr4-runtime_VERSION.
+string(CONFIGURE [=[
+find_package(utf8cpp CONFIG QUIET)
+find_package(antlr4-runtime CONFIG QUIET)
+set(_TSFILE_ANTLR4_VERSION "${antlr4-runtime_VERSION}")
+if ("${_TSFILE_ANTLR4_VERSION}" STREQUAL "" AND DEFINED ANTLR_VERSION)
+ set(_TSFILE_ANTLR4_VERSION "${ANTLR_VERSION}")
+endif ()
+if (NOT antlr4-runtime_FOUND OR
+ "${_TSFILE_ANTLR4_VERSION}" STREQUAL "" OR
+ _TSFILE_ANTLR4_VERSION VERSION_LESS "@TSFILE_ANTLR4_MIN_VERSION@" OR
+ NOT _TSFILE_ANTLR4_VERSION VERSION_LESS "@TSFILE_ANTLR4_NEXT_INCOMPATIBLE_VERSION@" OR
+ NOT TARGET @TSFILE_ANTLR4_SYSTEM_TARGET@)
+ set(TsFile_FOUND FALSE)
+ set(TsFile_NOT_FOUND_MESSAGE
+ "Static TsFile requires a compatible system ANTLR4 >=@TSFILE_ANTLR4_MIN_VERSION@ and <@TSFILE_ANTLR4_NEXT_INCOMPATIBLE_VERSION@ with target @TSFILE_ANTLR4_SYSTEM_TARGET@; reported version '${_TSFILE_ANTLR4_VERSION}'.")
+ unset(_TSFILE_ANTLR4_VERSION)
+ return()
+endif ()
+unset(_TSFILE_ANTLR4_VERSION)
+]=] _TSFILE_FIND_STATIC_ANTLR4 @ONLY)
+tsfile_install_static_dependency(ANTLR4 "${TSFILE_ANTLR4_SOURCE}"
+ "${TSFILE_ANTLR4_SYSTEM_TARGET}"
+ "${_TSFILE_FIND_STATIC_ANTLR4}")
+unset(_TSFILE_FIND_STATIC_ANTLR4)
+if (ENABLE_ANTLR4 AND TSFILE_ANTLR4_SOURCE STREQUAL "BUNDLED")
+ if (WIN32)
+ string(APPEND TSFILE_PKGCONFIG_LIBS_PRIVATE " -lole32")
+ 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)
+ string(APPEND TSFILE_PKGCONFIG_LIBS_PRIVATE " -framework CoreFoundation")
+ else ()
+ set(_PLATFORM_LIBRARY uuid)
+ string(APPEND TSFILE_PKGCONFIG_LIBS_PRIVATE " -luuid")
+ 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 ()")
+
+# pkg-config can be consumed by a C linker, which does not implicitly add the
+# C++ runtime. Keep compiler/platform libraries private to static linking.
+if (ENABLE_THREADS AND CMAKE_THREAD_LIBS_INIT)
+ string(APPEND TSFILE_PKGCONFIG_LIBS_PRIVATE " ${CMAKE_THREAD_LIBS_INIT}")
+endif ()
+set(_TSFILE_CXX_LIBRARIES ${CMAKE_CXX_IMPLICIT_LINK_LIBRARIES})
+if (CMAKE_C_IMPLICIT_LINK_LIBRARIES)
+ list(REMOVE_ITEM _TSFILE_CXX_LIBRARIES ${CMAKE_C_IMPLICIT_LINK_LIBRARIES})
+endif ()
+tsfile_pkgconfig_system_libraries(_TSFILE_CXX_FLAGS ${_TSFILE_CXX_LIBRARIES})
+string(APPEND TSFILE_PKGCONFIG_LIBS_PRIVATE "${_TSFILE_CXX_FLAGS}")
+set(TSFILE_PKGCONFIG_LIBS_PRIVATE "${TSFILE_PKGCONFIG_LIBS_PRIVATE}" PARENT_SCOPE)
+unset(_TSFILE_CXX_LIBRARIES)
+unset(_TSFILE_CXX_FLAGS)
+
+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/TsFileStaticDependencies.cmake.in b/cpp/cmake/TsFileStaticDependencies.cmake.in
new file mode 100644
index 000000000..877bc21df
--- /dev/null
+++ b/cpp/cmake/TsFileStaticDependencies.cmake.in
@@ -0,0 +1,20 @@
+#[[
+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.
+]]
+
+@TSFILE_STATIC_DEPENDENCY_CODE@
diff --git a/cpp/cmake/libtsfile.pc.in b/cpp/cmake/libtsfile.pc.in
new file mode 100644
index 000000000..658b0de2a
--- /dev/null
+++ b/cpp/cmake/libtsfile.pc.in
@@ -0,0 +1,32 @@
+# 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.
+
+# 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}/@TSFILE_PKGCONFIG_PREFIX_RELATIVE@
+exec_prefix=${prefix}
+libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@
+includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
+
+Name: libtsfile
+Description: Apache TsFile C++ library
+Version: @TSFILE_PACKAGE_VERSION@
+URL: https://tsfile.apache.org/
+Libs: -L${libdir} -ltsfile
+Libs.private: @TSFILE_PKGCONFIG_LIBS_PRIVATE@
+Cflags: -I${includedir} -I${includedir}/tsfile@TSFILE_PKGCONFIG_CFLAGS@
diff --git a/cpp/cmake/tests/ANTLR4DependencyTest.cmake b/cpp/cmake/tests/ANTLR4DependencyTest.cmake
index f3594929b..b30df26b7 100644
--- a/cpp/cmake/tests/ANTLR4DependencyTest.cmake
+++ b/cpp/cmake/tests/ANTLR4DependencyTest.cmake
@@ -22,6 +22,14 @@ if (NOT TEST_BINARY_ROOT)
endif ()
include("${CMAKE_CURRENT_LIST_DIR}/TestGeneratorArguments.cmake")
+# Package lookup is intentionally isolated below. Keep compiler discovery out
+# of that isolation by reusing the compiler selected by the outer build.
+set(_TSFILE_ANTLR4_COMPILER_ARGUMENTS "")
+if (TEST_CMAKE_CXX_COMPILER)
+ list(APPEND _TSFILE_ANTLR4_COMPILER_ARGUMENTS
+ "-DCMAKE_CXX_COMPILER=${TEST_CMAKE_CXX_COMPILER}")
+endif ()
+
set(_TSFILE_FIXTURE_SOURCE
"${CMAKE_CURRENT_LIST_DIR}/projects/ANTLR4Dependency")
set(_TSFILE_TEST_ROOT "${TEST_BINARY_ROOT}/antlr4-dependency")
@@ -54,6 +62,7 @@ function(_tsfile_run_antlr4_case NAME POLICY EXPECTED_SOURCE EXPECT_SUCCESS ROOT
execute_process(
COMMAND "${CMAKE_COMMAND}"
${_TSFILE_TEST_GENERATOR_ARGUMENTS}
+ ${_TSFILE_ANTLR4_COMPILER_ARGUMENTS}
"-DTSFILE_DEPENDENCY_SOURCE=${POLICY}"
"-DEXPECTED_ANTLR4_SOURCE=${EXPECTED_SOURCE}"
"-DCMAKE_PREFIX_PATH=${ROOT}"
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/ANTLR4Dependency/CMakeLists.txt b/cpp/cmake/tests/projects/ANTLR4Dependency/CMakeLists.txt
index 61901e38a..551373afe 100644
--- a/cpp/cmake/tests/projects/ANTLR4Dependency/CMakeLists.txt
+++ b/cpp/cmake/tests/projects/ANTLR4Dependency/CMakeLists.txt
@@ -18,7 +18,7 @@ under the License.
]]
cmake_minimum_required(VERSION 3.11)
-project(TsFileANTLR4DependencyTest NONE)
+project(TsFileANTLR4DependencyTest LANGUAGES CXX)
get_filename_component(_TSFILE_CMAKE_DIR
"${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE)
diff --git a/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt b/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt
new file mode 100644
index 000000000..40ea9481d
--- /dev/null
+++ b/cpp/cmake/tests/projects/InstalledConsumer/CMakeLists.txt
@@ -0,0 +1,45 @@
+#[[
+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.
+]]
+cmake_minimum_required(VERSION 3.11)
+if (POLICY CMP0091)
+ cmake_policy(SET CMP0091 NEW)
+endif ()
+project(TsFileInstalledConsumer LANGUAGES CXX)
+
+find_package(TsFile CONFIG REQUIRED)
+
+get_target_property(_TSFILE_INCLUDE_DIRS TsFile::tsfile
+ INTERFACE_INCLUDE_DIRECTORIES)
+if (NOT _TSFILE_INCLUDE_DIRS MATCHES "/include")
+ message(FATAL_ERROR "TsFile target does not expose the installed include root")
+endif ()
+if (_TSFILE_INCLUDE_DIRS MATCHES "cpp/src|build/")
+ message(FATAL_ERROR "TsFile target leaks a source or build include path: ${_TSFILE_INCLUDE_DIRS}")
+endif ()
+
+get_target_property(_TSFILE_LINK_LIBRARIES TsFile::tsfile
+ INTERFACE_LINK_LIBRARIES)
+if (_TSFILE_LINK_LIBRARIES MATCHES "(^|;)[^;]*_obj($|;)|cpp/src|build/")
+ message(FATAL_ERROR
+ "TsFile target leaks private build link requirements: "
+ "${_TSFILE_LINK_LIBRARIES}")
+endif ()
+
+add_executable(tsfile_installed_consumer main.cc)
+target_link_libraries(tsfile_installed_consumer PRIVATE TsFile::tsfile)
diff --git a/cpp/cmake/tests/projects/InstalledConsumer/main.cc b/cpp/cmake/tests/projects/InstalledConsumer/main.cc
new file mode 100644
index 000000000..2d265d429
--- /dev/null
+++ b/cpp/cmake/tests/projects/InstalledConsumer/main.cc
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+#include <tsfile/cwrapper/tsfile_cwrapper.h>
+#include <tsfile/reader/tsfile_reader.h>
+#include <tsfile/writer/tsfile_table_writer.h>
+#include <tsfile/writer/tsfile_tree_writer.h>
+
+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/CMakeLists.txt b/cpp/cmake/tests/projects/PkgConfigConsumer/CMakeLists.txt
new file mode 100644
index 000000000..698f16ef9
--- /dev/null
+++ b/cpp/cmake/tests/projects/PkgConfigConsumer/CMakeLists.txt
@@ -0,0 +1,48 @@
+#[[
+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.
+]]
+cmake_minimum_required(VERSION 3.11)
+project(TsFilePkgConfigConsumer LANGUAGES C CXX)
+
+find_package(PkgConfig REQUIRED)
+option(TSFILE_PKGCONFIG_STATIC "Link using pkg-config --static" OFF)
+pkg_check_modules(TsFile REQUIRED libtsfile)
+if (NOT TsFile_INCLUDEDIR MATCHES "/include$")
+ message(FATAL_ERROR "pkg-config did not report the installed include root: ${TsFile_INCLUDEDIR}")
+endif ()
+
+# FindPkgConfig's STATIC variables also work with the CMake 3.11 baseline.
+if (TSFILE_PKGCONFIG_STATIC)
+ set(_TSFILE_PKGCONFIG_FLAGS ${TsFile_STATIC_LDFLAGS})
+else ()
+ set(_TSFILE_PKGCONFIG_FLAGS ${TsFile_LDFLAGS})
+endif ()
+# target_link_libraries treats a bare framework name as -l<name>. Preserve
+# pkg-config's paired Darwin linker option as one flag instead.
+string(REGEX REPLACE "-framework;([^;]+)" "-framework \\1"
+ _TSFILE_PKGCONFIG_FLAGS "${_TSFILE_PKGCONFIG_FLAGS}")
+add_library(tsfile_pkgconfig INTERFACE)
+target_compile_options(tsfile_pkgconfig INTERFACE ${TsFile_CFLAGS})
+target_link_libraries(tsfile_pkgconfig INTERFACE ${_TSFILE_PKGCONFIG_FLAGS})
+set(CMAKE_BUILD_RPATH ${TsFile_LIBRARY_DIRS})
+
+add_executable(tsfile_pkgconfig_consumer main.c)
+target_link_libraries(tsfile_pkgconfig_consumer PRIVATE tsfile_pkgconfig)
+
+add_executable(tsfile_pkgconfig_cpp_consumer main.cc)
+target_link_libraries(tsfile_pkgconfig_cpp_consumer PRIVATE tsfile_pkgconfig)
diff --git a/cpp/cmake/tests/projects/PkgConfigConsumer/main.c b/cpp/cmake/tests/projects/PkgConfigConsumer/main.c
new file mode 100644
index 000000000..05a1566f7
--- /dev/null
+++ b/cpp/cmake/tests/projects/PkgConfigConsumer/main.c
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+#include <tsfile/cwrapper/tsfile_cwrapper.h>
+
+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
new file mode 100644
index 000000000..3ffba2bef
--- /dev/null
+++ b/cpp/cmake/tests/projects/PkgConfigConsumer/main.cc
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+#include <tsfile/common/path.h>
+#include <tsfile/file/write_file.h>
+#include <tsfile/reader/tsfile_reader.h>
+#include <tsfile/writer/tsfile_table_writer.h>
+
+#include <cstdio>
+#include <vector>
+
+static int round_trip(common::CompressionType compression) {
+ const char* path = "pkgconfig-consumer.tsfile";
+ {
+ storage::WriteFile file;
+ int flags = O_WRONLY | O_CREAT | O_TRUNC;
+#ifdef _WIN32
+ flags |= O_BINARY;
+#endif
+ if (file.create(path, flags, 0666) != common::E_OK) return 1;
+ storage::TableSchema schema(
+ "table1", {common::ColumnSchema("value", common::INT64, compression,
+ common::PLAIN,
+ common::ColumnCategory::FIELD)});
+ storage::TsFileTableWriter writer(&file, &schema);
+ storage::Tablet tablet("table1", {"value"}, {common::INT64},
+ {common::ColumnCategory::FIELD}, 16);
+ for (int row = 0; row < 16; ++row) {
+ tablet.add_timestamp(row, row);
+ tablet.add_value(row, "value", static_cast<int64_t>(row * 7));
+ }
+ if (writer.write_table(tablet) != common::E_OK ||
+ writer.close() != common::E_OK)
+ return 2;
+ }
+ storage::TsFileReader reader;
+ if (reader.open(path) != common::E_OK) return 3;
+ storage::ResultSet* result = nullptr;
+ if (reader.query("table1", {"value"}, 0, 100, result) != common::E_OK)
+ return 4;
+ bool has_next = false;
+ int rows = 0;
+ int code = common::E_OK;
+ while ((code = result->next(has_next)) == common::E_OK && has_next) {
+ if (result->get_value<int64_t>(1) != rows ||
+ result->get_value<int64_t>(2) != rows * 7)
+ return 5;
+ ++rows;
+ }
+ result->close();
+ reader.close();
+ std::remove(path);
+ return code == common::E_OK && rows == 16 ? 0 : 6;
+}
+
+int main() {
+ storage::libtsfile_init();
+ // The inline Path constructor and the out-of-line device parser must
+ // agree, including identifiers whose raw bytes are replaced only by CLI
+ // output formatting. Different exported macros used to violate this.
+ const std::string device_name = "root.device\xff";
+ const std::string measurement = "value\xe1\x80";
+ storage::StringArrayDeviceID device(device_name);
+ storage::Path path(device_name + "." + measurement);
+ if (*path.device_id_ != device || path.measurement_ != measurement)
+ return 7;
+ std::vector<common::CompressionType> codecs = {common::UNCOMPRESSED};
+#ifdef ENABLE_SNAPPY
+ codecs.push_back(common::SNAPPY);
+#endif
+#ifdef ENABLE_LZ4
+ codecs.push_back(common::LZ4);
+#endif
+#ifdef ENABLE_LZOKAY
+ codecs.push_back(common::LZO);
+#endif
+#ifdef ENABLE_ZLIB
+ codecs.push_back(common::GZIP);
+#endif
+#ifdef ENABLE_ZSTD
+ codecs.push_back(common::ZSTD);
+#endif
+#ifdef ENABLE_LZMA2
+ codecs.push_back(common::LZMA2);
+#endif
+ for (auto codec : codecs) {
+ int code = round_trip(codec);
+ if (code != 0) return code;
+ }
+ return 0;
+}
diff --git a/cpp/pom.xml b/cpp/pom.xml
index d7db7ba01..7a4d960e3 100644
--- a/cpp/pom.xml
+++ b/cpp/pom.xml
@@ -36,7 +36,7 @@
<enable.asan>OFF</enable.asan>
<build.test>ON</build.test>
<tsfile.build.shared>ON</tsfile.build.shared>
- <tsfile.dependency.source>AUTO</tsfile.dependency.source>
+ <tsfile.dependency.source>BUNDLED</tsfile.dependency.source>
<tsfile.dependency.offline>OFF</tsfile.dependency.offline>
<tsfile.dependency.cache>${project.build.directory}/dependency-cache</tsfile.dependency.cache>
<enable.snappy>ON</enable.snappy>
@@ -52,6 +52,24 @@
<enable.lzma2>OFF</enable.lzma2>
<enable.antlr4>ON</enable.antlr4>
<enable.simd>ON</enable.simd>
+ <!--
+ Generate CPack configuration so the same build tree can produce
+ native packages (DEB/RPM/ZIP) without reconfiguring or rebuilding.
+ Off by default: consumers who only need the library should not pay
+ for packaging metadata.
+ -->
+ <enable.cpack>OFF</enable.cpack>
+ <!--
+ Windows native ZIPs use the static MSVC runtime. Maven is the
+ single parameter source, so this option is always passed even
+ though it is a no-op outside an MSVC build.
+ -->
+ <tsfile.msvc.static.runtime>OFF</tsfile.msvc.static.runtime>
+ <!--
+ Optional CMake project pre-include, used by the Windows package
+ job to run CheckStaticMSVCRuntime.cmake during configure.
+ -->
+ <tsfile.project.include/>
</properties>
<build>
<sourceDirectory>${project.basedir}</sourceDirectory>
@@ -105,6 +123,9 @@
<option>-DENABLE_ZSTD=${enable.zstd}</option>
<option>-DENABLE_LZMA2=${enable.lzma2}</option>
<option>-DENABLE_SIMD=${enable.simd}</option>
+ <option>-DTSFILE_ENABLE_CPACK=${enable.cpack}</option>
+ <option>-DTSFILE_MSVC_STATIC_RUNTIME=${tsfile.msvc.static.runtime}</option>
+ <option>-DCMAKE_PROJECT_TsFile_CPP_INCLUDE=${tsfile.project.include}</option>
<!--
The Visual Studio generator additionally
needs -DCMAKE_GENERATOR_PLATFORM (and
@@ -234,6 +255,42 @@
</plugins>
</build>
</profile>
+ <!--
+ Native package jobs supply the complete version identity produced
+ by native_package_versions.py. The profile is activated only when
+ tsfile.archive.version is present; callers that enable CPack
+ directly keep the CMake defaults. All four values must be passed
+ together by packaging jobs.
+ -->
+ <profile>
+ <id>native-package-versions</id>
+ <activation>
+ <property>
+ <name>tsfile.archive.version</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.googlecode.cmake-maven-project</groupId>
+ <artifactId>cmake-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>cmake-generate-test-compile</id>
+ <configuration>
+ <options combine.children="append">
+ <option>-DTSFILE_ARCHIVE_VERSION=${tsfile.archive.version}</option>
+ <option>-DTSFILE_DEBIAN_PACKAGE_VERSION=${tsfile.debian.package.version}</option>
+ <option>-DTSFILE_RPM_PACKAGE_VERSION=${tsfile.rpm.package.version}</option>
+ <option>-DTSFILE_RPM_PACKAGE_RELEASE=${tsfile.rpm.package.release}</option>
+ </options>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
<profile>
<id>with-code-coverage</id>
<properties>
diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt
index 2ab2dd5d6..7fe0eb549 100644
--- a/cpp/src/CMakeLists.txt
+++ b/cpp/src/CMakeLists.txt
@@ -79,6 +79,18 @@ add_subdirectory(reader)
add_subdirectory(utils)
add_subdirectory(writer)
+# Object libraries need the directory-level project dependencies on older
+# CMake versions, but those dependencies are implementation details of the
+# shared library and must not become exported consumer requirements. Keep the
+# platform thread target in the directory scope for its public link interface,
+# then link codecs explicitly as PRIVATE below.
+get_property(_TSFILE_SRC_LINK_LIBRARIES DIRECTORY PROPERTY LINK_LIBRARIES)
+if (ENABLE_THREADS)
+ set_property(DIRECTORY PROPERTY LINK_LIBRARIES Threads::Threads)
+else ()
+ set_property(DIRECTORY PROPERTY LINK_LIBRARIES "")
+endif ()
+
set(_TSFILE_OBJECT_TARGETS
common_obj
compress_obj
@@ -114,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})
@@ -125,35 +141,120 @@ if (${COV_ENABLED})
else()
set(COV_LINK_LIB -lgcov)
endif()
- if (CMAKE_VERSION VERSION_LESS "3.12")
- target_link_libraries(tsfile ${COV_LINK_LIB})
- else()
- target_link_libraries(tsfile ${_TSFILE_OBJECT_TARGETS} ${COV_LINK_LIB})
- endif()
+ target_link_libraries(tsfile PRIVATE ${COV_LINK_LIB})
else()
message("Disable code cov...")
- if (NOT CMAKE_VERSION VERSION_LESS "3.12")
- target_link_libraries(tsfile ${_TSFILE_OBJECT_TARGETS})
- endif()
endif()
+# Linking object libraries directly makes static-library exports depend on
+# private, non-exported object targets. Add their object files as sources
+# instead; this works on the CMake 3.11 baseline and keeps the install target
+# self-contained for both shared and static builds.
+foreach (_TSFILE_OBJECT_TARGET IN LISTS _TSFILE_OBJECT_TARGETS)
+ target_sources(tsfile PRIVATE
+ $<TARGET_OBJECTS:${_TSFILE_OBJECT_TARGET}>)
+endforeach ()
+
+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)
unset(_TSFILE_OBJECT_TARGETS)
add_dependencies(tsfile utils_obj encoding_obj)
+set_property(DIRECTORY PROPERTY LINK_LIBRARIES
+ "${_TSFILE_SRC_LINK_LIBRARIES}")
+unset(_TSFILE_SRC_LINK_LIBRARIES)
+
if (TSFILE_BUILD_SHARED)
- set(LIBTSFILE_PROJECT_VERSION ${TsFile_CPP_VERSION})
- set(LIBTSFILE_SO_VERSION ${TsFile_CPP_VERSION})
+ # Keep development suffixes out of the install name and SONAME. The ABI
+ # epoch changes only for incompatible binary interface changes.
+ set(LIBTSFILE_PROJECT_VERSION ${TSFILE_PACKAGE_VERSION})
+ set(LIBTSFILE_SO_VERSION ${TSFILE_ABI_VERSION})
set_target_properties(tsfile PROPERTIES VERSION ${LIBTSFILE_PROJECT_VERSION})
set_target_properties(tsfile PROPERTIES SOVERSION ${LIBTSFILE_SO_VERSION})
endif()
+add_library(TsFile::tsfile ALIAS tsfile)
+
+target_include_directories(tsfile PUBLIC
+ $<BUILD_INTERFACE:${LIBRARY_INCLUDE_DIR}>
+ $<BUILD_INTERFACE:${PROJECT_INCLUDE_DIR}>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/tsfile>)
+if (ENABLE_SIMD AND TSFILE_SIMDE_INCLUDE_ROOT)
+ # Public TsFile headers include SIMDe directly. Copy the resolved header
+ # tree into the install prefix so consumers do not need the build host's
+ # system or dependency-cache paths.
+ target_include_directories(tsfile PUBLIC
+ $<BUILD_INTERFACE:${TSFILE_SIMDE_INCLUDE_ROOT}>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
+endif ()
+if (ENABLE_ANTLR4 AND TSFILE_ANTLR4_INCLUDE_ROOT)
+ target_include_directories(tsfile PUBLIC
+ $<BUILD_INTERFACE:${TSFILE_ANTLR4_INCLUDE_ROOT}>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
+endif ()
+if (ENABLE_ANTLR4 AND TSFILE_UTF8CPP_INCLUDE_ROOT)
+ target_include_directories(tsfile PUBLIC
+ $<BUILD_INTERFACE:${TSFILE_UTF8CPP_INCLUDE_ROOT}>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
+endif ()
+target_compile_features(tsfile PUBLIC cxx_std_11)
+# ENABLE_ANTLR4 selects the parser dependency but is not a compile definition
+# on the SDK object targets. Exporting it would change inline Path definitions
+# only in consumers, violating the ODR (and rejecting stored raw identifiers).
+set(_TSFILE_PUBLIC_FEATURE_DEFINITIONS "")
+foreach (_TSFILE_PUBLIC_FEATURE
+ ENABLE_MEM_STAT
+ ENABLE_SNAPPY
+ ENABLE_LZ4
+ ENABLE_LZOKAY
+ ENABLE_ZLIB
+ ENABLE_GZIP
+ ENABLE_ZSTD
+ ENABLE_LZMA2
+ ENABLE_THREADS
+ ENABLE_SIMD)
+ if (${_TSFILE_PUBLIC_FEATURE})
+ list(APPEND _TSFILE_PUBLIC_FEATURE_DEFINITIONS
+ ${_TSFILE_PUBLIC_FEATURE})
+ endif ()
+endforeach ()
+if (NOT "${_TSFILE_PUBLIC_FEATURE_DEFINITIONS}" STREQUAL "")
+ target_compile_definitions(tsfile INTERFACE
+ ${_TSFILE_PUBLIC_FEATURE_DEFINITIONS})
+endif ()
+unset(_TSFILE_PUBLIC_FEATURE)
+unset(_TSFILE_PUBLIC_FEATURE_DEFINITIONS)
+
+if (TSFILE_BUILD_SHARED)
+ # A shared library already resolves its private codec and object-library
+ # 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
# on Unix. A static library is an ARCHIVE on every platform. Cover all three so
# the install step works for either library type.
install(TARGETS tsfile
- RUNTIME DESTINATION ${LIBRARY_OUTPUT_PATH}
- LIBRARY DESTINATION ${LIBRARY_OUTPUT_PATH}
- ARCHIVE DESTINATION ${LIBRARY_OUTPUT_PATH})
+ EXPORT TsFileTargets
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ COMPONENT runtime
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ COMPONENT runtime
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ COMPONENT development)
+
+install(EXPORT TsFileTargets
+ FILE TsFileTargets.cmake
+ NAMESPACE TsFile::
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/TsFile
+ COMPONENT development)
diff --git a/cpp/src/utils/util_define.h b/cpp/src/utils/util_define.h
index 564581a11..33784e152 100644
--- a/cpp/src/utils/util_define.h
+++ b/cpp/src/utils/util_define.h
@@ -120,14 +120,14 @@ typedef int mode_t;
#endif // __GNUC__ >= 4
/* ======== nullptr ======== */
-#if __cplusplus < 201103L
+#if __cplusplus < 201103L && !defined(_MSC_VER)
#ifndef nullptr
#define nullptr NULL
#endif
#define OVERRIDE
#else
#define OVERRIDE override
-#endif // __cplusplus < 201103L
+#endif // __cplusplus < 201103L && !defined(_MSC_VER)
/* ======== cache line ======== */
#ifndef CACHE_LINE_SIZE
@@ -147,13 +147,13 @@ typedef int mode_t;
* @msg should be a single word (use -/_ to concat)
* such as This_should_be_TRUE
*/
-#if __cplusplus < 201103L
+#if __cplusplus < 201103L && !defined(_MSC_VER)
// TODO only define this when DEBUG
#define STATIC_ASSERT(cond, msg) \
typedef char static_assertion_##msg[(cond) ? 1 : -1] __attribute__((unused))
#else
#define STATIC_ASSERT(cond, msg) static_assert((cond), #msg)
-#endif // __cplusplus < 201103L
+#endif // __cplusplus < 201103L && !defined(_MSC_VER)
/* ======== atomic operation ========
*
diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt
index 3c3da441b..fa160bc61 100644
--- a/cpp/test/CMakeLists.txt
+++ b/cpp/test/CMakeLists.txt
@@ -19,6 +19,13 @@ under the License.
cmake_minimum_required(VERSION 3.11)
project(TsFile_CPP_TEST)
+# Configuring cpp/test directly validates an already installed SDK. The normal
+# add_subdirectory(test) path continues to test the source-built library.
+if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
+ include("${CMAKE_CURRENT_SOURCE_DIR}/InstalledSDKTests.cmake")
+ return()
+endif ()
+
set(CMAKE_VERBOSE_MAKEFILE ON)
set(GTEST_URL_LIST
@@ -215,6 +222,7 @@ add_test(
NAME ANTLR4DependencyCMakeTest
COMMAND ${CMAKE_COMMAND}
${_TSFILE_DEPENDENCY_TEST_CMAKE_ARGUMENTS}
+ "-DTEST_CMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}"
-DTEST_BINARY_ROOT=${CMAKE_CURRENT_BINARY_DIR}/cmake-tests
-DTEST_ANTLR4_ARCHIVE=${TSFILE_ANTLR4_ARCHIVE_RESOLVED}
-P ${CMAKE_SOURCE_DIR}/cmake/tests/ANTLR4DependencyTest.cmake)
diff --git a/cpp/test/InstalledSDKTests.cmake b/cpp/test/InstalledSDKTests.cmake
new file mode 100644
index 000000000..facd3ae7e
--- /dev/null
+++ b/cpp/test/InstalledSDKTests.cmake
@@ -0,0 +1,63 @@
+#[[
+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.
+]]
+# This entry point compiles test sources only. All TsFile headers, feature
+# definitions and libraries must come from the installed CMake package.
+find_package(TsFile CONFIG REQUIRED)
+find_package(GTest CONFIG REQUIRED)
+find_package(Threads REQUIRED)
+
+enable_testing()
+
+# Reuse existing API regression tests without linking CLI implementation objects,
+# codec implementation targets, or a library compiled with ENABLE_TEST hooks.
+add_executable(TsFile_Installed_Test
+ cwrapper/cwrapper_test.cc
+ cwrapper/cwrapper_public_writer_test.cc
+ cwrapper/cwrapper_metadata_test.cc
+ cwrapper/cwrapper_properties_test.cc
+ cwrapper/c_release_test.cc
+ cwrapper/query_by_row_cwrapper_test.cc
+ reader/table_view/tsfile_reader_table_test.cc
+ reader/table_view/tsfile_reader_table_batch_test.cc
+ reader/table_view/tsfile_table_query_by_row_test.cc
+ reader/tree_view/tsfile_reader_tree_test.cc
+ reader/tree_view/tsfile_tree_query_by_row_test.cc
+ writer/table_view/tsfile_writer_table_test.cc
+ writer/table_view/tsfile_parallel_tablet_write_regression_test.cc
+ writer/tsfile_writer_test.cc
+ writer/tsfile_properties_test.cc)
+target_compile_features(TsFile_Installed_Test PRIVATE cxx_std_14)
+target_link_libraries(TsFile_Installed_Test PRIVATE
+ TsFile::tsfile GTest::gtest_main Threads::Threads)
+
+# Windows resolves DLLs beside the executable before searching PATH. Copy the
+# imported SDK library, rather than relying on a build machine's library path.
+get_target_property(_TSFILE_INSTALLED_LIBRARY_TYPE TsFile::tsfile TYPE)
+if (WIN32 AND _TSFILE_INSTALLED_LIBRARY_TYPE STREQUAL "SHARED_LIBRARY")
+ add_custom_command(TARGET TsFile_Installed_Test POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ $<TARGET_FILE:TsFile::tsfile>
+ $<TARGET_FILE_DIR:TsFile_Installed_Test>
+ VERBATIM)
+endif ()
+unset(_TSFILE_INSTALLED_LIBRARY_TYPE)
+
+include(GoogleTest)
+gtest_discover_tests(TsFile_Installed_Test DISCOVERY_TIMEOUT 120
+ PROPERTIES TIMEOUT 120)
diff --git a/cpp/third_party/CMakeLists.txt b/cpp/third_party/CMakeLists.txt
index f7a8851b5..203b93c34 100755
--- a/cpp/third_party/CMakeLists.txt
+++ b/cpp/third_party/CMakeLists.txt
@@ -35,6 +35,12 @@ if (ENABLE_ANTLR4)
"${TSFILE_UTF8CPP_SOURCE_DIR}/source")
add_library(tsfile_antlr4_bundled STATIC ${_TSFILE_ANTLR4_SOURCES})
+ set(TSFILE_ANTLR4_INCLUDE_ROOT
+ "${TSFILE_ANTLR4_SOURCE_DIR}/runtime/Cpp/runtime/src"
+ CACHE INTERNAL "Resolved ANTLR4 include root" FORCE)
+ set(TSFILE_UTF8CPP_INCLUDE_ROOT
+ "${TSFILE_UTF8CPP_SOURCE_DIR}/source"
+ CACHE INTERNAL "Resolved utf8cpp include root" FORCE)
set_target_properties(tsfile_antlr4_bundled PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_STANDARD 11)
@@ -93,6 +99,17 @@ if (ENABLE_ANTLR4)
target_link_libraries(tsfile_antlr4 INTERFACE
tsfile_antlr4_bundled)
else ()
+ set(TSFILE_ANTLR4_INCLUDE_ROOT
+ "${TSFILE_ANTLR4_SYSTEM_INCLUDE_DIR}"
+ CACHE INTERNAL "Resolved ANTLR4 include root" FORCE)
+ if (TARGET utf8cpp)
+ get_target_property(_TSFILE_UTF8CPP_INCLUDE_ROOT utf8cpp
+ INTERFACE_INCLUDE_DIRECTORIES)
+ set(TSFILE_UTF8CPP_INCLUDE_ROOT
+ "${_TSFILE_UTF8CPP_INCLUDE_ROOT}"
+ CACHE INTERNAL "Resolved utf8cpp include root" FORCE)
+ unset(_TSFILE_UTF8CPP_INCLUDE_ROOT)
+ endif ()
target_link_libraries(tsfile_antlr4 INTERFACE
${TSFILE_ANTLR4_SYSTEM_TARGET})
if (TSFILE_ANTLR4_SYSTEM_INCLUDE_DIR)
@@ -449,9 +466,15 @@ if (ENABLE_SIMD)
add_library(tsfile_simde INTERFACE)
if (TSFILE_SIMDE_SOURCE STREQUAL "BUNDLED")
+ set(TSFILE_SIMDE_INCLUDE_ROOT "${TSFILE_SIMDE_SOURCE_DIR}"
+ CACHE INTERNAL "Resolved SIMDe include root" FORCE)
target_include_directories(tsfile_simde INTERFACE
"${TSFILE_SIMDE_SOURCE_DIR}")
else ()
+ get_target_property(TSFILE_SIMDE_INCLUDE_ROOT simde::simde
+ INTERFACE_INCLUDE_DIRECTORIES)
+ set(TSFILE_SIMDE_INCLUDE_ROOT "${TSFILE_SIMDE_INCLUDE_ROOT}"
+ CACHE INTERNAL "Resolved SIMDe include root" FORCE)
target_link_libraries(tsfile_simde INTERFACE simde::simde)
endif ()
add_library(TsFile::SIMDe ALIAS tsfile_simde)
diff --git a/cpp/third_party/README.md b/cpp/third_party/README.md
index 74ae8c65e..f1bf7fd44 100644
--- a/cpp/third_party/README.md
+++ b/cpp/third_party/README.md
@@ -31,7 +31,9 @@ must be updated together.
The build provides explicit `SYSTEM`, `BUNDLED`, and `AUTO` dependency source
modes. ANTLR4, Snappy, LZ4, lzokay, SIMDe, zlib, Zstandard, and liblzma have
migrated to these modes. In `BUNDLED` mode, source is downloaded into the build
-cache rather than kept in this repository.
+cache rather than kept in this repository. `BUNDLED` is the default for both
+CMake and Maven and is used by every native release package, including Homebrew.
+`AUTO` and `SYSTEM` remain explicit options for custom source builds.
LZMA2 support is disabled by default so this optional codec does not raise the
CMake 3.11 baseline for long-lived industrial and legacy toolchains. Set
diff --git a/cpp/tools/CMakeLists.txt b/cpp/tools/CMakeLists.txt
index 00bc20ea0..5fbb34346 100644
--- a/cpp/tools/CMakeLists.txt
+++ b/cpp/tools/CMakeLists.txt
@@ -59,4 +59,19 @@ set_target_properties(tsfile_cli PROPERTIES
OUTPUT_NAME tsfile-cli
RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
-install(TARGETS tsfile_cli RUNTIME DESTINATION bin)
+# CMake supplies the build-tree RPATH. Only installed binaries use the path
+# from the installed bindir to libdir, including lib64 and multiarch layouts.
+file(RELATIVE_PATH _TSFILE_CLI_LIBDIR
+ "${CMAKE_INSTALL_FULL_BINDIR}" "${CMAKE_INSTALL_FULL_LIBDIR}")
+if (APPLE)
+ set_target_properties(tsfile_cli PROPERTIES
+ INSTALL_RPATH "@loader_path/${_TSFILE_CLI_LIBDIR}")
+elseif (UNIX)
+ set_target_properties(tsfile_cli PROPERTIES
+ INSTALL_RPATH "\$ORIGIN/${_TSFILE_CLI_LIBDIR}")
+endif ()
+unset(_TSFILE_CLI_LIBDIR)
+
+install(TARGETS tsfile_cli
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ COMPONENT tools)
diff --git a/packaging/README.md b/packaging/README.md
new file mode 100644
index 000000000..f6282d491
--- /dev/null
+++ b/packaging/README.md
@@ -0,0 +1,428 @@
+<!--
+
+ 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.
+
+-->
+
+# TsFile C++ Packages
+
+The C++ installation contract is shared by all package formats. It installs
+the library, the current compatibility header closure, `TsFileConfig.cmake`,
+pkg-config metadata, the CLI, and Apache license files below one prefix.
+
+The header closure mirrors the include relationships used by the current C++
+implementation. It is intentionally broader than the long-term stable public
+API; a separate API cleanup will narrow it in a future version.
+
+## Manual artifact workflow
+
+`Build native release artifacts` (`.github/workflows/native-packages.yml`) is
+the authoritative native packaging workflow. In the Apache TsFile repository's GitHub Actions tab,
+select this workflow, choose **Run workflow**, select the branch containing the
+commit to build, and dispatch it manually. It runs only on `workflow_dispatch`,
+with read-only repository permissions; pushes and pull requests do not trigger
+native packaging.
+
+A successful run builds the C++ core through Maven, stages one SDK per
+platform and compatibility baseline, and then produces Ubuntu amd64 and arm64 DEBs, AlmaLinux
+x86_64 and aarch64 RPMs, ARM64 and Intel Homebrew release bottles with a
+merged Formula, Windows x86_64 and ARM64 SDK/CLI ZIPs, and Python wheels for Linux and macOS
+on both architectures plus Windows x86_64. Every wheel platform covers CPython
+3.9 through 3.14. The Go consumer and Python extensions reuse staged SDKs; C++
+is built once per compatibility baseline. The job combines these into
+`tsfile-native-packages-<archive-version>` with `manifest.json` and
+`SHA256SUMS`. Download this final artifact from the workflow
+run; both intermediate and final artifacts are retained for 14 days.
+
+Publishing is a separate, manual step. This workflow only builds, tests, and
+uploads GitHub Actions artifacts. It has no publishing credentials and does
+not upload to JFrog, sign packages, update `latest`, create tags or releases, or
+promote a build to an official release.
+
+For a source version of `2.5.0`, the workflow generates release-format
+metadata: SDK archives and Python wheels use `2.5.0`, DEBs use `2.5.0-1`,
+and RPMs use `2.5.0-1.el9`. This workflow accepts only `MAJOR.MINOR.PATCH`
+source versions; development and prerelease versions fail during preparation.
+The bundle records the exact source repository and commit in `manifest.json`.
+Final bundle assembly rejects wheels whose version differs from the release
+version. C++ publication targets always use the `release` directory or Debian
+component. The DEB package name `tsfile-dev` denotes SDK headers and is retained
+for release packages.
+
+## Portable archive
+
+Build a relocatable SDK and portable archive through the Maven entry point:
+
+```bash
+./mvnw -Pwith-cpp package \
+ -Dbuild.test=OFF \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Denable.cpack=ON
+cmake --install cpp/target/build --prefix cpp/build/sdk-root
+cpack --config cpp/target/build/CPackConfig.cmake -G TGZ -B cpp/build/packages
+```
+
+The staged `cpp/build/sdk-root` contains a standard prefix layout and can be
+unpacked at `/usr/local`, a user directory, or a relocated application prefix.
+
+## Native Linux packages
+
+DEB and RPM packages must be built in the target distribution environment so
+CPack can run the native dependency scanner (`dpkg-shlibdeps` or
+`rpmbuild`). Build the same Maven-driven tree once, then select the native
+generator in the same job:
+
+```bash
+./mvnw -Pwith-cpp package \
+ -Dbuild.test=OFF \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Denable.cpack=ON
+cpack -G DEB --config cpp/target/build/CPackConfig.cmake -B cpp/build/packages
+# or: cpack -G RPM --config cpp/target/build/CPackConfig.cmake -B cpp/build/packages
+```
+
+All release packages use `BUNDLED`, including DEB, RPM and Homebrew builds.
+Pinned, SHA-256-verified third-party sources are compiled into the native library,
+so installing TsFile does not require separate Snappy, LZ4 or other codec libraries.
+Operating-system runtimes remain dynamic dependencies. `AUTO` and `SYSTEM` are
+optional custom source-build policies, not release packaging policies.
+
+The manual `Build native release artifacts` workflow is the authoritative
+native package build. Its Linux jobs create `tsfile`, `tsfile-dev`, and
+`tsfile-tools` DEBs on native Ubuntu 22.04 amd64 and arm64 runners, and
+`tsfile`, `tsfile-devel`, and `tsfile-tools` RPMs on native AlmaLinux 9
+x86_64 and aarch64 runners. Fresh Ubuntu 22.04, Ubuntu 24.04, and AlmaLinux 9
+containers install the matching packages and verify both `tsfile-cli` and an
+external CMake consumer. The workflow uploads intermediate artifacts for 14
+days and does not publish packages.
+
+## Test an installed C/C++ SDK
+
+Configure `cpp/test` directly to compile existing C/C++ API regression tests
+against an installed package or extracted SDK. This standalone entry point uses
+`find_package(TsFile CONFIG REQUIRED)` and links `TsFile::tsfile`; it does not
+configure or compile the TsFile library or CLI. Install a CMake-configured
+GoogleTest package for the same architecture and compatible compiler runtime.
+
+```bash
+cmake -S cpp/test -B installed-sdk-tests \
+ -DCMAKE_PREFIX_PATH=/absolute/path/to/sdk
+cmake --build installed-sdk-tests --config Release --parallel
+ctest --test-dir installed-sdk-tests -C Release --output-on-failure
+```
+
+For a system-installed DEB or RPM development package, omit
+`CMAKE_PREFIX_PATH`. If GoogleTest is installed under a separate prefix, pass
+its config directory with `-DGTest_DIR=/absolute/path/to/lib/cmake/GTest`.
+Only the contents of `cpp/test` are needed on the test machine; neither
+`cpp/src` nor `cpp/tools` is required. Run CTest sequentially because some
+reused tests write fixed filenames in their working directory.
+
+The standalone suite reuses 15 existing test files covering C bindings,
+table/tree reads and writes, row queries, Arrow batches, properties, recovery,
+and parallel writes. Existing disabled tests remain disabled. The complete
+source-built suite continues to run through `-Pwith-cpp verify`; internal
+allocator/fault-injection tests, codec implementation tests and CLI object
+unit tests remain there. Every native installation job now runs this suite
+through `packaging/tests/cpp_installed_tests.py`, in addition to the small
+consumer check. The runner builds a SHA-256-verified GoogleTest 1.12.1 dependency
+and links the test executable to the installed `TsFile::tsfile` target. Windows
+tests and GoogleTest use the same static MSVC runtime as the packaged DLL.
+
+## CLI installation acceptance
+
+`packaging/tests/cli_package_smoke.py` executes an installed CLI against the
+four resources in the current source checkout's `python/tests/resources`:
+
+| Resource | Model | Expected rows |
+| --- | --- | ---: |
+| `simple_tree.tsfile` | Tree | 105 |
+| `simple_table_t1.tsfile` | Table | 60 |
+| `simple_table_t2.tsfile` | Table | 40 |
+| `table_with_time_column.tsfile` | Table with a named time column | 25 |
+
+The workflow takes these files from the same checked-out commit used for the
+build and distributes one `native-cli-test-data` artifact. The script verifies each file's SHA-256
+before using it. It checks successful exit status, schema/types, metadata,
+row/null counts, selected data sums and BOOLEAN values, head/offset/limit,
+exported CSV versus `cat`, and sketch availability. Expected data facts come
+from `python/tests/test_load_tsfile_from_iotdb.py`.
+
+```bash
+python3 packaging/tests/cli_package_smoke.py \
+ --cli /absolute/path/to/installed/bin/tsfile-cli \
+ --resources /absolute/path/to/source/python/tests/resources
+```
+
+Only Python's standard library is required. Every CLI invocation has a
+30-second timeout; generated exports live in a temporary directory. The script
+compares data semantics and requires no captured output baselines or CLI test
+implementation objects.
+
+Fresh Ubuntu 22.04/24.04 and AlmaLinux 9 containers install DEB/RPM packages
+before running the checks. Separate Windows x86_64/ARM64 jobs unpack the built
+ZIPs and run with the package's binary directory and Windows system paths.
+Separate macOS Intel/ARM64 jobs merge the downloaded bottle metadata into a
+trusted temporary tap, seed Homebrew's verified bottle cache, and install by
+formula name. This supports Homebrew's package-path restrictions while keeping
+SHA-256 verification and the `poured_from_bottle` receipt check. Acceptance jobs check out only
+the acceptance scripts, existing C++ tests, and Go module. They compile the
+C++ test executable and Go consumers against the installed package; they do not
+compile TsFile or the CLI. Final assembly waits for all installation checks.
+
+## macOS
+
+Homebrew is the native macOS distribution path. The workflow renders the
+release Formula `tsfile` from `packaging/homebrew/tsfile.rb.in`, using the exact
+source repository and full workflow commit. It hashes that source archive,
+uses the C++ release version, and builds and tests the installed C++ consumer
+and CLI before bottling. Its bundled build has no third-party runtime Formula
+dependencies. The generated Formula is shipped as `homebrew/Formula/tsfile.rb`
+in the final artifact; no source URL or checksum needs to be edited after download.
+
+The Formula is keg-only because the SDK ships its dependency header closure;
+linking that closure into Homebrew's shared prefix would collide with headers
+owned by dependencies such as `simde`. Invoke the CLI as
+`$(brew --prefix tsfile)/bin/tsfile-cli`, or add the Formula's `bin` directory
+to `PATH`. The temporary `apache/tsfile` tap used by CI is local to each job;
+a public Git tap is a separate distribution step.
+
+The ARM64 job runs on `macos-latest` and the Intel job on `macos-15-intel`.
+Their `native-homebrew-macos-arm64` and `native-homebrew-macos-x86_64`
+intermediate artifacts remain separate until merge. Each job also archives its
+installed Formula prefix as a standalone SDK artifact,
+`native-sdk-macos-arm64` or `native-sdk-macos-x86_64`, so macOS C++ consumers
+do not have to depend on Homebrew. The SDK archive is checked with the CLI,
+pkg-config, and an installed CMake consumer before upload.
+`verify_macos_runtime.py` inspects Mach-O load commands in the installed bottle
+and staged SDK, allowing only macOS system libraries and the packaged TsFile
+library. Downloaded SDK tests repeat this check before executing any binaries,
+so preinstalled Homebrew dependencies cannot conceal an external reference.
+The merge job checks that
+both source Formula files match, merges both platform JSON files with Homebrew,
+and checks both generated tags and checksums in the resulting Formula.
+
+The `native-homebrew` artifact contains `Formula/tsfile.rb` and `bottles/`
+with both bottle tarballs and both JSON metadata files. When downloaded into
+`homebrew/`, this gives the final `homebrew/Formula/` and `homebrew/bottles/`
+layout. The configured future bottle root is
+`https://packages.apache.org/artifactory/tsfile/homebrew/release/versions/<homebrew-version>/bottles`.
+Homebrew generates local tarballs with a double dash before the version but
+requests a single dash in HTTP URLs. After merge, the workflow renames each
+tarball to the JSON `filename` (URL-decoded) and updates its `local_filename`
+to match, preserving its checksum. The workflow only uploads GitHub Actions
+artifacts for 14 days; it does not publish to that root or update `latest`.
+
+## Windows
+
+The manual workflow builds native x86_64 and ARM64 Release packages with
+Visual Studio 2022 and bundled dependencies. Each job stages and tests the CLI
+plus an external CMake SDK consumer before producing
+`tsfile-<archive-version>-windows-x86_64.zip` or
+`tsfile-<archive-version>-windows-arm64.zip`. Each archive is one combined ZIP
+rather than one archive per CPack component, and contains the runtime,
+development files, and tools under a relocatable prefix. 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 an MSVC Release toolset compatible with the
+compiler used to build that architecture 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 the ZIPs as `native-windows-msvc-x86_64` and
+`native-windows-msvc-arm64` 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 artifacts. Windows ARM uses the
+native ARM64 compiler and produces unsigned binary archives; no MSIX/AppX
+signing step is part of this workflow.
+
+## 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
+```
+
+## Go SDK verification
+
+`packaging/tests/go_installed_tests.py` runs the Go module's own `go vet`,
+`go test`, and `go test -race` after native package installation on Linux,
+macOS and Windows, on both architectures. The jobs use Go 1.26. Windows ARM64
+runs `vet` and normal tests; Go does not support its race detector on that
+platform, so the runner explicitly reports that one check as not applicable.
+
+On Unix, the runner obtains both installed include roots, feature definitions,
+and the library search path from the package's relocatable `libtsfile.pc`.
+On Windows, a SHA-256-verified LLVM-MinGW 20261006 toolchain compiles cgo's C
+bridge. The runner reads the installed MSVC DLL's C exports and generates a
+MinGW import library, without rebuilding or replacing the DLL. This exercises
+the same manually installed native package that Go users consume.
+
+## Python wheels
+
+The native workflow is the single packaging entry point. It replaces the former
+`.github/workflows/wheels.yml` workflow; pushing an `rc/**` branch no longer
+starts a separate wheel build. Dispatch the native workflow to build the full
+package bundle, including all 30 Python wheels:
+
+| Wheel platform | CPython | C++ SDK source |
+| --- | --- | --- |
+| manylinux_2_28 x86_64 | 3.9–3.14 | One bundled build inside the manylinux container |
+| manylinux_2_28 aarch64 | 3.9–3.14 | One bundled build inside the native ARM container |
+| macOS 12 x86_64 | 3.9–3.14 | One bundled build with deployment target 12.0 |
+| macOS 12 arm64 | 3.9–3.14 | One bundled build with deployment target 12.0 |
+| Windows x86_64 | 3.9–3.14 | The existing native MSVC SDK |
+
+Each SDK installs `share/tsfile/tsfile-build-config.json`, recording its compiler,
+configuration, resolved MSVC runtime and runtime library filename. The same
+file is generated beside the library in a CMake build tree. `setup.py` reads
+this metadata to select `/MT`, `/MD` or their Debug variants automatically;
+no Python build environment variable duplicates the SDK's CRT selection.
+Rebuild the native SDK if this metadata is missing.
+
+Wheels contain one TsFile runtime library under its runtime filename, the
+Python extensions, Python code and license/notice texts. SDK headers, Cython build declarations,
+import libraries and static archives remain build inputs and are excluded from
+the wheel. Release wheel builds strip Unix debug/local symbols before
+`auditwheel` or `delocate` changes library load commands, and macOS binaries
+are signed again after stripping. SDKs and in-place development builds retain
+their symbols. `packaging/scripts/check_python_wheel.py` rejects duplicate
+native libraries, build-only files and ELF debug sections in every built and
+downloaded wheel; fresh installation jobs then run the complete pytest suite.
+
+`cibuildwheel==3.4.1` builds the Python extensions and handles platform wheel
+repair. Its `before-all` hook invokes `packaging/scripts/build_python_sdk.sh`
+once per Unix compatibility baseline. Every Python ABI then links against that
+SDK through `TSFILE_CPP_BUILD`. The manylinux hook exports the SDK archive
+through cibuildwheel's `/host` mount so the subsequent upload runs on the host.
+The Windows job downloads the existing MSVC SDK
+and copies its DLLs from `bin/` into a Python build view under `lib/`; it does
+not compile another C++ core. The native Windows SDK enables LZMA2, matching
+the wheel compatibility SDKs.
+
+The wheel SDKs remain distinct from the Ubuntu/AlmaLinux SDKs and Homebrew
+SDKs: an Ubuntu SDK does not by itself establish manylinux_2_28 compatibility,
+and a Homebrew SDK may require newer macOS. All of these SDKs bundle their
+third-party dependencies. The four
+Unix wheel SDKs use bundled dependencies and are included in the final bundle
+as `native-sdk-manylinux_2_28-{x86_64,aarch64}` and
+`native-sdk-macos12-{x86_64,arm64}`. This also makes them available for direct
+download and other consumers of the same compatibility baseline.
+
+Wheel jobs build, repair and upload artifacts. Separate `test-python-unix`
+and `test-python-windows` jobs download those artifacts on fresh runners, one
+job for each of the 30 platform/CPython combinations. They check out only
+`python/tests` and the installation test runner; neither the Python package
+implementation nor C++ sources are present.
+
+`packaging/tests/python_installed_tests.py` selects exactly one downloaded
+wheel matching the expected package version and interpreter ABI, installs it
+with binary-only dependencies, and runs the existing full `pytest` suite.
+Imports use Python's isolated mode from a temporary working directory, and the
+runner checks the installed module path and distribution version. Individual
+tests have a 120-second timeout with stack traces; import and whole-suite
+timeouts prevent native hangs from consuming an entire job. Each combination
+uploads a JUnit result artifact. Existing test-suite platform skips remain
+visible in the report.
+
+Final assembly rejects missing Python ABIs, duplicate ABIs, incompatible
+platform tags, and inconsistent wheel versions. Wheel manifest entries include
+the Python ABI, wheel version, and the SDK platform used to build the extension.
+
+## Installed package test matrix
+
+| Installed package | Architectures | Separate installation tests |
+| --- | --- | --- |
+| DEB on Ubuntu 22.04 and 24.04 | x86_64, ARM64 | C++ API regressions, CLI resource checks, Go vet/test/race |
+| RPM on AlmaLinux 9 | x86_64, ARM64 | C++ API regressions, CLI resource checks, Go vet/test/race |
+| Homebrew bottle on macOS | x86_64, ARM64 | C++ API regressions, CLI resource checks, Go vet/test/race |
+| Windows ZIP | x86_64, ARM64 | C++ API regressions, CLI resource checks, Go vet/test; race on x86_64 |
+| Python wheel | Linux/macOS x86_64/ARM64, Windows x86_64 | Full installed-wheel pytest for every CPython 3.9–3.14 ABI |
+| Standalone SDK archives | All 12 distribution and compatibility SDK variants | Download, extract to a new prefix, then C++ API regressions, CLI resource checks and Go tests |
+
+All native tests consume downloaded, installed packages. CLI acceptance uses
+the source checkout's Python resources and does not use CLI golden/E2E files.
+The C++ regression suite compiles existing API tests through `find_package`;
+internal tests requiring a special test build remain in source CI. The release
+bundle is gated on every installation-test matrix completing successfully.
+
+Each native installation job also downloads its separate SDK archive and runs
+`packaging/tests/sdk_archive_tests.py` against an extracted prefix. Its C++ test
+executable links that prefix's `TsFile::tsfile`; CLI and Go consume the same
+extracted files. The existing GoogleTest dependency is reused, while the SDK's
+tests are rebuilt and rerun. Separate `test-compatibility-sdk` jobs cover the
+four bundled manylinux/macOS 12 SDK archives produced for wheel builds. These
+jobs check out consumer tests only and also gate final assembly. SDK archive
+test reports are retained alongside native-package test reports.
+
+## Final native package bundle
+
+Only after the Ubuntu amd64 and arm64 DEB installation tests, AlmaLinux x86_64
+and aarch64 RPM installation tests, macOS and Windows C++/CLI/Go installation
+tests, all standalone and compatibility SDK archive tests, all 30 installed-wheel
+pytest jobs, macOS SDK and Homebrew bottle merge,
+and Windows x86_64 and ARM64 SDK/CLI builds
+all succeed, the workflow assembles
+`tsfile-native-packages-<archive-version>`. The final
+GitHub Actions artifact retains the platform SDKs, including both Linux ARM
+archives, both macOS archives, the Windows ARM64 archive, the Python
+wheels and their compatibility SDKs, the DEBs, RPMs, merged Formula and bottles,
+and Windows ZIPs in their package-family layouts for 14 days. It also contains a
+sorted `SHA256SUMS` and `manifest.json` with the source identity, generated
+versions, byte sizes, SHA-256 values, and the JFrog repository, immutable target
+path, and properties required for later manual publication.
+
+The final job has no publishing credentials and does not upload to JFrog. A
+maintainer can later use the manifest to upload DEBs to `tsfile-debian` with the
+recorded Debian coordinates, RPMs to `tsfile-rpm/release/el9/<architecture>`,
+Homebrew formula/bottles to `tsfile/homebrew/release/versions/<version>`, SDKs to
+`tsfile/sdk/release/versions/<version>`, Windows ZIPs to
+`tsfile/windows/release/versions/<version>`. DEBs use the `release` component
+and `pool/release/<platform>` paths. Python wheels retain their platform,
+version, size and checksum metadata, but have no Apache repository or target
+path: they are published separately through the project's PyPI process.
diff --git a/packaging/cmake/InstalledGTest/CMakeLists.txt b/packaging/cmake/InstalledGTest/CMakeLists.txt
new file mode 100644
index 000000000..3f310ace3
--- /dev/null
+++ b/packaging/cmake/InstalledGTest/CMakeLists.txt
@@ -0,0 +1,36 @@
+#[[
+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.
+]]
+
+cmake_minimum_required(VERSION 3.15)
+if (POLICY CMP0135)
+ cmake_policy(SET CMP0135 NEW)
+endif ()
+project(TsFileInstalledTestDependencies LANGUAGES CXX)
+
+# Build the test framework only; the TsFile implementation comes from the
+# downloaded and installed package. Match the Windows SDK's static CRT.
+set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
+set(INSTALL_GTEST ON CACHE BOOL "" FORCE)
+set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
+set(gtest_force_shared_crt OFF CACHE BOOL "" FORCE)
+include(FetchContent)
+FetchContent_Declare(googletest
+ URL "https://github.com/google/googletest/archive/refs/tags/release-1.12.1.zip"
+ URL_HASH SHA256=24564e3b712d3eb30ac9a85d92f7d720f60cc0173730ac166f27dda7fed76cb2)
+FetchContent_MakeAvailable(googletest)
diff --git a/packaging/cmake/PythonSDK.cmake b/packaging/cmake/PythonSDK.cmake
new file mode 100644
index 000000000..872d30803
--- /dev/null
+++ b/packaging/cmake/PythonSDK.cmake
@@ -0,0 +1,24 @@
+#[[
+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.
+]]
+
+# setup.py consumes an SDK with include/ and lib/ on every platform.
+set(CMAKE_INSTALL_LIBDIR "lib" CACHE PATH "Python SDK library directory" FORCE)
+if (APPLE)
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Wheel deployment target" FORCE)
+endif ()
diff --git a/packaging/homebrew/tsfile.rb.in b/packaging/homebrew/tsfile.rb.in
new file mode 100644
index 000000000..ad4c3aa29
--- /dev/null
+++ b/packaging/homebrew/tsfile.rb.in
@@ -0,0 +1,61 @@
+# 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.
+
+class Tsfile < Formula
+ desc "Columnar storage library for time series data"
+ homepage "https://tsfile.apache.org/"
+ url "https://github.com/@SOURCE_REPOSITORY@/archive/@GIT_SHA@.tar.gz"
+ version "@HOMEBREW_VERSION@"
+ sha256 "@SOURCE_SHA256@"
+ license "Apache-2.0"
+ keg_only "the SDK installs its dependency header closure"
+
+@BOTTLE_BLOCK@
+
+ depends_on "cmake" => :build
+
+ def install
+ args = %W[
+ -DBUILD_TEST=OFF
+ -DBUILD_TOOLS=ON
+ -DENABLE_LZMA2=OFF
+ -DTSFILE_DEPENDENCY_SOURCE=BUNDLED
+ -DTSFILE_ENABLE_NATIVE_ARCH=OFF
+ -DCMAKE_INSTALL_RPATH=#{rpath}
+ ]
+
+ system "cmake", "-S", "cpp", "-B", "build", *args, *std_cmake_args
+ system "cmake", "--build", "build"
+ system "cmake", "--install", "build"
+ end
+
+ test do
+ (testpath / "test.cpp").write <<~CPP
+ #include <tsfile/cwrapper/tsfile_cwrapper.h>
+
+ 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;
+ }
+ CPP
+
+ system ENV.cxx, "-std=c++11", "test.cpp", "-I#{include}", "-L#{lib}",
+ "-Wl,-rpath,#{lib}", "-ltsfile", "-o", "test"
+ system "./test"
+ system bin / "tsfile-cli", "--version"
+ end
+end
diff --git a/packaging/scripts/assemble_native_packages.py b/packaging/scripts/assemble_native_packages.py
new file mode 100644
index 000000000..c18a3f079
--- /dev/null
+++ b/packaging/scripts/assemble_native_packages.py
@@ -0,0 +1,542 @@
+#!/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.
+
+"""Assemble native package artifacts into a checksummed publishable bundle."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import re
+import shutil
+from pathlib import Path
+from typing import Sequence
+
+DEB_PROPERTIES = {
+ "deb.distribution": ["jammy", "noble"],
+ "deb.component": ["release"],
+}
+DEB_PLATFORMS = {"ubuntu22.04-amd64", "ubuntu22.04-arm64"}
+RPM_PLATFORMS = {"almalinux9-x86_64", "almalinux9-aarch64"}
+WINDOWS_PLATFORMS = {"windows-msvc-x86_64", "windows-msvc-arm64"}
+REQUIRED_VERSIONS = {"archive_version", "homebrew_version"}
+REQUIRED_SOURCE = {"commit", "repository"}
+SDK_PLATFORMS = {
+ "ubuntu22.04-amd64",
+ "ubuntu22.04-arm64",
+ "almalinux9-x86_64",
+ "almalinux9-aarch64",
+ "macos-arm64",
+ "macos-x86_64",
+ "windows-msvc-x86_64",
+ "windows-msvc-arm64",
+ "manylinux_2_28-x86_64",
+ "manylinux_2_28-aarch64",
+ "macos12-x86_64",
+ "macos12-arm64",
+}
+PYTHON_PLATFORMS = {
+ "manylinux_2_28-x86_64",
+ "manylinux_2_28-aarch64",
+ "macos12-x86_64",
+ "macos12-arm64",
+ "windows-msvc-x86_64",
+}
+PYTHON_TAGS = {"cp39", "cp310", "cp311", "cp312", "cp313", "cp314"}
+
+
+def _wheel_metadata(filename: str, platform: str) -> dict[str, str]:
+ match = re.fullmatch(r"tsfile-([^-]+)-(cp\d+)-(cp\d+)-([^-]+)\.whl", filename)
+ if match is None:
+ raise ValueError(f"unsupported Python wheel filename: {filename}")
+ version, python_tag, abi_tag, platform_tags = match.groups()
+ if python_tag not in PYTHON_TAGS or abi_tag != python_tag:
+ raise ValueError(f"unsupported Python wheel ABI: {filename}")
+ tags = platform_tags.split(".")
+ if platform.startswith("manylinux_2_28-"):
+ architecture = platform.removeprefix("manylinux_2_28-")
+ valid = f"manylinux_2_28_{architecture}" in tags and all(
+ re.fullmatch(rf"manylinux(?:_\d+_\d+|\d+)_{architecture}", tag)
+ for tag in tags
+ )
+ elif platform.startswith("macos12-"):
+ architecture = platform.removeprefix("macos12-")
+ matches = [
+ re.fullmatch(rf"macosx_(\d+)_(\d+)_{architecture}", tag) for tag in tags
+ ]
+ valid = all(
+ item is not None and tuple(map(int, item.groups())) <= (12, 0)
+ for item in matches
+ )
+ else:
+ valid = tags == ["win_amd64"]
+ if not valid:
+ raise ValueError(f"incompatible Python wheel tags for {platform}: {filename}")
+ return {
+ "pythonTag": python_tag,
+ "abiTag": abi_tag,
+ "wheelVersion": version,
+ "sdkPlatform": platform,
+ }
+
+
+def _validate_metadata(values: dict[str, str], required: set[str], name: str) -> None:
+ if not isinstance(values, dict) or not required.issubset(values):
+ raise ValueError(f"{name} must contain {sorted(required)}")
+ if any(
+ not isinstance(key, str) or not isinstance(value, str)
+ for key, value in values.items()
+ ):
+ raise ValueError(f"{name} must map strings to strings")
+
+
+def _validate_versions(versions: dict[str, str]) -> None:
+ _validate_metadata(versions, REQUIRED_VERSIONS, "versions")
+ for name in REQUIRED_VERSIONS:
+ value = versions[name]
+ if (
+ not value.strip()
+ or value in {".", ".."}
+ or "/" in value
+ or "\\" in value
+ or "\x00" in value
+ ):
+ raise ValueError(f"version {name} must be a safe path component")
+ if not re.fullmatch(
+ r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", value
+ ):
+ raise ValueError(
+ f"version {name} must be a release version MAJOR.MINOR.PATCH"
+ )
+ release_version = versions["archive_version"]
+ for name in (
+ "homebrew_version",
+ "base_version",
+ "logical_version",
+ "python_version",
+ ):
+ if name in versions and versions[name] != release_version:
+ raise ValueError(
+ f"version {name} must match release version {release_version}"
+ )
+
+
+def _validate_source(source: dict[str, str]) -> None:
+ _validate_metadata(source, REQUIRED_SOURCE, "source")
+ for name in REQUIRED_SOURCE:
+ if not source[name].strip():
+ raise ValueError(f"source {name} must be nonempty")
+
+
+def _input_files(input_dir: Path) -> list[Path]:
+ if not input_dir.is_dir() or input_dir.is_symlink():
+ raise ValueError(f"input directory is not a real directory: {input_dir}")
+ files: list[Path] = []
+ for directory, directories, names in os.walk(input_dir, followlinks=False):
+ directory_path = Path(directory)
+ for child in directories:
+ path = directory_path / child
+ if path.is_symlink():
+ raise ValueError(f"symbolic link input is not allowed: {path}")
+ for name in names:
+ path = directory_path / name
+ if path.is_symlink():
+ raise ValueError(f"symbolic link input is not allowed: {path}")
+ if not path.is_file():
+ raise ValueError(f"undeclared file type: {path}")
+ if path.stat().st_size == 0:
+ raise ValueError(f"empty file input is not allowed: {path}")
+ files.append(path)
+ return sorted(files, key=lambda path: path.relative_to(input_dir).as_posix())
+
+
+def _artifact_description(
+ input_dir: Path, source_path: Path, versions: dict[str, str]
+) -> tuple[Path, dict[str, object]]:
+ relative_path = source_path.relative_to(input_dir)
+ parts = relative_path.parts
+ filename = source_path.name
+ if len(parts) >= 2 and parts[0] == "deb" and filename.endswith(".deb"):
+ match = re.search(r"_(amd64|arm64)\.deb$", filename)
+ if not match:
+ raise ValueError(f"unsupported DEB architecture: {filename}")
+ architecture = match.group(1)
+ platform = f"ubuntu22.04-{architecture}"
+ target = Path("deb") / platform / filename
+ return target, {
+ "family": "deb",
+ "platform": platform,
+ "targetRepository": "tsfile-debian",
+ "targetPath": f"pool/release/{platform}/{filename}",
+ "properties": {
+ **DEB_PROPERTIES,
+ "deb.architecture": [architecture],
+ },
+ }
+ if len(parts) >= 2 and parts[0] == "rpm" and filename.endswith(".rpm"):
+ match = re.search(r"\.(x86_64|aarch64)\.rpm$", filename)
+ if not match:
+ raise ValueError(f"unsupported RPM architecture: {filename}")
+ architecture = match.group(1)
+ platform = f"almalinux9-{architecture}"
+ target = Path("rpm") / platform / filename
+ return target, {
+ "family": "rpm",
+ "platform": platform,
+ "targetRepository": "tsfile-rpm",
+ "targetPath": f"release/el9/{architecture}/{filename}",
+ "properties": {},
+ }
+ if (
+ len(parts) == 3
+ and parts[:2] == ("homebrew", "Formula")
+ and filename == "tsfile.rb"
+ ):
+ target = Path(*parts)
+ return target, {
+ "family": "homebrew",
+ "platform": "homebrew",
+ "targetRepository": "tsfile",
+ "targetPath": f"homebrew/release/versions/{versions['homebrew_version']}/{target.as_posix()[len('homebrew/'):]}",
+ "properties": {},
+ }
+ if (
+ len(parts) == 3
+ and parts[:2] == ("homebrew", "bottles")
+ and (filename.endswith(".tar.gz") or filename.endswith(".bottle.json"))
+ ):
+ target = Path(*parts)
+ return target, {
+ "family": "homebrew",
+ "platform": "homebrew",
+ "targetRepository": "tsfile",
+ "targetPath": f"homebrew/release/versions/{versions['homebrew_version']}/{target.as_posix()[len('homebrew/'):]}",
+ "properties": {},
+ }
+ if len(parts) == 3 and parts[0] == "sdk" and filename.endswith(".tar.gz"):
+ platform = parts[1]
+ if platform not in SDK_PLATFORMS:
+ raise ValueError(f"unsupported SDK platform: {platform}")
+ target = Path("sdk") / platform / filename
+ return target, {
+ "family": "sdk",
+ "platform": platform,
+ "targetRepository": "tsfile",
+ "targetPath": (
+ f"sdk/release/versions/{versions['archive_version']}/{platform}/{filename}"
+ ),
+ "properties": {},
+ }
+ if len(parts) == 3 and parts[0] == "python" and filename.endswith(".whl"):
+ platform = parts[1]
+ if platform not in PYTHON_PLATFORMS:
+ raise ValueError(f"unsupported Python wheel platform: {platform}")
+ target = Path("python/wheels") / platform / filename
+ return target, {
+ "family": "python",
+ "platform": platform,
+ **_wheel_metadata(filename, platform),
+ }
+ if len(parts) >= 2 and parts[0] == "windows" and filename.endswith(".zip"):
+ match = re.search(r"-windows-(x86_64|arm64)\.zip$", filename)
+ if not match:
+ raise ValueError(f"unsupported Windows ZIP architecture: {filename}")
+ platform = f"windows-msvc-{match.group(1)}"
+ target = Path("windows") / filename
+ return target, {
+ "family": "windows",
+ "platform": platform,
+ "targetRepository": "tsfile",
+ "targetPath": f"windows/release/versions/{versions['archive_version']}/{filename}",
+ "properties": {},
+ }
+ raise ValueError(f"undeclared file type: {relative_path.as_posix()}")
+
+
+def _require_complete_families(
+ planned: list[tuple[Path, Path, dict[str, object]]],
+) -> None:
+ platforms_by_family = {
+ family: {
+ metadata["platform"]
+ for _, _, metadata in planned
+ if metadata["family"] == family
+ }
+ for family in ("deb", "rpm", "windows")
+ }
+ for family, required_platforms in (
+ ("deb", DEB_PLATFORMS),
+ ("rpm", RPM_PLATFORMS),
+ ("windows", WINDOWS_PLATFORMS),
+ ):
+ missing_platforms = required_platforms - platforms_by_family[family]
+ if missing_platforms:
+ raise ValueError(
+ f"missing {family.upper()} inputs for: "
+ + ", ".join(sorted(missing_platforms))
+ )
+ sdk_platforms = {
+ metadata["platform"]
+ for _, _, metadata in planned
+ if metadata["family"] == "sdk"
+ }
+ missing_sdk_platforms = SDK_PLATFORMS - sdk_platforms
+ if missing_sdk_platforms:
+ raise ValueError(
+ "missing SDK inputs for: " + ", ".join(sorted(missing_sdk_platforms))
+ )
+ python_platforms = {
+ metadata["platform"]
+ for _, _, metadata in planned
+ if metadata["family"] == "python"
+ }
+ missing_python_platforms = PYTHON_PLATFORMS - python_platforms
+ if missing_python_platforms:
+ raise ValueError(
+ "missing Python wheel inputs for: "
+ + ", ".join(sorted(missing_python_platforms))
+ )
+ wheel_versions: set[str] = set()
+ for platform in sorted(PYTHON_PLATFORMS):
+ wheels = [
+ metadata
+ for _, _, metadata in planned
+ if metadata["family"] == "python" and metadata["platform"] == platform
+ ]
+ tags = [wheel["pythonTag"] for wheel in wheels]
+ missing = PYTHON_TAGS - set(tags)
+ if missing:
+ raise ValueError(
+ f"missing Python wheel ABIs for {platform}: "
+ + ", ".join(sorted(missing))
+ )
+ if len(tags) != len(set(tags)):
+ raise ValueError(f"duplicate Python wheel ABI for {platform}")
+ wheel_versions.update(wheel["wheelVersion"] for wheel in wheels)
+ if len(wheel_versions) != 1:
+ raise ValueError("Python wheels must all have the same version")
+ homebrew_paths = {
+ target.as_posix()
+ for _, target, metadata in planned
+ if metadata["family"] == "homebrew"
+ }
+ if "homebrew/Formula/tsfile.rb" not in homebrew_paths:
+ raise ValueError("missing Homebrew Formula input")
+ if not any(path.endswith(".tar.gz") for path in homebrew_paths):
+ raise ValueError("missing Homebrew Bottle input")
+ if not any(path.endswith(".bottle.json") for path in homebrew_paths):
+ raise ValueError("missing Homebrew Bottle metadata input")
+
+
+def _sha256(path: Path) -> str:
+ digest = hashlib.sha256()
+ with path.open("rb") as source:
+ for block in iter(lambda: source.read(1024 * 1024), b""):
+ digest.update(block)
+ return digest.hexdigest()
+
+
+def _artifact_paths(output_dir: Path) -> set[Path]:
+ paths: set[Path] = set()
+ for path in output_dir.rglob("*"):
+ if path.is_symlink():
+ raise ValueError(f"symbolic link output is not allowed: {path}")
+ if path.is_file() and path.name not in {"SHA256SUMS", "manifest.json"}:
+ paths.add(path.relative_to(output_dir))
+ return paths
+
+
+def verify_bundle(output_dir: Path) -> None:
+ """Verify manifest hashes, sizes, and path-sorted checksums in a bundle."""
+ output_dir = Path(output_dir)
+ if not output_dir.is_dir() or output_dir.is_symlink():
+ raise ValueError(f"bundle directory is not a real directory: {output_dir}")
+ manifest_path = output_dir / "manifest.json"
+ checksums_path = output_dir / "SHA256SUMS"
+ if (
+ not manifest_path.is_file()
+ or manifest_path.is_symlink()
+ or not checksums_path.is_file()
+ or checksums_path.is_symlink()
+ ):
+ raise ValueError("bundle must contain real manifest.json and SHA256SUMS files")
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ artifacts = manifest.get("artifacts") if isinstance(manifest, dict) else None
+ if not isinstance(artifacts, list):
+ raise ValueError("manifest artifacts must be a list")
+
+ paths: set[Path] = set()
+ for artifact in artifacts:
+ if not isinstance(artifact, dict):
+ raise ValueError("manifest artifacts must be objects")
+ relative_text = artifact.get("path")
+ if not isinstance(relative_text, str):
+ raise ValueError("manifest artifact path must be a string")
+ relative = Path(relative_text)
+ if (
+ relative.is_absolute()
+ or relative.as_posix() != relative_text
+ or ".." in relative.parts
+ ):
+ raise ValueError(f"unsafe manifest path: {relative_text}")
+ if relative in paths:
+ raise ValueError(f"duplicate manifest path: {relative_text}")
+ paths.add(relative)
+ path = output_dir / relative
+ if not path.is_file() or path.is_symlink():
+ raise ValueError(f"missing artifact: {relative_text}")
+ sha256 = artifact.get("sha256")
+ if not isinstance(sha256, str) or re.fullmatch(r"[0-9a-f]{64}", sha256) is None:
+ raise ValueError(f"invalid artifact SHA-256: {relative_text}")
+ if _sha256(path) != sha256:
+ raise ValueError(f"hash mismatch: {relative_text}")
+ if path.stat().st_size != artifact.get("size"):
+ raise ValueError(f"size mismatch: {relative_text}")
+
+ actual_checksums = checksums_path.read_text(encoding="utf-8").splitlines()
+ expected_checksums = [
+ f"{artifact['sha256']} {artifact['path']}"
+ for artifact in sorted(artifacts, key=lambda artifact: artifact["path"])
+ ]
+ if actual_checksums != expected_checksums:
+ raise ValueError("SHA256SUMS does not match path-sorted manifest artifacts")
+ if _artifact_paths(output_dir) != paths:
+ raise ValueError("bundle files do not match manifest artifacts")
+
+
+def assemble(
+ input_dir: Path, output_dir: Path, versions: dict[str, str], source: dict[str, str]
+) -> dict[str, object]:
+ """Copy approved artifacts and return their deterministic publication manifest."""
+ _validate_versions(versions)
+ _validate_source(source)
+ input_dir = Path(input_dir)
+ output_dir = Path(output_dir)
+ files = _input_files(input_dir)
+ planned: list[tuple[Path, Path, dict[str, object]]] = []
+ target_paths: set[Path] = set()
+ for source_path in files:
+ target, metadata = _artifact_description(input_dir, source_path, versions)
+ if target in target_paths:
+ raise ValueError(f"duplicate target path: {target.as_posix()}")
+ target_paths.add(target)
+ planned.append((source_path, target, metadata))
+ _require_complete_families(planned)
+ release_version = versions["archive_version"]
+ for _, _, metadata in planned:
+ if (
+ metadata["family"] == "python"
+ and metadata["wheelVersion"] != release_version
+ ):
+ raise ValueError(
+ f"Python wheel version must match release version {release_version}"
+ )
+ if output_dir.is_symlink():
+ raise ValueError(f"output directory must not be a symbolic link: {output_dir}")
+ if output_dir.exists() and any(output_dir.iterdir()):
+ raise ValueError(f"output directory must be empty: {output_dir}")
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ artifacts: list[dict[str, object]] = []
+ for source_path, target, metadata in sorted(
+ planned, key=lambda item: item[1].as_posix()
+ ):
+ destination = output_dir / target
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copyfile(source_path, destination)
+ artifact = {
+ **metadata,
+ "filename": destination.name,
+ "path": target.as_posix(),
+ "size": destination.stat().st_size,
+ "sha256": _sha256(destination),
+ }
+ artifacts.append(artifact)
+
+ checksums = "".join(
+ f"{artifact['sha256']} {artifact['path']}\n" for artifact in artifacts
+ )
+ (output_dir / "SHA256SUMS").write_text(checksums, encoding="utf-8")
+ manifest: dict[str, object] = {
+ "artifacts": artifacts,
+ "source": source,
+ "versions": versions,
+ }
+ (output_dir / "manifest.json").write_text(
+ json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ return manifest
+
+
+def _argument_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--input-dir", type=Path)
+ parser.add_argument("--output-dir", type=Path)
+ parser.add_argument("--versions-json", type=Path)
+ parser.add_argument("--source-repository")
+ parser.add_argument("--source-commit")
+ parser.add_argument("--verify-bundle", type=Path)
+ return parser
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ """Assemble a bundle from an Actions artifact download directory."""
+ arguments = _argument_parser().parse_args(argv)
+ if arguments.verify_bundle is not None:
+ if any(
+ value is not None
+ for value in (
+ arguments.input_dir,
+ arguments.output_dir,
+ arguments.versions_json,
+ arguments.source_repository,
+ arguments.source_commit,
+ )
+ ):
+ raise ValueError(
+ "--verify-bundle cannot be combined with assembly arguments"
+ )
+ verify_bundle(arguments.verify_bundle)
+ return 0
+ missing = [
+ name
+ for name, value in (
+ ("--input-dir", arguments.input_dir),
+ ("--output-dir", arguments.output_dir),
+ ("--versions-json", arguments.versions_json),
+ ("--source-repository", arguments.source_repository),
+ ("--source-commit", arguments.source_commit),
+ )
+ if value is None
+ ]
+ if missing:
+ raise ValueError(f"missing assembly arguments: {', '.join(missing)}")
+ versions = json.loads(arguments.versions_json.read_text(encoding="utf-8"))
+ assemble(
+ arguments.input_dir,
+ arguments.output_dir,
+ versions,
+ {"repository": arguments.source_repository, "commit": arguments.source_commit},
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/packaging/scripts/build_python_sdk.sh b/packaging/scripts/build_python_sdk.sh
new file mode 100644
index 000000000..888e55a2e
--- /dev/null
+++ b/packaging/scripts/build_python_sdk.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# 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.
+
+# One build per compatibility baseline, reused by every Python ABI.
+set -euo pipefail
+
+platform="${1:?usage: build_python_sdk.sh <platform> <archive-version>}"
+archive_version="${2:?usage: build_python_sdk.sh <platform> <archive-version>}"
+repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$repository_root"
+
+case "$platform" in
+ manylinux_2_28-x86_64|manylinux_2_28-aarch64)
+ test "$(uname -s)" = Linux
+ test "$(uname -m)" = "${platform#manylinux_2_28-}"
+ # This hook runs inside cibuildwheel's manylinux container.
+ yum install -y java-17-openjdk-devel cmake pkgconfig libuuid-devel libblkid-devel
+ export JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
+ export PATH="$JAVA_HOME/bin:$PATH"
+ ;;
+ macos12-x86_64|macos12-arm64)
+ test "$(uname -s)" = Darwin
+ test "$(uname -m)" = "${platform#macos12-}"
+ export MACOSX_DEPLOYMENT_TARGET=12.0
+ export CFLAGS="${CFLAGS:-} -mmacosx-version-min=12.0"
+ export CXXFLAGS="${CXXFLAGS:-} -mmacosx-version-min=12.0"
+ export LDFLAGS="${LDFLAGS:-} -mmacosx-version-min=12.0"
+ ;;
+ *)
+ printf 'Unsupported Python SDK platform: %s\n' "$platform" >&2
+ exit 1
+ ;;
+esac
+
+python3 packaging/scripts/run_maven_with_retry.py -Pwith-cpp package \
+ -Dbuild.type=Release \
+ -Dbuild.test=OFF \
+ -DskipTests \
+ -Dspotless.skip=true \
+ -Dtsfile.version.sync.skip=true \
+ -Dtsfile.dependency.source=BUNDLED \
+ -Denable.lzma2=ON \
+ "-Dtsfile.project.include=$repository_root/packaging/cmake/PythonSDK.cmake"
+
+# A fixed path lets every Python build consume this SDK without rebuilding C++.
+sdk_root="$repository_root/python-sdk"
+test ! -e "$sdk_root"
+cmake --install cpp/target/build --prefix "$sdk_root"
+test -f "$sdk_root/include/tsfile/cwrapper/tsfile_cwrapper.h"
+test -d "$sdk_root/lib"
+if [[ "$(uname -s)" == Darwin ]]; then
+ python3 packaging/scripts/verify_macos_runtime.py "$sdk_root"
+fi
+"$sdk_root/bin/tsfile-cli" --version
+sdk_output="${TSFILE_PYTHON_SDK_EXPORT:-$repository_root/python-sdk-output}"
+mkdir -p "$sdk_output"
+tar -czf "$sdk_output/tsfile-sdk-$platform-$archive_version.tar.gz" python-sdk
diff --git a/packaging/scripts/check_python_wheel.py b/packaging/scripts/check_python_wheel.py
new file mode 100644
index 000000000..aad85b8a2
--- /dev/null
+++ b/packaging/scripts/check_python_wheel.py
@@ -0,0 +1,118 @@
+# 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 build-only files, repeated native libraries and ELF debug sections."""
+
+import argparse
+import hashlib
+from pathlib import Path
+import struct
+from zipfile import ZipFile
+
+
+def elf_debug_sections(data):
+ if data[:4] != b"\x7fELF":
+ return []
+ if data[4] != 2:
+ raise ValueError("Expected a 64-bit ELF for the release wheel matrix")
+ endian = "<" if data[5] == 1 else ">"
+ offset = struct.unpack_from(endian + "Q", data, 40)[0]
+ size, count, names_index = struct.unpack_from(endian + "HHH", data, 58)
+ sections = [
+ struct.unpack_from(endian + "IIQQQQIIQQ", data, offset + i * size)
+ for i in range(count)
+ ]
+ names_header = sections[names_index]
+ names = data[names_header[4] : names_header[4] + names_header[5]]
+ result = []
+ for section in sections:
+ name = names[section[0] :].split(b"\0", 1)[0].decode("ascii")
+ if name.startswith((".debug_", ".zdebug_")):
+ result.append(name)
+ return result
+
+
+def check_wheel(path):
+ with ZipFile(path) as wheel:
+ files = [info for info in wheel.infolist() if not info.is_dir()]
+ names = [info.filename for info in files]
+ if len(names) != len(set(names)):
+ raise ValueError(f"Duplicate ZIP entries in {path}")
+ forbidden = [
+ name
+ for name in names
+ if name.startswith("tsfile/include/")
+ or Path(name).suffix.lower()
+ in {
+ ".h",
+ ".hpp",
+ ".pxd",
+ ".pxi",
+ ".pyx",
+ ".a",
+ ".lib",
+ ".pdb",
+ ".cc",
+ ".cpp",
+ }
+ ]
+ if forbidden:
+ raise ValueError(f"Build-only files in {path}: {forbidden[:8]}")
+ native = [
+ name
+ for name in names
+ if Path(name).name.startswith("libtsfile")
+ or Path(name).name == "tsfile.dll"
+ ]
+ if len(native) != 1:
+ raise ValueError(f"Expected one TsFile runtime library in {path}: {native}")
+ hashes = {}
+ for name in names:
+ if not (name.endswith((".so", ".dylib", ".dll", ".pyd")) or ".so." in name):
+ continue
+ data = wheel.read(name)
+ debug = elf_debug_sections(data)
+ if debug:
+ raise ValueError(f"ELF debug sections in {path}/{name}: {debug}")
+ digest = hashlib.sha256(data).hexdigest()
+ if digest in hashes:
+ raise ValueError(f"Repeated binary in {path}: {hashes[digest]}, {name}")
+ hashes[digest] = name
+ raw = sum(info.file_size for info in files)
+ print(
+ f"PASS: {Path(path).name}: {Path(path).stat().st_size} bytes compressed, "
+ f"{raw} bytes installed; one runtime library, no build-only files or ELF debug sections"
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--wheelhouse", type=Path)
+ parser.add_argument("wheels", type=Path, nargs="*")
+ arguments = parser.parse_args()
+ wheels = arguments.wheels
+ if arguments.wheelhouse:
+ wheels += sorted(arguments.wheelhouse.glob("*.whl"))
+ if not wheels:
+ raise ValueError("No wheels found for content validation")
+ for wheel in wheels:
+ check_wheel(wheel)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/scripts/native_package_versions.py b/packaging/scripts/native_package_versions.py
new file mode 100644
index 000000000..f8bc68a48
--- /dev/null
+++ b/packaging/scripts/native_package_versions.py
@@ -0,0 +1,95 @@
+#!/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.
+
+"""Build release versions for native TsFile packages."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+from pathlib import Path
+from typing import Sequence
+
+_SOURCE_VERSION_RE = re.compile(
+ r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$"
+)
+_CMAKE_VERSION_RE = re.compile(
+ r"^\s*set\(\s*TsFile_CPP_VERSION\s+(?P<version>[^\s)]+)\s*\)\s*(?:#.*)?$",
+ re.MULTILINE,
+)
+
+
+def read_cpp_version(path: Path) -> str:
+ """Read the release version declared by CMake."""
+ match = _CMAKE_VERSION_RE.search(path.read_text(encoding="utf-8"))
+ if match is None or _SOURCE_VERSION_RE.fullmatch(match.group("version")) is None:
+ raise ValueError(
+ f"{path} must declare a release TsFile_CPP_VERSION as MAJOR.MINOR.PATCH"
+ )
+ return match.group("version")
+
+
+def build_versions(source_version: str) -> dict[str, str]:
+ """Create matching archive, Python, DEB, RPM, and Homebrew release versions."""
+ if _SOURCE_VERSION_RE.fullmatch(source_version) is None:
+ raise ValueError("source_version must be a release version MAJOR.MINOR.PATCH")
+ return {
+ "base_version": source_version,
+ "python_version": source_version,
+ "logical_version": source_version,
+ "deb_version": f"{source_version}-1",
+ "rpm_version": source_version,
+ "rpm_release": "1.el9",
+ "archive_version": source_version,
+ "homebrew_version": source_version,
+ }
+
+
+def _argument_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--cmake-file", type=Path, required=True)
+ parser.add_argument("--json-out", type=Path, required=True)
+ parser.add_argument("--github-output", type=Path)
+ return parser
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ """Write release versions to JSON and, optionally, GitHub outputs."""
+ arguments = _argument_parser().parse_args(argv)
+ versions = build_versions(read_cpp_version(arguments.cmake_file))
+ 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+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"
+ ).encode("utf-8")
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/packaging/scripts/render_homebrew_formula.py b/packaging/scripts/render_homebrew_formula.py
new file mode 100644
index 000000000..d13c9065c
--- /dev/null
+++ b/packaging/scripts/render_homebrew_formula.py
@@ -0,0 +1,85 @@
+# 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.
+
+"""Render the release Formula using an immutable source identity."""
+
+import argparse
+import re
+from pathlib import Path
+
+VALUE_PATTERNS = {
+ "SOURCE_REPOSITORY": r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+",
+ "GIT_SHA": r"[0-9a-f]{40}",
+ "HOMEBREW_VERSION": r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)",
+ "SOURCE_SHA256": r"[0-9a-f]{64}",
+}
+TOKEN = re.compile(r"@([A-Z0-9_]+)@")
+
+
+def render_formula(
+ template: str, values: dict[str, str], bottle_block: str = ""
+) -> str:
+ """Replace declared tokens without interpreting Ruby braces or at-signs."""
+ if values.keys() != VALUE_PATTERNS.keys():
+ raise ValueError(
+ "Formula values must contain exactly the declared source fields"
+ )
+ for name, pattern in VALUE_PATTERNS.items():
+ if not re.fullmatch(pattern, values[name]):
+ raise ValueError(f"Invalid Formula value: {name}")
+ replacements = {**values, "BOTTLE_BLOCK": bottle_block}
+ unknown = set(TOKEN.findall(template)) - replacements.keys()
+ if unknown:
+ raise ValueError(f"Unknown Formula placeholders: {sorted(unknown)}")
+ if not bottle_block:
+ # Omit an empty standalone block and its following separator line.
+ template = re.sub(r"(?m)^[ \t]*@BOTTLE_BLOCK@\n(?:[ \t]*\n)?", "", template)
+ rendered = TOKEN.sub(lambda match: replacements[match.group(1)], template)
+ if TOKEN.search(rendered):
+ raise ValueError("Unresolved Formula placeholder")
+ if len(re.findall(r"^\s*bottle\s+do\b", rendered, re.MULTILINE)) > 1:
+ raise ValueError("Duplicate bottle blocks")
+ return rendered
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--template", type=Path, required=True)
+ parser.add_argument("--repository", required=True)
+ parser.add_argument("--git-sha", required=True)
+ parser.add_argument("--version", required=True)
+ parser.add_argument("--source-sha256", required=True)
+ parser.add_argument("--bottle-block", type=Path)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ values = {
+ "SOURCE_REPOSITORY": args.repository,
+ "GIT_SHA": args.git_sha,
+ "HOMEBREW_VERSION": args.version,
+ "SOURCE_SHA256": args.source_sha256,
+ }
+ bottle_block = (
+ args.bottle_block.read_text(encoding="utf-8") if args.bottle_block else ""
+ )
+ args.output.write_text(
+ render_formula(args.template.read_text(encoding="utf-8"), values, bottle_block),
+ encoding="utf-8",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/scripts/run_maven_with_retry.py b/packaging/scripts/run_maven_with_retry.py
new file mode 100644
index 000000000..ccc950cc5
--- /dev/null
+++ b/packaging/scripts/run_maven_with_retry.py
@@ -0,0 +1,72 @@
+#!/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
+#
+# 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.
+
+"""Retry Maven only when artifact downloads fail with transient transport errors."""
+
+from collections import deque
+import os
+from pathlib import Path
+import re
+import subprocess
+import sys
+import time
+
+ROOT = Path(__file__).resolve().parents[2]
+TRANSIENT_DOWNLOAD = re.compile(
+ r"Could not transfer artifact .*?(?:Connection reset|Connection timed out|"
+ r"Read timed out|Remote host terminated the handshake|"
+ r"status code: (?:408|429|500|502|503|504)(?:\D|$))",
+ re.IGNORECASE,
+)
+
+
+def run_maven(arguments, *, root=ROOT, command_prefix=None):
+ if command_prefix is None:
+ command_prefix = [str(root / ("mvnw.cmd" if os.name == "nt" else "mvnw"))]
+ for attempt in range(1, 4):
+ # Maven caches failed resolutions. Force a new resolution on retry.
+ options = ["-U"] if attempt > 1 and "-U" not in arguments else []
+ command = [*command_prefix, *options, *arguments]
+ print("+ " + subprocess.list2cmdline(command), flush=True)
+ tail = deque(maxlen=200)
+ with subprocess.Popen(
+ command,
+ cwd=root,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ errors="replace",
+ ) as process:
+ for line in process.stdout:
+ print(line, end="", flush=True)
+ tail.append(line)
+ returncode = process.wait()
+ if returncode == 0:
+ return 0
+ errors = "".join(line for line in tail if "[ERROR]" in line)
+ if attempt == 3 or TRANSIENT_DOWNLOAD.search(errors) is None:
+ return returncode
+ print(
+ f"Transient Maven artifact download failure; retry {attempt}/2", flush=True
+ )
+ time.sleep(attempt * 2)
+ raise AssertionError("Unreachable Maven retry state")
+
+
+if __name__ == "__main__":
+ raise SystemExit(run_maven(sys.argv[1:]))
diff --git a/packaging/scripts/verify_macos_runtime.py b/packaging/scripts/verify_macos_runtime.py
new file mode 100644
index 000000000..22a833dfe
--- /dev/null
+++ b/packaging/scripts/verify_macos_runtime.py
@@ -0,0 +1,124 @@
+#!/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
+#
+# 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.
+
+"""Reject macOS packages that load external third-party dynamic libraries."""
+
+import argparse
+from pathlib import Path
+import re
+import subprocess
+
+LOAD_COMMANDS = {
+ "LC_LOAD_DYLIB",
+ "LC_LOAD_WEAK_DYLIB",
+ "LC_REEXPORT_DYLIB",
+ "LC_LAZY_LOAD_DYLIB",
+ "LC_LOAD_UPWARD_DYLIB",
+}
+MACHO_MAGICS = {
+ bytes.fromhex(value)
+ for value in (
+ "feedface",
+ "cefaedfe",
+ "feedfacf",
+ "cffaedfe",
+ "cafebabe",
+ "bebafeca",
+ "cafebabf",
+ "bfbafeca",
+ )
+}
+
+
+def parse_dependencies(output):
+ imports = set()
+ for block in re.split(r"(?m)^Load command \d+\s*$", output):
+ command = re.search(r"^\s*cmd (LC_\w+)\s*$", block, re.M)
+ if command is None or command[1] not in LOAD_COMMANDS:
+ continue
+ name = re.search(r"^\s*name (.+?) \(offset \d+\)\s*$", block, re.M)
+ if name is None:
+ raise ValueError("otool reported a dylib load command without a name")
+ imports.add(name[1])
+ if not imports:
+ raise ValueError("otool did not report any dylib dependencies")
+ return imports
+
+
+def validate_dependencies(binary, imports, prefix):
+ binary = Path(binary).resolve()
+ prefix = Path(prefix).resolve()
+ for name in sorted(imports):
+ if name.startswith(("/usr/lib/", "/System/Library/")):
+ continue
+ # BUNDLED codecs must not remain separate dylibs, even when installed
+ # on the runner. The only non-system dylib is TsFile itself.
+ if not re.fullmatch(r"libtsfile(?:\.[0-9]+)*\.dylib", Path(name).name):
+ raise ValueError(
+ f"{binary.name} needs an external third-party dylib: {name}"
+ )
+ if name.startswith("@rpath/"):
+ suffix = name.removeprefix("@rpath/")
+ candidates = [prefix / "lib" / suffix, prefix / suffix]
+ elif name.startswith("@loader_path/"):
+ candidates = [binary.parent / name.removeprefix("@loader_path/")]
+ elif name.startswith("@executable_path/") and binary.name == "tsfile-cli":
+ candidates = [binary.parent / name.removeprefix("@executable_path/")]
+ elif Path(name).is_absolute():
+ candidates = [Path(name)]
+ else:
+ candidates = []
+ if not any(
+ path.is_file() and prefix in path.resolve().parents for path in candidates
+ ):
+ raise ValueError(f"{binary.name} needs an unshipped TsFile dylib: {name}")
+
+
+def verify_prefix(prefix):
+ prefix = Path(prefix).resolve()
+ binaries = set()
+ for path in prefix.rglob("*"):
+ if path.is_file():
+ with path.open("rb") as stream:
+ if stream.read(4) in MACHO_MAGICS:
+ binaries.add(path.resolve())
+ if not binaries:
+ raise ValueError(f"No Mach-O binaries found under {prefix}")
+ for binary in sorted(binaries):
+ output = subprocess.run(
+ ["otool", "-l", str(binary)],
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ ).stdout
+ imports = parse_dependencies(output)
+ validate_dependencies(binary, imports, prefix)
+ print(
+ f"{binary.name}: all dylib dependencies are macOS system libraries or packaged TsFile"
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("prefix", type=Path)
+ verify_prefix(parser.parse_args().prefix)
+
+
+if __name__ == "__main__":
+ main()
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..f1be69aec
--- /dev/null
+++ b/packaging/tests/check_native_workflow.rb
@@ -0,0 +1,295 @@
+# 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.
+
+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")
+raise "Workflow must be dedicated to release artifacts" unless workflow.fetch("name") == "Build native release artifacts"
+
+def run_text(job)
+ job.fetch("steps").filter_map { |step| step["run"] }.join("\n")
+end
+
+%w[build-deb build-deb-arm64 build-rpm build-rpm-aarch64 build-windows build-windows-arm64].each do |name|
+ text = run_text(jobs.fetch(name))
+ raise "#{name} must build C++ through Maven" unless text.include?("-Pwith-cpp package")
+ raise "#{name} must bundle third-party dependencies" unless text.include?("-Dtsfile.dependency.source=BUNDLED")
+ raise "#{name} must enable CPack through Maven" unless text.include?("-Denable.cpack=ON")
+ raise "#{name} must use the generated CPack config" unless text.include?("cpp/target/build/CPackConfig.cmake")
+ raise "#{name} must stage an SDK" unless text.include?("cmake --install cpp/target/build")
+ raise "#{name} must use an absolute SDK prefix" unless text.include?("$PWD/sdk/") || text.include?('Join-Path $PWD "sdk/')
+ unless name.start_with?("build-windows")
+ raise "#{name} must locate pkg-config without assuming lib" unless text.include?('dirname "$sdk_pkgconfig"')
+ end
+ raise "#{name} must pass the generated archive version" unless text.include?("-Dtsfile.archive.version")
+ raise "#{name} must freeze source versions" unless text.include?("-Dtsfile.version.sync.skip=true")
+end
+
+rpm_bootstrap = jobs.fetch("build-rpm").fetch("steps").find { |step| step["name"] == "Install RPM build prerequisites" }
+packages = Shellwords.split(rpm_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")
+raise "AlmaLinux 9 default repositories do not provide ninja-build" if packages.include?("ninja-build")
+raise "RPM bootstrap must install make" unless packages.include?("make")
+raise "RPM bootstrap must install Java" unless packages.include?("java-17-openjdk-devel")
+
+windows = run_text(jobs.fetch("build-windows"))
+raise "Windows ZIP needs a consistent static CRT" unless windows.include?("-Dtsfile.msvc.static.runtime=ON") && windows.include?("-Dtsfile.dependency.source=BUNDLED")
+raise "Windows runtime check must be wired through Maven" unless windows.include?("CheckStaticMSVCRuntime.cmake") && windows.include?("-Dtsfile.project.include")
+raise "Windows Maven properties must be quoted for PowerShell" unless windows.include?('"-Dcpp.toolchain=msvc"') && windows.include?('"-Dbuild.type=Release"')
+raise "Both staged and extracted PE imports must be checked" unless windows.scan("python packaging/scripts/verify_windows_runtime.py").size == 2
+
+raise "Ubuntu ARM64 packages must use a native ARM runner" unless jobs.fetch("build-deb-arm64").fetch("runs-on") == "ubuntu-22.04-arm"
+raise "AlmaLinux ARM64 packages must use a native ARM runner" unless jobs.fetch("build-rpm-aarch64").fetch("runs-on") == "ubuntu-24.04-arm"
+raise "Windows ARM64 binaries must use the pinned native ARM runner" unless jobs.fetch("build-windows-arm64").fetch("runs-on") == "windows-11-vs2026-arm"
+windows_arm = run_text(jobs.fetch("build-windows-arm64"))
+raise "Windows ARM64 build must use its Maven toolchain profile" unless windows_arm.include?("-Dcpp.toolchain=msvc-arm64-native")
+raise "Windows ARM64 build must put the native compiler first on PATH" unless windows_arm.include?("HostARM64") && windows_arm.include?("Get-Command cl.exe")
+raise "Windows ARM64 consumer must use the same Ninja Multi-Config toolchain" unless windows_arm.include?('-G "Ninja Multi-Config"')
+raise "Windows ARM64 ZIP must have an architecture-specific name" unless windows_arm.include?("windows-arm64")
+raise "Windows ARM64 must not add signed MSIX/AppX packaging" if windows_arm.match?(/msix|appx|signtool/i)
+root_pom = File.read(File.expand_path("../../pom.xml", __dir__))
+raise "Windows ARM64 Maven profile must use Ninja Multi-Config" unless root_pom.include?("<id>cpp-msvc-arm64-native</id>") && root_pom.include?("<cmake.generator>Ninja Multi-Config</cmake.generator>")
+
+%w[test-deb-arm64 test-rpm-aarch64].each do |name|
+ job = jobs.fetch(name)
+ raise "#{name} must run on a native ARM runner" unless job.fetch("runs-on").end_with?("-arm")
+ runs = run_text(job)
+ raise "#{name} must build and run an installed CMake consumer" unless runs.include?("cpp/cmake/tests/projects/InstalledConsumer")
+end
+
+unix_python = jobs.fetch("build-python-unix")
+raise "Unix wheels must use the prepared build identity" unless unix_python.fetch("needs") == "prepare"
+unix_matrix = unix_python.fetch("strategy").fetch("matrix").fetch("include")
+expected_unix_platforms = %w[manylinux_2_28-x86_64 manylinux_2_28-aarch64 macos12-x86_64 macos12-arm64]
+raise "Unix wheel matrix must retain both architectures on Linux and macOS" unless unix_matrix.map { |row| row.fetch("name") }.sort == expected_unix_platforms.sort
+raise "Linux ARM wheels must use a native ARM runner" unless unix_matrix.find { |row| row["name"] == "manylinux_2_28-aarch64" }.fetch("os") == "ubuntu-22.04-arm"
+unix_build = unix_python.fetch("steps").find { |step| step["name"] == "Build wheels from one SDK per compatibility baseline" }
+unix_env = unix_build.fetch("env")
+%w[X86_64 AARCH64].each do |arch|
+ raise "Linux wheels must retain manylinux_2_28" unless unix_env.fetch("CIBW_MANYLINUX_#{arch}_IMAGE") == "manylinux_2_28"
+end
+%w[LINUX MACOS].each do |platform|
+ raise "#{platform} must build C++ once per wheel baseline" unless unix_env.fetch("CIBW_BEFORE_ALL_#{platform}").include?("build_python_sdk.sh")
+ raise "#{platform} wheels must point at the staged SDK" unless unix_env.fetch("CIBW_ENVIRONMENT_#{platform}").include?("TSFILE_CPP_BUILD=")
+end
+raise "macOS wheels must retain the 12.0 deployment target" unless unix_env.fetch("MACOSX_DEPLOYMENT_TARGET") == "12.0"
+raise "manylinux must export its SDK archive to the host" unless unix_env.fetch("CIBW_ENVIRONMENT_LINUX").include?("TSFILE_PYTHON_SDK_EXPORT=\"/host")
+
+windows_python = jobs.fetch("build-python-windows")
+raise "Windows wheels must reuse the native MSVC SDK" unless windows_python.fetch("needs") == "build-windows"
+raise "Windows wheels must download the MSVC SDK" unless windows_python.fetch("steps").any? { |step| step["with"].to_h["name"] == "native-sdk-windows-msvc-x86_64" }
+windows_python_text = run_text(windows_python)
+windows_wheel_build = windows_python.fetch("steps").find { |step| step["run"].to_s.include?("python -m cibuildwheel") }
+raise "Windows wheel runtime must come from the SDK" if windows_wheel_build.fetch("env").key?("TSFILE_MSVC_STATIC_RUNTIME")
+raise "Windows wheels must adapt installed DLLs into the Python SDK view" unless windows_python_text.include?('Copy-Item "$sdkRoot/bin/*.dll" "$sdkRoot/lib/"')
+raise "Windows wheels must point setup.py at the SDK" unless windows_python_text.include?("TSFILE_CPP_BUILD=")
+raise "Windows wheel job must not rebuild the C++ core" if windows_python_text.include?("-Pwith-cpp")
+raise "Windows wheels must use MSVC rather than MinGW" unless windows_python.fetch("steps").any? { |step| step["uses"].to_s.start_with?("ilammy/msvc-dev-cmd@") }
+
+[unix_python, windows_python].each do |job|
+ build = job.fetch("steps").find { |step| step["run"].to_s.include?("python -m cibuildwheel") }
+ env = build.fetch("env")
+ raise "Wheel matrix must retain CPython 3.9 through 3.14" unless env.fetch("CIBW_BUILD").split == %w[cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*]
+ raise "Wheel tests must run on separate installation jobs" unless env.fetch("CIBW_TEST_SKIP") == "*"
+ raise "Wheel build jobs must not run package tests" if env.key?("CIBW_TEST_COMMAND")
+ raise "Every wheel build must validate runtime-only contents" unless run_text(job).include?("packaging/scripts/check_python_wheel.py --wheelhouse wheelhouse")
+end
+sdk_builder_path = File.expand_path("../scripts/build_python_sdk.sh", __dir__)
+sdk_builder = File.read(sdk_builder_path)
+%w[-Pwith-cpp -Dtsfile.version.sync.skip=true -Dtsfile.dependency.source=BUNDLED -Denable.lzma2=ON].each do |option|
+ raise "Python SDK builder must use #{option}" unless sdk_builder.include?(option)
+end
+raise "Python SDK builder must stage installed headers and libraries" unless sdk_builder.include?("cmake --install cpp/target/build")
+raise "Python SDK export must support the container host mount" unless sdk_builder.include?("TSFILE_PYTHON_SDK_EXPORT")
+raise "Windows SDKs must retain Python wheel codec coverage" unless windows.include?("-Denable.lzma2=ON")
+output, status = Open3.capture2e("bash", "-n", sdk_builder_path)
+raise "Invalid Python SDK shell script: #{output}" unless status.success?
+raise "The old wheel workflow must be retired" if File.exist?(File.expand_path("../../.github/workflows/wheels.yml", __dir__))
+
+homebrew = jobs.fetch("build-homebrew")
+raise "Homebrew must use the current repository" unless homebrew.fetch("env").fetch("SOURCE_REPOSITORY") == "${{ github.repository }}"
+homebrew_text = run_text(homebrew)
+raise "Homebrew must build the release Formula" unless homebrew_text.include?("packaging/homebrew/tsfile.rb.in") && !homebrew_text.include?("tsfile-dev")
+expected_bottle_root = "https://packages.apache.org/artifactory/tsfile/homebrew/release/versions/${{ needs.prepare.outputs.homebrew_version }}/bottles"
+%w[build-homebrew merge-homebrew].each do |name|
+ raise "#{name} must use the release bottle root" unless jobs.fetch(name).fetch("env").fetch("BOTTLE_ROOT_URL") == expected_bottle_root
+end
+raise "Homebrew bottles and SDKs must validate dylib dependencies" unless homebrew_text.scan("python3 packaging/scripts/verify_macos_runtime.py").size == 2
+%w[tsfile.rb.in].each do |name|
+ formula = File.read(File.expand_path("../homebrew/#{name}", __dir__))
+ raise "#{name} must use bundled dependencies" unless formula.include?("-DTSFILE_DEPENDENCY_SOURCE=BUNDLED")
+ dependencies = formula.lines.grep(/^\s*depends_on/)
+ raise "#{name} must not install third-party runtime Formulae" unless dependencies.all? { |line| line.include?("=> :build") }
+ raise "#{name} must bundle zlib" if formula.include?("uses_from_macos")
+end
+raise "Homebrew job must stage a macOS SDK from the installed formula" unless homebrew_text.include?("brew --prefix apache/tsfile/tsfile")
+raise "Homebrew job must archive the staged macOS SDK" unless homebrew_text.include?("tsfile-sdk-${{ matrix.name }}-$ARCHIVE_VERSION")
+raise "Homebrew SDK must be verified from its own staged tree" unless homebrew_text.include?('sdk_root="$PWD/sdk/$sdk_name"')
+raise "Homebrew SDK must be archived from the staging root" unless homebrew_text.include?('tar -C sdk -czf "sdk/$sdk_name.tar.gz" "$sdk_name"')
+raise "Homebrew prefix capture must tolerate extra stdout lines" unless homebrew_text.include?("brew --prefix apache/tsfile/tsfile | tail -n 1")
+raise "Homebrew SDK verification must provide pkg-config" unless homebrew_text.include?("brew install pkgconf")
+raise "Homebrew SDK step must keep bash 3.2 compatibility" if homebrew_text.include?("set -euo pipefail")
+raise "Homebrew job must upload a macOS SDK artifact" unless homebrew.fetch("steps").any? { |step| step["with"].to_h["name"] == "native-sdk-${{ matrix.name }}" }
+raise "Homebrew SDK must drop keg bookkeeping" unless homebrew_text.include?("INSTALL_RECEIPT.json") && homebrew_text.include?("sbom.spdx.json")
+raise "Homebrew SDK must normalize the dylib identity" unless homebrew_text.include?("install_name_tool -id")
+raise "Homebrew SDK must re-sign the normalized dylib" unless homebrew_text.include?("codesign --force --sign -")
+
+homebrew_merge = run_text(jobs.fetch("merge-homebrew"))
+trust = homebrew_merge.index("brew trust apache/tsfile")
+merge = homebrew_merge.index("brew bottle --merge")
+raise "Homebrew merge must trust its temporary tap before loading the Formula" unless trust && merge && trust < merge
+
+%w[test-deb test-deb-arm64 test-rpm test-rpm-aarch64 build-windows build-windows-arm64].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
+
+prepare_steps = jobs.fetch("prepare").fetch("steps")
+prepare_text = run_text(jobs.fetch("prepare"))
+%w[--build-date --run-number --run-attempt].each do |option|
+ raise "Release versions must not depend on a development build identity" if prepare_text.include?(option)
+end
+cli_data_upload = prepare_steps.find { |step| step["with"].to_h["name"] == "native-cli-test-data" }.fetch("with")
+raise "CLI data must come from the current source checkout" unless cli_data_upload.fetch("path") == "python/tests/resources/*.tsfile"
+raise "prepare must use one source checkout for code and CLI data" unless prepare_steps.count { |step| step["uses"].to_s.start_with?("actions/checkout@") } == 1
+
+%w[test-deb test-deb-arm64 test-rpm test-rpm-aarch64 test-windows test-homebrew].each do |name|
+ job = jobs.fetch(name)
+ steps = job.fetch("steps")
+ raise "#{name} must download the Python CLI test data" unless steps.any? { |step| step["with"].to_h["name"] == "native-cli-test-data" }
+ raise "#{name} must run the installed CLI acceptance script" unless run_text(job).include?("packaging/tests/cli_package_smoke.py")
+ checkout = steps.find { |step| step["uses"].to_s.start_with?("actions/checkout@") }.fetch("with")
+ raise "#{name} must check out only its acceptance fixtures" if checkout.fetch("sparse-checkout").include?("cpp/src") || checkout.fetch("sparse-checkout").include?("cpp/tools")
+ raise "#{name} must not rebuild TsFile" if run_text(job).include?("-Pwith-cpp")
+end
+
+%w[test-homebrew test-compatibility-sdk].each do |name|
+ checkout = jobs.fetch(name).fetch("steps").find { |step| step["uses"].to_s.start_with?("actions/checkout@") }
+ raise "#{name} must provide the macOS runtime checker for downloaded SDKs" unless checkout.fetch("with").fetch("sparse-checkout").include?("packaging/scripts/verify_macos_runtime.py")
+end
+raise "Installed Homebrew bottle must validate dylib dependencies" unless run_text(jobs.fetch("test-homebrew")).include?("python3 packaging/scripts/verify_macos_runtime.py")
+windows_cli = jobs.fetch("test-windows")
+raise "Windows CLI acceptance must wait for both ZIP architectures" unless windows_cli.fetch("needs").sort == %w[build-windows build-windows-arm64].sort
+windows_cli_matrix = windows_cli.fetch("strategy").fetch("matrix").fetch("include")
+raise "Windows CLI acceptance must cover x86_64 and ARM64" unless windows_cli_matrix.map { |row| row.fetch("name") }.sort == %w[windows-msvc-x86_64 windows-msvc-arm64].sort
+raise "Windows CLI acceptance must install the ZIP" unless run_text(windows_cli).include?("Expand-Archive")
+homebrew_cli = jobs.fetch("test-homebrew")
+raise "Homebrew CLI acceptance must wait for the bottles" unless homebrew_cli.fetch("needs") == "build-homebrew"
+raise "Homebrew CLI acceptance must cover both architectures" unless homebrew_cli.fetch("strategy").fetch("matrix").fetch("include").map { |row| row.fetch("name") }.sort == %w[macos-arm64 macos-x86_64].sort
+raise "Homebrew CLI acceptance must install a bottle" unless run_text(homebrew_cli).include?("brew install --force-bottle") && run_text(homebrew_cli).include?("poured_from_bottle")
+homebrew_install = run_text(homebrew_cli)
+raise "Homebrew acceptance must install from its trusted tap" unless homebrew_install.include?("brew install --force-bottle apache/tsfile/tsfile")
+raise "Homebrew acceptance must merge bottle checksums" unless homebrew_install.include?("brew bottle --merge --write --no-commit")
+raise "Homebrew acceptance must seed the bottle download cache" unless homebrew_install.include?("brew --cache --force-bottle") && homebrew_install.include?('cp "${bottles[0]}" "$cached_bottle"')
+raise "Homebrew acceptance must not bypass package path restrictions" if homebrew_install.include?("HOMEBREW_INTERNAL_ALLOW_PACKAGES_FROM_PATHS")
+raise "Homebrew CLI acceptance must not build a bottle" if run_text(homebrew_cli).include?("--build-bottle")
+
+# Every native target runs the real API regressions and Go's own tests after
+# downloading and installing a package on a separate runner/container.
+%w[test-deb test-deb-arm64 test-rpm test-rpm-aarch64 test-windows test-homebrew].each do |name|
+ job = jobs.fetch(name)
+ text = run_text(job)
+ raise "#{name} must compile installed C++ API regressions" unless text.include?("packaging/tests/cpp_installed_tests.py")
+ raise "#{name} must run Go's installed-SDK tests" unless text.include?("packaging/tests/go_installed_tests.py")
+ raise "#{name} must set up Go" unless job.fetch("steps").any? { |step| step["uses"].to_s.start_with?("actions/setup-go@") }
+ checkout = job.fetch("steps").find { |step| step["uses"].to_s.start_with?("actions/checkout@") }.fetch("with").fetch("sparse-checkout")
+ %w[cpp/test go packaging/tests packaging/cmake/InstalledGTest].each do |required|
+ raise "#{name} must check out #{required}" unless checkout.lines.map(&:strip).include?(required)
+ end
+end
+windows_text = run_text(jobs.fetch("test-windows"))
+raise "Windows cgo must use a verified MinGW compiler" unless windows_text.include?("LLVM-MinGW checksum mismatch") && windows_text.include?("--llvm-bin")
+raise "Windows C++ tests must use the native MSVC environment" unless jobs.fetch("test-windows").fetch("steps").any? { |step| step["uses"].to_s.start_with?("ilammy/msvc-dev-cmd@") }
+
+sdk_archive_jobs = {
+ "test-deb" => "native-sdk-ubuntu22.04-amd64",
+ "test-deb-arm64" => "native-sdk-ubuntu22.04-arm64",
+ "test-rpm" => "native-sdk-almalinux9-x86_64",
+ "test-rpm-aarch64" => "native-sdk-almalinux9-aarch64",
+ "test-windows" => "native-sdk-${{ matrix.name }}",
+ "test-homebrew" => "native-sdk-${{ matrix.name }}",
+}
+sdk_archive_jobs.each do |name, artifact|
+ job = jobs.fetch(name)
+ raise "#{name} must download its standalone SDK" unless job.fetch("steps").any? { |step| step["with"].to_h["name"] == artifact }
+ raise "#{name} must test the extracted SDK" unless run_text(job).include?("packaging/tests/sdk_archive_tests.py")
+end
+compatibility_sdk = jobs.fetch("test-compatibility-sdk")
+raise "Compatibility SDK tests must wait for wheel baseline builds" unless compatibility_sdk.fetch("needs").sort == %w[prepare build-python-unix].sort
+raise "Compatibility SDK tests must cover every Unix baseline" unless compatibility_sdk.fetch("strategy").fetch("matrix").fetch("include").map { |row| row.fetch("name") }.sort == expected_unix_platforms.sort
+raise "Compatibility SDK tests must exercise the extracted archive" unless run_text(compatibility_sdk).include?("packaging/tests/sdk_archive_tests.py")
+raise "Compatibility SDK tests must not rebuild the product" if run_text(compatibility_sdk).match?(/with-cpp|build_python_sdk/)
+compat_checkout = compatibility_sdk.fetch("steps").find { |step| step["uses"].to_s.start_with?("actions/checkout@") }.fetch("with").fetch("sparse-checkout")
+raise "Compatibility SDK tests must exclude product sources" if compat_checkout.include?("cpp/src") || compat_checkout.include?("cpp/tools") || compat_checkout.include?("python/tsfile")
+
+expected_versions = %w[3.9 3.10 3.11 3.12 3.13 3.14]
+%w[test-python-unix test-python-windows].each do |name|
+ job = jobs.fetch(name)
+ expected_build = name == "test-python-unix" ? "build-python-unix" : "build-python-windows"
+ raise "#{name} must wait for downloaded wheels" unless job.fetch("needs").sort == ["prepare", expected_build].sort
+ raise "#{name} must cover every Python ABI" unless job.fetch("strategy").fetch("matrix").fetch("python-version") == expected_versions
+ checkout = job.fetch("steps").find { |step| step["uses"].to_s.start_with?("actions/checkout@") }.fetch("with").fetch("sparse-checkout")
+ raise "#{name} must check out Python tests only" unless checkout.lines.map(&:strip).sort == %w[packaging/scripts/check_python_wheel.py packaging/tests/python_installed_tests.py python/tests].sort
+ raise "#{name} must run installed-wheel pytest" unless run_text(job).include?("packaging/tests/python_installed_tests.py")
+ report = job.fetch("steps").find { |step| step["name"] == "Upload Python installation test results" }
+ raise "#{name} must require the pytest report" unless report.fetch("with").fetch("if-no-files-found") == "error"
+ raise "#{name} must consume the expected Python package version" unless run_text(job).include?("needs.prepare.outputs.python_version")
+ raise "#{name} must download the built wheels" unless job.fetch("steps").any? { |step| step["with"].to_h["name"].to_s.start_with?("native-python-wheel-") }
+ raise "#{name} must not compile native code" if run_text(job).match?(/cibuildwheel|with-cpp|build_ext/)
+end
+python_platforms = jobs.fetch("test-python-unix").fetch("strategy").fetch("matrix").fetch("platform")
+raise "Independent Unix pytest must cover the four wheel platforms" unless python_platforms.map { |row| row.fetch("name") }.sort == expected_unix_platforms.sort
+raise "Windows pytest must run on Windows" unless jobs.fetch("test-python-windows").fetch("runs-on") == "windows-2022"
+cpp_test_entry = File.read(File.expand_path("../../cpp/test/InstalledSDKTests.cmake", __dir__))
+raise "C++ regressions must link the installed CMake target" unless cpp_test_entry.include?("find_package(TsFile CONFIG REQUIRED)") && cpp_test_entry.include?("TsFile::tsfile")
+
+assemble = jobs.fetch("assemble")
+needs = Array(assemble.fetch("needs"))
+%w[test-deb test-deb-arm64 test-rpm test-rpm-aarch64 test-windows test-homebrew test-compatibility-sdk test-python-unix test-python-windows build-python-unix build-python-windows merge-homebrew build-windows build-windows-arm64].each do |name|
+ raise "assemble must depend on #{name}" unless needs.include?(name)
+end
+assemble_text = run_text(assemble)
+%w[native-deb-ubuntu22.04-arm64 native-rpm-almalinux9-aarch64 native-windows-msvc-arm64 native-sdk-ubuntu22.04-amd64 native-sdk-ubuntu22.04-arm64 native-sdk-almalinux9-x86_64 native-sdk-almalinux9-aarch64 native-sdk-windows-msvc-x86_64 native-sdk-windows-msvc-arm64 native-sdk-macos-arm64 native-sdk-macos-x86_64 native-python-wheel-manylinux_2_28-x86_64 native-python-wheel-manylinux_2_28-aarch64 native-python-wheel-macos12-x86_64 native-python-wheel-macos12-arm64 native-python-wheel-windo [...]
+ raise "assemble must download #{name}" unless assemble.fetch("steps").any? { |step| step["with"].to_h["name"] == name }
+end
+expected_unix_platforms.each do |platform|
+ raise "assemble must download the #{platform} SDK" unless assemble.fetch("steps").any? { |step| step["with"].to_h["name"] == "native-sdk-#{platform}" }
+end
+raise "assemble must verify the bundle" unless assemble_text.include?("--verify-bundle")
+raise "workflow must not hard-code a fork repository" if File.read(path).include?("ColinLeeo/tsfile")
+
+jobs.each_value do |job|
+ job.fetch("steps").each do |step|
+ artifact = step["with"].to_h["name"].to_s
+ raise "artifact names must not repeat the macOS matrix prefix" if artifact.include?("macos-macos")
+ 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 Maven build hierarchy, artifact consumers, workflow contracts, and shell syntax"
diff --git a/packaging/tests/cli_package_smoke.py b/packaging/tests/cli_package_smoke.py
new file mode 100644
index 000000000..b89a64f2c
--- /dev/null
+++ b/packaging/tests/cli_package_smoke.py
@@ -0,0 +1,278 @@
+#!/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
+#
+# 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.
+
+"""Validate an installed CLI with the current source checkout's Python resources."""
+
+import argparse
+import csv
+import hashlib
+import io
+import math
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+
+# Stable fixtures from python/tests/resources in the source checkout.
+# Expected data facts come from python/tests/test_load_tsfile_from_iotdb.py.
+CASES = (
+ {
+ "file": "simple_tree.tsfile",
+ "sha256": "bd221e61be15ac57b6197622b3a302015b2bf25a6d3cab84f31e74d919bbec26",
+ "model": "tree",
+ "object": "root.ln.wf01.wt01",
+ "rows": 105,
+ "schema": {"status": ("FIELD", "BOOLEAN"), "temperature": ("FIELD", "FLOAT")},
+ "nulls": {"status": 5, "temperature": 5},
+ "sums": {"time": 15960},
+ },
+ {
+ "file": "simple_table_t1.tsfile",
+ "sha256": "a7b7736dbe414d68eff90e792db93669738817a58bd3d560801a3ec5a7b3edef",
+ "model": "table",
+ "object": "test",
+ "rows": 60,
+ "schema": {
+ "s0": ("TAG", "STRING"),
+ "s1": ("TAG", "STRING"),
+ "s2": ("FIELD", "INT64"),
+ "s3": ("FIELD", "FLOAT"),
+ "s4": ("FIELD", "STRING"),
+ "s5": ("FIELD", "TIMESTAMP"),
+ "s6": ("FIELD", "INT32"),
+ "s7": ("FIELD", "DOUBLE"),
+ "s8": ("FIELD", "DATE"),
+ "s9": ("FIELD", "TEXT"),
+ },
+ "nulls": {"s2": 8, "s6": 8, "s9": 8},
+ "sums": {"s5": 63270, "s6": 3342},
+ },
+ {
+ "file": "simple_table_t2.tsfile",
+ "sha256": "9bea5c683ef22ee01bf3c31c8a066bfdfe8db9cedee5a3360772547980508d3a",
+ "model": "table",
+ "object": "test",
+ "rows": 40,
+ "schema": {
+ "s0": ("TAG", "STRING"),
+ "s1": ("TAG", "STRING"),
+ "s2": ("FIELD", "INT64"),
+ },
+ "nulls": {"s2": 5, "s6": 5, "s9": 5},
+ "sums": {"time": 70404242080000, "s2": 36450, "s5": 41680, "s6": 1800},
+ },
+ {
+ "file": "table_with_time_column.tsfile",
+ "sha256": "22006050d75559466e653d61c4e8182f8f2531c667f2ca7e234b43648cc696fd",
+ "model": "table",
+ "object": "table2",
+ "rows": 25,
+ "schema": {
+ "id": ("TIME", "TIMESTAMP"),
+ "region_id": ("TAG", "STRING"),
+ "temperature": ("FIELD", "FLOAT"),
+ "humidity": ("FIELD", "DOUBLE"),
+ },
+ "nulls": {"temperature": 0, "humidity": 0},
+ "sums": {},
+ },
+)
+
+
+def require(condition, message):
+ if not condition:
+ raise RuntimeError(message)
+
+
+def csv_rows(output):
+ reader = csv.DictReader(io.StringIO(output))
+ require(reader.fieldnames, "CLI returned CSV without a header")
+ rows = list(reader)
+ require(
+ all(None not in row and None not in row.values() for row in rows),
+ "Malformed CSV rows",
+ )
+ return rows
+
+
+def column(rows, name):
+ # Tree outputs may qualify the measurement with its device path.
+ matches = [key for key in rows[0] if key == name or key.endswith("." + name)]
+ require(len(matches) == 1, f"Expected exactly one column for {name}: {matches}")
+ return [row[matches[0]] for row in rows]
+
+
+def check_case(cli, resources, work, case, timeout):
+ source = resources / case["file"]
+ require(source.is_file(), f"Missing Python test resource: {source}")
+ require(
+ hashlib.sha256(source.read_bytes()).hexdigest() == case["sha256"],
+ f"Resource differs from the expected Python test fixture: {source}",
+ )
+ print(f"Testing {case['file']} with {cli}", flush=True)
+
+ def run(*arguments):
+ command = [str(cli), *map(str, arguments)]
+ result = subprocess.run(
+ command,
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ timeout=timeout,
+ cwd=work,
+ )
+ require(
+ result.returncode == 0,
+ f"Command failed ({result.returncode}): {command}\nstdout: {result.stdout}\nstderr: {result.stderr}",
+ )
+ require(not result.stderr, f"Unexpected stderr from {command}: {result.stderr}")
+ return result.stdout
+
+ objects = csv_rows(run("ls", "-f", "csv", source))
+ require(
+ objects == [{"model": case["model"], "object": case["object"]}],
+ f"Unexpected objects: {objects}",
+ )
+ metadata = csv_rows(run("meta", "-f", "csv", source))
+ require(
+ len(metadata) == 1 and metadata[0]["model"] == case["model"],
+ f"Unexpected metadata: {metadata}",
+ )
+ require(
+ int(metadata[0]["size_bytes"]) == source.stat().st_size
+ and int(metadata[0]["format_version"]) == 4,
+ f"Invalid file metadata: {metadata}",
+ )
+ schema = {
+ row["column"]: (row["category"], row["data_type"])
+ for row in csv_rows(run("schema", "-f", "csv", source))
+ }
+ for name, expected in case["schema"].items():
+ require(
+ schema.get(name) == expected,
+ f"Unexpected schema for {name}: {schema.get(name)}",
+ )
+
+ statistics = csv_rows(run("stats", "-f", "csv", source))
+ require(statistics, "CLI returned no field statistics")
+ counts = csv_rows(run("count", "-f", "csv", source))
+ scope = ("-d" if case["model"] == "tree" else "-t", case["object"])
+ content = run("cat", *scope, "-f", "csv", source)
+ rows = csv_rows(content)
+ require(len(rows) == case["rows"], f"Expected {case['rows']} rows, got {len(rows)}")
+ for name, expected_nulls in case["nulls"].items():
+ values = column(rows, name)
+ require(
+ values.count("\\N") == expected_nulls, f"Unexpected null count for {name}"
+ )
+ matching = [row for row in counts if row["column"].split(".")[-1] == name]
+ require(len(matching) == 1, f"Missing/duplicate count for {name}")
+ require(
+ int(matching[0]["row_count"]) == case["rows"]
+ and int(matching[0]["null_count"]) == expected_nulls
+ and int(matching[0]["non_null_count"]) == case["rows"] - expected_nulls,
+ f"Incorrect count result for {name}: {matching}",
+ )
+ for name, expected_sum in case["sums"].items():
+ actual_sum = sum(int(value) for value in column(rows, name) if value != "\\N")
+ require(actual_sum == expected_sum, f"Unexpected sum for {name}: {actual_sum}")
+ if case["model"] == "tree":
+ values = column(rows, "status")
+ require(
+ values.count("true") == 50 and values.count("false") == 50,
+ "Incorrect BOOLEAN values",
+ )
+ if case["object"] == "table2":
+ require(
+ column(rows, "region_id") == ["loc"] * 25, "Incorrect region TAG values"
+ )
+ for name in ("temperature", "humidity"):
+ require(
+ math.isclose(sum(map(float, column(rows, name))), 2.5, rel_tol=1e-6),
+ f"Incorrect FLOAT/DOUBLE values for {name}",
+ )
+
+ require(
+ csv_rows(run("head", *scope, "-n", "3", "-f", "csv", source)) == rows[:3],
+ "head differs from first three cat rows",
+ )
+ require(
+ csv_rows(run("cat", *scope, "--offset", "1", "-n", "2", "-f", "csv", source))
+ == rows[1:3],
+ "Incorrect offset/limit",
+ )
+ exported = work / (case["file"] + ".csv")
+ require(
+ run("export", *scope, "--type", "csv", "-o", exported, source) == "",
+ "export unexpectedly wrote to stdout",
+ )
+ require(
+ exported.read_text(encoding="utf-8") == content,
+ "Exported CSV differs from cat output",
+ )
+ require("TsFile" in run("sketch", source), "CLI returned no TsFile sketch")
+ print(
+ f"PASS: {case['file']}; {len(rows)} rows, schema/counts/values/query/export checked",
+ flush=True,
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--cli", required=True, help="Installed tsfile-cli executable or command"
+ )
+ parser.add_argument(
+ "--resources",
+ required=True,
+ type=Path,
+ help="Python test .tsfile resources from the source checkout",
+ )
+ parser.add_argument(
+ "--timeout",
+ type=float,
+ default=30,
+ help="Timeout in seconds per CLI invocation",
+ )
+ args = parser.parse_args()
+ cli = shutil.which(args.cli)
+ require(cli is not None, f"CLI executable not found: {args.cli}")
+ require(args.timeout > 0, "Timeout must be positive")
+ version = subprocess.run(
+ [cli, "--version"],
+ check=True,
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ timeout=args.timeout,
+ )
+ print(version.stdout.strip(), flush=True)
+ with tempfile.TemporaryDirectory(prefix="tsfile-cli-install-") as temporary:
+ for case in CASES:
+ check_case(
+ Path(cli).resolve(),
+ args.resources.resolve(),
+ Path(temporary),
+ case,
+ args.timeout,
+ )
+ print("PASS: installed CLI validated with all four Python test resources")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/cpp_installed_tests.py b/packaging/tests/cpp_installed_tests.py
new file mode 100644
index 000000000..3850d30f8
--- /dev/null
+++ b/packaging/tests/cpp_installed_tests.py
@@ -0,0 +1,141 @@
+#!/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
+#
+# 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.
+
+"""Compile existing C++ regression tests against an installed SDK only."""
+
+import argparse
+import os
+from pathlib import Path
+import subprocess
+import sys
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def package_config(prefix):
+ candidates = sorted(
+ {
+ *prefix.glob("lib*/cmake/TsFile/TsFileConfig.cmake"),
+ *prefix.glob("lib*/*/cmake/TsFile/TsFileConfig.cmake"),
+ }
+ )
+ if len(candidates) != 1:
+ raise ValueError(
+ f"Expected one installed TsFileConfig.cmake under {prefix}: {candidates}"
+ )
+ return candidates[0].parent
+
+
+def run(command, **kwargs):
+ print("+ " + subprocess.list2cmdline([str(item) for item in command]), flush=True)
+ subprocess.run(command, check=True, timeout=900, **kwargs)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--prefix", type=Path, required=True)
+ parser.add_argument("--build-dir", type=Path, required=True)
+ parser.add_argument("--gtest-prefix", type=Path)
+ arguments = parser.parse_args()
+ prefix = arguments.prefix.resolve()
+ build = arguments.build_dir.resolve()
+ config = package_config(prefix)
+ dependency_prefix = (
+ arguments.gtest_prefix.resolve()
+ if arguments.gtest_prefix is not None
+ else build / "dependencies"
+ )
+ options = ["-DCMAKE_BUILD_TYPE=Release"]
+ if sys.platform == "win32":
+ options += [
+ "-G",
+ "Ninja Multi-Config",
+ "-DCMAKE_POLICY_DEFAULT_CMP0091=NEW",
+ "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded",
+ ]
+ elif sys.platform == "darwin":
+ sdk = subprocess.check_output(["xcrun", "--show-sdk-path"], text=True).strip()
+ options += [f"-DCMAKE_OSX_SYSROOT={sdk}"]
+ if arguments.gtest_prefix is None:
+ run(
+ [
+ "cmake",
+ "-S",
+ ROOT / "packaging/cmake/InstalledGTest",
+ "-B",
+ build / "gtest",
+ *options,
+ f"-DCMAKE_INSTALL_PREFIX={dependency_prefix}",
+ ]
+ )
+ run(
+ [
+ "cmake",
+ "--build",
+ build / "gtest",
+ "--config",
+ "Release",
+ "--parallel",
+ "4",
+ ]
+ )
+ run(["cmake", "--install", build / "gtest", "--config", "Release"])
+ gtest_configs = list(dependency_prefix.glob("lib*/cmake/GTest/GTestConfig.cmake"))
+ if len(gtest_configs) != 1:
+ raise ValueError(
+ f"Expected one installed GoogleTest config under {dependency_prefix}"
+ )
+ run(
+ [
+ "cmake",
+ "-S",
+ ROOT / "cpp/test",
+ "-B",
+ build / "tests",
+ *options,
+ f"-DTsFile_DIR={config}",
+ f"-DGTest_DIR={gtest_configs[0].parent}",
+ f"-DCMAKE_PREFIX_PATH={dependency_prefix}",
+ "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF",
+ "-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF",
+ ]
+ )
+ run(["cmake", "--build", build / "tests", "--config", "Release", "--parallel", "4"])
+ environment = os.environ.copy()
+ if sys.platform == "win32":
+ environment["PATH"] = (
+ str(prefix / "bin") + os.pathsep + environment.get("PATH", "")
+ )
+ run(
+ [
+ "ctest",
+ "--test-dir",
+ build / "tests",
+ "-C",
+ "Release",
+ "--output-on-failure",
+ "--no-tests=error",
+ "--output-junit",
+ build / "cpp-results.xml",
+ ],
+ env=environment,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/go_installed_tests.py b/packaging/tests/go_installed_tests.py
new file mode 100644
index 000000000..05667d9fb
--- /dev/null
+++ b/packaging/tests/go_installed_tests.py
@@ -0,0 +1,168 @@
+#!/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
+#
+# 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.
+
+"""Run Go's own tests against the manually installed native package."""
+
+import argparse
+import os
+from pathlib import Path
+import re
+import shlex
+import subprocess
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def exported_c_symbols(output):
+ symbols = re.findall(
+ r"^\s*Name: ([A-Za-z_][A-Za-z0-9_]*)\s*$", output, re.MULTILINE
+ )
+ if not symbols:
+ raise ValueError("No C exports found in the installed TsFile DLL")
+ return sorted(set(symbols))
+
+
+def race_supported(goos, goarch):
+ return (goos, goarch) in {
+ ("linux", "amd64"),
+ ("linux", "arm64"),
+ ("darwin", "amd64"),
+ ("darwin", "arm64"),
+ ("windows", "amd64"),
+ }
+
+
+def pkgconfig_file(prefix):
+ candidates = sorted(
+ {
+ *prefix.glob("lib*/pkgconfig/libtsfile.pc"),
+ *prefix.glob("lib*/*/pkgconfig/libtsfile.pc"),
+ }
+ )
+ if len(candidates) != 1:
+ raise ValueError(
+ f"Expected one installed libtsfile.pc under {prefix}: {candidates}"
+ )
+ return candidates[0]
+
+
+def run(command, **kwargs):
+ print("+ " + subprocess.list2cmdline([str(item) for item in command]), flush=True)
+ subprocess.run(command, check=True, timeout=900, **kwargs)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--prefix", type=Path, required=True)
+ parser.add_argument("--goarch", choices=("amd64", "arm64"), required=True)
+ parser.add_argument("--llvm-bin", type=Path)
+ parser.add_argument("--build-dir", type=Path, default=Path("go-test-output"))
+ arguments = parser.parse_args()
+ prefix = arguments.prefix.resolve()
+ header = prefix / "include/tsfile/cwrapper/tsfile_cwrapper.h"
+ if not header.is_file():
+ raise ValueError(f"Missing installed C header: {header}")
+ goos, goarch = subprocess.check_output(
+ ["go", "env", "GOOS", "GOARCH"], text=True
+ ).split()
+ if goarch != arguments.goarch:
+ raise ValueError(f"Go architecture {goarch} does not match {arguments.goarch}")
+ environment = os.environ.copy()
+ environment["CGO_ENABLED"] = "1"
+ if goos == "windows":
+ if arguments.llvm_bin is None:
+ raise ValueError("Windows cgo requires --llvm-bin")
+ tools = arguments.llvm_bin.resolve()
+ triple = {"amd64": "x86_64", "arm64": "aarch64"}[goarch] + "-w64-mingw32"
+ environment["CC"] = (tools / f"{triple}-clang.exe").as_posix()
+ environment["CXX"] = (tools / f"{triple}-clang++.exe").as_posix()
+ dlls = [prefix / "bin" / name for name in ("tsfile.dll", "libtsfile.dll")]
+ dlls = [dll for dll in dlls if dll.is_file()]
+ if len(dlls) != 1:
+ raise ValueError(f"Expected one installed TsFile DLL: {dlls}")
+ dll = dlls[0]
+ output = subprocess.check_output(
+ [tools / "llvm-readobj.exe", "--coff-exports", dll], text=True
+ )
+ symbols = exported_c_symbols(output)
+ imports = arguments.build_dir.resolve() / "imports"
+ imports.mkdir(parents=True, exist_ok=True)
+ definition = imports / "tsfile.def"
+ definition.write_text(
+ f"LIBRARY {dll.name}\nEXPORTS\n" + "\n".join(symbols) + "\n"
+ )
+ machine = {"amd64": "i386:x86-64", "arm64": "arm64"}[goarch]
+ run(
+ [
+ tools / "llvm-dlltool.exe",
+ "-m",
+ machine,
+ "-d",
+ definition,
+ "-l",
+ imports / "libtsfile.dll.a",
+ ]
+ )
+ cflags = [
+ f"-I{prefix.as_posix()}/include",
+ f"-I{prefix.as_posix()}/include/tsfile",
+ ]
+ ldflags = [f"-L{imports.as_posix()}", "-ltsfile"]
+ environment["PATH"] = os.pathsep.join(
+ [str(dll.parent), str(tools), environment.get("PATH", "")]
+ )
+ else:
+ metadata = pkgconfig_file(prefix)
+ environment["PKG_CONFIG_PATH"] = str(metadata.parent)
+ cflags = shlex.split(
+ subprocess.check_output(
+ ["pkg-config", "--cflags", "libtsfile"], text=True, env=environment
+ )
+ )
+ ldflags = shlex.split(
+ subprocess.check_output(
+ ["pkg-config", "--libs", "libtsfile"], text=True, env=environment
+ )
+ )
+ ldflags.append(f"-Wl,-rpath,{metadata.parent.parent}")
+ environment["CGO_CFLAGS"] = shlex.join(cflags)
+ environment["CGO_LDFLAGS"] = shlex.join(ldflags)
+ print(f"Testing installed SDK {prefix} with Go {goos}/{goarch}", flush=True)
+ print(f"CGO_CFLAGS={environment['CGO_CFLAGS']}", flush=True)
+ print(f"CGO_LDFLAGS={environment['CGO_LDFLAGS']}", flush=True)
+ run(["go", "version"], env=environment)
+ for command in (
+ ["go", "vet", "./..."],
+ ["go", "test", "-count=1", "-timeout=10m", "./..."],
+ ):
+ run(command, cwd=ROOT / "go", env=environment)
+ if race_supported(goos, goarch):
+ run(
+ ["go", "test", "-race", "-count=1", "-timeout=10m", "./..."],
+ cwd=ROOT / "go",
+ env=environment,
+ )
+ else:
+ print(
+ f"NOT APPLICABLE: Go's race detector does not support {goos}/{goarch}",
+ flush=True,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/python_installed_tests.py b/packaging/tests/python_installed_tests.py
new file mode 100644
index 000000000..0858b1279
--- /dev/null
+++ b/packaging/tests/python_installed_tests.py
@@ -0,0 +1,149 @@
+#!/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
+#
+# 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.
+
+"""Install one downloaded wheel and run the existing pytest suite in isolation."""
+
+import argparse
+import os
+from pathlib import Path
+import re
+import subprocess
+import sys
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def select_wheel(directory, version, python_tag):
+ pattern = re.compile(
+ rf"^tsfile-{re.escape(version)}-{python_tag}-{python_tag}-.+\.whl$"
+ )
+ wheels = [
+ wheel.resolve()
+ for wheel in directory.glob("*.whl")
+ if pattern.fullmatch(wheel.name)
+ ]
+ if len(wheels) != 1:
+ raise ValueError(
+ f"Expected one tsfile {version} {python_tag} wheel in {directory}: {wheels}"
+ )
+ return wheels[0]
+
+
+def run(command, **kwargs):
+ print("+ " + subprocess.list2cmdline([str(item) for item in command]), flush=True)
+ subprocess.run(command, check=True, **kwargs)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--wheelhouse", type=Path, required=True)
+ parser.add_argument("--version", required=True)
+ parser.add_argument("--results", type=Path, required=True)
+ arguments = parser.parse_args()
+ python_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
+ wheel = select_wheel(arguments.wheelhouse, arguments.version, python_tag)
+ run(
+ [
+ sys.executable,
+ str(ROOT / "packaging/scripts/check_python_wheel.py"),
+ str(wheel),
+ ],
+ timeout=60,
+ )
+ # Keep the report outside the temporary cwd even when older Windows
+ # pathlib versions leave a nonexistent path relative after resolve().
+ results = Path(os.path.abspath(arguments.results))
+ results.parent.mkdir(parents=True, exist_ok=True)
+ tests = ROOT / "python/tests"
+ if not list(tests.glob("test_*.py")):
+ raise ValueError(f"No existing Python tests found in {tests}")
+ run(
+ [
+ sys.executable,
+ "-m",
+ "pip",
+ "install",
+ "--only-binary=:all:",
+ str(wheel),
+ "pytest",
+ "pytest-timeout",
+ ],
+ timeout=600,
+ )
+ with tempfile.TemporaryDirectory(prefix="tsfile-installed-pytest-") as directory:
+ # -I excludes the checkout and PYTHONPATH from imports. The wheel must
+ # work with its bundled native library on this separate runner.
+ check = """import faulthandler
+faulthandler.dump_traceback_later(30, repeat=True)
+from importlib.metadata import version
+from pathlib import Path
+import sys
+import tsfile
+faulthandler.cancel_dump_traceback_later()
+module = Path(tsfile.__file__).resolve()
+checkout = Path(sys.argv[2]).resolve()
+if checkout in module.parents:
+ raise RuntimeError(f'Imported source checkout: {module}')
+if version('tsfile') != sys.argv[1]:
+ raise RuntimeError(f'Unexpected installed version: {version("tsfile")}')
+print(f'Installed tsfile {version("tsfile")} from {module}', flush=True)
+"""
+ run(
+ [
+ sys.executable,
+ "-I",
+ "-u",
+ "-X",
+ "faulthandler",
+ "-c",
+ check,
+ arguments.version,
+ str(ROOT),
+ ],
+ cwd=directory,
+ timeout=60,
+ )
+ run(
+ [
+ sys.executable,
+ "-I",
+ "-u",
+ "-X",
+ "faulthandler",
+ "-m",
+ "pytest",
+ "--import-mode=importlib",
+ f"--rootdir={tests}",
+ "-o",
+ f"cache_dir={Path(directory) / 'pytest-cache'}",
+ "-ra",
+ "--timeout=120",
+ "--timeout-method=thread",
+ "-o",
+ "faulthandler_timeout=60",
+ f"--junitxml={results}",
+ str(tests),
+ ],
+ cwd=directory,
+ timeout=1800,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/python_wheel_smoke.py b/packaging/tests/python_wheel_smoke.py
new file mode 100644
index 000000000..cf7ffc154
--- /dev/null
+++ b/packaging/tests/python_wheel_smoke.py
@@ -0,0 +1,98 @@
+#!/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
+#
+# 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.
+
+"""Smoke-test an installed wheel, outside the Python source directory."""
+
+from importlib.metadata import version
+from pathlib import Path
+import tempfile
+
+import tsfile
+from tsfile import (
+ ColumnCategory,
+ ColumnSchema,
+ Field,
+ RowRecord,
+ TableSchema,
+ Tablet,
+ TimeseriesSchema,
+ TSDataType,
+ TsFileReader,
+ TsFileTableWriter,
+ TsFileWriter,
+)
+
+
+def main():
+ repository = Path(__file__).resolve().parents[2]
+ module = Path(tsfile.__file__).resolve()
+ if repository / "python" in module.parents:
+ raise RuntimeError(
+ f"Imported the source tree instead of the installed wheel: {module}"
+ )
+
+ with tempfile.TemporaryDirectory(prefix="tsfile-wheel-smoke-") as temporary:
+ table_path = str(Path(temporary) / "table.tsfile")
+ schema = TableSchema(
+ "sensors",
+ [
+ ColumnSchema("site", TSDataType.STRING, ColumnCategory.TAG),
+ ColumnSchema("value", TSDataType.INT64, ColumnCategory.FIELD),
+ ],
+ )
+ with TsFileTableWriter(table_path, schema) as writer:
+ tablet = Tablet(["site", "value"], [TSDataType.STRING, TSDataType.INT64], 3)
+ for row, value in enumerate([10, 20, 30]):
+ tablet.add_timestamp(row, row)
+ tablet.add_value_by_name("site", row, "north")
+ tablet.add_value_by_name("value", row, value)
+ writer.write_table(tablet)
+
+ with TsFileReader(table_path) as reader:
+ with reader.query_table("sensors", ["site", "value"], 0, 2) as result:
+ values = []
+ while result.next():
+ assert result.get_value_by_name("site") == "north"
+ values.append(result.get_value_by_name("value"))
+ assert values == [10, 20, 30], values
+
+ tree_path = str(Path(temporary) / "tree.tsfile")
+ with TsFileWriter(tree_path) as writer:
+ writer.register_timeseries(
+ "root.device", TimeseriesSchema("value", TSDataType.INT64)
+ )
+ for row, value in enumerate([40, 50]):
+ writer.write_row_record(
+ RowRecord(
+ "root.device", row, [Field("value", value, TSDataType.INT64)]
+ )
+ )
+ with TsFileReader(tree_path) as reader:
+ with reader.query_timeseries("root.device", ["value"], 0, 1) as result:
+ values = []
+ while result.next():
+ values.append(result.get_value_by_index(2))
+ assert values == [40, 50], values
+
+ print(
+ f"PASS: installed tsfile {version('tsfile')} from {module}; table/tree read-write"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/sdk_archive_tests.py b/packaging/tests/sdk_archive_tests.py
new file mode 100644
index 000000000..3db636f90
--- /dev/null
+++ b/packaging/tests/sdk_archive_tests.py
@@ -0,0 +1,157 @@
+#!/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
+#
+# 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.
+
+"""Download-contract tests for a relocated SDK archive, without product sources."""
+
+import argparse
+import json
+from pathlib import Path, PurePosixPath
+import subprocess
+import sys
+import tarfile
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def extract_sdk(archive_path, destination):
+ destination = destination.resolve()
+ if destination.exists() and any(destination.iterdir()):
+ raise ValueError(f"SDK extraction directory must be empty: {destination}")
+ destination.mkdir(parents=True, exist_ok=True)
+ with tarfile.open(archive_path, "r:gz") as archive:
+ members = archive.getmembers()
+ for member in members:
+ path = PurePosixPath(member.name)
+ if (
+ path.is_absolute()
+ or ".." in path.parts
+ or "\\" in member.name
+ or ":" in member.name
+ ):
+ raise ValueError(f"Unsafe SDK archive path: {member.name}")
+ if not (
+ member.isfile() or member.isdir() or member.issym() or member.islnk()
+ ):
+ raise ValueError(f"Unsupported SDK archive entry: {member.name}")
+ if member.issym() or member.islnk():
+ if "\\" in member.linkname or ":" in member.linkname:
+ raise ValueError(f"Unsafe SDK archive link: {member.name}")
+ base = (
+ (destination / member.name).parent
+ if member.issym()
+ else destination
+ )
+ target = (base / member.linkname).resolve()
+ if target != destination and destination not in target.parents:
+ raise ValueError(
+ f"SDK archive link escapes extraction directory: {member.name}"
+ )
+ if hasattr(tarfile, "data_filter"):
+ archive.extractall(destination, members=members, filter="data")
+ else:
+ archive.extractall(destination, members=members)
+ headers = list(destination.rglob("include/tsfile/cwrapper/tsfile_cwrapper.h"))
+ if len(headers) != 1:
+ raise ValueError(f"Expected one SDK installation prefix in {archive_path}")
+ return headers[0].parents[3]
+
+
+def run(command):
+ print(
+ "+ " + subprocess.list2cmdline([str(item) for item in command]),
+ flush=True,
+ )
+ subprocess.run(command, check=True, timeout=1800)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--archives", type=Path, required=True)
+ parser.add_argument("--destination", type=Path, required=True)
+ parser.add_argument("--build-dir", type=Path, required=True)
+ parser.add_argument("--resources", type=Path, required=True)
+ parser.add_argument("--goarch", choices=("amd64", "arm64"), required=True)
+ parser.add_argument("--llvm-bin", type=Path)
+ parser.add_argument("--gtest-prefix", type=Path)
+ arguments = parser.parse_args()
+ archives = list(arguments.archives.glob("*.tar.gz"))
+ if len(archives) != 1:
+ raise ValueError(f"Expected one downloaded SDK archive: {archives}")
+ prefix = extract_sdk(archives[0], arguments.destination)
+ cli = (
+ prefix / "bin" / ("tsfile-cli.exe" if sys.platform == "win32" else "tsfile-cli")
+ )
+ if not cli.is_file():
+ raise ValueError(f"Missing CLI in SDK archive: {cli}")
+ metadata = json.loads(
+ (prefix / "share/tsfile/tsfile-build-config.json").read_text()
+ )
+ if metadata.get("schema_version") != 1 or not metadata.get("runtime_library"):
+ raise ValueError("Missing shared-library build configuration in SDK")
+ if sys.platform == "win32" and metadata.get("msvc_runtime") != "MultiThreaded":
+ raise ValueError("Portable Windows SDK must record its static Release CRT")
+ print(
+ f"Testing downloaded SDK {archives[0]} relocated to {prefix}",
+ flush=True,
+ )
+ if sys.platform == "darwin":
+ run(
+ [
+ sys.executable,
+ ROOT / "packaging/scripts/verify_macos_runtime.py",
+ prefix,
+ ]
+ )
+ run(
+ [
+ sys.executable,
+ ROOT / "packaging/tests/cli_package_smoke.py",
+ "--cli",
+ cli,
+ "--resources",
+ arguments.resources,
+ ]
+ )
+ cpp = [
+ sys.executable,
+ ROOT / "packaging/tests/cpp_installed_tests.py",
+ "--prefix",
+ prefix,
+ "--build-dir",
+ arguments.build_dir,
+ ]
+ if arguments.gtest_prefix is not None:
+ cpp += ["--gtest-prefix", arguments.gtest_prefix]
+ run(cpp)
+ go = [
+ sys.executable,
+ ROOT / "packaging/tests/go_installed_tests.py",
+ "--prefix",
+ prefix,
+ "--goarch",
+ arguments.goarch,
+ "--build-dir",
+ arguments.build_dir / "go",
+ ]
+ if arguments.llvm_bin is not None:
+ go += ["--llvm-bin", arguments.llvm_bin]
+ run(go)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/test_assemble_native_packages.py b/packaging/tests/test_assemble_native_packages.py
new file mode 100644
index 000000000..2dab920ef
--- /dev/null
+++ b/packaging/tests/test_assemble_native_packages.py
@@ -0,0 +1,591 @@
+# 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 hashlib
+import importlib.util
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
+MODULE_PATH = REPOSITORY_ROOT / "packaging/scripts/assemble_native_packages.py"
+VERSIONS = {
+ "archive_version": "2.5.0",
+ "homebrew_version": "2.5.0",
+ "logical_version": "2.5.0",
+}
+SOURCE = {
+ "commit": "abcdef1234567890abcdef1234567890abcdef12",
+ "repository": "apache/tsfile",
+}
+
+
+PYTHON_TAGS = ("cp39", "cp310", "cp311", "cp312", "cp313", "cp314")
+WHEEL_VERSION = "2.5.0"
+WHEEL_PLATFORMS = {
+ "manylinux_2_28-x86_64": "manylinux_2_28_x86_64",
+ "manylinux_2_28-aarch64": "manylinux_2_28_aarch64",
+ "macos12-x86_64": "macosx_12_0_x86_64",
+ "macos12-arm64": "macosx_12_0_arm64",
+ "windows-msvc-x86_64": "win_amd64",
+}
+PYTHON_SDK_PLATFORMS = tuple(
+ platform for platform in WHEEL_PLATFORMS if platform != "windows-msvc-x86_64"
+)
+
+
+def wheel_filename(platform, tag="cp311", version=WHEEL_VERSION):
+ return f"tsfile-{version}-{tag}-{tag}-{WHEEL_PLATFORMS[platform]}.whl"
+
+
+def load_assembler_module():
+ if not MODULE_PATH.is_file():
+ return None
+ spec = importlib.util.spec_from_file_location(
+ "assemble_native_packages", MODULE_PATH
+ )
+ if spec is None or spec.loader is None:
+ raise ImportError(f"cannot load native package assembler from {MODULE_PATH}")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+class AssembleNativePackagesTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.module = load_assembler_module()
+
+ def require_module(self):
+ self.assertIsNotNone(
+ self.module, "native package assembler must exist for final bundle assembly"
+ )
+ return self.module
+
+ def write_fixture(self, directory):
+ files = {
+ "deb/tsfile_2.5.0-1_amd64.deb": b"deb artifact\n",
+ "deb/tsfile_2.5.0-1_arm64.deb": b"arm64 deb artifact\n",
+ "rpm/tsfile-2.5.0-1.el9.x86_64.rpm": b"rpm artifact\n",
+ "rpm/tsfile-2.5.0-1.el9.aarch64.rpm": b"aarch64 rpm artifact\n",
+ "homebrew/Formula/tsfile.rb": b"formula artifact\n",
+ "homebrew/bottles/tsfile.bottle.tar.gz": b"bottle artifact\n",
+ "homebrew/bottles/tsfile.bottle.json": b"bottle metadata\n",
+ "windows/tsfile-2.5.0-windows-x86_64.zip": b"windows artifact\n",
+ "windows/tsfile-2.5.0-windows-arm64.zip": b"windows arm64 artifact\n",
+ "sdk/ubuntu22.04-amd64/tsfile-sdk-ubuntu22.04-amd64-2.5.0.tar.gz": b"sdk ubuntu\n",
+ "sdk/ubuntu22.04-arm64/tsfile-sdk-ubuntu22.04-arm64-2.5.0.tar.gz": b"sdk ubuntu arm64\n",
+ "sdk/almalinux9-x86_64/tsfile-sdk-almalinux9-x86_64-2.5.0.tar.gz": b"sdk almalinux\n",
+ "sdk/almalinux9-aarch64/tsfile-sdk-almalinux9-aarch64-2.5.0.tar.gz": b"sdk almalinux arm64\n",
+ "sdk/macos-arm64/tsfile-sdk-macos-arm64-2.5.0.tar.gz": b"sdk macos arm64\n",
+ "sdk/macos-x86_64/tsfile-sdk-macos-x86_64-2.5.0.tar.gz": b"sdk macos x86_64\n",
+ "sdk/windows-msvc-x86_64/tsfile-sdk-windows-msvc-x86_64-2.5.0.tar.gz": b"sdk windows\n",
+ "sdk/windows-msvc-arm64/tsfile-sdk-windows-msvc-arm64-2.5.0.tar.gz": b"sdk windows arm64\n",
+ }
+ for platform in PYTHON_SDK_PLATFORMS:
+ files[
+ f"sdk/{platform}/tsfile-sdk-{platform}-{VERSIONS['archive_version']}.tar.gz"
+ ] = b"Python compatibility SDK\n"
+ for platform in WHEEL_PLATFORMS:
+ for tag in PYTHON_TAGS:
+ files[f"python/{platform}/{wheel_filename(platform, tag)}"] = (
+ b"Python wheel\n"
+ )
+ for relative_path, contents in files.items():
+ path = directory / relative_path
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_bytes(contents)
+ return files
+
+ def test_assembles_publishable_bundle_with_literal_metadata(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ input_directory = Path(temporary_directory) / "input"
+ output_directory = Path(temporary_directory) / "bundle"
+ self.write_fixture(input_directory)
+
+ manifest = module.assemble(
+ input_directory, output_directory, VERSIONS, SOURCE
+ )
+
+ expected_paths = [
+ "deb/ubuntu22.04-amd64/tsfile_2.5.0-1_amd64.deb",
+ "deb/ubuntu22.04-arm64/tsfile_2.5.0-1_arm64.deb",
+ "homebrew/Formula/tsfile.rb",
+ "homebrew/bottles/tsfile.bottle.json",
+ "homebrew/bottles/tsfile.bottle.tar.gz",
+ "rpm/almalinux9-aarch64/tsfile-2.5.0-1.el9.aarch64.rpm",
+ "rpm/almalinux9-x86_64/tsfile-2.5.0-1.el9.x86_64.rpm",
+ "sdk/almalinux9-aarch64/tsfile-sdk-almalinux9-aarch64-2.5.0.tar.gz",
+ "sdk/almalinux9-x86_64/tsfile-sdk-almalinux9-x86_64-2.5.0.tar.gz",
+ "sdk/macos-arm64/tsfile-sdk-macos-arm64-2.5.0.tar.gz",
+ "sdk/macos-x86_64/tsfile-sdk-macos-x86_64-2.5.0.tar.gz",
+ "sdk/ubuntu22.04-amd64/tsfile-sdk-ubuntu22.04-amd64-2.5.0.tar.gz",
+ "sdk/ubuntu22.04-arm64/tsfile-sdk-ubuntu22.04-arm64-2.5.0.tar.gz",
+ "sdk/windows-msvc-arm64/tsfile-sdk-windows-msvc-arm64-2.5.0.tar.gz",
+ "sdk/windows-msvc-x86_64/tsfile-sdk-windows-msvc-x86_64-2.5.0.tar.gz",
+ "windows/tsfile-2.5.0-windows-arm64.zip",
+ "windows/tsfile-2.5.0-windows-x86_64.zip",
+ ]
+ expected_paths.extend(
+ f"sdk/{platform}/tsfile-sdk-{platform}-{VERSIONS['archive_version']}.tar.gz"
+ for platform in PYTHON_SDK_PLATFORMS
+ )
+ expected_paths.extend(
+ f"python/wheels/{platform}/{wheel_filename(platform, tag)}"
+ for platform in WHEEL_PLATFORMS
+ for tag in PYTHON_TAGS
+ )
+ expected_paths.sort()
+ self.assertEqual(
+ sorted(
+ path.relative_to(output_directory).as_posix()
+ for path in output_directory.rglob("*")
+ if path.is_file()
+ and path.name not in {"SHA256SUMS", "manifest.json"}
+ ),
+ expected_paths,
+ )
+ self.assertEqual(
+ json.loads((output_directory / "manifest.json").read_text()), manifest
+ )
+ self.assertEqual(manifest["source"], SOURCE)
+ self.assertEqual(manifest["versions"], VERSIONS)
+ self.assertEqual(
+ {artifact["family"] for artifact in manifest["artifacts"]},
+ {"deb", "homebrew", "python", "rpm", "sdk", "windows"},
+ )
+ self.assertEqual(
+ {artifact["platform"] for artifact in manifest["artifacts"]},
+ {
+ "almalinux9-aarch64",
+ "almalinux9-x86_64",
+ "homebrew",
+ "macos-arm64",
+ "macos-x86_64",
+ "ubuntu22.04-amd64",
+ "ubuntu22.04-arm64",
+ "manylinux_2_28-x86_64",
+ "manylinux_2_28-aarch64",
+ "macos12-x86_64",
+ "macos12-arm64",
+ "windows-msvc-arm64",
+ "windows-msvc-x86_64",
+ },
+ )
+ wheels = [
+ artifact
+ for artifact in manifest["artifacts"]
+ if artifact["family"] == "python"
+ ]
+ self.assertEqual(len(wheels), 30)
+ for wheel in wheels:
+ self.assertEqual(wheel["sdkPlatform"], wheel["platform"])
+ self.assertEqual(wheel["wheelVersion"], WHEEL_VERSION)
+ self.assertEqual(wheel["abiTag"], wheel["pythonTag"])
+ for key in ("targetRepository", "targetPath", "properties"):
+ self.assertNotIn(key, wheel)
+ for artifact in manifest["artifacts"]:
+ if artifact["family"] == "python":
+ continue
+ self.assertIn(
+ artifact["targetRepository"],
+ {"tsfile", "tsfile-debian", "tsfile-rpm"},
+ )
+ self.assertIn("release/", artifact["targetPath"])
+ if artifact["family"] == "deb":
+ self.assertEqual(
+ artifact["properties"]["deb.component"], ["release"]
+ )
+ artifact_platforms = {
+ artifact["filename"]: artifact["platform"]
+ for artifact in manifest["artifacts"]
+ }
+ self.assertEqual(
+ artifact_platforms["tsfile_2.5.0-1_arm64.deb"],
+ "ubuntu22.04-arm64",
+ )
+ self.assertEqual(
+ artifact_platforms["tsfile-2.5.0-1.el9.aarch64.rpm"],
+ "almalinux9-aarch64",
+ )
+ self.assertEqual(
+ artifact_platforms["tsfile-2.5.0-windows-arm64.zip"],
+ "windows-msvc-arm64",
+ )
+ deb_arm64 = next(
+ artifact
+ for artifact in manifest["artifacts"]
+ if artifact["filename"] == "tsfile_2.5.0-1_arm64.deb"
+ )
+ self.assertEqual(deb_arm64["properties"]["deb.architecture"], ["arm64"])
+ rpm_arm64 = next(
+ artifact
+ for artifact in manifest["artifacts"]
+ if artifact["filename"] == "tsfile-2.5.0-1.el9.aarch64.rpm"
+ )
+ self.assertEqual(
+ rpm_arm64["targetPath"],
+ "release/el9/aarch64/tsfile-2.5.0-1.el9.aarch64.rpm",
+ )
+ for artifact in manifest["artifacts"]:
+ output_path = output_directory / artifact["path"]
+ input_path = next(
+ path
+ for path in input_directory.rglob(artifact["filename"])
+ if path.is_file()
+ )
+ self.assertEqual(artifact["size"], input_path.stat().st_size)
+ self.assertEqual(
+ artifact["sha256"],
+ hashlib.sha256(input_path.read_bytes()).hexdigest(),
+ )
+
+ def test_requires_every_python_abi_on_every_platform(self):
+ module = self.require_module()
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ root = Path(temporary_directory)
+ for platform in WHEEL_PLATFORMS:
+ for tag in PYTHON_TAGS:
+ with self.subTest(platform=platform, tag=tag):
+ source = root / platform / tag
+ self.write_fixture(source)
+ (
+ source / "python" / platform / wheel_filename(platform, tag)
+ ).unlink()
+ with self.assertRaisesRegex(
+ ValueError, "missing Python wheel ABIs"
+ ):
+ module.assemble(source, root / "bundle", VERSIONS, SOURCE)
+
+ def test_rejects_incompatible_wheel_tags_and_duplicate_abis(self):
+ module = self.require_module()
+ invalid = [
+ ("manylinux_2_28-x86_64", "linux_x86_64", "incompatible Python wheel tags"),
+ (
+ "manylinux_2_28-aarch64",
+ "manylinux_2_28_x86_64",
+ "incompatible Python wheel tags",
+ ),
+ ("macos12-arm64", "macosx_13_0_arm64", "incompatible Python wheel tags"),
+ ("windows-msvc-x86_64", "win_arm64", "incompatible Python wheel tags"),
+ ]
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ root = Path(temporary_directory)
+ for index, (platform, tag, error) in enumerate(invalid):
+ with self.subTest(platform=platform, tag=tag):
+ source = root / f"invalid-{index}"
+ self.write_fixture(source)
+ original = source / "python" / platform / wheel_filename(platform)
+ original.rename(
+ original.with_name(
+ f"tsfile-{WHEEL_VERSION}-cp311-cp311-{tag}.whl"
+ )
+ )
+ with self.assertRaisesRegex(ValueError, error):
+ module.assemble(source, root / "bundle", VERSIONS, SOURCE)
+ source = root / "duplicate"
+ self.write_fixture(source)
+ wheel = (
+ source
+ / "python/manylinux_2_28-x86_64"
+ / wheel_filename("manylinux_2_28-x86_64", version="9.0.0")
+ )
+ wheel.write_bytes(b"duplicate ABI with a different version")
+ with self.assertRaisesRegex(ValueError, "duplicate Python wheel ABI"):
+ module.assemble(source, root / "bundle", VERSIONS, SOURCE)
+
+ def test_rejects_different_versions_across_wheel_platforms(self):
+ module = self.require_module()
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ source = Path(temporary_directory) / "input"
+ self.write_fixture(source)
+ original = (
+ source
+ / "python/windows-msvc-x86_64"
+ / wheel_filename("windows-msvc-x86_64")
+ )
+ original.rename(
+ original.with_name(
+ wheel_filename("windows-msvc-x86_64", version="9.0.0")
+ )
+ )
+ with self.assertRaisesRegex(ValueError, "must all have the same version"):
+ module.assemble(
+ source, Path(temporary_directory) / "bundle", VERSIONS, SOURCE
+ )
+
+ def test_release_bundle_requires_matching_wheel_version(self):
+ module = self.require_module()
+ release_versions = {
+ "base_version": "2.5.0",
+ "logical_version": "2.5.0",
+ "archive_version": "2.5.0",
+ "homebrew_version": "2.5.0",
+ }
+ for wheel_version in ("2.5.0", "2.3.2.dev0"):
+ with self.subTest(
+ wheel_version=wheel_version
+ ), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "input"
+ output = root / "bundle"
+ self.write_fixture(source)
+ for wheel in source.rglob("*.whl"):
+ wheel.rename(
+ wheel.with_name(
+ wheel.name.replace(WHEEL_VERSION, wheel_version)
+ )
+ )
+ if wheel_version == "2.5.0":
+ manifest = module.assemble(source, output, release_versions, SOURCE)
+ self.assertEqual(manifest["versions"], release_versions)
+ module.verify_bundle(output)
+ else:
+ with self.assertRaisesRegex(
+ ValueError, "must match release version 2.5.0"
+ ):
+ module.assemble(source, output, release_versions, SOURCE)
+ self.assertFalse(output.exists())
+
+ def test_rejects_development_or_mismatched_release_versions(self):
+ module = self.require_module()
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "input"
+ self.write_fixture(source)
+ for field, value in (
+ ("archive_version", "2.5.0.dev"),
+ ("homebrew_version", "2.5.0.dev0"),
+ ("homebrew_version", "2.6.0"),
+ ("logical_version", "2.5.0.dev0+build"),
+ ):
+ with self.subTest(field=field, value=value):
+ output = root / "bundle"
+ with self.assertRaisesRegex(ValueError, "release version"):
+ module.assemble(
+ source, output, {**VERSIONS, field: value}, SOURCE
+ )
+ self.assertFalse(output.exists())
+
+ def test_rejects_bundles_missing_arm_architectures(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ missing_artifacts = (
+ (
+ "deb/tsfile_2.5.0-1_arm64.deb",
+ "missing DEB inputs for: ubuntu22.04-arm64",
+ ),
+ (
+ "rpm/tsfile-2.5.0-1.el9.aarch64.rpm",
+ "missing RPM inputs for: almalinux9-aarch64",
+ ),
+ (
+ "windows/tsfile-2.5.0-windows-arm64.zip",
+ "missing WINDOWS inputs for: windows-msvc-arm64",
+ ),
+ (
+ "sdk/ubuntu22.04-arm64/tsfile-sdk-ubuntu22.04-arm64-2.5.0.tar.gz",
+ "missing SDK inputs for: ubuntu22.04-arm64",
+ ),
+ (
+ "sdk/almalinux9-aarch64/tsfile-sdk-almalinux9-aarch64-2.5.0.tar.gz",
+ "missing SDK inputs for: almalinux9-aarch64",
+ ),
+ (
+ "sdk/windows-msvc-arm64/tsfile-sdk-windows-msvc-arm64-2.5.0.tar.gz",
+ "missing SDK inputs for: windows-msvc-arm64",
+ ),
+ )
+ for index, (missing_path, error_message) in enumerate(missing_artifacts):
+ with self.subTest(missing=missing_path):
+ input_directory = Path(temporary_directory) / f"input-{index}"
+ self.write_fixture(input_directory)
+ (input_directory / missing_path).unlink()
+
+ with self.assertRaisesRegex(ValueError, error_message):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / f"incomplete-{index}",
+ VERSIONS,
+ SOURCE,
+ )
+
+ def test_verifies_assembler_checksum_lines_sorted_by_path(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ input_directory = Path(temporary_directory) / "input"
+ output_directory = Path(temporary_directory) / "bundle"
+ self.write_fixture(input_directory)
+ module.assemble(input_directory, output_directory, VERSIONS, SOURCE)
+
+ checksum_lines = (
+ (output_directory / "SHA256SUMS")
+ .read_text(encoding="utf-8")
+ .splitlines()
+ )
+ self.assertNotEqual(
+ checksum_lines,
+ sorted(checksum_lines),
+ "fixture must distinguish path ordering from hash ordering",
+ )
+ self.assertIsNone(module.verify_bundle(output_directory))
+
+ def test_rejects_unsafe_versions_and_empty_source_identity(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ input_directory = Path(temporary_directory) / "input"
+ self.write_fixture(input_directory)
+ for index, (field, value) in enumerate(
+ (
+ ("archive_version", "../../latest"),
+ ("homebrew_version", "../latest"),
+ ("archive_version", ""),
+ ("homebrew_version", "version\\latest"),
+ )
+ ):
+ with self.subTest(field=field, value=value):
+ versions = {**VERSIONS, field: value}
+ with self.assertRaisesRegex(ValueError, "safe path component"):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / f"invalid-{index}",
+ versions,
+ SOURCE,
+ )
+ for index, (field, value) in enumerate(
+ (("commit", ""), ("repository", " "))
+ ):
+ with self.subTest(field=field, value=value):
+ source = {**SOURCE, field: value}
+ with self.assertRaisesRegex(ValueError, "nonempty"):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / f"empty-{index}",
+ VERSIONS,
+ source,
+ )
+
+ def test_rejects_unknown_files(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ input_directory = Path(temporary_directory) / "input"
+ self.write_fixture(input_directory)
+ (input_directory / "deb/README.txt").write_text(
+ "not a package\n", encoding="utf-8"
+ )
+
+ with self.assertRaisesRegex(ValueError, "undeclared file type"):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / "bundle",
+ VERSIONS,
+ SOURCE,
+ )
+
+ def test_rejects_empty_and_symbolic_link_inputs(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ input_directory = Path(temporary_directory) / "input"
+ self.write_fixture(input_directory)
+ (input_directory / "rpm/tsfile-2.5.0-1.el9.x86_64.rpm").write_bytes(b"")
+ with self.assertRaisesRegex(ValueError, "empty file"):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / "empty",
+ VERSIONS,
+ SOURCE,
+ )
+
+ self.write_fixture(input_directory)
+ os.symlink(
+ input_directory / "deb/tsfile_2.5.0-1_amd64.deb",
+ input_directory / "deb/linked.deb",
+ )
+ with self.assertRaisesRegex(ValueError, "symbolic link"):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / "linked",
+ VERSIONS,
+ SOURCE,
+ )
+
+ def test_rejects_duplicate_destination_names(self):
+ module = self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ input_directory = Path(temporary_directory) / "input"
+ self.write_fixture(input_directory)
+ duplicate = input_directory / "deb/duplicate/tsfile_2.5.0-1_amd64.deb"
+ duplicate.parent.mkdir()
+ duplicate.write_bytes(b"another deb artifact\n")
+
+ with self.assertRaisesRegex(ValueError, "duplicate target path"):
+ module.assemble(
+ input_directory,
+ Path(temporary_directory) / "bundle",
+ VERSIONS,
+ SOURCE,
+ )
+
+ def test_cli_assembles_from_versions_json(self):
+ self.require_module()
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ temporary_path = Path(temporary_directory)
+ input_directory = temporary_path / "input"
+ self.write_fixture(input_directory)
+ versions_path = temporary_path / "versions.json"
+ versions_path.write_text(json.dumps(VERSIONS), encoding="utf-8")
+ output_directory = temporary_path / "bundle"
+
+ subprocess.run(
+ [
+ sys.executable,
+ str(MODULE_PATH),
+ "--input-dir",
+ str(input_directory),
+ "--output-dir",
+ str(output_directory),
+ "--versions-json",
+ str(versions_path),
+ "--source-repository",
+ SOURCE["repository"],
+ "--source-commit",
+ SOURCE["commit"],
+ ],
+ check=True,
+ )
+ manifest_path = output_directory / "manifest.json"
+ self.assertTrue(manifest_path.is_file(), "CLI must write manifest.json")
+ self.assertEqual(
+ json.loads(manifest_path.read_text(encoding="utf-8"))["source"], SOURCE
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_dependency_download.py b/packaging/tests/test_dependency_download.py
new file mode 100644
index 000000000..453872476
--- /dev/null
+++ b/packaging/tests/test_dependency_download.py
@@ -0,0 +1,105 @@
+# 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.
+
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+import subprocess
+import tempfile
+import threading
+import unittest
+
+ROOT = Path(__file__).resolve().parents[2]
+HELPER = ROOT / "cpp/cmake/DownloadWithRetry.cmake"
+
+
+class DependencyDownloadTest(unittest.TestCase):
+ def download(self, responses):
+ state = {"requests": 0}
+ payload = b"verified upstream archive payload"
+
+ class Handler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ index = state["requests"]
+ state["requests"] += 1
+ status = responses[min(index, len(responses) - 1)]
+ self.send_response(status)
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+
+ def log_message(self, *args):
+ pass
+
+ with ThreadingHTTPServer(("127.0.0.1", 0), Handler) as server:
+ worker = threading.Thread(target=server.serve_forever, daemon=True)
+ worker.start()
+ try:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ destination = root / "archive.part"
+ status_file = root / "status.txt"
+ script = root / "download.cmake"
+ script.write_text(
+ "cmake_minimum_required(VERSION 3.11)\n"
+ f'include("{HELPER.as_posix()}")\n'
+ f'tsfile_download_with_retry("http://127.0.0.1:{server.server_port}/archive" '
+ f'"{destination.as_posix()}" status)\n'
+ f'file(WRITE "{status_file.as_posix()}" "${{status}}")\n'
+ )
+ subprocess.run(
+ ["cmake", "-P", str(script)],
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ timeout=30,
+ )
+ status = int(status_file.read_text().split(";", 1)[0])
+ contents = (
+ destination.read_bytes() if destination.exists() else None
+ )
+ return status, state["requests"], contents, payload
+ finally:
+ server.shutdown()
+ worker.join(timeout=5)
+
+ def test_transient_server_failures_retry_then_succeed(self):
+ status, requests, contents, payload = self.download([503, 503, 200])
+ self.assertEqual(status, 0)
+ self.assertEqual(requests, 3)
+ self.assertEqual(contents, payload)
+
+ def test_repeated_failures_stop_after_three_attempts_and_remove_partial_file(self):
+ status, requests, contents, _ = self.download([503])
+ self.assertNotEqual(status, 0)
+ self.assertEqual(requests, 3)
+ self.assertIsNone(contents)
+
+ def test_permanent_http_error_is_not_retried(self):
+ status, requests, contents, _ = self.download([404])
+ self.assertNotEqual(status, 0)
+ self.assertEqual(requests, 1)
+ self.assertIsNone(contents)
+
+ def test_success_does_not_repeat_the_download(self):
+ status, requests, contents, payload = self.download([200])
+ self.assertEqual(status, 0)
+ self.assertEqual(requests, 1)
+ self.assertEqual(contents, payload)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_installed_test_runners.py b/packaging/tests/test_installed_test_runners.py
new file mode 100644
index 000000000..749414c79
--- /dev/null
+++ b/packaging/tests/test_installed_test_runners.py
@@ -0,0 +1,111 @@
+# 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
+from pathlib import Path
+import tempfile
+import unittest
+
+HERE = Path(__file__).resolve().parent
+
+
+def load_runner(name):
+ spec = importlib.util.spec_from_file_location(name, HERE / f"{name}.py")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+class InstalledTestRunnersTest(unittest.TestCase):
+ def test_selects_downloaded_wheel_for_exact_version_and_abi(self):
+ runner = load_runner("python_installed_tests")
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ selected = root / "tsfile-2.5.0-cp312-cp312-manylinux_2_28_x86_64.whl"
+ selected.touch()
+ (root / "tsfile-2.3.2.dev0-cp312-cp312-manylinux_2_28_x86_64.whl").touch()
+ (root / "tsfile-2.5.0-cp311-cp311-manylinux_2_28_x86_64.whl").touch()
+ self.assertEqual(
+ runner.select_wheel(root, "2.5.0", "cp312"), selected.resolve()
+ )
+ with self.assertRaisesRegex(ValueError, "Expected one"):
+ runner.select_wheel(root, "2.5.0", "cp314")
+
+ def test_rejects_ambiguous_downloaded_wheels(self):
+ runner = load_runner("python_installed_tests")
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ for platform in ("macosx_12_0_arm64", "macosx_12_0_x86_64"):
+ (root / f"tsfile-2.5.0-cp312-cp312-{platform}.whl").touch()
+ with self.assertRaisesRegex(ValueError, "Expected one"):
+ runner.select_wheel(root, "2.5.0", "cp312")
+
+ def test_cpp_config_is_required_under_the_installed_prefix(self):
+ runner = load_runner("cpp_installed_tests")
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ with self.assertRaisesRegex(ValueError, "Expected one installed"):
+ runner.package_config(root)
+ config = root / "lib/x86_64-linux-gnu/cmake/TsFile/TsFileConfig.cmake"
+ config.parent.mkdir(parents=True)
+ config.touch()
+ self.assertEqual(runner.package_config(root), config.parent)
+
+ def test_go_pkgconfig_supports_multiarch_and_lib64_installations(self):
+ runner = load_runner("go_installed_tests")
+ for libdir in ("lib", "lib64", "lib/x86_64-linux-gnu"):
+ with self.subTest(
+ libdir=libdir
+ ), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ metadata = root / libdir / "pkgconfig/libtsfile.pc"
+ metadata.parent.mkdir(parents=True)
+ metadata.touch()
+ self.assertEqual(runner.pkgconfig_file(root), metadata)
+
+ def test_windows_go_import_library_uses_only_c_exports(self):
+ runner = load_runner("go_installed_tests")
+ output = """File: tsfile.dll
+Export {
+ Ordinal: 1
+ Name: tsfile_open
+}
+Export {
+ Ordinal: 2
+ Name: ?query@TsFileReader@storage@@QEAAXXZ
+}
+Export {
+ Name: free_tsfile
+}
+"""
+ self.assertEqual(
+ runner.exported_c_symbols(output), ["free_tsfile", "tsfile_open"]
+ )
+ with self.assertRaisesRegex(ValueError, "No C exports"):
+ runner.exported_c_symbols("invalid PE input")
+
+ def test_race_runs_on_supported_targets_only(self):
+ runner = load_runner("go_installed_tests")
+ for goos in ("linux", "darwin", "windows"):
+ self.assertTrue(runner.race_supported(goos, "amd64"))
+ self.assertTrue(runner.race_supported("linux", "arm64"))
+ self.assertTrue(runner.race_supported("darwin", "arm64"))
+ self.assertFalse(runner.race_supported("windows", "arm64"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_macos_runtime.py b/packaging/tests/test_macos_runtime.py
new file mode 100644
index 000000000..9f819b4d8
--- /dev/null
+++ b/packaging/tests/test_macos_runtime.py
@@ -0,0 +1,120 @@
+# 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.
+
+import importlib.util
+from pathlib import Path
+import tempfile
+import unittest
+
+MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts/verify_macos_runtime.py"
+spec = importlib.util.spec_from_file_location("verify_macos_runtime", MODULE_PATH)
+checker = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(checker)
+
+
+class MacOSRuntimeTest(unittest.TestCase):
+ def test_reads_all_dylib_load_commands_but_not_library_identity(self):
+ output = """library.dylib:
+Load command 0
+ cmd LC_ID_DYLIB
+ cmdsize 96
+ name /opt/homebrew/Cellar/tsfile/2.5.0/lib/libtsfile.1.dylib (offset 24)
+Load command 1
+ cmd LC_LOAD_DYLIB
+ cmdsize 56
+ name /usr/lib/libSystem.B.dylib (offset 24)
+"""
+ for index, command in enumerate(sorted(checker.LOAD_COMMANDS), 2):
+ output += f"Load command {index}\n cmd {command}\n name @rpath/libtsfile.1.dylib (offset 24)\n"
+ self.assertEqual(
+ checker.parse_dependencies(output),
+ {"/usr/lib/libSystem.B.dylib", "@rpath/libtsfile.1.dylib"},
+ )
+
+ def test_allows_system_libraries_and_packaged_tsfile(self):
+ with tempfile.TemporaryDirectory() as directory:
+ prefix = Path(directory)
+ (prefix / "lib").mkdir()
+ (prefix / "bin").mkdir()
+ library = prefix / "lib/libtsfile.1.dylib"
+ library.touch()
+ checker.validate_dependencies(
+ prefix / "bin/tsfile-cli",
+ {
+ "/usr/lib/libc++.1.dylib",
+ "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation",
+ "@rpath/libtsfile.1.dylib",
+ "@loader_path/../lib/libtsfile.1.dylib",
+ str(library),
+ },
+ prefix,
+ )
+
+ def test_rejects_codec_dylibs_even_when_they_exist(self):
+ with tempfile.TemporaryDirectory() as directory:
+ prefix = Path(directory)
+ (prefix / "lib").mkdir()
+ codec = prefix / "lib/liblz4.1.dylib"
+ codec.touch()
+ for name in (
+ "/opt/homebrew/opt/snappy/lib/libsnappy.1.dylib",
+ "/usr/local/opt/lz4/lib/liblz4.1.dylib",
+ "@rpath/libsnappy.1.dylib",
+ str(codec),
+ ):
+ with self.subTest(name=name), self.assertRaisesRegex(
+ ValueError, "external third-party dylib"
+ ):
+ checker.validate_dependencies(
+ prefix / "bin/tsfile-cli", {name}, prefix
+ )
+
+ def test_rejects_missing_or_external_tsfile_even_through_symlinks(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ prefix = root / "sdk"
+ (prefix / "lib").mkdir(parents=True)
+ external = root / "libtsfile.1.dylib"
+ external.touch()
+ (prefix / "lib/libtsfile.1.dylib").symlink_to(external)
+ for name in (
+ "@rpath/libtsfile.2.dylib",
+ "@rpath/libtsfile.1.dylib",
+ str(external),
+ "@loader_path/../../libtsfile.1.dylib",
+ ):
+ with self.subTest(name=name), self.assertRaisesRegex(
+ ValueError, "unshipped TsFile dylib"
+ ):
+ checker.validate_dependencies(
+ prefix / "bin/tsfile-cli", {name}, prefix
+ )
+
+ def test_rejects_empty_or_malformed_inspector_output(self):
+ for output in (
+ "not a Mach-O file",
+ "Load command 0\n cmd LC_LOAD_DYLIB\n",
+ ):
+ with self.subTest(output=output), self.assertRaises(ValueError):
+ checker.parse_dependencies(output)
+ with tempfile.TemporaryDirectory() as directory:
+ with self.assertRaisesRegex(ValueError, "No Mach-O"):
+ checker.verify_prefix(directory)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_maven_retry.py b/packaging/tests/test_maven_retry.py
new file mode 100644
index 000000000..916c40b8b
--- /dev/null
+++ b/packaging/tests/test_maven_retry.py
@@ -0,0 +1,92 @@
+#!/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
+#
+# 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.
+
+"""Exercise the Maven wrapper retry boundary with real subprocess failures."""
+
+import importlib.util
+import json
+from pathlib import Path
+import sys
+import tempfile
+import unittest
+from unittest.mock import patch
+
+SCRIPT = Path(__file__).resolve().parents[1] / "scripts/run_maven_with_retry.py"
+SPEC = importlib.util.spec_from_file_location("maven_retry", SCRIPT)
+MAVEN = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(MAVEN)
+
+
+class MavenRetryTest(unittest.TestCase):
+ def execute(self, message, failures):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ fake = root / "maven.py"
+ fake.write_text(
+ "from pathlib import Path\nimport json, sys\n"
+ "path = Path('calls.json')\n"
+ "calls = json.loads(path.read_text()) if path.exists() else []\n"
+ "calls.append(sys.argv[1:])\n"
+ "path.write_text(json.dumps(calls))\n"
+ f"if len(calls) <= {failures}:\n"
+ f" print({message!r}, flush=True)\n sys.exit(1)\n"
+ "print('[INFO] BUILD SUCCESS', flush=True)\n"
+ )
+ with patch.object(MAVEN.time, "sleep"):
+ status = MAVEN.run_maven(
+ ["package"], root=root, command_prefix=[sys.executable, str(fake)]
+ )
+ return status, json.loads((root / "calls.json").read_text())
+
+ def test_connection_reset_retries_and_refreshes_negative_cache(self):
+ status, calls = self.execute(
+ "[ERROR] Could not transfer artifact org.apache.maven:maven-model-builder:pom:3.0 "
+ "from/to central (https://repo.maven.apache.org/maven2): Connection reset",
+ 1,
+ )
+ self.assertEqual(status, 0)
+ self.assertEqual(calls, [["package"], ["-U", "package"]])
+
+ def test_transient_http_failure_stops_after_three_attempts(self):
+ status, calls = self.execute(
+ "[ERROR] Could not transfer artifact example:pom:1 from/to central: "
+ "status code: 503, reason phrase: Service Unavailable",
+ 5,
+ )
+ self.assertEqual(status, 1)
+ self.assertEqual(len(calls), 3)
+
+ def test_missing_artifact_does_not_retry(self):
+ status, calls = self.execute(
+ "[ERROR] Could not transfer artifact example:pom:1 from/to central: "
+ "status code: 404, reason phrase: Not Found",
+ 5,
+ )
+ self.assertEqual(status, 1)
+ self.assertEqual(len(calls), 1)
+
+ def test_compiler_failure_does_not_retry(self):
+ status, calls = self.execute(
+ "[ERROR] Failed to execute goal exec-maven-plugin: compiler error", 5
+ )
+ self.assertEqual(status, 1)
+ self.assertEqual(len(calls), 1)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_native_install.py b/packaging/tests/test_native_install.py
new file mode 100644
index 000000000..293159ac6
--- /dev/null
+++ b/packaging/tests/test_native_install.py
@@ -0,0 +1,515 @@
+# 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 builds requiring CMake, Ninja, pkg-config, LZ4 and an archive cache."""
+
+import os
+import re
+import shlex
+import shutil
+import subprocess
+import tempfile
+import textwrap
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+FIXTURES = ROOT / "cpp/cmake/tests/projects"
+
+
+class PublicHeaderCompatibilityTest(unittest.TestCase):
+ def test_simd_int64_reduction_normalizes_lane_types(self):
+ statistic = (ROOT / "cpp/src/common/statistic.h").read_text()
+ self.assertIn("std::min<int64_t>(simde_mm_cvtsi128_si64(vmin)", statistic)
+ self.assertIn("std::max<int64_t>(simde_mm_cvtsi128_si64(vmax)", statistic)
+ source = textwrap.dedent("""\
+ #include <algorithm>
+ #include <cstdint>
+
+ long simde_mm_cvtsi128_si64(int);
+ long long simde_mm_extract_epi64(int, int);
+
+ std::int64_t reduce_min(int value) {
+ return std::min<std::int64_t>(simde_mm_cvtsi128_si64(value),
+ simde_mm_extract_epi64(value, 1));
+ }
+ """)
+ result = subprocess.run(
+ [
+ *shlex.split(os.environ.get("CXX", "c++")),
+ "-fsyntax-only",
+ "-x",
+ "c++",
+ "-std=c++11",
+ "-",
+ ],
+ input=source,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+ def test_msvc_old_cplusplus_uses_native_cxx11_features(self):
+ source = textwrap.dedent("""\
+ #include "utils/util_define.h"
+ class Probe { STATIC_ASSERT(true, MSVC_old_cplusplus); };
+ class Base { virtual void reset(); };
+ class Derived : public Base { void reset() OVERRIDE; };
+ """)
+ result = subprocess.run(
+ [
+ *shlex.split(os.environ.get("CXX", "c++")),
+ "-E",
+ "-P",
+ "-x",
+ "c++",
+ "-std=c++11",
+ "-D_MSC_VER=1930",
+ "-DTSFILE_STATIC",
+ "-D__cplusplus=199711L",
+ f"-I{ROOT / 'cpp/src'}",
+ "-",
+ ],
+ input=source,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout[-4000:])
+ self.assertRegex(
+ result.stdout, r"class Probe \{\s*(?:static_assert|_Static_assert)"
+ )
+ self.assertNotIn("static_assertion_MSVC_old_cplusplus", result.stdout)
+ self.assertIn("void reset() override", result.stdout)
+
+ def test_msvc_old_cplusplus_does_not_define_nullptr(self):
+ compatibility = (ROOT / "cpp/src/utils/util_define.h").read_text()
+ section = re.search(
+ r"/\* ======== nullptr ======== \*/\n(.*?)\n/\* ======== cache line",
+ compatibility,
+ re.S,
+ )
+ self.assertIsNotNone(section)
+ source = section.group(1) + textwrap.dedent("""\
+
+ #ifdef nullptr
+ #error nullptr must remain a native MSVC keyword
+ #endif
+ class Base { virtual void reset(); };
+ class Derived : public Base { void reset() OVERRIDE; };
+ int* pointer = nullptr;
+ """)
+ result = subprocess.run(
+ [
+ *shlex.split(os.environ.get("CXX", "c++")),
+ "-E",
+ "-P",
+ "-nostdinc",
+ "-x",
+ "c++",
+ "-std=c++11",
+ "-D_MSC_VER=1930",
+ "-D__cplusplus=199711L",
+ "-",
+ ],
+ input=source,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout)
+ self.assertIn("void reset() override", result.stdout)
+ self.assertRegex(result.stdout, r"int\s*\*\s*pointer\s*=\s*nullptr")
+
+
+class StaticANTLRPackageTest(unittest.TestCase):
+ """Exercise installed metadata without compiling the ANTLR runtime."""
+
+ def test_installed_system_antlr_requires_compatible_runtime(self):
+ with tempfile.TemporaryDirectory(prefix="tsfile-antlr-config-") as temporary:
+ directory = Path(temporary)
+ producer = directory / "producer"
+ (producer / "cmake").mkdir(parents=True)
+ shutil.copyfile(
+ ROOT / "cpp/cmake/TsFileStaticDependencies.cmake.in",
+ producer / "cmake/TsFileStaticDependencies.cmake.in",
+ )
+ (producer / "CMakeLists.txt").write_text(textwrap.dedent("""\
+ cmake_minimum_required(VERSION 3.11)
+ project(StaticANTLRPackage NONE)
+ include(CMakePackageConfigHelpers)
+ include("${TSFILE_CMAKE_DIR}/DependencySource.cmake")
+ include("${TSFILE_CMAKE_DIR}/ANTLR4Dependency.cmake")
+ set(CMAKE_INSTALL_LIBDIR lib)
+ set(ENABLE_ANTLR4 ON)
+ set(ENABLE_THREADS OFF)
+ set(TSFILE_BUILD_SHARED OFF)
+ add_library(tsfile INTERFACE)
+ add_library(TsFile::ANTLR4 INTERFACE IMPORTED)
+ include("${TSFILE_CMAKE_DIR}/TsFileStaticDependencies.cmake")
+ configure_package_config_file(
+ "${TSFILE_CMAKE_DIR}/TsFileConfig.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfig.cmake"
+ INSTALL_DESTINATION lib/cmake/TsFile)
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfig.cmake"
+ DESTINATION lib/cmake/TsFile)
+ install(TARGETS tsfile EXPORT TsFileTargets)
+ install(EXPORT TsFileTargets NAMESPACE TsFile::
+ DESTINATION lib/cmake/TsFile)
+ """))
+ consumer = directory / "consumer"
+ consumer.mkdir()
+ (consumer / "CMakeLists.txt").write_text(textwrap.dedent("""\
+ cmake_minimum_required(VERSION 3.11)
+ project(StaticANTLRConsumer NONE)
+ find_package(TsFile CONFIG REQUIRED)
+ get_target_property(runtime TsFile::Static_ANTLR4
+ INTERFACE_LINK_LIBRARIES)
+ if (NOT runtime STREQUAL EXPECTED_TARGET OR NOT TARGET ${runtime})
+ message(FATAL_ERROR "Static TsFile lost its system target")
+ endif ()
+ if (NOT TARGET utf8cpp)
+ message(FATAL_ERROR "Static TsFile lost utf8cpp compatibility")
+ endif ()
+ """))
+
+ def configure(source, build, *options):
+ return subprocess.run(
+ [
+ "cmake",
+ "-S",
+ str(source),
+ "-B",
+ str(build),
+ "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=FALSE",
+ "-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=FALSE",
+ "-DCMAKE_FIND_USE_CMAKE_SYSTEM_PATH=FALSE",
+ "-DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=FALSE",
+ f"-DCMAKE_MAKE_PROGRAM={shutil.which('make')}",
+ "-G",
+ "Unix Makefiles",
+ *options,
+ ],
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+
+ def package(prefix, version, target, version_field):
+ prefix.mkdir(parents=True)
+ (prefix / "utf8cppConfig.cmake").write_text(
+ "add_library(utf8cpp INTERFACE IMPORTED)\n"
+ )
+ if version == "missing-package":
+ return
+ content = ""
+ if version is not None:
+ content += f'set({version_field} "{version}")\n'
+ if target is not None:
+ content += (
+ f"add_library({target} INTERFACE IMPORTED)\n"
+ f"set_property(TARGET {target} PROPERTY "
+ "INTERFACE_LINK_LIBRARIES utf8cpp)\n"
+ )
+ (prefix / "antlr4-runtime-config.cmake").write_text(content)
+
+ targets = (
+ "antlr4_static",
+ "antlr4-runtime::antlr4_static",
+ "antlr4_shared",
+ "antlr4-runtime::antlr4_shared",
+ )
+ for index, target in enumerate(targets):
+ build = directory / f"producer-build-{index}"
+ stage = directory / f"stage-{index}"
+ build_package = directory / f"build-package-{index}"
+ package(build_package, "4.9.3", target, "ANTLR_VERSION")
+ result = configure(
+ producer,
+ build,
+ f"-DTSFILE_CMAKE_DIR={ROOT / 'cpp/cmake'}",
+ "-DTSFILE_DEPENDENCY_SOURCE=SYSTEM",
+ f"-DCMAKE_PREFIX_PATH={build_package}",
+ f"-DCMAKE_INSTALL_PREFIX={stage}",
+ )
+ self.assertEqual(result.returncode, 0, result.stdout)
+ result = subprocess.run(
+ ["cmake", "--install", str(build)],
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout)
+ relocated = directory / f"relocated-{index}"
+ stage.rename(relocated)
+ cases = (
+ ("4.9.3", target, True),
+ ("4.12.0", target, True),
+ ("4.13.0", target, False),
+ ("4.13.2", target, False),
+ ("4.9.2", target, False),
+ (None, target, False),
+ ("4.9.3", None, False),
+ ("missing-package", None, False),
+ )
+ for field in ("antlr4-runtime_VERSION", "ANTLR_VERSION"):
+ for case, (version, consumer_target, succeeds) in enumerate(cases):
+ with self.subTest(
+ target=target,
+ field=field,
+ version=version,
+ consumer_target=consumer_target,
+ ):
+ suffix = f"{index}-{field}-{case}"
+ runtime = directory / f"runtime-{suffix}"
+ package(runtime, version, consumer_target, field)
+ result = configure(
+ consumer,
+ directory / f"consumer-{suffix}",
+ f"-DCMAKE_PREFIX_PATH={relocated};{runtime}",
+ f"-DEXPECTED_TARGET={target}",
+ )
+ if succeeds:
+ self.assertEqual(result.returncode, 0, result.stdout)
+ else:
+ self.assertNotEqual(
+ result.returncode,
+ 0,
+ f"Installed static TsFile accepted {version}:\n"
+ + result.stdout,
+ )
+ diagnostic = " ".join(result.stdout.split())
+ self.assertIn("compatible system ANTLR4", diagnostic)
+ self.assertIn(">=4.9.3", diagnostic)
+ self.assertIn("<4.13.0", diagnostic)
+
+
+@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, "--config", "Release", "--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")
+ self.pkgconfig_consumers(static=True)
+
+ def test_static_bundled_install_and_relocated_consumer(self):
+ self.configure(
+ "-G",
+ "Ninja Multi-Config",
+ "-DTSFILE_BUILD_SHARED=OFF",
+ "-DTSFILE_DEPENDENCY_SOURCE=BUNDLED",
+ "-DENABLE_ANTLR4=ON",
+ "-DENABLE_SNAPPY=ON",
+ "-DENABLE_LZ4=ON",
+ "-DENABLE_ZLIB=ON",
+ "-DENABLE_LZOKAY=ON",
+ "-DENABLE_ZSTD=ON",
+ "-DENABLE_LZMA2=ON",
+ "-DTSFILE_DEPENDENCY_OFFLINE=ON",
+ f"-DTSFILE_DEPENDENCY_CACHE={os.environ['TSFILE_TEST_DEPENDENCY_CACHE']}",
+ )
+ self.install()
+ self.consumer("InstalledConsumer", "tsfile_installed_consumer")
+ self.pkgconfig_consumers(static=True)
+
+ def pkgconfig_consumers(self, libdir="lib", static=False):
+ env = dict(
+ os.environ,
+ PKG_CONFIG_PATH=str(self.stage / libdir / "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,
+ f"-DTSFILE_PKGCONFIG_STATIC={'ON' if static else 'OFF'}",
+ )
+ self.run_command(
+ self.directory / "PkgConfigConsumer/tsfile_pkgconfig_cpp_consumer", env=env
+ )
+
+ def test_multiarch_pkgconfig_install_and_relocated_consumers(self):
+ self.configure("-DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu")
+ self.install()
+ self.pkgconfig_consumers("lib/x86_64-linux-gnu")
+
+ def cli_rpath(self, libdir):
+ self.configure("-DBUILD_TOOLS=ON", f"-DCMAKE_INSTALL_LIBDIR={libdir}")
+ env = dict(os.environ)
+ for name in (
+ "LD_LIBRARY_PATH",
+ "DYLD_LIBRARY_PATH",
+ "DYLD_FALLBACK_LIBRARY_PATH",
+ ):
+ env.pop(name, None)
+ self.run_command("cmake", "--build", self.build, "--parallel", "8")
+ self.run_command(self.build / "bin/tsfile-cli", "--version", env=env)
+ self.install()
+ # A build-directory RPATH must not mask a broken installed RPATH.
+ shutil.rmtree(self.build)
+ self.run_command(self.stage / "bin/tsfile-cli", "--version", env=env)
+
+ def test_lib64_cli_build_and_relocated_install(self):
+ self.cli_rpath("lib64")
+
+ def test_multiarch_cli_build_and_relocated_install(self):
+ self.cli_rpath("lib/x86_64-linux-gnu")
+
+ 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.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
new file mode 100644
index 000000000..8be994f93
--- /dev/null
+++ b/packaging/tests/test_native_package_versions.py
@@ -0,0 +1,182 @@
+# 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 json
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
+MODULE_PATH = REPOSITORY_ROOT / "packaging/scripts/native_package_versions.py"
+RELEASE_VERSION = "2.5.0"
+
+
+def write_release_cmake(root):
+ # Successful CLI cases must also run in a development-version checkout.
+ path = root / "CMakeLists.txt"
+ path.write_text(f"set(TsFile_CPP_VERSION {RELEASE_VERSION})\n", encoding="utf-8")
+ return path
+
+
+class NativePackageVersionsTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ spec = importlib.util.spec_from_file_location(
+ "native_package_versions", MODULE_PATH
+ )
+ cls.module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(cls.module)
+
+ def test_reads_release_version(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cmake_file = write_release_cmake(Path(directory))
+ self.assertEqual(self.module.read_cpp_version(cmake_file), RELEASE_VERSION)
+
+ def test_builds_exact_release_package_versions(self):
+ self.assertEqual(
+ self.module.build_versions("2.5.0"),
+ {
+ "base_version": "2.5.0",
+ "python_version": "2.5.0",
+ "logical_version": "2.5.0",
+ "deb_version": "2.5.0-1",
+ "rpm_version": "2.5.0",
+ "rpm_release": "1.el9",
+ "archive_version": "2.5.0",
+ "homebrew_version": "2.5.0",
+ },
+ )
+ self.assertEqual(self.module.build_versions("3.0.0")["deb_version"], "3.0.0-1")
+
+ def test_rejects_development_prerelease_and_invalid_versions(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cmake_file = Path(directory) / "CMakeLists.txt"
+ for version in (
+ "2.5.0.dev",
+ "2.5.0.dev0",
+ "2.5.0rc1",
+ "2.5.dev",
+ "2.5",
+ "02.5.0",
+ "2.5.0+build",
+ "2.5.0-release",
+ "",
+ ):
+ with self.subTest(version=version):
+ with self.assertRaisesRegex(ValueError, "release"):
+ self.module.build_versions(version)
+ cmake_file.write_text(f"set(TsFile_CPP_VERSION {version})\n")
+ with self.assertRaisesRegex(ValueError, "release"):
+ self.module.read_cpp_version(cmake_file)
+
+ def test_cli_rejects_development_checkout_before_writing_outputs(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ cmake_file = root / "CMakeLists.txt"
+ cmake_file.write_text("set(TsFile_CPP_VERSION 2.5.0.dev)\n")
+ json_out = root / "versions.json"
+ github_out = root / "github-output.txt"
+ result = subprocess.run(
+ [
+ sys.executable,
+ str(MODULE_PATH),
+ "--cmake-file",
+ str(cmake_file),
+ "--json-out",
+ str(json_out),
+ "--github-output",
+ str(github_out),
+ ],
+ capture_output=True,
+ text=True,
+ )
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn("release TsFile_CPP_VERSION", result.stderr)
+ self.assertFalse(json_out.exists())
+ self.assertFalse(github_out.exists())
+
+ def test_cli_writes_json_and_github_outputs(self):
+ expected = self.module.build_versions(RELEASE_VERSION)
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ cmake_file = write_release_cmake(root)
+ json_out = root / "versions.json"
+ github_out = root / "github-output.txt"
+ github_out.write_text("existing=value\n")
+ subprocess.run(
+ [
+ sys.executable,
+ str(MODULE_PATH),
+ "--cmake-file",
+ str(cmake_file),
+ "--json-out",
+ str(json_out),
+ "--github-output",
+ str(github_out),
+ ],
+ check=True,
+ )
+ self.assertEqual(json.loads(json_out.read_text()), expected)
+ self.assertEqual(
+ github_out.read_text().splitlines(),
+ [
+ "existing=value",
+ *[f"{key}={value}" for key, value in expected.items()],
+ ],
+ )
+
+ def test_cli_preserves_existing_github_output_bytes(self):
+ version = RELEASE_VERSION
+ 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:
+ root = Path(directory)
+ cmake_file = write_release_cmake(root)
+ github_out = root / "github-output.txt"
+ github_out.write_bytes(existing)
+ subprocess.run(
+ [
+ sys.executable,
+ str(MODULE_PATH),
+ "--cmake-file",
+ str(cmake_file),
+ "--json-out",
+ str(root / "versions.json"),
+ "--github-output",
+ str(github_out),
+ ],
+ check=True,
+ )
+ separator = b"\n" if existing and not existing.endswith(b"\n") else b""
+ self.assertTrue(
+ github_out.read_bytes().startswith(
+ existing + separator + f"base_version={version}\n".encode()
+ )
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_python_wheel_contents.py b/packaging/tests/test_python_wheel_contents.py
new file mode 100644
index 000000000..6d015862b
--- /dev/null
+++ b/packaging/tests/test_python_wheel_contents.py
@@ -0,0 +1,86 @@
+# 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.
+#
+
+"""Validate real wheel ZIP layouts and ELF debug-section rejection."""
+
+import importlib.util
+from pathlib import Path
+import struct
+import tempfile
+import unittest
+from zipfile import ZipFile
+
+ROOT = Path(__file__).resolve().parents[2]
+spec = importlib.util.spec_from_file_location(
+ "wheel_contents", ROOT / "packaging/scripts/check_python_wheel.py"
+)
+checker = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(checker)
+
+
+class WheelContentsTest(unittest.TestCase):
+ def check(self, files):
+ with tempfile.TemporaryDirectory() as temporary:
+ path = Path(temporary) / "tsfile.whl"
+ with ZipFile(path, "w") as wheel:
+ for name, data in files.items():
+ wheel.writestr(name, data)
+ checker.check_wheel(path)
+
+ def test_runtime_only_wheel_is_accepted(self):
+ self.check(
+ {
+ "tsfile/libtsfile.so.1": b"native",
+ "tsfile/reader.so": b"binding",
+ "tsfile/__init__.py": b"",
+ }
+ )
+
+ def test_sdk_headers_and_import_libraries_are_rejected(self):
+ for name in [
+ "tsfile/include/tsfile/cwrapper/tsfile_cwrapper.h",
+ "tsfile/tsfile.lib",
+ "tsfile/tsfile_cpp.pxd",
+ ]:
+ with self.subTest(name=name), self.assertRaisesRegex(
+ ValueError, "Build-only"
+ ):
+ self.check({"tsfile/tsfile.dll": b"native", name: b"build-input"})
+
+ def test_library_alias_copies_are_rejected(self):
+ with self.assertRaisesRegex(ValueError, "one TsFile runtime library"):
+ self.check(
+ {"tsfile/libtsfile.so": b"native", "tsfile/libtsfile.so.1": b"native"}
+ )
+
+ def test_elf_debug_sections_are_rejected(self):
+ # ELF64 fixture: null section, section-name string table, debug section.
+ strings = b"\0.shstrtab\0.debug_info\0"
+ data = bytearray(64 + 3 * 64 + len(strings) + 1)
+ data[:6] = b"\x7fELF\x02\x01"
+ struct.pack_into("<Q", data, 40, 64)
+ struct.pack_into("<HHH", data, 58, 64, 3, 1)
+ struct.pack_into(
+ "<IIQQQQIIQQ", data, 128, 1, 3, 0, 0, 256, len(strings), 0, 0, 1, 0
+ )
+ struct.pack_into(
+ "<IIQQQQIIQQ", data, 192, 11, 1, 0, 0, 256 + len(strings), 1, 0, 0, 1, 0
+ )
+ data[256 : 256 + len(strings)] = strings
+ with self.assertRaisesRegex(ValueError, "ELF debug sections"):
+ self.check({"tsfile/libtsfile.so.1": bytes(data)})
diff --git a/packaging/tests/test_render_homebrew_formula.py b/packaging/tests/test_render_homebrew_formula.py
new file mode 100644
index 000000000..63711eca0
--- /dev/null
+++ b/packaging/tests/test_render_homebrew_formula.py
@@ -0,0 +1,182 @@
+# 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 subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+MODULE_PATH = ROOT / "packaging/scripts/render_homebrew_formula.py"
+TEMPLATE_PATH = ROOT / "packaging/homebrew/tsfile.rb.in"
+VALUES = {
+ "SOURCE_REPOSITORY": "apache/tsfile",
+ "GIT_SHA": "abcdef1234567890abcdef1234567890abcdef12",
+ "HOMEBREW_VERSION": "2.5.0",
+ "SOURCE_SHA256": "0123456789abcdef" * 4,
+}
+TEMPLATE = """class Tsfile < Formula
+ url "https://github.com/@SOURCE_REPOSITORY@/archive/@GIT_SHA@.tar.gz"
+ version "@HOMEBREW_VERSION@"
+ sha256 "@SOURCE_SHA256@"
+@BOTTLE_BLOCK@
+end
+"""
+BOTTLE_BLOCK = """ bottle do
+ root_url "https://packages.apache.org/artifactory/tsfile/homebrew/release/versions/2.5.0/bottles"
+ sha256 cellar: :any, arm64_sequoia: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ sha256 cellar: :any, sequoia: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ end"""
+
+
+class RenderHomebrewFormulaTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.module = None
+ if MODULE_PATH.is_file():
+ spec = importlib.util.spec_from_file_location(
+ "render_homebrew_formula", MODULE_PATH
+ )
+ cls.module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(cls.module)
+
+ def renderer(self):
+ self.assertIsNotNone(self.module, "Homebrew Formula renderer must exist")
+ return self.module.render_formula
+
+ def test_renders_exact_immutable_source_and_both_bottle_tags(self):
+ rendered = self.renderer()(TEMPLATE, VALUES, BOTTLE_BLOCK)
+ self.assertEqual(
+ rendered,
+ """class Tsfile < Formula
+ url "https://github.com/apache/tsfile/archive/abcdef1234567890abcdef1234567890abcdef12.tar.gz"
+ version "2.5.0"
+ sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+""" + BOTTLE_BLOCK + "\nend\n",
+ )
+
+ def test_pre_bottle_formula_has_no_bottle_block(self):
+ rendered = self.renderer()(TEMPLATE, VALUES)
+ self.assertNotIn("bottle do", rendered)
+ self.assertNotRegex(rendered, r"@[A-Z0-9_]+@")
+
+ def test_rejects_missing_and_undeclared_placeholders(self):
+ render = self.renderer()
+ with self.assertRaises(ValueError):
+ render(
+ TEMPLATE,
+ {key: value for key, value in VALUES.items() if key != "GIT_SHA"},
+ )
+ with self.assertRaises(ValueError):
+ render(TEMPLATE + "@UNKNOWN@", {**VALUES, "UNKNOWN": "anything"})
+
+ def test_rejects_template_tokens_in_bottle_block(self):
+ with self.assertRaises(ValueError):
+ self.renderer()(TEMPLATE, VALUES, BOTTLE_BLOCK.replace("sequoia", "@TAG@"))
+
+ def test_rejects_duplicate_bottle_blocks(self):
+ render = self.renderer()
+ for template, block in (
+ (TEMPLATE, BOTTLE_BLOCK + "\n" + BOTTLE_BLOCK),
+ (TEMPLATE + "\nbottle do\nend\n", BOTTLE_BLOCK),
+ (
+ TEMPLATE.replace("@BOTTLE_BLOCK@", "@BOTTLE_BLOCK@\n@BOTTLE_BLOCK@"),
+ BOTTLE_BLOCK,
+ ),
+ ):
+ with self.subTest(template=template, block=block):
+ with self.assertRaises(ValueError):
+ render(template, VALUES, block)
+
+ def test_rejects_empty_or_non_immutable_source_identity(self):
+ render = self.renderer()
+ for key, value in (
+ ("SOURCE_SHA256", ""),
+ ("SOURCE_SHA256", "bad"),
+ ("GIT_SHA", ""),
+ ("GIT_SHA", "abcdef1"),
+ ("GIT_SHA", "develop"),
+ ("SOURCE_REPOSITORY", 'apache/tsfile"'),
+ ("HOMEBREW_VERSION", 'bad"\nversion "injected'),
+ ("HOMEBREW_VERSION", "2.5.0.dev"),
+ ("HOMEBREW_VERSION", "2.5.0rc1"),
+ ):
+ with self.subTest(key=key, value=value):
+ with self.assertRaises(ValueError):
+ render(TEMPLATE, {**VALUES, key: value})
+
+ def test_preserves_ruby_interpolation_and_other_at_signs(self):
+ extra = "\n# maintainer@example.org\n# #{rpath}\n"
+ self.assertTrue(self.renderer()(TEMPLATE + extra, VALUES).endswith(extra))
+
+ def test_cli_renders_real_template_with_optional_bottle_file(self):
+ self.renderer()
+ template = TEMPLATE_PATH.read_text(encoding="utf-8")
+ self.assertIn(
+ 'keg_only "the SDK installs its dependency header closure"',
+ template,
+ )
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ directory = Path(temporary_directory)
+ output = directory / "tsfile.rb"
+ block = directory / "bottle.rb"
+ block.write_text(BOTTLE_BLOCK, encoding="utf-8")
+ command = [
+ sys.executable,
+ str(MODULE_PATH),
+ "--template",
+ str(TEMPLATE_PATH),
+ "--repository",
+ VALUES["SOURCE_REPOSITORY"],
+ "--git-sha",
+ VALUES["GIT_SHA"],
+ "--version",
+ VALUES["HOMEBREW_VERSION"],
+ "--source-sha256",
+ VALUES["SOURCE_SHA256"],
+ "--output",
+ str(output),
+ ]
+ for bottle_args, expected_block in (
+ ([], ""),
+ (["--bottle-block", str(block)], BOTTLE_BLOCK),
+ ):
+ subprocess.run(command + bottle_args, check=True)
+ rendered = output.read_text(encoding="utf-8")
+ self.assertIn("class Tsfile < Formula", rendered)
+ self.assertIn(
+ 'url "https://github.com/apache/tsfile/archive/abcdef1234567890abcdef1234567890abcdef12.tar.gz"',
+ rendered,
+ )
+ self.assertIn('version "2.5.0"', rendered)
+ self.assertIn(
+ 'sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"',
+ rendered,
+ )
+ self.assertIn("#{rpath}", rendered)
+ self.assertNotRegex(rendered, r"@[A-Z0-9_]+@")
+ self.assertNotIn("\n\n\n", rendered)
+ if expected_block:
+ self.assertIn(BOTTLE_BLOCK, rendered)
+ else:
+ self.assertNotIn("bottle do", rendered)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_sdk_archive_tests.py b/packaging/tests/test_sdk_archive_tests.py
new file mode 100644
index 000000000..f42b4e76f
--- /dev/null
+++ b/packaging/tests/test_sdk_archive_tests.py
@@ -0,0 +1,122 @@
+# 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 io
+from pathlib import Path
+import tarfile
+import tempfile
+import unittest
+
+HERE = Path(__file__).resolve().parent
+
+
+def load_runner():
+ spec = importlib.util.spec_from_file_location(
+ "sdk_archive_tests", HERE / "sdk_archive_tests.py"
+ )
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+class SDKArchiveTestsTest(unittest.TestCase):
+ def archive(self, path, entries):
+ with tarfile.open(path, "w:gz") as archive:
+ for name, data, link in entries:
+ member = tarfile.TarInfo(name)
+ if link is not None:
+ member.type = tarfile.SYMTYPE
+ member.linkname = link
+ archive.addfile(member)
+ else:
+ member.size = len(data)
+ member.mode = 0o755 if "/bin/" in name else 0o644
+ archive.addfile(member, io.BytesIO(data))
+
+ def test_extracts_relocated_sdk_and_preserves_library_symlink(self):
+ runner = load_runner()
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "sdk.tar.gz"
+ self.archive(
+ source,
+ [
+ ("sdk/include/tsfile/cwrapper/tsfile_cwrapper.h", b"header", None),
+ ("sdk/lib/libtsfile.so.2.5.0", b"library", None),
+ ("sdk/lib/libtsfile.so", b"", "libtsfile.so.2.5.0"),
+ ("sdk/bin/tsfile-cli", b"executable", None),
+ ],
+ )
+ prefix = runner.extract_sdk(source, root / "relocated")
+ self.assertEqual(prefix, (root / "relocated/sdk").resolve())
+ self.assertEqual((prefix / "lib/libtsfile.so").read_bytes(), b"library")
+ self.assertTrue((prefix / "bin/tsfile-cli").stat().st_mode & 0o111)
+
+ def test_rejects_archive_paths_and_links_outside_extraction_directory(self):
+ runner = load_runner()
+ for name, link in (
+ ("../escape", None),
+ ("/escape", None),
+ ("C:/escape", None),
+ ("sdk/link", "../../escape"),
+ ):
+ with self.subTest(
+ name=name, link=link
+ ), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "sdk.tar.gz"
+ self.archive(source, [(name, b"payload", link)])
+ with self.assertRaises(ValueError):
+ runner.extract_sdk(source, root / "extracted")
+ self.assertFalse((root / "escape").exists())
+
+ def test_rejects_missing_or_duplicate_sdk_prefix(self):
+ runner = load_runner()
+ for entries in (
+ [],
+ [
+ ("one/include/tsfile/cwrapper/tsfile_cwrapper.h", b"header", None),
+ ("two/include/tsfile/cwrapper/tsfile_cwrapper.h", b"header", None),
+ ],
+ ):
+ with self.subTest(
+ entries=entries
+ ), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "sdk.tar.gz"
+ self.archive(source, entries)
+ with self.assertRaisesRegex(
+ ValueError, "Expected one SDK installation prefix"
+ ):
+ runner.extract_sdk(source, root / "extracted")
+
+ def test_preserves_existing_extraction_directory(self):
+ runner = load_runner()
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ destination = root / "existing"
+ destination.mkdir()
+ sentinel = destination / "keep.txt"
+ sentinel.write_text("keep")
+ with self.assertRaisesRegex(ValueError, "must be empty"):
+ runner.extract_sdk(root / "unused.tar.gz", destination)
+ self.assertEqual(sentinel.read_text(), "keep")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packaging/tests/test_sdk_build_config.py b/packaging/tests/test_sdk_build_config.py
new file mode 100644
index 000000000..af889450f
--- /dev/null
+++ b/packaging/tests/test_sdk_build_config.py
@@ -0,0 +1,163 @@
+# 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.
+#
+
+"""Exercise generated SDK runtime metadata and runtime-only wheel staging."""
+
+import importlib.util
+import json
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+ROOT = Path(__file__).resolve().parents[2]
+spec = importlib.util.spec_from_file_location(
+ "tsfile_build_support", ROOT / "python/_build_support.py"
+)
+support = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(support)
+
+
+class SDKBuildConfigTest(unittest.TestCase):
+ def test_isolated_antlr_lookup_inherits_the_selected_compiler(self):
+ compiler = shutil.which("c++") or shutil.which("g++") or shutil.which("clang++")
+ with tempfile.TemporaryDirectory() as temporary:
+ subprocess.run(
+ [
+ "cmake",
+ f"-DTEST_BINARY_ROOT={temporary}",
+ "-DTEST_CMAKE_GENERATOR=Unix Makefiles",
+ f"-DTEST_CMAKE_MAKE_PROGRAM={shutil.which('make')}",
+ f"-DTEST_CMAKE_CXX_COMPILER={compiler}",
+ "-P",
+ str(ROOT / "cpp/cmake/tests/ANTLR4DependencyTest.cmake"),
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ timeout=120,
+ )
+
+ def test_cmake_exports_resolved_crt_per_configuration_and_after_install(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ (root / "probe.cc").write_text("int probe() { return 0; }\n")
+ (root / "CMakeLists.txt").write_text(
+ "cmake_minimum_required(VERSION 3.15)\n"
+ "project(SDKMetadata LANGUAGES CXX)\n"
+ "include(GNUInstallDirs)\n"
+ "add_library(tsfile SHARED probe.cc)\n"
+ "set_target_properties(tsfile PROPERTIES VERSION 2.5.0 SOVERSION 1)\n"
+ "set(TSFILE_BUILD_SHARED ON)\n"
+ "set(TSFILE_PACKAGE_VERSION 2.5.0)\n"
+ # Test the metadata producer with MSVC's standard runtime
+ # property while using the host compiler only for configure.
+ "set(MSVC TRUE)\n"
+ "set(CMAKE_CXX_COMPILER_ID MSVC)\n"
+ 'set_property(TARGET tsfile PROPERTY MSVC_RUNTIME_LIBRARY "${TEST_RUNTIME}")\n'
+ f'include("{ROOT / "cpp/cmake/TsFileBuildConfig.cmake"}")\n'
+ )
+ for runtime, configuration, flag in [
+ ("", "Release", "/MD"),
+ ("", "Debug", "/MDd"),
+ ("MultiThreaded$<$<CONFIG:Debug>:Debug>", "Release", "/MT"),
+ ("MultiThreaded$<$<CONFIG:Debug>:Debug>", "Debug", "/MTd"),
+ ("MultiThreaded$<$<CONFIG:Debug>:Debug>DLL", "Release", "/MD"),
+ ("MultiThreaded$<$<CONFIG:Debug>:Debug>DLL", "Debug", "/MDd"),
+ ]:
+ with self.subTest(runtime=runtime, configuration=configuration):
+ build = root / (flag[1:] + "-build")
+ subprocess.run(
+ [
+ "cmake",
+ "-S",
+ str(root),
+ "-B",
+ str(build),
+ f"-DCMAKE_BUILD_TYPE={configuration}",
+ f"-DTEST_RUNTIME={runtime}",
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ generated = list(build.rglob("tsfile-build-config.json"))
+ self.assertEqual(len(generated), 1)
+ config = support.read_sdk_config(
+ build, generated[0].parent / "library"
+ )
+ self.assertEqual(support.msvc_runtime_flag(config), flag)
+ self.assertEqual(config["configuration"], configuration)
+ prefix = root / (flag[1:] + "-sdk")
+ subprocess.run(
+ [
+ "cmake",
+ "--install",
+ str(build),
+ "--prefix",
+ str(prefix),
+ "--config",
+ configuration,
+ ],
+ check=True,
+ capture_output=True,
+ )
+ moved = root / (flag[1:] + "-relocated")
+ shutil.move(prefix, moved)
+ self.assertEqual(
+ support.read_sdk_config(moved, moved / "lib/library"), config
+ )
+
+ def test_missing_or_invalid_runtime_metadata_fails_instead_of_guessing(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ with self.assertRaisesRegex(FileNotFoundError, r"Rebuild/install C\+\+"):
+ support.read_sdk_config(root, root / "library")
+ for config in [
+ {"compiler": "MSVC", "msvc_runtime": ""},
+ {"compiler": "GNU", "msvc_runtime": "MultiThreaded"},
+ ]:
+ with self.subTest(config=config), self.assertRaises(ValueError):
+ support.msvc_runtime_flag(config)
+
+ def test_staging_one_soname_removes_stale_alias_copies(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ package = root / "tsfile"
+ package.mkdir()
+ source = root / "libtsfile.so.2.5.0"
+ source.write_bytes(b"native-library")
+ for name in ("libtsfile.so", "libtsfile.so.1", "libtsfile.so.2.5.0"):
+ (package / name).write_bytes(b"old-copy")
+ target = support.copy_runtime_library(
+ package, source, {"runtime_library": "libtsfile.so.1"}
+ )
+ self.assertEqual([p.name for p in package.iterdir()], ["libtsfile.so.1"])
+ self.assertEqual(target.read_bytes(), source.read_bytes())
+
+ def test_runtime_filename_cannot_escape_package(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ info = root / "share/tsfile"
+ info.mkdir(parents=True)
+ (info / "tsfile-build-config.json").write_text(
+ json.dumps({"schema_version": 1, "runtime_library": "../tsfile.dll"})
+ )
+ with self.assertRaisesRegex(ValueError, "shared runtime library"):
+ support.read_sdk_config(root, root / "lib/tsfile.dll")
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
... 603 lines suppressed ...