##// END OF EJS Templates
Variants validation + fail-fast
cin -
r28:1a0b4caf9976 default
parent child
Show More
@@ -1,291 +1,301
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.Collection;
5 5 import java.util.Collections;
6 6 import java.util.LinkedHashMap;
7 7 import java.util.LinkedHashSet;
8 8 import java.util.List;
9 9 import java.util.Set;
10 10
11 11 import javax.inject.Inject;
12 12
13 13 import org.implab.gradle.common.core.lang.Closures;
14 14 import org.gradle.api.Action;
15 15 import org.gradle.api.InvalidUserDataException;
16 16 import org.gradle.api.Named;
17 17 import org.gradle.api.model.ObjectFactory;
18 18 import org.gradle.api.provider.Provider;
19 19 import org.gradle.api.provider.ProviderFactory;
20 20 import org.gradle.api.attributes.Attribute;
21 21
22 22 import groovy.lang.Closure;
23 23
24 24 public abstract class BuildVariant implements Named {
25 25 private final String name;
26 26 private final ObjectFactory objects;
27 27 private boolean finalized;
28 28
29 29 /**
30 30 * Variant aggregate parts.
31 31 */
32 32 private final VariantAttributes attributes;
33 33 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
34 34 private final List<LayerLink> links = new ArrayList<>();
35 35 private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();
36 36
37 37 @Inject
38 38 public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
39 39 this.name = name;
40 40 this.objects = objects;
41 41 attributes = new VariantAttributes(providers);
42 42 }
43 43
44 44 @Override
45 45 public String getName() {
46 46 return name;
47 47 }
48 48
49 49 /**
50 50 * Generic variant attributes interpreted by adapters.
51 51 */
52 52 public VariantAttributes getAttributes() {
53 53 return attributes;
54 54 }
55 55
56 56 public void attributes(Action<? super AttributesSpec> action) {
57 57 ensureMutable("configure attributes");
58 58 action.execute(new AttributesSpec(attributes));
59 59 }
60 60
61 61 public void attributes(Closure<?> configure) {
62 62 attributes(Closures.action(configure));
63 63 }
64 64
65 65 public <T> void attribute(Attribute<T> key, T value) {
66 66 ensureMutable("set attributes");
67 67 attributes.attribute(key, value);
68 68 }
69 69
70 70 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
71 71 ensureMutable("set attributes");
72 72 attributes.attributeProvider(key, value);
73 73 }
74 74
75 75 public Collection<BuildRole> getRoles() {
76 76 return Collections.unmodifiableCollection(roles.values());
77 77 }
78 78
79 79 public void roles(Action<? super RolesSpec> action) {
80 80 ensureMutable("configure roles");
81 81 action.execute(new RolesSpec());
82 82 }
83 83
84 84 public void roles(Closure<?> configure) {
85 85 roles(Closures.action(configure));
86 86 }
87 87
88 88 public BuildRole role(String name, Action<? super BuildRole> configure) {
89 89 ensureMutable("configure roles");
90 90 var role = roles.computeIfAbsent(name, this::newRole);
91 91 configure.execute(role);
92 92 return role;
93 93 }
94 94
95 95 public BuildRole role(String name, Closure<?> configure) {
96 96 return role(name, Closures.action(configure));
97 97 }
98 98
99 99 public BuildRole role(String name) {
100 100 return role(name, r -> {
101 101 });
102 102 }
103 103
104 104 public BuildRole getRoleByName(String name) {
105 105 return roles.get(name);
106 106 }
107 107
108 108 public Collection<LayerLink> getLinks() {
109 109 return Collections.unmodifiableList(links);
110 110 }
111 111
112 112 public void links(Action<? super LinksSpec> action) {
113 113 ensureMutable("configure links");
114 114 action.execute(new LinksSpec());
115 115 }
116 116
117 117 public void links(Closure<?> configure) {
118 118 links(Closures.action(configure));
119 119 }
120 120
121 121 public LayerLink link(String from, String to, String kind) {
122 122 ensureMutable("add links");
123 var link = new LayerLink(from, to, kind);
123 var link = new LayerLink(
124 requireLinkValue("from", from),
125 requireLinkValue("to", to),
126 requireLinkValue("kind", kind));
124 127 links.add(link);
125 128 return link;
126 129 }
127 130
128 131 public Collection<BuildArtifactSlot> getArtifactSlots() {
129 132 return Collections.unmodifiableCollection(artifactSlots.values());
130 133 }
131 134
132 135 public void artifactSlots(Action<? super ArtifactSlotsSpec> action) {
133 136 ensureMutable("configure artifact slots");
134 137 action.execute(new ArtifactSlotsSpec());
135 138 }
136 139
137 140 public void artifactSlots(Closure<?> configure) {
138 141 artifactSlots(Closures.action(configure));
139 142 }
140 143
141 144 public BuildArtifactSlot artifactSlot(String name) {
142 145 return artifactSlot(name, it -> {
143 146 });
144 147 }
145 148
146 149 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
147 150 ensureMutable("configure artifact slots");
148 151 var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot);
149 152 configure.execute(slot);
150 153 return slot;
151 154 }
152 155
153 156 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
154 157 return artifactSlot(name, Closures.action(configure));
155 158 }
156 159
157 160 public BuildArtifactSlot getArtifactSlotByName(String name) {
158 161 return artifactSlots.get(name);
159 162 }
160 163
161 164 Set<String> declaredLayerNames() {
162 165 var result = new LinkedHashSet<String>();
163 166
164 167 for (var role : roles.values())
165 168 result.addAll(role.getLayers().getOrElse(java.util.List.of()));
166 169
167 170 return result;
168 171 }
169 172
170 173 void finalizeModel() {
171 174 if (finalized)
172 175 return;
173 176
174 177 for (var role : roles.values())
175 178 role.finalizeModel();
176 179
177 180 attributes.finalizeModel();
178 181 finalized = true;
179 182 }
180 183
181 184 private BuildRole newRole(String roleName) {
182 185 return objects.newInstance(BuildRole.class, roleName);
183 186 }
184 187
185 188 private BuildArtifactSlot newArtifactSlot(String slotName) {
186 189 return objects.newInstance(BuildArtifactSlot.class, slotName);
187 190 }
188 191
189 192 private void ensureMutable(String operation) {
190 193 if (finalized)
191 194 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
192 195 }
193 196
197 private static String requireLinkValue(String field, String value) {
198 if (value == null || value.trim().isEmpty())
199 throw new InvalidUserDataException("Link '" + field + "' must not be null or blank");
200
201 return value.trim();
202 }
203
194 204 public final class RolesSpec {
195 205 public BuildRole role(String name, Action<? super BuildRole> configure) {
196 206 return BuildVariant.this.role(name, configure);
197 207 }
198 208
199 209 public BuildRole role(String name, Closure<?> configure) {
200 210 return BuildVariant.this.role(name, configure);
201 211 }
202 212
203 213 public BuildRole role(String name) {
204 214 return BuildVariant.this.role(name);
205 215 }
206 216
207 217 public Collection<BuildRole> getAll() {
208 218 return BuildVariant.this.getRoles();
209 219 }
210 220
211 221 public BuildRole getByName(String name) {
212 222 return BuildVariant.this.getRoleByName(name);
213 223 }
214 224 }
215 225
216 226 public final class LinksSpec {
217 227 public LayerLink link(String from, String to, String kind) {
218 228 return BuildVariant.this.link(from, to, kind);
219 229 }
220 230
221 231 public Collection<LayerLink> getAll() {
222 232 return BuildVariant.this.getLinks();
223 233 }
224 234 }
225 235
226 236 public final class ArtifactSlotsSpec {
227 237 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
228 238 return BuildVariant.this.artifactSlot(name, configure);
229 239 }
230 240
231 241 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
232 242 return BuildVariant.this.artifactSlot(name, configure);
233 243 }
234 244
235 245 public BuildArtifactSlot artifactSlot(String name) {
236 246 return BuildVariant.this.artifactSlot(name);
237 247 }
238 248
239 249 public Collection<BuildArtifactSlot> getAll() {
240 250 return BuildVariant.this.getArtifactSlots();
241 251 }
242 252
243 253 public BuildArtifactSlot getByName(String name) {
244 254 return BuildVariant.this.getArtifactSlotByName(name);
245 255 }
246 256 }
247 257
248 258 public static final class AttributesSpec {
249 259 private final VariantAttributes attributes;
250 260
251 261 AttributesSpec(VariantAttributes attributes) {
252 262 this.attributes = attributes;
253 263 }
254 264
255 265 public <T> void attribute(Attribute<T> key, T value) {
256 266 attributes.attribute(key, value);
257 267 }
258 268
259 269 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
260 270 attributes.attributeProvider(key, value);
261 271 }
262 272
263 273 public void string(String name, String value) {
264 274 attribute(Attribute.of(name, String.class), value);
265 275 }
266 276
267 277 public void string(String name, Provider<? extends String> value) {
268 278 attributeProvider(Attribute.of(name, String.class), value);
269 279 }
270 280
271 281 public void bool(String name, boolean value) {
272 282 attribute(Attribute.of(name, Boolean.class), value);
273 283 }
274 284
275 285 public void bool(String name, Provider<? extends Boolean> value) {
276 286 attributeProvider(Attribute.of(name, Boolean.class), value);
277 287 }
278 288
279 289 public void integer(String name, int value) {
280 290 attribute(Attribute.of(name, Integer.class), value);
281 291 }
282 292
283 293 public void integer(String name, Provider<? extends Integer> value) {
284 294 attributeProvider(Attribute.of(name, Integer.class), value);
285 295 }
286 296
287 297 public VariantAttributes asAttributes() {
288 298 return attributes;
289 299 }
290 300 }
291 301 }
@@ -1,295 +1,346
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.Collection;
5 5 import java.util.Collections;
6 6 import java.util.HashMap;
7 7 import java.util.HashSet;
8 8 import java.util.LinkedHashMap;
9 9 import java.util.LinkedHashSet;
10 10 import java.util.List;
11 11 import java.util.Map;
12 12 import java.util.Set;
13 13
14 14 import javax.inject.Inject;
15 15
16 16 import org.implab.gradle.common.core.lang.Closures;
17 17 import org.gradle.api.Action;
18 18 import org.gradle.api.InvalidUserDataException;
19 19 import org.gradle.api.NamedDomainObjectContainer;
20 20 import org.gradle.api.model.ObjectFactory;
21 21
22 22 import groovy.lang.Closure;
23 23
24 24 public abstract class BuildVariantsExtension {
25 25 private final NamedDomainObjectContainer<BuildLayer> layers;
26 26 private final NamedDomainObjectContainer<BuildVariant> variants;
27 27 private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
28 28 private boolean finalized;
29 29
30 30 @Inject
31 31 public BuildVariantsExtension(ObjectFactory objects) {
32 32 layers = objects.domainObjectContainer(BuildLayer.class);
33 33 variants = objects.domainObjectContainer(BuildVariant.class);
34 34
35 35 layers.all(layer -> {
36 36 if (finalized)
37 37 throw new InvalidUserDataException(
38 38 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
39 39 });
40 40
41 41 variants.all(variant -> {
42 42 if (finalized)
43 43 throw new InvalidUserDataException(
44 44 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
45 45 });
46 46 }
47 47
48 48 public NamedDomainObjectContainer<BuildLayer> getLayers() {
49 49 return layers;
50 50 }
51 51
52 52 public NamedDomainObjectContainer<BuildVariant> getVariants() {
53 53 return variants;
54 54 }
55 55
56 56 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
57 57 ensureMutable("configure layers");
58 58 action.execute(layers);
59 59 }
60 60
61 61 public void layers(Closure<?> configure) {
62 62 layers(Closures.action(configure));
63 63 }
64 64
65 65 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
66 66 ensureMutable("configure variants");
67 67 action.execute(variants);
68 68 }
69 69
70 70 public void variants(Closure<?> configure) {
71 71 variants(Closures.action(configure));
72 72 }
73 73
74 74 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
75 75 ensureMutable("configure layers");
76 76 var layer = layers.maybeCreate(name);
77 77 configure.execute(layer);
78 78 return layer;
79 79 }
80 80
81 81 public BuildLayer layer(String name, Closure<?> configure) {
82 82 return layer(name, Closures.action(configure));
83 83 }
84 84
85 85 public BuildLayer layer(String name) {
86 86 return layer(name, it -> {
87 87 });
88 88 }
89 89
90 90 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
91 91 ensureMutable("configure variants");
92 92 var variant = variants.maybeCreate(name);
93 93 configure.execute(variant);
94 94 return variant;
95 95 }
96 96
97 97 public BuildVariant variant(String name, Closure<?> configure) {
98 98 return variant(name, Closures.action(configure));
99 99 }
100 100
101 101 public BuildVariant variant(String name) {
102 102 return variant(name, it -> {
103 103 });
104 104 }
105 105
106 106 public void all(Action<? super BuildVariant> action) {
107 107 variants.all(action);
108 108 }
109 109
110 110 public void all(Closure<?> configure) {
111 111 all(Closures.action(configure));
112 112 }
113 113
114 114 public Collection<BuildVariant> getAll() {
115 115 var all = new ArrayList<BuildVariant>();
116 116 variants.forEach(all::add);
117 117 return Collections.unmodifiableList(all);
118 118 }
119 119
120 120 public BuildVariant getByName(String name) {
121 121 return variants.findByName(name);
122 122 }
123 123
124 124 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
125 125 if (finalized) {
126 126 action.execute(this);
127 127 return;
128 128 }
129 129 finalizedActions.add(action);
130 130 }
131 131
132 132 public void whenFinalized(Closure<?> configure) {
133 133 whenFinalized(Closures.action(configure));
134 134 }
135 135
136 136 public boolean isFinalized() {
137 137 return finalized;
138 138 }
139 139
140 140 public void finalizeModel() {
141 141 if (finalized)
142 142 return;
143 143
144 144 validate();
145 145
146 146 for (var variant : variants)
147 147 variant.finalizeModel();
148 148
149 149 finalized = true;
150 150
151 151 var actions = new ArrayList<>(finalizedActions);
152 152 finalizedActions.clear();
153 153 for (var action : actions)
154 154 action.execute(this);
155 155 }
156 156
157 157 public void validate() {
158 158 var errors = new ArrayList<String>();
159 159
160 160 var layersByName = new LinkedHashMap<String, BuildLayer>();
161 for (var layer : layers)
162 layersByName.put(layer.getName(), layer);
161 for (var layer : layers) {
162 var layerName = normalize(layer.getName());
163 if (layerName == null) {
164 errors.add("Layer name must not be blank");
165 continue;
166 }
167
168 var previous = layersByName.putIfAbsent(layerName, layer);
169 if (previous != null) {
170 errors.add("Layer '" + layerName + "' is declared more than once");
171 }
172 }
163 173
164 174 for (var variant : variants)
165 175 validateVariant(variant, layersByName, errors);
166 176
167 177 if (!errors.isEmpty()) {
168 178 var message = new StringBuilder("Invalid variants model:");
169 179 for (var error : errors)
170 180 message.append("\n - ").append(error);
171 181
172 182 throw new InvalidUserDataException(message.toString());
173 183 }
174 184 }
175 185
176 186 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
187 var variantName = normalize(variant.getName());
188 if (variantName == null) {
189 errors.add("Variant name must not be blank");
190 return;
191 }
192
193 validateRoleAndArtifactNames(variant, errors);
177 194 var variantLayers = validateRoleMappings(variant, layersByName, errors);
178 195 validateLinks(variant, variantLayers, errors);
179 196 }
180 197
198 private static void validateRoleAndArtifactNames(BuildVariant variant, List<String> errors) {
199 var roleNames = new LinkedHashSet<String>();
200 for (var role : variant.getRoles()) {
201 var roleName = normalize(role.getName());
202 if (roleName == null) {
203 errors.add("Variant '" + variant.getName() + "' contains blank role name");
204 continue;
205 }
206 if (!roleNames.add(roleName)) {
207 errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
208 }
209 }
210
211 var slotNames = new LinkedHashSet<String>();
212 for (var slot : variant.getArtifactSlots()) {
213 var slotName = normalize(slot.getName());
214 if (slotName == null) {
215 errors.add("Variant '" + variant.getName() + "' contains blank artifact slot name");
216 continue;
217 }
218 if (!slotNames.add(slotName)) {
219 errors.add("Variant '" + variant.getName() + "' contains duplicated artifact slot name '" + slotName + "'");
220 }
221 }
222 }
223
181 224 private static Set<String> validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
182 225 List<String> errors) {
183 226 var variantLayers = new LinkedHashSet<String>();
184 227
185 228 for (var role : variant.getRoles()) {
229 var seenLayers = new LinkedHashSet<String>();
186 230 for (var layerName : role.getLayers().getOrElse(List.of())) {
187 if (isBlank(layerName)) {
231 var normalizedLayerName = normalize(layerName);
232 if (normalizedLayerName == null) {
188 233 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
189 234 continue;
190 235 }
191 236
192 var layer = layersByName.get(layerName);
237 var layer = layersByName.get(normalizedLayerName);
193 238 if (layer == null) {
194 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + layerName + "'");
239 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
195 240 continue;
196 241 }
197 242
198 variantLayers.add(layerName);
243 if (!seenLayers.add(normalizedLayerName)) {
244 errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
245 + "' contains duplicated layer reference '" + normalizedLayerName + "'");
246 continue;
247 }
248
249 variantLayers.add(normalizedLayerName);
199 250 }
200 251 }
201 252
202 253 return variantLayers;
203 254 }
204 255
205 256 private static void validateLinks(BuildVariant variant, Set<String> variantLayers, List<String> errors) {
206 257 var seenLinks = new HashSet<String>();
207 258 var edgesByKind = new HashMap<String, Map<String, Set<String>>>();
208 259
209 260 for (var link : variant.getLinks()) {
210 261 var from = normalize(link.from());
211 262 var to = normalize(link.to());
212 263 var kind = normalize(link.kind());
213 264
214 265 if (from == null || to == null || kind == null) {
215 266 errors.add("Variant '" + variant.getName() + "' has incomplete link (from/to/kind are required)");
216 267 continue;
217 268 }
218 269
219 270 if (!variantLayers.contains(from)) {
220 271 errors.add("Variant '" + variant.getName() + "' link references unknown source layer '"
221 272 + from + "'");
222 273 continue;
223 274 }
224 275
225 276 if (!variantLayers.contains(to)) {
226 277 errors.add("Variant '" + variant.getName() + "' link references unknown target layer '"
227 278 + to + "'");
228 279 continue;
229 280 }
230 281
231 282 var linkKey = from + "\u0000" + to + "\u0000" + kind;
232 283 if (!seenLinks.add(linkKey)) {
233 284 errors.add("Variant '" + variant.getName() + "' has duplicated link tuple (from='" + from
234 285 + "', to='" + to + "', kind='" + kind + "')");
235 286 }
236 287
237 288 edgesByKind
238 289 .computeIfAbsent(kind, x -> new LinkedHashMap<>())
239 290 .computeIfAbsent(from, x -> new LinkedHashSet<>())
240 291 .add(to);
241 292 }
242 293
243 294 for (var entry : edgesByKind.entrySet()) {
244 295 if (hasCycle(variantLayers, entry.getValue())) {
245 296 errors.add("Variant '" + variant.getName() + "' contains cycle in links with kind '" + entry.getKey() + "'");
246 297 }
247 298 }
248 299 }
249 300
250 301 private static boolean hasCycle(Set<String> nodes, Map<String, Set<String>> edges) {
251 302 var state = new HashMap<String, Integer>();
252 303
253 304 for (var node : nodes) {
254 305 if (dfs(node, state, edges))
255 306 return true;
256 307 }
257 308
258 309 return false;
259 310 }
260 311
261 312 private static boolean dfs(String node, Map<String, Integer> state, Map<String, Set<String>> edges) {
262 313 var current = state.getOrDefault(node, 0);
263 314 if (current == 1)
264 315 return true;
265 316 if (current == 2)
266 317 return false;
267 318
268 319 state.put(node, 1);
269 320
270 321 for (var next : edges.getOrDefault(node, Set.of())) {
271 322 if (dfs(next, state, edges))
272 323 return true;
273 324 }
274 325
275 326 state.put(node, 2);
276 327 return false;
277 328 }
278 329
279 330 private static String normalize(String value) {
280 331 if (value == null)
281 332 return null;
282 333
283 334 var trimmed = value.trim();
284 335 return trimmed.isEmpty() ? null : trimmed;
285 336 }
286 337
287 338 private static boolean isBlank(String value) {
288 339 return normalize(value) == null;
289 340 }
290 341
291 342 private void ensureMutable(String operation) {
292 343 if (finalized)
293 344 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
294 345 }
295 346 }
@@ -1,304 +1,311
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.LinkedHashMap;
5 5 import java.util.List;
6 6 import java.util.regex.Matcher;
7 7 import java.util.regex.Pattern;
8 8 import java.util.stream.Stream;
9 9
10 10 import javax.inject.Inject;
11 11
12 12 import org.implab.gradle.common.core.lang.Closures;
13 13 import org.implab.gradle.common.core.lang.Strings;
14 14 import org.eclipse.jdt.annotation.NonNullByDefault;
15 15 import org.gradle.api.Action;
16 16 import org.gradle.api.InvalidUserDataException;
17 17 import org.gradle.api.NamedDomainObjectContainer;
18 18 import org.gradle.api.NamedDomainObjectProvider;
19 19 import org.gradle.api.model.ObjectFactory;
20 20 import org.gradle.api.logging.Logger;
21 21 import org.gradle.api.logging.Logging;
22 22
23 23 import groovy.lang.Closure;
24 24 import groovy.lang.DelegatesTo;
25 25
26 26 /**
27 27 * Adapter extension that registers source sets for variant/layer pairs.
28 28 */
29 29 @NonNullByDefault
30 30 public abstract class VariantSourcesExtension {
31 31 private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class);
32 32 private static final Pattern INVALID_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_.-]");
33 33 private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}");
34 34
35 35 private final ObjectFactory objects;
36 36 private final NamedDomainObjectContainer<BuildLayerBinding> bindings;
37 37 private final List<Action<? super SourceSetContext>> registeredActions = new ArrayList<>();
38 38 private final List<Action<? super SourceSetContext>> boundActions = new ArrayList<>();
39 39 private final List<SourceSetContext> registeredContexts = new ArrayList<>();
40 40 private final List<SourceSetContext> boundContexts = new ArrayList<>();
41 41 private final LinkedHashMap<String, NamedDomainObjectProvider<GenericSourceSet>> sourceSetsByName = new LinkedHashMap<>();
42 42 private final LinkedHashMap<String, String> sourceSetLayersByName = new LinkedHashMap<>();
43 private boolean sourceSetsRegistered;
43 44
44 45 @Inject
45 46 public VariantSourcesExtension(ObjectFactory objects) {
46 47 this.objects = objects;
47 48 bindings = objects.domainObjectContainer(BuildLayerBinding.class);
48 49 }
49 50
50 51 public NamedDomainObjectContainer<BuildLayerBinding> getBindings() {
51 52 return bindings;
52 53 }
53 54
54 55 public void bindings(Action<? super NamedDomainObjectContainer<BuildLayerBinding>> action) {
55 56 action.execute(bindings);
56 57 }
57 58
58 59 public void bindings(
59 60 @DelegatesTo(value = NamedDomainObjectContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
60 61 bindings(Closures.action(action));
61 62 }
62 63
63 64 public BuildLayerBinding bind(String layer) {
64 65 return bindings.maybeCreate(normalize(layer));
65 66 }
66 67
67 68 /**
68 69 * Configures per-layer binding.
69 70 */
70 71 public BuildLayerBinding bind(String layer, Action<? super BuildLayerBinding> configure) {
71 72 var binding = bind(layer);
72 73 configure.execute(binding);
73 74 return binding;
74 75 }
75 76
76 77 public BuildLayerBinding bind(String layer,
77 78 @DelegatesTo(value = BuildLayerBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
78 79 return bind(layer, Closures.action(configure));
79 80 }
80 81
81 82 /**
82 83 * Global callback fired for each registered source-set context.
83 84 * Already emitted contexts are delivered immediately (replay).
84 85 * For simple callbacks you can use delegate-only style
85 86 * (for example {@code whenRegistered { sourceSetName() }}).
86 87 * For nested closures prefer explicit parameter
87 88 * ({@code whenRegistered { ctx -> ... }}).
88 89 */
89 90 public void whenRegistered(Action<? super SourceSetContext> action) {
90 91 registeredActions.add(action);
91 92 for (var context : registeredContexts)
92 93 action.execute(context);
93 94 }
94 95
95 96 public void whenRegistered(
96 97 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
97 98 whenRegistered(Closures.action(action));
98 99 }
99 100
100 101 public void whenRegistered(String variantName, Action<? super SourceSetContext> action) {
101 102 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
102 103 whenRegistered(filterByVariant(normalizedVariantName, action));
103 104 }
104 105
105 106 public void whenRegistered(String variantName,
106 107 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
107 108 whenRegistered(variantName, Closures.action(action));
108 109 }
109 110
110 111 /**
111 112 * Global callback fired for every resolved variant/role/layer usage.
112 113 * Already emitted contexts are delivered immediately (replay).
113 114 * For simple callbacks you can use delegate-only style
114 115 * (for example {@code whenBound { variantName() }}).
115 116 * For nested closures prefer explicit parameter
116 117 * ({@code whenBound { ctx -> ... }}).
117 118 */
118 119 public void whenBound(Action<? super SourceSetContext> action) {
119 120 boundActions.add(action);
120 121 for (var context : boundContexts)
121 122 action.execute(context);
122 123 }
123 124
124 125 public void whenBound(
125 126 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
126 127 whenBound(Closures.action(action));
127 128 }
128 129
129 130 public void whenBound(String variantName, Action<? super SourceSetContext> action) {
130 131 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
131 132 whenBound(filterByVariant(normalizedVariantName, action));
132 133 }
133 134
134 135 public void whenBound(String variantName,
135 136 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
136 137 whenBound(variantName, Closures.action(action));
137 138 }
138 139
139 140 void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer<GenericSourceSet> sources) {
141 if (sourceSetsRegistered) {
142 throw new InvalidUserDataException("variantSources source sets are already registered");
143 }
144
140 145 validateBindings(variants);
141 146
142 147 var usages = layerUsages(variants).toList();
143 148 var registeredBefore = registeredContexts.size();
144 149 var boundBefore = boundContexts.size();
145 150
146 151 logger.debug(
147 152 "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})",
148 153 variants.getVariants().size(),
149 154 variants.getLayers().size(),
150 155 bindings.size(),
151 156 usages.size());
152 157
153 158 usages.forEach(usage -> registerLayerUsage(usage, sources));
154 159
155 160 logger.debug(
156 161 "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})",
157 162 registeredContexts.size() - registeredBefore,
158 163 boundContexts.size() - boundBefore,
159 164 sourceSetsByName.size());
165
166 sourceSetsRegistered = true;
160 167 }
161 168
162 169 private Stream<LayerUsage> layerUsages(BuildVariantsExtension variants) {
163 170 return variants.getVariants().stream()
164 171 .flatMap(variant -> variant.getRoles().stream()
165 172 .flatMap(role -> role.getLayers().getOrElse(List.of()).stream()
166 173 .map(layerName -> new LayerUsage(
167 174 variant.getName(),
168 175 role.getName(),
169 176 normalize(layerName)))));
170 177 }
171 178
172 179 private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
173 180 var resolvedBinding = bind(usage.layerName());
174 181 var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern();
175 182 sourceSetNamePattern.finalizeValueOnRead();
176 183
177 184 var sourceSetName = sourceSetName(usage, sourceSetNamePattern.get());
178 185
179 186 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layerName());
180 187 var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName);
181 188 var sourceSet = sourceSetsByName.computeIfAbsent(sourceSetName,
182 189 name -> sources.register(name));
183 190
184 191 var context = new SourceSetContext(
185 192 usage.variantName(),
186 193 usage.roleName(),
187 194 usage.layerName(),
188 195 sourceSetName,
189 196 sourceSet);
190 197
191 198 if (isNewSourceSet) {
192 199 resolvedBinding.notifyRegistered(context);
193 200 notifyRegistered(context);
194 201 }
195 202
196 203 resolvedBinding.notifyBound(context);
197 204 notifyBound(context);
198 205 }
199 206
200 207 private void notifyRegistered(SourceSetContext context) {
201 208 registeredContexts.add(context);
202 209 for (var action : registeredActions)
203 210 action.execute(context);
204 211 }
205 212
206 213 private void notifyBound(SourceSetContext context) {
207 214 boundContexts.add(context);
208 215 for (var action : boundActions)
209 216 action.execute(context);
210 217 }
211 218
212 219 private static Action<? super SourceSetContext> filterByVariant(String variantName,
213 220 Action<? super SourceSetContext> action) {
214 221 return context -> {
215 222 if (variantName.equals(context.variantName()))
216 223 action.execute(context);
217 224 };
218 225 }
219 226
220 227 private void ensureSourceSetNameBoundToSingleLayer(String sourceSetName, String layerName) {
221 228 var existingLayer = sourceSetLayersByName.putIfAbsent(sourceSetName, layerName);
222 229 if (existingLayer != null && !existingLayer.equals(layerName)) {
223 230 throw new InvalidUserDataException("Source set '" + sourceSetName + "' is resolved from multiple layers: '"
224 231 + existingLayer + "' and '" + layerName + "'");
225 232 }
226 233 }
227 234
228 235 private void validateBindings(BuildVariantsExtension variants) {
229 236 var knownLayerNames = new java.util.LinkedHashSet<String>();
230 237 for (var layer : variants.getLayers())
231 238 knownLayerNames.add(layer.getName());
232 239
233 240 var errors = new ArrayList<String>();
234 241 for (var binding : bindings) {
235 242 if (!knownLayerNames.contains(binding.getName())) {
236 243 errors.add("Layer binding '" + binding.getName() + "' references unknown layer");
237 244 }
238 245 }
239 246
240 247 if (!errors.isEmpty()) {
241 248 var message = new StringBuilder("Invalid variantSources model:");
242 249 for (var error : errors)
243 250 message.append("\n - ").append(error);
244 251 throw new InvalidUserDataException(message.toString());
245 252 }
246 253 }
247 254
248 255 private static String sourceSetName(LayerUsage usage, String pattern) {
249 256 var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank");
250 257 var resolved = resolveSourceSetNamePattern(normalizedPattern, usage);
251 258 var result = sanitize(resolved);
252 259
253 260 if (result.isEmpty())
254 261 throw new InvalidUserDataException("sourceSetNamePattern '" + pattern + "' resolved to empty source set name");
255 262
256 263 return result;
257 264 }
258 265
259 266 private static String resolveSourceSetNamePattern(String pattern, LayerUsage usage) {
260 267 var matcher = SOURCE_SET_NAME_TOKEN.matcher(pattern);
261 268 var output = new StringBuffer();
262 269
263 270 while (matcher.find()) {
264 271 var token = matcher.group(1);
265 272 matcher.appendReplacement(output, Matcher.quoteReplacement(tokenValue(token, usage)));
266 273 }
267 274 matcher.appendTail(output);
268 275
269 276 return output.toString();
270 277 }
271 278
272 279 private static String tokenValue(String token, LayerUsage usage) {
273 280 return switch (token) {
274 281 case "variant" -> sanitize(usage.variantName());
275 282 case "variantCap" -> Strings.capitalize(sanitize(usage.variantName()));
276 283 case "role" -> sanitize(usage.roleName());
277 284 case "roleCap" -> Strings.capitalize(sanitize(usage.roleName()));
278 285 case "layer" -> sanitize(usage.layerName());
279 286 case "layerCap" -> Strings.capitalize(sanitize(usage.layerName()));
280 287 default -> throw new InvalidUserDataException(
281 288 "sourceSetNamePattern contains unsupported token '{" + token + "}'");
282 289 };
283 290 }
284 291
285 292 private static String sanitize(String value) {
286 293 return INVALID_NAME_CHAR.matcher(value).replaceAll("_");
287 294 }
288 295
289 296 private static String normalize(String value) {
290 297 return normalize(value, "Value must not be null or blank");
291 298 }
292 299
293 300 private static String normalize(String value, String errorMessage) {
294 301 if (value == null)
295 302 throw new InvalidUserDataException(errorMessage);
296 303 var trimmed = value.trim();
297 304 if (trimmed.isEmpty())
298 305 throw new InvalidUserDataException(errorMessage);
299 306 return trimmed;
300 307 }
301 308
302 309 private record LayerUsage(String variantName, String roleName, String layerName) {
303 310 }
304 311 }
@@ -1,311 +1,330
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 12
13 13 import org.gradle.testkit.runner.BuildResult;
14 14 import org.gradle.testkit.runner.GradleRunner;
15 15 import org.gradle.testkit.runner.TaskOutcome;
16 16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 17 import org.junit.jupiter.api.Test;
18 18 import org.junit.jupiter.api.io.TempDir;
19 19
20 20 class VariantsPluginFunctionalTest {
21 21 private static final String SETTINGS_FILE = "settings.gradle";
22 22 private static final String BUILD_FILE = "build.gradle";
23 23 private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n";
24 24
25 25 @TempDir
26 26 Path testProjectDir;
27 27
28 28 @Test
29 29 void configuresVariantModelWithDsl() throws Exception {
30 30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 31 writeFile(BUILD_FILE, """
32 32 plugins {
33 33 id 'org.implab.gradle-variants'
34 34 }
35 35
36 36 variants {
37 37 layer('mainBase') {
38 38 }
39 39
40 40 layer('mainAmd') {
41 41 }
42 42
43 43 variant('browser') {
44 44 attributes {
45 45 string('jsRuntime', 'browser')
46 46 string('jsModule', 'amd')
47 47 }
48 48 role('main') {
49 49 layers('mainBase', 'mainAmd')
50 50 }
51 51 link('mainBase', 'mainAmd', 'ts:api')
52 52 artifactSlot('mainCompiled')
53 53 }
54 54 }
55 55
56 56 tasks.register('probe') {
57 57 doLast {
58 58 def browser = variants.getByName('browser')
59 59 println('attributes=' + browser.attributes.size())
60 60 println('roles=' + browser.roles.size())
61 61 println('links=' + browser.links.size())
62 62 println('slots=' + browser.artifactSlots.size())
63 63 }
64 64 }
65 65 """);
66 66
67 67 BuildResult result = runner("probe").build();
68 68
69 69 assertTrue(result.getOutput().contains("attributes=2"));
70 70 assertTrue(result.getOutput().contains("roles=1"));
71 71 assertTrue(result.getOutput().contains("links=1"));
72 72 assertTrue(result.getOutput().contains("slots=1"));
73 73 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
74 74 }
75 75
76 76 @Test
77 77 void failsOnUnknownLayerReference() throws Exception {
78 78 assertBuildFails("""
79 79 plugins {
80 80 id 'org.implab.gradle-variants'
81 81 }
82 82
83 83 variants {
84 84 layer('mainBase') {
85 85 }
86 86
87 87 variant('browser') {
88 88 role('main') {
89 89 layers('mainBase', 'missingLayer')
90 90 }
91 91 }
92 92 }
93 93 """, "references unknown layer 'missingLayer'");
94 94 }
95 95
96 96 @Test
97 97 void failsOnCycleInLinksByKind() throws Exception {
98 98 assertBuildFails("""
99 99 plugins {
100 100 id 'org.implab.gradle-variants'
101 101 }
102 102
103 103 variants {
104 104 layer('a')
105 105 layer('b')
106 106
107 107 variant('browser') {
108 108 role('main') {
109 109 layers('a', 'b')
110 110 }
111 111 link('a', 'b', 'ts:api')
112 112 link('b', 'a', 'ts:api')
113 113 }
114 114 }
115 115 """, "contains cycle in links with kind 'ts:api'");
116 116 }
117 117
118 118 @Test
119 119 void allowsUsingLayerFromDifferentVariantRole() throws Exception {
120 120 writeFile(SETTINGS_FILE, ROOT_NAME);
121 121 writeFile(BUILD_FILE, """
122 122 plugins {
123 123 id 'org.implab.gradle-variants'
124 124 }
125 125
126 126 variants {
127 127 layer('mainBase')
128 128
129 129 variant('browser') {
130 130 role('test') {
131 131 layers('mainBase')
132 132 }
133 133 }
134 134 }
135 135 """);
136 136
137 137 BuildResult result = runner("help").build();
138 138 assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
139 139 }
140 140
141 141 @Test
142 142 void failsOnIncompleteLink() throws Exception {
143 143 assertBuildFails("""
144 144 plugins {
145 145 id 'org.implab.gradle-variants'
146 146 }
147 147
148 148 variants {
149 149 layer('a')
150 150 layer('b')
151 151
152 152 variant('browser') {
153 153 role('main') {
154 154 layers('a', 'b')
155 155 }
156 156 link('a', 'b', null)
157 157 }
158 158 }
159 """, "has incomplete link (from/to/kind are required)");
159 """, "Link 'kind' must not be null or blank");
160 160 }
161 161
162 162 @Test
163 163 void failsOnDuplicatedLinkTuple() throws Exception {
164 164 assertBuildFails("""
165 165 plugins {
166 166 id 'org.implab.gradle-variants'
167 167 }
168 168
169 169 variants {
170 170 layer('a')
171 171 layer('b')
172 172
173 173 variant('browser') {
174 174 role('main') {
175 175 layers('a', 'b')
176 176 }
177 177 link('a', 'b', 'ts:api')
178 178 link('a', 'b', 'ts:api')
179 179 }
180 180 }
181 181 """, "has duplicated link tuple (from='a', to='b', kind='ts:api')");
182 182 }
183 183
184 184 @Test
185 185 void failsOnUnknownSourceLayerInLink() throws Exception {
186 186 assertBuildFails("""
187 187 plugins {
188 188 id 'org.implab.gradle-variants'
189 189 }
190 190
191 191 variants {
192 192 layer('a') {
193 193 }
194 194
195 195 variant('browser') {
196 196 role('main') {
197 197 layers('a')
198 198 }
199 199 link('missing', 'a', 'ts:api')
200 200 }
201 201 }
202 202 """, "references unknown source layer 'missing'");
203 203 }
204 204
205 205 @Test
206 206 void failsOnUnknownTargetLayerInLink() throws Exception {
207 207 assertBuildFails("""
208 208 plugins {
209 209 id 'org.implab.gradle-variants'
210 210 }
211 211
212 212 variants {
213 213 layer('a') {
214 214 }
215 215
216 216 variant('browser') {
217 217 role('main') {
218 218 layers('a')
219 219 }
220 220 link('a', 'missing', 'ts:api')
221 221 }
222 222 }
223 223 """, "references unknown target layer 'missing'");
224 224 }
225 225
226 226 @Test
227 void failsOnDuplicatedLayerReferenceInRole() throws Exception {
228 assertBuildFails("""
229 plugins {
230 id 'org.implab.gradle-variants'
231 }
232
233 variants {
234 layer('a')
235
236 variant('browser') {
237 role('main') {
238 layers('a', 'a')
239 }
240 }
241 }
242 """, "contains duplicated layer reference 'a'");
243 }
244
245 @Test
227 246 void failsOnLateLayerMutationAfterFinalize() throws Exception {
228 247 assertBuildFails("""
229 248 plugins {
230 249 id 'org.implab.gradle-variants'
231 250 }
232 251
233 252 variants {
234 253 layer('a')
235 254 variant('browser') {
236 255 role('main') { layers('a') }
237 256 }
238 257 }
239 258
240 259 afterEvaluate {
241 260 variants.layer('late')
242 261 }
243 262 """, "Variants model is finalized and cannot configure layers");
244 263 }
245 264
246 265 @Test
247 266 void failsOnLateVariantMutationAfterFinalize() throws Exception {
248 267 assertBuildFails("""
249 268 plugins {
250 269 id 'org.implab.gradle-variants'
251 270 }
252 271
253 272 variants {
254 273 layer('a')
255 274 variant('browser') {
256 275 role('main') { layers('a') }
257 276 }
258 277 }
259 278
260 279 afterEvaluate {
261 280 variants.getByName('browser').role('late') { layers('a') }
262 281 }
263 282 """, "Variant 'browser' is finalized and cannot configure roles");
264 283 }
265 284
266 285 private GradleRunner runner(String... arguments) {
267 286 return GradleRunner.create()
268 287 .withProjectDir(testProjectDir.toFile())
269 288 .withPluginClasspath(pluginClasspath())
270 289 .withArguments(arguments)
271 290 .forwardOutput();
272 291 }
273 292
274 293 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
275 294 writeFile(SETTINGS_FILE, ROOT_NAME);
276 295 writeFile(BUILD_FILE, buildScript);
277 296
278 297 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
279 298 var output = ex.getBuildResult().getOutput();
280 299
281 300 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
282 301 }
283 302
284 303 private static List<File> pluginClasspath() {
285 304 try {
286 305 var classesDir = Path.of(BuildVariant.class
287 306 .getProtectionDomain()
288 307 .getCodeSource()
289 308 .getLocation()
290 309 .toURI());
291 310
292 311 var markerResource = VariantsPlugin.class.getClassLoader()
293 312 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
294 313
295 314 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
296 315
297 316 var markerPath = Path.of(markerResource.toURI());
298 317 var resourcesDir = markerPath.getParent().getParent().getParent();
299 318
300 319 return List.of(classesDir.toFile(), resourcesDir.toFile());
301 320 } catch (Exception e) {
302 321 throw new RuntimeException("Unable to build plugin classpath for test", e);
303 322 }
304 323 }
305 324
306 325 private void writeFile(String relativePath, String content) throws IOException {
307 326 Path path = testProjectDir.resolve(relativePath);
308 327 Files.createDirectories(path.getParent());
309 328 Files.writeString(path, content);
310 329 }
311 330 }
General Comments 0
You need to be logged in to leave comments. Login now