2026-05-27 15:16:51 +08:00
# Commands
## Building
Update submodules first. The ClashMeta Go core lives in `core/Clash.Meta/` .
``` bash
git submodule update --init --recursive
```
Full package build, including Go core, Flutter, and packaging, runs through `setup.dart` :
``` bash
dart setup.dart macos
dart setup.dart linux
dart setup.dart windows
dart setup.dart android
```
2026-10-08 17:58:43 +08:00
The desktop Go core and the Rust helper build automatically: Flutter runs
2026-09-05 20:09:26 +08:00
`plugins/setup/hook/build.dart` on every `flutter build` and `flutter test` ,
and the hook drives `CoreBuilder` from the `setup_hooks` package in
2026-10-08 17:58:43 +08:00
`plugins/setup/setup_hooks/` . Artifacts land in `libclash/` . Android's Core is
the AAR the `:core` Gradle module builds with `gomobile bind` before anything
compiles against it; that needs `go` on `PATH` and nothing else, since the
gomobile tools come from `core/go.mod` and the NDK from AGP. The hook reruns only when Go, Rust, or
2026-09-05 20:09:26 +08:00
`setup_hooks` inputs change; the fingerprint cache lives in
`.dart_tool/setup_build_cache/` . To force a rebuild, delete that directory:
2026-05-27 15:16:51 +08:00
``` bash
2026-09-05 20:09:26 +08:00
rm -rf .dart_tool/setup_build_cache
2026-05-27 15:16:51 +08:00
```
2026-09-05 20:09:26 +08:00
Flutter hides the hook's output on success, so to see why the Core was or was
not rebuilt read `.dart_tool/setup_build_cache/hook.log` ; each invocation starts
with a `===` line carrying its timestamp and target.
The hook runs for `flutter test` and for `dart run` of any root-package script
too, so a pure Dart test run needs the Go toolchain, and on Linux and Windows
also `cargo` and `rustc` — the Helper fingerprint shells out to both even on a
cache hit. The hook protocol carries no build mode and no caller, so the only
switch is the user-define in the root `pubspec.yaml` :
``` yaml
hooks :
user_defines :
setup :
build_assets : true # false turns the Go core and Helper hook into a no-op
rust_api :
build_assets : true # false does the same for the Rust library
2026-05-27 15:16:51 +08:00
```
2026-10-08 17:58:43 +08:00
CI flips `setup` to `false` with `yq` in its throwaway checkout before
2026-10-16 22:42:17 +08:00
`dart run tool/changelog.dart` and `dart run tool/test.dart` ; no test loads the Go core. `rust_api`
2026-10-08 17:58:43 +08:00
stays on there because the editor tests in `test/plugins/code_forge/` and the socket
test in `test/core/desktop/ipc_native_test.dart` load the Rust library the hook
builds; the release-notes job, which runs no tests, flips both.
2026-09-05 20:09:26 +08:00
Never commit `false` : a build with it set stages whatever `libclash/` already
holds and bundles no Rust library, which is why `setup.dart` refuses to package
while it is set. Locally, `false` is worth setting for a Dart-only test loop,
but the hook cache keys on the user-define, so the first build afterwards runs
2026-10-08 17:58:43 +08:00
the hooks again. The editor and IPC socket tests still need the Rust library: keep `rust_api`
2026-09-25 03:56:55 +08:00
on, or point `FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR` at a directory that
holds a `rust_api` library built from the same sources.
2026-05-27 15:16:51 +08:00
## Flutter Development
2026-08-05 10:57:40 +08:00
Use the default Flutter SDK directly:
2026-05-27 15:16:51 +08:00
``` bash
flutter pub get
flutter run
flutter test
```
Use `flutter test` , not `dart test` , because models pull in Flutter types.
2026-09-25 03:56:55 +08:00
A development build takes over the host exactly like a release: it creates the TUN device, sets the system proxy, on
macOS rewrites the system DNS, registers itself as the login item, and on Windows and Linux starts its Core through the
shared privileged Helper, which stops the Core an installed FlClash is running. Only the data directory is kept apart,
and only for some builds: a macOS debug build is `com.follow.clash.debug` and an Android debug build `com.follow.clash.dev` ,
while profile and release builds, and every build on Windows and Linux, share the directory and preferences of the
installed app. Safe mode keeps all of the host changes off and never opens the Core's listeners or external
controller, so it does not compete with the installed app for the mixed, DNS, RESTful API and other loopback ports:
``` bash
flutter run --dart-define-from-file= env.json --dart-define= SAFE_MODE = true
```
`.run/main_safe_mode.run.xml` is the same launch for IntelliJ, and `Flutter App (Safe Mode)` in `.zed/debug.json` (debugger) or `flutter run (Safe Mode)` in `.zed/tasks.json`
(plain run) for Zed. The build is bannered `SAFE MODE` , the tray shows the
`status_4` bug icon with a menu of only show and exit and no traffic title, and the settings keep their values, so
nothing has to be flipped back before a normal run. See the safe mode section of `architecture.md` for the owners that consult the flag.
2026-05-27 15:16:51 +08:00
## Code Generation
Run code generation after modifying models, providers, or database schema:
``` bash
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch
```
Code generation covers:
- Riverpod providers through `riverpod_generator` .
- Models through `freezed` and `json_serializable` .
- Database tables through `drift_dev` .
2026-09-25 03:56:55 +08:00
The Dart-Rust bindings in `plugins/rust_api` are generated by `flutter_rust_bridge_codegen` from
`plugins/rust_api/flutter_rust_bridge.yaml` ; rerun it from that directory after changing anything under
`rust/src/api/` :
``` bash
cd plugins/rust_api && flutter_rust_bridge_codegen generate
```
2026-05-27 15:16:51 +08:00
Generated output paths, configured in `build.yaml` :
- `lib/models/generated/*.g.dart` , `*.freezed.dart` .
- `lib/providers/generated/*.g.dart` .
- `lib/database/generated/*.g.dart` .
2026-09-05 20:09:26 +08:00
Tray and Windows app icons are generated, not hand-edited. `assets_source/images/icon/*.svg` is
the source of truth; the script needs `rsvg-convert` (librsvg) on `PATH` :
``` bash
dart run tool/generate_status_icons.dart
```
It writes the tray PNGs with Flutter `2.0x/` – `4.0x/` resolution variants to `assets/images/tray/unix/` ,
multi-size tray `.ico` files to `assets/images/tray/windows/` , and `windows/runner/resources/app_icon.ico`
2026-09-25 03:56:56 +08:00
from `assets/images/icon.png` . macOS draws its menu bar icon as a template, so it skips the ringed color
icons, whose logo shrinks to a small glyph once only alpha shows: `status_1` renders the bare logo from
`assets_source/images/icon/glyph.svg` and `status_4` reuses `status_4.svg` , both recolored to black into
`assets/images/tray/macos/` ; the sources keep their colors. `glyph.svg` is also the geometry of the Android notification drawables
`android/service/src/main/res/drawable/ic.xml` and `ic_service.xml` ; change them together. `pubspec.yaml` declares the three tray directories with `platforms:` so each
2026-09-05 20:09:26 +08:00
build only bundles the format its tray loads; a new status icon needs a source SVG and an entry in the
script's `statusIconNames` , nothing in `pubspec.yaml` .
2026-05-27 15:16:51 +08:00
## Testing
Tests use `package:test/test.dart` for pure Dart logic and `flutter_test` for provider and widget tests. `mocktail` is the mocking framework.
``` bash
flutter test test/models/
flutter test test/core/
2026-08-05 10:57:40 +08:00
flutter test test/core/desktop/
2026-05-27 15:16:51 +08:00
flutter test test/providers/
flutter test test/common/
flutter test test/database/
flutter test test/widgets/
flutter test test/setup_test.dart
flutter test plugins/proxy/test/proxy_test.dart
```
2026-10-16 22:42:17 +08:00
For the whole suite, or a directory of more than a few files, use `tool/test.dart` , which is also what CI runs:
``` bash
dart run tool/test.dart # all of test/
dart run tool/test.dart test/widgets test/views # some paths
dart run tool/test.dart -- --reporter expanded --coverage
```
Plain `flutter test` compiles each file into its own program and starts an engine for it, one file after another, and
that dominates a full run: 329 files took 128 s on a 12-core Mac with at most four running at once. `tool/test.dart`
imports the files into 32 generated entrypoints under `.dart_tool/test_bundles/` and runs those with one worker per
core: about 60 s for the same run, and 67 s instead of 180– 213 s with `--coverage` . Files in one entrypoint share an
isolate, so a test can see state a file before it left behind; the script runs every file that failed there again on
its own, lets that run decide, and names the files that only failed together. See the Shared Test Isolate section of
`rules.md` before fixing one.
2026-09-05 20:09:26 +08:00
`plugins/setup/setup_hooks/` is a pure Dart package and is tested with `dart test` from its own directory, which is
also what CI runs; `tool/check_plugins.sh` does not descend into it. It must stay out of `flutter test` , because a test
run of `plugins/setup` would execute the build hook itself.
2026-09-25 03:56:55 +08:00
Native plugin code under `plugins/*/{linux,macos,windows}` is compiled by the CI `desktop` job, which runs
2026-10-16 22:42:17 +08:00
`flutter build <platform> --debug` on every branch push, and by the release matrix on a tag; `tool/check_plugins.sh`
only analyzes and tests the Dart side.
2026-09-25 03:56:55 +08:00
2026-09-05 20:09:26 +08:00
Root `flutter test` only discovers the root package's `test/` directory by default. Include bundled plugin Dart tests by passing paths explicitly, or run `flutter test` from that plugin package directory, or run `bash tool/check_plugins.sh` to analyze and test every plugin package the way CI does. Native plugin tests under platform folders are not run by `flutter test` .
2026-05-27 15:16:51 +08:00
2026-08-05 10:57:40 +08:00
For the current Core/service architecture, useful focused checks are:
``` bash
flutter test test/core/desktop/
flutter test test/core/service_test.dart
flutter test test/core/protocol_contract_test.dart
flutter test test/manager/core_manager_test.dart
flutter test test/providers/action_test.dart test/providers/system_action_test.dart
flutter test test/widgets/core_status_button_test.dart
```
What those suites own:
- `test/core/desktop/` : replaceable IPC transport, RPC request correlation/failure, direct/Helper process leases, and
2026-10-08 17:58:43 +08:00
latest-intent desktop lifecycle convergence. `ipc_native_test.dart` runs the transport over a real socket through
the Rust `IpcServer` ; the server's own behavior is covered by `cargo test` in `plugins/rust_api/rust` .
2026-08-05 10:57:40 +08:00
- `test/core/service_test.dart` : `CoreService` composition and terminal close behavior.
- `test/core/protocol_contract_test.dart` : shared Dart/Go method and event-envelope compatibility, including event batches.
- `test/providers/action_test.dart` : Core start/restart orchestration and overlapping restart requests.
- `test/providers/system_action_test.dart` : ordered, idempotent exit cleanup and watchdog behavior.
- `test/widgets/core_status_button_test.dart` : 600-millisecond connecting presentation hold, immediate failure display,
long-running connecting state, and disconnected restart.
## Native Component Verification
2026-10-08 17:58:43 +08:00
The CI Go-wrapper checks can be reproduced without CGO, the Android files included:
2026-08-05 10:57:40 +08:00
``` bash
cd core
CGO_ENABLED = 0 go test .
2026-10-08 17:58:43 +08:00
CGO_ENABLED = 0 go vet . ./cmd/...
GOOS = android GOARCH = arm64 CGO_ENABLED = 0 go vet -tags= with_gvisor . ./mobile ./tun ./platform
2026-08-05 10:57:40 +08:00
```
The Windows Helper's loopback/session protocol tests are host-independent by default. Windows CI additionally enables its
service implementation:
``` bash
cargo fmt --manifest-path services/helper/Cargo.toml -- --check
cargo test --manifest-path services/helper/Cargo.toml
cargo test --manifest-path services/helper/Cargo.toml --features windows-service
```
The last command requires Windows for meaningful service coverage. Native Android lifecycle edits should at minimum
2026-09-25 03:56:55 +08:00
compile the modules they touch, with the JDK 17 that CI installs. On macOS the Homebrew `openjdk@17` keg is not
registered with `java_home` , so export it by prefix:
2026-08-05 10:57:40 +08:00
``` bash
cd android
2026-09-25 03:56:55 +08:00
export JAVA_HOME = " $( brew --prefix openjdk@17) /libexec/openjdk.jdk/Contents/Home " # elsewhere: any JDK 17
2026-10-08 17:58:43 +08:00
./gradlew :core:compileDebugKotlin -Ptarget-platform= android-arm64 # builds the gomobile AAR for one ABI
2026-09-25 03:56:55 +08:00
./gradlew :service:compileDebugKotlin
./gradlew :app:compileDebugKotlin
2026-08-05 10:57:40 +08:00
```
Always-on VPN entry, system VPN revoke, actual permission UI, and rapid device start/stop still require Android device or
emulator validation; Kotlin compilation cannot prove those system callbacks.
2026-09-05 20:09:26 +08:00
## Changelog And Release
The changelog is derived from Conventional Commits by `tool/changelog.dart` and written to two committed files:
`CHANGELOG.md` for readers and `changelog.json` for the renderers. See `.agents/rules.md` for the `Changelog:` trailers
that decide the wording.
The app ships no changelog of its own. `render release` appends the released version as JSON inside an HTML comment
(`<!-- flclash:changelog:json … -->` ), so the release body GitHub already returns to `checkForUpdate` carries the
notes shown in the update dialog. `parseReleaseChangelog` reads that block and falls back to the English
bullets when a release predates it.
``` bash
dart run tool/changelog.dart verify # what CI checks
dart run tool/changelog.dart release --version 0.8.96
dart run tool/changelog.dart build --unreleased # changelog.json only, includes untagged work
dart run tool/changelog.dart render release --out release.md
dart run tool/changelog.dart render telegram --out telegram.md
```
Releasing a stable version, in order:
``` bash
tool/bump_version.sh all
dart run tool/changelog.dart release --version 0.8.96
git commit -am "chore(release): v0.8.96"
git tag v0.8.96
git push origin main && git push origin v0.8.96
```
Push the tag by name. Every release tag here is lightweight, and `--follow-tags` carries annotated tags only: it skips a
lightweight one silently, so the branch lands, the tag does not, and the release workflow never fires.
`tool/release.sh` drives both paths so the ordering below cannot be got wrong by hand. It resolves the version (bumping
the patch when pubspec still names an already tagged one), prints the notes the tag would ship, and only pushes with
`--push` :
``` bash
tool/release.sh pre --dry-run # plan and notes, changes nothing
tool/release.sh pre --push # bump, tag vX.Y.Z-pre.N, push
tool/release.sh stable --push # changelog, chore(release) commit, tag, push
```
The release commit comes before the tag on purpose: the generated wording is reviewable in the diff before it ships, and
the tag is what `render release` reads. CI never writes back to the repository; it only runs `verify` . Wording in
`changelog.json` may be edited by hand as long as no derivable entry disappears and every entry still points at a commit
inside that version's range.
Entries at or below `v0.8.96` are frozen: they predate the pipeline, live under the `<!-- changelog:frozen -->` marker
in `CHANGELOG.md` , and are never regenerated.
`verify` compares a version only when its tag is reachable from `HEAD` , because that is the same scope the builder walks
(`git tag --merged` ). A branch cut before the newest release cannot derive that version at all, so `verify` names it as
skipped and moves on instead of reporting drift that does not exist. Checking mere tag existence is what made every such
branch fail on an unrelated release.
Prerelease tags (`v0.8.96-pre.N` ) skip the release commit, and CI renders their notes with `build --unreleased` for the
Telegram post. They publish no GitHub release, so the update dialog never sees them. `build --unreleased` reads the
version from `pubspec.yaml` rather than the tag, so the patch has to be bumped before the first `-pre.N` of a cycle:
while `v<pubspec version>` is still tagged it refuses to collect anything and the release job fails.
2026-10-01 12:29:02 +08:00
### Website
`site/` is the GitHub Pages site: a static page that renders `CHANGELOG.md` and the download links in the browser, and
works as is for local previews. `tool/build_site.sh [dir]` assembles it with the preview images, the app icon,
`CHANGELOG.md` read at the latest release tag, and a `release.json` of asset names, sizes and digests fetched through
`gh` . Without `gh` access the page falls back to the newest version in `CHANGELOG.md` and lists the files without sizes.
It then runs `tool/render_site.mjs` (Node), which writes the English page and a `zh/` copy for search engines: page
text, the first changelog releases, canonical and `hreflang` links, JSON-LD and `sitemap.xml` , all from the strings,
2026-10-08 17:58:43 +08:00
changelog parser and release markup in `site/assets/shared.js` , plus the GitHub star count from `release.json` so the
nav does not grow when the page loads. `SITE_URL` overrides the published URL these point at.
2026-10-01 12:29:02 +08:00
On the built site each URL keeps its language: the language button links to the other page and carries the section in
view, the chosen platform and the changelog search. Only the English root sends readers who chose or prefer Chinese on
to `zh/` , unless its URL carries `?lang=en` : the button on `zh/` links there with it, so getting back to English never
depends on browser storage. A project site cannot serve the `robots.txt` that would announce the sitemap, so submit
`sitemap.xml` in Google Search Console and Bing Webmaster Tools once.
``` bash
tool/build_site.sh && python3 -m http.server -d build/site
```
The page's look is deliberate: low-saturation flat colour, no blur, glow, glass or soft shadow, and motion that follows
the logo's 30° lattice. `site/assets/field.js` draws the hero's capsules on that lattice with the glyph's own
proportions, and the slanted wipes, the dot-to-bar marks and the theme transition reuse the same angle. Text that sits
inside the hero has to be listed in `wireField` in `app.js` , or the capsules run behind it.
`.github/workflows/pages.yaml` deploys it after every successful stable tag build and on pushes to `main` that touch the
site, in the public repository only; its Pages source has to be set to GitHub Actions once. The file names come from the
release matrix in `build.yaml` , so a renamed artifact needs the same change in `buildsFor` in `site/assets/app.js` .
2026-05-27 15:16:51 +08:00
## Verify
2026-09-25 03:56:55 +08:00
In the public repository every branch push runs the `dart` job; in the private
`origin` every check job runs only when started by hand (`workflow_dispatch` ).
The `dart` job performs these root-package checks in order:
2026-05-27 15:16:51 +08:00
``` bash
2026-10-08 17:58:43 +08:00
bash tool/hooks/test.sh
2026-09-06 06:06:55 +08:00
flutter pub get
dart format --output= none --set-exit-if-changed lib test tool plugins setup.dart
flutter analyze --no-fatal-infos
dart run tool/changelog.dart verify # main and tags only
flutter test --reporter expanded --coverage
dart run tool/check_coverage.dart coverage/lcov.info 75
2026-05-27 15:16:51 +08:00
```
2026-10-08 17:58:43 +08:00
`tool/hooks/` holds the scripts the git hooks run, and `tool/hooks/test.sh` runs every `*_test.sh` beside them, so a
new hook script needs only its test file next to it, not a workflow step.
2026-09-25 03:56:55 +08:00
Run `flutter analyze` before committing; CI fails on anything it reports.
2026-08-05 10:57:40 +08:00
2026-09-06 06:06:55 +08:00
Release builds run only for `v*` tag pushes; pull requests trigger nothing.
2026-08-05 10:57:40 +08:00
Root analysis excludes `plugins/**` , and root tests do not discover nested
2026-09-06 06:06:55 +08:00
plugin packages, so parallel jobs validate the rest from their own package
directories: `plugins` (local Flutter packages and the setup build tool), `go`
2026-10-08 17:58:43 +08:00
(the Core wrapper, plus a vet of the Android files), `android` (JVM unit tests
for `:common` , `:service` and `:app` , with the Flutter compile tasks excluded
and the gomobile AAR built for one ABI), `rust` (both crates), and a
2026-10-16 22:42:17 +08:00
Windows runner for the helper's `windows-service` feature. On branch pushes a
`desktop` job also compiles a debug build per desktop platform; a tag skips it,
since its release matrix compiles the same native code. Release builds run only
in the public repository and start alongside the checks; `upload` publishes
only once every build and every check job passes.
2026-09-05 20:09:26 +08:00
`bash tool/check_plugins.sh` is that plugin gate, and CI runs the same script.
It discovers every `plugins/*/pubspec.yaml` , analyzes each package, and runs
`flutter test` wherever `test/*_test.dart` exists. Adding a plugin package needs
no workflow edit; enumerating packages by hand in the workflow is what
previously left `plugins/tray` unanalyzed and untested.
2026-10-16 22:42:17 +08:00
### CI Caches
A run restores only cache entries saved on its own branch or tag, or on the default branch (`main` ), and the
repository holds 10 GB before evicting the least recently used entries. Every release is a new tag that no later run
reads from, so `BRANCH_CACHE` turns caching off in tag runs: one release used to write about 9 GB of such entries and
evict everything `dev` had saved. Release builds therefore start cold; branch runs, chiefly `dev` , keep the caches.
- setup-go keys its entry on the `cache-dependency-path` files alone and saves only on a miss, so jobs listing the
same files share one entry holding the build cache of whichever finished first. `desktop` and `android` each add
the file that drives their Core build to get an entry of their own.
- setup-gradle writes only on the default branch unless told otherwise, so `android` lets `dev` write as well. Its
entries are keyed by job, which is one more reason the release build gains nothing from caching.
2026-09-05 20:09:26 +08:00
## Worktree Tooling
``` bash
bash tool/worktrees.sh list # every worktree with owner tool and dirty/clean state
bash tool/worktrees.sh prune # remove clean worktrees; add --force to drop dirty ones too
```