Skip to content

Commit bf763c1

Browse files
Enforce 16KB native alignment checks on Android builds
1 parent 5a2abde commit bf763c1

2 files changed

Lines changed: 182 additions & 1 deletion

File tree

app/build.gradle.kts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import java.io.ByteArrayOutputStream
2+
13
plugins {
24
id("com.android.application")
35
id("org.jetbrains.kotlin.android")
@@ -29,6 +31,7 @@ val isReleaseTaskRequested = gradle.startParameter.taskNames.any { task ->
2931
}
3032

3133
val missingReleaseSigningEnvText = missingReleaseSigningEnv.joinToString(separator = ", ")
34+
val supportedAbis = listOf("arm64-v8a", "x86_64")
3235

3336
android {
3437
namespace = "com.google.ai.sample"
@@ -41,7 +44,7 @@ android {
4144
versionCode = 1
4245
versionName = "1.0"
4346
ndk {
44-
abiFilters += listOf("arm64-v8a", "x86_64")
47+
abiFilters += supportedAbis
4548
}
4649

4750
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -93,6 +96,92 @@ android {
9396
composeOptions {
9497
kotlinCompilerExtensionVersion = "1.5.4"
9598
}
99+
packaging {
100+
jniLibs {
101+
useLegacyPackaging = false
102+
}
103+
}
104+
}
105+
106+
fun parseLoadAlignments(readelfOutput: String): List<Long> {
107+
val lines = readelfOutput.lineSequence().toList()
108+
val alignments = mutableListOf<Long>()
109+
for (index in 0 until lines.lastIndex) {
110+
if (!lines[index].trimStart().startsWith("LOAD")) continue
111+
val alignToken = lines[index + 1].trim().split(Regex("\\s+")).lastOrNull() ?: continue
112+
val alignValue = alignToken.removePrefix("0x").toLongOrNull(16) ?: continue
113+
alignments += alignValue
114+
}
115+
return alignments
116+
}
117+
118+
androidComponents {
119+
onVariants(selector().all()) { variant ->
120+
val variantName = variant.name
121+
val variantNameCap = variantName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
122+
val mergeNativeTaskName = "merge${variantNameCap}NativeLibs"
123+
val verifyTaskName = "verify${variantNameCap}Native16KbAlignment"
124+
125+
val verifyTask = tasks.register(verifyTaskName) {
126+
group = "verification"
127+
description = "Verifies that all merged native libs for $variantName use at least 16KB PT_LOAD alignment."
128+
dependsOn(mergeNativeTaskName)
129+
130+
doLast {
131+
val nativeOutDir = layout.buildDirectory
132+
.dir("intermediates/merged_native_libs/$variantName/$mergeNativeTaskName/out/lib")
133+
.get()
134+
.asFile
135+
136+
if (!nativeOutDir.exists()) {
137+
throw GradleException("Native lib output directory not found: ${nativeOutDir.absolutePath}")
138+
}
139+
140+
val soFiles = nativeOutDir.walkTopDown().filter { it.isFile && it.extension == "so" }.toList()
141+
val filteredSoFiles = soFiles.filter { soFile ->
142+
val abiDir = soFile.parentFile?.name
143+
abiDir in supportedAbis
144+
}
145+
if (filteredSoFiles.isEmpty()) {
146+
logger.lifecycle("No native .so files found under ${nativeOutDir.absolutePath} for variant $variantName.")
147+
return@doLast
148+
}
149+
150+
val invalidLibraries = mutableListOf<String>()
151+
filteredSoFiles.forEach { soFile ->
152+
val stdout = ByteArrayOutputStream()
153+
val execResult = exec {
154+
commandLine("readelf", "-l", soFile.absolutePath)
155+
standardOutput = stdout
156+
isIgnoreExitValue = false
157+
}
158+
if (execResult.exitValue != 0) {
159+
throw GradleException("readelf failed for ${soFile.absolutePath}")
160+
}
161+
162+
val alignments = parseLoadAlignments(stdout.toString())
163+
if (alignments.isEmpty() || alignments.any { it < 0x4000L }) {
164+
val relativePath = soFile.relativeTo(nativeOutDir).path
165+
val shownAlignments = if (alignments.isEmpty()) "none" else alignments.joinToString(", ") { "0x${it.toString(16)}" }
166+
invalidLibraries += "$relativePath (PT_LOAD alignments: $shownAlignments)"
167+
}
168+
}
169+
170+
if (invalidLibraries.isNotEmpty()) {
171+
throw GradleException(
172+
"Found native libraries without required 16KB alignment in variant '$variantName':\n" +
173+
invalidLibraries.joinToString("\n")
174+
)
175+
}
176+
}
177+
}
178+
179+
tasks.configureEach {
180+
if (name == "assemble$variantNameCap") {
181+
dependsOn(verifyTask)
182+
}
183+
}
184+
}
96185
}
97186

98187
if (isReleaseTaskRequested && missingReleaseSigningEnv.isNotEmpty()) {

humanoperator/build.gradle.kts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import java.io.ByteArrayOutputStream
2+
13
plugins {
24
id("com.android.application")
35
id("org.jetbrains.kotlin.android")
@@ -20,6 +22,7 @@ val isReleaseTaskRequested = gradle.startParameter.taskNames.any { task ->
2022
}
2123

2224
val missingReleaseSigningEnvText = missingReleaseSigningEnv.joinToString(separator = ", ")
25+
val supportedAbis = listOf("arm64-v8a", "x86_64")
2326

2427
android {
2528
namespace = "com.screenoperator.humanoperator"
@@ -31,6 +34,9 @@ android {
3134
targetSdk = 35
3235
versionCode = 1
3336
versionName = "1.0"
37+
ndk {
38+
abiFilters += supportedAbis
39+
}
3440

3541
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3642
vectorDrawables {
@@ -70,6 +76,92 @@ android {
7076
composeOptions {
7177
kotlinCompilerExtensionVersion = "1.5.4"
7278
}
79+
packaging {
80+
jniLibs {
81+
useLegacyPackaging = false
82+
}
83+
}
84+
}
85+
86+
fun parseLoadAlignments(readelfOutput: String): List<Long> {
87+
val lines = readelfOutput.lineSequence().toList()
88+
val alignments = mutableListOf<Long>()
89+
for (index in 0 until lines.lastIndex) {
90+
if (!lines[index].trimStart().startsWith("LOAD")) continue
91+
val alignToken = lines[index + 1].trim().split(Regex("\\s+")).lastOrNull() ?: continue
92+
val alignValue = alignToken.removePrefix("0x").toLongOrNull(16) ?: continue
93+
alignments += alignValue
94+
}
95+
return alignments
96+
}
97+
98+
androidComponents {
99+
onVariants(selector().all()) { variant ->
100+
val variantName = variant.name
101+
val variantNameCap = variantName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
102+
val mergeNativeTaskName = "merge${variantNameCap}NativeLibs"
103+
val verifyTaskName = "verify${variantNameCap}Native16KbAlignment"
104+
105+
val verifyTask = tasks.register(verifyTaskName) {
106+
group = "verification"
107+
description = "Verifies that all merged native libs for $variantName use at least 16KB PT_LOAD alignment."
108+
dependsOn(mergeNativeTaskName)
109+
110+
doLast {
111+
val nativeOutDir = layout.buildDirectory
112+
.dir("intermediates/merged_native_libs/$variantName/$mergeNativeTaskName/out/lib")
113+
.get()
114+
.asFile
115+
116+
if (!nativeOutDir.exists()) {
117+
throw GradleException("Native lib output directory not found: ${nativeOutDir.absolutePath}")
118+
}
119+
120+
val soFiles = nativeOutDir.walkTopDown().filter { it.isFile && it.extension == "so" }.toList()
121+
val filteredSoFiles = soFiles.filter { soFile ->
122+
val abiDir = soFile.parentFile?.name
123+
abiDir in supportedAbis
124+
}
125+
if (filteredSoFiles.isEmpty()) {
126+
logger.lifecycle("No native .so files found under ${nativeOutDir.absolutePath} for variant $variantName.")
127+
return@doLast
128+
}
129+
130+
val invalidLibraries = mutableListOf<String>()
131+
filteredSoFiles.forEach { soFile ->
132+
val stdout = ByteArrayOutputStream()
133+
val execResult = exec {
134+
commandLine("readelf", "-l", soFile.absolutePath)
135+
standardOutput = stdout
136+
isIgnoreExitValue = false
137+
}
138+
if (execResult.exitValue != 0) {
139+
throw GradleException("readelf failed for ${soFile.absolutePath}")
140+
}
141+
142+
val alignments = parseLoadAlignments(stdout.toString())
143+
if (alignments.isEmpty() || alignments.any { it < 0x4000L }) {
144+
val relativePath = soFile.relativeTo(nativeOutDir).path
145+
val shownAlignments = if (alignments.isEmpty()) "none" else alignments.joinToString(", ") { "0x${it.toString(16)}" }
146+
invalidLibraries += "$relativePath (PT_LOAD alignments: $shownAlignments)"
147+
}
148+
}
149+
150+
if (invalidLibraries.isNotEmpty()) {
151+
throw GradleException(
152+
"Found native libraries without required 16KB alignment in variant '$variantName':\n" +
153+
invalidLibraries.joinToString("\n")
154+
)
155+
}
156+
}
157+
}
158+
159+
tasks.configureEach {
160+
if (name == "assemble$variantNameCap") {
161+
dependsOn(verifyTask)
162+
}
163+
}
164+
}
73165
}
74166

75167
if (isReleaseTaskRequested && missingReleaseSigningEnv.isNotEmpty()) {

0 commit comments

Comments
 (0)