Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,16 @@ The author(s) of StemDeck provide this software "as is", without warranty of any

---

## License

StemDeck is [Apache-2.0](LICENSE).

Every download ships a `THIRD_PARTY_NOTICES.txt` and a `licenses/` folder. `licenses/INDEX.txt` lists each packaged Python dependency with its version and license, and each one's full license text sits beside it. That inventory is generated from the packaged interpreter at build time, so it describes what actually shipped rather than what was expected to.

FFmpeg is a GPL build. StemDeck downloads it and runs it as a separate executable, so it does not change StemDeck's own license, and `THIRD_PARTY_NOTICES.txt` carries the written offer of source for the exact build your platform receives.

---

## Community

| Platform | Link |
Expand Down
28 changes: 25 additions & 3 deletions packaging/linux/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ THIRD-PARTY NOTICES
StemDeck includes third-party open-source software. Each component is
copyrighted by its respective authors and distributed under its own license.

This starter notice is not a substitute for the full license inventory that
must be generated from the final packaged Python runtime before a public
release.
The full inventory is generated from the packaged Python runtime at build
time and ships in this download under licenses/. INDEX.txt there lists
every packaged dependency with its version and license, and each entry's
own license text sits beside it.

Bundled Components
------------------
Expand Down Expand Up @@ -76,3 +77,24 @@ Disclaimer

This notice file is not legal advice. Before public release, verify the exact
licenses of every bundled package and generated binary artifact.

FFmpeg (GPL build)
------------------
StemDeck downloads FFmpeg at first run and calls it as a separate
executable. It is not linked into StemDeck and does not change
StemDeck's own license.

Build: ffmpeg-release-amd64-static, from johnvansickle.com/ffmpeg
License: GNU General Public License v3 or later
https://www.gnu.org/licenses/gpl-3.0.html

Written offer of source. The complete corresponding source, along with
the build's own readme naming its configure flags, is published beside
the binary:

https://johnvansickle.com/ffmpeg/
https://git.ffmpeg.org/ffmpeg.git

If either link stops resolving, open an issue at
https://github.com/stemdeckapp/stemdeck/issues and the source will be
provided by another means.
30 changes: 26 additions & 4 deletions packaging/macos/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,29 @@ Downloaded during first-run setup:
- FFmpeg and ffprobe
- Demucs model weights

The runtime pack includes its own dependency inventory under
runtime/licenses/pip-list.json. Before public release, generate and include
full license texts for every packaged dependency and verify the exact FFmpeg
build license/provenance.
The runtime pack carries the full inventory under licenses/. INDEX.txt there
lists every packaged Python dependency with its version and license, and each
entry's own license text sits beside it. It is generated from the packaged
interpreter at build time rather than maintained by hand, so it describes what
actually shipped.

FFmpeg
------
StemDeck downloads FFmpeg and ffprobe at first run and calls them as
separate executables. They are not linked into StemDeck and do not
change StemDeck's own license.

Build: ffmpeg 8.1.1 and ffprobe 8.1.1, from evermeet.cx
License: GNU General Public License v3 or later
https://www.gnu.org/licenses/gpl-3.0.html

Written offer of source. The complete corresponding source is:

FFmpeg itself, release 8.1.1
https://git.ffmpeg.org/ffmpeg.git
The build's configuration, reported by `ffmpeg -version`
https://evermeet.cx/ffmpeg/

If either link stops resolving, open an issue at
https://github.com/stemdeckapp/stemdeck/issues and the source will be
provided by another means.
29 changes: 26 additions & 3 deletions packaging/windows/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ THIRD-PARTY NOTICES
StemDeck includes third-party open-source software. Each component is
copyrighted by its respective authors and distributed under its own license.

This starter notice is not a substitute for the full license inventory that
must be generated from the final packaged Python runtime before a public
release.
The full inventory is generated from the packaged Python runtime at build
time and ships in this download under licenses/. INDEX.txt there lists
every packaged dependency with its version and license, and each entry's
own license text sits beside it.

Bundled Components
------------------
Expand Down Expand Up @@ -72,3 +73,25 @@ Disclaimer

This notice file is not legal advice. Before public release, verify the exact
licenses of every bundled package and generated binary artifact.

FFmpeg (GPL build)
------------------
StemDeck downloads FFmpeg at first run and calls it as a separate
executable. It is not linked into StemDeck and does not change
StemDeck's own license.

