Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/grog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Grog (Wheel Install Smoke)

on:
pull_request:
branches:
- master
- main
push:
branches:
- master
- main

jobs:
grog:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install grog
shell: bash
run: |
curl -L https://grog.build/latest/grog-linux-amd64 -o grog
chmod +x grog
sudo mv grog /usr/local/bin/grog

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Run grog install smoke tests
run: grog test
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ venv.bak/
.coverage
htmlcov/

# Grog
.grog/
.grog-cache/

# Hatch build hook intermediates
.hatch-ext/

# Jupyter Notebook
.ipynb_checkpoints

Expand Down
54 changes: 54 additions & 0 deletions BUILD.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
targets:
- name: wheel_py
command: uv build --wheel --out-dir dist/grog/py --clear --no-create-gitignore
inputs:
- pyproject.toml
- hatch_build.py
- visdet/**
- README.md
- LICENSE
- MANIFEST.in
outputs:
- dir::dist/grog/py

- name: wheel_cpp
command: VISDET_BUILD_CPP_EXT=1 uv build --wheel --out-dir dist/grog/cpp --clear --no-create-gitignore
inputs:
- pyproject.toml
- hatch_build.py
- visdet/**
- README.md
- LICENSE
- MANIFEST.in
outputs:
- dir::dist/grog/cpp

- name: install_smoke_py_test
dependencies:
- :wheel_py
command: |
set -e
ROOT="$(pwd)"
rm -rf "$ROOT/.grog/venv-py"
uv venv "$ROOT/.grog/venv-py"
uv pip install --python "$ROOT/.grog/venv-py" "$ROOT/dist/grog/py"/*.whl
(cd /tmp && "$ROOT/.grog/venv-py/bin/python" -c "import visdet; from visdet._ext_demo import add, HAS_EXT; assert add(1,2)==3; assert HAS_EXT is False")
mkdir -p "$ROOT/.grog/out"
touch "$ROOT/.grog/out/install_smoke_py.ok"
outputs:
- .grog/out/install_smoke_py.ok

- name: install_smoke_cpp_test
dependencies:
- :wheel_cpp
command: |
set -e
ROOT="$(pwd)"
rm -rf "$ROOT/.grog/venv-cpp"
uv venv "$ROOT/.grog/venv-cpp"
uv pip install --python "$ROOT/.grog/venv-cpp" "$ROOT/dist/grog/cpp"/*.whl
(cd /tmp && "$ROOT/.grog/venv-cpp/bin/python" -c "import visdet; from visdet._ext_demo import add, HAS_EXT; assert add(2,3)==5; assert HAS_EXT is True")
mkdir -p "$ROOT/.grog/out"
touch "$ROOT/.grog/out/install_smoke_cpp.ok"
outputs:
- .grog/out/install_smoke_cpp.ok
2 changes: 2 additions & 0 deletions grog.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Grog configuration
# Targets are defined in BUILD.yaml at repo root.
90 changes: 90 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

import os
import shlex
import shutil
import subprocess
import sysconfig
from pathlib import Path
from typing import Any

from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class CustomBuildHook(BuildHookInterface):
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
# Keep installs working everywhere by default.
if os.environ.get("VISDET_BUILD_CPP_EXT", "0") != "1":
return

# Only build compiled artifacts for wheels.
if self.target_name != "wheel":
return

root = Path(self.root)
build_tmp = root / ".hatch-ext" / "cpp"
build_tmp.mkdir(parents=True, exist_ok=True)

src = build_tmp / "demo_ext.c"
src.write_text(
"""
#include <Python.h>

static PyObject* demo_add(PyObject* self, PyObject* args) {
long a;
long b;
if (!PyArg_ParseTuple(args, \"ll\", &a, &b)) {
return NULL;
}
return PyLong_FromLong(a + b);
}

static PyMethodDef DemoMethods[] = {
{\"add\", demo_add, METH_VARARGS, \"Add two integers.\"},
{NULL, NULL, 0, NULL}
};

static struct PyModuleDef demomodule = {
PyModuleDef_HEAD_INIT,
\"_demo_ext\",
\"visdet demo C extension\",
-1,
DemoMethods
};

PyMODINIT_FUNC PyInit__demo_ext(void) {
return PyModule_Create(&demomodule);
}
""".lstrip(),
encoding="utf-8",
)

ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
if not ext_suffix:
self.app.display_warning("Could not determine EXT_SUFFIX; skipping C extension build")
return

out = build_tmp / f"_demo_ext{ext_suffix}"

include_dir = sysconfig.get_path("include")
ldshared = sysconfig.get_config_var("LDSHARED") or "cc -shared"
cflags = sysconfig.get_config_var("CFLAGS") or ""
cppflags = sysconfig.get_config_var("CPPFLAGS") or ""

cmd = (
shlex.split(ldshared)
+ shlex.split(cflags)
+ shlex.split(cppflags)
+ [f"-I{include_dir}", "-o", str(out), str(src)]
)

self.app.display_info(f"Building C extension: {out.name}")
subprocess.check_call(cmd, cwd=str(root))

# Ensure hatchling includes the generated artifact in the wheel (without polluting the source tree).
build_data.setdefault("force_include", {})[str(out)] = f"visdet/_ext_demo/{out.name}"

# Avoid accidentally shipping .dSYM bundles (macOS).
dsym = out.with_suffix(out.suffix + ".dSYM")
if dsym.exists():
shutil.rmtree(dsym)
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,25 @@ Documentation = "https://binitai.github.io/visdet/"
# Exclude nested pyproject.toml that was part of old workspace structure
exclude = [
"visdet/pyproject.toml",
# Don't ship build intermediates
".hatch-ext/**",
# Don't ship macOS debug bundles
"visdet/_ext_demo/*.dSYM/**",
]

# Hatchling has built-in ignores for directories like `dist/`, which clashes
# with our internal module path `visdet/engine/dist/`. Force-include it.
artifacts = [
"visdet/engine/dist/**",
]

[tool.hatch.build.targets.wheel]
# The package code is in visdet/ - tell hatchling where to find it
packages = ["visdet"]

[tool.hatch.build.hooks.custom]
path = "hatch_build.py"

[tool.ruff]
line-length = 120
target-version = "py310"
Expand Down
24 changes: 24 additions & 0 deletions visdet/_ext_demo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

import importlib
import importlib.util

_DEMO_EXT_SPEC = importlib.util.find_spec("visdet._ext_demo._demo_ext")
HAS_EXT = _DEMO_EXT_SPEC is not None

if HAS_EXT:
_demo_ext = importlib.import_module("visdet._ext_demo._demo_ext")
else:
_demo_ext = None


def add(a: int, b: int) -> int:
"""Add two integers.

Uses the compiled extension when available, otherwise falls back to Python.
"""

if _demo_ext is None:
return int(a) + int(b)

return int(_demo_ext.add(int(a), int(b)))