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