##// 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 }
@@ -1,268 +1,119
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.common.sources;
2
2
3 import java.util.Collection;
3 import java.util.Collection;
4 import java.util.Collections;
4 import java.util.Collections;
5 import java.util.LinkedHashMap;
5 import java.util.LinkedHashMap;
6 import java.util.Optional;
6 import java.util.Optional;
7
7
8 import javax.inject.Inject;
8 import javax.inject.Inject;
9
9
10 import org.implab.gradle.common.core.lang.Closures;
10 import org.implab.gradle.common.core.lang.Closures;
11 import org.gradle.api.Action;
11 import org.gradle.api.Action;
12 import org.gradle.api.InvalidUserDataException;
12 import org.gradle.api.InvalidUserDataException;
13 import org.gradle.api.Named;
13 import org.gradle.api.Named;
14 import org.gradle.api.model.ObjectFactory;
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 import groovy.lang.Closure;
16 import groovy.lang.Closure;
20
17
21 public abstract class BuildVariant implements Named {
18 public abstract class BuildVariant implements Named {
22 private final String name;
19 private final String name;
23 private final ObjectFactory objects;
20 private final ObjectFactory objects;
24 private boolean finalized;
21 private boolean finalized;
25
22
26 /**
27 * Variant aggregate parts.
28 */
29 private final VariantAttributes attributes;
30 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
23 private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
31 private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();
32
24
33 @Inject
25 @Inject
34 public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
26 public BuildVariant(String name, ObjectFactory objects) {
35 this.name = name;
27 this.name = name;
36 this.objects = objects;
28 this.objects = objects;
37 attributes = new VariantAttributes(providers);
38 }
29 }
39
30
40 @Override
31 @Override
41 public String getName() {
32 public String getName() {
42 return name;
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 public Collection<BuildRole> getRoles() {
36 public Collection<BuildRole> getRoles() {
72 return Collections.unmodifiableCollection(roles.values());
37 return Collections.unmodifiableCollection(roles.values());
73 }
38 }
74
39
75 public void roles(Action<? super RolesSpec> action) {
40 public void roles(Action<? super RolesSpec> action) {
76 ensureMutable("configure roles");
41 ensureMutable("configure roles");
77 action.execute(new RolesSpec());
42 action.execute(new RolesSpec());
78 }
43 }
79
44
80 public void roles(Closure<?> configure) {
45 public void roles(Closure<?> configure) {
81 roles(Closures.action(configure));
46 roles(Closures.action(configure));
82 }
47 }
83
48
84 public BuildRole role(String name, Action<? super BuildRole> configure) {
49 public BuildRole role(String name, Action<? super BuildRole> configure) {
85 ensureMutable("configure roles");
50 ensureMutable("configure roles");
86 var role = roles.computeIfAbsent(name, this::newRole);
51 var role = roles.computeIfAbsent(name, this::newRole);
87 configure.execute(role);
52 configure.execute(role);
88 return role;
53 return role;
89 }
54 }
90
55
91 public BuildRole role(String name, Closure<?> configure) {
56 public BuildRole role(String name, Closure<?> configure) {
92 return role(name, Closures.action(configure));
57 return role(name, Closures.action(configure));
93 }
58 }
94
59
95 public BuildRole role(String name) {
60 public BuildRole role(String name) {
96 return role(name, r -> {
61 return role(name, r -> {
97 });
62 });
98 }
63 }
99
64
100 public Optional<BuildRole> findRole(String name) {
65 public Optional<BuildRole> findRole(String name) {
101 return Optional.ofNullable(roles.get(name));
66 return Optional.ofNullable(roles.get(name));
102 }
67 }
103
68
104 public BuildRole requireRole(String name) {
69 public BuildRole requireRole(String name) {
105 return findRole(name)
70 return findRole(name)
106 .orElseThrow(() -> new InvalidUserDataException(
71 .orElseThrow(() -> new InvalidUserDataException(
107 "Variant '" + this.name + "' doesn't define role '" + name + "'"));
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 void finalizeModel() {
75 void finalizeModel() {
150 if (finalized)
76 if (finalized)
151 return;
77 return;
152
78
153 for (var role : roles.values())
79 for (var role : roles.values())
154 role.finalizeModel();
80 role.finalizeModel();
155
81
156 attributes.finalizeModel();
157 finalized = true;
82 finalized = true;
158 }
83 }
159
84
160 private BuildRole newRole(String roleName) {
85 private BuildRole newRole(String roleName) {
161 return objects.newInstance(BuildRole.class, roleName);
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 private void ensureMutable(String operation) {
89 private void ensureMutable(String operation) {
169 if (finalized)
90 if (finalized)
170 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
91 throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation);
171 }
92 }
172
93
173 public final class RolesSpec {
94 public final class RolesSpec {
174 public BuildRole role(String name, Action<? super BuildRole> configure) {
95 public BuildRole role(String name, Action<? super BuildRole> configure) {
175 return BuildVariant.this.role(name, configure);
96 return BuildVariant.this.role(name, configure);
176 }
97 }
177
98
178 public BuildRole role(String name, Closure<?> configure) {
99 public BuildRole role(String name, Closure<?> configure) {
179 return BuildVariant.this.role(name, configure);
100 return BuildVariant.this.role(name, configure);
180 }
101 }
181
102
182 public BuildRole role(String name) {
103 public BuildRole role(String name) {
183 return BuildVariant.this.role(name);
104 return BuildVariant.this.role(name);
184 }
105 }
185
106
186 public Collection<BuildRole> getAll() {
107 public Collection<BuildRole> getAll() {
187 return BuildVariant.this.getRoles();
108 return BuildVariant.this.getRoles();
188 }
109 }
189
110
190 public Optional<BuildRole> find(String name) {
111 public Optional<BuildRole> find(String name) {
191 return BuildVariant.this.findRole(name);
112 return BuildVariant.this.findRole(name);
192 }
113 }
193
114
194 public BuildRole require(String name) {
115 public BuildRole require(String name) {
195 return BuildVariant.this.requireRole(name);
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 }
@@ -1,263 +1,251
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.common.sources;
2
2
3 import java.util.ArrayList;
3 import java.util.ArrayList;
4 import java.util.Collection;
4 import java.util.Collection;
5 import java.util.Collections;
5 import java.util.Collections;
6 import java.util.LinkedHashMap;
6 import java.util.LinkedHashMap;
7 import java.util.LinkedHashSet;
7 import java.util.LinkedHashSet;
8 import java.util.List;
8 import java.util.List;
9 import java.util.Map;
9 import java.util.Map;
10 import java.util.Optional;
10 import java.util.Optional;
11
11
12 import javax.inject.Inject;
12 import javax.inject.Inject;
13
13
14 import org.implab.gradle.common.core.lang.Closures;
14 import org.implab.gradle.common.core.lang.Closures;
15 import org.gradle.api.Action;
15 import org.gradle.api.Action;
16 import org.gradle.api.InvalidUserDataException;
16 import org.gradle.api.InvalidUserDataException;
17 import org.gradle.api.NamedDomainObjectContainer;
17 import org.gradle.api.NamedDomainObjectContainer;
18 import org.gradle.api.model.ObjectFactory;
18 import org.gradle.api.model.ObjectFactory;
19
19
20 import groovy.lang.Closure;
20 import groovy.lang.Closure;
21
21
22 public abstract class BuildVariantsExtension {
22 public abstract class BuildVariantsExtension {
23 private final NamedDomainObjectContainer<BuildLayer> layers;
23 private final NamedDomainObjectContainer<BuildLayer> layers;
24 private final NamedDomainObjectContainer<BuildVariant> variants;
24 private final NamedDomainObjectContainer<BuildVariant> variants;
25 private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
25 private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
26 private boolean finalized;
26 private boolean finalized;
27
27
28 @Inject
28 @Inject
29 public BuildVariantsExtension(ObjectFactory objects) {
29 public BuildVariantsExtension(ObjectFactory objects) {
30 layers = objects.domainObjectContainer(BuildLayer.class);
30 layers = objects.domainObjectContainer(BuildLayer.class);
31 variants = objects.domainObjectContainer(BuildVariant.class);
31 variants = objects.domainObjectContainer(BuildVariant.class);
32
32
33 layers.all(layer -> {
33 layers.all(layer -> {
34 if (finalized)
34 if (finalized)
35 throw new InvalidUserDataException(
35 throw new InvalidUserDataException(
36 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
36 "Variants model is finalized and cannot add layer '" + layer.getName() + "'");
37 });
37 });
38
38
39 variants.all(variant -> {
39 variants.all(variant -> {
40 if (finalized)
40 if (finalized)
41 throw new InvalidUserDataException(
41 throw new InvalidUserDataException(
42 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
42 "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
43 });
43 });
44 }
44 }
45
45
46 public NamedDomainObjectContainer<BuildLayer> getLayers() {
46 public NamedDomainObjectContainer<BuildLayer> getLayers() {
47 return layers;
47 return layers;
48 }
48 }
49
49
50 public NamedDomainObjectContainer<BuildVariant> getVariants() {
50 public NamedDomainObjectContainer<BuildVariant> getVariants() {
51 return variants;
51 return variants;
52 }
52 }
53
53
54 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
54 public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
55 ensureMutable("configure layers");
55 ensureMutable("configure layers");
56 action.execute(layers);
56 action.execute(layers);
57 }
57 }
58
58
59 public void layers(Closure<?> configure) {
59 public void layers(Closure<?> configure) {
60 layers(Closures.action(configure));
60 layers(Closures.action(configure));
61 }
61 }
62
62
63 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
63 public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
64 ensureMutable("configure variants");
64 ensureMutable("configure variants");
65 action.execute(variants);
65 action.execute(variants);
66 }
66 }
67
67
68 public void variants(Closure<?> configure) {
68 public void variants(Closure<?> configure) {
69 variants(Closures.action(configure));
69 variants(Closures.action(configure));
70 }
70 }
71
71
72 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
72 public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
73 ensureMutable("configure layers");
73 ensureMutable("configure layers");
74 var layer = layers.maybeCreate(name);
74 var layer = layers.maybeCreate(name);
75 configure.execute(layer);
75 configure.execute(layer);
76 return layer;
76 return layer;
77 }
77 }
78
78
79 public BuildLayer layer(String name, Closure<?> configure) {
79 public BuildLayer layer(String name, Closure<?> configure) {
80 return layer(name, Closures.action(configure));
80 return layer(name, Closures.action(configure));
81 }
81 }
82
82
83 public BuildLayer layer(String name) {
83 public BuildLayer layer(String name) {
84 return layer(name, it -> {
84 return layer(name, it -> {
85 });
85 });
86 }
86 }
87
87
88 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
88 public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
89 ensureMutable("configure variants");
89 ensureMutable("configure variants");
90 var variant = variants.maybeCreate(name);
90 var variant = variants.maybeCreate(name);
91 configure.execute(variant);
91 configure.execute(variant);
92 return variant;
92 return variant;
93 }
93 }
94
94
95 public BuildVariant variant(String name, Closure<?> configure) {
95 public BuildVariant variant(String name, Closure<?> configure) {
96 return variant(name, Closures.action(configure));
96 return variant(name, Closures.action(configure));
97 }
97 }
98
98
99 public BuildVariant variant(String name) {
99 public BuildVariant variant(String name) {
100 return variant(name, it -> {
100 return variant(name, it -> {
101 });
101 });
102 }
102 }
103
103
104 public void all(Action<? super BuildVariant> action) {
104 public void all(Action<? super BuildVariant> action) {
105 variants.all(action);
105 variants.all(action);
106 }
106 }
107
107
108 public void all(Closure<?> configure) {
108 public void all(Closure<?> configure) {
109 all(Closures.action(configure));
109 all(Closures.action(configure));
110 }
110 }
111
111
112 public Collection<BuildVariant> getAll() {
112 public Collection<BuildVariant> getAll() {
113 var all = new ArrayList<BuildVariant>();
113 var all = new ArrayList<BuildVariant>();
114 variants.forEach(all::add);
114 variants.forEach(all::add);
115 return Collections.unmodifiableList(all);
115 return Collections.unmodifiableList(all);
116 }
116 }
117
117
118 public Optional<BuildVariant> find(String name) {
118 public Optional<BuildVariant> find(String name) {
119 return Optional.ofNullable(variants.findByName(name));
119 return Optional.ofNullable(variants.findByName(name));
120 }
120 }
121
121
122 public BuildVariant require(String name) {
122 public BuildVariant require(String name) {
123 return find(name)
123 return find(name)
124 .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't defined"));
124 .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't defined"));
125 }
125 }
126
126
127 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
127 public void whenFinalized(Action<? super BuildVariantsExtension> action) {
128 if (finalized) {
128 if (finalized) {
129 action.execute(this);
129 action.execute(this);
130 return;
130 return;
131 }
131 }
132 finalizedActions.add(action);
132 finalizedActions.add(action);
133 }
133 }
134
134
135 public void whenFinalized(Closure<?> configure) {
135 public void whenFinalized(Closure<?> configure) {
136 whenFinalized(Closures.action(configure));
136 whenFinalized(Closures.action(configure));
137 }
137 }
138
138
139 public boolean isFinalized() {
139 public boolean isFinalized() {
140 return finalized;
140 return finalized;
141 }
141 }
142
142
143 public void finalizeModel() {
143 public void finalizeModel() {
144 if (finalized)
144 if (finalized)
145 return;
145 return;
146
146
147 validate();
147 validate();
148
148
149 for (var variant : variants)
149 for (var variant : variants)
150 variant.finalizeModel();
150 variant.finalizeModel();
151
151
152 finalized = true;
152 finalized = true;
153
153
154 var actions = new ArrayList<>(finalizedActions);
154 var actions = new ArrayList<>(finalizedActions);
155 finalizedActions.clear();
155 finalizedActions.clear();
156 for (var action : actions)
156 for (var action : actions)
157 action.execute(this);
157 action.execute(this);
158 }
158 }
159
159
160 public void validate() {
160 public void validate() {
161 var errors = new ArrayList<String>();
161 var errors = new ArrayList<String>();
162
162
163 var layersByName = new LinkedHashMap<String, BuildLayer>();
163 var layersByName = new LinkedHashMap<String, BuildLayer>();
164 for (var layer : layers) {
164 for (var layer : layers) {
165 var layerName = normalize(layer.getName());
165 var layerName = normalize(layer.getName());
166 if (layerName == null) {
166 if (layerName == null) {
167 errors.add("Layer name must not be blank");
167 errors.add("Layer name must not be blank");
168 continue;
168 continue;
169 }
169 }
170
170
171 var previous = layersByName.putIfAbsent(layerName, layer);
171 var previous = layersByName.putIfAbsent(layerName, layer);
172 if (previous != null) {
172 if (previous != null) {
173 errors.add("Layer '" + layerName + "' is declared more than once");
173 errors.add("Layer '" + layerName + "' is declared more than once");
174 }
174 }
175 }
175 }
176
176
177 for (var variant : variants)
177 for (var variant : variants)
178 validateVariant(variant, layersByName, errors);
178 validateVariant(variant, layersByName, errors);
179
179
180 if (!errors.isEmpty()) {
180 if (!errors.isEmpty()) {
181 var message = new StringBuilder("Invalid variants model:");
181 var message = new StringBuilder("Invalid variants model:");
182 for (var error : errors)
182 for (var error : errors)
183 message.append("\n - ").append(error);
183 message.append("\n - ").append(error);
184
184
185 throw new InvalidUserDataException(message.toString());
185 throw new InvalidUserDataException(message.toString());
186 }
186 }
187 }
187 }
188
188
189 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
189 private static void validateVariant(BuildVariant variant, Map<String, BuildLayer> layersByName, List<String> errors) {
190 var variantName = normalize(variant.getName());
190 var variantName = normalize(variant.getName());
191 if (variantName == null) {
191 if (variantName == null) {
192 errors.add("Variant name must not be blank");
192 errors.add("Variant name must not be blank");
193 return;
193 return;
194 }
194 }
195
195
196 validateRoleAndArtifactNames(variant, errors);
196 validateRoleNames(variant, errors);
197 validateRoleMappings(variant, layersByName, errors);
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 var roleNames = new LinkedHashSet<String>();
201 var roleNames = new LinkedHashSet<String>();
202 for (var role : variant.getRoles()) {
202 for (var role : variant.getRoles()) {
203 var roleName = normalize(role.getName());
203 var roleName = normalize(role.getName());
204 if (roleName == null) {
204 if (roleName == null) {
205 errors.add("Variant '" + variant.getName() + "' contains blank role name");
205 errors.add("Variant '" + variant.getName() + "' contains blank role name");
206 continue;
206 continue;
207 }
207 }
208 if (!roleNames.add(roleName)) {
208 if (!roleNames.add(roleName)) {
209 errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
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 private static void validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
214 private static void validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
227 List<String> errors) {
215 List<String> errors) {
228 for (var role : variant.getRoles()) {
216 for (var role : variant.getRoles()) {
229 var seenLayers = new LinkedHashSet<String>();
217 var seenLayers = new LinkedHashSet<String>();
230 for (var layerName : role.getLayers().getOrElse(List.of())) {
218 for (var layerName : role.getLayers().getOrElse(List.of())) {
231 var normalizedLayerName = normalize(layerName);
219 var normalizedLayerName = normalize(layerName);
232 if (normalizedLayerName == null) {
220 if (normalizedLayerName == null) {
233 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
221 errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
234 continue;
222 continue;
235 }
223 }
236
224
237 var layer = layersByName.get(normalizedLayerName);
225 var layer = layersByName.get(normalizedLayerName);
238 if (layer == null) {
226 if (layer == null) {
239 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
227 errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
240 continue;
228 continue;
241 }
229 }
242
230
243 if (!seenLayers.add(normalizedLayerName)) {
231 if (!seenLayers.add(normalizedLayerName)) {
244 errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
232 errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
245 + "' contains duplicated layer reference '" + normalizedLayerName + "'");
233 + "' contains duplicated layer reference '" + normalizedLayerName + "'");
246 }
234 }
247 }
235 }
248 }
236 }
249 }
237 }
250
238
251 private static String normalize(String value) {
239 private static String normalize(String value) {
252 if (value == null)
240 if (value == null)
253 return null;
241 return null;
254
242
255 var trimmed = value.trim();
243 var trimmed = value.trim();
256 return trimmed.isEmpty() ? null : trimmed;
244 return trimmed.isEmpty() ? null : trimmed;
257 }
245 }
258
246
259 private void ensureMutable(String operation) {
247 private void ensureMutable(String operation) {
260 if (finalized)
248 if (finalized)
261 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
249 throw new InvalidUserDataException("Variants model is finalized and cannot " + operation);
262 }
250 }
263 }
251 }
@@ -1,297 +1,308
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.common.sources;
2
2
3 import java.util.ArrayList;
3 import java.util.ArrayList;
4 import java.util.LinkedHashMap;
4 import java.util.LinkedHashMap;
5 import java.util.List;
5 import java.util.List;
6 import java.util.regex.Matcher;
6 import java.util.regex.Matcher;
7 import java.util.regex.Pattern;
7 import java.util.regex.Pattern;
8 import java.util.stream.Stream;
8 import java.util.stream.Stream;
9
9
10 import javax.inject.Inject;
10 import javax.inject.Inject;
11
11
12 import org.implab.gradle.common.core.lang.Closures;
12 import org.implab.gradle.common.core.lang.Closures;
13 import org.implab.gradle.common.core.lang.Strings;
13 import org.implab.gradle.common.core.lang.Strings;
14 import org.eclipse.jdt.annotation.NonNullByDefault;
14 import org.eclipse.jdt.annotation.NonNullByDefault;
15 import org.eclipse.jdt.annotation.Nullable;
15 import org.eclipse.jdt.annotation.Nullable;
16 import org.gradle.api.Action;
16 import org.gradle.api.Action;
17 import org.gradle.api.InvalidUserDataException;
17 import org.gradle.api.InvalidUserDataException;
18 import org.gradle.api.NamedDomainObjectContainer;
18 import org.gradle.api.NamedDomainObjectContainer;
19 import org.gradle.api.NamedDomainObjectProvider;
19 import org.gradle.api.NamedDomainObjectProvider;
20 import org.gradle.api.file.ProjectLayout;
20 import org.gradle.api.model.ObjectFactory;
21 import org.gradle.api.model.ObjectFactory;
21 import org.gradle.api.logging.Logger;
22 import org.gradle.api.logging.Logger;
22 import org.gradle.api.logging.Logging;
23 import org.gradle.api.logging.Logging;
23
24
24 import groovy.lang.Closure;
25 import groovy.lang.Closure;
25 import groovy.lang.DelegatesTo;
26 import groovy.lang.DelegatesTo;
26
27
27 import static org.implab.gradle.common.core.lang.Strings.sanitizeName;
28 import static org.implab.gradle.common.core.lang.Strings.sanitizeName;
28
29
29 /**
30 /**
30 * Adapter extension that registers source sets for variant/layer pairs.
31 * Adapter extension that registers source sets for variant/layer pairs.
31 */
32 */
32 @NonNullByDefault
33 @NonNullByDefault
33 public abstract class VariantSourcesExtension {
34 public abstract class VariantSourcesExtension {
34 private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class);
35 private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class);
35 private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}");
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 private final NamedDomainObjectContainer<BuildLayerBinding> bindings;
39 private final NamedDomainObjectContainer<BuildLayerBinding> bindings;
38 private final List<Action<? super SourceSetRegistration>> registeredActions = new ArrayList<>();
40 private final List<Action<? super SourceSetRegistration>> registeredActions = new ArrayList<>();
39 private final List<Action<? super SourceSetUsageBinding>> boundActions = new ArrayList<>();
41 private final List<Action<? super SourceSetUsageBinding>> boundActions = new ArrayList<>();
40 private final List<SourceSetRegistration> registeredContexts = new ArrayList<>();
42 private final List<SourceSetRegistration> registeredContexts = new ArrayList<>();
41 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
43 private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();
42 private final LinkedHashMap<String, NamedDomainObjectProvider<GenericSourceSet>> sourceSetsByName = new LinkedHashMap<>();
44 private final LinkedHashMap<String, NamedDomainObjectProvider<GenericSourceSet>> sourceSetsByName = new LinkedHashMap<>();
43 private final LinkedHashMap<String, String> sourceSetLayersByName = new LinkedHashMap<>();
45 private final LinkedHashMap<String, String> sourceSetLayersByName = new LinkedHashMap<>();
44 private boolean sourceSetsRegistered;
46 private boolean sourceSetsRegistered;
45
47
46 @Inject
48 @Inject
47 public VariantSourcesExtension(ObjectFactory objects) {
49 public VariantSourcesExtension(ObjectFactory objects, ProjectLayout layout) {
50 this.layout = layout;
48 bindings = objects.domainObjectContainer(BuildLayerBinding.class);
51 bindings = objects.domainObjectContainer(BuildLayerBinding.class);
49 }
52 }
50
53
51 public NamedDomainObjectContainer<BuildLayerBinding> getBindings() {
54 public NamedDomainObjectContainer<BuildLayerBinding> getBindings() {
52 return bindings;
55 return bindings;
53 }
56 }
54
57
55 public void bindings(Action<? super NamedDomainObjectContainer<BuildLayerBinding>> action) {
58 public void bindings(Action<? super NamedDomainObjectContainer<BuildLayerBinding>> action) {
56 action.execute(bindings);
59 action.execute(bindings);
57 }
60 }
58
61
59 public void bindings(
62 public void bindings(
60 @DelegatesTo(value = NamedDomainObjectContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
63 @DelegatesTo(value = NamedDomainObjectContainer.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
61 bindings(Closures.action(action));
64 bindings(Closures.action(action));
62 }
65 }
63
66
64 public BuildLayerBinding bind(String layer) {
67 public BuildLayerBinding bind(String layer) {
65 return bindings.maybeCreate(normalize(layer, "Layer name must not be null or blank"));
68 return bindings.maybeCreate(normalize(layer, "Layer name must not be null or blank"));
66 }
69 }
67
70
68 /**
71 /**
69 * Configures per-layer binding.
72 * Configures per-layer binding.
70 */
73 */
71 public BuildLayerBinding bind(String layer, Action<? super BuildLayerBinding> configure) {
74 public BuildLayerBinding bind(String layer, Action<? super BuildLayerBinding> configure) {
72 var binding = bind(layer);
75 var binding = bind(layer);
73 configure.execute(binding);
76 configure.execute(binding);
74 return binding;
77 return binding;
75 }
78 }
76
79
77 public BuildLayerBinding bind(String layer,
80 public BuildLayerBinding bind(String layer,
78 @DelegatesTo(value = BuildLayerBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
81 @DelegatesTo(value = BuildLayerBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
79 return bind(layer, Closures.action(configure));
82 return bind(layer, Closures.action(configure));
80 }
83 }
81
84
82 /**
85 /**
83 * Global callback fired for each registered source set.
86 * Global callback fired for each registered source set.
84 * Already emitted registrations are delivered immediately (replay).
87 * Already emitted registrations are delivered immediately (replay).
85 * For simple callbacks you can use delegate-only style
88 * For simple callbacks you can use delegate-only style
86 * (for example {@code whenRegistered { sourceSetName() }}).
89 * (for example {@code whenRegistered { sourceSetName() }}).
87 * For nested closures prefer explicit parameter
90 * For nested closures prefer explicit parameter
88 * ({@code whenRegistered { ctx -> ... }}).
91 * ({@code whenRegistered { ctx -> ... }}).
89 */
92 */
90 public void whenRegistered(Action<? super SourceSetRegistration> action) {
93 public void whenRegistered(Action<? super SourceSetRegistration> action) {
91 registeredActions.add(action);
94 registeredActions.add(action);
92 for (var context : registeredContexts)
95 for (var context : registeredContexts)
93 action.execute(context);
96 action.execute(context);
94 }
97 }
95
98
96 public void whenRegistered(
99 public void whenRegistered(
97 @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
100 @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
98 whenRegistered(Closures.action(action));
101 whenRegistered(Closures.action(action));
99 }
102 }
100
103
101 /**
104 /**
102 * Global callback fired for every resolved variant/role/layer usage.
105 * Global callback fired for every resolved variant/role/layer usage.
103 * Already emitted usage bindings are delivered immediately (replay).
106 * Already emitted usage bindings are delivered immediately (replay).
104 * For simple callbacks you can use delegate-only style
107 * For simple callbacks you can use delegate-only style
105 * (for example {@code whenBound { variantName() }}).
108 * (for example {@code whenBound { variantName() }}).
106 * For nested closures prefer explicit parameter
109 * For nested closures prefer explicit parameter
107 * ({@code whenBound { ctx -> ... }}).
110 * ({@code whenBound { ctx -> ... }}).
108 */
111 */
109 public void whenBound(Action<? super SourceSetUsageBinding> action) {
112 public void whenBound(Action<? super SourceSetUsageBinding> action) {
110 boundActions.add(action);
113 boundActions.add(action);
111 for (var context : boundContexts)
114 for (var context : boundContexts)
112 action.execute(context);
115 action.execute(context);
113 }
116 }
114
117
115 public void whenBound(
118 public void whenBound(
116 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
119 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
117 whenBound(Closures.action(action));
120 whenBound(Closures.action(action));
118 }
121 }
119
122
120 public void whenBound(String variantName, Action<? super SourceSetUsageBinding> action) {
123 public void whenBound(String variantName, Action<? super SourceSetUsageBinding> action) {
121 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
124 var normalizedVariantName = normalize(variantName, "variantName must not be null or blank");
122 whenBound(filterByVariant(normalizedVariantName, action));
125 whenBound(filterByVariant(normalizedVariantName, action));
123 }
126 }
124
127
125 public void whenBound(String variantName,
128 public void whenBound(String variantName,
126 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
129 @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
127 whenBound(variantName, Closures.action(action));
130 whenBound(variantName, Closures.action(action));
128 }
131 }
129
132
130 void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer<GenericSourceSet> sources) {
133 void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer<GenericSourceSet> sources) {
131 if (sourceSetsRegistered) {
134 if (sourceSetsRegistered) {
132 throw new InvalidUserDataException("variantSources source sets are already registered");
135 throw new InvalidUserDataException("variantSources source sets are already registered");
133 }
136 }
134
137
135 validateBindings(variants);
138 validateBindings(variants);
136
139
137 var usages = layerUsages(variants).toList();
140 var usages = layerUsages(variants).toList();
138 var registeredBefore = registeredContexts.size();
141 var registeredBefore = registeredContexts.size();
139 var boundBefore = boundContexts.size();
142 var boundBefore = boundContexts.size();
140
143
141 logger.debug(
144 logger.debug(
142 "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})",
145 "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})",
143 variants.getVariants().size(),
146 variants.getVariants().size(),
144 variants.getLayers().size(),
147 variants.getLayers().size(),
145 bindings.size(),
148 bindings.size(),
146 usages.size());
149 usages.size());
147
150
148 usages.forEach(usage -> registerLayerUsage(usage, sources));
151 usages.forEach(usage -> registerLayerUsage(usage, sources));
149
152
150 logger.debug(
153 logger.debug(
151 "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})",
154 "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})",
152 registeredContexts.size() - registeredBefore,
155 registeredContexts.size() - registeredBefore,
153 boundContexts.size() - boundBefore,
156 boundContexts.size() - boundBefore,
154 sourceSetsByName.size());
157 sourceSetsByName.size());
155
158
156 sourceSetsRegistered = true;
159 sourceSetsRegistered = true;
157 }
160 }
158
161
159 private Stream<LayerUsage> layerUsages(BuildVariantsExtension variants) {
162 private Stream<LayerUsage> layerUsages(BuildVariantsExtension variants) {
160 return variants.getVariants().stream()
163 return variants.getVariants().stream()
161 .flatMap(variant -> variant.getRoles().stream()
164 .flatMap(variant -> variant.getRoles().stream()
162 .flatMap(role -> role.getLayers().getOrElse(List.of()).stream()
165 .flatMap(role -> role.getLayers().getOrElse(List.of()).stream()
163 .map(layerName -> new LayerUsage(
166 .map(layerName -> new LayerUsage(
164 variant.getName(),
167 variant.getName(),
165 role.getName(),
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 private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
173 private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer<GenericSourceSet> sources) {
170 var resolvedBinding = bind(usage.layerName());
174 var resolvedBinding = bind(usage.layerName());
171 var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern();
175 var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern();
172 sourceSetNamePattern.finalizeValueOnRead();
176 sourceSetNamePattern.finalizeValueOnRead();
173
177
174 var sourceSetName = sourceSetName(usage, sourceSetNamePattern.get());
178 var sourceSetName = sourceSetName(usage, sourceSetNamePattern.get());
175
179
176 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layerName());
180 ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layerName());
177 var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName);
181 var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName);
178 var sourceSet = sourceSetsByName.computeIfAbsent(sourceSetName,
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 var binding = new SourceSetUsageBinding(
191 var binding = new SourceSetUsageBinding(
182 usage.variantName(),
192 usage.variantName(),
183 usage.roleName(),
193 usage.roleName(),
184 usage.layerName(),
194 usage.layerName(),
185 sourceSetName,
195 sourceSetName,
186 sourceSet);
196 sourceSet);
187
197
188 if (isNewSourceSet) {
198 if (isNewSourceSet) {
189 var registration = new SourceSetRegistration(
199 var registration = new SourceSetRegistration(
190 usage.layerName(),
200 usage.layerName(),
191 sourceSetName,
201 sourceSetName,
192 sourceSet);
202 sourceSet);
193 resolvedBinding.notifyRegistered(registration);
203 resolvedBinding.notifyRegistered(registration);
194 notifyRegistered(registration);
204 notifyRegistered(registration);
195 }
205 }
196
206
197 resolvedBinding.notifyBound(binding);
207 resolvedBinding.notifyBound(binding);
198 notifyBound(binding);
208 notifyBound(binding);
199 }
209 }
200
210
201 private void notifyRegistered(SourceSetRegistration registration) {
211 private void notifyRegistered(SourceSetRegistration registration) {
202 registeredContexts.add(registration);
212 registeredContexts.add(registration);
203 for (var action : registeredActions)
213 for (var action : registeredActions)
204 action.execute(registration);
214 action.execute(registration);
205 }
215 }
206
216
207 private void notifyBound(SourceSetUsageBinding binding) {
217 private void notifyBound(SourceSetUsageBinding binding) {
208 boundContexts.add(binding);
218 boundContexts.add(binding);
209 for (var action : boundActions)
219 for (var action : boundActions)
210 action.execute(binding);
220 action.execute(binding);
211 }
221 }
212
222
213 private static Action<? super SourceSetUsageBinding> filterByVariant(String variantName,
223 private static Action<? super SourceSetUsageBinding> filterByVariant(String variantName,
214 Action<? super SourceSetUsageBinding> action) {
224 Action<? super SourceSetUsageBinding> action) {
215 return binding -> {
225 return binding -> {
216 if (variantName.equals(binding.variantName()))
226 if (variantName.equals(binding.variantName()))
217 action.execute(binding);
227 action.execute(binding);
218 };
228 };
219 }
229 }
220
230
221 private void ensureSourceSetNameBoundToSingleLayer(String sourceSetName, String layerName) {
231 private void ensureSourceSetNameBoundToSingleLayer(String sourceSetName, String layerName) {
222 var existingLayer = sourceSetLayersByName.putIfAbsent(sourceSetName, layerName);
232 var existingLayer = sourceSetLayersByName.putIfAbsent(sourceSetName, layerName);
223 if (existingLayer != null && !existingLayer.equals(layerName)) {
233 if (existingLayer != null && !existingLayer.equals(layerName)) {
224 throw new InvalidUserDataException("Source set '" + sourceSetName + "' is resolved from multiple layers: '"
234 throw new InvalidUserDataException("Source set '" + sourceSetName + "' is resolved from multiple layers: '"
225 + existingLayer + "' and '" + layerName + "'");
235 + existingLayer + "' and '" + layerName + "'");
226 }
236 }
227 }
237 }
228
238
229 private void validateBindings(BuildVariantsExtension variants) {
239 private void validateBindings(BuildVariantsExtension variants) {
230 var knownLayerNames = new java.util.LinkedHashSet<String>();
240 var knownLayerNames = new java.util.LinkedHashSet<String>();
231 for (var layer : variants.getLayers())
241 for (var layer : variants.getLayers())
232 knownLayerNames.add(layer.getName());
242 knownLayerNames.add(layer.getName());
233
243
234 var errors = new ArrayList<String>();
244 var errors = new ArrayList<String>();
235 for (var binding : bindings) {
245 for (var binding : bindings) {
236 if (!knownLayerNames.contains(binding.getName())) {
246 if (!knownLayerNames.contains(binding.getName())) {
237 errors.add("Layer binding '" + binding.getName() + "' references unknown layer");
247 errors.add("Layer binding '" + binding.getName() + "' references unknown layer");
238 }
248 }
239 }
249 }
240
250
241 if (!errors.isEmpty()) {
251 if (!errors.isEmpty()) {
242 var message = new StringBuilder("Invalid variantSources model:");
252 var message = new StringBuilder("Invalid variantSources model:");
243 for (var error : errors)
253 for (var error : errors)
244 message.append("\n - ").append(error);
254 message.append("\n - ").append(error);
245 throw new InvalidUserDataException(message.toString());
255 throw new InvalidUserDataException(message.toString());
246 }
256 }
247 }
257 }
248
258
249 private static String sourceSetName(LayerUsage usage, String pattern) {
259 private static String sourceSetName(LayerUsage usage, String pattern) {
250 var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank");
260 var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank");
251 var resolved = resolveSourceSetNamePattern(normalizedPattern, usage);
261 var resolved = resolveSourceSetNamePattern(normalizedPattern, usage);
252 var result = sanitizeName(resolved);
262 var result = sanitizeName(resolved);
253
263
254 if (result.isEmpty())
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 return result;
268 return result;
258 }
269 }
259
270
260 private static String resolveSourceSetNamePattern(String pattern, LayerUsage usage) {
271 private static String resolveSourceSetNamePattern(String pattern, LayerUsage usage) {
261 var matcher = SOURCE_SET_NAME_TOKEN.matcher(pattern);
272 var matcher = SOURCE_SET_NAME_TOKEN.matcher(pattern);
262 var output = new StringBuffer();
273 var output = new StringBuffer();
263
274
264 while (matcher.find()) {
275 while (matcher.find()) {
265 var token = matcher.group(1);
276 var token = matcher.group(1);
266 matcher.appendReplacement(output, Matcher.quoteReplacement(tokenValue(token, usage)));
277 matcher.appendReplacement(output, Matcher.quoteReplacement(tokenValue(token, usage)));
267 }
278 }
268 matcher.appendTail(output);
279 matcher.appendTail(output);
269
280
270 return output.toString();
281 return output.toString();
271 }
282 }
272
283
273 private static String tokenValue(String token, LayerUsage usage) {
284 private static String tokenValue(String token, LayerUsage usage) {
274 return switch (token) {
285 return switch (token) {
275 case "variant" -> sanitizeName(usage.variantName());
286 case "variant" -> sanitizeName(usage.variantName());
276 case "variantCap" -> Strings.capitalize(sanitizeName(usage.variantName()));
287 case "variantCap" -> Strings.capitalize(sanitizeName(usage.variantName()));
277 case "role" -> sanitizeName(usage.roleName());
288 case "role" -> sanitizeName(usage.roleName());
278 case "roleCap" -> Strings.capitalize(sanitizeName(usage.roleName()));
289 case "roleCap" -> Strings.capitalize(sanitizeName(usage.roleName()));
279 case "layer" -> sanitizeName(usage.layerName());
290 case "layer" -> sanitizeName(usage.layerName());
280 case "layerCap" -> Strings.capitalize(sanitizeName(usage.layerName()));
291 case "layerCap" -> Strings.capitalize(sanitizeName(usage.layerName()));
281 default -> throw new InvalidUserDataException(
292 default -> throw new InvalidUserDataException(
282 "sourceSetNamePattern contains unsupported token '{" + token + "}'");
293 "sourceSetNamePattern contains unsupported token '{" + token + "}'");
283 };
294 };
284 }
295 }
285
296
286 private static String normalize(@Nullable String value, String errorMessage) {
297 private static String normalize(@Nullable String value, String errorMessage) {
287 if (value == null)
298 if (value == null)
288 throw new InvalidUserDataException(errorMessage);
299 throw new InvalidUserDataException(errorMessage);
289 var trimmed = value.trim();
300 var trimmed = value.trim();
290 if (trimmed.isEmpty())
301 if (trimmed.isEmpty())
291 throw new InvalidUserDataException(errorMessage);
302 throw new InvalidUserDataException(errorMessage);
292 return trimmed;
303 return trimmed;
293 }
304 }
294
305
295 private record LayerUsage(String variantName, String roleName, String layerName) {
306 private record LayerUsage(String variantName, String roleName, String layerName) {
296 }
307 }
297 }
308 }
@@ -1,15 +1,16
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 * <p>Naming convention for callbacks and lifecycle hooks:
5 * <p>Naming convention for callbacks and lifecycle hooks:
5 * <ul>
6 * <ul>
6 * <li>{@code whenXxx(...)}: register callback (supports replay where documented);</li>
7 * <li>{@code whenXxx(...)}: register callback (supports replay where documented);</li>
7 * <li>{@code configureXxx(...)}: configure model elements;</li>
8 * <li>{@code configureXxx(...)}: configure model elements;</li>
8 * <li>{@code notifyXxx(...)}: internal event dispatch helpers (not part of public DSL).</li>
9 * <li>{@code notifyXxx(...)}: internal event dispatch helpers (not part of public DSL).</li>
9 * </ul>
10 * </ul>
10 *
11 *
11 * <p>Closure-based callbacks use delegate-first resolution via
12 * <p>Closure-based callbacks use delegate-first resolution via
12 * {@code @DelegatesTo}. Delegate-only style is suitable for simple callbacks.
13 * {@code @DelegatesTo}. Delegate-only style is suitable for simple callbacks.
13 * For nested closures prefer explicit callback parameters ({@code ctx -> ...}).
14 * For nested closures prefer explicit callback parameters ({@code ctx -> ...}).
14 */
15 */
15 package org.implab.gradle.common.sources;
16 package org.implab.gradle.common.sources;
@@ -1,220 +1,213
1 package org.implab.gradle.common.sources;
1 package org.implab.gradle.common.sources;
2
2
3 import static org.junit.jupiter.api.Assertions.assertNotNull;
3 import static org.junit.jupiter.api.Assertions.assertNotNull;
4 import static org.junit.jupiter.api.Assertions.assertThrows;
4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 import static org.junit.jupiter.api.Assertions.assertTrue;
5 import static org.junit.jupiter.api.Assertions.assertTrue;
6
6
7 import java.io.File;
7 import java.io.File;
8 import java.io.IOException;
8 import java.io.IOException;
9 import java.nio.file.Files;
9 import java.nio.file.Files;
10 import java.nio.file.Path;
10 import java.nio.file.Path;
11 import java.util.List;
11 import java.util.List;
12
12
13 import org.gradle.testkit.runner.BuildResult;
13 import org.gradle.testkit.runner.BuildResult;
14 import org.gradle.testkit.runner.GradleRunner;
14 import org.gradle.testkit.runner.GradleRunner;
15 import org.gradle.testkit.runner.TaskOutcome;
15 import org.gradle.testkit.runner.TaskOutcome;
16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
16 import org.gradle.testkit.runner.UnexpectedBuildFailure;
17 import org.junit.jupiter.api.Test;
17 import org.junit.jupiter.api.Test;
18 import org.junit.jupiter.api.io.TempDir;
18 import org.junit.jupiter.api.io.TempDir;
19
19
20 class VariantsPluginFunctionalTest {
20 class VariantsPluginFunctionalTest {
21 private static final String SETTINGS_FILE = "settings.gradle";
21 private static final String SETTINGS_FILE = "settings.gradle";
22 private static final String BUILD_FILE = "build.gradle";
22 private static final String BUILD_FILE = "build.gradle";
23 private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n";
23 private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n";
24
24
25 @TempDir
25 @TempDir
26 Path testProjectDir;
26 Path testProjectDir;
27
27
28 @Test
28 @Test
29 void configuresVariantModelWithDsl() throws Exception {
29 void configuresVariantModelWithDsl() throws Exception {
30 writeFile(SETTINGS_FILE, ROOT_NAME);
30 writeFile(SETTINGS_FILE, ROOT_NAME);
31 writeFile(BUILD_FILE, """
31 writeFile(BUILD_FILE, """
32 plugins {
32 plugins {
33 id 'org.implab.gradle-variants'
33 id 'org.implab.gradle-variants'
34 }
34 }
35
35
36 variants {
36 variants {
37 layer('mainBase') {
37 layer('mainBase') {
38 }
38 }
39
39
40 layer('mainAmd') {
40 layer('mainAmd') {
41 }
41 }
42
42
43 variant('browser') {
43 variant('browser') {
44 attributes {
45 string('jsRuntime', 'browser')
46 string('jsModule', 'amd')
47 }
48 role('main') {
44 role('main') {
49 layers('mainBase', 'mainAmd')
45 layers('mainBase', 'mainAmd')
50 }
46 }
51 artifactSlot('mainCompiled')
52 }
47 }
53 }
48 }
54
49
55 tasks.register('probe') {
50 tasks.register('probe') {
56 doLast {
51 doLast {
57 def browser = variants.require('browser')
52 def browser = variants.require('browser')
58 println('attributes=' + browser.attributes.size())
59 println('roles=' + browser.roles.size())
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 BuildResult result = runner("probe").build();
59 BuildResult result = runner("probe").build();
66
60
67 assertTrue(result.getOutput().contains("attributes=2"));
68 assertTrue(result.getOutput().contains("roles=1"));
61 assertTrue(result.getOutput().contains("roles=1"));
69 assertTrue(result.getOutput().contains("slots=1"));
62 assertTrue(result.getOutput().contains("roleLayers=mainBase,mainAmd"));
70 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
63 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
71 }
64 }
72
65
73 @Test
66 @Test
74 void failsOnUnknownLayerReference() throws Exception {
67 void failsOnUnknownLayerReference() throws Exception {
75 assertBuildFails("""
68 assertBuildFails("""
76 plugins {
69 plugins {
77 id 'org.implab.gradle-variants'
70 id 'org.implab.gradle-variants'
78 }
71 }
79
72
80 variants {
73 variants {
81 layer('mainBase') {
74 layer('mainBase') {
82 }
75 }
83
76
84 variant('browser') {
77 variant('browser') {
85 role('main') {
78 role('main') {
86 layers('mainBase', 'missingLayer')
79 layers('mainBase', 'missingLayer')
87 }
80 }
88 }
81 }
89 }
82 }
90 """, "references unknown layer 'missingLayer'");
83 """, "references unknown layer 'missingLayer'");
91 }
84 }
92
85
93 @Test
86 @Test
94 void allowsUsingLayerFromDifferentVariantRole() throws Exception {
87 void allowsUsingLayerFromDifferentVariantRole() throws Exception {
95 writeFile(SETTINGS_FILE, ROOT_NAME);
88 writeFile(SETTINGS_FILE, ROOT_NAME);
96 writeFile(BUILD_FILE, """
89 writeFile(BUILD_FILE, """
97 plugins {
90 plugins {
98 id 'org.implab.gradle-variants'
91 id 'org.implab.gradle-variants'
99 }
92 }
100
93
101 variants {
94 variants {
102 layer('mainBase')
95 layer('mainBase')
103
96
104 variant('browser') {
97 variant('browser') {
105 role('test') {
98 role('test') {
106 layers('mainBase')
99 layers('mainBase')
107 }
100 }
108 }
101 }
109 }
102 }
110 """);
103 """);
111
104
112 BuildResult result = runner("help").build();
105 BuildResult result = runner("help").build();
113 assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
106 assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
114 }
107 }
115
108
116 @Test
109 @Test
117 void failsOnDuplicatedLayerReferenceInRole() throws Exception {
110 void failsOnDuplicatedLayerReferenceInRole() throws Exception {
118 assertBuildFails("""
111 assertBuildFails("""
119 plugins {
112 plugins {
120 id 'org.implab.gradle-variants'
113 id 'org.implab.gradle-variants'
121 }
114 }
122
115
123 variants {
116 variants {
124 layer('a')
117 layer('a')
125
118
126 variant('browser') {
119 variant('browser') {
127 role('main') {
120 role('main') {
128 layers('a', 'a')
121 layers('a', 'a')
129 }
122 }
130 }
123 }
131 }
124 }
132 """, "contains duplicated layer reference 'a'");
125 """, "contains duplicated layer reference 'a'");
133 }
126 }
134
127
135 @Test
128 @Test
136 void failsOnLateLayerMutationAfterFinalize() throws Exception {
129 void failsOnLateLayerMutationAfterFinalize() throws Exception {
137 assertBuildFails("""
130 assertBuildFails("""
138 plugins {
131 plugins {
139 id 'org.implab.gradle-variants'
132 id 'org.implab.gradle-variants'
140 }
133 }
141
134
142 variants {
135 variants {
143 layer('a')
136 layer('a')
144 variant('browser') {
137 variant('browser') {
145 role('main') { layers('a') }
138 role('main') { layers('a') }
146 }
139 }
147 }
140 }
148
141
149 afterEvaluate {
142 afterEvaluate {
150 variants.layer('late')
143 variants.layer('late')
151 }
144 }
152 """, "Variants model is finalized and cannot configure layers");
145 """, "Variants model is finalized and cannot configure layers");
153 }
146 }
154
147
155 @Test
148 @Test
156 void failsOnLateVariantMutationAfterFinalize() throws Exception {
149 void failsOnLateVariantMutationAfterFinalize() throws Exception {
157 assertBuildFails("""
150 assertBuildFails("""
158 plugins {
151 plugins {
159 id 'org.implab.gradle-variants'
152 id 'org.implab.gradle-variants'
160 }
153 }
161
154
162 variants {
155 variants {
163 layer('a')
156 layer('a')
164 variant('browser') {
157 variant('browser') {
165 role('main') { layers('a') }
158 role('main') { layers('a') }
166 }
159 }
167 }
160 }
168
161
169 afterEvaluate {
162 afterEvaluate {
170 variants.require('browser').role('late') { layers('a') }
163 variants.require('browser').role('late') { layers('a') }
171 }
164 }
172 """, "Variant 'browser' is finalized and cannot configure roles");
165 """, "Variant 'browser' is finalized and cannot configure roles");
173 }
166 }
174
167
175 private GradleRunner runner(String... arguments) {
168 private GradleRunner runner(String... arguments) {
176 return GradleRunner.create()
169 return GradleRunner.create()
177 .withProjectDir(testProjectDir.toFile())
170 .withProjectDir(testProjectDir.toFile())
178 .withPluginClasspath(pluginClasspath())
171 .withPluginClasspath(pluginClasspath())
179 .withArguments(arguments)
172 .withArguments(arguments)
180 .forwardOutput();
173 .forwardOutput();
181 }
174 }
182
175
183 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
176 private void assertBuildFails(String buildScript, String expectedError) throws Exception {
184 writeFile(SETTINGS_FILE, ROOT_NAME);
177 writeFile(SETTINGS_FILE, ROOT_NAME);
185 writeFile(BUILD_FILE, buildScript);
178 writeFile(BUILD_FILE, buildScript);
186
179
187 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
180 var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
188 var output = ex.getBuildResult().getOutput();
181 var output = ex.getBuildResult().getOutput();
189
182
190 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
183 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
191 }
184 }
192
185
193 private static List<File> pluginClasspath() {
186 private static List<File> pluginClasspath() {
194 try {
187 try {
195 var classesDir = Path.of(BuildVariant.class
188 var classesDir = Path.of(BuildVariant.class
196 .getProtectionDomain()
189 .getProtectionDomain()
197 .getCodeSource()
190 .getCodeSource()
198 .getLocation()
191 .getLocation()
199 .toURI());
192 .toURI());
200
193
201 var markerResource = VariantsPlugin.class.getClassLoader()
194 var markerResource = VariantsPlugin.class.getClassLoader()
202 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
195 .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties");
203
196
204 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
197 assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");
205
198
206 var markerPath = Path.of(markerResource.toURI());
199 var markerPath = Path.of(markerResource.toURI());
207 var resourcesDir = markerPath.getParent().getParent().getParent();
200 var resourcesDir = markerPath.getParent().getParent().getParent();
208
201
209 return List.of(classesDir.toFile(), resourcesDir.toFile());
202 return List.of(classesDir.toFile(), resourcesDir.toFile());
210 } catch (Exception e) {
203 } catch (Exception e) {
211 throw new RuntimeException("Unable to build plugin classpath for test", e);
204 throw new RuntimeException("Unable to build plugin classpath for test", e);
212 }
205 }
213 }
206 }
214
207
215 private void writeFile(String relativePath, String content) throws IOException {
208 private void writeFile(String relativePath, String content) throws IOException {
216 Path path = testProjectDir.resolve(relativePath);
209 Path path = testProjectDir.resolve(relativePath);
217 Files.createDirectories(path.getParent());
210 Files.createDirectories(path.getParent());
218 Files.writeString(path, content);
211 Files.writeString(path, content);
219 }
212 }
220 }
213 }
1 NO CONTENT: file was removed
NO CONTENT: file was removed
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