plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.serialization) apply false } group = "com.xuqm" // Global fallback — individual modules override via their own SDK__VERSION property. // Publish all at once: ./gradlew publish -PPUBLISH_VERSION=1.0.2 // Publish one module: ./gradlew :sdk-update:publish -PSDK_UPDATE_VERSION=2.0.0-SNAPSHOT version = providers.gradleProperty("PUBLISH_VERSION").getOrElse("0.1.0-SNAPSHOT") ext["nexusUrl"] = "https://nexus.xuqinmin.com/repository/android-hosted/" ext["nexusUser"] = providers.gradleProperty("NEXUS_USER").getOrElse("") ext["nexusPassword"] = providers.gradleProperty("NEXUS_PASSWORD").getOrElse("") val verifyNoSensitiveLogging by tasks.registering { group = "verification" description = "阻止日志、标准输出和异常文案包含凭据或用户标识实际值。" doLast { val sensitiveIdentifiers = Regex( """(?i)\b(accessToken|token|userSig|pushToken|password|apiKey|signingKey|""" + """userId|groupId|targetId|messageId|requestId|toId|fromId|phone|email|regId)\b""", ) val sensitiveInterpolation = Regex( """(?i)\$(?:\{[^}]*\b(accessToken|token|userSig|pushToken|password|apiKey|""" + """signingKey|userId|groupId|targetId|messageId|requestId|toId|fromId|""" + """phone|email|regId)\b[^}]*}|(accessToken|token|userSig|pushToken|password|""" + """apiKey|signingKey|userId|groupId|targetId|messageId|requestId|toId|""" + """fromId|phone|email|regId)\b)""", ) val sinkStart = Regex( """(?:(?:android\.util\.)?Log\.[vdiew]|println|System\.out\.print(?:ln)?|""" + """[A-Za-z_][A-Za-z0-9_]*Exception|error)""" + """\s*\(""", ) fun invocationAt(source: String, start: Int): String { val open = source.indexOf('(', start) if (open < 0) return source.substring(start) var depth = 0 var quote: Char? = null var escaped = false for (index in open until source.length) { val char = source[index] if (quote != null) { if (escaped) { escaped = false } else if (char == '\\') { escaped = true } else if (char == quote) { quote = null } continue } if (char == '"' || char == '\'') { quote = char continue } if (char == '(') depth++ if (char == ')' && --depth == 0) return source.substring(start, index + 1) } return source.substring(start) } fun withoutStringLiterals(value: String): String { val result = StringBuilder(value.length) var quote: Char? = null var escaped = false value.forEach { char -> if (quote != null) { if (escaped) { escaped = false } else if (char == '\\') { escaped = true } else if (char == quote) { quote = null } result.append(' ') } else if (char == '"' || char == '\'') { quote = char result.append(' ') } else { result.append(char) } } return result.toString() } val violations = mutableListOf() fileTree(rootDir) { include("**/*.kt", "**/*.java", "**/*.kts", "**/*.gradle") exclude("**/build/**", "**/.gradle/**") }.files.sortedBy { it.path }.forEach { sourceFile -> val source = sourceFile.readText(Charsets.UTF_8) sinkStart.findAll(source).forEach { match -> val invocation = invocationAt(source, match.range.first) if ( sensitiveInterpolation.containsMatchIn(invocation) || sensitiveIdentifiers.containsMatchIn(withoutStringLiterals(invocation)) ) { val line = source.take(match.range.first).count { it == '\n' } + 1 violations += "${sourceFile.relativeTo(rootDir)}:$line" } } } check(violations.isEmpty()) { "Sensitive value may reach a log, stdout, or exception message:\n" + violations.joinToString("\n") } } } subprojects { tasks.matching { it.name == "testDebugUnitTest" || it.name == "lintRelease" || it.name == "check" }.configureEach { dependsOn(rootProject.tasks.named("verifyNoSensitiveLogging")) } } val verifySdkManifestOwnership by tasks.registering { group = "verification" description = "验证 common 的文件共享 Provider 与扩展初始化组件归属。" dependsOn( ":sdk-core:processDebugManifest", ":sdk-update:processDebugManifest", ":sdk-push:processDebugManifest", ":sdk-im:processDebugManifest", ":sdk-webview:processDebugManifest", ":sdk-bugcollect:processDebugManifest", ) doLast { fun mergedManifest(module: String): String = file("$module/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml") .readText() val core = mergedManifest("sdk-core") check("XuqmMergedProvider" !in core) check("POST_NOTIFICATIONS" !in core) check("com.xuqm.sdk.file.XuqmFileProvider" in core) check("androidx.core.content.FileProvider" !in core) val update = mergedManifest("sdk-update") check("XuqmMergedProvider" in update) check("androidx.core.content.FileProvider" !in update) val push = mergedManifest("sdk-push") check("XuqmMergedProvider" in push) check("POST_NOTIFICATIONS" in push) listOf("sdk-im", "sdk-webview", "sdk-bugcollect").forEach { module -> check("XuqmMergedProvider" in mergedManifest(module)) } } } val verifySampleManifestMerge by tasks.registering { group = "verification" description = "验证多个扩展合并后只存在一个初始化 Provider。" dependsOn(":sample-app:processDebugMainManifest") doLast { val manifest = file( "sample-app/build/intermediates/merged_manifest/debug/" + "processDebugMainManifest/AndroidManifest.xml", ).readText() check(Regex("com\\.xuqm\\.sdk\\.internal\\.XuqmMergedProvider") .findAll(manifest).count() == 1) check(Regex("com\\.xuqm\\.sdk\\.file\\.XuqmFileProvider") .findAll(manifest).count() == 1) check(Regex("com\\.xuqm\\.common_plugin_applied") .findAll(manifest).count() == 1) check("com.xuqm.bugcollect_build_enabled" in manifest) check("com.xuqm.bugcollect_automatic_enabled" in manifest) } }