Linux support (#381)

* Add desktop release CI and support for Arch Linux packaging

* Make Gradle wrapper executable in desktop-release workflow

* Make Gradle wrapper executable in desktop-release workflow

* Make Gradle wrapper executable in desktop-release workflow

* Configure Gradle and update Java environment in desktop-release workflow

* Update Java setup and AUR packaging in desktop release workflow

* Update Java setup and AUR packaging in desktop release workflow

* Add MSIX packaging support for Windows desktop distribution

* Update AUR packaging metadata and validation

* Use spine toc attribute for NCX resolution

* crash fixes

* Implement automatic discovery and injection of EPUB font face siblings

* Enhance custom font support with family grouping and variable font handling

* Optimize metadata loading and improve TTS highlighting

* Add keyboard navigation support for EPUB reader

* Refine PDF spread page sizing to respect aspect ratios

* Implement responsive maximum height for reader popups and sheets

* Handle TTS generation failures by skipping problematic chunks

* Refactor PDF tile rendering logic and zoom indicator behavior

* Prefer block and offset locators over page index in native vertical flow

* Implement save and share actions for original book files

* Add Estonian language support

* Implement temporary viewing mode for external files

* Implement direct opening for temporary external files without library persistence

* fix failing tests

* Import SharedFileCapabilities in DesktopLibraryUi

* Improve native vertical reader progress, persistence, and image support

* Center target in viewport for native vertical reader and support animated scrolling
This commit is contained in:
Aryan 2026-06-14 13:43:49 +05:30 committed by GitHub
parent a13d6599d1
commit 625a4d5d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 6012 additions and 687 deletions

View file

@ -1,19 +1,31 @@
import org.gradle.api.GradleException
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.Compression
import org.gradle.api.tasks.bundling.Tar
import org.gradle.jvm.tasks.Jar
import org.gradle.process.ExecOperations
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.gradle.work.DisableCachingByDefault
import java.io.File
import java.security.MessageDigest
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
@ -21,6 +33,8 @@ import java.util.Properties
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import javax.imageio.ImageIO
import javax.inject.Inject
plugins {
alias(libs.plugins.kotlin.multiplatform)
@ -81,6 +95,244 @@ abstract class RenameDesktopMsiOutputTask : DefaultTask() {
}
}
@DisableCachingByDefault(because = "Generates an MSIX manifest from package metadata.")
abstract class GenerateDesktopMsixManifestTask : DefaultTask() {
@get:Input
abstract val identityName: Property<String>
@get:Input
abstract val publisher: Property<String>
@get:Input
abstract val publisherDisplayName: Property<String>
@get:Input
abstract val packageName: Property<String>
@get:Input
abstract val packageDescription: Property<String>
@get:Input
abstract val packageVersion: Property<String>
@get:Input
abstract val architecture: Property<String>
@get:Input
abstract val executablePath: Property<String>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun generate() {
fun xmlEscaped(value: String): String {
return value.replace("&", "&amp;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
val file = outputFile.get().asFile
file.parentFile.mkdirs()
file.writeText(
"""
<?xml version="1.0" encoding="utf-8"?>
<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="${xmlEscaped(identityName.get())}"
Publisher="${xmlEscaped(publisher.get())}"
Version="${xmlEscaped(packageVersion.get())}"
ProcessorArchitecture="${xmlEscaped(architecture.get())}" />
<Properties>
<DisplayName>${xmlEscaped(packageName.get())}</DisplayName>
<PublisherDisplayName>${xmlEscaped(publisherDisplayName.get())}</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Applications>
<Application Id="Episteme" Executable="${xmlEscaped(executablePath.get())}" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="${xmlEscaped(packageName.get())}"
Description="${xmlEscaped(packageDescription.get())}"
BackgroundColor="transparent"
Square44x44Logo="Assets\Square44x44Logo.png"
Square150x150Logo="Assets\Square150x150Logo.png" />
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
""".trimIndent() + "\n",
Charsets.UTF_8
)
}
}
@DisableCachingByDefault(because = "Generates fixed-size MSIX logo assets from the desktop icon.")
abstract class GenerateDesktopMsixAssetsTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val sourceIconFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun generate() {
val source = ImageIO.read(sourceIconFile.get().asFile)
?: throw GradleException("Could not read MSIX source icon ${sourceIconFile.get().asFile.absolutePath}.")
val output = outputDirectory.get().asFile
output.mkdirs()
writePng(source, output.resolve("Square44x44Logo.png"), 44)
writePng(source, output.resolve("Square150x150Logo.png"), 150)
writePng(source, output.resolve("StoreLogo.png"), 50)
}
private fun writePng(source: BufferedImage, target: File, size: Int) {
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
try {
graphics.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC
)
graphics.setRenderingHint(
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY
)
graphics.drawImage(source, 0, 0, size, size, null)
} finally {
graphics.dispose()
}
ImageIO.write(image, "png", target)
}
}
@DisableCachingByDefault(because = "Packages the staged MSIX app image with Windows SDK makeappx.")
abstract class PackageDesktopMsixTask @Inject constructor(
private val execOperations: ExecOperations
) : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val packageRootDirectory: DirectoryProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:Input
abstract val makeAppxPath: Property<String>
@get:Input
abstract val hostOsId: Property<String>
@get:Input
abstract val hostArchId: Property<String>
@TaskAction
fun packageMsix() {
if (hostOsId.get() != "windows" || hostArchId.get() != "x64") {
throw GradleException(
"MSIX packaging requires a Windows x64 packaging host. " +
"Current host: ${hostOsId.get()} ${hostArchId.get()}."
)
}
val makeAppx = File(makeAppxPath.get())
if (!makeAppx.isFile) {
throw GradleException(
"Windows SDK makeappx.exe was not found at ${makeAppx.absolutePath}. " +
"Install the Windows SDK MSIX packaging tools or set " +
"-PdesktopMakeAppxPath=<path-to-makeappx.exe>."
)
}
val output = outputFile.get().asFile
output.parentFile.mkdirs()
if (output.exists() && !output.delete()) {
throw GradleException("Could not replace existing MSIX at ${output.absolutePath}.")
}
execOperations.exec {
executable = makeAppx.absolutePath
args(
"pack",
"/d",
packageRootDirectory.get().asFile.absolutePath,
"/p",
output.absolutePath,
"/o"
)
}
}
}
@DisableCachingByDefault(because = "Signs the MSIX package with Windows SDK signtool.")
abstract class SignDesktopMsixTask @Inject constructor(
private val execOperations: ExecOperations
) : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val unsignedMsixFile: RegularFileProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val certificateFile: RegularFileProperty
@get:Input
abstract val signToolPath: Property<String>
@get:Input
abstract val certificatePassword: Property<String>
@get:Input
abstract val timestampUrl: Property<String>
@TaskAction
fun signMsix() {
val signTool = File(signToolPath.get())
if (!signTool.isFile) {
throw GradleException(
"Windows SDK signtool.exe was not found at ${signTool.absolutePath}. " +
"Install the Windows SDK or set -PdesktopSignToolPath=<path-to-signtool.exe>."
)
}
val signArgs = mutableListOf(
"sign",
"/fd",
"SHA256",
"/f",
certificateFile.get().asFile.absolutePath
)
val password = certificatePassword.get().trim()
if (password.isNotEmpty()) {
signArgs += listOf("/p", password)
}
val timestamp = timestampUrl.get().trim()
if (timestamp.isNotEmpty()) {
signArgs += listOf("/tr", timestamp, "/td", "SHA256")
}
signArgs += unsignedMsixFile.get().asFile.absolutePath
execOperations.exec {
executable = signTool.absolutePath
args(signArgs)
}
}
}
@DisableCachingByDefault(because = "Generates local desktop service config for native packages.")
abstract class GenerateDesktopCloudConfigTask : DefaultTask() {
@get:Input
@ -136,6 +388,275 @@ abstract class VerifyDesktopNativePackagingTask : DefaultTask() {
}
}
@DisableCachingByDefault(because = "Generates AUR package metadata from the local Linux distributable.")
abstract class PrepareDesktopAurPackageTask : DefaultTask() {
@get:Input
abstract val aurPackageName: Property<String>
@get:Input
abstract val providedPackageName: Property<String>
@get:Input
abstract val packageVersion: Property<String>
@get:Input
abstract val packageRelease: Property<String>
@get:Input
abstract val packageDescription: Property<String>
@get:Input
abstract val appDisplayName: Property<String>
@get:Input
abstract val installDirectoryName: Property<String>
@get:Input
abstract val launcherName: Property<String>
@get:Input
abstract val executableName: Property<String>
@get:Input
abstract val sourceUrl: Property<String>
@get:Input
abstract val projectUrl: Property<String>
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val linuxTarFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun prepare() {
val output = outputDirectory.get().asFile
val sourceTar = linuxTarFile.get().asFile
if (!sourceTar.isFile) {
throw GradleException("Missing Linux tarball for AUR packaging: ${sourceTar.absolutePath}")
}
output.deleteRecursively()
output.mkdirs()
val stagedTar = output.resolve(sourceTar.name)
sourceTar.copyTo(stagedTar, overwrite = true)
val sha256 = stagedTar.sha256()
val configuredSourceUrl = sourceUrl.get().trim()
val sourceEntry = if (configuredSourceUrl.isBlank()) {
stagedTar.name
} else {
"${stagedTar.name}::$configuredSourceUrl"
}
output.resolve("PKGBUILD").writeText(
aurPkgbuild(
pkgname = aurPackageName.get(),
providedPackage = providedPackageName.get(),
pkgver = packageVersion.get(),
pkgrel = packageRelease.get(),
pkgdesc = packageDescription.get(),
appName = appDisplayName.get(),
installDir = installDirectoryName.get(),
launcher = launcherName.get(),
executable = executableName.get(),
source = sourceEntry,
sha256 = sha256,
projectUrl = projectUrl.get()
)
)
output.resolve(".SRCINFO").writeText(
aurSrcInfo(
pkgname = aurPackageName.get(),
providedPackage = providedPackageName.get(),
pkgver = packageVersion.get(),
pkgrel = packageRelease.get(),
pkgdesc = packageDescription.get(),
source = sourceEntry,
sha256 = sha256,
projectUrl = projectUrl.get()
)
)
}
private fun File.sha256(): String {
val digest = MessageDigest.getInstance("SHA-256")
inputStream().use { input ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
val read = input.read(buffer)
if (read < 0) break
digest.update(buffer, 0, read)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
private fun shellSingleQuoted(value: String): String {
return "'" + value.replace("'", "'\"'\"'") + "'"
}
private fun archRuntimeDependencies(): List<String> {
return listOf(
"alsa-lib",
"atk",
"cairo",
"dbus",
"expat",
"fontconfig",
"freetype2",
"gcc-libs",
"gdk-pixbuf2",
"glib2",
"glibc",
"gtk3",
"libcups",
"libarchive",
"libsecret",
"libx11",
"libxcomposite",
"libxdamage",
"libxext",
"libxi",
"libxrandr",
"libxrender",
"libxtst",
"nss",
"pango",
"zlib"
)
}
private fun aurPkgbuild(
pkgname: String,
providedPackage: String,
pkgver: String,
pkgrel: String,
pkgdesc: String,
appName: String,
installDir: String,
launcher: String,
executable: String,
source: String,
sha256: String,
projectUrl: String
): String {
val desktopFile = "$providedPackage.desktop"
val iconName = providedPackage
val depends = archRuntimeDependencies()
val mimeTypes = archDesktopMimeTypes()
return """
pkgname=${shellSingleQuoted(pkgname)}
pkgver=${shellSingleQuoted(pkgver)}
pkgrel=${shellSingleQuoted(pkgrel)}
pkgdesc=${shellSingleQuoted(pkgdesc)}
arch=('x86_64')
url=${shellSingleQuoted(projectUrl)}
license=('AGPL-3.0-only')
depends=(${depends.joinToString(" ") { shellSingleQuoted(it) }})
provides=(${shellSingleQuoted(providedPackage)})
conflicts=(${shellSingleQuoted(providedPackage)})
source=(${shellSingleQuoted(source)})
sha256sums=(${shellSingleQuoted(sha256)})
options=('!debug')
package() {
install -dm755 "${'$'}pkgdir/opt/$installDir"
cp -a "$installDir/." "${'$'}pkgdir/opt/$installDir/"
chmod 755 "${'$'}pkgdir/opt/$installDir/bin/$executable"
install -dm755 "${'$'}pkgdir/usr/bin"
ln -sf "/opt/$installDir/bin/$executable" "${'$'}pkgdir/usr/bin/$launcher"
install -Dm644 "${'$'}pkgdir/opt/$installDir/share/licenses/LICENSE" "${'$'}pkgdir/usr/share/licenses/${'$'}pkgname/LICENSE"
local icon_path
icon_path="${'$'}(find "${'$'}pkgdir/opt/$installDir" -name 'episteme_icon.png' -print -quit)"
if [[ -n "${'$'}icon_path" ]]; then
install -Dm644 "${'$'}icon_path" "${'$'}pkgdir/usr/share/icons/hicolor/512x512/apps/$iconName.png"
install -Dm644 "${'$'}icon_path" "${'$'}pkgdir/usr/share/pixmaps/$iconName.png"
fi
install -Dm644 /dev/stdin "${'$'}pkgdir/usr/share/applications/$desktopFile" <<'EOF'
[Desktop Entry]
Type=Application
Name=$appName
Comment=$pkgdesc
Exec=$launcher %F
Icon=$iconName
Terminal=false
Categories=Office;Viewer;
MimeType=${mimeTypes.joinToString(";")};
EOF
}
""".trimIndent() + "\n"
}
private fun aurSrcInfo(
pkgname: String,
providedPackage: String,
pkgver: String,
pkgrel: String,
pkgdesc: String,
source: String,
sha256: String,
projectUrl: String
): String {
val depends = archRuntimeDependencies()
return """
pkgbase = $pkgname
pkgdesc = $pkgdesc
pkgver = $pkgver
pkgrel = $pkgrel
url = $projectUrl
arch = x86_64
license = AGPL-3.0-only
${depends.joinToString("\n") { "\tdepends = $it" }}
provides = $providedPackage
conflicts = $providedPackage
source = $source
sha256sums = $sha256
pkgname = $pkgname
""".trimIndent() + "\n"
}
private fun archDesktopMimeTypes(): List<String> {
return listOf(
"application/pdf",
"application/epub+zip",
"application/x-mobipocket-ebook",
"application/vnd.amazon.ebook",
"application/vnd.amazon.mobi8-ebook",
"text/markdown",
"text/x-markdown",
"text/plain",
"text/html",
"application/xhtml+xml",
"application/x-fictionbook+xml",
"application/x-zip-compressed-fb2",
"application/zip",
"application/vnd.comicbook+zip",
"application/x-cbz",
"application/vnd.comicbook-rar",
"application/x-cbr",
"application/x-rar-compressed",
"application/x-cb7",
"application/x-7z-compressed",
"application/vnd.comicbook+tar",
"application/x-cbt",
"application/x-tar",
"application/tar",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.oasis.opendocument.text",
"application/x-vnd.oasis.opendocument.text-flat-xml"
)
}
}
@DisableCachingByDefault(because = "Strips stale jar signatures in-place after ProGuard rewrites signed dependencies.")
abstract class StripInvalidJarSignaturesTask : DefaultTask() {
@get:Input
@ -546,6 +1067,79 @@ fun normalizeDesktopPackageFormats(
return formats
}
fun normalizeDesktopMsixVersion(rawVersion: String): String {
val parts = rawVersion.trim().split('.')
if (parts.size !in 3..4 || parts.any { it.isBlank() || it.all(Char::isDigit).not() }) {
throw GradleException(
"desktopMsixVersion must be a numeric Windows package version with three or four parts, " +
"for example 1.0.1 or 1.0.1.0."
)
}
val normalized = if (parts.size == 3) parts + "0" else parts
normalized.forEach { part ->
val value = part.toIntOrNull()
if (value == null || value !in 0..65535) {
throw GradleException("desktopMsixVersion part '$part' is outside the MSIX range 0..65535.")
}
}
return normalized.joinToString(".")
}
fun normalizeDesktopMsixIdentityName(rawName: String): String {
val normalized = rawName.trim()
if (!Regex("[A-Za-z0-9][A-Za-z0-9.-]{2,49}").matches(normalized)) {
throw GradleException(
"desktopMsixIdentityName must be 3-50 characters using letters, numbers, dots, or hyphens."
)
}
return normalized
}
fun desktopMsixArchitecture(osArch: String = System.getProperty("os.arch")): String {
return when (desktopArchId(osArch)) {
"x64" -> "x64"
"arm64" -> "arm64"
"x86" -> "x86"
else -> "neutral"
}
}
fun latestExistingFile(candidates: List<File>): File? {
return candidates.filter { it.isFile }.maxByOrNull { it.absolutePath }
}
fun windowsSdkToolCandidates(toolName: String): List<File> {
val roots = listOfNotNull(
System.getenv("WindowsSdkDir")?.let(::File),
File("C:/Program Files (x86)/Windows Kits/10"),
File("C:/Program Files/Windows Kits/10"),
File("C:/Program Files (x86)/Windows Kits/10/App Certification Kit"),
File("C:/Program Files/Windows Kits/10/App Certification Kit")
).distinctBy { it.absolutePath.lowercase() }
val sdkBins = roots.flatMap { root ->
safeChildDirectories(root.resolve("bin")).flatMap { versionDir ->
listOf(
versionDir.resolve("x64/$toolName.exe"),
versionDir.resolve("x86/$toolName.exe"),
versionDir.resolve(toolName)
)
}
}
val directBins = roots.map { root -> root.resolve("$toolName.exe") }
val pathBins = (System.getenv("PATH") ?: "")
.split(File.pathSeparator)
.filter { it.isNotBlank() }
.map { File(it).resolve("$toolName.exe") }
return sdkBins + directBins + pathBins
}
fun findWindowsSdkTool(toolName: String, explicitPath: String?): File {
val explicit = explicitPath?.trim()?.takeIf { it.isNotEmpty() }?.let(::File)
if (explicit != null) return explicit
return latestExistingFile(windowsSdkToolCandidates(toolName))
?: File(rootProject.projectDir, "__missing_windows_sdk_tool__/$toolName.exe")
}
val desktopVersionName = "1.0.1"
val desktopFlavor = providers.gradleProperty("desktopFlavor")
.orElse("standard")
@ -565,19 +1159,49 @@ val desktopPackageVersion = providers.gradleProperty("desktopPackageVersion")
.orElse(desktopResolvedVersionName)
.map(::normalizeDesktopPackageVersion)
val desktopPackageName = if (isOssOfflineDesktop) "Episteme oss" else "Episteme"
val desktopLinuxPackageName = if (isOssOfflineDesktop) "episteme-oss" else "episteme"
val desktopPackageDescription = if (isOssOfflineDesktop) {
"Episteme oss offline desktop reader"
} else {
"Episteme desktop reader"
}
val desktopVendor = providers.gradleProperty("desktopVendor").orElse("Aryan")
val desktopVendorName = desktopVendor.get()
val desktopProjectUrl = providers.gradleProperty("desktopProjectUrl")
.orElse("https://github.com/Aryan-Raj3112/episteme")
val desktopOsName = System.getProperty("os.name")
val desktopOsArch = System.getProperty("os.arch")
val desktopPackageArchitecture = normalizeDesktopPackageArchitecture(desktopOsArch)
val desktopAurPackageName = providers.gradleProperty("desktopAurPackageName")
.orElse(if (isOssOfflineDesktop) "episteme-oss-bin" else "episteme-bin")
val desktopAurPackageRelease = providers.gradleProperty("desktopAurPackageRelease")
.orElse("1")
val desktopAurSourceUrl = providers.gradleProperty("desktopAurSourceUrl")
.orElse("")
val desktopPackageTargetFormats = providers.gradleProperty("desktopPackageFormats")
.orElse(desktopDefaultPackageFormats(desktopOsName))
.map { normalizeDesktopPackageFormats(it, desktopOsName) }
.get()
val desktopMsixIdentityName = providers.gradleProperty("desktopMsixIdentityName")
.orElse(if (isOssOfflineDesktop) "Aryan.EpistemeOss" else "Aryan.Episteme")
.map(::normalizeDesktopMsixIdentityName)
.get()
val desktopMsixPublisher = providers.gradleProperty("desktopMsixPublisher")
.orElse("CN=$desktopVendorName")
val desktopMsixPublisherDisplayName = providers.gradleProperty("desktopMsixPublisherDisplayName")
.orElse(desktopVendor)
val desktopMsixVersion = providers.gradleProperty("desktopMsixVersion")
.orElse(desktopPackageVersion)
.map(::normalizeDesktopMsixVersion)
.get()
val desktopMsixArchitecture = desktopMsixArchitecture(desktopOsArch)
val desktopMakeAppxPath = providers.gradleProperty("desktopMakeAppxPath").orNull
val desktopSignToolPath = providers.gradleProperty("desktopSignToolPath").orNull
val desktopMsixCertificatePath = providers.gradleProperty("desktopMsixCertificatePath").orNull
val desktopMsixCertificatePassword = providers.gradleProperty("desktopMsixCertificatePassword")
.orElse("")
val desktopMsixTimestampUrl = providers.gradleProperty("desktopMsixTimestampUrl")
.orElse("http://timestamp.digicert.com")
val desktopNativePackageSupportedHost = desktopOsId(desktopOsName) in setOf("windows", "linux") &&
desktopArchId(desktopOsArch) == "x64"
val desktopReleaseProguardEnabled = providers.gradleProperty("desktopReleaseProguard")
@ -688,6 +1312,127 @@ val verifyDesktopNativePackaging by tasks.registering(VerifyDesktopNativePackagi
missingStandardServiceConfig.set(desktopMissingStandardServiceConfig)
}
val desktopDistributableAppDir = layout.buildDirectory.dir("compose/binaries/main/app/$desktopPackageName")
val desktopReleaseDistributableAppDir = layout.buildDirectory.dir("compose/binaries/main-release/app/$desktopPackageName")
val desktopLinuxTarFileName = "${desktopLinuxPackageName}-${desktopPackageVersion.get()}-linux-$desktopPackageArchitecture.tar.gz"
val desktopAurOutputDir = layout.buildDirectory.dir("aur/${desktopAurPackageName.get()}")
val desktopMsixPackageDir = layout.buildDirectory.dir("msix/package")
val desktopMsixAssetsDir = layout.buildDirectory.dir("msix/generated/assets")
val desktopMsixManifestFile = layout.buildDirectory.file("msix/generated/AppxManifest.xml")
val desktopMsixOutputFile = layout.buildDirectory.file(
"compose/binaries/main-release/msix/${desktopLinuxPackageName}-${desktopPackageVersion.get()}-windows-$desktopPackageArchitecture.msix"
)
val packageLinuxTar by tasks.registering(Tar::class) {
group = "distribution"
description = "Packages the Linux desktop distributable as a tar.gz for Arch/AUR packaging."
dependsOn("createDistributable")
archiveFileName.set(desktopLinuxTarFileName)
destinationDirectory.set(layout.buildDirectory.dir("compose/binaries/main/linux-tar"))
compression = Compression.GZIP
from(desktopDistributableAppDir) {
into(desktopLinuxPackageName)
}
from(desktopLinuxIconFile) {
into("$desktopLinuxPackageName/share")
}
from(rootProject.layout.projectDirectory.file("LICENSE")) {
into("$desktopLinuxPackageName/share/licenses")
}
}
val prepareAurPackage by tasks.registering(PrepareDesktopAurPackageTask::class) {
group = "distribution"
description = "Generates a local AUR package directory with PKGBUILD and .SRCINFO."
dependsOn(packageLinuxTar)
aurPackageName.set(desktopAurPackageName)
providedPackageName.set(desktopLinuxPackageName)
packageVersion.set(desktopPackageVersion)
packageRelease.set(desktopAurPackageRelease)
packageDescription.set(desktopPackageDescription)
appDisplayName.set(desktopPackageName)
installDirectoryName.set(desktopLinuxPackageName)
launcherName.set(desktopLinuxPackageName)
executableName.set(desktopPackageName)
sourceUrl.set(desktopAurSourceUrl)
projectUrl.set(desktopProjectUrl)
linuxTarFile.set(packageLinuxTar.flatMap { it.archiveFile })
outputDirectory.set(desktopAurOutputDir)
}
tasks.register<Exec>("packageAur") {
group = "distribution"
description = "Builds the generated AUR package with makepkg. Run this on Arch Linux."
dependsOn(prepareAurPackage)
commandLine("makepkg", "-sf", "--cleanbuild")
workingDir = desktopAurOutputDir.get().asFile
}
val generateDesktopMsixManifest by tasks.registering(GenerateDesktopMsixManifestTask::class) {
identityName.set(desktopMsixIdentityName)
publisher.set(desktopMsixPublisher)
publisherDisplayName.set(desktopMsixPublisherDisplayName)
packageName.set(desktopPackageName)
packageDescription.set(desktopPackageDescription)
packageVersion.set(desktopMsixVersion)
architecture.set(desktopMsixArchitecture)
executablePath.set("$desktopPackageName.exe")
outputFile.set(desktopMsixManifestFile)
}
val generateDesktopMsixAssets by tasks.registering(GenerateDesktopMsixAssetsTask::class) {
sourceIconFile.set(desktopLinuxIconFile)
outputDirectory.set(desktopMsixAssetsDir)
}
val prepareReleaseMsixPackage by tasks.registering(Sync::class) {
group = "distribution"
description = "Stages the release Windows app image and MSIX metadata for makeappx."
dependsOn("createReleaseDistributable", generateDesktopMsixManifest, generateDesktopMsixAssets)
from(desktopReleaseDistributableAppDir)
from(desktopMsixManifestFile)
from(desktopMsixAssetsDir) {
into("Assets")
}
into(desktopMsixPackageDir)
}
val packageReleaseMsix by tasks.registering(PackageDesktopMsixTask::class) {
group = "distribution"
description = "Packages the release Windows app image as an MSIX using Windows SDK makeappx."
dependsOn(prepareReleaseMsixPackage)
val makeAppx = findWindowsSdkTool("makeappx", desktopMakeAppxPath)
packageRootDirectory.set(desktopMsixPackageDir)
outputFile.set(desktopMsixOutputFile)
makeAppxPath.set(makeAppx.absolutePath)
hostOsId.set(desktopOsId(desktopOsName))
hostArchId.set(desktopArchId(desktopOsArch))
}
val signReleaseMsix = desktopMsixCertificatePath?.trim()?.takeIf { it.isNotEmpty() }?.let { certificatePath ->
tasks.register<SignDesktopMsixTask>("signReleaseMsix") {
group = "distribution"
description = "Signs the release MSIX with signtool when -PdesktopMsixCertificatePath is configured."
dependsOn(packageReleaseMsix)
val signTool = findWindowsSdkTool("signtool", desktopSignToolPath)
val resolvedCertificateFile = File(certificatePath).let { file ->
if (file.isAbsolute) file else project.file(certificatePath)
}
unsignedMsixFile.set(desktopMsixOutputFile)
certificateFile.set(resolvedCertificateFile)
signToolPath.set(signTool.absolutePath)
certificatePassword.set(desktopMsixCertificatePassword)
timestampUrl.set(desktopMsixTimestampUrl)
}
}
kotlin {
jvm("desktop")
jvmToolchain(21)
@ -771,7 +1516,7 @@ compose.desktop {
}
linux {
iconFile.set(desktopLinuxIconFile)
packageName = if (isOssOfflineDesktop) "episteme-oss" else "episteme"
packageName = desktopLinuxPackageName
debMaintainer = "epistemereader@gmail.com"
menuGroup = "Office"
appCategory = "Office"
@ -818,6 +1563,7 @@ tasks.matching {
"packageReleaseDistributionForCurrentOS",
"packageReleaseExe",
"packageReleaseMsi",
"packageReleaseMsix",
"packageReleaseDeb",
"packageReleaseRpm",
"runReleaseDistributable"
@ -853,10 +1599,16 @@ tasks.matching {
"packageReleaseExe",
"packageMsi",
"packageReleaseMsi",
"prepareReleaseMsixPackage",
"packageReleaseMsix",
"signReleaseMsix",
"packageDeb",
"packageReleaseDeb",
"packageRpm",
"packageReleaseRpm",
"packageLinuxTar",
"prepareAurPackage",
"packageAur",
"runDistributable",
"runReleaseDistributable"
)