##// END OF EJS Templates
Refactor variant artifact slots into contribution-based inputs
cin -
r35:389e9d6c7860 default
parent child
Show More
@@ -134,7 +134,11 public abstract class GenericSourceSet
134 *
134 *
135 * @throws InvalidUserDataException if the output was not declared
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 requireDeclaredOutput(name);
142 requireDeclaredOutput(name);
139 return outputs.computeIfAbsent(name, key -> objects.fileCollection());
143 return outputs.computeIfAbsent(name, key -> objects.fileCollection());
140 }
144 }
@@ -153,7 +157,7 public abstract class GenericSourceSet
153 * Registers files produced elsewhere under the given output.
157 * Registers files produced elsewhere under the given output.
154 */
158 */
155 public void registerOutput(String name, Object... files) {
159 public void registerOutput(String name, Object... files) {
156 output(name).from(files);
160 configurableOutput(name).from(files);
157 }
161 }
158
162
159 /**
163 /**
@@ -163,7 +167,7 public abstract class GenericSourceSet
163 */
167 */
164 public <T extends Task> void registerOutput(String name, TaskProvider<T> task,
168 public <T extends Task> void registerOutput(String name, TaskProvider<T> task,
165 Function<? super T, ?> mapper) {
169 Function<? super T, ?> mapper) {
166 output(name).from(task.map(mapper::apply))
170 configurableOutput(name).from(task.map(mapper::apply))
167 .builtBy(task);
171 .builtBy(task);
168 }
172 }
169
173
@@ -14,6 +14,15 import org.implab.gradle.common.core.lan
14 import groovy.lang.Closure;
14 import groovy.lang.Closure;
15 import groovy.lang.DelegatesTo;
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 @NonNullByDefault
26 @NonNullByDefault
18 public class VariantArtifact implements Named {
27 public class VariantArtifact implements Named {
19 private final String name;
28 private final String name;
@@ -1,7 +1,13
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.common.sources;
2
2
3 import java.util.ArrayList;
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.LinkedHashSet;
4 import java.util.List;
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 import javax.inject.Inject;
12 import javax.inject.Inject;
7
13
@@ -14,10 +20,44 import org.implab.gradle.common.core.lan
14 import groovy.lang.Closure;
20 import groovy.lang.Closure;
15 import groovy.lang.DelegatesTo;
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 @NonNullByDefault
55 @NonNullByDefault
18 public class VariantArtifactSlot implements Named {
56 public class VariantArtifactSlot implements Named {
19 private final String name;
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 private boolean finalized;
61 private boolean finalized;
22
62
23 @Inject
63 @Inject
@@ -31,7 +71,7 public class VariantArtifactSlot impleme
31 }
71 }
32
72
33 public void fromVariant(Action<? super OutputSelectionSpec> configure) {
73 public void fromVariant(Action<? super OutputSelectionSpec> configure) {
34 addRules(BindingSelector.variant(), configure);
74 addContributions(context -> true, configure);
35 }
75 }
36
76
37 public void fromVariant(
77 public void fromVariant(
@@ -40,8 +80,9 public class VariantArtifactSlot impleme
40 }
80 }
41
81
42 public void fromRole(String roleName, Action<? super OutputSelectionSpec> configure) {
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")),
83 var normalizedRoleName = VariantArtifact.normalize(roleName, "role name must not be null or blank");
44 configure);
84 addContributions(context -> context.roleName().equals(normalizedRoleName), configure);
85 referencedRoleNames.add(normalizedRoleName);
45 }
86 }
46
87
47 public void fromRole(
88 public void fromRole(
@@ -51,8 +92,9 public class VariantArtifactSlot impleme
51 }
92 }
52
93
53 public void fromLayer(String layerName, Action<? super OutputSelectionSpec> configure) {
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")),
95 var normalizedLayerName = VariantArtifact.normalize(layerName, "layer name must not be null or blank");
55 configure);
96 addContributions(context -> context.layerName().equals(normalizedLayerName), configure);
97 referencedLayerNames.add(normalizedLayerName);
56 }
98 }
57
99
58 public void fromLayer(
100 public void fromLayer(
@@ -61,78 +103,136 public class VariantArtifactSlot impleme
61 fromLayer(layerName, Closures.action(configure));
103 fromLayer(layerName, Closures.action(configure));
62 }
104 }
63
105
64 List<BindingRule> bindingRules() {
106 /**
65 return List.copyOf(rules);
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 void finalizeModel() {
135 void finalizeModel() {
69 finalized = true;
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 ensureMutable("configure sources");
142 ensureMutable("configure sources");
74
143
75 var spec = new OutputSelectionSpec(selector);
144 var spec = new OutputSelectionSpec(selector);
76 configure.execute(spec);
145 configure.execute(spec);
77 rules.addAll(spec.rules());
146 spec.accept(bindings::add);
78 }
147 }
79
148
80 private void ensureMutable(String operation) {
149 private void ensureMutable(String operation) {
81 if (finalized)
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 public final class OutputSelectionSpec {
162 public final class OutputSelectionSpec {
86 private final BindingSelector selector;
163 private final Predicate<SourceSetUsageBinding> selector;
87 private final List<BindingRule> rules = new ArrayList<>();
164 private final List<BindingResolver> bindings = new ArrayList<>();
88
165
89 private OutputSelectionSpec(BindingSelector selector) {
166 private OutputSelectionSpec(Predicate<SourceSetUsageBinding> selector) {
90 this.selector = selector;
167 this.selector = selector;
91 }
168 }
92
169
93 public void output(String name) {
170 public void output(String name) {
94 rules.add(new BindingRule(selector,
171 var outputName = VariantArtifact.normalize(name, "output name must not be null or blank");
95 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 public void output(String name, String... extra) {
178 public void output(String name, String... extra) {
99 output(name);
179 Stream.concat(Stream.of(name), Stream.of(extra))
100 for (var item : extra)
180 .forEach(this::output);
101 output(item);
102 }
181 }
103
182
104 private List<BindingRule> rules() {
183 private ResolvedBinding resolveOutput(SourceSetUsageBinding context, String outputName) {
105 return List.copyOf(rules);
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) {
194 @FunctionalInterface
110 boolean matches(SourceSetUsageBinding context) {
195 interface BindingResolver {
111 return switch (selector.kind()) {
196 void resolve(Collection<SourceSetUsageBinding> contexts, Consumer<? super ResolvedBinding> consumer);
112 case VARIANT -> true;
197 }
113 case ROLE -> selector.value().equals(context.roleName());
198
114 case LAYER -> selector.value().equals(context.layerName());
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) {
229 /**
120 static BindingSelector variant() {
230 * Stable dedupe key for one named output of one resolved source set.
121 return new BindingSelector(SelectorKind.VARIANT, "");
231 */
122 }
232 record SourceSetOutputKey(String sourceSetName, String outputName) implements BindingKey {
123
233 @Override
124 static BindingSelector role(String roleName) {
234 public String toString() {
125 return new BindingSelector(SelectorKind.ROLE, roleName);
235 return "sourceSet '" + sourceSetName + "' output '" + outputName + "'";
126 }
127
128 static BindingSelector layer(String layerName) {
129 return new BindingSelector(SelectorKind.LAYER, layerName);
130 }
236 }
131 }
237 }
132
133 enum SelectorKind {
134 VARIANT,
135 ROLE,
136 LAYER
137 }
238 }
138 }
@@ -4,6 +4,7 import java.util.ArrayList;
4 import java.util.LinkedHashSet;
4 import java.util.LinkedHashSet;
5 import java.util.List;
5 import java.util.List;
6 import java.util.Optional;
6 import java.util.Optional;
7 import java.util.Set;
7
8
8 import javax.inject.Inject;
9 import javax.inject.Inject;
9
10
@@ -17,6 +18,22 import org.implab.gradle.common.core.lan
17 import groovy.lang.Closure;
18 import groovy.lang.Closure;
18 import groovy.lang.DelegatesTo;
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 @NonNullByDefault
37 @NonNullByDefault
21 public abstract class VariantArtifactsExtension {
38 public abstract class VariantArtifactsExtension {
22 private final NamedDomainObjectContainer<VariantArtifact> variants;
39 private final NamedDomainObjectContainer<VariantArtifact> variants;
@@ -111,27 +128,83 public abstract class VariantArtifactsEx
111 private void validate(BuildVariantsExtension topology) {
128 private void validate(BuildVariantsExtension topology) {
112 var errors = new ArrayList<String>();
129 var errors = new ArrayList<String>();
113
130
114 for (var variantArtifact : variants) {
131 for (var variantArtifact : variants)
132 validateVariantArtifact(topology, variantArtifact, errors);
133
134 throwIfInvalid(errors);
135 }
136
137 private static void validateVariantArtifact(
138 BuildVariantsExtension topology,
139 VariantArtifact variantArtifact,
140 List<String> errors) {
115 var topologyVariant = topology.find(variantArtifact.getName());
141 var topologyVariant = topology.find(variantArtifact.getName());
116 if (topologyVariant.isEmpty()) {
142 if (topologyVariant.isEmpty()) {
117 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
143 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
118 + variantArtifact.getName() + "'");
144 + variantArtifact.getName() + "'");
119 continue;
145 return;
146 }
147
148 var topologyScope = TopologyScope.from(topologyVariant.get());
149 validateTopologyReferences(variantArtifact, topologyScope, errors);
150 validatePrimarySlot(variantArtifact, errors);
151 }
152
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);
160 }
120 }
161 }
121
162
122 validateVariantArtifact(variantArtifact, topologyVariant.get(), errors);
163 private static void validatePrimarySlot(VariantArtifact variantArtifact, List<String> errors) {
164 if (variantArtifact.getSlots().isEmpty())
165 return;
166
167 if (variantArtifact.findPrimarySlotName().isEmpty()) {
168 errors.add("Variant artifact '" + variantArtifact.getName()
169 + "' must declare primary slot because it has multiple slots");
170 return;
171 }
172
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 }
123 }
178 }
124
179
125 if (!errors.isEmpty()) {
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 + "'");
191 }
192 }
193 }
194
195 private static void throwIfInvalid(List<String> errors) {
196 if (errors.isEmpty())
197 return;
198
126 var message = new StringBuilder("Invalid variantArtifacts model:");
199 var message = new StringBuilder("Invalid variantArtifacts model:");
127 for (var error : errors)
200 for (var error : errors)
128 message.append("\n - ").append(error);
201 message.append("\n - ").append(error);
129
202
130 throw new InvalidUserDataException(message.toString());
203 throw new InvalidUserDataException(message.toString());
131 }
204 }
132 }
133
205
134 private static void validateVariantArtifact(VariantArtifact variantArtifact, BuildVariant topologyVariant, List<String> errors) {
206 private record TopologyScope(Set<String> roleNames, Set<String> layerNames) {
207 private static TopologyScope from(BuildVariant topologyVariant) {
135 var roleNames = new LinkedHashSet<String>();
208 var roleNames = new LinkedHashSet<String>();
136 var layerNames = new LinkedHashSet<String>();
209 var layerNames = new LinkedHashSet<String>();
137
210
@@ -140,38 +213,7 public abstract class VariantArtifactsEx
140 layerNames.addAll(role.getLayers().getOrElse(List.of()));
213 layerNames.addAll(role.getLayers().getOrElse(List.of()));
141 }
214 }
142
215
143 if (!variantArtifact.getSlots().isEmpty()) {
216 return new TopologyScope(Set.copyOf(roleNames), Set.copyOf(layerNames));
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 }
153 }
154 }
155
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 }
174 }
175 }
217 }
176 }
218 }
177
219
@@ -16,8 +16,8 import org.gradle.api.logging.Logger;
16 import org.gradle.api.logging.Logging;
16 import org.gradle.api.logging.Logging;
17 import org.implab.gradle.common.core.lang.Strings;
17 import org.implab.gradle.common.core.lang.Strings;
18
18
19 public abstract class VariantsArtifactsPlugin implements Plugin<Project> {
19 public abstract class VariantArtifactsPlugin implements Plugin<Project> {
20 private static final Logger logger = Logging.getLogger(VariantsArtifactsPlugin.class);
20 private static final Logger logger = Logging.getLogger(VariantArtifactsPlugin.class);
21 public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";
21 public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";
22
22
23 @Override
23 @Override
@@ -33,6 +33,7 public abstract class VariantsArtifactsP
33 var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
33 var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
34 var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks());
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 variantSources.whenBound(variantArtifactsResolver::recordBinding);
37 variantSources.whenBound(variantArtifactsResolver::recordBinding);
37
38
38 variants.whenFinalized(model -> {
39 variants.whenFinalized(model -> {
@@ -9,7 +9,43 import org.eclipse.jdt.annotation.NonNul
9 import org.gradle.api.file.ConfigurableFileCollection;
9 import org.gradle.api.file.ConfigurableFileCollection;
10 import org.gradle.api.file.FileCollection;
10 import org.gradle.api.file.FileCollection;
11 import org.gradle.api.model.ObjectFactory;
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 @NonNullByDefault
49 @NonNullByDefault
14 public final class VariantArtifactsResolver {
50 public final class VariantArtifactsResolver {
15 private final ObjectFactory objects;
51 private final ObjectFactory objects;
@@ -19,40 +55,67 public final class VariantArtifactsResol
19 this.objects = objects;
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 public void recordBinding(SourceSetUsageBinding context) {
66 public void recordBinding(SourceSetUsageBinding context) {
23 boundContexts.add(context);
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 public FileCollection files(String variantName, VariantArtifactSlot slot) {
88 public FileCollection files(String variantName, VariantArtifactSlot slot) {
27 var files = objects.fileCollection();
89 var builder = new FileCollectionBuilder();
28 var boundOutputs = new LinkedHashSet<String>();
90 var contexts = boundContexts.stream()
91 .filter(context -> variantName.equals(context.variantName()))
92 .toList();
29
93
30 boundContexts.stream()
94 slot.bindings().forEach(binding -> binding.resolve(contexts, builder::addOutput));
31 .filter(context -> variantName.equals(context.variantName()))
95
32 .forEach(context -> bindMatchingOutputs(files, boundOutputs, slot, context));
96 return builder.build();
97 }
33
98
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() {
34 return files;
112 return files;
35 }
113 }
36
114
37 private static void bindMatchingOutputs(
115 void addOutput(VariantArtifactSlot.ResolvedBinding binding) {
38 ConfigurableFileCollection files,
116 if (boundOutputs.add(binding.key()))
39 Set<String> boundOutputs,
117 files.from(binding.files());
40 VariantArtifactSlot slot,
118 }
41 SourceSetUsageBinding context) {
42 slot.bindingRules().stream()
43 .filter(rule -> rule.matches(context))
44 .forEach(rule -> bindOutput(files, boundOutputs, context, rule.outputName()));
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 }
121 }
58 }
@@ -1,1 +1,1
1 implementation-class=org.implab.gradle.common.sources.VariantsArtifactsPlugin
1 implementation-class=org.implab.gradle.common.sources.VariantArtifactsPlugin
@@ -186,6 +186,111 class VariantsArtifactsPluginFunctionalT
186 }
186 }
187
187
188 @Test
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 void failsOnUnknownVariantReference() throws Exception {
294 void failsOnUnknownVariantReference() throws Exception {
190 assertBuildFails("""
295 assertBuildFails("""
191 plugins {
296 plugins {
@@ -475,13 +580,13 class VariantsArtifactsPluginFunctionalT
475
580
476 private static List<File> pluginClasspath() {
581 private static List<File> pluginClasspath() {
477 try {
582 try {
478 var classesDir = Path.of(VariantsArtifactsPlugin.class
583 var classesDir = Path.of(VariantArtifactsPlugin.class
479 .getProtectionDomain()
584 .getProtectionDomain()
480 .getCodeSource()
585 .getCodeSource()
481 .getLocation()
586 .getLocation()
482 .toURI());
587 .toURI());
483
588
484 var markerResource = VariantsArtifactsPlugin.class.getClassLoader()
589 var markerResource = VariantArtifactsPlugin.class.getClassLoader()
485 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
590 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
486
591
487 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
592 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
@@ -98,16 +98,43 build variant:
98 - graph selection build variant-а;
98 - graph selection build variant-а;
99 - artifact selection внутри уже выбранного variant-а.
99 - artifact selection внутри уже выбранного variant-а.
100
100
101 ### slot bindings
101 ### slot contributions и DSL
102
102
103 `slot('<name>')` описывает, какие outputs из `variantSources` войдут в artifact
103 `slot('<name>')` описывает artifact representation не как один файл или одну
104 representation этого slot-а.
104 задачу, а как набор contributions, которые потом materialize-ятся в отдельный
105 `ArtifactAssembly`.
105
106
106 Binding rules:
107 Текущий DSL поддерживает два вида contributions:
107
108
109 - topology-aware:
108 - `fromVariant { output(...) }`
110 - `fromVariant { output(...) }`
109 - `fromRole('<role>') { output(...) }`
111 - `fromRole('<role>') { output(...) }`
110 - `fromLayer('<layer>') { output(...) }`
112 - `fromLayer('<layer>') { output(...) }`
113 - direct:
114 - `from(someFileOrProviderOrTaskOutput)`
115
116 Смысл DSL по слоям:
117
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.
128
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 Каждый slot materialize-ится в отдельный `ArtifactAssembly`:
139 Каждый slot materialize-ится в отдельный `ArtifactAssembly`:
113
140
@@ -254,7 +281,7 artifact representation.
254 Проверяется:
281 Проверяется:
255
282
256 - variant существует в topology model;
283 - variant существует в topology model;
257 - slot bindings не ссылаются на неизвестные role/layer;
284 - slot contributions не ссылаются на неизвестные role/layer;
258 - при нескольких slots указан `primarySlot`;
285 - при нескольких slots указан `primarySlot`;
259 - `primarySlot` ссылается на существующий slot.
286 - `primarySlot` ссылается на существующий slot.
260
287
@@ -281,21 +308,37 artifact representation.
281
308
282 ### VariantArtifactSlot
309 ### VariantArtifactSlot
283
310
311 - `from(Object)`;
284 - `fromVariant(...)`;
312 - `fromVariant(...)`;
285 - `fromRole(String, ...)`;
313 - `fromRole(String, ...)`;
286 - `fromLayer(String, ...)`.
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 ### OutputSelectionSpec
325 ### OutputSelectionSpec
289
326
290 - `output(name)`;
327 - `output(name)`;
291 - `output(name, extra...)`.
328 - `output(name, extra...)`.
292
329
330 `OutputSelectionSpec` это внутренний DSL-buffer для одного блока
331 `fromVariant/fromRole/fromLayer`. Он локально накапливает contributions и
332 передает их в slot только после успешного завершения configure-блока.
333
293 ## KEY CLASSES
334 ## KEY CLASSES
294
335
295 - `VariantsArtifactsPlugin` — plugin adapter и materialization outgoing variants.
336 - `VariantsArtifactsPlugin` — plugin adapter и materialization outgoing variants.
296 - `VariantArtifactsExtension` — root DSL и lifecycle.
337 - `VariantArtifactsExtension` — root DSL и lifecycle.
297 - `VariantArtifact` — outgoing build variant model.
338 - `VariantArtifact` — outgoing build variant model.
298 - `VariantArtifactSlot` — artifact representation slot.
339 - `VariantArtifactSlot` — artifact representation slot.
340 - `VariantArtifactsResolver` — adapter между `variantSources` bindings и
341 contribution model slot-а.
299 - `OutgoingVariantPublication` — payload variant-level publication callback.
342 - `OutgoingVariantPublication` — payload variant-level publication callback.
300 - `OutgoingArtifactSlotPublication` — payload per-slot publication callback.
343 - `OutgoingArtifactSlotPublication` — payload per-slot publication callback.
301 - `ArtifactAssembly` — assembled files for a slot.
344 - `ArtifactAssembly` — assembled files for a slot.
General Comments 0
You need to be logged in to leave comments. Login now