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