# HG changeset patch # User cin # Date 2026-04-20 09:05:01 # Node ID 3939ecb6e9a430bf1855c40573dfb0bcbc7d2866 # Parent 9db7822cd26c6115c7a7cc8704b6fe1d65ed416c Refine variant artifacts publication lifecycle- Remove assembly task access from outgoing slot publication spec- Keep whenOutgoingSlot focused on publication attributes only- Decouple materialization policy handler from artifact assemblies- Drop eager afterEvaluate outgoing configuration realization- Add reference coverage for lazy Gradle outgoing variants- Exercise primary and secondary artifact resolution without forced realization- Keep slot body customization in ArtifactAssemblySpec diff --git a/common/src/main/java/org/implab/gradle/common/sources/ArtifactAssembly.java b/common/src/main/java/org/implab/gradle/common/sources/ArtifactAssembly.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/ArtifactAssembly.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Task; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.Directory; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.TaskProvider; - -@NonNullByDefault -public final record ArtifactAssembly( - String name, - Provider outputDirectory, - TaskProvider task, - ConfigurableFileCollection output -) { - -}; - diff --git a/common/src/main/java/org/implab/gradle/common/sources/ArtifactAssemblyRegistry.java b/common/src/main/java/org/implab/gradle/common/sources/ArtifactAssemblyRegistry.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/ArtifactAssemblyRegistry.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Task; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.Directory; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.Copy; -import org.gradle.api.tasks.TaskContainer; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.language.base.plugins.LifecycleBasePlugin; - -@NonNullByDefault -public final class ArtifactAssemblyRegistry { - private final ObjectFactory objects; - private final TaskContainer tasks; - private final Map assemblies = new LinkedHashMap<>(); - - public ArtifactAssemblyRegistry(ObjectFactory objects, TaskContainer tasks) { - this.objects = objects; - this.tasks = tasks; - } - - public ArtifactAssembly register( - String name, - String taskName, - Provider outputDirectory, - Action configureSources) { - - var sources = objects.fileCollection(); - configureSources.execute(sources); - - var task = tasks.register(taskName, Copy.class, copy -> { - copy.setGroup(LifecycleBasePlugin.BUILD_GROUP); - copy.into(outputDirectory); - copy.from(sources); - }); - - return register(name, task, t -> outputDirectory); - } - - public ArtifactAssembly register( - String name, - TaskProvider task, - Function> mapOutputDirectory) { - if (assemblies.containsKey(name)) { - throw new InvalidUserDataException("Artifact assembly '" + name + "' is already registered"); - } - var outputDirectory = task.flatMap(t -> mapOutputDirectory.apply(t)); - - var output = objects.fileCollection() - .from(outputDirectory) - .builtBy(task); - - var assembly = new ArtifactAssembly(name, outputDirectory, task, output); - assemblies.put(name, assembly); - return assembly; - } - - public Optional find(String name) { - return Optional.ofNullable(assemblies.get(name)); - } - - public ArtifactAssembly require(String name) { - return find(name) - .orElseThrow(() -> new InvalidUserDataException("Artifact assembly '" + name + "' isn't registered")); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java b/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.Objects; - -import javax.inject.Inject; - -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Named; -import org.gradle.api.provider.ListProperty; - -/** - * Role binding inside a variant, points to layer names. - */ -public abstract class BuildRole implements Named { - private final String name; - private boolean finalized; - - @Inject - public BuildRole(String name) { - this.name = name; - } - - @Override - public String getName() { - return name; - } - - public abstract ListProperty getLayers(); - - /** - * Binds this role to one or more declared layers. - */ - public void layers(String layer, String... extra) { - ensureMutable("add role layers"); - - var values = new ArrayList(1 + extra.length); - - values.add(Objects.requireNonNull(layer, "Layer name is required")); - for (var item : extra) - values.add(Objects.requireNonNull(item, "Layer name is required")); - - getLayers().addAll(values); - } - - void finalizeModel() { - if (finalized) - return; - - getLayers().finalizeValue(); - getLayers().disallowChanges(); - finalized = true; - } - - private void ensureMutable(String operation) { - if (finalized) - throw new InvalidUserDataException("Role '" + name + "' is finalized and cannot " + operation); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java b/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Optional; - -import javax.inject.Inject; - -import org.implab.gradle.common.core.lang.Closures; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Named; -import org.gradle.api.model.ObjectFactory; - -import groovy.lang.Closure; - -public abstract class BuildVariant implements Named { - private final String name; - private final ObjectFactory objects; - private boolean finalized; - - private final LinkedHashMap roles = new LinkedHashMap<>(); - - @Inject - public BuildVariant(String name, ObjectFactory objects) { - this.name = name; - this.objects = objects; - } - - @Override - public String getName() { - return name; - } - - public Collection getRoles() { - return Collections.unmodifiableCollection(roles.values()); - } - - public void roles(Action action) { - ensureMutable("configure roles"); - action.execute(new RolesSpec()); - } - - public void roles(Closure configure) { - roles(Closures.action(configure)); - } - - public BuildRole role(String name, Action configure) { - ensureMutable("configure roles"); - var role = roles.computeIfAbsent(name, this::newRole); - configure.execute(role); - return role; - } - - public BuildRole role(String name, Closure configure) { - return role(name, Closures.action(configure)); - } - - public BuildRole role(String name) { - return role(name, r -> { - }); - } - - public Optional findRole(String name) { - return Optional.ofNullable(roles.get(name)); - } - - public BuildRole requireRole(String name) { - return findRole(name) - .orElseThrow(() -> new InvalidUserDataException( - "Variant '" + this.name + "' doesn't define role '" + name + "'")); - } - - void finalizeModel() { - if (finalized) - return; - - for (var role : roles.values()) - role.finalizeModel(); - - finalized = true; - } - - private BuildRole newRole(String roleName) { - return objects.newInstance(BuildRole.class, roleName); - } - - private void ensureMutable(String operation) { - if (finalized) - throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation); - } - - public final class RolesSpec { - public BuildRole role(String name, Action configure) { - return BuildVariant.this.role(name, configure); - } - - public BuildRole role(String name, Closure configure) { - return BuildVariant.this.role(name, configure); - } - - public BuildRole role(String name) { - return BuildVariant.this.role(name); - } - - public Collection getAll() { - return BuildVariant.this.getRoles(); - } - - public Optional find(String name) { - return BuildVariant.this.findRole(name); - } - - public BuildRole require(String name) { - return BuildVariant.this.requireRole(name); - } - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java b/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java +++ /dev/null @@ -1,235 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import javax.inject.Inject; - -import org.implab.gradle.common.core.lang.Closures; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.NamedDomainObjectContainer; -import org.gradle.api.model.ObjectFactory; - -import groovy.lang.Closure; - -public abstract class BuildVariantsExtension { - private final ObjectFactory objects; - private final LinkedHashMap layersByName = new LinkedHashMap<>(); - private final NamedDomainObjectContainer variants; - private final List> finalizedActions = new ArrayList<>(); - private boolean finalized; - - @Inject - public BuildVariantsExtension(ObjectFactory objects) { - this.objects = objects; - variants = objects.domainObjectContainer(BuildVariant.class); - - variants.all(variant -> { - if (finalized) - throw new InvalidUserDataException( - "Variants model is finalized and cannot add variant '" + variant.getName() + "'"); - }); - } - - public Collection getLayers() { - return Collections.unmodifiableCollection(layersByName.values()); - } - - public NamedDomainObjectContainer getVariants() { - return variants; - } - - public LayoutLayer layer(String name, Action configure) { - ensureMutable("configure layers"); - var layer = layersByName.computeIfAbsent(requireName(name, "Layer name must not be null or blank"), this::newLayer); - configure.execute(layer); - return layer; - } - - public LayoutLayer layer(String name, Closure configure) { - return layer(name, Closures.action(configure)); - } - - public LayoutLayer layer(String name) { - return layer(name, it -> { - }); - } - - public BuildVariant variant(String name, Action configure) { - ensureMutable("configure variants"); - var variant = variants.maybeCreate(name); - configure.execute(variant); - return variant; - } - - public BuildVariant variant(String name, Closure configure) { - return variant(name, Closures.action(configure)); - } - - public BuildVariant variant(String name) { - return variant(name, it -> { - }); - } - - public void all(Action action) { - variants.all(action); - } - - public void all(Closure configure) { - all(Closures.action(configure)); - } - - public Collection getAll() { - var all = new ArrayList(); - variants.forEach(all::add); - return Collections.unmodifiableList(all); - } - - public Optional findLayer(String name) { - var normalizedName = normalize(name); - return normalizedName == null ? Optional.empty() : Optional.ofNullable(layersByName.get(normalizedName)); - } - - public LayoutLayer requireLayer(String name) { - return findLayer(name) - .orElseThrow(() -> new InvalidUserDataException("Layer '" + name + "' isn't defined")); - } - - public Optional find(String name) { - return Optional.ofNullable(variants.findByName(name)); - } - - public BuildVariant require(String name) { - return find(name) - .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't defined")); - } - - public void whenFinalized(Action action) { - if (finalized) { - action.execute(this); - return; - } - finalizedActions.add(action); - } - - public void whenFinalized(Closure configure) { - whenFinalized(Closures.action(configure)); - } - - public boolean isFinalized() { - return finalized; - } - - public void finalizeModel() { - if (finalized) - return; - - validate(); - - for (var variant : variants) - variant.finalizeModel(); - - finalized = true; - - var actions = new ArrayList<>(finalizedActions); - finalizedActions.clear(); - for (var action : actions) - action.execute(this); - } - - public void validate() { - var errors = new ArrayList(); - - for (var variant : variants) - validateVariant(variant, layersByName, errors); - - if (!errors.isEmpty()) { - var message = new StringBuilder("Invalid variants model:"); - for (var error : errors) - message.append("\n - ").append(error); - - throw new InvalidUserDataException(message.toString()); - } - } - - private static void validateVariant(BuildVariant variant, Map layersByName, List errors) { - var variantName = normalize(variant.getName()); - if (variantName == null) { - errors.add("Variant name must not be blank"); - return; - } - - validateRoleNames(variant, errors); - validateRoleMappings(variant, layersByName, errors); - } - - private static void validateRoleNames(BuildVariant variant, List errors) { - var roleNames = new LinkedHashSet(); - for (var role : variant.getRoles()) { - var roleName = normalize(role.getName()); - if (roleName == null) { - errors.add("Variant '" + variant.getName() + "' contains blank role name"); - continue; - } - if (!roleNames.add(roleName)) { - errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'"); - } - } - } - - private static void validateRoleMappings(BuildVariant variant, Map layersByName, - List errors) { - for (var role : variant.getRoles()) { - var seenLayers = new LinkedHashSet(); - for (var layerName : role.getLayers().getOrElse(List.of())) { - var normalizedLayerName = normalize(layerName); - if (normalizedLayerName == null) { - errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name"); - continue; - } - - var layer = layersByName.get(normalizedLayerName); - if (layer == null) { - errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'"); - continue; - } - - if (!seenLayers.add(normalizedLayerName)) { - errors.add("Variant '" + variant.getName() + "', role '" + role.getName() - + "' contains duplicated layer reference '" + normalizedLayerName + "'"); - } - } - } - } - - private static String normalize(String value) { - if (value == null) - return null; - - var trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - - private static String requireName(String value, String errorMessage) { - var normalized = normalize(value); - if (normalized == null) - throw new InvalidUserDataException(errorMessage); - return normalized; - } - - private LayoutLayer newLayer(String name) { - return objects.newInstance(LayoutLayer.class, name); - } - - private void ensureMutable(String operation) { - if (finalized) - throw new InvalidUserDataException("Variants model is finalized and cannot " + operation); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/LayerBinding.java b/common/src/main/java/org/implab/gradle/common/sources/LayerBinding.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/LayerBinding.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.gradle.api.Action; -import org.gradle.api.NamedDomainObjectProvider; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.Property; - -final class LayerBinding implements LayerBindingSpec { - static final String DEFAULT_SOURCE_SET_NAME_PATTERN = "{variant}{layerCap}"; - - private final String name; - private final Property sourceSetNamePattern; - - private final List> sourceSetConfigureActions = new ArrayList<>(); - private final List> registeredActions = new ArrayList<>(); - private final List> boundActions = new ArrayList<>(); - private final List> registeredSourceSets = new ArrayList<>(); - private final List registeredContexts = new ArrayList<>(); - private final List boundContexts = new ArrayList<>(); - private final Set registeredSourceSetNames = new LinkedHashSet<>(); - - LayerBinding(String name, ObjectFactory objects) { - this.name = name; - sourceSetNamePattern = objects.property(String.class); - sourceSetNamePattern.convention(DEFAULT_SOURCE_SET_NAME_PATTERN); - } - - @Override - public String getName() { - return name; - } - - @Override - public Property getSourceSetNamePattern() { - return sourceSetNamePattern; - } - - @Override - public void configureSourceSet(Action configure) { - sourceSetConfigureActions.add(configure); - for (var sourceSet : registeredSourceSets) - sourceSet.configure(configure); - } - - @Override - public void whenRegistered(Action action) { - registeredActions.add(action); - for (var context : registeredContexts) - action.execute(context); - } - - @Override - public void whenBound(Action action) { - boundActions.add(action); - for (var context : boundContexts) - action.execute(context); - } - - void notifyRegistered(SourceSetRegistration registration) { - if (registeredSourceSetNames.add(registration.sourceSetName())) { - var sourceSet = registration.sourceSet(); - registeredSourceSets.add(sourceSet); - - for (var action : sourceSetConfigureActions) - sourceSet.configure(action); - } - - registeredContexts.add(registration); - for (var action : registeredActions) - action.execute(registration); - } - - void notifyBound(SourceSetUsageBinding binding) { - boundContexts.add(binding); - for (var action : boundActions) - action.execute(binding); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/LayerBindingSpec.java b/common/src/main/java/org/implab/gradle/common/sources/LayerBindingSpec.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/LayerBindingSpec.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.implab.gradle.common.core.lang.Closures; -import org.gradle.api.Action; -import org.gradle.api.Named; -import org.gradle.api.provider.Property; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * Public DSL contract for per-layer source-set policy and callbacks. - */ -public interface LayerBindingSpec extends Named { - Property getSourceSetNamePattern(); - - default void setSourceSetNamePattern(String pattern) { - getSourceSetNamePattern().set(pattern); - } - - void configureSourceSet(Action configure); - - default void configureSourceSet( - @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - configureSourceSet(Closures.action(configure)); - } - - void whenRegistered(Action action); - - default void whenRegistered( - @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - whenRegistered(Closures.action(action)); - } - - void whenBound(Action action); - - default void whenBound( - @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - whenBound(Closures.action(action)); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/LayoutLayer.java b/common/src/main/java/org/implab/gradle/common/sources/LayoutLayer.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/LayoutLayer.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.implab.gradle.common.sources; - -import javax.inject.Inject; - -import org.gradle.api.Named; - -/** - * Canonical identity model for a declared layout layer. - */ -public abstract class LayoutLayer implements Named { - private final String name; - - @Inject - public LayoutLayer(String name) { - this.name = name; - } - - @Override - public String getName() { - return name; - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/OutgoingArtifactSlotPublication.java b/common/src/main/java/org/implab/gradle/common/sources/OutgoingArtifactSlotPublication.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/OutgoingArtifactSlotPublication.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Action; -import org.gradle.api.Task; -import org.gradle.api.attributes.AttributeContainer; -import org.gradle.api.attributes.HasConfigurableAttributes; -import org.implab.gradle.common.core.lang.Closures; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -@NonNullByDefault -public final class OutgoingArtifactSlotPublication { - private final String slotName; - private final boolean primary; - private final VariantArtifactSlot slot; - private final ArtifactAssembly assembly; - private final HasConfigurableAttributes attributesCarrier; - - OutgoingArtifactSlotPublication( - String slotName, - boolean primary, - VariantArtifactSlot slot, - ArtifactAssembly assembly, - HasConfigurableAttributes attributesCarrier) { - this.slotName = slotName; - this.primary = primary; - this.slot = slot; - this.assembly = assembly; - this.attributesCarrier = attributesCarrier; - } - - public String slotName() { - return slotName; - } - - public boolean primary() { - return primary; - } - - public VariantArtifactSlot slot() { - return slot; - } - - public void configureTask(Action action) { - assembly.task().configure(action::execute); - } - - public void configureTask( - @DelegatesTo(value = Task.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - configureTask(Closures.action(action)); - } - - public void configureArtifactAttributes(Action action) { - attributesCarrier.attributes(action); - } - - public void configureArtifactAttributes( - @DelegatesTo(value = AttributeContainer.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - configureArtifactAttributes(Closures.action(action)); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/OutgoingVariantPublication.java b/common/src/main/java/org/implab/gradle/common/sources/OutgoingVariantPublication.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/OutgoingVariantPublication.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.implab.gradle.common.core.lang.Closures; -import org.gradle.api.Action; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.InvalidUserDataException; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -@NonNullByDefault -public final class OutgoingVariantPublication { - private final String variantName; - private final BuildVariant topologyVariant; - private final VariantArtifact variantArtifact; - private final Configuration configuration; - private final OutgoingArtifactSlotPublication primarySlot; - private final java.util.List slots; - - public OutgoingVariantPublication( - String variantName, - BuildVariant topologyVariant, - VariantArtifact variantArtifact, - Configuration configuration, - OutgoingArtifactSlotPublication primarySlot, - java.util.List slots) { - this.variantName = variantName; - this.topologyVariant = topologyVariant; - this.variantArtifact = variantArtifact; - this.configuration = configuration; - this.primarySlot = primarySlot; - this.slots = java.util.List.copyOf(slots); - } - - public String variantName() { - return variantName; - } - - public BuildVariant topologyVariant() { - return topologyVariant; - } - - public VariantArtifact variantArtifact() { - return variantArtifact; - } - - public Configuration configuration() { - return configuration; - } - - public OutgoingArtifactSlotPublication primarySlot() { - return primarySlot; - } - - public java.util.List slots() { - return slots; - } - - public java.util.List secondarySlots() { - return slots.stream() - .filter(slotPublication -> !slotPublication.primary()) - .toList(); - } - - public java.util.Optional findSlot(String slotName) { - var normalizedSlotName = VariantArtifact.normalize(slotName, "slot name must not be null or blank"); - return slots.stream() - .filter(slotPublication -> normalizedSlotName.equals(slotPublication.slotName())) - .findFirst(); - } - - public OutgoingArtifactSlotPublication requireSlot(String slotName) { - var normalizedSlotName = VariantArtifact.normalize(slotName, "slot name must not be null or blank"); - return findSlot(normalizedSlotName) - .orElseThrow(() -> new InvalidUserDataException( - "Outgoing publication for variant '" + variantName + "' doesn't declare slot '" - + normalizedSlotName + "'")); - } - - public void configureConfiguration(Action action) { - action.execute(configuration); - } - - public void configureConfiguration( - @DelegatesTo(value = Configuration.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - configureConfiguration(Closures.action(action)); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/SourceSetRegistration.java b/common/src/main/java/org/implab/gradle/common/sources/SourceSetRegistration.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/SourceSetRegistration.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.implab.gradle.common.core.lang.Closures; -import org.gradle.api.Action; -import org.gradle.api.NamedDomainObjectProvider; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * Immutable payload for a newly registered {@link GenericSourceSet}. - * - *

