diff --git a/common/readme.md b/common/readme.md --- a/common/readme.md +++ b/common/readme.md @@ -87,11 +87,11 @@ outputs. Это уже "физический" уровень, к которому удобно привязывать задачи, ## DOMAIN MODEL -- `BuildLayer` — глобальный идентификатор слоя. +- `BuildLayer` — canonical identity-model объявленного слоя. - `BuildVariant` — агрегат ролей, атрибутов, артефактных слотов. -- `BuildRole` — роль внутри варианта, содержит ссылки на layer names. +- `BuildRole` — роль внутри варианта, содержит ссылки на declared layer names. - `GenericSourceSet` — зарегистрированный набор исходников и outputs. -- `BuildLayerBinding` — правила registration source set для конкретного layer. +- `LayerBindingSpec` — публичный DSL-contract adapter policy/callbacks для слоя. - `SourceSetRegistration` — payload события регистрации source set. - `SourceSetUsageBinding` — payload события usage-binding. @@ -116,7 +116,7 @@ Closure callbacks работают в delegate-first режиме (`@DelegatesTo`). Для - `BuildVariant` — API ролей, attributes и artifact slots варианта. - `VariantsSourcesPlugin` — применяет `variants` + `sources` и запускает адаптер. - `VariantSourcesExtension` — API bind/events registration. -- `BuildLayerBinding` — слой-конкретный DSL для имени и конфигурации source set. +- `LayerBindingSpec` — слой-конкретный DSL для policy/configuration source set. - `SourceSetRegistration` — payload `whenRegistered(...)`. - `SourceSetUsageBinding` — payload `whenBound(...)`. 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 --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java +++ b/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java @@ -20,22 +20,17 @@ import org.gradle.api.model.ObjectFactor import groovy.lang.Closure; public abstract class BuildVariantsExtension { - private final NamedDomainObjectContainer layers; + 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) { - layers = objects.domainObjectContainer(BuildLayer.class); + this.objects = objects; variants = objects.domainObjectContainer(BuildVariant.class); - layers.all(layer -> { - if (finalized) - throw new InvalidUserDataException( - "Variants model is finalized and cannot add layer '" + layer.getName() + "'"); - }); - variants.all(variant -> { if (finalized) throw new InvalidUserDataException( @@ -43,44 +38,26 @@ public abstract class BuildVariantsExten }); } - public NamedDomainObjectContainer getLayers() { - return layers; + public Collection getLayers() { + return Collections.unmodifiableCollection(layersByName.values()); } public NamedDomainObjectContainer getVariants() { return variants; } - public void layers(Action> action) { + public LayoutLayer layer(String name, Action configure) { ensureMutable("configure layers"); - action.execute(layers); - } - - public void layers(Closure configure) { - layers(Closures.action(configure)); - } - - public void variants(Action> action) { - ensureMutable("configure variants"); - action.execute(variants); - } - - public void variants(Closure configure) { - variants(Closures.action(configure)); - } - - public BuildLayer layer(String name, Action configure) { - ensureMutable("configure layers"); - var layer = layers.maybeCreate(name); + var layer = layersByName.computeIfAbsent(requireName(name, "Layer name must not be null or blank"), this::newLayer); configure.execute(layer); return layer; } - public BuildLayer layer(String name, Closure configure) { + public LayoutLayer layer(String name, Closure configure) { return layer(name, Closures.action(configure)); } - public BuildLayer layer(String name) { + public LayoutLayer layer(String name) { return layer(name, it -> { }); } @@ -115,6 +92,16 @@ public abstract class BuildVariantsExten 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)); } @@ -160,20 +147,6 @@ public abstract class BuildVariantsExten public void validate() { var errors = new ArrayList(); - var layersByName = new LinkedHashMap(); - for (var layer : layers) { - var layerName = normalize(layer.getName()); - if (layerName == null) { - errors.add("Layer name must not be blank"); - continue; - } - - var previous = layersByName.putIfAbsent(layerName, layer); - if (previous != null) { - errors.add("Layer '" + layerName + "' is declared more than once"); - } - } - for (var variant : variants) validateVariant(variant, layersByName, errors); @@ -186,7 +159,7 @@ public abstract class BuildVariantsExten } } - private static void validateVariant(BuildVariant variant, Map layersByName, List errors) { + 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"); @@ -211,7 +184,7 @@ public abstract class BuildVariantsExten } } - private static void validateRoleMappings(BuildVariant variant, Map layersByName, + private static void validateRoleMappings(BuildVariant variant, Map layersByName, List errors) { for (var role : variant.getRoles()) { var seenLayers = new LinkedHashSet(); @@ -244,6 +217,17 @@ public abstract class BuildVariantsExten 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/BuildLayerBinding.java b/common/src/main/java/org/implab/gradle/common/sources/LayerBinding.java rename from common/src/main/java/org/implab/gradle/common/sources/BuildLayerBinding.java rename to common/src/main/java/org/implab/gradle/common/sources/LayerBinding.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildLayerBinding.java +++ b/common/src/main/java/org/implab/gradle/common/sources/LayerBinding.java @@ -5,24 +5,16 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import javax.inject.Inject; - -import org.implab.gradle.common.core.lang.Closures; import org.gradle.api.Action; -import org.gradle.api.Named; import org.gradle.api.NamedDomainObjectProvider; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; -import groovy.lang.Closure; -import groovy.lang.DelegatesTo; - -/** - * Maps a logical layer to per-source-set hooks. - */ -public abstract class BuildLayerBinding implements Named { - public static final String DEFAULT_SOURCE_SET_NAME_PATTERN = "{variant}{layerCap}"; +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<>(); @@ -32,10 +24,10 @@ public abstract class BuildLayerBinding private final List boundContexts = new ArrayList<>(); private final Set registeredSourceSetNames = new LinkedHashSet<>(); - @Inject - public BuildLayerBinding(String name) { + LayerBinding(String name, ObjectFactory objects) { this.name = name; - getSourceSetNamePattern().convention(DEFAULT_SOURCE_SET_NAME_PATTERN); + sourceSetNamePattern = objects.property(String.class); + sourceSetNamePattern.convention(DEFAULT_SOURCE_SET_NAME_PATTERN); } @Override @@ -43,61 +35,32 @@ public abstract class BuildLayerBinding return name; } - public abstract Property getSourceSetNamePattern(); + @Override + public Property getSourceSetNamePattern() { + return sourceSetNamePattern; + } - /** - * Action applied to every registered source set for this layer. - * Already registered source sets are configured immediately (replay). - */ + @Override public void configureSourceSet(Action configure) { sourceSetConfigureActions.add(configure); for (var sourceSet : registeredSourceSets) sourceSet.configure(configure); } - public void configureSourceSet( - @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { - configureSourceSet(Closures.action(configure)); - } - - /** - * Layer-local callback fired after source-set registration. - * 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 -> ... }}). - */ + @Override 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)); - } - - /** - * Layer-local 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 -> ... }}). - */ + @Override 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)); - } - void notifyRegistered(SourceSetRegistration registration) { if (registeredSourceSetNames.add(registration.sourceSetName())) { var sourceSet = registration.sourceSet(); 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 new file mode 100644 --- /dev/null +++ b/common/src/main/java/org/implab/gradle/common/sources/LayerBindingSpec.java @@ -0,0 +1,41 @@ +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/BuildLayer.java b/common/src/main/java/org/implab/gradle/common/sources/LayoutLayer.java rename from common/src/main/java/org/implab/gradle/common/sources/BuildLayer.java rename to common/src/main/java/org/implab/gradle/common/sources/LayoutLayer.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildLayer.java +++ b/common/src/main/java/org/implab/gradle/common/sources/LayoutLayer.java @@ -5,13 +5,13 @@ import javax.inject.Inject; import org.gradle.api.Named; /** - * Global layer declaration used by build variants. + * Canonical identity model for a declared layout layer. */ -public abstract class BuildLayer implements Named { +public abstract class LayoutLayer implements Named { private final String name; @Inject - public BuildLayer(String name) { + public LayoutLayer(String name) { this.name = name; } 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 --- a/common/src/main/java/org/implab/gradle/common/sources/SourceSetRegistration.java +++ b/common/src/main/java/org/implab/gradle/common/sources/SourceSetRegistration.java @@ -12,7 +12,7 @@ import groovy.lang.DelegatesTo; * *

