Files
ffmpeg_wrapper/build.py
Sean MTS fb2b5ce91f Add Unlicense and ship ffmpeg's GPL license in release archives
The project is now public domain under the Unlicense (LICENSE, README).

The packaged archives bundle GPL ffmpeg/ffprobe builds but did not include
their license. fetch_ffmpeg.py now extracts the build's LICENSE.txt into
ffmpeg/LICENSE.txt, and build.py --ffmpeg-dir copies it into the archive as
ffmpeg-LICENSE.txt, failing before the PyInstaller run if it can't find one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:18:55 +00:00

102 lines
3.8 KiB
Python

"""Build a single-file GUI executable for the current OS with PyInstaller.
python build.py # -> dist/FFmpegWrapper(.exe)
python build.py --ffmpeg-dir DIR # also copy ffmpeg + ffprobe from DIR next to the exe,
# plus ffmpeg's LICENSE from DIR or its parent,
# and pack everything into a .zip / .tar.gz
PyInstaller cannot cross-compile: run this on Windows to get the .exe and on
Linux to get the Linux binary (or use the CI workflow, which does both).
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
NAME = "FFmpegWrapper"
IS_WINDOWS = sys.platform.startswith("win")
EXE = ".exe" if IS_WINDOWS else ""
def find_ffmpeg_license(ffmpeg_dir: Path) -> Path | None:
"""ffmpeg's license file. BtbN and gyan.dev builds keep it one level above bin/."""
for folder in (ffmpeg_dir, ffmpeg_dir.parent):
for name in ("LICENSE.txt", "LICENSE", "ffmpeg-LICENSE.txt"):
if (folder / name).is_file():
return folder / name
return None
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--ffmpeg-dir", type=Path, help="folder containing ffmpeg and ffprobe to bundle")
ap.add_argument("--onedir", action="store_true", help="build a folder instead of a single file (faster start-up)")
args = ap.parse_args()
if args.ffmpeg_dir:
# Check before the slow PyInstaller run: ffmpeg is GPL, so its license must ship with it.
ffmpeg_license = find_ffmpeg_license(args.ffmpeg_dir)
if ffmpeg_license is None:
print(f"error: no ffmpeg license (LICENSE.txt or LICENSE) in {args.ffmpeg_dir} or its parent "
"folder. ffmpeg is GPL, so the license has to be packaged with it.", file=sys.stderr)
return 1
import PyInstaller.__main__
from ffwrap import __version__
sep = ";" if IS_WINDOWS else ":"
pyi_args = [
str(ROOT / "run.py"),
"--name", NAME,
"--windowed",
"--noconfirm",
"--clean",
"--onedir" if args.onedir else "--onefile",
"--icon", str(ROOT / "assets" / "icon.ico"),
"--add-data", f"{ROOT / 'assets' / 'icon.png'}{sep}assets",
"--distpath", str(ROOT / "dist"),
"--workpath", str(ROOT / "build"),
"--specpath", str(ROOT / "build"),
# Not needed and only add weight.
"--exclude-module", "tkinter",
"--exclude-module", "unittest",
"--exclude-module", "pydoc",
]
PyInstaller.__main__.run(pyi_args)
dist = ROOT / "dist"
built = dist / NAME if args.onedir else dist / f"{NAME}{EXE}"
print(f"\nBuilt {built}")
if args.ffmpeg_dir:
# Stage <exe or onedir tree> + ffmpeg + ffprobe + ffmpeg's license in one folder and archive it.
stage = dist / "package" / NAME
shutil.rmtree(stage.parent, ignore_errors=True)
if args.onedir:
shutil.copytree(built, stage)
else:
stage.mkdir(parents=True)
shutil.copy2(built, stage)
for tool in ("ffmpeg", "ffprobe"):
src = args.ffmpeg_dir / f"{tool}{EXE}"
if not src.is_file():
print(f"error: {src} not found", file=sys.stderr)
return 1
shutil.copy2(src, stage)
shutil.copy2(ffmpeg_license, stage / "ffmpeg-LICENSE.txt")
plat ="windows-x64" if IS_WINDOWS else "linux-x64"
archive = shutil.make_archive(
str(dist / f"{NAME}-{__version__}-{plat}"), "zip" if IS_WINDOWS else "gztar",
root_dir=stage.parent, base_dir=NAME)
print(f"Packed {archive}")
return 0
if __name__ == "__main__":
sys.exit(main())