##// END OF EJS Templates
Refactor variant artifact slots into contribution-based inputs
cin -
r35:389e9d6c7860 default
parent child
Show More
@@ -1,201 +1,205
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 14
15 15 import javax.inject.Inject;
16 16
17 17 import org.gradle.api.InvalidUserDataException;
18 18 import org.gradle.api.Named;
19 19 import org.gradle.api.NamedDomainObjectContainer;
20 20 import org.gradle.api.Task;
21 21 import org.gradle.api.file.ConfigurableFileCollection;
22 22 import org.gradle.api.file.DirectoryProperty;
23 23 import org.gradle.api.file.FileCollection;
24 24 import org.gradle.api.file.ProjectLayout;
25 25 import org.gradle.api.file.SourceDirectorySet;
26 26 import org.gradle.api.model.ObjectFactory;
27 27 import org.gradle.api.tasks.TaskProvider;
28 28 import org.gradle.util.Configurable;
29 29 import org.implab.gradle.common.core.lang.Closures;
30 30
31 31 import groovy.lang.Closure;
32 32
33 33 /**
34 34 * A configurable source set abstraction with named outputs.
35 35 *
36 36 * <p>
37 37 * Each instance aggregates multiple {@link SourceDirectorySet source sets}
38 38 * under a shared name and exposes typed outputs that must be declared up front.
39 39 * Default locations are {@code src/<name>} for sources and
40 40 * {@code build/<name>} for outputs, both of which can be customized via the
41 41 * exposed {@link DirectoryProperty} setters.
42 42 * </p>
43 43 *
44 44 * <p>
45 45 * Outputs are grouped by names to make task wiring explicit. An output must be
46 46 * declared with {@link #declareOutputs(String, String...)} before files can be
47 47 * registered against it. Attempting to register or retrieve an undeclared
48 48 * output results in
49 49 * {@link InvalidUserDataException}.
50 50 * </p>
51 51 */
52 52 public abstract class GenericSourceSet
53 53 implements Named, Configurable<GenericSourceSet> {
54 54 private final String name;
55 55
56 56 private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets;
57 57
58 58 private final Map<String, ConfigurableFileCollection> outputs;
59 59
60 60 private final FileCollection allOutputs;
61 61
62 62 private final FileCollection allSourceDirectories;
63 63
64 64 private final ObjectFactory objects;
65 65
66 66 private final Set<String> declaredOutputs = new HashSet<>();
67 67
68 68 @Inject
69 69 public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) {
70 70 this.name = name;
71 71 this.objects = objects;
72 72
73 73 sourceDirectorySets = objects.domainObjectContainer(
74 74 SourceDirectorySet.class,
75 75 this::createSourceDirectorySet);
76 76
77 77 outputs = new LinkedHashMap<>();
78 78
79 79 allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider());
80 80
81 81 allOutputs = objects.fileCollection().from(outputsProvider());
82 82
83 83 getSourceSetDir().convention(layout
84 84 .getProjectDirectory()
85 85 .dir(Paths.get("src", name).toString()));
86 86
87 87 getOutputsDir().convention(layout
88 88 .getBuildDirectory()
89 89 .dir(name));
90 90 }
91 91
92 92 @Override
93 93 public String getName() {
94 94 return name;
95 95 }
96 96
97 97 /**
98 98 * Base directory for this source set. Defaults to {@code src/<name>} under
99 99 * the project directory.
100 100 */
101 101 public abstract DirectoryProperty getSourceSetDir();
102 102
103 103 /**
104 104 * Base directory for outputs of this source set. Defaults to
105 105 * {@code build/<name>}.
106 106 */
107 107 public abstract DirectoryProperty getOutputsDir();
108 108
109 109 /**
110 110 * The container of {@link SourceDirectorySet} instances that belong to this
111 111 * logical source set.
112 112 */
113 113 public NamedDomainObjectContainer<SourceDirectorySet> getSets() {
114 114 return sourceDirectorySets;
115 115 }
116 116
117 117 /**
118 118 * All registered outputs grouped across output names.
119 119 */
120 120 public FileCollection getAllOutputs() {
121 121 return allOutputs;
122 122 }
123 123
124 124 /**
125 125 * All source directories from every contained {@link SourceDirectorySet}.
126 126 */
127 127 public FileCollection getAllSourceDirectories() {
128 128 return allSourceDirectories;
129 129 }
130 130
131 131 /**
132 132 * Returns the file collection for the specified output name, creating it
133 133 * if necessary.
134 134 *
135 135 * @throws InvalidUserDataException if the output was not declared
136 136 */
137 public ConfigurableFileCollection output(String name) {
137 public FileCollection output(String name) {
138 return configurableOutput(name);
139 }
140
141 private ConfigurableFileCollection configurableOutput(String name) {
138 142 requireDeclaredOutput(name);
139 143 return outputs.computeIfAbsent(name, key -> objects.fileCollection());
140 144 }
141 145
142 146 /**
143 147 * Declares allowed output names. Outputs must be declared before registering
144 148 * files under them.
145 149 */
146 150 public void declareOutputs(String name, String... extra) {
147 151 declaredOutputs.add(Objects.requireNonNull(name, "declareOutputs: The output name cannot be null"));
148 152 for (var x : extra)
149 153 declaredOutputs.add(Objects.requireNonNull(x, "declareOutputs: The output name cannot be null"));
150 154 }
151 155
152 156 /**
153 157 * Registers files produced elsewhere under the given output.
154 158 */
155 159 public void registerOutput(String name, Object... files) {
156 output(name).from(files);
160 configurableOutput(name).from(files);
157 161 }
158 162
159 163 /**
160 164 * Registers output files produced by a task, using a mapper to extract the
161 165 * output from the task. The task will be added as a build dependency of this
162 166 * output.
163 167 */
164 168 public <T extends Task> void registerOutput(String name, TaskProvider<T> task,
165 169 Function<? super T, ?> mapper) {
166 output(name).from(task.map(mapper::apply))
170 configurableOutput(name).from(task.map(mapper::apply))
167 171 .builtBy(task);
168 172 }
169 173
170 174 /**
171 175 * Applies a Groovy closure to this source set, enabling DSL-style
172 176 * configuration.
173 177 */
174 178 @Override
175 179 public GenericSourceSet configure(Closure configure) {
176 180 Closures.apply(configure, this);
177 181 return this;
178 182 }
179 183
180 184 private SourceDirectorySet createSourceDirectorySet(String name) {
181 185 return objects.sourceDirectorySet(name, name);
182 186 }
183 187
184 188 private void requireDeclaredOutput(String outputName) {
185 189 if (!declaredOutputs.contains(outputName)) {
186 190 throw new InvalidUserDataException(
187 191 "Output '" + outputName + "' is not declared for source set '" + name + "'");
188 192 }
189 193 }
190 194
191 195 private Callable<List<? extends FileCollection>> outputsProvider() {
192 196 return () -> outputs.values().stream().toList();
193 197 }
194 198
195 199 private Callable<Set<File>> sourceDirectoriesProvider() {
196 200 return () -> sourceDirectorySets.stream()
197 201 .flatMap(x -> x.getSrcDirs().stream())
198 202 .collect(Collectors.toSet());
199 203 }
200 204
201 205 }
@@ -1,135 +1,144
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.Optional;
4 4
5 5 import javax.inject.Inject;
6 6
7 7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 8 import org.gradle.api.Action;
9 9 import org.gradle.api.InvalidUserDataException;
10 10 import org.gradle.api.Named;
11 11 import org.gradle.api.NamedDomainObjectContainer;
12 12 import org.implab.gradle.common.core.lang.Closures;
13 13
14 14 import groovy.lang.Closure;
15 15 import groovy.lang.DelegatesTo;
16 16
17 /**
18 * Artifact model for one topology variant declared in
19 * {@link VariantArtifactsExtension}.
20 *
21 * <p>A {@code VariantArtifact} groups one or more
22 * {@link VariantArtifactSlot artifact representation slots}. The primary slot
23 * becomes the main artifact of {@code <variant>Elements}; remaining slots are
24 * published as secondary outgoing variants by {@link VariantArtifactsPlugin}.
25 */
17 26 @NonNullByDefault
18 27 public class VariantArtifact implements Named {
19 28 private final String name;
20 29 private final NamedDomainObjectContainer<VariantArtifactSlot> slots;
21 30 private String primarySlotName;
22 31 private boolean finalized;
23 32
24 33 @Inject
25 34 public VariantArtifact(String name, NamedDomainObjectContainer<VariantArtifactSlot> slots) {
26 35 this.name = normalize(name, "variant artifact name must not be null or blank");
27 36 this.slots = slots;
28 37
29 38 slots.all(slot -> {
30 39 if (finalized)
31 40 throw new InvalidUserDataException(
32 41 "Variant artifact '" + this.name + "' is finalized and cannot add slot '" + slot.getName() + "'");
33 42 });
34 43 }
35 44
36 45 @Override
37 46 public String getName() {
38 47 return name;
39 48 }
40 49
41 50 public NamedDomainObjectContainer<VariantArtifactSlot> getSlots() {
42 51 return slots;
43 52 }
44 53
45 54 public VariantArtifactSlot slot(String name) {
46 55 return slot(name, slot -> {
47 56 });
48 57 }
49 58
50 59 public VariantArtifactSlot slot(String name, Action<? super VariantArtifactSlot> configure) {
51 60 ensureMutable("configure slots");
52 61 var slot = slots.maybeCreate(normalize(name, "slot name must not be null or blank"));
53 62 configure.execute(slot);
54 63 return slot;
55 64 }
56 65
57 66 public VariantArtifactSlot slot(
58 67 String name,
59 68 @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
60 69 return slot(name, Closures.action(configure));
61 70 }
62 71
63 72 public Optional<VariantArtifactSlot> findSlot(String slotName) {
64 73 return Optional.ofNullable(slots.findByName(normalize(slotName, "slot name must not be null or blank")));
65 74 }
66 75
67 76 public void primarySlot(String slotName) {
68 77 ensureMutable("configure primary slot");
69 78 primarySlotName = normalize(slotName, "primary slot name must not be null or blank");
70 79 }
71 80
72 81 public VariantArtifactSlot primarySlot(String slotName, Action<? super VariantArtifactSlot> configure) {
73 82 ensureMutable("configure primary slot");
74 83 var slot = slot(slotName, configure);
75 84 primarySlot(slot.getName());
76 85 return slot;
77 86 }
78 87
79 88 public VariantArtifactSlot primarySlot(
80 89 String slotName,
81 90 @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
82 91 return primarySlot(slotName, Closures.action(configure));
83 92 }
84 93
85 94 public Optional<String> findPrimarySlotName() {
86 95 return Optional.ofNullable(primarySlotName)
87 96 .or(() -> slots.getNames().size() == 1 ? Optional.of(slots.iterator().next().getName()) : Optional.empty());
88 97 }
89 98
90 99 public String requirePrimarySlotName() {
91 100 return findPrimarySlotName()
92 101 .orElseThrow(() -> new InvalidUserDataException(
93 102 "Variant artifact '" + name + "' must declare primary slot because it has multiple slots"));
94 103 }
95 104
96 105 public Optional<VariantArtifactSlot> findPrimarySlot() {
97 106 return findPrimarySlotName().flatMap(this::findSlot);
98 107 }
99 108
100 109 public VariantArtifactSlot requirePrimarySlot() {
101 110 var resolvedPrimarySlotName = requirePrimarySlotName();
102 111 return findSlot(resolvedPrimarySlotName)
103 112 .orElseThrow(() -> new InvalidUserDataException(
104 113 "Variant artifact '" + name + "' declares unknown primary slot '" + resolvedPrimarySlotName + "'"));
105 114 }
106 115
107 116 public VariantArtifactSlot requireSlot(String slotName) {
108 117 var normalizedSlotName = normalize(slotName, "slot name must not be null or blank");
109 118 return Optional.ofNullable(slots.findByName(normalizedSlotName))
110 119 .orElseThrow(() -> new InvalidUserDataException(
111 120 "Variant artifact '" + name + "' doesn't declare slot '" + normalizedSlotName + "'"));
112 121 }
113 122
114 123 void finalizeModel() {
115 124 if (finalized)
116 125 return;
117 126
118 127 for (var slot : slots)
119 128 slot.finalizeModel();
120 129
121 130 finalized = true;
122 131 }
123 132
124 133 static String normalize(String value, String message) {
125 134 return Optional.ofNullable(value)
126 135 .map(String::trim)
127 136 .filter(trimmed -> !trimmed.isEmpty())
128 137 .orElseThrow(() -> new InvalidUserDataException(message));
129 138 }
130 139
131 140 private void ensureMutable(String operation) {
132 141 if (finalized)
133 142 throw new InvalidUserDataException("Variant artifact '" + name + "' is finalized and cannot " + operation);
134 143 }
135 144 }
@@ -1,138 +1,238
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.LinkedHashSet;
4 6 import java.util.List;
7 import java.util.Set;
8 import java.util.function.Consumer;
9 import java.util.function.Predicate;
10 import java.util.stream.Stream;
5 11
6 12 import javax.inject.Inject;
7 13
8 14 import org.eclipse.jdt.annotation.NonNullByDefault;
9 15 import org.gradle.api.Action;
10 16 import org.gradle.api.InvalidUserDataException;
11 17 import org.gradle.api.Named;
12 18 import org.implab.gradle.common.core.lang.Closures;
13 19
14 20 import groovy.lang.Closure;
15 21 import groovy.lang.DelegatesTo;
16 22
23 /**
24 * One artifact representation slot inside {@link VariantArtifact}.
25 *
26 * <p>
27 * The DSL exposed by this type is topology-aware sugar over an internal
28 * contribution model:
29 * <ul>
30 * <li>{@link #from(Object)} adds one direct contribution that does not depend
31 * on {@link VariantSourcesExtension} bindings;</li>
32 * <li>{@link #fromVariant(Action)}, {@link #fromRole(String, Action)} and
33 * {@link #fromLayer(String, Action)} define where a contribution is active in
34 * the variant/role/layer topology;</li>
35 * <li>{@link OutputSelectionSpec#output(String)} defines which named output of
36 * the matched {@link GenericSourceSet} should be added to the slot.</li>
37 * </ul>
38 *
39 * <p>
40 * Internally the slot stores contribution resolvers rather than raw output
41 * names. Each contribution can later materialize itself against the
42 * variant-specific source-set bindings and return:
43 * <ul>
44 * <li>a file notation object suitable for {@code files.from(...)}</li>
45 * <li>a {@link BindingKey} used to deduplicate repeated logical inputs during
46 * materialization</li>
47 * </ul>
48 *
49 * <p>
50 * Validation is intentionally separated from materialization: the slot keeps
51 * topology references in {@link #referencedRoleNames()} and
52 * {@link #referencedLayerNames()}, while the actual contribution pipeline is
53 * exposed through {@link #bindings()}.
54 */
17 55 @NonNullByDefault
18 56 public class VariantArtifactSlot implements Named {
19 57 private final String name;
20 private final List<BindingRule> rules = new ArrayList<>();
58 private final List<BindingResolver> bindings = new ArrayList<>();
59 private final Set<String> referencedRoleNames = new LinkedHashSet<>();
60 private final Set<String> referencedLayerNames = new LinkedHashSet<>();
21 61 private boolean finalized;
22 62
23 63 @Inject
24 64 public VariantArtifactSlot(String name) {
25 65 this.name = VariantArtifact.normalize(name, "slot name must not be null or blank");
26 66 }
27 67
28 68 @Override
29 69 public String getName() {
30 70 return name;
31 71 }
32 72
33 73 public void fromVariant(Action<? super OutputSelectionSpec> configure) {
34 addRules(BindingSelector.variant(), configure);
74 addContributions(context -> true, configure);
35 75 }
36 76
37 77 public void fromVariant(
38 78 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
39 79 fromVariant(Closures.action(configure));
40 80 }
41 81
42 82 public void fromRole(String roleName, Action<? super OutputSelectionSpec> configure) {
43 addRules(BindingSelector.role(VariantArtifact.normalize(roleName, "role name must not be null or blank")),
44 configure);
83 var normalizedRoleName = VariantArtifact.normalize(roleName, "role name must not be null or blank");
84 addContributions(context -> context.roleName().equals(normalizedRoleName), configure);
85 referencedRoleNames.add(normalizedRoleName);
45 86 }
46 87
47 88 public void fromRole(
48 89 String roleName,
49 90 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
50 91 fromRole(roleName, Closures.action(configure));
51 92 }
52 93
53 94 public void fromLayer(String layerName, Action<? super OutputSelectionSpec> configure) {
54 addRules(BindingSelector.layer(VariantArtifact.normalize(layerName, "layer name must not be null or blank")),
55 configure);
95 var normalizedLayerName = VariantArtifact.normalize(layerName, "layer name must not be null or blank");
96 addContributions(context -> context.layerName().equals(normalizedLayerName), configure);
97 referencedLayerNames.add(normalizedLayerName);
56 98 }
57 99
58 100 public void fromLayer(
59 101 String layerName,
60 102 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
61 103 fromLayer(layerName, Closures.action(configure));
62 104 }
63 105
64 List<BindingRule> bindingRules() {
65 return List.copyOf(rules);
106 /**
107 * Adds one direct slot contribution.
108 *
109 * <p>The supplied object is forwarded as-is to {@code files.from(...)}
110 * during slot materialization and does not depend on
111 * {@link VariantSourcesExtension#whenBound(Action)} callbacks.
112 */
113 public void from(Object files) {
114 ensureMutable("configure sources");
115
116 if (files == null)
117 throw new InvalidUserDataException("slot source must not be null");
118
119 var key = BindingKey.newUniqueKey("direct slot input for '" + name + "'");
120 bindings.add((contexts, consumer) -> consumer.accept(new ResolvedBinding(key, files)));
121 }
122
123 List<BindingResolver> bindings() {
124 return List.copyOf(bindings);
125 }
126
127 Set<String> referencedRoleNames() {
128 return Set.copyOf(referencedRoleNames);
129 }
130
131 Set<String> referencedLayerNames() {
132 return Set.copyOf(referencedLayerNames);
66 133 }
67 134
68 135 void finalizeModel() {
69 136 finalized = true;
70 137 }
71 138
72 private void addRules(BindingSelector selector, Action<? super OutputSelectionSpec> configure) {
139 private void addContributions(
140 Predicate<SourceSetUsageBinding> selector,
141 Action<? super OutputSelectionSpec> configure) {
73 142 ensureMutable("configure sources");
74 143
75 144 var spec = new OutputSelectionSpec(selector);
76 145 configure.execute(spec);
77 rules.addAll(spec.rules());
146 spec.accept(bindings::add);
78 147 }
79 148
80 149 private void ensureMutable(String operation) {
81 150 if (finalized)
82 throw new InvalidUserDataException("Variant artifact slot '" + name + "' is finalized and cannot " + operation);
151 throw new InvalidUserDataException(
152 "Variant artifact slot '" + name + "' is finalized and cannot " + operation);
83 153 }
84 154
155 /**
156 * Local DSL buffer for one {@code fromVariant/fromRole/fromLayer} block.
157 *
158 * <p>
159 * The spec accumulates contributions locally and flushes them to the
160 * owning slot only after the outer configure block completes successfully.
161 */
85 162 public final class OutputSelectionSpec {
86 private final BindingSelector selector;
87 private final List<BindingRule> rules = new ArrayList<>();
163 private final Predicate<SourceSetUsageBinding> selector;
164 private final List<BindingResolver> bindings = new ArrayList<>();
88 165
89 private OutputSelectionSpec(BindingSelector selector) {
166 private OutputSelectionSpec(Predicate<SourceSetUsageBinding> selector) {
90 167 this.selector = selector;
91 168 }
92 169
93 170 public void output(String name) {
94 rules.add(new BindingRule(selector,
95 VariantArtifact.normalize(name, "output name must not be null or blank")));
171 var outputName = VariantArtifact.normalize(name, "output name must not be null or blank");
172 bindings.add((contexts, consumer) -> contexts.stream()
173 .filter(selector)
174 .map(context -> resolveOutput(context, outputName))
175 .forEach(consumer));
96 176 }
97 177
98 178 public void output(String name, String... extra) {
99 output(name);
100 for (var item : extra)
101 output(item);
179 Stream.concat(Stream.of(name), Stream.of(extra))
180 .forEach(this::output);
102 181 }
103 182
104 private List<BindingRule> rules() {
105 return List.copyOf(rules);
183 private ResolvedBinding resolveOutput(SourceSetUsageBinding context, String outputName) {
184 var key = new SourceSetOutputKey(context.sourceSetName(), outputName);
185 var files = context.sourceSet().map(sourceSet -> sourceSet.output(outputName));
186 return new ResolvedBinding(key, files);
187 }
188
189 void accept(Consumer<? super BindingResolver> consumer) {
190 bindings.forEach(consumer);
106 191 }
107 192 }
108 193
109 record BindingRule(BindingSelector selector, String outputName) {
110 boolean matches(SourceSetUsageBinding context) {
111 return switch (selector.kind()) {
112 case VARIANT -> true;
113 case ROLE -> selector.value().equals(context.roleName());
114 case LAYER -> selector.value().equals(context.layerName());
194 @FunctionalInterface
195 interface BindingResolver {
196 void resolve(Collection<SourceSetUsageBinding> contexts, Consumer<? super ResolvedBinding> consumer);
197 }
198
199 /**
200 * Materialized slot contribution for one concrete source-set binding.
201 */
202 record ResolvedBinding(BindingKey key, Object files) {
203 }
204
205 /**
206 * Marker key for deduplicating logical slot inputs during materialization.
207 *
208 * <p>
209 * Semantic keys such as {@link SourceSetOutputKey} collapse repeated
210 * references to the same logical output. Identity keys created via
211 * {@link #newUniqueKey()} or {@link #newUniqueKey(String)} can be used by contributions
212 * that must flow through the same pipeline but should never be merged.
213 */
214 interface BindingKey {
215 static BindingKey newUniqueKey(String hint) {
216 return new BindingKey() {
217 @Override
218 public String toString() {
219 return hint;
220 }
115 221 };
116 222 }
223
224 static BindingKey newUniqueKey() {
225 return newUniqueKey("unnamed");
226 }
117 227 }
118 228
119 record BindingSelector(SelectorKind kind, String value) {
120 static BindingSelector variant() {
121 return new BindingSelector(SelectorKind.VARIANT, "");
122 }
123
124 static BindingSelector role(String roleName) {
125 return new BindingSelector(SelectorKind.ROLE, roleName);
126 }
127
128 static BindingSelector layer(String layerName) {
129 return new BindingSelector(SelectorKind.LAYER, layerName);
229 /**
230 * Stable dedupe key for one named output of one resolved source set.
231 */
232 record SourceSetOutputKey(String sourceSetName, String outputName) implements BindingKey {
233 @Override
234 public String toString() {
235 return "sourceSet '" + sourceSetName + "' output '" + outputName + "'";
130 236 }
131 237 }
132
133 enum SelectorKind {
134 VARIANT,
135 ROLE,
136 LAYER
137 }
138 238 }
@@ -1,182 +1,224
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.LinkedHashSet;
5 5 import java.util.List;
6 6 import java.util.Optional;
7 import java.util.Set;
7 8
8 9 import javax.inject.Inject;
9 10
10 11 import org.eclipse.jdt.annotation.NonNullByDefault;
11 12 import org.gradle.api.Action;
12 13 import org.gradle.api.InvalidUserDataException;
13 14 import org.gradle.api.NamedDomainObjectContainer;
14 15 import org.gradle.api.model.ObjectFactory;
15 16 import org.implab.gradle.common.core.lang.Closures;
16 17
17 18 import groovy.lang.Closure;
18 19 import groovy.lang.DelegatesTo;
19 20
21 /**
22 * Root DSL and lifecycle holder for the {@code variantArtifacts} model.
23 *
24 * <p>This extension sits on top of the build topology defined by
25 * {@link BuildVariantsExtension} and the source-set materialization performed by
26 * {@link VariantSourcesExtension}:
27 * <ul>
28 * <li>{@link #variant(String, Action)} declares one outgoing artifact model per
29 * topology variant;</li>
30 * <li>each {@link VariantArtifact} declares one or more
31 * {@link VariantArtifactSlot slots};</li>
32 * <li>after topology finalization this extension validates the artifact model,
33 * freezes it and later receives replayable outgoing-publication callbacks via
34 * {@link #whenOutgoingVariant(Action)}.</li>
35 * </ul>
36 */
20 37 @NonNullByDefault
21 38 public abstract class VariantArtifactsExtension {
22 39 private final NamedDomainObjectContainer<VariantArtifact> variants;
23 40 private final ObjectFactory objects;
24 41 private final List<Action<? super OutgoingVariantPublication>> outgoingVariantActions = new ArrayList<>();
25 42 private final List<OutgoingVariantPublication> outgoingVariants = new ArrayList<>();
26 43 private boolean finalized;
27 44
28 45 @Inject
29 46 public VariantArtifactsExtension(ObjectFactory objects) {
30 47 this.objects = objects;
31 48 variants = objects.domainObjectContainer(VariantArtifact.class, this::newVariantArtifact);
32 49
33 50 variants.all(variant -> {
34 51 if (finalized)
35 52 throw new InvalidUserDataException(
36 53 "variantArtifacts model is finalized and cannot add variant '" + variant.getName() + "'");
37 54 });
38 55 }
39 56
40 57 public NamedDomainObjectContainer<VariantArtifact> getVariants() {
41 58 return variants;
42 59 }
43 60
44 61 public VariantArtifact variant(String name) {
45 62 return variant(name, variant -> {
46 63 });
47 64 }
48 65
49 66 public VariantArtifact variant(String name, Action<? super VariantArtifact> configure) {
50 67 ensureMutable("configure variants");
51 68 var variant = variants.maybeCreate(VariantArtifact.normalize(name, "variant name must not be null or blank"));
52 69 configure.execute(variant);
53 70 return variant;
54 71 }
55 72
56 73 public VariantArtifact variant(
57 74 String name,
58 75 @DelegatesTo(value = VariantArtifact.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
59 76 return variant(name, Closures.action(configure));
60 77 }
61 78
62 79 public Optional<VariantArtifact> findVariant(String variantName) {
63 80 return Optional
64 81 .ofNullable(variants.findByName(VariantArtifact.normalize(variantName, "variant name must not be null or blank")));
65 82 }
66 83
67 84 public VariantArtifact requireVariant(String variantName) {
68 85 var normalizedVariantName = VariantArtifact.normalize(variantName, "variant name must not be null or blank");
69 86 return findVariant(normalizedVariantName)
70 87 .orElseThrow(() -> new InvalidUserDataException(
71 88 "Variant artifacts do not declare variant '" + normalizedVariantName + "'"));
72 89 }
73 90
74 91 public void whenOutgoingVariant(Action<? super OutgoingVariantPublication> action) {
75 92 outgoingVariantActions.add(action);
76 93 for (var publication : outgoingVariants)
77 94 action.execute(publication);
78 95 }
79 96
80 97 public void whenOutgoingVariant(
81 98 @DelegatesTo(value = OutgoingVariantPublication.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
82 99 whenOutgoingVariant(Closures.action(action));
83 100 }
84 101
85 102 public boolean isFinalized() {
86 103 return finalized;
87 104 }
88 105
89 106 void finalizeModel(BuildVariantsExtension topology) {
90 107 if (finalized)
91 108 return;
92 109
93 110 validate(topology);
94 111
95 112 for (var variant : variants)
96 113 variant.finalizeModel();
97 114
98 115 finalized = true;
99 116 }
100 117
101 118 void notifyOutgoingVariant(OutgoingVariantPublication publication) {
102 119 outgoingVariants.add(publication);
103 120 for (var action : outgoingVariantActions)
104 121 action.execute(publication);
105 122 }
106 123
107 124 private VariantArtifact newVariantArtifact(String name) {
108 125 return objects.newInstance(VariantArtifact.class, name, objects.domainObjectContainer(VariantArtifactSlot.class));
109 126 }
110 127
111 128 private void validate(BuildVariantsExtension topology) {
112 129 var errors = new ArrayList<String>();
113 130
114 for (var variantArtifact : variants) {
115 var topologyVariant = topology.find(variantArtifact.getName());
116 if (topologyVariant.isEmpty()) {
117 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
118 + variantArtifact.getName() + "'");
119 continue;
120 }
131 for (var variantArtifact : variants)
132 validateVariantArtifact(topology, variantArtifact, errors);
133
134 throwIfInvalid(errors);
135 }
121 136
122 validateVariantArtifact(variantArtifact, topologyVariant.get(), errors);
137 private static void validateVariantArtifact(
138 BuildVariantsExtension topology,
139 VariantArtifact variantArtifact,
140 List<String> errors) {
141 var topologyVariant = topology.find(variantArtifact.getName());
142 if (topologyVariant.isEmpty()) {
143 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
144 + variantArtifact.getName() + "'");
145 return;
123 146 }
124 147
125 if (!errors.isEmpty()) {
126 var message = new StringBuilder("Invalid variantArtifacts model:");
127 for (var error : errors)
128 message.append("\n - ").append(error);
148 var topologyScope = TopologyScope.from(topologyVariant.get());
149 validateTopologyReferences(variantArtifact, topologyScope, errors);
150 validatePrimarySlot(variantArtifact, errors);
151 }
129 152
130 throw new InvalidUserDataException(message.toString());
153 private static void validateTopologyReferences(
154 VariantArtifact variantArtifact,
155 TopologyScope topologyScope,
156 List<String> errors) {
157 for (var slot : variantArtifact.getSlots()) {
158 validateSlotReferences(variantArtifact, slot, "role", slot.referencedRoleNames(), topologyScope.roleNames(), errors);
159 validateSlotReferences(variantArtifact, slot, "layer", slot.referencedLayerNames(), topologyScope.layerNames(), errors);
131 160 }
132 161 }
133 162
134 private static void validateVariantArtifact(VariantArtifact variantArtifact, BuildVariant topologyVariant, List<String> errors) {
135 var roleNames = new LinkedHashSet<String>();
136 var layerNames = new LinkedHashSet<String>();
163 private static void validatePrimarySlot(VariantArtifact variantArtifact, List<String> errors) {
164 if (variantArtifact.getSlots().isEmpty())
165 return;
137 166
138 for (var role : topologyVariant.getRoles()) {
139 roleNames.add(role.getName());
140 layerNames.addAll(role.getLayers().getOrElse(List.of()));
167 if (variantArtifact.findPrimarySlotName().isEmpty()) {
168 errors.add("Variant artifact '" + variantArtifact.getName()
169 + "' must declare primary slot because it has multiple slots");
170 return;
141 171 }
142 172
143 if (!variantArtifact.getSlots().isEmpty()) {
144 if (variantArtifact.findPrimarySlotName().isEmpty()) {
145 errors.add("Variant artifact '" + variantArtifact.getName()
146 + "' must declare primary slot because it has multiple slots");
147 } else {
148 var primarySlotName = variantArtifact.requirePrimarySlotName();
149 if (variantArtifact.findSlot(primarySlotName).isEmpty()) {
150 errors.add("Variant artifact '" + variantArtifact.getName()
151 + "' declares unknown primary slot '" + primarySlotName + "'");
152 }
173 var primarySlotName = variantArtifact.requirePrimarySlotName();
174 if (variantArtifact.findSlot(primarySlotName).isEmpty()) {
175 errors.add("Variant artifact '" + variantArtifact.getName()
176 + "' declares unknown primary slot '" + primarySlotName + "'");
177 }
178 }
179
180 private static void validateSlotReferences(
181 VariantArtifact variantArtifact,
182 VariantArtifactSlot slot,
183 String referenceKind,
184 Set<String> referencedNames,
185 Set<String> knownNames,
186 List<String> errors) {
187 for (var referencedName : referencedNames) {
188 if (!knownNames.contains(referencedName)) {
189 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
190 + "' references unknown " + referenceKind + " '" + referencedName + "'");
153 191 }
154 192 }
193 }
155 194
156 for (var slot : variantArtifact.getSlots()) {
157 for (var rule : slot.bindingRules()) {
158 switch (rule.selector().kind()) {
159 case VARIANT -> {
160 }
161 case ROLE -> {
162 if (!roleNames.contains(rule.selector().value())) {
163 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
164 + "' references unknown role '" + rule.selector().value() + "'");
165 }
166 }
167 case LAYER -> {
168 if (!layerNames.contains(rule.selector().value())) {
169 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
170 + "' references unknown layer '" + rule.selector().value() + "'");
171 }
172 }
173 }
195 private static void throwIfInvalid(List<String> errors) {
196 if (errors.isEmpty())
197 return;
198
199 var message = new StringBuilder("Invalid variantArtifacts model:");
200 for (var error : errors)
201 message.append("\n - ").append(error);
202
203 throw new InvalidUserDataException(message.toString());
204 }
205
206 private record TopologyScope(Set<String> roleNames, Set<String> layerNames) {
207 private static TopologyScope from(BuildVariant topologyVariant) {
208 var roleNames = new LinkedHashSet<String>();
209 var layerNames = new LinkedHashSet<String>();
210
211 for (var role : topologyVariant.getRoles()) {
212 roleNames.add(role.getName());
213 layerNames.addAll(role.getLayers().getOrElse(List.of()));
174 214 }
215
216 return new TopologyScope(Set.copyOf(roleNames), Set.copyOf(layerNames));
175 217 }
176 218 }
177 219
178 220 private void ensureMutable(String operation) {
179 221 if (finalized)
180 222 throw new InvalidUserDataException("variantArtifacts model is finalized and cannot " + operation);
181 223 }
182 224 }
@@ -1,180 +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 public abstract class VariantsArtifactsPlugin implements Plugin<Project> {
20 private static final Logger logger = Logging.getLogger(VariantsArtifactsPlugin.class);
19 public abstract class VariantArtifactsPlugin implements Plugin<Project> {
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 // Bind variant artifacts resolution to variant sources registration, so that artifact resolution can be performed
36 37 variantSources.whenBound(variantArtifactsResolver::recordBinding);
37 38
38 39 variants.whenFinalized(model -> {
39 40 logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath());
40 41 variantArtifacts.finalizeModel(model);
41 42 materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies);
42 43 logger.debug("variantArtifacts model finalized on project '{}'", target.getPath());
43 44 });
44 45 }
45 46
46 47 public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) {
47 48 var extension = target.getExtensions().findByType(VariantArtifactsExtension.class);
48 49
49 50 if (extension == null) {
50 51 logger.error("variantArtifacts extension '{}' isn't found on project '{}'",
51 52 VARIANT_ARTIFACTS_EXTENSION_NAME,
52 53 target.getPath());
53 54 throw new GradleException("variantArtifacts extension isn't found");
54 55 }
55 56
56 57 logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
57 58
58 59 return extension;
59 60 }
60 61
61 62 private static void materializeOutgoingVariants(
62 63 Project project,
63 64 BuildVariantsExtension topology,
64 65 VariantArtifactsExtension variantArtifacts,
65 66 VariantArtifactsResolver variantArtifactsResolver,
66 67 ArtifactAssemblyRegistry artifactAssemblies) {
67 68 variantArtifacts.getVariants().stream()
68 69 .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty())
69 70 .forEach(variantArtifact -> materializeOutgoingVariant(
70 71 project,
71 72 topology.require(variantArtifact.getName()),
72 73 variantArtifact,
73 74 variantArtifactsResolver,
74 75 artifactAssemblies,
75 76 variantArtifacts));
76 77 }
77 78
78 79 private static void materializeOutgoingVariant(
79 80 Project project,
80 81 BuildVariant topologyVariant,
81 82 VariantArtifact variantArtifact,
82 83 VariantArtifactsResolver variantArtifactsResolver,
83 84 ArtifactAssemblyRegistry artifactAssemblies,
84 85 VariantArtifactsExtension variantArtifacts) {
85 86 var assemblies = variantArtifact.getSlots().stream()
86 87 .collect(Collectors.toMap(
87 88 VariantArtifactSlot::getName,
88 89 slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, slot),
89 90 (left, right) -> left,
90 91 LinkedHashMap::new));
91 92
92 93 var primarySlot = variantArtifact.requirePrimarySlot();
93 94 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), primarySlot.getName());
94 95 var primaryAssembly = assemblies.get(primarySlot.getName());
95 96 publishPrimaryArtifact(configuration, primaryAssembly);
96 97 var primaryPublication = new OutgoingArtifactSlotPublication(
97 98 primarySlot.getName(),
98 99 true,
99 100 primarySlot,
100 101 primaryAssembly,
101 102 configuration);
102 103 var secondarySlots = variantArtifact.getSlots().stream()
103 104 .filter(slot -> !slot.getName().equals(primarySlot.getName()))
104 105 .map(slot -> new SecondarySlot(slot, assemblies.get(slot.getName())))
105 106 .toList();
106 107 var secondaryPublications = new ArrayList<OutgoingArtifactSlotPublication>(secondarySlots.size());
107 108 secondarySlots.forEach(secondarySlot -> {
108 109 var secondaryVariant = configuration.getOutgoing().getVariants().create(secondarySlot.slot().getName());
109 110 publishSecondaryArtifact(secondaryVariant, secondarySlot.assembly());
110 111 secondaryPublications.add(new OutgoingArtifactSlotPublication(
111 112 secondarySlot.slot().getName(),
112 113 false,
113 114 secondarySlot.slot(),
114 115 secondarySlot.assembly(),
115 116 secondaryVariant));
116 117 });
117 118
118 119 var slotPublications = Stream.concat(
119 120 Stream.of(primaryPublication),
120 121 secondaryPublications.stream())
121 122 .toList();
122 123
123 124 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
124 125 variantArtifact.getName(),
125 126 topologyVariant,
126 127 variantArtifact,
127 128 configuration,
128 129 primaryPublication,
129 130 slotPublications));
130 131 }
131 132
132 133 private static ArtifactAssembly registerAssembly(
133 134 Project project,
134 135 VariantArtifactsResolver variantArtifactsResolver,
135 136 ArtifactAssemblyRegistry artifactAssemblies,
136 137 VariantArtifact variantArtifact,
137 138 VariantArtifactSlot slot) {
138 139 return artifactAssemblies.register(
139 140 variantArtifact.getName() + Strings.capitalize(slot.getName()),
140 141 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
141 142 project.getLayout().getBuildDirectory()
142 143 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
143 144 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
144 145 }
145 146
146 147 private static Configuration createOutgoingConfiguration(
147 148 Project project,
148 149 String variantName,
149 150 String primarySlotName) {
150 151 var configName = variantName + "Elements";
151 152 return project.getConfigurations().consumable(configName, config -> {
152 153 config.setVisible(true);
153 154 config.setDescription("Consumable assembled artifacts for variant '" + variantName
154 155 + "' with primary slot '" + primarySlotName + "'");
155 156 }).get();
156 157 }
157 158
158 159 private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) {
159 160 publishArtifact(configuration.getOutgoing(), assembly);
160 161 }
161 162
162 163 private static void publishSecondaryArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
163 164 publishArtifact(variant, assembly);
164 165 }
165 166
166 167 private static void publishArtifact(ConfigurationPublications outgoing, ArtifactAssembly assembly) {
167 168 outgoing.artifact(assembly.getOutput().getSingleFile(), published -> {
168 169 published.builtBy(assembly.getOutput().getBuildDependencies());
169 170 });
170 171 }
171 172
172 173 private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
173 174 variant.artifact(assembly.getOutput().getSingleFile(), published -> {
174 175 published.builtBy(assembly.getOutput().getBuildDependencies());
175 176 });
176 177 }
177 178
178 179 private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) {
179 180 }
180 181 }
@@ -1,58 +1,121
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.LinkedHashSet;
5 5 import java.util.List;
6 6 import java.util.Set;
7 7
8 8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 9 import org.gradle.api.file.ConfigurableFileCollection;
10 10 import org.gradle.api.file.FileCollection;
11 11 import org.gradle.api.model.ObjectFactory;
12 import org.implab.gradle.common.sources.VariantArtifactSlot.BindingKey;
12 13
14 /**
15 * Resolves artifact-slot inputs from already bound variant source-set usages.
16 *
17 * <p>This type is the bridge between two models:
18 * <ul>
19 * <li>{@link VariantSourcesExtension}, which emits resolved
20 * {@link SourceSetUsageBinding variant/role/layer -> source-set} bindings;</li>
21 * <li>{@link VariantArtifactSlot}, which exposes a DSL over slot contributions
22 * and stores the resulting {@link VariantArtifactSlot.BindingResolver
23 * contribution resolvers}.</li>
24 * </ul>
25 *
26 * <p>The resolver records each emitted {@link SourceSetUsageBinding} and later
27 * materializes a {@link FileCollection} for one concrete variant/slot pair.
28 * For each variant/slot pair it asks the slot to materialize its contributions,
29 * passes in the resolved source-set bindings for that variant, and
30 * deduplicates resulting inputs by {@link BindingKey}. Contributions that do
31 * not depend on topology bindings can still emit direct inputs even when that
32 * binding collection is empty. The returned files are then typically wired into
33 * an {@link ArtifactAssembly} as its sources.
34 *
35 * <p>Direct clients are infrastructure code rather than build scripts. The
36 * typical usage pattern is:
37 * <ol>
38 * <li>create one resolver per project;</li>
39 * <li>subscribe {@link #recordBinding(SourceSetUsageBinding)} to
40 * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)};</li>
41 * <li>call {@link #files(String, VariantArtifactSlot)} while registering an
42 * {@link ArtifactAssembly} or another consumer that needs the slot inputs.</li>
43 * </ol>
44 *
45 * <p>Build-script users normally do not instantiate this class directly. They
46 * configure {@code variantArtifacts}, and {@link VariantArtifactsPlugin} uses
47 * this resolver internally to turn slot rules into assembly inputs.
48 */
13 49 @NonNullByDefault
14 50 public final class VariantArtifactsResolver {
15 51 private final ObjectFactory objects;
16 52 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
17 53
18 54 public VariantArtifactsResolver(ObjectFactory objects) {
19 55 this.objects = objects;
20 56 }
21 57
58 /**
59 * Records one resolved variant source-set usage.
60 *
61 * <p>Intended to be used as a callback target for
62 * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)}.
63 *
64 * @param context resolved variant/role/layer usage bound to a source set
65 */
22 66 public void recordBinding(SourceSetUsageBinding context) {
23 67 boundContexts.add(context);
24 68 }
25 69
70 /**
71 * Returns all source-set outputs selected by the given slot for the given
72 * variant.
73 *
74 * <p>The result is built from recorded {@link SourceSetUsageBinding}
75 * instances whose {@link SourceSetUsageBinding#variantName()} matches
76 * {@code variantName}. Each matching binding is then fed into the slot
77 * contribution pipeline; if multiple contributions resolve to the same
78 * {@link BindingKey}, that source is included only once.
79 *
80 * <p>This method does not validate the model; validation is expected to be
81 * performed earlier by {@link VariantArtifactsExtension}. Unknown variants
82 * or slots with no matching rules simply produce an empty collection.
83 *
84 * @param variantName variant whose bound source-set usages should be scanned
85 * @param slot slot definition that selects which outputs should be included
86 * @return lazily wired file collection for the selected outputs
87 */
26 88 public FileCollection files(String variantName, VariantArtifactSlot slot) {
27 var files = objects.fileCollection();
28 var boundOutputs = new LinkedHashSet<String>();
89 var builder = new FileCollectionBuilder();
90 var contexts = boundContexts.stream()
91 .filter(context -> variantName.equals(context.variantName()))
92 .toList();
29 93
30 boundContexts.stream()
31 .filter(context -> variantName.equals(context.variantName()))
32 .forEach(context -> bindMatchingOutputs(files, boundOutputs, slot, context));
94 slot.bindings().forEach(binding -> binding.resolve(contexts, builder::addOutput));
33 95
34 return files;
96 return builder.build();
35 97 }
36 98
37 private static void bindMatchingOutputs(
38 ConfigurableFileCollection files,
39 Set<String> boundOutputs,
40 VariantArtifactSlot slot,
41 SourceSetUsageBinding context) {
42 slot.bindingRules().stream()
43 .filter(rule -> rule.matches(context))
44 .forEach(rule -> bindOutput(files, boundOutputs, context, rule.outputName()));
99 /**
100 * Local materialization helper for one {@link #files(String, VariantArtifactSlot)}
101 * call.
102 */
103 class FileCollectionBuilder {
104 private final ConfigurableFileCollection files;
105 private final Set<BindingKey> boundOutputs = new LinkedHashSet<>();
106
107 FileCollectionBuilder() {
108 this.files = objects.fileCollection();
109 }
110
111 FileCollection build() {
112 return files;
113 }
114
115 void addOutput(VariantArtifactSlot.ResolvedBinding binding) {
116 if (boundOutputs.add(binding.key()))
117 files.from(binding.files());
118 }
45 119 }
46 120
47 private static void bindOutput(
48 ConfigurableFileCollection files,
49 Set<String> boundOutputs,
50 SourceSetUsageBinding context,
51 String outputName) {
52 var key = context.sourceSetName() + "\u0000" + outputName;
53 if (!boundOutputs.add(key))
54 return;
55
56 files.from(context.sourceSet().map(sourceSet -> sourceSet.output(outputName)));
57 }
58 121 }
@@ -1,1 +1,1
1 implementation-class=org.implab.gradle.common.sources.VariantsArtifactsPlugin
1 implementation-class=org.implab.gradle.common.sources.VariantArtifactsPlugin
@@ -1,503 +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 99 slotPublication.configureAssembly {
100 100 sources.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 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
190 writeFile(SETTINGS_FILE, ROOT_NAME);
191 writeFile("inputs/bundle.js", "console.log('bundle')\n");
192 writeFile(BUILD_FILE, """
193 plugins {
194 id 'org.implab.gradle-variants-artifacts'
195 }
196
197 variants {
198 layer('main')
199
200 variant('browser') {
201 role('main') {
202 layers('main')
203 }
204 }
205 }
206
207 variantArtifacts {
208 variant('browser') {
209 primarySlot('bundle') {
210 from(layout.projectDirectory.file('inputs/bundle.js'))
211 }
212 }
213 }
214
215 tasks.register('probe') {
216 dependsOn 'processBrowserBundle'
217
218 doLast {
219 def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
220 assert new File(bundleDir, 'bundle.js').exists()
221 println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
222 }
223 }
224 """);
225
226 BuildResult result = runner("probe").build();
227
228 assertTrue(result.getOutput().contains("primary=bundle"));
229 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
230 }
231
232 @Test
233 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
234 writeFile(SETTINGS_FILE, ROOT_NAME);
235 writeFile("inputs/base.js", "console.log('base')\n");
236 writeFile("inputs/marker.txt", "marker\n");
237 writeFile(BUILD_FILE, """
238 plugins {
239 id 'org.implab.gradle-variants-artifacts'
240 }
241
242 variants {
243 layer('main')
244
245 variant('browser') {
246 role('main') {
247 layers('main')
248 }
249 }
250 }
251
252 variantSources {
253 bind('main') {
254 configureSourceSet {
255 declareOutputs('js')
256 }
257 }
258
259 whenBound { ctx ->
260 ctx.configureSourceSet {
261 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
262 }
263 }
264 }
265
266 variantArtifacts {
267 variant('browser') {
268 primarySlot('bundle') {
269 fromVariant {
270 output('js')
271 }
272 from(layout.projectDirectory.file('inputs/marker.txt'))
273 }
274 }
275 }
276
277 tasks.register('probe') {
278 dependsOn 'processBrowserBundle'
279
280 doLast {
281 def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
282 assert new File(bundleDir, 'base.js').exists()
283 assert new File(bundleDir, 'marker.txt').exists()
284 }
285 }
286 """);
287
288 BuildResult result = runner("probe").build();
289
290 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
291 }
292
293 @Test
189 294 void failsOnUnknownVariantReference() throws Exception {
190 295 assertBuildFails("""
191 296 plugins {
192 297 id 'org.implab.gradle-variants-artifacts'
193 298 }
194 299
195 300 variants {
196 301 layer('main')
197 302 }
198 303
199 304 variantArtifacts {
200 305 variant('browser') {
201 306 slot('mainJs') {
202 307 fromVariant {
203 308 output('js')
204 309 }
205 310 }
206 311 }
207 312 }
208 313 """, "Variant artifact 'browser' references unknown variant 'browser'");
209 314 }
210 315
211 316 @Test
212 317 void failsOnUnknownRoleReference() throws Exception {
213 318 assertBuildFails("""
214 319 plugins {
215 320 id 'org.implab.gradle-variants-artifacts'
216 321 }
217 322
218 323 variants {
219 324 layer('main')
220 325
221 326 variant('browser') {
222 327 role('main') {
223 328 layers('main')
224 329 }
225 330 }
226 331 }
227 332
228 333 variantArtifacts {
229 334 variant('browser') {
230 335 slot('mainJs') {
231 336 fromRole('test') {
232 337 output('js')
233 338 }
234 339 }
235 340 }
236 341 }
237 342 """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'");
238 343 }
239 344
240 345 @Test
241 346 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
242 347 assertBuildFails("""
243 348 plugins {
244 349 id 'org.implab.gradle-variants-artifacts'
245 350 }
246 351
247 352 variants {
248 353 layer('main')
249 354
250 355 variant('browser') {
251 356 role('main') {
252 357 layers('main')
253 358 }
254 359 }
255 360 }
256 361
257 362 variantArtifacts {
258 363 variant('browser') {
259 364 slot('typesPackage') {
260 365 fromVariant {
261 366 output('types')
262 367 }
263 368 }
264 369
265 370 slot('js') {
266 371 fromVariant {
267 372 output('js')
268 373 }
269 374 }
270 375 }
271 376 }
272 377 """, "Variant artifact 'browser' must declare primary slot because it has multiple slots");
273 378 }
274 379
275 380 @Test
276 381 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
277 382 assertBuildFails("""
278 383 plugins {
279 384 id 'org.implab.gradle-variants-artifacts'
280 385 }
281 386
282 387 variants {
283 388 layer('mainBase')
284 389 layer('extra')
285 390
286 391 variant('browser') {
287 392 role('main') {
288 393 layers('mainBase')
289 394 }
290 395 }
291 396 }
292 397
293 398 variantArtifacts {
294 399 variant('browser') {
295 400 slot('extraJs') {
296 401 fromLayer('extra') {
297 402 output('js')
298 403 }
299 404 }
300 405 }
301 406 }
302 407 """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'");
303 408 }
304 409
305 410 @Test
306 411 void failsOnLateMutationAfterFinalize() throws Exception {
307 412 assertBuildFails("""
308 413 plugins {
309 414 id 'org.implab.gradle-variants-artifacts'
310 415 }
311 416
312 417 variants {
313 418 layer('main')
314 419
315 420 variant('browser') {
316 421 role('main') {
317 422 layers('main')
318 423 }
319 424 }
320 425 }
321 426
322 427 afterEvaluate {
323 428 variantArtifacts.variant('late') {
324 429 slot('js') {
325 430 fromVariant {
326 431 output('js')
327 432 }
328 433 }
329 434 }
330 435 }
331 436 """, "variantArtifacts model is finalized and cannot configure variants");
332 437 }
333 438
334 439 @Test
335 440 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
336 441 writeFile(SETTINGS_FILE, """
337 442 rootProject.name = 'variants-artifacts-fixture'
338 443 include 'producer', 'consumer'
339 444 """);
340 445 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
341 446 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
342 447 var buildscriptClasspath = pluginClasspath().stream()
343 448 .map(File::getAbsolutePath)
344 449 .map(path -> "'" + path.replace("\\", "\\\\") + "'")
345 450 .collect(Collectors.joining(", "));
346 451 writeFile(BUILD_FILE, """
347 452 buildscript {
348 453 dependencies {
349 454 classpath files(%s)
350 455 }
351 456 }
352 457
353 458 import org.gradle.api.attributes.Attribute
354 459
355 460 def variantAttr = Attribute.of('test.variant', String)
356 461 def slotAttr = Attribute.of('test.slot', String)
357 462
358 463 subprojects {
359 464 apply plugin: 'org.implab.gradle-variants-artifacts'
360 465 }
361 466
362 467 project(':producer') {
363 468 variants {
364 469 layer('main')
365 470
366 471 variant('browser') {
367 472 role('main') {
368 473 layers('main')
369 474 }
370 475 }
371 476 }
372 477
373 478 variantSources {
374 479 bind('main') {
375 480 configureSourceSet {
376 481 declareOutputs('types', 'js')
377 482 }
378 483 }
379 484
380 485 whenBound { ctx ->
381 486 ctx.configureSourceSet {
382 487 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
383 488 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
384 489 }
385 490 }
386 491 }
387 492
388 493 variantArtifacts {
389 494 variant('browser') {
390 495 primarySlot('typesPackage') {
391 496 fromVariant {
392 497 output('types')
393 498 }
394 499 }
395 500
396 501 slot('js') {
397 502 fromVariant {
398 503 output('js')
399 504 }
400 505 }
401 506 }
402 507
403 508 whenOutgoingVariant { publication ->
404 509 publication.configureConfiguration {
405 510 attributes.attribute(variantAttr, publication.variantName())
406 511 }
407 512
408 513 publication.primarySlot().configureArtifactAttributes {
409 514 attribute(slotAttr, publication.primarySlot().slotName())
410 515 }
411 516
412 517 publication.requireSlot('js').configureArtifactAttributes {
413 518 attribute(slotAttr, 'js')
414 519 }
415 520 }
416 521 }
417 522 }
418 523
419 524 project(':consumer') {
420 525 configurations {
421 526 compileView {
422 527 canBeResolved = true
423 528 canBeConsumed = false
424 529 canBeDeclared = true
425 530 attributes {
426 531 attribute(variantAttr, 'browser')
427 532 attribute(slotAttr, 'typesPackage')
428 533 }
429 534 }
430 535 }
431 536
432 537 dependencies {
433 538 compileView project(':producer')
434 539 }
435 540
436 541 tasks.register('probe') {
437 542 doLast {
438 543 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
439 544 def jsFiles = configurations.compileView.incoming.artifactView {
440 545 attributes {
441 546 attribute(slotAttr, 'js')
442 547 }
443 548 }.files.files.collect { it.name }.sort().join(',')
444 549
445 550 println('compileFiles=' + compileFiles)
446 551 println('jsFiles=' + jsFiles)
447 552 }
448 553 }
449 554 }
450 555 """.formatted(buildscriptClasspath));
451 556
452 557 BuildResult result = runner(":consumer:probe").build();
453 558
454 559 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
455 560 assertTrue(result.getOutput().contains("jsFiles=js"));
456 561 }
457 562
458 563 private GradleRunner runner(String... arguments) {
459 564 return GradleRunner.create()
460 565 .withProjectDir(testProjectDir.toFile())
461 566 .withPluginClasspath(pluginClasspath())
462 567 .withArguments(arguments)
463 568 .forwardOutput();
464 569 }
465 570
466 571 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
467 572 writeFile(SETTINGS_FILE, ROOT_NAME);
468 573 writeFile(BUILD_FILE, buildScript);
469 574
470 575 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
471 576 var output = ex.getBuildResult().getOutput();
472 577
473 578 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
474 579 }
475 580
476 581 private static List<File> pluginClasspath() {
477 582 try {
478 var classesDir = Path.of(VariantsArtifactsPlugin.class
583 var classesDir = Path.of(VariantArtifactsPlugin.class
479 584 .getProtectionDomain()
480 585 .getCodeSource()
481 586 .getLocation()
482 587 .toURI());
483 588
484 var markerResource = VariantsArtifactsPlugin.class.getClassLoader()
589 var markerResource = VariantArtifactsPlugin.class.getClassLoader()
485 590 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
486 591
487 592 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
488 593
489 594 var markerPath = Path.of(markerResource.toURI());
490 595 var resourcesDir = markerPath.getParent().getParent().getParent();
491 596
492 597 return List.of(classesDir.toFile(), resourcesDir.toFile());
493 598 } catch (Exception e) {
494 599 throw new RuntimeException("Unable to build plugin classpath for test", e);
495 600 }
496 601 }
497 602
498 603 private void writeFile(String relativePath, String content) throws IOException {
499 604 Path path = testProjectDir.resolve(relativePath);
500 605 Files.createDirectories(path.getParent());
501 606 Files.writeString(path, content);
502 607 }
503 608 }
@@ -1,311 +1,354
1 1 # Variant Artifacts Plugin
2 2
3 3 ## NAME
4 4
5 5 `VariantsArtifactsPlugin` и extension `variantArtifacts`.
6 6
7 7 ## SYNOPSIS
8 8
9 9 ```groovy
10 10 import org.gradle.api.attributes.Attribute
11 11
12 12 plugins {
13 13 id 'org.implab.gradle-variants-artifacts'
14 14 }
15 15
16 16 def variantAttr = Attribute.of('test.variant', String)
17 17 def slotAttr = Attribute.of('test.slot', String)
18 18
19 19 variants {
20 20 layer('main')
21 21
22 22 variant('browser') {
23 23 role('main') { layers('main') }
24 24 }
25 25 }
26 26
27 27 variantSources {
28 28 bind('main') {
29 29 configureSourceSet {
30 30 declareOutputs('types', 'js', 'resources')
31 31 }
32 32 }
33 33 }
34 34
35 35 variantArtifacts {
36 36 variant('browser') {
37 37 primarySlot('typesPackage') {
38 38 fromVariant {
39 39 output('types')
40 40 }
41 41 }
42 42
43 43 slot('js') {
44 44 fromVariant {
45 45 output('js')
46 46 }
47 47 }
48 48
49 49 slot('resources') {
50 50 fromVariant {
51 51 output('resources')
52 52 }
53 53 }
54 54 }
55 55
56 56 whenOutgoingVariant { publication ->
57 57 publication.configureConfiguration {
58 58 attributes.attribute(variantAttr, publication.variantName())
59 59 }
60 60
61 61 publication.primarySlot().configureArtifactAttributes {
62 62 attribute(slotAttr, publication.primarySlot().slotName())
63 63 }
64 64
65 65 publication.requireSlot('js').configureArtifactAttributes {
66 66 attribute(slotAttr, 'js')
67 67 }
68 68
69 69 publication.requireSlot('resources').configureArtifactAttributes {
70 70 attribute(slotAttr, 'resources')
71 71 }
72 72 }
73 73 }
74 74 ```
75 75
76 76 ## DESCRIPTION
77 77
78 78 `VariantsArtifactsPlugin` применяет `VariantsSourcesPlugin`, затем строит
79 79 outgoing publication model поверх `variantSources`.
80 80
81 81 ### publication model
82 82
83 83 Для каждого `variantArtifacts.variant('<name>')` публикуется один outgoing
84 84 build variant:
85 85
86 86 - primary configuration `<variant>Elements`;
87 87 - primary artifact slot на самой configuration;
88 88 - secondary variants внутри `configuration.outgoing.variants` для остальных slots.
89 89
90 90 Пример:
91 91
92 92 - `browserElements`
93 93 - primary slot: `typesPackage`
94 94 - secondary variants: `js`, `resources`
95 95
96 96 Это разделяет:
97 97
98 98 - graph selection build variant-а;
99 99 - artifact selection внутри уже выбранного variant-а.
100 100
101 ### slot bindings
101 ### slot contributions и DSL
102
103 `slot('<name>')` описывает artifact representation не как один файл или одну
104 задачу, а как набор contributions, которые потом materialize-ятся в отдельный
105 `ArtifactAssembly`.
106
107 Текущий DSL поддерживает два вида contributions:
102 108
103 `slot('<name>')` описывает, какие outputs из `variantSources` войдут в artifact
104 representation этого slot-а.
109 - topology-aware:
110 - `fromVariant { output(...) }`
111 - `fromRole('<role>') { output(...) }`
112 - `fromLayer('<layer>') { output(...) }`
113 - direct:
114 - `from(someFileOrProviderOrTaskOutput)`
115
116 Смысл DSL по слоям:
105 117
106 Binding rules:
118 - `fromVariant/fromRole/fromLayer` выбирают область topology model, в которой
119 contribution активен;
120 - `output(...)` выбирает named output соответствующего `GenericSourceSet`;
121 - `from(Object)` добавляет direct contribution, не зависящий от
122 `variantSources` bindings;
123 - итоговый contribution при materialization:
124 - проверяет, подходит ли текущий `SourceSetUsageBinding`;
125 - выдает object для `files.from(...)`;
126 - при необходимости выдает `BindingKey`, если такой contribution должен
127 схлопываться по logical identity.
107 128
108 - `fromVariant { output(...) }`
109 - `fromRole('<role>') { output(...) }`
110 - `fromLayer('<layer>') { output(...) }`
129 Связь slot-а с остальной моделью:
130
131 - `variants` задает topology variant/role/layer;
132 - `variantSources` превращает topology в concrete `SourceSetUsageBinding`;
133 - `variantArtifacts.slot(...)` описывает, какие bindings надо включить в slot;
134 - `VariantArtifactsResolver` превращает contributions slot-а в `FileCollection`;
135 - `VariantArtifactsPlugin` регистрирует для slot-а отдельный `ArtifactAssembly`;
136 - `OutgoingVariantPublication` и `OutgoingArtifactSlotPublication` публикуют
137 уже собранные slot artifacts наружу.
111 138
112 139 Каждый slot materialize-ится в отдельный `ArtifactAssembly`:
113 140
114 141 - task: `process<Variant><Slot>`;
115 142 - output dir: `build/variant-artifacts/<variant>/<slot>`.
116 143
117 144 ### primary slot
118 145
119 146 Primary slot задает artifact, который публикуется как основной artifact
120 147 configuration `<variant>Elements`.
121 148
122 149 Формы DSL:
123 150
124 151 ```groovy
125 152 variant('browser') {
126 153 primarySlot('typesPackage')
127 154
128 155 slot('typesPackage') {
129 156 fromVariant { output('types') }
130 157 }
131 158 }
132 159 ```
133 160
134 161 или sugar:
135 162
136 163 ```groovy
137 164 variant('browser') {
138 165 primarySlot('typesPackage') {
139 166 fromVariant { output('types') }
140 167 }
141 168 }
142 169 ```
143 170
144 171 Правила:
145 172
146 173 - если slot один, он считается primary неявно;
147 174 - если slots несколько, `primarySlot(...)` обязателен;
148 175 - `primarySlot` должен ссылаться на существующий slot.
149 176
150 177 ## LIFECYCLE
151 178
152 179 - `VariantsArtifactsPlugin` ждет `variants.whenFinalized(...)`;
153 180 - после этого валидирует `variantArtifacts`;
154 181 - регистрирует `ArtifactAssembly` по каждому slot;
155 182 - materialize-ит outgoing publications;
156 183 - вызывает `whenOutgoingVariant(...)`;
157 184 - callbacks replayable.
158 185
159 186 После finalize мутации `variantArtifacts` запрещены.
160 187
161 188 ## EVENTS
162 189
163 190 ### whenOutgoingVariant
164 191
165 192 Replayable callback на готовую outgoing publication variant-а.
166 193
167 194 Подходит для:
168 195
169 196 - настройки общих attributes build variant-а один раз;
170 197 - настройки per-slot artifact attributes;
171 198 - доконфигурации `ArtifactAssembly`.
172 199
173 200 ## PAYLOAD TYPES
174 201
175 202 ### OutgoingVariantPublication
176 203
177 204 Содержит:
178 205
179 206 - `variantName()`;
180 207 - `topologyVariant()`;
181 208 - `variantArtifact()`;
182 209 - `configuration()` — primary `<variant>Elements`;
183 210 - `primarySlot()`;
184 211 - `slots()` — все slot publications;
185 212 - `secondarySlots()`;
186 213 - `findSlot(name)`, `requireSlot(name)`.
187 214
188 215 Sugar:
189 216
190 217 - `configureConfiguration(Action|Closure)`.
191 218
192 219 ### OutgoingArtifactSlotPublication
193 220
194 221 Содержит:
195 222
196 223 - `slotName()`;
197 224 - `primary()`;
198 225 - `slot()` — модель `VariantArtifactSlot`;
199 226 - `assembly()`.
200 227
201 228 Sugar:
202 229
203 230 - `configureAssembly(Action|Closure)`;
204 231 - `configureArtifactAttributes(Action|Closure)`.
205 232
206 233 `configureArtifactAttributes(...)` пишет attributes:
207 234
208 235 - в `Configuration.attributes` для primary slot;
209 236 - в `ConfigurationVariant.attributes` для secondary slot.
210 237
211 238 ## CONSUMER SIDE
212 239
213 240 ### primary resolution
214 241
215 242 Обычное inter-project resolution выбирает primary artifact `<variant>Elements`.
216 243
217 244 Пример:
218 245
219 246 ```groovy
220 247 configurations {
221 248 compileView {
222 249 canBeResolved = true
223 250 canBeConsumed = false
224 251 canBeDeclared = true
225 252 attributes {
226 253 attribute(variantAttr, 'browser')
227 254 attribute(slotAttr, 'typesPackage')
228 255 }
229 256 }
230 257 }
231 258
232 259 dependencies {
233 260 compileView project(':producer')
234 261 }
235 262 ```
236 263
237 264 ### artifact selection for secondary slots
238 265
239 266 Secondary artifacts выбираются через `artifactView`.
240 267
241 268 ```groovy
242 269 def jsFiles = configurations.compileView.incoming.artifactView {
243 270 attributes {
244 271 attribute(slotAttr, 'js')
245 272 }
246 273 }.files
247 274 ```
248 275
249 276 Здесь graph variant уже выбран, а `artifactView` выбирает нужный secondary
250 277 artifact representation.
251 278
252 279 ## VALIDATION
253 280
254 281 Проверяется:
255 282
256 283 - variant существует в topology model;
257 - slot bindings не ссылаются на неизвестные role/layer;
284 - slot contributions не ссылаются на неизвестные role/layer;
258 285 - при нескольких slots указан `primarySlot`;
259 286 - `primarySlot` ссылается на существующий slot.
260 287
261 288 ## API
262 289
263 290 ### VariantArtifactsExtension
264 291
265 292 - `variant(String)` — получить/создать variant artifact model;
266 293 - `variant(String, Action|Closure)` — сконфигурировать variant artifact;
267 294 - `getVariants()` — контейнер variant artifacts;
268 295 - `findVariant(name)`, `requireVariant(name)`;
269 296 - `whenOutgoingVariant(...)`.
270 297
271 298 ### VariantArtifact
272 299
273 300 - `slot(String)` — получить/создать slot;
274 301 - `slot(String, Action|Closure)` — сконфигурировать slot;
275 302 - `primarySlot(String)` — назначить primary slot;
276 303 - `primarySlot(String, Action|Closure)` — sugar: configure slot + mark as primary;
277 304 - `getSlots()`;
278 305 - `findSlot(name)`, `requireSlot(name)`;
279 306 - `findPrimarySlotName()`, `requirePrimarySlotName()`;
280 307 - `findPrimarySlot()`, `requirePrimarySlot()`.
281 308
282 309 ### VariantArtifactSlot
283 310
311 - `from(Object)`;
284 312 - `fromVariant(...)`;
285 313 - `fromRole(String, ...)`;
286 314 - `fromLayer(String, ...)`.
287 315
316 Внутренняя модель:
317
318 - slot хранит contributions, а не строковые rules;
319 - `fromVariant/fromRole/fromLayer` создают topology-aware contributions;
320 - `from(Object)` создает direct contribution, который materialize-ится даже
321 если у variant-а нет ни одного `SourceSetUsageBinding`;
322 - slot отдельно хранит topology references для validation:
323 `referencedRoleNames()` и `referencedLayerNames()`.
324
288 325 ### OutputSelectionSpec
289 326
290 327 - `output(name)`;
291 328 - `output(name, extra...)`.
292 329
330 `OutputSelectionSpec` это внутренний DSL-buffer для одного блока
331 `fromVariant/fromRole/fromLayer`. Он локально накапливает contributions и
332 передает их в slot только после успешного завершения configure-блока.
333
293 334 ## KEY CLASSES
294 335
295 336 - `VariantsArtifactsPlugin` — plugin adapter и materialization outgoing variants.
296 337 - `VariantArtifactsExtension` — root DSL и lifecycle.
297 338 - `VariantArtifact` — outgoing build variant model.
298 339 - `VariantArtifactSlot` — artifact representation slot.
340 - `VariantArtifactsResolver` — adapter между `variantSources` bindings и
341 contribution model slot-а.
299 342 - `OutgoingVariantPublication` — payload variant-level publication callback.
300 343 - `OutgoingArtifactSlotPublication` — payload per-slot publication callback.
301 344 - `ArtifactAssembly` — assembled files for a slot.
302 345
303 346 ## NOTES
304 347
305 348 - `common` не навязывает доменную логику выбора primary slot.
306 349 - `common` не фиксирует значения `usage`, `libraryelements` и прочих
307 350 slot-specific attributes.
308 351 - `common` не смешивает эту модель с отдельными publish осями вроде package
309 352 metadata.
310 353 - Closure callbacks используют delegate-first; для вложенных closure удобнее
311 354 явный параметр (`publication -> ...`, `slotPublication -> ...`).
General Comments 0
You need to be logged in to leave comments. Login now