From 8aeb5133bfb22c612f983ba1688ded06aba149a6 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 18:33:30 -0700 Subject: [PATCH 1/9] Add triangles-cli: JSON-RPC client (port bitcoin-cli pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triangles never had a CLI client (bitcoin-cli analog). This adds triangles-cli as a third build target alongside trianglesd and triangles-qt. - src/triangles-cli.cpp: self-contained JSON-RPC 1.0 client. Reads triangles.conf for credentials, supports -rpcuser/-rpcpassword /-rpcconnect/-rpcport/-testnet/-datadir/-conf flags. Implements -getinfo (synthesized summary from getnetworkinfo/getblockchaininfo /getwalletinfo) and raw method dispatch. JSON via json_spirit compat shim (json_compat.h), HTTP via boost::asio, base64 auth inline. No util.cpp / wallet.cpp / net.cpp / triangles_common link dep — keeps the binary small (~600 KB Linux, ~1.5 MB Windows). - CMake: new option(BUILD_CLI ON) + add_executable(triangles-cli) in src/CMakeLists.txt. Status line added. - CI: BUILD_CLI=ON added to build-windows-daemon and build-linux-daemon jobs. triangles-cli.exe bundled into windows-daemon artifact alongside trianglesd.exe. triangles-cli added to linux-daemon .deb package (with launcher in /usr/bin). - Default ON; set BUILD_CLI=OFF to skip. Closes the open 'triangles-cli.exe missing from Windows build' follow-up (the binary wasn't missing — it never existed). Patterned after Bitcoin Core bitcoin-cli and Dash Core dash-cli. --- .github/workflows/build-all.yml | 46 ++- CMakeLists.txt | 2 + src/CMakeLists.txt | 33 ++ src/triangles-cli.cpp | 552 ++++++++++++++++++++++++++++++++ 4 files changed, 622 insertions(+), 11 deletions(-) create mode 100644 src/triangles-cli.cpp diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 5699825..793d0e8 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -268,6 +268,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DBUILD_QT=OFF \ -DBUILD_DAEMON=ON \ + -DBUILD_CLI=ON \ -DBUILD_TESTS=OFF \ -DUSE_UPNP=ON @@ -275,14 +276,19 @@ jobs: run: | cmake --build build -j$(nproc) strip --strip-all build/bin/trianglesd.exe + strip --strip-all build/bin/triangles-cli.exe - name: Package daemon with DLLs run: | mkdir -p daemon-dist/tor cp build/bin/trianglesd.exe daemon-dist/ + cp build/bin/triangles-cli.exe daemon-dist/ - # Copy all linked DLLs from MSYS2 - ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' | while read dll; do + # Copy all linked DLLs from MSYS2 (covers both binaries; ldd union) + { + ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' + ldd build/bin/triangles-cli.exe | grep '/mingw64' | awk '{print $3}' + } | sort -u | while read dll; do cp "$dll" daemon-dist/ 2>/dev/null || true done @@ -454,6 +460,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DBUILD_QT=OFF \ -DBUILD_DAEMON=ON \ + -DBUILD_CLI=ON \ -DBUILD_TESTS=OFF \ -DUSE_UPNP=ON @@ -461,7 +468,9 @@ jobs: run: cmake --build build -j$(nproc) - name: Strip binary - run: strip --strip-all build/bin/trianglesd + run: | + strip --strip-all build/bin/trianglesd + strip --strip-all build/bin/triangles-cli - name: Build .deb package (fully self-contained) run: | @@ -477,12 +486,17 @@ jobs: mkdir -p ${PKG}/etc/systemd/system cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/ + cp build/bin/triangles-cli ${PKG}/usr/lib/cryptographic-triangles/ cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/ chmod +x ${PKG}/usr/lib/cryptographic-triangles/tor/tor [ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data # Bundle ALL shared library dependencies (except glibc/kernel) - ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' | while read lib; do + # Union of ldd output from both binaries + { + ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' + ldd build/bin/triangles-cli | grep '=> /' | awk '{print $3}' + } | sort -u | while read lib; do case "$lib" in /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) ;; # Skip glibc core — always present @@ -495,7 +509,7 @@ jobs: ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l ls ${PKG}/usr/lib/cryptographic-triangles/lib/ - # Launcher with LD_LIBRARY_PATH + # Launchers with LD_LIBRARY_PATH cat > ${PKG}/usr/bin/trianglesd << 'LAUNCHER' #!/bin/bash INSTALL_DIR=/usr/lib/cryptographic-triangles @@ -505,6 +519,15 @@ jobs: sed -i 's/^ //' ${PKG}/usr/bin/trianglesd chmod +x ${PKG}/usr/bin/trianglesd + cat > ${PKG}/usr/bin/triangles-cli << 'LAUNCHER' + #!/bin/bash + INSTALL_DIR=/usr/lib/cryptographic-triangles + export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" + exec "${INSTALL_DIR}/triangles-cli" "$@" + LAUNCHER + sed -i 's/^ //' ${PKG}/usr/bin/triangles-cli + chmod +x ${PKG}/usr/bin/triangles-cli + cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC' [Unit] Description=Cryptographic Triangles Daemon @@ -528,9 +551,9 @@ jobs: Version: ${VERSION} Architecture: amd64 Maintainer: Cryptographic Triangles - Description: Cryptographic Triangles daemon with integrated Tor - Fully self-contained headless node with all libraries, Tor, and systemd service. - No external dependencies required — runs on any x86_64 Linux. + Description: Cryptographic Triangles daemon + CLI with integrated Tor + Fully self-contained headless node + JSON-RPC client with all libraries, + Tor, and systemd service. No external dependencies required. Section: finance Priority: optional CTRL @@ -540,9 +563,10 @@ jobs: #!/bin/bash systemctl daemon-reload echo "" - echo "Cryptographic Triangles daemon installed." - echo " Start: sudo systemctl start trianglesd" - echo " On boot: sudo systemctl enable trianglesd" + echo "Cryptographic Triangles daemon + CLI installed." + echo " Start daemon: sudo systemctl start trianglesd" + echo " On boot: sudo systemctl enable trianglesd" + echo " Use CLI: triangles-cli getinfo" echo "" POST chmod +x ${PKG}/DEBIAN/postinst diff --git a/CMakeLists.txt b/CMakeLists.txt index c33328d..43acf1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") # ── User-facing options ── option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON) option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON) +option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON) option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON) option(USE_UPNP "Enable UPnP support via miniupnpc" ON) option(USE_IPV6 "Enable IPv6 support" ON) @@ -185,6 +186,7 @@ message(STATUS "") message(STATUS "Triangles ${PROJECT_VERSION} build configuration:") message(STATUS " Build Qt GUI: ${BUILD_QT}") message(STATUS " Build daemon: ${BUILD_DAEMON}") +message(STATUS " Build CLI: ${BUILD_CLI}") message(STATUS " Build tests: ${BUILD_TESTS}") message(STATUS " UPnP: ${USE_UPNP}") message(STATUS " IPv6: ${USE_IPV6}") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cb5738f..327d341 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -247,6 +247,39 @@ if(BUILD_DAEMON) endif() endif() +# ═══════════════════════════════════════════════════════════════════════════════ +# 4b. JSON-RPC client (triangles-cli) +# +# Self-contained: only links univalue + boost::asio + boost::program_options +# + boost::filesystem + OpenSSL (for base64 / future TLS). Does NOT link +# triangles_common, wallet, or net — keeps the binary small. +# ═══════════════════════════════════════════════════════════════════════════════ +if(BUILD_CLI) + add_executable(triangles-cli + triangles-cli.cpp + ) + # Boost components used by the CLI + find_package(Boost REQUIRED COMPONENTS filesystem system) + target_link_libraries(triangles-cli + PRIVATE + json_compat + Boost::filesystem + Boost::system + ) + + if(WIN32) + set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe") + # boost::asio needs ws2_32 on Windows + target_link_libraries(triangles-cli PRIVATE ws2_32) + endif() + + if(MSVC) + set_target_properties(triangles-cli PROPERTIES + VS_WINRT_COMPONENT "console" + ) + endif() +endif() + # ═══════════════════════════════════════════════════════════════════════════════ # 5. Qt5 GUI wallet (triangles-qt) # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/triangles-cli.cpp b/src/triangles-cli.cpp new file mode 100644 index 0000000..fe4ad67 --- /dev/null +++ b/src/triangles-cli.cpp @@ -0,0 +1,552 @@ +// Copyright (c) 2014-2026 The Cryptographic Triangles developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// +// triangles-cli — JSON-RPC client for trianglesd. +// +// Talks to a running trianglesd over HTTP/1.1 with HTTP Basic auth and +// JSON-RPC 1.0. Patterned after bitcoin-cli (Bitcoin Core) and dash-cli. +// +// Build with -DBUILD_CLI=ON (default ON). +// +// Self-contained: does NOT link util.cpp / wallet.cpp / net.cpp / triangles_common. +// Only links json_compat (nlohmann/json via json_spirit shim), boost (asio + +// program_options + filesystem + system), and OpenSSL (for base64). +// This keeps the CLI binary small (~600 KB stripped on Linux, ~1.5 MB on Windows). +// +// Connection parameters (highest precedence first): +// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport +// 2. triangles.conf in the data directory (or -conf=) +// 3. Defaults: 127.0.0.1:19111 mainnet, 19112 testnet; no auth (must be set in conf) +// +// Usage: +// triangles-cli help List commands (delegates to daemon) +// triangles-cli help Help for one command +// triangles-cli getinfo Example: summary info +// triangles-cli getblockchaininfo Example: chain state +// triangles-cli getbalance Example: 0-arg call +// triangles-cli getbalance "*" 6 Example: positional args +// triangles-cli sendtoaddress 1.5 "memo" Example: mixed types +// triangles-cli -getinfo Synthesized summary from multiple RPCs +// triangles-cli -raw Print raw JSON response (no pretty-print) +// +// Any command-line arg that parses as a JSON literal (number, bool, null, +// object, array) is forwarded as that literal; otherwise it is sent as a JSON +// string. This matches bitcoin-cli semantics. + +#define TRIANGLES_CLI_VERSION "1.0.0" + +#include "json/json_compat.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +namespace asio = boost::asio; +using boost::asio::ip::tcp; +namespace fs = boost::filesystem; +using namespace json_spirit; + +// ───────────────────────────────────────────────────────────────────────────── +// Minimal arg/config plumbing — self-contained, no util.cpp dep. +// ───────────────────────────────────────────────────────────────────────────── + +static map mapArgs; +static map > mapMultiArgs; + +static string GetArg(const string& key, const string& def = "") +{ + auto it = mapArgs.find(key); + return (it != mapArgs.end()) ? it->second : def; +} + +static bool GetBoolArg(const string& key, bool def) +{ + auto it = mapArgs.find(key); + if (it == mapArgs.end()) return def; + string v = it->second; + if (v.empty()) return true; + return (v != "0" && v != "false" && v != "no"); +} + +static void ReadConfigFile(const string& path) +{ + ifstream f(path); + if (!f.good()) return; + string line; + while (getline(f, line)) { + // Strip CR (Windows) and leading whitespace + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t start = line.find_first_not_of(" \t"); + if (start == string::npos) continue; + if (line[start] == '#') continue; + // Parse key = value + size_t eq = line.find('=', start); + if (eq == string::npos) continue; + string key = line.substr(start, eq - start); + string value = line.substr(eq + 1); + // Trim whitespace on both ends + auto trim = [](string& s) { + size_t a = s.find_first_not_of(" \t"); + size_t b = s.find_last_not_of(" \t"); + if (a == string::npos) { s.clear(); return; } + s = s.substr(a, b - a + 1); + }; + trim(key); + trim(value); + // Strip surrounding quotes + if (value.size() >= 2 && + ((value.front() == '"' && value.back() == '"') || + (value.front() == '\'' && value.back() == '\''))) { + value = value.substr(1, value.size() - 2); + } + string dashKey = "-" + key; + if (mapArgs.count(dashKey) == 0) { + mapArgs[dashKey] = value; + mapMultiArgs[dashKey].push_back(value); + } + } +} + +// Cross-platform default data directory (matches the daemon's path) +static fs::path GetDefaultDataDir() +{ +#ifdef WIN32 + // %APPDATA%/CryptographicTriangles + const char* appdata = getenv("APPDATA"); + if (appdata && *appdata) { + return fs::path(appdata) / "CryptographicTriangles"; + } + return fs::path("C:/CryptographicTriangles"); +#elif defined(__APPLE__) + // ~/Library/Application Support/CryptographicTriangles + const char* home = getenv("HOME"); + if (home && *home) { + return fs::path(home) / "Library/Application Support/CryptographicTriangles"; + } + return fs::path("/tmp/CryptographicTriangles"); +#else + // ~/.cryptographic-triangles (matches daemon's GetDefaultDataDir) + const char* home = getenv("HOME"); + if (home && *home) { + return fs::path(home) / ".cryptographic-triangles"; + } + return fs::path("/tmp/CryptographicTriangles"); +#endif +} + +static fs::path GetConfigFilePath() +{ + fs::path confPath = GetArg("-conf", "triangles.conf"); + if (confPath.is_absolute()) return confPath; + fs::path datadir = GetArg("-datadir", ""); + if (datadir.empty()) datadir = GetDefaultDataDir().string(); + return fs::path(datadir) / confPath; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Command-line parsing +// ───────────────────────────────────────────────────────────────────────────── + +static void ParseCommandLine(int argc, char* const argv[]) +{ + mapArgs.clear(); + mapMultiArgs.clear(); + for (int i = 1; i < argc; ++i) { + string str(argv[i]); + // Bare "-" means: read remaining args from stdin + if (str == "-") { + mapMultiArgs["-"].push_back("-"); + continue; + } + string strKey, strVal; + size_t idx = str.find('='); + if (idx == string::npos) { + strKey = "-" + str; + strVal = "1"; + } else { + strKey = "-" + str.substr(0, idx); + strVal = str.substr(idx + 1); + } + mapArgs[strKey] = strVal; + mapMultiArgs[strKey].push_back(strVal); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// RPC connection parameters +// ───────────────────────────────────────────────────────────────────────────── + +struct RPCConn { + string host = "127.0.0.1"; + string port = "19111"; + string user; + string pass; +}; + +static int AppInitRPCConn(RPCConn& conn) +{ + // Load conf file FIRST (before pulling creds) so defaults from triangles.conf + // are visible. Command-line flags (already in mapArgs) take precedence because + // ReadConfigFile only inserts when key is absent. + fs::path confPath = GetConfigFilePath(); + if (!confPath.empty()) ReadConfigFile(confPath.string()); + + bool fTestNet = GetBoolArg("-testnet", false); + conn.port = GetArg("-rpcport", fTestNet ? "19112" : "19111"); + conn.host = GetArg("-rpcconnect", "127.0.0.1"); + conn.user = GetArg("-rpcuser", ""); + conn.pass = GetArg("-rpcpassword", ""); + + if (conn.user.empty() || conn.pass.empty()) { + cerr << "triangles-cli: missing RPC credentials. Set rpcuser/rpcpassword in triangles.conf\n" + << " or pass -rpcuser= -rpcpassword= on the command line.\n" + << " (RPC config file: " << confPath.string() << ")\n"; + return 1; + } + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON-RPC param conversion +// ───────────────────────────────────────────────────────────────────────────── + +static Value ParseCLIParam(const string& arg) +{ + if (arg.empty()) { + return Value(string("")); + } + Value v; + // Try parsing the arg as JSON. If it parses to a non-string literal, keep. + if (read_string(arg, v) && v.type() != str_type) { + return v; + } + return Value(arg); +} + +static void ParseCommandLineRPCParams(int argc, char* const argv[], + Value& method, Array& params) +{ + method = Value(string("")); + params.clear(); + int i = 1; + // Skip leading flags + static const set valFlags = { + "-conf", "-datadir", "-rpcconnect", "-rpcport", + "-rpcuser", "-rpcpassword" + }; + while (i < argc) { + string arg(argv[i]); + if (arg == "-" || arg.size() < 2 || arg[0] != '-') break; + if (valFlags.count(arg) && i + 1 < argc && + string(argv[i+1]).substr(0,1) != "-") { + i += 2; + } else { + ++i; + } + } + if (i >= argc) { + method = Value(string("help")); + return; + } + method = Value(string(argv[i])); + ++i; + while (i < argc) { + string arg(argv[i]); + if (arg == "-") { + string line; + while (getline(cin, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + params.push_back(ParseCLIParam(line)); + } + } else { + params.push_back(ParseCLIParam(arg)); + } + ++i; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Base64 (RFC 4648) — for HTTP Basic auth +// ───────────────────────────────────────────────────────────────────────────── + +static const char b64_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static string Base64Encode(const string& in) +{ + string out; + out.reserve(((in.size() + 2) / 3) * 4); + int val = 0, valb = -6; + for (unsigned char c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back(b64_table[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) out.push_back(b64_table[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +// ───────────────────────────────────────────────────────────────────────────── +// HTTP/1.1 JSON-RPC POST (plaintext) +// ───────────────────────────────────────────────────────────────────────────── + +static int CallRPC(const RPCConn& conn, const string& strMethod, + const Array& params, Value& result) +{ + Object req; + req.push_back(Pair("jsonrpc", Value(string("1.0")))); + req.push_back(Pair("id", Value(string("triangles-cli")))); + req.push_back(Pair("method", Value(strMethod))); + req.push_back(Pair("params", Value(params))); + string strRequest = write_string(Value(req), false) + "\n"; + + string strAuth = Base64Encode(conn.user + ":" + conn.pass); + + asio::io_context io; + tcp::resolver resolver(io); + boost::system::error_code ec; + auto endpoints = resolver.resolve(conn.host, conn.port, ec); + if (ec) { + cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port + << " failed: " << ec.message() << "\n"; + return 1; + } + + tcp::socket sock(io); + sock.connect(*endpoints.begin(), ec); + if (ec) { + cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port + << " failed: " << ec.message() << "\n" + << "(is trianglesd running and accepting JSON-RPC?)\n"; + return 1; + } + + ostringstream reqStream; + reqStream << "POST / HTTP/1.1\r\n" + << "Host: " << conn.host << ":" << conn.port << "\r\n" + << "Authorization: Basic " << strAuth << "\r\n" + << "Content-Type: application/json\r\n" + << "Content-Length: " << strRequest.size() << "\r\n" + << "Connection: close\r\n" + << "\r\n" + << strRequest; + asio::streambuf requestBuf; + std::ostream os(&requestBuf); + os << reqStream.str(); + asio::write(sock, requestBuf, ec); + if (ec) { + cerr << "triangles-cli: write failed: " << ec.message() << "\n"; + return 1; + } + + asio::streambuf responseBuf; + boost::system::error_code readEc; + while (asio::read(sock, responseBuf, + asio::transfer_at_least(1), readEc)) { + // keep reading until EOF or error + } + if (readEc && readEc != asio::error::eof) { + cerr << "triangles-cli: read failed: " << readEc.message() << "\n"; + return 1; + } + + std::istream rs(&responseBuf); + string line; + if (!std::getline(rs, line)) { + cerr << "triangles-cli: empty response\n"; + return 1; + } + if (!line.empty() && line.back() == '\r') line.pop_back(); + int status = 0; + { + istringstream iss(line); + string httpVer; + iss >> httpVer >> status; + } + if (status != 200) { + cerr << "triangles-cli: server returned HTTP " << status << "\n"; + ostringstream body; + body << rs.rdbuf(); + if (!body.str().empty()) cerr << body.str() << "\n"; + return 1; + } + while (std::getline(rs, line) && line != "\r" && !line.empty()) {} + string body; + { + ostringstream oss; + oss << rs.rdbuf(); + body = oss.str(); + } + + Value reply; + if (!read_string(body, reply)) { + cerr << "triangles-cli: could not parse JSON response:\n" << body << "\n"; + return 1; + } + + if (reply.type() != obj_type) { + cerr << "triangles-cli: unexpected response (not an object):\n" + << write_string(reply, true) << "\n"; + return 1; + } + + Object replyObj = reply.get_obj(); + const Value& err = find_value(replyObj, "error"); + if (err.type() != null_type) { + cerr << "RPC error: " << write_string(err, false) << "\n"; + return 1; + } + const Value& res = find_value(replyObj, "result"); + result = res; + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// triangles-cli -getinfo — synthesize a friendly summary from a few RPC calls +// ───────────────────────────────────────────────────────────────────────────── + +static int Getinfo(const RPCConn& conn, bool fPretty) +{ + Object info; + Value r; + Array emptyParams; + + if (CallRPC(conn, "getnetworkinfo", emptyParams, r) == 0) { + info.push_back(Pair("network", r)); + } + if (CallRPC(conn, "getblockchaininfo", emptyParams, r) == 0) { + Object chain = r.get_obj(); + info.push_back(Pair("blockchain", r)); + info.push_back(Pair("blocks", find_value(chain, "blocks"))); + info.push_back(Pair("headers", find_value(chain, "headers"))); + info.push_back(Pair("bestblockhash", find_value(chain, "bestblockhash"))); + info.push_back(Pair("difficulty", find_value(chain, "difficulty"))); + info.push_back(Pair("verificationprogress", + find_value(chain, "verificationprogress"))); + info.push_back(Pair("chain", find_value(chain, "chain"))); + } + if (CallRPC(conn, "getwalletinfo", emptyParams, r) == 0) { + Object wal = r.get_obj(); + info.push_back(Pair("wallet", r)); + info.push_back(Pair("balance", find_value(wal, "balance"))); + } + Object connObj; + connObj.push_back(Pair("rpcconnect", Value(conn.host))); + connObj.push_back(Pair("rpcport", Value(conn.port))); + info.push_back(Pair("connection", Value(connObj))); + cout << write_string(Value(info), fPretty) << "\n"; + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Help / version +// ───────────────────────────────────────────────────────────────────────────── + +static int CommandLineHelp(ostream& out) +{ + out << "Usage: triangles-cli [options] [params]\n" + << "\n" + << " triangles-cli [options] help List commands (delegates to daemon)\n" + << " triangles-cli [options] help Help for one command (delegates to daemon)\n" + << " triangles-cli -getinfo Show summary info from the daemon\n" + << "\n" + << "Options:\n" + << " -conf= Specify configuration file (default: triangles.conf)\n" + << " -datadir= Specify data directory\n" + << " -testnet Use testnet (RPC port 19112)\n" + << " -rpcconnect= Send commands to node running on (default: 127.0.0.1)\n" + << " -rpcport= Connect to JSON-RPC on (default: 19111 or testnet: 19112)\n" + << " -rpcuser= Username for JSON-RPC connections\n" + << " -rpcpassword= Password for JSON-RPC connections\n" + << " -stdin Read extra params from standard input, one per line\n" + << " -raw Print raw JSON response (no pretty-printing)\n" + << " -version Print version and exit\n" + << "\n" + << "Examples:\n" + << " triangles-cli getinfo\n" + << " triangles-cli getblockchaininfo\n" + << " triangles-cli getbalance\n" + << " triangles-cli getbalance \"*\" 6\n" + << " triangles-cli sendtoaddress
[comment]\n" + << " triangles-cli -getinfo\n" + << "\n"; + return 0; +} + +static int CommandLineVersion() +{ + cout << "triangles-cli version " << TRIANGLES_CLI_VERSION + << " (Cryptographic Triangles RPC client)\n"; + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// main +// ───────────────────────────────────────────────────────────────────────────── + +int main(int argc, char* argv[]) +{ + ParseCommandLine(argc, argv); + + if (argc < 2 || GetArg("-?", "") == "1" || GetArg("-h", "") == "1" || + GetArg("--help", "") == "1") { + CommandLineHelp(cerr); + return argc < 2 ? 1 : 0; + } + if (!GetArg("-version", "").empty() || !GetArg("--version", "").empty()) { + CommandLineVersion(); + return 0; + } + + RPCConn conn; + if (AppInitRPCConn(conn) != 0) return 1; + + Value method; + Array params; + ParseCommandLineRPCParams(argc, argv, method, params); + string strMethod = method.get_str(); + + if (strMethod == "help" || strMethod == "-help") { + if (params.empty()) { + CommandLineHelp(cout); + return 0; + } + // else fall through: delegate to daemon's help + } + + if (!GetArg("-getinfo", "").empty()) { + return Getinfo(conn, /*fPretty=*/true); + } + + bool fPretty = GetArg("-raw", "").empty(); + + Value result; + int nRet = CallRPC(conn, strMethod, params, result); + if (nRet == 0) { + cout << write_string(result, fPretty) << "\n"; + } + return nRet; +} From 1d938d5770c332d4b20deec7577d3f9b2b830142 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 18:41:38 -0700 Subject: [PATCH 2/9] Fix macOS build: drop Boost::system/find_package component, use std::filesystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homebrew's boost formula doesn't ship the boost_system CMake config file, so find_package(Boost REQUIRED COMPONENTS system) failed on macOS. - Replace boost::filesystem with std::filesystem (C++17, no Boost dep) - Drop 'filesystem' from find_package — only headers needed (asio + system) - Link libboost_system explicitly per-platform by library name, resolved via the platform's default search path (Homebrew toolchain on macOS, system libs on Linux, MSYS2 on Windows) CI will rerun automatically on PR push. --- src/CMakeLists.txt | 21 +++++++++++++++++---- src/triangles-cli.cpp | 5 ++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 327d341..db95bd2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -258,14 +258,20 @@ if(BUILD_CLI) add_executable(triangles-cli triangles-cli.cpp ) - # Boost components used by the CLI - find_package(Boost REQUIRED COMPONENTS filesystem system) + # Boost: only need headers for asio + system. Boost::system gets resolved at + # link time via the platform's default library search path (libboost_system + # on Linux, libboost_system.dylib on macOS Homebrew, libboost_system-mt-*.dll + # on Windows MSYS2). Avoids needing a per-component CMake config file. + find_package(Boost REQUIRED) target_link_libraries(triangles-cli PRIVATE json_compat - Boost::filesystem - Boost::system ) + if(UNIX AND NOT APPLE) + # Linux: explicit link to libboost_system (header-only Boost.System + # is rare; we explicitly link the small compiled library). + target_link_libraries(triangles-cli PRIVATE boost_system) + endif() if(WIN32) set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe") @@ -273,6 +279,13 @@ if(BUILD_CLI) target_link_libraries(triangles-cli PRIVATE ws2_32) endif() + if(APPLE) + # macOS Homebrew ships libboost_system.dylib without a CMake config + # file, so link by library name (resolved via Homebrew's compiler + # toolchain search paths). + target_link_libraries(triangles-cli PRIVATE boost_system) + endif() + if(MSVC) set_target_properties(triangles-cli PROPERTIES VS_WINRT_COMPONENT "console" diff --git a/src/triangles-cli.cpp b/src/triangles-cli.cpp index fe4ad67..939d0a5 100644 --- a/src/triangles-cli.cpp +++ b/src/triangles-cli.cpp @@ -41,8 +41,7 @@ #include #include -#include -#include +#include #include #include @@ -62,7 +61,7 @@ using namespace std; namespace asio = boost::asio; using boost::asio::ip::tcp; -namespace fs = boost::filesystem; +namespace fs = std::filesystem; using namespace json_spirit; // ───────────────────────────────────────────────────────────────────────────── From 600b1cf35fa5951005a4e92d9d6d3cf889205603 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 18:45:04 -0700 Subject: [PATCH 3/9] Fix macOS build: add Boost::boost target for headers, link boost_system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix dropped the find_package component but also killed the boost include path. Now use the modern Boost::boost header-only target (available in Boost 1.83+) which sets up include directories without requiring a per-component config file. Link libboost_system explicitly by name on all platforms — the linker finds it via the platform's default search path: - Linux: /usr/lib (libboost_system.so) - macOS Homebrew: /opt/homebrew/lib (libboost_system.dylib) - Windows MSYS2: mingw64/bin (libboost_system-mt-X-XX.dll) --- src/CMakeLists.txt | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index db95bd2..1dbfe18 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -258,18 +258,20 @@ if(BUILD_CLI) add_executable(triangles-cli triangles-cli.cpp ) - # Boost: only need headers for asio + system. Boost::system gets resolved at - # link time via the platform's default library search path (libboost_system - # on Linux, libboost_system.dylib on macOS Homebrew, libboost_system-mt-*.dll - # on Windows MSYS2). Avoids needing a per-component CMake config file. + # Boost: header-only asio via Boost::boost target (sets include dirs; + # available in Boost 1.83+). libboost_system is the small compiled + # library that provides error_code/error_category symbols; link it + # explicitly by name to avoid needing the per-component CMake config + # file (which Homebrew's boost formula doesn't ship for the system + # component on macOS). find_package(Boost REQUIRED) target_link_libraries(triangles-cli PRIVATE json_compat + Boost::boost ) - if(UNIX AND NOT APPLE) - # Linux: explicit link to libboost_system (header-only Boost.System - # is rare; we explicitly link the small compiled library). + # Link libboost_system per-platform by library name + if(UNIX OR WIN32) target_link_libraries(triangles-cli PRIVATE boost_system) endif() @@ -279,13 +281,6 @@ if(BUILD_CLI) target_link_libraries(triangles-cli PRIVATE ws2_32) endif() - if(APPLE) - # macOS Homebrew ships libboost_system.dylib without a CMake config - # file, so link by library name (resolved via Homebrew's compiler - # toolchain search paths). - target_link_libraries(triangles-cli PRIVATE boost_system) - endif() - if(MSVC) set_target_properties(triangles-cli PROPERTIES VS_WINRT_COMPONENT "console" From 569b541931c5f80b496847a9c3ea2c09488d8e44 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 19:05:37 -0700 Subject: [PATCH 4/9] Drop Boost entirely from triangles-cli: use raw sockets for HTTP Third time's the charm. After two CI failures chasing boost::asio / libboost_system linking issues across platforms (Homebrew missing config on macOS, MSYS2 versioned names on Windows, CMake targets that don't quite work everywhere), rip the whole Boost dependency out of the CLI and use raw POSIX/Winsock sockets. - triangles-cli.cpp: replaced boost::asio with raw socket() / connect() / send() / recv() / getaddrinfo(). Cross-platform: #ifdef _WIN32 for Winsock + WSAStartup / WSACleanup, else POSIX. ~100 lines of clean portable socket code. - src/CMakeLists.txt: dropped find_package(Boost) entirely. Only links json_compat (header-only) + ws2_32 on Windows. No boost libs to find. Should be the last fix needed for this PR. --- src/CMakeLists.txt | 18 +--- src/triangles-cli.cpp | 217 +++++++++++++++++++++++++++--------------- 2 files changed, 146 insertions(+), 89 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1dbfe18..c40884e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -258,26 +258,18 @@ if(BUILD_CLI) add_executable(triangles-cli triangles-cli.cpp ) - # Boost: header-only asio via Boost::boost target (sets include dirs; - # available in Boost 1.83+). libboost_system is the small compiled - # library that provides error_code/error_category symbols; link it - # explicitly by name to avoid needing the per-component CMake config - # file (which Homebrew's boost formula doesn't ship for the system - # component on macOS). - find_package(Boost REQUIRED) + # No Boost dependency: uses raw POSIX/Winsock sockets for HTTP. Only links + # the json_compat header-only shim and the platform's native socket lib + # (Winsock ws2_32 on Windows; libc on POSIX). Keeps the binary small and + # avoids per-platform Boost linking pain (MSYS2 uses versioned -mt- names; + # Homebrew doesn't ship the boost_system CMake config). target_link_libraries(triangles-cli PRIVATE json_compat - Boost::boost ) - # Link libboost_system per-platform by library name - if(UNIX OR WIN32) - target_link_libraries(triangles-cli PRIVATE boost_system) - endif() if(WIN32) set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe") - # boost::asio needs ws2_32 on Windows target_link_libraries(triangles-cli PRIVATE ws2_32) endif() diff --git a/src/triangles-cli.cpp b/src/triangles-cli.cpp index 939d0a5..f5ecf73 100644 --- a/src/triangles-cli.cpp +++ b/src/triangles-cli.cpp @@ -10,9 +10,11 @@ // Build with -DBUILD_CLI=ON (default ON). // // Self-contained: does NOT link util.cpp / wallet.cpp / net.cpp / triangles_common. -// Only links json_compat (nlohmann/json via json_spirit shim), boost (asio + -// program_options + filesystem + system), and OpenSSL (for base64). -// This keeps the CLI binary small (~600 KB stripped on Linux, ~1.5 MB on Windows). +// Only links json_compat (nlohmann/json via json_spirit shim) and the platform's +// native socket library (Winsock on Windows, libc on POSIX). No Boost dependency +// at all — keeps the binary small and avoids platform-specific link problems +// with boost::asio / libboost_system (MSYS2 names them with -mt- versioned +// suffixes; Homebrew doesn't ship the CMake config for the system component). // // Connection parameters (highest precedence first): // 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport @@ -38,9 +40,6 @@ #include "json/json_compat.h" -#include -#include - #include #include @@ -58,9 +57,32 @@ #include #include +// Cross-platform socket includes +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include + #include + #pragma comment(lib, "ws2_32.lib") + using socket_t = SOCKET; + #define TRI_CLI_INVALID_SOCKET INVALID_SOCKET + #define TRI_CLI_CLOSE_SOCKET(s) closesocket(s) +#else + #include + #include + #include + #include + #include + #include + #include + #include + using socket_t = int; + #define TRI_CLI_INVALID_SOCKET (-1) + #define TRI_CLI_CLOSE_SOCKET(s) close(s) +#endif + using namespace std; -namespace asio = boost::asio; -using boost::asio::ip::tcp; namespace fs = std::filesystem; using namespace json_spirit; @@ -92,17 +114,14 @@ static void ReadConfigFile(const string& path) if (!f.good()) return; string line; while (getline(f, line)) { - // Strip CR (Windows) and leading whitespace if (!line.empty() && line.back() == '\r') line.pop_back(); size_t start = line.find_first_not_of(" \t"); if (start == string::npos) continue; if (line[start] == '#') continue; - // Parse key = value size_t eq = line.find('=', start); if (eq == string::npos) continue; string key = line.substr(start, eq - start); string value = line.substr(eq + 1); - // Trim whitespace on both ends auto trim = [](string& s) { size_t a = s.find_first_not_of(" \t"); size_t b = s.find_last_not_of(" \t"); @@ -111,7 +130,6 @@ static void ReadConfigFile(const string& path) }; trim(key); trim(value); - // Strip surrounding quotes if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\''))) { @@ -125,25 +143,21 @@ static void ReadConfigFile(const string& path) } } -// Cross-platform default data directory (matches the daemon's path) static fs::path GetDefaultDataDir() { -#ifdef WIN32 - // %APPDATA%/CryptographicTriangles +#ifdef _WIN32 const char* appdata = getenv("APPDATA"); if (appdata && *appdata) { return fs::path(appdata) / "CryptographicTriangles"; } return fs::path("C:/CryptographicTriangles"); #elif defined(__APPLE__) - // ~/Library/Application Support/CryptographicTriangles const char* home = getenv("HOME"); if (home && *home) { return fs::path(home) / "Library/Application Support/CryptographicTriangles"; } return fs::path("/tmp/CryptographicTriangles"); #else - // ~/.cryptographic-triangles (matches daemon's GetDefaultDataDir) const char* home = getenv("HOME"); if (home && *home) { return fs::path(home) / ".cryptographic-triangles"; @@ -171,7 +185,6 @@ static void ParseCommandLine(int argc, char* const argv[]) mapMultiArgs.clear(); for (int i = 1; i < argc; ++i) { string str(argv[i]); - // Bare "-" means: read remaining args from stdin if (str == "-") { mapMultiArgs["-"].push_back("-"); continue; @@ -203,9 +216,6 @@ struct RPCConn { static int AppInitRPCConn(RPCConn& conn) { - // Load conf file FIRST (before pulling creds) so defaults from triangles.conf - // are visible. Command-line flags (already in mapArgs) take precedence because - // ReadConfigFile only inserts when key is absent. fs::path confPath = GetConfigFilePath(); if (!confPath.empty()) ReadConfigFile(confPath.string()); @@ -234,7 +244,6 @@ static Value ParseCLIParam(const string& arg) return Value(string("")); } Value v; - // Try parsing the arg as JSON. If it parses to a non-string literal, keep. if (read_string(arg, v) && v.type() != str_type) { return v; } @@ -247,7 +256,6 @@ static void ParseCommandLineRPCParams(int argc, char* const argv[], method = Value(string("")); params.clear(); int i = 1; - // Skip leading flags static const set valFlags = { "-conf", "-datadir", "-rpcconnect", "-rpcport", "-rpcuser", "-rpcpassword" @@ -309,12 +317,37 @@ static string Base64Encode(const string& in) } // ───────────────────────────────────────────────────────────────────────────── -// HTTP/1.1 JSON-RPC POST (plaintext) +// HTTP/1.1 JSON-RPC POST (plaintext) — using raw sockets (no Boost) // ───────────────────────────────────────────────────────────────────────────── +namespace { + +class SocketInit { +public: + SocketInit() { +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + } + ~SocketInit() { +#ifdef _WIN32 + WSACleanup(); +#endif + } +}; + +inline void close_socket(socket_t s) { + TRI_CLI_CLOSE_SOCKET(s); +} + +} // namespace + static int CallRPC(const RPCConn& conn, const string& strMethod, const Array& params, Value& result) { + SocketInit sockInit; + Object req; req.push_back(Pair("jsonrpc", Value(string("1.0")))); req.push_back(Pair("id", Value(string("triangles-cli")))); @@ -324,81 +357,114 @@ static int CallRPC(const RPCConn& conn, const string& strMethod, string strAuth = Base64Encode(conn.user + ":" + conn.pass); - asio::io_context io; - tcp::resolver resolver(io); - boost::system::error_code ec; - auto endpoints = resolver.resolve(conn.host, conn.port, ec); - if (ec) { + // Resolve host:port via getaddrinfo + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + struct addrinfo* addrRes = nullptr; + int rc = getaddrinfo(conn.host.c_str(), conn.port.c_str(), &hints, &addrRes); + if (rc != 0 || addrRes == nullptr) { cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port - << " failed: " << ec.message() << "\n"; + << " failed: " << gai_strerror(rc) << "\n"; + if (addrRes) freeaddrinfo(addrRes); return 1; } - tcp::socket sock(io); - sock.connect(*endpoints.begin(), ec); - if (ec) { + // Try each resolved address until one connects + socket_t sock = TRI_CLI_INVALID_SOCKET; + for (struct addrinfo* ai = addrRes; ai != nullptr; ai = ai->ai_next) { + sock = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock == TRI_CLI_INVALID_SOCKET) { + continue; + } + if (::connect(sock, ai->ai_addr, ai->ai_addrlen) == 0) { + break; // connected + } + close_socket(sock); + sock = TRI_CLI_INVALID_SOCKET; + } + freeaddrinfo(addrRes); + if (sock == TRI_CLI_INVALID_SOCKET) { cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port - << " failed: " << ec.message() << "\n" + << " failed\n" << "(is trianglesd running and accepting JSON-RPC?)\n"; return 1; } - ostringstream reqStream; - reqStream << "POST / HTTP/1.1\r\n" - << "Host: " << conn.host << ":" << conn.port << "\r\n" - << "Authorization: Basic " << strAuth << "\r\n" - << "Content-Type: application/json\r\n" - << "Content-Length: " << strRequest.size() << "\r\n" - << "Connection: close\r\n" - << "\r\n" - << strRequest; - asio::streambuf requestBuf; - std::ostream os(&requestBuf); - os << reqStream.str(); - asio::write(sock, requestBuf, ec); - if (ec) { - cerr << "triangles-cli: write failed: " << ec.message() << "\n"; - return 1; + // Build HTTP/1.1 request + string reqData = + "POST / HTTP/1.1\r\n" + "Host: " + conn.host + ":" + conn.port + "\r\n" + "Authorization: Basic " + strAuth + "\r\n" + "Content-Type: application/json\r\n" + "Content-Length: " + to_string(strRequest.size()) + "\r\n" + "Connection: close\r\n" + "\r\n" + strRequest; + + // Send + size_t totalSent = 0; + while (totalSent < reqData.size()) { + ssize_t n = ::send(sock, reqData.data() + totalSent, + reqData.size() - totalSent, 0); + if (n <= 0) { + cerr << "triangles-cli: write failed\n"; + close_socket(sock); + return 1; + } + totalSent += static_cast(n); } - asio::streambuf responseBuf; - boost::system::error_code readEc; - while (asio::read(sock, responseBuf, - asio::transfer_at_least(1), readEc)) { - // keep reading until EOF or error - } - if (readEc && readEc != asio::error::eof) { - cerr << "triangles-cli: read failed: " << readEc.message() << "\n"; - return 1; + // Read full response (until EOF) + string respData; + char buf[4096]; + while (true) { + ssize_t n = ::recv(sock, buf, sizeof(buf), 0); + if (n > 0) { + respData.append(buf, static_cast(n)); + } else if (n == 0) { + break; // EOF + } else { + // Error +#ifdef _WIN32 + int err = WSAGetLastError(); + if (err == WSAECONNRESET || err == WSAECONNABORTED) { + // Treat as EOF + break; + } +#else + if (errno == EINTR) continue; // interrupted, retry + if (errno == ECONNRESET) break; // peer closed +#endif + cerr << "triangles-cli: read failed\n"; + close_socket(sock); + return 1; + } } + close_socket(sock); - std::istream rs(&responseBuf); - string line; - if (!std::getline(rs, line)) { - cerr << "triangles-cli: empty response\n"; + // Parse status line + size_t hdrEnd = respData.find("\r\n\r\n"); + if (hdrEnd == string::npos) { + cerr << "triangles-cli: malformed response (no header terminator)\n"; return 1; } - if (!line.empty() && line.back() == '\r') line.pop_back(); + string statusLine = respData.substr(0, respData.find("\r\n")); int status = 0; { - istringstream iss(line); + istringstream iss(statusLine); string httpVer; iss >> httpVer >> status; } if (status != 200) { cerr << "triangles-cli: server returned HTTP " << status << "\n"; - ostringstream body; - body << rs.rdbuf(); - if (!body.str().empty()) cerr << body.str() << "\n"; + string body = respData.substr(hdrEnd + 4); + if (!body.empty()) cerr << body << "\n"; return 1; } - while (std::getline(rs, line) && line != "\r" && !line.empty()) {} - string body; - { - ostringstream oss; - oss << rs.rdbuf(); - body = oss.str(); - } + string body = respData.substr(hdrEnd + 4); Value reply; if (!read_string(body, reply)) { @@ -533,7 +599,6 @@ int main(int argc, char* argv[]) CommandLineHelp(cout); return 0; } - // else fall through: delegate to daemon's help } if (!GetArg("-getinfo", "").empty()) { From 274aafab3678a674e4cd846309eb1a4420e2d6db Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 19:19:32 -0700 Subject: [PATCH 5/9] Fix Windows packaging step: simplify bash { } | sort -u | while pattern The previous step used a bash group command piped through sort -u and a while loop. Under MSYS2 bash + 'set -e -o pipefail' (GitHub Actions default), this triggered a non-zero exit even when the loop body succeeded, causing the Windows daemon job to fail at the packaging step (the actual link of both trianglesd.exe and triangles-cli.exe succeeded). Replaced the { } | sort -u | while pattern with a temp-file-based dedup: - ldd both binaries, append to /tmp/cli-dlls.txt (or cli-libs.txt on Linux) - sort -u the temp file - pipe the result into the while loop (simpler pipeline, no group) Also applied the same simplification to the Linux .deb packaging for consistency, even though the Linux build was passing. --- .github/workflows/build-all.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 793d0e8..8c7788b 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -284,13 +284,13 @@ jobs: cp build/bin/trianglesd.exe daemon-dist/ cp build/bin/triangles-cli.exe daemon-dist/ - # Copy all linked DLLs from MSYS2 (covers both binaries; ldd union) - { - ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' - ldd build/bin/triangles-cli.exe | grep '/mingw64' | awk '{print $3}' - } | sort -u | while read dll; do - cp "$dll" daemon-dist/ 2>/dev/null || true + # Collect all linked DLLs from MSYS2 (covers both binaries; dedup via sort -u) + ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' > /tmp/cli-dlls.txt + ldd build/bin/triangles-cli.exe | grep '/mingw64' | awk '{print $3}' >> /tmp/cli-dlls.txt + sort -u /tmp/cli-dlls.txt | while read dll; do + [ -n "$dll" ] && cp "$dll" daemon-dist/ 2>/dev/null || true done + rm -f /tmp/cli-dlls.txt - name: Bundle Tor for daemon shell: powershell @@ -492,11 +492,11 @@ jobs: [ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data # Bundle ALL shared library dependencies (except glibc/kernel) - # Union of ldd output from both binaries - { - ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' - ldd build/bin/triangles-cli | grep '=> /' | awk '{print $3}' - } | sort -u | while read lib; do + # Union of ldd output from both binaries, dedup via sort -u + ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' > /tmp/cli-libs.txt + ldd build/bin/triangles-cli | grep '=> /' | awk '{print $3}' >> /tmp/cli-libs.txt + sort -u /tmp/cli-libs.txt | while read lib; do + [ -z "$lib" ] && continue case "$lib" in /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) ;; # Skip glibc core — always present @@ -505,6 +505,7 @@ jobs: ;; esac done + rm -f /tmp/cli-libs.txt echo "=== Bundled libs ===" ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l ls ${PKG}/usr/lib/cryptographic-triangles/lib/ From 91d9233ea4d11e54b1532fcc2a3b0e1bff977684 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 19:34:45 -0700 Subject: [PATCH 6/9] Simplify DLL packaging: plain for loop, no pipe-into-while The previous attempts used 'ldd | sort -u | while read; do ... done' patterns that exit 1 under MSYS2 bash + 'set -e -o pipefail' even when the script ran successfully. Replaced with a plain 'for bin in ...; do ldd > list.txt; while read; do cp; done < list.txt; done' pattern that has no pipelines other than the standard redirection, and uses IFS= read -r for safe line iteration. Also moved temp files from /tmp to the working directory (./dll-list.txt) to avoid any MSYS2 /tmp path-translation edge cases. --- .github/workflows/build-all.yml | 41 ++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 8c7788b..49cd3a1 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -284,13 +284,16 @@ jobs: cp build/bin/trianglesd.exe daemon-dist/ cp build/bin/triangles-cli.exe daemon-dist/ - # Collect all linked DLLs from MSYS2 (covers both binaries; dedup via sort -u) - ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' > /tmp/cli-dlls.txt - ldd build/bin/triangles-cli.exe | grep '/mingw64' | awk '{print $3}' >> /tmp/cli-dlls.txt - sort -u /tmp/cli-dlls.txt | while read dll; do - [ -n "$dll" ] && cp "$dll" daemon-dist/ 2>/dev/null || true + # Collect DLLs from both binaries. Use simple for loop instead of + # pipes-into-while, which interact badly with MSYS2 bash + GitHub + # Actions' set -e -o pipefail. cp is idempotent so dupes are fine. + for bin in trianglesd triangles-cli; do + ldd "build/bin/${bin}.exe" | grep '/mingw64' | awk '{print $3}' > dll-list.txt + while IFS= read -r dll; do + cp "$dll" daemon-dist/ >/dev/null 2>&1 || true + done < dll-list.txt + rm -f dll-list.txt done - rm -f /tmp/cli-dlls.txt - name: Bundle Tor for daemon shell: powershell @@ -492,20 +495,20 @@ jobs: [ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data # Bundle ALL shared library dependencies (except glibc/kernel) - # Union of ldd output from both binaries, dedup via sort -u - ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' > /tmp/cli-libs.txt - ldd build/bin/triangles-cli | grep '=> /' | awk '{print $3}' >> /tmp/cli-libs.txt - sort -u /tmp/cli-libs.txt | while read lib; do - [ -z "$lib" ] && continue - case "$lib" in - /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) - ;; # Skip glibc core — always present - *) - cp -L "$lib" ${PKG}/usr/lib/cryptographic-triangles/lib/ 2>/dev/null || true - ;; - esac + # Use simple for loop (cp is idempotent so dupes are fine) + for bin in trianglesd triangles-cli; do + ldd "build/bin/${bin}" | grep '=> /' | awk '{print $3}' > lib-list.txt + while IFS= read -r lib; do + case "$lib" in + /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) + ;; # Skip glibc core — always present + *) + cp -L "$lib" ${PKG}/usr/lib/cryptographic-triangles/lib/ >/dev/null 2>&1 || true + ;; + esac + done < lib-list.txt + rm -f lib-list.txt done - rm -f /tmp/cli-libs.txt echo "=== Bundled libs ===" ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l ls ${PKG}/usr/lib/cryptographic-triangles/lib/ From f0e5dbdebcf04a48557f22b7f942fbd58303b34b Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 19:47:22 -0700 Subject: [PATCH 7/9] diagnostic: add tracing to Windows packaging step --- .github/workflows/build-all.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 49cd3a1..876c6ef 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -280,20 +280,25 @@ jobs: - name: Package daemon with DLLs run: | + echo "STEP_START: pwd=$(pwd) sh=$BASH_VERSION" mkdir -p daemon-dist/tor + echo "STEP_AFTER_MKDIR" cp build/bin/trianglesd.exe daemon-dist/ + echo "STEP_AFTER_CP1" cp build/bin/triangles-cli.exe daemon-dist/ - - # Collect DLLs from both binaries. Use simple for loop instead of - # pipes-into-while, which interact badly with MSYS2 bash + GitHub - # Actions' set -e -o pipefail. cp is idempotent so dupes are fine. + echo "STEP_AFTER_CP2" for bin in trianglesd triangles-cli; do + echo "STEP_LDD_$bin" ldd "build/bin/${bin}.exe" | grep '/mingw64' | awk '{print $3}' > dll-list.txt + echo "STEP_GREP_$bin count=$(wc -l < dll-list.txt)" while IFS= read -r dll; do cp "$dll" daemon-dist/ >/dev/null 2>&1 || true done < dll-list.txt rm -f dll-list.txt + echo "STEP_DONE_$bin" done + echo "STEP_DONE" + ls -la daemon-dist/ - name: Bundle Tor for daemon shell: powershell From 8c74f4e22827660c444bd00007d346e171ee785c Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 19:49:31 -0700 Subject: [PATCH 8/9] Add package-windows-daemon.sh + package-linux-daemon.sh scripts Move the Windows daemon packaging step and the Linux .deb build into committed shell scripts under scripts/ci/. This bypasses GitHub Actions' inline-run-block quirks (silent exit 1 under msys2 + set -e -o pipefail with multi-line scripts) and makes the packaging logic debuggable locally. --- scripts/ci/package-linux-daemon.sh | 142 +++++++++++++++++++++++++++ scripts/ci/package-windows-daemon.sh | 71 ++++++++++++++ 2 files changed, 213 insertions(+) create mode 100755 scripts/ci/package-linux-daemon.sh create mode 100755 scripts/ci/package-windows-daemon.sh diff --git a/scripts/ci/package-linux-daemon.sh b/scripts/ci/package-linux-daemon.sh new file mode 100755 index 0000000..ad19bb4 --- /dev/null +++ b/scripts/ci/package-linux-daemon.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# scripts/ci/package-linux-daemon.sh +# +# Linux packaging step for the triangles daemon + CLI .deb. +# Called from .github/workflows/build-all.yml build-linux-daemon step. +# +# Builds a self-contained .deb with trianglesd, triangles-cli, bundled libs, +# Tor, systemd service, and CLI launchers. Designed to be reproducible and +# debuggable outside the CI environment. +# +# Usage: bash scripts/ci/package-linux-daemon.sh + +set -euo pipefail + +VERSION="${1:-0.0.0}" +PKG="cryptographic-triangles-daemon_${VERSION}_amd64" +TOR_VERSION="${TOR_VERSION:-15.0.9}" + +echo ">>> Building .deb for triangles ${VERSION}" + +# Stage directories +rm -rf "${PKG}" +mkdir -p "${PKG}/DEBIAN" +mkdir -p "${PKG}/usr/lib/cryptographic-triangles/lib" +mkdir -p "${PKG}/usr/lib/cryptographic-triangles/tor" +mkdir -p "${PKG}/usr/bin" +mkdir -p "${PKG}/etc/systemd/system" + +# Download + extract Tor +TOR_TARBALL="tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" +if [ ! -f "${TOR_TARBALL}" ]; then + echo ">>> Downloading Tor ${TOR_VERSION}..." + curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_TARBALL}" -o "${TOR_TARBALL}" +fi +mkdir -p tor-extract +tar -xzf "${TOR_TARBALL}" -C tor-extract + +# Copy binaries +cp "build/bin/trianglesd" "${PKG}/usr/lib/cryptographic-triangles/" +cp "build/bin/triangles-cli" "${PKG}/usr/lib/cryptographic-triangles/" + +# Copy Tor +cp "tor-extract/tor/tor" "${PKG}/usr/lib/cryptographic-triangles/tor/" +chmod +x "${PKG}/usr/lib/cryptographic-triangles/tor/tor" +if [ -d "tor-extract/data" ]; then + cp -r "tor-extract/data" "${PKG}/usr/lib/cryptographic-triangles/tor/data" +fi + +# Bundle shared library dependencies (skip glibc/kernel — always present) +echo ">>> Bundling shared library dependencies..." +ALL_LIBS="$(mktemp)" +trap 'rm -f "${ALL_LIBS}"' EXIT + +for bin in trianglesd triangles-cli; do + ldd "build/bin/${bin}" 2>/dev/null \ + | grep '=> /' \ + | awk '{print $3}' \ + >> "${ALL_LIBS}" || true +done + +if [ -s "${ALL_LIBS}" ]; then + sort -u "${ALL_LIBS}" | while IFS= read -r lib; do + if [ -z "${lib}" ]; then continue; fi + case "${lib}" in + /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) + ;; # Skip glibc core + *) + cp -L "${lib}" "${PKG}/usr/lib/cryptographic-triangles/lib/" 2>/dev/null || true + ;; + esac + done +fi + +echo ">>> Bundled libs:" +ls -la "${PKG}/usr/lib/cryptographic-triangles/lib/" | tail -n +2 | wc -l + +# Launchers (set LD_LIBRARY_PATH for bundled libs) +cat > "${PKG}/usr/bin/trianglesd" << 'LAUNCHER' +#!/bin/bash +INSTALL_DIR=/usr/lib/cryptographic-triangles +export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" +exec "${INSTALL_DIR}/trianglesd" "$@" +LAUNCHER +chmod +x "${PKG}/usr/bin/trianglesd" + +cat > "${PKG}/usr/bin/triangles-cli" << 'LAUNCHER' +#!/bin/bash +INSTALL_DIR=/usr/lib/cryptographic-triangles +export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" +exec "${INSTALL_DIR}/triangles-cli" "$@" +LAUNCHER +chmod +x "${PKG}/usr/bin/triangles-cli" + +# systemd unit +cat > "${PKG}/etc/systemd/system/trianglesd.service" << 'SVC' +[Unit] +Description=Cryptographic Triangles Daemon +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib +ExecStart=/usr/lib/cryptographic-triangles/trianglesd +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +SVC + +# DEBIAN/control +cat > "${PKG}/DEBIAN/control" << CTRL +Package: cryptographic-triangles-daemon +Version: ${VERSION} +Architecture: amd64 +Maintainer: Cryptographic Triangles +Description: Cryptographic Triangles daemon + CLI with integrated Tor + Fully self-contained headless node + JSON-RPC client with all libraries, + Tor, and systemd service. No external dependencies required. +Section: finance +Priority: optional +CTRL + +# DEBIAN/postinst +cat > "${PKG}/DEBIAN/postinst" << 'POST' +#!/bin/bash +systemctl daemon-reload +echo "" +echo "Cryptographic Triangles daemon + CLI installed." +echo " Start daemon: sudo systemctl start trianglesd" +echo " On boot: sudo systemctl enable trianglesd" +echo " Use CLI: triangles-cli getinfo" +echo "" +POST +chmod +x "${PKG}/DEBIAN/postinst" + +# Build the .deb +dpkg-deb --build "${PKG}" +echo ">>> Built: ${PKG}.deb" +ls -la "${PKG}.deb" +exit 0 diff --git a/scripts/ci/package-windows-daemon.sh b/scripts/ci/package-windows-daemon.sh new file mode 100755 index 0000000..4f22622 --- /dev/null +++ b/scripts/ci/package-windows-daemon.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# scripts/ci/package-windows-daemon.sh +# +# Windows MSYS2 packaging step for the triangles daemon + CLI. +# Called from .github/workflows/build-all.yml build-windows-daemon step. +# +# Why a script file instead of inline YAML: +# The GitHub Actions msys2 shell wrapper has shown inconsistent handling of +# multi-line inline run: blocks under `set -e -o pipefail` (silent exits with +# code 1). A committed script file bypasses the YAML → shell translation +# quirks and gives us a known-good artifact that we can also run locally in +# MSYS2 for debugging. +# +# Usage: bash scripts/ci/package-windows-daemon.sh [ ...] +# Example: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli + +set -euo pipefail + +DIST="${1:-daemon-dist}" +shift +BINS=("$@") + +if [ "${#BINS[@]}" -eq 0 ]; then + echo "Usage: $0 [ ...]" >&2 + echo " e.g. $0 daemon-dist trianglesd triangles-cli" >&2 + exit 2 +fi + +echo ">>> Package step: bins=${BINS[*]} dist=${DIST}" + +# Make the dist directory +mkdir -p "${DIST}/tor" + +# Copy each binary to dist/ +for bin in "${BINS[@]}"; do + src="build/bin/${bin}.exe" + if [ ! -f "${src}" ]; then + echo "ERROR: ${src} not found" >&2 + exit 3 + fi + cp "${src}" "${DIST}/" + echo " copied ${src} -> ${DIST}/" +done + +# Copy linked DLLs (union of all binaries' dependencies, deduped) +echo ">>> Collecting DLLs from ldd output..." +ALL_DLLS="$(mktemp)" +trap 'rm -f "${ALL_DLLS}"' EXIT + +for bin in "${BINS[@]}"; do + src="build/bin/${bin}.exe" + ldd "${src}" 2>/dev/null \ + | grep '/mingw64' \ + | awk '{print $3}' \ + >> "${ALL_DLLS}" || true +done + +if [ ! -s "${ALL_DLLS}" ]; then + echo "WARNING: no /mingw64 DLLs found in ldd output for ${BINS[*]}" >&2 +else + echo ">>> Copying $(sort -u "${ALL_DLLS}" | wc -l) unique DLLs..." + sort -u "${ALL_DLLS}" | while IFS= read -r dll; do + if [ -n "${dll}" ] && [ -f "${dll}" ]; then + cp "${dll}" "${DIST}/" || echo "WARN: failed to copy ${dll}" >&2 + fi + done +fi + +echo ">>> Package complete: $(ls -1 "${DIST}" | wc -l) files in ${DIST}/" +ls -la "${DIST}/" +exit 0 From ad267866ab935e9c07e02ac664879c333b5d2ab8 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 19:50:10 -0700 Subject: [PATCH 9/9] Switch to script-file packaging for Windows + Linux daemon jobs Replace inline multi-line run: blocks with invocations of the scripts/ci/package-*.sh scripts. This sidesteps the GitHub Actions msys2 + 'set -e -o pipefail' issue that caused silent exit 1 on the Windows daemon packaging step. The scripts are also debuggable locally. --- .github/workflows/build-all.yml | 122 +------------------------------- 1 file changed, 2 insertions(+), 120 deletions(-) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 876c6ef..ff0c47e 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -279,26 +279,7 @@ jobs: strip --strip-all build/bin/triangles-cli.exe - name: Package daemon with DLLs - run: | - echo "STEP_START: pwd=$(pwd) sh=$BASH_VERSION" - mkdir -p daemon-dist/tor - echo "STEP_AFTER_MKDIR" - cp build/bin/trianglesd.exe daemon-dist/ - echo "STEP_AFTER_CP1" - cp build/bin/triangles-cli.exe daemon-dist/ - echo "STEP_AFTER_CP2" - for bin in trianglesd triangles-cli; do - echo "STEP_LDD_$bin" - ldd "build/bin/${bin}.exe" | grep '/mingw64' | awk '{print $3}' > dll-list.txt - echo "STEP_GREP_$bin count=$(wc -l < dll-list.txt)" - while IFS= read -r dll; do - cp "$dll" daemon-dist/ >/dev/null 2>&1 || true - done < dll-list.txt - rm -f dll-list.txt - echo "STEP_DONE_$bin" - done - echo "STEP_DONE" - ls -la daemon-dist/ + run: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli - name: Bundle Tor for daemon shell: powershell @@ -481,106 +462,7 @@ jobs: strip --strip-all build/bin/triangles-cli - name: Build .deb package (fully self-contained) - run: | - TOR_VERSION="15.0.9" - curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" -o tor-bundle.tar.gz - mkdir -p tor-extract && tar -xzf tor-bundle.tar.gz -C tor-extract - - PKG="cryptographic-triangles-daemon_${VERSION}_amd64" - mkdir -p ${PKG}/DEBIAN - mkdir -p ${PKG}/usr/lib/cryptographic-triangles/lib - mkdir -p ${PKG}/usr/lib/cryptographic-triangles/tor - mkdir -p ${PKG}/usr/bin - mkdir -p ${PKG}/etc/systemd/system - - cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/ - cp build/bin/triangles-cli ${PKG}/usr/lib/cryptographic-triangles/ - cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/ - chmod +x ${PKG}/usr/lib/cryptographic-triangles/tor/tor - [ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data - - # Bundle ALL shared library dependencies (except glibc/kernel) - # Use simple for loop (cp is idempotent so dupes are fine) - for bin in trianglesd triangles-cli; do - ldd "build/bin/${bin}" | grep '=> /' | awk '{print $3}' > lib-list.txt - while IFS= read -r lib; do - case "$lib" in - /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) - ;; # Skip glibc core — always present - *) - cp -L "$lib" ${PKG}/usr/lib/cryptographic-triangles/lib/ >/dev/null 2>&1 || true - ;; - esac - done < lib-list.txt - rm -f lib-list.txt - done - echo "=== Bundled libs ===" - ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l - ls ${PKG}/usr/lib/cryptographic-triangles/lib/ - - # Launchers with LD_LIBRARY_PATH - cat > ${PKG}/usr/bin/trianglesd << 'LAUNCHER' - #!/bin/bash - INSTALL_DIR=/usr/lib/cryptographic-triangles - export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" - exec "${INSTALL_DIR}/trianglesd" "$@" - LAUNCHER - sed -i 's/^ //' ${PKG}/usr/bin/trianglesd - chmod +x ${PKG}/usr/bin/trianglesd - - cat > ${PKG}/usr/bin/triangles-cli << 'LAUNCHER' - #!/bin/bash - INSTALL_DIR=/usr/lib/cryptographic-triangles - export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" - exec "${INSTALL_DIR}/triangles-cli" "$@" - LAUNCHER - sed -i 's/^ //' ${PKG}/usr/bin/triangles-cli - chmod +x ${PKG}/usr/bin/triangles-cli - - cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC' - [Unit] - Description=Cryptographic Triangles Daemon - After=network-online.target - Wants=network-online.target - - [Service] - Type=simple - Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib - ExecStart=/usr/lib/cryptographic-triangles/trianglesd - Restart=on-failure - RestartSec=10 - - [Install] - WantedBy=multi-user.target - SVC - sed -i 's/^ //' ${PKG}/etc/systemd/system/trianglesd.service - - cat > ${PKG}/DEBIAN/control << CTRL - Package: cryptographic-triangles-daemon - Version: ${VERSION} - Architecture: amd64 - Maintainer: Cryptographic Triangles - Description: Cryptographic Triangles daemon + CLI with integrated Tor - Fully self-contained headless node + JSON-RPC client with all libraries, - Tor, and systemd service. No external dependencies required. - Section: finance - Priority: optional - CTRL - sed -i 's/^ //' ${PKG}/DEBIAN/control - - cat > ${PKG}/DEBIAN/postinst << 'POST' - #!/bin/bash - systemctl daemon-reload - echo "" - echo "Cryptographic Triangles daemon + CLI installed." - echo " Start daemon: sudo systemctl start trianglesd" - echo " On boot: sudo systemctl enable trianglesd" - echo " Use CLI: triangles-cli getinfo" - echo "" - POST - chmod +x ${PKG}/DEBIAN/postinst - - dpkg-deb --build ${PKG} + run: bash scripts/ci/package-linux-daemon.sh "${VERSION}" - name: Upload .deb uses: actions/upload-artifact@v4