# SPDX-License-Identifier: Apache-2.0

# Leyline — top-level developer entry points. See docs/dev/setup.md.
#
#   make proto      regenerate leyline.v1 code (Go + Swift) from proto/
#   make version    regenerate the engine's version constant and ley's fallback from the root
#                   VERSION file
#   make go         build the Go clients and tools (ley, leyfix, leyshots) into go/bin
#   make bands-json regenerate the app's seed copy of the band table from `ley bands --json`
#   make go-test    Go unit + contract tests
#   make race       Go tests that exercise goroutines, under the race detector
#   make swift      build the engine (leylined) — macOS for the real thing, Linux compiles the non-DSP core
#   make swift-test engine tests and the optional SDR-library matrix; depends on fixtures so the fixture round-trips actually run (set
#                   LEYLINE_FIXTURES to point the tests elsewhere). The suite runs optimised, then EngineCoreTests again in debug
#   make fixtures   generate IQ fixtures into fixtures/ with leyfix (FIXTURE_DURATION=0.5 for a quick set)
#   make e2e        cross-language contract test: `ley` driving a locally built leylined over UDS
#   make eval       the agent evals: an agent on `ley mcp` against fixtures, graded (costs tokens)
#   make eval-test  the eval runner's own tests against a locally built leylined: scripted agents,
#                   no tokens
#   make speech-spike RECORDING=<id> ENGINES="speech=whisper-1 speech=gpt-4o-transcribe"
#                   one row per speech engine on a gated recording, scored against its hand
#                   transcript (docs/dev/speech-spike.md); a cloud engine costs what it bills
#   make shots      the site's screenshots into tmp/shots (docs/plans/site-shots.md): only the scenes
#                   whose inputs changed since their image was taken; ONLY=a,b takes those scenes
#                   whatever changed. The app scenes need the Mac; elsewhere terminal
#                   scenes stop at HTML
#   make shots-publish  release the reviewed tmp/shots images as shots-YYYY-MM-DD, merged with the
#                   latest shots-* release; ONLY=a,b refreshes some (SHOTS_ARGS=--dry-run to check)
#   make shots-release  make shots, then release the images that differ from the latest shots-*
#                   release, or nothing when none does (SHOTS_ARGS=--dry-run to check)
#   make app        build the Mac app package (app/): the app and the client façade on macOS, the
#                   façade alone on Linux, where the SwiftUI target is not declared
#   make app-test   the façade's unit tests (no daemon)
#   make app-e2e    the façade against a locally built leylined --no-hardware playing a fixture
#   make app-run    macOS: run the app straight from the package (no bundle, no signature)
#   make app-bundle macOS: assemble and sign app/dist/Leyline.app (scripts/bundle-app.sh)
#   make app-check  macOS: the app's checks (docs/plans/mac-loop.md, ML-4): each scene in
#                   app/checks/scenes.yaml against its own daemon, its pictures and trees compared
#                   with the goldens in tmp/goldens, a line each in tmp/checks; ONLY=a,b runs those
#                   scenes, RECORD=1 writes their goldens instead, JOBS=N runs N side by side
#   make app-check-smoke     macOS: the smoke scenes alone (ML-11's first tier)
#   make app-check-affected  macOS: the scenes covering what changed since the merge base with
#                   main (BASE=<ref> for another), uncommitted files included; fails on a
#                   changed file no scene covers, and on selecting no scene
#   make release    macOS: cut the next release from start to finish (scripts/cut-release.sh):
#                   check the build Mac, bump VERSION, date the CHANGELOG's Unreleased section,
#                   commit and tag, make release-build, push, make release-publish, then name the
#                   two manual steps left. Run again after a failure, it carries on from where it
#                   stopped. VERSION X-dev releases as X and X.Y.Z as X.Y.Z+1; BUMP=minor or
#                   BUMP=major picks another number, NEXT=0.2.0 names it, YES=1 skips the question,
#                   DRY_RUN=1 is a rehearsal that changes nothing and builds an ad-hoc DMG in a
#                   temporary directory
#   make release-build  macOS: the signed, notarized Leyline-<VERSION>.dmg, the source tarballs it
#                   owes and the update feed in dist/<VERSION>/ (scripts/release.sh), from a clean
#                   tree whose VERSION is a release's. CODESIGN_IDENTITY and NOTARY_PROFILE default
#                   to the build Mac's identity and notary profile
#   make release-publish  the GitHub release v<VERSION> from dist/<VERSION>/, refused until the
#                   commit it was built from is pushed
#   make release-rehearse  macOS: the tree as it is as an ad-hoc-signed app and DMG in OUT (default a
#                   new temporary directory), without credentials
#   make release-test  the parts of scripts/release.sh, scripts/cut-release.sh and scripts/reload.sh
#                   that need no Mac
#   make reload     macOS: rebuild ley and leylined (release), copy them to ~/Library/Application
#                   Support/Leyline/bin signed with CODESIGN_IDENTITY (ad hoc, with a warning, without
#                   it), stop the running daemon, reinstall the LaunchAgent on the copy and start it —
#                   the edit-build-try loop in one step (scripts/reload.sh)
#   make lint       golangci-lint + gofumpt (pinned versions, installed into .tools/<host>/bin)
#   make app-lint   swift-format over the app package, and no colour or font outside Theme.swift
#   make docs-check every relative link and backticked repository path in the docs resolves
#   make vulncheck  govulncheck over the Go module: known vulnerabilities in code ley calls
#   make workflow-lint  actionlint over .github/workflows
#   make hot-path   macOS: allocations per block in the release S2 harness (invariant 4)
#   make check      everything CI runs on this platform (includes the fixture and e2e suites);
#                   vulncheck, workflow-lint and hot-path need the network or a Mac and run in CI
SHELL := /bin/bash
GOBIN := $(CURDIR)/go/bin
SWIFT_CONFIG ?= debug
FIXTURE_DURATION ?= 1
# The root VERSION file is the single source of truth. Swift reads a generated constant (make
# version); the Go binaries are stamped at link time. A checkout sitting on the tag v$(VERSION)
# prints the bare number; anything else carries what it actually is — `0.1.0+3-gd34db33-dirty`, or
# `0.1.0-dev+d34db33` before the first tag — so a bug report names one tree.
VERSION := $(shell tr -d '[:space:]' < $(CURDIR)/VERSION)
GIT_DESCRIBE := $(shell git -C $(CURDIR) describe --tags --always --dirty --match 'v*' --abbrev=7 2>/dev/null | sed 's/^v$(VERSION)-//')
BUILD_VERSION := $(if $(filter v$(VERSION),$(shell git -C $(CURDIR) describe --tags --exact-match --match 'v*' 2>/dev/null)),$(VERSION),$(VERSION)$(if $(GIT_DESCRIBE),+$(GIT_DESCRIBE)))
GO_LDFLAGS := -X github.com/reflexive-labs/leysdr/go/internal/cli.Version=$(BUILD_VERSION)
# Repo-pinned developer tools, per host (a checkout shared between a Mac and a Linux container must
# not hand one host the other's binaries). scripts/gen-proto.sh keeps the protoc plugins here too.
HOST := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(shell uname -m)
TOOLS := $(CURDIR)/.tools/$(HOST)/bin
GOLANGCI_LINT_VERSION := v2.14.0
GOFUMPT_VERSION := v0.12.0
GOVULNCHECK_VERSION := v1.8.0
ACTIONLINT_VERSION := v1.7.12

