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