Files
NilsBriggen fa0864394f
CI / pull_request_policy (push) Skipped
CI / markdown (push) Successful in 53s
CI / build (push) Failing after 2m17s
Rework Gitea CI and cross-platform release builds
2026-09-24 22:56:40 +02:00

1166 lines
40 KiB
Kotlin
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Logisim-evolution - digital logic design tool and simulator
* Copyright by the Logisim-evolution developers
*
* https://github.com/logisim-evolution/
*
* This is free software released under GNU GPLv3 license
*/
import org.gradle.api.logging.Logging
import org.gradle.internal.os.OperatingSystem
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat
import java.util.Date
import org.gradle.jvm.application.tasks.CreateStartScripts
plugins {
checkstyle
id("io.github.ben-manes.versions") version "0.64.0"
java
application
id("com.gradleup.shadow") version "9.6.1"
id("org.sonarqube") version "7.5.0.8588"
}
repositories {
mavenCentral()
}
sonar {
properties {
property("sonar.projectKey", "dev_briggen_logisim_revolution")
}
}
application {
mainClass.set("com.cburch.logisim.Main")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}
dependencies {
implementation("org.hamcrest:hamcrest:3.0")
implementation("javax.help:javahelp:2.0.05")
implementation("com.fifesoft:rsyntaxtextarea:4.0.1")
implementation("org.drjekyll:colorpicker:2.0.1")
implementation("at.swimmesberger:swingx-core:1.6.8")
implementation("org.scijava:swing-checkbox-tree:1.0.2")
implementation("org.slf4j:slf4j-api:2.0.19")
implementation("org.slf4j:slf4j-simple:2.0.19")
implementation("com.formdev:flatlaf:3.7.2")
implementation("com.formdev:flatlaf-extras:3.7.2")
implementation("commons-cli:commons-cli:1.11.0")
implementation("com.vladsch.flexmark:flexmark-all:0.64.8")
implementation("org.apache.commons:commons-text:1.15.0")
// NOTE: Be aware of reported issues with Eclipse and Batik
// See: https://github.com/logisim-evolution/logisim-evolution/issues/709
// implementation("org.apache.xmlgraphics:batik-swing:1.14")
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
testImplementation("org.mockito:mockito-junit-jupiter:5.23.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
/**
* Strings used as keys to reference shared variables (via `ext.*`)
*/
val APP_DIR_NAME = "appDirName"
val APP_VERSION = "appVersion"
val APP_VERSION_SHORT = "appVersionShort"
val APP_URL = "appUrl"
val BUILD_DIR = "buildDir"
val JDEPS = "jdeps"
val JDEPS_FILE = "jdepsFile"
val JPACKAGE = "jpackage"
val LIBS_DIR = "libsDir"
val LINUX_PARAMS = "linuxParameters"
val OS_ARCH = "osArch"
val PACKAGE_INPUT_DIR = "packageInputDir"
val SHADOW_JAR_FILE_NAME = "shadowJarFilename"
val SHARED_PARAMS = "sharedParameters"
val SUPPORT_DIR = "supportDir"
val TARGET_DIR = "targetDir"
val TARGET_FILE_PATH_BASE = "targetFilePathBase"
val TARGET_FILE_PATH_BASE_SHORT = "targetFilePathBaseShort"
val UPPERCASE_PROJECT_NAME = "uppercaseProjectName"
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
/**
* Setting up all shared vars and parameters.
*/
extra.apply {
// NOTE: optional suffix is prefixed with `-` (because of how LogisimVersion class parses it), which
// I remove here because `jpackage` tool does not like it when used to build the RPM package.
// Do NOT use `project.version` instead.
val appVersion = (project.version as String).replace("-", "")
set(APP_VERSION, appVersion)
logger.info("appVersion: ${appVersion}")
val appUrl = findProperty("url")
set(APP_URL, appUrl)
logger.info("appUrl: ${appUrl}")
// Short (with suffix removed) version string, i.e. for "3.6.0beta1", short form is "3.6.0".
// This is used by createApp and createMsi as version numbering is pretty strict on macOS and Windows.
// Do NOT use `project.version` instead.
val appVersionShort = (project.version as String).split('-')[0]
set(APP_VERSION_SHORT, appVersionShort)
logger.info("appVersionShort: ${appVersionShort}")
// Architecture used for build
val osArch = providers.systemProperty("os.arch").get()
set(OS_ARCH, osArch)
// Build Directory
val buildDir = getLayout().getBuildDirectory().get().asFile.toString()
set(BUILD_DIR, buildDir)
// Destination folder where packages are stored.
val targetDir="${buildDir}/dist"
set(TARGET_DIR, targetDir)
// JAR folder.
val libsDir="${buildDir}/libs"
set(LIBS_DIR, libsDir)
// PackageInput folder that hold the shadowJar
val packageInputDir="${buildDir}/packageInput"
set(PACKAGE_INPUT_DIR, packageInputDir)
// The root dir for jpackage extra files.
val supportDir="${projectDir}/support/jpackage"
set(SUPPORT_DIR, supportDir)
// Project name with uppercase first letter
val uppercaseProjectName = "Logisim Revolution"
set(UPPERCASE_PROJECT_NAME, uppercaseProjectName)
// Base name of produced artifacts. Suffixes will be added later by relevant tasks.
val baseFilename = "${project.name}-${appVersion}"
set(TARGET_FILE_PATH_BASE, "${targetDir}/${baseFilename}")
logger.debug("targetFilePathBase: \"${targetDir}/${baseFilename}\"")
val baseFilenameShort = "${project.name}-${appVersionShort}"
set(TARGET_FILE_PATH_BASE_SHORT, "${targetDir}/${baseFilenameShort}")
logger.debug("targetFilePathBaseShort: \"${targetDir}/${baseFilenameShort}\"")
// Name of application shadowJar file.
val shadowJarFilename = "${baseFilename}-all.jar"
set(SHADOW_JAR_FILE_NAME, shadowJarFilename)
logger.debug("shadowJarFilename: \"${shadowJarFilename}\"")
// JDK/jpackage vars
val javaHome = providers.systemProperty("java.home").get()
val jpackage = "${javaHome}/bin/jpackage"
set(JPACKAGE, jpackage)
val jdeps = "${javaHome}/bin/jdeps"
set(JDEPS, jdeps)
val jdepsFile = "${buildDir}/neededJavaModules.txt"
set(JDEPS_FILE, jdepsFile)
// Copyrights note.
val copyrights = "Copyright ©2001–${SimpleDateFormat("yyyy").format(Date())} Logisim-evolution and Logisim Revolution contributors"
// Platform-agnostic jpackage parameters shared across all the builds.
var params = listOf(
jpackage,
// NOTE: we cannot use --app-version as part of platform agnostic set as i.e. both macOS and
// Windows packages do not allow use of any suffixes like "-dev" etc, so --app-version is set
// in these builders separately.
"--input", packageInputDir,
"--main-class", "com.cburch.logisim.Main",
"--main-jar", shadowJarFilename,
"--java-options", "--enable-native-access=ALL-UNNAMED",
"--copyright", copyrights,
"--description", "Digital logic design tool and simulator",
"--vendor", "Logisim Revolution contributors",
)
if (logger.isDebugEnabled()) {
params += listOf("--verbose")
}
set(SHARED_PARAMS, params)
// Linux (DEB/RPM) specific settings for jpackage.
val linuxParams = params + listOf(
"--name", project.name,
"--dest", targetDir,
"--app-version", appVersion,
"--file-associations", "${supportDir}/linux/file.jpackage",
"--icon", "${supportDir}/linux/logisim-revolution-icon-128.png",
"--install-dir", "/opt",
"--linux-shortcut"
)
set(LINUX_PARAMS, linuxParams)
// All the macOS specific stuff.
set(APP_DIR_NAME, "${buildDir}/macOS-${osArch}/${uppercaseProjectName}.app")
}
val generatedDocumentationResources =
layout.buildDirectory.dir("generated/documentation-resources")
java {
sourceSets["main"].java {
val buildDir = getLayout().getBuildDirectory().get().asFile
srcDir("${buildDir}/generated/logisim/java")
srcDir("${buildDir}/generated/sources/srcgen")
}
sourceSets["main"].resources.srcDir(generatedDocumentationResources)
}
val docgen = sourceSets.create("docgen") {
java.srcDir("src/docgen/java")
}
dependencies {
testImplementation(docgen.output)
}
tasks.register<JavaExec>("generateDocumentationPrototype") {
group = "documentation"
description = "Generates the prototype English and German JavaHelp memory trees."
dependsOn(docgen.classesTaskName)
val manifest = layout.projectDirectory.file("src/main/doc/guide-memory.xml")
val germanOverlay = layout.projectDirectory.file("src/main/doc/locales/de-guide-memory.xml")
val docRoot = layout.projectDirectory.dir("src/main/resources/doc")
val outputRoot = layout.buildDirectory.dir("generated/documentation-prototype")
classpath = docgen.runtimeClasspath
mainClass.set("com.cburch.logisim.docs.DocumentationGenerator")
args(
manifest.asFile.absolutePath,
docRoot.asFile.absolutePath,
outputRoot.get().asFile.absolutePath,
germanOverlay.asFile.absolutePath,
)
inputs.files(manifest, germanOverlay)
inputs.dir(docRoot)
outputs.dir(outputRoot)
}
val generateHelpSets = tasks.register<JavaExec>("generateHelpSets") {
group = "documentation"
description = "Generates the JavaHelp HelpSet descriptors packaged by the application."
dependsOn(docgen.classesTaskName)
val metadata = layout.projectDirectory.file("src/main/doc/help-sets.xml")
val docRoot = layout.projectDirectory.dir("src/main/resources/doc")
val outputRoot = generatedDocumentationResources.map { it.dir("doc") }
classpath = docgen.runtimeClasspath
mainClass.set("com.cburch.logisim.docs.DocumentationGenerator")
args(
"--help-sets",
metadata.asFile.absolutePath,
docRoot.asFile.absolutePath,
outputRoot.get().asFile.absolutePath,
)
inputs.file(metadata)
inputs.dir(docRoot)
outputs.dir(outputRoot)
doFirst {
val directory = outputRoot.get().asFile
if (directory.exists() && !directory.deleteRecursively()) {
error("Could not clear generated HelpSet output directory: ${directory}")
}
}
}
tasks.named("processResources") {
dependsOn(generateHelpSets)
}
tasks.register<Jar>("sourcesJar") {
group = "build"
description = "Creates a JAR archive with project sources."
dependsOn.add("classes")
archiveClassifier.set("src")
from(sourceSets.main.get().allSource)
archiveVersion.set(ext.get(APP_VERSION) as String)
}
object func {
val logger: Logger = Logging.getLogger("BuildUtils")
/**
* Helper method that simplifies running external commands using ProcessBuilder().
* Will throw GradleException on command failure (non-zero return code).
*
* params: List of strings which signifies the external program file to be invoked and its arguments (if any).
* exMsg: Optional error message to be used with thrown exception on failure.
*
* Returns content of invoked app's stdout
*/
fun runCommand(params: List<String>, exceptionMsg: String): String {
val procBuilder = ProcessBuilder()
procBuilder
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.command(params)
val proc = procBuilder.start()
logger.debug("EXECUTING CMD: " + params.joinToString(" "))
var rc = -1
try {
rc = proc.waitFor()
logger.debug("CMD COMPLETED. RC: ${rc}")
} catch (ex: Exception) {
logger.error(ex.message)
logger.error(ex.stackTraceToString())
}
if (rc != 0) {
logger.error(proc.errorStream.bufferedReader().readText().trim())
logger.error("Command \"${params[0]}\" failed with RC ${rc}.")
throw GradleException(exceptionMsg)
}
return proc.inputStream.bufferedReader().readText().trim()
}
/** Helper function to remove all contents from the given directory */
fun deleteDirectoryContents(directory: String) {
val dir = File(directory)
if (!dir.exists() && !dir.mkdirs()) {
throw GradleException("Cannot create ${directory}")
}
if (!dir.isDirectory) {
throw GradleException("Cannot remove contents of ${directory}")
}
val dirList = File(directory).list()
if (dirList == null) return
for (file in dirList) {
val filename = "${directory}/$file"
val theFile = File(filename)
if (theFile.isDirectory()) {
deleteDirectoryContents(filename)
}
if (!theFile.delete()) {
throw GradleException("Could not delete ${filename}")
}
}
}
/** Helper function to copy a file from a source location to a destination */
fun copyFile(from: String, to:String) {
try {
Files.copy(Paths.get(from), Paths.get(to), StandardCopyOption.REPLACE_EXISTING)
} catch (ex: Exception) {
logger.error(ex.message)
throw GradleException("Failed to copy file from ${from} to ${to}")
}
}
/**
* Helper function to verify the distribution file now exists in build/dist.
* It fails if the expected package is missing, and lists the actual directory contents.
*/
fun verifyFileExists(filename: String) {
val theFile = File(filename)
if (theFile.isFile()) {
return
}
logger.error("File does not exist: ${filename}")
val parentDir = theFile.getParentFile();
if (parentDir != null && parentDir.isDirectory()) {
logger.error("Directory actually contains:")
val dirList = parentDir.list()
for (file in dirList.orEmpty()) {
logger.error(" ${file}")
}
} else {
logger.error("Parent directory does not exist: ${parentDir}")
}
throw GradleException("Expected package was not created: ${filename}")
}
/**
* Function that returns the named parameters list plus the --adds-modules option
*/
fun getNeededModules(fileName: String): List<String> {
val file = File(fileName)
if (!file.isFile()) {
throw GradleException("No ${fileName} exists")
}
val dependencies = File(fileName).readLines()[0]
return listOf("--add-modules", dependencies)
// return (ext.get(parametersName) as List<Any?>).filterIsInstance<String>() + addModules
}
/** Matches a bare package name that carries the Debian 64-bit `time_t` suffix. */
private val T64_PACKAGE_NAME = Regex("""[a-z0-9][a-z0-9+.-]*t64""")
/**
* Rewrites the `Depends` field of the given DEB package so that every `t64` package name is
* offered together with its pre-transition name, i.e. `libasound2t64 | libasound2`.
*
* Debian's 64-bit `time_t` transition renamed a lot of library packages by appending a `t64`
* suffix. `jpackage` reads the dependency names off the build machine, so a package built on a
* recent Ubuntu lists the new names only and refuses to install on Debian 12 or Ubuntu 22.04.
* On the architectures we ship (amd64, arm64) `time_t` was 64 bits all along, so both packages
* expose the very same ABI and either one satisfies us. `apt` honours the order of the
* alternatives, therefore recent systems keep using the `t64` package.
*
* See https://github.com/logisim-evolution/logisim-evolution/issues/2959
*/
fun addLegacyDebDependencyAliases(debFile: String, workDir: String) {
val root = File(workDir)
root.deleteRecursively()
root.mkdirs()
// `--raw-extract` unpacks the payload and drops the control files into `DEBIAN/`,
// which is exactly the layout `--build` expects back.
runCommand(listOf("dpkg-deb", "--raw-extract", debFile, workDir),
"Error while unpacking the DEB package.")
val controlFile = File("${workDir}/DEBIAN/control")
val lines = controlFile.readLines()
val patched = mutableListOf<String>()
var aliased = false
var idx = 0
while (idx < lines.size) {
val line = lines[idx]
idx++
if (!line.startsWith("Depends:")) {
patched.add(line)
continue
}
// Control fields may be folded over continuation lines.
val value = StringBuilder(line.removePrefix("Depends:"))
while (idx < lines.size && (lines[idx].startsWith(" ") || lines[idx].startsWith("\t"))) {
value.append(" ").append(lines[idx].trim())
idx++
}
val deps = value.toString().split(",").map { it.trim() }.filter { it.isNotEmpty() }
patched.add("Depends: " + deps.joinToString(", ") { dep ->
if (!T64_PACKAGE_NAME.matches(dep)) dep else {
aliased = true
"${dep} | ${dep.dropLast(3)}"
}
})
}
if (!aliased) {
logger.lifecycle("No t64 dependencies found in ${debFile}. Package left untouched.")
root.deleteRecursively()
return
}
controlFile.writeText(patched.joinToString("\n") + "\n")
// `--root-owner-group` is a must, as the build user owns the unpacked files but the package
// has to hand them over to root, just like the one `jpackage` produced.
runCommand(listOf("dpkg-deb", "--root-owner-group", "--build", workDir, debFile),
"Error while repacking the DEB package.")
root.deleteRecursively()
}
}
/**
* Patches the start‑scripts of Windows
*/
tasks.withType<CreateStartScripts>().configureEach {
doLast {
windowsScript.writeText(
windowsScript.readText().replace(
Regex("""set CLASSPATH=%APP_HOME%\\lib\\.*""", RegexOption.IGNORE_CASE),
"""set CLASSPATH=%APP_HOME%\\lib\\*"""
)
)
}
}
/**
* Task createNeededJavaModules
*
* Uses jdeps to create a file containing a list of the needed Java modules.
*/
tasks.register("createNeededJavaModules") {
group = "build"
description = "Creates a file containing the jdeps dependencies"
dependsOn("shadowJar")
// AWT loads accessibility providers from user configuration at runtime, so jdeps cannot detect
// this optional module through static analysis.
val additionalJavaModules = listOf("jdk.accessibility")
val libsDir = ext.get(LIBS_DIR) as String
val shadowJarFilename = ext.get(SHADOW_JAR_FILE_NAME) as String
val jarFileName = "${libsDir}/${shadowJarFilename}"
val outFileName = ext.get(JDEPS_FILE) as String
val cmd = listOf(ext.get(JDEPS) as String, "--multi-release", "base","--print-module-deps", "--ignore-missing-deps", jarFileName)
inputs.file(jarFileName)
inputs.property("additionalJavaModules", additionalJavaModules)
outputs.file(outFileName)
doLast {
val neededJavaModules =
(func.runCommand(cmd, "Error while finding Java dependencies with jdeps.").split(',') +
additionalJavaModules)
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.sorted()
.joinToString(",")
File(outFileName).writeText(neededJavaModules)
func.verifyFileExists(outFileName)
}
}
/**
* Task createPackageInput
*
* Creates a packageInput directory containing only the current shadowJar file
* because jpackage includes everything in its input directory in the package.
*/
tasks.register("createPackageInput") {
group = "build"
description = "Creates a packageInput directory that only contains the current shadowJar file"
dependsOn("shadowJar")
val libsDir = ext.get(LIBS_DIR) as String
val shadowJarFilename = ext.get(SHADOW_JAR_FILE_NAME) as String
val packageInputDir = ext.get(PACKAGE_INPUT_DIR) as String
inputs.file("${libsDir}/${shadowJarFilename}")
outputs.dir(packageInputDir)
doLast {
func.deleteDirectoryContents(packageInputDir)
func.copyFile("${libsDir}/${shadowJarFilename}", "${packageInputDir}/${shadowJarFilename}")
}
}
/**
* Task: createDeb
*
* Creates the Linux DEB package file (Debian, Ubuntu and derrivatives).
*/
tasks.register("createDeb") {
group = "build"
description = "Makes DEB Linux installation package."
dependsOn("createPackageInput", "createNeededJavaModules")
// Debian uses `_` to separate name from version string.
// https://www.debian.org/doc/manuals/debian-faq/pkg-basics.en.html
val appVersion = ext.get(APP_VERSION) as String
val targetDir = ext.get(TARGET_DIR) as String
// Map system architecture to Debian package architecture naming convention
val systemArch = (ext.get(OS_ARCH) as String).lowercase()
val debArch = when (systemArch) {
"x86_64", "amd64" -> "amd64"
"aarch64", "arm64" -> "arm64"
else -> systemArch
}
val outputFile = "${targetDir}/${project.name}_${appVersion}_${debArch}.deb"
val linuxParams = (ext.get(LINUX_PARAMS) as List<Any?>).filterIsInstance<String>()
val jdepsFile = ext.get(JDEPS_FILE) as String
val repackDir = "${ext.get(BUILD_DIR) as String}/debRepack"
inputs.dir(ext.get(PACKAGE_INPUT_DIR) as String)
inputs.dir("${ext.get(SUPPORT_DIR) as String}/linux")
inputs.file(jdepsFile)
outputs.file(outputFile)
doFirst {
if (!OperatingSystem.current().isLinux) {
throw GradleException("This task runs on Linux only.")
}
}
doLast {
val params = linuxParams + func.getNeededModules(jdepsFile) + listOf("--type", "deb")
func.runCommand(params, "Error while creating the DEB package.")
func.verifyFileExists(outputFile);
func.addLegacyDebDependencyAliases(outputFile, repackDir)
}
}
/**
* Task: createRpm
*
* Creates the Linux RPM package file (RedHat and derrivatives).
*/
tasks.register("createRpm") {
group = "build"
description = "Makes RPM Linux installation package."
dependsOn("createPackageInput", "createNeededJavaModules")
// Map system architecture to RPM package architecture naming convention
val systemArch = (ext.get(OS_ARCH) as String).lowercase()
val rpmArch = when (systemArch) {
"x86_64", "amd64" -> "x86_64"
"aarch64", "arm64" -> "aarch64"
else -> systemArch
}
val outputFile = "${ext.get(TARGET_FILE_PATH_BASE) as String}-1.${rpmArch}.rpm"
val linuxParams = (ext.get(LINUX_PARAMS) as List<Any?>).filterIsInstance<String>()
val jdepsFile = ext.get(JDEPS_FILE) as String
inputs.dir(ext.get(PACKAGE_INPUT_DIR) as String)
inputs.dir("${ext.get(SUPPORT_DIR) as String}/linux")
inputs.file(jdepsFile)
outputs.file(outputFile);
doFirst {
if (!OperatingSystem.current().isLinux) {
throw GradleException("This task runs on Linux only.")
}
}
doLast {
val params = linuxParams + func.getNeededModules(jdepsFile) + listOf("--type", "rpm")
func.runCommand(params, "Error while creating the RPM package.")
func.verifyFileExists(outputFile);
}
}
/**
* Task: createMsi
*
* Creates MSI installer file for Microsoft Windows.
*/
tasks.register("createMsi") {
group = "build"
description = "Makes the Windows installation package."
dependsOn("createPackageInput", "createNeededJavaModules")
val supportDir = ext.get(SUPPORT_DIR) as String
val osArch = ext.get(OS_ARCH) as String
val projectName = project.name
val sharedParams = (ext.get(SHARED_PARAMS) as List<Any?>).filterIsInstance<String>()
val jdepsFile = ext.get(JDEPS_FILE) as String
val outputFile = "${ext.get(TARGET_FILE_PATH_BASE_SHORT) as String}-${osArch}.msi"
val targetDir = ext.get(TARGET_DIR) as String
val version = ext.get(APP_VERSION_SHORT) as String
inputs.dir(ext.get(PACKAGE_INPUT_DIR) as String)
inputs.dir("${supportDir}/windows")
inputs.file(jdepsFile)
outputs.file(outputFile);
doFirst {
if (!OperatingSystem.current().isWindows) {
throw GradleException("This task runs on Windows only.")
}
}
doLast {
val params = sharedParams + func.getNeededModules(jdepsFile) + listOf(
"--name", projectName,
"--dest", targetDir,
"--file-associations", "${supportDir}/windows/file.jpackage",
"--icon", "${supportDir}/windows/Logisim-Revolution.ico",
"--win-menu-group", projectName,
"--win-shortcut",
"--win-dir-chooser",
"--win-menu",
"--type", "msi",
// we MUST use short version form (without any suffix like "-dev", as it is not allowed in MSI package:
// https://docs.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
// NOTE: release.py validates these package names before publishing.
"--app-version", version,
)
func.runCommand(params, "Error while creating the MSI package.")
val fromFile = "${targetDir}/${projectName}-${version}.msi"
val toFile = "${targetDir}/${projectName}-${version}-${osArch}.msi"
func.copyFile(fromFile, toFile)
File(fromFile).delete()
func.verifyFileExists(outputFile);
}
}
/**
* Task: createExe
*
* Creates an executable for Windows.
*/
tasks.register("createExe") {
group = "build"
description = "Creates the executable for Windows"
dependsOn("createPackageInput", "createNeededJavaModules")
val supportDir = ext.get(SUPPORT_DIR) as String
val buildDir = ext.get(BUILD_DIR) as String
val osArch = ext.get(OS_ARCH) as String
val projectName = project.name
val dest = "${buildDir}/windows-${osArch}"
val version = ext.get(APP_VERSION_SHORT) as String
val sharedParams = (ext.get(SHARED_PARAMS) as List<Any?>).filterIsInstance<String>()
val jdepsFile = ext.get(JDEPS_FILE) as String
inputs.dir(ext.get(PACKAGE_INPUT_DIR) as String)
inputs.dir("${supportDir}/windows")
inputs.file(jdepsFile)
outputs.dir("$dest/$projectName")
doFirst {
if (!OperatingSystem.current().isWindows) {
throw GradleException("This task runs on Windows only.")
}
}
doLast {
func.deleteDirectoryContents(dest)
val params = sharedParams + func.getNeededModules(jdepsFile) + listOf(
"--name", projectName,
"--dest", dest,
"--icon", "${supportDir}/windows/Logisim-Revolution.ico",
"--type", "app-image",
// we MUST use short version form (without any suffix like "-dev", as it is not allowed in MSI package:
// https://docs.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
// NOTE: release.py validates these package names before publishing.
"--app-version", version,
)
func.runCommand(params, "Error while creating the Windows executable.")
func.verifyFileExists("${dest}/${projectName}/${projectName}.exe")
}
}
/**
* Task: createWindowsPortableZip
*
* Create a self-contained archive for Windows.
*/
tasks.register<Zip>("createWindowsPortableZip") {
group = "build"
description = "Makes the self-contained zip archive for Windows"
val inputFiles = tasks.getByName("createExe").outputs.files
dependsOn("createExe")
from(inputFiles)
val osArch = ext.get(OS_ARCH) as String
val version = ext.get(APP_VERSION) as String
val targetDir = ext.get(TARGET_DIR) as String
val projectName = project.name
archiveFileName = "${projectName}-${version}-windows-${osArch}.zip"
destinationDirectory.set(file(targetDir))
}
/**
* Task: createApp
*
* Creates macOS application.
*/
tasks.register("createApp") {
val supportDir = ext.get(SUPPORT_DIR) as String
val buildDir = ext.get(BUILD_DIR) as String
val arch = ext.get(OS_ARCH) as String
val dest = "${buildDir}/macOS-${arch}"
val sharedParams = (ext.get(SHARED_PARAMS) as List<Any?>).filterIsInstance<String>()
val jdepsFile = ext.get(JDEPS_FILE) as String
val appDirName = ext.get(APP_DIR_NAME) as String
val projectName = ext.get(UPPERCASE_PROJECT_NAME) as String
val appVersion = ext.get(APP_VERSION_SHORT) as String
group = "build"
description = "Makes the macOS application."
dependsOn("createPackageInput", "createNeededJavaModules")
inputs.dir(ext.get(PACKAGE_INPUT_DIR) as String)
inputs.dir("${supportDir}/macos")
inputs.file(jdepsFile)
outputs.dir(dest)
doFirst {
if (!OperatingSystem.current().isMacOsX) {
throw GradleException("This task runs on macOS only.")
}
}
doLast {
func.deleteDirectoryContents(dest)
val params = sharedParams + func.getNeededModules(jdepsFile) + listOf(
"--dest", dest,
"--name", projectName,
"--file-associations", "${supportDir}/macos/file.jpackage",
"--icon", "${supportDir}/macos/Logisim-Revolution.icns",
// app versioning is strictly checked for macOS. No suffix allowed for `app-image` type.
"--app-version", appVersion,
"--type", "app-image",
"--mac-app-category", "education"
)
func.runCommand(params, "Error while creating the .app directory.")
if ("x86_64".equals(arch)) {
val pListFilename = "${appDirName}/Contents/Info.plist"
val tempPList = "${dest}/Info.plist"
func.runCommand(listOf(
"awk",
"{print >\"${tempPList}\"};"
+ "/NSHighResolutionCapable/{"
+ "print \" <string>true</string>\" >\"${tempPList}\";"
+ "print \" <key>NSSupportsAutomaticGraphicsSwitching</key>\" >\"${tempPList}\""
+ "}",
pListFilename,
), "Error while patching Info.plist file.")
func.runCommand(listOf(
"mv", tempPList, pListFilename
), "Error while moving Info.plist into the .app directory.")
func.runCommand(listOf(
"codesign", "--force", "--sign", "-", appDirName
), "Error while executing: codesign")
}
}
}
/**
* Task: createDmg
*
* Creates macOS DMG package file.
*/
tasks.register("createDmg") {
group = "build"
description = "Makes the macOS DMG package."
dependsOn("createApp")
val appDirName = ext.get(APP_DIR_NAME) as String
val osArch = ext.get(OS_ARCH) as String
val projectName = project.name
val jPackage = ext.get(JPACKAGE) as String
val appVersion = ext.get(APP_VERSION_SHORT) as String
val targetDir = ext.get(TARGET_DIR) as String
val outputFile = "${ext.get(TARGET_FILE_PATH_BASE) as String}-${osArch}.dmg"
inputs.dir(appDirName)
outputs.file(outputFile);
doFirst {
if (!OperatingSystem.current().isMacOsX) {
throw GradleException("This task runs on macOS only.")
}
}
doLast {
val params = listOf(
jPackage,
"--app-image", appDirName,
"--name", projectName,
// app versioning is strictly checked for macOS. No suffix allowed for `app-image` type.
"--app-version", appVersion,
"--dest", targetDir,
"--type", "dmg",
)
func.runCommand(params, "Error while creating the DMG package")
val fromFile = "${targetDir}/${projectName}-${appVersion}.dmg"
val toFile = "${outputFile}"
func.copyFile(fromFile, toFile)
File(fromFile).delete()
func.verifyFileExists(outputFile);
}
}
/**
* Task: genBuildInfo
*
* Generates Java class file with project information like current version, branch name, last commit hash etc.
*/
tasks.register("genBuildInfo") {
// Target location for generated files.
val buildDir = ext.get(BUILD_DIR) as String
val buildInfoDir = "${buildDir}/generated/logisim/java/com/cburch/logisim/generated"
val projectDir = project.projectDir.path as String
group = "build"
description = "Creates Java class file with vital project information."
inputs.dir("${projectDir}/src")
inputs.dir(ext.get(SUPPORT_DIR) as String)
inputs.files("${projectDir}/gradle.properties", "${projectDir}/README.md", "${projectDir}/LICENSE.md")
inputs.file("${projectDir}/build.gradle.kts")
// Source files need not change when HEAD/branch changes (for example after a commit).
val gitRevision = if (File("${projectDir}/.git").exists()) providers.exec {
commandLine("git", "-C", projectDir, "rev-parse", "HEAD")
}.standardOutput.asText.map { it.trim() } else providers.provider { "source-archive" }
val gitBranch = if (File("${projectDir}/.git").exists()) providers.exec {
commandLine("git", "-C", projectDir, "rev-parse", "--abbrev-ref", "HEAD")
}.standardOutput.asText.map { it.trim() } else providers.provider { "" }
inputs.property("gitRevision", gitRevision)
inputs.property("gitBranch", gitBranch)
outputs.dir(buildInfoDir)
val buildInfoFilePath = "${buildInfoDir}/BuildInfo.java"
val appVersion = ext.get(APP_VERSION) as String
val projectName = ext.get(UPPERCASE_PROJECT_NAME) as String
val displayName = "${projectName} v${appVersion}"
val url = ext.get(APP_URL) as String
doLast {
val now = Date()
val nowIso = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(now)
var branchName = ""
var branchLastCommitHash = "";
var buildId = "(Not built from Git repo)";
if (File("${projectDir}/.git").exists()) {
var errMsg = "Failed getting branch name."
branchName = func.runCommand(listOf("git", "-C", projectDir, "rev-parse", "--abbrev-ref", "HEAD"), errMsg)
errMsg = "Failed getting last commit hash."
branchLastCommitHash = func.runCommand(listOf("git", "-C", projectDir, "rev-parse", "--short=8", "HEAD"), errMsg)
buildId = "${branchName}/${branchLastCommitHash}"
}
val currentMillis = Date().time
val buildYear = SimpleDateFormat("yyyy").format(now)
val buildInfoClass = """
// ************************************************************************
// THIS IS A COMPILE TIME GENERATED FILE! DO NOT EDIT BY HAND!
// Generated at ${nowIso}
// ************************************************************************
package com.cburch.logisim.generated;
import com.cburch.logisim.LogisimVersion;
import java.util.Date;
public final class BuildInfo {
// Build time VCS details
public static final String branchName = "${branchName}";
public static final String branchLastCommitHash = "${branchLastCommitHash}";
public static final String buildId = "${buildId}";
// Project build timestamp
public static final long millis = ${currentMillis}L; // keep trailing 'L'
public static final String year = "${buildYear}";
public static final String dateIso8601 = "${nowIso}";
public static final Date date = new Date();
static { date.setTime(millis); }
// Project version
public static final LogisimVersion version = LogisimVersion.fromString("${appVersion}");
public static final String name = "${projectName}";
public static final String displayName = "${displayName}";
public static final String url = "${url}";
// JRE info
public static final String jvm_version =
String.format("%s v%s", System.getProperty("java.vm.name"), System.getProperty("java.version"));
public static final String jvm_vendor = System.getProperty("java.vendor");
}
// End of generated BuildInfo
"""
logger.info("Generating: ${buildInfoFilePath}")
val buildInfoFile = File(buildInfoFilePath)
buildInfoFile.parentFile.mkdirs()
buildInfoFile.writeText(buildInfoClass.trimIndent())
}
}
/**
* Task: genFiles
*
* Umbrella task to generate all generated files
*/
tasks.register("genFiles") {
group = "build"
description = "Generates all generated files."
dependsOn("genBuildInfo")
}
/**
* Task: createAll
*
* Umbrella task to create all packages for the current platform.
*/
tasks.register("createAll") {
group = "build"
description = "Makes the platform specific packages for the current platform."
if (OperatingSystem.current().isLinux) {
dependsOn("createDeb", "createRpm")
}
if (OperatingSystem.current().isWindows) {
dependsOn("createMsi")
dependsOn("createWindowsPortableZip")
}
if (OperatingSystem.current().isMacOsX) {
dependsOn("createDmg")
}
}
/**
* @deprecated. Use `createAll()`
*/
tasks.register("jpackage") {
group = "build"
var desc = "DEPRECATED: Use `createAll` task instead."
description = desc
dependsOn("createAll")
doFirst {
logger.warn(desc)
}
}
val compilerOptions = listOf("-Xlint:deprecation", "-Xlint:unchecked")
tasks {
compileJava {
options.encoding = "UTF-8"
options.compilerArgs = compilerOptions
dependsOn("genFiles")
}
compileTestJava {
options.encoding = "UTF-8"
options.compilerArgs = compilerOptions
dependsOn("genFiles")
}
test {
useJUnitPlatform()
jvmArgs("--enable-native-access=ALL-UNNAMED")
doFirst {
val isolated = Files.createTempDirectory("logisim-tests-").toFile()
systemProperty("user.home", File(isolated, "home").apply { mkdirs() }.path)
systemProperty("java.util.prefs.userRoot", File(isolated, "prefs").apply { mkdirs() }.path)
systemProperty("java.util.prefs.systemRoot", File(isolated, "system-prefs").apply { mkdirs() }.path)
}
// testLogging {
// events("passed", "skipped", "failed")
// }
}
jar {
manifest {
attributes.putAll(mapOf(
"Implementation-Title" to name,
"Implementation-Version" to archiveVersion
))
}
from(".") {
include("LICENSE.md")
include("README.md")
include("CHANGES.md")
}
}
shadowJar {
archiveBaseName.set(project.name)
archiveVersion.set(ext.get(APP_VERSION) as String)
from(".") {
include("LICENSE.md")
include("README.md")
include("CHANGES.md")
}
}
// Checkstyles related tasks: "checkstylMain" and "checkstyleTest"
checkstyle {
// Checkstyle version to use. Should match version in checkstyle workflow.
toolVersion = "13.9.0"
// let's use google_checks.xml config provided with Checkstyle.
// https://stackoverflow.com/a/67513272/1235698
val archive = configurations.checkstyle.get().resolve().filter {
it.name.startsWith("checkstyle")
}
config = resources.text.fromArchiveEntry(archive, "google_checks.xml")
configProperties["org.checkstyle.google.suppressionfilter.config"] =
"$projectDir/checkstyle-suppressions.xml"
// FIXME: There should be cleaner way of using custom suppression config with built-in style.
// https://stackoverflow.com/a/64703619/1235698
System.setProperty( "org.checkstyle.google.suppressionfilter.config", "${projectDir}/checkstyle-suppressions.xml")
}
checkstyleMain {
source = fileTree("src/main/java")
}
checkstyleTest {
source = fileTree("src/test/java")
}
}
/**
* Renders the main window to a PNG for reviewing the interface.
*
* Development only: the tool lives in the test sources and never ships.
* Usage: ./gradlew uiSnapshot -Ptheme=dark -Pout=build/ui-dark.png
*/
tasks.register<JavaExec>("uiSnapshot") {
group = "verification"
description = "Renders the main window to a PNG."
classpath = sourceSets["test"].runtimeClasspath
mainClass.set("com.cburch.logisim.gui.UiSnapshot")
systemProperty("snapshot.theme", project.findProperty("theme") ?: "dark")
systemProperty("snapshot.out", project.findProperty("out") ?: "build/ui-snapshot.png")
systemProperty("snapshot.width", project.findProperty("width") ?: "1440")
systemProperty("snapshot.height", project.findProperty("height") ?: "900")
systemProperty("snapshot.scale", project.findProperty("scale") ?: "1.0")
systemProperty("snapshot.welcome", project.findProperty("welcome") ?: "false")
systemProperty("snapshot.preferences", project.findProperty("preferences") ?: "false")
systemProperty("snapshot.page", project.findProperty("page") ?: "-1")
systemProperty("snapshot.file", project.findProperty("file") ?: "")
systemProperty("snapshot.print", project.findProperty("print") ?: "false")
systemProperty("snapshot.select", project.findProperty("select") ?: "")
systemProperty("snapshot.canvasZoom", project.findProperty("canvasZoom") ?: "0")
systemProperty("snapshot.side", project.findProperty("side") ?: "")
}
tasks.register<JavaExec>("optionsProbe") {
group = "verification"
classpath = sourceSets["test"].runtimeClasspath
mainClass.set("com.cburch.logisim.gui.OptionsProbe")
args(project.findProperty("file")?.toString() ?: "")
}
tasks.register<JavaExec>("scaleProbe") {
group = "verification"
classpath = sourceSets["test"].runtimeClasspath
mainClass.set("com.cburch.logisim.gui.ScaleProbe")
}
tasks.register<JavaExec>("themeProbe") {
group = "verification"
classpath = sourceSets["test"].runtimeClasspath
mainClass.set("com.cburch.logisim.gui.theme.ThemeProbe")
args(project.findProperty("theme")?.toString() ?: "light")
}
tasks.register<JavaExec>("splashSnapshot") {
group = "verification"
description = "Renders the loading splash to a PNG."
classpath = sourceSets["test"].runtimeClasspath
mainClass.set("com.cburch.logisim.gui.start.SplashSnapshot")
systemProperty("snapshot.theme", project.findProperty("theme") ?: "dark")
systemProperty("snapshot.scale", project.findProperty("scale") ?: "1.0")
systemProperty("snapshot.stage", project.findProperty("stage") ?: "progressLibraries")
systemProperty("snapshot.out", project.findProperty("out") ?: "build/splash-snapshot.png")
}
// Development probes must never read/write the developer's real settings or recovery files.
tasks.withType<JavaExec>().configureEach {
if (name in setOf("uiSnapshot", "splashSnapshot", "optionsProbe", "scaleProbe", "themeProbe")) {
doFirst {
val isolated = Files.createTempDirectory("logisim-${name}-").toFile()
systemProperty("user.home", File(isolated, "home").apply { mkdirs() }.path)
systemProperty("java.util.prefs.userRoot", File(isolated, "prefs").apply { mkdirs() }.path)
systemProperty("java.util.prefs.systemRoot", File(isolated, "system-prefs").apply { mkdirs() }.path)
}
}
}