diff --git a/build.gradle.kts b/build.gradle.kts index c904937..b2843a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,17 +39,23 @@ kotlin { val posixMain by creating { dependsOn(commonMain.get()) } + val macosMain by creating { + dependsOn(posixMain) + } + val linuxMain by creating { + dependsOn(posixMain) + } val macosArm64Main by getting { - dependsOn(posixMain) + dependsOn(macosMain) } val macosX64Main by getting { - dependsOn(posixMain) + dependsOn(macosMain) } val linuxArm64Main by getting { - dependsOn(posixMain) + dependsOn(linuxMain) } val linuxX64Main by getting { - dependsOn(posixMain) + dependsOn(linuxMain) } // HACK: Prevent "Variable is never used" warnings. // Unfortunately, @Suppress("UNUSED_PARAMETER") does not do the trick. diff --git a/configs/fiji.toml b/configs/fiji.toml index 7cd78d9..be6570a 100644 --- a/configs/fiji.toml +++ b/configs/fiji.toml @@ -89,7 +89,8 @@ modes = [ directives = [ '--print-ij-dir|print-app-dir,ABORT', # For backwards compatibility. - 'LAUNCH:JVM|INIT_THREADS', + 'LAUNCH:JVM|apply-update,INIT_THREADS', + 'LAUNCH:PYTHON|apply-update,INIT_THREADS', ] # /============================================================================\ diff --git a/src/commonMain/kotlin/file.kt b/src/commonMain/kotlin/file.kt index ee17793..57df7b2 100644 --- a/src/commonMain/kotlin/file.kt +++ b/src/commonMain/kotlin/file.kt @@ -13,8 +13,12 @@ expect class File(rawPath: String) { val isFile: Boolean val isDirectory: Boolean val isRoot: Boolean + val length : Long fun ls(): List fun lines(): List + fun mv(dest:File): Boolean + fun rm() : Boolean + fun rmdir() : Boolean } // -- Platform-agnostic File members -- @@ -44,6 +48,15 @@ val File.base: File return if (dot < lastSlash(path)) this else File(path.substring(0, dot)) } +fun File.mkdir(): Boolean { + if (!exists) return mkdir(path) + if (!isDirectory) { + warn("Error: '$path' already exists but is not a directory.") + return false + } + return true +} + operator fun File.div(p: String): File = File("$path$SLASH$p") // -- File-related utility functions -- diff --git a/src/commonMain/kotlin/main.kt b/src/commonMain/kotlin/main.kt index 8d43777..c8d9b04 100644 --- a/src/commonMain/kotlin/main.kt +++ b/src/commonMain/kotlin/main.kt @@ -98,7 +98,8 @@ fun main(args: Array) { // Declare the global (runtime-agnostic) directives. val globalDirectiveFunctions: DirectivesMap = mutableMapOf( - "help" to { _ -> help(exeFile, programName, supportedOptions) } + "help" to { _ -> help(exeFile, programName, supportedOptions) }, + "apply-update" to { _ -> applyUpdate(appDir, appDir / "update") } ) // Finally, execute all the directives! \^_^/ @@ -572,3 +573,30 @@ private fun help(exeFile: File?, programName: String, supportedOptions: JaunchOp val optionsUnique = linkedSetOf(*supportedOptions.values.toTypedArray()) optionsUnique.forEach { printlnErr(it.help()) } } + +private fun applyUpdate(appDir: File, updateSubDir: File) { + if (!updateSubDir.exists) return + + // Recursively copy over all files in the update subdir + for (file in updateSubDir.ls()) { + val dest = appDir / file.path.substring((appDir / "update").path.length) + if (file.isDirectory) { + debug("+ mkdir '$dest'") + dest.mkdir() || error("Couldn't create path $dest") + applyUpdate(appDir, file) + } + else { + if (file.length == 0L) { + debug("+ rm '$dest'") + dest.rm() || error("Couldn't remove $dest") + debug("+ rm '$file'") + file.rm() || error("Couldn't remove $file") + } else { + debug("+ mv '$file' '$dest'") + file.mv(dest) || error("Couldn't replace $dest") + } + } + } + + updateSubDir.rmdir() +} diff --git a/src/commonMain/kotlin/platform.kt b/src/commonMain/kotlin/platform.kt index be405bb..86e22eb 100644 --- a/src/commonMain/kotlin/platform.kt +++ b/src/commonMain/kotlin/platform.kt @@ -16,6 +16,8 @@ expect fun printlnErr(s: String = "") expect fun stdinLines(): Array +expect fun mkdir(path: String): Boolean + data class MemoryInfo(var total: Long? = null, var free: Long? = null) expect fun memInfo(): MemoryInfo diff --git a/src/linuxMain/kotlin/platform.kt b/src/linuxMain/kotlin/platform.kt new file mode 100644 index 0000000..1b79342 --- /dev/null +++ b/src/linuxMain/kotlin/platform.kt @@ -0,0 +1,13 @@ +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.* + +@OptIn(ExperimentalForeignApi::class) +actual fun mkdir(path: String): Boolean { + val result = mkdir(path, S_IRWXU.toUInt()) + if (result != 0) { + val errorCode = errno + platform.posix.warn("Error creating directory '$path': ${strerror(errorCode)?.toKString()}") + } + return result == 0 +} diff --git a/src/macosMain/kotlin/platform.kt b/src/macosMain/kotlin/platform.kt new file mode 100644 index 0000000..da8b5b4 --- /dev/null +++ b/src/macosMain/kotlin/platform.kt @@ -0,0 +1,13 @@ +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.* + +@OptIn(ExperimentalForeignApi::class) +actual fun mkdir(path: String): Boolean { + val result = mkdir(path, S_IRWXU.toUShort()) + if (result != 0) { + val errorCode = errno + platform.posix.warn("Error creating directory '$path': ${strerror(errorCode)?.toKString()}") + } + return result == 0 +} diff --git a/src/posixMain/kotlin/file.kt b/src/posixMain/kotlin/file.kt index f955089..b535f0b 100644 --- a/src/posixMain/kotlin/file.kt +++ b/src/posixMain/kotlin/file.kt @@ -14,6 +14,13 @@ actual class File actual constructor(private val rawPath: String) { actual val isRoot: Boolean = path == SLASH @OptIn(ExperimentalForeignApi::class) + actual val length: Long get() = memScoped { + val statResult = alloc() + stat(path, statResult.ptr) + return statResult.st_size + } + + @OptIn(ExperimentalForeignApi::class, UnsafeNumber::class) private fun isMode(modeBits: Int): Boolean { val statMode = memScoped { val statResult = alloc() @@ -67,6 +74,26 @@ actual class File actual constructor(private val rawPath: String) { return path } + @OptIn(ExperimentalForeignApi::class) + actual fun mv(dest: File): Boolean { + memScoped { + return rename(path, dest.path) == 0 + } + } + + @OptIn(ExperimentalForeignApi::class) + actual fun rm(): Boolean { + memScoped { + return remove(path) == 0 + } + } + + @OptIn(ExperimentalForeignApi::class) + actual fun rmdir(): Boolean { + memScoped { + return rmdir(path) == 0 + } + } } @OptIn(ExperimentalForeignApi::class) diff --git a/src/windowsMain/kotlin/file.kt b/src/windowsMain/kotlin/file.kt index 8a12ec2..9a99c20 100644 --- a/src/windowsMain/kotlin/file.kt +++ b/src/windowsMain/kotlin/file.kt @@ -1,5 +1,6 @@ import kotlinx.cinterop.* import platform.windows.* +import kotlin.math.min @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") actual class File actual constructor(private val rawPath: String) { @@ -16,15 +17,20 @@ actual class File actual constructor(private val rawPath: String) { actual val isDirectory: Boolean get() = isMode(FILE_ATTRIBUTE_DIRECTORY) - private fun isMode(modeBits: Int): Boolean { - val attrs = GetFileAttributesA(path) - return attrs != INVALID_FILE_ATTRIBUTES && (attrs.toInt() and modeBits) != 0 - } - actual val isRoot: Boolean = // Is it a drive letter plus backslash (e.g. `C:\`)? path.length == 3 && (path[0] in 'a'..'z' || path[0] in 'A'..'Z') && path[1] == ':' && path[2] == '\\' + @OptIn(ExperimentalForeignApi::class) + actual val length: Long get() { + val fileHandle = openFile(path) ?: return -1 + try { + return fileSize(fileHandle) ?: -1 + } finally { + CloseHandle(fileHandle) + } + } + @OptIn(ExperimentalForeignApi::class) actual fun ls(): List { if (!isDirectory) throw IllegalArgumentException("Not a directory: $path") @@ -55,45 +61,90 @@ actual class File actual constructor(private val rawPath: String) { @OptIn(ExperimentalForeignApi::class) actual fun lines(): List { val lines = mutableListOf() - memScoped { - val fileHandle = CreateFileA( - path, - GENERIC_READ, - FILE_SHARE_READ.toUInt(), - null, - OPEN_EXISTING.toUInt(), - FILE_ATTRIBUTE_NORMAL.toUInt(), - null - ) - - if (fileHandle == INVALID_HANDLE_VALUE) { - println("Error opening file: ${GetLastError()}") - return emptyList() - } - + val fileHandle = openFile(path) ?: return lines try { - val fileSize = GetFileSize(fileHandle, null).toInt() - val buffer = allocArray(fileSize) + val fileSize = fileSize(fileHandle) ?: return lines + val size = min(fileSize, Int.MAX_VALUE.toLong()).toInt() + if (size < fileSize) warn("Reading only $size bytes of large file $path") + val buffer = allocArray(size) val bytesRead = alloc() // TODO: Is bytesRead < fileSize possible? If so, do we need to loop here? - if (ReadFile(fileHandle, buffer, fileSize.toUInt(), bytesRead.ptr, null) != 0) { + if (ReadFile(fileHandle, buffer, size.toUInt(), bytesRead.ptr, null) != 0) { lines.addAll(buffer.toKString().split(Regex("(\\r\\n|\\n)"))) } else { - println("Error reading file: ${GetLastError()}") + printlnErr("Error reading file: ${GetLastError()}") } } finally { CloseHandle(fileHandle) } } - return lines } override fun toString(): String { return path } + + @OptIn(ExperimentalForeignApi::class) + actual fun mv(dest: File): Boolean { + memScoped { + val pathW = path.wcstr.ptr + val destW = dest.path.wcstr.ptr + val flags = MOVEFILE_REPLACE_EXISTING.toUInt() + return MoveFileEx!!(pathW, destW, flags) != 0 + } + } + + @OptIn(ExperimentalForeignApi::class) + actual fun rm(): Boolean { + memScoped { + return DeleteFileW(path) != 0 + } + } + + @OptIn(ExperimentalForeignApi::class) + actual fun rmdir(): Boolean { + memScoped { + return RemoveDirectoryW(path) != 0 + } + } + + private fun isMode(modeBits: Int): Boolean { + val attrs = GetFileAttributesA(path) + return attrs != INVALID_FILE_ATTRIBUTES && (attrs.toInt() and modeBits) != 0 + } + + @OptIn(ExperimentalForeignApi::class) + private fun openFile(path: String): HANDLE? { + val fileHandle = CreateFileW( + path, + GENERIC_READ, + FILE_SHARE_READ.toUInt(), + null, + OPEN_EXISTING.toUInt(), + FILE_ATTRIBUTE_NORMAL.toUInt(), + null + ) + + if (fileHandle == INVALID_HANDLE_VALUE) { + warn("Error opening file: ${GetLastError()}") + return null + } + return fileHandle + } + + @OptIn(ExperimentalForeignApi::class) + private fun fileSize(fileHandle: HANDLE): Long? = memScoped { + val fileSize = alloc() + if (GetFileSizeEx(fileHandle, fileSize.ptr) == 0) { + warn("Error getting file size: ${GetLastError()}") + return null + } + return fileSize.QuadPart + } } + @OptIn(ExperimentalForeignApi::class) private fun canonicalize(path: String): String { if (path.isEmpty()) return canonicalize(".") @@ -109,8 +160,7 @@ private fun canonicalize(path: String): String { val bufferLength = MAX_PATH memScoped { val buffer = allocArray(bufferLength) - val fullPathLength = GetFullPathName?.let { it(p.wcstr.ptr, bufferLength.toUInt(), buffer, null) } ?: - throw RuntimeException("GetFullPathName function not available") + val fullPathLength = GetFullPathName!!(p.wcstr.ptr, bufferLength.toUInt(), buffer, null) if (fullPathLength == 0u) throw RuntimeException("Failed to get full path: ${GetLastError()}") return buffer.toKString() } diff --git a/src/windowsMain/kotlin/platform.kt b/src/windowsMain/kotlin/platform.kt index 9545363..ba034e1 100644 --- a/src/windowsMain/kotlin/platform.kt +++ b/src/windowsMain/kotlin/platform.kt @@ -63,6 +63,23 @@ actual fun stdinLines(): Array { return lines } +@OptIn(ExperimentalForeignApi::class) +actual fun mkdir(path: String): Boolean { + memScoped { + val result = CreateDirectoryW(path, null) + if (result == 0) { + val errorCode = GetLastError() + if (errorCode == ERROR_ALREADY_EXISTS.toUInt()) { + return true + } else { + warn("Error creating directory '$path': $errorCode") + return false + } + } + return true + } +} + @OptIn(ExperimentalForeignApi::class) actual fun memInfo(): MemoryInfo { val memInfo = MemoryInfo() @@ -75,7 +92,7 @@ actual fun memInfo(): MemoryInfo { memInfo.total = memoryStatus.ullTotalPhys.toLong() memInfo.free = memoryStatus.ullAvailPhys.toLong() } else { - println("Error getting memory status: ${GetLastError()}") + printlnErr("Error getting memory status: ${GetLastError()}") } } return memInfo