diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/BuildCodegenCLITask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/BuildCodegenCLITask.kt index 91c0080a1e3f..9586ed6af16b 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/BuildCodegenCLITask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/BuildCodegenCLITask.kt @@ -8,6 +8,7 @@ package com.facebook.react.tasks.internal import com.facebook.react.utils.Os.unixifyPath +import com.facebook.react.utils.findGitBashOnWindows import com.facebook.react.utils.windowsAwareBashCommandLine import java.io.FileOutputStream import org.gradle.api.file.DirectoryProperty @@ -53,7 +54,7 @@ abstract class BuildCodegenCLITask : Exec() { commandLine( windowsAwareBashCommandLine( codegenDir.asFile.get().canonicalPath.unixifyPath().plus(BUILD_SCRIPT_PATH), - bashWindowsHome = bashWindowsHome.orNull, + bashWindowsHome = bashWindowsHome.orNull ?: findGitBashOnWindows(), ), ) super.exec() diff --git a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/TaskUtils.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/TaskUtils.kt index a4482aba158d..fc0fa2ee21c3 100644 --- a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/TaskUtils.kt +++ b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/TaskUtils.kt @@ -7,6 +7,8 @@ package com.facebook.react.utils +import java.io.File + fun windowsAwareCommandLine(vararg args: Any): List = windowsAwareCommandLine(args.toList()) fun windowsAwareCommandLine(args: List): List = @@ -25,3 +27,27 @@ fun windowsAwareBashCommandLine( } else { args.toList() } + +/** + * Returns `\bin\bash.exe` for the first `\\git.exe` on the PATH that has one, + * or null. This is where Git for Windows keeps its bash. + * + * A bare `bash` is not reliable on Windows: the Git installer adds only `\cmd` to the PATH by + * default, and when WSL is installed `bash` resolves to the WSL launcher in `System32`, which + * Windows searches before the PATH. + */ +fun findGitBashOnWindows(path: String? = System.getenv("PATH")): String? { + if (!Os.isWindows() || path == null) { + return null + } + return path + .split(File.pathSeparatorChar) + .asSequence() + .map { it.trim().removeSurrounding("\"") } + .filter { it.isNotEmpty() } + .map { File(it, "git.exe") } + .filter { it.isFile } + .mapNotNull { it.absoluteFile.parentFile?.parentFile?.resolve("bin/bash.exe") } + .firstOrNull { it.isFile } + ?.absolutePath +} diff --git a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/TaskUtilsTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/TaskUtilsTest.kt index 276c42f7f797..f61a51af1e34 100644 --- a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/TaskUtilsTest.kt +++ b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/TaskUtilsTest.kt @@ -10,14 +10,18 @@ package com.facebook.react.utils import com.facebook.react.tests.OS import com.facebook.react.tests.OsRule import com.facebook.react.tests.WithOs +import java.io.File import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder class TaskUtilsTest { @get:Rule val osRule = OsRule() + @get:Rule val tempFolder = TemporaryFolder() + @Test fun windowsAwareCommandLine_withEmptyInput_isEmpty() { assertThat(windowsAwareCommandLine().isEmpty()).isTrue() @@ -73,4 +77,66 @@ class TaskUtilsTest { assertThat(listOf("/custom/bash", "-c", "a", "b", "c")) .isEqualTo(windowsAwareBashCommandLine("a", "b", "c", bashWindowsHome = "/custom/bash")) } + + @Test + @WithOs(OS.WIN) + fun findGitBashOnWindows_withGitOnPath_returnsBashOfThatInstall() { + val gitRoot = createGitInstall("Git") + + assertThat(findGitBashOnWindows(File(gitRoot, "cmd").absolutePath)) + .isEqualTo(File(gitRoot, "bin/bash.exe").absolutePath) + } + + @Test + @WithOs(OS.WIN) + fun findGitBashOnWindows_skipsGitWithoutBashNextToIt() { + val gitWithoutBash = tempFolder.newFolder("mingw64", "bin") + File(gitWithoutBash, "git.exe").createNewFile() + val gitRoot = createGitInstall("Git") + val path = + listOf(gitWithoutBash.absolutePath, File(gitRoot, "cmd").absolutePath) + .joinToString(File.pathSeparator) + + assertThat(findGitBashOnWindows(path)).isEqualTo(File(gitRoot, "bin/bash.exe").absolutePath) + } + + @Test + @WithOs(OS.WIN) + fun findGitBashOnWindows_withoutGitOnPath_returnsNull() { + assertThat(findGitBashOnWindows(tempFolder.newFolder("empty").absolutePath)).isNull() + } + + @Test + @WithOs(OS.WIN) + fun findGitBashOnWindows_withBashButNoGitOnPath_returnsNull() { + val toolsRoot = tempFolder.newFolder("tools") + File(toolsRoot, "cmd").mkdirs() + File(toolsRoot, "bin").mkdirs() + File(toolsRoot, "bin/bash.exe").createNewFile() + + assertThat(findGitBashOnWindows(File(toolsRoot, "cmd").absolutePath)).isNull() + } + + @Test + @WithOs(OS.WIN) + fun findGitBashOnWindows_withNullPath_returnsNull() { + assertThat(findGitBashOnWindows(null)).isNull() + } + + @Test + @WithOs(OS.LINUX) + fun findGitBashOnWindows_onLinux_returnsNull() { + val gitRoot = createGitInstall("Git") + + assertThat(findGitBashOnWindows(File(gitRoot, "cmd").absolutePath)).isNull() + } + + private fun createGitInstall(name: String): File { + val gitRoot = tempFolder.newFolder(name) + File(gitRoot, "cmd").mkdirs() + File(gitRoot, "cmd/git.exe").createNewFile() + File(gitRoot, "bin").mkdirs() + File(gitRoot, "bin/bash.exe").createNewFile() + return gitRoot + } } diff --git a/packages/react-native-codegen/scripts/oss/build.sh b/packages/react-native-codegen/scripts/oss/build.sh index b390da38098f..e96123d1da44 100755 --- a/packages/react-native-codegen/scripts/oss/build.sh +++ b/packages/react-native-codegen/scripts/oss/build.sh @@ -64,7 +64,7 @@ else # during recursive copying, and `rsync` is not installed by default in Git Bash. # As an added benefit, blob copy is faster. if [ "$OSTYPE" = "msys" ] || [ "$OSTYPE" = "cygwin" ]; then - tar cf - --exclude='*.lock' "$CODEGEN_DIR" | (cd "$TMP_DIR" && tar xvf - ); + tar cf - --exclude='*.lock' -C "$CODEGEN_DIR" . | (cd "$TMP_DIR" && tar xvf - ); else /bin/cp -R "$CODEGEN_DIR/." "$TMP_DIR"; fi diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 5d88d6f8f772..dda7f1a85f16 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -484,7 +484,7 @@ val prepareKotlinBuildScriptModel by val buildCodegenCLI by tasks.registering(BuildCodegenCLITask::class) { codegenDir.set(file("$rootDir/node_modules/@react-native/codegen")) - bashWindowsHome.set(project.findProperty("react.internal.windowsBashPath").toString()) + bashWindowsHome.set(project.findProperty("react.internal.windowsBashPath")?.toString()) logFile.set(file("$buildDir/codegen.log")) inputFiles.set(fileTree(codegenDir) { include("src/**/*.js") }) outputFiles.set(