diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4adcf02..822df4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,37 @@ jobs: - name: Build run: cmake --build build --config Release --parallel 2 + # 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, a minimum OS TouchDesigner supports, both architectures, and + # a bundle signature codesign accepts. v0.4.0 shipped arm64-only with a + # minimum of macOS 26 (the runner's) and a linker-only signature. + - name: Verify macOS plugin + if: runner.os == 'macOS' + run: | + status=0 + plugin=build/bin/AsyncioDAT.plugin + bin="$plugin/Contents/MacOS/AsyncioDAT" + 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 + exit $status + # tests/cpp (Catch2) and tests/python (pytest) only — the tests/td # integration suite needs a licensed TouchDesigner and a GPU, so it runs # locally via run_td_tests.ps1 / run_td_tests.sh instead. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbdd879..2063583 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,37 @@ jobs: - name: Build run: cmake --build build --config Release --parallel 2 + # 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, a minimum OS TouchDesigner supports, both architectures, and + # a bundle signature codesign accepts. v0.4.0 shipped arm64-only with a + # minimum of macOS 26 (the runner's) and a linker-only signature. + - name: Verify macOS plugin + if: runner.os == 'macOS' + run: | + status=0 + plugin=build/bin/AsyncioDAT.plugin + bin="$plugin/Contents/MacOS/AsyncioDAT" + 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 + exit $status + - name: Install test dependencies run: python -m pip install --upgrade pip -r tests/python/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0c64d..d089ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,33 @@ While the major version is `0`, breaking changes may land in a minor release. ## [Unreleased] +## [0.4.1] - 2026-09-08 + +Fixes the macOS release build, which TouchDesigner reported as corrupted. + +### Fixed + +- The v0.4.0 `AsyncioDAT.plugin` failed to load on every Mac. The release + runner is macOS 26 on Apple silicon, and nothing in the build pinned a + deployment target or an architecture, so the bundle inherited the runner's + macOS 26 as its minimum OS and arm64 as its only architecture. TouchDesigner + itself runs on macOS 13 and later, universal. The plugin is now built for + `arm64` and `x86_64` with a minimum of macOS 13.3 (the earliest libc++ that + provides the `std::format` the status messages use). +- The bundle carried only the linker's implicit ad-hoc signature, which covers + the Mach-O but not the bundle, so `codesign --verify` rejected it. The whole + bundle is now ad-hoc signed after linking. It is still not notarized, so a + downloaded copy still needs `xattr -dr com.apple.quarantine` (see the README + install steps). +- CI and the release gate now verify the macOS plugin links no Python library, + is universal, has the expected minimum OS, and passes `codesign --verify`, so + this class of binary cannot be published again. + +### Changed + +- README: the macOS build is universal, not arm64 only, and requires macOS 13.3 + or newer. + ## [0.4.0] - 2026-07-26 The release that opens the repository to the public. One crash fix, and the @@ -95,7 +122,8 @@ registry, the lifecycle callbacks, and the CI and release workflows. -[Unreleased]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.2.2...v0.3.0 [0.2.2]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.2.1...v0.2.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c15a94..ead1e12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,16 @@ cmake_minimum_required(VERSION 3.25) -project(AsyncioDAT VERSION 0.4.0 LANGUAGES CXX) + +# 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 asyncio_dat.cpp formats its status messages +# 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 plugin loads on") +set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures to build the plugin for") + +project(AsyncioDAT VERSION 0.4.1 LANGUAGES CXX) set(VERBOSE_STATUS OFF CACHE BOOL "Verbose status messages" FORCE) set(SUPPRESS_NOT_REFERENCED_WARNINGS ON CACHE BOOL "Suppress not referenced warnings" FORCE) @@ -127,6 +138,18 @@ if(SUPPRESS_NOT_REFERENCED_WARNINGS) endif() endif() +if(APPLE) + # 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). This runs before the copy to tests/td/Plugins below, + # so the copy carries the signature too. + add_custom_command(TARGET asyncio_dat POST_BUILD + COMMAND codesign --force --sign - "$" + COMMENT "Ad-hoc signing AsyncioDAT.plugin" + ) +endif() + # Add post-build command to copy the library to the Plugins folder add_custom_command(TARGET asyncio_dat POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_SOURCE_DIR}/tests/td/Plugins" diff --git a/README.md b/README.md index 8c09050..5d548b5 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ AsyncioDAT is a C++ TouchDesigner operator that provides a managed asyncio event - **TouchDesigner 2025.32820** or newer. The operator is built against Custom Operator SDK v4 and TouchDesigner's embedded CPython **3.11**. -- **Windows 10/11**, or **macOS on Apple Silicon**. The macOS build is arm64 - only, matching TouchDesigner. +- **Windows 10/11**, or **macOS 13.3 or newer**. The macOS build is universal + (Apple silicon and Intel), like TouchDesigner itself. ## Installation @@ -415,6 +415,14 @@ Python is **not** vendored in this repository — a uv-managed CPython ships the headers and import library CMake needs. The TouchDesigner Custom Operator SDK headers are included under `ext/td/include/` (see [NOTICE](NOTICE)). +On macOS only the headers are used: the plugin links no Python library and +resolves Python symbols from TouchDesigner's own interpreter when it loads, as +any CPython extension module does. The build produces a universal (`arm64` and +`x86_64`) bundle with a minimum of macOS 13.3, ad-hoc signed as a whole so +`codesign --verify` accepts it; `CMAKE_OSX_DEPLOYMENT_TARGET` and +`CMAKE_OSX_ARCHITECTURES` can be overridden on the CMake command line. Because +they are cache variables, change them in a fresh build directory. + ### Build Steps 1. Clone the repository and install Python 3.11: `uv python install 3.11` @@ -436,7 +444,9 @@ headers are included under `ext/td/include/` (see [NOTICE](NOTICE)). The project includes GitHub Actions workflows for: - **CI**: builds and runs the unit suites on Windows and macOS for every push and - pull request to `main` + pull request to `main`. On macOS it also checks that the plugin links no + Python library, is universal, targets macOS 13.3, and has a valid bundle + signature - **Release**: triggered by pushing a `v*.*.*` tag. It builds, tests, and packages on both platforms before publishing anything, and takes the release notes from the matching [CHANGELOG.md](CHANGELOG.md) entry. See