diff --git a/THIRDPARTY b/THIRDPARTY index b0eaae5..7c7562c 100644 --- a/THIRDPARTY +++ b/THIRDPARTY @@ -46,6 +46,10 @@ flatlaf 3.5.4 Apache License 2.0 https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE +flatlaf 3.5.4-no-natives +Apache License 2.0 +https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE + flatlaf-extras 3.5.4 Apache License 2.0 https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE diff --git a/build.gradle.kts b/build.gradle.kts index 9c70eee..c14a89f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,7 @@ import org.gradle.internal.jvm.Jvm import org.gradle.kotlin.dsl.support.uppercaseFirstChar -import org.gradle.nativeplatform.platform.internal.ArchitectureInternal import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform -import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem +import org.jetbrains.kotlin.org.apache.commons.io.FileUtils import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils plugins { @@ -16,9 +15,18 @@ plugins { group = "app.termora" version = "1.0.1" -val os: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() -var arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() +val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() +val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture() +// macOS 签名信息 +val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY +val macOSSign = os.isMacOsX && macOSSignUsername.isNotBlank() + && System.getenv("TERMORA_MAC_SIGN").toBoolean() + +// macOS 公证信息 +val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE") ?: StringUtils.EMPTY +val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank() + && System.getenv("TERMORA_MAC_NOTARY").toBoolean() repositories { mavenCentral() @@ -27,6 +35,9 @@ repositories { } dependencies { + // 由于签名和公证,macOS 不携带 natives + val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean() + testImplementation(kotlin("test")) testImplementation(libs.hutool) testImplementation(libs.sshj) @@ -50,9 +61,25 @@ dependencies { implementation(libs.commons.compress) implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.core) - implementation(libs.flatlaf) - implementation(libs.flatlaf.extras) - implementation(libs.flatlaf.swingx) + + implementation(libs.flatlaf) { + artifact { + if (useNoNativesFlatLaf) { + classifier = "no-natives" + } + } + } + implementation(libs.flatlaf.extras) { + if (useNoNativesFlatLaf) { + exclude(group = "com.formdev", module = "flatlaf") + } + } + implementation(libs.flatlaf.swingx) { + if (useNoNativesFlatLaf) { + exclude(group = "com.formdev", module = "flatlaf") + } + } + implementation(libs.kotlinx.serialization.json) implementation(libs.swingx) implementation(libs.jgoodies.forms) @@ -104,8 +131,44 @@ tasks.test { } tasks.register("copy-dependencies") { - from(configurations.runtimeClasspath) - .into("${layout.buildDirectory.get()}/libs") + val dir = layout.buildDirectory.dir("libs") + from(configurations.runtimeClasspath).into(dir) + + // 对 JNA 和 PTY4J 的本地库提取 + // 提取出来是为了单独签名,不然无法通过公证 + if (os.isMacOsX) { + doLast { + val jna = libs.jna.asProvider().get() + val dylib = dir.get().dir("dylib").asFile + val pty4j = libs.pty4j.get() + for (file in dir.get().asFile.listFiles() ?: emptyArray()) { + if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) { + val targetDir = File(dylib, jna.name) + FileUtils.forceMkdir(targetDir) + // @formatter:off + exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) } + // @formatter:on + // 删除所有二进制类库 + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") } + } else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) { + val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin") + FileUtils.forceMkdir(targetDir) + // @formatter:off + exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) } + // @formatter:on + // 删除所有二进制类库 + exec { commandLine("zip", "-d", file.absolutePath, "resources/*") } + } + } + } + } } tasks.register("jlink") { @@ -137,6 +200,7 @@ tasks.register("jlink") { } tasks.register("jpackage") { + val buildDir = layout.buildDirectory.get() val options = mutableListOf( "--add-exports java.base/sun.nio.ch=ALL-UNNAMED", @@ -165,6 +229,9 @@ tasks.register("jpackage") { arguments.addAll(listOf("--temp", "$buildDir/jpackage")) arguments.addAll(listOf("--dest", "$buildDir/distributions")) arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE))) + arguments.addAll(listOf("--vendor", "TermoraDev")) + arguments.addAll(listOf("--copyright", "TermoraDev")) + arguments.addAll(listOf("--description", "A terminal emulator and SSH client.")) if (os.isMacOsX) { @@ -194,6 +261,12 @@ tasks.register("jpackage") { throw UnsupportedOperationException() } + if (os.isMacOsX && macOSSign) { + arguments.add("--mac-sign") + arguments.add("--mac-signing-key-user-name") + arguments.add(macOSSignUsername) + } + commandLine(arguments) } @@ -201,53 +274,113 @@ tasks.register("jpackage") { tasks.register("dist") { doLast { val vendor = Jvm.current().vendor ?: StringUtils.EMPTY - @Suppress("UnstableApiUsage") - if (!JvmVendorSpec.JETBRAINS.matches(vendor)) { + @Suppress("UnstableApiUsage") if (!JvmVendorSpec.JETBRAINS.matches(vendor)) { throw GradleException("JVM: $vendor is not supported") } val distributionDir = layout.buildDirectory.dir("distributions").get() val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath + val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux" + val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}" + val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath // 清空目录 - exec { commandLine(gradlew, "clean") } + exec { + commandLine(gradlew, "clean") + } // 打包并复制依赖 - exec { commandLine(gradlew, "jar", "copy-dependencies") } + exec { + commandLine(gradlew, "jar", "copy-dependencies") + environment("ENABLE_BUILD" to true) + } // 检查依赖的开源协议 exec { commandLine(gradlew, "check-license") } // jlink - exec { commandLine(gradlew, "jlink") } + exec { + commandLine(gradlew, "jlink") + environment("ENABLE_BUILD" to true) + } // 打包 exec { commandLine(gradlew, "jpackage") } // pack - exec { - if (os.isWindows) { // zip + if (os.isWindows) { // zip and msi + // zip + exec { commandLine( - "tar", "-vacf", - distributionDir.file("${project.name}-${project.version}-windows-${arch.name}.zip").asFile.absolutePath, + "tar", + "-vacf", + distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath, project.name.uppercaseFirstChar() ) workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile - } else if (os.isLinux) { // tar.gz + } + + // msi + exec { commandLine( - "tar", "-czvf", - distributionDir.file("${project.name}-${project.version}-linux-${arch.name}.tar.gz").asFile.absolutePath, + "cmd", "/c", "move", + "${project.name.uppercaseFirstChar()}-${project.version}.msi", + "${finalFilenameWithoutExtension}.msi" + ) + workingDir = distributionDir.asFile + } + } else if (os.isLinux) { // tar.gz + exec { + commandLine( + "tar", + "-czvf", + distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath, project.name.uppercaseFirstChar() ) workingDir = distributionDir.asFile - } else if (os.isMacOsX) { // rename + } + } else if (os.isMacOsX) { // rename + exec { commandLine( "mv", distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath, - distributionDir.file("${project.name}-${project.version}-osx-${arch.name}.dmg").asFile.absolutePath, + macOSFinalFilePath, ) - } else { - throw GradleException("${os.name} is not supported") + } + } else { + throw GradleException("${os.name} is not supported") + } + + + // sign dmg + if (os.isMacOsX && macOSSign) { + exec { + commandLine( + "/usr/bin/codesign", + "-s", + macOSSignUsername, + "--timestamp", + "--force", + "-vvvv", + "--options", + "runtime", + macOSFinalFilePath + ) + } + + // 公证 + if (macOSNotary) { + exec { + commandLine( + "/usr/bin/xcrun", + "notarytool", + "submit", + macOSFinalFilePath, + "--keychain-profile", + macOSNotaryKeychainProfile, + "--wait", + ) + } } } } diff --git a/src/main/kotlin/app/termora/Main.kt b/src/main/kotlin/app/termora/Main.kt index 6486990..9574e7f 100644 --- a/src/main/kotlin/app/termora/Main.kt +++ b/src/main/kotlin/app/termora/Main.kt @@ -1,6 +1,44 @@ package app.termora +import com.pty4j.util.PtyUtil +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.SystemUtils +import java.io.File + fun main() { + // 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹 + if (SystemUtils.IS_OS_MAC_OSX) { + setupNativeLibraries() + } + ApplicationRunner().run() } + +private fun setupNativeLibraries() { + if (!SystemUtils.IS_OS_MAC_OSX) { + return + } + + val appPath = Application.getAppPath() + if (StringUtils.isBlank(appPath)) { + return + } + + val contents = File(appPath).parentFile?.parentFile ?: return + val dylib = FileUtils.getFile(contents, "app", "dylib") + if (!dylib.exists()) { + return + } + + val jna = FileUtils.getFile(dylib, "jna") + if (jna.exists()) { + System.setProperty("jna.boot.library.path", jna.absolutePath) + } + + val pty4j = FileUtils.getFile(dylib, "pty4j") + if (pty4j.exists()) { + System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt b/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt index cd9b064..e324288 100644 --- a/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt +++ b/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt @@ -88,7 +88,6 @@ class TerminalLoggerAction : AnAction(I18n.getString("termora.terminal-logger"), } fc.defaultDirectory = getLogDir().absolutePath - println(fc.defaultDirectory) fc.showOpenDialog(owner).thenAccept { files -> if (files.isNotEmpty()) { SwingUtilities.invokeLater {