开发之旅
年末重磅 全网Kotlin Multiplatform首个StableDiffusion本地部署应用开发之旅A
全网 Kotlin Multiplatform 首个本地小语言模型(SLM)对话应用开发之旅C
全网 Kotlin Multiplatform 首个本地小语言模型(SLM)对话应用开发之旅B
全网 Kotlin Multiplatform 首个本地小语言模型(SLM)对话应用开发之旅A
逻辑带你从A到B,想象力带你去任何地方。 - 爱因斯坦
事已至此,不管怎么样,一个初步的版本总算发布了,全平台都基于Vulkun加速的StableDiffusion
但是呀,事情没有这么简单,我只有一两台设备,全平台所触及的地方可就多了!
MacOS上的权限异常问题
这是一个阿三哥反馈的问题,从报错日志可以看出,在MacOS内部目录的App应该只有只读权限,因为我们这个App开屏有Resource资源复制出来的操作,所以应该是这个阶段导致的异常
既然内部文件读取存在异常,那就看看JVM内部有没有适用的API,要不要逐个平台指定可读目录也是个问题
Java NIO.2 API提供了对使用临时文件夹/文件的支持,所以对于mac上dylib库的处理,可以改成使用createTempDirectory
- 通常,在Windows中,默认的临时文件夹为 C:\Temp , %Windows%\Temp 或每个用户所在的临时目录 Local Settings\Temp (此位置通常由TEMP 环境变量控制 )。
- 在Linux / Unix中,全局临时目录为 /tmp 和 /var/tmp 。前一行代码将返回默认位置,具体取决于操作系统。接下来,我们将学习如何创建一个临时文件夹/文件
所以JVM平台动态库处理如下
object NativeLibraryLoader {
private val loadedLibraries = mutableSetOf<String>()
@Synchronized // Ensure thread safety
fun loadFromResources(baseName: String) {
if (baseName in loadedLibraries) {
println("Native library '$baseName' already loaded.")
return
}
val osName = System.getProperty("os.name").lowercase()
val libFileName: String
val resourcePath: String
// Determine library file name and resource path based on OS
// Assumes the library files are directly in "libs/" within resources
when {
osName.contains("win") -> {
libFileName = "lib$baseName.dll"
resourcePath = "/libs/$libFileName" // Path relative to resources root
}
osName.contains("mac") -> {
libFileName = "lib$baseName.dylib"
resourcePath = "/libs/$libFileName"
}
osName.contains("nix") || osName.contains("nux") -> {
libFileName = "lib$baseName.so"
resourcePath = "/libs/$libFileName"
}
else -> {
throw UnsatisfiedLinkError("Unsupported OS: $osName for library '$baseName'")
}
}
println("Attempting to load '$libFileName' from resources path: '$resourcePath'")
val libFileStream: InputStream = NativeLibraryLoader::class.java.getResourceAsStream(resourcePath)
?: throw UnsatisfiedLinkError(
"Native library '$libFileName' not found in resources at path '$resourcePath'. " +
"Ensure it's in 'src/jvmMain/resources/libs/'."
)
val libFileLibraryStream: InputStream? = NativeLibraryLoader::class.java.getResourceAsStream("$resourcePath.a")
val tempLibFile: File
val tempLibLibraryFile: File
try {
// Create a temporary directory to hold the library files
val tempDir = java.nio.file.Files.createTempDirectory("native_libs_${baseName}_").toFile()
tempDir.deleteOnExit() // Clean up directory on exit
tempLibFile = File(tempDir, libFileName)
tempLibFile.deleteOnExit() // Important for cleanup
println("tempFile Name ${tempLibFile.absolutePath}")
FileOutputStream(tempLibFile).use { outputStream ->
libFileStream.use { input ->
input.copyTo(outputStream)
}
}
if(libFileLibraryStream != null){
tempLibLibraryFile = File(tempDir, "$libFileName.a")
tempLibLibraryFile.deleteOnExit()
FileOutputStream(tempLibLibraryFile).use { outputStream ->
libFileLibraryStream.use { input ->
input.copyTo(outputStream)
}
}
}
} catch (e: Exception) {
throw UnsatisfiedLinkError("Failed to create temporary file for library '$libFileName': ${e.message}").initCause(e) as UnsatisfiedLinkError
} finally {
try {
libFileStream.close()
libFileLibraryStream?.close()
} catch (e: Exception) {
// Log or ignore
}
}
try {
System.load(tempLibFile.absolutePath)
loadedLibraries.add(baseName)
println("Successfully loaded native library '$baseName' ('$libFileName') from temporary file: ${tempLibFile.absolutePath}")
} catch (e: UnsatisfiedLinkError) {
println("ERROR: Failed to load native library '$baseName' from ${tempLibFile.absolutePath}: ${e.message}")
// Add more debug info if needed, e.g., if the DLL has other dependencies not found
if (osName.contains("win") && e.message?.contains("Can't find dependent libraries") == true) {
println("This error on Windows might indicate that '$libFileName' has other DLL dependencies that are not in the system PATH or alongside the loaded DLL.")
}
throw e // Re-throw the original error
}
}
}
但这就完了吗?并没有
这还越改越多了...
可以知道的是,vunkun.lib库找不到,但本地另一台机子又可以了,见鬼了
思来想去,MacOS又不只有Vulkun,还有Metal呀
if(APPLE)
set(SD_METAL ON CACHE BOOL "sd: metal backend" FORCE)
set(SD_VULKAN OFF CACHE BOOL "sd: vulkan backend" FORCE)
else()
set(SD_METAL OFF CACHE BOOL "sd: metal backend" FORCE)
set(SD_VULKAN ON CACHE BOOL "sd: vulkan backend" FORCE)
endif()
这不立马好了吗,记住呀,不要全平台死脑筋
Window上不是有效的win32应用异常
卧槽,真的无语,一堆乱码,难道的Kotlin Multiplatform全平台之路就此搁浅吗?
大咩,不要呀!🤣🤣🤣
老惯例,不懂的就给到AI就好了,人生第一次感觉AI这么强大,竟然解析出来,指出dll是32位程序编译出来的,这就好办多了
在window平台上,强制x64架构编译就好了
abstract class BuildNativeLibTask : DefaultTask() {
@get:Inject
abstract val execOps: ExecOperations
@get:Inject
abstract val fs: FileSystemOperations
// 定义输入参数,Gradle 需要知道这些才能处理缓存
@get:Input
abstract val platformName: Property<String>
@get:Input
abstract val cmakeGenerator: Property<String>
@get:Input
abstract val cmakeOptions: ListProperty<String>
@get:Internal // 标记为 Internal 因为这不是构建的输入/输出文件,而是工作目录
abstract val targetWorkingDir: Property<File>
@TaskAction
fun execute() {
val platform = platformName.get()
println("正在为当前平台 $platform 构建原生库 (TaskAction)")
val workDir = targetWorkingDir.get()
val generator = cmakeGenerator.get()
val options = cmakeOptions.get()
// 1. Configure CMake
execOps.exec {
workingDir = workDir
commandLine("cmake", "-S", ".", "-B", "build-$platform", "-G", generator)
args(options)
isIgnoreExitValue = false
}
// 2. Build CMake
execOps.exec {
workingDir = workDir
commandLine("cmake", "--build", "build-$platform", "--config", "Release")
isIgnoreExitValue = false
}
}
}
// 捕获 Configuration Phase 的变量,供 Execution Phase 使用,避免 configuration cache 问题
val rootDirVal = rootDir
val desktopPlatforms = listOf("windows", "macos", "linux")
desktopPlatforms.forEach { platform ->
tasks.register<BuildNativeLibTask>("buildNativeLibFor${platform.capitalize()}") {
println("配置 buildNativeLibFor${platform.capitalize()} 任务")
// --- 配置阶段 (Configuration Phase) ---
// 在这里赋值给 Task 的 Property,此时可以使用 project 上下文
// 注意:cmake -S . 通常需要指向包含 CMakeLists.txt 的目录,而不是具体cpp文件。假设是上一级目录:
this.targetWorkingDir.set(file("$rootDirVal/cpp/diffusion-loader.cpp"))
this.platformName.set(platform)
val currentPlatformName = platform // 捕获循环变量
val generator = when(currentPlatformName) {
"windows" -> {
// GitHub Actions 和 CI 环境使用 Visual Studio
// 本地开发可以通过环境变量 USE_MINGW=true 切换到 MinGW
if (System.getenv("USE_MINGW") == "true") {
"MinGW Makefiles"
} else {
"Visual Studio 17 2022"
}
}
//"macos" -> "Xcode"
"linux" -> "Unix Makefiles"
else -> "Unix Makefiles"
}
this.cmakeGenerator.set(generator)
val options = when(platform) {
"windows" -> {
// 为 Visual Studio generator 明确指定 x64 架构
if (System.getenv("USE_MINGW") != "true") {
listOf("-A", "x64")
} else {
listOf()
}
}
"macos" -> listOf("-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64")
else -> listOf()
}
// 注意: 使用 Visual Studio generator 时,不需要手动指定编译器路径
// MinGW 本地开发备注:
// 如需使用 MinGW,设置环境变量 USE_MINGW=true 并确保以下路径正确:
// cmakeOptions.add("-DCMAKE_C_COMPILER=D:/MyApp/Code/mingw64/bin/x86_64-w64-mingw32-gcc.exe")
// cmakeOptions.add("-DCMAKE_CXX_COMPILER=D:/MyApp/Code/mingw64/bin/x86_64-w64-mingw32-g++.exe")
// cmakeOptions.add("-DCMAKE_MAKE_PROGRAM=C:/msys64/mingw64/bin/mingw32-make.exe")
this.cmakeOptions.set(options)
// 检查是否为当前平台,只有当前平台才执行 TaskAction
// 注意:TaskAction 无法被动态跳过(除了 onlyIf),但我们可以用 onlyIf
val osName = System.getProperty("os.name").lowercase(Locale.getDefault())
val isCurrentPlatform = when(platform) {
"windows" -> osName.contains("windows")
"macos" -> osName.contains("mac")
"linux" -> osName.contains("linux")
else -> false
}
onlyIf { isCurrentPlatform }
// 捕获需要的路径字符串,供 doLast 使用
val cppLibsDirStr = cppLibsDirVal
val jvmResourceLibDirStr = jvmResourceLibDirVal
doLast {
// 这里只能使用局部变量 cppLibsDirStr, jvmResourceLibDirStr
// 绝对不能用 project.file 或 rootDirVal,也就是全局变量,也不能使用全局方法
val srcDir = File(cppLibsDirStr)
val destDir = File(jvmResourceLibDirStr)
// 迁移到JVM资源目录
if (!destDir.exists()) destDir.mkdirs()
if (srcDir.exists() && srcDir.isDirectory) {
srcDir.listFiles { _, name ->
name.endsWith(".dll") || name.endsWith(".dll.a") ||
name.endsWith(".so") || name.endsWith(".dylib")
}?.forEach { f ->
f.copyTo(File(destDir, f.name), overwrite = true)
}
println("第一次SO迁移到JVM资源目录")
println("cppLibsDirVal:$cppLibsDirStr")
println("jvmResourceLibDirStr:$jvmResourceLibDirStr")
println("${destDir.listFiles().map { it.name }}")
}
}
}
}