Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
245 lines (212 loc) · 12.7 KB
/
Copy pathMakefile
File metadata and controls
245 lines (212 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
BIN := bin/mobium
VERSION := $(shell cat VERSION 2>/dev/null || echo dev)
.PHONY: all build dist mcpb test fmt fmt-check vet lint clients java crosscompile api api-check flags flags-check quickstart license-check docs-check ci clean reader
all: build test
build:
@mkdir -p bin
go build -ldflags "-X main.version=$(VERSION)" -o $(BIN) ./cmd/mobium
@echo "built $(BIN)"
# reader rebuilds internal/mobiumdriver/reader/reader.dex, the screen reader-
# safe hierarchy reader, from Main.java. Only for changing the reader: the
# dex is checked in and pinned by checksum, so a build of Mobium needs neither
# a JDK nor the Android SDK. Needs javac, d8 (9.3.16 builds the pinned bytes)
# and an android.jar, found under ANDROID_HOME.
READER := internal/mobiumdriver/reader
ANDROID_JAR ?= $(lastword $(sort $(wildcard $(ANDROID_HOME)/platforms/android-*/android.jar)))
reader:
@test -n "$(ANDROID_JAR)" || { echo "no android.jar — set ANDROID_HOME or ANDROID_JAR"; exit 1; }
@tmp=$$(mktemp -d) && \
javac --release 8 -Xlint:-options -nowarn -cp "$(ANDROID_JAR)" -d $$tmp $(READER)/Main.java && \
d8 --release --min-api 24 --output $$tmp $$tmp/dev/mobium/reader/*.class && \
cp $$tmp/classes.dex $(READER)/reader.dex && rm -rf $$tmp && \
echo "built $(READER)/reader.dex — set reader.SHA256 to:" && shasum -a 256 $(READER)/reader.dex
# The Go client is a separate module, so ./... does not reach it and it has to
# be named. Forgetting that would mean it never runs in CI.
MODULES := . clients/go
test:
@for m in $(MODULES); do echo "test $$m"; (cd $$m && go test ./...) || exit 1; done
fmt:
gofmt -l -w cmd internal clients/go
# fmt-check reports rather than rewrites, because CI must fail on unformatted
# code instead of quietly fixing it and passing.
fmt-check:
@out=$$(gofmt -l cmd internal clients/go); \
if [ -n "$$out" ]; then echo "gofmt needed:"; echo "$$out"; exit 1; fi
@echo "gofmt clean"
vet:
@for m in $(MODULES); do (cd $$m && go vet ./...) || exit 1; done
# The version is pinned so a lint failure is a change in this repo and never
# an upgrade landing underneath it. Bump deliberately, and expect new findings
# when you do.
GOLANGCI_VERSION := 2.13.2
GOLANGCI := $(shell go env GOPATH)/bin/golangci-lint
lint:
@if ! [ -x "$(GOLANGCI)" ]; then echo "golangci-lint is not installed. Run: make lint-install"; exit 1; fi
@have=$$("$(GOLANGCI)" version 2>/dev/null | sed -n 's/.*has version \([0-9.]*\).*/\1/p'); if [ "$$have" != "$(GOLANGCI_VERSION)" ]; then echo "golangci-lint $$have installed, $(GOLANGCI_VERSION) pinned. Run: make lint-install"; exit 1; fi
@for m in $(MODULES); do (cd $$m && "$(GOLANGCI)" run ./...) || exit 1; done
# Downloads the pinned release and verifies it against the checksum the
# release itself publishes.
#
# Not upstream's install.sh: its checksum step greps the checksums file for
# the archive name, which also matches the `.sbom.json` line, so it compares
# the SBOM's hash against the tarball's and reports a mismatch on a download
# that is in fact genuine. An integrity check that fails for the wrong reason
# is no better than one that passes for the wrong reason. The `\$$` below
# anchors the match to the end of the line, which is the whole fix.
lint-install:
@os=$$(go env GOOS); arch=$$(go env GOARCH); v=$(GOLANGCI_VERSION); base=golangci-lint-$$v-$$os-$$arch; tmp=$$(mktemp -d); trap 'rm -rf "$$tmp"' EXIT; url=https://github.com/golangci/golangci-lint/releases/download/v$$v; echo "downloading $$base"; curl -sSfL "$$url/$$base.tar.gz" -o "$$tmp/$$base.tar.gz"; curl -sSfL "$$url/golangci-lint-$$v-checksums.txt" -o "$$tmp/sums.txt"; want=$$(grep " $$base\.tar\.gz$$" "$$tmp/sums.txt" | awk '{print $$1}'); got=$$(shasum -a 256 "$$tmp/$$base.tar.gz" | awk '{print $$1}'); if [ -z "$$want" ]; then echo "no checksum published for $$base"; exit 1; fi; if [ "$$want" != "$$got" ]; then echo "checksum mismatch for $$base"; echo " got $$got"; echo " want $$want"; exit 1; fi; echo "checksum verified"; tar xzf "$$tmp/$$base.tar.gz" -C "$$tmp"; install -m 0755 "$$tmp/$$base/golangci-lint" "$(GOLANGCI)"; "$(GOLANGCI)" version
# clients checks the two non-Go clients parse, and runs the one thing each can
# test without a device: how a failed tool call becomes an exception. They
# have no suite beyond that — what they do is verified against a device by
# hand. The error tests need no framework, only node and python3.
clients:
node --check clients/javascript/index.js
@python3 -c "import ast, pathlib, sys; \
[ast.parse(f.read_text(), str(f)) for f in pathlib.Path('clients/python').rglob('*.py')]" \
|| exit 1
@echo "clients parse"
@node clients/javascript/test/errors.test.mjs
@node clients/javascript/test/connection.test.mjs
@node clients/javascript/test/types.test.mjs
@python3 clients/python/tests/test_errors.py
@python3 clients/python/tests/test_connection.py
# crosscompile is the check that Windows and Linux still build. Windows has no
# daemon transport (see docs/WINDOWS.md) but everything must still compile for
# it, and that has broken before without anyone on a Mac noticing.
CROSS := windows/amd64 windows/arm64 linux/amd64 linux/arm64 darwin/amd64 darwin/arm64
# dist builds the release archives: every CROSS target, static and with no
# build paths in it, as mobium_<version>_<os>_<arch> — a .tar.gz, or a .zip
# for Windows — holding the binary, the license and its notices, and the
# README, with SHA256SUMS over them all. `make dist VERSION=0.1.0` for a
# release; the release workflow runs it on a tag.
DIST := dist
dist:
@rm -rf $(DIST) && mkdir -p $(DIST)
@for t in $(CROSS); do \
os=$${t%/*}; arch=$${t#*/}; ext=""; [ $$os = windows ] && ext=.exe; \
name=mobium_$(VERSION)_$${os}_$${arch}; dir=$(DIST)/$$name; mkdir -p $$dir; \
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch go build -trimpath \
-ldflags "-s -w -X main.version=$(VERSION)" -o $$dir/mobium$$ext ./cmd/mobium || exit 1; \
cp LICENSE NOTICE THIRD_PARTY_NOTICES.md README.md $$dir/; \
if [ $$os = windows ]; then (cd $(DIST) && zip -qr $$name.zip $$name) || exit 1; \
else tar -C $(DIST) -czf $(DIST)/$$name.tar.gz $$name || exit 1; fi; \
rm -rf $$dir; echo " $$name"; \
done
@cd $(DIST) && shasum -a 256 mobium_* > SHA256SUMS
@echo "wrote $(DIST)/SHA256SUMS"
# mcpb packs the MCP bundle and the MCP Registry entry from what make dist
# built: dist/mobium-$(VERSION).mcpb and dist/server.json. See packaging/mcp.
mcpb:
@python3 packaging/mcp/build.py $(VERSION) $(DIST)
crosscompile:
@for t in $(CROSS); do \
os=$${t%/*}; arch=$${t#*/}; \
GOOS=$$os GOARCH=$$arch go build -o /dev/null ./cmd/mobium || exit 1; \
(cd clients/go && GOOS=$$os GOARCH=$$arch go build ./...) || exit 1; \
echo " $$t"; \
done
@echo "cross-compiles"
# java builds, tests and packages the Java client the way it is published:
# through the Maven wrapper, which fetches a pinned, checksum-verified Maven
# on first use. `verify` compiles with -Xlint:all -Werror, runs the tests,
# builds the jar, sources and javadoc jars with javadoc's doclint on and
# warnings fatal, and fails if the client has acquired a dependency. Skipped
# with a note when there is no JDK, because most people working on the Go
# here will not have one and should not be blocked by it.
JAVA_HOME_GUESS := $(shell /usr/libexec/java_home 2>/dev/null || echo /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home)
JAVA := $(shell command -v java 2>/dev/null || echo $(JAVA_HOME_GUESS)/bin/java)
java:
@if [ ! -x "$(JAVA)" ]; then \
echo "no JDK found, skipping the Java client (set JAVA_HOME or install one)"; \
exit 0; \
fi; \
cd clients/java && ./mvnw -B --no-transfer-progress -q verify
# ci is everything that runs without a device. Keep this the single definition
# of that, so the workflow and a person checking before a commit cannot drift.
# api regenerates docs/API.md and docs/api/surface.json from the source. The
# table is 35 tools across five surfaces; maintained by hand it would be wrong
# within a week.
api:
@go run ./internal/apisurface/generate .
@go run ./internal/apisurface/outputschema . > /dev/null
@echo "api surface written"
# api-check fails if the generated files are out of date, so a tool added
# without regenerating them is caught in CI rather than read as truth later.
api-check:
@go run ./internal/apisurface/generate .
@go run ./internal/apisurface/outputschema . > /dev/null
@git diff --quiet -- docs/API.md docs/api/surface.json internal/agent/outputschema_gen.go || { \
echo "docs/API.md, docs/api/surface.json or internal/agent/outputschema_gen.go is stale — run 'make api' and commit the result"; \
git --no-pager diff --stat -- docs/API.md docs/api/surface.json internal/agent/outputschema_gen.go; \
exit 1; }
@echo "api surface current"
# The .NET client. Skips rather than fails when there is no SDK, matching the
# Java target: a machine doing Android work has no reason to have one.
#
# dotnet build, then the test binary. No test framework and no NuGet restore
# beyond the SDK's own packs, so this needs no network after the SDK is there.
DOTNET := $(shell command -v dotnet 2>/dev/null || echo $(HOME)/.dotnet/dotnet)
dotnet:
@if [ ! -x "$(DOTNET)" ]; then \
echo "no .NET SDK found, skipping the .NET client (install one, or see clients/dotnet/README.md)"; \
exit 0; \
fi; \
"$(DOTNET)" build clients/dotnet/Mobium/Mobium.csproj -v q --nologo && \
"$(DOTNET)" run --project clients/dotnet/Tests -v q --nologo
# The flag surface: what each tool accepts, and which command sets it. Written
# by the same generator as the API surface, because it is the same procedure
# one level down and a second generator would be a second thing to keep in step.
flags: api
# flags-check fails if the generated flag documents are out of date. Separate
# from api-check because they answer different questions and a failure should
# name which one drifted -- and because api-check diffs only its own two files,
# so a stale FLAGS.md would otherwise sail through.
flags-check:
@go run ./internal/apisurface/generate .
@git diff --quiet -- docs/FLAGS.md docs/api/flags.json || { \
echo "docs/FLAGS.md or docs/api/flags.json is stale — run 'make flags' and commit the result"; \
git --no-pager diff --stat -- docs/FLAGS.md docs/api/flags.json; \
exit 1; }
@echo "flag surface current"
# The documentation checks. Both existed for months and neither was wired to
# anything: the spelling sweep was never invoked by the Makefile or by any
# workflow, so `make ci` had never once checked spelling, and it silently
# scanned nothing when run with no arguments. Both tools now refuse an empty
# file list, and both are passed every tracked file rather than a hand-written
# list of extensions -- the list was the part that was wrong, having omitted
# '*.sh' while a British spelling sat in one of the check scripts. The word
# itself is deliberately not written here: this target scans the Makefile too,
# and a tool whose input vocabulary is the thing it removes will consume its
# own documentation. See CHALLENGES, "a spelling sweep that ate its own
# documentation".
# quickstart builds the per-client quick-start pages from their examples and
# the output those examples printed on a real emulator and simulator, so a page
# cannot show code that did not run. docs-check fails if one is out of date.
quickstart:
@python3 docs/quickstart/build.py
# license-check fails if a client's copy of LICENSE or NOTICE differs from the
# root's. Python, npm and Go package a client from its own folder, so each
# carries copies — the Apache License asks for itself and the NOTICE in every
# redistribution — and a copy is only worth having while it is the same text.
LICENSE_COPIES := clients/python/LICENSE clients/javascript/LICENSE clients/go/LICENSE
NOTICE_COPIES := clients/python/NOTICE clients/javascript/NOTICE clients/go/NOTICE
license-check:
@for f in $(LICENSE_COPIES); do \
cmp -s LICENSE $$f || { echo "$$f differs from LICENSE: cp LICENSE $$f"; exit 1; }; \
done
@for f in $(NOTICE_COPIES); do \
cmp -s NOTICE $$f || { echo "$$f differs from NOTICE: cp NOTICE $$f"; exit 1; }; \
done
@echo "license and notice copies match"
docs-check:
@python3 docs/checks/american-spelling.py scan $$(git ls-files) >/dev/null \
|| { python3 docs/checks/american-spelling.py scan $$(git ls-files); exit 1; }
@python3 docs/checks/doc-links.py $$(git ls-files '*.md') >/dev/null \
|| { python3 docs/checks/doc-links.py $$(git ls-files '*.md'); exit 1; }
@python3 docs/quickstart/build.py --check >/dev/null \
|| { python3 docs/quickstart/build.py --check; exit 1; }
@sh docs/checks/lib-selftest.sh >/dev/null || { sh docs/checks/lib-selftest.sh; exit 1; }
@echo "docs: spelling, anchors, quick-start pages and the checks' prelude clean"
ci: fmt-check vet lint test clients crosscompile java api-check flags-check license-check docs-check dotnet
clean:
rm -rf bin clients/java/target