Build: ffmpeg-n8.1-latest-win64-gpl, from BtbN/FFmpeg-Builds
License: GNU General Public License v3 or later
https://www.gnu.org/licenses/gpl-3.0.html

Written offer of source. The complete corresponding source for this
build is:

FFmpeg itself, tag n8.1
https://github.com/FFmpeg/FFmpeg/tree/n8.1
The build scripts and the exact configure flags used
https://github.com/BtbN/FFmpeg-Builds

If either link stops resolving, open an issue at
https://github.com/stemdeckapp/stemdeck/issues and the source will be
provided by another means.
123 changes: 123 additions & 0 deletions scripts/collect_licenses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Write the license text of every packaged Python dependency into the bundle.

Listing a dependency's name and the word "MIT" is not what MIT asks for. The
permissive licenses in this stack -- MIT, BSD, Apache-2.0 -- all require the
copyright notice and the license text itself to travel with a binary
distribution, and the hand-written THIRD_PARTY_NOTICES.txt says as much about
itself: "not a substitute for the full license inventory that must be generated
from the final packaged Python runtime".

Generated rather than maintained, because the list is whatever the packaged venv
actually contains. Three hand-edited notices files across three platforms drift
from the lockfile the first time a dependency is added, and nobody notices,
because nothing reads them.

Stdlib only, on purpose: this runs against the bundled interpreter partway
through packaging, before anything has been installed that is not a runtime
dependency.

