name: CI on: push: branches: [main] tags: ['v*'] pull_request: workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: ghcr.io/sulfurnitride/fluorine-builder jobs: build: runs-on: ubuntu-latest permissions: contents: write packages: write steps: - name: Checkout uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Pull or build Docker image run: | if docker pull ${{ env.IMAGE_NAME }}:latest; then docker tag ${{ env.IMAGE_NAME }}:latest fluorine-builder:latest else echo "Pull failed, building from scratch..." docker build -t fluorine-builder:latest docker/ fi - name: Restore ccache uses: actions/cache@v4 with: path: ~/.cache/fluorine-ccache key: ccache-${{ runner.os }}-${{ github.sha }} restore-keys: | ccache-${{ runner.os }}- - name: Determine build metadata id: channel run: | # Tag push (refs/tags/v*) → stable release. # Branch push / PR / workflow_dispatch → beta rolling build. # Stable releases are CI-built (not local) so AVX-512 / host CPU # flags can never leak into the binary that users download. if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then CHANNEL=stable VERSION="${GITHUB_REF#refs/tags/v}" else CHANNEL=beta VERSION="" fi echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "timestamp=$(date -u +%Y%m%d%H%M)" >> "$GITHUB_OUTPUT" echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - name: Build inside container run: | mkdir -p ~/.cache/fluorine-ccache docker run --rm \ -v "${{ github.workspace }}:/src:rw" \ -v "$HOME/.cache/fluorine-ccache:/ccache:rw" \ -e CCACHE_DIR=/ccache \ -e CCACHE_BASEDIR=/src \ -e CCACHE_COMPILERCHECK=content \ -e CCACHE_SLOPPINESS=time_macros,include_file_mtime,include_file_ctime,pch_defines \ -e CCACHE_MAXSIZE=5G \ -e BUILD_MODE=tarball \ -e FLUORINE_BUILD_CHANNEL=${{ steps.channel.outputs.channel }} \ -e FLUORINE_BUILD_TIMESTAMP=${{ steps.channel.outputs.timestamp }} \ -e FLUORINE_BUILD_COMMIT=${{ steps.channel.outputs.commit }} \ -w /src \ fluorine-builder:latest \ bash -c 'set -e; uv pip install --system --break-system-packages --python /opt/python-bundled/bin/python3 --no-cache --upgrade pybind11==2.13.6 sip psutil larian-formats==0.2.0 vdf "PyQt6>=6.11,<6.12"; ccache -s; bash /src/docker/build-inner.sh; status=$?; echo "=== ccache stats after build ==="; ccache -s; exit $status' - name: Package tarball run: | # The docker build runs as root inside the container, leaving # build/ root-owned on the runner. Fix up ownership so subsequent # steps (tar write, artifact upload) can operate on it as the # regular runner user. sudo chown -R "$(id -u):$(id -g)" build cd build if [[ "${{ steps.channel.outputs.channel }}" == "stable" ]]; then ARCHIVE_NAME="fluorine-manager-${{ steps.channel.outputs.version }}.tar.gz" else ARCHIVE_NAME="fluorine-manager-beta.tar.gz" fi tar czf "${ARCHIVE_NAME}" fluorine-manager/ echo "archive=${ARCHIVE_NAME}" >> "$GITHUB_ENV" echo "archive_path=build/${ARCHIVE_NAME}" >> "$GITHUB_ENV" - name: Upload build uses: actions/upload-artifact@v4 with: name: Fluorine-Manager path: build/fluorine-manager/ compression-level: 6 if-no-files-found: warn # ------------------------------------------------------------------- # Beta rolling release: every push to main overwrites the `beta` tag's # release assets so the in-app updater always sees the latest build # without cluttering the release history with new tags. # ------------------------------------------------------------------- - name: Publish beta rolling release if: github.ref == 'refs/heads/main' && github.event_name == 'push' && steps.channel.outputs.channel == 'beta' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} run: | TAG="beta" BETA_TS="${{ steps.channel.outputs.timestamp }}" BETA_COMMIT="${GITHUB_SHA}" BETA_SHORT="${BETA_COMMIT:0:7}" TITLE="Fluorine Manager beta ${BETA_TS} (${BETA_SHORT})" NOTES_FILE="$(mktemp)" { echo "Rolling beta build from main @ ${BETA_SHORT}." echo "" echo "- Channel: beta" echo "- Timestamp (UTC): ${BETA_TS}" echo "- Commit: ${BETA_COMMIT}" echo "" echo "This release is overwritten with each successful CI build on main." echo "For stable releases, see tagged v* releases." echo "" # Machine-parseable block used by the in-app updater to decide # whether the currently-installed beta build already matches. Do # not remove or reformat — the updater reads it line-by-line. echo "" } > "${NOTES_FILE}" # Move the rolling tag to the current commit. git tag -f "${TAG}" "${GITHUB_SHA}" git push -f origin "refs/tags/${TAG}" if gh release view "${TAG}" >/dev/null 2>&1; then gh release edit "${TAG}" \ --title "${TITLE}" \ --notes-file "${NOTES_FILE}" \ --prerelease # Replace the tarball asset (clobber wins over upload+delete race). gh release upload "${TAG}" "${archive_path}" --clobber else gh release create "${TAG}" "${archive_path}" \ --title "${TITLE}" \ --notes-file "${NOTES_FILE}" \ --prerelease fi # ------------------------------------------------------------------- # Stable release: triggered by pushing a v* tag. Uses CI's generic # x86-64 toolchain so AVX-512 (or other host-CPU flags) from the # maintainer's local box never leak into the binary users download. # ------------------------------------------------------------------- - name: Publish stable release if: startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' && steps.channel.outputs.channel == 'stable' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} run: | TAG="${GITHUB_REF#refs/tags/}" VERSION="${{ steps.channel.outputs.version }}" COMMIT="${GITHUB_SHA}" SHORT="${COMMIT:0:7}" TITLE="Fluorine Manager ${VERSION}" NOTES_FILE="$(mktemp)" { echo "Stable release ${VERSION} built from ${TAG} @ ${SHORT}." echo "" echo "- Channel: stable" echo "- Commit: ${COMMIT}" echo "" echo "See the GitHub release notes for the full list of changes." } > "${NOTES_FILE}" EXISTING_DRAFT="$(gh release view "${TAG}" --json isDraft --jq '.isDraft' 2>/dev/null || true)" if [[ -n "${EXISTING_DRAFT}" ]]; then if [[ "${EXISTING_DRAFT}" == "true" ]]; then gh release edit "${TAG}" \ --title "${TITLE}" \ --draft \ --prerelease=false \ --latest=false else gh release edit "${TAG}" \ --title "${TITLE}" \ --notes-file "${NOTES_FILE}" \ --draft=false \ --prerelease=false \ --latest fi gh release upload "${TAG}" "${archive_path}" --clobber else gh release create "${TAG}" "${archive_path}" \ --title "${TITLE}" \ --notes-file "${NOTES_FILE}" \ --latest fi build-nixos-mobase: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install Nix uses: cachix/install-nix-action@v30 with: nix_path: nixpkgs=channel:nixos-unstable - name: Cache Nix store uses: actions/cache@v4 with: path: /tmp/nix-cache key: nix-mobase-${{ hashFiles('nix/mobase-shell.nix') }} restore-keys: nix-mobase- - name: Build mobase.so for NixOS run: | nix-shell nix/mobase-shell.nix --run " set -euo pipefail # Build libloot from source (not in nixpkgs) git clone --depth 1 https://github.com/loot/libloot.git /tmp/libloot cmake -S /tmp/libloot/cpp -B /tmp/libloot/build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ -DLIBLOOT_INSTALL_DOCS=OFF \ -DBUILD_TESTING=OFF \ -DCMAKE_INSTALL_PREFIX=/tmp/libloot-install cmake --build /tmp/libloot/build --parallel cmake --install /tmp/libloot/build # Configure full project (only mobase target will be built) # Use NIX_PYTHON3 (real nix python) for cmake, not the venv wrapper PYBIND11_DIR=\$(python3 -c 'import pybind11; print(pybind11.get_cmake_dir())') cmake -S . -B build-nix -G Ninja \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DPython_EXECUTABLE=\"\${NIX_PYTHON3}\" \ -Dpybind11_DIR=\"\${PYBIND11_DIR}\" \ -DBUILD_PLUGIN_PYTHON=ON \ -DCMAKE_PREFIX_PATH=/tmp/libloot-install \ -Dlibloot_DIR=/tmp/libloot-install # Build only mobase target cmake --build build-nix --target mobase --parallel # Package it mkdir -p nixos-mobase cp build-nix/libs/plugin_python/src/mobase/mobase.cpython-*-linux-gnu.so nixos-mobase/ echo 'NixOS-compatible mobase pybind11 module for Fluorine Manager.' > nixos-mobase/README.txt echo 'Drop this .so into your plugins/libs/ directory, replacing the existing one.' >> nixos-mobase/README.txt " - name: Upload NixOS mobase uses: actions/upload-artifact@v4 with: name: mobase-nixos path: nixos-mobase/ compression-level: 6 if-no-files-found: warn