Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI to wrap CLAP plugins #316

Open
wants to merge 11 commits into
base: next
Choose a base branch
from
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,5 @@ endif()
if (${CLAP_WRAPPER_BUILD_TESTS})
add_subdirectory(tests)
endif()

add_subdirectory(cli)
20 changes: 20 additions & 0 deletions cli/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
add_executable(clap-wrapper-cli
src/main.cpp
)

CPMAddPackage(NAME CL11
GITHUB_REPOSITORY CLIUtils/CLI11
GIT_TAG v2.4.2)

# TODO: deduplicate this with the vendored fmt -
# for some reason, the cli won't link
# if we include the vendored version
CPMAddPackage(
NAME fmt
GITHUB_REPOSITORY fmtlib/fmt
GIT_TAG 11.0.2
OPTIONS
"FMT_INSTALL ON"
)

target_link_libraries(clap-wrapper-cli PUBLIC CLI11::CLI11 fmt::fmt)
267 changes: 267 additions & 0 deletions cli/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
#include <iostream>
#include <string>
#include <fstream>
#include <stdexcept>
#include <CLI/CLI.hpp>
#include <fmt/format.h>

// rationale against using std::filesystem is that
// it requires macOS 10.15, but our min supported version is 10.13 atm
#ifdef _WIN32
#include <direct.h>
#define CREATE_DIRECTORY(dir) _mkdir(dir)
#define DIRECTORY_EXISTS (errno == EEXIST)
#define PATH_SEPARATOR "\\"
#define GET_CURRENT_DIR _getcwd
#else
#include <sys/stat.h>
#include <unistd.h>
#define CREATE_DIRECTORY(dir) mkdir(dir, 0777)
#define DIRECTORY_EXISTS (errno == EEXIST)
#define PATH_SEPARATOR "/"
#define GET_CURRENT_DIR getcwd
#endif

std::string get_absolute_path(const std::string& path)
{
if (path.empty())
{
return "";
}

if (path[0] == '/' || (path.length() > 1 && path[1] == ':'))
{
// Path is already absolute
return path;
}

char current_dir[1024];
if (GET_CURRENT_DIR(current_dir, sizeof(current_dir)) == nullptr)
{
throw std::runtime_error("Failed to get current directory");
}

return std::string(current_dir) + PATH_SEPARATOR + path;
}

/**
* Creates a directory if it doesn't exist.
* @param path Path of the directory to create.
*/
void create_directory(const std::string& path)
{
if (CREATE_DIRECTORY(path.c_str()) != 0)
{
if (!DIRECTORY_EXISTS)
{
throw std::runtime_error("Failed to create directory: " + path +
" Error: " + std::strerror(errno));
}
}
}