Usage: python collect_licenses.py <site-packages-dir> <output-dir>
"""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path

# .dist-info/licenses/ is the modern location (PEP 639). Older wheels drop the
# file at the top of .dist-info instead, under any of these names.
LEGACY_NAMES = re.compile(
r"^(LICEN[CS]E|COPYING|NOTICE|AUTHORS|COPYRIGHT)([._-].*)?$", re.IGNORECASE
)


def metadata_field(dist_info: Path, field: str) -> str:
meta = dist_info / "METADATA"
if not meta.is_file():
return ""
prefix = f"{field}:".lower()
for line in meta.read_text(encoding="utf-8", errors="replace").splitlines():
if not line.strip():
break # headers end at the first blank line; the body is the README
if line.lower().startswith(prefix):
return line.split(":", 1)[1].strip()
return ""


def license_files(dist_info: Path) -> list[Path]:
found = list((dist_info / "licenses").rglob("*")) if (dist_info / "licenses").is_dir() else []
found += [p for p in dist_info.iterdir() if p.is_file() and LEGACY_NAMES.match(p.name)]
return sorted(p for p in found if p.is_file())


def main() -> int:
site_packages = Path(sys.argv[1])
out = Path(sys.argv[2])
if not site_packages.is_dir():
print(f"not a directory: {site_packages}", file=sys.stderr)
return 1
out.mkdir(parents=True, exist_ok=True)

index: list[dict[str, object]] = []
missing: list[str] = []

for dist_info in sorted(site_packages.glob("*.dist-info")):
name = metadata_field(dist_info, "Name") or dist_info.name.split("-")[0]
version = metadata_field(dist_info, "Version")
declared = metadata_field(dist_info, "License-Expression") or metadata_field(
dist_info, "License"
)
# A classifier is the only statement of license for a good number of
# older wheels, and it is more readable than a pasted license body.
if not declared or len(declared) > 80:
classifiers = [
line.split("::")[-1].strip()
for line in (dist_info / "METADATA")
.read_text(encoding="utf-8", errors="replace")
.splitlines()
if line.startswith("Classifier: License ::")
]
declared = ", ".join(classifiers) or declared[:80] or "see license text"

texts = license_files(dist_info)
stem = f"{name}-{version}" if version else name
written = []
for src in texts:
suffix = "" if len(texts) == 1 else f".{src.name}"
dest = out / f"{stem}{suffix}.txt"
dest.write_text(src.read_text(encoding="utf-8", errors="replace"), encoding="utf-8")
written.append(dest.name)
if not written:
missing.append(stem)
index.append({"name": name, "version": version, "license": declared, "files": written})

(out / "index.json").write_text(
json.dumps(index, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)

lines = [
"PACKAGED PYTHON DEPENDENCIES",
"============================",
"",
"Generated from the bundled interpreter at package time. Each entry's",
"full license text, where the wheel ships one, is in this directory.",
"",
]
width = max((len(str(e["name"])) for e in index), default=4)
for e in index:
note = "" if e["files"] else " (no license file in wheel)"
lines.append(f"{str(e['name']).ljust(width)} {e['version']} {e['license']}{note}")
(out / "INDEX.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")

print(f"licenses: {len(index)} packages, {len(index) - len(missing)} with license text")
if missing:
# Not fatal. Some wheels genuinely ship no license file, and failing the
# release build over one would be worse than recording the fact.
print(f"licenses: no text bundled for {len(missing)}: {', '.join(sorted(missing)[:8])}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
9 changes: 9 additions & 0 deletions scripts/linux/make-portable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,15 @@ done
# catch a strip that removed something load-bearing (#407, #421).
"$BUNDLED_PYTHON" -c "import fastapi, uvicorn, yt_dlp, demucs, torch, torchaudio, librosa, pyloudnorm, soundfile, audio_separator, onnxruntime; print('Post-strip import check OK')"

# Full license texts for everything in the venv, generated from the venv rather
# than maintained by hand. MIT, BSD and Apache-2.0 all require the copyright
# notice and the license itself to travel with a binary; a name and the word
# "MIT" in THIRD_PARTY_NOTICES.txt is not that. Runs here, after the strip, so
# the inventory describes what actually ships.
"$BUNDLED_PYTHON" "${REPO_ROOT}/scripts/collect_licenses.py" \
"${PYTHON_DIR}/lib/python${PYTHON_VERSION}/site-packages" \
"${STAGE}/licenses"

# Runtime fingerprint, shipped inside python/ in every package (#421).
#
# The in-app updater's SAFETY GATE, not a download trigger: it replaces
Expand Down
6 changes: 6 additions & 0 deletions scripts/macos/make-runtime-pack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ chmod +x "${QJS_DIR}/qjs"
echo "==> Capturing dependency inventory"
mkdir -p "$RUNTIME_DIR/licenses"
uv pip list --system --python "$PYTHON_DIR/bin/python" --format=json > "$RUNTIME_DIR/licenses/pip-list.json"
# pip-list.json is names and versions, which is an inventory but not a
# notice. MIT, BSD and Apache-2.0 all require the copyright line and the
# license text itself to travel with a binary, so collect those too.
"$PYTHON_DIR/bin/python" "${REPO_ROOT}/scripts/collect_licenses.py" \
"$PYTHON_DIR/lib/python${PYTHON_VERSION}/site-packages" \
"$RUNTIME_DIR/licenses"

cat > "$RUNTIME_DIR/runtime-manifest.json" <<JSON
{
Expand Down
9 changes: 9 additions & 0 deletions scripts/windows/make-portable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@ Get-ChildItem -Path (Join-Path $PythonDir "Lib\site-packages") -Directory -Force
# rather than in a release (#407).
& $PythonExe -c "import fastapi, uvicorn, yt_dlp, demucs, torch, torchaudio, librosa, pyloudnorm, soundfile, audio_separator, onnxruntime; print('Post-strip import check OK')"

# Full license texts for everything in the venv, generated from the venv rather
# than maintained by hand. MIT, BSD and Apache-2.0 all require the copyright
# notice and the license itself to travel with a binary; a name and the word
# "MIT" in THIRD_PARTY_NOTICES.txt is not that. Runs here, after the strip, so
# the inventory describes what actually ships.
& $PythonExe (Join-Path $Root "scripts\collect_licenses.py") `
(Join-Path $PythonDir "Lib\site-packages") (Join-Path $Stage "licenses")
Assert-LastExitCode "collecting dependency licenses"

# Runtime fingerprint, shipped INSIDE python/ in every package (#421).
#
# This is the in-app updater's SAFETY GATE, not a download trigger. The updater
Expand Down
2 changes: 1 addition & 1 deletion templates/stemdeck.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<Container version="2">
<Name>StemDeck</Name>
<Repository>ghcr.io/stemdeckapp/stemdeck:0.16.0</Repository>
<Repository>ghcr.io/stemdeckapp/stemdeck:0.16.1</Repository>
<Registry>https://github.com/stemdeckapp/stemdeck/pkgs/container/stemdeck</Registry>
<Network>bridge</Network>
<Shell>sh</Shell>
Expand Down
Loading