diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92d2808a7cc3..1dfdc61c9b7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,9 +34,12 @@ jobs: - os: macos-15 label: macOS (Apple silicon) slug: macos-arm64 - - os: ubuntu-latest + - os: ubuntu-24.04 label: Linux (x86_64) slug: linux-x86_64 + - os: ubuntu-24.04-arm + label: Linux (ARM64) + slug: linux-arm64 steps: - uses: actions/checkout@v4 @@ -67,7 +70,10 @@ jobs: - name: Build run: | - cmake -B build -G Ninja \ + # Release, not the default RelWithDebInfo: on Linux that embedded + # 276MB of DWARF straight into the ELF (macOS hid the problem by + # keeping debug info in a separate .dSYM). + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTS=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF -DBUILD_GUI=OFF cmake --build build -j"$(getconf _NPROCESSORS_ONLN)" @@ -87,13 +93,23 @@ jobs: test "$(./build/bin/bitcoin-cli -datadir="$DD" -chain=regtest getblockcount)" = "101" ./build/bin/bitcoin-cli -datadir="$DD" -chain=regtest stop - # Strip before packaging: the Linux artifact was 132MB of mostly debug - # symbols, which is a long download for something we hand to beginners. - - name: Strip + # Strip, and prove it worked. The previous version swallowed every error + # into "2>/dev/null || true" and shipped a 292MB binary — 276MB of it + # debug sections — while reporting success. + - name: Strip and check the size run: | - strip build/bin/bitcoind build/bin/bitcoin-cli 2>/dev/null || \ - strip -S build/bin/bitcoind build/bin/bitcoin-cli 2>/dev/null || true - ls -lh build/bin/bitcoind + set -e + strip -S build/bin/bitcoind build/bin/bitcoin-cli || \ + strip build/bin/bitcoind build/bin/bitcoin-cli + ls -lh build/bin/bitcoind build/bin/bitcoin-cli + # A Raspberry Pi on home broadband is the worst audience to hand a + # bloated download, so make the bloat a red X on our run instead. + BYTES=$(wc -c < build/bin/bitcoind) + echo "bitcoind is $((BYTES / 1048576)) MiB" + if [ "$BYTES" -gt 62914560 ]; then + echo "::error::bitcoind is $((BYTES / 1048576)) MiB — expected well under 60. Debug info is probably still embedded." + exit 1 + fi # The README offers "download the app" from /releases/latest, so every # release has to carry it — otherwise that link 404s the moment a diff --git a/contrib/vibes/bitcoin-vibes b/contrib/vibes/bitcoin-vibes index 64e5d8580651..31ec763f8f5e 100755 --- a/contrib/vibes/bitcoin-vibes +++ b/contrib/vibes/bitcoin-vibes @@ -1032,6 +1032,51 @@ class Console: "subject": parts[2][len("vibe: "):] if parts[2].startswith("vibe: ") else parts[2]}) return vibes + # The trailer the console appends to every decree commit; stripped again + # when reading one back out so a shared decree is the operator's words only. + DECREE_TRAILER = "Decreed through the Bitcoin Vibes console." + + def decree(self, sha): + """Everything needed to promulgate a decree: the wish, and honest + provenance. Note what is NOT here — no patch. A decree travels as a + wish, so the recipient's own agent writes their own code under their own + guardrails. Shipping a diff would turn 'a stranger suggests words to + your agent' into 'a stranger ships C++ into your Bitcoin node'.""" + text = self.decree_text(sha) + if text is None: + return None + _, when, _ = self.git("log", "-1", "--format=%ct", sha) + _, patch, _ = self.git("show", "--format=", "--unified=0", sha) + _, stat, _ = self.git("show", "--format=", "--numstat", sha) + files = [] + for line in stat.splitlines(): + parts = line.split("\t") + if len(parts) == 3: + add, rem, path = parts + files.append([path, 0 if add == "-" else int(add), + 0 if rem == "-" else int(rem)]) + return {"v": 1, "p": text, "sha": sha[:12], + "ts": int(when.strip() or 0), + "c": self._judge_consensus(patch), + "f": files[:12]} + + def decree_text(self, sha): + """The full wish behind a decree, as originally typed. + + The subject line in the history is truncated to 72 characters, which is + fine for a list and useless for sharing — the whole point is to pass on + exactly what was asked for.""" + if not re.fullmatch(r"[0-9a-f]{7,40}", sha or ""): + return None + code, subject, _ = self.git("log", "-1", "--format=%s", sha) + if code != 0 or not subject.strip().startswith("vibe:"): + return None # only decrees are shareable + _, body, _ = self.git("log", "-1", "--format=%b", sha) + text = body.split(self.DECREE_TRAILER)[0].strip() + if not text: # very old decrees had no body + text = subject.strip()[len("vibe: "):] + return text + # ----- job engine ---------------------------------------------------- def start_job(self, kind, prompt="", runner=None): @@ -1597,7 +1642,11 @@ class Console: PREBUILT_SLUGS = {("Darwin", "arm64"): "macos-arm64", ("Darwin", "x86_64"): "macos-x86_64", - ("Linux", "x86_64"): "linux-x86_64"} + ("Linux", "x86_64"): "linux-x86_64", + # platform.machine() says aarch64 on most ARM Linux; + # a few distros report arm64. + ("Linux", "aarch64"): "linux-arm64", + ("Linux", "arm64"): "linux-arm64"} @staticmethod def host_machine(): @@ -1798,6 +1847,13 @@ class Handler(BaseHTTPRequestHandler): return if path == "/api/status": self._send(200, self.console.status()) + elif path == "/api/decree": + sha = parse_qs(urlparse(self.path).query).get("sha", [""])[0] + if not re.fullmatch(r"[0-9a-f]{7,40}", sha): + self._send(400, {"error": "bad sha"}) + return + d = self.console.decree(sha) + self._send(200, d) if d else self._send(404, {"error": "not a decree"}) elif path == "/api/settings": self._send(200, {"schema": self.console.all_specs(), "groups": self.console.all_groups(), diff --git a/contrib/vibes/index.html b/contrib/vibes/index.html index c090d687ac94..dfc5fd672221 100644 --- a/contrib/vibes/index.html +++ b/contrib/vibes/index.html @@ -302,6 +302,62 @@ .settings-note.good{color:var(--verdigris);font-weight:600} #settings-save{margin-left:auto} + + /* ---- promulgating and receiving decrees ---- */ + .share{cursor:pointer;border:1px solid var(--gold-line);background:var(--marble); + color:var(--ink-2);border-radius:2px;padding:8px 14px;font-family:var(--serif); + font-size:13px;letter-spacing:.1em;text-transform:uppercase;min-height:38px} + .share:hover{color:#fffdf8;background:var(--gold-ink);border-color:var(--gold-ink)} + .sharepanel{flex:1 1 100%;border:1px solid var(--gold-line);background:var(--marble); + border-radius:3px;padding:16px 18px;margin-top:4px} + .sharepanel h4{font-family:var(--serif);font-size:14px;letter-spacing:.12em; + text-transform:uppercase;color:var(--gold-ink);margin-bottom:8px;font-weight:600} + .sharepanel p{font-size:15px;color:var(--ink-2);margin-bottom:12px} + .sharepanel label{display:block;font-size:14px;color:var(--ink);margin:10px 0 4px} + .sharepanel input[type=text]{width:100%;background:#fffdf8;border:1px solid var(--gold-line); + border-radius:2px;padding:9px 11px;font:15px var(--text);color:var(--ink);min-height:42px} + .sharepanel .row{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px} + .sharepanel .row button{flex:1 1 auto;min-height:42px} + .sharemeter{font-family:var(--mono);font-size:12.5px;color:var(--ink-2);margin-top:8px} + .sharemeter.over{color:var(--porphyry);font-weight:700} + .shareout{width:100%;min-height:80px;margin-top:12px;font:12.5px var(--mono); + background:#fffdf8;border:1px solid var(--gold-line);border-radius:2px;padding:10px; + color:var(--ink);user-select:all;white-space:pre-wrap;word-break:break-all} + + /* an arriving decree: the shape reserved for moments of consequence */ + .offering{border:2px solid var(--gold-leaf);border-radius:120px 120px 3px 3px; + background:linear-gradient(180deg,#fffdf8,var(--marble-2)); + padding:34px 30px 26px;margin-bottom:22px; + box-shadow:0 12px 36px rgba(28,24,20,.09)} + .offering .eyebrow{font-family:var(--serif);font-size:12.5px;letter-spacing:.26em; + text-transform:uppercase;color:var(--gold-ink);text-align:center;margin-bottom:14px} + .offering h3{font-family:var(--serif);font-size:21px;font-weight:400;text-align:center; + color:var(--ink);margin-bottom:14px;line-height:1.3} + .offering .prov{font-family:var(--mono);font-size:12.5px;color:var(--ink-2); + text-align:center;margin-bottom:4px} + .offering .unproved{font-size:14px;color:var(--ink-2);text-align:center; + margin-bottom:16px;font-style:italic} + .offering .full{white-space:pre-wrap;unicode-bidi:isolate;direction:ltr; + font-family:var(--mono);font-size:13.5px;line-height:1.7;color:var(--ink); + background:#fffdf8;border:1px solid var(--gold-line);border-left:3px solid var(--gold-leaf); + border-radius:2px;padding:14px 16px;margin-bottom:14px;max-height:340px;overflow:auto} + .offering .note{border-left:3px solid var(--ink-2);background:var(--marble-3); + padding:12px 15px;border-radius:2px;margin-bottom:14px;font-size:15.5px;color:var(--ink)} + .offering .danger{border:2px solid var(--porphyry);background:#fdf6f4;color:var(--porphyry); + border-radius:2px;padding:12px 15px;margin-bottom:14px;font-size:15px;font-weight:600} + .offering .consequence{font-size:15.5px;color:var(--ink);margin-bottom:18px; + text-align:center;max-width:640px;margin-left:auto;margin-right:auto} + .offering .acts{display:flex;gap:12px;justify-content:center;flex-wrap:wrap} + .offering .acts .go{margin-left:0} + #wish-origin{display:none;font-size:14.5px;color:var(--porphyry); + background:#fdf6f4;border:1px solid var(--porphyry);border-radius:2px; + padding:9px 12px;margin-bottom:9px} + @media (max-width:720px){ + .share{order:4;flex:1 1 42%} + .revert{order:5;flex:1 1 42%} + .offering{border-radius:70px 70px 3px 3px;padding:24px 16px 20px} + } + /* ---- the book of decrees ---- */ .book{background:#fffdf8;border:1px solid var(--gold-line);border-radius:3px;padding:4px 18px} .vibe{display:flex;align-items:center;gap:13px;padding:14px 0; @@ -395,6 +451,27 @@

You are a Bitcoin God.

+ +