Skip to content

Commit

Permalink
Add MultiplatformSystem utils (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt authored May 15, 2024
1 parent 8ec9af4 commit 0bd7b4b
Show file tree
Hide file tree
Showing 23 changed files with 206 additions and 32 deletions.
7 changes: 2 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,12 @@ jobs:
- name: Run R8 Jar
if: ${{ matrix.os == 'ubuntu-latest' }}
run: java -jar test/proguard/build/libs/main-r8.jar
- name: Bundle the build report
if: failure()
run: find . -type d -name 'reports' | zip -@ -r build-reports.zip
- name: Upload the build report
if: failure()
uses: actions/upload-artifact@master
with:
name: error-report
path: build-reports.zip
name: build-report-${{ matrix.os }}
path: '**/build/reports'
publish-snapshot:
needs: test
runs-on: macos-latest
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
## Unreleased
### Added
- Publish `iosArm64` and `iosX64` targets.
- Added `MultiplatformSystem` that provides multiplatform implementations of some non-terminal functionality that commonly used for command line apps: `readEnvironmentVariable`, `exitProcess`, and `readFileAsUtf8`.

## 2.5.0
### Added
- Publish `linuxArm64` and `wasmJs` targets.
- Publish `linuxArm64` and `wasmJs` targets.

## 2.4.0
This release includes a complete rewrite of the progress bar system. The new system is more
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ kotlin {
val posixMain by creating { dependsOn(nativeMain.get()) }
linuxMain.get().dependsOn(posixMain)
appleMain.get().dependsOn(posixMain)
val appleNonDesktopMain by creating { dependsOn(appleMain.get()) }
for (target in listOf(iosMain, tvosMain, watchosMain)) {
target.get().dependsOn(appleNonDesktopMain)
}
}
}
7 changes: 7 additions & 0 deletions mordant/api/mordant.api
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ public final class com/github/ajalt/mordant/markdown/Markdown : com/github/ajalt
public fun render (Lcom/github/ajalt/mordant/terminal/Terminal;I)Lcom/github/ajalt/mordant/rendering/Lines;
}

public final class com/github/ajalt/mordant/platform/MultiplatformSystem {
public static final field INSTANCE Lcom/github/ajalt/mordant/platform/MultiplatformSystem;
public final fun exitProcess (I)V
public final fun readEnvironmentVariable (Ljava/lang/String;)Ljava/lang/String;
public final fun readFileAsUtf8 (Ljava/lang/String;)Ljava/lang/String;
}

public final class com/github/ajalt/mordant/rendering/AnsiLevel : java/lang/Enum {
public static final field ANSI16 Lcom/github/ajalt/mordant/rendering/AnsiLevel;
public static final field ANSI256 Lcom/github/ajalt/mordant/rendering/AnsiLevel;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalForeignApi::class)

package com.github.ajalt.mordant.internal

import kotlinx.cinterop.ExperimentalForeignApi
Expand All @@ -8,7 +10,6 @@ import platform.posix.TIOCGWINSZ
import platform.posix.ioctl
import platform.posix.winsize

