'Generic Commit'
Some checks failed
Build-Test-Publish / build (push) Failing after 7s

This commit is contained in:
Your Name
2025-06-16 21:51:10 +12:00
parent d73ae46fe9
commit 24f112ceaf
19 changed files with 0 additions and 0 deletions

47
getpkg/CMakeLists.txt Normal file
View File

@ -0,0 +1,47 @@
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
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)
# Include directories
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/src/autogen
src)
# Find packages
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 Drogon::Drogon
/usr/local/lib/libpgcommon.a /usr/local/lib/libpgport.a
lzma dl)

View File

@ -0,0 +1,72 @@
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
# Create cache directories
RUN mkdir -p /ccache
# Set up ccache
ENV CCACHE_DIR=/ccache
ENV CCACHE_COMPILERCHECK=content
ENV CCACHE_MAXSIZE=2G
# Copy only build files first (for better layer caching)
COPY CMakeLists.txt cmake_prebuild.sh ./
COPY src/version.hpp.in src/
# Run prebuild script early (this rarely changes)
RUN bash cmake_prebuild.sh
# Copy source files (this invalidates cache when source changes)
COPY src/ src/
# Configure project (this step is cached unless CMakeLists.txt changes)
RUN --mount=type=cache,target=/ccache \
--mount=type=cache,target=/build \
mkdir -p /build && \
SSL_LIB=$(find /usr/local -name "libssl.a" | head -1) && \
CRYPTO_LIB=$(find /usr/local -name "libcrypto.a" | head -1) && \
echo "Found SSL: $SSL_LIB, Crypto: $CRYPTO_LIB" && \
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 \
-DOPENSSL_SSL_LIBRARY="$SSL_LIB" \
-DOPENSSL_CRYPTO_LIBRARY="$CRYPTO_LIB" \
-DOPENSSL_INCLUDE_DIR=/usr/local/include \
${CMAKE_TOOLCHAIN_FILE:+-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE}
# Run prebuild script
RUN --mount=type=cache,target=/ccache \
--mount=type=cache,target=/build \
cmake --build /build --target run_prebuild_script
# Build project (ccache will help here when only some files change)
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} \;
# 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}

22
getpkg/build.sh Executable file
View File

@ -0,0 +1,22 @@
#!/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="dropshell-tool"
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}"

8
getpkg/cmake_prebuild.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
#SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
echo "cmake_prebuild.sh complete."

19
getpkg/install.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail
#SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
echo "Installing dropshell-tool"
ARCH=$(uname -m)
wget "https://getbin.xyz/dropshell-tool:latest-${ARCH}" -O bootstrap && chmod a+x bootstrap
./bootstrap install dropshell-tool
rm ./bootstrap
VERSION=$(dropshell-tool version)
echo "Dropshell tool $VERSION installed"

46
getpkg/publish.sh Executable file
View File

@ -0,0 +1,46 @@
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ARCH=$(uname -m)
TEMP_DIR="${SCRIPT_DIR}/temp"
SOS="${TEMP_DIR}/sos"
echo "Publishing ${PROJECT} to gitea.jde.nz/public/${PROJECT}:latest"
function die() {
title "error: $1"
exit 1
}
[[ -n $SOS_WRITE_TOKEN ]] || die "SOS_WRITE_TOKEN not specified"
# clear output dir
rm -rf "${SCRIPT_DIR}/output"
mkdir -p "${SCRIPT_DIR}/output"
# build release version
export CMAKE_BUILD_TYPE="Release"
export PROJECT="dropshell-tool"
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}"
[ -f ${SCRIPT_DIR}/output/dropshell-tool ] || die "Build failed."
# download the sos binary
mkdir -p "${TEMP_DIR}"
trap 'rm -rf "${TEMP_DIR}"' EXIT
curl -L -o "${SOS}" "https://getbin.xyz/sos"
chmod +x "${SOS}"
# upload arch-specific binary
"${SOS}" upload "getbin.xyz" "${SCRIPT_DIR}/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"

View File

@ -0,0 +1,71 @@
#include <string>
#include <vector>
#include <fstream>
#include <filesystem>
#include <cstdlib>
#include <sstream>
#include <iostream>
#include <unistd.h>
#include "ArchiveManager.hpp"
namespace fs = std::filesystem;
ArchiveManager::ArchiveManager() {}
bool ArchiveManager::pack(const std::string& folderPath, const std::string& archivePath) {
// Use system tar to create gzipped tarball
std::ostringstream cmd;
cmd << "tar -czf '" << archivePath << "' -C '" << folderPath << "' .";
int ret = std::system(cmd.str().c_str());
return ret == 0;
}
bool ArchiveManager::unpack(const std::string& archivePath, const std::string& outDir) {
fs::create_directories(outDir);
std::ostringstream cmd;
cmd << "tar -xzf '" << archivePath << "' -C '" << outDir << "'";
int ret = std::system(cmd.str().c_str());
return ret == 0;
}
bool ArchiveManager::readConfigJson(const std::string& archivePath, std::string& outJson) {
// Extract config json to stdout
std::ostringstream cmd;
cmd << "tar -xOzf '" << archivePath << "' dropshell-tool-config.json";
FILE* pipe = popen(cmd.str().c_str(), "r");
if (!pipe) return false;
char buffer[4096];
std::ostringstream ss;
size_t n;
while ((n = fread(buffer, 1, sizeof(buffer), pipe)) > 0) {
ss.write(buffer, n);
}
int ret = pclose(pipe);
if (ret != 0) return false;
outJson = ss.str();
return true;
}
bool ArchiveManager::writeConfigJson(const std::string& archivePath, const std::string& json) {
// 1. Extract archive to temp dir
std::string tmpDir = "/tmp/dropshell_tool_tmp_" + std::to_string(::getpid());
fs::create_directories(tmpDir);
std::ostringstream extractCmd;
extractCmd << "tar -xzf '" << archivePath << "' -C '" << tmpDir << "'";
if (std::system(extractCmd.str().c_str()) != 0) return false;
// 2. Write new config json
std::ofstream ofs(tmpDir + "/dropshell-tool-config.json", std::ios::binary);
if (!ofs) return false;
ofs << json;
ofs.close();
// 3. Repack
std::ostringstream packCmd;
packCmd << "tar -czf '" << archivePath << "' -C '" << tmpDir << "' .";
int ret = std::system(packCmd.str().c_str());
// 4. Cleanup
std::ostringstream rmCmd;
rmCmd << "rm -rf '" << tmpDir << "'";
std::system(rmCmd.str().c_str());
return ret == 0;
}

View File

@ -0,0 +1,11 @@
#pragma once
#include <string>
class ArchiveManager {
public:
ArchiveManager();
bool pack(const std::string& folderPath, const std::string& archivePath);
bool unpack(const std::string& archivePath, const std::string& outDir);
bool readConfigJson(const std::string& archivePath, std::string& outJson);
bool writeConfigJson(const std::string& archivePath, const std::string& json);
};

134
getpkg/src/BashrcEditor.cpp Normal file
View File

