M12 — the Android client, end to end #2
@@ -30,7 +30,7 @@ Or grab the `.pkg.tar.*` from the
|
||||
and install it directly:
|
||||
|
||||
```sh
|
||||
sudo pacman -U thoughtsync-desktop-*-x86_64.pkg.tar.*
|
||||
sudo pacman -U thoughtsync-*-x86_64.pkg.tar.*
|
||||
```
|
||||
|
||||
The compression suffix depends on what the build image provides — `.zst` when
|
||||
@@ -45,7 +45,11 @@ Either way you get:
|
||||
|
||||
Launch **ThoughtSync** from your app menu, or run `thoughtsync`.
|
||||
|
||||
Uninstall: `sudo pacman -R thoughtsync-desktop`.
|
||||
Uninstall: `sudo pacman -R thoughtsync`.
|
||||
|
||||
The package was called `thoughtsync-desktop` before; it declares `replaces`/
|
||||
`conflicts` on that name, so an upgrade from it is a normal `pacman -U` and
|
||||
leaves nothing behind.
|
||||
|
||||
## How the package is built
|
||||
|
||||
|
||||
@@ -29,10 +29,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
SRC_TAURI="$REPO_ROOT/desktop/src-tauri"
|
||||
BINARY="$SRC_TAURI/target/release/thoughtsync-desktop"
|
||||
BINARY="$SRC_TAURI/target/release/thoughtsync"
|
||||
OUT_DIR="${1:-$SRC_TAURI/target/release/bundle/arch}"
|
||||
|
||||
PKGNAME="thoughtsync-desktop"
|
||||
PKGNAME="thoughtsync"
|
||||
# The name this package used to ship under. pacman needs both to retire it: without
|
||||
# them a `pacman -U` of the renamed package installs ALONGSIDE the old one, and two
|
||||
# packages both own /usr/bin/thoughtsync (issue 2075).
|
||||
REPLACES=(thoughtsync-desktop)
|
||||
PKGREL=1
|
||||
PKGDESC="ThoughtSync desktop — local-first Keep-style thought capture"
|
||||
URL="https://git.fabledsword.com/bvandeusen/thoughtsync"
|
||||
@@ -73,9 +77,10 @@ STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT INT TERM
|
||||
|
||||
# --- lay out the filesystem tree --------------------------------------------
|
||||
# /usr/bin/thoughtsync (not thoughtsync-desktop): matches the CLI name the
|
||||
# AppImage installer symlinks into ~/.local/bin, so the command is the same
|
||||
# whichever way the app was installed.
|
||||
# /usr/bin/thoughtsync — the same command name the .deb installs and the AppImage
|
||||
# installer symlinks into ~/.local/bin, so it's identical whichever way the app
|
||||
# arrived. The binary already carries this name (Cargo `[[bin]]`), which is also
|
||||
# what the .desktop entry's StartupWMClass has to match.
|
||||
install -Dm755 "$BINARY" "$STAGE/usr/bin/thoughtsync"
|
||||
install -Dm644 "$SCRIPT_DIR/thoughtsync.desktop" \
|
||||
"$STAGE/usr/share/applications/thoughtsync.desktop"
|
||||
@@ -109,6 +114,10 @@ INSTALLED_SIZE="$(du -sb "$STAGE" | cut -f1)"
|
||||
echo "arch = x86_64"
|
||||
echo "license = $LICENSE"
|
||||
for d in "${DEPENDS[@]}"; do echo "depend = $d"; done
|
||||
# conflict + replaces together: `conflict` is what makes pacman remove the old
|
||||
# package rather than refuse the transaction, `replaces` is what makes an upgrade
|
||||
# pick this one up under its new name.
|
||||
for r in "${REPLACES[@]}"; do echo "conflict = $r"; echo "replaces = $r"; done
|
||||
} >"$STAGE/.PKGINFO"
|
||||
|
||||
# --- .MTREE (optional) ------------------------------------------------------
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# StartupWMClass must equal the BINARY name, not the product name: GTK derives
|
||||
# WM_CLASS from the executable, so anything else silently breaks taskbar icon
|
||||
# grouping. Tauri writes the same value into the .deb's generated entry.
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ThoughtSync
|
||||
@@ -6,4 +9,4 @@ Exec=thoughtsync %U
|
||||
Icon=thoughtsync
|
||||
Terminal=false
|
||||
Categories=Utility;Office;
|
||||
StartupWMClass=ThoughtSync
|
||||
StartupWMClass=thoughtsync
|
||||
|
||||
@@ -10,20 +10,25 @@
|
||||
# that installs and then won't launch because a library it needs was never
|
||||
# declared.
|
||||
#
|
||||
# Four checks, cheapest first:
|
||||
# Five checks, cheapest first:
|
||||
# 1. Print the control file + contents — the generated metadata becomes ground
|
||||
# truth in the build log instead of an assumption.
|
||||
# 2. dpkg-shlibdeps: the canonical Debian answer for "what does this ELF
|
||||
# 2. Naming: the binary is /usr/bin/thoughtsync and the generated .desktop
|
||||
# entry's StartupWMClass matches it. The app used to identify itself three
|
||||
# different ways depending on install channel (issue 2075); this is what
|
||||
# keeps the .deb — the only channel whose entry Tauri generates for us —
|
||||
# from drifting away from the two we write by hand.
|
||||
# 3. dpkg-shlibdeps: the canonical Debian answer for "what does this ELF
|
||||
# actually need". Compared against what the package declares.
|
||||
# 3. Every declared dependency resolves to a real package in apt (catches a
|
||||
# 4. Every declared dependency resolves to a real package in apt (catches a
|
||||
# typo in the hand-written list, which would break install for everyone).
|
||||
# 4. If a docker CLI is present, install into a clean debian container — the
|
||||
# 5. If a docker CLI is present, install into a clean debian container — the
|
||||
# highest-fidelity check, since the build image already has the -dev
|
||||
# packages installed and so can't prove resolution on its own.
|
||||
#
|
||||
# Check 4 is opportunistic on purpose: the build image is not guaranteed to carry
|
||||
# Check 5 is opportunistic on purpose: the build image is not guaranteed to carry
|
||||
# a docker CLI, and adding one at job time would violate "the image is the
|
||||
# toolchain" (rule 5). Checks 1-3 are self-contained and always run.
|
||||
# toolchain" (rule 5). Checks 1-4 are self-contained and always run.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -63,7 +68,41 @@ BIN="$(find "$WORK/root" -type f -path '*/bin/*' -print -quit)"
|
||||
[ -n "$BIN" ] || { echo "ERROR: no binary found under */bin/ in the package" >&2; exit 1; }
|
||||
echo " (binary: ${BIN#"$WORK/root"})"
|
||||
|
||||
# --- 2. what the ELF actually needs -----------------------------------------
|
||||
# --- 2. naming is consistent -------------------------------------------------
|
||||
# CANON is the one name the app answers to everywhere: the binary, the CLI
|
||||
# command, the icon, and the WM_CLASS the window reports. Hardcoded here on
|
||||
# purpose — this literal IS the contract the three install channels are held to.
|
||||
# Deliberately NOT asserted: the control file's `Package:` field, which is
|
||||
# `thought-sync`. tauri-bundler derives it as kebab-case(productName) with no
|
||||
# config override, so "ThoughtSync" splits at the hump. Fixing it would mean
|
||||
# unpacking and rewriting the control archive on every build — a fragile step for
|
||||
# a cosmetic gain on one uninstall command. Left as a known wart (issue 2075).
|
||||
CANON="thoughtsync"
|
||||
|
||||
installed_bin="${BIN#"$WORK/root"}"
|
||||
[ "$installed_bin" = "/usr/bin/$CANON" ] ||
|
||||
note_fail "binary is at $installed_bin, expected /usr/bin/$CANON (mainBinaryName in tauri.conf.json)."
|
||||
|
||||
# Tauri generates this entry from the binary name, so a mismatch means the config
|
||||
# and the bundler have diverged — exactly the drift that made the app group under
|
||||
# a different taskbar icon depending on how it was installed.
|
||||
entry="$(find "$WORK/root/usr/share/applications" -name '*.desktop' -print -quit 2>/dev/null || true)"
|
||||
if [ -z "$entry" ]; then
|
||||
note_fail "no .desktop entry in the package — the app would not appear in any menu."
|
||||
else
|
||||
echo "--- desktop entry ($(basename "$entry")) ---"
|
||||
sed 's/^/ /' "$entry"
|
||||
wmclass="$(sed -n 's/^StartupWMClass=//p' "$entry" | head -1)"
|
||||
[ "$wmclass" = "$CANON" ] ||
|
||||
note_fail "StartupWMClass is '${wmclass:-<unset>}', expected '$CANON' — the taskbar icon will not group."
|
||||
# `%u`/`%U` field codes may follow, so match the first word only.
|
||||
exec_cmd="$(sed -n 's/^Exec=//p' "$entry" | head -1 | awk '{print $1}')"
|
||||
[ "$exec_cmd" = "$CANON" ] ||
|
||||
note_fail "Exec runs '${exec_cmd:-<unset>}', expected '$CANON'."
|
||||
fi
|
||||
[ "$fail" -eq 0 ] && echo "OK: binary, Exec and StartupWMClass all agree on '$CANON'."
|
||||
|
||||
# --- 3. what the ELF actually needs -----------------------------------------
|
||||
if command -v dpkg-shlibdeps >/dev/null 2>&1; then
|
||||
# dpkg-shlibdeps insists on a debian/control in the working directory even with
|
||||
# -O (write to stdout); a stub is enough to let it do the ELF analysis.
|
||||
@@ -110,7 +149,7 @@ else
|
||||
echo "WARN: dpkg-shlibdeps unavailable — skipping the ELF dependency check." >&2
|
||||
fi
|
||||
|
||||
# --- 3. declared deps are real packages -------------------------------------
|
||||
# --- 4. declared deps are real packages -------------------------------------
|
||||
if apt-cache policy dpkg >/dev/null 2>&1 && [ -n "$(apt-cache policy dpkg 2>/dev/null)" ]; then
|
||||
for pkg in $declared; do
|
||||
if [ -z "$(apt-cache policy "$pkg" 2>/dev/null)" ]; then
|
||||
@@ -120,7 +159,7 @@ if apt-cache policy dpkg >/dev/null 2>&1 && [ -n "$(apt-cache policy dpkg 2>/dev
|
||||
[ "$fail" -eq 0 ] && echo "OK: every declared dependency exists in apt."
|
||||
fi
|
||||
|
||||
# --- 4. clean-container install (opportunistic) -----------------------------
|
||||
# --- 5. clean-container install (opportunistic) -----------------------------
|
||||
# The build image already has libwebkit2gtk-4.1-dev etc. installed, so installing
|
||||
# here would pass no matter what we declared. Only a pristine container proves
|
||||
# apt can actually resolve the package for a real user.
|
||||
|
||||
@@ -236,6 +236,10 @@ if ( cd "$tmp" && "$dest" --appimage-extract .DirIcon >/dev/null 2>&1 ) \
|
||||
fi
|
||||
rm -rf "$tmp/squashfs-root" 2>/dev/null || true
|
||||
|
||||
# StartupWMClass is the BINARY name, not the product name and not the AppImage
|
||||
# filename: the AppImage's AppRun execs usr/bin/thoughtsync, and GTK derives
|
||||
# WM_CLASS from whatever it ends up running. Anything else here means the window
|
||||
# never associates with this entry and the taskbar shows a second, generic icon.
|
||||
cat > "$apps_menu/thoughtsync.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
@@ -245,7 +249,7 @@ Exec=$dest %U
|
||||
Icon=$icon_ref
|
||||
Terminal=false
|
||||
Categories=Utility;Office;
|
||||
StartupWMClass=ThoughtSync
|
||||
StartupWMClass=thoughtsync
|
||||
EOF
|
||||
|
||||
have update-desktop-database && update-desktop-database "$apps_menu" >/dev/null 2>&1 || true
|
||||
|
||||
@@ -4,6 +4,10 @@ version = "0.1.0"
|
||||
description = "ThoughtSync desktop — local-first Keep-style thought capture"
|
||||
authors = ["bvandeusen"]
|
||||
edition = "2021"
|
||||
# The executable is named below, not inferred from this package name. Off so the
|
||||
# inferred `thoughtsync-desktop` target and the explicit one can't both claim
|
||||
# src/main.rs.
|
||||
autobins = false
|
||||
|
||||
# App logic lives in the library (Tauri v2 pattern: a single `run()` entry point
|
||||
# reusable across desktop and any future mobile target); main.rs is a thin shim.
|
||||
@@ -11,6 +15,17 @@ edition = "2021"
|
||||
name = "thoughtsync_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
# The shipped command is `thoughtsync` on every install channel, and the binary's
|
||||
# own name is what the desktop actually keys on: GTK derives the window's WM_CLASS
|
||||
# from it, and Tauri's generated .desktop file writes `StartupWMClass={binary}`.
|
||||
# So the canonical name has to be carried by the build target itself, not just by
|
||||
# the path it gets installed to (issue 2075). The crate stays `thoughtsync-desktop`
|
||||
# — only the executable is renamed. `thoughtsync` is also fixed under kebab-casing,
|
||||
# which the AppImage bundler applies to the binary name on its way in.
|
||||
[[bin]]
|
||||
name = "thoughtsync"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
|
||||
@@ -85,6 +85,11 @@ pub fn integrate_desktop() -> Result<IntegrationStatus, String> {
|
||||
|
||||
// .desktop launcher -> user applications dir. Icon/Exec are absolute paths, so no
|
||||
// icon-theme lookup is needed.
|
||||
//
|
||||
// StartupWMClass is the exception: it must be the BINARY name, because GTK
|
||||
// derives the window's WM_CLASS from the executable and the desktop matches the
|
||||
// two to group the taskbar icon. Not the product name, and not this AppImage's
|
||||
// filename — AppRun execs `usr/bin/thoughtsync` inside it (issue 2075).
|
||||
let entry = desktop_entry_path().ok_or("Cannot resolve the applications dir.")?;
|
||||
if let Some(parent) = entry.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("create applications dir: {e}"))?;
|
||||
@@ -98,7 +103,7 @@ pub fn integrate_desktop() -> Result<IntegrationStatus, String> {
|
||||
Icon={icon}\n\
|
||||
Terminal=false\n\
|
||||
Categories=Utility;Office;\n\
|
||||
StartupWMClass=ThoughtSync\n",
|
||||
StartupWMClass=thoughtsync\n",
|
||||
exec = installed.to_string_lossy(),
|
||||
icon = icon.to_string_lossy(),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ThoughtSync",
|
||||
"mainBinaryName": "thoughtsync",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.fabledsword.thoughtsync",
|
||||
"build": {
|
||||
|
||||
Reference in New Issue
Block a user