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