# The build Mac's Developer ID Application identity and `xcrun notarytool store-credentials`
# profile (docs/plans/distribution.md, "Owner prerequisites"); either can be overridden from the
# environment or the command line. Only the release targets and reload pass them on, so `make
# app-bundle` stays ad-hoc signed.
CODESIGN_IDENTITY ?= Developer ID Application: Reflexive Labs LLC. (P2KZW25PL8)
NOTARY_PROFILE ?= leysdr-notary

.PHONY: reload all proto proto-check version version-check go go-test bands-json race swift swift-release swift-test sdr-loader-test fixtures e2e eval eval-test speech-spike shots shots-bundle shots-publish shots-release app-check app-check-smoke app-check-affected release release-build release-publish release-rehearse release-test app app-test app-e2e app-run app-bundle lint app-lint docs-check vulncheck workflow-lint hot-path check clean install-decoders

all: go swift app

proto:
	./scripts/gen-proto.sh

# Fails if generated code is stale relative to proto/.
proto-check:
	./scripts/gen-proto.sh
	git diff --exit-code -- go/gen swift/LeylineProto/Sources

version:
	./scripts/gen-version.sh

# Fails if the engine's version constant or the Go fallback literal has drifted from VERSION.
version-check:
	./scripts/gen-version.sh
	git diff --exit-code -- engine/Sources/LeylineDaemon/Version.swift go/internal/cli/root.go
	cd go && go test -count=1 -run TestVersionMatchesTheSourceOfTruth ./internal/cli/

