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