Skip to content
Merged
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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,38 @@ jobs:
- name: Build
run: cmake --build build --config ${{ matrix.build_type }}

# The macOS plugin loads inside TouchDesigner on users' machines, so nothing
# about this runner may leak into it: no link against a Python that only
# exists here (v0.4.0 shipped linked to Homebrew's 3.14 and failed to load
# on every Mac), a minimum OS TouchDesigner supports, both architectures,
# and a bundle signature codesign accepts.
- name: Verify macOS plugins
if: runner.os == 'macOS'
run: |
status=0
for plugin in build/bin/AnimationCHOP.plugin build/bin/AnimationViewCHOP.plugin; do
bin="$plugin/Contents/MacOS/$(basename "${plugin%.plugin}")"
echo "== $plugin"
otool -L "$bin"
if otool -L "$bin" | grep -qi python; then
echo "::error::$plugin links a Python library; it must resolve Python from the host process." >&2; status=1
fi
archs=$(lipo -archs "$bin")
echo "archs: $archs"
case "$archs" in *arm64*x86_64*|*x86_64*arm64*) ;; *)
echo "::error::$plugin is not a universal binary (got: $archs)." >&2; status=1 ;;
esac
minos=$(otool -l "$bin" | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print $2; exit}')
echo "minos: $minos"
if [ "$minos" != "13.3" ]; then
echo "::error::$plugin minimum macOS is $minos, expected 13.3." >&2; status=1
fi
if ! codesign --verify --deep --strict -vv "$plugin"; then
echo "::error::$plugin has no valid bundle signature." >&2; status=1
fi
done
exit $status

- name: Test
run: ctest --test-dir build --build-config ${{ matrix.build_type }} --output-on-failure

Expand Down
42 changes: 41 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ jobs:
- name: Build
run: cmake --build build --config Release

# The macOS plugin loads inside TouchDesigner on users' machines, so nothing
# about this runner may leak into it: no link against a Python that only
# exists here (v0.4.0 shipped linked to Homebrew's 3.14 and failed to load
# on every Mac), a minimum OS TouchDesigner supports, both architectures,
# and a bundle signature codesign accepts.
- name: Verify macOS plugins
if: runner.os == 'macOS'
run: |
status=0
for plugin in build/bin/AnimationCHOP.plugin build/bin/AnimationViewCHOP.plugin; do
bin="$plugin/Contents/MacOS/$(basename "${plugin%.plugin}")"
echo "== $plugin"
otool -L "$bin"
if otool -L "$bin" | grep -qi python; then
echo "::error::$plugin links a Python library; it must resolve Python from the host process." >&2; status=1
fi
archs=$(lipo -archs "$bin")
echo "archs: $archs"
case "$archs" in *arm64*x86_64*|*x86_64*arm64*) ;; *)
echo "::error::$plugin is not a universal binary (got: $archs)." >&2; status=1 ;;
esac
minos=$(otool -l "$bin" | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print $2; exit}')
echo "minos: $minos"
if [ "$minos" != "13.3" ]; then
echo "::error::$plugin minimum macOS is $minos, expected 13.3." >&2; status=1
fi
if ! codesign --verify --deep --strict -vv "$plugin"; then
echo "::error::$plugin has no valid bundle signature." >&2; status=1
fi
done
exit $status

# Don't publish a release that fails its own tests.
- name: Test
run: ctest --test-dir build --build-config Release --output-on-failure
Expand All @@ -66,7 +98,15 @@ jobs:
- name: Package
shell: bash
run: |
version="${GITHUB_REF_NAME:-dev}"
# The archive is named after the tag. A workflow_dispatch dry run from
# a branch has no tag, and a branch name can contain a slash, which
# would turn the stage name into a nested directory the upload glob
# never sees -- so those are named after the commit instead.
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
version="${GITHUB_REF_NAME}"
else
version="dev-${GITHUB_SHA::7}"
fi
stage="AnimationCHOP-${version}-${{ matrix.platform }}"
mkdir -p "$stage/Plugins" "$stage/modules"

Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
While the major version is `0`, breaking changes may land in a minor release.

## [0.4.1] - 2026-09-08

### Fixed

- The macOS release did not load: TouchDesigner reported the plugins as
corrupted. They were built on a runner without TouchDesigner, so CMake fell
back to the runner's Homebrew Python 3.14 and linked against a path that
exists on no user's Mac; they also required macOS 26 and Apple silicon, and
carried only the linker's partial ad-hoc signature, which `codesign` rejects
for the bundle. The plugins now link no Python library at all (the symbols
resolve from TouchDesigner's own interpreter at load time, as for any
CPython extension), build universal with a minimum of macOS 13.3, and are
ad-hoc signed as complete bundles. The release workflow verifies all four
properties before it will publish.
- The plugins are not notarized, so macOS still refuses a copy that carries the
browser's quarantine flag, with the same "corrupted" report. The README's
installation steps now include clearing the flag.
- `run_td_tests.sh` is executable.

## [0.4.0] - 2026-07-26

First release prepared for the public repository.
Expand Down
101 changes: 62 additions & 39 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
cmake_minimum_required(VERSION 3.14)

# macOS: TouchDesigner 2025 runs on macOS 13 and later, on Apple silicon and
# Intel. Without these the plugin inherits the build machine's OS release as its
# minimum and its CPU as its only architecture -- the CI runner is macOS 26 on
# arm64, so v0.4.0 would only ever have loaded on a Mac newer than most users'.
# 13.3 rather than 13.0 because the bindings format floats with std::format,
# which libc++ only provides from 13.3. They must be set before project(), and
# are ignored on other platforms.
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.3" CACHE STRING "Minimum macOS version the plugins load on")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures to build the plugins for")

# Stays on 0.x until the API is stable; see CHANGELOG.md.
project(AnimationCHOP VERSION 0.4.0 LANGUAGES CXX)
project(AnimationCHOP VERSION 0.4.1 LANGUAGES CXX)

set(VERBOSE_STATUS ON CACHE BOOL "Verbose status messages" FORCE)
set(SUPPRESS_NOT_REFERENCED_WARNINGS ON CACHE BOOL "Suppress not referenced warnings" FORCE)
Expand Down Expand Up @@ -90,45 +101,52 @@ if(WIN32)
)
endforeach()
elseif(APPLE)
# On macOS, use TouchDesigner's Python specifically
set(TD_PYTHON_EXECUTABLE "/Applications/TouchDesigner.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python3.11")
set(TD_PYTHON_INCLUDE_DIR "/Applications/TouchDesigner.app/Contents/Frameworks/Python.framework/Versions/Current/include/python3.11")
set(TD_PYTHON_LIBRARY_DIR "/Applications/TouchDesigner.app/Contents/Frameworks/Python.framework/Versions/Current/lib")

# Check if TouchDesigner's Python exists, otherwise fall back to system Python
if(EXISTS "${TD_PYTHON_EXECUTABLE}" AND EXISTS "${TD_PYTHON_INCLUDE_DIR}")
message(STATUS "Using TouchDesigner's Python: ${TD_PYTHON_EXECUTABLE}")

# Force CMake to use TouchDesigner's Python by setting the paths explicitly
set(Python3_EXECUTABLE "${TD_PYTHON_EXECUTABLE}" CACHE FILEPATH "Path to Python3 executable" FORCE)
set(Python3_INCLUDE_DIR "${TD_PYTHON_INCLUDE_DIR}" CACHE PATH "Path to Python3 include directory" FORCE)
set(Python3_LIBRARY_DIRS "${TD_PYTHON_LIBRARY_DIR}" CACHE PATH "Path to Python3 library directory" FORCE)

# Set the find strategy to prioritize our specified paths
set(Python3_FIND_STRATEGY LOCATION)
set(Python3_FIND_REGISTRY NEVER)
set(Python3_FIND_FRAMEWORK NEVER)

find_package(Python3 COMPONENTS Interpreter Development REQUIRED)

foreach(TARGET animation_chop animation_view_chop)
target_link_libraries(${TARGET} PRIVATE
Python3::Python
anim
)
target_compile_definitions(${TARGET} PRIVATE __cdecl=)
endforeach()
# TouchDesigner embeds CPython 3.11 and loads the plugin into its own
# process, so the plugin must not link a libpython of its own: whatever it
# linked would have to exist, at that exact path, on every user's Mac.
# (v0.4.0 was built on a runner without TouchDesigner, linked the runner's
# Homebrew Python 3.14, and failed to load everywhere.) Instead, as every
# CPython extension module does, leave the Python symbols undefined and let
# dyld resolve them from the host process at load time. All the build needs
# from Python is the 3.11 headers.
#
# TouchDesigner's own copy is preferred, because it is exactly the ABI the
# plugin runs against. Without a TouchDesigner install (CI), any CPython
# 3.11 supplies identical headers. EXACT matters: FindPython otherwise
# returns the newest interpreter it can find, and headers from another
# minor version would silently build against the wrong ABI.
set(ANIMATIONCHOP_TD_APP "/Applications/TouchDesigner.app"
CACHE PATH "TouchDesigner install to take the Python 3.11 headers from")
set(TD_PYTHON_INCLUDE_DIR
"${ANIMATIONCHOP_TD_APP}/Contents/Frameworks/Python.framework/Versions/3.11/include/python3.11")

if(EXISTS "${TD_PYTHON_INCLUDE_DIR}/Python.h")
message(STATUS "Using TouchDesigner's Python 3.11 headers: ${TD_PYTHON_INCLUDE_DIR}")
set(ANIMATIONCHOP_MACOS_PYTHON_INCLUDE_DIRS "${TD_PYTHON_INCLUDE_DIR}")
else()
message(WARNING "TouchDesigner's Python not found at ${TD_PYTHON_EXECUTABLE}, falling back to system Python")
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
foreach(TARGET animation_chop animation_view_chop)
target_link_libraries(${TARGET} PRIVATE
Python3::Python
anim
)
target_compile_definitions(${TARGET} PRIVATE __cdecl=)
endforeach()
set(Python3_FIND_FRAMEWORK LAST)
find_package(Python3 3.11 EXACT COMPONENTS Interpreter Development.Module REQUIRED)
message(STATUS "TouchDesigner not found at ${ANIMATIONCHOP_TD_APP}; "
"using Python ${Python3_VERSION} headers: ${Python3_INCLUDE_DIRS}")
set(ANIMATIONCHOP_MACOS_PYTHON_INCLUDE_DIRS ${Python3_INCLUDE_DIRS})
endif()

foreach(TARGET animation_chop animation_view_chop)
target_include_directories(${TARGET} PRIVATE ${ANIMATIONCHOP_MACOS_PYTHON_INCLUDE_DIRS})
target_link_libraries(${TARGET} PRIVATE anim)
target_link_options(${TARGET} PRIVATE "LINKER:-undefined,dynamic_lookup")
target_compile_definitions(${TARGET} PRIVATE __cdecl=)

