Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,138 @@ jobs:
SDL_VIDEODRIVER: dummy
SDL_AUDIODRIVER: dummy
LD_LIBRARY_PATH: /usr/local/lib

build-windows:
runs-on: windows-latest
timeout-minutes: 60

defaults:
run:
shell: msys2 {0}

steps:
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: false
install: >-
mingw-w64-x86_64-cmake
mingw-w64-x86_64-gcc
mingw-w64-x86_64-pkg-config
mingw-w64-x86_64-freetype
mingw-w64-x86_64-harfbuzz
mingw-w64-x86_64-libjpeg-turbo
mingw-w64-x86_64-libpng
mingw-w64-x86_64-libwebp
mingw-w64-x86_64-libtiff
mingw-w64-x86_64-chipmunk
mingw-w64-x86_64-make

- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache SDL3 source builds
uses: actions/cache@v4
id: sdl-source-cache
with:
path: |
D:\a\_temp\msys64\mingw64\lib\libSDL3*
D:\a\_temp\msys64\mingw64\bin\SDL3*
D:\a\_temp\msys64\mingw64\bin\libSDL3*
D:\a\_temp\msys64\mingw64\include\SDL3
D:\a\_temp\msys64\mingw64\include\SDL3_image
D:\a\_temp\msys64\mingw64\include\SDL3_ttf
D:\a\_temp\msys64\mingw64\include\SDL3_mixer
D:\a\_temp\msys64\mingw64\include\SDL3_net
key: sdl3-source-${{ runner.os }}-${{ hashFiles('.gitmodules', 'sdl3/build_sdl.sh') }}

- name: Build all SDL3 libs from source
if: steps.sdl-source-cache.outputs.cache-hit != 'true'
run: bash sdl3/build_sdl.sh

- name: Verify SDL3 libraries
run: |
echo "=== SDL3 DLLs ==="
ls /mingw64/bin/SDL3*.dll /mingw64/bin/libSDL3*.dll 2>/dev/null || true
echo "=== SDL3 import libs ==="
ls /mingw64/lib/libSDL3* 2>/dev/null || true
echo "=== Include dirs ==="
ls /mingw64/include/SDL3/ 2>/dev/null | head -5

- name: Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml') }}
restore-keys: gradle-${{ runner.os }}-

