From c8d70e2a5a0a52595f0fc0fa3e530dec9631154c Mon Sep 17 00:00:00 2001 From: hstyi Date: Wed, 16 Jul 2025 14:24:59 +0800 Subject: [PATCH] chore: Windows MSIX --- .github/workflows/linux.yml | 18 +- .github/workflows/osx.yml | 12 +- .github/workflows/windows.yml | 49 ++- build.gradle.kts | 303 ++++++++---------- src/main/kotlin/app/termora/AppLayout.kt | 1 + src/main/kotlin/app/termora/Application.kt | 2 + .../app/termora/ApplicationInitializr.kt | 38 ++- .../app/termora/ApplicationSingleton.kt | 11 - .../kotlin/app/termora/TermoraRestarter.kt | 13 + .../termora/plugin/internal/update/Updater.kt | 17 +- src/main/resources/AppxManifest.xml | 40 +++ src/main/resources/icons/termora_150x150.png | Bin 0 -> 7156 bytes src/main/resources/icons/termora_44x44.png | Bin 0 -> 2293 bytes 13 files changed, 292 insertions(+), 212 deletions(-) create mode 100644 src/main/resources/AppxManifest.xml create mode 100644 src/main/resources/icons/termora_150x150.png create mode 100644 src/main/resources/icons/termora_44x44.png diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2c79068..e0d3e61 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - uses: actions/cache@v4 with: @@ -59,11 +59,23 @@ jobs: shell: bash run: sudo chmod -R 777 ~/.gradle - - name: Upload artifact + - name: Upload targz artifact uses: actions/upload-artifact@v4 with: - name: termora-linux-${{ runner.arch }} + name: termora-linux-targz-${{ runner.arch }} path: | build/distributions/*.tar.gz + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: termora-linux-AppImage-${{ runner.arch }} + path: | build/distributions/*.AppImage + + - name: Upload deb artifact + uses: actions/upload-artifact@v4 + with: + name: termora-linux-deb-${{ runner.arch }} + path: | build/distributions/*.deb diff --git a/.github/workflows/osx.yml b/.github/workflows/osx.yml index 8a4ea24..87eb60e 100644 --- a/.github/workflows/osx.yml +++ b/.github/workflows/osx.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - name: Install the Apple certificate if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''" @@ -93,10 +93,16 @@ jobs: shell: bash run: ./gradlew :jpackage && ./gradlew :dist - - name: Upload artifact + - name: Upload zip artifact uses: actions/upload-artifact@v4 with: - name: termora-osx-${{ runner.arch }} + name: termora-osx-zip-${{ runner.arch }} path: | build/distributions/*.zip + + - name: Upload dmg artifact + uses: actions/upload-artifact@v4 + with: + name: termora-osx-dmg-${{ runner.arch }} + path: | build/distributions/*.dmg diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 1981365..ec5df32 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -11,11 +11,14 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ windows-11-arm, windows-latest ] + os: [ windows-11-arm, windows-2022 ] steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 + + - name: Setup MSbuild + uses: microsoft/setup-msbuild@v2 - name: Set architecture id: set-arch @@ -26,6 +29,24 @@ jobs: 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 run: | $system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32" @@ -63,13 +84,33 @@ jobs: - name: Package run: .\gradlew :jpackage && .\gradlew :dist + - name: MSIX + env: + TERMORA_TYPE: appx + run: | + .\gradlew --stop + .\gradlew :dist + - name: Stop Gradle run: .\gradlew.bat --stop - - name: Upload artifact + - name: Upload zip artifact uses: actions/upload-artifact@v4 with: - name: termora-windows-${{ runner.arch }} + name: termora-windows-zip-${{ runner.arch }} path: | build/distributions/*.zip + + - name: Upload exe artifact + uses: actions/upload-artifact@v4 + with: + name: termora-windows-exe-${{ runner.arch }} + path: | build/distributions/*.exe + + - name: Upload msix artifact + uses: actions/upload-artifact@v4 + with: + name: termora-windows-msix-${{ runner.arch }} + path: | + build/distributions/*.msix diff --git a/build.gradle.kts b/build.gradle.kts index 1b88afe..1ab7795 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import org.apache.tools.ant.filters.ReplaceTokens import org.gradle.internal.jvm.Jvm import org.gradle.kotlin.dsl.support.uppercaseFirstChar import org.gradle.nativeplatform.platform.internal.ArchitectureInternal @@ -28,7 +29,9 @@ version = rootProject.projectDir.resolve("VERSION").readText().trim() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() 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 isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx" // macOS 签名信息 val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY @@ -169,102 +172,145 @@ publishing { } } +tasks.processResources { + filesMatching("**/AppxManifest.xml") { + filter( + "tokens" to mapOf( + "version" to appVersion, + "architecture" to if (arch.isArm64) "arm64" else "x64", + "projectDir" to project.projectDir.absolutePath, + ) + ) + } +} + + tasks.test { useJUnitPlatform() } +@Suppress("CascadeIf") tasks.register("copy-dependencies") { val dir = layout.buildDirectory.dir("libs") from(configurations.runtimeClasspath).into(dir) val jna = libs.jna.asProvider().get() val pty4j = libs.pty4j.get() val flatlaf = libs.flatlaf.get() - val jSerialComm = libs.jSerialComm.get() val restart4j = libs.restart4j.get() val sqlite = libs.sqlite.get() + val archName = if (arch.isArm) "aarch64" else "x86_64" + val dylib = dir.get().dir("dylib").asFile - // 对 JNA 和 PTY4J 的本地库提取 - // 提取出来是为了单独签名,不然无法通过公证 - if (os.isMacOsX) { - doLast { - val archName = if (arch.isArm) "aarch64" else "x86_64" - val dylib = dir.get().dir("dylib").asFile - for (file in dir.get().asFile.listFiles() ?: emptyArray()) { - if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) { - val targetDir = File(dylib, jna.name) - FileUtils.forceMkdir(targetDir) + doLast { + for (file in dir.get().asFile.listFiles() ?: emptyArray()) { + if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) { + val targetDir = File(dylib, jna.name) + FileUtils.forceMkdir(targetDir) + if (os.isWindows) { + // @formatter:off + exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/win32-${arch.name}/*", "-d", targetDir.absolutePath) } + // @formatter:on + } 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 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) + } + + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") } + 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-*") } + } else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) { + 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 exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) } // @formatter:on - // 删除所有二进制类库 - exec { commandLine("zip", "-d", file.absolutePath, "resources/*") } - } else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) { - val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName) - FileUtils.forceMkdir(targetDir) + } + exec { commandLine("zip", "-d", file.absolutePath, "resources/*") } + } else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) { + val targetDir = FileUtils.getFile(dylib, restart4j.name) + FileUtils.forceMkdir(targetDir) + if (os.isWindows) { // @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 - // 删除所有二进制类库 - exec { commandLine("zip", "-d", file.absolutePath, "Android/*") } - exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") } - exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") } - exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") } - 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) + } else if (os.isLinux) { + // @formatter:off + exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "linux/${archName}/*", "-d", targetDir.absolutePath) } + // @formatter:on + } else if (os.isMacOsX) { // @formatter:off exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) } // @formatter:on - // 删除所有二进制类库 - exec { commandLine("zip", "-d", file.absolutePath, "win32/*") } - exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") } - exec { commandLine("zip", "-d", file.absolutePath, "linux/*") } - // 设置可执行权限 - for (e in FileUtils.listFiles( - targetDir, - FileFilterUtils.trueFileFilter(), - FileFilterUtils.falseFileFilter() - )) { - e.setExecutable(true) - } - } else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) { - val targetDir = FileUtils.getFile(dylib, sqlite.name) - FileUtils.forceMkdir(targetDir) + } + // 设置可执行权限 + for (e in FileUtils.listFiles( + targetDir, + FileFilterUtils.trueFileFilter(), + FileFilterUtils.falseFileFilter() + )) e.setExecutable(true) + exec { commandLine("zip", "-d", file.absolutePath, "win32/*") } + exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") } + exec { commandLine("zip", "-d", file.absolutePath, "linux/*") } + } else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) { + val targetDir = FileUtils.getFile(dylib, sqlite.name) + FileUtils.forceMkdir(targetDir) + if (os.isWindows) { + // @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 exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) } // @formatter:on - // 删除所有二进制类库 - exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") } - } else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) { - val targetDir = FileUtils.getFile(dylib, flatlaf.name) - FileUtils.forceMkdir(targetDir) - val isArm = arch.isArm + } + exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") } + } else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) { + val targetDir = FileUtils.getFile(dylib, flatlaf.name) + FileUtils.forceMkdir(targetDir) + 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 exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) } // @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 -> for (path in paths) { if (Files.isRegularFile(path)) { @@ -273,116 +319,8 @@ tasks.register("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("jlink") { @@ -568,6 +506,27 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg") 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 cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString()) exec { diff --git a/src/main/kotlin/app/termora/AppLayout.kt b/src/main/kotlin/app/termora/AppLayout.kt index 04920e7..47414b9 100644 --- a/src/main/kotlin/app/termora/AppLayout.kt +++ b/src/main/kotlin/app/termora/AppLayout.kt @@ -6,6 +6,7 @@ enum class AppLayout { */ Zip, Exe, + Appx, /** * macOS diff --git a/src/main/kotlin/app/termora/Application.kt b/src/main/kotlin/app/termora/Application.kt index 3dac1d0..61c8480 100644 --- a/src/main/kotlin/app/termora/Application.kt +++ b/src/main/kotlin/app/termora/Application.kt @@ -138,6 +138,8 @@ object Application { return AppLayout.Exe } else if ("zip" == layout) { return AppLayout.Zip + } else if ("appx" == layout) { + return AppLayout.Appx } } diff --git a/src/main/kotlin/app/termora/ApplicationInitializr.kt b/src/main/kotlin/app/termora/ApplicationInitializr.kt index 2f9b928..b0dbcaa 100644 --- a/src/main/kotlin/app/termora/ApplicationInitializr.kt +++ b/src/main/kotlin/app/termora/ApplicationInitializr.kt @@ -16,14 +16,8 @@ class ApplicationInitializr { fun run() { - // 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹 - if (SystemUtils.IS_OS_MAC_OSX) { - setupNativeLibraries() - } - - if (SystemUtils.IS_OS_MAC_OSX) { - System.setProperty("apple.awt.application.name", Application.getName()) - } + // 依赖二进制依赖会单独在一个文件夹 + setupNativeLibraries() // 设置 tinylog setupTinylog() @@ -31,6 +25,11 @@ class ApplicationInitializr { // 检查是否单例 checkSingleton() + + if (SystemUtils.IS_OS_MAC_OSX) { + System.setProperty("apple.awt.application.name", Application.getName()) + } + // 启动 val runtime = measureTimeMillis { ApplicationRunner().run() } val log = LoggerFactory.getLogger(javaClass) @@ -42,23 +41,29 @@ class ApplicationInitializr { 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 + 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") - if (!dylib.exists()) { + if (dylib.exists().not()) { return } val jna = FileUtils.getFile(dylib, "jna") if (jna.exists()) { + System.setProperty("jna.nounpack", "true") System.setProperty("jna.boot.library.path", jna.absolutePath) } @@ -72,7 +77,10 @@ class ApplicationInitializr { 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()) { System.setProperty("restarter.path", restart4j.absolutePath) } diff --git a/src/main/kotlin/app/termora/ApplicationSingleton.kt b/src/main/kotlin/app/termora/ApplicationSingleton.kt index 5ee68e5..24dab8e 100644 --- a/src/main/kotlin/app/termora/ApplicationSingleton.kt +++ b/src/main/kotlin/app/termora/ApplicationSingleton.kt @@ -7,7 +7,6 @@ import com.sun.jna.platform.win32.WinDef.* import com.sun.jna.platform.win32.WinError import com.sun.jna.platform.win32.WinUser.* import com.sun.jna.platform.win32.Wtsapi32 -import org.slf4j.LoggerFactory import java.nio.channels.FileChannel import java.nio.channels.FileLock import java.nio.file.Paths @@ -95,7 +94,6 @@ class ApplicationSingleton private constructor() : Disposable { private class Win32HelperWindow private constructor() : Runnable { companion object { - private val log = LoggerFactory.getLogger(Win32HelperWindow::class.java) private val WindowClass = "${Application.getName()}HelperWindowClass" private val WindowName = "${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 { when (uMsg) { WM_CREATE -> { - if (log.isDebugEnabled) { - log.debug("win32 helper window created") - } return LRESULT() } TICK -> { - if (log.isDebugEnabled) { - log.debug("win32 helper window tick") - } onTick() return LRESULT() } WM_DESTROY -> { - if (log.isDebugEnabled) { - log.debug("win32 helper window destroyed") - } User32.INSTANCE.PostQuitMessage(0) return LRESULT() } diff --git a/src/main/kotlin/app/termora/TermoraRestarter.kt b/src/main/kotlin/app/termora/TermoraRestarter.kt index d398077..17279d3 100644 --- a/src/main/kotlin/app/termora/TermoraRestarter.kt +++ b/src/main/kotlin/app/termora/TermoraRestarter.kt @@ -6,9 +6,11 @@ import org.apache.commons.io.FileUtils import org.apache.commons.lang3.StringUtils import org.slf4j.LoggerFactory import java.awt.Component +import java.awt.Window import java.awt.event.WindowEvent import java.nio.file.Paths import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.JDialog import javax.swing.JOptionPane import javax.swing.SwingUtilities import kotlin.jvm.optionals.getOrNull @@ -121,11 +123,22 @@ class TermoraRestarter { val instance = TermoraFrameManager.getInstance() for (window in instance.getWindows()) { + disposeChildren(window) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSED)) } 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 { val appPath = Application.getAppPath() diff --git a/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt b/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt index 3bcaccc..fed4b2b 100644 --- a/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt +++ b/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt @@ -1,10 +1,7 @@ package app.termora.plugin.internal.update -import app.termora.Application +import app.termora.* import app.termora.Application.httpClient -import app.termora.ApplicationScope -import app.termora.Disposable -import app.termora.UpdaterManager import com.formdev.flatlaf.util.SystemInfo import kotlinx.coroutines.* import okhttp3.Request @@ -32,6 +29,7 @@ internal class Updater private constructor() : Disposable { private val updaterManager get() = UpdaterManager.getInstance() private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) 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 fun scheduleUpdate() { + + if (disabledUpdater) { + if (coroutineScope.isActive) { + coroutineScope.cancel() + } + return + } + coroutineScope.launch(Dispatchers.IO) { // 启动 3 分钟后才是检查 if (Application.isUnknownVersion().not()) { @@ -66,6 +72,9 @@ internal class Updater private constructor() : Disposable { private fun checkUpdate() { + // Windows 应用商店 + if (disabledUpdater) return + val latestVersion = updaterManager.fetchLatestVersion() if (latestVersion.isSelf) { return diff --git a/src/main/resources/AppxManifest.xml b/src/main/resources/AppxManifest.xml new file mode 100644 index 0000000..f5ae9aa --- /dev/null +++ b/src/main/resources/AppxManifest.xml @@ -0,0 +1,40 @@ + + + + + + Termora + TermoraDev + icons\termora.png + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/termora_150x150.png b/src/main/resources/icons/termora_150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..57b41e11df88855ac9dc61b82c78214e3cb31074 GIT binary patch literal 7156 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 literal 0 HcmV?d00001