diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/AppleFrameworkIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/AppleFrameworkIT.kt index fd33bc90632..7aec2dd27ed 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/AppleFrameworkIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/AppleFrameworkIT.kt @@ -13,8 +13,9 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.io.TempDir import java.nio.file.Path -import kotlin.io.path.absolutePathString -import kotlin.io.path.appendText +import java.nio.file.attribute.PosixFilePermission +import kotlin.io.path.* + @OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC]) @DisplayName("Tests for K/N with Apple Framework") @@ -28,27 +29,34 @@ class AppleFrameworkIT : KGPBaseTest() { fun shouldAssembleAppleFrameworkForXcodeForIosArm64( gradleVersion: GradleVersion, ) { - nativeProject( "sharedAppleFramework", gradleVersion, - buildOptions = defaultBuildOptions, - environmentVariables = EnvironmentalVariables( + buildOptions = defaultBuildOptions + ) { + + val environmentVariables = mapOf( "CONFIGURATION" to "debug", "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" - ), - ) { + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), + ) - build("assembleDebugAppleFrameworkForXcodeIosArm64") { + build( + "assembleDebugAppleFrameworkForXcodeIosArm64", + environmentVariables = EnvironmentalVariables(environmentVariables) + ) { assertTasksExecuted(":shared:assembleDebugAppleFrameworkForXcodeIosArm64") assertDirectoryInProjectExists("shared/build/xcode-frameworks/debug/iphoneos123/sdk.framework") assertDirectoryInProjectExists("shared/build/xcode-frameworks/debug/iphoneos123/sdk.framework.dSYM") } - build("assembleCustomDebugAppleFrameworkForXcodeIosArm64") { + build( + "assembleCustomDebugAppleFrameworkForXcodeIosArm64", + environmentVariables = EnvironmentalVariables(environmentVariables) + ) { assertTasksExecuted(":shared:assembleCustomDebugAppleFrameworkForXcodeIosArm64") assertDirectoryInProjectExists("shared/build/xcode-frameworks/debug/iphoneos123/lib.framework") assertDirectoryInProjectExists("shared/build/xcode-frameworks/debug/iphoneos123/lib.framework.dSYM") @@ -73,7 +81,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphonesimulator", "ARCHS" to "arm64 x86_64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) build("assembleReleaseAppleFrameworkForXcode", environmentVariables = EnvironmentalVariables(environmentVariables)) { assertTasksExecuted(":shared:linkReleaseFrameworkIosSimulatorArm64") @@ -103,7 +112,8 @@ class AppleFrameworkIT : KGPBaseTest() { "ARCHS" to "x86_64", "EXPANDED_CODE_SIGN_IDENTITY" to "-", "TARGET_BUILD_DIR" to testBuildDir.toString(), - "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived" + "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) build(":shared:embedAndSignAppleFrameworkForXcode", environmentVariables = EnvironmentalVariables(environmentVariables)) { assertTasksExecuted(":shared:assembleDebugAppleFrameworkForXcodeMacosX64") @@ -129,7 +139,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to projectPath.absolutePathString(), - "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived" + "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) build(":shared:embedAndSignAppleFrameworkForXcode", environmentVariables = EnvironmentalVariables(environmentVariables)) { assertDirectoryInProjectExists("build/xcode-derived/sdk.framework") @@ -154,7 +165,8 @@ class AppleFrameworkIT : KGPBaseTest() { "ARCHS" to "arm64", "EXPANDED_CODE_SIGN_IDENTITY" to "-", "TARGET_BUILD_DIR" to projectPath.absolutePathString(), - "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived" + "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) build(":shared:embedAndSignAppleFrameworkForXcode", environmentVariables = EnvironmentalVariables(environmentVariables)) { assertDirectoryInProjectExists("build/xcode-derived/sdk.framework") @@ -192,7 +204,8 @@ class AppleFrameworkIT : KGPBaseTest() { "ARCHS" to "arm64", "EXPANDED_CODE_SIGN_IDENTITY" to "-", "TARGET_BUILD_DIR" to testBuildDir.toString(), - "FRAMEWORKS_FOLDER_PATH" to "testFrameworksDir" + "FRAMEWORKS_FOLDER_PATH" to "testFrameworksDir", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) buildAndAssertAllTasks( registeredTasks = listOf( @@ -229,14 +242,44 @@ class AppleFrameworkIT : KGPBaseTest() { "EXPANDED_CODE_SIGN_IDENTITY" to "-", "TARGET_BUILD_DIR" to projectPath.absolutePathString(), "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived", - "ENABLE_USER_SCRIPT_SANDBOXING" to "YES" + "ENABLE_USER_SCRIPT_SANDBOXING" to "YES", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) buildAndFail( ":shared:embedAndSignAppleFrameworkForXcode", environmentVariables = EnvironmentalVariables(environmentVariables) ) { - assertTasksFailed(":shared:embedAndSignAppleFrameworkForXcode") - assertOutputContains("Sandboxing for user scripts is currently enabled") + assertTasksFailed(":shared:checkSandboxAndWriteProtection") + assertOutputContains("You have sandboxing for user scripts enabled.") + } + } + } + + @DisplayName("embedAndSignAppleFrameworkForXcode was registered with missing BUILT_PRODUCTS_DIR directory") + @OptIn(EnvironmentalVariablesOverride::class) + @GradleTest + fun shouldFailEmbedAndSignAppleFrameworkForXcodeWithMissingBuildProductsDir( + gradleVersion: GradleVersion, + ) { + nativeProject( + "sharedAppleFramework", + gradleVersion, + ) { + val environmentVariables = mapOf( + "CONFIGURATION" to "Debug", + "SDK_NAME" to "iphoneos", + "ARCHS" to "arm64", + "EXPANDED_CODE_SIGN_IDENTITY" to "-", + "TARGET_BUILD_DIR" to projectPath.absolutePathString(), + "FRAMEWORKS_FOLDER_PATH" to "build/xcode-derived", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir(true).absolutePathString(), + ) + buildAndFail( + ":shared:embedAndSignAppleFrameworkForXcode", + environmentVariables = EnvironmentalVariables(environmentVariables) + ) { + assertTasksFailed(":shared:checkSandboxAndWriteProtection") + assertOutputContains("BUILT_PRODUCTS_DIR is not accessible, probably you have sandboxing for user scripts enabled.") } } } @@ -255,7 +298,8 @@ class AppleFrameworkIT : KGPBaseTest() { mapOf( "CONFIGURATION" to "Debug", "SDK_NAME" to "iphoneos", - "ARCHS" to "arm64" + "ARCHS" to "arm64", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) buildAndAssertAllTasks( @@ -303,7 +347,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) @@ -339,7 +384,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) @@ -375,7 +421,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) @@ -405,7 +452,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) @@ -435,7 +483,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) @@ -469,7 +518,8 @@ class AppleFrameworkIT : KGPBaseTest() { "SDK_NAME" to "iphoneos123", "ARCHS" to "arm64", "TARGET_BUILD_DIR" to "no use", - "FRAMEWORKS_FOLDER_PATH" to "no use" + "FRAMEWORKS_FOLDER_PATH" to "no use", + "BUILT_PRODUCTS_DIR" to iosBuildProductsDir().absolutePathString(), ) ) @@ -534,4 +584,17 @@ class AppleFrameworkIT : KGPBaseTest() { } } } +} + +private val GradleProject.darwinBuildProductsDir: Path + get() = projectPath.resolve("DerivedSources").apply { + if (notExists()) { + createDirectory() + } + } + +private fun GradleProject.iosBuildProductsDir(writeProtected: Boolean = false): Path = darwinBuildProductsDir.apply { + if (writeProtected) { + setPosixFilePermissions(setOf(PosixFilePermission.OWNER_READ)) + } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt index 487e6204482..52baa4226a4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt @@ -30,11 +30,14 @@ import org.jetbrains.kotlin.gradle.utils.mapToFile import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly import java.io.File +import java.io.IOException +import java.nio.file.Files import javax.inject.Inject internal object AppleXcodeTasks { const val embedAndSignTaskPrefix = "embedAndSign" const val embedAndSignTaskPostfix = "AppleFrameworkForXcode" + const val checkSandboxAndWriteProtection = "checkSandboxAndWriteProtection" } private object XcodeEnvironment { @@ -74,9 +77,15 @@ private object XcodeEnvironment { return File(xcodeTargetBuildDir, xcodeFrameworksFolderPath).absoluteFile } + val buildProductsDir: File? + get() { + val derivedFileDir = System.getenv("BUILT_PRODUCTS_DIR") ?: return null + return File(derivedFileDir).absoluteFile + } + val sign: String? get() = System.getenv("EXPANDED_CODE_SIGN_IDENTITY") - val userScriptSandboxingEnabled: String? get() = System.getenv("ENABLE_USER_SCRIPT_SANDBOXING") + val userScriptSandboxingEnabled: Boolean get() = System.getenv("ENABLE_USER_SCRIPT_SANDBOXING") == "YES" override fun toString() = """ XcodeEnvironment: @@ -84,7 +93,9 @@ private object XcodeEnvironment { targets=$targets frameworkSearchDir=$frameworkSearchDir embeddedFrameworksDir=$embeddedFrameworksDir + buildProductsDir=$buildProductsDir sign=$sign + userScriptSandboxingEnabled=$userScriptSandboxingEnabled """.trimIndent() } @@ -143,6 +154,63 @@ private fun Project.registerAssembleAppleFrameworkTask(framework: Framework): Ta } } +private fun fireEnvException(frameworkTaskName: String) { + val envBuildType = XcodeEnvironment.buildType + val envConfiguration = System.getenv("CONFIGURATION") + if (envConfiguration != null && envBuildType == null) { + throw IllegalStateException( + "Unable to detect Kotlin framework build type for CONFIGURATION=$envConfiguration automatically. " + + "Specify 'KOTLIN_FRAMEWORK_BUILD_TYPE' to 'debug' or 'release'" + + "\n$XcodeEnvironment" + ) + } else { + throw IllegalStateException( + "Please run the $frameworkTaskName task from Xcode " + + "('SDK_NAME', 'CONFIGURATION', 'TARGET_BUILD_DIR', 'ARCHS' and 'FRAMEWORKS_FOLDER_PATH' not provided)" + + "\n$XcodeEnvironment" + ) + } +} + +private fun fireSandboxException(frameworkTaskName: String) { + val userScriptSandboxingEnabled = XcodeEnvironment.userScriptSandboxingEnabled + val message = if (userScriptSandboxingEnabled) "You " else "BUILT_PRODUCTS_DIR is not accessible, probably you " + throw IllegalStateException( + message + + "have sandboxing for user scripts enabled." + + "\nTo make the $frameworkTaskName task pass, disable this feature. " + + "\nIn your Xcode project, navigate to \"Build Setting\", " + + "and under \"Build Options\" set \"User script sandboxing\" (ENABLE_USER_SCRIPT_SANDBOXING) to \"NO\". " + + "\nThen, run \"./gradlew --stop\" to stop the Gradle daemon" + + "\nFor more information, see documentation: https://jb.gg/ltd9e6" + ) +} + +private enum class DirAccessibility { + ACCESSIBLE, + NOT_ACCESSIBLE, + DOES_NOT_EXIST +} + +private fun buildProductsDirAccessibility(): DirAccessibility { + val buildProductsDir = XcodeEnvironment.buildProductsDir + + return if (buildProductsDir != null) { + try { + Files.createDirectories(buildProductsDir.toPath()) + val tempFile = File.createTempFile("sandbox", ".tmp", buildProductsDir) + if (tempFile.exists()) { + tempFile.delete() + } + DirAccessibility.ACCESSIBLE + } catch (e: IOException) { + DirAccessibility.NOT_ACCESSIBLE + } + } else { + DirAccessibility.DOES_NOT_EXIST + } +} + internal fun Project.registerEmbedAndSignAppleFrameworkTask(framework: Framework) { val envBuildType = XcodeEnvironment.buildType val envTargets = XcodeEnvironment.targets @@ -151,48 +219,37 @@ internal fun Project.registerEmbedAndSignAppleFrameworkTask(framework: Framework val envSign = XcodeEnvironment.sign val userScriptSandboxingEnabled = XcodeEnvironment.userScriptSandboxingEnabled - val frameworkTaskName = lowerCamelCaseName(AppleXcodeTasks.embedAndSignTaskPrefix, framework.namePrefix, AppleXcodeTasks.embedAndSignTaskPostfix) + val frameworkTaskName = lowerCamelCaseName( + AppleXcodeTasks.embedAndSignTaskPrefix, + framework.namePrefix, + AppleXcodeTasks.embedAndSignTaskPostfix + ) if (envBuildType == null || envTargets.isEmpty() || envEmbeddedFrameworksDir == null || envFrameworkSearchDir == null) { locateOrRegisterTask(frameworkTaskName) { task -> task.group = BasePlugin.BUILD_GROUP task.description = "Embed and sign ${framework.namePrefix} framework as requested by Xcode's environment variables" task.doFirst { - val envConfiguration = System.getenv("CONFIGURATION") - if (envConfiguration != null && envBuildType == null) { - throw IllegalStateException( - "Unable to detect Kotlin framework build type for CONFIGURATION=$envConfiguration automatically. " + - "Specify 'KOTLIN_FRAMEWORK_BUILD_TYPE' to 'debug' or 'release'" + - "\n$XcodeEnvironment" - ) - } else { - throw IllegalStateException( - "Please run the $frameworkTaskName task from Xcode " + - "('SDK_NAME', 'CONFIGURATION', 'TARGET_BUILD_DIR', 'ARCHS' and 'FRAMEWORKS_FOLDER_PATH' not provided)" + - "\n$XcodeEnvironment" - ) - } + fireEnvException(frameworkTaskName) } } return } - if (userScriptSandboxingEnabled == "YES") { - locateOrRegisterTask(frameworkTaskName) { task -> - task.group = BasePlugin.BUILD_GROUP - task.description = "Embed and sign ${framework.namePrefix} framework as requested by Xcode's environment variables" - task.doFirst { - throw IllegalStateException( - "You have sandboxing for user scripts enabled. " + - "\nTo make the $frameworkTaskName task pass, disable this feature. " + - "\nIn your Xcode project, navigate to \"Build Setting\", " + - "and under \"Build Options\" set \"User script sandboxing\" (ENABLE_USER_SCRIPT_SANDBOXING) to \"NO\". " + - "\nThen, run \"./gradlew --stop\" to stop the Gradle daemon" + - "\nFor more information, see documentation: https://jb.gg/ltd9e6" - ) + val checkSandboxAndWriteProtectionTask = locateOrRegisterTask(AppleXcodeTasks.checkSandboxAndWriteProtection) { task -> + task.group = BasePlugin.BUILD_GROUP + task.description = "Check BUILT_PRODUCTS_DIR accessible and ENABLE_USER_SCRIPT_SANDBOXING not enabled" + task.doFirst { + val dirAccessible = buildProductsDirAccessibility() + when (dirAccessible) { + DirAccessibility.NOT_ACCESSIBLE -> fireSandboxException(frameworkTaskName) + DirAccessibility.DOES_NOT_EXIST, + DirAccessibility.ACCESSIBLE + -> if (userScriptSandboxingEnabled) { + fireSandboxException(frameworkTaskName) + } } } - return } val embedAndSignTask = locateOrRegisterTask(frameworkTaskName) { task -> @@ -203,12 +260,10 @@ internal fun Project.registerEmbedAndSignAppleFrameworkTask(framework: Framework property("type", envBuildType) property("targets", envTargets) property("embeddedFrameworksDir", envEmbeddedFrameworksDir) + property("userScriptSandboxingEnabled", userScriptSandboxingEnabled) if (envSign != null) { property("sign", envSign) } - if (userScriptSandboxingEnabled != null) { - property("userScriptSandboxingEnabled", userScriptSandboxingEnabled) - } } } @@ -217,6 +272,7 @@ internal fun Project.registerEmbedAndSignAppleFrameworkTask(framework: Framework embedAndSignTask.configure { task -> val frameworkFile = framework.outputFile + task.dependsOn(checkSandboxAndWriteProtectionTask) task.dependsOn(assembleTask) task.sourceFramework.fileProvider(appleFrameworkDir(envFrameworkSearchDir).map { it.resolve(frameworkFile.name) }) task.destinationDirectory.set(envEmbeddedFrameworksDir)