Used as callback payload for * {@link VariantSourcesExtension#whenRegistered(org.gradle.api.Action)} and - * {@link BuildLayerBinding#whenRegistered(org.gradle.api.Action)}. + * {@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 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 --- a/common/src/main/java/org/implab/gradle/common/sources/SourceSetUsageBinding.java +++ b/common/src/main/java/org/implab/gradle/common/sources/SourceSetUsageBinding.java @@ -12,7 +12,7 @@ import groovy.lang.DelegatesTo; * *

Used as callback payload for * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)} and - * {@link BuildLayerBinding#whenBound(org.gradle.api.Action)}. + * {@link LayerBindingSpec#whenBound(org.gradle.api.Action)}. * * @param variantName variant name from the build-variants model * @param roleName role name inside the resolved variant 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 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactSlot.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactSlot.java @@ -124,6 +124,10 @@ public class VariantArtifactSlot impleme return List.copyOf(bindings); } + void acceptBindings(Consumer consumer) { + bindings.forEach(consumer); + } + Set referencedRoleNames() { return Set.copyOf(referencedRoleNames); } 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 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsPlugin.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsPlugin.java @@ -33,7 +33,8 @@ public abstract class VariantArtifactsPl 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 + // Bind variant artifacts resolution to variant sources registration, so that + // artifact resolution can be performed variantSources.whenBound(variantArtifactsResolver::recordBinding); variants.whenFinalized(model -> { @@ -86,7 +87,8 @@ public abstract class VariantArtifactsPl var assemblies = variantArtifact.getSlots().stream() .collect(Collectors.toMap( VariantArtifactSlot::getName, - slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, slot), + slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact, + slot), (left, right) -> left, LinkedHashMap::new)); @@ -136,9 +138,10 @@ public abstract class VariantArtifactsPl ArtifactAssemblyRegistry artifactAssemblies, VariantArtifact variantArtifact, VariantArtifactSlot slot) { + String assemblyName = variantArtifact.getName() + Strings.capitalize(slot.getName()); return artifactAssemblies.register( - variantArtifact.getName() + Strings.capitalize(slot.getName()), - "process" + Strings.capitalize(variantArtifact.getName()) + Strings.capitalize(slot.getName()), + assemblyName, + "process" + Strings.capitalize(assemblyName), project.getLayout().getBuildDirectory() .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()), files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot))); 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 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsResolver.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantArtifactsResolver.java @@ -1,6 +1,7 @@ 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; @@ -86,13 +87,11 @@ public final class VariantArtifactsResol * @return lazily wired file collection for the selected outputs */ public FileCollection files(String variantName, VariantArtifactSlot slot) { - var builder = new FileCollectionBuilder(); var contexts = boundContexts.stream() .filter(context -> variantName.equals(context.variantName())) .toList(); - - slot.bindings().forEach(binding -> binding.resolve(contexts, builder::addOutput)); - + var builder = new FileCollectionBuilder(contexts); + slot.acceptBindings(builder::visitBinding); return builder.build(); } @@ -103,9 +102,11 @@ public final class VariantArtifactsResol class FileCollectionBuilder { private final ConfigurableFileCollection files; private final Set boundOutputs = new LinkedHashSet<>(); + private final Collection contexts; - FileCollectionBuilder() { + FileCollectionBuilder(Collection contexts) { this.files = objects.fileCollection(); + this.contexts = contexts; } FileCollection build() { @@ -116,6 +117,10 @@ public final class VariantArtifactsResol 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 --- a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java @@ -15,7 +15,6 @@ import org.eclipse.jdt.annotation.NonNul import org.eclipse.jdt.annotation.Nullable; import org.gradle.api.Action; import org.gradle.api.InvalidUserDataException; -import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.file.ProjectLayout; import org.gradle.api.model.ObjectFactory; @@ -35,8 +34,9 @@ public abstract class VariantSourcesExte 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 NamedDomainObjectContainer bindings; + private final LinkedHashMap bindingsByName = new LinkedHashMap<>(); private final List> registeredActions = new ArrayList<>(); private final List> boundActions = new ArrayList<>(); private final List registeredContexts = new ArrayList<>(); @@ -47,38 +47,46 @@ public abstract class VariantSourcesExte @Inject public VariantSourcesExtension(ObjectFactory objects, ProjectLayout layout) { + this.objects = objects; this.layout = layout; - bindings = objects.domainObjectContainer(BuildLayerBinding.class); } - public NamedDomainObjectContainer getBindings() { - return bindings; + public List getBindings() { + return bindingsByName.values().stream().map(x -> (LayerBindingSpec)x).toList(); } - public void bindings(Action> action) { - action.execute(bindings); + public LayerBindingSpec bind(String layer) { + return bindingsByName.computeIfAbsent( + normalize(layer, "Layer name must not be null or blank"), + name -> new LayerBinding(name, objects)); } - public void bindings( - @DelegatesTo(value = NamedDomainObjectContainer.class, strategy = Closure.DELEGATE_FIRST) Closure action) { - bindings(Closures.action(action)); - } - - public BuildLayerBinding bind(String layer) { - return bindings.maybeCreate(normalize(layer, "Layer name must not be null or blank")); + public LayerBindingSpec bind(LayoutLayer layer) { + return bind(layer.getName()); } /** * Configures per-layer binding. */ - public BuildLayerBinding bind(String layer, Action configure) { + public LayerBindingSpec bind(String layer, Action configure) { var binding = bind(layer); configure.execute(binding); return binding; } - public BuildLayerBinding bind(String layer, - @DelegatesTo(value = BuildLayerBinding.class, strategy = Closure.DELEGATE_FIRST) Closure configure) { + 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)); } @@ -130,12 +138,12 @@ public abstract class VariantSourcesExte whenBound(variantName, Closures.action(action)); } - void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer sources) { + void registerSourceSets(BuildVariantsExtension variants, org.gradle.api.NamedDomainObjectContainer sources) { if (sourceSetsRegistered) { throw new InvalidUserDataException("variantSources source sets are already registered"); } - validateBindings(variants); + resolveBindings(variants); var usages = layerUsages(variants).toList(); var registeredBefore = registeredContexts.size(); @@ -145,7 +153,7 @@ public abstract class VariantSourcesExte "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})", variants.getVariants().size(), variants.getLayers().size(), - bindings.size(), + bindingsByName.size(), usages.size()); usages.forEach(usage -> registerLayerUsage(usage, sources)); @@ -166,24 +174,25 @@ public abstract class VariantSourcesExte .map(layerName -> new LayerUsage( variant.getName(), role.getName(), - normalize(layerName, "Layer name in variant '" + variant.getName() - + "' and role '" + role.getName() + "' must not be null or blank"))))); + 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, NamedDomainObjectContainer sources) { - var resolvedBinding = bind(usage.layerName()); + 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.layerName()); + 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.layerName())); + x.getSourceSetDir().set(layout.getProjectDirectory().dir("src/" + usage.layer().getName())); }); return ssp; }); @@ -191,13 +200,13 @@ public abstract class VariantSourcesExte var binding = new SourceSetUsageBinding( usage.variantName(), usage.roleName(), - usage.layerName(), + usage.layer().getName(), sourceSetName, sourceSet); if (isNewSourceSet) { var registration = new SourceSetRegistration( - usage.layerName(), + usage.layer().getName(), sourceSetName, sourceSet); resolvedBinding.notifyRegistered(registration); @@ -236,14 +245,10 @@ public abstract class VariantSourcesExte } } - private void validateBindings(BuildVariantsExtension variants) { - var knownLayerNames = new java.util.LinkedHashSet(); - for (var layer : variants.getLayers()) - knownLayerNames.add(layer.getName()); - + private void resolveBindings(BuildVariantsExtension variants) { var errors = new ArrayList(); - for (var binding : bindings) { - if (!knownLayerNames.contains(binding.getName())) { + for (var binding : bindingsByName.values()) { + if (variants.findLayer(binding.getName()).isEmpty()) { errors.add("Layer binding '" + binding.getName() + "' references unknown layer"); } } @@ -256,6 +261,10 @@ public abstract class VariantSourcesExte } } + 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); @@ -287,8 +296,8 @@ public abstract class VariantSourcesExte case "variantCap" -> Strings.capitalize(sanitizeName(usage.variantName())); case "role" -> sanitizeName(usage.roleName()); case "roleCap" -> Strings.capitalize(sanitizeName(usage.roleName())); - case "layer" -> sanitizeName(usage.layerName()); - case "layerCap" -> Strings.capitalize(sanitizeName(usage.layerName())); + case "layer" -> sanitizeName(usage.layer().getName()); + case "layerCap" -> Strings.capitalize(sanitizeName(usage.layer().getName())); default -> throw new InvalidUserDataException( "sourceSetNamePattern contains unsupported token '{" + token + "}'"); }; @@ -303,6 +312,6 @@ public abstract class VariantSourcesExte return trimmed; } - private record LayerUsage(String variantName, String roleName, String layerName) { + private record LayerUsage(String variantName, String roleName, LayoutLayer layer) { } } 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 --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsArtifactsPluginFunctionalTest.java +++ b/common/src/test/java/org/implab/gradle/common/sources/VariantsArtifactsPluginFunctionalTest.java @@ -41,8 +41,8 @@ class VariantsArtifactsPluginFunctionalT } variants { - layer('mainBase') - layer('mainAmd') + layers('mainBase', 'mainAmd') + roles('main', 'test') variant('browser') { role('main') { 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 --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java +++ b/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java @@ -64,6 +64,41 @@ class VariantsPluginFunctionalTest { } @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 { 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 --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java +++ b/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java @@ -133,6 +133,83 @@ class VariantsSourcesPluginFunctionalTes } @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, """ diff --git a/common/variant-sources-plugin.md b/common/variant-sources-plugin.md --- a/common/variant-sources-plugin.md +++ b/common/variant-sources-plugin.md @@ -54,7 +54,7 @@ variantSources { ### binding -`bind('')` возвращает `BuildLayerBinding` и задает policy для этого +`bind('')` возвращает `LayerBindingSpec` и задает policy для этого слоя: - как именовать source set; @@ -125,14 +125,16 @@ Sugar: ### VariantSourcesExtension +- `bind(BuildLayer)` — получить/создать binding для canonical layer identity. - `bind(String)` — получить/создать binding по имени слоя. - `bind(String, Action|Closure)` — сконфигурировать binding. -- `bindings(Action|Closure)` — контейнерная конфигурация bindings. +- `bind(BuildLayer, Action|Closure)` — сконфигурировать binding по `BuildLayer`. +- `getBindings()` — read-only snapshot текущих bindings. - `whenRegistered(...)` — глобальные callbacks регистрации source set. - `whenBound(...)` — глобальные callbacks usage-binding. - `whenBound(String variantName, ...)` — usage-binding callbacks с variant-filter. -### BuildLayerBinding +### LayerBindingSpec - `sourceSetNamePattern` — naming policy для source set слоя. - `configureSourceSet(...)` — слойная конфигурация `GenericSourceSet`. @@ -143,7 +145,7 @@ Sugar: - `VariantsSourcesPlugin` — точка входа plugin adapter. - `VariantSourcesExtension` — глобальный DSL bind/events. -- `BuildLayerBinding` — layer-local policy и callbacks. +- `LayerBindingSpec` — публичный DSL-contract layer-local policy/callbacks. - `SourceSetRegistration` — payload регистрации source set. - `SourceSetUsageBinding` — payload usage-binding. @@ -151,5 +153,7 @@ Sugar: - `sourceSetNamePattern` фиксируется при первом чтении в registration (`finalizeValueOnRead`). +- runtime state bindings скрыт внутри adapter implementation (`LayerBinding`). +- name-based bindings резолвятся к canonical `BuildLayer` через registry `variants`. - Closure callbacks используют delegate-first. - Для вложенных closure лучше явный параметр (`ctx -> ...`). diff --git a/common/variants-plugin.md b/common/variants-plugin.md --- a/common/variants-plugin.md +++ b/common/variants-plugin.md @@ -81,8 +81,9 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в - `layer(...)` — объявление или конфигурация `BuildLayer`. - `variant(...)` — объявление или конфигурация `BuildVariant`. -- `layers { ... }`, `variants { ... }` — контейнерный DSL. +- `layers { layer(...) }`, `variants { ... }` — grouped DSL без публикации container API наружу. - `all(...)` — callback для всех вариантов. +- `findLayer(name)`, `requireLayer(name)`, `getAllLayers()` — доступ к registry слоев. - `getAll()`, `find(name)`, `require(name)` — доступ к вариантам. - `validate()` — явный запуск валидации. - `finalizeModel()` — валидация + финализация модели. @@ -99,7 +100,7 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в - `VariantsPlugin` — точка входа плагина. - `BuildVariantsExtension` — root extension и lifecycle. - `BuildVariant` — агрегатная модель варианта. -- `BuildLayer` — модель слоя. +- `BuildLayer` — canonical identity-model слоя. - `BuildRole` — модель роли. - `BuildArtifactSlot` — модель артефактного слота. - `VariantAttributes` — typed wrapper для variant attributes. @@ -107,4 +108,5 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в ## NOTES - Модель `variants` intentionally agnostic к toolchain. +- Layer registry хранится как явная identity-map, а не как публичный `NamedDomainObjectContainer`. - Интеграция с задачами выполняется через `variantSources` и адаптеры. diff --git a/settings.gradle b/settings.gradle --- a/settings.gradle +++ b/settings.gradle @@ -21,3 +21,4 @@ dependencyResolutionManagement { rootProject.name = 'gradle-common' include 'common' +include 'variants' diff --git a/variants/build.gradle b/variants/build.gradle new file mode 100644 --- /dev/null +++ b/variants/build.gradle @@ -0,0 +1,53 @@ +plugins { + id "java-library" + id "ivy-publish" +} + +java { + withJavadocJar() + withSourcesJar() + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +dependencies { + compileOnly libs.jdt.annotations + + api gradleApi(), + libs.bundles.jackson + + implementation project(":common") + + testImplementation gradleTestKit() + testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.4" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.4" + testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.4" +} + +task printVersion{ + doLast { + println "project: $project.group:$project.name:$project.version" + println "jar: ${->jar.archiveFileName.get()}" + } +} + +test { + useJUnitPlatform() +} + +publishing { + repositories { + ivy { + url "${System.properties["user.home"]}/ivy-repo" + } + } + publications { + ivy(IvyPublication) { + from components.java + descriptor.description { + text = providers.provider({ description }) + } + } + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java @@ -0,0 +1,17 @@ +package org.implab.gradle.variants; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.implab.gradle.variants.model.VariantsExtension; + +public abstract class VariantsPlugin implements Plugin { + @Override + public void apply(Project target) { + var extension = target.getExtensions().create("variants", VariantsExtension.class); + + target.afterEvaluate(project -> { + + }); + + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/model/Layer.java b/variants/src/main/java/org/implab/gradle/variants/model/Layer.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/model/Layer.java @@ -0,0 +1,9 @@ +package org.implab.gradle.variants.model; + +import org.gradle.api.Named; + +/** + * Identity-only domain object. + */ +public interface Layer extends Named { +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/model/Role.java b/variants/src/main/java/org/implab/gradle/variants/model/Role.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/model/Role.java @@ -0,0 +1,9 @@ +package org.implab.gradle.variants.model; + +import org.gradle.api.Named; + +/** + * Identity-only domain object. + */ +public interface Role extends Named { +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/model/RoleBinding.java b/variants/src/main/java/org/implab/gradle/variants/model/RoleBinding.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/model/RoleBinding.java @@ -0,0 +1,35 @@ +package org.implab.gradle.variants.model; + +import org.gradle.api.Named; +import org.gradle.api.provider.SetProperty; + +/** + * Binds a role to a set of layers inside a particular variant. + * + * The binding name is the role name, e.g. "production", "test", "tool". + */ +public interface RoleBinding extends Named { + + /** + * Layer names participating in this (variant, role) selection. + * + * Core model keeps names here deliberately: + * source/materialization semantics live elsewhere. + */ + SetProperty getLayerNames(); + + /** + * Adds one layer to this binding. + */ + void layer(String name); + + /** + * Adds several layers to this binding. + */ + void layers(String... names); + + /** + * Adds several layers to this binding. + */ + void layers(Iterable names); +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/model/Variant.java b/variants/src/main/java/org/implab/gradle/variants/model/Variant.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/model/Variant.java @@ -0,0 +1,44 @@ +package org.implab.gradle.variants.model; + +import org.gradle.api.Action; +import org.gradle.api.Named; +import org.gradle.api.NamedDomainObjectContainer; +import org.implab.gradle.common.core.lang.Closures; + +import groovy.lang.Closure; + +/** + * A named variant, e.g. "browser", "electron". + * + * A variant does not "have a role" directly. + * It owns a set of role bindings. + */ +public interface Variant extends Named { + + /** + * Role bindings declared inside this variant. + * + * The binding name is the role name. + */ + NamedDomainObjectContainer getRoleBindings(); + + /** + * Creates or returns an existing role binding and configures it. + */ + default RoleBinding role(String name) { + return getRoleBindings().maybeCreate(name); + } + + /** + * Creates or returns an existing role binding and configures it. + */ + default RoleBinding role(String name, Action action) { + var role = role(name); + action.execute(role); + return role; + } + + default RoleBinding role(String name, Closure closure) { + return role(name, Closures.action(closure)); + } +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/model/VariantsExtension.java b/variants/src/main/java/org/implab/gradle/variants/model/VariantsExtension.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/model/VariantsExtension.java @@ -0,0 +1,59 @@ +package org.implab.gradle.variants.model; + +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.implab.gradle.common.core.lang.Closures; + +import groovy.lang.Closure; + +/** + * Root extension: + * + * variants { + * layers { ... } + * roles { ... } + * + * variant("browser") { + * role("production") { + * layers("main", "generated", "mainRjs") + * } + * } + * } + */ +public interface VariantsExtension { + + /** + * Domain of layers. + */ + NamedDomainObjectContainer getLayers(); + + /** + * Domain of roles. + */ + NamedDomainObjectContainer getRoles(); + + /** + * Declared variants. + */ + NamedDomainObjectContainer getVariantDefinitions(); + + /** + * Creates or returns an existing variant and configures it. + */ + default Variant variant(String name) { + return getVariantDefinitions().maybeCreate(name); + } + + /** + * Creates or returns an existing variant and configures it. + */ + default Variant variant(String name, Action action) { + var variant = variant(name); + action.execute(variant); + return variant; + } + + default Variant variant(String name, Closure closure) { + return variant(name, Closures.action(closure)); + } +} \ No newline at end of file