go:
	cd go && GOBIN=$(GOBIN) go install -ldflags '$(GO_LDFLAGS)' ./cmd/...

go-test:
	cd go && go test ./...

# bands.json is the app's seed layer: the band table lives in Go (go/pkg/bandplan/bands.go), the
# app has no Go library, so the sidebar's bands come from the exact bytes `ley bands --json`
# prints, checked in as a resource of the LeylineClient target. TestBandsJSONResource fails when
# the two drift and names this target; the app never edits the file.
bands-json: go
	mkdir -p app/Sources/LeylineClient/Resources
	$(GOBIN)/ley bands --json > app/Sources/LeylineClient/Resources/bands.json

# The verbs stream events on a background goroutine while the foreground reads the session mirror,
# which only the race detector can police; it is a separate target because -race is slow enough that
# nobody would run the whole suite that way.
race:
	cd go && go test -race ./internal/cli/...

swift:
	cd engine && swift build -c $(SWIFT_CONFIG)

swift-release:
	cd engine && swift build -c release

# The engine's suite runs optimised, as the daemon people run is: its real-time tests play 2.4 MSPS
# through the whole pipeline, and a debug daemon on a shared runner fell behind the radio and
# dropped samples the tests then missed (docs/plans/app.md, MC-9). -enable-testing keeps the
# @testable imports. It has its own scratch path, so it never rebuilds the plain release build
# `make hot-path` measures, nor that one it. EngineCoreTests then run again in debug, the
# configuration that compiles `assert` and the unoptimised kernels; they are the fast unit tests
# (about 40 s), so the second run is cheap.
SWIFT_TEST_SCRATCH := .build/release-testing
swift-test: fixtures sdr-loader-test
	cd engine && swift test -c release -Xswiftc -enable-testing --scratch-path $(SWIFT_TEST_SCRATCH)
	cd engine && swift test --filter '^EngineCoreTests\.'

sdr-loader-test:
	./scripts/test-optional-sdr-loaders.sh

fixtures: go
	$(GOBIN)/leyfix generate --out fixtures --duration $(FIXTURE_DURATION)

# go/internal/e2e skips itself unless both binaries are named, so this is the only place it runs.
# -count=1: the test's inputs are the binaries, which the go test cache does not see.
# --show-bin-path resolves SwiftPM's per-platform directory; the .build/<config> symlink can point
# at another host's build on a shared checkout.
# The decode e2e needs the repository's plugin manifests and the plugin binaries `make go` put beside
# ley: LEYLINE_DECODERS names the manifests and PATH is where the daemon finds `leydec-aprs`.
e2e: go swift fixtures
	cd go && LEYLINED_BIN="$$(cd ../engine && swift build -c $(SWIFT_CONFIG) --show-bin-path)/leylined" LEY_BIN=$(GOBIN)/ley \
		LEYLINE_DECODERS=$(CURDIR)/decoders PATH="$(GOBIN):$$PATH" \
		go test -count=1 -v ./internal/e2e/...

# The daemon under test is the one launchd runs, so a rebuild is only half the loop: the old process
# keeps serving until it is replaced. scripts/reload.sh copies the build out of engine/.build, which
# a check run relinks under a running daemon, signs the copy, and has the copied ley stop the daemon
# (a launchd job or a bare spawn alike) and `install` the plist on the copy, so this is safe to run
# whether or not an agent was installed before. It refuses when the Leyline app's agent holds the
# label.
# Copies the repo's decoder plugins (manifest + binary together) into the platform default decoder
# directory the daemon searches, so `ley decode`/`ley watch` find them. Depends on `go` for the
# leydec-* binaries.
install-decoders: go
	GOBIN=$(GOBIN) ./scripts/install-decoders.sh

reload: go swift-release install-decoders
	@[ "$$(uname -s)" = Darwin ] || { echo "make reload drives launchd; run it on the Mac" >&2; exit 2; }
	CODESIGN_IDENTITY="$(CODESIGN_IDENTITY)" LEY_SRC=$(GOBIN)/ley \
		LEYLINED_SRC="$$(cd engine && swift build -c release --show-bin-path)/leylined" ./scripts/reload.sh

# Each tool's file name carries its version and the Go toolchain it was built with, so bumping
# either (here or in go/go.mod) reinstalls it: golangci-lint refuses to lint code for a newer Go
# than built it.
GO_TOOLCHAIN := $(shell cd $(CURDIR)/go && go env GOVERSION 2>/dev/null)
GOLANGCI_LINT := $(TOOLS)/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(GO_TOOLCHAIN)
GOFUMPT := $(TOOLS)/gofumpt-$(GOFUMPT_VERSION)-$(GO_TOOLCHAIN)

$(GOLANGCI_LINT):
	cd go && GOBIN=$(TOOLS) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
	mv $(TOOLS)/golangci-lint $@

$(GOFUMPT):
	cd go && GOBIN=$(TOOLS) go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION)
	mv $(TOOLS)/gofumpt $@

# Every source file names its licence and every dependency is in third_party/licenses/MANIFEST.txt
# with terms the code that pulls it in may use (docs/decisions/D2-licensing.md). `--fix` adds headers.
license-check:
	./scripts/check-licenses.sh

docs-check:
	./scripts/check-docs.py

vulncheck:
	cd go && go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./...

workflow-lint:
	go run github.com/rhysd/actionlint/cmd/actionlint@$(ACTIONLINT_VERSION)

# Invariant 4 as a number: the S2 harness in a release build, counted for allocations that scale
# with blocks processed. macOS only (dyld interposition).
hot-path: swift-release
	./scripts/hot-path-allocations.sh

# The agent evals (docs/dev/evals.md): a daemon per scenario playing fixtures, an agent on `ley
# mcp` against it, graded. Costs tokens, so it is not in `check`; EVAL_ARGS passes scenario names
# or flags through (`make eval EVAL_ARGS="survey-2m --mode shell"`).
eval: go swift fixtures
	cd go && LEYLINED_BIN="$$(cd ../engine && swift build -c $(SWIFT_CONFIG) --show-bin-path)/leylined" LEY_BIN=$(GOBIN)/ley \
		LEYLINE_FIXTURES=$(CURDIR)/fixtures LEYLINE_DECODERS=$(CURDIR)/decoders PATH="$(GOBIN):$$PATH" \
		go run ./cmd/leyeval run --scenarios $(CURDIR)/evals/scenarios --out $(CURDIR)/evals/runs $(EVAL_ARGS)

