From 26977640e297263839cfa6eb448f7f99836ce79e Mon Sep 17 00:00:00 2001 From: Luis Mejia Date: Wed, 3 Apr 2024 01:58:22 -0700 Subject: [PATCH] Updated gb and scripts. --- .github/workflows/release.yml | 33 +-- .gitignore | 3 +- README.md | 22 +- _gcolors | 13 +- build | 21 +- build.bat | 76 ++++++ code/CMakeLists.txt | 27 ++- code/include/GradleParams.h | 2 +- code/include/gb.h | 39 +++- code/include/gb/DirectoryWatcher.h | 125 ++++++++++ code/include/gb/ReplaceableVars.h | 37 ++- code/include/gb/ShutdownMonitor.h | 20 +- code/include/gb/concurrent/Task.h | 137 +++++++++++ code/include/gb/concurrent/TaskRunner.h | 94 ++++++++ code/include/gb/constants.h | 17 ++ code/include/gb/functions.h | 34 +++ code/include/gb/hash.h | 31 +++ code/include/gb/process.h | 7 +- code/include/gb/strings.h | 54 ++--- code/include/gb/{console.h => terminal.h} | 12 +- code/src/App.cpp | 33 +-- code/src/gb/DirectoryWatcher.cpp | 273 ++++++++++++++++++++++ code/src/gb/ShutdownMonitor.cpp | 3 +- code/src/gb/concurrent/Task.cpp | 83 +++++++ code/src/gb/concurrent/TaskRunner.cpp | 102 ++++++++ code/src/gb/constants.cpp | 10 + code/src/gb/files.cpp | 10 +- code/src/gb/process.cpp | 22 +- code/src/gb/strings.cpp | 24 +- code/src/gb/{console.cpp => terminal.cpp} | 9 +- code/src/main.cpp | 3 +- code/test/CMakeLists.txt | 86 +++++++ code/test/TaskRunnerTest.cc | 114 +++++++++ release.md | 4 +- run | 26 ++- test | 70 ++++++ version | 2 +- 37 files changed, 1514 insertions(+), 164 deletions(-) create mode 100755 build.bat create mode 100644 code/include/gb/DirectoryWatcher.h create mode 100644 code/include/gb/concurrent/Task.h create mode 100644 code/include/gb/concurrent/TaskRunner.h create mode 100644 code/include/gb/constants.h create mode 100644 code/include/gb/functions.h create mode 100644 code/include/gb/hash.h rename code/include/gb/{console.h => terminal.h} (81%) create mode 100644 code/src/gb/DirectoryWatcher.cpp create mode 100644 code/src/gb/concurrent/Task.cpp create mode 100644 code/src/gb/concurrent/TaskRunner.cpp create mode 100644 code/src/gb/constants.cpp rename code/src/gb/{console.cpp => terminal.cpp} (87%) create mode 100644 code/test/CMakeLists.txt create mode 100644 code/test/TaskRunnerTest.cc create mode 100755 test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0796e4e..426dcbb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,35 +53,42 @@ jobs: strategy: matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ ubuntu-latest, macos-latest, macos-14, windows-latest ] steps: - name: Checkout uses: actions/checkout@v4 - name: Build - run: | - cd code - cmake -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -B build.cmake -S . - cmake --build build.cmake --config ${{ env.BUILD_TYPE }} --parallel - # Output Linux: ./code/bin/ - # Output Windows: ./code/bin/${{ env.BUILD_TYPE }}/.exe + run: ./build ${{ env.BUILD_TYPE }} clean + # Output Linux: ./code/build/bin/ + # Output Windows: ./code/build/bin/${{ env.BUILD_TYPE }}/.exe - - name: Zip Linux + - name: Zip Ubuntu if: matrix.os == 'ubuntu-latest' env: - ZIP_NAME: ${{ env.PROJECT_NAME }}-linux-x64.tar.gz - BIN: code/bin + ZIP_NAME: ${{ env.PROJECT_NAME }}-ubuntu-x64.tar.gz + BIN: code/build/bin PROJECT_BIN: ${{ env.PROJECT_NAME }} run: | tar -czf ${{ env.ZIP_NAME }} -C ${{ env.BIN }} ${{ env.PROJECT_BIN }} gh release upload ${{ env.VERSION }} ${{ env.ZIP_NAME }} --clobber - - name: Zip macOS + - name: Zip macOS x64 if: matrix.os == 'macos-latest' env: ZIP_NAME: ${{ env.PROJECT_NAME }}-macos-x64.tar.gz - BIN: code/bin + BIN: code/build/bin + PROJECT_BIN: ${{ env.PROJECT_NAME }} + run: | + tar -czf ${{ env.ZIP_NAME }} -C ${{ env.BIN }} ${{ env.PROJECT_BIN }} + gh release upload ${{ env.VERSION }} ${{ env.ZIP_NAME }} --clobber + + - name: Zip macOS aarch64 + if: matrix.os == 'macos-14' + env: + ZIP_NAME: ${{ env.PROJECT_NAME }}-macos-aarch64.tar.gz + BIN: code/build/bin PROJECT_BIN: ${{ env.PROJECT_NAME }} run: | tar -czf ${{ env.ZIP_NAME }} -C ${{ env.BIN }} ${{ env.PROJECT_BIN }} @@ -91,7 +98,7 @@ jobs: if: matrix.os == 'windows-latest' env: ZIP_NAME: ${{ env.PROJECT_NAME }}-windows-x64.zip - BIN: code/bin/${{ env.BUILD_TYPE }} + BIN: code/build/bin/${{ env.BUILD_TYPE }} PROJECT_BIN: ${{ env.PROJECT_NAME }}.exe run: | tar -czf ${{ env.ZIP_NAME }} -C ${{ env.BIN }} ${{ env.PROJECT_BIN }} diff --git a/.gitignore b/.gitignore index da745ca..146cf0b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ .vscode/ # Build dirs. -build.cmake/ -bin/ +/code/build/ diff --git a/README.md b/README.md index d07a6c4..a9b478f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # grun -![Version](https://img.shields.io/badge/Version-1.0.0-blue) +![Version](https://img.shields.io/badge/Version-1.1.0-blue) -This is a Gradle project runner. It builds, unpacks, and runs a Gradle project on the current console. +This is a Gradle project runner. It builds, unpacks, and runs a Gradle project on the current terminal. Also allows `SIGINT` (Ctrl+C) to be captured by your application. -As opposed to Gradle's run task which doesn't attach a proper console and simply kills your app immediately on `SIGINT`. ## Use it! @@ -22,24 +21,17 @@ As opposed to Gradle's run task which doesn't attach a proper console and simply ## Get it! -It is highly recommended that you build it yourself on your platform. *On my M1 Mac, the final binary is only 90KB.* +It is highly recommended that you build it yourself on your platform. -If you want a pre-compiled binary, check the [latest release](https://github.com/GlitchyByte/grun/releases) for your platform. -These are built using GitHub Actions on GitHub Runners and produce larger binaries than if you build locally. +If you want a pre-compiled binary for macOS, Windows, or Ubuntu, check the [latest release](https://github.com/GlitchyByte/grun/releases). +These are built using GitHub Actions on GitHub Runners. ## Build it! -Clone this repo. - -Then, on macOS or Linux: +Clone this repo. Then: ./build MinSizeRel clean -On Windows: - - cmake -DCMAKE_BUILD_TYPE=MinSizeRel -B build.cmake -S . - cmake --build build.cmake --config MinSizeRel --parallel - ### Receive your tasty binary! -After building, `grun` executable is in the `bin` directory. +After building, `grun` executable is in the `code/bin` directory. diff --git a/_gcolors b/_gcolors index f47a14b..81b9483 100644 --- a/_gcolors +++ b/_gcolors @@ -2,12 +2,16 @@ # Copyright 2022-2023 GlitchyByte # SPDX-License-Identifier: MIT -# _gcolors v1.2.0 +# _gcolors v1.2.1 # Find out terminal color support. -readonly _tput_colors="$(tput colors 2>/dev/null)" -if [[ -n "${_tput_colors}" && "${_tput_colors}" != "0" ]]; then - readonly term_color_count="${_tput_colors}" +if [[ -t 1 ]]; then + readonly _tput_colors="$(tput colors 2>/dev/null)" + if [[ -n "${_tput_colors}" && "${_tput_colors}" != "0" ]]; then + readonly term_color_count="${_tput_colors}" + else + readonly term_color_count="0" + fi else readonly term_color_count="0" fi @@ -59,6 +63,7 @@ else readonly cb_white=$(tput setab 7) fi +# Pick from 256 colors. color256() { if [ "${term_color_count}" -lt "256" ]; then echo "" diff --git a/build b/build index 27ea918..eb210f3 100755 --- a/build +++ b/build @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Copyright 2023 GlitchyByte +# Copyright 2023-2024 GlitchyByte # SPDX-License-Identifier: MIT-0 # Builds project. # [Setup] -set -u -set -e +set -u # Exit with an error if a variable is used without being set. +set -e # Exit if any command returns an error. # Capture caller directory and script directory. readonly calling_dir="${PWD}" readonly script_dir="$(cd "$(dirname "$0")" && pwd)" @@ -46,8 +46,13 @@ fi # Dir constants. cd "${script_dir}/code" -readonly buildConfigDir="build.cmake" -readonly binDir="bin" +readonly buildConfigDir="build/build.cmake" +readonly binDir="build/bin" + +# Make sure build dir exists. +if [ ! -d "build" ]; then + mkdir "build" +fi if [ "$clean" == "yes" ]; then echo "${c_bold}${cf_black}Refreshing configuration...${c_reset}" @@ -70,8 +75,8 @@ cmake -DCMAKE_BUILD_TYPE=${flavor} -B "${buildConfigDir}" -S . echo "${c_bold}${cf_black}Building: ${cf_white}${flavor}${c_reset}" cmake --build "${buildConfigDir}" --config ${flavor} --parallel -# Done! -echo "${cf_green}${c_bold}Build done!${c_reset}" - # [Teardown] cd "${calling_dir}" + +# Done! +echo "${cf_green}${c_bold}Build done!${c_reset}" diff --git a/build.bat b/build.bat new file mode 100755 index 0000000..7777df8 --- /dev/null +++ b/build.bat @@ -0,0 +1,76 @@ +:: Copyright 2024 GlitchyByte +:: SPDX-License-Identifier: MIT-0 + +:: Builds project. + +:: [Setup] +@echo off +setlocal enabledelayedexpansion +set "calling_dir=%CD%" +set "script_dir=%~dp0" +cd /d "%script_dir%" + +:: [Main] +goto afterFuncs + +:: Usage. +:printUsage +echo Usage: build [Debug^|Release^|MinSizeRel^|RelWithDebInfo] [clean] +cd /d "%calling_dir%" +exit /b 1 + +:afterFuncs + +:: Accept 1 or more parameters. +if "%~1"=="" goto printUsage + +:: If there is more than 1 parameter, the 2nd must be "clean". +if not "%~2"=="" ( + if /i "%~2"=="clean" ( + set "clean=yes" + ) else ( + goto printUsage + ) +) else ( + set "clean=no" +) + +:: Capture valid flavor. +set "flavor=%~1" +if /i not "%flavor%"=="Debug" if /i not "%flavor%"=="Release" if /i not "%flavor%"=="MinSizeRel" if /i not "%flavor%"=="RelWithDebInfo" ( + goto printUsage +) + +:: Dir constants. +cd /d "%script_dir%\code" +set "buildConfigDir=build\build.cmake" +set "binDir=build\bin" + +:: Make sure build dir exists. +if not exist "build" mkdir "build" + +if "%clean%"=="yes" ( + echo Refreshing configuration... + :: Remove build dir. + if exist "%buildConfigDir%" rmdir /s /q "%buildConfigDir%" + :: Clean bin dir. + if exist "%binDir%" ( + rmdir /s /q "%binDir%" + mkdir "%binDir%" + ) +) + +:: Configure. +echo Configuring: !flavor! +cmake -DCMAKE_BUILD_TYPE=!flavor! -B "%buildConfigDir%" -S . + +:: Build. +echo Building: !flavor! +cmake --build "%buildConfigDir%" --config %flavor% --parallel + + +:: [Teardown] +cd /d "%calling_dir%" + +:: Done! +echo Build done! diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index c2095c0..41bd9c0 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -1,8 +1,12 @@ -# Copyright 2023 GlitchyByte +# Copyright 2023-2024 GlitchyByte # SPDX-License-Identifier: MIT-0 cmake_minimum_required(VERSION 3.26) +set(IS_MACOS CMAKE_SYSTEM_NAME STREQUAL "Darwin") +set(IS_LINUX CMAKE_SYSTEM_NAME STREQUAL "Linux") +set(IS_WINDOWS CMAKE_SYSTEM_NAME STREQUAL "Windows") + if (NOT CMAKE_BUILD_TYPE) # Force MinSizeRel if no build type specified. set(CMAKE_BUILD_TYPE "MinSizeRel" CACHE STRING "Type of build." FORCE) @@ -20,14 +24,15 @@ message(STATUS "Platform: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR}") set(CMAKE_CXX_STANDARD 20) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../bin) +# Set flags. if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") - if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Darwin") + if (${IS_LINUX} OR ${IS_MACOS}) # Linux and macOS. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto -ffunction-sections -fdata-sections") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto -ffunction-sections -fdata-sections") - elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") - # Windows has not been tested. + elseif (${IS_WINDOWS}) + # Windows. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GL") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /GL") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LTCG") @@ -41,8 +46,17 @@ endif () file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") add_executable(${APP} ${SOURCES}) +#if (${IS_MACOS}) +# target_link_libraries(${APP} +# PUBLIC "-framework CoreFoundation" +# PUBLIC "-framework CoreServices" +# ) +#endif () + +# Add all h files in include dir. target_include_directories(${APP} PRIVATE include) +# Minimize binary size. if ((CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") AND (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Darwin")) # Strip all remaining symbols and relocation information. @@ -52,3 +66,8 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRe COMMAND strip $ ) endif () + +# Add test project if CLion build for code insight. +if (CLION_BUILD) + add_subdirectory(test) +endif () diff --git a/code/include/GradleParams.h b/code/include/GradleParams.h index 50824a0..8ba60ed 100644 --- a/code/include/GradleParams.h +++ b/code/include/GradleParams.h @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once diff --git a/code/include/gb.h b/code/include/gb.h index a1b1a7e..ea264c7 100644 --- a/code/include/gb.h +++ b/code/include/gb.h @@ -1,23 +1,45 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once #include +#ifdef __APPLE__ + #define GB_IS_MACOS +#endif +#ifdef __linux__ + #define GB_IS_LINUX +#endif +#ifdef _WIN32 + #define GB_IS_WINDOWS +#endif + +//#define GB_HASH #define GB_STRINGS - #define GB_CONSOLE + #define GB_TERMINAL +//#define GB_FUNCTIONS //#define GB_FILES #define GB_PROCESS #define GB_REPLACEABLE_VARS //#define GB_SHUTDOWN_MONITOR +//#define GB_CONCURRENT +// #define GB_TASK +//#define GB_DIRECTORY_WATCHER +#include "gb/constants.h" +#ifdef GB_HASH + #include "gb/hash.h" +#endif #ifdef GB_STRINGS #include "gb/strings.h" - #ifdef GB_CONSOLE - #include "gb/console.h" + #ifdef GB_TERMINAL + #include "gb/terminal.h" #endif #endif +#ifdef GB_FUNCTIONS + #include "gb/functions.h" +#endif #ifdef GB_FILES #include "gb/files.h" #endif @@ -30,3 +52,12 @@ #ifdef GB_SHUTDOWN_MONITOR #include "gb/ShutdownMonitor.h" #endif +#ifdef GB_CONCURRENT + #ifdef GB_TASK + #include "gb/concurrent/Task.h" + #include "gb/concurrent/TaskRunner.h" + #endif +#endif +#ifdef GB_DIRECTORY_WATCHER + #include "gb/DirectoryWatcher.h" +#endif diff --git a/code/include/gb/DirectoryWatcher.h b/code/include/gb/DirectoryWatcher.h new file mode 100644 index 0000000..32d809c --- /dev/null +++ b/code/include/gb/DirectoryWatcher.h @@ -0,0 +1,125 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gb.h" +#ifdef GB_DIRECTORY_WATCHER + +#include +#include +#include +#ifdef GB_IS_MACOS +#include +#endif +#ifdef GB_IS_LINUX +#include +#include +#include +#endif +#ifdef GB_IS_WINDOWS +#include +#include +#endif + +namespace gb { + + using WatchCallback = std::function; + + /** + * Directory watcher facility to react to changes in a directory. + */ + class DirectoryWatcher { + private: +#ifdef GB_IS_MACOS + static std::atomic queueId; +#endif + + private: + class CallbackRunnerTask; + + private: + std::vector paths; + WatchCallback callback; + void* callbackContext; + std::mutex watchLock; + std::atomic _isWatching { false }; + std::unique_ptr runner { std::make_unique() }; + std::mutex actionLock; + std::shared_ptr actionTask; +#ifdef GB_IS_MACOS + CFMutableArrayRef cfPaths; + FSEventStreamContext streamContext { 0, this, nullptr, nullptr, nullptr }; + FSEventStreamRef stream; + dispatch_queue_t queue; +#endif +#ifdef GB_IS_LINUX + std::thread watchThread; + int cancelPipeFds[2] {}; + int inotifyFd { 0 }; + int epollFd { 0 }; +#endif +#ifdef GB_IS_WINDOWS + std::thread watchThread; + OVERLAPPED watcherContext { .Pointer = this }; + HANDLE hCancelEvent { NULL }; +#endif + + public: + /** + * Creates a directory watcher. + * + *

