From 7ea4f0796cb6394f8556691b0ab3be0efa9dbf00 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Fri, 24 Jul 2026 22:59:40 +0200 Subject: [PATCH 1/2] initial changes to pip releases and dependency cleanup --- .github/workflows/build_server.yml | 3 +- .github/workflows/pip_package.yml | 65 +++++++++++++++++--- Dockerfile | 2 +- freedata_server/constants.py | 26 ++++++++ freedata_server/message_system_db_manager.py | 13 ++-- freedata_server/server.py | 46 +++++++++++--- pyproject.toml | 8 +-- requirements.txt | 1 - tools/Linux/install-freedata-linux.sh | 9 ++- 9 files changed, 139 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build_server.yml b/.github/workflows/build_server.yml index 21abdefc4..f408814b9 100644 --- a/.github/workflows/build_server.yml +++ b/.github/workflows/build_server.yml @@ -53,12 +53,11 @@ jobs: sudo apt update sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake patchelf - - name: Install MacOS pyAudio + - name: Install MacOS dependencies if: ${{startsWith(matrix.os, 'macos')}} run: | brew install portaudio python -m pip install --upgrade pip - pip3 install pyaudio - name: Install Python dependencies run: | diff --git a/.github/workflows/pip_package.yml b/.github/workflows/pip_package.yml index 3a1e64ad1..9947fb115 100644 --- a/.github/workflows/pip_package.yml +++ b/.github/workflows/pip_package.yml @@ -1,5 +1,8 @@ name: Deploy Python Package -on: [push] +on: + push: + tags: + - "v*" jobs: deploy: @@ -17,16 +20,64 @@ jobs: with: node-version: 24 - - name: Install Linux dependencies - run: | - sudo apt update - sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake patchelf - - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install .[build] + - name: Set package version from tag + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python3 - <<'EOF' + import os + import re + import sys + + from packaging.version import InvalidVersion, Version + + tag = os.environ["RELEASE_TAG"] + version = tag.removeprefix("v") + + if not re.fullmatch(r"\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?", version): + print( + f"::error::Tag '{tag}' does not look like a release version. " + f"Use e.g. v1.2.3, v1.2.3-beta, v1.2.3-rc1 or v1.2.3-alpha.1." + ) + sys.exit(1) + + try: + Version(version) + except InvalidVersion: + print( + f"::error::Tag '{tag}' has an unrecognized pre-release suffix " + f"('{version}' is not valid PEP 440). Use a standard suffix such " + f"as -alpha, -alpha.1, -beta, -rc1 or -dev." + ) + sys.exit(1) + + print(f"Releasing version {version} (from tag {tag})") + + path = "freedata_server/constants.py" + with open(path) as f: + content = f.read() + + new_content, count = re.subn( + r'^MODEM_VERSION = .*$', + f'MODEM_VERSION = "{version}"', + content, + count=1, + flags=re.MULTILINE, + ) + if count != 1: + print("::error::Could not find MODEM_VERSION in freedata_server/constants.py") + sys.exit(1) + + with open(path, "w") as f: + f.write(new_content) + EOF + grep "^MODEM_VERSION" freedata_server/constants.py + - name: Build GUI working-directory: freedata_gui run: | @@ -39,7 +90,7 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.14.0 - if: startsWith(github.ref, 'refs/tags/v') with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} + skip-existing: true diff --git a/Dockerfile b/Dockerfile index 1f0de20a2..86b2b15bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG HAMLIB_VERSION=4.5.5 ENV HAMLIB_VERSION=${HAMLIB_VERSION} RUN apt-get update && \ - apt-get install --upgrade -y fonts-noto-color-emoji git build-essential cmake portaudio19-dev python3-pyaudio python3-colorama wget && \ + apt-get install --upgrade -y fonts-noto-color-emoji git build-essential cmake portaudio19-dev python3-colorama wget && \ mkdir -p /app/FreeDATA WORKDIR /src diff --git a/freedata_server/constants.py b/freedata_server/constants.py index 781c2bae2..10497a720 100644 --- a/freedata_server/constants.py +++ b/freedata_server/constants.py @@ -1,6 +1,32 @@ # Module for saving some constants +import os +import sys + + +def _default_app_dir() -> str: + """ + Per-user directory for config, database and log file, following each + OS's own convention rather than forcing a single layout everywhere: + - Windows: %APPDATA%\\FreeDATA + - macOS: ~/Library/Application Support/FreeDATA + - Linux: $XDG_CONFIG_HOME/FreeDATA or ~/.config/FreeDATA + Used only when FREEDATA_CONFIG / FREEDATA_DATABASE are not set (e.g. a + plain `pip install freedata` run). Keeping this outside the installed + package directory means it survives package upgrades/reinstalls. + """ + home = os.path.expanduser("~") + if sys.platform == "win32": + base = os.getenv("APPDATA") or home + elif sys.platform == "darwin": + base = os.path.join(home, "Library", "Application Support") + else: + base = os.getenv("XDG_CONFIG_HOME") or os.path.join(home, ".config") + return os.path.join(base, "FreeDATA") + + CONFIG_ENV_VAR = "FREEDATA_CONFIG" DEFAULT_CONFIG_FILE = "config.ini" +DEFAULT_APP_DIR = _default_app_dir() MODEM_VERSION = "0.18.1" API_VERSION = 4 ARQ_PROTOCOL_VERSION = 1 diff --git a/freedata_server/message_system_db_manager.py b/freedata_server/message_system_db_manager.py index 12dcc5f27..cea8f4886 100644 --- a/freedata_server/message_system_db_manager.py +++ b/freedata_server/message_system_db_manager.py @@ -5,7 +5,7 @@ import structlog from freedata_server import helpers import os -from freedata_server.constants import MESSAGE_SYSTEM_DATABASE_VERSION +from freedata_server.constants import MESSAGE_SYSTEM_DATABASE_VERSION, DEFAULT_APP_DIR class DatabaseManager: @@ -42,18 +42,19 @@ def get_database(self): This method determines the database file path based on the environment variable `FREEDATA_DATABASE`. If the variable is set, its value is used as the path. Otherwise, it defaults to - `freedata-messages.db` in the script directory. + `freedata-messages.db` in the per-user app directory + (DEFAULT_APP_DIR), so a plain `pip install` keeps the database + outside the installed package and it survives upgrades. Returns: str: The database file path as a SQLAlchemy URL. """ - script_directory = os.path.dirname(os.path.abspath(__file__)) - if self.DATABASE_ENV_VAR in os.environ: - # db_path = os.getenv(self.DATABASE_ENV_VAR, os.path.join(script_directory, self.DEFAULT_DATABASE_FILE)) db_path = os.getenv(self.DATABASE_ENV_VAR) else: - db_path = os.path.join(script_directory, self.DEFAULT_DATABASE_FILE) + db_path = os.path.join(DEFAULT_APP_DIR, self.DEFAULT_DATABASE_FILE) + + os.makedirs(os.path.dirname(db_path), exist_ok=True) return "sqlite:///" + db_path def initialize_default_values(self): diff --git a/freedata_server/server.py b/freedata_server/server.py index 2da863389..84da828e9 100644 --- a/freedata_server/server.py +++ b/freedata_server/server.py @@ -1,4 +1,5 @@ import os +import shutil import sys import threading @@ -10,7 +11,7 @@ from fastapi.staticfiles import StaticFiles from freedata_server.log_handler import setup_logging -from freedata_server.constants import CONFIG_ENV_VAR, DEFAULT_CONFIG_FILE, API_VERSION +from freedata_server.constants import CONFIG_ENV_VAR, DEFAULT_CONFIG_FILE, DEFAULT_APP_DIR, API_VERSION from freedata_server.context import AppContext from freedata_server.api.general import router as general_router @@ -27,18 +28,39 @@ # --- Resolve config path FIRST (no logger needed yet) --- def resolve_config_path() -> str: """ - Determine the configuration file to use (env var or default next to this file). - Exits if not found. + Determine the configuration file to use. + + Uses FREEDATA_CONFIG if set, otherwise defaults to a per-user config + directory (DEFAULT_APP_DIR). If no config file exists yet at that + location, a fresh one is bootstrapped from the bundled + config.ini.example template so a plain `pip install freedata` followed + by `freedata` works out of the box without any manual setup. """ - candidate = os.getenv( - CONFIG_ENV_VAR, - os.path.join(os.path.dirname(__file__), DEFAULT_CONFIG_FILE), + candidate = os.path.abspath( + os.getenv( + CONFIG_ENV_VAR, + os.path.join(DEFAULT_APP_DIR, DEFAULT_CONFIG_FILE), + ) ) + if not os.path.exists(candidate): # We cannot log to file yet since we don't know the directory; write to stderr. - sys.stderr.write(f"[FATAL] Config file not found: {candidate}\n") - sys.exit(1) - return os.path.abspath(candidate) + template = os.path.join(os.path.dirname(__file__), "config.ini.example") + try: + os.makedirs(os.path.dirname(candidate), exist_ok=True) + if os.path.isfile(template): + shutil.copyfile(template, candidate) + sys.stderr.write(f"[INFO] No config found - created a default one at: {candidate}\n") + else: + sys.stderr.write( + f"[FATAL] Config file not found and no template available to create one: {candidate}\n" + ) + sys.exit(1) + except OSError as e: + sys.stderr.write(f"[FATAL] Could not create config file at {candidate}: {e}\n") + sys.exit(1) + + return candidate config_file = resolve_config_path() @@ -95,11 +117,15 @@ async def nocache(request: Request, call_next): # Static GUI mounting +# Order matters: prefer paths anchored to this file's location (work no +# matter what the current working directory is) over cwd-relative +# fallbacks kept for backwards compatibility with older layouts. potential_gui_dirs = [ + os.path.join(os.path.dirname(__file__), "gui"), # nuitka standalone build + os.path.join(os.path.dirname(os.path.dirname(__file__)), "freedata_gui", "dist"), # pip install (sibling package) "../freedata_gui/dist", "freedata_gui/dist", "FreeDATA/freedata_gui/dist", - os.path.join(os.path.dirname(__file__), "gui"), ] gui_dir = next((d for d in potential_gui_dirs if os.path.isdir(d)), None) if gui_dir: diff --git a/pyproject.toml b/pyproject.toml index cc64850eb..6685995de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ requires-python = ">=3.10" dependencies = [ "numpy", "psutil", - "PyAudio", "pyserial", "sounddevice", "structlog", @@ -78,12 +77,13 @@ nuitka = [ [tool.setuptools.packages.find] where = [ "." ] -exclude = [ - "tools*", +include = [ + "freedata_server*", + "freedata_gui", ] [tool.setuptools.package-data] -freedata_server = [ "lib/**/*" ] +freedata_server = [ "lib/**/*", "config.ini.example" ] freedata_gui = [ "dist/**/*" ] [tool.setuptools.dynamic] diff --git a/requirements.txt b/requirements.txt index 97d2bd0d0..533e64e4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ numpy psutil -PyAudio pyserial sounddevice structlog diff --git a/tools/Linux/install-freedata-linux.sh b/tools/Linux/install-freedata-linux.sh index f69c0fb9e..da3ab5a13 100755 --- a/tools/Linux/install-freedata-linux.sh +++ b/tools/Linux/install-freedata-linux.sh @@ -44,6 +44,9 @@ # # # Changelog: +# 2.10: 24 Jul 2026 +# Remove python3-pyaudio (unused dependency, FreeDATA uses sounddevice) +# # 2.9: 10 Jan Sep 2026 # Add Ubuntu 24.10 and 25.04 # Change hamlib default version to 4.6.5 @@ -164,7 +167,7 @@ case $osname in "Debian GNU/Linux") case $osversion in "11" | "12" | "13") - sudo apt install --upgrade -y fonts-noto-color-emoji git build-essential cmake python3 portaudio19-dev python3-pyaudio python3-pip python3-colorama python3-venv wget python3-dev + sudo apt install --upgrade -y fonts-noto-color-emoji git build-essential cmake python3 portaudio19-dev python3-pip python3-colorama python3-venv wget python3-dev ;; *) @@ -182,7 +185,7 @@ case $osname in "Ubuntu" | "Linux Mint") case $osversion in "21.3" | "22.04" | "24.04" | "24.10" | "25.04" ) - sudo apt install --upgrade -y fonts-noto-color-emoji git build-essential cmake python3 portaudio19-dev python3-pyaudio python3-pip python3-colorama python3-venv wget python3-dev + sudo apt install --upgrade -y fonts-noto-color-emoji git build-essential cmake python3 portaudio19-dev python3-pip python3-colorama python3-venv wget python3-dev ;; *) @@ -197,7 +200,7 @@ case $osname in "Fedora Linux") case $osversion in "VERSION_ID=40" | "VERSION_ID=41") - sudo dnf install -y git cmake make automake gcc gcc-c++ kernel-devel wget portaudio-devel python3-pyaudio python3-pip python3-colorama python3-virtualenv google-noto-emoji-fonts python3-devel + sudo dnf install -y git cmake make automake gcc gcc-c++ kernel-devel wget portaudio-devel python3-pip python3-colorama python3-virtualenv google-noto-emoji-fonts python3-devel ;; esac ;; From 21a85289e7ba2d9f724ede94a9584817b092e137 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Fri, 24 Jul 2026 23:07:59 +0200 Subject: [PATCH 2/2] attempt fixing nsis --- freedata_server/codec2.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/freedata_server/codec2.py b/freedata_server/codec2.py index 62bbfb997..a694f4cbd 100644 --- a/freedata_server/codec2.py +++ b/freedata_server/codec2.py @@ -89,20 +89,36 @@ def freedv_get_mode_name_by_value(mode: int) -> str: return FREEDV_MODE(mode).name -# Get the directory of the current script file -script_dir = os.path.dirname(os.path.abspath(__file__)) +# Determine the base directory to search for the codec2 shared library. +# +# In normal (non-frozen) execution this is simply the directory containing +# this script, and that's where "lib/codec2/*" lives relative to +# freedata_server/codec2.py. +# +# When compiled by Nuitka into a standalone binary however, data files added +# via --include-data-dir/--include-data-files (e.g. "lib=lib") are placed +# relative to the *distribution* directory (next to the produced .exe), not +# relative to this module's own (nested) package directory. Using +# os.path.dirname(__file__) in that case points at "/freedata_server" +# while the actual DLL ends up at "/lib/codec2/libcodec2.dll" - a +# sibling directory, not a child - so the glob below never finds it. +# +# Nuitka exposes the correct directory via the compiled-only global +# `__compiled__.containing_dir`, which always points at the distribution +# directory regardless of platform or nesting. See: +# https://nuitka.net/user-documentation/common-issue-solutions.html#standalone-finding-files +try: + script_dir = __compiled__.containing_dir # type: ignore[name-defined] +except NameError: + script_dir = os.path.dirname(os.path.abspath(__file__)) # Use script_dir to construct the paths for file search if sys.platform == "linux": - files = glob.glob(os.path.join(script_dir, "**/*libcodec2*"), recursive=True) - # files.append(os.path.join(script_dir, "libcodec2.so")) + files = glob.glob(os.path.join(script_dir, "**", "*libcodec2*"), recursive=True) elif sys.platform == "darwin": - if hasattr(sys, "_MEIPASS"): - files = glob.glob(os.path.join(getattr(sys, "_MEIPASS"), "**/*libcodec2*"), recursive=True) - else: - files = glob.glob(os.path.join(script_dir, "**/*libcodec2*.dylib"), recursive=True) + files = glob.glob(os.path.join(script_dir, "**", "*libcodec2*.dylib"), recursive=True) elif sys.platform in ["win32", "win64"]: - files = glob.glob(os.path.join(script_dir, "**\\*libcodec2*.dll"), recursive=True) + files = glob.glob(os.path.join(script_dir, "**", "*libcodec2*.dll"), recursive=True) else: files = [] api = None