##// END OF EJS Templates
Refine variant artifacts publication lifecycle- Remove assembly task access from outgoing slot publication spec- Keep whenOutgoingSlot focused on publication attributes only- Decouple materialization policy handler from artifact assemblies- Drop eager afterEvaluate outgoing configuration realization- Add reference coverage for lazy Gradle outgoing variants- Exercise primary and secondary artifact resolution without forced realization- Keep slot body customization in ArtifactAssemblySpec
cin -
r52:3939ecb6e9a4 default
parent child
Show More
@@ -0,0 +1,14
1 package org.implab.gradle.internal;
2
3 import org.gradle.api.Named;
4 import org.gradle.api.NamedDomainObjectContainer;
5 import org.gradle.api.model.ObjectFactory;
6
7 public final class IdentityContainerFactory {
8 private IdentityContainerFactory() {
9 }
10
11 public static <T extends Named> NamedDomainObjectContainer<T> create(ObjectFactory objectFactory, Class<T> type) {
12 return objectFactory.domainObjectContainer(type, name -> objectFactory.named(type, name));
13 }
14 }
@@ -0,0 +1,1
1 implementation-class=org.implab.gradle.variants.SourcesPlugin
@@ -0,0 +1,86
1 package org.implab.gradle.variants;
2
3 import static org.junit.jupiter.api.Assertions.assertEquals;
4 import static org.junit.jupiter.api.Assertions.assertNotNull;
5 import static org.junit.jupiter.api.Assertions.assertTrue;
6
7 import java.io.IOException;
8 import java.util.Properties;
9
10 import org.gradle.testkit.runner.BuildResult;
11 import org.junit.jupiter.api.Test;
12
13 class PluginMarkerFunctionalTest extends AbstractFunctionalTest {
14
15 @Test
16 void pluginMarkersPointToCurrentImplementations() throws Exception {
17 assertMarker("org.implab.gradle-variants.properties", VariantsPlugin.class);
18 assertMarker("org.implab.gradle-sources.properties", SourcesPlugin.class);
19 assertMarker("org.implab.gradle-variants-sources.properties", VariantSourcesPlugin.class);
20 assertMarker("org.implab.gradle-variants-artifacts.properties", VariantArtifactsPlugin.class);
21 }
22
23 @Test
24 void pluginIdsApplyCurrentExtensions() throws Exception {
25 writeSettings("variants-plugin-ids");
26 writeBuildFile("""
27 apply plugin: 'org.implab.gradle-variants'
28 apply plugin: 'org.implab.gradle-sources'
29 apply plugin: 'org.implab.gradle-variants-sources'
30 apply plugin: 'org.implab.gradle-variants-artifacts'
31
32 tasks.register('probe') {
33 doLast {
34 println("variantsExtension=" + (project.extensions.findByName('variants') != null))
35 println("sourcesExtension=" + (project.extensions.findByName('sources') != null))
36 println("variantSourcesExtension=" + (project.extensions.findByName('variantSources') != null))
37 println("variantArtifactsExtension=" + (project.extensions.findByName('variantArtifacts') != null))
38 }
39 }
40 """);
41
42 BuildResult result = runner("probe").build();
43
44 assertTrue(result.getOutput().contains("variantsExtension=true"));
45 assertTrue(result.getOutput().contains("sourcesExtension=true"));
46 assertTrue(result.getOutput().contains("variantSourcesExtension=true"));
47 assertTrue(result.getOutput().contains("variantArtifactsExtension=true"));
48 }
49
50 @Test
51 void sourcesPluginSupportsStandaloneSourceSetWorkflow() throws Exception {
52 writeSettings("sources-plugin-standalone");
53 writeFile("inputs/main.js", "console.log('main')\n");
54 writeBuildFile("""
55 apply plugin: 'org.implab.gradle-sources'
56
57 sources.create('main') {
58 declareOutputs('js')
59 registerOutput('js', layout.projectDirectory.file('inputs/main.js'))
60 }
61
62 tasks.register('probe') {
63 doLast {
64 def main = sources.named('main').get()
65 println("sourceSet=" + main.name)
66 println("jsFiles=" + main.output('js').files.collect { it.name }.sort().join(','))
67 }
68 }
69 """);
70
71 BuildResult result = runner("probe").build();
72
73 assertTrue(result.getOutput().contains("sourceSet=main"));
74 assertTrue(result.getOutput().contains("jsFiles=main.js"));
75 }
76
77 private static void assertMarker(String markerName, Class<?> implementationClass) throws IOException {
78 var resourceName = "META-INF/gradle-plugins/" + markerName;
79 var resource = PluginMarkerFunctionalTest.class.getClassLoader().getResourceAsStream(resourceName);
80 assertNotNull(resource, "Missing plugin marker " + resourceName);
81
82 var properties = new Properties();
83 properties.load(resource);
84 assertEquals(implementationClass.getName(), properties.getProperty("implementation-class"));
85 }
86 }
This diff has been collapsed as it changes many lines, (563 lines changed) Show them Hide them
@@ -0,0 +1,563
1 package org.implab.gradle.variants;
2
3 import static org.junit.jupiter.api.Assertions.assertTrue;
4
5 import org.gradle.testkit.runner.BuildResult;
6 import org.gradle.testkit.runner.TaskOutcome;
7 import org.junit.jupiter.api.Test;
8
9 class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest {
10
11 @Test
12 void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception {
13 writeFile("settings.gradle", """
14 rootProject.name = 'gradle-reference-outgoing-resolution'
15 include 'producer', 'consumer'
16 """);
17 writeFile("producer/inputs/typesPackage", "types\n");
18 writeFile("producer/inputs/js", "js\n");
19 writeBuildFile("""
20 import org.gradle.api.attributes.Attribute
21
22 def variantAttr = Attribute.of('test.variant', String)
23 def slotAttr = Attribute.of('test.slot', String)
24
25 project(':producer') {
26 def browserElements = configurations.consumable('browserElements')
27
28 println('reference: registered browserElements provider')
29
30 browserElements.configure { configuration ->
31 println('reference: configuring browserElements')
32
33 configuration.attributes.attribute(variantAttr, 'browser')
34 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
35 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
36
37 configuration.outgoing.variants.create('js') { secondary ->
38 println('reference: creating js outgoing variant')
39
40 secondary.attributes.attribute(slotAttr, 'js')
41 secondary.artifact(layout.projectDirectory.file('inputs/js'))
42 }
43 }
44 }
45
46 project(':consumer') {
47 configurations {
48 compileView {
49 canBeResolved = true
50 canBeConsumed = false
51 canBeDeclared = true
52 attributes {
53 attribute(variantAttr, 'browser')
54 attribute(slotAttr, 'typesPackage')
55 }
56 }
57 }
58
59 dependencies {
60 compileView project(':producer')
61 }
62
63 tasks.register('probe') {
64 doLast {
65 println('reference: resolving primary files')
66
67 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
68
69 println('reference: resolving secondary files')
70
71 def jsFiles = configurations.compileView.incoming.artifactView {
72 attributes {
73 attribute(slotAttr, 'js')
74 }
75 }.files.files.collect { it.name }.sort().join(',')
76
77 println('compileFiles=' + compileFiles)
78 println('jsFiles=' + jsFiles)
79 }
80 }
81 }
82 """);
83
84 BuildResult result = runner(":consumer:probe").build();
85 var output = result.getOutput();
86 var registered = output.indexOf("reference: registered browserElements provider");
87 var resolvingPrimary = output.indexOf("reference: resolving primary files");
88 var configuring = output.indexOf("reference: configuring browserElements");
89 var creatingSecondary = output.indexOf("reference: creating js outgoing variant");
90 var resolvingSecondary = output.indexOf("reference: resolving secondary files");
91
92 assertTrue(registered >= 0);
93 assertTrue(resolvingPrimary >= 0);
94 assertTrue(configuring >= 0);
95 assertTrue(creatingSecondary >= 0);
96 assertTrue(resolvingSecondary >= 0);
97 assertTrue(registered < resolvingPrimary);
98 assertTrue(resolvingPrimary < configuring);
99 assertTrue(configuring < creatingSecondary);
100 assertTrue(creatingSecondary < resolvingSecondary);
101 assertTrue(output.contains("compileFiles=typesPackage"));
102 assertTrue(output.contains("jsFiles=js"));
103 }
104
105 @Test
106 void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception {
107 writeSettings("variant-artifacts-slots");
108 writeFile("inputs/base.js", "console.log('base')\n");
109 writeFile("inputs/amd.js", "console.log('amd')\n");
110 writeFile("inputs/mainJs.txt", "mainJs marker\n");
111 writeFile("inputs/amdJs.txt", "amdJs marker\n");
112 writeBuildFile("""
113 import org.gradle.api.attributes.Attribute
114
115 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
116
117 def variantAttr = Attribute.of('test.variant', String)
118 def slotAttr = Attribute.of('test.slot', String)
119
120 variants.layers.create('mainBase')
121 variants.layers.create('mainAmd')
122 variants.roles.create('main')
123 variants.roles.create('test')
124 variants.variant('browser') {
125 role('main') {
126 layers('mainBase', 'mainAmd')
127 }
128 }
129
130 variantSources {
131 layer('mainBase') {
132 declareOutputs('js')
133 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
134 }
135 layer('mainAmd') {
136 declareOutputs('js')
137 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
138 }
139 }
140
141 variantArtifacts {
142 variant('browser') {
143 primarySlot('mainJs') {
144 fromRole('main') {
145 output('js')
146 }
147 from(layout.projectDirectory.file('inputs/mainJs.txt'))
148 }
149 slot('amdJs') {
150 fromLayer('mainAmd') {
151 output('js')
152 }
153 from(layout.projectDirectory.file('inputs/amdJs.txt'))
154 }
155 }
156
157 whenOutgoingConfiguration { publication ->
158 publication.configuration {
159 attributes.attribute(variantAttr, publication.variant.name)
160 }
161 }
162
163 whenOutgoingSlot { publication ->
164 def slotName = publication.artifactSlot.slot.name
165 publication.artifactAttributes {
166 attribute(slotAttr, slotName)
167 }
168 }
169 }
170
171 tasks.register('probe') {
172 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs'
173 dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs'
174
175 doLast {
176 def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile
177 def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile
178
179 assert new File(mainDir, 'base.js').exists()
180 assert new File(mainDir, 'amd.js').exists()
181 assert new File(mainDir, 'mainJs.txt').exists()
182
183 assert !new File(amdDir, 'base.js').exists()
184 assert new File(amdDir, 'amd.js').exists()
185 assert new File(amdDir, 'amdJs.txt').exists()
186
187 def elements = configurations.getByName('browserElements')
188 def amdVariant = elements.outgoing.variants.getByName('amdJs')
189
190 println('variantAttr=' + elements.attributes.getAttribute(variantAttr))
191 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
192 println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr))
193 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
194 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
195 }
196 }
197 """);
198
199 BuildResult result = runner("probe").build();
200
201 assertTrue(result.getOutput().contains("variantAttr=browser"));
202 assertTrue(result.getOutput().contains("primarySlotAttr=mainJs"));
203 assertTrue(result.getOutput().contains("amdSlotAttr=amdJs"));
204 assertTrue(result.getOutput().contains("configurations=browserElements"));
205 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
206 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
207 }
208
209 @Test
210 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
211 writeSettings("variant-artifacts-single-slot");
212 writeBuildFile("""
213 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
214
215 variants.layers.create('main')
216 variants.roles.create('main')
217 variants.variant('browser') {
218 role('main') {
219 layers('main')
220 }
221 }
222
223 variantSources.layer('main') {
224 declareOutputs('types')
225 }
226
227 variantArtifacts {
228 variant('browser') {
229 slot('typesPackage') {
230 fromVariant {
231 output('types')
232 }
233 }
234 }
235 }
236
237 tasks.register('probe') {
238 doLast {
239 variantArtifacts.whenAvailable { ctx ->
240 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
241 println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name)
242 }
243 }
244 }
245 """);
246
247 BuildResult result = runner("probe").build();
248
249 assertTrue(result.getOutput().contains("primary=typesPackage"));
250 }
251
252 @Test
253 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
254 writeSettings("variant-artifacts-direct-input");
255 writeFile("inputs/bundle.js", "console.log('bundle')\n");
256 writeBuildFile("""
257 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
258
259 variants.layers.create('main')
260 variants.roles.create('main')
261 variants.variant('browser') {
262 role('main') {
263 layers('main')
264 }
265 }
266
267 variantArtifacts {
268 variant('browser') {
269 primarySlot('bundle') {
270 from(layout.projectDirectory.file('inputs/bundle.js'))
271 }
272 }
273 }
274
275 tasks.register('probe') {
276 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
277
278 doLast {
279 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
280 assert new File(bundleDir, 'bundle.js').exists()
281 }
282 }
283 """);
284
285 BuildResult result = runner("probe").build();
286
287 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
288 }
289
290 @Test
291 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
292 writeSettings("variant-artifacts-combined-inputs");
293 writeFile("inputs/base.js", "console.log('base')\n");
294 writeFile("inputs/marker.txt", "marker\n");
295 writeBuildFile("""
296 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
297
298 variants.layers.create('main')
299 variants.roles.create('main')
300 variants.variant('browser') {
301 role('main') {
302 layers('main')
303 }
304 }
305
306 variantSources.layer('main') {
307 declareOutputs('js')
308 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
309 }
310
311 variantArtifacts {
312 variant('browser') {
313 primarySlot('bundle') {
314 fromVariant {
315 output('js')
316 }
317 from(layout.projectDirectory.file('inputs/marker.txt'))
318 }
319 }
320 }
321
322 tasks.register('probe') {
323 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
324
325 doLast {
326 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
327 assert new File(bundleDir, 'base.js').exists()
328 assert new File(bundleDir, 'marker.txt').exists()
329 }
330 }
331 """);
332
333 BuildResult result = runner("probe").build();
334
335 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
336 }
337
338 @Test
339 void failsOnUnknownVariantReference() throws Exception {
340 writeSettings("variant-artifacts-missing-variant");
341 writeBuildFile("""
342 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
343
344 variants.layers.create('main')
345
346 variantArtifacts {
347 variant('browser') {
348 slot('mainJs') {
349 fromVariant {
350 output('js')
351 }
352 }
353 }
354 }
355 """);
356
357 assertBuildFails("isn't declared", "help");
358 }
359
360 @Test
361 void failsOnUnknownRoleReference() throws Exception {
362 writeSettings("variant-artifacts-missing-role");
363 writeBuildFile("""
364 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
365
366 variants.layers.create('main')
367 variants.roles.create('main')
368 variants.variant('browser') {
369 role('main') {
370 layers('main')
371 }
372 }
373
374 variantArtifacts {
375 variant('browser') {
376 slot('mainJs') {
377 fromRole('test') {
378 output('js')
379 }
380 }
381 }
382 }
383 """);
384
385 assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help");
386 }
387
388 @Test
389 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
390 writeSettings("variant-artifacts-missing-primary");
391 writeBuildFile("""
392 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
393
394 variants.layers.create('main')
395 variants.roles.create('main')
396 variants.variant('browser') {
397 role('main') {
398 layers('main')
399 }
400 }
401
402 variantSources.layer('main') {
403 declareOutputs('types', 'js')
404 }
405
406 variantArtifacts {
407 variant('browser') {
408 slot('typesPackage') {
409 fromVariant {
410 output('types')
411 }
412 }
413 slot('js') {
414 fromVariant {
415 output('js')
416 }
417 }
418 }
419 }
420
421 tasks.register('probe') {
422 doLast {
423 variantArtifacts.whenAvailable { ctx ->
424 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
425 ctx.findOutgoing(browser).get().primarySlot.get()
426 }
427 }
428 }
429 """);
430
431 assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe");
432 }
433
434 @Test
435 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
436 writeSettings("variant-artifacts-layer-outside-topology");
437 writeBuildFile("""
438 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
439
440 variants.layers.create('mainBase')
441 variants.layers.create('extra')
442 variants.roles.create('main')
443 variants.variant('browser') {
444 role('main') {
445 layers('mainBase')
446 }
447 }
448
449 variantArtifacts {
450 variant('browser') {
451 slot('extraJs') {
452 fromLayer('extra') {
453 output('js')
454 }
455 }
456 }
457 }
458 """);
459
460 assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help");
461 }
462
463 @Test
464 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
465 writeFile("settings.gradle", """
466 rootProject.name = 'variant-artifacts-resolution'
467 include 'producer', 'consumer'
468 """);
469 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
470 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
471 writeBuildFile("""
472 import org.gradle.api.attributes.Attribute
473
474 def variantAttr = Attribute.of('test.variant', String)
475 def slotAttr = Attribute.of('test.slot', String)
476
477 subprojects {
478 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
479 }
480
481 project(':producer') {
482 variants.layers.create('main')
483 variants.roles.create('main')
484 variants.variant('browser') {
485 role('main') {
486 layers('main')
487 }
488 }
489
490 variantSources.layer('main') {
491 declareOutputs('types', 'js')
492 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
493 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
494 }
495
496 variantArtifacts {
497 variant('browser') {
498 primarySlot('typesPackage') {
499 fromVariant {
500 output('types')
501 }
502 }
503 slot('js') {
504 fromVariant {
505 output('js')
506 }
507 }
508 }
509
510 whenOutgoingConfiguration { publication ->
511 publication.configuration {
512 attributes.attribute(variantAttr, publication.variant.name)
513 }
514 }
515
516 whenOutgoingSlot { publication ->
517 publication.artifactAttributes {
518 attribute(slotAttr, publication.artifactSlot.slot.name)
519 }
520 }
521 }
522
523 }
524
525 project(':consumer') {
526 configurations {
527 compileView {
528 canBeResolved = true
529 canBeConsumed = false
530 canBeDeclared = true
531 attributes {
532 attribute(variantAttr, 'browser')
533 attribute(slotAttr, 'typesPackage')
534 }
535 }
536 }
537
538 dependencies {
539 compileView project(':producer')
540 }
541
542 tasks.register('probe') {
543 doLast {
544 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
545 def jsFiles = configurations.compileView.incoming.artifactView {
546 attributes {
547 attribute(slotAttr, 'js')
548 }
549 }.files.files.collect { it.name }.sort().join(',')
550
551 println('compileFiles=' + compileFiles)
552 println('jsFiles=' + jsFiles)
553 }
554 }
555 }
556 """);
557
558 BuildResult result = runner(":consumer:probe").build();
559
560 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
561 assertTrue(result.getOutput().contains("jsFiles=js"));
562 }
563 }
@@ -1,40 +1,41
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.variants;
2 2
3 3 import org.gradle.api.GradleException;
4 4 import org.gradle.api.NamedDomainObjectContainer;
5 5 import org.gradle.api.Plugin;
6 6 import org.gradle.api.Project;
7 7 import org.gradle.api.logging.Logger;
8 8 import org.gradle.api.logging.Logging;
9 import org.implab.gradle.variants.sources.GenericSourceSet;
9 10
10 11 /**
11 12 * This plugin creates a {@code sources} extension which is
12 13 * a container for {@link GenericSourceSet}.
13 14 *
14 15 */
15 16 public abstract class SourcesPlugin implements Plugin<Project> {
16 17 private static final Logger logger = Logging.getLogger(SourcesPlugin.class);
17 18
18 19 private static final String SOURCES_EXTENSION_NAME = "sources";
19 20
20 21 @Override
21 22 public void apply(Project target) {
22 23 logger.debug("Registering '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath());
23 24 var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class);
24 25 target.getExtensions().add(SOURCES_EXTENSION_NAME, sources);
25 26 }
26 27
27 28 public static NamedDomainObjectContainer<GenericSourceSet> getSourcesExtension(Project target) {
28 29 var extensions = target.getExtensions();
29 30
30 31 @SuppressWarnings("unchecked")
31 32 var extension = (NamedDomainObjectContainer<GenericSourceSet>) extensions.findByName(SOURCES_EXTENSION_NAME);
32 33
33 34 if (extension == null) {
34 35 logger.error("Sources extension '{}' isn't found on project '{}'", SOURCES_EXTENSION_NAME, target.getPath());
35 36 throw new GradleException("Sources extension isn't found");
36 37 }
37 38 logger.debug("Resolved '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath());
38 39 return extension;
39 40 }
40 41 }
@@ -1,112 +1,112
1 1 package org.implab.gradle.variants;
2 2
3 3 import org.gradle.api.Action;
4 4 import org.gradle.api.Plugin;
5 5 import org.gradle.api.Project;
6 6 import org.implab.gradle.common.core.lang.Deferred;
7 7 import org.implab.gradle.variants.artifacts.OutgoingConfigurationSpec;
8 8 import org.implab.gradle.variants.artifacts.OutgoingVariantsContext;
9 9 import org.implab.gradle.variants.artifacts.VariantArtifactsExtension;
10 10 import org.implab.gradle.variants.artifacts.VariantArtifactsSpec;
11 11 import org.implab.gradle.variants.artifacts.internal.ArtifactAssemblyBinder;
12 12 import org.implab.gradle.variants.artifacts.internal.ArtifactAssemblyHandler;
13 13 import org.implab.gradle.variants.artifacts.internal.ArtifactAssemblyRegistry;
14 14 import org.implab.gradle.variants.artifacts.internal.DefaultOutgoingVariantsContext;
15 15 import org.implab.gradle.variants.artifacts.internal.MaterializationPolicyHandler;
16 16 import org.implab.gradle.variants.artifacts.internal.OutgoingRegistry;
17 17 import org.implab.gradle.variants.artifacts.internal.SingleSlotConvention;
18 18 import org.implab.gradle.variants.core.Variant;
19 19 import org.implab.gradle.variants.core.VariantsExtension;
20 20 import org.implab.gradle.variants.sources.VariantSourcesExtension;
21 21 import org.implab.gradle.variants.artifacts.OutgoingConfigurationSlotSpec;
22 22
23 23 public abstract class VariantArtifactsPlugin implements Plugin<Project> {
24 24
25 25 @Override
26 26 public void apply(Project target) {
27 27 var extensions = target.getExtensions();
28 28 var objects = target.getObjects();
29 29 var providers = target.getProviders();
30 30 var plugins = target.getPlugins();
31 31 var configurations = target.getConfigurations();
32 32 var tasks = target.getTasks();
33 33 var layout = target.getLayout();
34 34
35 35 // Apply the main VariantsPlugin to ensure the core variant model is available.
36 36 plugins.apply(VariantsPlugin.class);
37 37 plugins.apply(VariantSourcesPlugin.class);
38 38 // Access the VariantsExtension to configure variant sources.
39 39 var variantsExtension = extensions.getByType(VariantsExtension.class);
40 40 var sourcesExtension = extensions.getByType(VariantSourcesExtension.class);
41 41
42 42 var deferred = new Deferred<OutgoingVariantsContext>();
43 43
44 44 variantsExtension.whenFinalized(variants -> {
45 45
46 46 var outgoing = new OutgoingRegistry(configurations, objects, variants.getVariants());
47 47 var assemblies = new ArtifactAssemblyRegistry();
48 48
49 49 // wire artifact assemblies to configuration variants
50 50 var assembliesBridge = new ArtifactAssemblyBinder(assemblies);
51 51 var primarySlotConvention = new SingleSlotConvention(providers);
52 var materializationHandler = new MaterializationPolicyHandler(assemblies);
52 var materializationHandler = new MaterializationPolicyHandler();
53 53
54 54 // bind slot assemblies to outgoing variants
55 55 outgoing.configureEach(assembliesBridge::execute);
56 56 // apply primary slot convention when outgoing variant has a single slot
57 57 outgoing.configureEach(primarySlotConvention::execute);
58 58 // apply materialization policy hooks to outgoing variants
59 59 outgoing.configureEach(materializationHandler::execute);
60 60
61 61 sourcesExtension.whenFinalized(sources -> {
62 62 var assemblyHandler = new ArtifactAssemblyHandler(
63 63 objects,
64 64 tasks,
65 65 assemblies,
66 66 sources.getCompileUnits(),
67 67 sources.getRoleProjections(),
68 68 sources.getSourceSets());
69 69 assemblyHandler.getAssembliesDirectory().set(layout.getBuildDirectory().dir("variant-assemblies"));
70 70
71 71 deferred.resolve(new DefaultOutgoingVariantsContext(
72 72 assemblies,
73 73 outgoing,
74 74 assemblyHandler,
75 75 materializationHandler));
76 76 });
77 77
78 78 });
79 79
80 80 var variantArtifacts = new VariantArtifactsExtension() {
81 81
82 82 @Override
83 83 public void variant(String variantName, Action<? super VariantArtifactsSpec> action) {
84 84 deferred.whenResolved(context -> {
85 85 var variant = objects.named(Variant.class, variantName);
86 86 context.configureVariant(variant, action);
87 87 });
88 88 }
89 89
90 90 @Override
91 91 public void whenAvailable(Action<? super OutgoingVariantsContext> action) {
92 92 deferred.whenResolved(handler -> action.execute(handler));
93 93 }
94 94
95 95 @Override
96 96 public void whenOutgoingConfiguration(Action<? super OutgoingConfigurationSpec> action) {
97 97 deferred.whenResolved(registry -> registry.whenOutgoingConfiguration(action));
98 98
99 99 }
100 100
101 101 @Override
102 102 public void whenOutgoingSlot(Action<? super OutgoingConfigurationSlotSpec> action) {
103 103 deferred.whenResolved(registry -> registry.whenOutgoingSlot(action));
104 104 }
105 105
106 106 };
107 107
108 108 extensions.add(VariantArtifactsExtension.class, "variantArtifacts", variantArtifacts);
109 109
110 110 }
111 111
112 112 }
@@ -1,156 +1,156
1 1 package org.implab.gradle.variants;
2 2
3 3 import java.util.Objects;
4 4 import java.util.function.Predicate;
5 5
6 6 import org.eclipse.jdt.annotation.NonNullByDefault;
7 7 import org.gradle.api.Action;
8 8 import org.gradle.api.InvalidUserDataException;
9 9 import org.gradle.api.Named;
10 10 import org.gradle.api.Plugin;
11 11 import org.gradle.api.Project;
12 12 import org.implab.gradle.common.core.lang.Deferred;
13 13 import org.implab.gradle.common.core.lang.Strings;
14 import org.implab.gradle.common.sources.GenericSourceSet;
15 14 import org.implab.gradle.variants.core.Layer;
16 15 import org.implab.gradle.variants.core.Variant;
17 16 import org.implab.gradle.variants.core.VariantsExtension;
18 17 import org.implab.gradle.variants.core.VariantsView;
19 18 import org.implab.gradle.variants.sources.CompileUnit;
20 19 import org.implab.gradle.variants.sources.CompileUnitsView;
20 import org.implab.gradle.variants.sources.GenericSourceSet;
21 21 import org.implab.gradle.variants.sources.RoleProjectionsView;
22 22 import org.implab.gradle.variants.sources.VariantSourcesContext;
23 23 import org.implab.gradle.variants.sources.VariantSourcesExtension;
24 24 import org.implab.gradle.variants.sources.internal.CompileUnitNamer;
25 25 import org.implab.gradle.variants.sources.internal.DefaultCompileUnitNamingPolicy;
26 26 import org.implab.gradle.variants.sources.internal.DefaultLateConfigurationPolicySpec;
27 27 import org.implab.gradle.variants.sources.internal.DefaultVariantSourcesContext;
28 28 import org.implab.gradle.variants.sources.internal.SourceSetConfigurationRegistry;
29 29 import org.implab.gradle.variants.sources.internal.SourceSetRegistry;
30 30
31 31 @NonNullByDefault
32 32 public abstract class VariantSourcesPlugin implements Plugin<Project> {
33 33 public static final String VARIANT_SOURCES_EXTENSION = "variantSources";
34 34
35 35 @Override
36 36 public void apply(Project target) {
37 37 var extensions = target.getExtensions();
38 38
39 39 // Apply the main VariantsPlugin to ensure the core variant model is available.
40 40 target.getPlugins().apply(VariantsPlugin.class);
41 41 // Access the VariantsExtension to configure variant sources.
42 42 var variantsExtension = extensions.getByType(VariantsExtension.class);
43 43 var objectFactory = target.getObjects();
44 44
45 45 var deferred = new Deferred<VariantSourcesContext>();
46 46
47 47 var lateConfigurationPolicy = new DefaultLateConfigurationPolicySpec();
48 48 var namingPolicy = new DefaultCompileUnitNamingPolicy();
49 49
50 50 variantsExtension.whenFinalized(variants -> {
51 51 // create variant views
52 52 var compileUnits = CompileUnitsView.of(variants);
53 53 var roleProjections = RoleProjectionsView.of(variants);
54 54
55 55 // create registries
56 56 var sourceSetRegistry = new SourceSetRegistry(objectFactory);
57 57 var sourceSetConfiguration = new SourceSetConfigurationRegistry(lateConfigurationPolicy::mode);
58 58
59 59 // build compile unit namer
60 60 var compileUnitNamer = CompileUnitNamer.builder()
61 61 .addUnits(compileUnits.getUnits())
62 62 .nameCollisionPolicy(namingPolicy.policy())
63 63 .build();
64 64
65 65 // create the context
66 66 var context = new DefaultVariantSourcesContext(
67 67 variants,
68 68 compileUnits,
69 69 roleProjections,
70 70 compileUnitNamer,
71 71 sourceSetRegistry,
72 72 sourceSetConfiguration
73 73 );
74 74 deferred.resolve(context);
75 75 });
76 76
77 77 var variantSourcesExtension = new VariantSourcesExtension() {
78 78 @Override
79 79 public void whenFinalized(Action<? super VariantSourcesContext> action) {
80 80 deferred.whenResolved(action::execute);
81 81 }
82 82
83 83 @Override
84 84 public void lateConfigurationPolicy(Action<? super LateConfigurationPolicySpec> action) {
85 85 action.execute(lateConfigurationPolicy);
86 86 }
87 87
88 88 @Override
89 89 public void namingPolicy(Action<? super NamingPolicySpec> action) {
90 90 action.execute(namingPolicy);
91 91 }
92 92
93 93 @Override
94 94 public void variant(String variantName, Action<? super GenericSourceSet> action) {
95 95 Strings.argumentNotNullOrBlank(variantName, "variantName");
96 96 Objects.requireNonNull(action, "action can't be null");
97 97
98 98 lateConfigurationPolicy.finalizePolicy();
99 99
100 100 whenFinalized(ctx -> ctx.configureVariant(resolveVariant(ctx.getVariants(), variantName), action));
101 101 }
102 102
103 103 @Override
104 104 public void layer(String layerName, Action<? super GenericSourceSet> action) {
105 105 // protect external DSL
106 106 Strings.argumentNotNullOrBlank(layerName, "layerName");
107 107 Objects.requireNonNull(action, "action can't be null");
108 108
109 109 lateConfigurationPolicy.finalizePolicy();
110 110
111 111 whenFinalized(ctx -> ctx.configureLayer(resolveLayer(ctx.getVariants(), layerName), action));
112 112 }
113 113
114 114 @Override
115 115 public void unit(String variantName, String layerName, Action<? super GenericSourceSet> action) {
116 116 Strings.argumentNotNullOrBlank(layerName, "layerName");
117 117 Strings.argumentNotNullOrBlank(variantName, "variantName");
118 118 Objects.requireNonNull(action, "action can't be null");
119 119
120 120 lateConfigurationPolicy.finalizePolicy();
121 121
122 122 whenFinalized(ctx -> ctx.configureUnit(resolveCompileUnit(ctx, variantName, layerName), action));
123 123 }
124 124 };
125 125
126 126 extensions.add(VariantSourcesExtension.class, VARIANT_SOURCES_EXTENSION, variantSourcesExtension);
127 127
128 128 }
129 129
130 130 private static Layer resolveLayer(VariantsView variants, String name) {
131 131 return variants.getLayers().stream()
132 132 .filter(named(name))
133 133 .findAny()
134 134 .orElseThrow(() -> new IllegalArgumentException("Layer '" + name + "' isn't declared"));
135 135 }
136 136
137 137 private static Variant resolveVariant(VariantsView variants, String name) {
138 138 return variants.getVariants().stream()
139 139 .filter(named(name))
140 140 .findAny()
141 141 .orElseThrow(() -> new IllegalArgumentException("Variant '" + name + "' is't declared"));
142 142 }
143 143
144 144 private static CompileUnit resolveCompileUnit(VariantSourcesContext ctx, String variantName, String layerName) {
145 145 return ctx.getCompileUnits().findUnit(
146 146 resolveVariant(ctx.getVariants(), variantName),
147 147 resolveLayer(ctx.getVariants(), layerName))
148 148 .orElseThrow(() -> new InvalidUserDataException(
149 149 "The CompileUnit isn't declared for variant '" + variantName + "', layer '" + layerName + "'"));
150 150 }
151 151
152 152 private static Predicate<Named> named(String name) {
153 153 return named -> named.getName().equals(name);
154 154 }
155 155
156 156 }
@@ -1,101 +1,102
1 1 package org.implab.gradle.variants;
2 2
3 3 import org.gradle.api.Action;
4 4 import org.gradle.api.NamedDomainObjectContainer;
5 5 import org.gradle.api.Plugin;
6 6 import org.gradle.api.Project;
7 7 import org.gradle.api.model.ObjectFactory;
8 8 import org.implab.gradle.common.core.lang.Deferred;
9 import org.implab.gradle.internal.IdentityContainerFactory;
9 10 import org.implab.gradle.variants.core.Layer;
10 11 import org.implab.gradle.variants.core.Role;
11 12 import org.implab.gradle.variants.core.Variant;
12 13 import org.implab.gradle.variants.core.VariantDefinition;
13 14 import org.implab.gradle.variants.core.VariantsExtension;
14 15 import org.implab.gradle.variants.core.VariantsView;
15 16
16 17 /**
17 18 * <ul>
18 19 * <li> {@link Variant} defines compilation semantics
19 20 * <li> {@link Layer} defines compilation partition
20 21 * <li> {@link Role} defines result grouping / publication intent
21 22 * </ul>
22 23 *
23 24 */
24 25 public abstract class VariantsPlugin implements Plugin<Project> {
25 26 @Override
26 27 public void apply(Project target) {
27 28
28 29 var objectFactory = target.getObjects();
29 30
30 31 var variantsExtension = new DefaultVariantsExtension(objectFactory);
31 32 target.getExtensions().add(VariantsExtension.class, "variants", variantsExtension);
32 33 target.afterEvaluate(t -> variantsExtension.finalizeExtension());
33 34
34 35 }
35 36
36 37 static class DefaultVariantsExtension implements VariantsExtension {
37 38
38 39 private final NamedDomainObjectContainer<Layer> layers;
39 40 private final NamedDomainObjectContainer<Role> roles;
40 41 private final NamedDomainObjectContainer<VariantDefinition> variantDefinitions;
41 42 private final Deferred<VariantsView> finalizedResult = new Deferred<>();
42 43 private final ObjectFactory objectFactory;
43 44 private boolean finalized = false;
44 45
45 46 public DefaultVariantsExtension(ObjectFactory objectFactory) {
46 47 this.objectFactory = objectFactory;
47 this.layers = objectFactory.domainObjectContainer(Layer.class);
48 this.roles = objectFactory.domainObjectContainer(Role.class);
48 this.layers = IdentityContainerFactory.create(objectFactory, Layer.class);
49 this.roles = IdentityContainerFactory.create(objectFactory, Role.class);
49 50 this.variantDefinitions = objectFactory.domainObjectContainer(VariantDefinition.class);
50 51 }
51 52
52 53 @Override
53 54 public NamedDomainObjectContainer<Layer> getLayers() {
54 55 return layers;
55 56 }
56 57
57 58 @Override
58 59 public NamedDomainObjectContainer<Role> getRoles() {
59 60 return roles;
60 61 }
61 62
62 63 @Override
63 64 public NamedDomainObjectContainer<VariantDefinition> getVariantDefinitions() {
64 65 return variantDefinitions;
65 66 }
66 67
67 68 @Override
68 69 public void whenFinalized(Action<? super VariantsView> action) {
69 70 finalizedResult.whenResolved(action::execute);
70 71 }
71 72
72 73 void finalizeExtension() {
73 74 if (finalized)
74 75 return;
75 76
76 77 finalized = true;
77 78
78 79 // freeze defined variants
79 80 variantDefinitions.forEach(VariantDefinition::finalizeVariant);
80 81
81 82 // build a snapshot
82 83 var viewBuilder = VariantsView.builder();
83 84
84 85 // calculate and add variants
85 86 variantDefinitions.stream()
86 87 .map(def -> objectFactory.named(Variant.class, def.getName()))
87 88 .forEach(viewBuilder::addVariant);
88 89 // add layers
89 90 layers.forEach(viewBuilder::addLayer);
90 91 // add roles
91 92 roles.forEach(viewBuilder::addRole);
92 93 // add definitions
93 94 variantDefinitions.forEach(viewBuilder::addDefinition);
94 95 // assemble the view
95 96 var view = viewBuilder.build();
96 97 // set the result and call hooks
97 98 finalizedResult.resolve(view);
98 99 }
99 100 }
100 101
101 102 }
@@ -1,61 +1,40
1 1 package org.implab.gradle.variants.artifacts;
2 2
3 3 import org.gradle.api.Action;
4 import org.gradle.api.Task;
5 4 import org.gradle.api.attributes.AttributeContainer;
6 5 import groovy.lang.Closure;
7 6 import org.implab.gradle.common.core.lang.Closures;
8 7
9 8 /**
10 9 * Materialized outgoing publication state of a single slot.
11 10 *
12 11 * <p>This type is a DSL facade to represent already created publication-facing state. Slot-specific
13 12 * publication tweaks should be applied here rather than through {@link OutgoingConfigurationSpec}, which
14 13 * is limited to the root outgoing configuration of the variant.
15 14 */
16 15 public interface OutgoingConfigurationSlotSpec {
17 16 /**
18 17 * Returns the published slot identity.
19 18 *
20 19 * @return slot identity
21 20 */
22 21 ArtifactSlot getArtifactSlot();
23 22
24 23 /**
25 * Returns the assembly backing the published slot.
26 *
27 * @return slot assembly
28 */
29 ArtifactAssembly getAssembly();
30
31 /**
32 24 * Returns whether this slot is the primary outgoing artifact set of the variant.
33 25 *
34 26 * @return {@code true} for the primary slot
35 27 */
36 28 boolean isPrimary();
37 29
38 30 /**
39 * Configures the task producing the slot artifact.
40 *
41 * @param action task configuration action
42 */
43 default void assemblyTask(Action<? super Task> action) {
44 getAssembly().getAssemblyTask().configure(action);
45 }
46
47 default void assemblyTask(Closure<?> closure) {
48 assemblyTask(Closures.action(closure));
49 }
50
51 /**
52 31 * Configures attributes of this slot publication.
53 32 *
54 33 * @param action artifact attribute configuration action
55 34 */
56 35 void artifactAttributes(Action<? super AttributeContainer> action);
57 36
58 37 default void artifactAttributes(Closure<?> closure) {
59 38 artifactAttributes(Closures.action(closure));
60 39 }
61 40 }
@@ -1,118 +1,107
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.gradle.api.artifacts.Configuration;
6 6 import org.gradle.api.attributes.AttributeContainer;
7 7 import org.implab.gradle.internal.ReplayableQueue;
8 import org.implab.gradle.variants.artifacts.ArtifactAssemblies;
9 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
10 8 import org.implab.gradle.variants.artifacts.ArtifactSlot;
11 9 import org.implab.gradle.variants.artifacts.OutgoingConfigurationSlotSpec;
12 10 import org.implab.gradle.variants.artifacts.OutgoingConfigurationSpec;
13 11 import org.implab.gradle.variants.artifacts.OutgoingVariant;
14 12 import org.implab.gradle.variants.core.Variant;
15 13
16 14 /**
17 15 * Handles outgoing artifact materialization policy.
18 16 *
19 17 * <p>Materialization is the phase where the plugin interprets variant artifact
20 18 * declarations as Gradle outgoing publication state: a consumable configuration,
21 * its primary artifact set, secondary artifact variants, attributes, and backing
22 * assembly tasks.
19 * its primary artifact set, secondary artifact variants, and attributes.
23 20 *
24 21 * <p>The handler provides extension points for customizing the materialized
25 22 * Gradle-facing objects. These hooks intentionally expose Gradle API objects.
26 23 * This allows advanced customization, but also means that callers can bypass
27 24 * the plugin model. Such customizations are considered caller responsibility.
28 25 *
29 26 * <p>The internal binding mechanics are not part of the public contract. The
30 27 * contract is the materialized outgoing state exposed through the specification
31 28 * objects.
32 29 */
33 30 @NonNullByDefault
34 31 public class MaterializationPolicyHandler implements Action<OutgoingVariant> {
35 32
36 private final ArtifactAssemblies resolver;
37
38 33 private final ReplayableQueue<OutgoingConfigurationSpec> variantMaterialization = new ReplayableQueue<>();
39 34 private final ReplayableQueue<OutgoingConfigurationSlotSpec> slotMaterialization = new ReplayableQueue<>();
40 35
41 public MaterializationPolicyHandler(ArtifactAssemblies resolver) {
42 this.resolver = resolver;
36 public MaterializationPolicyHandler() {
43 37 }
44 38
45 39 @Override
46 40 public void execute(OutgoingVariant outgoingVariant) {
47 41 var slots = outgoingVariant.getSlots();
48 42 var primarySlotProvider = outgoingVariant.getPrimarySlot();
49 43 var variant = outgoingVariant.getVariant();
50 44
51 45 // связываем конфигурацию
52 46 outgoingVariant.configureOutgoing(configuration -> {
53 47 var primarySlot = primarySlotProvider.get();
54 48 var outgoing = configuration.getOutgoing();
55 49
56 50 variantMaterialized(variant, configuration);
57 51
58 52 slotMaterialized(new ArtifactSlot(variant, primarySlot), true, outgoing.getAttributes());
59 53
60 outgoing.getVariants().configureEach(variantConfiguration -> {
61 var slotName = variantConfiguration.getName();
62 var slot = slots.findByName(slotName);
63 if (slot != null) {
54 slots.forEach(slot -> {
55 if (slot.equals(primarySlot))
56 return;
57
58 var variantConfiguration = outgoing.getVariants().getByName(slot.getName());
64 59 slotMaterialized(new ArtifactSlot(variant, slot), false, variantConfiguration.getAttributes());
65 }
66 60 });
67 61 });
68 62 };
69 63
70 64 public void whenVariantMaterialized(Action<? super OutgoingConfigurationSpec> action) {
71 65 variantMaterialization.forEach(action::execute);
72 66 }
73 67
74 68 public void whenSlotMaterialized(Action<? super OutgoingConfigurationSlotSpec> action) {
75 69 slotMaterialization.forEach(action::execute);
76 70 }
77 71
78 72 private void variantMaterialized(Variant variant, Configuration configuration) {
79 73 variantMaterialization.add(new OutgoingConfigurationSpec() {
80 74
81 75 @Override
82 76 public Variant getVariant() {
83 77 return variant;
84 78 }
85 79
86 80 @Override
87 81 public Configuration getConfiguration() {
88 82 return configuration;
89 83 }
90 84
91 85 });
92 86 }
93 87
94 88 private void slotMaterialized(ArtifactSlot slot, boolean primary, AttributeContainer attributes) {
95 89 slotMaterialization.add(new OutgoingConfigurationSlotSpec() {
96 90 @Override
97 91 public boolean isPrimary() {
98 92 return primary;
99 93 }
100 94
101 95 @Override
102 96 public ArtifactSlot getArtifactSlot() {
103 97 return slot;
104 98 }
105 99
106 100 @Override
107 public ArtifactAssembly getAssembly() {
108 return resolver.require(slot);
109 }
110
111 @Override
112 101 public void artifactAttributes(Action<? super AttributeContainer> action) {
113 102 action.execute(attributes);
114 103 }
115 104 });
116 105 }
117 106
118 107 }
@@ -1,121 +1,122
1 1 package org.implab.gradle.variants.artifacts.internal;
2 2
3 3 import java.util.LinkedHashMap;
4 4 import java.util.Map;
5 5 import java.util.Optional;
6 6 import java.util.Set;
7 7 import java.util.function.Consumer;
8 8
9 9 import org.eclipse.jdt.annotation.NonNullByDefault;
10 10 import org.gradle.api.InvalidUserDataException;
11 11 import org.gradle.api.NamedDomainObjectContainer;
12 12 import org.gradle.api.NamedDomainObjectProvider;
13 13 import org.gradle.api.artifacts.Configuration;
14 14 import org.gradle.api.artifacts.ConfigurationContainer;
15 15 import org.gradle.api.model.ObjectFactory;
16 16 import org.gradle.api.provider.Property;
17 import org.implab.gradle.internal.IdentityContainerFactory;
17 18 import org.implab.gradle.internal.ReplayableQueue;
18 19 import org.implab.gradle.variants.artifacts.OutgoingVariant;
19 20 import org.implab.gradle.variants.artifacts.Slot;
20 21 import org.implab.gradle.variants.core.Variant;
21 22
22 23 /**
23 24 * Реестр исходящих вариантов. Связывает исходящие конфигурации с вариантами
24 25 * сборки. Связь устанавливается 1:1.
25 26 */
26 27 @NonNullByDefault
27 28 public class OutgoingRegistry {
28 29 private final Map<Variant, OutgoingVariant> outgoingByVariant = new LinkedHashMap<>();
29 30 private final ReplayableQueue<OutgoingVariant> outgoingVariants = new ReplayableQueue<>();
30 31 private final ConfigurationContainer configurations;
31 32 private final ObjectFactory objects;
32 33 private final Set<Variant> declaredVariants;
33 34
34 35 public OutgoingRegistry(
35 36 ConfigurationContainer configurations,
36 37 ObjectFactory objects,
37 38 Set<Variant> declaredVariants) {
38 39 this.configurations = configurations;
39 40 this.objects = objects;
40 41 this.declaredVariants = declaredVariants;
41 42 }
42 43
43 44 public Optional<OutgoingVariant> find(Variant variant) {
44 45 return Optional.ofNullable(outgoingByVariant.get(variant));
45 46 }
46 47
47 48 public OutgoingVariant maybeCreate(Variant variant) {
48 49 return find(variant).orElseGet(() -> create(variant));
49 50 }
50 51
51 52 public OutgoingVariant create(Variant variant) {
52 53 if (!declaredVariants.contains(variant))
53 54 throw new InvalidUserDataException("Variant " + variant + " isn't declared");
54 55 if (outgoingByVariant.containsKey(variant))
55 56 throw new InvalidUserDataException("Outgoing variant " + variant + " already exists");
56 57
57 58 var configuration = configurations.consumable(outgoingConfigurationName(variant));
58 59 var outgoing = new Outgoing(variant, configuration);
59 60
60 61 outgoingByVariant.put(variant, outgoing);
61 62
62 63 outgoingVariants.add(outgoing);
63 64
64 65 return outgoing;
65 66 }
66 67
67 68 /**
68 69 * Replayable hook which is applied when an outgoing variant is defined
69 70 *
70 71 * @param action
71 72 */
72 73 public void configureEach(Consumer<? super OutgoingVariant> action) {
73 74 outgoingVariants.forEach(action);
74 75 }
75 76
76 77 private String outgoingConfigurationName(Variant variant) {
77 78 return variant.getName() + "Elements";
78 79 }
79 80
80 81 private class Outgoing implements OutgoingVariant {
81 82
82 83 private final Variant variant;
83 84
84 85 private final NamedDomainObjectProvider<? extends Configuration> configurationProvider;
85 86
86 87 private final NamedDomainObjectContainer<Slot> slots;
87 88
88 89 private final Property<Slot> primarySlot;
89 90
90 91 public Outgoing(
91 92 Variant variant,
92 93 NamedDomainObjectProvider<? extends Configuration> configurationProvider) {
93 94 this.variant = variant;
94 95 this.configurationProvider = configurationProvider;
95 this.slots = objects.domainObjectContainer(Slot.class);
96 this.slots = IdentityContainerFactory.create(objects, Slot.class);
96 97 this.primarySlot = objects.property(Slot.class);
97 98 primarySlot.finalizeValueOnRead();
98 99 }
99 100
100 101 @Override
101 102 public Property<Slot> getPrimarySlot() {
102 103 return primarySlot;
103 104 }
104 105
105 106 @Override
106 107 public Variant getVariant() {
107 108 return variant;
108 109 }
109 110
110 111 @Override
111 112 public NamedDomainObjectProvider<? extends Configuration> getConfiguration() {
112 113 return configurationProvider;
113 114 }
114 115
115 116 @Override
116 117 public NamedDomainObjectContainer<Slot> getSlots() {
117 118 return slots;
118 119 }
119 120 }
120 121
121 122 }
@@ -1,78 +1,78
1 1 package org.implab.gradle.variants.sources;
2 2
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5 import java.util.Objects;
6 6 import java.util.Optional;
7 7 import java.util.Set;
8 8 import java.util.stream.Collectors;
9 9
10 10 import org.eclipse.jdt.annotation.NonNullByDefault;
11 11 import org.implab.gradle.variants.core.Layer;
12 12 import org.implab.gradle.variants.core.Role;
13 13 import org.implab.gradle.variants.core.Variant;
14 14 import org.implab.gradle.variants.core.VariantsView;
15 15 import org.implab.gradle.variants.core.VariantsView.VariantRoleLayer;
16 16
17 17 @NonNullByDefault
18 18 public final class CompileUnitsView {
19 19
20 20 private final VariantsView variants;
21 21 private final Map<Variant, Set<CompileUnit>> unitsByVariant = new HashMap<>();
22 22
23 23 private CompileUnitsView(VariantsView variants) {
24 24 this.variants = variants;
25 25 }
26 26
27 27 public Set<CompileUnit> getUnits() {
28 28 return variants.getEntries().stream()
29 29 .map(CompileUnit::of)
30 30 .collect(Collectors.toUnmodifiableSet());
31 31 }
32 32
33 33 public Set<CompileUnit> getUnitsForVariant(Variant variant) {
34 34 Objects.requireNonNull(variant, "Variant can't be null");
35 35
36 36 return unitsByVariant.computeIfAbsent(variant, key -> variants
37 37 .getEntriesForVariant(variant).stream()
38 38 .map(CompileUnit::of)
39 39 .collect(Collectors.toUnmodifiableSet()));
40 40 }
41 41
42 42 public Optional<CompileUnit> findUnit(Variant variant, Layer layer) {
43 43 Objects.requireNonNull(variant, "Variant can't be null");
44 44 Objects.requireNonNull(layer, "Layer can't be null");
45 45
46 46 return getUnitsForVariant(variant).stream()
47 47 .filter(u -> u.layer().equals(layer))
48 48 .findAny();
49 49 }
50 50
51 51 public boolean contains(Variant variant, Layer layer) {
52 52 return findUnit(variant, layer).isPresent();
53 53 }
54 54
55 55 /**
56 56 * In which logical roles this compile unit participates.
57 57 */
58 58
59 59 public Set<Role> getRoles(CompileUnit unit) {
60 60 Objects.requireNonNull(unit, "Compile unit can't be null");
61 61 return variants.getEntriesForVariant(unit.variant()).stream()
62 62 .filter(entry -> entry.layer().equals(unit.layer()))
63 63 .map(VariantRoleLayer::role)
64 64 .collect(Collectors.toUnmodifiableSet());
65 65 }
66 66
67 67 public CompileUnit requireUnit(Variant variant, Layer layer) {
68 68 return findUnit(variant, layer)
69 69 .orElseThrow(() -> new IllegalArgumentException(
70 70 "Compile unit for variant '" + variant.getName()
71 71 + "' and layer '" + layer.getName() + "' not found"));
72 72 }
73 73
74 74 public static CompileUnitsView of(VariantsView variantsView) {
75 75 Objects.requireNonNull(variantsView, "variantsView can't be null");
76 76 return new CompileUnitsView(variantsView);
77 77 }
78 } No newline at end of file
78 }
@@ -1,206 +1,206
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.variants.sources;
2 2
3 3 import java.io.File;
4 4 import java.nio.file.Paths;
5 5 import java.util.HashSet;
6 6 import java.util.LinkedHashMap;
7 7 import java.util.List;
8 8 import java.util.Map;
9 9 import java.util.Objects;
10 10 import java.util.Set;
11 11 import java.util.concurrent.Callable;
12 12 import java.util.function.Function;
13 13 import java.util.stream.Collectors;
14 14 import java.util.stream.Stream;
15 15
16 16 import javax.inject.Inject;
17 17
18 18 import org.gradle.api.InvalidUserDataException;
19 19 import org.gradle.api.Named;
20 20 import org.gradle.api.NamedDomainObjectContainer;
21 21 import org.gradle.api.Task;
22 22 import org.gradle.api.file.ConfigurableFileCollection;
23 23 import org.gradle.api.file.DirectoryProperty;
24 24 import org.gradle.api.file.FileCollection;
25 25 import org.gradle.api.file.ProjectLayout;
26 26 import org.gradle.api.file.SourceDirectorySet;
27 27 import org.gradle.api.model.ObjectFactory;
28 28 import org.gradle.api.tasks.TaskProvider;
29 29 import org.gradle.util.Configurable;
30 30 import org.implab.gradle.common.core.lang.Closures;
31 31
32 32 import groovy.lang.Closure;
33 33
34 34 /**
35 35 * A configurable source set abstraction with named outputs.
36 36 *
37 37 * <p>
38 38 * Each instance aggregates multiple {@link SourceDirectorySet source sets}
39 39 * under a shared name and exposes typed outputs that must be declared up front.
40 40 * Default locations are {@code src/<name>} for sources and
41 41 * {@code build/<name>} for outputs, both of which can be customized via the
42 42 * exposed {@link DirectoryProperty} setters.
43 43 * </p>
44 44 *
45 45 * <p>
46 46 * Outputs are grouped by names to make task wiring explicit. An output must be
47 47 * declared with {@link #declareOutputs(String, String...)} before files can be
48 48 * registered against it. Attempting to register or retrieve an undeclared
49 49 * output results in
50 50 * {@link InvalidUserDataException}.
51 51 * </p>
52 52 */
53 53 public abstract class GenericSourceSet
54 54 implements Named, Configurable<GenericSourceSet> {
55 55 private final String name;
56 56
57 57 private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets;
58 58
59 59 private final Map<String, ConfigurableFileCollection> outputs;
60 60
61 61 private final FileCollection allOutputs;
62 62
63 63 private final FileCollection allSourceDirectories;
64 64
65 65 private final ObjectFactory objects;
66 66
67 67 private final Set<String> declaredOutputs = new HashSet<>();
68 68
69 69 @Inject
70 70 public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) {
71 71 this.name = name;
72 72 this.objects = objects;
73 73
74 74 sourceDirectorySets = objects.domainObjectContainer(
75 75 SourceDirectorySet.class,
76 76 this::createSourceDirectorySet);
77 77
78 78 outputs = new LinkedHashMap<>();
79 79
80 80 allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider());
81 81
82 82 allOutputs = objects.fileCollection().from(outputsProvider());
83 83
84 84 getSourceSetDir().convention(layout
85 85 .getProjectDirectory()
86 86 .dir(Paths.get("src", name).toString()));
87 87
88 88 getOutputsDir().convention(layout
89 89 .getBuildDirectory()
90 90 .dir(name));
91 91 }
92 92
93 93 @Override
94 94 public String getName() {
95 95 return name;
96 96 }
97 97
98 98 /**
99 99 * Base directory for this source set. Defaults to {@code src/<name>} under
100 100 * the project directory.
101 101 */
102 102 public abstract DirectoryProperty getSourceSetDir();
103 103
104 104 /**
105 105 * Base directory for outputs of this source set. Defaults to
106 106 * {@code build/<name>}.
107 107 */
108 108 public abstract DirectoryProperty getOutputsDir();
109 109
110 110 /**
111 111 * The container of {@link SourceDirectorySet} instances that belong to this
112 112 * logical source set.
113 113 */
114 114 public NamedDomainObjectContainer<SourceDirectorySet> getSets() {
115 115 return sourceDirectorySets;
116 116 }
117 117
118 118 /**
119 119 * All registered outputs grouped across output names.
120 120 */
121 121 public FileCollection getAllOutputs() {
122 122 return allOutputs;
123 123 }
124 124
125 125 /**
126 126 * All source directories from every contained {@link SourceDirectorySet}.
127 127 */
128 128 public FileCollection getAllSourceDirectories() {
129 129 return allSourceDirectories;
130 130 }
131 131
132 132 /**
133 133 * Returns the file collection for the specified output name, creating it
134 134 * if necessary.
135 135 *
136 136 * @throws InvalidUserDataException if the output was not declared
137 137 */
138 138 public FileCollection output(String name) {
139 139 return configurableOutput(name);
140 140 }
141 141
142 142 private ConfigurableFileCollection configurableOutput(String name) {
143 143 requireDeclaredOutput(name);
144 144 return outputs.computeIfAbsent(name, key -> objects.fileCollection());
145 145 }
146 146
147 147 /**
148 148 * Declares allowed output names. Outputs must be declared before registering
149 149 * files under them.
150 150 */
151 151 public void declareOutputs(String name, String... extra) {
152 152 Stream.concat(Stream.of(name), Stream.of(extra))
153 153 .map(Objects::requireNonNull)
154 154 .forEach(declaredOutputs::add);
155 155 }
156 156
157 157 /**
158 158 * Registers files produced elsewhere under the given output.
159 159 */
160 160 public void registerOutput(String name, Object... files) {
161 161 configurableOutput(name).from(files);
162 162 }
163 163
164 164 /**
165 165 * Registers output files produced by a task, using a mapper to extract the
166 166 * output from the task. The task will be added as a build dependency of this
167 167 * output.
168 168 */
169 169 public <T extends Task> void registerOutput(String name, TaskProvider<T> task,
170 170 Function<? super T, ?> mapper) {
171 171 configurableOutput(name).from(task.map(mapper::apply))
172 172 .builtBy(task);
173 173 }
174 174
175 175 /**
176 176 * Applies a Groovy closure to this source set, enabling DSL-style
177 177 * configuration.
178 178 */
179 179 @Override
180 180 public GenericSourceSet configure(Closure configure) {
181 181 Closures.apply(configure, this);
182 182 return this;
183 183 }
184 184
185 185 private SourceDirectorySet createSourceDirectorySet(String name) {
186 186 return objects.sourceDirectorySet(name, name);
187 187 }
188 188
189 189 private void requireDeclaredOutput(String outputName) {
190 190 if (!declaredOutputs.contains(outputName)) {
191 191 throw new InvalidUserDataException(
192 192 "Output '" + outputName + "' is not declared for source set '" + name + "'");
193 193 }
194 194 }
195 195
196 196 private Callable<List<? extends FileCollection>> outputsProvider() {
197 197 return () -> outputs.values().stream().toList();
198 198 }
199 199
200 200 private Callable<Set<File>> sourceDirectoriesProvider() {
201 201 return () -> sourceDirectorySets.stream()
202 202 .flatMap(x -> x.getSrcDirs().stream())
203 203 .collect(Collectors.toSet());
204 204 }
205 205
206 206 }
@@ -1,83 +1,83
1 1 package org.implab.gradle.variants.sources;
2 2
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5 import java.util.Objects;
6 6 import java.util.Optional;
7 7 import java.util.Set;
8 8 import java.util.stream.Collectors;
9 9
10 10 import org.implab.gradle.variants.core.Layer;
11 11 import org.implab.gradle.variants.core.Role;
12 12 import org.implab.gradle.variants.core.Variant;
13 13 import org.implab.gradle.variants.core.VariantsView;
14 14
15 15 public final class RoleProjectionsView {
16 16 private final VariantsView variants;
17 17
18 18 private final Map<Variant, Set<RoleProjection>> projectionsByVariant = new HashMap<>();
19 19
20 20 private RoleProjectionsView(VariantsView variants) {
21 21 this.variants = variants;
22 22 }
23 23
24 24 public Set<RoleProjection> getProjections() {
25 25 return variants.getEntries().stream()
26 26 .map(RoleProjection::of)
27 27 .collect(Collectors.toUnmodifiableSet());
28 28 }
29 29
30 30 public Set<RoleProjection> getProjectionsForVariant(Variant variant) {
31 31 Objects.requireNonNull(variant, "Variant can't be null");
32 32 return projectionsByVariant.computeIfAbsent(variant, key -> variants
33 33 .getEntriesForVariant(variant).stream()
34 34 .map(RoleProjection::of)
35 35 .collect(Collectors.toUnmodifiableSet()));
36 36 }
37 37
38 38 public Set<RoleProjection> getProjectionsForRole(Role role) {
39 39 Objects.requireNonNull(role, "Role can't be null");
40 40 return variants.getEntriesForRole(role).stream()
41 41 .map(RoleProjection::of)
42 42 .collect(Collectors.toUnmodifiableSet());
43 43 }
44 44
45 45 public Optional<RoleProjection> findProjection(Variant variant, Role role) {
46 46 Objects.requireNonNull(variant, "Variant can't be null");
47 47 Objects.requireNonNull(role, "Role can't be null");
48 48 return variants.getEntriesForVariant(variant).stream()
49 49 .filter(entry -> entry.role().equals(role))
50 50 .map(RoleProjection::of)
51 51 .findAny();
52 52 }
53 53
54 54 public boolean contains(Variant variant, Role role) {
55 55 return findProjection(variant, role).isPresent();
56 56 }
57 57
58 58 public Set<CompileUnit> getUnits(RoleProjection projection) {
59 59 Objects.requireNonNull(projection, "Role projection can't be null");
60 60 return variants.getEntriesForVariant(projection.variant()).stream()
61 61 .filter(entry -> entry.role().equals(projection.role()))
62 62 .map(CompileUnit::of)
63 63 .collect(Collectors.toUnmodifiableSet());
64 64
65 65 }
66 66
67 67 public RoleProjection requireProjection(Variant variant, Role role) {
68 68 return findProjection(variant, role)
69 69 .orElseThrow(() -> new IllegalArgumentException(
70 70 "Role projection for variant '" + variant.getName()
71 71 + "' and role '" + role.getName() + "' not found"));
72 72 }
73 73
74 74 public Set<Layer> getLayers(RoleProjection projection) {
75 75 return getUnits(projection).stream()
76 76 .map(CompileUnit::layer)
77 77 .collect(java.util.stream.Collectors.toUnmodifiableSet());
78 78 }
79 79
80 80 public static RoleProjectionsView of(VariantsView variantsView) {
81 81 return new RoleProjectionsView(variantsView);
82 82 }
83 } No newline at end of file
83 }
@@ -1,21 +1,20
1 1 package org.implab.gradle.variants.sources;
2 2
3 3 import org.gradle.api.NamedDomainObjectProvider;
4 import org.implab.gradle.common.sources.GenericSourceSet;
5 4
6 5 /**
7 6 * Materializes symbolic source set names into actual {@link GenericSourceSet}
8 7 * instances.
9 8 *
10 9 * <p>Symbolic names are assigned from the finalized compile-unit model using
11 10 * the selected
12 11 * {@link VariantSourcesExtension#namingPolicy(org.gradle.api.Action)}.
13 12 */
14 13 public interface SourceSetMaterializer {
15 14 /**
16 15 * Returns a lazy provider for the source set corresponding to the compile unit.
17 16 *
18 17 * <p>The provider is stable and cached per compile unit.
19 18 */
20 19 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(CompileUnit unit);
21 20 }
@@ -1,73 +1,72
1 1 package org.implab.gradle.variants.sources;
2 2
3 3 import org.gradle.api.Action;
4 import org.implab.gradle.common.sources.GenericSourceSet;
5 4 import org.implab.gradle.variants.core.Layer;
6 5 import org.implab.gradle.variants.core.Variant;
7 6 import org.implab.gradle.variants.core.VariantsView;
8 7
9 8 /**
10 9 * Registry of symbolic source set names produced by sources projection.
11 10 *
12 11 * <p>Identity in this registry is the {@link GenericSourceSet} name assigned
13 12 * by the finalized
14 13 * {@link VariantSourcesExtension#namingPolicy(org.gradle.api.Action)}.
15 14 */
16 15 public interface VariantSourcesContext {
17 16
18 17 /**
19 18 * Finalized core model.
20 19 */
21 20 VariantsView getVariants();
22 21
23 22 /**
24 23 * Derived compile-side view.
25 24 */
26 25 CompileUnitsView getCompileUnits();
27 26
28 27 /**
29 28 * Derived role-side view.
30 29 */
31 30 RoleProjectionsView getRoleProjections();
32 31
33 32 /**
34 33 * Lazy source set provider service.
35 34 */
36 35 SourceSetMaterializer getSourceSets();
37 36
38 37 /**
39 38 * Configures all GenericSourceSets produced from the given layer.
40 39 *
41 40 * The action is applied:
42 41 * - to already materialized source sets of this layer
43 42 * - to all future source sets of this layer
44 43 *
45 44 * <p>For future source sets, selector precedence and registration order are
46 45 * preserved by the materializer.
47 46 *
48 47 * <p>For already materialized source sets, behavior is governed by
49 48 * {@link VariantSourcesExtension#lateConfigurationPolicy(org.gradle.api.Action)}.
50 49 * In warn/allow modes the action is applied as a late imperative step and does
51 50 * not retroactively restore selector precedence.
52 51 */
53 52 void configureLayer(Layer layer, Action<? super GenericSourceSet> action);
54 53
55 54 /**
56 55 * Configures all GenericSourceSets produced from the given variant.
57 56 *
58 57 * <p>Late application semantics for already materialized source sets are
59 58 * governed by
60 59 * {@link VariantSourcesExtension#lateConfigurationPolicy(org.gradle.api.Action)}.
61 60 */
62 61 void configureVariant(Variant variant, Action<? super GenericSourceSet> action);
63 62
64 63 /**
65 64 * Configures the GenericSourceSet produced from the given compile unit.
66 65 *
67 66 * <p>Late application semantics for already materialized source sets are
68 67 * governed by
69 68 * {@link VariantSourcesExtension#lateConfigurationPolicy(org.gradle.api.Action)}.
70 69 */
71 70 void configureUnit(CompileUnit unit, Action<? super GenericSourceSet> action);
72 71
73 72 }
@@ -1,168 +1,167
1 1 package org.implab.gradle.variants.sources;
2 2
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 4 import org.gradle.api.Action;
5 5 import org.implab.gradle.common.core.lang.Closures;
6 import org.implab.gradle.common.sources.GenericSourceSet;
7 6 import groovy.lang.Closure;
8 7
9 8 @NonNullByDefault
10 9 public interface VariantSourcesExtension {
11 10
12 11 /**
13 12 * Selects how selector rules behave when they target an already materialized
14 13 * {@link GenericSourceSet}.
15 14 *
16 15 * <p>This policy is single-valued:
17 16 * <ul>
18 17 * <li>it must be selected before the first selector rule is registered via
19 18 * {@link #variant(String, Action)}, {@link #layer(String, Action)} or
20 19 * {@link #unit(String, String, Action)};</li>
21 20 * <li>once selected, it cannot be changed later;</li>
22 21 * <li>the policy controls both diagnostics and late-application semantics.</li>
23 22 * </ul>
24 23 *
25 24 * <p>If not selected explicitly, the default is
26 25 * {@link LateConfigurationPolicySpec#failOnLateConfiguration()}.
27 26 */
28 27 void lateConfigurationPolicy(Action<? super LateConfigurationPolicySpec> action);
29 28
30 29 default void lateConfigurationPolicy(Closure<?> closure) {
31 30 lateConfigurationPolicy(Closures.action(closure));
32 31 }
33 32
34 33 /**
35 34 * Selects how compile-unit name collisions are handled when the finalized
36 35 * source context is created.
37 36 *
38 37 * <p>This policy is single-valued:
39 38 * <ul>
40 39 * <li>it must be selected before the finalized
41 40 * {@link VariantSourcesContext} becomes observable through
42 41 * {@link #whenFinalized(Action)};</li>
43 42 * <li>once the context is being created, the policy is fixed and cannot be
44 43 * changed later;</li>
45 44 * <li>the policy governs validation of compile-unit names produced by the
46 45 * source-set materializer.</li>
47 46 * </ul>
48 47 *
49 48 * <p>If not selected explicitly, the default is
50 49 * {@link NamingPolicySpec#failOnNameCollision()}.
51 50 */
52 51 void namingPolicy(Action<? super NamingPolicySpec> action);
53 52
54 53 default void namingPolicy(Closure<?> closure) {
55 54 namingPolicy(Closures.action(closure));
56 55 }
57 56
58 57 /**
59 58 * Registers a selector rule for all compile units of the given layer.
60 59 *
61 60 * <p>Registering the first selector rule fixes the selected
62 61 * {@link #lateConfigurationPolicy(Action)} for the remaining extension
63 62 * lifecycle.
64 63 */
65 64 void layer(String layerName, Action<? super GenericSourceSet> action);
66 65
67 66 default void layer(String layerName, Closure<?> closure) {
68 67 layer(layerName, Closures.action(closure));
69 68 }
70 69
71 70 /**
72 71 * Registers a selector rule for all compile units of the given variant.
73 72 *
74 73 * <p>Registering the first selector rule fixes the selected
75 74 * {@link #lateConfigurationPolicy(Action)} for the remaining extension
76 75 * lifecycle.
77 76 */
78 77 void variant(String variantName, Action<? super GenericSourceSet> action);
79 78
80 79 default void variant(String variantName, Closure<?> closure) {
81 80 variant(variantName, Closures.action(closure));
82 81 }
83 82
84 83 /**
85 84 * Registers a selector rule for one exact compile unit.
86 85 *
87 86 * <p>Registering the first selector rule fixes the selected
88 87 * {@link #lateConfigurationPolicy(Action)} for the remaining extension
89 88 * lifecycle.
90 89 */
91 90 void unit(String variantName, String layerName, Action<? super GenericSourceSet> action);
92 91
93 92 default void unit(String variantName, String layerName, Closure<?> closure) {
94 93 unit(variantName, layerName, Closures.action(closure));
95 94 }
96 95
97 96 /**
98 97 * Invoked when finalized variants-derived source context becomes available.
99 98 *
100 99 * Replayable:
101 100 * <ul>
102 101 * <li>if called before variants finalization, action is queued
103 102 * <li>if called after variants finalization, action is invoked immediately
104 103 * </ul>
105 104 *
106 105 * <p>By the time this callback becomes observable, compile-unit naming
107 106 * policy has already been fixed and symbolic source-set names for finalized
108 107 * compile units are determined.
109 108 */
110 109 void whenFinalized(Action<? super VariantSourcesContext> action);
111 110
112 111 default void whenFinalized(Closure<?> closure) {
113 112 whenFinalized(Closures.action(closure));
114 113 }
115 114
116 115
117 116 /**
118 117 * Imperative selector for the late-configuration mode.
119 118 *
120 119 * <p>Exactly one mode is expected to be chosen for the extension lifecycle.
121 120 */
122 121 interface LateConfigurationPolicySpec {
123 122 /**
124 123 * Rejects selector registration if it targets any already materialized
125 124 * source set.
126 125 */
127 126 void failOnLateConfiguration();
128 127
129 128 /**
130 129 * Allows late selector registration, but emits a warning when it targets an
131 130 * already materialized source set.
132 131 *
133 132 * <p>For such targets, selector precedence is not re-established
134 133 * retroactively. The action is applied as a late imperative step, after the
135 134 * state already produced at the materialization moment.
136 135 */
137 136 void warnOnLateConfiguration();
138 137
139 138 /**
140 139 * Allows late selector registration without a warning when it targets an
141 140 * already materialized source set.
142 141 *
143 142 * <p>For such targets, selector precedence is not re-established
144 143 * retroactively. The action is applied as a late imperative step, after the
145 144 * state already produced at the materialization moment.
146 145 */
147 146 void allowLateConfiguration();
148 147 }
149 148
150 149 interface NamingPolicySpec {
151 150 /**
152 151 * Rejects finalized compile-unit models that project the same source-set
153 152 * name for different compile units.
154 153 */
155 154 void failOnNameCollision();
156 155
157 156 /**
158 157 * Resolves name collisions deterministically for the finalized
159 158 * compile-unit model.
160 159 *
161 160 * <p>Conflicting compile units are ordered canonically by
162 161 * {@code (variant.name, layer.name)}. The first unit keeps the base
163 162 * projected name, and each next unit receives a numeric suffix
164 163 * ({@code 2}, {@code 3}, ...).
165 164 */
166 165 void resolveNameCollision();
167 166 }
168 167 }
@@ -1,92 +1,92
1 1 package org.implab.gradle.variants.sources.internal;
2 2
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5 import org.gradle.api.Action;
6 6 import org.gradle.api.NamedDomainObjectProvider;
7 import org.implab.gradle.common.sources.GenericSourceSet;
8 7 import org.implab.gradle.variants.core.Layer;
9 8 import org.implab.gradle.variants.core.Variant;
10 9 import org.implab.gradle.variants.core.VariantsView;
11 10 import org.implab.gradle.variants.sources.CompileUnit;
12 11 import org.implab.gradle.variants.sources.CompileUnitsView;
12 import org.implab.gradle.variants.sources.GenericSourceSet;
13 13 import org.implab.gradle.variants.sources.RoleProjectionsView;
14 14 import org.implab.gradle.variants.sources.SourceSetMaterializer;
15 15 import org.implab.gradle.variants.sources.VariantSourcesContext;
16 16
17 17 public class DefaultVariantSourcesContext implements VariantSourcesContext {
18 18 private final VariantsView variantsView;
19 19 private final CompileUnitsView compileUnitsView;
20 20 private final RoleProjectionsView roleProjectionsView;
21 21 private final SourceSetMaterializer sourceSetMaterializer;
22 22 private final SourceSetRegistry sourceSetRegistry;
23 23 private final CompileUnitNamer compileUnitNamer;
24 24 private final SourceSetConfigurationRegistry sourceSetConfigurationRegistry;
25 25
26 26 public DefaultVariantSourcesContext(
27 27 VariantsView variantsView,
28 28 CompileUnitsView compileUnitsView,
29 29 RoleProjectionsView roleProjectionsView,
30 30 CompileUnitNamer compileUnitNamer,
31 31 SourceSetRegistry sourceSetRegistry,
32 32 SourceSetConfigurationRegistry sourceSetConfigurationRegistry) {
33 33 this.variantsView = variantsView;
34 34 this.compileUnitNamer = compileUnitNamer;
35 35 this.compileUnitsView = compileUnitsView;
36 36 this.roleProjectionsView = roleProjectionsView;
37 37 this.sourceSetRegistry = sourceSetRegistry;
38 38 this.sourceSetConfigurationRegistry = sourceSetConfigurationRegistry;
39 39
40 40 sourceSetMaterializer = new LocalSourceSetMaterializer();
41 41 }
42 42
43 43 @Override
44 44 public VariantsView getVariants() {
45 45 return variantsView;
46 46 }
47 47
48 48 @Override
49 49 public CompileUnitsView getCompileUnits() {
50 50 return compileUnitsView;
51 51 }
52 52
53 53 @Override
54 54 public RoleProjectionsView getRoleProjections() {
55 55 return roleProjectionsView;
56 56 }
57 57
58 58 @Override
59 59 public SourceSetMaterializer getSourceSets() {
60 60 return sourceSetMaterializer;
61 61 }
62 62
63 63 @Override
64 64 public void configureLayer(Layer layer, Action<? super GenericSourceSet> action) {
65 65 sourceSetConfigurationRegistry.addLayerAction(layer, action);
66 66 }
67 67
68 68 @Override
69 69 public void configureVariant(Variant variant, Action<? super GenericSourceSet> action) {
70 70 sourceSetConfigurationRegistry.addVariantAction(variant, action);
71 71 }
72 72
73 73 @Override
74 74 public void configureUnit(CompileUnit unit, Action<? super GenericSourceSet> action) {
75 75 sourceSetConfigurationRegistry.addCompileUnitAction(unit, action);
76 76 }
77 77
78 78 class LocalSourceSetMaterializer implements SourceSetMaterializer {
79 79 private final Map<CompileUnit, NamedDomainObjectProvider<GenericSourceSet>> registeredSources = new HashMap<>();
80 80
81 81 @Override
82 82 public NamedDomainObjectProvider<GenericSourceSet> getSourceSet(CompileUnit unit) {
83 83 return registeredSources.computeIfAbsent(unit, k -> {
84 84 var sourcesName = compileUnitNamer.resolveName(unit);
85 85 sourceSetRegistry.whenMaterialized(sourcesName,
86 86 sourceSet -> sourceSetConfigurationRegistry.applyConfiguration(unit, sourceSet));
87 87 return sourceSetRegistry.sourceSets().register(sourcesName);
88 88 });
89 89 }
90 90
91 91 }
92 92 }
@@ -1,114 +1,114
1 1 package org.implab.gradle.variants.sources.internal;
2 2
3 3 import java.text.MessageFormat;
4 4 import java.util.LinkedHashMap;
5 5 import java.util.LinkedList;
6 6 import java.util.List;
7 7 import java.util.Map;
8 8 import java.util.function.Consumer;
9 9 import java.util.function.Supplier;
10 10 import java.util.stream.Collectors;
11 11
12 12 import org.eclipse.jdt.annotation.NonNullByDefault;
13 13 import org.gradle.api.Action;
14 14 import org.gradle.api.Named;
15 15 import org.gradle.api.logging.Logger;
16 16 import org.gradle.api.logging.Logging;
17 import org.implab.gradle.common.sources.GenericSourceSet;
18 17 import org.implab.gradle.variants.core.Layer;
19 18 import org.implab.gradle.variants.core.Variant;
20 19 import org.implab.gradle.variants.sources.CompileUnit;
20 import org.implab.gradle.variants.sources.GenericSourceSet;
21 21
22 22 @NonNullByDefault
23 23 public class SourceSetConfigurationRegistry {
24 24 private static final Logger logger = Logging.getLogger(SourceSetConfigurationRegistry.class);
25 25
26 26 private final Map<Layer, ReplayableQueue<GenericSourceSet>> sourcesByLayer = new LinkedHashMap<>();
27 27 private final Map<Variant, ReplayableQueue<GenericSourceSet>> sourcesByVariant = new LinkedHashMap<>();
28 28 private final Map<CompileUnit, ReplayableQueue<GenericSourceSet>> sourcesByUnit = new LinkedHashMap<>();
29 29
30 30 private final Supplier<LateConfigurationMode> lateConfigurationMode;
31 31
32 32 public SourceSetConfigurationRegistry(Supplier<LateConfigurationMode> lateConfigurationMode) {
33 33 this.lateConfigurationMode = lateConfigurationMode;
34 34 }
35 35
36 36 public void addLayerAction(Layer layer, Action<? super GenericSourceSet> action) {
37 37 addToActions(
38 38 sourcesByLayer.computeIfAbsent(layer, key -> new ReplayableQueue<>()),
39 39 action,
40 40 MessageFormat.format(
41 41 "Source sets for [layer={0}] layer already materialized",
42 42 layer.getName()));
43 43 }
44 44
45 45 public void addVariantAction(Variant variant, Action<? super GenericSourceSet> action) {
46 46 addToActions(
47 47 sourcesByVariant.computeIfAbsent(variant, key -> new ReplayableQueue<>()),
48 48 action,
49 49 MessageFormat.format(
50 50 "Source sets for [variant={0}] variant already materialized",
51 51 variant.getName()));
52 52
53 53 }
54 54
55 55 public void addCompileUnitAction(CompileUnit unit, Action<? super GenericSourceSet> action) {
56 56 addToActions(
57 57 sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()),
58 58 action,
59 59 MessageFormat.format(
60 60 "Source set for [variant={0}, layer={1}] already materialed",
61 61 unit.variant().getName(),
62 62 unit.layer().getName()));
63 63 }
64 64
65 65 private void addToActions(
66 66 ReplayableQueue<GenericSourceSet> actions,
67 67 Action<? super GenericSourceSet> action,
68 68 String assertMessage) {
69 69 assertLazyConfiguration(actions.values(), assertMessage);
70 70 actions.forEach(action::execute);
71 71 }
72 72
73 73 void assertLazyConfiguration(List<GenericSourceSet> sets, String message) {
74 74 if (sets.size() == 0)
75 75 return;
76 76
77 77 var names = sets.stream().map(Named::getName).collect(Collectors.joining(", "));
78 78
79 79 switch (lateConfigurationMode.get()) {
80 80 case FAIL:
81 81 throw new IllegalStateException(message + " [" + names + "]");
82 82 case WARN:
83 83 logger.warn(message + "\n\t" + names);
84 84 break;
85 85 default:
86 86 break;
87 87 }
88 88 }
89 89
90 90 public void applyConfiguration(CompileUnit unit, GenericSourceSet sourceSet) {
91 91 sourcesByVariant.computeIfAbsent(unit.variant(), key -> new ReplayableQueue<>()).add(sourceSet);
92 92 sourcesByLayer.computeIfAbsent(unit.layer(), key -> new ReplayableQueue<>()).add(sourceSet);
93 93 sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()).add(sourceSet);
94 94 }
95 95
96 96 class ReplayableQueue<T> {
97 97 private final List<Consumer<? super T>> consumers = new LinkedList<>();
98 98 private final List<T> values = new LinkedList<>();
99 99
100 100 public void add(T value) {
101 101 consumers.forEach(consumer -> consumer.accept(value));
102 102 values.add(value);
103 103 }
104 104
105 105 List<T> values() {
106 106 return List.copyOf(values);
107 107 }
108 108
109 109 public void forEach(Consumer<? super T> consumer) {
110 110 values.forEach(consumer);
111 111 consumers.add(consumer);
112 112 }
113 113 }
114 114 }
@@ -1,46 +1,46
1 1 package org.implab.gradle.variants.sources.internal;
2 2
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5 import java.util.function.Consumer;
6 6
7 7 import org.gradle.api.NamedDomainObjectContainer;
8 8 import org.gradle.api.model.ObjectFactory;
9 9 import org.implab.gradle.common.core.lang.Deferred;
10 import org.implab.gradle.common.sources.GenericSourceSet;
10 import org.implab.gradle.variants.sources.GenericSourceSet;
11 11
12 12 public class SourceSetRegistry {
13 13 private final Map<String, Deferred<GenericSourceSet>> materialized = new HashMap<>();
14 14 private final NamedDomainObjectContainer<GenericSourceSet> sourceSets;
15 15 private final ObjectFactory objectFactory;
16 16
17 17 public SourceSetRegistry(ObjectFactory objectFactory) {
18 18 this.objectFactory = objectFactory;
19 19 this.sourceSets = objectFactory.domainObjectContainer(GenericSourceSet.class, this::createSourceSet);
20 20 }
21 21
22 22 void forEachMaterialized(Consumer<? super GenericSourceSet> consumer) {
23 23 materialized.values().stream()
24 24 .filter(Deferred::resolved)
25 25 .map(Deferred::value)
26 26 .forEach(consumer);
27 27 }
28 28
29 29 public NamedDomainObjectContainer<GenericSourceSet> sourceSets() {
30 30 return sourceSets;
31 31 }
32 32
33 33 public void whenMaterialized(String name, Consumer<? super GenericSourceSet> consumer) {
34 34 materialized(name).whenResolved(consumer);
35 35 }
36 36
37 37 private GenericSourceSet createSourceSet(String name) {
38 38 var sourceSet = objectFactory.newInstance(GenericSourceSet.class, name);
39 39 materialized(name).resolve(sourceSet);
40 40 return sourceSet;
41 41 }
42 42
43 43 private Deferred<GenericSourceSet> materialized(String name) {
44 44 return materialized.computeIfAbsent(name, k -> new Deferred<>());
45 45 }
46 46 }
@@ -1,1 +1,1
1 implementation-class=org.implab.gradle.common.sources.VariantArtifactsPlugin
1 implementation-class=org.implab.gradle.variants.VariantArtifactsPlugin
@@ -1,1 +1,1
1 implementation-class=org.implab.gradle.common.sources.VariantsSourcesPlugin
1 implementation-class=org.implab.gradle.variants.VariantSourcesPlugin
@@ -1,1 +1,1
1 implementation-class=org.implab.gradle.common.sources.VariantsPlugin
1 implementation-class=org.implab.gradle.variants.VariantsPlugin
@@ -1,83 +1,98
1 1 package org.implab.gradle.variants;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertTrue;
4 4
5 5 import java.io.File;
6 6 import java.io.IOException;
7 7 import java.nio.file.Files;
8 8 import java.nio.file.Path;
9 9 import java.util.LinkedHashSet;
10 10 import java.util.List;
11 11 import java.util.stream.Collectors;
12 12
13 13 import org.gradle.testkit.runner.GradleRunner;
14 14 import org.gradle.testkit.runner.UnexpectedBuildFailure;
15 import org.implab.gradle.common.sources.GenericSourceSet;
15 import org.implab.gradle.common.core.lang.Deferred;
16 import org.implab.gradle.variants.sources.GenericSourceSet;
16 17 import org.junit.jupiter.api.io.TempDir;
17 18
18 19 abstract class AbstractFunctionalTest {
19 20 private static final String SETTINGS_FILE = "settings.gradle";
20 21 private static final String BUILD_FILE = "build.gradle";
21 22
22 23 @TempDir
23 24 Path testProjectDir;
24 25
25 26 protected void writeSettings(String rootProjectName) throws IOException {
26 27 writeFile(SETTINGS_FILE, "rootProject.name = '" + rootProjectName + "'\n");
27 28 }
28 29
29 30 protected void writeBuildFile(String body) throws IOException {
30 31 writeFile(BUILD_FILE, buildscriptClasspathBlock() + "\n" + body);
31 32 }
32 33
33 34 protected GradleRunner runner(String... arguments) {
34 35 return GradleRunner.create()
35 36 .withProjectDir(testProjectDir.toFile())
36 37 .withArguments(arguments)
37 38 .forwardOutput();
38 39 }
39 40
40 41 protected void assertBuildFails(String expectedError, String... arguments) {
41 42 var ex = org.junit.jupiter.api.Assertions.assertThrows(
42 43 UnexpectedBuildFailure.class,
43 44 () -> runner(arguments).build());
44 45
45 46 var output = ex.getBuildResult().getOutput();
46 47 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
47 48 }
48 49
49 50 private static String buildscriptClasspathBlock() {
50 51 var classpath = pluginClasspath().stream()
51 52 .map(file -> "'" + file.getAbsolutePath().replace("\\", "\\\\") + "'")
52 53 .collect(Collectors.joining(", "));
53 54
54 55 return """
55 56 buildscript {
56 57 dependencies {
57 58 classpath files(%s)
58 59 }
59 60 }
60 61 """.formatted(classpath);
61 62 }
62 63
63 64 private static List<File> pluginClasspath() {
64 65 try {
65 66 var files = new LinkedHashSet<File>();
66 67 files.add(codeSource(VariantSourcesPlugin.class));
68 files.add(resourceRoot("META-INF/gradle-plugins/org.implab.gradle-variants.properties"));
67 69 files.add(codeSource(GenericSourceSet.class));
70 files.add(codeSource(Deferred.class));
68 71 return List.copyOf(files);
69 72 } catch (Exception e) {
70 73 throw new RuntimeException("Unable to build plugin classpath for test", e);
71 74 }
72 75 }
73 76
74 77 private static File codeSource(Class<?> type) throws Exception {
75 78 return Path.of(type.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile();
76 79 }
77 80
78 private void writeFile(String relativePath, String content) throws IOException {
81 private static File resourceRoot(String resourceName) throws Exception {
82 var url = VariantSourcesPlugin.class.getClassLoader().getResource(resourceName);
83 if (url == null)
84 throw new IllegalStateException("Classpath resource isn't found: " + resourceName);
85
86 var path = Path.of(url.toURI());
87 var segments = resourceName.split("/").length;
88 for (var i = 0; i < segments; i++)
89 path = path.getParent();
90 return path.toFile();
91 }
92
93 protected void writeFile(String relativePath, String content) throws IOException {
79 94 Path path = testProjectDir.resolve(relativePath);
80 95 Files.createDirectories(path.getParent());
81 96 Files.writeString(path, content);
82 97 }
83 98 }
@@ -1,431 +1,431
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.junit.jupiter.api.Test;
7 7
8 8 class VariantSourcesPluginFunctionalTest extends AbstractFunctionalTest {
9 9
10 10 @Test
11 11 void exposesDerivedViewsAndStableSourceSetProvider() throws Exception {
12 12 writeSettings("variant-sources-derived-views");
13 13 writeBuildFile("""
14 14 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
15 15
16 16 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
17 17 variantsExt.layers.create('main')
18 18 variantsExt.layers.create('test')
19 19 variantsExt.roles.create('production')
20 20 variantsExt.roles.create('test')
21 21
22 22 variantsExt.variant('browser') {
23 23 role('production') { layers('main') }
24 24 role('test') { layers('main', 'test') }
25 25 }
26 26
27 27 def lines = []
28 28
29 29 variantSources.whenFinalized { ctx ->
30 30 lines << "units=" + ctx.compileUnits.units
31 31 .collect { "${it.variant().name}:${it.layer().name}" }
32 32 .sort()
33 33 .join(',')
34 34
35 35 def browser = ctx.variants.variants.find { it.name == 'browser' }
36 36 def production = ctx.variants.roles.find { it.name == 'production' }
37 37 def mainLayer = ctx.variants.layers.find { it.name == 'main' }
38 def projection = ctx.roleProjections.getProjection(browser, production)
38 def projection = ctx.roleProjections.requireProjection(browser, production)
39 39 def unit = ctx.compileUnits.requireUnit(browser, mainLayer)
40 40
41 41 def left = ctx.sourceSets.getSourceSet(unit)
42 42 def right = ctx.sourceSets.getSourceSet(unit)
43 43
44 44 lines << "projectionUnits=" + ctx.roleProjections.getUnits(projection)
45 45 .collect { it.layer().name }
46 46 .sort()
47 47 .join(',')
48 48 lines << "mainSourceSet=" + left.name
49 49 lines << "sameProvider=" + left.is(right)
50 50 }
51 51
52 52 afterEvaluate {
53 53 variantSources.whenFinalized { ctx ->
54 54 lines << "late:variants=" + ctx.variants.variants.collect { it.name }.sort().join(',')
55 55 }
56 56 }
57 57
58 58 tasks.register('probe') {
59 59 doLast {
60 60 lines.each { println(it) }
61 61 }
62 62 }
63 63 """);
64 64
65 65 BuildResult result = runner("probe").build();
66 66
67 67 assertTrue(result.getOutput().contains("units=browser:main,browser:test"));
68 68 assertTrue(result.getOutput().contains("projectionUnits=main"));
69 69 assertTrue(result.getOutput().contains("mainSourceSet=browserMain"));
70 70 assertTrue(result.getOutput().contains("sameProvider=true"));
71 71 assertTrue(result.getOutput().contains("late:variants=browser"));
72 72 }
73 73
74 74 @Test
75 75 void appliesSelectorPrecedenceForFutureMaterialization() throws Exception {
76 76 writeSettings("variant-sources-precedence");
77 77 writeBuildFile("""
78 78 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
79 79
80 80 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
81 81 variantsExt.layers.create('main')
82 82 variantsExt.layers.create('test')
83 83 variantsExt.roles.create('production')
84 84 variantsExt.roles.create('test')
85 85
86 86 variantsExt.variant('browser') {
87 87 role('production') { layers('main') }
88 88 role('test') { layers('main', 'test') }
89 89 }
90 90
91 91 variantsExt.variant('node') {
92 92 role('production') { layers('main') }
93 93 }
94 94
95 95 def events = []
96 96
97 97 variantSources {
98 98 variant('browser') {
99 99 events << "variant:" + name
100 100 }
101 101 layer('main') {
102 102 events << "layer:" + name
103 103 }
104 104 unit('browser', 'main') {
105 105 events << "unit:" + name
106 106 }
107 107 }
108 108
109 109 afterEvaluate {
110 110 variantSources.whenFinalized { ctx ->
111 111 def browser = ctx.variants.variants.find { it.name == 'browser' }
112 112 def node = ctx.variants.variants.find { it.name == 'node' }
113 113 def mainLayer = ctx.variants.layers.find { it.name == 'main' }
114 114 def testLayer = ctx.variants.layers.find { it.name == 'test' }
115 115
116 116 def browserMain = ctx.sourceSets.getSourceSet(ctx.compileUnits.requireUnit(browser, mainLayer)).get()
117 117 def browserTest = ctx.sourceSets.getSourceSet(ctx.compileUnits.requireUnit(browser, testLayer)).get()
118 118 def nodeMain = ctx.sourceSets.getSourceSet(ctx.compileUnits.requireUnit(node, mainLayer)).get()
119 119 def bySourceSet = events.groupBy { it.split(':', 2)[1] }
120 120
121 121 println("browserMain=" + bySourceSet[browserMain.name].collect { it.split(':', 2)[0] }.join(','))
122 122 println("browserTest=" + bySourceSet[browserTest.name].collect { it.split(':', 2)[0] }.join(','))
123 123 println("nodeMain=" + bySourceSet[nodeMain.name].collect { it.split(':', 2)[0] }.join(','))
124 124 }
125 125 }
126 126 """);
127 127
128 128 BuildResult result = runner("help").build();
129 129
130 130 assertTrue(result.getOutput().contains("browserMain=variant,layer,unit"));
131 131 assertTrue(result.getOutput().contains("browserTest=variant"));
132 132 assertTrue(result.getOutput().contains("nodeMain=layer"));
133 133 }
134 134
135 135 @Test
136 136 void failsLateConfigurationByDefaultAfterMaterialization() throws Exception {
137 137 writeSettings("variant-sources-late-fail");
138 138 writeBuildFile("""
139 139 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
140 140
141 141 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
142 142 variantsExt.layers.create('main')
143 143 variantsExt.roles.create('production')
144 144 variantsExt.variant('browser') {
145 145 role('production') { layers('main') }
146 146 }
147 147
148 148 afterEvaluate {
149 149 variantSources.whenFinalized { ctx ->
150 150 def browser = ctx.variants.variants.find { it.name == 'browser' }
151 151 def mainLayer = ctx.variants.layers.find { it.name == 'main' }
152 152 def unit = ctx.compileUnits.requireUnit(browser, mainLayer)
153 153
154 154 ctx.sourceSets.getSourceSet(unit).get()
155 155 variantSources.layer('main') {
156 156 declareOutputs('late')
157 157 }
158 158 }
159 159 }
160 160 """);
161 161
162 162 assertBuildFails("Source sets for [layer=main] layer already materialized", "help");
163 163 }
164 164
165 165 @Test
166 166 void allowsLateConfigurationWhenSelectedBeforeFirstSelector() throws Exception {
167 167 writeSettings("variant-sources-late-allow");
168 168 writeBuildFile("""
169 169 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
170 170
171 171 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
172 172 variantsExt.layers.create('main')
173 173 variantsExt.roles.create('production')
174 174 variantsExt.variant('browser') {
175 175 role('production') { layers('main') }
176 176 }
177 177
178 178 variantSources {
179 179 lateConfigurationPolicy {
180 180 allowLateConfiguration()
181 181 }
182 182 }
183 183
184 184 afterEvaluate {
185 185 variantSources.whenFinalized { ctx ->
186 186 def browser = ctx.variants.variants.find { it.name == 'browser' }
187 187 def mainLayer = ctx.variants.layers.find { it.name == 'main' }
188 188 def unit = ctx.compileUnits.requireUnit(browser, mainLayer)
189 189
190 190 def sourceSet = ctx.sourceSets.getSourceSet(unit).get()
191 191 variantSources.layer('main') {
192 192 declareOutputs('late')
193 193 }
194 194 sourceSet.output('late')
195 195 println('lateAllowed=ok')
196 196 }
197 197 }
198 198 """);
199 199
200 200 BuildResult result = runner("help").build();
201 201 assertTrue(result.getOutput().contains("lateAllowed=ok"));
202 202 }
203 203
204 204 @Test
205 205 void warnsAndAppliesLateConfigurationWhenWarnModeSelected() throws Exception {
206 206 writeSettings("variant-sources-late-warn");
207 207 writeBuildFile("""
208 208 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
209 209
210 210 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
211 211 variantsExt.layers.create('main')
212 212 variantsExt.roles.create('production')
213 213 variantsExt.variant('browser') {
214 214 role('production') { layers('main') }
215 215 }
216 216
217 217 variantSources {
218 218 lateConfigurationPolicy {
219 219 warnOnLateConfiguration()
220 220 }
221 221 }
222 222
223 223 afterEvaluate {
224 224 variantSources.whenFinalized { ctx ->
225 225 def browser = ctx.variants.variants.find { it.name == 'browser' }
226 226 def mainLayer = ctx.variants.layers.find { it.name == 'main' }
227 227 def unit = ctx.compileUnits.requireUnit(browser, mainLayer)
228 228
229 229 def sourceSet = ctx.sourceSets.getSourceSet(unit).get()
230 230 variantSources.layer('main') {
231 231 declareOutputs('late')
232 232 }
233 233 sourceSet.output('late')
234 234 println('lateWarn=ok')
235 235 }
236 236 }
237 237 """);
238 238
239 239 BuildResult result = runner("help").build();
240 240
241 241 assertTrue(result.getOutput().contains("Source sets for [layer=main] layer already materialized"));
242 242 assertTrue(result.getOutput().contains("lateWarn=ok"));
243 243 }
244 244
245 245 @Test
246 246 void rejectsChangingLateConfigurationPolicyAfterFirstSelector() throws Exception {
247 247 writeSettings("variant-sources-late-policy-fixed");
248 248 writeBuildFile("""
249 249 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
250 250
251 251 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
252 252 variantsExt.layers.create('main')
253 253 variantsExt.roles.create('production')
254 254 variantsExt.variant('browser') {
255 255 role('production') { layers('main') }
256 256 }
257 257
258 258 variantSources {
259 259 variant('browser') {
260 260 declareOutputs('js')
261 261 }
262 262 lateConfigurationPolicy {
263 263 allowLateConfiguration()
264 264 }
265 265 }
266 266 """);
267 267
268 268 assertBuildFails("Lazy configuration policy already applied", "help");
269 269 }
270 270
271 271 @Test
272 272 void failsOnProjectedNameCollisionByDefault() throws Exception {
273 273 writeSettings("variant-sources-name-collision-fail");
274 274 writeBuildFile("""
275 275 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
276 276
277 277 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
278 278 variantsExt.layers.create('variantBar')
279 279 variantsExt.layers.create('bar')
280 280 variantsExt.roles.create('production')
281 281
282 282 variantsExt.variant('foo') {
283 283 role('production') { layers('variantBar') }
284 284 }
285 285 variantsExt.variant('fooVariant') {
286 286 role('production') { layers('bar') }
287 287 }
288 288 """);
289 289
290 290 assertBuildFails("The same source set names are produced by different compile units", "help");
291 291 }
292 292
293 293 @Test
294 294 void resolvesProjectedNameCollisionDeterministicallyWhenConfigured() throws Exception {
295 295 writeSettings("variant-sources-name-collision-resolve");
296 296 writeBuildFile("""
297 297 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
298 298
299 299 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
300 300 variantsExt.layers.create('variantBar')
301 301 variantsExt.layers.create('bar')
302 302 variantsExt.roles.create('production')
303 303
304 304 variantsExt.variant('foo') {
305 305 role('production') { layers('variantBar') }
306 306 }
307 307 variantsExt.variant('fooVariant') {
308 308 role('production') { layers('bar') }
309 309 }
310 310
311 311 variantSources {
312 312 namingPolicy {
313 313 resolveNameCollision()
314 314 }
315 315 }
316 316
317 317 afterEvaluate {
318 318 variantSources.whenFinalized { ctx ->
319 319 def foo = ctx.variants.variants.find { it.name == 'foo' }
320 320 def fooVariant = ctx.variants.variants.find { it.name == 'fooVariant' }
321 321 def variantBar = ctx.variants.layers.find { it.name == 'variantBar' }
322 322 def bar = ctx.variants.layers.find { it.name == 'bar' }
323 323
324 324 def later = ctx.compileUnits.requireUnit(fooVariant, bar)
325 325 def earlier = ctx.compileUnits.requireUnit(foo, variantBar)
326 326
327 327 println("map1=" + later.variant().name + ":" + later.layer().name + "->" + ctx.sourceSets.getSourceSet(later).name)
328 328 println("map2=" + earlier.variant().name + ":" + earlier.layer().name + "->" + ctx.sourceSets.getSourceSet(earlier).name)
329 329 }
330 330 }
331 331 """);
332 332
333 333 BuildResult result = runner("help").build();
334 334
335 335 assertTrue(result.getOutput().contains("map1=fooVariant:bar->fooVariantBar2"));
336 336 assertTrue(result.getOutput().contains("map2=foo:variantBar->fooVariantBar"));
337 337 }
338 338
339 339 @Test
340 340 void failsOnUnknownVariantSelectorTarget() throws Exception {
341 341 writeSettings("variant-sources-missing-variant");
342 342 writeBuildFile("""
343 343 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
344 344
345 345 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
346 346 variantsExt.layers.create('main')
347 347 variantsExt.roles.create('production')
348 348 variantsExt.variant('browser') {
349 349 role('production') { layers('main') }
350 350 }
351 351
352 352 variantSources {
353 353 variant('missing') {
354 354 declareOutputs('js')
355 355 }
356 356 }
357 357 """);
358 358
359 359 assertBuildFails("Variant 'missing' is't declared", "help");
360 360 }
361 361
362 362 @Test
363 363 void failsOnUnknownLayerSelectorTarget() throws Exception {
364 364 writeSettings("variant-sources-missing-layer");
365 365 writeBuildFile("""
366 366 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
367 367
368 368 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
369 369 variantsExt.layers.create('main')
370 370 variantsExt.roles.create('production')
371 371 variantsExt.variant('browser') {
372 372 role('production') { layers('main') }
373 373 }
374 374
375 375 variantSources {
376 376 layer('missing') {
377 377 declareOutputs('js')
378 378 }
379 379 }
380 380 """);
381 381
382 382 assertBuildFails("Layer 'missing' isn't declared", "help");
383 383 }
384 384
385 385 @Test
386 386 void failsOnUndeclaredCompileUnitSelectorTarget() throws Exception {
387 387 writeSettings("variant-sources-missing-unit");
388 388 writeBuildFile("""
389 389 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
390 390
391 391 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
392 392 variantsExt.layers.create('main')
393 393 variantsExt.layers.create('test')
394 394 variantsExt.roles.create('production')
395 395 variantsExt.variant('browser') {
396 396 role('production') { layers('main') }
397 397 }
398 398
399 399 variantSources {
400 400 unit('browser', 'test') {
401 401 declareOutputs('js')
402 402 }
403 403 }
404 404 """);
405 405
406 406 assertBuildFails("The CompileUnit isn't declared for variant 'browser', layer 'test'", "help");
407 407 }
408 408
409 409 @Test
410 410 void rejectsChangingNamingPolicyAfterContextBecomesObservable() throws Exception {
411 411 writeSettings("variant-sources-name-policy-fixed");
412 412 writeBuildFile("""
413 413 apply plugin: org.implab.gradle.variants.VariantSourcesPlugin
414 414
415 415 def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
416 416 variantsExt.layers.create('main')
417 417 variantsExt.roles.create('production')
418 418 variantsExt.variant('browser') {
419 419 role('production') { layers('main') }
420 420 }
421 421
422 422 variantSources.whenFinalized {
423 423 variantSources.namingPolicy {
424 424 resolveNameCollision()
425 425 }
426 426 }
427 427 """);
428 428
429 429 assertBuildFails("Naming policy already applied", "help");
430 430 }
431 431 }
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (608 lines changed) Show them Hide them
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now