Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ target/

# Python bytecode from the vibes console
__pycache__/

# macOS app build output
/dist/
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ contact with the compiler.
Install
-------

**On a Mac, and you would rather not see a terminal?**
[Download Bitcoin Vibes.app](https://github.com/hotpixelgroup/bitcoin/releases/latest),
drag it to Applications, open it. It does the rest and shows you what it is doing
in your browser. (It is not notarised yet, so the first time you will need to
right-click it and choose **Open** — macOS asks that of every small developer.)

**Everyone else, one line:**

```bash
curl -fsSL https://raw.githubusercontent.com/hotpixelgroup/bitcoin/master/contrib/vibes/install.sh | sh
```
Expand Down
137 changes: 124 additions & 13 deletions contrib/vibes/bitcoin-vibes
Original file line number Diff line number Diff line change
Expand Up @@ -1246,9 +1246,37 @@ class Console:
self.node_proc = subprocess.Popen(
[str(bitcoind), *self.chain_args(), "-server=1"],
stdout=self.node_log, stderr=self.node_log, start_new_session=True)

# Spawning is not starting. bitcoind exits early on a port already in
# use, a locked datadir or a full disk, and reporting success then
# leaves the operator staring at a node that is not there.
deadline = time.time() + 45
while time.time() < deadline:
if self.cli_json("uptime") is not None:
if job:
job.emit(t="phase", name="node",
detail=f"bitcoind started (pid {self.node_proc.pid})")
return True
if self.node_proc.poll() is not None:
break # it gave up before we did
time.sleep(0.5)

why = self._last_node_error() or "see .vibes/logs/bitcoind.log"
if job:
job.emit(t="phase", name="node", detail=f"bitcoind started (pid {self.node_proc.pid})")
return True
job.emit(t="log", line=f"bitcoind did not come up — {why}")
return False

def _last_node_error(self):
"""The most recent complaint bitcoind made, for reporting upward."""
try:
lines = (self.logs_dir / "bitcoind.log").read_text(
errors="replace").splitlines()
except OSError:
return None
for line in reversed(lines[-60:]):
if "Error:" in line or "[error]" in line:
return line.split("Error:")[-1].split("[error]")[-1].strip()
return None

def stop_node(self, job=None, timeout=30):
if not self.node_running():
Expand Down Expand Up @@ -1305,6 +1333,80 @@ class Console:
self._restart_node(job)
return go

def setup_runner(self):
"""First run, streamed into the browser instead of a terminal.

The desktop app has no terminal to print to, so preparing the node —
dependencies, configure, build, first start — runs as an ordinary job
and the operator watches it in the console like anything else."""
def go(job):
job.emit(t="phase", name="setup",
detail="preparing your node — this happens once")

missing = self.missing_deps()
if missing:
job.emit(t="phase", name="deps",
detail="installing what the compiler needs: " + ", ".join(missing))
if not self.install_deps(job, missing):
job.state = "error"
job.emit(t="phase", name="error",
detail="could not install: " + ", ".join(missing) +
". Install them by hand and reopen Bitcoin Vibes.")
return
else:
job.emit(t="log", line="build tools already present")

jobs = self.build_jobs()
low, high = self.build_estimate(jobs)
job.emit(t="phase", name="build",
detail=f"compiling the node with {jobs} parallel job(s) — "
f"roughly {low}-{high} minutes on this machine, once")
if not self._build(job):
return
job.emit(t="phase", name="node", detail="waking the node")
if not self.start_node(job):
job.state = "error"
job.emit(t="phase", name="error",
detail="The node was built, but it would not start. The usual "
"cause is another Bitcoin node already using this "
"network's ports. Full reason in the line above.")
return
job.emit(t="result",
detail="Your node is ready, Excellency. It has been waiting.")
return go

def missing_deps(self):
"""Which build tools are absent. Empty means we can compile."""
need = []
if not shutil.which("cmake"):
need.append("cmake")
if sys.platform == "darwin":
for formula, probe in (("boost", "/boost/version.hpp"),
("capnp", None)):
if formula == "capnp" and not shutil.which("capnp"):
need.append("capnp")
elif formula == "boost":
found = any(Path(p + probe).exists() for p in
("/opt/homebrew/include", "/usr/local/include"))
if not found:
need.append("boost")
return need

def install_deps(self, job, missing):
"""Best effort, and honest when it cannot."""
if sys.platform == "darwin":
brew = shutil.which("brew") or next(
(p for p in ("/opt/homebrew/bin/brew", "/usr/local/bin/brew")
if Path(p).exists()), None)
if not brew:
job.emit(t="log", line="Homebrew is not installed — it is how macOS "
"gets these. See https://brew.sh")
return False
return self._stream(job, [brew, "install", *missing]) == 0
job.emit(t="log", line="install these with your package manager, then reopen: "
+ " ".join(missing))
return False

def build_runner(self, autorestart):
def go(job):
ok = self._build(job)
Expand Down Expand Up @@ -1539,6 +1641,9 @@ def main():
ap.add_argument("--no-open", action="store_true", help="don't open the browser")
ap.add_argument("--no-autostart", dest="autostart", action="store_false",
help="don't build/start the node automatically on launch")
ap.add_argument("--app", action="store_true",
help="launched from the desktop app: no terminal is watching, so "
"first-run setup streams into the browser instead")
ap.add_argument("--print-key", action="store_true", help="print the session key (for scripting)")
opts = ap.parse_args()

Expand Down Expand Up @@ -1612,18 +1717,24 @@ def main():
# the node, so the browser opens onto something alive.
if opts.autostart:
if not console.bin_path("bitcoind"):
bjobs = console.build_jobs()
low, high = console.build_estimate(bjobs)
print(f" no node binary yet — building it now, once, with {bjobs} "
f"parallel job(s).")
print(f" on this machine expect roughly {low}-{high} minutes. "
f"It is the last slow thing that will ever happen.")
print()
if console.build_blocking():
print("\n build complete.\n")
if opts.app:
# Launched by double-click: there is no terminal to watch, so
# preparation streams into the browser instead.
print(" no node binary yet — preparing it in the console.")
console.start_job("setup", "", console.setup_runner())
else:
print("\n ⚠ build failed — see the output above. The console will still "
"start.\n")
bjobs = console.build_jobs()
low, high = console.build_estimate(bjobs)
print(f" no node binary yet — building it now, once, with {bjobs} "
f"parallel job(s).")
print(f" on this machine expect roughly {low}-{high} minutes. "
f"It is the last slow thing that will ever happen.")
print()
if console.build_blocking():
print("\n build complete.\n")
else:
print("\n ⚠ build failed — see the output above. The console "
"will still start.\n")
if console.bin_path("bitcoind") and not console.node_running():
if console.start_node():
print(f" node : awake on {opts.chain}")
Expand Down
44 changes: 44 additions & 0 deletions contrib/vibes/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,22 @@
.rule::after{background:linear-gradient(90deg,var(--gold-leaf),transparent)}
.rule span{color:var(--gold-ink);font-size:14px;letter-spacing:.4em}

/* ---- first run ---- */
.setup{border:2px solid var(--gold-leaf);border-radius:120px 120px 3px 3px;
background:linear-gradient(180deg,#fffdf8,var(--marble-2));
padding:34px 32px 26px;margin-bottom:22px;text-align:center;
box-shadow:0 12px 36px rgba(28,24,20,.09)}
.setup-title{font-family:var(--serif);font-size:21px;letter-spacing:.14em;
text-transform:uppercase;color:var(--gold-ink);margin-bottom:14px}
.setup-body{max-width:620px;margin:0 auto 20px;color:var(--ink);font-size:16.5px}
.setup-bar{height:8px;background:var(--marble-3);border:1px solid var(--gold-line);
border-radius:99px;overflow:hidden;max-width:520px;margin:0 auto}
.setup-bar span{display:block;height:100%;width:0;
background:linear-gradient(90deg,var(--gold-leaf),var(--gold-ink));
transition:width .6s ease}
.setup-step{margin-top:12px;font-family:var(--mono);font-size:13.5px;color:var(--ink-2)}
body.is-setup .tablet,body.is-setup #book-section{opacity:.45;pointer-events:none}

/* ---- the engine notice ---- */
.engine{border:2px solid var(--porphyry);background:#fdf6f4;border-radius:2px;
padding:15px 18px;margin-bottom:20px}
Expand Down Expand Up @@ -356,6 +372,16 @@ <h1>You are a Bitcoin God.</h1>
one man's mood. <b>You ask permission of no one.</b></p>
<div class="rule" role="presentation"><span aria-hidden="true">✦</span></div>

<div id="setup" class="setup" role="status" aria-live="polite" style="display:none">
<div class="setup-title">Preparing your node</div>
<p class="setup-body" id="setup-body">This happens once. Bitcoin Vibes is
compiling a real Bitcoin node from source — the same source Bitcoin Core
ships, plus a text box. You can watch it below, or come back in a few
minutes; nothing is required of you.</p>
<div class="setup-bar" aria-hidden="true"><span id="setup-fill"></span></div>
<p class="setup-step" id="setup-step">starting…</p>
</div>

<div id="engine" class="engine" role="alert" style="display:none">
<div class="engine-msg" id="engine-msg"></div>
<div class="engine-fix">Run this once, then return: <code id="engine-cmd"></code></div>
Expand Down Expand Up @@ -509,6 +535,16 @@ <h2 id="book-h">The Book of Decrees
function renderEvent(ev){
switch (ev.t) {
case "phase": {
// first-run setup drives the banner as well as the log
const setupSteps = {setup:[5,"getting ready"], deps:[15,"installing build tools"],
build:[35,"compiling the node — the slow part"],
node:[92,"waking the node"], done:[100,"ready"],
error:[100,"something went wrong"]};
if (setupSteps[ev.name] && document.body.classList.contains("is-setup")) {
const [pct, label] = setupSteps[ev.name];
$("setup-fill").style.width = pct + "%";
$("setup-step").textContent = ev.detail || label;
}
const names = {agent:"⛩ your word descends upon the codebase, which had been waiting",
commit:"✒ inscribing your decree where it can never be argued with",
build:"⚒ reality recompiles itself to your specification",
Expand Down Expand Up @@ -754,6 +790,14 @@ <h2 id="book-h">The Book of Decrees
$("nodebtn").dataset.action = n.running ? "stop" : "start";
$("minebtn").style.display = (n.running && s.chain === "regtest") ? "inline-block" : "none";

// first run: no node binary yet, so the console explains itself instead of
// presenting a decree box for a node that does not exist
const setting_up = !s.binary || (s.job && s.job.kind === "setup" && s.job.state === "running");
document.body.classList.toggle("is-setup", !!setting_up);
$("setup").style.display = setting_up ? "block" : "none";
if (!setting_up && $("setup-fill").style.width && $("setup-fill").style.width !== "100%")
$("setup-fill").style.width = "100%";

const eng = s.engine_ready || {ok:true};
$("engine").style.display = eng.ok ? "none" : "block";
if (!eng.ok){
Expand Down
78 changes: 78 additions & 0 deletions contrib/vibes/macos/build-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/sh
# Build "Bitcoin Vibes.app" — a double-clickable macOS launcher.
#
# sh contrib/vibes/macos/build-app.sh -> dist/Bitcoin Vibes.app
# sh contrib/vibes/macos/build-app.sh --dmg -> also dist/BitcoinVibes.dmg
#
# The bundle is a launcher, not the node. On first open it fetches the source
# into ~/Library/Application Support/Bitcoin Vibes and the console compiles the
# node there, so the app stays small and updates itself.
set -eu

cd "$(dirname "$0")/../../.."
ROOT="$PWD"
OUT="$ROOT/dist"
APP="$OUT/Bitcoin Vibes.app"
VERSION=$(sed -n 's/^set(CLIENT_VERSION_MAJOR \([0-9]*\))/\1/p' CMakeLists.txt).$(sed -n 's/^set(CLIENT_VERSION_MINOR \([0-9]*\))/\1/p' CMakeLists.txt)

echo "→ building Bitcoin Vibes.app $VERSION"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"

cat > "$APP/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key> <string>Bitcoin Vibes</string>
<key>CFBundleDisplayName</key> <string>Bitcoin Vibes</string>
<key>CFBundleIdentifier</key> <string>group.hotpixel.bitcoinvibes</string>
<key>CFBundleVersion</key> <string>$VERSION</string>
<key>CFBundleShortVersionString</key><string>$VERSION</string>
<key>CFBundleExecutable</key> <string>BitcoinVibes</string>
<key>CFBundleIconFile</key> <string>AppIcon</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>LSMinimumSystemVersion</key> <string>12.0</string>
<key>LSApplicationCategoryType</key> <string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key> <string>MIT. A fork of Bitcoin Core, maintained by Hot Pixel Group.</string>
<!-- The console is a local web app; the node runs headless behind it. -->
<key>LSUIElement</key> <true/>
</dict>
</plist>
PLIST

install -m 755 contrib/vibes/macos/launcher.sh "$APP/Contents/MacOS/BitcoinVibes"

# Icon: reuse the site's mark so the dock, the tab and the social card agree.
ICON_SRC="$ROOT/contrib/vibes/macos/icon.png"
if [ -f "$ICON_SRC" ]; then
ICONSET=$(mktemp -d)/AppIcon.iconset
mkdir -p "$ICONSET"
for s in 16 32 128 256 512; do
sips -z $s $s "$ICON_SRC" --out "$ICONSET/icon_${s}x${s}.png" >/dev/null
d=$((s * 2))
sips -z $d $d "$ICON_SRC" --out "$ICONSET/icon_${s}x${s}@2x.png" >/dev/null
done
iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns"
echo " icon built"
else
echo " note: no contrib/vibes/macos/icon.png — the app will use a blank icon"
fi

# Ad-hoc signature. Not notarised: Gatekeeper will still want a right-click →
# Open the first time, which the README says plainly rather than pretending.
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 \
&& echo " ad-hoc signed" || echo " note: could not sign (the app still runs)"

echo "→ $APP"

if [ "${1:-}" = "--dmg" ]; then
DMG="$OUT/BitcoinVibes.dmg"
rm -f "$DMG"
STAGE=$(mktemp -d)
cp -R "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
hdiutil create -volname "Bitcoin Vibes" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null
rm -rf "$STAGE"
echo "→ $DMG"
fi
Binary file added contrib/vibes/macos/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions contrib/vibes/macos/launcher.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/sh
# The executable inside Bitcoin Vibes.app.
#
# Double-clicked from Finder there is no terminal, no PATH worth trusting and
# nowhere to print an error. So this script does the smallest possible amount of
# work — find a source tree, find python, hand off — and reports anything that
# goes wrong in a native dialog rather than into the void.
set -u

APP_SUPPORT="${VIBES_HOME:-$HOME/Library/Application Support/Bitcoin Vibes}"
SRC="$APP_SUPPORT/source"
LOG="$APP_SUPPORT/launch.log"
REPO="${VIBES_REPO:-https://github.com/hotpixelgroup/bitcoin.git}"
BRANCH="${VIBES_BRANCH:-master}"

mkdir -p "$APP_SUPPORT"
exec 2>>"$LOG"
echo "--- launch $(date)" >>"$LOG"

# Finder gives us a minimal PATH; put the usual homes for Homebrew and
# user-installed tools back so git, cmake and claude can be found.
PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export PATH

say() { osascript -e "display dialog \"$1\" with title \"Bitcoin Vibes\" buttons {\"OK\"} default button 1 with icon note" >/dev/null 2>&1; }
die() { osascript -e "display dialog \"$1\" with title \"Bitcoin Vibes\" buttons {\"OK\"} default button 1 with icon stop" >/dev/null 2>&1; exit 1; }

PY=$(command -v python3 || true)
[ -n "$PY" ] || die "Bitcoin Vibes needs Python 3, which normally ships with macOS.\n\nOpening Terminal and running: xcode-select --install\n\nwill install it along with the developer tools."

# First run: fetch the source. Everything else assumes it is there.
if [ ! -d "$SRC/.git" ]; then
if ! command -v git >/dev/null 2>&1; then
osascript -e 'display dialog "Bitcoin Vibes needs Apple'"'"'s command line tools (they include git and the compiler).\n\nClick OK and macOS will offer to install them. Reopen Bitcoin Vibes once it finishes." with title "Bitcoin Vibes" buttons {"OK"} default button 1 with icon note' >/dev/null 2>&1
xcode-select --install >/dev/null 2>&1
exit 0
fi
say "Bitcoin Vibes is fetching the node's source code.\n\nThis takes a minute. The console will open by itself when it is ready."
git clone --depth 1 --branch "$BRANCH" "$REPO" "$SRC" >>"$LOG" 2>&1 \
|| die "Could not download Bitcoin Vibes.\n\nCheck your internet connection and try again.\n\nDetails: $LOG"
else
# Keep up to date, but never clobber decrees the operator made locally.
git -C "$SRC" fetch --depth 1 origin "$BRANCH" >>"$LOG" 2>&1 || true
git -C "$SRC" merge --ff-only FETCH_HEAD >>"$LOG" 2>&1 || true
fi

CONSOLE="$SRC/contrib/vibes/bitcoin-vibes"
[ -f "$CONSOLE" ] || die "The Bitcoin Vibes source looks incomplete.\n\nDelete this folder and reopen the app:\n$SRC"

# Hand over. --app makes first-run setup stream into the browser, because from
# here there is no terminal for it to print to.
exec "$PY" -u "$CONSOLE" --app "$@" >>"$LOG" 2>&1
Loading
Loading