Skip to content

Commit

Permalink
pyinstaller builds for macOS and Linux in the CI (#1348)
Browse files Browse the repository at this point in the history
* Build for macOS

* Add sdl2

* add sdl2_image

* Add assets back

* Try pysdl2-dll

* Pack theme

* Add templates

* add librsvg

* List all files in .app

* Try build with --wheel

* Attempt to bundle phazor

* Try adding pysdl2-dll and Rsvg as hidden imports

* Attempt to throw DYLD_LIBRARY_PATH back into spec

* Try adding librsvg-2.2.dylib

* Try fixing localization

* rsvg hidden import try #2

* Stop duping CustomLoggingFormatter

* compile_translations.py: Throw a proper exception on error

* Fix up command order

* Install gettext

* Use partial path for gettext

* Move logging classes to a separate module

* Cleanup and brew upgrade

* Add locale dir

* Fix brew commands

* Which genius decided to litter the FS with this garbage

* Remove extra space

* cleanup glib and gettext

* Attempt to bundle SDL2 prebuilt frameworks

* Try importing hidden chromecast

* try lower case pychromecast

* Fix error message for setproctitle

* Add support for PEP508 deps definitions

* Remove duped dep

* Add initial optdepends

* Fix up deps

* Move out import and add test exception

* pyinstaller debug build

* Try reverting deps changes

* Kill optdepends in pyproject

* Fix ext-modules

* Fix optional

* install pyinstaller in unified deps

* Fix opus include

* Init linux.spec

* Add Linux CI

* Swap macOS CI to unified reqs

* Try fixing up hiddenimports

* Linux CI: Add gobject-introspection

* Linux CI: Add python3-gi-cairo

* try adding libgirepository1.0-dev

* Add libcairo2-dev

* Linux CI: moar deps

* Log error for SDL renderer errors too

* Fix up CI

* Editor is rendering pipes curved here...

* Linux CI: Fix syntax

* Jpeg XL dep

* Try 24.10

* Make requirements.txt be the cross-platform full one

* yeet JXLPY to temp pass CI

* Fix backslash

* test if sdl framework still gets copied

* Linux CI: Clone submodules too

* Linux CI: Add devel packages for audio

* Linux CI: Add libsamplerate0-dev

* Linux CI: Fix backslash

* Try copying the entire sdl2dll dir

* Rename compile translations job

* Try adding zeroconf to reqs

* Fix pychromecast on pyinstaller

* Fix pyproject.toml indents

* Clean up requirements a bit

* Stop always printing pychromecast exception, now just to debug

* Move Chrome() creation back to try block

* pysdl fix attempt

* try to switch over to dev pyinstaller better

* Fix up reqs

* Use pysdl2-dll on all OSs

* Fix up Linux spec file

* Try bumping ffmpeg from v5 to v7

* Ffmpeg v2 attempt numero duo

* Attempt to add a Rsvg hook

* Indent user files location

* Add toggle console button to Misc

* Log PATH when looking for ffmpeg

* Add the second part necessary for Rsvg

* Fix up DConsole self reference

* Fix up optdeps

* try hacking libjxl on Linux

* Cleanup and document workarounds

* Fix dpkg -i

* Add libgif dep for jxl

* Cleanup TODO notes

* Try packaging ffmpeg better

* Cleanup docs

* Fix up action names
  • Loading branch information
C0rn3j authored Dec 20, 2024
1 parent 2d409c7 commit 51854aa
Show file tree
Hide file tree
Showing 18 changed files with 563 additions and 183 deletions.
94 changes: 94 additions & 0 deletions .github/workflows/build_Linux.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Build Linux app

on:
push:
pull_request:

jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: true

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install deps
run: |
sudo apt-get update
sudo apt-get install -y \
gettext \
gobject-introspection \
libgirepository1.0-dev \
python3-gi-cairo \
libcairo2-dev \
libpipewire-0.3-dev \
libdbus-1-dev \
libjxl-dev \
libflac-dev \
libgme-dev \
libmpg123-dev \
libopenmpt-dev \
libopusfile-dev \
libsamplerate0-dev \
libvorbis-dev \
libwavpack-dev
# JPEG-XL hack since 24.04 is too old
sudo apt-get install -y \
libgif7 \
wget
wget http://mirrors.kernel.org/ubuntu/pool/universe/j/jpeg-xl/libjxl-dev_0.10.3-4ubuntu1_amd64.deb
wget http://mirrors.kernel.org/ubuntu/pool/universe/j/jpeg-xl/libjxl0.10_0.10.3-4ubuntu1_amd64.deb
wget http://mirrors.kernel.org/ubuntu/pool/universe/h/highway/libhwy-dev_1.2.0-3ubuntu2_amd64.deb
wget http://mirrors.kernel.org/ubuntu/pool/universe/h/highway/libhwy1t64_1.2.0-3ubuntu2_amd64.deb
wget http://mirrors.kernel.org/ubuntu/pool/main/l/lcms2/liblcms2-dev_2.14-2build1_amd64.deb
sudo dpkg -i *.deb
- name: Install Python dependencies and setup venv
run: |
python -m pip install --upgrade pip
python -m venv .venv
source .venv/bin/activate
pip install \
-r requirements.txt \
build \
pyinstaller
- name: Build the project using python-build
run: |
source .venv/bin/activate
python -m compile_translations
python -m build --wheel
- name: Install the project into a venv
run: |
source .venv/bin/activate
pip install --prefix ".venv" dist/*.whl
- name: "[DEBUG] List all files"
run: find .

- name: Build Linux App with PyInstaller
run: |
source .venv/bin/activate
pyinstaller --log-level=DEBUG linux.spec
- name: Create ZIP
run: |
mkdir -p dist/zip
APP_NAME="TauonMusicBox"
APP_PATH="dist/${APP_NAME}"
ZIP_PATH="dist/zip/${APP_NAME}.zip"
zip -r "${ZIP_PATH}" "${APP_PATH}"
- name: Upload ZIP artifact
uses: actions/upload-artifact@v4
with:
name: TauonMusicBox-linux
path: dist/zip/TauonMusicBox.zip
95 changes: 95 additions & 0 deletions .github/workflows/build_macOS.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Build macOS app

on:
push:
pull_request:

jobs:
build:
runs-on: macos-latest

steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: true

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: brew update and upgrade
run: brew update && brew upgrade

- name: Install brew dependencies
run: |
brew install \
gobject-introspection \
gtk+3 \
pango \
sdl2 \
sdl2_image \
jpeg-xl \
ffmpeg \
librsvg \
opusfile \
libopenmpt \
wavpack \
game-music-emu
- name: Install Python dependencies and setup venv
run: |
python -m pip install --upgrade pip
python -m venv .venv
source .venv/bin/activate
pip install \
-r requirements.txt \
build
# Hack until https://github.com/pyinstaller/pyinstaller/issues/8936 is resolved
pip install https://github.com/rokm/pyinstaller/archive/refs/heads/macos-nested-framework-bundles.zip
# \
# pyinstaller
# pip uninstall pyinstaller
# CFLAGS: "-I/opt/homebrew/include"
# LDFLAGS: "-L/opt/homebrew/lib"

- name: Build the project using python-build
run: |
source .venv/bin/activate
python -m compile_translations
python -m build --wheel
- name: Install the project into a venv
run: |
source .venv/bin/activate
pip install --prefix ".venv" dist/*.whl
- name: "[DEBUG] List all files"
run: find .

- name: Build macOS app with PyInstaller
run: |
source .venv/bin/activate
pyinstaller --log-level=DEBUG mac.spec
env:
DYLD_LIBRARY_PATH: "/opt/homebrew/lib"

- name: "[DEBUG] List all files in .app"
run: find "dist/TauonMusicBox.app"

- name: Create DMG
run: |
mkdir -p dist/dmg
APP_NAME="TauonMusicBox"
APP_PATH="dist/${APP_NAME}.app"
DMG_PATH="dist/dmg/${APP_NAME}.dmg"
# Create a .dmg package
hdiutil create -volname "$APP_NAME" -srcfolder "$APP_PATH" -ov -format UDZO "$DMG_PATH"
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:
name: TauonMusicBox-dmg
path: dist/dmg/TauonMusicBox.dmg
2 changes: 1 addition & 1 deletion .github/workflows/compile_translations.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Tauon Linux CI
name: Compile translations on Linux

on:
push:
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
recursive-include src/phazor/kissfft *.h
recursive-include src/phazor/miniaudio *.h
40 changes: 4 additions & 36 deletions compile_translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,7 @@
import subprocess
from pathlib import Path


# TODO(Martin): import this class from tauon.py instead
class CustomLoggingFormatter(logging.Formatter):
"""Nicely format logging.loglevel logs"""

grey = "\x1b[38;20m"
grey_bold = "\x1b[38;1m"
yellow = "\x1b[33;20m"
yellow_bold = "\x1b[33;1m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s"
format_verbose = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s (%(filename)s:%(lineno)d)"

FORMATS = {
logging.DEBUG: grey_bold + format_verbose + reset,
logging.INFO: yellow + format + reset,
logging.WARNING: yellow_bold + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format_verbose + reset,
}

def format(self, record: dict) -> str:
log_fmt = self.FORMATS.get(record.levelno)
# Remove the miliseconds(%f) from the default string
date_fmt = "%Y-%m-%d %H:%M:%S"
formatter = logging.Formatter(log_fmt, date_fmt)
# Center align + min length things to prevent logs jumping around when switching between different values
record.levelname = f"{record.levelname:^7}"
record.module = f"{record.module:^10}"
return formatter.format(record)
from src.tauon.t_modules.logging import CustomLoggingFormatter

# DEBUG+ to file and std_err
logging.basicConfig(
Expand All @@ -56,7 +25,7 @@ def main() -> None:

for lang_file in languages:

if lang_file.name == "messages.pot":
if lang_file.name in ("messages.pot", ".DS_Store"):
continue

po_path = locale_folder / lang_file.name / "LC_MESSAGES" / "tauon.po"
Expand All @@ -69,10 +38,9 @@ def main() -> None:

if po_path.exists():
try:
subprocess.run(["/usr/bin/msgfmt", "-o", mo_path, po_path], check=True)
subprocess.run(["msgfmt", "-o", mo_path, po_path], check=True)
except Exception:
# Don't log the exception to make the build log clear
logging.error(f"Failed to compile translations for {lang_file.name}")
logging.exception(f"Failed to compile translations for {lang_file.name}")
compile_failure = True
else:
logging.info(f"Compiled: {lang_file.name}")
Expand Down
5 changes: 5 additions & 0 deletions extra/pyinstaller-hooks/hook-gi.repository.Rsvg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from PyInstaller.utils.hooks.gi import GiModuleInfo

module_info = GiModuleInfo("Rsvg", "2.0")
if module_info.available:
binaries, datas, hiddenimports = module_info.collect_typelib_data()
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------


def pre_safe_import_module(api):
# PyGObject modules loaded through the gi repository are marked as MissingModules by modulegraph, so we convert them
# to RuntimeModules in order for their hooks to be loaded and executed.
api.add_runtime_module(api.module_name)
58 changes: 58 additions & 0 deletions linux.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@


a = Analysis(
["src/tauon/__main__.py"],
pathex=[],
binaries=[],
datas=[
("src/tauon/assets", "assets"),
("src/tauon/locale", "locale"),
("src/tauon/theme", "theme"),
("src/tauon/templates", "templates"),
# This could only have SDL2.framework and SDL2_image.framework to save space...
(".venv/lib/python3.13/site-packages/sdl2dll/dll", "sdl2dll/dll"),
# (".venv/lib/python3.13/site-packages/sdl2dll/dll/SDL2.framework", "sdl2dll/dll/SDL2.framework"),
# (".venv/lib/python3.13/site-packages/sdl2dll/dll/SDL2_image.framework", "sdl2dll/dll/SDL2_image.framework"),
],
hiddenimports=[
"pylast",
"phazor",
# Zeroconf is hacked until this issue is resolved: https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/840
"zeroconf._utils.ipaddress",
"zeroconf._handlers.answers",
],
hookspath=["extra/pyinstaller-hooks"],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="Tauon Music Box",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name="TauonMusicBox",
)
Loading

0 comments on commit 51854aa

Please sign in to comment.