##// END OF EJS Templates
Removed layer links from variants
cin -
r30:2dd3356774b2 default
parent child
Show More
@@ -18,7 +18,6 variants {
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
@@ -54,13 +53,12 variantSources {
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
@@ -90,9 +88,8 outputs. Это уже "физический" уровень, к которому удобно привязывать задачи,
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.
@@ -115,7 +112,7 Closure callbacks работают в delegate-first режиме (`@DelegatesTo`). Для
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.
@@ -10,6 +10,8 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
@@ -43,6 +45,15 public class Strings {
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;
@@ -1,10 +1,8
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;
@@ -30,7 +28,6 public abstract class BuildVariant imple
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
@@ -110,29 +107,6 public abstract class BuildVariant imple
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 }
@@ -196,13 +170,6 public abstract class BuildVariant imple
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);
@@ -229,16 +196,6 public abstract class BuildVariant imple
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);
@@ -3,14 +3,11 package org.implab.gradle.common.sources
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
@@ -197,8 +194,7 public abstract class BuildVariantsExten
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) {
@@ -227,10 +223,8 public abstract class BuildVariantsExten
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())) {
@@ -249,88 +243,9 public abstract class BuildVariantsExten
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 }
254
255 variantLayers.add(normalizedLayerName);
256 }
257 }
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 246 }
304 247 }
305 248 }
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) {
@@ -341,10 +256,6 public abstract class BuildVariantsExten
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);
@@ -12,6 +12,7 import javax.inject.Inject;
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;
@@ -23,16 +24,16 import org.gradle.api.logging.Logging;
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<>();
@@ -44,7 +45,6 public abstract class VariantSourcesExte
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
@@ -62,7 +62,7 public abstract class VariantSourcesExte
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 /**
@@ -173,7 +173,7 public abstract class VariantSourcesExte
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) {
@@ -255,7 +255,7 public abstract class VariantSourcesExte
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");
@@ -278,26 +278,18 public abstract class VariantSourcesExte
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();
@@ -48,7 +48,6 class VariantsPluginFunctionalTest {
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 }
@@ -58,7 +57,6 class VariantsPluginFunctionalTest {
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 }
@@ -68,7 +66,6 class VariantsPluginFunctionalTest {
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 }
@@ -94,28 +91,6 class VariantsPluginFunctionalTest {
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, """
@@ -139,91 +114,6 class VariantsPluginFunctionalTest {
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 {
@@ -25,7 +25,6 variants {
25 25 layers('mainBase', 'mainAmd')
26 26 }
27 27
28 link('mainBase', 'mainAmd', 'ts:api')
29 28 artifactSlot('mainCompiled')
30 29 }
31 30 }
@@ -39,31 +38,18 variants {
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>`) для передачи параметров в
@@ -80,10 +66,8 Typed-атрибуты (`Attribute<T> -> Provider<T>`) для передачи параметров в
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
@@ -108,7 +92,6 Typed-атрибуты (`Attribute<T> -> Provider<T>`) для передачи параметров в
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
@@ -118,7 +101,6 Typed-атрибуты (`Attribute<T> -> Provider<T>`) для передачи параметров в
118 101 - `BuildVariant` — агрегатная модель варианта.
119 102 - `BuildLayer` — модель слоя.
120 103 - `BuildRole` — модель роли.
121 - `LayerLink` — модель направленной связи.
122 104 - `BuildArtifactSlot` — модель артефактного слота.
123 105 - `VariantAttributes` — typed wrapper для variant attributes.
124 106
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now