Used as callback payload for - * {@link VariantSourcesExtension#whenRegistered(org.gradle.api.Action)} and - * {@link LayerBindingSpec#whenRegistered(org.gradle.api.Action)}. - * - * @param layerName normalized layer name that owns the registration - * @param sourceSetName source-set name registered in the container - * @param sourceSet provider of the registered source set (realized later by Gradle on demand) - */ -public record SourceSetRegistration( - String layerName, - String sourceSetName, - NamedDomainObjectProvider sourceSet) { - public void configureSourceSet(Action action) { - sourceSet.configure(action); - } - - public void configureSourceSet( - @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - configureSourceSet(Closures.action(action)); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/SourceSetUsageBinding.java b/common/src/main/java/org/implab/gradle/common/sources/SourceSetUsageBinding.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/SourceSetUsageBinding.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.implab.gradle.common.core.lang.Closures; -import org.gradle.api.Action; -import org.gradle.api.NamedDomainObjectProvider; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * Immutable payload for a resolved variant/role/layer usage bound to a source set. - * - *

Used as callback payload for - * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)} and - * {@link LayerBindingSpec#whenBound(org.gradle.api.Action)}. - * - * @param variantName variant name from the build-variants model - * @param roleName role name inside the resolved variant - * @param layerName normalized layer name used to resolve the source set - * @param sourceSetName source-set name registered in the container - * @param sourceSet provider of the registered source set (realized later by Gradle on demand) - */ -public record SourceSetUsageBinding( - String variantName, - String roleName, - String layerName, - String sourceSetName, - NamedDomainObjectProvider sourceSet) { - public void configureSourceSet(Action action) { - sourceSet.configure(action); - } - - public void configureSourceSet( - @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - configureSourceSet(Closures.action(action)); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifact.java b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifact.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifact.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.Optional; - -import javax.inject.Inject; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Named; -import org.gradle.api.NamedDomainObjectContainer; -import org.implab.gradle.common.core.lang.Closures; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * Artifact model for one topology variant declared in - * {@link VariantArtifactsExtension}. - * - *

A {@code VariantArtifact} groups one or more - * {@link VariantArtifactSlot artifact representation slots}. The primary slot - * becomes the main artifact of {@code Elements}; remaining slots are - * published as secondary outgoing variants by {@link VariantArtifactsPlugin}. - */ -@NonNullByDefault -public class VariantArtifact implements Named { - private final String name; - private final NamedDomainObjectContainer slots; - private String primarySlotName; - private boolean finalized; - - @Inject - public VariantArtifact(String name, NamedDomainObjectContainer slots) { - this.name = normalize(name, "variant artifact name must not be null or blank"); - this.slots = slots; - - slots.all(slot -> { - if (finalized) - throw new InvalidUserDataException( - "Variant artifact '" + this.name + "' is finalized and cannot add slot '" + slot.getName() + "'"); - }); - } - - @Override - public String getName() { - return name; - } - - public NamedDomainObjectContainer getSlots() { - return slots; - } - - public VariantArtifactSlot slot(String name) { - return slot(name, slot -> { - }); - } - - public VariantArtifactSlot slot(String name, Action configure) { - ensureMutable("configure slots"); - var slot = slots.maybeCreate(normalize(name, "slot name must not be null or blank")); - configure.execute(slot); - return slot; - } - - public VariantArtifactSlot slot( - String name, - @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - return slot(name, Closures.action(configure)); - } - - public Optional findSlot(String slotName) { - return Optional.ofNullable(slots.findByName(normalize(slotName, "slot name must not be null or blank"))); - } - - public void primarySlot(String slotName) { - ensureMutable("configure primary slot"); - primarySlotName = normalize(slotName, "primary slot name must not be null or blank"); - } - - public VariantArtifactSlot primarySlot(String slotName, Action configure) { - ensureMutable("configure primary slot"); - var slot = slot(slotName, configure); - primarySlot(slot.getName()); - return slot; - } - - public VariantArtifactSlot primarySlot( - String slotName, - @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - return primarySlot(slotName, Closures.action(configure)); - } - - public Optional findPrimarySlotName() { - return Optional.ofNullable(primarySlotName) - .or(() -> slots.getNames().size() == 1 ? Optional.of(slots.iterator().next().getName()) : Optional.empty()); - } - - public String requirePrimarySlotName() { - return findPrimarySlotName() - .orElseThrow(() -> new InvalidUserDataException( - "Variant artifact '" + name + "' must declare primary slot because it has multiple slots")); - } - - public Optional findPrimarySlot() { - return findPrimarySlotName().flatMap(this::findSlot); - } - - public VariantArtifactSlot requirePrimarySlot() { - var resolvedPrimarySlotName = requirePrimarySlotName(); - return findSlot(resolvedPrimarySlotName) - .orElseThrow(() -> new InvalidUserDataException( - "Variant artifact '" + name + "' declares unknown primary slot '" + resolvedPrimarySlotName + "'")); - } - - public VariantArtifactSlot requireSlot(String slotName) { - var normalizedSlotName = normalize(slotName, "slot name must not be null or blank"); - return Optional.ofNullable(slots.findByName(normalizedSlotName)) - .orElseThrow(() -> new InvalidUserDataException( - "Variant artifact '" + name + "' doesn't declare slot '" + normalizedSlotName + "'")); - } - - void finalizeModel() { - if (finalized) - return; - - for (var slot : slots) - slot.finalizeModel(); - - finalized = true; - } - - static String normalize(String value, String message) { - return Optional.ofNullable(value) - .map(String::trim) - .filter(trimmed -> !trimmed.isEmpty()) - .orElseThrow(() -> new InvalidUserDataException(message)); - } - - private void ensureMutable(String operation) { - if (finalized) - throw new InvalidUserDataException("Variant artifact '" + name + "' is finalized and cannot " + operation); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactSlot.java b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactSlot.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactSlot.java +++ /dev/null @@ -1,242 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.Stream; - -import javax.inject.Inject; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Named; -import org.implab.gradle.common.core.lang.Closures; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * One artifact representation slot inside {@link VariantArtifact}. - * - *

- * The DSL exposed by this type is topology-aware sugar over an internal - * contribution model: - *

    - *
  • {@link #from(Object)} adds one direct contribution that does not depend - * on {@link VariantSourcesExtension} bindings;
  • - *
  • {@link #fromVariant(Action)}, {@link #fromRole(String, Action)} and - * {@link #fromLayer(String, Action)} define where a contribution is active in - * the variant/role/layer topology;
  • - *
  • {@link OutputSelectionSpec#output(String)} defines which named output of - * the matched {@link GenericSourceSet} should be added to the slot.
  • - *
- * - *

- * Internally the slot stores contribution resolvers rather than raw output - * names. Each contribution can later materialize itself against the - * variant-specific source-set bindings and return: - *

    - *
  • a file notation object suitable for {@code files.from(...)}
  • - *
  • a {@link BindingKey} used to deduplicate repeated logical inputs during - * materialization
  • - *
- * - *

- * Validation is intentionally separated from materialization: the slot keeps - * topology references in {@link #referencedRoleNames()} and - * {@link #referencedLayerNames()}, while the actual contribution pipeline is - * exposed through {@link #bindings()}. - */ -@NonNullByDefault -public class VariantArtifactSlot implements Named { - private final String name; - private final List bindings = new ArrayList<>(); - private final Set referencedRoleNames = new LinkedHashSet<>(); - private final Set referencedLayerNames = new LinkedHashSet<>(); - private boolean finalized; - - @Inject - public VariantArtifactSlot(String name) { - this.name = VariantArtifact.normalize(name, "slot name must not be null or blank"); - } - - @Override - public String getName() { - return name; - } - - public void fromVariant(Action configure) { - addContributions(context -> true, configure); - } - - public void fromVariant( - @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - fromVariant(Closures.action(configure)); - } - - public void fromRole(String roleName, Action configure) { - var normalizedRoleName = VariantArtifact.normalize(roleName, "role name must not be null or blank"); - addContributions(context -> context.roleName().equals(normalizedRoleName), configure); - referencedRoleNames.add(normalizedRoleName); - } - - public void fromRole( - String roleName, - @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - fromRole(roleName, Closures.action(configure)); - } - - public void fromLayer(String layerName, Action configure) { - var normalizedLayerName = VariantArtifact.normalize(layerName, "layer name must not be null or blank"); - addContributions(context -> context.layerName().equals(normalizedLayerName), configure); - referencedLayerNames.add(normalizedLayerName); - } - - public void fromLayer( - String layerName, - @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - fromLayer(layerName, Closures.action(configure)); - } - - /** - * Adds one direct slot contribution. - * - *

The supplied object is forwarded as-is to {@code files.from(...)} - * during slot materialization and does not depend on - * {@link VariantSourcesExtension#whenBound(Action)} callbacks. - */ - public void from(Object files) { - ensureMutable("configure sources"); - - if (files == null) - throw new InvalidUserDataException("slot source must not be null"); - - var key = BindingKey.newUniqueKey("direct slot input for '" + name + "'"); - bindings.add((contexts, consumer) -> consumer.accept(new ResolvedBinding(key, files))); - } - - List bindings() { - return List.copyOf(bindings); - } - - void acceptBindings(Consumer consumer) { - bindings.forEach(consumer); - } - - Set referencedRoleNames() { - return Set.copyOf(referencedRoleNames); - } - - Set referencedLayerNames() { - return Set.copyOf(referencedLayerNames); - } - - void finalizeModel() { - finalized = true; - } - - private void addContributions( - Predicate selector, - Action configure) { - ensureMutable("configure sources"); - - var spec = new OutputSelectionSpec(selector); - configure.execute(spec); - spec.accept(bindings::add); - } - - private void ensureMutable(String operation) { - if (finalized) - throw new InvalidUserDataException( - "Variant artifact slot '" + name + "' is finalized and cannot " + operation); - } - - /** - * Local DSL buffer for one {@code fromVariant/fromRole/fromLayer} block. - * - *

- * The spec accumulates contributions locally and flushes them to the - * owning slot only after the outer configure block completes successfully. - */ - public final class OutputSelectionSpec { - private final Predicate selector; - private final List bindings = new ArrayList<>(); - - private OutputSelectionSpec(Predicate selector) { - this.selector = selector; - } - - public void output(String name) { - var outputName = VariantArtifact.normalize(name, "output name must not be null or blank"); - bindings.add((contexts, consumer) -> contexts.stream() - .filter(selector) - .map(context -> resolveOutput(context, outputName)) - .forEach(consumer)); - } - - public void output(String name, String... extra) { - Stream.concat(Stream.of(name), Stream.of(extra)) - .forEach(this::output); - } - - private ResolvedBinding resolveOutput(SourceSetUsageBinding context, String outputName) { - var key = new SourceSetOutputKey(context.sourceSetName(), outputName); - var files = context.sourceSet().map(sourceSet -> sourceSet.output(outputName)); - return new ResolvedBinding(key, files); - } - - void accept(Consumer consumer) { - bindings.forEach(consumer); - } - } - - @FunctionalInterface - interface BindingResolver { - void resolve(Collection contexts, Consumer consumer); - } - - /** - * Materialized slot contribution for one concrete source-set binding. - */ - record ResolvedBinding(BindingKey key, Object files) { - } - - /** - * Marker key for deduplicating logical slot inputs during materialization. - * - *

- * Semantic keys such as {@link SourceSetOutputKey} collapse repeated - * references to the same logical output. Identity keys created via - * {@link #newUniqueKey()} or {@link #newUniqueKey(String)} can be used by contributions - * that must flow through the same pipeline but should never be merged. - */ - interface BindingKey { - static BindingKey newUniqueKey(String hint) { - return new BindingKey() { - @Override - public String toString() { - return hint; - } - }; - } - - static BindingKey newUniqueKey() { - return newUniqueKey("unnamed"); - } - } - - /** - * Stable dedupe key for one named output of one resolved source set. - */ - record SourceSetOutputKey(String sourceSetName, String outputName) implements BindingKey { - @Override - public String toString() { - return "sourceSet '" + sourceSetName + "' output '" + outputName + "'"; - } - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsExtension.java b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsExtension.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsExtension.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.inject.Inject; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.NamedDomainObjectContainer; -import org.gradle.api.model.ObjectFactory; -import org.implab.gradle.common.core.lang.Closures; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * Root DSL and lifecycle holder for the {@code variantArtifacts} model. - * - *

This extension sits on top of the build topology defined by - * {@link BuildVariantsExtension} and the source-set materialization performed by - * {@link VariantSourcesExtension}: - *

    - *
  • {@link #variant(String, Action)} declares one outgoing artifact model per - * topology variant;
  • - *
  • each {@link VariantArtifact} declares one or more - * {@link VariantArtifactSlot slots};
  • - *
  • after topology finalization this extension validates the artifact model, - * freezes it and later receives replayable outgoing-publication callbacks via - * {@link #whenOutgoingVariant(Action)}.
  • - *
- */ -@NonNullByDefault -public abstract class VariantArtifactsExtension { - private final NamedDomainObjectContainer variants; - private final ObjectFactory objects; - private final List> outgoingVariantActions = new ArrayList<>(); - private final List outgoingVariants = new ArrayList<>(); - private boolean finalized; - - @Inject - public VariantArtifactsExtension(ObjectFactory objects) { - this.objects = objects; - variants = objects.domainObjectContainer(VariantArtifact.class, this::newVariantArtifact); - - variants.all(variant -> { - if (finalized) - throw new InvalidUserDataException( - "variantArtifacts model is finalized and cannot add variant '" + variant.getName() + "'"); - }); - } - - public NamedDomainObjectContainer getVariants() { - return variants; - } - - public VariantArtifact variant(String name) { - return variant(name, variant -> { - }); - } - - public VariantArtifact variant(String name, Action configure) { - ensureMutable("configure variants"); - var variant = variants.maybeCreate(VariantArtifact.normalize(name, "variant name must not be null or blank")); - configure.execute(variant); - return variant; - } - - public VariantArtifact variant( - String name, - @DelegatesTo(value = VariantArtifact.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - return variant(name, Closures.action(configure)); - } - - public Optional findVariant(String variantName) { - return Optional - .ofNullable(variants.findByName(VariantArtifact.normalize(variantName, "variant name must not be null or blank"))); - } - - public VariantArtifact requireVariant(String variantName) { - var normalizedVariantName = VariantArtifact.normalize(variantName, "variant name must not be null or blank"); - return findVariant(normalizedVariantName) - .orElseThrow(() -> new InvalidUserDataException( - "Variant artifacts do not declare variant '" + normalizedVariantName + "'")); - } - - public void whenOutgoingVariant(Action action) { - outgoingVariantActions.add(action); - for (var publication : outgoingVariants) - action.execute(publication); - } - - public void whenOutgoingVariant( - @DelegatesTo(value = OutgoingVariantPublication.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - whenOutgoingVariant(Closures.action(action)); - } - - public boolean isFinalized() { - return finalized; - } - - void finalizeModel(BuildVariantsExtension topology) { - if (finalized) - return; - - validate(topology); - - for (var variant : variants) - variant.finalizeModel(); - - finalized = true; - } - - void notifyOutgoingVariant(OutgoingVariantPublication publication) { - outgoingVariants.add(publication); - for (var action : outgoingVariantActions) - action.execute(publication); - } - - private VariantArtifact newVariantArtifact(String name) { - return objects.newInstance(VariantArtifact.class, name, objects.domainObjectContainer(VariantArtifactSlot.class)); - } - - private void validate(BuildVariantsExtension topology) { - var errors = new ArrayList(); - - for (var variantArtifact : variants) - validateVariantArtifact(topology, variantArtifact, errors); - - throwIfInvalid(errors); - } - - private static void validateVariantArtifact( - BuildVariantsExtension topology, - VariantArtifact variantArtifact, - List errors) { - var topologyVariant = topology.find(variantArtifact.getName()); - if (topologyVariant.isEmpty()) { - errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '" - + variantArtifact.getName() + "'"); - return; - } - - var topologyScope = TopologyScope.from(topologyVariant.get()); - validateTopologyReferences(variantArtifact, topologyScope, errors); - validatePrimarySlot(variantArtifact, errors); - } - - private static void validateTopologyReferences( - VariantArtifact variantArtifact, - TopologyScope topologyScope, - List errors) { - for (var slot : variantArtifact.getSlots()) { - validateSlotReferences(variantArtifact, slot, "role", slot.referencedRoleNames(), topologyScope.roleNames(), errors); - validateSlotReferences(variantArtifact, slot, "layer", slot.referencedLayerNames(), topologyScope.layerNames(), errors); - } - } - - private static void validatePrimarySlot(VariantArtifact variantArtifact, List errors) { - if (variantArtifact.getSlots().isEmpty()) - return; - - if (variantArtifact.findPrimarySlotName().isEmpty()) { - errors.add("Variant artifact '" + variantArtifact.getName() - + "' must declare primary slot because it has multiple slots"); - return; - } - - var primarySlotName = variantArtifact.requirePrimarySlotName(); - if (variantArtifact.findSlot(primarySlotName).isEmpty()) { - errors.add("Variant artifact '" + variantArtifact.getName() - + "' declares unknown primary slot '" + primarySlotName + "'"); - } - } - - private static void validateSlotReferences( - VariantArtifact variantArtifact, - VariantArtifactSlot slot, - String referenceKind, - Set referencedNames, - Set knownNames, - List errors) { - for (var referencedName : referencedNames) { - if (!knownNames.contains(referencedName)) { - errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName() - + "' references unknown " + referenceKind + " '" + referencedName + "'"); - } - } - } - - private static void throwIfInvalid(List errors) { - if (errors.isEmpty()) - return; - - var message = new StringBuilder("Invalid variantArtifacts model:"); - for (var error : errors) - message.append("\n - ").append(error); - - throw new InvalidUserDataException(message.toString()); - } - - private record TopologyScope(Set roleNames, Set layerNames) { - private static TopologyScope from(BuildVariant topologyVariant) { - var roleNames = new LinkedHashSet(); - var layerNames = new LinkedHashSet(); - - for (var role : topologyVariant.getRoles()) { - roleNames.add(role.getName()); - layerNames.addAll(role.getLayers().getOrElse(List.of())); - } - - return new TopologyScope(Set.copyOf(roleNames), Set.copyOf(layerNames)); - } - } - - private void ensureMutable(String operation) { - if (finalized) - throw new InvalidUserDataException("variantArtifacts model is finalized and cannot " + operation); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsPlugin.java b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsPlugin.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsPlugin.java +++ /dev/null @@ -1,184 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.ArrayList; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.gradle.api.GradleException; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.ConfigurationPublications; -import org.gradle.api.artifacts.ConfigurationVariant; -import org.gradle.api.logging.Logger; -import org.gradle.api.logging.Logging; -import org.implab.gradle.common.core.lang.Strings; - -public abstract class VariantArtifactsPlugin implements Plugin { - private static final Logger logger = Logging.getLogger(VariantArtifactsPlugin.class); - public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts"; - - @Override - public void apply(Project target) { - logger.debug("Registering '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath()); - - target.getPluginManager().apply(VariantsSourcesPlugin.class); - - var variants = VariantsPlugin.getVariantsExtension(target); - var variantSources = target.getExtensions().getByType(VariantSourcesExtension.class); - var variantArtifacts = target.getExtensions() - .create(VARIANT_ARTIFACTS_EXTENSION_NAME, VariantArtifactsExtension.class); - var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects()); - var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks()); - - // Bind variant artifacts resolution to variant sources registration, so that - // artifact resolution can be performed - variantSources.whenBound(variantArtifactsResolver::recordBinding); - - variants.whenFinalized(model -> { - logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath()); - variantArtifacts.finalizeModel(model); - materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies); - logger.debug("variantArtifacts model finalized on project '{}'", target.getPath()); - }); - } - - public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) { - var extension = target.getExtensions().findByType(VariantArtifactsExtension.class); - - if (extension == null) { - logger.error("variantArtifacts extension '{}' isn't found on project '{}'", - VARIANT_ARTIFACTS_EXTENSION_NAME, - target.getPath()); - throw new GradleException("variantArtifacts extension isn't found"); - } - - logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath()); - - return extension; - } - - private static void materializeOutgoingVariants( - Project project, - BuildVariantsExtension topology, - VariantArtifactsExtension variantArtifacts, - VariantArtifactsResolver variantArtifactsResolver, - ArtifactAssemblyRegistry artifactAssemblies) { - variantArtifacts.getVariants().stream() - .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty()) - .forEach(variantArtifact -> materializeOutgoingVariant( - project, - topology.require(variantArtifact.getName()), - variantArtifact, - variantArtifactsResolver, - artifactAssemblies, - variantArtifacts)); - } - - private static void materializeOutgoingVariant( - Project project, - BuildVariant topologyVariant, - VariantArtifact variantArtifact, - VariantArtifactsResolver variantArtifactsResolver, - ArtifactAssemblyRegistry artifactAssemblies, - VariantArtifactsExtension variantArtifacts) { - var assemblies = variantArtifact.getSlots().stream() - .collect(Collectors.toMap( - VariantArtifactSlot::getName, - slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, - slot), - (left, right) -> left, - LinkedHashMap::new)); - - var primarySlot = variantArtifact.requirePrimarySlot(); - var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), primarySlot.getName()); - var primaryAssembly = assemblies.get(primarySlot.getName()); - publishPrimaryArtifact(configuration, primaryAssembly); - var primaryPublication = new OutgoingArtifactSlotPublication( - primarySlot.getName(), - true, - primarySlot, - primaryAssembly, - configuration); - var secondarySlots = variantArtifact.getSlots().stream() - .filter(slot -> !slot.getName().equals(primarySlot.getName())) - .map(slot -> new SecondarySlot(slot, assemblies.get(slot.getName()))) - .toList(); - var secondaryPublications = new ArrayList(secondarySlots.size()); - secondarySlots.forEach(secondarySlot -> { - var secondaryVariant = configuration.getOutgoing().getVariants().create(secondarySlot.slot().getName()); - publishSecondaryArtifact(secondaryVariant, secondarySlot.assembly()); - secondaryPublications.add(new OutgoingArtifactSlotPublication( - secondarySlot.slot().getName(), - false, - secondarySlot.slot(), - secondarySlot.assembly(), - secondaryVariant)); - }); - - var slotPublications = Stream.concat( - Stream.of(primaryPublication), - secondaryPublications.stream()) - .toList(); - - variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication( - variantArtifact.getName(), - topologyVariant, - variantArtifact, - configuration, - primaryPublication, - slotPublications)); - } - - private static ArtifactAssembly registerAssembly( - Project project, - VariantArtifactsResolver variantArtifactsResolver, - ArtifactAssemblyRegistry artifactAssemblies, - VariantArtifact variantArtifact, - VariantArtifactSlot slot) { - String assemblyName = variantArtifact.getName() + Strings.capitalize(slot.getName()); - return artifactAssemblies.register( - assemblyName, - "process" + Strings.capitalize(assemblyName), - project.getLayout().getBuildDirectory() - .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()), - files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot))); - } - - private static Configuration createOutgoingConfiguration( - Project project, - String variantName, - String primarySlotName) { - var configName = variantName + "Elements"; - return project.getConfigurations().consumable(configName, config -> { - config.setVisible(true); - config.setDescription("Consumable assembled artifacts for variant '" + variantName - + "' with primary slot '" + primarySlotName + "'"); - }).get(); - } - - private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) { - publishArtifact(configuration.getOutgoing(), assembly); - } - - private static void publishSecondaryArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) { - publishArtifact(variant, assembly); - } - - private static void publishArtifact(ConfigurationPublications outgoing, ArtifactAssembly assembly) { - outgoing.artifact(assembly.output().getSingleFile(), published -> { - published.builtBy(assembly.output().getBuildDependencies()); - }); - } - - private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) { - variant.artifact(assembly.output().getSingleFile(), published -> { - published.builtBy(assembly.output().getBuildDependencies()); - }); - } - - private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) { - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsResolver.java b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsResolver.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsResolver.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.FileCollection; -import org.gradle.api.model.ObjectFactory; -import org.implab.gradle.common.sources.VariantArtifactSlot.BindingKey; - -/** - * Resolves artifact-slot inputs from already bound variant source-set usages. - * - *

This type is the bridge between two models: - *

    - *
  • {@link VariantSourcesExtension}, which emits resolved - * {@link SourceSetUsageBinding variant/role/layer -> source-set} bindings;
  • - *
  • {@link VariantArtifactSlot}, which exposes a DSL over slot contributions - * and stores the resulting {@link VariantArtifactSlot.BindingResolver - * contribution resolvers}.
  • - *
- * - *

The resolver records each emitted {@link SourceSetUsageBinding} and later - * materializes a {@link FileCollection} for one concrete variant/slot pair. - * For each variant/slot pair it asks the slot to materialize its contributions, - * passes in the resolved source-set bindings for that variant, and - * deduplicates resulting inputs by {@link BindingKey}. Contributions that do - * not depend on topology bindings can still emit direct inputs even when that - * binding collection is empty. The returned files are then typically wired into - * an {@link ArtifactAssembly} as its sources. - * - *

Direct clients are infrastructure code rather than build scripts. The - * typical usage pattern is: - *

    - *
  1. create one resolver per project;
  2. - *
  3. subscribe {@link #recordBinding(SourceSetUsageBinding)} to - * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)};
  4. - *
  5. call {@link #files(String, VariantArtifactSlot)} while registering an - * {@link ArtifactAssembly} or another consumer that needs the slot inputs.
  6. - *
- * - *

Build-script users normally do not instantiate this class directly. They - * configure {@code variantArtifacts}, and {@link VariantArtifactsPlugin} uses - * this resolver internally to turn slot rules into assembly inputs. - */ -@NonNullByDefault -public final class VariantArtifactsResolver { - private final ObjectFactory objects; - private final List boundContexts = new ArrayList<>(); - - public VariantArtifactsResolver(ObjectFactory objects) { - this.objects = objects; - } - - /** - * Records one resolved variant source-set usage. - * - *

Intended to be used as a callback target for - * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)}. - * - * @param context resolved variant/role/layer usage bound to a source set - */ - public void recordBinding(SourceSetUsageBinding context) { - boundContexts.add(context); - } - - /** - * Returns all source-set outputs selected by the given slot for the given - * variant. - * - *

The result is built from recorded {@link SourceSetUsageBinding} - * instances whose {@link SourceSetUsageBinding#variantName()} matches - * {@code variantName}. Each matching binding is then fed into the slot - * contribution pipeline; if multiple contributions resolve to the same - * {@link BindingKey}, that source is included only once. - * - *

This method does not validate the model; validation is expected to be - * performed earlier by {@link VariantArtifactsExtension}. Unknown variants - * or slots with no matching rules simply produce an empty collection. - * - * @param variantName variant whose bound source-set usages should be scanned - * @param slot slot definition that selects which outputs should be included - * @return lazily wired file collection for the selected outputs - */ - public FileCollection files(String variantName, VariantArtifactSlot slot) { - var contexts = boundContexts.stream() - .filter(context -> variantName.equals(context.variantName())) - .toList(); - var builder = new FileCollectionBuilder(contexts); - slot.acceptBindings(builder::visitBinding); - return builder.build(); - } - - /** - * Local materialization helper for one {@link #files(String, VariantArtifactSlot)} - * call. - */ - class FileCollectionBuilder { - private final ConfigurableFileCollection files; - private final Set boundOutputs = new LinkedHashSet<>(); - private final Collection contexts; - - FileCollectionBuilder(Collection contexts) { - this.files = objects.fileCollection(); - this.contexts = contexts; - } - - FileCollection build() { - return files; - } - - void addOutput(VariantArtifactSlot.ResolvedBinding binding) { - if (boundOutputs.add(binding.key())) - files.from(binding.files()); - } - - void visitBinding(VariantArtifactSlot.BindingResolver resolver) { - resolver.resolve(contexts, this::addOutput); - } - } - -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java b/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java +++ /dev/null @@ -1,317 +0,0 @@ -package org.implab.gradle.common.sources; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import javax.inject.Inject; - -import org.implab.gradle.common.core.lang.Closures; -import org.implab.gradle.common.core.lang.Strings; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.NamedDomainObjectProvider; -import org.gradle.api.file.ProjectLayout; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.logging.Logger; -import org.gradle.api.logging.Logging; - -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -import static org.implab.gradle.common.core.lang.Strings.sanitizeName; - -/** - * Adapter extension that registers source sets for variant/layer pairs. - */ -@NonNullByDefault -public abstract class VariantSourcesExtension { - private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class); - private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}"); - - private final ObjectFactory objects; - private final ProjectLayout layout; - private final LinkedHashMap bindingsByName = new LinkedHashMap<>(); - private final List> registeredActions = new ArrayList<>(); - private final List> boundActions = new ArrayList<>(); - private final List registeredContexts = new ArrayList<>(); - private final List boundContexts = new ArrayList<>(); - private final LinkedHashMap> sourceSetsByName = new LinkedHashMap<>(); - private final LinkedHashMap sourceSetLayersByName = new LinkedHashMap<>(); - private boolean sourceSetsRegistered; - - @Inject - public VariantSourcesExtension(ObjectFactory objects, ProjectLayout layout) { - this.objects = objects; - this.layout = layout; - } - - public List getBindings() { - return bindingsByName.values().stream().map(x -> (LayerBindingSpec)x).toList(); - } - - public LayerBindingSpec bind(String layer) { - return bindingsByName.computeIfAbsent( - normalize(layer, "Layer name must not be null or blank"), - name -> new LayerBinding(name, objects)); - } - - public LayerBindingSpec bind(LayoutLayer layer) { - return bind(layer.getName()); - } - - /** - * Configures per-layer binding. - */ - public LayerBindingSpec bind(String layer, Action configure) { - var binding = bind(layer); - configure.execute(binding); - return binding; - } - - public LayerBindingSpec bind(LayoutLayer layer, Action configure) { - var binding = bind(layer); - configure.execute(binding); - return binding; - } - - public LayerBindingSpec bind(String layer, - @DelegatesTo(value = LayerBindingSpec.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - return bind(layer, Closures.action(configure)); - } - - public LayerBindingSpec bind(LayoutLayer layer, - @DelegatesTo(value = LayerBindingSpec.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - return bind(layer, Closures.action(configure)); - } - - /** - * Global callback fired for each registered source set. - * Already emitted registrations are delivered immediately (replay). - * For simple callbacks you can use delegate-only style - * (for example {@code whenRegistered { sourceSetName() }}). - * For nested closures prefer explicit parameter - * ({@code whenRegistered { ctx -> ... }}). - */ - public void whenRegistered(Action action) { - registeredActions.add(action); - for (var context : registeredContexts) - action.execute(context); - } - - public void whenRegistered( - @DelegatesTo(value = SourceSetRegistration.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - whenRegistered(Closures.action(action)); - } - - /** - * Global callback fired for every resolved variant/role/layer usage. - * Already emitted usage bindings are delivered immediately (replay). - * For simple callbacks you can use delegate-only style - * (for example {@code whenBound { variantName() }}). - * For nested closures prefer explicit parameter - * ({@code whenBound { ctx -> ... }}). - */ - public void whenBound(Action action) { - boundActions.add(action); - for (var context : boundContexts) - action.execute(context); - } - - public void whenBound( - @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - whenBound(Closures.action(action)); - } - - public void whenBound(String variantName, Action action) { - var normalizedVariantName = normalize(variantName, "variantName must not be null or blank"); - whenBound(filterByVariant(normalizedVariantName, action)); - } - - public void whenBound(String variantName, - @DelegatesTo(value = SourceSetUsageBinding.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - whenBound(variantName, Closures.action(action)); - } - - void registerSourceSets(BuildVariantsExtension variants, org.gradle.api.NamedDomainObjectContainer sources) { - if (sourceSetsRegistered) { - throw new InvalidUserDataException("variantSources source sets are already registered"); - } - - resolveBindings(variants); - - var usages = layerUsages(variants).toList(); - var registeredBefore = registeredContexts.size(); - var boundBefore = boundContexts.size(); - - logger.debug( - "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})", - variants.getVariants().size(), - variants.getLayers().size(), - bindingsByName.size(), - usages.size()); - - usages.forEach(usage -> registerLayerUsage(usage, sources)); - - logger.debug( - "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})", - registeredContexts.size() - registeredBefore, - boundContexts.size() - boundBefore, - sourceSetsByName.size()); - - sourceSetsRegistered = true; - } - - private Stream layerUsages(BuildVariantsExtension variants) { - return variants.getVariants().stream() - .flatMap(variant -> variant.getRoles().stream() - .flatMap(role -> role.getLayers().getOrElse(List.of()).stream() - .map(layerName -> new LayerUsage( - variant.getName(), - role.getName(), - variants.requireLayer(normalize(layerName, "Layer name in variant '" - + variant.getName() + "' and role '" + role.getName() - + "' must not be null or blank")))))); - } - - private void registerLayerUsage(LayerUsage usage, org.gradle.api.NamedDomainObjectContainer sources) { - var resolvedBinding = binding(usage.layer().getName()); - var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern(); - sourceSetNamePattern.finalizeValueOnRead(); - - var sourceSetName = sourceSetName(usage, sourceSetNamePattern.get()); - - ensureSourceSetNameBoundToSingleLayer(sourceSetName, usage.layer().getName()); - var isNewSourceSet = !sourceSetsByName.containsKey(sourceSetName); - var sourceSet = sourceSetsByName.computeIfAbsent(sourceSetName, - name -> { - var ssp = sources.register(name); - ssp.configure(x -> { - x.getSourceSetDir().set(layout.getProjectDirectory().dir("src/" + usage.layer().getName())); - }); - return ssp; - }); - - var binding = new SourceSetUsageBinding( - usage.variantName(), - usage.roleName(), - usage.layer().getName(), - sourceSetName, - sourceSet); - - if (isNewSourceSet) { - var registration = new SourceSetRegistration( - usage.layer().getName(), - sourceSetName, - sourceSet); - resolvedBinding.notifyRegistered(registration); - notifyRegistered(registration); - } - - resolvedBinding.notifyBound(binding); - notifyBound(binding); - } - - private void notifyRegistered(SourceSetRegistration registration) { - registeredContexts.add(registration); - for (var action : registeredActions) - action.execute(registration); - } - - private void notifyBound(SourceSetUsageBinding binding) { - boundContexts.add(binding); - for (var action : boundActions) - action.execute(binding); - } - - private static Action filterByVariant(String variantName, - Action action) { - return binding -> { - if (variantName.equals(binding.variantName())) - action.execute(binding); - }; - } - - private void ensureSourceSetNameBoundToSingleLayer(String sourceSetName, String layerName) { - var existingLayer = sourceSetLayersByName.putIfAbsent(sourceSetName, layerName); - if (existingLayer != null && !existingLayer.equals(layerName)) { - throw new InvalidUserDataException("Source set '" + sourceSetName + "' is resolved from multiple layers: '" - + existingLayer + "' and '" + layerName + "'"); - } - } - - private void resolveBindings(BuildVariantsExtension variants) { - var errors = new ArrayList(); - for (var binding : bindingsByName.values()) { - if (variants.findLayer(binding.getName()).isEmpty()) { - errors.add("Layer binding '" + binding.getName() + "' references unknown layer"); - } - } - - if (!errors.isEmpty()) { - var message = new StringBuilder("Invalid variantSources model:"); - for (var error : errors) - message.append("\n - ").append(error); - throw new InvalidUserDataException(message.toString()); - } - } - - private LayerBinding binding(String layerName) { - return bindingsByName.computeIfAbsent(layerName, name -> new LayerBinding(name, objects)); - } - - private static String sourceSetName(LayerUsage usage, String pattern) { - var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank"); - var resolved = resolveSourceSetNamePattern(normalizedPattern, usage); - var result = sanitizeName(resolved); - - if (result.isEmpty()) - throw new InvalidUserDataException( - "sourceSetNamePattern '" + pattern + "' resolved to empty source set name"); - - return result; - } - - private static String resolveSourceSetNamePattern(String pattern, LayerUsage usage) { - var matcher = SOURCE_SET_NAME_TOKEN.matcher(pattern); - var output = new StringBuffer(); - - while (matcher.find()) { - var token = matcher.group(1); - matcher.appendReplacement(output, Matcher.quoteReplacement(tokenValue(token, usage))); - } - matcher.appendTail(output); - - return output.toString(); - } - - private static String tokenValue(String token, LayerUsage usage) { - return switch (token) { - case "variant" -> sanitizeName(usage.variantName()); - case "variantCap" -> Strings.capitalize(sanitizeName(usage.variantName())); - case "role" -> sanitizeName(usage.roleName()); - case "roleCap" -> Strings.capitalize(sanitizeName(usage.roleName())); - case "layer" -> sanitizeName(usage.layer().getName()); - case "layerCap" -> Strings.capitalize(sanitizeName(usage.layer().getName())); - default -> throw new InvalidUserDataException( - "sourceSetNamePattern contains unsupported token '{" + token + "}'"); - }; - } - - private static String normalize(@Nullable String value, String errorMessage) { - if (value == null) - throw new InvalidUserDataException(errorMessage); - var trimmed = value.trim(); - if (trimmed.isEmpty()) - throw new InvalidUserDataException(errorMessage); - return trimmed; - } - - private record LayerUsage(String variantName, String roleName, LayoutLayer layer) { - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java b/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.gradle.api.GradleException; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.logging.Logger; -import org.gradle.api.logging.Logging; - -/** - * Registers {@code variants} extension for build-variant modeling. - */ -public abstract class VariantsPlugin implements Plugin { - private static final Logger logger = Logging.getLogger(VariantsPlugin.class); - public static final String VARIANTS_EXTENSION_NAME = "variants"; - - @Override - public void apply(Project target) { - logger.debug("Registering '{}' extension on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath()); - var variants = target.getExtensions().create(VARIANTS_EXTENSION_NAME, BuildVariantsExtension.class); - target.afterEvaluate(project -> { - logger.debug("Finalizing variants model on project '{}'", project.getPath()); - variants.finalizeModel(); - logger.debug("Variants model finalized on project '{}'", project.getPath()); - }); - } - - public static BuildVariantsExtension getVariantsExtension(Project target) { - var extension = target.getExtensions().findByType(BuildVariantsExtension.class); - - if (extension == null) { - logger.error("Variants extension '{}' isn't found on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath()); - throw new GradleException("Variants extension isn't found"); - } - - logger.debug("Resolved '{}' extension on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath()); - - return extension; - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java b/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.implab.gradle.common.sources; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.logging.Logger; -import org.gradle.api.logging.Logging; - -/** - * Binds variant layers to registered source sets. - */ -public abstract class VariantsSourcesPlugin implements Plugin { - private static final Logger logger = Logging.getLogger(VariantsSourcesPlugin.class); - public static final String VARIANT_SOURCES_EXTENSION_NAME = "variantSources"; - - @Override - public void apply(Project target) { - logger.debug("Applying variant-sources plugin on project '{}'", target.getPath()); - target.getPluginManager().apply(VariantsPlugin.class); - target.getPluginManager().apply(SourcesPlugin.class); - - var variants = VariantsPlugin.getVariantsExtension(target); - var sources = SourcesPlugin.getSourcesExtension(target); - - var variantSources = target.getExtensions() - .create(VARIANT_SOURCES_EXTENSION_NAME, VariantSourcesExtension.class); - - variants.whenFinalized(model -> { - logger.debug("Registering source sets for variants on project '{}'", target.getPath()); - variantSources.registerSourceSets(model, sources); - logger.debug("Registered source sets on project '{}'", target.getPath()); - }); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/package-info.java b/common/src/main/java/org/implab/gradle/common/sources/package-info.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/package-info.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Source model and DSL for variant topology, source bindings, artifact assembly - * and outgoing publication integration. - * - *

Naming convention for callbacks and lifecycle hooks: - *

    - *
  • {@code whenXxx(...)}: register callback (supports replay where documented);
  • - *
  • {@code configureXxx(...)}: configure model elements;
  • - *
  • {@code notifyXxx(...)}: internal event dispatch helpers (not part of public DSL).
  • - *
- * - *

Closure-based callbacks use delegate-first resolution via - * {@code @DelegatesTo}. Delegate-only style is suitable for simple callbacks. - * For nested closures prefer explicit callback parameters ({@code ctx -> ...}). - */ -package org.implab.gradle.common.sources; diff --git a/common/src/test/java/org/implab/gradle/common/sources/VariantsArtifactsPluginFunctionalTest.java b/common/src/test/java/org/implab/gradle/common/sources/VariantsArtifactsPluginFunctionalTest.java deleted file mode 100644 --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsArtifactsPluginFunctionalTest.java +++ /dev/null @@ -1,608 +0,0 @@ -package org.implab.gradle.common.sources; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Collectors; - -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.GradleRunner; -import org.gradle.testkit.runner.TaskOutcome; -import org.gradle.testkit.runner.UnexpectedBuildFailure; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class VariantsArtifactsPluginFunctionalTest { - private static final String SETTINGS_FILE = "settings.gradle"; - private static final String BUILD_FILE = "build.gradle"; - private static final String ROOT_NAME = "rootProject.name = 'variants-artifacts-fixture'\n"; - - @TempDir - Path testProjectDir; - - @Test - void materializesVariantArtifactsAndInvokesOutgoingHooks() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile("inputs/base.js", "console.log('base')\n"); - writeFile("inputs/amd.js", "console.log('amd')\n"); - writeFile("inputs/mainJs.txt", "mainJs marker\n"); - writeFile("inputs/amdJs.txt", "amdJs marker\n"); - writeFile(BUILD_FILE, """ - import org.gradle.api.attributes.Attribute - - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layers('mainBase', 'mainAmd') - roles('main', 'test') - - variant('browser') { - role('main') { - layers('mainBase', 'mainAmd') - } - } - } - - variantSources { - bind('mainBase') { - configureSourceSet { - declareOutputs('js') - } - } - - bind('mainAmd') { - configureSourceSet { - declareOutputs('js') - } - } - - whenBound { ctx -> - if (ctx.sourceSetName() == 'browserMainBase') { - ctx.configureSourceSet { - registerOutput('js', layout.projectDirectory.file('inputs/base.js')) - } - } - - if (ctx.sourceSetName() == 'browserMainAmd') { - ctx.configureSourceSet { - registerOutput('js', layout.projectDirectory.file('inputs/amd.js')) - } - } - } - } - - variantArtifacts { - variant('browser') { - primarySlot('mainJs') { - fromRole('main') { - output('js') - } - } - - slot('amdJs') { - fromLayer('mainAmd') { - output('js') - } - } - } - - whenOutgoingVariant { publication -> - publication.slots().each { slotPublication -> - slotPublication.configureTask { - from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt")) - } - - slotPublication.configureArtifactAttributes { - attribute(Attribute.of('test.slot', String), slotPublication.slotName()) - } - } - } - } - - tasks.register('probe') { - dependsOn 'processBrowserMainJs', 'processBrowserAmdJs' - - doLast { - def mainDir = layout.buildDirectory.dir('variant-artifacts/browser/mainJs').get().asFile - def amdDir = layout.buildDirectory.dir('variant-artifacts/browser/amdJs').get().asFile - - assert new File(mainDir, 'base.js').exists() - assert new File(mainDir, 'amd.js').exists() - assert new File(mainDir, 'mainJs.txt').exists() - - assert !new File(amdDir, 'base.js').exists() - assert new File(amdDir, 'amd.js').exists() - assert new File(amdDir, 'amdJs.txt').exists() - - def elements = configurations.getByName('browserElements') - def primaryAttr = elements.attributes.getAttribute(Attribute.of('test.slot', String)) - def amdVariant = elements.outgoing.variants.getByName('amdJs') - def amdAttr = amdVariant.attributes.getAttribute(Attribute.of('test.slot', String)) - - println('primarySlot=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName()) - println('primaryAttr=' + primaryAttr) - println('amdAttr=' + amdAttr) - println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(',')) - println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(',')) - } - } - """); - - BuildResult result = runner("probe").build(); - - assertTrue(result.getOutput().contains("primarySlot=mainJs")); - assertTrue(result.getOutput().contains("primaryAttr=mainJs")); - assertTrue(result.getOutput().contains("amdAttr=amdJs")); - assertTrue(result.getOutput().contains("configurations=browserElements")); - assertTrue(result.getOutput().contains("secondaryVariants=amdJs")); - assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); - } - - @Test - void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - variantArtifacts { - variant('browser') { - slot('typesPackage') { - fromVariant { - output('types') - } - } - } - } - - tasks.register('probe') { - doLast { - println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName()) - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("primary=typesPackage")); - } - - @Test - void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile("inputs/bundle.js", "console.log('bundle')\n"); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - variantArtifacts { - variant('browser') { - primarySlot('bundle') { - from(layout.projectDirectory.file('inputs/bundle.js')) - } - } - } - - tasks.register('probe') { - dependsOn 'processBrowserBundle' - - doLast { - def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile - assert new File(bundleDir, 'bundle.js').exists() - println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName()) - } - } - """); - - BuildResult result = runner("probe").build(); - - assertTrue(result.getOutput().contains("primary=bundle")); - assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); - } - - @Test - void combinesDirectAndTopologyAwareSlotInputs() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile("inputs/base.js", "console.log('base')\n"); - writeFile("inputs/marker.txt", "marker\n"); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - variantSources { - bind('main') { - configureSourceSet { - declareOutputs('js') - } - } - - whenBound { ctx -> - ctx.configureSourceSet { - registerOutput('js', layout.projectDirectory.file('inputs/base.js')) - } - } - } - - variantArtifacts { - variant('browser') { - primarySlot('bundle') { - fromVariant { - output('js') - } - from(layout.projectDirectory.file('inputs/marker.txt')) - } - } - } - - tasks.register('probe') { - dependsOn 'processBrowserBundle' - - doLast { - def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile - assert new File(bundleDir, 'base.js').exists() - assert new File(bundleDir, 'marker.txt').exists() - } - } - """); - - BuildResult result = runner("probe").build(); - - assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); - } - - @Test - void failsOnUnknownVariantReference() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - } - - variantArtifacts { - variant('browser') { - slot('mainJs') { - fromVariant { - output('js') - } - } - } - } - """, "Variant artifact 'browser' references unknown variant 'browser'"); - } - - @Test - void failsOnUnknownRoleReference() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - variantArtifacts { - variant('browser') { - slot('mainJs') { - fromRole('test') { - output('js') - } - } - } - } - """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'"); - } - - @Test - void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - variantArtifacts { - variant('browser') { - slot('typesPackage') { - fromVariant { - output('types') - } - } - - slot('js') { - fromVariant { - output('js') - } - } - } - } - """, "Variant artifact 'browser' must declare primary slot because it has multiple slots"); - } - - @Test - void failsOnLayerReferenceOutsideVariantTopology() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('mainBase') - layer('extra') - - variant('browser') { - role('main') { - layers('mainBase') - } - } - } - - variantArtifacts { - variant('browser') { - slot('extraJs') { - fromLayer('extra') { - output('js') - } - } - } - } - """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'"); - } - - @Test - void failsOnLateMutationAfterFinalize() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants-artifacts' - } - - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - afterEvaluate { - variantArtifacts.variant('late') { - slot('js') { - fromVariant { - output('js') - } - } - } - } - """, "variantArtifacts model is finalized and cannot configure variants"); - } - - @Test - void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception { - writeFile(SETTINGS_FILE, """ - rootProject.name = 'variants-artifacts-fixture' - include 'producer', 'consumer' - """); - writeFile("producer/inputs/types.d.ts", "export type Foo = string\n"); - writeFile("producer/inputs/index.js", "export const foo = 'bar'\n"); - var buildscriptClasspath = pluginClasspath().stream() - .map(File::getAbsolutePath) - .map(path -> "'" + path.replace("\\", "\\\\") + "'") - .collect(Collectors.joining(", ")); - writeFile(BUILD_FILE, """ - buildscript { - dependencies { - classpath files(%s) - } - } - - import org.gradle.api.attributes.Attribute - - def variantAttr = Attribute.of('test.variant', String) - def slotAttr = Attribute.of('test.slot', String) - - subprojects { - apply plugin: 'org.implab.gradle-variants-artifacts' - } - - project(':producer') { - variants { - layer('main') - - variant('browser') { - role('main') { - layers('main') - } - } - } - - variantSources { - bind('main') { - configureSourceSet { - declareOutputs('types', 'js') - } - } - - whenBound { ctx -> - ctx.configureSourceSet { - registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts')) - registerOutput('js', layout.projectDirectory.file('inputs/index.js')) - } - } - } - - variantArtifacts { - variant('browser') { - primarySlot('typesPackage') { - fromVariant { - output('types') - } - } - - slot('js') { - fromVariant { - output('js') - } - } - } - - whenOutgoingVariant { publication -> - publication.configureConfiguration { - attributes.attribute(variantAttr, publication.variantName()) - } - - publication.primarySlot().configureArtifactAttributes { - attribute(slotAttr, publication.primarySlot().slotName()) - } - - publication.requireSlot('js').configureArtifactAttributes { - attribute(slotAttr, 'js') - } - } - } - } - - project(':consumer') { - configurations { - compileView { - canBeResolved = true - canBeConsumed = false - canBeDeclared = true - attributes { - attribute(variantAttr, 'browser') - attribute(slotAttr, 'typesPackage') - } - } - } - - dependencies { - compileView project(':producer') - } - - tasks.register('probe') { - doLast { - def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',') - def jsFiles = configurations.compileView.incoming.artifactView { - attributes { - attribute(slotAttr, 'js') - } - }.files.files.collect { it.name }.sort().join(',') - - println('compileFiles=' + compileFiles) - println('jsFiles=' + jsFiles) - } - } - } - """.formatted(buildscriptClasspath)); - - BuildResult result = runner(":consumer:probe").build(); - - assertTrue(result.getOutput().contains("compileFiles=typesPackage")); - assertTrue(result.getOutput().contains("jsFiles=js")); - } - - private GradleRunner runner(String... arguments) { - return GradleRunner.create() - .withProjectDir(testProjectDir.toFile()) - .withPluginClasspath(pluginClasspath()) - .withArguments(arguments) - .forwardOutput(); - } - - private void assertBuildFails(String buildScript, String expectedError) throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, buildScript); - - var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build()); - var output = ex.getBuildResult().getOutput(); - - assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output); - } - - private static List pluginClasspath() { - try { - var classesDir = Path.of(VariantArtifactsPlugin.class - .getProtectionDomain() - .getCodeSource() - .getLocation() - .toURI()); - - var markerResource = VariantArtifactsPlugin.class.getClassLoader() - .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties"); - - assertNotNull(markerResource, "Plugin marker resource is missing from test classpath"); - - var markerPath = Path.of(markerResource.toURI()); - var resourcesDir = markerPath.getParent().getParent().getParent(); - - return List.of(classesDir.toFile(), resourcesDir.toFile()); - } catch (Exception e) { - throw new RuntimeException("Unable to build plugin classpath for test", e); - } - } - - private void writeFile(String relativePath, String content) throws IOException { - Path path = testProjectDir.resolve(relativePath); - Files.createDirectories(path.getParent()); - Files.writeString(path, content); - } -} diff --git a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java b/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java deleted file mode 100644 --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.implab.gradle.common.sources; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.GradleRunner; -import org.gradle.testkit.runner.TaskOutcome; -import org.gradle.testkit.runner.UnexpectedBuildFailure; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class VariantsPluginFunctionalTest { - private static final String SETTINGS_FILE = "settings.gradle"; - private static final String BUILD_FILE = "build.gradle"; - private static final String ROOT_NAME = "rootProject.name = 'variants-fixture'\n"; - - @TempDir - Path testProjectDir; - - @Test - void configuresVariantModelWithDsl() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('mainBase') { - } - - layer('mainAmd') { - } - - variant('browser') { - role('main') { - layers('mainBase', 'mainAmd') - } - } - } - - tasks.register('probe') { - doLast { - def browser = variants.require('browser') - println('roles=' + browser.roles.size()) - println('roleLayers=' + browser.requireRole('main').layers.get().join(',')) - } - } - """); - - BuildResult result = runner("probe").build(); - - assertTrue(result.getOutput().contains("roles=1")); - assertTrue(result.getOutput().contains("roleLayers=mainBase,mainAmd")); - assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); - } - - @Test - void supportsLayerRegistryDslAndLookupApi() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('mainBase') - layer('mainAmd') - - variant('browser') { - role('main') { - layers('mainBase', 'mainAmd') - } - } - } - - tasks.register('probe') { - doLast { - println('layers=' + variants.layers.collect { it.name }.sort().join(',')) - println('requireLayer=' + variants.requireLayer('mainBase').name) - println('findLayer=' + variants.findLayer('missing').isPresent()) - } - } - """); - - BuildResult result = runner("probe").build(); - - assertTrue(result.getOutput().contains("layers=mainAmd,mainBase")); - assertTrue(result.getOutput().contains("requireLayer=mainBase")); - assertTrue(result.getOutput().contains("findLayer=false")); - } - - @Test - void failsOnUnknownLayerReference() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('mainBase') { - } - - variant('browser') { - role('main') { - layers('mainBase', 'missingLayer') - } - } - } - """, "references unknown layer 'missingLayer'"); - } - - @Test - void allowsUsingLayerFromDifferentVariantRole() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('mainBase') - - variant('browser') { - role('test') { - layers('mainBase') - } - } - } - """); - - BuildResult result = runner("help").build(); - assertTrue(result.getOutput().contains("BUILD SUCCESSFUL")); - } - - @Test - void failsOnDuplicatedLayerReferenceInRole() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') - - variant('browser') { - role('main') { - layers('a', 'a') - } - } - } - """, "contains duplicated layer reference 'a'"); - } - - @Test - void failsOnLateLayerMutationAfterFinalize() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') - variant('browser') { - role('main') { layers('a') } - } - } - - afterEvaluate { - variants.layer('late') - } - """, "Variants model is finalized and cannot configure layers"); - } - - @Test - void failsOnLateVariantMutationAfterFinalize() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') - variant('browser') { - role('main') { layers('a') } - } - } - - afterEvaluate { - variants.require('browser').role('late') { layers('a') } - } - """, "Variant 'browser' is finalized and cannot configure roles"); - } - - private GradleRunner runner(String... arguments) { - return GradleRunner.create() - .withProjectDir(testProjectDir.toFile()) - .withPluginClasspath(pluginClasspath()) - .withArguments(arguments) - .forwardOutput(); - } - - private void assertBuildFails(String buildScript, String expectedError) throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, buildScript); - - var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build()); - var output = ex.getBuildResult().getOutput(); - - assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output); - } - - private static List pluginClasspath() { - try { - var classesDir = Path.of(BuildVariant.class - .getProtectionDomain() - .getCodeSource() - .getLocation() - .toURI()); - - var markerResource = VariantsPlugin.class.getClassLoader() - .getResource("META-INF/gradle-plugins/org.implab.gradle-variants.properties"); - - assertNotNull(markerResource, "Plugin marker resource is missing from test classpath"); - - var markerPath = Path.of(markerResource.toURI()); - var resourcesDir = markerPath.getParent().getParent().getParent(); - - return List.of(classesDir.toFile(), resourcesDir.toFile()); - } catch (Exception e) { - throw new RuntimeException("Unable to build plugin classpath for test", e); - } - } - - private void writeFile(String relativePath, String content) throws IOException { - Path path = testProjectDir.resolve(relativePath); - Files.createDirectories(path.getParent()); - Files.writeString(path, content); - } -} diff --git a/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java b/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java deleted file mode 100644 --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java +++ /dev/null @@ -1,444 +0,0 @@ -package org.implab.gradle.common.sources; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.GradleRunner; -import org.gradle.testkit.runner.TaskOutcome; -import org.gradle.testkit.runner.UnexpectedBuildFailure; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class VariantsSourcesPluginFunctionalTest { - private static final String SETTINGS_FILE = "settings.gradle"; - private static final String BUILD_FILE = "build.gradle"; - private static final String ROOT_NAME = "rootProject.name = 'variants-sources-fixture'\n"; - - @TempDir - Path testProjectDir; - - @Test - void registersVariantSourceSetsAndFiresCallbacks() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('mainBase') - layer('mainAmd') - - variant('browser') { - role('main') { layers('mainBase', 'mainAmd') } - } - - variant('node') { - role('main') { layers('mainBase') } - } - } - - def events = [] - def localEvents = [] - - variantSources { - bind('mainBase') { - configureSourceSet { - declareOutputs('compiled') - } - } - bind('mainAmd') { - configureSourceSet { - declareOutputs('compiled') - } - } - bind('mainAmd').whenRegistered { ctx -> - localEvents << "${ctx.layerName()}:${ctx.sourceSetName()}" - } - whenRegistered { ctx -> - events << "${ctx.layerName()}:${ctx.sourceSetName()}" - } - } - - tasks.register('probe') { - doLast { - println("sources=" + sources.collect { it.name }.sort().join(',')) - println("events=" + events.sort().join('|')) - println("local=" + localEvents.sort().join('|')) - - def base = sources.getByName('browserMainBase') - def amd = sources.getByName('browserMainAmd') - def nodeBase = sources.getByName('nodeMainBase') - - base.output('compiled') - amd.output('compiled') - nodeBase.output('compiled') - - println('outputs=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - - assertTrue(result.getOutput().contains("sources=browserMainAmd,browserMainBase,nodeMainBase")); - assertTrue(result.getOutput().contains("events=mainAmd:browserMainAmd|mainBase:browserMainBase|mainBase:nodeMainBase")); - assertTrue(result.getOutput().contains("local=mainAmd:browserMainAmd")); - assertTrue(result.getOutput().contains("outputs=ok")); - assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); - } - - @Test - void supportsTrailingClosureOnBind() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - variant('browser') { - role('main') { layers('main') } - } - } - - variantSources { - bind('main') { - configureSourceSet { - declareOutputs('compiled') - } - } - } - - tasks.register('probe') { - doLast { - def ss = sources.getByName('browserMain') - ss.output('compiled') - println('bindClosure=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("bindClosure=ok")); - } - - @Test - void supportsBindingByBuildLayerIdentity() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - variant('browser') { - role('main') { layers('main') } - } - } - - variantSources { - bind(variants.requireLayer('main')) { - configureSourceSet { - declareOutputs('compiled') - } - } - } - - tasks.register('probe') { - doLast { - def ss = sources.getByName('browserMain') - ss.output('compiled') - println('bindLayerIdentity=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("bindLayerIdentity=ok")); - } - - @Test - void exposesBindingsSnapshot() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - layer('extra') - variant('browser') { - role('main') { layers('main') } - } - } - - variantSources { - bind('main') { - sourceSetNamePattern = '{layer}' - configureSourceSet { - declareOutputs('compiled') - } - } - bind('extra') - } - - tasks.register('probe') { - doLast { - def ss = sources.getByName('main') - ss.output('compiled') - println("bindings=" + variantSources.bindings.collect { it.name }.sort().join(',')) - println('bindingsSnapshot=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("bindings=extra,main")); - assertTrue(result.getOutput().contains("bindingsSnapshot=ok")); - } - - @Test - void failsOnUnknownLayerBinding() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - variant('browser') { - role('main') { layers('main') } - } - } - - variantSources { - bind('missing') - } - """); - - var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build()); - assertTrue(ex.getBuildResult().getOutput().contains("Layer binding 'missing' references unknown layer")); - } - - @Test - void exposesProviderInSourceSetRegistration() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - variant('browser') { - role('main') { layers('main') } - } - } - - variantSources { - whenRegistered { - configureSourceSet { - declareOutputs('generated') - } - } - } - - tasks.register('probe') { - doLast { - def ss = sources.getByName('browserMain') - ss.output('generated') - println('contextProvider=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("contextProvider=ok")); - } - - @Test - void replaysLateBindingsAndCallbacksAfterRegistration() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - variant('browser') { - role('main') { layers('main') } - } - } - - def events = [] - - afterEvaluate { - variantSources { - bind('main') { - configureSourceSet { - declareOutputs('late') - } - } - - bind('main').whenRegistered { ctx -> - events << "layer:${ctx.sourceSetName()}" - } - - whenRegistered { ctx -> - events << "global:${ctx.sourceSetName()}" - } - } - } - - tasks.register('probe') { - doLast { - def ss = sources.getByName('browserMain') - ss.output('late') - println("events=" + events.sort().join('|')) - println('lateReplay=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("events=global:browserMain|layer:browserMain")); - assertTrue(result.getOutput().contains("lateReplay=ok")); - } - - @Test - void supportsSourceSetNamePatternAndSharedRegistration() throws Exception { - writeFile(SETTINGS_FILE, ROOT_NAME); - writeFile(BUILD_FILE, """ - plugins { - id 'org.implab.gradle-variants-sources' - } - - variants { - layer('main') - - variant('browser') { - role('main') { layers('main') } - } - - variant('node') { - role('main') { layers('main') } - } - } - - def registeredEvents = [] - def boundEvents = [] - def browserBoundEvents = [] - def localRegisteredEvents = [] - def localBoundEvents = [] - - variantSources { - bind('main').sourceSetNamePattern = '{layer}' - - bind('main') { - configureSourceSet { - declareOutputs('compiled') - } - } - - bind('main') { - whenRegistered { - localRegisteredEvents << "${layerName()}:${sourceSetName()}" - } - } - - bind('main') { - whenBound { - localBoundEvents << "${variantName()}:${roleName()}:${layerName()}:${sourceSetName()}" - } - } - - whenRegistered { ctx -> - registeredEvents << "${ctx.layerName()}:${ctx.sourceSetName()}" - } - - whenBound { ctx -> - boundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}" - } - - whenBound('browser') { ctx -> - browserBoundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}" - } - } - - tasks.register('probe') { - doLast { - println("sources=" + sources.collect { it.name }.sort().join(',')) - - def main = sources.getByName('main') - main.output('compiled') - - println("registered=" + registeredEvents.sort().join('|')) - println("localRegistered=" + localRegisteredEvents.sort().join('|')) - println("bound=" + boundEvents.sort().join('|')) - println("browserBound=" + browserBoundEvents.sort().join('|')) - println("localBound=" + localBoundEvents.sort().join('|')) - println('sharedPattern=ok') - } - } - """); - - BuildResult result = runner("probe").build(); - assertTrue(result.getOutput().contains("sources=main")); - assertTrue(result.getOutput().contains("registered=main:main")); - assertTrue(result.getOutput().contains("localRegistered=main:main")); - assertTrue(result.getOutput().contains("bound=browser:main:main:main|node:main:main:main")); - assertTrue(result.getOutput().contains("browserBound=browser:main:main:main")); - assertTrue(result.getOutput().contains("localBound=browser:main:main:main|node:main:main:main")); - assertTrue(result.getOutput().contains("sharedPattern=ok")); - } - - private GradleRunner runner(String... arguments) { - return GradleRunner.create() - .withProjectDir(testProjectDir.toFile()) - .withPluginClasspath(pluginClasspath()) - .withArguments(arguments) - .forwardOutput(); - } - - private static List pluginClasspath() { - try { - var classesDir = Path.of(VariantsSourcesPlugin.class - .getProtectionDomain() - .getCodeSource() - .getLocation() - .toURI()); - - var markerResource = VariantsSourcesPlugin.class.getClassLoader() - .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties"); - - assertNotNull(markerResource, "Plugin marker resource is missing from test classpath"); - - var markerPath = Path.of(markerResource.toURI()); - var resourcesDir = markerPath.getParent().getParent().getParent(); - - return List.of(classesDir.toFile(), resourcesDir.toFile()); - } catch (Exception e) { - throw new RuntimeException("Unable to build plugin classpath for test", e); - } - } - - private void writeFile(String relativePath, String content) throws IOException { - Path path = testProjectDir.resolve(relativePath); - Files.createDirectories(path.getParent()); - Files.writeString(path, content); - } -} diff --git a/variants/src/main/java/org/implab/gradle/internal/IdentityContainerFactory.java b/variants/src/main/java/org/implab/gradle/internal/IdentityContainerFactory.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/internal/IdentityContainerFactory.java @@ -0,0 +1,14 @@ +package org.implab.gradle.internal; + +import org.gradle.api.Named; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.model.ObjectFactory; + +public final class IdentityContainerFactory { + private IdentityContainerFactory() { + } + + public static NamedDomainObjectContainer create(ObjectFactory objectFactory, Class type) { + return objectFactory.domainObjectContainer(type, name -> objectFactory.named(type, name)); + } +} diff --git a/common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java b/variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java rename from common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java rename to variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java --- a/common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java @@ -1,4 +1,4 @@ -package org.implab.gradle.common.sources; +package org.implab.gradle.variants; import org.gradle.api.GradleException; import org.gradle.api.NamedDomainObjectContainer; @@ -6,6 +6,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; +import org.implab.gradle.variants.sources.GenericSourceSet; /** * This plugin creates a {@code sources} extension which is diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java @@ -49,7 +49,7 @@ public abstract class VariantArtifactsPl // wire artifact assemblies to configuration variants var assembliesBridge = new ArtifactAssemblyBinder(assemblies); var primarySlotConvention = new SingleSlotConvention(providers); - var materializationHandler = new MaterializationPolicyHandler(assemblies); + var materializationHandler = new MaterializationPolicyHandler(); // bind slot assemblies to outgoing variants outgoing.configureEach(assembliesBridge::execute); diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java @@ -11,13 +11,13 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.implab.gradle.common.core.lang.Deferred; import org.implab.gradle.common.core.lang.Strings; -import org.implab.gradle.common.sources.GenericSourceSet; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.core.VariantsExtension; import org.implab.gradle.variants.core.VariantsView; import org.implab.gradle.variants.sources.CompileUnit; import org.implab.gradle.variants.sources.CompileUnitsView; +import org.implab.gradle.variants.sources.GenericSourceSet; import org.implab.gradle.variants.sources.RoleProjectionsView; import org.implab.gradle.variants.sources.VariantSourcesContext; import org.implab.gradle.variants.sources.VariantSourcesExtension; diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java @@ -6,6 +6,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; import org.implab.gradle.common.core.lang.Deferred; +import org.implab.gradle.internal.IdentityContainerFactory; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Role; import org.implab.gradle.variants.core.Variant; @@ -44,8 +45,8 @@ public abstract class VariantsPlugin imp public DefaultVariantsExtension(ObjectFactory objectFactory) { this.objectFactory = objectFactory; - this.layers = objectFactory.domainObjectContainer(Layer.class); - this.roles = objectFactory.domainObjectContainer(Role.class); + this.layers = IdentityContainerFactory.create(objectFactory, Layer.class); + this.roles = IdentityContainerFactory.create(objectFactory, Role.class); this.variantDefinitions = objectFactory.domainObjectContainer(VariantDefinition.class); } diff --git a/variants/src/main/java/org/implab/gradle/variants/artifacts/OutgoingConfigurationSlotSpec.java b/variants/src/main/java/org/implab/gradle/variants/artifacts/OutgoingConfigurationSlotSpec.java --- a/variants/src/main/java/org/implab/gradle/variants/artifacts/OutgoingConfigurationSlotSpec.java +++ b/variants/src/main/java/org/implab/gradle/variants/artifacts/OutgoingConfigurationSlotSpec.java @@ -1,7 +1,6 @@ package org.implab.gradle.variants.artifacts; import org.gradle.api.Action; -import org.gradle.api.Task; import org.gradle.api.attributes.AttributeContainer; import groovy.lang.Closure; import org.implab.gradle.common.core.lang.Closures; @@ -22,13 +21,6 @@ public interface OutgoingConfigurationSl ArtifactSlot getArtifactSlot(); /** - * Returns the assembly backing the published slot. - * - * @return slot assembly - */ - ArtifactAssembly getAssembly(); - - /** * Returns whether this slot is the primary outgoing artifact set of the variant. * * @return {@code true} for the primary slot @@ -36,19 +28,6 @@ public interface OutgoingConfigurationSl boolean isPrimary(); /** - * Configures the task producing the slot artifact. - * - * @param action task configuration action - */ - default void assemblyTask(Action action) { - getAssembly().getAssemblyTask().configure(action); - } - - default void assemblyTask(Closure closure) { - assemblyTask(Closures.action(closure)); - } - - /** * Configures attributes of this slot publication. * * @param action artifact attribute configuration action diff --git a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/MaterializationPolicyHandler.java b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/MaterializationPolicyHandler.java --- a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/MaterializationPolicyHandler.java +++ b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/MaterializationPolicyHandler.java @@ -5,8 +5,6 @@ import org.gradle.api.Action; import org.gradle.api.artifacts.Configuration; import org.gradle.api.attributes.AttributeContainer; import org.implab.gradle.internal.ReplayableQueue; -import org.implab.gradle.variants.artifacts.ArtifactAssemblies; -import org.implab.gradle.variants.artifacts.ArtifactAssembly; import org.implab.gradle.variants.artifacts.ArtifactSlot; import org.implab.gradle.variants.artifacts.OutgoingConfigurationSlotSpec; import org.implab.gradle.variants.artifacts.OutgoingConfigurationSpec; @@ -18,8 +16,7 @@ import org.implab.gradle.variants.core.V * *

Materialization is the phase where the plugin interprets variant artifact * declarations as Gradle outgoing publication state: a consumable configuration, - * its primary artifact set, secondary artifact variants, attributes, and backing - * assembly tasks. + * its primary artifact set, secondary artifact variants, and attributes. * *

The handler provides extension points for customizing the materialized * Gradle-facing objects. These hooks intentionally expose Gradle API objects. @@ -33,13 +30,10 @@ import org.implab.gradle.variants.core.V @NonNullByDefault public class MaterializationPolicyHandler implements Action { - private final ArtifactAssemblies resolver; - private final ReplayableQueue variantMaterialization = new ReplayableQueue<>(); private final ReplayableQueue slotMaterialization = new ReplayableQueue<>(); - public MaterializationPolicyHandler(ArtifactAssemblies resolver) { - this.resolver = resolver; + public MaterializationPolicyHandler() { } @Override @@ -57,12 +51,12 @@ public class MaterializationPolicyHandle slotMaterialized(new ArtifactSlot(variant, primarySlot), true, outgoing.getAttributes()); - outgoing.getVariants().configureEach(variantConfiguration -> { - var slotName = variantConfiguration.getName(); - var slot = slots.findByName(slotName); - if (slot != null) { - slotMaterialized(new ArtifactSlot(variant, slot), false, variantConfiguration.getAttributes()); - } + slots.forEach(slot -> { + if (slot.equals(primarySlot)) + return; + + var variantConfiguration = outgoing.getVariants().getByName(slot.getName()); + slotMaterialized(new ArtifactSlot(variant, slot), false, variantConfiguration.getAttributes()); }); }); }; @@ -104,11 +98,6 @@ public class MaterializationPolicyHandle } @Override - public ArtifactAssembly getAssembly() { - return resolver.require(slot); - } - - @Override public void artifactAttributes(Action action) { action.execute(attributes); } diff --git a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java @@ -14,6 +14,7 @@ import org.gradle.api.artifacts.Configur import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; +import org.implab.gradle.internal.IdentityContainerFactory; import org.implab.gradle.internal.ReplayableQueue; import org.implab.gradle.variants.artifacts.OutgoingVariant; import org.implab.gradle.variants.artifacts.Slot; @@ -92,7 +93,7 @@ public class OutgoingRegistry { NamedDomainObjectProvider configurationProvider) { this.variant = variant; this.configurationProvider = configurationProvider; - this.slots = objects.domainObjectContainer(Slot.class); + this.slots = IdentityContainerFactory.create(objects, Slot.class); this.primarySlot = objects.property(Slot.class); primarySlot.finalizeValueOnRead(); } diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/CompileUnitsView.java b/variants/src/main/java/org/implab/gradle/variants/sources/CompileUnitsView.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/CompileUnitsView.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/CompileUnitsView.java @@ -75,4 +75,4 @@ public final class CompileUnitsView { Objects.requireNonNull(variantsView, "variantsView can't be null"); return new CompileUnitsView(variantsView); } -} \ No newline at end of file +} diff --git a/common/src/main/java/org/implab/gradle/common/sources/GenericSourceSet.java b/variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java rename from common/src/main/java/org/implab/gradle/common/sources/GenericSourceSet.java rename to variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java --- a/common/src/main/java/org/implab/gradle/common/sources/GenericSourceSet.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java @@ -1,4 +1,4 @@ -package org.implab.gradle.common.sources; +package org.implab.gradle.variants.sources; import java.io.File; import java.nio.file.Paths; diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/RoleProjectionsView.java b/variants/src/main/java/org/implab/gradle/variants/sources/RoleProjectionsView.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/RoleProjectionsView.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/RoleProjectionsView.java @@ -80,4 +80,4 @@ public final class RoleProjectionsView { public static RoleProjectionsView of(VariantsView variantsView) { return new RoleProjectionsView(variantsView); } -} \ No newline at end of file +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java @@ -1,7 +1,6 @@ package org.implab.gradle.variants.sources; import org.gradle.api.NamedDomainObjectProvider; -import org.implab.gradle.common.sources.GenericSourceSet; /** * Materializes symbolic source set names into actual {@link GenericSourceSet} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java @@ -1,7 +1,6 @@ package org.implab.gradle.variants.sources; import org.gradle.api.Action; -import org.implab.gradle.common.sources.GenericSourceSet; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.core.VariantsView; diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java @@ -3,7 +3,6 @@ package org.implab.gradle.variants.sourc import org.eclipse.jdt.annotation.NonNullByDefault; import org.gradle.api.Action; import org.implab.gradle.common.core.lang.Closures; -import org.implab.gradle.common.sources.GenericSourceSet; import groovy.lang.Closure; @NonNullByDefault diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultVariantSourcesContext.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultVariantSourcesContext.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultVariantSourcesContext.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultVariantSourcesContext.java @@ -4,12 +4,12 @@ import java.util.HashMap; import java.util.Map; import org.gradle.api.Action; import org.gradle.api.NamedDomainObjectProvider; -import org.implab.gradle.common.sources.GenericSourceSet; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.core.VariantsView; import org.implab.gradle.variants.sources.CompileUnit; import org.implab.gradle.variants.sources.CompileUnitsView; +import org.implab.gradle.variants.sources.GenericSourceSet; import org.implab.gradle.variants.sources.RoleProjectionsView; import org.implab.gradle.variants.sources.SourceSetMaterializer; import org.implab.gradle.variants.sources.VariantSourcesContext; diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java @@ -14,10 +14,10 @@ import org.gradle.api.Action; import org.gradle.api.Named; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; -import org.implab.gradle.common.sources.GenericSourceSet; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.sources.CompileUnit; +import org.implab.gradle.variants.sources.GenericSourceSet; @NonNullByDefault public class SourceSetConfigurationRegistry { diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetRegistry.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetRegistry.java @@ -7,7 +7,7 @@ import java.util.function.Consumer; import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.model.ObjectFactory; import org.implab.gradle.common.core.lang.Deferred; -import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.variants.sources.GenericSourceSet; public class SourceSetRegistry { private final Map> materialized = new HashMap<>(); diff --git a/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-sources.properties b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-sources.properties new file mode 100644 --- /dev/null +++ b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-sources.properties @@ -0,0 +1,1 @@ +implementation-class=org.implab.gradle.variants.SourcesPlugin diff --git a/common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties rename from common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties rename to variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties --- a/common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties +++ b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties @@ -1,1 +1,1 @@ -implementation-class=org.implab.gradle.common.sources.VariantArtifactsPlugin +implementation-class=org.implab.gradle.variants.VariantArtifactsPlugin diff --git a/common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties rename from common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties rename to variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties --- a/common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties +++ b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants-sources.properties @@ -1,1 +1,1 @@ -implementation-class=org.implab.gradle.common.sources.VariantsSourcesPlugin +implementation-class=org.implab.gradle.variants.VariantSourcesPlugin diff --git a/common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants.properties b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants.properties rename from common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants.properties rename to variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants.properties --- a/common/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants.properties +++ b/variants/src/main/resources/META-INF/gradle-plugins/org.implab.gradle-variants.properties @@ -1,1 +1,1 @@ -implementation-class=org.implab.gradle.common.sources.VariantsPlugin +implementation-class=org.implab.gradle.variants.VariantsPlugin diff --git a/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java --- a/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java +++ b/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java @@ -12,7 +12,8 @@ import java.util.stream.Collectors; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; -import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.common.core.lang.Deferred; +import org.implab.gradle.variants.sources.GenericSourceSet; import org.junit.jupiter.api.io.TempDir; abstract class AbstractFunctionalTest { @@ -64,7 +65,9 @@ abstract class AbstractFunctionalTest { try { var files = new LinkedHashSet(); files.add(codeSource(VariantSourcesPlugin.class)); + files.add(resourceRoot("META-INF/gradle-plugins/org.implab.gradle-variants.properties")); files.add(codeSource(GenericSourceSet.class)); + files.add(codeSource(Deferred.class)); return List.copyOf(files); } catch (Exception e) { throw new RuntimeException("Unable to build plugin classpath for test", e); @@ -75,7 +78,19 @@ abstract class AbstractFunctionalTest { return Path.of(type.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile(); } - private void writeFile(String relativePath, String content) throws IOException { + private static File resourceRoot(String resourceName) throws Exception { + var url = VariantSourcesPlugin.class.getClassLoader().getResource(resourceName); + if (url == null) + throw new IllegalStateException("Classpath resource isn't found: " + resourceName); + + var path = Path.of(url.toURI()); + var segments = resourceName.split("/").length; + for (var i = 0; i < segments; i++) + path = path.getParent(); + return path.toFile(); + } + + protected void writeFile(String relativePath, String content) throws IOException { Path path = testProjectDir.resolve(relativePath); Files.createDirectories(path.getParent()); Files.writeString(path, content); diff --git a/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java @@ -0,0 +1,86 @@ +package org.implab.gradle.variants; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Properties; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +class PluginMarkerFunctionalTest extends AbstractFunctionalTest { + + @Test + void pluginMarkersPointToCurrentImplementations() throws Exception { + assertMarker("org.implab.gradle-variants.properties", VariantsPlugin.class); + assertMarker("org.implab.gradle-sources.properties", SourcesPlugin.class); + assertMarker("org.implab.gradle-variants-sources.properties", VariantSourcesPlugin.class); + assertMarker("org.implab.gradle-variants-artifacts.properties", VariantArtifactsPlugin.class); + } + + @Test + void pluginIdsApplyCurrentExtensions() throws Exception { + writeSettings("variants-plugin-ids"); + writeBuildFile(""" + apply plugin: 'org.implab.gradle-variants' + apply plugin: 'org.implab.gradle-sources' + apply plugin: 'org.implab.gradle-variants-sources' + apply plugin: 'org.implab.gradle-variants-artifacts' + + tasks.register('probe') { + doLast { + println("variantsExtension=" + (project.extensions.findByName('variants') != null)) + println("sourcesExtension=" + (project.extensions.findByName('sources') != null)) + println("variantSourcesExtension=" + (project.extensions.findByName('variantSources') != null)) + println("variantArtifactsExtension=" + (project.extensions.findByName('variantArtifacts') != null)) + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains("variantsExtension=true")); + assertTrue(result.getOutput().contains("sourcesExtension=true")); + assertTrue(result.getOutput().contains("variantSourcesExtension=true")); + assertTrue(result.getOutput().contains("variantArtifactsExtension=true")); + } + + @Test + void sourcesPluginSupportsStandaloneSourceSetWorkflow() throws Exception { + writeSettings("sources-plugin-standalone"); + writeFile("inputs/main.js", "console.log('main')\n"); + writeBuildFile(""" + apply plugin: 'org.implab.gradle-sources' + + sources.create('main') { + declareOutputs('js') + registerOutput('js', layout.projectDirectory.file('inputs/main.js')) + } + + tasks.register('probe') { + doLast { + def main = sources.named('main').get() + println("sourceSet=" + main.name) + println("jsFiles=" + main.output('js').files.collect { it.name }.sort().join(',')) + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains("sourceSet=main")); + assertTrue(result.getOutput().contains("jsFiles=main.js")); + } + + private static void assertMarker(String markerName, Class implementationClass) throws IOException { + var resourceName = "META-INF/gradle-plugins/" + markerName; + var resource = PluginMarkerFunctionalTest.class.getClassLoader().getResourceAsStream(resourceName); + assertNotNull(resource, "Missing plugin marker " + resourceName); + + var properties = new Properties(); + properties.load(resource); + assertEquals(implementationClass.getName(), properties.getProperty("implementation-class")); + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/VariantArtifactsPluginFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/VariantArtifactsPluginFunctionalTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/VariantArtifactsPluginFunctionalTest.java @@ -0,0 +1,563 @@ +package org.implab.gradle.variants; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.Test; + +class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest { + + @Test + void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception { + writeFile("settings.gradle", """ + rootProject.name = 'gradle-reference-outgoing-resolution' + include 'producer', 'consumer' + """); + writeFile("producer/inputs/typesPackage", "types\n"); + writeFile("producer/inputs/js", "js\n"); + writeBuildFile(""" + import org.gradle.api.attributes.Attribute + + def variantAttr = Attribute.of('test.variant', String) + def slotAttr = Attribute.of('test.slot', String) + + project(':producer') { + def browserElements = configurations.consumable('browserElements') + + println('reference: registered browserElements provider') + + browserElements.configure { configuration -> + println('reference: configuring browserElements') + + configuration.attributes.attribute(variantAttr, 'browser') + configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage') + configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage')) + + configuration.outgoing.variants.create('js') { secondary -> + println('reference: creating js outgoing variant') + + secondary.attributes.attribute(slotAttr, 'js') + secondary.artifact(layout.projectDirectory.file('inputs/js')) + } + } + } + + project(':consumer') { + configurations { + compileView { + canBeResolved = true + canBeConsumed = false + canBeDeclared = true + attributes { + attribute(variantAttr, 'browser') + attribute(slotAttr, 'typesPackage') + } + } + } + + dependencies { + compileView project(':producer') + } + + tasks.register('probe') { + doLast { + println('reference: resolving primary files') + + def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',') + + println('reference: resolving secondary files') + + def jsFiles = configurations.compileView.incoming.artifactView { + attributes { + attribute(slotAttr, 'js') + } + }.files.files.collect { it.name }.sort().join(',') + + println('compileFiles=' + compileFiles) + println('jsFiles=' + jsFiles) + } + } + } + """); + + BuildResult result = runner(":consumer:probe").build(); + var output = result.getOutput(); + var registered = output.indexOf("reference: registered browserElements provider"); + var resolvingPrimary = output.indexOf("reference: resolving primary files"); + var configuring = output.indexOf("reference: configuring browserElements"); + var creatingSecondary = output.indexOf("reference: creating js outgoing variant"); + var resolvingSecondary = output.indexOf("reference: resolving secondary files"); + + assertTrue(registered >= 0); + assertTrue(resolvingPrimary >= 0); + assertTrue(configuring >= 0); + assertTrue(creatingSecondary >= 0); + assertTrue(resolvingSecondary >= 0); + assertTrue(registered < resolvingPrimary); + assertTrue(resolvingPrimary < configuring); + assertTrue(configuring < creatingSecondary); + assertTrue(creatingSecondary < resolvingSecondary); + assertTrue(output.contains("compileFiles=typesPackage")); + assertTrue(output.contains("jsFiles=js")); + } + + @Test + void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception { + writeSettings("variant-artifacts-slots"); + writeFile("inputs/base.js", "console.log('base')\n"); + writeFile("inputs/amd.js", "console.log('amd')\n"); + writeFile("inputs/mainJs.txt", "mainJs marker\n"); + writeFile("inputs/amdJs.txt", "amdJs marker\n"); + writeBuildFile(""" + import org.gradle.api.attributes.Attribute + + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + def variantAttr = Attribute.of('test.variant', String) + def slotAttr = Attribute.of('test.slot', String) + + variants.layers.create('mainBase') + variants.layers.create('mainAmd') + variants.roles.create('main') + variants.roles.create('test') + variants.variant('browser') { + role('main') { + layers('mainBase', 'mainAmd') + } + } + + variantSources { + layer('mainBase') { + declareOutputs('js') + registerOutput('js', layout.projectDirectory.file('inputs/base.js')) + } + layer('mainAmd') { + declareOutputs('js') + registerOutput('js', layout.projectDirectory.file('inputs/amd.js')) + } + } + + variantArtifacts { + variant('browser') { + primarySlot('mainJs') { + fromRole('main') { + output('js') + } + from(layout.projectDirectory.file('inputs/mainJs.txt')) + } + slot('amdJs') { + fromLayer('mainAmd') { + output('js') + } + from(layout.projectDirectory.file('inputs/amdJs.txt')) + } + } + + whenOutgoingConfiguration { publication -> + publication.configuration { + attributes.attribute(variantAttr, publication.variant.name) + } + } + + whenOutgoingSlot { publication -> + def slotName = publication.artifactSlot.slot.name + publication.artifactAttributes { + attribute(slotAttr, slotName) + } + } + } + + tasks.register('probe') { + dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs' + dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs' + + doLast { + def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile + def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile + + assert new File(mainDir, 'base.js').exists() + assert new File(mainDir, 'amd.js').exists() + assert new File(mainDir, 'mainJs.txt').exists() + + assert !new File(amdDir, 'base.js').exists() + assert new File(amdDir, 'amd.js').exists() + assert new File(amdDir, 'amdJs.txt').exists() + + def elements = configurations.getByName('browserElements') + def amdVariant = elements.outgoing.variants.getByName('amdJs') + + println('variantAttr=' + elements.attributes.getAttribute(variantAttr)) + println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr)) + println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr)) + println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(',')) + println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(',')) + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains("variantAttr=browser")); + assertTrue(result.getOutput().contains("primarySlotAttr=mainJs")); + assertTrue(result.getOutput().contains("amdSlotAttr=amdJs")); + assertTrue(result.getOutput().contains("configurations=browserElements")); + assertTrue(result.getOutput().contains("secondaryVariants=amdJs")); + assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); + } + + @Test + void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception { + writeSettings("variant-artifacts-single-slot"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('main') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('main') + } + } + + variantSources.layer('main') { + declareOutputs('types') + } + + variantArtifacts { + variant('browser') { + slot('typesPackage') { + fromVariant { + output('types') + } + } + } + } + + tasks.register('probe') { + doLast { + variantArtifacts.whenAvailable { ctx -> + def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser') + println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name) + } + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains("primary=typesPackage")); + } + + @Test + void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception { + writeSettings("variant-artifacts-direct-input"); + writeFile("inputs/bundle.js", "console.log('bundle')\n"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('main') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('main') + } + } + + variantArtifacts { + variant('browser') { + primarySlot('bundle') { + from(layout.projectDirectory.file('inputs/bundle.js')) + } + } + } + + tasks.register('probe') { + dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle' + + doLast { + def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile + assert new File(bundleDir, 'bundle.js').exists() + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); + } + + @Test + void combinesDirectAndTopologyAwareSlotInputs() throws Exception { + writeSettings("variant-artifacts-combined-inputs"); + writeFile("inputs/base.js", "console.log('base')\n"); + writeFile("inputs/marker.txt", "marker\n"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('main') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('main') + } + } + + variantSources.layer('main') { + declareOutputs('js') + registerOutput('js', layout.projectDirectory.file('inputs/base.js')) + } + + variantArtifacts { + variant('browser') { + primarySlot('bundle') { + fromVariant { + output('js') + } + from(layout.projectDirectory.file('inputs/marker.txt')) + } + } + } + + tasks.register('probe') { + dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle' + + doLast { + def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile + assert new File(bundleDir, 'base.js').exists() + assert new File(bundleDir, 'marker.txt').exists() + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); + } + + @Test + void failsOnUnknownVariantReference() throws Exception { + writeSettings("variant-artifacts-missing-variant"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('main') + + variantArtifacts { + variant('browser') { + slot('mainJs') { + fromVariant { + output('js') + } + } + } + } + """); + + assertBuildFails("isn't declared", "help"); + } + + @Test + void failsOnUnknownRoleReference() throws Exception { + writeSettings("variant-artifacts-missing-role"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('main') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('main') + } + } + + variantArtifacts { + variant('browser') { + slot('mainJs') { + fromRole('test') { + output('js') + } + } + } + } + """); + + assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help"); + } + + @Test + void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception { + writeSettings("variant-artifacts-missing-primary"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('main') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('main') + } + } + + variantSources.layer('main') { + declareOutputs('types', 'js') + } + + variantArtifacts { + variant('browser') { + slot('typesPackage') { + fromVariant { + output('types') + } + } + slot('js') { + fromVariant { + output('js') + } + } + } + } + + tasks.register('probe') { + doLast { + variantArtifacts.whenAvailable { ctx -> + def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser') + ctx.findOutgoing(browser).get().primarySlot.get() + } + } + } + """); + + assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe"); + } + + @Test + void failsOnLayerReferenceOutsideVariantTopology() throws Exception { + writeSettings("variant-artifacts-layer-outside-topology"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + + variants.layers.create('mainBase') + variants.layers.create('extra') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('mainBase') + } + } + + variantArtifacts { + variant('browser') { + slot('extraJs') { + fromLayer('extra') { + output('js') + } + } + } + } + """); + + assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help"); + } + + @Test + void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception { + writeFile("settings.gradle", """ + rootProject.name = 'variant-artifacts-resolution' + include 'producer', 'consumer' + """); + writeFile("producer/inputs/types.d.ts", "export type Foo = string\n"); + writeFile("producer/inputs/index.js", "export const foo = 'bar'\n"); + writeBuildFile(""" + import org.gradle.api.attributes.Attribute + + def variantAttr = Attribute.of('test.variant', String) + def slotAttr = Attribute.of('test.slot', String) + + subprojects { + apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin + } + + project(':producer') { + variants.layers.create('main') + variants.roles.create('main') + variants.variant('browser') { + role('main') { + layers('main') + } + } + + variantSources.layer('main') { + declareOutputs('types', 'js') + registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts')) + registerOutput('js', layout.projectDirectory.file('inputs/index.js')) + } + + variantArtifacts { + variant('browser') { + primarySlot('typesPackage') { + fromVariant { + output('types') + } + } + slot('js') { + fromVariant { + output('js') + } + } + } + + whenOutgoingConfiguration { publication -> + publication.configuration { + attributes.attribute(variantAttr, publication.variant.name) + } + } + + whenOutgoingSlot { publication -> + publication.artifactAttributes { + attribute(slotAttr, publication.artifactSlot.slot.name) + } + } + } + + } + + project(':consumer') { + configurations { + compileView { + canBeResolved = true + canBeConsumed = false + canBeDeclared = true + attributes { + attribute(variantAttr, 'browser') + attribute(slotAttr, 'typesPackage') + } + } + } + + dependencies { + compileView project(':producer') + } + + tasks.register('probe') { + doLast { + def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',') + def jsFiles = configurations.compileView.incoming.artifactView { + attributes { + attribute(slotAttr, 'js') + } + }.files.files.collect { it.name }.sort().join(',') + + println('compileFiles=' + compileFiles) + println('jsFiles=' + jsFiles) + } + } + } + """); + + BuildResult result = runner(":consumer:probe").build(); + + assertTrue(result.getOutput().contains("compileFiles=typesPackage")); + assertTrue(result.getOutput().contains("jsFiles=js")); + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java --- a/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java +++ b/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java @@ -35,7 +35,7 @@ class VariantSourcesPluginFunctionalTest def browser = ctx.variants.variants.find { it.name == 'browser' } def production = ctx.variants.roles.find { it.name == 'production' } def mainLayer = ctx.variants.layers.find { it.name == 'main' } - def projection = ctx.roleProjections.getProjection(browser, production) + def projection = ctx.roleProjections.requireProjection(browser, production) def unit = ctx.compileUnits.requireUnit(browser, mainLayer) def left = ctx.sourceSets.getSourceSet(unit)