年末重磅 全网Kotlin Multiplatform首个LocalStableDiffusion应用开发之旅B

38 阅读4分钟

开发之旅

年末重磅 全网Kotlin Multiplatform首个StableDiffusion本地部署应用开发之旅A

全网 Kotlin Multiplatform 首个本地小语言模型(SLM)对话应用开发之旅C

全网 Kotlin Multiplatform 首个本地小语言模型(SLM)对话应用开发之旅B

全网 Kotlin Multiplatform 首个本地小语言模型(SLM)对话应用开发之旅A

splash.png

逻辑带你从A到B,想象力带你去任何地方。 - 爱因斯坦

事已至此,不管怎么样,一个初步的版本总算发布了,全平台都基于Vulkun加速的StableDiffusion

desktop_screenshot.gif

但是呀,事情没有这么简单,我只有一两台设备,全平台所触及的地方可就多了!

MacOS上的权限异常问题

屏幕截图_27-12-2025_13252_github.com.jpeg

这是一个阿三哥反馈的问题,从报错日志可以看出,在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
        }
    }
}

但这就完了吗?并没有

这还越改越多了...

截屏2025-12-27 18.32.24.png

可以知道的是,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()

这不立马好了吗,记住呀,不要全平台死脑筋

macos_screenshot.png

Window上不是有效的win32应用异常

QQ截图20251227142046.png

卧槽,真的无语,一堆乱码,难道的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 }}")
            }
        }
    }
}