diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 876012be..b79b8168 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -187,7 +187,7 @@ jobs: sudo apt-get install -y libasound2-dev libfreetype6-dev libx11-dev libxcomposite-dev libxcursor-dev libxinerama-dev libxrandr-dev mesa-common-dev libwebkit2gtk-4.1-dev ladspa-sdk - name: Configure - run: cmake -B build -S tests/pluginval_as_dependency -DJUCE_VERSION="${{ matrix.juce }}" + run: cmake -B build -S tests/pluginval_as_dependency -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE}} -DJUCE_VERSION="${{ matrix.juce }}" - name: Build run: cmake --build build --target pluginval --parallel 4 diff --git a/.gitignore b/.gitignore index d599f895..e2c58172 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ pluginval.build/ pluginval.xcodeproj/ pluginval_artefacts/ +.claude/*local* \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index beb407af..fcec6889 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,44 @@ Follow these guidelines when working on this codebase: 6. **Never speculate about unread code**: Never make claims about code you haven't opened. If the user references a specific file, you MUST read the file before answering. Investigate and read relevant files BEFORE answering questions about the codebase. Give grounded, hallucination-free answers based on actual file contents. +## Getting CI Run Logs + +### Configuration + +- **Organisation:** `` +- **Repository:** `` + +For this project: +- **Organisation:** `Tracktion` +- **Repository:** `pluginval` + +### Setup + +Install the GitHub CLI: +```bash +brew install gh # macOS +# or +sudo apt install gh # Ubuntu/Debian +``` + +Authentication is handled via the `GH_TOKEN` environment variable (already configured). + +### Workflow + +1. **List recent workflow runs:** + ```bash + gh run list -R / + ``` + +2. **Find the most recent run for your branch** from the output above. + +3. **View failed log details:** + ```bash + gh run view -R / --log-failed + ``` + +Replace `` with the ID from step 2. + ## Directory Structure ``` @@ -49,6 +87,9 @@ pluginval/ │ ├── PluginvalLookAndFeel.h # Custom UI styling │ ├── StrictnessInfoPopup.h # Strictness level info UI │ ├── binarydata/ # Binary resources (icons) +│ ├── vst3validator/ # Embedded VST3 validator integration +│ │ ├── VST3ValidatorRunner.h +│ │ └── VST3ValidatorRunner.cpp │ └── tests/ # Individual test implementations │ ├── BasicTests.cpp # Core plugin tests (info, state, audio) │ ├── BusTests.cpp # Audio bus configuration tests @@ -59,7 +100,8 @@ pluginval/ ├── modules/ │ └── juce/ # JUCE framework (git submodule) ├── cmake/ -│ └── CPM.cmake # CMake Package Manager +│ ├── CPM.cmake # CMake Package Manager +│ └── GenerateBinaryHeader.cmake # Binary-to-C-header converter ├── tests/ │ ├── AddPluginvalTests.cmake # CMake module for CTest integration │ ├── test_plugins/ # Test plugin files @@ -101,6 +143,7 @@ cmake --build Builds/Debug --config Debug | Option | Description | Default | |--------|-------------|---------| | `PLUGINVAL_FETCH_JUCE` | Fetch JUCE with pluginval | ON | +| `PLUGINVAL_VST3_VALIDATOR` | Build with embedded VST3 validator | ON | | `WITH_ADDRESS_SANITIZER` | Enable AddressSanitizer | OFF | | `WITH_THREAD_SANITIZER` | Enable ThreadSanitizer | OFF | | `VST2_SDK_DIR` | Path to VST2 SDK (env var) | - | @@ -177,6 +220,26 @@ struct Requirements { | `LocaleTest.cpp` | Locale handling verification | | `ExtremeTests.cpp` | Edge cases, stress tests | +### VST3 Validator Integration + +The VST3 validator (Steinberg's vstvalidator) is embedded into pluginval when built with `PLUGINVAL_VST3_VALIDATOR=ON` (the default). This provides single-file distribution while keeping vstvalidator completely isolated from pluginval's link dependencies. + +**Architecture:** +1. The VST3 SDK is fetched via CPM during CMake configure +2. The SDK's own `validator` target is built as a separate executable +3. A CMake script (`cmake/GenerateBinaryHeader.cmake`) converts the compiled binary into a C byte array header +4. `VST3ValidatorRunner` (`Source/vst3validator/`) extracts the embedded binary to a temp file on first use +5. When the `VST3validator` test runs, it spawns the extracted validator as a subprocess + +**Key files:** +- `cmake/GenerateBinaryHeader.cmake` — binary-to-C-header conversion script +- `Source/vst3validator/VST3ValidatorRunner.h/cpp` — extracts embedded binary, returns `juce::File` + +**Disabling embedded validator:** +```bash +cmake -B Builds -DPLUGINVAL_VST3_VALIDATOR=OFF . +``` + ## Adding New Tests 1. Create a subclass of `PluginTest`: @@ -312,6 +375,7 @@ add_pluginval_tests(MyPluginTarget - **JUCE** (v8.0.x) - Audio application framework (git submodule) - **magic_enum** (v0.9.7) - Enum reflection (fetched via CPM) - **rtcheck** (optional, macOS) - Real-time safety checking (fetched via CPM) +- **VST3 SDK** (v3.7.x) - Steinberg VST3 SDK for embedded validator (fetched via CPM, optional) ### System - macOS: CoreAudio, AudioUnit frameworks diff --git a/CMakeLists.txt b/CMakeLists.txt index 323a98b0..7309aa43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,28 @@ if (pluginval_IS_TOP_LEVEL) CPMAddPackage("gh:juce-framework/juce#8.0.3") endif() + +# VST3 Validator integration - embeds Steinberg's vstvalidator directly into pluginval +option(PLUGINVAL_VST3_VALIDATOR "Build with embedded VST3 validator" ON) + +if(PLUGINVAL_VST3_VALIDATOR) + # Use static CRT on Windows to match pluginval's settings + set(SMTG_USE_STATIC_CRT ON CACHE BOOL "" FORCE) + + CPMAddPackage( + NAME vst3sdk + GITHUB_REPOSITORY steinbergmedia/vst3sdk + GIT_TAG v3.7.14_build_55 + OPTIONS + "SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF" + "SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF" + "SMTG_ENABLE_VSTGUI_SUPPORT OFF" + "SMTG_ADD_VSTGUI OFF" + "SMTG_RUN_VST_VALIDATOR OFF" + "SMTG_CREATE_BUNDLE_FOR_WINDOWS OFF" + ) +endif() + if (DEFINED ENV{VST2_SDK_DIR}) MESSAGE(STATUS "Building with VST2 SDK: $ENV{VST2_SDK_DIR}") juce_set_vst2_sdk_path($ENV{VST2_SDK_DIR}) @@ -109,6 +131,16 @@ set(SourceFiles target_sources(pluginval PRIVATE ${SourceFiles}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/Source PREFIX Source FILES ${SourceFiles}) +# Add VST3 validator runner sources to pluginval (extracts embedded binary at runtime) +if(PLUGINVAL_VST3_VALIDATOR) + set(VST3ValidatorFiles + Source/vst3validator/VST3ValidatorRunner.h + Source/vst3validator/VST3ValidatorRunner.cpp + ) + target_sources(pluginval PRIVATE ${VST3ValidatorFiles}) + source_group("Source/vst3validator" FILES ${VST3ValidatorFiles}) +endif() + if (DEFINED ENV{VST2_SDK_DIR}) target_compile_definitions(pluginval PRIVATE JUCE_PLUGINHOST_VST=1) @@ -124,8 +156,10 @@ target_compile_definitions(pluginval PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1 JUCE_GUI_BASICS_INCLUDE_XHEADERS=1 $<$:PLUGINVAL_ENABLE_RTCHECK=1> + $<$:PLUGINVAL_VST3_VALIDATOR=1> VERSION="${CURRENT_VERSION}") + if(MSVC AND NOT CMAKE_MSVC_RUNTIME_LIBRARY) # Default to statically linking the runtime libraries set_property(TARGET pluginval PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") @@ -143,6 +177,22 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") -static-libstdc++) endif() +# Embed the SDK's validator binary into pluginval +if(PLUGINVAL_VST3_VALIDATOR) + set(VSTVALIDATOR_HEADER "${CMAKE_BINARY_DIR}/generated/vstvalidator_data.h") + add_custom_command( + OUTPUT "${VSTVALIDATOR_HEADER}" + COMMAND ${CMAKE_COMMAND} + -DINPUT_FILE=$ + -DOUTPUT_FILE=${VSTVALIDATOR_HEADER} + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateBinaryHeader.cmake + DEPENDS validator + COMMENT "Embedding vstvalidator binary into header...") + + target_sources(pluginval PRIVATE "${VSTVALIDATOR_HEADER}") + target_include_directories(pluginval PRIVATE "${CMAKE_BINARY_DIR}/generated") +endif() + if (PLUGINVAL_ENABLE_RTCHECK) target_link_libraries(pluginval PRIVATE rtcheck) diff --git a/Source/CommandLine.cpp b/Source/CommandLine.cpp index 426c45c0..f5cd7033 100644 --- a/Source/CommandLine.cpp +++ b/Source/CommandLine.cpp @@ -290,7 +290,6 @@ static Option possibleOptions[] = { "--randomise", false }, { "--sample-rates", true }, { "--block-sizes", true }, - { "--vst3validator", true }, { "--rtcheck", false }, }; @@ -384,8 +383,6 @@ static juce::String getHelpMessage() --disabled-tests [pathToFile] If specified, sets a path to a file that should have the names of disabled tests on each row. - --vst3validator [pathToValidator] - If specified, this will run the VST3 validator as part of the test process. --output-dir [pathToDir] If specified, sets a directory to store the log files. This can be useful @@ -599,7 +596,6 @@ std::pair parseCommandLine (const juce::Argu options.disabledTests = getDisabledTests (args); options.sampleRates = getSampleRates (args); options.blockSizes = getBlockSizes (args); - options.vst3Validator = getOptionValue (args, "--vst3validator", "", "Expected a path for the --vst3validator option"); options.realtimeCheck = magic_enum::enum_cast (getOptionValue (args, "--rtcheck", "", "Expected one of [disabled, enabled, relaxed]").toString().toStdString()) .value_or (RealtimeCheck::disabled); @@ -669,9 +665,6 @@ juce::StringArray createCommandLine (juce::String fileOrID, PluginTests::Options args.addArray ({ "--block-sizes", blockSizes.joinIntoString (",") }); } - if (options.vst3Validator != juce::File()) - args.addArray ({ "--vst3validator", options.vst3Validator.getFullPathName().quoted() }); - if (auto rtCheckMode = options.realtimeCheck; rtCheckMode != RealtimeCheck::disabled) { diff --git a/Source/Main.cpp b/Source/Main.cpp index 99a8e44f..05f5c2a2 100644 --- a/Source/Main.cpp +++ b/Source/Main.cpp @@ -18,6 +18,7 @@ #include "CommandLine.h" #include "PluginvalLookAndFeel.h" + //============================================================================== class PluginValidatorApplication : public juce::JUCEApplication, private juce::AsyncUpdater @@ -174,7 +175,6 @@ class PluginValidatorApplication : public juce::JUCEApplication, }; //============================================================================== -// This macro generates the main() routine that launches the app. START_JUCE_APPLICATION (PluginValidatorApplication) juce::PropertiesFile& getAppPreferences() diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 1547ac8e..f2e75377 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -108,16 +108,6 @@ namespace return { 64, 128, 256, 512, 1024 }; } - void setVST3Validator (juce::File f) - { - getAppPreferences().setValue ("vst3validator", f.getFullPathName()); - } - - juce::File getVST3Validator() - { - return getAppPreferences().getValue ("vst3validator", juce::String()); - } - void setRealtimeCheckMode (RealtimeCheck rt) { getAppPreferences().setValue ("realtimeCheckMode", juce::String (std::string (magic_enum::enum_name (rt)))); @@ -210,7 +200,6 @@ namespace options.outputDir = getOutputDir(); options.sampleRates = getSampleRates(); options.blockSizes = getBlockSizes(); - options.vst3Validator = getVST3Validator(); options.realtimeCheck = getRealtimeCheckMode(); return options; @@ -296,34 +285,6 @@ namespace } })); } - - void showVST3ValidatorDialog() - { - juce::String message = TRANS("Set the location of the VST3 validator app"); - auto app = getVST3Validator(); - - if (app.getFullPathName().isNotEmpty()) - message << "\n\n" << app.getFullPathName().quoted(); - else - message << "\n\n" << "\"None set\""; - - std::shared_ptr aw (juce::LookAndFeel::getDefaultLookAndFeel().createAlertWindow (TRANS("Set VST3 validator"), message, - TRANS("Choose"), TRANS("Don't use VST3 validator"), TRANS("Cancel"), - juce::AlertWindow::QuestionIcon, 3, nullptr)); - aw->enterModalState (true, juce::ModalCallbackFunction::create ([aw] (int res) - { - if (res == 1) - setVST3Validator ({}); - - if (res == 1) - { - juce::FileChooser fc (TRANS("Choose VST3 validator"), {}); - - if (fc.browseForFileToOpen()) - setVST3Validator (fc.getResult().getFullPathName()); - } - })); - } } //============================================================================== @@ -633,9 +594,6 @@ juce::PopupMenu MainComponent::createOptionsMenu() m.addItem (TRANS("Randomise tests"), true, getRandomiseTests(), [] { setRandomiseTests (! getRandomiseTests()); }); - m.addItem (TRANS("Set VST3 validator location..."), - [] { showVST3ValidatorDialog(); }); - m.addSeparator(); m.addItem (TRANS("Show settings folder"), diff --git a/Source/PluginTests.h b/Source/PluginTests.h index 0b9e9384..560893aa 100644 --- a/Source/PluginTests.h +++ b/Source/PluginTests.h @@ -50,7 +50,6 @@ struct PluginTests : public juce::UnitTest juce::StringArray disabledTests; /**< List of disabled tests. */ std::vector sampleRates; /**< List of sample rates. */ std::vector blockSizes; /**< List of block sizes. */ - juce::File vst3Validator; /**< juce::File to use as the VST3 validator app. */ RealtimeCheck realtimeCheck = RealtimeCheck::disabled; /**< The type of real-time safety checking to perform. */ }; diff --git a/Source/tests/BasicTests.cpp b/Source/tests/BasicTests.cpp index 7d93289a..3e21baae 100644 --- a/Source/tests/BasicTests.cpp +++ b/Source/tests/BasicTests.cpp @@ -16,6 +16,10 @@ #include "../TestUtilities.h" #include "../RTCheck.h" +#if PLUGINVAL_VST3_VALIDATOR + #include "../vst3validator/VST3ValidatorRunner.h" +#endif + #include #include #include @@ -875,6 +879,7 @@ static AUvalTest auvalTest; //============================================================================== /** Runs Steinberg's validator on the plugin if it's a VST3. + Uses the embedded VST3 validator when built with PLUGINVAL_VST3_VALIDATOR. */ struct VST3validator : public PluginTest { @@ -890,24 +895,33 @@ struct VST3validator : public PluginTest if (desc.pluginFormatName != "VST3") return; - auto vst3Validator = ut.getOptions().vst3Validator; + #if ! PLUGINVAL_VST3_VALIDATOR + ut.logMessage ("INFO: Skipping vst3 validator (not built with VST3 validator support)"); + return; + #else + auto tempFile = vst3validator::getValidatorExecutable(); + auto vstvalidatorExe = tempFile->getFile(); - if (vst3Validator == juce::File()) + if (! vstvalidatorExe.existsAsFile()) { - ut.logMessage ("INFO: Skipping vst3 validator as validator path hasn't been set"); + ut.logMessage ("WARNING: Could not extract vstvalidator binary"); return; } - juce::StringArray cmd (vst3Validator.getFullPathName()); + juce::StringArray cmd; + cmd.add (vstvalidatorExe.getFullPathName()); if (ut.getOptions().strictnessLevel > 5) cmd.add ("-e"); + if (! ut.getOptions().verbose) + cmd.add ("-q"); + cmd.add (ut.getFileOrID()); juce::ChildProcess cp; const auto started = cp.start (cmd); - ut.expect (started, "VST3 validator app has been set but is unable to start"); + ut.expect (started, "Failed to start VST3 validator mode"); if (! started) return; @@ -947,15 +961,20 @@ struct VST3validator : public PluginTest if (! exitedCleanly && ! ut.getOptions().verbose) ut.logMessage (outputBuffer.toString()); + #endif } std::vector getDescription (int level) const override { + #if ! PLUGINVAL_VST3_VALIDATOR + return { { name, "Disabled (not built with VST3 validator support)" } }; + #else if (level > 5) return { { name, "Runs Steinberg's vstvalidator with -e flag for extended validation (VST3 only)" } }; return { { name, "Runs Steinberg's vstvalidator on the plugin file (VST3 only)" } }; + #endif } }; -static VST3validator vst3validator; +static VST3validator vst3validatorTest; diff --git a/Source/vst3validator/VST3ValidatorRunner.cpp b/Source/vst3validator/VST3ValidatorRunner.cpp new file mode 100644 index 00000000..c5c0711c --- /dev/null +++ b/Source/vst3validator/VST3ValidatorRunner.cpp @@ -0,0 +1,42 @@ +/*============================================================================== + + Copyright 2018 by Tracktion Corporation. + For more information visit www.tracktion.com + + You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + pluginval IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ==============================================================================*/ + +#include "VST3ValidatorRunner.h" +#include "vstvalidator_data.h" // Generated at build time + +#if ! JUCE_WINDOWS + #include +#endif + +namespace vst3validator { + +std::unique_ptr getValidatorExecutable() +{ + #if JUCE_WINDOWS + auto tempFile = std::make_unique (".exe"); + #else + auto tempFile = std::make_unique (); + #endif + + auto exe = tempFile->getFile(); + exe.replaceWithData (vstvalidator_binary, vstvalidator_binary_len); + + #if ! JUCE_WINDOWS + chmod (exe.getFullPathName().toRawUTF8(), 0755); + #endif + + return tempFile; +} + +} // namespace vst3validator diff --git a/Source/vst3validator/VST3ValidatorRunner.h b/Source/vst3validator/VST3ValidatorRunner.h new file mode 100644 index 00000000..cf5c9bb3 --- /dev/null +++ b/Source/vst3validator/VST3ValidatorRunner.h @@ -0,0 +1,26 @@ +/*============================================================================== + + Copyright 2018 by Tracktion Corporation. + For more information visit www.tracktion.com + + You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + pluginval IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ==============================================================================*/ + +#pragma once + +#include + +namespace vst3validator { + +/** Extracts the embedded vstvalidator binary to a temporary file. + The returned TemporaryFile auto-deletes when destroyed, so the caller + must keep it alive while the child process runs. */ +std::unique_ptr getValidatorExecutable(); + +} // namespace vst3validator diff --git a/cmake/GenerateBinaryHeader.cmake b/cmake/GenerateBinaryHeader.cmake new file mode 100644 index 00000000..b0ecd650 --- /dev/null +++ b/cmake/GenerateBinaryHeader.cmake @@ -0,0 +1,21 @@ +# GenerateBinaryHeader.cmake +# +# Converts a binary file into a C++ header containing a byte array. +# Similar to xxd -i but cross-platform (pure CMake). +# +# Usage: +# cmake -DINPUT_FILE=/path/to/binary -DOUTPUT_FILE=/path/to/output.h -P GenerateBinaryHeader.cmake + +if(NOT DEFINED INPUT_FILE OR NOT DEFINED OUTPUT_FILE) + message(FATAL_ERROR "INPUT_FILE and OUTPUT_FILE must be defined") +endif() + +file(READ "${INPUT_FILE}" BINARY_DATA HEX) +string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," HEX_ARRAY "${BINARY_DATA}") +file(SIZE "${INPUT_FILE}" BINARY_SIZE) + +file(WRITE "${OUTPUT_FILE}" + "#pragma once\n" + "#include \n" + "static const unsigned char vstvalidator_binary[] = {\n${HEX_ARRAY}\n};\n" + "static const size_t vstvalidator_binary_len = ${BINARY_SIZE};\n")