diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 55af32c..41f42fb 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -17,4 +17,5 @@ dependencies{ implementation(libs.semver) implementation("ir.amirab.util:platform:1") implementation("ir.amirab.plugin:git-version-plugin:1") + implementation("ir.amirab.plugin:installer-plugin:1") } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/buildlogic/CiUtils.kt b/buildSrc/src/main/kotlin/buildlogic/CiUtils.kt index d4a094c..f959d46 100644 --- a/buildSrc/src/main/kotlin/buildlogic/CiUtils.kt +++ b/buildSrc/src/main/kotlin/buildlogic/CiUtils.kt @@ -1,21 +1,20 @@ package buildlogic import io.github.z4kn4fein.semver.Version +import ir.amirab.installer.InstallerTargetFormat import ir.amirab.util.platform.Platform -import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions import org.jetbrains.compose.desktop.application.dsl.TargetFormat import java.io.File - object CiUtils { fun getTargetFileName( packageName: String, appVersion: Version, - target: TargetFormat, + target: InstallerTargetFormat?, ): String { val fileExtension = when (target) { - // we use archived for app image distribution - TargetFormat.AppImage -> { + // we use archived for app image distribution ( app image is a folder actually so there is no installer so we zip it instead) + null -> { when (Platform.getCurrentPlatform()) { Platform.Desktop.Linux -> "tar.gz" Platform.Desktop.MacOS -> "tar.gz" @@ -28,7 +27,7 @@ object CiUtils { } val platformName = when (target) { - TargetFormat.AppImage -> Platform.getCurrentPlatform() + null -> Platform.getCurrentPlatform() else -> { val packageFileExt = target.fileExtensionWithoutDot() requireNotNull(Platform.fromExecutableFileExtension(packageFileExt)) { @@ -41,9 +40,10 @@ object CiUtils { fun getFileOfPackagedTarget( baseOutputDir: File, - target: TargetFormat, + target: InstallerTargetFormat, ): File { - val folder = baseOutputDir.resolve(target.outputDirName) + val folder = baseOutputDir +// val folder = baseOutputDir.resolve(target.outputDirName) val exeFile = kotlin.runCatching { folder.walk().first { it.name.endsWith(target.fileExt) @@ -89,7 +89,7 @@ object CiUtils { fun movePackagedAndCreateSignature( appVersion: Version, packageName: String, - target: TargetFormat, + target: InstallerTargetFormat, basePackagedAppsDir: File, outputDir: File, ) { @@ -148,4 +148,4 @@ object CiUtils { */ } -private fun TargetFormat.fileExtensionWithoutDot() = fileExt.substring(".".length) \ No newline at end of file +private fun InstallerTargetFormat.fileExtensionWithoutDot() = fileExt.substring(".".length) \ No newline at end of file diff --git a/compositeBuilds/plugins/installer-plugin/build.gradle.kts b/compositeBuilds/plugins/installer-plugin/build.gradle.kts new file mode 100644 index 0000000..c817e9e --- /dev/null +++ b/compositeBuilds/plugins/installer-plugin/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `kotlin-dsl` +} +repositories { + mavenCentral() +} +version = 1 +group = "ir.amirab.plugin" +dependencies { + implementation("ir.amirab.util:platform:1") + implementation(libs.handlebarsJava) +} +gradlePlugin { + plugins { + create("installer-plugin") { + id = "ir.amirab.installer-plugin" + implementationClass = "ir.amirab.installer.InstallerPlugin" + } + } +} \ No newline at end of file diff --git a/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerPlugin.kt b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerPlugin.kt new file mode 100644 index 0000000..b3191dd --- /dev/null +++ b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerPlugin.kt @@ -0,0 +1,61 @@ +package ir.amirab.installer + +import ir.amirab.installer.extensiion.InstallerPluginExtension +import ir.amirab.installer.tasks.windows.NsisTask +import ir.amirab.installer.utils.Constants +import ir.amirab.util.platform.Platform +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register + +class InstallerPlugin : Plugin { + override fun apply(target: Project) { + val extension = target.extensions.create("installerPlugin", InstallerPluginExtension::class) + target.afterEvaluate { + registerTasks(target, extension) + } + } + + private fun registerTasks( + project: Project, + extension: InstallerPluginExtension + ) { + val windowConfig = extension.windowsConfig + val createInstallerTaskName = Constants.CREATE_INSTALLER_TASK_NAME + val createInstallerNsisTaskName = "${createInstallerTaskName}Nsis" + if (windowConfig != null) { + project.tasks + .register(createInstallerNsisTaskName) + .configure { + dependsOn(extension.taskDependencies.toTypedArray()) + this.nsisTemplate.set(requireNotNull(windowConfig.nsisTemplate) { "Nsis Template not provided" }) + this.commonParams.set(windowConfig) + this.extraParams.set(windowConfig.extraParams) + this.destFolder.set(extension.outputFolder.get().asFile) + this.outputFileName.set(requireNotNull(windowConfig.outputFileName) { " outputFileName not provided " }) + this.sourceFolder.set(requireNotNull(windowConfig.inputDir) { "inputDir not provided" }) + } + } + project.tasks.register(createInstallerTaskName) { + // when we want to create installer we need to prepare its input first! + when (val platform = Platform.getCurrentPlatform()) { + Platform.Desktop.Linux -> { + // nothing yet + } + + Platform.Desktop.MacOS -> { + // nothing yet + } + + Platform.Desktop.Windows -> { + if (windowConfig != null) { + dependsOn(createInstallerNsisTaskName) + } + } + + else -> error("unsupported platform: $platform") + } + } + } +} \ No newline at end of file diff --git a/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerTargetFormat.kt b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerTargetFormat.kt new file mode 100644 index 0000000..02957b4 --- /dev/null +++ b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerTargetFormat.kt @@ -0,0 +1,28 @@ +package ir.amirab.installer + +import ir.amirab.util.platform.Platform + + +enum class InstallerTargetFormat( + val id: String, + val targetOS: Platform, +) { + Deb("deb", Platform.Desktop.Linux), + Rpm("rpm", Platform.Desktop.Linux), + Dmg("dmg", Platform.Desktop.MacOS), + Pkg("pkg", Platform.Desktop.MacOS), + Exe("exe", Platform.Desktop.Windows), + Msi("msi", Platform.Desktop.Windows); + + val isCompatibleWithCurrentOS: Boolean by lazy { isCompatibleWith(Platform.getCurrentPlatform()) } + + fun isCompatibleWith(os: Platform): Boolean = os == targetOS + + val outputDirName: String + get() = id + + val fileExt: String + get() { + return ".$id" + } +} \ No newline at end of file diff --git a/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/extensiion/InstallerPluginExtension.kt b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/extensiion/InstallerPluginExtension.kt new file mode 100644 index 0000000..de45f67 --- /dev/null +++ b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/extensiion/InstallerPluginExtension.kt @@ -0,0 +1,85 @@ +package ir.amirab.installer.extensiion + +import ir.amirab.installer.InstallerTargetFormat +import ir.amirab.installer.utils.Constants +import ir.amirab.util.platform.Platform +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.TaskProvider +import java.io.File +import java.io.Serializable +import javax.inject.Inject + +abstract class InstallerPluginExtension { + @get:Inject + internal abstract val project: Project + + abstract val outputFolder: DirectoryProperty + + internal val taskDependencies = mutableListOf() + + fun dependsOn(vararg tasks: Any) { + taskDependencies.addAll(tasks) + } + + internal var windowsConfig: WindowsConfig? = null + private set + + fun windows( + config: WindowsConfig.() -> Unit + ) { + if (Platform.getCurrentPlatform() != Platform.Desktop.Windows) return + val windowsConfig = if (this.windowsConfig == null) { + WindowsConfig().also { + this.windowsConfig = it + } + } else { + this.windowsConfig!! + } + windowsConfig.config() + } + + val createInstallerTask: TaskProvider by lazy { + project.tasks.named(Constants.CREATE_INSTALLER_TASK_NAME) + } + + fun isThisPlatformSupported() = when (Platform.getCurrentPlatform()) { + Platform.Desktop.Windows -> windowsConfig != null + else -> { + false + } + } + + fun getCreatedInstallerTargetFormats(): List { + return buildList { + when (Platform.getCurrentPlatform()) { + Platform.Desktop.Windows -> { + if (windowsConfig != null) { + add(InstallerTargetFormat.Exe) + } + } + + else -> {} + } + } + } +} + +data class WindowsConfig( + var appName: String? = null, + var appDisplayName: String? = null, + var appVersion: String? = null, + var appDisplayVersion: String? = null, + var iconFile: File? = null, + var licenceFile: File? = null, + + var outputFileName: String? = null, + + var inputDir: File? = null, + + var nsisTemplate: File? = null, + + var extraParams: Map = emptyMap() +) : Serializable + diff --git a/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/tasks/windows/NsisTask.kt b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/tasks/windows/NsisTask.kt new file mode 100644 index 0000000..a6f4a6e --- /dev/null +++ b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/tasks/windows/NsisTask.kt @@ -0,0 +1,91 @@ +package ir.amirab.installer.tasks.windows + +import com.github.jknack.handlebars.Context +import com.github.jknack.handlebars.Handlebars +import ir.amirab.installer.extensiion.WindowsConfig +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.mapProperty +import java.io.ByteArrayInputStream +import java.io.File + +abstract class NsisTask : DefaultTask() { + + @get:InputDirectory + abstract val sourceFolder: DirectoryProperty + + @get:OutputDirectory + abstract val destFolder: DirectoryProperty + + @get:Input + abstract val outputFileName: Property + + @get:InputFile + abstract val nsisTemplate: Property + + @get:Input + abstract val commonParams: Property + + @get:Input + val extraParams: MapProperty = project.objects.mapProperty() + + @get:Internal + abstract val nsisExecutable: Property + + init { + nsisExecutable.convention( + project.provider { File("C:\\Program Files (x86)\\NSIS\\makensis.exe") } + ) + } + + private fun createHandleBarContext(): Context { + val commonParams = commonParams.get() + val common = mapOf( + "app_name" to commonParams.appName!!, + "app_display_name" to commonParams.appDisplayName!!, + "app_version" to commonParams.appVersion!!, + "app_display_version" to commonParams.appDisplayVersion!!, + "license_file" to commonParams.licenceFile!!, + "icon_file" to commonParams.iconFile!!, + ) + val overrides = mapOf( + "input_dir" to sourceFolder.get().asFile.absolutePath, + "output_file" to "${destFolder.file(outputFileName).get().asFile.path}.exe", + ) + return Context.newContext( + extraParams + .get() + .plus(common) + .plus(overrides) + ) + } + + @TaskAction + fun run() { + val executable = nsisExecutable.get() + val scriptTemplate = nsisTemplate.get() + val handlebars = Handlebars() + val context = createHandleBarContext() + val script = handlebars.compileInline( + scriptTemplate.readText() + ).apply(context) + logger.debug("NSIS Script:") + logger.debug(script) + project.exec { + executable( + executable, + ) + args("-") + standardInput = ByteArrayInputStream(script.toByteArray()) + + } + } +} \ No newline at end of file diff --git a/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/utils/Contants.kt b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/utils/Contants.kt new file mode 100644 index 0000000..214bb63 --- /dev/null +++ b/compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/utils/Contants.kt @@ -0,0 +1,5 @@ +package ir.amirab.installer.utils + +internal object Constants { + const val CREATE_INSTALLER_TASK_NAME = "createInstaller" +} \ No newline at end of file diff --git a/compositeBuilds/plugins/settings.gradle.kts b/compositeBuilds/plugins/settings.gradle.kts index 13fc438..f51e35c 100644 --- a/compositeBuilds/plugins/settings.gradle.kts +++ b/compositeBuilds/plugins/settings.gradle.kts @@ -5,4 +5,5 @@ dependencyResolutionManagement{ } } } -include("git-version-plugin") \ No newline at end of file +include("git-version-plugin") +include("installer-plugin") \ No newline at end of file diff --git a/desktop/app/build.gradle.kts b/desktop/app/build.gradle.kts index bc69a3d..c88d0c6 100644 --- a/desktop/app/build.gradle.kts +++ b/desktop/app/build.gradle.kts @@ -1,8 +1,10 @@ import buildlogic.* import buildlogic.versioning.* +import ir.amirab.installer.InstallerTargetFormat import org.jetbrains.changelog.Changelog import org.jetbrains.compose.desktop.application.dsl.TargetFormat import ir.amirab.util.platform.Platform +import org.jetbrains.compose.desktop.application.dsl.TargetFormat.* plugins { id(MyPlugins.kotlin) @@ -12,9 +14,9 @@ plugins { id(Plugins.changeLog) id(Plugins.ksp) id(Plugins.aboutLibraries) + id("ir.amirab.installer-plugin") // id(MyPlugins.proguardDesktop) } - dependencies { implementation(libs.decompose) implementation(libs.decompose.jbCompose) @@ -102,7 +104,7 @@ compose { mainClass = "$desktopPackageName.AppKt" nativeDistributions { modules("java.instrument", "jdk.unsupported") - targetFormats(TargetFormat.Msi, TargetFormat.Deb) + targetFormats(Msi, Deb) if (Platform.getCurrentPlatform() == Platform.Desktop.Linux) { // filekit library requires this module in linux. modules("jdk.security.auth") @@ -114,21 +116,21 @@ compose { val menuGroupName = getPrettifiedAppName() licenseFile.set(rootProject.file("LICENSE")) linux { - debPackageVersion = getAppVersionStringForPackaging(TargetFormat.Deb) - rpmPackageVersion = getAppVersionStringForPackaging(TargetFormat.Rpm) + debPackageVersion = getAppVersionStringForPackaging(Deb) + rpmPackageVersion = getAppVersionStringForPackaging(Rpm) appCategory = "Network" iconFile = project.file("icons/icon.png") menuGroup = menuGroupName shortcut = true } macOS { - pkgPackageVersion = getAppVersionStringForPackaging(TargetFormat.Pkg) - dmgPackageVersion = getAppVersionStringForPackaging(TargetFormat.Dmg) + pkgPackageVersion = getAppVersionStringForPackaging(Pkg) + dmgPackageVersion = getAppVersionStringForPackaging(Dmg) iconFile = project.file("icons/icon.icns") } windows { - exePackageVersion = getAppVersionStringForPackaging(TargetFormat.Exe) - msiPackageVersion = getAppVersionStringForPackaging(TargetFormat.Msi) + exePackageVersion = getAppVersionStringForPackaging(Exe) + msiPackageVersion = getAppVersionStringForPackaging(Msi) upgradeUuid = properties["INSTALLER.WINDOWS.UPGRADE_UUID"]?.toString() iconFile = project.file("icons/icon.ico") console = false @@ -142,6 +144,32 @@ compose { } } +installerPlugin { + dependsOn("createReleaseDistributable") + outputFolder.set(layout.buildDirectory.dir("custom-installer")) + windows { + appName = getAppName() + appDisplayName = getPrettifiedAppName() + appVersion = getAppVersionStringForPackaging(Exe) + appDisplayVersion = getAppVersionString() + inputDir = project.file("build/compose/binaries/main-release/app/${getAppName()}") + outputFileName = getAppName() + licenceFile = rootProject.file("LICENSE") + iconFile = project.file("icons/icon.ico") + nsisTemplate = project.file("resources/installer/nsis-script-template.nsi") + extraParams = mapOf( + "app_publisher" to "abdownloadmanager.com", + "app_version_with_build" to "${getAppVersionStringForPackaging(Exe)}.0", + "source_code_url" to "https://github.com/amir1376/ab-download-manager", + "project_website" to "www.abdownloadmanager.com", + "copyright" to "© 2024-present AB Download Manager App", + "header_image_file" to project.file("resources/installer/abdm-header-image.bmp"), + "sidebar_image_file" to project.file("resources/installer/abdm-sidebar-image.bmp") + ) + + } +} + // generate a file with these constants buildConfig { @@ -240,27 +268,51 @@ val createDistributableAppArchive by tasks.registering { val createBinariesForCi by tasks.registering { val nativeDistributions = compose.desktop.application.nativeDistributions val mainRelease = nativeDistributions.outputBaseDir.dir("main-release") - dependsOn("packageReleaseDistributionForCurrentOS") + if (installerPlugin.isThisPlatformSupported()) { + dependsOn(installerPlugin.createInstallerTask) + inputs.dir(installerPlugin.outputFolder) + } else { + dependsOn("packageReleaseDistributionForCurrentOS") + inputs.dir(mainRelease) + } dependsOn(createDistributableAppArchive) inputs.property("appVersion", getAppVersionString()) - inputs.dir(mainRelease) inputs.dir(distributableAppArchiveDir) outputs.dir(ciDir.binariesDir) doLast { val output = ciDir.binariesDir.get().asFile val packageName = appPackageNameByComposePlugin output.deleteRecursively() - val allowedTarget = nativeDistributions.targetFormats.filter { it.isCompatibleWithCurrentOS } - for (target in allowedTarget) { - CiUtils.movePackagedAndCreateSignature( - getAppVersion(), - packageName, - target, - mainRelease.get().asFile, - output, - ) + + if (installerPlugin.isThisPlatformSupported()) { + val targets = installerPlugin.getCreatedInstallerTargetFormats() + for (target in targets) { + CiUtils.movePackagedAndCreateSignature( + getAppVersion(), + packageName, + target, + installerPlugin.outputFolder.get().asFile, + output, + ) + } + logger.lifecycle("app packages for '${targets.joinToString(", ") { it.name }}' written in $output using the installer plugin") + } else { + val allowedTargets = nativeDistributions + .targetFormats.filter { it.isCompatibleWithCurrentOS } + .map { + it.toInstallerTargetFormat() + } + for (target in allowedTargets) { + CiUtils.movePackagedAndCreateSignature( + getAppVersion(), + packageName, + target, + mainRelease.get().asFile.resolve(target.outputDirName), + output, + ) + } + logger.lifecycle("app packages for '${allowedTargets.joinToString(", ") { it.name }}' written in $output using compose packager tool") } - logger.lifecycle("app packages for '${allowedTarget.joinToString(", ") { it.name }}' written in $output") val appArchiveDistributableDir = distributableAppArchiveDir.get().asFile CiUtils.copyAndHashToDestination( distributableAppArchiveDir.get().asFile.resolve( @@ -272,7 +324,7 @@ val createBinariesForCi by tasks.registering { CiUtils.getTargetFileName( packageName, getAppVersion(), - TargetFormat.AppImage, + null, // this is not an installer (it will be automatically converted to current os name ) ) logger.lifecycle("distributable app archive written in ${output}") @@ -299,3 +351,15 @@ val createReleaseFolderForCi by tasks.registering { dependsOn(createBinariesForCi, createChangeNoteForCi) } // ======= end of GitHub action stuff + +fun TargetFormat.toInstallerTargetFormat(): InstallerTargetFormat { + return when (this) { + AppImage -> error("$this is not recognized as installer") + Deb -> InstallerTargetFormat.Deb + Rpm -> InstallerTargetFormat.Rpm + Dmg -> InstallerTargetFormat.Dmg + Pkg -> InstallerTargetFormat.Pkg + Exe -> InstallerTargetFormat.Exe + Msi -> InstallerTargetFormat.Msi + } +} diff --git a/desktop/app/resources/installer/abdm-header-image.bmp b/desktop/app/resources/installer/abdm-header-image.bmp new file mode 100644 index 0000000..b589a96 Binary files /dev/null and b/desktop/app/resources/installer/abdm-header-image.bmp differ diff --git a/desktop/app/resources/installer/abdm-sidebar-image.bmp b/desktop/app/resources/installer/abdm-sidebar-image.bmp new file mode 100644 index 0000000..5b887c9 Binary files /dev/null and b/desktop/app/resources/installer/abdm-sidebar-image.bmp differ diff --git a/desktop/app/resources/installer/nsis-script-template.nsi b/desktop/app/resources/installer/nsis-script-template.nsi new file mode 100644 index 0000000..13fba40 --- /dev/null +++ b/desktop/app/resources/installer/nsis-script-template.nsi @@ -0,0 +1,212 @@ +Unicode True +RequestExecutionLevel user +SetCompressor /SOLID lzma +!include "LogicLib.nsh" +!include "MUI2.nsh" + + +!define APP_PUBLISHER "{{ app_publisher }}" +!define APP_NAME "{{ app_name }}" +!define APP_DISPLAY_NAME "{{ app_display_name }}" +!define APP_VERSION "{{ app_version }}" +!define APP_VERSION_WITH_BUILD "{{ app_version_with_build }}" +!define APP_DISPLAY_VERSION "{{ app_display_version }}" +!define SOURCE_CODE_URL "{{ source_code_url }}" +!define PROJECT_WEBSITE "{{ project_website }}" +!define COPYRIGHT "{{ copyright }}" + +!define INPUT_DIR "{{ input_dir }}" +!define LICENSE_FILE "{{ license_file }}" +!define MAIN_BINARY_NAME "${APP_NAME}" + +!define SIDEBAR_IMAGE "{{ sidebar_image_file }}" +!define HEADER_IMAGE "{{ header_image_file }}" +!define ICON_FILE "{{ icon_file }}" + +!define REG_UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" +!define REG_RUN_KEY "Software\Microsoft\Windows\CurrentVersion\Run\${APP_NAME}" +!define REG_APP_KEY "Software\${APP_NAME}" + +; icon for this installer! + +Icon "${ICON_FILE}" +!define MUI_ICON "${ICON_FILE}" +!define MUI_UNICON "${ICON_FILE}" + +!if "${SIDEBAR_IMAGE}" != "" + !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBAR_IMAGE}" + + !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${SIDEBAR_IMAGE}" +!endif + +!if "${HEADER_IMAGE}" != "" + !define MUI_HEADERIMAGE + !define MUI_HEADERIMAGE_BITMAP "${HEADER_IMAGE}" + + !define MUI_UNHEADERIMAGE + !define MUI_UNHEADERIMAGE_BITMAP "${HEADER_IMAGE}" +!endif + +VIProductVersion "${APP_VERSION_WITH_BUILD}" +VIAddVersionKey "ProductName" "${APP_DISPLAY_NAME}" +VIAddVersionKey "FileDescription" "${APP_DISPLAY_NAME}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileVersion" "${APP_VERSION_WITH_BUILD}" +VIAddVersionKey "ProductVersion" "${APP_VERSION_WITH_BUILD}" + +Name "${APP_DISPLAY_NAME}" +OutFile "{{ output_file }}" + +InstallDir "$LOCALAPPDATA\${APP_NAME}" + + + +!define INSTALL_DIR `$INSTDIR` +Function .onInit + + ; Call RestorePreviousInstallLocation + +FunctionEnd + +; configure instfiles page +!define MUI_FINISHPAGE_NOAUTOCLOSE +!define MUI_INSTFILESPAGE_NOAUTOCLOSE + +; configure finish page +!define MUI_FINISHPAGE_LINK "Open project in GitHub" +!define MUI_FINISHPAGE_LINK_LOCATION "${SOURCE_CODE_URL}" +!define MUI_FINISHPAGE_RUN +!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary + +;Installation Pages +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "${LICENSE_FILE}" +!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +;Uninstallation Pages +!insertmacro MUI_UNPAGE_WELCOME +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_UNPAGE_FINISH + +; set language +!insertmacro MUI_LANGUAGE "English" + +; a macro clear files to cleanup installation folder +!macro clearFiles + RmDir /r "${INSTALL_DIR}\app" + RmDir /r "${INSTALL_DIR}\runtime" + Delete "${INSTALL_DIR}\${MAIN_BINARY_NAME}.exe" + Delete "${INSTALL_DIR}\${MAIN_BINARY_NAME}.ico" + Delete "${INSTALL_DIR}\uninstall.exe" + RmDir "${INSTALL_DIR}" +!macroend + +Function RunMainBinary + Exec "${INSTALL_DIR}\${MAIN_BINARY_NAME}.exe" +FunctionEnd + +!macro GetBestExecutableName result + StrCpy ${result} "${MAIN_BINARY_NAME}.exe" +!macroend + +; Function RestorePreviousInstallLocation +; ReadRegStr $4 SHCTX "${REG_APP_KEY}" "InstallPath" +; ${if} $4 != "" +; StrCpy $INSTDIR $4 +; ${endif} +; FunctionEnd + +; I should improve this. +!macro closeApp + !insertmacro GetBestExecutableName $1 + DetailPrint "Stopping Executable $1" + ; I don't wanna kill myself! + ${If} "$EXEFILE" != "$1" + ExecWait 'taskkill /F /IM "$1"' $0 + ${Else} + DetailPrint "It seems that installer file name is same as app executable name" + DetailPrint "Please close app manually" + ; don't sleep the script for nothing. + StrCpy $0 "1" + ${EndIf} + ${If} $0 == "0" + Sleep 500 + BringToFront ; when we sleep it seems that window goes down + DetailPrint "Current app stopped successfully" + ${Endif} +!macroend + +!macro CreateStartMenu + createDirectory "$SMPROGRAMS\${APP_DISPLAY_NAME}" + createShortCut "$SMPROGRAMS\${APP_DISPLAY_NAME}\${APP_DISPLAY_NAME}.lnk" "${INSTALL_DIR}\${MAIN_BINARY_NAME}.exe" "" "${INSTALL_DIR}\${MAIN_BINARY_NAME}.ico" +!macroend + +!macro RemoveStartMenu + RmDir /r "$SMPROGRAMS\${APP_DISPLAY_NAME}" +!macroend + +!macro CreateDesktopShortcut + CreateShortcut "$DESKTOP\${APP_DISPLAY_NAME}.lnk" "${INSTALL_DIR}\${MAIN_BINARY_NAME}.exe" "" "${INSTALL_DIR}\${MAIN_BINARY_NAME}.ico" +!macroend + +!macro RemoveDesktopShortCut + Delete "$DESKTOP\${APP_DISPLAY_NAME}.lnk" +!macroend + + + +Section "${APP_DISPLAY_NAME}" + SectionInstType RO + + DetailPrint "Closing app (if any)" + !insertmacro closeApp + DetailPrint "clearing old app (if any)" + !insertmacro clearFiles + DetailPrint "writing new data" + SetOutPath "${INSTALL_DIR}" + CreateDirectory "${INSTALL_DIR}" + + WriteUninstaller "${INSTALL_DIR}\uninstall.exe" + + File /nonfatal /r "${INPUT_DIR}\" + + + ; Registry information for add/remove programs + WriteRegStr SHCTX "${REG_UNINSTALL_KEY}" "DisplayName" "${APP_DISPLAY_NAME}" + WriteRegStr SHCTX "${REG_UNINSTALL_KEY}" "DisplayIcon" "$\"${INSTALL_DIR}\${MAIN_BINARY_NAME}.exe$\"" + WriteRegStr SHCTX "${REG_UNINSTALL_KEY}" "DisplayVersion" "${APP_VERSION}" + WriteRegStr SHCTX "${REG_UNINSTALL_KEY}" "Publisher" "${APP_PUBLISHER}" + WriteRegStr SHCTX "${REG_UNINSTALL_KEY}" "InstallLocation" "$\"${INSTALL_DIR}$\"" + WriteRegStr SHCTX "${REG_UNINSTALL_KEY}" "UninstallString" "$\"${INSTALL_DIR}\uninstall.exe$\"" + WriteRegDWORD SHCTX "${REG_UNINSTALL_KEY}" "NoModify" "1" + WriteRegDWORD SHCTX "${REG_UNINSTALL_KEY}" "NoRepair" "1" + + ; Registry keys for app installation path and version + WriteRegStr SHCTX "${REG_APP_KEY}" "InstallPath" "${INSTALL_DIR}" + WriteRegStr SHCTX "${REG_APP_KEY}" "Version" "${APP_VERSION}" +SectionEnd + +Section "Start Menu" + !insertmacro CreateStartMenu +SectionEnd + +Section "Desktop Shortcut" + !insertmacro CreateDesktopShortcut +SectionEnd + +Section "Uninstall" + !insertmacro closeApp + !insertmacro clearFiles + + !insertmacro RemoveStartMenu + !insertmacro RemoveDesktopShortCut + + DeleteRegKey SHCTX "${REG_UNINSTALL_KEY}" + DeleteRegKey SHCTX "${REG_APP_KEY}" + + ; remove auto start on boot registry + DeleteRegValue SHCTX "${REG_RUN_KEY}" "${APP_NAME}" +SectionEnd diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c392bd..322c747 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,6 +117,8 @@ arrow-opticKsp = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } osThemeDetector = { module = "com.github.Dansoftowner:jSystemThemeDetector", version.ref = "osThemeDetector" } +handlebarsJava = "com.github.jknack:handlebars:4.4.0" + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose" }