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