Build Debug APK / build (push) Successful in 5m51s
Rewrites every British spelling found in the codebase to American: neighbour, colour, catalogue, behaviour, honour, flavour, grey, judgement, acknowledgement, licence, defence, organise/optimise/initialise/recognise/ analyse, artefact, cancelled/cancelling, labelled/labelling, modelled/ modelling, and centre/centred/centring all become their American forms — consistently in prose (docs, ADRs, comments) and in identifiers, including the FFI-shared CatalogueSync/Cancelled enum variants and catalogue_* fields that cross the Rust/Kotlin boundary. Left untouched: external API names that only look like a spelling variant (kotlinx.coroutines.CancellationException/NonCancellable, Compose Material3's labelLarge) and the EFF diceware wordlist used for key derivation. Adds a Code Style rule to AGENTS.md/CLAUDE.md requiring American spelling in user-facing copy and documentation going forward, with that same carve-out. Verified: cargo build --workspace, all 1410 Rust tests, Kotlin compile, fmt-rust/fmt-kotlin, lint-kotlin-fast (detekt), and the lint-gui-layout/ lint-sql-params/lint-p2p-admission custom checks all pass clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fHWzCF6oNiCV4ue1bBf1t
643 lines
29 KiB
Makefile
643 lines
29 KiB
Makefile
# Default recipe: list all available recipes
|
|
_default:
|
|
just --list
|
|
|
|
|
|
PERSONAL_TEST_ENSEMBLE_PK := "cosponsor·curry scientist·lung trial·bunny symptom·unread"
|
|
|
|
# Must match `ndkVersion` in android/app/build.gradle.kts.
|
|
ANDROID_NDK_VERSION := "28.0.13004108"
|
|
|
|
# The browser extension's recipes live next to the extension, in
|
|
# browser-extension/justfile: `just extension build`, `just extension check`,
|
|
# `just extension dev-path`. just runs a module's recipes with the working
|
|
# directory set to the module's own directory, which is what lets that file
|
|
# refer to `src/` and `dist/` rather than repeating `browser-extension/`.
|
|
[doc: "Browser extension recipes — `just extension build`/`check`/`dev-path`"]
|
|
mod extension 'browser-extension/justfile'
|
|
|
|
# Report which optional dev capabilities (GUI, Android toolchain, Nix) this
|
|
# machine has. Development happens across several machines with different
|
|
# capabilities — run this first rather than assuming.
|
|
[doc: "Report GUI/Android/Nix capability on this machine — run first, don't assume from a prior session or machine"]
|
|
[group: "dev"]
|
|
doctor:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
echo "== Machine capability check =="
|
|
|
|
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then
|
|
echo "GUI: yes — use \`just build-gui\` directly"
|
|
else
|
|
echo "GUI: no (no WAYLAND_DISPLAY/DISPLAY) — use \`just check-gui-nix\` to type-check (fast, incremental); \`just build-gui-nix\` for a full hermetic build"
|
|
fi
|
|
|
|
has_nix=0
|
|
command -v nix >/dev/null 2>&1 && has_nix=1
|
|
|
|
jdk="/opt/android-studio/jbr"
|
|
sdk="$HOME/Android/Sdk"
|
|
ndk="$sdk/ndk/{{ANDROID_NDK_VERSION}}"
|
|
if [ -d "$jdk" ] && [ -d "$sdk" ] && [ -d "$ndk" ]; then
|
|
echo "Android: yes — use \`just build-synchronicity-lib-android\`/\`just android-build\`/\`just gradlew <task>\` directly"
|
|
elif [ "$has_nix" = 1 ]; then
|
|
echo "Android: no (missing one of $jdk, $sdk, $ndk) — use \`just build-android-nix\` to build the debug APK hermetically instead"
|
|
else
|
|
echo "Android: no (missing one of $jdk, $sdk, $ndk) — Android/uniffi changes can't be built or verified on this machine; say so rather than attempting it"
|
|
fi
|
|
|
|
if [ "$has_nix" = 1 ]; then
|
|
echo "Nix: yes — \`just build-gui-nix\`/\`just build-android-nix\` available as hermetic build fallbacks"
|
|
else
|
|
echo "Nix: no"
|
|
fi
|
|
|
|
|
|
# Build the synchronicity_lib Rust crate for Android. This is the single
|
|
# source of truth for the cargo invocation and NDK toolchain env vars — the
|
|
# `buildRustLib` Gradle task (android/app/build.gradle.kts) delegates to this
|
|
# recipe rather than duplicating it, then copies the resulting .so into
|
|
# jniLibs itself.
|
|
#
|
|
# JAVA_HOME/ANDROID_HOME/NDK_HOME default to a local Android Studio install's
|
|
# layout, but are left alone if already set — `nix develop` (flake.nix's
|
|
# devShell) exports all three pointing at its own hermetic JDK/SDK/NDK, which
|
|
# `build-android-nix` relies on this recipe not clobbering.
|
|
[doc: "Build the synchronicity_lib Rust crate for Android (also invoked by Gradle's buildRustLib task)"]
|
|
[group: "build"]
|
|
build-synchronicity-lib-android:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
|
|
export NDK_HOME="${NDK_HOME:-$ANDROID_HOME/ndk/{{ANDROID_NDK_VERSION}}}"
|
|
if [ ! -d "$NDK_HOME" ]; then
|
|
echo "Android NDK not found at: $NDK_HOME" >&2
|
|
echo "Install it via Android Studio: Tools > SDK Manager > SDK Tools tab > check \"NDK (Side by side)\" > Apply" >&2
|
|
exit 1
|
|
fi
|
|
export PATH="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
|
|
export TARGET_CC="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang"
|
|
export TARGET_AR="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
|
|
if [ ! -x "$TARGET_CC" ]; then
|
|
echo "Android NDK compiler not found at: $TARGET_CC" >&2
|
|
exit 1
|
|
fi
|
|
if [ ! -x "$TARGET_AR" ]; then
|
|
echo "Android NDK archiver not found at: $TARGET_AR" >&2
|
|
exit 1
|
|
fi
|
|
export RUSTFLAGS="-C linker=$TARGET_CC -C link-arg=-Wl,-z,max-page-size=16384"
|
|
cd rust && cargo build --target aarch64-linux-android --package synchronicity_lib --release
|
|
|
|
# Generate Kotlin types using uniffi. Depends on build-synchronicity-lib-android
|
|
# so it never silently regenerates bindings from a stale .so.
|
|
#
|
|
# The bindings are a build output, not source. Gradle's per-variant
|
|
# `generate<Variant>UniffiBindings` task (see android/app/build.gradle.kts) calls
|
|
# this recipe with the output path AGP chose for it, under the Android module's
|
|
# `build/generated/`. The default below is only for running the recipe
|
|
# standalone — e.g. to read the Kotlin FFI surface without a Gradle build — and
|
|
# is deliberately under `build/` too, so `gradlew clean` clears it.
|
|
[doc: "Generate Kotlin types from the Rust FFI surface using uniffi"]
|
|
[group: "build"]
|
|
uniffi-bindgen-generate out_dir="android/app/build/generated/source/uniffi": build-synchronicity-lib-android
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
mkdir -p "{{out_dir}}"
|
|
# Absolute, because the cargo invocation below runs from rust/.
|
|
out_abs="$(cd "{{out_dir}}" && pwd)"
|
|
# --no-format: skip uniffi's built-in ktlint pass. The project formats Kotlin
|
|
# with ktfmt, which never sees this directory (it is under build/), so we
|
|
# leave uniffi's raw output as-is rather than running a second, conflicting
|
|
# formatter over it.
|
|
cd rust && cargo run --bin uniffi-bindgen -- generate \
|
|
--library "${CARGO_TARGET_DIR:-target}/aarch64-linux-android/release/libsynchronicity.so" \
|
|
--language kotlin \
|
|
--no-format \
|
|
--out-dir "$out_abs"
|
|
|
|
# JAVA_HOME defaults to a local Android Studio install but is left alone if
|
|
# already set, so this also works unmodified under `nix develop` — see
|
|
# build-android-nix. AAPT2_PATH is unset outside a Nix shell, so AAPT2_ARGS is
|
|
# empty there and Gradle resolves aapt2 itself as usual.
|
|
[doc: "Build Android app (debug)"]
|
|
[group: "build"]
|
|
android-build:
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
AAPT2_ARGS=()
|
|
[ -n "${AAPT2_PATH:-}" ] && AAPT2_ARGS=(-Pandroid.aapt2FromMavenOverride="$AAPT2_PATH")
|
|
cd android && ./gradlew "${AAPT2_ARGS[@]}" assembleDebug
|
|
|
|
# Build the Android debug APK hermetically via Nix: no local Android Studio
|
|
# install required, just `nix`. `flake.nix`'s devShell provides the JDK, the
|
|
# Android SDK/NDK (via androidenv), and a Rust toolchain with the
|
|
# aarch64-linux-android target, exporting ANDROID_HOME/ANDROID_SDK_ROOT (which
|
|
# Gradle falls back to when android/local.properties has no sdk.dir) and
|
|
# AAPT2_PATH (picked up by android-build/gradlew/lint-kotlin* below) instead
|
|
# of writing android/local.properties — that file is also what Android Studio
|
|
# uses interactively, and overwriting it with this ephemeral Nix store path
|
|
# broke Studio's own SDK/AVD resolution. `build-apk-ci` uses the same
|
|
# toolchain but does write local.properties itself, which is fine there: it
|
|
# runs in a throwaway CI checkout with no Studio session to clobber. Runs
|
|
# `buildRustLib`/`generate<Variant>UniffiBindings` (via android-build's `./gradlew
|
|
# assembleDebug`, which depends on them) inside that shell, so this also
|
|
# regenerates the uniffi Kotlin bindings from a hermetically-built .so.
|
|
# Output lands at android/app/build/outputs/apk/debug/app-debug.apk.
|
|
[doc: "Build the Android debug APK hermetically via Nix (SDK/NDK/JDK from flake.nix, no local Android Studio needed)"]
|
|
[group: "build"]
|
|
build-android-nix:
|
|
nix develop --command just android-build
|
|
|
|
# Run any gradlew command with the correct JDK. As with android-build,
|
|
# JAVA_HOME is left alone if already set, so this works unmodified under
|
|
# `nix develop` (e.g. `nix develop --command just gradlew test` on a machine
|
|
# with no local Android Studio install).
|
|
[group: "android"]
|
|
gradlew *args:
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
AAPT2_ARGS=()
|
|
[ -n "${AAPT2_PATH:-}" ] && AAPT2_ARGS=(-Pandroid.aapt2FromMavenOverride="$AAPT2_PATH")
|
|
cd android && ./gradlew "${AAPT2_ARGS[@]}" {{args}}
|
|
|
|
|
|
# Run the PC graphical application
|
|
[group: "run"]
|
|
[env("RUST_LOG", "info")]
|
|
gui *args:
|
|
cargo run --manifest-path rust/gui-app/Cargo.toml --bin syn-gui -- {{args}}
|
|
|
|
# Build the gpui desktop GUI (debug, local toolchain). Prefer build-gui-nix
|
|
# when there's no GUI session or the sandbox may be missing a native dep.
|
|
[doc: "Build the gpui desktop GUI (debug, local toolchain)"]
|
|
[group: "build"]
|
|
build-gui:
|
|
cargo build --manifest-path rust/gui-app/Cargo.toml
|
|
|
|
|
|
# Run the cli from source
|
|
[group: "run-cli"]
|
|
[env("RUST_LOG", "info")]
|
|
cli *args:
|
|
cargo run --manifest-path rust/Cargo.toml --bin syn-cli -- {{args}}
|
|
|
|
# test/store-dirs/ is gitignored and not created by any recipe — run
|
|
# `just init-test-ensemble-cli test/store-dirs/canonical` once beforehand.
|
|
[doc: "Run the cli against a manually-initialized store at test/store-dirs/canonical"]
|
|
[group: "run-cli"]
|
|
canon-cli *args:
|
|
just cli -s test/store-dirs/canonical {{args}}
|
|
|
|
# Run the command line program without cargo from the target/ directory
|
|
[group: "run-cli"]
|
|
[env("RUST_LOG", "info")]
|
|
cli-fast *args:
|
|
./rust/target/debug/syn-cli {{args}}
|
|
|
|
# Run the CLI daemon in the foreground.
|
|
#
|
|
# `--store` is a *global* flag and `--foreground` belongs to `daemon start`, so
|
|
# the caller's args go before the subcommand and the flag goes after it. Passing
|
|
# them the other way round — `daemon --foreground --store DIR`, which this
|
|
# recipe did until now — is refused by bpaf with "flag `--foreground` is not
|
|
# valid in this context", taking `test-env-daemon-a`/`-b` down with it.
|
|
[group: "run-cli"]
|
|
[env("RUST_LOG", "info")]
|
|
daemon-cli *args:
|
|
just cli {{args}} daemon start --foreground
|
|
|
|
# Launch the TUI
|
|
[group: "run"]
|
|
[env("RUST_LOG", "info")]
|
|
tui *args:
|
|
cargo run --manifest-path rust/Cargo.toml --bin syn-tui -- {{args}}
|
|
|
|
# Build all Rust binaries (native)
|
|
[group: "build"]
|
|
build-native:
|
|
cd rust && cargo build
|
|
|
|
# Build all Rust binaries in release mode (native)
|
|
[group: "build"]
|
|
build-native-release:
|
|
cd rust && cargo build --release
|
|
|
|
# Preferred way to verify GUI changes actually compile — a plain `cargo build -p gui-app` depends on system libs (fontconfig, alsa, libxcb, mupdf's bindgen/libclang, ...) that a dev sandbox may be missing, while this flake declares them all. Result lands at ./result/bin/syn-gui."
|
|
[doc: "Build the gpui desktop GUI (rust/gui-app) hermetically via Nix."]
|
|
[group: "build"]
|
|
build-gui-nix:
|
|
nix build .#syn-gui --max-jobs auto --cores 0
|
|
|
|
|
|
# Type-check the gpui desktop GUI inside the nix devShell.
|
|
#
|
|
# Prefer this over `build-gui-nix` while iterating: the hermetic build starts
|
|
# from a clean sandbox every time, so it rebuilds the whole workspace in
|
|
# release just to check a one-line change. This runs a normal incremental
|
|
# `cargo check` against `rust/target`, with the same native libraries the
|
|
# packaged build links against — seconds instead of minutes, after the first
|
|
# run warms the cache. Use `build-gui-nix` for final verification, or when you
|
|
# need a binary you can actually run.
|
|
[doc: "Type-check the gpui GUI incrementally in the nix devShell"]
|
|
[group: "build"]
|
|
check-gui-nix *args:
|
|
nix develop -c cargo check --manifest-path rust/gui-app/Cargo.toml {{args}}
|
|
|
|
|
|
# Run all formatters
|
|
[group: "format"]
|
|
fmt-all: fmt-kotlin fmt-rust
|
|
|
|
|
|
# Format Kotlin code with ktfmt. JAVA_HOME is left alone if already set, so
|
|
# this works under `nix develop` — see build-android-nix.
|
|
[group: "format"]
|
|
fmt-kotlin:
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
cd android && ./gradlew ktfmtFormat
|
|
|
|
# Check Kotlin formatting (fails if unformatted)
|
|
[group: "format"]
|
|
fmt-kotlin-check:
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
cd android && ./gradlew ktfmtCheck
|
|
|
|
# Lint Kotlin with detekt, WITH type resolution. There is deliberately no baseline
|
|
# file (see android/app/build.gradle.kts) — all debt was burned down, so any finding
|
|
# fails the build. JAVA_HOME is left alone if already set, so this works under
|
|
# `nix develop` — see build-android-nix.
|
|
#
|
|
# detektDebug rather than the plain `detekt` task because six of the rules this
|
|
# project has configured `active: true` — IgnoredReturnValue, ForbiddenMethodCall,
|
|
# UnsafeCallOnNullableType, UseOrEmpty, RedundantSuspendModifier, NoNameShadowing —
|
|
# are @RequiresTypeResolution and are SILENTLY SKIPPED on the plain task, which has
|
|
# no classpath. They looked enabled for a long time while enforcing nothing.
|
|
#
|
|
# The cost: detektDebug needs a compiled classpath, so it depends on the Rust
|
|
# cross-build (~15 min cold, seconds warm). Use `lint-kotlin-fast` for the quick
|
|
# structural-only pass; it cannot see the six rules above.
|
|
[doc: "Lint Kotlin with detekt + type resolution (no baseline — any finding fails)"]
|
|
[group: "format"]
|
|
lint-kotlin: lint-kotlin-rethrow
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
AAPT2_ARGS=()
|
|
[ -n "${AAPT2_PATH:-}" ] && AAPT2_ARGS=(-Pandroid.aapt2FromMavenOverride="$AAPT2_PATH")
|
|
cd android && ./gradlew "${AAPT2_ARGS[@]}" detektDebug
|
|
|
|
# Structural-only detekt: no classpath, so no Rust build and no type-resolution
|
|
# rules (see lint-kotlin). Seconds rather than minutes; use it while iterating,
|
|
# but `lint-kotlin` is what has to pass.
|
|
[doc: "Lint Kotlin with detekt, structural rules only (fast, no type resolution)"]
|
|
[group: "format"]
|
|
lint-kotlin-fast: lint-kotlin-rethrow
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
AAPT2_ARGS=()
|
|
[ -n "${AAPT2_PATH:-}" ] && AAPT2_ARGS=(-Pandroid.aapt2FromMavenOverride="$AAPT2_PATH")
|
|
cd android && ./gradlew "${AAPT2_ARGS[@]}" detekt
|
|
|
|
# The one hole ADR 0022 left open in the never-crash design. `getOrElse { throw it }`
|
|
# is the same hazard as the banned `getOrThrow` — it turns a Result failure back into a
|
|
# thrown exception that nothing requires a caller to catch — but it is a `throw`
|
|
# expression rather than a method call, so detekt's ForbiddenMethodCall cannot express
|
|
# it and no built-in rule covers it. The ADR converted the one occurrence
|
|
# (PodcastRepository) by hand and recorded that nothing prevented its return. This does.
|
|
#
|
|
# grep rather than a custom detekt rule module: the module is a RuleSetProvider, a
|
|
# Gradle subproject and a test suite to keep green across detekt upgrades, which ADR
|
|
# 0022 already weighed and rejected for a larger class of finding than this one. This
|
|
# needs no classpath, so it also runs under lint-kotlin-fast and costs milliseconds.
|
|
#
|
|
# Results.kt is excluded because its `bind()` IS the sanctioned rethrow: a private
|
|
# ShortCircuit exception caught by the enclosing `result { }`, which is what gives the
|
|
# codebase its `?`-equivalent. That is the only one there should ever be.
|
|
[doc: "Fail on `getOrElse { throw }` — the crash class ForbiddenMethodCall can't see"]
|
|
[group: "format"]
|
|
lint-kotlin-rethrow:
|
|
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
# -P for the lazy class, -z so one file is one record and the lambda may span lines;
|
|
# [^}]* stops the match at the lambda's own closing brace.
|
|
pattern='getOrElse\s*\{[^}]*\bthrow\b'
|
|
hits=$(grep -rlPz "$pattern" --include='*.kt' --exclude='Results.kt' android/app/src || true)
|
|
[ -z "$hits" ] && exit 0
|
|
echo "error: getOrElse { ... throw ... } rethrows past the Result boundary (ADR 0022)."
|
|
echo " Consume the failure instead: bind() inside launchReportingFailure, or"
|
|
echo " getOrElse { report; return }."
|
|
echo
|
|
while IFS= read -r f; do
|
|
echo " $f"
|
|
grep -Pzo "$pattern" "$f" | tr '\0' '\n' | sed 's/^/ /'
|
|
echo
|
|
done <<< "$hits"
|
|
exit 1
|
|
|
|
# Fail on `.flex_1()` paired with `.max_w()` in one gpui element chain — the
|
|
# grow-then-clamp shape that lays children out against the max-content width
|
|
# and never replaces them (a 630px search box with 2px of input; a node-name
|
|
# field too narrow to read). Clippy can't see it: both calls are perfectly
|
|
# ordinary on their own. `.flex_col()` in the same chain is allowed — there
|
|
# `flex_1` grows height and the cap is on the cross axis.
|
|
[doc: "Fail on gpui chains that grow a width and then cap it (see sizing.rs `layout` docs)"]
|
|
[group: "format"]
|
|
lint-gui-layout:
|
|
python3 scripts/lint-gui-layout.py
|
|
|
|
# A P2P protocol that skips the delegation-cert handshake is reachable by any
|
|
# node that knows the ensemble *public* key — which is not secret. The type
|
|
# system already makes this hard (protocols implement `EnsembleProtocol`, not
|
|
# iroh's `ProtocolHandler`; outbound code holds an `EnsembleEndpoint`, not a
|
|
# raw `Endpoint`); this closes the escape hatch of reaching past the wrapper
|
|
# to iroh directly. See `rust/lib/src/p2p/admission.rs`.
|
|
[doc: "Fail on a P2P protocol that bypasses ensemble admission control"]
|
|
[group: "format"]
|
|
lint-p2p-admission:
|
|
python3 scripts/lint-p2p-admission.py
|
|
|
|
# A value belongs in a `?`, never in the SQL string. The distinction is
|
|
# invisible at the call site — whether `format!(" AND x >= '{s}'")` is a bug
|
|
# depends on where `s` came from — and one of these shipped: `SyncRequest.since`
|
|
# is chosen by the peer asking to sync, so any ensemble member could close the
|
|
# quote and append its own predicate. Clippy can't see it; a `format!` is a
|
|
# `format!`. See `scripts/lint-sql-params.py` for the two shapes it flags and
|
|
# the `// sql-literal-ok:` escape hatch.
|
|
[doc: "Fail on SQL that carries a value as text instead of as a parameter"]
|
|
[group: "format"]
|
|
lint-sql-params:
|
|
python3 scripts/lint-sql-params.py
|
|
|
|
[doc: "Format rust with cargo fmt"]
|
|
[group: "format"]
|
|
fmt-rust:
|
|
cd rust && cargo fmt
|
|
|
|
# Lint Rust with clippy across the whole workspace (including gui-app, which
|
|
# plain `cargo check`/nextest at the workspace root skips — see default-members
|
|
# in rust/Cargo.toml)
|
|
#
|
|
# Because it covers gui-app, this needs the GUI's native libraries present; on
|
|
# a machine without them it dies in a build script before linting anything.
|
|
# Use `lint-rust-nix` there.
|
|
[doc: "Lint Rust with clippy across the whole workspace"]
|
|
[group: "format"]
|
|
lint-rust *args:
|
|
cd rust && cargo clippy --workspace --all-targets {{args}}
|
|
|
|
# `lint-rust` inside flake.nix's devShell, which supplies the GUI's native
|
|
# libraries — so this works on a headless VM or sandbox where the bare recipe
|
|
# can't get past gui-app's build scripts.
|
|
[doc: "Lint Rust with clippy in the nix devShell (works without GUI system libs)"]
|
|
[group: "format"]
|
|
lint-rust-nix *args:
|
|
nix develop -c just lint-rust {{args}}
|
|
|
|
# Apply clippy's machine-applicable suggestions across the workspace.
|
|
#
|
|
# Only rewrites what clippy is confident about (needless borrows, useless
|
|
# conversions, collapsible ifs); findings that need a judgment call — how to
|
|
# factor a complex type, whether an argument list wants a struct — are reported
|
|
# but left alone. Review the diff: it is mechanical, not automatically right.
|
|
[doc: "Apply clippy's mechanical fixes across the workspace"]
|
|
[group: "format"]
|
|
lint-rust-fix *args:
|
|
nix develop -c cargo clippy --fix --workspace --all-targets --manifest-path rust/Cargo.toml {{args}}
|
|
|
|
# Run all rust unit tests
|
|
[group: "test"]
|
|
unit-tests-rust *args:
|
|
cd rust && cargo nextest run {{args}}
|
|
|
|
[doc: "Run gui-app unit tests (excluded from unit-tests-rust — see default-members in rust/Cargo.toml)"]
|
|
[group: "test"]
|
|
unit-tests-gui *args:
|
|
cd rust && cargo nextest run -p gui-app {{args}}
|
|
|
|
# Run the Kotlin unit tests (android/app/src/test), and take a filter:
|
|
#
|
|
# just unit-tests-kotlin --tests "AdjacencyTest"
|
|
# just unit-tests-kotlin --tests "*.PodcastEqualityTest.a re-read*"
|
|
#
|
|
# `testDebugUnitTest`, not the aggregate `test` task, and the difference is not
|
|
# cosmetic: under AGP 9 `test` is a lifecycle task that owns no test executor,
|
|
# so it rejects `--tests` outright ("Unknown command-line option") and there is
|
|
# no way to run one test through it. It also runs both variants' suites, which
|
|
# is the same tests twice. Like lint-kotlin, this compiles Kotlin and so
|
|
# depends on the Rust cross-build for the uniffi bindings — seconds once
|
|
# rust/target has the aarch64-linux-android tree, ~15 min on a cold one.
|
|
[doc: "Run Kotlin unit tests; takes a --tests filter"]
|
|
[group: "test"]
|
|
unit-tests-kotlin *args:
|
|
#!/usr/bin/env bash
|
|
export JAVA_HOME="${JAVA_HOME:-/opt/android-studio/jbr}"
|
|
AAPT2_ARGS=()
|
|
[ -n "${AAPT2_PATH:-}" ] && AAPT2_ARGS=(-Pandroid.aapt2FromMavenOverride="$AAPT2_PATH")
|
|
cd android && ./gradlew "${AAPT2_ARGS[@]}" testDebugUnitTest {{args}}
|
|
|
|
# Run P2P integration tests (requires internet access for pkarr DNS)
|
|
[group: "test"]
|
|
integration-tests-p2p:
|
|
cd rust && cargo test --test p2p_ping
|
|
|
|
# Run ensemble gossip bootstrap integration test (requires internet access:
|
|
# pkarr DHT + iroh relay via dns.iroh.link)
|
|
[doc: "Run ensemble gossip bootstrap integration test (requires internet access)"]
|
|
[group: "test"]
|
|
integration-tests-ensemble-gossip:
|
|
cd rust && cargo test --test ensemble_gossip
|
|
|
|
|
|
# Install desktop entry and icon for Wayland (user-local)
|
|
[group: "install"]
|
|
install-desktop:
|
|
mkdir -p ~/.local/share/applications
|
|
mkdir -p ~/.local/share/icons/hicolor/256x256/apps
|
|
cp assets/synchronicity.desktop ~/.local/share/applications/synchronicity.desktop
|
|
cp assets/synchronicity-icon.png ~/.local/share/icons/hicolor/256x256/apps/synchronicity.png
|
|
update-desktop-database ~/.local/share/applications
|
|
gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor
|
|
@echo "Desktop entry and icon installed."
|
|
|
|
# Uninstall desktop entry and icon
|
|
[group: "install"]
|
|
uninstall-desktop:
|
|
rm -f ~/.local/share/applications/synchronicity.desktop
|
|
rm -f ~/.local/share/icons/hicolor/256x256/apps/synchronicity.png
|
|
|
|
# Create two test stores (node_a, node_b) with deterministic keypairs for local P2P experimentation.
|
|
# Pass `reset=true` to wipe and recreate existing stores.
|
|
[doc: "Create two deterministic test stores (node_a, node_b) for local P2P experimentation"]
|
|
[group: "dev"]
|
|
test-env-setup dir="test-nodes" reset="":
|
|
cargo run --manifest-path rust/Cargo.toml --example setup_test_nodes -- {{dir}} {{if reset != "" { "--reset" } else { "" }}}
|
|
|
|
# Start daemon for test node_a (run test-env-setup first)
|
|
[group: "dev"]
|
|
test-env-daemon-a dir="test-nodes":
|
|
just daemon-cli --store {{dir}}/node_a
|
|
|
|
# Start daemon for test node_b (run test-env-setup first)
|
|
[group: "dev"]
|
|
test-env-daemon-b dir="test-nodes":
|
|
just daemon-cli --store {{dir}}/node_b
|
|
|
|
# Start test node_b with a deliberately wrong wall clock, to exercise the
|
|
# timestamp bound in `p2p::wire_time` and the peer-health reporting built on it.
|
|
#
|
|
# `offset` is anything `date` can parse as a relative time — the `faketime`
|
|
# wrapper hands it straight to `date`, so it is "+3 days" rather than
|
|
# libfaketime's own terse "+3d" (which only its FAKETIME env var understands).
|
|
#
|
|
# "+3 days" puts node_b past the 24h bound, so node_a drops everything node_b
|
|
# writes and its roster row says so. "+13 hours" lands between the warning
|
|
# threshold and the bound — still merging, reported in amber. A negative offset
|
|
# ("-3 days") is the harmless case the bound deliberately does not police: a
|
|
# stamp in the past loses every comparison it takes part in, so node_a should
|
|
# say nothing at all.
|
|
#
|
|
# Three details this gets right that a narrower fake would not:
|
|
# * It skews the whole process, so node_b's signed gossip `announced_at_secs`
|
|
# *and* every `modified_at` it writes are wrong together — which is the real
|
|
# failure rather than a simulation of one.
|
|
# * It builds first and fakes only the built binary. Running `cargo` itself
|
|
# under a future clock would corrupt its mtime-based freshness check and
|
|
# silently mis-decide what to rebuild.
|
|
# * `-m` selects libfaketime's thread-safe build (the daemon is tokio, i.e.
|
|
# thoroughly multi-threaded), and FAKETIME_DONT_FAKE_MONOTONIC keeps QUIC's
|
|
# own timers on the real monotonic clock — without it iroh's timeouts move
|
|
# too and the nodes fail to connect for reasons unrelated to the test.
|
|
#
|
|
# libfaketime comes from the flake (devShells.default), so this needs no system
|
|
# package; it falls back to one on PATH for a machine without Nix.
|
|
[group: "dev"]
|
|
[env("RUST_LOG", "info")]
|
|
test-env-daemon-b-skewed dir="test-nodes" offset="+3 days":
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd rust && cargo build --bin syn-cli && cd ..
|
|
run=(faketime -m "{{offset}}" rust/target/debug/syn-cli --store "{{dir}}/node_b" daemon start --foreground)
|
|
export FAKETIME_DONT_FAKE_MONOTONIC=1
|
|
if command -v faketime >/dev/null 2>&1; then
|
|
exec "${run[@]}"
|
|
elif command -v nix >/dev/null 2>&1; then
|
|
exec nix develop -c "${run[@]}"
|
|
else
|
|
echo "Need libfaketime: run this from \`nix develop\`, or install it (Arch: pacman -S libfaketime)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
|
|
# Cap the size of rust/target/ instead of periodically `cargo clean`-ing it.
|
|
#
|
|
# Cargo never garbage-collects target/: it writes one hash-suffixed artifact per
|
|
# distinct build configuration (rustc version, feature set, git-dependency rev)
|
|
# and keeps every one forever. Measured on this repo at 144 GB, that meant 849
|
|
# stale `syn_gui` objects and 516 `incremental/` session directories, most of
|
|
# them months old and unreachable from any current build.
|
|
#
|
|
# Two mechanisms, because they cover disjoint parts of the tree:
|
|
# * `cargo sweep --maxsize` evicts oldest-first from deps/, build/, and
|
|
# examples/ until the managed set fits under the cap.
|
|
# * `incremental/` is invisible to cargo-sweep, so prune it by age here. It is
|
|
# pure cache — deleting a session directory costs one non-incremental
|
|
# rebuild of that crate, nothing else.
|
|
#
|
|
# Run it on a schedule (see `install-gc-timer`), not by hand after you've
|
|
# already run out of disk.
|
|
[doc: "Cap rust/target/ at `size` and prune incremental caches older than `days`"]
|
|
[group: "dev"]
|
|
gc-target size="25GB" days="14":
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd "{{justfile_directory()}}"
|
|
before=$(du -sb rust/target 2>/dev/null | cut -f1)
|
|
cargo sweep --maxsize {{size}} rust
|
|
# Located by search, not by glob: cross-compiled targets nest one level
|
|
# deeper (target/aarch64-linux-android/release/incremental) than native
|
|
# ones (target/debug/incremental). -mindepth 1 so an empty incremental/ is
|
|
# a no-op rather than an attempt to delete the directory itself.
|
|
while IFS= read -r inc; do
|
|
find "$inc" -maxdepth 1 -mindepth 1 -type d -mtime +{{days}} -exec rm -rf {} +
|
|
done < <(find rust/target -type d -name incremental -prune)
|
|
after=$(du -sb rust/target 2>/dev/null | cut -f1)
|
|
awk -v b="$before" -v a="$after" 'BEGIN {
|
|
printf "rust/target: %.1f GiB -> %.1f GiB (freed %.1f GiB)\n",
|
|
b/1073741824, a/1073741824, (b-a)/1073741824
|
|
}'
|
|
|
|
# Show where rust/target/ is actually going, worst offenders first. Run this
|
|
# before reaching for `cargo clean` — the answer is usually a few hundred stale
|
|
# hash-suffixed binaries, not anything you need.
|
|
[doc: "Report what is consuming rust/target/"]
|
|
[group: "dev"]
|
|
target-usage:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd "{{justfile_directory()}}"
|
|
du -sh rust/target 2>/dev/null
|
|
echo "--- by subdirectory ---"
|
|
du -sh rust/target/*/ 2>/dev/null | sort -rh | head -8
|
|
echo "--- largest artifacts ---"
|
|
find rust/target -type f -size +200M -printf "%s\t%p\n" 2>/dev/null \
|
|
| sort -rn | head -10 \
|
|
| awk -F'\t' '{printf "%7.2f GiB %s\n", $1/1073741824, $2}'
|
|
echo "--- stale incremental sessions ---"
|
|
find $(find rust/target -type d -name incremental -prune) \
|
|
-maxdepth 1 -mindepth 1 -type d -mtime +14 2>/dev/null \
|
|
| wc -l | xargs printf "%s directories older than 14 days\n"
|
|
|
|
# Install a systemd *user* timer that runs `gc-target` weekly.
|
|
#
|
|
# The point is that reclaiming disk stops being something you remember to do
|
|
# after a build already failed. User-level (not system) so it needs no root and
|
|
# runs as the account that owns the target directory; `Persistent=true` so a
|
|
# machine that was off on the scheduled day sweeps at next login instead of
|
|
# skipping the week.
|
|
[doc: "Install a weekly systemd user timer running `gc-target`"]
|
|
[group: "install"]
|
|
install-gc-timer size="25GB" days="14":
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
mkdir -p "$unit_dir"
|
|
cat > "$unit_dir/synchronicity-gc-target.service" <<EOF
|
|
[Unit]
|
|
Description=Cap synchronicity rust/target/ size
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
WorkingDirectory={{justfile_directory()}}
|
|
ExecStart=$(command -v just) gc-target {{size}} {{days}}
|
|
Nice=19
|
|
IOSchedulingClass=idle
|
|
EOF
|
|
cat > "$unit_dir/synchronicity-gc-target.timer" <<EOF
|
|
[Unit]
|
|
Description=Weekly cap on synchronicity rust/target/ size
|
|
|
|
[Timer]
|
|
OnCalendar=weekly
|
|
Persistent=true
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now synchronicity-gc-target.timer
|
|
@echo "Installed. Next run: $(systemctl --user list-timers synchronicity-gc-target.timer --no-pager 2>/dev/null | sed -n 2p)"
|
|
|
|
# Remove the weekly gc-target timer installed by `install-gc-timer`.
|
|
[group: "install"]
|
|
uninstall-gc-timer:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
systemctl --user disable --now synchronicity-gc-target.timer 2>/dev/null || true
|
|
rm -f "$unit_dir/synchronicity-gc-target.service" "$unit_dir/synchronicity-gc-target.timer"
|
|
systemctl --user daemon-reload
|
|
|