expo-modules-autolinking 3.1.0-canary-20251210-1f163e3 → 3.1.0-canary-20251211-7da85ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/android/expo-gradle-plugin/expo-autolinking-settings-plugin/src/main/kotlin/expo/modules/plugin/ExpoAutolinkingSettingsPlugin.kt +28 -1
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/build.gradle.kts +44 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/main/kotlin/expo/modules/plugin/AnalyzeManifestReport.kt +69 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/main/kotlin/expo/modules/plugin/ExpoMaxSdkOverridePlugin.kt +44 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/main/kotlin/expo/modules/plugin/ExpoMaxSdkOverrideTask.kt +129 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/main/kotlin/expo/modules/plugin/FindPermissionsToOverride.kt +62 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/main/kotlin/expo/modules/plugin/PermissionInfo.kt +6 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/main/kotlin/expo/modules/plugin/utils/ExtractPathFromLine.kt +16 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/test/java/expo/modules/plugin/AnalyzeManifestReportTest.kt +102 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/test/java/expo/modules/plugin/ExtractPathFromLineTest.kt +35 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/test/java/expo/modules/plugin/FindPermissionsToOverrideTest.kt +91 -0
- package/android/expo-gradle-plugin/expo-max-sdk-override-plugin/src/test/java/expo/modules/plugin/FixManifestMaxSdkTaskTest.kt +107 -0
- package/android/expo-gradle-plugin/settings.gradle.kts +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
- Refactor test suite and file discovery implementation to drop `glob` ([#40601](https://github.com/expo/expo/pull/40601) by [@kitten](https://github.com/kitten))
|
|
45
45
|
- Improve recursive dependency resolution performance ([#40651](https://github.com/expo/expo/pull/40651) by [@kitten](https://github.com/kitten))
|
|
46
|
+
- Add `expo-max-sdk-override-plugin` gradle plugin to change the default manifest merger behaviour for `android:maxSdkVersion` conflicts. ([#40973](https://github.com/expo/expo/pull/40973) by [@behenate](https://github.com/behenate))
|
|
46
47
|
|
|
47
48
|
## 3.0.19 - 2025-10-23
|
|
48
49
|
|
|
@@ -3,9 +3,13 @@ package expo.modules.plugin
|
|
|
3
3
|
import expo.modules.plugin.gradle.addBuildCache
|
|
4
4
|
import expo.modules.plugin.gradle.beforeRootProject
|
|
5
5
|
import expo.modules.plugin.gradle.loadLocalProperties
|
|
6
|
+
import expo.modules.plugin.text.Colors
|
|
7
|
+
import expo.modules.plugin.text.withColor
|
|
6
8
|
import expo.modules.plugin.utils.getPropertiesPrefixedBy
|
|
7
9
|
import org.gradle.api.Plugin
|
|
10
|
+
import org.gradle.api.UnknownProjectException
|
|
8
11
|
import org.gradle.api.initialization.Settings
|
|
12
|
+
import org.gradle.internal.cc.base.logger
|
|
9
13
|
import java.io.File
|
|
10
14
|
import java.util.Properties
|
|
11
15
|
|
|
@@ -28,7 +32,10 @@ open class ExpoAutolinkingSettingsPlugin : Plugin<Settings> {
|
|
|
28
32
|
rootProject
|
|
29
33
|
.buildscript
|
|
30
34
|
.dependencies
|
|
31
|
-
.
|
|
35
|
+
.apply {
|
|
36
|
+
add("classpath", "expo.modules:expo-autolinking-plugin")
|
|
37
|
+
add("classpath", "expo.modules:expo-max-sdk-override-plugin")
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
// Includes the `expo-gradle-plugin` subproject.
|
|
@@ -36,6 +43,8 @@ open class ExpoAutolinkingSettingsPlugin : Plugin<Settings> {
|
|
|
36
43
|
expoGradlePluginsFile.absolutePath
|
|
37
44
|
)
|
|
38
45
|
}
|
|
46
|
+
|
|
47
|
+
configureMaxSdkOverridePlugin(settings)
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
private fun getExpoGradlePluginsFile(settings: Settings): File {
|
|
@@ -52,4 +61,22 @@ open class ExpoAutolinkingSettingsPlugin : Plugin<Settings> {
|
|
|
52
61
|
"android/expo-gradle-plugin"
|
|
53
62
|
)
|
|
54
63
|
}
|
|
64
|
+
|
|
65
|
+
private fun configureMaxSdkOverridePlugin(settings: Settings) {
|
|
66
|
+
settings.gradle.beforeRootProject { rootProject ->
|
|
67
|
+
try {
|
|
68
|
+
rootProject.project(":app") { appProject ->
|
|
69
|
+
appProject.pluginManager.withPlugin("com.android.application") {
|
|
70
|
+
appProject.pluginManager.apply("expo-max-sdk-override-plugin")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (e: UnknownProjectException) {
|
|
74
|
+
logger.error(
|
|
75
|
+
" ℹ️ Failed to apply gradle plugin ".withColor(Colors.RESET)
|
|
76
|
+
+ "'expo-max-sdk-override-plugin'".withColor(Colors.GREEN)
|
|
77
|
+
+ ". Plugin has failed to find the ':app' project. It will not be applied.".withColor(Colors.RESET)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
55
82
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
2
|
+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
3
|
+
|
|
4
|
+
plugins {
|
|
5
|
+
kotlin("jvm")
|
|
6
|
+
id("java-gradle-plugin")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dependencies {
|
|
15
|
+
implementation(project(":expo-autolinking-plugin-shared"))
|
|
16
|
+
implementation(gradleApi())
|
|
17
|
+
compileOnly("com.android.tools.build:gradle:8.5.0")
|
|
18
|
+
|
|
19
|
+
testImplementation("junit:junit:4.13.2")
|
|
20
|
+
testImplementation("com.google.truth:truth:1.1.2")
|
|
21
|
+
testImplementation(gradleTestKit())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
java {
|
|
25
|
+
sourceCompatibility = JavaVersion.VERSION_11
|
|
26
|
+
targetCompatibility = JavaVersion.VERSION_11
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
tasks.withType<KotlinCompile> {
|
|
30
|
+
compilerOptions {
|
|
31
|
+
jvmTarget.set(JvmTarget.JVM_11)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
group = "expo.modules"
|
|
36
|
+
|
|
37
|
+
gradlePlugin {
|
|
38
|
+
plugins {
|
|
39
|
+
create("expoMaxSdkOverridePlugin") {
|
|
40
|
+
id = "expo-max-sdk-override-plugin"
|
|
41
|
+
implementationClass = "expo.modules.plugin.ExpoMaxSdkOverridePlugin"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import expo.modules.plugin.utils.extractPathFromLine
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Analyzes the manifest merge report content to find permissions that may be defined with android:maxSdkVersion,
|
|
7
|
+
* where one AndroidManifest.xml defines the permission with the aforementioned annotation and another one without.
|
|
8
|
+
*
|
|
9
|
+
* This method should be used to reduce the search scope for `findPermissionsToOverride` method.
|
|
10
|
+
*
|
|
11
|
+
* @param reportContent The content of the manifest merge report to analyze.
|
|
12
|
+
* @return A map of suspicious permission names to their corresponding PermissionInfo objects.
|
|
13
|
+
*/
|
|
14
|
+
internal fun analyzeManifestReport(reportContent: String): Map<String, PermissionInfo> {
|
|
15
|
+
val allPermissionInfo = mutableMapOf<String, PermissionInfo>()
|
|
16
|
+
var currentPermission: String? = null
|
|
17
|
+
var lastAttributeWasMaxSdk = false
|
|
18
|
+
|
|
19
|
+
for (line in reportContent.lines()) {
|
|
20
|
+
val trimmedLine = line.trimStart()
|
|
21
|
+
|
|
22
|
+
// This line starts a new permission definition
|
|
23
|
+
if (line.startsWith("uses-permission#")) {
|
|
24
|
+
currentPermission = line.substringAfter("uses-permission#").trim()
|
|
25
|
+
allPermissionInfo.getOrPut(currentPermission) {
|
|
26
|
+
PermissionInfo()
|
|
27
|
+
}
|
|
28
|
+
lastAttributeWasMaxSdk = false
|
|
29
|
+
} else if (currentPermission != null) {
|
|
30
|
+
when {
|
|
31
|
+
trimmedLine.startsWith("android:maxSdkVersion") -> {
|
|
32
|
+
lastAttributeWasMaxSdk = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Source of maxSdkVersion annotation
|
|
36
|
+
line.startsWith("\t") && (trimmedLine.startsWith("ADDED from") || trimmedLine.startsWith("MERGED from")) -> {
|
|
37
|
+
if (lastAttributeWasMaxSdk) {
|
|
38
|
+
extractPathFromLine(line)?.let { source ->
|
|
39
|
+
allPermissionInfo[currentPermission]?.maxSdkSources?.add(source)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lastAttributeWasMaxSdk = false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Source of the permission definition
|
|
46
|
+
!line.startsWith("\t") && (trimmedLine.startsWith("ADDED from") || trimmedLine.startsWith("MERGED from")) -> {
|
|
47
|
+
extractPathFromLine(line)?.let { path ->
|
|
48
|
+
allPermissionInfo[currentPermission]?.manifestPaths?.add(path)
|
|
49
|
+
}
|
|
50
|
+
lastAttributeWasMaxSdk = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
else -> {
|
|
54
|
+
lastAttributeWasMaxSdk = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Permissions which may have maxSdkConflicts, happen when there is more than one
|
|
61
|
+
// source fora a permission and the permission is annotated with maxSdkVersion
|
|
62
|
+
val problematicPermissions = allPermissionInfo.filter { (permission, info) ->
|
|
63
|
+
val multipleDefinitions = info.manifestPaths.size > 1
|
|
64
|
+
val maxSdkDefined = info.maxSdkSources.isNotEmpty()
|
|
65
|
+
multipleDefinitions && maxSdkDefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return problematicPermissions.toMap()
|
|
69
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import com.android.build.api.artifact.SingleArtifact
|
|
4
|
+
import org.gradle.api.Plugin
|
|
5
|
+
import org.gradle.api.Project
|
|
6
|
+
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
|
7
|
+
import expo.modules.plugin.text.Colors
|
|
8
|
+
import expo.modules.plugin.text.Emojis
|
|
9
|
+
import expo.modules.plugin.text.withColor
|
|
10
|
+
import org.gradle.internal.cc.base.logger
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Plugin, which registers ExpoMaxSdkOverrideTask and schedules it to run with `app:processDebugManifest`.
|
|
14
|
+
*
|
|
15
|
+
* The task finds all permissions declared with `android:maxSdkVersion`. If the permission was declared in more than one place, and one of the places
|
|
16
|
+
* defines the task without `android:maxSdkVersion` the task will remove the `android:maxSdkVersion` from the final merged manifest
|
|
17
|
+
*/
|
|
18
|
+
class ExpoMaxSdkOverridePlugin : Plugin<Project> {
|
|
19
|
+
override fun apply(project: Project) {
|
|
20
|
+
val androidComponents = project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
|
|
21
|
+
logger.quiet(" ${Emojis.INFORMATION} ${"Applying gradle plugin".withColor(Colors.YELLOW)} '${"expo-max-sdk-override-plugin".withColor(Colors.GREEN)}'")
|
|
22
|
+
logger.quiet(" [expo-max-sdk-override-plugin] This plugin will find all permissions declared with `android:maxSdkVersion`. If there exists a declaration with the `android:maxSdkVersion` annotation and another one without, the plugin will remove the annotation from the final merged manifest. In order to see a log with the changes run a clean build of the app.")
|
|
23
|
+
|
|
24
|
+
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
|
|
25
|
+
val taskName = "expo${variant.name.replaceFirstChar { it.uppercase() }}OverrideMaxSdkConflicts"
|
|
26
|
+
val blameReportPath = "outputs/logs/manifest-merger-${variant.name}-report.txt"
|
|
27
|
+
val reportFile = project.layout.buildDirectory.file(blameReportPath)
|
|
28
|
+
val fixTaskProvider = project.tasks.register(
|
|
29
|
+
taskName,
|
|
30
|
+
FixManifestMaxSdkTask::class.java
|
|
31
|
+
) { task ->
|
|
32
|
+
task.blameReportFile.set(reportFile)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variant.artifacts
|
|
36
|
+
.use(fixTaskProvider)
|
|
37
|
+
.wiredWithFiles(
|
|
38
|
+
FixManifestMaxSdkTask::mergedManifestIn,
|
|
39
|
+
FixManifestMaxSdkTask::modifiedManifestOut
|
|
40
|
+
)
|
|
41
|
+
.toTransform(SingleArtifact.MERGED_MANIFEST)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import expo.modules.plugin.text.Colors
|
|
4
|
+
import expo.modules.plugin.text.withColor
|
|
5
|
+
import org.gradle.api.DefaultTask
|
|
6
|
+
import org.gradle.api.file.RegularFileProperty
|
|
7
|
+
import org.gradle.api.tasks.InputFile
|
|
8
|
+
import org.gradle.api.tasks.OutputFile
|
|
9
|
+
import org.gradle.api.tasks.TaskAction
|
|
10
|
+
import java.io.File
|
|
11
|
+
import javax.xml.parsers.DocumentBuilderFactory
|
|
12
|
+
import javax.xml.transform.OutputKeys
|
|
13
|
+
import javax.xml.transform.TransformerFactory
|
|
14
|
+
import javax.xml.transform.dom.DOMSource
|
|
15
|
+
import javax.xml.transform.stream.StreamResult
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This task reads the manifest merge report, finds conflicting permissions, and removes 'android:maxSdkVersion' from them in the final merged manifest.
|
|
19
|
+
*/
|
|
20
|
+
abstract class FixManifestMaxSdkTask : DefaultTask() {
|
|
21
|
+
@get:InputFile
|
|
22
|
+
abstract val blameReportFile: RegularFileProperty
|
|
23
|
+
|
|
24
|
+
@get:InputFile
|
|
25
|
+
abstract val mergedManifestIn: RegularFileProperty
|
|
26
|
+
|
|
27
|
+
@get:OutputFile
|
|
28
|
+
abstract val modifiedManifestOut: RegularFileProperty
|
|
29
|
+
|
|
30
|
+
@TaskAction
|
|
31
|
+
fun taskAction() {
|
|
32
|
+
val blameFile = blameReportFile.get().asFile
|
|
33
|
+
val inManifest = mergedManifestIn.get().asFile
|
|
34
|
+
val outManifest = modifiedManifestOut.get().asFile
|
|
35
|
+
|
|
36
|
+
logger.quiet("---------- Expo Max Sdk Override Plugin ----------".withColor(Colors.YELLOW))
|
|
37
|
+
|
|
38
|
+
if (!blameFile.exists()) {
|
|
39
|
+
logger.warn("Manifest blame report not found: ${blameFile.absolutePath}. Skipping `android:maxSdkVersion` permission conflict check.")
|
|
40
|
+
inManifest.copyTo(outManifest, overwrite = true)
|
|
41
|
+
logNoChanges()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
val reportContents = blameFile.readText()
|
|
46
|
+
val potentialProblems = analyzeManifestReport(reportContents)
|
|
47
|
+
|
|
48
|
+
if (potentialProblems.isEmpty()) {
|
|
49
|
+
inManifest.copyTo(outManifest, overwrite = true)
|
|
50
|
+
logNoChanges()
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val brokenPermissions = findPermissionsToOverride(potentialProblems)
|
|
55
|
+
|
|
56
|
+
if (brokenPermissions.isEmpty()) {
|
|
57
|
+
inManifest.copyTo(outManifest, overwrite = true)
|
|
58
|
+
logNoChanges()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.quiet(">>> WARNING: Found ${brokenPermissions.size} permission(s) with conflicting 'android:maxSdkVersion' declarations.".withColor(Colors.YELLOW))
|
|
63
|
+
brokenPermissions.forEach { (permission, info) ->
|
|
64
|
+
val sourcesWithoutMaxSdk = info.manifestPaths.subtract(info.maxSdkSources)
|
|
65
|
+
logger.quiet(" - $permission".withColor(Colors.YELLOW))
|
|
66
|
+
logger.quiet(" > Defined WITH `android:maxSdkVersion` in: ${info.maxSdkSources.joinToString()}".withColor(Colors.YELLOW))
|
|
67
|
+
logger.quiet(" > Defined WITHOUT `android:maxSdkVersion` in: ${sourcesWithoutMaxSdk.joinToString()}".withColor(Colors.YELLOW))
|
|
68
|
+
}
|
|
69
|
+
logger.quiet(">>> Removing 'android:maxSdkVersion' from these permissions in the final manifest to prevent runtime issues.".withColor(Colors.YELLOW))
|
|
70
|
+
|
|
71
|
+
tryFixManifest(inManifest, outManifest, brokenPermissions)
|
|
72
|
+
|
|
73
|
+
logger.quiet("--------------------------------------------------".withColor(Colors.YELLOW))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun logNoChanges() {
|
|
77
|
+
logger.quiet(">>> No 'android:maxSdkVersion' conflicts found".withColor(Colors.GREEN))
|
|
78
|
+
logger.quiet("--------------------------------------------------".withColor(Colors.YELLOW))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun tryFixManifest(inManifest: File, outManifest: File, brokenPermissions: Map<String, PermissionInfo>) {
|
|
82
|
+
try {
|
|
83
|
+
val factory = DocumentBuilderFactory.newInstance().apply {
|
|
84
|
+
isNamespaceAware = true
|
|
85
|
+
}
|
|
86
|
+
val builder = factory.newDocumentBuilder()
|
|
87
|
+
|
|
88
|
+
val doc = builder.parse(inManifest)
|
|
89
|
+
val permissionNodes = doc.getElementsByTagName(ManifestConstants.USES_PERMISSION_TAG)
|
|
90
|
+
var modificationsMade = 0
|
|
91
|
+
|
|
92
|
+
val nodesToProcess = (0 until permissionNodes.length)
|
|
93
|
+
.map { permissionNodes.item(it) }
|
|
94
|
+
.filterIsInstance<org.w3c.dom.Element>()
|
|
95
|
+
|
|
96
|
+
for (element in nodesToProcess) {
|
|
97
|
+
val permissionName = element.getAttribute(ManifestConstants.ANDROID_NAME_ATTRIBUTE)
|
|
98
|
+
|
|
99
|
+
if (brokenPermissions.containsKey(permissionName) && element.hasAttribute(ManifestConstants.ANDROID_MAX_SDK_VERSION_ATTRIBUTE)) {
|
|
100
|
+
element.removeAttribute(ManifestConstants.ANDROID_MAX_SDK_VERSION_ATTRIBUTE)
|
|
101
|
+
modificationsMade++
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (modificationsMade > 0) {
|
|
106
|
+
logger.quiet(">>> Removed 'android:maxSdkVersion' from $modificationsMade instance(s) in the final manifest.".withColor(Colors.YELLOW))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
TransformerFactory.newInstance().newTransformer().apply {
|
|
110
|
+
setOutputProperty(OutputKeys.INDENT, "yes")
|
|
111
|
+
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
|
|
112
|
+
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")
|
|
113
|
+
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
|
|
114
|
+
transform(DOMSource(doc), StreamResult(outManifest))
|
|
115
|
+
}
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
logger.error("Failed to parse and fix merged manifest: ${e.message}".withColor(Colors.RESET), e)
|
|
118
|
+
logger.quiet(">>> Restored the original merged manifest.".withColor(Colors.YELLOW))
|
|
119
|
+
|
|
120
|
+
inManifest.copyTo(outManifest, overwrite = true)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private object ManifestConstants {
|
|
126
|
+
const val USES_PERMISSION_TAG = "uses-permission"
|
|
127
|
+
const val ANDROID_NAME_ATTRIBUTE = "android:name"
|
|
128
|
+
const val ANDROID_MAX_SDK_VERSION_ATTRIBUTE = "android:maxSdkVersion"
|
|
129
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import org.gradle.internal.cc.base.logger
|
|
4
|
+
import java.io.File
|
|
5
|
+
import javax.xml.parsers.DocumentBuilderFactory
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Based on a map of `String` and `PermissionInfo` read and parse manifest files, finds cases where
|
|
9
|
+
* a permission is defined in one place with `android:maxSdkVersion` and in another without that annotation.
|
|
10
|
+
*
|
|
11
|
+
* @param problematicPermissions A Map of `String` and `PermissionInfo` obtained with analyzeManifestReport
|
|
12
|
+
*/
|
|
13
|
+
internal fun findPermissionsToOverride(problematicPermissions: Map<String, PermissionInfo>): Map<String, PermissionInfo> {
|
|
14
|
+
val factory = DocumentBuilderFactory.newInstance()
|
|
15
|
+
factory.isNamespaceAware = true
|
|
16
|
+
|
|
17
|
+
// Basic security
|
|
18
|
+
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) // Disallow parsing <!DOCTYPE> files
|
|
19
|
+
factory.setFeature("http://xml.org/sax/features/external-general-entities", false) // Prevent external general entities
|
|
20
|
+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false) // Prevent external paramater entities
|
|
21
|
+
|
|
22
|
+
val builder = factory.newDocumentBuilder()
|
|
23
|
+
val brokenPermissions = mutableMapOf<String, PermissionInfo>()
|
|
24
|
+
|
|
25
|
+
problematicPermissions.forEach { permission, info ->
|
|
26
|
+
// Not actually a problematic permission
|
|
27
|
+
if (info.maxSdkSources.size == 0) {
|
|
28
|
+
return@forEach
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
info.manifestPaths.forEach { manifestPath ->
|
|
32
|
+
try {
|
|
33
|
+
val file = File(manifestPath)
|
|
34
|
+
if (!file.exists() || !file.canRead()) {
|
|
35
|
+
logger.error("Failed to open manifest file at: $manifestPath")
|
|
36
|
+
return@forEach
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
val doc = builder.parse(file)
|
|
40
|
+
val permissionNodes = doc.getElementsByTagName("uses-permission")
|
|
41
|
+
|
|
42
|
+
for (i in 0 until permissionNodes.length) {
|
|
43
|
+
val permissionNode = permissionNodes.item(i)
|
|
44
|
+
|
|
45
|
+
if (permissionNode.nodeType == org.w3c.dom.Node.ELEMENT_NODE) {
|
|
46
|
+
val element = permissionNode as org.w3c.dom.Element
|
|
47
|
+
val permissionName = element.getAttribute("android:name")
|
|
48
|
+
|
|
49
|
+
if (permissionName == permission && !element.hasAttribute("android:maxSdkVersion")) {
|
|
50
|
+
brokenPermissions[permission] = info
|
|
51
|
+
return@forEach
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (e: Exception) {
|
|
56
|
+
logger.error("Failed to parse manifest at ${manifestPath}", e)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return brokenPermissions
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package expo.modules.plugin.utils
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts a single file path from one line of an AndroidManifest merge log.
|
|
5
|
+
*
|
|
6
|
+
* @param line The raw single-line string from the build log.
|
|
7
|
+
* @return A String containing the absolute file path to the manifest, or null if no path is found.
|
|
8
|
+
*/
|
|
9
|
+
fun extractPathFromLine(line: String): String? {
|
|
10
|
+
// Regex to find a path starting with '/' and ending just before
|
|
11
|
+
// the line/column numbers (e.g., :11:3)
|
|
12
|
+
val regex = Regex("(/.*?):\\d+:\\d+.*")
|
|
13
|
+
val match = regex.find(line)
|
|
14
|
+
|
|
15
|
+
return match?.groups?.get(1)?.value
|
|
16
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import com.google.common.truth.Truth.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class AnalyzeManifestReportTest {
|
|
7
|
+
|
|
8
|
+
@Test
|
|
9
|
+
fun `finds permission with conflict`() {
|
|
10
|
+
val reportContent = """
|
|
11
|
+
uses-permission#android.permission.READ_CONTACTS
|
|
12
|
+
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
|
|
13
|
+
MERGED from /Users/user/project/library/src/main/AndroidManifest.xml:15:3-83
|
|
14
|
+
android:maxSdkVersion
|
|
15
|
+
ADDED from /Users/user/project/library/src/main/AndroidManifest.xml:16:7-34
|
|
16
|
+
""".trimIndent()
|
|
17
|
+
|
|
18
|
+
val problems = analyzeManifestReport(reportContent)
|
|
19
|
+
|
|
20
|
+
assertThat(problems).hasSize(1)
|
|
21
|
+
assertThat(problems).containsKey("android.permission.READ_CONTACTS")
|
|
22
|
+
|
|
23
|
+
val info = problems["android.permission.READ_CONTACTS"]
|
|
24
|
+
assertThat(info).isNotNull()
|
|
25
|
+
|
|
26
|
+
info?.let {
|
|
27
|
+
assertThat(info.manifestPaths).containsExactly(
|
|
28
|
+
"/Users/user/project/app/src/main/AndroidManifest.xml",
|
|
29
|
+
"/Users/user/project/library/src/main/AndroidManifest.xml"
|
|
30
|
+
)
|
|
31
|
+
assertThat(info.maxSdkSources).containsExactly(
|
|
32
|
+
"/Users/user/project/library/src/main/AndroidManifest.xml"
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun `ignores permission with no conflict`() {
|
|
39
|
+
val reportContent = """
|
|
40
|
+
uses-permission#android.permission.READ_CONTACTS
|
|
41
|
+
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
|
|
42
|
+
MERGED from /Users/user/project/library/src/main/AndroidManifest.xml:15:3-83
|
|
43
|
+
""".trimIndent()
|
|
44
|
+
|
|
45
|
+
val problems = analyzeManifestReport(reportContent)
|
|
46
|
+
assertThat(problems).isEmpty()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun `ignores permission with only one source`() {
|
|
51
|
+
val reportContent = """
|
|
52
|
+
uses-permission#android.permission.READ_CONTACTS
|
|
53
|
+
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
|
|
54
|
+
android:maxSdkVersion
|
|
55
|
+
ADDED from /Users/user/project/app/src/main/AndroidManifest.xml:12:7-34
|
|
56
|
+
""".trimIndent()
|
|
57
|
+
|
|
58
|
+
val problems = analyzeManifestReport(reportContent)
|
|
59
|
+
|
|
60
|
+
assertThat(problems).isEmpty()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun `handles multiple permissions`() {
|
|
65
|
+
val reportContent = """
|
|
66
|
+
uses-permission#android.permission.READ_CONTACTS
|
|
67
|
+
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
|
|
68
|
+
MERGED from /Users/user/project/library/src/main/AndroidManifest.xml:15:3-83
|
|
69
|
+
android:maxSdkVersion
|
|
70
|
+
ADDED from /Users/user/project/library/src/main/AndroidManifest.xml:16:7-34
|
|
71
|
+
|
|
72
|
+
uses-permission#android.permission.WRITE_EXTERNAL_STORAGE
|
|
73
|
+
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:13:3-33
|
|
74
|
+
|
|
75
|
+
uses-permission#android.permission.READ_EXTERNAL_STORAGE
|
|
76
|
+
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:14:3-33
|
|
77
|
+
MERGED from /Users/user/project/otherlib/src/main/AndroidManifest.xml:9:3-83
|
|
78
|
+
android:maxSdkVersion
|
|
79
|
+
ADDED from /Users/user/project/app/src/main/AndroidManifest.xml:15:7-34
|
|
80
|
+
""".trimIndent()
|
|
81
|
+
|
|
82
|
+
val problems = analyzeManifestReport(reportContent)
|
|
83
|
+
|
|
84
|
+
assertThat(problems).hasSize(2)
|
|
85
|
+
assertThat(problems).containsKey("android.permission.READ_CONTACTS")
|
|
86
|
+
assertThat(problems).containsKey("android.permission.READ_EXTERNAL_STORAGE")
|
|
87
|
+
|
|
88
|
+
val readContactsInfo = problems["android.permission.READ_CONTACTS"]!!
|
|
89
|
+
assertThat(readContactsInfo.maxSdkSources).containsExactly(
|
|
90
|
+
"/Users/user/project/library/src/main/AndroidManifest.xml"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
val readStorageInfo = problems["android.permission.READ_EXTERNAL_STORAGE"]!!
|
|
94
|
+
assertThat(readStorageInfo.manifestPaths).containsExactly(
|
|
95
|
+
"/Users/user/project/app/src/main/AndroidManifest.xml",
|
|
96
|
+
"/Users/user/project/otherlib/src/main/AndroidManifest.xml"
|
|
97
|
+
)
|
|
98
|
+
assertThat(readStorageInfo.maxSdkSources).containsExactly(
|
|
99
|
+
"/Users/user/project/app/src/main/AndroidManifest.xml"
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package expo.modules.plugin.utils
|
|
2
|
+
|
|
3
|
+
import com.google.common.truth.Truth.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class ExtractPathFromLineTest {
|
|
7
|
+
|
|
8
|
+
@Test
|
|
9
|
+
fun `extracts path from a standard merge log line`() {
|
|
10
|
+
val line = "\tMERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33"
|
|
11
|
+
val path = extractPathFromLine(line)
|
|
12
|
+
assertThat(path).isEqualTo("/Users/user/project/app/src/main/AndroidManifest.xml")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun `extracts path from an ADDED line`() {
|
|
17
|
+
val line = " ADDED from /Users/user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml:23:7-77"
|
|
18
|
+
val path = extractPathFromLine(line)
|
|
19
|
+
assertThat(path).isEqualTo("/Users/user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun `extracts path from an ADDED line with space`() {
|
|
24
|
+
val line = " ADDED from /Users/happy user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml:23:7-77"
|
|
25
|
+
val path = extractPathFromLine(line)
|
|
26
|
+
assertThat(path).isEqualTo("/Users/happy user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `returns null if no path is found`() {
|
|
31
|
+
val line = "\tandroid:maxSdkVersion"
|
|
32
|
+
val path = extractPathFromLine(line)
|
|
33
|
+
assertThat(path).isNull()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import com.google.common.truth.Truth.assertThat
|
|
4
|
+
import org.junit.Before
|
|
5
|
+
import org.junit.Rule
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.rules.TemporaryFolder
|
|
8
|
+
import java.io.File
|
|
9
|
+
|
|
10
|
+
class FindPermissionsToOverrideTest {
|
|
11
|
+
|
|
12
|
+
@get:Rule
|
|
13
|
+
val tempFolder = TemporaryFolder()
|
|
14
|
+
|
|
15
|
+
private lateinit var manifestWithMaxSdk: File
|
|
16
|
+
private lateinit var manifestWithoutMaxSdk: File
|
|
17
|
+
|
|
18
|
+
@Before
|
|
19
|
+
fun setup() {
|
|
20
|
+
manifestWithMaxSdk = File(tempFolder.root, "max_sdk_manifest.xml")
|
|
21
|
+
manifestWithMaxSdk.writeText("""
|
|
22
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
23
|
+
<uses-permission
|
|
24
|
+
android:name="android.permission.READ_CONTACTS"
|
|
25
|
+
android:maxSdkVersion="28" />
|
|
26
|
+
</manifest>
|
|
27
|
+
""".trimIndent())
|
|
28
|
+
|
|
29
|
+
manifestWithoutMaxSdk = File(tempFolder.root, "no_max_sdk_manifest.xml")
|
|
30
|
+
manifestWithoutMaxSdk.writeText("""
|
|
31
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
32
|
+
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
|
33
|
+
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
|
34
|
+
</manifest>
|
|
35
|
+
""".trimIndent())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Test
|
|
39
|
+
fun `finds permission that needs to be overridden`() {
|
|
40
|
+
val permissionInfo = PermissionInfo(
|
|
41
|
+
maxSdkSources = mutableSetOf(manifestWithMaxSdk.absolutePath),
|
|
42
|
+
manifestPaths = mutableSetOf(
|
|
43
|
+
manifestWithMaxSdk.absolutePath,
|
|
44
|
+
manifestWithoutMaxSdk.absolutePath
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
val problems = mapOf("android.permission.READ_CONTACTS" to permissionInfo)
|
|
48
|
+
val overrides = findPermissionsToOverride(problems)
|
|
49
|
+
|
|
50
|
+
assertThat(overrides).hasSize(1)
|
|
51
|
+
assertThat(overrides).containsKey("android.permission.READ_CONTACTS")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun `does not find override if no conflict exists`() {
|
|
56
|
+
val manifestWithMaxSdk2 = File(tempFolder.root, "max_sdk_manifest_2.xml")
|
|
57
|
+
manifestWithMaxSdk2.writeText("""
|
|
58
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
59
|
+
<uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="28" />
|
|
60
|
+
</manifest>
|
|
61
|
+
""".trimIndent())
|
|
62
|
+
|
|
63
|
+
val permissionInfo = PermissionInfo(
|
|
64
|
+
maxSdkSources = mutableSetOf(manifestWithMaxSdk.absolutePath, manifestWithMaxSdk2.absolutePath),
|
|
65
|
+
manifestPaths = mutableSetOf(
|
|
66
|
+
manifestWithMaxSdk.absolutePath,
|
|
67
|
+
manifestWithMaxSdk2.absolutePath
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
val problems = mapOf("android.permission.READ_CONTACTS" to permissionInfo)
|
|
71
|
+
val overrides = findPermissionsToOverride(problems)
|
|
72
|
+
|
|
73
|
+
assertThat(overrides).isEmpty()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun `ignores permission if file does not exist`() {
|
|
78
|
+
val nonExistentPath = "/path/to/nothing.xml"
|
|
79
|
+
val permissionInfo = PermissionInfo(
|
|
80
|
+
maxSdkSources = mutableSetOf(manifestWithMaxSdk.absolutePath),
|
|
81
|
+
manifestPaths = mutableSetOf(
|
|
82
|
+
manifestWithMaxSdk.absolutePath,
|
|
83
|
+
nonExistentPath
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
val problems = mapOf("android.permission.READ_CONTACTS" to permissionInfo)
|
|
87
|
+
val overrides = findPermissionsToOverride(problems)
|
|
88
|
+
|
|
89
|
+
assertThat(overrides).isEmpty()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package expo.modules.plugin
|
|
2
|
+
|
|
3
|
+
import com.google.common.truth.Truth.assertThat
|
|
4
|
+
import org.gradle.testfixtures.ProjectBuilder
|
|
5
|
+
import org.junit.Before
|
|
6
|
+
import org.junit.Rule
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.junit.rules.TemporaryFolder
|
|
9
|
+
import java.io.File
|
|
10
|
+
|
|
11
|
+
class FixManifestMaxSdkTaskTest {
|
|
12
|
+
@get:Rule
|
|
13
|
+
val tempFolder = TemporaryFolder()
|
|
14
|
+
|
|
15
|
+
private lateinit var blameReportFile: File
|
|
16
|
+
private lateinit var mergedManifestIn: File
|
|
17
|
+
private lateinit var modifiedManifestOut: File
|
|
18
|
+
|
|
19
|
+
private lateinit var manifest1: File
|
|
20
|
+
private lateinit var manifest2: File
|
|
21
|
+
|
|
22
|
+
@Before
|
|
23
|
+
fun setup() {
|
|
24
|
+
val projectDir = tempFolder.root
|
|
25
|
+
blameReportFile = File(projectDir, "blame-report.txt")
|
|
26
|
+
mergedManifestIn = File(projectDir, "merged-manifest-in.xml")
|
|
27
|
+
modifiedManifestOut = File(projectDir, "modified-manifest-out.xml")
|
|
28
|
+
|
|
29
|
+
val manifestDir1 = File(projectDir, "lib1/src/main").apply { mkdirs() }
|
|
30
|
+
val manifestDir2 = File(projectDir, "app/src/main").apply { mkdirs() }
|
|
31
|
+
|
|
32
|
+
manifest1 = File(manifestDir1, "AndroidManifest.xml")
|
|
33
|
+
manifest1.writeText("""
|
|
34
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
35
|
+
<uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="28" />
|
|
36
|
+
</manifest>
|
|
37
|
+
""".trimIndent())
|
|
38
|
+
|
|
39
|
+
manifest2 = File(manifestDir2, "AndroidManifest.xml")
|
|
40
|
+
manifest2.writeText("""
|
|
41
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
42
|
+
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
|
43
|
+
</manifest>
|
|
44
|
+
""".trimIndent())
|
|
45
|
+
|
|
46
|
+
blameReportFile.writeText("""
|
|
47
|
+
uses-permission#android.permission.READ_CONTACTS
|
|
48
|
+
MERGED from ${manifest2.absolutePath}:5:3-33
|
|
49
|
+
MERGED from ${manifest1.absolutePath}:3:3-83
|
|
50
|
+
android:maxSdkVersion
|
|
51
|
+
ADDED from ${manifest1.absolutePath}:4:7-34
|
|
52
|
+
""".trimIndent())
|
|
53
|
+
|
|
54
|
+
mergedManifestIn.writeText("""
|
|
55
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
56
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
57
|
+
package="com.example.app">
|
|
58
|
+
|
|
59
|
+
<uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="28" />
|
|
60
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
61
|
+
|
|
62
|
+
</manifest>
|
|
63
|
+
""".trimIndent())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `task removes maxSdkVersion from conflicting permission`() {
|
|
68
|
+
val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
|
|
69
|
+
val task = project.tasks.register("testFixTask", FixManifestMaxSdkTask::class.java).get()
|
|
70
|
+
|
|
71
|
+
task.blameReportFile.set(blameReportFile)
|
|
72
|
+
task.mergedManifestIn.set(mergedManifestIn)
|
|
73
|
+
task.modifiedManifestOut.set(modifiedManifestOut)
|
|
74
|
+
|
|
75
|
+
task.taskAction()
|
|
76
|
+
|
|
77
|
+
val outputContent = modifiedManifestOut.readText()
|
|
78
|
+
|
|
79
|
+
assertThat(outputContent).contains("<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>")
|
|
80
|
+
assertThat(outputContent).doesNotContain("maxSdkVersion")
|
|
81
|
+
assertThat(outputContent).contains("<uses-permission android:name=\"android.permission.INTERNET\"/>")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun `task copies file directly if no conflicts are found`() {
|
|
86
|
+
val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
|
|
87
|
+
val task = project.tasks.register("testFixTask", FixManifestMaxSdkTask::class.java).get()
|
|
88
|
+
|
|
89
|
+
blameReportFile.writeText("""
|
|
90
|
+
uses-permission#android.permission.READ_CONTACTS
|
|
91
|
+
MERGED from /app/src/main/AndroidManifest.xml:5:3-33
|
|
92
|
+
""".trimIndent())
|
|
93
|
+
|
|
94
|
+
val originalContent = mergedManifestIn.readText()
|
|
95
|
+
|
|
96
|
+
task.blameReportFile.set(blameReportFile)
|
|
97
|
+
task.mergedManifestIn.set(mergedManifestIn)
|
|
98
|
+
task.modifiedManifestOut.set(modifiedManifestOut)
|
|
99
|
+
|
|
100
|
+
task.taskAction()
|
|
101
|
+
|
|
102
|
+
val outputContent = modifiedManifestOut.readText()
|
|
103
|
+
|
|
104
|
+
assertThat(outputContent).isEqualTo(originalContent)
|
|
105
|
+
assertThat(outputContent).contains("maxSdkVersion=\"28\"")
|
|
106
|
+
}
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-autolinking",
|
|
3
|
-
"version": "3.1.0-canary-
|
|
3
|
+
"version": "3.1.0-canary-20251211-7da85ea",
|
|
4
4
|
"description": "Scripts that autolink Expo modules.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"homepage": "https://github.com/expo/expo/tree/main/packages/expo-modules-autolinking#readme",
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"expo-module-scripts": "5.1.0-canary-
|
|
37
|
+
"expo-module-scripts": "5.1.0-canary-20251211-7da85ea",
|
|
38
38
|
"memfs": "^3.2.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|