##// END OF EJS Templates
variants API cleanup
cin -
r29:b379fb9b52c4 default
parent child
Show More
@@ -0,0 +1,12
1 # AGENTS.md
2
3 ## Проектные договоренности
4
5 ### Публичное API библиотек
6
7 - Предпочтителен `non-null` подход.
8 - Там, где значение живет в Gradle Provider API, возвращается `Provider<T>` (не `null`).
9 - Там, где lookup синхронный, возвращается `Optional<T>` (не `null`).
10 - `find*` рассматривается как синоним legacy `get*` (поиск без `fail-fast`).
11 - `require*` это `find*` + `fail-fast` с понятной ошибкой в месте вызова.
12 - Для нового API предпочтительны формы `find/require`; новые `get*` по возможности не добавлять.
@@ -1,301 +1,311
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;
4 import java.util.Collection;
5 import java.util.Collections;
5 import java.util.Collections;
6 import java.util.LinkedHashMap;
6 import java.util.LinkedHashMap;
7 import java.util.LinkedHashSet;
8 import java.util.List;
7 import java.util.List;
9 import java.util.Set;
8 import java.util.Optional;
10
9
11 import javax.inject.Inject;
10 import javax.inject.Inject;
12
11
13 import org.implab.gradle.common.core.lang.Closures;
12 import org.implab.gradle.common.core.lang.Closures;
14 import org.gradle.api.Action;
13 import org.gradle.api.Action;
15 import org.gradle.api.InvalidUserDataException;
14 import org.gradle.api.InvalidUserDataException;
16 import org.gradle.api.Named;
15 import org.gradle.api.Named;
17 import org.gradle.api.model.ObjectFactory;
16 import org.gradle.api.model.ObjectFactory;
18 import org.gradle.api.provider.Provider;
17 import org.gradle.api.provider.Provider;
19 import org.gradle.api.provider.ProviderFactory;
18 import org.gradle.api.provider.ProviderFactory;
20 import org.gradle.api.attributes.Attribute;
19 import org.gradle.api.attributes.Attribute;
21
20
22 import groovy.lang.Closure;
21 import groovy.lang.Closure;
23
22
24 public abstract class BuildVariant implements Named {
23 public abstract class BuildVariant implements Named {
25 private final String name;
24 private final String name;
26 private final ObjectFactory objects;
25 private final ObjectFactory objects;
27 private boolean finalized;
26 private boolean finalized;
28
27
29 /**
28 /**
30 * Variant aggregate parts.
29 * Variant aggregate parts.
31 */
30 */
32 private final VariantAttributes attributes;
31 private final VariantAttributes attributes;
33 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
32 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
34 private final List<LayerLink> links = new ArrayList<>();
33 private final List<LayerLink> links = new ArrayList<>();
35 private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();
34 private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();
36
35
37 @Inject
36 @Inject
38 public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
37 public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
39 this.name = name;
38 this.name = name;
40 this.objects = objects;
39 this.objects = objects;
41 attributes = new VariantAttributes(providers);
40 attributes = new VariantAttributes(providers);
42 }
41 }
43
42
44 @Override
43 @Override
45 public String getName() {
44 public String getName() {
46 return name;
45 return name;
47 }
46 }
48
47
49 /**
48 /**
50 * Generic variant attributes interpreted by adapters.
49 * Generic variant attributes interpreted by adapters.
51 */
50 */
52 public VariantAttributes getAttributes() {
51 public VariantAttributes getAttributes() {
53 return attributes;
52 return attributes;
54 }
53 }
55
54
56 public void attributes(Action<? super AttributesSpec> action) {
55 public void attributes(Action<? super AttributesSpec> action) {
57 ensureMutable("configure attributes");
56 ensureMutable("configure attributes");
58 action.execute(new AttributesSpec(attributes));
57 action.execute(new AttributesSpec(attributes));
59 }
58 }
60
59
61 public void attributes(Closure<?> configure) {
60 public void attributes(Closure<?> configure) {
62 attributes(Closures.action(configure));
61 attributes(Closures.action(configure));
63 }
62 }
64
63
65 public <T> void attribute(Attribute<T> key, T value) {
64 public <T> void attribute(Attribute<T> key, T value) {
66 ensureMutable("set attributes");
65 ensureMutable("set attributes");
67 attributes.attribute(key, value);
66 attributes.attribute(key, value);
68 }
67 }
69
68
70 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
69 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
71 ensureMutable("set attributes");
70 ensureMutable("set attributes");
72 attributes.attributeProvider(key, value);
71 attributes.attributeProvider(key, value);
73 }
72 }
74
73
75 public Collection<BuildRole> getRoles() {
74 public Collection<BuildRole> getRoles() {
76 return Collections.unmodifiableCollection(roles.values());
75 return Collections.unmodifiableCollection(roles.values());
77 }
76 }
78
77
79 public void roles(Action<? super RolesSpec> action) {
78 public void roles(Action<? super RolesSpec> action) {
80 ensureMutable("configure roles");
79 ensureMutable("configure roles");
81 action.execute(new RolesSpec());
80 action.execute(new RolesSpec());
82 }
81 }
83
82
84 public void roles(Closure<?> configure) {
83 public void roles(Closure<?> configure) {
85 roles(Closures.action(configure));
84 roles(Closures.action(configure));
86 }
85 }
87
86
88 public BuildRole role(String name, Action<? super BuildRole> configure) {
87 public BuildRole role(String name, Action<? super BuildRole> configure) {
89 ensureMutable("configure roles");
88 ensureMutable("configure roles");
90 var role = roles.computeIfAbsent(name, this::newRole);
89 var role = roles.computeIfAbsent(name, this::newRole);
91 configure.execute(role);
90 configure.execute(role);
92 return role;
91 return role;
93 }
92 }
94
93
95 public BuildRole role(String name, Closure<?> configure) {
94 public BuildRole role(String name, Closure<?> configure) {
96 return role(name, Closures.action(configure));
95 return role(name, Closures.action(configure));
97 }
96 }
98
97
99 public BuildRole role(String name) {
98 public BuildRole role(String name) {
100 return role(name, r -> {
99 return role(name, r -> {
101 });
100 });
102 }
101 }
103
102
104 public BuildRole getRoleByName(String name) {
103 public Optional<BuildRole> findRole(String name) {
105 return roles.get(name);
104 return Optional.ofNullable(roles.get(name));
105 }
106
107 public BuildRole requireRole(String name) {
108 return findRole(name)
109 .orElseThrow(() -> new InvalidUserDataException(
110 "Variant '" + this.name + "' doesn't define role '" + name + "'"));
106 }
111 }
107
112
108 public Collection<LayerLink> getLinks() {
113 public Collection<LayerLink> getLinks() {
109 return Collections.unmodifiableList(links);
114 return Collections.unmodifiableList(links);
110 }
115 }
111
116
112 public void links(Action<? super LinksSpec> action) {
117 public void links(Action<? super LinksSpec> action) {
113 ensureMutable("configure links");
118 ensureMutable("configure links");
114 action.execute(new LinksSpec());
119 action.execute(new LinksSpec());
115 }
120 }
116
121
117 public void links(Closure<?> configure) {
122 public void links(Closure<?> configure) {
118 links(Closures.action(configure));
123 links(Closures.action(configure));
119 }
124 }
120
125
121 public LayerLink link(String from, String to, String kind) {
126 public LayerLink link(String from, String to, String kind) {
122 ensureMutable("add links");
127 ensureMutable("add links");
123 var link = new LayerLink(
128 var link = new LayerLink(
124 requireLinkValue("from", from),
129 requireLinkValue("from", from),
125 requireLinkValue("to", to),
130 requireLinkValue("to", to),
126 requireLinkValue("kind", kind));
131 requireLinkValue("kind", kind));
127 links.add(link);
132 links.add(link);
128 return link;
133 return link;
129 }
134 }
130
135
131 public Collection<BuildArtifactSlot> getArtifactSlots() {
136 public Collection<BuildArtifactSlot> getArtifactSlots() {
132 return Collections.unmodifiableCollection(artifactSlots.values());
137 return Collections.unmodifiableCollection(artifactSlots.values());
133 }
138 }
134
139
135 public void artifactSlots(Action<? super ArtifactSlotsSpec> action) {
140 public void artifactSlots(Action<? super ArtifactSlotsSpec> action) {
136 ensureMutable("configure artifact slots");
141 ensureMutable("configure artifact slots");
137 action.execute(new ArtifactSlotsSpec());
142 action.execute(new ArtifactSlotsSpec());
138 }
143 }
139
144
140 public void artifactSlots(Closure<?> configure) {
145 public void artifactSlots(Closure<?> configure) {
141 artifactSlots(Closures.action(configure));
146 artifactSlots(Closures.action(configure));
142 }
147 }
143
148
144 public BuildArtifactSlot artifactSlot(String name) {
149 public BuildArtifactSlot artifactSlot(String name) {
145 return artifactSlot(name, it -> {
150 return artifactSlot(name, it -> {
146 });
151 });
147 }
152 }
148
153
149 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
154 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
150 ensureMutable("configure artifact slots");
155 ensureMutable("configure artifact slots");
151 var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot);
156 var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot);
152 configure.execute(slot);
157 configure.execute(slot);
153 return slot;
158 return slot;
154 }
159 }
155
160
156 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
161 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
157 return artifactSlot(name, Closures.action(configure));
162 return artifactSlot(name, Closures.action(configure));
158 }
163 }
159
164
160 public BuildArtifactSlot getArtifactSlotByName(String name) {
165 public Optional<BuildArtifactSlot> findArtifactSlot(String name) {
161 return artifactSlots.get(name);
166 return Optional.ofNullable(artifactSlots.get(name));
162 }
167 }
163
168
164 Set<String> declaredLayerNames() {
169 public BuildArtifactSlot requireArtifactSlot(String name) {
165 var result = new LinkedHashSet<String>();
170 return findArtifactSlot(name)
166
171 .orElseThrow(() -> new InvalidUserDataException(
167 for (var role : roles.values())
172 "Variant '" + this.name + "' doesn't define artifact slot '" + name + "'"));
168 result.addAll(role.getLayers().getOrElse(java.util.List.of()));
169
170 return result;
171 }
173 }
172
174
173 void finalizeModel() {
175 void finalizeModel() {
174 if (finalized)
176 if (finalized)
175 return;
177 return;
176
178
177 for (var role : roles.values())
179 for (var role : roles.values())
178 role.finalizeModel();
180 role.finalizeModel();
179
181
180 attributes.finalizeModel();
182 attributes.finalizeModel();
181 finalized = true;
183 finalized = true;
182 }
184 }
183
185
184 private BuildRole newRole(String roleName) {
186 private BuildRole newRole(String roleName) {
185 return objects.newInstance(BuildRole.class, roleName);
187 return objects.newInstance(BuildRole.class, roleName);
186 }
188 }
187
189
188 private BuildArtifactSlot newArtifactSlot(String slotName) {
190 private BuildArtifactSlot newArtifactSlot(String slotName) {
189 return objects.newInstance(BuildArtifactSlot.class, slotName);
191 return objects.newInstance(BuildArtifactSlot.class, slotName);
190 }
192 }
191
193
192 private void ensureMutable(String operation) {
194 private void ensureMutable(String operation) {
193 if (finalized)
195 if (finalized)
194 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
196 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
195 }
197 }
196
198
197 private static String requireLinkValue(String field, String value) {
199 private static String requireLinkValue(String field, String value) {
198 if (value == null || value.trim().isEmpty())
200 if (value == null || value.trim().isEmpty())
199 throw new InvalidUserDataException("Link '" + field + "' must not be null or blank");
201 throw new InvalidUserDataException("Link '" + field + "' must not be null or blank");
200
202
201 return value.trim();
203 return value.trim();
202 }
204 }
203
205
204 public final class RolesSpec {
206 public final class RolesSpec {
205 public BuildRole role(String name, Action<? super BuildRole> configure) {
207 public BuildRole role(String name, Action<? super BuildRole> configure) {
206 return BuildVariant.this.role(name, configure);
208 return BuildVariant.this.role(name, configure);
207 }
209 }
208
210
209 public BuildRole role(String name, Closure<?> configure) {
211 public BuildRole role(String name, Closure<?> configure) {
210 return BuildVariant.this.role(name, configure);
212 return BuildVariant.this.role(name, configure);
211 }
213 }
212
214
213 public BuildRole role(String name) {
215 public BuildRole role(String name) {
214 return BuildVariant.this.role(name);
216 return BuildVariant.this.role(name);
215 }
217 }
216
218
217 public Collection<BuildRole> getAll() {
219 public Collection<BuildRole> getAll() {
218 return BuildVariant.this.getRoles();
220 return BuildVariant.this.getRoles();
219 }
221 }
220
222
221 public BuildRole getByName(String name) {
223 public Optional<BuildRole> find(String name) {
222 return BuildVariant.this.getRoleByName(name);
224 return BuildVariant.this.findRole(name);
225 }
226
227 public BuildRole require(String name) {
228 return BuildVariant.this.requireRole(name);
223 }
229 }
224 }
230 }
225
231
226 public final class LinksSpec {
232 public final class LinksSpec {
227 public LayerLink link(String from, String to, String kind) {
233 public LayerLink link(String from, String to, String kind) {
228 return BuildVariant.this.link(from, to, kind);
234 return BuildVariant.this.link(from, to, kind);
229 }
235 }
230
236
231 public Collection<LayerLink> getAll() {
237 public Collection<LayerLink> getAll() {
232 return BuildVariant.this.getLinks();
238 return BuildVariant.this.getLinks();
233 }
239 }
234 }
240 }
235
241
236 public final class ArtifactSlotsSpec {
242 public final class ArtifactSlotsSpec {
237 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
243 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
238 return BuildVariant.this.artifactSlot(name, configure);
244 return BuildVariant.this.artifactSlot(name, configure);
239 }
245 }
240
246
241 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
247 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
242 return BuildVariant.this.artifactSlot(name, configure);
248 return BuildVariant.this.artifactSlot(name, configure);
243 }
249 }
244
250
245 public BuildArtifactSlot artifactSlot(String name) {
251 public BuildArtifactSlot artifactSlot(String name) {
246 return BuildVariant.this.artifactSlot(name);
252 return BuildVariant.this.artifactSlot(name);
247 }
253 }
248
254
249 public Collection<BuildArtifactSlot> getAll() {
255 public Collection<BuildArtifactSlot> getAll() {
250 return BuildVariant.this.getArtifactSlots();
256 return BuildVariant.this.getArtifactSlots();
251 }
257 }
252
258
253 public BuildArtifactSlot getByName(String name) {
259 public Optional<BuildArtifactSlot> find(String name) {
254 return BuildVariant.this.getArtifactSlotByName(name);
260 return BuildVariant.this.findArtifactSlot(name);
261 }
262
263 public BuildArtifactSlot require(String name) {
264 return BuildVariant.this.requireArtifactSlot(name);
255 }
265 }
256 }
266 }
257
267
258 public static final class AttributesSpec {
268 public static final class AttributesSpec {
259 private final VariantAttributes attributes;
269 private final VariantAttributes attributes;
260
270
261 AttributesSpec(VariantAttributes attributes) {
271 AttributesSpec(VariantAttributes attributes) {
262 this.attributes = attributes;
272 this.attributes = attributes;
263 }
273 }
264
274
265 public <T> void attribute(Attribute<T> key, T value) {
275 public <T> void attribute(Attribute<T> key, T value) {
266 attributes.attribute(key, value);
276 attributes.attribute(key, value);
267 }
277 }
268
278
269 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
279 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
270 attributes.attributeProvider(key, value);
280 attributes.attributeProvider(key, value);
271 }
281 }
272
282
273 public void string(String name, String value) {
283 public void string(String name, String value) {
274 attribute(Attribute.of(name, String.class), value);
284 attribute(Attribute.of(name, String.class), value);
275 }
285 }
276
286
277 public void string(String name, Provider<? extends String> value) {
287 public void string(String name, Provider<? extends String> value) {
278 attributeProvider(Attribute.of(name, String.class), value);
288 attributeProvider(Attribute.of(name, String.class), value);
279 }
289 }
280
290
281 public void bool(String name, boolean value) {
291 public void bool(String name, boolean value) {
282 attribute(Attribute.of(name, Boolean.class), value);
292 attribute(Attribute.of(name, Boolean.class), value);
283 }
293 }
284
294
285 public void bool(String name, Provider<? extends Boolean> value) {
295 public void bool(String name, Provider<? extends Boolean> value) {
286 attributeProvider(Attribute.of(name, Boolean.class), value);
296 attributeProvider(Attribute.of(name, Boolean.class), value);
287 }
297 }
288
298
289 public void integer(String name, int value) {
299 public void integer(String name, int value) {
290 attribute(Attribute.of(name, Integer.class), value);
300 attribute(Attribute.of(name, Integer.class), value);
291 }
301 }
292
302
293 public void integer(String name, Provider<? extends Integer> value) {
303 public void integer(String name, Provider<? extends Integer> value) {
294 attributeProvider(Attribute.of(name, Integer.class), value);
304 attributeProvider(Attribute.of(name, Integer.class), value);
295 }
305 }
296
306
297 public VariantAttributes asAttributes() {
307 public VariantAttributes asAttributes() {
298 return attributes;
308 return attributes;
299 }
309 }
300 }
310 }
301 }
311 }
@@ -1,346 +1,352
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;
4 import java.util.Collection;
5 import java.util.Collections;
5 import java.util.Collections;
6 import java.util.HashMap;
6 import java.util.HashMap;
7 import java.util.HashSet;
7 import java.util.HashSet;
8 import java.util.LinkedHashMap;
8 import java.util.LinkedHashMap;
9 import java.util.LinkedHashSet;
9 import java.util.LinkedHashSet;
10 import java.util.List;
10 import java.util.List;
11 import java.util.Map;
11 import java.util.Map;
12 import java.util.Optional;
12 import java.util.Set;
13 import java.util.Set;
13
14
14 import javax.inject.Inject;
15 import javax.inject.Inject;
15
16
16 import org.implab.gradle.common.core.lang.Closures;
17 import org.implab.gradle.common.core.lang.Closures;
17 import org.gradle.api.Action;
18 import org.gradle.api.Action;
18 import org.gradle.api.InvalidUserDataException;
19 import org.gradle.api.InvalidUserDataException;
19 import org.gradle.api.NamedDomainObjectContainer;
20 import org.gradle.api.NamedDomainObjectContainer;
20 import org.gradle.api.model.ObjectFactory;
21 import org.gradle.api.model.ObjectFactory;
21
22
22 import groovy.lang.Closure;
23 import groovy.lang.Closure;
23
24
24 public abstract class BuildVariantsExtension {
25 public abstract class BuildVariantsExtension {
25 private final NamedDomainObjectContainer<BuildLayer> layers;
26 private final NamedDomainObjectContainer<BuildLayer> layers;
26 private final NamedDomainObjectContainer<BuildVariant> variants;
27 private final NamedDomainObjectContainer<BuildVariant> variants;
27 private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
28 private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
28 private boolean finalized;
29 private boolean finalized;
29
30
30 @Inject
31 @Inject
31 public BuildVariantsExtension(ObjectFactory objects) {
32 public BuildVariantsExtension(ObjectFactory objects) {
32 layers = objects.domainObjectContainer(BuildLayer.class);
33 layers = objects.domainObjectContainer(BuildLayer.class);
33 variants = objects.domainObjectContainer(BuildVariant.class);
34 variants = objects.domainObjectContainer(BuildVariant.class);
34
35
35 layers.all(layer -> {
36 layers.all(layer -> {
36 if (finalized)
37 if (finalized)
37 throw new InvalidUserDataException(
38 throw new InvalidUserDataException(
38 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
39 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
39 });
40 });
40
41
41 variants.all(variant -> {
42 variants.all(variant -> {
42 if (finalized)
43 if (finalized)
43 throw new InvalidUserDataException(
44 throw new InvalidUserDataException(
44 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
45 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
45 });
46 });
46 }
47 }
47
48
48 public NamedDomainObjectContainer<BuildLayer> getLayers() {
49 public NamedDomainObjectContainer<BuildLayer> getLayers() {
49 return layers;
50 return layers;
50 }
51 }
51
52
52 public NamedDomainObjectContainer<BuildVariant> getVariants() {
53 public NamedDomainObjectContainer<BuildVariant> getVariants() {
53 return variants;
54 return variants;
54 }
55 }
55
56
56 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
57 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
57 ensureMutable("configure layers");
58 ensureMutable("configure layers");
58 action.execute(layers);
59 action.execute(layers);
59 }
60 }
60
61
61 public void layers(Closure<?> configure) {
62 public void layers(Closure<?> configure) {
62 layers(Closures.action(configure));
63 layers(Closures.action(configure));
63 }
64 }
64
65
65 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
66 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
66 ensureMutable("configure variants");
67 ensureMutable("configure variants");
67 action.execute(variants);
68 action.execute(variants);
68 }
69 }
69
70
70 public void variants(Closure<?> configure) {
71 public void variants(Closure<?> configure) {
71 variants(Closures.action(configure));
72 variants(Closures.action(configure));
72 }
73 }
73
74
74 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
75 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
75 ensureMutable("configure layers");
76 ensureMutable("configure layers");
76 var layer = layers.maybeCreate(name);
77 var layer = layers.maybeCreate(name);
77 configure.execute(layer);
78 configure.execute(layer);
78 return layer;
79 return layer;
79 }
80 }
80
81
81 public BuildLayer layer(String name, Closure<?> configure) {
82 public BuildLayer layer(String name, Closure<?> configure) {
82 return layer(name, Closures.action(configure));
83 return layer(name, Closures.action(configure));
83 }
84 }
84
85
85 public BuildLayer layer(String name) {
86 public BuildLayer layer(String name) {
86 return layer(name, it -> {
87 return layer(name, it -> {
87 });
88 });
88 }
89 }
89
90
90 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
91 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
91 ensureMutable("configure variants");
92 ensureMutable("configure variants");
92 var variant = variants.maybeCreate(name);
93 var variant = variants.maybeCreate(name);
93 configure.execute(variant);
94 configure.execute(variant);
94 return variant;
95 return variant;
95 }
96 }
96
97
97 public BuildVariant variant(String name, Closure<?> configure) {
98 public BuildVariant variant(String name, Closure<?> configure) {
98 return variant(name, Closures.action(configure));
99 return variant(name, Closures.action(configure));
99 }
100 }
100
101
101 public BuildVariant variant(String name) {
102 public BuildVariant variant(String name) {
102 return variant(name, it -> {
103 return variant(name, it -> {
103 });
104 });
104 }
105 }
105
106
106 public void all(Action<? super BuildVariant> action) {
107 public void all(Action<? super BuildVariant> action) {
107 variants.all(action);
108 variants.all(action);
108 }
109 }
109
110
110 public void all(Closure<?> configure) {
111 public void all(Closure<?> configure) {
111 all(Closures.action(configure));
112 all(Closures.action(configure));
112 }
113 }
113
114
114 public Collection<BuildVariant> getAll() {
115 public Collection<BuildVariant> getAll() {
115 var all = new ArrayList<BuildVariant>();
116 var all = new ArrayList<BuildVariant>();
116 variants.forEach(all::add);
117 variants.forEach(all::add);
117 return Collections.unmodifiableList(all);
118 return Collections.unmodifiableList(all);
118 }
119 }
119
120
120 public BuildVariant getByName(String name) {
121 public Optional<BuildVariant> find(String name) {
121 return variants.findByName(name);
122 return Optional.ofNullable(variants.findByName(name));
123 }
124
125 public BuildVariant require(String name) {
126 return find(name)
127 .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't defined"));
122 }
128 }
123
129
124 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
130 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
125 if (finalized) {
131 if (finalized) {
126 action.execute(this);
132 action.execute(this);
127 return;
133 return;
128 }
134 }
129 finalizedActions.add(action);
135 finalizedActions.add(action);
130 }
136 }
131
137
132 public void whenFinalized(Closure<?> configure) {
138 public void whenFinalized(Closure<?> configure) {
133 whenFinalized(Closures.action(configure));
139 whenFinalized(Closures.action(configure));
134 }
140 }
135
141
136 public boolean isFinalized() {
142 public boolean isFinalized() {
137 return finalized;
143 return finalized;
138 }
144 }
139
145
140 public void finalizeModel() {
146 public void finalizeModel() {
141 if (finalized)
147 if (finalized)
142 return;
148 return;
143
149
144 validate();
150 validate();
145
151
146 for (var variant : variants)
152 for (var variant : variants)
147 variant.finalizeModel();
153 variant.finalizeModel();
148
154
149 finalized = true;
155 finalized = true;
150
156
151 var actions = new ArrayList<>(finalizedActions);
157 var actions = new ArrayList<>(finalizedActions);
152 finalizedActions.clear();
158 finalizedActions.clear();
153 for (var action : actions)
159 for (var action : actions)
154 action.execute(this);
160 action.execute(this);
155 }
161 }
156
162
157 public void validate() {
163 public void validate() {
158 var errors = new ArrayList<String>();
164 var errors = new ArrayList<String>();
159
165
160 var layersByName = new LinkedHashMap<String, BuildLayer>();
166 var layersByName = new LinkedHashMap<String, BuildLayer>();
161 for (var layer : layers) {
167 for (var layer : layers) {
162 var layerName = normalize(layer.getName());
168 var layerName = normalize(layer.getName());
163 if (layerName == null) {
169 if (layerName == null) {
164 errors.add("Layer name must not be blank");
170 errors.add("Layer name must not be blank");
165 continue;
171 continue;
166 }
172 }
167
173
168 var previous = layersByName.putIfAbsent(layerName, layer);
174 var previous = layersByName.putIfAbsent(layerName, layer);
169 if (previous != null) {
175 if (previous != null) {
170 errors.add("Layer '" + layerName + "' is declared more than once");
176 errors.add("Layer '" + layerName + "' is declared more than once");
171 }
177 }
172 }
178 }
173
179
174 for (var variant : variants)
180 for (var variant : variants)
175 validateVariant(variant, layersByName, errors);
181 validateVariant(variant, layersByName, errors);
176
182
177 if (!errors.isEmpty()) {
183 if (!errors.isEmpty()) {
178 var message = new StringBuilder("Invalid variants model:");
184 var message = new StringBuilder("Invalid variants model:");
179 for (var error : errors)
185 for (var error : errors)
180 message.append("\n - ").append(error);
186 message.append("\n - ").append(error);
181
187
182 throw new InvalidUserDataException(message.toString());
188 throw new InvalidUserDataException(message.toString());
183 }
189 }
184 }
190 }
185
191
186 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
192 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
187 var variantName = normalize(variant.getName());
193 var variantName = normalize(variant.getName());
188 if (variantName == null) {
194 if (variantName == null) {
189 errors.add("Variant name must not be blank");
195 errors.add("Variant name must not be blank");
190 return;
196 return;
191 }
197 }
192
198
193 validateRoleAndArtifactNames(variant, errors);
199 validateRoleAndArtifactNames(variant, errors);
194 var variantLayers = validateRoleMappings(variant, layersByName, errors);
200 var variantLayers = validateRoleMappings(variant, layersByName, errors);
195 validateLinks(variant, variantLayers, errors);
201 validateLinks(variant, variantLayers, errors);
196 }
202 }
197
203
198 private static void validateRoleAndArtifactNames(BuildVariant variant, List<String> errors) {
204 private static void validateRoleAndArtifactNames(BuildVariant variant, List<String> errors) {
199 var roleNames = new LinkedHashSet<String>();
205 var roleNames = new LinkedHashSet<String>();
200 for (var role : variant.getRoles()) {
206 for (var role : variant.getRoles()) {
201 var roleName = normalize(role.getName());
207 var roleName = normalize(role.getName());
202 if (roleName == null) {
208 if (roleName == null) {
203 errors.add("Variant '" + variant.getName() + "' contains blank role name");
209 errors.add("Variant '" + variant.getName() + "' contains blank role name");
204 continue;
210 continue;
205 }
211 }
206 if (!roleNames.add(roleName)) {
212 if (!roleNames.add(roleName)) {
207 errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
213 errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
208 }
214 }
209 }
215 }
210
216
211 var slotNames = new LinkedHashSet<String>();
217 var slotNames = new LinkedHashSet<String>();
212 for (var slot : variant.getArtifactSlots()) {
218 for (var slot : variant.getArtifactSlots()) {
213 var slotName = normalize(slot.getName());
219 var slotName = normalize(slot.getName());
214 if (slotName == null) {
220 if (slotName == null) {
215 errors.add("Variant '" + variant.getName() + "' contains blank artifact slot name");
221 errors.add("Variant '" + variant.getName() + "' contains blank artifact slot name");
216 continue;
222 continue;
217 }
223 }
218 if (!slotNames.add(slotName)) {
224 if (!slotNames.add(slotName)) {
219 errors.add("Variant '" + variant.getName() + "' contains duplicated artifact slot name '" + slotName + "'");
225 errors.add("Variant '" + variant.getName() + "' contains duplicated artifact slot name '" + slotName + "'");
220 }
226 }
221 }
227 }
222 }
228 }
223
229
224 private static Set<String> validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
230 private static Set<String> validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
225 List<String> errors) {
231 List<String> errors) {
226 var variantLayers = new LinkedHashSet<String>();
232 var variantLayers = new LinkedHashSet<String>();
227
233
228 for (var role : variant.getRoles()) {
234 for (var role : variant.getRoles()) {
229 var seenLayers = new LinkedHashSet<String>();
235 var seenLayers = new LinkedHashSet<String>();
230 for (var layerName : role.getLayers().getOrElse(List.of())) {
236 for (var layerName : role.getLayers().getOrElse(List.of())) {
231 var normalizedLayerName = normalize(layerName);
237 var normalizedLayerName = normalize(layerName);
232 if (normalizedLayerName == null) {
238 if (normalizedLayerName == null) {
233 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
239 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
234 continue;
240 continue;
235 }
241 }
236
242
237 var layer = layersByName.get(normalizedLayerName);
243 var layer = layersByName.get(normalizedLayerName);
238 if (layer == null) {
244 if (layer == null) {
239 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
245 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
240 continue;
246 continue;
241 }
247 }
242
248
243 if (!seenLayers.add(normalizedLayerName)) {
249 if (!seenLayers.add(normalizedLayerName)) {
244 errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
250 errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
245 + "' contains duplicated layer reference '" + normalizedLayerName + "'");
251 + "' contains duplicated layer reference '" + normalizedLayerName + "'");
246 continue;
252 continue;
247 }
253 }
248
254
249 variantLayers.add(normalizedLayerName);
255 variantLayers.add(normalizedLayerName);
250 }
256 }
251 }
257 }
252
258
253 return variantLayers;
259 return variantLayers;
254 }
260 }
255
261
256 private static void validateLinks(BuildVariant variant, Set<String> variantLayers, List<String> errors) {
262 private static void validateLinks(BuildVariant variant, Set<String> variantLayers, List<String> errors) {
257 var seenLinks = new HashSet<String>();
263 var seenLinks = new HashSet<String>();
258 var edgesByKind = new HashMap<String, Map<String, Set<String>>>();
264 var edgesByKind = new HashMap<String, Map<String, Set<String>>>();
259
265
260 for (var link : variant.getLinks()) {
266 for (var link : variant.getLinks()) {
261 var from = normalize(link.from());
267 var from = normalize(link.from());
262 var to = normalize(link.to());
268 var to = normalize(link.to());
263 var kind = normalize(link.kind());
269 var kind = normalize(link.kind());
264
270
265 if (from == null || to == null || kind == null) {
271 if (from == null || to == null || kind == null) {
266 errors.add("Variant '" + variant.getName() + "' has incomplete link (from/to/kind are required)");
272 errors.add("Variant '" + variant.getName() + "' has incomplete link (from/to/kind are required)");
267 continue;
273 continue;
268 }
274 }
269
275
270 if (!variantLayers.contains(from)) {
276 if (!variantLayers.contains(from)) {
271 errors.add("Variant '" + variant.getName() + "' link references unknown source layer '"
277 errors.add("Variant '" + variant.getName() + "' link references unknown source layer '"
272 + from + "'");
278 + from + "'");
273 continue;
279 continue;
274 }
280 }
275
281
276 if (!variantLayers.contains(to)) {
282 if (!variantLayers.contains(to)) {
277 errors.add("Variant '" + variant.getName() + "' link references unknown target layer '"
283 errors.add("Variant '" + variant.getName() + "' link references unknown target layer '"
278 + to + "'");
284 + to + "'");
279 continue;
285 continue;
280 }
286 }
281
287
282 var linkKey = from + "\u0000" + to + "\u0000" + kind;
288 var linkKey = from + "\u0000" + to + "\u0000" + kind;
283 if (!seenLinks.add(linkKey)) {
289 if (!seenLinks.add(linkKey)) {
284 errors.add("Variant '" + variant.getName() + "' has duplicated link tuple (from='" + from
290 errors.add("Variant '" + variant.getName() + "' has duplicated link tuple (from='" + from
285 + "', to='" + to + "', kind='" + kind + "')");
291 + "', to='" + to + "', kind='" + kind + "')");
286 }
292 }
287
293
288 edgesByKind
294 edgesByKind
289 .computeIfAbsent(kind, x -> new LinkedHashMap<>())
295 .computeIfAbsent(kind, x -> new LinkedHashMap<>())
290 .computeIfAbsent(from, x -> new LinkedHashSet<>())
296 .computeIfAbsent(from, x -> new LinkedHashSet<>())
291 .add(to);
297 .add(to);
292 }
298 }
293
299
294 for (var entry : edgesByKind.entrySet()) {
300 for (var entry : edgesByKind.entrySet()) {
295 if (hasCycle(variantLayers, entry.getValue())) {
301 if (hasCycle(variantLayers, entry.getValue())) {
296 errors.add("Variant '" + variant.getName() + "' contains cycle in links with kind '" + entry.getKey() + "'");
302 errors.add("Variant '" + variant.getName() + "' contains cycle in links with kind '" + entry.getKey() + "'");
297 }
303 }
298 }
304 }
299 }
305 }
300
306
301 private static boolean hasCycle(Set<String> nodes, Map<String, Set<String>> edges) {
307 private static boolean hasCycle(Set<String> nodes, Map<String, Set<String>> edges) {
302 var state = new HashMap<String, Integer>();
308 var state = new HashMap<String, Integer>();
303
309
304 for (var node : nodes) {
310 for (var node : nodes) {
305 if (dfs(node, state, edges))
311 if (dfs(node, state, edges))
306 return true;
312 return true;
307 }
313 }
308
314
309 return false;
315 return false;
310 }
316 }
311
317
312 private static boolean dfs(String node, Map<String, Integer> state, Map<String, Set<String>> edges) {
318 private static boolean dfs(String node, Map<String, Integer> state, Map<String, Set<String>> edges) {
313 var current = state.getOrDefault(node, 0);
319 var current = state.getOrDefault(node, 0);
314 if (current == 1)
320 if (current == 1)
315 return true;
321 return true;
316 if (current == 2)
322 if (current == 2)
317 return false;
323 return false;
318
324
319 state.put(node, 1);
325 state.put(node, 1);
320
326
321 for (var next : edges.getOrDefault(node, Set.of())) {
327 for (var next : edges.getOrDefault(node, Set.of())) {
322 if (dfs(next, state, edges))
328 if (dfs(next, state, edges))
323 return true;
329 return true;
324 }
330 }
325
331
326 state.put(node, 2);
332 state.put(node, 2);
327 return false;
333 return false;
328 }
334 }
329
335
330 private static String normalize(String value) {
336 private static String normalize(String value) {
331 if (value == null)
337 if (value == null)
332 return null;
338 return null;
333
339
334 var trimmed = value.trim();
340 var trimmed = value.trim();
335 return trimmed.isEmpty() ? null : trimmed;
341 return trimmed.isEmpty() ? null : trimmed;
336 }
342 }
337
343
338 private static boolean isBlank(String value) {
344 private static boolean isBlank(String value) {
339 return normalize(value) == null;
345 return normalize(value) == null;
340 }
346 }
341
347
342 private void ensureMutable(String operation) {
348 private void ensureMutable(String operation) {
343 if (finalized)
349 if (finalized)
344 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
350 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
345 }
351 }
346 }
352 }
@@ -1,59 +1,71
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.common.sources;
2
2
3 import java.util.Collections;
3 import java.util.Collections;
4 import java.util.LinkedHashMap;
4 import java.util.LinkedHashMap;
5 import java.util.Map;
5 import java.util.Map;
6
6
7 import org.gradle.api.InvalidUserDataException;
7 import org.gradle.api.InvalidUserDataException;
8 import org.gradle.api.attributes.Attribute;
8 import org.gradle.api.attributes.Attribute;
9 import org.gradle.api.provider.ProviderFactory;
9 import org.gradle.api.provider.ProviderFactory;
10 import org.gradle.api.provider.Provider;
10 import org.gradle.api.provider.Provider;
11
11
12 /**
12 /**
13 * Typed attribute storage used by build variants.
13 * Typed attribute storage used by build variants.
14 */
14 */
15 public final class VariantAttributes {
15 public final class VariantAttributes {
16 private final ProviderFactory providers;
16 private final ProviderFactory providers;
17 private final LinkedHashMap<Attribute<?>, Provider<?>> values = new LinkedHashMap<>();
17 private final LinkedHashMap<Attribute<?>, Provider<?>> values = new LinkedHashMap<>();
18 private final Provider<Object> emptyValueProvider;
18 private boolean finalized;
19 private boolean finalized;
19
20
20 VariantAttributes(ProviderFactory providers) {
21 VariantAttributes(ProviderFactory providers) {
21 this.providers = providers;
22 this.providers = providers;
23 this.emptyValueProvider = providers.provider(() -> null);
22 }
24 }
23
25
24 public <T> void attribute(Attribute<T> key, T value) {
26 public <T> void attribute(Attribute<T> key, T value) {
25 ensureMutable("set attribute '" + key.getName() + "'");
27 ensureMutable("set attribute '" + key.getName() + "'");
26 attributeProvider(key, providers.provider(() -> value));
28 attributeProvider(key, providers.provider(() -> value));
27 }
29 }
28
30
29 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
31 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
30 ensureMutable("set attribute provider '" + key.getName() + "'");
32 ensureMutable("set attribute provider '" + key.getName() + "'");
31 values.put(key, value);
33 values.put(key, value);
32 }
34 }
33
35
34 @SuppressWarnings("unchecked")
36 @SuppressWarnings("unchecked")
35 public <T> Provider<T> get(Attribute<T> key) {
37 public <T> Provider<T> find(Attribute<T> key) {
36 return (Provider<T>) values.get(key);
38 return providers
39 .provider(() -> (Provider<T>) values.getOrDefault(key, emptyValueProvider))
40 .flatMap(provider -> provider);
41 }
42
43 public <T> Provider<T> require(Attribute<T> key) {
44 var value = find(key);
45 if (!value.isPresent())
46 throw new InvalidUserDataException("Attribute '" + key.getName() + "' doesn't have a value");
47
48 return value;
37 }
49 }
38
50
39 public boolean contains(Attribute<?> key) {
51 public boolean contains(Attribute<?> key) {
40 return values.containsKey(key);
52 return values.containsKey(key);
41 }
53 }
42
54
43 public int size() {
55 public int size() {
44 return values.size();
56 return values.size();
45 }
57 }
46
58
47 public Map<Attribute<?>, Provider<?>> asMap() {
59 public Map<Attribute<?>, Provider<?>> asMap() {
48 return Collections.unmodifiableMap(values);
60 return Collections.unmodifiableMap(values);
49 }
61 }
50
62
51 void finalizeModel() {
63 void finalizeModel() {
52 finalized = true;
64 finalized = true;
53 }
65 }
54
66
55 private void ensureMutable(String operation) {
67 private void ensureMutable(String operation) {
56 if (finalized)
68 if (finalized)
57 throw new InvalidUserDataException("Variant attributes are finalized and cannot " + operation);
69 throw new InvalidUserDataException("Variant attributes are finalized and cannot " + operation);
58 }
70 }
59 }
71 }
@@ -1,330 +1,330
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
12
13 import org.gradle.testkit.runner.BuildResult;
13 import org.gradle.testkit.runner.BuildResult;
14 import org.gradle.testkit.runner.GradleRunner;
14 import org.gradle.testkit.runner.GradleRunner;
15 import org.gradle.testkit.runner.TaskOutcome;
15 import org.gradle.testkit.runner.TaskOutcome;
16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 import org.junit.jupiter.api.Test;
17 import org.junit.jupiter.api.Test;
18 import org.junit.jupiter.api.io.TempDir;
18 import org.junit.jupiter.api.io.TempDir;
19
19
20 class VariantsPluginFunctionalTest {
20 class VariantsPluginFunctionalTest {
21 private static final String SETTINGS_FILE = "settings.gradle";
21 private static final String SETTINGS_FILE = "settings.gradle";
22 private static final String BUILD_FILE = "build.gradle";
22 private static final String BUILD_FILE = "build.gradle";
23 private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n";
23 private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n";
24
24
25 @TempDir
25 @TempDir
26 Path testProjectDir;
26 Path testProjectDir;
27
27
28 @Test
28 @Test
29 void configuresVariantModelWithDsl() throws Exception {
29 void configuresVariantModelWithDsl() throws Exception {
30 writeFile(SETTINGS_FILE, ROOT_NAME);
30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 writeFile(BUILD_FILE, """
31 writeFile(BUILD_FILE, """
32 plugins {
32 plugins {
33 id 'org.implab.gradle-variants'
33 id 'org.implab.gradle-variants'
34 }
34 }
35
35
36 variants {
36 variants {
37 layer('mainBase') {
37 layer('mainBase') {
38 }
38 }
39
39
40 layer('mainAmd') {
40 layer('mainAmd') {
41 }
41 }
42
42
43 variant('browser') {
43 variant('browser') {
44 attributes {
44 attributes {
45 string('jsRuntime', 'browser')
45 string('jsRuntime', 'browser')
46 string('jsModule', 'amd')
46 string('jsModule', 'amd')
47 }
47 }
48 role('main') {
48 role('main') {
49 layers('mainBase', 'mainAmd')
49 layers('mainBase', 'mainAmd')
50 }
50 }
51 link('mainBase', 'mainAmd', 'ts:api')
51 link('mainBase', 'mainAmd', 'ts:api')
52 artifactSlot('mainCompiled')
52 artifactSlot('mainCompiled')
53 }
53 }
54 }
54 }
55
55
56 tasks.register('probe') {
56 tasks.register('probe') {
57 doLast {
57 doLast {
58 def browser = variants.getByName('browser')
58 def browser = variants.require('browser')
59 println('attributes=' + browser.attributes.size())
59 println('attributes=' + browser.attributes.size())
60 println('roles=' + browser.roles.size())
60 println('roles=' + browser.roles.size())
61 println('links=' + browser.links.size())
61 println('links=' + browser.links.size())
62 println('slots=' + browser.artifactSlots.size())
62 println('slots=' + browser.artifactSlots.size())
63 }
63 }
64 }
64 }
65 """);
65 """);
66
66
67 BuildResult result = runner("probe").build();
67 BuildResult result = runner("probe").build();
68
68
69 assertTrue(result.getOutput().contains("attributes=2"));
69 assertTrue(result.getOutput().contains("attributes=2"));
70 assertTrue(result.getOutput().contains("roles=1"));
70 assertTrue(result.getOutput().contains("roles=1"));
71 assertTrue(result.getOutput().contains("links=1"));
71 assertTrue(result.getOutput().contains("links=1"));
72 assertTrue(result.getOutput().contains("slots=1"));
72 assertTrue(result.getOutput().contains("slots=1"));
73 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
73 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
74 }
74 }
75
75
76 @Test
76 @Test
77 void failsOnUnknownLayerReference() throws Exception {
77 void failsOnUnknownLayerReference() throws Exception {
78 assertBuildFails("""
78 assertBuildFails("""
79 plugins {
79 plugins {
80 id 'org.implab.gradle-variants'
80 id 'org.implab.gradle-variants'
81 }
81 }
82
82
83 variants {
83 variants {
84 layer('mainBase') {
84 layer('mainBase') {
85 }
85 }
86
86
87 variant('browser') {
87 variant('browser') {
88 role('main') {
88 role('main') {
89 layers('mainBase', 'missingLayer')
89 layers('mainBase', 'missingLayer')
90 }
90 }
91 }
91 }
92 }
92 }
93 """, "references unknown layer 'missingLayer'");
93 """, "references unknown layer 'missingLayer'");
94 }
94 }
95
95
96 @Test
96 @Test
97 void failsOnCycleInLinksByKind() throws Exception {
97 void failsOnCycleInLinksByKind() throws Exception {
98 assertBuildFails("""
98 assertBuildFails("""
99 plugins {
99 plugins {
100 id 'org.implab.gradle-variants'
100 id 'org.implab.gradle-variants'
101 }
101 }
102
102
103 variants {
103 variants {
104 layer('a')
104 layer('a')
105 layer('b')
105 layer('b')
106
106
107 variant('browser') {
107 variant('browser') {
108 role('main') {
108 role('main') {
109 layers('a', 'b')
109 layers('a', 'b')
110 }
110 }
111 link('a', 'b', 'ts:api')
111 link('a', 'b', 'ts:api')
112 link('b', 'a', 'ts:api')
112 link('b', 'a', 'ts:api')
113 }
113 }
114 }
114 }
115 """, "contains cycle in links with kind 'ts:api'");
115 """, "contains cycle in links with kind 'ts:api'");
116 }
116 }
117
117
118 @Test
118 @Test
119 void allowsUsingLayerFromDifferentVariantRole() throws Exception {
119 void allowsUsingLayerFromDifferentVariantRole() throws Exception {
120 writeFile(SETTINGS_FILE, ROOT_NAME);
120 writeFile(SETTINGS_FILE, ROOT_NAME);
121 writeFile(BUILD_FILE, """
121 writeFile(BUILD_FILE, """
122 plugins {
122 plugins {
123 id 'org.implab.gradle-variants'
123 id 'org.implab.gradle-variants'
124 }
124 }
125
125
126 variants {
126 variants {
127 layer('mainBase')
127 layer('mainBase')
128
128
129 variant('browser') {
129 variant('browser') {
130 role('test') {
130 role('test') {
131 layers('mainBase')
131 layers('mainBase')
132 }
132 }
133 }
133 }
134 }
134 }
135 """);
135 """);
136
136
137 BuildResult result = runner("help").build();
137 BuildResult result = runner("help").build();
138 assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
138 assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
139 }
139 }
140
140
141 @Test
141 @Test
142 void failsOnIncompleteLink() throws Exception {
142 void failsOnIncompleteLink() throws Exception {
143 assertBuildFails("""
143 assertBuildFails("""
144 plugins {
144 plugins {
145 id 'org.implab.gradle-variants'
145 id 'org.implab.gradle-variants'
146 }
146 }
147
147
148 variants {
148 variants {
149 layer('a')
149 layer('a')
150 layer('b')
150 layer('b')
151
151
152 variant('browser') {
152 variant('browser') {
153 role('main') {
153 role('main') {
154 layers('a', 'b')
154 layers('a', 'b')
155 }
155 }
156 link('a', 'b', null)
156 link('a', 'b', null)
157 }
157 }
158 }
158 }
159 """, "Link 'kind' must not be null or blank");
159 """, "Link 'kind' must not be null or blank");
160 }
160 }
161
161
162 @Test
162 @Test
163 void failsOnDuplicatedLinkTuple() throws Exception {
163 void failsOnDuplicatedLinkTuple() throws Exception {
164 assertBuildFails("""
164 assertBuildFails("""
165 plugins {
165 plugins {
166 id 'org.implab.gradle-variants'
166 id 'org.implab.gradle-variants'
167 }
167 }
168
168
169 variants {
169 variants {
170 layer('a')
170 layer('a')
171 layer('b')
171 layer('b')
172
172
173 variant('browser') {
173 variant('browser') {
174 role('main') {
174 role('main') {
175 layers('a', 'b')
175 layers('a', 'b')
176 }
176 }
177 link('a', 'b', 'ts:api')
177 link('a', 'b', 'ts:api')
178 link('a', 'b', 'ts:api')
178 link('a', 'b', 'ts:api')
179 }
179 }
180 }
180 }
181 """, "has duplicated link tuple (from='a', to='b', kind='ts:api')");
181 """, "has duplicated link tuple (from='a', to='b', kind='ts:api')");
182 }
182 }
183
183
184 @Test
184 @Test
185 void failsOnUnknownSourceLayerInLink() throws Exception {
185 void failsOnUnknownSourceLayerInLink() throws Exception {
186 assertBuildFails("""
186 assertBuildFails("""
187 plugins {
187 plugins {
188 id 'org.implab.gradle-variants'
188 id 'org.implab.gradle-variants'
189 }
189 }
190
190
191 variants {
191 variants {
192 layer('a') {
192 layer('a') {
193 }
193 }
194
194
195 variant('browser') {
195 variant('browser') {
196 role('main') {
196 role('main') {
197 layers('a')
197 layers('a')
198 }
198 }
199 link('missing', 'a', 'ts:api')
199 link('missing', 'a', 'ts:api')
200 }
200 }
201 }
201 }
202 """, "references unknown source layer 'missing'");
202 """, "references unknown source layer 'missing'");
203 }
203 }
204
204
205 @Test
205 @Test
206 void failsOnUnknownTargetLayerInLink() throws Exception {
206 void failsOnUnknownTargetLayerInLink() throws Exception {
207 assertBuildFails("""
207 assertBuildFails("""
208 plugins {
208 plugins {
209 id 'org.implab.gradle-variants'
209 id 'org.implab.gradle-variants'
210 }
210 }
211
211
212 variants {
212 variants {
213 layer('a') {
213 layer('a') {
214 }
214 }
215
215
216 variant('browser') {
216 variant('browser') {
217 role('main') {
217 role('main') {
218 layers('a')
218 layers('a')
219 }
219 }
220 link('a', 'missing', 'ts:api')
220 link('a', 'missing', 'ts:api')
221 }
221 }
222 }
222 }
223 """, "references unknown target layer 'missing'");
223 """, "references unknown target layer 'missing'");
224 }
224 }
225
225
226 @Test
226 @Test
227 void failsOnDuplicatedLayerReferenceInRole() throws Exception {
227 void failsOnDuplicatedLayerReferenceInRole() throws Exception {
228 assertBuildFails("""
228 assertBuildFails("""
229 plugins {
229 plugins {
230 id 'org.implab.gradle-variants'
230 id 'org.implab.gradle-variants'
231 }
231 }
232
232
233 variants {
233 variants {
234 layer('a')
234 layer('a')
235
235
236 variant('browser') {
236 variant('browser') {
237 role('main') {
237 role('main') {
238 layers('a', 'a')
238 layers('a', 'a')
239 }
239 }
240 }
240 }
241 }
241 }
242 """, "contains duplicated layer reference 'a'");
242 """, "contains duplicated layer reference 'a'");
243 }
243 }
244
244
245 @Test
245 @Test
246 void failsOnLateLayerMutationAfterFinalize() throws Exception {
246 void failsOnLateLayerMutationAfterFinalize() throws Exception {
247 assertBuildFails("""
247 assertBuildFails("""
248 plugins {
248 plugins {
249 id 'org.implab.gradle-variants'
249 id 'org.implab.gradle-variants'
250 }
250 }
251
251
252 variants {
252 variants {
253 layer('a')
253 layer('a')
254 variant('browser') {
254 variant('browser') {
255 role('main') { layers('a') }
255 role('main') { layers('a') }
256 }
256 }
257 }
257 }
258
258
259 afterEvaluate {
259 afterEvaluate {
260 variants.layer('late')
260 variants.layer('late')
261 }
261 }
262 """, "Variants model is finalized and cannot configure layers");
262 """, "Variants model is finalized and cannot configure layers");
263 }
263 }
264
264
265 @Test
265 @Test
266 void failsOnLateVariantMutationAfterFinalize() throws Exception {
266 void failsOnLateVariantMutationAfterFinalize() throws Exception {
267 assertBuildFails("""
267 assertBuildFails("""
268 plugins {
268 plugins {
269 id 'org.implab.gradle-variants'
269 id 'org.implab.gradle-variants'
270 }
270 }
271
271
272 variants {
272 variants {
273 layer('a')
273 layer('a')
274 variant('browser') {
274 variant('browser') {
275 role('main') { layers('a') }
275 role('main') { layers('a') }
276 }
276 }
277 }
277 }
278
278
279 afterEvaluate {
279 afterEvaluate {
280 variants.getByName('browser').role('late') { layers('a') }
280 variants.require('browser').role('late') { layers('a') }
281 }
281 }
282 """, "Variant 'browser' is finalized and cannot configure roles");
282 """, "Variant 'browser' is finalized and cannot configure roles");
283 }
283 }
284
284
285 private GradleRunner runner(String... arguments) {
285 private GradleRunner runner(String... arguments) {
286 return GradleRunner.create()
286 return GradleRunner.create()
287 .withProjectDir(testProjectDir.toFile())
287 .withProjectDir(testProjectDir.toFile())
288 .withPluginClasspath(pluginClasspath())
288 .withPluginClasspath(pluginClasspath())
289 .withArguments(arguments)
289 .withArguments(arguments)
290 .forwardOutput();
290 .forwardOutput();
291 }
291 }
292
292
293 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
293 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
294 writeFile(SETTINGS_FILE, ROOT_NAME);
294 writeFile(SETTINGS_FILE, ROOT_NAME);
295 writeFile(BUILD_FILE, buildScript);
295 writeFile(BUILD_FILE, buildScript);
296
296
297 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
297 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
298 var output = ex.getBuildResult().getOutput();
298 var output = ex.getBuildResult().getOutput();
299
299
300 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
300 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
301 }
301 }
302
302
303 private static List<File> pluginClasspath() {
303 private static List<File> pluginClasspath() {
304 try {
304 try {
305 var classesDir = Path.of(BuildVariant.class
305 var classesDir = Path.of(BuildVariant.class
306 .getProtectionDomain()
306 .getProtectionDomain()
307 .getCodeSource()
307 .getCodeSource()
308 .getLocation()
308 .getLocation()
309 .toURI());
309 .toURI());
310
310
311 var markerResource = VariantsPlugin.class.getClassLoader()
311 var markerResource = VariantsPlugin.class.getClassLoader()
312 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
312 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
313
313
314 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
314 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
315
315
316 var markerPath = Path.of(markerResource.toURI());
316 var markerPath = Path.of(markerResource.toURI());
317 var resourcesDir = markerPath.getParent().getParent().getParent();
317 var resourcesDir = markerPath.getParent().getParent().getParent();
318
318
319 return List.of(classesDir.toFile(), resourcesDir.toFile());
319 return List.of(classesDir.toFile(), resourcesDir.toFile());
320 } catch (Exception e) {
320 } catch (Exception e) {
321 throw new RuntimeException("Unable to build plugin classpath for test", e);
321 throw new RuntimeException("Unable to build plugin classpath for test", e);
322 }
322 }
323 }
323 }
324
324
325 private void writeFile(String relativePath, String content) throws IOException {
325 private void writeFile(String relativePath, String content) throws IOException {
326 Path path = testProjectDir.resolve(relativePath);
326 Path path = testProjectDir.resolve(relativePath);
327 Files.createDirectories(path.getParent());
327 Files.createDirectories(path.getParent());
328 Files.writeString(path, content);
328 Files.writeString(path, content);
329 }
329 }
330 }
330 }
@@ -1,128 +1,128
1 # Variants Plugin
1 # Variants Plugin
2
2
3 ## NAME
3 ## NAME
4
4
5 `VariantsPlugin` и extension `variants`.
5 `VariantsPlugin` и extension `variants`.
6
6
7 ## SYNOPSIS
7 ## SYNOPSIS
8
8
9 ```groovy
9 ```groovy
10 plugins {
10 plugins {
11 id 'org.implab.gradle-variants'
11 id 'org.implab.gradle-variants'
12 }
12 }
13
13
14 variants {
14 variants {
15 layer('mainBase')
15 layer('mainBase')
16 layer('mainAmd')
16 layer('mainAmd')
17
17
18 variant('browser') {
18 variant('browser') {
19 attributes {
19 attributes {
20 string('jsRuntime', 'browser')
20 string('jsRuntime', 'browser')
21 string('jsModule', 'amd')
21 string('jsModule', 'amd')
22 }
22 }
23
23
24 role('main') {
24 role('main') {
25 layers('mainBase', 'mainAmd')
25 layers('mainBase', 'mainAmd')
26 }
26 }
27
27
28 link('mainBase', 'mainAmd', 'ts:api')
28 link('mainBase', 'mainAmd', 'ts:api')
29 artifactSlot('mainCompiled')
29 artifactSlot('mainCompiled')
30 }
30 }
31 }
31 }
32 ```
32 ```
33
33
34 ## DESCRIPTION
34 ## DESCRIPTION
35
35
36 `VariantsPlugin` задает доменную модель сборки и ее валидацию. Плагин не
36 `VariantsPlugin` задает доменную модель сборки и ее валидацию. Плагин не
37 регистрирует compile/copy/bundle задачи напрямую.
37 регистрирует compile/copy/bundle задачи напрямую.
38
38
39 ### layers
39 ### layers
40
40
41 Глобальные логические слои. Служат единым словарем имен, на которые затем
41 Глобальные логические слои. Служат единым словарем имен, на которые затем
42 ссылаются роли и связи.
42 ссылаются роли и связи.
43
43
44 ### variants
44 ### variants
45
45
46 Именованные варианты исполнения/пакетирования (`browser`, `node`, и т.д.).
46 Именованные варианты исполнения/пакетирования (`browser`, `node`, и т.д.).
47 Вариант агрегирует роли, связи, атрибуты и artifact slots.
47 Вариант агрегирует роли, связи, атрибуты и artifact slots.
48
48
49 ### roles
49 ### roles
50
50
51 Роль описывает набор слоев в пределах варианта (`main`, `test`, `tools`).
51 Роль описывает набор слоев в пределах варианта (`main`, `test`, `tools`).
52 Одна роль может ссылаться на несколько слоев.
52 Одна роль может ссылаться на несколько слоев.
53
53
54 ### links
54 ### links
55
55
56 `link(from, to, kind)` — ориентированная связь между слоями внутри варианта.
56 `link(from, to, kind)` — ориентированная связь между слоями внутри варианта.
57
57
58 `kind` задает независимый тип графа (например `ts:api`, `bundle:runtime`). Это
58 `kind` задает независимый тип графа (например `ts:api`, `bundle:runtime`). Это
59 позволяет вести несколько параллельных графов зависимостей над теми же слоями.
59 позволяет вести несколько параллельных графов зависимостей над теми же слоями.
60
60
61 Практические сценарии использования `link` в адаптерах:
61 Практические сценарии использования `link` в адаптерах:
62
62
63 - расчет topological order по выбранному `kind`;
63 - расчет topological order по выбранному `kind`;
64 - wiring task inputs/outputs между слоями;
64 - wiring task inputs/outputs между слоями;
65 - проверка допустимости дополнительных pipeline-зависимостей.
65 - проверка допустимости дополнительных pipeline-зависимостей.
66
66
67 ### attributes
67 ### attributes
68
68
69 Typed-атрибуты (`Attribute<T> -> Provider<T>`) для передачи параметров в
69 Typed-атрибуты (`Attribute<T> -> Provider<T>`) для передачи параметров в
70 адаптеры и публикацию артефактов.
70 адаптеры и публикацию артефактов.
71
71
72 ### artifact slots
72 ### artifact slots
73
73
74 Именованные слоты ожидаемых артефактов варианта. Используются как контракт
74 Именованные слоты ожидаемых артефактов варианта. Используются как контракт
75 между моделью варианта и плагинами, создающими/публикующими результаты.
75 между моделью варианта и плагинами, создающими/публикующими результаты.
76
76
77 ## VALIDATION
77 ## VALIDATION
78
78
79 В `finalizeModel()` выполняется проверка:
79 В `finalizeModel()` выполняется проверка:
80
80
81 - роль не может ссылаться на неизвестный layer;
81 - роль не может ссылаться на неизвестный layer;
82 - пустые имена layer запрещены;
82 - пустые имена layer запрещены;
83 - у link обязательны `from`, `to`, `kind`;
83 - у link обязательны `from`, `to`, `kind`;
84 - `from`/`to` должны входить в слойную область варианта;
84 - `from`/`to` должны входить в слойную область варианта;
85 - tuple `(from, to, kind)` должен быть уникален;
85 - tuple `(from, to, kind)` должен быть уникален;
86 - циклы в графе одного `kind` запрещены.
86 - циклы в графе одного `kind` запрещены.
87
87
88 ## LIFECYCLE
88 ## LIFECYCLE
89
89
90 - `VariantsPlugin` вызывает `variants.finalizeModel()` на `afterEvaluate`.
90 - `VariantsPlugin` вызывает `variants.finalizeModel()` на `afterEvaluate`.
91 - после `finalizeModel()` мутации модели запрещены.
91 - после `finalizeModel()` мутации модели запрещены.
92 - `whenFinalized(...)` replayable.
92 - `whenFinalized(...)` replayable.
93
93
94 ## API
94 ## API
95
95
96 ### BuildVariantsExtension
96 ### BuildVariantsExtension
97
97
98 - `layer(...)` — объявление или конфигурация `BuildLayer`.
98 - `layer(...)` — объявление или конфигурация `BuildLayer`.
99 - `variant(...)` — объявление или конфигурация `BuildVariant`.
99 - `variant(...)` — объявление или конфигурация `BuildVariant`.
100 - `layers { ... }`, `variants { ... }` — контейнерный DSL.
100 - `layers { ... }`, `variants { ... }` — контейнерный DSL.
101 - `all(...)` — callback для всех вариантов.
101 - `all(...)` — callback для всех вариантов.
102 - `getAll()`, `getByName(name)` — доступ к вариантам.
102 - `getAll()`, `find(name)`, `require(name)` — доступ к вариантам.
103 - `validate()` — явный запуск валидации.
103 - `validate()` — явный запуск валидации.
104 - `finalizeModel()` — валидация + финализация модели.
104 - `finalizeModel()` — валидация + финализация модели.
105 - `whenFinalized(...)` — callback по завершенной модели (replayable).
105 - `whenFinalized(...)` — callback по завершенной модели (replayable).
106
106
107 ### BuildVariant
107 ### BuildVariant
108
108
109 - `attributes { ... }` — атрибуты варианта (+ sugar `string/bool/integer`).
109 - `attributes { ... }` — атрибуты варианта (+ sugar `string/bool/integer`).
110 - `role(...)`, `roles { ... }` — роли варианта.
110 - `role(...)`, `roles { ... }` — роли варианта.
111 - `link(...)`, `links { ... }` — связи слоев внутри варианта.
111 - `link(...)`, `links { ... }` — связи слоев внутри варианта.
112 - `artifactSlot(...)`, `artifactSlots { ... }` — артефактные слоты.
112 - `artifactSlot(...)`, `artifactSlots { ... }` — артефактные слоты.
113
113
114 ## KEY CLASSES
114 ## KEY CLASSES
115
115
116 - `VariantsPlugin` — точка входа плагина.
116 - `VariantsPlugin` — точка входа плагина.
117 - `BuildVariantsExtension` — root extension и lifecycle.
117 - `BuildVariantsExtension` — root extension и lifecycle.
118 - `BuildVariant` — агрегатная модель варианта.
118 - `BuildVariant` — агрегатная модель варианта.
119 - `BuildLayer` — модель слоя.
119 - `BuildLayer` — модель слоя.
120 - `BuildRole` — модель роли.
120 - `BuildRole` — модель роли.
121 - `LayerLink` — модель направленной связи.
121 - `LayerLink` — модель направленной связи.
122 - `BuildArtifactSlot` — модель артефактного слота.
122 - `BuildArtifactSlot` — модель артефактного слота.
123 - `VariantAttributes` — typed wrapper для variant attributes.
123 - `VariantAttributes` — typed wrapper для variant attributes.
124
124
125 ## NOTES
125 ## NOTES
126
126
127 - Модель `variants` intentionally agnostic к toolchain.
127 - Модель `variants` intentionally agnostic к toolchain.
128 - Интеграция с задачами выполняется через `variantSources` и адаптеры.
128 - Интеграция с задачами выполняется через `variantSources` и адаптеры.
General Comments 0
You need to be logged in to leave comments. Login now