##// END OF EJS Templates
refactor: replace configureAssembly with configureTask
cin -
r37:3be316021669 default
parent child
Show More
@@ -1,53 +1,19
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 import org.gradle.api.Named;
4 import org.gradle.api.Task;
5 5 import org.gradle.api.file.ConfigurableFileCollection;
6 6 import org.gradle.api.file.Directory;
7 import org.gradle.api.file.FileCollection;
8 7 import org.gradle.api.provider.Provider;
9 import org.gradle.api.tasks.Copy;
10 8 import org.gradle.api.tasks.TaskProvider;
11 9
12 10 @NonNullByDefault
13 public final class ArtifactAssembly implements Named {
14 private final String name;
15 private final ConfigurableFileCollection sources;
16 private final ConfigurableFileCollection output;
17 private final Provider<Directory> outputDirectory;
18 private final TaskProvider<Copy> task;
19
20 ArtifactAssembly(
21 String name,
22 ConfigurableFileCollection sources,
23 Provider<Directory> outputDirectory,
24 TaskProvider<Copy> task,
25 ConfigurableFileCollection output) {
26 this.name = name;
27 this.sources = sources;
28 this.outputDirectory = outputDirectory;
29 this.task = task;
30 this.output = output;
31 }
11 public final record ArtifactAssembly(
12 String name,
13 Provider<Directory> outputDirectory,
14 TaskProvider<? extends Task> task,
15 ConfigurableFileCollection output
16 ) {
32 17
33 @Override
34 public String getName() {
35 return name;
36 }
37
38 public ConfigurableFileCollection getSources() {
39 return sources;
40 }
18 };
41 19
42 public FileCollection getOutput() {
43 return output;
44 }
45
46 public Provider<Directory> getOutputDirectory() {
47 return outputDirectory;
48 }
49
50 public TaskProvider<Copy> getTask() {
51 return task;
52 }
53 }
@@ -1,64 +1,76
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.LinkedHashMap;
4 4 import java.util.Map;
5 5 import java.util.Optional;
6 import java.util.function.Function;
6 7
7 8 import org.eclipse.jdt.annotation.NonNullByDefault;
8 9 import org.gradle.api.Action;
9 10 import org.gradle.api.InvalidUserDataException;
11 import org.gradle.api.Task;
10 12 import org.gradle.api.file.ConfigurableFileCollection;
11 13 import org.gradle.api.file.Directory;
12 14 import org.gradle.api.model.ObjectFactory;
13 15 import org.gradle.api.provider.Provider;
14 16 import org.gradle.api.tasks.Copy;
15 17 import org.gradle.api.tasks.TaskContainer;
18 import org.gradle.api.tasks.TaskProvider;
16 19 import org.gradle.language.base.plugins.LifecycleBasePlugin;
17 20
18 21 @NonNullByDefault
19 22 public final class ArtifactAssemblyRegistry {
20 23 private final ObjectFactory objects;
21 24 private final TaskContainer tasks;
22 25 private final Map<String, ArtifactAssembly> assemblies = new LinkedHashMap<>();
23 26
24 27 public ArtifactAssemblyRegistry(ObjectFactory objects, TaskContainer tasks) {
25 28 this.objects = objects;
26 29 this.tasks = tasks;
27 30 }
28 31
29 32 public ArtifactAssembly register(
30 33 String name,
31 34 String taskName,
32 35 Provider<Directory> outputDirectory,
33 36 Action<? super ConfigurableFileCollection> configureSources) {
34 if (assemblies.containsKey(name)) {
35 throw new InvalidUserDataException("Artifact assembly '" + name + "' is already registered");
36 }
37 37
38 38 var sources = objects.fileCollection();
39 39 configureSources.execute(sources);
40 40
41 41 var task = tasks.register(taskName, Copy.class, copy -> {
42 42 copy.setGroup(LifecycleBasePlugin.BUILD_GROUP);
43 43 copy.into(outputDirectory);
44 44 copy.from(sources);
45 45 });
46 46
47 return register(name, task, t -> outputDirectory);
48 }
49
50 public <T extends Task> ArtifactAssembly register(
51 String name,
52 TaskProvider<T> task,
53 Function<? super T, ? extends Provider<Directory>> mapOutputDirectory) {
54 if (assemblies.containsKey(name)) {
55 throw new InvalidUserDataException("Artifact assembly '" + name + "' is already registered");
56 }
57 var outputDirectory = task.flatMap(t -> mapOutputDirectory.apply(t));
58
47 59 var output = objects.fileCollection()
48 60 .from(outputDirectory)
49 61 .builtBy(task);
50 62
51 var assembly = new ArtifactAssembly(name, sources, outputDirectory, task, output);
63 var assembly = new ArtifactAssembly(name, outputDirectory, task, output);
52 64 assemblies.put(name, assembly);
53 65 return assembly;
54 66 }
55 67
56 68 public Optional<ArtifactAssembly> find(String name) {
57 69 return Optional.ofNullable(assemblies.get(name));
58 70 }
59 71
60 72 public ArtifactAssembly require(String name) {
61 73 return find(name)
62 74 .orElseThrow(() -> new InvalidUserDataException("Artifact assembly '" + name + "' isn't registered"));
63 75 }
64 76 }
@@ -1,205 +1,206
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.io.File;
4 4 import java.nio.file.Paths;
5 5 import java.util.HashSet;
6 6 import java.util.LinkedHashMap;
7 7 import java.util.List;
8 8 import java.util.Map;
9 9 import java.util.Objects;
10 10 import java.util.Set;
11 11 import java.util.concurrent.Callable;
12 12 import java.util.function.Function;
13 13 import java.util.stream.Collectors;
14 import java.util.stream.Stream;
14 15
15 16 import javax.inject.Inject;
16 17
17 18 import org.gradle.api.InvalidUserDataException;
18 19 import org.gradle.api.Named;
19 20 import org.gradle.api.NamedDomainObjectContainer;
20 21 import org.gradle.api.Task;
21 22 import org.gradle.api.file.ConfigurableFileCollection;
22 23 import org.gradle.api.file.DirectoryProperty;
23 24 import org.gradle.api.file.FileCollection;
24 25 import org.gradle.api.file.ProjectLayout;
25 26 import org.gradle.api.file.SourceDirectorySet;
26 27 import org.gradle.api.model.ObjectFactory;
27 28 import org.gradle.api.tasks.TaskProvider;
28 29 import org.gradle.util.Configurable;
29 30 import org.implab.gradle.common.core.lang.Closures;
30 31
31 32 import groovy.lang.Closure;
32 33
33 34 /**
34 35 * A configurable source set abstraction with named outputs.
35 36 *
36 37 * <p>
37 38 * Each instance aggregates multiple {@link SourceDirectorySet source sets}
38 39 * under a shared name and exposes typed outputs that must be declared up front.
39 40 * Default locations are {@code src/<name>} for sources and
40 41 * {@code build/<name>} for outputs, both of which can be customized via the
41 42 * exposed {@link DirectoryProperty} setters.
42 43 * </p>
43 44 *
44 45 * <p>
45 46 * Outputs are grouped by names to make task wiring explicit. An output must be
46 47 * declared with {@link #declareOutputs(String, String...)} before files can be
47 48 * registered against it. Attempting to register or retrieve an undeclared
48 49 * output results in
49 50 * {@link InvalidUserDataException}.
50 51 * </p>
51 52 */
52 53 public abstract class GenericSourceSet
53 54 implements Named, Configurable<GenericSourceSet> {
54 55 private final String name;
55 56
56 57 private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets;
57 58
58 59 private final Map<String, ConfigurableFileCollection> outputs;
59 60
60 61 private final FileCollection allOutputs;
61 62
62 63 private final FileCollection allSourceDirectories;
63 64
64 65 private final ObjectFactory objects;
65 66
66 67 private final Set<String> declaredOutputs = new HashSet<>();
67 68
68 69 @Inject
69 70 public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) {
70 71 this.name = name;
71 72 this.objects = objects;
72 73
73 74 sourceDirectorySets = objects.domainObjectContainer(
74 75 SourceDirectorySet.class,
75 76 this::createSourceDirectorySet);
76 77
77 78 outputs = new LinkedHashMap<>();
78 79
79 80 allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider());
80 81
81 82 allOutputs = objects.fileCollection().from(outputsProvider());
82 83
83 84 getSourceSetDir().convention(layout
84 85 .getProjectDirectory()
85 86 .dir(Paths.get("src", name).toString()));
86 87
87 88 getOutputsDir().convention(layout
88 89 .getBuildDirectory()
89 90 .dir(name));
90 91 }
91 92
92 93 @Override
93 94 public String getName() {
94 95 return name;
95 96 }
96 97
97 98 /**
98 99 * Base directory for this source set. Defaults to {@code src/<name>} under
99 100 * the project directory.
100 101 */
101 102 public abstract DirectoryProperty getSourceSetDir();
102 103
103 104 /**
104 105 * Base directory for outputs of this source set. Defaults to
105 106 * {@code build/<name>}.
106 107 */
107 108 public abstract DirectoryProperty getOutputsDir();
108 109
109 110 /**
110 111 * The container of {@link SourceDirectorySet} instances that belong to this
111 112 * logical source set.
112 113 */
113 114 public NamedDomainObjectContainer<SourceDirectorySet> getSets() {
114 115 return sourceDirectorySets;
115 116 }
116 117
117 118 /**
118 119 * All registered outputs grouped across output names.
119 120 */
120 121 public FileCollection getAllOutputs() {
121 122 return allOutputs;
122 123 }
123 124
124 125 /**
125 126 * All source directories from every contained {@link SourceDirectorySet}.
126 127 */
127 128 public FileCollection getAllSourceDirectories() {
128 129 return allSourceDirectories;
129 130 }
130 131
131 132 /**
132 133 * Returns the file collection for the specified output name, creating it
133 134 * if necessary.
134 135 *
135 136 * @throws InvalidUserDataException if the output was not declared
136 137 */
137 138 public FileCollection output(String name) {
138 139 return configurableOutput(name);
139 140 }
140 141
141 142 private ConfigurableFileCollection configurableOutput(String name) {
142 143 requireDeclaredOutput(name);
143 144 return outputs.computeIfAbsent(name, key -> objects.fileCollection());
144 145 }
145 146
146 147 /**
147 148 * Declares allowed output names. Outputs must be declared before registering
148 149 * files under them.
149 150 */
150 151 public void declareOutputs(String name, String... extra) {
151 declaredOutputs.add(Objects.requireNonNull(name, "declareOutputs: The output name cannot be null"));
152 for (var x : extra)
153 declaredOutputs.add(Objects.requireNonNull(x, "declareOutputs: The output name cannot be null"));
152 Stream.concat(Stream.of(name), Stream.of(extra))
153 .map(Objects::requireNonNull)
154 .forEach(declaredOutputs::add);
154 155 }
155 156
156 157 /**
157 158 * Registers files produced elsewhere under the given output.
158 159 */
159 160 public void registerOutput(String name, Object... files) {
160 161 configurableOutput(name).from(files);
161 162 }
162 163
163 164 /**
164 165 * Registers output files produced by a task, using a mapper to extract the
165 166 * output from the task. The task will be added as a build dependency of this
166 167 * output.
167 168 */
168 169 public <T extends Task> void registerOutput(String name, TaskProvider<T> task,
169 170 Function<? super T, ?> mapper) {
170 171 configurableOutput(name).from(task.map(mapper::apply))
171 172 .builtBy(task);
172 173 }
173 174
174 175 /**
175 176 * Applies a Groovy closure to this source set, enabling DSL-style
176 177 * configuration.
177 178 */
178 179 @Override
179 180 public GenericSourceSet configure(Closure configure) {
180 181 Closures.apply(configure, this);
181 182 return this;
182 183 }
183 184
184 185 private SourceDirectorySet createSourceDirectorySet(String name) {
185 186 return objects.sourceDirectorySet(name, name);
186 187 }
187 188
188 189 private void requireDeclaredOutput(String outputName) {
189 190 if (!declaredOutputs.contains(outputName)) {
190 191 throw new InvalidUserDataException(
191 192 "Output '" + outputName + "' is not declared for source set '" + name + "'");
192 193 }
193 194 }
194 195
195 196 private Callable<List<? extends FileCollection>> outputsProvider() {
196 197 return () -> outputs.values().stream().toList();
197 198 }
198 199
199 200 private Callable<Set<File>> sourceDirectoriesProvider() {
200 201 return () -> sourceDirectorySets.stream()
201 202 .flatMap(x -> x.getSrcDirs().stream())
202 203 .collect(Collectors.toSet());
203 204 }
204 205
205 206 }
@@ -1,66 +1,63
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 4 import org.gradle.api.Action;
5 import org.gradle.api.Task;
5 6 import org.gradle.api.attributes.AttributeContainer;
6 7 import org.gradle.api.attributes.HasConfigurableAttributes;
7 8 import org.implab.gradle.common.core.lang.Closures;
8 9
9 10 import groovy.lang.Closure;
10 11 import groovy.lang.DelegatesTo;
11 12
12 13 @NonNullByDefault
13 14 public final class OutgoingArtifactSlotPublication {
14 15 private final String slotName;
15 16 private final boolean primary;
16 17 private final VariantArtifactSlot slot;
17 18 private final ArtifactAssembly assembly;
18 19 private final HasConfigurableAttributes<?> attributesCarrier;
19 20
20 21 OutgoingArtifactSlotPublication(
21 22 String slotName,
22 23 boolean primary,
23 24 VariantArtifactSlot slot,
24 25 ArtifactAssembly assembly,
25 26 HasConfigurableAttributes<?> attributesCarrier) {
26 27 this.slotName = slotName;
27 28 this.primary = primary;
28 29 this.slot = slot;
29 30 this.assembly = assembly;
30 31 this.attributesCarrier = attributesCarrier;
31 32 }
32 33
33 34 public String slotName() {
34 35 return slotName;
35 36 }
36 37
37 38 public boolean primary() {
38 39 return primary;
39 40 }
40 41
41 42 public VariantArtifactSlot slot() {
42 43 return slot;
43 44 }
44 45
45 public ArtifactAssembly assembly() {
46 return assembly;
46 public void configureTask(Action<? super Task> action) {
47 assembly.task().configure(action::execute);
47 48 }
48 49
49 public void configureAssembly(Action<? super ArtifactAssembly> action) {
50 action.execute(assembly);
51 }
52
53 public void configureAssembly(
54 @DelegatesTo(value = ArtifactAssembly.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
55 configureAssembly(Closures.action(action));
50 public void configureTask(
51 @DelegatesTo(value = Task.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
52 configureTask(Closures.action(action));
56 53 }
57 54
58 55 public void configureArtifactAttributes(Action<? super AttributeContainer> action) {
59 56 attributesCarrier.attributes(action);
60 57 }
61 58
62 59 public void configureArtifactAttributes(
63 60 @DelegatesTo(value = AttributeContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
64 61 configureArtifactAttributes(Closures.action(action));
65 62 }
66 63 }
@@ -1,181 +1,181
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.LinkedHashMap;
4 4 import java.util.List;
5 5 import java.util.ArrayList;
6 6 import java.util.stream.Collectors;
7 7 import java.util.stream.Stream;
8 8
9 9 import org.gradle.api.GradleException;
10 10 import org.gradle.api.Plugin;
11 11 import org.gradle.api.Project;
12 12 import org.gradle.api.artifacts.Configuration;
13 13 import org.gradle.api.artifacts.ConfigurationPublications;
14 14 import org.gradle.api.artifacts.ConfigurationVariant;
15 15 import org.gradle.api.logging.Logger;
16 16 import org.gradle.api.logging.Logging;
17 17 import org.implab.gradle.common.core.lang.Strings;
18 18
19 19 public abstract class VariantArtifactsPlugin implements Plugin<Project> {
20 20 private static final Logger logger = Logging.getLogger(VariantArtifactsPlugin.class);
21 21 public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";
22 22
23 23 @Override
24 24 public void apply(Project target) {
25 25 logger.debug("Registering '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
26 26
27 27 target.getPluginManager().apply(VariantsSourcesPlugin.class);
28 28
29 29 var variants = VariantsPlugin.getVariantsExtension(target);
30 30 var variantSources = target.getExtensions().getByType(VariantSourcesExtension.class);
31 31 var variantArtifacts = target.getExtensions()
32 32 .create(VARIANT_ARTIFACTS_EXTENSION_NAME, VariantArtifactsExtension.class);
33 33 var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
34 34 var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks());
35 35
36 36 // Bind variant artifacts resolution to variant sources registration, so that artifact resolution can be performed
37 37 variantSources.whenBound(variantArtifactsResolver::recordBinding);
38 38
39 39 variants.whenFinalized(model -> {
40 40 logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath());
41 41 variantArtifacts.finalizeModel(model);
42 42 materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies);
43 43 logger.debug("variantArtifacts model finalized on project '{}'", target.getPath());
44 44 });
45 45 }
46 46
47 47 public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) {
48 48 var extension = target.getExtensions().findByType(VariantArtifactsExtension.class);
49 49
50 50 if (extension == null) {
51 51 logger.error("variantArtifacts extension '{}' isn't found on project '{}'",
52 52 VARIANT_ARTIFACTS_EXTENSION_NAME,
53 53 target.getPath());
54 54 throw new GradleException("variantArtifacts extension isn't found");
55 55 }
56 56
57 57 logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
58 58
59 59 return extension;
60 60 }
61 61
62 62 private static void materializeOutgoingVariants(
63 63 Project project,
64 64 BuildVariantsExtension topology,
65 65 VariantArtifactsExtension variantArtifacts,
66 66 VariantArtifactsResolver variantArtifactsResolver,
67 67 ArtifactAssemblyRegistry artifactAssemblies) {
68 68 variantArtifacts.getVariants().stream()
69 69 .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty())
70 70 .forEach(variantArtifact -> materializeOutgoingVariant(
71 71 project,
72 72 topology.require(variantArtifact.getName()),
73 73 variantArtifact,
74 74 variantArtifactsResolver,
75 75 artifactAssemblies,
76 76 variantArtifacts));
77 77 }
78 78
79 79 private static void materializeOutgoingVariant(
80 80 Project project,
81 81 BuildVariant topologyVariant,
82 82 VariantArtifact variantArtifact,
83 83 VariantArtifactsResolver variantArtifactsResolver,
84 84 ArtifactAssemblyRegistry artifactAssemblies,
85 85 VariantArtifactsExtension variantArtifacts) {
86 86 var assemblies = variantArtifact.getSlots().stream()
87 87 .collect(Collectors.toMap(
88 88 VariantArtifactSlot::getName,
89 89 slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, slot),
90 90 (left, right) -> left,
91 91 LinkedHashMap::new));
92 92
93 93 var primarySlot = variantArtifact.requirePrimarySlot();
94 94 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), primarySlot.getName());
95 95 var primaryAssembly = assemblies.get(primarySlot.getName());
96 96 publishPrimaryArtifact(configuration, primaryAssembly);
97 97 var primaryPublication = new OutgoingArtifactSlotPublication(
98 98 primarySlot.getName(),
99 99 true,
100 100 primarySlot,
101 101 primaryAssembly,
102 102 configuration);
103 103 var secondarySlots = variantArtifact.getSlots().stream()
104 104 .filter(slot -> !slot.getName().equals(primarySlot.getName()))
105 105 .map(slot -> new SecondarySlot(slot, assemblies.get(slot.getName())))
106 106 .toList();
107 107 var secondaryPublications = new ArrayList<OutgoingArtifactSlotPublication>(secondarySlots.size());
108 108 secondarySlots.forEach(secondarySlot -> {
109 109 var secondaryVariant = configuration.getOutgoing().getVariants().create(secondarySlot.slot().getName());
110 110 publishSecondaryArtifact(secondaryVariant, secondarySlot.assembly());
111 111 secondaryPublications.add(new OutgoingArtifactSlotPublication(
112 112 secondarySlot.slot().getName(),
113 113 false,
114 114 secondarySlot.slot(),
115 115 secondarySlot.assembly(),
116 116 secondaryVariant));
117 117 });
118 118
119 119 var slotPublications = Stream.concat(
120 120 Stream.of(primaryPublication),
121 121 secondaryPublications.stream())
122 122 .toList();
123 123
124 124 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
125 125 variantArtifact.getName(),
126 126 topologyVariant,
127 127 variantArtifact,
128 128 configuration,
129 129 primaryPublication,
130 130 slotPublications));
131 131 }
132 132
133 133 private static ArtifactAssembly registerAssembly(
134 134 Project project,
135 135 VariantArtifactsResolver variantArtifactsResolver,
136 136 ArtifactAssemblyRegistry artifactAssemblies,
137 137 VariantArtifact variantArtifact,
138 138 VariantArtifactSlot slot) {
139 139 return artifactAssemblies.register(
140 140 variantArtifact.getName() + Strings.capitalize(slot.getName()),
141 141 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
142 142 project.getLayout().getBuildDirectory()
143 143 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
144 144 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
145 145 }
146 146
147 147 private static Configuration createOutgoingConfiguration(
148 148 Project project,
149 149 String variantName,
150 150 String primarySlotName) {
151 151 var configName = variantName + "Elements";
152 152 return project.getConfigurations().consumable(configName, config -> {
153 153 config.setVisible(true);
154 154 config.setDescription("Consumable assembled artifacts for variant '" + variantName
155 155 + "' with primary slot '" + primarySlotName + "'");
156 156 }).get();
157 157 }
158 158
159 159 private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) {
160 160 publishArtifact(configuration.getOutgoing(), assembly);
161 161 }
162 162
163 163 private static void publishSecondaryArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
164 164 publishArtifact(variant, assembly);
165 165 }
166 166
167 167 private static void publishArtifact(ConfigurationPublications outgoing, ArtifactAssembly assembly) {
168 outgoing.artifact(assembly.getOutput().getSingleFile(), published -> {
169 published.builtBy(assembly.getOutput().getBuildDependencies());
168 outgoing.artifact(assembly.output().getSingleFile(), published -> {
169 published.builtBy(assembly.output().getBuildDependencies());
170 170 });
171 171 }
172 172
173 173 private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
174 variant.artifact(assembly.getOutput().getSingleFile(), published -> {
175 published.builtBy(assembly.getOutput().getBuildDependencies());
174 variant.artifact(assembly.output().getSingleFile(), published -> {
175 published.builtBy(assembly.output().getBuildDependencies());
176 176 });
177 177 }
178 178
179 179 private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) {
180 180 }
181 181 }
@@ -1,608 +1,608
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 5 import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
7 7 import java.io.File;
8 8 import java.io.IOException;
9 9 import java.nio.file.Files;
10 10 import java.nio.file.Path;
11 11 import java.util.List;
12 12 import java.util.stream.Collectors;
13 13
14 14 import org.gradle.testkit.runner.BuildResult;
15 15 import org.gradle.testkit.runner.GradleRunner;
16 16 import org.gradle.testkit.runner.TaskOutcome;
17 17 import org.gradle.testkit.runner.UnexpectedBuildFailure;
18 18 import org.junit.jupiter.api.Test;
19 19 import org.junit.jupiter.api.io.TempDir;
20 20
21 21 class VariantsArtifactsPluginFunctionalTest {
22 22 private static final String SETTINGS_FILE = "settings.gradle";
23 23 private static final String BUILD_FILE = "build.gradle";
24 24 private static final String ROOT_NAME = "rootProject.name = 'variants-artifacts-fixture'\n";
25 25
26 26 @TempDir
27 27 Path testProjectDir;
28 28
29 29 @Test
30 30 void materializesVariantArtifactsAndInvokesOutgoingHooks() throws Exception {
31 31 writeFile(SETTINGS_FILE, ROOT_NAME);
32 32 writeFile("inputs/base.js", "console.log('base')\n");
33 33 writeFile("inputs/amd.js", "console.log('amd')\n");
34 34 writeFile("inputs/mainJs.txt", "mainJs marker\n");
35 35 writeFile("inputs/amdJs.txt", "amdJs marker\n");
36 36 writeFile(BUILD_FILE, """
37 37 import org.gradle.api.attributes.Attribute
38 38
39 39 plugins {
40 40 id 'org.implab.gradle-variants-artifacts'
41 41 }
42 42
43 43 variants {
44 44 layer('mainBase')
45 45 layer('mainAmd')
46 46
47 47 variant('browser') {
48 48 role('main') {
49 49 layers('mainBase', 'mainAmd')
50 50 }
51 51 }
52 52 }
53 53
54 54 variantSources {
55 55 bind('mainBase') {
56 56 configureSourceSet {
57 57 declareOutputs('js')
58 58 }
59 59 }
60 60
61 61 bind('mainAmd') {
62 62 configureSourceSet {
63 63 declareOutputs('js')
64 64 }
65 65 }
66 66
67 67 whenBound { ctx ->
68 68 if (ctx.sourceSetName() == 'browserMainBase') {
69 69 ctx.configureSourceSet {
70 70 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
71 71 }
72 72 }
73 73
74 74 if (ctx.sourceSetName() == 'browserMainAmd') {
75 75 ctx.configureSourceSet {
76 76 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
77 77 }
78 78 }
79 79 }
80 80 }
81 81
82 82 variantArtifacts {
83 83 variant('browser') {
84 84 primarySlot('mainJs') {
85 85 fromRole('main') {
86 86 output('js')
87 87 }
88 88 }
89 89
90 90 slot('amdJs') {
91 91 fromLayer('mainAmd') {
92 92 output('js')
93 93 }
94 94 }
95 95 }
96 96
97 97 whenOutgoingVariant { publication ->
98 98 publication.slots().each { slotPublication ->
99 slotPublication.configureAssembly {
100 sources.from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt"))
99 slotPublication.configureTask {
100 from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt"))
101 101 }
102 102
103 103 slotPublication.configureArtifactAttributes {
104 104 attribute(Attribute.of('test.slot', String), slotPublication.slotName())
105 105 }
106 106 }
107 107 }
108 108 }
109 109
110 110 tasks.register('probe') {
111 111 dependsOn 'processBrowserMainJs', 'processBrowserAmdJs'
112 112
113 113 doLast {
114 114 def mainDir = layout.buildDirectory.dir('variant-artifacts/browser/mainJs').get().asFile
115 115 def amdDir = layout.buildDirectory.dir('variant-artifacts/browser/amdJs').get().asFile
116 116
117 117 assert new File(mainDir, 'base.js').exists()
118 118 assert new File(mainDir, 'amd.js').exists()
119 119 assert new File(mainDir, 'mainJs.txt').exists()
120 120
121 121 assert !new File(amdDir, 'base.js').exists()
122 122 assert new File(amdDir, 'amd.js').exists()
123 123 assert new File(amdDir, 'amdJs.txt').exists()
124 124
125 125 def elements = configurations.getByName('browserElements')
126 126 def primaryAttr = elements.attributes.getAttribute(Attribute.of('test.slot', String))
127 127 def amdVariant = elements.outgoing.variants.getByName('amdJs')
128 128 def amdAttr = amdVariant.attributes.getAttribute(Attribute.of('test.slot', String))
129 129
130 130 println('primarySlot=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
131 131 println('primaryAttr=' + primaryAttr)
132 132 println('amdAttr=' + amdAttr)
133 133 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
134 134 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
135 135 }
136 136 }
137 137 """);
138 138
139 139 BuildResult result = runner("probe").build();
140 140
141 141 assertTrue(result.getOutput().contains("primarySlot=mainJs"));
142 142 assertTrue(result.getOutput().contains("primaryAttr=mainJs"));
143 143 assertTrue(result.getOutput().contains("amdAttr=amdJs"));
144 144 assertTrue(result.getOutput().contains("configurations=browserElements"));
145 145 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
146 146 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
147 147 }
148 148
149 149 @Test
150 150 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
151 151 writeFile(SETTINGS_FILE, ROOT_NAME);
152 152 writeFile(BUILD_FILE, """
153 153 plugins {
154 154 id 'org.implab.gradle-variants-artifacts'
155 155 }
156 156
157 157 variants {
158 158 layer('main')
159 159
160 160 variant('browser') {
161 161 role('main') {
162 162 layers('main')
163 163 }
164 164 }
165 165 }
166 166
167 167 variantArtifacts {
168 168 variant('browser') {
169 169 slot('typesPackage') {
170 170 fromVariant {
171 171 output('types')
172 172 }
173 173 }
174 174 }
175 175 }
176 176
177 177 tasks.register('probe') {
178 178 doLast {
179 179 println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
180 180 }
181 181 }
182 182 """);
183 183
184 184 BuildResult result = runner("probe").build();
185 185 assertTrue(result.getOutput().contains("primary=typesPackage"));
186 186 }
187 187
188 188 @Test
189 189 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
190 190 writeFile(SETTINGS_FILE, ROOT_NAME);
191 191 writeFile("inputs/bundle.js", "console.log('bundle')\n");
192 192 writeFile(BUILD_FILE, """
193 193 plugins {
194 194 id 'org.implab.gradle-variants-artifacts'
195 195 }
196 196
197 197 variants {
198 198 layer('main')
199 199
200 200 variant('browser') {
201 201 role('main') {
202 202 layers('main')
203 203 }
204 204 }
205 205 }
206 206
207 207 variantArtifacts {
208 208 variant('browser') {
209 209 primarySlot('bundle') {
210 210 from(layout.projectDirectory.file('inputs/bundle.js'))
211 211 }
212 212 }
213 213 }
214 214
215 215 tasks.register('probe') {
216 216 dependsOn 'processBrowserBundle'
217 217
218 218 doLast {
219 219 def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
220 220 assert new File(bundleDir, 'bundle.js').exists()
221 221 println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
222 222 }
223 223 }
224 224 """);
225 225
226 226 BuildResult result = runner("probe").build();
227 227
228 228 assertTrue(result.getOutput().contains("primary=bundle"));
229 229 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
230 230 }
231 231
232 232 @Test
233 233 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
234 234 writeFile(SETTINGS_FILE, ROOT_NAME);
235 235 writeFile("inputs/base.js", "console.log('base')\n");
236 236 writeFile("inputs/marker.txt", "marker\n");
237 237 writeFile(BUILD_FILE, """
238 238 plugins {
239 239 id 'org.implab.gradle-variants-artifacts'
240 240 }
241 241
242 242 variants {
243 243 layer('main')
244 244
245 245 variant('browser') {
246 246 role('main') {
247 247 layers('main')
248 248 }
249 249 }
250 250 }
251 251
252 252 variantSources {
253 253 bind('main') {
254 254 configureSourceSet {
255 255 declareOutputs('js')
256 256 }
257 257 }
258 258
259 259 whenBound { ctx ->
260 260 ctx.configureSourceSet {
261 261 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
262 262 }
263 263 }
264 264 }
265 265
266 266 variantArtifacts {
267 267 variant('browser') {
268 268 primarySlot('bundle') {
269 269 fromVariant {
270 270 output('js')
271 271 }
272 272 from(layout.projectDirectory.file('inputs/marker.txt'))
273 273 }
274 274 }
275 275 }
276 276
277 277 tasks.register('probe') {
278 278 dependsOn 'processBrowserBundle'
279 279
280 280 doLast {
281 281 def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
282 282 assert new File(bundleDir, 'base.js').exists()
283 283 assert new File(bundleDir, 'marker.txt').exists()
284 284 }
285 285 }
286 286 """);
287 287
288 288 BuildResult result = runner("probe").build();
289 289
290 290 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
291 291 }
292 292
293 293 @Test
294 294 void failsOnUnknownVariantReference() throws Exception {
295 295 assertBuildFails("""
296 296 plugins {
297 297 id 'org.implab.gradle-variants-artifacts'
298 298 }
299 299
300 300 variants {
301 301 layer('main')
302 302 }
303 303
304 304 variantArtifacts {
305 305 variant('browser') {
306 306 slot('mainJs') {
307 307 fromVariant {
308 308 output('js')
309 309 }
310 310 }
311 311 }
312 312 }
313 313 """, "Variant artifact 'browser' references unknown variant 'browser'");
314 314 }
315 315
316 316 @Test
317 317 void failsOnUnknownRoleReference() throws Exception {
318 318 assertBuildFails("""
319 319 plugins {
320 320 id 'org.implab.gradle-variants-artifacts'
321 321 }
322 322
323 323 variants {
324 324 layer('main')
325 325
326 326 variant('browser') {
327 327 role('main') {
328 328 layers('main')
329 329 }
330 330 }
331 331 }
332 332
333 333 variantArtifacts {
334 334 variant('browser') {
335 335 slot('mainJs') {
336 336 fromRole('test') {
337 337 output('js')
338 338 }
339 339 }
340 340 }
341 341 }
342 342 """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'");
343 343 }
344 344
345 345 @Test
346 346 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
347 347 assertBuildFails("""
348 348 plugins {
349 349 id 'org.implab.gradle-variants-artifacts'
350 350 }
351 351
352 352 variants {
353 353 layer('main')
354 354
355 355 variant('browser') {
356 356 role('main') {
357 357 layers('main')
358 358 }
359 359 }
360 360 }
361 361
362 362 variantArtifacts {
363 363 variant('browser') {
364 364 slot('typesPackage') {
365 365 fromVariant {
366 366 output('types')
367 367 }
368 368 }
369 369
370 370 slot('js') {
371 371 fromVariant {
372 372 output('js')
373 373 }
374 374 }
375 375 }
376 376 }
377 377 """, "Variant artifact 'browser' must declare primary slot because it has multiple slots");
378 378 }
379 379
380 380 @Test
381 381 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
382 382 assertBuildFails("""
383 383 plugins {
384 384 id 'org.implab.gradle-variants-artifacts'
385 385 }
386 386
387 387 variants {
388 388 layer('mainBase')
389 389 layer('extra')
390 390
391 391 variant('browser') {
392 392 role('main') {
393 393 layers('mainBase')
394 394 }
395 395 }
396 396 }
397 397
398 398 variantArtifacts {
399 399 variant('browser') {
400 400 slot('extraJs') {
401 401 fromLayer('extra') {
402 402 output('js')
403 403 }
404 404 }
405 405 }
406 406 }
407 407 """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'");
408 408 }
409 409
410 410 @Test
411 411 void failsOnLateMutationAfterFinalize() throws Exception {
412 412 assertBuildFails("""
413 413 plugins {
414 414 id 'org.implab.gradle-variants-artifacts'
415 415 }
416 416
417 417 variants {
418 418 layer('main')
419 419
420 420 variant('browser') {
421 421 role('main') {
422 422 layers('main')
423 423 }
424 424 }
425 425 }
426 426
427 427 afterEvaluate {
428 428 variantArtifacts.variant('late') {
429 429 slot('js') {
430 430 fromVariant {
431 431 output('js')
432 432 }
433 433 }
434 434 }
435 435 }
436 436 """, "variantArtifacts model is finalized and cannot configure variants");
437 437 }
438 438
439 439 @Test
440 440 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
441 441 writeFile(SETTINGS_FILE, """
442 442 rootProject.name = 'variants-artifacts-fixture'
443 443 include 'producer', 'consumer'
444 444 """);
445 445 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
446 446 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
447 447 var buildscriptClasspath = pluginClasspath().stream()
448 448 .map(File::getAbsolutePath)
449 449 .map(path -> "'" + path.replace("\\", "\\\\") + "'")
450 450 .collect(Collectors.joining(", "));
451 451 writeFile(BUILD_FILE, """
452 452 buildscript {
453 453 dependencies {
454 454 classpath files(%s)
455 455 }
456 456 }
457 457
458 458 import org.gradle.api.attributes.Attribute
459 459
460 460 def variantAttr = Attribute.of('test.variant', String)
461 461 def slotAttr = Attribute.of('test.slot', String)
462 462
463 463 subprojects {
464 464 apply plugin: 'org.implab.gradle-variants-artifacts'
465 465 }
466 466
467 467 project(':producer') {
468 468 variants {
469 469 layer('main')
470 470
471 471 variant('browser') {
472 472 role('main') {
473 473 layers('main')
474 474 }
475 475 }
476 476 }
477 477
478 478 variantSources {
479 479 bind('main') {
480 480 configureSourceSet {
481 481 declareOutputs('types', 'js')
482 482 }
483 483 }
484 484
485 485 whenBound { ctx ->
486 486 ctx.configureSourceSet {
487 487 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
488 488 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
489 489 }
490 490 }
491 491 }
492 492
493 493 variantArtifacts {
494 494 variant('browser') {
495 495 primarySlot('typesPackage') {
496 496 fromVariant {
497 497 output('types')
498 498 }
499 499 }
500 500
501 501 slot('js') {
502 502 fromVariant {
503 503 output('js')
504 504 }
505 505 }
506 506 }
507 507
508 508 whenOutgoingVariant { publication ->
509 509 publication.configureConfiguration {
510 510 attributes.attribute(variantAttr, publication.variantName())
511 511 }
512 512
513 513 publication.primarySlot().configureArtifactAttributes {
514 514 attribute(slotAttr, publication.primarySlot().slotName())
515 515 }
516 516
517 517 publication.requireSlot('js').configureArtifactAttributes {
518 518 attribute(slotAttr, 'js')
519 519 }
520 520 }
521 521 }
522 522 }
523 523
524 524 project(':consumer') {
525 525 configurations {
526 526 compileView {
527 527 canBeResolved = true
528 528 canBeConsumed = false
529 529 canBeDeclared = true
530 530 attributes {
531 531 attribute(variantAttr, 'browser')
532 532 attribute(slotAttr, 'typesPackage')
533 533 }
534 534 }
535 535 }
536 536
537 537 dependencies {
538 538 compileView project(':producer')
539 539 }
540 540
541 541 tasks.register('probe') {
542 542 doLast {
543 543 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
544 544 def jsFiles = configurations.compileView.incoming.artifactView {
545 545 attributes {
546 546 attribute(slotAttr, 'js')
547 547 }
548 548 }.files.files.collect { it.name }.sort().join(',')
549 549
550 550 println('compileFiles=' + compileFiles)
551 551 println('jsFiles=' + jsFiles)
552 552 }
553 553 }
554 554 }
555 555 """.formatted(buildscriptClasspath));
556 556
557 557 BuildResult result = runner(":consumer:probe").build();
558 558
559 559 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
560 560 assertTrue(result.getOutput().contains("jsFiles=js"));
561 561 }
562 562
563 563 private GradleRunner runner(String... arguments) {
564 564 return GradleRunner.create()
565 565 .withProjectDir(testProjectDir.toFile())
566 566 .withPluginClasspath(pluginClasspath())
567 567 .withArguments(arguments)
568 568 .forwardOutput();
569 569 }
570 570
571 571 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
572 572 writeFile(SETTINGS_FILE, ROOT_NAME);
573 573 writeFile(BUILD_FILE, buildScript);
574 574
575 575 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
576 576 var output = ex.getBuildResult().getOutput();
577 577
578 578 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
579 579 }
580 580
581 581 private static List<File> pluginClasspath() {
582 582 try {
583 583 var classesDir = Path.of(VariantArtifactsPlugin.class
584 584 .getProtectionDomain()
585 585 .getCodeSource()
586 586 .getLocation()
587 587 .toURI());
588 588
589 589 var markerResource = VariantArtifactsPlugin.class.getClassLoader()
590 590 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
591 591
592 592 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
593 593
594 594 var markerPath = Path.of(markerResource.toURI());
595 595 var resourcesDir = markerPath.getParent().getParent().getParent();
596 596
597 597 return List.of(classesDir.toFile(), resourcesDir.toFile());
598 598 } catch (Exception e) {
599 599 throw new RuntimeException("Unable to build plugin classpath for test", e);
600 600 }
601 601 }
602 602
603 603 private void writeFile(String relativePath, String content) throws IOException {
604 604 Path path = testProjectDir.resolve(relativePath);
605 605 Files.createDirectories(path.getParent());
606 606 Files.writeString(path, content);
607 607 }
608 608 }
General Comments 0
You need to be logged in to leave comments. Login now