com.palantir.baseline.plugins.BaselineModuleJvmArgs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-baseline-java Show documentation
Show all versions of gradle-baseline-java Show documentation
A Gradle plugin for applying Baseline-recommended build and IDE settings
/*
* (c) Copyright 2021 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.baseline.plugins;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.palantir.baseline.extensions.BaselineModuleJvmArgsExtension;
import com.palantir.baseline.plugins.javaversions.BaselineJavaVersion;
import com.palantir.baseline.plugins.javaversions.BaselineJavaVersionExtension;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.UnknownTaskException;
import org.gradle.api.file.FileCollection;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.api.tasks.javadoc.Javadoc;
import org.gradle.api.tasks.testing.Test;
import org.gradle.external.javadoc.CoreJavadocOptions;
import org.gradle.external.javadoc.MinimalJavadocOptions;
import org.gradle.jvm.tasks.Jar;
import org.gradle.process.CommandLineArgumentProvider;
import org.immutables.value.Value;
/**
* This plugin reuses the {@code Add-Exports} manifest entry defined in
* JEP-261 to propagate and collect required exports
* from transitive dependencies, and applies them to compilation (for annotation processors) and
* execution (tests, javaExec, etc) for runtime dependencies.
*/
public final class BaselineModuleJvmArgs implements Plugin {
private static final String EXTENSION_NAME = "moduleJvmArgs";
private static final String ENABLE_PREVIEW_ATTRIBUTE = "Baseline-Enable-Preview";
private static final String ADD_EXPORTS_ATTRIBUTE = "Add-Exports";
private static final String ADD_OPENS_ATTRIBUTE = "Add-Opens";
private static final Splitter ENTRY_SPLITTER =
Splitter.on(' ').trimResults().omitEmptyStrings();
@Override
@SuppressWarnings("checkstyle:MethodLength")
public void apply(Project project) {
project.getPluginManager().withPlugin("java", unused -> {
BaselineModuleJvmArgsExtension extension =
project.getExtensions().create(EXTENSION_NAME, BaselineModuleJvmArgsExtension.class, project);
// javac isn't provided `--add-exports` args for the time being due to
// https://github.com/gradle/gradle/issues/18824
// However, we set sourceCompatibility in BaselineJavaVersion to opt out of the '--release' flag.
project.getExtensions().getByType(SourceSetContainer.class).configureEach(sourceSet -> {
TaskProvider javaCompileProvider =
project.getTasks().named(sourceSet.getCompileJavaTaskName(), JavaCompile.class);
javaCompileProvider.configure(javaCompile -> {
javaCompile
.getOptions()
.getCompilerArgumentProviders()
// Use an anonymous class because tasks with lambda inputs cannot be cached
.add(new CommandLineArgumentProvider() {
@Override
public Iterable asArguments() {
// The '--release' flag is set when BaselineJavaVersion is not used.
if (!project.getPlugins().hasPlugin(BaselineJavaVersion.class)) {
project.getLogger()
.debug(
"BaselineModuleJvmArgs not applying args to compilation task "
+ "{} on {} due to lack of BaselineJavaVersion",
javaCompile.getName(),
project);
return ImmutableList.of();
}
ImmutableList arguments =
collectCompilationArgs(project, extension, sourceSet);
project.getLogger()
.debug(
"BaselineModuleJvmArgs compiling {} on {} with exports: {}",
javaCompile.getName(),
project,
arguments);
return arguments;
}
});
setTaskInputsFromExtension(javaCompile, extension);
});
TaskProvider javadocTaskProvider = null;
try {
javadocTaskProvider = project.getTasks().named(sourceSet.getJavadocTaskName());
} catch (UnknownTaskException e) {
// skip
}
if (javadocTaskProvider != null) {
javadocTaskProvider.configure(javadocTask -> {
javadocTask.doFirst(new Action() {
@Override
public void execute(Task task) {
// The '--release' flag is set when BaselineJavaVersion is not used.
if (!project.getPlugins().hasPlugin(BaselineJavaVersion.class)) {
project.getLogger()
.debug(
"BaselineModuleJvmArgs not applying args to compilation task "
+ "{} on {} due to lack of BaselineJavaVersion",
task.getName(),
project);
return;
}
Javadoc javadoc = (Javadoc) task;
MinimalJavadocOptions options = javadoc.getOptions();
if (options instanceof CoreJavadocOptions) {
CoreJavadocOptions coreOptions = (CoreJavadocOptions) options;
ImmutableList info =
collectClasspathInfo(project, sourceSet);
List exportValues = Stream.concat(
// Compilation only supports exports, so we union with opens.
Stream.concat(
extension.exports().get().stream(),
extension.opens().get().stream()),
info.stream()
.flatMap(item -> Stream.concat(
item.exports().stream(), item.opens().stream())))
.distinct()
.sorted()
.map(item -> item + "=ALL-UNNAMED")
.collect(ImmutableList.toImmutableList());
project.getLogger()
.debug(
"BaselineModuleJvmArgs building {} on {} " + "with exports: {}",
javadoc.getName(),
project,
exportValues);
if (!exportValues.isEmpty()) {
coreOptions
// options are automatically prefixed with '-' internally
.addMultilineStringsOption("-add-exports")
.setValue(exportValues);
}
} else {
project.getLogger()
.error(
"MinimalJavadocOptions implementation was "
+ "not CoreJavadocOptions, rather '{}'",
options.getClass().getName());
}
}
});
setTaskInputsFromExtension(javadocTask, extension);
});
}
});
project.getTasks().withType(Test.class).configureEach(new Action() {
@Override
public void execute(Test test) {
test.getJvmArgumentProviders().add(new CommandLineArgumentProvider() {
@Override
public Iterable asArguments() {
ImmutableList arguments =
collectClasspathArgs(project, extension, test.getClasspath(), OpensMode.RUNTIME);
project.getLogger()
.debug(
"BaselineModuleJvmArgs executing {} on {} with exports: {}",
test.getName(),
project,
arguments);
return arguments;
}
});
setTaskInputsFromExtension(test, extension);
}
});
project.getTasks().withType(JavaExec.class).configureEach(new Action() {
@Override
public void execute(JavaExec javaExec) {
javaExec.getJvmArgumentProviders().add(new CommandLineArgumentProvider() {
@Override
public Iterable asArguments() {
ImmutableList arguments = collectClasspathArgs(
project, extension, javaExec.getClasspath(), OpensMode.RUNTIME);
project.getLogger()
.debug(
"BaselineModuleJvmArgs executing {} on {} with exports: {}",
javaExec.getName(),
project,
arguments);
return arguments;
}
});
setTaskInputsFromExtension(javaExec, extension);
}
});
// Derive this plugin's `enablePreview` property from BaselineJavaVersion's extension
project.getPlugins().withType(BaselineJavaVersion.class, _unused -> {
BaselineJavaVersionExtension javaVersionsExtension =
project.getExtensions().getByType(BaselineJavaVersionExtension.class);
extension.setEnablePreview(javaVersionsExtension.runtime().map(chosenJavaVersion -> {
return chosenJavaVersion.enablePreview()
? Optional.of(chosenJavaVersion.javaLanguageVersion())
: Optional.empty();
}));
});
project.getTasks().withType(Jar.class).configureEach(new Action() {
@Override
public void execute(Jar jar) {
jar.doFirst(new Action() {
@Override
public void execute(Task task) {
jar.manifest(new Action() {
@Override
public void execute(Manifest manifest) {
addManifestAttribute(jar, manifest, ADD_EXPORTS_ATTRIBUTE, extension.exports());
addManifestAttribute(jar, manifest, ADD_OPENS_ATTRIBUTE, extension.opens());
addManifestAttribute(
jar,
manifest,
ENABLE_PREVIEW_ATTRIBUTE,
extension.getEnablePreview().map(maybeVersion -> maybeVersion.stream()
.map(v -> Integer.toString(v.asInt()))
.collect(Collectors.toSet())));
}
});
}
});
setTaskInputsFromExtension(jar, extension);
}
});
});
}
private static void setTaskInputsFromExtension(Task task, BaselineModuleJvmArgsExtension extension) {
task.getInputs().property("baseline-module-jvm-args-extension-exports", extension.exports());
task.getInputs().property("baseline-module-jvm-args-extension-opens", extension.opens());
task.getInputs().property("baseline-module-jvm-args-extension-enablePreview", extension.getEnablePreview());
}
private static void addManifestAttribute(
Jar jarTask, Manifest manifest, String attributeName, Provider> valueProperty) {
Project project = jarTask.getProject();
// Only locally defined values are applied to jars
Set values = valueProperty.get();
if (!values.isEmpty()) {
project.getLogger()
.debug(
"BaselineModuleJvmArgs adding {} attribute to {} in {}: {}",
attributeName,
jarTask.getName(),
project,
values);
manifest.attributes(ImmutableMap.of(attributeName, String.join(" ", values)));
} else {
project.getLogger()
.debug(
"BaselineModuleJvmArgs not adding {} attribute to {} in {}",
attributeName,
jarTask.getName(),
project);
}
}
private static ImmutableList collectCompilationArgs(
Project project, BaselineModuleJvmArgsExtension extension, SourceSet sourceSet) {
return collectClasspathArgs(
project,
extension,
project.getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName()),
OpensMode.COMPILATION);
}
private static ImmutableList collectClasspathArgs(
Project project, BaselineModuleJvmArgsExtension extension, FileCollection classpath, OpensMode mode) {
ImmutableList classpathInfo = collectClasspathInfo(project, classpath);
Stream allExports = Stream.concat(
extension.exports().get().stream(), classpathInfo.stream().flatMap(info -> info.exports().stream()));
Stream allOpens = Stream.concat(
extension.opens().get().stream(), classpathInfo.stream().flatMap(info -> info.opens().stream()));
switch (mode) {
case COMPILATION:
return Stream.concat(allExports, allOpens)
.distinct()
.sorted()
.flatMap(BaselineModuleJvmArgs::addExportArg)
.collect(ImmutableList.toImmutableList());
case RUNTIME:
Stream exports = allExports.distinct().sorted().flatMap(BaselineModuleJvmArgs::addExportArg);
Stream opens = allOpens.distinct().sorted().flatMap(BaselineModuleJvmArgs::addOpensArg);
return Stream.concat(exports, opens).collect(ImmutableList.toImmutableList());
}
throw new IllegalStateException("unknown mode: " + mode);
}
private static ImmutableList collectClasspathInfo(Project project, SourceSet sourceSet) {
return collectClasspathInfo(
project, project.getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName()));
}
private static ImmutableList collectClasspathInfo(
Project project, FileCollection classpath) {
return classpath.getFiles().stream()
.map(file -> {
try {
if (file.getName().endsWith(".jar") && file.isFile()) {
try (JarFile jar = new JarFile(file)) {
java.util.jar.Manifest maybeJarManifest = jar.getManifest();
Optional parsedModuleInfo = parseModuleInfo(maybeJarManifest);
project.getLogger()
.debug("Jar '{}' produced manifest info: {}", file, parsedModuleInfo);
return parsedModuleInfo.orElse(null);
}
}
return null;
} catch (IOException e) {
project.getLogger().warn("Failed to check jar {} for manifest attributes", file, e);
return null;
}
})
.filter(Objects::nonNull)
.collect(ImmutableList.toImmutableList());
}
private static Optional parseModuleInfo(@Nullable java.util.jar.Manifest jarManifest) {
return Optional.ofNullable(jarManifest)
.map(manifest -> JarManifestModuleInfo.builder()
.exports(readManifestAttribute(manifest, ADD_EXPORTS_ATTRIBUTE))
.opens(readManifestAttribute(manifest, ADD_OPENS_ATTRIBUTE))
.build())
.filter(JarManifestModuleInfo::isPresent);
}
private static List readManifestAttribute(java.util.jar.Manifest jarManifest, String attribute) {
return Optional.ofNullable(
Strings.emptyToNull(jarManifest.getMainAttributes().getValue(attribute)))
.map(ENTRY_SPLITTER::splitToList)
.orElseGet(ImmutableList::of);
}
private static Stream addExportArg(String modulePackagePair) {
return Stream.of("--add-exports", modulePackagePair + "=ALL-UNNAMED");
}
private static Stream addOpensArg(String modulePackagePair) {
return Stream.of("--add-opens", modulePackagePair + "=ALL-UNNAMED");
}
@Value.Immutable
interface JarManifestModuleInfo {
ImmutableList exports();
ImmutableList opens();
default boolean isEmpty() {
return exports().isEmpty() && opens().isEmpty();
}
default boolean isPresent() {
return !isEmpty();
}
static Builder builder() {
return new Builder();
}
class Builder extends ImmutableJarManifestModuleInfo.Builder {}
}
enum OpensMode {
COMPILATION,
RUNTIME;
}
}