# The eval runner's own tests (go/internal/eval) against the real daemon, with scripted agents and
# the keyless speech-scripted decoder, so they cost nothing. They skip themselves unless the
# binaries, the decoder manifests and the leydec-* plugins are named, as the e2e does, and
# `make e2e` runs only ./internal/e2e/..., so this is where they run.
eval-test: go swift fixtures
	cd go && LEYLINED_BIN="$$(cd ../engine && swift build -c $(SWIFT_CONFIG) --show-bin-path)/leylined" LEY_BIN=$(GOBIN)/ley \
		LEYLINE_DECODERS=$(CURDIR)/decoders PATH="$(GOBIN):$$PATH" \
		go test -count=1 ./internal/eval/...

# The speech-to-text spike (docs/dev/speech-spike.md): leyspeech transcribes RECORDING once per
# engine on the running daemon and scores each against the transcript.txt beside it. ENGINES is
# space-separated, each a decoder or decoder=model; SPIKE_ARGS passes more flags (--json, --ref F,
# --daemon). Costs whatever a cloud engine bills, so it is not in `check`.
speech-spike: go
	@test -n "$(RECORDING)" || { echo 'usage: make speech-spike RECORDING=<id> ENGINES="speech=whisper-1 speech=gpt-4o-transcribe"' >&2; exit 2; }
	$(GOBIN)/leyspeech compare --ley $(GOBIN)/ley --recording $(RECORDING) \
		$(foreach e,$(or $(ENGINES),speech),--engine $(e)) $(SPIKE_ARGS)

# The site's screenshots (docs/plans/site-shots.md): leyshots runs each scene in site/shots/scenes.yaml
# against its own leylined --no-hardware playing a scene fixture it generates into the cache. On the
# Mac the app is bundled first, because the app scenes launch app/dist/Leyline.app.
#
# Without ONLY, make takes only the stale images. While this file is read, before any mtime is
# compared, `leyshots keys` writes tmp/shots/keys/<scene>.key, rewritten only when one of the
# scene's own inputs changes (its entry in scenes.yaml, the files it reads, its fixtures' generator
# records, leyfix's source), and `leyshots makefile` writes tmp/shots/shots.mk: one rule per image,
# which depends on the scene's key and on its kind's SHOTS_DEPS_* sources and takes that one scene.
# A scene that leaves no PNG (an app scene on Linux, a notification that did not appear) runs again
# at the next `make shots`. go, swift and the bundle are phony, so they are order-only: as plain
# prerequisites they would make every image stale on every run. Scenes share tmp/shots/shots.json
# and one tmux server, so the run is serial. A failed scene stops make; `make -k shots` goes on.
SHOTS_OUT := $(CURDIR)/tmp/shots
LEYSHOTS = LEYLINED_BIN="$$(cd engine && swift build -c $(SWIFT_CONFIG) --show-bin-path)/leylined" LEY_BIN=$(GOBIN)/ley \
	LEYFIX_BIN=$(GOBIN)/leyfix $(GOBIN)/leyshots
SHOTS_RUN = $(LEYSHOTS) run
ifneq ($(filter shots-release,$(MAKECMDGOALS))$(if $(ONLY),,$(filter shots,$(MAKECMDGOALS))),)
SHOTS_GEN := $(shell mkdir -p $(SHOTS_OUT)/bin && cd go && go build -o $(SHOTS_OUT)/bin/ ./cmd/leyshots ./cmd/leyfix && \
	$(SHOTS_OUT)/bin/leyshots keys --leyfix $(SHOTS_OUT)/bin/leyfix --out $(SHOTS_OUT)/keys >&2 && \
	$(SHOTS_OUT)/bin/leyshots makefile > $(SHOTS_OUT)/shots.mk && echo ok)
ifneq ($(SHOTS_GEN),ok)
$(error leyshots could not write $(SHOTS_OUT)/shots.mk; the lines above say why)
endif
.NOTPARALLEL:
.DELETE_ON_ERROR:
# make expands a rule's prerequisites as it reads the rule, so these come before the include.
SHOTS_GO := $(shell find go/cmd/ley go/cmd/leyshots go/cmd/leydec-* go/internal go/pkg go/gen -name '*.go' ! -name '*_test.go') \
	go/go.mod go/go.sum $(shell find decoders -type f)
