##// END OF EJS Templates
Variants mutation guard after finalize
cin -
r27:33c11dfc2c2e default
parent child
Show More
@@ -0,0 +1,7
1 package org.implab.gradle.common.sources;
2
3 /**
4 * Directed relation between two layers within a variant.
5 */
6 public record LayerLink(String from, String to, String kind) {
7 }
@@ -1,135 +1,135
1 1 # Gradle Common Sources Model
2 2
3 3 ## NAME
4 4
5 5 `gradle-common/common` — набор плагинов для моделирования вариантов сборки,
6 материализации source sets и интеграции этой модели с toolchain-адаптерами.
6 регистрации source sets и интеграции этой модели с toolchain-адаптерами.
7 7
8 8 ## SYNOPSIS
9 9
10 10 ```groovy
11 11 plugins {
12 12 id 'org.implab.gradle-variants-sources'
13 13 }
14 14
15 15 variants {
16 16 layer('mainBase')
17 17 layer('mainAmd')
18 18
19 19 variant('browser') {
20 20 role('main') { layers('mainBase', 'mainAmd') }
21 21 link('mainBase', 'mainAmd', 'ts:api')
22 22 }
23 23 }
24 24
25 25 variantSources {
26 26 bind('mainBase') {
27 27 configureSourceSet {
28 28 declareOutputs('compiled')
29 29 }
30 30 }
31 31
32 32 bind('mainAmd').sourceSetNamePattern = '{variant}{layerCap}'
33 33
34 34 whenRegistered { sourceSetName() }
35 35
36 36 whenBound { ctx ->
37 37 ctx.configureSourceSet {
38 38 declareOutputs('typings')
39 39 }
40 40 }
41 41 }
42 42 ```
43 43
44 44 ## DESCRIPTION
45 45
46 46 Модуль состоит из трех логических частей:
47 47
48 48 - `variants` — декларативная доменная модель сборки;
49 - `sources` — модель физически материализуемых source sets;
49 - `sources` — модель физически регистрируемых source sets;
50 50 - `variantSources` — адаптер, который связывает первые две модели.
51 51
52 52 Ниже раскрытие каждой части.
53 53
54 54 ### variants
55 55
56 56 `variants` задает структуру пространства сборки: какие есть слои, какие роли
57 57 используют эти слои в каждом варианте, какие направленные связи между слоями
58 58 существуют. Модель не создает задачи и не привязана к TS/JS.
59 59
60 60 Практический смысл:
61 61
62 62 - формализовать архитектуру сборки;
63 63 - централизовать валидацию связей;
64 64 - дать адаптерам единый источник правды.
65 65
66 66 ### sources
67 67
68 68 `sources` описывает независимые source sets (`GenericSourceSet`) с именованными
69 69 outputs. Это уже "физический" уровень, к которому удобно привязывать задачи,
70 70 артефакты и task inputs/outputs.
71 71
72 72 Практический смысл:
73 73
74 74 - создать единый контракт по входам/выходам;
75 75 - регистрировать результаты задач как outputs source set;
76 76 - минимизировать ручные `dependsOn` за счет модели outputs.
77 77
78 78 ### variantSources
79 79
80 `variantSources` материализует source sets на основе `variants`, применяет
80 `variantSources` регистрирует source sets на основе `variants`, применяет
81 81 конфигурацию layer-bindings и отдает события (`whenRegistered`, `whenBound`) для
82 82 адаптеров других плагинов.
83 83
84 84 Практический смысл:
85 85
86 86 - переводить логическую модель `variants` в executable-модель `sources`;
87 - навешивать политики toolchain на materialized source sets;
87 - навешивать политики toolchain на зарегистрированные source sets;
88 88 - синхронизировать плагины через replayable callback-контракт.
89 89
90 90 ## DOMAIN MODEL
91 91
92 92 - `BuildLayer` — глобальный идентификатор слоя.
93 93 - `BuildVariant` — агрегат ролей, связей, атрибутов, артефактных слотов.
94 94 - `BuildRole` — роль внутри варианта, содержит ссылки на layer names.
95 - `BuildLink` — ориентированная связь `from -> to` в графе определенного `kind`.
96 - `GenericSourceSet`materialized набор исходников и outputs.
97 - `BuildLayerBinding` — правила materialization source set для конкретного layer.
98 - `SourceSetContext` — контекст callback-событий materialization.
95 - `LayerLink` — ориентированная связь `from -> to` в графе определенного `kind`.
96 - `GenericSourceSet`зарегистрированный набор исходников и outputs.
97 - `BuildLayerBinding` — правила registration source set для конкретного layer.
98 - `SourceSetContext` — контекст callback-событий registration.
99 99
100 100 ## EVENT CONTRACT
101 101
102 102 - `whenRegistered`:
103 103 - событие нового уникального source set name;
104 104 - replayable.
105 105 - `whenBound`:
106 106 - событие каждой usage-связки `variant/role/layer`;
107 107 - replayable.
108 108
109 109 Closure callbacks работают в delegate-first режиме (`@DelegatesTo`). Для
110 110 вложенных closure рекомендуется явный параметр (`ctx -> ...`).
111 111
112 112 ## KEY CLASSES
113 113
114 114 - `SourcesPlugin` — регистрирует extension `sources`.
115 115 - `GenericSourceSet` — модель источников/outputs для конкретного имени.
116 116 - `VariantsPlugin` — регистрирует extension `variants` и lifecycle finalize.
117 117 - `BuildVariantsExtension` — корневой API модели вариантов.
118 118 - `BuildVariant` — API ролей, links, attributes и artifact slots варианта.
119 119 - `VariantsSourcesPlugin` — применяет `variants` + `sources` и запускает адаптер.
120 - `VariantSourcesExtension` — API bind/events materialization.
120 - `VariantSourcesExtension` — API bind/events registration.
121 121 - `BuildLayerBinding` — слой-конкретный DSL для имени и конфигурации source set.
122 122 - `SourceSetContext` — payload событий и sugar `configureSourceSet(...)`.
123 123
124 124 ## NOTES
125 125
126 126 - Marker ids:
127 127 - `org.implab.gradle-variants`
128 128 - `org.implab.gradle-variants-sources`
129 129 - `SourcesPlugin` пока class-only (без marker id).
130 130
131 131 ## SEE ALSO
132 132
133 133 - `sources-plugin.md`
134 134 - `variants-plugin.md`
135 135 - `variant-sources-plugin.md`
@@ -1,41 +1,59
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.Objects;
5 5
6 6 import javax.inject.Inject;
7 7
8 import org.gradle.api.InvalidUserDataException;
8 9 import org.gradle.api.Named;
9 10 import org.gradle.api.provider.ListProperty;
10 11
11 12 /**
12 13 * Role binding inside a variant, points to layer names.
13 14 */
14 15 public abstract class BuildRole implements Named {
15 16 private final String name;
17 private boolean finalized;
16 18
17 19 @Inject
18 20 public BuildRole(String name) {
19 21 this.name = name;
20 22 }
21 23
22 24 @Override
23 25 public String getName() {
24 26 return name;
25 27 }
26 28
27 29 public abstract ListProperty<String> getLayers();
28 30
29 31 /**
30 32 * Binds this role to one or more declared layers.
31 33 */
32 34 public void layers(String layer, String... extra) {
35 ensureMutable("add role layers");
36
33 37 var values = new ArrayList<String>(1 + extra.length);
34 38
35 39 values.add(Objects.requireNonNull(layer, "Layer name is required"));
36 40 for (var item : extra)
37 41 values.add(Objects.requireNonNull(item, "Layer name is required"));
38 42
39 43 getLayers().addAll(values);
40 44 }
45
46 void finalizeModel() {
47 if (finalized)
48 return;
49
50 getLayers().finalizeValue();
51 getLayers().disallowChanges();
52 finalized = true;
41 53 }
54
55 private void ensureMutable(String operation) {
56 if (finalized)
57 throw new InvalidUserDataException("Role '" + name + "' is finalized and cannot " + operation);
58 }
59 }
@@ -1,315 +1,291
1 1 package org.implab.gradle.common.sources;
2 2
3 import java.util.ArrayList;
3 4 import java.util.Collection;
4 5 import java.util.Collections;
5 6 import java.util.LinkedHashMap;
6 7 import java.util.LinkedHashSet;
8 import java.util.List;
7 9 import java.util.Set;
8 import java.util.regex.Pattern;
9 10
10 11 import javax.inject.Inject;
11 12
12 13 import org.implab.gradle.common.core.lang.Closures;
13 14 import org.gradle.api.Action;
15 import org.gradle.api.InvalidUserDataException;
14 16 import org.gradle.api.Named;
15 17 import org.gradle.api.model.ObjectFactory;
16 18 import org.gradle.api.provider.Provider;
17 19 import org.gradle.api.provider.ProviderFactory;
18 20 import org.gradle.api.attributes.Attribute;
19 21
20 22 import groovy.lang.Closure;
21 23
22 24 public abstract class BuildVariant implements Named {
23 private static final Pattern INVALID_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_.-]");
24
25 25 private final String name;
26 26 private final ObjectFactory objects;
27 private boolean finalized;
27 28
28 29 /**
29 30 * Variant aggregate parts.
30 31 */
31 32 private final VariantAttributes attributes;
32 33 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
33 private final LinkedHashMap<String, BuildLink> links = new LinkedHashMap<>();
34 private final List<LayerLink> links = new ArrayList<>();
34 35 private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();
35 36
36 37 @Inject
37 38 public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
38 39 this.name = name;
39 40 this.objects = objects;
40 41 attributes = new VariantAttributes(providers);
41 42 }
42 43
43 44 @Override
44 45 public String getName() {
45 46 return name;
46 47 }
47 48
48 49 /**
49 50 * Generic variant attributes interpreted by adapters.
50 51 */
51 52 public VariantAttributes getAttributes() {
52 53 return attributes;
53 54 }
54 55
55 56 public void attributes(Action<? super AttributesSpec> action) {
57 ensureMutable("configure attributes");
56 58 action.execute(new AttributesSpec(attributes));
57 59 }
58 60
59 61 public void attributes(Closure<?> configure) {
60 62 attributes(Closures.action(configure));
61 63 }
62 64
63 65 public <T> void attribute(Attribute<T> key, T value) {
66 ensureMutable("set attributes");
64 67 attributes.attribute(key, value);
65 68 }
66 69
67 70 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
71 ensureMutable("set attributes");
68 72 attributes.attributeProvider(key, value);
69 73 }
70 74
71 75 public Collection<BuildRole> getRoles() {
72 76 return Collections.unmodifiableCollection(roles.values());
73 77 }
74 78
75 79 public void roles(Action<? super RolesSpec> action) {
80 ensureMutable("configure roles");
76 81 action.execute(new RolesSpec());
77 82 }
78 83
79 84 public void roles(Closure<?> configure) {
80 85 roles(Closures.action(configure));
81 86 }
82 87
83 88 public BuildRole role(String name, Action<? super BuildRole> configure) {
89 ensureMutable("configure roles");
84 90 var role = roles.computeIfAbsent(name, this::newRole);
85 91 configure.execute(role);
86 92 return role;
87 93 }
88 94
89 95 public BuildRole role(String name, Closure<?> configure) {
90 96 return role(name, Closures.action(configure));
91 97 }
92 98
93 99 public BuildRole role(String name) {
94 100 return role(name, r -> {
95 101 });
96 102 }
97 103
98 104 public BuildRole getRoleByName(String name) {
99 105 return roles.get(name);
100 106 }
101 107
102 public Collection<BuildLink> getLinks() {
103 return Collections.unmodifiableCollection(links.values());
108 public Collection<LayerLink> getLinks() {
109 return Collections.unmodifiableList(links);
104 110 }
105 111
106 112 public void links(Action<? super LinksSpec> action) {
113 ensureMutable("configure links");
107 114 action.execute(new LinksSpec());
108 115 }
109 116
110 117 public void links(Closure<?> configure) {
111 118 links(Closures.action(configure));
112 119 }
113 120
114 public BuildLink link(String from, String to, String kind, Action<? super BuildLink> configure) {
115 return link(defaultLinkName(from, to, kind), link -> {
116 link.from(from);
117 link.to(to);
118 link.kind(kind);
119 configure.execute(link);
120 });
121 }
122
123 public BuildLink link(String from, String to, String kind, Closure<?> configure) {
124 return link(from, to, kind, Closures.action(configure));
125 }
126
127 public BuildLink link(String from, String to, String kind) {
128 return link(from, to, kind, it -> {
129 });
130 }
131
132 public BuildLink link(String name, Action<? super BuildLink> configure) {
133 var link = links.computeIfAbsent(name, this::newLink);
134 configure.execute(link);
121 public LayerLink link(String from, String to, String kind) {
122 ensureMutable("add links");
123 var link = new LayerLink(from, to, kind);
124 links.add(link);
135 125 return link;
136 126 }
137 127
138 public BuildLink link(String name, Closure<?> configure) {
139 return link(name, Closures.action(configure));
140 }
141
142 public BuildLink getLinkByName(String name) {
143 return links.get(name);
144 }
145
146 128 public Collection<BuildArtifactSlot> getArtifactSlots() {
147 129 return Collections.unmodifiableCollection(artifactSlots.values());
148 130 }
149 131
150 132 public void artifactSlots(Action<? super ArtifactSlotsSpec> action) {
133 ensureMutable("configure artifact slots");
151 134 action.execute(new ArtifactSlotsSpec());
152 135 }
153 136
154 137 public void artifactSlots(Closure<?> configure) {
155 138 artifactSlots(Closures.action(configure));
156 139 }
157 140
158 141 public BuildArtifactSlot artifactSlot(String name) {
159 142 return artifactSlot(name, it -> {
160 143 });
161 144 }
162 145
163 146 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
147 ensureMutable("configure artifact slots");
164 148 var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot);
165 149 configure.execute(slot);
166 150 return slot;
167 151 }
168 152
169 153 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
170 154 return artifactSlot(name, Closures.action(configure));
171 155 }
172 156
173 157 public BuildArtifactSlot getArtifactSlotByName(String name) {
174 158 return artifactSlots.get(name);
175 159 }
176 160
177 161 Set<String> declaredLayerNames() {
178 162 var result = new LinkedHashSet<String>();
179 163
180 164 for (var role : roles.values())
181 165 result.addAll(role.getLayers().getOrElse(java.util.List.of()));
182 166
183 167 return result;
184 168 }
185 169
170 void finalizeModel() {
171 if (finalized)
172 return;
173
174 for (var role : roles.values())
175 role.finalizeModel();
176
177 attributes.finalizeModel();
178 finalized = true;
179 }
180
186 181 private BuildRole newRole(String roleName) {
187 182 return objects.newInstance(BuildRole.class, roleName);
188 183 }
189 184
190 private BuildLink newLink(String linkName) {
191 return objects.newInstance(BuildLink.class, linkName);
192 }
193
194 185 private BuildArtifactSlot newArtifactSlot(String slotName) {
195 186 return objects.newInstance(BuildArtifactSlot.class, slotName);
196 187 }
197 188
198 private static String defaultLinkName(String from, String to, String kind) {
199 return "link_" + sanitize(from) + "__" + sanitize(to) + "__" + sanitize(kind);
200 }
201
202 private static String sanitize(String value) {
203 return INVALID_NAME_CHAR.matcher(String.valueOf(value)).replaceAll("_");
189 private void ensureMutable(String operation) {
190 if (finalized)
191 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
204 192 }
205 193
206 194 public final class RolesSpec {
207 195 public BuildRole role(String name, Action<? super BuildRole> configure) {
208 196 return BuildVariant.this.role(name, configure);
209 197 }
210 198
211 199 public BuildRole role(String name, Closure<?> configure) {
212 200 return BuildVariant.this.role(name, configure);
213 201 }
214 202
215 203 public BuildRole role(String name) {
216 204 return BuildVariant.this.role(name);
217 205 }
218 206
219 207 public Collection<BuildRole> getAll() {
220 208 return BuildVariant.this.getRoles();
221 209 }
222 210
223 211 public BuildRole getByName(String name) {
224 212 return BuildVariant.this.getRoleByName(name);
225 213 }
226 214 }
227 215
228 216 public final class LinksSpec {
229 public BuildLink link(String from, String to, String kind, Action<? super BuildLink> configure) {
230 return BuildVariant.this.link(from, to, kind, configure);
231 }
232
233 public BuildLink link(String from, String to, String kind, Closure<?> configure) {
234 return BuildVariant.this.link(from, to, kind, configure);
235 }
236
237 public BuildLink link(String from, String to, String kind) {
217 public LayerLink link(String from, String to, String kind) {
238 218 return BuildVariant.this.link(from, to, kind);
239 219 }
240 220
241 public Collection<BuildLink> getAll() {
221 public Collection<LayerLink> getAll() {
242 222 return BuildVariant.this.getLinks();
243 223 }
244
245 public BuildLink getByName(String name) {
246 return BuildVariant.this.getLinkByName(name);
247 }
248 224 }
249 225
250 226 public final class ArtifactSlotsSpec {
251 227 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
252 228 return BuildVariant.this.artifactSlot(name, configure);
253 229 }
254 230
255 231 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
256 232 return BuildVariant.this.artifactSlot(name, configure);
257 233 }
258 234
259 235 public BuildArtifactSlot artifactSlot(String name) {
260 236 return BuildVariant.this.artifactSlot(name);
261 237 }
262 238
263 239 public Collection<BuildArtifactSlot> getAll() {
264 240 return BuildVariant.this.getArtifactSlots();
265 241 }
266 242
267 243 public BuildArtifactSlot getByName(String name) {
268 244 return BuildVariant.this.getArtifactSlotByName(name);
269 245 }
270 246 }
271 247
272 248 public static final class AttributesSpec {
273 249 private final VariantAttributes attributes;
274 250
275 251 AttributesSpec(VariantAttributes attributes) {
276 252 this.attributes = attributes;
277 253 }
278 254
279 255 public <T> void attribute(Attribute<T> key, T value) {
280 256 attributes.attribute(key, value);
281 257 }
282 258
283 259 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
284 260 attributes.attributeProvider(key, value);
285 261 }
286 262
287 263 public void string(String name, String value) {
288 264 attribute(Attribute.of(name, String.class), value);
289 265 }
290 266
291 267 public void string(String name, Provider<? extends String> value) {
292 268 attributeProvider(Attribute.of(name, String.class), value);
293 269 }
294 270
295 271 public void bool(String name, boolean value) {
296 272 attribute(Attribute.of(name, Boolean.class), value);
297 273 }
298 274
299 275 public void bool(String name, Provider<? extends Boolean> value) {
300 276 attributeProvider(Attribute.of(name, Boolean.class), value);
301 277 }
302 278
303 279 public void integer(String name, int value) {
304 280 attribute(Attribute.of(name, Integer.class), value);
305 281 }
306 282
307 283 public void integer(String name, Provider<? extends Integer> value) {
308 284 attributeProvider(Attribute.of(name, Integer.class), value);
309 285 }
310 286
311 287 public VariantAttributes asAttributes() {
312 288 return attributes;
313 289 }
314 290 }
315 291 }
@@ -1,271 +1,295
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
35 layers.all(layer -> {
36 if (finalized)
37 throw new InvalidUserDataException(
38 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
39 });
40
41 variants.all(variant -> {
42 if (finalized)
43 throw new InvalidUserDataException(
44 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
45 });
34 46 }
35 47
36 48 public NamedDomainObjectContainer<BuildLayer> getLayers() {
37 49 return layers;
38 50 }
39 51
40 52 public NamedDomainObjectContainer<BuildVariant> getVariants() {
41 53 return variants;
42 54 }
43 55
44 56 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
57 ensureMutable("configure layers");
45 58 action.execute(layers);
46 59 }
47 60
48 61 public void layers(Closure<?> configure) {
49 62 layers(Closures.action(configure));
50 63 }
51 64
52 65 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
66 ensureMutable("configure variants");
53 67 action.execute(variants);
54 68 }
55 69
56 70 public void variants(Closure<?> configure) {
57 71 variants(Closures.action(configure));
58 72 }
59 73
60 74 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
75 ensureMutable("configure layers");
61 76 var layer = layers.maybeCreate(name);
62 77 configure.execute(layer);
63 78 return layer;
64 79 }
65 80
66 81 public BuildLayer layer(String name, Closure<?> configure) {
67 82 return layer(name, Closures.action(configure));
68 83 }
69 84
70 85 public BuildLayer layer(String name) {
71 86 return layer(name, it -> {
72 87 });
73 88 }
74 89
75 90 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
91 ensureMutable("configure variants");
76 92 var variant = variants.maybeCreate(name);
77 93 configure.execute(variant);
78 94 return variant;
79 95 }
80 96
81 97 public BuildVariant variant(String name, Closure<?> configure) {
82 98 return variant(name, Closures.action(configure));
83 99 }
84 100
85 101 public BuildVariant variant(String name) {
86 102 return variant(name, it -> {
87 103 });
88 104 }
89 105
90 106 public void all(Action<? super BuildVariant> action) {
91 107 variants.all(action);
92 108 }
93 109
94 110 public void all(Closure<?> configure) {
95 111 all(Closures.action(configure));
96 112 }
97 113
98 114 public Collection<BuildVariant> getAll() {
99 115 var all = new ArrayList<BuildVariant>();
100 116 variants.forEach(all::add);
101 117 return Collections.unmodifiableList(all);
102 118 }
103 119
104 120 public BuildVariant getByName(String name) {
105 121 return variants.findByName(name);
106 122 }
107 123
108 124 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
109 125 if (finalized) {
110 126 action.execute(this);
111 127 return;
112 128 }
113 129 finalizedActions.add(action);
114 130 }
115 131
116 132 public void whenFinalized(Closure<?> configure) {
117 133 whenFinalized(Closures.action(configure));
118 134 }
119 135
120 136 public boolean isFinalized() {
121 137 return finalized;
122 138 }
123 139
124 140 public void finalizeModel() {
125 141 if (finalized)
126 142 return;
127 143
128 144 validate();
145
146 for (var variant : variants)
147 variant.finalizeModel();
148
129 149 finalized = true;
130 150
131 151 var actions = new ArrayList<>(finalizedActions);
132 152 finalizedActions.clear();
133 153 for (var action : actions)
134 154 action.execute(this);
135 155 }
136 156
137 157 public void validate() {
138 158 var errors = new ArrayList<String>();
139 159
140 160 var layersByName = new LinkedHashMap<String, BuildLayer>();
141 161 for (var layer : layers)
142 162 layersByName.put(layer.getName(), layer);
143 163
144 164 for (var variant : variants)
145 165 validateVariant(variant, layersByName, errors);
146 166
147 167 if (!errors.isEmpty()) {
148 168 var message = new StringBuilder("Invalid variants model:");
149 169 for (var error : errors)
150 170 message.append("\n - ").append(error);
151 171
152 172 throw new InvalidUserDataException(message.toString());
153 173 }
154 174 }
155 175
156 176 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
157 177 var variantLayers = validateRoleMappings(variant, layersByName, errors);
158 178 validateLinks(variant, variantLayers, errors);
159 179 }
160 180
161 181 private static Set<String> validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
162 182 List<String> errors) {
163 183 var variantLayers = new LinkedHashSet<String>();
164 184
165 185 for (var role : variant.getRoles()) {
166 186 for (var layerName : role.getLayers().getOrElse(List.of())) {
167 187 if (isBlank(layerName)) {
168 188 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
169 189 continue;
170 190 }
171 191
172 192 var layer = layersByName.get(layerName);
173 193 if (layer == null) {
174 194 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + layerName + "'");
175 195 continue;
176 196 }
177 197
178 198 variantLayers.add(layerName);
179 199 }
180 200 }
181 201
182 202 return variantLayers;
183 203 }
184 204
185 205 private static void validateLinks(BuildVariant variant, Set<String> variantLayers, List<String> errors) {
186 206 var seenLinks = new HashSet<String>();
187 207 var edgesByKind = new HashMap<String, Map<String, Set<String>>>();
188 208
189 209 for (var link : variant.getLinks()) {
190 var from = normalize(link.getFrom().getOrNull());
191 var to = normalize(link.getTo().getOrNull());
192 var kind = normalize(link.getKind().getOrNull());
210 var from = normalize(link.from());
211 var to = normalize(link.to());
212 var kind = normalize(link.kind());
193 213
194 214 if (from == null || to == null || kind == null) {
195 errors.add("Variant '" + variant.getName() + "' has incomplete link '" + link.getName()
196 + "' (from/to/kind are required)");
215 errors.add("Variant '" + variant.getName() + "' has incomplete link (from/to/kind are required)");
197 216 continue;
198 217 }
199 218
200 219 if (!variantLayers.contains(from)) {
201 errors.add("Variant '" + variant.getName() + "' link '" + link.getName() + "' references unknown source layer '"
220 errors.add("Variant '" + variant.getName() + "' link references unknown source layer '"
202 221 + from + "'");
203 222 continue;
204 223 }
205 224
206 225 if (!variantLayers.contains(to)) {
207 errors.add("Variant '" + variant.getName() + "' link '" + link.getName() + "' references unknown target layer '"
226 errors.add("Variant '" + variant.getName() + "' link references unknown target layer '"
208 227 + to + "'");
209 228 continue;
210 229 }
211 230
212 231 var linkKey = from + "\u0000" + to + "\u0000" + kind;
213 232 if (!seenLinks.add(linkKey)) {
214 233 errors.add("Variant '" + variant.getName() + "' has duplicated link tuple (from='" + from
215 234 + "', to='" + to + "', kind='" + kind + "')");
216 235 }
217 236
218 237 edgesByKind
219 238 .computeIfAbsent(kind, x -> new LinkedHashMap<>())
220 239 .computeIfAbsent(from, x -> new LinkedHashSet<>())
221 240 .add(to);
222 241 }
223 242
224 243 for (var entry : edgesByKind.entrySet()) {
225 244 if (hasCycle(variantLayers, entry.getValue())) {
226 245 errors.add("Variant '" + variant.getName() + "' contains cycle in links with kind '" + entry.getKey() + "'");
227 246 }
228 247 }
229 248 }
230 249
231 250 private static boolean hasCycle(Set<String> nodes, Map<String, Set<String>> edges) {
232 251 var state = new HashMap<String, Integer>();
233 252
234 253 for (var node : nodes) {
235 254 if (dfs(node, state, edges))
236 255 return true;
237 256 }
238 257
239 258 return false;
240 259 }
241 260
242 261 private static boolean dfs(String node, Map<String, Integer> state, Map<String, Set<String>> edges) {
243 262 var current = state.getOrDefault(node, 0);
244 263 if (current == 1)
245 264 return true;
246 265 if (current == 2)
247 266 return false;
248 267
249 268 state.put(node, 1);
250 269
251 270 for (var next : edges.getOrDefault(node, Set.of())) {
252 271 if (dfs(next, state, edges))
253 272 return true;
254 273 }
255 274
256 275 state.put(node, 2);
257 276 return false;
258 277 }
259 278
260 279 private static String normalize(String value) {
261 280 if (value == null)
262 281 return null;
263 282
264 283 var trimmed = value.trim();
265 284 return trimmed.isEmpty() ? null : trimmed;
266 285 }
267 286
268 287 private static boolean isBlank(String value) {
269 288 return normalize(value) == null;
270 289 }
290
291 private void ensureMutable(String operation) {
292 if (finalized)
293 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
271 294 }
295 }
@@ -1,36 +1,40
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.gradle.api.GradleException;
4 4 import org.gradle.api.NamedDomainObjectContainer;
5 5 import org.gradle.api.Plugin;
6 6 import org.gradle.api.Project;
7 7 import org.gradle.api.logging.Logger;
8 8 import org.gradle.api.logging.Logging;
9 9
10 10 /**
11 11 * This plugin creates a {@code sources} extension which is
12 12 * a container for {@link GenericSourceSet}.
13 13 *
14 14 */
15 15 public abstract class SourcesPlugin implements Plugin<Project> {
16 16 private static final Logger logger = Logging.getLogger(SourcesPlugin.class);
17 17
18 18 private static final String SOURCES_EXTENSION_NAME = "sources";
19 19
20 20 @Override
21 21 public void apply(Project target) {
22 logger.debug("Registering '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath());
22 23 var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class);
23 24 target.getExtensions().add(SOURCES_EXTENSION_NAME, sources);
24 25 }
25 26
26 27 public static NamedDomainObjectContainer<GenericSourceSet> getSourcesExtension(Project target) {
27 28 var extensions = target.getExtensions();
28 29
29 30 @SuppressWarnings("unchecked")
30 var extension = (NamedDomainObjectContainer<GenericSourceSet>)extensions.getByName(SOURCES_EXTENSION_NAME);
31 var extension = (NamedDomainObjectContainer<GenericSourceSet>) extensions.findByName(SOURCES_EXTENSION_NAME);
31 32
32 if (extension == null)
33 if (extension == null) {
34 logger.error("Sources extension '{}' isn't found on project '{}'", SOURCES_EXTENSION_NAME, target.getPath());
33 35 throw new GradleException("Sources extension isn't found");
36 }
37 logger.debug("Resolved '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath());
34 38 return extension;
35 39 }
36 40 }
@@ -1,46 +1,59
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 import org.gradle.api.InvalidUserDataException;
7 8 import org.gradle.api.attributes.Attribute;
8 9 import org.gradle.api.provider.ProviderFactory;
9 10 import org.gradle.api.provider.Provider;
10 11
11 12 /**
12 13 * Typed attribute storage used by build variants.
13 14 */
14 15 public final class VariantAttributes {
15 16 private final ProviderFactory providers;
16 17 private final LinkedHashMap<Attribute<?>, Provider<?>> values = new LinkedHashMap<>();
18 private boolean finalized;
17 19
18 20 VariantAttributes(ProviderFactory providers) {
19 21 this.providers = providers;
20 22 }
21 23
22 24 public <T> void attribute(Attribute<T> key, T value) {
25 ensureMutable("set attribute '" + key.getName() + "'");
23 26 attributeProvider(key, providers.provider(() -> value));
24 27 }
25 28
26 29 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
30 ensureMutable("set attribute provider '" + key.getName() + "'");
27 31 values.put(key, value);
28 32 }
29 33
30 34 @SuppressWarnings("unchecked")
31 35 public <T> Provider<T> get(Attribute<T> key) {
32 36 return (Provider<T>) values.get(key);
33 37 }
34 38
35 39 public boolean contains(Attribute<?> key) {
36 40 return values.containsKey(key);
37 41 }
38 42
39 43 public int size() {
40 44 return values.size();
41 45 }
42 46
43 47 public Map<Attribute<?>, Provider<?>> asMap() {
44 48 return Collections.unmodifiableMap(values);
45 49 }
50
51 void finalizeModel() {
52 finalized = true;
46 53 }
54
55 private void ensureMutable(String operation) {
56 if (finalized)
57 throw new InvalidUserDataException("Variant attributes are finalized and cannot " + operation);
58 }
59 }
@@ -1,283 +1,304
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 import org.gradle.api.logging.Logger;
21 import org.gradle.api.logging.Logging;
20 22
21 23 import groovy.lang.Closure;
22 24 import groovy.lang.DelegatesTo;
23 25
24 26 /**
25 * Adapter extension that materializes source sets for variant/layer pairs.
27 * Adapter extension that registers source sets for variant/layer pairs.
26 28 */
27 29 @NonNullByDefault
28 30 public abstract class VariantSourcesExtension {
31 private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class);
29 32 private static final Pattern INVALID_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_.-]");
30 33 private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}");
31 34
32 35 private final ObjectFactory objects;
33 36 private final NamedDomainObjectContainer<BuildLayerBinding> bindings;
34 37 private final List<Action<? super SourceSetContext>> registeredActions = new ArrayList<>();
35 38 private final List<Action<? super SourceSetContext>> boundActions = new ArrayList<>();
36 39 private final List<SourceSetContext> registeredContexts = new ArrayList<>();
37 40 private final List<SourceSetContext> boundContexts = new ArrayList<>();
38 41 private final LinkedHashMap<String, NamedDomainObjectProvider<GenericSourceSet>> sourceSetsByName = new LinkedHashMap<>();
39 42 private final LinkedHashMap<String, String> sourceSetLayersByName = new LinkedHashMap<>();
40 43
41 44 @Inject
42 45 public VariantSourcesExtension(ObjectFactory objects) {
43 46 this.objects = objects;
44 47 bindings = objects.domainObjectContainer(BuildLayerBinding.class);
45 48 }
46 49
47 50 public NamedDomainObjectContainer<BuildLayerBinding> getBindings() {
48 51 return bindings;
49 52 }
50 53
51 54 public void bindings(Action<? super NamedDomainObjectContainer<BuildLayerBinding>> action) {
52 55 action.execute(bindings);
53 56 }
54 57
55 58 public void bindings(
56 59 @DelegatesTo(value = NamedDomainObjectContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
57 60 bindings(Closures.action(action));
58 61 }
59 62
60 63 public BuildLayerBinding bind(String layer) {
61 64 return bindings.maybeCreate(normalize(layer));
62 65 }
63 66
64 67 /**
65 68 * Configures per-layer binding.
66 69 */
67 70 public BuildLayerBinding bind(String layer, Action<? super BuildLayerBinding> configure) {
68 71 var binding = bind(layer);
69 72 configure.execute(binding);
70 73 return binding;
71 74 }
72 75
73 76 public BuildLayerBinding bind(String layer,
74 77 @DelegatesTo(value = BuildLayerBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
75 78 return bind(layer, Closures.action(configure));
76 79 }
77 80
78 81 /**
79 82 * Global callback fired for each registered source-set context.
80 83 * Already emitted contexts are delivered immediately (replay).
81 84 * For simple callbacks you can use delegate-only style
82 85 * (for example {@code whenRegistered { sourceSetName() }}).
83 86 * For nested closures prefer explicit parameter
84 87 * ({@code whenRegistered { ctx -> ... }}).
85 88 */
86 89 public void whenRegistered(Action<? super SourceSetContext> action) {
87 90 registeredActions.add(action);
88 91 for (var context : registeredContexts)
89 92 action.execute(context);
90 93 }
91 94
92 95 public void whenRegistered(
93 96 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
94 97 whenRegistered(Closures.action(action));
95 98 }
96 99
97 100 public void whenRegistered(String variantName, Action<? super SourceSetContext> action) {
98 101 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
99 102 whenRegistered(filterByVariant(normalizedVariantName, action));
100 103 }
101 104
102 105 public void whenRegistered(String variantName,
103 106 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
104 107 whenRegistered(variantName, Closures.action(action));
105 108 }
106 109
107 110 /**
108 111 * Global callback fired for every resolved variant/role/layer usage.
109 112 * Already emitted contexts are delivered immediately (replay).
110 113 * For simple callbacks you can use delegate-only style
111 114 * (for example {@code whenBound { variantName() }}).
112 115 * For nested closures prefer explicit parameter
113 116 * ({@code whenBound { ctx -> ... }}).
114 117 */
115 118 public void whenBound(Action<? super SourceSetContext> action) {
116 119 boundActions.add(action);
117 120 for (var context : boundContexts)
118 121 action.execute(context);
119 122 }
120 123
121 124 public void whenBound(
122 125 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
123 126 whenBound(Closures.action(action));
124 127 }
125 128
126 129 public void whenBound(String variantName, Action<? super SourceSetContext> action) {
127 130 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
128 131 whenBound(filterByVariant(normalizedVariantName, action));
129 132 }
130 133
131 134 public void whenBound(String variantName,
132 135 @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
133 136 whenBound(variantName, Closures.action(action));
134 137 }
135 138
136 139 void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer<GenericSourceSet> sources) {
137 140 validateBindings(variants);
138 layerUsages(variants).forEach(usage -> materializeLayerUsage(usage, sources));
141
142 var usages = layerUsages(variants).toList();
143 var registeredBefore = registeredContexts.size();
144 var boundBefore = boundContexts.size();
145
146 logger.debug(
147 "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})",
148 variants.getVariants().size(),
149 variants.getLayers().size(),
150 bindings.size(),
151 usages.size());
152
153 usages.forEach(usage -> registerLayerUsage(usage, sources));
154
155 logger.debug(
156 "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})",
157 registeredContexts.size() - registeredBefore,
158 boundContexts.size() - boundBefore,
159 sourceSetsByName.size());
139 160 }
140 161
141 162 private Stream<LayerUsage> layerUsages(BuildVariantsExtension variants) {
142 163 return variants.getVariants().stream()
143 164 .flatMap(variant -> variant.getRoles().stream()
144 165 .flatMap(role -> role.getLayers().getOrElse(List.of()).stream()
145 166 .map(layerName -> new LayerUsage(
146 167 variant.getName(),
147 168 role.getName(),
148 169 normalize(layerName)))));
149 170 }
150 171
151 private void materializeLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
172 private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
152 173 var resolvedBinding = bind(usage.layerName());
153 174 var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern();
154 175 sourceSetNamePattern.finalizeValueOnRead();
155 176
156 177 var sourceSetName = sourceSetName(usage, sourceSetNamePattern.get());
157 178
158 179 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layerName());
159 180 var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName);
160 181 var sourceSet = sourceSetsByName.computeIfAbsent(sourceSetName,
161 182 name -> sources.register(name));
162 183
163 184 var context = new SourceSetContext(
164 185 usage.variantName(),
165 186 usage.roleName(),
166 187 usage.layerName(),
167 188 sourceSetName,
168 189 sourceSet);
169 190
170 191 if (isNewSourceSet) {
171 192 resolvedBinding.notifyRegistered(context);
172 193 notifyRegistered(context);
173 194 }
174 195
175 196 resolvedBinding.notifyBound(context);
176 197 notifyBound(context);
177 198 }
178 199
179 200 private void notifyRegistered(SourceSetContext context) {
180 201 registeredContexts.add(context);
181 202 for (var action : registeredActions)
182 203 action.execute(context);
183 204 }
184 205
185 206 private void notifyBound(SourceSetContext context) {
186 207 boundContexts.add(context);
187 208 for (var action : boundActions)
188 209 action.execute(context);
189 210 }
190 211
191 212 private static Action<? super SourceSetContext> filterByVariant(String variantName,
192 213 Action<? super SourceSetContext> action) {
193 214 return context -> {
194 215 if (variantName.equals(context.variantName()))
195 216 action.execute(context);
196 217 };
197 218 }
198 219
199 220 private void ensureSourceSetNameBoundToSingleLayer(String sourceSetName, String layerName) {
200 221 var existingLayer = sourceSetLayersByName.putIfAbsent(sourceSetName, layerName);
201 222 if (existingLayer != null && !existingLayer.equals(layerName)) {
202 223 throw new InvalidUserDataException("Source set '" + sourceSetName + "' is resolved from multiple layers: '"
203 224 + existingLayer + "' and '" + layerName + "'");
204 225 }
205 226 }
206 227
207 228 private void validateBindings(BuildVariantsExtension variants) {
208 229 var knownLayerNames = new java.util.LinkedHashSet<String>();
209 230 for (var layer : variants.getLayers())
210 231 knownLayerNames.add(layer.getName());
211 232
212 233 var errors = new ArrayList<String>();
213 234 for (var binding : bindings) {
214 235 if (!knownLayerNames.contains(binding.getName())) {
215 236 errors.add("Layer binding '" + binding.getName() + "' references unknown layer");
216 237 }
217 238 }
218 239
219 240 if (!errors.isEmpty()) {
220 241 var message = new StringBuilder("Invalid variantSources model:");
221 242 for (var error : errors)
222 243 message.append("\n - ").append(error);
223 244 throw new InvalidUserDataException(message.toString());
224 245 }
225 246 }
226 247
227 248 private static String sourceSetName(LayerUsage usage, String pattern) {
228 249 var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank");
229 250 var resolved = resolveSourceSetNamePattern(normalizedPattern, usage);
230 251 var result = sanitize(resolved);
231 252
232 253 if (result.isEmpty())
233 254 throw new InvalidUserDataException("sourceSetNamePattern '" + pattern + "' resolved to empty source set name");
234 255
235 256 return result;
236 257 }
237 258
238 259 private static String resolveSourceSetNamePattern(String pattern, LayerUsage usage) {
239 260 var matcher = SOURCE_SET_NAME_TOKEN.matcher(pattern);
240 261 var output = new StringBuffer();
241 262
242 263 while (matcher.find()) {
243 264 var token = matcher.group(1);
244 265 matcher.appendReplacement(output, Matcher.quoteReplacement(tokenValue(token, usage)));
245 266 }
246 267 matcher.appendTail(output);
247 268
248 269 return output.toString();
249 270 }
250 271
251 272 private static String tokenValue(String token, LayerUsage usage) {
252 273 return switch (token) {
253 274 case "variant" -> sanitize(usage.variantName());
254 275 case "variantCap" -> Strings.capitalize(sanitize(usage.variantName()));
255 276 case "role" -> sanitize(usage.roleName());
256 277 case "roleCap" -> Strings.capitalize(sanitize(usage.roleName()));
257 278 case "layer" -> sanitize(usage.layerName());
258 279 case "layerCap" -> Strings.capitalize(sanitize(usage.layerName()));
259 280 default -> throw new InvalidUserDataException(
260 281 "sourceSetNamePattern contains unsupported token '{" + token + "}'");
261 282 };
262 283 }
263 284
264 285 private static String sanitize(String value) {
265 286 return INVALID_NAME_CHAR.matcher(value).replaceAll("_");
266 287 }
267 288
268 289 private static String normalize(String value) {
269 290 return normalize(value, "Value must not be null or blank");
270 291 }
271 292
272 293 private static String normalize(String value, String errorMessage) {
273 294 if (value == null)
274 295 throw new InvalidUserDataException(errorMessage);
275 296 var trimmed = value.trim();
276 297 if (trimmed.isEmpty())
277 298 throw new InvalidUserDataException(errorMessage);
278 299 return trimmed;
279 300 }
280 301
281 302 private record LayerUsage(String variantName, String roleName, String layerName) {
282 303 }
283 304 }
@@ -1,27 +1,39
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.gradle.api.GradleException;
4 4 import org.gradle.api.Plugin;
5 5 import org.gradle.api.Project;
6 import org.gradle.api.logging.Logger;
7 import org.gradle.api.logging.Logging;
6 8
7 9 /**
8 10 * Registers {@code variants} extension for build-variant modeling.
9 11 */
10 12 public abstract class VariantsPlugin implements Plugin<Project> {
13 private static final Logger logger = Logging.getLogger(VariantsPlugin.class);
11 14 public static final String VARIANTS_EXTENSION_NAME = "variants";
12 15
13 16 @Override
14 17 public void apply(Project target) {
18 logger.debug("Registering '{}' extension on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath());
15 19 var variants = target.getExtensions().create(VARIANTS_EXTENSION_NAME, BuildVariantsExtension.class);
16 target.afterEvaluate(project -> variants.finalizeModel());
20 target.afterEvaluate(project -> {
21 logger.debug("Finalizing variants model on project '{}'", project.getPath());
22 variants.finalizeModel();
23 logger.debug("Variants model finalized on project '{}'", project.getPath());
24 });
17 25 }
18 26
19 27 public static BuildVariantsExtension getVariantsExtension(Project target) {
20 28 var extension = target.getExtensions().findByType(BuildVariantsExtension.class);
21 29
22 if (extension == null)
30 if (extension == null) {
31 logger.error("Variants extension '{}' isn't found on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath());
23 32 throw new GradleException("Variants extension isn't found");
33 }
34
35 logger.debug("Resolved '{}' extension on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath());
24 36
25 37 return extension;
26 38 }
27 39 }
@@ -1,25 +1,33
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.gradle.api.Plugin;
4 4 import org.gradle.api.Project;
5 import org.gradle.api.logging.Logger;
6 import org.gradle.api.logging.Logging;
5 7
6 8 /**
7 * Binds variant layers to materialized source sets.
9 * Binds variant layers to registered source sets.
8 10 */
9 11 public abstract class VariantsSourcesPlugin implements Plugin<Project> {
12 private static final Logger logger = Logging.getLogger(VariantsSourcesPlugin.class);
10 13 public static final String VARIANT_SOURCES_EXTENSION_NAME = "variantSources";
11 14
12 15 @Override
13 16 public void apply(Project target) {
17 logger.debug("Applying variant-sources plugin on project '{}'", target.getPath());
14 18 target.getPluginManager().apply(VariantsPlugin.class);
15 19 target.getPluginManager().apply(SourcesPlugin.class);
16 20
17 21 var variants = VariantsPlugin.getVariantsExtension(target);
18 22 var sources = SourcesPlugin.getSourcesExtension(target);
19 23
20 24 var variantSources = target.getExtensions()
21 25 .create(VARIANT_SOURCES_EXTENSION_NAME, VariantSourcesExtension.class);
22 26
23 variants.whenFinalized(model -> variantSources.registerSourceSets(model, sources));
27 variants.whenFinalized(model -> {
28 logger.debug("Registering source sets for variants on project '{}'", target.getPath());
29 variantSources.registerSourceSets(model, sources);
30 logger.debug("Registered source sets on project '{}'", target.getPath());
31 });
24 32 }
25 33 }
@@ -1,290 +1,311
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 link('l1') {
157 from('a')
158 to('b')
156 link('a', 'b', null)
159 157 }
160 158 }
161 }
162 """, "has incomplete link 'l1'");
159 """, "has incomplete link (from/to/kind are required)");
163 160 }
164 161
165 162 @Test
166 163 void failsOnDuplicatedLinkTuple() throws Exception {
167 164 assertBuildFails("""
168 165 plugins {
169 166 id 'org.implab.gradle-variants'
170 167 }
171 168
172 169 variants {
173 170 layer('a')
174 171 layer('b')
175 172
176 173 variant('browser') {
177 174 role('main') {
178 175 layers('a', 'b')
179 176 }
180 link('first') {
181 from('a')
182 to('b')
183 kind('ts:api')
184 }
185 link('second') {
186 from('a')
187 to('b')
188 kind('ts:api')
189 }
177 link('a', 'b', 'ts:api')
178 link('a', 'b', 'ts:api')
190 179 }
191 180 }
192 181 """, "has duplicated link tuple (from='a', to='b', kind='ts:api')");
193 182 }
194 183
195 184 @Test
196 185 void failsOnUnknownSourceLayerInLink() throws Exception {
197 186 assertBuildFails("""
198 187 plugins {
199 188 id 'org.implab.gradle-variants'
200 189 }
201 190
202 191 variants {
203 192 layer('a') {
204 193 }
205 194
206 195 variant('browser') {
207 196 role('main') {
208 197 layers('a')
209 198 }
210 link('l1') {
211 from('missing')
212 to('a')
213 kind('ts:api')
214 }
199 link('missing', 'a', 'ts:api')
215 200 }
216 201 }
217 202 """, "references unknown source layer 'missing'");
218 203 }
219 204
220 205 @Test
221 206 void failsOnUnknownTargetLayerInLink() throws Exception {
222 207 assertBuildFails("""
223 208 plugins {
224 209 id 'org.implab.gradle-variants'
225 210 }
226 211
227 212 variants {
228 213 layer('a') {
229 214 }
230 215
231 216 variant('browser') {
232 217 role('main') {
233 218 layers('a')
234 219 }
235 link('l1') {
236 from('a')
237 to('missing')
238 kind('ts:api')
239 }
220 link('a', 'missing', 'ts:api')
240 221 }
241 222 }
242 223 """, "references unknown target layer 'missing'");
243 224 }
244 225
226 @Test
227 void failsOnLateLayerMutationAfterFinalize() throws Exception {
228 assertBuildFails("""
229 plugins {
230 id 'org.implab.gradle-variants'
231 }
232
233 variants {
234 layer('a')
235 variant('browser') {
236 role('main') { layers('a') }
237 }
238 }
239
240 afterEvaluate {
241 variants.layer('late')
242 }
243 """, "Variants model is finalized and cannot configure layers");
244 }
245
246 @Test
247 void failsOnLateVariantMutationAfterFinalize() throws Exception {
248 assertBuildFails("""
249 plugins {
250 id 'org.implab.gradle-variants'
251 }
252
253 variants {
254 layer('a')
255 variant('browser') {
256 role('main') { layers('a') }
257 }
258 }
259
260 afterEvaluate {
261 variants.getByName('browser').role('late') { layers('a') }
262 }
263 """, "Variant 'browser' is finalized and cannot configure roles");
264 }
265
245 266 private GradleRunner runner(String... arguments) {
246 267 return GradleRunner.create()
247 268 .withProjectDir(testProjectDir.toFile())
248 269 .withPluginClasspath(pluginClasspath())
249 270 .withArguments(arguments)
250 271 .forwardOutput();
251 272 }
252 273
253 274 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
254 275 writeFile(SETTINGS_FILE, ROOT_NAME);
255 276 writeFile(BUILD_FILE, buildScript);
256 277
257 278 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
258 279 var output = ex.getBuildResult().getOutput();
259 280
260 281 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
261 282 }
262 283
263 284 private static List<File> pluginClasspath() {
264 285 try {
265 286 var classesDir = Path.of(BuildVariant.class
266 287 .getProtectionDomain()
267 288 .getCodeSource()
268 289 .getLocation()
269 290 .toURI());
270 291
271 292 var markerResource = VariantsPlugin.class.getClassLoader()
272 293 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
273 294
274 295 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
275 296
276 297 var markerPath = Path.of(markerResource.toURI());
277 298 var resourcesDir = markerPath.getParent().getParent().getParent();
278 299
279 300 return List.of(classesDir.toFile(), resourcesDir.toFile());
280 301 } catch (Exception e) {
281 302 throw new RuntimeException("Unable to build plugin classpath for test", e);
282 303 }
283 304 }
284 305
285 306 private void writeFile(String relativePath, String content) throws IOException {
286 307 Path path = testProjectDir.resolve(relativePath);
287 308 Files.createDirectories(path.getParent());
288 309 Files.writeString(path, content);
289 310 }
290 311 }
@@ -1,366 +1,366
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 VariantsSourcesPluginFunctionalTest {
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-sources-fixture'\n";
24 24
25 25 @TempDir
26 26 Path testProjectDir;
27 27
28 28 @Test
29 void materializesVariantSourceSetsAndFiresCallbacks() throws Exception {
29 void registersVariantSourceSetsAndFiresCallbacks() throws Exception {
30 30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 31 writeFile(BUILD_FILE, """
32 32 plugins {
33 33 id 'org.implab.gradle-variants-sources'
34 34 }
35 35
36 36 variants {
37 37 layer('mainBase')
38 38 layer('mainAmd')
39 39
40 40 variant('browser') {
41 41 role('main') { layers('mainBase', 'mainAmd') }
42 42 }
43 43
44 44 variant('node') {
45 45 role('main') { layers('mainBase') }
46 46 }
47 47 }
48 48
49 49 def events = []
50 50 def localEvents = []
51 51
52 52 variantSources {
53 53 bind('mainBase') {
54 54 configureSourceSet {
55 55 declareOutputs('compiled')
56 56 }
57 57 }
58 58 bind('mainAmd') {
59 59 configureSourceSet {
60 60 declareOutputs('compiled')
61 61 }
62 62 }
63 63 bind('mainAmd').whenRegistered { ctx ->
64 64 localEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
65 65 }
66 66 whenRegistered { ctx ->
67 67 events << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
68 68 }
69 69 }
70 70
71 71 tasks.register('probe') {
72 72 doLast {
73 73 println("sources=" + sources.collect { it.name }.sort().join(','))
74 74 println("events=" + events.sort().join('|'))
75 75 println("local=" + localEvents.sort().join('|'))
76 76
77 77 def base = sources.getByName('browserMainBase')
78 78 def amd = sources.getByName('browserMainAmd')
79 79 def nodeBase = sources.getByName('nodeMainBase')
80 80
81 81 base.output('compiled')
82 82 amd.output('compiled')
83 83 nodeBase.output('compiled')
84 84
85 85 println('outputs=ok')
86 86 }
87 87 }
88 88 """);
89 89
90 90 BuildResult result = runner("probe").build();
91 91
92 92 assertTrue(result.getOutput().contains("sources=browserMainAmd,browserMainBase,nodeMainBase"));
93 93 assertTrue(result.getOutput().contains(
94 94 "events=browser:main:mainAmd:browserMainAmd|browser:main:mainBase:browserMainBase|node:main:mainBase:nodeMainBase"));
95 95 assertTrue(result.getOutput().contains("local=browser:main:mainAmd:browserMainAmd"));
96 96 assertTrue(result.getOutput().contains("outputs=ok"));
97 97 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
98 98 }
99 99
100 100 @Test
101 101 void supportsTrailingClosureOnBind() throws Exception {
102 102 writeFile(SETTINGS_FILE, ROOT_NAME);
103 103 writeFile(BUILD_FILE, """
104 104 plugins {
105 105 id 'org.implab.gradle-variants-sources'
106 106 }
107 107
108 108 variants {
109 109 layer('main')
110 110 variant('browser') {
111 111 role('main') { layers('main') }
112 112 }
113 113 }
114 114
115 115 variantSources {
116 116 bind('main') {
117 117 configureSourceSet {
118 118 declareOutputs('compiled')
119 119 }
120 120 }
121 121 }
122 122
123 123 tasks.register('probe') {
124 124 doLast {
125 125 def ss = sources.getByName('browserMain')
126 126 ss.output('compiled')
127 127 println('bindClosure=ok')
128 128 }
129 129 }
130 130 """);
131 131
132 132 BuildResult result = runner("probe").build();
133 133 assertTrue(result.getOutput().contains("bindClosure=ok"));
134 134 }
135 135
136 136 @Test
137 137 void failsOnUnknownLayerBinding() throws Exception {
138 138 writeFile(SETTINGS_FILE, ROOT_NAME);
139 139 writeFile(BUILD_FILE, """
140 140 plugins {
141 141 id 'org.implab.gradle-variants-sources'
142 142 }
143 143
144 144 variants {
145 145 layer('main')
146 146 variant('browser') {
147 147 role('main') { layers('main') }
148 148 }
149 149 }
150 150
151 151 variantSources {
152 152 bind('missing')
153 153 }
154 154 """);
155 155
156 156 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
157 157 assertTrue(ex.getBuildResult().getOutput().contains("Layer binding 'missing' references unknown layer"));
158 158 }
159 159
160 160 @Test
161 161 void exposesProviderInSourceSetRegisteredContext() throws Exception {
162 162 writeFile(SETTINGS_FILE, ROOT_NAME);
163 163 writeFile(BUILD_FILE, """
164 164 plugins {
165 165 id 'org.implab.gradle-variants-sources'
166 166 }
167 167
168 168 variants {
169 169 layer('main')
170 170 variant('browser') {
171 171 role('main') { layers('main') }
172 172 }
173 173 }
174 174
175 175 variantSources {
176 176 whenRegistered {
177 177 configureSourceSet {
178 178 declareOutputs('generated')
179 179 }
180 180 }
181 181 }
182 182
183 183 tasks.register('probe') {
184 184 doLast {
185 185 def ss = sources.getByName('browserMain')
186 186 ss.output('generated')
187 187 println('contextProvider=ok')
188 188 }
189 189 }
190 190 """);
191 191
192 192 BuildResult result = runner("probe").build();
193 193 assertTrue(result.getOutput().contains("contextProvider=ok"));
194 194 }
195 195
196 196 @Test
197 void replaysLateBindingsAndCallbacksAfterMaterialization() throws Exception {
197 void replaysLateBindingsAndCallbacksAfterRegistration() throws Exception {
198 198 writeFile(SETTINGS_FILE, ROOT_NAME);
199 199 writeFile(BUILD_FILE, """
200 200 plugins {
201 201 id 'org.implab.gradle-variants-sources'
202 202 }
203 203
204 204 variants {
205 205 layer('main')
206 206 variant('browser') {
207 207 role('main') { layers('main') }
208 208 }
209 209 }
210 210
211 211 def events = []
212 212
213 213 afterEvaluate {
214 214 variantSources {
215 215 bind('main') {
216 216 configureSourceSet {
217 217 declareOutputs('late')
218 218 }
219 219 }
220 220
221 221 bind('main').whenRegistered { ctx ->
222 222 events << "layer:${ctx.sourceSetName()}"
223 223 }
224 224
225 225 whenRegistered { ctx ->
226 226 events << "global:${ctx.sourceSetName()}"
227 227 }
228 228 }
229 229 }
230 230
231 231 tasks.register('probe') {
232 232 doLast {
233 233 def ss = sources.getByName('browserMain')
234 234 ss.output('late')
235 235 println("events=" + events.sort().join('|'))
236 236 println('lateReplay=ok')
237 237 }
238 238 }
239 239 """);
240 240
241 241 BuildResult result = runner("probe").build();
242 242 assertTrue(result.getOutput().contains("events=global:browserMain|layer:browserMain"));
243 243 assertTrue(result.getOutput().contains("lateReplay=ok"));
244 244 }
245 245
246 246 @Test
247 247 void supportsSourceSetNamePatternAndSharedRegistration() throws Exception {
248 248 writeFile(SETTINGS_FILE, ROOT_NAME);
249 249 writeFile(BUILD_FILE, """
250 250 plugins {
251 251 id 'org.implab.gradle-variants-sources'
252 252 }
253 253
254 254 variants {
255 255 layer('main')
256 256
257 257 variant('browser') {
258 258 role('main') { layers('main') }
259 259 }
260 260
261 261 variant('node') {
262 262 role('main') { layers('main') }
263 263 }
264 264 }
265 265
266 266 def registeredEvents = []
267 267 def browserRegisteredEvents = []
268 268 def boundEvents = []
269 269 def browserBoundEvents = []
270 270 def localBoundEvents = []
271 271
272 272 variantSources {
273 273 bind('main').sourceSetNamePattern = '{layer}'
274 274
275 275 bind('main') {
276 276 configureSourceSet {
277 277 declareOutputs('compiled')
278 278 }
279 279 }
280 280
281 281 bind('main') {
282 282 whenBound {
283 283 localBoundEvents << "${variantName()}:${roleName()}:${layerName()}:${sourceSetName()}"
284 284 }
285 285 }
286 286
287 287 whenRegistered { ctx ->
288 288 registeredEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
289 289 }
290 290
291 291 whenRegistered('browser') { ctx ->
292 292 browserRegisteredEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
293 293 }
294 294
295 295 whenBound { ctx ->
296 296 boundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
297 297 }
298 298
299 299 whenBound('browser') { ctx ->
300 300 browserBoundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
301 301 }
302 302 }
303 303
304 304 tasks.register('probe') {
305 305 doLast {
306 306 println("sources=" + sources.collect { it.name }.sort().join(','))
307 307
308 308 def main = sources.getByName('main')
309 309 main.output('compiled')
310 310
311 311 println("registered=" + registeredEvents.sort().join('|'))
312 312 println("browserRegistered=" + browserRegisteredEvents.sort().join('|'))
313 313 println("bound=" + boundEvents.sort().join('|'))
314 314 println("browserBound=" + browserBoundEvents.sort().join('|'))
315 315 println("localBound=" + localBoundEvents.sort().join('|'))
316 316 println('sharedPattern=ok')
317 317 }
318 318 }
319 319 """);
320 320
321 321 BuildResult result = runner("probe").build();
322 322 assertTrue(result.getOutput().contains("sources=main"));
323 323 assertTrue(result.getOutput().contains("registered=browser:main:main:main"));
324 324 assertTrue(result.getOutput().contains("browserRegistered=browser:main:main:main"));
325 325 assertTrue(result.getOutput().contains("bound=browser:main:main:main|node:main:main:main"));
326 326 assertTrue(result.getOutput().contains("browserBound=browser:main:main:main"));
327 327 assertTrue(result.getOutput().contains("localBound=browser:main:main:main|node:main:main:main"));
328 328 assertTrue(result.getOutput().contains("sharedPattern=ok"));
329 329 }
330 330
331 331 private GradleRunner runner(String... arguments) {
332 332 return GradleRunner.create()
333 333 .withProjectDir(testProjectDir.toFile())
334 334 .withPluginClasspath(pluginClasspath())
335 335 .withArguments(arguments)
336 336 .forwardOutput();
337 337 }
338 338
339 339 private static List<File> pluginClasspath() {
340 340 try {
341 341 var classesDir = Path.of(VariantsSourcesPlugin.class
342 342 .getProtectionDomain()
343 343 .getCodeSource()
344 344 .getLocation()
345 345 .toURI());
346 346
347 347 var markerResource = VariantsSourcesPlugin.class.getClassLoader()
348 348 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties");
349 349
350 350 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
351 351
352 352 var markerPath = Path.of(markerResource.toURI());
353 353 var resourcesDir = markerPath.getParent().getParent().getParent();
354 354
355 355 return List.of(classesDir.toFile(), resourcesDir.toFile());
356 356 } catch (Exception e) {
357 357 throw new RuntimeException("Unable to build plugin classpath for test", e);
358 358 }
359 359 }
360 360
361 361 private void writeFile(String relativePath, String content) throws IOException {
362 362 Path path = testProjectDir.resolve(relativePath);
363 363 Files.createDirectories(path.getParent());
364 364 Files.writeString(path, content);
365 365 }
366 366 }
@@ -1,145 +1,145
1 1 # Variant Sources Plugin
2 2
3 3 ## NAME
4 4
5 5 `VariantsSourcesPlugin` и extension `variantSources`.
6 6
7 7 ## SYNOPSIS
8 8
9 9 ```groovy
10 10 plugins {
11 11 id 'org.implab.gradle-variants-sources'
12 12 }
13 13
14 14 variants {
15 15 layer('main')
16 16
17 17 variant('browser') {
18 18 role('main') { layers('main') }
19 19 }
20 20
21 21 variant('node') {
22 22 role('main') { layers('main') }
23 23 }
24 24 }
25 25
26 26 variantSources {
27 27 bind('main').sourceSetNamePattern = '{layer}'
28 28
29 29 bind('main') {
30 30 configureSourceSet {
31 31 declareOutputs('compiled')
32 32 }
33 33 }
34 34
35 35 whenRegistered { sourceSetName() }
36 36 whenBound('browser') { roleName() }
37 37 }
38 38 ```
39 39
40 40 ## DESCRIPTION
41 41
42 42 `VariantsSourcesPlugin` применяет `VariantsPlugin` и `SourcesPlugin`, затем
43 материализует source sets из модели `variants`.
43 регистрирует source sets из модели `variants`.
44 44
45 Точка запуска materialization:
45 Точка запуска registration:
46 46
47 47 - `variants.whenFinalized(model -> registerSourceSets(...))`
48 48
49 ### materialization
49 ### registration
50 50
51 51 Для каждой usage-связки `variant/role/layer` вычисляется имя source set,
52 52 регистрируется `GenericSourceSet` (если он еще не существует), затем
53 53 вызываются callbacks.
54 54
55 55 ### binding
56 56
57 57 `bind('<layer>')` возвращает `BuildLayerBinding` и задает policy для этого
58 58 слоя:
59 59
60 60 - как именовать source set;
61 61 - как конфигурировать source set;
62 62 - какие callbacks вызвать на registration/binding.
63 63
64 64 ### sourceSetNamePattern
65 65
66 `sourceSetNamePattern` определяет naming policy materialized source set.
66 `sourceSetNamePattern` определяет naming policy зарегистрированного source set.
67 67
68 68 Default:
69 69
70 70 - `{variant}{layerCap}`
71 71
72 72 Tokens:
73 73
74 74 - `{variant}`, `{variantCap}`
75 75 - `{role}`, `{roleCap}`
76 76 - `{layer}`, `{layerCap}`
77 77
78 78 Имя санитизируется (`[^A-Za-z0-9_.-] -> _`).
79 79
80 80 Ограничение:
81 81
82 82 - один `sourceSetName` не может быть порожден разными слоями.
83 83
84 84 ## EVENTS
85 85
86 86 ### whenRegistered
87 87
88 88 - callback на новый уникальный source set;
89 89 - replayable;
90 90 - при shared source set срабатывает один раз.
91 91
92 92 ### whenBound
93 93
94 94 - callback на каждую usage-связку `variant/role/layer`;
95 95 - replayable;
96 96 - подходит для per-usage логики.
97 97
98 98 ### variant filter
99 99
100 100 Глобальные callbacks поддерживают фильтр по варианту:
101 101
102 102 - `whenRegistered(String variantName, ...)`
103 103 - `whenBound(String variantName, ...)`
104 104
105 105 ## SOURCE SET CONTEXT
106 106
107 107 `SourceSetContext` содержит:
108 108
109 109 - `variantName`, `roleName`, `layerName`, `sourceSetName`;
110 110 - `sourceSet` (`NamedDomainObjectProvider<GenericSourceSet>`).
111 111
112 112 Sugar:
113 113
114 114 - `configureSourceSet(Action|Closure)`.
115 115
116 116 ## API
117 117
118 118 ### VariantSourcesExtension
119 119
120 120 - `bind(String)` — получить/создать binding по имени слоя.
121 121 - `bind(String, Action|Closure)` — сконфигурировать binding.
122 122 - `bindings(Action|Closure)` — контейнерная конфигурация bindings.
123 123 - `whenRegistered(...)` — глобальные callbacks регистрации source set.
124 124 - `whenBound(...)` — глобальные callbacks usage-binding.
125 125
126 126 ### BuildLayerBinding
127 127
128 128 - `sourceSetNamePattern` — naming policy для source set слоя.
129 129 - `configureSourceSet(...)` — слойная конфигурация `GenericSourceSet`.
130 130 - `whenRegistered(...)` — callbacks регистрации в рамках слоя.
131 131 - `whenBound(...)` — callbacks usage-binding в рамках слоя.
132 132
133 133 ## KEY CLASSES
134 134
135 135 - `VariantsSourcesPlugin` — точка входа plugin adapter.
136 136 - `VariantSourcesExtension` — глобальный DSL bind/events.
137 137 - `BuildLayerBinding` — layer-local policy и callbacks.
138 138 - `SourceSetContext` — payload callbacks и sugar-конфигурирование.
139 139
140 140 ## NOTES
141 141
142 - `sourceSetNamePattern` фиксируется при первом чтении в materialization
142 - `sourceSetNamePattern` фиксируется при первом чтении в registration
143 143 (`finalizeValueOnRead`).
144 144 - Closure callbacks используют delegate-first.
145 145 - Для вложенных closure лучше явный параметр (`ctx -> ...`).
@@ -1,127 +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 - после `finalizeModel()` мутации модели запрещены.
91 92 - `whenFinalized(...)` replayable.
92 93
93 94 ## API
94 95
95 96 ### BuildVariantsExtension
96 97
97 98 - `layer(...)` — объявление или конфигурация `BuildLayer`.
98 99 - `variant(...)` — объявление или конфигурация `BuildVariant`.
99 100 - `layers { ... }`, `variants { ... }` — контейнерный DSL.
100 101 - `all(...)` — callback для всех вариантов.
101 102 - `getAll()`, `getByName(name)` — доступ к вариантам.
102 103 - `validate()` — явный запуск валидации.
103 104 - `finalizeModel()` — валидация + финализация модели.
104 105 - `whenFinalized(...)` — callback по завершенной модели (replayable).
105 106
106 107 ### BuildVariant
107 108
108 109 - `attributes { ... }` — атрибуты варианта (+ sugar `string/bool/integer`).
109 110 - `role(...)`, `roles { ... }` — роли варианта.
110 111 - `link(...)`, `links { ... }` — связи слоев внутри варианта.
111 112 - `artifactSlot(...)`, `artifactSlots { ... }` — артефактные слоты.
112 113
113 114 ## KEY CLASSES
114 115
115 116 - `VariantsPlugin` — точка входа плагина.
116 117 - `BuildVariantsExtension` — root extension и lifecycle.
117 118 - `BuildVariant` — агрегатная модель варианта.
118 119 - `BuildLayer` — модель слоя.
119 120 - `BuildRole` — модель роли.
120 - `BuildLink` — модель направленной связи.
121 - `LayerLink` — модель направленной связи.
121 122 - `BuildArtifactSlot` — модель артефактного слота.
122 123 - `VariantAttributes` — typed wrapper для variant attributes.
123 124
124 125 ## NOTES
125 126
126 127 - Модель `variants` intentionally agnostic к toolchain.
127 128 - Интеграция с задачами выполняется через `variantSources` и адаптеры.
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now