From cbcbf30228d37e472c03796746ea14f72590a8b2 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Fri, 21 Aug 2026 15:06:06 +0100 Subject: [PATCH] build: drop the Nix packaging and test inside the container The deployment target is a container on TrueNAS Scale, so the flake sat on no path between the source and the NAS. It was carried over from a sibling project where the flake is the deployment mechanism; here it only added a second build path and a second dependency pin. Worse, it tested the wrong thing: `nix flake check` ran the suite against nixpkgs' ffmpeg while the shipped artefact contains Debian's, and encoder and muxer behaviour is exactly what these tests cover. The Dockerfile gains a `test` stage that installs pytest and runs the suite against the image's own ffmpeg; a failing test fails the build. CI runs `docker build --target test` in place of the host-based Python setup, and the push build states `target: runtime` so the published image is the lean stage rather than the last one in the file. The runtime image carries neither the tests nor pytest. The release step now calls python3 rather than python, since setup-python is no longer in the job to provide the alias. Removes package.nix, flake.nix and flake.lock. A dev shell is a `nix shell` away for anyone who wants one, and the README says so. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 4 -- .gitea/workflows/build-and-publish.yaml | 24 +++++------- Dockerfile | 15 +++++++- README.md | 30 +++++++-------- flake.lock | 27 ------------- flake.nix | 42 -------------------- package.nix | 51 ------------------------- 7 files changed, 37 insertions(+), 156 deletions(-) delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 package.nix diff --git a/.dockerignore b/.dockerignore index 7dff806..edd1516 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,8 @@ .git .gitignore -tests result result-* __pycache__ *.pyc .pytest_cache -flake.nix -flake.lock -package.nix compose.yaml diff --git a/.gitea/workflows/build-and-publish.yaml b/.gitea/workflows/build-and-publish.yaml index 057279f..c751672 100644 --- a/.gitea/workflows/build-and-publish.yaml +++ b/.gitea/workflows/build-and-publish.yaml @@ -43,20 +43,11 @@ jobs: # from the conventional-commit messages since the last release. fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - # The test suite runs real encodes, so ffmpeg is a test dependency. - - name: Install ffmpeg - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y ffmpeg - - - name: Install test dependencies - run: python -m pip install --upgrade pip && pip install pytest - - - name: Run unit tests - run: python -m pytest + # The suite runs inside the image, against the ffmpeg that ships, rather + # than against whatever the runner happens to provide. A failing test + # fails the build. Layers are shared with the push build below. + - name: Run the test suite inside the image + run: docker build --target test -t music-mirror:test . - name: Determine registry host run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV" @@ -145,6 +136,9 @@ jobs: uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 with: context: . + # Without this the last stage in the Dockerfile -- the test stage -- + # would be what gets published. + target: runtime # amd64 for the NAS, arm64 so the same image runs on a Pi. platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} @@ -165,7 +159,7 @@ jobs: run: | set -euo pipefail - python - "$VERSION" <<'PY' + python3 - "$VERSION" <<'PY' import pathlib import re import sys diff --git a/Dockerfile b/Dockerfile index 3dc3283..8f50791 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.13-slim +FROM python:3.13-slim AS runtime ENV PYTHONUNBUFFERED=1 @@ -10,8 +10,19 @@ RUN apt-get update \ WORKDIR /app COPY pyproject.toml README.md ./ COPY music_mirror.py ./ -RUN pip install --no-cache-dir . +RUN pip install --no-cache-dir . \ + && rm -rf build music_mirror.egg-info # Runs as root by default so a bind-mounted dataset of any ownership is # writable. Override with `user:` in compose to run as the dataset's owner. ENTRYPOINT ["music-mirror"] + +# Test stage: the suite runs against this image's own ffmpeg, which is the one +# that ships. Build it with `--target test`; a failing test fails the build. +# The published image is the `runtime` stage above and carries none of this. +FROM runtime AS test + +RUN pip install --no-cache-dir pytest +COPY pytest.ini ./ +COPY tests ./tests +RUN python -m pytest diff --git a/README.md b/README.md index 1e33785..7c5fffe 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,7 @@ music-mirror --source /music --mirror /music-mp3 --subdir "Artist/Album" `--subdir` never prunes: a partial pass cannot tell an orphan from a file outside its own scope. -Requires `ffmpeg` and `ffprobe` on `PATH`. The container and the Nix package -both provide them. +Requires `ffmpeg` and `ffprobe` on `PATH`. The container image provides both. ## Running it on TrueNAS Scale @@ -103,25 +102,26 @@ New Lidarr imports are picked up on the next pass. With `MUSIC_MIRROR_INTERVAL` at `6h` that is the worst case; run `--subdir` by hand if you want an album immediately. -## Nix - -```sh -nix run .#music-mirror -- --source ./flac --mirror ./mp3 -nix build .#music-mirror # the test suite runs as part of the build -nix develop # python, pytest and ffmpeg -``` - -`overlays.default` provides `pkgs.music-mirror`. - ## Tests ```sh -pytest +docker build --target test . # what CI runs +pytest # needs ffmpeg and pytest on PATH ``` -The tests run real ffmpeg encodes rather than mocking them — the interesting +The suite runs real ffmpeg encodes rather than mocking them. The interesting failures are in what ffmpeg actually does with tags, cover art and container -formats, and a mock cannot fail that way. They skip if ffmpeg is absent. +formats, and a mock cannot fail that way — which is also why CI runs the tests +_inside the image_, against the ffmpeg that ships, rather than against whatever +the build runner provides. The published image is the `runtime` stage and +carries neither the tests nor pytest. + +Run them directly instead if you prefer; they skip when ffmpeg is absent. On a +Nix machine: + +```sh +nix shell nixpkgs#python3Packages.pytest nixpkgs#ffmpeg -c pytest +``` ## Getting the result onto an iPod diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 841c839..0000000 --- a/flake.lock +++ /dev/null @@ -1,27 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1787135253, - "narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 9a4d27a..0000000 --- a/flake.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - description = "Maintain a lossy MP3 mirror of a lossless music library"; - - inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - - outputs = - { self, nixpkgs }: - let - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system}); - in - { - overlays.default = final: _prev: { - music-mirror = final.callPackage ./package.nix { }; - }; - - packages = forAllSystems (pkgs: rec { - music-mirror = pkgs.callPackage ./package.nix { }; - default = music-mirror; - }); - - # The package builds only if the test suite passes, so this covers both. - checks = forAllSystems (pkgs: { inherit (self.packages.${pkgs.system}) music-mirror; }); - - devShells = forAllSystems (pkgs: { - default = pkgs.mkShellNoCC { - packages = [ - pkgs.python3 - pkgs.python3Packages.pytest - pkgs.ffmpeg - ]; - }; - }); - - formatter = forAllSystems (pkgs: pkgs.nixfmt-tree); - }; -} diff --git a/package.nix b/package.nix deleted file mode 100644 index 65b5d77..0000000 --- a/package.nix +++ /dev/null @@ -1,51 +0,0 @@ -{ - lib, - python3Packages, - makeWrapper, - ffmpeg, -}: -python3Packages.buildPythonApplication { - pname = "music-mirror"; - inherit ((lib.importTOML ./pyproject.toml).project) version; - pyproject = true; - - src = lib.fileset.toSource { - root = ./.; - fileset = lib.fileset.unions [ - ./music_mirror.py - ./pyproject.toml - ./pytest.ini - ./tests - ]; - }; - - build-system = [ python3Packages.setuptools ]; - nativeBuildInputs = [ makeWrapper ]; - - # ffmpeg and ffprobe are called as subprocesses, so they belong on the - # wrapper's PATH rather than in the Python environment. - makeWrapperArgs = [ - "--prefix" - "PATH" - ":" - (lib.makeBinPath [ ffmpeg ]) - ]; - - nativeCheckInputs = [ - python3Packages.pytestCheckHook - ffmpeg - ]; - - meta = { - description = "Maintain a lossy MP3 mirror of a lossless music library"; - longDescription = '' - Reproduces a lossless library, path for path, as MP3 in a separate tree: - tags and cover art carried across, sources that are already MP3 copied - rather than re-encoded, and mirror files whose source has gone deleted. - The source library is never written to. - ''; - homepage = "https://code.emmathe.dev/lyrathorpe/music-mirror"; - mainProgram = "music-mirror"; - platforms = lib.platforms.unix; - }; -}