@ -0,0 +1,134 @@
#include "BashrcEditor.hpp"
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <algorithm>
#include <filesystem>
#include <iostream>
#include "DropshellScriptManager.hpp"
namespace dropshelltool
{
const std::filesystem::path DROPSHELL_RC_PATH = std::filesystem::path(std::getenv("HOME")) / ".bashrc_dropshell_tool";
static const std::filesystem::path BASHRC_PATH = std::filesystem::path(std::getenv("HOME")) / ".bashrc";
std::string removeWhitespace(const std::string &s)
{
std::string out;
for (char c : s)
{
if (!isspace(static_cast<unsigned char>(c)))
out += c;
}
return out;
}
static constexpr const char *BLOCK_START = "#---DROPSHELL-TOOL-START---";
static constexpr const char *BLOCK_END = "#---DROPSHELL-TOOL-END---";
BashrcEditor::BashrcEditor()
{
}
bool BashrcEditor::hasSourceLine() const
{
std::ifstream infile(BASHRC_PATH);
if (!infile)
return false;
std::string line;
bool inBlock = false;
const std::string blockStart = removeWhitespace(BLOCK_START);
const std::string blockEnd = removeWhitespace(BLOCK_END);
const std::string target = "source \"" + DROPSHELL_RC_PATH.string() + "\"";
while (std::getline(infile, line))
{
std::string trimmed = removeWhitespace(line);
if (trimmed == blockStart)
{
return true;
}
}
return false;
}
void BashrcEditor::addSourceLine()
{
std::ifstream infile(BASHRC_PATH);
std::vector<std::string> lines;
std::string line;
bool inBlock = false;
const std::string blockStart = removeWhitespace(BLOCK_START);
const std::string blockEnd = removeWhitespace(BLOCK_END);
while (std::getline(infile, line))
{
std::string trimmed = removeWhitespace(line);
if (trimmed == blockStart)
{
inBlock = true;
continue;
}
if (trimmed == blockEnd)
{
inBlock = false;
continue;
}
if (!inBlock)
{
lines.push_back(line);
}
}
infile.close();
lines.push_back(BLOCK_START);
lines.push_back("source \"" + DROPSHELL_RC_PATH.string() + "\"");
lines.push_back(BLOCK_END);
std::ofstream outfile(BASHRC_PATH, std::ios::trunc);
for (const auto &l : lines)
{
outfile << l << "\n";
}
outfile.close();
if (!std::filesystem::exists(DROPSHELL_RC_PATH))
{
DropshellScriptManager scriptManager;
scriptManager.ensureExists();
}
}
void BashrcEditor::removeSourceLine()
{
std::ifstream infile(BASHRC_PATH);
std::vector<std::string> lines;
std::string line;
bool inBlock = false;
const std::string blockStart = removeWhitespace(BLOCK_START);
const std::string blockEnd = removeWhitespace(BLOCK_END);
while (std::getline(infile, line))
{
std::string trimmed = removeWhitespace(line);
if (trimmed == blockStart)
{
inBlock = true;
continue;
}
if (trimmed == blockEnd)
{
inBlock = false;
continue;
}
if (!inBlock)
{
lines.push_back(line);
}
}
infile.close();
std::ofstream outfile(BASHRC_PATH, std::ios::trunc);
for (const auto &l : lines)
{
outfile << l << "\n";
}
}
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <filesystem>
namespace dropshelltool {
extern const std::filesystem::path DROPSHELL_RC_PATH;
class BashrcEditor {
public:
BashrcEditor();
// Checks if the unique block with the source line exists
bool hasSourceLine() const;
// Adds or updates the unique block with the given scriptPath
void addSourceLine();
// Removes the unique block
void removeSourceLine();
};
} // namespace dropshelltool

View File

@ -0,0 +1,170 @@
#include "DropshellScriptManager.hpp"
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
#include <algorithm>
namespace dropshelltool {
extern const std::filesystem::path DROPSHELL_RC_PATH;
}
DropshellScriptManager::DropshellScriptManager() {}
// Helper function to trim whitespace from both ends of a string
static std::string trim(const std::string& s) {
auto start = s.begin();
while (start != s.end() && std::isspace(*start)) ++start;
auto end = s.end();
do { --end; } while (std::distance(start, end) > 0 && std::isspace(*end));
return std::string(start, end + 1);
}
void DropshellScriptManager::ensureExists() const {
if (!std::filesystem::exists(dropshelltool::DROPSHELL_RC_PATH)) {
std::filesystem::create_directories(dropshelltool::DROPSHELL_RC_PATH.parent_path());
std::ofstream outfile(dropshelltool::DROPSHELL_RC_PATH, std::ios::trunc);
outfile << "# This file is managed by dropshell-tool. Do not edit manually!" << std::endl;
outfile.close();
}
}
void DropshellScriptManager::addToolEntry(const std::string& toolName, const std::string& toolDir) {
ensureExists();
std::ifstream infile(dropshelltool::DROPSHELL_RC_PATH);
std::vector<std::string> lines;
std::string line;
std::string exportLine = "export PATH=\"" + toolDir + ":$PATH\" # dropshell-tool:" + toolName;
bool found = false;
while (std::getline(infile, line)) {
if (line.find("# dropshell-tool:" + toolName) != std::string::npos) {
found = true;
lines.push_back(exportLine);
} else {
lines.push_back(line);
}
}
infile.close();
if (!found) lines.push_back(exportLine);
std::ofstream outfile(dropshelltool::DROPSHELL_RC_PATH, std::ios::trunc);
for (const auto& l : lines) outfile << l << "\n";
outfile.close();
}
void DropshellScriptManager::removeToolEntry(const std::string& toolName) {
ensureExists();
std::ifstream infile(dropshelltool::DROPSHELL_RC_PATH);
std::vector<std::string> lines;
std::string line;
while (std::getline(infile, line)) {
if (line.find("# dropshell-tool:" + toolName) == std::string::npos) {
lines.push_back(line);
}
}
infile.close();
std::ofstream outfile(dropshelltool::DROPSHELL_RC_PATH, std::ios::trunc);
for (const auto& l : lines) outfile << l << "\n";
outfile.close();
}
void DropshellScriptManager::addAlias(const std::string& alias, const std::string& toolName) {
ensureExists();
std::ifstream infile(dropshelltool::DROPSHELL_RC_PATH);
std::vector<std::string> lines;
std::string line;
std::string aliasLine = "alias " + alias + "='" + toolName + "' # dropshell-alias:" + alias;
bool found = false;
while (std::getline(infile, line)) {
if (line.find("# dropshell-alias:" + alias) != std::string::npos) {
found = true;
lines.push_back(aliasLine);
} else {
lines.push_back(line);
}
}
infile.close();
if (!found) lines.push_back(aliasLine);
std::ofstream outfile(dropshelltool::DROPSHELL_RC_PATH, std::ios::trunc);
for (const auto& l : lines) outfile << l << "\n";
outfile.close();
}
void DropshellScriptManager::addAutocomplete(const std::string& toolName) {
ensureExists();
std::ifstream infile(dropshelltool::DROPSHELL_RC_PATH);
std::vector<std::string> lines;
std::string line;
std::string funcName = "_" + toolName + "_autocomplete";
std::string blockStart = "# DROPSHELL-AUTOCOMPLETE-START: " + toolName;
std::string blockEnd = "# DROPSHELL-AUTOCOMPLETE-END: " + toolName;
std::string funcDef;
funcDef += blockStart + "\n";
funcDef += funcName + "() {\n";
funcDef += " local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n";
funcDef += " mapfile -t completions < <(" + toolName + " autocomplete \"${COMP_WORDS[@]:1:${COMP_CWORD}-1}\")\n";
funcDef += " mapfile -t COMPREPLY < <(compgen -W \"${completions[*]}\" -- \"$cur\")\n";
funcDef += "}\n";
funcDef += blockEnd + "\n";
std::string completeLine = "complete -F " + funcName + " " + toolName + " # dropshell-autocomplete:" + toolName;
bool blockFound = false, completeFound = false;
bool inBlock = false;
while (std::getline(infile, line)) {
if (!inBlock && trim(line) == blockStart) {
blockFound = true;
inBlock = true;
continue;
}
if (inBlock) {
if (trim(line) == blockEnd) {
inBlock = false;
lines.push_back(funcDef); // Add new function block
}
continue; // Skip all lines in the old block
}
if (line.find("# dropshell-autocomplete:" + toolName) != std::string::npos) {
completeFound = true;
lines.push_back(completeLine);
} else {
lines.push_back(line);
}
}
infile.close();
if (!blockFound) lines.push_back(funcDef);
if (!completeFound) lines.push_back(completeLine);
std::ofstream outfile(dropshelltool::DROPSHELL_RC_PATH, std::ios::trunc);
for (const auto& l : lines) outfile << l << "\n";
outfile.close();
}
bool DropshellScriptManager::hasAlias(const std::string& alias) const {
ensureExists();
std::ifstream infile(dropshelltool::DROPSHELL_RC_PATH);
std::string line;
while (std::getline(infile, line)) {
if (line.find("# dropshell-alias:" + alias) != std::string::npos) {
return true;
}
}
return false;
}
std::vector<std::string> DropshellScriptManager::listAliases() const {
ensureExists();
std::ifstream infile(dropshelltool::DROPSHELL_RC_PATH);
std::string line;
std::vector<std::string> aliases;
while (std::getline(infile, line)) {
auto pos = line.find("# dropshell-alias:");
if (pos != std::string::npos) {
auto aliasStart = line.find("alias ");
if (aliasStart != std::string::npos) {
auto eq = line.find('=', aliasStart + 6);
if (eq != std::string::npos) {
std::string alias = line.substr(aliasStart + 6, eq - (aliasStart + 6));
aliases.push_back(alias);
}
}
}
}
return aliases;
}

View File

@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
class DropshellScriptManager {
public:
DropshellScriptManager();
void ensureExists() const;
void addToolEntry(const std::string& toolName, const std::string& toolDir);
void removeToolEntry(const std::string& toolName);
void addAlias(const std::string& alias, const std::string& toolName);
void addAutocomplete(const std::string& toolName);
bool hasAlias(const std::string& alias) const;
std::vector<std::string> listAliases() const;
};

172
getpkg/src/GetbinClient.cpp Normal file
View File

@ -0,0 +1,172 @@
#include "GetbinClient.hpp"
#include <drogon/HttpClient.h>
#include <drogon/utils/Utilities.h>
#include <fstream>
#include <sstream>
#include <nlohmann/json.hpp>
#include <string>
#include <iostream>
#include <thread>
#include <chrono>
#include <cstdio>
#include <map>
using json = nlohmann::json;
static constexpr const char* SERVER_HOST = "tools.dropshell.app";
GetbinClient::GetbinClient() {}
bool GetbinClient::download(const std::string& toolName, const std::string& arch, const std::string& outPath) {
auto client = drogon::HttpClient::newHttpClient("https://" + std::string(SERVER_HOST));
std::string object_path = "/object/" + toolName + ":" + arch;
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->setPath(object_path);
bool success = false;
client->sendRequest(req, [&](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
if (result != drogon::ReqResult::Ok || !response || response->getStatusCode() != drogon::k200OK) {
std::cerr << "[GetbinClient::download] HTTP request failed." << std::endl;
return;
}
std::ofstream ofs(outPath, std::ios::binary);
if (!ofs) return;
const auto& body = response->getBody();
ofs.write(body.data(), body.size());
success = ofs.good();
});
// Wait for the async request to complete
std::this_thread::sleep_for(std::chrono::milliseconds(100));
while (!success) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Add timeout logic here if needed
}
return success;
}
bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, std::string& outHash, const std::string& token) {
auto client = drogon::HttpClient::newHttpClient("https://" + std::string(SERVER_HOST));
// Read file
std::ifstream ifs(archivePath, std::ios::binary);
if (!ifs) {
std::cerr << "[GetbinClient::upload] Failed to open archive file: " << archivePath << std::endl;
return false;
}
std::string file_content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
// Compose metadata (minimal, can be extended)
json metadata = { {"labeltags", json::array()} };
// Try to extract tool:arch from filename
std::string filename = archivePath.substr(archivePath.find_last_of("/\\") + 1);
size_t dot = filename.find('.');
std::string labeltag = dot != std::string::npos ? filename.substr(0, dot) : filename;
metadata["labeltags"].push_back(labeltag);
// Create request with raw body upload (simplified approach)
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Put);
req->setPath("/upload");
req->addHeader("Authorization", "Bearer " + token);
req->addHeader("Content-Type", "application/gzip");
req->addHeader("X-Metadata", metadata.dump());
req->setBody(file_content);
bool success = false;
std::string response_body;
int status_code = 0;
client->sendRequest(req, [&](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
if (result != drogon::ReqResult::Ok || !response) {
std::cerr << "[GetbinClient::upload] HTTP /upload request failed (did not get OK response)." << std::endl;
if (response) {
std::cerr << response->getStatusCode() << std::endl;
std::cerr << response->getBody() << std::endl;
}
return;
}
status_code = static_cast<int>(response->getStatusCode());
response_body = response->getBody();
if (status_code != 200 && status_code != 201) {
std::cerr << "[GetbinClient::upload] HTTP error: status code " << status_code << std::endl;
std::cerr << "[GetbinClient::upload] Response body: " << response_body << std::endl;
return;
}
// Parse response for URL/hash
try {
auto resp_json = json::parse(response_body);
if (resp_json.contains("url")) outUrl = resp_json["url"].get<std::string>();
if (resp_json.contains("hash")) outHash = resp_json["hash"].get<std::string>();
success = true;
} catch (const std::exception& e) {
std::cerr << "[GetbinClient::upload] Failed to parse JSON response: " << e.what() << std::endl;
std::cerr << "[GetbinClient::upload] Response body: " << response_body << std::endl;
} catch (...) {
std::cerr << "[GetbinClient::upload] Unknown error while parsing JSON response." << std::endl;
std::cerr << "[GetbinClient::upload] Response body: " << response_body << std::endl;
}
});
// Wait for the async request to complete
std::this_thread::sleep_for(std::chrono::milliseconds(100));
while (!success && status_code == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Add timeout logic here if needed
}
return success;
}
bool GetbinClient::getHash(const std::string& toolName, const std::string& arch, std::string& outHash) {
auto client = drogon::HttpClient::newHttpClient("https://" + std::string(SERVER_HOST));
std::string exists_path = "/exists/" + toolName + ":" + arch;
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->setPath(exists_path);
bool success = false;
std::string response_body;
client->sendRequest(req, [&](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
if (result != drogon::ReqResult::Ok || !response || response->getStatusCode() != drogon::k200OK) {
return;
}
response_body = response->getBody();
// Try to parse hash from response body (assume plain text or JSON)
try {
// Try JSON
auto resp_json = json::parse(response_body);
if (resp_json.contains("hash")) {
outHash = resp_json["hash"].get<std::string>();
success = true;
return;
}
} catch (...) {
// Not JSON, treat as plain text
outHash = response_body;
success = !outHash.empty();
return;
}
});
// Wait for the async request to complete
std::this_thread::sleep_for(std::chrono::milliseconds(100));
while (!success && response_body.empty()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Add timeout logic here if needed
}
return success;
}

