##// END OF EJS Templates
WIP almost stable variants model, working on variantSources
cin -
r41:f19d2b751aa9 default
parent child
Show More
@@ -0,0 +1,210
1 # `variantSources`: selectors and precedence
2
3 `variantSources` configures source-set materialization over the compile-unit space.
4
5 A compile unit is defined as:
6
7 - `(variant, layer)`
8
9 This means:
10
11 - `variant` defines compilation semantics
12 - `layer` defines compilation partitioning
13
14 The `variantSources` DSL does not introduce a separate source model.
15 Instead, it provides configuration selectors over the existing compile-unit space.
16
17 ## Selectors
18
19 Three selectors are available:
20
21 - `variant(...)`
22 - `layer(...)`
23 - `unit(...)`
24
25 They all target the same set of compile units, but at different levels of specificity.
26
27 ### `variant(...)`
28
29 `variant(...)` applies configuration to all compile units that belong to the given variant.
30
31 Example:
32
33 ```groovy
34 variantSources {
35 variant("browser") {
36 declareOutputs("js", "dts")
37 }
38 }
39 ```
40
41 This affects all compile units of `browser`, for example:
42
43 - `(browser, main)`
44 - `(browser, rjs)`
45 - `(browser, test)`
46
47 Use this selector for variant-wide conventions.
48
49 ---
50
51 ### `layer(...)`
52
53 `layer(...)` applies configuration to all compile units that use the given layer.
54
55 Example:
56
57 ```groovy
58 variantSources {
59 layer("main") {
60 set("ts") {
61 srcDir("src/main/ts")
62 }
63 }
64 }
65 ```
66
67 This affects all compile units with layer `main`, for example:
68
69 - `(browser, main)`
70 - `(nodejs, main)`
71 - `(electron, main)`
72
73 Use this selector for cross-variant layer conventions.
74
75 ---
76
77 ### `unit(...)`
78
79 `unit(...)` applies configuration to one exact compile unit.
80
81 Example:
82
83 ```groovy
84 variantSources {
85 unit("browser", "main") {
86 set("resources") {
87 srcDir("src/browserMain/resources")
88 }
89 }
90 }
91 ```
92
93 This affects only:
94
95 - `(browser, main)`
96
97 Use this selector for the most specific adjustments.
98
99 ---
100
101 ## Precedence
102
103 For each compile unit, source-set configuration is applied in the following order:
104
105 ```text
106 variant < layer < unit
107 ```
108
109 This means:
110
111 1. `variant(...)` actions are applied first
112 2. `layer(...)` actions are applied next
113 3. `unit(...)` actions are applied last
114
115 Each next level is allowed to refine or override the previous one.
116
117 ### Within the same level
118
119 Within the same selector level, actions are applied in registration order.
120
121 For example, if two plugins both configure `layer("main")`, their actions are applied in the same order in which they were registered.
122
123 ---
124
125 ## Example
126
127 ```groovy
128 variantSources {
129 variant("browser") {
130 declareOutputs("js", "dts")
131 }
132
133 layer("main") {
134 set("ts") {
135 srcDir("src/main/ts")
136 }
137 }
138
139 unit("browser", "main") {
140 set("resources") {
141 srcDir("src/browserMain/resources")
142 }
143 }
144 }
145 ```
146
147 For compile unit `(browser, main)` the effective configuration is built in this order:
148
149 1. `variant("browser")`
150 2. `layer("main")`
151 3. `unit("browser", "main")`
152
153 For compile unit `(browser, rjs)` the effective configuration is built in this order:
154
155 1. `variant("browser")`
156 2. `layer("rjs")` if present
157 3. `unit("browser", "rjs")` if present
158
159 For compile unit `(nodejs, main)` the effective configuration is built in this order:
160
161 1. `variant("nodejs")` if present
162 2. `layer("main")`
163 3. `unit("nodejs", "main")` if present
164
165 ---
166
167 ## Model boundary
168
169 These selectors do not define compile units.
170 Compile units are derived from finalized `variants`.
171
172 `variantSources` only configures how source sets are materialized for those units.
173
174 This means:
175
176 - `variants` is the source of truth for compile-unit existence
177 - `variantSources` is the source of truth for compile-unit source-set configuration
178
179 ---
180
181 ## Operational semantics
182
183 The `variantSources` API is exposed through a finalized context.
184
185 Conceptually, configuration is registered against finalized model objects, while DSL sugar may still use names for convenience.
186
187 Internally, selector-based configuration is accumulated and later applied by the source-set materializer when a `GenericSourceSet` is created for a compile unit.
188
189 This guarantees that:
190
191 - selector precedence is stable
192 - registration order is preserved
193 - configuration does not depend on the materialization moment
194 - adapters do not need to depend on raw Gradle lifecycle timing
195
196 ---
197
198 ## Summary
199
200 - compile unit space is `(variant, layer)`
201 - `variant(...)`, `layer(...)`, and `unit(...)` are selectors over that space
202 - precedence is:
203
204 ```text
205 variant < layer < unit
206 ```
207
208 - registration order is preserved within the same selector level
209 - `variants` defines what exists
210 - `variantSources` defines how those compile units are materialized as source sets
@@ -0,0 +1,151
1 package org.implab.gradle.variants.core;
2
3 import java.util.ArrayList;
4 import java.util.LinkedHashMap;
5 import java.util.List;
6 import java.util.Map;
7 import java.util.Objects;
8 import java.util.Optional;
9 import java.util.Set;
10 import java.util.stream.Collectors;
11
12 import org.eclipse.jdt.annotation.NonNullByDefault;
13 import org.gradle.api.InvalidUserDataException;
14
15 @NonNullByDefault
16 public class VariantsView {
17 private final Set<Layer> layers;
18 private final Set<Role> roles;
19 private final Set<Variant> variants;
20 private final Set<VariantRoleLayer> entries;
21
22 private final Map<Variant, Set<VariantRoleLayer>> entriesByVariant;
23 private final Map<Role, Set<VariantRoleLayer>> entriesByRole;
24 private final Map<Layer, Set<VariantRoleLayer>> entriesByLayer;
25
26 private VariantsView(Set<Layer> layers, Set<Role> roles, Set<Variant> variants, Set<VariantRoleLayer> entries) {
27 this.layers = layers;
28 this.roles = roles;
29 this.variants = variants;
30 this.entries = entries;
31 this.entriesByVariant = entries.stream()
32 .collect(Collectors.groupingBy(VariantRoleLayer::variant, Collectors.toSet()));
33 this.entriesByRole = entries.stream()
34 .collect(Collectors.groupingBy(VariantRoleLayer::role, Collectors.toSet()));
35 this.entriesByLayer = entries.stream()
36 .collect(Collectors.groupingBy(VariantRoleLayer::layer, Collectors.toSet()));
37 }
38
39 public Set<Layer> getLayers() {
40 return layers;
41 }
42
43 public Set<Role> getRoles() {
44 return roles;
45 }
46
47 public Set<Variant> getVariants() {
48 return variants;
49 }
50
51 public Set<VariantRoleLayer> getEntries() {
52 return entries;
53 }
54
55 public Set<VariantRoleLayer> getEntriesForVariant(Variant variant) {
56 return entriesByVariant.getOrDefault(variant, Set.of());
57 }
58
59 public Set<VariantRoleLayer> getEntriesForLayer(Layer layer) {
60 return entriesByLayer.getOrDefault(layer, Set.of());
61 }
62
63 public Set<VariantRoleLayer> getEntriesForRole(Role role) {
64 return entriesByRole.getOrDefault(role, Set.of());
65 }
66
67 public record VariantRoleLayer(Variant variant, Role role, Layer layer) {
68 }
69
70 public static Builder builder() {
71 return new Builder();
72 }
73
74 public static class Builder {
75
76 private final Map<String, Layer> layers = new LinkedHashMap<>();
77 private final Map<String, Role> roles = new LinkedHashMap<>();
78 private final Map<String, Variant> variants = new LinkedHashMap<>();
79 private final List<VariantDefinition> definitions = new ArrayList<>();
80
81 private Builder() {
82 }
83
84 public Builder addRole(Role role) {
85 Objects.requireNonNull(role, "role can't be null");
86 roles.put(role.getName(), role);
87 return this;
88 }
89
90 public Builder addLayer(Layer layer) {
91 Objects.requireNonNull(layer, "layer can't be null");
92 layers.put(layer.getName(), layer);
93 return this;
94 }
95
96 public Builder addVariant(Variant variant) {
97 Objects.requireNonNull(variant, "variant can't be null");
98 variants.put(variant.getName(), variant);
99 return this;
100 }
101
102 public Builder addDefinition(VariantDefinition definition) {
103 Objects.requireNonNull(definition, "definition can't be null");
104 definitions.add(definition);
105 return this;
106 }
107
108 public VariantsView build() {
109
110 var entries = definitions.stream()
111 .flatMap(def -> def.getRoleBindings().get().stream()
112 .map(layerRole -> createVariantRoleLayer(
113 def.getName(), // variantName
114 layerRole.roleName(),
115 layerRole.layerName())))
116 .collect(Collectors.toSet());
117
118 return new VariantsView(
119 Set.copyOf(layers.values()),
120 Set.copyOf(roles.values()),
121 Set.copyOf(variants.values()),
122 entries);
123 }
124
125 private VariantRoleLayer createVariantRoleLayer(String variantName, String roleName, String layerName) {
126 return new VariantRoleLayer(
127 resolveVariant(variantName,
128 "Variant '" + variantName + "' isn't declared"),
129 resolveRole(roleName,
130 "Role '" + roleName + "' isn't declared, referenced in '" + variantName + "' variant"),
131 resolveLayer(layerName,
132 "Layer '" + layerName + "' isn't declared, referenced in '" + variantName
133 + "' variant with '" + roleName + "' role"));
134 }
135
136 private Layer resolveLayer(String layerName, String errorMessage) {
137 return Optional.ofNullable(layers.get(layerName))
138 .orElseThrow(() -> new InvalidUserDataException(errorMessage));
139 }
140
141 private Variant resolveVariant(String variantName, String errorMessage) {
142 return Optional.ofNullable(variants.get(variantName))
143 .orElseThrow(() -> new InvalidUserDataException(errorMessage));
144 }
145
146 private Role resolveRole(String roleName, String errorMessage) {
147 return Optional.ofNullable(roles.get(roleName))
148 .orElseThrow(() -> new InvalidUserDataException(errorMessage));
149 }
150 }
151 }
@@ -0,0 +1,7
1 package org.implab.gradle.variants.derived;
2
3 import org.implab.gradle.variants.core.Layer;
4 import org.implab.gradle.variants.core.Variant;
5
6 public record CompileUnit(Variant variant, Layer layer) {
7 }
@@ -0,0 +1,32
1 package org.implab.gradle.variants.derived;
2
3 import java.util.Optional;
4 import java.util.Set;
5
6 import org.eclipse.jdt.annotation.NonNullByDefault;
7 import org.implab.gradle.variants.core.Layer;
8 import org.implab.gradle.variants.core.Role;
9 import org.implab.gradle.variants.core.Variant;
10
11 @NonNullByDefault
12 public interface CompileUnitsView {
13 Set<CompileUnit> getUnits();
14
15 Set<CompileUnit> getUnitsForVariant(Variant variant);
16
17 Optional<CompileUnit> findUnit(Variant variant, Layer layer);
18
19 default CompileUnit getUnit(Variant variant, Layer layer) {
20 return findUnit(variant, layer)
21 .orElseThrow(() -> new IllegalArgumentException(
22 "Compile unit for variant '" + variant.getName()
23 + "' and layer '" + layer.getName() + "' not found"));
24 }
25
26 boolean contains(Variant variant, Layer layer);
27
28 /**
29 * In which logical roles this compile unit participates.
30 */
31 Set<Role> getRoles(CompileUnit unit);
32 } No newline at end of file
@@ -0,0 +1,7
1 package org.implab.gradle.variants.derived;
2
3 import org.implab.gradle.variants.core.Role;
4 import org.implab.gradle.variants.core.Variant;
5
6 public record RoleProjection(Variant variant, Role role) {
7 }
@@ -0,0 +1,35
1 package org.implab.gradle.variants.derived;
2
3 import java.util.Optional;
4 import java.util.Set;
5
6 import org.implab.gradle.variants.core.Layer;
7 import org.implab.gradle.variants.core.Role;
8 import org.implab.gradle.variants.core.Variant;
9
10 public interface RoleProjectionsView {
11 Set<RoleProjection> getProjections();
12
13 Set<RoleProjection> getProjectionsForVariant(Variant variant);
14
15 Set<RoleProjection> getProjectionsForRole(Role role);
16
17 Optional<RoleProjection> findProjection(Variant variant, Role role);
18
19 default RoleProjection getProjection(Variant variant, Role role) {
20 return findProjection(variant, role)
21 .orElseThrow(() -> new IllegalArgumentException(
22 "Role projection for variant '" + variant.getName()
23 + "' and role '" + role.getName() + "' not found"));
24 }
25
26 boolean contains(Variant variant, Role role);
27
28 Set<CompileUnit> getUnits(RoleProjection projection);
29
30 default Set<Layer> getLayers(RoleProjection projection) {
31 return getUnits(projection).stream()
32 .map(CompileUnit::layer)
33 .collect(java.util.stream.Collectors.toUnmodifiableSet());
34 }
35 } No newline at end of file
@@ -0,0 +1,39
1 package org.implab.gradle.variants.sources.internal;
2
3 import java.util.ArrayList;
4 import java.util.LinkedHashMap;
5 import java.util.List;
6 import java.util.Map;
7 import java.util.Objects;
8
9 import org.gradle.api.Action;
10 import org.implab.gradle.common.sources.GenericSourceSet;
11 import org.implab.gradle.variants.core.Layer;
12 import org.implab.gradle.variants.derived.CompileUnit;
13
14 public class DefaultLayerConfigurationRegistry implements LayerConfigurationRegistry {
15 private final Map<Layer, List<Action<? super GenericSourceSet>>> actionsByLayer = new LinkedHashMap<>();
16
17 @Override
18 public void add(Layer layer, Action<? super GenericSourceSet> action) {
19 Objects.requireNonNull(layer, "layer can't be null");
20 Objects.requireNonNull(action, "action can't be null");
21 actionsByLayer.computeIfAbsent(layer, key -> new ArrayList<>()).add(action);
22 }
23
24 @Override
25 public void applyLayer(Layer layer, GenericSourceSet sourceSet) {
26 var actions = actionsByLayer.get(layer);
27 if (actions == null) {
28 return;
29 }
30 for (var action : actions) {
31 action.execute(sourceSet);
32 }
33 }
34
35 @Override
36 public void applyUnit(CompileUnit unit, GenericSourceSet sourceSet) {
37 applyLayer(unit.layer(), sourceSet);
38 }
39 }
@@ -0,0 +1,15
1 package org.implab.gradle.variants.sources.internal;
2
3 import org.gradle.api.Action;
4 import org.implab.gradle.common.sources.GenericSourceSet;
5 import org.implab.gradle.variants.core.Layer;
6 import org.implab.gradle.variants.derived.CompileUnit;
7
8 public interface LayerConfigurationRegistry {
9
10 void add(Layer layer, Action<? super GenericSourceSet> action);
11
12 void applyLayer(Layer layer, GenericSourceSet sourceSet);
13
14 void applyUnit(CompileUnit unit, GenericSourceSet sourceSet);
15 }
This diff has been collapsed as it changes many lines, (719 lines changed) Show them Hide them
@@ -0,0 +1,719
1 # Variants and Variant Sources
2
3 ## Overview
4
5 This document describes a two-layer model for build variants:
6
7 - `variants` defines the **core domain model**
8 - `variantSources` defines **source materialization semantics** for that model
9
10 The main goal is to keep the core model small, explicit, and stable, while allowing source-related behavior to remain flexible and adapter-friendly.
11
12 The model is intentionally split into:
13
14 1. a **closed, finalized domain model**
15 2. an **open, runtime-oriented source materialization model**
16
17 This separation is important because compilation, source aggregation, publication, and adapter-specific behavior do not belong to the same abstraction layer.
18
19 ---
20
21 ## Core idea
22
23 The `variants` model is based on three independent domains:
24
25 - `Layer`
26 - `Role`
27 - `Variant`
28
29 A finalized `VariantsView` contains the normalized relation:
30
31 - `(variant, role, layer)`
32
33 This relation is the source of truth.
34
35 Everything else is derived from it.
36
37 ---
38
39 ## `variants`: the core domain model
40
41 ### Purpose
42
43 `variants` describes:
44
45 - what layers exist
46 - what roles exist
47 - what variants exist
48 - which `(variant, role, layer)` combinations are valid
49
50 It does **not** describe:
51
52 - source directories
53 - source roots
54 - source set materialization
55 - compilation tasks
56 - publication mechanics
57 - source set inheritance
58 - layer merge behavior for a concrete toolchain
59
60 Those concerns are intentionally outside the core model.
61
62 ---
63
64 ## Core DSL example
65
66 ```groovy
67 variants {
68 layers {
69 main()
70 test()
71 generated()
72 rjs()
73 cjs()
74 }
75
76 roles {
77 production()
78 test()
79 tool()
80 }
81
82 variant("browser") {
83 role("production") {
84 layers("main", "generated", "rjs")
85 }
86 role("test") {
87 layers("main", "test", "generated", "rjs")
88 }
89 }
90
91 variant("nodejs") {
92 role("production") {
93 layers("main", "generated", "cjs")
94 }
95 role("test") {
96 layers("main", "test", "generated", "cjs")
97 }
98 role("tool") {
99 layers("main", "generated", "cjs")
100 }
101 }
102 }
103 ```
104
105 ### Interpretation
106
107 This example means:
108
109 * `browser` production uses `main`, `generated`, `rjs`
110 * `browser` test uses `main`, `test`, `generated`, `rjs`
111 * `nodejs` production uses `main`, `generated`, `cjs`
112 * `nodejs` test uses `main`, `test`, `generated`, `cjs`
113 * `nodejs` tool uses `main`, `generated`, `cjs`
114
115 The model is purely declarative.
116
117 ---
118
119 ## Identity and references
120
121 `Layer`, `Role`, and `Variant` are identity objects.
122
123 They exist as declared domain values.
124
125 References between model elements are symbolic:
126
127 * layers are referenced by layer name
128 * roles are referenced by role name
129 * variants are referenced by variant name
130
131 This is intentional.
132
133 The core model is declarative, not navigation-oriented.
134
135 It is acceptable for aggregates to hold symbolic references to foreign domain values, as long as identity is clearly defined and validated later.
136
137 ---
138
139 ## Finalization
140
141 The `variants` model is finalized once.
142
143 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.
144
145 The public contract is:
146
147 * `variants.whenFinalized(...)`
148
149 This callback is **replayable**:
150
151 * if called before finalization, the action is queued
152 * if called after finalization, the action is invoked immediately
153
154 The callback receives a finalized, read-only view of the model.
155
156 Example:
157
158 ```java
159 variants.whenFinalized(view -> {
160 // use finalized VariantsView here
161 });
162 ```
163
164 ---
165
166 ## `VariantsView`
167
168 `VariantsView` is the finalized representation of the core model.
169
170 It contains:
171
172 * all declared `Layer`
173 * all declared `Role`
174 * all declared `Variant`
175 * all normalized entries `(variant, role, layer)`
176
177 Conceptually:
178
179 ```java
180 interface VariantsView {
181 Set<Layer> getLayers();
182 Set<Role> getRoles();
183 Set<Variant> getVariants();
184 Set<VariantRoleLayer> getEntries();
185 }
186 ```
187
188 Where:
189
190 ```java
191 record VariantRoleLayer(Variant variant, Role role, Layer layer) {}
192 ```
193
194 This view is:
195
196 * immutable
197 * normalized
198 * validated
199 * independent from DSL internals
200
201 ---
202
203 ## Derived views
204
205 Two important views can be derived from `VariantsView`:
206
207 * `CompileUnitsView`
208 * `RoleProjectionsView`
209
210 These views are not part of the raw core model itself, but they are naturally derived from it.
211
212 ---
213
214 ## `CompileUnitsView`
215
216 ### Purpose
217
218 A compile unit is defined as:
219
220 * `(variant, layer)`
221
222 This is based on the following rationale:
223
224 * `variant` defines compilation semantics
225 * `layer` partitions a variant into separate compilation units
226 * `role` is not a compilation boundary
227
228 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.
229
230 ### Example
231
232 From:
233
234 * `(browser, production, main)`
235 * `(browser, production, rjs)`
236 * `(browser, test, main)`
237 * `(browser, test, test)`
238 * `(browser, test, rjs)`
239
240 we derive compile units:
241
242 * `(browser, main)`
243 * `(browser, rjs)`
244 * `(browser, test)`
245
246 ### Conceptual API
247
248 ```java
249 interface CompileUnitsView {
250 Set<CompileUnit> getUnits();
251 Set<CompileUnit> getUnitsForVariant(Variant variant);
252 boolean contains(Variant variant, Layer layer);
253 Set<Role> getRoles(CompileUnit unit);
254 }
255
256 record CompileUnit(Variant variant, Layer layer) {}
257 ```
258
259 ### Meaning
260
261 `CompileUnitsView` answers:
262
263 * what can be compiled
264 * how a variant is partitioned into compile units
265 * which logical roles include a given compile unit
266
267 ---
268
269 ## `RoleProjectionsView`
270
271 ### Purpose
272
273 A role projection is defined as:
274
275 * `(variant, role)`
276
277 This is based on the following rationale:
278
279 * `role` is not about compilation
280 * `role` groups compile units by purpose
281 * roles are more closely related to publication, aggregation, assembly, or result grouping
282
283 ### Example
284
285 For `browser`:
286
287 * `production` includes compile units:
288
289 * `(browser, main)`
290 * `(browser, rjs)`
291
292 * `test` includes compile units:
293
294 * `(browser, main)`
295 * `(browser, test)`
296 * `(browser, rjs)`
297
298 ### Conceptual API
299
300 ```java
301 interface RoleProjectionsView {
302 Set<RoleProjection> getProjections();
303 Set<RoleProjection> getProjectionsForVariant(Variant variant);
304 Set<RoleProjection> getProjectionsForRole(Role role);
305 Set<CompileUnit> getUnits(RoleProjection projection);
306 }
307
308 record RoleProjection(Variant variant, Role role) {}
309 ```
310
311 ### Meaning
312
313 `RoleProjectionsView` answers:
314
315 * how compile units are grouped by purpose
316 * what belongs to `production`, `test`, `tool`, etc.
317 * what should be aggregated or published together
318
319 ---
320
321 ## Why `CompileUnitsView` and `RoleProjectionsView` are not part of `VariantsView`
322
323 `VariantsView` is intentionally minimal.
324
325 It expresses the domain relation:
326
327 * `(variant, role, layer)`
328
329 `CompileUnitsView` and `RoleProjectionsView` are **derived interpretations** of that relation.
330
331 They are natural and useful, but they are still interpretations:
332
333 * `CompileUnit = (variant, layer)`
334 * `RoleProjection = (variant, role)`
335
336 This is why they are better treated as derived views rather than direct core model primitives.
337
338 ---
339
340 ## `variantSources`: source semantics for layers
341
342 ### Purpose
343
344 `variantSources` does **not** define variants.
345
346 It defines how a declared `Layer` contributes sources.
347
348 In other words:
349
350 * `variants` defines **what exists**
351 * `variantSources` defines **how layers become source inputs**
352
353 This distinction is important.
354
355 `variantSources` does not own the variant model. It interprets it.
356
357 ---
358
359 ## Main idea
360
361 A layer source rule describes the source contribution of a layer.
362
363 This is independent of any concrete variant or role.
364
365 Conceptually:
366
367 * `Layer -> source contribution rule`
368
369 Examples of source contribution semantics:
370
371 * base directory
372 * source directories
373 * logical source kinds (`ts`, `js`, `resources`)
374 * declared outputs (`js`, `dts`, `resources`)
375
376 ---
377
378 ## `variantSources` DSL example
379
380 ```groovy
381 variantSources {
382 layerRule("main") {
383 from("src/main")
384
385 set("ts") {
386 srcDir("ts")
387 }
388 set("js") {
389 srcDir("js")
390 }
391 set("resources") {
392 srcDir("resources")
393 }
394
395 outputs("js", "dts", "resources")
396 }
397
398 layerRule("test") {
399 from("src/test")
400
401 set("ts") {
402 srcDir("ts")
403 }
404 set("resources") {
405 srcDir("resources")
406 }
407
408 outputs("js", "dts", "resources")
409 }
410
411 layerRule("rjs") {
412 from("src/rjs")
413
414 set("ts") {
415 srcDir("ts")
416 }
417
418 outputs("js", "dts")
419 }
420
421 layerRule("cjs") {
422 from("src/cjs")
423
424 set("ts") {
425 srcDir("ts")
426 }
427
428 outputs("js", "dts")
429 }
430 }
431 ```
432
433 ### Interpretation
434
435 This means:
436
437 * `main` contributes `ts`, `js`, and `resources`
438 * `test` contributes `ts` and `resources`
439 * `rjs` contributes `ts`
440 * `cjs` contributes `ts`
441
442 These are layer rules only.
443
444 They do not yet say which variant consumes them.
445
446 ---
447
448 ## Why `variantSources` remains open
449
450 Unlike `variants`, `variantSources` does not need to be closed in the same way.
451
452 Reasons:
453
454 * the DSL is internal to source materialization
455 * the source of truth for unit existence is already finalized in `VariantsView`
456 * `GenericSourceSetMaterializer` returns `NamedDomainObjectProvider<GenericSourceSet>`
457 * late configuration of providers is expected and acceptable
458 * adapters may need to refine source-related behavior after `variants` is finalized
459
460 Therefore:
461
462 * `variants` is finalized
463 * `variantSources` may remain open
464
465 This is not a contradiction.
466
467 It reflects the difference between:
468
469 * a closed domain model
470 * an open infrastructure/materialization model
471
472 ---
473
474 ## `VariantSourcesContext`
475
476 `variantSources.whenFinalized(...)` remains useful, but not because `variantSources` itself is frozen.
477
478 Its purpose is to provide access to a finalized context derived from `variants`.
479
480 This context contains:
481
482 * `CompileUnitsView`
483 * `RoleProjectionsView`
484 * `GenericSourceSetMaterializer`
485
486 Conceptually:
487
488 ```java
489 interface VariantSourcesContext {
490 CompileUnitsView getCompileUnits();
491 RoleProjectionsView getRoleProjections();
492 GenericSourceSetMaterializer getSourceSets();
493 }
494 ```
495
496 This callback is also replayable.
497
498 Example:
499
500 ```java
501 variantSources.whenFinalized(ctx -> {
502 var units = ctx.getCompileUnits();
503 var roles = ctx.getRoleProjections();
504 var sourceSets = ctx.getSourceSets();
505 });
506 ```
507
508 ---
509
510 ## `GenericSourceSetMaterializer`
511
512 ### Purpose
513
514 `GenericSourceSetMaterializer` is the official source of truth for materialized source sets.
515
516 It is responsible for:
517
518 * lazy creation of `GenericSourceSet`
519 * applying `layerRule`
520 * connecting a compile unit to a source set provider
521 * exposing source sets to adapters
522
523 This is the correct place to apply `layerRule`.
524
525 Adapters should not apply layer rules themselves.
526
527 ### Conceptual API
528
529 ```java
530 interface GenericSourceSetMaterializer {
531 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(CompileUnit unit);
532 }
533 ```
534
535 ---
536
537 ## Why `GenericSourceSetMaterializer` should own `layerRule` application
538
539 If adapters applied `layerRule` directly, responsibility would leak across multiple layers:
540
541 * one component would know compile units
542 * another would know source semantics
543 * another would know how to configure `GenericSourceSet`
544
545 This would make the model harder to reason about.
546
547 Instead:
548
549 * `layerRule` is DSL/spec-level
550 * `GenericSourceSetMaterializer` is execution/materialization-level
551 * adapters are consumption-level
552
553 This gives a much cleaner separation.
554
555 ---
556
557 ## `GenericSourceSet` as materialization target
558
559 `GenericSourceSet` is the materialized source aggregation object.
560
561 It is a good fit because it can represent:
562
563 * multiple logical source sets
564 * aggregated source directories
565 * declared outputs
566 * lazy registration through providers
567
568 The materializer is therefore the owner of:
569
570 * creating `GenericSourceSet`
571 * populating its source sets
572 * declaring outputs
573 * returning a provider for later use
574
575 ---
576
577 ## How an adapter should use the model
578
579 Example:
580
581 ```java
582 variantSources.whenFinalized(ctx -> {
583 for (CompileUnit unit : ctx.getCompileUnits().getUnits()) {
584 var sourceSetProvider = ctx.getSourceSets().getSourceSet(unit);
585
586 var variant = unit.variant();
587 var layer = unit.layer();
588
589 // create compile task for this compile unit
590 // configure compiler options from variant semantics
591 // use sourceSetProvider as task input
592 }
593
594 for (RoleProjection projection : ctx.getRoleProjections().getProjections()) {
595 var units = ctx.getRoleProjections().getUnits(projection);
596
597 // aggregate outputs of included compile units
598 // use for publication or assembly
599 }
600 });
601 ```
602
603 ---
604
605 ## Why compile unit is `(variant, layer)` and not `(variant, role)` or `(variant, role, layer)`
606
607 ### Not `(variant, role)`
608
609 Because role is not a compilation boundary.
610
611 Role is a logical grouping of results.
612
613 ### Not `(variant, role, layer)`
614
615 Because role does not define the compile unit itself.
616
617 A compile unit is a unit of compilation, not a unit of publication grouping.
618
619 ### Correct interpretation
620
621 * `(variant, layer)` = compile unit
622 * `(variant, role)` = logical result group
623 * `(variant, role, layer)` = membership relation between them
624
625 This is the most coherent separation.
626
627 ---
628
629 ## Model boundaries
630
631 ### What belongs to `variants`
632
633 * declared domains: `Layer`, `Role`, `Variant`
634 * normalized relation `(variant, role, layer)`
635 * finalization lifecycle
636 * finalized `VariantsView`
637
638 ### What belongs to derived views
639
640 * compile units: `(variant, layer)`
641 * role projections: `(variant, role)`
642
643 ### What belongs to `variantSources`
644
645 * source semantics of layers
646 * source materialization rules
647 * lazy `GenericSourceSet` provisioning
648 * source adapter integration
649
650 ### What does not belong to `variants`
651
652 * source directories
653 * base paths
654 * output declarations
655 * source set layout
656 * task registration
657 * compiler-specific assumptions
658
659 ---
660
661 ## Design principles
662
663 ### 1. Keep the core model small
664
665 The core model should only contain domain facts.
666
667 ### 2. Separate domain truth from materialization
668
669 The existence of compile units comes from `VariantsView`, not from source rules.
670
671 ### 3. Treat source materialization as infrastructure
672
673 `variantSources` is an interpretation layer, not the source of truth.
674
675 ### 4. Prefer replayable finalized hooks
676
677 Adapters should not depend on raw Gradle lifecycle callbacks such as `afterEvaluate`.
678
679 ### 5. Keep heavy runtime objects behind providers
680
681 Materialized `GenericSourceSet` objects should remain behind a lazy API.
682
683 ---
684
685 ## Summary
686
687 The model is intentionally split into two layers.
688
689 ### `variants`
690
691 A closed, finalized domain model:
692
693 * `Layer`
694 * `Role`
695 * `Variant`
696 * `(variant, role, layer)`
697
698 ### `variantSources`
699
700 An open, source-materialization layer:
701
702 * layer source rules
703 * compile-unit source set materialization
704 * adapter-facing `GenericSourceSet` providers
705
706 ### Derived views
707
708 From the finalized variant model:
709
710 * `CompileUnitsView`: `(variant, layer)`
711 * `RoleProjectionsView`: `(variant, role)`
712
713 ### Operational interpretation
714
715 * `variant` defines compilation semantics
716 * `layer` partitions compilation
717 * `role` groups results by purpose
718
719 This keeps the core model stable and minimal, while allowing source handling and adapter integration to remain flexible.
@@ -1,35 +1,12
1 1 package org.implab.gradle.variants;
2 2
3 import java.util.ArrayList;
4 import java.util.HashMap;
5 import java.util.HashSet;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.Set;
9 import java.util.function.Consumer;
10 import java.util.stream.Stream;
11
12 3 import org.eclipse.jdt.annotation.NonNullByDefault;
13 import org.gradle.api.Action;
14 import org.gradle.api.NamedDomainObjectCollection;
15 import org.gradle.api.NamedDomainObjectContainer;
16 import org.gradle.api.NamedDomainObjectProvider;
17 4 import org.gradle.api.Plugin;
18 5 import org.gradle.api.Project;
19 6 import org.implab.gradle.common.core.lang.Deferred;
20 import org.implab.gradle.common.core.lang.Strings;
21 import org.implab.gradle.common.sources.GenericSourceSet;
22 7 import org.implab.gradle.common.sources.SourcesPlugin;
23 import org.implab.gradle.variants.model.Layer;
24 import org.implab.gradle.variants.model.Variant;
25 import org.implab.gradle.variants.model.VariantsExtension;
26 import org.implab.gradle.variants.sources.LayerProjectionRule;
27 import org.implab.gradle.variants.sources.SourceSetMaterializer;
28 import org.implab.gradle.variants.sources.SourceSetProjection;
29 import org.implab.gradle.variants.sources.SourceSetProjections;
30 import org.implab.gradle.variants.sources.VariantLayerBinding;
8 import org.implab.gradle.variants.core.VariantsExtension;
31 9 import org.implab.gradle.variants.sources.VariantSourcesContext;
32 import org.implab.gradle.variants.sources.VariantSourcesExtension;
33 10
34 11 @NonNullByDefault
35 12 public abstract class VariantSourcesPlugin implements Plugin<Project> {
@@ -45,141 +22,7 public abstract class VariantSourcesPlug
45 22 var sources = SourcesPlugin.getSourcesExtension(target);
46 23
47 24 var deferred = new Deferred<VariantSourcesContext>();
48 var layerProjectionRules = objectFactory.domainObjectContainer(LayerProjectionRule.class);
49 25
50 var variantSourcesExtension = new VariantSourcesExtension() {
51 @Override
52 public NamedDomainObjectContainer<LayerProjectionRule> getLayerRules() {
53 return layerProjectionRules;
54 }
55
56 @Override
57 public void whenFinalized(Action<? super VariantSourcesContext> action) {
58 deferred.whenResolved(action::execute);
59 }
60 };
61 target.getExtensions().add(VariantSourcesExtension.class, "variantSources", variantSourcesExtension);
62
63 // create convention to automatically create layer projection rules for each
64 // variant layer
65 variantsExtension.getLayers().all(layer -> {
66 // Automatically create a layer projection rule for each variant layer.
67 variantSourcesExtension.layer(layer.getName(), rule -> {
68 // Configure the source set name pattern based on the layer name.
69 rule.getSourceSetNamePattern()
70 .convention("{variant}{layerCapitalized}")
71 .finalizeValueOnRead();
72 });
73 });
74
75 var projections = objectFactory.domainObjectContainer(SourceSetProjection.class);
76
77 Map<String, Set<VariantLayerBinding>> projectionBindings = new HashMap<>();
78
79 var sourceSetProjections = new SourceSetProjections() {
80 @Override
81 public NamedDomainObjectCollection<SourceSetProjection> getProjections() {
82 return projections;
83 }
84
85 @Override
86 public Set<VariantLayerBinding> getBindings(String sourceSetName) {
87 return projectionBindings.getOrDefault(sourceSetName, Set.of());
88 }
89
90 @Override
91 public Set<VariantLayerBinding> getBindings(SourceSetProjection projection) {
92 return getBindings(projection.getName());
93 }
94 };
95
96 Set<String> materializedSourceSets = new HashSet<>();
97
98 var materializer = new SourceSetMaterializer() {
99 @Override
100 public NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName) {
101 return materializedSourceSets.add(sourceSetName)
102 ? sources.register(sourceSetName)
103 : sources.named(sourceSetName);
104 }
105 };
106
107 var bindings = new VariantBindings();
108
109 target.afterEvaluate(t -> {
110 // Once the project is evaluated, resolve the deferred context and finalize the
111 // sources configuration.
112 variantsExtension.getLayers().all(bindings::addLayer);
113 variantsExtension.getVariants().all(bindings::addVariant);
114
115 variantsExtension.getLayers().all(layer -> {
116 // For each layer, apply the projection rules to generate source set projections.
117
118 var rule = layerProjectionRules.maybeCreate(layer.getName());
119 var pattern = rule.getSourceSetNamePattern().getOrElse("{variant}{layerCapitalized}");
120 // Generate source set names based on the pattern and variant/layer information.
121 // This is a simplified example; real implementation would need to consider
122 // all variants and layers.
123 var sourceSetName = pattern.replace("{layer}", layer.getName())
124 .replace("{variant}", "main") // Placeholder for actual variant name
125 .replace("{layerCapitalized}", Strings.capitalize(layer.getName()));
126
127 var projection = objectFactory.newInstance(SourceSetProjection.class, sourceSetName);
128 projections.add(projection);
129 // Bind the projection to the corresponding variant layer.
130 projectionBindings.computeIfAbsent(sourceSetName, k -> new HashSet<>())
131 .add(new VariantLayerBinding(layer.getName(), projection));
132 });
133
134 var context = new VariantSourcesContext() {
135
136 @Override
137 public SourceSetProjections getProjections() {
138 return sourceSetProjections;
139 }
140
141 @Override
142 public SourceSetMaterializer getMaterializer() {
143 return materializer;
144 }
145
146 // Implementation of the context that provides access to variant and layer
147 // information.
148 };
149 deferred.resolve(context);
150 });
151 }
152
153 class VariantBindings {
154 private final Set<Layer> layers = new HashSet<>();
155 private final Set<Variant> variants = new HashSet<>();
156
157 private final List<Consumer<? super VariantLayerBinding>> listeners = new ArrayList<>();
158
159 void addLayer(Layer layer) {
160 layers.add(layer);
161 variants.stream()
162 .map(variant -> VariantLayerBinding.of(variant, layer))
163 .forEach(this::notifyBindingAdded);
164 }
165
166 void addVariant(Variant variant) {
167 variants.add(variant);
168 layers.stream()
169 .map(layer -> VariantLayerBinding.of(variant, layer))
170 .forEach(this::notifyBindingAdded);
171 }
172
173 void whenBindingAdded(Consumer<? super VariantLayerBinding> listener) {
174 layers.stream()
175 .flatMap(layer -> variants.stream().map(variant -> VariantLayerBinding.of(variant, layer)))
176 .forEach(listener);
177 listeners.add(listener);
178 }
179
180 private void notifyBindingAdded(VariantLayerBinding binding) {
181 listeners.forEach(listener -> listener.accept(binding));
182 }
183 26 }
184 27
185 28 }
@@ -1,15 +1,101
1 1 package org.implab.gradle.variants;
2 2
3 import org.gradle.api.Action;
4 import org.gradle.api.NamedDomainObjectContainer;
3 5 import org.gradle.api.Plugin;
4 6 import org.gradle.api.Project;
5 import org.implab.gradle.variants.model.VariantsExtension;
7 import org.gradle.api.model.ObjectFactory;
8 import org.implab.gradle.common.core.lang.Deferred;
9 import org.implab.gradle.variants.core.Layer;
10 import org.implab.gradle.variants.core.Role;
11 import org.implab.gradle.variants.core.Variant;
12 import org.implab.gradle.variants.core.VariantDefinition;
13 import org.implab.gradle.variants.core.VariantsExtension;
14 import org.implab.gradle.variants.core.VariantsView;
6 15
16 /**
17 * <ul>
18 * <li> {@link Variant} defines compilation semantics
19 * <li> {@link Layer} defines compilation partition
20 * <li> {@link Role} defines result grouping / publication intent
21 * </ul>
22 *
23 */
7 24 public abstract class VariantsPlugin implements Plugin<Project> {
8 25 @Override
9 26 public void apply(Project target) {
10 27
11 target.getExtensions().create("variants", VariantsExtension.class);
28 var objectFactory = target.getObjects();
29
30 var variantsExtension = new DefaultVariantsExtension(objectFactory);
31 target.getExtensions().add(VariantsExtension.class, "variants", variantsExtension);
32 target.afterEvaluate(t -> variantsExtension.finalizeExtension());
12 33
13 34 }
14 35
36 static class DefaultVariantsExtension implements VariantsExtension {
37
38 private final NamedDomainObjectContainer<Layer> layers;
39 private final NamedDomainObjectContainer<Role> roles;
40 private final NamedDomainObjectContainer<VariantDefinition> variantDefinitions;
41 private final Deferred<VariantsView> finalizedResult = new Deferred<>();
42 private final ObjectFactory objectFactory;
43 private boolean finalized = false;
44
45 public DefaultVariantsExtension(ObjectFactory objectFactory) {
46 this.objectFactory = objectFactory;
47 this.layers = objectFactory.domainObjectContainer(Layer.class);
48 this.roles = objectFactory.domainObjectContainer(Role.class);
49 this.variantDefinitions = objectFactory.domainObjectContainer(VariantDefinition.class);
15 50 }
51
52 @Override
53 public NamedDomainObjectContainer<Layer> getLayers() {
54 return layers;
55 }
56
57 @Override
58 public NamedDomainObjectContainer<Role> getRoles() {
59 return roles;
60 }
61
62 @Override
63 public NamedDomainObjectContainer<VariantDefinition> getVariantDefinitions() {
64 return variantDefinitions;
65 }
66
67 @Override
68 public void whenFinalized(Action<? super VariantsView> action) {
69 finalizedResult.whenResolved(action::execute);
70 }
71
72 void finalizeExtension() {
73 if (finalized)
74 return;
75
76 finalized = true;
77
78 // freeze defined variants
79 variantDefinitions.forEach(VariantDefinition::finalizeVariant);
80
81 // build a snapshot
82 var viewBuilder = VariantsView.builder();
83
84 // calculate and add variants
85 variantDefinitions.stream()
86 .map(def -> objectFactory.named(Variant.class, def.getName()))
87 .forEach(viewBuilder::addVariant);
88 // add layers
89 layers.forEach(viewBuilder::addLayer);
90 // add roles
91 roles.forEach(viewBuilder::addRole);
92 // add definitions
93 variantDefinitions.forEach(viewBuilder::addDefinition);
94 // assemble the view
95 var view = viewBuilder.build();
96 // set the result and call hooks
97 finalizedResult.resolve(view);
98 }
99 }
100
101 }
@@ -1,4 +1,4
1 package org.implab.gradle.variants.model;
1 package org.implab.gradle.variants.core;
2 2
3 3 import org.gradle.api.Named;
4 4
@@ -1,4 +1,4
1 package org.implab.gradle.variants.model;
1 package org.implab.gradle.variants.core;
2 2
3 3 import org.gradle.api.Named;
4 4
@@ -1,8 +1,8
1 package org.implab.gradle.variants.model;
1 package org.implab.gradle.variants.core;
2 2
3 3 /** A binding between a role and a layer inside a specific variant.
4 4 *
5 5 * @see {@link VariantDefinition} for the context of this binding.
6 6 */
7 public record RoleLayerBinding(String name, String layerName) {
7 public record RoleLayerBinding(String roleName, String layerName) {
8 8 } No newline at end of file
@@ -1,4 +1,4
1 package org.implab.gradle.variants.model;
1 package org.implab.gradle.variants.core;
2 2
3 3 import org.gradle.api.Named;
4 4
@@ -1,4 +1,4
1 package org.implab.gradle.variants.model;
1 package org.implab.gradle.variants.core;
2 2
3 3 import java.util.HashSet;
4 4 import java.util.Set;
@@ -1,4 +1,4
1 package org.implab.gradle.variants.model;
1 package org.implab.gradle.variants.core;
2 2
3 3 import org.gradle.api.Action;
4 4 import org.gradle.api.NamedDomainObjectContainer;
@@ -19,6 +19,7 import groovy.lang.Closure;
19 19 * }
20 20 * }
21 21 * }
22 *
22 23 */
23 24 public interface VariantsExtension {
24 25
@@ -33,11 +34,6 public interface VariantsExtension {
33 34 NamedDomainObjectContainer<Role> getRoles();
34 35
35 36 /**
36 * Domain of variants.
37 */
38 NamedDomainObjectContainer<Variant> getVariants();
39
40 /**
41 37 * Declared variants.
42 38 */
43 39 NamedDomainObjectContainer<VariantDefinition> getVariantDefinitions();
@@ -46,7 +42,6 public interface VariantsExtension {
46 42 * Creates or returns an existing variant and configures it.
47 43 */
48 44 default VariantDefinition variant(String name) {
49
50 45 return getVariantDefinitions().maybeCreate(name);
51 46 }
52 47
@@ -62,4 +57,18 public interface VariantsExtension {
62 57 default VariantDefinition variant(String name, Closure<?> closure) {
63 58 return variant(name, Closures.action(closure));
64 59 }
60
61 /**
62 * Registers an action to be executed when the extension is finalized.
63 *
64 * The specified action will receive the unmodifiable snapshot of the variants.
65 *
66 * @param action The callback to be executed when variants are finalized
67 */
68 void whenFinalized(Action<? super VariantsView> action);
69
70 default void whenFinalized(Closure<?> action) {
71 whenFinalized(Closures.action(action));
72 }
73
65 74 } No newline at end of file
@@ -2,14 +2,17 package org.implab.gradle.variants.sourc
2 2
3 3 import org.gradle.api.NamedDomainObjectProvider;
4 4 import org.implab.gradle.common.sources.GenericSourceSet;
5 import org.implab.gradle.variants.derived.CompileUnit;
5 6
6 7 /**
7 * Materializes symbolic source set names into actual GenericSourceSet instances.
8 * Materializes symbolic source set names into actual GenericSourceSet
9 * instances.
8 10 */
9 11 public interface SourceSetMaterializer {
10 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
11
12 default NamedDomainObjectProvider<GenericSourceSet> getSourceSet(SourceSetProjection projection) {
13 return getSourceSet(projection.getName());
14 }
12 /**
13 * Returns a lazy provider for the source set corresponding to the compile unit.
14 *
15 * The provider is stable and cached per compile unit.
16 */
17 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(CompileUnit unit);
15 18 } No newline at end of file
@@ -1,7 +1,50
1 1 package org.implab.gradle.variants.sources;
2 2
3 import java.util.Set;
4
5 import org.gradle.api.Action;
6 import org.gradle.api.NamedDomainObjectCollection;
7 import org.implab.gradle.common.sources.GenericSourceSet;
8 import org.implab.gradle.variants.core.Layer;
9 import org.implab.gradle.variants.core.VariantsView;
10 import org.implab.gradle.variants.derived.CompileUnitsView;
11 import org.implab.gradle.variants.derived.RoleProjectionsView;
12
13 /**
14 * Registry of symbolic source set names produced by sources projection.
15 *
16 * Identity in this registry is the GenericSourceSet name.
17 */
3 18 public interface VariantSourcesContext {
4 SourceSetProjections getProjections();
19
20 /**
21 * Finalized core model.
22 */
23 VariantsView getVariants();
5 24
6 SourceSetMaterializer getMaterializer();
7 }
25 /**
26 * Derived compile-side view.
27 */
28 CompileUnitsView getCompileUnits();
29
30 /**
31 * Derived role-side view.
32 */
33 RoleProjectionsView getRoleProjections();
34
35 /**
36 * Lazy source set provider service.
37 */
38 SourceSetMaterializer getSourceSets();
39
40 /**
41 * Configures all GenericSourceSets produced from the given layer.
42 *
43 * The action is applied:
44 * - to already materialized source sets of this layer
45 * - to all future source sets of this layer
46 *
47 * Actions are applied in registration order.
48 */
49 void configureLayer(Layer layer, Action<? super GenericSourceSet> action);
50 } No newline at end of file
@@ -1,7 +1,6
1 1 package org.implab.gradle.variants.sources;
2 2
3 3 import org.gradle.api.Action;
4 import org.gradle.api.NamedDomainObjectContainer;
5 4 import org.implab.gradle.common.core.lang.Closures;
6 5
7 6 import groovy.lang.Closure;
@@ -9,26 +8,12 import groovy.lang.Closure;
9 8 public interface VariantSourcesExtension {
10 9
11 10 /**
12 * Projection rules keyed by layer name.
13 */
14 NamedDomainObjectContainer<LayerProjectionRule> getLayerRules();
15
16 /**
17 * Creates or returns an existing layer rule and configures it.
11 * Invoked when finalized variants-derived source context becomes available.
12 *
13 * Replayable:
14 * - if called before variants finalization, action is queued
15 * - if called after variants finalization, action is invoked immediately
18 16 */
19 default LayerProjectionRule layerRule(String name) {
20 return getLayerRules().maybeCreate(name);
21 }
22
23 /**
24 * Creates or returns an existing layer rule and configures it.
25 */
26 default LayerProjectionRule layer(String name, Action<? super LayerProjectionRule> action) {
27 LayerProjectionRule rule = layerRule(name);
28 action.execute(rule);
29 return rule;
30 }
31
32 17 void whenFinalized(Action<? super VariantSourcesContext> action);
33 18
34 19 default void whenFinalized(Closure<?> closure) {
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now