mobanisto / pinpit-gradle-plugin

Platform Independent Native Packaging and Installer Toolkit
Apache License 2.0
46 stars 0 forks source link

Support for MSIX packages #22

Open sebkur opened 1 year ago

sebkur commented 1 year ago

We might be able to use MSIX-Hero (Github, Website)

sebkur commented 1 year ago

Here's an official MSIX overview page

sebkur commented 1 year ago

It might be possible to use microsoft/msix-packaging, an official cross-platform C library for packaging MSIX packages. There are samples within that repository. The API seems to be documented here.

benkuly commented 8 months ago

We use MSIXHero if Windows-only would be okay. I can post our gradle config, which builds and signs a msix file via MSIXHero. It also creates an .appinstaller file (basically just XML) for auto-update functionality.

sebkur commented 8 months ago

Windows-only is not optimal, ideally we find a cross-platform solution. Still curious to see your configuration file.

benkuly commented 8 months ago
val appName = "ABC"
val appDescription = "Lorem Ipsum"
val appPackage = "org.example.abc"
val appVersion = "1.2.3"
val msixFileName = "$appName-${appVersion}.msix"
val publisherName = "example Company"
val publisherCN = "CN=xample Company, O=xample Company, L=Bla, S=Bla, C=US"

val urlSchema = "abc"
val logoFileName = "logo.png"
val logo44FileName = "logo_44.png"
val logo155FileName = "logo_155.png"

val distributionDir: Provider<Directory> = compose.desktop.nativeApplication.distributions.outputBaseDir
    .map { it.dir("main").dir("app") }
val msixBuildDir: File = layout.buildDirectory.dir("msix").get().asFile.apply { createDirectory() }

val createMsixManifest by tasks.registering {
    doLast {
        distributionDir.get().dir(appName).file("AppxManifest.xml").asFile.apply {
            createNewFile()
            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:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
                  xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
                  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
                  IgnorableNamespaces="uap10 rescap">
                  <Identity Name="$appPackage" Publisher="$publisherCN" Version="${appVersion}.0" ProcessorArchitecture="x64" />
                  <Properties>
                    <DisplayName>$appName</DisplayName>
                    <PublisherDisplayName>$publisherName</PublisherDisplayName>
                    <Description>$appDescription</Description>
                    <Logo>$logoFileName</Logo>
                    <uap10:PackageIntegrity>
                      <uap10:Content Enforcement="on" />
                    </uap10:PackageIntegrity>
                  </Properties>
                  <Resources>
                    <Resource Language="de-de" />
                  </Resources>
                  <Dependencies>
                    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.22000.1" />
                  </Dependencies>
                  <Capabilities>
                    <rescap:Capability Name="runFullTrust" />
                  </Capabilities>
                  <Applications>
                    <Application
                      Id="$appPackage"
                      Executable="$appName.exe"
                      EntryPoint="Windows.FullTrustApplication">
                      <uap:VisualElements DisplayName="$appName" Description="$appDescription"  Square150x150Logo="$logo155FileName"
                         Square44x44Logo="$logo44FileName" BackgroundColor="white" />
                      <Extensions>
                        <uap:Extension Category="windows.protocol">
                          <uap:Protocol Name="$urlSchema" />
                        </uap:Extension>
                      </Extensions>
                    </Application>
                  </Applications>
                </Package>
                """.trimIndent()
            )
        }
    }
    dependsOn(tasks.getByName("createDistributable"))
    onlyIf { isWindows }
}

val createMsixAppinstaller by tasks.registering {
    doLast {
        val msixBaseUrl = requireNotNull(System.getenv("MSIX_BASE_URL"))
        val appinstallerFileName = "$appName.appinstaller"
        msixBuildDir.resolve(appinstallerFileName).apply {
            createNewFile()
            writeText(
                """
                <?xml version="1.0" encoding="utf-8"?>
                <AppInstaller
                    xmlns="http://schemas.microsoft.com/appx/appinstaller/2018"
                    Version="${appVersion}.0"
                    Uri="$msixBaseUrl/$appinstallerFileName">
                    <MainPackage
                        Name="$appPackage"
                        Publisher="$publisherCN"
                        Version="${appVersion}.0"
                        ProcessorArchitecture="x64"
                        Uri="$msixBaseUrl/$msixFileName" />
                    <UpdateSettings>
                        <OnLaunch 
                            HoursBetweenUpdateChecks="12"
                            UpdateBlocksActivation="true"
                            ShowPrompt="true" />
                        <ForceUpdateFromAnyVersion>false</ForceUpdateFromAnyVersion>
                        <AutomaticBackgroundTask />
                    </UpdateSettings>
                </AppInstaller>
                """.trimIndent()
            )
        }
    }
    onlyIf { isWindows && isCI }
}

val copyMsixLogos by tasks.registering(Copy::class) {
    from(projectDir.resolve("src").resolve("desktopMain").resolve("resources")) {
        include(logoFileName, logo44FileName, logo155FileName)
    }
    into(distributionDir.get().dir(appName).asFile)
    dependsOn(tasks.getByName("createDistributable"))
    onlyIf { isWindows }
}

val msixHeroPack by tasks.registering(Exec::class) {
    workingDir(msixBuildDir)
    executable = "MSIXHeroCLI.exe"
    args(
        "pack",
        "--directory", distributionDir.get().dir(appName).asFile.absolutePath,
        "--package", msixFileName,
    )
    dependsOn(tasks.getByName("createDistributable"), createMsixManifest, copyMsixLogos)
    onlyIf { isWindows }
}

val msixHeroSign by tasks.registering(Exec::class) {
    workingDir(msixBuildDir)
    executable = "MSIXHeroCLI.exe"
    args(
        "sign",
        "--file", System.getenv("CODE_SIGNING_CERT_FILE")?.let { rootDir.resolve(it).absolutePath } ?: "",
        "--password", System.getenv("CODE_SIGNING_CERT_FILE_PASSWORD") ?: "",
        "--timestamp", System.getenv("CODE_SIGNING_TIMESTAMP_SERVER") ?: "",
        msixFileName
    )
    dependsOn(msixHeroPack)
    onlyIf { isWindows }
}

val packageMsix by tasks.registering {
    group = "compose desktop"
    dependsOn(msixHeroPack, msixHeroSign, createMsixAppinstaller)
    onlyIf { isWindows }
}

It works very good including auto update :)

sebkur commented 8 months ago

Thanks. I guess it's worth a try to see if this maybe even works via wine on Linux.