test: Update 19 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 34s
Build-Test-Publish / build (linux/arm64) (push) Successful in 44s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 8s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 8s
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 34s
Build-Test-Publish / build (linux/arm64) (push) Successful in 44s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 8s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 8s
This commit is contained in:
parent
0065a41012
commit
dd0fc37798
25
dehydrate/.gitignore
vendored
Normal file
25
dehydrate/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Build directories
|
||||||
|
build*/
|
||||||
|
output/
|
||||||
|
test_temp/
|
||||||
|
|
||||||
|
# CMake generated files
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
Makefile
|
||||||
|
|
||||||
|
# Compiled executables
|
||||||
|
dehydrate
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
38
dehydrate/CMakeLists.txt
Normal file
38
dehydrate/CMakeLists.txt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
# Project setup
|
||||||
|
if(NOT DEFINED PROJECT_NAME)
|
||||||
|
message(FATAL_ERROR "PROJECT_NAME is not defined. Pass it via -DPROJECT_NAME=<name>")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(TIMESTAMP PROJECT_VERSION "%Y.%m%d.%H%M")
|
||||||
|
project(${PROJECT_NAME} VERSION ${PROJECT_VERSION} LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Build configuration
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "-static")
|
||||||
|
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
|
||||||
|
set(BUILD_SHARED_LIBS OFF)
|
||||||
|
set(CMAKE_PREFIX_PATH /usr/local)
|
||||||
|
|
||||||
|
# Create executable
|
||||||
|
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||||
|
add_executable(${PROJECT_NAME} ${SOURCES})
|
||||||
|
|
||||||
|
# Configure version.hpp
|
||||||
|
configure_file("src/version.hpp.in" "src/autogen/version.hpp" @ONLY)
|
||||||
|
|
||||||
|
# Pre-build script (optional - remove if not needed)
|
||||||
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake_prebuild.sh")
|
||||||
|
add_custom_target(run_prebuild_script ALL
|
||||||
|
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/cmake_prebuild.sh
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
add_dependencies(${PROJECT_NAME} run_prebuild_script)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/src/autogen
|
||||||
|
include
|
||||||
|
contrib)
|
66
dehydrate/Dockerfile.dropshell-build
Normal file
66
dehydrate/Dockerfile.dropshell-build
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
ARG IMAGE_TAG
|
||||||
|
FROM gitea.jde.nz/public/dropshell-build-base:latest AS builder
|
||||||
|
|
||||||
|
ARG PROJECT
|
||||||
|
ARG CMAKE_BUILD_TYPE=Debug
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
|
||||||
|
# Create cache directories
|
||||||
|
RUN mkdir -p /ccache
|
||||||
|
|
||||||
|
# Set up ccache
|
||||||
|
ENV CCACHE_DIR=/ccache
|
||||||
|
ENV CCACHE_COMPILERCHECK=content
|
||||||
|
ENV CCACHE_MAXSIZE=2G
|
||||||
|
|
||||||
|
# Copy build files
|
||||||
|
COPY CMakeLists.txt ./
|
||||||
|
COPY src/version.hpp.in src/
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
COPY src/ src/
|
||||||
|
COPY include/ include/
|
||||||
|
COPY contrib/ contrib/
|
||||||
|
|
||||||
|
# Configure project
|
||||||
|
RUN --mount=type=cache,target=/ccache \
|
||||||
|
--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" \
|
||||||
|
-DPROJECT_NAME="${PROJECT}" \
|
||||||
|
-DCMAKE_STRIP=OFF \
|
||||||
|
${CMAKE_TOOLCHAIN_FILE:+-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE}
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
RUN --mount=type=cache,target=/ccache \
|
||||||
|
--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} \;
|
||||||
|
|
||||||
|
# if we're a release build, then run upx on the binary.
|
||||||
|
RUN if [ "${CMAKE_BUILD_TYPE}" = "Release" ]; then \
|
||||||
|
upx /output/${PROJECT}; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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}
|
57
dehydrate/README.md
Normal file
57
dehydrate/README.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Dehydrate
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Automated system-wide installation:
|
||||||
|
```
|
||||||
|
curl -fsSL https://gitea.jde.nz/public/dehydrate/releases/download/latest/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
To download just the dehydrate executable:
|
||||||
|
```
|
||||||
|
curl -fsSL -o dehydrate https://gitea.jde.nz/public/dehydrate/releases/download/latest/dehydrate.amd64 && chmod a+x dehydrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it Works
|
||||||
|
|
||||||
|
Dehydrate converts existing files to C++ source code which can be used to recreate the original files.
|
||||||
|
Works on individual files or entire directory trees.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage: dehydrate [OPTIONS] SOURCE DEST
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-s Silent mode (no output)
|
||||||
|
-u Update dehydrate to the latest version
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dehydrate file.txt output/ Creates _file.txt.cpp and _file.txt.hpp in output/
|
||||||
|
dehydrate src/ output/ Creates _src.cpp and _src.hpp in output/
|
||||||
|
dehydrate -u Updates dehydrate to the latest version
|
||||||
|
```
|
||||||
|
|
||||||
|
All c++ code produced is in namespace `recreate_{SOURCEFILE|SOURCEFOLDER}`
|
||||||
|
|
||||||
|
When the source is a file, the C++ function created is:
|
||||||
|
```
|
||||||
|
bool recreate_file(std::string destination_folder);
|
||||||
|
```
|
||||||
|
The path is the full path to the destination folder, excluding the filename. The original filename is used.
|
||||||
|
If a file exists at that location:
|
||||||
|
The hash of the content is compared, and the file is overwritten if the hash is different, otherwise
|
||||||
|
left unchanged.
|
||||||
|
If there is no file:
|
||||||
|
SOURCEFILE is written in destination_folder.
|
||||||
|
|
||||||
|
|
||||||
|
When the source is a folder, the C++ function created is:
|
||||||
|
```
|
||||||
|
bool recreate_tree(std::string destination_folder);
|
||||||
|
```
|
||||||
|
The destination_folder (and any needed subfolders) are created if they don't exist.
|
||||||
|
The contents of the original SOURCEFOLDER are recreated inside destination_folder,
|
||||||
|
overwriting any existing content in that folder if the hash of the individual file
|
||||||
|
is different than existing content, or if the file doesn't exist.
|
||||||
|
|
||||||
|
In both cases, unless in silent mode, confirmation is displayed for each file, showing
|
||||||
|
the existing and new hash, and whether the file was updated or not.
|
24
dehydrate/build.sh
Executable file
24
dehydrate/build.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
|
||||||
|
|
||||||
|
export CMAKE_BUILD_TYPE="Debug"
|
||||||
|
|
||||||
|
rm -rf "${SCRIPT_DIR}/output"
|
||||||
|
mkdir -p "${SCRIPT_DIR}/output"
|
||||||
|
|
||||||
|
PROJECT="dehydrate"
|
||||||
|
|
||||||
|
# make sure we have the latest base image.
|
||||||
|
docker pull gitea.jde.nz/public/dropshell-build-base:latest
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
-t "${PROJECT}-build" \
|
||||||
|
-f "${SCRIPT_DIR}/Dockerfile.dropshell-build" \
|
||||||
|
--build-arg PROJECT="${PROJECT}" \
|
||||||
|
--build-arg CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
|
||||||
|
--output "${SCRIPT_DIR}/output" \
|
||||||
|
"${SCRIPT_DIR}"
|
7343
dehydrate/contrib/xxhash.hpp
Normal file
7343
dehydrate/contrib/xxhash.hpp
Normal file
File diff suppressed because it is too large
Load Diff
1
dehydrate/include/.gitkeep
Normal file
1
dehydrate/include/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file ensures the include directory is tracked by git.
|
11
dehydrate/include/argparse.hpp
Normal file
11
dehydrate/include/argparse.hpp
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
bool silent = false;
|
||||||
|
bool update = false;
|
||||||
|
std::string source;
|
||||||
|
std::string dest;
|
||||||
|
};
|
||||||
|
|
||||||
|
Args parse_args(int argc, char* argv[]);
|
5
dehydrate/include/generator.hpp
Normal file
5
dehydrate/include/generator.hpp
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
void generate_file_code(const std::string& source, const std::string& destfolder, bool silent);
|
||||||
|
void generate_folder_code(const std::string& source, const std::string& destfolder, bool silent);
|
5
dehydrate/include/hash.hpp
Normal file
5
dehydrate/include/hash.hpp
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string hash_file(const std::string& path);
|
||||||
|
std::string hash_data(const std::string& data);
|
51
dehydrate/install.sh
Executable file
51
dehydrate/install.sh
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROJECT="dehydrate"
|
||||||
|
|
||||||
|
# RUN AS ROOT
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "This script must be run as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 0. see if we were passed a folder to install to
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
INSTALL_DIR="$1"
|
||||||
|
if [[ -z "$INSTALL_DIR" ]]; then
|
||||||
|
INSTALL_DIR="/usr/local/bin"
|
||||||
|
else
|
||||||
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "Installing $PROJECT to $INSTALL_DIR"
|
||||||
|
|
||||||
|
# 1. Determine architecture
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
if [[ "$ARCH" == "x86_64" ]]; then
|
||||||
|
BIN=$PROJECT.amd64
|
||||||
|
elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
|
||||||
|
BIN=$PROJECT.arm64
|
||||||
|
else
|
||||||
|
echo "Unsupported architecture: $ARCH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Download the appropriate binary
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
URL="https://gitea.jde.nz/public/$PROJECT/releases/download/latest/$BIN"
|
||||||
|
echo "Downloading $BIN from $URL to $TMPDIR..."
|
||||||
|
|
||||||
|
curl -fsSL -o "$INSTALL_DIR/$PROJECT" "$URL"
|
||||||
|
|
||||||
|
# 4. Make it executable
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
chmod +x "$INSTALL_DIR/$PROJECT"
|
||||||
|
|
||||||
|
# 6. Print success message
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
echo "$PROJECT installed successfully to $INSTALL_DIR/$PROJECT (arch $ARCH)"
|
80
dehydrate/publish.sh
Executable file
80
dehydrate/publish.sh
Executable file
@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
PROJECT="dehydrate"
|
||||||
|
OUTPUT="${SCRIPT_DIR}/output"
|
||||||
|
|
||||||
|
|
||||||
|
function heading() {
|
||||||
|
# print a heading with a line of dashe
|
||||||
|
echo "--------------------------------"
|
||||||
|
echo "$1"
|
||||||
|
echo "--------------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
heading "Publishing ${PROJECT}"
|
||||||
|
|
||||||
|
function die() {
|
||||||
|
heading "error: $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -n $SOS_WRITE_TOKEN ]] || die "SOS_WRITE_TOKEN not specified"
|
||||||
|
|
||||||
|
# clear output dir
|
||||||
|
rm -rf "${OUTPUT}"
|
||||||
|
mkdir -p "${OUTPUT}"
|
||||||
|
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
heading "Building ${PROJECT}"
|
||||||
|
|
||||||
|
# build release version
|
||||||
|
export CMAKE_BUILD_TYPE="Release"
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
-t "${PROJECT}-build" \
|
||||||
|
-f "${SCRIPT_DIR}/Dockerfile.dropshell-build" \
|
||||||
|
--build-arg PROJECT="${PROJECT}" \
|
||||||
|
--build-arg CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
|
||||||
|
--output "${OUTPUT}" \
|
||||||
|
"${SCRIPT_DIR}"
|
||||||
|
|
||||||
|
[ -f "${OUTPUT}/${PROJECT}" ] || die "Build failed."
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
SOS="${SCRIPT_DIR}/../sos/sos"
|
||||||
|
[ -f "${SOS}" ] || die "Failed to find sos"
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
heading "Uploading ${PROJECT} to getbin.xyz"
|
||||||
|
|
||||||
|
# upload arch-specific binary
|
||||||
|
"${SOS}" upload "getbin.xyz" "${OUTPUT}/${PROJECT}" "${PROJECT}:latest-${ARCH}"
|
||||||
|
|
||||||
|
# upload generic install script (ok if multiple times as we iterate through arch's)
|
||||||
|
"${SOS}" upload "getbin.xyz" "${SCRIPT_DIR}/install.sh" "${PROJECT}-install:latest"
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
heading "Publishing ${PROJECT} as tool to getpkg.xyz"
|
||||||
|
|
||||||
|
# Create tool directory structure
|
||||||
|
TOOLDIR="${OUTPUT}/tool"
|
||||||
|
mkdir "${TOOLDIR}"
|
||||||
|
cp "${OUTPUT}/${PROJECT}" "${TOOLDIR}/${PROJECT}"
|
||||||
|
|
||||||
|
# Use getpkg to publish the tool
|
||||||
|
GETPKG="${SCRIPT_DIR}/../getpkg/output/getpkg"
|
||||||
|
if [ ! -f "$GETPKG" ]; then
|
||||||
|
GETPKG="${SCRIPT_DIR}/../getpkg/getpkg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$GETPKG" ]; then
|
||||||
|
"${GETPKG}" publish "${PROJECT}:${ARCH}" "${TOOLDIR}"
|
||||||
|
else
|
||||||
|
echo "Warning: getpkg not found, skipping tool publishing to getpkg.xyz"
|
||||||
|
fi
|
1
dehydrate/src/.gitkeep
Normal file
1
dehydrate/src/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file ensures the src directory is tracked by git.
|
45
dehydrate/src/argparse.cpp
Normal file
45
dehydrate/src/argparse.cpp
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#include "argparse.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
static const std::string HELP_TEXT = R"(
|
||||||
|
Converts existing files to C++ source code which can be used to recreate the original files.
|
||||||
|
|
||||||
|
Usage: dehydrate [OPTIONS] SOURCE DEST
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-s Silent mode (no output)
|
||||||
|
-u Update dehydrate to the latest version
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dehydrate file.txt output/ Creates _file.txt.cpp and _file.txt.hpp in output/
|
||||||
|
dehydrate src/ output/ Creates _src.cpp and _src.hpp in output/
|
||||||
|
dehydrate -u Updates dehydrate to the latest version
|
||||||
|
)";
|
||||||
|
|
||||||
|
Args parse_args(int argc, char* argv[]) {
|
||||||
|
Args args;
|
||||||
|
int idx = 1;
|
||||||
|
|
||||||
|
// Check for silent flag
|
||||||
|
if (idx < argc && std::string(argv[idx]) == "-s") {
|
||||||
|
args.silent = true;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for update flag
|
||||||
|
if (idx < argc && std::string(argv[idx]) == "-u") {
|
||||||
|
args.update = true;
|
||||||
|
idx++;
|
||||||
|
return args; // No need for source and dest parameters when updating
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require source and dest parameters for normal operation
|
||||||
|
if (argc - idx != 2) {
|
||||||
|
throw std::runtime_error(HELP_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.source = argv[idx];
|
||||||
|
args.dest = argv[idx + 1];
|
||||||
|
return args;
|
||||||
|
}
|
372
dehydrate/src/generator.cpp
Normal file
372
dehydrate/src/generator.cpp
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
#include "generator.hpp"
|
||||||
|
#include "../include/hash.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
#include "xxhash.hpp"
|
||||||
|
#include <sys/stat.h> // For file permissions
|
||||||
|
#include <cstring> // For strlen
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
static std::string sanitize(const std::string& name) {
|
||||||
|
std::string out = name;
|
||||||
|
for (char& c : out) if (!isalnum(c)) c = '_';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static uint64_t fnv1a_64(const void* data, size_t len) {
|
||||||
|
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||||
|
uint64_t h = 0xcbf29ce484222325ULL;
|
||||||
|
for (size_t i = 0; i < len; ++i)
|
||||||
|
h = (h ^ p[i]) * 0x100000001b3ULL;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 encoding function - no dependencies
|
||||||
|
static std::string base64_encode(const unsigned char* data, size_t len) {
|
||||||
|
const char* base64_chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string result;
|
||||||
|
result.reserve((len + 2) / 3 * 4); // Reserve space for the full encoded size
|
||||||
|
|
||||||
|
int val = 0, valb = -6;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
val = (val << 8) + data[i];
|
||||||
|
valb += 8;
|
||||||
|
while (valb >= 0) {
|
||||||
|
result.push_back(base64_chars[(val >> valb) & 0x3F]);
|
||||||
|
valb -= 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valb > -6) {
|
||||||
|
result.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
while (result.size() % 4) {
|
||||||
|
result.push_back('=');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to output the _recreate_file_ utility function and Base64 decoder
|
||||||
|
static void output_recreate_file_utility(std::ofstream& cpp) {
|
||||||
|
cpp << R"cpp(
|
||||||
|
// Base64 decoding function - no dependencies
|
||||||
|
static void base64_decode(const char* encoded_data, size_t encoded_len, unsigned char* output, size_t* output_len) {
|
||||||
|
const char* base64_chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
size_t out_pos = 0;
|
||||||
|
int val = 0, valb = -8;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < encoded_len; i++) {
|
||||||
|
char c = encoded_data[i];
|
||||||
|
if (c == '=') break;
|
||||||
|
|
||||||
|
// Find position in base64_chars
|
||||||
|
const char* pos = strchr(base64_chars, c);
|
||||||
|
if (pos == nullptr) continue; // Skip invalid characters
|
||||||
|
|
||||||
|
val = (val << 6) + static_cast<int>(pos - base64_chars);
|
||||||
|
valb += 6;
|
||||||
|
if (valb >= 0) {
|
||||||
|
output[out_pos++] = static_cast<unsigned char>((val >> valb) & 0xFF);
|
||||||
|
valb -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*output_len = out_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function to recreate a file with proper permissions
|
||||||
|
static bool _recreate_file_(const std::filesystem::path& outpath, uint64_t file_hash, std::filesystem::perms file_perms, const unsigned char* filedata, size_t filedata_len) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
bool needs_write = false;
|
||||||
|
|
||||||
|
// Check if file exists and has correct hash
|
||||||
|
if (fs::exists(outpath)) {
|
||||||
|
// Check content hash
|
||||||
|
std::ifstream in(outpath, std::ios::binary);
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << in.rdbuf();
|
||||||
|
std::string data = oss.str();
|
||||||
|
uint64_t existing_hash = fnv1a_64(data.data(), data.size());
|
||||||
|
needs_write = existing_hash != file_hash;
|
||||||
|
} else {
|
||||||
|
needs_write = true; // File doesn't exist, need to create it
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needs_permission_update = true;
|
||||||
|
if (!needs_write) { // we always update permissions if the file is written or changed. Othewise we check.
|
||||||
|
fs::perms current_perms = fs::status(outpath).permissions();
|
||||||
|
needs_permission_update = current_perms != file_perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needs_write) {
|
||||||
|
bool existed = fs::exists(outpath);
|
||||||
|
|
||||||
|
fs::create_directories(outpath.parent_path());
|
||||||
|
std::ofstream out(outpath, std::ios::binary);
|
||||||
|
out.write(reinterpret_cast<const char*>(filedata), filedata_len);
|
||||||
|
out.close();
|
||||||
|
// Set the file permissions
|
||||||
|
fs::permissions(outpath, file_perms);
|
||||||
|
|
||||||
|
if (!existed) {
|
||||||
|
std::cout << "[dehydrate] " << outpath.filename() << ": created\n";
|
||||||
|
} else {
|
||||||
|
std::cout << "[dehydrate] " << outpath.filename() << ": updated (hash changed)\n";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needs_permission_update) {
|
||||||
|
// Update only permissions
|
||||||
|
fs::permissions(outpath, file_perms);
|
||||||
|
std::cout << "[dehydrate] " << outpath.filename() << ": updated (permissions changed)\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
)cpp";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void generate_file_code(const std::string& source, const std::string& destfolder, bool silent) {
|
||||||
|
fs::path src(source);
|
||||||
|
fs::path dest(destfolder);
|
||||||
|
std::string ns = "recreate_" + sanitize(src.stem().string());
|
||||||
|
std::string cppname = "_" + src.stem().string() + ".cpp";
|
||||||
|
std::string hppname = "_" + src.stem().string() + ".hpp";
|
||||||
|
std::string bothname = "_" + src.stem().string() + ".{cpp,hpp}";
|
||||||
|
fs::create_directories(dest);
|
||||||
|
std::ifstream in(source, std::ios::binary);
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << in.rdbuf();
|
||||||
|
std::string filedata = oss.str();
|
||||||
|
uint64_t hash = fnv1a_64(filedata.data(), filedata.size());
|
||||||
|
|
||||||
|
// Get source file permissions
|
||||||
|
fs::perms src_perms = fs::status(src).permissions();
|
||||||
|
|
||||||
|
// Write HPP
|
||||||
|
std::ofstream hpp(dest / hppname);
|
||||||
|
hpp << "#pragma once\n#include <string>\nnamespace " << ns << " {\nbool recreate_file(std::string destination_folder);\n}\n";
|
||||||
|
// Write CPP
|
||||||
|
std::ofstream cpp(dest / cppname);
|
||||||
|
cpp << R"cpp(#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// Tiny dependency-free FNV-1a 64-bit hash
|
||||||
|
static uint64_t fnv1a_64(const void* data, size_t len) {
|
||||||
|
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||||
|
uint64_t h = 0xcbf29ce484222325ULL;
|
||||||
|
for (size_t i = 0; i < len; ++i)
|
||||||
|
h = (h ^ p[i]) * 0x100000001b3ULL;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
)cpp";
|
||||||
|
cpp << "#include \"" << hppname << "\"\n";
|
||||||
|
cpp << "namespace " << ns << " {\n";
|
||||||
|
|
||||||
|
// Output the recreate_file utility function
|
||||||
|
output_recreate_file_utility(cpp);
|
||||||
|
|
||||||
|
// Write recreate_file function with embedded file data
|
||||||
|
cpp << R"cpp(
|
||||||
|
bool recreate_file(std::string destination_folder) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
fs::path outpath = fs::path(destination_folder) / ")cpp" << src.filename().string() << R"cpp(";
|
||||||
|
|
||||||
|
// File data embedded as Base64
|
||||||
|
static const char filedata_base64[] = )cpp";
|
||||||
|
|
||||||
|
// Encode the file data to Base64
|
||||||
|
std::string base64 = base64_encode(reinterpret_cast<const unsigned char*>(filedata.data()), filedata.size());
|
||||||
|
|
||||||
|
// Split into 76-character chunks for readability
|
||||||
|
const size_t line_length = 76;
|
||||||
|
for (size_t i = 0; i < base64.length(); i += line_length) {
|
||||||
|
if (i > 0) cpp << "\n ";
|
||||||
|
cpp << "\"" << base64.substr(i, std::min(line_length, base64.length() - i)) << "\"";
|
||||||
|
if (i + line_length < base64.length()) cpp << "\\";
|
||||||
|
}
|
||||||
|
cpp << ";\n\n";
|
||||||
|
|
||||||
|
// Decode Base64 at runtime
|
||||||
|
cpp << " // Decode Base64 data\n";
|
||||||
|
cpp << " size_t decoded_size = (strlen(filedata_base64) * 3) / 4;\n";
|
||||||
|
cpp << " unsigned char* decoded_data = new unsigned char[decoded_size];\n";
|
||||||
|
cpp << " size_t actual_size;\n";
|
||||||
|
cpp << " base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);\n\n";
|
||||||
|
|
||||||
|
// Call _recreate_file_ with the decoded data
|
||||||
|
cpp << " bool result = _recreate_file_(outpath, " << hash << "ULL, "
|
||||||
|
<< "std::filesystem::perms(" << static_cast<unsigned>(src_perms) << "), "
|
||||||
|
<< "decoded_data, actual_size);\n";
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
cpp << " delete[] decoded_data;\n";
|
||||||
|
cpp << " return result;\n";
|
||||||
|
cpp << "}\n";
|
||||||
|
cpp << "}\n";
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
std::cout << "[dehydrate] Generated: " << (dest / bothname) << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to recursively collect all files in a directory
|
||||||
|
template<typename F>
|
||||||
|
void walk_dir(const fs::path& dir, F&& f) {
|
||||||
|
for (auto& p : fs::recursive_directory_iterator(dir)) {
|
||||||
|
if (fs::is_regular_file(p)) f(p.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void generate_folder_code(const std::string& source, const std::string& destfolder, bool silent) {
|
||||||
|
fs::path src(source);
|
||||||
|
fs::path dest(destfolder);
|
||||||
|
std::string ns = "recreate_" + sanitize(src.stem().string());
|
||||||
|
std::string cppname = "_" + src.stem().string() + ".cpp";
|
||||||
|
std::string hppname = "_" + src.stem().string() + ".hpp";
|
||||||
|
std::string bothname = "_" + src.stem().string() + ".{cpp,hpp}";
|
||||||
|
fs::create_directories(dest);
|
||||||
|
// Collect all files
|
||||||
|
std::vector<fs::path> files;
|
||||||
|
walk_dir(src, [&](const fs::path& p) { files.push_back(p); });
|
||||||
|
// Write HPP
|
||||||
|
std::ofstream hpp(dest / hppname);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Generate HPP
|
||||||
|
hpp << R"hpp(
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
|
||||||
|
DO NOT EDIT THIS FILE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
namespace )hpp" << ns << R"hpp( {
|
||||||
|
bool recreate_tree(std::string destination_folder);
|
||||||
|
}
|
||||||
|
)hpp";
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Write CPP
|
||||||
|
std::ofstream cpp(dest / cppname);
|
||||||
|
cpp << R"cpp(#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
|
||||||
|
DO NOT EDIT THIS FILE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
)cpp";
|
||||||
|
cpp << "#include \"" << hppname << "\"\n";
|
||||||
|
cpp << "namespace " << ns << " {\n";
|
||||||
|
|
||||||
|
cpp << R"cpp(
|
||||||
|
|
||||||
|
// Tiny dependency-free FNV-1a 64-bit hash
|
||||||
|
static uint64_t fnv1a_64(const void* data, size_t len) {
|
||||||
|
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||||
|
uint64_t h = 0xcbf29ce484222325ULL;
|
||||||
|
for (size_t i = 0; i < len; ++i)
|
||||||
|
h = (h ^ p[i]) * 0x100000001b3ULL;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
)cpp";
|
||||||
|
|
||||||
|
// Output the recreate_file utility function
|
||||||
|
output_recreate_file_utility(cpp);
|
||||||
|
|
||||||
|
// Start writing recreate_tree - we'll embed file data directly in function
|
||||||
|
cpp << R"cpp(
|
||||||
|
bool recreate_tree(std::string destination_folder) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
bool any_written = false;
|
||||||
|
)cpp";
|
||||||
|
|
||||||
|
// Process each file
|
||||||
|
for (const auto& file : files) {
|
||||||
|
std::ifstream in(file, std::ios::binary);
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << in.rdbuf();
|
||||||
|
std::string filedata = oss.str();
|
||||||
|
uint64_t hash = fnv1a_64(filedata.data(), filedata.size());
|
||||||
|
fs::perms file_perms = fs::status(file).permissions();
|
||||||
|
std::string rel = fs::relative(file, src).string();
|
||||||
|
std::string var = sanitize(rel);
|
||||||
|
|
||||||
|
// Start a scope to limit data's lifetime
|
||||||
|
cpp << " {\n";
|
||||||
|
cpp << " // File: " << rel << "\n";
|
||||||
|
cpp << " fs::path outpath = fs::path(destination_folder) / \"" << rel << "\";\n";
|
||||||
|
|
||||||
|
// Embed file data as Base64
|
||||||
|
cpp << " static const char filedata_base64[] = ";
|
||||||
|
|
||||||
|
// Encode the file data to Base64
|
||||||
|
std::string base64 = base64_encode(reinterpret_cast<const unsigned char*>(filedata.data()), filedata.size());
|
||||||
|
|
||||||
|
// Split into 76-character chunks for readability
|
||||||
|
const size_t line_length = 76;
|
||||||
|
for (size_t i = 0; i < base64.length(); i += line_length) {
|
||||||
|
if (i > 0) cpp << "\n ";
|
||||||
|
cpp << "\"" << base64.substr(i, std::min(line_length, base64.length() - i)) << "\"";
|
||||||
|
if (i + line_length < base64.length()) cpp << "\\";
|
||||||
|
}
|
||||||
|
cpp << ";\n\n";
|
||||||
|
|
||||||
|
// Decode Base64 at runtime
|
||||||
|
cpp << " // Decode Base64 data\n";
|
||||||
|
cpp << " size_t decoded_size = (strlen(filedata_base64) * 3) / 4;\n";
|
||||||
|
cpp << " unsigned char* decoded_data = new unsigned char[decoded_size];\n";
|
||||||
|
cpp << " size_t actual_size;\n";
|
||||||
|
cpp << " base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);\n\n";
|
||||||
|
|
||||||
|
// Call _recreate_file_ with the decoded data
|
||||||
|
cpp << " bool file_written = _recreate_file_(outpath, "
|
||||||
|
<< hash << "ULL, std::filesystem::perms(" << static_cast<unsigned>(file_perms) << "), "
|
||||||
|
<< "decoded_data, actual_size);\n";
|
||||||
|
|
||||||
|
// Clean up and update flag
|
||||||
|
cpp << " delete[] decoded_data;\n";
|
||||||
|
cpp << " any_written = any_written || file_written;\n";
|
||||||
|
cpp << " }\n"; // Close scope to free memory
|
||||||
|
}
|
||||||
|
|
||||||
|
cpp << " return any_written;\n";
|
||||||
|
cpp << "}\n";
|
||||||
|
cpp << "}\n";
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
std::cout << "[dehydrate] Generated: " << (dest / bothname) << std::endl;
|
||||||
|
}
|
||||||
|
}
|
33
dehydrate/src/hash.cpp
Normal file
33
dehydrate/src/hash.cpp
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#include "hash.hpp"
|
||||||
|
#define XXH_INLINE_ALL
|
||||||
|
#include "xxhash.hpp"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
|
static std::string to_hex64(uint64_t value) {
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::hex << std::setw(16) << std::setfill('0') << value;
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hash_data(const std::string& data) {
|
||||||
|
uint64_t h = XXH3_64bits(data.data(), data.size());
|
||||||
|
return to_hex64(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hash_file(const std::string& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) return "";
|
||||||
|
XXH64_state_t* state = XXH64_createState();
|
||||||
|
XXH64_reset(state, 0);
|
||||||
|
char buf[4096];
|
||||||
|
while (file) {
|
||||||
|
file.read(buf, sizeof(buf));
|
||||||
|
std::streamsize n = file.gcount();
|
||||||
|
if (n > 0) XXH64_update(state, buf, static_cast<size_t>(n));
|
||||||
|
}
|
||||||
|
uint64_t h = XXH64_digest(state);
|
||||||
|
XXH64_freeState(state);
|
||||||
|
return to_hex64(h);
|
||||||
|
}
|
86
dehydrate/src/main.cpp
Normal file
86
dehydrate/src/main.cpp
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "argparse.hpp"
|
||||||
|
#include "generator.hpp"
|
||||||
|
#include "version.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
std::string get_arch()
|
||||||
|
{
|
||||||
|
// determine the architecture of the system
|
||||||
|
std::string arch;
|
||||||
|
#ifdef __aarch64__
|
||||||
|
arch = "arm64";
|
||||||
|
#elif __x86_64__
|
||||||
|
arch = "amd64";
|
||||||
|
#endif
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
|
|
||||||
|
int update()
|
||||||
|
{
|
||||||
|
// determine path to this executable
|
||||||
|
std::filesystem::path exepath = std::filesystem::canonical("/proc/self/exe");
|
||||||
|
std::filesystem::path parent_path = exepath.parent_path();
|
||||||
|
std::string project_name = exepath.filename().string();
|
||||||
|
|
||||||
|
// determine the architecture of the system
|
||||||
|
std::string arch = get_arch();
|
||||||
|
|
||||||
|
std::string url = "https://gitea.jde.nz/public/"+project_name+"/releases/download/latest/"+project_name+"." + arch;
|
||||||
|
|
||||||
|
// download new version, preserve permissions and ownership
|
||||||
|
std::string bash_script;
|
||||||
|
bash_script += "docker run --rm -v "+parent_path.string()+":/target";
|
||||||
|
bash_script += " gitea.jde.nz/public/debian-curl:latest";
|
||||||
|
bash_script += " sh -c \"";
|
||||||
|
bash_script += " curl -fsSL " + url + " -o /target/"+project_name+"_temp &&";
|
||||||
|
bash_script += " chmod --reference=/target/"+project_name+" /target/"+project_name+"_temp &&";
|
||||||
|
bash_script += " chown --reference=/target/"+project_name+" /target/"+project_name+"_temp &&";
|
||||||
|
bash_script += " mv /target/"+project_name+"_temp /target/"+project_name;
|
||||||
|
bash_script += "\"";
|
||||||
|
|
||||||
|
std::cout << "Updating " << exepath << " to the latest " << arch << " version." << std::endl;
|
||||||
|
|
||||||
|
// std::cout << "bash_script: " << std::endl
|
||||||
|
// << bash_script << std::endl;
|
||||||
|
|
||||||
|
// run the bash script
|
||||||
|
execlp("bash", "bash", "-c", bash_script.c_str(), (char *)nullptr);
|
||||||
|
std::cerr << "Failed to execute command." << std::endl;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
try {
|
||||||
|
std::cout << "Dehydrate version " << VERSION << std::endl;
|
||||||
|
Args args = parse_args(argc, argv);
|
||||||
|
|
||||||
|
// Handle update request
|
||||||
|
if (args.update) {
|
||||||
|
return update();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path src(args.source);
|
||||||
|
if (!std::filesystem::exists(src)) {
|
||||||
|
std::cerr << "Source does not exist: " << args.source << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (std::filesystem::is_regular_file(src)) {
|
||||||
|
generate_file_code(args.source, args.dest, args.silent);
|
||||||
|
} else if (std::filesystem::is_directory(src)) {
|
||||||
|
generate_folder_code(args.source, args.dest, args.silent);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Source is neither a file nor a directory: " << args.source << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
std::cerr << ex.what() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
1
dehydrate/src/version.hpp.in
Normal file
1
dehydrate/src/version.hpp.in
Normal file
@ -0,0 +1 @@
|
|||||||
|
static const char *VERSION = "@PROJECT_VERSION@";
|
135
dehydrate/test.sh
Executable file
135
dehydrate/test.sh
Executable file
@ -0,0 +1,135 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Don't use set -e because we want to continue even if tests fail
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
PROJECT="dehydrate"
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
DEHYDRATE="${SCRIPT_DIR}/output/${PROJECT}"
|
||||||
|
TEST_DIR="${SCRIPT_DIR}/test_temp"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Test counters
|
||||||
|
TESTS_PASSED=0
|
||||||
|
TESTS_FAILED=0
|
||||||
|
|
||||||
|
# Function to print test results
|
||||||
|
print_test_result() {
|
||||||
|
local test_name="$1"
|
||||||
|
local result="$2"
|
||||||
|
if [ "$result" -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} $test_name"
|
||||||
|
((TESTS_PASSED++))
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} $test_name"
|
||||||
|
((TESTS_FAILED++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to cleanup test artifacts
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n${YELLOW}Cleaning up test artifacts...${NC}"
|
||||||
|
rm -rf "$TEST_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up trap to ensure cleanup runs
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create test directory
|
||||||
|
mkdir -p "$TEST_DIR"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Running dehydrate tests...${NC}\n"
|
||||||
|
|
||||||
|
# Check if dehydrate binary exists
|
||||||
|
if [ ! -f "$DEHYDRATE" ]; then
|
||||||
|
echo -e "${RED}Error: dehydrate binary not found at $DEHYDRATE${NC}"
|
||||||
|
echo "Please run ./build.sh first to build dehydrate"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$DEHYDRATE" ]; then
|
||||||
|
echo -e "${RED}Error: dehydrate binary is not executable${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using dehydrate binary: $DEHYDRATE"
|
||||||
|
|
||||||
|
# Test 1: Version command (dehydrate shows version in help output)
|
||||||
|
echo "Test 1: Version command"
|
||||||
|
VERSION_OUTPUT=$("$DEHYDRATE" 2>&1 || true)
|
||||||
|
# Extract version from the beginning of help output
|
||||||
|
VERSION=$(echo "$VERSION_OUTPUT" | head -n 1 | sed 's/Dehydrate version //')
|
||||||
|
if [[ "$VERSION" =~ ^[0-9]{4}\.[0-9]{4}\.[0-9]{4}$ ]]; then
|
||||||
|
print_test_result "Version format (YYYY.MMDD.HHMM)" 0
|
||||||
|
else
|
||||||
|
print_test_result "Version format (YYYY.MMDD.HHMM)" 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Help command (shows help when no args provided)
|
||||||
|
echo -e "\nTest 2: Help command"
|
||||||
|
HELP_OUTPUT=$("$DEHYDRATE" 2>&1 || true)
|
||||||
|
if [[ "$HELP_OUTPUT" =~ "Usage: dehydrate" ]] && [[ "$HELP_OUTPUT" =~ "Converts existing files" ]]; then
|
||||||
|
print_test_result "Help command output" 0
|
||||||
|
else
|
||||||
|
print_test_result "Help command output" 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Basic dehydration test
|
||||||
|
echo -e "\nTest 3: Basic dehydration test"
|
||||||
|
# Create a test source directory
|
||||||
|
TEST_SRC_DIR="${TEST_DIR}/test_src"
|
||||||
|
mkdir -p "$TEST_SRC_DIR"
|
||||||
|
echo "int main() { return 0; }" > "$TEST_SRC_DIR/main.cpp"
|
||||||
|
echo "#include <iostream>" > "$TEST_SRC_DIR/header.hpp"
|
||||||
|
|
||||||
|
# Run dehydrate on the test source
|
||||||
|
DEHYDRATE_OUTPUT=$("$DEHYDRATE" "$TEST_SRC_DIR" "$TEST_DIR" 2>&1 || true)
|
||||||
|
# Dehydrate creates files with pattern _<source_dir_name>.{cpp,hpp}
|
||||||
|
if [ -f "$TEST_DIR/_test_src.hpp" ] && [ -f "$TEST_DIR/_test_src.cpp" ]; then
|
||||||
|
print_test_result "Basic dehydration creates output files" 0
|
||||||
|
else
|
||||||
|
print_test_result "Basic dehydration creates output files" 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: Test the generated files have valid syntax
|
||||||
|
echo -e "\nTest 4: Test generated files syntax"
|
||||||
|
if [ -f "$TEST_DIR/_test_src.hpp" ] && [ -f "$TEST_DIR/_test_src.cpp" ]; then
|
||||||
|
# Check that the header file has the expected namespace declaration
|
||||||
|
if grep -q "namespace recreate_test_src" "$TEST_DIR/_test_src.hpp"; then
|
||||||
|
# Check that the cpp file includes the header
|
||||||
|
if grep -q "#include \"_test_src.hpp\"" "$TEST_DIR/_test_src.cpp"; then
|
||||||
|
# Check that the function is declared
|
||||||
|
if grep -q "recreate_tree" "$TEST_DIR/_test_src.hpp"; then
|
||||||
|
print_test_result "Generated files have correct structure" 0
|
||||||
|
else
|
||||||
|
print_test_result "Generated files have correct structure" 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_test_result "Generated files have correct structure" 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_test_result "Generated files have correct structure" 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_test_result "Generated files have correct structure" 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo -e "\n${YELLOW}Test Summary:${NC}"
|
||||||
|
echo -e "Tests passed: ${GREEN}${TESTS_PASSED}${NC}"
|
||||||
|
echo -e "Tests failed: ${RED}${TESTS_FAILED}${NC}"
|
||||||
|
|
||||||
|
if [ "$TESTS_FAILED" -eq 0 ]; then
|
||||||
|
echo -e "\n${GREEN}All tests passed!${NC}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "\n${RED}Some tests failed!${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
Loading…
x
Reference in New Issue
Block a user