Building bundles¶
Releases ship as PyInstaller one-folder bundles, one per platform. There is no installer and no code signing.
Locally¶
build.py prints the path of what it produced:
| Platform | Output |
|---|---|
| Windows | dist/FFMPEGAudioEncoder/FFMPEGAudioEncoder.exe |
| Linux | dist/FFMPEGAudioEncoder/FFMPEGAudioEncoder |
| macOS | dist/FFMPEGAudioEncoder.app |
You must build on the platform you are targeting; PyInstaller does not cross-compile.
What build.py does¶
It drives PyInstaller with --onedir --windowed, adding:
--collect-all qtawesomeand--collect-all pymediainfo, both of which carry data files the analysis does not find on its own;--collect-data ffmpeg_audio_encoder, which carries the packaged icon into the bundle where the runtime reads it back out offfmpeg_audio_encoder/resources;--iconon Windows and macOS only. Linux executables carry no embedded icon, so PyInstaller ignores it there. On macOS the.icois converted to.icns, which is why Pillow is in thebuildgroup.
It then checks that the expected bundle actually exists and raises if not, so a PyInstaller run that exits 0 without producing anything is still a failure.
Smoke checks¶
Both CI and the release workflow run the built binary before going any further:
That is the cheapest possible check that the bundle starts and can find its toolchain. It is in the release workflow specifically so a tag can never publish a bundle that cannot launch.
CI¶
.github/workflows/ci.yml has two jobs:
quality- Ubuntu, twice over anffmpeg: [distro, latest]matrix, running ruff, basedpyright, and pytest. See Development setup for why both.bundle- Windows, Ubuntu, macOS Intel, and macOS Apple silicon. Builds and smoke tests the bundle, then uploads it as an artifact. It runs on pull requests and on pushes tomain,master, andv5.
Both use astral-sh/setup-uv@v6 with Python 3.12 and caching, and set QT_QPA_PLATFORM=offscreen.
Releasing¶
.github/workflows/release.yml triggers on a version tag - both the bare-numeric form this project has always used (5.0.0) and a v-prefixed one.
For each of the four platforms it builds, smoke tests, and archives the bundle as FFMPEGAudioEncoder-<tag>-<slug>:
| Platform | Slug | Archive method |
|---|---|---|
| Windows | windows-x64 |
7z a -tzip |
| Linux | linux-x64 |
tar -czf |
| macOS Intel | macos-x64 |
ditto -c -k --sequesterRsrc --keepParent |
| macOS Apple silicon | macos-arm64 |
ditto |
macOS uses ditto rather than zip because it preserves the bundle's symlinks and resource forks.
A second job collects all four and creates a draft release with generated notes. The draft is deliberate: you review and publish it by hand.
Publishing the release is what deploys the docs
The documentation workflow triggers on a published release, not on the tag. Drafting a release builds bundles; publishing it puts the docs live.
Version numbers¶
The version lives in one place, src/ffmpeg_audio_encoder/__init__.py:
Hatchling reads it from there, and --version prints it. Update it, update CHANGELOG.md, then tag.