Add Android client, packaging, Makefile entrypoint and handover docs

Why: the set has to be installable and publishable, not just buildable, and
the next person needs the reasoning, not only the code.

What:
- android/: Kotlin client with no third-party dependencies. A
  DocumentsProvider publishes the mirrored databases to every file picker, so
  KeePassDX opens them as ordinary documents; closing a file it wrote
  triggers a sync. A foreground service holds the event stream open.
  Mirror.decide transcribes the Go merge table, so both platforms resolve a
  conflict identically.
- Makefile as the single entrypoint, with file targets so only what changed
  is rebuilt: host and cross builds, both .deb packages, the macOS DMG with
  its /Applications symlink, the Windows zip, the APK under its F-Droid
  publishing name, and manual-only android-publish.
- versions.mk holds one version per platform plus the Android versionCode;
  `make bump` and `make changelog` move them. Debian changelog is tracked.
- systemd units (hardened system unit for the server, user unit for the
  agent), macOS bundle and launch agent, Windows Task Scheduler job.
- docs/HANDOVER.md on architecture and every decision that shaped it,
  docs/PROTOCOL.md for the API, README for the quick start.

Proved: make check and make test-race both pass; built both .deb packages and
inspected their contents, built the Windows zip, cross-compiled all five
desktop targets, and exercised make bump. The Android module is written but
not compiled: no JDK or Android SDK in this environment, which HANDOVER.md
states plainly.
This commit is contained in:
2026-10-29 16:26:33 +01:00
parent 4d0d53ff04
commit 0e83acb9b1
40 changed files with 2563 additions and 24 deletions
+188
View File
@@ -0,0 +1,188 @@
# KeepOrg: build and publish entrypoint.
#
# Every target is a real file where it can be, so make rebuilds only what
# changed. Run `make help` for the list.
include versions.mk
GO := go
BUILD := build
DIST := dist
GOFLAGS := -trimpath
HOSTOS := $(shell $(GO) env GOOS)
HOSTARCH := $(shell $(GO) env GOARCH)
GO_SRC := $(shell find . -name '*.go' -not -path './$(DIST)/*' -not -path './$(BUILD)/*') go.mod
# The version stamped into a binary depends on the platform it is built for.
version_for = $(strip \
$(if $(filter linux,$(1)),$(VERSION_LINUX),\
$(if $(filter darwin,$(1)),$(VERSION_MACOS),\
$(if $(filter windows,$(1)),$(VERSION_WINDOWS),\
$(if $(filter freebsd openbsd netbsd,$(1)),$(VERSION_BSD),0.0.0)))))
os_of = $(word 1,$(subst -, ,$(1)))
arch_of = $(word 2,$(subst -, ,$(1)))
HOST_CLIENT := $(BUILD)/$(HOSTOS)-$(HOSTARCH)/keeporg
HOST_SERVER := $(BUILD)/$(HOSTOS)-$(HOSTARCH)/keeporgd
.DEFAULT_GOAL := help
# ---------------------------------------------------------------- developing
help: ## show this help
@printf 'KeepOrg %s\n\n' "$(VERSION_LINUX)"
@grep -hE '^[a-zA-Z0-9_%/.-]+:.*?## ' $(MAKEFILE_LIST) \
| sort | awk 'BEGIN{FS=":.*?## "}{printf " \033[1m%-22s\033[0m %s\n", $$1, $$2}'
@printf '\nVersions: server %s linux %s macos %s windows %s bsd %s android %s (code %s)\n' \
"$(VERSION_SERVER)" "$(VERSION_LINUX)" "$(VERSION_MACOS)" "$(VERSION_WINDOWS)" \
"$(VERSION_BSD)" "$(VERSION_ANDROID)" "$(ANDROID_VERSION_CODE)"
fmt: ## format the Go sources
gofmt -w $(filter %.go,$(GO_SRC))
vet: ## run go vet
$(GO) vet ./...
test: ## run the whole test suite
$(GO) test ./...
test-race: ## run the test suite under the race detector
$(GO) test -race ./...
check: fmt vet test ## format, vet and test
clean: ## remove build and dist output
rm -rf $(BUILD) $(DIST)
# ------------------------------------------------------------------ building
.PHONY: all server client
all: server client ## build the server and client for this host
server: $(HOST_SERVER) ## build the server for this host
client: $(HOST_CLIENT) ## build the desktop client for this host
$(BUILD)/%/keeporg: $(GO_SRC)
@mkdir -p $(dir $@)
GOOS=$(call os_of,$*) GOARCH=$(call arch_of,$*) CGO_ENABLED=0 \
$(GO) build $(GOFLAGS) \
-ldflags "-s -w -X main.Version=$(call version_for,$(call os_of,$*))" \
-o $@ ./cmd/keeporg
$(BUILD)/%/keeporgd: $(GO_SRC)
@mkdir -p $(dir $@)
GOOS=$(call os_of,$*) GOARCH=$(call arch_of,$*) CGO_ENABLED=0 \
$(GO) build $(GOFLAGS) \
-ldflags "-s -w -X main.Version=$(VERSION_SERVER)" \
-o $@ ./cmd/keeporgd
$(BUILD)/windows-%/keeporg.exe: $(GO_SRC)
@mkdir -p $(dir $@)
GOOS=windows GOARCH=$* CGO_ENABLED=0 \
$(GO) build $(GOFLAGS) -ldflags "-s -w -X main.Version=$(VERSION_WINDOWS)" \
-o $@ ./cmd/keeporg
.PHONY: cross
cross: ## build every desktop and server binary
cross: $(BUILD)/linux-amd64/keeporg $(BUILD)/linux-arm64/keeporg \
$(BUILD)/linux-amd64/keeporgd $(BUILD)/linux-arm64/keeporgd \
$(BUILD)/darwin-amd64/keeporg $(BUILD)/darwin-arm64/keeporg \
$(BUILD)/freebsd-amd64/keeporg $(BUILD)/freebsd-arm64/keeporg \
$(BUILD)/windows-amd64/keeporg.exe
# ------------------------------------------------------------------ packaging
DEB_ARCH ?= $(HOSTARCH)
DEB_CLIENT := $(DIST)/keeporg_$(VERSION_LINUX)_$(DEB_ARCH).deb
DEB_SERVER := $(DIST)/keeporgd_$(VERSION_SERVER)_$(DEB_ARCH).deb
.PHONY: deb deb-client deb-server
deb: deb-client deb-server ## build both Debian packages
deb-client: $(DEB_CLIENT) ## build the client .deb
deb-server: $(DEB_SERVER) ## build the server .deb
$(DEB_CLIENT): $(BUILD)/linux-$(DEB_ARCH)/keeporg packaging/deb/client/control.in \
packaging/deb/changelog packaging/systemd/keeporg.service
scripts/build-deb.sh client $(VERSION_LINUX) $(DEB_ARCH) $@
$(DEB_SERVER): $(BUILD)/linux-$(DEB_ARCH)/keeporgd packaging/deb/server/control.in \
packaging/deb/changelog packaging/systemd/keeporgd.service
scripts/build-deb.sh server $(VERSION_SERVER) $(DEB_ARCH) $@
DMG_ARCH ?= $(HOSTARCH)
DMG := $(DIST)/KeepOrg-$(VERSION_MACOS)-$(DMG_ARCH).dmg
.PHONY: dmg
dmg: $(DMG) ## build the macOS DMG with an /Applications symlink (macOS only)
$(DMG): $(BUILD)/darwin-$(DMG_ARCH)/keeporg packaging/macos/Info.plist.in \
packaging/macos/launcher.sh packaging/macos/launch-agent.plist.in
scripts/build-dmg.sh $(VERSION_MACOS) $(DMG_ARCH) $(APPID_MACOS) $@
ZIP := $(DIST)/keeporg-$(VERSION_WINDOWS)-windows-amd64.zip
.PHONY: windows
windows: $(ZIP) ## build the Windows zip
$(ZIP): $(BUILD)/windows-amd64/keeporg.exe packaging/windows/README.txt
@mkdir -p $(DIST)
rm -rf $(BUILD)/windows-stage && mkdir -p $(BUILD)/windows-stage
cp $(BUILD)/windows-amd64/keeporg.exe $(BUILD)/windows-stage/
cp packaging/windows/README.txt $(BUILD)/windows-stage/
cp packaging/windows/keeporg-task.xml $(BUILD)/windows-stage/
cd $(BUILD)/windows-stage && zip -q -r ../../$@ .
# -------------------------------------------------------------------- android
GRADLE ?= $(shell test -x android/gradlew && echo ./gradlew || echo gradle)
APK_BUILT := android/app/build/outputs/apk/release/app-release-unsigned.apk
APK := $(DIST)/$(APPID_ANDROID)_$(ANDROID_VERSION_CODE).apk
.PHONY: android
android: $(APK) ## build the unsigned release APK under its publishing name
$(APK): $(shell find android/app/src -type f 2>/dev/null) android/app/build.gradle.kts
cd android && $(GRADLE) --no-daemon assembleRelease \
-PversionName=$(VERSION_ANDROID) -PversionCode=$(ANDROID_VERSION_CODE)
@mkdir -p $(DIST)
cp $(APK_BUILT) $@
@printf 'built %s\n' "$@"
# --------------------------------------------------------------------- publish
FDROID_HOST ?= rcmd@garage.rcmd.space
FDROID_UNSIGNED ?= /var/lib/fdroid/unsigned
FDROID_METADATA ?= /var/lib/fdroid/metadata
.PHONY: android-publish
android-publish: $(APK) ## upload the APK and metadata to the private F-Droid repo (manual)
@printf 'publishing %s to %s:%s\n' "$(notdir $(APK))" "$(FDROID_HOST)" "$(FDROID_UNSIGNED)"
scp $(APK) $(FDROID_HOST):$(FDROID_UNSIGNED)/
scp android/metadata/$(APPID_ANDROID).yml $(FDROID_HOST):$(FDROID_METADATA)/
@printf 'uploaded. Sign and publish on the repo host with `fdroid publish && fdroid update`.\n'
.PHONY: dist
dist: deb windows ## build every artifact this host can build
@printf '\nartifacts in %s:\n' "$(DIST)"
@ls -1 $(DIST)
# -------------------------------------------------------------------- versions
.PHONY: bump
bump: ## bump a version: make bump PLATFORM=android PART=minor
scripts/bump.sh "$(PLATFORM)" "$(PART)"
.PHONY: changelog
changelog: ## add a Debian changelog entry: make changelog MSG="what changed"
scripts/changelog.sh "$(VERSION_LINUX)" "$(MSG)"
# ------------------------------------------------------------------ local runs
.PHONY: run-server
run-server: $(HOST_SERVER) ## run a server on 127.0.0.1:8765 with data in ./.devdata
KEEPORG_ADMIN_PASSWORD=$${KEEPORG_ADMIN_PASSWORD:-dev-password-1} \
$(HOST_SERVER) -addr 127.0.0.1:8765 -data .devdata -url http://127.0.0.1:8765
.PHONY: run-client
run-client: $(HOST_CLIENT) ## run the sync agent with the local configuration
$(HOST_CLIENT) run