feat: 支持 macOS 签名以及 Windows MSI 安装包 (#71)

This commit is contained in:
hstyi
2025-01-14 19:03:41 +08:00
committed by GitHub
parent 5027fd9dfb
commit 6881b6376f
4 changed files with 200 additions and 26 deletions

View File

@@ -46,6 +46,10 @@ flatlaf 3.5.4
Apache License 2.0 Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE 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 flatlaf-extras 3.5.4
Apache License 2.0 Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE

View File

@@ -1,8 +1,7 @@
import org.gradle.internal.jvm.Jvm import org.gradle.internal.jvm.Jvm
import org.gradle.kotlin.dsl.support.uppercaseFirstChar 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.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 import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
plugins { plugins {
@@ -16,9 +15,18 @@ plugins {
group = "app.termora" group = "app.termora"
version = "1.0.1" version = "1.0.1"
val os: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
var arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() 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 { repositories {
mavenCentral() mavenCentral()
@@ -27,6 +35,9 @@ repositories {
} }
dependencies { dependencies {
// 由于签名和公证macOS 不携带 natives
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation(libs.hutool) testImplementation(libs.hutool)
testImplementation(libs.sshj) testImplementation(libs.sshj)
@@ -50,9 +61,25 @@ dependencies {
implementation(libs.commons.compress) implementation(libs.commons.compress)
implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.swing)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.flatlaf)
implementation(libs.flatlaf.extras) implementation(libs.flatlaf) {
implementation(libs.flatlaf.swingx) 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.kotlinx.serialization.json)
implementation(libs.swingx) implementation(libs.swingx)
implementation(libs.jgoodies.forms) implementation(libs.jgoodies.forms)
@@ -104,8 +131,44 @@ tasks.test {
} }
tasks.register<Copy>("copy-dependencies") { tasks.register<Copy>("copy-dependencies") {
from(configurations.runtimeClasspath) val dir = layout.buildDirectory.dir("libs")
.into("${layout.buildDirectory.get()}/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<Exec>("jlink") { tasks.register<Exec>("jlink") {
@@ -137,6 +200,7 @@ tasks.register<Exec>("jlink") {
} }
tasks.register<Exec>("jpackage") { tasks.register<Exec>("jpackage") {
val buildDir = layout.buildDirectory.get() val buildDir = layout.buildDirectory.get()
val options = mutableListOf( val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
@@ -165,6 +229,9 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--temp", "$buildDir/jpackage")) arguments.addAll(listOf("--temp", "$buildDir/jpackage"))
arguments.addAll(listOf("--dest", "$buildDir/distributions")) arguments.addAll(listOf("--dest", "$buildDir/distributions"))
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE))) 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) { if (os.isMacOsX) {
@@ -194,6 +261,12 @@ tasks.register<Exec>("jpackage") {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
if (os.isMacOsX && macOSSign) {
arguments.add("--mac-sign")
arguments.add("--mac-signing-key-user-name")
arguments.add(macOSSignUsername)
}
commandLine(arguments) commandLine(arguments)
} }
@@ -201,53 +274,113 @@ tasks.register<Exec>("jpackage") {
tasks.register("dist") { tasks.register("dist") {
doLast { doLast {
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage") if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
throw GradleException("JVM: $vendor is not supported") throw GradleException("JVM: $vendor is not supported")
} }
val distributionDir = layout.buildDirectory.dir("distributions").get() val distributionDir = layout.buildDirectory.dir("distributions").get()
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath 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") } exec { commandLine(gradlew, "check-license") }
// jlink // jlink
exec { commandLine(gradlew, "jlink") } exec {
commandLine(gradlew, "jlink")
environment("ENABLE_BUILD" to true)
}
// 打包 // 打包
exec { commandLine(gradlew, "jpackage") } exec { commandLine(gradlew, "jpackage") }
// pack // pack
exec { if (os.isWindows) { // zip and msi
if (os.isWindows) { // zip // zip
exec {
commandLine( commandLine(
"tar", "-vacf", "tar",
distributionDir.file("${project.name}-${project.version}-windows-${arch.name}.zip").asFile.absolutePath, "-vacf",
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
project.name.uppercaseFirstChar() project.name.uppercaseFirstChar()
) )
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
} else if (os.isLinux) { // tar.gz }
// msi
exec {
commandLine( commandLine(
"tar", "-czvf", "cmd", "/c", "move",
distributionDir.file("${project.name}-${project.version}-linux-${arch.name}.tar.gz").asFile.absolutePath, "${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() project.name.uppercaseFirstChar()
) )
workingDir = distributionDir.asFile workingDir = distributionDir.asFile
} else if (os.isMacOsX) { // rename }
} else if (os.isMacOsX) { // rename
exec {
commandLine( commandLine(
"mv", "mv",
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath, 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",
)
}
} }
} }
} }

View File

@@ -1,6 +1,44 @@
package app.termora 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() { fun main() {
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
if (SystemUtils.IS_OS_MAC_OSX) {
setupNativeLibraries()
}
ApplicationRunner().run() 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)
}
}

View File

@@ -88,7 +88,6 @@ class TerminalLoggerAction : AnAction(I18n.getString("termora.terminal-logger"),
} }
fc.defaultDirectory = getLogDir().absolutePath fc.defaultDirectory = getLogDir().absolutePath
println(fc.defaultDirectory)
fc.showOpenDialog(owner).thenAccept { files -> fc.showOpenDialog(owner).thenAccept { files ->
if (files.isNotEmpty()) { if (files.isNotEmpty()) {
SwingUtilities.invokeLater { SwingUtilities.invokeLater {