variant_artifacts.md
710 lines
| 20.0 KiB
| text/x-minidsrc
|
MarkdownLexer
|
|
r47 | # Variants and Variant Artifacts | ||
|
|
r60 | > Design note. The current user-facing DSL and local publication workflow are | ||
| > documented in [README.md](README.md). Some snippets below are conceptual and | ||||
| > describe model direction rather than exact public API. | ||||
|
|
r47 | ## Overview | ||
| This document describes the artifact model built on top of `variants` and | ||||
| `variantSources`. | ||||
| The goal is to define: | ||||
| - a stable artifact-facing model for outgoing contracts; | ||||
| - a DSL for declaring slots and their inputs; | ||||
| - a resolver bridge between `variantArtifacts` and `variantSources`; | ||||
| - extension points for deduplication and similar policies; | ||||
| - a model that remains live during configuration and does not require an | ||||
| artificial freeze of slot content. | ||||
| The design follows the same split already used elsewhere: | ||||
| - `variants` defines the closed domain topology; | ||||
| - `variantSources` defines source materialization semantics over that topology; | ||||
| - `variantArtifacts` defines outgoing artifact contracts over that topology. | ||||
| --- | ||||
| ## Core idea | ||||
| `variantArtifacts` is not the owner of the variant model and not the owner of | ||||
| source-set materialization. | ||||
| Its purpose is narrower: | ||||
| - decide which variants participate in outgoing publication; | ||||
| - define artifact slots for those outgoing variants; | ||||
| - declare how each slot gathers its inputs. | ||||
| This makes `variantArtifacts` an outgoing-contract layer, not a compilation or | ||||
| source-materialization layer. | ||||
| --- | ||||
| ## Model boundaries | ||||
| ### What belongs to `variants` | ||||
| - identities: `Variant`, `Role`, `Layer`; | ||||
| - normalized topology relation `(variant, role, layer)`; | ||||
| - finalization of the core domain model; | ||||
| - `VariantsView` and derived topology views. | ||||
| ### What belongs to `variantSources` | ||||
| - compile units and role projections derived from finalized `variants`; | ||||
| - source-set naming policy; | ||||
| - source-set materialization; | ||||
| - lazy access to `GenericSourceSet` providers and their named outputs. | ||||
| ### What belongs to `variantArtifacts` | ||||
| - selection of outgoing variants; | ||||
| - slot identities inside an outgoing variant; | ||||
| - slot assembly declarations; | ||||
| - root outgoing configurations for outgoing variants; | ||||
| - slot-level assembly bodies; | ||||
| - publication-facing hooks. | ||||
| ### What does not belong to `variantArtifacts` | ||||
| - ownership of variant existence; | ||||
| - ownership of source-set naming; | ||||
| - direct mutation of source materialization internals; | ||||
| - compiler- or toolchain-specific logic; | ||||
| - eager flattening of source inputs into files during DSL declaration. | ||||
| --- | ||||
| ## Outgoing subset | ||||
| Not every declared `Variant` must become outgoing. | ||||
| `variantArtifacts` defines an outgoing subset of variants. | ||||
| For each outgoing variant there is one root outgoing aggregate: | ||||
| - one root consumable `Configuration`; | ||||
| - one or more artifact slots; | ||||
| - one primary slot; | ||||
| - optional secondary slots. | ||||
| This is why `OutgoingConfiguration` is a real model object and not merely a | ||||
| publication event payload. | ||||
| It represents a live build-facing aggregate: | ||||
| - it has its own attributes; | ||||
| - it is visible to other build logic as soon as registered; | ||||
| - it contains lazy handles rather than eagerly materialized state; | ||||
| - it is distinct from slot assembly state. | ||||
| --- | ||||
| ## Identity-first split | ||||
| The artifact model should preserve the same identity-first principle used for | ||||
| the rest of the project. | ||||
| ### Identity objects | ||||
| - `Variant` | ||||
| - `Slot` | ||||
| - `ArtifactSlot = (variant, slot)` | ||||
| These objects are: | ||||
| - cheap; | ||||
| - replayable; | ||||
| - suitable for discovery and selection. | ||||
| ### Stateful objects | ||||
| - `OutgoingConfiguration` | ||||
| - `ArtifactAssembly` | ||||
| These objects are: | ||||
| - build-facing; | ||||
| - allowed to contain Gradle lazy handles; | ||||
| - obtained through dedicated APIs rather than embedded into identity objects. | ||||
| ### Rule of thumb | ||||
| - slot identity is cheap and replayable; | ||||
| - inside one `OutgoingConfiguration`, `Slot` is the natural local identity; | ||||
| - `ArtifactSlot` is useful when slot identity must be referenced outside the | ||||
| parent outgoing configuration; | ||||
| - slot body is stateful and resolved separately; | ||||
| - outgoing configuration is a live aggregate, not a mere snapshot. | ||||
| --- | ||||
| ## Artifact model | ||||
| Conceptually: | ||||
| ```java | ||||
| interface VariantArtifactsContext { | ||||
| VariantsView getVariants(); | ||||
| void all(Action<? super OutgoingConfiguration> action); | ||||
| Optional<OutgoingConfiguration> findArtifacts(Variant variant); | ||||
| OutgoingConfiguration requireArtifacts(Variant variant); | ||||
| ArtifactAssemblies getAssemblies(); | ||||
| } | ||||
| ``` | ||||
| ```java | ||||
| interface OutgoingConfiguration { | ||||
| Variant getVariant(); | ||||
| NamedDomainObjectProvider<Configuration> getOutgoingConfiguration(); | ||||
| NamedDomainObjectContainer<Slot> getSlots(); | ||||
| Property<Slot> getPrimarySlot(); | ||||
| } | ||||
| ``` | ||||
| ```java | ||||
| interface ArtifactAssemblies { | ||||
| ArtifactAssembly resolveSlot(ArtifactSlot slot); | ||||
| } | ||||
| ``` | ||||
| This intentionally uses a Gradle-style local slot container rather than a | ||||
| separate `ArtifactSlotsView`. | ||||
| The reason is simple: | ||||
| - inside one `OutgoingConfiguration`, slot identity is local and naturally | ||||
| expressed as `Slot`; | ||||
| - a dedicated `ArtifactSlotsView` would suggest a detached readonly projection | ||||
| without clearly owning mutation/configuration semantics; | ||||
| - `NamedDomainObjectContainer<Slot>` better matches the live configuration model | ||||
| and keeps the parent aggregate as the owner of slot structure. | ||||
| `ArtifactSlot` remains useful, but only as a fully qualified identity outside | ||||
| the parent aggregate: | ||||
| - resolver APIs; | ||||
| - global payloads; | ||||
| - cross-variant references. | ||||
| Important distinction: | ||||
| - `OutgoingConfiguration` describes outgoing publication structure; | ||||
| - `ArtifactAssembly` describes how one slot artifact is assembled. | ||||
| An `ArtifactAssembly` does not create the root outgoing configuration. It serves | ||||
| one already declared slot inside that configuration. | ||||
| ### Primary slot ownership | ||||
| Primary status belongs to the parent outgoing configuration, not to the slot | ||||
| itself. | ||||
| This is why the preferred container-level contract is: | ||||
| ```java | ||||
| Property<Slot> getPrimarySlot(); | ||||
| ``` | ||||
| and not a slot-local boolean or a derived `ArtifactSlot` reference. | ||||
| Reasons: | ||||
| - the primary role is assigned by the parent aggregate; | ||||
| - within one `OutgoingConfiguration`, the variant identity is already known, so | ||||
| `Slot` is sufficient and avoids redundant `(variant, slot)` duplication; | ||||
| - `Property` matches the rest of the Gradle-facing live model better than a | ||||
| custom `findPrimarySlot()` style API; | ||||
| - `Property` gives useful write/configure/finalize semantics without inventing a | ||||
| separate special lifecycle abstraction. | ||||
| Expected usage: | ||||
| - DSL or adapters may assign the primary slot through `set(...)`; | ||||
| - adapters that want to provide a default may use `convention(...)`; | ||||
| - the property may remain unset while the model is still incomplete; | ||||
| - at materialization time the property may be finalized; | ||||
| - if more than one slot exists and `primarySlot` is still unset at the | ||||
| materialization point, that is a model error. | ||||
| The model should still enforce that the selected primary slot belongs to the | ||||
| same `OutgoingConfiguration`. | ||||
| ### Proposed public shape | ||||
| With these constraints, the preferred public structure is: | ||||
| ```java | ||||
| interface VariantArtifactsContext { | ||||
| VariantsView getVariants(); | ||||
| void all(Action<? super OutgoingConfiguration> action); | ||||
| Optional<OutgoingConfiguration> findArtifacts(Variant variant); | ||||
| OutgoingConfiguration requireArtifacts(Variant variant); | ||||
| ArtifactAssemblies getAssemblies(); | ||||
| } | ||||
| interface OutgoingConfiguration { | ||||
| Variant getVariant(); | ||||
| NamedDomainObjectProvider<Configuration> getOutgoingConfiguration(); | ||||
| NamedDomainObjectContainer<Slot> getSlots(); | ||||
| Property<Slot> getPrimarySlot(); | ||||
| } | ||||
| interface ArtifactAssemblies { | ||||
| ArtifactAssembly resolveSlot(ArtifactSlot slot); | ||||
| } | ||||
| record ArtifactSlot(Variant variant, Slot slot) {} | ||||
| ``` | ||||
| This gives: | ||||
| - one global registry of outgoing variants; | ||||
| - one local slot container per outgoing variant; | ||||
| - one fully qualified slot identity for resolver and cross-aggregate use; | ||||
| - one explicit service for slot assembly materialization. | ||||
| ### Minimal internal shape | ||||
| The preferred minimal internal structure is: | ||||
| ```java | ||||
| final class VariantArtifactsRegistry implements VariantArtifactsContext { | ||||
| OutgoingConfiguration outgoingConfiguration(Variant variant); | ||||
| ArtifactAssemblyRules slotRules(ArtifactSlot slot); | ||||
| ArtifactAssemblies assemblies(); | ||||
| } | ||||
| interface ArtifactAssemblyRules { | ||||
| void from(Object input); | ||||
|
|
r61 | <T extends Task> void producedBy( | ||
| TaskProvider<T> task, | ||||
| Function<? super T, ? extends Provider<? extends FileSystemLocation>> output); | ||||
|
|
r47 | void fromVariant(Action<? super OutputSelectionSpec> action); | ||
| void fromRole(String roleName, Action<? super OutputSelectionSpec> action); | ||||
| void fromLayer(String layerName, Action<? super OutputSelectionSpec> action); | ||||
| } | ||||
| ``` | ||||
| Responsibility split: | ||||
| - `VariantArtifactsRegistry` owns the whole artifact model; | ||||
| - `OutgoingConfiguration` owns only the structural variant-local aggregate; | ||||
| - `ArtifactAssemblyRules` owns the content declaration of one slot; | ||||
| - `ArtifactAssemblies` materializes `ArtifactAssembly` from | ||||
| `ArtifactSlot -> ArtifactAssemblyRules`. | ||||
| This keeps `OutgoingConfiguration` focused on structure and avoids overloading it | ||||
| with slot-content APIs such as `rules(slot)`. | ||||
| ### DSL binding | ||||
| The DSL should be connected through the registry, not by making | ||||
| `OutgoingConfiguration` responsible for content rules. | ||||
| Conceptually: | ||||
| ```java | ||||
| variantArtifacts.variant("browser", spec -> { | ||||
| spec.slot("runtime", assembly -> { | ||||
| assembly.fromRole("production", out -> out.output("js")); | ||||
| }); | ||||
| }); | ||||
| ``` | ||||
| Operationally this means: | ||||
| 1. registry creates or returns `OutgoingConfiguration` for the variant; | ||||
| 2. slot declaration creates or returns `Slot` inside that outgoing configuration; | ||||
| 3. registry forms `ArtifactSlot(variant, slot)`; | ||||
| 4. registry resolves `slotRules(artifactSlot)`; | ||||
| 5. bound `ArtifactAssemblySpec` writes into those rules. | ||||
| So the DSL writes: | ||||
| - structure into `OutgoingConfiguration`; | ||||
| - slot content into registry-owned `ArtifactAssemblyRules`. | ||||
| This is the intended bridge point between the public DSL and the internal | ||||
| resolver/materialization model. | ||||
| --- | ||||
| ## Live model and monotonic structure | ||||
| `variantArtifacts` should be treated as a live configuration model during the | ||||
| whole configuration phase. | ||||
| This means: | ||||
| - slot inputs remain live; | ||||
| - `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep | ||||
| contributing inputs until task execution; | ||||
|
|
r61 | - `producedBy(...)` publishes an existing task output directly and does not | ||
| create the managed copy assembly for that slot; | ||||
|
|
r47 | - `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task | ||
| wiring; | ||||
| - external task outputs remain outside the control of this model and must be | ||||
| accepted as live inputs. | ||||
| The model should therefore avoid a mandatory freeze phase for slot content. | ||||
| Instead, it should follow a monotonic rule: | ||||
| - outgoing variant existence may grow; | ||||
| - slot existence may grow; | ||||
| - slot content may grow; | ||||
| - publication-visible identity should not be retroactively redefined. | ||||
| In practice this means: | ||||
| - slot names are stable once declared; | ||||
| - primary slot designation is structural; | ||||
| - slot input content remains live. | ||||
| This also means that the model does not need a dedicated freeze phase for slot | ||||
| content merely because the root outgoing configuration was registered earlier. | ||||
| Early registration of the root `Configuration` and live evolution of slot input | ||||
| content are compatible concerns. | ||||
| --- | ||||
| ## DSL principles | ||||
| The DSL should remain declarative and symbolic. | ||||
| It should describe: | ||||
| - which variant is outgoing; | ||||
| - which slots exist; | ||||
| - which slot is primary; | ||||
| - which selectors contribute inputs to each slot. | ||||
| It should not directly expose: | ||||
| - `GenericSourceSet`; | ||||
| - `FileCollection`; | ||||
| - concrete resolved files from `variantSources`; | ||||
| - internal resolver state. | ||||
| ### DSL shape | ||||
| Conceptually: | ||||
| ```groovy | ||||
| variantArtifacts { | ||||
| variant("browser") { | ||||
| primarySlot("runtime") { | ||||
| fromRole("production") { | ||||
| output("js") | ||||
| output("resources") | ||||
| } | ||||
| } | ||||
| slot("types") { | ||||
| fromVariant { | ||||
| output("dts") | ||||
| } | ||||
| } | ||||
| slot("sources") { | ||||
| fromLayer("main") { | ||||
| output("sources") | ||||
| } | ||||
| } | ||||
| slot("bundleMetadata") { | ||||
|
|
r61 | producedBy(writePackageMetadata) { | ||
| outputFile | ||||
| } | ||||
|
|
r47 | } | ||
| } | ||||
| } | ||||
| ``` | ||||
| ### Meaning of contribution forms | ||||
| - `from(Object)` adds a direct input independent from `variantSources`; | ||||
| - `fromVariant { output(...) }` selects named outputs from all compile units of | ||||
| the current variant; | ||||
| - `fromRole(role) { output(...) }` selects named outputs from compile units that | ||||
| belong to the given role projection; | ||||
| - `fromLayer(layer) { output(...) }` selects named outputs from the compile unit | ||||
| of the current variant and the given layer, if such unit exists. | ||||
|
|
r61 | - `producedBy(task) { outputFile }` maps an existing producing task to the single | ||
| file or directory published for the slot. | ||||
|
|
r47 | |||
|
|
r61 | Contribution forms and `producedBy(...)` are mutually exclusive for one slot. | ||
|
|
r47 | The DSL stores declarations, not resolved file collections. | ||
| --- | ||||
| ## Contribution model | ||||
| Internally the DSL should compile to slot contributions. | ||||
| Conceptually: | ||||
| - `DirectContribution` | ||||
| - `VariantOutputContribution` | ||||
| - `RoleOutputContribution` | ||||
| - `LayerOutputContribution` | ||||
| These contributions should remain symbolic for as long as possible. | ||||
| They should not resolve source sets or files at declaration time. | ||||
| Each contribution is expected to provide: | ||||
| - its selection scope; | ||||
| - the requested output names; | ||||
| - enough symbolic identity for later validation and resolver policies. | ||||
| --- | ||||
| ## Resolver bridge between `variantSources` and `variantArtifacts` | ||||
| This is the central integration point. | ||||
| `variantArtifacts` should not access mutable internals of `variantSources`. | ||||
| Instead, it should resolve slot inputs through the public finalized | ||||
| `VariantSourcesContext`. | ||||
| ### Bridge responsibilities | ||||
| The bridge: | ||||
| - takes slot contribution declarations; | ||||
| - expands them against finalized variant topology; | ||||
| - maps logical selectors to compile units and role projections; | ||||
| - obtains source sets lazily through `VariantSourcesContext`; | ||||
| - resolves named outputs from those source sets; | ||||
| - builds the live input model for an `ArtifactAssembly`. | ||||
| ### Bridge input | ||||
| - current outgoing variant identity; | ||||
| - slot contribution declarations; | ||||
| - `VariantSourcesContext`. | ||||
| ### Bridge output | ||||
| - a live collection of logical slot inputs; | ||||
| - later adapted to `FileCollection` or other assembly-facing input models. | ||||
| ### Resolution semantics | ||||
| For one outgoing variant: | ||||
| - `fromVariant { output(x) }` | ||||
| - expands to all `CompileUnit` of that variant; | ||||
| - `fromRole(role) { output(x) }` | ||||
| - expands to `RoleProjection(variant, role)` and then to its compile units; | ||||
| - `fromLayer(layer) { output(x) }` | ||||
| - expands to one compile unit `(variant, layer)` when it exists; | ||||
| - `from(Object)` | ||||
| - bypasses `variantSources` completely. | ||||
|
|
r61 | - `producedBy(task)` | ||
| - bypasses contribution resolution and registers the task output as the slot | ||||
| artifact directly. | ||||
|
|
r47 | |||
| After compile units are known, the bridge asks | ||||
| `ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the | ||||
| requested named output. | ||||
| This keeps `variantArtifacts` independent from source-set naming internals and | ||||
| other materialization details. | ||||
| --- | ||||
| ## Validation | ||||
| Validation should be structural and symbolic. | ||||
| It should validate: | ||||
| - outgoing variant refers to an existing `Variant`; | ||||
| - referenced `Role` exists in that variant projection space; | ||||
| - referenced `Layer` exists in that variant compile-unit space; | ||||
| - primary slot is defined when needed; | ||||
| - primary slot refers to a slot declared in the same outgoing configuration. | ||||
| Validation should not require eager materialization of source sets or eager | ||||
| resolution of files. | ||||
| --- | ||||
| ## Deduplication and policy extension points | ||||
| Deduplication is important, but it should not be baked into the DSL itself. | ||||
| The correct place for it is the resolver bridge, after symbolic contributions | ||||
| have been expanded to logical inputs but before they are finally adapted to | ||||
| assembly-facing file collections. | ||||
| ### Why not in the DSL | ||||
| At declaration time it is still unknown whether selectors overlap: | ||||
| - `fromVariant` | ||||
| - `fromRole` | ||||
| - `fromLayer` | ||||
| may all describe the same logical source output. | ||||
| ### Why not rely only on `FileCollection` | ||||
| `FileCollection` may still provide useful physical deduplication, but it is too | ||||
| late and too file-oriented to serve as the only semantic mechanism. | ||||
| The artifact model should first deduplicate logical inputs, then let Gradle | ||||
| perform any additional physical deduplication. | ||||
| ### Default expectation | ||||
| The default resolver should support: | ||||
| - deduplication of topology-aware inputs by logical identity; | ||||
| - no implicit deduplication of direct `from(Object)` inputs. | ||||
| Logical identity should be based on domain meaning, for example: | ||||
| - `(CompileUnit, outputName)` | ||||
| and not on projected source-set names. | ||||
| This is important because source-set naming policy belongs to `variantSources` | ||||
| and must not silently redefine artifact semantics. | ||||
| ### Extension points | ||||
| The model should provide explicit internal extension points for: | ||||
| - deduplication policy; | ||||
| - logical input identity; | ||||
| - adaptation of resolved logical inputs to assembly-facing objects. | ||||
| Conceptually: | ||||
| ```java | ||||
| interface SlotInputDedupPolicy { ... } | ||||
| interface LogicalSlotInputIdentity { ... } | ||||
| interface SlotInputAdapter { ... } | ||||
| ``` | ||||
| The default implementation may remain simple, but these seams should exist from | ||||
| the start. | ||||
| --- | ||||
| ## Publication hooks | ||||
| Publication hooks remain useful, but they should observe the live structural | ||||
| model rather than define it. | ||||
| Examples: | ||||
|
|
r60 | - `whenOutgoingConfiguration(...)` | ||
|
|
r47 | - `whenOutgoingSlot(...)` | ||
| These hooks are adapter-facing customization points over already declared | ||||
| outgoing structure: | ||||
| - root configuration attributes; | ||||
| - slot artifact attributes; | ||||
| - assembly task tweaks. | ||||
| The recommended way to connect publication-facing `Spec` objects to the | ||||
| structural model is by backlink, not by moving slot rules into publication | ||||
| types. | ||||
| Conceptually: | ||||
| ```java | ||||
| interface OutgoingConfigurationSpec { | ||||
| OutgoingConfiguration getOutgoingArtifacts(); | ||||
| Variant getVariant(); | ||||
| Configuration getConfiguration(); | ||||
| } | ||||
| interface OutgoingArtifactSlotSpec { | ||||
| ArtifactSlot getArtifactSlot(); | ||||
| ArtifactAssembly getAssembly(); | ||||
| boolean isPrimary(); | ||||
| } | ||||
| ``` | ||||
| In this arrangement: | ||||
| - `OutgoingConfigurationSpec` remains a publication-facing facade; | ||||
| - `OutgoingConfigurationSpec` may expose the structural aggregate when an | ||||
| adapter needs it; | ||||
| - slot rules still belong to `VariantArtifactsRegistry`; | ||||
| - publication specs do not become owners of declaration or resolver state. | ||||
| They should not become the primary structural API for the artifact model. | ||||
| This is why a separate phase-oriented `OutgoingPublicationsContext` is not | ||||
| required. | ||||
| The live `OutgoingConfiguration` aggregate is sufficient. | ||||
| --- | ||||
| ## Design principles | ||||
| ### 1. Keep topology ownership in `variants` | ||||
| `variantArtifacts` selects from the topology model. It does not own it. | ||||
| ### 2. Keep source ownership in `variantSources` | ||||
| `variantArtifacts` consumes source materialization through a resolver bridge. It | ||||
| does not own source-set semantics. | ||||
| ### 3. Keep the DSL symbolic | ||||
| The DSL declares intent and selection rules, not materialized files. | ||||
| ### 4. Keep slot content live | ||||
| Do not introduce an artificial finalize phase for slot content unless a real | ||||
| semantic need appears. | ||||
| ### 5. Fix only structural identity | ||||
| Slot name, primary designation, and outgoing shape are structural. Slot inputs | ||||
| remain live. | ||||
| ### 6. Resolve through dedicated bridges | ||||
| Cross-model integration belongs in a resolver service, not in DSL classes. | ||||
| ### 7. Add policy seams early | ||||
| Deduplication and similar concerns should have extension points from the start, | ||||
| even if the initial implementation is conservative. | ||||
| --- | ||||
| ## Summary | ||||
| `variantArtifacts` should be modeled as a live outgoing-contract layer over | ||||
| `variants`, with source input resolution delegated to a dedicated bridge over | ||||
| `variantSources`. | ||||
| The resulting shape is: | ||||
| - `variants` owns topology; | ||||
| - `variantSources` owns source materialization; | ||||
| - `variantArtifacts` owns outgoing contract structure; | ||||
| - a resolver bridge connects symbolic slot declarations to live source-derived | ||||
| inputs; | ||||
| - deduplication and similar concerns are policies of that bridge, not of the | ||||
| DSL itself; | ||||
| - slot content stays live during configuration; | ||||
| - only publication-visible structure is treated as stable identity. | ||||
