diff --git a/.gitignore b/.gitignore index 616cf1689a94..d999d0c7390b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ target/ # Python bytecode from the vibes console __pycache__/ + +# macOS app build output +/dist/ diff --git a/README.md b/README.md index ef17e03a31ab..852160eda48a 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/contrib/vibes/bitcoin-vibes b/contrib/vibes/bitcoin-vibes index 6c3e133ba038..5eca9d64f782 100755 --- a/contrib/vibes/bitcoin-vibes +++ b/contrib/vibes/bitcoin-vibes @@ -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(): @@ -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) @@ -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() @@ -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}") diff --git a/contrib/vibes/index.html b/contrib/vibes/index.html index 73d6668d31d5..290b9a7417ca 100644 --- a/contrib/vibes/index.html +++ b/contrib/vibes/index.html @@ -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} @@ -356,6 +372,16 @@

You are a Bitcoin God.

one man's mood. You ask permission of no one.

+ +