It doesn't start watching and calling the callback until start() is issued. + * + * @param paths Collection of paths to observe. + * @param callback Callback method to execute when a change is observed. + * @param callbackContext Callback object on which the callback method will be executed. + */ + explicit DirectoryWatcher(std::vector const& paths, WatchCallback const& callback, + void* callbackContext = nullptr) noexcept : paths(paths), callback(callback), callbackContext(callbackContext) {}; + + /** + * Creates a directory watcher. + * + *

It doesn't start watching and calling the callback until start() is issued. + * + * @param path Path to observe. + * @param callback Callback method to execute when a change is observed. + * @param callbackContext Callback object on which the callback method will be executed. + */ + explicit DirectoryWatcher(std::filesystem::path const& path, WatchCallback const& callback, + void* callbackContext = nullptr) noexcept; + + ~DirectoryWatcher() noexcept; + + /** + * Returns true if this watcher is active. + * + * @return True if this watcher is active. + */ + [[nodiscard]] + bool isWatching() const noexcept; + + /** + * Internal use only! Calls the callback when a change is observed. + */ + void callCallback() noexcept; + + /** + * Starts watching for changes. + */ + void start() noexcept; + + /** + * Stops watching for changes. + */ + void stop() noexcept; + + private: +#ifdef GB_IS_LINUX + bool initializeFds() noexcept; +#endif + }; +} + +#endif // GB_DIRECTORY_WATCHER diff --git a/code/include/gb/ReplaceableVars.h b/code/include/gb/ReplaceableVars.h index 5167c38..2dad20a 100644 --- a/code/include/gb/ReplaceableVars.h +++ b/code/include/gb/ReplaceableVars.h @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once @@ -12,17 +12,49 @@ namespace gb { /** * Class to hold variable names and their values for string replacement. + * + *