@OptIn(ExperimentalForeignApi::class)
internal actual fun getTerminalSize(): Size? {
return memScoped {
val size = alloc<winsize>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.ajalt.mordant.internal

internal actual fun hasFileSystem(): Boolean = false
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,9 @@ internal expect fun sendInterceptedPrintRequest(
internal expect val FAST_ISATTY: Boolean

internal expect val CR_IMPLIES_LF: Boolean

internal expect fun exitProcessMpp(status: Int)

internal expect fun readFileIfExists(filename: String): String?

internal expect fun hasFileSystem(): Boolean
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.github.ajalt.mordant.platform

import com.github.ajalt.mordant.internal.exitProcessMpp
import com.github.ajalt.mordant.internal.getEnv
import com.github.ajalt.mordant.internal.readFileIfExists

/**
* Utility functions useful for multiplatform command line apps interacting with the system.
*/
object MultiplatformSystem {
/**
* Get the value of an environment variable.
*
* If the variable is not set, this function returns `null`.
*/
fun readEnvironmentVariable(key: String): String? {
return getEnv(key)
}

/**
* Immediately exit the process with the given [status] code.
*
* On browsers, where it's not possible to exit the process, this function is a no-op.
*/
fun exitProcess(status: Int) {
exitProcessMpp(status)
}

/**
* Read the contents of a file as a UTF-8 string.
*
* @return The file contents decoded as a UTF-8 string, or `null` if the file could not be read.
*/
fun readFileAsUtf8(path: String): String? {
return try {
readFileIfExists(path)
} catch (e: Throwable) {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.ajalt.mordant.platform

import com.github.ajalt.mordant.internal.hasFileSystem
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldNotBeEmpty
import kotlin.test.Test


class MultiplatformSystemTest {
@Test
fun readEnvironmentVariable() {
// The kotlin.test plugin doesn't provide a way to set environment variables that works on
// all targets, so just pick a common one that should exist everywhere.
val actual = MultiplatformSystem.readEnvironmentVariable("PATH")
if (!hasFileSystem()) return
actual.shouldNotBeNull().shouldNotBeEmpty()
}

@Test
fun readFileAsUtf8() {
val actual = MultiplatformSystem.readFileAsUtf8(
// Most targets have a cwd of $moduleDir
"src/commonTest/resources/multiplatform_system_test.txt"
) ?: MultiplatformSystem.readFileAsUtf8(
// js targets have a cwd of $projectDir/build/js/packages/mordant-mordant-test
"../../../../mordant/src/commonTest/resources/multiplatform_system_test.txt"
)
if (!hasFileSystem()) return
actual?.trim() shouldBe "pass"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pass
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ internal interface JsMppImpls {
fun printStderr(message: String, newline: Boolean)
fun readLineOrNull(): String?
fun makeTerminalCursor(terminal: Terminal): TerminalCursor
fun exitProcess(status: Int)
fun cwd(): String
fun readFileIfExists(filename: String): String?
}

private object BrowserMppImpls : JsMppImpls {
Expand All @@ -59,12 +62,18 @@ private object BrowserMppImpls : JsMppImpls {
override fun stderrInteractive(): Boolean = false
override fun getTerminalSize(): Size? = null
override fun printStderr(message: String, newline: Boolean) = browserPrintln(message)
override fun exitProcess(status: Int) {}
override fun cwd(): String {
return "??"
}

// readlnOrNull will just throw an exception on browsers
override fun readLineOrNull(): String? = readlnOrNull()
override fun makeTerminalCursor(terminal: Terminal): TerminalCursor {
return BrowserTerminalCursor(terminal)
}

override fun readFileIfExists(filename: String): String? = null
}

internal abstract class BaseNodeMppImpls<BufferT> : JsMppImpls {
Expand All @@ -88,7 +97,6 @@ internal abstract class BaseNodeMppImpls<BufferT> : JsMppImpls {

abstract fun allocBuffer(size: Int): BufferT
abstract fun readSync(fd: Int, buffer: BufferT, offset: Int, len: Int): Int

}

private val impls: JsMppImpls = makeNodeMppImpls() ?: BrowserMppImpls
Expand All @@ -103,6 +111,8 @@ internal actual fun printStderr(message: String, newline: Boolean) {
impls.printStderr(message, newline)
}

internal actual fun exitProcessMpp(status: Int): Unit = impls.exitProcess(status)

// hideInput is not currently implemented
internal actual fun readLineOrNullMpp(hideInput: Boolean): String? = impls.readLineOrNull()

Expand All @@ -111,6 +121,8 @@ internal actual fun makePrintingTerminalCursor(terminal: Terminal): TerminalCurs
return impls.makeTerminalCursor(terminal)
}

internal actual fun readFileIfExists(filename: String): String? = impls.readFileIfExists(filename)

// There are no shutdown hooks on browsers, so we don't need to do anything here
private class BrowserTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal)

Expand All @@ -126,3 +138,4 @@ internal actual fun sendInterceptedPrintRequest(
}

internal actual val FAST_ISATTY: Boolean = true
internal actual fun hasFileSystem(): Boolean = impls !is BrowserMppImpls
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ private class NodeMppImpls(private val fs: dynamic) : BaseNodeMppImpls<dynamic>(
override fun stdoutInteractive(): Boolean = js("Boolean(process.stdout.isTTY)") as Boolean
override fun stdinInteractive(): Boolean = js("Boolean(process.stdin.isTTY)") as Boolean
override fun stderrInteractive(): Boolean = js("Boolean(process.stderr.isTTY)") as Boolean
override fun exitProcess(status: Int) {
process.exit(status)
}
override fun cwd(): String {
return process.cwd() as String
}
override fun getTerminalSize(): Size? {
// For some undocumented reason, getWindowSize is undefined sometimes, presumably when isTTY
// is false
Expand All @@ -42,6 +48,10 @@ private class NodeMppImpls(private val fs: dynamic) : BaseNodeMppImpls<dynamic>(
override fun makeTerminalCursor(terminal: Terminal): TerminalCursor {
return NodeTerminalCursor(terminal)
}

override fun readFileIfExists(filename: String): String? {
return fs.readFileSync(filename, "utf-8") as? String
}
}

internal actual fun makeNodeMppImpls(): JsMppImpls? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import com.github.ajalt.mordant.internal.jna.JnaWin32MppImpls
import com.github.ajalt.mordant.internal.nativeimage.NativeImagePosixMppImpls
import com.github.ajalt.mordant.internal.nativeimage.NativeImageWin32MppImpls
import com.github.ajalt.mordant.terminal.*
import java.io.File
import java.lang.management.ManagementFactory
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.system.exitProcess

private class JvmAtomicRef<T>(value: T) : MppAtomicRef<T> {
private val ref = AtomicReference(value)
Expand Down Expand Up @@ -141,3 +143,14 @@ internal actual fun stdinInteractive(): Boolean = impls.stdinInteractive()
internal actual fun getTerminalSize(): Size? = impls.getTerminalSize()
internal actual val FAST_ISATTY: Boolean = true
internal actual val CR_IMPLIES_LF: Boolean = false
internal actual fun hasFileSystem(): Boolean = true

internal actual fun exitProcessMpp(status: Int) {
exitProcess(status)
}

internal actual fun readFileIfExists(filename: String): String? {
val file = File(filename)
if (!file.isFile) return null
return file.bufferedReader().use { it.readText() }
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Args = --initialize-at-build-time=com.github.ajalt.mordant.internal.MppImplKt
Args = --initialize-at-build-time=com.github.ajalt.mordant.internal.MppInternal_jvmKt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalForeignApi::class)

package com.github.ajalt.mordant.internal

import kotlinx.cinterop.ExperimentalForeignApi
Expand All @@ -8,14 +10,15 @@ import platform.posix.TIOCGWINSZ
import platform.posix.ioctl
import platform.posix.winsize

@OptIn(ExperimentalForeignApi::class)
internal actual fun getTerminalSize(): Size? {
return memScoped {
val size = alloc<winsize>()
if (ioctl(STDIN_FILENO, TIOCGWINSZ.toULong(), size) < 0) {
null
} else {
Size(width=size.ws_col.toInt(), height=size.ws_row.toInt())
Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
}
}
}

internal actual fun hasFileSystem(): Boolean = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.ajalt.mordant.internal

internal actual fun hasFileSystem(): Boolean = true
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ internal actual fun ttySetEcho(echo: Boolean) = memScoped {
}
SetConsoleMode(stdinHandle, newMode)
}

internal actual fun hasFileSystem(): Boolean = true
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import platform.posix.*
import kotlin.concurrent.AtomicInt
import kotlin.concurrent.AtomicReference
import kotlin.experimental.ExperimentalNativeApi
import kotlin.system.exitProcess

private class NativeAtomicRef<T>(value: T) : MppAtomicRef<T> {
private val ref = AtomicReference(value)
Expand Down Expand Up @@ -141,7 +142,7 @@ internal actual fun sendInterceptedPrintRequest(
terminalInterface: TerminalInterface,
interceptors: List<TerminalInterceptor>,
) {
while(printRequestLock.compareAndSet(0, 1)) {
while (printRequestLock.compareAndSet(0, 1)) {
// spin until we get the lock
}
try {
Expand All @@ -153,5 +154,26 @@ internal actual fun sendInterceptedPrintRequest(
}
}

internal actual fun readFileIfExists(filename: String): String? {
val file = fopen(filename, "r") ?: return null
val chunks = StringBuilder()
try {
memScoped {
val bufferLength = 64 * 1024
val buffer = allocArray<ByteVar>(bufferLength)

while (true) {
val chunk = fgets(buffer, bufferLength, file)?.toKString()
if (chunk.isNullOrEmpty()) break
chunks.append(chunk)
}
}
} finally {
fclose(file)
}
return chunks.toString()
}

internal actual fun exitProcessMpp(status: Int): Unit = exitProcess(status)
internal actual val FAST_ISATTY: Boolean = true
internal actual val CR_IMPLIES_LF: Boolean = false
Loading

0 comments on commit 0bd7b4b

Please sign in to comment.