From d797f37f8a36791a7f1ba3cabf05240f4c85150d Mon Sep 17 00:00:00 2001 From: Ken Matsui <26405363+ken-matsui@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:50:41 -0400 Subject: [PATCH] feat!: isolate public/private interfaces lib/ & include/ represents public interface => yields libcabin.a src/ represents private interface => yields each object file src/main.cc & unit tests link against object files from src/ and libcabin.a. tests/*.cc link against libcabin.a from lib/, not object files from src/. --- Dockerfile | 2 + Makefile | 8 +- {src => include}/Algos.hpp | 0 {src => include}/Builder/BuildProfile.hpp | 0 {src => include}/Builder/Compiler.hpp | 1 + {src => include}/Command.hpp | 0 {src => include}/Dependency.hpp | 0 {src => include}/Git2.hpp | 0 {src => include}/Git2/Commit.hpp | 0 {src => include}/Git2/Config.hpp | 0 {src => include}/Git2/Describe.hpp | 0 {src => include}/Git2/Exception.hpp | 0 {src => include}/Git2/Global.hpp | 0 {src => include}/Git2/Object.hpp | 0 {src => include}/Git2/Oid.hpp | 0 {src => include}/Git2/Repository.hpp | 0 {src => include}/Git2/Revparse.hpp | 0 {src => include}/Git2/Revwalk.hpp | 0 {src => include}/Git2/Time.hpp | 0 {src => include}/Git2/Version.hpp | 0 {src => include}/Manifest.hpp | 0 {src => include}/Rustify/Result.hpp | 0 {src => include}/Rustify/Tests.hpp | 7 +- {src => include}/Semver.hpp | 0 {src => include}/TermColor.hpp | 0 {src => include}/VersionReq.hpp | 0 {src => lib}/Algos.cc | 0 {src => lib}/Builder/Compiler.cc | 190 +++++++++- {src => lib}/Command.cc | 0 {src => lib}/Dependency.cc | 0 {src => lib}/Git2/Commit.cc | 10 +- {src => lib}/Git2/Config.cc | 4 +- {src => lib}/Git2/Describe.cc | 6 +- {src => lib}/Git2/Exception.cc | 2 +- {src => lib}/Git2/Global.cc | 4 +- {src => lib}/Git2/Object.cc | 4 +- {src => lib}/Git2/Oid.cc | 4 +- {src => lib}/Git2/Repository.cc | 8 +- {src => lib}/Git2/Revparse.cc | 2 +- {src => lib}/Git2/Revwalk.cc | 8 +- {src => lib}/Git2/Time.cc | 2 +- {src => lib}/Git2/Version.cc | 4 +- {src => lib}/Manifest.cc | 0 {src => lib}/Semver.cc | 0 {src => lib}/TermColor.cc | 0 {src => lib}/VersionReq.cc | 0 src/BuildConfig.cc | 410 +++++++++++++++------- src/BuildConfig.hpp | 23 +- src/Builder/Project.cc | 6 +- src/Builder/Project.hpp | 4 +- src/Cmd/Build.cc | 20 +- src/Cmd/Init.cc | 9 +- src/Cmd/New.cc | 52 ++- src/Cmd/New.hpp | 5 + src/Cmd/Test.cc | 32 +- tests/build.cc | 31 ++ tests/fmt.cc | 5 +- tests/init.cc | 16 + tests/run.cc | 19 +- tests/test.cc | 36 +- tests/version.cc | 10 +- 61 files changed, 714 insertions(+), 230 deletions(-) rename {src => include}/Algos.hpp (100%) rename {src => include}/Builder/BuildProfile.hpp (100%) rename {src => include}/Builder/Compiler.hpp (99%) rename {src => include}/Command.hpp (100%) rename {src => include}/Dependency.hpp (100%) rename {src => include}/Git2.hpp (100%) rename {src => include}/Git2/Commit.hpp (100%) rename {src => include}/Git2/Config.hpp (100%) rename {src => include}/Git2/Describe.hpp (100%) rename {src => include}/Git2/Exception.hpp (100%) rename {src => include}/Git2/Global.hpp (100%) rename {src => include}/Git2/Object.hpp (100%) rename {src => include}/Git2/Oid.hpp (100%) rename {src => include}/Git2/Repository.hpp (100%) rename {src => include}/Git2/Revparse.hpp (100%) rename {src => include}/Git2/Revwalk.hpp (100%) rename {src => include}/Git2/Time.hpp (100%) rename {src => include}/Git2/Version.hpp (100%) rename {src => include}/Manifest.hpp (100%) rename {src => include}/Rustify/Result.hpp (100%) rename {src => include}/Rustify/Tests.hpp (97%) rename {src => include}/Semver.hpp (100%) rename {src => include}/TermColor.hpp (100%) rename {src => include}/VersionReq.hpp (100%) rename {src => lib}/Algos.cc (100%) rename {src => lib}/Builder/Compiler.cc (54%) rename {src => lib}/Command.cc (100%) rename {src => lib}/Dependency.cc (100%) rename {src => lib}/Git2/Commit.cc (70%) rename {src => lib}/Git2/Config.cc (90%) rename {src => lib}/Git2/Describe.cc (96%) rename {src => lib}/Git2/Exception.cc (94%) rename {src => lib}/Git2/Global.cc (77%) rename {src => lib}/Git2/Object.cc (81%) rename {src => lib}/Git2/Oid.cc (94%) rename {src => lib}/Git2/Repository.cc (95%) rename {src => lib}/Git2/Revparse.cc (90%) rename {src => lib}/Git2/Revwalk.cc (93%) rename {src => lib}/Git2/Time.cc (94%) rename {src => lib}/Git2/Version.cc (95%) rename {src => lib}/Manifest.cc (100%) rename {src => lib}/Semver.cc (100%) rename {src => lib}/TermColor.cc (100%) rename {src => lib}/VersionReq.cc (100%) diff --git a/Dockerfile b/Dockerfile index 1892db9be..4bca8b659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,8 @@ COPY .clang-tidy . COPY cabin.toml . COPY .git . COPY Makefile . +COPY include ./include/ +COPY lib ./lib/ COPY src ./src/ RUN make BUILD=release install diff --git a/Makefile b/Makefile index ab0b68127..4bf6dee03 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ DEFINES := -DCABIN_CABIN_PKG_VERSION='"$(VERSION)"' \ -DCABIN_CABIN_COMMIT_HASH='"$(COMMIT_HASH)"' \ -DCABIN_CABIN_COMMIT_SHORT_HASH='"$(COMMIT_SHORT_HASH)"' \ -DCABIN_CABIN_COMMIT_DATE='"$(COMMIT_DATE)"' -INCLUDES := -Isrc -isystem $(O)/DEPS/toml11/include \ +INCLUDES := -Iinclude -Isrc -isystem $(O)/DEPS/toml11/include \ -isystem $(O)/DEPS/mitama-cpp-result/include CXXFLAGS := -std=c++$(EDITION) -fdiagnostics-color $(CUSTOM_CXXFLAGS) \ @@ -62,8 +62,8 @@ endif LDLIBS := $(PKG_LIBS) # Source files -SRCS := $(shell find src -name '*.cc') -OBJS := $(SRCS:src/%.cc=$(O)/%.o) +SRCS := $(shell find src -name '*.cc') $(shell find lib -name '*.cc') +OBJS := $(SRCS:%.cc=$(O)/%.o) DEPS := $(OBJS:.o=.d) # Targets @@ -79,7 +79,7 @@ $(PROJECT): $(OBJS) @mkdir -p $(@D) $(CXX) $(LDFLAGS) $^ -o $@ $(LDLIBS) -$(O)/%.o: src/%.cc $(GIT_DEPS) +$(O)/%.o: %.cc $(GIT_DEPS) @mkdir -p $(@D) $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/src/Algos.hpp b/include/Algos.hpp similarity index 100% rename from src/Algos.hpp rename to include/Algos.hpp diff --git a/src/Builder/BuildProfile.hpp b/include/Builder/BuildProfile.hpp similarity index 100% rename from src/Builder/BuildProfile.hpp rename to include/Builder/BuildProfile.hpp diff --git a/src/Builder/Compiler.hpp b/include/Builder/Compiler.hpp similarity index 99% rename from src/Builder/Compiler.hpp rename to include/Builder/Compiler.hpp index 1382294a9..7d03ed712 100644 --- a/src/Builder/Compiler.hpp +++ b/include/Builder/Compiler.hpp @@ -104,6 +104,7 @@ class Compiler { const std::string& sourceFile) const; Command makePreprocessCmd(const CompilerOpts& opts, const std::string& sourceFile) const; + std::string detectArchiver(bool useLTO) const; private: explicit Compiler(std::string cxx) noexcept : cxx(std::move(cxx)) {} diff --git a/src/Command.hpp b/include/Command.hpp similarity index 100% rename from src/Command.hpp rename to include/Command.hpp diff --git a/src/Dependency.hpp b/include/Dependency.hpp similarity index 100% rename from src/Dependency.hpp rename to include/Dependency.hpp diff --git a/src/Git2.hpp b/include/Git2.hpp similarity index 100% rename from src/Git2.hpp rename to include/Git2.hpp diff --git a/src/Git2/Commit.hpp b/include/Git2/Commit.hpp similarity index 100% rename from src/Git2/Commit.hpp rename to include/Git2/Commit.hpp diff --git a/src/Git2/Config.hpp b/include/Git2/Config.hpp similarity index 100% rename from src/Git2/Config.hpp rename to include/Git2/Config.hpp diff --git a/src/Git2/Describe.hpp b/include/Git2/Describe.hpp similarity index 100% rename from src/Git2/Describe.hpp rename to include/Git2/Describe.hpp diff --git a/src/Git2/Exception.hpp b/include/Git2/Exception.hpp similarity index 100% rename from src/Git2/Exception.hpp rename to include/Git2/Exception.hpp diff --git a/src/Git2/Global.hpp b/include/Git2/Global.hpp similarity index 100% rename from src/Git2/Global.hpp rename to include/Git2/Global.hpp diff --git a/src/Git2/Object.hpp b/include/Git2/Object.hpp similarity index 100% rename from src/Git2/Object.hpp rename to include/Git2/Object.hpp diff --git a/src/Git2/Oid.hpp b/include/Git2/Oid.hpp similarity index 100% rename from src/Git2/Oid.hpp rename to include/Git2/Oid.hpp diff --git a/src/Git2/Repository.hpp b/include/Git2/Repository.hpp similarity index 100% rename from src/Git2/Repository.hpp rename to include/Git2/Repository.hpp diff --git a/src/Git2/Revparse.hpp b/include/Git2/Revparse.hpp similarity index 100% rename from src/Git2/Revparse.hpp rename to include/Git2/Revparse.hpp diff --git a/src/Git2/Revwalk.hpp b/include/Git2/Revwalk.hpp similarity index 100% rename from src/Git2/Revwalk.hpp rename to include/Git2/Revwalk.hpp diff --git a/src/Git2/Time.hpp b/include/Git2/Time.hpp similarity index 100% rename from src/Git2/Time.hpp rename to include/Git2/Time.hpp diff --git a/src/Git2/Version.hpp b/include/Git2/Version.hpp similarity index 100% rename from src/Git2/Version.hpp rename to include/Git2/Version.hpp diff --git a/src/Manifest.hpp b/include/Manifest.hpp similarity index 100% rename from src/Manifest.hpp rename to include/Manifest.hpp diff --git a/src/Rustify/Result.hpp b/include/Rustify/Result.hpp similarity index 100% rename from src/Rustify/Result.hpp rename to include/Rustify/Result.hpp diff --git a/src/Rustify/Tests.hpp b/include/Rustify/Tests.hpp similarity index 97% rename from src/Rustify/Tests.hpp rename to include/Rustify/Tests.hpp index f5460dcd6..eb9a1ce6c 100644 --- a/src/Rustify/Tests.hpp +++ b/include/Rustify/Tests.hpp @@ -48,13 +48,16 @@ constexpr std::string_view getModName(std::string_view file) noexcept { return file; } - const std::size_t start = file.find("src/"); + std::size_t start = file.find("src/"); + if (start == std::string_view::npos) { + start = file.find("lib/"); + } if (start == std::string_view::npos) { return file; } const std::size_t end = file.find_last_of('.'); - if (end == std::string_view::npos) { + if (end == std::string_view::npos || end <= start) { return file; } diff --git a/src/Semver.hpp b/include/Semver.hpp similarity index 100% rename from src/Semver.hpp rename to include/Semver.hpp diff --git a/src/TermColor.hpp b/include/TermColor.hpp similarity index 100% rename from src/TermColor.hpp rename to include/TermColor.hpp diff --git a/src/VersionReq.hpp b/include/VersionReq.hpp similarity index 100% rename from src/VersionReq.hpp rename to include/VersionReq.hpp diff --git a/src/Algos.cc b/lib/Algos.cc similarity index 100% rename from src/Algos.cc rename to lib/Algos.cc diff --git a/src/Builder/Compiler.cc b/lib/Builder/Compiler.cc similarity index 54% rename from src/Builder/Compiler.cc rename to lib/Builder/Compiler.cc index ac19d882a..373c4a092 100644 --- a/src/Builder/Compiler.cc +++ b/lib/Builder/Compiler.cc @@ -1,11 +1,14 @@ -#include "Compiler.hpp" +#include "Builder/Compiler.hpp" #include "Algos.hpp" #include "Command.hpp" #include "Rustify/Result.hpp" #include +#include +#include #include +#include #include #include #include @@ -15,6 +18,126 @@ namespace cabin { +static std::optional getEnvVar(const char* name) { + if (const char* value = std::getenv(name); + value != nullptr && *value != '\0') { + return std::string(value); + } + return std::nullopt; +} + +static std::optional +findSiblingTool(const fs::path& base, const std::string& candidate) { + if (base.has_parent_path()) { + const fs::path sibling = base.parent_path() / candidate; + if (fs::exists(sibling)) { + return sibling.string(); + } + } + return std::nullopt; +} + +static std::optional +makeToolNameForCompiler(const std::string& compilerName, + std::string_view suffix, std::string_view tool) { + const std::size_t pos = compilerName.rfind(suffix); + if (pos == std::string::npos) { + return std::nullopt; + } + if (pos + suffix.size() > compilerName.size()) { + return std::nullopt; + } + if (pos != 0) { + const auto prev = static_cast(compilerName[pos - 1]); + if (std::isalnum(prev)) { + return std::nullopt; + } + } + + const std::string prefix = compilerName.substr(0, pos); + const std::string postfix = + compilerName.substr(pos + static_cast(suffix.size())); + return fmt::format("{}{}{}", prefix, tool, postfix); +} + +static std::optional resolveToolWithSuffix(const fs::path& cxxPath, + std::string_view suffix, + std::string_view tool) { + const std::string filename = cxxPath.filename().string(); + const auto candidateName = makeToolNameForCompiler(filename, suffix, tool); + if (!candidateName.has_value()) { + return std::nullopt; + } + const std::string& candidate = candidateName.value(); + + if (auto sibling = findSiblingTool(cxxPath, candidate); sibling.has_value()) { + return sibling; + } + if (commandExists(candidate)) { + return candidate; + } + return std::nullopt; +} + +static std::optional resolveLlvmAr(const fs::path& cxxPath) { + if (auto resolved = resolveToolWithSuffix(cxxPath, "clang++", "llvm-ar"); + resolved.has_value()) { + return resolved; + } + if (auto resolved = resolveToolWithSuffix(cxxPath, "clang", "llvm-ar"); + resolved.has_value()) { + return resolved; + } + if (commandExists("llvm-ar")) { + return std::string("llvm-ar"); + } + return std::nullopt; +} + +static std::optional resolveGccAr(const fs::path& cxxPath) { + if (auto resolved = resolveToolWithSuffix(cxxPath, "g++", "gcc-ar"); + resolved.has_value()) { + return resolved; + } + if (auto resolved = resolveToolWithSuffix(cxxPath, "gcc", "gcc-ar"); + resolved.has_value()) { + return resolved; + } + if (commandExists("gcc-ar")) { + return std::string("gcc-ar"); + } + return std::nullopt; +} + +enum class CompilerFlavor : std::uint8_t { Clang, Gcc, Other }; + +static CompilerFlavor detectCompilerFlavor(const fs::path& cxxPath) { + const std::string name = cxxPath.filename().string(); + if (name.contains("clang")) { + return CompilerFlavor::Clang; + } + if (name.contains("g++") || name.contains("gcc")) { + return CompilerFlavor::Gcc; + } + return CompilerFlavor::Other; +} + +static std::optional envArchiverOverride() { + if (auto ar = getEnvVar("CABIN_AR"); ar.has_value()) { + return ar; + } + if (auto ar = getEnvVar("AR"); ar.has_value()) { + return ar; + } + if (auto ar = getEnvVar("LLVM_AR"); ar.has_value()) { + return ar; + } + if (auto ar = getEnvVar("GCC_AR"); ar.has_value()) { + return ar; + } + return std::nullopt; +} + // TODO: The parsing of pkg-config output might not be robust. It assumes // that there wouldn't be backquotes or double quotes in the output, (should // be treated as a single flag). The current code just splits the output by @@ -215,4 +338,69 @@ Command Compiler::makePreprocessCmd(const CompilerOpts& opts, .addArg(sourceFile); } +std::string Compiler::detectArchiver(const bool useLTO) const { + if (auto override = envArchiverOverride(); override.has_value()) { + return override.value(); + } + if (!useLTO) { + return "ar"; + } + + const fs::path cxxPath(cxx); + switch (detectCompilerFlavor(cxxPath)) { + case CompilerFlavor::Clang: + if (auto llvmAr = resolveLlvmAr(cxxPath); llvmAr.has_value()) { + return llvmAr.value(); + } + break; + case CompilerFlavor::Gcc: + if (auto gccAr = resolveGccAr(cxxPath); gccAr.has_value()) { + return gccAr.value(); + } + break; + case CompilerFlavor::Other: + break; + } + + return "ar"; +} + } // namespace cabin + +#ifdef CABIN_TEST + +# include "Rustify/Tests.hpp" + +namespace tests { + +using namespace cabin; // NOLINT(build/namespaces,google-build-using-namespace) + +static void testMakeToolNameForCompiler() { + auto expectValue = [](const std::optional& value, + const std::string& expected) { + assertTrue(value.has_value()); + assertEq(*value, expected); + }; + + expectValue(makeToolNameForCompiler("clang++", "clang++", "llvm-ar"), + "llvm-ar"); + expectValue(makeToolNameForCompiler("clang++-19", "clang++", "llvm-ar"), + "llvm-ar-19"); + expectValue(makeToolNameForCompiler("aarch64-linux-gnu-clang++", "clang++", + "llvm-ar"), + "aarch64-linux-gnu-llvm-ar"); + expectValue( + makeToolNameForCompiler("x86_64-w64-mingw32-g++-13", "g++", "gcc-ar"), + "x86_64-w64-mingw32-gcc-ar-13"); + + assertFalse(makeToolNameForCompiler("clang++", "g++", "gcc-ar").has_value()); + assertFalse(makeToolNameForCompiler("foo", "clang++", "llvm-ar").has_value()); + + pass(); +} + +} // namespace tests + +int main() { tests::testMakeToolNameForCompiler(); } + +#endif diff --git a/src/Command.cc b/lib/Command.cc similarity index 100% rename from src/Command.cc rename to lib/Command.cc diff --git a/src/Dependency.cc b/lib/Dependency.cc similarity index 100% rename from src/Dependency.cc rename to lib/Dependency.cc diff --git a/src/Git2/Commit.cc b/lib/Git2/Commit.cc similarity index 70% rename from src/Git2/Commit.cc rename to lib/Git2/Commit.cc index 94e47cf47..f18f1b2a8 100644 --- a/src/Git2/Commit.cc +++ b/lib/Git2/Commit.cc @@ -1,9 +1,9 @@ -#include "Commit.hpp" +#include "Git2/Commit.hpp" -#include "Exception.hpp" -#include "Oid.hpp" -#include "Repository.hpp" -#include "Time.hpp" +#include "Git2/Exception.hpp" +#include "Git2/Oid.hpp" +#include "Git2/Repository.hpp" +#include "Git2/Time.hpp" #include diff --git a/src/Git2/Config.cc b/lib/Git2/Config.cc similarity index 90% rename from src/Git2/Config.cc rename to lib/Git2/Config.cc index b2a0118dc..bb7cd39de 100644 --- a/src/Git2/Config.cc +++ b/lib/Git2/Config.cc @@ -1,6 +1,6 @@ -#include "Config.hpp" +#include "Git2/Config.hpp" -#include "Exception.hpp" +#include "Git2/Exception.hpp" #include #include diff --git a/src/Git2/Describe.cc b/lib/Git2/Describe.cc similarity index 96% rename from src/Git2/Describe.cc rename to lib/Git2/Describe.cc index 9f0f003f6..461a338e8 100644 --- a/src/Git2/Describe.cc +++ b/lib/Git2/Describe.cc @@ -1,7 +1,7 @@ -#include "Describe.hpp" +#include "Git2/Describe.hpp" -#include "Exception.hpp" -#include "Repository.hpp" +#include "Git2/Exception.hpp" +#include "Git2/Repository.hpp" #include #include diff --git a/src/Git2/Exception.cc b/lib/Git2/Exception.cc similarity index 94% rename from src/Git2/Exception.cc rename to lib/Git2/Exception.cc index aefd10d98..0d28f2848 100644 --- a/src/Git2/Exception.cc +++ b/lib/Git2/Exception.cc @@ -1,4 +1,4 @@ -#include "Exception.hpp" +#include "Git2/Exception.hpp" #include #include diff --git a/src/Git2/Global.cc b/lib/Git2/Global.cc similarity index 77% rename from src/Git2/Global.cc rename to lib/Git2/Global.cc index 168e91a31..d078e67f2 100644 --- a/src/Git2/Global.cc +++ b/lib/Git2/Global.cc @@ -1,6 +1,6 @@ -#include "Global.hpp" +#include "Git2/Global.hpp" -#include "Exception.hpp" +#include "Git2/Exception.hpp" #include diff --git a/src/Git2/Object.cc b/lib/Git2/Object.cc similarity index 81% rename from src/Git2/Object.cc rename to lib/Git2/Object.cc index b931fb54a..8fe45e335 100644 --- a/src/Git2/Object.cc +++ b/lib/Git2/Object.cc @@ -1,6 +1,6 @@ -#include "Object.hpp" +#include "Git2/Object.hpp" -#include "Oid.hpp" +#include "Git2/Oid.hpp" #include diff --git a/src/Git2/Oid.cc b/lib/Git2/Oid.cc similarity index 94% rename from src/Git2/Oid.cc rename to lib/Git2/Oid.cc index 3282abf4b..2c2f0fbce 100644 --- a/src/Git2/Oid.cc +++ b/lib/Git2/Oid.cc @@ -1,6 +1,6 @@ -#include "Oid.hpp" +#include "Git2/Oid.hpp" -#include "Exception.hpp" +#include "Git2/Exception.hpp" #include #include diff --git a/src/Git2/Repository.cc b/lib/Git2/Repository.cc similarity index 95% rename from src/Git2/Repository.cc rename to lib/Git2/Repository.cc index 34622692e..b053351be 100644 --- a/src/Git2/Repository.cc +++ b/lib/Git2/Repository.cc @@ -1,8 +1,8 @@ -#include "Repository.hpp" +#include "Git2/Repository.hpp" -#include "Config.hpp" -#include "Exception.hpp" -#include "Oid.hpp" +#include "Git2/Config.hpp" +#include "Git2/Exception.hpp" +#include "Git2/Oid.hpp" #include #include diff --git a/src/Git2/Revparse.cc b/lib/Git2/Revparse.cc similarity index 90% rename from src/Git2/Revparse.cc rename to lib/Git2/Revparse.cc index 219fab236..8b4e7e528 100644 --- a/src/Git2/Revparse.cc +++ b/lib/Git2/Revparse.cc @@ -1,4 +1,4 @@ -#include "Revparse.hpp" +#include "Git2/Revparse.hpp" #include diff --git a/src/Git2/Revwalk.cc b/lib/Git2/Revwalk.cc similarity index 93% rename from src/Git2/Revwalk.cc rename to lib/Git2/Revwalk.cc index 27b590ad1..434edc3b9 100644 --- a/src/Git2/Revwalk.cc +++ b/lib/Git2/Revwalk.cc @@ -1,8 +1,8 @@ -#include "Revwalk.hpp" +#include "Git2/Revwalk.hpp" -#include "Exception.hpp" -#include "Oid.hpp" -#include "Repository.hpp" +#include "Git2/Exception.hpp" +#include "Git2/Oid.hpp" +#include "Git2/Repository.hpp" #include #include diff --git a/src/Git2/Time.cc b/lib/Git2/Time.cc similarity index 94% rename from src/Git2/Time.cc rename to lib/Git2/Time.cc index ab1a56df5..2b1bd3f23 100644 --- a/src/Git2/Time.cc +++ b/lib/Git2/Time.cc @@ -1,4 +1,4 @@ -#include "Time.hpp" +#include "Git2/Time.hpp" #include #include diff --git a/src/Git2/Version.cc b/lib/Git2/Version.cc similarity index 95% rename from src/Git2/Version.cc rename to lib/Git2/Version.cc index c4a917ad3..66c5d8bab 100644 --- a/src/Git2/Version.cc +++ b/lib/Git2/Version.cc @@ -1,6 +1,6 @@ -#include "Version.hpp" +#include "Git2/Version.hpp" -#include "Exception.hpp" +#include "Git2/Exception.hpp" #include #include diff --git a/src/Manifest.cc b/lib/Manifest.cc similarity index 100% rename from src/Manifest.cc rename to lib/Manifest.cc diff --git a/src/Semver.cc b/lib/Semver.cc similarity index 100% rename from src/Semver.cc rename to lib/Semver.cc diff --git a/src/TermColor.cc b/lib/TermColor.cc similarity index 100% rename from src/TermColor.cc rename to lib/TermColor.cc diff --git a/src/VersionReq.cc b/lib/VersionReq.cc similarity index 100% rename from src/VersionReq.cc rename to lib/VersionReq.cc diff --git a/src/BuildConfig.cc b/src/BuildConfig.cc index 5b998d2a3..566c1ba38 100644 --- a/src/BuildConfig.cc +++ b/src/BuildConfig.cc @@ -9,9 +9,8 @@ #include "Parallelism.hpp" #include -#include +#include #include -#include #include #include #include @@ -28,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -129,25 +129,73 @@ bool BuildConfig::isUpToDate(const std::string_view fileName) const { } const fs::file_time_type configTime = fs::last_write_time(filePath); - const fs::path srcDir = project.rootPath / "src"; - for (const auto& entry : fs::recursive_directory_iterator(srcDir)) { - if (fs::last_write_time(entry.path()) > configTime) { - return false; + const std::array watchedDirs{ + project.rootPath / "src", + project.rootPath / "lib", + project.rootPath / "include", + }; + for (const fs::path& dir : watchedDirs) { + if (!fs::exists(dir)) { + continue; + } + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (fs::last_write_time(entry.path()) > configTime) { + return false; + } } } return fs::last_write_time(project.manifest.path) <= configTime; } std::string BuildConfig::mapHeaderToObj(const fs::path& headerPath) const { - fs::path objBase = fs::relative(project.buildOutPath, outBasePath); - const fs::path relHeader = - fs::relative(headerPath.parent_path(), project.rootPath / "src"); - if (relHeader != ".") { - objBase /= relHeader; + const fs::path objBase = fs::relative(project.buildOutPath, outBasePath); + + const auto makeObjPath = [&](const fs::path& relDir, const fs::path& prefix) { + fs::path objPath = objBase; + if (!prefix.empty()) { + objPath /= prefix; + } + if (!relDir.empty() && relDir != ".") { + objPath /= relDir; + } + objPath /= headerPath.stem(); + objPath += ".o"; + return objPath; + }; + + const auto tryMap = + [&](const fs::path& rootDir, + const fs::path& prefix) -> std::optional { + std::error_code ec; + const fs::path rel = fs::relative(headerPath.parent_path(), rootDir, ec); + if (ec) { + return std::nullopt; + } + if (!rel.empty()) { + const auto first = rel.begin(); + if (first != rel.end() && *first == "..") { + return std::nullopt; + } + } + return makeObjPath(rel, prefix).generic_string(); + }; + + if (auto mapped = tryMap(project.rootPath / "src", fs::path()); + mapped.has_value()) { + return *mapped; + } + if (auto mapped = tryMap(project.rootPath / "include", fs::path("lib")); + mapped.has_value()) { + return *mapped; } - objBase /= headerPath.stem(); - objBase += ".o"; - return objBase.generic_string(); + if (auto mapped = tryMap(project.rootPath / "lib", fs::path("lib")); + mapped.has_value()) { + return *mapped; + } + + fs::path fallback = objBase / headerPath.stem(); + fallback += ".o"; + return fallback.generic_string(); } void BuildConfig::addEdge(NinjaEdge edge) { @@ -200,6 +248,7 @@ void BuildConfig::writeConfigNinja() const { cfg << "INCLUDES = " << includes << '\n'; cfg << "LDFLAGS = " << ldFlags << '\n'; cfg << "LIBS = " << libs << '\n'; + cfg << "AR = " << archiver << '\n'; } void BuildConfig::writeRulesNinja() const { @@ -215,7 +264,7 @@ void BuildConfig::writeRulesNinja() const { rules << " description = Linking CXX executable $out\n\n"; rules << "rule cxx_link_static_lib\n"; - rules << " command = ar rcs $out $in\n"; + rules << " command = rm -f $out && $AR rcs $out $in\n"; rules << " description = Linking CXX static library $out\n\n"; } @@ -292,16 +341,27 @@ BuildConfig::containsTestCode(const std::string& sourceFile) const { } Result -BuildConfig::processSrc(const fs::path& sourceFilePath, +BuildConfig::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); + std::error_code ec; const fs::path targetBaseDir = - fs::relative(sourceFilePath.parent_path(), project.rootPath / "src"); + fs::relative(sourceFilePath.parent_path(), root.directory, ec); + Ensure(!ec, "failed to compute relative path for {}", sourceFilePath); + if (!targetBaseDir.empty()) { + const auto first = targetBaseDir.begin(); + Ensure(first == targetBaseDir.end() || *first != "..", + "source file `{}` must reside under `{}`", sourceFilePath, + root.directory); + } fs::path buildTargetBaseDir = project.buildOutPath; + if (!root.objectSubdir.empty()) { + buildTargetBaseDir /= root.objectSubdir; + } if (targetBaseDir != ".") { buildTargetBaseDir /= targetBaseDir; } @@ -323,7 +383,8 @@ BuildConfig::processSrc(const fs::path& sourceFilePath, } Result> -BuildConfig::processSources(const std::vector& sourceFilePaths) { +BuildConfig::processSources(const std::vector& sourceFilePaths, + const SourceRoot& root) { std::unordered_set buildObjTargets; if (isParallel()) { @@ -333,10 +394,11 @@ BuildConfig::processSources(const std::vector& sourceFilePaths) { tbb::blocked_range(0, sourceFilePaths.size()), [&](const tbb::blocked_range& rng) { for (std::size_t i = rng.begin(); i != rng.end(); ++i) { - std::ignore = processSrc(sourceFilePaths[i], buildObjTargets, &mtx) - .map_err([&results](const auto& err) { - results.push_back(err->what()); - }); + std::ignore = + processSrc(sourceFilePaths[i], root, buildObjTargets, &mtx) + .map_err([&results](const auto& err) { + results.push_back(err->what()); + }); } }); if (!results.empty()) { @@ -344,16 +406,15 @@ BuildConfig::processSources(const std::vector& sourceFilePaths) { } } else { for (const fs::path& sourceFilePath : sourceFilePaths) { - Try(processSrc(sourceFilePath, buildObjTargets)); + Try(processSrc(sourceFilePath, root, buildObjTargets)); } } return Ok(buildObjTargets); } -Result> BuildConfig::processUnittestSrc( - const fs::path& sourceFilePath, - const std::unordered_set& buildObjTargets, - tbb::spin_mutex* mtx) { +Result> +BuildConfig::processUnittestSrc(const fs::path& sourceFilePath, + tbb::spin_mutex* mtx) { if (!Try(containsTestCode(sourceFilePath))) { return Ok(std::optional()); } @@ -362,44 +423,114 @@ Result> BuildConfig::processUnittestSrc( const std::unordered_set objTargetDeps = parseMMOutput(Try(runMM(sourceFilePath, /*isTest=*/true)), objTarget); - const fs::path targetBaseDir = - fs::relative(sourceFilePath.parent_path(), project.rootPath / "src"); - fs::path testTargetBaseDir = project.unittestOutPath; - if (targetBaseDir != ".") { - testTargetBaseDir /= targetBaseDir; - } + fs::path relBase = fs::path("unit"); - const fs::path testObjOutput = testTargetBaseDir / objTarget; - const std::string testObjTarget = - fs::relative(testObjOutput, outBasePath).generic_string(); - const fs::path testBinaryPath = - (testTargetBaseDir / sourceFilePath.filename()).concat(".test"); - const std::string testBinary = - fs::relative(testBinaryPath, outBasePath).generic_string(); + const auto canonicalOrGeneric = [](const fs::path& path) { + std::error_code ec; + const fs::path canonical = fs::weakly_canonical(path, ec); + if (ec) { + return path.lexically_normal().generic_string(); + } + return canonical.generic_string(); + }; - std::unordered_set deps = { testObjTarget }; - collectBinDepObjs(deps, sourceFilePath.stem().string(), objTargetDeps, - buildObjTargets); + const std::string canonicalSource = canonicalOrGeneric(sourceFilePath); + const std::string canonicalSrcRoot = + canonicalOrGeneric(project.rootPath / "src"); + const std::string canonicalLibRoot = + canonicalOrGeneric(project.rootPath / "lib"); - std::vector linkInputs(deps.begin(), deps.end()); - std::ranges::sort(linkInputs); + const auto setRelBaseFrom = [&](const std::string& baseCanonical, + std::string_view subdir) -> bool { + if (baseCanonical.empty()) { + return false; + } + if (canonicalSource.size() <= baseCanonical.size()) { + return false; + } + if (!canonicalSource.starts_with(baseCanonical)) { + return false; + } + const char divider = canonicalSource[baseCanonical.size()]; + if (divider != '/') { + return false; + } + const std::string remainder = + canonicalSource.substr(baseCanonical.size() + 1); + if (remainder.empty()) { + return false; + } - NinjaEdge linkEdge; - linkEdge.outputs = { testBinary }; - linkEdge.rule = "cxx_link_exe"; - linkEdge.inputs = std::move(linkInputs); - linkEdge.bindings.emplace_back("out_dir", parentDirOrDot(testBinary)); + relBase /= fs::path(subdir); + const fs::path remainderPath(remainder); + const fs::path parent = remainderPath.parent_path(); + if (!parent.empty()) { + relBase /= parent; + } + return true; + }; + + bool handled = false; + bool isSrcUnit = false; + if (setRelBaseFrom(canonicalSrcRoot, "src")) { + handled = true; + isSrcUnit = true; + } else if (setRelBaseFrom(canonicalLibRoot, "lib")) { + handled = true; + } + + if (!handled) { + std::error_code relRootEc; + const fs::path relRootParent = + fs::relative(sourceFilePath.parent_path(), project.rootPath, relRootEc); + Ensure(!relRootEc, "failed to compute relative path for {}", + sourceFilePath); + if (relRootParent != "." && !relRootParent.empty()) { + relBase /= relRootParent; + } + } + + const fs::path testObjRel = relBase / objTarget; + const std::string testObjTarget = testObjRel.generic_string(); + + fs::path testBinaryRel = relBase / sourceFilePath.filename(); + testBinaryRel += ".test"; + const std::string testBinary = testBinaryRel.generic_string(); if (mtx) { mtx->lock(); } registerCompileUnit(testObjTarget, sourceFilePath.string(), objTargetDeps, /*isTest=*/true); - addEdge(std::move(linkEdge)); if (mtx) { mtx->unlock(); } + std::vector linkInputs; + linkInputs.push_back(testObjTarget); + + if (isSrcUnit) { + std::unordered_set deps; + collectBinDepObjs(deps, sourceFilePath.stem().string(), objTargetDeps, + srcObjectTargets); + + std::vector srcDeps(deps.begin(), deps.end()); + std::ranges::sort(srcDeps); + linkInputs.insert(linkInputs.end(), srcDeps.begin(), srcDeps.end()); + } + + if (hasLibraryTarget) { + linkInputs.push_back(libName); + } + + NinjaEdge linkEdge; + linkEdge.outputs = { testBinary }; + linkEdge.rule = "cxx_link_exe"; + linkEdge.inputs = std::move(linkInputs); + linkEdge.bindings.emplace_back("out_dir", parentDirOrDot(testBinary)); + + addEdge(std::move(linkEdge)); + TestTarget testTarget; testTarget.ninjaTarget = testBinary; testTarget.sourcePath = @@ -410,10 +541,8 @@ Result> BuildConfig::processUnittestSrc( } Result> -BuildConfig::processIntegrationTestSrc( - const fs::path& sourceFilePath, - const std::unordered_set& buildObjTargets, - tbb::spin_mutex* mtx) { +BuildConfig::processIntegrationTestSrc(const fs::path& sourceFilePath, + tbb::spin_mutex* mtx) { std::string objTarget; const std::unordered_set objTargetDeps = parseMMOutput(Try(runMM(sourceFilePath, /*isTest=*/true)), objTarget); @@ -432,20 +561,10 @@ BuildConfig::processIntegrationTestSrc( const std::string testBinary = fs::relative(testBinaryPath, outBasePath).generic_string(); - std::unordered_set deps = { testObjTarget }; - collectBinDepObjs(deps, sourceFilePath.stem().string(), objTargetDeps, - buildObjTargets); - for (const std::string& obj : buildObjTargets) { - if (obj.ends_with("/main.o") || obj == "main.o") { - continue; - } - deps.insert(obj); - } + std::vector linkInputs{ testObjTarget }; if (hasLibraryTarget) { - deps.insert(libName); + linkInputs.push_back(libName); } - - std::vector linkInputs(deps.begin(), deps.end()); std::ranges::sort(linkInputs); NinjaEdge linkEdge; @@ -521,51 +640,35 @@ void BuildConfig::enableCoverage() { Result BuildConfig::configureBuild() { const fs::path srcDir = project.rootPath / "src"; - if (!fs::exists(srcDir)) { - Bail("{} is required but not found", srcDir); - } + const bool hasSrcDir = fs::exists(srcDir); + const fs::path libDir = project.rootPath / "lib"; + + const Profile& profile = project.manifest.profiles.at(buildProfile); + archiver = compiler.detectArchiver(profile.lto); + + hasBinaryTarget = false; + hasLibraryTarget = false; const auto isMainSource = [](const fs::path& file) { return file.filename().stem() == "main"; }; - const auto isLibSource = [](const fs::path& file) { - return file.filename().stem() == "lib"; - }; fs::path mainSource; - for (const auto& entry : fs::directory_iterator(srcDir)) { - const fs::path& path = entry.path(); - if (!SOURCE_FILE_EXTS.contains(path.extension().string())) { - continue; - } - if (!isMainSource(path)) { - continue; - } - if (!mainSource.empty()) { - Bail("multiple main sources were found"); - } - mainSource = path; - hasBinaryTarget = true; - } - - fs::path libSource; - for (const auto& entry : fs::directory_iterator(srcDir)) { - const fs::path& path = entry.path(); - if (!SOURCE_FILE_EXTS.contains(path.extension().string())) { - continue; - } - if (!isLibSource(path)) { - continue; - } - if (!libSource.empty()) { - Bail("multiple lib sources were found"); + if (hasSrcDir) { + for (const auto& entry : fs::directory_iterator(srcDir)) { + const fs::path& path = entry.path(); + if (!SOURCE_FILE_EXTS.contains(path.extension().string())) { + continue; + } + if (!isMainSource(path)) { + continue; + } + if (!mainSource.empty()) { + Bail("multiple main sources were found"); + } + mainSource = path; + hasBinaryTarget = true; } - libSource = path; - hasLibraryTarget = true; - } - - if (!hasBinaryTarget && !hasLibraryTarget) { - Bail("src/(main|lib){} was not found", SOURCE_FILE_EXTS); } if (!fs::exists(outBasePath)) { @@ -585,7 +688,8 @@ Result BuildConfig::configureBuild() { ldFlags = combineFlags({ ldOthers, libDirs }); libs = joinFlags(project.compilerOpts.ldFlags.libs); - const std::vector sourceFilePaths = listSourceFilePaths(srcDir); + const std::vector sourceFilePaths = + hasSrcDir ? listSourceFilePaths(srcDir) : std::vector{}; for (const fs::path& sourceFilePath : sourceFilePaths) { if (sourceFilePath != mainSource && isMainSource(sourceFilePath)) { Diag::warn( @@ -594,18 +698,38 @@ Result BuildConfig::configureBuild() { "This file will not be treated as the program's entry point. " "Move it directly to 'src/' if intended as such.", sourceFilePath.string()); - } else if (sourceFilePath != libSource && isLibSource(sourceFilePath)) { - Diag::warn( - "source file `{}` is named `lib` but is not located directly in the " - "`src/` directory. " - "This file will not be treated as a hasLibraryTarget. " - "Move it directly to 'src/' if intended as such.", - sourceFilePath.string()); } } - const std::unordered_set buildObjTargets = - Try(processSources(sourceFilePaths)); + std::vector publicSourceFilePaths; + if (fs::exists(libDir)) { + publicSourceFilePaths = listSourceFilePaths(libDir); + } + hasLibraryTarget = !publicSourceFilePaths.empty(); + + if (!hasBinaryTarget && !hasLibraryTarget) { + Bail("expected either `src/main{}` or at least one source file under " + "`lib/` matching {}", + SOURCE_FILE_EXTS, SOURCE_FILE_EXTS); + } + + const SourceRoot srcRoot(srcDir); + const SourceRoot libRoot(libDir, fs::path("lib")); + + const std::unordered_set srcObjTargets = + Try(processSources(sourceFilePaths, srcRoot)); + srcObjectTargets = srcObjTargets; + std::erase_if(srcObjectTargets, [](const std::string& obj) { + return obj == "main.o" || obj.ends_with("/main.o"); + }); + + std::unordered_set libObjTargets; + if (!publicSourceFilePaths.empty()) { + libObjTargets = Try(processSources(publicSourceFilePaths, libRoot)); + } + + std::unordered_set buildObjTargets = srcObjTargets; + buildObjTargets.insert(libObjTargets.begin(), libObjTargets.end()); if (hasBinaryTarget) { const fs::path mainObjPath = project.buildOutPath / "main.o"; @@ -618,8 +742,26 @@ Result BuildConfig::configureBuild() { collectBinDepObjs(deps, "", compileUnits.at(mainObj).dependencies, buildObjTargets); - std::vector inputs(deps.begin(), deps.end()); - std::ranges::sort(inputs); + std::vector inputs; + if (hasLibraryTarget) { + deps.erase(mainObj); + std::vector srcInputs; + srcInputs.reserve(deps.size()); + for (const std::string& dep : deps) { + if (libObjTargets.contains(dep)) { + continue; + } + srcInputs.push_back(dep); + } + std::ranges::sort(srcInputs); + + inputs.push_back(mainObj); + inputs.insert(inputs.end(), srcInputs.begin(), srcInputs.end()); + inputs.push_back(libName); + } else { + inputs.assign(deps.begin(), deps.end()); + std::ranges::sort(inputs); + } NinjaEdge linkEdge; linkEdge.outputs = { project.manifest.package.name }; @@ -632,23 +774,20 @@ Result BuildConfig::configureBuild() { } if (hasLibraryTarget) { - const fs::path libObjPath = project.buildOutPath / "lib.o"; - const std::string libObj = - fs::relative(libObjPath, outBasePath).generic_string(); - Ensure(compileUnits.contains(libObj), - "internal error: missing compile unit for {}", libObj); - - std::unordered_set deps = { libObj }; - collectBinDepObjs(deps, "", compileUnits.at(libObj).dependencies, - buildObjTargets); + std::vector libraryInputs; + libraryInputs.reserve(libObjTargets.size()); + for (const std::string& obj : libObjTargets) { + libraryInputs.push_back(obj); + } - std::vector inputs(deps.begin(), deps.end()); - std::ranges::sort(inputs); + Ensure(!libraryInputs.empty(), + "internal error: expected objects for library target"); + std::ranges::sort(libraryInputs); NinjaEdge archiveEdge; archiveEdge.outputs = { libName }; archiveEdge.rule = "cxx_link_static_lib"; - archiveEdge.inputs = std::move(inputs); + archiveEdge.inputs = std::move(libraryInputs); archiveEdge.bindings.emplace_back("out_dir", parentDirOrDot(libName)); addEdge(std::move(archiveEdge)); defaultTargets.push_back(libName); @@ -658,8 +797,14 @@ Result BuildConfig::configureBuild() { std::vector discoveredTests; discoveredTests.reserve(sourceFilePaths.size()); for (const fs::path& sourceFilePath : sourceFilePaths) { - if (auto maybeTarget = - Try(processUnittestSrc(sourceFilePath, buildObjTargets)); + if (auto maybeTarget = Try(processUnittestSrc(sourceFilePath)); + maybeTarget.has_value()) { + discoveredTests.push_back(std::move(maybeTarget.value())); + } + } + + for (const fs::path& sourceFilePath : publicSourceFilePaths) { + if (auto maybeTarget = Try(processUnittestSrc(sourceFilePath)); maybeTarget.has_value()) { discoveredTests.push_back(std::move(maybeTarget.value())); } @@ -670,8 +815,7 @@ Result BuildConfig::configureBuild() { const std::vector integrationSources = listSourceFilePaths(integrationTestDir); for (const fs::path& sourceFilePath : integrationSources) { - if (auto maybeTarget = - Try(processIntegrationTestSrc(sourceFilePath, buildObjTargets)); + if (auto maybeTarget = Try(processIntegrationTestSrc(sourceFilePath)); maybeTarget.has_value()) { discoveredTests.push_back(std::move(maybeTarget.value())); } diff --git a/src/BuildConfig.hpp b/src/BuildConfig.hpp index 56651f12b..065e09636 100644 --- a/src/BuildConfig.hpp +++ b/src/BuildConfig.hpp @@ -48,6 +48,15 @@ class BuildConfig { bool isTest = false; }; + struct SourceRoot { + fs::path directory; + fs::path objectSubdir; + + explicit SourceRoot(fs::path directory, fs::path objectSubdir = fs::path()) + : directory(std::move(directory)), + objectSubdir(std::move(objectSubdir)) {} + }; + struct NinjaEdge { std::vector outputs; std::string rule; @@ -61,6 +70,8 @@ class BuildConfig { std::vector ninjaEdges; std::vector defaultTargets; std::vector testTargets; + std::unordered_set srcObjectTargets; + std::string archiver = "ar"; std::string cxxFlags; std::string defines; @@ -113,19 +124,19 @@ class BuildConfig { void enableCoverage(); Result processSrc(const fs::path& sourceFilePath, + const SourceRoot& root, std::unordered_set& buildObjTargets, tbb::spin_mutex* mtx = nullptr); Result> - processSources(const std::vector& sourceFilePaths); + processSources(const std::vector& sourceFilePaths, + const SourceRoot& root); Result> processUnittestSrc(const fs::path& sourceFilePath, - const std::unordered_set& buildObjTargets, tbb::spin_mutex* mtx = nullptr); - Result> processIntegrationTestSrc( - const fs::path& sourceFilePath, - const std::unordered_set& buildObjTargets, - tbb::spin_mutex* mtx = nullptr); + Result> + processIntegrationTestSrc(const fs::path& sourceFilePath, + tbb::spin_mutex* mtx = nullptr); void collectBinDepObjs( // NOLINT(misc-no-recursion) std::unordered_set& deps, std::string_view sourceFileName, diff --git a/src/Builder/Project.cc b/src/Builder/Project.cc index 5324a9401..771ed53b0 100644 --- a/src/Builder/Project.cc +++ b/src/Builder/Project.cc @@ -1,7 +1,7 @@ #include "Project.hpp" #include "Algos.hpp" -#include "BuildProfile.hpp" +#include "Builder/BuildProfile.hpp" #include "Git2.hpp" #include "Rustify/Result.hpp" #include "TermColor.hpp" @@ -86,7 +86,9 @@ Project::Project(const BuildProfile& buildProfile, Manifest m, { includeIfExist(rootPath / "src", /*isSystem=*/false); includeIfExist(rootPath / "include", /*isSystem=*/false); - includeIfExist(rootPath / "tests", /*isSystem=*/false); + if (buildProfile == BuildProfile::Test) { + includeIfExist(rootPath / "tests", /*isSystem=*/false); + } compilerOpts.cFlags.others.emplace_back("-std=c++" + manifest.package.edition.str); diff --git a/src/Builder/Project.hpp b/src/Builder/Project.hpp index cf0b0ca3f..f8173b8be 100644 --- a/src/Builder/Project.hpp +++ b/src/Builder/Project.hpp @@ -1,7 +1,7 @@ #pragma once -#include "BuildProfile.hpp" -#include "Compiler.hpp" +#include "Builder/BuildProfile.hpp" +#include "Builder/Compiler.hpp" #include "Manifest.hpp" #include "Rustify/Result.hpp" diff --git a/src/Cmd/Build.cc b/src/Cmd/Build.cc index fb1237d52..f7ece8030 100644 --- a/src/Cmd/Build.cc +++ b/src/Cmd/Build.cc @@ -37,7 +37,8 @@ const Subcmd BUILD_CMD = Result runBuildCommand(const Manifest& manifest, const std::string& outDir, - const std::string& targetName) { + const std::string& targetName, + std::string displayName) { const std::vector targets{ targetName }; const bool needsBuild = Try(ninjaNeedsWork(outDir, targets)); @@ -46,7 +47,7 @@ Result runBuildCommand(const Manifest& manifest, ExitStatus exitStatus(EXIT_SUCCESS); if (needsBuild) { - Diag::info("Compiling", "{} v{} ({})", targetName, + Diag::info("Compiling", "{} v{} ({})", displayName, manifest.package.version.toString(), manifest.path.parent_path().string()); Command buildCmd(baseCmd); @@ -64,14 +65,17 @@ Result buildImpl(const Manifest& manifest, std::string& outDir, Try(emitNinja(manifest, buildProfile, /*includeDevDeps=*/false)); outDir = config.outBasePath; - ExitStatus exitStatus; - if (config.hasBinTarget()) { - exitStatus = Try(runBuildCommand(manifest, outDir, manifest.package.name)); + 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.hasLibTarget() && exitStatus.success()) { - const std::string& libName = config.getLibName(); - exitStatus = Try(runBuildCommand(manifest, outDir, libName)); + if (config.hasBinTarget() && exitStatus.success()) { + exitStatus = Try(runBuildCommand(manifest, outDir, manifest.package.name, + manifest.package.name)); } const auto end = std::chrono::steady_clock::now(); diff --git a/src/Cmd/Init.cc b/src/Cmd/Init.cc index 49c05bac7..2d218b960 100644 --- a/src/Cmd/Init.cc +++ b/src/Cmd/Init.cc @@ -46,14 +46,11 @@ static Result initMain(const CliArgsView args) { Ensure(!fs::exists("cabin.toml"), "cannot initialize an existing cabin package"); - const std::string packageName = fs::current_path().stem().string(); + const fs::path root = fs::current_path(); + const std::string packageName = root.stem().string(); Try(validatePackageName(packageName)); - std::ofstream ofs("cabin.toml"); - ofs << createCabinToml(packageName); - - Diag::info("Created", "{} `{}` package", - isBin ? "binary (application)" : "library", packageName); + Try(createProjectFiles(isBin, root, packageName, /*skipExisting=*/true)); return Ok(); } diff --git a/src/Cmd/New.cc b/src/Cmd/New.cc index c4cb25a61..c4af33a36 100644 --- a/src/Cmd/New.cc +++ b/src/Cmd/New.cc @@ -33,6 +33,8 @@ static constexpr std::string_view MAIN_CC = " return 0;\n" "}\n"; +static constexpr std::string_view LIB_CC = "int libFunction() { return 0; }\n"; + static std::string getAuthor() noexcept { try { git2::Config config = git2::Config(); @@ -74,7 +76,15 @@ static std::string getHeader(const std::string_view projectName) noexcept { } static Result writeToFile(std::ofstream& ofs, const fs::path& fpath, - const std::string_view text) { + const std::string_view text, + const bool skipIfExists = false) { + if (fs::exists(fpath)) { + if (skipIfExists) { + return Ok(); + } + Bail("refusing to overwrite `{}`; file already exists", fpath.string()); + } + ofs.open(fpath); if (ofs.is_open()) { ofs << text; @@ -88,29 +98,39 @@ static Result writeToFile(std::ofstream& ofs, const fs::path& fpath, return Ok(); } -static Result createTemplateFiles(const bool isBin, - const std::string_view projectName) { +Result createProjectFiles(const bool isBin, const fs::path& root, + const std::string_view projectName, + const bool skipExisting) { std::ofstream ofs; if (isBin) { - fs::create_directories(projectName / fs::path("src")); - Try(writeToFile(ofs, projectName / fs::path("cabin.toml"), - createCabinToml(projectName))); - Try(writeToFile(ofs, projectName / fs::path(".gitignore"), "/cabin-out")); - Try(writeToFile(ofs, projectName / fs::path("src") / "main.cc", MAIN_CC)); + fs::create_directories(root / fs::path("src")); + fs::create_directories(root / fs::path("lib")); + Try(writeToFile(ofs, root / fs::path("cabin.toml"), + createCabinToml(projectName), skipExisting)); + Try(writeToFile(ofs, root / fs::path(".gitignore"), "/cabin-out", + skipExisting)); + Try(writeToFile(ofs, root / fs::path("src") / "main.cc", MAIN_CC, + skipExisting)); + Try(writeToFile(ofs, root / fs::path("lib") / "lib.cc", LIB_CC, + skipExisting)); Diag::info("Created", "binary (application) `{}` package", projectName); } else { - fs::create_directories(projectName / fs::path("include") / projectName); - Try(writeToFile(ofs, projectName / fs::path("cabin.toml"), - createCabinToml(projectName))); - Try(writeToFile(ofs, projectName / fs::path(".gitignore"), - "/cabin-out\ncabin.lock")); + fs::create_directories(root / fs::path("include") / projectName); + fs::create_directories(root / fs::path("lib")); + fs::create_directories(root / fs::path("src")); + Try(writeToFile(ofs, root / fs::path("cabin.toml"), + createCabinToml(projectName), skipExisting)); + Try(writeToFile(ofs, root / fs::path(".gitignore"), + "/cabin-out\ncabin.lock", skipExisting)); Try(writeToFile( ofs, - (projectName / fs::path("include") / projectName / projectName).string() + (root / fs::path("include") / projectName / projectName).string() + ".hpp", - getHeader(projectName))); + getHeader(projectName), skipExisting)); + Try(writeToFile(ofs, root / fs::path("lib") / "lib.cc", LIB_CC, + skipExisting)); Diag::info("Created", "library `{}` package", projectName); } @@ -144,7 +164,7 @@ static Result newMain(const CliArgsView args) { Ensure(!fs::exists(packageName), "directory `{}` already exists", packageName); - Try(createTemplateFiles(isBin, packageName)); + Try(createProjectFiles(isBin, fs::path(packageName), packageName)); git2::Repository().init(packageName); return Ok(); } diff --git a/src/Cmd/New.hpp b/src/Cmd/New.hpp index 5c8c88a5b..74186683c 100644 --- a/src/Cmd/New.hpp +++ b/src/Cmd/New.hpp @@ -1,7 +1,9 @@ #pragma once #include "Cli.hpp" +#include "Rustify/Result.hpp" +#include #include #include @@ -9,5 +11,8 @@ namespace cabin { extern const Subcmd NEW_CMD; std::string createCabinToml(std::string_view projectName) noexcept; +Result createProjectFiles(bool isBin, const std::filesystem::path& root, + std::string_view projectName, + bool skipExisting = false); } // namespace cabin diff --git a/src/Cmd/Test.cc b/src/Cmd/Test.cc index 30b6c6270..ccf5a2713 100644 --- a/src/Cmd/Test.cc +++ b/src/Cmd/Test.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -61,30 +62,45 @@ Result Test::compileTestTargets() { return Ok(); } - std::vector ninjaTargets; - ninjaTargets.reserve(collectedTargets.size()); + std::vector testTargets; + testTargets.reserve(collectedTargets.size()); for (const BuildConfig::TestTarget& target : collectedTargets) { - ninjaTargets.push_back(target.ninjaTarget); + testTargets.push_back(target.ninjaTarget); } Command baseCmd = getNinjaCommand(); baseCmd.addArg("-C").addArg(outDir.string()); - const bool needsBuild = Try(ninjaNeedsWork(outDir, ninjaTargets)); + const auto buildTargets = [&](const std::vector& targets, + std::string_view label) -> Result { + if (targets.empty()) { + return Ok(); + } + if (!Try(ninjaNeedsWork(outDir, targets))) { + return Ok(); + } - if (needsBuild) { - Diag::info("Compiling", "{} v{} ({})", manifest.package.name, + 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& targetName : ninjaTargets) { - buildCmd.addArg(targetName); + 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")); + const auto end = std::chrono::steady_clock::now(); const std::chrono::duration elapsed = end - start; diff --git a/tests/build.cc b/tests/build.cc index 26ec27428..8ef3bf13e 100644 --- a/tests/build.cc +++ b/tests/build.cc @@ -21,6 +21,37 @@ int main() { expect(tests::fs::is_regular_file(outDir / "targets.ninja")); expect(tests::fs::is_regular_file(outDir / "ninja_project")); expect(tests::fs::is_directory(outDir / "ninja_project.d")); + expect(tests::fs::is_regular_file(outDir / "libninja_project.a")); expect(!tests::fs::exists(outDir / "Makefile")); }; + + "cabin build handles src-only packages"_test = [] { + const tests::TempDir tmp; + tests::runCabin({ "new", "binary_only" }, tmp.path).unwrap(); + const auto project = tmp.path / "binary_only"; + tests::fs::remove_all(project / "lib"); + expect(!tests::fs::exists(project / "lib")); + + const auto result = tests::runCabin({ "build" }, project).unwrap(); + expect(result.status.success()) << result.status.toString(); + + const auto outDir = project / "cabin-out" / "dev"; + expect(tests::fs::is_regular_file(outDir / "binary_only")); + expect(!tests::fs::exists(outDir / "libbinary_only.a")); + }; + + "cabin build handles library-only packages"_test = [] { + const tests::TempDir tmp; + tests::runCabin({ "new", "--lib", "widget" }, tmp.path).unwrap(); + const auto project = tmp.path / "widget"; + tests::fs::remove_all(project / "src"); + expect(!tests::fs::exists(project / "src")); + + const auto result = tests::runCabin({ "build" }, project).unwrap(); + expect(result.status.success()) << result.status.toString(); + + const auto outDir = project / "cabin-out" / "dev"; + expect(tests::fs::is_regular_file(outDir / "libwidget.a")); + expect(!tests::fs::exists(outDir / "widget")); + }; } diff --git a/tests/fmt.cc b/tests/fmt.cc index 2ac13a00b..644608ea1 100644 --- a/tests/fmt.cc +++ b/tests/fmt.cc @@ -52,7 +52,7 @@ int main() { auto sanitizedFirstOut = tests::sanitizeOutput(firstFmt.out); expect(sanitizedFirstOut.empty()); auto sanitizedFirstErr = tests::sanitizeOutput(firstFmt.err); - const std::string expectedFirstErr = " Formatted 1 out of 1 file\n"; + const std::string expectedFirstErr = " Formatted 1 out of 2 files\n"; expect(sanitizedFirstErr == expectedFirstErr); const auto afterFirst = tests::readFile(mainFile); @@ -63,7 +63,7 @@ int main() { auto sanitizedSecondOut = tests::sanitizeOutput(secondFmt.out); expect(sanitizedSecondOut.empty()); auto sanitizedSecondErr = tests::sanitizeOutput(secondFmt.err); - const std::string expectedSecondErr = " Formatted 0 out of 1 file\n"; + const std::string expectedSecondErr = " Formatted 0 out of 2 files\n"; expect(sanitizedSecondErr == expectedSecondErr); const auto afterSecond = tests::readFile(mainFile); @@ -81,6 +81,7 @@ int main() { const auto project = tmp.path / "pkg"; tests::fs::remove(project / "src/main.cc"); + tests::fs::remove(project / "lib/lib.cc"); const auto result = tests::runCabin({ "fmt" }, project).unwrap(); expect(result.status.success()); diff --git a/tests/init.cc b/tests/init.cc index 14486a0c4..5f807acb3 100644 --- a/tests/init.cc +++ b/tests/init.cc @@ -47,4 +47,20 @@ int main() { expect(secondErr == expectedSecondErr); expect(tests::fs::is_regular_file(project / "cabin.toml")); }; + + "cabin init preserves files"_test = [] { + const tests::TempDir tmp; + const auto project = tmp.path / "pkg"; + tests::fs::create_directories(project / "src"); + tests::fs::create_directories(project / "lib"); + const auto mainPath = project / "src" / "main.cc"; + tests::writeFile(mainPath, "int main() { return 42; }\n"); + + const auto result = tests::runCabin({ "init" }, project).unwrap(); + expect(result.status.success()) << result.status.toString(); + + expect(tests::readFile(mainPath) == "int main() { return 42; }\n"); + expect(tests::fs::is_regular_file(project / "lib" / "lib.cc")); + expect(tests::fs::is_regular_file(project / "cabin.toml")); + }; } diff --git a/tests/run.cc b/tests/run.cc index 05e77a75f..18cf63a2e 100644 --- a/tests/run.cc +++ b/tests/run.cc @@ -18,17 +18,26 @@ int main() { expect(result.status.success()) << result.status.toString(); auto sanitizedOut = tests::sanitizeOutput(result.out); - expect(sanitizedOut == "Hello, world!\n"); + expect(sanitizedOut == "Hello, world!\n") << sanitizedOut; const auto projectPath = tests::fs::weakly_canonical(project).string(); auto sanitizedErr = tests::sanitizeOutput(result.err, { { projectPath, "" } }); - const std::string expectedErr = - " Analyzing project dependencies...\n" - " Compiling hello_world v0.1.0 ()\n" + const std::string analyzing = " Analyzing project dependencies...\n"; + const std::string binErr = " Compiling hello_world v0.1.0 ()\n"; + const std::string libErr = + " Compiling hello_world(lib) v0.1.0 ()\n"; + const std::string tailErr = " Finished `dev` profile [unoptimized + debuginfo] target(s) in " "s\n" " Running `cabin-out/dev/hello_world`\n"; - expect(sanitizedErr == expectedErr); + + const bool errMatchesWithLib = + sanitizedErr == analyzing + binErr + libErr + tailErr; + const bool errMatchesWithLibFirst = + sanitizedErr == analyzing + libErr + binErr + tailErr; + const bool errMatchesWithoutLib = sanitizedErr == analyzing + tailErr; + expect(errMatchesWithLib || errMatchesWithLibFirst || errMatchesWithoutLib) + << sanitizedErr; expect(tests::fs::is_directory(project / "cabin-out")); expect(tests::fs::is_directory(project / "cabin-out/dev")); diff --git a/tests/test.cc b/tests/test.cc index 4c07bb855..c63e7eb19 100644 --- a/tests/test.cc +++ b/tests/test.cc @@ -23,12 +23,14 @@ static std::size_t countFiles(const tests::fs::path& root, static std::string expectedTestSummary(std::string_view projectName) { return fmt::format( " Analyzing project dependencies...\n" - " Compiling {} v0.1.0 ()\n" + " Compiling {}(lib) v0.1.0 ()\n" + " Compiling {}(test) v0.1.0 ()\n" " Finished `test` profile [unoptimized + debuginfo] target(s) in " "s\n" - " Running unit test src/main.cc (cabin-out/test/unit/main.cc.test)\n" + " Running unit test src/main.cc " + "(cabin-out/test/unit/src/main.cc.test)\n" " Ok 1 passed; 0 failed; finished in s\n", - projectName); + projectName, projectName); } int main() { @@ -204,4 +206,32 @@ int main() { const auto outDir = project / "cabin-out" / "test"; expect(countFiles(outDir, ".gcda") == 0U); }; + + "cabin test integration without lib"_test = [] { + const tests::TempDir tmp; + tests::runCabin({ "new", "bin_integration" }, tmp.path).unwrap(); + const auto project = tmp.path / "bin_integration"; + tests::fs::remove_all(project / "lib"); + const auto testsDir = project / "tests"; + tests::fs::create_directories(testsDir); + tests::writeFile(testsDir / "smoke.cc", + R"(#include + +#ifdef CABIN_TEST +int main() { + std::cout << "integration smoke ... ok" << std::endl; + return 0; +} +#else +int main() { return 0; } +#endif +)"); + + const auto result = tests::runCabin({ "test" }, project).unwrap(); + expect(result.status.success()) << result.status.toString(); + const auto sanitizedOut = tests::sanitizeOutput(result.out); + expect(sanitizedOut.contains("integration smoke ... ok")); + const auto testBinary = project / "cabin-out" / "test" / "intg" / "smoke"; + expect(tests::fs::is_regular_file(testBinary)); + }; } diff --git a/tests/version.cc b/tests/version.cc index c2646c340..c679deef8 100644 --- a/tests/version.cc +++ b/tests/version.cc @@ -31,15 +31,19 @@ int main() { expect(result.status.success()); const auto actual = trim(result.out); static const std::regex pattern( - R"(^cabin ([^\s]+) \(([0-9a-f]{8}) ([0-9]{4}-[0-9]{2}-[0-9]{2})\)$)"); + R"(^cabin ([^\s]+) \(([0-9a-f]{8})(?: ([0-9]{4}-[0-9]{2}-[0-9]{2}))?\)$)"); std::smatch match; expect(std::regex_match(actual, match, pattern)); expect(match[1].str() == version); auto sanitizedOut = tests::sanitizeOutput(result.out); - const std::string expectedOut = + const std::string expectedWithDate = fmt::format("cabin {} ( )\n", version); - expect(sanitizedOut == expectedOut); + const std::string expectedWithoutDate = + fmt::format("cabin {} ()\n", version); + expect(sanitizedOut == expectedWithDate + || sanitizedOut == expectedWithoutDate) + << sanitizedOut; auto sanitizedErr = tests::sanitizeOutput(result.err); expect(sanitizedErr.empty()); };