##// END OF EJS Templates
Keep contribution-based assemblies on managed Sync tasks, register direct producers through ArtifactAssemblyRegistry, wire builtBy on outgoing artifacts, and cover the behavior with functional tests.
cin -
r61:9b11838beca6 default
parent child
Show More
@@ -1,296 +1,306
1 # gradle-common
1 # gradle-common
2
2
3 Java 21 multi-project build with shared Gradle utilities and experimental
3 Java 21 multi-project build with shared Gradle utilities and experimental
4 variant-oriented Gradle plugins.
4 variant-oriented Gradle plugins.
5
5
6 The repository currently publishes to a local Ivy repository only. Maven Central
6 The repository currently publishes to a local Ivy repository only. Maven Central
7 and Gradle Plugin Portal publication are intentionally not configured yet.
7 and Gradle Plugin Portal publication are intentionally not configured yet.
8
8
9 ## Modules
9 ## Modules
10
10
11 - `common` - shared Gradle utilities, JSON helpers, shell execution helpers, and
11 - `common` - shared Gradle utilities, JSON helpers, shell execution helpers, and
12 small core value/util classes.
12 small core value/util classes.
13 - `variants` - Gradle plugins for variant topology, source-set materialization,
13 - `variants` - Gradle plugins for variant topology, source-set materialization,
14 and outgoing artifact slots.
14 and outgoing artifact slots.
15
15
16 ## Requirements
16 ## Requirements
17
17
18 - JDK 21.
18 - JDK 21.
19 - Gradle Wrapper from this repository, currently Gradle 8.10.2.
19 - Gradle Wrapper from this repository, currently Gradle 8.10.2.
20
20
21 The produced bytecode targets Java 21.
21 The produced bytecode targets Java 21.
22
22
23 ## License
23 ## License
24
24
25 This project is licensed under the BSD 2-Clause "Simplified" License
25 This project is licensed under the BSD 2-Clause "Simplified" License
26 (`BSD-2-Clause`). See [LICENSE](LICENSE).
26 (`BSD-2-Clause`). See [LICENSE](LICENSE).
27
27
28 ## Local Build
28 ## Local Build
29
29
30 ```bash
30 ```bash
31 ./gradlew clean check javadoc jar sourcesJar javadocJar --rerun-tasks
31 ./gradlew clean check javadoc jar sourcesJar javadocJar --rerun-tasks
32 ```
32 ```
33
33
34 Configuration cache smoke check:
34 Configuration cache smoke check:
35
35
36 ```bash
36 ```bash
37 ./gradlew check --configuration-cache
37 ./gradlew check --configuration-cache
38 ```
38 ```
39
39
40 ## Local Ivy Publication
40 ## Local Ivy Publication
41
41
42 The current publication target is:
42 The current publication target is:
43
43
44 ```text
44 ```text
45 ${user.home}/ivy-repo
45 ${user.home}/ivy-repo
46 ```
46 ```
47
47
48 Publish both modules locally:
48 Publish both modules locally:
49
49
50 ```bash
50 ```bash
51 ./gradlew :common:publishIvyPublicationToIvyRepository \
51 ./gradlew :common:publishIvyPublicationToIvyRepository \
52 :variants:publishIvyPublicationToIvyRepository
52 :variants:publishIvyPublicationToIvyRepository
53 ```
53 ```
54
54
55 or:
55 or:
56
56
57 ```bash
57 ```bash
58 ./gradlew publish
58 ./gradlew publish
59 ```
59 ```
60
60
61 The publication includes:
61 The publication includes:
62
62
63 - main jar
63 - main jar
64 - sources jar
64 - sources jar
65 - javadoc jar
65 - javadoc jar
66 - Ivy descriptor
66 - Ivy descriptor
67 - Gradle module metadata
67 - Gradle module metadata
68
68
69 ## Local Consumption
69 ## Local Consumption
70
70
71 Current plugin ids are packaged as classic Gradle plugin marker resources inside
71 Current plugin ids are packaged as classic Gradle plugin marker resources inside
72 the `variants` jar:
72 the `variants` jar:
73
73
74 - `org.implab.gradle-variants`
74 - `org.implab.gradle-variants`
75 - `org.implab.gradle-sources`
75 - `org.implab.gradle-sources`
76 - `org.implab.gradle-variants-sources`
76 - `org.implab.gradle-variants-sources`
77 - `org.implab.gradle-variants-artifacts`
77 - `org.implab.gradle-variants-artifacts`
78
78
79 Until Gradle Plugin Portal marker artifacts are configured, consume the plugin
79 Until Gradle Plugin Portal marker artifacts are configured, consume the plugin
80 through `buildscript` classpath:
80 through `buildscript` classpath:
81
81
82 ```groovy
82 ```groovy
83 buildscript {
83 buildscript {
84 repositories {
84 repositories {
85 ivy {
85 ivy {
86 url "${System.properties['user.home']}/ivy-repo"
86 url "${System.properties['user.home']}/ivy-repo"
87 }
87 }
88 mavenCentral()
88 mavenCentral()
89 }
89 }
90 dependencies {
90 dependencies {
91 classpath 'org.implab.gradle:variants:0.1.0'
91 classpath 'org.implab.gradle:variants:0.1.0'
92 }
92 }
93 }
93 }
94
94
95 apply plugin: 'org.implab.gradle-variants'
95 apply plugin: 'org.implab.gradle-variants'
96 apply plugin: 'org.implab.gradle-variants-sources'
96 apply plugin: 'org.implab.gradle-variants-sources'
97 ```
97 ```
98
98
99 The `plugins { id(...) version(...) }` DSL is not part of the current local Ivy
99 The `plugins { id(...) version(...) }` DSL is not part of the current local Ivy
100 contract.
100 contract.
101
101
102 ## Variants DSL
102 ## Variants DSL
103
103
104 `variants` defines the finalized build topology. It does not create compile
104 `variants` defines the finalized build topology. It does not create compile
105 tasks, source directories, or outgoing publications by itself.
105 tasks, source directories, or outgoing publications by itself.
106
106
107 ```groovy
107 ```groovy
108 apply plugin: 'org.implab.gradle-variants'
108 apply plugin: 'org.implab.gradle-variants'
109
109
110 variants.layers.create('main')
110 variants.layers.create('main')
111 variants.layers.create('test')
111 variants.layers.create('test')
112 variants.roles.create('main')
112 variants.roles.create('main')
113 variants.roles.create('test')
113 variants.roles.create('test')
114
114
115 variants.variant('browser') {
115 variants.variant('browser') {
116 role('main') {
116 role('main') {
117 layers('main')
117 layers('main')
118 }
118 }
119 role('test') {
119 role('test') {
120 layers('main', 'test')
120 layers('main', 'test')
121 }
121 }
122 }
122 }
123
123
124 variants.whenFinalized { view ->
124 variants.whenFinalized { view ->
125 println view.entries.collect {
125 println view.entries.collect {
126 "${it.variant().name}:${it.role().name}:${it.layer().name}"
126 "${it.variant().name}:${it.role().name}:${it.layer().name}"
127 }.sort()
127 }.sort()
128 }
128 }
129 ```
129 ```
130
130
131 The finalized model exposes cheap identity objects: `Variant`, `Role`, `Layer`,
131 The finalized model exposes cheap identity objects: `Variant`, `Role`, `Layer`,
132 and the normalized relation `(variant, role, layer)`.
132 and the normalized relation `(variant, role, layer)`.
133
133
134 ## Sources DSL
134 ## Sources DSL
135
135
136 `sources` creates standalone `GenericSourceSet` objects. This is useful for
136 `sources` creates standalone `GenericSourceSet` objects. This is useful for
137 fallback workflows that do not need variant topology.
137 fallback workflows that do not need variant topology.
138
138
139 ```groovy
139 ```groovy
140 apply plugin: 'org.implab.gradle-sources'
140 apply plugin: 'org.implab.gradle-sources'
141
141
142 sources.create('main') {
142 sources.create('main') {
143 declareOutputs('js')
143 declareOutputs('js')
144 registerOutput('js', layout.projectDirectory.file('inputs/main.js'))
144 registerOutput('js', layout.projectDirectory.file('inputs/main.js'))
145
145
146 sets.create('ts') {
146 sets.create('ts') {
147 srcDir 'src/main/ts'
147 srcDir 'src/main/ts'
148 }
148 }
149 }
149 }
150 ```
150 ```
151
151
152 `SourcesPlugin` applies layout conventions:
152 `SourcesPlugin` applies layout conventions:
153
153
154 - `sourceSetDir = src/<sourceSet>`
154 - `sourceSetDir = src/<sourceSet>`
155 - `outputsDir = build/out/<sourceSet>`
155 - `outputsDir = build/out/<sourceSet>`
156
156
157 The base `GenericSourceSet` model itself is convention-free.
157 The base `GenericSourceSet` model itself is convention-free.
158
158
159 ## Variant Sources DSL
159 ## Variant Sources DSL
160
160
161 `variantSources` derives compile units from finalized `variants`.
161 `variantSources` derives compile units from finalized `variants`.
162
162
163 A compile unit is:
163 A compile unit is:
164
164
165 ```text
165 ```text
166 (variant, layer)
166 (variant, layer)
167 ```
167 ```
168
168
169 Selectors configure materialized compile-unit source sets:
169 Selectors configure materialized compile-unit source sets:
170
170
171 ```groovy
171 ```groovy
172 apply plugin: 'org.implab.gradle-variants-sources'
172 apply plugin: 'org.implab.gradle-variants-sources'
173
173
174 variants.layers.create('main')
174 variants.layers.create('main')
175 variants.roles.create('main')
175 variants.roles.create('main')
176 variants.variant('browser') {
176 variants.variant('browser') {
177 role('main') {
177 role('main') {
178 layers('main')
178 layers('main')
179 }
179 }
180 }
180 }
181
181
182 variantSources {
182 variantSources {
183 layer('main') {
183 layer('main') {
184 sourceSet {
184 sourceSet {
185 declareOutputs('js')
185 declareOutputs('js')
186 registerOutput('js', layout.projectDirectory.file('inputs/browser.js'))
186 registerOutput('js', layout.projectDirectory.file('inputs/browser.js'))
187 }
187 }
188 }
188 }
189
189
190 configureEach {
190 configureEach {
191 println "sourceSet=${sourceSet.name}, variant=${variant.name}, layer=${layer.name}"
191 println "sourceSet=${sourceSet.name}, variant=${variant.name}, layer=${layer.name}"
192 }
192 }
193 }
193 }
194 ```
194 ```
195
195
196 Selector order for future materialization is:
196 Selector order for future materialization is:
197
197
198 ```text
198 ```text
199 configureEach -> variant -> layer -> unit
199 configureEach -> variant -> layer -> unit
200 ```
200 ```
201
201
202 Late selector registration is controlled by:
202 Late selector registration is controlled by:
203
203
204 ```groovy
204 ```groovy
205 variantSources {
205 variantSources {
206 lateConfigurationPolicy {
206 lateConfigurationPolicy {
207 failOnLateConfiguration()
207 failOnLateConfiguration()
208 }
208 }
209 }
209 }
210 ```
210 ```
211
211
212 Compile-unit source set names are generated by default as:
212 Compile-unit source set names are generated by default as:
213
213
214 ```text
214 ```text
215 <variant> + capitalize(<layer>)
215 <variant> + capitalize(<layer>)
216 ```
216 ```
217
217
218 Name collisions fail by default and may be resolved deterministically:
218 Name collisions fail by default and may be resolved deterministically:
219
219
220 ```groovy
220 ```groovy
221 variantSources {
221 variantSources {
222 namingPolicy {
222 namingPolicy {
223 resolveNameCollision()
223 resolveNameCollision()
224 }
224 }
225 }
225 }
226 ```
226 ```
227
227
228 ## Variant Artifacts DSL
228 ## Variant Artifacts DSL
229
229
230 `variantArtifacts` is an experimental outgoing artifact layer over `variants`
230 `variantArtifacts` is an experimental outgoing artifact layer over `variants`
231 and `variantSources`.
231 and `variantSources`.
232
232
233 The current model maps:
233 The current model maps:
234
234
235 - `Variant` to a variant-level consumable outgoing configuration.
235 - `Variant` to a variant-level consumable outgoing configuration.
236 - `Slot` to a Gradle outgoing artifact variant inside that configuration.
236 - `Slot` to a Gradle outgoing artifact variant inside that configuration.
237 - `primarySlot` to the primary artifact set of the outgoing configuration.
237 - `primarySlot` to the primary artifact set of the outgoing configuration.
238
238
239 ```groovy
239 ```groovy
240 apply plugin: 'org.implab.gradle-variants-artifacts'
240 apply plugin: 'org.implab.gradle-variants-artifacts'
241
241
242 variants.layers.create('main')
242 variants.layers.create('main')
243 variants.roles.create('main')
243 variants.roles.create('main')
244 variants.variant('browser') {
244 variants.variant('browser') {
245 role('main') {
245 role('main') {
246 layers('main')
246 layers('main')
247 }
247 }
248 }
248 }
249
249
250 variantSources.layer('main') {
250 variantSources.layer('main') {
251 sourceSet {
251 sourceSet {
252 declareOutputs('types', 'js')
252 declareOutputs('types', 'js')
253 registerOutput('types', layout.projectDirectory.file('inputs/index.d.ts'))
253 registerOutput('types', layout.projectDirectory.file('inputs/index.d.ts'))
254 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
254 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
255 }
255 }
256 }
256 }
257
257
258 variantArtifacts {
258 variantArtifacts {
259 variant('browser') {
259 variant('browser') {
260 primarySlot('typesPackage') {
260 primarySlot('typesPackage') {
261 fromVariant {
261 fromVariant {
262 output('types')
262 output('types')
263 }
263 }
264 }
264 }
265 slot('js') {
265 slot('js') {
266 fromVariant {
266 fromVariant {
267 output('js')
267 output('js')
268 }
268 }
269 }
269 }
270 }
270 }
271
271
272 whenOutgoingConfiguration { publication ->
272 whenOutgoingConfiguration { publication ->
273 publication.configuration {
273 publication.configuration {
274 description = "Outgoing contract for ${publication.variant.name}"
274 description = "Outgoing contract for ${publication.variant.name}"
275 }
275 }
276 }
276 }
277
277
278 whenOutgoingSlot { publication ->
278 whenOutgoingSlot { publication ->
279 println "slot=${publication.artifactSlot.slot.name}, primary=${publication.primary}"
279 println "slot=${publication.artifactSlot.slot.name}, primary=${publication.primary}"
280 }
280 }
281 }
281 }
282 ```
282 ```
283
283
284 Slot bodies have two assembly modes:
285
286 - contribution-based assembly with `from(...)`, `fromVariant(...)`,
287 `fromRole(...)`, or `fromLayer(...)`; the plugin copies selected inputs into a
288 managed directory and publishes that directory;
289 - task-produced assembly with `producedBy(task) { outputFile }`; the mapped task
290 output file or directory is published directly.
291
292 These modes are mutually exclusive for one slot.
293
284 The artifact API is still considered pre-1.0 and may change.
294 The artifact API is still considered pre-1.0 and may change.
285
295
286 ## Publication Status
296 ## Publication Status
287
297
288 Current status:
298 Current status:
289
299
290 - local Ivy publication only
300 - local Ivy publication only
291 - no Maven Central publication metadata
301 - no Maven Central publication metadata
292 - no signing
302 - no signing
293 - no Gradle Plugin Portal marker artifacts
303 - no Gradle Plugin Portal marker artifacts
294 - BSD-2-Clause license committed
304 - BSD-2-Clause license committed
295
305
296 Before external publication, see [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md).
306 Before external publication, see [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md).
@@ -1,698 +1,710
1 # Variants and Variant Artifacts
1 # Variants and Variant Artifacts
2
2
3 > Design note. The current user-facing DSL and local publication workflow are
3 > Design note. The current user-facing DSL and local publication workflow are
4 > documented in [README.md](README.md). Some snippets below are conceptual and
4 > documented in [README.md](README.md). Some snippets below are conceptual and
5 > describe model direction rather than exact public API.
5 > describe model direction rather than exact public API.
6
6
7 ## Overview
7 ## Overview
8
8
9 This document describes the artifact model built on top of `variants` and
9 This document describes the artifact model built on top of `variants` and
10 `variantSources`.
10 `variantSources`.
11
11
12 The goal is to define:
12 The goal is to define:
13
13
14 - a stable artifact-facing model for outgoing contracts;
14 - a stable artifact-facing model for outgoing contracts;
15 - a DSL for declaring slots and their inputs;
15 - a DSL for declaring slots and their inputs;
16 - a resolver bridge between `variantArtifacts` and `variantSources`;
16 - a resolver bridge between `variantArtifacts` and `variantSources`;
17 - extension points for deduplication and similar policies;
17 - extension points for deduplication and similar policies;
18 - a model that remains live during configuration and does not require an
18 - a model that remains live during configuration and does not require an
19 artificial freeze of slot content.
19 artificial freeze of slot content.
20
20
21 The design follows the same split already used elsewhere:
21 The design follows the same split already used elsewhere:
22
22
23 - `variants` defines the closed domain topology;
23 - `variants` defines the closed domain topology;
24 - `variantSources` defines source materialization semantics over that topology;
24 - `variantSources` defines source materialization semantics over that topology;
25 - `variantArtifacts` defines outgoing artifact contracts over that topology.
25 - `variantArtifacts` defines outgoing artifact contracts over that topology.
26
26
27 ---
27 ---
28
28
29 ## Core idea
29 ## Core idea
30
30
31 `variantArtifacts` is not the owner of the variant model and not the owner of
31 `variantArtifacts` is not the owner of the variant model and not the owner of
32 source-set materialization.
32 source-set materialization.
33
33
34 Its purpose is narrower:
34 Its purpose is narrower:
35
35
36 - decide which variants participate in outgoing publication;
36 - decide which variants participate in outgoing publication;
37 - define artifact slots for those outgoing variants;
37 - define artifact slots for those outgoing variants;
38 - declare how each slot gathers its inputs.
38 - declare how each slot gathers its inputs.
39
39
40 This makes `variantArtifacts` an outgoing-contract layer, not a compilation or
40 This makes `variantArtifacts` an outgoing-contract layer, not a compilation or
41 source-materialization layer.
41 source-materialization layer.
42
42
43 ---
43 ---
44
44
45 ## Model boundaries
45 ## Model boundaries
46
46
47 ### What belongs to `variants`
47 ### What belongs to `variants`
48
48
49 - identities: `Variant`, `Role`, `Layer`;
49 - identities: `Variant`, `Role`, `Layer`;
50 - normalized topology relation `(variant, role, layer)`;
50 - normalized topology relation `(variant, role, layer)`;
51 - finalization of the core domain model;
51 - finalization of the core domain model;
52 - `VariantsView` and derived topology views.
52 - `VariantsView` and derived topology views.
53
53
54 ### What belongs to `variantSources`
54 ### What belongs to `variantSources`
55
55
56 - compile units and role projections derived from finalized `variants`;
56 - compile units and role projections derived from finalized `variants`;
57 - source-set naming policy;
57 - source-set naming policy;
58 - source-set materialization;
58 - source-set materialization;
59 - lazy access to `GenericSourceSet` providers and their named outputs.
59 - lazy access to `GenericSourceSet` providers and their named outputs.
60
60
61 ### What belongs to `variantArtifacts`
61 ### What belongs to `variantArtifacts`
62
62
63 - selection of outgoing variants;
63 - selection of outgoing variants;
64 - slot identities inside an outgoing variant;
64 - slot identities inside an outgoing variant;
65 - slot assembly declarations;
65 - slot assembly declarations;
66 - root outgoing configurations for outgoing variants;
66 - root outgoing configurations for outgoing variants;
67 - slot-level assembly bodies;
67 - slot-level assembly bodies;
68 - publication-facing hooks.
68 - publication-facing hooks.
69
69
70 ### What does not belong to `variantArtifacts`
70 ### What does not belong to `variantArtifacts`
71
71
72 - ownership of variant existence;
72 - ownership of variant existence;
73 - ownership of source-set naming;
73 - ownership of source-set naming;
74 - direct mutation of source materialization internals;
74 - direct mutation of source materialization internals;
75 - compiler- or toolchain-specific logic;
75 - compiler- or toolchain-specific logic;
76 - eager flattening of source inputs into files during DSL declaration.
76 - eager flattening of source inputs into files during DSL declaration.
77
77
78 ---
78 ---
79
79
80 ## Outgoing subset
80 ## Outgoing subset
81
81
82 Not every declared `Variant` must become outgoing.
82 Not every declared `Variant` must become outgoing.
83
83
84 `variantArtifacts` defines an outgoing subset of variants.
84 `variantArtifacts` defines an outgoing subset of variants.
85
85
86 For each outgoing variant there is one root outgoing aggregate:
86 For each outgoing variant there is one root outgoing aggregate:
87
87
88 - one root consumable `Configuration`;
88 - one root consumable `Configuration`;
89 - one or more artifact slots;
89 - one or more artifact slots;
90 - one primary slot;
90 - one primary slot;
91 - optional secondary slots.
91 - optional secondary slots.
92
92
93 This is why `OutgoingConfiguration` is a real model object and not merely a
93 This is why `OutgoingConfiguration` is a real model object and not merely a
94 publication event payload.
94 publication event payload.
95
95
96 It represents a live build-facing aggregate:
96 It represents a live build-facing aggregate:
97
97
98 - it has its own attributes;
98 - it has its own attributes;
99 - it is visible to other build logic as soon as registered;
99 - it is visible to other build logic as soon as registered;
100 - it contains lazy handles rather than eagerly materialized state;
100 - it contains lazy handles rather than eagerly materialized state;
101 - it is distinct from slot assembly state.
101 - it is distinct from slot assembly state.
102
102
103 ---
103 ---
104
104
105 ## Identity-first split
105 ## Identity-first split
106
106
107 The artifact model should preserve the same identity-first principle used for
107 The artifact model should preserve the same identity-first principle used for
108 the rest of the project.
108 the rest of the project.
109
109
110 ### Identity objects
110 ### Identity objects
111
111
112 - `Variant`
112 - `Variant`
113 - `Slot`
113 - `Slot`
114 - `ArtifactSlot = (variant, slot)`
114 - `ArtifactSlot = (variant, slot)`
115
115
116 These objects are:
116 These objects are:
117
117
118 - cheap;
118 - cheap;
119 - replayable;
119 - replayable;
120 - suitable for discovery and selection.
120 - suitable for discovery and selection.
121
121
122 ### Stateful objects
122 ### Stateful objects
123
123
124 - `OutgoingConfiguration`
124 - `OutgoingConfiguration`
125 - `ArtifactAssembly`
125 - `ArtifactAssembly`
126
126
127 These objects are:
127 These objects are:
128
128
129 - build-facing;
129 - build-facing;
130 - allowed to contain Gradle lazy handles;
130 - allowed to contain Gradle lazy handles;
131 - obtained through dedicated APIs rather than embedded into identity objects.
131 - obtained through dedicated APIs rather than embedded into identity objects.
132
132
133 ### Rule of thumb
133 ### Rule of thumb
134
134
135 - slot identity is cheap and replayable;
135 - slot identity is cheap and replayable;
136 - inside one `OutgoingConfiguration`, `Slot` is the natural local identity;
136 - inside one `OutgoingConfiguration`, `Slot` is the natural local identity;
137 - `ArtifactSlot` is useful when slot identity must be referenced outside the
137 - `ArtifactSlot` is useful when slot identity must be referenced outside the
138 parent outgoing configuration;
138 parent outgoing configuration;
139 - slot body is stateful and resolved separately;
139 - slot body is stateful and resolved separately;
140 - outgoing configuration is a live aggregate, not a mere snapshot.
140 - outgoing configuration is a live aggregate, not a mere snapshot.
141
141
142 ---
142 ---
143
143
144 ## Artifact model
144 ## Artifact model
145
145
146 Conceptually:
146 Conceptually:
147
147
148 ```java
148 ```java
149 interface VariantArtifactsContext {
149 interface VariantArtifactsContext {
150 VariantsView getVariants();
150 VariantsView getVariants();
151 void all(Action<? super OutgoingConfiguration> action);
151 void all(Action<? super OutgoingConfiguration> action);
152 Optional<OutgoingConfiguration> findArtifacts(Variant variant);
152 Optional<OutgoingConfiguration> findArtifacts(Variant variant);
153 OutgoingConfiguration requireArtifacts(Variant variant);
153 OutgoingConfiguration requireArtifacts(Variant variant);
154 ArtifactAssemblies getAssemblies();
154 ArtifactAssemblies getAssemblies();
155 }
155 }
156 ```
156 ```
157
157
158 ```java
158 ```java
159 interface OutgoingConfiguration {
159 interface OutgoingConfiguration {
160 Variant getVariant();
160 Variant getVariant();
161 NamedDomainObjectProvider<Configuration> getOutgoingConfiguration();
161 NamedDomainObjectProvider<Configuration> getOutgoingConfiguration();
162 NamedDomainObjectContainer<Slot> getSlots();
162 NamedDomainObjectContainer<Slot> getSlots();
163 Property<Slot> getPrimarySlot();
163 Property<Slot> getPrimarySlot();
164 }
164 }
165 ```
165 ```
166
166
167 ```java
167 ```java
168 interface ArtifactAssemblies {
168 interface ArtifactAssemblies {
169 ArtifactAssembly resolveSlot(ArtifactSlot slot);
169 ArtifactAssembly resolveSlot(ArtifactSlot slot);
170 }
170 }
171 ```
171 ```
172
172
173 This intentionally uses a Gradle-style local slot container rather than a
173 This intentionally uses a Gradle-style local slot container rather than a
174 separate `ArtifactSlotsView`.
174 separate `ArtifactSlotsView`.
175
175
176 The reason is simple:
176 The reason is simple:
177
177
178 - inside one `OutgoingConfiguration`, slot identity is local and naturally
178 - inside one `OutgoingConfiguration`, slot identity is local and naturally
179 expressed as `Slot`;
179 expressed as `Slot`;
180 - a dedicated `ArtifactSlotsView` would suggest a detached readonly projection
180 - a dedicated `ArtifactSlotsView` would suggest a detached readonly projection
181 without clearly owning mutation/configuration semantics;
181 without clearly owning mutation/configuration semantics;
182 - `NamedDomainObjectContainer<Slot>` better matches the live configuration model
182 - `NamedDomainObjectContainer<Slot>` better matches the live configuration model
183 and keeps the parent aggregate as the owner of slot structure.
183 and keeps the parent aggregate as the owner of slot structure.
184
184
185 `ArtifactSlot` remains useful, but only as a fully qualified identity outside
185 `ArtifactSlot` remains useful, but only as a fully qualified identity outside
186 the parent aggregate:
186 the parent aggregate:
187
187
188 - resolver APIs;
188 - resolver APIs;
189 - global payloads;
189 - global payloads;
190 - cross-variant references.
190 - cross-variant references.
191
191
192 Important distinction:
192 Important distinction:
193
193
194 - `OutgoingConfiguration` describes outgoing publication structure;
194 - `OutgoingConfiguration` describes outgoing publication structure;
195 - `ArtifactAssembly` describes how one slot artifact is assembled.
195 - `ArtifactAssembly` describes how one slot artifact is assembled.
196
196
197 An `ArtifactAssembly` does not create the root outgoing configuration. It serves
197 An `ArtifactAssembly` does not create the root outgoing configuration. It serves
198 one already declared slot inside that configuration.
198 one already declared slot inside that configuration.
199
199
200 ### Primary slot ownership
200 ### Primary slot ownership
201
201
202 Primary status belongs to the parent outgoing configuration, not to the slot
202 Primary status belongs to the parent outgoing configuration, not to the slot
203 itself.
203 itself.
204
204
205 This is why the preferred container-level contract is:
205 This is why the preferred container-level contract is:
206
206
207 ```java
207 ```java
208 Property<Slot> getPrimarySlot();
208 Property<Slot> getPrimarySlot();
209 ```
209 ```
210
210
211 and not a slot-local boolean or a derived `ArtifactSlot` reference.
211 and not a slot-local boolean or a derived `ArtifactSlot` reference.
212
212
213 Reasons:
213 Reasons:
214
214
215 - the primary role is assigned by the parent aggregate;
215 - the primary role is assigned by the parent aggregate;
216 - within one `OutgoingConfiguration`, the variant identity is already known, so
216 - within one `OutgoingConfiguration`, the variant identity is already known, so
217 `Slot` is sufficient and avoids redundant `(variant, slot)` duplication;
217 `Slot` is sufficient and avoids redundant `(variant, slot)` duplication;
218 - `Property` matches the rest of the Gradle-facing live model better than a
218 - `Property` matches the rest of the Gradle-facing live model better than a
219 custom `findPrimarySlot()` style API;
219 custom `findPrimarySlot()` style API;
220 - `Property` gives useful write/configure/finalize semantics without inventing a
220 - `Property` gives useful write/configure/finalize semantics without inventing a
221 separate special lifecycle abstraction.
221 separate special lifecycle abstraction.
222
222
223 Expected usage:
223 Expected usage:
224
224
225 - DSL or adapters may assign the primary slot through `set(...)`;
225 - DSL or adapters may assign the primary slot through `set(...)`;
226 - adapters that want to provide a default may use `convention(...)`;
226 - adapters that want to provide a default may use `convention(...)`;
227 - the property may remain unset while the model is still incomplete;
227 - the property may remain unset while the model is still incomplete;
228 - at materialization time the property may be finalized;
228 - at materialization time the property may be finalized;
229 - if more than one slot exists and `primarySlot` is still unset at the
229 - if more than one slot exists and `primarySlot` is still unset at the
230 materialization point, that is a model error.
230 materialization point, that is a model error.
231
231
232 The model should still enforce that the selected primary slot belongs to the
232 The model should still enforce that the selected primary slot belongs to the
233 same `OutgoingConfiguration`.
233 same `OutgoingConfiguration`.
234
234
235 ### Proposed public shape
235 ### Proposed public shape
236
236
237 With these constraints, the preferred public structure is:
237 With these constraints, the preferred public structure is:
238
238
239 ```java
239 ```java
240 interface VariantArtifactsContext {
240 interface VariantArtifactsContext {
241 VariantsView getVariants();
241 VariantsView getVariants();
242 void all(Action<? super OutgoingConfiguration> action);
242 void all(Action<? super OutgoingConfiguration> action);
243 Optional<OutgoingConfiguration> findArtifacts(Variant variant);
243 Optional<OutgoingConfiguration> findArtifacts(Variant variant);
244 OutgoingConfiguration requireArtifacts(Variant variant);
244 OutgoingConfiguration requireArtifacts(Variant variant);
245 ArtifactAssemblies getAssemblies();
245 ArtifactAssemblies getAssemblies();
246 }
246 }
247
247
248 interface OutgoingConfiguration {
248 interface OutgoingConfiguration {
249 Variant getVariant();
249 Variant getVariant();
250 NamedDomainObjectProvider<Configuration> getOutgoingConfiguration();
250 NamedDomainObjectProvider<Configuration> getOutgoingConfiguration();
251 NamedDomainObjectContainer<Slot> getSlots();
251 NamedDomainObjectContainer<Slot> getSlots();
252 Property<Slot> getPrimarySlot();
252 Property<Slot> getPrimarySlot();
253 }
253 }
254
254
255 interface ArtifactAssemblies {
255 interface ArtifactAssemblies {
256 ArtifactAssembly resolveSlot(ArtifactSlot slot);
256 ArtifactAssembly resolveSlot(ArtifactSlot slot);
257 }
257 }
258
258
259 record ArtifactSlot(Variant variant, Slot slot) {}
259 record ArtifactSlot(Variant variant, Slot slot) {}
260 ```
260 ```
261
261
262 This gives:
262 This gives:
263
263
264 - one global registry of outgoing variants;
264 - one global registry of outgoing variants;
265 - one local slot container per outgoing variant;
265 - one local slot container per outgoing variant;
266 - one fully qualified slot identity for resolver and cross-aggregate use;
266 - one fully qualified slot identity for resolver and cross-aggregate use;
267 - one explicit service for slot assembly materialization.
267 - one explicit service for slot assembly materialization.
268
268
269 ### Minimal internal shape
269 ### Minimal internal shape
270
270
271 The preferred minimal internal structure is:
271 The preferred minimal internal structure is:
272
272
273 ```java
273 ```java
274 final class VariantArtifactsRegistry implements VariantArtifactsContext {
274 final class VariantArtifactsRegistry implements VariantArtifactsContext {
275 OutgoingConfiguration outgoingConfiguration(Variant variant);
275 OutgoingConfiguration outgoingConfiguration(Variant variant);
276 ArtifactAssemblyRules slotRules(ArtifactSlot slot);
276 ArtifactAssemblyRules slotRules(ArtifactSlot slot);
277 ArtifactAssemblies assemblies();
277 ArtifactAssemblies assemblies();
278 }
278 }
279
279
280 interface ArtifactAssemblyRules {
280 interface ArtifactAssemblyRules {
281 void from(Object input);
281 void from(Object input);
282 <T extends Task> void producedBy(
283 TaskProvider<T> task,
284 Function<? super T, ? extends Provider<? extends FileSystemLocation>> output);
282 void fromVariant(Action<? super OutputSelectionSpec> action);
285 void fromVariant(Action<? super OutputSelectionSpec> action);
283 void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
286 void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
284 void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
287 void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
285 }
288 }
286 ```
289 ```
287
290
288 Responsibility split:
291 Responsibility split:
289
292
290 - `VariantArtifactsRegistry` owns the whole artifact model;
293 - `VariantArtifactsRegistry` owns the whole artifact model;
291 - `OutgoingConfiguration` owns only the structural variant-local aggregate;
294 - `OutgoingConfiguration` owns only the structural variant-local aggregate;
292 - `ArtifactAssemblyRules` owns the content declaration of one slot;
295 - `ArtifactAssemblyRules` owns the content declaration of one slot;
293 - `ArtifactAssemblies` materializes `ArtifactAssembly` from
296 - `ArtifactAssemblies` materializes `ArtifactAssembly` from
294 `ArtifactSlot -> ArtifactAssemblyRules`.
297 `ArtifactSlot -> ArtifactAssemblyRules`.
295
298
296 This keeps `OutgoingConfiguration` focused on structure and avoids overloading it
299 This keeps `OutgoingConfiguration` focused on structure and avoids overloading it
297 with slot-content APIs such as `rules(slot)`.
300 with slot-content APIs such as `rules(slot)`.
298
301
299 ### DSL binding
302 ### DSL binding
300
303
301 The DSL should be connected through the registry, not by making
304 The DSL should be connected through the registry, not by making
302 `OutgoingConfiguration` responsible for content rules.
305 `OutgoingConfiguration` responsible for content rules.
303
306
304 Conceptually:
307 Conceptually:
305
308
306 ```java
309 ```java
307 variantArtifacts.variant("browser", spec -> {
310 variantArtifacts.variant("browser", spec -> {
308 spec.slot("runtime", assembly -> {
311 spec.slot("runtime", assembly -> {
309 assembly.fromRole("production", out -> out.output("js"));
312 assembly.fromRole("production", out -> out.output("js"));
310 });
313 });
311 });
314 });
312 ```
315 ```
313
316
314 Operationally this means:
317 Operationally this means:
315
318
316 1. registry creates or returns `OutgoingConfiguration` for the variant;
319 1. registry creates or returns `OutgoingConfiguration` for the variant;
317 2. slot declaration creates or returns `Slot` inside that outgoing configuration;
320 2. slot declaration creates or returns `Slot` inside that outgoing configuration;
318 3. registry forms `ArtifactSlot(variant, slot)`;
321 3. registry forms `ArtifactSlot(variant, slot)`;
319 4. registry resolves `slotRules(artifactSlot)`;
322 4. registry resolves `slotRules(artifactSlot)`;
320 5. bound `ArtifactAssemblySpec` writes into those rules.
323 5. bound `ArtifactAssemblySpec` writes into those rules.
321
324
322 So the DSL writes:
325 So the DSL writes:
323
326
324 - structure into `OutgoingConfiguration`;
327 - structure into `OutgoingConfiguration`;
325 - slot content into registry-owned `ArtifactAssemblyRules`.
328 - slot content into registry-owned `ArtifactAssemblyRules`.
326
329
327 This is the intended bridge point between the public DSL and the internal
330 This is the intended bridge point between the public DSL and the internal
328 resolver/materialization model.
331 resolver/materialization model.
329
332
330 ---
333 ---
331
334
332 ## Live model and monotonic structure
335 ## Live model and monotonic structure
333
336
334 `variantArtifacts` should be treated as a live configuration model during the
337 `variantArtifacts` should be treated as a live configuration model during the
335 whole configuration phase.
338 whole configuration phase.
336
339
337 This means:
340 This means:
338
341
339 - slot inputs remain live;
342 - slot inputs remain live;
340 - `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep
343 - `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep
341 contributing inputs until task execution;
344 contributing inputs until task execution;
345 - `producedBy(...)` publishes an existing task output directly and does not
346 create the managed copy assembly for that slot;
342 - `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task
347 - `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task
343 wiring;
348 wiring;
344 - external task outputs remain outside the control of this model and must be
349 - external task outputs remain outside the control of this model and must be
345 accepted as live inputs.
350 accepted as live inputs.
346
351
347 The model should therefore avoid a mandatory freeze phase for slot content.
352 The model should therefore avoid a mandatory freeze phase for slot content.
348
353
349 Instead, it should follow a monotonic rule:
354 Instead, it should follow a monotonic rule:
350
355
351 - outgoing variant existence may grow;
356 - outgoing variant existence may grow;
352 - slot existence may grow;
357 - slot existence may grow;
353 - slot content may grow;
358 - slot content may grow;
354 - publication-visible identity should not be retroactively redefined.
359 - publication-visible identity should not be retroactively redefined.
355
360
356 In practice this means:
361 In practice this means:
357
362
358 - slot names are stable once declared;
363 - slot names are stable once declared;
359 - primary slot designation is structural;
364 - primary slot designation is structural;
360 - slot input content remains live.
365 - slot input content remains live.
361
366
362 This also means that the model does not need a dedicated freeze phase for slot
367 This also means that the model does not need a dedicated freeze phase for slot
363 content merely because the root outgoing configuration was registered earlier.
368 content merely because the root outgoing configuration was registered earlier.
364
369
365 Early registration of the root `Configuration` and live evolution of slot input
370 Early registration of the root `Configuration` and live evolution of slot input
366 content are compatible concerns.
371 content are compatible concerns.
367
372
368 ---
373 ---
369
374
370 ## DSL principles
375 ## DSL principles
371
376
372 The DSL should remain declarative and symbolic.
377 The DSL should remain declarative and symbolic.
373
378
374 It should describe:
379 It should describe:
375
380
376 - which variant is outgoing;
381 - which variant is outgoing;
377 - which slots exist;
382 - which slots exist;
378 - which slot is primary;
383 - which slot is primary;
379 - which selectors contribute inputs to each slot.
384 - which selectors contribute inputs to each slot.
380
385
381 It should not directly expose:
386 It should not directly expose:
382
387
383 - `GenericSourceSet`;
388 - `GenericSourceSet`;
384 - `FileCollection`;
389 - `FileCollection`;
385 - concrete resolved files from `variantSources`;
390 - concrete resolved files from `variantSources`;
386 - internal resolver state.
391 - internal resolver state.
387
392
388 ### DSL shape
393 ### DSL shape
389
394
390 Conceptually:
395 Conceptually:
391
396
392 ```groovy
397 ```groovy
393 variantArtifacts {
398 variantArtifacts {
394 variant("browser") {
399 variant("browser") {
395 primarySlot("runtime") {
400 primarySlot("runtime") {
396 fromRole("production") {
401 fromRole("production") {
397 output("js")
402 output("js")
398 output("resources")
403 output("resources")
399 }
404 }
400 }
405 }
401
406
402 slot("types") {
407 slot("types") {
403 fromVariant {
408 fromVariant {
404 output("dts")
409 output("dts")
405 }
410 }
406 }
411 }
407
412
408 slot("sources") {
413 slot("sources") {
409 fromLayer("main") {
414 fromLayer("main") {
410 output("sources")
415 output("sources")
411 }
416 }
412 }
417 }
413
418
414 slot("bundleMetadata") {
419 slot("bundleMetadata") {
415 from(someTask)
420 producedBy(writePackageMetadata) {
416 from(layout.buildDirectory.file("generated/meta.json"))
421 outputFile
422 }
417 }
423 }
418 }
424 }
419 }
425 }
420 ```
426 ```
421
427
422 ### Meaning of contribution forms
428 ### Meaning of contribution forms
423
429
424 - `from(Object)` adds a direct input independent from `variantSources`;
430 - `from(Object)` adds a direct input independent from `variantSources`;
425 - `fromVariant { output(...) }` selects named outputs from all compile units of
431 - `fromVariant { output(...) }` selects named outputs from all compile units of
426 the current variant;
432 the current variant;
427 - `fromRole(role) { output(...) }` selects named outputs from compile units that
433 - `fromRole(role) { output(...) }` selects named outputs from compile units that
428 belong to the given role projection;
434 belong to the given role projection;
429 - `fromLayer(layer) { output(...) }` selects named outputs from the compile unit
435 - `fromLayer(layer) { output(...) }` selects named outputs from the compile unit
430 of the current variant and the given layer, if such unit exists.
436 of the current variant and the given layer, if such unit exists.
437 - `producedBy(task) { outputFile }` maps an existing producing task to the single
438 file or directory published for the slot.
431
439
440 Contribution forms and `producedBy(...)` are mutually exclusive for one slot.
432 The DSL stores declarations, not resolved file collections.
441 The DSL stores declarations, not resolved file collections.
433
442
434 ---
443 ---
435
444
436 ## Contribution model
445 ## Contribution model
437
446
438 Internally the DSL should compile to slot contributions.
447 Internally the DSL should compile to slot contributions.
439
448
440 Conceptually:
449 Conceptually:
441
450
442 - `DirectContribution`
451 - `DirectContribution`
443 - `VariantOutputContribution`
452 - `VariantOutputContribution`
444 - `RoleOutputContribution`
453 - `RoleOutputContribution`
445 - `LayerOutputContribution`
454 - `LayerOutputContribution`
446
455
447 These contributions should remain symbolic for as long as possible.
456 These contributions should remain symbolic for as long as possible.
448
457
449 They should not resolve source sets or files at declaration time.
458 They should not resolve source sets or files at declaration time.
450
459
451 Each contribution is expected to provide:
460 Each contribution is expected to provide:
452
461
453 - its selection scope;
462 - its selection scope;
454 - the requested output names;
463 - the requested output names;
455 - enough symbolic identity for later validation and resolver policies.
464 - enough symbolic identity for later validation and resolver policies.
456
465
457 ---
466 ---
458
467
459 ## Resolver bridge between `variantSources` and `variantArtifacts`
468 ## Resolver bridge between `variantSources` and `variantArtifacts`
460
469
461 This is the central integration point.
470 This is the central integration point.
462
471
463 `variantArtifacts` should not access mutable internals of `variantSources`.
472 `variantArtifacts` should not access mutable internals of `variantSources`.
464
473
465 Instead, it should resolve slot inputs through the public finalized
474 Instead, it should resolve slot inputs through the public finalized
466 `VariantSourcesContext`.
475 `VariantSourcesContext`.
467
476
468 ### Bridge responsibilities
477 ### Bridge responsibilities
469
478
470 The bridge:
479 The bridge:
471
480
472 - takes slot contribution declarations;
481 - takes slot contribution declarations;
473 - expands them against finalized variant topology;
482 - expands them against finalized variant topology;
474 - maps logical selectors to compile units and role projections;
483 - maps logical selectors to compile units and role projections;
475 - obtains source sets lazily through `VariantSourcesContext`;
484 - obtains source sets lazily through `VariantSourcesContext`;
476 - resolves named outputs from those source sets;
485 - resolves named outputs from those source sets;
477 - builds the live input model for an `ArtifactAssembly`.
486 - builds the live input model for an `ArtifactAssembly`.
478
487
479 ### Bridge input
488 ### Bridge input
480
489
481 - current outgoing variant identity;
490 - current outgoing variant identity;
482 - slot contribution declarations;
491 - slot contribution declarations;
483 - `VariantSourcesContext`.
492 - `VariantSourcesContext`.
484
493
485 ### Bridge output
494 ### Bridge output
486
495
487 - a live collection of logical slot inputs;
496 - a live collection of logical slot inputs;
488 - later adapted to `FileCollection` or other assembly-facing input models.
497 - later adapted to `FileCollection` or other assembly-facing input models.
489
498
490 ### Resolution semantics
499 ### Resolution semantics
491
500
492 For one outgoing variant:
501 For one outgoing variant:
493
502
494 - `fromVariant { output(x) }`
503 - `fromVariant { output(x) }`
495 - expands to all `CompileUnit` of that variant;
504 - expands to all `CompileUnit` of that variant;
496 - `fromRole(role) { output(x) }`
505 - `fromRole(role) { output(x) }`
497 - expands to `RoleProjection(variant, role)` and then to its compile units;
506 - expands to `RoleProjection(variant, role)` and then to its compile units;
498 - `fromLayer(layer) { output(x) }`
507 - `fromLayer(layer) { output(x) }`
499 - expands to one compile unit `(variant, layer)` when it exists;
508 - expands to one compile unit `(variant, layer)` when it exists;
500 - `from(Object)`
509 - `from(Object)`
501 - bypasses `variantSources` completely.
510 - bypasses `variantSources` completely.
511 - `producedBy(task)`
512 - bypasses contribution resolution and registers the task output as the slot
513 artifact directly.
502
514
503 After compile units are known, the bridge asks
515 After compile units are known, the bridge asks
504 `ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the
516 `ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the
505 requested named output.
517 requested named output.
506
518
507 This keeps `variantArtifacts` independent from source-set naming internals and
519 This keeps `variantArtifacts` independent from source-set naming internals and
508 other materialization details.
520 other materialization details.
509
521
510 ---
522 ---
511
523
512 ## Validation
524 ## Validation
513
525
514 Validation should be structural and symbolic.
526 Validation should be structural and symbolic.
515
527
516 It should validate:
528 It should validate:
517
529
518 - outgoing variant refers to an existing `Variant`;
530 - outgoing variant refers to an existing `Variant`;
519 - referenced `Role` exists in that variant projection space;
531 - referenced `Role` exists in that variant projection space;
520 - referenced `Layer` exists in that variant compile-unit space;
532 - referenced `Layer` exists in that variant compile-unit space;
521 - primary slot is defined when needed;
533 - primary slot is defined when needed;
522 - primary slot refers to a slot declared in the same outgoing configuration.
534 - primary slot refers to a slot declared in the same outgoing configuration.
523
535
524 Validation should not require eager materialization of source sets or eager
536 Validation should not require eager materialization of source sets or eager
525 resolution of files.
537 resolution of files.
526
538
527 ---
539 ---
528
540
529 ## Deduplication and policy extension points
541 ## Deduplication and policy extension points
530
542
531 Deduplication is important, but it should not be baked into the DSL itself.
543 Deduplication is important, but it should not be baked into the DSL itself.
532
544
533 The correct place for it is the resolver bridge, after symbolic contributions
545 The correct place for it is the resolver bridge, after symbolic contributions
534 have been expanded to logical inputs but before they are finally adapted to
546 have been expanded to logical inputs but before they are finally adapted to
535 assembly-facing file collections.
547 assembly-facing file collections.
536
548
537 ### Why not in the DSL
549 ### Why not in the DSL
538
550
539 At declaration time it is still unknown whether selectors overlap:
551 At declaration time it is still unknown whether selectors overlap:
540
552
541 - `fromVariant`
553 - `fromVariant`
542 - `fromRole`
554 - `fromRole`
543 - `fromLayer`
555 - `fromLayer`
544
556
545 may all describe the same logical source output.
557 may all describe the same logical source output.
546
558
547 ### Why not rely only on `FileCollection`
559 ### Why not rely only on `FileCollection`
548
560
549 `FileCollection` may still provide useful physical deduplication, but it is too
561 `FileCollection` may still provide useful physical deduplication, but it is too
550 late and too file-oriented to serve as the only semantic mechanism.
562 late and too file-oriented to serve as the only semantic mechanism.
551
563
552 The artifact model should first deduplicate logical inputs, then let Gradle
564 The artifact model should first deduplicate logical inputs, then let Gradle
553 perform any additional physical deduplication.
565 perform any additional physical deduplication.
554
566
555 ### Default expectation
567 ### Default expectation
556
568
557 The default resolver should support:
569 The default resolver should support:
558
570
559 - deduplication of topology-aware inputs by logical identity;
571 - deduplication of topology-aware inputs by logical identity;
560 - no implicit deduplication of direct `from(Object)` inputs.
572 - no implicit deduplication of direct `from(Object)` inputs.
561
573
562 Logical identity should be based on domain meaning, for example:
574 Logical identity should be based on domain meaning, for example:
563
575
564 - `(CompileUnit, outputName)`
576 - `(CompileUnit, outputName)`
565
577
566 and not on projected source-set names.
578 and not on projected source-set names.
567
579
568 This is important because source-set naming policy belongs to `variantSources`
580 This is important because source-set naming policy belongs to `variantSources`
569 and must not silently redefine artifact semantics.
581 and must not silently redefine artifact semantics.
570
582
571 ### Extension points
583 ### Extension points
572
584
573 The model should provide explicit internal extension points for:
585 The model should provide explicit internal extension points for:
574
586
575 - deduplication policy;
587 - deduplication policy;
576 - logical input identity;
588 - logical input identity;
577 - adaptation of resolved logical inputs to assembly-facing objects.
589 - adaptation of resolved logical inputs to assembly-facing objects.
578
590
579 Conceptually:
591 Conceptually:
580
592
581 ```java
593 ```java
582 interface SlotInputDedupPolicy { ... }
594 interface SlotInputDedupPolicy { ... }
583 interface LogicalSlotInputIdentity { ... }
595 interface LogicalSlotInputIdentity { ... }
584 interface SlotInputAdapter { ... }
596 interface SlotInputAdapter { ... }
585 ```
597 ```
586
598
587 The default implementation may remain simple, but these seams should exist from
599 The default implementation may remain simple, but these seams should exist from
588 the start.
600 the start.
589
601
590 ---
602 ---
591
603
592 ## Publication hooks
604 ## Publication hooks
593
605
594 Publication hooks remain useful, but they should observe the live structural
606 Publication hooks remain useful, but they should observe the live structural
595 model rather than define it.
607 model rather than define it.
596
608
597 Examples:
609 Examples:
598
610
599 - `whenOutgoingConfiguration(...)`
611 - `whenOutgoingConfiguration(...)`
600 - `whenOutgoingSlot(...)`
612 - `whenOutgoingSlot(...)`
601
613
602 These hooks are adapter-facing customization points over already declared
614 These hooks are adapter-facing customization points over already declared
603 outgoing structure:
615 outgoing structure:
604
616
605 - root configuration attributes;
617 - root configuration attributes;
606 - slot artifact attributes;
618 - slot artifact attributes;
607 - assembly task tweaks.
619 - assembly task tweaks.
608
620
609 The recommended way to connect publication-facing `Spec` objects to the
621 The recommended way to connect publication-facing `Spec` objects to the
610 structural model is by backlink, not by moving slot rules into publication
622 structural model is by backlink, not by moving slot rules into publication
611 types.
623 types.
612
624
613 Conceptually:
625 Conceptually:
614
626
615 ```java
627 ```java
616 interface OutgoingConfigurationSpec {
628 interface OutgoingConfigurationSpec {
617 OutgoingConfiguration getOutgoingArtifacts();
629 OutgoingConfiguration getOutgoingArtifacts();
618 Variant getVariant();
630 Variant getVariant();
619 Configuration getConfiguration();
631 Configuration getConfiguration();
620 }
632 }
621
633
622 interface OutgoingArtifactSlotSpec {
634 interface OutgoingArtifactSlotSpec {
623 ArtifactSlot getArtifactSlot();
635 ArtifactSlot getArtifactSlot();
624 ArtifactAssembly getAssembly();
636 ArtifactAssembly getAssembly();
625 boolean isPrimary();
637 boolean isPrimary();
626 }
638 }
627 ```
639 ```
628
640
629 In this arrangement:
641 In this arrangement:
630
642
631 - `OutgoingConfigurationSpec` remains a publication-facing facade;
643 - `OutgoingConfigurationSpec` remains a publication-facing facade;
632 - `OutgoingConfigurationSpec` may expose the structural aggregate when an
644 - `OutgoingConfigurationSpec` may expose the structural aggregate when an
633 adapter needs it;
645 adapter needs it;
634 - slot rules still belong to `VariantArtifactsRegistry`;
646 - slot rules still belong to `VariantArtifactsRegistry`;
635 - publication specs do not become owners of declaration or resolver state.
647 - publication specs do not become owners of declaration or resolver state.
636
648
637 They should not become the primary structural API for the artifact model.
649 They should not become the primary structural API for the artifact model.
638
650
639 This is why a separate phase-oriented `OutgoingPublicationsContext` is not
651 This is why a separate phase-oriented `OutgoingPublicationsContext` is not
640 required.
652 required.
641
653
642 The live `OutgoingConfiguration` aggregate is sufficient.
654 The live `OutgoingConfiguration` aggregate is sufficient.
643
655
644 ---
656 ---
645
657
646 ## Design principles
658 ## Design principles
647
659
648 ### 1. Keep topology ownership in `variants`
660 ### 1. Keep topology ownership in `variants`
649
661
650 `variantArtifacts` selects from the topology model. It does not own it.
662 `variantArtifacts` selects from the topology model. It does not own it.
651
663
652 ### 2. Keep source ownership in `variantSources`
664 ### 2. Keep source ownership in `variantSources`
653
665
654 `variantArtifacts` consumes source materialization through a resolver bridge. It
666 `variantArtifacts` consumes source materialization through a resolver bridge. It
655 does not own source-set semantics.
667 does not own source-set semantics.
656
668
657 ### 3. Keep the DSL symbolic
669 ### 3. Keep the DSL symbolic
658
670
659 The DSL declares intent and selection rules, not materialized files.
671 The DSL declares intent and selection rules, not materialized files.
660
672
661 ### 4. Keep slot content live
673 ### 4. Keep slot content live
662
674
663 Do not introduce an artificial finalize phase for slot content unless a real
675 Do not introduce an artificial finalize phase for slot content unless a real
664 semantic need appears.
676 semantic need appears.
665
677
666 ### 5. Fix only structural identity
678 ### 5. Fix only structural identity
667
679
668 Slot name, primary designation, and outgoing shape are structural. Slot inputs
680 Slot name, primary designation, and outgoing shape are structural. Slot inputs
669 remain live.
681 remain live.
670
682
671 ### 6. Resolve through dedicated bridges
683 ### 6. Resolve through dedicated bridges
672
684
673 Cross-model integration belongs in a resolver service, not in DSL classes.
685 Cross-model integration belongs in a resolver service, not in DSL classes.
674
686
675 ### 7. Add policy seams early
687 ### 7. Add policy seams early
676
688
677 Deduplication and similar concerns should have extension points from the start,
689 Deduplication and similar concerns should have extension points from the start,
678 even if the initial implementation is conservative.
690 even if the initial implementation is conservative.
679
691
680 ---
692 ---
681
693
682 ## Summary
694 ## Summary
683
695
684 `variantArtifacts` should be modeled as a live outgoing-contract layer over
696 `variantArtifacts` should be modeled as a live outgoing-contract layer over
685 `variants`, with source input resolution delegated to a dedicated bridge over
697 `variants`, with source input resolution delegated to a dedicated bridge over
686 `variantSources`.
698 `variantSources`.
687
699
688 The resulting shape is:
700 The resulting shape is:
689
701
690 - `variants` owns topology;
702 - `variants` owns topology;
691 - `variantSources` owns source materialization;
703 - `variantSources` owns source materialization;
692 - `variantArtifacts` owns outgoing contract structure;
704 - `variantArtifacts` owns outgoing contract structure;
693 - a resolver bridge connects symbolic slot declarations to live source-derived
705 - a resolver bridge connects symbolic slot declarations to live source-derived
694 inputs;
706 inputs;
695 - deduplication and similar concerns are policies of that bridge, not of the
707 - deduplication and similar concerns are policies of that bridge, not of the
696 DSL itself;
708 DSL itself;
697 - slot content stays live during configuration;
709 - slot content stays live during configuration;
698 - only publication-visible structure is treated as stable identity.
710 - only publication-visible structure is treated as stable identity.
@@ -1,60 +1,106
1 package org.implab.gradle.variants.artifacts;
1 package org.implab.gradle.variants.artifacts;
2
2
3 import java.util.function.Function;
4
3 import org.gradle.api.Action;
5 import org.gradle.api.Action;
6 import org.gradle.api.InvalidUserDataException;
7 import org.gradle.api.Task;
8 import org.gradle.api.file.FileSystemLocation;
9 import org.gradle.api.provider.Provider;
10 import org.gradle.api.tasks.TaskProvider;
4 import groovy.lang.Closure;
11 import groovy.lang.Closure;
5 import org.implab.gradle.common.core.lang.Closures;
12 import org.implab.gradle.common.core.lang.Closures;
6
13
7 /**
14 /**
8 * DSL model describing how a slot artifact is assembled.
15 * DSL model describing how a slot artifact is assembled.
9 *
16 *
10 * <p>Selection rules declared here may refer to internal build topology such as roles, layers or units.
17 * <p>Selection rules declared here may refer to internal build topology such as roles, layers or units.
11 * Those selectors influence slot assembly only and do not become part of published artifact identity.
18 * Those selectors influence slot assembly only and do not become part of published artifact identity.
12 *
19 *
13 * <p>Regardless of the number of declared inputs, a slot is expected to materialize to a single published
20 * <p>Regardless of the number of declared inputs, a slot is expected to materialize to a single published
14 * artifact.
21 * artifact.
15 */
22 */
16 public interface ArtifactAssemblySpec {
23 public interface ArtifactAssemblySpec {
17 /**
24 /**
18 * Contributes direct input material to the slot assembly.
25 * Contributes direct input material to the slot assembly.
19 *
26 *
20 * <p>The resulting slot still represents one published artifact.
27 * <p>The resulting slot still represents one published artifact.
21 *
28 *
22 * @param artifact direct input notation understood by the implementation
29 * @param artifact direct input notation understood by the implementation
23 */
30 */
24 void from(Object artifact);
31 void from(Object artifact);
25
32
26 /**
33 /**
34 * Registers a task that directly produces the published slot artifact.
35 *
36 * <p>Use this method when the slot is produced as one file or directory by an
37 * existing task, for example generated package metadata. Unlike {@link #from(Object)}
38 * and topology-aware selectors, this does not copy inputs into a managed assembly
39 * directory. The mapped task output becomes the published artifact itself.
40 *
41 * <p>This mode is mutually exclusive with contribution-based assembly methods
42 * such as {@link #from(Object)}, {@link #fromVariant(Action)}, {@link #fromRole(String, Action)},
43 * and {@link #fromLayer(String, Action)} for the same slot.
44 *
45 * @param <T> task type
46 * @param task task provider producing the artifact
47 * @param artifact maps the producing task to its output file or directory provider
48 */
49 <T extends Task> void producedBy(
50 TaskProvider<T> task,
51 Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact);
52
53 default <T extends Task> void producedBy(TaskProvider<T> task, Closure<?> closure) {
54 producedBy(task, taskInstance -> producedArtifact(closure, taskInstance));
55 }
56
57 @SuppressWarnings("unchecked")
58 private static Provider<? extends FileSystemLocation> producedArtifact(Closure<?> closure, Task task) {
59 var c = (Closure<?>) closure.clone();
60 c.setResolveStrategy(Closure.DELEGATE_FIRST);
61 c.setDelegate(task);
62
63 var artifact = c.call(task);
64 if (artifact instanceof Provider<?>) {
65 return (Provider<? extends FileSystemLocation>) artifact;
66 }
67
68 throw new InvalidUserDataException("Produced artifact mapper for task '" + task.getName()
69 + "' must return Provider<? extends FileSystemLocation>");
70 }
71
72 /**
27 * Selects outputs from the whole variant scope.
73 * Selects outputs from the whole variant scope.
28 *
74 *
29 * @param action output selection rule
75 * @param action output selection rule
30 */
76 */
31 void fromVariant(Action<? super OutputSelectionSpec> action);
77 void fromVariant(Action<? super OutputSelectionSpec> action);
32
78
33 default void fromVariant(Closure<?> closure) {
79 default void fromVariant(Closure<?> closure) {
34 fromVariant(Closures.action(closure));
80 fromVariant(Closures.action(closure));
35 }
81 }
36
82
37 /**
83 /**
38 * Selects outputs from a role inside the current variant.
84 * Selects outputs from a role inside the current variant.
39 *
85 *
40 * @param roleName role name used only for assembly-time selection
86 * @param roleName role name used only for assembly-time selection
41 * @param action output selection rule
87 * @param action output selection rule
42 */
88 */
43 void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
89 void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
44
90
45 default void fromRole(String roleName, Closure<?> closure) {
91 default void fromRole(String roleName, Closure<?> closure) {
46 fromRole(roleName, Closures.action(closure));
92 fromRole(roleName, Closures.action(closure));
47 }
93 }
48
94
49 /**
95 /**
50 * Selects outputs from a layer inside the current variant.
96 * Selects outputs from a layer inside the current variant.
51 *
97 *
52 * @param layerName layer name used only for assembly-time selection
98 * @param layerName layer name used only for assembly-time selection
53 * @param action output selection rule
99 * @param action output selection rule
54 */
100 */
55 void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
101 void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
56
102
57 default void fromLayer(String layerName, Closure<?> closure) {
103 default void fromLayer(String layerName, Closure<?> closure) {
58 fromLayer(layerName, Closures.action(closure));
104 fromLayer(layerName, Closures.action(closure));
59 }
105 }
60 }
106 }
@@ -1,56 +1,60
1 package org.implab.gradle.variants.artifacts.internal;
1 package org.implab.gradle.variants.artifacts.internal;
2
2
3 import org.eclipse.jdt.annotation.NonNullByDefault;
3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 import org.gradle.api.Action;
4 import org.gradle.api.Action;
5 import org.implab.gradle.variants.artifacts.ArtifactAssemblies;
5 import org.implab.gradle.variants.artifacts.ArtifactAssemblies;
6 import org.implab.gradle.variants.artifacts.ArtifactSlot;
6 import org.implab.gradle.variants.artifacts.ArtifactSlot;
7 import org.implab.gradle.variants.artifacts.OutgoingVariant;
7 import org.implab.gradle.variants.artifacts.OutgoingVariant;
8
8
9 /**
9 /**
10 * Binds materialized slot assemblies to Gradle outgoing publications.
10 * Binds materialized slot assemblies to Gradle outgoing publications.
11 */
11 */
12 @NonNullByDefault
12 @NonNullByDefault
13 public class ArtifactAssemblyBinder implements Action<OutgoingVariant> {
13 public class ArtifactAssemblyBinder implements Action<OutgoingVariant> {
14
14
15 private final ArtifactAssemblies resolver;
15 private final ArtifactAssemblies resolver;
16
16
17 public ArtifactAssemblyBinder(ArtifactAssemblies resolver) {
17 public ArtifactAssemblyBinder(ArtifactAssemblies resolver) {
18 this.resolver = resolver;
18 this.resolver = resolver;
19 }
19 }
20
20
21 @Override
21 @Override
22 public void execute(OutgoingVariant outgoingVariant) {
22 public void execute(OutgoingVariant outgoingVariant) {
23 var slots = outgoingVariant.getSlots();
23 var slots = outgoingVariant.getSlots();
24 var primarySlotProvider = outgoingVariant.getPrimarySlot();
24 var primarySlotProvider = outgoingVariant.getPrimarySlot();
25 var variant = outgoingVariant.getVariant();
25 var variant = outgoingVariant.getVariant();
26
26
27 // Bind publication state when the owning configuration is materialized.
27 // Bind publication state when the owning configuration is materialized.
28 outgoingVariant.configureOutgoing(configuration -> {
28 outgoingVariant.configureOutgoing(configuration -> {
29 var primarySlot = primarySlotProvider.get();
29 var primarySlot = primarySlotProvider.get();
30 var outgoing = configuration.getOutgoing();
30 var outgoing = configuration.getOutgoing();
31
31
32 // Bind the primary artifact set to the root outgoing configuration.
32 // Bind the primary artifact set to the root outgoing configuration.
33 resolver.when(
33 resolver.when(
34 new ArtifactSlot(variant, primarySlot),
34 new ArtifactSlot(variant, primarySlot),
35 assembly -> outgoing.artifact(assembly.getArtifact()));
35 assembly -> outgoing.artifact(
36 assembly.getArtifact(),
37 artifact -> artifact.builtBy(assembly.getAssemblyTask())));
36
38
37 // Bind non-primary slots to Gradle secondary artifact variants.
39 // Bind non-primary slots to Gradle secondary artifact variants.
38 slots.all(slot -> {
40 slots.all(slot -> {
39 if (slot.equals(primarySlot))
41 if (slot.equals(primarySlot))
40 return;
42 return;
41
43
42 resolver.when(
44 resolver.when(
43 new ArtifactSlot(variant, slot),
45 new ArtifactSlot(variant, slot),
44 // Gradle artifact variants must be created while the owning
46 // Gradle artifact variants must be created while the owning
45 // configuration is being materialized. Lazy registration may
47 // configuration is being materialized. Lazy registration may
46 // otherwise be realized only after dependency resolution starts.
48 // otherwise be realized only after dependency resolution starts.
47 assembly -> outgoing.getVariants()
49 assembly -> outgoing.getVariants()
48 .create(slot.getName())
50 .create(slot.getName())
49 .artifact(assembly.getArtifact()));
51 .artifact(
52 assembly.getArtifact(),
53 artifact -> artifact.builtBy(assembly.getAssemblyTask())));
50 });
54 });
51 });
55 });
52 }
56 }
53
57
54
58
55
59
56 }
60 }
@@ -1,202 +1,321
1 package org.implab.gradle.variants.artifacts.internal;
1 package org.implab.gradle.variants.artifacts.internal;
2
2
3 import java.util.HashMap;
3 import java.util.HashMap;
4 import java.util.HashSet;
4 import java.util.HashSet;
5 import java.util.Map;
5 import java.util.Map;
6 import java.util.Set;
6 import java.util.Set;
7 import java.util.function.Function;
8 import java.util.stream.Stream;
7
9
8 import org.eclipse.jdt.annotation.NonNullByDefault;
10 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.gradle.api.Action;
11 import org.gradle.api.Action;
12 import org.gradle.api.InvalidUserDataException;
13 import org.gradle.api.Task;
10 import org.gradle.api.file.ConfigurableFileCollection;
14 import org.gradle.api.file.ConfigurableFileCollection;
11 import org.gradle.api.file.Directory;
15 import org.gradle.api.file.Directory;
12 import org.gradle.api.file.DirectoryProperty;
16 import org.gradle.api.file.DirectoryProperty;
13 import org.gradle.api.file.FileCollection;
17 import org.gradle.api.file.FileCollection;
18 import org.gradle.api.file.FileSystemLocation;
14 import org.gradle.api.model.ObjectFactory;
19 import org.gradle.api.model.ObjectFactory;
15 import org.gradle.api.provider.Provider;
20 import org.gradle.api.provider.Provider;
16 import org.gradle.api.tasks.Sync;
21 import org.gradle.api.tasks.Sync;
17 import org.gradle.api.tasks.TaskContainer;
22 import org.gradle.api.tasks.TaskContainer;
23 import org.gradle.api.tasks.TaskProvider;
18 import org.gradle.language.base.plugins.LifecycleBasePlugin;
24 import org.gradle.language.base.plugins.LifecycleBasePlugin;
19 import org.implab.gradle.common.core.lang.FilePaths;
25 import org.implab.gradle.common.core.lang.FilePaths;
26 import org.implab.gradle.common.core.lang.Strings;
20 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
27 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
21 import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec;
28 import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec;
22 import org.implab.gradle.variants.artifacts.ArtifactSlot;
29 import org.implab.gradle.variants.artifacts.ArtifactSlot;
30 import org.implab.gradle.variants.artifacts.OutputSelectionSpec;
31 import org.implab.gradle.variants.core.Layer;
32 import org.implab.gradle.variants.core.Role;
23 import org.implab.gradle.variants.sources.CompileUnit;
33 import org.implab.gradle.variants.sources.CompileUnit;
24 import org.implab.gradle.variants.sources.CompileUnitsView;
34 import org.implab.gradle.variants.sources.CompileUnitsView;
25 import org.implab.gradle.variants.sources.RoleProjectionsView;
35 import org.implab.gradle.variants.sources.RoleProjectionsView;
26 import org.implab.gradle.variants.sources.SourceSetMaterializer;
36 import org.implab.gradle.variants.sources.SourceSetMaterializer;
27
37
28 /**
38 /**
29 * Adapts slot contribution declarations to materialized {@link ArtifactAssembly}
39 * Adapts slot contribution declarations to materialized
40 * {@link ArtifactAssembly}
30 * handles.
41 * handles.
31 *
42 *
32 * <p>The handler creates one {@link Sync} task per {@link ArtifactSlot}. The task
43 * <p>
33 * copies all collected slot inputs into a single output directory. That output
44 * Contribution-based assemblies create one {@link Sync} task per
34 * directory is then registered in {@link ArtifactAssemblyRegistry} as the
45 * {@link ArtifactSlot}. The task copies all collected slot inputs into a single
35 * published artifact for the slot.
46 * output directory. That output directory is then registered in
47 * {@link ArtifactAssemblyRegistry} as the published artifact for the slot.
36 *
48 *
37 * <p>Input collection uses {@link SlotContributionVisitor}. Each contribution is
49 * <p>
38 * converted to a {@link SlotInputKey}; duplicate keys are ignored so that repeated
50 * Task-produced assemblies bypass the managed copy task. The producer task is
51 * registered directly in {@link ArtifactAssemblyRegistry}, and its mapped output
52 * file or directory becomes the published slot artifact.
53 *
54 * <p>
55 * Input collection uses {@link SlotContributionVisitor}. Each contribution is
56 * converted to a {@link SlotInputKey}; duplicate keys are ignored so that
57 * repeated
39 * topology-based selections do not add the same input twice.
58 * topology-based selections do not add the same input twice.
40 */
59 */
41 @NonNullByDefault
60 @NonNullByDefault
42 public class ArtifactAssemblyHandler {
61 public class ArtifactAssemblyHandler {
43 private final ObjectFactory objects;
62 private final ObjectFactory objects;
44
63
45 private final ArtifactAssemblyRegistry assemblyRegistry;
64 private final ArtifactAssemblyRegistry assemblyRegistry;
46
65
47 private final DirectoryProperty assembliesDirectory;
66 private final DirectoryProperty assembliesDirectory;
48
67
49 private final TaskContainer tasks;
68 private final TaskContainer tasks;
50
69
51 private final CompileUnitsView compileUnitsView;
70 private final CompileUnitsView compileUnitsView;
52
71
53 private final RoleProjectionsView roleProjectionsView;
72 private final RoleProjectionsView roleProjectionsView;
54
73
55 private final SourceSetMaterializer sourceSetMaterializer;
74 private final SourceSetMaterializer sourceSetMaterializer;
56
75
57 private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>();
76 private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>();
58
77
78 private final Map<ArtifactSlot, AssemblyMode> assemblyModes = new HashMap<>();
79
59 public ArtifactAssemblyHandler(
80 public ArtifactAssemblyHandler(
60 ObjectFactory objects,
81 ObjectFactory objects,
61 TaskContainer tasks,
82 TaskContainer tasks,
62 ArtifactAssemblyRegistry assemblyRegistry,
83 ArtifactAssemblyRegistry assemblyRegistry,
63 CompileUnitsView compileUnitsView,
84 CompileUnitsView compileUnitsView,
64 RoleProjectionsView roleProjectionsView,
85 RoleProjectionsView roleProjectionsView,
65 SourceSetMaterializer sourceSetMaterializer) {
86 SourceSetMaterializer sourceSetMaterializer) {
66 this.objects = objects;
87 this.objects = objects;
67 this.tasks = tasks;
88 this.tasks = tasks;
68 this.assemblyRegistry = assemblyRegistry;
89 this.assemblyRegistry = assemblyRegistry;
69 this.compileUnitsView = compileUnitsView;
90 this.compileUnitsView = compileUnitsView;
70 this.roleProjectionsView = roleProjectionsView;
91 this.roleProjectionsView = roleProjectionsView;
71 this.sourceSetMaterializer = sourceSetMaterializer;
92 this.sourceSetMaterializer = sourceSetMaterializer;
72
93
73 assembliesDirectory = objects.directoryProperty();
94 assembliesDirectory = objects.directoryProperty();
74 }
95 }
75
96
76 public DirectoryProperty getAssembliesDirectory() {
97 public DirectoryProperty getAssembliesDirectory() {
77 return assembliesDirectory;
98 return assembliesDirectory;
78 }
99 }
79
100
80 public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) {
101 public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) {
81 var visitor = contributionVisitor(artifactSlot);
102 var spec = new DefaultArtifactAssemblySpec(artifactSlot);
82 var spec = new DefaultArtifactAssemblySpec(objects, c -> c.accept(visitor));
83 action.execute(spec);
103 action.execute(spec);
84 }
104 }
85
105
86 public SlotContributionVisitor contributionVisitor(ArtifactSlot artifactSlot) {
106 private void useAssemblyMode(ArtifactSlot artifactSlot, AssemblyMode mode) {
87 var assembly = slotInputs.computeIfAbsent(artifactSlot, this::createSlotAssembly);
107 var previous = assemblyModes.putIfAbsent(artifactSlot, mode);
88 return new ContributionVisitor(artifactSlot, assembly);
108 if (previous != null && previous != mode) {
109 throw new InvalidUserDataException("Artifact slot '" + artifactSlot
110 + "' cannot mix task-produced artifact and contribution-based assembly");
111 }
89 }
112 }
90
113
91 /**
114 /**
92 * Creates the assembly task for the given slot and registers its output artifact.
115 * Creates the assembly task for the given slot and registers its output
116 * artifact.
93 */
117 */
94 private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) {
118 private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) {
95 var assembly = new SlotAssembly();
119 var assembly = new SlotAssembly();
96 var fileCollection = assembly.inputs();
120 var fileCollection = assembly.inputs();
97
121
98 var outputDirectory = outputDirectory(artifactSlot);
122 var outputDirectory = outputDirectory(artifactSlot);
99
123
100 var task = tasks.register(assembleTaskName(artifactSlot), Sync.class, copy -> {
124 var task = tasks.register(assembleTaskName(artifactSlot), Sync.class, copy -> {
101 copy.setGroup(LifecycleBasePlugin.BUILD_GROUP);
125 copy.setGroup(LifecycleBasePlugin.BUILD_GROUP);
102 copy.into(outputDirectory);
126 copy.into(outputDirectory);
103 copy.from(fileCollection);
127 copy.from(fileCollection);
104 });
128 });
105
129
106 assemblyRegistry.register(artifactSlot, task, t -> outputDirectory);
130 assemblyRegistry.register(artifactSlot, task, t -> outputDirectory);
107
131
108 return assembly;
132 return assembly;
109 }
133 }
110
134
111 private String assembleTaskName(ArtifactSlot artifactSlot) {
135 private String assembleTaskName(ArtifactSlot artifactSlot) {
112 var variantName = artifactSlot.variant().getName();
136 var variantName = artifactSlot.variant().getName();
113 var slotName = artifactSlot.slot().getName();
137 var slotName = artifactSlot.slot().getName();
114
138
115 return "assembleVariantArtifactSlot"
139 return "assembleVariantArtifactSlot"
116 + "_v" + variantName.length() + "_" + variantName
140 + "_v" + variantName.length() + "_" + variantName
117 + "_s" + slotName.length() + "_" + slotName;
141 + "_s" + slotName.length() + "_" + slotName;
118 }
142 }
119
143
120 private Provider<Directory> outputDirectory(ArtifactSlot artifactSlot) {
144 private Provider<Directory> outputDirectory(ArtifactSlot artifactSlot) {
121 return assembliesDirectory.dir(
145 return assembliesDirectory.dir(
122 FilePaths.cat(
146 FilePaths.cat(
123 artifactSlot.variant().getName(),
147 artifactSlot.variant().getName(),
124 artifactSlot.slot().getName()));
148 artifactSlot.slot().getName()));
125 }
149 }
126
150
127 /**
151 /**
128 * Collects slot contributions into one {@link ConfigurableFileCollection}.
152 * Collects slot contributions into one {@link ConfigurableFileCollection}.
129 */
153 */
130 private class ContributionVisitor implements SlotContributionVisitor {
154 private class ContributionVisitor implements SlotContributionVisitor {
131 // artifact slot for this assembly
155 // artifact slot for this assembly
132 private final ArtifactSlot artifactSlot;
156 private final ArtifactSlot artifactSlot;
133
157
134 // seen inputs, used for deduplication
158 // seen inputs, used for deduplication
135 private final SlotAssembly assembly;
159 private final SlotAssembly assembly;
136
160
137 ContributionVisitor(ArtifactSlot artifactSlot, SlotAssembly assembly) {
161 ContributionVisitor(ArtifactSlot artifactSlot, SlotAssembly assembly) {
138 this.artifactSlot = artifactSlot;
162 this.artifactSlot = artifactSlot;
139 this.assembly = assembly;
163 this.assembly = assembly;
140 }
164 }
141
165
142 @Override
166 @Override
143 public void visit(DirectContribution contribution) {
167 public void visit(DirectContribution contribution) {
144 contribute(
168 contribute(
145 SlotInputKey.newUniqueKey("Direct input for " + artifactSlot),
169 SlotInputKey.newUniqueKey("Direct input for " + artifactSlot),
146 contribution.input());
170 contribution.input());
147 }
171 }
148
172
149 @Override
173 @Override
150 public void visit(VariantOutputsContribution contribution) {
174 public void visit(VariantOutputsContribution contribution) {
151 var units = compileUnitsView.getUnitsForVariant(artifactSlot.variant());
175 var units = compileUnitsView.getUnitsForVariant(artifactSlot.variant());
152 contributeCompileUnits(units, contribution.outputs());
176 contributeCompileUnits(units, contribution.outputs());
153 }
177 }
154
178
155 @Override
179 @Override
156 public void visit(RoleOutputsContribution contribution) {
180 public void visit(RoleOutputsContribution contribution) {
157 var roleProjection = roleProjectionsView.requireProjection(artifactSlot.variant(),
181 var roleProjection = roleProjectionsView.requireProjection(artifactSlot.variant(),
158 contribution.role());
182 contribution.role());
159 var units = roleProjectionsView.getUnits(roleProjection);
183 var units = roleProjectionsView.getUnits(roleProjection);
160
184
161 contributeCompileUnits(units, contribution.outputs());
185 contributeCompileUnits(units, contribution.outputs());
162
186
163 }
187 }
164
188
165 @Override
189 @Override
166 public void visit(LayerOutputsContribution contribution) {
190 public void visit(LayerOutputsContribution contribution) {
167 var unit = compileUnitsView.requireUnit(artifactSlot.variant(), contribution.layer());
191 var unit = compileUnitsView.requireUnit(artifactSlot.variant(), contribution.layer());
168 contributeCompileUnits(Set.of(unit), contribution.outputs());
192 contributeCompileUnits(Set.of(unit), contribution.outputs());
169 }
193 }
170
194
171 private void contributeCompileUnits(Set<CompileUnit> units, Set<String> outputs) {
195 private void contributeCompileUnits(Set<CompileUnit> units, Set<String> outputs) {
172 units.stream()
196 units.stream()
173 // expand variant compile units, make (compileUnit, outputName) pairs
197 // expand variant compile units, make (compileUnit, outputName) pairs
174 .flatMap(unit -> outputs.stream()
198 .flatMap(unit -> outputs.stream()
175 .map(output -> new CompileUnitOutputKey(unit, output)))
199 .map(output -> new CompileUnitOutputKey(unit, output)))
176 .forEach(key -> contribute(
200 .forEach(key -> contribute(
177 key,
201 key,
178 sourceSetMaterializer.getSourceSet(key.unit())
202 sourceSetMaterializer.getSourceSet(key.unit())
179 .map(s -> s.output(key.outputName()))));
203 .map(s -> s.output(key.outputName()))));
180 }
204 }
181
205
182 private void contribute(SlotInputKey key, Object resolvedInput) {
206 private void contribute(SlotInputKey key, Object resolvedInput) {
183 assembly.addSlotInput(key, resolvedInput);
207 assembly.addSlotInput(key, resolvedInput);
184 }
208 }
185 }
209 }
186
210
187 /** Mutable input state for one slot assembly. */
211 /** Mutable input state for one slot assembly. */
188 class SlotAssembly {
212 class SlotAssembly {
189 private final ConfigurableFileCollection inputs = objects.fileCollection();
213 private final ConfigurableFileCollection inputs = objects.fileCollection();
190 private final Set<SlotInputKey> seen = new HashSet<>();
214 private final Set<SlotInputKey> seen = new HashSet<>();
191
215
192 public void addSlotInput(SlotInputKey key, Object input) {
216 public void addSlotInput(SlotInputKey key, Object input) {
193 if (!seen.add(key))
217 if (!seen.add(key))
194 return;
218 return;
195 inputs.from(input);
219 inputs.from(input);
196 }
220 }
197
221
198 public FileCollection inputs() {
222 public FileCollection inputs() {
199 return inputs;
223 return inputs;
200 }
224 }
201 }
225 }
226
227 private enum AssemblyMode {
228 CONTRIBUTIONS,
229 TASK_PRODUCER
230 }
231
232 /**
233 * Default DSL facade for collecting {@link SlotContribution} declarations.
234 *
235 * <p>
236 * The spec does not validate topology references immediately. It translates DSL
237 * calls to contribution objects and passes them to the supplied consumer;
238 * semantic
239 * validation happens later when the assembly handler resolves contributions
240 * against the finalized source model.
241 */
242 class DefaultArtifactAssemblySpec implements ArtifactAssemblySpec {
243
244 private final ArtifactSlot artifactSlot;
245
246 DefaultArtifactAssemblySpec(ArtifactSlot artifactSlot) {
247 this.artifactSlot = artifactSlot;
248 }
249
250 @Override
251 public void from(Object artifact) {
252 contribute(new DirectContribution(artifact));
253 }
254
255 @Override
256 public <T extends Task> void producedBy(
257 TaskProvider<T> task,
258 Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact) {
259 registerProducedArtifact(task, artifact);
260 }
261
262 @Override
263 public void fromVariant(Action<? super OutputSelectionSpec> action) {
264 contribute(new VariantOutputsContribution(outputs(action)));
265 }
266
267 @Override
268 public void fromRole(String roleName, Action<? super OutputSelectionSpec> action) {
269
270 contribute(new RoleOutputsContribution(
271 objects.named(Role.class, roleName),
272 outputs(action)));
273 }
274
275 @Override
276 public void fromLayer(String layerName, Action<? super OutputSelectionSpec> action) {
277 contribute(new LayerOutputsContribution(
278 objects.named(Layer.class, layerName),
279 outputs(action)));
280 }
281
282 private static Set<String> outputs(Action<? super OutputSelectionSpec> action) {
283 var spec = new OutputsSetSpec();
284 action.execute(spec);
285 return spec.outputs();
286 }
287
288 void contribute(SlotContribution contribution) {
289 useAssemblyMode(artifactSlot, AssemblyMode.CONTRIBUTIONS);
290 var assembly = slotInputs.computeIfAbsent(artifactSlot, ArtifactAssemblyHandler.this::createSlotAssembly);
291 var contributionVisitor = new ContributionVisitor(artifactSlot, assembly);
292 contribution.accept(contributionVisitor);
293 }
294
295 <T extends Task> void registerProducedArtifact(
296 TaskProvider<T> task,
297 Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact) {
298 useAssemblyMode(artifactSlot, AssemblyMode.TASK_PRODUCER);
299 assemblyRegistry.register(artifactSlot, task, artifact);
300 }
301
302 }
303
304 /** Simple implementation of {@link OutputSelectionSpec}. */
305 static class OutputsSetSpec implements OutputSelectionSpec {
306 private final Set<String> outputs = new HashSet<>();
307
308 @Override
309 public void output(String name, String... extra) {
310 Stream.concat(Stream.of(name), Stream.of(extra))
311 .map(Strings::requireNonBlank)
312 .forEach(outputs::add);
313 }
314
315 Set<String> outputs() {
316 return Set.copyOf(outputs);
317 }
318
319 }
320
202 }
321 }
@@ -1,703 +1,867
1 package org.implab.gradle.variants;
1 package org.implab.gradle.variants;
2
2
3 import static org.junit.jupiter.api.Assertions.assertTrue;
3 import static org.junit.jupiter.api.Assertions.assertTrue;
4
4
5 import org.gradle.testkit.runner.BuildResult;
5 import org.gradle.testkit.runner.BuildResult;
6 import org.gradle.testkit.runner.TaskOutcome;
6 import org.gradle.testkit.runner.TaskOutcome;
7 import org.junit.jupiter.api.Test;
7 import org.junit.jupiter.api.Test;
8
8
9 class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest {
9 class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest {
10
10
11 @Test
11 @Test
12 void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception {
12 void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception {
13 writeFile("settings.gradle", """
13 writeFile("settings.gradle", """
14 rootProject.name = 'gradle-reference-outgoing-resolution'
14 rootProject.name = 'gradle-reference-outgoing-resolution'
15 include 'producer', 'consumer'
15 include 'producer', 'consumer'
16 """);
16 """);
17 writeFile("producer/inputs/typesPackage", "types\n");
17 writeFile("producer/inputs/typesPackage", "types\n");
18 writeFile("producer/inputs/js", "js\n");
18 writeFile("producer/inputs/js", "js\n");
19 writeBuildFile("""
19 writeBuildFile("""
20 import org.gradle.api.attributes.Attribute
20 import org.gradle.api.attributes.Attribute
21
21
22 def variantAttr = Attribute.of('test.variant', String)
22 def variantAttr = Attribute.of('test.variant', String)
23 def slotAttr = Attribute.of('test.slot', String)
23 def slotAttr = Attribute.of('test.slot', String)
24
24
25 project(':producer') {
25 project(':producer') {
26 def browserElements = configurations.consumable('browserElements')
26 def browserElements = configurations.consumable('browserElements')
27
27
28 println('reference: registered browserElements provider')
28 println('reference: registered browserElements provider')
29
29
30 browserElements.configure { configuration ->
30 browserElements.configure { configuration ->
31 println('reference: configuring browserElements')
31 println('reference: configuring browserElements')
32
32
33 configuration.attributes.attribute(variantAttr, 'browser')
33 configuration.attributes.attribute(variantAttr, 'browser')
34 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
34 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
35 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
35 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
36
36
37 configuration.outgoing.variants.create('js') { secondary ->
37 configuration.outgoing.variants.create('js') { secondary ->
38 println('reference: creating js outgoing variant')
38 println('reference: creating js outgoing variant')
39
39
40 secondary.attributes.attribute(slotAttr, 'js')
40 secondary.attributes.attribute(slotAttr, 'js')
41 secondary.artifact(layout.projectDirectory.file('inputs/js'))
41 secondary.artifact(layout.projectDirectory.file('inputs/js'))
42 }
42 }
43 }
43 }
44 }
44 }
45
45
46 project(':consumer') {
46 project(':consumer') {
47 configurations {
47 configurations {
48 compileView {
48 compileView {
49 canBeResolved = true
49 canBeResolved = true
50 canBeConsumed = false
50 canBeConsumed = false
51 canBeDeclared = true
51 canBeDeclared = true
52 attributes {
52 attributes {
53 attribute(variantAttr, 'browser')
53 attribute(variantAttr, 'browser')
54 attribute(slotAttr, 'typesPackage')
54 attribute(slotAttr, 'typesPackage')
55 }
55 }
56 }
56 }
57 }
57 }
58
58
59 dependencies {
59 dependencies {
60 compileView project(':producer')
60 compileView project(':producer')
61 }
61 }
62
62
63 tasks.register('probe') {
63 tasks.register('probe') {
64 doLast {
64 doLast {
65 println('reference: resolving primary files')
65 println('reference: resolving primary files')
66
66
67 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
67 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
68
68
69 println('reference: resolving secondary files')
69 println('reference: resolving secondary files')
70
70
71 def jsFiles = configurations.compileView.incoming.artifactView {
71 def jsFiles = configurations.compileView.incoming.artifactView {
72 attributes {
72 attributes {
73 attribute(slotAttr, 'js')
73 attribute(slotAttr, 'js')
74 }
74 }
75 }.files.files.collect { it.name }.sort().join(',')
75 }.files.files.collect { it.name }.sort().join(',')
76
76
77 println('compileFiles=' + compileFiles)
77 println('compileFiles=' + compileFiles)
78 println('jsFiles=' + jsFiles)
78 println('jsFiles=' + jsFiles)
79 }
79 }
80 }
80 }
81 }
81 }
82 """);
82 """);
83
83
84 BuildResult result = runner(":consumer:probe").build();
84 BuildResult result = runner(":consumer:probe").build();
85 var output = result.getOutput();
85 var output = result.getOutput();
86 var registered = output.indexOf("reference: registered browserElements provider");
86 var registered = output.indexOf("reference: registered browserElements provider");
87 var resolvingPrimary = output.indexOf("reference: resolving primary files");
87 var resolvingPrimary = output.indexOf("reference: resolving primary files");
88 var configuring = output.indexOf("reference: configuring browserElements");
88 var configuring = output.indexOf("reference: configuring browserElements");
89 var creatingSecondary = output.indexOf("reference: creating js outgoing variant");
89 var creatingSecondary = output.indexOf("reference: creating js outgoing variant");
90 var resolvingSecondary = output.indexOf("reference: resolving secondary files");
90 var resolvingSecondary = output.indexOf("reference: resolving secondary files");
91
91
92 assertTrue(registered >= 0);
92 assertTrue(registered >= 0);
93 assertTrue(resolvingPrimary >= 0);
93 assertTrue(resolvingPrimary >= 0);
94 assertTrue(configuring >= 0);
94 assertTrue(configuring >= 0);
95 assertTrue(creatingSecondary >= 0);
95 assertTrue(creatingSecondary >= 0);
96 assertTrue(resolvingSecondary >= 0);
96 assertTrue(resolvingSecondary >= 0);
97 assertTrue(registered < resolvingPrimary);
97 assertTrue(registered < resolvingPrimary);
98 assertTrue(resolvingPrimary < configuring);
98 assertTrue(resolvingPrimary < configuring);
99 assertTrue(configuring < creatingSecondary);
99 assertTrue(configuring < creatingSecondary);
100 assertTrue(creatingSecondary < resolvingSecondary);
100 assertTrue(creatingSecondary < resolvingSecondary);
101 assertTrue(output.contains("compileFiles=typesPackage"));
101 assertTrue(output.contains("compileFiles=typesPackage"));
102 assertTrue(output.contains("jsFiles=js"));
102 assertTrue(output.contains("jsFiles=js"));
103 }
103 }
104
104
105 @Test
105 @Test
106 void gradleReferenceRegisteredSecondaryArtifactVariantIsNotRealizedBeforeResolution() throws Exception {
106 void gradleReferenceRegisteredSecondaryArtifactVariantIsNotRealizedBeforeResolution() throws Exception {
107 // Gradle issue: https://github.com/gradle/gradle/issues/27441
107 // Gradle issue: https://github.com/gradle/gradle/issues/27441
108 // Registered outgoing artifact variants are not realized before dependency resolution.
108 // Registered outgoing artifact variants are not realized before dependency resolution.
109 writeFile("settings.gradle", """
109 writeFile("settings.gradle", """
110 rootProject.name = 'gradle-reference-registered-secondary-variant'
110 rootProject.name = 'gradle-reference-registered-secondary-variant'
111 include 'producer', 'consumer'
111 include 'producer', 'consumer'
112 """);
112 """);
113 writeFile("producer/inputs/typesPackage", "types\n");
113 writeFile("producer/inputs/typesPackage", "types\n");
114 writeFile("producer/inputs/js", "js\n");
114 writeFile("producer/inputs/js", "js\n");
115 writeFile("build.gradle", """
115 writeFile("build.gradle", """
116 import org.gradle.api.attributes.Attribute
116 import org.gradle.api.attributes.Attribute
117
117
118 def variantAttr = Attribute.of('test.variant', String)
118 def variantAttr = Attribute.of('test.variant', String)
119 def slotAttr = Attribute.of('test.slot', String)
119 def slotAttr = Attribute.of('test.slot', String)
120
120
121 project(':producer') {
121 project(':producer') {
122 def browserElements = configurations.consumable('browserElements')
122 def browserElements = configurations.consumable('browserElements')
123
123
124 browserElements.configure { configuration ->
124 browserElements.configure { configuration ->
125 configuration.attributes.attribute(variantAttr, 'browser')
125 configuration.attributes.attribute(variantAttr, 'browser')
126 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
126 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
127 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
127 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
128
128
129 configuration.outgoing.variants.register('js') { secondary ->
129 configuration.outgoing.variants.register('js') { secondary ->
130 secondary.attributes.attribute(slotAttr, 'js')
130 secondary.attributes.attribute(slotAttr, 'js')
131 secondary.artifact(layout.projectDirectory.file('inputs/js'))
131 secondary.artifact(layout.projectDirectory.file('inputs/js'))
132 }
132 }
133 }
133 }
134 }
134 }
135
135
136 project(':consumer') {
136 project(':consumer') {
137 configurations {
137 configurations {
138 compileView {
138 compileView {
139 canBeResolved = true
139 canBeResolved = true
140 canBeConsumed = false
140 canBeConsumed = false
141 canBeDeclared = true
141 canBeDeclared = true
142 attributes {
142 attributes {
143 attribute(variantAttr, 'browser')
143 attribute(variantAttr, 'browser')
144 attribute(slotAttr, 'typesPackage')
144 attribute(slotAttr, 'typesPackage')
145 }
145 }
146 }
146 }
147 }
147 }
148
148
149 dependencies {
149 dependencies {
150 compileView project(':producer')
150 compileView project(':producer')
151 }
151 }
152
152
153 tasks.register('probe') {
153 tasks.register('probe') {
154 doLast {
154 doLast {
155 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
155 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
156 def jsFiles = configurations.compileView.incoming.artifactView {
156 def jsFiles = configurations.compileView.incoming.artifactView {
157 attributes {
157 attributes {
158 attribute(slotAttr, 'js')
158 attribute(slotAttr, 'js')
159 }
159 }
160 }.files.files.collect { it.name }.sort().join(',')
160 }.files.files.collect { it.name }.sort().join(',')
161
161
162 println('compileFiles=' + compileFiles)
162 println('compileFiles=' + compileFiles)
163 println('jsFiles=' + jsFiles)
163 println('jsFiles=' + jsFiles)
164 }
164 }
165 }
165 }
166 }
166 }
167 """);
167 """);
168
168
169 assertBuildFails("Cannot create variant 'js' after dependency configuration ':producer:browserElements' has been resolved",
169 assertBuildFails("Cannot create variant 'js' after dependency configuration ':producer:browserElements' has been resolved",
170 ":consumer:probe");
170 ":consumer:probe");
171 }
171 }
172
172
173 @Test
173 @Test
174 void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception {
174 void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception {
175 writeSettings("variant-artifacts-slots");
175 writeSettings("variant-artifacts-slots");
176 writeFile("inputs/base.js", "console.log('base')\n");
176 writeFile("inputs/base.js", "console.log('base')\n");
177 writeFile("inputs/amd.js", "console.log('amd')\n");
177 writeFile("inputs/amd.js", "console.log('amd')\n");
178 writeFile("inputs/mainJs.txt", "mainJs marker\n");
178 writeFile("inputs/mainJs.txt", "mainJs marker\n");
179 writeFile("inputs/amdJs.txt", "amdJs marker\n");
179 writeFile("inputs/amdJs.txt", "amdJs marker\n");
180 writeBuildFile("""
180 writeBuildFile("""
181 import org.gradle.api.attributes.Attribute
181 import org.gradle.api.attributes.Attribute
182
182
183 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
183 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
184
184
185 def variantAttr = Attribute.of('test.variant', String)
185 def variantAttr = Attribute.of('test.variant', String)
186 def slotAttr = Attribute.of('test.slot', String)
186 def slotAttr = Attribute.of('test.slot', String)
187
187
188 variants.layers.create('mainBase')
188 variants.layers.create('mainBase')
189 variants.layers.create('mainAmd')
189 variants.layers.create('mainAmd')
190 variants.roles.create('main')
190 variants.roles.create('main')
191 variants.roles.create('test')
191 variants.roles.create('test')
192 variants.variant('browser') {
192 variants.variant('browser') {
193 role('main') {
193 role('main') {
194 layers('mainBase', 'mainAmd')
194 layers('mainBase', 'mainAmd')
195 }
195 }
196 }
196 }
197
197
198 variantSources {
198 variantSources {
199 layer('mainBase') {
199 layer('mainBase') {
200 sourceSet {
200 sourceSet {
201 declareOutputs('js')
201 declareOutputs('js')
202 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
202 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
203 }
203 }
204 }
204 }
205 layer('mainAmd') {
205 layer('mainAmd') {
206 sourceSet {
206 sourceSet {
207 declareOutputs('js')
207 declareOutputs('js')
208 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
208 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
209 }
209 }
210 }
210 }
211 }
211 }
212
212
213 variantArtifacts {
213 variantArtifacts {
214 variant('browser') {
214 variant('browser') {
215 primarySlot('mainJs') {
215 primarySlot('mainJs') {
216 fromRole('main') {
216 fromRole('main') {
217 output('js')
217 output('js')
218 }
218 }
219 from(layout.projectDirectory.file('inputs/mainJs.txt'))
219 from(layout.projectDirectory.file('inputs/mainJs.txt'))
220 }
220 }
221 slot('amdJs') {
221 slot('amdJs') {
222 fromLayer('mainAmd') {
222 fromLayer('mainAmd') {
223 output('js')
223 output('js')
224 }
224 }
225 from(layout.projectDirectory.file('inputs/amdJs.txt'))
225 from(layout.projectDirectory.file('inputs/amdJs.txt'))
226 }
226 }
227 }
227 }
228
228
229 whenOutgoingConfiguration { publication ->
229 whenOutgoingConfiguration { publication ->
230 publication.configuration {
230 publication.configuration {
231 attributes.attribute(variantAttr, publication.variant.name)
231 attributes.attribute(variantAttr, publication.variant.name)
232 }
232 }
233 }
233 }
234
234
235 whenOutgoingSlot { publication ->
235 whenOutgoingSlot { publication ->
236 def slotName = publication.artifactSlot.slot.name
236 def slotName = publication.artifactSlot.slot.name
237 publication.artifactAttributes {
237 publication.artifactAttributes {
238 attribute(slotAttr, slotName)
238 attribute(slotAttr, slotName)
239 }
239 }
240 }
240 }
241 }
241 }
242
242
243 tasks.register('probe') {
243 tasks.register('probe') {
244 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs'
244 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs'
245 dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs'
245 dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs'
246
246
247 doLast {
247 doLast {
248 def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile
248 def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile
249 def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile
249 def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile
250
250
251 assert new File(mainDir, 'base.js').exists()
251 assert new File(mainDir, 'base.js').exists()
252 assert new File(mainDir, 'amd.js').exists()
252 assert new File(mainDir, 'amd.js').exists()
253 assert new File(mainDir, 'mainJs.txt').exists()
253 assert new File(mainDir, 'mainJs.txt').exists()
254
254
255 assert !new File(amdDir, 'base.js').exists()
255 assert !new File(amdDir, 'base.js').exists()
256 assert new File(amdDir, 'amd.js').exists()
256 assert new File(amdDir, 'amd.js').exists()
257 assert new File(amdDir, 'amdJs.txt').exists()
257 assert new File(amdDir, 'amdJs.txt').exists()
258
258
259 def elements = configurations.getByName('browserElements')
259 def elements = configurations.getByName('browserElements')
260 def amdVariant = elements.outgoing.variants.getByName('amdJs')
260 def amdVariant = elements.outgoing.variants.getByName('amdJs')
261
261
262 println('variantAttr=' + elements.attributes.getAttribute(variantAttr))
262 println('variantAttr=' + elements.attributes.getAttribute(variantAttr))
263 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
263 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
264 println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr))
264 println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr))
265 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
265 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
266 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
266 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
267 }
267 }
268 }
268 }
269 """);
269 """);
270
270
271 BuildResult result = runner("probe").build();
271 BuildResult result = runner("probe").build();
272
272
273 assertTrue(result.getOutput().contains("variantAttr=browser"));
273 assertTrue(result.getOutput().contains("variantAttr=browser"));
274 assertTrue(result.getOutput().contains("primarySlotAttr=mainJs"));
274 assertTrue(result.getOutput().contains("primarySlotAttr=mainJs"));
275 assertTrue(result.getOutput().contains("amdSlotAttr=amdJs"));
275 assertTrue(result.getOutput().contains("amdSlotAttr=amdJs"));
276 assertTrue(result.getOutput().contains("configurations=browserElements"));
276 assertTrue(result.getOutput().contains("configurations=browserElements"));
277 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
277 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
278 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
278 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
279 }
279 }
280
280
281 @Test
281 @Test
282 void outgoingSlotHookFollowsMaterializedGradleArtifactVariants() throws Exception {
282 void outgoingSlotHookFollowsMaterializedGradleArtifactVariants() throws Exception {
283 writeSettings("variant-artifacts-materialized-gradle-variant");
283 writeSettings("variant-artifacts-materialized-gradle-variant");
284 writeFile("inputs/typesPackage", "types\n");
284 writeFile("inputs/typesPackage", "types\n");
285 writeFile("inputs/js", "js\n");
285 writeFile("inputs/js", "js\n");
286 writeBuildFile("""
286 writeBuildFile("""
287 import org.gradle.api.attributes.Attribute
287 import org.gradle.api.attributes.Attribute
288
288
289 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
289 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
290
290
291 def slotAttr = Attribute.of('test.slot', String)
291 def slotAttr = Attribute.of('test.slot', String)
292
292
293 variants.layers.create('main')
293 variants.layers.create('main')
294 variants.roles.create('main')
294 variants.roles.create('main')
295 variants.variant('browser') {
295 variants.variant('browser') {
296 role('main') {
296 role('main') {
297 layers('main')
297 layers('main')
298 }
298 }
299 }
299 }
300
300
301 variantArtifacts {
301 variantArtifacts {
302 variant('browser') {
302 variant('browser') {
303 primarySlot('typesPackage') {
303 primarySlot('typesPackage') {
304 from(layout.projectDirectory.file('inputs/typesPackage'))
304 from(layout.projectDirectory.file('inputs/typesPackage'))
305 }
305 }
306 }
306 }
307
307
308 whenOutgoingConfiguration { publication ->
308 whenOutgoingConfiguration { publication ->
309 publication.configuration {
309 publication.configuration {
310 outgoing.variants.create('js') { secondary ->
310 outgoing.variants.create('js') { secondary ->
311 secondary.artifact(layout.projectDirectory.file('inputs/js'))
311 secondary.artifact(layout.projectDirectory.file('inputs/js'))
312 }
312 }
313 }
313 }
314 }
314 }
315
315
316 whenOutgoingSlot { publication ->
316 whenOutgoingSlot { publication ->
317 publication.artifactAttributes {
317 publication.artifactAttributes {
318 attribute(slotAttr, publication.artifactSlot.slot.name)
318 attribute(slotAttr, publication.artifactSlot.slot.name)
319 }
319 }
320 }
320 }
321 }
321 }
322
322
323 tasks.register('probe') {
323 tasks.register('probe') {
324 doLast {
324 doLast {
325 def elements = configurations.getByName('browserElements')
325 def elements = configurations.getByName('browserElements')
326 def jsVariant = elements.outgoing.variants.getByName('js')
326 def jsVariant = elements.outgoing.variants.getByName('js')
327
327
328 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
328 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
329 println('jsSlotAttr=' + jsVariant.attributes.getAttribute(slotAttr))
329 println('jsSlotAttr=' + jsVariant.attributes.getAttribute(slotAttr))
330 }
330 }
331 }
331 }
332 """);
332 """);
333
333
334 BuildResult result = runner("probe").build();
334 BuildResult result = runner("probe").build();
335
335
336 assertTrue(result.getOutput().contains("primarySlotAttr=typesPackage"));
336 assertTrue(result.getOutput().contains("primarySlotAttr=typesPackage"));
337 assertTrue(result.getOutput().contains("jsSlotAttr=js"));
337 assertTrue(result.getOutput().contains("jsSlotAttr=js"));
338 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
338 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
339 }
339 }
340
340
341 @Test
341 @Test
342 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
342 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
343 writeSettings("variant-artifacts-single-slot");
343 writeSettings("variant-artifacts-single-slot");
344 writeBuildFile("""
344 writeBuildFile("""
345 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
345 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
346
346
347 variants.layers.create('main')
347 variants.layers.create('main')
348 variants.roles.create('main')
348 variants.roles.create('main')
349 variants.variant('browser') {
349 variants.variant('browser') {
350 role('main') {
350 role('main') {
351 layers('main')
351 layers('main')
352 }
352 }
353 }
353 }
354
354
355 variantSources.layer('main') {
355 variantSources.layer('main') {
356 sourceSet {
356 sourceSet {
357 declareOutputs('types')
357 declareOutputs('types')
358 }
358 }
359 }
359 }
360
360
361 variantArtifacts {
361 variantArtifacts {
362 variant('browser') {
362 variant('browser') {
363 slot('typesPackage') {
363 slot('typesPackage') {
364 fromVariant {
364 fromVariant {
365 output('types')
365 output('types')
366 }
366 }
367 }
367 }
368 }
368 }
369 }
369 }
370
370
371 tasks.register('probe') {
371 tasks.register('probe') {
372 doLast {
372 doLast {
373 variantArtifacts.whenAvailable { ctx ->
373 variantArtifacts.whenAvailable { ctx ->
374 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
374 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
375 println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name)
375 println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name)
376 }
376 }
377 }
377 }
378 }
378 }
379 """);
379 """);
380
380
381 BuildResult result = runner("probe").build();
381 BuildResult result = runner("probe").build();
382
382
383 assertTrue(result.getOutput().contains("primary=typesPackage"));
383 assertTrue(result.getOutput().contains("primary=typesPackage"));
384 }
384 }
385
385
386 @Test
386 @Test
387 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
387 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
388 writeSettings("variant-artifacts-direct-input");
388 writeSettings("variant-artifacts-direct-input");
389 writeFile("inputs/bundle.js", "console.log('bundle')\n");
389 writeFile("inputs/bundle.js", "console.log('bundle')\n");
390 writeBuildFile("""
390 writeBuildFile("""
391 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
391 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
392
392
393 variants.layers.create('main')
393 variants.layers.create('main')
394 variants.roles.create('main')
394 variants.roles.create('main')
395 variants.variant('browser') {
395 variants.variant('browser') {
396 role('main') {
396 role('main') {
397 layers('main')
397 layers('main')
398 }
398 }
399 }
399 }
400
400
401 variantArtifacts {
401 variantArtifacts {
402 variant('browser') {
402 variant('browser') {
403 primarySlot('bundle') {
403 primarySlot('bundle') {
404 from(layout.projectDirectory.file('inputs/bundle.js'))
404 from(layout.projectDirectory.file('inputs/bundle.js'))
405 }
405 }
406 }
406 }
407 }
407 }
408
408
409 tasks.register('probe') {
409 tasks.register('probe') {
410 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
410 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
411
411
412 doLast {
412 doLast {
413 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
413 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
414 assert new File(bundleDir, 'bundle.js').exists()
414 assert new File(bundleDir, 'bundle.js').exists()
415 }
415 }
416 }
416 }
417 """);
417 """);
418
418
419 BuildResult result = runner("probe").build();
419 BuildResult result = runner("probe").build();
420
420
421 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
421 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
422 }
422 }
423
423
424 @Test
424 @Test
425 void publishesTaskProducedFileArtifactDirectly() throws Exception {
426 writeFile("settings.gradle", """
427 rootProject.name = 'variant-artifacts-task-produced-file'
428 include 'producer', 'consumer'
429 """);
430 writeBuildFile("""
431 import org.gradle.api.DefaultTask
432 import org.gradle.api.attributes.Attribute
433 import org.gradle.api.file.RegularFileProperty
434 import org.gradle.api.tasks.OutputFile
435 import org.gradle.api.tasks.TaskAction
436
437 def variantAttr = Attribute.of('test.variant', String)
438 def slotAttr = Attribute.of('test.slot', String)
439
440 abstract class WritePackageMetadata extends DefaultTask {
441 @OutputFile
442 abstract RegularFileProperty getOutputFile()
443
444 @TaskAction
445 void write() {
446 def file = outputFile.get().asFile
447 file.parentFile.mkdirs()
448 file.text = '{"name":"demo"}\\n'
449 }
450 }
451
452 project(':producer') {
453 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
454
455 variants.layers.create('main')
456 variants.roles.create('main')
457 variants.variant('browser') {
458 role('main') {
459 layers('main')
460 }
461 }
462
463 def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) {
464 outputFile.set(layout.buildDirectory.file('generated/package.json'))
465 }
466
467 variantArtifacts {
468 variant('browser') {
469 primarySlot('packageMetadata') {
470 producedBy(writePackageMetadata) {
471 outputFile
472 }
473 }
474 }
475
476 whenOutgoingConfiguration { publication ->
477 publication.configuration {
478 attributes.attribute(variantAttr, publication.variant.name)
479 }
480 }
481
482 whenOutgoingSlot { publication ->
483 publication.artifactAttributes {
484 attribute(slotAttr, publication.artifactSlot.slot.name)
485 }
486 }
487 }
488
489 tasks.register('checkNoManagedAssembly') {
490 doLast {
491 def assemblyTasks = tasks.names
492 .findAll { it.startsWith('assembleVariantArtifactSlot') }
493 .sort()
494 println('producerAssemblyTasks=' + assemblyTasks.join(','))
495 assert assemblyTasks.empty
496 }
497 }
498 }
499
500 project(':consumer') {
501 configurations {
502 compileView {
503 canBeResolved = true
504 canBeConsumed = false
505 canBeDeclared = true
506 attributes {
507 attribute(variantAttr, 'browser')
508 attribute(slotAttr, 'packageMetadata')
509 }
510 }
511 }
512
513 dependencies {
514 compileView project(':producer')
515 }
516
517 tasks.register('probe') {
518 dependsOn configurations.compileView
519 dependsOn ':producer:checkNoManagedAssembly'
520
521 doLast {
522 def files = configurations.compileView.files
523 println('resolvedFiles=' + files.collect { it.name }.sort().join(','))
524 println('metadata=' + files.iterator().next().text.trim())
525 }
526 }
527 }
528 """);
529
530 BuildResult result = runner(":consumer:probe").build();
531
532 assertTrue(result.getOutput().contains("producerAssemblyTasks="));
533 assertTrue(result.getOutput().contains("resolvedFiles=package.json"));
534 assertTrue(result.getOutput().contains("metadata={\"name\":\"demo\"}"));
535 assertTrue(result.task(":producer:writePackageMetadata").getOutcome() == TaskOutcome.SUCCESS);
536 assertTrue(result.task(":consumer:probe").getOutcome() == TaskOutcome.SUCCESS);
537 }
538
539 @Test
540 void failsWhenTaskProducedArtifactIsMixedWithContributionAssembly() throws Exception {
541 writeSettings("variant-artifacts-mixed-assembly-mode");
542 writeFile("inputs/marker.txt", "marker\n");
543 writeBuildFile("""
544 import org.gradle.api.DefaultTask
545 import org.gradle.api.file.RegularFileProperty
546 import org.gradle.api.tasks.OutputFile
547 import org.gradle.api.tasks.TaskAction
548
549 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
550
551 abstract class WritePackageMetadata extends DefaultTask {
552 @OutputFile
553 abstract RegularFileProperty getOutputFile()
554
555 @TaskAction
556 void write() {
557 outputFile.get().asFile.text = '{}\\n'
558 }
559 }
560
561 variants.layers.create('main')
562 variants.roles.create('main')
563 variants.variant('browser') {
564 role('main') {
565 layers('main')
566 }
567 }
568
569 def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) {
570 outputFile.set(layout.buildDirectory.file('generated/package.json'))
571 }
572
573 variantArtifacts {
574 variant('browser') {
575 primarySlot('packageMetadata') {
576 producedBy(writePackageMetadata) {
577 outputFile
578 }
579 from(layout.projectDirectory.file('inputs/marker.txt'))
580 }
581 }
582 }
583 """);
584
585 assertBuildFails("cannot mix task-produced artifact and contribution-based assembly", "help");
586 }
587
588 @Test
425 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
589 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
426 writeSettings("variant-artifacts-combined-inputs");
590 writeSettings("variant-artifacts-combined-inputs");
427 writeFile("inputs/base.js", "console.log('base')\n");
591 writeFile("inputs/base.js", "console.log('base')\n");
428 writeFile("inputs/marker.txt", "marker\n");
592 writeFile("inputs/marker.txt", "marker\n");
429 writeBuildFile("""
593 writeBuildFile("""
430 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
594 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
431
595
432 variants.layers.create('main')
596 variants.layers.create('main')
433 variants.roles.create('main')
597 variants.roles.create('main')
434 variants.variant('browser') {
598 variants.variant('browser') {
435 role('main') {
599 role('main') {
436 layers('main')
600 layers('main')
437 }
601 }
438 }
602 }
439
603
440 variantSources.layer('main') {
604 variantSources.layer('main') {
441 sourceSet {
605 sourceSet {
442 declareOutputs('js')
606 declareOutputs('js')
443 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
607 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
444 }
608 }
445 }
609 }
446
610
447 variantArtifacts {
611 variantArtifacts {
448 variant('browser') {
612 variant('browser') {
449 primarySlot('bundle') {
613 primarySlot('bundle') {
450 fromVariant {
614 fromVariant {
451 output('js')
615 output('js')
452 }
616 }
453 from(layout.projectDirectory.file('inputs/marker.txt'))
617 from(layout.projectDirectory.file('inputs/marker.txt'))
454 }
618 }
455 }
619 }
456 }
620 }
457
621
458 tasks.register('probe') {
622 tasks.register('probe') {
459 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
623 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
460
624
461 doLast {
625 doLast {
462 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
626 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
463 assert new File(bundleDir, 'base.js').exists()
627 assert new File(bundleDir, 'base.js').exists()
464 assert new File(bundleDir, 'marker.txt').exists()
628 assert new File(bundleDir, 'marker.txt').exists()
465 }
629 }
466 }
630 }
467 """);
631 """);
468
632
469 BuildResult result = runner("probe").build();
633 BuildResult result = runner("probe").build();
470
634
471 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
635 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
472 }
636 }
473
637
474 @Test
638 @Test
475 void failsOnUnknownVariantReference() throws Exception {
639 void failsOnUnknownVariantReference() throws Exception {
476 writeSettings("variant-artifacts-missing-variant");
640 writeSettings("variant-artifacts-missing-variant");
477 writeBuildFile("""
641 writeBuildFile("""
478 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
642 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
479
643
480 variants.layers.create('main')
644 variants.layers.create('main')
481
645
482 variantArtifacts {
646 variantArtifacts {
483 variant('browser') {
647 variant('browser') {
484 slot('mainJs') {
648 slot('mainJs') {
485 fromVariant {
649 fromVariant {
486 output('js')
650 output('js')
487 }
651 }
488 }
652 }
489 }
653 }
490 }
654 }
491 """);
655 """);
492
656
493 assertBuildFails("isn't declared", "help");
657 assertBuildFails("isn't declared", "help");
494 }
658 }
495
659
496 @Test
660 @Test
497 void failsOnUnknownRoleReference() throws Exception {
661 void failsOnUnknownRoleReference() throws Exception {
498 writeSettings("variant-artifacts-missing-role");
662 writeSettings("variant-artifacts-missing-role");
499 writeBuildFile("""
663 writeBuildFile("""
500 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
664 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
501
665
502 variants.layers.create('main')
666 variants.layers.create('main')
503 variants.roles.create('main')
667 variants.roles.create('main')
504 variants.variant('browser') {
668 variants.variant('browser') {
505 role('main') {
669 role('main') {
506 layers('main')
670 layers('main')
507 }
671 }
508 }
672 }
509
673
510 variantArtifacts {
674 variantArtifacts {
511 variant('browser') {
675 variant('browser') {
512 slot('mainJs') {
676 slot('mainJs') {
513 fromRole('test') {
677 fromRole('test') {
514 output('js')
678 output('js')
515 }
679 }
516 }
680 }
517 }
681 }
518 }
682 }
519 """);
683 """);
520
684
521 assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help");
685 assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help");
522 }
686 }
523
687
524 @Test
688 @Test
525 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
689 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
526 writeSettings("variant-artifacts-missing-primary");
690 writeSettings("variant-artifacts-missing-primary");
527 writeBuildFile("""
691 writeBuildFile("""
528 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
692 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
529
693
530 variants.layers.create('main')
694 variants.layers.create('main')
531 variants.roles.create('main')
695 variants.roles.create('main')
532 variants.variant('browser') {
696 variants.variant('browser') {
533 role('main') {
697 role('main') {
534 layers('main')
698 layers('main')
535 }
699 }
536 }
700 }
537
701
538 variantSources.layer('main') {
702 variantSources.layer('main') {
539 sourceSet {
703 sourceSet {
540 declareOutputs('types', 'js')
704 declareOutputs('types', 'js')
541 }
705 }
542 }
706 }
543
707
544 variantArtifacts {
708 variantArtifacts {
545 variant('browser') {
709 variant('browser') {
546 slot('typesPackage') {
710 slot('typesPackage') {
547 fromVariant {
711 fromVariant {
548 output('types')
712 output('types')
549 }
713 }
550 }
714 }
551 slot('js') {
715 slot('js') {
552 fromVariant {
716 fromVariant {
553 output('js')
717 output('js')
554 }
718 }
555 }
719 }
556 }
720 }
557 }
721 }
558
722
559 tasks.register('probe') {
723 tasks.register('probe') {
560 doLast {
724 doLast {
561 variantArtifacts.whenAvailable { ctx ->
725 variantArtifacts.whenAvailable { ctx ->
562 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
726 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
563 ctx.findOutgoing(browser).get().primarySlot.get()
727 ctx.findOutgoing(browser).get().primarySlot.get()
564 }
728 }
565 }
729 }
566 }
730 }
567 """);
731 """);
568
732
569 assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe");
733 assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe");
570 }
734 }
571
735
572 @Test
736 @Test
573 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
737 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
574 writeSettings("variant-artifacts-layer-outside-topology");
738 writeSettings("variant-artifacts-layer-outside-topology");
575 writeBuildFile("""
739 writeBuildFile("""
576 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
740 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
577
741
578 variants.layers.create('mainBase')
742 variants.layers.create('mainBase')
579 variants.layers.create('extra')
743 variants.layers.create('extra')
580 variants.roles.create('main')
744 variants.roles.create('main')
581 variants.variant('browser') {
745 variants.variant('browser') {
582 role('main') {
746 role('main') {
583 layers('mainBase')
747 layers('mainBase')
584 }
748 }
585 }
749 }
586
750
587 variantArtifacts {
751 variantArtifacts {
588 variant('browser') {
752 variant('browser') {
589 slot('extraJs') {
753 slot('extraJs') {
590 fromLayer('extra') {
754 fromLayer('extra') {
591 output('js')
755 output('js')
592 }
756 }
593 }
757 }
594 }
758 }
595 }
759 }
596 """);
760 """);
597
761
598 assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help");
762 assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help");
599 }
763 }
600
764
601 @Test
765 @Test
602 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
766 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
603 writeFile("settings.gradle", """
767 writeFile("settings.gradle", """
604 rootProject.name = 'variant-artifacts-resolution'
768 rootProject.name = 'variant-artifacts-resolution'
605 include 'producer', 'consumer'
769 include 'producer', 'consumer'
606 """);
770 """);
607 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
771 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
608 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
772 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
609 writeBuildFile("""
773 writeBuildFile("""
610 import org.gradle.api.attributes.Attribute
774 import org.gradle.api.attributes.Attribute
611
775
612 def variantAttr = Attribute.of('test.variant', String)
776 def variantAttr = Attribute.of('test.variant', String)
613 def slotAttr = Attribute.of('test.slot', String)
777 def slotAttr = Attribute.of('test.slot', String)
614
778
615 subprojects {
779 subprojects {
616 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
780 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
617 }
781 }
618
782
619 project(':producer') {
783 project(':producer') {
620 variants.layers.create('main')
784 variants.layers.create('main')
621 variants.roles.create('main')
785 variants.roles.create('main')
622 variants.variant('browser') {
786 variants.variant('browser') {
623 role('main') {
787 role('main') {
624 layers('main')
788 layers('main')
625 }
789 }
626 }
790 }
627
791
628 variantSources.layer('main') {
792 variantSources.layer('main') {
629 sourceSet {
793 sourceSet {
630 declareOutputs('types', 'js')
794 declareOutputs('types', 'js')
631 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
795 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
632 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
796 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
633 }
797 }
634 }
798 }
635
799
636 variantArtifacts {
800 variantArtifacts {
637 variant('browser') {
801 variant('browser') {
638 primarySlot('typesPackage') {
802 primarySlot('typesPackage') {
639 fromVariant {
803 fromVariant {
640 output('types')
804 output('types')
641 }
805 }
642 }
806 }
643 slot('js') {
807 slot('js') {
644 fromVariant {
808 fromVariant {
645 output('js')
809 output('js')
646 }
810 }
647 }
811 }
648 }
812 }
649
813
650 whenOutgoingConfiguration { publication ->
814 whenOutgoingConfiguration { publication ->
651 publication.configuration {
815 publication.configuration {
652 attributes.attribute(variantAttr, publication.variant.name)
816 attributes.attribute(variantAttr, publication.variant.name)
653 }
817 }
654 }
818 }
655
819
656 whenOutgoingSlot { publication ->
820 whenOutgoingSlot { publication ->
657 publication.artifactAttributes {
821 publication.artifactAttributes {
658 attribute(slotAttr, publication.artifactSlot.slot.name)
822 attribute(slotAttr, publication.artifactSlot.slot.name)
659 }
823 }
660 }
824 }
661 }
825 }
662
826
663 }
827 }
664
828
665 project(':consumer') {
829 project(':consumer') {
666 configurations {
830 configurations {
667 compileView {
831 compileView {
668 canBeResolved = true
832 canBeResolved = true
669 canBeConsumed = false
833 canBeConsumed = false
670 canBeDeclared = true
834 canBeDeclared = true
671 attributes {
835 attributes {
672 attribute(variantAttr, 'browser')
836 attribute(variantAttr, 'browser')
673 attribute(slotAttr, 'typesPackage')
837 attribute(slotAttr, 'typesPackage')
674 }
838 }
675 }
839 }
676 }
840 }
677
841
678 dependencies {
842 dependencies {
679 compileView project(':producer')
843 compileView project(':producer')
680 }
844 }
681
845
682 tasks.register('probe') {
846 tasks.register('probe') {
683 doLast {
847 doLast {
684 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
848 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
685 def jsFiles = configurations.compileView.incoming.artifactView {
849 def jsFiles = configurations.compileView.incoming.artifactView {
686 attributes {
850 attributes {
687 attribute(slotAttr, 'js')
851 attribute(slotAttr, 'js')
688 }
852 }
689 }.files.files.collect { it.name }.sort().join(',')
853 }.files.files.collect { it.name }.sort().join(',')
690
854
691 println('compileFiles=' + compileFiles)
855 println('compileFiles=' + compileFiles)
692 println('jsFiles=' + jsFiles)
856 println('jsFiles=' + jsFiles)
693 }
857 }
694 }
858 }
695 }
859 }
696 """);
860 """);
697
861
698 BuildResult result = runner(":consumer:probe").build();
862 BuildResult result = runner(":consumer:probe").build();
699
863
700 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
864 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
701 assertTrue(result.getOutput().contains("jsFiles=js"));
865 assertTrue(result.getOutput().contains("jsFiles=js"));
702 }
866 }
703 }
867 }
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now