# Bitflash — top-level build # # make linux build Bitflash-*-x86_64.AppImage # make windows build Bitflash-*-windows.zip (from MSYS2 UCRT64) # make windows-tor build Bitflash-*-windows-with-tor.zip # make tests build and run standalone unit tests # make checksums create SHA256SUMS for release assets # make verify-release TAG=v1.2.13 # make clean remove build artifacts ROOT := $(shell pwd) NPROC := $(shell nproc 2>/dev/null || echo 2) SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") # Not "1.2.20". This tree is upstream v1.2.20 plus our own hardening commits and # a hand-merged payout ledger, so a binary built here must not be mistakable for # the upstream release of that number -- it names every release artifact. VERSION := 1.2.20+sec # ---- Pinned third-party sources ------------------------------------------- # # These were `git clone --depth 1 ` with no revision, which resolves to # whatever the default branch happens to point at the moment the build runs. # For a dependency that only had to link, that is untidy. For these two it is # not: # # RandomX is the proof of work. Its master is not frozen -- v2.0 (Mar 2026) # changed the algorithm -- so two builds of the same Bitflash commit, made a # few weeks apart, can compute different hashes for the same header. That is # not a supply-chain worry, it is a chain split arriving as a build artifact, # with no version recorded anywhere to explain it afterwards. # # libsecp256k1 is signature verification, i.e. the rule deciding who may # spend. Whatever is on master on the day of the build is what enforces it. # # Pinned to full commit ids, not tags: a tag is a mutable pointer, and `git # checkout ` is the only form here that verifies what it fetched (git # resolves the object by its own hash). Bump deliberately, and rebuild and # resync before shipping the result. # # The check has to decide the exit status, so each recipe below joins its # commands with && the whole way to the closing echo. Ending that chain with a # `;` meant a failed `test` only skipped the build steps: the echo still ran, # the target still exited 0, and make announced a pinned dependency that # nothing had verified. # # RandomX pin is the commit this tree has been building and syncing against # (post-v2.0.1 master, 2026-05-24) -- identical to upstream's pin. # # libsecp256k1 is pinned to upstream v1.2.18's commit rather than the one this # fork carried, so signature validation matches the binaries the rest of the # network runs. Consensus code must not differ from the network by accident. # There must be exactly ONE assignment of each of these variables in this file: # upstream defines them near the top too, and GNU Make takes the last `:=`, so # a second block silently decides the build. SECP256K1_REPO := https://github.com/bitcoin-core/secp256k1 SECP256K1_COMMIT := 7fecac74aed8e1fd9078380d67dd04663705c989 RANDOMX_REPO := https://github.com/tevador/RandomX RANDOMX_COMMIT := 1e9d4b2df63fa6edf46b06789486e23c0fcaa55a # appimagetool came from the "continuous" release, a tag that is moved in place # on every upstream push, fetched over the network and executed. Pinned to a # numbered release and checked by digest before it is allowed to run. APPIMAGETOOL_URL := https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage APPIMAGETOOL_SHA256 := ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 # ---- Linux ---------------------------------------------------------------- # RandomX used to build in $(HOME)/RandomX, and deps-randomx would rm -rf that # path whenever the library was missing -- deleting a tree the build did not # create and does not own (issue #27; a hand-managed pinned checkout there is # what this machine's miners build against). The default is now in-tree, like # secp256k1-build. A host that deliberately keeps RandomX elsewhere can set # RANDOMX_DIR=... explicitly; the recipe below will still never delete a # directory that lacks the marker file this build writes after a successful # build. Exported so src/Makefile's own RANDOMX_DIR ?= picks the same tree. # # Every use of it below is quoted, and the recipe rejects a value that cannot # name exactly one directory before it reaches anything destructive. Unquoted, # the marker guard could be stepped around: a value holding a glob that matches # two directories made `[ -d $(RANDOMX_DIR) ]` a two-argument test, which dies # with "unexpected operator" instead of answering, so the && short-circuited, # the guard was skipped, and the rm -rf below ran against every match. A value # holding a space or a `&&` broke the outer test the same way and reported # "already built". Whitespace and glob characters are refused outright rather # than merely quoted, because src/Makefile interpolates this same value into # -I$(RANDOMX_DIR)/src and -L$(RANDOMX_DIR)/build unquoted, where neither can # survive. A relative value is refused rather than silently made absolute: it # would mean two different directories to this Makefile and to src/Makefile, # whose cwd is src/, and a command-line assignment reaches the sub-make through # MAKEFLAGS, where it outranks anything exported from here. RANDOMX_DIR ?= $(ROOT)/randomx-build export RANDOMX_DIR linux: deps-linux $(MAKE) -C src -f Makefile -j$(NPROC) strip src/bitflash $(MAKE) -f Makefile appimage $(MAKE) -f Makefile linux-node # Headless node, no ImGui/GLFW/OpenGL. The GUI build needs libGL present at load # time even when started with -nogui -- the dynamic loader resolves everything # before main() runs, so the flag comes far too late. On a clean server, which is # where a node most naturally lives, that is a hard failure. This binary has no # such dependency and needs no FUSE either, so it drops onto a VPS and runs. linux-node: deps-linux $(MAKE) -C src -f Makefile bitflash-node -j$(NPROC) strip src/bitflash-node cp src/bitflash-node bitflash-node-$(VERSION)-x86_64 @echo "Built: bitflash-node-$(VERSION)-x86_64" deps-linux: deps-apt deps-secp256k1 deps-randomx # libboost-dev, not libboost-system-dev: Boost.System has been header-only # since 1.69 and Ubuntu 26.04 (resolute) no longer ships the -system package. # This tree only includes two boost headers and links no boost library. The # apt-get update comes first: a fresh image's package lists are empty, and # install then fails on the first name it cannot find. # # A failed update is deliberately not fatal: one unreachable repository should # not block an install the lists already on disk can satisfy. It does say so, # though -- "unable to locate package" several seconds later is otherwise hard # to connect back to it. The install is the last command in the branch, so its # status is the target's, and that one does abort. deps-apt: @if command -v apt-get >/dev/null 2>&1; then \ $(SUDO) apt-get update || \ echo " warning: apt-get update failed, using the package lists on disk"; \ $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ libboost-dev libglfw3-dev libgl-dev libsqlite3-dev \ python3-pil clang; \ else \ echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl sqlite3 python3-pil"; \ fi deps-secp256k1: @if [ ! -f /usr/local/lib/libsecp256k1.a ]; then \ echo "==> building libsecp256k1"; \ rm -rf $(ROOT)/secp256k1-build && \ git init -q $(ROOT)/secp256k1-build && \ cd $(ROOT)/secp256k1-build && \ git remote add origin $(SECP256K1_REPO) && \ git fetch -q --depth 1 origin $(SECP256K1_COMMIT) && \ git checkout -q --detach FETCH_HEAD && \ test "$$(git rev-parse HEAD)" = "$(SECP256K1_COMMIT)" && \ echo "==> libsecp256k1 pinned at $(SECP256K1_COMMIT)" && \ cd $(ROOT)/secp256k1-build && ./autogen.sh && \ ./configure --enable-module-schnorrsig --enable-module-extrakeys \ --disable-shared --with-pic --disable-benchmark --disable-tests && \ make -j$(NPROC) && $(SUDO) make install && $(SUDO) ldconfig && \ echo "==> libsecp256k1 done"; \ else \ echo "==> libsecp256k1 already installed"; \ fi deps-randomx: @ws="$$(printf ' \t')"; \ case "$(RANDOMX_DIR)" in \ "") echo "ERROR: RANDOMX_DIR is empty"; exit 1 ;; \ /) echo "ERROR: RANDOMX_DIR is /, refusing"; exit 1 ;; \ *[$$ws]*) echo "ERROR: RANDOMX_DIR contains whitespace: $(RANDOMX_DIR)"; exit 1 ;; \ *[*?[]*) echo "ERROR: RANDOMX_DIR contains a glob character: $(RANDOMX_DIR)"; exit 1 ;; \ /*) ;; \ *) echo "ERROR: RANDOMX_DIR must be an absolute path: $(RANDOMX_DIR)"; exit 1 ;; \ esac @if [ ! -f "$(RANDOMX_DIR)/build/librandomx.a" ]; then \ if [ -d "$(RANDOMX_DIR)" ] && [ ! -f "$(RANDOMX_DIR)/.bitflash-built" ]; then \ echo "ERROR: $(RANDOMX_DIR) exists but was not created by this build"; \ echo " refusing to rm -rf it -- remove it yourself or set RANDOMX_DIR"; \ exit 1; \ fi; \ echo "==> building RandomX"; \ rm -rf "$(RANDOMX_DIR)" && \ git init -q "$(RANDOMX_DIR)" && \ cd "$(RANDOMX_DIR)" && \ git remote add origin $(RANDOMX_REPO) && \ git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ git checkout -q --detach FETCH_HEAD && \ test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ mkdir -p "$(RANDOMX_DIR)/build" && \ cd "$(RANDOMX_DIR)/build" && cmake .. -DCMAKE_BUILD_TYPE=Release && \ make -j$(NPROC) randomx && \ touch "$(RANDOMX_DIR)/.bitflash-built" && \ echo "==> RandomX done"; \ else \ echo "==> RandomX already built"; \ fi # APPDIR and the tool both used fixed paths under /tmp, on a directory every # local account can write. `[ ! -x /tmp/appimagetool ]` means an executable # already sitting at that name is taken as the tool and run, so anyone with a # shell on the build machine could have put one there first and had it execute # as whoever ran make; `rm -rf /tmp/bitflash.AppDir` then mkdir is the same # problem one step later, through symlinks. Both now live under the build tree, # which the build already owns. APPDIR := $(ROOT)/build/bitflash.AppDir APPIMAGETOOL := $(ROOT)/build/appimagetool appimage: src/bitflash @echo "==> building AppImage" @mkdir -p $(ROOT)/build @if [ ! -x "$(APPIMAGETOOL)" ]; then \ echo " fetching appimagetool"; \ wget -q "$(APPIMAGETOOL_URL)" -O "$(APPIMAGETOOL).tmp" || \ { echo " ERROR: appimagetool download failed"; rm -f "$(APPIMAGETOOL).tmp"; exit 1; }; \ echo "$(APPIMAGETOOL_SHA256) $(APPIMAGETOOL).tmp" | sha256sum -c - >/dev/null 2>&1 || \ { echo " ERROR: appimagetool digest mismatch, refusing to run it"; \ echo " expected $(APPIMAGETOOL_SHA256)"; \ echo " got $$(sha256sum "$(APPIMAGETOOL).tmp" | cut -d" " -f1)"; \ rm -f "$(APPIMAGETOOL).tmp"; exit 1; }; \ chmod +x "$(APPIMAGETOOL).tmp" && mv "$(APPIMAGETOOL).tmp" "$(APPIMAGETOOL)"; \ fi @rm -rf $(APPDIR) @mkdir -p $(APPDIR)/usr/bin \ $(APPDIR)/usr/lib \ $(APPDIR)/usr/share/applications \ $(APPDIR)/usr/share/icons/hicolor/256x256/apps @cp src/bitflash $(APPDIR)/usr/bin/bitflash @printf '#!/bin/sh\nHERE="$$(dirname "$$(readlink -f "$$0")")"\nexport LD_LIBRARY_PATH="$$HERE/usr/lib:$$LD_LIBRARY_PATH"\nexec "$$HERE/usr/bin/bitflash" "$$@"\n' \ > $(APPDIR)/AppRun && chmod +x $(APPDIR)/AppRun @printf '[Desktop Entry]\nName=Bitflash\nExec=bitflash\nIcon=bitflash\nType=Application\nCategories=Finance;\n' \ > $(APPDIR)/bitflash.desktop @cp $(APPDIR)/bitflash.desktop \ $(APPDIR)/usr/share/applications/ @APPDIR=$(APPDIR) python3 -c "\ import os; \ from PIL import Image; \ d = os.environ['APPDIR']; \ ico = Image.open('src/rc/bitcoin.ico'); \ img = ico.convert('RGBA'); \ img.save(d + '/bitflash.png'); \ import shutil; shutil.copy(d + '/bitflash.png', \ d + '/usr/share/icons/hicolor/256x256/apps/bitflash.png')" @for lib in libsodium.so.23 libdb_cxx-5.3.so libsqlite3.so.0 libglfw.so.3; do \ path=$$(ldconfig -p | grep " $$lib " | awk '{print $$NF}' | head -1); \ [ -z "$$path" ] && path=$$(find /usr/lib /lib -name "$$lib" 2>/dev/null | head -1); \ [ -n "$$path" ] && cp -L "$$path" $(APPDIR)/usr/lib/ && echo " bundled $$lib" || echo " missing $$lib"; \ done # appimagetool's output used to be piped through grep ... || true, which # discarded its exit status: the target succeeded and announced "Built:" # even when no AppImage existed. Run the tool bare so its status decides, # then require the artefact to exist, be non-empty, and be newer than the # binary it was packaged from before saying so. @ARCH=x86_64 $(APPIMAGETOOL) --no-appstream $(APPDIR) \ $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage @test -s $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage || \ { echo "ERROR: Bitflash-$(VERSION)-x86_64.AppImage missing or empty"; exit 1; } @test $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage -nt src/bitflash || \ { echo "ERROR: Bitflash-$(VERSION)-x86_64.AppImage is not newer than src/bitflash"; exit 1; } @echo "Built: Bitflash-$(VERSION)-x86_64.AppImage" # ---- Windows (MSYS2 UCRT64) ----------------------------------------------- deps-windows: pacman -S --noconfirm --needed \ mingw-w64-ucrt-x86_64-gcc \ mingw-w64-ucrt-x86_64-make \ mingw-w64-ucrt-x86_64-glfw \ mingw-w64-ucrt-x86_64-openssl \ mingw-w64-ucrt-x86_64-db \ mingw-w64-ucrt-x86_64-sqlite3 \ mingw-w64-ucrt-x86_64-libsodium \ mingw-w64-ucrt-x86_64-nlohmann-json \ mingw-w64-ucrt-x86_64-boost \ mingw-w64-ucrt-x86_64-cmake \ autoconf automake libtool @if [ ! -f deps/lib/libsecp256k1.a ]; then \ rm -rf secp256k1-build && \ git init -q secp256k1-build && \ cd secp256k1-build && \ git remote add origin $(SECP256K1_REPO) && \ git fetch -q --depth 1 origin $(SECP256K1_COMMIT) && \ git checkout -q --detach FETCH_HEAD && \ test "$$(git rev-parse HEAD)" = "$(SECP256K1_COMMIT)" && \ echo "==> secp256k1 pinned at $(SECP256K1_COMMIT)" && \ cd $(ROOT)/secp256k1-build && ./autogen.sh && \ ./configure --prefix=$(ROOT)/deps \ --enable-module-schnorrsig --enable-module-extrakeys \ --disable-shared --with-pic --disable-benchmark --disable-tests && \ make install && \ echo "==> secp256k1 done"; \ else \ echo "==> secp256k1 already built"; \ fi @if [ ! -f RandomX/build/librandomx.a ]; then \ rm -rf RandomX && \ git init -q RandomX && \ cd RandomX && \ git remote add origin $(RANDOMX_REPO) && \ git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ git checkout -q --detach FETCH_HEAD && \ test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ mkdir -p $(ROOT)/RandomX/build && \ cd $(ROOT)/RandomX/build && cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release && \ make randomx && \ echo "==> RandomX done"; \ else \ echo "==> RandomX already built"; \ fi windows: deps-windows cd src && make -f makefile.mingw @rm -rf Bitflash-$(VERSION)-windows @mkdir -p Bitflash-$(VERSION)-windows @cp src/bitflash.exe Bitflash-$(VERSION)-windows/Bitflash.exe @ldd src/bitflash.exe | grep ucrt64 | awk '{print $$3}' | \ xargs -I{} cp {} Bitflash-$(VERSION)-windows/ 2>/dev/null || true @zip -r Bitflash-$(VERSION)-windows.zip Bitflash-$(VERSION)-windows/ @echo "Built: Bitflash-$(VERSION)-windows.zip" windows-tor: windows ./scripts/package-windows-tor.sh # ---- Clean ---------------------------------------------------------------- clean: $(MAKE) -C src -f Makefile clean 2>/dev/null || true cd src && make -f makefile.mingw clean 2>/dev/null || true rm -f Bitflash-*.AppImage Bitflash-*.zip rm -rf Bitflash-*-windows build tests: $(MAKE) -C src -f Makefile tests fuzz-net-message-smoke: deps-linux $(MAKE) -C src -f Makefile fuzz-net-message-smoke fuzz-script-smoke: deps-linux $(MAKE) -C src -f Makefile fuzz-script-smoke checksums: ./scripts/make-release-checksums.sh sign-checksums: ./scripts/make-release-checksums.sh --sign $(if $(KEY),--local-user $(KEY),) verify-release: ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) .PHONY: linux windows windows-tor clean appimage \ tests fuzz-net-message-smoke fuzz-script-smoke checksums sign-checksums verify-release \ deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx