# HG changeset patch # User cin # Date 2026-03-26 08:12:10 # Node ID f19d2b751aa975baa35bf5958015b4052a5d974b # Parent 924d9107c025a02b5dd38992c00ec825b4320ee3 WIP almost stable variants model, working on variantSources diff --git a/variant_sources_precedence.md b/variant_sources_precedence.md new file mode 100644 --- /dev/null +++ b/variant_sources_precedence.md @@ -0,0 +1,210 @@ +# `variantSources`: selectors and precedence + +`variantSources` configures source-set materialization over the compile-unit space. + +A compile unit is defined as: + +- `(variant, layer)` + +This means: + +- `variant` defines compilation semantics +- `layer` defines compilation partitioning + +The `variantSources` DSL does not introduce a separate source model. +Instead, it provides configuration selectors over the existing compile-unit space. + +## Selectors + +Three selectors are available: + +- `variant(...)` +- `layer(...)` +- `unit(...)` + +They all target the same set of compile units, but at different levels of specificity. + +### `variant(...)` + +`variant(...)` applies configuration to all compile units that belong to the given variant. + +Example: + +```groovy +variantSources { + variant("browser") { + declareOutputs("js", "dts") + } +} +``` + +This affects all compile units of `browser`, for example: + +- `(browser, main)` +- `(browser, rjs)` +- `(browser, test)` + +Use this selector for variant-wide conventions. + +--- + +### `layer(...)` + +`layer(...)` applies configuration to all compile units that use the given layer. + +Example: + +```groovy +variantSources { + layer("main") { + set("ts") { + srcDir("src/main/ts") + } + } +} +``` + +This affects all compile units with layer `main`, for example: + +- `(browser, main)` +- `(nodejs, main)` +- `(electron, main)` + +Use this selector for cross-variant layer conventions. + +--- + +### `unit(...)` + +`unit(...)` applies configuration to one exact compile unit. + +Example: + +```groovy +variantSources { + unit("browser", "main") { + set("resources") { + srcDir("src/browserMain/resources") + } + } +} +``` + +This affects only: + +- `(browser, main)` + +Use this selector for the most specific adjustments. + +--- + +## Precedence + +For each compile unit, source-set configuration is applied in the following order: + +```text +variant < layer < unit +``` + +This means: + +1. `variant(...)` actions are applied first +2. `layer(...)` actions are applied next +3. `unit(...)` actions are applied last + +Each next level is allowed to refine or override the previous one. + +### Within the same level + +Within the same selector level, actions are applied in registration order. + +For example, if two plugins both configure `layer("main")`, their actions are applied in the same order in which they were registered. + +--- + +## Example + +```groovy +variantSources { + variant("browser") { + declareOutputs("js", "dts") + } + + layer("main") { + set("ts") { + srcDir("src/main/ts") + } + } + + unit("browser", "main") { + set("resources") { + srcDir("src/browserMain/resources") + } + } +} +``` + +For compile unit `(browser, main)` the effective configuration is built in this order: + +1. `variant("browser")` +2. `layer("main")` +3. `unit("browser", "main")` + +For compile unit `(browser, rjs)` the effective configuration is built in this order: + +1. `variant("browser")` +2. `layer("rjs")` if present +3. `unit("browser", "rjs")` if present + +For compile unit `(nodejs, main)` the effective configuration is built in this order: + +1. `variant("nodejs")` if present +2. `layer("main")` +3. `unit("nodejs", "main")` if present + +--- + +## Model boundary + +These selectors do not define compile units. +Compile units are derived from finalized `variants`. + +`variantSources` only configures how source sets are materialized for those units. + +This means: + +- `variants` is the source of truth for compile-unit existence +- `variantSources` is the source of truth for compile-unit source-set configuration + +--- + +## Operational semantics + +The `variantSources` API is exposed through a finalized context. + +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. + +This guarantees that: + +- selector precedence is stable +- registration order is preserved +- configuration does not depend on the materialization moment +- adapters do not need to depend on raw Gradle lifecycle timing + +--- + +## Summary + +- compile unit space is `(variant, layer)` +- `variant(...)`, `layer(...)`, and `unit(...)` are selectors over that space +- precedence is: + +```text +variant < layer < unit +``` + +- registration order is preserved within the same selector level +- `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,35 +1,12 @@ package org.implab.gradle.variants; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Stream; - import org.eclipse.jdt.annotation.NonNullByDefault; -import org.gradle.api.Action; -import org.gradle.api.NamedDomainObjectCollection; -import org.gradle.api.NamedDomainObjectContainer; -import org.gradle.api.NamedDomainObjectProvider; 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.model.Layer; -import org.implab.gradle.variants.model.Variant; -import org.implab.gradle.variants.model.VariantsExtension; -import org.implab.gradle.variants.sources.LayerProjectionRule; -import org.implab.gradle.variants.sources.SourceSetMaterializer; -import org.implab.gradle.variants.sources.SourceSetProjection; -import org.implab.gradle.variants.sources.SourceSetProjections; -import org.implab.gradle.variants.sources.VariantLayerBinding; +import org.implab.gradle.variants.core.VariantsExtension; import org.implab.gradle.variants.sources.VariantSourcesContext; -import org.implab.gradle.variants.sources.VariantSourcesExtension; @NonNullByDefault public abstract class VariantSourcesPlugin implements Plugin { @@ -45,141 +22,7 @@ public abstract class VariantSourcesPlug var sources = SourcesPlugin.getSourcesExtension(target); var deferred = new Deferred(); - var layerProjectionRules = objectFactory.domainObjectContainer(LayerProjectionRule.class); - var variantSourcesExtension = new VariantSourcesExtension() { - @Override - public NamedDomainObjectContainer getLayerRules() { - return layerProjectionRules; - } - - @Override - public void whenFinalized(Action action) { - deferred.whenResolved(action::execute); - } - }; - target.getExtensions().add(VariantSourcesExtension.class, "variantSources", variantSourcesExtension); - - // create convention to automatically create layer projection rules for each - // variant layer - variantsExtension.getLayers().all(layer -> { - // Automatically create a layer projection rule for each variant layer. - variantSourcesExtension.layer(layer.getName(), rule -> { - // Configure the source set name pattern based on the layer name. - rule.getSourceSetNamePattern() - .convention("{variant}{layerCapitalized}") - .finalizeValueOnRead(); - }); - }); - - var projections = objectFactory.domainObjectContainer(SourceSetProjection.class); - - Map> projectionBindings = new HashMap<>(); - - var sourceSetProjections = new SourceSetProjections() { - @Override - public NamedDomainObjectCollection getProjections() { - return projections; - } - - @Override - public Set getBindings(String sourceSetName) { - return projectionBindings.getOrDefault(sourceSetName, Set.of()); - } - - @Override - public Set getBindings(SourceSetProjection projection) { - return getBindings(projection.getName()); - } - }; - - Set materializedSourceSets = new HashSet<>(); - - var materializer = new SourceSetMaterializer() { - @Override - public NamedDomainObjectProvider getSourceSet(String sourceSetName) { - return materializedSourceSets.add(sourceSetName) - ? sources.register(sourceSetName) - : sources.named(sourceSetName); - } - }; - - var bindings = new VariantBindings(); - - target.afterEvaluate(t -> { - // Once the project is evaluated, resolve the deferred context and finalize the - // sources configuration. - variantsExtension.getLayers().all(bindings::addLayer); - variantsExtension.getVariants().all(bindings::addVariant); - - variantsExtension.getLayers().all(layer -> { - // For each layer, apply the projection rules to generate source set projections. - - var rule = layerProjectionRules.maybeCreate(layer.getName()); - var pattern = rule.getSourceSetNamePattern().getOrElse("{variant}{layerCapitalized}"); - // Generate source set names based on the pattern and variant/layer information. - // This is a simplified example; real implementation would need to consider - // all variants and layers. - var sourceSetName = pattern.replace("{layer}", layer.getName()) - .replace("{variant}", "main") // Placeholder for actual variant name - .replace("{layerCapitalized}", Strings.capitalize(layer.getName())); - - var projection = objectFactory.newInstance(SourceSetProjection.class, sourceSetName); - projections.add(projection); - // Bind the projection to the corresponding variant layer. - projectionBindings.computeIfAbsent(sourceSetName, k -> new HashSet<>()) - .add(new VariantLayerBinding(layer.getName(), projection)); - }); - - var context = new VariantSourcesContext() { - - @Override - public SourceSetProjections getProjections() { - return sourceSetProjections; - } - - @Override - public SourceSetMaterializer getMaterializer() { - return materializer; - } - - // Implementation of the context that provides access to variant and layer - // information. - }; - deferred.resolve(context); - }); - } - - class VariantBindings { - private final Set layers = new HashSet<>(); - private final Set variants = new HashSet<>(); - - private final List> listeners = new ArrayList<>(); - - void addLayer(Layer layer) { - layers.add(layer); - variants.stream() - .map(variant -> VariantLayerBinding.of(variant, layer)) - .forEach(this::notifyBindingAdded); - } - - void addVariant(Variant variant) { - variants.add(variant); - layers.stream() - .map(layer -> VariantLayerBinding.of(variant, layer)) - .forEach(this::notifyBindingAdded); - } - - void whenBindingAdded(Consumer listener) { - layers.stream() - .flatMap(layer -> variants.stream().map(variant -> VariantLayerBinding.of(variant, layer))) - .forEach(listener); - listeners.add(listener); - } - - private void notifyBindingAdded(VariantLayerBinding binding) { - listeners.forEach(listener -> listener.accept(binding)); - } } } 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 @@ -1,15 +1,101 @@ package org.implab.gradle.variants; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.implab.gradle.variants.model.VariantsExtension; +import org.gradle.api.model.ObjectFactory; +import org.implab.gradle.common.core.lang.Deferred; +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Role; +import org.implab.gradle.variants.core.Variant; +import org.implab.gradle.variants.core.VariantDefinition; +import org.implab.gradle.variants.core.VariantsExtension; +import org.implab.gradle.variants.core.VariantsView; +/** + *
    + *
  • {@link Variant} defines compilation semantics + *
  • {@link Layer} defines compilation partition + *
  • {@link Role} defines result grouping / publication intent + *
+ * + */ public abstract class VariantsPlugin implements Plugin { @Override public void apply(Project target) { - target.getExtensions().create("variants", VariantsExtension.class); + var objectFactory = target.getObjects(); + + var variantsExtension = new DefaultVariantsExtension(objectFactory); + target.getExtensions().add(VariantsExtension.class, "variants", variantsExtension); + target.afterEvaluate(t -> variantsExtension.finalizeExtension()); } + static class DefaultVariantsExtension implements VariantsExtension { + + private final NamedDomainObjectContainer layers; + private final NamedDomainObjectContainer roles; + private final NamedDomainObjectContainer variantDefinitions; + private final Deferred finalizedResult = new Deferred<>(); + private final ObjectFactory objectFactory; + private boolean finalized = false; + + public DefaultVariantsExtension(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + this.layers = objectFactory.domainObjectContainer(Layer.class); + this.roles = objectFactory.domainObjectContainer(Role.class); + this.variantDefinitions = objectFactory.domainObjectContainer(VariantDefinition.class); + } + + @Override + public NamedDomainObjectContainer getLayers() { + return layers; + } + + @Override + public NamedDomainObjectContainer getRoles() { + return roles; + } + + @Override + public NamedDomainObjectContainer getVariantDefinitions() { + return variantDefinitions; + } + + @Override + public void whenFinalized(Action action) { + finalizedResult.whenResolved(action::execute); + } + + void finalizeExtension() { + if (finalized) + return; + + finalized = true; + + // freeze defined variants + variantDefinitions.forEach(VariantDefinition::finalizeVariant); + + // build a snapshot + var viewBuilder = VariantsView.builder(); + + // calculate and add variants + variantDefinitions.stream() + .map(def -> objectFactory.named(Variant.class, def.getName())) + .forEach(viewBuilder::addVariant); + // add layers + layers.forEach(viewBuilder::addLayer); + // add roles + roles.forEach(viewBuilder::addRole); + // add definitions + variantDefinitions.forEach(viewBuilder::addDefinition); + // assemble the view + var view = viewBuilder.build(); + // set the result and call hooks + finalizedResult.resolve(view); + } + } + } diff --git a/variants/src/main/java/org/implab/gradle/variants/model/Layer.java b/variants/src/main/java/org/implab/gradle/variants/core/Layer.java rename from variants/src/main/java/org/implab/gradle/variants/model/Layer.java rename to variants/src/main/java/org/implab/gradle/variants/core/Layer.java --- a/variants/src/main/java/org/implab/gradle/variants/model/Layer.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/Layer.java @@ -1,4 +1,4 @@ -package org.implab.gradle.variants.model; +package org.implab.gradle.variants.core; import org.gradle.api.Named; diff --git a/variants/src/main/java/org/implab/gradle/variants/model/Role.java b/variants/src/main/java/org/implab/gradle/variants/core/Role.java rename from variants/src/main/java/org/implab/gradle/variants/model/Role.java rename to variants/src/main/java/org/implab/gradle/variants/core/Role.java --- a/variants/src/main/java/org/implab/gradle/variants/model/Role.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/Role.java @@ -1,4 +1,4 @@ -package org.implab.gradle.variants.model; +package org.implab.gradle.variants.core; import org.gradle.api.Named; diff --git a/variants/src/main/java/org/implab/gradle/variants/model/RoleLayerBinding.java b/variants/src/main/java/org/implab/gradle/variants/core/RoleLayerBinding.java rename from variants/src/main/java/org/implab/gradle/variants/model/RoleLayerBinding.java rename to variants/src/main/java/org/implab/gradle/variants/core/RoleLayerBinding.java --- a/variants/src/main/java/org/implab/gradle/variants/model/RoleLayerBinding.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/RoleLayerBinding.java @@ -1,8 +1,8 @@ -package org.implab.gradle.variants.model; +package org.implab.gradle.variants.core; /** A binding between a role and a layer inside a specific variant. * * @see {@link VariantDefinition} for the context of this binding. */ -public record RoleLayerBinding(String name, String layerName) { +public record RoleLayerBinding(String roleName, String layerName) { } \ 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/core/Variant.java rename from variants/src/main/java/org/implab/gradle/variants/model/Variant.java rename to variants/src/main/java/org/implab/gradle/variants/core/Variant.java --- a/variants/src/main/java/org/implab/gradle/variants/model/Variant.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/Variant.java @@ -1,4 +1,4 @@ -package org.implab.gradle.variants.model; +package org.implab.gradle.variants.core; import org.gradle.api.Named; diff --git a/variants/src/main/java/org/implab/gradle/variants/model/VariantDefinition.java b/variants/src/main/java/org/implab/gradle/variants/core/VariantDefinition.java rename from variants/src/main/java/org/implab/gradle/variants/model/VariantDefinition.java rename to variants/src/main/java/org/implab/gradle/variants/core/VariantDefinition.java --- a/variants/src/main/java/org/implab/gradle/variants/model/VariantDefinition.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/VariantDefinition.java @@ -1,4 +1,4 @@ -package org.implab.gradle.variants.model; +package org.implab.gradle.variants.core; import java.util.HashSet; import java.util.Set; diff --git a/variants/src/main/java/org/implab/gradle/variants/model/VariantsExtension.java b/variants/src/main/java/org/implab/gradle/variants/core/VariantsExtension.java rename from variants/src/main/java/org/implab/gradle/variants/model/VariantsExtension.java rename to variants/src/main/java/org/implab/gradle/variants/core/VariantsExtension.java --- a/variants/src/main/java/org/implab/gradle/variants/model/VariantsExtension.java +++ b/variants/src/main/java/org/implab/gradle/variants/core/VariantsExtension.java @@ -1,4 +1,4 @@ -package org.implab.gradle.variants.model; +package org.implab.gradle.variants.core; import org.gradle.api.Action; import org.gradle.api.NamedDomainObjectContainer; @@ -19,6 +19,7 @@ import groovy.lang.Closure; * } * } * } + * */ public interface VariantsExtension { @@ -33,11 +34,6 @@ public interface VariantsExtension { NamedDomainObjectContainer getRoles(); /** - * Domain of variants. - */ - NamedDomainObjectContainer getVariants(); - - /** * Declared variants. */ NamedDomainObjectContainer getVariantDefinitions(); @@ -46,7 +42,6 @@ public interface VariantsExtension { * Creates or returns an existing variant and configures it. */ default VariantDefinition variant(String name) { - return getVariantDefinitions().maybeCreate(name); } @@ -62,4 +57,18 @@ public interface VariantsExtension { default VariantDefinition variant(String name, Closure closure) { return variant(name, Closures.action(closure)); } + + /** + * Registers an action to be executed when the extension is finalized. + * + * The specified action will receive the unmodifiable snapshot of the variants. + * + * @param action The callback to be executed when variants are finalized + */ + void whenFinalized(Action action); + + default void whenFinalized(Closure action) { + whenFinalized(Closures.action(action)); + } + } \ No newline at end of file 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 new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/core/VariantsView.java @@ -0,0 +1,151 @@ +package org.implab.gradle.variants.core; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.gradle.api.InvalidUserDataException; + +@NonNullByDefault +public class VariantsView { + private final Set layers; + private final Set roles; + private final Set variants; + private final Set entries; + + private final Map> entriesByVariant; + private final Map> entriesByRole; + private final Map> entriesByLayer; + + private VariantsView(Set layers, Set roles, Set variants, Set entries) { + this.layers = layers; + this.roles = roles; + this.variants = variants; + this.entries = entries; + this.entriesByVariant = entries.stream() + .collect(Collectors.groupingBy(VariantRoleLayer::variant, Collectors.toSet())); + this.entriesByRole = entries.stream() + .collect(Collectors.groupingBy(VariantRoleLayer::role, Collectors.toSet())); + this.entriesByLayer = entries.stream() + .collect(Collectors.groupingBy(VariantRoleLayer::layer, Collectors.toSet())); + } + + public Set getLayers() { + return layers; + } + + public Set getRoles() { + return roles; + } + + public Set getVariants() { + return variants; + } + + public Set getEntries() { + return entries; + } + + public Set getEntriesForVariant(Variant variant) { + return entriesByVariant.getOrDefault(variant, Set.of()); + } + + public Set getEntriesForLayer(Layer layer) { + return entriesByLayer.getOrDefault(layer, Set.of()); + } + + public Set getEntriesForRole(Role role) { + return entriesByRole.getOrDefault(role, Set.of()); + } + + public record VariantRoleLayer(Variant variant, Role role, Layer layer) { + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final Map layers = new LinkedHashMap<>(); + private final Map roles = new LinkedHashMap<>(); + private final Map variants = new LinkedHashMap<>(); + private final List definitions = new ArrayList<>(); + + private Builder() { + } + + public Builder addRole(Role role) { + Objects.requireNonNull(role, "role can't be null"); + roles.put(role.getName(), role); + return this; + } + + public Builder addLayer(Layer layer) { + Objects.requireNonNull(layer, "layer can't be null"); + layers.put(layer.getName(), layer); + return this; + } + + public Builder addVariant(Variant variant) { + Objects.requireNonNull(variant, "variant can't be null"); + variants.put(variant.getName(), variant); + return this; + } + + public Builder addDefinition(VariantDefinition definition) { + Objects.requireNonNull(definition, "definition can't be null"); + definitions.add(definition); + return this; + } + + public VariantsView build() { + + var entries = definitions.stream() + .flatMap(def -> def.getRoleBindings().get().stream() + .map(layerRole -> createVariantRoleLayer( + def.getName(), // variantName + layerRole.roleName(), + layerRole.layerName()))) + .collect(Collectors.toSet()); + + return new VariantsView( + Set.copyOf(layers.values()), + Set.copyOf(roles.values()), + Set.copyOf(variants.values()), + entries); + } + + private VariantRoleLayer createVariantRoleLayer(String variantName, String roleName, String layerName) { + return new VariantRoleLayer( + resolveVariant(variantName, + "Variant '" + variantName + "' isn't declared"), + resolveRole(roleName, + "Role '" + roleName + "' isn't declared, referenced in '" + variantName + "' variant"), + resolveLayer(layerName, + "Layer '" + layerName + "' isn't declared, referenced in '" + variantName + + "' variant with '" + roleName + "' role")); + } + + private Layer resolveLayer(String layerName, String errorMessage) { + return Optional.ofNullable(layers.get(layerName)) + .orElseThrow(() -> new InvalidUserDataException(errorMessage)); + } + + private Variant resolveVariant(String variantName, String errorMessage) { + return Optional.ofNullable(variants.get(variantName)) + .orElseThrow(() -> new InvalidUserDataException(errorMessage)); + } + + private Role resolveRole(String roleName, String errorMessage) { + return Optional.ofNullable(roles.get(roleName)) + .orElseThrow(() -> new InvalidUserDataException(errorMessage)); + } + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/derived/CompileUnit.java b/variants/src/main/java/org/implab/gradle/variants/derived/CompileUnit.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/derived/CompileUnit.java @@ -0,0 +1,7 @@ +package org.implab.gradle.variants.derived; + +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Variant; + +public record CompileUnit(Variant variant, Layer layer) { +} diff --git a/variants/src/main/java/org/implab/gradle/variants/derived/CompileUnitsView.java b/variants/src/main/java/org/implab/gradle/variants/derived/CompileUnitsView.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/derived/CompileUnitsView.java @@ -0,0 +1,32 @@ +package org.implab.gradle.variants.derived; + +import java.util.Optional; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Role; +import org.implab.gradle.variants.core.Variant; + +@NonNullByDefault +public interface CompileUnitsView { + Set getUnits(); + + Set getUnitsForVariant(Variant variant); + + Optional findUnit(Variant variant, Layer layer); + + default CompileUnit getUnit(Variant variant, Layer layer) { + return findUnit(variant, layer) + .orElseThrow(() -> new IllegalArgumentException( + "Compile unit for variant '" + variant.getName() + + "' and layer '" + layer.getName() + "' not found")); + } + + boolean contains(Variant variant, Layer layer); + + /** + * In which logical roles this compile unit participates. + */ + Set getRoles(CompileUnit unit); +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/derived/RoleProjection.java b/variants/src/main/java/org/implab/gradle/variants/derived/RoleProjection.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/derived/RoleProjection.java @@ -0,0 +1,7 @@ +package org.implab.gradle.variants.derived; + +import org.implab.gradle.variants.core.Role; +import org.implab.gradle.variants.core.Variant; + +public record RoleProjection(Variant variant, Role role) { +} diff --git a/variants/src/main/java/org/implab/gradle/variants/derived/RoleProjectionsView.java b/variants/src/main/java/org/implab/gradle/variants/derived/RoleProjectionsView.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/derived/RoleProjectionsView.java @@ -0,0 +1,35 @@ +package org.implab.gradle.variants.derived; + +import java.util.Optional; +import java.util.Set; + +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Role; +import org.implab.gradle.variants.core.Variant; + +public interface RoleProjectionsView { + Set getProjections(); + + Set getProjectionsForVariant(Variant variant); + + Set getProjectionsForRole(Role role); + + Optional findProjection(Variant variant, Role role); + + default RoleProjection getProjection(Variant variant, Role role) { + return findProjection(variant, role) + .orElseThrow(() -> new IllegalArgumentException( + "Role projection for variant '" + variant.getName() + + "' and role '" + role.getName() + "' not found")); + } + + boolean contains(Variant variant, Role role); + + Set getUnits(RoleProjection projection); + + default Set getLayers(RoleProjection projection) { + return getUnits(projection).stream() + .map(CompileUnit::layer) + .collect(java.util.stream.Collectors.toUnmodifiableSet()); + } +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/LayerProjectionRule.java b/variants/src/main/java/org/implab/gradle/variants/sources/LayerProjectionRule.java deleted file mode 100644 --- a/variants/src/main/java/org/implab/gradle/variants/sources/LayerProjectionRule.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.implab.gradle.variants.sources; - -import org.gradle.api.Action; -import org.gradle.api.Named; -import org.gradle.api.provider.Property; - -/** - * Projection rule for a layer. - */ -public interface LayerProjectionRule extends Named { - - /** - * Pattern used to calculate the source set name. - * Examples: - * "{layer}" - * "{variant}{layerCapitalized}" - */ - Property getSourceSetNamePattern(); - - /** - * Optional hook for future extension. - */ - default void configure(Action action) { - action.execute(this); - } -} \ 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 @@ -2,14 +2,17 @@ package org.implab.gradle.variants.sourc import org.gradle.api.NamedDomainObjectProvider; import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.variants.derived.CompileUnit; /** - * Materializes symbolic source set names into actual GenericSourceSet instances. + * Materializes symbolic source set names into actual GenericSourceSet + * instances. */ public interface SourceSetMaterializer { - NamedDomainObjectProvider getSourceSet(String sourceSetName); - - default NamedDomainObjectProvider getSourceSet(SourceSetProjection projection) { - return getSourceSet(projection.getName()); - } + /** + * Returns a lazy provider for the source set corresponding to the compile unit. + * + * The provider is stable and cached per compile unit. + */ + NamedDomainObjectProvider getSourceSet(CompileUnit unit); } \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjection.java b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjection.java deleted file mode 100644 --- a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjection.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.implab.gradle.variants.sources; - -import org.gradle.api.Named; - -/** - * Represents a projected source set. This is an identity object and doesn't contain any state. - * The name of the projection is used as the source set name by the {@link SourceSetMaterializer}. - */ -public interface SourceSetProjection extends Named { -} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjections.java b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjections.java deleted file mode 100644 --- a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjections.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.implab.gradle.variants.sources; - -import java.util.Set; - -import org.gradle.api.NamedDomainObjectCollection; - -/** - * Registry of symbolic source set names produced by sources projection. - * - * Identity in this registry is the GenericSourceSet name. - */ -public interface SourceSetProjections { - - /** - * Returns all source set projections. This is a separate - */ - NamedDomainObjectCollection getProjections(); - - /** - * Returns all logical bindings projected into the given source set name. - */ - Set getBindings(String sourceSetName); - - /** - * Returns all logical bindings projected into the given source set name. - */ - Set getBindings(SourceSetProjection projection); -} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantLayerBinding.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantLayerBinding.java deleted file mode 100644 --- a/variants/src/main/java/org/implab/gradle/variants/sources/VariantLayerBinding.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.implab.gradle.variants.sources; - -import org.implab.gradle.variants.model.Layer; -import org.implab.gradle.variants.model.Variant; - -/** - * Logical usage of a layer inside a variant. - * Identity: (variantName, layerName) - */ -public interface VariantLayerBinding { - Variant getVariant(); - Layer getLayer(); - - public static VariantLayerBinding of(Variant variant, Layer layer) { - return new VariantLayerBinding() { - @Override - public Variant getVariant() { - return variant; - } - - @Override - public Layer getLayer() { - return layer; - } - }; - } -} \ No newline at end of file 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,50 @@ package org.implab.gradle.variants.sources; +import java.util.Set; + +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectCollection; +import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.VariantsView; +import org.implab.gradle.variants.derived.CompileUnitsView; +import org.implab.gradle.variants.derived.RoleProjectionsView; + +/** + * Registry of symbolic source set names produced by sources projection. + * + * Identity in this registry is the GenericSourceSet name. + */ public interface VariantSourcesContext { - SourceSetProjections getProjections(); + + /** + * Finalized core model. + */ + VariantsView getVariants(); - SourceSetMaterializer getMaterializer(); -} + /** + * Derived compile-side view. + */ + CompileUnitsView getCompileUnits(); + + /** + * Derived role-side view. + */ + RoleProjectionsView getRoleProjections(); + + /** + * Lazy source set provider service. + */ + SourceSetMaterializer getSourceSets(); + + /** + * Configures all GenericSourceSets produced from the given layer. + * + * The action is applied: + * - to already materialized source sets of this layer + * - to all future source sets of this layer + * + * Actions are applied in registration order. + */ + void configureLayer(Layer layer, 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,7 +1,6 @@ package org.implab.gradle.variants.sources; import org.gradle.api.Action; -import org.gradle.api.NamedDomainObjectContainer; import org.implab.gradle.common.core.lang.Closures; import groovy.lang.Closure; @@ -9,26 +8,12 @@ import groovy.lang.Closure; public interface VariantSourcesExtension { /** - * Projection rules keyed by layer name. - */ - NamedDomainObjectContainer getLayerRules(); - - /** - * Creates or returns an existing layer rule and configures it. + * Invoked when finalized variants-derived source context becomes available. + * + * Replayable: + * - if called before variants finalization, action is queued + * - if called after variants finalization, action is invoked immediately */ - default LayerProjectionRule layerRule(String name) { - return getLayerRules().maybeCreate(name); - } - - /** - * Creates or returns an existing layer rule and configures it. - */ - default LayerProjectionRule layer(String name, Action action) { - LayerProjectionRule rule = layerRule(name); - action.execute(rule); - return rule; - } - void whenFinalized(Action action); default void whenFinalized(Closure closure) { 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 new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/DefaultLayerConfigurationRegistry.java @@ -0,0 +1,39 @@ +package org.implab.gradle.variants.sources.internal; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.gradle.api.Action; +import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.derived.CompileUnit; + +public class DefaultLayerConfigurationRegistry implements LayerConfigurationRegistry { + private final Map>> actionsByLayer = new LinkedHashMap<>(); + + @Override + public void add(Layer layer, Action action) { + Objects.requireNonNull(layer, "layer can't be null"); + Objects.requireNonNull(action, "action can't be null"); + actionsByLayer.computeIfAbsent(layer, 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/LayerConfigurationRegistry.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/LayerConfigurationRegistry.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/LayerConfigurationRegistry.java @@ -0,0 +1,15 @@ +package org.implab.gradle.variants.sources.internal; + +import org.gradle.api.Action; +import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.derived.CompileUnit; + +public interface LayerConfigurationRegistry { + + void add(Layer layer, Action action); + + void applyLayer(Layer layer, GenericSourceSet sourceSet); + + void applyUnit(CompileUnit unit, GenericSourceSet sourceSet); +} diff --git a/variants_variant_sources.md b/variants_variant_sources.md new file mode 100644 --- /dev/null +++ b/variants_variant_sources.md @@ -0,0 +1,719 @@ +# Variants and Variant Sources + +## Overview + +This document describes a two-layer model for build variants: + +- `variants` defines the **core domain model** +- `variantSources` defines **source materialization semantics** for that model + +The main goal is to keep the core model small, explicit, and stable, while allowing source-related behavior to remain flexible and adapter-friendly. + +The model is intentionally split into: + +1. a **closed, finalized domain model** +2. an **open, runtime-oriented source materialization model** + +This separation is important because compilation, source aggregation, publication, and adapter-specific behavior do not belong to the same abstraction layer. + +--- + +## Core idea + +The `variants` model is based on three independent domains: + +- `Layer` +- `Role` +- `Variant` + +A finalized `VariantsView` contains the normalized relation: + +- `(variant, role, layer)` + +This relation is the source of truth. + +Everything else is derived from it. + +--- + +## `variants`: the core domain model + +### Purpose + +`variants` describes: + +- what layers exist +- what roles exist +- what variants exist +- which `(variant, role, layer)` combinations are valid + +It does **not** describe: + +- source directories +- source roots +- source set materialization +- compilation tasks +- publication mechanics +- source set inheritance +- layer merge behavior for a concrete toolchain + +Those concerns are intentionally outside the core model. + +--- + +## Core DSL example + +```groovy +variants { + layers { + main() + test() + generated() + rjs() + cjs() + } + + roles { + production() + test() + tool() + } + + variant("browser") { + role("production") { + layers("main", "generated", "rjs") + } + role("test") { + layers("main", "test", "generated", "rjs") + } + } + + variant("nodejs") { + role("production") { + layers("main", "generated", "cjs") + } + role("test") { + layers("main", "test", "generated", "cjs") + } + role("tool") { + layers("main", "generated", "cjs") + } + } +} +``` + +### Interpretation + +This example means: + +* `browser` production uses `main`, `generated`, `rjs` +* `browser` test uses `main`, `test`, `generated`, `rjs` +* `nodejs` production uses `main`, `generated`, `cjs` +* `nodejs` test uses `main`, `test`, `generated`, `cjs` +* `nodejs` tool uses `main`, `generated`, `cjs` + +The model is purely declarative. + +--- + +## Identity and references + +`Layer`, `Role`, and `Variant` are identity objects. + +They exist as declared domain values. + +References between model elements are symbolic: + +* layers are referenced by layer name +* roles are referenced by role name +* variants are referenced by variant name + +This is intentional. + +The core model is declarative, not navigation-oriented. + +It is acceptable for aggregates to hold symbolic references to foreign domain values, as long as identity is clearly defined and validated later. + +--- + +## Finalization + +The `variants` model is finalized once. + +Finalization is an internal lifecycle transition. It is typically triggered privately, for example from `afterEvaluate`, but that mechanism is not part of the public API contract. + +The public contract is: + +* `variants.whenFinalized(...)` + +This callback is **replayable**: + +* if called before finalization, the action is queued +* if called after finalization, the action is invoked immediately + +The callback receives a finalized, read-only view of the model. + +Example: + +```java +variants.whenFinalized(view -> { + // use finalized VariantsView here +}); +``` + +--- + +## `VariantsView` + +`VariantsView` is the finalized representation of the core model. + +It contains: + +* all declared `Layer` +* all declared `Role` +* all declared `Variant` +* all normalized entries `(variant, role, layer)` + +Conceptually: + +```java +interface VariantsView { + Set getLayers(); + Set getRoles(); + Set getVariants(); + Set getEntries(); +} +``` + +Where: + +```java +record VariantRoleLayer(Variant variant, Role role, Layer layer) {} +``` + +This view is: + +* immutable +* normalized +* validated +* independent from DSL internals + +--- + +## Derived views + +Two important views can be derived from `VariantsView`: + +* `CompileUnitsView` +* `RoleProjectionsView` + +These views are not part of the raw core model itself, but they are naturally derived from it. + +--- + +## `CompileUnitsView` + +### Purpose + +A compile unit is defined as: + +* `(variant, layer)` + +This is based on the following rationale: + +* `variant` defines compilation semantics +* `layer` partitions a variant into separate compilation units +* `role` is not a compilation boundary + +This is especially useful for toolchains such as TypeScript, where compilation is often more practical or more correct per layer than for the whole variant at once. + +### Example + +From: + +* `(browser, production, main)` +* `(browser, production, rjs)` +* `(browser, test, main)` +* `(browser, test, test)` +* `(browser, test, rjs)` + +we derive compile units: + +* `(browser, main)` +* `(browser, rjs)` +* `(browser, test)` + +### Conceptual API + +```java +interface CompileUnitsView { + Set getUnits(); + Set getUnitsForVariant(Variant variant); + boolean contains(Variant variant, Layer layer); + Set getRoles(CompileUnit unit); +} + +record CompileUnit(Variant variant, Layer layer) {} +``` + +### Meaning + +`CompileUnitsView` answers: + +* what can be compiled +* how a variant is partitioned into compile units +* which logical roles include a given compile unit + +--- + +## `RoleProjectionsView` + +### Purpose + +A role projection is defined as: + +* `(variant, role)` + +This is based on the following rationale: + +* `role` is not about compilation +* `role` groups compile units by purpose +* roles are more closely related to publication, aggregation, assembly, or result grouping + +### Example + +For `browser`: + +* `production` includes compile units: + + * `(browser, main)` + * `(browser, rjs)` + +* `test` includes compile units: + + * `(browser, main)` + * `(browser, test)` + * `(browser, rjs)` + +### Conceptual API + +```java +interface RoleProjectionsView { + Set getProjections(); + Set getProjectionsForVariant(Variant variant); + Set getProjectionsForRole(Role role); + Set getUnits(RoleProjection projection); +} + +record RoleProjection(Variant variant, Role role) {} +``` + +### Meaning + +`RoleProjectionsView` answers: + +* how compile units are grouped by purpose +* what belongs to `production`, `test`, `tool`, etc. +* what should be aggregated or published together + +--- + +## Why `CompileUnitsView` and `RoleProjectionsView` are not part of `VariantsView` + +`VariantsView` is intentionally minimal. + +It expresses the domain relation: + +* `(variant, role, layer)` + +`CompileUnitsView` and `RoleProjectionsView` are **derived interpretations** of that relation. + +They are natural and useful, but they are still interpretations: + +* `CompileUnit = (variant, layer)` +* `RoleProjection = (variant, role)` + +This is why they are better treated as derived views rather than direct core model primitives. + +--- + +## `variantSources`: source semantics for layers + +### Purpose + +`variantSources` does **not** define variants. + +It defines how a declared `Layer` contributes sources. + +In other words: + +* `variants` defines **what exists** +* `variantSources` defines **how layers become source inputs** + +This distinction is important. + +`variantSources` does not own the variant model. It interprets it. + +--- + +## Main idea + +A layer source rule describes the source contribution of a layer. + +This is independent of any concrete variant or role. + +Conceptually: + +* `Layer -> source contribution rule` + +Examples of source contribution semantics: + +* base directory +* source directories +* logical source kinds (`ts`, `js`, `resources`) +* declared outputs (`js`, `dts`, `resources`) + +--- + +## `variantSources` DSL example + +```groovy +variantSources { + layerRule("main") { + from("src/main") + + set("ts") { + srcDir("ts") + } + set("js") { + srcDir("js") + } + set("resources") { + srcDir("resources") + } + + outputs("js", "dts", "resources") + } + + layerRule("test") { + from("src/test") + + set("ts") { + srcDir("ts") + } + set("resources") { + srcDir("resources") + } + + outputs("js", "dts", "resources") + } + + layerRule("rjs") { + from("src/rjs") + + set("ts") { + srcDir("ts") + } + + outputs("js", "dts") + } + + layerRule("cjs") { + from("src/cjs") + + set("ts") { + srcDir("ts") + } + + outputs("js", "dts") + } +} +``` + +### Interpretation + +This means: + +* `main` contributes `ts`, `js`, and `resources` +* `test` contributes `ts` and `resources` +* `rjs` contributes `ts` +* `cjs` contributes `ts` + +These are layer rules only. + +They do not yet say which variant consumes them. + +--- + +## Why `variantSources` remains open + +Unlike `variants`, `variantSources` does not need to be closed in the same way. + +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: + +* `variants` is finalized +* `variantSources` may remain open + +This is not a contradiction. + +It reflects the difference between: + +* a closed domain model +* an open infrastructure/materialization model + +--- + +## `VariantSourcesContext` + +`variantSources.whenFinalized(...)` remains useful, but not because `variantSources` itself is frozen. + +Its purpose is to provide access to a finalized context derived from `variants`. + +This context contains: + +* `CompileUnitsView` +* `RoleProjectionsView` +* `GenericSourceSetMaterializer` + +Conceptually: + +```java +interface VariantSourcesContext { + CompileUnitsView getCompileUnits(); + RoleProjectionsView getRoleProjections(); + GenericSourceSetMaterializer getSourceSets(); +} +``` + +This callback is also replayable. + +Example: + +```java +variantSources.whenFinalized(ctx -> { + var units = ctx.getCompileUnits(); + var roles = ctx.getRoleProjections(); + var sourceSets = ctx.getSourceSets(); +}); +``` + +--- + +## `GenericSourceSetMaterializer` + +### Purpose + +`GenericSourceSetMaterializer` is the official source of truth for materialized source sets. + +It is responsible for: + +* lazy creation of `GenericSourceSet` +* applying `layerRule` +* connecting a compile unit to a source set provider +* exposing source sets to adapters + +This is the correct place to apply `layerRule`. + +Adapters should not apply layer rules themselves. + +### Conceptual API + +```java +interface GenericSourceSetMaterializer { + NamedDomainObjectProvider getSourceSet(CompileUnit unit); +} +``` + +--- + +## Why `GenericSourceSetMaterializer` should own `layerRule` application + +If adapters applied `layerRule` directly, responsibility would leak across multiple layers: + +* one component would know compile units +* another would know source semantics +* another would know how to configure `GenericSourceSet` + +This would make the model harder to reason about. + +Instead: + +* `layerRule` is DSL/spec-level +* `GenericSourceSetMaterializer` is execution/materialization-level +* adapters are consumption-level + +This gives a much cleaner separation. + +--- + +## `GenericSourceSet` as materialization target + +`GenericSourceSet` is the materialized source aggregation object. + +It is a good fit because it can represent: + +* multiple logical source sets +* aggregated source directories +* declared outputs +* lazy registration through providers + +The materializer is therefore the owner of: + +* creating `GenericSourceSet` +* populating its source sets +* declaring outputs +* returning a provider for later use + +--- + +## How an adapter should use the model + +Example: + +```java +variantSources.whenFinalized(ctx -> { + for (CompileUnit unit : ctx.getCompileUnits().getUnits()) { + var sourceSetProvider = ctx.getSourceSets().getSourceSet(unit); + + var variant = unit.variant(); + var layer = unit.layer(); + + // create compile task for this compile unit + // configure compiler options from variant semantics + // use sourceSetProvider as task input + } + + for (RoleProjection projection : ctx.getRoleProjections().getProjections()) { + var units = ctx.getRoleProjections().getUnits(projection); + + // aggregate outputs of included compile units + // use for publication or assembly + } +}); +``` + +--- + +## Why compile unit is `(variant, layer)` and not `(variant, role)` or `(variant, role, layer)` + +### Not `(variant, role)` + +Because role is not a compilation boundary. + +Role is a logical grouping of results. + +### Not `(variant, role, layer)` + +Because role does not define the compile unit itself. + +A compile unit is a unit of compilation, not a unit of publication grouping. + +### Correct interpretation + +* `(variant, layer)` = compile unit +* `(variant, role)` = logical result group +* `(variant, role, layer)` = membership relation between them + +This is the most coherent separation. + +--- + +## Model boundaries + +### What belongs to `variants` + +* declared domains: `Layer`, `Role`, `Variant` +* normalized relation `(variant, role, layer)` +* finalization lifecycle +* finalized `VariantsView` + +### What belongs to derived views + +* compile units: `(variant, layer)` +* role projections: `(variant, role)` + +### What belongs to `variantSources` + +* source semantics of layers +* source materialization rules +* lazy `GenericSourceSet` provisioning +* source adapter integration + +### What does not belong to `variants` + +* source directories +* base paths +* output declarations +* source set layout +* task registration +* compiler-specific assumptions + +--- + +## Design principles + +### 1. Keep the core model small + +The core model should only contain domain facts. + +### 2. Separate domain truth from materialization + +The existence of compile units comes from `VariantsView`, not from source rules. + +### 3. Treat source materialization as infrastructure + +`variantSources` is an interpretation layer, not the source of truth. + +### 4. Prefer replayable finalized hooks + +Adapters should not depend on raw Gradle lifecycle callbacks such as `afterEvaluate`. + +### 5. Keep heavy runtime objects behind providers + +Materialized `GenericSourceSet` objects should remain behind a lazy API. + +--- + +## Summary + +The model is intentionally split into two layers. + +### `variants` + +A closed, finalized domain model: + +* `Layer` +* `Role` +* `Variant` +* `(variant, role, layer)` + +### `variantSources` + +An open, source-materialization layer: + +* layer source rules +* compile-unit source set materialization +* adapter-facing `GenericSourceSet` providers + +### Derived views + +From the finalized variant model: + +* `CompileUnitsView`: `(variant, layer)` +* `RoleProjectionsView`: `(variant, role)` + +### Operational interpretation + +* `variant` defines compilation semantics +* `layer` partitions compilation +* `role` groups results by purpose + +This keeps the core model stable and minimal, while allowing source handling and adapter integration to remain flexible.