Files
margin-mail/justfile
T
pj eb59cb5f8d build the app, and ship it on Linux and Windows as well as macOS
The client itself: the sync engine and mirror, the Gmail and IMAP providers,
the screener, the three boxes and two piles, compose and send, the sanitiser
and tracker stripping, contacts, clips, backup, notifications and the guide.

The pipeline that ships it. Three runners now build in parallel and the
publish gate wants all four platform keys in latest.json before a release
leaves draft. Linux gets two more packages Tauri does not build: a flatpak
repackaged from the deb against GNOME 48, and a Nix package that relinks the
deb against nixpkgs so it runs as a native Wayland client rather than through
Xwayland. CI runs the Rust suite on all three desktops rather than on Linux
alone, and rebuilds the flatpak manifest on every push to main.

Two things that were only ever exercised on macOS and were wrong everywhere
else. The menu bar was built by adjusting the submenus macOS is given, so on
a platform whose default menu has no File or View the new File menu landed
after Edit, Check for Updates landed nowhere, and the three places and the
reading pane had no menu at all. The deb declared libwebkit2gtk-4.1-0 and
libgtk-3-0 twice, because tauri.conf.json named the dependencies Tauri
already emits.

The updater key exists now, so releases can sign their artifacts.
2026-09-06 12:15:30 +05:30

150 lines
5.9 KiB
Makefile

# Building and installing Margin Mail on the machine you are sitting at. The release pipeline in
# .github/workflows/release.yml is what builds for everyone else; this is the local equivalent, and
# `just install` is deliberately the same command whether or not the app is already installed.
set shell := ["bash", "-euo", "pipefail", "-c"]
app := "Margin Mail"
bundle := "src-tauri/target/release/bundle"
# List the recipes.
default:
@just --list
# Run the app against the Vite dev server.
dev:
pnpm tauri dev
# The gate: the frontend suites and the Rust suites, both of which must be green.
test:
pnpm test
cd src-tauri && cargo test
# The browser suite that drives the real UI, including the screenshots of every screen.
test-ui:
pnpm test:ui
# Recapture the pictures the guide ships with, from the fixture rather than from anybody's mailbox.
# They are committed, so this is not part of `test-ui`: an ordinary run must not rewrite the tree.
guide-shots:
GUIDE_SHOTS=1 pnpm exec playwright test tests/guide-shots.spec.ts
# The prose gate: no em dashes, no broken relative links, no directory trees.
docs:
node scripts/docs-check.mjs
# Build the release bundle for this machine.
build:
#!/usr/bin/env bash
set -euo pipefail
# The .app on macOS, the .deb and AppImage on Linux. No dmg: nothing here needs a disk image to
# copy a bundle into place, and building one is the slowest part of a mac bundle.
pnpm install
# macOS shows no notifications from a bundle that is not signed, so tauri.conf.json ad-hoc signs
# at minimum and a real identity replaces that when this machine has one: the signing directory
# holds an env file naming it, and Tauri reads APPLE_SIGNING_IDENTITY over the config.
if [ "$(uname -s)" = Darwin ]; then
signing="${MARGIN_SIGNING_DIR:-$HOME/.margin-signing}/studio.margin.app.env"
if [ -f "$signing" ]; then
set -a; . "$signing"; set +a
echo "Signing as $APPLE_SIGNING_IDENTITY"
else
echo "No $signing; the bundle will be ad-hoc signed."
fi
fi
case "$(uname -s)" in
Darwin) pnpm tauri build --bundles app ;;
Linux) pnpm tauri build --bundles deb,appimage ;;
*) echo "just: no local build for $(uname -s); macOS and Linux are the desktop targets." >&2; exit 1 ;;
esac
# Build, install over whatever version is already installed, and start the new one.
install: build
#!/usr/bin/env bash
set -euo pipefail
case "$(uname -s)" in
Darwin) just _install-macos ;;
Linux) just _install-linux ;;
*) echo "just: no installer for $(uname -s); macOS and Linux are the desktop targets." >&2; exit 1 ;;
esac
_install-macos:
#!/usr/bin/env bash
set -euo pipefail
src="{{bundle}}/macos/{{app}}.app"
[ -d "$src" ] || { echo "just: nothing to install, $src does not exist." >&2; exit 1; }
# /Applications is writable by admin users, so the common case needs no sudo. When it is not,
# ~/Applications is a real Launchpad location and beats prompting for a password mid-build.
if [ -w /Applications ]; then dir=/Applications; else dir="$HOME/Applications"; mkdir -p "$dir"; fi
dest="$dir/{{app}}.app"
running() { pgrep -x "{{app}}" > /dev/null || pgrep -x margin-mail > /dev/null; }
# Replacing the bundle under a running app leaves it half old and half new, and the running copy
# holds the deleted files open. Ask it to quit and wait, rather than killing it.
if running; then
echo "Quitting the running {{app}}…"
osascript -e 'quit app "{{app}}"' 2> /dev/null || true
for _ in $(seq 40); do running || break; sleep 0.25; done
if running; then echo "just: {{app}} is still running; quit it and try again." >&2; exit 1; fi
fi
rm -rf "$dest"
cp -R "$src" "$dest"
version=$(defaults read "$dest/Contents/Info.plist" CFBundleShortVersionString 2> /dev/null || echo "?")
echo "Installed $version to $dest"
open "$dest"
_install-linux:
#!/usr/bin/env bash
set -euo pipefail
deb=$(ls -t {{bundle}}/deb/*.deb 2> /dev/null | head -1 || true)
appimage=$(ls -t {{bundle}}/appimage/*.AppImage 2> /dev/null | head -1 || true)
if [ -n "$deb" ] && command -v apt-get > /dev/null; then
sudo apt-get install -y --reinstall "$PWD/$deb"
echo "Installed $deb"
elif [ -n "$deb" ] && command -v dpkg > /dev/null; then
sudo dpkg -i "$deb"
echo "Installed $deb"
elif [ -n "$appimage" ]; then
install -Dm755 "$appimage" "$HOME/.local/bin/margin-mail"
install -Dm644 src-tauri/icons/128x128.png "$HOME/.local/share/icons/margin-mail.png"
mkdir -p "$HOME/.local/share/applications"
printf '%s\n' \
'[Desktop Entry]' \
'Type=Application' \
'Name=Margin Mail' \
'Comment=A calm, keyboard-first mail client for Gmail' \
'Exec=margin-mail' \
'Icon=margin-mail' \
'Categories=Office;Email;' \
> "$HOME/.local/share/applications/margin-mail.desktop"
echo "Installed $appimage to ~/.local/bin/margin-mail"
echo "Make sure ~/.local/bin is on your PATH."
else
echo "just: nothing to install, no .deb or AppImage under {{bundle}}." >&2
exit 1
fi
# Remove the installed app, leaving its data directory alone.
uninstall:
#!/usr/bin/env bash
set -euo pipefail
case "$(uname -s)" in
Darwin)
for dest in "/Applications/{{app}}.app" "$HOME/Applications/{{app}}.app"; do
if [ -d "$dest" ]; then rm -rf "$dest"; echo "Removed $dest"; fi
done
;;
Linux)
if dpkg -s margin-mail > /dev/null 2>&1; then sudo apt-get remove -y margin-mail; fi
rm -f "$HOME/.local/bin/margin-mail" \
"$HOME/.local/share/icons/margin-mail.png" \
"$HOME/.local/share/applications/margin-mail.desktop"
echo "Removed the AppImage install, if there was one."
;;
*) echo "just: nothing to uninstall on $(uname -s)." >&2; exit 1 ;;
esac