7 Commits
Author SHA1 Message Date
beefcarver 6dff7700e1 docs(agents): document Homebrew resource blocks and --check drift reporting 2026-11-15 11:30:33 -05:00
beefcarver 4c2f498e41 feat(packaging): check Homebrew formula has a resource per runtime dependency 2026-11-15 11:30:29 -05:00
beefcarver 86b22226e2 build: bump tap to vendored resources 2026-11-15 11:24:48 -05:00
beefcarver fffeb7f227 chore(homebrew): bump tap submodule to the 1.2.5 formula 2026-11-15 10:17:01 -05:00
beefcarver 4d5156f7d3 build(aur): bump to 1.2.5 and build from the release sdist 2026-11-15 10:16:58 -05:00
beefcarver 4fd688e673 fix(packaging): verify the release download against a CA bundle that exists
python-build-standalone's OpenSSL has default trust paths that need not
exist on the host, so urlopen failed with "unable to get local issuer
certificate" while browsers and curl accepted the same chain.  Fall back
to certifi when OpenSSL's default cafile is missing, and name the bundle
in use when verification still fails so the next occurrence is one line
to diagnose.
2026-11-15 09:51:01 -05:00
beefcarver 512c9a663f fix(packaging): resolve repo root from script directory 2026-11-15 09:45:48 -05:00
5 changed files with 112 additions and 20 deletions

No files matched your search

