From f48fcd79935e09d12e7e41f14672a9c966212a5e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 18:48:52 +0000 Subject: [PATCH 1/2] Fix macOS VLC integration on Apple Silicon and Intel VLC for macOS only loads lib*_plugin.dylib, but the plugin was built with CMake's default .so module suffix, so every macOS release was silently ignored by VLC. Build it as .dylib. - Pin CMAKE_OSX_DEPLOYMENT_TARGET to 14.0 (the osx-arm64 ONNX Runtime minimum) so packages built on the newest runners load on older Macs. - Use ONNX Runtime 1.23.2 for Intel/universal2 macOS; 1.26.0 has no x86_64 macOS archive, so the Intel package shipped without a runtime. Never pick up a Homebrew onnxruntime for macOS packages. - Ad-hoc sign packaged dylibs that do not verify (optional Developer ID). - Ship install_icop_plugin.sh inside Linux/macOS archives; add --release-dir and --arch. On macOS, find VLC.app from the DMG, Homebrew cask (incl. --appdir), ~/Applications, MacPorts or Spotlight; match the payload to VLC's architecture (Rosetta aware); reject VLC 4; clear download quarantine; remove the stale .so; tolerate a missing vlc-cache-gen. - Add verify_macos_package.sh and macos_vlc_smoke_test.sh, run them in release CI with Homebrew VLC, and check plugin registration in PR CI. - Document macOS installation paths and troubleshooting. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VDfqb9f52nSf89az5rPZBz --- .agents/skills/vlc-runtime-check/SKILL.md | 12 ++ .github/workflows/ci.yml | 16 ++ .github/workflows/release.yml | 38 ++++ CHANGELOG.md | 20 +- CMakeLists.txt | 12 ++ INSTALL.md | 71 +++++- README.md | 25 ++- cmake/macos_codesign.cmake | 49 +++++ cmake/onnxruntime.cmake | 34 ++- cmake/options.cmake | 2 + cmake/packaging.cmake | 18 ++ docs/index.md | 8 +- docs/releasing.md | 11 + tools/detect_vlc_plugin_dir.sh | 3 +- tools/install_icop_plugin.sh | 251 ++++++++++++++++++---- tools/macos_vlc_smoke_test.sh | 62 ++++++ tools/verify_macos_package.sh | 84 ++++++++ 17 files changed, 657 insertions(+), 59 deletions(-) create mode 100644 cmake/macos_codesign.cmake create mode 100755 tools/macos_vlc_smoke_test.sh create mode 100755 tools/verify_macos_package.sh diff --git a/.agents/skills/vlc-runtime-check/SKILL.md b/.agents/skills/vlc-runtime-check/SKILL.md index 2cf65ff..ade8ab3 100644 --- a/.agents/skills/vlc-runtime-check/SKILL.md +++ b/.agents/skills/vlc-runtime-check/SKILL.md @@ -31,6 +31,18 @@ Copy-Item .\stage\plugins\video_filter\* "C:\Program Files\VideoLAN\VLC\plugins\ & "C:\Program Files\VideoLAN\VLC\vlc.exe" -vvv --file-logging --logfile=vlc-installed-test.log --video-filter=icop .\sample.mp4 ``` +## macOS Flow + +```sh +sh tools/verify_macos_package.sh releases/v/mac arm64 +sh tools/macos_vlc_smoke_test.sh /Applications/VLC.app sample.mp4 releases/v/mac/plugins +sh releases/v/mac/install_icop_plugin.sh --dry-run +``` + +- The plugin must be `libicop_plugin.dylib`; VLC for macOS ignores `.so`. +- The payload architecture must match VLC.app (`lipo -archs`), not the shell. +- Downloaded payloads carry `com.apple.quarantine`, which blocks `dlopen`. + ## Failure Checklist - Missing side-by-side DLLs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a678e94..282a116 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,6 +141,14 @@ jobs: run: cmake --build build-ci --target icop_plugin icop_core icop_test -j 2 - name: Test run: ctest --test-dir build-ci --output-on-failure + - name: Check VLC registers the plugin + run: | + brew install --cask vlc + xattr -dr com.apple.quarantine /Applications/VLC.app || true + mkdir -p vlc-plugin-check/video_filter + cp build-ci/libicop_plugin.dylib build-ci/libicop_core.dylib vlc-plugin-check/video_filter/ + VLC_PLUGIN_PATH="$PWD/vlc-plugin-check" /Applications/VLC.app/Contents/MacOS/VLC -I dummy --list > vlc-modules.log 2>&1 || true + grep -E '^[[:space:]]+icop[[:space:]]' vlc-modules.log macos-arm64: name: macOS ARM64 @@ -170,3 +178,11 @@ jobs: run: cmake --build build-ci --target icop_plugin icop_core icop_test -j 2 - name: Test run: ctest --test-dir build-ci --output-on-failure + - name: Check VLC registers the plugin + run: | + brew install --cask vlc + xattr -dr com.apple.quarantine /Applications/VLC.app || true + mkdir -p vlc-plugin-check/video_filter + cp build-ci/libicop_plugin.dylib build-ci/libicop_core.dylib vlc-plugin-check/video_filter/ + VLC_PLUGIN_PATH="$PWD/vlc-plugin-check" /Applications/VLC.app/Contents/MacOS/VLC -I dummy --list > vlc-modules.log 2>&1 || true + grep -E '^[[:space:]]+icop[[:space:]]' vlc-modules.log diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec9c8d0..bc364fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -305,6 +305,25 @@ jobs: -DNSFW_INSTALL_VLC_PLUGIN=OFF - name: Package run: cmake --build build-ci --target icop_package -j $(sysctl -n hw.logicalcpu) + - name: Verify macOS package + run: sh tools/verify_macos_package.sh "releases/v${ICOP_VERSION}/mac" x86_64 14.0 + - name: Install VLC from Homebrew + run: | + brew install --cask vlc + xattr -dr com.apple.quarantine /Applications/VLC.app || true + - name: Check bundled installer against Homebrew VLC + run: sh "releases/v${ICOP_VERSION}/mac/install_icop_plugin.sh" --dry-run + - name: Smoke test icop in VLC + run: >- + sh tools/macos_vlc_smoke_test.sh /Applications/VLC.app sample.mp4 + "releases/v${ICOP_VERSION}/mac/plugins" vlc-macos-smoke.log + - name: Upload VLC smoke logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: vlc-macos-x86_64-smoke-logs + path: vlc-macos-smoke*.log + if-no-files-found: ignore - name: Upload release artifact uses: actions/upload-artifact@v4 with: @@ -355,6 +374,25 @@ jobs: -DNSFW_INSTALL_VLC_PLUGIN=OFF - name: Package run: cmake --build build-ci --target icop_package -j $(sysctl -n hw.logicalcpu) + - name: Verify macOS package + run: sh tools/verify_macos_package.sh "releases/v${ICOP_VERSION}/mac" arm64 14.0 + - name: Install VLC from Homebrew + run: | + brew install --cask vlc + xattr -dr com.apple.quarantine /Applications/VLC.app || true + - name: Check bundled installer against Homebrew VLC + run: sh "releases/v${ICOP_VERSION}/mac/install_icop_plugin.sh" --dry-run + - name: Smoke test icop in VLC + run: >- + sh tools/macos_vlc_smoke_test.sh /Applications/VLC.app sample.mp4 + "releases/v${ICOP_VERSION}/mac/plugins" vlc-macos-smoke.log + - name: Upload VLC smoke logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: vlc-macos-arm64-smoke-logs + path: vlc-macos-smoke*.log + if-no-files-found: ignore - name: Upload release artifact uses: actions/upload-artifact@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 957b297..67a30a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,25 @@ first, and short hashes identify the commit that introduced each change. ## Unreleased -No unreleased changes are documented yet. +### macOS / Apple Silicon + +- Build the macOS plugin as `libicop_plugin.dylib`. VLC for macOS only loads + `lib*_plugin.dylib`, so previous macOS releases were silently ignored. +- Pin the macOS deployment target to 14.0 so packages built on the newest CI + runners still load on older Apple Silicon Macs. +- Package ONNX Runtime 1.23.2 for Intel and universal2 builds, because 1.26.0 + has no x86_64 macOS archive and the Intel package shipped without a runtime. + Never pick up a Homebrew `onnxruntime` for macOS packages. +- Ad-hoc sign any packaged dylib whose signature does not verify, with an + optional Developer ID identity. +- The POSIX installer now ships inside Linux/macOS archives and accepts + `--release-dir`. On macOS it finds VLC.app from the DMG, Homebrew cask + (including `--appdir`), `~/Applications`, MacPorts, or Spotlight, and + matches the payload to VLC's architecture (Rosetta aware). It also rejects + VLC 4, clears download quarantine, removes the stale `.so` plugin, and + treats a missing `vlc-cache-gen` as non-fatal. +- Release CI verifies the macOS package and runs a Homebrew VLC smoke test. + PR CI checks that VLC registers the plugin on both macOS architectures. ## 0.1.4 - 2026-07-17 diff --git a/CMakeLists.txt b/CMakeLists.txt index 50bad5d..ee68735 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,13 @@ cmake_minimum_required(VERSION 3.16) +# Without an explicit floor, Apple toolchains stamp binaries with the build +# host's macOS version, so a package built on the newest CI runner refuses to +# load on older Apple Silicon Macs. 14.0 matches the minimum of the packaged +# osx-arm64 ONNX Runtime; it must be set before project(). +if(CMAKE_HOST_APPLE AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET AND + NOT DEFINED ENV{MACOSX_DEPLOYMENT_TARGET}) + set(CMAKE_OSX_DEPLOYMENT_TARGET "14.0" CACHE STRING + "Minimum macOS version for the VLC plugin and detector core") +endif() project(icop VERSION 0.1.6 DESCRIPTION "Privacy-first sensitive-content filtering for VLC media player" @@ -226,6 +235,9 @@ if(NSFW_BUILD_VLC_MODULE) target_link_libraries(icop_plugin PRIVATE va va-drm) endif() if(APPLE) + # VLC for macOS only scans plugins named lib*_plugin.dylib; CMake's + # default MODULE suffix (.so) makes VLC silently skip the filter. + set_target_properties(icop_plugin PROPERTIES SUFFIX ".dylib") target_link_options(icop_plugin PRIVATE "-Wl,-undefined,dynamic_lookup" ) diff --git a/INSTALL.md b/INSTALL.md index 002259d..7be9511 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -12,8 +12,13 @@ Get the package for your platform from the | Windows x86_64 | `icop-v0.1.4-windows-x86_64.7z.001` and subsequent volumes | | Linux x86_64 | `icop-v0.1.4-linux-x86_64.tar.gz` | | Linux ARM64 | `icop-v0.1.4-linux-arm64.tar.gz` | -| macOS x86_64 | `icop-v0.1.4-mac-x86_64.tar.gz` | -| macOS ARM64 | `icop-v0.1.4-mac-arm64.tar.gz` | +| macOS Intel (x86_64) | `icop-v0.1.4-mac-x86_64.tar.gz` | +| macOS Apple Silicon (M1/M2/M3/M4, ARM64) | `icop-v0.1.4-mac-arm64.tar.gz` | + +On macOS, pick the archive that matches **VLC**, not the Mac: an Apple Silicon +Mac running the Intel-only VLC build needs the x86_64 archive. The bundled +installer (Step 2) detects this for you. Releases up to v0.1.6 shipped a macOS +plugin that VLC could not load; use a newer release. For the Windows CUDA package, download every `.7z` volume into the same folder and extract the `.7z.001` file with [7-Zip](https://www.7-zip.org/). @@ -26,6 +31,8 @@ and extract the `.7z.001` file with [7-Zip](https://www.7-zip.org/). > an elevated PowerShell (Run as Administrator). Do not ask the user to perform > setup steps when they asked you to do the work directly. +On macOS, follow [Step 2 (macOS)](#step-2-macos--install-into-vlcapp) below. + Extract the archive and copy the files from `plugins/video_filter/` into VLC's plugin directory: @@ -47,11 +54,67 @@ After copying, regenerate VLC's plugin cache: sudo /usr/lib/vlc/vlc-cache-gen /usr/lib/vlc/plugins ``` -**macOS**: +## Step 2 (macOS) — Install into VLC.app + +Requirements: macOS 14 (Sonoma) or newer and VLC **3.0.x** (VLC 4 nightlies +use a different plugin ABI). All of these VLC installs are supported: + +| How VLC was installed | Location the installer finds | +|---|---| +| DMG from [videolan.org](https://www.videolan.org/vlc/download-macosx.html) | `/Applications/VLC.app` | +| `brew install --cask vlc` | `/Applications/VLC.app`, or the `--appdir` in `HOMEBREW_CASK_OPTS` | +| Per-user install | `~/Applications/VLC.app` | +| MacPorts | `/Applications/MacPorts/VLC.app` | +| Anywhere else | Found through Spotlight, or pass `--vlc-root /path/to/VLC.app` | + +Quit VLC, then extract the archive and run the installer that ships inside it: + +```bash +tar xzf icop-v-mac-arm64.tar.gz +sh mac/install_icop_plugin.sh --dry-run # shows which VLC.app and payload it picked +sh mac/install_icop_plugin.sh +``` + +The installer: + +- verifies `SHA256SUMS`, then copies the payload into + `VLC.app/Contents/MacOS/plugins/video_filter/`, asking for `sudo` only if the + bundle is not writable, and rolls back if any step fails; +- checks the VLC.app architecture with `lipo`, so an Intel-only VLC running + under Rosetta on Apple Silicon gets the x86_64 payload (use `--arch` to + override); +- clears the `com.apple.quarantine` flag that browsers add to downloads. + Without this, Gatekeeper refuses to load the dylibs inside VLC; +- removes the unusable `libicop_plugin.so` left by releases up to v0.1.6; +- regenerates VLC's plugin cache when `vlc-cache-gen` is bundled. When it is + not, VLC still loads the new plugin at launch. + +If the copy fails with *Operation not permitted*, macOS App Management is +protecting VLC.app. Allow your terminal app under +*System Settings → Privacy & Security → App Management* and rerun. + +VLC updates (Sparkle auto-update or `brew upgrade --cask vlc`) replace +VLC.app and remove the plugin, so rerun the installer after each VLC update. + +Manual install, if you prefer not to run the script: + ```bash -sudo /Applications/VLC.app/Contents/MacOS/vlc-cache-gen /Applications/VLC.app/Contents/MacOS/plugins +PLUGINS=/Applications/VLC.app/Contents/MacOS/plugins +cp mac/plugins/video_filter/* "$PLUGINS/video_filter/" +xattr -d com.apple.quarantine "$PLUGINS"/video_filter/libicop_* "$PLUGINS"/video_filter/libonnxruntime*.dylib "$PLUGINS"/video_filter/*.onnx 2>/dev/null +rm -f "$PLUGINS/video_filter/libicop_plugin.so" +/Applications/VLC.app/Contents/MacOS/VLC --reset-plugins-cache vlc://quit ``` +Start VLC from Terminal once to confirm the filter loads: + +```bash +/Applications/VLC.app/Contents/MacOS/VLC --video-filter=icop /path/to/video.mp4 2>&1 | grep '^icop' +``` + +Expect a line such as `icop: using marqo model profile (...)`. If it reports +`unable to load core DLL`, the plugin files are not all in the same folder. + ## Step 3 — Enable the filter Launch VLC with the icop filter active on the command line: diff --git a/README.md b/README.md index 9e3cfc1..73c4e9e 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,10 @@ locally and are not committed. | macOS x86_64 | Tested | CPU / CoreML | | macOS ARM64 | Tested | CPU / CoreML | -GPU runtimes are detected automatically at plugin load. Contributions that -improve GPU support or validate the macOS path are welcome. +GPU runtimes are detected automatically at plugin load. macOS packages need +macOS 14 or newer and VLC 3.0.x. Apple Silicon uses ONNX Runtime 1.26; Intel +Macs use ONNX Runtime 1.23.2, the last release Microsoft published for x86_64 +macOS. Contributions that improve GPU support are welcome. ## VLC AI Filter Features @@ -195,6 +197,25 @@ sh tools/install_icop_plugin.sh --vlc-root /usr Close VLC before installation. To stop it explicitly, pass `INSTALL_ARGS=-StopVlc` on Windows or `INSTALL_ARGS=--stop-vlc` on Linux/macOS. +### macOS (Apple Silicon and Intel) + +Linux and macOS release archives include `install_icop_plugin.sh`, so a +downloaded release installs without a source checkout: + +```sh +tar xzf icop-v-mac-arm64.tar.gz +sh mac/install_icop_plugin.sh +``` + +The installer finds VLC.app wherever it was installed: the videolan.org DMG, +`brew install --cask vlc` (including a custom `--appdir`), `~/Applications`, +MacPorts, or through Spotlight. It matches the payload to the VLC.app +architecture, so an Intel-only VLC running under Rosetta gets the x86_64 +build. It also clears the download quarantine flag that would otherwise stop +Gatekeeper from loading the plugin. Rerun it after VLC updates, because updates +replace VLC.app. See [INSTALL.md](INSTALL.md#step-2-macos--install-into-vlcapp) +for manual steps and troubleshooting. + See [CONTRIBUTING.md](CONTRIBUTING.md) for lightweight Windows, Linux, and WSL build and test commands. See [docs/releasing.md](docs/releasing.md) for package versioning, checksums, and release verification. diff --git a/cmake/macos_codesign.cmake b/cmake/macos_codesign.cmake new file mode 100644 index 0000000..8211f9e --- /dev/null +++ b/cmake/macos_codesign.cmake @@ -0,0 +1,49 @@ +# Ensures every Mach-O in a macOS release payload carries a valid signature. +# +# Apple Silicon refuses to map arm64 code without one. Linker-signed outputs +# and Microsoft's ONNX Runtime normally verify already and are left untouched; +# anything that does not verify is ad-hoc signed. VLC.app is built with the +# com.apple.security.cs.disable-library-validation entitlement, so ad-hoc +# signed plugins are accepted. Set CODESIGN_IDENTITY to a Developer ID to +# re-sign every file with a hardened-runtime signature instead. + +if(NOT DEFINED PAYLOAD_DIR OR NOT IS_DIRECTORY "${PAYLOAD_DIR}") + message(FATAL_ERROR "PAYLOAD_DIR must name the release plugin directory") +endif() +if(NOT DEFINED CODESIGN_IDENTITY OR CODESIGN_IDENTITY STREQUAL "") + set(CODESIGN_IDENTITY "-") +endif() + +find_program(_codesign codesign) +if(NOT _codesign) + message(FATAL_ERROR "codesign is required to package icop for macOS") +endif() + +file(GLOB_RECURSE _dylibs LIST_DIRECTORIES false "${PAYLOAD_DIR}/*.dylib") +if(NOT _dylibs) + message(FATAL_ERROR "No .dylib files were found in ${PAYLOAD_DIR}") +endif() + +foreach(_dylib IN LISTS _dylibs) + if(CODESIGN_IDENTITY STREQUAL "-") + execute_process( + COMMAND "${_codesign}" --verify --strict "${_dylib}" + RESULT_VARIABLE _verify_result + OUTPUT_QUIET ERROR_QUIET) + if(_verify_result EQUAL 0) + continue() + endif() + set(_sign_args --force --sign - --timestamp=none) + else() + set(_sign_args --force --sign "${CODESIGN_IDENTITY}" + --options runtime --timestamp) + endif() + + execute_process( + COMMAND "${_codesign}" ${_sign_args} "${_dylib}" + RESULT_VARIABLE _sign_result) + if(NOT _sign_result EQUAL 0) + message(FATAL_ERROR "Failed to sign ${_dylib}") + endif() + message(STATUS "Signed ${_dylib} (${CODESIGN_IDENTITY})") +endforeach() diff --git a/cmake/onnxruntime.cmake b/cmake/onnxruntime.cmake index 406fc47..c5255a8 100644 --- a/cmake/onnxruntime.cmake +++ b/cmake/onnxruntime.cmake @@ -16,6 +16,29 @@ if(NOT ICOP_RELEASE_VERSION MATCHES "ICOP_RELEASE_VERSION must be a semantic version such as 0.1.0 or 0.2.0-rc.1") endif() set(NSFW_ONNXRUNTIME_VERSION "1.26.0") +# Microsoft publishes only osx-arm64 macOS runtimes after 1.23.2, so Intel and +# universal2 macOS builds pin the last release that ships their archive. The +# headers below follow this version, keeping the C API matched to the runtime. +set(_nsfw_apple_ort_suffix "") +if(APPLE) + if(CMAKE_OSX_ARCHITECTURES MATCHES ";") + set(_nsfw_apple_ort_suffix "osx-universal2") + elseif(CMAKE_OSX_ARCHITECTURES) + set(_nsfw_apple_arch "${CMAKE_OSX_ARCHITECTURES}") + else() + set(_nsfw_apple_arch "${CMAKE_SYSTEM_PROCESSOR}") + endif() + if(NOT _nsfw_apple_ort_suffix) + if(_nsfw_apple_arch MATCHES "^(aarch64|arm64)$") + set(_nsfw_apple_ort_suffix "osx-arm64") + elseif(_nsfw_apple_arch MATCHES "^(x86_64|amd64|AMD64)$") + set(_nsfw_apple_ort_suffix "osx-x86_64") + endif() + endif() + if(_nsfw_apple_ort_suffix MATCHES "^osx-(x86_64|universal2)$") + set(NSFW_ONNXRUNTIME_VERSION "1.23.2") + endif() +endif() # DirectML is shipped by ONNX Runtime as a separate Windows runtime, rather # than as a provider DLL compatible with the CUDA package above. Keep this # version pinned with its headers so the isolated DirectML host has a matching @@ -580,7 +603,10 @@ else() "${ONNXRUNTIME_ROOT}/lib64" NO_DEFAULT_PATH) endif() - if(NOT NSFW_ONNXRUNTIME_LIBRARY_PATH) + # A Homebrew onnxruntime links other Homebrew dylibs by absolute path and + # does not match the pinned headers, so macOS packages always use the + # self-contained Microsoft archive unless ONNXRUNTIME_ROOT is given. + if(NOT NSFW_ONNXRUNTIME_LIBRARY_PATH AND NOT APPLE) find_library(NSFW_ONNXRUNTIME_LIBRARY_PATH NAMES onnxruntime libonnxruntime) endif() @@ -591,11 +617,7 @@ else() set(_nsfw_ort_glob_providers "libonnxruntime_providers_*.so") if(APPLE) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") - set(_nsfw_ort_archive_suffix "osx-arm64") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") - set(_nsfw_ort_archive_suffix "osx-x86_64") - endif() + set(_nsfw_ort_archive_suffix "${_nsfw_apple_ort_suffix}") set(_nsfw_ort_lib_name "libonnxruntime.dylib") set(_nsfw_ort_glob_providers "") elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") diff --git a/cmake/options.cmake b/cmake/options.cmake index 14b1ca1..48ff4cd 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -15,3 +15,5 @@ option(NSFW_GPU_RUNTIME "Download the GPU-enabled ONNX Runtime binary and stage GPU provider libraries" OFF) set(NSFW_CUDA_VERSION "" CACHE STRING "CUDA major version for GPU ONNX Runtime (auto-detected if empty)") +set(NSFW_MACOS_CODESIGN_IDENTITY "-" CACHE STRING + "macOS codesign identity for packaged dylibs ('-' ad-hoc signs only files that do not verify)") diff --git a/cmake/packaging.cmake b/cmake/packaging.cmake index 3ab35f1..e3f1a16 100644 --- a/cmake/packaging.cmake +++ b/cmake/packaging.cmake @@ -209,6 +209,24 @@ if(WIN32) endif() endif() +if(APPLE) + list(APPEND _nsfw_package_commands + COMMAND "${CMAKE_COMMAND}" + "-DPAYLOAD_DIR=${NSFW_RELEASE_PLUGIN_DIR}" + "-DCODESIGN_IDENTITY=${NSFW_MACOS_CODESIGN_IDENTITY}" + -P "${CMAKE_SOURCE_DIR}/cmake/macos_codesign.cmake") +endif() + +# Ship the installer inside POSIX archives so a downloaded release can be +# installed without a source checkout. It sits beside release.json, outside +# the checksummed plugin payload. +if(NOT WIN32) + list(APPEND _nsfw_package_commands + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${CMAKE_SOURCE_DIR}/tools/install_icop_plugin.sh" + "${NSFW_RELEASE_PLATFORM_DIR}/install_icop_plugin.sh") +endif() + list(APPEND _nsfw_package_commands COMMAND "${CMAKE_COMMAND}" "-DRELEASE_DIR=${NSFW_RELEASE_PLATFORM_DIR}" diff --git a/docs/index.md b/docs/index.md index 626cd9e..365ac3e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,13 @@ - [tools/install_icop_plugin.ps1](../tools/install_icop_plugin.ps1) Checksum-verified Windows VLC detection and plugin installation. - [tools/install_icop_plugin.sh](../tools/install_icop_plugin.sh) - Checksum-verified Linux/macOS VLC detection and plugin installation. + Checksum-verified Linux/macOS VLC detection and plugin installation, also + bundled inside Linux/macOS release archives. +- [tools/verify_macos_package.sh](../tools/verify_macos_package.sh) + macOS release check: `.dylib` naming, architectures, minimum macOS, code + signatures, and portable library references. +- [tools/macos_vlc_smoke_test.sh](../tools/macos_vlc_smoke_test.sh) + Loads the filter in a real VLC.app and fails unless the detector initializes. ## Validation Paths diff --git a/docs/releasing.md b/docs/releasing.md index f56832c..175ee49 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -27,6 +27,17 @@ their platform folders. Never present an unbuilt platform marker as a release. - On Windows, run an installed or portable VLC smoke test and confirm the detector loads the side-by-side model and ONNX Runtime rather than a system DLL. +- On macOS, the release workflow runs + `tools/verify_macos_package.sh releases/v/mac `. This checks + that the plugin is named `libicop_plugin.dylib` (VLC ignores `.so` on + macOS), that every dylib has the expected architecture, requires macOS 14.0 + at most, has a valid code signature, and links only system or sibling + libraries. The workflow then installs VLC with + `brew install --cask vlc`, dry-runs the bundled installer, and runs + `tools/macos_vlc_smoke_test.sh`, which fails unless VLC registers `icop` and + the detector initializes. Run both scripts by hand when packaging on a Mac. + Set `NSFW_MACOS_CODESIGN_IDENTITY` to a Developer ID to sign the payload with + a hardened-runtime signature instead of the ad-hoc default. - Review [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) before distributing any model or runtime binary. diff --git a/tools/detect_vlc_plugin_dir.sh b/tools/detect_vlc_plugin_dir.sh index d5473c7..9adbe7e 100755 --- a/tools/detect_vlc_plugin_dir.sh +++ b/tools/detect_vlc_plugin_dir.sh @@ -10,7 +10,8 @@ esac if [ "$platform" = mac ]; then candidates=" /Applications/VLC.app/Contents/MacOS/plugins/video_filter -$HOME/Applications/VLC.app/Contents/MacOS/plugins/video_filter" +$HOME/Applications/VLC.app/Contents/MacOS/plugins/video_filter +/Applications/MacPorts/VLC.app/Contents/MacOS/plugins/video_filter" else multiarch="" if command -v gcc >/dev/null 2>&1; then diff --git a/tools/install_icop_plugin.sh b/tools/install_icop_plugin.sh index 03ec1e0..44572ca 100755 --- a/tools/install_icop_plugin.sh +++ b/tools/install_icop_plugin.sh @@ -3,8 +3,10 @@ set -eu script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -release_root="$script_dir/../releases" +release_root="" +release_dir="" vlc_root="" +requested_arch="" version="" dry_run=0 stop_vlc=0 @@ -16,7 +18,12 @@ Usage: install_icop_plugin.sh [options] Options: --release-root PATH Root containing versioned icop releases + --release-dir PATH One extracted platform release (the folder holding + release.json); used automatically when this script + runs from inside a downloaded release archive --vlc-root PATH VLC installation prefix or VLC.app path + --arch ARCH Override the payload architecture (x86_64 or arm64); + on macOS it defaults to the VLC.app architecture --version VERSION Install a specific release version --dry-run Detect and verify without changing VLC --stop-vlc Stop the selected platform's running VLC process @@ -37,6 +44,16 @@ while [ "$#" -gt 0 ]; do release_root=$2 shift 2 ;; + --release-dir) + [ "$#" -ge 2 ] || die '--release-dir requires a path' + release_dir=$2 + shift 2 + ;; + --arch) + [ "$#" -ge 2 ] || die '--arch requires a value' + requested_arch=$2 + shift 2 + ;; --vlc-root) [ "$#" -ge 2 ] || die '--vlc-root requires a path' vlc_root=$2 @@ -83,27 +100,50 @@ case "$(uname -s)" in ;; esac -case "$(uname -m)" in - x86_64|amd64) - architecture=x86_64 - ;; - arm64|aarch64) - architecture=arm64 - ;; - i386|i486|i586|i686) - architecture=x86 - ;; - *) - die "unsupported host architecture: $(uname -m)" - ;; -esac +normalize_arch() { + case "$1" in + x86_64|amd64) printf 'x86_64\n' ;; + arm64|aarch64|arm64e) printf 'arm64\n' ;; + i386|i486|i586|i686) printf 'x86\n' ;; + *) return 1 ;; + esac +} + +# Every place VLC.app is commonly installed on macOS: the DMG from +# videolan.org and `brew install --cask vlc` both use /Applications (or +# ~/Applications with a per-user --appdir), MacPorts uses /Applications/MacPorts, +# and Spotlight finds renamed or relocated bundles by their bundle identifier. +mac_vlc_app_candidates() { + printf '%s\n' /Applications/VLC.app "$HOME/Applications/VLC.app" \ + /Applications/MacPorts/VLC.app + cask_appdir=$(printf '%s\n' "${HOMEBREW_CASK_OPTS:-}" | + sed -n 's/.*--appdir[= ]\{1,\}\([^ ]*\).*/\1/p') + if [ -n "$cask_appdir" ]; then + case "$cask_appdir" in + "~"/*) cask_appdir="$HOME/${cask_appdir#??}" ;; + esac + printf '%s\n' "$cask_appdir/VLC.app" + fi + if command -v mdfind >/dev/null 2>&1; then + mdfind "kMDItemCFBundleIdentifier == 'org.videolan.vlc'" 2>/dev/null || true + fi +} + +mac_app_for_root() { + case "$1" in + *.app|*.app/) printf '%s\n' "${1%/}" ;; + */Contents/MacOS|*/Contents/MacOS/) printf '%s\n' "${1%/Contents/MacOS*}" ;; + *) return 1 ;; + esac +} find_plugin_directory() { if [ -n "$vlc_root" ]; then if [ "$platform" = mac ]; then candidates=" $vlc_root/Contents/MacOS/plugins/video_filter -$vlc_root/plugins/video_filter" +$vlc_root/plugins/video_filter +$vlc_root/lib/vlc/plugins/video_filter" else multiarch="" if command -v gcc >/dev/null 2>&1; then @@ -119,9 +159,11 @@ $vlc_root/lib/$multiarch/vlc/plugins/video_filter" fi fi elif [ "$platform" = mac ]; then - candidates=" -/Applications/VLC.app/Contents/MacOS/plugins/video_filter -$HOME/Applications/VLC.app/Contents/MacOS/plugins/video_filter" + candidates=$(mac_vlc_app_candidates | while IFS= read -r app; do + if [ -n "$app" ]; then + printf '%s/Contents/MacOS/plugins/video_filter\n' "$app" + fi + done) else multiarch="" if command -v gcc >/dev/null 2>&1; then @@ -154,6 +196,70 @@ plugin_directory=$(find_plugin_directory) [ -n "$plugin_directory" ] || die 'no VLC video_filter plugin directory was found; pass --vlc-root' plugin_root=$(dirname -- "$plugin_directory") +vlc_app="" +if [ "$platform" = mac ]; then + case "$plugin_directory" in + */Contents/MacOS/plugins/video_filter) + vlc_app=${plugin_directory%/Contents/MacOS/plugins/video_filter} + ;; + esac + [ -n "$vlc_app" ] || vlc_app=$(mac_app_for_root "$vlc_root" 2>/dev/null || true) +fi + +# icop is built against the VLC 3.0 plugin ABI; VLC 4 nightlies cannot load it. +if [ -n "$vlc_app" ] && [ -f "$vlc_app/Contents/Info.plist" ]; then + vlc_version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \ + "$vlc_app/Contents/Info.plist" 2>/dev/null || true) + case "$vlc_version" in + 3.*|'') ;; + *) die "VLC $vlc_version at $vlc_app is not supported; icop requires VLC 3.0.x" ;; + esac +fi + +if [ -n "$requested_arch" ]; then + architecture=$(normalize_arch "$requested_arch") || + die "unsupported --arch value: $requested_arch" +elif [ "$platform" = mac ]; then + # The payload must match the process that loads it, not the shell: an + # Intel-only VLC runs under Rosetta on Apple Silicon, and a Rosetta + # terminal reports x86_64 even when VLC itself runs natively on arm64. + native_arm64=0 + if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = 1 ]; then + native_arm64=1 + fi + vlc_archs="" + if [ -n "$vlc_app" ] && [ -f "$vlc_app/Contents/MacOS/VLC" ]; then + vlc_archs=$(lipo -archs "$vlc_app/Contents/MacOS/VLC" 2>/dev/null || true) + fi + case " $vlc_archs " in + *" arm64 "*) + if [ "$native_arm64" -eq 1 ]; then + architecture=arm64 + else + architecture=x86_64 + fi + ;; + *" x86_64 "*) + architecture=x86_64 + if [ "$native_arm64" -eq 1 ]; then + printf 'icop installer: %s is an Intel-only VLC running under Rosetta; installing the x86_64 payload.\n' "$vlc_app" >&2 + printf 'icop installer: install the Apple Silicon or Universal VLC to run icop natively.\n' >&2 + fi + ;; + *) + if [ "$native_arm64" -eq 1 ]; then + architecture=arm64 + else + architecture=$(normalize_arch "$(uname -m)") || + die "unsupported host architecture: $(uname -m)" + fi + ;; + esac +else + architecture=$(normalize_arch "$(uname -m)") || + die "unsupported host architecture: $(uname -m)" +fi + release_matches() { manifest=$1 [ -f "$manifest" ] || return 1 @@ -196,17 +302,39 @@ select_latest_release() { printf '%s\n' "$best_directory" } -if [ -n "$version" ]; then +# A downloaded release archive carries this script beside its release.json. +if [ -z "$release_dir" ] && [ -z "$release_root" ] && + [ -f "$script_dir/release.json" ]; then + release_dir=$script_dir +fi +[ -n "$release_root" ] || release_root="$script_dir/../releases" + +if [ -n "$release_dir" ]; then + platform_release=$(CDPATH= cd -- "$release_dir" && pwd) || + die "release directory does not exist: $release_dir" + release_matches "$platform_release/release.json" || { + manifest_arch=$(sed -n 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$platform_release/release.json" 2>/dev/null | head -n 1) + die "$platform_release is not an icop $platform $architecture release${manifest_arch:+ (it is $manifest_arch); download the $architecture archive or pass --arch}" + } + release_version=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$platform_release/release.json" | head -n 1) + if [ -n "$version" ] && [ "$version" != "$release_version" ]; then + die "$platform_release contains icop $release_version, not $version" + fi + version=$release_version +elif [ -n "$version" ]; then version_directory="$release_root/v$version" release_matches "$version_directory/$platform/release.json" || die "no matching icop $platform $architecture release exists for version $version" + platform_release="$version_directory/$platform" else version_directory=$(select_latest_release) || die "no matching icop $platform $architecture release exists under $release_root" version=${version_directory##*/v} + platform_release="$version_directory/$platform" fi -platform_release="$version_directory/$platform" checksum_file="$platform_release/SHA256SUMS" [ -f "$checksum_file" ] || die "release checksum file is missing: $checksum_file" @@ -339,37 +467,43 @@ cleanup_install() { } trap cleanup_install 0 HUP INT TERM +legacy_names=" +libnsfw_filter_plugin.so +libnsfw_filter_core.so +libnsfw_filter_plugin.dylib +libnsfw_filter_core.dylib +freepik-nsfw.onnx +nsfw-classifier-int8.onnx +dml/freepik-nsfw.onnx +dml/nsfw-classifier-int8.onnx" +if [ "$platform" = mac ]; then + # Releases up to 0.1.6 shipped the macOS plugin with a .so suffix, which + # VLC for macOS never loads. + legacy_names="$legacy_names +libicop_plugin.so" +fi + while IFS= read -r payload_relative; do backup_destination "$plugin_directory/$payload_relative" done < "$payload_list" -for legacy_name in \ - libnsfw_filter_plugin.so \ - libnsfw_filter_core.so \ - libnsfw_filter_plugin.dylib \ - libnsfw_filter_core.dylib \ - freepik-nsfw.onnx \ - nsfw-classifier-int8.onnx \ - dml/freepik-nsfw.onnx \ - dml/nsfw-classifier-int8.onnx; do +for legacy_name in $legacy_names; do backup_destination "$plugin_directory/$legacy_name" done while IFS= read -r payload_relative; do source_file="$platform_release/plugins/video_filter/$payload_relative" destination="$plugin_directory/$payload_relative" - run_admin mkdir -p "$(dirname -- "$destination")" - run_admin cp -p "$source_file" "$destination" + if ! run_admin mkdir -p "$(dirname -- "$destination")" || + ! run_admin cp -p "$source_file" "$destination"; then + if [ "$platform" = mac ]; then + printf 'icop installer: macOS blocked writing into %s.\n' "$vlc_app" >&2 + printf 'icop installer: allow your terminal in System Settings > Privacy & Security > App Management, then rerun.\n' >&2 + fi + die "failed to copy $payload_relative into $plugin_directory" + fi done < "$payload_list" -for legacy_name in \ - libnsfw_filter_plugin.so \ - libnsfw_filter_core.so \ - libnsfw_filter_plugin.dylib \ - libnsfw_filter_core.dylib \ - freepik-nsfw.onnx \ - nsfw-classifier-int8.onnx \ - dml/freepik-nsfw.onnx \ - dml/nsfw-classifier-int8.onnx; do +for legacy_name in $legacy_names; do run_admin rm -f "$plugin_directory/$legacy_name" done @@ -380,9 +514,30 @@ while IFS= read -r payload_relative; do die "installed checksum mismatch: $destination" done < "$payload_list" +if [ "$platform" = mac ]; then + # Browsers and Archive Utility tag downloads with com.apple.quarantine and + # `cp -p` carries it over; Gatekeeper then refuses to dlopen the dylibs + # inside VLC. The payload was checksum-verified above, so clear the flag. + while IFS= read -r payload_relative; do + destination="$plugin_directory/$payload_relative" + if xattr "$destination" 2>/dev/null | grep -qx 'com.apple.quarantine'; then + run_admin xattr -d com.apple.quarantine "$destination" || + die "could not clear the quarantine flag on $destination" + fi + case "$destination" in + *.dylib) + codesign --verify "$destination" >/dev/null 2>&1 || + die "code signature is invalid: $destination" + ;; + esac + done < "$payload_list" +fi + if [ "$skip_cache" -ne 1 ]; then cache_generator="" - if command -v vlc-cache-gen >/dev/null 2>&1; then + if [ -n "$vlc_app" ] && [ -x "$vlc_app/Contents/MacOS/vlc-cache-gen" ]; then + cache_generator="$vlc_app/Contents/MacOS/vlc-cache-gen" + elif command -v vlc-cache-gen >/dev/null 2>&1; then cache_generator=$(command -v vlc-cache-gen) elif [ -n "$vlc_root" ] && [ -x "$vlc_root/Contents/MacOS/vlc-cache-gen" ]; then cache_generator="$vlc_root/Contents/MacOS/vlc-cache-gen" @@ -391,8 +546,16 @@ if [ "$skip_cache" -ne 1 ]; then elif [ -x "$(dirname -- "$plugin_root")/vlc-cache-gen" ]; then cache_generator="$(dirname -- "$plugin_root")/vlc-cache-gen" fi - [ -n "$cache_generator" ] || die 'vlc-cache-gen was not found; rerun with --skip-cache only for testing' - run_admin "$cache_generator" "$plugin_root" + if [ -n "$cache_generator" ]; then + run_admin "$cache_generator" "$plugin_root" + elif [ "$platform" = mac ]; then + # VLC.app does not always ship vlc-cache-gen. VLC 3 still scans the + # plugin folder and loads files missing from plugins.dat, so the + # filter works without a cache refresh. + printf 'icop installer: vlc-cache-gen is not bundled with this VLC.app; VLC will load icop without a cache entry.\n' >&2 + else + die 'vlc-cache-gen was not found; rerun with --skip-cache only for testing' + fi fi success=1 diff --git a/tools/macos_vlc_smoke_test.sh b/tools/macos_vlc_smoke_test.sh new file mode 100755 index 0000000..8fa4e35 --- /dev/null +++ b/tools/macos_vlc_smoke_test.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# Plays a clip through VLC.app with the icop filter and checks that VLC +# registered the plugin and that the detector reached ONNX Runtime. +# +# Usage: macos_vlc_smoke_test.sh VLC_APP VIDEO [PLUGIN_PATH] [LOG_FILE] +# PLUGIN_PATH optional folder exported as VLC_PLUGIN_PATH, e.g. an extracted +# release's plugins/ folder, so the signed VLC.app bundle does +# not have to be modified; omit it to test an installed plugin. + +set -eu + +die() { + printf 'macos_vlc_smoke_test: %s\n' "$*" >&2 + exit 1 +} + +[ "$#" -ge 2 ] || die 'usage: macos_vlc_smoke_test.sh VLC_APP VIDEO [PLUGIN_PATH] [LOG_FILE]' +vlc_app=$1 +video=$2 +plugin_path=${3:-} +log_file=${4:-vlc-macos-smoke.log} +if [ -n "$plugin_path" ]; then + [ -d "$plugin_path" ] || die "plugin path not found: $plugin_path" + VLC_PLUGIN_PATH=$(CDPATH= cd -- "$plugin_path" && pwd) + export VLC_PLUGIN_PATH +fi +vlc="$vlc_app/Contents/MacOS/VLC" +[ -x "$vlc" ] || die "VLC executable not found: $vlc" +[ -f "$video" ] || die "video not found: $video" + +# perl ships with macOS; coreutils `timeout` does not. +with_timeout() { + perl -e 'alarm shift @ARGV; exec @ARGV or die "exec: $!"' "$@" +} + +list_log="${log_file%.log}-modules.log" +with_timeout 120 "$vlc" -I dummy --list > "$list_log" 2>&1 || true +if ! grep -Eq '^[[:space:]]+icop[[:space:]]' "$list_log"; then + grep -i icop "$list_log" >&2 || true + die 'VLC did not register the icop module' +fi +printf 'VLC registered the icop module\n' + +with_timeout 180 "$vlc" -I dummy --vout=dummy --aout=dummy \ + --no-video-title-show --play-and-exit --video-filter=icop \ + "$video" > "$log_file" 2>&1 || true +grep '^icop' "$log_file" || true + +for failure in \ + 'unable to load core DLL' \ + 'unable to load the packaged ONNX Runtime' \ + 'ONNX detector unavailable' \ + 'detector initialization failed' \ + 'does not support C API version'; do + if grep -q "$failure" "$log_file"; then + die "VLC reported: $failure (see $log_file)" + fi +done +grep -Eq '^icop: using [a-z0-9_-]+ model profile' "$log_file" || + die "the icop filter never initialized its detector (see $log_file)" + +printf 'icop filter initialized in %s\n' "$vlc_app" diff --git a/tools/verify_macos_package.sh b/tools/verify_macos_package.sh new file mode 100755 index 0000000..8d664e9 --- /dev/null +++ b/tools/verify_macos_package.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# Verifies a staged macOS icop release before it is published. +# +# Usage: verify_macos_package.sh RELEASE_DIR ARCH [MIN_MACOS] +# RELEASE_DIR releases/v/mac (the folder holding release.json) +# ARCH arm64, x86_64 or universal2 +# MIN_MACOS highest minimum macOS version any binary may require +# (default 14.0) + +set -eu + +die() { + printf 'verify_macos_package: %s\n' "$*" >&2 + exit 1 +} + +[ "$#" -ge 2 ] || die 'usage: verify_macos_package.sh RELEASE_DIR ARCH [MIN_MACOS]' +release_dir=$1 +expected_arch=$2 +max_minos=${3:-14.0} +payload="$release_dir/plugins/video_filter" + +[ -d "$payload" ] || die "missing payload directory: $payload" +[ -f "$release_dir/release.json" ] || die 'missing release.json' +[ -f "$release_dir/install_icop_plugin.sh" ] || die 'missing bundled installer' +grep -Eq '"architecture"[[:space:]]*:[[:space:]]*"'"$expected_arch"'"' \ + "$release_dir/release.json" || die "release.json does not declare $expected_arch" +(cd "$release_dir" && shasum -a 256 -c SHA256SUMS >/dev/null) || + die 'SHA256SUMS verification failed' + +# VLC for macOS only loads lib*_plugin.dylib. +[ -f "$payload/libicop_plugin.dylib" ] || die 'libicop_plugin.dylib is missing' +[ ! -e "$payload/libicop_plugin.so" ] || die 'libicop_plugin.so would be ignored by VLC' +[ -f "$payload/libicop_core.dylib" ] || die 'libicop_core.dylib is missing' +[ -f "$payload/model.onnx" ] || die 'model.onnx is missing' +set -- "$payload"/libonnxruntime*.dylib +[ -f "$1" ] || die 'the packaged ONNX Runtime dylib is missing' + +version_le() { + [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -n 1)" = "$1" ] +} + +case "$expected_arch" in + universal2) required_archs='x86_64 arm64' ;; + *) required_archs=$expected_arch ;; +esac + +for dylib in "$payload"/*.dylib; do + name=${dylib##*/} + archs=$(lipo -archs "$dylib") + for arch in $required_archs; do + case " $archs " in + *" $arch "*) ;; + *) die "$name is built for '$archs', not $arch" ;; + esac + done + + for arch in $required_archs; do + minos=$(otool -arch "$arch" -l "$dylib" | awk ' + $1 == "cmd" && $2 == "LC_BUILD_VERSION" { build = 1; next } + build && $1 == "minos" { print $2; exit } + $1 == "cmd" && $2 == "LC_VERSION_MIN_MACOSX" { legacy = 1; next } + legacy && $1 == "version" { print $2; exit }') + [ -n "$minos" ] || die "$name ($arch) has no minimum macOS version" + version_le "$minos" "$max_minos" || + die "$name ($arch) requires macOS $minos, newer than $max_minos" + done + + codesign --verify --strict "$dylib" || + die "$name does not carry a valid code signature" + + # Only system libraries and siblings may be referenced; a Homebrew or + # build-tree path would fail on every other Mac. + otool -L "$dylib" | tail -n +2 | awk '{print $1}' | while IFS= read -r dep; do + case "$dep" in + /usr/lib/*|/System/Library/*|@rpath/*|@loader_path/*) ;; + *) die "$name links a non-portable library: $dep" ;; + esac + done + + printf 'ok %-34s %-14s\n' "$name" "$archs" +done + +printf 'macOS %s package verified: %s\n' "$expected_arch" "$release_dir" From 79621b3940375b0e86840559e2bd2b6b03273b8f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 19:13:09 +0000 Subject: [PATCH 2/2] Add Homebrew tap integration and installer uninstall mode - install_icop_plugin.sh --uninstall removes exactly the files listed in the release SHA256SUMS; the Homebrew cask uses it on uninstall. - New homebrew-tap workflow dispatches icop-release to asayed18/homebrew-icop when a release is published (optional HOMEBREW_TAP_TOKEN; the tap also polls on a schedule). - Document `brew install --cask icop`. - Bump to 0.1.7 so the fixed macOS packages do not reuse the v0.1.6 draft. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VDfqb9f52nSf89az5rPZBz --- .github/workflows/homebrew-tap.yml | 38 ++++++++++++++++++++++++++++++ CHANGELOG.md | 5 ++++ CMakeLists.txt | 2 +- INSTALL.md | 14 ++++++++++- README.md | 13 ++++++++-- tools/install_icop_plugin.sh | 16 +++++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/homebrew-tap.yml diff --git a/.github/workflows/homebrew-tap.yml b/.github/workflows/homebrew-tap.yml new file mode 100644 index 0000000..cd5f73f --- /dev/null +++ b/.github/workflows/homebrew-tap.yml @@ -0,0 +1,38 @@ +name: Homebrew tap + +# Asks asayed18/homebrew-icop to render, test and publish Casks/icop.rb for a +# newly published release. The tap also polls every six hours, so a missing +# HOMEBREW_TAP_TOKEN only delays the update. The token needs Contents +# read/write access to the tap repository. +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Release version to publish to the tap (e.g. 0.1.7)' + required: true + type: string + +permissions: + contents: read + +jobs: + notify: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Dispatch tap update + env: + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + VERSION: ${{ inputs.version || github.event.release.tag_name }} + run: | + if [ -z "$TAP_TOKEN" ]; then + echo "HOMEBREW_TAP_TOKEN is not set; the tap's scheduled check will pick up v${VERSION#v}." + exit 0 + fi + curl -fsS -X POST \ + -H "Authorization: Bearer $TAP_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/asayed18/homebrew-icop/dispatches \ + -d "{\"event_type\":\"icop-release\",\"client_payload\":{\"version\":\"${VERSION#v}\"}}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a30a1..abcee25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ first, and short hashes identify the commit that introduced each change. treats a missing `vlc-cache-gen` as non-fatal. - Release CI verifies the macOS package and runs a Homebrew VLC smoke test. PR CI checks that VLC registers the plugin on both macOS architectures. +- Add `install_icop_plugin.sh --uninstall`, and publish releases to the + `asayed18/homebrew-icop` tap (`brew install --cask icop`). A new workflow + notifies the tap when a release is published. +- Bump the version to 0.1.7 so the fixed macOS packages do not reuse the + broken v0.1.6 draft. ## 0.1.4 - 2026-07-17 diff --git a/CMakeLists.txt b/CMakeLists.txt index ee68735..16344c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ if(CMAKE_HOST_APPLE AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET AND "Minimum macOS version for the VLC plugin and detector core") endif() project(icop - VERSION 0.1.6 + VERSION 0.1.7 DESCRIPTION "Privacy-first sensitive-content filtering for VLC media player" HOMEPAGE_URL "https://github.com/asayed18/icop" LANGUAGES C CXX) diff --git a/INSTALL.md b/INSTALL.md index 7be9511..c615402 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -67,7 +67,19 @@ use a different plugin ABI). All of these VLC installs are supported: | MacPorts | `/Applications/MacPorts/VLC.app` | | Anywhere else | Found through Spotlight, or pass `--vlc-root /path/to/VLC.app` | -Quit VLC, then extract the archive and run the installer that ships inside it: +**Homebrew (recommended):** quit VLC, then run: + +```bash +brew tap asayed18/icop +brew install --cask icop +``` + +Use `brew reinstall --cask icop` after a VLC update and +`brew uninstall --cask icop` to remove the plugin. The cask runs the same +installer described below. + +**Direct download:** quit VLC, then extract the archive and run the installer +that ships inside it: ```bash tar xzf icop-v-mac-arm64.tar.gz diff --git a/README.md b/README.md index 73c4e9e..bf32fe4 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,17 @@ Close VLC before installation. To stop it explicitly, pass ### macOS (Apple Silicon and Intel) -Linux and macOS release archives include `install_icop_plugin.sh`, so a -downloaded release installs without a source checkout: +With Homebrew: + +```sh +brew tap asayed18/icop +brew install --cask icop +``` + +The [asayed18/homebrew-icop](https://github.com/asayed18/homebrew-icop) tap +updates itself when an icop release is published. Linux and macOS release +archives also include `install_icop_plugin.sh`, so a downloaded release +installs without a source checkout: ```sh tar xzf icop-v-mac-arm64.tar.gz diff --git a/tools/install_icop_plugin.sh b/tools/install_icop_plugin.sh index 44572ca..dcf5c9c 100755 --- a/tools/install_icop_plugin.sh +++ b/tools/install_icop_plugin.sh @@ -9,6 +9,7 @@ vlc_root="" requested_arch="" version="" dry_run=0 +uninstall=0 stop_vlc=0 skip_cache=0 @@ -26,6 +27,8 @@ Options: on macOS it defaults to the VLC.app architecture --version VERSION Install a specific release version --dry-run Detect and verify without changing VLC + --uninstall Remove this release's files from VLC instead of + installing them --stop-vlc Stop the selected platform's running VLC process --skip-cache Do not regenerate VLC's plugin cache --help Show this help @@ -64,6 +67,10 @@ while [ "$#" -gt 0 ]; do version=${2#v} shift 2 ;; + --uninstall) + uninstall=1 + shift + ;; --dry-run) dry_run=1 shift @@ -421,6 +428,15 @@ run_admin() { fi } +if [ "$uninstall" -eq 1 ]; then + while IFS= read -r payload_relative; do + run_admin rm -f "$plugin_directory/$payload_relative" + done < "$payload_list" + rm -f "$payload_list" + printf 'Removed icop %s from %s\n' "$version" "$plugin_directory" + exit 0 +fi + backup_directory=$(mktemp -d "${TMPDIR:-/tmp}/icop-install-backup.XXXXXX") existing_list="$backup_directory/existing" created_list="$backup_directory/created"