diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/NativeLibraryAabCleanupTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/NativeLibraryAabCleanupTask.kt new file mode 100644 index 00000000000..9e46e419d93 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/NativeLibraryAabCleanupTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.tasks + +import com.facebook.react.utils.SoCleanerUtils +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class NativeLibraryAabCleanupTask : DefaultTask() { + + @get:InputFile abstract val inputBundle: RegularFileProperty + + @get:OutputFile abstract val outputBundle: RegularFileProperty + + @get:Input abstract val enableHermes: Property + + @get:Input abstract val debuggableVariant: Property + + @TaskAction + fun run() { + val inputBundleFile = inputBundle.get().asFile + val outputBundleFile = outputBundle.get().asFile + SoCleanerUtils.clean( + input = inputBundleFile, + prefix = "base/lib", + enableHermes = enableHermes.get(), + debuggableVariant = debuggableVariant.get()) + inputBundleFile.copyTo(outputBundleFile, overwrite = true) + } +} diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/NativeLibraryApkCleanupTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/NativeLibraryApkCleanupTask.kt new file mode 100644 index 00000000000..8c0064d4739 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/NativeLibraryApkCleanupTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.tasks + +import com.facebook.react.utils.SoCleanerUtils +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class NativeLibraryApkCleanupTask : DefaultTask() { + + @get:InputDirectory abstract val inputApkDirectory: DirectoryProperty + + @get:OutputDirectory abstract val outputApkDirectory: DirectoryProperty + + @get:Input abstract val enableHermes: Property + + @get:Input abstract val debuggableVariant: Property + + @TaskAction + fun run() { + inputApkDirectory.get().asFile.walk().forEach { + if (it.name.endsWith(".apk")) { + SoCleanerUtils.clean( + input = it, + prefix = "lib/", + enableHermes = enableHermes.get(), + debuggableVariant = debuggableVariant.get()) + } + } + } +} diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/SoCleanerUtils.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/SoCleanerUtils.kt new file mode 100644 index 00000000000..ece6924d5aa --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/SoCleanerUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.utils + +import java.io.File +import java.net.URI +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Files + +internal object SoCleanerUtils { + + private val zipProperties = mapOf("create" to "false") + + private val archs = listOf("x86", "x86_64", "armeabi-v7a", "arm64-v8a") + + fun clean(input: File, prefix: String, enableHermes: Boolean, debuggableVariant: Boolean) { + val zipDisk: URI = URI.create("jar:file:${input.absolutePath}") + FileSystems.newFileSystem(zipDisk, zipProperties).use { zipfs -> + buildList { + if (enableHermes) { + add("libjsc.so") + add("libjscexecutor.so") + if (debuggableVariant) { + add("libhermes-executor-release.so") + } else { + add("libhermes-executor-debug.so") + } + } else { + add("libhermes.so") + add("libhermes-executor-debug.so") + add("libhermes-executor-release.so") + } + } + .forEach { removeSoFiles(zipfs, prefix, archs, it) } + } + } + + fun removeSoFiles( + zipfs: FileSystem, + prefix: String, + archs: List, + libraryToRemove: String + ) { + archs.forEach { arch -> + try { + Files.delete(zipfs.getPath("$prefix/$arch/$libraryToRemove")) + } catch (e: Exception) { + // File was already missing due to ABI split, nothing to do here. + } + } + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/NativeLibraryAabCleanupTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/NativeLibraryAabCleanupTaskTest.kt new file mode 100644 index 00000000000..e48cbda67e6 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/NativeLibraryAabCleanupTaskTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.tasks + +import com.facebook.react.tests.createTestTask +import com.facebook.react.tests.createZip +import java.io.File +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files.exists +import org.gradle.api.tasks.* +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class NativeLibraryAabCleanupTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun nativeLibraryAabCleanupTask_withHermesDebug_cleansCorrectly() { + val inputAab = File(tempFolder.root, "input.aab") + val outputAab = File(tempFolder.root, "output.aab") + createZip( + inputAab, + listOf( + "base/lib/x86/libhermes.so", + "base/lib/x86/libhermes-executor-debug.so", + "base/lib/x86/libhermes-executor-release.so", + "base/lib/x86/libjsc.so", + "base/lib/x86/libjscexecutor.so", + )) + val task = + createTestTask { + it.inputBundle.set(inputAab) + it.outputBundle.set(outputAab) + it.enableHermes.set(true) + it.debuggableVariant.set(true) + } + + task.run() + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${inputAab.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertTrue(exists(it.getPath("base/lib/x86/libhermes.so"))) + assertTrue(exists(it.getPath("base/lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libhermes-executor-release.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libjsc.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libjscexecutor.so"))) + } + } + + @Test + fun nativeLibraryAabCleanupTask_withHermesRelease_cleansCorrectly() { + val inputAab = File(tempFolder.root, "input.aab") + val outputAab = File(tempFolder.root, "output.aab") + createZip( + inputAab, + listOf( + "base/lib/x86/libhermes.so", + "base/lib/x86/libhermes-executor-debug.so", + "base/lib/x86/libhermes-executor-release.so", + "base/lib/x86/libjsc.so", + "base/lib/x86/libjscexecutor.so", + )) + val task = + createTestTask { + it.inputBundle.set(inputAab) + it.outputBundle.set(outputAab) + it.enableHermes.set(true) + it.debuggableVariant.set(false) + } + + task.run() + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${inputAab.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertTrue(exists(it.getPath("base/lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libhermes-executor-debug.so"))) + assertTrue(exists(it.getPath("base/lib/x86/libhermes-executor-release.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libjsc.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libjscexecutor.so"))) + } + } + + @Test + fun nativeLibraryAabCleanupTask_withJscDebug_cleansCorrectly() { + val inputAab = File(tempFolder.root, "input.aab") + val outputAab = File(tempFolder.root, "output.aab") + createZip( + inputAab, + listOf( + "base/lib/x86/libhermes.so", + "base/lib/x86/libhermes-executor-debug.so", + "base/lib/x86/libhermes-executor-release.so", + "base/lib/x86/libjsc.so", + "base/lib/x86/libjscexecutor.so", + )) + val task = + createTestTask { + it.inputBundle.set(inputAab) + it.outputBundle.set(outputAab) + it.enableHermes.set(false) + it.debuggableVariant.set(true) + } + + task.run() + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${inputAab.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertFalse(exists(it.getPath("base/lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libhermes-executor-release.so"))) + assertTrue(exists(it.getPath("base/lib/x86/libjsc.so"))) + assertTrue(exists(it.getPath("base/lib/x86/libjscexecutor.so"))) + } + } + + @Test + fun nativeLibraryAabCleanupTask_withJscRelease_cleansCorrectly() { + val inputAab = File(tempFolder.root, "input.aab") + val outputAab = File(tempFolder.root, "output.aab") + createZip( + inputAab, + listOf( + "base/lib/x86/libhermes.so", + "base/lib/x86/libhermes-executor-debug.so", + "base/lib/x86/libhermes-executor-release.so", + "base/lib/x86/libjsc.so", + "base/lib/x86/libjscexecutor.so", + )) + val task = + createTestTask { + it.inputBundle.set(inputAab) + it.outputBundle.set(outputAab) + it.enableHermes.set(false) + it.debuggableVariant.set(false) + } + + task.run() + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${inputAab.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertFalse(exists(it.getPath("base/lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("base/lib/x86/libhermes-executor-release.so"))) + assertTrue(exists(it.getPath("base/lib/x86/libjsc.so"))) + assertTrue(exists(it.getPath("base/lib/x86/libjscexecutor.so"))) + } + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/NativeLibraryApkCleanupTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/NativeLibraryApkCleanupTaskTest.kt new file mode 100644 index 00000000000..7abc0e6c121 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/NativeLibraryApkCleanupTaskTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.tasks + +import com.facebook.react.tests.createTestTask +import com.facebook.react.tests.createZip +import java.io.File +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files.exists +import org.gradle.api.tasks.* +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class NativeLibraryApkCleanupTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun nativeLibraryApkCleanupTask_runWithAppApk() { + val tempApk = File(tempFolder.root, "app.apk") + createZip( + tempApk, + listOf( + "lib/x86/libhermes.so", + "lib/x86/libhermes-executor-debug.so", + "lib/x86/libhermes-executor-release.so", + "lib/x86/libjsc.so", + "lib/x86/libjscexecutor.so", + )) + val task = + createTestTask { + it.inputApkDirectory.set(tempFolder.root) + it.outputApkDirectory.set(tempFolder.root) + it.enableHermes.set(true) + it.debuggableVariant.set(true) + } + + task.run() + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempApk.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertTrue(exists(it.getPath("lib/x86/libhermes.so"))) + assertTrue(exists(it.getPath("lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-release.so"))) + assertFalse(exists(it.getPath("lib/x86/libjsc.so"))) + assertFalse(exists(it.getPath("lib/x86/libjscexecutor.so"))) + } + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt index 5bf7cee7214..b677cca6670 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt @@ -8,6 +8,9 @@ package com.facebook.react.tests import java.io.* +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import org.gradle.api.Project @@ -49,3 +52,18 @@ internal fun zipFiles(destination: File, contents: List) { } } } + +/** A util function to create a zip given a list of dummy files path. */ +internal fun createZip(dest: File, paths: List) { + val env = mapOf("create" to "true") + val uri = URI.create("jar:file:$dest") + + FileSystems.newFileSystem(uri, env).use { zipfs -> + paths.forEach { + val zipEntryPath = zipfs.getPath(it) + val zipEntryFolder = zipEntryPath.subpath(0, zipEntryPath.nameCount - 1) + Files.createDirectories(zipEntryFolder) + Files.createFile(zipEntryPath) + } + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/SoCleanerUtilsTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/SoCleanerUtilsTest.kt new file mode 100644 index 00000000000..2c887b48fac --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/SoCleanerUtilsTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.utils + +import com.facebook.react.tests.createZip +import com.facebook.react.utils.SoCleanerUtils.clean +import com.facebook.react.utils.SoCleanerUtils.removeSoFiles +import java.io.File +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files.* +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.Test.None +import org.junit.rules.TemporaryFolder + +class SoCleanerUtilsTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun clean_withEnableHermesAndDebuggableVariant_removesCorrectly() { + val tempZip = + File(tempFolder.root, "app.apk").apply { + createZip( + this, + listOf( + "lib/x86/libhermes.so", + "lib/x86/libhermes-executor-debug.so", + "lib/x86/libhermes-executor-release.so", + "lib/x86/libjsc.so", + "lib/x86/libjscexecutor.so", + )) + } + + clean(tempZip, "lib", enableHermes = true, debuggableVariant = true) + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempZip.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertTrue(exists(it.getPath("lib/x86/libhermes.so"))) + assertTrue(exists(it.getPath("lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-release.so"))) + assertFalse(exists(it.getPath("lib/x86/libjsc.so"))) + assertFalse(exists(it.getPath("lib/x86/libjscexecutor.so"))) + } + } + + @Test + fun clean_withEnableHermesAndNonDebuggableVariant_removesCorrectly() { + val tempZip = + File(tempFolder.root, "app.apk").apply { + createZip( + this, + listOf( + "lib/x86/libhermes.so", + "lib/x86/libhermes-executor-debug.so", + "lib/x86/libhermes-executor-release.so", + "lib/x86/libjsc.so", + "lib/x86/libjscexecutor.so", + )) + } + + clean(tempZip, "lib", enableHermes = true, debuggableVariant = false) + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempZip.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertTrue(exists(it.getPath("lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-debug.so"))) + assertTrue(exists(it.getPath("lib/x86/libhermes-executor-release.so"))) + assertFalse(exists(it.getPath("lib/x86/libjsc.so"))) + assertFalse(exists(it.getPath("lib/x86/libjscexecutor.so"))) + } + } + + @Test + fun clean_withJscAndDebuggableVariant_removesCorrectly() { + val tempZip = + File(tempFolder.root, "app.apk").apply { + createZip( + this, + listOf( + "lib/x86/libhermes.so", + "lib/x86/libhermes-executor-debug.so", + "lib/x86/libhermes-executor-release.so", + "lib/x86/libjsc.so", + "lib/x86/libjscexecutor.so", + )) + } + + clean(tempZip, "lib", enableHermes = false, debuggableVariant = true) + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempZip.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertFalse(exists(it.getPath("lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-release.so"))) + assertTrue(exists(it.getPath("lib/x86/libjsc.so"))) + assertTrue(exists(it.getPath("lib/x86/libjscexecutor.so"))) + } + } + + @Test + fun clean_withJscAndNonDebuggableVariant_removesCorrectly() { + val tempZip = + File(tempFolder.root, "app.apk").apply { + createZip( + this, + listOf( + "lib/x86/libhermes.so", + "lib/x86/libhermes-executor-debug.so", + "lib/x86/libhermes-executor-release.so", + "lib/x86/libjsc.so", + "lib/x86/libjscexecutor.so", + )) + } + + clean(tempZip, "lib", enableHermes = false, debuggableVariant = false) + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempZip.absoluteFile}"), mapOf("create" to "false")) + fs.use { + assertFalse(exists(it.getPath("lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-debug.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes-executor-release.so"))) + assertTrue(exists(it.getPath("lib/x86/libjsc.so"))) + assertTrue(exists(it.getPath("lib/x86/libjscexecutor.so"))) + } + } + + @Test(expected = None::class) + fun removeSoFiles_withEmptyZip_doesNothing() { + val tempZip = File(tempFolder.root, "app.apk") + createZip(tempZip, emptyList()) + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempZip.absoluteFile}"), mapOf("create" to "false")) + val archs = listOf("x86") + val libraryToRemove = "libhello.so" + + removeSoFiles(fs, "lib", archs, libraryToRemove) + } + + @Test + fun removeSoFiles_withValidFiles_filtersThemCorrectly() { + val tempZip = File(tempFolder.root, "app.apk") + createZip( + tempZip, + listOf( + "base/lib/x86_64/libhermes.so", + "base/lib/x86/libhermes.so", + "lib/arm64-v8a/libhermes.so", + "lib/armeabi-v7a/libhermes.so", + "lib/x86/libhermes.so", + "lib/x86_64/libhermes.so", + )) + + val fs = + FileSystems.newFileSystem( + URI.create("jar:file:${tempZip.absoluteFile}"), mapOf("create" to "false")) + val archs = listOf("x86", "x86_64") + val libraryToRemove = "libhermes.so" + + removeSoFiles(fs, "lib", archs, libraryToRemove) + + fs.use { + assertTrue(exists(it.getPath("base/lib/x86/libhermes.so"))) + assertTrue(exists(it.getPath("base/lib/x86_64/libhermes.so"))) + assertTrue(exists(it.getPath("lib/arm64-v8a/libhermes.so"))) + assertTrue(exists(it.getPath("lib/armeabi-v7a/libhermes.so"))) + assertFalse(exists(it.getPath("lib/x86/libhermes.so"))) + assertFalse(exists(it.getPath("lib/x86_64/libhermes.so"))) + } + } +}