From f0eddc8dc5951873cd45ca885b00eb3823e23060 Mon Sep 17 00:00:00 2001 From: Zephyr Lykos Date: Sat, 17 Jan 2026 09:39:24 +0800 Subject: [PATCH] Refactor meson build system (#1280) - Bump meson version to 1.3 - Bump default cpp_std (required by Catch2 3.x, still possible for c++11 fallback) - Move option `single-file-header` and `precompiled` to `mode` - Make pkgconfig optional - Refactor tests, fix a bizzare situation where filename contains quote or escape characters --- .github/workflows/tests.yml | 12 ++-- MODULE.bazel | 1 + meson.build | 108 ++++++++++++++++++------------- meson_options.txt | 5 +- single-include/meson.build | 26 ++------ tests/AppTest.cpp | 108 +++++++++++++++++++++++++++---- tests/BUILD.bazel | 5 ++ tests/CMakeLists.txt | 4 ++ tests/meson.build | 123 ++++++++++++++++-------------------- 9 files changed, 241 insertions(+), 151 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b5dd06a..9300f9bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ concurrency: permissions: contents: read - + env: CTEST_OUTPUT_ON_FAILURE: "1" @@ -191,7 +191,7 @@ jobs: pipx install ninja - name: Configure - run: meson setup build-meson . -Dtests=true + run: meson setup build-meson . -Dtests=enabled - name: Build run: meson compile -C build-meson @@ -263,7 +263,7 @@ jobs: - name: Run tests run: ctest --output-on-failure -L Packaging working-directory: build - + install-precompiled-macos: name: install tests precompiled macos runs-on: macos-15 @@ -280,7 +280,7 @@ jobs: - name: Run tests run: ctest --output-on-failure -L Packaging working-directory: build - + install-precompiled-macos-no-validators: name: install tests precompiled macos no validators runs-on: macos-15 @@ -314,7 +314,7 @@ jobs: - name: Run tests run: ctest --output-on-failure -L Packaging working-directory: build - + install-module: name: install module tests runs-on: ubuntu-latest @@ -472,7 +472,7 @@ jobs: with: cmake-version: "4.1" if: success() || failure() - + - name: Check CMake 4.2 uses: ./.github/actions/quick_cmake with: diff --git a/MODULE.bazel b/MODULE.bazel index 234baa3e..d5909fb7 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,3 +1,4 @@ module(name = "cli11") +bazel_dep(name = "rules_cc", version = "0.2.16") bazel_dep(name = "catch2", version = "3.5.4", dev_dependency = True) diff --git a/meson.build b/meson.build index 8ca57021..8683db37 100644 --- a/meson.build +++ b/meson.build @@ -1,18 +1,13 @@ project('CLI11', ['cpp'], version : run_command(find_program('scripts/ExtractVersion.py'), check: true).stdout().strip(), license : 'BSD-3-clause', - meson_version : '>= 0.60', - default_options : ['cpp_std=c++11', 'warning_level=3'] + meson_version : '>= 1.3', + default_options : ['cpp_std=c++17,c++14,c++11', 'warning_level=3'] ) cxx = meson.get_compiler('cpp') -use_single_header = get_option('single-file-header') -use_precompiled = get_option('precompiled') - -if use_precompiled and use_single_header - error('Options "single-file"header" and "precompiled" are mutually exclusive') -endif +buildmode = get_option('mode') cli11_headers = files( 'include/CLI/App.hpp', @@ -47,52 +42,79 @@ cli11_impl_headers = files( 'include/CLI/impl/ExtraValidators_inl.hpp', ) -subdir('single-include') +cli11_inc = include_directories('include') -CLI11_inc = include_directories(['include']) - -warnings = ['-Wshadow', '-Wsign-conversion', '-Wswitch-enum'] -if cxx.get_id() == 'gcc' and cxx.version().version_compare('>=4.9') - warnings += '-Weffc++' +if cxx.get_argument_syntax() == 'gcc' + warnings = ['-Wshadow', '-Wsign-conversion', '-Wswitch-enum'] + if cxx.get_id() == 'gcc' and cxx.version().version_compare('>=4.9') + warnings += '-Weffc++' + endif + if cxx.get_id() == 'clang' + warnings += [ + '-Wcast-align', + '-Wimplicit-atomic-properties', + '-Wmissing-declarations', + '-Woverlength-strings', + '-Wstrict-selector-match', + '-Wundeclared-selector', + ] + endif + add_project_arguments(cxx.get_supported_arguments(warnings), language: 'cpp') endif -if cxx.get_id() == 'clang' - warnings += [ - '-Wcast-align', - '-Wimplicit-atomic-properties', - '-Wmissing-declarations', - '-Woverlength-strings', - '-Wstrict-selector-match', - '-Wundeclared-selector', - ] -endif -add_project_arguments(cxx.get_supported_arguments(warnings), language: 'cpp') -if use_precompiled +if buildmode == 'amalgamated' + subdir('single-include') + + cli11_dep = declare_dependency( + sources: single_header, + include_directories : include_directories(meson.current_build_dir() / 'single-include'), + ) +elif buildmode == 'headeronly' + install_headers(cli11_headers, subdir: 'CLI') + + pkg = import('pkgconfig', required: false) + if pkg.found() + pkg.generate( + name : 'CLI11', + description : 'CLI11 is a command line parser for C++11 and beyond that provides a rich feature set with a simple and intuitive interface.', + url : 'https://github.com/CLIUtils/CLI11', + install_dir : get_option('datadir') / 'pkgconfig', + ) + endif + + cli11_dep = declare_dependency(include_directories : cli11_inc) +elif buildmode == 'precompiled' + cli11_cflags = ['-DCLI11_COMPILE', '-DCLI11_ENABLE_EXTRA_VALIDATORS=1'] + libcli11 = library( 'CLI11', 'src/Precompile.cpp', - include_directories : CLI11_inc, - cpp_args : ['-DCLI11_COMPILE -DCLI11_ENABLE_EXTRA_VALIDATORS=1'], + include_directories : cli11_inc, + cpp_args : cli11_cflags, install : true, ) - pkg = import('pkgconfig') - pkg.generate(libcli11, extra_cflags: ['-DCLI11_COMPILE -DCLI11_ENABLE_EXTRA_VALIDATORS=1']) + pkg = import('pkgconfig', required: false) + if pkg.found() + pkg.generate( + libcli11, + description : 'CLI11 is a command line parser for C++11 and beyond that provides a rich feature set with a simple and intuitive interface.', + extra_cflags : cli11_cflags, + url : 'https://github.com/CLIUtils/CLI11', + ) + endif install_headers(cli11_headers, subdir: 'CLI') -else - libcli11 = [] + + cli11_dep = declare_dependency( + link_with : libcli11, + include_directories : cli11_inc, + ) endif -CLI11_dep = declare_dependency( - sources : single_header, - link_with : libcli11, - include_directories : CLI11_inc, - version : meson.project_version(), -) +meson.override_dependency('CLI11', cli11_dep) -meson.override_dependency('CLI11', CLI11_dep) - -if get_option('tests') - subdir('tests') -endif +tests = get_option('tests') +tests = tests.disable_auto_if(meson.is_subproject()) +catch2_dep = dependency('catch2', required: tests) +subdir('tests', if_found: catch2_dep) diff --git a/meson_options.txt b/meson_options.txt index f32583d8..9e85b4fd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,2 @@ -option('tests', type: 'boolean', value: false, description: 'Build CLI11 tests') -option('single-file-header', type: 'boolean', value: false, description : 'Generate a single header file.') -option('precompiled', type: 'boolean', value: false, description : 'Generate a precompiled static library instead of a header-only') +option('tests', type: 'feature', value: 'auto', description: 'Build CLI11 tests') +option('mode', type: 'combo', choices: ['amalgamated', 'headeronly', 'precompiled'], value: 'headeronly') diff --git a/single-include/meson.build b/single-include/meson.build index e889021d..b2fe9fe5 100644 --- a/single-include/meson.build +++ b/single-include/meson.build @@ -2,23 +2,11 @@ # meson.build here when generating the single file header so that it is placed # in the correct location. -pymod = import('python') -prog_python = pymod.find_installation() - single_main_file = files('CLI11.hpp.in') - -if use_single_header - single_header = custom_target( - 'CLI11.hpp', - input: [files('../scripts/MakeSingleHeader.py'), cli11_headers, cli11_impl_headers], - output: 'CLI11.hpp', - command : [prog_python, '@INPUT@', '--main', single_main_file, '--output', '@OUTPUT@'], - depend_files: [single_main_file], - ) -else - # the `declare_dependency` needs to have the single_header source as a source - # dependency, to ensure that the generator runs before any attempts to include - # the header happen. Adding an empty list is an idiomatic way to ensure the - # variable exists but does nothing - single_header = [] -endif +single_header = custom_target( + 'CLI11.hpp', + input: [cli11_headers, cli11_impl_headers], + output: 'CLI11.hpp', + command : [find_program('../scripts/MakeSingleHeader.py'), '@INPUT@', '--main', single_main_file, '--output', '@OUTPUT@'], + depend_files: [single_main_file], +) diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 2026b35c..d42b3cdf 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -5,19 +5,32 @@ // SPDX-License-Identifier: BSD-3-Clause #include "app_helper.hpp" -#include #include +#include #include #include -#include #include -#include #include #include #include #include +#ifdef _WIN32 +#define PLATFORM_TEXT(x) _PLATFORM_TEXT(x) +#define _PLATFORM_TEXT(x) L##x +using tchar = wchar_t; +#include +#else +#define PLATFORM_TEXT(x) x +using tchar = char; +#include +#include +#include +#include +#include +#endif + TEST_CASE_METHOD(TApp, "OneFlagShort", "[app]") { app.add_flag("-c,--count"); args = {"-c"}; @@ -2979,38 +2992,107 @@ TEST_CASE("C20_compile", "simple") { CHECK_FALSE(flag->empty()); } +#ifdef _WIN32 +static int spawn_subprocess_win32(const wchar_t *path, wchar_t *commandline) { + STARTUPINFOW si{}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{}; + REQUIRE(CreateProcessW(path, commandline, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)); + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exitcode; // NOLINT(cppcoreguidelines-init-variables) + REQUIRE(GetExitCodeProcess(pi.hProcess, &exitcode)); + + return static_cast(exitcode); +} +#else +static int spawn_subprocess_posix(const char *path, char *const *argv) { + // NOLINTBEGIN(cppcoreguidelines-init-variables) + pid_t pid; + sigset_t old, reset; + struct sigaction sa, oldint, oldquit; + std::memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + int status = -1, ret; + posix_spawnattr_t attr; + // NOLINTEND(cppcoreguidelines-init-variables) + + pthread_testcancel(); + + sigaction(SIGINT, &sa, &oldint); + sigaction(SIGQUIT, &sa, &oldquit); + sigaddset(&sa.sa_mask, SIGCHLD); + sigprocmask(SIG_BLOCK, &sa.sa_mask, &old); + + sigemptyset(&reset); + if(oldint.sa_handler != SIG_IGN) + sigaddset(&reset, SIGINT); + if(oldquit.sa_handler != SIG_IGN) + sigaddset(&reset, SIGQUIT); + posix_spawnattr_init(&attr); + posix_spawnattr_setsigmask(&attr, &old); + posix_spawnattr_setsigdefault(&attr, &reset); + posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK); + CHECK((ret = posix_spawn(&pid, path, nullptr, &attr, argv, nullptr)) == 0); + + if(ret == 0) + while(waitpid(pid, &status, 0) < 0 && errno != EINTR) { + } + + sigaction(SIGINT, &oldint, nullptr); + sigaction(SIGQUIT, &oldquit, nullptr); + sigprocmask(SIG_SETMASK, &old, nullptr); + + return status; +} +#endif + +static int spawn_app_exe(const tchar *path) { +#ifdef _WIN32 + std::wstring args{L"app_exe 1234 false \"hello world\""}; + return spawn_subprocess_win32(path, &args[0]); +#else + std::string arg0{"app_exe"}; + std::string arg1{"1234"}; + std::string arg2{"false"}; + std::string arg3{"hello world"}; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + char *const args[] = {&arg0[0], &arg1[0], &arg2[0], &arg3[0], nullptr}; + return spawn_subprocess_posix(path, args); +#endif +} + // #845 TEST_CASE("Ensure UTF-8", "[app]") { - const char *commandline = CLI11_ENSURE_UTF8_EXE " 1234 false \"hello world\""; - int retval = std::system(commandline); + auto retval = spawn_app_exe(PLATFORM_TEXT(CLI11_ENSURE_UTF8_EXE)); if(retval == -1) { - FAIL("Executable '" << commandline << "' reported that argv pointer changed where it should not have been"); + FAIL("Executable " CLI11_ENSURE_UTF8_EXE " reported that argv pointer changed where it should not have been"); } if(retval > 0) { - FAIL("Executable '" << commandline << "' reported different argv at index " << (retval - 1)); + FAIL("Executable " CLI11_ENSURE_UTF8_EXE " reported different argv at index " << (retval - 1)); } if(retval != 0) { - FAIL("Executable '" << commandline << "' failed with an unknown return code"); + FAIL("Executable " CLI11_ENSURE_UTF8_EXE " failed with an unknown return code"); } } // #845 TEST_CASE("Ensure UTF-8 called twice", "[app]") { - const char *commandline = CLI11_ENSURE_UTF8_TWICE_EXE " 1234 false \"hello world\""; - int retval = std::system(commandline); + auto retval = spawn_app_exe(PLATFORM_TEXT(CLI11_ENSURE_UTF8_TWICE_EXE)); if(retval == -1) { - FAIL("Executable '" << commandline << "' reported that argv pointer changed where it should not have been"); + FAIL("Executable " CLI11_ENSURE_UTF8_TWICE_EXE + " reported that argv pointer changed where it should not have been"); } if(retval > 0) { - FAIL("Executable '" << commandline << "' reported different argv at index " << (retval - 1)); + FAIL("Executable " CLI11_ENSURE_UTF8_TWICE_EXE " reported different argv at index " << (retval - 1)); } if(retval != 0) { - FAIL("Executable '" << commandline << "' failed with an unknown return code"); + FAIL("Executable " CLI11_ENSURE_UTF8_TWICE_EXE " failed with an unknown return code"); } } diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 85bf31c0..9bdeab5f 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -37,6 +37,11 @@ cc_test( "//:cli11", "@catch2", ], + linkopts = select({ + "@rules_cc//cc/compiler:msvc-cl": [], + "@rules_cc//cc/compiler:clang-cl": [], + "//conditions:default": ["-pthread"], + }), ) [ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3156291f..863604fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -213,6 +213,10 @@ if(CMAKE_CXX_STANDARD GREATER 16) file(REMOVE ${CMAKE_BINARY_DIR}/test_atomic.cpp) endif() +find_package(Threads) + +target_link_libraries(AppTest PRIVATE Threads::Threads) + # Add -Wno-deprecated-declarations to DeprecatedTest set(no-deprecated-declarations $<$:/wd4996> $<$>:-Wno-deprecated-declarations>) diff --git a/tests/meson.build b/tests/meson.build index d61747e7..704d425a 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,106 +1,95 @@ -catch2 = dependency('catch2') - -if catch2.version().version_compare('<3') +if catch2_dep.version().version_compare('< 3') testmain = static_library( 'catch_main', 'main.cpp', 'catch.hpp', - dependencies: catch2, + dependencies: catch2_dep, ) testdep = declare_dependency( link_with: testmain, - dependencies: [catch2, CLI11_dep] + dependencies: [catch2_dep, cli11_dep] ) else testdep = declare_dependency( - dependencies: [CLI11_dep, dependency('catch2-with-main')], + dependencies: [cli11_dep, dependency('catch2-with-main')], compile_args: '-DCLI11_CATCH3' ) endif -link_test_lib = library( +link_test_lib = static_library( 'link_test_1', 'link_test_1.cpp', - dependencies: CLI11_dep, + dependencies: cli11_dep, + build_by_default: false, ) -if cxx.get_id() == 'msvc' +if cxx.get_argument_syntax() == 'msvc' nodeprecated = ['/wd4996'] else nodeprecated = ['-Wno-deprecated-declarations'] endif -boost = dependency('boost', required: false) -if boost.found() +boost_dep = dependency('boost', required: false, disabler: true) +if boost_dep.found() boost_dep = declare_dependency( - dependencies: boost, + dependencies: boost_dep, compile_args: '-DCLI11_BOOST_OPTIONAL', ) + maybe_boost_dep = boost_dep else - boost_dep = declare_dependency() + maybe_boost_dep = declare_dependency() endif -testnames = [ - ['HelpersTest', {}], - ['ConfigFileTest', {}], - ['OptionTypeTest', {}], - ['NumericTypeTest', {}], - ['SimpleTest', {}], - ['AppTest', {}], - ['SetTest', {}], - ['TransformTest', {}], - ['CreationTest', {}], - ['SubcommandTest', {}], - ['HelpTest', {}], - ['FormatterTest', {}], - ['NewParseTest', {}], - ['OptionalTest', {'dependencies': boost_dep}], - ['DeprecatedTest', {'cpp_args': nodeprecated}], - ['StringParseTest', {}], - ['ComplexTypeTest', {}], - ['TrueFalseTest', {}], - ['localeTest', {}], - ['OptionGroupTest', {}], - ['ExtraValidatorsTest', {}], - ['EncodingTest', {}], +testnames = { + 'HelpersTest': {'workdir': meson.project_build_root()}, + 'ConfigFileTest': {}, + 'OptionTypeTest': {}, + 'NumericTypeTest': {}, + 'SimpleTest': {}, + 'AppTest': {}, + 'SetTest': {}, + 'TransformTest': {}, + 'CreationTest': {}, + 'SubcommandTest': {}, + 'HelpTest': {}, + 'FormatterTest': {}, + 'NewParseTest': {}, + 'OptionalTest': {'dependencies': maybe_boost_dep}, + 'BoostOptionTypeTest': {'dependencies': boost_dep}, + 'DeprecatedTest': {'cpp_args': nodeprecated}, + 'StringParseTest': {}, + 'ComplexTypeTest': {}, + 'TrueFalseTest': {}, + 'localeTest': {}, + 'OptionGroupTest': {}, + 'ExtraValidatorsTest': {}, + 'EncodingTest': {}, + 'WindowsTest': {'dependencies': host_machine.system() == 'windows' ? declare_dependency() : disabler()}, # multi-only - ['TimerTest', {}], + 'TimerTest': {}, # link_test - ['link_test_2', {'link_with': link_test_lib}], -] + 'link_test_2': {'link_with': link_test_lib}, +} -dependent_applications = [ - 'ensure_utf8', - 'ensure_utf8_twice', -] -dependent_applications_definitions = [] -dependent_applications_targets = [] -foreach app: dependent_applications - app_target = executable( +fs = import('fs') +app_cfgdata = configuration_data() +app_cflags = [] +app_tgts = [] +foreach app: ['ensure_utf8', 'ensure_utf8_twice'] + app_tgt = executable( app, 'applications'/app + '.cpp', - dependencies: CLI11_dep, + dependencies: cli11_dep, + build_by_default: false, ) - - dependent_applications_targets += app_target - dependent_applications_definitions += '-DCLI11_@0@_EXE="@1@/@2@"'.format( - app.to_upper(), meson.current_build_dir(), app_target) + app_cfgdata.set_quoted(app, fs.relative_to(app_tgt.full_path(), meson.current_build_dir())) + app_cflags += '-DCLI11_@0@_EXE=@1@'.format(app.to_upper(), app_cfgdata.get(app)) + app_tgts += app_tgt endforeach -if host_machine.system() == 'windows' - testnames += [['WindowsTest', {}]] -endif - -if boost.found() - testnames += [['BoostOptionTypeTest', {'dependencies': boost_dep}]] -endif - -foreach n: testnames - name = n[0] - kwargs = n[1] - t = executable(name, name + '.cpp', - cpp_args: kwargs.get('cpp_args', []) + dependent_applications_definitions, +foreach name, kwargs: testnames + test(name, executable(name, name + '.cpp', + cpp_args: app_cflags + kwargs.get('cpp_args', []), build_by_default: false, dependencies: [testdep] + kwargs.get('dependencies', []), link_with: kwargs.get('link_with', []) - ) - test(name, t, depends: dependent_applications_targets) + ), depends: app_tgts, workdir: kwargs.get('workdir', meson.current_build_dir())) endforeach