# The linker's implicit ad-hoc signature covers only the Mach-O, not the
# bundle, so `codesign --verify` rejects the .plugin and Gatekeeper
# reports a downloaded copy as damaged. Sign the whole bundle (still
# ad-hoc: no certificate needed). Runs before the copies below, which
# are added later in this file, so every copy carries the signature.
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND codesign --force --sign - "$<TARGET_BUNDLE_DIR:${TARGET}>"
COMMENT "Ad-hoc signing $<TARGET_PROPERTY:${TARGET},OUTPUT_NAME>.plugin"
)
endforeach()
else()
# On other platforms, use pkg-config to find Python
find_package(PkgConfig REQUIRED)
Expand Down Expand Up @@ -283,9 +301,14 @@ if(ANIMATIONCHOP_BUILD_TESTS)
# Windows builds against the vendored headers/import library, matching
# the operator targets above.
set(ANIMATIONCHOP_PYTHON_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/ext/Python/Include)
elseif(APPLE)
# Same headers and undefined-symbol linking as the operators above; the
# importing interpreter supplies the symbols.
set(ANIMATIONCHOP_PYEXT_SUFFIX ".so")
set(ANIMATIONCHOP_PYTHON_INCLUDE_DIRS ${ANIMATIONCHOP_MACOS_PYTHON_INCLUDE_DIRS})
else()
set(ANIMATIONCHOP_PYEXT_SUFFIX ".so")
find_package(Python3 3.11 COMPONENTS Development.Module REQUIRED)
find_package(Python3 3.11 EXACT COMPONENTS Development.Module REQUIRED)
set(ANIMATIONCHOP_PYTHON_INCLUDE_DIRS ${Python3_INCLUDE_DIRS})
set(ANIMATIONCHOP_PYTHON_LIBRARIES Python3::Module)
endif()
Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ it.
operators. That build is where the Custom Operator API gained node data
persistence (`saveData`/`loadData`), which is how animations are stored in the
`.toe`; the operators target CHOP API version 10 / common API version 2.
- **Windows or macOS.** Windows builds are x64.
- **Windows or macOS.** Windows builds are x64. macOS builds are universal and
need macOS 13.3 or newer.

## Installation

Expand All @@ -62,9 +63,17 @@ it.

TouchDesigner only loads Custom Operators from a `Plugins` folder next to the
project file, or from the system-wide plugin folder.
3. Open the project. The first load of a new operator build shows a prompt
3. **macOS only:** clear the quarantine flag the browser put on the download,
or macOS refuses to load the plugins and TouchDesigner reports them as
corrupted. The plugins are not notarized with Apple, so this is a one-time
step per download. In Terminal, from the folder holding `Plugins`:

```bash
xattr -dr com.apple.quarantine Plugins
```
4. Open the project. The first load of a new operator build shows a prompt
asking you to trust it — approve it once.
4. Add an **AnimationCHOP** from the operator palette, or drop in the included
5. Add an **AnimationCHOP** from the operator palette, or drop in the included
`Keyframer.tox` for the full editor.

The release archive also contains an example project you can open directly.
Expand Down Expand Up @@ -137,8 +146,12 @@ Either way the built operators are copied into `td/Plugins/` so the example
project picks them up.

Windows vendors the CPython 3.11 headers and import libraries it needs, so there
is nothing to install. macOS builds against TouchDesigner's own Python framework
and expects TouchDesigner in `/Applications`.
is nothing to install. macOS compiles against the Python 3.11 headers inside
TouchDesigner when it is in `/Applications` (pass
`-DANIMATIONCHOP_TD_APP=<path>` for another location), and otherwise against any
CPython 3.11 it can find; the plugins never link a Python library, taking the
symbols from TouchDesigner's own interpreter at load time. Builds are universal
(Apple silicon and Intel) with a minimum of macOS 13.3.

## Testing

Expand Down
Empty file modified run_td_tests.sh
100644 → 100755
Empty file.
3 changes: 3 additions & 0 deletions tests/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ else()
# Extension modules leave Python's symbols undefined; the host interpreter
# supplies them at import.
target_link_libraries(animationchop_testext PRIVATE ${ANIMATIONCHOP_PYTHON_LIBRARIES})
if(APPLE)
target_link_options(animationchop_testext PRIVATE "LINKER:-undefined,dynamic_lookup")
endif()
target_compile_definitions(animationchop_testext PRIVATE __cdecl=)
target_compile_options(animationchop_testext PRIVATE
-Wno-unused-parameter -Wno-unused-variable)
Expand Down
Loading