Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b396e8e
fix: include libfoundation_models.dylib in wheels and build for multi…
btucker Nov 7, 2025
e74a24a
chore: uv.lock update
btucker Nov 7, 2025
67801b6
Merge branch 'main' of github.com:btucker/apple-foundation-models-py …
btucker Nov 7, 2025
67ad97d
chore: bump version
btucker Nov 7, 2025
055e1be
ci: remove build-sdist
btucker Nov 7, 2025
d6b4981
fix: ensure dylib is included in wheel by building during build_py phase
btucker Nov 7, 2025
65de3c1
ci: set MACOSX_DEPLOYMENT_TARGET and ARCHFLAGS for wheel builds
btucker Nov 7, 2025
80bfb95
ci: force arm64 platform tag by setting _PYTHON_HOST_PLATFORM
btucker Nov 7, 2025
498d729
ci: use macOS 11.0 platform tag for PyPI compatibility
btucker Nov 7, 2025
cccb0c0
docs: add macOS 26.0+ requirement to package description
btucker Nov 7, 2025
2c22811
ci: adopt uv build for wheel building
btucker Nov 7, 2025
1ba5208
ci: properly build wheels for macOS 26.0 platform
btucker Nov 7, 2025
bd196ac
refactor: simplify package data configuration
btucker Nov 7, 2025
2bad884
docs: document uv as primary build tool in README
btucker Nov 7, 2025
1d099c3
fix: update Python version requirement to 3.9+
btucker Nov 7, 2025
0eb78dd
feat: add Python 3.14 support
btucker Nov 7, 2025
0502fdc
refactor: exclude Swift source files from wheels
btucker Nov 7, 2025
98b3c66
ci: enhance wheel verification error messages
btucker Nov 7, 2025
ed1b1c2
refactor: simplify setup.py
btucker Nov 7, 2025
c8c8f7f
ci: add comprehensive wheel testing
btucker Nov 7, 2025
5b41a69
refactor: move wheel testing to test workflow
btucker Nov 7, 2025
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
60 changes: 45 additions & 15 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,65 +10,95 @@ permissions:
id-token: write # Required for trusted publishing to PyPI

jobs:
build:
name: Build package on macOS
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
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.11"
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
env:
MACOSX_DEPLOYMENT_TARGET: "26.0"
ARCHFLAGS: "-arch arm64"
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-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
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -51,7 +51,7 @@ packages = ["applefoundationmodels"]
include-package-data = true

[tool.setuptools.package-data]
applefoundationmodels = ["py.typed", "*.pxd"]
applefoundationmodels = ["py.typed", "*.pxd", "*.dylib", "swift/*.swift", "swift/*.h"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
180 changes: 105 additions & 75 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@
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
from setuptools.command.build_py import build_py as _build_py
from Cython.Build import cythonize


# 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()
Expand All @@ -29,87 +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")

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(
Expand Down Expand Up @@ -149,6 +178,7 @@ def build_swift_dylib(self):
setup(
ext_modules=ext_modules,
cmdclass={
'build_py': BuildPyWithDylib,
'build_ext': BuildSwiftThenExt,
},
)
Loading