Variables in strings are of the form "${myVar}". + * + *

Such variable would be made known to this class by calling add("myVar", "5"). */ class ReplaceableVars { + private: + std::map vars; + public: + /** + * Creates a replaceable var container. + */ ReplaceableVars() noexcept = default; + /** + * Copy constructor. + * + * @param other Other object to copy from. + */ ReplaceableVars(ReplaceableVars const& other) noexcept; + /** + * Move constructor. + * + * @param other Other object to move the contents from. + */ ReplaceableVars(ReplaceableVars&& other) noexcept; + /** + * Copy assignment. + * + * @param other Other object to copy from. + * @return This object with new contents. + */ ReplaceableVars& operator=(ReplaceableVars const& other) noexcept; + /** + * Move assignment. + * + * @param other Other object to move the contents from. + * @return This object with new contents. + */ ReplaceableVars& operator=(ReplaceableVars&& other) noexcept; /** @@ -50,9 +82,6 @@ namespace gb { */ [[nodiscard]] std::string string() const noexcept; - - private: - std::map vars; }; } diff --git a/code/include/gb/ShutdownMonitor.h b/code/include/gb/ShutdownMonitor.h index 8a7b569..add48e9 100644 --- a/code/include/gb/ShutdownMonitor.h +++ b/code/include/gb/ShutdownMonitor.h @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -17,6 +18,11 @@ namespace gb { * Utility to monitor SIGINT and SIGTERM for proper application shutdown. */ class ShutdownMonitor { + private: + std::atomic isShuttingDown { false }; + std::mutex shutdownLock; + std::condition_variable shuttingDown; + public: /** * Creates a monitor that will get notified when it's time for an orderly shutdown. @@ -41,8 +47,8 @@ namespace gb { /** * Awaits for a shutdown or expiration of the given timeout. - * - *

If a shutdown has been triggered, the method will exit fast. + *

+ * If a shutdown has been triggered, the method will exit fast. * * @param timeout Time to wait for shutdown. */ @@ -50,8 +56,8 @@ namespace gb { /** * Awaits for a shutdown. - * - *

If a shutdown has been triggered, the method will exit fast. + *

+ * If a shutdown has been triggered, the method will exit fast. */ void awaitShutdown() noexcept; @@ -66,10 +72,6 @@ namespace gb { private: - std::atomic isShuttingDown { false }; - std::mutex shutdownLock; - std::condition_variable shuttingDown; - explicit ShutdownMonitor(bool const shuttingDown) noexcept : isShuttingDown(shuttingDown) {} }; } diff --git a/code/include/gb/concurrent/Task.h b/code/include/gb/concurrent/Task.h new file mode 100644 index 0000000..f042408 --- /dev/null +++ b/code/include/gb/concurrent/Task.h @@ -0,0 +1,137 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gb.h" +#ifdef GB_TASK + +#include +#include +#include + +namespace gb::concurrent { + + class TaskRunner; + + /** + * Task states. + */ + enum class TaskState : int { + Created, + Started, + Canceled, + Finished + }; + + /** + * A single concurrent task that starts in a separate thread and runs until + * finished or the task is canceled. + */ + class Task { + friend class TaskRunner; + + private: + static std::atomic nextTaskId; + + protected: + /** + * Lock for any state related operation. + */ + std::mutex stateLock; + + private: + uint64_t const taskId; + std::thread thread; + TaskRunner* runner { nullptr }; + std::atomic state { TaskState::Created }; + std::condition_variable stateChangedSignal; + std::atomic _shouldCancel { false }; + + public: + /** + * Creates a task with a unique id. + */ + Task() noexcept : taskId(nextTaskId++) {}; + + virtual ~Task() noexcept = default; + + /** + * Returns the current state of the task, as of the time of calling this method. + * + *

Note that the state can change by the time you are comparing the result. + * + * @return The current state of the task. + */ + TaskState getState() const noexcept; + + /** + * Signals the task to cancel. + * + *

It's up to the task itself to check for its cancellation and needing to exit. + */ + void cancel() noexcept; + + /** + * Blocks the calling thread until the task stops, either by cancellation or by finishing. + */ + void awaitStop() noexcept; + + /** + * Tests if the task has stopped, either by cancellation or by finishing. + * + *

Once the task enters a stopped state its state will never change again. + * + * @return True if the task has stopped. + */ + bool isStopped() const noexcept; + + protected: + /** + * Actual action that is executed on its own thread. + * + *

Implementations MUST call started(), usually after initialization, indicating the task is ready and running. + */ + virtual void action() noexcept = 0; + + /** + * Returns the task runner that is running this task. + * + *

Can be used to start other tasks with the same runner. + * + * @return The task runner that is running this task. + */ + [[nodiscard]] + TaskRunner* getTaskRunner() const noexcept; + + /** + * Signals the tasks internals that the action has started and is functional. + * + *

It MUST be called within action by implementations. + */ + void started() noexcept; + + /** + * Tests if the task was canceled and should exit. + * + *

Long running action implementations should check this periodically and exit when requested. + * + * @return True if the task was canceled and should exit. + */ + [[nodiscard]] + bool shouldCancel() const noexcept; + + private: + void setTaskRunner(TaskRunner* const newRunner) noexcept; + + void canceled() noexcept; + + void finished() noexcept; + + void awaitState(TaskState const desiredState) noexcept; + + void awaitStart() noexcept; + }; +} + +#endif // GB_TASK diff --git a/code/include/gb/concurrent/TaskRunner.h b/code/include/gb/concurrent/TaskRunner.h new file mode 100644 index 0000000..55a477b --- /dev/null +++ b/code/include/gb/concurrent/TaskRunner.h @@ -0,0 +1,94 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gb.h" +#ifdef GB_TASK + +#include "Task.h" +#include +#include +#include +#include +#include + +namespace gb::concurrent { + + class Task; + + /** + * Task runner facility to run tasks in their own threads. + * + *

