From 324d8498ec3cb80a2f75050b7ec0154fafd7b4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Sat, 17 Feb 2024 11:52:38 +0100 Subject: [PATCH] add jpackage plugin --- gradle-plugin-jpackage/build.gradle | 32 +++ .../plugin/jpackage/JPackageExtension.java | 204 +++++++++++++++ .../plugin/jpackage/JPackagePlugin.java | 42 ++++ .../plugin/jpackage/JPackageTarget.java | 27 ++ .../gradle/plugin/jpackage/JPackageTask.java | 234 ++++++++++++++++++ settings.gradle | 1 + 6 files changed, 540 insertions(+) create mode 100755 gradle-plugin-jpackage/build.gradle create mode 100644 gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageExtension.java create mode 100644 gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackagePlugin.java create mode 100644 gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTarget.java create mode 100644 gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTask.java diff --git a/gradle-plugin-jpackage/build.gradle b/gradle-plugin-jpackage/build.gradle new file mode 100755 index 0000000..2b28238 --- /dev/null +++ b/gradle-plugin-jpackage/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java-gradle-plugin' + alias(libs.plugins.publish) +} + +apply plugin: 'java-gradle-plugin' +apply plugin: 'com.gradle.plugin-publish' + +apply from: rootProject.file('gradle/compile/groovy.gradle') +apply from: rootProject.file('gradle/test/junit5.gradle') + +dependencies { + api gradleApi() + testImplementation gradleTestKit() +} + +if (project.hasProperty('gradle.publish.key')) { + gradlePlugin { + website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-jpackage' + vcsUrl = 'https://xbib.org/joerg/gradle-plugins' + plugins { + jaccPlugin { + id = 'org.xbib.gradle.plugin.jpackage' + implementationClass = 'org.xbib.gradle.plugin.jpackage.JPackagePlugin' + version = project.version + description = 'Gradle JPackage plugin' + displayName = 'Gradle JPackage plugin' + tags.set(['jpackage']) + } + } + } +} diff --git a/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageExtension.java b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageExtension.java new file mode 100644 index 0000000..93ce045 --- /dev/null +++ b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageExtension.java @@ -0,0 +1,204 @@ +package org.xbib.gradle.plugin.jpackage; + +import java.util.Arrays; +import java.util.Collections; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.NonNullApi; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.attributes.Attribute; +import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; +import org.gradle.api.attributes.java.TargetJvmEnvironment; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.ApplicationPlugin; +import org.gradle.api.plugins.JavaApplication; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.nativeplatform.MachineArchitecture; +import org.gradle.nativeplatform.OperatingSystemFamily; +import static org.gradle.language.base.plugins.LifecycleBasePlugin.ASSEMBLE_TASK_NAME; +import static org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP; +import static org.gradle.nativeplatform.MachineArchitecture.ARCHITECTURE_ATTRIBUTE; +import static org.gradle.nativeplatform.OperatingSystemFamily.LINUX; +import static org.gradle.nativeplatform.OperatingSystemFamily.MACOS; +import static org.gradle.nativeplatform.OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE; +import static org.gradle.nativeplatform.OperatingSystemFamily.WINDOWS; + +@NonNullApi +public abstract class JPackageExtension { + + public abstract Property getApplicationName(); + + public abstract Property getApplicationVersion(); + + public abstract Property getApplicationDescription(); + + public abstract Property getVendor(); + + public abstract Property getCopyright(); + + public abstract DirectoryProperty getJpackageResources(); + + public abstract ConfigurableFileCollection getResources(); + + private final NamedDomainObjectContainer targets = getObjects().domainObjectContainer(JPackageTarget.class); + + @Inject + protected abstract JavaToolchainService getJavaToolchains(); + + @Inject + protected abstract ObjectFactory getObjects(); + + @Inject + protected abstract Project getProject(); + + public JPackageTarget target(String label, Action action) { + JPackageTarget target; + if (targets.getNames().contains(label)) { + target = targets.getByName(label); + } else { + target = targets.create(label, this::newTarget); + } + action.execute(target); + return target; + } + + public JPackageTarget primaryTarget(JPackageTarget target) { + SourceSetContainer sourceSets = getProject().getExtensions().getByType(SourceSetContainer.class); + ConfigurationContainer configurations = getProject().getConfigurations(); + sourceSets.all(sourceSet -> { + configureTargetAttributes(configurations.getByName(sourceSet.getCompileClasspathConfigurationName()), target); + configureTargetAttributes(configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()), target); + configurations.matching(conf -> "mainRuntimeClasspath".equals(conf.getName())).all(conf -> configureTargetAttributes(conf, target)); + }); + + return target; + } + + private void newTarget(JPackageTarget target) { + target.getPackageTypes().convention(target.getOperatingSystem().map(os -> switch (os) { + case WINDOWS -> Arrays.asList("exe", "msi"); + case MACOS -> Arrays.asList("pkg", "dmg"); + case LINUX -> Arrays.asList("rpm", "deb"); + default -> Collections.emptyList(); + })); + ConfigurationContainer configurations = getProject().getConfigurations(); + SourceSetContainer sourceSets = getProject().getExtensions().getByType(SourceSetContainer.class); + sourceSets.all(sourceSet -> { + Configuration internal = maybeCreateInternalConfiguration(); + configurations.create(target.getName() + capitalize(sourceSet.getCompileClasspathConfigurationName()), c -> { + c.setCanBeConsumed(false); + c.setVisible(false); + configureJavaStandardAttributes(c, Usage.JAVA_API); + configureTargetAttributes(c, target); + c.extendsFrom( + configurations.getByName(sourceSet.getImplementationConfigurationName()), + configurations.getByName(sourceSet.getCompileOnlyConfigurationName()), + internal + ); + }); + Configuration runtimeClasspath = configurations.create(target.getName() + capitalize(sourceSet.getRuntimeClasspathConfigurationName()), c -> { + c.setCanBeConsumed(false); + c.setVisible(false); + configureJavaStandardAttributes(c, Usage.JAVA_RUNTIME); + configureTargetAttributes(c, target); + c.extendsFrom( + configurations.getByName(sourceSet.getImplementationConfigurationName()), + configurations.getByName(sourceSet.getRuntimeOnlyConfigurationName()), + internal + ); + }); + if (SourceSet.isMain(sourceSet)) { + getProject().getPlugins().withType(ApplicationPlugin.class, p -> registerTargetSpecificTasks(target, sourceSet.getJarTaskName(), runtimeClasspath)); + } + }); + } + + private void configureJavaStandardAttributes(Configuration resolvable, String usage) { + resolvable.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, getObjects().named(Usage.class, usage)); + resolvable.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, getObjects().named(Category.class, Category.LIBRARY)); + resolvable.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, getObjects().named(LibraryElements.class, LibraryElements.JAR)); + resolvable.getAttributes().attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, getObjects().named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); + resolvable.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling.class, Bundling.EXTERNAL)); + resolvable.getAttributes().attribute(Attribute.of("javaModule", Boolean.class), true); + } + + private void configureTargetAttributes(Configuration resolvable, JPackageTarget target) { + resolvable.getAttributes().attributeProvider(OPERATING_SYSTEM_ATTRIBUTE, target.getOperatingSystem().map(name -> getObjects().named(OperatingSystemFamily.class, name))); + resolvable.getAttributes().attributeProvider(ARCHITECTURE_ATTRIBUTE, target.getArchitecture().map(name -> getObjects().named(MachineArchitecture.class, name))); + } + + private void registerTargetSpecificTasks(JPackageTarget target, String applicationJarTask, Configuration runtimeClasspath) { + TaskContainer tasks = getProject().getTasks(); + JavaPluginExtension java = getProject().getExtensions().getByType(JavaPluginExtension.class); + JavaApplication application = getProject().getExtensions().getByType(JavaApplication.class); + TaskProvider jpackage = tasks.register("jpackage" + capitalize(target.getName()), JPackageTask.class, t -> { + t.getJavaInstallation().convention(getJavaToolchains().compilerFor(java.getToolchain()).get().getMetadata()); + t.getOperatingSystem().convention(target.getOperatingSystem()); + t.getArchitecture().convention(target.getArchitecture()); + t.getMainModule().convention(application.getMainModule()); + t.getVersion().convention(getApplicationVersion()); + t.getModulePath().from(tasks.named(applicationJarTask)); + t.getModulePath().from(runtimeClasspath); + t.getApplicationName().convention(getApplicationName()); + t.getJpackageResources().convention(getJpackageResources().dir(target.getOperatingSystem())); + t.getApplicationDescription().convention(getApplicationDescription()); + t.getVendor().convention(getVendor()); + t.getCopyright().convention(getCopyright()); + t.getJavaOptions().convention(application.getApplicationDefaultJvmArgs()); + t.getOptions().convention(target.getOptions()); + t.getPackageTypes().convention(target.getPackageTypes()); + t.getResources().from(getResources()); + t.getDestination().convention(getProject().getLayout().getBuildDirectory().dir("packages/" + target.getName())); + t.getTempDirectory().convention(getProject().getLayout().getBuildDirectory().dir("tmp/jpackage/" + target.getName())); + }); + tasks.register("run" + capitalize(target.getName()), JavaExec.class, t -> { + t.setGroup(ApplicationPlugin.APPLICATION_GROUP); + t.setDescription("Run this project as a JVM application on " + target.getName()); + t.getJavaLauncher().convention(getJavaToolchains().launcherFor(java.getToolchain())); + t.getMainModule().convention(application.getMainModule()); + t.getMainClass().convention(application.getMainClass()); + t.setJvmArgs(application.getApplicationDefaultJvmArgs()); + t.classpath(tasks.named("jar"), runtimeClasspath); + }); + String targetAssembleLifecycle = "assemble" + capitalize(target.getName()); + if (!tasks.getNames().contains(targetAssembleLifecycle)) { + TaskProvider lifecycleTask = tasks.register(targetAssembleLifecycle, t -> { + t.setGroup(BUILD_GROUP); + t.setDescription("Builds this project for " + target.getName()); + }); + tasks.named(ASSEMBLE_TASK_NAME, t -> t.dependsOn(lifecycleTask)); + } + tasks.named(targetAssembleLifecycle, t -> t.dependsOn(jpackage)); + } + + private Configuration maybeCreateInternalConfiguration() { + Configuration internal = getProject().getConfigurations().findByName("internal"); + if (internal != null) { + return internal; + } + return getProject().getConfigurations().create("internal", i -> { + i.setCanBeResolved(false); + i.setCanBeConsumed(false); + }); + } + + private String capitalize(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1); + } +} diff --git a/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackagePlugin.java b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackagePlugin.java new file mode 100644 index 0000000..8a6bd04 --- /dev/null +++ b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackagePlugin.java @@ -0,0 +1,42 @@ +/* + * Copyright the GradleX team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbib.gradle.plugin.jpackage; + +import org.gradle.api.NonNullApi; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.SourceSetContainer; + +@SuppressWarnings("unused") +@NonNullApi +public abstract class JPackagePlugin implements Plugin { + + @Override + public void apply(Project project) { + project.getPlugins().apply(JavaPlugin.class); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + SourceDirectorySet mainResources = sourceSets.getByName("main").getResources(); + JPackageExtension jPackageExtension = project.getExtensions().create("jpackage", JPackageExtension.class); + jPackageExtension.getApplicationName().convention(project.getName()); + jPackageExtension.getApplicationVersion().convention(project.provider(() -> + (String) project.getVersion())); + jPackageExtension.getJpackageResources().convention(project.provider(() -> + project.getLayout().getProjectDirectory().dir(mainResources.getSrcDirs().iterator().next().getParent() + "/resourcesPackage"))); + } +} diff --git a/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTarget.java b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTarget.java new file mode 100644 index 0000000..3ffe86d --- /dev/null +++ b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTarget.java @@ -0,0 +1,27 @@ +package org.xbib.gradle.plugin.jpackage; + +import javax.inject.Inject; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; + +public abstract class JPackageTarget { + + private final String name; + + public abstract Property getOperatingSystem(); + + public abstract Property getArchitecture(); + + public abstract ListProperty getPackageTypes(); + + public abstract ListProperty getOptions(); + + @Inject + public JPackageTarget(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTask.java b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTask.java new file mode 100644 index 0000000..cbb4ac0 --- /dev/null +++ b/gradle-plugin-jpackage/src/main/java/org/xbib/gradle/plugin/jpackage/JPackageTask.java @@ -0,0 +1,234 @@ +package org.xbib.gradle.plugin.jpackage; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import javax.inject.Inject; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.Directory; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.internal.file.FileOperations; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.gradle.jvm.toolchain.JavaInstallationMetadata; +import org.gradle.process.ExecOperations; +import static java.util.Objects.requireNonNull; +import static org.gradle.nativeplatform.OperatingSystemFamily.WINDOWS; + +@CacheableTask +public abstract class JPackageTask extends DefaultTask { + + @Nested + public abstract Property getJavaInstallation(); + + @Input + public abstract Property getOperatingSystem(); + + @Input + public abstract Property getArchitecture(); + + @Input + public abstract Property getMainModule(); + + @Input + public abstract Property getVersion(); + + @Classpath + public abstract ConfigurableFileCollection getModulePath(); + + @Input + public abstract Property getApplicationName(); + + @Input + @Optional + public abstract Property getApplicationDescription(); + + @InputDirectory + @PathSensitive(PathSensitivity.RELATIVE) + public abstract DirectoryProperty getJpackageResources(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getResources(); + + @Input + @Optional + public abstract Property getVendor(); + + @Input + @Optional + public abstract Property getCopyright(); + + @Input + public abstract ListProperty getJavaOptions(); + + @Input + public abstract ListProperty getOptions(); + + @Input + public abstract ListProperty getPackageTypes(); + + @OutputDirectory + public abstract DirectoryProperty getDestination(); + + @Internal + public abstract DirectoryProperty getTempDirectory(); + + @Inject + protected abstract FileOperations getFiles(); + + @Inject + protected abstract ExecOperations getExec(); + + @TaskAction + public void runJpackage() throws Exception { + getFiles().delete(getTempDirectory()); + getFiles().delete(getDestination()); + String os = getOperatingSystem().get(); + String arch = getArchitecture().get(); + String hostOs = System.getProperty("os.name").replace(" ", "").toLowerCase(); + String hostArch = System.getProperty("os.arch"); + validateHostSystem(arch, hostArch, os, hostOs); + Directory resourcesDir = getTempDirectory().get().dir("jpackage-resources"); + Directory appImageParent = getTempDirectory().get().dir("app-image"); + resourcesDir.getAsFile().mkdirs(); + getFiles().copy(c -> { + c.from(getJpackageResources()); + c.into(resourcesDir); + c.rename(f -> f.replace("icon", getApplicationName().get())); + }); + String executableName = WINDOWS.equals(os) ? "jpackage.exe" : "jpackage"; + String jpackage = getJavaInstallation().get().getInstallationPath().file("bin/" + executableName).getAsFile().getAbsolutePath(); + getExec().exec(e -> { + e.commandLine( + jpackage, + "--type", + "app-image", + "--module", + getMainModule().get(), + "--resource-dir", + resourcesDir.getAsFile().getPath(), + "--app-version", + getVersion().get(), + "--module-path", + getModulePath().getAsPath(), + "--name", + getApplicationName().get(), + "--dest", + appImageParent.getAsFile().getPath() + ); + if (getApplicationDescription().isPresent()) { + e.args("--description", getApplicationDescription().get()); + } + if (getVendor().isPresent()) { + e.args("--vendor", getVendor().get()); + } + if (getCopyright().isPresent()) { + e.args("--copyright", getCopyright().get()); + } + for (String javaOption : getJavaOptions().get()) { + e.args("--java-options", javaOption); + } + }); + File appImageFolder = requireNonNull(appImageParent.getAsFile().listFiles())[0]; + File appResourcesFolder; + if (os.contains("macos")) { + appResourcesFolder = new File(appImageFolder, "Contents/app"); + } else if (os.contains("windows")) { + appResourcesFolder = new File(appImageFolder, "app"); + } else { + appResourcesFolder = new File(appImageFolder, "lib/app"); + } + getFiles().copy(c -> { + c.from(getResources()); + c.into(appResourcesFolder); + }); + getPackageTypes().get().forEach(packageType -> + getExec().exec(e -> { + e.commandLine( + jpackage, + "--type", + packageType, + "--app-image", + appImageFolder.getPath(), + "--dest", + getDestination().get().getAsFile().getPath() + ); + for (String option : getOptions().get()) { + e.args(option); + } + }) + ); + generateChecksums(); + } + + private void generateChecksums() throws NoSuchAlgorithmException, IOException { + File destination = getDestination().get().getAsFile(); + List allFiles = Arrays.stream(requireNonNull(destination.listFiles())) + .filter(File::isFile) + .toList(); + for (File result : allFiles) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] encoded = digest.digest(Files.readAllBytes(result.toPath())); + Files.write(new File(destination, result.getName() + ".sha256").toPath(), bytesToHex(encoded).getBytes()); + } + } + + private String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + private void validateHostSystem(String arch, String hostArch, String os, String hostOs) { + if (os.contains("macos")) { + if (!hostOs.contains(os)) { + wrongHostSystemError(hostOs, os); + } + } else if (os.contains("windows")) { + if (!hostOs.contains(os)) { + wrongHostSystemError(hostOs, os); + } + } else { + if (hostOs.contains("windows") || hostOs.contains("macos")) { + wrongHostSystemError(hostOs, os); + } + } + if (arch.contains("64") && !hostArch.contains("64")) { + wrongHostSystemError(hostArch, arch); + } + if (arch.contains("aarch") && !hostArch.contains("aarch")) { + wrongHostSystemError(hostArch, arch); + } + if (!arch.contains("aarch") && hostArch.contains("aarch")) { + wrongHostSystemError(hostArch, arch); + } + } + + private void wrongHostSystemError(String hostOs, String os) { + throw new RuntimeException("Running on " + hostOs + "; cannot build for " + os); + } +} diff --git a/settings.gradle b/settings.gradle index 3ca56fd..1a01c1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -49,5 +49,6 @@ include 'gradle-plugin-docker' include 'gradle-plugin-git' include 'gradle-plugin-jacc' include 'gradle-plugin-jflex' +include 'gradle-plugin-jpackage' include 'gradle-plugin-rpm' include 'gradle-plugin-shadow'