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