diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c64580..db32732 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ project( LANGUAGES CXX VERSION ${VERSION_STRIPPED} ) +include(GNUInstallDirs) add_subdirectory(src) diff --git a/meson.build b/meson.build index 3c4faae..89318c9 100644 --- a/meson.build +++ b/meson.build @@ -33,30 +33,4 @@ cps_config = executable( implicit_include_directories : false, ) -build_tests = get_option('tests') - -python_interpreter = find_program('python', version : '>=3.11', required : build_tests, disabler : true) -foreach t : [['cps integration tests', 'cps-config.toml'], ['pkg-config compatibility', 'pkg-config-compat.toml']] - test( - t[0], - python_interpreter, - args: [files('tests/runner.py'), cps_config, 'tests/cases/' + t[1]], - protocol : 'tap', - env : {'CPS_PREFIX_PATH' : meson.current_source_dir() / 'tests' / 'cps-files' }, - ) -endforeach - -dep_gtest = dependency('gtest_main', required : build_tests, disabler : true, allow_fallback : true) - -foreach t : ['loader', 'version', 'utils'] - test( - t, - executable( - f'@t@_test', - f'tests/@t@.cpp', - dependencies : [dep_cps, dep_gtest, dep_fmt, dep_expected], - implicit_include_directories : false, - ), - protocol : 'gtest', - ) -endforeach +subdir('tests') diff --git a/src/cps/config.hpp.in b/src/cps/config.hpp.in index 6a96f53..df0b403 100644 --- a/src/cps/config.hpp.in +++ b/src/cps/config.hpp.in @@ -1,3 +1,5 @@ #pragma once #define CPS_CONFIG_VERSION "${CMAKE_PROJECT_VERSION}" +#define CPS_CONFIG_LIBDIR "${CMAKE_INSTALL_LIBDIR}" +#define CPS_CONFIG_DATADIR "${CMAKE_INSTALL_DATADIR}" diff --git a/src/cps/env.cpp b/src/cps/env.cpp index 8fe4b71..71c9f41 100644 --- a/src/cps/env.cpp +++ b/src/cps/env.cpp @@ -2,11 +2,12 @@ // Copyright © 2024 Tyler Weaver // SPDX-License-Identifier: MIT -#include "cps/env.hpp" - #include #include +#include "cps/env.hpp" +#include "cps/utils.hpp" + namespace cps { Env get_env() { @@ -15,7 +16,8 @@ namespace cps { env.cps_path = std::string(env_c); } if (const char * env_c = std::getenv("CPS_PREFIX_PATH")) { - env.cps_prefix_path = std::string(env_c); + // TODO: Windows + env.cps_prefix_path = utils::split(env_c, ":"); } if (std::getenv("PKG_CONFIG_DEBUG_SPEW") || std::getenv("CPS_CONFIG_DEBUG_SPEW")) { env.debug_spew = true; diff --git a/src/cps/env.hpp b/src/cps/env.hpp index 9b12160..d2f3d57 100644 --- a/src/cps/env.hpp +++ b/src/cps/env.hpp @@ -6,12 +6,13 @@ #include #include +#include namespace cps { struct Env { std::optional cps_path = std::nullopt; - std::optional cps_prefix_path = std::nullopt; + std::optional> cps_prefix_path = std::nullopt; bool debug_spew = false; }; diff --git a/src/cps/meson.build b/src/cps/meson.build index d3e3e31..74a7769 100644 --- a/src/cps/meson.build +++ b/src/cps/meson.build @@ -3,6 +3,8 @@ conf = configuration_data() conf.set_quoted('CPS_CONFIG_VERSION', meson.project_version()) +conf.set_quoted('CPS_CONFIG_LIBDIR', get_option('libdir')) +conf.set_quoted('CPS_CONFIG_DATADIR', get_option('datadir')) conf_h = configure_file( configuration : conf, diff --git a/src/cps/platform.cpp b/src/cps/platform.cpp index ec72f15..1e85bd2 100644 --- a/src/cps/platform.cpp +++ b/src/cps/platform.cpp @@ -2,11 +2,12 @@ // Copyright © 2024 Dylan Baker #include "cps/platform.hpp" +#include "cps/config.hpp" namespace cps::platform { - fs::path libdir() { return "lib"; } + fs::path libdir() { return CPS_CONFIG_LIBDIR; } - fs::path datadir() { return "share"; } + fs::path datadir() { return CPS_CONFIG_DATADIR; } } // namespace cps::platform diff --git a/src/cps/search.cpp b/src/cps/search.cpp index 9496cb0..43a78d5 100644 --- a/src/cps/search.cpp +++ b/src/cps/search.cpp @@ -110,7 +110,7 @@ namespace cps::search { } if (env.cps_prefix_path) { - auto && prefixes = utils::split(env.cps_prefix_path.value()); + auto && prefixes = env.cps_prefix_path.value(); for (auto && p : prefixes) { auto && paths = expand_prefix(p); cached_paths.reserve(cached_paths.size() + paths.size()); @@ -309,7 +309,8 @@ namespace cps::search { } } - fs::path calculate_prefix(const std::optional & path, const fs::path & filename) { + tl::expected calculate_prefix(const std::optional & path, + const fs::path & filename) { // TODO: Windows // TODO: /cps/ if (path) { @@ -319,33 +320,43 @@ namespace cps::search { } fs::path f = filename.parent_path(); while (p != "@prefix@") { - utils::assert_fn( - p.stem() == f.stem(), - fmt::format("filepath and cps_path have non overlapping stems, prefix: {}, filename {}", - std::string{p}, std::string{f})); + if (p.stem() != f.stem()) { + return tl::unexpected( + fmt::format("filepath and cps_path have non overlapping stems, prefix: {}, filename {}", + std::string{p}, std::string{f})); + } p = p.parent_path(); f = f.parent_path(); } return f; } - std::vector split = utils::split(std::string{filename.parent_path()}, "/"); - if (split.back() == "cps") { - split.pop_back(); - } - if (split.back() == "share") { - split.pop_back(); - } - // TODO: this needs to be generic - if (split.back() == "lib") { - split.pop_back(); - } + fs::path p = filename.parent_path(); + + const auto reducer = [&p](fs::path dir) -> std::optional { + fs::path np = p; + + // remove a trailing slash + if (dir.stem() == "") { + dir = dir.parent_path(); + } + + while (dir.stem() == np.stem()) { + dir = dir.parent_path(); + np = np.parent_path(); + } - fs::path p{"/"}; - for (auto && s : split) { - p /= s; + // If our new path has changed and we have consumed the entire + // directory, then return that, otherwise this was not + // successful. + return np != p && dir == dir.root_path() ? std::optional{np} : std::nullopt; + }; + + if (p.stem() == "cps") { + p = p.parent_path(); } - return p; + + return reducer(platform::libdir()).value_or(reducer(platform::datadir()).value_or(p)); } /// @brief Calculate the required components in the graph @@ -418,8 +429,8 @@ namespace cps::search { for (auto && node : flat) { - const auto prefix = - prefix_path.value_or(calculate_prefix(node->data.package.cps_path, node->data.package.filename)); + const auto prefix = prefix_path.value_or( + CPS_TRY(calculate_prefix(node->data.package.cps_path, node->data.package.filename))); const auto && prefix_replacer = [&](const std::string & s) -> std::string { // TODO: Windows… diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b68a3de..ae5562c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,24 @@ foreach (test_name test_case IN ZIP_LISTS test_names test_cases) COMMAND ${CMAKE_COMMAND} -E env CPS_PREFIX_PATH=${CMAKE_CURRENT_SOURCE_DIR}/cps-files ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/runner.py - $ ${test_case} + $ ${test_case} --libdir "${CMAKE_INSTALL_LIBDIR}" ) endforeach () + +set(infiles + "cps-files/lib/cps/cps-path-not-set.cps" + "cps-files/lib/cps/cps-path-set.cps") +set(libdir ${CMAKE_INSTALL_LIBDIR}) +set(prefix "@prefix@") # Work around for not having the inverse of @ONLY +foreach(infile ${infiles}) + configure_file("${infile}.in" "${infile}") +endforeach() + +add_test( + NAME "prefix calculation tests" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMAND + ${CMAKE_COMMAND} -E env CPS_PREFIX_PATH=${CMAKE_CURRENT_BINARY_DIR}/cps-files + ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/runner.py + $ ${CMAKE_CURRENT_SOURCE_DIR}/cases/cps-prefix-calculation.toml --libdir "${CMAKE_INSTALL_LIBDIR}" --prefix ${PROJECT_BINARY_DIR} +) diff --git a/tests/cases/cps-config.toml b/tests/cases/cps-config.toml index 74f7d5a..390f951 100644 --- a/tests/cases/cps-config.toml +++ b/tests/cases/cps-config.toml @@ -72,24 +72,6 @@ cps = "multiple-components" args = ["flags", "--cflags-only-I", "--component", "requires-external"] expected = "-I/err" -[[case]] -name = "prefix set" -cps = "cps-path-set" -args = ["flags", "--cflags-only-I", "--libs-only-l"] -expected = "-I{prefix}/err -l{prefix}/lib/libfoo.a" - -[[case]] -name = "prefix set (called by path)" -cps = "{prefix}/cps-path-set.cps" -args = ["flags", "--cflags-only-I", "--libs-only-l"] -expected = "-I{prefix}/err -l{prefix}/lib/libfoo.a" - -[[case]] -name = "prefix calculated" -cps = "cps-path-not-set" -args = ["flags", "--cflags-only-I", "--libs-only-l"] -expected = "-I{prefix}/err -l{prefix}/lib/libfoo.a" - [[case]] name = "component diamond" cps = "diamond" diff --git a/tests/cases/cps-prefix-calculation.toml b/tests/cases/cps-prefix-calculation.toml new file mode 100644 index 0000000..c85b5eb --- /dev/null +++ b/tests/cases/cps-prefix-calculation.toml @@ -0,0 +1,17 @@ +[[case]] +name = "prefix set" +cps = "cps-path-set" +args = ["flags", "--cflags-only-I", "--libs-only-l", "--print-errors"] +expected = "-I{prefix}/err -l{prefix}/{libdir}/libfoo.a" + +[[case]] +name = "prefix set (called by path)" +cps = "{prefix}/cps-path-set.cps" +args = ["flags", "--cflags-only-I", "--libs-only-l", "--print-errors"] +expected = "-I{prefix}/err -l{prefix}/{libdir}/libfoo.a" + +[[case]] +name = "prefix calculated" +cps = "cps-path-not-set" +args = ["flags", "--cflags-only-I", "--libs-only-l"] +expected = "-I{prefix}/err -l{prefix}/{libdir}/libfoo.a" diff --git a/tests/cps-files/lib/cps/cps-path-not-set.cps b/tests/cps-files/lib/cps/cps-path-not-set.cps.in similarity index 86% rename from tests/cps-files/lib/cps/cps-path-not-set.cps rename to tests/cps-files/lib/cps/cps-path-not-set.cps.in index 8ac0202..cb3b4a1 100644 --- a/tests/cps-files/lib/cps/cps-path-not-set.cps +++ b/tests/cps-files/lib/cps/cps-path-not-set.cps.in @@ -10,7 +10,7 @@ "@prefix@/err" ] }, - "location": "@prefix@/lib/libfoo.a" + "location": "@prefix@/${libdir}/libfoo.a" } }, "default_components": [ diff --git a/tests/cps-files/lib/cps/cps-path-set.cps b/tests/cps-files/lib/cps/cps-path-set.cps.in similarity index 77% rename from tests/cps-files/lib/cps/cps-path-set.cps rename to tests/cps-files/lib/cps/cps-path-set.cps.in index 5f4afee..59ccebc 100644 --- a/tests/cps-files/lib/cps/cps-path-set.cps +++ b/tests/cps-files/lib/cps/cps-path-set.cps.in @@ -1,7 +1,7 @@ { "name": "cps-path-set", "cps_version": "0.10.0", - "cps_path": "@prefix@/lib/cps/", + "cps_path": "@prefix@/${libdir}/cps/", "version": "1.0.0", "components": { "default": { @@ -11,7 +11,7 @@ "@prefix@/err" ] }, - "location": "@prefix@/lib/libfoo.a" + "location": "@prefix@/${libdir}/libfoo.a" } }, "default_components": [ diff --git a/tests/cps-files/lib/cps/meson.build b/tests/cps-files/lib/cps/meson.build new file mode 100644 index 0000000..390e0ce --- /dev/null +++ b/tests/cps-files/lib/cps/meson.build @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: MIT +# Copyright © 2024 Dylan Baker + +conf = configuration_data() +conf.set('libdir', get_option('libdir')) +# Should be required, but: https://github.com/mesonbuild/meson/issues/13665 +conf.set('prefix', '@prefix@') + +foreach infile : ['cps-path-set.cps.in', 'cps-path-not-set.cps.in'] + configure_file( + configuration : conf, + input : infile, + output : '@BASENAME@', + format : 'cmake', + ) +endforeach diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..63b2d7a --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MIT +# Copyright © 2024 Dylan Baker + +subdir('cps-files/lib/cps') + +build_tests = get_option('tests') + +python_interpreter = find_program('python', version : '>=3.11', required : build_tests, disabler : true) +runner_args = [ + files('runner.py'), + cps_config, + '--libdir', get_option('libdir'), +] + +foreach t : [['cps integration tests', 'cps-config.toml'], ['pkg-config compatibility', 'pkg-config-compat.toml']] + test( + t[0], + python_interpreter, + args: [runner_args, meson.current_source_dir() / 'cases' / t[1]], + protocol : 'tap', + env : {'CPS_PREFIX_PATH' : meson.current_source_dir() / 'cps-files' }, + ) +endforeach +test( + 'prefix calculation tests', + python_interpreter, + args: [ + runner_args, + '--prefix', meson.project_build_root(), + meson.current_source_dir() / 'cases' / 'cps-prefix-calculation.toml', + ], + protocol : 'tap', + env : {'CPS_PREFIX_PATH' : meson.current_build_dir() / 'cps-files' }, +) + +dep_gtest = dependency('gtest_main', required : build_tests, disabler : true, allow_fallback : true) + +foreach t : ['loader', 'version', 'utils'] + test( + t, + executable( + f'@t@_test', + f'@t@.cpp', + dependencies : [dep_cps, dep_gtest, dep_fmt, dep_expected], + implicit_include_directories : false, + ), + protocol : 'gtest', + ) +endforeach diff --git a/tests/runner.py b/tests/runner.py index a31158c..5e2f9e4 100755 --- a/tests/runner.py +++ b/tests/runner.py @@ -6,10 +6,13 @@ import argparse import asyncio +import contextlib import dataclasses import enum import os +import shutil import sys +import tempfile import tomllib import typing @@ -19,6 +22,8 @@ class Arguments(typing.Protocol): runner: str cases: str + libdir: str + prefix: str | None class TestCase(typing.TypedDict): @@ -35,7 +40,6 @@ class TestDescription(typing.TypedDict): SOURCE_DIR = os.path.normpath(os.path.dirname(os.path.dirname(__file__))) -PREFIX = os.path.join(SOURCE_DIR, 'tests/cps-files') _PRINT_LOCK = asyncio.Lock() @@ -58,7 +62,8 @@ class Result: expected: str command: list[str] -def unordered_compare(out, expected): + +def unordered_compare(out: str, expected: str) -> bool: if out == expected: return True @@ -66,13 +71,16 @@ def unordered_compare(out, expected): expected_parts = expected.split() return sorted(out_parts) == sorted(expected_parts) -async def test(runner: str, case_: TestCase) -> Result: - cmd = [runner] + case_['args'] - cmd.append(case_['cps'].replace('{prefix}', os.path.join(PREFIX, 'lib/cps'))) + +async def test(args: Arguments, case_: TestCase) -> Result: + prefix = args.prefix or SOURCE_DIR + + cmd = [args.runner] + case_['args'] + cmd.append(case_['cps'].replace('{prefix}', os.path.join(prefix, args.libdir, 'cps'))) if 'mode' in case_: cmd.extend([f"--format={case_['mode']}"]) - expected = case_['expected'].format(prefix=PREFIX) + expected = case_['expected'].format(prefix=prefix, libdir=args.libdir) try: async with asyncio.timeout(5): @@ -100,38 +108,69 @@ async def test(runner: str, case_: TestCase) -> Result: return Result(case_['name'], result, out, err, returncode, expected, cmd) -async def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument('runner', help="The compiled cps-config binary") - parser.add_argument('cases', help="A toml file containing case descriptions") - args: Arguments = parser.parse_args() - - with open(os.path.join(SOURCE_DIR, args.cases), 'rb') as f: - tests = typing.cast('TestDescription', tomllib.load(f)) - +async def run_tests(args: Arguments, tests: TestDescription) -> bool: print(f'1..{len(tests["case"])}') results = typing.cast( 'list[Result]', - await asyncio.gather(*[test(args.runner, c) for c in tests['case']])) + await asyncio.gather(*[test(args, c) for c in tests['case']])) encountered_failure: bool = False for r in results: if r.status is not Status.PASS: print(f'{r.name}:', file=sys.stderr) - print(' result:', 'timeout' if r.status is Status.TIMEOUT else 'fail') + print(' result:', 'timeout' if r.status is Status.TIMEOUT else 'fail', file=sys.stderr) print(' returncode:', r.returncode, file=sys.stderr) print(' stdout: ', r.stdout, file=sys.stderr) print(' expected:', r.expected, file=sys.stderr) print(' stderr:', r.stderr, file=sys.stderr) print(' command:', ' '.join(r.command), file=sys.stderr) - print('\n') + print('\n', file=sys.stderr) encountered_failure = True - if encountered_failure: - exit(1) + return encountered_failure + + +async def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('runner', help="The compiled cps-config binary") + parser.add_argument('cases', help="A toml file containing case descriptions") + parser.add_argument('--libdir', default='lib', help="the build system configured libdir") + parser.add_argument('--prefix', default=None, help="The prefix tests are realtive to") + args: Arguments = parser.parse_args() + + with open(os.path.join(SOURCE_DIR, args.cases), 'rb') as f: + tests = typing.cast('TestDescription', tomllib.load(f)) + + with contextlib.ExitStack() as stack: + # If the libdir is not "lib" (which tests assume), create a symlink to + # the expected libdir + if args.libdir != 'lib' or args.prefix: + prefix = str(os.environ['CPS_PREFIX_PATH']) + tmpdir = tempfile.mkdtemp() + stack.callback(shutil.rmtree, tmpdir) + os.environ['CPS_PREFIX_PATH'] = tmpdir + + # Also override the prefix, which is used to calculate @prefix@ + args.prefix = tmpdir + + # Handle libdir with multiple paths, like lib/x86_64-linux-gnu + root, libdir = os.path.split(args.libdir) + if root: + tmpdir = os.path.join(tmpdir, root) + os.makedirs(tmpdir, exist_ok=True) + + source = os.path.join(prefix, 'lib') + dest = os.path.join(tmpdir, libdir) + os.symlink(source, dest) + stack.callback(os.unlink, dest) + + failed = await run_tests(args, tests) + + sys.exit(1 if failed else 0) + if __name__ == "__main__": asyncio.run(main())