This is an automated email from the ASF dual-hosted git repository.
junrushao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new ac63fb9 chore(build): Introduce `setuptools_scm` for better
versioning (#154)
ac63fb9 is described below
commit ac63fb9b0e13aed9da18153398e5f2332cbbc02f
Author: Junru Shao <[email protected]>
AuthorDate: Sun Oct 26 16:26:19 2025 -0700
chore(build): Introduce `setuptools_scm` for better versioning (#154)
This PR introduces `setuptools_scm` and a linter to make versioning
easier for Python, C++ and Rust package.
More specifically,
- versioning of Python package is in full control of `setuptools_scm`,
which is guaranteed to be correct.
- C++ and Rust versioning is checked by a `setuptools_scm`-based linter,
which is run per commit.
The tool can be used standalone as:
```python
>>> python tests/lint/check_version.py --cpp --rust
Project version: 0.1.1.dev9+g43f0820f6.d20251021
major: 0
minor: 1
micro: 1
pre: None
dev: 9
local: g43f0820f6.d20251021
post: None
base_version: 0.1.1
release: (0, 1, 1)
public: 0.1.1.dev9
is_prerelease: True
is_postrelease: False
is_devrelease: True
[C++] include/tvm/ffi/c_api.h: Macro version mismatch: found 0.1.0,
expected 0.1.1.
[Rust] rust/tvm-ffi/Cargo.toml: version not compatible with project
version. Allowed: ['0.1.1']; got: 0.1.0-alpha.0.
[Rust] rust/tvm-ffi-macros/Cargo.toml: version not compatible with project
version. Allowed: ['0.1.1']; got: 0.1.0-alpha.0.
[Rust] rust/tvm-ffi-sys/Cargo.toml: version not compatible with project
version. Allowed: ['0.1.1']; got: 0.1.0-alpha.0.
```
---
.github/actions/detect-skip-ci/action.yaml | 2 +-
.github/workflows/ci_test.yml | 18 ++-
.github/workflows/publish_wheel.yml | 8 +-
.gitignore | 3 +
.pre-commit-config.yaml | 11 +-
docs/conf.py | 9 +-
include/tvm/ffi/c_api.h | 2 +-
pyproject.toml | 11 +-
python/tvm_ffi/__init__.py | 9 +-
tests/lint/check_version.py | 218 +++++++++++++++++++++--------
10 files changed, 213 insertions(+), 78 deletions(-)
diff --git a/.github/actions/detect-skip-ci/action.yaml
b/.github/actions/detect-skip-ci/action.yaml
index f487949..36d9b54 100644
--- a/.github/actions/detect-skip-ci/action.yaml
+++ b/.github/actions/detect-skip-ci/action.yaml
@@ -35,7 +35,7 @@ runs:
using: "composite"
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml
index b463682..380e97e 100644
--- a/.github/workflows/ci_test.yml
+++ b/.github/workflows/ci_test.yml
@@ -34,9 +34,10 @@ jobs:
should_skip_ci_commit: ${{ steps.detect.outputs.should_skip_ci_commit }}
should_skip_ci_docs_only: ${{
steps.detect.outputs.should_skip_ci_docs_only }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
- fetch-depth: 2
+ fetch-depth: 0
+ fetch-tags: true
- name: Detect skip ci and docs changes
id: detect
uses: ./.github/actions/detect-skip-ci
@@ -49,16 +50,21 @@ jobs:
needs: [prepare]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ fetch-tags: true
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd #
v3.0.1
doc:
needs: [lint, prepare]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
submodules: recursive
+ fetch-depth: 0
+ fetch-tags: true
- name: Set up uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 #
v6.7.0
with:
@@ -85,9 +91,11 @@ jobs:
- {os: macos-14, arch: arm64, python_version: '3.13'}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
submodules: recursive
+ fetch-depth: 0
+ fetch-tags: true
- name: Print current commit
run: git log -1 --oneline
diff --git a/.github/workflows/publish_wheel.yml
b/.github/workflows/publish_wheel.yml
index 6c6ea86..f337834 100644
--- a/.github/workflows/publish_wheel.yml
+++ b/.github/workflows/publish_wheel.yml
@@ -52,11 +52,13 @@ jobs:
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4
if: matrix.os != 'macOS' || matrix.arch != 'arm64'
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Use the input only for manual runs; otherwise use the triggering
ref
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch
|| github.ref }}
submodules: recursive
+ fetch-depth: 0
+ fetch-tags: true
- uses: ./.github/actions/detect-env-vars
id: env_vars
@@ -97,10 +99,12 @@ jobs:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch
|| github.ref }}
submodules: recursive
+ fetch-depth: 0
+ fetch-tags: true
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 #
v6.7.0
diff --git a/.gitignore b/.gitignore
index ae2591b..b97498d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# version file is auto-generated
+python/tvm_ffi/_version.py
+
/tmp/
*.bak
# Byte-compiled / optimized / DLL files
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index be42c58..3c2c43d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -38,14 +38,17 @@ repos:
verbose: false
- repo: local
hooks:
- - id: check-version
- name: check version
- entry: python tests/lint/check_version.py
+ - id: check-version-consistency
+ name: check version consistency
+ entry: python tests/lint/check_version.py --cpp # TODO: add `--rust`
once Rust binding matures
language: python
language_version: python3
+ additional_dependencies:
+ - setuptools-scm
+ - packaging
+ - tomli
pass_filenames: false
verbose: false
- additional_dependencies: [tomli]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
diff --git a/docs/conf.py b/docs/conf.py
index befe7c1..7118b41 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,8 +27,8 @@ import subprocess
import sys
from pathlib import Path
+import setuptools_scm
import sphinx
-import tomli
os.environ["TVM_FFI_BUILD_DOCS"] = "1"
@@ -43,10 +43,9 @@ _DOCS_DIR = Path(__file__).resolve().parent
_RUST_DIR = _DOCS_DIR.parent / "rust"
# -- General configuration ------------------------------------------------
-# Load version from pyproject.toml
-with Path("../pyproject.toml").open("rb") as f:
- pyproject_data = tomli.load(f)
-__version__ = pyproject_data["project"]["version"]
+# Determine version without reading pyproject.toml
+# Always use setuptools_scm (assumed available in docs env)
+__version__ = setuptools_scm.get_version(root="..")
project = "tvm-ffi"
diff --git a/include/tvm/ffi/c_api.h b/include/tvm/ffi/c_api.h
index 5e16ed1..240dd76 100644
--- a/include/tvm/ffi/c_api.h
+++ b/include/tvm/ffi/c_api.h
@@ -61,7 +61,7 @@
/*! \brief TVM FFI minor version. */
#define TVM_FFI_VERSION_MINOR 1
/*! \brief TVM FFI patch version. */
-#define TVM_FFI_VERSION_PATCH 0
+#define TVM_FFI_VERSION_PATCH 1
#ifdef __cplusplus
extern "C" {
diff --git a/pyproject.toml b/pyproject.toml
index 92e1fce..57de316 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,7 @@
[project]
name = "apache-tvm-ffi"
-version = "0.1.0"
+dynamic = ["version"]
description = "tvm ffi"
authors = [{ name = "TVM FFI team" }]
@@ -61,6 +61,8 @@ dev = [
"cython",
"cmake",
"scikit-build-core",
+ "tomli",
+ "setuptools-scm",
]
docs = [
"autodocsumm",
@@ -85,6 +87,7 @@ docs = [
"sphinxcontrib-napoleon",
"sphinxcontrib_httpdomain",
"setuptools",
+ "setuptools-scm",
"tomli",
"urllib3",
]
@@ -94,10 +97,11 @@ tvm-ffi-config = "tvm_ffi.config:__main__"
tvm-ffi-stubgen = "tvm_ffi.stub.stubgen:__main__"
[build-system]
-requires = ["scikit-build-core>=0.10.0", "cython", "cmake>=3.18", "ninja"]
+requires = ["scikit-build-core>=0.10.0", "cython", "setuptools-scm"]
build-backend = "scikit_build_core.build"
[tool.scikit-build]
+metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
wheel.py-api = "cp312"
minimum-version = "build-system.requires"
ninja.version = ">=1.11"
@@ -142,6 +146,7 @@ sdist.include = [
"/python/tvm_ffi/**/*.pyx",
"/python/tvm_ffi/**/*.pyi",
"/python/tvm_ffi/py.typed",
+ "/python/tvm_ffi/_version.py",
# Third party files
"/3rdparty/libbacktrace/**/*",
@@ -267,3 +272,5 @@ ignore_missing_imports = true
docs = { requires-python = ">=3.13" }
[tool.setuptools_scm]
+version_file = "python/tvm_ffi/_version.py"
+write_to = "python/tvm_ffi/_version.py"
diff --git a/python/tvm_ffi/__init__.py b/python/tvm_ffi/__init__.py
index 8043524..79f81a1 100644
--- a/python/tvm_ffi/__init__.py
+++ b/python/tvm_ffi/__init__.py
@@ -18,7 +18,6 @@
# order matters here so we need to skip isort here
# isort: skip_file
-__version__ = "0.1.0"
# HACK: try importing torch first, to avoid a potential
# symbol conflict when both torch and tvm_ffi are imported.
@@ -61,6 +60,13 @@ from . import cpp
# optional module to speedup dlpack conversion
from . import _optional_torch_c_dlpack
+
+try:
+ from ._version import __version__, __version_tuple__ # type:
ignore[import-not-found]
+except ImportError:
+ __version__ = "0.0.0.dev0"
+ __version_tuple__ = (0, 0, 0, "dev0", "7d34eb8ab.d20250913")
+
__all__ = [
"Array",
"DLDeviceType",
@@ -74,6 +80,7 @@ __all__ = [
"StreamContext",
"Tensor",
"__version__",
+ "__version_tuple__",
"access_path",
"convert",
"cpp",
diff --git a/tests/lint/check_version.py b/tests/lint/check_version.py
index 10aca3b..f7c19e0 100644
--- a/tests/lint/check_version.py
+++ b/tests/lint/check_version.py
@@ -14,76 +14,180 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-"""Helper tool to check version consistency between pyproject.toml and
__init__.py."""
+"""Version consistency linter across Python, C++, and Rust.
+
+This script checks that:
+ 1) C++ version macros in headers are internally consistent and match the
+ canonical project version (major/minor/micro) derived from setuptools_scm.
+ 2) Rust crate versions are internally consistent and compatible with
+ the canonical project version.
+
+Usage: python tests/lint/check_version.py [--cpp] [--rust]
+"""
from __future__ import annotations
+import argparse
import re
import sys
from pathlib import Path
+from typing import Any
+import setuptools_scm
import tomli
-
-
-def read_pyproject_version(pyproject_path: Path) -> str | None:
- """Read version from pyproject.toml."""
- with pyproject_path.open("rb") as f:
- data = tomli.load(f)
-
- return data.get("project", {}).get("version")
-
-
-def read_init_version(init_path: Path) -> str | None:
- """Read __version__ from __init__.py."""
- with init_path.open(encoding="utf-8") as f:
- content = f.read()
-
- # Look for __version__ = "..." pattern
- match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
- if match:
- return match.group(1)
- return None
-
-
-def update_init_version(init_path: Path, new_version: str) -> bool:
- """Update __version__ in __init__.py."""
- with init_path.open(encoding="utf-8") as f:
- content = f.read()
-
- # Replace the version line
- new_content = re.sub(
- r'__version__\s*=\s*["\'][^"\']+["\']', f'__version__ =
"{new_version}"', content
+from packaging.version import Version as packaging_version
+
+RE_MAJOR = re.compile(r"^\s*#\s*define\s+TVM_FFI_VERSION_MAJOR\s+(\d+)\b")
+RE_MINOR = re.compile(r"^\s*#\s*define\s+TVM_FFI_VERSION_MINOR\s+(\d+)\b")
+RE_PATCH = re.compile(r"^\s*#\s*define\s+TVM_FFI_VERSION_PATCH\s+(\d+)\b")
+
+
+def _version_info() -> dict[str, Any]:
+ """Return project version information using setuptools_scm."""
+ version = setuptools_scm.get_version()
+ v = packaging_version(version)
+
+ return {
+ "full": version,
+ "major": v.major,
+ "minor": v.minor,
+ "micro": v.micro,
+ "pre": v.pre,
+ "dev": v.dev,
+ "local": v.local,
+ "post": v.post,
+ "release": v.release,
+ "is_prerelease": v.is_prerelease,
+ "is_postrelease": v.is_postrelease,
+ "is_devrelease": v.is_devrelease,
+ "base_version": v.base_version,
+ "public": v.public,
+ }
+
+
+def _map_pep440_pre_to_semver(pre: tuple[str, int] | None) -> str | None:
+ if pre is None:
+ return None
+ tag, num = pre
+ tag = tag.lower()
+ if tag in {"a", "alpha"}:
+ tag = "alpha"
+ elif tag in {"b", "beta"}:
+ tag = "beta"
+ elif tag in {"rc", "c"}:
+ tag = "rc"
+ else:
+ return None
+ return f"{tag}.{num}"
+
+
+def _check_cpp(version_info: dict) -> list[str]:
+ errors: list[str] = []
+ c_api_path = Path("include") / "tvm" / "ffi" / "c_api.h"
+ if not c_api_path.exists():
+ errors.append(f"[C++] Missing expected header file: {c_api_path}")
+ return errors
+
+ def _scan_cpp_macros() -> tuple[int, int, int]:
+ file = c_api_path
+ major = minor = patch = None
+ for line in file.read_text(encoding="utf-8").splitlines():
+ if m := RE_MAJOR.match(line):
+ major = int(m.group(1))
+ if m := RE_MINOR.match(line):
+ minor = int(m.group(1))
+ if m := RE_PATCH.match(line):
+ patch = int(m.group(1))
+ return major, minor, patch
+
+ (major, minor, patch) = _scan_cpp_macros()
+
+ if major is None or minor is None or patch is None:
+ errors.append(f"[C++] {c_api_path}: No version macros found: {major=},
{minor=}, {patch=}.")
+ return errors
+
+ exp_major, exp_minor, exp_patch = (
+ version_info["major"],
+ version_info["minor"],
+ version_info["micro"],
)
-
- with init_path.open("w", encoding="utf-8") as f:
- f.write(new_content)
-
- return True
+ if (major, minor, patch) != (exp_major, exp_minor, exp_patch):
+ errors.append(
+ f"[C++] {c_api_path}: Macro version mismatch: found
{major}.{minor}.{patch}, "
+ f"expected {exp_major}.{exp_minor}.{exp_patch}."
+ )
+ return errors
+
+
+def _check_rust(version_info: dict) -> list[str]:
+ errors: list[str] = []
+ rust_dir = Path("rust")
+ found_versions: dict[Path, str] = {}
+ for path in [
+ rust_dir / "tvm-ffi" / "Cargo.toml",
+ rust_dir / "tvm-ffi-macros" / "Cargo.toml",
+ rust_dir / "tvm-ffi-sys" / "Cargo.toml",
+ ]:
+ found_versions[path] =
tomli.loads(path.read_text(encoding="utf-8"))["package"]["version"]
+
+ if not found_versions:
+ # No crates found, skip silently
+ return errors
+
+ # 1) All crates must agree on a single version
+ unique_versions = set(found_versions.values())
+ if len(unique_versions) > 1:
+ errors.append(
+ "[Rust] Crates have inconsistent versions: "
+ + ", ".join(f"{p} -> {v}" for p, v in
sorted(found_versions.items()))
+ )
+
+ # 2) Optionally enforce compatibility with Python version
+ base = version_info["base_version"]
+ allowed: set[str] = {base}
+ pre = _map_pep440_pre_to_semver(version_info.get("pre"))
+ if pre:
+ allowed.add(f"{base}-{pre}")
+ allowed = sorted(allowed)
+ for path, v in found_versions.items():
+ if v not in allowed:
+ errors.append(
+ f"[Rust] {path}: version not compatible with project version.
Allowed: {allowed}; got: {v}."
+ )
+ return errors
def main() -> int:
- """Execute the main function."""
- # Hardcoded paths
- pyproject_path = Path("pyproject.toml")
- init_path = Path("python/tvm_ffi/__init__.py")
-
- # Read versions
- pyproject_version = read_pyproject_version(pyproject_path)
- init_version = read_init_version(init_path)
-
- if pyproject_version is None or init_version is None:
- return 1
-
- if pyproject_version == init_version:
- print("Version check passed!")
- return 0
- else:
- print("Version check failed!")
- print(f"pyproject.toml version: {pyproject_version}")
- print(f"__init__.py version: {init_version}")
- print("Run precommit locally to fix the version.")
- update_init_version(init_path, pyproject_version)
+ parser = argparse.ArgumentParser(description="Check version consistency
across languages")
+ parser.add_argument("--cpp", action="store_true", help="Check C++ version
macros")
+ parser.add_argument("--rust", action="store_true", help="Check Rust crate
versions")
+ args = parser.parse_args()
+ info = _version_info()
+ print(
+ f"Project version: {info['full']}\n"
+ f" major: {info['major']}\n"
+ f" minor: {info['minor']}\n"
+ f" micro: {info['micro']}\n"
+ f" pre: {info['pre']}\n"
+ f" dev: {info['dev']}\n"
+ f" local: {info['local']}\n"
+ f" post: {info['post']}\n"
+ f" base_version: {info['base_version']}\n"
+ f" release: {info['release']}\n"
+ f" public: {info['public']}\n"
+ f" is_prerelease: {info['is_prerelease']}\n"
+ f" is_postrelease: {info['is_postrelease']}\n"
+ f" is_devrelease: {info['is_devrelease']}"
+ )
+ errors: list[str] = []
+ if args.cpp:
+ errors += _check_cpp(info)
+ if args.rust:
+ errors += _check_rust(info)
+ if errors:
+ print("\n".join(errors))
return 1
+ return 0
if __name__ == "__main__":