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:
parent
a13d6599d1
commit
625a4d5d2e
102 changed files with 6012 additions and 687 deletions
|
|
@ -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("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue