Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to filter dependencies in multi-build mode #16600

Merged
merged 1 commit into from
Apr 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Optional;
import java.util.Set;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
Expand Down Expand Up @@ -56,6 +57,37 @@ public class PackageConfig {
@ConfigItem
public Optional<List<String>> userConfiguredIgnoredEntries;

/**
* List of all the dependencies that have been defined as optional to include into the final package of the application.
* Each optional dependency needs to be expressed in the following format:
* <p>
* groupId:artifactId:classifier:type
* <p>
* With the classifier and type being optional.
* <p>
* If the type is missing, the artifact is assumed to be of type {@code jar}.
* <p>
* This parameter is optional, if absent, no optional dependencies will be included into the final package of
* the application.
* <p>
* For backward compatibility reasons, this parameter is ignored by default and can be enabled by setting the
* parameter {@code quarkus.package.filter-optional-dependencies} to {@code true}.
* <p>
* This parameter is meant to be used in modules where multi-builds have been configured to avoid getting a final
* package with unused dependencies.
*/
@ConfigItem
public Optional<Set<String>> includedOptionalDependencies;

/**
* Flag indicating whether the optional dependencies should be filtered out or not.
* <p>
* This parameter is meant to be used in modules where multi-builds have been configured to avoid getting a final
* package with unused dependencies.
*/
@ConfigItem(defaultValue = "false")
public boolean filterOptionalDependencies;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give a reasonable example of having configured optionalDependencies and wishing filterOptionalDependencies to be false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aloubyansky No there is no such example. But I need a mechanism to enable the feature in order to distinguish, the case where we don't want to include any optional dependencies by not setting optionalDependencies intentionally and the case where optionalDependencies has not been set after migrating an application (what I called "backward compatibility reasons")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there is some config redundancy. I.e. if you specify which dependencies you want to keep, it implies the rest of the optional deps you will want to be filtered out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, I get what you mean. This is only a flag to enable the feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't use this flag, there are several tests with optional dependencies that fail because it will filter out all the optional dependencies

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably a sign of a bug? Why would all the optional dependencies be filtered out w/o it?
What I'm saying is if you configure optionalDependencies, you list the dependencies you want to keep. Which means the rest of the optional dependencies aren't desired, right?

Copy link
Contributor Author

@essobedo essobedo Apr 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably a sign of a bug? Why would all the optional dependencies be filtered out w/o it?

@aloubyansky No it is actually intended, let's say that you want to build your application twice with two different Quarkus profiles foo and bar. The code for bar needs one additional library "ext-bar". So the dependency "ext-bar" will be marked as optional, foo won't define anything for includedOptionalDependencies as it doesn't need any optional dependencies while bar will set ext-bar as includedOptionalDependencies. Do you see what I mean?

What I'm saying is if you configure optionalDependencies, you list the dependencies you want to keep. Which means the rest of the optional dependencies aren't desired, right?

That's right, it also means that if no optionalDependencies are configured and filterOptionalDependencies has been set to true, then it will not include any optional dependencies.

Here are the use cases:

  1. filterOptionalDependencies set to false (the default value), it will behaves like before in other words, the optional dependencies are not filtered whatever the value of includedOptionalDependencies since the feature is disabled (what I refer as "backward compatibility reasons")
  2. filterOptionalDependencies set to true and includedOptionalDependencies are absent, then all the optional dependencies are filtered out.
  3. filterOptionalDependencies set to true and includedOptionalDependencies are present, then only the optional dependencies in includedOptionalDependencies are kept, the rest is filtered out.

Is it clear enough now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks. This PR actually enables reasonable use-cases for optional dependencies in Quarkus apps. Otherwise, I am not sure they make sense in Quarkus apps. There is a use-case of re-augmenting mutable-jar packaged apps though. But even then it might benefit from these config options.


/**
* The suffix that is applied to the runner jar and native images
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.quarkus.deployment.pkg.builditem;

import java.nio.file.Path;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;

import io.quarkus.bootstrap.model.AppArtifactKey;
import io.quarkus.builder.item.SimpleBuildItem;

/**
Expand All @@ -16,12 +19,15 @@ public final class OutputTargetBuildItem extends SimpleBuildItem {
private final String baseName;
private final boolean rebuild;
private final Properties buildSystemProperties;
private final Optional<Set<AppArtifactKey>> includedOptionalDependencies;

public OutputTargetBuildItem(Path outputDirectory, String baseName, boolean rebuild, Properties buildSystemProperties) {
public OutputTargetBuildItem(Path outputDirectory, String baseName, boolean rebuild, Properties buildSystemProperties,
Optional<Set<AppArtifactKey>> includedOptionalDependencies) {
this.outputDirectory = outputDirectory;
this.baseName = baseName;
this.rebuild = rebuild;
this.buildSystemProperties = buildSystemProperties;
this.includedOptionalDependencies = includedOptionalDependencies;
}

public Path getOutputDirectory() {
Expand All @@ -39,4 +45,8 @@ public boolean isRebuild() {
public Properties getBuildSystemProperties() {
return buildSystemProperties;
}

public Optional<Set<AppArtifactKey>> getIncludedOptionalDependencies() {
return includedOptionalDependencies;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,10 @@
import org.apache.commons.lang3.SystemUtils;
import org.jboss.logging.Logger;

import io.quarkus.bootstrap.BootstrapDependencyProcessingException;
import io.quarkus.bootstrap.model.AppArtifact;
import io.quarkus.bootstrap.model.AppArtifactKey;
import io.quarkus.bootstrap.model.AppDependency;
import io.quarkus.bootstrap.model.PersistentAppModel;
import io.quarkus.bootstrap.resolver.AppModelResolverException;
import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
import io.quarkus.bootstrap.runner.SerializedApplication;
import io.quarkus.bootstrap.util.IoUtils;
Expand Down Expand Up @@ -155,7 +153,15 @@ OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig
String name = packageConfig.outputName.orElseGet(bst::getBaseName);
Path path = packageConfig.outputDirectory.map(s -> bst.getOutputDirectory().resolve(s))
.orElseGet(bst::getOutputDirectory);
return new OutputTargetBuildItem(path, name, bst.isRebuild(), bst.getBuildSystemProps());
Optional<Set<AppArtifactKey>> includedOptionalDependencies;
if (packageConfig.filterOptionalDependencies) {
includedOptionalDependencies = Optional.of(packageConfig.includedOptionalDependencies
.map(set -> set.stream().map(AppArtifactKey::fromString).collect(Collectors.toSet()))
.orElse(Collections.emptySet()));
} else {
includedOptionalDependencies = Optional.empty();
}
return new OutputTargetBuildItem(path, name, bst.isRebuild(), bst.getBuildSystemProps(), includedOptionalDependencies);
}

@BuildStep(onlyIf = JarRequired.class)
Expand Down Expand Up @@ -195,8 +201,7 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem
if (legacyJarRequired.isEmpty() && (!uberJarRequired.isEmpty()
|| packageConfig.type.equalsIgnoreCase(PackageConfig.UBER_JAR))) {
return buildUberJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem,
packageConfig, applicationInfo, generatedClasses, generatedResources, closeablesBuildItem,
mainClassBuildItem);
packageConfig, applicationInfo, generatedClasses, generatedResources, mainClassBuildItem);
} else if (!legacyJarRequired.isEmpty() || packageConfig.isLegacyJar()
|| packageConfig.type.equalsIgnoreCase(PackageConfig.LEGACY)) {
return buildLegacyThinJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses,
Expand Down Expand Up @@ -243,7 +248,6 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
ApplicationInfoBuildItem applicationInfo,
List<GeneratedClassBuildItem> generatedClasses,
List<GeneratedResourceBuildItem> generatedResources,
QuarkusBuildCloseablesBuildItem closeablesBuildItem,
MainClassBuildItem mainClassBuildItem) throws Exception {

//we use the -runner jar name, unless we are building both types
Expand All @@ -252,6 +256,7 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
Files.deleteIfExists(runnerJar);

buildUberJar0(curateOutcomeBuildItem,
outputTargetBuildItem,
transformedClasses,
applicationArchivesBuildItem,
packageConfig,
Expand All @@ -276,6 +281,7 @@ private String suffixToClassifier(String suffix) {
}

private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem,
OutputTargetBuildItem outputTargetBuildItem,
TransformedClassesBuildItem transformedClasses,
ApplicationArchivesBuildItem applicationArchivesBuildItem,
PackageConfig packageConfig,
Expand Down Expand Up @@ -306,7 +312,8 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem,
final AppArtifact depArtifact = appDep.getArtifact();

// Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852)
if (!isAppDepAJar(depArtifact)) {
// and are not part of the optional dependencies to include
if (!includeAppDep(appDep, outputTargetBuildItem.getIncludedOptionalDependencies())) {
continue;
}

Expand Down Expand Up @@ -342,8 +349,30 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem,
runnerJar.toFile().setReadable(true, false);
}

private boolean isAppDepAJar(AppArtifact artifact) {
return "jar".equals(artifact.getType());
/**
* Indicates whether the given dependency should be included or not.
* <p>
* A dependency should be included if it is a jar file and:
* <p>
* <ul>
* <li>The dependency is not optional or</li>
* <li>The dependency is part of the optional dependencies to include or</li>
* <li>The optional dependencies to include are absent</li>
* </ul>
*
* @param appDep the dependency to test.
* @param optionalDependencies the optional dependencies to include into the final package.
* @return {@code true} if the dependency should be included, {@code false} otherwise.
*/
private static boolean includeAppDep(AppDependency appDep, Optional<Set<AppArtifactKey>> optionalDependencies) {
if (!"jar".equals(appDep.getArtifact().getType())) {
return false;
}
if (appDep.isOptional()) {
return optionalDependencies.map(appArtifactKeys -> appArtifactKeys.contains(appDep.getArtifact().getKey()))
.orElse(true);
}
return true;
}

private void walkFileDependencyForDependency(Path root, FileSystem runnerZipFs, Map<String, String> seen,
Expand Down Expand Up @@ -424,7 +453,8 @@ private JarBuildItem buildLegacyThinJar(CurateOutcomeBuildItem curateOutcomeBuil

log.info("Building thin jar: " + runnerJar);

doLegacyThinJarGeneration(curateOutcomeBuildItem, transformedClasses, applicationArchivesBuildItem, applicationInfo,
doLegacyThinJarGeneration(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses,
applicationArchivesBuildItem, applicationInfo,
packageConfig, generatedResources, libDir, generatedClasses, runnerZipFs, mainClassBuildItem);
}
runnerJar.toFile().setReadable(true, false);
Expand Down Expand Up @@ -576,7 +606,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
if (rebuild) {
jars.addAll(appDep.getArtifact().getPaths().toList());
} else {
copyDependency(curateOutcomeBuildItem, copiedArtifacts, mainLib, baseLib, jars, true, classPath, appDep);
copyDependency(curateOutcomeBuildItem, outputTargetBuildItem, copiedArtifacts, mainLib, baseLib, jars, true,
classPath, appDep);
}
if (curateOutcomeBuildItem.getEffectiveModel().getRunnerParentFirstArtifacts()
.contains(appDep.getArtifact().getKey())) {
Expand Down Expand Up @@ -631,7 +662,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
Path deploymentLib = libDir.resolve(DEPLOYMENT_LIB);
Files.createDirectories(deploymentLib);
for (AppDependency appDep : curateOutcomeBuildItem.getEffectiveModel().getFullDeploymentDeps()) {
copyDependency(curateOutcomeBuildItem, copiedArtifacts, deploymentLib, baseLib, jars, false, classPath,
copyDependency(curateOutcomeBuildItem, outputTargetBuildItem, copiedArtifacts, deploymentLib, baseLib, jars,
false, classPath,
appDep);
}

Expand Down Expand Up @@ -753,13 +785,15 @@ private boolean decompile(Path fernflowerJar, Path decompiledOutputDir, Path jar
return true;
}

private void copyDependency(CurateOutcomeBuildItem curateOutcomeBuildItem, Map<AppArtifactKey, List<Path>> runtimeArtifacts,
Path libDir, Path baseLib, List<Path> jars, boolean allowParentFirst, StringBuilder classPath, AppDependency appDep)
private void copyDependency(CurateOutcomeBuildItem curateOutcomeBuildItem, OutputTargetBuildItem outputTargetBuildItem,
Map<AppArtifactKey, List<Path>> runtimeArtifacts, Path libDir, Path baseLib, List<Path> jars,
boolean allowParentFirst, StringBuilder classPath, AppDependency appDep)
throws IOException {
final AppArtifact depArtifact = appDep.getArtifact();

// Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852)
if (!isAppDepAJar(depArtifact)) {
// and are not part of the optional dependencies to include
if (!includeAppDep(appDep, outputTargetBuildItem.getIncludedOptionalDependencies())) {
return;
}
if (runtimeArtifacts.containsKey(depArtifact.getKey())) {
Expand Down Expand Up @@ -876,8 +910,9 @@ private NativeImageSourceJarBuildItem buildNativeImageThinJar(CurateOutcomeBuild

log.info("Building native image source jar: " + runnerJar);

doLegacyThinJarGeneration(curateOutcomeBuildItem, transformedClasses, applicationArchivesBuildItem, applicationInfo,
packageConfig, generatedResources, libDir, allClasses, runnerZipFs, mainClassBuildItem);
doLegacyThinJarGeneration(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses,
applicationArchivesBuildItem, applicationInfo, packageConfig, generatedResources, libDir, allClasses,
runnerZipFs, mainClassBuildItem);
}
runnerJar.toFile().setReadable(true, false);
return new NativeImageSourceJarBuildItem(runnerJar, libDir);
Expand All @@ -898,6 +933,7 @@ private NativeImageSourceJarBuildItem buildNativeImageUberJar(CurateOutcomeBuild
.resolve(outputTargetBuildItem.getBaseName() + packageConfig.runnerSuffix + ".jar");

buildUberJar0(curateOutcomeBuildItem,
outputTargetBuildItem,
transformedClasses,
applicationArchivesBuildItem,
packageConfig,
Expand Down Expand Up @@ -937,6 +973,7 @@ public void accept(Path jsonPath) {
}

private void doLegacyThinJarGeneration(CurateOutcomeBuildItem curateOutcomeBuildItem,
OutputTargetBuildItem outputTargetBuildItem,
TransformedClassesBuildItem transformedClasses,
ApplicationArchivesBuildItem applicationArchivesBuildItem,
ApplicationInfoBuildItem applicationInfo,
Expand All @@ -946,7 +983,7 @@ private void doLegacyThinJarGeneration(CurateOutcomeBuildItem curateOutcomeBuild
List<GeneratedClassBuildItem> allClasses,
FileSystem runnerZipFs,
MainClassBuildItem mainClassBuildItem)
throws BootstrapDependencyProcessingException, AppModelResolverException, IOException {
throws IOException {
final Map<String, String> seen = new HashMap<>();
final StringBuilder classPath = new StringBuilder();
final Map<String, List<byte[]>> services = new HashMap<>();
Expand All @@ -955,7 +992,8 @@ private void doLegacyThinJarGeneration(CurateOutcomeBuildItem curateOutcomeBuild
final Set<String> finalIgnoredEntries = new HashSet<>(IGNORED_ENTRIES);
packageConfig.userConfiguredIgnoredEntries.ifPresent(finalIgnoredEntries::addAll);

copyLibraryJars(runnerZipFs, transformedClasses, libDir, classPath, appDeps, services, finalIgnoredEntries);
copyLibraryJars(runnerZipFs, outputTargetBuildItem, transformedClasses, libDir, classPath, appDeps, services,
finalIgnoredEntries);

AppArtifact appArtifact = curateOutcomeBuildItem.getEffectiveModel().getAppArtifact();
// the manifest needs to be the first entry in the jar, otherwise JarInputStream does not work properly
Expand All @@ -967,15 +1005,17 @@ private void doLegacyThinJarGeneration(CurateOutcomeBuildItem curateOutcomeBuild
generatedResources, seen, finalIgnoredEntries);
}

private void copyLibraryJars(FileSystem runnerZipFs, TransformedClassesBuildItem transformedClasses, Path libDir,
private void copyLibraryJars(FileSystem runnerZipFs, OutputTargetBuildItem outputTargetBuildItem,
TransformedClassesBuildItem transformedClasses, Path libDir,
StringBuilder classPath, List<AppDependency> appDeps, Map<String, List<byte[]>> services,
Set<String> ignoredEntries) throws IOException {

for (AppDependency appDep : appDeps) {
final AppArtifact depArtifact = appDep.getArtifact();

// Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852)
if (!isAppDepAJar(depArtifact)) {
// and are not part of the optional dependencies to include
if (!includeAppDep(appDep, outputTargetBuildItem.getIncludedOptionalDependencies())) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,29 @@ void testMultiBuildMode() throws MavenInvocationException, InterruptedException,
testDir = initProject("projects/multi-build-mode");
build(null);

launch("foo-", "Foo: hello, from foo");
launch("bar-", "Bar: hello, from bar");
for (TestContext context : TestContext.values()) {
if (context == TestContext.FAST_NO_PREFIX) {
continue;
}
launch(context, "foo-", "Foo: hello, from foo-?/MultiSet");
launch(context, "bar-", "Bar: hello, from bar-FileUtils/?");
launch(context, "foo-full-", "Foo: hello, from foo-FileUtils/MultiSet");
launch(context, "bar-empty-", "Bar: hello, from bar-?/?");
}
}

private void launch() throws IOException {
launch("", "hello, from foo");
launch(TestContext.FAST_NO_PREFIX, "", "hello, from foo");
}

private void launch(String outputPrefix, String expectedMessage) throws IOException {
File output = new File(testDir, String.format("target/%soutput.log", outputPrefix));
private void launch(TestContext context, String outputPrefix, String expectedMessage) throws IOException {
File output = new File(testDir, String.format("target/%s%soutput.log", context.prefix, outputPrefix));
output.createNewFile();
Process process = JarRunnerIT.doLaunch(new File(testDir, String.format("target/%squarkus-app", outputPrefix)),
Paths.get("quarkus-run.jar"), output,
Collections.emptyList()).start();
Process process = JarRunnerIT
.doLaunch(new File(testDir, String.format("target/%s%squarkus-app", context.prefix, outputPrefix)),
Paths.get(context.jarFileName), output,
Collections.emptyList())
.start();
try {
Assertions.assertEquals(expectedMessage, DevModeTestUtils.getHttpResponse("/hello"));
} finally {
Expand Down Expand Up @@ -133,4 +142,19 @@ private void ensureManifestOfJarIsReadableByJarInputStream(File jar) throws IOEx
}
}
}

enum TestContext {
FAST_NO_PREFIX("", "quarkus-run.jar"),
FAST("fast-", "quarkus-run.jar"),
LEGACY("legacy-", "legacy-runner.jar"),
UBER("uber-", "uber-runner.jar");

final String prefix;
final String jarFileName;

TestContext(String prefix, String jarFileName) {
this.prefix = prefix;
this.jarFileName = jarFileName;
}
}
}
Loading