325 lines
13 KiB
Python
Executable File
325 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
PROJECT_ROOT = SCRIPT_DIR.parent.parent
|
|
BUILD_DIR = Path(os.environ["BUILD_DIR"]) if "BUILD_DIR" in os.environ else PROJECT_ROOT / "build/linux/x64/release/bundle"
|
|
OUTPUT_DIR = Path(os.environ.get("OUTPUT_DIR", PROJECT_ROOT))
|
|
ARCH_SUFFIX = os.environ.get("ARCH_SUFFIX", "x64")
|
|
|
|
METADATA = {
|
|
"name": "plezy",
|
|
"license": "GPL-3.0",
|
|
"vendor": "edde746",
|
|
"maintainer": "edde746 <noreply@github.com>",
|
|
"url": "https://github.com/edde746/plezy",
|
|
"description": "A modern Plex and Jellyfin client for desktop and mobile",
|
|
}
|
|
|
|
ICON_SIZES = [16, 32, 48, 64, 128, 256, 512]
|
|
|
|
ARCH_MAP = {
|
|
"x64": {"deb": "amd64", "rpm": "x86_64", "pacman": "x86_64"},
|
|
"arm64": {"deb": "arm64", "rpm": "aarch64", "pacman": "aarch64"},
|
|
}
|
|
|
|
DISTROS = {
|
|
"deb": {
|
|
"type": "deb",
|
|
"category": "video",
|
|
"ext": "deb",
|
|
"compression": ["--deb-compression", "xz", "--deb-priority", "optional"],
|
|
# The native video plane links wayland-client, wayland-egl and EGL, and
|
|
# bundle-libs.sh deliberately never bundles those: they are coupled to
|
|
# the running compositor and GPU driver. So they have to be declared.
|
|
#
|
|
# libmpv itself travels *in* the package rather than being depended on,
|
|
# because the plane needs the pinned Wayland-enabled build - a distro
|
|
# libmpv without Wayland silently drops hwdec to vaapi-copy. That means
|
|
# the libraries the host libmpv used to drag in transitively are now ours
|
|
# to declare: libva and its drm/wayland backends for hwdec, and drm/gbm
|
|
# underneath them.
|
|
#
|
|
# The X11 and xcb entries are not a mistake on a Wayland-only video
|
|
# path: GTK links them whatever backend it runs, and ffmpeg's VA-API
|
|
# brings libva-x11 with it. They used to arrive via the host gtk3 and
|
|
# mpv packages.
|
|
#
|
|
# Miss any one of these and the loader fails before main() runs, so
|
|
# check-bundle-host-deps.py re-derives the whole list from the built
|
|
# bundle rather than trusting this comment - main() below runs it before
|
|
# anything is packaged, and the release workflow runs it again.
|
|
"depends": [
|
|
"libgtk-3-0",
|
|
"libepoxy0",
|
|
"libasound2",
|
|
"libevdev2",
|
|
"libglib2.0-0",
|
|
"libwayland-client0",
|
|
"libwayland-cursor0",
|
|
"libwayland-egl1",
|
|
"libegl1",
|
|
# libEGL.so.1 defers to libGLdispatch.so.0, which is a separate
|
|
# package Debian only pulls in behind libegl1. The runner links it
|
|
# directly, so it is declared directly.
|
|
"libglvnd0",
|
|
"libva2",
|
|
"libva-drm2",
|
|
"libva-wayland2",
|
|
"libva-x11-2",
|
|
"libdrm2",
|
|
"libgbm1",
|
|
"libx11-6",
|
|
"libx11-xcb1",
|
|
"libxext6",
|
|
"libxcb1",
|
|
"libxcb-dri3-0",
|
|
"libxcb-render0",
|
|
"libxcb-shm0",
|
|
],
|
|
},
|
|
"rpm": {
|
|
"type": "rpm",
|
|
"category": "Multimedia",
|
|
"ext": "rpm",
|
|
"compression": ["--rpm-compression", "xzmt"],
|
|
# Fedora splits glvnd: libglvnd-egl carries libEGL.so.1 but the
|
|
# libGLdispatch.so.0 it defers to lives in the base libglvnd package.
|
|
"depends": [
|
|
"gtk3",
|
|
"libepoxy",
|
|
"alsa-lib",
|
|
"libevdev",
|
|
"glib2",
|
|
"libwayland-client",
|
|
"libwayland-cursor",
|
|
"libwayland-egl",
|
|
"libglvnd",
|
|
"libglvnd-egl",
|
|
"libva",
|
|
"libdrm",
|
|
"mesa-libgbm",
|
|
"libX11",
|
|
"libX11-xcb",
|
|
"libXext",
|
|
"libxcb",
|
|
],
|
|
},
|
|
"pacman": {
|
|
"type": "pacman",
|
|
"category": None,
|
|
"ext": "pkg.tar.zst",
|
|
"compression": ["--pacman-compression", "zstd"],
|
|
# Arch ships every libwayland-* in the one `wayland` package, libglvnd is
|
|
# what provides both libEGL.so.1 and libGLdispatch.so.0, mesa is what
|
|
# provides libgbm.so.1, and libx11 carries libX11-xcb.so.1 alongside
|
|
# libX11.so.6.
|
|
"depends": [
|
|
"gtk3",
|
|
"libepoxy",
|
|
"alsa-lib",
|
|
"libevdev",
|
|
"glib2",
|
|
"wayland",
|
|
"libglvnd",
|
|
"libva",
|
|
"libdrm",
|
|
"mesa",
|
|
"libx11",
|
|
"libxext",
|
|
"libxcb",
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def get_version() -> str:
|
|
"""Extract version from pubspec.yaml."""
|
|
pubspec = PROJECT_ROOT / "pubspec.yaml"
|
|
content = pubspec.read_text()
|
|
match = re.search(r"^version:\s*(.+)$", content, re.MULTILINE)
|
|
if not match:
|
|
raise RuntimeError("Could not find version in pubspec.yaml")
|
|
return match.group(1).split("+")[0]
|
|
|
|
|
|
def generate_icons():
|
|
"""Generate icons at multiple sizes using ImageMagick."""
|
|
print("Generating icons...")
|
|
source = PROJECT_ROOT / "assets/plezy.png"
|
|
|
|
for size in ICON_SIZES:
|
|
dest_dir = SCRIPT_DIR / f"icons/{size}x{size}"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
dest = dest_dir / "plezy.png"
|
|
|
|
# Try magick (ImageMagick 7) first, then convert (ImageMagick 6)
|
|
for cmd in ["magick", "convert"]:
|
|
if shutil.which(cmd):
|
|
subprocess.run([cmd, str(source), "-resize", f"{size}x{size}", str(dest)], check=True)
|
|
break
|
|
else:
|
|
print(f"Warning: ImageMagick not found, copying original icon for {size}x{size}")
|
|
shutil.copy(source, dest)
|
|
|
|
|
|
def get_file_mappings() -> list[str]:
|
|
"""Get file mappings for fpm."""
|
|
mappings = [
|
|
f"{BUILD_DIR}/=/opt/plezy/",
|
|
f"{SCRIPT_DIR}/com.edde746.plezy.desktop=/usr/share/applications/com.edde746.plezy.desktop",
|
|
f"{SCRIPT_DIR}/plezy.sh=/usr/bin/plezy",
|
|
]
|
|
|
|
for size in ICON_SIZES:
|
|
mappings.append(
|
|
f"{SCRIPT_DIR}/icons/{size}x{size}/plezy.png=/usr/share/icons/hicolor/{size}x{size}/apps/plezy.png"
|
|
)
|
|
|
|
return mappings
|
|
|
|
|
|
def build_package(distro: str, version: str):
|
|
"""Build a package for the specified distro."""
|
|
config = DISTROS[distro]
|
|
arch = ARCH_MAP[ARCH_SUFFIX][distro]
|
|
output_file = OUTPUT_DIR / f"{METADATA['name']}-linux-{ARCH_SUFFIX}.{config['ext']}"
|
|
|
|
print(f"Building .{config['ext']} package ({arch})...")
|
|
|
|
cmd = [
|
|
"fpm",
|
|
"-s", "dir",
|
|
"-t", config["type"],
|
|
"-n", METADATA["name"],
|
|
"-v", version,
|
|
"--iteration", "1",
|
|
"--license", METADATA["license"],
|
|
"--vendor", METADATA["vendor"],
|
|
"--maintainer", METADATA["maintainer"],
|
|
"--url", METADATA["url"],
|
|
"--description", METADATA["description"],
|
|
"--architecture", arch,
|
|
]
|
|
|
|
if config["category"]:
|
|
cmd.extend(["--category", config["category"]])
|
|
|
|
for dep in config["depends"]:
|
|
cmd.extend(["--depends", dep])
|
|
|
|
cmd.extend(config["compression"])
|
|
|
|
cmd.extend([
|
|
"--after-install", str(SCRIPT_DIR / "after-install.sh"),
|
|
"--after-remove", str(SCRIPT_DIR / "after-remove.sh"),
|
|
"--package", str(output_file),
|
|
])
|
|
|
|
cmd.extend(get_file_mappings())
|
|
|
|
subprocess.run(cmd, check=True)
|
|
print(f"Created: {output_file}")
|
|
|
|
|
|
def main():
|
|
# Verify build exists
|
|
if not BUILD_DIR.exists():
|
|
print(f"Error: Build directory not found at {BUILD_DIR}")
|
|
print("Please run 'flutter build linux --release' first or set BUILD_DIR.")
|
|
# The runner requires pkg-config to find mpv, and build-libmpv.sh exports
|
|
# PKG_CONFIG_PATH only inside its own process. Without carrying it across,
|
|
# the build either cannot configure at all or - worse, because it looks
|
|
# like success - links whatever distro libmpv happens to be installed
|
|
# instead of the pinned Wayland-enabled one these packages assume. CI sets
|
|
# this explicitly for the same reason and installs no libmpv-dev.
|
|
print("The build needs the pinned libmpv on its pkg-config path, which build-libmpv.sh")
|
|
print("exports only for itself:")
|
|
print(' PKG_CONFIG_PATH="$(pwd)/libmpv-prefix/lib/pkgconfig:$(pwd)/libmpv-prefix/lib/x86_64-linux-gnu/pkgconfig" \\')
|
|
print(" flutter build linux --release")
|
|
exit(1)
|
|
|
|
# None of the three package manifests declares a host libmpv any more,
|
|
# because the plane needs the pinned Wayland-enabled build and so ships it.
|
|
# That makes a bundle without one unpackageable rather than merely thinner:
|
|
# the runner's DT_NEEDED libmpv.so.2 would be satisfied by nothing and the
|
|
# loader would fail before main(), on a package that installed cleanly.
|
|
# Both workflows stage it first; this refuses the standalone path that the
|
|
# message above otherwise invites.
|
|
# The versioned soname, not `libmpv.so`. That bare name is the development
|
|
# linker name; the runner records DT_NEEDED libmpv.so.2 and the loader will
|
|
# not accept the unversioned file in its place. Matching `libmpv.so*` would
|
|
# pass a staging tree carrying only the linker name - the exact failure this
|
|
# check is named for, and the only thing standing between a hand-staged
|
|
# bundle and a broken package when the host-dep guard is opted out of below.
|
|
if not sorted((BUILD_DIR / "lib").glob("libmpv.so.[0-9]*")):
|
|
print(f"Error: no libmpv found in {BUILD_DIR / 'lib'}")
|
|
print("The packages declare no host libmpv, so one has to travel inside them.")
|
|
print("Stage the bundle first, exactly as both workflows do:")
|
|
# meson installs libmpv under an architecture triple on Debian and Ubuntu
|
|
# while CMake installed shaderc straight into lib/, which is why the two
|
|
# lines below are not the same shape. The workflows locate libmpv the same
|
|
# way; a hardcoded libmpv-prefix/lib/libmpv.so* finds nothing there.
|
|
print(' cp -a "$(dirname "$(find libmpv-prefix -name libmpv.so | head -1)")"/libmpv.so* <bundle>/lib/')
|
|
# The pinned libmpv links libshaderc_shared, which build-libmpv.sh leaves
|
|
# in libmpv-prefix - not a directory the loader searches. bundle-libs.sh
|
|
# cannot recover it either: it resolves what ldd reports, and ldd cannot
|
|
# find a soname that is only in the build prefix. So it is copied by hand
|
|
# before the walk, or the bundle gets an unpinned host shaderc at best.
|
|
print(" cp -a libmpv-prefix/lib/libshaderc_shared.so* <bundle>/lib/")
|
|
print(" bash linux/packaging/bundle-libs.sh <bundle>")
|
|
exit(1)
|
|
|
|
# Nothing above reconciles the hand-maintained depends lists with what the
|
|
# staged bundle actually loads, and a package that is short one host library
|
|
# installs cleanly and then dies in the loader. Run the guard that does
|
|
# reconcile them before any package exists. As a subprocess under this
|
|
# interpreter, so its exit code and ::error:: annotations arrive unaltered.
|
|
guard = SCRIPT_DIR / "check-bundle-host-deps.py"
|
|
# Truthy tokens only. `PLEZY_SKIP_HOST_DEP_CHECK=0` reads to almost everyone
|
|
# as "do not skip", and a bare non-empty test would have done the opposite -
|
|
# which is the single outcome this whole block exists to prevent.
|
|
skip = os.environ.get("PLEZY_SKIP_HOST_DEP_CHECK", "").strip().lower()
|
|
if skip in {"1", "true", "yes", "on"}:
|
|
# The guard reads deb ownership out of dpkg-query, which Fedora and Arch
|
|
# do not have, so it cannot run there at all. The opt-out is for that
|
|
# case and is an environment variable precisely so it cannot happen by
|
|
# accident: a skip the caller did not ask for would hand back exactly the
|
|
# unverified package this check exists to prevent, so it is never silent.
|
|
print("WARNING: PLEZY_SKIP_HOST_DEP_CHECK is set, so the host dependency guard did not run.")
|
|
print(f"WARNING: the depends lists in {Path(__file__).name} are unverified against {BUILD_DIR}.")
|
|
else:
|
|
guard_result = subprocess.run([sys.executable, str(guard), str(BUILD_DIR)])
|
|
if guard_result.returncode != 0:
|
|
print(f"Error: {guard.name} failed, so no packages were built.")
|
|
print("Declare the libraries it named, or - only on a host without dpkg-query, such as")
|
|
print("Fedora or Arch - set PLEZY_SKIP_HOST_DEP_CHECK=1 to package without the check.")
|
|
exit(guard_result.returncode)
|
|
|
|
version = get_version()
|
|
print(f"Building {ARCH_SUFFIX} packages for {METADATA['name']} version {version}")
|
|
|
|
# Make scripts executable
|
|
for script in ["plezy.sh", "after-install.sh", "after-remove.sh"]:
|
|
(SCRIPT_DIR / script).chmod(0o755)
|
|
|
|
# Generate icons
|
|
generate_icons()
|
|
|
|
# Build all packages
|
|
for distro in DISTROS:
|
|
build_package(distro, version)
|
|
|
|
print("\nAll packages built successfully!")
|
|
for f in sorted(OUTPUT_DIR.glob(f"{METADATA['name']}-linux-{ARCH_SUFFIX}.*")):
|
|
print(f" {f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|