- name: Patch Konan MinGW sysroot
env:
MSYS2_ROOT: D:/a/_temp/msys64
run: |
# Kotlin/Native bundles an outdated MinGW sysroot whose CRT objects
# reference symbols missing from its bundled libs. Copy the system
# MSYS2 CRT libs over so the linker can resolve them.
# NOTE: $HOME in MSYS2 != Windows home; use USERPROFILE for .konan path
WIN_HOME="$(cygpath "$USERPROFILE")"
KONAN_MINGW="$WIN_HOME/.konan/dependencies/msys2-mingw-w64-x86_64-2/x86_64-w64-mingw32/lib"
if [ ! -d "$KONAN_MINGW" ]; then
echo "Konan sysroot not found, triggering download..."
./gradlew :kengine:cinteropSdl3Native || true
fi
echo "KONAN_MINGW=$KONAN_MINGW"
ls "$KONAN_MINGW"/*.o 2>/dev/null | head -5
if [ -d "$KONAN_MINGW" ]; then
echo "Replacing Konan sysroot lib with system MSYS2 CRT"
# The bundled sysroot is too old — its crt2.o references symbols
# that don't exist in its bundled libs. Copying individual files
# causes version mismatches, so replace the entire lib directory.
SYS_LIB="/mingw64/lib"
echo "System lib at: $SYS_LIB ($(ls "$SYS_LIB"/*.a 2>/dev/null | wc -l) .a files)"
echo "Konan lib at: $KONAN_MINGW ($(ls "$KONAN_MINGW"/*.a 2>/dev/null | wc -l) .a files)"
# Back up the original, then overlay system libs
cp -r "$KONAN_MINGW" "${KONAN_MINGW}.bak"
cp -f "$SYS_LIB"/*.a "$KONAN_MINGW/" 2>/dev/null || true
cp -f "$SYS_LIB"/*.o "$KONAN_MINGW/" 2>/dev/null || true
echo "After patch: $(ls "$KONAN_MINGW"/*.a 2>/dev/null | wc -l) .a files"
echo "Patch applied successfully"
else
echo "ERROR: Konan MinGW sysroot not found at $KONAN_MINGW"
find "$WIN_HOME/.konan" -name "crt2.o" 2>/dev/null || echo "No crt2.o found"
fi

- name: Build all modules
env:
MSYS2_ROOT: D:/a/_temp/msys64
run: |
./gradlew clean build -x allTests -x nativeTest -x jvmTest -x jsTest

- name: Compile and run kengine tests
env:
MSYS2_ROOT: D:/a/_temp/msys64
SDL_VIDEODRIVER: dummy
SDL_AUDIODRIVER: dummy
run: |
./gradlew :kengine:linkDebugTestNative || true
if [ -f ./kengine/build/bin/native/debugTest/test.kexe ]; then
# Copy DLLs next to the test binary
cp /mingw64/bin/SDL3.dll ./kengine/build/bin/native/debugTest/ 2>/dev/null || true
cp /mingw64/bin/libSDL3.dll ./kengine/build/bin/native/debugTest/ 2>/dev/null || true
cp /mingw64/bin/SDL3_image.dll ./kengine/build/bin/native/debugTest/ 2>/dev/null || true
cp /mingw64/bin/SDL3_ttf.dll ./kengine/build/bin/native/debugTest/ 2>/dev/null || true
cp /mingw64/bin/SDL3_mixer.dll ./kengine/build/bin/native/debugTest/ 2>/dev/null || true
cp /mingw64/bin/SDL3_net.dll ./kengine/build/bin/native/debugTest/ 2>/dev/null || true
./kengine/build/bin/native/debugTest/test.kexe \
"--ktest_filter=*-*IT.*:*TiledMapLoaderTest.*"
fi
22 changes: 20 additions & 2 deletions buildSrc/src/main/kotlin/PlatformConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ object PlatformConfig {
val hostOs: String = System.getProperty("os.name")
val isMacOS: Boolean = hostOs == "Mac OS X"
val isLinux: Boolean = hostOs == "Linux"
val isWindows: Boolean = hostOs.startsWith("Windows")

// MSYS2 mingw64 root — standard location on GitHub Actions and local installs
private val msys2Root: String = System.getenv("MSYS2_ROOT") ?: "C:/msys64"
private val mingw64: String = "$msys2Root/mingw64"

val includePaths: List<String> = when {
isMacOS -> listOf("/opt/homebrew/include", "/usr/local/include")
isLinux -> listOf("/usr/local/include", "/usr/include", "/usr/include/x86_64-linux-gnu")
isWindows -> listOf("$mingw64/include")
else -> listOf("/usr/local/include")
}

val libPaths: List<String> = when {
isMacOS -> listOf("/opt/homebrew/lib", "/usr/local/lib")
isLinux -> listOf("/usr/local/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib")
isWindows -> listOf("$mingw64/lib")
else -> listOf("/usr/local/lib")
}

Expand Down Expand Up @@ -50,8 +57,19 @@ object PlatformConfig {
"-lpthread", "-ldl", "-lm"
) else emptyList()

val windowsSystemLibs: List<String>
get() = if (isWindows) listOf(
"-lmingw32", "-lole32", "-loleaut32", "-limm32",
"-lwinmm", "-lgdi32", "-lsetupapi", "-lversion"
) else emptyList()

fun sharedLibLinkerOpts(vararg libs: String): List<String> =
libPathOpts + libs.map { "-l$it" } + macFrameworks + linuxSystemLibs + rpathOpts
libPathOpts + libs.map { "-l$it" } +
macFrameworks + linuxSystemLibs + windowsSystemLibs + rpathOpts

val sharedLibExt: String = if (isMacOS) "dylib" else "so"
val sharedLibExt: String = when {
isMacOS -> "dylib"
isWindows -> "dll"
else -> "so"
}
}
31 changes: 20 additions & 11 deletions buildSrc/src/main/kotlin/SdlDylibCopier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,25 @@ class SdlDylibCopier(private val project: Project) {

private val isMacOS = PlatformConfig.isMacOS
private val isLinux = PlatformConfig.isLinux
private val isWindows = PlatformConfig.isWindows

private val msys2Root: String = System.getenv("MSYS2_ROOT") ?: "C:/msys64"

private val searchPaths: List<String> = when {
isMacOS -> listOf("/opt/homebrew/lib", "/usr/local/lib")
isLinux -> listOf("/usr/local/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib")
isWindows -> listOf("$msys2Root/mingw64/bin", "$msys2Root/mingw64/lib")
else -> listOf("/usr/local/lib")
}

/**
* Resolve a shared library by base name (e.g. "SDL3"), searching platform-specific locations.
* On macOS looks for .dylib, on Linux looks for .so.
*/
private fun resolveLib(baseName: String): String {
val candidates = if (isMacOS) {
listOf("lib${baseName}.0.dylib", "lib${baseName}.dylib")
} else {
listOf("lib${baseName}.so.0", "lib${baseName}.so")
val candidates = when {
isMacOS -> listOf("lib${baseName}.0.dylib", "lib${baseName}.dylib")
isWindows -> listOf("${baseName}.dll", "lib${baseName}.dll")
else -> listOf("lib${baseName}.so.0", "lib${baseName}.so")
}
for (candidate in candidates) {
for (searchPath in searchPaths) {
Expand All @@ -33,10 +36,13 @@ class SdlDylibCopier(private val project: Project) {
}
}
}
val installHint = when {
isMacOS -> "Install via Homebrew (brew install sdl3) or build from source (bash sdl3/build_sdl.sh)."
isWindows -> "Install via MSYS2 (pacman -S mingw-w64-x86_64-SDL3) or build from source."
else -> "Build from source (bash sdl3/build_sdl.sh) or install via your package manager."
}
throw IllegalStateException(
"Could not find lib$baseName in any of: ${searchPaths.joinToString()}. " +
if (isMacOS) "Install via Homebrew (brew install sdl3) or build from source (bash sdl3/build_sdl.sh)."
else "Build from source (bash sdl3/build_sdl.sh) or install via your package manager."
"Could not find $baseName in any of: ${searchPaths.joinToString()}. $installHint"
)
}

Expand Down Expand Up @@ -64,7 +70,7 @@ class SdlDylibCopier(private val project: Project) {
libTargetDirs: List<String>
) {
libsToCopy.forEach { libPath ->
val libName = libPath.substringAfterLast("/")
val libName = libPath.substringAfterLast("/").substringAfterLast("\\")

libTargetDirs.forEach { toDir ->
val targetDir = toDir.substringAfter("${project.buildDir}/bin/native/")
Expand All @@ -87,7 +93,9 @@ class SdlDylibCopier(private val project: Project) {
}
}

fileMode = 0b111101101 // rwxr-xr-x (755)
if (!isWindows) {
fileMode = 0b111101101 // rwxr-xr-x (755)
}
}
} else {
println("Task $taskName already exists. Skipping registration.")
Expand All @@ -98,7 +106,7 @@ class SdlDylibCopier(private val project: Project) {
project.tasks.named("nativeTest") {
dependsOn(
libsToCopy.flatMap { libPath ->
val libName = libPath.substringAfterLast("/")
val libName = libPath.substringAfterLast("/").substringAfterLast("\\")
libTargetDirs.map { toDir ->
val targetDir = toDir.substringAfter("${project.buildDir}/bin/native/")
generateTaskName(project, libName, targetDir)
Expand All @@ -116,6 +124,7 @@ class SdlDylibCopier(private val project: Project) {
.removeSuffix(".dylib")
.removeSuffix(".so.0")
.removeSuffix(".so")
.removeSuffix(".dll")
.capitalize()
return if (targetDir.contains("debugTest"))
"copy${moduleName}${prefix}ToDebugTestFrameworks"
Expand Down
6 changes: 4 additions & 2 deletions kengine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ kotlin {
nativeTarget.apply {
binaries {
staticLib()
sharedLib {
baseName = "kengine"
if (!PlatformConfig.isWindows) {
sharedLib {
baseName = "kengine"
}
}
all {
linkerOpts(PlatformConfig.sharedLibLinkerOpts("SDL3", "SDL3_image", "SDL3_ttf"))
Expand Down
3 changes: 2 additions & 1 deletion kengine/src/nativeMain/kotlin/com/kengine/file/File.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kengine.file

import kotlinx.cinterop.convert
import kotlinx.cinterop.refTo
import kotlinx.cinterop.toKString
import platform.posix.F_OK
Expand All @@ -9,7 +10,7 @@ import platform.posix.getcwd
object File {
fun pwd(): String {
val buffer = ByteArray(1024)
return getcwd(buffer.refTo(0), buffer.size.toULong())?.toKString() ?: "Unknown"
return getcwd(buffer.refTo(0), buffer.size.convert())?.toKString() ?: "Unknown"
}
fun isExist(path: String): Boolean {
return access(path, F_OK) == 0 // F_OK checks existence only
Expand Down
Loading
Loading