##// END OF EJS Templates
Refactor variantArtifacts to variant-level publications with primary and secondary slots
cin -
r34:5ec65d9e5a34 default
parent child
Show More
@@ -0,0 +1,66
1 package org.implab.gradle.common.sources;
2
3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 import org.gradle.api.Action;
5 import org.gradle.api.attributes.AttributeContainer;
6 import org.gradle.api.attributes.HasConfigurableAttributes;
7 import org.implab.gradle.common.core.lang.Closures;
8
9 import groovy.lang.Closure;
10 import groovy.lang.DelegatesTo;
11
12 @NonNullByDefault
13 public final class OutgoingArtifactSlotPublication {
14 private final String slotName;
15 private final boolean primary;
16 private final VariantArtifactSlot slot;
17 private final ArtifactAssembly assembly;
18 private final HasConfigurableAttributes<?> attributesCarrier;
19
20 OutgoingArtifactSlotPublication(
21 String slotName,
22 boolean primary,
23 VariantArtifactSlot slot,
24 ArtifactAssembly assembly,
25 HasConfigurableAttributes<?> attributesCarrier) {
26 this.slotName = slotName;
27 this.primary = primary;
28 this.slot = slot;
29 this.assembly = assembly;
30 this.attributesCarrier = attributesCarrier;
31 }
32
33 public String slotName() {
34 return slotName;
35 }
36
37 public boolean primary() {
38 return primary;
39 }
40
41 public VariantArtifactSlot slot() {
42 return slot;
43 }
44
45 public ArtifactAssembly assembly() {
46 return assembly;
47 }
48
49 public void configureAssembly(Action<? super ArtifactAssembly> action) {
50 action.execute(assembly);
51 }
52
53 public void configureAssembly(
54 @DelegatesTo(value = ArtifactAssembly.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
55 configureAssembly(Closures.action(action));
56 }
57
58 public void configureArtifactAttributes(Action<? super AttributeContainer> action) {
59 attributesCarrier.attributes(action);
60 }
61
62 public void configureArtifactAttributes(
63 @DelegatesTo(value = AttributeContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
64 configureArtifactAttributes(Closures.action(action));
65 }
66 }
@@ -0,0 +1,311
1 # Variant Artifacts Plugin
2
3 ## NAME
4
5 `VariantsArtifactsPlugin` ΠΈ extension `variantArtifacts`.
6
7 ## SYNOPSIS
8
9 ```groovy
10 import org.gradle.api.attributes.Attribute
11
12 plugins {
13 id 'org.implab.gradle-variants-artifacts'
14 }
15
16 def variantAttr = Attribute.of('test.variant', String)
17 def slotAttr = Attribute.of('test.slot', String)
18
19 variants {
20 layer('main')
21
22 variant('browser') {
23 role('main') { layers('main') }
24 }
25 }
26
27 variantSources {
28 bind('main') {
29 configureSourceSet {
30 declareOutputs('types', 'js', 'resources')
31 }
32 }
33 }
34
35 variantArtifacts {
36 variant('browser') {
37 primarySlot('typesPackage') {
38 fromVariant {
39 output('types')
40 }
41 }
42
43 slot('js') {
44 fromVariant {
45 output('js')
46 }
47 }
48
49 slot('resources') {
50 fromVariant {
51 output('resources')
52 }
53 }
54 }
55
56 whenOutgoingVariant { publication ->
57 publication.configureConfiguration {
58 attributes.attribute(variantAttr, publication.variantName())
59 }
60
61 publication.primarySlot().configureArtifactAttributes {
62 attribute(slotAttr, publication.primarySlot().slotName())
63 }
64
65 publication.requireSlot('js').configureArtifactAttributes {
66 attribute(slotAttr, 'js')
67 }
68
69 publication.requireSlot('resources').configureArtifactAttributes {
70 attribute(slotAttr, 'resources')
71 }
72 }
73 }
74 ```
75
76 ## DESCRIPTION
77
78 `VariantsArtifactsPlugin` примСняСт `VariantsSourcesPlugin`, Π·Π°Ρ‚Π΅ΠΌ строит
79 outgoing publication model ΠΏΠΎΠ²Π΅Ρ€Ρ… `variantSources`.
80
81 ### publication model
82
83 Для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ `variantArtifacts.variant('<name>')` публикуСтся ΠΎΠ΄ΠΈΠ½ outgoing
84 build variant:
85
86 - primary configuration `<variant>Elements`;
87 - primary artifact slot на самой configuration;
88 - secondary variants Π²Π½ΡƒΡ‚Ρ€ΠΈ `configuration.outgoing.variants` для ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… slots.
89
90 ΠŸΡ€ΠΈΠΌΠ΅Ρ€:
91
92 - `browserElements`
93 - primary slot: `typesPackage`
94 - secondary variants: `js`, `resources`
95
96 Π­Ρ‚ΠΎ раздСляСт:
97
98 - graph selection build variant-Π°;
99 - artifact selection Π²Π½ΡƒΡ‚Ρ€ΠΈ ΡƒΠΆΠ΅ Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ variant-Π°.
100
101 ### slot bindings
102
103 `slot('<name>')` описываСт, ΠΊΠ°ΠΊΠΈΠ΅ outputs ΠΈΠ· `variantSources` Π²ΠΎΠΉΠ΄ΡƒΡ‚ Π² artifact
104 representation этого slot-Π°.
105
106 Binding rules:
107
108 - `fromVariant { output(...) }`
109 - `fromRole('<role>') { output(...) }`
110 - `fromLayer('<layer>') { output(...) }`
111
112 ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ slot materialize-ится Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ `ArtifactAssembly`:
113
114 - task: `process<Variant><Slot>`;
115 - output dir: `build/variant-artifacts/<variant>/<slot>`.
116
117 ### primary slot
118
119 Primary slot Π·Π°Π΄Π°Π΅Ρ‚ artifact, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ публикуСтся ΠΊΠ°ΠΊ основной artifact
120 configuration `<variant>Elements`.
121
122 Π€ΠΎΡ€ΠΌΡ‹ DSL:
123
124 ```groovy
125 variant('browser') {
126 primarySlot('typesPackage')
127
128 slot('typesPackage') {
129 fromVariant { output('types') }
130 }
131 }
132 ```
133
134 ΠΈΠ»ΠΈ sugar:
135
136 ```groovy
137 variant('browser') {
138 primarySlot('typesPackage') {
139 fromVariant { output('types') }
140 }
141 }
142 ```
143
144 ΠŸΡ€Π°Π²ΠΈΠ»Π°:
145
146 - Ссли slot ΠΎΠ΄ΠΈΠ½, ΠΎΠ½ считаСтся primary нСявно;
147 - Ссли slots нСсколько, `primarySlot(...)` обязатСлСн;
148 - `primarySlot` Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΡΡ‹Π»Π°Ρ‚ΡŒΡΡ Π½Π° ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ slot.
149
150 ## LIFECYCLE
151
152 - `VariantsArtifactsPlugin` ΠΆΠ΄Π΅Ρ‚ `variants.whenFinalized(...)`;
153 - послС этого Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅Ρ‚ `variantArtifacts`;
154 - рСгистрируСт `ArtifactAssembly` ΠΏΠΎ ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡƒ slot;
155 - materialize-ΠΈΡ‚ outgoing publications;
156 - Π²Ρ‹Π·Ρ‹Π²Π°Π΅Ρ‚ `whenOutgoingVariant(...)`;
157 - callbacks replayable.
158
159 ПослС finalize ΠΌΡƒΡ‚Π°Ρ†ΠΈΠΈ `variantArtifacts` Π·Π°ΠΏΡ€Π΅Ρ‰Π΅Π½Ρ‹.
160
161 ## EVENTS
162
163 ### whenOutgoingVariant
164
165 Replayable callback Π½Π° Π³ΠΎΡ‚ΠΎΠ²ΡƒΡŽ outgoing publication variant-Π°.
166
167 ΠŸΠΎΠ΄Ρ…ΠΎΠ΄ΠΈΡ‚ для:
168
169 - настройки ΠΎΠ±Ρ‰ΠΈΡ… attributes build variant-Π° ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·;
170 - настройки per-slot artifact attributes;
171 - Π΄ΠΎΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ `ArtifactAssembly`.
172
173 ## PAYLOAD TYPES
174
175 ### OutgoingVariantPublication
176
177 Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΡ‚:
178
179 - `variantName()`;
180 - `topologyVariant()`;
181 - `variantArtifact()`;
182 - `configuration()` β€” primary `<variant>Elements`;
183 - `primarySlot()`;
184 - `slots()` β€” всС slot publications;
185 - `secondarySlots()`;
186 - `findSlot(name)`, `requireSlot(name)`.
187
188 Sugar:
189
190 - `configureConfiguration(Action|Closure)`.
191
192 ### OutgoingArtifactSlotPublication
193
194 Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΡ‚:
195
196 - `slotName()`;
197 - `primary()`;
198 - `slot()` β€” модСль `VariantArtifactSlot`;
199 - `assembly()`.
200
201 Sugar:
202
203 - `configureAssembly(Action|Closure)`;
204 - `configureArtifactAttributes(Action|Closure)`.
205
206 `configureArtifactAttributes(...)` ΠΏΠΈΡˆΠ΅Ρ‚ attributes:
207
208 - в `Configuration.attributes` для primary slot;
209 - в `ConfigurationVariant.attributes` для secondary slot.
210
211 ## CONSUMER SIDE
212
213 ### primary resolution
214
215 ΠžΠ±Ρ‹Ρ‡Π½ΠΎΠ΅ inter-project resolution Π²Ρ‹Π±ΠΈΡ€Π°Π΅Ρ‚ primary artifact `<variant>Elements`.
216
217 ΠŸΡ€ΠΈΠΌΠ΅Ρ€:
218
219 ```groovy
220 configurations {
221 compileView {
222 canBeResolved = true
223 canBeConsumed = false
224 canBeDeclared = true
225 attributes {
226 attribute(variantAttr, 'browser')
227 attribute(slotAttr, 'typesPackage')
228 }
229 }
230 }
231
232 dependencies {
233 compileView project(':producer')
234 }
235 ```
236
237 ### artifact selection for secondary slots
238
239 Secondary artifacts Π²Ρ‹Π±ΠΈΡ€Π°ΡŽΡ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· `artifactView`.
240
241 ```groovy
242 def jsFiles = configurations.compileView.incoming.artifactView {
243 attributes {
244 attribute(slotAttr, 'js')
245 }
246 }.files
247 ```
248
249 Π—Π΄Π΅ΡΡŒ graph variant ΡƒΠΆΠ΅ Π²Ρ‹Π±Ρ€Π°Π½, Π° `artifactView` Π²Ρ‹Π±ΠΈΡ€Π°Π΅Ρ‚ Π½ΡƒΠΆΠ½Ρ‹ΠΉ secondary
250 artifact representation.
251
252 ## VALIDATION
253
254 ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ΡΡ:
255
256 - variant сущСствуСт Π² topology model;
257 - slot bindings Π½Π΅ ΡΡΡ‹Π»Π°ΡŽΡ‚ΡΡ Π½Π° нСизвСстныС role/layer;
258 - ΠΏΡ€ΠΈ Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… slots ΡƒΠΊΠ°Π·Π°Π½ `primarySlot`;
259 - `primarySlot` ссылаСтся Π½Π° ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ slot.
260
261 ## API
262
263 ### VariantArtifactsExtension
264
265 - `variant(String)` β€” ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ/ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ variant artifact model;
266 - `variant(String, Action|Closure)` β€” ΡΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ variant artifact;
267 - `getVariants()` β€” ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ variant artifacts;
268 - `findVariant(name)`, `requireVariant(name)`;
269 - `whenOutgoingVariant(...)`.
270
271 ### VariantArtifact
272
273 - `slot(String)` β€” ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ/ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ slot;
274 - `slot(String, Action|Closure)` β€” ΡΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ slot;
275 - `primarySlot(String)` β€” Π½Π°Π·Π½Π°Ρ‡ΠΈΡ‚ΡŒ primary slot;
276 - `primarySlot(String, Action|Closure)` β€” sugar: configure slot + mark as primary;
277 - `getSlots()`;
278 - `findSlot(name)`, `requireSlot(name)`;
279 - `findPrimarySlotName()`, `requirePrimarySlotName()`;
280 - `findPrimarySlot()`, `requirePrimarySlot()`.
281
282 ### VariantArtifactSlot
283
284 - `fromVariant(...)`;
285 - `fromRole(String, ...)`;
286 - `fromLayer(String, ...)`.
287
288 ### OutputSelectionSpec
289
290 - `output(name)`;
291 - `output(name, extra...)`.
292
293 ## KEY CLASSES
294
295 - `VariantsArtifactsPlugin` β€” plugin adapter ΠΈ materialization outgoing variants.
296 - `VariantArtifactsExtension` β€” root DSL ΠΈ lifecycle.
297 - `VariantArtifact` β€” outgoing build variant model.
298 - `VariantArtifactSlot` β€” artifact representation slot.
299 - `OutgoingVariantPublication` β€” payload variant-level publication callback.
300 - `OutgoingArtifactSlotPublication` β€” payload per-slot publication callback.
301 - `ArtifactAssembly` β€” assembled files for a slot.
302
303 ## NOTES
304
305 - `common` Π½Π΅ навязываСт Π΄ΠΎΠΌΠ΅Π½Π½ΡƒΡŽ Π»ΠΎΠ³ΠΈΠΊΡƒ Π²Ρ‹Π±ΠΎΡ€Π° primary slot.
306 - `common` Π½Π΅ фиксируСт значСния `usage`, `libraryelements` ΠΈ ΠΏΡ€ΠΎΡ‡ΠΈΡ…
307 slot-specific attributes.
308 - `common` Π½Π΅ ΡΠΌΠ΅ΡˆΠΈΠ²Π°Π΅Ρ‚ эту модСль с ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ publish осями Π²Ρ€ΠΎΠ΄Π΅ package
309 metadata.
310 - Closure callbacks ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ delegate-first; для Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Ρ… closure ΡƒΠ΄ΠΎΠ±Π½Π΅Π΅
311 явный ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ (`publication -> ...`, `slotPublication -> ...`).
@@ -3,36 +3,87 package org.implab.gradle.common.sources
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 4 import org.implab.gradle.common.core.lang.Closures;
5 5 import org.gradle.api.Action;
6 import org.gradle.api.NamedDomainObjectProvider;
7 6 import org.gradle.api.artifacts.Configuration;
7 import org.gradle.api.InvalidUserDataException;
8 8
9 9 import groovy.lang.Closure;
10 10 import groovy.lang.DelegatesTo;
11 11
12 12 @NonNullByDefault
13 public record OutgoingVariantPublication(
14 String variantName,
15 String slotName,
16 BuildVariant topologyVariant,
17 VariantArtifact variantArtifact,
18 VariantArtifactSlot slot,
19 ArtifactAssembly assembly,
20 NamedDomainObjectProvider<? extends Configuration> configuration) {
13 public final class OutgoingVariantPublication {
14 private final String variantName;
15 private final BuildVariant topologyVariant;
16 private final VariantArtifact variantArtifact;
17 private final Configuration configuration;
18 private final OutgoingArtifactSlotPublication primarySlot;
19 private final java.util.List<OutgoingArtifactSlotPublication> slots;
20
21 public OutgoingVariantPublication(
22 String variantName,
23 BuildVariant topologyVariant,
24 VariantArtifact variantArtifact,
25 Configuration configuration,
26 OutgoingArtifactSlotPublication primarySlot,
27 java.util.List<OutgoingArtifactSlotPublication> slots) {
28 this.variantName = variantName;
29 this.topologyVariant = topologyVariant;
30 this.variantArtifact = variantArtifact;
31 this.configuration = configuration;
32 this.primarySlot = primarySlot;
33 this.slots = java.util.List.copyOf(slots);
34 }
35
36 public String variantName() {
37 return variantName;
38 }
39
40 public BuildVariant topologyVariant() {
41 return topologyVariant;
42 }
43
44 public VariantArtifact variantArtifact() {
45 return variantArtifact;
46 }
47
48 public Configuration configuration() {
49 return configuration;
50 }
51
52 public OutgoingArtifactSlotPublication primarySlot() {
53 return primarySlot;
54 }
55
56 public java.util.List<OutgoingArtifactSlotPublication> slots() {
57 return slots;
58 }
59
60 public java.util.List<OutgoingArtifactSlotPublication> secondarySlots() {
61 return slots.stream()
62 .filter(slotPublication -> !slotPublication.primary())
63 .toList();
64 }
65
66 public java.util.Optional<OutgoingArtifactSlotPublication> findSlot(String slotName) {
67 var normalizedSlotName = VariantArtifact.normalize(slotName, "slot name must not be null or blank");
68 return slots.stream()
69 .filter(slotPublication -> normalizedSlotName.equals(slotPublication.slotName()))
70 .findFirst();
71 }
72
73 public OutgoingArtifactSlotPublication requireSlot(String slotName) {
74 var normalizedSlotName = VariantArtifact.normalize(slotName, "slot name must not be null or blank");
75 return findSlot(normalizedSlotName)
76 .orElseThrow(() -> new InvalidUserDataException(
77 "Outgoing publication for variant '" + variantName + "' doesn't declare slot '"
78 + normalizedSlotName + "'"));
79 }
80
21 81 public void configureConfiguration(Action<? super Configuration> action) {
22 configuration.configure(action);
82 action.execute(configuration);
23 83 }
24 84
25 85 public void configureConfiguration(
26 86 @DelegatesTo(value = Configuration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
27 87 configureConfiguration(Closures.action(action));
28 88 }
29
30 public void configureAssembly(Action<? super ArtifactAssembly> action) {
31 action.execute(assembly);
32 }
33
34 public void configureAssembly(
35 @DelegatesTo(value = ArtifactAssembly.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
36 configureAssembly(Closures.action(action));
37 }
38 89 }
@@ -18,6 +18,7 import groovy.lang.DelegatesTo;
18 18 public class VariantArtifact implements Named {
19 19 private final String name;
20 20 private final NamedDomainObjectContainer<VariantArtifactSlot> slots;
21 private String primarySlotName;
21 22 private boolean finalized;
22 23
23 24 @Inject
@@ -63,6 +64,46 public class VariantArtifact implements
63 64 return Optional.ofNullable(slots.findByName(normalize(slotName, "slot name must not be null or blank")));
64 65 }
65 66
67 public void primarySlot(String slotName) {
68 ensureMutable("configure primary slot");
69 primarySlotName = normalize(slotName, "primary slot name must not be null or blank");
70 }
71
72 public VariantArtifactSlot primarySlot(String slotName, Action<? super VariantArtifactSlot> configure) {
73 ensureMutable("configure primary slot");
74 var slot = slot(slotName, configure);
75 primarySlot(slot.getName());
76 return slot;
77 }
78
79 public VariantArtifactSlot primarySlot(
80 String slotName,
81 @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
82 return primarySlot(slotName, Closures.action(configure));
83 }
84
85 public Optional<String> findPrimarySlotName() {
86 return Optional.ofNullable(primarySlotName)
87 .or(() -> slots.getNames().size() == 1 ? Optional.of(slots.iterator().next().getName()) : Optional.empty());
88 }
89
90 public String requirePrimarySlotName() {
91 return findPrimarySlotName()
92 .orElseThrow(() -> new InvalidUserDataException(
93 "Variant artifact '" + name + "' must declare primary slot because it has multiple slots"));
94 }
95
96 public Optional<VariantArtifactSlot> findPrimarySlot() {
97 return findPrimarySlotName().flatMap(this::findSlot);
98 }
99
100 public VariantArtifactSlot requirePrimarySlot() {
101 var resolvedPrimarySlotName = requirePrimarySlotName();
102 return findSlot(resolvedPrimarySlotName)
103 .orElseThrow(() -> new InvalidUserDataException(
104 "Variant artifact '" + name + "' declares unknown primary slot '" + resolvedPrimarySlotName + "'"));
105 }
106
66 107 public VariantArtifactSlot requireSlot(String slotName) {
67 108 var normalizedSlotName = normalize(slotName, "slot name must not be null or blank");
68 109 return Optional.ofNullable(slots.findByName(normalizedSlotName))
@@ -140,6 +140,19 public abstract class VariantArtifactsEx
140 140 layerNames.addAll(role.getLayers().getOrElse(List.of()));
141 141 }
142 142
143 if (!variantArtifact.getSlots().isEmpty()) {
144 if (variantArtifact.findPrimarySlotName().isEmpty()) {
145 errors.add("Variant artifact '" + variantArtifact.getName()
146 + "' must declare primary slot because it has multiple slots");
147 } else {
148 var primarySlotName = variantArtifact.requirePrimarySlotName();
149 if (variantArtifact.findSlot(primarySlotName).isEmpty()) {
150 errors.add("Variant artifact '" + variantArtifact.getName()
151 + "' declares unknown primary slot '" + primarySlotName + "'");
152 }
153 }
154 }
155
143 156 for (var slot : variantArtifact.getSlots()) {
144 157 for (var rule : slot.bindingRules()) {
145 158 switch (rule.selector().kind()) {
@@ -1,9 +1,17
1 1 package org.implab.gradle.common.sources;
2 2
3 import java.util.LinkedHashMap;
4 import java.util.List;
5 import java.util.ArrayList;
6 import java.util.stream.Collectors;
7 import java.util.stream.Stream;
8
3 9 import org.gradle.api.GradleException;
4 10 import org.gradle.api.Plugin;
5 11 import org.gradle.api.Project;
6 12 import org.gradle.api.artifacts.Configuration;
13 import org.gradle.api.artifacts.ConfigurationPublications;
14 import org.gradle.api.artifacts.ConfigurationVariant;
7 15 import org.gradle.api.logging.Logger;
8 16 import org.gradle.api.logging.Logging;
9 17 import org.implab.gradle.common.core.lang.Strings;
@@ -56,41 +64,117 public abstract class VariantsArtifactsP
56 64 VariantArtifactsExtension variantArtifacts,
57 65 VariantArtifactsResolver variantArtifactsResolver,
58 66 ArtifactAssemblyRegistry artifactAssemblies) {
59 for (var variantArtifact : variantArtifacts.getVariants()) {
60 var topologyVariant = topology.require(variantArtifact.getName());
61 for (var slot : variantArtifact.getSlots()) {
62 var assembly = artifactAssemblies.register(
63 variantArtifact.getName() + Strings.capitalize(slot.getName()),
64 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
65 project.getLayout().getBuildDirectory()
66 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
67 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
68 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), slot.getName(), assembly);
69
70 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
71 variantArtifact.getName(),
72 slot.getName(),
73 topologyVariant,
67 variantArtifacts.getVariants().stream()
68 .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty())
69 .forEach(variantArtifact -> materializeOutgoingVariant(
70 project,
71 topology.require(variantArtifact.getName()),
74 72 variantArtifact,
75 slot,
76 assembly,
77 configuration));
78 }
79 }
73 variantArtifactsResolver,
74 artifactAssemblies,
75 variantArtifacts));
80 76 }
81 77
82 private static org.gradle.api.NamedDomainObjectProvider<? extends Configuration> createOutgoingConfiguration(
78 private static void materializeOutgoingVariant(
79 Project project,
80 BuildVariant topologyVariant,
81 VariantArtifact variantArtifact,
82 VariantArtifactsResolver variantArtifactsResolver,
83 ArtifactAssemblyRegistry artifactAssemblies,
84 VariantArtifactsExtension variantArtifacts) {
85 var assemblies = variantArtifact.getSlots().stream()
86 .collect(Collectors.toMap(
87 VariantArtifactSlot::getName,
88 slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, slot),
89 (left, right) -> left,
90 LinkedHashMap::new));
91
92 var primarySlot = variantArtifact.requirePrimarySlot();
93 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), primarySlot.getName());
94 var primaryAssembly = assemblies.get(primarySlot.getName());
95 publishPrimaryArtifact(configuration, primaryAssembly);
96 var primaryPublication = new OutgoingArtifactSlotPublication(
97 primarySlot.getName(),
98 true,
99 primarySlot,
100 primaryAssembly,
101 configuration);
102 var secondarySlots = variantArtifact.getSlots().stream()
103 .filter(slot -> !slot.getName().equals(primarySlot.getName()))
104 .map(slot -> new SecondarySlot(slot, assemblies.get(slot.getName())))
105 .toList();
106 var secondaryPublications = new ArrayList<OutgoingArtifactSlotPublication>(secondarySlots.size());
107 secondarySlots.forEach(secondarySlot -> {
108 var secondaryVariant = configuration.getOutgoing().getVariants().create(secondarySlot.slot().getName());
109 publishSecondaryArtifact(secondaryVariant, secondarySlot.assembly());
110 secondaryPublications.add(new OutgoingArtifactSlotPublication(
111 secondarySlot.slot().getName(),
112 false,
113 secondarySlot.slot(),
114 secondarySlot.assembly(),
115 secondaryVariant));
116 });
117
118 var slotPublications = Stream.concat(
119 Stream.of(primaryPublication),
120 secondaryPublications.stream())
121 .toList();
122
123 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
124 variantArtifact.getName(),
125 topologyVariant,
126 variantArtifact,
127 configuration,
128 primaryPublication,
129 slotPublications));
130 }
131
132 private static ArtifactAssembly registerAssembly(
133 Project project,
134 VariantArtifactsResolver variantArtifactsResolver,
135 ArtifactAssemblyRegistry artifactAssemblies,
136 VariantArtifact variantArtifact,
137 VariantArtifactSlot slot) {
138 return artifactAssemblies.register(
139 variantArtifact.getName() + Strings.capitalize(slot.getName()),
140 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
141 project.getLayout().getBuildDirectory()
142 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
143 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
144 }
145
146 private static Configuration createOutgoingConfiguration(
83 147 Project project,
84 148 String variantName,
85 String slotName,
86 ArtifactAssembly assembly) {
87 var configName = variantName + Strings.capitalize(slotName) + "Elements";
149 String primarySlotName) {
150 var configName = variantName + "Elements";
88 151 return project.getConfigurations().consumable(configName, config -> {
89 152 config.setVisible(true);
90 config.setDescription("Consumable assembled artifacts for variant '" + variantName + "', slot '" + slotName + "'");
91 config.getOutgoing().artifact(assembly.getOutput().getSingleFile(), published -> {
92 published.builtBy(assembly.getOutput().getBuildDependencies());
93 });
153 config.setDescription("Consumable assembled artifacts for variant '" + variantName
154 + "' with primary slot '" + primarySlotName + "'");
155 }).get();
156 }
157
158 private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) {
159 publishArtifact(configuration.getOutgoing(), assembly);
160 }
161
162 private static void publishSecondaryArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
163 publishArtifact(variant, assembly);
164 }
165
166 private static void publishArtifact(ConfigurationPublications outgoing, ArtifactAssembly assembly) {
167 outgoing.artifact(assembly.getOutput().getSingleFile(), published -> {
168 published.builtBy(assembly.getOutput().getBuildDependencies());
94 169 });
95 170 }
171
172 private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
173 variant.artifact(assembly.getOutput().getSingleFile(), published -> {
174 published.builtBy(assembly.getOutput().getBuildDependencies());
175 });
176 }
177
178 private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) {
179 }
96 180 }
@@ -9,6 +9,7 import java.io.IOException;
9 9 import java.nio.file.Files;
10 10 import java.nio.file.Path;
11 11 import java.util.List;
12 import java.util.stream.Collectors;
12 13
13 14 import org.gradle.testkit.runner.BuildResult;
14 15 import org.gradle.testkit.runner.GradleRunner;
@@ -80,7 +81,7 class VariantsArtifactsPluginFunctionalT
80 81
81 82 variantArtifacts {
82 83 variant('browser') {
83 slot('mainJs') {
84 primarySlot('mainJs') {
84 85 fromRole('main') {
85 86 output('js')
86 87 }
@@ -94,12 +95,14 class VariantsArtifactsPluginFunctionalT
94 95 }
95 96
96 97 whenOutgoingVariant { publication ->
97 publication.configureAssembly {
98 sources.from(layout.projectDirectory.file("inputs/${publication.slotName()}.txt"))
99 }
98 publication.slots().each { slotPublication ->
99 slotPublication.configureAssembly {
100 sources.from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt"))
101 }
100 102
101 publication.configureConfiguration {
102 attributes.attribute(Attribute.of('test.slot', String), publication.slotName())
103 slotPublication.configureArtifactAttributes {
104 attribute(Attribute.of('test.slot', String), slotPublication.slotName())
105 }
103 106 }
104 107 }
105 108 }
@@ -119,23 +122,70 class VariantsArtifactsPluginFunctionalT
119 122 assert new File(amdDir, 'amd.js').exists()
120 123 assert new File(amdDir, 'amdJs.txt').exists()
121 124
122 def mainElements = configurations.getByName('browserMainJsElements')
123 def attr = mainElements.attributes.getAttribute(Attribute.of('test.slot', String))
125 def elements = configurations.getByName('browserElements')
126 def primaryAttr = elements.attributes.getAttribute(Attribute.of('test.slot', String))
127 def amdVariant = elements.outgoing.variants.getByName('amdJs')
128 def amdAttr = amdVariant.attributes.getAttribute(Attribute.of('test.slot', String))
124 129
125 println('mainAttr=' + attr)
126 println('configurations=' + [mainElements.name, configurations.getByName('browserAmdJsElements').name].sort().join(','))
130 println('primarySlot=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
131 println('primaryAttr=' + primaryAttr)
132 println('amdAttr=' + amdAttr)
133 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
134 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
127 135 }
128 136 }
129 137 """);
130 138
131 139 BuildResult result = runner("probe").build();
132 140
133 assertTrue(result.getOutput().contains("mainAttr=mainJs"));
134 assertTrue(result.getOutput().contains("configurations=browserAmdJsElements,browserMainJsElements"));
141 assertTrue(result.getOutput().contains("primarySlot=mainJs"));
142 assertTrue(result.getOutput().contains("primaryAttr=mainJs"));
143 assertTrue(result.getOutput().contains("amdAttr=amdJs"));
144 assertTrue(result.getOutput().contains("configurations=browserElements"));
145 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
135 146 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
136 147 }
137 148
138 149 @Test
150 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
151 writeFile(SETTINGS_FILE, ROOT_NAME);
152 writeFile(BUILD_FILE, """
153 plugins {
154 id 'org.implab.gradle-variants-artifacts'
155 }
156
157 variants {
158 layer('main')
159
160 variant('browser') {
161 role('main') {
162 layers('main')
163 }
164 }
165 }
166
167 variantArtifacts {
168 variant('browser') {
169 slot('typesPackage') {
170 fromVariant {
171 output('types')
172 }
173 }
174 }
175 }
176
177 tasks.register('probe') {
178 doLast {
179 println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
180 }
181 }
182 """);
183
184 BuildResult result = runner("probe").build();
185 assertTrue(result.getOutput().contains("primary=typesPackage"));
186 }
187
188 @Test
139 189 void failsOnUnknownVariantReference() throws Exception {
140 190 assertBuildFails("""
141 191 plugins {
@@ -188,6 +238,41 class VariantsArtifactsPluginFunctionalT
188 238 }
189 239
190 240 @Test
241 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
242 assertBuildFails("""
243 plugins {
244 id 'org.implab.gradle-variants-artifacts'
245 }
246
247 variants {
248 layer('main')
249
250 variant('browser') {
251 role('main') {
252 layers('main')
253 }
254 }
255 }
256
257 variantArtifacts {
258 variant('browser') {
259 slot('typesPackage') {
260 fromVariant {
261 output('types')
262 }
263 }
264
265 slot('js') {
266 fromVariant {
267 output('js')
268 }
269 }
270 }
271 }
272 """, "Variant artifact 'browser' must declare primary slot because it has multiple slots");
273 }
274
275 @Test
191 276 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
192 277 assertBuildFails("""
193 278 plugins {
@@ -246,6 +331,130 class VariantsArtifactsPluginFunctionalT
246 331 """, "variantArtifacts model is finalized and cannot configure variants");
247 332 }
248 333
334 @Test
335 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
336 writeFile(SETTINGS_FILE, """
337 rootProject.name = 'variants-artifacts-fixture'
338 include 'producer', 'consumer'
339 """);
340 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
341 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
342 var buildscriptClasspath = pluginClasspath().stream()
343 .map(File::getAbsolutePath)
344 .map(path -> "'" + path.replace("\\", "\\\\") + "'")
345 .collect(Collectors.joining(", "));
346 writeFile(BUILD_FILE, """
347 buildscript {
348 dependencies {
349 classpath files(%s)
350 }
351 }
352
353 import org.gradle.api.attributes.Attribute
354
355 def variantAttr = Attribute.of('test.variant', String)
356 def slotAttr = Attribute.of('test.slot', String)
357
358 subprojects {
359 apply plugin: 'org.implab.gradle-variants-artifacts'
360 }
361
362 project(':producer') {
363 variants {
364 layer('main')
365
366 variant('browser') {
367 role('main') {
368 layers('main')
369 }
370 }
371 }
372
373 variantSources {
374 bind('main') {
375 configureSourceSet {
376 declareOutputs('types', 'js')
377 }
378 }
379
380 whenBound { ctx ->
381 ctx.configureSourceSet {
382 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
383 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
384 }
385 }
386 }
387
388 variantArtifacts {
389 variant('browser') {
390 primarySlot('typesPackage') {
391 fromVariant {
392 output('types')
393 }
394 }
395
396 slot('js') {
397 fromVariant {
398 output('js')
399 }
400 }
401 }
402
403 whenOutgoingVariant { publication ->
404 publication.configureConfiguration {
405 attributes.attribute(variantAttr, publication.variantName())
406 }
407
408 publication.primarySlot().configureArtifactAttributes {
409 attribute(slotAttr, publication.primarySlot().slotName())
410 }
411
412 publication.requireSlot('js').configureArtifactAttributes {
413 attribute(slotAttr, 'js')
414 }
415 }
416 }
417 }
418
419 project(':consumer') {
420 configurations {
421 compileView {
422 canBeResolved = true
423 canBeConsumed = false
424 canBeDeclared = true
425 attributes {
426 attribute(variantAttr, 'browser')
427 attribute(slotAttr, 'typesPackage')
428 }
429 }
430 }
431
432 dependencies {
433 compileView project(':producer')
434 }
435
436 tasks.register('probe') {
437 doLast {
438 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
439 def jsFiles = configurations.compileView.incoming.artifactView {
440 attributes {
441 attribute(slotAttr, 'js')
442 }
443 }.files.files.collect { it.name }.sort().join(',')
444
445 println('compileFiles=' + compileFiles)
446 println('jsFiles=' + jsFiles)
447 }
448 }
449 }
450 """.formatted(buildscriptClasspath));
451
452 BuildResult result = runner(":consumer:probe").build();
453
454 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
455 assertTrue(result.getOutput().contains("jsFiles=js"));
456 }
457
249 458 private GradleRunner runner(String... arguments) {
250 459 return GradleRunner.create()
251 460 .withProjectDir(testProjectDir.toFile())
General Comments 0
You need to be logged in to leave comments. Login now