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