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
|
plugins {
id 'com.android.application'
}
import java.security.MessageDigest
android {
namespace 'invalid.lena.scrcpy'
compileSdk 35
// Pin build-tools explicitly so the aapt2/zipalign/d8 toolchain is
// fixed and builds stay reproducible, not whatever AGP defaults to.
// 35.0.0 is the compileSdk's.
buildToolsVersion '35.0.0'
defaultConfig {
// The published package id. Immutable across releases.
applicationId 'invalid.lena.scrcpy'
minSdk 31
targetSdk 35
versionCode 3
versionName '0.3'
}
// AGP otherwise embeds a Google-signed dependency-metadata blob in the
// APK that is not byte-reproducible. Strip it so the build stays
// deterministic. This is the single most common reproducible-build
// breaker on AGP.
dependenciesInfo {
includeInApk false
includeInBundle false
}
// Release signing pulls credentials from the environment. Set all
// four of KEYSTORE_PATH / KEYSTORE_PASS / KEY_ALIAS / KEY_PASS and
// assembleRelease produces a signed APK. With any of them missing
// the release build is unsigned (Gradle's default); the same
// all-four condition gates the buildTypes wiring below so a
// half-set environment cannot select a half-populated config.
def signingReady = ['KEYSTORE_PATH', 'KEYSTORE_PASS', 'KEY_ALIAS', 'KEY_PASS']
.every { System.getenv(it) }
signingConfigs {
release {
if (signingReady) {
storeFile file(System.getenv('KEYSTORE_PATH'))
storePassword System.getenv('KEYSTORE_PASS')
keyAlias System.getenv('KEY_ALIAS')
keyPassword System.getenv('KEY_PASS')
}
}
}
// Native ABI policy: release ships arm64-v8a only. Every targeted
// source device is 64-bit ARM in practice; 32-bit-only devices are
// not supported. The test rig uses an x86_64 emulator, so debug keeps
// x86_64 (and arm64) so ./test e2e still works.
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-debug'
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
release {
ndk {
abiFilters 'arm64-v8a'
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
if (signingReady) {
signingConfig signingConfigs.release
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
packaging {
// bouncycastle (transitive via :adb) is the dominant source of
// dead weight in the APK. Each entry below is dead code/data for
// our use case (we only call BC's asn1, crypto, util.encoders):
// - picnic post-quantum lookup tables: ~1.2 MB of .properties
// - cert-path-reviewer i18n messages: ~92 KB, en+de only
// - kotlin metadata: bleed from spake2-android (Kotlin lib);
// our code is pure Java and doesn't reflect on .kotlin_builtins
// - androidx annotation LICENSE.txt: 10 KB blob, no runtime use
// - duplicate META-INF licenses/notices: ordinary AGP cleanup
resources.excludes += [
'META-INF/LICENSE*',
'META-INF/NOTICE*',
'META-INF/versions/9/OSGI-INF/MANIFEST.MF',
'META-INF/androidx/**',
'META-INF/kotlin-stdlib*',
'org/bouncycastle/pqc/crypto/picnic/**',
'org/bouncycastle/pqc/legacy/picnic/**',
'org/bouncycastle/x509/CertPathReviewerMessages*.properties',
'kotlin/**',
]
}
testOptions {
// android.util.Log and friends are stubs on the JVM unit-test
// classpath; let them return defaults instead of throwing.
unitTests.returnDefaultValues = true
}
sourceSets {
main.assets.srcDir '../vendor/libadb-android/LICENSES'
}
}
dependencies {
implementation project(':adb')
// bcprov is already pulled in transitively by :adb at runtime; we
// need it on the compile classpath too for Adb.java's ASN.1
// cert-builder. :adb keeps it 'implementation'-scoped upstream so
// we declare it explicitly here rather than patch the vendor tree.
implementation 'org.bouncycastle:bcprov-jdk15to18:1.84'
// Bundled Conscrypt. libadb-android's TLS pairing exports keying
// material via Conscrypt. Without a bundled copy it reflects into the
// platform's hidden com.android.org.conscrypt.Conscrypt, which under
// targetSdk 35 is invisible to reflection (getMethod throws
// NoSuchMethodException and pairing fails on real devices). Bundling a
// standalone Conscrypt flips libadb onto the public
// org.conscrypt.Conscrypt API, which is not restricted. Ships a native
// .so (filtered to arm64-v8a in release).
implementation 'org.conscrypt:conscrypt-android:2.5.2'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20240303'
}
def serverJar = file('src/main/assets/scrcpy-server.jar')
def serverSum = file('src/main/assets/scrcpy-server.sha256')
def serverVersion = file('src/main/assets/scrcpy-server.version')
def expectedServerSum = providers.gradleProperty('scrcpyServerSha256')
.orElse(providers.provider { serverSum.text.trim() })
tasks.register('verifyScrcpyServer') {
inputs.files(serverJar, serverSum, serverVersion)
inputs.property('expectedChecksum', expectedServerSum)
doLast {
if (!serverJar.isFile() || !serverSum.isFile() || !serverVersion.isFile()) {
throw new GradleException('scrcpy server assets are incomplete; run scripts/update-server')
}
def expected = expectedServerSum.get()
def version = serverVersion.text.trim()
if (!(expected ==~ /[0-9a-f]{64}/) || version.isEmpty()) {
throw new GradleException('scrcpy server checksum or version is invalid')
}
def digest = MessageDigest.getInstance('SHA-256')
serverJar.withInputStream { input ->
byte[] buffer = new byte[64 * 1024]
for (int n; (n = input.read(buffer)) >= 0; ) {
if (n > 0) digest.update(buffer, 0, n)
}
}
def actual = digest.digest().encodeHex().toString()
if (actual != expected) {
throw new GradleException("scrcpy server checksum mismatch: expected ${expected}, got ${actual}")
}
}
}
tasks.configureEach { task ->
if (task.name.startsWith('merge') && task.name.endsWith('Assets')) {
task.dependsOn tasks.named('verifyScrcpyServer')
}
}
|