Skip to content

Commit cc0ed83

Browse files
committed
add mingw local build support
1 parent 39dfad8 commit cc0ed83

File tree

10 files changed

+408
-127
lines changed

10 files changed

+408
-127
lines changed

runtime/build.gradle.kts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,20 @@ subprojects {
125125
tasks.withType<KotlinNativeSimulatorTest> {
126126
enabled = false
127127
}
128+
129+
tasks.withType<AbstractTestTask> {
130+
if (this is Test) {
131+
useJUnitPlatform()
132+
}
133+
134+
testLogging {
135+
events("passed", "skipped", "failed")
136+
showStandardStreams = true
137+
showStackTraces = true
138+
showExceptions = true
139+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
140+
}
141+
}
128142
}
129143

130144
// configureIosSimulatorTasks()

runtime/runtime-core/build.gradle.kts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
6+
import aws.sdk.kotlin.gradle.util.typedProp
7+
import org.jetbrains.kotlin.konan.target.HostManager
8+
import java.nio.file.Files
9+
import java.nio.file.Paths
710

811
plugins {
912
alias(libs.plugins.kotlinx.serialization)
@@ -55,16 +58,68 @@ kotlin {
5558
all {
5659
languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi")
5760
}
61+
62+
// FIXME this config should live in aws-kotlin-repo-tools
63+
val posixMain by getting
64+
posixMain.dependsOn(nativeMain.get())
5865
}
5966

60-
targets.withType<KotlinNativeTarget> {
61-
compilations["main"].cinterops {
62-
val interopDir = "$projectDir/native/src/nativeInterop/cinterop"
63-
create("environ") {
64-
includeDirs(interopDir)
65-
packageName("aws.smithy.platform.posix")
66-
headers(listOf("$interopDir/environ.h"))
67+
if (HostManager.hostIsMingw) {
68+
mingwX64 {
69+
val mingwHome = findMingwHome()
70+
val defPath = layout.buildDirectory.file("cinterop/winver.def")
71+
72+
// Dynamically construct def file because of dynamic mingw paths
73+
val defFileTask by tasks.registering {
74+
outputs.file(defPath)
75+
76+
val mingwLibs = Paths.get(mingwHome, "lib").toString().replace("\\", "\\\\") // Windows path shenanigans
77+
78+
doLast {
79+
Files.writeString(
80+
defPath.get().asFile.toPath(),
81+
"""
82+
package = aws.smithy.kotlin.native.winver
83+
headers = windows.h
84+
compilerOpts = \
85+
-DUNICODE \
86+
-DWINVER=0x0601 \
87+
-D_WIN32_WINNT=0x0601 \
88+
-DWINAPI_FAMILY=3 \
89+
-DOEMRESOURCE \
90+
-Wno-incompatible-pointer-types \
91+
-Wno-deprecated-declarations
92+
libraryPaths = $mingwLibs
93+
staticLibraries = libversion.a
94+
""".trimIndent(),
95+
)
96+
}
6797
}
98+
compilations["main"].cinterops {
99+
create("winver") {
100+
val mingwIncludes = Paths.get(mingwHome, "include").toString()
101+
includeDirs(mingwIncludes)
102+
definitionFile.set(defPath)
103+
104+
// Ensure that the def file is written first
105+
tasks[interopProcessingTaskName].dependsOn(defFileTask)
106+
}
107+
}
108+
109+
// TODO clean up
110+
val compilerArgs = listOf(
111+
"-Xverbose-phases=linker", // Enable verbose linking phase from the compiler
112+
"-linker-option",
113+
"-v",
114+
)
115+
compilerOptions.freeCompilerArgs.addAll(compilerArgs)
116+
68117
}
69118
}
70119
}
120+
121+
private fun findMingwHome(): String =
122+
System.getenv("MINGW_PREFIX")?.takeUnless { it.isBlank() } ?:
123+
typedProp("mingw.prefix") ?:
124+
throw IllegalStateException("Cannot determine MinGW prefix location. Please verify MinGW is installed correctly " +
125+
"and that either the `MINGW_PREFIX` environment variable or the `mingw.prefix` Gradle property is set.")
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package aws.smithy.kotlin.runtime.net
2+
3+
import kotlinx.cinterop.UnsafeNumber
4+
import kotlinx.cinterop.alloc
5+
import kotlinx.cinterop.allocPointerTo
6+
import kotlinx.cinterop.invoke
7+
import kotlinx.cinterop.memScoped
8+
import kotlinx.cinterop.pointed
9+
import kotlinx.cinterop.ptr
10+
import kotlinx.cinterop.refTo
11+
import kotlinx.cinterop.reinterpret
12+
import kotlinx.cinterop.toKString
13+
import kotlinx.cinterop.value
14+
import platform.posix.AF_INET
15+
import platform.posix.AF_INET6
16+
import platform.posix.AF_UNSPEC
17+
import platform.posix.SOCK_STREAM
18+
import platform.posix.WSADATA
19+
import platform.posix.memcpy
20+
import platform.posix.sockaddr
21+
import platform.posix.sockaddr_in
22+
import platform.windows.AI_PASSIVE
23+
import platform.windows.WSACleanup
24+
import platform.windows.WSAStartup
25+
import platform.windows.addrinfo
26+
import platform.windows.freeaddrinfo
27+
import platform.windows.gai_strerror
28+
import platform.windows.getaddrinfo
29+
import platform.windows.sockaddr_in6
30+
31+
internal actual object DefaultHostResolver : HostResolver {
32+
actual override suspend fun resolve(hostname: String): List<HostAddress> = memScoped {
33+
// Version format specified in https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsastartup
34+
val wsaMajorVersion = 1u
35+
val wsaMinorVersion = 1u
36+
val wsaVersion = (wsaMajorVersion shl 8 or wsaMinorVersion).toUShort()
37+
38+
val wsaInfo = alloc<WSADATA>()
39+
val wsaResult = WSAStartup(wsaVersion, wsaInfo.ptr)
40+
check(wsaResult == 0) { "Failed to initialize Windows Sockets (error code $wsaResult)" }
41+
42+
try {
43+
44+
val hints = alloc<addrinfo>().apply {
45+
ai_family = AF_UNSPEC // Allow both IPv4 and IPv6
46+
ai_socktype = SOCK_STREAM // TCP stream sockets
47+
ai_flags = AI_PASSIVE // For wildcard IP address
48+
}
49+
50+
val result = allocPointerTo<addrinfo>()
51+
52+
try {
53+
// Perform the DNS lookup
54+
val status = getaddrinfo(hostname, null, hints.ptr, result.ptr)
55+
check(status == 0) { "Failed to resolve host $hostname: ${gai_strerror?.invoke(status)?.toKString()}" }
56+
57+
return generateSequence(result.value) { it.pointed.ai_next }
58+
.map { it.pointed.ai_addr!!.pointed.toIpAddr() }
59+
.map { HostAddress(hostname, it) }
60+
.toList()
61+
} finally {
62+
freeaddrinfo(result.value)
63+
}
64+
} finally {
65+
WSACleanup()
66+
}
67+
}
68+
69+
@OptIn(UnsafeNumber::class)
70+
private fun sockaddr.toIpAddr(): IpAddr {
71+
val (size, addrPtr, constructor) = when (sa_family.toInt()) {
72+
AF_INET -> Triple(
73+
4,
74+
reinterpret<sockaddr_in>().sin_addr.ptr,
75+
{ bytes: ByteArray -> IpV4Addr(bytes) },
76+
)
77+
AF_INET6 -> Triple(
78+
16,
79+
reinterpret<sockaddr_in6>().sin6_addr.ptr,
80+
{ bytes: ByteArray -> IpV6Addr(bytes) },
81+
)
82+
else -> throw IllegalArgumentException("Unsupported sockaddr family $sa_family")
83+
}
84+
85+
val ipBytes = ByteArray(size)
86+
memcpy(ipBytes.refTo(0), addrPtr, size.toULong())
87+
return constructor(ipBytes)
88+
}
89+
90+
actual override fun reportFailure(addr: HostAddress) {
91+
// No-op, same as JVM implementation
92+
}
93+
94+
actual override fun purgeCache(addr: HostAddress?) {
95+
// No-op, same as JVM implementation
96+
}
97+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package aws.smithy.kotlin.runtime.util
2+
3+
import kotlinx.cinterop.*
4+
import platform.posix.environ
5+
import platform.posix.memcpy
6+
import aws.smithy.kotlin.native.winver.*
7+
8+
internal actual val rawEnvironmentVariables: CPointer<CPointerVarOf<CPointer<ByteVarOf<Byte>>>>? = environ
9+
10+
public actual object SystemDefaultProvider : SystemDefaultProviderBase() {
11+
actual override val filePathSeparator: String = "\\"
12+
actual override fun osInfo(): OperatingSystem = OperatingSystem(OsFamily.Windows, osVersionFromKernel())
13+
}
14+
15+
// The functions below are adapted from C++ SDK:
16+
// https://github.com/aws/aws-sdk-cpp/blob/0e6085bf0dd9a1cb1f27d101c4cf2db6ade6f307/src/aws-cpp-sdk-core/source/platform/windows/OSVersionInfo.cpp#L49-L106
17+
18+
private val wordHexFormat = HexFormat {
19+
upperCase = false
20+
number {
21+
removeLeadingZeros = true
22+
minLength = 4
23+
}
24+
}
25+
26+
private data class LangCodePage(
27+
val language: UShort,
28+
val codePage: UShort,
29+
)
30+
31+
private fun osVersionFromKernel(): String? = memScoped {
32+
withFileVersionInfo("Kernel32.dll") { versionInfoPtr ->
33+
getLangCodePage(versionInfoPtr)?.let { langCodePage ->
34+
getProductVersion(versionInfoPtr, langCodePage)
35+
}
36+
}
37+
}
38+
39+
private inline fun <R> withFileVersionInfo(fileName: String, block: (CPointer<ByteVarOf<Byte>>) -> R?): R? {
40+
val blobSize = GetFileVersionInfoSizeW(fileName, null)
41+
val blob = ByteArray(blobSize.convert())
42+
blob.usePinned { pinned ->
43+
val result = GetFileVersionInfoW(fileName, 0u, blobSize, pinned.addressOf(0))
44+
return if (result == 0) {
45+
null
46+
} else {
47+
block(pinned.addressOf(0))
48+
}
49+
}
50+
}
51+
52+
private fun MemScope.getLangCodePage(versionInfoPtr: CPointer<ByteVarOf<Byte>>): LangCodePage? {
53+
// Get _any_ language pack and codepage since they should all have the same version
54+
val langAndCodePagePtr = alloc<COpaquePointerVar>()
55+
val codePageSize = alloc<UIntVar>()
56+
val result = VerQueryValueW(
57+
versionInfoPtr,
58+
"""\VarFileInfo\Translation""",
59+
langAndCodePagePtr.ptr,
60+
codePageSize.ptr,
61+
)
62+
63+
return if (result == 0) {
64+
null
65+
} else {
66+
val langAndCodePage = langAndCodePagePtr.value!!.reinterpret<UIntVar>().pointed.value
67+
val language = (langAndCodePage and 0x0000ffffu).toUShort() // low WORD
68+
val codePage = (langAndCodePage and 0xffff0000u shr 16).toUShort() // high WORD
69+
LangCodePage(language, codePage)
70+
}
71+
}
72+
73+
private fun MemScope.getProductVersion(versionInfoPtr: CPointer<ByteVarOf<Byte>>, langCodePage: LangCodePage): String? {
74+
val versionId = buildString {
75+
// Something like: \StringFileInfo\04090fb0\ProductVersion
76+
append("""\StringFileInfo\""")
77+
append(langCodePage.language.toHexString(wordHexFormat))
78+
append(langCodePage.codePage.toHexString(wordHexFormat))
79+
append("""\ProductVersion""")
80+
}
81+
82+
// Get the block corresponding to versionId
83+
val block = alloc<COpaquePointerVar>()
84+
val blockSize = alloc<UIntVar>()
85+
val result = VerQueryValueW(versionInfoPtr, versionId, block.ptr, blockSize.ptr)
86+
87+
return if (result == 0) {
88+
null
89+
} else {
90+
// Copy the bytes into a Kotlin byte array
91+
val blockBytes = ByteArray(blockSize.value.convert())
92+
blockBytes.usePinned { pinned ->
93+
memcpy(pinned.addressOf(0), block.value!!.reinterpret<ByteVar>(), blockSize.value.convert())
94+
}
95+
blockBytes.decodeToString()
96+
}
97+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.util
6+
7+
import kotlinx.coroutines.test.runTest
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertNotNull
11+
12+
class SystemPlatformProviderMingwTest {
13+
@Test
14+
fun testOsInfo() = runTest {
15+
val osInfo = PlatformProvider.System.osInfo()
16+
println(osInfo)
17+
assertEquals(OsFamily.Windows, osInfo.family)
18+
assertNotNull(osInfo.version)
19+
}
20+
}

0 commit comments

Comments
 (0)