diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index da9a786b3a6..5994a02f3fe 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -161,35 +161,11 @@ final def prepareGlog = tasks.register("prepareGlog", PrepareGlogTask) { } // Create Android.mk library module based on jsc from npm -task prepareJSC { - doLast { - def jscPackagePath = findNodeModulePath(projectDir, "jsc-android") - if (!jscPackagePath) { - throw new GradleScriptException("Could not find the jsc-android npm package", null) - } - - def jscDist = file("$jscPackagePath/dist") - if (!jscDist.exists()) { - throw new GradleScriptException("The jsc-android npm package is missing its \"dist\" directory", null) - } - - def jscAAR = fileTree(jscDist).matching({ it.include "**/android-jsc/**/*.aar" }).singleFile - def soFiles = zipTree(jscAAR).matching({ it.include "**/*.so" }) - - def headerFiles = fileTree(jscDist).matching({ it.include "**/include/*.h" }) - - copy { - from(soFiles) - from(headerFiles) - from("src/main/jni/third-party/jsc/Android.mk") - - filesMatching("**/*.h", { it.path = "JavaScriptCore/${it.name}" }) - - includeEmptyDirs(false) - into("$thirdPartyNdkDir/jsc") - } - } +tasks.register('prepareJSC', PrepareJSCTask) { + it.jscPackagePath.set(findNodeModulePath(projectDir, "jsc-android")) + it.outputDir = project.layout.buildDirectory.dir("third-party-ndk/jsc") } + task downloadNdkBuildDependencies { if (!boostPath) { dependsOn(downloadBoost) diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareJSCTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareJSCTask.kt new file mode 100644 index 00000000000..b4701df4223 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareJSCTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.internal + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* + +/** + * A task that takes care of unbundling JSC and preparing it for be consumed by the Android NDK. + * Specifically it will unbundle shared libs, headers and will copy over the Makefile from + * `src/main/jni/third-party/jsc/` + */ +abstract class PrepareJSCTask : DefaultTask() { + + @get:Input abstract val jscPackagePath: Property + + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @TaskAction + fun taskAction() { + if (!jscPackagePath.isPresent || jscPackagePath.orNull == null) { + error("Could not find the jsc-android npm package") + } + val jscDist = File(jscPackagePath.get(), "dist") + if (!jscDist.exists()) { + error("The jsc-android npm package is missing its \"dist\" directory") + } + val jscAAR = + project.fileTree(jscDist).matching { it.include("**/android-jsc/**/*.aar") }.singleFile + val soFiles = project.zipTree(jscAAR).matching { it.include("**/*.so") } + val headerFiles = project.fileTree(jscDist).matching { it.include("**/include/*.h") } + + project.copy { it -> + it.from(soFiles) + it.from(headerFiles) + it.from(project.file("src/main/jni/third-party/jsc/Android.mk")) + it.filesMatching("**/*.h") { it.path = "JavaScriptCore/${it.name}" } + it.includeEmptyDirs = false + it.into(outputDir) + } + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareJSCTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareJSCTaskTest.kt new file mode 100644 index 00000000000..f420c80d00b --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareJSCTaskTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.internal + +import com.facebook.react.tests.createProject +import com.facebook.react.tests.createTestTask +import com.facebook.react.tests.zipFiles +import java.io.* +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class PrepareJSCTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test(expected = IllegalStateException::class) + fun prepareJSCTask_withMissingPackage_fails() { + val task = createTestTask() + + task.taskAction() + } + + @Test(expected = IllegalStateException::class) + fun prepareJSCTask_withNullPackage_fails() { + val task = createTestTask { it.jscPackagePath.set(null as String?) } + + task.taskAction() + } + + @Test(expected = IllegalStateException::class) + fun prepareJSCTask_withMissingDistFolder_fails() { + val task = + createTestTask { it.jscPackagePath.set(tempFolder.root.absolutePath) } + + task.taskAction() + } + + @Test + fun prepareJSCTask_ignoresEmptyDirs() { + prepareInputFolder() + val output = tempFolder.newFolder("output") + File(tempFolder.root, "dist/just/an/empty/folders/").apply { mkdirs() } + + val task = + createTestTask { + it.jscPackagePath.set(tempFolder.root.absolutePath) + it.outputDir.set(output) + } + + task.taskAction() + + assertFalse(File(output, "just/an/empty/folders/").exists()) + } + + @Test + fun prepareJSCTask_copiesSoFiles() { + val soFile = tempFolder.newFile("libsomething.so") + prepareInputFolder(aarContent = listOf(soFile)) + val output = tempFolder.newFolder("output") + + val task = + createTestTask { + it.jscPackagePath.set(tempFolder.root.absolutePath) + it.outputDir.set(output) + } + + task.taskAction() + + assertEquals("libsomething.so", output.listFiles()?.first()?.name) + } + + @Test + fun prepareJSCTask_copiesHeaderFilesToCorrectFolder() { + prepareInputFolder() + File(tempFolder.root, "dist/include/justaheader.h").apply { + parentFile.mkdirs() + createNewFile() + } + val output = tempFolder.newFolder("output") + + val task = + createTestTask { + it.jscPackagePath.set(tempFolder.root.absolutePath) + it.outputDir.set(output) + } + + task.taskAction() + + assertTrue(File(output, "JavaScriptCore/justaheader.h").exists()) + } + + @Test + fun prepareJSCTask_copiesMakefile() { + val project = createProject() + prepareInputFolder() + File(project.projectDir, "src/main/jni/third-party/jsc/Android.mk").apply { + parentFile.mkdirs() + createNewFile() + } + val output = tempFolder.newFolder("output") + + val task = + createTestTask(project = project) { + it.jscPackagePath.set(tempFolder.root.absolutePath) + it.outputDir.set(output) + } + + task.taskAction() + + assertTrue(File(output, "Android.mk").exists()) + } + + private fun prepareInputFolder(aarContent: List = listOf(tempFolder.newFile())) { + val dist = tempFolder.newFolder("dist") + File(dist, "android-jsc/android-library.aar").apply { + parentFile.mkdirs() + createNewFile() + } + zipFiles(File(dist, "android-jsc/android-library.aar"), aarContent) + } +} 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 67e89e1fa14..57d247ee99f 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 @@ -7,6 +7,9 @@ package com.facebook.react.tests +import java.io.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import org.gradle.api.Project import org.gradle.api.Task import org.gradle.testfixtures.ProjectBuilder @@ -23,3 +26,18 @@ internal inline fun createTestTask( taskName: String = T::class.java.simpleName, crossinline block: (T) -> Unit = {} ): T = project.tasks.register(taskName, T::class.java) { block(it) }.get() + +/** A util function to zip a list of files from [contents] inside the zipfile at [destination]. */ +internal fun zipFiles(destination: File, contents: List) { + ZipOutputStream(BufferedOutputStream(FileOutputStream(destination.absolutePath))).use { out -> + for (file in contents) { + FileInputStream(file).use { fi -> + BufferedInputStream(fi).use { origin -> + val entry = ZipEntry(file.name) + out.putNextEntry(entry) + origin.copyTo(out, 1024) + } + } + } + } +}