diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 00000000..7ebdd000 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,58 @@ +# Build Callimachus from its checksum-verified, prebuilt Linux release archive. +name: Snap package + +on: + pull_request: + paths: + - 'snap/**' + - 'packaging/prepare_snap_release.py' + - '.github/workflows/snap.yml' + workflow_dispatch: + inputs: + release_tag: + description: Published Callimachus release (e.g. v2.0.2) + type: string + required: true + default: v2.0.2 + release: + types: [published] + +permissions: + contents: read + +jobs: + snap: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Pin the exact GitHub release and Linux asset checksum + env: + CALLIMACHUS_RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag || 'v2.0.2' }} + run: python packaging/prepare_snap_release.py "$CALLIMACHUS_RELEASE_TAG" + - name: Build Snap package + id: build + uses: canonical/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 + - name: Retain built package for testing + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: callimachus-snap + path: ${{ steps.build.outputs.snap }} + if-no-files-found: error + - name: Check whether store publishing is configured + id: creds + env: + STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + run: | + if [ -n "$STORE_CREDENTIALS" ]; then + echo 'available=true' >> "$GITHUB_OUTPUT" + else + echo 'available=false' >> "$GITHUB_OUTPUT" + fi + - name: Upload to Snap Store edge channel + if: github.event_name != 'pull_request' && steps.creds.outputs.available == 'true' + uses: canonical/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ steps.build.outputs.snap }} + release: edge diff --git a/packaging/prepare_snap_release.py b/packaging/prepare_snap_release.py new file mode 100644 index 00000000..a2b74161 --- /dev/null +++ b/packaging/prepare_snap_release.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Pin a Snapcraft build to an immutable, checksummed GitHub release asset.""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from urllib.parse import quote +from urllib.request import Request, urlopen + + +REPOSITORY = "SNatangelo/Callimachus" +ASSET_NAME = "Callimachus-linux-x64.tar.gz" +SNAPCRAFT_YAML = Path(__file__).resolve().parents[1] / "snap" / "snapcraft.yaml" + + +def replace_once(text: str, pattern: str, replacement: str) -> str: + updated, count = re.subn(pattern, lambda _: replacement, text, flags=re.MULTILINE) + if count != 1: + raise RuntimeError(f"expected exactly one Snapcraft field matching {pattern!r}; got {count}") + return updated + + +def main(tag: str) -> None: + if not re.fullmatch(r"v\d+\.\d+\.\d+", tag): + raise ValueError(f"expected a stable release tag (vMAJOR.MINOR.PATCH), got {tag!r}") + + url = f"https://api.github.com/repos/{REPOSITORY}/releases/tags/{quote(tag)}" + request = Request(url, headers={"Accept": "application/vnd.github+json", "User-Agent": "Callimachus-Snapcraft"}) + with urlopen(request, timeout=30) as response: + release = json.load(response) + if release.get("draft") or release.get("prerelease") or release.get("tag_name") != tag: + raise RuntimeError("the requested tag is not a published stable release") + assets = [asset for asset in release.get("assets", []) if asset.get("name") == ASSET_NAME] + if len(assets) != 1: + raise RuntimeError(f"expected one asset named {ASSET_NAME}; found {len(assets)}") + asset = assets[0] + digest = asset.get("digest", "") + download_url = asset.get("browser_download_url", "") + expected_url = f"https://github.com/{REPOSITORY}/releases/download/{tag}/{ASSET_NAME}" + if not re.fullmatch(r"sha256:[0-9a-f]{64}", digest) or download_url != expected_url: + raise RuntimeError("release asset digest or download URL is missing or inconsistent") + + recipe = SNAPCRAFT_YAML.read_text(encoding="utf-8") + recipe = replace_once(recipe, r"^version: .*$", f"version: '{tag[1:]}'") + recipe = replace_once(recipe, r"^ source: https://github\.com/SNatangelo/Callimachus/releases/download/.*$", f" source: {download_url}") + recipe = replace_once(recipe, r"^ source-checksum: sha256/[0-9a-f]{64}$", f" source-checksum: sha256/{digest[7:]}") + SNAPCRAFT_YAML.write_text(recipe, encoding="utf-8") + print(f"Pinned Snapcraft to {tag}: {digest}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise SystemExit("usage: python packaging/prepare_snap_release.py vX.Y.Z") + main(sys.argv[1]) diff --git a/snap/gui/callimachus.desktop b/snap/gui/callimachus.desktop new file mode 100644 index 00000000..4aef1b07 --- /dev/null +++ b/snap/gui/callimachus.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=Callimachus +GenericName=Citation Verifier +Comment=Verify citations and trace source evidence +Exec=callimachus +Icon=${SNAP}/meta/gui/callimachus.png +Terminal=false +Categories=Science;Education;Office; +StartupNotify=true diff --git a/snap/gui/callimachus.png b/snap/gui/callimachus.png new file mode 100644 index 00000000..7028b769 Binary files /dev/null and b/snap/gui/callimachus.png differ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000..db1bdc7b --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,60 @@ +name: callimachus +title: Callimachus +base: core22 +version: '2.0.2' +summary: Verify citations and source evidence in scientific manuscripts +description: | + Callimachus is an open-source citation verification workspace for scientific + writing. It checks references, identifies potentially unsupported claims, + retrieves source evidence, and produces traceable verification reports. + + The desktop interface includes guided source retrieval, PDF handling, and + configurable language-model checks. Results remain reviewable by the user. + + To store provider credentials in the Linux Secret Service, connect the + password-manager-service interface after installation: + sudo snap connect callimachus:password-manager-service + + Files on removable drives require the removable-media connection. +website: https://callimachus.science +source-code: https://github.com/SNatangelo/Callimachus +issues: https://github.com/SNatangelo/Callimachus/issues +license: AGPL-3.0-only +grade: stable +confinement: strict +architectures: + - build-on: amd64 + build-for: amd64 +icon: snap/gui/callimachus.png + +apps: + callimachus: + command: Callimachus/Callimachus + plugs: + - desktop + - desktop-legacy + - home + - network + - opengl + - password-manager-service + - removable-media + - wayland + - x11 + +parts: + callimachus: + plugin: dump + # Use the audited, prebuilt Linux bundle from the GitHub release rather + # than rebuilding the Qt/PySide/PyInstaller toolchain a second time. + # packaging/prepare_snap_release.py updates all three pinned values for CI. + source: https://github.com/SNatangelo/Callimachus/releases/download/v2.0.2/Callimachus-linux-x64.tar.gz + source-type: tar + source-checksum: sha256/19567db4ecc13835753f25b9977224a78d5d2ef379197ae4c32be7b302eb154d + stage-packages: + - libegl1 + - libfontconfig1 + - libfreetype6 + - libgl1 + - libgtk-3-0 + - libxcb1 + - libxkbcommon-x11-0