SHOTS_ENGINE := $(shell find engine/Sources swift/LeylineProto/Sources -type f) engine/Package.swift swift/LeylineProto/Package.swift
SHOTS_APP := $(shell find app/Sources -type f) app/Package.swift scripts/bundle-app.sh
SHOTS_PAGE := scripts/ansi2html.py scripts/render-html.swift
SHOTS_DEPS_APP := $(SHOTS_GO) $(SHOTS_ENGINE) $(SHOTS_APP)
SHOTS_DEPS_TERMINAL := $(SHOTS_GO) $(SHOTS_ENGINE) $(SHOTS_PAGE)
# A table is drawn by leyshots alone, with no daemon and no ley.
SHOTS_DEPS_TABLE := $(filter go/cmd/leyshots/%,$(SHOTS_GO)) scripts/render-html.swift
SHOTS_DEPS_COMPOSITE := $(SHOTS_DEPS_APP) $(SHOTS_PAGE)
SHOTS_DEPS_ICON := scripts/render-icon.swift
include $(SHOTS_OUT)/shots.mk
$(SHOTS_ASSETS): | go swift
$(SHOTS_ASSETS_APP) $(SHOTS_ASSETS_COMPOSITE): | shots-bundle
endif

ifeq ($(ONLY),)
shots: $(SHOTS_ASSETS)
else
shots: go swift shots-bundle
	$(SHOTS_RUN) --only $(ONLY)
endif

shots-bundle:
	@if [ "$$(uname -s)" = Darwin ]; then ./scripts/bundle-app.sh; fi

shots-publish: go
	$(GOBIN)/leyshots publish $(if $(ONLY),--only $(ONLY)) $(SHOTS_ARGS)

# The stale images first, then a release of those whose png_sha256 differs from the latest shots-*
# release's; with none, leyshots says the latest release is up to date and creates nothing.
shots-release: $(SHOTS_ASSETS) | go
	$(GOBIN)/leyshots publish $(SHOTS_ARGS)

# The app's checks (docs/plans/mac-loop.md, ML-4): what make shots builds for an app scene, then
# leyshots check. Every scene runs, or ONLY's; RECORD=1 writes the goldens rather than comparing.
# The daemon is a release build whatever SWIFT_CONFIG says: a debug leylined plays scene_vhf20's
# 21 MSPS at about half of real time, which puts every timing in a scene out.
# The run is under caffeinate -i, so the Mac does not sleep through it with the display off.
# JOBS=N runs N scenes side by side (leyshots check --jobs; serial: true scenes run alone).
# app-check-smoke runs the smoke scenes; app-check-affected the scenes whose covers match a file
# changed under app/Sources or app/checks, or app/Package.swift, since the merge base with main,
# or BASE=<ref>; a base that is HEAD with nothing changed since compares with the commit before.
APP_CHECK = LEYLINED_BIN="$$(cd engine && swift build -c release --show-bin-path)/leylined" LEY_BIN=$(GOBIN)/ley \
	LEYFIX_BIN=$(GOBIN)/leyfix caffeinate -i $(GOBIN)/leyshots check $(if $(JOBS),--jobs $(JOBS)) \
	$(if $(filter 1,$(RECORD)),--record)
app-check: go swift-release shots-bundle
	$(APP_CHECK) $(if $(ONLY),--only $(ONLY))

app-check-smoke: go swift-release shots-bundle
	$(APP_CHECK) --smoke

app-check-affected: go swift-release shots-bundle
	$(APP_CHECK) --affected $(if $(BASE),--base $(BASE))

