diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 68516be..389c744 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: pacman-key --populate archlinux pacman -Sy --noconfirm --needed archlinux-keyring pacman -Syu --noconfirm --needed \ - base-devel git zip tar \ + base-devel git zip tar jq \ clang lld libc++ cmake \ mingw-w64-gcc \ wasi-libc wasi-libc++ wasi-libc++abi wasi-compiler-rt \ diff --git a/build.cmd b/build.cmd index 4ca107e..33d3430 100644 --- a/build.cmd +++ b/build.cmd @@ -16,6 +16,7 @@ copy /Y interfaces\Crafter.Build-Implementation.cppm share\crafter-build\ copy /Y interfaces\Crafter.Build-External.cppm share\crafter-build\ copy /Y interfaces\Crafter.Build-Clang.cppm share\crafter-build\ copy /Y interfaces\Crafter.Build-Test.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Progress.cppm share\crafter-build\ copy /Y interfaces\Crafter.Build-Api.h share\crafter-build\ if not exist .\build\glslang\NUL git clone https://github.com/KhronosGroup/glslang.git .\build\glslang @@ -53,6 +54,7 @@ clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Implementation clang++ %common_options% -fmodule-output interfaces\Crafter.Build-External.cppm -o .\build\Crafter.Build-External.o clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Clang.cppm -o .\build\Crafter.Build-Clang.o clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Test.cppm -o .\build\Crafter.Build-Test.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Progress.cppm -o .\build\Crafter.Build-Progress.o clang++ %common_options% -fmodule-output interfaces\Crafter.Build.cppm -o .\build\Crafter.Build.o clang++ %common_options% .\implementations\Crafter.Build-Shader.cpp -o .\build\Crafter.Build-Shader_impl.o @@ -62,6 +64,7 @@ clang++ %common_options% .\implementations\Crafter.Build-Implementation.cpp -o . clang++ %common_options% .\implementations\Crafter.Build-External.cpp -o .\build\Crafter.Build-External_impl.o clang++ %common_options% .\implementations\Crafter.Build-Clang.cpp -o .\build\Crafter.Build-Clang_impl.o clang++ %common_options% .\implementations\Crafter.Build-Test.cpp -o .\build\Crafter.Build-Test_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Progress.cpp -o .\build\Crafter.Build-Progress_impl.o clang++ %common_options% .\implementations\main.cpp -o .\build\main.o REM Step 1: link all impl .o files into crafter-build.dll, generating crafter-build.lib import lib @@ -75,6 +78,7 @@ clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtu .\build\Crafter.Build-External.o ^ .\build\Crafter.Build-Clang.o ^ .\build\Crafter.Build-Test.o ^ + .\build\Crafter.Build-Progress.o ^ .\build\Crafter.Build.o ^ .\build\Crafter.Build-Shader_impl.o ^ .\build\Crafter.Build-Platform_impl.o ^ @@ -83,6 +87,7 @@ clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtu .\build\Crafter.Build-External_impl.o ^ .\build\Crafter.Build-Clang_impl.o ^ .\build\Crafter.Build-Test_impl.o ^ + .\build\Crafter.Build-Progress_impl.o ^ -o .\bin\crafter-build.dll REM Step 2: link the launcher exe against crafter-build.lib @@ -100,6 +105,7 @@ llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^ .\build\Crafter.Build-External.o ^ .\build\Crafter.Build-Clang.o ^ .\build\Crafter.Build-Test.o ^ + .\build\Crafter.Build-Progress.o ^ .\build\Crafter.Build.o ^ .\build\Crafter.Build-Shader_impl.o ^ .\build\Crafter.Build-Platform_impl.o ^ @@ -107,6 +113,7 @@ llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^ .\build\Crafter.Build-Implementation_impl.o ^ .\build\Crafter.Build-External_impl.o ^ .\build\Crafter.Build-Clang_impl.o ^ - .\build\Crafter.Build-Test_impl.o + .\build\Crafter.Build-Test_impl.o ^ + .\build\Crafter.Build-Progress_impl.o copy /Y "%LIBCXX_DIR%\lib\c++.dll" ".\bin\c++.dll" diff --git a/build.sh b/build.sh index b93c2ee..ef2c886 100755 --- a/build.sh +++ b/build.sh @@ -15,6 +15,7 @@ cp interfaces/Crafter.Build-Implementation.cppm share/crafter-build/ cp interfaces/Crafter.Build-External.cppm share/crafter-build/ cp interfaces/Crafter.Build-Clang.cppm share/crafter-build/ cp interfaces/Crafter.Build-Test.cppm share/crafter-build/ +cp interfaces/Crafter.Build-Progress.cppm share/crafter-build/ cp interfaces/Crafter.Build-Api.h share/crafter-build/ git clone https://github.com/KhronosGroup/glslang.git ./build/glslang @@ -50,6 +51,7 @@ clang++ $common_options -fmodule-output interfaces/Crafter.Build-Implementation. clang++ $common_options -fmodule-output interfaces/Crafter.Build-External.cppm -o ./build/Crafter.Build-External.o clang++ $common_options -fmodule-output interfaces/Crafter.Build-Clang.cppm -o ./build/Crafter.Build-Clang.o clang++ $common_options -fmodule-output interfaces/Crafter.Build-Test.cppm -o ./build/Crafter.Build-Test.o +clang++ $common_options -fmodule-output interfaces/Crafter.Build-Progress.cppm -o ./build/Crafter.Build-Progress.o clang++ $common_options -fmodule-output interfaces/Crafter.Build.cppm -o ./build/Crafter.Build.o clang++ $common_options ./implementations/Crafter.Build-Shader.cpp -o ./build/Crafter.Build-Shader_impl.o @@ -59,6 +61,7 @@ clang++ $common_options ./implementations/Crafter.Build-Implementation.cpp -o ./ clang++ $common_options ./implementations/Crafter.Build-External.cpp -o ./build/Crafter.Build-External_impl.o clang++ $common_options ./implementations/Crafter.Build-Clang.cpp -o ./build/Crafter.Build-Clang_impl.o clang++ $common_options ./implementations/Crafter.Build-Test.cpp -o ./build/Crafter.Build-Test_impl.o +clang++ $common_options ./implementations/Crafter.Build-Progress.cpp -o ./build/Crafter.Build-Progress_impl.o clang++ $common_options ./implementations/main.cpp -o ./build/main.o ar rcs ./lib/libcrafter-build.a \ @@ -69,6 +72,7 @@ ar rcs ./lib/libcrafter-build.a \ ./build/Crafter.Build-External.o \ ./build/Crafter.Build-Clang.o \ ./build/Crafter.Build-Test.o \ + ./build/Crafter.Build-Progress.o \ ./build/Crafter.Build.o \ ./build/Crafter.Build-Shader_impl.o \ ./build/Crafter.Build-Platform_impl.o \ @@ -76,7 +80,8 @@ ar rcs ./lib/libcrafter-build.a \ ./build/Crafter.Build-Implementation_impl.o \ ./build/Crafter.Build-External_impl.o \ ./build/Crafter.Build-Clang_impl.o \ - ./build/Crafter.Build-Test_impl.o + ./build/Crafter.Build-Test_impl.o \ + ./build/Crafter.Build-Progress_impl.o clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \ -Wl,--export-dynamic \ @@ -88,6 +93,7 @@ clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \ ./build/Crafter.Build-External.o \ ./build/Crafter.Build-Clang.o \ ./build/Crafter.Build-Test.o \ + ./build/Crafter.Build-Progress.o \ ./build/Crafter.Build.o \ ./build/Crafter.Build-Shader_impl.o \ ./build/Crafter.Build-Platform_impl.o \ @@ -96,6 +102,7 @@ clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \ ./build/Crafter.Build-External_impl.o \ ./build/Crafter.Build-Clang_impl.o \ ./build/Crafter.Build-Test_impl.o \ + ./build/Crafter.Build-Progress_impl.o \ ./build/main.o \ -lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \ -ldl \ diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 885c779..80248c1 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -22,6 +22,7 @@ import std; import :Clang; import :Platform; import :Test; +import :Progress; namespace fs = std::filesystem; using namespace Crafter; @@ -252,6 +253,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map fs::last_write_time(objPath)) { threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config]() { + Progress::Task task(std::format("Compiling {}.c", cFile.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; std::string result = RunCommand(std::format("clang {}.c --target={} -march={} -mtune={} -O3 -c -o {}_source.o", cFile.string(), config.target, config.march, config.mtune, (buildDir / cFile.filename()).string())); @@ -533,6 +542,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map fs::last_write_time(objPath)) { threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled]() { + Progress::Task task(std::format("Compiling {}.cu", cFile.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; std::string result = RunCommand(std::format("nvcc {}.cu -c -o {}_source.o -O3 -arch=sm_89", cFile.string(), (buildDir / cFile.filename()).string())); @@ -580,6 +590,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_mapCheck(pcmDir, externalFloor)) { Module* mod = interface.get(); threads.emplace_back([mod, &command, &pcmDir, &buildDir, &buildCancelled, &buildError]() { + Progress::Task task(std::format("Compiling interface {}", mod->path.filename().string())); try { mod->Compile(command, pcmDir, buildDir, buildCancelled, buildError); } catch (const std::exception& e) { @@ -602,6 +613,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_mappath.filename().string())); try { impl->Compile(command, buildDir, buildCancelled, buildError); } catch (const std::exception& e) { @@ -694,6 +706,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map buffer; std::string result; @@ -555,6 +557,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span buffer; std::string result; diff --git a/implementations/Crafter.Build-Progress.cpp b/implementations/Crafter.Build-Progress.cpp new file mode 100644 index 0000000..31ddcd3 --- /dev/null +++ b/implementations/Crafter.Build-Progress.cpp @@ -0,0 +1,165 @@ +/* +Crafter® Build +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +module; +#include +#include +#if defined(_WIN32) + #include + #include + #ifndef STDOUT_FILENO + #define STDOUT_FILENO _fileno(stdout) + #endif + #define CRAFTER_PROGRESS_ISATTY _isatty +#else + #include + #include + #define CRAFTER_PROGRESS_ISATTY isatty +#endif + +export module Crafter.Build:Progress_impl; +import std; +import :Progress; + +namespace { + std::mutex g_mutex; + std::atomic g_total{0}; + std::atomic g_done{0}; + Crafter::Progress::Verbosity g_verbosity = Crafter::Progress::Verbosity::Default; + bool g_isTty = false; + bool g_lineDirty = false; // status line has uncleared content + bool g_finalized = false; + std::chrono::steady_clock::time_point g_startTime = std::chrono::steady_clock::now(); + + int TerminalWidth() { + #if defined(_WIN32) + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO info; + if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) { + int w = info.srWindow.Right - info.srWindow.Left + 1; + if (w > 0) return w; + } + #else + winsize ws{}; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { + return ws.ws_col; + } + #endif + if (const char* col = std::getenv("COLUMNS")) { + try { int c = std::stoi(col); if (c > 0) return c; } catch (...) {} + } + return 80; + } + + // Caller holds g_mutex. + void RenderStatus(std::string_view label) { + if (g_verbosity != Crafter::Progress::Verbosity::Default || !g_isTty) return; + int done = g_done.load(std::memory_order_relaxed); + int total = g_total.load(std::memory_order_relaxed); + std::string prefix = std::format("[{}/{}] ", done, total); + int width = TerminalWidth(); + int avail = width - static_cast(prefix.size()) - 1; + std::string trimmed{label}; + if (avail > 0 && static_cast(trimmed.size()) > avail) { + trimmed.resize(static_cast(avail)); + } + // \r returns to col 0, \033[2K erases the whole line. No newline. + std::fputs("\r\033[2K", stdout); + std::fputs(prefix.c_str(), stdout); + std::fputs(trimmed.c_str(), stdout); + std::fflush(stdout); + g_lineDirty = true; + } + + // Caller holds g_mutex. + void ClearLineLocked() { + if (g_lineDirty) { + std::fputs("\r\033[2K", stdout); + std::fflush(stdout); + g_lineDirty = false; + } + } +} + +namespace Crafter::Progress { + +void SetVerbosity(Verbosity v) { + std::lock_guard lock(g_mutex); + g_verbosity = v; + g_isTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0; + g_startTime = std::chrono::steady_clock::now(); + g_total.store(0); + g_done.store(0); + g_finalized = false; +} + +Verbosity GetVerbosity() { + std::lock_guard lock(g_mutex); + return g_verbosity; +} + +Task::Task(std::string label) : label_(std::move(label)) { + g_total.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_mutex); + if (g_verbosity == Verbosity::Default && g_isTty) { + RenderStatus(label_); + } +} + +Task::~Task() { + int done = g_done.fetch_add(1, std::memory_order_relaxed) + 1; + std::lock_guard lock(g_mutex); + if (g_verbosity == Verbosity::Default) { + if (g_isTty) { + RenderStatus(label_); + } else { + // Non-TTY: one append-only line per completed task (ninja-style). + int total = g_total.load(std::memory_order_relaxed); + std::println("[{}/{}] {}", done, total, label_); + } + } +} + +void EchoCommand(std::string_view command) { + std::lock_guard lock(g_mutex); + if (g_verbosity == Verbosity::Verbose) { + std::println("$ {}", command); + } +} + +void Clear() { + std::lock_guard lock(g_mutex); + ClearLineLocked(); +} + +void Finalize() { + std::lock_guard lock(g_mutex); + if (g_finalized) return; + g_finalized = true; + ClearLineLocked(); + if (g_verbosity == Verbosity::Quiet) return; + int done = g_done.load(); + if (done == 0) return; // Nothing happened (cached build); stay silent. + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - g_startTime); + std::println("Built {} step{} in {}ms", + done, done == 1 ? "" : "s", elapsed.count()); +} + +} // namespace Crafter::Progress diff --git a/implementations/Crafter.Build-Test.cpp b/implementations/Crafter.Build-Test.cpp index bdae218..67a2b09 100644 --- a/implementations/Crafter.Build-Test.cpp +++ b/implementations/Crafter.Build-Test.cpp @@ -22,6 +22,7 @@ import std; import :Test; import :Clang; import :Platform; +import :Progress; namespace fs = std::filesystem; using namespace Crafter; @@ -161,6 +162,7 @@ namespace { } void PrintResult(const TestResult& r, std::string_view runnerName) { + Progress::Clear(); auto ms = r.duration.count(); std::string runnerSuffix = (runnerName.empty() || runnerName == "local") ? std::string() @@ -719,6 +721,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions& } summary.results = std::move(results); + Progress::Clear(); std::print("\n"); std::vector parts; if (summary.passed) parts.push_back(std::format("{} passed", summary.passed)); diff --git a/interfaces/Crafter.Build-Progress.cppm b/interfaces/Crafter.Build-Progress.cppm new file mode 100644 index 0000000..b62571f --- /dev/null +++ b/interfaces/Crafter.Build-Progress.cppm @@ -0,0 +1,57 @@ +/* +Crafter® Build +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +module; +#include "Crafter.Build-Api.h" +export module Crafter.Build:Progress; +import std; + +export namespace Crafter::Progress { + enum class Verbosity { Quiet, Default, Verbose }; + + CRAFTER_API void SetVerbosity(Verbosity v); + CRAFTER_API Verbosity GetVerbosity(); + + // RAII work-unit marker. Construction inflates the running total; destruction + // increments completed and (on Default + TTY) redraws the status line. Move + // semantics are disabled — keep these as plain stack locals inside the worker + // lambda so the destructor fires when that thread's work ends. + class CRAFTER_API Task { + public: + explicit Task(std::string label); + ~Task(); + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + Task(Task&&) = delete; + Task& operator=(Task&&) = delete; + private: + std::string label_; + }; + + // Verbose-mode command echo. No-op outside Verbose. + CRAFTER_API void EchoCommand(std::string_view command); + + // Erase the in-place status line so subsequent stderr writes (errors, + // banners) don't collide with it. No-op when not in TTY-redraw mode. + CRAFTER_API void Clear(); + + // Print "Built N targets in Xms" (or equivalent) and clear in-place state. + // Safe to call multiple times; subsequent calls are no-ops. + CRAFTER_API void Finalize(); +} diff --git a/interfaces/Crafter.Build.cppm b/interfaces/Crafter.Build.cppm index 52e49cf..faf3f72 100644 --- a/interfaces/Crafter.Build.cppm +++ b/interfaces/Crafter.Build.cppm @@ -23,4 +23,5 @@ export import :Platform; export import :Implementation; export import :Shader; export import :External; -export import :Test; \ No newline at end of file +export import :Test; +export import :Progress; \ No newline at end of file diff --git a/project.cpp b/project.cpp index 54e484a..f90051b 100644 --- a/project.cpp +++ b/project.cpp @@ -35,7 +35,7 @@ extern "C" Configuration CrafterBuildProject(std::span a : ConfigurationType::LibraryStatic; crafterBuildLib->debug = debug; { - std::array interfaces = { + std::array interfaces = { "interfaces/Crafter.Build", "interfaces/Crafter.Build-Shader", "interfaces/Crafter.Build-Platform", @@ -44,8 +44,9 @@ extern "C" Configuration CrafterBuildProject(std::span a "interfaces/Crafter.Build-External", "interfaces/Crafter.Build-Clang", "interfaces/Crafter.Build-Test", + "interfaces/Crafter.Build-Progress", }; - std::array implementations = { + std::array implementations = { "implementations/Crafter.Build-Shader", "implementations/Crafter.Build-Platform", "implementations/Crafter.Build-Interface", @@ -53,6 +54,7 @@ extern "C" Configuration CrafterBuildProject(std::span a "implementations/Crafter.Build-External", "implementations/Crafter.Build-Clang", "implementations/Crafter.Build-Test", + "implementations/Crafter.Build-Progress", }; crafterBuildLib->GetInterfacesAndImplementations(interfaces, implementations); }