diff --git a/.gitea/workflows/publish-arch-package.yml b/.gitea/workflows/publish-arch-package.yml deleted file mode 100644 index b9c59f6..0000000 --- a/.gitea/workflows/publish-arch-package.yml +++ /dev/null @@ -1,368 +0,0 @@ -name: Publish Arch Package - -# Builds the `triple-c-bin` Arch package (packaging/arch/PKGBUILD) for a -# given release, or the latest one if none is given, and attaches the built -# .pkg.tar.zst to that release on GitHub as a downloadable asset. Manual -# dispatch only — deliberately not triggered by `release` or `push`, for the -# same reason sync-release.yml (removed in triple-c#32) never worked safely -# as an automatic trigger: this repo's releases are assembled by -# build-app.yml across three separate platform jobs, and there is no single -# automatic event that fires only once everything (including the Linux .deb -# this workflow needs) is actually uploaded. A human deciding "this release -# is ready, go package it" is the correct trigger, the same reasoning -# backfill-releases.yml already uses for its own manual-only GitHub sync. -# -# ## What this does and does not do -# -# It renders `packaging/arch/PKGBUILD` for one specific version (real -# download URL, real sha256sums — never guessed; see the resolve-asset step), -# validates it with `makepkg`/`namcap` in a real Arch container, and attaches -# the resulting `.pkg.tar.zst` — installable by hand with `pacman -U` — to -# *both* the GitHub release it was built from and the corresponding Gitea -# release (the plain, unsuffixed `vX.Y.Z` tag build-app.yml's Linux job -# creates; the `-win`/`-mac` suffixed Gitea releases are a different tag and -# don't get this asset). It does NOT commit anything back to this repo — -# `packaging/arch/PKGBUILD` stays a hand-maintained template with a -# placeholder version, and the workflow never starts from or writes to it. -# -# ## Not published to the AUR (yet) -# -# This originally also pushed the rendered PKGBUILD to an AUR git repo, which -# needs a maintainer AUR account and its SSH key registered as a secret here -# — both manual, one-time steps neither this workflow nor anyone but a -# maintainer can do. Until that setup happens, a downloadable release asset -# gets the same package to users without it. The AUR push step is still in -# this file's git history (see the commit that added this comment) if that -# setup is ever done and it's worth reinstating. -on: - workflow_dispatch: - inputs: - version: - description: >- - Release version to package, without a leading "v" (e.g. "0.4.14"). - Leave empty to use the latest published GitHub release. - required: false - -env: - GITHUB_REPO: shadowdao/triple-c - GITEA_URL: ${{ gitea.server_url }} - REPO: ${{ gitea.repository }} - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Resolve version and find the Linux asset - id: resolve - env: - VERSION_INPUT: ${{ inputs.version }} - GH_PAT: ${{ secrets.GH_PAT }} - run: | - set -euo pipefail - - # Authenticated when the secret is available (it is, everywhere - # else in this repo's workflows) to avoid the unauthenticated - # 60-requests/hour-per-IP cap; still works without it, just at that - # lower limit, since this hits nothing but a public repo's public - # releases. - AUTH=() - [ -n "${GH_PAT}" ] && AUTH=(-H "Authorization: Bearer ${GH_PAT}") - - if [ -z "${VERSION_INPUT}" ]; then - echo "No version given — resolving the latest GitHub release" - RELEASE_JSON=$(curl -fsS "${AUTH[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") - else - echo "Using requested version ${VERSION_INPUT}" - RELEASE_JSON=$(curl -fsS "${AUTH[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${VERSION_INPUT}") - fi - - TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name') - VERSION="${TAG#v}" - echo "Resolved to ${TAG}" - - # Discovered from the real release, not assumed: Tauri names the - # asset after `productName` verbatim ("Triple-C"), not the - # lowercase Cargo binary name, and asset naming is exactly the kind - # of thing that silently drifts if a future Tauri upgrade changes - # bundler defaults — a hardcoded pattern here would then 404 - # forever until someone noticed. `head -1` guards against a release - # somehow carrying more than one matching asset, which would - # otherwise pass the emptiness check below and then break the - # download step with two URLs on one line. - DEB_URL=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .browser_download_url' | head -1) - DEB_NAME=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .name' | head -1) - if [ -z "$DEB_URL" ] || [ "$DEB_URL" = "null" ]; then - echo "No *_amd64.deb asset found on release ${TAG}" >&2 - exit 1 - fi - echo "Found asset: ${DEB_NAME}" - - # For attaching the built package to this same release later — - # every release object carries its own `upload_url` regardless of - # whether it was just created or (as here) already existed, and - # the `{?name,label}` URI-template suffix has to come off before - # this is usable as a plain URL to POST to. - RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id') - UPLOAD_URL=$(echo "$RELEASE_JSON" | jq -r '.upload_url' | sed 's/{?name,label}//') - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "deb_url=${DEB_URL}" >> "$GITHUB_OUTPUT" - echo "deb_name=${DEB_NAME}" >> "$GITHUB_OUTPUT" - echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" - echo "upload_url=${UPLOAD_URL}" >> "$GITHUB_OUTPUT" - - - name: Download the release asset and compute real checksums - id: checksums - env: - DEB_URL: ${{ steps.resolve.outputs.deb_url }} - DEB_NAME: ${{ steps.resolve.outputs.deb_name }} - TAG: ${{ steps.resolve.outputs.tag }} - run: | - set -euo pipefail - curl -fsSL -o "${DEB_NAME}" "${DEB_URL}" - curl -fsSL -o LICENSE "https://raw.githubusercontent.com/${GITHUB_REPO}/${TAG}/LICENSE" - - echo "deb_sha256=$(sha256sum "${DEB_NAME}" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" - echo "license_sha256=$(sha256sum LICENSE | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" - - - name: Render PKGBUILD - id: render - env: - VERSION: ${{ steps.resolve.outputs.version }} - DEB_NAME: ${{ steps.resolve.outputs.deb_name }} - DEB_SHA256: ${{ steps.checksums.outputs.deb_sha256 }} - LICENSE_SHA256: ${{ steps.checksums.outputs.license_sha256 }} - run: | - set -euo pipefail - mkdir -p rendered - cp packaging/arch/PKGBUILD rendered/PKGBUILD - cd rendered - - # Plain string replacement throughout, not sed — the source URL - # contains slashes and the repo name does too, and getting a sed - # delimiter choice AND its escaping right for that is exactly the - # kind of thing that looks correct, passes review, and breaks the - # next time someone touches it. `re.sub` with `count=1` and an - # exact `.format`-free literal match is boring and that's the - # point: every substitution below fails loudly (an assertion / - # the checks after) rather than silently no-op'ing if the - # template's shape ever drifts from what this expects. - # - # pkgrel resets to 1 for a new pkgver — a packaging-only fix to the - # same upstream version (a dependency bump, say) is what pkgrel is - # for, and this workflow always republishes the current PKGBUILD - # verbatim rather than incrementing anything, so 1 is always - # correct for what this workflow does. It is NOT correct for a - # dependency-only fix republished at the *same* pkgver: pkgrel - # would be forced back to 1, and no existing installation sees an - # upgrade. That case needs a manual pkgrel bump in the template - # before dispatching, which this workflow has no input for. - python3 - "$VERSION" "$DEB_NAME" "$DEB_SHA256" "$LICENSE_SHA256" "$GITHUB_REPO" <<'PY' - import re, sys - version, deb_name, deb_sha, license_sha, github_repo = sys.argv[1:6] - - with open("PKGBUILD") as f: - text = f.read() - - text, n = re.subn(r"(?m)^pkgver=.*$", f"pkgver={version}", text, count=1) - assert n == 1, "pkgver=... line not found" - text, n = re.subn(r"(?m)^pkgrel=.*$", "pkgrel=1", text, count=1) - assert n == 1, "pkgrel=... line not found" - - # Built with a "$" variable and plain "+" concatenation rather than - # an f-string's double-brace escape for a literal brace: writing - # this as an f-string put a dollar sign directly against two open - # braces, right here in this workflow's own YAML text — and this - # runner's own expression templating scans a run: block for that - # exact two-character opening sequence and tries to evaluate - # whatever sits inside as one of ITS OWN expressions (a step - # output, a secret, ...) before the shell ever sees this script. - # "pkgver" isn't one of those, so that lookup failed and silently - # emptied this whole step rather than raising anything here. - # Spelling the dollar sign out of a variable instead means this - # file's own text never contains that trigger sequence. - DOLLAR = "$" - old_source = ( - "source=(\"Triple-C_" + DOLLAR + "{pkgver}_amd64.deb::" - + "https://github.com/" + github_repo + "/releases/download/v" + DOLLAR + "{pkgver}/" - + "Triple-C_" + DOLLAR + "{pkgver}_amd64.deb\"" - ) - new_source = ( - f'source=("{deb_name}::' - f'https://github.com/{github_repo}/releases/download/v{version}/{deb_name}"' - ) - assert old_source in text, "source=() line does not match the expected template shape" - text = text.replace(old_source, new_source, 1) - - old_sums = "sha256sums=('SKIP'\n 'SKIP')" - assert old_sums in text, "sha256sums=() placeholders not found" - text = text.replace(old_sums, f"sha256sums=('{deb_sha}'\n '{license_sha}')", 1) - - with open("PKGBUILD", "w") as f: - f.write(text) - PY - - grep -q "pkgver=${VERSION}$" PKGBUILD - ! grep -q "SKIP" PKGBUILD - - - name: Validate with makepkg and namcap - id: build - run: | - set -euo pipefail - - # A bind mount (`docker run -v "$PWD/...":/work`) is the more - # obvious way to write this, and was the first draft — but on a - # containerized Gitea act_runner job, `$PWD` is a path inside this - # job's own container, which the daemon's host cannot resolve; the - # mount would silently attach an empty directory instead of failing - # loudly. `docker cp` moves real bytes across that boundary - # regardless of where the daemon actually lives, which is what - # makes this work under both a bind-mount-capable runner and a - # containerized one. - docker pull archlinux:latest - CID=$(docker create -w /work archlinux:latest bash -c ' - set -euo pipefail - pacman -Syu --noconfirm --needed base-devel namcap sudo git openssh >/dev/null - useradd -m builder - chown -R builder:builder /work - echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder - sudo -u builder bash -c "cd /work && makepkg --printsrcinfo > .SRCINFO" - sudo -u builder bash -c "cd /work && makepkg -s --noconfirm" - # Named once here, inside the container, rather than guessed - # from options=(!strip !debug) plus pkgver/pkgrel/arch on the - # host after the fact — makepkg is the one place that actually - # knows its own output name, and `!debug` already guarantees - # this glob can only ever match the one real package (no - # -debug split package gets produced). - basename /work/*.pkg.tar.* > /work/.pkgfile - echo "--- namcap ---" - NAMCAP_OUT=$(sudo -u builder bash -c "cd /work && namcap PKGBUILD *.pkg.tar.*" || true) - echo "$NAMCAP_OUT" - # Matches "triple-c-bin E:", "PKGBUILD (triple-c-bin) E:" and any - # split-package variant ("triple-c-bin-debug E:") alike — namcap - # uses more than one line shape for its two rule families, and - # namcap itself exits 0 regardless of what it reports, so this - # grep is the only thing standing between an E: and a green job. - if echo "$NAMCAP_OUT" | grep -q " E: "; then - echo "namcap reported an error — see above" >&2 - exit 1 - fi - ') - mkdir -p rendered - docker cp rendered/. "${CID}:/work" - # `docker start -a` streams output and its exit code is the - # container's own — the same failure this would have hit with a - # bind mount still fails the job the same way. - docker start -a "${CID}" - docker cp "${CID}:/work/.SRCINFO" rendered/.SRCINFO - docker cp "${CID}:/work/.pkgfile" rendered/.pkgfile - PKG_FILE=$(cat rendered/.pkgfile) - docker cp "${CID}:/work/${PKG_FILE}" "rendered/${PKG_FILE}" - docker rm -f "${CID}" >/dev/null - - echo "pkg_file=${PKG_FILE}" >> "$GITHUB_OUTPUT" - - - name: Attach the package to the GitHub release - env: - GH_PAT: ${{ secrets.GH_PAT }} - TAG: ${{ steps.resolve.outputs.tag }} - RELEASE_ID: ${{ steps.resolve.outputs.release_id }} - UPLOAD_URL: ${{ steps.resolve.outputs.upload_url }} - PKG_FILE: ${{ steps.build.outputs.pkg_file }} - run: | - set -euo pipefail - if [ -z "${GH_PAT}" ]; then - echo "GH_PAT is not set — this step needs it to attach a release asset." >&2 - exit 1 - fi - - # A manual re-dispatch for a version that's already been packaged - # would otherwise hit GitHub's 422 "already_exists" here instead - # of just replacing the stale build with this one. - EXISTING_ID=$(curl -fsS -H "Authorization: Bearer ${GH_PAT}" -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${GITHUB_REPO}/releases/${RELEASE_ID}/assets" \ - | jq -r --arg name "$PKG_FILE" '.[] | select(.name == $name) | .id') - if [ -n "$EXISTING_ID" ]; then - echo "Replacing the existing ${PKG_FILE} (asset id ${EXISTING_ID}) already on ${TAG}" - curl -fsS -X DELETE -H "Authorization: Bearer ${GH_PAT}" -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${GITHUB_REPO}/releases/assets/${EXISTING_ID}" - fi - - curl -fsS -X POST \ - -H "Authorization: Bearer ${GH_PAT}" \ - -H "Accept: application/vnd.github+json" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@rendered/${PKG_FILE}" \ - "${UPLOAD_URL}?name=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "${PKG_FILE}")" \ - > /dev/null - - echo "Attached ${PKG_FILE} to ${TAG} on GitHub" - - - name: Attach the package to the Gitea release - env: - TOKEN: ${{ secrets.REGISTRY_TOKEN }} - TAG: ${{ steps.resolve.outputs.tag }} - PKG_FILE: ${{ steps.build.outputs.pkg_file }} - run: | - set -euo pipefail - - # Same get-or-create-by-tag, delete-existing-asset, - # upload-as-octet-stream shape build-app.yml's own Gitea upload - # step already uses — this is expected to always hit the "reuse" - # branch, since build-app.yml's Linux job already created this - # exact release for this exact tag; the create fallback is here - # only so this doesn't hard-depend on that ordering. - HTTP_CODE=$(curl -sS -o release.json -w '%{http_code}' \ - -H "Authorization: token ${TOKEN}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}") - case "${HTTP_CODE}" in - 200) - echo "Release ${TAG} already exists on Gitea, reusing" - ;; - 404) - echo "Creating release ${TAG} on Gitea" - curl -fsS -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"tag_name\": \"${TAG}\", \"name\": \"Triple-C ${TAG} (Linux)\"}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases" > release.json - ;; - *) - echo "Unexpected ${HTTP_CODE} looking up release ${TAG} on Gitea:" >&2 - cat release.json >&2 - exit 1 - ;; - esac - - RELEASE_ID=$(python3 -c "import json; print(json.load(open('release.json')).get('id',''))") - if [ -z "${RELEASE_ID}" ]; then - echo "No Gitea release id for ${TAG}; refusing to upload into nothing:" >&2 - cat release.json >&2 - exit 1 - fi - - EXISTING_ID=$(curl -sS \ - -H "Authorization: token ${TOKEN}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \ - | python3 -c "import json,sys; t=sys.argv[1]; print(next((a['id'] for a in json.load(sys.stdin) if a.get('name')==t), ''))" "${PKG_FILE}") - if [ -n "${EXISTING_ID}" ]; then - echo "Replacing the existing ${PKG_FILE} (asset id ${EXISTING_ID}) already on ${TAG}" - curl -fsS -X DELETE \ - -H "Authorization: token ${TOKEN}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${EXISTING_ID}" - fi - - curl -fsS --http1.1 \ - --retry 5 --retry-all-errors --retry-delay 5 \ - --max-time 600 \ - -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@rendered/${PKG_FILE}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${PKG_FILE}" - - echo "Attached ${PKG_FILE} to ${TAG} on Gitea" diff --git a/CLAUDE.md b/CLAUDE.md index c03fb36..2092355 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -677,6 +677,26 @@ deliberately out of scope — this is not a project backup. specifically to make them unmissable — the frontend's `
  • `/warning boxes also get `break-all` as a second layer against the same failure mode. +## Packaging + +Linux ships as `.deb`, `.rpm` and AppImage, all three built by `build-app.yml` (releases) and +`build-app-preview.yml` (the PR check). **There is deliberately no Arch package.** A +`triple-c-bin` `PKGBUILD` and a `publish-arch-package.yml` existed and were removed; they live on +`hold/arch-packaging`. Do not re-add them without the piece that was always missing: the package +was never on the AUR, so it was a manual `pacman -U` of a downloaded file — the same gesture as +the AppImage, for a second artifact to keep working. Being `workflow_dispatch`-only it also +reached 1 release in 28, while `HOW-TO-USE.md` told Arch users to download it from every release. +An AUR account and its SSH key as a repo secret are what would make it worth having; until then +the AppImage is the Arch story. + +`scripts/install-appimage.sh` is the desktop-integration half, and it exists because an AppImage +has no installer: it extracts the bundled icons into `~/.local/share/icons/hicolor` and writes a +`.desktop` entry. It **rewrites** the `Exec` line rather than copying the bundled entry — the +bundled one is `Exec=triple-c`, which resolves only inside the AppImage's own mount, so a +verbatim copy yields a launcher entry that starts nothing. It keeps `StartupWMClass` exactly as +the bundle sets it, which is what lets the shell match the window to the entry. Extraction uses +`--appimage-extract`, which needs no FUSE, so the script works before `fuse2` is installed. + ## Testing Frontend tests use Vitest with jsdom environment and React Testing Library. Setup file at `src/test/setup.ts`. Run a single test file: diff --git a/HOW-TO-USE.md b/HOW-TO-USE.md index 8975bb4..008aebd 100644 --- a/HOW-TO-USE.md +++ b/HOW-TO-USE.md @@ -43,12 +43,52 @@ Download the build for your platform from [GitHub Releases](https://github.com/s | **macOS** | `Triple-C__universal.dmg` | Open the `.dmg` and drag Triple-C to Applications. | | **Debian / Ubuntu** | `Triple-C__amd64.deb` | `sudo apt install ./Triple-C__amd64.deb` | | **Fedora / RHEL** | `Triple-C--1.x86_64.rpm` | `sudo dnf install ./Triple-C--1.x86_64.rpm` | -| **Arch / CachyOS** | `triple-c-bin--1-x86_64.pkg.tar.zst` | `sudo pacman -U ./triple-c-bin--1-x86_64.pkg.tar.zst` | -| **Other Linux** | `Triple-C__amd64.AppImage` | `chmod +x` it, then run it directly. | +| **Arch / CachyOS / other Linux** | `Triple-C__amd64.AppImage` | `chmod +x` it, then run it directly. See the AppImage notes below. | > **macOS note:** The app is not signed or notarized. On first launch, macOS Gatekeeper may block it — right-click the app and select "Open" to bypass, or remove the quarantine attribute: `xattr -cr /Applications/Triple-C.app`. -> **Arch / CachyOS note:** This package is not on the AUR — it's a `pacman`-installable file built and attached to each GitHub release by a maintainer-triggered step (`.gitea/workflows/publish-arch-package.yml`), so it can lag behind the very latest release by a bit. See [`packaging/arch/README.md`](packaging/arch/README.md) for details, including why "-bin" and what's verified about it. +> **AppImage note:** Two things are worth knowing. Running an AppImage needs FUSE 2, which Arch and CachyOS do not install by default — `sudo pacman -S fuse2` once, or run it with `--appimage-extract-and-run` to sidestep FUSE entirely. And an AppImage is just an executable file: nothing registers it with the desktop, so it will not appear in your app launcher on its own. Run [`scripts/install-appimage.sh`](scripts/install-appimage.sh) to add a launcher entry and icons — see [Adding an AppImage to the app launcher](#adding-an-appimage-to-the-app-launcher). + +> **No Arch package.** There was a `triple-c-bin` `.pkg.tar.zst` attached to some releases, built by a maintainer-triggered workflow. It was never on the AUR, so installing it meant downloading a file and running `pacman -U` — no better than the AppImage — and being manual-only it reached 1 release in 28, which made the promise of it worse than not making it. The `PKGBUILD` and its workflow are preserved on the `hold/arch-packaging` branch if an AUR package is ever worth doing properly. + +### Adding an AppImage to the app launcher + +An AppImage is a single executable file and nothing else. It carries a `.desktop` +entry and icons *inside* itself, but nothing on your system ever reads them, +because nothing installed it — so it will not show up in your app launcher, and +running it from a file manager gives you a generic icon in the taskbar. + +Put the AppImage somewhere stable first — `~/Apps` or `~/.local/bin`, not +`~/Downloads` — because the launcher entry points at wherever the file is: + +```bash +mkdir -p ~/Apps +mv ~/Downloads/Triple-C_*_amd64.AppImage ~/Apps/ +./scripts/install-appimage.sh ~/Apps/Triple-C_0.4.17_amd64.AppImage +``` + +That copies the bundled icons into `~/.local/share/icons/hicolor` and writes +`~/.local/share/applications/triple-c.desktop` pointing at the file you named. +No sudo, nothing outside your home directory, and the AppImage itself is never +copied or moved. To remove the entry again: + +```bash +./scripts/install-appimage.sh --uninstall +``` + +The script rewrites the `Exec` line rather than reusing the bundled `.desktop` +verbatim: the bundled one says `Exec=triple-c`, which resolves only inside the +running AppImage's own mount, so a launcher entry copied straight out of the +bundle would appear in the menu and then fail to start anything. + +Two follow-ups worth knowing: + +- **Upgrading.** The entry names one specific file. If you replace the AppImage + with a newer version under a different filename, re-run the script against the + new one. Keeping a stable name (`~/Apps/Triple-C.AppImage`) avoids this. +- **The icon may not appear until you log out.** That is the desktop shell's + icon cache, not a failed install — see + [App Icon Missing After Installing (Linux)](#app-icon-missing-after-installing-linux). ## Prerequisites diff --git a/TECHNICAL.md b/TECHNICAL.md index 7f05c04..c82189d 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -418,12 +418,6 @@ triple-c/ │ ├── build-stt.yml # Build the STT image │ ├── backfill-releases.yml # Bulk copy releases to GitHub │ ├── cleanup-releases.yml # Prune old releases -│ └── publish-arch-package.yml # Build triple-c-bin, attach it to the GitHub release (packaging/arch/) -│ -├── packaging/ -│ └── arch/ # triple-c-bin Arch package — see packaging/arch/README.md -│ ├── PKGBUILD -│ └── README.md │ └── app/ # Tauri v2 desktop application ├── package.json # React, xterm.js, zustand, tailwindcss diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD deleted file mode 100644 index 4c4405c..0000000 --- a/packaging/arch/PKGBUILD +++ /dev/null @@ -1,86 +0,0 @@ -# Maintainer: Triple-C Contributors -# -# This file is regenerated by .gitea/workflows/publish-arch-package.yml on every -# publish — pkgver, the source URL and sha256sums are rewritten from the real, -# already-uploaded release asset, never guessed. Editing pkgver/source/ -# sha256sums by hand here only matters until the next automated run overwrites -# them; everything else (depends, pkgdesc, package()) is meant to be hand- -# maintained normally. -# -# "-bin" rather than building from source: this repackages the same .deb -# build-app.yml already produces and publishes, so a user gets exactly the -# binary the project ships and tests, and `makepkg` never needs a Rust -# toolchain, Node, or the dozen -dev packages CLAUDE.md lists for building -# Triple-C itself. The trade-off is the one every "-bin" package makes: it -# assumes the glibc the CI runner (Ubuntu 24.04) linked against is compatible -# with the installing system's — true for essentially every currently -# supported Arch install, since Arch tracks glibc newer than Ubuntu 24.04 -# ships, and forward compatibility is the direction that holds. -pkgname=triple-c-bin -pkgver=0.4.0 -pkgrel=1 -pkgdesc="Sandbox Claude Code inside Docker containers" -arch=('x86_64') -url="https://github.com/shadowdao/triple-c" -license=('MIT') -# Verified against a real release asset (v0.4.14), not Tauri's generic docs: -# downloaded Triple-C_0.4.14_amd64.deb, installed each of these into a real -# Arch container, and re-ran `ldd` on the actual binary until nothing came -# back "not found". `pango` and `libayatana-appindicator` were both in an -# earlier draft — pango isn't directly linked (gtk3 already pulls it in -# transitively, and namcap correctly flags declaring it as redundant), and -# libayatana-appindicator is in Tauri's own linux dependency list but this -# binary never links it at all: there is no tray icon or menu in this app -# (see CLAUDE.md's note that `core:menu`/`core:tray` are dropped for the -# same reason), so it was never a real dependency to begin with. -depends=('cairo' 'desktop-file-utils' 'gdk-pixbuf2' 'glib2' 'gtk3' - 'hicolor-icon-theme' 'libsoup3' 'webkit2gtk-4.1') -optdepends=('docker: to actually run the sandboxed containers' - 'xdg-utils: opening links from the app in your default browser') -provides=('triple-c') -conflicts=('triple-c') -# !strip: the upstream .deb's binary is already the release build Tauri -# produced and tested; re-stripping a prebuilt binary is unnecessary risk for -# no benefit. It's also what actually suppresses makepkg's debug-package -# machinery here (debug-package extraction requires strip; verified in a -# real build — with !strip alone, no debug package is produced at all). -# !debug is kept anyway, explicit about intent rather than relying on that -# side effect. Without either, makepkg built a usr/src/debug/triple-c-bin -# tree containing a dangling .build-id symlink, which is a real namcap -# error (not just the empty-directory warning it looks like) — there is no -# debug info in this release binary for the machinery to have extracted in -# the first place. -options=('!strip' '!debug') -# Tauri names the asset after `productName` verbatim ("Triple-C"), not the -# lowercase Cargo binary name — verified against the real release, not -# assumed; a lowercase guess here would 404. The LICENSE fetch is separate -# because the .deb itself carries no license file — namcap flags an MIT -# package with nothing under /usr/share/licenses/ as an error, correctly. -source=("Triple-C_${pkgver}_amd64.deb::https://github.com/shadowdao/triple-c/releases/download/v${pkgver}/Triple-C_${pkgver}_amd64.deb" - "LICENSE::https://raw.githubusercontent.com/shadowdao/triple-c/v${pkgver}/LICENSE") -sha256sums=('SKIP' - 'SKIP') - -package() { - cd "$srcdir" - # A .deb is an ar archive of debian-binary, control.tar.*, data.tar.* — `ar` - # (part of base-devel's binutils) pulls just the payload out. Extracting - # that tar directly into $pkgdir works here with no path rewriting at all: - # verified against the real archive, whose entire payload is - # usr/bin/triple-c, usr/share/applications/Triple-C.desktop and - # usr/share/icons/hicolor/*/apps/triple-c.png — Tauri's Linux bundle for - # this app carries no separate resource directory under usr/lib/, so there - # is nothing that could disagree between Debian's and Arch's package trees - # for it to land in the wrong place. - # - # Globbed rather than named literally: the publish workflow discovers the - # real asset name from the release itself specifically so a Tauri bundler - # naming change can't silently break this — naming the file again here - # would throw that away and fail this one line with an opaque "No such - # file or directory" instead. `source=()` above guarantees exactly one - # `*_amd64.deb` entry, so the glob can only ever match that one file. - ar x ./*_amd64.deb - tar xf data.tar.* -C "$pkgdir" - - install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" -} diff --git a/packaging/arch/README.md b/packaging/arch/README.md deleted file mode 100644 index a3b24e6..0000000 --- a/packaging/arch/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Arch / CachyOS package - -`PKGBUILD` here is the `triple-c-bin` package's template — see triple-c#34 -(the "I would like to also have an Arch/CachyOS native version" part of it). -It's written to AUR conventions (and may go there eventually — see -"Publishing" below) but isn't published to the AUR yet. - -## Why "-bin" - -It repackages the same `.deb` `build-app.yml` already produces, rather than -building from source. That means `makepkg` never needs a Rust toolchain, -Node, or the dozen `-dev` packages CLAUDE.md lists for building Triple-C -itself — and a user gets exactly the binary the project ships and tests, -built on Ubuntu 24.04 in CI. Verified end to end against a real release -(v0.4.14): downloaded the actual `.deb`, confirmed every `depends` entry -against a real `ldd` of the actual binary (two packages that looked right -from Tauri's own docs — `pango`, `libayatana-appindicator` — turned out not -to be real dependencies of *this* binary and were dropped), and ran a real -`makepkg`/`namcap`/`pacman -U` cycle rather than guessing at the shape. - -## Publishing - -`.gitea/workflows/publish-arch-package.yml` does the actual work: given a -version (or "latest" if none is given), it finds that release's real Linux -asset on GitHub, downloads it, computes real checksums, renders this -template into a version-specific PKGBUILD, validates it with `makepkg` and -`namcap` inside a real Arch container, and attaches the resulting -`.pkg.tar.zst` to that same GitHub release as a downloadable asset — -installable by hand with `sudo pacman -U`. - -It is `workflow_dispatch`-only, deliberately — see the workflow file's own -header comment for why an automatic trigger isn't safe here (the same reason -`sync-release.yml` didn't work and was removed in triple-c#32). - -**Not on the AUR yet.** Publishing there would need a maintainer AUR account -and its SSH key added as a secret on this repo — both manual, one-time steps -on https://aur.archlinux.org that only a maintainer can do. The workflow's -git history still has the AUR-push step from before this was descoped, if -that setup happens later and it's worth reinstating. - -## What's hand-maintained vs. generated - -`pkgver`/`pkgrel`/`source`/`sha256sums` in this file are placeholders — -the workflow rewrites them for every real publish and never commits the -result back here, so don't read this file's `pkgver` as "the last published -version." Everything else (`depends`, `pkgdesc`, `package()`) is meant to be -edited by hand normally, the same as any other PKGBUILD. - -**A hand-edit made to the rendered PKGBUILD attached to a GitHub release is -not this file.** Every run renders fresh from *this* repo's template, so a -packaging fix belongs here, not in a downloaded copy — the next dispatch for -that version would just overwrite it anyway. diff --git a/scripts/install-appimage.sh b/scripts/install-appimage.sh new file mode 100755 index 0000000..b3e44f3 --- /dev/null +++ b/scripts/install-appimage.sh @@ -0,0 +1,127 @@ +#!/bin/sh +# Register a Triple-C AppImage with the desktop, so it appears in the app +# launcher with its icon instead of only being runnable from a file manager. +# +# An AppImage is a single executable file and nothing more: it ships a +# `.desktop` entry and icons *inside* itself, but nothing on the host ever +# reads them, because nothing installed it. This script does what a package +# manager's install hooks would — copies the icons into the user's icon theme +# and writes a `.desktop` entry pointing at wherever the AppImage actually +# lives. +# +# ./scripts/install-appimage.sh ~/Apps/Triple-C_0.4.17_amd64.AppImage +# ./scripts/install-appimage.sh --uninstall +# +# Everything goes under ~/.local/share, so there is no sudo and no root-owned +# file to clean up later. The AppImage itself is never copied or moved — the +# launcher entry points at the path you give here, so keep the file somewhere +# stable (`~/Apps` or `~/.local/bin`, not `~/Downloads`) or re-run this after +# moving it. +# +# Extraction uses `--appimage-extract`, which unpacks the payload directly and +# needs no FUSE. So this script works even on a system where *running* the +# AppImage would need `fuse2` installed first. + +set -eu + +APP_ID="triple-c" +DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" +ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" +DESKTOP_FILE="${DESKTOP_DIR}/${APP_ID}.desktop" + +refresh_caches() { + # Both are best-effort: a minimal desktop may ship neither, and neither + # failing means the install did not work. + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "${DESKTOP_DIR}" 2>/dev/null || true + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -f -t "${ICON_DIR}" 2>/dev/null || true + fi +} + +uninstall() { + rm -f "${DESKTOP_FILE}" + find "${ICON_DIR}" -name "${APP_ID}.png" -delete 2>/dev/null || true + refresh_caches + echo "Removed the Triple-C launcher entry and icons." + echo "The AppImage itself was not touched." +} + +if [ "${1:-}" = "--uninstall" ]; then + uninstall + exit 0 +fi + +APPIMAGE="${1:-}" +if [ -z "${APPIMAGE}" ]; then + echo "usage: $0 [--uninstall] /path/to/Triple-C__amd64.AppImage" >&2 + exit 2 +fi +if [ ! -f "${APPIMAGE}" ]; then + echo "No such file: ${APPIMAGE}" >&2 + exit 1 +fi + +# An absolute path, because the .desktop Exec line is read from anywhere. +APPIMAGE=$(cd "$(dirname "${APPIMAGE}")" && printf '%s/%s' "$(pwd)" "$(basename "${APPIMAGE}")") + +if [ ! -x "${APPIMAGE}" ]; then + echo "Making ${APPIMAGE} executable" + chmod +x "${APPIMAGE}" +fi + +WORK=$(mktemp -d) +# shellcheck disable=SC2064 # WORK is expanded now on purpose. +trap "rm -rf '${WORK}'" EXIT INT TERM + +echo "Extracting bundled icons from $(basename "${APPIMAGE}")..." +( cd "${WORK}" && "${APPIMAGE}" --appimage-extract >/dev/null ) + +SRC="${WORK}/squashfs-root" +if [ ! -d "${SRC}/usr/share/icons/hicolor" ]; then + echo "That AppImage has no bundled icons — is it really Triple-C?" >&2 + exit 1 +fi + +# Copy every size the bundle ships, keeping the theme's directory layout. +COUNT=0 +while IFS= read -r icon; do + [ -n "${icon}" ] || continue + rel=${icon#"${SRC}/usr/share/icons/hicolor/"} + install -Dm644 "${icon}" "${ICON_DIR}/${rel}" + COUNT=$((COUNT + 1)) +done < "${DESKTOP_FILE}" <