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