# Every step of a release in order, resumable (docs/dev/release-checklist.md, "The signed build").
# It calls make release-build, release-publish and release-rehearse itself. The recipe does not
# name $(MAKE), which would make `make -n release` run it.
release:
	CODESIGN_IDENTITY="$(CODESIGN_IDENTITY)" NOTARY_PROFILE="$(NOTARY_PROFILE)" \
		NEXT="$(NEXT)" BUMP="$(BUMP)" YES="$(YES)" DRY_RUN="$(DRY_RUN)" ./scripts/cut-release.sh

# The build of a release (docs/plans/distribution.md, "Sign, notarize, package"; the pass around it
# is docs/dev/release-checklist.md). scripts/release.sh builds its own ley and decoders rather than
# using go/bin, and refuses a dirty tree, a -dev VERSION and a dist/<VERSION>/ that already exists.
release-build:
	CODESIGN_IDENTITY="$(CODESIGN_IDENTITY)" NOTARY_PROFILE="$(NOTARY_PROFILE)" ./scripts/release.sh

release-publish:
	./scripts/release.sh publish

release-rehearse:
	./scripts/release.sh rehearse "$(or $(OUT),$$(mktemp -d "$${TMPDIR:-/tmp}/leyline-rehearsal.XXXXXX"))"

release-test:
	./scripts/test-release.sh
	./scripts/test-reload.sh

# The Mac app (docs/dev/app.md). One package at app/, depending on swift/LeylineProto for the
# generated contract and on the engine package not at all. `swift test` in app/ would run the
# daemon-backed suite too and silently skip it without LEYLINED_BIN, so the two targets name their
# suites: app-test skips it, app-e2e is the only place it runs, with the daemon `make swift` built
# and the fixtures the file device plays.
#
# app/Package.resolved is the macOS graph, which pins Sparkle; Linux evaluates the manifest without
# Sparkle (app/Package.swift), and a resolve there would rewrite the file without that pin. So on
# Linux the app targets only read the file, and fail rather than rewrite it when it is out of date:
# a dependency change is resolved on the Mac.
APP_RESOLVED := $(if $(filter Linux,$(shell uname -s)),--only-use-versions-from-resolved-file)

app:
	cd app && swift build $(APP_RESOLVED) -c $(SWIFT_CONFIG)

app-test:
	cd app && swift test $(APP_RESOLVED) --skip LeylineClientDaemonTests

app-e2e: swift fixtures
	cd app && LEYLINED_BIN="$$(cd ../engine && swift build -c $(SWIFT_CONFIG) --show-bin-path)/leylined" \
		LEYLINE_FIXTURES=$(CURDIR)/fixtures swift test $(APP_RESOLVED) --filter LeylineClientDaemonTests

app-run:
	@[ "$$(uname -s)" = Darwin ] || { echo "the app runs on the Mac" >&2; exit 2; }
	@mkdir -p tmp
	cd app && LEYLINE_APP_LOG=$(CURDIR)/tmp/leyline-app.log swift run -c $(SWIFT_CONFIG) LeylineApp

app-bundle:
	./scripts/bundle-app.sh $(BUNDLE_ARGS)

# swift-format ships in the toolchain; app/.swift-format pins its defaults at four spaces and 100
# columns (docs/dev/swift-style.md, "Files"). app-format rewrites, app-lint only reports. app-lint
# also runs scripts/check-theme.sh: no colour or font in the app outside Theme.swift.
app-format:
	cd app && swift format --in-place --recursive Sources Tests Package.swift

app-lint:
	cd app && swift format lint --strict --recursive Sources Tests Package.swift
	./scripts/check-theme.sh

lint: $(GOLANGCI_LINT) $(GOFUMPT)
	cd go && $(GOLANGCI_LINT) run ./... && test -z "$$($(GOFUMPT) -l .)"

check: proto-check version-check license-check docs-check go-test race lint app-lint swift swift-test e2e app app-test app-e2e release-test

clean:
	rm -rf go/bin engine/.build swift/LeylineProto/.build app/.build app/dist
