Skip to content

Commit c46a957

Browse files
committed
Merge branch 'main_upstream' into search_fix
# Conflicts: # android/src/main/java/com/tailscale/ipn/ui/view/SearchView.kt
2 parents 39e3594 + d911b89 commit c46a957

43 files changed

Lines changed: 2175 additions & 590 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Integration Test
2+
3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
5+
cancel-in-progress: true
6+
7+
on:
8+
workflow_dispatch:
9+
pull_request:
10+
types: [labeled, synchronize, reopened]
11+
12+
jobs:
13+
integration-test:
14+
if: |
15+
github.event_name == 'workflow_dispatch' ||
16+
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-integration-test'))
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 90
19+
20+
steps:
21+
- name: Check out code
22+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23+
24+
- name: Enable KVM
25+
run: |
26+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
27+
sudo udevadm control --reload-rules
28+
sudo udevadm trigger --name-match=kvm
29+
ls -l /dev/kvm
30+
31+
- name: Run integration test
32+
run: make android-integration-test

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ android/local.properties
2626
tailscale.jks
2727

2828
# android sdk dir
29-
./android-sdk
29+
/android-sdk/
3030

3131
# Persistent $HOME/.android for `make docker-*` (keeps debug.keystore so
3232
# the debug signer is stable across container runs).
3333
.android-docker
34+
.android-integration-docker
35+
.cache-docker
36+
.gradle-docker
3437

3538
# Java profiling output
3639
*.hprof
@@ -51,3 +54,6 @@ libtailscale-sources.jar
5154
.DS_Store
5255

5356
tailscale.version
57+
58+
# local tmp folder
59+
.tmp/

Makefile

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
# with this name, it will be used.
1111
#
1212
# The convention here is tailscale-android-build-amd64-<date>
13-
DOCKER_IMAGE := tailscale-android-build-amd64-041425-1
13+
DOCKER_IMAGE := tailscale-android-build-amd64-072226-3
14+
15+
# The integration test image contains the Android emulator, system image, SDK,
16+
# build-tools, NDK, adb, and helper tools needed to run the emulator-backed Go
17+
# integration tests. Bump this tag when docker/Dockerfile.android-integration
18+
# or the required tool versions change, using:
19+
# tailscale-android-integration-amd64-YYYYMMDD-N
20+
ANDROID_INTEGRATION_DOCKER_IMAGE := tailscale-android-integration-amd64-20260609-1
1421
export TS_USE_TOOLCHAIN=1
1522

1623
# If set, additional comma-separated build tags passed to the libtailscale Go
@@ -20,6 +27,15 @@ export TS_USE_TOOLCHAIN=1
2027
# affordances for debugging it.
2128
GOMOBILE_BUILD_TAGS := ts_omit_cachenetmap
2229

30+
# Pull androidApiLevel from gradle.properties.
31+
ANDROID_API_LEVEL := $(shell grep '^androidApiLevel=' android/gradle.properties | cut -d'=' -f2)
32+
33+
ifeq ($(ANDROID_API_LEVEL),)
34+
$(error androidApiLevel missing from android/gradle.properties)
35+
endif
36+
37+
ANDROID_BUILD_TOOLS_VERSION := $(shell grep '^androidBuildToolsVersion=' android/gradle.properties | cut -d'=' -f2)
38+
2339
DEBUG_APK := tailscale-debug.apk
2440
RELEASE_AAB := tailscale-release.aab
2541
RELEASE_TV_AAB := tailscale-tv-release.aab
@@ -40,7 +56,7 @@ else
4056
ANDROID_TOOLS_URL := "https://dl.google.com/android/repository/commandlinetools-mac-9477386_latest.zip"
4157
ANDROID_TOOLS_SUM := "2072ffce4f54cdc0e6d2074d2f381e7e579b7d63e915c220b96a7db95b2900ee commandlinetools-mac-9477386_latest.zip"
4258
endif
43-
ANDROID_SDK_PACKAGES := 'platforms;android-34' 'extras;android;m2repository' 'ndk;23.1.7779620' 'platform-tools' 'build-tools;34.0.0'
59+
ANDROID_SDK_PACKAGES := 'platforms;android-$(ANDROID_API_LEVEL)' 'extras;android;m2repository' 'ndk;23.1.7779620' 'platform-tools' 'build-tools;$(ANDROID_BUILD_TOOLS_VERSION)'
4460

4561
# Attempt to find an ANDROID_SDK_ROOT / ANDROID_HOME based either from
4662
# preexisting environment or common locations.
@@ -84,7 +100,7 @@ else
84100
export PATH := $(JAVA_HOME)/bin:$(PATH)
85101
endif
86102

87-
AVD_BASE_IMAGE := "system-images;android-33;google_apis;"
103+
AVD_BASE_IMAGE := 'system-images;android-$(ANDROID_API_LEVEL);google_apis;'
88104
export HOST_ARCH := $(shell uname -m)
89105
ifeq ($(HOST_ARCH),aarch64)
90106
AVD_IMAGE := "$(AVD_BASE_IMAGE)arm64-v8a"
@@ -142,23 +158,35 @@ release-tv: jarsign-env $(RELEASE_TV_AAB)
142158

143159
# gradle-dependencies groups together the android sources and libtailscale needed to assemble tests/debug/release builds.
144160
.PHONY: gradle-dependencies
145-
gradle-dependencies: $(shell find android -type f -not -path "android/build/*" -not -path '*/.*') $(LIBTAILSCALE_AAR) tailscale.version
161+
gradle-dependencies: $(shell find android -type f -not -path "android/build/*" -not -path "android/libs/*" -not -path '*/.*') $(LIBTAILSCALE_AAR) tailscale.version
146162

147163
$(RELEASE_AAB): version gradle-dependencies
148164
@echo "Building release AAB"
149165
(cd android && ./gradlew test bundleRelease)
150166
install -C ./android/build/outputs/bundle/release/android-release.aab $@
151167

168+
# PLATFORM=tv signals to gradle that we should build for AndroidTV. To
169+
# distinguish the TV variant from the phone/tablet build in the Play Store,
170+
# we temporarily increment the versionCode in android/build.gradle by 1 for
171+
# the duration of the build, then restore the original value via a shell trap
172+
# so the working tree is left clean even if the gradle build fails.
152173
$(RELEASE_TV_AAB): version gradle-dependencies
153174
@echo "Building TV release AAB"
154-
(cd android && ./gradlew test bundleRelease_tv)
155-
install -C ./android/build/outputs/bundle/release_tv/android-release_tv.aab $@
175+
@set -e; \
176+
ORIG_VC=$$(grep -oE 'versionCode [0-9]+' android/build.gradle | awk '{print $$2}'); \
177+
TV_VC=$$((ORIG_VC + 1)); \
178+
echo "TV versionCode: $$ORIG_VC -> $$TV_VC"; \
179+
trap "sed -i.bak -E 's/versionCode [0-9]+/versionCode $$ORIG_VC/' android/build.gradle && rm -f android/build.gradle.bak" EXIT INT TERM HUP; \
180+
sed -i.bak -E "s/versionCode [0-9]+/versionCode $$TV_VC/" android/build.gradle; \
181+
rm -f android/build.gradle.bak; \
182+
(cd android && ./gradlew test bundleRelease -PPLATFORM=tv)
183+
install -C ./android/build/outputs/bundle/release/android-release.aab $@
156184

157185
tailscale-test.apk: version gradle-dependencies
158186
(cd android && ./gradlew assembleApplicationTestAndroidTest)
159187
install -C ./android/build/outputs/apk/androidTest/applicationTest/android-applicationTest-androidTest.apk $@
160188

161-
tailscale.version: go.mod go.sum $(wildcard .git/HEAD)
189+
tailscale.version: go.mod go.sum go.toolchain.rev $(wildcard .git/HEAD)
162190
@bash -c "./tool/go run tailscale.com/cmd/mkversion > tailscale.version"
163191

164192
.PHONY: version
@@ -175,10 +203,10 @@ android/libs:
175203
$(GOBIN):
176204
mkdir -p $(GOBIN)
177205

178-
$(GOBIN)/gomobile: $(GOBIN)/gobind go.mod go.sum | $(GOBIN)
206+
$(GOBIN)/gomobile: $(GOBIN)/gobind go.mod go.sum go.toolchain.rev | $(GOBIN)
179207
./tool/go install golang.org/x/mobile/cmd/gomobile
180208

181-
$(GOBIN)/gobind: go.mod go.sum
209+
$(GOBIN)/gobind: go.mod go.sum go.toolchain.rev
182210
./tool/go install golang.org/x/mobile/cmd/gobind
183211

184212
.PHONY: build-unstripped-aar
@@ -271,17 +299,26 @@ androidpath:
271299
@echo 'export PATH=$(ANDROID_HOME)/cmdline-tools/latest/bin:$(ANDROID_HOME)/platform-tools:$$PATH'
272300

273301
.PHONY: tag_release
274-
tag_release: debug-symbols tailscale.version ## Tag the current commit with the current version
275-
source tailscale.version && git tag -a "$${VERSION_LONG}" -m "OSS and Version updated to $${VERSION_LONG}"
302+
tag_release: tailscale.version bump-version-code ## Tag the current commit with the current version
303+
@if ! git diff --quiet -- android/build.gradle; then \
304+
source tailscale.version && git commit -sm "android: bump versionCode for $${VERSION_LONG}" android/build.gradle; \
305+
fi
306+
source tailscale.version && git tag -a "$${VERSION_LONG}" -m "Version updated to $${VERSION_LONG}"
276307

277308
.PHONY: bumposs ## Bump to the latest oss and update the versions.
278-
bumposs: update-oss tailscale.version
309+
bumposs: update-oss tailscale.version bump-version-code
279310
source tailscale.version && git commit -sm "android: bump OSS" -m "OSS and Version updated to $${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum
280311
source tailscale.version && git tag -a "$${VERSION_LONG}" -m "OSS and Version updated to $${VERSION_LONG}"
281312

282-
.PHONY: bump_version_code ## Bump the version code in build.gradle
283-
bump_version_code:
284-
sed -i'.bak' "s/versionCode .*/versionCode $$(expr $$(awk '/versionCode ([0-9]+)/{print $$2}' android/build.gradle) + 1)/" android/build.gradle && rm android/build.gradle.bak
313+
# Recomputes the base versionCode from tailscale.version and rewrites the
314+
# `versionCode <n>` line in android/build.gradled
315+
.PHONY: bump-version-code
316+
bump-version-code: tailscale.version
317+
@source tailscale.version && \
318+
BASE_VERSION_CODE=$$((VERSION_MAJOR * 100000000 + VERSION_MINOR * 100000 + VERSION_PATCH * 10)) && \
319+
echo "Setting android/build.gradle versionCode to $$BASE_VERSION_CODE" && \
320+
sed -i.bak -E "s/versionCode [0-9]+/versionCode $$BASE_VERSION_CODE/" android/build.gradle && \
321+
rm android/build.gradle.bak
285322

286323
.PHONY: update-oss ## Update the tailscale.com go module
287324
update-oss:
@@ -365,6 +402,14 @@ docker-build-image: ## Builds the docker image for the android build environment
365402
docker build -f docker/DockerFile.amd64-build -t $(DOCKER_IMAGE) .; \
366403
fi
367404

405+
.PHONY: docker-build-android-integration-image
406+
docker-build-android-integration-image: ## Build the Docker image used to run Android integration tests
407+
@echo "Checking if docker image $(ANDROID_INTEGRATION_DOCKER_IMAGE) already exists..."
408+
@if ! docker images $(ANDROID_INTEGRATION_DOCKER_IMAGE) -q | grep -q . ; then \
409+
echo "Image does not exist. Building..."; \
410+
docker build -f docker/Dockerfile.android-integration -t $(ANDROID_INTEGRATION_DOCKER_IMAGE) .; \
411+
fi
412+
368413
# DOCKER_ANDROID_DIR is bind-mounted as /root/.android inside the container
369414
# so the Gradle-generated debug keystore (and anything else under ~/.android)
370415
# persists across docker runs. Without this, every docker-based debug build
@@ -373,12 +418,19 @@ docker-build-image: ## Builds the docker image for the android build environment
373418
# JVM's user.home resolves to /root for the container's root user, regardless
374419
# of the Dockerfile's HOME=/build env.
375420
DOCKER_ANDROID_DIR := $(CURDIR)/.android-docker
421+
DOCKER_ANDROID_INTEGRATION_DIR := $(CURDIR)/.android-integration-docker
422+
DOCKER_GRADLE_DIR := $(CURDIR)/.gradle-docker
423+
DOCKER_GO_CACHE_DIR := $(CURDIR)/.cache-docker
376424

377425
.PHONY: docker-android-dir
378426
docker-android-dir:
379-
@mkdir -p $(DOCKER_ANDROID_DIR)
427+
@mkdir -p $(DOCKER_ANDROID_DIR) $(DOCKER_GRADLE_DIR) $(DOCKER_GO_CACHE_DIR)
428+
429+
.PHONY: docker-android-integration-dir
430+
docker-android-integration-dir:
431+
@mkdir -p $(DOCKER_ANDROID_INTEGRATION_DIR) $(DOCKER_GO_CACHE_DIR)
380432

381-
DOCKER_RUN_VOLS := -v $(CURDIR):/build/tailscale-android -v $(DOCKER_ANDROID_DIR):/root/.android
433+
DOCKER_RUN_VOLS := -v $(CURDIR):/build/tailscale-android -v $(DOCKER_ANDROID_DIR):/root/.android -v $(DOCKER_GRADLE_DIR):/build/.gradle -v $(DOCKER_GO_CACHE_DIR):/build/.cache --env GOPATH=/build/.cache/go --env GOMODCACHE=/build/.cache/go/pkg/mod
382434

383435
.PHONY: docker-run-build
384436
docker-run-build: clean jarsign-env docker-build-image docker-android-dir ## Runs the docker image for the android build environment and builds release
@@ -388,6 +440,21 @@ docker-run-build: clean jarsign-env docker-build-image docker-android-dir ## Run
388440
docker-tailscale-debug: docker-build-image docker-android-dir ## Build tailscale-debug.apk inside the docker env (stable signer across runs)
389441
@docker run --rm $(DOCKER_RUN_VOLS) $(DOCKER_IMAGE) make tailscale-debug
390442

443+
.PHONY: android-integration-test
444+
android-integration-test: docker-tailscale-debug android-integration-test-run ## Build APK and run adb-backed Android integration tests in Docker
445+
446+
.PHONY: android-integration-test-run
447+
android-integration-test-run: docker-build-android-integration-image docker-android-integration-dir ## Run adb-backed Android integration tests in Docker using existing APK
448+
@docker run --rm --device /dev/kvm \
449+
-v $(CURDIR):/workspace \
450+
-v $(DOCKER_ANDROID_INTEGRATION_DIR):/root/.android \
451+
-v $(DOCKER_GO_CACHE_DIR):/root/.cache \
452+
--env GOPATH=/root/.cache/go \
453+
--env GOMODCACHE=/root/.cache/go/pkg/mod \
454+
-w /workspace \
455+
$(ANDROID_INTEGRATION_DOCKER_IMAGE) \
456+
/usr/local/bin/run-android-integration-test /workspace/$(DEBUG_APK)
457+
391458
.PHONY: docker-remove-build-image
392459
docker-remove-build-image: ## Removes the current docker build image
393460
docker rmi --force $(DOCKER_IMAGE)

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,37 @@ alias nix='nix --extra-experimental-features "nix-command flakes"'
9292
nix develop
9393
```
9494

95+
The flake provides host tools such as Java, `make`, `curl`, and `git`, and
96+
points the build at a repo-local Android SDK in `./android-sdk`. The SDK
97+
directory is ignored by Git and is reused across builds.
98+
99+
On first use, install the Android SDK components:
100+
101+
```sh
102+
make androidsdk
103+
```
104+
105+
Then build normally:
106+
107+
```sh
108+
make tailscale-debug
109+
```
110+
111+
The debug APK is written to `./tailscale-debug.apk`.
112+
113+
For one-shot commands without entering an interactive shell:
114+
115+
```sh
116+
nix develop --command make androidsdk
117+
nix develop --command make tailscale-debug
118+
```
119+
120+
For faster Kotlin-only iteration while avoiding the `gomobile bind` step:
121+
122+
```sh
123+
nix develop --command bash -lc 'cd android && ./gradlew ktfmtCheck compileDebugKotlin'
124+
```
125+
95126
## Building
96127

97128
```sh

android/build.gradle

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ buildscript {
1111
}
1212
}
1313
dependencies {
14-
classpath 'com.android.tools.build:gradle:8.6.1'
14+
classpath 'com.android.tools.build:gradle:8.13.0'
1515
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1616
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
1717
classpath("com.ncorti.ktfmt.gradle:plugin:0.17.0")
@@ -32,12 +32,18 @@ apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
3232
apply plugin: 'com.ncorti.ktfmt.gradle'
3333

3434
android {
35+
def androidApiLevel = providers.gradleProperty("androidApiLevel").get().toInteger()
36+
3537
ndkVersion "23.1.7779620"
36-
compileSdkVersion 34
38+
compileSdkVersion androidApiLevel
3739
defaultConfig {
3840
minSdkVersion 26
39-
targetSdkVersion 35
40-
versionCode 468
41+
targetSdkVersion androidApiLevel
42+
// versionCode is written here by `make bumposs` / `make tag_release`
43+
// (base = major*100000000 + minor*100000 + patch*10). AndroidTV builds
44+
// increment this by 1 in the Makefile's release-tv target so the two
45+
// variants are distinguishable in the Play Store.
46+
versionCode 110102910
4147
versionName getVersionProperty("VERSION_LONG")
4248
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
4349

@@ -79,16 +85,16 @@ android {
7985
buildTypes {
8086
applicationTest {
8187
initWith debug
82-
manifestPlaceholders.leanbackRequired = false
88+
manifestPlaceholders.leanbackRequired = isTV()
8389
buildConfigField "String", "GITHUB_USERNAME", "\"" + getLocalProperty("githubUsername", "")+"\""
8490
buildConfigField "String", "GITHUB_PASSWORD", "\"" + getLocalProperty("githubPassword", "")+"\""
8591
buildConfigField "String", "GITHUB_2FA_SECRET", "\"" + getLocalProperty("github2FASecret", "")+"\""
8692
}
8793
debug {
88-
manifestPlaceholders.leanbackRequired = false
94+
manifestPlaceholders.leanbackRequired = isTV()
8995
}
9096
release {
91-
manifestPlaceholders.leanbackRequired = false
97+
manifestPlaceholders.leanbackRequired = isTV()
9298

9399
minifyEnabled true
94100

@@ -98,10 +104,6 @@ android {
98104
'proguard-android-optimize.txt'),
99105
'proguard-rules.pro'
100106
}
101-
release_tv {
102-
initWith release
103-
manifestPlaceholders.leanbackRequired = true
104-
}
105107
}
106108

107109
testBuildType "applicationTest"
@@ -114,6 +116,7 @@ dependencies {
114116
implementation "androidx.browser:browser:1.8.0"
115117
implementation "androidx.security:security-crypto:1.1.0-alpha06"
116118
implementation "androidx.work:work-runtime:2.9.1"
119+
implementation "androidx.work:work-runtime-ktx:2.9.1"
117120

118121
// Kotlin dependencies.
119122
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
@@ -170,6 +173,7 @@ dependencies {
170173
testImplementation 'org.mockito:mockito-core:5.12.0'
171174
testImplementation 'org.mockito:mockito-inline:5.2.0'
172175
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0'
176+
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
173177

174178
debugImplementation("androidx.compose.ui:ui-tooling")
175179
implementation("androidx.compose.ui:ui-tooling-preview")
@@ -194,3 +198,7 @@ def getVersionProperty(key) {
194198
versionProps.load(project.file('../tailscale.version').newDataInputStream())
195199
return versionProps.getProperty(key).replaceAll('^\"|\"$', '')
196200
}
201+
202+
def isTV() {
203+
return project.findProperty('PLATFORM')?.toString() == 'tv'
204+
}

android/gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
androidApiLevel=36
2+
androidBuildToolsVersion=36.0.0
13
android.defaults.buildfeatures.buildconfig=true
24
android.nonFinalResIds=false
35
android.nonTransitiveRClass=true
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
4-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
3+
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)