release #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| # Build the Old Cathode FFGL plugin for the two platforms Resolume runs on: | |
| # - macOS -> universal (arm64 + x86_64) OldCathode.bundle | |
| # - Windows -> x64 OldCathode.dll (GLEW static-linked via vcpkg) | |
| # Each ships as a zip; drop the bundle/dll into Resolume's "Extra Effects" | |
| # folder. Trigger manually (Actions > release > Run workflow) or by pushing a | |
| # v* tag. | |
| on: | |
| workflow_dispatch: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| name: ${{ matrix.name }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { name: macos-universal, os: macos-14, artifact: OldCathode.bundle } | |
| - { name: windows-x86_64, os: windows-latest, artifact: OldCathode.dll } | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| # --- macOS: no extra deps, universal build straight from CMake --- | |
| - name: Configure (macOS) | |
| if: runner.os == 'macOS' | |
| run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DOLDCATHODE_BUILD_TOOLS=OFF | |
| # --- Windows: GLEW comes from vcpkg, static-linked (static-md keeps the | |
| # CRT dynamic so the plugin matches the host) so the .dll has no | |
| # external glew32.dll runtime dependency. --- | |
| - name: Configure (Windows) | |
| if: runner.os == 'Windows' | |
| run: > | |
| cmake -B build -A x64 | |
| -DCMAKE_BUILD_TYPE=Release | |
| -DVCPKG_TARGET_TRIPLET=x64-windows-static-md | |
| -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" | |
| - name: Build | |
| run: cmake --build build --config Release --parallel | |
| - name: Derive version | |
| id: ver | |
| shell: bash | |
| run: | | |
| if [[ "$GITHUB_REF" == refs/tags/v* ]]; then | |
| echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "version=0.0.0-dev.${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Collect artifact | |
| shell: bash | |
| run: | | |
| mkdir -p out | |
| if [ "${{ runner.os }}" = "macOS" ]; then | |
| cp -R "build/OldCathode.bundle" "out/OldCathode.bundle" | |
| # Sanity-check the bundle really is universal and exports plugMain. | |
| lipo -archs "out/OldCathode.bundle/Contents/MacOS/OldCathode" | |
| # Actions runs `shell: bash` as `bash -eo pipefail`, so `cmd | grep -q X` | |
| # FAILS when grep FINDS its match: grep exits at once, the writer takes | |
| # SIGPIPE, and pipefail reports the pipeline as failed -- failing the | |
| # release on a correct binary. It is output-size dependent, so it bites | |
| # the biggest artefact first. Captured and matched with `case` instead. | |
| __symbols=$( nm -gU "out/OldCathode.bundle/Contents/MacOS/OldCathode" ) | |
| case "$__symbols" in | |
| *_plugMain*) ;; | |
| *) echo "expected _plugMain, got: $__symbols"; exit 1 ;; | |
| esac | |
| else | |
| cp "build/Release/OldCathode.dll" "out/OldCathode.dll" | |
| fi | |
| # OpenFX build: shipped as its own zip, not folded into the FFGL | |
| # artefact — it installs to the OFX folder, not Resolume's. | |
| mkdir -p ofxout | |
| if [ "${{ runner.os }}" = "macOS" ]; then | |
| cp -R "build/OldCathode.ofx.bundle" "ofxout/OldCathode.ofx.bundle" | |
| lipo -archs "ofxout/OldCathode.ofx.bundle/Contents/MacOS/OldCathode.ofx" | |
| nm -gU "ofxout/OldCathode.ofx.bundle/Contents/MacOS/OldCathode.ofx" | grep "_OfxGetPlugin" >/dev/null | |
| codesign --force --sign - --timestamp=none "ofxout/OldCathode.ofx.bundle" | |
| else | |
| mkdir -p "ofxout/OldCathode.ofx.bundle/Contents/Win64" | |
| cp "build/OldCathode.ofx.bundle/Contents/Win64/OldCathode.ofx" "ofxout/OldCathode.ofx.bundle/Contents/Win64/OldCathode.ofx" | |
| fi | |
| ls -R ofxout | |
| ls -R out | |
| - name: Archive (zip) | |
| shell: bash | |
| run: | | |
| cd out | |
| if command -v 7z >/dev/null 2>&1; then | |
| 7z a "../old-cathode-${{ matrix.name }}.zip" ./* | |
| else | |
| zip -r "../old-cathode-${{ matrix.name }}.zip" ./* | |
| fi | |
| cd ../ofxout | |
| if command -v 7z >/dev/null 2>&1; then | |
| 7z a "../old-cathode-ofx-${{ matrix.name }}.zip" ./* | |
| else | |
| zip -r "../old-cathode-ofx-${{ matrix.name }}.zip" ./* | |
| fi | |
| - name: Disk image (macOS) | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| # A .dmg, but deliberately NOT a .pkg. An FFGL plugin's destination is | |
| # per-user and edition-specific — the README installs to | |
| # ~/Documents/Resolume {Arena,Avenue}/Extra Effects — so a .pkg would | |
| # have to guess a path and would silently install to the wrong one for | |
| # half of users. Mounting an image and dragging the bundle where the | |
| # README says is the honest shape here. | |
| run: | | |
| source scripts/release-lib.sh | |
| rl_init "Old Cathode" old-cathode "${{ steps.ver.outputs.version }}" \ | |
| com.stoatworks.old-cathode "$GITHUB_WORKSPACE" | |
| cp README.md out/ 2>/dev/null || true | |
| rl_adhoc_sign "out/OldCathode.bundle" | |
| rl_dmg "${{ matrix.name }}" out | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: old-cathode-${{ matrix.name }} | |
| path: | | |
| *.zip | |
| *.dmg | |
| if-no-files-found: error | |
| windows-installer: | |
| name: Windows installer | |
| needs: build | |
| runs-on: ubuntu-latest | |
| # makensis cross-compiles, so the installer is wrapped here from the zip the | |
| # Windows job uploaded. Its directory page defaults to the common FFGL | |
| # folder but stays editable, because Resolume reads plugins from a path the | |
| # user chooses per edition. | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Install NSIS | |
| run: sudo apt-get update && sudo apt-get install -y nsis | |
| - name: Build installer | |
| shell: bash | |
| run: | | |
| source scripts/release-lib.sh | |
| version="${GITHUB_REF_NAME#v}" | |
| case "$version" in [0-9]*) ;; *) version="0.0.0" ;; esac | |
| rl_init "Old Cathode" old-cathode "$version" com.stoatworks.old-cathode "$PWD/installers" | |
| zip="$(find artifacts -name "old-cathode-windows-x86_64.zip" | head -1)" | |
| if [ -z "$zip" ]; then echo "no Windows artefact, nothing to do"; exit 0; fi | |
| stage="$RUNNER_TEMP/stage-win" | |
| rm -rf "$stage"; mkdir -p "$stage" | |
| unzip -q "$zip" -d "$stage" | |
| cp README.md "$stage/" 2>/dev/null || true | |
| # --cli: a plugin has no executable to make a shortcut to. | |
| rl_nsis windows-x86_64 "$stage" --cli | |
| ls -l installers | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: old-cathode-windows-installer | |
| path: installers/* | |
| if-no-files-found: warn | |
| linux-ofx: | |
| name: linux-x86_64 (OpenFX) | |
| runs-on: ubuntu-latest | |
| # AlmaLinux 8 rather than the ubuntu image, for glibc 2.28 against 2.39. | |
| # Resolve's supported Linux is Rocky 8.6, and a plugin linked against a | |
| # newer glibc is refused by the loader on the distro Blackmagic actually | |
| # ships for -- while anything newer than Rocky 8 loads a 2.28 build | |
| # perfectly well. This is the widest target, not a concession. | |
| # | |
| # There is no FFGL half to this job. Resolume has no Linux build, the FFGL | |
| # SDK has no Linux path, and the OpenFX plugin is a CPU render that never | |
| # makes a GL call. | |
| container: almalinux:8 | |
| steps: | |
| # Ahead of checkout on purpose: actions/checkout shells out to git and | |
| # the base image has none. gcc-toolset-13 because the stock gcc 8.5 | |
| # predates parts of the C++17 library this code uses. | |
| - name: Toolchain | |
| run: | | |
| dnf -y install git cmake zip binutils gcc-toolset-13 | |
| source /opt/rh/gcc-toolset-13/enable | |
| gcc --version | |
| # No submodules: the FFGL SDK is the only one and this job does not build | |
| # FFGL. The OpenFX SDK subset is vendored in-tree. | |
| - uses: actions/checkout@v7 | |
| # OLDCATHODE_BUILD_FFGL=OFF is what makes the job possible at all. With it | |
| # ON, the SDK's own find_package(GLEW REQUIRED) fails at CONFIGURE time, | |
| # so the build dies before compiling a line of a plugin that needs no GL | |
| # loader. | |
| # | |
| # Static libstdc++/libgcc because gcc-toolset-13's runtime is not | |
| # installed on a stock Rocky or Alma 8 -- a dynamically linked build | |
| # would fail to load on precisely the machines this targets. | |
| - name: Configure | |
| run: | | |
| source /opt/rh/gcc-toolset-13/enable | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DOLDCATHODE_BUILD_FFGL=OFF \ | |
| -DOLDCATHODE_BUILD_TOOLS=OFF \ | |
| -DCMAKE_CXX_FLAGS="-static-libstdc++ -static-libgcc" | |
| - name: Build | |
| run: | | |
| source /opt/rh/gcc-toolset-13/enable | |
| cmake --build build --parallel | |
| - name: Collect artifact | |
| run: | | |
| so="build/OldCathode.ofx.bundle/Contents/Linux-x86-64/OldCathode.ofx" | |
| test -f "$so" | |
| # `nm ... | grep -q` SIGPIPEs under `bash -eo pipefail` when grep | |
| # FINDS its match, failing the release on a correct binary. Captured | |
| # and matched with `case` instead, as the macOS job does. | |
| __syms=$( nm -D --defined-only "$so" ) | |
| case "$__syms" in | |
| *OfxGetPlugin*) ;; | |
| *) echo "expected OfxGetPlugin, got: $__syms"; exit 1 ;; | |
| esac | |
| # The whole reason for the container, asserted rather than assumed: | |
| # the highest glibc symbol version the binary asks for must not be | |
| # newer than Rocky 8's 2.28. Building in the right image is easy to | |
| # get right and impossible to notice getting wrong. | |
| __glibc=$( objdump -T "$so" | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1 ) | |
| echo "highest glibc requirement: $__glibc" | |
| if [ "$( printf '%s\nGLIBC_2.28\n' "$__glibc" | sort -uV | tail -1 )" != "GLIBC_2.28" ]; then | |
| echo "$__glibc is newer than Rocky 8 provides"; exit 1 | |
| fi | |
| readelf -d "$so" | grep NEEDED | |
| mkdir -p "ofxout/OldCathode.ofx.bundle/Contents/Linux-x86-64" | |
| cp "$so" "ofxout/OldCathode.ofx.bundle/Contents/Linux-x86-64/OldCathode.ofx" | |
| ls -R ofxout | |
| - name: Archive (zip) | |
| run: | | |
| cd ofxout | |
| zip -r "../old-cathode-ofx-linux-x86_64.zip" ./* | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: old-cathode-linux-x86_64 | |
| path: "*.zip" | |
| if-no-files-found: error | |
| linux-load: | |
| name: linux-x86_64 (load test) | |
| needs: linux-ofx | |
| runs-on: ubuntu-latest | |
| # Rocky 8 and not the AlmaLinux 8 that built it: the claim being tested is | |
| # "this loads on the distro Resolve supports", and testing it in the image | |
| # that produced the binary would only prove the image agrees with itself. | |
| # | |
| # This is a LOAD test, not a render test. It dlopens the shipped .ofx and | |
| # calls the two entry points an OFX host calls first, which runs the OFX | |
| # Support library's static initialisers and the factory construction in | |
| # getPluginIDs -- the parts that actually fail when an ABI or a glibc floor | |
| # is wrong. What it does not do is render a frame or drive a real host; | |
| # Resolve is x86_64-only and needs a GPU, so nothing here stands in for | |
| # having run the plugin in Resolve. | |
| container: rockylinux:8 | |
| steps: | |
| - name: Toolchain | |
| run: dnf -y install gcc unzip findutils bash | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: old-cathode-linux-x86_64 | |
| path: art | |
| - name: Load the plugin and ask it what it is | |
| shell: bash | |
| run: | | |
| unzip -q art/*.zip -d unz | |
| so="$( find unz -name '*.ofx' | head -1 )" | |
| test -n "$so" | |
| echo "testing $so" | |
| ldd "$so" || true | |
| cat > probe.c <<'EOF' | |
| #include <dlfcn.h> | |
| #include <stdio.h> | |
| /* The head of OfxPlugin from ofxCore.h, which is all this needs. */ | |
| typedef struct { | |
| const char *pluginApi; | |
| int apiVersion; | |
| const char *pluginIdentifier; | |
| unsigned int pluginVersionMajor; | |
| unsigned int pluginVersionMinor; | |
| void *setHost; | |
| void *mainEntry; | |
| } OfxPluginHead; | |
| int main( int argc, char **argv ) | |
| { | |
| if( argc < 2 ) { fprintf( stderr, "usage: probe <plugin.ofx>\n" ); return 2; } | |
| void *h = dlopen( argv[ 1 ], RTLD_NOW | RTLD_LOCAL ); | |
| if( !h ) { fprintf( stderr, "dlopen: %s\n", dlerror() ); return 1; } | |
| int ( *count )( void ) = dlsym( h, "OfxGetNumberOfPlugins" ); | |
| OfxPluginHead *( *get )( int ) = dlsym( h, "OfxGetPlugin" ); | |
| if( !count || !get ) { fprintf( stderr, "entry points missing\n" ); return 1; } | |
| int n = count(); | |
| printf( "OfxGetNumberOfPlugins -> %d\n", n ); | |
| if( n < 1 ) { fprintf( stderr, "no plugins in the bundle\n" ); return 1; } | |
| for( int i = 0; i < n; ++i ) { | |
| OfxPluginHead *p = get( i ); | |
| if( !p ) { fprintf( stderr, "OfxGetPlugin(%d) is NULL\n", i ); return 1; } | |
| printf( " [%d] %s api=%s v%u.%u\n", i, p->pluginIdentifier, | |
| p->pluginApi, p->pluginVersionMajor, p->pluginVersionMinor ); | |
| } | |
| return 0; | |
| } | |
| EOF | |
| gcc -O0 -o probe probe.c -ldl | |
| ./probe "$so" | |
| release: | |
| name: Publish release | |
| needs: [build, linux-ofx, linux-load, windows-installer] | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - uses: softprops/action-gh-release@v3 | |
| with: | |
| files: artifacts/**/* | |
| generate_release_notes: true |