# HG changeset patch # User cin # Date 2026-03-30 01:24:48 # Node ID 3285592a0ee9692990245618916b4d6a39292c47 # Parent d67a4d2c04cf6b800a2e5a9c7670252968a62965 Add compile-unit naming policy and late-configuration enforcement to VariantSourcesPlugin diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -1,4 +1,7 @@ syntax: glob .gradle/ +.codex/ common/build/ common/bin/ +variants/build/ +variants/bin/ diff --git a/AGENTS.md b/AGENTS.md --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,16 @@ - `require*` это `find*` + `fail-fast` с понятной ошибкой в месте вызова. - Для нового API предпочтительны формы `find/require`; новые `get*` по возможности не добавлять. +### Документация + +- документирование кода должно быть на английском языке +- к публичному API + - описание должно отражать назначение, где используется и какое влияние оказывает на остальные части программы + - давать небольшое описание концепции, а также краткие примеры +- к приватному API достаточно давать краткую справку о назначении и использовании +- реализацию алгоритмов в коде сопровождать комментариями с пояснениями, тривиальные операции пояснять не требуется. +- документация должна формироваться согласно требованиям по форматированию типа javadoc, jsdoc и т.п., в зависимости от используемых в проекте языках и инструментах. + ## Identity-first modeling Prefer an **identity-first** split between: diff --git a/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java b/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java --- a/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java +++ b/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java @@ -5,10 +5,20 @@ import java.util.List; import java.util.function.Consumer; public final class Deferred { - private final List> listeners = new LinkedList<>(); + private final List> listeners = new LinkedList<>(); private T value; private boolean resolved = false; + public boolean resolved() { + return resolved; + } + + public T value() { + if (!resolved) + throw new IllegalStateException(); + return value; + } + public void resolve(T value) { if (resolved) { throw new IllegalStateException("Already resolved"); @@ -19,7 +29,7 @@ public final class Deferred { listeners.clear(); } - public void whenResolved(Consumer listener) { + public void whenResolved(Consumer listener) { if (resolved) { listener.accept(value); } else { diff --git a/variant_sources_precedence.md b/variant_sources_precedence.md --- a/variant_sources_precedence.md +++ b/variant_sources_precedence.md @@ -120,6 +120,60 @@ Within the same selector level, actions For example, if two plugins both configure `layer("main")`, their actions are applied in the same order in which they were registered. +### Scope of this guarantee + +This precedence describes the normal materialization order used by the source-set +materializer. + +It is stable for source sets that are configured before they are materialized. + +If a selector rule is added after a target source set has already been +materialized, the behavior depends on the selected late-configuration policy. + +- in `fail` mode, such late configuration is rejected +- in `warn` and `allow` modes, the late action is applied as an imperative + follow-up step +- in `warn` and `allow` modes, selector precedence is not reconstructed + retroactively for already materialized targets + +--- + +## Late Configuration Policy + +`variantSources` exposes a policy switch for selector rules that target already +materialized source sets: + +```groovy +variantSources { + lateConfigurationPolicy { + failOnLateConfiguration() + } +} +``` + +Available modes: + +- `failOnLateConfiguration()` rejects such rules +- `warnOnLateConfiguration()` allows them and emits a warning +- `allowLateConfiguration()` allows them silently + +Policy rules: + +- the policy must be chosen before the first selector rule is added +- selector rules here mean `variant(...)`, `layer(...)`, and `unit(...)` +- once chosen, the policy cannot be changed later +- the policy is single-valued; it is not intended to be switched during further + configuration + +Operationally: + +- `fail` preserves the strict precedence contract by rejecting late mutation of + already materialized targets +- `warn` and `allow` keep compatibility with imperative late mutation +- in `warn` and `allow`, already materialized targets observe the late action in + actual registration order, not in reconstructed `variant < layer < unit` + order + --- ## Example @@ -184,13 +238,15 @@ The `variantSources` API is exposed thro Conceptually, configuration is registered against finalized model objects, while DSL sugar may still use names for convenience. -Internally, selector-based configuration is accumulated and later applied by the source-set materializer when a `GenericSourceSet` is created for a compile unit. +Internally, selector-based configuration is accumulated and later applied by the +source-set materializer when a `GenericSourceSet` is created for a compile unit. This guarantees that: -- selector precedence is stable +- selector precedence is stable before materialization - registration order is preserved -- configuration does not depend on the materialization moment +- configuration of already materialized targets is governed by the selected + late-configuration policy - adapters do not need to depend on raw Gradle lifecycle timing --- @@ -206,5 +262,8 @@ variant < layer < unit ``` - registration order is preserved within the same selector level +- already materialized targets are handled by `lateConfigurationPolicy(...)` +- the late-configuration policy must be selected before the first selector rule + and cannot be changed later - `variants` defines what exists - `variantSources` defines how those compile units are materialized as source sets 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 @@ -1,12 +1,17 @@ package org.implab.gradle.variants; +import java.util.Objects; +import java.util.function.Predicate; + 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.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.common.sources.SourcesPlugin; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.core.VariantsExtension; @@ -14,89 +19,138 @@ import org.implab.gradle.variants.core.V import org.implab.gradle.variants.sources.CompileUnit; import org.implab.gradle.variants.sources.CompileUnitsView; import org.implab.gradle.variants.sources.RoleProjectionsView; -import org.implab.gradle.variants.sources.SourceSetMaterializer; import org.implab.gradle.variants.sources.VariantSourcesContext; +import org.implab.gradle.variants.sources.VariantSourcesExtension; +import org.implab.gradle.variants.sources.internal.CompileUnitNamer; +import org.implab.gradle.variants.sources.internal.DefaultCompileUnitNamingPolicy; +import org.implab.gradle.variants.sources.internal.DefaultLateConfigurationPolicySpec; +import org.implab.gradle.variants.sources.internal.DefaultVariantSourcesContext; +import org.implab.gradle.variants.sources.internal.SourceSetConfigurationRegistry; +import org.implab.gradle.variants.sources.internal.SourceSetRegistry; @NonNullByDefault public abstract class VariantSourcesPlugin implements Plugin { + public static final String VARIANT_SOURCES_EXTENSION = "variantSources"; + @Override public void apply(Project target) { + var extensions = target.getExtensions(); + // Apply the main VariantsPlugin to ensure the core variant model is available. target.getPlugins().apply(VariantsPlugin.class); - target.getPlugins().apply(SourcesPlugin.class); // Access the VariantsExtension to configure variant sources. - var variantsExtension = target.getExtensions().getByType(VariantsExtension.class); + var variantsExtension = extensions.getByType(VariantsExtension.class); var objectFactory = target.getObjects(); - var sources = SourcesPlugin.getSourcesExtension(target); - var deferred = new Deferred(); + var lateConfigurationPolicy = new DefaultLateConfigurationPolicySpec(); + var namingPolicy = new DefaultCompileUnitNamingPolicy(); + variantsExtension.whenFinalized(variants -> { + // create variant views var compileUnits = CompileUnitsView.of(variants); var roleProjections = RoleProjectionsView.of(variants); - var context = new Context(variants, compileUnits, roleProjections); + // create registries + var sourceSetRegistry = new SourceSetRegistry(objectFactory); + var sourceSetConfiguration = new SourceSetConfigurationRegistry(lateConfigurationPolicy::mode); + + // build compile unit namer + var compileUnitNamer = CompileUnitNamer.builder() + .addUnits(compileUnits.getUnits()) + .nameCollisionPolicy(namingPolicy.policy()) + .build(); + // create the context + var context = new DefaultVariantSourcesContext( + variants, + compileUnits, + roleProjections, + compileUnitNamer, + sourceSetRegistry, + sourceSetConfiguration + ); deferred.resolve(context); }); - // var + var variantSourcesExtension = new VariantSourcesExtension() { + @Override + public void whenFinalized(Action action) { + deferred.whenResolved(action::execute); + } + + @Override + public void lateConfigurationPolicy(Action action) { + action.execute(lateConfigurationPolicy); + } + + @Override + public void namingPolicy(Action action) { + action.execute(namingPolicy); + } + + @Override + public void variant(String variantName, Action action) { + Strings.argumentNotNullOrBlank(variantName, "variantName"); + Objects.requireNonNull(action, "action can't be null"); + + lateConfigurationPolicy.finalizePolicy(); + + whenFinalized(ctx -> ctx.configureVariant(resolveVariant(ctx.getVariants(), variantName), action)); + } + + @Override + public void layer(String layerName, Action action) { + // protect external DSL + Strings.argumentNotNullOrBlank(layerName, "layerName"); + Objects.requireNonNull(action, "action can't be null"); + + lateConfigurationPolicy.finalizePolicy(); + + whenFinalized(ctx -> ctx.configureLayer(resolveLayer(ctx.getVariants(), layerName), action)); + } + + @Override + public void unit(String variantName, String layerName, Action action) { + Strings.argumentNotNullOrBlank(layerName, "layerName"); + Strings.argumentNotNullOrBlank(variantName, "variantName"); + Objects.requireNonNull(action, "action can't be null"); + + lateConfigurationPolicy.finalizePolicy(); + + whenFinalized(ctx -> ctx.configureUnit(resolveCompileUnit(ctx, variantName, layerName), action)); + } + }; + + extensions.add(VariantSourcesExtension.class, VARIANT_SOURCES_EXTENSION, variantSourcesExtension); } - private static class Context implements VariantSourcesContext { - - private final VariantsView variantsView; - private final CompileUnitsView compileUnitsView; - private final RoleProjectionsView roleProjectionsView; - - Context(VariantsView variantsView, CompileUnitsView compileUnitsView, RoleProjectionsView roleProjectionsView) { - this.variantsView = variantsView; - this.compileUnitsView = compileUnitsView; - this.roleProjectionsView = roleProjectionsView; - } - - @Override - public VariantsView getVariants() { - return variantsView; - } - - @Override - public CompileUnitsView getCompileUnits() { - return compileUnitsView; - } + private static Layer resolveLayer(VariantsView variants, String name) { + return variants.getLayers().stream() + .filter(named(name)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Layer '" + name + "' isn't declared")); + } - @Override - public RoleProjectionsView getRoleProjections() { - return roleProjectionsView; - } - - @Override - public SourceSetMaterializer getSourceSets() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getSourceSets'"); - } - - @Override - public void configureLayer(Layer layer, Action action) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'configureLayer'"); - } - - @Override - public void configureVariant(Variant variant, Action action) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'configureVariant'"); - } - - @Override - public void configureUnit(CompileUnit unit, Action action) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'configureUnit'"); - } - + private static Variant resolveVariant(VariantsView variants, String name) { + return variants.getVariants().stream() + .filter(named(name)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Variant '" + name + "' is't declared")); } + private static CompileUnit resolveCompileUnit(VariantSourcesContext ctx, String variantName, String layerName) { + return ctx.getCompileUnits().findUnit( + resolveVariant(ctx.getVariants(), variantName), + resolveLayer(ctx.getVariants(), layerName)) + .orElseThrow(() -> new InvalidUserDataException( + "The CompileUnit isn't declared for variant '" + variantName + "', layer '" + layerName + "'")); + } + + private static Predicate named(String name) { + return named -> named.getName().equals(name); + } } diff --git a/variants/src/main/java/org/implab/gradle/variants/core/VariantsView.java b/variants/src/main/java/org/implab/gradle/variants/core/VariantsView.java --- a/variants/src/main/java/org/implab/gradle/variants/core/VariantsView.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/VariantsView.java @@ -12,6 +12,16 @@ import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.gradle.api.InvalidUserDataException; +/** + * A resolved view of declared variants, roles, layers, and their bindings. + * + * Built from {@link VariantDefinition} instances, this class materializes validated + * {@link VariantRoleLayer} entries and provides lookup APIs grouped by variant, + * role, or layer. + * + * Typical usage is to collect identities and variant definitions through + * {@link Builder}, then use the resulting view to traverse resolved bindings. + */ @NonNullByDefault public class VariantsView { private final Set layers; @@ -36,41 +46,82 @@ public class VariantsView { .collect(Collectors.groupingBy(VariantRoleLayer::layer, Collectors.toSet())); } + /** + * Returns all declared layers included in this view. + */ public Set getLayers() { return layers; } + /** + * Returns all declared roles included in this view. + */ public Set getRoles() { return roles; } + /** + * Returns all declared variants included in this view. + */ public Set getVariants() { return variants; } + /** + * Returns all resolved variant-role-layer bindings. + */ public Set getEntries() { return entries; } + /** + * Returns all bindings associated with the specified variant. + * + * An empty set is returned when the variant has no bindings in this view. + */ public Set getEntriesForVariant(Variant variant) { return entriesByVariant.getOrDefault(variant, Set.of()); } + /** + * Returns all bindings associated with the specified layer. + * + * An empty set is returned when the layer has no bindings in this view. + */ public Set getEntriesForLayer(Layer layer) { return entriesByLayer.getOrDefault(layer, Set.of()); } + /** + * Returns all bindings associated with the specified role. + * + * An empty set is returned when the role has no bindings in this view. + */ public Set getEntriesForRole(Role role) { return entriesByRole.getOrDefault(role, Set.of()); } + /** + * A resolved binding between a variant, a role, and a layer. + * + * @param variant the resolved variant + * @param role the resolved role + * @param layer the resolved layer + */ public record VariantRoleLayer(Variant variant, Role role, Layer layer) { } + /** + * Creates a builder for assembling a {@link VariantsView}. + */ public static Builder builder() { return new Builder(); } + /** + * Collects declared identities and variant definitions, then resolves them + * into a {@link VariantsView}. + */ public static class Builder { private final Map layers = new LinkedHashMap<>(); @@ -81,30 +132,48 @@ public class VariantsView { private Builder() { } + /** + * Adds or replaces a role by its name. + */ public Builder addRole(Role role) { Objects.requireNonNull(role, "role can't be null"); roles.put(role.getName(), role); return this; } + /** + * Adds or replaces a layer by its name. + */ public Builder addLayer(Layer layer) { Objects.requireNonNull(layer, "layer can't be null"); layers.put(layer.getName(), layer); return this; } + /** + * Adds or replaces a variant by its name. + */ public Builder addVariant(Variant variant) { Objects.requireNonNull(variant, "variant can't be null"); variants.put(variant.getName(), variant); return this; } + /** + * Adds a variant definition to be resolved during {@link #build()}. + */ public Builder addDefinition(VariantDefinition definition) { Objects.requireNonNull(definition, "definition can't be null"); definitions.add(definition); return this; } + /** + * Resolves collected identities and definitions into an immutable view. + * + * Missing variant, role, or layer declarations referenced by definitions + * cause {@link InvalidUserDataException}. + */ public VariantsView build() { var entries = definitions.stream() 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 @@ -40,12 +40,32 @@ public interface VariantSourcesContext { * - to already materialized source sets of this layer * - to all future source sets of this layer * - * Actions are applied in registration order. + *

For future source sets, selector precedence and registration order are + * preserved by the materializer. + * + *

For already materialized source sets, behavior is governed by + * {@link VariantSourcesExtension#lateConfigurationPolicy(org.gradle.api.Action)}. + * In warn/allow modes the action is applied as a late imperative step and does + * not retroactively restore selector precedence. */ void configureLayer(Layer layer, Action action); + /** + * Configures all GenericSourceSets produced from the given variant. + * + *

Late application semantics for already materialized source sets are + * governed by + * {@link VariantSourcesExtension#lateConfigurationPolicy(org.gradle.api.Action)}. + */ void configureVariant(Variant variant, Action action); + /** + * Configures the GenericSourceSet produced from the given compile unit. + * + *

Late application semantics for already materialized source sets are + * governed by + * {@link VariantSourcesExtension#lateConfigurationPolicy(org.gradle.api.Action)}. + */ void configureUnit(CompileUnit unit, Action action); -} \ No newline at end of file +} 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 @@ -1,54 +1,52 @@ package org.implab.gradle.variants.sources; -import java.util.Objects; -import java.util.function.Predicate; - 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 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.VariantsView; - import groovy.lang.Closure; @NonNullByDefault public interface VariantSourcesExtension { - default void layer(String layerName, Action action) { - // protect external DSL - Strings.argumentNotNullOrBlank(layerName, "layerName"); - Objects.requireNonNull(action, "action can't be null"); + /** + * Selects how selector rules behave when they target an already materialized + * {@link GenericSourceSet}. + * + *

This policy is single-valued: + *

    + *
  • it must be selected before the first selector rule is registered via + * {@link #variant(String, Action)}, {@link #layer(String, Action)} or + * {@link #unit(String, String, Action)};
  • + *
  • once selected, it cannot be changed later;
  • + *
  • the policy controls both diagnostics and late-application semantics.
  • + *
+ */ + void lateConfigurationPolicy(Action action); - whenFinalized(ctx -> ctx.configureLayer(resolveLayer(ctx.getVariants(), layerName), action)); + default void lateConfigurationPolicy(Closure closure) { + lateConfigurationPolicy(Closures.action(closure)); } + void namingPolicy(Action action); + + default void namingPolicy(Closure closure) { + namingPolicy(Closures.action(closure)); + } + + void layer(String layerName, Action action); + default void layer(String layerName, Closure closure) { layer(layerName, Closures.action(closure)); } - default void variant(String variantName, Action action) { - Strings.argumentNotNullOrBlank(variantName, "variantName"); - Objects.requireNonNull(action, "action can't be null"); - - whenFinalized(ctx -> ctx.configureVariant(resolveVariant(ctx.getVariants(), variantName), action)); - } + void variant(String variantName, Action action); default void variant(String variantName, Closure closure) { variant(variantName, Closures.action(closure)); } - default void unit(String variantName, String layerName, Action action) { - Strings.argumentNotNullOrBlank(layerName, "layerName"); - Strings.argumentNotNullOrBlank(variantName, "variantName"); - Objects.requireNonNull(action, "action can't be null"); - - whenFinalized(ctx -> ctx.configureUnit(resolveCompileUnit(ctx, variantName, layerName), action)); - } + void unit(String variantName, String layerName, Action action); /** * Invoked when finalized variants-derived source context becomes available. @@ -65,29 +63,43 @@ public interface VariantSourcesExtension whenFinalized(Closures.action(closure)); } - private static Layer resolveLayer(VariantsView variants, String name) { - return variants.getLayers().stream() - .filter(named(name)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException("Layer '" + name + "' isn't declared")); + + /** + * Imperative selector for the late-configuration mode. + * + *

Exactly one mode is expected to be chosen for the extension lifecycle. + */ + interface LateConfigurationPolicySpec { + /** + * Rejects selector registration if it targets any already materialized + * source set. + */ + void failOnLateConfiguration(); + + /** + * Allows late selector registration, but emits a warning when it targets an + * already materialized source set. + * + *

For such targets, selector precedence is not re-established + * retroactively. The action is applied as a late imperative step, after the + * state already produced at the materialization moment. + */ + void warnOnLateConfiguration(); + + /** + * Allows late selector registration without a warning when it targets an + * already materialized source set. + * + *

For such targets, selector precedence is not re-established + * retroactively. The action is applied as a late imperative step, after the + * state already produced at the materialization moment. + */ + void allowLateConfiguration(); } - private static Variant resolveVariant(VariantsView variants, String name) { - return variants.getVariants().stream() - .filter(named(name)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException("Variant '" + name + "' is't declared")); - } + interface NamingPolicySpec { + void failOnNameCollision(); - private static CompileUnit resolveCompileUnit(VariantSourcesContext ctx, String variantName, String layerName) { - return ctx.getCompileUnits().findUnit( - resolveVariant(ctx.getVariants(), variantName), - resolveLayer(ctx.getVariants(), layerName)) - .orElseThrow(() -> new InvalidUserDataException( - "The CompileUnit isn't declared for variant '" + variantName + "', layer '" + layerName + "'")); + void resolveNameCollision(); } - - private static Predicate named(String name) { - return named -> named.getName().equals(name); - } -} \ No newline at end of file +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/CompileUnitNamer.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/CompileUnitNamer.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/CompileUnitNamer.java @@ -0,0 +1,91 @@ +package org.implab.gradle.variants.sources.internal; + +import java.text.MessageFormat; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.gradle.api.InvalidUserDataException; +import org.implab.gradle.common.core.lang.Strings; +import org.implab.gradle.variants.sources.CompileUnit; + +public interface CompileUnitNamer { + + String resolveName(CompileUnit unit); + + public static Builder builder() { + return new Builder(); + } + + static class Builder { + private final Set units = new HashSet<>(); + private NameCollisionPolicy nameCollisionPolicy = NameCollisionPolicy.FAIL; + + private Builder() { + } + + public Builder addUnits(Collection other) { + units.addAll(other); + return this; + } + + public Builder nameCollisionPolicy(NameCollisionPolicy policy) { + nameCollisionPolicy = policy; + return this; + } + + public CompileUnitNamer build() { + Map seen = new HashMap<>(); + + if (nameCollisionPolicy == NameCollisionPolicy.FAIL) { + var collisions = units.stream() + .collect(Collectors.groupingBy(this::projectName)) + .entrySet().stream() + .filter(pair -> pair.getValue().size() > 1) + .map(pair -> MessageFormat.format( + "({0}: {1})", + pair.getKey(), + pair.getValue().stream() + .map(Object::toString) + .collect(Collectors.joining(",")))) + .collect(Collectors.joining(",")); + if (!collisions.isEmpty()) + throw new InvalidUserDataException( + "The same source set names are produced by different compile units: " + collisions); + } + + var unitNames = units.stream() + .sorted(Comparator + .comparing((CompileUnit unit) -> unit.variant().getName()) + .thenComparing(unit -> unit.layer().getName())) + .collect(Collectors.toUnmodifiableMap(Function.identity(), unit -> { + var baseName = projectName(unit); + + var c = seen.compute(baseName, (key, count) -> count == null ? 1 : count + 1); + return c == 1 ? baseName : baseName + String.valueOf(c); + })); + + return new CompileUnitNamer() { + + @Override + public String resolveName(CompileUnit unit) { + return Optional.ofNullable(unitNames.get(unit)).orElseThrow( + () -> new IllegalArgumentException(MessageFormat.format( + "Compile unit {0} doesn't have an associated name", + unit))); + } + + }; + } + + private String projectName(CompileUnit unit) { + return unit.variant().getName() + Strings.capitalize(unit.layer().getName()); + } + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultCompileUnitNamingPolicy.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultCompileUnitNamingPolicy.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultCompileUnitNamingPolicy.java @@ -0,0 +1,36 @@ +package org.implab.gradle.variants.sources.internal; + +import org.implab.gradle.variants.sources.VariantSourcesExtension.NamingPolicySpec; + +public class DefaultCompileUnitNamingPolicy implements NamingPolicySpec { + private NameCollisionPolicy policy = NameCollisionPolicy.FAIL; + private boolean policyApplied = false; + + public NameCollisionPolicy policy() { + finalizePolicy(); + return policy; + } + + public void finalizePolicy() { + policyApplied = true; + } + + @Override + public void failOnNameCollision() { + assertApplyOnce(); + policy = NameCollisionPolicy.FAIL; + } + + @Override + public void resolveNameCollision() { + assertApplyOnce(); + policy = NameCollisionPolicy.RESOLVE; + } + + private void assertApplyOnce() { + if (policyApplied) + throw new IllegalStateException("Naming policy already applied"); + policyApplied = true; + + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLateConfigurationPolicySpec.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLateConfigurationPolicySpec.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLateConfigurationPolicySpec.java @@ -0,0 +1,42 @@ +package org.implab.gradle.variants.sources.internal; + +import org.implab.gradle.variants.sources.VariantSourcesExtension.LateConfigurationPolicySpec; + +public class DefaultLateConfigurationPolicySpec implements LateConfigurationPolicySpec { + + private LateConfigurationMode policyMode = LateConfigurationMode.FAIL; + private boolean policyApplied = false; + + public LateConfigurationMode mode() { + return policyMode; + } + + public void finalizePolicy() { + policyApplied = true; + } + + @Override + public void failOnLateConfiguration() { + assertApplyOnce(); + policyMode = LateConfigurationMode.FAIL; + } + + @Override + public void warnOnLateConfiguration() { + assertApplyOnce(); + policyMode = LateConfigurationMode.WARN; + } + + @Override + public void allowLateConfiguration() { + assertApplyOnce(); + policyMode = LateConfigurationMode.APPLY; + } + + private void assertApplyOnce() { + if (policyApplied) + throw new IllegalStateException("Lazy configuration policy already applied"); + policyApplied = true; + } + +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLayerConfigurationRegistry.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLayerConfigurationRegistry.java deleted file mode 100644 --- a/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLayerConfigurationRegistry.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.implab.gradle.variants.sources.internal; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -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.sources.CompileUnit; - -@NonNullByDefault -public class DefaultLayerConfigurationRegistry implements LayerConfigurationRegistry { - private final Map>> actionsByLayer = new LinkedHashMap<>(); - private final Map>> actionsByVariant = new LinkedHashMap<>(); - private final Map>> actionsByUnit = new LinkedHashMap<>(); - - public void addLayerAction(Layer layer, Action action) { - actionsByLayer.computeIfAbsent(layer, key -> new ArrayList<>()).add(action); - } - - public void addVariantAction(Variant variant, Action action) { - actionsByVariant.computeIfAbsent(variant, key -> new ArrayList<>()).add(action); - } - - public void addUnitAction(CompileUnit unit, Action action) { - actionsByUnit.computeIfAbsent(unit, key -> new ArrayList<>()).add(action); - } - - @Override - public void applyLayer(Layer layer, GenericSourceSet sourceSet) { - var actions = actionsByLayer.get(layer); - if (actions == null) { - return; - } - for (var action : actions) { - action.execute(sourceSet); - } - } - - @Override - public void applyUnit(CompileUnit unit, GenericSourceSet sourceSet) { - applyLayer(unit.layer(), sourceSet); - } -} 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 new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultVariantSourcesContext.java @@ -0,0 +1,92 @@ +package org.implab.gradle.variants.sources.internal; + +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.RoleProjectionsView; +import org.implab.gradle.variants.sources.SourceSetMaterializer; +import org.implab.gradle.variants.sources.VariantSourcesContext; + +public class DefaultVariantSourcesContext implements VariantSourcesContext { + private final VariantsView variantsView; + private final CompileUnitsView compileUnitsView; + private final RoleProjectionsView roleProjectionsView; + private final SourceSetMaterializer sourceSetMaterializer; + private final SourceSetRegistry sourceSetRegistry; + private final CompileUnitNamer compileUnitNamer; + private final SourceSetConfigurationRegistry sourceSetConfigurationRegistry; + + public DefaultVariantSourcesContext( + VariantsView variantsView, + CompileUnitsView compileUnitsView, + RoleProjectionsView roleProjectionsView, + CompileUnitNamer compileUnitNamer, + SourceSetRegistry sourceSetRegistry, + SourceSetConfigurationRegistry sourceSetConfigurationRegistry) { + this.variantsView = variantsView; + this.compileUnitNamer = compileUnitNamer; + this.compileUnitsView = compileUnitsView; + this.roleProjectionsView = roleProjectionsView; + this.sourceSetRegistry = sourceSetRegistry; + this.sourceSetConfigurationRegistry = sourceSetConfigurationRegistry; + + sourceSetMaterializer = new LocalSourceSetMaterializer(); + } + + @Override + public VariantsView getVariants() { + return variantsView; + } + + @Override + public CompileUnitsView getCompileUnits() { + return compileUnitsView; + } + + @Override + public RoleProjectionsView getRoleProjections() { + return roleProjectionsView; + } + + @Override + public SourceSetMaterializer getSourceSets() { + return sourceSetMaterializer; + } + + @Override + public void configureLayer(Layer layer, Action action) { + sourceSetConfigurationRegistry.addLayerAction(layer, action); + } + + @Override + public void configureVariant(Variant variant, Action action) { + sourceSetConfigurationRegistry.addVariantAction(variant, action); + } + + @Override + public void configureUnit(CompileUnit unit, Action action) { + sourceSetConfigurationRegistry.addCompileUnitAction(unit, action); + } + + class LocalSourceSetMaterializer implements SourceSetMaterializer { + private final Map> registeredSources = new HashMap<>(); + + @Override + public NamedDomainObjectProvider getSourceSet(CompileUnit unit) { + return registeredSources.computeIfAbsent(unit, k -> { + var sourcesName = compileUnitNamer.resolveName(unit); + sourceSetRegistry.whenMaterialized(sourcesName, + sourceSet -> sourceSetConfigurationRegistry.applyConfiguration(unit, sourceSet)); + return sourceSetRegistry.sourceSets().register(sourcesName); + }); + } + + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/LateConfigurationMode.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/LateConfigurationMode.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/LateConfigurationMode.java @@ -0,0 +1,7 @@ +package org.implab.gradle.variants.sources.internal; + +public enum LateConfigurationMode { + FAIL, + WARN, + APPLY +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/NameCollisionPolicy.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/NameCollisionPolicy.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/NameCollisionPolicy.java @@ -0,0 +1,6 @@ +package org.implab.gradle.variants.sources.internal; + +public enum NameCollisionPolicy { + FAIL, + RESOLVE +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/LayerConfigurationRegistry.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java rename from variants/src/main/java/org/implab/gradle/variants/sources/internal/LayerConfigurationRegistry.java rename to variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/internal/LayerConfigurationRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java @@ -1,15 +1,114 @@ package org.implab.gradle.variants.sources.internal; +import java.text.MessageFormat; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; 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; -public interface LayerConfigurationRegistry { +@NonNullByDefault +public class SourceSetConfigurationRegistry { + private static final Logger logger = Logging.getLogger(SourceSetConfigurationRegistry.class); + + private final Map> sourcesByLayer = new LinkedHashMap<>(); + private final Map> sourcesByVariant = new LinkedHashMap<>(); + private final Map> sourcesByUnit = new LinkedHashMap<>(); + + private final Supplier lateConfigurationMode; + + public SourceSetConfigurationRegistry(Supplier lateConfigurationMode) { + this.lateConfigurationMode = lateConfigurationMode; + } - void addLayerAction(Layer layer, Action action); + public void addLayerAction(Layer layer, Action action) { + addToActions( + sourcesByLayer.computeIfAbsent(layer, key -> new ReplayableQueue<>()), + action, + MessageFormat.format( + "Source sets for [layer={0}] layer already materialized", + layer.getName())); + } + + public void addVariantAction(Variant variant, Action action) { + addToActions( + sourcesByVariant.computeIfAbsent(variant, key -> new ReplayableQueue<>()), + action, + MessageFormat.format( + "Source sets for [variant={0}] variant already materialized", + variant.getName())); + + } + + public void addCompileUnitAction(CompileUnit unit, Action action) { + addToActions( + sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()), + action, + MessageFormat.format( + "Source set for [variant={0}, layer={1}] already materialed", + unit.variant().getName(), + unit.layer().getName())); + } - void applyLayer(Layer layer, GenericSourceSet sourceSet); + private void addToActions( + ReplayableQueue actions, + Action action, + String assertMessage) { + assertLazyConfiguration(actions.values(), assertMessage); + actions.forEach(action::execute); + } + + void assertLazyConfiguration(List sets, String message) { + if (sets.size() == 0) + return; + + var names = sets.stream().map(Named::getName).collect(Collectors.joining(", ")); + + switch (lateConfigurationMode.get()) { + case FAIL: + throw new IllegalStateException(message + " [" + names + "]"); + case WARN: + logger.warn(message + "\n\t" + names); + break; + default: + break; + } + } - void applyUnit(CompileUnit unit, GenericSourceSet sourceSet); + public void applyConfiguration(CompileUnit unit, GenericSourceSet sourceSet) { + sourcesByVariant.computeIfAbsent(unit.variant(), key -> new ReplayableQueue<>()).add(sourceSet); + sourcesByLayer.computeIfAbsent(unit.layer(), key -> new ReplayableQueue<>()).add(sourceSet); + sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()).add(sourceSet); + } + + class ReplayableQueue { + private final List> consumers = new LinkedList<>(); + private final List values = new LinkedList<>(); + + public void add(T value) { + consumers.forEach(consumer -> consumer.accept(value)); + values.add(value); + } + + List values() { + return List.copyOf(values); + } + + public void forEach(Consumer consumer) { + values.forEach(consumer); + consumers.add(consumer); + } + } } 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 new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetRegistry.java @@ -0,0 +1,46 @@ +package org.implab.gradle.variants.sources.internal; + +import java.util.HashMap; +import java.util.Map; +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; + +public class SourceSetRegistry { + private final Map> materialized = new HashMap<>(); + private final NamedDomainObjectContainer sourceSets; + private final ObjectFactory objectFactory; + + public SourceSetRegistry(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + this.sourceSets = objectFactory.domainObjectContainer(GenericSourceSet.class, this::createSourceSet); + } + + void forEachMaterialized(Consumer consumer) { + materialized.values().stream() + .filter(Deferred::resolved) + .map(Deferred::value) + .forEach(consumer); + } + + public NamedDomainObjectContainer sourceSets() { + return sourceSets; + } + + public void whenMaterialized(String name, Consumer consumer) { + materialized(name).whenResolved(consumer); + } + + private GenericSourceSet createSourceSet(String name) { + var sourceSet = objectFactory.newInstance(GenericSourceSet.class, name); + materialized(name).resolve(sourceSet); + return sourceSet; + } + + private Deferred materialized(String name) { + return materialized.computeIfAbsent(name, k -> new Deferred<>()); + } +} diff --git a/variants_variant_sources.md b/variants_variant_sources.md --- a/variants_variant_sources.md +++ b/variants_variant_sources.md @@ -454,7 +454,6 @@ Reasons: * the DSL is internal to source materialization * the source of truth for unit existence is already finalized in `VariantsView` * `GenericSourceSetMaterializer` returns `NamedDomainObjectProvider` -* late configuration of providers is expected and acceptable * adapters may need to refine source-related behavior after `variants` is finalized Therefore: @@ -471,6 +470,59 @@ It reflects the difference between: --- +## Late configuration policy + +Openness of `variantSources` does not mean that late configuration is +semantically neutral. + +Selector rules may be added after the finalized context becomes available, but +their behavior against already materialized `GenericSourceSet` objects must be +controlled explicitly. + +Conceptually, `variantSources` exposes a policy choice such as: + +```groovy +variantSources { + lateConfigurationPolicy { + failOnLateConfiguration() + } +} +``` + +Available modes are: + +* `failOnLateConfiguration()` +* `warnOnLateConfiguration()` +* `allowLateConfiguration()` + +Meaning: + +* `fail` rejects selector rules that target already materialized source sets +* `warn` allows them but emits a warning +* `allow` allows them silently + +This policy is intentionally modeled as an imperative choice, not as a mutable +property: + +* it must be chosen before the first selector rule is added +* selector rules here mean `variant(...)`, `layer(...)`, and `unit(...)` +* once chosen, it cannot be changed later +* it controls runtime behavior, not just a stored value + +For source sets configured before materialization, selector precedence remains: + +```text +variant < layer < unit +``` + +For already materialized source sets in `warn` and `allow` modes: + +* the late action is applied as an imperative follow-up step +* selector precedence is not reconstructed retroactively +* actual observation order is the order in which late actions are registered + +--- + ## `VariantSourcesContext` `variantSources.whenFinalized(...)` remains useful, but not because `variantSources` itself is frozen. @@ -676,7 +728,12 @@ The existence of compile units comes fro Adapters should not depend on raw Gradle lifecycle callbacks such as `afterEvaluate`. -### 5. Keep heavy runtime objects behind providers +### 5. Make late behavior explicit + +Late configuration after materialization is a policy decision, not an implicit +guarantee. + +### 6. Keep heavy runtime objects behind providers Materialized `GenericSourceSet` objects should remain behind a lazy API.