Note that start method will block until the task signals it has started. This is by design + * to ensure a task is ready to accept input, for example. + */ + class TaskRunner { + private: + static bool taskComparator(std::shared_ptr const& lhs, std::shared_ptr const& rhs) noexcept; + + private: + std::atomic _isActive { true }; + std::set, decltype(&taskComparator)> tasks { taskComparator }; + std::mutex tasksLock; + std::condition_variable isEmptySignal; + std::thread threadController; + std::mutex threadControllerLock; + bool threadControllerShouldExit { false }; + std::vector> finishingTasks; + std::condition_variable threadControllerWakeUpSignal; + + public: + /** + * Creates a task runner. + */ + TaskRunner() noexcept; + + /** + * Destroys the task runner. + * + *

It shuts down the runner as per shutdown(). + */ + ~TaskRunner() noexcept; + + /** + * Returns true if the runner is active and able to accept tasks. + * + * @return True if the runner is active and able to accept tasks. + */ + [[nodiscard]] + bool isActive() const noexcept; + + /** + * Shuts down the runner. + * + *

Cancels all tasks and awaits on all tasks to stop. + */ + void shutdown() noexcept; + + /** + * Starts a task. + * + *

This method will block until the task signals it has started. + * + * @param task Task to start. + * @return True if the task was started, false if the runner is not active. + */ + bool start(std::shared_ptr const& task) noexcept; + + /** + * Cancels all tasks. + */ + void cancelAll() noexcept; + + /** + * Awaits for all tasks to finish. + */ + void awaitAll() noexcept; + + private: + void removeTask(std::shared_ptr const& task) noexcept; + }; +} + +#endif // GB_TASK diff --git a/code/include/gb/constants.h b/code/include/gb/constants.h new file mode 100644 index 0000000..69a6769 --- /dev/null +++ b/code/include/gb/constants.h @@ -0,0 +1,17 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +namespace gb { + + /** + * False typed RValue. Normally used with atomic compares. + */ + extern constinit bool _false; + + /** + * True typed RValue. Normally used with atomic compares. + */ + extern constinit bool _true; +} diff --git a/code/include/gb/functions.h b/code/include/gb/functions.h new file mode 100644 index 0000000..d82b43c --- /dev/null +++ b/code/include/gb/functions.h @@ -0,0 +1,34 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gb.h" +#ifdef GB_FUNCTIONS + +#include + +namespace gb::functions { + + /** + * Template function to wrap a method in a function whose 1st argument is a context void pointer. + *

+ * The function will cast that 1st void pointer to an instance of the class and call the method + * with the rest of the arguments. + * + * @tparam TClass Class defining the method. + * @tparam TReturn Return type of the function. + * @tparam TArgs Function arguments, not counting the 1st void pointer. + * @param method Class method to call. + * @return A function that returns the given type, and takes a void pointer and other defined parameters. + */ + template + std::function methodWrapper(TReturn(TClass::*method)(TArgs... args)) { + return [method](void* context, TArgs... args) -> TReturn { + auto instance { static_cast(context) }; + return (instance->*method)(args...); + }; + } +} + +#endif // GB_FUNCTIONS diff --git a/code/include/gb/hash.h b/code/include/gb/hash.h new file mode 100644 index 0000000..f058134 --- /dev/null +++ b/code/include/gb/hash.h @@ -0,0 +1,31 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gb.h" +#ifdef GB_HASH + +#include + +namespace gb::hash { + + class Hashable { + public: + virtual size_t hash() const noexcept = 0; + + virtual ~Hashable() = default; + }; +} + +namespace std { + template + requires std::is_class::value + struct hash { + size_t operator()(TClass const& obj) const noexcept { + return obj.hash(); + } + }; +} + +#endif // GB_HASH diff --git a/code/include/gb/process.h b/code/include/gb/process.h index fb5474c..a4661b0 100644 --- a/code/include/gb/process.h +++ b/code/include/gb/process.h @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -16,7 +15,7 @@ namespace gb::process { /** - * Executes a command and optionally captures it's output. + * Executes a command and optionally captures its output. * *

