diff --git a/.gitea/workflows/buildtestpublish.yaml b/.gitea/workflows/buildtestpublish.yaml index 0c8a098..7f48e71 100644 --- a/.gitea/workflows/buildtestpublish.yaml +++ b/.gitea/workflows/buildtestpublish.yaml @@ -1,27 +1,30 @@ name: Build-Test-Publish -run-name: ${{ gitea.actor }} is building, testing and publishing Simple Object Storage +run-name: Build test and publish dropshell-build + on: [push] +defaults: + run: + shell: bash + jobs: - Build: + build: runs-on: ubuntu-latest - container: - image: gitea.jde.nz/public/dropshell-build:latest steps: - - name: Check out repository code + - name: Checkout uses: actions/checkout@v4 - - name: Build and Test the x86 version of the project - run: | - ./testing/test-docker.sh - - name: Build cross-platform versions (amd64 and arm64) - run: | - ./build.sh all - - name: Login to Docker Hub + - name: Login to Gitea uses: docker/login-action@v3 with: registry: gitea.jde.nz - username: anything - password: ${{ secrets.PRIVATE_TOKEN }} - - name: Build and Push amd64 + arm64 Docker image to the registry + username: DoesntMatter + password: ${{ secrets.DOCKER_PUSH_TOKEN }} + - name: Build run: | - ./.runner/publish.sh + ./build.sh + - name: Test + run: | + ./test.sh + - name: Publish + run: | + SOS_WRITE_TOKEN=${{ secrets.SOS_WRITE_TOKEN }} ./publish.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 187846d..abab72d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,71 +1,69 @@ -cmake_minimum_required(VERSION 3.10) -project(simple_object_storage) -set(PROJECT_EXE_NAME simple_object_storage) +cmake_minimum_required(VERSION 3.16) -# Set the build type to Debug -#set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type (e.g., Debug, Release)" FORCE) +# Generate version from current date +execute_process( + COMMAND date "+%Y.%m%d.%H%M" + OUTPUT_VARIABLE PROJECT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) +# Project setup +if(NOT DEFINED PROJECT_NAME) + message(FATAL_ERROR "PROJECT_NAME is not defined. Pass it via -DPROJECT_NAME=") +endif() + +project(${PROJECT_NAME} VERSION ${PROJECT_VERSION} LANGUAGES CXX) + +# C++ standard and build configuration set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Linking flags (removed -static) -set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") +# Static linking configuration +set(CMAKE_EXE_LINKER_FLAGS "-static -Wl,--allow-multiple-definition") +set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") set(BUILD_SHARED_LIBS OFF) -# Configure version information -string(TIMESTAMP CURRENT_YEAR "%Y") -string(TIMESTAMP CURRENT_MONTH "%m") -string(TIMESTAMP CURRENT_DAY "%d") -string(TIMESTAMP CURRENT_HOUR "%H") -string(TIMESTAMP CURRENT_MINUTE "%M") -set(PROJECT_VERSION "${CURRENT_YEAR}.${CURRENT_MONTH}${CURRENT_DAY}.${CURRENT_HOUR}${CURRENT_MINUTE}") -string(TIMESTAMP RELEASE_DATE "%Y-%m-%d") - -# Configure version.hpp file +# Configure version.hpp configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/src/version.hpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/autogen/version.hpp" @ONLY ) -# Find all source files in src directory -file(GLOB_RECURSE SOURCES - "src/*.cpp" - "src/sqlite3/*.c" +# Pre-build script +add_custom_target(run_prebuild_script ALL + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/cmake_prebuild.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) -# Find all header files in src directory -file(GLOB_RECURSE HEADERS - "src/*.hpp" - "src/sqlite3/*.h" -) - -# Add include directories -include_directories( - $ - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/src - ${CMAKE_CURRENT_SOURCE_DIR}/src/autogen -) +# Source files +file(GLOB_RECURSE SOURCES "src/*.cpp") +set_source_files_properties(${SOURCES} PROPERTIES GENERATED TRUE) # Create executable -add_executable(${PROJECT_EXE_NAME} ${SOURCES}) +add_executable(${PROJECT_NAME} ${SOURCES}) +add_dependencies(${PROJECT_NAME} run_prebuild_script) -include(FetchContent) -# Add nlohmann/json -FetchContent_Declare( - nlohmann_json - GIT_REPOSITORY https://github.com/nlohmann/json.git - GIT_TAG v3.11.3 +# Include directories +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/src/autogen + ${CMAKE_CURRENT_SOURCE_DIR}/src ) -FetchContent_MakeAvailable(nlohmann_json) -# Remove specific static flags as CMAKE_EXE_LINKER_FLAGS handles it now -target_link_libraries(${PROJECT_EXE_NAME} +# Find packages +set(CMAKE_PREFIX_PATH /usr/local) +find_package(OpenSSL REQUIRED) +find_package(Drogon CONFIG REQUIRED) +find_package(nlohmann_json REQUIRED) + +# Link libraries +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json -) - -# Install target -install(TARGETS ${PROJECT_EXE_NAME} - RUNTIME DESTINATION bin -) \ No newline at end of file + Drogon::Drogon + /usr/lib/libunwind.a + /usr/lib/libunwind-x86_64.a + /usr/local/lib/libpgcommon.a + /usr/local/lib/libpgport.a + lzma + dl +) \ No newline at end of file diff --git a/Dockerfile.dropshell-build b/Dockerfile.dropshell-build new file mode 100644 index 0000000..74142a3 --- /dev/null +++ b/Dockerfile.dropshell-build @@ -0,0 +1,48 @@ +FROM --platform=$BUILDPLATFORM gitea.jde.nz/public/dropshell-build-base:latest AS builder + +ARG PROJECT +ARG CMAKE_BUILD_TYPE=Debug + +# Set working directory +WORKDIR /app + +COPY . . + +# Configure and build with ccache +RUN --mount=type=cache,target=/build \ + mkdir -p /build && \ + cmake -G Ninja -S /app -B /build \ + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold -static -g" \ + -DCMAKE_CXX_FLAGS="-g -fno-omit-frame-pointer" \ + -DCMAKE_C_FLAGS="-g -fno-omit-frame-pointer" \ + -DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \ + -DZLIB_BUILD_SHARED=OFF \ + -DZLIB_BUILD_STATIC=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DPROJECT_NAME="${PROJECT}" \ + -DCMAKE_STRIP=OFF \ + -DIGNORE_DYNAMIC_LOADING=ON \ + ${CMAKE_TOOLCHAIN_FILE:+-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE} + + +# Explicitly build dependencies first (cached separately) +RUN --mount=type=cache,target=/build cmake --build /build --target run_prebuild_script + +RUN --mount=type=cache,target=/build cmake --build /build + +# Copy the built executable to a regular directory for the final stage +RUN --mount=type=cache,target=/build mkdir -p /output && \ + find /build -type f -executable -name "*${PROJECT}*" -exec cp {} /output/${PROJECT} \; || \ + find /build -type f -executable -exec cp {} /output/${PROJECT} \; + +# Final stage that only contains the binary +FROM scratch AS project + +ARG PROJECT + +# Copy the actual binary from the regular directory +COPY --from=builder /output/${PROJECT} /${PROJECT} + diff --git a/Dockerfile b/Dockerfile.sos similarity index 82% rename from Dockerfile rename to Dockerfile.sos index 120f6d8..a42e3e7 100644 --- a/Dockerfile +++ b/Dockerfile.sos @@ -10,7 +10,7 @@ RUN apk add --no-cache wget curl bash jq RUN mkdir -p /sos && mkdir -p /data/storage -COPY --chmod=0755 output/simple_object_storage.${TARGETARCH} /sos/sos +COPY --chmod=0755 output/simple-object-server /sos/sos COPY testing/ /testing/ # Expose port diff --git a/build.sh b/build.sh index 12ab1d1..7268315 100755 --- a/build.sh +++ b/build.sh @@ -1,110 +1,27 @@ #!/bin/bash -# Exit on error set -euo pipefail -# DIRECTORIES -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -EXE_DIR="${SCRIPT_DIR}/output" -PROJECTNAME="simple_object_storage" -JOBS=${JOBS:-$(nproc)} - -# FUNCTIONS -function title() { - echo "----------------------------------------" - # Center the text - local text="$1" - local line_length=40 - local text_length=${#text} - local padding=$(( (line_length - text_length) / 2 )) - printf "%*s%s%*s\n" $padding "" "$text" $padding "" - echo "----------------------------------------" -} - -function die() { - echo " " - title "$1" - echo " " - exit 1 -} - -function build_for_arch() { - # Arguments: - local arch="$1" - local musl_path="$2" - local c_compiler="$3" - local cxx_compiler="$4" - local exe_linker_flags="$5" - local cxx_flags="$6" - local cmake_system_processor="$7" - - echo "Building for $arch (musl)..." - - local build_dir="build_${arch}" - local cmake_args=( - -B "$build_dir" - -DCMAKE_BUILD_TYPE=Release - "-DCMAKE_C_COMPILER=$musl_path/bin/$c_compiler" - "-DCMAKE_CXX_COMPILER=$musl_path/bin/$cxx_compiler" - -DCMAKE_EXE_LINKER_FLAGS="$exe_linker_flags" - -DCMAKE_CXX_FLAGS="$cxx_flags" - . - ) - if [[ -n "$cmake_system_processor" ]]; then - cmake_args+=("-DCMAKE_SYSTEM_PROCESSOR=$cmake_system_processor") - fi - - cmake "${cmake_args[@]}" - cmake --build "$build_dir" --target simple_object_storage --config Release -j"$JOBS" - mkdir -p "${EXE_DIR}" - cp "$build_dir/${PROJECTNAME}" "${EXE_DIR}/${PROJECTNAME}.$arch" - - echo "$arch executable built: ${PROJECTNAME}.$arch" -} - -function build_amd64() { - build_for_arch \ - "amd64" \ - "$HOME/.musl-cross/x86_64-linux-musl-cross" \ - "x86_64-linux-musl-gcc" \ - "x86_64-linux-musl-g++" \ - "-static" \ - "-march=x86-64" \ - "" -} - -function build_arm64() { - build_for_arch \ - "arm64" \ - "$HOME/.musl-cross/aarch64-linux-musl-cross" \ - "aarch64-linux-musl-gcc" \ - "aarch64-linux-musl-g++" \ - "-static" \ - "-march=armv8-a" \ - "aarch64" -} - -#-------------------------------- -# MAIN -#-------------------------------- - -OLD_DIR=$(pwd) -cd "$SCRIPT_DIR" +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -mkdir -p "${EXE_DIR}" +export CMAKE_BUILD_TYPE="Debug" -BUILDSTR="${1:-amd64}" +rm -rf "${SCRIPT_DIR}/output" +mkdir -p "${SCRIPT_DIR}/output" -if [ "$BUILDSTR" = "all" ] || [ "$BUILDSTR" = "arm64" ]; then - build_arm64 || die "Failed to build linux-arm64 executable" -fi +PROJECT="simple-object-server" +docker build \ + -t "gitea.jde.nz/public/${PROJECT}-build:latest" \ + -f "${SCRIPT_DIR}/Dockerfile.dropshell-build" \ + --build-arg PROJECT="${PROJECT}" \ + --build-arg CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \ + --output "${SCRIPT_DIR}/output" \ + "${SCRIPT_DIR}" -if [ "$BUILDSTR" = "all" ] || [ "$BUILDSTR" = "amd64" ]; then - build_amd64 || die "Failed to build linux-x86_64 executable" -fi +docker buildx build --load \ + -f "${SCRIPT_DIR}/Dockerfile.sos" \ + -t gitea.jde.nz/public/simple-object-server:test \ + "${SCRIPT_DIR}" -echo "Build completed successfully!" -"${EXE_DIR}/${PROJECTNAME}.amd64" version - -cd "$OLD_DIR" \ No newline at end of file +# --platform linux/amd64 \ diff --git a/cmake_prebuild.sh b/cmake_prebuild.sh new file mode 100755 index 0000000..62b287c --- /dev/null +++ b/cmake_prebuild.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "cmake_prebuild.sh complete." diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..ed85abd --- /dev/null +++ b/publish.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +echo "Publishing simple-object-server to gitea.jde.nz/public/simple-object-server:latest" diff --git a/src/assert.hpp b/src/assert.hpp new file mode 100644 index 0000000..19dba04 --- /dev/null +++ b/src/assert.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ANSI color codes +namespace colors { + constexpr const char* reset = "\033[0m"; + constexpr const char* red = "\033[31m"; + constexpr const char* green = "\033[32m"; + constexpr const char* yellow = "\033[33m"; + constexpr const char* blue = "\033[34m"; + constexpr const char* magenta = "\033[35m"; + constexpr const char* cyan = "\033[36m"; +} + +// Simple RAII wrapper for file descriptor +class FileDescriptor { + int fd_ = -1; +public: + explicit FileDescriptor(int fd) : fd_(fd) {} + ~FileDescriptor() { if (fd_ != -1) close(fd_); } + operator int() const { return fd_; } + FileDescriptor(const FileDescriptor&) = delete; + FileDescriptor& operator=(const FileDescriptor&) = delete; +}; + +struct SourceInfo { + std::string function; + std::string file; + int line = 0; +}; + +std::vector get_source_info_batch(const char* executable, const std::vector& addresses) { + if (addresses.empty()) return {}; + + // Build command with all addresses + std::string cmd = std::string("addr2line -f -e ") + executable; + for (void* addr : addresses) { + char addr_buf[32]; + snprintf(addr_buf, sizeof(addr_buf), " %p", addr); + cmd += addr_buf; + } + cmd += " 2>/dev/null"; + + FILE* pipe = popen(cmd.c_str(), "r"); + if (!pipe) return std::vector(addresses.size()); + + std::vector results(addresses.size()); + char buffer[1024] = {0}; + size_t current_frame = 0; + + // Read function names and file/line info for each address + while (current_frame < addresses.size() && fgets(buffer, sizeof(buffer), pipe)) { + // Remove trailing newline + size_t len = strlen(buffer); + if (len > 0 && buffer[len-1] == '\n') { + buffer[len-1] = '\0'; + } + + // Demangle function name + int status = 0; + char* demangled = abi::__cxa_demangle(buffer, nullptr, nullptr, &status); + if (demangled) { + results[current_frame].function = demangled; + free(demangled); + } else { + results[current_frame].function = buffer; + } + + // Read file and line number + if (fgets(buffer, sizeof(buffer), pipe)) { + char* colon = strchr(buffer, ':'); + if (colon) { + *colon = '\0'; + results[current_frame].line = atoi(colon + 1); + + // Extract just the filename + const char* slash = strrchr(buffer, '/'); + results[current_frame].file = (slash ? slash + 1 : buffer); + } + } + + current_frame++; + } + + pclose(pipe); + return results; +} + +void print_stacktrace() { + unw_cursor_t cursor; + unw_context_t context; + unw_word_t pc; + char exe_path[1024] = {0}; + bool found_main = false; + std::vector frames; + + // Get the path to the current executable + ssize_t count = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (count == -1) { + strcpy(exe_path, "[unknown]"); + } else { + exe_path[count] = '\0'; + } + + // Initialize cursor to current frame + unw_getcontext(&context); + unw_init_local(&cursor, &context); + + // First pass: collect frames until we find main + std::vector addresses; + while (unw_step(&cursor) > 0) { + if (unw_get_reg(&cursor, UNW_REG_IP, &pc) != 0 || pc == 0) { + break; + } + addresses.push_back(reinterpret_cast(pc)); + } + + // Get source info for all addresses in batch + std::vector results = get_source_info_batch(exe_path, addresses); + + // Process results and check for main + for (auto& info : results) { + // Stop collecting after main() + if (!info.function.empty() && + (info.function.find("main") != std::string::npos || + info.function.find("__libc_start_main") != std::string::npos)) { + found_main = true; + frames.push_back(info); + break; + } + frames.push_back(info); + } + + // Print the collected frames in reverse order (most recent last) + std::cerr << colors::yellow << "Stack trace (most recent call last):" << colors::reset << "\n"; + + for (size_t i = frames.size(); i-- > 0; ) { + const auto& frame = frames[i]; + std::cerr << " "; // Indent each line + + if (frame.function.empty()) { + std::cerr << colors::red << "[unknown function]" << colors::reset; + } else { + std::cerr << colors::green << frame.function << colors::reset; + } + + if (!frame.file.empty() && frame.line > 0) { + std::cerr << " at " << colors::blue << frame.file << colors::reset + << ":" << colors::magenta << frame.line << colors::reset; + } + + std::cerr << "\n"; + } + + if (!found_main) { + std::cerr << " " << colors::yellow << "[truncated - main() not found]" << colors::reset << "\n"; + } +} + +void assert_failed( + bool condition, + std::string_view message, + std::source_location location = std::source_location::current() +) { + if (!condition) { + std::cerr << "Assertion failed at " << location.file_name() << ":" << location.line() << ": " + << location.function_name() << ": " << message << "\n"; + print_stacktrace(); + std::abort(); + } +} + +#define ASSERT(condition, message) assert_failed(condition, message, std::source_location::current()) diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..98f72d0 --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + + +"${SCRIPT_DIR}/testing/test-docker.sh" diff --git a/testing/compose.yaml b/testing/compose.yaml index 6b210a0..fa88e6b 100644 --- a/testing/compose.yaml +++ b/testing/compose.yaml @@ -1,6 +1,6 @@ services: sos: - image: simple-object-storage-test + image: gitea.jde.nz/public/simple-object-server:test container_name: sos-test network_mode: "host" ports: diff --git a/testing/test-docker.sh b/testing/test-docker.sh index a65c1af..7bd3227 100755 --- a/testing/test-docker.sh +++ b/testing/test-docker.sh @@ -44,15 +44,10 @@ function wait_for_container { [ "${health_status}" == "healthy" ] } -#------------------------------------------------------------------------------------------------ -# build the executable -title "Building amd64 executable" -"${MAIN_DIR}/build.sh" amd64 -#------------------------------------------------------------------------------------------------ -# build the docker image -title "Building docker image" -docker buildx build --no-cache --load -t simple-object-storage-test --platform linux/amd64 "${MAIN_DIR}" +title "Building" +${SCRIPT_DIR}/../build.sh + #------------------------------------------------------------------------------------------------ # run the docker container