Skip to content

Commit 03236bc

Browse files
Copilotalexandru
andcommitted
WIP: Add JVM target for build testing (incomplete)
Added JVM target alongside native to enable build testing in sandbox environment. Created expect/actual pattern for platform-specific code (file I/O, process execution, crypto). Current issues with source files having corruption from file copies. Need to properly restructure source files for multiplatform setup. Alternative approach needed - either: 1. Use JVM-only build temporarily for testing 2. Create proper multiplatform structure from scratch 3. Use Docker-based build that has internet access for native toolchain Co-authored-by: alexandru <[email protected]>
1 parent c149242 commit 03236bc

File tree

15 files changed

+236
-367
lines changed

15 files changed

+236
-367
lines changed

build.gradle.kts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ repositories {
1515
}
1616

1717
kotlin {
18-
// Configure native targets for Linux
18+
// JVM target for testing and development
19+
jvm {
20+
compilations.all {
21+
compilerOptions.configure {
22+
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
23+
}
24+
}
25+
}
26+
27+
// Native target for production
1928
linuxX64("native") {
2029
binaries {
2130
executable {
@@ -32,7 +41,7 @@ kotlin {
3241
}
3342

3443
sourceSets {
35-
val nativeMain by getting {
44+
val commonMain by getting {
3645
dependencies {
3746
// Arrow libraries with native support
3847
implementation(libs.arrow.core)
@@ -56,19 +65,32 @@ kotlin {
5665
// Coroutines
5766
implementation(libs.kotlinx.coroutines.core)
5867

59-
// Crypto for HMAC
60-
implementation(libs.kcrypto)
61-
6268
// Logging - using kotlin-logging with native support
6369
implementation(libs.kotlin.logging)
6470
}
6571
}
72+
73+
val jvmMain by getting {
74+
}
6675

67-
val nativeTest by getting {
76+
val nativeMain by getting {
77+
dependencies {
78+
// KCrypto only for native (not available in Maven Central, need to add repository)
79+
implementation("com.soywiz:krypto:6.0.1")
80+
}
81+
}
82+
83+
val commonTest by getting {
6884
dependencies {
6985
implementation(libs.kotlin.test)
7086
}
7187
}
88+
89+
val jvmTest by getting {
90+
}
91+
92+
val nativeTest by getting {
93+
}
7294
}
7395
}
7496

settings.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ dependencyResolutionManagement {
7676
.versionRef("kotlinLogging")
7777

7878
// Crypto for HMAC (native support)
79-
version("kcrypto", "5.4.0")
80-
library("kcrypto", "com.soywiz.korlibs.krypto", "krypto")
79+
version("kcrypto", "6.0.1")
80+
library("kcrypto", "com.soywiz.krypto", "krypto")
8181
.versionRef("kcrypto")
8282

8383
// https://github.com/ajalt/clikt

src/nativeMain/kotlin/org/alexn/hook/AppConfig.kt renamed to src/commonMain/kotlin/org/alexn/hook/AppConfig.kt

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@ package org.alexn.hook
55
import arrow.core.Either
66
import com.charleskorn.kaml.Yaml
77
import com.charleskorn.kaml.YamlConfiguration
8-
import kotlinx.cinterop.ExperimentalForeignApi
9-
import kotlinx.cinterop.toKString
108
import kotlinx.serialization.ExperimentalSerializationApi
119
import kotlinx.serialization.Serializable
12-
import platform.posix.fclose
13-
import platform.posix.fgets
14-
import platform.posix.fopen
1510
import kotlin.time.Duration
1611

1712
@Serializable
@@ -44,12 +39,11 @@ data class AppConfig(
4439
)
4540

4641
companion object {
47-
@OptIn(ExperimentalForeignApi::class)
4842
fun parseFile(filePath: String): Either<ConfigException, AppConfig> {
4943
val extension = filePath.substringAfterLast('.', "").lowercase()
5044

5145
val content = try {
52-
readFile(filePath)
46+
readFileContent(filePath)
5347
} catch (ex: Exception) {
5448
return Either.Left(
5549
ConfigException(
@@ -94,23 +88,6 @@ data class AppConfig(
9488
strictMode = false,
9589
),
9690
)
97-
98-
@OptIn(ExperimentalForeignApi::class)
99-
private fun readFile(path: String): String {
100-
val file = fopen(path, "r") ?: throw Exception("Cannot open file: $path")
101-
try {
102-
val content = StringBuilder()
103-
val buffer = ByteArray(4096)
104-
while (true) {
105-
val line = fgets(buffer.refTo(0), buffer.size, file)?.toKString()
106-
if (line == null) break
107-
content.append(line)
108-
}
109-
return content.toString()
110-
} finally {
111-
fclose(file)
112-
}
113-
}
11491
}
11592
}
11693

@@ -122,3 +99,6 @@ class ConfigException(
12299
message: String,
123100
cause: Throwable? = null,
124101
) : Exception(message, cause)
102+
103+
// Platform-specific file reading
104+
expect fun readFileContent(path: String): String

src/nativeMain/kotlin/org/alexn/hook/CommandTrigger.kt renamed to src/commonMain/kotlin/org/alexn/hook/CommandTrigger.kt

File renamed without changes.

src/nativeMain/kotlin/org/alexn/hook/EventPayload.kt renamed to src/commonMain/kotlin/org/alexn/hook/EventPayload.kt

Lines changed: 3 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package org.alexn.hook
33
import arrow.core.Either
44
import arrow.core.left
55
import arrow.core.right
6-
import com.soywiz.krypto.HMAC
7-
import com.soywiz.krypto.encoding.Hex
86
import io.ktor.http.ContentType
97
import kotlinx.serialization.ExperimentalSerializationApi
108
import kotlinx.serialization.Serializable
@@ -100,23 +98,6 @@ data class EventPayload(
10098
RequestError.BadInput("Invalid form-urlencoded data", null).left()
10199
}
102100

103-
// HMAC using KCrypto library with native support
104-
private fun hmacSha256(data: String, key: String): String {
105-
val hmac = HMAC.hmacSHA256(
106-
key.encodeToByteArray(),
107-
data.encodeToByteArray()
108-
)
109-
return Hex.encode(hmac).lowercase()
110-
}
111-
112-
private fun hmacSha1(data: String, key: String): String {
113-
val hmac = HMAC.hmacSHA1(
114-
key.encodeToByteArray(),
115-
data.encodeToByteArray()
116-
)
117-
return Hex.encode(hmac).lowercase()
118-
}
119-
120101
// Simple URL decoding for native
121102
private fun urlDecode(str: String): String {
122103
return str.replace("+", " ")
@@ -260,68 +241,7 @@ sealed class RequestError(
260241
class RequestException(
261242
message: String,
262243
cause: Throwable?,
263-
) : java.lang.Exception(message, cause)
264-
265-
sealed class RequestError(
266-
val httpCode: Int,
267-
) {
268-
abstract val message: String
269-
270-
fun toException(): Exception =
271-
when (this) {
272-
is BadInput ->
273-
RequestException("$httpCode Bad Input — $message", exception)
274-
is Forbidden ->
275-
RequestException("$httpCode Forbidden — $message", null)
276-
is Internal -> {
277-
val metaStr = (meta ?: mapOf()).map { "\n ${it.key}:${it.value}" }.joinToString("")
278-
RequestException("$httpCode Internal Server Error — $message$metaStr", exception)
279-
}
280-
is NotFound ->
281-
RequestException("$httpCode Not Found — $message", null)
282-
is Skipped ->
283-
RequestException("$httpCode Skipped — $message", null)
284-
is TimedOut ->
285-
RequestException("$httpCode Timed out — $message", null)
286-
is UnsupportedMediaType ->
287-
RequestException("$httpCode Unsupported Media Type — $message", null)
288-
}
289-
290-
data class BadInput(
291-
override val message: String,
292-
val exception: Exception? = null,
293-
) : RequestError(400)
294-
295-
data class Forbidden(
296-
override val message: String,
297-
) : RequestError(403)
298-
299-
data class Internal(
300-
override val message: String,
301-
val exception: Exception? = null,
302-
val meta: Map<String, String>? = null,
303-
) : RequestError(
304-
500,
305-
)
306-
307-
data class NotFound(
308-
override val message: String,
309-
) : RequestError(404)
310244

311-
data class Skipped(
312-
override val message: String,
313-
) : RequestError(200)
314-
315-
data class TimedOut(
316-
override val message: String,
317-
) : RequestError(408)
318-
319-
data class UnsupportedMediaType(
320-
override val message: String,
321-
) : RequestError(415)
322-
}
323-
324-
class RequestException(
325-
message: String,
326-
cause: Throwable?,
327-
) : Exception(message, cause)
245+
// Platform-specific HMAC implementations
246+
expect fun hmacSha256(data: String, key: String): String
247+
expect fun hmacSha1(data: String, key: String): String
File renamed without changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.alexn.hook
2+
3+
import arrow.core.Option
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.IO
6+
import kotlinx.coroutines.withContext
7+
8+
data class CommandResult(
9+
val exitCode: Int,
10+
val stdout: String,
11+
val stderr: String,
12+
) {
13+
val isSuccessful get() = exitCode == 0
14+
}
15+
16+
/**
17+
* Executes shell commands.
18+
*/
19+
suspend fun executeRawShellCommand(
20+
command: String,
21+
dir: File? = null,
22+
): CommandResult =
23+
withContext(Dispatchers.IO) {
24+
executeCommand(command, dir)
25+
}
26+
27+
// File abstraction
28+
data class File(val path: String)
29+
30+
val USER_HOME: File? by lazy {
31+
Option
32+
.fromNullable(getUserHomeDir())
33+
.filter { it.isNotEmpty() }
34+
.map { File(it) }
35+
.getOrNull()
36+
}
37+
38+
// Platform-specific implementations
39+
expect fun executeCommand(command: String, dir: File?): CommandResult
40+
expect fun getUserHomeDir(): String?
41+

src/nativeMain/kotlin/org/alexn/hook/Server.kt renamed to src/commonMain/kotlin/org/alexn/hook/Server.kt

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -148,97 +148,4 @@ private fun urlEncode(str: String): String {
148148
.replace("@", "%40")
149149
.replace("[", "%5B")
150150
.replace("]", "%5D")
151-
}
152-
configureRouting(appConfig, commandTrigger)
153-
}
154-
runInterruptible {
155-
server.start(wait = true)
156-
}
157-
}
158-
159-
fun Application.configureRouting(
160-
config: AppConfig,
161-
commandTriggerService: CommandTrigger,
162-
) {
163-
val logger: Logger by lazy {
164-
LoggerFactory.getLogger("org.alexn.hook.Routing")
165-
}
166-
val basePath = config.http.basePath
167-
168-
routing {
169-
if (config.http.basePath.isNotEmpty()) {
170-
get(config.http.basePath) {
171-
call.respondRedirect("$basePath/")
172-
}
173-
}
174-
175-
get("$basePath/") {
176-
call.respondHtml(HttpStatusCode.OK) {
177-
head {
178-
title { +"GitHub Webhook Listener" }
179-
}
180-
body {
181-
p { +"Configured hooks:" }
182-
ul {
183-
for (p in config.projects) {
184-
li { +URLEncoder.encode(p.key, UTF_8) }
185-
}
186-
}
187-
}
188-
}
189-
}
190-
191-
post("$basePath/{project}") {
192-
val projectKey = call.parameters["project"]
193-
if (projectKey == null) {
194-
call.respondText("Project key not specified", status = HttpStatusCode.BadRequest)
195-
return@post
196-
}
197-
198-
val response =
199-
either {
200-
val project = config.projects[projectKey]
201-
ensureNotNull(project) {
202-
RequestError.NotFound("Project `$projectKey` does not exist")
203-
}
204-
205-
val signature = call.request.header("X-Hub-Signature-256") ?: call.request.header("X-Hub-Signature")
206-
val body = call.receiveText()
207-
EventPayload
208-
.authenticateRequest(body, project.secret, signature)
209-
.bind()
210-
211-
val parsed =
212-
EventPayload.parse(call.request.contentType(), body).bind()
213-
214-
val result =
215-
if (parsed.shouldProcess(project)) {
216-
commandTriggerService.triggerCommand(projectKey)
217-
} else {
218-
RequestError.Skipped("Nothing to do for project `$projectKey`").left()
219-
}
220-
221-
result.bind()
222-
}
223-
224-
when (response) {
225-
is Either.Right -> {
226-
call.respondText("OK", status = HttpStatusCode.OK)
227-
logger.info("POST /$projectKey — OK")
228-
}
229-
is Either.Left -> {
230-
val err = response.value
231-
call.respondText(err.message, status = HttpStatusCode.fromValue(err.httpCode))
232-
when (err) {
233-
is RequestError.Skipped ->
234-
logger.info("POST /$projectKey — Skipped")
235-
else -> {
236-
val ex = err.toException()
237-
logger.warn("POST /$projectKey${ex.message}", ex.cause)
238-
}
239-
}
240-
}
241-
}
242-
}
243-
}
244151
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.alexn.hook
2+
3+
import javax.crypto.Mac
4+
import javax.crypto.spec.SecretKeySpec
5+
6+
actual fun hmacSha256(data: String, key: String): String {
7+
val mac = Mac.getInstance("HmacSHA256")
8+
mac.init(SecretKeySpec(key.toByteArray(), "HmacSHA256"))
9+
val bytes = mac.doFinal(data.toByteArray())
10+
return bytes.joinToString("") { "%02x".format(it) }
11+
}
12+
13+
actual fun hmacSha1(data: String, key: String): String {
14+
val mac = Mac.getInstance("HmacSHA1")
15+
mac.init(SecretKeySpec(key.toByteArray(), "HmacSHA1"))
16+
val bytes = mac.doFinal(data.toByteArray())
17+
return bytes.joinToString("") { "%02x".format(it) }
18+
}

0 commit comments

Comments
 (0)