Windows installer

This commit is contained in:
2026-08-24 14:47:14 +03:00
Unverified
parent d25cb7e57a
commit 529e090b25
61 changed files with 7395 additions and 49 deletions
@@ -0,0 +1,118 @@
package com.pr0gramm3r101.utils.files
import java.io.File
/**
* Resolves the FromChat desktop data/cache root on the JVM.
*
* - Portable (`-Dfromchat.portable=true`): `<exeDir>/fromchat-data`
* - Windows installed: `%LOCALAPPDATA%\FromChat`
* - macOS: `~/Library/Application Support/FromChat`
* - Linux: `${XDG_DATA_HOME:-~/.local/share}/FromChat`
*
* Migrates once from the legacy `~/.fromchat/cache` directory when present.
*/
internal object AppDataRoot {
private const val LEGACY_RELATIVE = ".fromchat/cache"
private const val PORTABLE_DIR_NAME = "fromchat-data"
private const val APP_DIR_NAME = "FromChat"
@Volatile
private var cached: File? = null
fun resolve(): File {
cached?.let { return it }
synchronized(this) {
cached?.let { return it }
val root = resolveUncached().also { it.mkdirs() }
migrateLegacyIfNeeded(root)
cached = root
return root
}
}
private fun resolveUncached(): File {
if (isPortable()) {
return File(executableDirectory(), PORTABLE_DIR_NAME)
}
val os = System.getProperty("os.name").orEmpty().lowercase()
return when {
os.contains("win") -> windowsInstalledRoot()
os.contains("mac") -> macInstalledRoot()
else -> linuxInstalledRoot()
}
}
private fun isPortable(): Boolean =
System.getProperty("fromchat.portable")
?.equals("true", ignoreCase = true) == true
private fun executableDirectory(): File {
System.getProperty("fromchat.exe.dir")?.takeIf { it.isNotBlank() }?.let {
return File(it).absoluteFile
}
// jpackage layout: <app>/runtime → parent is the install/portable folder
val javaHome = System.getProperty("java.home")
if (!javaHome.isNullOrBlank()) {
val runtime = File(javaHome).absoluteFile
if (runtime.name.equals("runtime", ignoreCase = true)) {
val parent = runtime.parentFile
if (parent != null) return parent
}
}
return File(System.getProperty("user.dir") ?: ".").absoluteFile
}
private fun windowsInstalledRoot(): File {
val localAppData = System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }
return if (localAppData != null) {
File(localAppData, APP_DIR_NAME)
} else {
File(requireHome(), APP_DIR_NAME)
}
}
private fun macInstalledRoot(): File =
File(requireHome(), "Library/Application Support/$APP_DIR_NAME")
private fun linuxInstalledRoot(): File {
val xdg = System.getenv("XDG_DATA_HOME")?.takeIf { it.isNotBlank() }
return if (xdg != null) {
File(xdg, APP_DIR_NAME)
} else {
File(requireHome(), ".local/share/$APP_DIR_NAME")
}
}
private fun requireHome(): File {
val home = System.getProperty("user.home")
return if (!home.isNullOrBlank()) {
File(home)
} else {
File(System.getProperty("java.io.tmpdir") ?: ".", "fromchat-home")
}
}
private fun legacyCacheDir(): File? {
val home = System.getProperty("user.home")?.takeIf { it.isNotBlank() } ?: return null
return File(home, LEGACY_RELATIVE)
}
private fun migrateLegacyIfNeeded(target: File) {
if (isPortable()) return
val legacy = legacyCacheDir() ?: return
if (!legacy.isDirectory) return
val marker = File(target, ".migrated-from-legacy-cache")
if (marker.isFile) return
val targetEmpty = target.list().isNullOrEmpty()
if (!targetEmpty) {
marker.writeText("skipped-non-empty\n")
return
}
runCatching {
legacy.copyRecursively(target, overwrite = false)
marker.writeText("ok\n")
legacy.deleteRecursively()
}
}
}
@@ -49,14 +49,8 @@ internal actual fun expectListFileNamesInDirectory(dirPath: String): List<String
return dir.list()?.toList().orEmpty()
}
internal actual fun expectGetAppCacheDirectory(): String {
val home = System.getProperty("user.home")
return if (!home.isNullOrBlank()) {
File(home, ".fromchat/cache").absolutePath
} else {
File(System.getProperty("java.io.tmpdir"), "fromchat").absolutePath
}
}
internal actual fun expectGetAppCacheDirectory(): String =
AppDataRoot.resolve().absolutePath
internal actual fun expectEnsureDirectory(path: String) {
File(path).mkdirs()