View File

@ -0,0 +1,10 @@
#pragma once
#include <string>
class GetbinClient {
public:
GetbinClient();
bool download(const std::string& toolName, const std::string& arch, const std::string& outPath);
bool upload(const std::string& archivePath, std::string& outUrl, std::string& outHash, const std::string& token);
bool getHash(const std::string& toolName, const std::string& arch, std::string& outHash);
};

245
getpkg/src/assert.hpp Normal file
View File

@ -0,0 +1,245 @@
#pragma once
#include <iostream>
#include <string_view>
#include <source_location>
#include <cxxabi.h>
// execinfo.h not available in Alpine Linux - using alternative approach
#include <dlfcn.h>
#include <cstring>
#include <memory>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <vector>
// 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<SourceInfo> get_source_info_batch(const char* executable, const std::vector<void*>& 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<SourceInfo>(addresses.size());
std::vector<SourceInfo> 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);
std::string func_name;
if (demangled) {
func_name = demangled;
free(demangled);
} else {
func_name = buffer;
}
// Simplify function signature: replace argument list with (...)
size_t paren_pos = func_name.find('(');
if (paren_pos != std::string::npos) {
func_name = func_name.substr(0, paren_pos) + "(...)";
}
results[current_frame].function = func_name;
// 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;
}
// Helper function to get the program counter (return address) in a portable way
inline void* get_pc() {
void* pc;
#if defined(__aarch64__) || defined(__arm__)
// For ARM/AArch64, we can use the link register (x30 on AArch64, r14 on ARM)
#if defined(__aarch64__)
asm volatile ("mov %0, x30" : "=r" (pc));
#else
asm volatile ("mov %0, lr" : "=r" (pc));
#endif
#else
// For x86/x86_64, use __builtin_return_address
pc = __builtin_return_address(0);
#endif
return pc;
}
void print_stacktrace() {
std::cerr << colors::yellow << "Stack trace:" << colors::reset << "\n";
char exe_path[1024] = {0};
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';
}
std::vector<void*> addresses;
// Get current frame pointer and return address
void* frame_ptr = __builtin_frame_address(0);
void* return_addr = get_pc();
// Add current return address
if (return_addr) {
addresses.push_back(return_addr);
}
// Try to walk up the stack if we have a valid frame pointer
if (frame_ptr) {
void** frame = static_cast<void**>(frame_ptr);
// Limit the number of frames to prevent potential issues
for (int i = 0; i < 5; ++i) {
// The return address is typically at frame[1]
if (frame[1] == nullptr) break;
addresses.push_back(frame[1]);
// Move to the next frame
if (frame[0] == nullptr) break;
frame = static_cast<void**>(frame[0]);
}
}
if (addresses.empty()) {
std::cerr << " " << colors::red << "[no frames available]" << colors::reset << "\n";
return;
}
// Get source info
std::vector<SourceInfo> results = get_source_info_batch(exe_path, addresses);
// Filter out frames from assert.hpp
results.erase(std::remove_if(results.begin(), results.end(),
[](const SourceInfo& frame) {
if (frame.file.empty()) return false;
std::string_view file(frame.file);
return file.find("assert.hpp") != std::string_view::npos;
}),
results.end()
);
if (results.empty()) {
std::cerr << " " << colors::red << "[no user frames available]" << colors::reset << "\n";
return;
}
// First pass: find the maximum function name length for alignment
size_t max_func_length = 0;
for (const auto& frame : results) {
size_t length = frame.function.empty()
? strlen("[unknown function]")
: frame.function.length();
if (length > max_func_length) {
max_func_length = length;
}
}
// Second pass: print with aligned output
for (size_t i = 0; i < results.size(); ++i) {
const auto& frame = results[i];
std::cerr << " ";
// Print function name with alignment
size_t current_length;
if (!frame.function.empty()) {
std::cerr << colors::green << frame.function << colors::reset;
current_length = frame.function.length();
} else {
std::cerr << colors::red << "[unknown function]" << colors::reset;
current_length = strlen("[unknown function]");
}
// Add padding to align the file/line text
if (!frame.file.empty() && frame.line > 0) {
// Calculate padding (minimum 1 space)
size_t padding = (max_func_length > current_length)
? (max_func_length - current_length + 1)
: 1;
std::cerr << std::string(padding, ' ')
<< " " << colors::blue << frame.file << colors::reset
<< ":" << colors::magenta << frame.line << colors::reset;
}
std::cerr << "\n";
}
}
void assert_failed(
bool condition,
std::string_view message,
std::source_location location
) {
if (!condition) {
std::cout << std::flush;
std::cerr << std::flush;
std::cerr << colors::red << "Assertion failed at " << location.file_name() << ":" << location.line() << ": "
<< location.function_name() << ": " << message << colors::reset << "\n";
print_stacktrace();
std::abort();
}
}
#define ASSERT(condition, message) assert_failed(condition, message, std::source_location::current())

