##// 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 -> ...`).
@@ -1,38 +1,89
1 1 package org.implab.gradle.common.sources;
2 2
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(
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(
14 22 String variantName,
15 String slotName,
16 23 BuildVariant topologyVariant,
17 24 VariantArtifact variantArtifact,
18 VariantArtifactSlot slot,
19 ArtifactAssembly assembly,
20 NamedDomainObjectProvider<? extends Configuration> configuration) {
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 89 }
33
34 public void configureAssembly(
35 @DelegatesTo(value = ArtifactAssembly.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
36 configureAssembly(Closures.action(action));
37 }
38 }
@@ -1,94 +1,135
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.Optional;
4 4
5 5 import javax.inject.Inject;
6 6
7 7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 8 import org.gradle.api.Action;
9 9 import org.gradle.api.InvalidUserDataException;
10 10 import org.gradle.api.Named;
11 11 import org.gradle.api.NamedDomainObjectContainer;
12 12 import org.implab.gradle.common.core.lang.Closures;
13 13
14 14 import groovy.lang.Closure;
15 15 import groovy.lang.DelegatesTo;
16 16
17 17 @NonNullByDefault
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
24 25 public VariantArtifact(String name, NamedDomainObjectContainer<VariantArtifactSlot> slots) {
25 26 this.name = normalize(name, "variant artifact name must not be null or blank");
26 27 this.slots = slots;
27 28
28 29 slots.all(slot -> {
29 30 if (finalized)
30 31 throw new InvalidUserDataException(
31 32 "Variant artifact '" + this.name + "' is finalized and cannot add slot '" + slot.getName() + "'");
32 33 });
33 34 }
34 35
35 36 @Override
36 37 public String getName() {
37 38 return name;
38 39 }
39 40
40 41 public NamedDomainObjectContainer<VariantArtifactSlot> getSlots() {
41 42 return slots;
42 43 }
43 44
44 45 public VariantArtifactSlot slot(String name) {
45 46 return slot(name, slot -> {
46 47 });
47 48 }
48 49
49 50 public VariantArtifactSlot slot(String name, Action<? super VariantArtifactSlot> configure) {
50 51 ensureMutable("configure slots");
51 52 var slot = slots.maybeCreate(normalize(name, "slot name must not be null or blank"));
52 53 configure.execute(slot);
53 54 return slot;
54 55 }
55 56
56 57 public VariantArtifactSlot slot(
57 58 String name,
58 59 @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
59 60 return slot(name, Closures.action(configure));
60 61 }
61 62
62 63 public Optional<VariantArtifactSlot> findSlot(String slotName) {
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))
69 110 .orElseThrow(() -> new InvalidUserDataException(
70 111 "Variant artifact '" + name + "' doesn't declare slot '" + normalizedSlotName + "'"));
71 112 }
72 113
73 114 void finalizeModel() {
74 115 if (finalized)
75 116 return;
76 117
77 118 for (var slot : slots)
78 119 slot.finalizeModel();
79 120
80 121 finalized = true;
81 122 }
82 123
83 124 static String normalize(String value, String message) {
84 125 return Optional.ofNullable(value)
85 126 .map(String::trim)
86 127 .filter(trimmed -> !trimmed.isEmpty())
87 128 .orElseThrow(() -> new InvalidUserDataException(message));
88 129 }
89 130
90 131 private void ensureMutable(String operation) {
91 132 if (finalized)
92 133 throw new InvalidUserDataException("Variant artifact '" + name + "' is finalized and cannot " + operation);
93 134 }
94 135 }
@@ -1,169 +1,182
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.LinkedHashSet;
5 5 import java.util.List;
6 6 import java.util.Optional;
7 7
8 8 import javax.inject.Inject;
9 9
10 10 import org.eclipse.jdt.annotation.NonNullByDefault;
11 11 import org.gradle.api.Action;
12 12 import org.gradle.api.InvalidUserDataException;
13 13 import org.gradle.api.NamedDomainObjectContainer;
14 14 import org.gradle.api.model.ObjectFactory;
15 15 import org.implab.gradle.common.core.lang.Closures;
16 16
17 17 import groovy.lang.Closure;
18 18 import groovy.lang.DelegatesTo;
19 19
20 20 @NonNullByDefault
21 21 public abstract class VariantArtifactsExtension {
22 22 private final NamedDomainObjectContainer<VariantArtifact> variants;
23 23 private final ObjectFactory objects;
24 24 private final List<Action<? super OutgoingVariantPublication>> outgoingVariantActions = new ArrayList<>();
25 25 private final List<OutgoingVariantPublication> outgoingVariants = new ArrayList<>();
26 26 private boolean finalized;
27 27
28 28 @Inject
29 29 public VariantArtifactsExtension(ObjectFactory objects) {
30 30 this.objects = objects;
31 31 variants = objects.domainObjectContainer(VariantArtifact.class, this::newVariantArtifact);
32 32
33 33 variants.all(variant -> {
34 34 if (finalized)
35 35 throw new InvalidUserDataException(
36 36 "variantArtifacts model is finalized and cannot add variant '" + variant.getName() + "'");
37 37 });
38 38 }
39 39
40 40 public NamedDomainObjectContainer<VariantArtifact> getVariants() {
41 41 return variants;
42 42 }
43 43
44 44 public VariantArtifact variant(String name) {
45 45 return variant(name, variant -> {
46 46 });
47 47 }
48 48
49 49 public VariantArtifact variant(String name, Action<? super VariantArtifact> configure) {
50 50 ensureMutable("configure variants");
51 51 var variant = variants.maybeCreate(VariantArtifact.normalize(name, "variant name must not be null or blank"));
52 52 configure.execute(variant);
53 53 return variant;
54 54 }
55 55
56 56 public VariantArtifact variant(
57 57 String name,
58 58 @DelegatesTo(value = VariantArtifact.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
59 59 return variant(name, Closures.action(configure));
60 60 }
61 61
62 62 public Optional<VariantArtifact> findVariant(String variantName) {
63 63 return Optional
64 64 .ofNullable(variants.findByName(VariantArtifact.normalize(variantName, "variant name must not be null or blank")));
65 65 }
66 66
67 67 public VariantArtifact requireVariant(String variantName) {
68 68 var normalizedVariantName = VariantArtifact.normalize(variantName, "variant name must not be null or blank");
69 69 return findVariant(normalizedVariantName)
70 70 .orElseThrow(() -> new InvalidUserDataException(
71 71 "Variant artifacts do not declare variant '" + normalizedVariantName + "'"));
72 72 }
73 73
74 74 public void whenOutgoingVariant(Action<? super OutgoingVariantPublication> action) {
75 75 outgoingVariantActions.add(action);
76 76 for (var publication : outgoingVariants)
77 77 action.execute(publication);
78 78 }
79 79
80 80 public void whenOutgoingVariant(
81 81 @DelegatesTo(value = OutgoingVariantPublication.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
82 82 whenOutgoingVariant(Closures.action(action));
83 83 }
84 84
85 85 public boolean isFinalized() {
86 86 return finalized;
87 87 }
88 88
89 89 void finalizeModel(BuildVariantsExtension topology) {
90 90 if (finalized)
91 91 return;
92 92
93 93 validate(topology);
94 94
95 95 for (var variant : variants)
96 96 variant.finalizeModel();
97 97
98 98 finalized = true;
99 99 }
100 100
101 101 void notifyOutgoingVariant(OutgoingVariantPublication publication) {
102 102 outgoingVariants.add(publication);
103 103 for (var action : outgoingVariantActions)
104 104 action.execute(publication);
105 105 }
106 106
107 107 private VariantArtifact newVariantArtifact(String name) {
108 108 return objects.newInstance(VariantArtifact.class, name, objects.domainObjectContainer(VariantArtifactSlot.class));
109 109 }
110 110
111 111 private void validate(BuildVariantsExtension topology) {
112 112 var errors = new ArrayList<String>();
113 113
114 114 for (var variantArtifact : variants) {
115 115 var topologyVariant = topology.find(variantArtifact.getName());
116 116 if (topologyVariant.isEmpty()) {
117 117 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
118 118 + variantArtifact.getName() + "'");
119 119 continue;
120 120 }
121 121
122 122 validateVariantArtifact(variantArtifact, topologyVariant.get(), errors);
123 123 }
124 124
125 125 if (!errors.isEmpty()) {
126 126 var message = new StringBuilder("Invalid variantArtifacts model:");
127 127 for (var error : errors)
128 128 message.append("\n - ").append(error);
129 129
130 130 throw new InvalidUserDataException(message.toString());
131 131 }
132 132 }
133 133
134 134 private static void validateVariantArtifact(VariantArtifact variantArtifact, BuildVariant topologyVariant, List<String> errors) {
135 135 var roleNames = new LinkedHashSet<String>();
136 136 var layerNames = new LinkedHashSet<String>();
137 137
138 138 for (var role : topologyVariant.getRoles()) {
139 139 roleNames.add(role.getName());
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()) {
146 159 case VARIANT -> {
147 160 }
148 161 case ROLE -> {
149 162 if (!roleNames.contains(rule.selector().value())) {
150 163 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
151 164 + "' references unknown role '" + rule.selector().value() + "'");
152 165 }
153 166 }
154 167 case LAYER -> {
155 168 if (!layerNames.contains(rule.selector().value())) {
156 169 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
157 170 + "' references unknown layer '" + rule.selector().value() + "'");
158 171 }
159 172 }
160 173 }
161 174 }
162 175 }
163 176 }
164 177
165 178 private void ensureMutable(String operation) {
166 179 if (finalized)
167 180 throw new InvalidUserDataException("variantArtifacts model is finalized and cannot " + operation);
168 181 }
169 182 }
@@ -1,96 +1,180
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;
10 18
11 19 public abstract class VariantsArtifactsPlugin implements Plugin<Project> {
12 20 private static final Logger logger = Logging.getLogger(VariantsArtifactsPlugin.class);
13 21 public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";
14 22
15 23 @Override
16 24 public void apply(Project target) {
17 25 logger.debug("Registering '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
18 26
19 27 target.getPluginManager().apply(VariantsSourcesPlugin.class);
20 28
21 29 var variants = VariantsPlugin.getVariantsExtension(target);
22 30 var variantSources = target.getExtensions().getByType(VariantSourcesExtension.class);
23 31 var variantArtifacts = target.getExtensions()
24 32 .create(VARIANT_ARTIFACTS_EXTENSION_NAME, VariantArtifactsExtension.class);
25 33 var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
26 34 var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks());
27 35
28 36 variantSources.whenBound(variantArtifactsResolver::recordBinding);
29 37
30 38 variants.whenFinalized(model -> {
31 39 logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath());
32 40 variantArtifacts.finalizeModel(model);
33 41 materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies);
34 42 logger.debug("variantArtifacts model finalized on project '{}'", target.getPath());
35 43 });
36 44 }
37 45
38 46 public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) {
39 47 var extension = target.getExtensions().findByType(VariantArtifactsExtension.class);
40 48
41 49 if (extension == null) {
42 50 logger.error("variantArtifacts extension '{}' isn't found on project '{}'",
43 51 VARIANT_ARTIFACTS_EXTENSION_NAME,
44 52 target.getPath());
45 53 throw new GradleException("variantArtifacts extension isn't found");
46 54 }
47 55
48 56 logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
49 57
50 58 return extension;
51 59 }
52 60
53 61 private static void materializeOutgoingVariants(
54 62 Project project,
55 63 BuildVariantsExtension topology,
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(
67 variantArtifacts.getVariants().stream()
68 .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty())
69 .forEach(variantArtifact -> materializeOutgoingVariant(
70 project,
71 topology.require(variantArtifact.getName()),
72 variantArtifact,
73 variantArtifactsResolver,
74 artifactAssemblies,
75 variantArtifacts));
76 }
77
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(
63 139 variantArtifact.getName() + Strings.capitalize(slot.getName()),
64 140 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
65 141 project.getLayout().getBuildDirectory()
66 142 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
67 143 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
68 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), slot.getName(), assembly);
144 }
69 145
70 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
71 variantArtifact.getName(),
72 slot.getName(),
73 topologyVariant,
74 variantArtifact,
75 slot,
76 assembly,
77 configuration));
146 private static Configuration createOutgoingConfiguration(
147 Project project,
148 String variantName,
149 String primarySlotName) {
150 var configName = variantName + "Elements";
151 return project.getConfigurations().consumable(configName, config -> {
152 config.setVisible(true);
153 config.setDescription("Consumable assembled artifacts for variant '" + variantName
154 + "' with primary slot '" + primarySlotName + "'");
155 }).get();
78 156 }
79 }
157
158 private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) {
159 publishArtifact(configuration.getOutgoing(), assembly);
80 160 }
81 161
82 private static org.gradle.api.NamedDomainObjectProvider<? extends Configuration> createOutgoingConfiguration(
83 Project project,
84 String variantName,
85 String slotName,
86 ArtifactAssembly assembly) {
87 var configName = variantName + Strings.capitalize(slotName) + "Elements";
88 return project.getConfigurations().consumable(configName, config -> {
89 config.setVisible(true);
90 config.setDescription("Consumable assembled artifacts for variant '" + variantName + "', slot '" + slotName + "'");
91 config.getOutgoing().artifact(assembly.getOutput().getSingleFile(), published -> {
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 -> {
92 168 published.builtBy(assembly.getOutput().getBuildDependencies());
93 169 });
170 }
171
172 private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
173 variant.artifact(assembly.getOutput().getSingleFile(), published -> {
174 published.builtBy(assembly.getOutput().getBuildDependencies());
94 175 });
95 176 }
177
178 private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) {
96 179 }
180 }
@@ -1,294 +1,503
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 5 import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
7 7 import java.io.File;
8 8 import java.io.IOException;
9 9 import java.nio.file.Files;
10 10 import java.nio.file.Path;
11 11 import java.util.List;
12 import java.util.stream.Collectors;
12 13
13 14 import org.gradle.testkit.runner.BuildResult;
14 15 import org.gradle.testkit.runner.GradleRunner;
15 16 import org.gradle.testkit.runner.TaskOutcome;
16 17 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 18 import org.junit.jupiter.api.Test;
18 19 import org.junit.jupiter.api.io.TempDir;
19 20
20 21 class VariantsArtifactsPluginFunctionalTest {
21 22 private static final String SETTINGS_FILE = "settings.gradle";
22 23 private static final String BUILD_FILE = "build.gradle";
23 24 private static final String ROOT_NAME = "rootProject.name = 'variants-artifacts-fixture'\n";
24 25
25 26 @TempDir
26 27 Path testProjectDir;
27 28
28 29 @Test
29 30 void materializesVariantArtifactsAndInvokesOutgoingHooks() throws Exception {
30 31 writeFile(SETTINGS_FILE, ROOT_NAME);
31 32 writeFile("inputs/base.js", "console.log('base')\n");
32 33 writeFile("inputs/amd.js", "console.log('amd')\n");
33 34 writeFile("inputs/mainJs.txt", "mainJs marker\n");
34 35 writeFile("inputs/amdJs.txt", "amdJs marker\n");
35 36 writeFile(BUILD_FILE, """
36 37 import org.gradle.api.attributes.Attribute
37 38
38 39 plugins {
39 40 id 'org.implab.gradle-variants-artifacts'
40 41 }
41 42
42 43 variants {
43 44 layer('mainBase')
44 45 layer('mainAmd')
45 46
46 47 variant('browser') {
47 48 role('main') {
48 49 layers('mainBase', 'mainAmd')
49 50 }
50 51 }
51 52 }
52 53
53 54 variantSources {
54 55 bind('mainBase') {
55 56 configureSourceSet {
56 57 declareOutputs('js')
57 58 }
58 59 }
59 60
60 61 bind('mainAmd') {
61 62 configureSourceSet {
62 63 declareOutputs('js')
63 64 }
64 65 }
65 66
66 67 whenBound { ctx ->
67 68 if (ctx.sourceSetName() == 'browserMainBase') {
68 69 ctx.configureSourceSet {
69 70 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
70 71 }
71 72 }
72 73
73 74 if (ctx.sourceSetName() == 'browserMainAmd') {
74 75 ctx.configureSourceSet {
75 76 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
76 77 }
77 78 }
78 79 }
79 80 }
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 }
87 88 }
88 89
89 90 slot('amdJs') {
90 91 fromLayer('mainAmd') {
91 92 output('js')
92 93 }
93 94 }
94 95 }
95 96
96 97 whenOutgoingVariant { publication ->
97 publication.configureAssembly {
98 sources.from(layout.projectDirectory.file("inputs/${publication.slotName()}.txt"))
98 publication.slots().each { slotPublication ->
99 slotPublication.configureAssembly {
100 sources.from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt"))
99 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 }
106 109
107 110 tasks.register('probe') {
108 111 dependsOn 'processBrowserMainJs', 'processBrowserAmdJs'
109 112
110 113 doLast {
111 114 def mainDir = layout.buildDirectory.dir('variant-artifacts/browser/mainJs').get().asFile
112 115 def amdDir = layout.buildDirectory.dir('variant-artifacts/browser/amdJs').get().asFile
113 116
114 117 assert new File(mainDir, 'base.js').exists()
115 118 assert new File(mainDir, 'amd.js').exists()
116 119 assert new File(mainDir, 'mainJs.txt').exists()
117 120
118 121 assert !new File(amdDir, 'base.js').exists()
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 {
142 192 id 'org.implab.gradle-variants-artifacts'
143 193 }
144 194
145 195 variants {
146 196 layer('main')
147 197 }
148 198
149 199 variantArtifacts {
150 200 variant('browser') {
151 201 slot('mainJs') {
152 202 fromVariant {
153 203 output('js')
154 204 }
155 205 }
156 206 }
157 207 }
158 208 """, "Variant artifact 'browser' references unknown variant 'browser'");
159 209 }
160 210
161 211 @Test
162 212 void failsOnUnknownRoleReference() throws Exception {
163 213 assertBuildFails("""
164 214 plugins {
165 215 id 'org.implab.gradle-variants-artifacts'
166 216 }
167 217
168 218 variants {
169 219 layer('main')
170 220
171 221 variant('browser') {
172 222 role('main') {
173 223 layers('main')
174 224 }
175 225 }
176 226 }
177 227
178 228 variantArtifacts {
179 229 variant('browser') {
180 230 slot('mainJs') {
181 231 fromRole('test') {
182 232 output('js')
183 233 }
184 234 }
185 235 }
186 236 }
187 237 """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'");
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 {
194 279 id 'org.implab.gradle-variants-artifacts'
195 280 }
196 281
197 282 variants {
198 283 layer('mainBase')
199 284 layer('extra')
200 285
201 286 variant('browser') {
202 287 role('main') {
203 288 layers('mainBase')
204 289 }
205 290 }
206 291 }
207 292
208 293 variantArtifacts {
209 294 variant('browser') {
210 295 slot('extraJs') {
211 296 fromLayer('extra') {
212 297 output('js')
213 298 }
214 299 }
215 300 }
216 301 }
217 302 """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'");
218 303 }
219 304
220 305 @Test
221 306 void failsOnLateMutationAfterFinalize() throws Exception {
222 307 assertBuildFails("""
223 308 plugins {
224 309 id 'org.implab.gradle-variants-artifacts'
225 310 }
226 311
227 312 variants {
228 313 layer('main')
229 314
230 315 variant('browser') {
231 316 role('main') {
232 317 layers('main')
233 318 }
234 319 }
235 320 }
236 321
237 322 afterEvaluate {
238 323 variantArtifacts.variant('late') {
239 324 slot('js') {
240 325 fromVariant {
241 326 output('js')
242 327 }
243 328 }
244 329 }
245 330 }
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())
252 461 .withPluginClasspath(pluginClasspath())
253 462 .withArguments(arguments)
254 463 .forwardOutput();
255 464 }
256 465
257 466 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
258 467 writeFile(SETTINGS_FILE, ROOT_NAME);
259 468 writeFile(BUILD_FILE, buildScript);
260 469
261 470 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
262 471 var output = ex.getBuildResult().getOutput();
263 472
264 473 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
265 474 }
266 475
267 476 private static List<File> pluginClasspath() {
268 477 try {
269 478 var classesDir = Path.of(VariantsArtifactsPlugin.class
270 479 .getProtectionDomain()
271 480 .getCodeSource()
272 481 .getLocation()
273 482 .toURI());
274 483
275 484 var markerResource = VariantsArtifactsPlugin.class.getClassLoader()
276 485 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
277 486
278 487 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
279 488
280 489 var markerPath = Path.of(markerResource.toURI());
281 490 var resourcesDir = markerPath.getParent().getParent().getParent();
282 491
283 492 return List.of(classesDir.toFile(), resourcesDir.toFile());
284 493 } catch (Exception e) {
285 494 throw new RuntimeException("Unable to build plugin classpath for test", e);
286 495 }
287 496 }
288 497
289 498 private void writeFile(String relativePath, String content) throws IOException {
290 499 Path path = testProjectDir.resolve(relativePath);
291 500 Files.createDirectories(path.getParent());
292 501 Files.writeString(path, content);
293 502 }
294 503 }
General Comments 0
You need to be logged in to leave comments. Login now