##// END OF EJS Templates
Working on separating variants as standalone plugin
cin -
r38:87d6128f0bc8 default
parent child
Show More
@@ -0,0 +1,41
1 package org.implab.gradle.common.sources;
2
3 import org.implab.gradle.common.core.lang.Closures;
4 import org.gradle.api.Action;
5 import org.gradle.api.Named;
6 import org.gradle.api.provider.Property;
7
8 import groovy.lang.Closure;
9 import groovy.lang.DelegatesTo;
10
11 /**
12 * Public DSL contract for per-layer source-set policy and callbacks.
13 */
14 public interface LayerBindingSpec extends Named {
15 Property<String> getSourceSetNamePattern();
16
17 default void setSourceSetNamePattern(String pattern) {
18 getSourceSetNamePattern().set(pattern);
19 }
20
21 void configureSourceSet(Action<? super GenericSourceSet> configure);
22
23 default void configureSourceSet(
24 @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
25 configureSourceSet(Closures.action(configure));
26 }
27
28 void whenRegistered(Action<? super SourceSetRegistration> action);
29
30 default void whenRegistered(
31 @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
32 whenRegistered(Closures.action(action));
33 }
34
35 void whenBound(Action<? super SourceSetUsageBinding> action);
36
37 default void whenBound(
38 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
39 whenBound(Closures.action(action));
40 }
41 }
@@ -0,0 +1,53
1 plugins {
2 id "java-library"
3 id "ivy-publish"
4 }
5
6 java {
7 withJavadocJar()
8 withSourcesJar()
9 toolchain {
10 languageVersion = JavaLanguageVersion.of(21)
11 }
12 }
13
14 dependencies {
15 compileOnly libs.jdt.annotations
16
17 api gradleApi(),
18 libs.bundles.jackson
19
20 implementation project(":common")
21
22 testImplementation gradleTestKit()
23 testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.4"
24 testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.4"
25 testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.4"
26 }
27
28 task printVersion{
29 doLast {
30 println "project: $project.group:$project.name:$project.version"
31 println "jar: ${->jar.archiveFileName.get()}"
32 }
33 }
34
35 test {
36 useJUnitPlatform()
37 }
38
39 publishing {
40 repositories {
41 ivy {
42 url "${System.properties["user.home"]}/ivy-repo"
43 }
44 }
45 publications {
46 ivy(IvyPublication) {
47 from components.java
48 descriptor.description {
49 text = providers.provider({ description })
50 }
51 }
52 }
53 }
@@ -0,0 +1,17
1 package org.implab.gradle.variants;
2
3 import org.gradle.api.Plugin;
4 import org.gradle.api.Project;
5 import org.implab.gradle.variants.model.VariantsExtension;
6
7 public abstract class VariantsPlugin implements Plugin<Project> {
8 @Override
9 public void apply(Project target) {
10 var extension = target.getExtensions().create("variants", VariantsExtension.class);
11
12 target.afterEvaluate(project -> {
13
14 });
15
16 }
17 }
@@ -0,0 +1,9
1 package org.implab.gradle.variants.model;
2
3 import org.gradle.api.Named;
4
5 /**
6 * Identity-only domain object.
7 */
8 public interface Layer extends Named {
9 } No newline at end of file
@@ -0,0 +1,9
1 package org.implab.gradle.variants.model;
2
3 import org.gradle.api.Named;
4
5 /**
6 * Identity-only domain object.
7 */
8 public interface Role extends Named {
9 } No newline at end of file
@@ -0,0 +1,35
1 package org.implab.gradle.variants.model;
2
3 import org.gradle.api.Named;
4 import org.gradle.api.provider.SetProperty;
5
6 /**
7 * Binds a role to a set of layers inside a particular variant.
8 *
9 * The binding name is the role name, e.g. "production", "test", "tool".
10 */
11 public interface RoleBinding extends Named {
12
13 /**
14 * Layer names participating in this (variant, role) selection.
15 *
16 * Core model keeps names here deliberately:
17 * source/materialization semantics live elsewhere.
18 */
19 SetProperty<String> getLayerNames();
20
21 /**
22 * Adds one layer to this binding.
23 */
24 void layer(String name);
25
26 /**
27 * Adds several layers to this binding.
28 */
29 void layers(String... names);
30
31 /**
32 * Adds several layers to this binding.
33 */
34 void layers(Iterable<String> names);
35 } No newline at end of file
@@ -0,0 +1,44
1 package org.implab.gradle.variants.model;
2
3 import org.gradle.api.Action;
4 import org.gradle.api.Named;
5 import org.gradle.api.NamedDomainObjectContainer;
6 import org.implab.gradle.common.core.lang.Closures;
7
8 import groovy.lang.Closure;
9
10 /**
11 * A named variant, e.g. "browser", "electron".
12 *
13 * A variant does not "have a role" directly.
14 * It owns a set of role bindings.
15 */
16 public interface Variant extends Named {
17
18 /**
19 * Role bindings declared inside this variant.
20 *
21 * The binding name is the role name.
22 */
23 NamedDomainObjectContainer<RoleBinding> getRoleBindings();
24
25 /**
26 * Creates or returns an existing role binding and configures it.
27 */
28 default RoleBinding role(String name) {
29 return getRoleBindings().maybeCreate(name);
30 }
31
32 /**
33 * Creates or returns an existing role binding and configures it.
34 */
35 default RoleBinding role(String name, Action<? super RoleBinding> action) {
36 var role = role(name);
37 action.execute(role);
38 return role;
39 }
40
41 default RoleBinding role(String name, Closure<?> closure) {
42 return role(name, Closures.action(closure));
43 }
44 } No newline at end of file
@@ -0,0 +1,59
1 package org.implab.gradle.variants.model;
2
3 import org.gradle.api.Action;
4 import org.gradle.api.NamedDomainObjectContainer;
5 import org.implab.gradle.common.core.lang.Closures;
6
7 import groovy.lang.Closure;
8
9 /**
10 * Root extension:
11 *
12 * variants {
13 * layers { ... }
14 * roles { ... }
15 *
16 * variant("browser") {
17 * role("production") {
18 * layers("main", "generated", "mainRjs")
19 * }
20 * }
21 * }
22 */
23 public interface VariantsExtension {
24
25 /**
26 * Domain of layers.
27 */
28 NamedDomainObjectContainer<Layer> getLayers();
29
30 /**
31 * Domain of roles.
32 */
33 NamedDomainObjectContainer<Role> getRoles();
34
35 /**
36 * Declared variants.
37 */
38 NamedDomainObjectContainer<Variant> getVariantDefinitions();
39
40 /**
41 * Creates or returns an existing variant and configures it.
42 */
43 default Variant variant(String name) {
44 return getVariantDefinitions().maybeCreate(name);
45 }
46
47 /**
48 * Creates or returns an existing variant and configures it.
49 */
50 default Variant variant(String name, Action<? super Variant> action) {
51 var variant = variant(name);
52 action.execute(variant);
53 return variant;
54 }
55
56 default Variant variant(String name, Closure<?> closure) {
57 return variant(name, Closures.action(closure));
58 }
59 } No newline at end of file
@@ -1,134 +1,134
1 1 # Gradle Common Sources Model
2 2
3 3 ## NAME
4 4
5 5 `gradle-common/common` — набор плагинов для моделирования вариантов сборки,
6 6 регистрации source sets и интеграции этой модели с toolchain-адаптерами.
7 7
8 8 ## SYNOPSIS
9 9
10 10 ```groovy
11 11 plugins {
12 12 id 'org.implab.gradle-variants-sources'
13 13 }
14 14
15 15 variants {
16 16 layer('mainBase')
17 17 layer('mainAmd')
18 18
19 19 variant('browser') {
20 20 role('main') { layers('mainBase', 'mainAmd') }
21 21 }
22 22 }
23 23
24 24 variantSources {
25 25 bind('mainBase') {
26 26 configureSourceSet {
27 27 declareOutputs('compiled')
28 28 }
29 29 }
30 30
31 31 bind('mainAmd').sourceSetNamePattern = '{variant}{layerCap}'
32 32
33 33 whenRegistered { sourceSetName() }
34 34
35 35 whenBound { ctx ->
36 36 ctx.configureSourceSet {
37 37 declareOutputs('typings')
38 38 }
39 39 }
40 40 }
41 41 ```
42 42
43 43 ## DESCRIPTION
44 44
45 45 Модуль состоит из трех логических частей:
46 46
47 47 - `variants` — декларативная доменная модель сборки;
48 48 - `sources` — модель физически регистрируемых source sets;
49 49 - `variantSources` — адаптер, который связывает первые две модели.
50 50
51 51 Ниже раскрытие каждой части.
52 52
53 53 ### variants
54 54
55 55 `variants` задает структуру пространства сборки: какие есть слои, какие роли
56 56 используют эти слои в каждом варианте, какие есть атрибуты и artifact slots.
57 57 Модель не создает задачи и не привязана к TS/JS.
58 58
59 59 Практический смысл:
60 60
61 61 - формализовать архитектуру сборки;
62 62 - дать адаптерам единый источник правды.
63 63
64 64 ### sources
65 65
66 66 `sources` описывает независимые source sets (`GenericSourceSet`) с именованными
67 67 outputs. Это уже "физический" уровень, к которому удобно привязывать задачи,
68 68 артефакты и task inputs/outputs.
69 69
70 70 Практический смысл:
71 71
72 72 - создать единый контракт по входам/выходам;
73 73 - регистрировать результаты задач как outputs source set;
74 74 - минимизировать ручные `dependsOn` за счет модели outputs.
75 75
76 76 ### variantSources
77 77
78 78 `variantSources` регистрирует source sets на основе `variants`, применяет
79 79 конфигурацию layer-bindings и отдает события (`whenRegistered`, `whenBound`) для
80 80 адаптеров других плагинов.
81 81
82 82 Практический смысл:
83 83
84 84 - переводить логическую модель `variants` в executable-модель `sources`;
85 85 - навешивать политики toolchain на зарегистрированные source sets;
86 86 - синхронизировать плагины через replayable callback-контракт.
87 87
88 88 ## DOMAIN MODEL
89 89
90 - `BuildLayer` — глобальный идентификатор слоя.
90 - `BuildLayer` — canonical identity-model объявленного слоя.
91 91 - `BuildVariant` — агрегат ролей, атрибутов, артефактных слотов.
92 - `BuildRole` — роль внутри варианта, содержит ссылки на layer names.
92 - `BuildRole` — роль внутри варианта, содержит ссылки на declared layer names.
93 93 - `GenericSourceSet` — зарегистрированный набор исходников и outputs.
94 - `BuildLayerBinding` — правила registration source set для конкретного layer.
94 - `LayerBindingSpec` — публичный DSL-contract adapter policy/callbacks для слоя.
95 95 - `SourceSetRegistration` — payload события регистрации source set.
96 96 - `SourceSetUsageBinding` — payload события usage-binding.
97 97
98 98 ## EVENT CONTRACT
99 99
100 100 - `whenRegistered`:
101 101 - событие нового уникального source set name;
102 102 - replayable.
103 103 - `whenBound`:
104 104 - событие каждой usage-связки `variant/role/layer`;
105 105 - replayable.
106 106
107 107 Closure callbacks работают в delegate-first режиме (`@DelegatesTo`). Для
108 108 вложенных closure рекомендуется явный параметр (`ctx -> ...`).
109 109
110 110 ## KEY CLASSES
111 111
112 112 - `SourcesPlugin` — регистрирует extension `sources`.
113 113 - `GenericSourceSet` — модель источников/outputs для конкретного имени.
114 114 - `VariantsPlugin` — регистрирует extension `variants` и lifecycle finalize.
115 115 - `BuildVariantsExtension` — корневой API модели вариантов.
116 116 - `BuildVariant` — API ролей, attributes и artifact slots варианта.
117 117 - `VariantsSourcesPlugin` — применяет `variants` + `sources` и запускает адаптер.
118 118 - `VariantSourcesExtension` — API bind/events registration.
119 - `BuildLayerBinding` — слой-конкретный DSL для имени и конфигурации source set.
119 - `LayerBindingSpec` — слой-конкретный DSL для policy/configuration source set.
120 120 - `SourceSetRegistration` — payload `whenRegistered(...)`.
121 121 - `SourceSetUsageBinding` — payload `whenBound(...)`.
122 122
123 123 ## NOTES
124 124
125 125 - Marker ids:
126 126 - `org.implab.gradle-variants`
127 127 - `org.implab.gradle-variants-sources`
128 128 - `SourcesPlugin` пока class-only (без marker id).
129 129
130 130 ## SEE ALSO
131 131
132 132 - `sources-plugin.md`
133 133 - `variants-plugin.md`
134 134 - `variant-sources-plugin.md`
@@ -1,251 +1,235
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.Collection;
5 5 import java.util.Collections;
6 6 import java.util.LinkedHashMap;
7 7 import java.util.LinkedHashSet;
8 8 import java.util.List;
9 9 import java.util.Map;
10 10 import java.util.Optional;
11 11
12 12 import javax.inject.Inject;
13 13
14 14 import org.implab.gradle.common.core.lang.Closures;
15 15 import org.gradle.api.Action;
16 16 import org.gradle.api.InvalidUserDataException;
17 17 import org.gradle.api.NamedDomainObjectContainer;
18 18 import org.gradle.api.model.ObjectFactory;
19 19
20 20 import groovy.lang.Closure;
21 21
22 22 public abstract class BuildVariantsExtension {
23 private final NamedDomainObjectContainer<BuildLayer> layers;
23 private final ObjectFactory objects;
24 private final LinkedHashMap<String, LayoutLayer> layersByName = new LinkedHashMap<>();
24 25 private final NamedDomainObjectContainer<BuildVariant> variants;
25 26 private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
26 27 private boolean finalized;
27 28
28 29 @Inject
29 30 public BuildVariantsExtension(ObjectFactory objects) {
30 layers = objects.domainObjectContainer(BuildLayer.class);
31 this.objects = objects;
31 32 variants = objects.domainObjectContainer(BuildVariant.class);
32 33
33 layers.all(layer -> {
34 if (finalized)
35 throw new InvalidUserDataException(
36 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
37 });
38
39 34 variants.all(variant -> {
40 35 if (finalized)
41 36 throw new InvalidUserDataException(
42 37 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
43 38 });
44 39 }
45 40
46 public NamedDomainObjectContainer<BuildLayer> getLayers() {
47 return layers;
41 public Collection<LayoutLayer> getLayers() {
42 return Collections.unmodifiableCollection(layersByName.values());
48 43 }
49 44
50 45 public NamedDomainObjectContainer<BuildVariant> getVariants() {
51 46 return variants;
52 47 }
53 48
54 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
49 public LayoutLayer layer(String name, Action<? super LayoutLayer> configure) {
55 50 ensureMutable("configure layers");
56 action.execute(layers);
57 }
58
59 public void layers(Closure<?> configure) {
60 layers(Closures.action(configure));
61 }
62
63 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
64 ensureMutable("configure variants");
65 action.execute(variants);
66 }
67
68 public void variants(Closure<?> configure) {
69 variants(Closures.action(configure));
70 }
71
72 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
73 ensureMutable("configure layers");
74 var layer = layers.maybeCreate(name);
51 var layer = layersByName.computeIfAbsent(requireName(name, "Layer name must not be null or blank"), this::newLayer);
75 52 configure.execute(layer);
76 53 return layer;
77 54 }
78 55
79 public BuildLayer layer(String name, Closure<?> configure) {
56 public LayoutLayer layer(String name, Closure<?> configure) {
80 57 return layer(name, Closures.action(configure));
81 58 }
82 59
83 public BuildLayer layer(String name) {
60 public LayoutLayer layer(String name) {
84 61 return layer(name, it -> {
85 62 });
86 63 }
87 64
88 65 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
89 66 ensureMutable("configure variants");
90 67 var variant = variants.maybeCreate(name);
91 68 configure.execute(variant);
92 69 return variant;
93 70 }
94 71
95 72 public BuildVariant variant(String name, Closure<?> configure) {
96 73 return variant(name, Closures.action(configure));
97 74 }
98 75
99 76 public BuildVariant variant(String name) {
100 77 return variant(name, it -> {
101 78 });
102 79 }
103 80
104 81 public void all(Action<? super BuildVariant> action) {
105 82 variants.all(action);
106 83 }
107 84
108 85 public void all(Closure<?> configure) {
109 86 all(Closures.action(configure));
110 87 }
111 88
112 89 public Collection<BuildVariant> getAll() {
113 90 var all = new ArrayList<BuildVariant>();
114 91 variants.forEach(all::add);
115 92 return Collections.unmodifiableList(all);
116 93 }
117 94
95 public Optional<LayoutLayer> findLayer(String name) {
96 var normalizedName = normalize(name);
97 return normalizedName == null ? Optional.empty() : Optional.ofNullable(layersByName.get(normalizedName));
98 }
99
100 public LayoutLayer requireLayer(String name) {
101 return findLayer(name)
102 .orElseThrow(() -> new InvalidUserDataException("Layer '" + name + "' isn't defined"));
103 }
104
118 105 public Optional<BuildVariant> find(String name) {
119 106 return Optional.ofNullable(variants.findByName(name));
120 107 }
121 108
122 109 public BuildVariant require(String name) {
123 110 return find(name)
124 111 .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't defined"));
125 112 }
126 113
127 114 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
128 115 if (finalized) {
129 116 action.execute(this);
130 117 return;
131 118 }
132 119 finalizedActions.add(action);
133 120 }
134 121
135 122 public void whenFinalized(Closure<?> configure) {
136 123 whenFinalized(Closures.action(configure));
137 124 }
138 125
139 126 public boolean isFinalized() {
140 127 return finalized;
141 128 }
142 129
143 130 public void finalizeModel() {
144 131 if (finalized)
145 132 return;
146 133
147 134 validate();
148 135
149 136 for (var variant : variants)
150 137 variant.finalizeModel();
151 138
152 139 finalized = true;
153 140
154 141 var actions = new ArrayList<>(finalizedActions);
155 142 finalizedActions.clear();
156 143 for (var action : actions)
157 144 action.execute(this);
158 145 }
159 146
160 147 public void validate() {
161 148 var errors = new ArrayList<String>();
162 149
163 var layersByName = new LinkedHashMap<String, BuildLayer>();
164 for (var layer : layers) {
165 var layerName = normalize(layer.getName());
166 if (layerName == null) {
167 errors.add("Layer name must not be blank");
168 continue;
169 }
170
171 var previous = layersByName.putIfAbsent(layerName, layer);
172 if (previous != null) {
173 errors.add("Layer '" + layerName + "' is declared more than once");
174 }
175 }
176
177 150 for (var variant : variants)
178 151 validateVariant(variant, layersByName, errors);
179 152
180 153 if (!errors.isEmpty()) {
181 154 var message = new StringBuilder("Invalid variants model:");
182 155 for (var error : errors)
183 156 message.append("\n - ").append(error);
184 157
185 158 throw new InvalidUserDataException(message.toString());
186 159 }
187 160 }
188 161
189 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
162 private static void validateVariant(BuildVariant variant, Map<String, LayoutLayer> layersByName, List<String> errors) {
190 163 var variantName = normalize(variant.getName());
191 164 if (variantName == null) {
192 165 errors.add("Variant name must not be blank");
193 166 return;
194 167 }
195 168
196 169 validateRoleNames(variant, errors);
197 170 validateRoleMappings(variant, layersByName, errors);
198 171 }
199 172
200 173 private static void validateRoleNames(BuildVariant variant, List<String> errors) {
201 174 var roleNames = new LinkedHashSet<String>();
202 175 for (var role : variant.getRoles()) {
203 176 var roleName = normalize(role.getName());
204 177 if (roleName == null) {
205 178 errors.add("Variant '" + variant.getName() + "' contains blank role name");
206 179 continue;
207 180 }
208 181 if (!roleNames.add(roleName)) {
209 182 errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
210 183 }
211 184 }
212 185 }
213 186
214 private static void validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
187 private static void validateRoleMappings(BuildVariant variant, Map<String, LayoutLayer> layersByName,
215 188 List<String> errors) {
216 189 for (var role : variant.getRoles()) {
217 190 var seenLayers = new LinkedHashSet<String>();
218 191 for (var layerName : role.getLayers().getOrElse(List.of())) {
219 192 var normalizedLayerName = normalize(layerName);
220 193 if (normalizedLayerName == null) {
221 194 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
222 195 continue;
223 196 }
224 197
225 198 var layer = layersByName.get(normalizedLayerName);
226 199 if (layer == null) {
227 200 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
228 201 continue;
229 202 }
230 203
231 204 if (!seenLayers.add(normalizedLayerName)) {
232 205 errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
233 206 + "' contains duplicated layer reference '" + normalizedLayerName + "'");
234 207 }
235 208 }
236 209 }
237 210 }
238 211
239 212 private static String normalize(String value) {
240 213 if (value == null)
241 214 return null;
242 215
243 216 var trimmed = value.trim();
244 217 return trimmed.isEmpty() ? null : trimmed;
245 218 }
246 219
220 private static String requireName(String value, String errorMessage) {
221 var normalized = normalize(value);
222 if (normalized == null)
223 throw new InvalidUserDataException(errorMessage);
224 return normalized;
225 }
226
227 private LayoutLayer newLayer(String name) {
228 return objects.newInstance(LayoutLayer.class, name);
229 }
230
247 231 private void ensureMutable(String operation) {
248 232 if (finalized)
249 233 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
250 234 }
251 235 }
@@ -1,120 +1,83
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.LinkedHashSet;
5 5 import java.util.List;
6 6 import java.util.Set;
7 7
8 import javax.inject.Inject;
9
10 import org.implab.gradle.common.core.lang.Closures;
11 8 import org.gradle.api.Action;
12 import org.gradle.api.Named;
13 9 import org.gradle.api.NamedDomainObjectProvider;
10 import org.gradle.api.model.ObjectFactory;
14 11 import org.gradle.api.provider.Property;
15 12
16 import groovy.lang.Closure;
17 import groovy.lang.DelegatesTo;
18
19 /**
20 * Maps a logical layer to per-source-set hooks.
21 */
22 public abstract class BuildLayerBinding implements Named {
23 public static final String DEFAULT_SOURCE_SET_NAME_PATTERN = "{variant}{layerCap}";
13 final class LayerBinding implements LayerBindingSpec {
14 static final String DEFAULT_SOURCE_SET_NAME_PATTERN = "{variant}{layerCap}";
24 15
25 16 private final String name;
17 private final Property<String> sourceSetNamePattern;
26 18
27 19 private final List<Action<? super GenericSourceSet>> sourceSetConfigureActions = new ArrayList<>();
28 20 private final List<Action<? super SourceSetRegistration>> registeredActions = new ArrayList<>();
29 21 private final List<Action<? super SourceSetUsageBinding>> boundActions = new ArrayList<>();
30 22 private final List<NamedDomainObjectProvider<GenericSourceSet>> registeredSourceSets = new ArrayList<>();
31 23 private final List<SourceSetRegistration> registeredContexts = new ArrayList<>();
32 24 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
33 25 private final Set<String> registeredSourceSetNames = new LinkedHashSet<>();
34 26
35 @Inject
36 public BuildLayerBinding(String name) {
27 LayerBinding(String name, ObjectFactory objects) {
37 28 this.name = name;
38 getSourceSetNamePattern().convention(DEFAULT_SOURCE_SET_NAME_PATTERN);
29 sourceSetNamePattern = objects.property(String.class);
30 sourceSetNamePattern.convention(DEFAULT_SOURCE_SET_NAME_PATTERN);
39 31 }
40 32
41 33 @Override
42 34 public String getName() {
43 35 return name;
44 36 }
45 37
46 public abstract Property<String> getSourceSetNamePattern();
38 @Override
39 public Property<String> getSourceSetNamePattern() {
40 return sourceSetNamePattern;
41 }
47 42
48 /**
49 * Action applied to every registered source set for this layer.
50 * Already registered source sets are configured immediately (replay).
51 */
43 @Override
52 44 public void configureSourceSet(Action<? super GenericSourceSet> configure) {
53 45 sourceSetConfigureActions.add(configure);
54 46 for (var sourceSet : registeredSourceSets)
55 47 sourceSet.configure(configure);
56 48 }
57 49
58 public void configureSourceSet(
59 @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
60 configureSourceSet(Closures.action(configure));
61 }
62
63 /**
64 * Layer-local callback fired after source-set registration.
65 * Already emitted registrations are delivered immediately (replay).
66 * For simple callbacks you can use delegate-only style
67 * (for example {@code whenRegistered { sourceSetName() }}).
68 * For nested closures prefer explicit parameter
69 * ({@code whenRegistered { ctx -> ... }}).
70 */
50 @Override
71 51 public void whenRegistered(Action<? super SourceSetRegistration> action) {
72 52 registeredActions.add(action);
73 53 for (var context : registeredContexts)
74 54 action.execute(context);
75 55 }
76 56
77 public void whenRegistered(
78 @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
79 whenRegistered(Closures.action(action));
80 }
81
82 /**
83 * Layer-local callback fired for every resolved variant/role/layer usage.
84 * Already emitted usage bindings are delivered immediately (replay).
85 * For simple callbacks you can use delegate-only style
86 * (for example {@code whenBound { variantName() }}).
87 * For nested closures prefer explicit parameter
88 * ({@code whenBound { ctx -> ... }}).
89 */
57 @Override
90 58 public void whenBound(Action<? super SourceSetUsageBinding> action) {
91 59 boundActions.add(action);
92 60 for (var context : boundContexts)
93 61 action.execute(context);
94 62 }
95 63
96 public void whenBound(
97 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
98 whenBound(Closures.action(action));
99 }
100
101 64 void notifyRegistered(SourceSetRegistration registration) {
102 65 if (registeredSourceSetNames.add(registration.sourceSetName())) {
103 66 var sourceSet = registration.sourceSet();
104 67 registeredSourceSets.add(sourceSet);
105 68
106 69 for (var action : sourceSetConfigureActions)
107 70 sourceSet.configure(action);
108 71 }
109 72
110 73 registeredContexts.add(registration);
111 74 for (var action : registeredActions)
112 75 action.execute(registration);
113 76 }
114 77
115 78 void notifyBound(SourceSetUsageBinding binding) {
116 79 boundContexts.add(binding);
117 80 for (var action : boundActions)
118 81 action.execute(binding);
119 82 }
120 83 }
@@ -1,22 +1,22
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import javax.inject.Inject;
4 4
5 5 import org.gradle.api.Named;
6 6
7 7 /**
8 * Global layer declaration used by build variants.
8 * Canonical identity model for a declared layout layer.
9 9 */
10 public abstract class BuildLayer implements Named {
10 public abstract class LayoutLayer implements Named {
11 11 private final String name;
12 12
13 13 @Inject
14 public BuildLayer(String name) {
14 public LayoutLayer(String name) {
15 15 this.name = name;
16 16 }
17 17
18 18 @Override
19 19 public String getName() {
20 20 return name;
21 21 }
22 22 }
@@ -1,33 +1,33
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.implab.gradle.common.core.lang.Closures;
4 4 import org.gradle.api.Action;
5 5 import org.gradle.api.NamedDomainObjectProvider;
6 6
7 7 import groovy.lang.Closure;
8 8 import groovy.lang.DelegatesTo;
9 9
10 10 /**
11 11 * Immutable payload for a newly registered {@link GenericSourceSet}.
12 12 *
13 13 * <p>Used as callback payload for
14 14 * {@link VariantSourcesExtension#whenRegistered(org.gradle.api.Action)} and
15 * {@link BuildLayerBinding#whenRegistered(org.gradle.api.Action)}.
15 * {@link LayerBindingSpec#whenRegistered(org.gradle.api.Action)}.
16 16 *
17 17 * @param layerName normalized layer name that owns the registration
18 18 * @param sourceSetName source-set name registered in the container
19 19 * @param sourceSet provider of the registered source set (realized later by Gradle on demand)
20 20 */
21 21 public record SourceSetRegistration(
22 22 String layerName,
23 23 String sourceSetName,
24 24 NamedDomainObjectProvider<GenericSourceSet> sourceSet) {
25 25 public void configureSourceSet(Action<? super GenericSourceSet> action) {
26 26 sourceSet.configure(action);
27 27 }
28 28
29 29 public void configureSourceSet(
30 30 @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
31 31 configureSourceSet(Closures.action(action));
32 32 }
33 33 }
@@ -1,37 +1,37
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import org.implab.gradle.common.core.lang.Closures;
4 4 import org.gradle.api.Action;
5 5 import org.gradle.api.NamedDomainObjectProvider;
6 6
7 7 import groovy.lang.Closure;
8 8 import groovy.lang.DelegatesTo;
9 9
10 10 /**
11 11 * Immutable payload for a resolved variant/role/layer usage bound to a source set.
12 12 *
13 13 * <p>Used as callback payload for
14 14 * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)} and
15 * {@link BuildLayerBinding#whenBound(org.gradle.api.Action)}.
15 * {@link LayerBindingSpec#whenBound(org.gradle.api.Action)}.
16 16 *
17 17 * @param variantName variant name from the build-variants model
18 18 * @param roleName role name inside the resolved variant
19 19 * @param layerName normalized layer name used to resolve the source set
20 20 * @param sourceSetName source-set name registered in the container
21 21 * @param sourceSet provider of the registered source set (realized later by Gradle on demand)
22 22 */
23 23 public record SourceSetUsageBinding(
24 24 String variantName,
25 25 String roleName,
26 26 String layerName,
27 27 String sourceSetName,
28 28 NamedDomainObjectProvider<GenericSourceSet> sourceSet) {
29 29 public void configureSourceSet(Action<? super GenericSourceSet> action) {
30 30 sourceSet.configure(action);
31 31 }
32 32
33 33 public void configureSourceSet(
34 34 @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
35 35 configureSourceSet(Closures.action(action));
36 36 }
37 37 }
@@ -1,238 +1,242
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.Collection;
5 5 import java.util.LinkedHashSet;
6 6 import java.util.List;
7 7 import java.util.Set;
8 8 import java.util.function.Consumer;
9 9 import java.util.function.Predicate;
10 10 import java.util.stream.Stream;
11 11
12 12 import javax.inject.Inject;
13 13
14 14 import org.eclipse.jdt.annotation.NonNullByDefault;
15 15 import org.gradle.api.Action;
16 16 import org.gradle.api.InvalidUserDataException;
17 17 import org.gradle.api.Named;
18 18 import org.implab.gradle.common.core.lang.Closures;
19 19
20 20 import groovy.lang.Closure;
21 21 import groovy.lang.DelegatesTo;
22 22
23 23 /**
24 24 * One artifact representation slot inside {@link VariantArtifact}.
25 25 *
26 26 * <p>
27 27 * The DSL exposed by this type is topology-aware sugar over an internal
28 28 * contribution model:
29 29 * <ul>
30 30 * <li>{@link #from(Object)} adds one direct contribution that does not depend
31 31 * on {@link VariantSourcesExtension} bindings;</li>
32 32 * <li>{@link #fromVariant(Action)}, {@link #fromRole(String, Action)} and
33 33 * {@link #fromLayer(String, Action)} define where a contribution is active in
34 34 * the variant/role/layer topology;</li>
35 35 * <li>{@link OutputSelectionSpec#output(String)} defines which named output of
36 36 * the matched {@link GenericSourceSet} should be added to the slot.</li>
37 37 * </ul>
38 38 *
39 39 * <p>
40 40 * Internally the slot stores contribution resolvers rather than raw output
41 41 * names. Each contribution can later materialize itself against the
42 42 * variant-specific source-set bindings and return:
43 43 * <ul>
44 44 * <li>a file notation object suitable for {@code files.from(...)}</li>
45 45 * <li>a {@link BindingKey} used to deduplicate repeated logical inputs during
46 46 * materialization</li>
47 47 * </ul>
48 48 *
49 49 * <p>
50 50 * Validation is intentionally separated from materialization: the slot keeps
51 51 * topology references in {@link #referencedRoleNames()} and
52 52 * {@link #referencedLayerNames()}, while the actual contribution pipeline is
53 53 * exposed through {@link #bindings()}.
54 54 */
55 55 @NonNullByDefault
56 56 public class VariantArtifactSlot implements Named {
57 57 private final String name;
58 58 private final List<BindingResolver> bindings = new ArrayList<>();
59 59 private final Set<String> referencedRoleNames = new LinkedHashSet<>();
60 60 private final Set<String> referencedLayerNames = new LinkedHashSet<>();
61 61 private boolean finalized;
62 62
63 63 @Inject
64 64 public VariantArtifactSlot(String name) {
65 65 this.name = VariantArtifact.normalize(name, "slot name must not be null or blank");
66 66 }
67 67
68 68 @Override
69 69 public String getName() {
70 70 return name;
71 71 }
72 72
73 73 public void fromVariant(Action<? super OutputSelectionSpec> configure) {
74 74 addContributions(context -> true, configure);
75 75 }
76 76
77 77 public void fromVariant(
78 78 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
79 79 fromVariant(Closures.action(configure));
80 80 }
81 81
82 82 public void fromRole(String roleName, Action<? super OutputSelectionSpec> configure) {
83 83 var normalizedRoleName = VariantArtifact.normalize(roleName, "role name must not be null or blank");
84 84 addContributions(context -> context.roleName().equals(normalizedRoleName), configure);
85 85 referencedRoleNames.add(normalizedRoleName);
86 86 }
87 87
88 88 public void fromRole(
89 89 String roleName,
90 90 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
91 91 fromRole(roleName, Closures.action(configure));
92 92 }
93 93
94 94 public void fromLayer(String layerName, Action<? super OutputSelectionSpec> configure) {
95 95 var normalizedLayerName = VariantArtifact.normalize(layerName, "layer name must not be null or blank");
96 96 addContributions(context -> context.layerName().equals(normalizedLayerName), configure);
97 97 referencedLayerNames.add(normalizedLayerName);
98 98 }
99 99
100 100 public void fromLayer(
101 101 String layerName,
102 102 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
103 103 fromLayer(layerName, Closures.action(configure));
104 104 }
105 105
106 106 /**
107 107 * Adds one direct slot contribution.
108 108 *
109 109 * <p>The supplied object is forwarded as-is to {@code files.from(...)}
110 110 * during slot materialization and does not depend on
111 111 * {@link VariantSourcesExtension#whenBound(Action)} callbacks.
112 112 */
113 113 public void from(Object files) {
114 114 ensureMutable("configure sources");
115 115
116 116 if (files == null)
117 117 throw new InvalidUserDataException("slot source must not be null");
118 118
119 119 var key = BindingKey.newUniqueKey("direct slot input for '" + name + "'");
120 120 bindings.add((contexts, consumer) -> consumer.accept(new ResolvedBinding(key, files)));
121 121 }
122 122
123 123 List<BindingResolver> bindings() {
124 124 return List.copyOf(bindings);
125 125 }
126 126
127 void acceptBindings(Consumer<? super BindingResolver> consumer) {
128 bindings.forEach(consumer);
129 }
130
127 131 Set<String> referencedRoleNames() {
128 132 return Set.copyOf(referencedRoleNames);
129 133 }
130 134
131 135 Set<String> referencedLayerNames() {
132 136 return Set.copyOf(referencedLayerNames);
133 137 }
134 138
135 139 void finalizeModel() {
136 140 finalized = true;
137 141 }
138 142
139 143 private void addContributions(
140 144 Predicate<SourceSetUsageBinding> selector,
141 145 Action<? super OutputSelectionSpec> configure) {
142 146 ensureMutable("configure sources");
143 147
144 148 var spec = new OutputSelectionSpec(selector);
145 149 configure.execute(spec);
146 150 spec.accept(bindings::add);
147 151 }
148 152
149 153 private void ensureMutable(String operation) {
150 154 if (finalized)
151 155 throw new InvalidUserDataException(
152 156 "Variant artifact slot '" + name + "' is finalized and cannot " + operation);
153 157 }
154 158
155 159 /**
156 160 * Local DSL buffer for one {@code fromVariant/fromRole/fromLayer} block.
157 161 *
158 162 * <p>
159 163 * The spec accumulates contributions locally and flushes them to the
160 164 * owning slot only after the outer configure block completes successfully.
161 165 */
162 166 public final class OutputSelectionSpec {
163 167 private final Predicate<SourceSetUsageBinding> selector;
164 168 private final List<BindingResolver> bindings = new ArrayList<>();
165 169
166 170 private OutputSelectionSpec(Predicate<SourceSetUsageBinding> selector) {
167 171 this.selector = selector;
168 172 }
169 173
170 174 public void output(String name) {
171 175 var outputName = VariantArtifact.normalize(name, "output name must not be null or blank");
172 176 bindings.add((contexts, consumer) -> contexts.stream()
173 177 .filter(selector)
174 178 .map(context -> resolveOutput(context, outputName))
175 179 .forEach(consumer));
176 180 }
177 181
178 182 public void output(String name, String... extra) {
179 183 Stream.concat(Stream.of(name), Stream.of(extra))
180 184 .forEach(this::output);
181 185 }
182 186
183 187 private ResolvedBinding resolveOutput(SourceSetUsageBinding context, String outputName) {
184 188 var key = new SourceSetOutputKey(context.sourceSetName(), outputName);
185 189 var files = context.sourceSet().map(sourceSet -> sourceSet.output(outputName));
186 190 return new ResolvedBinding(key, files);
187 191 }
188 192
189 193 void accept(Consumer<? super BindingResolver> consumer) {
190 194 bindings.forEach(consumer);
191 195 }
192 196 }
193 197
194 198 @FunctionalInterface
195 199 interface BindingResolver {
196 200 void resolve(Collection<SourceSetUsageBinding> contexts, Consumer<? super ResolvedBinding> consumer);
197 201 }
198 202
199 203 /**
200 204 * Materialized slot contribution for one concrete source-set binding.
201 205 */
202 206 record ResolvedBinding(BindingKey key, Object files) {
203 207 }
204 208
205 209 /**
206 210 * Marker key for deduplicating logical slot inputs during materialization.
207 211 *
208 212 * <p>
209 213 * Semantic keys such as {@link SourceSetOutputKey} collapse repeated
210 214 * references to the same logical output. Identity keys created via
211 215 * {@link #newUniqueKey()} or {@link #newUniqueKey(String)} can be used by contributions
212 216 * that must flow through the same pipeline but should never be merged.
213 217 */
214 218 interface BindingKey {
215 219 static BindingKey newUniqueKey(String hint) {
216 220 return new BindingKey() {
217 221 @Override
218 222 public String toString() {
219 223 return hint;
220 224 }
221 225 };
222 226 }
223 227
224 228 static BindingKey newUniqueKey() {
225 229 return newUniqueKey("unnamed");
226 230 }
227 231 }
228 232
229 233 /**
230 234 * Stable dedupe key for one named output of one resolved source set.
231 235 */
232 236 record SourceSetOutputKey(String sourceSetName, String outputName) implements BindingKey {
233 237 @Override
234 238 public String toString() {
235 239 return "sourceSet '" + sourceSetName + "' output '" + outputName + "'";
236 240 }
237 241 }
238 242 }
@@ -1,181 +1,184
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.LinkedHashMap;
4 4 import java.util.List;
5 5 import java.util.ArrayList;
6 6 import java.util.stream.Collectors;
7 7 import java.util.stream.Stream;
8 8
9 9 import org.gradle.api.GradleException;
10 10 import org.gradle.api.Plugin;
11 11 import org.gradle.api.Project;
12 12 import org.gradle.api.artifacts.Configuration;
13 13 import org.gradle.api.artifacts.ConfigurationPublications;
14 14 import org.gradle.api.artifacts.ConfigurationVariant;
15 15 import org.gradle.api.logging.Logger;
16 16 import org.gradle.api.logging.Logging;
17 17 import org.implab.gradle.common.core.lang.Strings;
18 18
19 19 public abstract class VariantArtifactsPlugin implements Plugin<Project> {
20 20 private static final Logger logger = Logging.getLogger(VariantArtifactsPlugin.class);
21 21 public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";
22 22
23 23 @Override
24 24 public void apply(Project target) {
25 25 logger.debug("Registering '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
26 26
27 27 target.getPluginManager().apply(VariantsSourcesPlugin.class);
28 28
29 29 var variants = VariantsPlugin.getVariantsExtension(target);
30 30 var variantSources = target.getExtensions().getByType(VariantSourcesExtension.class);
31 31 var variantArtifacts = target.getExtensions()
32 32 .create(VARIANT_ARTIFACTS_EXTENSION_NAME, VariantArtifactsExtension.class);
33 33 var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
34 34 var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks());
35 35
36 // Bind variant artifacts resolution to variant sources registration, so that artifact resolution can be performed
36 // Bind variant artifacts resolution to variant sources registration, so that
37 // artifact resolution can be performed
37 38 variantSources.whenBound(variantArtifactsResolver::recordBinding);
38 39
39 40 variants.whenFinalized(model -> {
40 41 logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath());
41 42 variantArtifacts.finalizeModel(model);
42 43 materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies);
43 44 logger.debug("variantArtifacts model finalized on project '{}'", target.getPath());
44 45 });
45 46 }
46 47
47 48 public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) {
48 49 var extension = target.getExtensions().findByType(VariantArtifactsExtension.class);
49 50
50 51 if (extension == null) {
51 52 logger.error("variantArtifacts extension '{}' isn't found on project '{}'",
52 53 VARIANT_ARTIFACTS_EXTENSION_NAME,
53 54 target.getPath());
54 55 throw new GradleException("variantArtifacts extension isn't found");
55 56 }
56 57
57 58 logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
58 59
59 60 return extension;
60 61 }
61 62
62 63 private static void materializeOutgoingVariants(
63 64 Project project,
64 65 BuildVariantsExtension topology,
65 66 VariantArtifactsExtension variantArtifacts,
66 67 VariantArtifactsResolver variantArtifactsResolver,
67 68 ArtifactAssemblyRegistry artifactAssemblies) {
68 69 variantArtifacts.getVariants().stream()
69 70 .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty())
70 71 .forEach(variantArtifact -> materializeOutgoingVariant(
71 72 project,
72 73 topology.require(variantArtifact.getName()),
73 74 variantArtifact,
74 75 variantArtifactsResolver,
75 76 artifactAssemblies,
76 77 variantArtifacts));
77 78 }
78 79
79 80 private static void materializeOutgoingVariant(
80 81 Project project,
81 82 BuildVariant topologyVariant,
82 83 VariantArtifact variantArtifact,
83 84 VariantArtifactsResolver variantArtifactsResolver,
84 85 ArtifactAssemblyRegistry artifactAssemblies,
85 86 VariantArtifactsExtension variantArtifacts) {
86 87 var assemblies = variantArtifact.getSlots().stream()
87 88 .collect(Collectors.toMap(
88 89 VariantArtifactSlot::getName,
89 slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, slot),
90 slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact,
91 slot),
90 92 (left, right) -> left,
91 93 LinkedHashMap::new));
92 94
93 95 var primarySlot = variantArtifact.requirePrimarySlot();
94 96 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), primarySlot.getName());
95 97 var primaryAssembly = assemblies.get(primarySlot.getName());
96 98 publishPrimaryArtifact(configuration, primaryAssembly);
97 99 var primaryPublication = new OutgoingArtifactSlotPublication(
98 100 primarySlot.getName(),
99 101 true,
100 102 primarySlot,
101 103 primaryAssembly,
102 104 configuration);
103 105 var secondarySlots = variantArtifact.getSlots().stream()
104 106 .filter(slot -> !slot.getName().equals(primarySlot.getName()))
105 107 .map(slot -> new SecondarySlot(slot, assemblies.get(slot.getName())))
106 108 .toList();
107 109 var secondaryPublications = new ArrayList<OutgoingArtifactSlotPublication>(secondarySlots.size());
108 110 secondarySlots.forEach(secondarySlot -> {
109 111 var secondaryVariant = configuration.getOutgoing().getVariants().create(secondarySlot.slot().getName());
110 112 publishSecondaryArtifact(secondaryVariant, secondarySlot.assembly());
111 113 secondaryPublications.add(new OutgoingArtifactSlotPublication(
112 114 secondarySlot.slot().getName(),
113 115 false,
114 116 secondarySlot.slot(),
115 117 secondarySlot.assembly(),
116 118 secondaryVariant));
117 119 });
118 120
119 121 var slotPublications = Stream.concat(
120 122 Stream.of(primaryPublication),
121 123 secondaryPublications.stream())
122 124 .toList();
123 125
124 126 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
125 127 variantArtifact.getName(),
126 128 topologyVariant,
127 129 variantArtifact,
128 130 configuration,
129 131 primaryPublication,
130 132 slotPublications));
131 133 }
132 134
133 135 private static ArtifactAssembly registerAssembly(
134 136 Project project,
135 137 VariantArtifactsResolver variantArtifactsResolver,
136 138 ArtifactAssemblyRegistry artifactAssemblies,
137 139 VariantArtifact variantArtifact,
138 140 VariantArtifactSlot slot) {
141 String assemblyName = variantArtifact.getName() + Strings.capitalize(slot.getName());
139 142 return artifactAssemblies.register(
140 variantArtifact.getName() + Strings.capitalize(slot.getName()),
141 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
143 assemblyName,
144 "process" + Strings.capitalize(assemblyName),
142 145 project.getLayout().getBuildDirectory()
143 146 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
144 147 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
145 148 }
146 149
147 150 private static Configuration createOutgoingConfiguration(
148 151 Project project,
149 152 String variantName,
150 153 String primarySlotName) {
151 154 var configName = variantName + "Elements";
152 155 return project.getConfigurations().consumable(configName, config -> {
153 156 config.setVisible(true);
154 157 config.setDescription("Consumable assembled artifacts for variant '" + variantName
155 158 + "' with primary slot '" + primarySlotName + "'");
156 159 }).get();
157 160 }
158 161
159 162 private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) {
160 163 publishArtifact(configuration.getOutgoing(), assembly);
161 164 }
162 165
163 166 private static void publishSecondaryArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
164 167 publishArtifact(variant, assembly);
165 168 }
166 169
167 170 private static void publishArtifact(ConfigurationPublications outgoing, ArtifactAssembly assembly) {
168 171 outgoing.artifact(assembly.output().getSingleFile(), published -> {
169 172 published.builtBy(assembly.output().getBuildDependencies());
170 173 });
171 174 }
172 175
173 176 private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
174 177 variant.artifact(assembly.output().getSingleFile(), published -> {
175 178 published.builtBy(assembly.output().getBuildDependencies());
176 179 });
177 180 }
178 181
179 182 private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) {
180 183 }
181 184 }
@@ -1,121 +1,126
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 import java.util.Collection;
4 5 import java.util.LinkedHashSet;
5 6 import java.util.List;
6 7 import java.util.Set;
7 8
8 9 import org.eclipse.jdt.annotation.NonNullByDefault;
9 10 import org.gradle.api.file.ConfigurableFileCollection;
10 11 import org.gradle.api.file.FileCollection;
11 12 import org.gradle.api.model.ObjectFactory;
12 13 import org.implab.gradle.common.sources.VariantArtifactSlot.BindingKey;
13 14
14 15 /**
15 16 * Resolves artifact-slot inputs from already bound variant source-set usages.
16 17 *
17 18 * <p>This type is the bridge between two models:
18 19 * <ul>
19 20 * <li>{@link VariantSourcesExtension}, which emits resolved
20 21 * {@link SourceSetUsageBinding variant/role/layer -> source-set} bindings;</li>
21 22 * <li>{@link VariantArtifactSlot}, which exposes a DSL over slot contributions
22 23 * and stores the resulting {@link VariantArtifactSlot.BindingResolver
23 24 * contribution resolvers}.</li>
24 25 * </ul>
25 26 *
26 27 * <p>The resolver records each emitted {@link SourceSetUsageBinding} and later
27 28 * materializes a {@link FileCollection} for one concrete variant/slot pair.
28 29 * For each variant/slot pair it asks the slot to materialize its contributions,
29 30 * passes in the resolved source-set bindings for that variant, and
30 31 * deduplicates resulting inputs by {@link BindingKey}. Contributions that do
31 32 * not depend on topology bindings can still emit direct inputs even when that
32 33 * binding collection is empty. The returned files are then typically wired into
33 34 * an {@link ArtifactAssembly} as its sources.
34 35 *
35 36 * <p>Direct clients are infrastructure code rather than build scripts. The
36 37 * typical usage pattern is:
37 38 * <ol>
38 39 * <li>create one resolver per project;</li>
39 40 * <li>subscribe {@link #recordBinding(SourceSetUsageBinding)} to
40 41 * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)};</li>
41 42 * <li>call {@link #files(String, VariantArtifactSlot)} while registering an
42 43 * {@link ArtifactAssembly} or another consumer that needs the slot inputs.</li>
43 44 * </ol>
44 45 *
45 46 * <p>Build-script users normally do not instantiate this class directly. They
46 47 * configure {@code variantArtifacts}, and {@link VariantArtifactsPlugin} uses
47 48 * this resolver internally to turn slot rules into assembly inputs.
48 49 */
49 50 @NonNullByDefault
50 51 public final class VariantArtifactsResolver {
51 52 private final ObjectFactory objects;
52 53 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
53 54
54 55 public VariantArtifactsResolver(ObjectFactory objects) {
55 56 this.objects = objects;
56 57 }
57 58
58 59 /**
59 60 * Records one resolved variant source-set usage.
60 61 *
61 62 * <p>Intended to be used as a callback target for
62 63 * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)}.
63 64 *
64 65 * @param context resolved variant/role/layer usage bound to a source set
65 66 */
66 67 public void recordBinding(SourceSetUsageBinding context) {
67 68 boundContexts.add(context);
68 69 }
69 70
70 71 /**
71 72 * Returns all source-set outputs selected by the given slot for the given
72 73 * variant.
73 74 *
74 75 * <p>The result is built from recorded {@link SourceSetUsageBinding}
75 76 * instances whose {@link SourceSetUsageBinding#variantName()} matches
76 77 * {@code variantName}. Each matching binding is then fed into the slot
77 78 * contribution pipeline; if multiple contributions resolve to the same
78 79 * {@link BindingKey}, that source is included only once.
79 80 *
80 81 * <p>This method does not validate the model; validation is expected to be
81 82 * performed earlier by {@link VariantArtifactsExtension}. Unknown variants
82 83 * or slots with no matching rules simply produce an empty collection.
83 84 *
84 85 * @param variantName variant whose bound source-set usages should be scanned
85 86 * @param slot slot definition that selects which outputs should be included
86 87 * @return lazily wired file collection for the selected outputs
87 88 */
88 89 public FileCollection files(String variantName, VariantArtifactSlot slot) {
89 var builder = new FileCollectionBuilder();
90 90 var contexts = boundContexts.stream()
91 91 .filter(context -> variantName.equals(context.variantName()))
92 92 .toList();
93
94 slot.bindings().forEach(binding -> binding.resolve(contexts, builder::addOutput));
95
93 var builder = new FileCollectionBuilder(contexts);
94 slot.acceptBindings(builder::visitBinding);
96 95 return builder.build();
97 96 }
98 97
99 98 /**
100 99 * Local materialization helper for one {@link #files(String, VariantArtifactSlot)}
101 100 * call.
102 101 */
103 102 class FileCollectionBuilder {
104 103 private final ConfigurableFileCollection files;
105 104 private final Set<BindingKey> boundOutputs = new LinkedHashSet<>();
105 private final Collection<SourceSetUsageBinding> contexts;
106 106
107 FileCollectionBuilder() {
107 FileCollectionBuilder(Collection<SourceSetUsageBinding> contexts) {
108 108 this.files = objects.fileCollection();
109 this.contexts = contexts;
109 110 }
110 111
111 112 FileCollection build() {
112 113 return files;
113 114 }
114 115
115 116 void addOutput(VariantArtifactSlot.ResolvedBinding binding) {
116 117 if (boundOutputs.add(binding.key()))
117 118 files.from(binding.files());
118 119 }
120
121 void visitBinding(VariantArtifactSlot.BindingResolver resolver) {
122 resolver.resolve(contexts, this::addOutput);
123 }
119 124 }
120 125
121 126 }
@@ -1,308 +1,317
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.LinkedHashMap;
5 5 import java.util.List;
6 6 import java.util.regex.Matcher;
7 7 import java.util.regex.Pattern;
8 8 import java.util.stream.Stream;
9 9
10 10 import javax.inject.Inject;
11 11
12 12 import org.implab.gradle.common.core.lang.Closures;
13 13 import org.implab.gradle.common.core.lang.Strings;
14 14 import org.eclipse.jdt.annotation.NonNullByDefault;
15 15 import org.eclipse.jdt.annotation.Nullable;
16 16 import org.gradle.api.Action;
17 17 import org.gradle.api.InvalidUserDataException;
18 import org.gradle.api.NamedDomainObjectContainer;
19 18 import org.gradle.api.NamedDomainObjectProvider;
20 19 import org.gradle.api.file.ProjectLayout;
21 20 import org.gradle.api.model.ObjectFactory;
22 21 import org.gradle.api.logging.Logger;
23 22 import org.gradle.api.logging.Logging;
24 23
25 24 import groovy.lang.Closure;
26 25 import groovy.lang.DelegatesTo;
27 26
28 27 import static org.implab.gradle.common.core.lang.Strings.sanitizeName;
29 28
30 29 /**
31 30 * Adapter extension that registers source sets for variant/layer pairs.
32 31 */
33 32 @NonNullByDefault
34 33 public abstract class VariantSourcesExtension {
35 34 private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class);
36 35 private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}");
37 36
37 private final ObjectFactory objects;
38 38 private final ProjectLayout layout;
39 private final NamedDomainObjectContainer<BuildLayerBinding> bindings;
39 private final LinkedHashMap<String, LayerBinding> bindingsByName = new LinkedHashMap<>();
40 40 private final List<Action<? super SourceSetRegistration>> registeredActions = new ArrayList<>();
41 41 private final List<Action<? super SourceSetUsageBinding>> boundActions = new ArrayList<>();
42 42 private final List<SourceSetRegistration> registeredContexts = new ArrayList<>();
43 43 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
44 44 private final LinkedHashMap<String, NamedDomainObjectProvider<GenericSourceSet>> sourceSetsByName = new LinkedHashMap<>();
45 45 private final LinkedHashMap<String, String> sourceSetLayersByName = new LinkedHashMap<>();
46 46 private boolean sourceSetsRegistered;
47 47
48 48 @Inject
49 49 public VariantSourcesExtension(ObjectFactory objects, ProjectLayout layout) {
50 this.objects = objects;
50 51 this.layout = layout;
51 bindings = objects.domainObjectContainer(BuildLayerBinding.class);
52 52 }
53 53
54 public NamedDomainObjectContainer<BuildLayerBinding> getBindings() {
55 return bindings;
54 public List<LayerBindingSpec> getBindings() {
55 return bindingsByName.values().stream().map(x -> (LayerBindingSpec)x).toList();
56 56 }
57 57
58 public void bindings(Action<? super NamedDomainObjectContainer<BuildLayerBinding>> action) {
59 action.execute(bindings);
58 public LayerBindingSpec bind(String layer) {
59 return bindingsByName.computeIfAbsent(
60 normalize(layer, "Layer name must not be null or blank"),
61 name -> new LayerBinding(name, objects));
60 62 }
61 63
62 public void bindings(
63 @DelegatesTo(value = NamedDomainObjectContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
64 bindings(Closures.action(action));
65 }
66
67 public BuildLayerBinding bind(String layer) {
68 return bindings.maybeCreate(normalize(layer, "Layer name must not be null or blank"));
64 public LayerBindingSpec bind(LayoutLayer layer) {
65 return bind(layer.getName());
69 66 }
70 67
71 68 /**
72 69 * Configures per-layer binding.
73 70 */
74 public BuildLayerBinding bind(String layer, Action<? super BuildLayerBinding> configure) {
71 public LayerBindingSpec bind(String layer, Action<? super LayerBindingSpec> configure) {
75 72 var binding = bind(layer);
76 73 configure.execute(binding);
77 74 return binding;
78 75 }
79 76
80 public BuildLayerBinding bind(String layer,
81 @DelegatesTo(value = BuildLayerBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
77 public LayerBindingSpec bind(LayoutLayer layer, Action<? super LayerBindingSpec> configure) {
78 var binding = bind(layer);
79 configure.execute(binding);
80 return binding;
81 }
82
83 public LayerBindingSpec bind(String layer,
84 @DelegatesTo(value = LayerBindingSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
85 return bind(layer, Closures.action(configure));
86 }
87
88 public LayerBindingSpec bind(LayoutLayer layer,
89 @DelegatesTo(value = LayerBindingSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
82 90 return bind(layer, Closures.action(configure));
83 91 }
84 92
85 93 /**
86 94 * Global callback fired for each registered source set.
87 95 * Already emitted registrations are delivered immediately (replay).
88 96 * For simple callbacks you can use delegate-only style
89 97 * (for example {@code whenRegistered { sourceSetName() }}).
90 98 * For nested closures prefer explicit parameter
91 99 * ({@code whenRegistered { ctx -> ... }}).
92 100 */
93 101 public void whenRegistered(Action<? super SourceSetRegistration> action) {
94 102 registeredActions.add(action);
95 103 for (var context : registeredContexts)
96 104 action.execute(context);
97 105 }
98 106
99 107 public void whenRegistered(
100 108 @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
101 109 whenRegistered(Closures.action(action));
102 110 }
103 111
104 112 /**
105 113 * Global callback fired for every resolved variant/role/layer usage.
106 114 * Already emitted usage bindings are delivered immediately (replay).
107 115 * For simple callbacks you can use delegate-only style
108 116 * (for example {@code whenBound { variantName() }}).
109 117 * For nested closures prefer explicit parameter
110 118 * ({@code whenBound { ctx -> ... }}).
111 119 */
112 120 public void whenBound(Action<? super SourceSetUsageBinding> action) {
113 121 boundActions.add(action);
114 122 for (var context : boundContexts)
115 123 action.execute(context);
116 124 }
117 125
118 126 public void whenBound(
119 127 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
120 128 whenBound(Closures.action(action));
121 129 }
122 130
123 131 public void whenBound(String variantName, Action<? super SourceSetUsageBinding> action) {
124 132 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
125 133 whenBound(filterByVariant(normalizedVariantName, action));
126 134 }
127 135
128 136 public void whenBound(String variantName,
129 137 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
130 138 whenBound(variantName, Closures.action(action));
131 139 }
132 140
133 void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer<GenericSourceSet> sources) {
141 void registerSourceSets(BuildVariantsExtension variants, org.gradle.api.NamedDomainObjectContainer<GenericSourceSet> sources) {
134 142 if (sourceSetsRegistered) {
135 143 throw new InvalidUserDataException("variantSources source sets are already registered");
136 144 }
137 145
138 validateBindings(variants);
146 resolveBindings(variants);
139 147
140 148 var usages = layerUsages(variants).toList();
141 149 var registeredBefore = registeredContexts.size();
142 150 var boundBefore = boundContexts.size();
143 151
144 152 logger.debug(
145 153 "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})",
146 154 variants.getVariants().size(),
147 155 variants.getLayers().size(),
148 bindings.size(),
156 bindingsByName.size(),
149 157 usages.size());
150 158
151 159 usages.forEach(usage -> registerLayerUsage(usage, sources));
152 160
153 161 logger.debug(
154 162 "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})",
155 163 registeredContexts.size() - registeredBefore,
156 164 boundContexts.size() - boundBefore,
157 165 sourceSetsByName.size());
158 166
159 167 sourceSetsRegistered = true;
160 168 }
161 169
162 170 private Stream<LayerUsage> layerUsages(BuildVariantsExtension variants) {
163 171 return variants.getVariants().stream()
164 172 .flatMap(variant -> variant.getRoles().stream()
165 173 .flatMap(role -> role.getLayers().getOrElse(List.of()).stream()
166 174 .map(layerName -> new LayerUsage(
167 175 variant.getName(),
168 176 role.getName(),
169 normalize(layerName, "Layer name in variant '" + variant.getName()
170 + "' and role '" + role.getName() + "' must not be null or blank")))));
177 variants.requireLayer(normalize(layerName, "Layer name in variant '"
178 + variant.getName() + "' and role '" + role.getName()
179 + "' must not be null or blank"))))));
171 180 }
172 181
173 private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
174 var resolvedBinding = bind(usage.layerName());
182 private void registerLayerUsage(LayerUsage usage, org.gradle.api.NamedDomainObjectContainer<GenericSourceSet> sources) {
183 var resolvedBinding = binding(usage.layer().getName());
175 184 var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern();
176 185 sourceSetNamePattern.finalizeValueOnRead();
177 186
178 187 var sourceSetName = sourceSetName(usage, sourceSetNamePattern.get());
179 188
180 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layerName());
189 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layer().getName());
181 190 var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName);
182 191 var sourceSet = sourceSetsByName.computeIfAbsent(sourceSetName,
183 192 name -> {
184 193 var ssp = sources.register(name);
185 194 ssp.configure(x -> {
186 x.getSourceSetDir().set(layout.getProjectDirectory().dir("src/" + usage.layerName()));
195 x.getSourceSetDir().set(layout.getProjectDirectory().dir("src/" + usage.layer().getName()));
187 196 });
188 197 return ssp;
189 198 });
190 199
191 200 var binding = new SourceSetUsageBinding(
192 201 usage.variantName(),
193 202 usage.roleName(),
194 usage.layerName(),
203 usage.layer().getName(),
195 204 sourceSetName,
196 205 sourceSet);
197 206
198 207 if (isNewSourceSet) {
199 208 var registration = new SourceSetRegistration(
200 usage.layerName(),
209 usage.layer().getName(),
201 210 sourceSetName,
202 211 sourceSet);
203 212 resolvedBinding.notifyRegistered(registration);
204 213 notifyRegistered(registration);
205 214 }
206 215
207 216 resolvedBinding.notifyBound(binding);
208 217 notifyBound(binding);
209 218 }
210 219
211 220 private void notifyRegistered(SourceSetRegistration registration) {
212 221 registeredContexts.add(registration);
213 222 for (var action : registeredActions)
214 223 action.execute(registration);
215 224 }
216 225
217 226 private void notifyBound(SourceSetUsageBinding binding) {
218 227 boundContexts.add(binding);
219 228 for (var action : boundActions)
220 229 action.execute(binding);
221 230 }
222 231
223 232 private static Action<? super SourceSetUsageBinding> filterByVariant(String variantName,
224 233 Action<? super SourceSetUsageBinding> action) {
225 234 return binding -> {
226 235 if (variantName.equals(binding.variantName()))
227 236 action.execute(binding);
228 237 };
229 238 }
230 239
231 240 private void ensureSourceSetNameBoundToSingleLayer(String sourceSetName, String layerName) {
232 241 var existingLayer = sourceSetLayersByName.putIfAbsent(sourceSetName, layerName);
233 242 if (existingLayer != null && !existingLayer.equals(layerName)) {
234 243 throw new InvalidUserDataException("Source set '" + sourceSetName + "' is resolved from multiple layers: '"
235 244 + existingLayer + "' and '" + layerName + "'");
236 245 }
237 246 }
238 247
239 private void validateBindings(BuildVariantsExtension variants) {
240 var knownLayerNames = new java.util.LinkedHashSet<String>();
241 for (var layer : variants.getLayers())
242 knownLayerNames.add(layer.getName());
243
248 private void resolveBindings(BuildVariantsExtension variants) {
244 249 var errors = new ArrayList<String>();
245 for (var binding : bindings) {
246 if (!knownLayerNames.contains(binding.getName())) {
250 for (var binding : bindingsByName.values()) {
251 if (variants.findLayer(binding.getName()).isEmpty()) {
247 252 errors.add("Layer binding '" + binding.getName() + "' references unknown layer");
248 253 }
249 254 }
250 255
251 256 if (!errors.isEmpty()) {
252 257 var message = new StringBuilder("Invalid variantSources model:");
253 258 for (var error : errors)
254 259 message.append("\n - ").append(error);
255 260 throw new InvalidUserDataException(message.toString());
256 261 }
257 262 }
258 263
264 private LayerBinding binding(String layerName) {
265 return bindingsByName.computeIfAbsent(layerName, name -> new LayerBinding(name, objects));
266 }
267
259 268 private static String sourceSetName(LayerUsage usage, String pattern) {
260 269 var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank");
261 270 var resolved = resolveSourceSetNamePattern(normalizedPattern, usage);
262 271 var result = sanitizeName(resolved);
263 272
264 273 if (result.isEmpty())
265 274 throw new InvalidUserDataException(
266 275 "sourceSetNamePattern '" + pattern + "' resolved to empty source set name");
267 276
268 277 return result;
269 278 }
270 279
271 280 private static String resolveSourceSetNamePattern(String pattern, LayerUsage usage) {
272 281 var matcher = SOURCE_SET_NAME_TOKEN.matcher(pattern);
273 282 var output = new StringBuffer();
274 283
275 284 while (matcher.find()) {
276 285 var token = matcher.group(1);
277 286 matcher.appendReplacement(output, Matcher.quoteReplacement(tokenValue(token, usage)));
278 287 }
279 288 matcher.appendTail(output);
280 289
281 290 return output.toString();
282 291 }
283 292
284 293 private static String tokenValue(String token, LayerUsage usage) {
285 294 return switch (token) {
286 295 case "variant" -> sanitizeName(usage.variantName());
287 296 case "variantCap" -> Strings.capitalize(sanitizeName(usage.variantName()));
288 297 case "role" -> sanitizeName(usage.roleName());
289 298 case "roleCap" -> Strings.capitalize(sanitizeName(usage.roleName()));
290 case "layer" -> sanitizeName(usage.layerName());
291 case "layerCap" -> Strings.capitalize(sanitizeName(usage.layerName()));
299 case "layer" -> sanitizeName(usage.layer().getName());
300 case "layerCap" -> Strings.capitalize(sanitizeName(usage.layer().getName()));
292 301 default -> throw new InvalidUserDataException(
293 302 "sourceSetNamePattern contains unsupported token '{" + token + "}'");
294 303 };
295 304 }
296 305
297 306 private static String normalize(@Nullable String value, String errorMessage) {
298 307 if (value == null)
299 308 throw new InvalidUserDataException(errorMessage);
300 309 var trimmed = value.trim();
301 310 if (trimmed.isEmpty())
302 311 throw new InvalidUserDataException(errorMessage);
303 312 return trimmed;
304 313 }
305 314
306 private record LayerUsage(String variantName, String roleName, String layerName) {
315 private record LayerUsage(String variantName, String roleName, LayoutLayer layer) {
307 316 }
308 317 }
@@ -1,608 +1,608
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 5 import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
7 7 import java.io.File;
8 8 import java.io.IOException;
9 9 import java.nio.file.Files;
10 10 import java.nio.file.Path;
11 11 import java.util.List;
12 12 import java.util.stream.Collectors;
13 13
14 14 import org.gradle.testkit.runner.BuildResult;
15 15 import org.gradle.testkit.runner.GradleRunner;
16 16 import org.gradle.testkit.runner.TaskOutcome;
17 17 import org.gradle.testkit.runner.UnexpectedBuildFailure;
18 18 import org.junit.jupiter.api.Test;
19 19 import org.junit.jupiter.api.io.TempDir;
20 20
21 21 class VariantsArtifactsPluginFunctionalTest {
22 22 private static final String SETTINGS_FILE = "settings.gradle";
23 23 private static final String BUILD_FILE = "build.gradle";
24 24 private static final String ROOT_NAME = "rootProject.name = 'variants-artifacts-fixture'\n";
25 25
26 26 @TempDir
27 27 Path testProjectDir;
28 28
29 29 @Test
30 30 void materializesVariantArtifactsAndInvokesOutgoingHooks() throws Exception {
31 31 writeFile(SETTINGS_FILE, ROOT_NAME);
32 32 writeFile("inputs/base.js", "console.log('base')\n");
33 33 writeFile("inputs/amd.js", "console.log('amd')\n");
34 34 writeFile("inputs/mainJs.txt", "mainJs marker\n");
35 35 writeFile("inputs/amdJs.txt", "amdJs marker\n");
36 36 writeFile(BUILD_FILE, """
37 37 import org.gradle.api.attributes.Attribute
38 38
39 39 plugins {
40 40 id 'org.implab.gradle-variants-artifacts'
41 41 }
42 42
43 43 variants {
44 layer('mainBase')
45 layer('mainAmd')
44 layers('mainBase', 'mainAmd')
45 roles('main', 'test')
46 46
47 47 variant('browser') {
48 48 role('main') {
49 49 layers('mainBase', 'mainAmd')
50 50 }
51 51 }
52 52 }
53 53
54 54 variantSources {
55 55 bind('mainBase') {
56 56 configureSourceSet {
57 57 declareOutputs('js')
58 58 }
59 59 }
60 60
61 61 bind('mainAmd') {
62 62 configureSourceSet {
63 63 declareOutputs('js')
64 64 }
65 65 }
66 66
67 67 whenBound { ctx ->
68 68 if (ctx.sourceSetName() == 'browserMainBase') {
69 69 ctx.configureSourceSet {
70 70 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
71 71 }
72 72 }
73 73
74 74 if (ctx.sourceSetName() == 'browserMainAmd') {
75 75 ctx.configureSourceSet {
76 76 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
77 77 }
78 78 }
79 79 }
80 80 }
81 81
82 82 variantArtifacts {
83 83 variant('browser') {
84 84 primarySlot('mainJs') {
85 85 fromRole('main') {
86 86 output('js')
87 87 }
88 88 }
89 89
90 90 slot('amdJs') {
91 91 fromLayer('mainAmd') {
92 92 output('js')
93 93 }
94 94 }
95 95 }
96 96
97 97 whenOutgoingVariant { publication ->
98 98 publication.slots().each { slotPublication ->
99 99 slotPublication.configureTask {
100 100 from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt"))
101 101 }
102 102
103 103 slotPublication.configureArtifactAttributes {
104 104 attribute(Attribute.of('test.slot', String), slotPublication.slotName())
105 105 }
106 106 }
107 107 }
108 108 }
109 109
110 110 tasks.register('probe') {
111 111 dependsOn 'processBrowserMainJs', 'processBrowserAmdJs'
112 112
113 113 doLast {
114 114 def mainDir = layout.buildDirectory.dir('variant-artifacts/browser/mainJs').get().asFile
115 115 def amdDir = layout.buildDirectory.dir('variant-artifacts/browser/amdJs').get().asFile
116 116
117 117 assert new File(mainDir, 'base.js').exists()
118 118 assert new File(mainDir, 'amd.js').exists()
119 119 assert new File(mainDir, 'mainJs.txt').exists()
120 120
121 121 assert !new File(amdDir, 'base.js').exists()
122 122 assert new File(amdDir, 'amd.js').exists()
123 123 assert new File(amdDir, 'amdJs.txt').exists()
124 124
125 125 def elements = configurations.getByName('browserElements')
126 126 def primaryAttr = elements.attributes.getAttribute(Attribute.of('test.slot', String))
127 127 def amdVariant = elements.outgoing.variants.getByName('amdJs')
128 128 def amdAttr = amdVariant.attributes.getAttribute(Attribute.of('test.slot', String))
129 129
130 130 println('primarySlot=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
131 131 println('primaryAttr=' + primaryAttr)
132 132 println('amdAttr=' + amdAttr)
133 133 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
134 134 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
135 135 }
136 136 }
137 137 """);
138 138
139 139 BuildResult result = runner("probe").build();
140 140
141 141 assertTrue(result.getOutput().contains("primarySlot=mainJs"));
142 142 assertTrue(result.getOutput().contains("primaryAttr=mainJs"));
143 143 assertTrue(result.getOutput().contains("amdAttr=amdJs"));
144 144 assertTrue(result.getOutput().contains("configurations=browserElements"));
145 145 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
146 146 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
147 147 }
148 148
149 149 @Test
150 150 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
151 151 writeFile(SETTINGS_FILE, ROOT_NAME);
152 152 writeFile(BUILD_FILE, """
153 153 plugins {
154 154 id 'org.implab.gradle-variants-artifacts'
155 155 }
156 156
157 157 variants {
158 158 layer('main')
159 159
160 160 variant('browser') {
161 161 role('main') {
162 162 layers('main')
163 163 }
164 164 }
165 165 }
166 166
167 167 variantArtifacts {
168 168 variant('browser') {
169 169 slot('typesPackage') {
170 170 fromVariant {
171 171 output('types')
172 172 }
173 173 }
174 174 }
175 175 }
176 176
177 177 tasks.register('probe') {
178 178 doLast {
179 179 println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
180 180 }
181 181 }
182 182 """);
183 183
184 184 BuildResult result = runner("probe").build();
185 185 assertTrue(result.getOutput().contains("primary=typesPackage"));
186 186 }
187 187
188 188 @Test
189 189 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
190 190 writeFile(SETTINGS_FILE, ROOT_NAME);
191 191 writeFile("inputs/bundle.js", "console.log('bundle')\n");
192 192 writeFile(BUILD_FILE, """
193 193 plugins {
194 194 id 'org.implab.gradle-variants-artifacts'
195 195 }
196 196
197 197 variants {
198 198 layer('main')
199 199
200 200 variant('browser') {
201 201 role('main') {
202 202 layers('main')
203 203 }
204 204 }
205 205 }
206 206
207 207 variantArtifacts {
208 208 variant('browser') {
209 209 primarySlot('bundle') {
210 210 from(layout.projectDirectory.file('inputs/bundle.js'))
211 211 }
212 212 }
213 213 }
214 214
215 215 tasks.register('probe') {
216 216 dependsOn 'processBrowserBundle'
217 217
218 218 doLast {
219 219 def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
220 220 assert new File(bundleDir, 'bundle.js').exists()
221 221 println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
222 222 }
223 223 }
224 224 """);
225 225
226 226 BuildResult result = runner("probe").build();
227 227
228 228 assertTrue(result.getOutput().contains("primary=bundle"));
229 229 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
230 230 }
231 231
232 232 @Test
233 233 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
234 234 writeFile(SETTINGS_FILE, ROOT_NAME);
235 235 writeFile("inputs/base.js", "console.log('base')\n");
236 236 writeFile("inputs/marker.txt", "marker\n");
237 237 writeFile(BUILD_FILE, """
238 238 plugins {
239 239 id 'org.implab.gradle-variants-artifacts'
240 240 }
241 241
242 242 variants {
243 243 layer('main')
244 244
245 245 variant('browser') {
246 246 role('main') {
247 247 layers('main')
248 248 }
249 249 }
250 250 }
251 251
252 252 variantSources {
253 253 bind('main') {
254 254 configureSourceSet {
255 255 declareOutputs('js')
256 256 }
257 257 }
258 258
259 259 whenBound { ctx ->
260 260 ctx.configureSourceSet {
261 261 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
262 262 }
263 263 }
264 264 }
265 265
266 266 variantArtifacts {
267 267 variant('browser') {
268 268 primarySlot('bundle') {
269 269 fromVariant {
270 270 output('js')
271 271 }
272 272 from(layout.projectDirectory.file('inputs/marker.txt'))
273 273 }
274 274 }
275 275 }
276 276
277 277 tasks.register('probe') {
278 278 dependsOn 'processBrowserBundle'
279 279
280 280 doLast {
281 281 def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
282 282 assert new File(bundleDir, 'base.js').exists()
283 283 assert new File(bundleDir, 'marker.txt').exists()
284 284 }
285 285 }
286 286 """);
287 287
288 288 BuildResult result = runner("probe").build();
289 289
290 290 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
291 291 }
292 292
293 293 @Test
294 294 void failsOnUnknownVariantReference() throws Exception {
295 295 assertBuildFails("""
296 296 plugins {
297 297 id 'org.implab.gradle-variants-artifacts'
298 298 }
299 299
300 300 variants {
301 301 layer('main')
302 302 }
303 303
304 304 variantArtifacts {
305 305 variant('browser') {
306 306 slot('mainJs') {
307 307 fromVariant {
308 308 output('js')
309 309 }
310 310 }
311 311 }
312 312 }
313 313 """, "Variant artifact 'browser' references unknown variant 'browser'");
314 314 }
315 315
316 316 @Test
317 317 void failsOnUnknownRoleReference() throws Exception {
318 318 assertBuildFails("""
319 319 plugins {
320 320 id 'org.implab.gradle-variants-artifacts'
321 321 }
322 322
323 323 variants {
324 324 layer('main')
325 325
326 326 variant('browser') {
327 327 role('main') {
328 328 layers('main')
329 329 }
330 330 }
331 331 }
332 332
333 333 variantArtifacts {
334 334 variant('browser') {
335 335 slot('mainJs') {
336 336 fromRole('test') {
337 337 output('js')
338 338 }
339 339 }
340 340 }
341 341 }
342 342 """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'");
343 343 }
344 344
345 345 @Test
346 346 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
347 347 assertBuildFails("""
348 348 plugins {
349 349 id 'org.implab.gradle-variants-artifacts'
350 350 }
351 351
352 352 variants {
353 353 layer('main')
354 354
355 355 variant('browser') {
356 356 role('main') {
357 357 layers('main')
358 358 }
359 359 }
360 360 }
361 361
362 362 variantArtifacts {
363 363 variant('browser') {
364 364 slot('typesPackage') {
365 365 fromVariant {
366 366 output('types')
367 367 }
368 368 }
369 369
370 370 slot('js') {
371 371 fromVariant {
372 372 output('js')
373 373 }
374 374 }
375 375 }
376 376 }
377 377 """, "Variant artifact 'browser' must declare primary slot because it has multiple slots");
378 378 }
379 379
380 380 @Test
381 381 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
382 382 assertBuildFails("""
383 383 plugins {
384 384 id 'org.implab.gradle-variants-artifacts'
385 385 }
386 386
387 387 variants {
388 388 layer('mainBase')
389 389 layer('extra')
390 390
391 391 variant('browser') {
392 392 role('main') {
393 393 layers('mainBase')
394 394 }
395 395 }
396 396 }
397 397
398 398 variantArtifacts {
399 399 variant('browser') {
400 400 slot('extraJs') {
401 401 fromLayer('extra') {
402 402 output('js')
403 403 }
404 404 }
405 405 }
406 406 }
407 407 """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'");
408 408 }
409 409
410 410 @Test
411 411 void failsOnLateMutationAfterFinalize() throws Exception {
412 412 assertBuildFails("""
413 413 plugins {
414 414 id 'org.implab.gradle-variants-artifacts'
415 415 }
416 416
417 417 variants {
418 418 layer('main')
419 419
420 420 variant('browser') {
421 421 role('main') {
422 422 layers('main')
423 423 }
424 424 }
425 425 }
426 426
427 427 afterEvaluate {
428 428 variantArtifacts.variant('late') {
429 429 slot('js') {
430 430 fromVariant {
431 431 output('js')
432 432 }
433 433 }
434 434 }
435 435 }
436 436 """, "variantArtifacts model is finalized and cannot configure variants");
437 437 }
438 438
439 439 @Test
440 440 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
441 441 writeFile(SETTINGS_FILE, """
442 442 rootProject.name = 'variants-artifacts-fixture'
443 443 include 'producer', 'consumer'
444 444 """);
445 445 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
446 446 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
447 447 var buildscriptClasspath = pluginClasspath().stream()
448 448 .map(File::getAbsolutePath)
449 449 .map(path -> "'" + path.replace("\\", "\\\\") + "'")
450 450 .collect(Collectors.joining(", "));
451 451 writeFile(BUILD_FILE, """
452 452 buildscript {
453 453 dependencies {
454 454 classpath files(%s)
455 455 }
456 456 }
457 457
458 458 import org.gradle.api.attributes.Attribute
459 459
460 460 def variantAttr = Attribute.of('test.variant', String)
461 461 def slotAttr = Attribute.of('test.slot', String)
462 462
463 463 subprojects {
464 464 apply plugin: 'org.implab.gradle-variants-artifacts'
465 465 }
466 466
467 467 project(':producer') {
468 468 variants {
469 469 layer('main')
470 470
471 471 variant('browser') {
472 472 role('main') {
473 473 layers('main')
474 474 }
475 475 }
476 476 }
477 477
478 478 variantSources {
479 479 bind('main') {
480 480 configureSourceSet {
481 481 declareOutputs('types', 'js')
482 482 }
483 483 }
484 484
485 485 whenBound { ctx ->
486 486 ctx.configureSourceSet {
487 487 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
488 488 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
489 489 }
490 490 }
491 491 }
492 492
493 493 variantArtifacts {
494 494 variant('browser') {
495 495 primarySlot('typesPackage') {
496 496 fromVariant {
497 497 output('types')
498 498 }
499 499 }
500 500
501 501 slot('js') {
502 502 fromVariant {
503 503 output('js')
504 504 }
505 505 }
506 506 }
507 507
508 508 whenOutgoingVariant { publication ->
509 509 publication.configureConfiguration {
510 510 attributes.attribute(variantAttr, publication.variantName())
511 511 }
512 512
513 513 publication.primarySlot().configureArtifactAttributes {
514 514 attribute(slotAttr, publication.primarySlot().slotName())
515 515 }
516 516
517 517 publication.requireSlot('js').configureArtifactAttributes {
518 518 attribute(slotAttr, 'js')
519 519 }
520 520 }
521 521 }
522 522 }
523 523
524 524 project(':consumer') {
525 525 configurations {
526 526 compileView {
527 527 canBeResolved = true
528 528 canBeConsumed = false
529 529 canBeDeclared = true
530 530 attributes {
531 531 attribute(variantAttr, 'browser')
532 532 attribute(slotAttr, 'typesPackage')
533 533 }
534 534 }
535 535 }
536 536
537 537 dependencies {
538 538 compileView project(':producer')
539 539 }
540 540
541 541 tasks.register('probe') {
542 542 doLast {
543 543 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
544 544 def jsFiles = configurations.compileView.incoming.artifactView {
545 545 attributes {
546 546 attribute(slotAttr, 'js')
547 547 }
548 548 }.files.files.collect { it.name }.sort().join(',')
549 549
550 550 println('compileFiles=' + compileFiles)
551 551 println('jsFiles=' + jsFiles)
552 552 }
553 553 }
554 554 }
555 555 """.formatted(buildscriptClasspath));
556 556
557 557 BuildResult result = runner(":consumer:probe").build();
558 558
559 559 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
560 560 assertTrue(result.getOutput().contains("jsFiles=js"));
561 561 }
562 562
563 563 private GradleRunner runner(String... arguments) {
564 564 return GradleRunner.create()
565 565 .withProjectDir(testProjectDir.toFile())
566 566 .withPluginClasspath(pluginClasspath())
567 567 .withArguments(arguments)
568 568 .forwardOutput();
569 569 }
570 570
571 571 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
572 572 writeFile(SETTINGS_FILE, ROOT_NAME);
573 573 writeFile(BUILD_FILE, buildScript);
574 574
575 575 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
576 576 var output = ex.getBuildResult().getOutput();
577 577
578 578 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
579 579 }
580 580
581 581 private static List<File> pluginClasspath() {
582 582 try {
583 583 var classesDir = Path.of(VariantArtifactsPlugin.class
584 584 .getProtectionDomain()
585 585 .getCodeSource()
586 586 .getLocation()
587 587 .toURI());
588 588
589 589 var markerResource = VariantArtifactsPlugin.class.getClassLoader()
590 590 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
591 591
592 592 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
593 593
594 594 var markerPath = Path.of(markerResource.toURI());
595 595 var resourcesDir = markerPath.getParent().getParent().getParent();
596 596
597 597 return List.of(classesDir.toFile(), resourcesDir.toFile());
598 598 } catch (Exception e) {
599 599 throw new RuntimeException("Unable to build plugin classpath for test", e);
600 600 }
601 601 }
602 602
603 603 private void writeFile(String relativePath, String content) throws IOException {
604 604 Path path = testProjectDir.resolve(relativePath);
605 605 Files.createDirectories(path.getParent());
606 606 Files.writeString(path, content);
607 607 }
608 608 }
@@ -1,213 +1,248
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 5 import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
7 7 import java.io.File;
8 8 import java.io.IOException;
9 9 import java.nio.file.Files;
10 10 import java.nio.file.Path;
11 11 import java.util.List;
12 12
13 13 import org.gradle.testkit.runner.BuildResult;
14 14 import org.gradle.testkit.runner.GradleRunner;
15 15 import org.gradle.testkit.runner.TaskOutcome;
16 16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 17 import org.junit.jupiter.api.Test;
18 18 import org.junit.jupiter.api.io.TempDir;
19 19
20 20 class VariantsPluginFunctionalTest {
21 21 private static final String SETTINGS_FILE = "settings.gradle";
22 22 private static final String BUILD_FILE = "build.gradle";
23 23 private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n";
24 24
25 25 @TempDir
26 26 Path testProjectDir;
27 27
28 28 @Test
29 29 void configuresVariantModelWithDsl() throws Exception {
30 30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 31 writeFile(BUILD_FILE, """
32 32 plugins {
33 33 id 'org.implab.gradle-variants'
34 34 }
35 35
36 36 variants {
37 37 layer('mainBase') {
38 38 }
39 39
40 40 layer('mainAmd') {
41 41 }
42 42
43 43 variant('browser') {
44 44 role('main') {
45 45 layers('mainBase', 'mainAmd')
46 46 }
47 47 }
48 48 }
49 49
50 50 tasks.register('probe') {
51 51 doLast {
52 52 def browser = variants.require('browser')
53 53 println('roles=' + browser.roles.size())
54 54 println('roleLayers=' + browser.requireRole('main').layers.get().join(','))
55 55 }
56 56 }
57 57 """);
58 58
59 59 BuildResult result = runner("probe").build();
60 60
61 61 assertTrue(result.getOutput().contains("roles=1"));
62 62 assertTrue(result.getOutput().contains("roleLayers=mainBase,mainAmd"));
63 63 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
64 64 }
65 65
66 66 @Test
67 void supportsLayerRegistryDslAndLookupApi() throws Exception {
68 writeFile(SETTINGS_FILE, ROOT_NAME);
69 writeFile(BUILD_FILE, """
70 plugins {
71 id 'org.implab.gradle-variants'
72 }
73
74 variants {
75 layer('mainBase')
76 layer('mainAmd')
77
78 variant('browser') {
79 role('main') {
80 layers('mainBase', 'mainAmd')
81 }
82 }
83 }
84
85 tasks.register('probe') {
86 doLast {
87 println('layers=' + variants.layers.collect { it.name }.sort().join(','))
88 println('requireLayer=' + variants.requireLayer('mainBase').name)
89 println('findLayer=' + variants.findLayer('missing').isPresent())
90 }
91 }
92 """);
93
94 BuildResult result = runner("probe").build();
95
96 assertTrue(result.getOutput().contains("layers=mainAmd,mainBase"));
97 assertTrue(result.getOutput().contains("requireLayer=mainBase"));
98 assertTrue(result.getOutput().contains("findLayer=false"));
99 }
100
101 @Test
67 102 void failsOnUnknownLayerReference() throws Exception {
68 103 assertBuildFails("""
69 104 plugins {
70 105 id 'org.implab.gradle-variants'
71 106 }
72 107
73 108 variants {
74 109 layer('mainBase') {
75 110 }
76 111
77 112 variant('browser') {
78 113 role('main') {
79 114 layers('mainBase', 'missingLayer')
80 115 }
81 116 }
82 117 }
83 118 """, "references unknown layer 'missingLayer'");
84 119 }
85 120
86 121 @Test
87 122 void allowsUsingLayerFromDifferentVariantRole() throws Exception {
88 123 writeFile(SETTINGS_FILE, ROOT_NAME);
89 124 writeFile(BUILD_FILE, """
90 125 plugins {
91 126 id 'org.implab.gradle-variants'
92 127 }
93 128
94 129 variants {
95 130 layer('mainBase')
96 131
97 132 variant('browser') {
98 133 role('test') {
99 134 layers('mainBase')
100 135 }
101 136 }
102 137 }
103 138 """);
104 139
105 140 BuildResult result = runner("help").build();
106 141 assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
107 142 }
108 143
109 144 @Test
110 145 void failsOnDuplicatedLayerReferenceInRole() throws Exception {
111 146 assertBuildFails("""
112 147 plugins {
113 148 id 'org.implab.gradle-variants'
114 149 }
115 150
116 151 variants {
117 152 layer('a')
118 153
119 154 variant('browser') {
120 155 role('main') {
121 156 layers('a', 'a')
122 157 }
123 158 }
124 159 }
125 160 """, "contains duplicated layer reference 'a'");
126 161 }
127 162
128 163 @Test
129 164 void failsOnLateLayerMutationAfterFinalize() throws Exception {
130 165 assertBuildFails("""
131 166 plugins {
132 167 id 'org.implab.gradle-variants'
133 168 }
134 169
135 170 variants {
136 171 layer('a')
137 172 variant('browser') {
138 173 role('main') { layers('a') }
139 174 }
140 175 }
141 176
142 177 afterEvaluate {
143 178 variants.layer('late')
144 179 }
145 180 """, "Variants model is finalized and cannot configure layers");
146 181 }
147 182
148 183 @Test
149 184 void failsOnLateVariantMutationAfterFinalize() throws Exception {
150 185 assertBuildFails("""
151 186 plugins {
152 187 id 'org.implab.gradle-variants'
153 188 }
154 189
155 190 variants {
156 191 layer('a')
157 192 variant('browser') {
158 193 role('main') { layers('a') }
159 194 }
160 195 }
161 196
162 197 afterEvaluate {
163 198 variants.require('browser').role('late') { layers('a') }
164 199 }
165 200 """, "Variant 'browser' is finalized and cannot configure roles");
166 201 }
167 202
168 203 private GradleRunner runner(String... arguments) {
169 204 return GradleRunner.create()
170 205 .withProjectDir(testProjectDir.toFile())
171 206 .withPluginClasspath(pluginClasspath())
172 207 .withArguments(arguments)
173 208 .forwardOutput();
174 209 }
175 210
176 211 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
177 212 writeFile(SETTINGS_FILE, ROOT_NAME);
178 213 writeFile(BUILD_FILE, buildScript);
179 214
180 215 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
181 216 var output = ex.getBuildResult().getOutput();
182 217
183 218 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
184 219 }
185 220
186 221 private static List<File> pluginClasspath() {
187 222 try {
188 223 var classesDir = Path.of(BuildVariant.class
189 224 .getProtectionDomain()
190 225 .getCodeSource()
191 226 .getLocation()
192 227 .toURI());
193 228
194 229 var markerResource = VariantsPlugin.class.getClassLoader()
195 230 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
196 231
197 232 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
198 233
199 234 var markerPath = Path.of(markerResource.toURI());
200 235 var resourcesDir = markerPath.getParent().getParent().getParent();
201 236
202 237 return List.of(classesDir.toFile(), resourcesDir.toFile());
203 238 } catch (Exception e) {
204 239 throw new RuntimeException("Unable to build plugin classpath for test", e);
205 240 }
206 241 }
207 242
208 243 private void writeFile(String relativePath, String content) throws IOException {
209 244 Path path = testProjectDir.resolve(relativePath);
210 245 Files.createDirectories(path.getParent());
211 246 Files.writeString(path, content);
212 247 }
213 248 }
@@ -1,367 +1,444
1 1 package org.implab.gradle.common.sources;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 5 import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
7 7 import java.io.File;
8 8 import java.io.IOException;
9 9 import java.nio.file.Files;
10 10 import java.nio.file.Path;
11 11 import java.util.List;
12 12
13 13 import org.gradle.testkit.runner.BuildResult;
14 14 import org.gradle.testkit.runner.GradleRunner;
15 15 import org.gradle.testkit.runner.TaskOutcome;
16 16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 17 import org.junit.jupiter.api.Test;
18 18 import org.junit.jupiter.api.io.TempDir;
19 19
20 20 class VariantsSourcesPluginFunctionalTest {
21 21 private static final String SETTINGS_FILE = "settings.gradle";
22 22 private static final String BUILD_FILE = "build.gradle";
23 23 private static final String ROOT_NAME = "rootProject.name = 'variants-sources-fixture'\n";
24 24
25 25 @TempDir
26 26 Path testProjectDir;
27 27
28 28 @Test
29 29 void registersVariantSourceSetsAndFiresCallbacks() throws Exception {
30 30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 31 writeFile(BUILD_FILE, """
32 32 plugins {
33 33 id 'org.implab.gradle-variants-sources'
34 34 }
35 35
36 36 variants {
37 37 layer('mainBase')
38 38 layer('mainAmd')
39 39
40 40 variant('browser') {
41 41 role('main') { layers('mainBase', 'mainAmd') }
42 42 }
43 43
44 44 variant('node') {
45 45 role('main') { layers('mainBase') }
46 46 }
47 47 }
48 48
49 49 def events = []
50 50 def localEvents = []
51 51
52 52 variantSources {
53 53 bind('mainBase') {
54 54 configureSourceSet {
55 55 declareOutputs('compiled')
56 56 }
57 57 }
58 58 bind('mainAmd') {
59 59 configureSourceSet {
60 60 declareOutputs('compiled')
61 61 }
62 62 }
63 63 bind('mainAmd').whenRegistered { ctx ->
64 64 localEvents << "${ctx.layerName()}:${ctx.sourceSetName()}"
65 65 }
66 66 whenRegistered { ctx ->
67 67 events << "${ctx.layerName()}:${ctx.sourceSetName()}"
68 68 }
69 69 }
70 70
71 71 tasks.register('probe') {
72 72 doLast {
73 73 println("sources=" + sources.collect { it.name }.sort().join(','))
74 74 println("events=" + events.sort().join('|'))
75 75 println("local=" + localEvents.sort().join('|'))
76 76
77 77 def base = sources.getByName('browserMainBase')
78 78 def amd = sources.getByName('browserMainAmd')
79 79 def nodeBase = sources.getByName('nodeMainBase')
80 80
81 81 base.output('compiled')
82 82 amd.output('compiled')
83 83 nodeBase.output('compiled')
84 84
85 85 println('outputs=ok')
86 86 }
87 87 }
88 88 """);
89 89
90 90 BuildResult result = runner("probe").build();
91 91
92 92 assertTrue(result.getOutput().contains("sources=browserMainAmd,browserMainBase,nodeMainBase"));
93 93 assertTrue(result.getOutput().contains("events=mainAmd:browserMainAmd|mainBase:browserMainBase|mainBase:nodeMainBase"));
94 94 assertTrue(result.getOutput().contains("local=mainAmd:browserMainAmd"));
95 95 assertTrue(result.getOutput().contains("outputs=ok"));
96 96 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
97 97 }
98 98
99 99 @Test
100 100 void supportsTrailingClosureOnBind() throws Exception {
101 101 writeFile(SETTINGS_FILE, ROOT_NAME);
102 102 writeFile(BUILD_FILE, """
103 103 plugins {
104 104 id 'org.implab.gradle-variants-sources'
105 105 }
106 106
107 107 variants {
108 108 layer('main')
109 109 variant('browser') {
110 110 role('main') { layers('main') }
111 111 }
112 112 }
113 113
114 114 variantSources {
115 115 bind('main') {
116 116 configureSourceSet {
117 117 declareOutputs('compiled')
118 118 }
119 119 }
120 120 }
121 121
122 122 tasks.register('probe') {
123 123 doLast {
124 124 def ss = sources.getByName('browserMain')
125 125 ss.output('compiled')
126 126 println('bindClosure=ok')
127 127 }
128 128 }
129 129 """);
130 130
131 131 BuildResult result = runner("probe").build();
132 132 assertTrue(result.getOutput().contains("bindClosure=ok"));
133 133 }
134 134
135 135 @Test
136 void supportsBindingByBuildLayerIdentity() throws Exception {
137 writeFile(SETTINGS_FILE, ROOT_NAME);
138 writeFile(BUILD_FILE, """
139 plugins {
140 id 'org.implab.gradle-variants-sources'
141 }
142
143 variants {
144 layer('main')
145 variant('browser') {
146 role('main') { layers('main') }
147 }
148 }
149
150 variantSources {
151 bind(variants.requireLayer('main')) {
152 configureSourceSet {
153 declareOutputs('compiled')
154 }
155 }
156 }
157
158 tasks.register('probe') {
159 doLast {
160 def ss = sources.getByName('browserMain')
161 ss.output('compiled')
162 println('bindLayerIdentity=ok')
163 }
164 }
165 """);
166
167 BuildResult result = runner("probe").build();
168 assertTrue(result.getOutput().contains("bindLayerIdentity=ok"));
169 }
170
171 @Test
172 void exposesBindingsSnapshot() throws Exception {
173 writeFile(SETTINGS_FILE, ROOT_NAME);
174 writeFile(BUILD_FILE, """
175 plugins {
176 id 'org.implab.gradle-variants-sources'
177 }
178
179 variants {
180 layer('main')
181 layer('extra')
182 variant('browser') {
183 role('main') { layers('main') }
184 }
185 }
186
187 variantSources {
188 bind('main') {
189 sourceSetNamePattern = '{layer}'
190 configureSourceSet {
191 declareOutputs('compiled')
192 }
193 }
194 bind('extra')
195 }
196
197 tasks.register('probe') {
198 doLast {
199 def ss = sources.getByName('main')
200 ss.output('compiled')
201 println("bindings=" + variantSources.bindings.collect { it.name }.sort().join(','))
202 println('bindingsSnapshot=ok')
203 }
204 }
205 """);
206
207 BuildResult result = runner("probe").build();
208 assertTrue(result.getOutput().contains("bindings=extra,main"));
209 assertTrue(result.getOutput().contains("bindingsSnapshot=ok"));
210 }
211
212 @Test
136 213 void failsOnUnknownLayerBinding() throws Exception {
137 214 writeFile(SETTINGS_FILE, ROOT_NAME);
138 215 writeFile(BUILD_FILE, """
139 216 plugins {
140 217 id 'org.implab.gradle-variants-sources'
141 218 }
142 219
143 220 variants {
144 221 layer('main')
145 222 variant('browser') {
146 223 role('main') { layers('main') }
147 224 }
148 225 }
149 226
150 227 variantSources {
151 228 bind('missing')
152 229 }
153 230 """);
154 231
155 232 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
156 233 assertTrue(ex.getBuildResult().getOutput().contains("Layer binding 'missing' references unknown layer"));
157 234 }
158 235
159 236 @Test
160 237 void exposesProviderInSourceSetRegistration() throws Exception {
161 238 writeFile(SETTINGS_FILE, ROOT_NAME);
162 239 writeFile(BUILD_FILE, """
163 240 plugins {
164 241 id 'org.implab.gradle-variants-sources'
165 242 }
166 243
167 244 variants {
168 245 layer('main')
169 246 variant('browser') {
170 247 role('main') { layers('main') }
171 248 }
172 249 }
173 250
174 251 variantSources {
175 252 whenRegistered {
176 253 configureSourceSet {
177 254 declareOutputs('generated')
178 255 }
179 256 }
180 257 }
181 258
182 259 tasks.register('probe') {
183 260 doLast {
184 261 def ss = sources.getByName('browserMain')
185 262 ss.output('generated')
186 263 println('contextProvider=ok')
187 264 }
188 265 }
189 266 """);
190 267
191 268 BuildResult result = runner("probe").build();
192 269 assertTrue(result.getOutput().contains("contextProvider=ok"));
193 270 }
194 271
195 272 @Test
196 273 void replaysLateBindingsAndCallbacksAfterRegistration() throws Exception {
197 274 writeFile(SETTINGS_FILE, ROOT_NAME);
198 275 writeFile(BUILD_FILE, """
199 276 plugins {
200 277 id 'org.implab.gradle-variants-sources'
201 278 }
202 279
203 280 variants {
204 281 layer('main')
205 282 variant('browser') {
206 283 role('main') { layers('main') }
207 284 }
208 285 }
209 286
210 287 def events = []
211 288
212 289 afterEvaluate {
213 290 variantSources {
214 291 bind('main') {
215 292 configureSourceSet {
216 293 declareOutputs('late')
217 294 }
218 295 }
219 296
220 297 bind('main').whenRegistered { ctx ->
221 298 events << "layer:${ctx.sourceSetName()}"
222 299 }
223 300
224 301 whenRegistered { ctx ->
225 302 events << "global:${ctx.sourceSetName()}"
226 303 }
227 304 }
228 305 }
229 306
230 307 tasks.register('probe') {
231 308 doLast {
232 309 def ss = sources.getByName('browserMain')
233 310 ss.output('late')
234 311 println("events=" + events.sort().join('|'))
235 312 println('lateReplay=ok')
236 313 }
237 314 }
238 315 """);
239 316
240 317 BuildResult result = runner("probe").build();
241 318 assertTrue(result.getOutput().contains("events=global:browserMain|layer:browserMain"));
242 319 assertTrue(result.getOutput().contains("lateReplay=ok"));
243 320 }
244 321
245 322 @Test
246 323 void supportsSourceSetNamePatternAndSharedRegistration() throws Exception {
247 324 writeFile(SETTINGS_FILE, ROOT_NAME);
248 325 writeFile(BUILD_FILE, """
249 326 plugins {
250 327 id 'org.implab.gradle-variants-sources'
251 328 }
252 329
253 330 variants {
254 331 layer('main')
255 332
256 333 variant('browser') {
257 334 role('main') { layers('main') }
258 335 }
259 336
260 337 variant('node') {
261 338 role('main') { layers('main') }
262 339 }
263 340 }
264 341
265 342 def registeredEvents = []
266 343 def boundEvents = []
267 344 def browserBoundEvents = []
268 345 def localRegisteredEvents = []
269 346 def localBoundEvents = []
270 347
271 348 variantSources {
272 349 bind('main').sourceSetNamePattern = '{layer}'
273 350
274 351 bind('main') {
275 352 configureSourceSet {
276 353 declareOutputs('compiled')
277 354 }
278 355 }
279 356
280 357 bind('main') {
281 358 whenRegistered {
282 359 localRegisteredEvents << "${layerName()}:${sourceSetName()}"
283 360 }
284 361 }
285 362
286 363 bind('main') {
287 364 whenBound {
288 365 localBoundEvents << "${variantName()}:${roleName()}:${layerName()}:${sourceSetName()}"
289 366 }
290 367 }
291 368
292 369 whenRegistered { ctx ->
293 370 registeredEvents << "${ctx.layerName()}:${ctx.sourceSetName()}"
294 371 }
295 372
296 373 whenBound { ctx ->
297 374 boundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
298 375 }
299 376
300 377 whenBound('browser') { ctx ->
301 378 browserBoundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
302 379 }
303 380 }
304 381
305 382 tasks.register('probe') {
306 383 doLast {
307 384 println("sources=" + sources.collect { it.name }.sort().join(','))
308 385
309 386 def main = sources.getByName('main')
310 387 main.output('compiled')
311 388
312 389 println("registered=" + registeredEvents.sort().join('|'))
313 390 println("localRegistered=" + localRegisteredEvents.sort().join('|'))
314 391 println("bound=" + boundEvents.sort().join('|'))
315 392 println("browserBound=" + browserBoundEvents.sort().join('|'))
316 393 println("localBound=" + localBoundEvents.sort().join('|'))
317 394 println('sharedPattern=ok')
318 395 }
319 396 }
320 397 """);
321 398
322 399 BuildResult result = runner("probe").build();
323 400 assertTrue(result.getOutput().contains("sources=main"));
324 401 assertTrue(result.getOutput().contains("registered=main:main"));
325 402 assertTrue(result.getOutput().contains("localRegistered=main:main"));
326 403 assertTrue(result.getOutput().contains("bound=browser:main:main:main|node:main:main:main"));
327 404 assertTrue(result.getOutput().contains("browserBound=browser:main:main:main"));
328 405 assertTrue(result.getOutput().contains("localBound=browser:main:main:main|node:main:main:main"));
329 406 assertTrue(result.getOutput().contains("sharedPattern=ok"));
330 407 }
331 408
332 409 private GradleRunner runner(String... arguments) {
333 410 return GradleRunner.create()
334 411 .withProjectDir(testProjectDir.toFile())
335 412 .withPluginClasspath(pluginClasspath())
336 413 .withArguments(arguments)
337 414 .forwardOutput();
338 415 }
339 416
340 417 private static List<File> pluginClasspath() {
341 418 try {
342 419 var classesDir = Path.of(VariantsSourcesPlugin.class
343 420 .getProtectionDomain()
344 421 .getCodeSource()
345 422 .getLocation()
346 423 .toURI());
347 424
348 425 var markerResource = VariantsSourcesPlugin.class.getClassLoader()
349 426 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties");
350 427
351 428 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
352 429
353 430 var markerPath = Path.of(markerResource.toURI());
354 431 var resourcesDir = markerPath.getParent().getParent().getParent();
355 432
356 433 return List.of(classesDir.toFile(), resourcesDir.toFile());
357 434 } catch (Exception e) {
358 435 throw new RuntimeException("Unable to build plugin classpath for test", e);
359 436 }
360 437 }
361 438
362 439 private void writeFile(String relativePath, String content) throws IOException {
363 440 Path path = testProjectDir.resolve(relativePath);
364 441 Files.createDirectories(path.getParent());
365 442 Files.writeString(path, content);
366 443 }
367 444 }
@@ -1,155 +1,159
1 1 # Variant Sources Plugin
2 2
3 3 ## NAME
4 4
5 5 `VariantsSourcesPlugin` и extension `variantSources`.
6 6
7 7 ## SYNOPSIS
8 8
9 9 ```groovy
10 10 plugins {
11 11 id 'org.implab.gradle-variants-sources'
12 12 }
13 13
14 14 variants {
15 15 layer('main')
16 16
17 17 variant('browser') {
18 18 role('main') { layers('main') }
19 19 }
20 20
21 21 variant('node') {
22 22 role('main') { layers('main') }
23 23 }
24 24 }
25 25
26 26 variantSources {
27 27 bind('main').sourceSetNamePattern = '{layer}'
28 28
29 29 bind('main') {
30 30 configureSourceSet {
31 31 declareOutputs('compiled')
32 32 }
33 33 }
34 34
35 35 whenRegistered { sourceSetName() }
36 36 whenBound('browser') { roleName() }
37 37 }
38 38 ```
39 39
40 40 ## DESCRIPTION
41 41
42 42 `VariantsSourcesPlugin` применяет `VariantsPlugin` и `SourcesPlugin`, затем
43 43 регистрирует source sets из модели `variants`.
44 44
45 45 Точка запуска registration:
46 46
47 47 - `variants.whenFinalized(model -> registerSourceSets(...))`
48 48
49 49 ### registration
50 50
51 51 Для каждой usage-связки `variant/role/layer` вычисляется имя source set,
52 52 регистрируется `GenericSourceSet` (если он еще не существует), затем
53 53 вызываются callbacks.
54 54
55 55 ### binding
56 56
57 `bind('<layer>')` возвращает `BuildLayerBinding` и задает policy для этого
57 `bind('<layer>')` возвращает `LayerBindingSpec` и задает policy для этого
58 58 слоя:
59 59
60 60 - как именовать source set;
61 61 - как конфигурировать source set;
62 62 - какие callbacks вызвать на registration/binding.
63 63
64 64 ### sourceSetNamePattern
65 65
66 66 `sourceSetNamePattern` определяет naming policy зарегистрированного source set.
67 67
68 68 Default:
69 69
70 70 - `{variant}{layerCap}`
71 71
72 72 Tokens:
73 73
74 74 - `{variant}`, `{variantCap}`
75 75 - `{role}`, `{roleCap}`
76 76 - `{layer}`, `{layerCap}`
77 77
78 78 Имя санитизируется (`[^A-Za-z0-9_.-] -> _`).
79 79
80 80 Ограничение:
81 81
82 82 - один `sourceSetName` не может быть порожден разными слоями.
83 83
84 84 ## EVENTS
85 85
86 86 ### whenRegistered
87 87
88 88 - callback на новый уникальный source set;
89 89 - replayable;
90 90 - при shared source set срабатывает один раз.
91 91
92 92 ### whenBound
93 93
94 94 - callback на каждую usage-связку `variant/role/layer`;
95 95 - replayable;
96 96 - подходит для per-usage логики.
97 97
98 98 ### variant filter
99 99
100 100 Фильтр по варианту поддерживает только usage-binding:
101 101
102 102 - `whenBound(String variantName, ...)`
103 103
104 104 ## PAYLOAD TYPES
105 105
106 106 `SourceSetRegistration` содержит:
107 107
108 108 - `layerName`, `sourceSetName`;
109 109 - `sourceSet` (`NamedDomainObjectProvider<GenericSourceSet>`).
110 110
111 111 Sugar:
112 112
113 113 - `configureSourceSet(Action|Closure)`.
114 114
115 115 `SourceSetUsageBinding` содержит:
116 116
117 117 - `variantName`, `roleName`, `layerName`, `sourceSetName`;
118 118 - `sourceSet` (`NamedDomainObjectProvider<GenericSourceSet>`).
119 119
120 120 Sugar:
121 121
122 122 - `configureSourceSet(Action|Closure)`.
123 123
124 124 ## API
125 125
126 126 ### VariantSourcesExtension
127 127
128 - `bind(BuildLayer)` — получить/создать binding для canonical layer identity.
128 129 - `bind(String)` — получить/создать binding по имени слоя.
129 130 - `bind(String, Action|Closure)` — сконфигурировать binding.
130 - `bindings(Action|Closure)`контейнерная конфигурация bindings.
131 - `bind(BuildLayer, Action|Closure)`сконфигурировать binding по `BuildLayer`.
132 - `getBindings()` — read-only snapshot текущих bindings.
131 133 - `whenRegistered(...)` — глобальные callbacks регистрации source set.
132 134 - `whenBound(...)` — глобальные callbacks usage-binding.
133 135 - `whenBound(String variantName, ...)` — usage-binding callbacks с variant-filter.
134 136
135 ### BuildLayerBinding
137 ### LayerBindingSpec
136 138
137 139 - `sourceSetNamePattern` — naming policy для source set слоя.
138 140 - `configureSourceSet(...)` — слойная конфигурация `GenericSourceSet`.
139 141 - `whenRegistered(...)` — callbacks регистрации в рамках слоя.
140 142 - `whenBound(...)` — callbacks usage-binding в рамках слоя.
141 143
142 144 ## KEY CLASSES
143 145
144 146 - `VariantsSourcesPlugin` — точка входа plugin adapter.
145 147 - `VariantSourcesExtension` — глобальный DSL bind/events.
146 - `BuildLayerBinding` — layer-local policy и callbacks.
148 - `LayerBindingSpec`публичный DSL-contract layer-local policy/callbacks.
147 149 - `SourceSetRegistration` — payload регистрации source set.
148 150 - `SourceSetUsageBinding` — payload usage-binding.
149 151
150 152 ## NOTES
151 153
152 154 - `sourceSetNamePattern` фиксируется при первом чтении в registration
153 155 (`finalizeValueOnRead`).
156 - runtime state bindings скрыт внутри adapter implementation (`LayerBinding`).
157 - name-based bindings резолвятся к canonical `BuildLayer` через registry `variants`.
154 158 - Closure callbacks используют delegate-first.
155 159 - Для вложенных closure лучше явный параметр (`ctx -> ...`).
@@ -1,110 +1,112
1 1 # Variants Plugin
2 2
3 3 ## NAME
4 4
5 5 `VariantsPlugin` и extension `variants`.
6 6
7 7 ## SYNOPSIS
8 8
9 9 ```groovy
10 10 plugins {
11 11 id 'org.implab.gradle-variants'
12 12 }
13 13
14 14 variants {
15 15 layer('mainBase')
16 16 layer('mainAmd')
17 17
18 18 variant('browser') {
19 19 attributes {
20 20 string('jsRuntime', 'browser')
21 21 string('jsModule', 'amd')
22 22 }
23 23
24 24 role('main') {
25 25 layers('mainBase', 'mainAmd')
26 26 }
27 27
28 28 artifactSlot('mainCompiled')
29 29 }
30 30 }
31 31 ```
32 32
33 33 ## DESCRIPTION
34 34
35 35 `VariantsPlugin` задает доменную модель сборки и ее валидацию. Плагин не
36 36 регистрирует compile/copy/bundle задачи напрямую.
37 37
38 38 ### layers
39 39
40 40 Глобальные логические слои. Служат единым словарем имен, на которые затем
41 41 ссылаются роли.
42 42
43 43 ### variants
44 44
45 45 Именованные варианты исполнения/пакетирования (`browser`, `node`, и т.д.).
46 46 Вариант агрегирует роли, атрибуты и artifact slots.
47 47
48 48 ### roles
49 49
50 50 Роль описывает набор слоев в пределах варианта (`main`, `test`, `tools`).
51 51 Одна роль может ссылаться на несколько слоев.
52 52
53 53 ### attributes
54 54
55 55 Typed-атрибуты (`Attribute<T> -> Provider<T>`) для передачи параметров в
56 56 адаптеры и публикацию артефактов.
57 57
58 58 ### artifact slots
59 59
60 60 Именованные слоты ожидаемых артефактов варианта. Используются как контракт
61 61 между моделью варианта и плагинами, создающими/публикующими результаты.
62 62
63 63 ## VALIDATION
64 64
65 65 В `finalizeModel()` выполняется проверка:
66 66
67 67 - роль не может ссылаться на неизвестный layer;
68 68 - пустые имена layer запрещены;
69 69 - имена ролей в варианте должны быть уникальны;
70 70 - имена artifact slots в варианте должны быть уникальны.
71 71
72 72 ## LIFECYCLE
73 73
74 74 - `VariantsPlugin` вызывает `variants.finalizeModel()` на `afterEvaluate`.
75 75 - после `finalizeModel()` мутации модели запрещены.
76 76 - `whenFinalized(...)` replayable.
77 77
78 78 ## API
79 79
80 80 ### BuildVariantsExtension
81 81
82 82 - `layer(...)` — объявление или конфигурация `BuildLayer`.
83 83 - `variant(...)` — объявление или конфигурация `BuildVariant`.
84 - `layers { ... }`, `variants { ... }` — контейнерный DSL.
84 - `layers { layer(...) }`, `variants { ... }` — grouped DSL без публикации container API наружу.
85 85 - `all(...)` — callback для всех вариантов.
86 - `findLayer(name)`, `requireLayer(name)`, `getAllLayers()` — доступ к registry слоев.
86 87 - `getAll()`, `find(name)`, `require(name)` — доступ к вариантам.
87 88 - `validate()` — явный запуск валидации.
88 89 - `finalizeModel()` — валидация + финализация модели.
89 90 - `whenFinalized(...)` — callback по завершенной модели (replayable).
90 91
91 92 ### BuildVariant
92 93
93 94 - `attributes { ... }` — атрибуты варианта (+ sugar `string/bool/integer`).
94 95 - `role(...)`, `roles { ... }` — роли варианта.
95 96 - `artifactSlot(...)`, `artifactSlots { ... }` — артефактные слоты.
96 97
97 98 ## KEY CLASSES
98 99
99 100 - `VariantsPlugin` — точка входа плагина.
100 101 - `BuildVariantsExtension` — root extension и lifecycle.
101 102 - `BuildVariant` — агрегатная модель варианта.
102 - `BuildLayer`модель слоя.
103 - `BuildLayer`canonical identity-model слоя.
103 104 - `BuildRole` — модель роли.
104 105 - `BuildArtifactSlot` — модель артефактного слота.
105 106 - `VariantAttributes` — typed wrapper для variant attributes.
106 107
107 108 ## NOTES
108 109
109 110 - Модель `variants` intentionally agnostic к toolchain.
111 - Layer registry хранится как явная identity-map, а не как публичный `NamedDomainObjectContainer`.
110 112 - Интеграция с задачами выполняется через `variantSources` и адаптеры.
@@ -1,23 +1,24
1 1 /*
2 2 * This settings file was generated by the Gradle 'init' task.
3 3 *
4 4 * The settings file is used to specify which projects to include in your build.
5 5 * In a single project build this file can be empty or even removed.
6 6 *
7 7 * Detailed information about configuring a multi-project build in Gradle can be found
8 8 * in the user guide at https://docs.gradle.org/3.5/userguide/multi_project_builds.html
9 9 */
10 10
11 11 dependencyResolutionManagement {
12 12 repositories {
13 13 mavenCentral()
14 14 mavenLocal()
15 15 ivy {
16 16 url "${System.properties["user.home"]}/ivy-repo"
17 17 }
18 18 }
19 19 }
20 20
21 21 rootProject.name = 'gradle-common'
22 22
23 23 include 'common'
24 include 'variants'
General Comments 0
You need to be logged in to leave comments. Login now