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>
This commit is contained in:
10
LICENSE
Normal file
10
LICENSE
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org/>
|
||||||
24
README.md
24
README.md
@@ -46,8 +46,9 @@ The app looks for `ffmpeg` and `ffprobe` in this order:
|
|||||||
3. Your `PATH`
|
3. Your `PATH`
|
||||||
|
|
||||||
The release archives (`FFmpegWrapper-<version>-windows-x64.zip` / `-linux-x64.tar.gz`) already
|
The release archives (`FFmpegWrapper-<version>-windows-x64.zip` / `-linux-x64.tar.gz`) already
|
||||||
include ffmpeg and ffprobe next to the executable. If you use the bare executable, install ffmpeg
|
include ffmpeg and ffprobe next to the executable, with ffmpeg's license as `ffmpeg-LICENSE.txt`.
|
||||||
(`sudo apt install ffmpeg`, `winget install ffmpeg`, ...) or point the app at it.
|
If you use the bare executable, install ffmpeg (`sudo apt install ffmpeg`, `winget install ffmpeg`,
|
||||||
|
...) or point the app at it.
|
||||||
|
|
||||||
## Running from source
|
## Running from source
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ PyInstaller does not cross-compile. Build the Windows `.exe` on Windows and the
|
|||||||
```bash
|
```bash
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
python build.py # -> dist/FFmpegWrapper or dist/FFmpegWrapper.exe
|
python build.py # -> dist/FFmpegWrapper or dist/FFmpegWrapper.exe
|
||||||
python build.py --ffmpeg-dir ffmpeg/bin # also bundles ffmpeg + ffprobe into a .zip / .tar.gz
|
python build.py --ffmpeg-dir ffmpeg/bin # also bundles ffmpeg + ffprobe + their license into a .zip / .tar.gz
|
||||||
python scripts/smoke_test.py dist/FFmpegWrapper # optional: headless start-up check
|
python scripts/smoke_test.py dist/FFmpegWrapper # optional: headless start-up check
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,9 +88,11 @@ Forgejo Actions can run it too, but they need a Windows runner registered.
|
|||||||
- **PySide6 is pinned below 6.12.** PySide6 6.12.0 mis-counts references to Python's `None`
|
- **PySide6 is pinned below 6.12.** PySide6 6.12.0 mis-counts references to Python's `None`
|
||||||
(in `QComboBox.clear()`/`currentData()`), which crashes the app on Python 3.11. Python 3.12+ is
|
(in `QComboBox.clear()`/`currentData()`), which crashes the app on Python 3.11. Python 3.12+ is
|
||||||
not affected, but keep the pin until a fixed PySide6 ships.
|
not affected, but keep the pin until a fixed PySide6 ships.
|
||||||
- **Licensing:** the bundled ffmpeg builds come from
|
- **ffmpeg licensing:** the bundled ffmpeg builds come from
|
||||||
[BtbN/FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds) and are GPL. Keep that in mind if you
|
[BtbN/FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds) and are GPLv3, so their license has to
|
||||||
redistribute the archives.
|
ship with them. `scripts/fetch_ffmpeg.py` saves it as `ffmpeg/LICENSE.txt`, and
|
||||||
|
`build.py --ffmpeg-dir` copies it into the archive as `ffmpeg-LICENSE.txt`. It looks in the ffmpeg
|
||||||
|
folder and the folder above it, and stops with an error if the license isn't in either.
|
||||||
- Settings (last options, window size, ffmpeg location) are saved per user through Qt's QSettings:
|
- Settings (last options, window size, ffmpeg location) are saved per user through Qt's QSettings:
|
||||||
the registry on Windows, `~/.config/ffwrap/FFmpegWrapper.conf` on Linux.
|
the registry on Windows, `~/.config/ffwrap/FFmpegWrapper.conf` on Linux.
|
||||||
|
|
||||||
@@ -106,3 +109,12 @@ build.py PyInstaller build + packaging
|
|||||||
scripts/ fetch_ffmpeg.py, smoke_test.py, make_icon.py
|
scripts/ fetch_ffmpeg.py, smoke_test.py, make_icon.py
|
||||||
tests/ unit tests + real-ffmpeg integration tests
|
tests/ unit tests + real-ffmpeg integration tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
FFmpeg Wrapper is public domain, released under [the Unlicense](LICENSE). Kopimi: copy it, change
|
||||||
|
it, share it, sell it. No permission needed.
|
||||||
|
|
||||||
|
That covers this project's own code. The ffmpeg and ffprobe binaries in the release archives are
|
||||||
|
GPLv3 (license in `ffmpeg-LICENSE.txt`, source at <https://ffmpeg.org/download.html>), and the Qt
|
||||||
|
libraries built into the executable are LGPLv3.
|
||||||
|
|||||||
23
build.py
23
build.py
@@ -1,7 +1,8 @@
|
|||||||
"""Build a single-file GUI executable for the current OS with PyInstaller.
|
"""Build a single-file GUI executable for the current OS with PyInstaller.
|
||||||
|
|
||||||
python build.py # -> dist/FFmpegWrapper(.exe)
|
python build.py # -> dist/FFmpegWrapper(.exe)
|
||||||
python build.py --ffmpeg-dir DIR # also copy ffmpeg + ffprobe from DIR next to the 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
|
# and pack everything into a .zip / .tar.gz
|
||||||
|
|
||||||
PyInstaller cannot cross-compile: run this on Windows to get the .exe and on
|
PyInstaller cannot cross-compile: run this on Windows to get the .exe and on
|
||||||
@@ -21,12 +22,29 @@ IS_WINDOWS = sys.platform.startswith("win")
|
|||||||
EXE = ".exe" if IS_WINDOWS else ""
|
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:
|
def main() -> int:
|
||||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
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("--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)")
|
ap.add_argument("--onedir", action="store_true", help="build a folder instead of a single file (faster start-up)")
|
||||||
args = ap.parse_args()
|
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__
|
import PyInstaller.__main__
|
||||||
|
|
||||||
from ffwrap import __version__
|
from ffwrap import __version__
|
||||||
@@ -56,7 +74,7 @@ def main() -> int:
|
|||||||
print(f"\nBuilt {built}")
|
print(f"\nBuilt {built}")
|
||||||
|
|
||||||
if args.ffmpeg_dir:
|
if args.ffmpeg_dir:
|
||||||
# Stage <exe or onedir tree> + ffmpeg + ffprobe in one folder and archive it.
|
# Stage <exe or onedir tree> + ffmpeg + ffprobe + ffmpeg's license in one folder and archive it.
|
||||||
stage = dist / "package" / NAME
|
stage = dist / "package" / NAME
|
||||||
shutil.rmtree(stage.parent, ignore_errors=True)
|
shutil.rmtree(stage.parent, ignore_errors=True)
|
||||||
if args.onedir:
|
if args.onedir:
|
||||||
@@ -70,6 +88,7 @@ def main() -> int:
|
|||||||
print(f"error: {src} not found", file=sys.stderr)
|
print(f"error: {src} not found", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
shutil.copy2(src, stage)
|
shutil.copy2(src, stage)
|
||||||
|
shutil.copy2(ffmpeg_license, stage / "ffmpeg-LICENSE.txt")
|
||||||
plat ="windows-x64" if IS_WINDOWS else "linux-x64"
|
plat ="windows-x64" if IS_WINDOWS else "linux-x64"
|
||||||
archive = shutil.make_archive(
|
archive = shutil.make_archive(
|
||||||
str(dist / f"{NAME}-{__version__}-{plat}"), "zip" if IS_WINDOWS else "gztar",
|
str(dist / f"{NAME}-{__version__}-{plat}"), "zip" if IS_WINDOWS else "gztar",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Uses the GPL builds from https://github.com/BtbN/FFmpeg-Builds (newest stable
|
Uses the GPL builds from https://github.com/BtbN/FFmpeg-Builds (newest stable
|
||||||
release branch). The app finds ./ffmpeg/bin automatically when run from source,
|
release branch). The app finds ./ffmpeg/bin automatically when run from source,
|
||||||
and build.py --ffmpeg-dir ffmpeg/bin bundles them with the executable.
|
and build.py --ffmpeg-dir ffmpeg/bin bundles them with the executable, together
|
||||||
|
with the build's license, which is saved as ./ffmpeg/LICENSE.txt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -52,27 +53,50 @@ def main() -> int:
|
|||||||
archive = Path(tmp) / asset["name"]
|
archive = Path(tmp) / asset["name"]
|
||||||
with urllib.request.urlopen(asset["browser_download_url"], timeout=60) as r, open(archive, "wb") as f:
|
with urllib.request.urlopen(asset["browser_download_url"], timeout=60) as r, open(archive, "wb") as f:
|
||||||
shutil.copyfileobj(r, f)
|
shutil.copyfileobj(r, f)
|
||||||
DEST.mkdir(parents=True, exist_ok=True)
|
extract(archive, exe, DEST)
|
||||||
|
print(f"Installed ffmpeg and ffprobe into {DEST}, and their license as {DEST.parent / 'LICENSE.txt'}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def extract(archive: Path, exe: str, dest: Path) -> None:
|
||||||
|
"""Copy ffmpeg + ffprobe from the archive's bin/ into dest, and its LICENSE.txt into dest's parent.
|
||||||
|
|
||||||
|
A BtbN archive holds one top folder with bin/ and LICENSE.txt in it. ffmpeg is GPL, so the
|
||||||
|
license has to ship with the binaries; build.py picks it up from there.
|
||||||
|
"""
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
license_out = dest.parent / "LICENSE.txt"
|
||||||
|
license_out.unlink(missing_ok=True)
|
||||||
wanted = {f"ffmpeg{exe}", f"ffprobe{exe}"}
|
wanted = {f"ffmpeg{exe}", f"ffprobe{exe}"}
|
||||||
|
|
||||||
|
def target(member: str) -> Path | None:
|
||||||
|
name = Path(member).name
|
||||||
|
if name in wanted and "/bin/" in member:
|
||||||
|
return dest / name
|
||||||
|
if member.strip("/").count("/") == 1 and name == "LICENSE.txt":
|
||||||
|
return license_out
|
||||||
|
return None
|
||||||
|
|
||||||
if archive.suffix == ".zip":
|
if archive.suffix == ".zip":
|
||||||
with zipfile.ZipFile(archive) as z:
|
with zipfile.ZipFile(archive) as z:
|
||||||
for info in z.infolist():
|
for info in z.infolist():
|
||||||
name = Path(info.filename).name
|
out = target(info.filename)
|
||||||
if name in wanted and "/bin/" in info.filename:
|
if out:
|
||||||
(DEST / name).write_bytes(z.read(info))
|
out.write_bytes(z.read(info))
|
||||||
else:
|
else:
|
||||||
with tarfile.open(archive) as t:
|
with tarfile.open(archive) as t:
|
||||||
for m in t.getmembers():
|
for m in t.getmembers():
|
||||||
name = Path(m.name).name
|
out = target(m.name) if m.isfile() else None
|
||||||
if name in wanted and "/bin/" in m.name:
|
if out:
|
||||||
with t.extractfile(m) as src, open(DEST / name, "wb") as out:
|
with t.extractfile(m) as src, open(out, "wb") as f:
|
||||||
shutil.copyfileobj(src, out)
|
shutil.copyfileobj(src, f)
|
||||||
(DEST / name).chmod(0o755)
|
if out.name in wanted:
|
||||||
missing = [n for n in wanted if not (DEST / n).is_file()]
|
out.chmod(0o755)
|
||||||
|
missing = [n for n in sorted(wanted) if not (dest / n).is_file()]
|
||||||
|
if not license_out.is_file():
|
||||||
|
missing.append("LICENSE.txt")
|
||||||
if missing:
|
if missing:
|
||||||
raise SystemExit(f"Archive did not contain {missing}")
|
raise SystemExit(f"Archive did not contain {missing}")
|
||||||
print(f"Installed ffmpeg and ffprobe into {DEST}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user