##// 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 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 }
@@ -153,7 +157,7 public abstract class GenericSourceSet
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 /**
@@ -163,7 +167,7 public abstract class GenericSourceSet
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
@@ -14,6 +14,15 import org.implab.gradle.common.core.lan
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;
@@ -1,7 +1,13
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
@@ -14,10 +20,44 import org.implab.gradle.common.core.lan
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
@@ -31,7 +71,7 public class VariantArtifactSlot impleme
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(
@@ -40,8 +80,9 public class VariantArtifactSlot impleme
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(
@@ -51,8 +92,9 public class VariantArtifactSlot impleme
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(
@@ -61,78 +103,136 public class VariantArtifactSlot impleme
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 238 }
138 }
@@ -4,6 +4,7 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
@@ -17,6 +18,22 import org.implab.gradle.common.core.lan
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;
@@ -111,27 +128,83 public abstract class VariantArtifactsEx
111 128 private void validate(BuildVariantsExtension topology) {
112 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 141 var topologyVariant = topology.find(variantArtifact.getName());
116 142 if (topologyVariant.isEmpty()) {
117 143 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
118 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 199 var message = new StringBuilder("Invalid variantArtifacts model:");
127 200 for (var error : errors)
128 201 message.append("\n - ").append(error);
129 202
130 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 208 var roleNames = new LinkedHashSet<String>();
136 209 var layerNames = new LinkedHashSet<String>();
137 210
@@ -140,38 +213,7 public abstract class VariantArtifactsEx
140 213 layerNames.addAll(role.getLayers().getOrElse(List.of()));
141 214 }
142 215
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 }
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 }
216 return new TopologyScope(Set.copyOf(roleNames), Set.copyOf(layerNames));
175 217 }
176 218 }
177 219
@@ -16,8 +16,8 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
@@ -33,6 +33,7 public abstract class VariantsArtifactsP
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 -> {
@@ -9,7 +9,43 import org.eclipse.jdt.annotation.NonNul
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;
@@ -19,40 +55,67 public final class VariantArtifactsResol
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));
95
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 112 return files;
35 113 }
36 114
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()));
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 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 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 {
@@ -475,13 +580,13 class VariantsArtifactsPluginFunctionalT
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");
@@ -98,16 +98,43 build variant:
98 98 - graph selection build variant-а;
99 99 - artifact selection внутри уже выбранного variant-а.
100 100
101 ### slot bindings
101 ### slot contributions и DSL
102 102
103 `slot('<name>')` описывает, какие outputs из `variantSources` войдут в artifact
104 representation этого slot-а.
103 `slot('<name>')` описывает artifact representation не как один файл или одну
104 задачу, а как набор contributions, которые потом materialize-ятся в отдельный
105 `ArtifactAssembly`.
105 106
106 Binding rules:
107 Текущий DSL поддерживает два вида contributions:
107 108
109 - topology-aware:
108 110 - `fromVariant { output(...) }`
109 111 - `fromRole('<role>') { output(...) }`
110 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 139 Каждый slot materialize-ится в отдельный `ArtifactAssembly`:
113 140
@@ -254,7 +281,7 artifact representation.
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
@@ -281,21 +308,37 artifact representation.
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.
General Comments 0
You need to be logged in to leave comments. Login now