void write_cmakelists(const std::string& output_dir)
{
std::string cmake_path = output_dir + "/CMakeLists.txt";
std::ofstream cmake_file(cmake_path);
if (!cmake_file)
{
throw std::runtime_error("Failed to create CMakeLists.txt");
}

const char* cmake_content = R"(
cmake_minimum_required(VERSION 3.15)
project(${PROJECT_NAME})

set(CMAKE_CXX_STANDARD 17)
if (APPLE)
CrushedPixel marked this conversation as resolved.
Show resolved Hide resolved
enable_language(OBJC)
enable_language(OBJCXX)
endif()

# Download CPM.cmake
file(
DOWNLOAD
https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.40.2/CPM.cmake
${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake
EXPECTED_HASH SHA256=C8CDC32C03816538CE22781ED72964DC864B2A34A310D3B7104812A5CA2D835D
)
include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake)

# Add clap-wrapper as a dependency
set(CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES ON)
CPMAddPackage(
NAME clap-wrapper
GITHUB_REPOSITORY free-audio/clap-wrapper
GIT_TAG main
)

if(BUILD_AU)
set(AUV2_TARGET ${PROJECT_NAME}_auv2)
add_library(${AUV2_TARGET} MODULE)
target_add_auv2_wrapper(
TARGET ${AUV2_TARGET}
MACOS_EMBEDDED_CLAP_LOCATION ${CLAP_PLUGIN_PATH}

# TODO: remove all the below once baconpaul reads it from the CLAP
OUTPUT_NAME "${PROJECT_NAME}"
BUNDLE_IDENTIFIER "com.yourcompany.${PROJECT_NAME}clap"
BUNDLE_VERSION "1.0"
MANUFACTURER_NAME "Your Company"
MANUFACTURER_CODE "YuCu"
SUBTYPE_CODE "${AU_SUBTYPE_CODE}"
INSTRUMENT_TYPE "aufx"
)

add_custom_command(TARGET ${AUV2_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
$<TARGET_BUNDLE_DIR:${AUV2_TARGET}>
"${OUTPUT_DIR}/${PROJECT_NAME}.component"
)
endif()

if(BUILD_VST3)
set(VST3_TARGET ${PROJECT_NAME}_vst3)
add_library(${VST3_TARGET} MODULE)
target_add_vst3_wrapper(
TARGET ${VST3_TARGET}
MACOS_EMBEDDED_CLAP_LOCATION ${CLAP_PLUGIN_PATH}
WINDOWS_FOLDER_VST3 ON
OUTPUT_NAME "${PROJECT_NAME}"
)

add_custom_command(TARGET ${VST3_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
$<TARGET_BUNDLE_DIR:${VST3_TARGET}>
"${OUTPUT_DIR}/${PROJECT_NAME}.vst3"
)
endif()

if(BUILD_STANDALONE)
set(STANDALONE_TARGET ${PROJECT_NAME}_standalone)
add_executable(${STANDALONE_TARGET})
target_add_standalone_wrapper(
TARGET ${STANDALONE_TARGET}
MACOS_EMBEDDED_CLAP_LOCATION ${CLAP_PLUGIN_PATH}
OUTPUT_NAME "${PROJECT_NAME}"
)

if(APPLE)
set_target_properties(${STANDALONE_TARGET} PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER "com.yourcompany.${PROJECT_NAME}standalone"
MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
)
add_custom_command(TARGET ${STANDALONE_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
$<TARGET_BUNDLE_DIR:${STANDALONE_TARGET}>
"${OUTPUT_DIR}/${PROJECT_NAME}.app"
)
elseif(WIN32)
add_custom_command(TARGET ${STANDALONE_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:${STANDALONE_TARGET}>
"${OUTPUT_DIR}/${PROJECT_NAME}.exe"
)
else()
add_custom_command(TARGET ${STANDALONE_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:${STANDALONE_TARGET}>
"${OUTPUT_DIR}/${PROJECT_NAME}"
)
endif()
endif()
)";

cmake_file << cmake_content;
cmake_file.close();
CrushedPixel marked this conversation as resolved.
Show resolved Hide resolved
}

int main(int argc, char** argv)
{
CLI::App app{"CLAP Wrapper CLI"};

std::string input_path;
std::string output_dir = ".";
std::string build_dir = "";
bool build_au = false;
bool build_vst3 = false;
bool build_standalone = false;

app.add_option("-i,--input", input_path, "Input .clap file or directory")->required();
app.add_option("-o,--outputDir", output_dir, "Output directory");
app.add_option("-b,--buildDir", build_dir, "Build directory for CMake");
app.add_flag("--au", build_au, "Build Audio Unit wrapper");
app.add_flag("--vst3", build_vst3, "Build VST3 wrapper");
app.add_flag("--standalone", build_standalone, "Build standalone application");

CLI11_PARSE(app, argc, argv);

if (!build_au && !build_vst3 && !build_standalone)
{
throw std::runtime_error("At least one output format must be specified");
}


// resolve paths to absolute
input_path = get_absolute_path(input_path);
output_dir = get_absolute_path(output_dir);

if (build_dir.empty())
{
// default to adding a build dir in the working directory
build_dir = get_absolute_path("wrap-build");
}
else
{
build_dir = get_absolute_path(build_dir);
}

try
{
// create the output directory if it doesn't exist
create_directory(output_dir);

// create build directory
create_directory(build_dir);

// write CMakeLists.txt
write_cmakelists(build_dir);

// extract project name from input path
std::string::size_type pos = input_path.find_last_of("/\\");
std::string clap_filename = (pos == std::string::npos) ? input_path : input_path.substr(pos + 1);
std::string project_name = clap_filename.substr(0, clap_filename.find_last_of('.'));

// construct cmake command with variables supplied
std::string cmake_cmd = fmt::format(
"cmake . "
CrushedPixel marked this conversation as resolved.
Show resolved Hide resolved
"-DPROJECT_NAME=\"{}\" "
"-DCLAP_PLUGIN_PATH=\"{}\" "
"-DOUTPUT_DIR=\"{}\" "
"-DBUILD_AU={} "
"-DBUILD_VST3={} "
"-DBUILD_STANDALONE={} "
"-DAU_SUBTYPE_CODE=\"{}\"",
CrushedPixel marked this conversation as resolved.
Show resolved Hide resolved
project_name, input_path, output_dir, build_au ? "ON" : "OFF", build_vst3 ? "ON" : "OFF",
build_standalone ? "ON" : "OFF", project_name.substr(0, 4));

// run CMake
cmake_cmd = fmt::format("cd \"{}\" && {} && cmake --build . --config Release", build_dir, cmake_cmd);

int result = std::system(cmake_cmd.c_str());
if (result != 0)
{
throw std::runtime_error("CMake build failed");
}

std::cout << "Wrapping completed successfully. Output files are in: " << output_dir << std::endl;
}
catch (const std::exception& e)
{
std::cerr << "Error during wrapping: " << e.what() << std::endl;
return 1;
}

return 0;
}