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