add jpackage plugin

This commit is contained in:
Jörg Prante 2024-02-17 11:52:38 +01:00
parent d88bc058fa
commit 324d8498ec
6 changed files with 540 additions and 0 deletions

View file

@ -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'])
}
}
}
}

View file

@ -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<String> getApplicationName();
public abstract Property<String> getApplicationVersion();
public abstract Property<String> getApplicationDescription();
public abstract Property<String> getVendor();
public abstract Property<String> getCopyright();
public abstract DirectoryProperty getJpackageResources();
public abstract ConfigurableFileCollection getResources();
private final NamedDomainObjectContainer<JPackageTarget> 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<? super JPackageTarget> 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<JPackageTask> 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<Task> 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);
}
}

View file

@ -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<Project> {
@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")));
}
}

View file

@ -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<String> getOperatingSystem();
public abstract Property<String> getArchitecture();
public abstract ListProperty<String> getPackageTypes();
public abstract ListProperty<String> getOptions();
@Inject
public JPackageTarget(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View file

@ -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<JavaInstallationMetadata> getJavaInstallation();
@Input
public abstract Property<String> getOperatingSystem();
@Input
public abstract Property<String> getArchitecture();
@Input
public abstract Property<String> getMainModule();
@Input
public abstract Property<String> getVersion();
@Classpath
public abstract ConfigurableFileCollection getModulePath();
@Input
public abstract Property<String> getApplicationName();
@Input
@Optional
public abstract Property<String> getApplicationDescription();
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getJpackageResources();
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public abstract ConfigurableFileCollection getResources();
@Input
@Optional
public abstract Property<String> getVendor();
@Input
@Optional
public abstract Property<String> getCopyright();
@Input
public abstract ListProperty<String> getJavaOptions();
@Input
public abstract ListProperty<String> getOptions();
@Input
public abstract ListProperty<String> 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<File> 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);
}
}

View file

@ -49,5 +49,6 @@ include 'gradle-plugin-docker'
include 'gradle-plugin-git' include 'gradle-plugin-git'
include 'gradle-plugin-jacc' include 'gradle-plugin-jacc'
include 'gradle-plugin-jflex' include 'gradle-plugin-jflex'
include 'gradle-plugin-jpackage'
include 'gradle-plugin-rpm' include 'gradle-plugin-rpm'
include 'gradle-plugin-shadow' include 'gradle-plugin-shadow'