##// END OF EJS Templates
Added variant artifacts model
cin -
r33:6f01b47b894d default
parent child
Show More
@@ -0,0 +1,53
1 package org.implab.gradle.common.sources;
2
3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 import org.gradle.api.Named;
5 import org.gradle.api.file.ConfigurableFileCollection;
6 import org.gradle.api.file.Directory;
7 import org.gradle.api.file.FileCollection;
8 import org.gradle.api.provider.Provider;
9 import org.gradle.api.tasks.Copy;
10 import org.gradle.api.tasks.TaskProvider;
11
12 @NonNullByDefault
13 public final class ArtifactAssembly implements Named {
14 private final String name;
15 private final ConfigurableFileCollection sources;
16 private final ConfigurableFileCollection output;
17 private final Provider<Directory> outputDirectory;
18 private final TaskProvider<Copy> task;
19
20 ArtifactAssembly(
21 String name,
22 ConfigurableFileCollection sources,
23 Provider<Directory> outputDirectory,
24 TaskProvider<Copy> task,
25 ConfigurableFileCollection output) {
26 this.name = name;
27 this.sources = sources;
28 this.outputDirectory = outputDirectory;
29 this.task = task;
30 this.output = output;
31 }
32
33 @Override
34 public String getName() {
35 return name;
36 }
37
38 public ConfigurableFileCollection getSources() {
39 return sources;
40 }
41
42 public FileCollection getOutput() {
43 return output;
44 }
45
46 public Provider<Directory> getOutputDirectory() {
47 return outputDirectory;
48 }
49
50 public TaskProvider<Copy> getTask() {
51 return task;
52 }
53 }
@@ -0,0 +1,64
1 package org.implab.gradle.common.sources;
2
3 import java.util.LinkedHashMap;
4 import java.util.Map;
5 import java.util.Optional;
6
7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 import org.gradle.api.Action;
9 import org.gradle.api.InvalidUserDataException;
10 import org.gradle.api.file.ConfigurableFileCollection;
11 import org.gradle.api.file.Directory;
12 import org.gradle.api.model.ObjectFactory;
13 import org.gradle.api.provider.Provider;
14 import org.gradle.api.tasks.Copy;
15 import org.gradle.api.tasks.TaskContainer;
16 import org.gradle.language.base.plugins.LifecycleBasePlugin;
17
18 @NonNullByDefault
19 public final class ArtifactAssemblyRegistry {
20 private final ObjectFactory objects;
21 private final TaskContainer tasks;
22 private final Map<String, ArtifactAssembly> assemblies = new LinkedHashMap<>();
23
24 public ArtifactAssemblyRegistry(ObjectFactory objects, TaskContainer tasks) {
25 this.objects = objects;
26 this.tasks = tasks;
27 }
28
29 public ArtifactAssembly register(
30 String name,
31 String taskName,
32 Provider<Directory> outputDirectory,
33 Action<? super ConfigurableFileCollection> configureSources) {
34 if (assemblies.containsKey(name)) {
35 throw new InvalidUserDataException("Artifact assembly '" + name + "' is already registered");
36 }
37
38 var sources = objects.fileCollection();
39 configureSources.execute(sources);
40
41 var task = tasks.register(taskName, Copy.class, copy -> {
42 copy.setGroup(LifecycleBasePlugin.BUILD_GROUP);
43 copy.into(outputDirectory);
44 copy.from(sources);
45 });
46
47 var output = objects.fileCollection()
48 .from(outputDirectory)
49 .builtBy(task);
50
51 var assembly = new ArtifactAssembly(name, sources, outputDirectory, task, output);
52 assemblies.put(name, assembly);
53 return assembly;
54 }
55
56 public Optional<ArtifactAssembly> find(String name) {
57 return Optional.ofNullable(assemblies.get(name));
58 }
59
60 public ArtifactAssembly require(String name) {
61 return find(name)
62 .orElseThrow(() -> new InvalidUserDataException("Artifact assembly '" + name + "' isn't registered"));
63 }
64 }
@@ -0,0 +1,38
1 package org.implab.gradle.common.sources;
2
3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 import org.implab.gradle.common.core.lang.Closures;
5 import org.gradle.api.Action;
6 import org.gradle.api.NamedDomainObjectProvider;
7 import org.gradle.api.artifacts.Configuration;
8
9 import groovy.lang.Closure;
10 import groovy.lang.DelegatesTo;
11
12 @NonNullByDefault
13 public record OutgoingVariantPublication(
14 String variantName,
15 String slotName,
16 BuildVariant topologyVariant,
17 VariantArtifact variantArtifact,
18 VariantArtifactSlot slot,
19 ArtifactAssembly assembly,
20 NamedDomainObjectProvider<? extends Configuration> configuration) {
21 public void configureConfiguration(Action<? super Configuration> action) {
22 configuration.configure(action);
23 }
24
25 public void configureConfiguration(
26 @DelegatesTo(value = Configuration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
27 configureConfiguration(Closures.action(action));
28 }
29
30 public void configureAssembly(Action<? super ArtifactAssembly> action) {
31 action.execute(assembly);
32 }
33
34 public void configureAssembly(
35 @DelegatesTo(value = ArtifactAssembly.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
36 configureAssembly(Closures.action(action));
37 }
38 }
@@ -0,0 +1,94
1 package org.implab.gradle.common.sources;
2
3 import java.util.Optional;
4
5 import javax.inject.Inject;
6
7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 import org.gradle.api.Action;
9 import org.gradle.api.InvalidUserDataException;
10 import org.gradle.api.Named;
11 import org.gradle.api.NamedDomainObjectContainer;
12 import org.implab.gradle.common.core.lang.Closures;
13
14 import groovy.lang.Closure;
15 import groovy.lang.DelegatesTo;
16
17 @NonNullByDefault
18 public class VariantArtifact implements Named {
19 private final String name;
20 private final NamedDomainObjectContainer<VariantArtifactSlot> slots;
21 private boolean finalized;
22
23 @Inject
24 public VariantArtifact(String name, NamedDomainObjectContainer<VariantArtifactSlot> slots) {
25 this.name = normalize(name, "variant artifact name must not be null or blank");
26 this.slots = slots;
27
28 slots.all(slot -> {
29 if (finalized)
30 throw new InvalidUserDataException(
31 "Variant artifact '" + this.name + "' is finalized and cannot add slot '" + slot.getName() + "'");
32 });
33 }
34
35 @Override
36 public String getName() {
37 return name;
38 }
39
40 public NamedDomainObjectContainer<VariantArtifactSlot> getSlots() {
41 return slots;
42 }
43
44 public VariantArtifactSlot slot(String name) {
45 return slot(name, slot -> {
46 });
47 }
48
49 public VariantArtifactSlot slot(String name, Action<? super VariantArtifactSlot> configure) {
50 ensureMutable("configure slots");
51 var slot = slots.maybeCreate(normalize(name, "slot name must not be null or blank"));
52 configure.execute(slot);
53 return slot;
54 }
55
56 public VariantArtifactSlot slot(
57 String name,
58 @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
59 return slot(name, Closures.action(configure));
60 }
61
62 public Optional<VariantArtifactSlot> findSlot(String slotName) {
63 return Optional.ofNullable(slots.findByName(normalize(slotName, "slot name must not be null or blank")));
64 }
65
66 public VariantArtifactSlot requireSlot(String slotName) {
67 var normalizedSlotName = normalize(slotName, "slot name must not be null or blank");
68 return Optional.ofNullable(slots.findByName(normalizedSlotName))
69 .orElseThrow(() -> new InvalidUserDataException(
70 "Variant artifact '" + name + "' doesn't declare slot '" + normalizedSlotName + "'"));
71 }
72
73 void finalizeModel() {
74 if (finalized)
75 return;
76
77 for (var slot : slots)
78 slot.finalizeModel();
79
80 finalized = true;
81 }
82
83 static String normalize(String value, String message) {
84 return Optional.ofNullable(value)
85 .map(String::trim)
86 .filter(trimmed -> !trimmed.isEmpty())
87 .orElseThrow(() -> new InvalidUserDataException(message));
88 }
89
90 private void ensureMutable(String operation) {
91 if (finalized)
92 throw new InvalidUserDataException("Variant artifact '" + name + "' is finalized and cannot " + operation);
93 }
94 }
@@ -0,0 +1,138
1 package org.implab.gradle.common.sources;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 import javax.inject.Inject;
7
8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.gradle.api.Action;
10 import org.gradle.api.InvalidUserDataException;
11 import org.gradle.api.Named;
12 import org.implab.gradle.common.core.lang.Closures;
13
14 import groovy.lang.Closure;
15 import groovy.lang.DelegatesTo;
16
17 @NonNullByDefault
18 public class VariantArtifactSlot implements Named {
19 private final String name;
20 private final List<BindingRule> rules = new ArrayList<>();
21 private boolean finalized;
22
23 @Inject
24 public VariantArtifactSlot(String name) {
25 this.name = VariantArtifact.normalize(name, "slot name must not be null or blank");
26 }
27
28 @Override
29 public String getName() {
30 return name;
31 }
32
33 public void fromVariant(Action<? super OutputSelectionSpec> configure) {
34 addRules(BindingSelector.variant(), configure);
35 }
36
37 public void fromVariant(
38 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
39 fromVariant(Closures.action(configure));
40 }
41
42 public void fromRole(String roleName, Action<? super OutputSelectionSpec> configure) {
43 addRules(BindingSelector.role(VariantArtifact.normalize(roleName, "role name must not be null or blank")),
44 configure);
45 }
46
47 public void fromRole(
48 String roleName,
49 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
50 fromRole(roleName, Closures.action(configure));
51 }
52
53 public void fromLayer(String layerName, Action<? super OutputSelectionSpec> configure) {
54 addRules(BindingSelector.layer(VariantArtifact.normalize(layerName, "layer name must not be null or blank")),
55 configure);
56 }
57
58 public void fromLayer(
59 String layerName,
60 @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
61 fromLayer(layerName, Closures.action(configure));
62 }
63
64 List<BindingRule> bindingRules() {
65 return List.copyOf(rules);
66 }
67
68 void finalizeModel() {
69 finalized = true;
70 }
71
72 private void addRules(BindingSelector selector, Action<? super OutputSelectionSpec> configure) {
73 ensureMutable("configure sources");
74
75 var spec = new OutputSelectionSpec(selector);
76 configure.execute(spec);
77 rules.addAll(spec.rules());
78 }
79
80 private void ensureMutable(String operation) {
81 if (finalized)
82 throw new InvalidUserDataException("Variant artifact slot '" + name + "' is finalized and cannot " + operation);
83 }
84
85 public final class OutputSelectionSpec {
86 private final BindingSelector selector;
87 private final List<BindingRule> rules = new ArrayList<>();
88
89 private OutputSelectionSpec(BindingSelector selector) {
90 this.selector = selector;
91 }
92
93 public void output(String name) {
94 rules.add(new BindingRule(selector,
95 VariantArtifact.normalize(name, "output name must not be null or blank")));
96 }
97
98 public void output(String name, String... extra) {
99 output(name);
100 for (var item : extra)
101 output(item);
102 }
103
104 private List<BindingRule> rules() {
105 return List.copyOf(rules);
106 }
107 }
108
109 record BindingRule(BindingSelector selector, String outputName) {
110 boolean matches(SourceSetUsageBinding context) {
111 return switch (selector.kind()) {
112 case VARIANT -> true;
113 case ROLE -> selector.value().equals(context.roleName());
114 case LAYER -> selector.value().equals(context.layerName());
115 };
116 }
117 }
118
119 record BindingSelector(SelectorKind kind, String value) {
120 static BindingSelector variant() {
121 return new BindingSelector(SelectorKind.VARIANT, "");
122 }
123
124 static BindingSelector role(String roleName) {
125 return new BindingSelector(SelectorKind.ROLE, roleName);
126 }
127
128 static BindingSelector layer(String layerName) {
129 return new BindingSelector(SelectorKind.LAYER, layerName);
130 }
131 }
132
133 enum SelectorKind {
134 VARIANT,
135 ROLE,
136 LAYER
137 }
138 }
@@ -0,0 +1,169
1 package org.implab.gradle.common.sources;
2
3 import java.util.ArrayList;
4 import java.util.LinkedHashSet;
5 import java.util.List;
6 import java.util.Optional;
7
8 import javax.inject.Inject;
9
10 import org.eclipse.jdt.annotation.NonNullByDefault;
11 import org.gradle.api.Action;
12 import org.gradle.api.InvalidUserDataException;
13 import org.gradle.api.NamedDomainObjectContainer;
14 import org.gradle.api.model.ObjectFactory;
15 import org.implab.gradle.common.core.lang.Closures;
16
17 import groovy.lang.Closure;
18 import groovy.lang.DelegatesTo;
19
20 @NonNullByDefault
21 public abstract class VariantArtifactsExtension {
22 private final NamedDomainObjectContainer<VariantArtifact> variants;
23 private final ObjectFactory objects;
24 private final List<Action<? super OutgoingVariantPublication>> outgoingVariantActions = new ArrayList<>();
25 private final List<OutgoingVariantPublication> outgoingVariants = new ArrayList<>();
26 private boolean finalized;
27
28 @Inject
29 public VariantArtifactsExtension(ObjectFactory objects) {
30 this.objects = objects;
31 variants = objects.domainObjectContainer(VariantArtifact.class, this::newVariantArtifact);
32
33 variants.all(variant -> {
34 if (finalized)
35 throw new InvalidUserDataException(
36 "variantArtifacts model is finalized and cannot add variant '" + variant.getName() + "'");
37 });
38 }
39
40 public NamedDomainObjectContainer<VariantArtifact> getVariants() {
41 return variants;
42 }
43
44 public VariantArtifact variant(String name) {
45 return variant(name, variant -> {
46 });
47 }
48
49 public VariantArtifact variant(String name, Action<? super VariantArtifact> configure) {
50 ensureMutable("configure variants");
51 var variant = variants.maybeCreate(VariantArtifact.normalize(name, "variant name must not be null or blank"));
52 configure.execute(variant);
53 return variant;
54 }
55
56 public VariantArtifact variant(
57 String name,
58 @DelegatesTo(value = VariantArtifact.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
59 return variant(name, Closures.action(configure));
60 }
61
62 public Optional<VariantArtifact> findVariant(String variantName) {
63 return Optional
64 .ofNullable(variants.findByName(VariantArtifact.normalize(variantName, "variant name must not be null or blank")));
65 }
66
67 public VariantArtifact requireVariant(String variantName) {
68 var normalizedVariantName = VariantArtifact.normalize(variantName, "variant name must not be null or blank");
69 return findVariant(normalizedVariantName)
70 .orElseThrow(() -> new InvalidUserDataException(
71 "Variant artifacts do not declare variant '" + normalizedVariantName + "'"));
72 }
73
74 public void whenOutgoingVariant(Action<? super OutgoingVariantPublication> action) {
75 outgoingVariantActions.add(action);
76 for (var publication : outgoingVariants)
77 action.execute(publication);
78 }
79
80 public void whenOutgoingVariant(
81 @DelegatesTo(value = OutgoingVariantPublication.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
82 whenOutgoingVariant(Closures.action(action));
83 }
84
85 public boolean isFinalized() {
86 return finalized;
87 }
88
89 void finalizeModel(BuildVariantsExtension topology) {
90 if (finalized)
91 return;
92
93 validate(topology);
94
95 for (var variant : variants)
96 variant.finalizeModel();
97
98 finalized = true;
99 }
100
101 void notifyOutgoingVariant(OutgoingVariantPublication publication) {
102 outgoingVariants.add(publication);
103 for (var action : outgoingVariantActions)
104 action.execute(publication);
105 }
106
107 private VariantArtifact newVariantArtifact(String name) {
108 return objects.newInstance(VariantArtifact.class, name, objects.domainObjectContainer(VariantArtifactSlot.class));
109 }
110
111 private void validate(BuildVariantsExtension topology) {
112 var errors = new ArrayList<String>();
113
114 for (var variantArtifact : variants) {
115 var topologyVariant = topology.find(variantArtifact.getName());
116 if (topologyVariant.isEmpty()) {
117 errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
118 + variantArtifact.getName() + "'");
119 continue;
120 }
121
122 validateVariantArtifact(variantArtifact, topologyVariant.get(), errors);
123 }
124
125 if (!errors.isEmpty()) {
126 var message = new StringBuilder("Invalid variantArtifacts model:");
127 for (var error : errors)
128 message.append("\n - ").append(error);
129
130 throw new InvalidUserDataException(message.toString());
131 }
132 }
133
134 private static void validateVariantArtifact(VariantArtifact variantArtifact, BuildVariant topologyVariant, List<String> errors) {
135 var roleNames = new LinkedHashSet<String>();
136 var layerNames = new LinkedHashSet<String>();
137
138 for (var role : topologyVariant.getRoles()) {
139 roleNames.add(role.getName());
140 layerNames.addAll(role.getLayers().getOrElse(List.of()));
141 }
142
143 for (var slot : variantArtifact.getSlots()) {
144 for (var rule : slot.bindingRules()) {
145 switch (rule.selector().kind()) {
146 case VARIANT -> {
147 }
148 case ROLE -> {
149 if (!roleNames.contains(rule.selector().value())) {
150 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
151 + "' references unknown role '" + rule.selector().value() + "'");
152 }
153 }
154 case LAYER -> {
155 if (!layerNames.contains(rule.selector().value())) {
156 errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
157 + "' references unknown layer '" + rule.selector().value() + "'");
158 }
159 }
160 }
161 }
162 }
163 }
164
165 private void ensureMutable(String operation) {
166 if (finalized)
167 throw new InvalidUserDataException("variantArtifacts model is finalized and cannot " + operation);
168 }
169 }
@@ -0,0 +1,58
1 package org.implab.gradle.common.sources;
2
3 import java.util.ArrayList;
4 import java.util.LinkedHashSet;
5 import java.util.List;
6 import java.util.Set;
7
8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.gradle.api.file.ConfigurableFileCollection;
10 import org.gradle.api.file.FileCollection;
11 import org.gradle.api.model.ObjectFactory;
12
13 @NonNullByDefault
14 public final class VariantArtifactsResolver {
15 private final ObjectFactory objects;
16 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
17
18 public VariantArtifactsResolver(ObjectFactory objects) {
19 this.objects = objects;
20 }
21
22 public void recordBinding(SourceSetUsageBinding context) {
23 boundContexts.add(context);
24 }
25
26 public FileCollection files(String variantName, VariantArtifactSlot slot) {
27 var files = objects.fileCollection();
28 var boundOutputs = new LinkedHashSet<String>();
29
30 boundContexts.stream()
31 .filter(context -> variantName.equals(context.variantName()))
32 .forEach(context -> bindMatchingOutputs(files, boundOutputs, slot, context));
33
34 return files;
35 }
36
37 private static void bindMatchingOutputs(
38 ConfigurableFileCollection files,
39 Set<String> boundOutputs,
40 VariantArtifactSlot slot,
41 SourceSetUsageBinding context) {
42 slot.bindingRules().stream()
43 .filter(rule -> rule.matches(context))
44 .forEach(rule -> bindOutput(files, boundOutputs, context, rule.outputName()));
45 }
46
47 private static void bindOutput(
48 ConfigurableFileCollection files,
49 Set<String> boundOutputs,
50 SourceSetUsageBinding context,
51 String outputName) {
52 var key = context.sourceSetName() + "\u0000" + outputName;
53 if (!boundOutputs.add(key))
54 return;
55
56 files.from(context.sourceSet().map(sourceSet -> sourceSet.output(outputName)));
57 }
58 }
@@ -0,0 +1,96
1 package org.implab.gradle.common.sources;
2
3 import org.gradle.api.GradleException;
4 import org.gradle.api.Plugin;
5 import org.gradle.api.Project;
6 import org.gradle.api.artifacts.Configuration;
7 import org.gradle.api.logging.Logger;
8 import org.gradle.api.logging.Logging;
9 import org.implab.gradle.common.core.lang.Strings;
10
11 public abstract class VariantsArtifactsPlugin implements Plugin<Project> {
12 private static final Logger logger = Logging.getLogger(VariantsArtifactsPlugin.class);
13 public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";
14
15 @Override
16 public void apply(Project target) {
17 logger.debug("Registering '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
18
19 target.getPluginManager().apply(VariantsSourcesPlugin.class);
20
21 var variants = VariantsPlugin.getVariantsExtension(target);
22 var variantSources = target.getExtensions().getByType(VariantSourcesExtension.class);
23 var variantArtifacts = target.getExtensions()
24 .create(VARIANT_ARTIFACTS_EXTENSION_NAME, VariantArtifactsExtension.class);
25 var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
26 var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks());
27
28 variantSources.whenBound(variantArtifactsResolver::recordBinding);
29
30 variants.whenFinalized(model -> {
31 logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath());
32 variantArtifacts.finalizeModel(model);
33 materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies);
34 logger.debug("variantArtifacts model finalized on project '{}'", target.getPath());
35 });
36 }
37
38 public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) {
39 var extension = target.getExtensions().findByType(VariantArtifactsExtension.class);
40
41 if (extension == null) {
42 logger.error("variantArtifacts extension '{}' isn't found on project '{}'",
43 VARIANT_ARTIFACTS_EXTENSION_NAME,
44 target.getPath());
45 throw new GradleException("variantArtifacts extension isn't found");
46 }
47
48 logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());
49
50 return extension;
51 }
52
53 private static void materializeOutgoingVariants(
54 Project project,
55 BuildVariantsExtension topology,
56 VariantArtifactsExtension variantArtifacts,
57 VariantArtifactsResolver variantArtifactsResolver,
58 ArtifactAssemblyRegistry artifactAssemblies) {
59 for (var variantArtifact : variantArtifacts.getVariants()) {
60 var topologyVariant = topology.require(variantArtifact.getName());
61 for (var slot : variantArtifact.getSlots()) {
62 var assembly = artifactAssemblies.register(
63 variantArtifact.getName() + Strings.capitalize(slot.getName()),
64 "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()),
65 project.getLayout().getBuildDirectory()
66 .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
67 files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
68 var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), slot.getName(), assembly);
69
70 variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
71 variantArtifact.getName(),
72 slot.getName(),
73 topologyVariant,
74 variantArtifact,
75 slot,
76 assembly,
77 configuration));
78 }
79 }
80 }
81
82 private static org.gradle.api.NamedDomainObjectProvider<? extends Configuration> createOutgoingConfiguration(
83 Project project,
84 String variantName,
85 String slotName,
86 ArtifactAssembly assembly) {
87 var configName = variantName + Strings.capitalize(slotName) + "Elements";
88 return project.getConfigurations().consumable(configName, config -> {
89 config.setVisible(true);
90 config.setDescription("Consumable assembled artifacts for variant '" + variantName + "', slot '" + slotName + "'");
91 config.getOutgoing().artifact(assembly.getOutput().getSingleFile(), published -> {
92 published.builtBy(assembly.getOutput().getBuildDependencies());
93 });
94 });
95 }
96 }
@@ -0,0 +1,1
1 implementation-class=org.implab.gradle.common.sources.VariantsArtifactsPlugin
@@ -0,0 +1,294
1 package org.implab.gradle.common.sources;
2
3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 import static org.junit.jupiter.api.Assertions.assertTrue;
6
7 import java.io.File;
8 import java.io.IOException;
9 import java.nio.file.Files;
10 import java.nio.file.Path;
11 import java.util.List;
12
13 import org.gradle.testkit.runner.BuildResult;
14 import org.gradle.testkit.runner.GradleRunner;
15 import org.gradle.testkit.runner.TaskOutcome;
16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 import org.junit.jupiter.api.Test;
18 import org.junit.jupiter.api.io.TempDir;
19
20 class VariantsArtifactsPluginFunctionalTest {
21 private static final String SETTINGS_FILE = "settings.gradle";
22 private static final String BUILD_FILE = "build.gradle";
23 private static final String ROOT_NAME = "rootProject.name = 'variants-artifacts-fixture'\n";
24
25 @TempDir
26 Path testProjectDir;
27
28 @Test
29 void materializesVariantArtifactsAndInvokesOutgoingHooks() throws Exception {
30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 writeFile("inputs/base.js", "console.log('base')\n");
32 writeFile("inputs/amd.js", "console.log('amd')\n");
33 writeFile("inputs/mainJs.txt", "mainJs marker\n");
34 writeFile("inputs/amdJs.txt", "amdJs marker\n");
35 writeFile(BUILD_FILE, """
36 import org.gradle.api.attributes.Attribute
37
38 plugins {
39 id 'org.implab.gradle-variants-artifacts'
40 }
41
42 variants {
43 layer('mainBase')
44 layer('mainAmd')
45
46 variant('browser') {
47 role('main') {
48 layers('mainBase', 'mainAmd')
49 }
50 }
51 }
52
53 variantSources {
54 bind('mainBase') {
55 configureSourceSet {
56 declareOutputs('js')
57 }
58 }
59
60 bind('mainAmd') {
61 configureSourceSet {
62 declareOutputs('js')
63 }
64 }
65
66 whenBound { ctx ->
67 if (ctx.sourceSetName() == 'browserMainBase') {
68 ctx.configureSourceSet {
69 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
70 }
71 }
72
73 if (ctx.sourceSetName() == 'browserMainAmd') {
74 ctx.configureSourceSet {
75 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
76 }
77 }
78 }
79 }
80
81 variantArtifacts {
82 variant('browser') {
83 slot('mainJs') {
84 fromRole('main') {
85 output('js')
86 }
87 }
88
89 slot('amdJs') {
90 fromLayer('mainAmd') {
91 output('js')
92 }
93 }
94 }
95
96 whenOutgoingVariant { publication ->
97 publication.configureAssembly {
98 sources.from(layout.projectDirectory.file("inputs/${publication.slotName()}.txt"))
99 }
100
101 publication.configureConfiguration {
102 attributes.attribute(Attribute.of('test.slot', String), publication.slotName())
103 }
104 }
105 }
106
107 tasks.register('probe') {
108 dependsOn 'processBrowserMainJs', 'processBrowserAmdJs'
109
110 doLast {
111 def mainDir = layout.buildDirectory.dir('variant-artifacts/browser/mainJs').get().asFile
112 def amdDir = layout.buildDirectory.dir('variant-artifacts/browser/amdJs').get().asFile
113
114 assert new File(mainDir, 'base.js').exists()
115 assert new File(mainDir, 'amd.js').exists()
116 assert new File(mainDir, 'mainJs.txt').exists()
117
118 assert !new File(amdDir, 'base.js').exists()
119 assert new File(amdDir, 'amd.js').exists()
120 assert new File(amdDir, 'amdJs.txt').exists()
121
122 def mainElements = configurations.getByName('browserMainJsElements')
123 def attr = mainElements.attributes.getAttribute(Attribute.of('test.slot', String))
124
125 println('mainAttr=' + attr)
126 println('configurations=' + [mainElements.name, configurations.getByName('browserAmdJsElements').name].sort().join(','))
127 }
128 }
129 """);
130
131 BuildResult result = runner("probe").build();
132
133 assertTrue(result.getOutput().contains("mainAttr=mainJs"));
134 assertTrue(result.getOutput().contains("configurations=browserAmdJsElements,browserMainJsElements"));
135 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
136 }
137
138 @Test
139 void failsOnUnknownVariantReference() throws Exception {
140 assertBuildFails("""
141 plugins {
142 id 'org.implab.gradle-variants-artifacts'
143 }
144
145 variants {
146 layer('main')
147 }
148
149 variantArtifacts {
150 variant('browser') {
151 slot('mainJs') {
152 fromVariant {
153 output('js')
154 }
155 }
156 }
157 }
158 """, "Variant artifact 'browser' references unknown variant 'browser'");
159 }
160
161 @Test
162 void failsOnUnknownRoleReference() throws Exception {
163 assertBuildFails("""
164 plugins {
165 id 'org.implab.gradle-variants-artifacts'
166 }
167
168 variants {
169 layer('main')
170
171 variant('browser') {
172 role('main') {
173 layers('main')
174 }
175 }
176 }
177
178 variantArtifacts {
179 variant('browser') {
180 slot('mainJs') {
181 fromRole('test') {
182 output('js')
183 }
184 }
185 }
186 }
187 """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'");
188 }
189
190 @Test
191 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
192 assertBuildFails("""
193 plugins {
194 id 'org.implab.gradle-variants-artifacts'
195 }
196
197 variants {
198 layer('mainBase')
199 layer('extra')
200
201 variant('browser') {
202 role('main') {
203 layers('mainBase')
204 }
205 }
206 }
207
208 variantArtifacts {
209 variant('browser') {
210 slot('extraJs') {
211 fromLayer('extra') {
212 output('js')
213 }
214 }
215 }
216 }
217 """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'");
218 }
219
220 @Test
221 void failsOnLateMutationAfterFinalize() throws Exception {
222 assertBuildFails("""
223 plugins {
224 id 'org.implab.gradle-variants-artifacts'
225 }
226
227 variants {
228 layer('main')
229
230 variant('browser') {
231 role('main') {
232 layers('main')
233 }
234 }
235 }
236
237 afterEvaluate {
238 variantArtifacts.variant('late') {
239 slot('js') {
240 fromVariant {
241 output('js')
242 }
243 }
244 }
245 }
246 """, "variantArtifacts model is finalized and cannot configure variants");
247 }
248
249 private GradleRunner runner(String... arguments) {
250 return GradleRunner.create()
251 .withProjectDir(testProjectDir.toFile())
252 .withPluginClasspath(pluginClasspath())
253 .withArguments(arguments)
254 .forwardOutput();
255 }
256
257 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
258 writeFile(SETTINGS_FILE, ROOT_NAME);
259 writeFile(BUILD_FILE, buildScript);
260
261 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
262 var output = ex.getBuildResult().getOutput();
263
264 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
265 }
266
267 private static List<File> pluginClasspath() {
268 try {
269 var classesDir = Path.of(VariantsArtifactsPlugin.class
270 .getProtectionDomain()
271 .getCodeSource()
272 .getLocation()
273 .toURI());
274
275 var markerResource = VariantsArtifactsPlugin.class.getClassLoader()
276 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");
277
278 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
279
280 var markerPath = Path.of(markerResource.toURI());
281 var resourcesDir = markerPath.getParent().getParent().getParent();
282
283 return List.of(classesDir.toFile(), resourcesDir.toFile());
284 } catch (Exception e) {
285 throw new RuntimeException("Unable to build plugin classpath for test", e);
286 }
287 }
288
289 private void writeFile(String relativePath, String content) throws IOException {
290 Path path = testProjectDir.resolve(relativePath);
291 Files.createDirectories(path.getParent());
292 Files.writeString(path, content);
293 }
294 }
@@ -12,9 +12,6 import org.gradle.api.Action;
12 12 import org.gradle.api.InvalidUserDataException;
13 13 import org.gradle.api.Named;
14 14 import org.gradle.api.model.ObjectFactory;
15 import org.gradle.api.provider.Provider;
16 import org.gradle.api.provider.ProviderFactory;
17 import org.gradle.api.attributes.Attribute;
18 15
19 16 import groovy.lang.Closure;
20 17
@@ -23,18 +20,12 public abstract class BuildVariant imple
23 20 private final ObjectFactory objects;
24 21 private boolean finalized;
25 22
26 /**
27 * Variant aggregate parts.
28 */
29 private final VariantAttributes attributes;
30 23 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
31 private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();
32 24
33 25 @Inject
34 public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
26 public BuildVariant(String name, ObjectFactory objects) {
35 27 this.name = name;
36 28 this.objects = objects;
37 attributes = new VariantAttributes(providers);
38 29 }
39 30
40 31 @Override
@@ -42,32 +33,6 public abstract class BuildVariant imple
42 33 return name;
43 34 }
44 35
45 /**
46 * Generic variant attributes interpreted by adapters.
47 */
48 public VariantAttributes getAttributes() {
49 return attributes;
50 }
51
52 public void attributes(Action<? super AttributesSpec> action) {
53 ensureMutable("configure attributes");
54 action.execute(new AttributesSpec(attributes));
55 }
56
57 public void attributes(Closure<?> configure) {
58 attributes(Closures.action(configure));
59 }
60
61 public <T> void attribute(Attribute<T> key, T value) {
62 ensureMutable("set attributes");
63 attributes.attribute(key, value);
64 }
65
66 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
67 ensureMutable("set attributes");
68 attributes.attributeProvider(key, value);
69 }
70
71 36 public Collection<BuildRole> getRoles() {
72 37 return Collections.unmodifiableCollection(roles.values());
73 38 }
@@ -107,45 +72,6 public abstract class BuildVariant imple
107 72 "Variant '" + this.name + "' doesn't define role '" + name + "'"));
108 73 }
109 74
110 public Collection<BuildArtifactSlot> getArtifactSlots() {
111 return Collections.unmodifiableCollection(artifactSlots.values());
112 }
113
114 public void artifactSlots(Action<? super ArtifactSlotsSpec> action) {
115 ensureMutable("configure artifact slots");
116 action.execute(new ArtifactSlotsSpec());
117 }
118
119 public void artifactSlots(Closure<?> configure) {
120 artifactSlots(Closures.action(configure));
121 }
122
123 public BuildArtifactSlot artifactSlot(String name) {
124 return artifactSlot(name, it -> {
125 });
126 }
127
128 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
129 ensureMutable("configure artifact slots");
130 var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot);
131 configure.execute(slot);
132 return slot;
133 }
134
135 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
136 return artifactSlot(name, Closures.action(configure));
137 }
138
139 public Optional<BuildArtifactSlot> findArtifactSlot(String name) {
140 return Optional.ofNullable(artifactSlots.get(name));
141 }
142
143 public BuildArtifactSlot requireArtifactSlot(String name) {
144 return findArtifactSlot(name)
145 .orElseThrow(() -> new InvalidUserDataException(
146 "Variant '" + this.name + "' doesn't define artifact slot '" + name + "'"));
147 }
148
149 75 void finalizeModel() {
150 76 if (finalized)
151 77 return;
@@ -153,7 +79,6 public abstract class BuildVariant imple
153 79 for (var role : roles.values())
154 80 role.finalizeModel();
155 81
156 attributes.finalizeModel();
157 82 finalized = true;
158 83 }
159 84
@@ -161,10 +86,6 public abstract class BuildVariant imple
161 86 return objects.newInstance(BuildRole.class, roleName);
162 87 }
163 88
164 private BuildArtifactSlot newArtifactSlot(String slotName) {
165 return objects.newInstance(BuildArtifactSlot.class, slotName);
166 }
167
168 89 private void ensureMutable(String operation) {
169 90 if (finalized)
170 91 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
@@ -195,74 +116,4 public abstract class BuildVariant imple
195 116 return BuildVariant.this.requireRole(name);
196 117 }
197 118 }
198
199 public final class ArtifactSlotsSpec {
200 public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
201 return BuildVariant.this.artifactSlot(name, configure);
202 119 }
203
204 public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
205 return BuildVariant.this.artifactSlot(name, configure);
206 }
207
208 public BuildArtifactSlot artifactSlot(String name) {
209 return BuildVariant.this.artifactSlot(name);
210 }
211
212 public Collection<BuildArtifactSlot> getAll() {
213 return BuildVariant.this.getArtifactSlots();
214 }
215
216 public Optional<BuildArtifactSlot> find(String name) {
217 return BuildVariant.this.findArtifactSlot(name);
218 }
219
220 public BuildArtifactSlot require(String name) {
221 return BuildVariant.this.requireArtifactSlot(name);
222 }
223 }
224
225 public static final class AttributesSpec {
226 private final VariantAttributes attributes;
227
228 AttributesSpec(VariantAttributes attributes) {
229 this.attributes = attributes;
230 }
231
232 public <T> void attribute(Attribute<T> key, T value) {
233 attributes.attribute(key, value);
234 }
235
236 public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
237 attributes.attributeProvider(key, value);
238 }
239
240 public void string(String name, String value) {
241 attribute(Attribute.of(name, String.class), value);
242 }
243
244 public void string(String name, Provider<? extends String> value) {
245 attributeProvider(Attribute.of(name, String.class), value);
246 }
247
248 public void bool(String name, boolean value) {
249 attribute(Attribute.of(name, Boolean.class), value);
250 }
251
252 public void bool(String name, Provider<? extends Boolean> value) {
253 attributeProvider(Attribute.of(name, Boolean.class), value);
254 }
255
256 public void integer(String name, int value) {
257 attribute(Attribute.of(name, Integer.class), value);
258 }
259
260 public void integer(String name, Provider<? extends Integer> value) {
261 attributeProvider(Attribute.of(name, Integer.class), value);
262 }
263
264 public VariantAttributes asAttributes() {
265 return attributes;
266 }
267 }
268 }
@@ -193,11 +193,11 public abstract class BuildVariantsExten
193 193 return;
194 194 }
195 195
196 validateRoleAndArtifactNames(variant, errors);
196 validateRoleNames(variant, errors);
197 197 validateRoleMappings(variant, layersByName, errors);
198 198 }
199 199
200 private static void validateRoleAndArtifactNames(BuildVariant variant, List<String> errors) {
200 private static void validateRoleNames(BuildVariant variant, List<String> errors) {
201 201 var roleNames = new LinkedHashSet<String>();
202 202 for (var role : variant.getRoles()) {
203 203 var roleName = normalize(role.getName());
@@ -209,18 +209,6 public abstract class BuildVariantsExten
209 209 errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
210 210 }
211 211 }
212
213 var slotNames = new LinkedHashSet<String>();
214 for (var slot : variant.getArtifactSlots()) {
215 var slotName = normalize(slot.getName());
216 if (slotName == null) {
217 errors.add("Variant '" + variant.getName() + "' contains blank artifact slot name");
218 continue;
219 }
220 if (!slotNames.add(slotName)) {
221 errors.add("Variant '" + variant.getName() + "' contains duplicated artifact slot name '" + slotName + "'");
222 }
223 }
224 212 }
225 213
226 214 private static void validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
@@ -17,6 +17,7 import org.gradle.api.Action;
17 17 import org.gradle.api.InvalidUserDataException;
18 18 import org.gradle.api.NamedDomainObjectContainer;
19 19 import org.gradle.api.NamedDomainObjectProvider;
20 import org.gradle.api.file.ProjectLayout;
20 21 import org.gradle.api.model.ObjectFactory;
21 22 import org.gradle.api.logging.Logger;
22 23 import org.gradle.api.logging.Logging;
@@ -34,6 +35,7 public abstract class VariantSourcesExte
34 35 private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class);
35 36 private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}");
36 37
38 private final ProjectLayout layout;
37 39 private final NamedDomainObjectContainer<BuildLayerBinding> bindings;
38 40 private final List<Action<? super SourceSetRegistration>> registeredActions = new ArrayList<>();
39 41 private final List<Action<? super SourceSetUsageBinding>> boundActions = new ArrayList<>();
@@ -44,7 +46,8 public abstract class VariantSourcesExte
44 46 private boolean sourceSetsRegistered;
45 47
46 48 @Inject
47 public VariantSourcesExtension(ObjectFactory objects) {
49 public VariantSourcesExtension(ObjectFactory objects, ProjectLayout layout) {
50 this.layout = layout;
48 51 bindings = objects.domainObjectContainer(BuildLayerBinding.class);
49 52 }
50 53
@@ -163,7 +166,8 public abstract class VariantSourcesExte
163 166 .map(layerName -> new LayerUsage(
164 167 variant.getName(),
165 168 role.getName(),
166 normalize(layerName, "Layer name in variant '" + variant.getName() + "' and role '" + role.getName() + "' must not be null or blank")))));
169 normalize(layerName, "Layer name in variant '" + variant.getName()
170 + "' and role '" + role.getName() + "' must not be null or blank")))));
167 171 }
168 172
169 173 private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
@@ -176,7 +180,13 public abstract class VariantSourcesExte
176 180 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layerName());
177 181 var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName);
178 182 var sourceSet = sourceSetsByName.computeIfAbsent(sourceSetName,
179 name -> sources.register(name));
183 name -> {
184 var ssp = sources.register(name);
185 ssp.configure(x -> {
186 x.getSourceSetDir().set(layout.getProjectDirectory().dir("src/" + usage.layerName()));
187 });
188 return ssp;
189 });
180 190
181 191 var binding = new SourceSetUsageBinding(
182 192 usage.variantName(),
@@ -252,7 +262,8 public abstract class VariantSourcesExte
252 262 var result = sanitizeName(resolved);
253 263
254 264 if (result.isEmpty())
255 throw new InvalidUserDataException("sourceSetNamePattern '" + pattern + "' resolved to empty source set name");
265 throw new InvalidUserDataException(
266 "sourceSetNamePattern '" + pattern + "' resolved to empty source set name");
256 267
257 268 return result;
258 269 }
@@ -1,5 +1,6
1 1 /**
2 * Source model and DSL for variants/sources integration.
2 * Source model and DSL for variant topology, source bindings, artifact assembly
3 * and outgoing publication integration.
3 4 *
4 5 * <p>Naming convention for callbacks and lifecycle hooks:
5 6 * <ul>
@@ -41,32 +41,25 class VariantsPluginFunctionalTest {
41 41 }
42 42
43 43 variant('browser') {
44 attributes {
45 string('jsRuntime', 'browser')
46 string('jsModule', 'amd')
47 }
48 44 role('main') {
49 45 layers('mainBase', 'mainAmd')
50 46 }
51 artifactSlot('mainCompiled')
52 47 }
53 48 }
54 49
55 50 tasks.register('probe') {
56 51 doLast {
57 52 def browser = variants.require('browser')
58 println('attributes=' + browser.attributes.size())
59 53 println('roles=' + browser.roles.size())
60 println('slots=' + browser.artifactSlots.size())
54 println('roleLayers=' + browser.requireRole('main').layers.get().join(','))
61 55 }
62 56 }
63 57 """);
64 58
65 59 BuildResult result = runner("probe").build();
66 60
67 assertTrue(result.getOutput().contains("attributes=2"));
68 61 assertTrue(result.getOutput().contains("roles=1"));
69 assertTrue(result.getOutput().contains("slots=1"));
62 assertTrue(result.getOutput().contains("roleLayers=mainBase,mainAmd"));
70 63 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
71 64 }
72 65
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