From 0e774ad1af1e444989deff902db6e6915fbf8e99 Mon Sep 17 00:00:00 2001 From: Ken Matsui <26405363+ken-matsui@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:55:02 -0500 Subject: [PATCH] chore: add NinjaPlan --- include/Builder/NinjaPlan.hpp | 51 +++++++++++++++ lib/Builder/NinjaPlan.cc | 119 +++++++++++++++++++++++++++++++++ src/BuildConfig.cc | 120 ++++++++-------------------------- src/BuildConfig.hpp | 21 ++---- 4 files changed, 200 insertions(+), 111 deletions(-) create mode 100644 include/Builder/NinjaPlan.hpp create mode 100644 lib/Builder/NinjaPlan.cc diff --git a/include/Builder/NinjaPlan.hpp b/include/Builder/NinjaPlan.hpp new file mode 100644 index 000000000..956f7df82 --- /dev/null +++ b/include/Builder/NinjaPlan.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +namespace cabin { + +struct NinjaEdge { + std::vector outputs; + std::string rule; + std::vector inputs; + std::vector implicitInputs; + std::vector orderOnlyInputs; + std::vector> bindings; +}; + +struct NinjaToolchain { + std::string cxx; + std::string cxxFlags; + std::string defines; + std::string includes; + std::string ldFlags; + std::string libs; + std::string archiver; +}; + +class NinjaPlan { +public: + explicit NinjaPlan(std::filesystem::path outBasePath); + + void reset(); + void addEdge(NinjaEdge edge); + void addDefaultTarget(std::string target); + void setTestTargets(std::vector testTargets); + void writeFiles(const NinjaToolchain& toolchain) const; + +private: + void writeBuildNinja() const; + void writeConfigNinja(const NinjaToolchain& toolchain) const; + void writeRulesNinja() const; + void writeTargetsNinja() const; + + std::filesystem::path outBasePath_; + std::vector edges_; + std::vector defaultTargets_; + std::vector testTargets_; +}; + +} // namespace cabin diff --git a/lib/Builder/NinjaPlan.cc b/lib/Builder/NinjaPlan.cc new file mode 100644 index 000000000..486c9facd --- /dev/null +++ b/lib/Builder/NinjaPlan.cc @@ -0,0 +1,119 @@ +#include "Builder/NinjaPlan.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cabin { + +template +static std::string joinWithSpace(const Range& range) { + if (range.empty()) { + return ""; + } + return fmt::format("{}", fmt::join(range, " ")); +} + +NinjaPlan::NinjaPlan(std::filesystem::path outBasePath) + : outBasePath_(std::move(outBasePath)) {} + +void NinjaPlan::reset() { + edges_.clear(); + defaultTargets_.clear(); + testTargets_.clear(); +} + +void NinjaPlan::addEdge(NinjaEdge edge) { edges_.push_back(std::move(edge)); } + +void NinjaPlan::addDefaultTarget(std::string target) { + defaultTargets_.push_back(std::move(target)); +} + +void NinjaPlan::setTestTargets(std::vector testTargets) { + testTargets_ = std::move(testTargets); +} + +void NinjaPlan::writeFiles(const NinjaToolchain& toolchain) const { + writeBuildNinja(); + writeConfigNinja(toolchain); + writeRulesNinja(); + writeTargetsNinja(); +} + +void NinjaPlan::writeBuildNinja() const { + std::ofstream buildFile(outBasePath_ / "build.ninja"); + buildFile << "# Generated by Cabin\n"; + buildFile << "ninja_required_version = 1.11\n\n"; + buildFile << "include config.ninja\n"; + buildFile << "include rules.ninja\n"; + buildFile << "include targets.ninja\n\n"; + if (!defaultTargets_.empty()) { + buildFile << "default " << joinWithSpace(defaultTargets_) << '\n'; + } +} + +void NinjaPlan::writeConfigNinja(const NinjaToolchain& toolchain) const { + std::ofstream cfg(outBasePath_ / "config.ninja"); + cfg << "# Build variables\n"; + cfg << "CXX = " << toolchain.cxx << '\n'; + cfg << "CXXFLAGS = " << toolchain.cxxFlags << '\n'; + cfg << "DEFINES = " << toolchain.defines << '\n'; + cfg << "INCLUDES = " << toolchain.includes << '\n'; + cfg << "LDFLAGS = " << toolchain.ldFlags << '\n'; + cfg << "LIBS = " << toolchain.libs << '\n'; + cfg << "AR = " << toolchain.archiver << '\n'; +} + +void NinjaPlan::writeRulesNinja() const { + std::ofstream rules(outBasePath_ / "rules.ninja"); + + rules << "rule cxx_compile\n"; + rules << " command = $CXX $DEFINES $INCLUDES $CXXFLAGS $extra_flags -c $in " + "-o $out\n"; + rules << " description = Building CXX object $out\n\n"; + + rules << "rule cxx_link_exe\n"; + rules << " command = $CXX $in $LDFLAGS $LIBS -o $out\n"; + rules << " description = Linking CXX executable $out\n\n"; + + rules << "rule cxx_link_static_lib\n"; + rules << " command = rm -f $out && $AR rcs $out $in\n"; + rules << " description = Linking CXX static library $out\n\n"; +} + +void NinjaPlan::writeTargetsNinja() const { + std::ofstream targetsFile(outBasePath_ / "targets.ninja"); + + for (const NinjaEdge& edge : edges_) { + targetsFile << "build " << joinWithSpace(edge.outputs); + targetsFile << ": " << edge.rule; + if (!edge.inputs.empty()) { + targetsFile << ' ' << joinWithSpace(edge.inputs); + } + if (!edge.implicitInputs.empty()) { + targetsFile << " | " << joinWithSpace(edge.implicitInputs); + } + if (!edge.orderOnlyInputs.empty()) { + targetsFile << " || " << joinWithSpace(edge.orderOnlyInputs); + } + targetsFile << '\n'; + for (const auto& [key, value] : edge.bindings) { + targetsFile << " " << key << " = " << value << '\n'; + } + targetsFile << '\n'; + } + + if (!defaultTargets_.empty()) { + targetsFile << "build all: phony " << joinWithSpace(defaultTargets_) << '\n' + << '\n'; + } + if (!testTargets_.empty()) { + targetsFile << "build tests: phony " << joinWithSpace(testTargets_) << '\n' + << '\n'; + } +} + +} // namespace cabin diff --git a/src/BuildConfig.cc b/src/BuildConfig.cc index 566c1ba38..99a9d3c41 100644 --- a/src/BuildConfig.cc +++ b/src/BuildConfig.cc @@ -198,10 +198,6 @@ std::string BuildConfig::mapHeaderToObj(const fs::path& headerPath) const { return fallback.generic_string(); } -void BuildConfig::addEdge(NinjaEdge edge) { - ninjaEdges.push_back(std::move(edge)); -} - void BuildConfig::registerCompileUnit( const std::string& objTarget, const std::string& sourceFile, const std::unordered_set& dependencies, const bool isTest) { @@ -217,92 +213,21 @@ void BuildConfig::registerCompileUnit( std::ranges::sort(edge.implicitInputs); edge.bindings.emplace_back("out_dir", parentDirOrDot(objTarget)); edge.bindings.emplace_back("extra_flags", isTest ? "-DCABIN_TEST" : ""); - addEdge(std::move(edge)); + ninjaPlan.addEdge(std::move(edge)); } void BuildConfig::writeBuildFiles() const { - writeBuildNinja(); - writeConfigNinja(); - writeRulesNinja(); - writeTargetsNinja(); -} - -void BuildConfig::writeBuildNinja() const { - std::ofstream buildFile(outBasePath / "build.ninja"); - buildFile << "# Generated by Cabin\n"; - buildFile << "ninja_required_version = 1.11\n\n"; - buildFile << "include config.ninja\n"; - buildFile << "include rules.ninja\n"; - buildFile << "include targets.ninja\n\n"; - if (!defaultTargets.empty()) { - buildFile << "default " << joinFlags(defaultTargets) << '\n'; - } -} - -void BuildConfig::writeConfigNinja() const { - std::ofstream cfg(outBasePath / "config.ninja"); - cfg << "# Build variables\n"; - cfg << "CXX = " << compiler.cxx << '\n'; - cfg << "CXXFLAGS = " << cxxFlags << '\n'; - cfg << "DEFINES = " << defines << '\n'; - cfg << "INCLUDES = " << includes << '\n'; - cfg << "LDFLAGS = " << ldFlags << '\n'; - cfg << "LIBS = " << libs << '\n'; - cfg << "AR = " << archiver << '\n'; -} - -void BuildConfig::writeRulesNinja() const { - std::ofstream rules(outBasePath / "rules.ninja"); - - rules << "rule cxx_compile\n"; - rules << " command = $CXX $DEFINES $INCLUDES $CXXFLAGS $extra_flags -c $in " - "-o $out\n"; - rules << " description = Building CXX object $out\n\n"; - - rules << "rule cxx_link_exe\n"; - rules << " command = $CXX $in $LDFLAGS $LIBS -o $out\n"; - rules << " description = Linking CXX executable $out\n\n"; - - rules << "rule cxx_link_static_lib\n"; - rules << " command = rm -f $out && $AR rcs $out $in\n"; - rules << " description = Linking CXX static library $out\n\n"; -} - -void BuildConfig::writeTargetsNinja() const { - std::ofstream targetsFile(outBasePath / "targets.ninja"); - - for (const NinjaEdge& edge : ninjaEdges) { - targetsFile << "build " << joinFlags(edge.outputs); - targetsFile << ": " << edge.rule; - if (!edge.inputs.empty()) { - targetsFile << ' ' << joinFlags(edge.inputs); - } - if (!edge.implicitInputs.empty()) { - targetsFile << " | " << joinFlags(edge.implicitInputs); - } - if (!edge.orderOnlyInputs.empty()) { - targetsFile << " || " << joinFlags(edge.orderOnlyInputs); - } - targetsFile << '\n'; - for (const auto& [key, value] : edge.bindings) { - targetsFile << " " << key << " = " << value << '\n'; - } - targetsFile << '\n'; - } + const NinjaToolchain toolchain{ + .cxx = compiler.cxx, + .cxxFlags = cxxFlags, + .defines = defines, + .includes = includes, + .ldFlags = ldFlags, + .libs = libs, + .archiver = archiver, + }; - if (!defaultTargets.empty()) { - targetsFile << "build all: phony " << joinFlags(defaultTargets) << '\n' - << '\n'; - } - if (!testTargets.empty()) { - std::vector testTargetNames; - testTargetNames.reserve(testTargets.size()); - for (const TestTarget& target : testTargets) { - testTargetNames.push_back(target.ninjaTarget); - } - targetsFile << "build tests: phony " << joinFlags(testTargetNames) << '\n' - << '\n'; - } + ninjaPlan.writeFiles(toolchain); } Result BuildConfig::runMM(const std::string& sourceFile, @@ -529,7 +454,7 @@ BuildConfig::processUnittestSrc(const fs::path& sourceFilePath, linkEdge.inputs = std::move(linkInputs); linkEdge.bindings.emplace_back("out_dir", parentDirOrDot(testBinary)); - addEdge(std::move(linkEdge)); + ninjaPlan.addEdge(std::move(linkEdge)); TestTarget testTarget; testTarget.ninjaTarget = testBinary; @@ -578,7 +503,7 @@ BuildConfig::processIntegrationTestSrc(const fs::path& sourceFilePath, } registerCompileUnit(testObjTarget, sourceFilePath.string(), objTargetDeps, /*isTest=*/true); - addEdge(std::move(linkEdge)); + ninjaPlan.addEdge(std::move(linkEdge)); if (mtx) { mtx->unlock(); } @@ -676,8 +601,7 @@ Result BuildConfig::configureBuild() { } compileUnits.clear(); - ninjaEdges.clear(); - defaultTargets.clear(); + ninjaPlan.reset(); testTargets.clear(); cxxFlags = joinFlags(project.compilerOpts.cFlags.others); @@ -769,8 +693,8 @@ Result BuildConfig::configureBuild() { linkEdge.inputs = std::move(inputs); linkEdge.bindings.emplace_back( "out_dir", parentDirOrDot(project.manifest.package.name)); - addEdge(std::move(linkEdge)); - defaultTargets.push_back(project.manifest.package.name); + ninjaPlan.addEdge(std::move(linkEdge)); + ninjaPlan.addDefaultTarget(project.manifest.package.name); } if (hasLibraryTarget) { @@ -789,8 +713,8 @@ Result BuildConfig::configureBuild() { archiveEdge.rule = "cxx_link_static_lib"; archiveEdge.inputs = std::move(libraryInputs); archiveEdge.bindings.emplace_back("out_dir", parentDirOrDot(libName)); - addEdge(std::move(archiveEdge)); - defaultTargets.push_back(libName); + ninjaPlan.addEdge(std::move(archiveEdge)); + ninjaPlan.addDefaultTarget(libName); } if (buildProfile == BuildProfile::Test) { @@ -827,8 +751,16 @@ Result BuildConfig::configureBuild() { return lhs.ninjaTarget < rhs.ninjaTarget; }); testTargets = std::move(discoveredTests); + + std::vector testTargetNames; + testTargetNames.reserve(testTargets.size()); + for (const TestTarget& target : testTargets) { + testTargetNames.push_back(target.ninjaTarget); + } + ninjaPlan.setTestTargets(std::move(testTargetNames)); } else { testTargets.clear(); + ninjaPlan.setTestTargets({}); } return Ok(); diff --git a/src/BuildConfig.hpp b/src/BuildConfig.hpp index 065e09636..8e758953d 100644 --- a/src/BuildConfig.hpp +++ b/src/BuildConfig.hpp @@ -1,6 +1,7 @@ #pragma once #include "Builder/BuildProfile.hpp" +#include "Builder/NinjaPlan.hpp" #include "Builder/Project.hpp" #include "Command.hpp" #include "Manifest.hpp" @@ -57,18 +58,7 @@ class BuildConfig { objectSubdir(std::move(objectSubdir)) {} }; - struct NinjaEdge { - std::vector outputs; - std::string rule; - std::vector inputs; - std::vector implicitInputs; - std::vector orderOnlyInputs; - std::vector> bindings; - }; - std::unordered_map compileUnits; - std::vector ninjaEdges; - std::vector defaultTargets; std::vector testTargets; std::unordered_set srcObjectTargets; std::string archiver = "ar"; @@ -79,24 +69,21 @@ class BuildConfig { std::string ldFlags; std::string libs; + NinjaPlan ninjaPlan; + bool isUpToDate(std::string_view fileName) const; std::string mapHeaderToObj(const fs::path& headerPath) const; - void addEdge(NinjaEdge edge); void registerCompileUnit(const std::string& objTarget, const std::string& sourceFile, const std::unordered_set& dependencies, bool isTest); - void writeBuildNinja() const; - void writeConfigNinja() const; - void writeRulesNinja() const; - void writeTargetsNinja() const; 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)) {} + libName(std::move(libName)), ninjaPlan(outBasePath) {} public: enum class TestKind : std::uint8_t { Unit, Integration };