Output can be filtered before capture, allowing the caller to even process the lines * as they come and not capture. It is advised that the filter lambda be as fast as possible @@ -30,7 +29,7 @@ namespace gb::process { * filter must return true to add the line to lines, false to skip. * @return True if command execution was successful. */ - bool execute(std::string_view const& command, std::optionalconst& workDir = std::nullopt, + bool execute(std::string_view const& command, std::filesystem::path const* workDir = nullptr, std::deque* lines = nullptr, int* exitCode = nullptr, std::functionconst& filter = nullptr); } diff --git a/code/include/gb/strings.h b/code/include/gb/strings.h index 6d90156..fccfdc6 100644 --- a/code/include/gb/strings.h +++ b/code/include/gb/strings.h @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once @@ -7,8 +7,6 @@ #ifdef GB_STRINGS #include -#include -#include #include #include @@ -16,11 +14,10 @@ namespace gb::strings { /** * Converts a C string array into a vector of string_views. - * - *

The contents of the vector will be valid as long as the original array is valid. - * - *

This function is specifically designed to convert main function args - * into a string vector. + *

+ * The contents of the vector will be valid as long as the original array is valid. + *

+ * This function is specifically designed to convert main function args into a string vector. * * @param argc String array size. * @param argv Array of C strings. @@ -30,9 +27,8 @@ namespace gb::strings { /** * Converts a C string array into a vector of strings. - * - *

This function is specifically designed to convert main function args - * into a string vector. + *

+ * This function is specifically designed to convert main function args into a string vector. * * @param argc String array size. * @param argv Array of C strings. @@ -42,8 +38,8 @@ namespace gb::strings { /** * Convenience replace of a token in a string. - * - *

This creates a new string, so it is only a shortcut, not efficient for replacing many tokens. + *

+ * This creates a new string, so it is only a shortcut, not efficient for replacing many tokens. * * @param str String to search for token. * @param token Token to find. @@ -55,8 +51,8 @@ namespace gb::strings { /** * Splits a string by the delimiter into a vector of strings. - * - *

Storage is weak. Meaning, the resulting vector is actually pointing at sections in the original string. + *

+ * Storage is weak. Meaning, the resulting vector is actually pointing at sections in the original string. * This is faster than regular split, but if the original string goes away, the contents of this * vector are not valid. * @@ -80,8 +76,8 @@ namespace gb::strings { /** * Unindents a multiline block of text by removing all common spaces or tabs * from the beginning of the lines. Empty lines are left alone. - * - *

This function is specifically designed to use with raw string literals. + *

+ * This function is specifically designed to use with raw string literals. * And it will remove indentation created to align the string with the rest * of the surrounding code. * @@ -104,27 +100,28 @@ namespace gb::strings { std::string fromVector(std::vector const& vector, std::string_view const& separator = ", ") noexcept; /** - * Inserts thousand separator in the given string that must be a numeric representation. - * - *

Insertion happens in place. The return string is the same as the parameter string. + * Adds thousand separators in the given string that must be a numeric representation. * * @param str String numeric representation. * @return The numeric representation with thousand separators. */ - std::string& insertThousandSeparatorsInPlace(std::string& str) noexcept; + [[nodiscard]] + std::string addThousandSeparators(std::string_view const& str) noexcept; /** - * Inserts thousand separator in the given string that must be a numeric representation. + * Returns a string representation of the given time. * - * @param str String numeric representation. - * @return The numeric representation with thousand separators. + * @param time Time to convert to string. + * @param format Time format. + * @return String representation of the given time. */ - std::string insertThousandSeparators(std::string_view const& str) noexcept; + [[nodiscard]] + std::string fromTime(std::time_t const& time, std::string const& format) noexcept; /** * Default precision for numeric values. */ - extern constinit int DefaultPrecision; + constexpr int DefaultPrecision { -1 }; /** * Converts an integral value to a human representation. @@ -168,8 +165,7 @@ namespace gb::strings { template std::string fromIntegral(T const value) noexcept { - std::string str { std::to_string(value) }; - return insertThousandSeparatorsInPlace(str); + return addThousandSeparators(std::to_string(value)); } template @@ -182,7 +178,7 @@ namespace gb::strings { ss << std::fixed << std::setprecision(precision) << value; str = ss.str(); } - return insertThousandSeparators(str); + return addThousandSeparators(str); } } diff --git a/code/include/gb/console.h b/code/include/gb/terminal.h similarity index 81% rename from code/include/gb/console.h rename to code/include/gb/terminal.h index 37797e3..175f37d 100644 --- a/code/include/gb/console.h +++ b/code/include/gb/terminal.h @@ -1,14 +1,14 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #pragma once #include "gb.h" -#ifdef GB_CONSOLE +#ifdef GB_TERMINAL #include -namespace gb::console { +namespace gb::terminal { typedef uint32_t color_t; @@ -26,7 +26,7 @@ namespace gb::console { * @return The color code. */ [[nodiscard]] - extern color_t rgb(color_t const r, color_t const g, color_t const b) noexcept; + color_t rgb(color_t const r, color_t const g, color_t const b) noexcept; /** * Converts a grey step into a color code. @@ -35,7 +35,7 @@ namespace gb::console { * @return The color code. */ [[nodiscard]] - extern color_t grey(color_t const step) noexcept; + color_t grey(color_t const step) noexcept; /** * Returns a string that represents the given string in the given color. @@ -48,4 +48,4 @@ namespace gb::console { std::string colorText(std::string_view const& str, color_t const color) noexcept; } -#endif // GB_CONSOLE +#endif // GB_TERMINAL diff --git a/code/src/App.cpp b/code/src/App.cpp index 12676d0..82edfd7 100644 --- a/code/src/App.cpp +++ b/code/src/App.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #include @@ -9,10 +9,10 @@ #include "GradleParams.h" #include "App.h" -gb::console::color_t const textColor { gb::console::rgb(1, 1, 1) }; -gb::console::color_t const highlightColor { gb::console::rgb(5, 5, 5) }; -gb::console::color_t const cmdColor { gb::console::rgb(3, 5, 3) }; -gb::console::color_t const argColor { gb::console::rgb(0, 2, 0) }; +gb::terminal::color_t const textColor { gb::terminal::rgb(1, 1, 1) }; +gb::terminal::color_t const highlightColor { gb::terminal::rgb(5, 5, 5) }; +gb::terminal::color_t const cmdColor { gb::terminal::rgb(3, 5, 3) }; +gb::terminal::color_t const argColor { gb::terminal::rgb(0, 2, 0) }; void printUsage() noexcept { std::string const text { gb::strings::unindent(R"===( @@ -25,10 +25,10 @@ void printUsage() noexcept { ${args} Arguments for the running project. )===") }; std::string const usage { gb::ReplaceableVars() - .add("app", gb::console::colorText("grun", cmdColor)) - .add("param1", gb::console::colorText("project_gradle_root", argColor)) - .add("param2", gb::console::colorText("project", argColor)) - .add("args", gb::console::colorText("args ...", argColor)) + .add("app", gb::terminal::colorText("grun", cmdColor)) + .add("param1", gb::terminal::colorText("project_gradle_root", argColor)) + .add("param2", gb::terminal::colorText("project", argColor)) + .add("args", gb::terminal::colorText("args ...", argColor)) .replace(text) }; std::cout << usage; } @@ -48,7 +48,8 @@ std::map buildAndRetrieveGradleProperties(GradleParams int exitCode; std::regex const re { "^([a-zA-Z]+): ([^ ].*)$" }; std::smatch match; - gb::process::execute(command, gradleParams.getGradleRoot(), nullptr, &exitCode, + auto const gradleRoot = gradleParams.getGradleRoot(); + gb::process::execute(command, &gradleRoot, nullptr, &exitCode, [&](std::string const& line) { if (!std::regex_match(line, match, re) || (match.size() != 3)) { return false; @@ -66,9 +67,9 @@ std::map buildAndRetrieveGradleProperties(GradleParams void printMessage(std::string_view const& msg, std::string_view const& param) noexcept { size_t const pos = msg.find('@'); - std::cerr << gb::console::colorText(msg.substr(0, pos), textColor) - << gb::console::colorText(param, highlightColor) - << gb::console::colorText(msg.substr(pos +1), textColor) + std::cerr << gb::terminal::colorText(msg.substr(0, pos), textColor) + << gb::terminal::colorText(param, highlightColor) + << gb::terminal::colorText(msg.substr(pos +1), textColor) << std::endl; } @@ -105,7 +106,7 @@ int extractBin(GradleParams const& gradleParams, std::filesystem::path* binPath) // Untar. std::filesystem::path workDir { distDir / tarName.substr(0, tarName.length() - 4) }; std::filesystem::remove_all(workDir); - if (!gb::process::execute("tar -xf " + tarName, distDir)) { + if (!gb::process::execute("tar -xf " + tarName, &distDir)) { printMessage("Can't untar: @", tarPath.string()); return 3; } @@ -126,8 +127,8 @@ int App::run(std::vector const& args) noexcept { } std::string const command { binPath.string() + ' ' + gb::strings::fromVector(gradleParams.getProjectArgs(), " ") }; result = std::system(command.c_str()); - std::cout << gb::console::colorText("Exit code: ", textColor) - << gb::console::colorText(std::to_string(result), highlightColor) + std::cout << gb::terminal::colorText("Exit code: ", textColor) + << gb::terminal::colorText(std::to_string(result), highlightColor) << std::endl; return 0; } diff --git a/code/src/gb/DirectoryWatcher.cpp b/code/src/gb/DirectoryWatcher.cpp new file mode 100644 index 0000000..fa37fe8 --- /dev/null +++ b/code/src/gb/DirectoryWatcher.cpp @@ -0,0 +1,273 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#include "gb/DirectoryWatcher.h" +#ifdef GB_DIRECTORY_WATCHER + +namespace gb { + +#ifdef GB_IS_MACOS + std::atomic DirectoryWatcher::queueId { 0 }; +#endif + + class DirectoryWatcher::CallbackRunnerTask : public gb::concurrent::Task { + private: + DirectoryWatcher* watcher; + std::chrono::time_point timeToCall; + + public: + explicit CallbackRunnerTask(DirectoryWatcher* const watcher) noexcept; + + bool resetTimer() noexcept; + + protected: + void action() noexcept override; + }; + + DirectoryWatcher::CallbackRunnerTask::CallbackRunnerTask(DirectoryWatcher* const watcher) noexcept { + this->watcher = watcher; + resetTimer(); + } + + bool DirectoryWatcher::CallbackRunnerTask::resetTimer() noexcept { + std::lock_guard lock { stateLock }; + if (isStopped()) { + return false; + } + std::chrono::time_point const now = std::chrono::system_clock::now(); + std::chrono::seconds const oneSecond { 1 }; + timeToCall = now + oneSecond; + return true; + } + + void DirectoryWatcher::CallbackRunnerTask::action() noexcept { + started(); + while (!shouldCancel()) { + std::chrono::time_point const now = std::chrono::system_clock::now(); + if (timeToCall <= now) { + watcher->callback(watcher->callbackContext); + return; + } + std::this_thread::sleep_until(timeToCall); + } + } + + DirectoryWatcher::DirectoryWatcher(std::filesystem::path const& path, WatchCallback const& callback, + void* callbackContext) noexcept { + this->paths = std::vector(); + this->paths.push_back(path); + this->callback = callback; + this->callbackContext = callbackContext; + } + + DirectoryWatcher::~DirectoryWatcher() noexcept { + stop(); + } + + bool DirectoryWatcher::isWatching() const noexcept { + return _isWatching; + } + + void DirectoryWatcher::callCallback() noexcept { + std::lock_guard lock { actionLock }; + if (!actionTask || !actionTask->resetTimer()) { + actionTask = std::make_shared(this); + runner->start(actionTask); + } + } + +#ifdef GB_IS_MACOS + void eventCallback(ConstFSEventStreamRef const stream, void* const callbackInfo, size_t const eventPathCount, + void* const eventPaths, FSEventStreamEventFlags const* const eventFlags, + FSEventStreamEventId const* const eventIds) noexcept { + auto watcher = static_cast(callbackInfo); + watcher->callCallback(); + } +#endif +#ifdef GB_IS_WINDOWS + void CALLBACK eventCallback(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) { + if (dwErrorCode != ERROR_SUCCESS) { + // Something bad happened, but we don't capture. It works or it doesn't. + return; + } + auto watcher = static_cast(lpOverlapped->Pointer); + watcher->callCallback(); + } +#endif + +#ifdef GB_IS_LINUX + bool DirectoryWatcher::initializeFds() noexcept { + if (pipe(cancelPipeFds) == -1) { + return false; + } + inotifyFd = inotify_init(); + if (inotifyFd == -1) { + return false; + } + for (auto const& path: paths) { + int const wd = inotify_add_watch(inotifyFd, path.c_str(), IN_MODIFY | IN_CREATE | IN_DELETE); + if (wd == -1) { + return false; + } + } + epollFd = epoll_create1(0); + if (epollFd == -1) { + return false; + } + epoll_event event { .events = EPOLLIN, .data { .fd = cancelPipeFds[0] } }; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, cancelPipeFds[0], &event) == -1) { + return false; + } + event.data.fd = inotifyFd; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, inotifyFd, &event) == -1) { + return false; + } + return true; + } +#endif + + void DirectoryWatcher::start() noexcept { + std::unique_lock lock { watchLock }; + if (isWatching()) { + return; + } +#ifdef GB_IS_MACOS + std::string const queueName { "com.glitchybyte.tanuki.directorywatcher.queue" + std::to_string(queueId++) }; + cfPaths = CFArrayCreateMutable(nullptr, static_cast(paths.size()), &kCFTypeArrayCallBacks); + for (auto const& path: paths) { + CFStringRef cfPath = CFStringCreateWithCString(nullptr, path.c_str(), kCFStringEncodingUTF8); + CFArrayAppendValue(cfPaths, cfPath); + CFRelease(cfPath); + } + stream = FSEventStreamCreate(nullptr, eventCallback, &streamContext, cfPaths, kFSEventStreamEventIdSinceNow, 2, + kFSEventStreamCreateFlagNone); + queue = dispatch_queue_create(queueName.c_str(), DISPATCH_QUEUE_SERIAL); + FSEventStreamSetDispatchQueue(stream, queue); + FSEventStreamStart(stream); +#endif +#ifdef GB_IS_LINUX + if (!initializeFds()) { + if (cancelPipeFds[0] != 0) { + close(cancelPipeFds[0]); + } + if (cancelPipeFds[1] != 0) { + close(cancelPipeFds[1]); + } + if (inotifyFd != 0) { + close(inotifyFd); + } + if (epollFd != 0) { + close(epollFd); + } + return; + } + watchThread = std::thread([this]() { + epoll_event events[16]; + while (true) { + int const readyFdCount = epoll_wait(epollFd, events, 16, -1); + if (readyFdCount == -1) { + if (errno == EINTR) { + // Supurious interrupt! We continue. + continue; + } + break; + } + for (int i = 0; i < readyFdCount; ++i) { + if (events[i].data.fd == cancelPipeFds[0]) { + // User exit signal. + goto exitThread; + } else if (events[i].data.fd == inotifyFd) { + char buffer[8 * 1024]; + auto const bytesRead = read(inotifyFd, buffer, sizeof(buffer)); + if (bytesRead == -1) { + goto exitThread; + } + callCallback(); + } + } + } + exitThread: + close(cancelPipeFds[0]); + close(cancelPipeFds[1]); + close(inotifyFd); + close(epollFd); + }); +#endif +#ifdef GB_IS_WINDOWS + hCancelEvent = CreateEvent(NULL, true, false, NULL); + watchThread = std::thread([this]() { + std::vector handles; + handles.reserve(paths.size() + 1); + handles.push_back(hCancelEvent); + for (auto const& path: paths) { + HANDLE hDir = CreateFileW(path.wstring().c_str(), FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + if (hDir == INVALID_HANDLE_VALUE) { + return; + } + handles.push_back(hDir); + if (!ReadDirectoryChangesW(hDir, NULL, 0, true, + FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME, NULL, &watcherContext, + eventCallback) + ) { + for (auto const& handle: handles) { + CloseHandle(handle); + } + return; + } + } + while (true) { + DWORD const result = WaitForMultipleObjectsEx(handles.size(), handles.data(), false, INFINITE, true); + if (result == WAIT_FAILED) { + break; + } + if (result == WAIT_OBJECT_0) { + // Cancel event is at index 0. We exit if it's set. + break; + } + if ((result > WAIT_OBJECT_0) && (result < (MAXIMUM_WAIT_OBJECTS - WAIT_OBJECT_0))) { + int const index = result - WAIT_OBJECT_0; + HANDLE hDir = handles[index]; + if (!ReadDirectoryChangesW(hDir, NULL, 0, true, + FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME, NULL, &watcherContext, + eventCallback) + ) { + break; + } + } + } + for (auto const& handle: handles) { + CloseHandle(handle); + } + }); +#endif + _isWatching = true; + } + + void DirectoryWatcher::stop() noexcept { + std::unique_lock lock { watchLock }; + if (!isWatching()) { + return; + } +#ifdef GB_IS_MACOS + FSEventStreamStop(stream); + FSEventStreamInvalidate(stream); + FSEventStreamRelease(stream); + dispatch_release(queue); + CFRelease(cfPaths); +#endif +#ifdef GB_IS_LINUX + char const buffer = 'x'; + [[maybe_unused]] auto result = write(cancelPipeFds[1], &buffer, 1); + watchThread.join(); +#endif +#ifdef GB_IS_WINDOWS + SetEvent(hCancelEvent); + watchThread.join(); +#endif + _isWatching = false; + } +} + +#endif // GB_DIRECTORY_WATCHER diff --git a/code/src/gb/ShutdownMonitor.cpp b/code/src/gb/ShutdownMonitor.cpp index ff35af9..083d7c9 100644 --- a/code/src/gb/ShutdownMonitor.cpp +++ b/code/src/gb/ShutdownMonitor.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #include "gb/ShutdownMonitor.h" @@ -8,7 +8,6 @@ namespace gb { - constinit bool _false { false }; std::atomic generalShutdownInitiated { false }; std::mutex generalShutdownLock; std::vector> shutdownMonitors; diff --git a/code/src/gb/concurrent/Task.cpp b/code/src/gb/concurrent/Task.cpp new file mode 100644 index 0000000..76e6752 --- /dev/null +++ b/code/src/gb/concurrent/Task.cpp @@ -0,0 +1,83 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#include "gb/concurrent/Task.h" +#ifdef GB_TASK + +namespace gb::concurrent { + + std::atomic Task::nextTaskId { 0 }; + + TaskState Task::getState() const noexcept { + return state; + } + + void Task::cancel() noexcept { + std::lock_guard lock { stateLock }; + if (state != TaskState::Started) { + return; + } + _shouldCancel = true; + } + + void Task::awaitStop() noexcept { + std::unique_lock lock { stateLock }; + stateChangedSignal.wait(lock, [this]{ return isStopped(); }); + } + + bool Task::isStopped() const noexcept { + TaskState const currentState = state; + return (currentState == TaskState::Canceled) || (currentState == TaskState::Finished); + } + + TaskRunner* Task::getTaskRunner() const noexcept { + return runner; + } + + void Task::started() noexcept { + std::lock_guard lock { stateLock }; + if (state != TaskState::Created) { + return; + } + state = TaskState::Started; + stateChangedSignal.notify_all(); + } + + bool Task::shouldCancel() const noexcept { + return _shouldCancel; + } + + void Task::setTaskRunner(TaskRunner* const newRunner) noexcept { + runner = newRunner; + } + + void Task::canceled() noexcept { + std::lock_guard lock { stateLock }; + if (state != TaskState::Started) { + return; + } + state = TaskState::Canceled; + stateChangedSignal.notify_all(); + } + + void Task::finished() noexcept { + std::lock_guard lock { stateLock }; + if (state != TaskState::Started) { + return; + } + state = TaskState::Finished; + stateChangedSignal.notify_all(); + } + + void Task::awaitState(TaskState const desiredState) noexcept { + std::unique_lock lock { stateLock }; + stateChangedSignal.wait(lock, [this, desiredState]{ return state == desiredState; }); + } + + void Task::awaitStart() noexcept { + std::unique_lock lock { stateLock }; + stateChangedSignal.wait(lock, [this]{ return state != TaskState::Created; }); + } +} + +#endif // GB_TASK diff --git a/code/src/gb/concurrent/TaskRunner.cpp b/code/src/gb/concurrent/TaskRunner.cpp new file mode 100644 index 0000000..96dd0ba --- /dev/null +++ b/code/src/gb/concurrent/TaskRunner.cpp @@ -0,0 +1,102 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#include "gb/concurrent/TaskRunner.h" +#ifdef GB_TASK + +namespace gb::concurrent { + + bool TaskRunner::taskComparator(std::shared_ptr const& lhs, std::shared_ptr const& rhs) noexcept { + return lhs->taskId < rhs->taskId; + } + + TaskRunner::TaskRunner() noexcept { + threadController = std::thread { + // Waits for threads to join and removes them from the set. + [this]{ + std::unique_lock lock { threadControllerLock }; + while (true) { + threadControllerWakeUpSignal.wait(lock, [this]{ return threadControllerShouldExit || !finishingTasks.empty(); }); + if (threadControllerShouldExit) { + break; + } + for (auto& task: finishingTasks) { + task->thread.join(); + removeTask(task); + } + finishingTasks.clear(); + } + } + }; + } + + TaskRunner::~TaskRunner() noexcept { + shutdown(); + } + + bool TaskRunner::isActive() const noexcept { + return _isActive.load(); + } + + void TaskRunner::shutdown() noexcept { + std::unique_lock lock { tasksLock }; + if (!_isActive.compare_exchange_strong(_true, false)) { + return; + } + for (auto const& task: tasks) { + task->cancel(); + } + isEmptySignal.wait(lock, [this]{ return tasks.empty(); }); + lock.unlock(); + std::unique_lock controllerLock { threadControllerLock }; + threadControllerShouldExit = true; + threadControllerWakeUpSignal.notify_one(); + controllerLock.unlock(); + threadController.join(); + } + + bool TaskRunner::start(std::shared_ptr const& task) noexcept { + std::lock_guard lock { tasksLock }; + if (!isActive()) { + return false; + } + auto const [_, success] = tasks.insert(task); + if (!success) { + return false; + } + task->setTaskRunner(this); + task->thread = std::thread { + [this, task]{ + task->action(); + task->finished(); + std::lock_guard lock { threadControllerLock }; + finishingTasks.push_back(task); + threadControllerWakeUpSignal.notify_one(); + } + }; + task->awaitStart(); + return true; + } + + void TaskRunner::cancelAll() noexcept { + std::lock_guard lock { tasksLock }; + for (auto const& task: tasks) { + task->cancel(); + } + } + + void TaskRunner::awaitAll() noexcept { + std::unique_lock lock { tasksLock }; + isEmptySignal.wait(lock, [this]{ return tasks.empty(); }); + } + + void TaskRunner::removeTask(std::shared_ptr const& task) noexcept { + std::lock_guard lock { tasksLock }; + tasks.erase(task); + if (tasks.empty()) { + isEmptySignal.notify_one(); + } + } +} + +#endif // GB_TASK diff --git a/code/src/gb/constants.cpp b/code/src/gb/constants.cpp new file mode 100644 index 0000000..866ed3a --- /dev/null +++ b/code/src/gb/constants.cpp @@ -0,0 +1,10 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#include "gb/constants.h" + +namespace gb { + + constinit bool _false { false }; + constinit bool _true { true }; +} diff --git a/code/src/gb/files.cpp b/code/src/gb/files.cpp index 68ccdf9..7f05c4e 100644 --- a/code/src/gb/files.cpp +++ b/code/src/gb/files.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #include "gb/files.h" @@ -11,8 +11,12 @@ namespace gb::files { std::filesystem::path canonicalPath(std::filesystem::path const& path) noexcept { std::string pathStr { path.string() }; std::string const fullPath { - pathStr.starts_with('~') ? - std::getenv("HOME") + pathStr.substr(1) : + pathStr.starts_with("~/") + #ifdef _WIN32 + || pathStr.starts_with("~\\") + #endif + ? + std::getenv("HOME") + pathStr.substr(2) : std::move(pathStr) }; return std::filesystem::weakly_canonical(fullPath); diff --git a/code/src/gb/process.cpp b/code/src/gb/process.cpp index 416f13d..5ece602 100644 --- a/code/src/gb/process.cpp +++ b/code/src/gb/process.cpp @@ -1,12 +1,11 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #include "gb/process.h" #ifdef GB_PROCESS #include - -#ifdef _WIN32 +#ifdef GB_IS_WINDOWS #define popen _popen #define pclose _pclose #define WIFEXITED(x) (x != -1) @@ -15,12 +14,13 @@ namespace gb::process { +#ifdef GB_IS_WINDOWS + constexpr size_t const eolSize = 2; +#else + constexpr size_t const eolSize = 1; +#endif + bool readLine(FILE* file, std::string& line) { - #ifdef _WIN32 - size_t const eolSize = 2; - #else - size_t const eolSize = 1; - #endif char buffer[1024]; line.clear(); while (fgets(buffer, sizeof(buffer), file) != NULL) { @@ -33,12 +33,12 @@ namespace gb::process { return false; } - bool execute(std::string_view const& command, std::optional const& workDir, + bool execute(std::string_view const& command, std::filesystem::path const* workDir, std::deque* lines, int* exitCode, std::function const& filter) { auto const& originalDir { std::filesystem::current_path() }; - if (workDir.has_value()) { - std::filesystem::current_path(workDir.value()); + if (workDir != nullptr) { + std::filesystem::current_path(*workDir); } std::string redirectedCommand { command }; redirectedCommand += " 2>&1"; diff --git a/code/src/gb/strings.cpp b/code/src/gb/strings.cpp index 0b84cc4..1ab1ce4 100644 --- a/code/src/gb/strings.cpp +++ b/code/src/gb/strings.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #include "gb/strings.h" @@ -130,24 +130,24 @@ namespace gb::strings { return ss.str(); } - std::string& insertThousandSeparatorsInPlace(std::string& str) noexcept { - size_t const period { str.find('.') }; - size_t const start { period == std::string::npos ? str.length() : period }; - size_t const finish { static_cast(str.starts_with('-') ? 4 : 3) }; + std::string addThousandSeparators(std::string const& str) noexcept { + std::string number { str }; + size_t const period { number.find('.') }; + size_t const start { period == std::string::npos ? number.length() : period }; + size_t const finish { static_cast(number.starts_with('-') ? 4 : 3) }; size_t index { start }; while (index > finish) { index -= 3; - str.insert(index, ","); + number.insert(index, ","); } - return str; + return number; } - std::string insertThousandSeparators(std::string_view const& str) noexcept { - std::string newStr { str }; - return insertThousandSeparatorsInPlace(newStr); + std::string fromTime(std::time_t const& time, std::string const& format) noexcept { + std::stringstream ss; + ss << std::put_time(std::localtime(&time), format.c_str()); + return ss.str(); } - - constinit int DefaultPrecision { -1 }; } #endif // GB_STRINGS diff --git a/code/src/gb/console.cpp b/code/src/gb/terminal.cpp similarity index 87% rename from code/src/gb/console.cpp rename to code/src/gb/terminal.cpp index 9c64641..438c756 100644 --- a/code/src/gb/console.cpp +++ b/code/src/gb/terminal.cpp @@ -1,14 +1,13 @@ // Copyright 2023 GlitchyByte // SPDX-License-Identifier: Apache-2.0 -#include "gb/console.h" -#ifdef GB_CONSOLE +#include "gb/terminal.h" +#ifdef GB_TERMINAL #include #include -#include "gb/strings.h" -namespace gb::console { +namespace gb::terminal { color_t rgb(color_t const r, color_t const g, color_t const b) noexcept { assert(r < 6); @@ -33,4 +32,4 @@ namespace gb::console { } } -#endif // GB_CONSOLE +#endif // GB_TERMINAL diff --git a/code/src/main.cpp b/code/src/main.cpp index bdeca0a..1b0d339 100644 --- a/code/src/main.cpp +++ b/code/src/main.cpp @@ -1,10 +1,11 @@ -// Copyright 2023 GlitchyByte +// Copyright 2023-2024 GlitchyByte // SPDX-License-Identifier: Apache-2.0 #include "gb.h" #include "App.h" int main(int const argc, char const* argv[]) { + setlocale(LC_ALL, "en_US.UTF-8"); auto const& args = gb::strings::createVectorStringViewFromCArray(argc, argv); return App::run(args); } diff --git a/code/test/CMakeLists.txt b/code/test/CMakeLists.txt new file mode 100644 index 0000000..7dcee76 --- /dev/null +++ b/code/test/CMakeLists.txt @@ -0,0 +1,86 @@ +# Copyright 2024 GlitchyByte +# SPDX-License-Identifier: MIT-0 + +cmake_minimum_required(VERSION 3.26) + +set(IS_MACOS CMAKE_SYSTEM_NAME STREQUAL "Darwin") +set(IS_LINUX CMAKE_SYSTEM_NAME STREQUAL "Linux") +set(IS_WINDOWS CMAKE_SYSTEM_NAME STREQUAL "Windows") + +if (NOT CMAKE_BUILD_TYPE) + # Force MinSizeRel if no build type specified. + set(CMAKE_BUILD_TYPE "MinSizeRel" CACHE STRING "Type of build." FORCE) +endif () + +# Main binary to build. +set(APP "gbtest") + +file(STRINGS "../../version" APP_VERSION LIMIT_COUNT 1) +project(${APP} VERSION "${APP_VERSION}") +message(STATUS "Project: ${APP} v${APP_VERSION}") +message(STATUS "Platform: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR}") + +# Set C++ standard and binary output dir. +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../bin) + +# Set flags. +if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") + if (${IS_LINUX} OR ${IS_MACOS}) + # Linux and macOS. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto -ffunction-sections -fdata-sections") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto -ffunction-sections -fdata-sections") + elseif (${IS_WINDOWS}) + # Windows has not been tested. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GL") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /GL") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LTCG") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LTCG") + else () + message(WARNING "Unknown OS.") + endif () +endif () + +# Test framework. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/9d43b27f7a873596496a2ea70721b3f9eb82df01.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +#set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) +enable_testing() + +# Add all cpp files in src dir. +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "../src/*.cpp") +list(FILTER SOURCES EXCLUDE REGEX "/src/main.cpp$") +file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS "*.cc") +add_executable(${APP} ${SOURCES} ${TEST_SOURCES}) + +if (${IS_MACOS}) + target_link_libraries(${APP} + PUBLIC "-framework CoreFoundation" + PUBLIC "-framework CoreServices" + ) +endif () + +# Add all h files in include dir. +target_include_directories(${APP} PRIVATE "../include") + +# Add GoogleTest libraries. +target_link_libraries(${APP} PUBLIC GTest::gtest_main) +include(GoogleTest) +gtest_discover_tests(${APP}) + +# Minimize binary size. +if ((CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") AND +(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Darwin")) + # Strip all remaining symbols and relocation information. + add_custom_command( + TARGET ${APP} + POST_BUILD + COMMAND strip $ + ) +endif () diff --git a/code/test/TaskRunnerTest.cc b/code/test/TaskRunnerTest.cc new file mode 100644 index 0000000..50a83d9 --- /dev/null +++ b/code/test/TaskRunnerTest.cc @@ -0,0 +1,114 @@ +// Copyright 2024 GlitchyByte +// SPDX-License-Identifier: Apache-2.0 + +#include "gb.h" +#ifdef GB_TASK + +#include + +class TaskRunnerTest : public ::testing::Test { +protected: + gb::concurrent::TaskRunner* runner { nullptr }; + +protected: + void SetUp() override { + runner = new gb::concurrent::TaskRunner(); + } + + void TearDown() override { + delete runner; + } +}; + +class SimpleTask : public gb::concurrent::Task { +public: + std::set items; + std::mutex itemsLock; + +protected: + void action() noexcept override { + started(); + addItem("one"); + } + + void addItem(std::string const& item) { + std::lock_guard lock { itemsLock }; + items.insert(item); + } +}; + +TEST_F(TaskRunnerTest, CanStartTask) { + std::shared_ptr task = std::make_shared(); + ASSERT_TRUE(task->items.empty()); + runner->start(task); + task->awaitStop(); + ASSERT_TRUE(task->items.contains("one")); +} + +class SlowTask : public SimpleTask { +protected: + void action() noexcept override { + started(); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + if (shouldCancel()) { + return; + } + addItem("one"); + } +}; + +TEST_F(TaskRunnerTest, CanCancelTask) { + std::shared_ptr task = std::make_shared(); + ASSERT_TRUE(task->items.empty()); + runner->start(task); + task->cancel(); + task->awaitStop(); + ASSERT_FALSE(task->items.contains("one")); +} + +class CancelableTask : public gb::concurrent::Task { +public: + static std::set items; + static std::mutex itemsLock; + +protected: + static void addItem(std::string const& item) { + std::lock_guard lock { itemsLock }; + items.insert(item); + } + +private: + std::string const itemToAdd; + +public: + explicit CancelableTask(std::string const& str) noexcept : itemToAdd(str) {}; + +protected: + void action() noexcept override { + started(); + while (!shouldCancel()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + addItem(itemToAdd); + } +}; + +std::set CancelableTask::items; +std::mutex CancelableTask::itemsLock; + +TEST_F(TaskRunnerTest, CanCancelAllTasks) { + std::shared_ptr task1 = std::make_shared("one"); + std::shared_ptr task2 = std::make_shared("two"); + std::shared_ptr task3 = std::make_shared("three"); + ASSERT_TRUE(CancelableTask::items.empty()); + runner->start(task1); + runner->start(task2); + runner->start(task3); + runner->cancelAll(); + runner->awaitAll(); + ASSERT_TRUE(CancelableTask::items.contains("one")); + ASSERT_TRUE(CancelableTask::items.contains("two")); + ASSERT_TRUE(CancelableTask::items.contains("three")); +} + +#endif // GB_TASK diff --git a/release.md b/release.md index 2e4f3b1..56e123c 100644 --- a/release.md +++ b/release.md @@ -1,5 +1,3 @@ # grun -**Download the compressed archive for your platform.** - -These are built using GitHub Actions on GitHub Runners and produce larger binaries than if you build locally. +Gradle project runner. Builds, unpacks, and runs a Gradle project on the current terminal. diff --git a/run b/run index a058a60..e075a53 100755 --- a/run +++ b/run @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Copyright 2023 GlitchyByte +# Copyright 2023-2024 GlitchyByte # SPDX-License-Identifier: MIT-0 # Runs project. # [Setup] -set -u -set -e +set -u # Exit with an error if a variable is used without being set. +set -e # Exit if any command returns an error. # Capture caller directory and script directory. readonly calling_dir="${PWD}" readonly script_dir="$(cd "$(dirname "$0")" && pwd)" @@ -17,19 +17,31 @@ cd "${script_dir}" # [Main] if [ $# -lt 1 ]; then - echoerr "Need the name of the project." + echoerr "Need project name." + exit 1 fi name=$1 +# Regroup arguments. shift -readonly args=$* +args=() +if [ "$#" -gt 0 ]; then + for arg in "$@"; do + args+=("$arg") + done +fi # Build. -./build MinSizeRel +# ./build MinSizeRel +./build Debug # Run. echo "${c_bold}${cf_black}Running: ${cf_white}${name}${c_reset}" -"./code/bin/${name}" $args +if [ "$#" -gt 0 ]; then + "./code/build/bin/${name}" "${args[@]}" +else + "./code/build/bin/${name}" +fi # [Teardown] cd "${calling_dir}" diff --git a/test b/test new file mode 100755 index 0000000..3a8b2f4 --- /dev/null +++ b/test @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Copyright 2024 GlitchyByte +# SPDX-License-Identifier: MIT-0 + +# Runs project. + +# [Setup] +set -u # Exit with an error if a variable is used without being set. +set -e # Exit if any command returns an error. +# Capture caller directory and script directory. +readonly calling_dir="${PWD}" +readonly script_dir="$(cd "$(dirname "$0")" && pwd)" +# Go to script directory and load utilities. +cd "${script_dir}" +. ./_gcolors + +# [Main] + +# Check for "clean". +if [ $# -gt 0 ]; then + if [ "$1" == "clean" ]; then + readonly clean="yes" + else + readonly clean="no" + fi +else + readonly clean="no" +fi + +# Dir constants. +cd "${script_dir}/code/test" +readonly buildConfigDir="../build/test.cmake" +readonly binDir="../build/bin" + +# Make sure build exists. +if [ ! -d "../build" ]; then + mkdir "../build" +fi + +if [ "$clean" == "yes" ]; then + echo "${c_bold}${cf_black}Refreshing configuration...${c_reset}" + # Remove build dir. + if [ -d "${buildConfigDir}" ]; then + rm -dr "${buildConfigDir}" + fi + # Clean bin dir. + if [ -d "${binDir}" ]; then + rm -dr "${binDir}" + mkdir "${binDir}" + fi +fi + +# Configure. +echo "${c_bold}${cf_black}Configuring: ${cf_white}MinSizeRel${c_reset}" +cmake -DCMAKE_BUILD_TYPE=MinSizeRel -B "${buildConfigDir}" -S . + +# Build. +echo "${c_bold}${cf_black}Building: ${cf_white}MinSizeRel${c_reset}" +cmake --build "${buildConfigDir}" --config MinSizeRel --parallel + +# Back to root. +cd "${calling_dir}" + +# Test. +echo "${c_bold}${cf_black}Testing: ${cf_white}gbtest${c_reset}" +"./code/build/bin/gbtest" + +# [Teardown] +# Done! +echo "${cf_green}${c_bold}Testing done!${c_reset}" diff --git a/version b/version index 3eefcb9..9084fa2 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.0.0 +1.1.0