Summary
yukihookapi-ksp-xposed writes its assets/xposed_init and resources/META-INF/yukihookapi_init outputs directly into the source tree with File.writeText() instead of going through codeGenerator.createNewFile(). This bypasses Gradle/KSP's task wiring, so on a fresh checkout (CI) AGP's merge{Variant}Assets and merge{Variant}JavaResource tasks can run before ksp{Variant}Kotlin writes the files — and the published APK ships without assets/xposed_init. LSPosed (especially Vector / older flavors that strictly rely on assets/xposed_init) then can't find the entry-class pointer and the Manager's enable toggle silently no-ops, even though the module shows up in the list because its manifest meta-data is read independently.
Locally this is invisible: previous builds leave the files in app/src/main/assets/xposed_init and app/src/main/resources/META-INF/yukihookapi_init. Subsequent assembleRelease runs always see them, so the race never manifests. Every CI build, however, is fresh.
Affected code
yukihookapi-ksp-xposed/src/api/kotlin/com/highcapable/yukihookapi/YukiHookXposedProcessor.kt#L208-L228:
private fun generateAssetsFile(codePath: String, sourcePath: String, data: GenerateData) = environment {
...
val assetsDir = projectDir.resolve(sourcePath).resolve("assets")
val metaInfDir = projectDir.resolve(sourcePath).resolve("resources").resolve("META-INF")
...
assetsDir.resolve("xposed_init").writeText(text = "${data.entryPackageName}.${data.xInitClassName}")
metaInfDir.resolve("yukihookapi_init").writeText(text = "${data.entryPackageName}.${data.entryClassName}")
...
}
projectDir.resolve(sourcePath) resolves to <module>/src/main/, i.e. into the source tree. Compare with generateClassFile right below, which correctly uses environment.codeGenerator.createNewFile(...) and lands under build/generated/ksp/<variant>/kotlin/.
Reproduction
Any module that uses yukihookapi:api:1.3.x + ksp 'com.highcapable.yukihookapi:ksp-xposed:1.3.1' and adds app/src/main/assets/xposed_init to .gitignore:
# Simulate a CI fresh checkout
rm -f app/src/main/assets/xposed_init \
app/src/main/resources/META-INF/yukihookapi_init
./gradlew :app:clean :app:assembleRelease
# Inspect packaged APK
unzip -l app/build/outputs/apk/release/app-release-unsigned.apk | grep -E "xposed|yuki"
Result:
40 META-INF/yukihookapi_init
Note the missing assets/xposed_init. Do not run a second assembleRelease — the file from the first failed build now lives in the source tree, masking the bug forever after.
Symptom on devices
End users hit this as: PixelMask shows up in LSPosed Manager's module list (with the correct icon/description, since xposedmodule etc. meta-data is in the manifest), but the enable toggle silently does nothing when tapped. No error is surfaced to the user. With newer LSPosed builds that fall back to reading META-INF/yukihookapi_init and inferring the _YukiHookXposedInit suffix, the module loads anyway. With Vector / older LSPosed that only reads assets/xposed_init, it doesn't.
We hit it in production with PixelMask 1.0.16 — see user report. Diagnosed by diffing the GitHub-released APK against a freshly-cleaned local release build: the only difference was the missing assets/xposed_init.
Workaround
Consumers can paper over this in their app/build.gradle:
android.applicationVariants.configureEach { variant ->
def cap = variant.name.capitalize()
def kspTask = "ksp${cap}Kotlin"
tasks.named("merge${cap}Assets").configure { dependsOn(kspTask) }
tasks.named("merge${cap}JavaResource").configure { dependsOn(kspTask) }
}
This forces the merge tasks to wait on KSP, so the source-tree files are on disk by the time AGP samples its inputs. Verified to fix the issue; we're shipping it in PixelMask 1.0.17.
Proposed upstream fix
Use environment.codeGenerator.createNewFile(...) for the two text files as well, so KSP places them under build/generated/ksp/<variant>/resources/... and AGP picks them up via its existing KSP wiring. Roughly:
codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false),
packageName = "",
fileName = "xposed_init",
extensionName = ""
).bufferedWriter().use { it.write("${data.entryPackageName}.${data.xInitClassName}") }
(plus the equivalent for META-INF/yukihookapi_init).
If there's a reason to keep writing into the source tree (legacy IDE behaviour? user-friendly inspectability?), an alternative fix is for the KSP plugin to register the file as a Gradle task output and wire merge{Variant}Assets / merge{Variant}JavaResource to depend on it — i.e. do the workaround above on behalf of all consumers.
Happy to send a PR if a direction is settled. Thanks for the great library.
Environment
- YukiHookAPI 1.3.1 (
api + ksp-xposed)
- KSP 2.2.10-2.0.2
- Kotlin 2.2.10
- AGP 8.13.2
- Affected: any CI / fresh-checkout build path
- Not affected (silently): local incremental builds where the previous run left the file in
src/main/
Summary
yukihookapi-ksp-xposedwrites itsassets/xposed_initandresources/META-INF/yukihookapi_initoutputs directly into the source tree withFile.writeText()instead of going throughcodeGenerator.createNewFile(). This bypasses Gradle/KSP's task wiring, so on a fresh checkout (CI) AGP'smerge{Variant}Assetsandmerge{Variant}JavaResourcetasks can run beforeksp{Variant}Kotlinwrites the files — and the published APK ships withoutassets/xposed_init. LSPosed (especially Vector / older flavors that strictly rely onassets/xposed_init) then can't find the entry-class pointer and the Manager's enable toggle silently no-ops, even though the module shows up in the list because its manifest meta-data is read independently.Locally this is invisible: previous builds leave the files in
app/src/main/assets/xposed_initandapp/src/main/resources/META-INF/yukihookapi_init. SubsequentassembleReleaseruns always see them, so the race never manifests. Every CI build, however, is fresh.Affected code
yukihookapi-ksp-xposed/src/api/kotlin/com/highcapable/yukihookapi/YukiHookXposedProcessor.kt#L208-L228:projectDir.resolve(sourcePath)resolves to<module>/src/main/, i.e. into the source tree. Compare withgenerateClassFileright below, which correctly usesenvironment.codeGenerator.createNewFile(...)and lands underbuild/generated/ksp/<variant>/kotlin/.Reproduction
Any module that uses
yukihookapi:api:1.3.x+ksp 'com.highcapable.yukihookapi:ksp-xposed:1.3.1'and addsapp/src/main/assets/xposed_initto.gitignore:Result:
Note the missing
assets/xposed_init. Do not run a secondassembleRelease— the file from the first failed build now lives in the source tree, masking the bug forever after.Symptom on devices
End users hit this as: PixelMask shows up in LSPosed Manager's module list (with the correct icon/description, since
xposedmoduleetc. meta-data is in the manifest), but the enable toggle silently does nothing when tapped. No error is surfaced to the user. With newer LSPosed builds that fall back to readingMETA-INF/yukihookapi_initand inferring the_YukiHookXposedInitsuffix, the module loads anyway. With Vector / older LSPosed that only readsassets/xposed_init, it doesn't.We hit it in production with PixelMask 1.0.16 — see user report. Diagnosed by diffing the GitHub-released APK against a freshly-cleaned local release build: the only difference was the missing
assets/xposed_init.Workaround
Consumers can paper over this in their
app/build.gradle:This forces the merge tasks to wait on KSP, so the source-tree files are on disk by the time AGP samples its inputs. Verified to fix the issue; we're shipping it in PixelMask 1.0.17.
Proposed upstream fix
Use
environment.codeGenerator.createNewFile(...)for the two text files as well, so KSP places them underbuild/generated/ksp/<variant>/resources/...and AGP picks them up via its existing KSP wiring. Roughly:codeGenerator.createNewFile( dependencies = Dependencies(aggregating = false), packageName = "", fileName = "xposed_init", extensionName = "" ).bufferedWriter().use { it.write("${data.entryPackageName}.${data.xInitClassName}") }(plus the equivalent for
META-INF/yukihookapi_init).If there's a reason to keep writing into the source tree (legacy IDE behaviour? user-friendly inspectability?), an alternative fix is for the KSP plugin to register the file as a Gradle task output and wire
merge{Variant}Assets/merge{Variant}JavaResourceto depend on it — i.e. do the workaround above on behalf of all consumers.Happy to send a PR if a direction is settled. Thanks for the great library.
Environment
api+ksp-xposed)src/main/