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__":

Reply via email to