Skip to content

Commit

Permalink
Add Apple TV (tvOS) support (#1703)
Browse files Browse the repository at this point in the history
  • Loading branch information
soywiz authored Jun 15, 2023
1 parent 76ecfed commit 311f93c
Show file tree
Hide file tree
Showing 23 changed files with 170 additions and 286 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ fun NamedDomainObjectContainer<KotlinSourceSet>.createPairSourceSet(name: String

data class PairSourceSet(val main: KotlinSourceSet, val test: KotlinSourceSet) {
fun get(test: Boolean) = if (test) this.test else this.main
fun dependsOn(other: PairSourceSet) {
main.dependsOn(other.main)
test.dependsOn(other.test)
fun dependsOn(vararg others: PairSourceSet) {
for (other in others) {
main.dependsOn(other.main)
test.dependsOn(other.test)
}
}
}
131 changes: 76 additions & 55 deletions buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,40 @@ import korlibs.korge.gradle.targets.desktop.*
import korlibs.korge.gradle.targets.native.*
import korlibs.korge.gradle.util.*
import org.gradle.api.*
import org.gradle.api.file.*
import org.gradle.api.tasks.*
import org.gradle.configurationcache.extensions.*
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.tasks.*
import java.io.*

fun Project.configureNativeIos(projectType: ProjectType) {
val prepareKotlinNativeBootstrapIos = tasks.createThis<Task>("prepareKotlinNativeBootstrapIos") {
configureNativeIosTvos(projectType, "ios")
configureNativeIosTvos(projectType, "tvos")
}

fun Project.configureNativeIosTvos(projectType: ProjectType, targetName: String) {
val targetNameCapitalized = targetName.capitalized()

val platformNativeFolderName = "platforms/native-$targetName"
val platformNativeFolder = File(buildDir, platformNativeFolderName)

val prepareKotlinNativeBootstrapIosTvos = tasks.createThis<Task>("prepareKotlinNativeBootstrap${targetNameCapitalized}") {
doLast {
File(buildDir, "platforms/native-ios/bootstrap.kt").apply {
File(platformNativeFolder, "bootstrap.kt").apply {
parentFile.mkdirs()
writeText(IosProjectTools.genBootstrapKt(korge.realEntryPoint))
}
}
}
}

val iosTargets = listOf(kotlin.iosX64(), kotlin.iosArm64(), kotlin.iosSimulatorArm64())
val iosTvosTargets = when (targetName) {
"ios" -> listOf(kotlin.iosX64(), kotlin.iosArm64(), kotlin.iosSimulatorArm64())
"tvos" -> listOf(kotlin.tvosX64(), kotlin.tvosArm64(), kotlin.tvosSimulatorArm64())
else -> TODO()
}

kotlin.apply {
for (target in iosTargets) {
for (target in iosTvosTargets) {
target.configureKotlinNativeTarget(project)
//createCopyToExecutableTarget(target.name)
//for (target in listOf(iosX64())) {
Expand All @@ -39,7 +53,7 @@ fun Project.configureNativeIos(projectType: ProjectType) {

//compilation.outputKind(NativeOutputKind.FRAMEWORK)

compilation.defaultSourceSet.kotlin.srcDir(File(buildDir, "platforms/native-ios"))
compilation.defaultSourceSet.kotlin.srcDir(platformNativeFolder)

afterEvaluate {
target.binaries {
Expand All @@ -51,8 +65,8 @@ fun Project.configureNativeIos(projectType: ProjectType) {
}
}
for (type in listOf(NativeBuildType.DEBUG, NativeBuildType.RELEASE)) {
compilation.getCompileTask(NativeOutputKind.FRAMEWORK, type, project).dependsOn(prepareKotlinNativeBootstrapIos)
compilation.getLinkTask(NativeOutputKind.FRAMEWORK, type, project).dependsOn("prepareKotlinNativeIosProject")
compilation.getCompileTask(NativeOutputKind.FRAMEWORK, type, project).dependsOn(prepareKotlinNativeBootstrapIosTvos)
compilation.getLinkTask(NativeOutputKind.FRAMEWORK, type, project).dependsOn("prepareKotlinNative${targetNameCapitalized}Project")
}
}
}
Expand All @@ -61,43 +75,48 @@ fun Project.configureNativeIos(projectType: ProjectType) {
}

if (projectType.isExecutable) {
configureNativeIosRun()
configureNativeIosTvosRun(targetName)
}
}

fun Project.configureNativeIosRun() {
fun Project.configureNativeIosTvosRun(targetName: String) {
val targetNameCapitalized = targetName.capitalized()

val iosXcodegenExt = project.iosXcodegenExt
val iosSdkExt = project.iosSdkExt

tasks.createThis<Task>("installXcodeGen") {
onlyIf { !iosXcodegenExt.isInstalled() }
doLast { iosXcodegenExt.install() }
if (tasks.findByName("installXcodeGen") == null) {
tasks.createThis<Task>("installXcodeGen") {
onlyIf { !iosXcodegenExt.isInstalled() }
doLast { iosXcodegenExt.install() }
}
}

val combinedResourcesFolder = File(buildDir, "combinedResources/resources")
val processedResourcesFolder = File(buildDir, "processedResources/iosArm64/main")
val copyIosResources = tasks.createTyped<Copy>("copyIosResources") {
val processResourcesTaskName = getProcessResourcesTaskName("iosArm64", "main")
val processedResourcesFolder = File(buildDir, "processedResources/${targetName}Arm64/main")
val copyIosTvosResources = tasks.createTyped<Copy>("copy${targetNameCapitalized}Resources") {
val processResourcesTaskName = getProcessResourcesTaskName("${targetName}Arm64", "main")
dependsOn(processResourcesTaskName)
from(processedResourcesFolder)
into(combinedResourcesFolder)
}

val prepareKotlinNativeIosProject = tasks.createThis<Task>("prepareKotlinNativeIosProject") {
dependsOn("installXcodeGen", "prepareKotlinNativeBootstrapIos", prepareKotlinNativeBootstrap, copyIosResources)
val prepareKotlinNativeIosTvosProject = tasks.createThis<Task>("prepareKotlinNative${targetNameCapitalized}Project") {
dependsOn("installXcodeGen", "prepareKotlinNativeBootstrap${targetNameCapitalized}", prepareKotlinNativeBootstrap, copyIosTvosResources)
doLast {
// project.yml requires these folders to be available, or it will fail
//File(rootDir, "src/commonMain/resources").mkdirs()

val folder = File(buildDir, "platforms/ios")
IosProjectTools.prepareKotlinNativeIosProject(folder)
val folder = File(buildDir, "platforms/$targetName")
IosProjectTools.prepareKotlinNativeIosProject(folder, targetName)
IosProjectTools.prepareKotlinNativeIosProjectIcons(folder) { korge.getIconBytes(it) }
IosProjectTools.prepareKotlinNativeIosProjectYml(
folder,
id = korge.id,
name = korge.name,
team = korge.iosDevelopmentTeam ?: korge.appleDevelopmentTeamId ?: iosSdkExt.appleGetDefaultDeveloperCertificateTeamId(),
combinedResourcesFolder = combinedResourcesFolder
combinedResourcesFolder = combinedResourcesFolder,
targetName = targetName
)

execLogger {
Expand All @@ -107,15 +126,15 @@ fun Project.configureNativeIosRun() {
}
}

tasks.createThis<Task>("iosShutdownSimulator") {
tasks.createThis<Task>("${targetName}ShutdownSimulator") {
doFirst {
execLogger { it.commandLine("xcrun", "simctl", "shutdown", "booted") }
}
}

val iphoneVersion = korge.preferredIphoneSimulatorVersion

val iosCreateIphone = tasks.createThis<Task>("iosCreateIphone") {
val iosCreateIphone = tasks.createThis<Task>("${targetName}CreateIphone") {
onlyIf { iosSdkExt.appleGetDevices().none { it.name == "iPhone $iphoneVersion" } }
doFirst {
val result = execOutput("xcrun", "simctl", "list")
Expand All @@ -126,7 +145,7 @@ fun Project.configureNativeIosRun() {
}
}

tasks.createThis<Task>("iosBootSimulator") {
tasks.createThis<Task>("${targetName}BootSimulator") {
onlyIf { iosSdkExt.appleGetBootedDevice() == null }
dependsOn(iosCreateIphone)
doLast {
Expand All @@ -143,6 +162,19 @@ fun Project.configureNativeIosRun() {
}
}

val installIosTvosDeploy = tasks.findByName("installIosDeploy") ?: tasks.createThis<Task>("installIosDeploy") {
onlyIf { !iosTvosDeployExt.isInstalled }
doFirst {
iosTvosDeployExt.installIfRequired()
}
}

val updateIosTvosDeploy = tasks.findByName("updateIosDeploy") ?: tasks.createThis<Task>("updateIosDeploy") {
doFirst {
iosTvosDeployExt.update()
}
}

for (debug in listOf(false, true)) {
val debugSuffix = if (debug) "Debug" else "Release"
for (simulator in listOf(false, true)) {
Expand All @@ -152,11 +184,11 @@ fun Project.configureNativeIosRun() {
val arch = if (simulator) "X64" else "Arm64"
val arch2 = if (simulator) "x86_64" else "arm64"
val sdkName = if (simulator) "iphonesimulator" else "iphoneos"
tasks.createThis<Exec>("iosBuild$simulatorSuffix$debugSuffix") {
tasks.createThis<Exec>("${targetName}Build$simulatorSuffix$debugSuffix") {
//task.dependsOn(prepareKotlinNativeIosProject, "linkMain${debugSuffix}FrameworkIos$arch")
val linkTaskName = "link${debugSuffix}FrameworkIos$arch"
dependsOn(prepareKotlinNativeIosProject, linkTaskName)
val xcodeProjDir = buildDir["platforms/ios/app.xcodeproj"]
val linkTaskName = "link${debugSuffix}Framework${targetNameCapitalized}$arch"
dependsOn(prepareKotlinNativeIosTvosProject, linkTaskName)
val xcodeProjDir = buildDir["platforms/$targetName/app.xcodeproj"]
afterEvaluate {
val linkTask: KotlinNativeLink = tasks.findByName(linkTaskName) as KotlinNativeLink
inputs.dir(linkTask.outputFile)
Expand All @@ -172,39 +204,40 @@ fun Project.configureNativeIosRun() {
}
}

val installIosSimulator = tasks.createThis<Task>("installIosSimulator$debugSuffix") {
val buildTaskName = "iosBuildSimulator$debugSuffix"

val installIosSimulator = tasks.createThis<Task>("install${targetNameCapitalized}Simulator$debugSuffix") {
val buildTaskName = "${targetName}BuildSimulator$debugSuffix"
group = GROUP_KORGE_INSTALL

dependsOn(buildTaskName, "iosBootSimulator")
dependsOn(buildTaskName, "${targetName}BootSimulator")
doLast {
val appFolder = tasks.getByName(buildTaskName).outputs.files.first().parentFile
val device = iosSdkExt.appleGetInstallDevice(iphoneVersion)
execLogger { it.commandLine("xcrun", "simctl", "install", device.udid, appFolder.absolutePath) }
}
}

val installIosDevice = tasks.createThis<Task>("installIosDevice$debugSuffix") {
val installIosTvosDevice = tasks.createThis<Task>("install${targetNameCapitalized}Device$debugSuffix") {
group = GROUP_KORGE_INSTALL
val buildTaskName = "iosBuildDevice$debugSuffix"
dependsOn("installIosDeploy", buildTaskName)
val buildTaskName = "${targetName}BuildDevice$debugSuffix"
dependsOn(installIosTvosDeploy, buildTaskName)
doLast {
val appFolder = tasks.getByName(buildTaskName).outputs.files.first().parentFile
iosDeployExt.command("--bundle", appFolder.absolutePath)
iosTvosDeployExt.command("--bundle", appFolder.absolutePath)
}
}

val runIosDevice = tasks.createTyped<Exec>("runIosDevice$debugSuffix") {
val runIosDevice = tasks.createTyped<Exec>("run${targetNameCapitalized}Device$debugSuffix") {
group = GROUP_KORGE_RUN
val buildTaskName = "iosBuildDevice$debugSuffix"
dependsOn("installIosDeploy", buildTaskName)
val buildTaskName = "${targetName}BuildDevice$debugSuffix"
dependsOn(installIosTvosDeploy, buildTaskName)
doFirst {
val appFolder = tasks.getByName(buildTaskName).outputs.files.first().parentFile
iosDeployExt.command("--noninteractive", "-d", "--bundle", appFolder.absolutePath)
iosTvosDeployExt.command("--noninteractive", "-d", "--bundle", appFolder.absolutePath)
}
}

val runIosSimulator = tasks.createTyped<Exec>("runIosSimulator$debugSuffix") {
val runIosSimulator = tasks.createTyped<Exec>("run${targetNameCapitalized}Simulator$debugSuffix") {
group = GROUP_KORGE_RUN
dependsOn(installIosSimulator)
doFirst {
Expand All @@ -215,27 +248,15 @@ fun Project.configureNativeIosRun() {
}
}

tasks.createTyped<Task>("runIos$debugSuffix") {
tasks.createTyped<Task>("run${targetNameCapitalized}$debugSuffix") {
dependsOn(runIosDevice)
}
}

tasks.createThis<Task>("iosEraseAllSimulators") {
tasks.createThis<Task>("${targetName}EraseAllSimulators") {
doLast { execLogger { it.commandLine("osascript", "-e", "tell application \"iOS Simulator\" to quit") } }
doLast { execLogger { it.commandLine("osascript", "-e", "tell application \"Simulator\" to quit") } }
doLast { execLogger { it.commandLine("xcrun", "simctl", "erase", "all") } }
}

tasks.createThis<Task>("installIosDeploy") {
onlyIf { !iosDeployExt.isInstalled }
doFirst {
iosDeployExt.installIfRequired()
}
}

tasks.createThis<Task>("updateIosDeploy") {
doFirst {
iosDeployExt.update()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import korlibs.korge.gradle.util.projectExtension
import korlibs.korge.gradle.util.execLogger
import java.io.File

val Project.iosDeployExt by projectExtension {
val Project.iosTvosDeployExt by projectExtension {
IosDeploy(this)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package korlibs.korge.gradle.targets.ios

import korlibs.korge.gradle.korge
import korlibs.korge.gradle.targets.getIconBytes
import java.io.File
import korlibs.korge.gradle.util.*
import org.gradle.configurationcache.extensions.*

object IosProjectTools {
fun genBootstrapKt(entrypoint: String): String = """
Expand Down Expand Up @@ -50,9 +49,26 @@ object IosProjectTools {
@end
""".trimIndent()

fun genLaunchScreenStoryboard(): String = """
fun genLaunchScreenStoryboard(targetName: String): String {
val documentType = when (targetName) {
"ios" -> "com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB"
"tvos" -> "com.apple.InterfaceBuilder.AppleTV.Storyboard"
else -> TODO()
}
val targetRuntime = when (targetName) {
"ios" -> "iOS.CocoaTouch"
"tvos" -> "AppleTV"
else -> TODO()
}
val (sizeWidth, sizeHeight) = when (targetName) {
"ios" -> 375 to 667
"tvos" -> 1920 to 1000
else -> TODO()
}

return """
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="$documentType" version="3.0" toolsVersion="13122.16" targetRuntime="$targetRuntime" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
Expand All @@ -64,7 +80,7 @@ object IosProjectTools {
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="$sizeWidth" height="$sizeHeight"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
Expand All @@ -77,10 +93,11 @@ object IosProjectTools {
</scenes>
</document>
""".trimIndent()
}

fun prepareKotlinNativeIosProject(folder: File) {
fun prepareKotlinNativeIosProject(folder: File, targetName: String) {
folder["app/main.m"].ensureParents().writeText(genMainObjC())
folder["app/Base.lproj/LaunchScreen.storyboard"].ensureParents().writeText(genLaunchScreenStoryboard())
folder["app/Base.lproj/LaunchScreen.storyboard"].ensureParents().writeText(genLaunchScreenStoryboard(targetName))
folder["app/Assets.xcassets/Contents.json"].ensureParents().writeText("""
{
"info" : {
Expand Down Expand Up @@ -203,8 +220,11 @@ object IosProjectTools {
id: String,
name: String,
team: String?,
combinedResourcesFolder: File
combinedResourcesFolder: File,
targetName: String
) {
val targetNameCapitalized = targetName.capitalized()

folder["project.yml"].ensureParents().writeText(Indenter {
line("name: app")
line("options:")
Expand All @@ -224,10 +244,10 @@ object IosProjectTools {
indent {
for (debug in listOf(false, true)) {
val debugSuffix = if (debug) "Debug" else "Release"
for (target in listOf("X64", "Arm64", "Arm32")) {
line("app-$target-$debugSuffix:")
for (arch in listOf("X64", "Arm64", "Arm32")) {
line("app-$arch-$debugSuffix:")
indent {
line("platform: iOS")
line("platform: ${if (targetName == "ios") "iOS" else "tvOS"}")
line("type: application")
line("deploymentTarget: \"10.0\"")
line("sources:")
Expand Down Expand Up @@ -256,7 +276,7 @@ object IosProjectTools {
line(" DEVELOPMENT_TEAM: $team")
}
line("dependencies:")
line(" - framework: ../../bin/ios$target/${debugSuffix.toLowerCase()}Framework/GameMain.framework")
line(" - framework: ../../bin/${targetName}$arch/${debugSuffix.toLowerCase()}Framework/GameMain.framework")
}
}
}
Expand Down
Loading

0 comments on commit 311f93c

Please sign in to comment.