variants_variant_sources.md
776 lines
| 16.6 KiB
| text/x-minidsrc
|
MarkdownLexer
|
|
r41 | # 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<Layer> getLayers(); | ||||
| Set<Role> getRoles(); | ||||
| Set<Variant> getVariants(); | ||||
| Set<VariantRoleLayer> 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<CompileUnit> getUnits(); | ||||
| Set<CompileUnit> getUnitsForVariant(Variant variant); | ||||
| boolean contains(Variant variant, Layer layer); | ||||
| Set<Role> 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<RoleProjection> getProjections(); | ||||
| Set<RoleProjection> getProjectionsForVariant(Variant variant); | ||||
| Set<RoleProjection> getProjectionsForRole(Role role); | ||||
| Set<CompileUnit> 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<GenericSourceSet>` | ||||
| * 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 | ||||
| --- | ||||
|
|
r43 | ## 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 | ||||
| --- | ||||
|
|
r41 | ## `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<GenericSourceSet> 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`. | ||||
|
|
r43 | ### 5. Make late behavior explicit | ||
| Late configuration after materialization is a policy decision, not an implicit | ||||
| guarantee. | ||||
| ### 6. Keep heavy runtime objects behind providers | ||||
|
|
r41 | |||
| 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. | ||||
