# 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` * `SourceSetMaterializer` returns `NamedDomainObjectProvider` * 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 This openness is still constrained by explicit policy fixation points: * late-configuration policy is fixed when the first selector rule is registered * naming policy is fixed when the finalized `VariantSourcesContext` is created --- ## 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 * the enforcement point is the first selector registration itself, not variants finalization in isolation 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 --- ## Compile-unit naming policy Source-set naming is treated as a separate policy concern from selector registration. Conceptually, `variantSources` exposes: ```groovy variantSources { namingPolicy { failOnNameCollision() } } ``` The base projected name of a compile unit is: ```text variantName + capitalize(layerName) ``` Examples: * `(browser, main)` -> `browserMain` * `(browser, rjs)` -> `browserRjs` Available modes are: * `failOnNameCollision()` - reject finalized compile-unit models that project the same source-set name for different compile units * `resolveNameCollision()` - resolve such conflicts deterministically ### `resolveNameCollision()` semantics Conflicting compile units are ordered canonically by: ```text (variant.name, layer.name) ``` Within one conflicting group: * the first compile unit keeps the base name * the second gets suffix `2` * the third gets suffix `3` * and so on For example, if: * `(foo, variantBar)` projects to `fooVariantBar` * `(fooVariant, bar)` also projects to `fooVariantBar` then canonical ordering yields: * `(foo, variantBar)` -> `fooVariantBar` * `(fooVariant, bar)` -> `fooVariantBar2` ### Fixation point Naming policy is fixed when the finalized `VariantSourcesContext` is created. Operationally this means: * naming policy must be selected before `variantSources.whenFinalized(...)` becomes observable * compile-unit names are projected and validated before queued `whenFinalized(...)` callbacks are replayed * changing naming policy from inside a `whenFinalized(...)` callback is too late This differs intentionally from late-configuration policy: * late-configuration policy is fixed by the first selector rule * naming policy is fixed by finalized-context creation --- ## `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` * `SourceSetMaterializer` By the time the context becomes observable: * compile-unit naming policy is already fixed * symbolic source-set names for finalized compile units are already determined Conceptually: ```java interface VariantSourcesContext { CompileUnitsView getCompileUnits(); RoleProjectionsView getRoleProjections(); SourceSetMaterializer getSourceSets(); } ``` This callback is also replayable. Example: ```java variantSources.whenFinalized(ctx -> { var units = ctx.getCompileUnits(); var roles = ctx.getRoleProjections(); var sourceSets = ctx.getSourceSets(); }); ``` --- ## `SourceSetMaterializer` ### Purpose `SourceSetMaterializer` is the official source of truth for materialized source sets. It is responsible for: * lazy creation of `GenericSourceSet` * projecting finalized compile units to symbolic source-set names * validating or resolving name collisions according to naming policy * 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 SourceSetMaterializer { NamedDomainObjectProvider getSourceSet(CompileUnit unit); } ``` --- ## Why `SourceSetMaterializer` 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 * `SourceSetMaterializer` 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. 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. ### 7. Make name-collision behavior explicit Compile-unit naming must be governed by an explicit policy, not by incidental materialization order. --- ## 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 * compile-unit naming policy * 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.