From 503300b4f35e7d26ec6ad9cb35554a5a927ab5c2 Mon Sep 17 00:00:00 2001 From: Felix Rudat Date: Mon, 17 Aug 2026 19:17:53 +0200 Subject: [PATCH 1/2] Apply ruff format to parse.py Pre-existing formatting drift that was failing `make lint` on main. Whitespace and line-wrapping only, no behaviour change. Co-Authored-By: Claude Opus 5 --- src/fithitcli/parse.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/fithitcli/parse.py b/src/fithitcli/parse.py index d2532ec..a5c7234 100644 --- a/src/fithitcli/parse.py +++ b/src/fithitcli/parse.py @@ -83,10 +83,7 @@ def _check_link_works(link: str, timeout: int) -> bool: return False return True except urllib.error.HTTPError as exc: - if ( - exc.code in RETRYABLE_HTTP_STATUS_CODES - and attempt < LINK_CHECK_RETRIES - ): + if exc.code in RETRYABLE_HTTP_STATUS_CODES and attempt < LINK_CHECK_RETRIES: time.sleep(0.25 * (2**attempt)) continue return False @@ -130,9 +127,9 @@ def _filter_unreachable_link_rows( timeout: int = LINK_CHECK_TIMEOUT_SECONDS, checker: Callable[[str, int], bool] = _check_link_works, ) -> tuple[int, int]: - prepared_tables: list[tuple[dict[str, Any], list[tuple[dict[str, Any], str | None]]]] = ( - [] - ) + prepared_tables: list[ + tuple[dict[str, Any], list[tuple[dict[str, Any], str | None]]] + ] = [] unique_links: set[str] = set() for table in content.get("tables", []): From 2d3306428cc48cda6fe08a4923d1ef19210c216f Mon Sep 17 00:00:00 2001 From: Felix Rudat Date: Mon, 17 Aug 2026 19:18:04 +0200 Subject: [PATCH 2/2] Ship Linux binaries via Homebrew fithit was macOS-arm64 only, purely because of how it was distributed: the spec pinned target_arch='arm64', release built on a single macOS runner, and the tap formula had one url/sha256 pointing at a Mach-O tarball. Installing via Linuxbrew produced a binary the system could not execute. The Python code itself was already portable (XDG data paths, stdlib urllib), so no source changes were needed. - fithit.spec: target_arch=None, so builds follow the host arch - Makefile: detect host os/arch, pick shasum vs sha256sum, name tarballs fithit-cli---.tar.gz; build via the spec instead of duplicating flags, so the committed spec is no longer dead code - packaging/fithit.rb.tmpl: formula is now generated from this repo rather than hand-edited in the tap. on_macos/on_linux x on_arm/on_intel url+sha pairs, plus depends_on arch: :arm64 on macOS so Intel Macs get a clear Homebrew error instead of an unrunnable binary - release.yml: matrix over macos-14, ubuntu-22.04, ubuntu-22.04-arm; each uploads its asset and passes its checksum as an artifact, then bump-tap renders the template and opens a tap PR. Replaces brew bump-formula-pr, which cannot express three url/sha pairs - ci.yml: build on all three platforms so Linux breakage shows up on PRs Linux is built on 22.04 rather than 24.04 because PyInstaller output is only forward-compatible w.r.t. glibc. The result runs on glibc 2.28+ (verified on Rocky 8, Debian 11, Ubuntu 20.04/22.04). Supported: macOS arm64, Linux x86_64, Linux arm64. No Intel macOS. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 22 ++++++++ .github/workflows/release.yml | 95 +++++++++++++++++++++++++++-------- AGENTS.md | 8 +++ Makefile | 37 ++++++++++---- README.md | 26 +++++++++- fithit.spec | 2 +- packaging/fithit.rb.tmpl | 32 ++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 9 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 packaging/fithit.rb.tmpl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b2d1ab..b26c5d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,25 @@ jobs: - name: Test run: make test + + build: + strategy: + fail-fast: false + matrix: + runner: [macos-14, ubuntu-22.04, ubuntu-22.04-arm] + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Setup project + run: | + uv python install 3.13 + make setup + + - name: Build and smoke test binary + run: make smoke diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74099e8..de241b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,13 +6,26 @@ on: permissions: contents: write - pull-requests: write jobs: - build-and-bump-tap: - runs-on: macos-14 # Apple Silicon runner - env: - HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} + build: + strategy: + fail-fast: false + matrix: + include: + - runner: macos-14 # Apple Silicon + os: macos + arch: arm64 + # Built on 22.04, not 24.04, on purpose: PyInstaller binaries are only + # forward-compatible w.r.t. glibc. The 22.04 output is verified to run + # on glibc 2.28+ (Rocky 8, Debian 11, Ubuntu 20.04+). + - runner: ubuntu-22.04 + os: linux + arch: x86_64 + - runner: ubuntu-22.04-arm + os: linux + arch: arm64 + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 @@ -35,28 +48,70 @@ jobs: - name: Package tarball run: | VERSION="${GITHUB_REF_NAME#v}" - TAR="fithit-cli-${VERSION}-macos.tar.gz" - tar -czf "$TAR" -C dist fithit - echo "VERSION=$VERSION" >> "$GITHUB_ENV" + TAR="fithit-cli-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" + # COPYFILE_DISABLE keeps macOS tar from embedding AppleDouble xattrs. + COPYFILE_DISABLE=1 tar -czf "$TAR" -C dist fithit echo "TAR=$TAR" >> "$GITHUB_ENV" - name: Compute sha256 run: | - SHA256=$(shasum -a 256 "$TAR" | awk '{print $1}') - echo "SHA256=$SHA256" >> "$GITHUB_ENV" + if command -v sha256sum >/dev/null 2>&1; then + SHA256=$(sha256sum "$TAR" | awk '{print $1}') + else + SHA256=$(shasum -a 256 "$TAR" | awk '{print $1}') + fi + echo "$SHA256" > "sha256-${{ matrix.os }}-${{ matrix.arch }}.txt" - name: Upload release asset env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release upload "$GITHUB_REF_NAME" "$TAR" --clobber + run: gh release upload "$GITHUB_REF_NAME" "$TAR" --clobber + + - name: Share checksum with tap job + uses: actions/upload-artifact@v4 + with: + name: sha256-${{ matrix.os }}-${{ matrix.arch }} + path: sha256-${{ matrix.os }}-${{ matrix.arch }}.txt + + bump-tap: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download checksums + uses: actions/download-artifact@v4 + with: + pattern: sha256-* + merge-multiple: true + path: checksums - - name: Bump tap formula + - name: Render formula and open tap PR + env: + GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} run: | - URL="https://github.com/voydz/fithit/releases/download/${GITHUB_REF_NAME}/${TAR}" - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - brew tap voydz/homebrew-tap - brew bump-formula-pr voydz/tap/fithit \ - --url "$URL" \ - --sha256 "$SHA256" + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + BRANCH="bump-fithit-${VERSION}" + + git clone --depth 1 \ + "https://x-access-token:${GH_TOKEN}@github.com/voydz/homebrew-tap.git" tap + git -C tap config user.name "github-actions[bot]" + git -C tap config user.email "github-actions[bot]@users.noreply.github.com" + git -C tap checkout -b "$BRANCH" + + sed \ + -e "s/@VERSION@/${VERSION}/g" \ + -e "s/@SHA256_MACOS_ARM64@/$(cat checksums/sha256-macos-arm64.txt)/" \ + -e "s/@SHA256_LINUX_ARM64@/$(cat checksums/sha256-linux-arm64.txt)/" \ + -e "s/@SHA256_LINUX_X86_64@/$(cat checksums/sha256-linux-x86_64.txt)/" \ + packaging/fithit.rb.tmpl > tap/Formula/fithit.rb + + git -C tap commit -am "fithit ${VERSION}" + git -C tap push origin "$BRANCH" + + gh pr create \ + --repo voydz/homebrew-tap \ + --head "$BRANCH" \ + --title "fithit ${VERSION}" \ + --body "Automated bump for [fithit ${GITHUB_REF_NAME}](https://github.com/voydz/fithit/releases/tag/${GITHUB_REF_NAME}). Builds: macOS arm64, Linux arm64, Linux x86_64." diff --git a/AGENTS.md b/AGENTS.md index 2dd5882..cd23fb5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,3 +22,11 @@ Standard: `$XDG_DATA_HOME/fithit/workouts.json` (falls back to the standard XDG - `fithit parse `: extrahiert `content.json` aus der `.dtable` (ZIP) und schreibt `workouts.json` + `summary.json`. - `fithit search ...`: filtert Workouts 1:1 wie das ursprüngliche Script `filter_workouts.py`. - `fithit info`: Live-Statistiken aus `workouts.json`. + +## Build & Release + +- `make build` baut die Binary via `fithit.spec` (PyInstaller, immer für die Host-Architektur — Cross-Compiling gibt es nicht). +- `make package` erzeugt `dist/fithit-cli---.tar.gz` für die Host-Plattform. +- Unterstützte Release-Ziele: macOS `arm64`, Linux `x86_64`, Linux `arm64`. Kein Intel-macOS. +- Linux wird bewusst auf Ubuntu 22.04 gebaut (nicht 24.04): PyInstaller-Binaries sind nur vorwärtskompatibel. Das Ergebnis läuft ab glibc 2.28 (getestet: Rocky 8, Debian 11, Ubuntu 20.04/22.04). +- Die Homebrew-Formula wird generiert. `packaging/fithit.rb.tmpl` ist die Quelle; nicht direkt im Tap editieren. diff --git a/Makefile b/Makefile index 6c314df..eb0fe31 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,29 @@ .DEFAULT_GOAL := check +# Release artifacts are named fithit-cli---.tar.gz. +# The binary is always built for the host platform; cross-compiling is not +# supported by PyInstaller, so each target gets its own CI runner. +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + +ifeq ($(UNAME_S),Darwin) + TARGET_OS := macos + SHA256 := shasum -a 256 +else + TARGET_OS := linux + SHA256 := sha256sum +endif + +ifneq ($(filter $(UNAME_M),arm64 aarch64),) + TARGET_ARCH := arm64 +else + TARGET_ARCH := x86_64 +endif + +VERSION ?= $(shell grep '^version' pyproject.toml | head -1 | cut -d'"' -f2) +TARBALL := fithit-cli-$(VERSION)-$(TARGET_OS)-$(TARGET_ARCH).tar.gz + setup: uv venv uv sync --extra dev @@ -23,20 +46,14 @@ test: check: lint test build: - uv run pyinstaller \ - --onefile \ - --name fithit \ - --target-arch arm64 \ - --collect-all rich \ - src/fithitcli/__main__.py + uv run pyinstaller --clean --noconfirm fithit.spec package: build @set -e; \ - VERSION=$$(grep '^version' pyproject.toml | head -1 | cut -d'"' -f2); \ - echo "Packaging fithit v$$VERSION..."; \ + echo "Packaging fithit v$(VERSION) for $(TARGET_OS)/$(TARGET_ARCH)..."; \ cd dist && \ - tar -czf "fithit-cli-$$VERSION-macos.tar.gz" fithit && \ - shasum -a 256 "fithit-cli-$$VERSION-macos.tar.gz" + COPYFILE_DISABLE=1 tar -czf "$(TARBALL)" fithit && \ + $(SHA256) "$(TARBALL)" smoke: build @set -e; \ diff --git a/README.md b/README.md index 2c82dab..98724a3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,18 @@ Fast, deterministic, and ideal for scripts, notebooks, or a personal training da brew install voydz/tap/fithit ``` +Works with Homebrew on macOS and with Homebrew on Linux (Linuxbrew). Prebuilt +binaries are published for: + +| Platform | Architecture | +| --- | --- | +| macOS | `arm64` (Apple Silicon) | +| Linux | `x86_64`, `arm64` | + +Intel Macs are not covered by a prebuilt binary — install from source there. +The Linux binaries are built on Ubuntu 22.04 and verified to run on glibc 2.28 +and newer (tested on Rocky Linux 8, Debian 11, Ubuntu 20.04/22.04). + Or install from source with [uv](https://docs.astral.sh/uv/): ```bash @@ -82,8 +94,18 @@ make test ## Homebrew (Tap) -The tap repo is `voydz/homebrew-tap`, and the formula lives at `Formula/fithit.rb`. -Before publishing, update `homepage`, `url`, and `sha256`. +The tap repo is `voydz/homebrew-tap`, and the formula lives at `Formula/fithit.rb`. + +The formula is generated, not hand-edited: `packaging/fithit.rb.tmpl` in this +repo is the source of truth. On a published release, `.github/workflows/release.yml` +builds one binary per platform, uploads the tarballs as release assets, renders +the template with the three checksums, and opens a PR against the tap. + +To package locally for the host platform only: + +```bash +make package +``` ## Skill Integration diff --git a/fithit.spec b/fithit.spec index d5ed9b7..1b584b0 100644 --- a/fithit.spec +++ b/fithit.spec @@ -39,7 +39,7 @@ exe = EXE( console=True, disable_windowed_traceback=False, argv_emulation=False, - target_arch='arm64', + target_arch=None, # build for the host arch; see Makefile codesign_identity=None, entitlements_file=None, ) diff --git a/packaging/fithit.rb.tmpl b/packaging/fithit.rb.tmpl new file mode 100644 index 0000000..f0b44b9 --- /dev/null +++ b/packaging/fithit.rb.tmpl @@ -0,0 +1,32 @@ +class Fithit < Formula + desc "CLI zum Parsen und Durchsuchen von Apple Fitness+ Workouts" + homepage "https://github.com/voydz/fithit" + version "@VERSION@" + + on_macos do + # Only Apple Silicon binaries are published. + depends_on arch: :arm64 + + url "https://github.com/voydz/fithit/releases/download/v@VERSION@/fithit-cli-@VERSION@-macos-arm64.tar.gz" + sha256 "@SHA256_MACOS_ARM64@" + end + + on_linux do + on_arm do + url "https://github.com/voydz/fithit/releases/download/v@VERSION@/fithit-cli-@VERSION@-linux-arm64.tar.gz" + sha256 "@SHA256_LINUX_ARM64@" + end + on_intel do + url "https://github.com/voydz/fithit/releases/download/v@VERSION@/fithit-cli-@VERSION@-linux-x86_64.tar.gz" + sha256 "@SHA256_LINUX_X86_64@" + end + end + + def install + bin.install "fithit" + end + + test do + assert_match "fithit", shell_output("#{bin}/fithit --help") + end +end diff --git a/pyproject.toml b/pyproject.toml index ca1a090..cf9a194 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fithit-cli" -version = "0.1.0" +version = "0.2.0" description = "CLI zum Parsen und Durchsuchen von Apple Fitness+ Workouts" readme = "README.md" requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index e1dffc6..b1e44c6 100644 --- a/uv.lock +++ b/uv.lock @@ -43,7 +43,7 @@ wheels = [ [[package]] name = "fithit-cli" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "rich" },