View File

@ -0,0 +1,21 @@
#pragma once
/*
version.hpp is automatically generated by the build system, from version.hpp.in.
DO NOT EDIT VERSION.HPP!
*/
#include <string>
namespace dropshell {
// Version information
const std::string VERSION = "@PROJECT_VERSION@";
const std::string RELEASE_DATE = "@RELEASE_DATE@";
const std::string AUTHOR = "j842";
const std::string LICENSE = "MIT";
} // namespace dropshell

327
getpkg/src/main.cpp Normal file
View File

@ -0,0 +1,327 @@
/*
dropshell-tool
dropshell-tool install <tool_name>
- confirms dropshell-tool is fully initialised:
- adds a line to the user's .bashrc file to source the dropshell-tool ~/.bashrc_dropshell_tool script, if it doesn't exist
- creates an empty ~/.bashrc_dropshell_tool script if it doesn't exist
- creates the ~/.config/dropshell-tool directory, if it does not exist
- creates the ~/.local/bin/dropshell-tool directory, if it does not exist
- removes the tool from the user's system if it is already installed, and the tool's entries in the ~/.bashrc_dropshell_tool script
- downloads the tool archive (tgz) from tools.dropshell.app (tool_name:ARCH), where ARCH is the architecture of the user's system, and unpacks it to the new tool directory: ~/.local/bin/dropshell-tool/<tool_name>/
- sets the PATH to include the tool directory, by modifying the ~/.bashrc_dropshell_tool script
- adds an entry for autocompletion for the tool to the ~/.bashrc_dropshell_tool script, where autocomplete just runs <tool_name> autocomplete <args>
- creates a ~/.config/dropshell-tool/tool_name.json file, which contains the tool's name, version (by running <tool_name> version), hash from tools.dropshell.app, and architecture
- reads the json file from the tgz called dropshell-tool-config.json:
- for each alias in the aliases array:
- check if another command is using the alias, and continue only if not
- check that the alias is not already in the ~/.bashrc_dropshell_tool script and continue only if not
- add an entry to the ~/.bashrc_dropshell_tool script to run the tool via the alias
- if the json file has a setup_script entry, run the script named in the entry in the tool directory - using sudo if the entry has sudo set to true.
dropshell-tool publish <tool_name:ARCH> <folder>
- checks that dropshell-tool-config.json exists in the folder, and is valid (see above)
- creates a tgz archive of the folder, and uploads it to tools.dropshell.app, a simple object server.
- prints the URL and hash of the uploaded archive
- uses the token from env variable SOS_WRITE_TOKEN to write to tools.dropshell.app
dropshell-tool update <tool_name>
- compares the hash from the ~/.config/dropshell-tool/tool_name.json file with the hash from tools.dropshell.app (tool_name:ARCH), and continues only if they are different
- checks the version from the ~/.config/dropshell-tool/tool_name.json file with the version from tools.dropshell.app (tool_name:ARCH), and continues only if the remote version is newer (installed is older)
- installs the tool as per the install command
dropshell-tool update all
- runs update on all installed tools
dropshell-tool autocomplete <args>
- shows autocomplete for dropshell-tool, and then exits
- the tool list to choose from when calling install is hard coded in the autocomplete function
dropshell-tool version
- prints the version of dropshell-tool
dropshell-tool create <tool_name> <directory_name>
- creates a new tool source directory in relative path <directory_name> if it doesn't exist
- creates a dropshell-tool-config.json file in the tool source directory if it doesn't exist, with the following entries:
- aliases: an array of aliases for the tool
- setup_script: the name of the setup script to run (setup_script.sh)
- creates a setup_script.sh file in the tool source directory if it doesn't exist, that just prints a completion message and exits
dropshell-tool help
- shows this help message
*/
#include "version.hpp"
#include "BashrcEditor.hpp"
#include "DropshellScriptManager.hpp"
#include "GetbinClient.hpp"
#include "ArchiveManager.hpp"
#include <iostream>
#include <string>
#include <vector>
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
namespace {
using json = nlohmann::json;
std::string get_arch() {
#if defined(__x86_64__) || defined(_M_X64)
return "x86_64";
#elif defined(__aarch64__)
return "aarch64";
#else
return "unknown";
#endif
}
std::string get_home() {
const char* home = getenv("HOME");
return home ? std::string(home) : std::string("");
}
int install_tool(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: dropshell-tool install <tool_name>" << std::endl;
return 1;
}
std::string toolName = argv[2];
std::string arch = get_arch();
std::string home = get_home();
std::filesystem::path configDir = std::filesystem::path(home) / ".config/dropshell-tool";
std::filesystem::path binDir = std::filesystem::path(home) / ".local/bin/dropshell-tool" / toolName;
std::filesystem::path archivePath = configDir / (toolName + ".tgz");
std::filesystem::create_directories(configDir);
std::filesystem::create_directories(binDir);
dropshelltool::BashrcEditor bashrcEditor;
bashrcEditor.addSourceLine();
DropshellScriptManager scriptManager;
scriptManager.removeToolEntry(toolName);
if (std::filesystem::exists(binDir)) std::filesystem::remove_all(binDir);
GetbinClient getbin;
std::cout << "Downloading " << toolName << ":" << arch << "..." << std::endl;
if (!getbin.download(toolName, arch, archivePath.string())) {
std::cerr << "Failed to download tool archive." << std::endl;
return 1;
}
ArchiveManager archiver;
if (!archiver.unpack(archivePath.string(), binDir.string())) {
std::cerr << "Failed to unpack tool archive." << std::endl;
return 1;
}
scriptManager.addToolEntry(toolName, binDir.string());
scriptManager.addAutocomplete(toolName);
std::string hash;
getbin.getHash(toolName, arch, hash);
std::string version;
std::string toolPath = binDir.string() + "/" + toolName;
FILE* fp = popen((toolPath + " version").c_str(), "r");
if (fp) {
char buf[128];
if (fgets(buf, sizeof(buf), fp)) version = std::string(buf);
pclose(fp);
}
json toolInfo = {
{"name", toolName},
{"version", version},
{"hash", hash},
{"arch", arch}
};
std::ofstream toolInfoFile(configDir / (toolName + ".json"));
toolInfoFile << toolInfo.dump(2);
toolInfoFile.close();
std::string configJson;
if (archiver.readConfigJson(archivePath.string(), configJson)) {
try {
auto config = json::parse(configJson);
if (config.contains("aliases")) {
for (const auto& alias : config["aliases"]) {
std::string aliasStr = alias.get<std::string>();
if (!scriptManager.hasAlias(aliasStr)) {
scriptManager.addAlias(aliasStr, toolName);
}
}
}
if (config.contains("setup_script")) {
std::string setupScript = config["setup_script"].get<std::string>();
bool useSudo = config.value("sudo", false);
std::string setupPath = binDir.string() + "/" + setupScript;
std::string cmd = (useSudo ? "sudo " : "") + setupPath;
std::system(cmd.c_str());
}
} catch (...) {
std::cerr << "Warning: failed to parse dropshell-tool-config.json" << std::endl;
}
}
std::cout << "Installed " << toolName << " successfully." << std::endl;
return 0;
}
int publish_tool(int argc, char* argv[]) {
if (argc < 4) {
std::cerr << "Usage: dropshell-tool publish <tool_name:ARCH> <folder>" << std::endl;
return 1;
}
std::string labeltag = argv[2];
std::string folder = argv[3];
std::string home = get_home();
std::filesystem::path configPath = std::filesystem::path(folder) / "dropshell-tool-config.json";
if (!std::filesystem::exists(configPath)) {
std::cerr << "dropshell-tool-config.json not found in " << folder << std::endl;
return 1;
}
std::filesystem::path archivePath = std::filesystem::path(home) / ".tmp" / (labeltag + ".tgz");
std::filesystem::create_directories(archivePath.parent_path());
ArchiveManager archiver;
if (!archiver.pack(folder, archivePath.string())) {
std::cerr << "Failed to create archive." << std::endl;
return 1;
}
std::filesystem::path tokenPath = std::filesystem::path(home) / ".config/tools.dropshell.app/write_token.txt";
std::string token;
if (std::filesystem::exists(tokenPath)) {
std::ifstream tfile(tokenPath);
std::getline(tfile, token);
} else {
std::cout << "Enter tools.dropshell.app write token: ";
std::getline(std::cin, token);
std::filesystem::create_directories(tokenPath.parent_path());
std::ofstream tfile(tokenPath);
tfile << token << std::endl;
}
GetbinClient getbin;
std::string url, hash;
if (!getbin.upload(archivePath.string(), url, hash, token)) {
std::cerr << "Failed to upload archive." << std::endl;
return 1;
}
std::cout << "Published! URL: " << url << "\nHash: " << hash << std::endl;
return 0;
}
int update_tool(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: dropshell-tool update <tool_name|all>" << std::endl;
return 1;
}
std::string toolName = argv[2];
std::string home = get_home();
std::filesystem::path configDir = std::filesystem::path(home) / ".config/dropshell-tool";
if (toolName == "all") {
for (const auto& entry : std::filesystem::directory_iterator(configDir)) {
if (entry.path().extension() == ".json") {
std::string tname = entry.path().stem();
char* fakeArgv[] = {argv[0], (char*)"update", (char*)tname.c_str()};
update_tool(3, fakeArgv);
}
}
return 0;
}
std::filesystem::path toolInfoPath = configDir / (toolName + ".json");
if (!std::filesystem::exists(toolInfoPath)) {
std::cerr << "Tool not installed: " << toolName << std::endl;
return 1;
}
std::ifstream tfile(toolInfoPath);
json toolInfo;
tfile >> toolInfo;
tfile.close();
std::string arch = toolInfo.value("arch", get_arch());
std::string localHash = toolInfo.value("hash", "");
std::string localVersion = toolInfo.value("version", "");
GetbinClient getbin;
std::string remoteHash;
getbin.getHash(toolName, arch, remoteHash);
if (remoteHash.empty() || remoteHash == localHash) {
std::cout << "No update needed for " << toolName << std::endl;
return 0;
}
std::cout << "Updating " << toolName << "..." << std::endl;
char* fakeArgv[] = {argv[0], (char*)"install", (char*)toolName.c_str()};
return install_tool(3, fakeArgv);
}
int create_tool(int argc, char* argv[]) {
if (argc < 4) {
std::cerr << "Usage: dropshell-tool create <tool_name> <directory_name>" << std::endl;
return 1;
}
std::string toolName = argv[2];
std::string directoryName = argv[3];
std::filesystem::path toolDir = std::filesystem::current_path() / directoryName;
if (!std::filesystem::exists(toolDir)) {
std::filesystem::create_directories(toolDir);
std::cout << "Created directory: " << toolDir << std::endl;
} else {
std::cout << "Directory already exists: " << toolDir << std::endl;
}
std::filesystem::path configPath = toolDir / "dropshell-tool-config.json";
if (!std::filesystem::exists(configPath)) {
nlohmann::json config = {
{"aliases", nlohmann::json::array()},
{"setup_script", "setup_script.sh"}
};
std::ofstream configFile(configPath);
configFile << config.dump(2);
configFile.close();
std::cout << "Created config: " << configPath << std::endl;
} else {
std::cout << "Config already exists: " << configPath << std::endl;
}
std::filesystem::path setupScriptPath = toolDir / "setup_script.sh";
if (!std::filesystem::exists(setupScriptPath)) {
std::ofstream setupFile(setupScriptPath);
setupFile << "#!/bin/bash\necho 'Setup complete.'\n";
setupFile.close();
std::filesystem::permissions(setupScriptPath, std::filesystem::perms::owner_exec | std::filesystem::perms::owner_write | std::filesystem::perms::owner_read);
std::cout << "Created setup script: " << setupScriptPath << std::endl;
} else {
std::cout << "Setup script already exists: " << setupScriptPath << std::endl;
}
return 0;
}
} // end anonymous namespace
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "Usage: dropshell-tool <command> [args...]" << std::endl;
return 1;
}
std::string command = argv[1];
if (command == "install") {
return install_tool(argc, argv);
} else if (command == "publish") {
return publish_tool(argc, argv);
} else if (command == "update") {
return update_tool(argc, argv);
} else if (command == "autocomplete") {
std::vector<std::string> args(argv + 2, argv + argc);
if (args.empty()) std::cout << R"(install
publish
update
version
create
help
)";
} else if (command == "version") {
std::cout << dropshell::VERSION << std::endl;
} else if (command == "create") {
return create_tool(argc, argv);
} else if (command == "help") {
std::cout << "Usage: dropshell-tool <command> [args...]" << std::endl;
std::cout << "Commands:" << std::endl;
std::cout << " install <tool_name>" << std::endl;
std::cout << " publish <tool_name:ARCH> <folder>" << std::endl;
std::cout << " update <tool_name>" << std::endl;
std::cout << " version" << std::endl;
} else {
std::cout << "Unknown command: " << command << std::endl;
return 1;
}
return 0;
}

21
getpkg/src/version.hpp.in Normal file
View File

@ -0,0 +1,21 @@
#pragma once
/*
version.hpp is automatically generated by the build system, from version.hpp.in.
DO NOT EDIT VERSION.HPP!
*/
#include <string>
namespace dropshell {
// Version information
const std::string VERSION = "@PROJECT_VERSION@";
const std::string RELEASE_DATE = "@RELEASE_DATE@";
const std::string AUTHOR = "j842";
const std::string LICENSE = "MIT";
} // namespace dropshell

9
getpkg/test.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
VERSION=$("${SCRIPT_DIR}/output/dropshell-tool" version)
echo "Version: $VERSION"