diff --git a/.github/actions/build-and-test/action.yml b/.github/actions/build-and-test/action.yml index 940745d0a..26c081167 100644 --- a/.github/actions/build-and-test/action.yml +++ b/.github/actions/build-and-test/action.yml @@ -3,10 +3,8 @@ description: Shared build/test steps for all runners inputs: build: required: true - type: string coverage: required: true - type: boolean runs: using: "composite" steps: diff --git a/.github/actions/setup-llvm/action.yml b/.github/actions/setup-llvm/action.yml index 6fac1ed2d..afd84b70c 100644 --- a/.github/actions/setup-llvm/action.yml +++ b/.github/actions/setup-llvm/action.yml @@ -2,7 +2,6 @@ name: Setup LLVM description: Install LLVM from apt.llvm.org inputs: version: - type: string required: true runs: using: "composite" diff --git a/src/BuildConfig.hpp b/include/Builder/BuildGraph.hpp similarity index 59% rename from src/BuildConfig.hpp rename to include/Builder/BuildGraph.hpp index 8e758953d..1822faa3e 100644 --- a/src/BuildConfig.hpp +++ b/include/Builder/BuildGraph.hpp @@ -1,14 +1,17 @@ #pragma once #include "Builder/BuildProfile.hpp" +#include "Builder/Compiler.hpp" #include "Builder/NinjaPlan.hpp" #include "Builder/Project.hpp" +#include "Builder/SourceLayout.hpp" #include "Command.hpp" #include "Manifest.hpp" +#include "Rustify/Result.hpp" #include +#include #include -#include #include #include #include @@ -19,30 +22,42 @@ namespace cabin { -// clang-format off -inline const std::unordered_set SOURCE_FILE_EXTS{ - ".c", ".c++", ".cc", ".cpp", ".cxx" -}; -inline const std::unordered_set HEADER_FILE_EXTS{ - ".h", ".h++", ".hh", ".hpp", ".hxx" -}; -// clang-format on +namespace fs = std::filesystem; -class BuildConfig { +class BuildGraph { public: - struct TestTarget; - // NOLINTNEXTLINE(*-non-private-member-variables-in-classes) - fs::path outBasePath; + enum class TestKind : std::uint8_t { Unit, Integration }; -private: - Project project; - Compiler compiler; - BuildProfile buildProfile; - std::string libName; + struct TestTarget { + std::string ninjaTarget; + std::string sourcePath; + TestKind kind = TestKind::Unit; + }; + + static Result create(const Manifest& manifest, + const BuildProfile& buildProfile); + + const fs::path& outBasePath() const { return outBasePath_; } + const Manifest& manifest() const { return project.manifest; } + const BuildProfile& buildProfile() const { return buildProfile_; } + + bool hasBinaryTarget() const { return hasBinaryTarget_; } + bool hasLibraryTarget() const { return hasLibraryTarget_; } + const std::string& libraryName() const { return libName; } + const std::vector& testTargets() const { return testTargets_; } + + Result installDeps(bool includeDevDeps); + void enableCoverage(); + Result plan(); + Result writeBuildFilesIfNeeded() const; + Result generateCompdb() const; - bool hasBinaryTarget{ false }; - bool hasLibraryTarget{ false }; + Result needsBuild(const std::vector& targets) const; + Command ninjaCommand(bool dryRun = false) const; + Result buildTargets(const std::vector& targets, + std::string_view displayName) const; +private: struct CompileUnit { std::string source; std::unordered_set dependencies; @@ -58,18 +73,8 @@ class BuildConfig { objectSubdir(std::move(objectSubdir)) {} }; - std::unordered_map compileUnits; - std::vector testTargets; - std::unordered_set srcObjectTargets; - std::string archiver = "ar"; - - std::string cxxFlags; - std::string defines; - std::string includes; - std::string ldFlags; - std::string libs; - - NinjaPlan ninjaPlan; + BuildGraph(BuildProfile buildProfile, std::string libName, Project project, + Compiler compiler); bool isUpToDate(std::string_view fileName) const; std::string mapHeaderToObj(const fs::path& headerPath) const; @@ -79,36 +84,9 @@ class BuildConfig { const std::unordered_set& dependencies, bool isTest); - explicit BuildConfig(BuildProfile buildProfile, std::string libName, - Project project, Compiler compiler) - : outBasePath(project.outBasePath), project(std::move(project)), - compiler(std::move(compiler)), buildProfile(std::move(buildProfile)), - libName(std::move(libName)), ninjaPlan(outBasePath) {} - -public: - enum class TestKind : std::uint8_t { Unit, Integration }; - - struct TestTarget { - std::string ninjaTarget; - std::string sourcePath; - TestKind kind = TestKind::Unit; - }; - - static Result - init(const Manifest& manifest, - const BuildProfile& buildProfile = BuildProfile::Dev); - - bool hasBinTarget() const { return hasBinaryTarget; } - bool hasLibTarget() const { return hasLibraryTarget; } - const std::string& getLibName() const { return libName; } - - bool ninjaIsUpToDate() const { return isUpToDate("build.ninja"); } - bool compdbIsUpToDate() const { return isUpToDate("compile_commands.json"); } - - const std::vector& getTestTargets() const { return testTargets; } - - Result installDeps(bool includeDevDeps); - void enableCoverage(); + Result runMM(const std::string& sourceFile, + bool isTest = false) const; + Result containsTestCode(const std::string& sourceFile) const; Result processSrc(const fs::path& sourceFilePath, const SourceRoot& root, @@ -130,24 +108,32 @@ class BuildConfig { const std::unordered_set& objTargetDeps, const std::unordered_set& buildObjTargets) const; - Result configureBuild(); + Result configure(); + void writeBuildFiles() const; - void emitCompdb(std::ostream& os) const; - Result runMM(const std::string& sourceFile, - bool isTest = false) const; - Result containsTestCode(const std::string& sourceFile) const; + fs::path outBasePath_; + Project project; + Compiler compiler; + BuildProfile buildProfile_; + std::string libName; - void writeBuildFiles() const; + bool hasBinaryTarget_{ false }; + bool hasLibraryTarget_{ false }; + + std::unordered_map compileUnits; + std::vector testTargets_; + std::unordered_set srcObjectTargets; + std::string archiver = "ar"; + + std::string cxxFlags; + std::string defines; + std::string includes; + std::string ldFlags; + std::string libs; + + NinjaPlan ninjaPlan; }; -Result emitNinja(const Manifest& manifest, - const BuildProfile& buildProfile, - bool includeDevDeps, bool enableCoverage = false); -Result emitCompdb(const Manifest& manifest, - const BuildProfile& buildProfile, - bool includeDevDeps); -Command getNinjaCommand(); -Result ninjaNeedsWork(const fs::path& outDir, - const std::vector& targets); +std::vector listSourceFilePaths(const fs::path& dir); } // namespace cabin diff --git a/include/Builder/DepGraph.hpp b/include/Builder/DepGraph.hpp new file mode 100644 index 000000000..e38940191 --- /dev/null +++ b/include/Builder/DepGraph.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "Builder/BuildGraph.hpp" +#include "Builder/BuildProfile.hpp" +#include "Manifest.hpp" +#include "Rustify/Result.hpp" + +#include +#include +#include + +namespace cabin { + +namespace fs = std::filesystem; + +class DepGraph { +public: + explicit DepGraph(fs::path rootPath) : rootPath(std::move(rootPath)) {} + + Result resolve(); + Result computeBuildGraph(const BuildProfile& buildProfile) const; + +private: + fs::path rootPath; + std::optional rootManifest; +}; + +} // namespace cabin diff --git a/include/Builder/SourceLayout.hpp b/include/Builder/SourceLayout.hpp new file mode 100644 index 000000000..f741cbdf2 --- /dev/null +++ b/include/Builder/SourceLayout.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace cabin { + +// clang-format off +inline const std::unordered_set SOURCE_FILE_EXTS{ + ".c", ".c++", ".cc", ".cpp", ".cxx" +}; +inline const std::unordered_set HEADER_FILE_EXTS{ + ".h", ".h++", ".hh", ".hpp", ".hxx" +}; +// clang-format on + +} // namespace cabin diff --git a/src/BuildConfig.cc b/lib/Builder/BuildGraph.cc similarity index 78% rename from src/BuildConfig.cc rename to lib/Builder/BuildGraph.cc index 99a9d3c41..d643bbffd 100644 --- a/src/BuildConfig.cc +++ b/lib/Builder/BuildGraph.cc @@ -1,11 +1,9 @@ -#include "BuildConfig.hpp" +#include "Builder/BuildGraph.hpp" #include "Algos.hpp" -#include "Builder/Compiler.hpp" #include "Command.hpp" #include "Diag.hpp" #include "Git2.hpp" -#include "Manifest.hpp" #include "Parallelism.hpp" #include @@ -20,9 +18,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -69,8 +64,8 @@ static std::string combineFlags(std::initializer_list parts) { return result; } -static std::unordered_set -parseMMOutput(const std::string& mmOutput, std::string& target) { +std::unordered_set parseMMOutput(const std::string& mmOutput, + std::string& target) { std::istringstream iss(mmOutput); std::getline(iss, target, ':'); @@ -93,7 +88,7 @@ parseMMOutput(const std::string& mmOutput, std::string& target) { return deps; } -static std::vector listSourceFilePaths(const fs::path& dir) { +std::vector listSourceFilePaths(const fs::path& dir) { std::vector sourceFilePaths; for (const auto& entry : fs::recursive_directory_iterator(dir)) { if (!SOURCE_FILE_EXTS.contains(entry.path().extension().string())) { @@ -105,10 +100,14 @@ static std::vector listSourceFilePaths(const fs::path& dir) { return sourceFilePaths; } -Result BuildConfig::init(const Manifest& manifest, - const BuildProfile& buildProfile) { - using std::string_view_literals::operator""sv; +BuildGraph::BuildGraph(BuildProfile buildProfileIn, std::string libNameIn, + Project projectIn, Compiler compilerIn) + : outBasePath_(projectIn.outBasePath), project(std::move(projectIn)), + compiler(std::move(compilerIn)), buildProfile_(std::move(buildProfileIn)), + libName(std::move(libNameIn)), ninjaPlan(outBasePath_) {} +Result BuildGraph::create(const Manifest& manifest, + const BuildProfile& buildProfile) { std::string libName; if (manifest.package.name.starts_with("lib")) { libName = fmt::format("{}.a", manifest.package.name); @@ -117,12 +116,12 @@ Result BuildConfig::init(const Manifest& manifest, } Project project = Try(Project::init(buildProfile, manifest)); - return Ok(BuildConfig(buildProfile, std::move(libName), std::move(project), - Try(Compiler::init()))); + return Ok(BuildGraph(buildProfile, std::move(libName), std::move(project), + Try(Compiler::init()))); } -bool BuildConfig::isUpToDate(const std::string_view fileName) const { - const fs::path filePath = outBasePath / fileName; +bool BuildGraph::isUpToDate(const std::string_view fileName) const { + const fs::path filePath = outBasePath_ / fileName; if (!fs::exists(filePath)) { return false; @@ -147,8 +146,8 @@ bool BuildConfig::isUpToDate(const std::string_view fileName) const { return fs::last_write_time(project.manifest.path) <= configTime; } -std::string BuildConfig::mapHeaderToObj(const fs::path& headerPath) const { - const fs::path objBase = fs::relative(project.buildOutPath, outBasePath); +std::string BuildGraph::mapHeaderToObj(const fs::path& headerPath) const { + const fs::path objBase = fs::relative(project.buildOutPath, outBasePath_); const auto makeObjPath = [&](const fs::path& relDir, const fs::path& prefix) { fs::path objPath = objBase; @@ -198,7 +197,7 @@ std::string BuildConfig::mapHeaderToObj(const fs::path& headerPath) const { return fallback.generic_string(); } -void BuildConfig::registerCompileUnit( +void BuildGraph::registerCompileUnit( const std::string& objTarget, const std::string& sourceFile, const std::unordered_set& dependencies, const bool isTest) { compileUnits[objTarget] = CompileUnit{ .source = sourceFile, @@ -216,7 +215,7 @@ void BuildConfig::registerCompileUnit( ninjaPlan.addEdge(std::move(edge)); } -void BuildConfig::writeBuildFiles() const { +void BuildGraph::writeBuildFiles() const { const NinjaToolchain toolchain{ .cxx = compiler.cxx, .cxxFlags = cxxFlags, @@ -230,18 +229,17 @@ void BuildConfig::writeBuildFiles() const { ninjaPlan.writeFiles(toolchain); } -Result BuildConfig::runMM(const std::string& sourceFile, - const bool isTest) const { +Result BuildGraph::runMM(const std::string& sourceFile, + const bool isTest) const { Command command = compiler.makeMMCmd(project.compilerOpts, sourceFile); if (isTest) { command.addArg("-DCABIN_TEST"); } - command.setWorkingDirectory(outBasePath); + command.setWorkingDirectory(outBasePath_); return getCmdOutput(command); } -Result -BuildConfig::containsTestCode(const std::string& sourceFile) const { +Result BuildGraph::containsTestCode(const std::string& sourceFile) const { std::ifstream ifs(sourceFile); std::string line; while (std::getline(ifs, line)) { @@ -266,9 +264,9 @@ BuildConfig::containsTestCode(const std::string& sourceFile) const { } Result -BuildConfig::processSrc(const fs::path& sourceFilePath, const SourceRoot& root, - std::unordered_set& buildObjTargets, - tbb::spin_mutex* mtx) { +BuildGraph::processSrc(const fs::path& sourceFilePath, const SourceRoot& root, + std::unordered_set& buildObjTargets, + tbb::spin_mutex* mtx) { std::string objTarget; const std::unordered_set objTargetDeps = parseMMOutput(Try(runMM(sourceFilePath)), objTarget); @@ -293,7 +291,7 @@ BuildConfig::processSrc(const fs::path& sourceFilePath, const SourceRoot& root, const fs::path objOutput = buildTargetBaseDir / objTarget; const std::string buildObjTarget = - fs::relative(objOutput, outBasePath).generic_string(); + fs::relative(objOutput, outBasePath_).generic_string(); if (mtx) { mtx->lock(); @@ -308,8 +306,8 @@ BuildConfig::processSrc(const fs::path& sourceFilePath, const SourceRoot& root, } Result> -BuildConfig::processSources(const std::vector& sourceFilePaths, - const SourceRoot& root) { +BuildGraph::processSources(const std::vector& sourceFilePaths, + const SourceRoot& root) { std::unordered_set buildObjTargets; if (isParallel()) { @@ -337,9 +335,9 @@ BuildConfig::processSources(const std::vector& sourceFilePaths, return Ok(buildObjTargets); } -Result> -BuildConfig::processUnittestSrc(const fs::path& sourceFilePath, - tbb::spin_mutex* mtx) { +Result> +BuildGraph::processUnittestSrc(const fs::path& sourceFilePath, + tbb::spin_mutex* mtx) { if (!Try(containsTestCode(sourceFilePath))) { return Ok(std::optional()); } @@ -444,7 +442,7 @@ BuildConfig::processUnittestSrc(const fs::path& sourceFilePath, linkInputs.insert(linkInputs.end(), srcDeps.begin(), srcDeps.end()); } - if (hasLibraryTarget) { + if (hasLibraryTarget_) { linkInputs.push_back(libName); } @@ -465,9 +463,9 @@ BuildConfig::processUnittestSrc(const fs::path& sourceFilePath, return Ok(std::optional(std::move(testTarget))); } -Result> -BuildConfig::processIntegrationTestSrc(const fs::path& sourceFilePath, - tbb::spin_mutex* mtx) { +Result> +BuildGraph::processIntegrationTestSrc(const fs::path& sourceFilePath, + tbb::spin_mutex* mtx) { std::string objTarget; const std::unordered_set objTargetDeps = parseMMOutput(Try(runMM(sourceFilePath, /*isTest=*/true)), objTarget); @@ -481,13 +479,13 @@ BuildConfig::processIntegrationTestSrc(const fs::path& sourceFilePath, const fs::path testObjOutput = testTargetBaseDir / objTarget; const std::string testObjTarget = - fs::relative(testObjOutput, outBasePath).generic_string(); + fs::relative(testObjOutput, outBasePath_).generic_string(); const fs::path testBinaryPath = testTargetBaseDir / sourceFilePath.stem(); const std::string testBinary = - fs::relative(testBinaryPath, outBasePath).generic_string(); + fs::relative(testBinaryPath, outBasePath_).generic_string(); std::vector linkInputs{ testObjTarget }; - if (hasLibraryTarget) { + if (hasLibraryTarget_) { linkInputs.push_back(libName); } std::ranges::sort(linkInputs); @@ -517,7 +515,7 @@ BuildConfig::processIntegrationTestSrc(const fs::path& sourceFilePath, return Ok(std::optional(std::move(testTarget))); } -void BuildConfig::collectBinDepObjs( +void BuildGraph::collectBinDepObjs( std::unordered_set& deps, const std::string_view sourceFileName, const std::unordered_set& objTargetDeps, @@ -548,7 +546,7 @@ void BuildConfig::collectBinDepObjs( } } -Result BuildConfig::installDeps(const bool includeDevDeps) { +Result BuildGraph::installDeps(const bool includeDevDeps) { const std::vector depsCompOpts = Try(project.manifest.installDeps(includeDevDeps)); @@ -558,21 +556,21 @@ Result BuildConfig::installDeps(const bool includeDevDeps) { return Ok(); } -void BuildConfig::enableCoverage() { +void BuildGraph::enableCoverage() { project.compilerOpts.cFlags.others.emplace_back("--coverage"); project.compilerOpts.ldFlags.others.emplace_back("--coverage"); } -Result BuildConfig::configureBuild() { +Result BuildGraph::configure() { const fs::path srcDir = project.rootPath / "src"; const bool hasSrcDir = fs::exists(srcDir); const fs::path libDir = project.rootPath / "lib"; - const Profile& profile = project.manifest.profiles.at(buildProfile); + const Profile& profile = project.manifest.profiles.at(buildProfile_); archiver = compiler.detectArchiver(profile.lto); - hasBinaryTarget = false; - hasLibraryTarget = false; + hasBinaryTarget_ = false; + hasLibraryTarget_ = false; const auto isMainSource = [](const fs::path& file) { return file.filename().stem() == "main"; @@ -592,17 +590,17 @@ Result BuildConfig::configureBuild() { Bail("multiple main sources were found"); } mainSource = path; - hasBinaryTarget = true; + hasBinaryTarget_ = true; } } - if (!fs::exists(outBasePath)) { - fs::create_directories(outBasePath); + if (!fs::exists(outBasePath_)) { + fs::create_directories(outBasePath_); } compileUnits.clear(); ninjaPlan.reset(); - testTargets.clear(); + testTargets_.clear(); cxxFlags = joinFlags(project.compilerOpts.cFlags.others); defines = joinFlags(project.compilerOpts.cFlags.macros); @@ -612,16 +610,18 @@ Result BuildConfig::configureBuild() { ldFlags = combineFlags({ ldOthers, libDirs }); libs = joinFlags(project.compilerOpts.ldFlags.libs); - const std::vector sourceFilePaths = - hasSrcDir ? listSourceFilePaths(srcDir) : std::vector{}; - for (const fs::path& sourceFilePath : sourceFilePaths) { - if (sourceFilePath != mainSource && isMainSource(sourceFilePath)) { - Diag::warn( - "source file `{}` is named `main` but is not located directly in the " - "`src/` directory. " - "This file will not be treated as the program's entry point. " - "Move it directly to 'src/' if intended as such.", - sourceFilePath.string()); + std::vector sourceFilePaths; + if (hasSrcDir) { + sourceFilePaths = listSourceFilePaths(srcDir); + for (const fs::path& sourceFilePath : sourceFilePaths) { + if (sourceFilePath != mainSource && isMainSource(sourceFilePath)) { + Diag::warn( + "source file `{}` is named `main` but is not located directly in " + "the `src/` directory. " + "This file will not be treated as the program's entry point. " + "Move it directly to 'src/' if intended as such.", + sourceFilePath.string()); + } } } @@ -629,9 +629,9 @@ Result BuildConfig::configureBuild() { if (fs::exists(libDir)) { publicSourceFilePaths = listSourceFilePaths(libDir); } - hasLibraryTarget = !publicSourceFilePaths.empty(); + hasLibraryTarget_ = !publicSourceFilePaths.empty(); - if (!hasBinaryTarget && !hasLibraryTarget) { + if (!hasBinaryTarget_ && !hasLibraryTarget_) { Bail("expected either `src/main{}` or at least one source file under " "`lib/` matching {}", SOURCE_FILE_EXTS, SOURCE_FILE_EXTS); @@ -655,10 +655,10 @@ Result BuildConfig::configureBuild() { std::unordered_set buildObjTargets = srcObjTargets; buildObjTargets.insert(libObjTargets.begin(), libObjTargets.end()); - if (hasBinaryTarget) { + if (hasBinaryTarget_) { const fs::path mainObjPath = project.buildOutPath / "main.o"; const std::string mainObj = - fs::relative(mainObjPath, outBasePath).generic_string(); + fs::relative(mainObjPath, outBasePath_).generic_string(); Ensure(compileUnits.contains(mainObj), "internal error: missing compile unit for {}", mainObj); @@ -667,7 +667,7 @@ Result BuildConfig::configureBuild() { buildObjTargets); std::vector inputs; - if (hasLibraryTarget) { + if (hasLibraryTarget_) { deps.erase(mainObj); std::vector srcInputs; srcInputs.reserve(deps.size()); @@ -697,7 +697,7 @@ Result BuildConfig::configureBuild() { ninjaPlan.addDefaultTarget(project.manifest.package.name); } - if (hasLibraryTarget) { + if (hasLibraryTarget_) { std::vector libraryInputs; libraryInputs.reserve(libObjTargets.size()); for (const std::string& obj : libObjTargets) { @@ -717,7 +717,7 @@ Result BuildConfig::configureBuild() { ninjaPlan.addDefaultTarget(libName); } - if (buildProfile == BuildProfile::Test) { + if (buildProfile_ == BuildProfile::Test) { std::vector discoveredTests; discoveredTests.reserve(sourceFilePaths.size()); for (const fs::path& sourceFilePath : sourceFilePaths) { @@ -750,23 +750,32 @@ Result BuildConfig::configureBuild() { [](const TestTarget& lhs, const TestTarget& rhs) { return lhs.ninjaTarget < rhs.ninjaTarget; }); - testTargets = std::move(discoveredTests); + testTargets_ = std::move(discoveredTests); std::vector testTargetNames; - testTargetNames.reserve(testTargets.size()); - for (const TestTarget& target : testTargets) { + testTargetNames.reserve(testTargets_.size()); + for (const TestTarget& target : testTargets_) { testTargetNames.push_back(target.ninjaTarget); } ninjaPlan.setTestTargets(std::move(testTargetNames)); } else { - testTargets.clear(); + testTargets_.clear(); ninjaPlan.setTestTargets({}); } return Ok(); } -static Result generateCompdb(const fs::path& outDir) { +Result BuildGraph::writeBuildFilesIfNeeded() const { + if (isUpToDate("build.ninja")) { + return Ok(); + } + writeBuildFiles(); + return Ok(); +} + +Result BuildGraph::generateCompdb() const { + const fs::path& outDir = outBasePath_; const fs::path cabinOutRoot = outDir.parent_path(); std::vector buildDirs{ outDir }; @@ -830,57 +839,44 @@ static Result generateCompdb(const fs::path& outDir) { return Ok(); } -Result emitNinja(const Manifest& manifest, - const BuildProfile& buildProfile, - const bool includeDevDeps, - const bool enableCoverage) { - Diag::info("Analyzing", "project dependencies..."); - - auto config = Try(BuildConfig::init(manifest, buildProfile)); - Try(config.installDeps(includeDevDeps)); - if (enableCoverage) { - config.enableCoverage(); +Result BuildGraph::plan() { + static bool loggedAnalysis = false; + if (!loggedAnalysis) { + Diag::info("Analyzing", "project dependencies..."); + loggedAnalysis = true; } - const bool buildProj = !config.ninjaIsUpToDate(); + + const bool buildProj = !isUpToDate("build.ninja"); spdlog::debug("build.ninja is {}up to date", buildProj ? "NOT " : ""); - Try(config.configureBuild()); + Try(configure()); if (buildProj) { - config.writeBuildFiles(); + writeBuildFiles(); } - Try(generateCompdb(config.outBasePath)); + Try(generateCompdb()); - return Ok(config); -} - -Result emitCompdb(const Manifest& manifest, - const BuildProfile& buildProfile, - const bool includeDevDeps) { - auto config = Try(emitNinja(manifest, buildProfile, includeDevDeps, - /*enableCoverage=*/false)); - return Ok(config.outBasePath.parent_path().string()); + return Ok(); } -static Command makeNinjaCommand(const bool forDryRun) { - Command ninjaCommand("ninja"); +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +Command BuildGraph::ninjaCommand(const bool forDryRun) const { + Command ninja("ninja"); if (!isVerbose() && !forDryRun) { - ninjaCommand.addArg("--quiet"); + ninja.addArg("--quiet"); } else if (isVeryVerbose()) { - ninjaCommand.addArg("--verbose"); + ninja.addArg("--verbose"); } const std::size_t numThreads = getParallelism(); - ninjaCommand.addArg(fmt::format("-j{}", numThreads)); + ninja.addArg(fmt::format("-j{}", numThreads)); - return ninjaCommand; + return ninja; } -Command getNinjaCommand() { return makeNinjaCommand(false); } - -Result ninjaNeedsWork(const fs::path& outDir, - const std::vector& targets) { - Command dryRunCmd = makeNinjaCommand(true); - dryRunCmd.addArg("-C").addArg(outDir.string()).addArg("-n"); +Result +BuildGraph::needsBuild(const std::vector& targets) const { + Command dryRunCmd = ninjaCommand(true); + dryRunCmd.addArg("-C").addArg(outBasePath_.string()).addArg("-n"); for (const std::string& target : targets) { dryRunCmd.addArg(target); } @@ -891,62 +887,22 @@ Result ninjaNeedsWork(const fs::path& outDir, return Ok(!hasNoWork || !dryRun.exitStatus.success()); } -} // namespace cabin - -#ifdef CABIN_TEST - -# include "Rustify/Tests.hpp" - -namespace tests { - -using namespace cabin; // NOLINT(build/namespaces,google-build-using-namespace) - -static void testJoinFlags() { - const std::vector flags{ "-Ifoo", "-Ibar" }; - assertEq(joinFlags(flags), "-Ifoo -Ibar"); - - const std::vector empty; - assertEq(joinFlags(empty), ""); - - pass(); -} - -static void testCombineFlags() { - const std::string combined = combineFlags({ "-O2", "", "-fno-rtti", "-g" }); - assertEq(combined, "-O2 -fno-rtti -g"); - - pass(); -} - -static void testParentDirOrDot() { - assertEq(parentDirOrDot("objs/main.o"), "objs"); - assertEq(parentDirOrDot("main.o"), "."); - - pass(); -} - -static void testParseMMOutput() { - const std::string input = - "main.o: src/main.cc include/foo.hpp include/bar.hpp \\\n" - " include/baz.hh\n"; - std::string target; - const auto deps = parseMMOutput(input, target); - - assertEq(target, "main.o"); - assertTrue(deps.contains("include/foo.hpp")); - assertTrue(deps.contains("include/bar.hpp")); - assertTrue(deps.contains("include/baz.hh")); - - pass(); -} +Result +BuildGraph::buildTargets(const std::vector& targets, + const std::string_view displayName) const { + Command buildCmd = ninjaCommand(false); + buildCmd.addArg("-C").addArg(outBasePath_.string()); + for (const std::string& target : targets) { + buildCmd.addArg(target); + } -} // namespace tests + if (Try(needsBuild(targets))) { + Diag::info("Compiling", "{} v{} ({})", displayName, + project.manifest.package.version.toString(), + project.manifest.path.parent_path().string()); + } -int main() { - tests::testJoinFlags(); - tests::testCombineFlags(); - tests::testParentDirOrDot(); - tests::testParseMMOutput(); + return execCmd(buildCmd); } -#endif +} // namespace cabin diff --git a/lib/Builder/DepGraph.cc b/lib/Builder/DepGraph.cc new file mode 100644 index 000000000..984d0ac60 --- /dev/null +++ b/lib/Builder/DepGraph.cc @@ -0,0 +1,18 @@ +#include "Builder/DepGraph.hpp" + +#include "Manifest.hpp" + +namespace cabin { + +Result DepGraph::resolve() { + rootManifest.emplace(Try(Manifest::tryParse(rootPath / Manifest::FILE_NAME))); + return Ok(); +} + +Result +DepGraph::computeBuildGraph(const BuildProfile& buildProfile) const { + Ensure(rootManifest.has_value(), "dependency graph not resolved"); + return BuildGraph::create(*rootManifest, buildProfile); +} + +} // namespace cabin diff --git a/src/Builder/Builder.cc b/src/Builder/Builder.cc new file mode 100644 index 000000000..6113707e5 --- /dev/null +++ b/src/Builder/Builder.cc @@ -0,0 +1,182 @@ +#include "Builder/Builder.hpp" + +#include "Algos.hpp" +#include "Command.hpp" +#include "Diag.hpp" +#include "Parallelism.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace cabin { + +Builder::Builder(fs::path rootPath, BuildProfile buildProfile) + : basePath(std::move(rootPath)), buildProfile(std::move(buildProfile)), + depGraph(basePath) {} + +Result Builder::schedule(const ScheduleOptions& options) { + this->options = options; + + Try(depGraph.resolve()); + graphState.emplace(Try(depGraph.computeBuildGraph(buildProfile))); + + if (options.enableCoverage) { + graphState->enableCoverage(); + } + Try(graphState->installDeps(options.includeDevDeps)); + Try(graphState->plan()); + outDir = graphState->outBasePath(); + return Ok(); +} + +Result Builder::ensurePlanned() const { + Ensure(graphState.has_value(), "builder.schedule() must be called first"); + return Ok(); +} + +Result Builder::build() { + Try(ensurePlanned()); + const auto startBuild = std::chrono::steady_clock::now(); + + ExitStatus status(EXIT_SUCCESS); + const Manifest& mf = graphState->manifest(); + + if (graphState->hasLibraryTarget()) { + status = + Try(graphState->buildTargets({ graphState->libraryName() }, + fmt::format("{}(lib)", mf.package.name))); + } + + if (status.success() && graphState->hasBinaryTarget()) { + status = + Try(graphState->buildTargets({ mf.package.name }, mf.package.name)); + } + + const auto endBuild = std::chrono::steady_clock::now(); + const std::chrono::duration buildElapsed = endBuild - startBuild; + + const Profile& profile = mf.profiles.at(buildProfile); + Ensure(status.success(), "build failed"); + Diag::info("Finished", "`{}` profile [{}] target(s) in {:.2f}s", buildProfile, + profile, buildElapsed.count()); + return Ok(); +} + +Result Builder::test() { + Try(ensurePlanned()); + + const Manifest& mf = graphState->manifest(); + const std::vector& targets = + graphState->testTargets(); + + const auto buildStart = std::chrono::steady_clock::now(); + ExitStatus status(EXIT_SUCCESS); + + if (graphState->hasLibraryTarget()) { + status = + Try(graphState->buildTargets({ graphState->libraryName() }, + fmt::format("{}(lib)", mf.package.name))); + Ensure(status.success(), "build failed"); + } + + if (!targets.empty()) { + std::vector names; + names.reserve(targets.size()); + for (const auto& target : targets) { + names.push_back(target.ninjaTarget); + } + status = Try(graphState->buildTargets( + names, fmt::format("{}(test)", mf.package.name))); + Ensure(status.success(), "build failed"); + } else { + Diag::warn("No test targets found"); + return Ok(); + } + + const auto buildEnd = std::chrono::steady_clock::now(); + const std::chrono::duration buildElapsed = buildEnd - buildStart; + const Profile& profile = mf.profiles.at(buildProfile); + Diag::info("Finished", "`{}` profile [{}] target(s) in {:.2f}s", buildProfile, + profile, buildElapsed.count()); + + const auto runStart = std::chrono::steady_clock::now(); + + std::size_t numPassed = 0; + std::size_t numFailed = 0; + ExitStatus summaryStatus(EXIT_SUCCESS); + + const auto labelFor = [](BuildGraph::TestKind kind) { + switch (kind) { + case BuildGraph::TestKind::Integration: + return std::string_view("integration"); + case BuildGraph::TestKind::Unit: + return std::string_view("unit"); + } + std::unreachable(); + }; + + for (const auto& target : targets) { + const fs::path absoluteBinary = outDir / target.ninjaTarget; + const std::string testBinPath = + fs::relative(absoluteBinary, mf.path.parent_path()).string(); + Diag::info("Running", "{} test {} ({})", labelFor(target.kind), + target.sourcePath, testBinPath); + + const ExitStatus curExitStatus = + Try(execCmd(Command(absoluteBinary.string()))); + if (curExitStatus.success()) { + ++numPassed; + } else { + ++numFailed; + summaryStatus = curExitStatus; + } + } + + const auto runEnd = std::chrono::steady_clock::now(); + const std::chrono::duration runElapsed = runEnd - runStart; + + const std::string summary = + fmt::format("{} passed; {} failed; finished in {:.2f}s", numPassed, + numFailed, runElapsed.count()); + if (!summaryStatus.success()) { + return Err(anyhow::anyhow(summary)); + } + Diag::info("Ok", "{}", summary); + return Ok(); +} + +Result Builder::run(const std::vector& args) { + Try(build()); + + const Manifest& mf = graphState->manifest(); + Diag::info("Running", "`{}/{}`", + fs::relative(outDir, mf.path.parent_path()).string(), + mf.package.name); + const Command command((outDir / mf.package.name).string(), args); + const ExitStatus exitStatus = Try(execCmd(command)); + if (exitStatus.success()) { + return Ok(); + } + Bail("run {}", exitStatus); +} + +const BuildGraph& Builder::graph() const { + if (!graphState) { + throw std::logic_error("builder.schedule() must be called first"); + } + return *graphState; +} + +std::string Builder::compdbRoot() const { + if (!graphState) { + throw std::logic_error("builder.schedule() must be called first"); + } + return outDir.parent_path().string(); +} + +} // namespace cabin diff --git a/src/Builder/Builder.hpp b/src/Builder/Builder.hpp new file mode 100644 index 000000000..b652c3282 --- /dev/null +++ b/src/Builder/Builder.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "Builder/BuildGraph.hpp" +#include "Builder/BuildProfile.hpp" +#include "Builder/DepGraph.hpp" +#include "Rustify/Result.hpp" + +#include +#include +#include +#include + +namespace cabin { + +namespace fs = std::filesystem; + +struct ScheduleOptions { + bool includeDevDeps = false; + bool enableCoverage = false; +}; + +class Builder { +public: + Builder(fs::path rootPath, BuildProfile buildProfile); + + Result schedule(const ScheduleOptions& options = {}); + Result build(); + Result test(); + Result run(const std::vector& args); + + const BuildGraph& graph() const; + const fs::path& outDirPath() const { return outDir; } + std::string compdbRoot() const; + +private: + fs::path basePath; + BuildProfile buildProfile; + ScheduleOptions options; + + DepGraph depGraph; + std::optional graphState; + fs::path outDir; + + Result ensurePlanned() const; +}; + +} // namespace cabin diff --git a/src/Cmd/Build.cc b/src/Cmd/Build.cc index f7ece8030..107c22ae9 100644 --- a/src/Cmd/Build.cc +++ b/src/Cmd/Build.cc @@ -1,10 +1,9 @@ #include "Build.hpp" #include "Algos.hpp" -#include "BuildConfig.hpp" #include "Builder/BuildProfile.hpp" +#include "Builder/Builder.hpp" #include "Cli.hpp" -#include "Command.hpp" #include "Common.hpp" #include "Diag.hpp" #include "Manifest.hpp" @@ -14,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -35,62 +35,6 @@ const Subcmd BUILD_CMD = .addOpt(OPT_JOBS) .setMainFn(buildMain); -Result runBuildCommand(const Manifest& manifest, - const std::string& outDir, - const std::string& targetName, - std::string displayName) { - const std::vector targets{ targetName }; - const bool needsBuild = Try(ninjaNeedsWork(outDir, targets)); - - Command baseCmd = getNinjaCommand(); - baseCmd.addArg("-C").addArg(outDir); - - ExitStatus exitStatus(EXIT_SUCCESS); - if (needsBuild) { - Diag::info("Compiling", "{} v{} ({})", displayName, - manifest.package.version.toString(), - manifest.path.parent_path().string()); - Command buildCmd(baseCmd); - buildCmd.addArg(targetName); - exitStatus = Try(execCmd(buildCmd)); - } - return Ok(exitStatus); -} - -Result buildImpl(const Manifest& manifest, std::string& outDir, - const BuildProfile& buildProfile) { - const auto start = std::chrono::steady_clock::now(); - - const BuildConfig config = - Try(emitNinja(manifest, buildProfile, /*includeDevDeps=*/false)); - outDir = config.outBasePath; - - ExitStatus exitStatus(EXIT_SUCCESS); - if (config.hasLibTarget()) { - const std::string& libName = config.getLibName(); - exitStatus = - Try(runBuildCommand(manifest, outDir, libName, - fmt::format("{}(lib)", manifest.package.name))); - } - - if (config.hasBinTarget() && exitStatus.success()) { - exitStatus = Try(runBuildCommand(manifest, outDir, manifest.package.name, - manifest.package.name)); - } - - const auto end = std::chrono::steady_clock::now(); - const std::chrono::duration elapsed = end - start; - - if (exitStatus.success()) { - const Profile& profile = manifest.profiles.at(buildProfile); - Diag::info("Finished", "`{}` profile [{}] target(s) in {:.2f}s", - buildProfile, profile, elapsed.count()); - return Ok(); - } else { - return Err(anyhow::anyhow("build failed")); - } -} - static Result buildMain(const CliArgsView args) { // Parse args BuildProfile buildProfile = BuildProfile::Dev; @@ -123,18 +67,18 @@ static Result buildMain(const CliArgsView args) { } } - const auto manifest = Try(Manifest::tryParse()); - if (!buildCompdb) { - std::string outDir; - return buildImpl(manifest, outDir, buildProfile); + const Manifest manifest = Try(Manifest::tryParse()); + Builder builder(manifest.path.parent_path(), buildProfile); + Try(builder.schedule()); + + if (buildCompdb) { + Diag::info("Generated", "{}/compile_commands.json", + fs::relative(builder.compdbRoot(), manifest.path.parent_path()) + .string()); + return Ok(); } - // Build compilation database - const std::string outDir = - Try(emitCompdb(manifest, buildProfile, /*includeDevDeps=*/false)); - Diag::info("Generated", "{}/compile_commands.json", - fs::relative(outDir, manifest.path.parent_path()).string()); - return Ok(); + return builder.build(); } } // namespace cabin diff --git a/src/Cmd/Build.hpp b/src/Cmd/Build.hpp index a07aadc9d..26e924340 100644 --- a/src/Cmd/Build.hpp +++ b/src/Cmd/Build.hpp @@ -10,7 +10,5 @@ namespace cabin { extern const Subcmd BUILD_CMD; -Result buildImpl(const Manifest& manifest, std::string& outDir, - const BuildProfile& profile); } // namespace cabin diff --git a/src/Cmd/Fmt.cc b/src/Cmd/Fmt.cc index cd9615e98..448451c1f 100644 --- a/src/Cmd/Fmt.cc +++ b/src/Cmd/Fmt.cc @@ -1,7 +1,7 @@ #include "Fmt.hpp" #include "Algos.hpp" -#include "BuildConfig.hpp" +#include "Builder/SourceLayout.hpp" #include "Cli.hpp" #include "Diag.hpp" #include "Git2/Exception.hpp" diff --git a/src/Cmd/Run.cc b/src/Cmd/Run.cc index c3c6b45e4..2587de1bf 100644 --- a/src/Cmd/Run.cc +++ b/src/Cmd/Run.cc @@ -1,8 +1,8 @@ #include "Run.hpp" #include "Algos.hpp" -#include "Build.hpp" #include "Builder/BuildProfile.hpp" +#include "Builder/Builder.hpp" #include "Cli.hpp" #include "Command.hpp" #include "Common.hpp" @@ -73,13 +73,16 @@ static Result runMain(const CliArgsView args) { } const auto manifest = Try(Manifest::tryParse()); - std::string outDir; - Try(buildImpl(manifest, outDir, buildProfile)); + Builder builder(manifest.path.parent_path(), buildProfile); + Try(builder.schedule()); + Try(builder.build()); - Diag::info("Running", "`{}/{}`", - fs::relative(outDir, manifest.path.parent_path()).string(), - manifest.package.name); - const Command command(outDir + "/" + manifest.package.name, runArgs); + Diag::info( + "Running", "`{}/{}`", + fs::relative(builder.outDirPath(), manifest.path.parent_path()).string(), + manifest.package.name); + const Command command((builder.outDirPath() / manifest.package.name).string(), + runArgs); const ExitStatus exitStatus = Try(execCmd(command)); if (exitStatus.success()) { return Ok(); diff --git a/src/Cmd/Test.cc b/src/Cmd/Test.cc index ccf5a2713..19b02fc19 100644 --- a/src/Cmd/Test.cc +++ b/src/Cmd/Test.cc @@ -1,10 +1,9 @@ #include "Test.hpp" #include "Algos.hpp" -#include "BuildConfig.hpp" #include "Builder/BuildProfile.hpp" +#include "Builder/Builder.hpp" #include "Cli.hpp" -#include "Command.hpp" #include "Common.hpp" #include "Diag.hpp" #include "Manifest.hpp" @@ -12,33 +11,14 @@ #include "Rustify/Result.hpp" #include -#include #include -#include -#include -#include #include #include #include -#include -#include namespace cabin { -class Test { - Manifest manifest; - fs::path outDir; - std::vector collectedTargets; - bool enableCoverage = false; - - explicit Test(Manifest manifest) : manifest(std::move(manifest)) {} - - Result compileTestTargets(); - Result runTestTargets(); - -public: - static Result exec(CliArgsView cliArgs); -}; +static Result testMain(CliArgsView args); const Subcmd TEST_CMD = // Subcmd{ "test" } @@ -46,131 +26,23 @@ const Subcmd TEST_CMD = // .setDesc("Run the tests of a local package") .addOpt(OPT_JOBS) .addOpt(Opt{ "--coverage" }.setDesc("Enable code coverage analysis")) - .setMainFn(Test::exec); - -Result Test::compileTestTargets() { - const auto start = std::chrono::steady_clock::now(); - - const BuildProfile buildProfile = BuildProfile::Test; - const BuildConfig config = Try(emitNinja( - manifest, buildProfile, /*includeDevDeps=*/true, enableCoverage)); - outDir = config.outBasePath; - collectedTargets = config.getTestTargets(); - - if (collectedTargets.empty()) { - Diag::warn("No test targets found"); - return Ok(); - } - - std::vector testTargets; - testTargets.reserve(collectedTargets.size()); - for (const BuildConfig::TestTarget& target : collectedTargets) { - testTargets.push_back(target.ninjaTarget); - } - - Command baseCmd = getNinjaCommand(); - baseCmd.addArg("-C").addArg(outDir.string()); - const auto buildTargets = [&](const std::vector& targets, - std::string_view label) -> Result { - if (targets.empty()) { - return Ok(); - } - if (!Try(ninjaNeedsWork(outDir, targets))) { - return Ok(); - } - - Diag::info("Compiling", "{} v{} ({})", - fmt::format("{}({})", manifest.package.name, label), - manifest.package.version.toString(), - manifest.path.parent_path().string()); - - Command buildCmd(baseCmd); - for (const std::string& target : targets) { - buildCmd.addArg(target); - } - - const ExitStatus exitStatus = Try(execCmd(buildCmd)); - Ensure(exitStatus.success(), "compilation failed"); - return Ok(); - }; - - if (config.hasLibTarget()) { - const std::vector libTargets{ config.getLibName() }; - Try(buildTargets(libTargets, "lib")); - } - - Try(buildTargets(testTargets, "test")); + .setMainFn(testMain); - const auto end = std::chrono::steady_clock::now(); - const std::chrono::duration elapsed = end - start; - - const Profile& profile = manifest.profiles.at(buildProfile); - Diag::info("Finished", "`{}` profile [{}] target(s) in {:.2f}s", buildProfile, - profile, elapsed.count()); - - return Ok(); -} - -Result Test::runTestTargets() { - const auto start = std::chrono::steady_clock::now(); - - std::size_t numPassed = 0; - std::size_t numFailed = 0; - ExitStatus exitStatus; - const auto labelFor = [](BuildConfig::TestKind kind) { - switch (kind) { - case BuildConfig::TestKind::Integration: - return std::string_view("integration"); - case BuildConfig::TestKind::Unit: - default: - return std::string_view("unit"); - } - }; - - for (const BuildConfig::TestTarget& target : collectedTargets) { - const fs::path absoluteBinary = outDir / target.ninjaTarget; - const std::string testBinPath = - fs::relative(absoluteBinary, manifest.path.parent_path()).string(); - Diag::info("Running", "{} test {} ({})", labelFor(target.kind), - target.sourcePath, testBinPath); - - const ExitStatus curExitStatus = - Try(execCmd(Command(absoluteBinary.string()))); - if (curExitStatus.success()) { - ++numPassed; - } else { - ++numFailed; - exitStatus = curExitStatus; - } - } - - const auto end = std::chrono::steady_clock::now(); - const std::chrono::duration elapsed = end - start; - - // TODO: collect stdout/err's of failed tests and print them here. - const std::string summary = - fmt::format("{} passed; {} failed; finished in {:.2f}s", numPassed, - numFailed, elapsed.count()); - if (!exitStatus.success()) { - return Err(anyhow::anyhow(summary)); - } - Diag::info("Ok", "{}", summary); - return Ok(); -} - -Result Test::exec(const CliArgsView cliArgs) { +static Result testMain(const CliArgsView args) { bool enableCoverage = false; - for (auto itr = cliArgs.begin(); itr != cliArgs.end(); ++itr) { + for (auto itr = args.begin(); itr != args.end(); ++itr) { const std::string_view arg = *itr; - const auto control = Try(Cli::handleGlobalOpts(itr, cliArgs.end(), "test")); + const auto control = Try(Cli::handleGlobalOpts(itr, args.end(), "test")); if (control == Cli::Return) { return Ok(); - } else if (control == Cli::Continue) { + } + if (control == Cli::Continue) { continue; - } else if (matchesAny(arg, { "-j", "--jobs" })) { - if (itr + 1 == cliArgs.end()) { + } + if (matchesAny(arg, { "-j", "--jobs" })) { + if (itr + 1 == args.end()) { return Subcmd::missingOptArgumentFor(arg); } const std::string_view nextArg = *++itr; @@ -187,17 +59,11 @@ Result Test::exec(const CliArgsView cliArgs) { } } - Manifest manifest = Try(Manifest::tryParse()); - Test cmd(std::move(manifest)); - cmd.enableCoverage = enableCoverage; - - Try(cmd.compileTestTargets()); - if (cmd.collectedTargets.empty()) { - return Ok(); - } - - Try(cmd.runTestTargets()); - return Ok(); + const Manifest manifest = Try(Manifest::tryParse()); + Builder builder(manifest.path.parent_path(), BuildProfile::Test); + Try(builder.schedule(ScheduleOptions{ .includeDevDeps = true, + .enableCoverage = enableCoverage })); + return builder.test(); } } // namespace cabin diff --git a/src/Cmd/Tidy.cc b/src/Cmd/Tidy.cc index 416ccaacb..f14fd8cdc 100644 --- a/src/Cmd/Tidy.cc +++ b/src/Cmd/Tidy.cc @@ -1,15 +1,17 @@ #include "Tidy.hpp" #include "Algos.hpp" -#include "BuildConfig.hpp" #include "Builder/BuildProfile.hpp" +#include "Builder/Builder.hpp" #include "Cli.hpp" #include "Command.hpp" #include "Common.hpp" #include "Diag.hpp" +#include "Manifest.hpp" #include "Parallelism.hpp" #include "Rustify/Result.hpp" +#include #include #include #include @@ -83,8 +85,19 @@ static Result tidyMain(const CliArgsView args) { const auto manifest = Try(Manifest::tryParse()); const fs::path projectRoot = manifest.path.parent_path(); - const std::string compdbDir = - Try(emitCompdb(manifest, BuildProfile::Dev, /*includeDevDeps=*/false)); + + // Generate compile_commands for the dev and test profiles so tidy sees both + // normal and test builds. + std::string compdbDir; + const std::array profiles{ BuildProfile::Dev, + BuildProfile::Test }; + for (const BuildProfile& profile : profiles) { + Builder builder(projectRoot, profile); + const bool includeDevDeps = (profile == BuildProfile::Test); + Try(builder.schedule(ScheduleOptions{ .includeDevDeps = includeDevDeps, + .enableCoverage = false })); + compdbDir = builder.compdbRoot(); + } std::string runClangTidy = "run-clang-tidy"; if (const char* tidyEnv = std::getenv("CABIN_TIDY")) { diff --git a/tests/test.cc b/tests/test.cc index c63e7eb19..9c3cb865c 100644 --- a/tests/test.cc +++ b/tests/test.cc @@ -234,4 +234,25 @@ int main() { return 0; } const auto testBinary = project / "cabin-out" / "test" / "intg" / "smoke"; expect(tests::fs::is_regular_file(testBinary)); }; + + "cabin test library-only"_test = [] { + const tests::TempDir tmp; + tests::runCabin({ "new", "--lib", "lib_only" }, tmp.path).unwrap(); + const auto project = tmp.path / "lib_only"; + tests::fs::remove_all(project / "src"); + tests::writeFile(project / "lib" / "lib.cc", + R"(int libFunction() { return 1; } + +#ifdef CABIN_TEST +int main() { + return libFunction() == 1 ? 0 : 1; +} +#endif +)"); + + const auto result = tests::runCabin({ "test" }, project).unwrap(); + expect(result.status.success()) << result.status.toString(); + const auto outDir = project / "cabin-out" / "test" / "unit" / "lib"; + expect(tests::fs::is_regular_file(outDir / "lib.cc.test")); + }; }