You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tsfile.apache.org by co...@apache.org on 2026/09/17 07:36:22 UTC
(tsfile) 07/22: feat(ci): add native package development versions
This is an automated email from the ASF dual-hosted git repository.
ColinLeeo pushed a commit to branch feat/native-package-artifacts
in repository https://gitbox.apache.org/repos/asf/tsfile.git
commit ba63f5b7b4ccc72e72c1c86c0d945cb72eac3c60
Author: ColinLee <sh...@163.com>
AuthorDate: Thu Sep 10 18:09:03 2026 +0800
feat(ci): add native package development versions
---
cpp/CMakeLists.txt | 23 +++-
packaging/scripts/native_package_versions.py | 128 +++++++++++++++++++++
packaging/tests/test_native_package_versions.py | 144 ++++++++++++++++++++++++
3 files changed, 289 insertions(+), 6 deletions(-)
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 567286fa5..4b6f868b2 100755
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -47,6 +47,14 @@ if ("${TSFILE_PACKAGE_VERSION}" STREQUAL "")
"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]+$")
@@ -547,7 +555,7 @@ if (TSFILE_ENABLE_CPACK)
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_PACKAGE_VERSION}")
+ 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)
@@ -560,25 +568,28 @@ if (TSFILE_ENABLE_CPACK)
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_PACKAGE_VERSION})")
+ "tsfile (= ${TSFILE_DEBIAN_PACKAGE_VERSION})")
set(CPACK_DEBIAN_TOOLS_PACKAGE_DEPENDS
- "tsfile (= ${TSFILE_PACKAGE_VERSION})")
+ "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_RELEASE "1")
+ 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_PACKAGE_VERSION}-1")
+ "tsfile = ${TSFILE_RPM_PACKAGE_VERSION}-${TSFILE_RPM_PACKAGE_RELEASE}")
set(CPACK_RPM_TOOLS_PACKAGE_REQUIRES
- "tsfile = ${TSFILE_PACKAGE_VERSION}-1")
+ "tsfile = ${TSFILE_RPM_PACKAGE_VERSION}-${TSFILE_RPM_PACKAGE_RELEASE}")
include(CPack)
unset(_TSFILE_PACKAGE_VERSION_PARTS)
diff --git a/packaging/scripts/native_package_versions.py b/packaging/scripts/native_package_versions.py
new file mode 100644
index 000000000..f9849267f
--- /dev/null
+++ b/packaging/scripts/native_package_versions.py
@@ -0,0 +1,128 @@
+#!/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 deterministic development 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"^(?P<base>(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))\.dev$"
+)
+_CMAKE_VERSION_RE = re.compile(
+ r"^\s*set\(\s*TsFile_CPP_VERSION\s+(?P<version>[^\s)]+)\s*\)\s*(?:#.*)?$",
+ re.MULTILINE,
+)
+_BUILD_DATE_RE = re.compile(r"^[0-9]{8}$")
+_GIT_SHA_RE = re.compile(r"^[0-9a-fA-F]{7,}$")
+
+
+def read_cpp_version(path: Path) -> str:
+ """Read and validate the development 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 TsFile_CPP_VERSION as MAJOR.MINOR.PATCH.dev")
+ return match.group("version")
+
+
+def _positive_integer(value: int, name: str) -> str:
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
+ raise ValueError(f"{name} must be a positive integer")
+ return str(value)
+
+
+def build_versions(
+ source_version: str,
+ build_date: str,
+ run_number: int,
+ run_attempt: int,
+ git_sha: str,
+) -> dict[str, str]:
+ """Create archive, DEB, RPM, and Homebrew versions from one build identity."""
+ source_match = _SOURCE_VERSION_RE.fullmatch(source_version)
+ if source_match is None:
+ raise ValueError("source_version must be MAJOR.MINOR.PATCH.dev")
+ if _BUILD_DATE_RE.fullmatch(build_date) is None:
+ raise ValueError("build_date must be YYYYMMDD")
+ if _GIT_SHA_RE.fullmatch(git_sha) is None:
+ raise ValueError("git_sha must contain at least seven hexadecimal characters")
+
+ run_number_text = _positive_integer(run_number, "run_number")
+ run_attempt_text = _positive_integer(run_attempt, "run_attempt")
+ base_version = source_match.group("base")
+ short_sha = git_sha[:7].lower()
+ build_identity = f"{build_date}.{run_number_text}.{run_attempt_text}.g{short_sha}"
+
+ return {
+ "base_version": base_version,
+ "logical_version": f"{base_version}.dev0+{build_identity}",
+ "deb_version": f"{base_version}~dev0+{build_identity}-1",
+ "rpm_version": base_version,
+ "rpm_release": f"0.dev0.{build_identity}.el9",
+ "archive_version": f"{base_version}-dev0.{build_identity}",
+ "homebrew_version": f"{base_version}.dev0.{build_identity}",
+ }
+
+
+def _parse_positive_integer(value: str) -> int:
+ if not re.fullmatch(r"[1-9][0-9]*", value):
+ raise argparse.ArgumentTypeError("must be a positive integer")
+ return int(value)
+
+
+def _argument_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="Generate deterministic development versions for native TsFile packages."
+ )
+ parser.add_argument("--cmake-file", type=Path, required=True)
+ parser.add_argument("--build-date", required=True)
+ parser.add_argument("--run-number", type=_parse_positive_integer, required=True)
+ parser.add_argument("--run-attempt", type=_parse_positive_integer, required=True)
+ parser.add_argument("--git-sha", 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 the generated versions to JSON and, optionally, GitHub outputs."""
+ arguments = _argument_parser().parse_args(argv)
+ versions = build_versions(
+ read_cpp_version(arguments.cmake_file),
+ arguments.build_date,
+ arguments.run_number,
+ arguments.run_attempt,
+ arguments.git_sha,
+ )
+ arguments.json_out.write_text(json.dumps(versions, indent=2) + "\n", encoding="utf-8")
+ if arguments.github_output is not None:
+ arguments.github_output.write_text(
+ "\n".join(f"{key}={value}" for key, value in versions.items()) + "\n",
+ encoding="utf-8",
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(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..6b864c44c
--- /dev/null
+++ b/packaging/tests/test_native_package_versions.py
@@ -0,0 +1,144 @@
+# 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"
+CPP_CMAKE_FILE = REPOSITORY_ROOT / "cpp/CMakeLists.txt"
+
+
+def load_version_module():
+ if not MODULE_PATH.is_file():
+ return None
+ spec = importlib.util.spec_from_file_location("native_package_versions", MODULE_PATH)
+ if spec is None or spec.loader is None:
+ raise ImportError(f"cannot load native package version helper from {MODULE_PATH}")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+class NativePackageVersionsTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.module = load_version_module()
+
+ def require_module(self):
+ self.assertIsNotNone(
+ self.module, "native_package_versions helper must exist for native package builds"
+ )
+ return self.module
+
+ def test_reads_development_version_from_cpp_cmake(self):
+ module = self.require_module()
+
+ self.assertEqual(module.read_cpp_version(CPP_CMAKE_FILE), "2.5.0.dev")
+
+ def test_builds_exact_development_package_versions(self):
+ module = self.require_module()
+
+ self.assertEqual(
+ module.build_versions("2.5.0.dev", "20260910", 123, 1, "abcdef123456"),
+ {
+ "base_version": "2.5.0",
+ "logical_version": "2.5.0.dev0+20260910.123.1.gabcdef1",
+ "deb_version": "2.5.0~dev0+20260910.123.1.gabcdef1-1",
+ "rpm_version": "2.5.0",
+ "rpm_release": "0.dev0.20260910.123.1.gabcdef1.el9",
+ "archive_version": "2.5.0-dev0.20260910.123.1.gabcdef1",
+ "homebrew_version": "2.5.0.dev0.20260910.123.1.gabcdef1",
+ },
+ )
+
+ def test_rejects_invalid_source_versions(self):
+ module = self.require_module()
+
+ for source_version in ("2.5.0", "2.5.dev"):
+ with self.subTest(source_version=source_version):
+ with self.assertRaises(ValueError):
+ module.build_versions(source_version, "20260910", 123, 1, "abcdef123456")
+
+ def test_rejects_invalid_build_identity_fields(self):
+ module = self.require_module()
+
+ invalid_cases = (
+ ("2026-09-10", 123, 1, "abcdef123456"),
+ ("2026091", 123, 1, "abcdef123456"),
+ ("20260910", "run", 1, "abcdef123456"),
+ ("20260910", 123, "attempt", "abcdef123456"),
+ ("20260910", 123, 1, "abcdef"),
+ ("20260910", 123, 1, "abcdefg123456"),
+ )
+ for build_date, run_number, run_attempt, git_sha in invalid_cases:
+ with self.subTest(
+ build_date=build_date,
+ run_number=run_number,
+ run_attempt=run_attempt,
+ git_sha=git_sha,
+ ):
+ with self.assertRaises(ValueError):
+ module.build_versions(
+ "2.5.0.dev", build_date, run_number, run_attempt, git_sha
+ )
+
+ def test_cli_writes_json_and_github_outputs(self):
+ module = self.require_module()
+ expected = module.build_versions("2.5.0.dev", "20260910", 123, 1, "abcdef123456")
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ temporary_path = Path(temporary_directory)
+ json_output = temporary_path / "versions.json"
+ github_output = temporary_path / "github-output.txt"
+ subprocess.run(
+ [
+ sys.executable,
+ str(MODULE_PATH),
+ "--cmake-file",
+ str(CPP_CMAKE_FILE),
+ "--build-date",
+ "20260910",
+ "--run-number",
+ "123",
+ "--run-attempt",
+ "1",
+ "--git-sha",
+ "abcdef123456",
+ "--json-out",
+ str(json_output),
+ "--github-output",
+ str(github_output),
+ ],
+ check=True,
+ )
+
+ self.assertEqual(json.loads(json_output.read_text(encoding="utf-8")), expected)
+ self.assertEqual(
+ github_output.read_text(encoding="utf-8").splitlines(),
+ [f"{key}={value}" for key, value in expected.items()],
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()