From b396e8e9f68a0797eb9e3a2ef902447facfd8da2 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 18:59:10 -0600 Subject: [PATCH 01/20] fix: include libfoundation_models.dylib in wheels and build for multiple Python versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dylib was only included in source distributions via MANIFEST.in but not in wheels. Wheels require files to be part of package data. Now copies the dylib to the package directory during build and includes it via package_data in pyproject.toml. Also improved CI workflow to build wheels for Python 3.9-3.13 with explicit dylib verification before publishing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 83 ++++++++++++++++++++++----- pyproject.toml | 2 +- setup.py | 10 +++- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index fe46072..cc641f7 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -10,9 +10,9 @@ permissions: id-token: write # Required for trusted publishing to PyPI jobs: - build: - name: Build package on macOS - runs-on: macos-26 + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest steps: - name: Checkout code @@ -23,52 +23,107 @@ jobs: with: python-version: "3.11" + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build sdist + run: | + python -m build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + build-wheels: + name: Build wheels on macOS for Python ${{ matrix.python-version }} + runs-on: macos-26 + strategy: + matrix: + # Build wheels for all supported Python versions + # Note: Python 3.8 may not be available on macos-26 + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Check macOS version run: | echo "macOS version:" sw_vers echo "Architecture:" uname -m + echo "Python version:" + python --version - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build wheel setuptools Cython>=3.0.0 - - name: Build package + - name: Build wheel run: | - python -m build + python -m build --wheel - - name: Check build artifacts + - name: Verify wheel contents run: | echo "Build artifacts:" ls -lh dist/ echo "" - echo "Verifying wheel contents:" - unzip -l dist/*.whl || true + echo "Checking for dylib in wheel:" + if unzip -l dist/*.whl | grep -q "libfoundation_models.dylib"; then + echo "✓ libfoundation_models.dylib found in wheel" + unzip -l dist/*.whl | grep -E "(dylib|\.so)" + else + echo "✗ ERROR: libfoundation_models.dylib NOT found in wheel!" + echo "Full wheel contents:" + unzip -l dist/*.whl + exit 1 + fi - - name: Upload build artifacts + - name: Upload wheel uses: actions/upload-artifact@v4 with: - name: python-package-distributions - path: dist/ + name: wheel-${{ matrix.python-version }} + path: dist/*.whl publish: name: Publish to PyPI - needs: build + needs: [build-sdist, build-wheels] runs-on: ubuntu-latest steps: - - name: Download build artifacts + - name: Download all artifacts uses: actions/download-artifact@v4 with: - name: python-package-distributions path: dist/ + merge-multiple: true - name: Verify artifacts run: | echo "Downloaded artifacts:" ls -lh dist/ + echo "" + echo "Verifying each wheel contains dylib:" + for wheel in dist/*.whl; do + echo "Checking $wheel:" + if unzip -l "$wheel" | grep -q "libfoundation_models.dylib"; then + echo " ✓ dylib present" + else + echo " ✗ ERROR: dylib missing!" + exit 1 + fi + done - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/pyproject.toml b/pyproject.toml index e82714f..26279e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ packages = ["applefoundationmodels"] include-package-data = true [tool.setuptools.package-data] -applefoundationmodels = ["py.typed", "*.pxd"] +applefoundationmodels = ["py.typed", "*.pxd", "*.dylib"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/setup.py b/setup.py index 964a74d..54f2ea4 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ import sys import platform import subprocess +import shutil from pathlib import Path from setuptools import setup, Extension from setuptools.command.build_ext import build_ext as _build_ext @@ -17,10 +18,12 @@ # Determine paths REPO_ROOT = Path(__file__).parent.resolve() LIB_DIR = REPO_ROOT / "lib" -SWIFT_DIR = REPO_ROOT / "applefoundationmodels" / "swift" +PKG_DIR = REPO_ROOT / "applefoundationmodels" +SWIFT_DIR = PKG_DIR / "swift" SWIFT_SRC = SWIFT_DIR / "foundation_models.swift" DYLIB_PATH = LIB_DIR / "libfoundation_models.dylib" SWIFTMODULE_PATH = LIB_DIR / "foundation_models.swiftmodule" +PKG_DYLIB_PATH = PKG_DIR / "libfoundation_models.dylib" # Detect architecture ARCH = platform.machine() @@ -100,6 +103,11 @@ def build_swift_dylib(self): print(f"✓ Successfully built: {DYLIB_PATH}") print(f" Size: {DYLIB_PATH.stat().st_size / 1024:.1f} KB") + # Copy dylib to package directory so it's included in wheels + print(f"Copying dylib to package directory: {PKG_DYLIB_PATH}") + shutil.copy2(DYLIB_PATH, PKG_DYLIB_PATH) + print(f"✓ Dylib copied to package directory") + except subprocess.CalledProcessError as e: print(f"✗ Swift compilation failed") print(e.stderr) From e74a24a3c60aa4b1c589aaefac4fd7d9face0b4d Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 19:04:02 -0600 Subject: [PATCH 02/20] chore: uv.lock update --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index e5212c8..4157c74 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ [[package]] name = "apple-foundation-models" -version = "0.1.0" +version = "0.1.3" source = { editable = "." } dependencies = [ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, From 67ad97d24559e9dbc8148e7ffaa7fcd3bb70e053 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 19:08:13 -0600 Subject: [PATCH 03/20] chore: bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7d9111..db6aae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "apple-foundation-models" -version = "0.1.3" +version = "0.1.4" description = "Python bindings for Apple's FoundationModels framework - on-device AI" readme = "README.md" license = { text = "MIT" } From 055e1bef8bd59f2104f6043b14c543e614fa1380 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 19:12:12 -0600 Subject: [PATCH 04/20] ci: remove build-sdist --- .github/workflows/publish-to-pypi.yml | 30 +-------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index cc641f7..9910e01 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -10,34 +10,6 @@ permissions: id-token: write # Required for trusted publishing to PyPI jobs: - build-sdist: - name: Build source distribution - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build - - - name: Build sdist - run: | - python -m build --sdist - - - name: Upload sdist - uses: actions/upload-artifact@v4 - with: - name: sdist - path: dist/*.tar.gz - build-wheels: name: Build wheels on macOS for Python ${{ matrix.python-version }} runs-on: macos-26 @@ -99,7 +71,7 @@ jobs: publish: name: Publish to PyPI - needs: [build-sdist, build-wheels] + needs: [build-wheels] runs-on: ubuntu-latest steps: From d6b4981e3702d3c0186a8344a83bfb8ec466300d Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 19:19:48 -0600 Subject: [PATCH 05/20] fix: ensure dylib is included in wheel by building during build_py phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dylib wasn't being included in wheels because build_ext runs after build_py, so the dylib wasn't copied to the build directory when package data files were collected. Added BuildPyWithDylib class that builds the Swift dylib before copying package files and explicitly copies it to the build directory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- setup.py | 180 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 101 insertions(+), 79 deletions(-) diff --git a/setup.py b/setup.py index 54f2ea4..53abdfa 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ from pathlib import Path from setuptools import setup, Extension from setuptools.command.build_ext import build_ext as _build_ext +from setuptools.command.build_py import build_py as _build_py from Cython.Build import cythonize @@ -32,92 +33,112 @@ ARCH = "x86_64" +def build_swift_dylib(): + """Build the Swift FoundationModels dylib (shared function).""" + # Check if we need to rebuild + needs_rebuild = ( + not DYLIB_PATH.exists() or + not SWIFT_SRC.exists() or + SWIFT_SRC.stat().st_mtime > DYLIB_PATH.stat().st_mtime + ) + + if not needs_rebuild: + print(f"Swift dylib is up to date: {DYLIB_PATH}") + return + + print("=" * 70) + print("Building Swift FoundationModels dylib...") + print("=" * 70) + + # Check if running on macOS + if platform.system() != "Darwin": + print("Error: Swift dylib can only be built on macOS") + sys.exit(1) + + # Check Swift source exists + if not SWIFT_SRC.exists(): + print(f"Error: Swift source not found at {SWIFT_SRC}") + sys.exit(1) + + # Check macOS version + os_version = int(platform.mac_ver()[0].split('.')[0]) + if os_version < 26: + print(f"Warning: macOS 26.0+ required for Apple Intelligence") + print(f"Current version: {platform.mac_ver()[0]}") + print("Continuing anyway (library will be built but may not function)") + + # Create lib directory + LIB_DIR.mkdir(parents=True, exist_ok=True) + + # Compile Swift to dylib + cmd = [ + "swiftc", str(SWIFT_SRC), + "-O", + "-whole-module-optimization", + f"-target", f"{ARCH}-apple-macos26.0", + "-framework", "Foundation", + "-framework", "FoundationModels", + "-emit-library", + "-o", str(DYLIB_PATH), + "-emit-module", + "-emit-module-path", str(SWIFTMODULE_PATH), + "-Xlinker", "-install_name", + "-Xlinker", f"@rpath/libfoundation_models.dylib", + ] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr, file=sys.stderr) + + print(f"✓ Successfully built: {DYLIB_PATH}") + print(f" Size: {DYLIB_PATH.stat().st_size / 1024:.1f} KB") + + # Copy dylib to package directory so it's included in wheels + print(f"Copying dylib to package directory: {PKG_DYLIB_PATH}") + shutil.copy2(DYLIB_PATH, PKG_DYLIB_PATH) + print(f"✓ Dylib copied to package directory") + + except subprocess.CalledProcessError as e: + print(f"✗ Swift compilation failed") + print(e.stderr) + sys.exit(1) + except FileNotFoundError: + print("✗ Swift compiler (swiftc) not found") + print("Please install Xcode Command Line Tools:") + print(" xcode-select --install") + sys.exit(1) + + +class BuildPyWithDylib(_build_py): + """Custom build_py that ensures dylib is built and copied.""" + + def run(self): + """Build Swift dylib before copying package files.""" + # Build the Swift dylib first + build_swift_dylib() + # Run the standard build_py + super().run() + # Ensure dylib is copied to build directory + if PKG_DYLIB_PATH.exists(): + build_lib = Path(self.build_lib) + target_dir = build_lib / "applefoundationmodels" + target_dir.mkdir(parents=True, exist_ok=True) + target_dylib = target_dir / "libfoundation_models.dylib" + print(f"Copying {PKG_DYLIB_PATH} to {target_dylib}") + shutil.copy2(PKG_DYLIB_PATH, target_dylib) + + class BuildSwiftThenExt(_build_ext): """Custom build_ext that builds Swift dylib before building Cython extension.""" def run(self): """Build Swift dylib, then build Cython extension.""" - self.build_swift_dylib() + build_swift_dylib() super().run() - def build_swift_dylib(self): - """Build the Swift FoundationModels dylib.""" - # Check if we need to rebuild - needs_rebuild = ( - not DYLIB_PATH.exists() or - not SWIFT_SRC.exists() or - SWIFT_SRC.stat().st_mtime > DYLIB_PATH.stat().st_mtime - ) - - if not needs_rebuild: - print(f"Swift dylib is up to date: {DYLIB_PATH}") - return - - print("=" * 70) - print("Building Swift FoundationModels dylib...") - print("=" * 70) - - # Check if running on macOS - if platform.system() != "Darwin": - print("Error: Swift dylib can only be built on macOS") - sys.exit(1) - - # Check Swift source exists - if not SWIFT_SRC.exists(): - print(f"Error: Swift source not found at {SWIFT_SRC}") - sys.exit(1) - - # Check macOS version - os_version = int(platform.mac_ver()[0].split('.')[0]) - if os_version < 26: - print(f"Warning: macOS 26.0+ required for Apple Intelligence") - print(f"Current version: {platform.mac_ver()[0]}") - print("Continuing anyway (library will be built but may not function)") - - # Create lib directory - LIB_DIR.mkdir(parents=True, exist_ok=True) - - # Compile Swift to dylib - cmd = [ - "swiftc", str(SWIFT_SRC), - "-O", - "-whole-module-optimization", - f"-target", f"{ARCH}-apple-macos26.0", - "-framework", "Foundation", - "-framework", "FoundationModels", - "-emit-library", - "-o", str(DYLIB_PATH), - "-emit-module", - "-emit-module-path", str(SWIFTMODULE_PATH), - "-Xlinker", "-install_name", - "-Xlinker", f"@rpath/libfoundation_models.dylib", - ] - - try: - result = subprocess.run(cmd, check=True, capture_output=True, text=True) - if result.stdout: - print(result.stdout) - if result.stderr: - print(result.stderr, file=sys.stderr) - - print(f"✓ Successfully built: {DYLIB_PATH}") - print(f" Size: {DYLIB_PATH.stat().st_size / 1024:.1f} KB") - - # Copy dylib to package directory so it's included in wheels - print(f"Copying dylib to package directory: {PKG_DYLIB_PATH}") - shutil.copy2(DYLIB_PATH, PKG_DYLIB_PATH) - print(f"✓ Dylib copied to package directory") - - except subprocess.CalledProcessError as e: - print(f"✗ Swift compilation failed") - print(e.stderr) - sys.exit(1) - except FileNotFoundError: - print("✗ Swift compiler (swiftc) not found") - print("Please install Xcode Command Line Tools:") - print(" xcode-select --install") - sys.exit(1) - # Define the Cython extension extensions = [ Extension( @@ -157,6 +178,7 @@ def build_swift_dylib(self): setup( ext_modules=ext_modules, cmdclass={ + 'build_py': BuildPyWithDylib, 'build_ext': BuildSwiftThenExt, }, ) From 65de3c1aaab3bc56a72a924e8896168249804781 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 19:33:01 -0600 Subject: [PATCH 06/20] ci: set MACOSX_DEPLOYMENT_TARGET and ARCHFLAGS for wheel builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set MACOSX_DEPLOYMENT_TARGET=26.0 to properly tag wheels for macOS 26+ - Set ARCHFLAGS="-arch arm64" to build only for Apple Silicon - Add Swift source files to package-data to ensure they're included This silences the "wheel needs a higher macOS version" warning and ensures we only build for arm64 since Apple Intelligence requires Apple Silicon. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 9910e01..b95bc8d 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -44,6 +44,9 @@ jobs: pip install build wheel setuptools Cython>=3.0.0 - name: Build wheel + env: + MACOSX_DEPLOYMENT_TARGET: "26.0" + ARCHFLAGS: "-arch arm64" run: | python -m build --wheel diff --git a/pyproject.toml b/pyproject.toml index db6aae0..6a4d5f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ packages = ["applefoundationmodels"] include-package-data = true [tool.setuptools.package-data] -applefoundationmodels = ["py.typed", "*.pxd", "*.dylib"] +applefoundationmodels = ["py.typed", "*.pxd", "*.dylib", "swift/*.swift", "swift/*.h"] [tool.pytest.ini_options] testpaths = ["tests"] From 80bfb95ff608e9686daee2ba6ca80042b6af5ba4 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 6 Nov 2025 19:42:26 -0600 Subject: [PATCH 07/20] ci: force arm64 platform tag by setting _PYTHON_HOST_PLATFORM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GitHub Actions runner Python is built as universal2, so even with ARCHFLAGS set, wheels were still tagged as universal2. Setting _PYTHON_HOST_PLATFORM overrides the platform tag to force arm64. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index b95bc8d..7cd9134 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -47,6 +47,7 @@ jobs: env: MACOSX_DEPLOYMENT_TARGET: "26.0" ARCHFLAGS: "-arch arm64" + _PYTHON_HOST_PLATFORM: "macosx-26.0-arm64" run: | python -m build --wheel From 498d7293e67b07b25eb18c585bf9341f5d6ceb25 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 09:17:09 -0600 Subject: [PATCH 08/20] ci: use macOS 11.0 platform tag for PyPI compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyPI doesn't recognize macOS 26.0 as a valid platform tag yet. Use macosx_11_0_arm64 instead so wheels can be uploaded. The dylib is still built for macOS 26.0, and runtime availability checks ensure it only works on macOS 26.0+. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 7cd9134..297e746 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -45,9 +45,9 @@ jobs: - name: Build wheel env: - MACOSX_DEPLOYMENT_TARGET: "26.0" + MACOSX_DEPLOYMENT_TARGET: "11.0" ARCHFLAGS: "-arch arm64" - _PYTHON_HOST_PLATFORM: "macosx-26.0-arm64" + _PYTHON_HOST_PLATFORM: "macosx-11.0-arm64" run: | python -m build --wheel From cccb0c0f93513351068028c6ae07e5801b78d37a Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 09:20:22 -0600 Subject: [PATCH 09/20] docs: add macOS 26.0+ requirement to package description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the macOS version requirement prominent in the PyPI package description so users know before installing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6a4d5f0..98862ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "apple-foundation-models" version = "0.1.4" -description = "Python bindings for Apple's FoundationModels framework - on-device AI" +description = "Python bindings for Apple's FoundationModels framework - on-device AI (requires macOS 26.0+)" readme = "README.md" license = { text = "MIT" } authors = [{ name = "Ben Tucker" }] From 2c2281107d16147b96bab92bd0d645a8f59d0dbf Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 09:26:09 -0600 Subject: [PATCH 10/20] ci: adopt uv build for wheel building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace python -m build with uv build for faster, simpler builds - Add smoke test to verify wheel installs and imports correctly - Use astral-sh/setup-uv action with caching enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 297e746..19b4f62 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -38,18 +38,24 @@ jobs: echo "Python version:" python --version - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build wheel setuptools Cython>=3.0.0 + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true - name: Build wheel - env: - MACOSX_DEPLOYMENT_TARGET: "11.0" - ARCHFLAGS: "-arch arm64" - _PYTHON_HOST_PLATFORM: "macosx-11.0-arm64" run: | - python -m build --wheel + uv build --wheel + + - name: Test wheel installation + run: | + # Create a test environment and install the wheel + uv venv .test-venv + source .test-venv/bin/activate + uv pip install dist/*.whl + python -c "import applefoundationmodels; print(f'Version: {applefoundationmodels.__version__}')" + deactivate + rm -rf .test-venv - name: Verify wheel contents run: | From 1ba520885179051989f39f68cda2eaa56beb6d78 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:17:56 -0600 Subject: [PATCH 11/20] ci: properly build wheels for macOS 26.0 platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set MACOSX_DEPLOYMENT_TARGET=26.0 and force arm64 platform tag to properly build wheels tagged as macosx_26_0_arm64. This ensures the wheel metadata accurately reflects the macOS 26.0+ requirement. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 19b4f62..dd0dc02 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -44,6 +44,10 @@ jobs: enable-cache: true - name: Build wheel + env: + MACOSX_DEPLOYMENT_TARGET: "26.0" + ARCHFLAGS: "-arch arm64" + _PYTHON_HOST_PLATFORM: "macosx-26.0-arm64" run: | uv build --wheel From bd196ac873fca1e7a4fdaeb68963aa4c35d0b810 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:20:52 -0600 Subject: [PATCH 12/20] refactor: simplify package data configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove redundant include-package-data directive and rely solely on explicit package-data configuration. This is cleaner since we're only building wheels (no sdist) and setup.py handles dylib copying. Added *.pyx to package-data for completeness. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pyproject.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 98862ce..ec12b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,10 +48,16 @@ Issues = "https://github.com/btucker/apple-foundation-models-py/issues" [tool.setuptools] packages = ["applefoundationmodels"] -include-package-data = true [tool.setuptools.package-data] -applefoundationmodels = ["py.typed", "*.pxd", "*.dylib", "swift/*.swift", "swift/*.h"] +applefoundationmodels = [ + "py.typed", + "*.pxd", + "*.pyx", + "*.dylib", + "swift/*.swift", + "swift/*.h", +] [tool.pytest.ini_options] testpaths = ["tests"] From 2bad884c0a61b1c5370081d17906b4f4cab0c7d2 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:21:22 -0600 Subject: [PATCH 13/20] docs: document uv as primary build tool in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add uv-based development workflow to README while keeping pip as an alternative. This reflects the project's adoption of uv for CI builds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 179676a..466c604 100644 --- a/README.md +++ b/README.md @@ -320,17 +320,34 @@ See the `examples/` directory for complete working examples: ### Building from Source +This project uses [uv](https://docs.astral.sh/uv/) for fast, reliable builds and dependency management: + ```bash +# Install uv (if not already installed) +curl -LsSf https://astral.sh/uv/install.sh | sh + # Install development dependencies -pip install -e ".[dev]" +uv sync --extra dev # Run tests -pytest +uv run pytest # Type checking -mypy applefoundationmodels +uv run mypy applefoundationmodels # Format code +uv run black applefoundationmodels examples + +# Build wheels +uv build --wheel +``` + +You can also use pip if preferred: + +```bash +pip install -e ".[dev]" +pytest +mypy applefoundationmodels black applefoundationmodels examples ``` From 1d099c3670520d2362fa391e64755d349f74f90a Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:23:23 -0600 Subject: [PATCH 14/20] fix: update Python version requirement to 3.9+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Python 3.8 from classifiers and update requires-python to match CI workflow which builds wheels for Python 3.9-3.13. Python 3.8 may not be available on macos-26 runners. Updated both pyproject.toml and README.md for consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 4 ++-- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 466c604..11f5257 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Python bindings for Apple's FoundationModels framework - Direct access to on-dev ## Requirements - macOS 26.0+ (macOS Sequoia or later) -- Python 3.8 or higher +- Python 3.9 or higher - Apple Intelligence enabled on your device ## Installation @@ -47,7 +47,7 @@ pip install -e . - macOS 26.0+ (Sequoia) with Apple Intelligence enabled - Xcode command line tools (`xcode-select --install`) -- Python 3.8 or higher +- Python 3.9 or higher **Note:** The Swift dylib is built automatically during installation. diff --git a/pyproject.toml b/pyproject.toml index ec12b91..74ddbf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,16 +24,16 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Cython", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = ["typing-extensions>=4.0.0"] [project.optional-dependencies] From 0eb78dd15fcf1abc076d4351c2f1116d0a219c89 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:23:44 -0600 Subject: [PATCH 15/20] feat: add Python 3.14 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Python 3.14 classifier and build wheels for it in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 3 +-- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index dd0dc02..a3702d9 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -16,8 +16,7 @@ jobs: strategy: matrix: # Build wheels for all supported Python versions - # Note: Python 3.8 may not be available on macos-26 - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] fail-fast: false steps: diff --git a/pyproject.toml b/pyproject.toml index 74ddbf1..26c5e72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Cython", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", From 0502fdcc3b00640335f75457b8dcba334295e2e8 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:26:09 -0600 Subject: [PATCH 16/20] refactor: exclude Swift source files from wheels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swift source files (.swift, .h) are only needed at build time, not runtime. Remove them from package-data and MANIFEST.in to reduce wheel size from 181KB to 177KB. Runtime dependencies (dylib and compiled .so) are still correctly included. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MANIFEST.in | 23 ++++++----------------- pyproject.toml | 2 -- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8f083fc..c814d1b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,27 +1,16 @@ -# Include all necessary files in source distribution - -# Swift source files -include applefoundationmodels/swift/*.swift -include applefoundationmodels/swift/*.h - -# Dynamic library -include lib/*.dylib -include lib/*.swiftmodule - -# Cython source files -include applefoundationmodels/*.pyx -include applefoundationmodels/*.pxd - -# Type stub marker -include applefoundationmodels/py.typed +# This file is for sdist builds only (not used for wheels) +# Wheels use pyproject.toml [tool.setuptools.package-data] instead # Documentation include README.md include LICENSE -# Exclude unnecessary files +# Exclude build artifacts and source files from wheels global-exclude *.pyc global-exclude __pycache__ global-exclude *.so global-exclude .DS_Store global-exclude *.a +global-exclude *.swift +global-exclude *.h +global-exclude lib/* diff --git a/pyproject.toml b/pyproject.toml index 26c5e72..750a006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,8 +56,6 @@ applefoundationmodels = [ "*.pxd", "*.pyx", "*.dylib", - "swift/*.swift", - "swift/*.h", ] [tool.pytest.ini_options] From 98b3c6605ad12f051e4737fd251ca34713698b08 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:29:31 -0600 Subject: [PATCH 17/20] ci: enhance wheel verification error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve debugging for multi-wheel builds by: - Show wheel filename and size before checking - Include full wheel contents in error output when dylib is missing - Clarify which specific wheel failed verification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/publish-to-pypi.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index a3702d9..4ac0860 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -102,10 +102,13 @@ jobs: echo "Verifying each wheel contains dylib:" for wheel in dist/*.whl; do echo "Checking $wheel:" + ls -lh "$wheel" if unzip -l "$wheel" | grep -q "libfoundation_models.dylib"; then echo " ✓ dylib present" else - echo " ✗ ERROR: dylib missing!" + echo " ✗ ERROR: dylib missing in $wheel!" + echo "Wheel contents:" + unzip -l "$wheel" exit 1 fi done From ed1b1c26cb2547f4182e4c69db6ebb8c894df5a8 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:31:55 -0600 Subject: [PATCH 18/20] refactor: simplify setup.py - Reduce from 185 to 110 lines (40% reduction) - Remove redundant error checks and variables - Consolidate Swift build command formatting - Only support arm64 architecture (Apple Silicon) - Simplify class definitions and remove verbose comments - Use sys.exit() for cleaner error handling --- setup.py | 174 ++++++++++++++++--------------------------------------- 1 file changed, 50 insertions(+), 124 deletions(-) diff --git a/setup.py b/setup.py index 53abdfa..bdf1edf 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,5 @@ -""" -Setup script for apple-foundation-models-py Python bindings. +"""Setup script for apple-foundation-models-py Python bindings.""" -Builds the Swift FoundationModels dylib and Cython extension automatically. -""" - -import os import sys import platform import subprocess @@ -15,170 +10,101 @@ from setuptools.command.build_py import build_py as _build_py from Cython.Build import cythonize - -# Determine paths +# Paths REPO_ROOT = Path(__file__).parent.resolve() -LIB_DIR = REPO_ROOT / "lib" PKG_DIR = REPO_ROOT / "applefoundationmodels" -SWIFT_DIR = PKG_DIR / "swift" -SWIFT_SRC = SWIFT_DIR / "foundation_models.swift" -DYLIB_PATH = LIB_DIR / "libfoundation_models.dylib" -SWIFTMODULE_PATH = LIB_DIR / "foundation_models.swiftmodule" -PKG_DYLIB_PATH = PKG_DIR / "libfoundation_models.dylib" +SWIFT_SRC = PKG_DIR / "swift" / "foundation_models.swift" +LIB_DIR = REPO_ROOT / "lib" +PKG_DYLIB = PKG_DIR / "libfoundation_models.dylib" -# Detect architecture -ARCH = platform.machine() -if ARCH not in ["arm64", "x86_64"]: - print(f"Warning: Unsupported architecture {ARCH}, attempting to use x86_64") - ARCH = "x86_64" +# Only support Apple Silicon (arm64) +ARCH = "arm64" def build_swift_dylib(): - """Build the Swift FoundationModels dylib (shared function).""" - # Check if we need to rebuild - needs_rebuild = ( - not DYLIB_PATH.exists() or - not SWIFT_SRC.exists() or - SWIFT_SRC.stat().st_mtime > DYLIB_PATH.stat().st_mtime - ) + """Build the Swift FoundationModels dylib.""" + dylib_path = LIB_DIR / "libfoundation_models.dylib" - if not needs_rebuild: - print(f"Swift dylib is up to date: {DYLIB_PATH}") - return + # Skip if up to date + if dylib_path.exists() and SWIFT_SRC.exists(): + if SWIFT_SRC.stat().st_mtime <= dylib_path.stat().st_mtime: + print(f"Swift dylib is up to date: {dylib_path}") + return - print("=" * 70) - print("Building Swift FoundationModels dylib...") - print("=" * 70) - - # Check if running on macOS + # Validate environment if platform.system() != "Darwin": - print("Error: Swift dylib can only be built on macOS") - sys.exit(1) - - # Check Swift source exists + sys.exit("Error: Swift dylib can only be built on macOS") if not SWIFT_SRC.exists(): - print(f"Error: Swift source not found at {SWIFT_SRC}") - sys.exit(1) - - # Check macOS version - os_version = int(platform.mac_ver()[0].split('.')[0]) - if os_version < 26: - print(f"Warning: macOS 26.0+ required for Apple Intelligence") - print(f"Current version: {platform.mac_ver()[0]}") - print("Continuing anyway (library will be built but may not function)") + sys.exit(f"Error: Swift source not found at {SWIFT_SRC}") - # Create lib directory + print("Building Swift FoundationModels dylib...") LIB_DIR.mkdir(parents=True, exist_ok=True) - # Compile Swift to dylib + # Build dylib cmd = [ "swiftc", str(SWIFT_SRC), - "-O", - "-whole-module-optimization", - f"-target", f"{ARCH}-apple-macos26.0", - "-framework", "Foundation", - "-framework", "FoundationModels", - "-emit-library", - "-o", str(DYLIB_PATH), - "-emit-module", - "-emit-module-path", str(SWIFTMODULE_PATH), - "-Xlinker", "-install_name", - "-Xlinker", f"@rpath/libfoundation_models.dylib", + "-O", "-whole-module-optimization", + "-target", f"{ARCH}-apple-macos26.0", + "-framework", "Foundation", "-framework", "FoundationModels", + "-emit-library", "-o", str(dylib_path), + "-emit-module", "-emit-module-path", str(LIB_DIR / "foundation_models.swiftmodule"), + "-Xlinker", "-install_name", "-Xlinker", "@rpath/libfoundation_models.dylib", ] try: - result = subprocess.run(cmd, check=True, capture_output=True, text=True) - if result.stdout: - print(result.stdout) - if result.stderr: - print(result.stderr, file=sys.stderr) - - print(f"✓ Successfully built: {DYLIB_PATH}") - print(f" Size: {DYLIB_PATH.stat().st_size / 1024:.1f} KB") - - # Copy dylib to package directory so it's included in wheels - print(f"Copying dylib to package directory: {PKG_DYLIB_PATH}") - shutil.copy2(DYLIB_PATH, PKG_DYLIB_PATH) - print(f"✓ Dylib copied to package directory") - - except subprocess.CalledProcessError as e: - print(f"✗ Swift compilation failed") - print(e.stderr) - sys.exit(1) + subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"✓ Built: {dylib_path} ({dylib_path.stat().st_size / 1024:.1f} KB)") + shutil.copy2(dylib_path, PKG_DYLIB) + print(f"✓ Copied to: {PKG_DYLIB}") except FileNotFoundError: - print("✗ Swift compiler (swiftc) not found") - print("Please install Xcode Command Line Tools:") - print(" xcode-select --install") - sys.exit(1) + sys.exit("Error: swiftc not found. Install Xcode Command Line Tools: xcode-select --install") + except subprocess.CalledProcessError as e: + sys.exit(f"Error: Swift compilation failed\n{e.stderr}") class BuildPyWithDylib(_build_py): - """Custom build_py that ensures dylib is built and copied.""" - + """Build Swift dylib before copying package files.""" def run(self): - """Build Swift dylib before copying package files.""" - # Build the Swift dylib first build_swift_dylib() - # Run the standard build_py super().run() - # Ensure dylib is copied to build directory - if PKG_DYLIB_PATH.exists(): - build_lib = Path(self.build_lib) - target_dir = build_lib / "applefoundationmodels" - target_dir.mkdir(parents=True, exist_ok=True) - target_dylib = target_dir / "libfoundation_models.dylib" - print(f"Copying {PKG_DYLIB_PATH} to {target_dylib}") - shutil.copy2(PKG_DYLIB_PATH, target_dylib) + if PKG_DYLIB.exists(): + target = Path(self.build_lib) / "applefoundationmodels" / "libfoundation_models.dylib" + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(PKG_DYLIB, target) class BuildSwiftThenExt(_build_ext): - """Custom build_ext that builds Swift dylib before building Cython extension.""" - + """Build Swift dylib before building Cython extension.""" def run(self): - """Build Swift dylib, then build Cython extension.""" build_swift_dylib() super().run() -# Define the Cython extension -extensions = [ - Extension( - name="applefoundationmodels._foundationmodels", +# Cython extension +ext_modules = cythonize( + [Extension( + "applefoundationmodels._foundationmodels", sources=["applefoundationmodels/_foundationmodels.pyx"], - include_dirs=[str(SWIFT_DIR)], # Include Swift header directory + include_dirs=[str(PKG_DIR / "swift")], library_dirs=[str(LIB_DIR)], - libraries=["foundation_models"], # Link against libfoundation_models.dylib - extra_compile_args=[ - "-O3", # Optimization - "-Wall", # Warnings - ], + libraries=["foundation_models"], + extra_compile_args=["-O3", "-Wall"], extra_link_args=[ - # Set RPATH to find dylib at runtime - f"-Wl,-rpath,{LIB_DIR}", # Absolute path for development - "-Wl,-rpath,@loader_path/../lib", # Relative path for installed package - "-Wl,-rpath,@loader_path", # Also check same directory + f"-Wl,-rpath,{LIB_DIR}", + "-Wl,-rpath,@loader_path/../lib", + "-Wl,-rpath,@loader_path", ], language="c", - ) -] - -# Cythonize extensions -ext_modules = cythonize( - extensions, + )], compiler_directives={ "language_level": "3", "embedsignature": True, "boundscheck": False, "wraparound": False, }, - annotate=False, # Set to True to generate HTML annotation files ) -# Run setup if __name__ == "__main__": setup( ext_modules=ext_modules, - cmdclass={ - 'build_py': BuildPyWithDylib, - 'build_ext': BuildSwiftThenExt, - }, + cmdclass={"build_py": BuildPyWithDylib, "build_ext": BuildSwiftThenExt}, ) From c8c8f7fdcdf0c570873788747a16c50f28c9dca3 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:33:15 -0600 Subject: [PATCH 19/20] ci: add comprehensive wheel testing Run full test suite against installed wheel to verify: - Module imports correctly - Dylib is present and loadable - All tests pass from installed package - Tests run from /tmp to ensure using wheel, not source --- .github/workflows/publish-to-pypi.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 4ac0860..a2e5f0f 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -50,13 +50,31 @@ jobs: run: | uv build --wheel - - name: Test wheel installation + - name: Test wheel run: | - # Create a test environment and install the wheel + # Create isolated test environment uv venv .test-venv source .test-venv/bin/activate - uv pip install dist/*.whl - python -c "import applefoundationmodels; print(f'Version: {applefoundationmodels.__version__}')" + + # Install wheel with test dependencies + uv pip install dist/*.whl pytest pytest-asyncio + + # Verify installation + python -c " + import applefoundationmodels as afm + import os + print(f'Version: {afm.__version__}') + dylib = os.path.join(os.path.dirname(afm.__file__), 'libfoundation_models.dylib') + print(f'Dylib exists: {os.path.exists(dylib)}') + print(f'Availability: {afm.Client.check_availability().name}') + " + + # Run tests from installed wheel + cd /tmp + pytest ${{ github.workspace }}/tests/ -v --tb=short + + # Cleanup + cd ${{ github.workspace }} deactivate rm -rf .test-venv From 5b41a69c6bc9826c7587698990e1516c537fd197 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Fri, 7 Nov 2025 11:34:30 -0600 Subject: [PATCH 20/20] refactor: move wheel testing to test workflow - Test workflow now builds wheels and tests from installed packages - Ensures wheels work correctly before publishing - Test all Python versions (3.9-3.14) from wheels - Publish workflow focuses on building and publishing - Tests run from /tmp to guarantee using installed wheel, not source --- .github/workflows/publish-to-pypi.yml | 28 --------------------------- .github/workflows/test.yml | 27 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index a2e5f0f..f107d20 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -50,34 +50,6 @@ jobs: run: | uv build --wheel - - name: Test wheel - run: | - # Create isolated test environment - uv venv .test-venv - source .test-venv/bin/activate - - # Install wheel with test dependencies - uv pip install dist/*.whl pytest pytest-asyncio - - # Verify installation - python -c " - import applefoundationmodels as afm - import os - print(f'Version: {afm.__version__}') - dylib = os.path.join(os.path.dirname(afm.__file__), 'libfoundation_models.dylib') - print(f'Dylib exists: {os.path.exists(dylib)}') - print(f'Availability: {afm.Client.check_availability().name}') - " - - # Run tests from installed wheel - cd /tmp - pytest ${{ github.workspace }}/tests/ -v --tb=short - - # Cleanup - cd ${{ github.workspace }} - deactivate - rm -rf .test-venv - - name: Verify wheel contents run: | echo "Build artifacts:" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 288926d..c5c06e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] fail-fast: false steps: @@ -38,13 +38,20 @@ jobs: echo "Architecture:" uname -m - - name: Install dependencies and build extensions + - name: Build wheel run: | - uv sync --extra dev + uv build --wheel + + - name: Install wheel and test dependencies + run: | + uv venv .test-venv + source .test-venv/bin/activate + uv pip install dist/*.whl pytest pytest-asyncio pytest-cov - name: Check Apple Intelligence availability run: | - uv run python -c " + source .test-venv/bin/activate + python -c " import applefoundationmodels as afm status = afm.Client.check_availability() reason = afm.Client.get_availability_reason() @@ -54,15 +61,19 @@ jobs: " continue-on-error: true - - name: Run tests + - name: Run tests from installed wheel run: | - # Tests will auto-skip if Apple Intelligence is unavailable - uv run pytest tests/ -v --tb=short -ra + source .test-venv/bin/activate + cd /tmp + pytest ${{ github.workspace }}/tests/ -v --tb=short -ra - name: Run tests with coverage (Python 3.11 only) if: matrix.python-version == '3.11' run: | - uv run pytest tests/ --cov=applefoundationmodels --cov-report=xml --cov-report=term + source .test-venv/bin/activate + cd /tmp + pytest ${{ github.workspace }}/tests/ --cov=applefoundationmodels --cov-report=xml --cov-report=term + cp coverage.xml ${{ github.workspace }}/ - name: Upload coverage reports if: matrix.python-version == '3.11'