Build standalone binary with bunfig autoload disabled

Set autoloadBunfig: false in build.ts so the compiled runtime ignores any
bunfig.toml in the launching directory, preventing startup failures from a
CWD preload the standalone cannot resolve. Update release.yml, Makefile,
bunfig.toml, CONTRIBUTING.md, and README.md to match.
This commit is contained in:
2026-08-10 09:00:30 -04:00
parent 491a736c32
commit 2e69868ffc
6 changed files with 161 additions and 137 deletions

View File

@@ -66,12 +66,14 @@ jobs:
env: env:
DIST_TAR: podtui-${{ matrix.plat }}-${{ matrix.arch }}.tar.gz DIST_TAR: podtui-${{ matrix.plat }}-${{ matrix.arch }}.tar.gz
run: | run: |
# The embedded runtime reads the launching process's CWD bunfig.toml. # The binary is compiled with bunfig autoload disabled
# This repo's bunfig lists a preload the standalone can't resolve # (autoloadBunfig: false in build.ts), so it must boot even from a
# ("preload not found"), so kicking the binary from the workspace root # directory holding a bunfig.toml with a top-level preload the
# would falsely fail every build. cd into a clean dir first. # standalone can't resolve. Plant one to make this a real regression
# test for "preload not found".
SMOKE_DIR=$(mktemp -d) SMOKE_DIR=$(mktemp -d)
tar -xzf "dist/$DIST_TAR" -C "$SMOKE_DIR" tar -xzf "dist/$DIST_TAR" -C "$SMOKE_DIR"
printf 'preload = ["./definitely-missing.ts"]\n' > "$SMOKE_DIR/bunfig.toml"
cd "$SMOKE_DIR" cd "$SMOKE_DIR"
./podtui-*/podtui --version ./podtui-*/podtui --version

View File

