chore: Windows MSIX

This commit is contained in:
hstyi
2025-07-16 14:24:59 +08:00
committed by hstyi
parent cee7eb8928
commit c8d70e2a5a
13 changed files with 292 additions and 212 deletions

View File

@@ -15,7 +15,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 1
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
@@ -59,11 +59,23 @@ jobs:
shell: bash shell: bash
run: sudo chmod -R 777 ~/.gradle run: sudo chmod -R 777 ~/.gradle
- name: Upload artifact - name: Upload targz artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: termora-linux-${{ runner.arch }} name: termora-linux-targz-${{ runner.arch }}
path: | path: |
build/distributions/*.tar.gz build/distributions/*.tar.gz
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-AppImage-${{ runner.arch }}
path: |
build/distributions/*.AppImage build/distributions/*.AppImage
- name: Upload deb artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-deb-${{ runner.arch }}
path: |
build/distributions/*.deb build/distributions/*.deb

View File

@@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 1
- name: Install the Apple certificate - name: Install the Apple certificate
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''" if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''"
@@ -93,10 +93,16 @@ jobs:
shell: bash shell: bash
run: ./gradlew :jpackage && ./gradlew :dist run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload artifact - name: Upload zip artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: termora-osx-${{ runner.arch }} name: termora-osx-zip-${{ runner.arch }}
path: | path: |
build/distributions/*.zip build/distributions/*.zip
- name: Upload dmg artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-dmg-${{ runner.arch }}
path: |
build/distributions/*.dmg build/distributions/*.dmg

View File

@@ -11,11 +11,14 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ windows-11-arm, windows-latest ] os: [ windows-11-arm, windows-2022 ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 1
- name: Setup MSbuild
uses: microsoft/setup-msbuild@v2
- name: Set architecture - name: Set architecture
id: set-arch id: set-arch
@@ -26,6 +29,24 @@ jobs:
echo "ARCH=x64" >> $env:GITHUB_ENV echo "ARCH=x64" >> $env:GITHUB_ENV
} }
- name: Find MakeAppx
shell: pwsh
run: |
$installedRootsKey = "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots"
$kitsRoot = (Get-ItemProperty $installedRootsKey).KitsRoot10
$versions = Get-ChildItem -Path $installedRootsKey | Select-Object -ExpandProperty PSChildName
$maxVersion = $versions | ForEach-Object { [version]$_ } | Sort-Object -Descending | Select-Object -First 1
$arch = if ($env:ARCH -eq "aarch64") { "arm64" } else { "x64" }
$makeAppXPath = Join-Path -Path $kitsRoot -ChildPath "bin\$maxVersion\$arch\makeappx.exe"
Write-Output "MakeAppx.exe path: $makeAppXPath"
if (Test-Path $makeAppXPath) {
"MAKEAPPX_PATH=$makeAppXPath" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
} else {
Write-Output "MakeAppx.exe not found!"
exit 1
}
- name: Install zip - name: Install zip
run: | run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32" $system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
@@ -63,13 +84,33 @@ jobs:
- name: Package - name: Package
run: .\gradlew :jpackage && .\gradlew :dist run: .\gradlew :jpackage && .\gradlew :dist
- name: MSIX
env:
TERMORA_TYPE: appx
run: |
.\gradlew --stop
.\gradlew :dist
- name: Stop Gradle - name: Stop Gradle
run: .\gradlew.bat --stop run: .\gradlew.bat --stop
- name: Upload artifact - name: Upload zip artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: termora-windows-${{ runner.arch }} name: termora-windows-zip-${{ runner.arch }}
path: | path: |
build/distributions/*.zip build/distributions/*.zip
- name: Upload exe artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-exe-${{ runner.arch }}
path: |
build/distributions/*.exe build/distributions/*.exe
- name: Upload msix artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-msix-${{ runner.arch }}
path: |
build/distributions/*.msix

View File

@@ -1,3 +1,4 @@
import org.apache.tools.ant.filters.ReplaceTokens
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.ArchitectureInternal
@@ -28,7 +29,9 @@ version = rootProject.projectDir.resolve("VERSION").readText().trim()
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
val appVersion = project.version.toString().split("-")[0] val appVersion = project.version.toString().split("-")[0]
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb" val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
// macOS 签名信息 // macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -169,102 +172,145 @@ publishing {
} }
} }
tasks.processResources {
filesMatching("**/AppxManifest.xml") {
filter<ReplaceTokens>(
"tokens" to mapOf(
"version" to appVersion,
"architecture" to if (arch.isArm64) "arm64" else "x64",
"projectDir" to project.projectDir.absolutePath,
)
)
}
}
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
@Suppress("CascadeIf")
tasks.register<Copy>("copy-dependencies") { tasks.register<Copy>("copy-dependencies") {
val dir = layout.buildDirectory.dir("libs") val dir = layout.buildDirectory.dir("libs")
from(configurations.runtimeClasspath).into(dir) from(configurations.runtimeClasspath).into(dir)
val jna = libs.jna.asProvider().get() val jna = libs.jna.asProvider().get()
val pty4j = libs.pty4j.get() val pty4j = libs.pty4j.get()
val flatlaf = libs.flatlaf.get() val flatlaf = libs.flatlaf.get()
val jSerialComm = libs.jSerialComm.get()
val restart4j = libs.restart4j.get() val restart4j = libs.restart4j.get()
val sqlite = libs.sqlite.get() val sqlite = libs.sqlite.get()
val archName = if (arch.isArm) "aarch64" else "x86_64"
val dylib = dir.get().dir("dylib").asFile
// 对 JNA 和 PTY4J 的本地库提取 doLast {
// 提取出来是为了单独签名,不然无法通过公证 for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if (os.isMacOsX) { if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
doLast { val targetDir = File(dylib, jna.name)
val archName = if (arch.isArm) "aarch64" else "x86_64" FileUtils.forceMkdir(targetDir)
val dylib = dir.get().dir("dylib").asFile if (os.isWindows) {
for (file in dir.get().asFile.listFiles() ?: emptyArray()) { // @formatter:off
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) { exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/win32-${arch.name}/*", "-d", targetDir.absolutePath) }
val targetDir = File(dylib, jna.name) // @formatter:on
FileUtils.forceMkdir(targetDir) } else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/linux-${arch.name}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @formatter:off // @formatter:off
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) } exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
// @formatter:on // @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/win32-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) { exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin") } else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
FileUtils.forceMkdir(targetDir) val targetDir = FileUtils.getFile(dylib, pty4j.name, if (os.isWindows) "win32" else "linux")
FileUtils.forceMkdir(targetDir)
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*linux/${myArchName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
// @formatter:on // @formatter:on
// 删除所有二进制类库 }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") } exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) { } else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName) val targetDir = FileUtils.getFile(dylib, restart4j.name)
FileUtils.forceMkdir(targetDir) FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "OSX/${archName}/*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "win32/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on // @formatter:on
// 删除所有二进制类库 } else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") } // @formatter:off
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "linux/${archName}/*", "-d", targetDir.absolutePath) }
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") } // @formatter:on
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") } } else if (os.isMacOsX) {
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, restart4j.name)
FileUtils.forceMkdir(targetDir)
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on // @formatter:on
// 删除所有二进制类库 }
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") } // 设置可执行权限
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") } for (e in FileUtils.listFiles(
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") } targetDir,
// 设置可执行权限 FileFilterUtils.trueFileFilter(),
for (e in FileUtils.listFiles( FileFilterUtils.falseFileFilter()
targetDir, )) e.setExecutable(true)
FileFilterUtils.trueFileFilter(), exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
FileFilterUtils.falseFileFilter() exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
)) { exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
e.setExecutable(true) } else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
} val targetDir = FileUtils.getFile(dylib, sqlite.name)
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) { FileUtils.forceMkdir(targetDir)
val targetDir = FileUtils.getFile(dylib, sqlite.name) if (os.isWindows) {
FileUtils.forceMkdir(targetDir) // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Windows/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Linux/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on // @formatter:on
// 删除所有二进制类库 }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") } exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") }
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) { } else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, flatlaf.name) val targetDir = FileUtils.getFile(dylib, flatlaf.name)
FileUtils.forceMkdir(targetDir) FileUtils.forceMkdir(targetDir)
val isArm = arch.isArm val isArm = arch.isArm
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*windows*${if (isArm) "arm64" else "x86_64"}*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*linux*${if (isArm) "arm64" else "x86_64"}*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) }
// @formatter:on // @formatter:on
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
} }
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
} }
}
// 对二进制签名 // 对二进制签名
if (os.isMacOsX) {
Files.walk(dylib.toPath()).use { paths -> Files.walk(dylib.toPath()).use { paths ->
for (path in paths) { for (path in paths) {
if (Files.isRegularFile(path)) { if (Files.isRegularFile(path)) {
@@ -273,116 +319,8 @@ tasks.register<Copy>("copy-dependencies") {
} }
} }
} }
} else if (os.isLinux || os.isWindows) { // 缩减安装包
doLast {
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
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/freebsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-aarch64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
}
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*darwin*") }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*freebsd*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*linux*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86-64*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win*") }
}
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
}
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "win32/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "win32/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "linux/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
}
}
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux-*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/FreeBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Mac/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/armv7/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/arm*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/ppc64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/riscv64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/aarch64/*") }
}
}
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*macos*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*linux*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86.dll") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*windows*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*arm*") }
}
}
}
}
}
} }
} }
tasks.register<Exec>("jlink") { tasks.register<Exec>("jlink") {
@@ -568,6 +506,27 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg") val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
val configText = cfg.readText() val configText = cfg.readText()
// appx
if (isAppx) {
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=appx").toString())
val appxManifest = FileUtils.getFile(dir, projectName, "AppxManifest.xml")
layout.buildDirectory.file("resources/main/AppxManifest.xml").get().asFile
.renameTo(appxManifest)
val icons = setOf("termora.png", "termora_44x44.png", "termora_150x150.png")
for (file in projectDir.resolve("src/main/resources/icons/").listFiles()) {
if (icons.contains(file.name)) {
val p = appxManifest.parentFile.resolve("icons/${file.name}")
FileUtils.forceMkdirParent(p)
file.copyTo(p, true)
}
}
exec {
commandLine(makeAppx, "pack", "/d", projectName, "/p", "${finalFilenameWithoutExtension}.msix")
workingDir = dir
}
return
}
// zip // zip
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString()) cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString())
exec { exec {

View File

@@ -6,6 +6,7 @@ enum class AppLayout {
*/ */
Zip, Zip,
Exe, Exe,
Appx,
/** /**
* macOS * macOS

View File

@@ -138,6 +138,8 @@ object Application {
return AppLayout.Exe return AppLayout.Exe
} else if ("zip" == layout) { } else if ("zip" == layout) {
return AppLayout.Zip return AppLayout.Zip
} else if ("appx" == layout) {
return AppLayout.Appx
} }
} }

View File

@@ -16,14 +16,8 @@ class ApplicationInitializr {
fun run() { fun run() {
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹 // 依赖二进制依赖会单独在一个文件夹
if (SystemUtils.IS_OS_MAC_OSX) { setupNativeLibraries()
setupNativeLibraries()
}
if (SystemUtils.IS_OS_MAC_OSX) {
System.setProperty("apple.awt.application.name", Application.getName())
}
// 设置 tinylog // 设置 tinylog
setupTinylog() setupTinylog()
@@ -31,6 +25,11 @@ class ApplicationInitializr {
// 检查是否单例 // 检查是否单例
checkSingleton() checkSingleton()
if (SystemUtils.IS_OS_MAC_OSX) {
System.setProperty("apple.awt.application.name", Application.getName())
}
// 启动 // 启动
val runtime = measureTimeMillis { ApplicationRunner().run() } val runtime = measureTimeMillis { ApplicationRunner().run() }
val log = LoggerFactory.getLogger(javaClass) val log = LoggerFactory.getLogger(javaClass)
@@ -42,23 +41,29 @@ class ApplicationInitializr {
private fun setupNativeLibraries() { private fun setupNativeLibraries() {
if (!SystemUtils.IS_OS_MAC_OSX) {
return
}
val appPath = Application.getAppPath() val appPath = Application.getAppPath()
if (StringUtils.isBlank(appPath)) { if (StringUtils.isBlank(appPath)) {
return return
} }
val contents = File(appPath).parentFile?.parentFile ?: return var contents = File(appPath)
if (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) {
contents = contents.parentFile?.parentFile ?: return
if (SystemUtils.IS_OS_LINUX) {
contents = File(contents, "lib")
}
} else if (SystemUtils.IS_OS_WINDOWS) {
contents = contents.parentFile ?: return
}
val dylib = FileUtils.getFile(contents, "app", "dylib") val dylib = FileUtils.getFile(contents, "app", "dylib")
if (!dylib.exists()) { if (dylib.exists().not()) {
return return
} }
val jna = FileUtils.getFile(dylib, "jna") val jna = FileUtils.getFile(dylib, "jna")
if (jna.exists()) { if (jna.exists()) {
System.setProperty("jna.nounpack", "true")
System.setProperty("jna.boot.library.path", jna.absolutePath) System.setProperty("jna.boot.library.path", jna.absolutePath)
} }
@@ -72,7 +77,10 @@ class ApplicationInitializr {
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath) System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
} }
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter") val restart4j = FileUtils.getFile(
dylib, "restart4j",
if (SystemUtils.IS_OS_WINDOWS) "restarter.exe" else "restarter"
)
if (restart4j.exists()) { if (restart4j.exists()) {
System.setProperty("restarter.path", restart4j.absolutePath) System.setProperty("restarter.path", restart4j.absolutePath)
} }

View File

@@ -7,7 +7,6 @@ import com.sun.jna.platform.win32.WinDef.*
import com.sun.jna.platform.win32.WinError import com.sun.jna.platform.win32.WinError
import com.sun.jna.platform.win32.WinUser.* import com.sun.jna.platform.win32.WinUser.*
import com.sun.jna.platform.win32.Wtsapi32 import com.sun.jna.platform.win32.Wtsapi32
import org.slf4j.LoggerFactory
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.channels.FileLock import java.nio.channels.FileLock
import java.nio.file.Paths import java.nio.file.Paths
@@ -95,7 +94,6 @@ class ApplicationSingleton private constructor() : Disposable {
private class Win32HelperWindow private constructor() : Runnable { private class Win32HelperWindow private constructor() : Runnable {
companion object { companion object {
private val log = LoggerFactory.getLogger(Win32HelperWindow::class.java)
private val WindowClass = "${Application.getName()}HelperWindowClass" private val WindowClass = "${Application.getName()}HelperWindowClass"
private val WindowName = private val WindowName =
"${Application.getName()} hidden helper window, used only to catch the windows events" "${Application.getName()} hidden helper window, used only to catch the windows events"
@@ -166,24 +164,15 @@ class ApplicationSingleton private constructor() : Disposable {
override fun callback(hwnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT { override fun callback(hwnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT {
when (uMsg) { when (uMsg) {
WM_CREATE -> { WM_CREATE -> {
if (log.isDebugEnabled) {
log.debug("win32 helper window created")
}
return LRESULT() return LRESULT()
} }
TICK -> { TICK -> {
if (log.isDebugEnabled) {
log.debug("win32 helper window tick")
}
onTick() onTick()
return LRESULT() return LRESULT()
} }
WM_DESTROY -> { WM_DESTROY -> {
if (log.isDebugEnabled) {
log.debug("win32 helper window destroyed")
}
User32.INSTANCE.PostQuitMessage(0) User32.INSTANCE.PostQuitMessage(0)
return LRESULT() return LRESULT()
} }

View File

@@ -6,9 +6,11 @@ import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.Component import java.awt.Component
import java.awt.Window
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import java.nio.file.Paths import java.nio.file.Paths
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JDialog
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@@ -121,11 +123,22 @@ class TermoraRestarter {
val instance = TermoraFrameManager.getInstance() val instance = TermoraFrameManager.getInstance()
for (window in instance.getWindows()) { for (window in instance.getWindows()) {
disposeChildren(window)
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSED)) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSED))
} }
Disposer.dispose(instance) Disposer.dispose(instance)
} }
private fun disposeChildren(window: Window) {
for (win in Window.getWindows()) {
if (win is JDialog) {
if (win.owner == window) {
disposeChildren(win)
}
win.dispatchEvent(WindowEvent(win, WindowEvent.WINDOW_CLOSED))
}
}
}
private fun checkIsSupported(): Boolean { private fun checkIsSupported(): Boolean {
val appPath = Application.getAppPath() val appPath = Application.getAppPath()

View File

@@ -1,10 +1,7 @@
package app.termora.plugin.internal.update package app.termora.plugin.internal.update
import app.termora.Application import app.termora.*
import app.termora.Application.httpClient import app.termora.Application.httpClient
import app.termora.ApplicationScope
import app.termora.Disposable
import app.termora.UpdaterManager
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.Request import okhttp3.Request
@@ -32,6 +29,7 @@ internal class Updater private constructor() : Disposable {
private val updaterManager get() = UpdaterManager.getInstance() private val updaterManager get() = UpdaterManager.getInstance()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var isRemindMeNextTime = false private var isRemindMeNextTime = false
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx
/** /**
* 安装包位置 * 安装包位置
@@ -39,6 +37,14 @@ internal class Updater private constructor() : Disposable {
private var pkg: LatestPkg? = null private var pkg: LatestPkg? = null
fun scheduleUpdate() { fun scheduleUpdate() {
if (disabledUpdater) {
if (coroutineScope.isActive) {
coroutineScope.cancel()
}
return
}
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
// 启动 3 分钟后才是检查 // 启动 3 分钟后才是检查
if (Application.isUnknownVersion().not()) { if (Application.isUnknownVersion().not()) {
@@ -66,6 +72,9 @@ internal class Updater private constructor() : Disposable {
private fun checkUpdate() { private fun checkUpdate() {
// Windows 应用商店
if (disabledUpdater) return
val latestVersion = updaterManager.fetchLatestVersion() val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) { if (latestVersion.isSelf) {
return return

View File

@@ -0,0 +1,40 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="TermoraDev.Termora"
Publisher="CN=C804E131-4368-4BF7-9E7F-95C681AD0AAC"
Version="@version@.0"
ProcessorArchitecture="@architecture@"/>
<Properties>
<DisplayName>Termora</DisplayName>
<PublisherDisplayName>TermoraDev</PublisherDisplayName>
<Logo>icons\termora.png</Logo>
</Properties>
<Resources>
<Resource Language="en-US"/>
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0"/>
</Dependencies>
<Applications>
<Application Id="Termora" Executable="Termora.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="Termora"
Description="Termora is a cross-platform terminal emulator and SSH client, available on Windows, macOS, and Linux"
BackgroundColor="transparent"
Square150x150Logo="icons\termora_150x150.png"
Square44x44Logo="icons\termora_44x44.png"/>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
</Package>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB