mirror of
https://github.com/projectM-visualizer/projectm.git
synced 2026-03-31 03:33:39 +00:00
Add framework CI jobs and harden test script
New CI job matrix (build-framework): - Tests framework builds on both arm64 and x86_64 - Tests with and without C++ interface - Runs strict validation after build AND after install - Verifies installed frameworks match build output Test script improvements: - Exhaustive header lists (all C API + all C++ headers) - Exact header count validation (catches stale/unexpected files) - Strict mode (STRICT=1) where SKIPs become FAILs - Info.plist CFBundleExecutable validation - Symlink target verification (Current, Headers, Resources) - Mach-O dylib binary type check - dylib install name validation - Flexible framework search across build tree and install prefix - Test pass counter in summary
This commit is contained in:
72
.github/workflows/build_osx.yml
vendored
72
.github/workflows/build_osx.yml
vendored
@ -89,7 +89,7 @@ jobs:
|
||||
-B "${{ github.workspace }}/cmake-build-cxx-api" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="${{ matrix.arch }}" \
|
||||
-DCMAKE_PREFIX_PATH="${{ github.workspace }}/install"
|
||||
|
||||
|
||||
cmake --build "${{ github.workspace }}/cmake-build-cxx-api" --config "Debug"
|
||||
cmake --build "${{ github.workspace }}/cmake-build-cxx-api" --config "Release"
|
||||
|
||||
@ -98,3 +98,73 @@ jobs:
|
||||
with:
|
||||
name: projectm-osx-${{ matrix.libs }}-${{ matrix.fslib }}-${{ matrix.arch }}-${{ matrix.runs-on }}
|
||||
path: install/*
|
||||
|
||||
build-framework:
|
||||
name: "Framework: ${{ matrix.cxx_interface && 'C + C++' || 'C only' }}, Arch: ${{ matrix.arch }}, Build OS: ${{ matrix.runs-on }}"
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: ['arm64', 'x86_64']
|
||||
cxx_interface: [true, false]
|
||||
runs-on: ['macos-15', 'macos-15-intel']
|
||||
exclude:
|
||||
- arch: arm64
|
||||
runs-on: macos-15-intel
|
||||
- arch: x86_64
|
||||
runs-on: macos-15
|
||||
|
||||
steps:
|
||||
- name: Install Packages
|
||||
run: brew install ninja
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Configure Framework Build
|
||||
run: |
|
||||
if [ "${{ matrix.cxx_interface }}" == "true" ]; then
|
||||
cxx_iface=ON
|
||||
else
|
||||
cxx_iface=OFF
|
||||
fi
|
||||
cmake -G "Ninja Multi-Config" \
|
||||
-S "${{ github.workspace }}" \
|
||||
-B "${{ github.workspace }}/cmake-build" \
|
||||
-DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install" \
|
||||
-DCMAKE_VERBOSE_MAKEFILE=YES \
|
||||
-DBUILD_SHARED_LIBS=ON \
|
||||
-DENABLE_MACOS_FRAMEWORK=ON \
|
||||
-DENABLE_CXX_INTERFACE="${cxx_iface}" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="${{ matrix.arch }}"
|
||||
|
||||
- name: Build Release
|
||||
run: cmake --build "${{ github.workspace }}/cmake-build" --config "Release" --parallel
|
||||
|
||||
- name: Validate Frameworks
|
||||
run: |
|
||||
STRICT=1 bash "${{ github.workspace }}/scripts/test-macos-framework.sh" \
|
||||
"${{ github.workspace }}/cmake-build"
|
||||
|
||||
- name: Install
|
||||
run: cmake --build "${{ github.workspace }}/cmake-build" --config "Release" --target install
|
||||
|
||||
- name: Verify Installed Frameworks
|
||||
run: |
|
||||
echo "--- Checking installed framework structure ---"
|
||||
ls -la "${{ github.workspace }}/install/lib/"
|
||||
# Verify frameworks were installed (not bare dylibs)
|
||||
test -d "${{ github.workspace }}/install/lib/projectM-4.framework" \
|
||||
|| { echo "FAIL: projectM-4.framework not installed"; exit 1; }
|
||||
test -d "${{ github.workspace }}/install/lib/projectM-4-playlist.framework" \
|
||||
|| { echo "FAIL: projectM-4-playlist.framework not installed"; exit 1; }
|
||||
# Run the same validation on installed frameworks
|
||||
STRICT=1 bash "${{ github.workspace }}/scripts/test-macos-framework.sh" \
|
||||
"${{ github.workspace }}/install"
|
||||
|
||||
- name: Upload Framework Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: projectm-osx-framework-${{ matrix.cxx_interface && 'cxx' || 'c-only' }}-${{ matrix.arch }}-${{ matrix.runs-on }}
|
||||
path: install/*
|
||||
|
||||
@ -2,14 +2,25 @@
|
||||
# test-macos-framework.sh
|
||||
# CI test script for validating macOS framework builds
|
||||
#
|
||||
# Usage: ./scripts/test-macos-framework.sh <build-dir>
|
||||
# Usage: ./scripts/test-macos-framework.sh <dir> [<dir>...]
|
||||
#
|
||||
# Searches the given directories for projectM-4.framework and
|
||||
# projectM-4-playlist.framework. Works with both build trees
|
||||
# (where frameworks are in src/libprojectM/ and src/playlist/)
|
||||
# and install prefixes (where frameworks are in lib/).
|
||||
#
|
||||
# Environment:
|
||||
# STRICT=1 (default) SKIPs become FAILs - use for CI
|
||||
# STRICT=0 SKIPs are warnings - use for local testing
|
||||
#
|
||||
# This script validates:
|
||||
# 1. Framework directory structure
|
||||
# 2. Header completeness
|
||||
# 3. Linkability (compile and link a test program)
|
||||
# 1. Framework directory structure (symlinks, directories, Info.plist)
|
||||
# 2. Header completeness (every expected header must be present)
|
||||
# 3. No unexpected headers (catch stale or misplaced files)
|
||||
# 4. Linkability (compile and link a test program against framework)
|
||||
# 5. dylib identity (install name matches framework convention)
|
||||
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
# Cleanup temp files on exit
|
||||
TEMP_FILES=()
|
||||
@ -20,19 +31,46 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
BUILD_DIR="${1:-.}"
|
||||
SEARCH_DIRS=("${@:-.}")
|
||||
STRICT="${STRICT:-1}" # In strict mode, SKIPs become FAILs
|
||||
|
||||
# Find a framework by name in the search directories
|
||||
find_framework() {
|
||||
local name="$1"
|
||||
for dir in "${SEARCH_DIRS[@]}"; do
|
||||
# Check common locations
|
||||
for candidate in \
|
||||
"$dir/$name.framework" \
|
||||
"$dir/src/libprojectM/$name.framework" \
|
||||
"$dir/src/playlist/$name.framework" \
|
||||
"$dir/lib/$name.framework"; do
|
||||
if [ -d "$candidate" ]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "=== macOS Framework CI Tests ==="
|
||||
echo "Build directory: $BUILD_DIR"
|
||||
echo "Search directories: ${SEARCH_DIRS[*]}"
|
||||
echo "Strict mode: $STRICT"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
|
||||
pass() {
|
||||
echo -e "${GREEN}PASS${NC}: $1"
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
@ -40,6 +78,14 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
skip() {
|
||||
if [ "$STRICT" = "1" ]; then
|
||||
fail "SKIP not allowed in strict mode: $1"
|
||||
else
|
||||
echo -e "${YELLOW}SKIP${NC}: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 1: Framework Structure Validation
|
||||
test_structure() {
|
||||
local framework_path="$1"
|
||||
@ -55,20 +101,41 @@ test_structure() {
|
||||
[ -d "$framework_path/Versions/A" ] || fail "Missing Versions/A directory"
|
||||
[ -L "$framework_path/Versions/Current" ] || fail "Missing Versions/Current symlink"
|
||||
|
||||
# Verify Current symlink target
|
||||
local current_target
|
||||
current_target=$(readlink "$framework_path/Versions/Current")
|
||||
[ "$current_target" = "A" ] || fail "Versions/Current symlink points to '$current_target', expected 'A'"
|
||||
|
||||
# Check Version A contents
|
||||
[ -f "$framework_path/Versions/A/$framework_name" ] || fail "Missing binary: Versions/A/$framework_name"
|
||||
[ -d "$framework_path/Versions/A/Headers" ] || fail "Missing Headers directory"
|
||||
[ -d "$framework_path/Versions/A/Resources" ] || fail "Missing Resources directory"
|
||||
[ -f "$framework_path/Versions/A/Resources/Info.plist" ] || fail "Missing Info.plist"
|
||||
|
||||
# Check top-level symlinks
|
||||
# Validate Info.plist has correct bundle executable name
|
||||
local bundle_exec
|
||||
bundle_exec=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$framework_path/Versions/A/Resources/Info.plist" 2>/dev/null || true)
|
||||
[ "$bundle_exec" = "$framework_name" ] || fail "Info.plist CFBundleExecutable is '$bundle_exec', expected '$framework_name'"
|
||||
|
||||
# Check top-level symlinks exist and are symlinks (not copies)
|
||||
[ -L "$framework_path/$framework_name" ] || fail "Missing top-level binary symlink"
|
||||
[ -L "$framework_path/Headers" ] || fail "Missing top-level Headers symlink"
|
||||
[ -L "$framework_path/Resources" ] || fail "Missing top-level Resources symlink"
|
||||
|
||||
# Verify symlinks point to correct locations
|
||||
local binary_target=$(readlink "$framework_path/$framework_name")
|
||||
[ "$binary_target" = "Versions/Current/$framework_name" ] || fail "Binary symlink points to wrong location: $binary_target"
|
||||
local binary_target headers_target resources_target
|
||||
binary_target=$(readlink "$framework_path/$framework_name")
|
||||
headers_target=$(readlink "$framework_path/Headers")
|
||||
resources_target=$(readlink "$framework_path/Resources")
|
||||
[ "$binary_target" = "Versions/Current/$framework_name" ] || fail "Binary symlink points to '$binary_target', expected 'Versions/Current/$framework_name'"
|
||||
[ "$headers_target" = "Versions/Current/Headers" ] || fail "Headers symlink points to '$headers_target', expected 'Versions/Current/Headers'"
|
||||
[ "$resources_target" = "Versions/Current/Resources" ] || fail "Resources symlink points to '$resources_target', expected 'Versions/Current/Resources'"
|
||||
|
||||
# Verify the binary is a Mach-O dylib
|
||||
local file_type
|
||||
file_type=$(file -b "$framework_path/Versions/A/$framework_name")
|
||||
echo "$file_type" | grep -q "Mach-O" || fail "Binary is not Mach-O: $file_type"
|
||||
echo "$file_type" | grep -qi "dynamically linked\|dynamic library" || fail "Binary is not a dynamic library: $file_type"
|
||||
|
||||
pass "Structure validation for $framework_name.framework"
|
||||
}
|
||||
@ -88,15 +155,25 @@ test_headers() {
|
||||
local headers_dir="$framework_path/Headers/$header_subdir"
|
||||
[ -d "$headers_dir" ] || fail "Headers directory does not exist: $headers_dir"
|
||||
|
||||
# Check each expected header is present
|
||||
for header in "${expected_headers[@]}"; do
|
||||
[ -f "$headers_dir/$header" ] || fail "Missing header: $header"
|
||||
[ -f "$headers_dir/$header" ] || fail "Missing expected header: $header_subdir/$header"
|
||||
done
|
||||
|
||||
# Count total headers found
|
||||
local header_count=$(find "$headers_dir" -name "*.h" -o -name "*.hpp" | wc -l | tr -d ' ')
|
||||
echo " Found $header_count header files"
|
||||
# Check for unexpected headers (catches stale files or wrong headers being copied)
|
||||
local actual_count expected_count
|
||||
actual_count=$(find "$headers_dir" -name "*.h" -o -name "*.hpp" | wc -l | tr -d ' ')
|
||||
expected_count=${#expected_headers[@]}
|
||||
echo " Found $actual_count header files (expected $expected_count)"
|
||||
if [ "$actual_count" -ne "$expected_count" ]; then
|
||||
echo " Expected headers:"
|
||||
for h in "${expected_headers[@]}"; do echo " $h"; done
|
||||
echo " Actual headers:"
|
||||
find "$headers_dir" \( -name "*.h" -o -name "*.hpp" \) -exec basename {} \; | sort | while read -r h; do echo " $h"; done
|
||||
fail "Header count mismatch: found $actual_count, expected $expected_count"
|
||||
fi
|
||||
|
||||
pass "Header validation for $framework_name.framework"
|
||||
pass "Header validation for $framework_name.framework ($actual_count headers)"
|
||||
}
|
||||
|
||||
# Test 3: Linkability Test
|
||||
@ -107,12 +184,14 @@ test_linkability() {
|
||||
|
||||
echo "--- Testing linkability: $framework_name.framework ---"
|
||||
|
||||
local framework_parent=$(dirname "$framework_path")
|
||||
local framework_parent
|
||||
framework_parent=$(dirname "$framework_path")
|
||||
# Headers are in Headers/<framework-name>/ to support include patterns like
|
||||
# #include <projectM-4/projectM.h>, so we add -I Framework.framework/Headers
|
||||
local include_path="$framework_path/Headers"
|
||||
local test_file=$(mktemp -t framework_test).c
|
||||
local test_output=$(mktemp -t framework_test)
|
||||
local test_file test_output
|
||||
test_file=$(mktemp -t framework_test).c
|
||||
test_output=$(mktemp -t framework_test)
|
||||
TEMP_FILES+=("$test_file" "$test_output")
|
||||
|
||||
echo "$test_code" > "$test_file"
|
||||
@ -126,26 +205,74 @@ test_linkability() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 4: dylib install name check
|
||||
test_install_name() {
|
||||
local framework_path="$1"
|
||||
local framework_name="$2"
|
||||
|
||||
echo "--- Testing install name: $framework_name.framework ---"
|
||||
|
||||
local binary="$framework_path/Versions/A/$framework_name"
|
||||
local install_name
|
||||
install_name=$(otool -D "$binary" 2>/dev/null | tail -1)
|
||||
|
||||
# For a framework, the install name should contain the framework name
|
||||
if echo "$install_name" | grep -q "$framework_name"; then
|
||||
echo " Install name: $install_name"
|
||||
pass "Install name check for $framework_name.framework"
|
||||
else
|
||||
echo " Install name: $install_name"
|
||||
fail "Install name does not reference framework name '$framework_name'"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main tests
|
||||
# ============================================================================
|
||||
|
||||
# projectM-4.framework
|
||||
PROJECTM_FRAMEWORK="$BUILD_DIR/src/libprojectM/projectM-4.framework"
|
||||
if [ -d "$PROJECTM_FRAMEWORK" ]; then
|
||||
PROJECTM_FRAMEWORK=$(find_framework "projectM-4" || true)
|
||||
if [ -n "$PROJECTM_FRAMEWORK" ] && [ -d "$PROJECTM_FRAMEWORK" ]; then
|
||||
echo "Found: $PROJECTM_FRAMEWORK"
|
||||
test_structure "$PROJECTM_FRAMEWORK" "projectM-4"
|
||||
|
||||
# Expected C API headers
|
||||
# All expected C API headers (must be exhaustive)
|
||||
PROJECTM_HEADERS=(
|
||||
"projectM.h"
|
||||
"audio.h"
|
||||
"callbacks.h"
|
||||
"core.h"
|
||||
"types.h"
|
||||
"debug.h"
|
||||
"logging.h"
|
||||
"memory.h"
|
||||
"parameters.h"
|
||||
"render_opengl.h"
|
||||
"touch.h"
|
||||
"types.h"
|
||||
"user_sprites.h"
|
||||
"projectM_export.h"
|
||||
"version.h"
|
||||
)
|
||||
# C++ headers are present when built with ENABLE_CXX_INTERFACE
|
||||
# Check if any C++ headers exist to determine if we should validate them
|
||||
if [ -d "$PROJECTM_FRAMEWORK/Headers/projectM-4/Audio" ]; then
|
||||
PROJECTM_HEADERS+=(
|
||||
"projectM_cxx_export.h"
|
||||
"Logging.hpp"
|
||||
"ProjectM.hpp"
|
||||
"Audio/AudioConstants.hpp"
|
||||
"Audio/FrameAudioData.hpp"
|
||||
"Audio/Loudness.hpp"
|
||||
"Audio/MilkdropFFT.hpp"
|
||||
"Audio/PCM.hpp"
|
||||
"Audio/WaveformAligner.hpp"
|
||||
"Renderer/RenderContext.hpp"
|
||||
"Renderer/TextureTypes.hpp"
|
||||
)
|
||||
fi
|
||||
test_headers "$PROJECTM_FRAMEWORK" "projectM-4" "projectM-4" "${PROJECTM_HEADERS[@]}"
|
||||
|
||||
# Linkability test - use the full include path to match existing header patterns
|
||||
# Linkability test with the C API
|
||||
PROJECTM_TEST_CODE='
|
||||
#include <projectM-4/projectM.h>
|
||||
int main() {
|
||||
@ -155,40 +282,44 @@ int main() {
|
||||
}
|
||||
'
|
||||
test_linkability "$PROJECTM_FRAMEWORK" "projectM-4" "$PROJECTM_TEST_CODE"
|
||||
test_install_name "$PROJECTM_FRAMEWORK" "projectM-4"
|
||||
else
|
||||
echo "SKIP: projectM-4.framework not found at $PROJECTM_FRAMEWORK"
|
||||
skip "projectM-4.framework not found in search directories"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# projectM-4-playlist.framework
|
||||
PLAYLIST_FRAMEWORK="$BUILD_DIR/src/playlist/projectM-4-playlist.framework"
|
||||
if [ -d "$PLAYLIST_FRAMEWORK" ]; then
|
||||
PLAYLIST_FRAMEWORK=$(find_framework "projectM-4-playlist" || true)
|
||||
if [ -n "$PLAYLIST_FRAMEWORK" ] && [ -d "$PLAYLIST_FRAMEWORK" ]; then
|
||||
echo "Found: $PLAYLIST_FRAMEWORK"
|
||||
test_structure "$PLAYLIST_FRAMEWORK" "projectM-4-playlist"
|
||||
|
||||
# Expected headers (in projectM-4/ subdir to match include pattern)
|
||||
# All expected playlist headers (must be exhaustive)
|
||||
PLAYLIST_HEADERS=(
|
||||
"playlist.h"
|
||||
"playlist_callbacks.h"
|
||||
"playlist_core.h"
|
||||
"playlist_filter.h"
|
||||
"playlist_items.h"
|
||||
"playlist_memory.h"
|
||||
"playlist_playback.h"
|
||||
"playlist_types.h"
|
||||
"projectM_playlist_export.h"
|
||||
)
|
||||
test_headers "$PLAYLIST_FRAMEWORK" "projectM-4-playlist" "projectM-4" "${PLAYLIST_HEADERS[@]}"
|
||||
|
||||
# Linkability test (needs projectM framework too)
|
||||
# Playlist headers use projectM-4/ prefix to match the main library
|
||||
PLAYLIST_TEST_CODE='
|
||||
#include <projectM-4/playlist.h>
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
||||
'
|
||||
# For playlist, we need both frameworks
|
||||
if [ -d "$PROJECTM_FRAMEWORK" ]; then
|
||||
echo "--- Testing linkability: projectM-4-playlist.framework ---"
|
||||
framework_parent=$(dirname "$PLAYLIST_FRAMEWORK")
|
||||
projectm_parent=$(dirname "$PROJECTM_FRAMEWORK")
|
||||
# Include paths for both frameworks
|
||||
include_path="$PLAYLIST_FRAMEWORK/Headers"
|
||||
projectm_include_path="$PROJECTM_FRAMEWORK/Headers"
|
||||
test_file=$(mktemp -t framework_test).c
|
||||
@ -206,10 +337,14 @@ int main() {
|
||||
echo "$compile_errors"
|
||||
fail "Failed to compile and link against projectM-4-playlist.framework"
|
||||
fi
|
||||
else
|
||||
skip "Cannot test playlist linkability without projectM-4.framework"
|
||||
fi
|
||||
|
||||
test_install_name "$PLAYLIST_FRAMEWORK" "projectM-4-playlist"
|
||||
else
|
||||
echo "SKIP: projectM-4-playlist.framework not found at $PLAYLIST_FRAMEWORK"
|
||||
skip "projectM-4-playlist.framework not found in search directories"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== All framework tests passed ==="
|
||||
echo "=== All framework tests passed ($TESTS_PASSED/$TESTS_RUN) ==="
|
||||
|
||||
Reference in New Issue
Block a user