diff --git a/build.gradle b/build.gradle index 47c45b3..99c31a0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,21 +1,5 @@ buildscript { - ext.versions = [ - kotlin: '1.3.20', - ] - - ext.deps = [ - plugins: [ - kotlin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", - ], - - kotlin: [ - stdlib: [ - jdk: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}", - ], - ], - junit: 'junit:junit:4.12', - truth: 'com.google.truth:truth:0.42', - ] + apply from: "$rootDir/gradle/dependencies.gradle" repositories { mavenCentral() diff --git a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsCompileTask.kt b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsCompileTask.kt new file mode 100644 index 0000000..b465d27 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsCompileTask.kt @@ -0,0 +1,93 @@ +package com.alecstrong.cocoapods.gradle.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink +import org.jetbrains.kotlin.konan.target.Architecture +import java.io.File + +open class CocoapodsCompileTask : DefaultTask() { + internal lateinit var buildType: NativeBuildType + internal lateinit var compilations: Collection + + @TaskAction + fun compileFatFramework() { + compileFatBinary( + binaryPath = project.name, + bundleName = "${project.name}.framework" + ) + } + + @TaskAction + fun compileFatDsym() { + compileFatBinary( + binaryPath = "Contents/Resources/DWARF/${project.name}", + bundleName = "${project.name}.framework.dSYM" + ) + } + + private fun compileFatBinary( + binaryPath: String, + bundleName: String + ) { + val finalContainerPath = "${project.buildDir.path}/$bundleName" + val finalOutputPath = "$finalContainerPath/$binaryPath" + + var deviceParentDir: String? = null + + project.exec { exec -> + File(finalOutputPath).parentFile.mkdirs() + + val args = mutableListOf("-create") + + compilations.forEach { compilation -> + val output = compilation.outputFile.get().parentFile.absolutePath + val target = compilation.binary.target.konanTarget + if (target.architecture == Architecture.ARM64) { + deviceParentDir = output + } + + args.addAll(listOf( + "-arch", target.architecture(), "$output/$bundleName/$binaryPath" + )) + } + + args.addAll(listOf( + "-output", finalOutputPath + )) + + exec.executable = "lipo" + exec.args = args + exec.isIgnoreExitValue = true + } + + if (deviceParentDir == null) { + throw IllegalStateException("You need to have a compilation target for X64") + } + + val initialContainer = "$deviceParentDir/$bundleName" + project.copy { copy -> + copy.from(initialContainer) { from -> + from.exclude(binaryPath) + } + copy.into(finalContainerPath) + } + + // clean plist (only works for frameworks) + val plistPath = "$finalContainerPath/Info.plist" + if (File(plistPath).exists()) { + project.exec { exec -> + exec.executable = "/usr/libexec/PlistBuddy" + exec.args = listOf("-c", "Delete :UIRequiredDeviceCapabilities", plistPath) + exec.isIgnoreExitValue = true + } + + project.exec { exec -> + exec.executable = "/usr/libexec/PlistBuddy" + exec.args = listOf("-c", "Add :CFBundleSupportedPlatforms:1 string iPhoneSimulator", plistPath) + exec.isIgnoreExitValue = true + } + } + } +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsPlugin.kt b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsPlugin.kt index a1bd730..46327b0 100644 --- a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsPlugin.kt +++ b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsPlugin.kt @@ -2,22 +2,16 @@ package com.alecstrong.cocoapods.gradle.plugin import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType -import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink -import org.jetbrains.kotlin.konan.target.Architecture -import org.jetbrains.kotlin.konan.target.Family -import org.jetbrains.kotlin.konan.target.KonanTarget -import java.io.File +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeOutputKind open class CocoapodsPlugin : Plugin { override fun apply(project: Project) { val extension = project.extensions.create("cocoapods", CocoapodsExtension::class.java) + project.extensions.add("cocoapodsPreset", CocoapodsTargetPreset(project)) + project.afterEvaluate { project.tasks.register("generatePodspec", GeneratePodspecTask::class.java) { task -> task.group = GROUP @@ -37,154 +31,26 @@ open class CocoapodsPlugin : Plugin { task.description = "Create a dummy dynamic framework to be used during Cocoapods installation" } - createFatFrameworkTasks(project) - } - } - - private fun createFatFrameworkTasks(project: Project) { - val mppExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) - - mppExtension.targets - .flatMap { target -> - target.compilations - .filterIsInstance() - .filter { !it.isTestCompilation } - .flatMap { compilation -> - target.components.flatMap { component -> - (component.target as KotlinNativeTarget).binaries.map { binary -> - compilation to binary - } - } - } - } - .filter { (_, binary) -> - binary.target.konanTarget.family == Family.IOS - } - .flatMap { (compilation, binary) -> - compilation.buildTypes.map { buildType -> - Configuration( - buildType = buildType, - linkTask = binary.linkTask - ) - } - } - .groupBy { it.buildType } - .forEach { (buildType, configurations) -> - val dsym = project.registerTaskFor( - buildType = buildType, - configurations = configurations, - binaryPath = "Contents/Resources/DWARF/${project.name}", - bundleName = "${project.name}.framework.dSYM", - taskSuffix = "DSYM" - ) - val framework = project.registerTaskFor( - buildType = buildType, - configurations = configurations, - binaryPath = project.name, - bundleName = "${project.name}.framework", - taskSuffix = "Framework" - ) - project.tasks.register("createIos${buildType.name()}Artifacts") { task -> - task.dependsOn(dsym, framework) - } - } - } - - private fun Project.registerTaskFor( - buildType: NativeBuildType, - configurations: Collection, - binaryPath: String, - bundleName: String, - taskSuffix: String - ): TaskProvider { - return tasks.register("createIos${buildType.name()}Fat$taskSuffix") { task -> - task.dependsOn(configurations.map { it.linkTask }) - - task.doLast { - // put together final path - val finalContainerParentDir = property(POD_FRAMEWORK_DIR_ENV) as String - val finalContainerPath = "$finalContainerParentDir/$bundleName" - val finalOutputPath = "$finalContainerPath/$binaryPath" - - var deviceParentDir: String? = null - - exec { exec -> - File(finalOutputPath).parentFile.mkdirs() - - val args = mutableListOf("-create") - - configurations.forEach { (_, linkTask) -> - val output = linkTask.outputFile.get().parentFile.absolutePath - val target = linkTask.compilation.target.konanTarget - if (target.architecture == Architecture.ARM64) { - deviceParentDir = output + val mppExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + mppExtension.targets + .filterIsInstance() + .flatMap { it.binaries } + .filter { it.outputKind == NativeOutputKind.FRAMEWORK } + .map { it.buildType to it.linkTask } + .groupBy({ it.first }, { it.second }) + .forEach { (buildType, compilations) -> + project.tasks.register("createIos${buildType.name()}Artifacts", + CocoapodsCompileTask::class.java) { task -> + task.dependsOn(compilations) + task.buildType = buildType + task.compilations = compilations + task.group = GROUP } - - args.addAll(listOf( - "-arch", target.architecture(), "$output/$bundleName/$binaryPath" - )) } - - args.addAll(listOf( - "-output", finalOutputPath - )) - - exec.executable = "lipo" - exec.args = args - exec.isIgnoreExitValue = true - } - - if (deviceParentDir == null) { - throw IllegalStateException("You need to have a compilation target for X64") - } - - val initialContainer = "$deviceParentDir/$bundleName" - copy { copy -> - copy.from(initialContainer) { from -> - from.exclude(binaryPath) - } - copy.into(finalContainerPath) - } - - // clean plist (only works for frameworks) - val plistPath = "$finalContainerPath/Info.plist" - if (File(plistPath).exists()) { - exec { exec -> - exec.executable = "/usr/libexec/PlistBuddy" - exec.args = listOf("-c", "Delete :UIRequiredDeviceCapabilities", plistPath) - exec.isIgnoreExitValue = true - } - - exec { exec -> - exec.executable = "/usr/libexec/PlistBuddy" - exec.args = listOf("-c", "Add :CFBundleSupportedPlatforms:1 string iPhoneSimulator", plistPath) - exec.isIgnoreExitValue = true - } - } - } } } - private data class Configuration( - val buildType: NativeBuildType, - val linkTask: KotlinNativeLink - ) - - private fun KonanTarget.architecture() = when (this) { - is KonanTarget.IOS_X64 -> "x86_64" - is KonanTarget.IOS_ARM64 -> "arm64" - is KonanTarget.IOS_ARM32 -> "arm32" - else -> throw IllegalStateException("Cannot collapse non-ios target $this into descriptor.") - } - - private fun NativeBuildType.name() = when (this) { - NativeBuildType.RELEASE -> "Release" - NativeBuildType.DEBUG -> "Debug" - } - companion object { - private const val GROUP = "cocoapods" - - internal const val POD_FRAMEWORK_DIR_ENV = "POD_FRAMEWORK_DIR" + internal const val GROUP = "cocoapods" } } diff --git a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsTargetPreset.kt b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsTargetPreset.kt new file mode 100644 index 0000000..d76deb9 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsTargetPreset.kt @@ -0,0 +1,61 @@ +package com.alecstrong.cocoapods.gradle.plugin + +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeOutputKind.EXECUTABLE + +class CocoapodsTargetPreset( + private val project: Project +) : KotlinTargetPreset { + override fun createTarget(name: String): KotlinNativeTarget { + val extension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val simulator = (extension.targetFromPreset( + extension.presets.getByName("iosX64"), name + ) as KotlinNativeTarget).apply { + binaries { + framework() + } + } + + val device = (extension.targetFromPreset( + extension.presets.getByName("iosArm64"), "${name}Device" + ) as KotlinNativeTarget).apply { + binaries { + framework { + embedBitcode("disable") + } + } + } + + configureSources(name, device.compilations) + + project.tasks.register("${name}Test", CocoapodsTestTask::class.java) { task -> + task.dependsOn(simulator.compilations.getByName("test").getLinkTask(EXECUTABLE, DEBUG)) + task.group = CocoapodsPlugin.GROUP + task.description = "Run tests for target '$name' on an iOS Simulator" + task.target = simulator + } + + return simulator + } + + override fun getName() = "Cocoapods" + + private fun configureSources(name: String, compilations: NamedDomainObjectContainer) { + val extension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + + project.afterEvaluate { + compilations.named("main").configure { compilation -> + extension.sourceSets.findByName("${name}Main")?.let(compilation::source) + } + compilations.named("test").configure { compilation -> + extension.sourceSets.findByName("${name}Test")?.let(compilation::source) + } + } + } +} diff --git a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsTestTask.kt b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsTestTask.kt new file mode 100644 index 0000000..e2c606c --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/CocoapodsTestTask.kt @@ -0,0 +1,22 @@ +package com.alecstrong.cocoapods.gradle.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeOutputKind.EXECUTABLE + +open class CocoapodsTestTask : DefaultTask() { + internal lateinit var target: KotlinNativeTarget + + @Input var device: String = "iPhone 8" + + @TaskAction + fun performTest() { + val binary = target.compilations.getByName("test").getBinary(EXECUTABLE, DEBUG) + project.exec { exec -> + exec.commandLine("xcrun", "simctl", "spawn", device, binary.absolutePath) + } + } +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/Compatibility.kt b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/Compatibility.kt new file mode 100644 index 0000000..1cd6193 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/Compatibility.kt @@ -0,0 +1,16 @@ +package com.alecstrong.cocoapods.gradle.plugin + +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.konan.target.KonanTarget + +internal fun NativeBuildType.name() = when (this) { + NativeBuildType.RELEASE -> "Release" + NativeBuildType.DEBUG -> "Debug" +} + +internal fun KonanTarget.architecture() = when (this) { + is KonanTarget.IOS_X64 -> "x86_64" + is KonanTarget.IOS_ARM64 -> "arm64" + is KonanTarget.IOS_ARM32 -> "arm32" + else -> throw IllegalStateException("Cannot collapse non-ios target $this into descriptor.") +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/GeneratePodspecTask.kt b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/GeneratePodspecTask.kt index 8a11c88..67969a9 100644 --- a/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/GeneratePodspecTask.kt +++ b/gradle-plugin/src/main/kotlin/com/alecstrong/cocoapods/gradle/plugin/GeneratePodspecTask.kt @@ -33,8 +33,6 @@ open class GeneratePodspecTask : DefaultTask() { File(project.projectDir, "${project.name}.podspec").writeText(""" |Pod::Spec.new do |spec| - | framework_dir = "${'$'}{PODS_TARGET_SRCROOT}/${project.buildDir.name}" - | | spec.name = '${project.name}' | spec.version = '$version' | ${if (homepage != null) "spec.homepage = '$homepage'" else "# homepage can be provided from gradle"} @@ -45,9 +43,6 @@ open class GeneratePodspecTask : DefaultTask() { | ${if (summary != null) "spec.summary = '$summary'" else "# summary can be provided from gradle"} | spec.ios.vendored_frameworks = "${project.buildDir.name}/#{spec.name}.framework" | - | preserve_path_patterns = ['*.gradle', 'gradle*', '*.properties', 'src/**/*.*'] - | spec.preserve_paths = preserve_path_patterns + ['src/**/*'] # also include empty dirs for full hierarchy - | | spec.prepare_command = <<-SCRIPT | set -ev | $gradlew ${if (daemon) "" else "--no-daemon" } -P${InitializeFrameworkTask.FRAMEWORK_PROPERTY}=#{spec.name}.framework initializeFramework --stacktrace @@ -56,14 +51,12 @@ open class GeneratePodspecTask : DefaultTask() { | spec.script_phases = [ | { | :name => 'Build ${project.name}', - | :input_files => Dir.glob(preserve_path_patterns).map {|f| "${'$'}(PODS_TARGET_SRCROOT)/#{f}"}, - | :output_files => ["#{framework_dir}/#{spec.name}.framework", "#{framework_dir}/#{spec.name}.dSYM"], | :shell_path => '/bin/sh', | :script => <<-SCRIPT | set -ev | REPO_ROOT=`realpath "${'$'}PODS_TARGET_SRCROOT"` | rm -rf "${'$'}{REPO_ROOT}/#{spec.name}.framework"* - | ${'$'}REPO_ROOT/$gradlew ${if (daemon) "" else "--no-daemon" } -P${CocoapodsPlugin.POD_FRAMEWORK_DIR_ENV}=`realpath "#{framework_dir}"` -p "${'$'}REPO_ROOT" "createIos${'$'}{CONFIGURATION}Artifacts" + | ${'$'}REPO_ROOT/$gradlew ${if (daemon) "" else "--no-daemon" } -p "${'$'}REPO_ROOT" "createIos${'$'}{CONFIGURATION}Artifacts" | SCRIPT | } | ] diff --git a/gradle-plugin/src/test/kotlin/com/alecstrong/cocoapods/gradle/plugin/PluginTest.kt b/gradle-plugin/src/test/kotlin/com/alecstrong/cocoapods/gradle/plugin/PluginTest.kt index fe8e507..72c780c 100644 --- a/gradle-plugin/src/test/kotlin/com/alecstrong/cocoapods/gradle/plugin/PluginTest.kt +++ b/gradle-plugin/src/test/kotlin/com/alecstrong/cocoapods/gradle/plugin/PluginTest.kt @@ -53,18 +53,40 @@ class PluginTest { val runner = GradleRunner.create() .withProjectDir(fixtureRoot) .withPluginClasspath() + .forwardOutput() val framework = File(fixtureRoot, "build/sample.framework").apply { deleteRecursively() } val dsym = File(fixtureRoot, "build/sample.framework.dSYM").apply { deleteRecursively() } - runner.withArguments( - "-P${CocoapodsPlugin.POD_FRAMEWORK_DIR_ENV}=${fixtureRoot.absolutePath}/build", "createIosDebugArtifacts", "--stacktrace" - ).build() + runner.withArguments("createIosDebugArtifacts", "--stacktrace", "--info").build() assertThat(framework.exists()).isTrue() assertThat(dsym.exists()).isTrue() + val plist = File(framework, "Info.plist") + assertThat(plist.exists()).isTrue() + + assertThat(plist.readText()).apply { + contains("iPhoneSimulator") + contains("iPhoneOS") + } + framework.deleteRecursively() dsym.deleteRecursively() } + + @Test + fun `run iosTest`() { + val fixtureName = "sample" + val fixtureRoot = File("src/test/$fixtureName") + val runner = GradleRunner.create() + .withProjectDir(fixtureRoot) + .withPluginClasspath() + .forwardOutput() + + val result = runner.withArguments("iosTest", "--stacktrace", "--info").build() + + assertThat(result.output) + .contains("[ PASSED ] 1 tests.") + } } diff --git a/gradle-plugin/src/test/sample/build.gradle b/gradle-plugin/src/test/sample/build.gradle index 2cb7cb3..2ba9966 100644 --- a/gradle-plugin/src/test/sample/build.gradle +++ b/gradle-plugin/src/test/sample/build.gradle @@ -5,28 +5,26 @@ plugins { archivesBaseName = 'Sample' +apply from: '../../../../gradle/dependencies.gradle' + +repositories { + mavenCentral() +} + kotlin { sourceSets { commonMain {} - commonTest {} + commonTest { + dependencies { + implementation deps.kotlin.test.common + implementation deps.kotlin.test.commonAnnotations + } + } iosMain {} iosTest {} } targets { - targetFromPreset(presets.iosArm64, 'iosDevice') { - compilations.main.outputKinds('FRAMEWORK') - } - targetFromPreset(presets.iosX64, 'iosSim') { - compilations.main.outputKinds('FRAMEWORK') - } - } - - configure([targets.iosSim, targets.iosDevice]) { - compilations.main.source(sourceSets.iosMain) - compilations.test.source(sourceSets.iosTest) - compilations.each { - it.extraOpts("-linker-options", "-lsqlite3") - } + targetFromPreset(cocoapodsPreset, 'ios') } } \ No newline at end of file diff --git a/gradle-plugin/src/test/sample/src/commonMain/kotlin/com/example/SampleClass.kt b/gradle-plugin/src/test/sample/src/commonMain/kotlin/com/example/SampleClass.kt new file mode 100644 index 0000000..bedc2ea --- /dev/null +++ b/gradle-plugin/src/test/sample/src/commonMain/kotlin/com/example/SampleClass.kt @@ -0,0 +1,7 @@ +package com.example + +class SampleClass { + fun someMethod(): String { + return "sup" + } +} \ No newline at end of file diff --git a/gradle-plugin/src/test/sample/src/commonMain/kotlin/com/example/Test.kt b/gradle-plugin/src/test/sample/src/commonMain/kotlin/com/example/Test.kt deleted file mode 100644 index 94cb715..0000000 --- a/gradle-plugin/src/test/sample/src/commonMain/kotlin/com/example/Test.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.example - -class Test { - fun someMethod() { - println("Sup") - } -} \ No newline at end of file diff --git a/gradle-plugin/src/test/sample/src/commonTest/kotlin/com/example/ATest.kt b/gradle-plugin/src/test/sample/src/commonTest/kotlin/com/example/ATest.kt new file mode 100644 index 0000000..cfc3c22 --- /dev/null +++ b/gradle-plugin/src/test/sample/src/commonTest/kotlin/com/example/ATest.kt @@ -0,0 +1,11 @@ +package com.example + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ATest { + @Test + fun doATest() { + assertEquals("sup", SampleClass().someMethod()) + } +} \ No newline at end of file diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle new file mode 100644 index 0000000..8f61869 --- /dev/null +++ b/gradle/dependencies.gradle @@ -0,0 +1,21 @@ +ext.versions = [ + kotlin: '1.3.20', +] + +ext.deps = [ + plugins: [ + kotlin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", + ], + + kotlin: [ + stdlib: [ + jdk: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}", + ], + test: [ + common: "org.jetbrains.kotlin:kotlin-test-common:${versions.kotlin}", + commonAnnotations: "org.jetbrains.kotlin:kotlin-test-annotations-common:${versions.kotlin}", + ], + ], + junit: 'junit:junit:4.12', + truth: 'com.google.truth:truth:0.42', +]