##// END OF EJS Templates
Keep contribution-based assemblies on managed Sync tasks, register direct producers through ArtifactAssemblyRegistry, wire builtBy on outgoing artifacts, and cover the behavior with functional tests.
cin -
r61:9b11838beca6 default
parent child
Show More
@@ -281,6 +281,16 variantArtifacts {
281 }
281 }
282 ```
282 ```
283
283
284 Slot bodies have two assembly modes:
285
286 - contribution-based assembly with `from(...)`, `fromVariant(...)`,
287 `fromRole(...)`, or `fromLayer(...)`; the plugin copies selected inputs into a
288 managed directory and publishes that directory;
289 - task-produced assembly with `producedBy(task) { outputFile }`; the mapped task
290 output file or directory is published directly.
291
292 These modes are mutually exclusive for one slot.
293
284 The artifact API is still considered pre-1.0 and may change.
294 The artifact API is still considered pre-1.0 and may change.
285
295
286 ## Publication Status
296 ## Publication Status
@@ -279,6 +279,9 final class VariantArtifactsRegistry imp
279
279
280 interface ArtifactAssemblyRules {
280 interface ArtifactAssemblyRules {
281 void from(Object input);
281 void from(Object input);
282 <T extends Task> void producedBy(
283 TaskProvider<T> task,
284 Function<? super T, ? extends Provider<? extends FileSystemLocation>> output);
282 void fromVariant(Action<? super OutputSelectionSpec> action);
285 void fromVariant(Action<? super OutputSelectionSpec> action);
283 void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
286 void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
284 void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
287 void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
@@ -339,6 +342,8 This means:
339 - slot inputs remain live;
342 - slot inputs remain live;
340 - `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep
343 - `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep
341 contributing inputs until task execution;
344 contributing inputs until task execution;
345 - `producedBy(...)` publishes an existing task output directly and does not
346 create the managed copy assembly for that slot;
342 - `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task
347 - `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task
343 wiring;
348 wiring;
344 - external task outputs remain outside the control of this model and must be
349 - external task outputs remain outside the control of this model and must be
@@ -412,8 +417,9 variantArtifacts {
412 }
417 }
413
418
414 slot("bundleMetadata") {
419 slot("bundleMetadata") {
415 from(someTask)
420 producedBy(writePackageMetadata) {
416 from(layout.buildDirectory.file("generated/meta.json"))
421 outputFile
422 }
417 }
423 }
418 }
424 }
419 }
425 }
@@ -428,7 +434,10 variantArtifacts {
428 belong to the given role projection;
434 belong to the given role projection;
429 - `fromLayer(layer) { output(...) }` selects named outputs from the compile unit
435 - `fromLayer(layer) { output(...) }` selects named outputs from the compile unit
430 of the current variant and the given layer, if such unit exists.
436 of the current variant and the given layer, if such unit exists.
437 - `producedBy(task) { outputFile }` maps an existing producing task to the single
438 file or directory published for the slot.
431
439
440 Contribution forms and `producedBy(...)` are mutually exclusive for one slot.
432 The DSL stores declarations, not resolved file collections.
441 The DSL stores declarations, not resolved file collections.
433
442
434 ---
443 ---
@@ -499,6 +508,9 For one outgoing variant:
499 - expands to one compile unit `(variant, layer)` when it exists;
508 - expands to one compile unit `(variant, layer)` when it exists;
500 - `from(Object)`
509 - `from(Object)`
501 - bypasses `variantSources` completely.
510 - bypasses `variantSources` completely.
511 - `producedBy(task)`
512 - bypasses contribution resolution and registers the task output as the slot
513 artifact directly.
502
514
503 After compile units are known, the bridge asks
515 After compile units are known, the bridge asks
504 `ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the
516 `ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the
@@ -1,6 +1,13
1 package org.implab.gradle.variants.artifacts;
1 package org.implab.gradle.variants.artifacts;
2
2
3 import java.util.function.Function;
4
3 import org.gradle.api.Action;
5 import org.gradle.api.Action;
6 import org.gradle.api.InvalidUserDataException;
7 import org.gradle.api.Task;
8 import org.gradle.api.file.FileSystemLocation;
9 import org.gradle.api.provider.Provider;
10 import org.gradle.api.tasks.TaskProvider;
4 import groovy.lang.Closure;
11 import groovy.lang.Closure;
5 import org.implab.gradle.common.core.lang.Closures;
12 import org.implab.gradle.common.core.lang.Closures;
6
13
@@ -24,6 +31,45 public interface ArtifactAssemblySpec {
24 void from(Object artifact);
31 void from(Object artifact);
25
32
26 /**
33 /**
34 * Registers a task that directly produces the published slot artifact.
35 *
36 * <p>Use this method when the slot is produced as one file or directory by an
37 * existing task, for example generated package metadata. Unlike {@link #from(Object)}
38 * and topology-aware selectors, this does not copy inputs into a managed assembly
39 * directory. The mapped task output becomes the published artifact itself.
40 *
41 * <p>This mode is mutually exclusive with contribution-based assembly methods
42 * such as {@link #from(Object)}, {@link #fromVariant(Action)}, {@link #fromRole(String, Action)},
43 * and {@link #fromLayer(String, Action)} for the same slot.
44 *
45 * @param <T> task type
46 * @param task task provider producing the artifact
47 * @param artifact maps the producing task to its output file or directory provider
48 */
49 <T extends Task> void producedBy(
50 TaskProvider<T> task,
51 Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact);
52
53 default <T extends Task> void producedBy(TaskProvider<T> task, Closure<?> closure) {
54 producedBy(task, taskInstance -> producedArtifact(closure, taskInstance));
55 }
56
57 @SuppressWarnings("unchecked")
58 private static Provider<? extends FileSystemLocation> producedArtifact(Closure<?> closure, Task task) {
59 var c = (Closure<?>) closure.clone();
60 c.setResolveStrategy(Closure.DELEGATE_FIRST);
61 c.setDelegate(task);
62
63 var artifact = c.call(task);
64 if (artifact instanceof Provider<?>) {
65 return (Provider<? extends FileSystemLocation>) artifact;
66 }
67
68 throw new InvalidUserDataException("Produced artifact mapper for task '" + task.getName()
69 + "' must return Provider<? extends FileSystemLocation>");
70 }
71
72 /**
27 * Selects outputs from the whole variant scope.
73 * Selects outputs from the whole variant scope.
28 *
74 *
29 * @param action output selection rule
75 * @param action output selection rule
@@ -32,7 +32,9 public class ArtifactAssemblyBinder impl
32 // Bind the primary artifact set to the root outgoing configuration.
32 // Bind the primary artifact set to the root outgoing configuration.
33 resolver.when(
33 resolver.when(
34 new ArtifactSlot(variant, primarySlot),
34 new ArtifactSlot(variant, primarySlot),
35 assembly -> outgoing.artifact(assembly.getArtifact()));
35 assembly -> outgoing.artifact(
36 assembly.getArtifact(),
37 artifact -> artifact.builtBy(assembly.getAssemblyTask())));
36
38
37 // Bind non-primary slots to Gradle secondary artifact variants.
39 // Bind non-primary slots to Gradle secondary artifact variants.
38 slots.all(slot -> {
40 slots.all(slot -> {
@@ -46,7 +48,9 public class ArtifactAssemblyBinder impl
46 // otherwise be realized only after dependency resolution starts.
48 // otherwise be realized only after dependency resolution starts.
47 assembly -> outgoing.getVariants()
49 assembly -> outgoing.getVariants()
48 .create(slot.getName())
50 .create(slot.getName())
49 .artifact(assembly.getArtifact()));
51 .artifact(
52 assembly.getArtifact(),
53 artifact -> artifact.builtBy(assembly.getAssemblyTask())));
50 });
54 });
51 });
55 });
52 }
56 }
@@ -4,38 +4,57 import java.util.HashMap;
4 import java.util.HashSet;
4 import java.util.HashSet;
5 import java.util.Map;
5 import java.util.Map;
6 import java.util.Set;
6 import java.util.Set;
7 import java.util.function.Function;
8 import java.util.stream.Stream;
7
9
8 import org.eclipse.jdt.annotation.NonNullByDefault;
10 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.gradle.api.Action;
11 import org.gradle.api.Action;
12 import org.gradle.api.InvalidUserDataException;
13 import org.gradle.api.Task;
10 import org.gradle.api.file.ConfigurableFileCollection;
14 import org.gradle.api.file.ConfigurableFileCollection;
11 import org.gradle.api.file.Directory;
15 import org.gradle.api.file.Directory;
12 import org.gradle.api.file.DirectoryProperty;
16 import org.gradle.api.file.DirectoryProperty;
13 import org.gradle.api.file.FileCollection;
17 import org.gradle.api.file.FileCollection;
18 import org.gradle.api.file.FileSystemLocation;
14 import org.gradle.api.model.ObjectFactory;
19 import org.gradle.api.model.ObjectFactory;
15 import org.gradle.api.provider.Provider;
20 import org.gradle.api.provider.Provider;
16 import org.gradle.api.tasks.Sync;
21 import org.gradle.api.tasks.Sync;
17 import org.gradle.api.tasks.TaskContainer;
22 import org.gradle.api.tasks.TaskContainer;
23 import org.gradle.api.tasks.TaskProvider;
18 import org.gradle.language.base.plugins.LifecycleBasePlugin;
24 import org.gradle.language.base.plugins.LifecycleBasePlugin;
19 import org.implab.gradle.common.core.lang.FilePaths;
25 import org.implab.gradle.common.core.lang.FilePaths;
26 import org.implab.gradle.common.core.lang.Strings;
20 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
27 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
21 import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec;
28 import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec;
22 import org.implab.gradle.variants.artifacts.ArtifactSlot;
29 import org.implab.gradle.variants.artifacts.ArtifactSlot;
30 import org.implab.gradle.variants.artifacts.OutputSelectionSpec;
31 import org.implab.gradle.variants.core.Layer;
32 import org.implab.gradle.variants.core.Role;
23 import org.implab.gradle.variants.sources.CompileUnit;
33 import org.implab.gradle.variants.sources.CompileUnit;
24 import org.implab.gradle.variants.sources.CompileUnitsView;
34 import org.implab.gradle.variants.sources.CompileUnitsView;
25 import org.implab.gradle.variants.sources.RoleProjectionsView;
35 import org.implab.gradle.variants.sources.RoleProjectionsView;
26 import org.implab.gradle.variants.sources.SourceSetMaterializer;
36 import org.implab.gradle.variants.sources.SourceSetMaterializer;
27
37
28 /**
38 /**
29 * Adapts slot contribution declarations to materialized {@link ArtifactAssembly}
39 * Adapts slot contribution declarations to materialized
40 * {@link ArtifactAssembly}
30 * handles.
41 * handles.
31 *
42 *
32 * <p>The handler creates one {@link Sync} task per {@link ArtifactSlot}. The task
43 * <p>
33 * copies all collected slot inputs into a single output directory. That output
44 * Contribution-based assemblies create one {@link Sync} task per
34 * directory is then registered in {@link ArtifactAssemblyRegistry} as the
45 * {@link ArtifactSlot}. The task copies all collected slot inputs into a single
35 * published artifact for the slot.
46 * output directory. That output directory is then registered in
47 * {@link ArtifactAssemblyRegistry} as the published artifact for the slot.
36 *
48 *
37 * <p>Input collection uses {@link SlotContributionVisitor}. Each contribution is
49 * <p>
38 * converted to a {@link SlotInputKey}; duplicate keys are ignored so that repeated
50 * Task-produced assemblies bypass the managed copy task. The producer task is
51 * registered directly in {@link ArtifactAssemblyRegistry}, and its mapped output
52 * file or directory becomes the published slot artifact.
53 *
54 * <p>
55 * Input collection uses {@link SlotContributionVisitor}. Each contribution is
56 * converted to a {@link SlotInputKey}; duplicate keys are ignored so that
57 * repeated
39 * topology-based selections do not add the same input twice.
58 * topology-based selections do not add the same input twice.
40 */
59 */
41 @NonNullByDefault
60 @NonNullByDefault
@@ -56,6 +75,8 public class ArtifactAssemblyHandler {
56
75
57 private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>();
76 private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>();
58
77
78 private final Map<ArtifactSlot, AssemblyMode> assemblyModes = new HashMap<>();
79
59 public ArtifactAssemblyHandler(
80 public ArtifactAssemblyHandler(
60 ObjectFactory objects,
81 ObjectFactory objects,
61 TaskContainer tasks,
82 TaskContainer tasks,
@@ -78,18 +99,21 public class ArtifactAssemblyHandler {
78 }
99 }
79
100
80 public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) {
101 public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) {
81 var visitor = contributionVisitor(artifactSlot);
102 var spec = new DefaultArtifactAssemblySpec(artifactSlot);
82 var spec = new DefaultArtifactAssemblySpec(objects, c -> c.accept(visitor));
83 action.execute(spec);
103 action.execute(spec);
84 }
104 }
85
105
86 public SlotContributionVisitor contributionVisitor(ArtifactSlot artifactSlot) {
106 private void useAssemblyMode(ArtifactSlot artifactSlot, AssemblyMode mode) {
87 var assembly = slotInputs.computeIfAbsent(artifactSlot, this::createSlotAssembly);
107 var previous = assemblyModes.putIfAbsent(artifactSlot, mode);
88 return new ContributionVisitor(artifactSlot, assembly);
108 if (previous != null && previous != mode) {
109 throw new InvalidUserDataException("Artifact slot '" + artifactSlot
110 + "' cannot mix task-produced artifact and contribution-based assembly");
111 }
89 }
112 }
90
113
91 /**
114 /**
92 * Creates the assembly task for the given slot and registers its output artifact.
115 * Creates the assembly task for the given slot and registers its output
116 * artifact.
93 */
117 */
94 private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) {
118 private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) {
95 var assembly = new SlotAssembly();
119 var assembly = new SlotAssembly();
@@ -199,4 +223,99 public class ArtifactAssemblyHandler {
199 return inputs;
223 return inputs;
200 }
224 }
201 }
225 }
226
227 private enum AssemblyMode {
228 CONTRIBUTIONS,
229 TASK_PRODUCER
202 }
230 }
231
232 /**
233 * Default DSL facade for collecting {@link SlotContribution} declarations.
234 *
235 * <p>
236 * The spec does not validate topology references immediately. It translates DSL
237 * calls to contribution objects and passes them to the supplied consumer;
238 * semantic
239 * validation happens later when the assembly handler resolves contributions
240 * against the finalized source model.
241 */
242 class DefaultArtifactAssemblySpec implements ArtifactAssemblySpec {
243
244 private final ArtifactSlot artifactSlot;
245
246 DefaultArtifactAssemblySpec(ArtifactSlot artifactSlot) {
247 this.artifactSlot = artifactSlot;
248 }
249
250 @Override
251 public void from(Object artifact) {
252 contribute(new DirectContribution(artifact));
253 }
254
255 @Override
256 public <T extends Task> void producedBy(
257 TaskProvider<T> task,
258 Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact) {
259 registerProducedArtifact(task, artifact);
260 }
261
262 @Override
263 public void fromVariant(Action<? super OutputSelectionSpec> action) {
264 contribute(new VariantOutputsContribution(outputs(action)));
265 }
266
267 @Override
268 public void fromRole(String roleName, Action<? super OutputSelectionSpec> action) {
269
270 contribute(new RoleOutputsContribution(
271 objects.named(Role.class, roleName),
272 outputs(action)));
273 }
274
275 @Override
276 public void fromLayer(String layerName, Action<? super OutputSelectionSpec> action) {
277 contribute(new LayerOutputsContribution(
278 objects.named(Layer.class, layerName),
279 outputs(action)));
280 }
281
282 private static Set<String> outputs(Action<? super OutputSelectionSpec> action) {
283 var spec = new OutputsSetSpec();
284 action.execute(spec);
285 return spec.outputs();
286 }
287
288 void contribute(SlotContribution contribution) {
289 useAssemblyMode(artifactSlot, AssemblyMode.CONTRIBUTIONS);
290 var assembly = slotInputs.computeIfAbsent(artifactSlot, ArtifactAssemblyHandler.this::createSlotAssembly);
291 var contributionVisitor = new ContributionVisitor(artifactSlot, assembly);
292 contribution.accept(contributionVisitor);
293 }
294
295 <T extends Task> void registerProducedArtifact(
296 TaskProvider<T> task,
297 Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact) {
298 useAssemblyMode(artifactSlot, AssemblyMode.TASK_PRODUCER);
299 assemblyRegistry.register(artifactSlot, task, artifact);
300 }
301
302 }
303
304 /** Simple implementation of {@link OutputSelectionSpec}. */
305 static class OutputsSetSpec implements OutputSelectionSpec {
306 private final Set<String> outputs = new HashSet<>();
307
308 @Override
309 public void output(String name, String... extra) {
310 Stream.concat(Stream.of(name), Stream.of(extra))
311 .map(Strings::requireNonBlank)
312 .forEach(outputs::add);
313 }
314
315 Set<String> outputs() {
316 return Set.copyOf(outputs);
317 }
318
319 }
320
321 }
@@ -422,6 +422,170 class VariantArtifactsPluginFunctionalTe
422 }
422 }
423
423
424 @Test
424 @Test
425 void publishesTaskProducedFileArtifactDirectly() throws Exception {
426 writeFile("settings.gradle", """
427 rootProject.name = 'variant-artifacts-task-produced-file'
428 include 'producer', 'consumer'
429 """);
430 writeBuildFile("""
431 import org.gradle.api.DefaultTask
432 import org.gradle.api.attributes.Attribute
433 import org.gradle.api.file.RegularFileProperty
434 import org.gradle.api.tasks.OutputFile
435 import org.gradle.api.tasks.TaskAction
436
437 def variantAttr = Attribute.of('test.variant', String)
438 def slotAttr = Attribute.of('test.slot', String)
439
440 abstract class WritePackageMetadata extends DefaultTask {
441 @OutputFile
442 abstract RegularFileProperty getOutputFile()
443
444 @TaskAction
445 void write() {
446 def file = outputFile.get().asFile
447 file.parentFile.mkdirs()
448 file.text = '{"name":"demo"}\\n'
449 }
450 }
451
452 project(':producer') {
453 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
454
455 variants.layers.create('main')
456 variants.roles.create('main')
457 variants.variant('browser') {
458 role('main') {
459 layers('main')
460 }
461 }
462
463 def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) {
464 outputFile.set(layout.buildDirectory.file('generated/package.json'))
465 }
466
467 variantArtifacts {
468 variant('browser') {
469 primarySlot('packageMetadata') {
470 producedBy(writePackageMetadata) {
471 outputFile
472 }
473 }
474 }
475
476 whenOutgoingConfiguration { publication ->
477 publication.configuration {
478 attributes.attribute(variantAttr, publication.variant.name)
479 }
480 }
481
482 whenOutgoingSlot { publication ->
483 publication.artifactAttributes {
484 attribute(slotAttr, publication.artifactSlot.slot.name)
485 }
486 }
487 }
488
489 tasks.register('checkNoManagedAssembly') {
490 doLast {
491 def assemblyTasks = tasks.names
492 .findAll { it.startsWith('assembleVariantArtifactSlot') }
493 .sort()
494 println('producerAssemblyTasks=' + assemblyTasks.join(','))
495 assert assemblyTasks.empty
496 }
497 }
498 }
499
500 project(':consumer') {
501 configurations {
502 compileView {
503 canBeResolved = true
504 canBeConsumed = false
505 canBeDeclared = true
506 attributes {
507 attribute(variantAttr, 'browser')
508 attribute(slotAttr, 'packageMetadata')
509 }
510 }
511 }
512
513 dependencies {
514 compileView project(':producer')
515 }
516
517 tasks.register('probe') {
518 dependsOn configurations.compileView
519 dependsOn ':producer:checkNoManagedAssembly'
520
521 doLast {
522 def files = configurations.compileView.files
523 println('resolvedFiles=' + files.collect { it.name }.sort().join(','))
524 println('metadata=' + files.iterator().next().text.trim())
525 }
526 }
527 }
528 """);
529
530 BuildResult result = runner(":consumer:probe").build();
531
532 assertTrue(result.getOutput().contains("producerAssemblyTasks="));
533 assertTrue(result.getOutput().contains("resolvedFiles=package.json"));
534 assertTrue(result.getOutput().contains("metadata={\"name\":\"demo\"}"));
535 assertTrue(result.task(":producer:writePackageMetadata").getOutcome() == TaskOutcome.SUCCESS);
536 assertTrue(result.task(":consumer:probe").getOutcome() == TaskOutcome.SUCCESS);
537 }
538
539 @Test
540 void failsWhenTaskProducedArtifactIsMixedWithContributionAssembly() throws Exception {
541 writeSettings("variant-artifacts-mixed-assembly-mode");
542 writeFile("inputs/marker.txt", "marker\n");
543 writeBuildFile("""
544 import org.gradle.api.DefaultTask
545 import org.gradle.api.file.RegularFileProperty
546 import org.gradle.api.tasks.OutputFile
547 import org.gradle.api.tasks.TaskAction
548
549 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
550
551 abstract class WritePackageMetadata extends DefaultTask {
552 @OutputFile
553 abstract RegularFileProperty getOutputFile()
554
555 @TaskAction
556 void write() {
557 outputFile.get().asFile.text = '{}\\n'
558 }
559 }
560
561 variants.layers.create('main')
562 variants.roles.create('main')
563 variants.variant('browser') {
564 role('main') {
565 layers('main')
566 }
567 }
568
569 def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) {
570 outputFile.set(layout.buildDirectory.file('generated/package.json'))
571 }
572
573 variantArtifacts {
574 variant('browser') {
575 primarySlot('packageMetadata') {
576 producedBy(writePackageMetadata) {
577 outputFile
578 }
579 from(layout.projectDirectory.file('inputs/marker.txt'))
580 }
581 }
582 }
583 """);
584
585 assertBuildFails("cannot mix task-produced artifact and contribution-based assembly", "help");
586 }
587
588 @Test
425 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
589 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
426 writeSettings("variant-artifacts-combined-inputs");
590 writeSettings("variant-artifacts-combined-inputs");
427 writeFile("inputs/base.js", "console.log('base')\n");
591 writeFile("inputs/base.js", "console.log('base')\n");
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now