@@ -38,6 +38,7 @@ The app is a TUI — it expects a real terminal (Ghostty, kitty, iTerm2,
| `bun run lint` | Type-check | | `bun run lint` | Type-check |
| `bun run build` | Bundle JS into `dist/` + copy native libs (the `podtui` npm script path) | | `bun run build` | Bundle JS into `dist/` + copy native libs (the `podtui` npm script path) |
| `make dist` | Compile the standalone binary + make the current platform's tarball | | `make dist` | Compile the standalone binary + make the current platform's tarball |
| `make dist-mac` / `make dist-linux` | Aliases for `dist` on their platform (CI runs these) |
| `make clean` | Remove `dist/` | | `make clean` | Remove `dist/` |
## Repository layout ## Repository layout
@@ -90,19 +91,21 @@ Cavacore smoke test: `bun tests/cavacore-smoke.ts`
## Gotchas (read before touching anything) ## Gotchas (read before touching anything)
1. **Never add a top-level `preload` to `bunfig.toml`.** 1. **The compiled binary must keep bunfig autoload disabled.**
A compiled PodTui binary's embedded runtime reads the *launching process's* `build.ts` compiles the standalone with `autoloadBunfig: false`, so its
CWD `bunfig.toml`, and a `preload` entry points at a module the standalone embedded runtime *never* reads the launching CWD's `bunfig.toml`. Without
can't resolve (`@opentui/solid/preload`) → the binary dies at startup with that flag, a top-level `preload` in the CWD bunfig (common in Bun project
`preload not found`. This is why `bunfig.toml` has **no** top-level dirs) resolves against the CWD rather than the binary and kills startup
`preload`; dev-mode preloading happens via explicit `--preload` flags in with `preload not found`. Don't remove the flag. Preloads for dev/test
`package.json`. The `[test]` section *does* keep a preload — that only belong in the explicit `--preload` flags in `package.json` and the
affects `bun test`. `[test]` section of `bunfig.toml` — not as a top-level entry.
2. **Smoke-test the compiled binary from a bunfig-free dir.** 2. **Smoke-test the binary from a dir with a poisoned bunfig.**
Because of (1), `./dist/podtui --version` run from the repo root launched The CI smoke test unpacks the tarball into a `mktemp` dir, drops a
inside CI would fail. CI always unpacks the tarball into a `mktemp` dir `bunfig.toml` containing an unresolvable top-level `preload` next to it,
before booting. Do the same when testing a release build locally. and boots the binary — proving bunfig autoload stayed disabled. `./dist/
podtui --version` must work from any directory, including the repo root;
do the same check when testing a release build locally.
3. **Homebrew's dylib-repair warning is benign.** 3. **Homebrew's dylib-repair warning is benign.**
`brew install` may print “load commands do not fit in the header … needs `brew install` may print “load commands do not fit in the header … needs
@@ -166,7 +169,7 @@ Releases are built and published from **tags**
test: `brew install mikefreno/tap/podtui`. test: `brew install mikefreno/tap/podtui`.
5. **AUR packaging** (`packaging/aur/PKGBUILD`): the `podtui-bin` package is 5. **AUR packaging** (`packaging/aur/PKGBUILD`): the `podtui-bin` package is
staged, not yet published (AUR account registrations are closed; see the staged, not yet published (AUR account registrations are closed; see the
README note in section 3). On each release, keep the AUR sources in sync README's Installation section). On each release, keep the AUR sources in sync
with the new tag: bump `pkgver`, recompute the two tarball `sha256sums` with the new tag: bump `pkgver`, recompute the two tarball `sha256sums`
entries, keep the `LICENSE` asset source (the workflow above uploads entries, keep the `LICENSE` asset source (the workflow above uploads
`LICENSE` to every release), and regenerate `packaging/aur/.SRCINFO` with `LICENSE` to every release), and regenerate `packaging/aur/.SRCINFO` with
@@ -190,13 +193,36 @@ make dist # builds the binary + tarball for THIS machine only
Bun cannot cross-compile — the other platforms come from CI. Bun cannot cross-compile — the other platforms come from CI.
## Distribution & packaging
A release tarball is three files sitting side by side: the `podtui` binary
plus its two FFI libraries (`libopentui.<dylib|so>`,
`libcavacore.<dylib|so>`). The sibling rule above is why they ship together.
PodTui deliberately ships **no** `.deb`, `.rpm`, Flatpak, or Snap packages:
for a terminal app that's overwhelmingly installed through repositories or
archives, those formats add desktop-sandboxing overhead and a packaging tax
with little benefit. Instead:
- **GitHub Release tarballs** are the universal path — one upload per
OS/arch, works on any distro with `curl` + `tar`.
- **AUR (`podtui-bin`)** covers Arch/Manjaro with the same binary through the
native package manager.
- **Nix / cross-distro** users build from source (or a Nix flake can be added
later).
This keeps maintenance to a single build per OS/arch while still reaching the
vast majority of desktop Linux users. The AUR PKGBUILD lives in
`packaging/aur/` and can be built locally to test before publication:
```bash
cd packaging/aur && makepkg -si
```
--- ---
## Open items / things to sort out ## Open items / things to sort out
- **LICENSE**: `README.md` says "TBD — choose and document a license before
the first release". Pick one (MIT/BSD-3) and add `LICENSE` + update the
README footer.
- **Native libs in `dist/` still need committing?** No — they're built from - **Native libs in `dist/` still need committing?** No — they're built from
sources kept in the repo (`cava/`, `node_modules/@opentui/core-*`). Only sources kept in the repo (`cava/`, `node_modules/@opentui/core-*`). Only
`src/native/libcavacore.dylib` is a committed binary artifact; macOS arm64 `src/native/libcavacore.dylib` is a committed binary artifact; macOS arm64

View File

@@ -47,9 +47,8 @@ native:
scripts/build-cavacore.sh scripts/build-cavacore.sh
## Standalone binary + native-libs tarball for the current platform. ## Standalone binary + native-libs tarball for the current platform.
## Unaffected by bunfig.toml at build time. Note: the compiled runtime reads ## Built with bunfig autoload disabled (build.ts sets autoloadBunfig: false),
## the launching process's CWD bunfig.toml, so smoke tests must run the binary ## so the embedded runtime ignores any bunfig.toml in the launching directory.
## from a bunfig-free dir (see release.yml).
dist: dist:
bun run build.ts --compile bun run build.ts --compile

208
README.md
View File

@@ -1,7 +1,6 @@
# PodTui # PodTui
A keyboard-first, yazi-style terminal podcast client written in TypeScript and A keyboard-first, terminal podcast client built on [OpenTUI](https://github.com/opentui/opentui). Subscribe to RSS feeds,
built on [OpenTUI](https://github.com/opentui/opentui). Subscribe to RSS feeds,
browse episodes in a three-pane file-manager layout, and play audio through an browse episodes in a three-pane file-manager layout, and play audio through an
external player with full transport control — all from your terminal. external player with full transport control — all from your terminal.
@@ -11,8 +10,7 @@ external player with full transport control — all from your terminal.
`Enter` to open, `16` / `[` `]` to switch tabs. The tab list is the app root: `Enter` to open, `16` / `[` `]` to switch tabs. The tab list is the app root:
at launch it fills the current pane, and drilling into a tab's contents slides at launch it fills the current pane, and drilling into a tab's contents slides
it into the parent pane. it into the parent pane.
- **Three-pane view** — parent / current / preview (Up | Current | Preview), - **Three-pane view** — parent / current / preview (Up | Current | Preview).
mirroring yazi's pane model.
- **Podcast feeds** — add feeds, browse episodes, and manage your library - **Podcast feeds** — add feeds, browse episodes, and manage your library
(My Shows, Discover, Feed tabs). (My Shows, Discover, Feed tabs).
- **Search** across your subscribed shows. - **Search** across your subscribed shows.
@@ -22,13 +20,28 @@ external player with full transport control — all from your terminal.
- Ships as a **standalone compiled binary** — no runtime or install step beyond - Ships as a **standalone compiled binary** — no runtime or install step beyond
a system audio player. a system audio player.
## Quick start
1. Install PodTui ([Installation](#installation)) and make sure **mpv** is in
your `PATH`.
2. Run `podtui` in your terminal.
3. Press `3` to open **Discover** (or `4` to open **Search**, then `s`), drill
in with `Enter`, and press `Enter` on a show to subscribe.
4. Press `1` (**Feed**) or `2` (**My Shows**), open an episode with `Enter`,
and use `P` to play/pause, `N`/`B` for next/previous, and `shift-.` /
`shift-,` to seek.
Press `~` any time for in-app help. All keys are remappable — see
[Keybindings](#keybindings).
## Requirements ## Requirements
- A terminal with UTF-8 and modern color support (kitty, iTerm2, WezTerm, - A terminal with UTF-8 and modern color support (kitty, iTerm2, WezTerm,
tmux, GNOME Terminal, etc.). Ghostty, tmux etc.).
- **mpv** on `PATH` for audio playback. PodTui drives mpv over JSON IPC, so - **mpv** on `PATH` for audio playback. PodTui drives mpv over JSON IPC, so
seek, speed, and position tracking all work. Without `mpv` on `PATH`, seek, speed, and position tracking all work. Without `mpv` on `PATH`,
playback is a silent no-op (the `none` backend). playback is a silent no-op (the `none` backend) — see
[Troubleshooting](#troubleshooting).
## Installation ## Installation
@@ -41,10 +54,6 @@ Linux (arm64/x64). Pick whichever fits your platform.
brew install mikefreno/tap/podtui brew install mikefreno/tap/podtui
``` ```
> The formula installs the standalone binary plus its two native libraries
> side by side (see [Packaging model](#packaging-model)). It does **not**
> depend on Bun.
### 2. Standalone tarball (all platforms) ### 2. Standalone tarball (all platforms)
Grab `podtui-<platform>-<arch>.tar.gz` from the latest Grab `podtui-<platform>-<arch>.tar.gz` from the latest
@@ -62,68 +71,29 @@ sudo ln -sf /opt/podtui/podtui /usr/local/bin/podtui
> The tarball contains `podtui` plus `libopentui.<ext>` and > The tarball contains `podtui` plus `libopentui.<ext>` and
> `libcavacore.<ext>` **beside it** — keep them together (don't move just the > `libcavacore.<ext>` **beside it** — keep them together (don't move just the
> binary alone), or the native FFI libraries won't load. > binary alone), or the native FFI libraries won't load.
>
> One caveat: the embedded runtime reads a `bunfig.toml` from the directory
> you launch from. If that file has a `preload` entry (as Bun project
> directories often do), startup fails with `preload not found`. Launching
> from a normal directory (home, `~/bin`, …) works fine.
### 3. Arch Linux (AUR) ### 3. Arch Linux (AUR)
```bash ```bash
# Status: PKGBUILD ready, not yet on the AUR (see note below)
yay -S podtui-bin # once published yay -S podtui-bin # once published
``` ```
Requires an AUR helper ([paru](https://github.com/morgan/paru)). The AUR Requires an AUR helper ([paru](https://github.com/morgan/paru)); the package
package (PKGBUILD lives in `packaging/aur/`) installs the released binary and pulls in `mpv` as a dependency.
its two FFI sibling libraries into `/usr/lib/podtui/` with a `/usr/bin/podtui`
symlink, and pulls in `mpv` (the sole audio backend) as a dependency.
> **Not yet on the AUR.** The `podtui-bin` PKGBUILD and `.SRCINFO` are ready > **Not yet on the AUR.** The `podtui-bin` package is staged and awaiting
> in `packaging/aur/` and can be built locally today: > publication (AUR account registrations are currently suspended). Until it
> > lands, use the standalone tarball above.
> ```bash
> cd packaging/aur && makepkg -si
> ```
>
> Publishing is on hold until [AUR account registrations](https://aur.archlinux.org)
> reopen (suspended while the AUR team works on suspicious-package
> moderation).
### 4. From source ### 4. From source
Requires [Bun](https://bun.sh) ≥ 1.2. PodTui is written in TypeScript and runs on [Bun](https://bun.sh). To build
from source (development, distro packaging, unreleased versions), see
```bash [CONTRIBUTING.md](CONTRIBUTING.md).
git clone https://github.com/mikefreno/podtui.git
cd podtui
bun install
bun run build:native # build the cavacore FFI lib from C source
bun run dev # run with hot reload, or: bun start
```
## Linux distribution notes
PodTUI deliberately does **not** ship `.deb`, `.rpm`, Flatpak, or Snap
packages. For a terminal application that's overwhelmingly installed through
repositories or archives, those formats add desktop-sandboxing overhead and a
packaging tax with little benefit. Instead:
- **GitHub Release tarballs** are the universal path — one upload, works on
any distro with `curl` + `tar`.
- **AUR (`podtui-bin`)** covers Arch. Anyone on Arch/Manjaro gets the same
binary through their native package manager.
- **Nix / cross-distro** users can build from source (or a Nix flake can be
added later).
This keeps maintenance to a single build per OS/arch and still reaches the
vast majority of desktop Linux users through their preferred path.
## Usage ## Usage
Launch `podtui` (or `bun src/index.tsx` from the source tree). Press `~` Launch `podtui`. Press `~` for the in-app help.
for the in-app help.
### Command-line flags ### Command-line flags
@@ -135,30 +105,70 @@ for the in-app help.
### Keybindings ### Keybindings
All keys are remappable — edit `~/.config/podtui/keybinds.jsonc`. All keys are remappable — edit `keybinds.jsonc` in your config directory
(see [Configuration](#configuration)).
**Movement**
| Keys | Action | | Keys | Action |
|------|--------| |------|--------|
| `j` / `k` | Move cursor down / up | | `j` / `k` (or `down` / `up`) | Move down / up |
| `J` / `K` | Jump 5 lines | | `J` / `K` | Jump 5 lines down / up |
| `ctrl-d` / `ctrl-u` | Page down / up | | `ctrl-d` / `ctrl-u` | Page down / up |
| `ctrl-f` / `ctrl-b` | Full page down / up |
| `gg` / `G` | Go to top / bottom | | `gg` / `G` | Go to top / bottom |
| `h` / `l` | Swipe to parent pane / preview pane |
**Panes**
| Keys | Action |
|------|--------|
| `h` / `l` (or `left` / `right`) | Focus parent pane / preview pane |
| `Enter` | Open the item under the cursor (a tab, episode, show…) | | `Enter` | Open the item under the cursor (a tab, episode, show…) |
| `Space` | Select / toggle selection | | `shift-enter` | Open with the interactive variant |
**Selection**
| Keys | Action |
|------|--------|
| `Space` | Toggle selection |
| `v` | Visual mode (multi-select) | | `v` | Visual mode (multi-select) |
| `ctrl-a` | Select / deselect all |
| `ctrl-r` | Invert selection |
| `Esc` | Cancel / escape |
**Tabs**
| Keys | Action |
|------|--------|
| `1``6` | Jump to tab 16 (Feed, My Shows, Discover, Search, Player, Settings) | | `1``6` | Jump to tab 16 (Feed, My Shows, Discover, Search, Player, Settings) |
| `[` / `]` | Previous / next tab | | `[` / `]` | Previous / next tab |
| `P` (shift) | Play / pause |
**Commands, help, quit**
| Keys | Action |
|------|--------|
| `:` or `q` | Open the command palette (type `q` + `Enter` there to quit) |
| `Q` or `ctrl-c` | Quit |
| `~` or `f1` | In-app help |
**Lists**
| Keys | Action |
|------|--------|
| `s` | Search |
| `f` | Filter |
| `,` | Sort |
| `.` | Toggle hidden |
| `r` | Refresh |
| `x` | Unsubscribe the focused show (My Shows) |
**Audio**
| Keys | Action |
|------|--------|
| `P` | Play / pause |
| `N` / `B` | Next / previous episode | | `N` / `B` | Next / previous episode |
| `shift-.` / `shift-,` | Seek forward / backward | | `shift-.` / `shift-,` | Seek forward / backward |
| `s` | Search (in a list) |
| `f` | Filter |
| `r` | Refresh |
| `:` | Command bar |
| `~`, `f1` | Help |
| `q`, `ctrl-c` | Quit |
| `Esc` | Escape / cancel |
## Configuration ## Configuration
@@ -175,50 +185,32 @@ default (`$XDG_CONFIG_HOME/podtui` if set).
Legacy `feeds.json`, `sources.json`, and `app-state.json` are auto-migrated Legacy `feeds.json`, `sources.json`, and `app-state.json` are auto-migrated
into `config.json` on first run. into `config.json` on first run.
Env overrides: `PODTUI_AUDIO_BACKEND`, `XDG_CONFIG_HOME`. Startup also reads Env overrides: `PODTUI_AUDIO_BACKEND`, `XDG_CONFIG_HOME`.
the same OpenTUI environment variables.
## Development ## Troubleshooting
```bash **`preload not found` at startup** — this used to happen when the binary was
bun install # install dependencies launched from a Bun project directory whose `bunfig.toml` had a `preload`
bun run dev # run with hot reload entry. Releases are compiled with bunfig autoload disabled
bun test # run the test suite (`autoloadBunfig: false`), so current binaries ignore the CWD's `bunfig.toml`
bun run build # bundle JS + copy native libs into dist/ entirely. If you still hit it, you're on an old release — upgrade.
make native # rebuild cavacore from C source
make lint # type-check (tsc)
```
### Releasing **No audio — playback is a silent no-op** — PodTui needs **mpv** on your
`PATH`. Install it (`brew install mpv`, `pacman -S mpv`, …) and relaunch.
Tag a release (e.g. `v0.1.0`); CI builds and uploads the per-platform tarballs **Homebrew prints a dylib warning** — “load commands do not fit in the header
to your GitHub Release automatically: … needs `-headerpad`” is benign: the app loads its libraries by path, the
install completes, and the app boots normally.
```bash **The app won't start / no spectrum after moving files**`podtui` loads its
make dist # build the standalone binary + tarball for THIS platform two native libraries relative to the binary, so keep `podtui`,
make dist-mac # (run on macOS) → podtui-darwin-<arch>.tar.gz `libopentui.*`, and `libcavacore.*` together in the same directory (the
make dist-linux # (run on Linux) → podtui-linux-<arch>.tar.gz tarball unpacks them side by side).
```
`make dist` emits a config-independent binary: Bun does not bake bunfig ## Building from source / contributing
settings into `--compile` output, and the solid JSX transform is registered in
`build.ts` itself. The binary then embeds the `preload`-free runtime, so launch
it from any normal directory.
## Packaging model Development setup, the test suite, packaging, and the release process are
documented in [CONTRIBUTING.md](CONTRIBUTING.md).
A release tarball is three files sitting side by side:
```
podtui # standalone compiled binary (embeds the Bun runtime)
libopentui.<dylib|so> # OpenTUI native renderer FFI library
libcavacore.<dylib|so> # cavacore spectrum FFI library (built from C)
```
PodTui loads its native libraries relative to the binary, so **keep them in
the same directory**. The compiled binary embeds the Bun runtime, so it runs
with no Bun installed. Each release builds one tarball per OS/arch in CI; there
is no cross-compilation.
## License ## License

View File

@@ -82,6 +82,12 @@ if (COMPILE) {
plugins: [solidPlugin], plugins: [solidPlugin],
compile: { compile: {
outfile, outfile,
// Don't let the embedded runtime autoload the launching CWD's
// bunfig.toml. A top-level `preload` there (common in Bun project
// dirs) resolves against the CWD, not the binary, so startup dies
// with "preload not found". With autoload disabled, the binary is
// config-independent and boots from any directory.
autoloadBunfig: false,
}, },
}); });
console.log(`Compiled standalone binary: ${outfile}`); console.log(`Compiled standalone binary: ${outfile}`);

View File

@@ -1,9 +1,8 @@
# NO top-level `preload` here — intentional. A compiled PodTUI binary's # No top-level `preload` here — dev/test get the solid JSX transform via the
# embedded Bun runtime reads the launching process's CWD bunfig.toml, and a # explicit `--preload` flags in package.json and the [test] section below.
# top-level `preload` entry (e.g. "@opentui/solid/preload", which the # Releases don't read this file at all: build.ts compiles the standalone with
# standalone cannot resolve) makes the binary die at startup with # `autoloadBunfig: false`, so its embedded runtime ignores any bunfig.toml in
# "preload not found". Dev/test still get the solid transform via explicit # the launching directory — no more "preload not found" from CWD bunfigs.
# `--preload` flags in package.json and the [test] section below.
[test] [test]
preload = "@opentui/solid/preload" preload = "@opentui/solid/preload"