+7 -2
View File
@@ -52,8 +52,13 @@ repo-wide reformat as a side effect of an unrelated change.
- **`contrib/homebrew/`** — a **git submodule** of the tap at
`https://tildeforge.dev/beefcarver/homebrew`, not a copy. Formula changes
belong in the tap repository; commit them there and bump the gitlink here.
Never add loose files under that path. The formula still carries a
placeholder `sha256`, so it only installs with `--HEAD`.
Never add loose files under that path. The formula's Python dependencies are
vendored as `resource` blocks: Homebrew installs the sdist with `--no-deps`,
so a dependency that has no block (or whose constraint changed in
`pyproject.toml`) is simply missing at runtime. Regenerate them in the tap
with `brew update-python-resources Formula/b/beefy-player.rb`;
`uv run python bump_packaging.py <version> --check` reports a dependency
without one.
## Releases
+91 -2
View File
@@ -33,15 +33,18 @@ from __future__ import annotations
import argparse
import hashlib
import os
import re
import ssl
import subprocess
import sys
import tarfile
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ROOT = Path(__file__).resolve().parent
PYPROJECT = ROOT / "pyproject.toml"
INIT = ROOT / "beefy_player/__init__.py"
FLAKE = ROOT / "flake.nix"
@@ -193,6 +196,30 @@ def sha256(path: Path) -> str:
return digest.hexdigest()
def ca_file() -> str | None:
"""The CA bundle to verify with, or None to let OpenSSL use its own default.
`uv python install` fetches a python-build-standalone interpreter whose
OpenSSL is compiled with default trust paths that need not exist on the
host (on macOS, and on distros that keep certs only in
/etc/ssl/certs/ca-certificates.crt, they often do not). Verification then
fails with `unable to get local issuer certificate` for every site, even
though curl and browsers are happy. Fall back to certifi, which `httpx`
already puts in the environment.
"""
if os.environ.get("SSL_CERT_FILE"):
return None # ssl honours the variable itself; do not second-guess it
paths = ssl.get_default_verify_paths()
for candidate in (paths.cafile, paths.openssl_cafile):
if candidate and Path(candidate).is_file():
return None
try:
import certifi
except ModuleNotFoundError:
return None
return certifi.where()
def fetch_asset(version: str, override: Path | None) -> Path:
if override is not None:
if not override.is_file():
@@ -200,10 +227,25 @@ def fetch_asset(version: str, override: Path | None) -> Path:
return override
url = RELEASE_ASSET.format(version=version, name=ASSET_NAME)
say(f"downloading {url}")
cafile = ca_file()
context = ssl.create_default_context(cafile=cafile)
target = Path(tempfile.mkdtemp()) / f"{ASSET_NAME}-{version}.tar.gz"
try:
with urllib.request.urlopen(url) as response, target.open("wb") as handle:
with urllib.request.urlopen(url, context=context) as response, target.open(
"wb"
) as handle:
handle.write(response.read())
except urllib.error.URLError as error:
# urlopen wraps socket/SSL failures; the cause is in `.reason`.
if isinstance(error.reason, ssl.SSLCertVerificationError):
bundle = cafile or ssl.get_default_verify_paths().cafile or "no bundle found"
raise fail(
f"cannot fetch the release asset ({error.reason.verify_message or error.reason}); "
f"verified against {bundle}. If that bundle is stale, point SSL_CERT_FILE "
"at a current one (on Arch and Debian that is "
"/etc/ssl/certs/ca-certificates.crt)"
) from error
raise fail(f"cannot fetch the release asset ({error.reason}); is the tag published?") from error
except OSError as error:
raise fail(f"cannot fetch the release asset ({error}); is the tag published?") from error
return target
@@ -293,6 +335,50 @@ def bump_srcinfo() -> None:
say("regenerated contrib/aur/.SRCINFO")
#: Names that may appear as `resource` blocks in the formula. A Python
#: dependency without one is silently skipped at install time because Homebrew
#: installs the sdist `--no-deps`, which surfaces as an ImportError on launch
#: rather than as a packaging error.
RESOURCE_LINE = re.compile(r'(?m)^ resource "([^"]+)" do$')
def pypi_name(name: str) -> str:
"""PEP 503 name: lowercase, runs of -_. collapsed to a single hyphen."""
return re.sub(r"[-_.]+", "-", name).lower()
def runtime_dependencies() -> list[str]:
"""The distribution names in `pyproject.toml`'s `[project] dependencies`."""
block = re.search(r"(?ms)^dependencies = \[(.*?)^\]$", PYPROJECT.read_text())
if block is None:
raise fail("no dependencies array in pyproject.toml")
return re.findall(r'^\s*"([A-Za-z0-9._-]+)', block.group(1), re.MULTILINE)
def formula_excluded_resources() -> set[str]:
"""Dependencies the formula deliberately leaves to other formulae."""
match = re.search(r'pypi_packages .*exclude_packages: "([^"]*)"', FORMULA.read_text())
if match is None:
return set()
return {pypi_name(name.strip()) for name in match.group(1).split(",") if name.strip()}
def check_formula_resources(problems: list[str]) -> None:
"""Every runtime dependency needs a `resource`, unless another formula has it."""
resources = {pypi_name(name) for name in RESOURCE_LINE.findall(FORMULA.read_text())}
if not resources:
problems.append("formula vendors no Python resources; it would install --no-deps")
excluded = formula_excluded_resources()
for dependency in runtime_dependencies():
name = pypi_name(dependency)
if name in excluded or name in resources:
continue
problems.append(
f"formula has no resource for {name}"
" (run `brew update-python-resources Formula/b/beefy-player.rb` in the tap)"
)
def bump_formula(version: str, checksum: str) -> None:
text = FORMULA.read_text()
url = RELEASE_ASSET.format(version=version, name=ASSET_NAME)
@@ -302,6 +388,8 @@ def bump_formula(version: str, checksum: str) -> None:
)
# The default branch is master, and --HEAD is only useful if it resolves.
text = text.replace('branch: "main"', 'branch: "master"')
# The `resource` blocks pin versions, not the release: they only go stale
# when pyproject.toml's dependencies change, which `--check` reports.
FORMULA.write_text(text)
say("wrote contrib/homebrew/Formula/b/beefy-player.rb")
@@ -393,6 +481,7 @@ def check(version: str) -> int:
problems.append('formula head branch is "main"; this repository uses master')
if re.search(r'sha256 "0{64}"', formula):
problems.append("formula still has the placeholder sha256")
check_formula_resources(problems)
for problem in problems:
say(f"drift: {problem}")
+3 -3
View File
@@ -1,6 +1,6 @@
pkgbase = beefy-player
pkgdesc = Flashy terminal player for internet radio with a live spectrum analyser
pkgver = 1.2.2
pkgver = 1.2.5
pkgrel = 1
url = https://tildeforge.dev/beefcarver/beefy-player
arch = any
@@ -22,7 +22,7 @@ pkgbase = beefy-player
optdepends = libpulse: capture the output monitor so the analyser follows what you hear (parec/pactl)
optdepends = mpv: preferred audio backend
optdepends = tmux: detect the outer terminal for kitty graphics passthrough inside tmux
source = beefy-player-1.2.2.tar.gz::https://tildeforge.dev/beefcarver/beefy-player/archive/941c7446384bf346e40d1438a7ce67939d9615df.tar.gz
sha256sums = 8ac568676ef5ef8345441c28e4f8f5d61fb06d5225e566ac578acae18672c778
source = beefy-player-1.2.5.tar.gz::https://tildeforge.dev/beefcarver/beefy-player/releases/download/v1.2.5/beefy_player-1.2.5.tar.gz
sha256sums = 344f2266cb63d05bf294349599ecef636b3c8c25c8b29bcf0eff8818394b5f74
pkgname = beefy-player
+10 -12
View File
@@ -1,7 +1,7 @@
# Maintainer: beefy-player contributors <[email protected]>
pkgname=beefy-player
pkgver=1.2.2
pkgver=1.2.5
pkgrel=1
pkgdesc="Flashy terminal player for internet radio with a live spectrum analyser"
arch=('any')
@@ -33,27 +33,25 @@ optdepends=(
'tmux: detect the outer terminal for kitty graphics passthrough inside tmux'
)
# Upstream does not tag releases yet, so the source is the archive of the
# commit that carries this version. Once a `v$pkgver` tag exists, switch to
#
# source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
#
_commit=941c7446384bf346e40d1438a7ce67939d9615df
source=("$pkgname-$pkgver.tar.gz::$url/archive/$_commit.tar.gz")
sha256sums=('8ac568676ef5ef8345441c28e4f8f5d61fb06d5225e566ac578acae18672c778')
# The sdist attached to the v$pkgver release. bump_packaging.py keeps
# this and sha256sums in step with the tag; build/check/package use
# $_srcname because the sdist unpacks to hatchling's PEP 503 name.
_srcname="beefy_player-$pkgver"
source=("$pkgname-$pkgver.tar.gz::$url/releases/download/v$pkgver/$_srcname.tar.gz")
sha256sums=('344f2266cb63d05bf294349599ecef636b3c8c25c8b29bcf0eff8818394b5f74')
build() {
cd "$pkgname"
cd "$_srcname"
python -m build --wheel --no-isolation
}
check() {
cd "$pkgname"
cd "$_srcname"
# The suite is offline: catalogues are fixtures, mpv/parec are faked.
python -m pytest -q
}
package() {
cd "$pkgname"
cd "$_srcname"
python -m installer --destdir="$pkgdir" dist/*.whl
}