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