diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e57a1c..41a1972 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,3 +68,14 @@ jobs: - name: Test working-directory: src-tauri run: cargo test + + # The flake at whatever release nix/release.json pins. A broken flake or lock, or a deb that no + # longer patches against current nixpkgs, shows up here rather than at the next release. + nix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: cachix/install-nix-action@v31 + + - run: nix build .#margin-calendar --print-build-logs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88d70c0..0a461dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -180,72 +180,57 @@ jobs: done gh release edit "$TAG" --repo "$REPO" --draft=false --latest - # Arch gets its own package rather than the AppImage. The AppImage carries Ubuntu's GTK stack, - # which cannot talk to a modern Wayland compositor and silently falls back to Xwayland; a package - # linked against the system webkit2gtk runs as a native Wayland client. Runs after publish so it + # Nix is the Linux package. The AppImage carries Ubuntu's GTK stack, which cannot talk to a modern + # Wayland compositor and silently falls back to Xwayland; the Nix package relinks the published deb + # against nixpkgs' webkit2gtk and runs as a native Wayland client. Runs after publish so the flake # can only ever point at a release that survived the manifest check. - aur: + nix: needs: [prepare, publish] runs-on: ubuntu-latest steps: - # The tag, not main, so the template and the licence are the ones this release shipped. + # main rather than the tag: the pin lands on main, and the tag was cut before the artifact + # it needs the hash of existed. - uses: actions/checkout@v7 with: - ref: ${{ needs.prepare.outputs.tag }} + ref: main - - name: Render the PKGBUILD for this release + - name: Pin the flake to this release env: VERSION: ${{ needs.prepare.outputs.version }} REPO: ${{ github.repository }} run: | URL="https://github.com/$REPO/releases/download/v$VERSION/Margin.Calendar_${VERSION}_amd64.deb" - # From the published asset, so the checksum is of the artifact users will actually fetch. + # From the published asset, so the hash is of the artifact users will actually fetch. curl -fsSL --retry 3 -o package.deb "$URL" - SHA=$(sha256sum package.deb | cut -d' ' -f1) - # The PKGBUILD fetches the licence from the tag, so checksum the copy at that same tag. - LICENSE_SHA=$(sha256sum LICENSE | cut -d' ' -f1) - sed -e "s/@VERSION@/$VERSION/g" \ - -e "s/@SHA256@/$SHA/g" \ - -e "s/@LICENSE_SHA256@/$LICENSE_SHA/g" \ - packaging/aur/PKGBUILD.in > PKGBUILD - echo "pkgver $VERSION, deb $SHA, licence $LICENSE_SHA" + HASH="sha256-$(openssl dgst -sha256 -binary package.deb | base64)" + jq -n --arg v "$VERSION" --arg h "$HASH" '{version: $v, hash: $h}' > nix/release.json + cat nix/release.json - - name: Generate .SRCINFO - # makepkg is Arch-only and refuses to run as root, hence the container. It runs as the - # runner's own uid so nothing in the bind-mounted checkout changes owner; chowning it to a - # container user left the workspace unwritable for the push step. --printsrcinfo parses the - # PKGBUILD, it does not build anything. - run: | - docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD:/w" -w /w \ - archlinux:base-devel makepkg --printsrcinfo > .SRCINFO - cat .SRCINFO + - uses: cachix/install-nix-action@v31 - - name: Push to the AUR + # Building it is the check: a wrong hash, a library autoPatchelf cannot find or a broken flake + # stops here rather than on someone's machine. + - name: Build the package + run: nix build .#margin-calendar --print-build-logs + + - name: Commit the pin env: - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} VERSION: ${{ needs.prepare.outputs.version }} run: | - if [ -z "$AUR_SSH_PRIVATE_KEY" ]; then - echo "::warning::AUR_SSH_PRIVATE_KEY is not set, so the AUR package was built but not published. The release itself is unaffected." - exit 0 - fi - mkdir -p ~/.ssh && chmod 700 ~/.ssh - printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur - chmod 600 ~/.ssh/aur - ssh-keyscan -t ed25519,rsa aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null - export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur -o IdentitiesOnly=yes" - git clone ssh://aur@aur.archlinux.org/margin-calendar-bin.git aur - cp PKGBUILD .SRCINFO aur/ - cd aur - # A package that does not exist yet clones as an empty repo, where the local branch name - # is whatever git defaults to. The AUR only accepts master. - git checkout -B master git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add PKGBUILD .SRCINFO - if git diff --cached --quiet; then - echo "AUR is already at this version, nothing to push." - exit 0 - fi - git commit -m "margin-calendar-bin $VERSION" - git push origin master + git add nix/release.json + git commit -m "point the nix package at v$VERSION" + for attempt in 1 2 3 4 5; do + git fetch origin main + git rebase origin/main + if git push origin HEAD:main; then + break + fi + if [ "$attempt" = "5" ]; then + echo "::error::main kept advancing; could not push the nix pin after 5 attempts. The release is published; rerun this job." + exit 1 + fi + echo "main advanced; rebasing and retrying ($attempt)…" + sleep 3 + done diff --git a/.gitignore b/.gitignore index 1f86787..934c2bc 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ xcuserdata/ # Screenshots & Playwright MCP artifacts .playwright-mcp/ /*.png + +# Nix build output +/result +/result-* diff --git a/README.md b/README.md index 4cdb59f..1b54b49 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ fits without scrolling, and on a desktop the whole thing is drivable from the ke It is a sibling to [margin](https://github.com/priyanshujain/margin) and shares its stack and its visual language. -`just install` builds it and installs it on the machine you are on. Setup and the Google OAuth -client it needs are in [docs/setup.md](docs/setup.md), and the extra clients a phone build needs -are in [docs/mobile.md](docs/mobile.md). How releases are cut is in -[docs/release.md](docs/release.md). The product decisions are in [docs/design.md](docs/design.md), -how it is built is in [docs/architecture.md](docs/architecture.md), and the conventions it follows -are in [docs/conventions.md](docs/conventions.md). +`just install` builds it and installs it on the machine you are on. On Linux, +`nix profile install github:priyanshujain/margin-calendar` installs the released build instead. +Setup and the Google OAuth client it needs are in [docs/setup.md](docs/setup.md), and the extra +clients a phone build needs are in [docs/mobile.md](docs/mobile.md). How releases are cut and how +the Nix package is made are in [docs/release.md](docs/release.md). The product decisions are in +[docs/design.md](docs/design.md), how it is built is in [docs/architecture.md](docs/architecture.md), +and the conventions it follows are in [docs/conventions.md](docs/conventions.md). diff --git a/docs/architecture.md b/docs/architecture.md index 43a0c12..c4b75fb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -133,7 +133,7 @@ Closing the window on macOS hides it rather than quitting, the way WhatsApp and process stays in the Dock with sync running, a Dock click brings the window back, and Cmd+Q quits. Both halves live in `lib.rs`. Linux has no traffic lights, so that padding is conditional, and closing the window there quits. Linux builds need `libwebkit2gtk-4.1-dev` and -ship as AppImage and deb. +ship as AppImage and deb, and the deb is what the Nix package in `nix/package.nix` relinks. ## Order of work diff --git a/docs/release.md b/docs/release.md index 1b432e6..923cc98 100644 --- a/docs/release.md +++ b/docs/release.md @@ -36,27 +36,47 @@ targets, at which point the publish gate wants `windows-x86_64` too. Phones do not come from this pipeline at all. The store is their update channel, and what building for one takes is in [mobile.md](mobile.md). -## Arch +## Linux -Arch gets its own package, `margin-calendar-bin` on the AUR, pushed by the release workflow after -the manifest check passes. It is worth the extra moving part: the AppImage bundles Ubuntu's GTK -stack, and a bundled `libwayland-client` cannot talk to a current compositor, so on Hyprland the -AppImage silently falls back to Xwayland. The packaged build links against the system +Linux installs come from Nix rather than from the AppImage. The AppImage bundles Ubuntu's GTK +stack, and a bundled `libwayland-client` cannot talk to a current compositor, so on Hyprland it +silently falls back to Xwayland. The Nix package relinks the published `.deb` against nixpkgs' own `webkit2gtk-4.1` and runs as a native Wayland client. +Installing, and later updating: + +``` +nix profile install github:priyanshujain/margin-calendar +nix profile upgrade margin-calendar +``` + +On NixOS or under home-manager, add `github:priyanshujain/margin-calendar` as a flake input and +either put `margin-calendar.packages.x86_64-linux.default` in the package list or apply +`margin-calendar.overlays.default` and use `pkgs.margin-calendar`. Updating is then +`nix flake update margin-calendar` and a rebuild. + +"Check for updates" inside a Nix install still checks. It reports the newer version and the +upgrade command rather than installing anything, because the package's wrapper sets +`MARGIN_CALENDAR_PACKAGED_BY=nix` and the app asks for that before it would touch its own binary, +which in the store it could not replace anyway. + It is a binary package by necessity rather than laziness. The Google OAuth client is embedded at compile time from a file that is deliberately not in the repo, so anything built from source on -someone else's machine would run and then tell them Google Calendar is not set up. The PKGBUILD -therefore repackages the published `.deb`, whose payload is already a normal `/usr` tree. +someone else's machine would run and then tell them Google Calendar is not set up. +`nix/package.nix` therefore unpacks the published `.deb`, whose payload is already a normal `/usr` +tree, and `autoPatchelfHook` points its libraries at nixpkgs. -`packaging/aur/PKGBUILD.in` is the template. The workflow fills in the version and the sha256 of -the artifact that was actually published, generates `.SRCINFO` with `makepkg` in an Arch container, -and pushes to `ssh://aur@aur.archlinux.org/margin-calendar-bin.git` using `AUR_SSH_PRIVATE_KEY`. -Without that secret the job renders the package, says so, and does not fail the release. +`nix/release.json` is the pin: the version and the hash of the `.deb` that was actually published. +The `nix` job in the release workflow writes it after the manifest check passes, builds the package +once to prove the pin is good, and commits it to main. That commit is what Nix users consume: +`github:priyanshujain/margin-calendar` means main, and main means the latest release that built. A +tag's own flake still points at the release before it, because the tag is cut before the artifact +exists. If the job fails, the release is out and Nix stays a version behind until the job is rerun. -The `license=('custom')` line is a placeholder for the fact that this repo has no licence file at -all. Nothing stops the package publishing, but a package on the AUR that nobody has licensed is -worth fixing before anyone else builds on it. +The flake is x86_64-linux only, which is the one Linux target the release builds. `flake.lock` +pins nixpkgs for anyone installing through the flake directly; the overlay uses whatever nixpkgs +the host already has. CI builds the package on every push, so a nixpkgs bump that breaks the +patching shows up before a release does. ## What the build needs diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4e641bd --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1788316716, + "narHash": "sha256-bc7rSpXIdn9QWGNqfWcPZWOhEVF8NoeAZkWq0XWnf/k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3ed67ec0a4d3c7ab4ae1f04f8ee8df07bfa506a2", + "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 new file mode 100644 index 0000000..80c9dcd --- /dev/null +++ b/flake.nix @@ -0,0 +1,22 @@ +{ + description = "Margin Calendar, a calendar for Google Calendar"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + in + { + overlays.default = final: prev: { + margin-calendar = final.callPackage ./nix/package.nix { }; + }; + + packages.${system} = { + margin-calendar = pkgs.callPackage ./nix/package.nix { }; + default = self.packages.${system}.margin-calendar; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..a705370 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,87 @@ +# The Linux package: the deb the release workflow published, relinked against nixpkgs' own GTK and +# WebKit so it runs as a native Wayland client. It is a binary package by necessity: the Google +# OAuth client is embedded at compile time from a file that is deliberately not in the repo, so +# anything built from source here would run and then say Google Calendar is not set up. +{ + lib, + stdenv, + fetchurl, + dpkg, + autoPatchelfHook, + wrapGAppsHook3, + cairo, + dbus, + gdk-pixbuf, + glib, + glib-networking, + gsettings-desktop-schemas, + gtk3, + libsoup_3, + webkitgtk_4_1, + xdg-utils, +}: + +let + release = lib.importJSON ./release.json; +in +stdenv.mkDerivation { + pname = "margin-calendar"; + inherit (release) version; + + src = fetchurl { + url = "https://github.com/priyanshujain/margin-calendar/releases/download/v${release.version}/Margin.Calendar_${release.version}_amd64.deb"; + inherit (release) hash; + }; + + unpackPhase = '' + runHook preUnpack + dpkg-deb -x $src . + runHook postUnpack + ''; + + nativeBuildInputs = [ + dpkg + autoPatchelfHook + wrapGAppsHook3 + ]; + + buildInputs = [ + cairo + dbus + gdk-pixbuf + glib + glib-networking + gsettings-desktop-schemas + gtk3 + libsoup_3 + webkitgtk_4_1 + ]; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -r usr/bin usr/share $out/ + mv "$out/share/applications/Margin Calendar.desktop" $out/share/applications/margin-calendar.desktop + runHook postInstall + ''; + + # The app opens the Google consent page and "Report an issue" through xdg-open. The variable is + # how it knows the store owns the binary, so "Check for updates" points at Nix instead of trying + # to replace itself. + preFixup = '' + gappsWrapperArgs+=( + --prefix PATH : ${lib.makeBinPath [ xdg-utils ]} + --set MARGIN_CALENDAR_PACKAGED_BY nix + ) + ''; + + meta = { + description = "A calendar for Google Calendar"; + longDescription = "A calendar where the grid owns the window and the day always fits without scrolling."; + homepage = "https://github.com/priyanshujain/margin-calendar"; + license = lib.licenses.mit; + sourceProvenance = [ lib.sourceTypes.binaryNativeCode ]; + platforms = [ "x86_64-linux" ]; + mainProgram = "margin-calendar"; + }; +} diff --git a/nix/release.json b/nix/release.json new file mode 100644 index 0000000..f30583c --- /dev/null +++ b/nix/release.json @@ -0,0 +1,4 @@ +{ + "version": "0.0.4", + "hash": "sha256-ZYrpxD0/7AxIeBMmYHufZU0I6/m2JWPMMSBk4CkJWd8=" +} diff --git a/packaging/aur/PKGBUILD.in b/packaging/aur/PKGBUILD.in deleted file mode 100644 index f7ea777..0000000 --- a/packaging/aur/PKGBUILD.in +++ /dev/null @@ -1,37 +0,0 @@ -# Maintainer: PJ - -# The template the release workflow renders. @VERSION@ and @SHA256@ are filled in from the release -# that has just published, so the PKGBUILD on the AUR always points at an artifact that exists. -# -# A binary package rather than one built from source, and that is forced rather than lazy: the -# Google OAuth client is embedded at compile time from a file that is not in the repo, so anything -# built from source on a stranger's machine runs and then says Google Calendar is not set up. - -pkgname=margin-calendar-bin -pkgver=@VERSION@ -pkgrel=1 -pkgdesc="A calendar for Google Calendar, where the grid owns the window and the day fits without scrolling" -arch=('x86_64') -url="https://github.com/priyanshujain/margin-calendar" -license=('MIT') -depends=('webkit2gtk-4.1' 'gtk3') -provides=('margin-calendar') -conflicts=('margin-calendar') -# Prebuilt and already linked: stripping it again buys nothing and risks the binary. -options=('!strip' '!debug') -source=("${pkgname}-${pkgver}.deb::${url}/releases/download/v${pkgver}/Margin.Calendar_${pkgver}_amd64.deb" - "LICENSE-${pkgver}::https://raw.githubusercontent.com/priyanshujain/margin-calendar/v${pkgver}/LICENSE") -noextract=("${pkgname}-${pkgver}.deb") -sha256sums=('@SHA256@' - '@LICENSE_SHA256@') - -package() { - bsdtar -xOf "${pkgname}-${pkgver}.deb" data.tar.gz | bsdtar -xf - -C "${pkgdir}" - # The bundler names it after the product, spaces and all. Everything that reads this directory - # copes with that, and nothing that reads it enjoys it. - mv "${pkgdir}/usr/share/applications/Margin Calendar.desktop" \ - "${pkgdir}/usr/share/applications/margin-calendar.desktop" - # MIT is not one of the licences Arch keeps in /usr/share/licenses/common, so every package - # under it has to carry its own copy. - install -Dm644 "LICENSE-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -}