diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 513d76c64f11..806c032ffe89 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -12,6 +12,8 @@ plugins { id("de.undercouch.download") } +import com.facebook.react.tasks.internal.* + import java.nio.file.Paths import de.undercouch.gradle.tasks.download.Download @@ -120,26 +122,12 @@ task downloadLibevent(dependsOn: createNativeDepsDirectories, type: Download) { dest(new File(downloadsDir, "libevent-${LIBEVENT_VERSION}.tar.gz")) } -task prepareLibevent(dependsOn: dependenciesPath ? [] : [downloadLibevent], type: Copy) { - from(dependenciesPath ?: tarTree(downloadLibevent.dest)) - from("src/main/jni/third-party/libevent/Android.mk") - from("src/main/jni/third-party/libevent/event-config.h") - from("src/main/jni/third-party/libevent/evconfig-private.h") - include( - "libevent-${LIBEVENT_VERSION}-stable/*.c", - "libevent-${LIBEVENT_VERSION}-stable/*.h", - "libevent-${LIBEVENT_VERSION}-stable/include/**/*", - "evconfig-private.h", - "event-config.h", - "Android.mk" - ) - eachFile { fname -> fname.path = (fname.path - "libevent-${LIBEVENT_VERSION}-stable/") } - includeEmptyDirs = false - into("$thirdPartyNdkDir/libevent") - doLast { - ant.move(file: "$thirdPartyNdkDir/libevent/event-config.h", tofile: "$thirdPartyNdkDir/libevent/include/event2/event-config.h") - } +final def prepareLibevent = tasks.register("prepareLibevent", PrepareLibeventTask) { + it.dependsOn(dependenciesPath ? [] : [downloadLibevent]) + it.libeventPath.setFrom(dependenciesPath ?: tarTree(downloadLibevent.dest)) + it.libeventVersion.set(LIBEVENT_VERSION) + it.outputDir.set(new File(thirdPartyNdkDir, "libevent")) } task prepareHermes(dependsOn: createNativeDepsDirectories, type: Copy) { @@ -169,41 +157,11 @@ task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) { // Prepare glog sources to be compiled, this task will perform steps that normally should've been // executed by automake. This way we can avoid dependencies on make/automake -task prepareGlog(dependsOn: dependenciesPath ? [] : [downloadGlog], type: Copy) { - duplicatesStrategy("warn") - from(dependenciesPath ?: tarTree(downloadGlog.dest)) - from("src/main/jni/third-party/glog/") - include("glog-${GLOG_VERSION}/src/**/*", "Android.mk", "config.h") - includeEmptyDirs = false - filesMatching("**/*.h.in") { - filter(ReplaceTokens, tokens: [ - ac_cv_have_unistd_h : "1", - ac_cv_have_stdint_h : "1", - ac_cv_have_systypes_h : "1", - ac_cv_have_inttypes_h : "1", - ac_cv_have_libgflags : "0", - ac_google_start_namespace : "namespace google {", - ac_cv_have_uint16_t : "1", - ac_cv_have_u_int16_t : "1", - ac_cv_have___uint16 : "0", - ac_google_end_namespace : "}", - ac_cv_have___builtin_expect : "1", - ac_google_namespace : "google", - ac_cv___attribute___noinline : "__attribute__ ((noinline))", - ac_cv___attribute___noreturn : "__attribute__ ((noreturn))", - ac_cv___attribute___printf_4_5: "__attribute__((__format__ (__printf__, 4, 5)))" - ]) - it.path = (it.name - ".in") - } - into("$thirdPartyNdkDir/glog") - - doLast { - copy { - from(fileTree(dir: "$thirdPartyNdkDir/glog", includes: ["stl_logging.h", "logging.h", "raw_logging.h", "vlog_is_on.h", "**/src/glog/log_severity.h"]).files) - includeEmptyDirs = false - into("$thirdPartyNdkDir/glog/exported/glog") - } - } +final def prepareGlog = tasks.register("prepareGlog", PrepareGlogTask) { + it.dependsOn(dependenciesPath ? [] : [downloadGlog]) + it.glogPath.setFrom(dependenciesPath ?: tarTree(downloadGlog.dest)) + it.glogVersion.set(GLOG_VERSION) + it.outputDir.set(new File(thirdPartyNdkDir, "glog")) } // Create Android.mk library module based on jsc from npm diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index ebad04dab807..ce9743eb3981 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -27,8 +27,8 @@ class ReactPlugin : Plugin { } private fun applyAppPlugin(project: Project, config: ReactExtension) { - if (config.applyAppPlugin.getOrElse(false)) { - project.afterEvaluate { + project.afterEvaluate { + if (config.applyAppPlugin.getOrElse(false)) { val androidConfiguration = project.extensions.getByType(BaseExtension::class.java) project.configureDevPorts(androidConfiguration) diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt index eefaef68a873..b14b138cc72b 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt @@ -165,23 +165,23 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactExte packageTask.configure { if (config.enableVmCleanup.get()) { val libDir = "$buildDir/intermediates/transforms/" - val targetVariant = ".*/transforms/[^/]*/$targetPath/.*".toRegex() + val targetVariant = ".*/transforms/[^/]*/${variant.name}/.*".toRegex() it.doFirst { cleanupVMFiles(libDir, targetVariant, enableHermes, cleanup) } } } stripDebugSymbolsTask?.configure { if (config.enableVmCleanup.get()) { - val libDir = "$buildDir/intermediates/stripped_native_libs/${targetPath}/out/lib/" - val targetVariant = ".*/stripped_native_libs/$targetPath/out/lib/.*".toRegex() + val libDir = "$buildDir/intermediates/stripped_native_libs/${variant.name}/out/lib/" + val targetVariant = ".*/stripped_native_libs/${variant.name}/out/lib/.*".toRegex() it.doLast { cleanupVMFiles(libDir, targetVariant, enableHermes, cleanup) } } } mergeNativeLibsTask?.configure { if (config.enableVmCleanup.get()) { - val libDir = "$buildDir/intermediates/merged_native_libs/${targetPath}/out/lib/" - val targetVariant = ".*/merged_native_libs/$targetPath/out/lib/.*".toRegex() + val libDir = "$buildDir/intermediates/merged_native_libs/${variant.name}/out/lib/" + val targetVariant = ".*/merged_native_libs/${variant.name}/out/lib/.*".toRegex() it.doLast { cleanupVMFiles(libDir, targetVariant, enableHermes, cleanup) } } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGlogTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGlogTask.kt new file mode 100644 index 000000000000..6344b66f4299 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGlogTask.kt @@ -0,0 +1,79 @@ +/* + * 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.apache.tools.ant.filters.ReplaceTokens +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* + +/** + * A task that takes care of extracting Glog from a source folder/zip and preparing it to be + * consumed by the NDK. This task will also take care of applying the mapping for Glog parameters. + */ +abstract class PrepareGlogTask : DefaultTask() { + + @get:InputFiles abstract val glogPath: ConfigurableFileCollection + + @get:Input abstract val glogVersion: Property + + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @TaskAction + fun taskAction() { + project.copy { + it.from(glogPath) + it.from(project.file("src/main/jni/third-party/glog/")) + it.include("glog-${glogVersion.get()}/src/**/*", "Android.mk", "config.h") + it.duplicatesStrategy = DuplicatesStrategy.WARN + it.includeEmptyDirs = false + it.filesMatching("**/*.h.in") { matchedFile -> + matchedFile.filter( + mapOf( + "tokens" to + mapOf( + "ac_cv_have_unistd_h" to "1", + "ac_cv_have_stdint_h" to "1", + "ac_cv_have_systypes_h" to "1", + "ac_cv_have_inttypes_h" to "1", + "ac_cv_have_libgflags" to "0", + "ac_google_start_namespace" to "namespace google {", + "ac_cv_have_uint16_t" to "1", + "ac_cv_have_u_int16_t" to "1", + "ac_cv_have___uint16" to "0", + "ac_google_end_namespace" to "}", + "ac_cv_have___builtin_expect" to "1", + "ac_google_namespace" to "google", + "ac_cv___attribute___noinline" to "__attribute__ ((noinline))", + "ac_cv___attribute___noreturn" to "__attribute__ ((noreturn))", + "ac_cv___attribute___printf_4_5" to + "__attribute__((__format__ (__printf__, 4, 5)))")), + ReplaceTokens::class.java) + matchedFile.path = (matchedFile.name.removeSuffix(".in")) + } + it.into(outputDir) + } + val exportedDir = File(outputDir.asFile.get(), "exported/glog/").apply { mkdirs() } + project.copy { + it.from(outputDir) + it.include( + "stl_logging.h", + "logging.h", + "raw_logging.h", + "vlog_is_on.h", + "**/src/glog/log_severity.h") + it.eachFile { file -> file.path = file.name } + it.includeEmptyDirs = false + it.into(exportedDir) + } + } +} diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareLibeventTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareLibeventTask.kt new file mode 100644 index 000000000000..416f77f17cdb --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareLibeventTask.kt @@ -0,0 +1,53 @@ +/* + * 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.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* + +/** + * A task that takes care of extracting Libevent from a source folder/zip and preparing it to be + * consumed by the NDK. + */ +abstract class PrepareLibeventTask : DefaultTask() { + + @get:InputFiles abstract val libeventPath: ConfigurableFileCollection + + @get:Input abstract val libeventVersion: Property + + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @TaskAction + fun taskAction() { + project.copy { it -> + it.from(libeventPath) + it.from(project.file("src/main/jni/third-party/libevent/Android.mk")) + it.from(project.file("src/main/jni/third-party/libevent/event-config.h")) + it.from(project.file("src/main/jni/third-party/libevent/evconfig-private.h")) + it.include( + "libevent-${libeventVersion.get()}-stable/*.c", + "libevent-${libeventVersion.get()}-stable/*.h", + "libevent-${libeventVersion.get()}-stable/include/**/*", + "evconfig-private.h", + "event-config.h", + "Android.mk") + it.eachFile { it.path = it.path.removePrefix("libevent-${libeventVersion.get()}-stable/") } + it.includeEmptyDirs = false + it.into(outputDir) + } + File(outputDir.asFile.get(), "event-config.h").apply { + val destination = + File(this.parentFile, "include/event2/event-config.h").apply { parentFile.mkdirs() } + renameTo(destination) + } + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt new file mode 100644 index 000000000000..602dd522f409 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt @@ -0,0 +1,56 @@ +/* + * 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 + +import com.android.build.gradle.AppExtension +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertTrue +import org.junit.Test + +class ReactPluginTest { + + @Test + fun reactPlugin_withApplyAppPluginSetToTrue_addsARelevantTask() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("com.android.application") + project.plugins.apply("com.facebook.react") + + project.extensions.getByType(AppExtension::class.java).apply { compileSdkVersion(30) } + project.extensions.getByType(ReactExtension::class.java).apply { + applyAppPlugin.set(true) + cliPath.set(".") + } + + // We check if the App Plugin si applied by finding one of the added task. + assertTrue(project.getTasksByName("bundleDebugJsAndAssets", false).isNotEmpty()) + } + + @Test + fun reactPlugin_withApplyAppPluginSetToFalse_doesNotApplyTheAppPlugin() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("com.android.application") + project.plugins.apply("com.facebook.react") + + project.extensions.getByType(AppExtension::class.java).apply { compileSdkVersion(30) } + project.extensions.getByType(ReactExtension::class.java).apply { applyAppPlugin.set(false) } + + assertTrue(project.getTasksByName("bundleDebugJsAndAssets", false).isEmpty()) + } + + @Test + fun reactPlugin_withApplyAppPluginSetToFalse_codegenPluginIsApplied() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("com.android.application") + project.plugins.apply("com.facebook.react") + + project.extensions.getByType(AppExtension::class.java).apply { compileSdkVersion(30) } + project.extensions.getByType(ReactExtension::class.java).apply { applyAppPlugin.set(false) } + + assertTrue(project.getTasksByName("buildCodegenCLI", false).isNotEmpty()) + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGlogTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGlogTaskTest.kt new file mode 100644 index 000000000000..782bac3dce74 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGlogTaskTest.kt @@ -0,0 +1,130 @@ +/* + * 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 java.io.* +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class PrepareGlogTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test(expected = IllegalStateException::class) + fun prepareGlogTask_withMissingConfiguration_fails() { + val task = createTestTask() + + task.taskAction() + } + + @Test + fun prepareGlogTask_copiesMakefile() { + val glogpath = tempFolder.newFolder("glogpath") + val output = tempFolder.newFolder("output") + val project = createProject() + val task = + createTestTask(project = project) { + it.glogPath.setFrom(glogpath) + it.glogVersion.set("1.0.0") + it.outputDir.set(output) + } + File(project.projectDir, "src/main/jni/third-party/glog/Android.mk").apply { + parentFile.mkdirs() + createNewFile() + } + task.taskAction() + + assertTrue(output.listFiles()!!.any { it.name == "Android.mk" }) + } + + @Test + fun prepareGlogTask_copiesConfigHeaderFile() { + val glogpath = tempFolder.newFolder("glogpath") + val output = tempFolder.newFolder("output") + val project = createProject() + val task = + createTestTask(project = project) { + it.glogPath.setFrom(glogpath) + it.glogVersion.set("1.0.0") + it.outputDir.set(output) + } + File(project.projectDir, "src/main/jni/third-party/glog/config.h").apply { + parentFile.mkdirs() + createNewFile() + } + task.taskAction() + + assertTrue(output.listFiles()!!.any { it.name == "config.h" }) + } + + @Test + fun prepareGlogTask_copiesSourceCode() { + val glogpath = tempFolder.newFolder("glogpath") + val output = tempFolder.newFolder("output") + val task = + createTestTask { + it.glogPath.setFrom(glogpath) + it.glogVersion.set("1.0.0") + it.outputDir.set(output) + } + File(glogpath, "glog-1.0.0/src/glog.cpp").apply { + parentFile.mkdirs() + createNewFile() + } + + task.taskAction() + + assertTrue(File(output, "glog-1.0.0/src/glog.cpp").exists()) + } + + @Test + fun prepareGlogTask_replacesTokenCorrectly() { + val glogpath = tempFolder.newFolder("glogpath") + val output = tempFolder.newFolder("output") + val task = + createTestTask { + it.glogPath.setFrom(glogpath) + it.glogVersion.set("1.0.0") + it.outputDir.set(output) + } + File(glogpath, "glog-1.0.0/src/glog.h.in").apply { + parentFile.mkdirs() + writeText("ac_google_start_namespace") + } + + task.taskAction() + + val expectedFile = File(output, "glog.h") + assertTrue(expectedFile.exists()) + assertEquals("ac_google_start_namespace", expectedFile.readText()) + } + + @Test + fun prepareGlogTask_exportsHeaderCorrectly() { + val glogpath = tempFolder.newFolder("glogpath") + val output = tempFolder.newFolder("output") + val task = + createTestTask { + it.glogPath.setFrom(glogpath) + it.glogVersion.set("1.0.0") + it.outputDir.set(output) + } + File(glogpath, "glog-1.0.0/src/logging.h.in").apply { + parentFile.mkdirs() + writeText("ac_google_start_namespace") + } + + task.taskAction() + + assertTrue(File(output, "exported/glog/logging.h").exists()) + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareLibeventTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareLibeventTaskTest.kt new file mode 100644 index 000000000000..7a126edb6b9d --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareLibeventTaskTest.kt @@ -0,0 +1,101 @@ +/* + * 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 java.io.* +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class PrepareLibeventTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test(expected = IllegalStateException::class) + fun prepareBoostTask_withMissingConfiguration_fails() { + val task = createTestTask() + + task.taskAction() + } + + @Test + fun prepareBoostTask_copiesMakefile() { + val libeventPath = tempFolder.newFolder("libeventPath") + val output = tempFolder.newFolder("output") + val project = createProject() + val task = + createTestTask(project = project) { + it.libeventPath.setFrom(libeventPath) + it.libeventVersion.set("1.0.0") + it.outputDir.set(output) + } + File(project.projectDir, "src/main/jni/third-party/libevent/Android.mk").apply { + parentFile.mkdirs() + createNewFile() + } + task.taskAction() + + assertTrue(File(output, "Android.mk").exists()) + } + + @Test + fun prepareBoostTask_copiesConfigFiles() { + val libeventPath = tempFolder.newFolder("libeventPath") + val output = tempFolder.newFolder("output") + val project = createProject() + val task = + createTestTask(project = project) { + it.libeventPath.setFrom(libeventPath) + it.libeventVersion.set("1.0.0") + it.outputDir.set(output) + } + File(project.projectDir, "src/main/jni/third-party/libevent/event-config.h").apply { + parentFile.mkdirs() + createNewFile() + } + File(project.projectDir, "src/main/jni/third-party/libevent/evconfig-private.h").createNewFile() + + task.taskAction() + + assertTrue(File(output, "evconfig-private.h").exists()) + assertTrue(File(output, "include/event2/event-config.h").exists()) + } + + @Test + fun prepareBoostTask_copiesSourceFiles() { + val libeventPath = tempFolder.newFolder("libeventPath") + val output = tempFolder.newFolder("output") + val task = + createTestTask { + it.libeventPath.setFrom(libeventPath) + it.libeventVersion.set("1.0.0") + it.outputDir.set(output) + } + File(libeventPath, "libevent-1.0.0-stable/sample.c").apply { + parentFile.mkdirs() + createNewFile() + } + File(libeventPath, "libevent-1.0.0-stable/sample.h").apply { + parentFile.mkdirs() + createNewFile() + } + File(libeventPath, "libevent-1.0.0-stable/include/sample.h").apply { + parentFile.mkdirs() + createNewFile() + } + + task.taskAction() + + assertTrue(File(output, "sample.c").exists()) + assertTrue(File(output, "sample.h").exists()) + assertTrue(File(output, "include/sample.h").exists()) + } +} diff --git a/react.gradle b/react.gradle index d9e27140aeaf..528b7a0c36b1 100644 --- a/react.gradle +++ b/react.gradle @@ -369,9 +369,9 @@ afterEvaluate { } } }.visit { details -> - def targetVariant1 = ".*/transforms/[^/]*/${targetPath}/.*" - def targetVariant2 = ".*/merged_native_libs/${targetPath}/out/lib/.*" - def targetVariant3 = ".*/stripped_native_libs/${targetPath}/out/lib/.*" + def targetVariant1 = ".*/transforms/[^/]*/${variant.name}/.*" + def targetVariant2 = ".*/merged_native_libs/${variant.name}/out/lib/.*" + def targetVariant3 = ".*/stripped_native_libs/${variant.name}/out/lib/.*" def path = details.file.getAbsolutePath().replace(File.separatorChar, '/' as char) if ((path.matches(targetVariant1) || path.matches(targetVariant2) || path.matches(targetVariant3)) && details.file.isFile()) { details.file.delete() @@ -386,13 +386,13 @@ afterEvaluate { def sTask = tasks.findByName("strip${targetName}DebugSymbols") if (sTask != null) { - def strippedLibDir = "$buildDir/intermediates/stripped_native_libs/${targetPath}/out/lib/" + def strippedLibDir = "$buildDir/intermediates/stripped_native_libs/${variant.name}/out/lib/" sTask.doLast { vmSelectionAction(strippedLibDir) } } def mTask = tasks.findByName("merge${targetName}NativeLibs") if (mTask != null) { - def mergedLibDir = "$buildDir/intermediates/merged_native_libs/${targetPath}/out/lib/" + def mergedLibDir = "$buildDir/intermediates/merged_native_libs/${variant.name}/out/lib/" mTask.doLast { vmSelectionAction(mergedLibDir) } } }