| @@ -1,296 +1,306 | |||
|
|
1 | 1 | # gradle-common |
|
|
2 | 2 | |
|
|
3 | 3 | Java 21 multi-project build with shared Gradle utilities and experimental |
|
|
4 | 4 | variant-oriented Gradle plugins. |
|
|
5 | 5 | |
|
|
6 | 6 | The repository currently publishes to a local Ivy repository only. Maven Central |
|
|
7 | 7 | and Gradle Plugin Portal publication are intentionally not configured yet. |
|
|
8 | 8 | |
|
|
9 | 9 | ## Modules |
|
|
10 | 10 | |
|
|
11 | 11 | - `common` - shared Gradle utilities, JSON helpers, shell execution helpers, and |
|
|
12 | 12 | small core value/util classes. |
|
|
13 | 13 | - `variants` - Gradle plugins for variant topology, source-set materialization, |
|
|
14 | 14 | and outgoing artifact slots. |
|
|
15 | 15 | |
|
|
16 | 16 | ## Requirements |
|
|
17 | 17 | |
|
|
18 | 18 | - JDK 21. |
|
|
19 | 19 | - Gradle Wrapper from this repository, currently Gradle 8.10.2. |
|
|
20 | 20 | |
|
|
21 | 21 | The produced bytecode targets Java 21. |
|
|
22 | 22 | |
|
|
23 | 23 | ## License |
|
|
24 | 24 | |
|
|
25 | 25 | This project is licensed under the BSD 2-Clause "Simplified" License |
|
|
26 | 26 | (`BSD-2-Clause`). See [LICENSE](LICENSE). |
|
|
27 | 27 | |
|
|
28 | 28 | ## Local Build |
|
|
29 | 29 | |
|
|
30 | 30 | ```bash |
|
|
31 | 31 | ./gradlew clean check javadoc jar sourcesJar javadocJar --rerun-tasks |
|
|
32 | 32 | ``` |
|
|
33 | 33 | |
|
|
34 | 34 | Configuration cache smoke check: |
|
|
35 | 35 | |
|
|
36 | 36 | ```bash |
|
|
37 | 37 | ./gradlew check --configuration-cache |
|
|
38 | 38 | ``` |
|
|
39 | 39 | |
|
|
40 | 40 | ## Local Ivy Publication |
|
|
41 | 41 | |
|
|
42 | 42 | The current publication target is: |
|
|
43 | 43 | |
|
|
44 | 44 | ```text |
|
|
45 | 45 | ${user.home}/ivy-repo |
|
|
46 | 46 | ``` |
|
|
47 | 47 | |
|
|
48 | 48 | Publish both modules locally: |
|
|
49 | 49 | |
|
|
50 | 50 | ```bash |
|
|
51 | 51 | ./gradlew :common:publishIvyPublicationToIvyRepository \ |
|
|
52 | 52 | :variants:publishIvyPublicationToIvyRepository |
|
|
53 | 53 | ``` |
|
|
54 | 54 | |
|
|
55 | 55 | or: |
|
|
56 | 56 | |
|
|
57 | 57 | ```bash |
|
|
58 | 58 | ./gradlew publish |
|
|
59 | 59 | ``` |
|
|
60 | 60 | |
|
|
61 | 61 | The publication includes: |
|
|
62 | 62 | |
|
|
63 | 63 | - main jar |
|
|
64 | 64 | - sources jar |
|
|
65 | 65 | - javadoc jar |
|
|
66 | 66 | - Ivy descriptor |
|
|
67 | 67 | - Gradle module metadata |
|
|
68 | 68 | |
|
|
69 | 69 | ## Local Consumption |
|
|
70 | 70 | |
|
|
71 | 71 | Current plugin ids are packaged as classic Gradle plugin marker resources inside |
|
|
72 | 72 | the `variants` jar: |
|
|
73 | 73 | |
|
|
74 | 74 | - `org.implab.gradle-variants` |
|
|
75 | 75 | - `org.implab.gradle-sources` |
|
|
76 | 76 | - `org.implab.gradle-variants-sources` |
|
|
77 | 77 | - `org.implab.gradle-variants-artifacts` |
|
|
78 | 78 | |
|
|
79 | 79 | Until Gradle Plugin Portal marker artifacts are configured, consume the plugin |
|
|
80 | 80 | through `buildscript` classpath: |
|
|
81 | 81 | |
|
|
82 | 82 | ```groovy |
|
|
83 | 83 | buildscript { |
|
|
84 | 84 | repositories { |
|
|
85 | 85 | ivy { |
|
|
86 | 86 | url "${System.properties['user.home']}/ivy-repo" |
|
|
87 | 87 | } |
|
|
88 | 88 | mavenCentral() |
|
|
89 | 89 | } |
|
|
90 | 90 | dependencies { |
|
|
91 | 91 | classpath 'org.implab.gradle:variants:0.1.0' |
|
|
92 | 92 | } |
|
|
93 | 93 | } |
|
|
94 | 94 | |
|
|
95 | 95 | apply plugin: 'org.implab.gradle-variants' |
|
|
96 | 96 | apply plugin: 'org.implab.gradle-variants-sources' |
|
|
97 | 97 | ``` |
|
|
98 | 98 | |
|
|
99 | 99 | The `plugins { id(...) version(...) }` DSL is not part of the current local Ivy |
|
|
100 | 100 | contract. |
|
|
101 | 101 | |
|
|
102 | 102 | ## Variants DSL |
|
|
103 | 103 | |
|
|
104 | 104 | `variants` defines the finalized build topology. It does not create compile |
|
|
105 | 105 | tasks, source directories, or outgoing publications by itself. |
|
|
106 | 106 | |
|
|
107 | 107 | ```groovy |
|
|
108 | 108 | apply plugin: 'org.implab.gradle-variants' |
|
|
109 | 109 | |
|
|
110 | 110 | variants.layers.create('main') |
|
|
111 | 111 | variants.layers.create('test') |
|
|
112 | 112 | variants.roles.create('main') |
|
|
113 | 113 | variants.roles.create('test') |
|
|
114 | 114 | |
|
|
115 | 115 | variants.variant('browser') { |
|
|
116 | 116 | role('main') { |
|
|
117 | 117 | layers('main') |
|
|
118 | 118 | } |
|
|
119 | 119 | role('test') { |
|
|
120 | 120 | layers('main', 'test') |
|
|
121 | 121 | } |
|
|
122 | 122 | } |
|
|
123 | 123 | |
|
|
124 | 124 | variants.whenFinalized { view -> |
|
|
125 | 125 | println view.entries.collect { |
|
|
126 | 126 | "${it.variant().name}:${it.role().name}:${it.layer().name}" |
|
|
127 | 127 | }.sort() |
|
|
128 | 128 | } |
|
|
129 | 129 | ``` |
|
|
130 | 130 | |
|
|
131 | 131 | The finalized model exposes cheap identity objects: `Variant`, `Role`, `Layer`, |
|
|
132 | 132 | and the normalized relation `(variant, role, layer)`. |
|
|
133 | 133 | |
|
|
134 | 134 | ## Sources DSL |
|
|
135 | 135 | |
|
|
136 | 136 | `sources` creates standalone `GenericSourceSet` objects. This is useful for |
|
|
137 | 137 | fallback workflows that do not need variant topology. |
|
|
138 | 138 | |
|
|
139 | 139 | ```groovy |
|
|
140 | 140 | apply plugin: 'org.implab.gradle-sources' |
|
|
141 | 141 | |
|
|
142 | 142 | sources.create('main') { |
|
|
143 | 143 | declareOutputs('js') |
|
|
144 | 144 | registerOutput('js', layout.projectDirectory.file('inputs/main.js')) |
|
|
145 | 145 | |
|
|
146 | 146 | sets.create('ts') { |
|
|
147 | 147 | srcDir 'src/main/ts' |
|
|
148 | 148 | } |
|
|
149 | 149 | } |
|
|
150 | 150 | ``` |
|
|
151 | 151 | |
|
|
152 | 152 | `SourcesPlugin` applies layout conventions: |
|
|
153 | 153 | |
|
|
154 | 154 | - `sourceSetDir = src/<sourceSet>` |
|
|
155 | 155 | - `outputsDir = build/out/<sourceSet>` |
|
|
156 | 156 | |
|
|
157 | 157 | The base `GenericSourceSet` model itself is convention-free. |
|
|
158 | 158 | |
|
|
159 | 159 | ## Variant Sources DSL |
|
|
160 | 160 | |
|
|
161 | 161 | `variantSources` derives compile units from finalized `variants`. |
|
|
162 | 162 | |
|
|
163 | 163 | A compile unit is: |
|
|
164 | 164 | |
|
|
165 | 165 | ```text |
|
|
166 | 166 | (variant, layer) |
|
|
167 | 167 | ``` |
|
|
168 | 168 | |
|
|
169 | 169 | Selectors configure materialized compile-unit source sets: |
|
|
170 | 170 | |
|
|
171 | 171 | ```groovy |
|
|
172 | 172 | apply plugin: 'org.implab.gradle-variants-sources' |
|
|
173 | 173 | |
|
|
174 | 174 | variants.layers.create('main') |
|
|
175 | 175 | variants.roles.create('main') |
|
|
176 | 176 | variants.variant('browser') { |
|
|
177 | 177 | role('main') { |
|
|
178 | 178 | layers('main') |
|
|
179 | 179 | } |
|
|
180 | 180 | } |
|
|
181 | 181 | |
|
|
182 | 182 | variantSources { |
|
|
183 | 183 | layer('main') { |
|
|
184 | 184 | sourceSet { |
|
|
185 | 185 | declareOutputs('js') |
|
|
186 | 186 | registerOutput('js', layout.projectDirectory.file('inputs/browser.js')) |
|
|
187 | 187 | } |
|
|
188 | 188 | } |
|
|
189 | 189 | |
|
|
190 | 190 | configureEach { |
|
|
191 | 191 | println "sourceSet=${sourceSet.name}, variant=${variant.name}, layer=${layer.name}" |
|
|
192 | 192 | } |
|
|
193 | 193 | } |
|
|
194 | 194 | ``` |
|
|
195 | 195 | |
|
|
196 | 196 | Selector order for future materialization is: |
|
|
197 | 197 | |
|
|
198 | 198 | ```text |
|
|
199 | 199 | configureEach -> variant -> layer -> unit |
|
|
200 | 200 | ``` |
|
|
201 | 201 | |
|
|
202 | 202 | Late selector registration is controlled by: |
|
|
203 | 203 | |
|
|
204 | 204 | ```groovy |
|
|
205 | 205 | variantSources { |
|
|
206 | 206 | lateConfigurationPolicy { |
|
|
207 | 207 | failOnLateConfiguration() |
|
|
208 | 208 | } |
|
|
209 | 209 | } |
|
|
210 | 210 | ``` |
|
|
211 | 211 | |
|
|
212 | 212 | Compile-unit source set names are generated by default as: |
|
|
213 | 213 | |
|
|
214 | 214 | ```text |
|
|
215 | 215 | <variant> + capitalize(<layer>) |
|
|
216 | 216 | ``` |
|
|
217 | 217 | |
|
|
218 | 218 | Name collisions fail by default and may be resolved deterministically: |
|
|
219 | 219 | |
|
|
220 | 220 | ```groovy |
|
|
221 | 221 | variantSources { |
|
|
222 | 222 | namingPolicy { |
|
|
223 | 223 | resolveNameCollision() |
|
|
224 | 224 | } |
|
|
225 | 225 | } |
|
|
226 | 226 | ``` |
|
|
227 | 227 | |
|
|
228 | 228 | ## Variant Artifacts DSL |
|
|
229 | 229 | |
|
|
230 | 230 | `variantArtifacts` is an experimental outgoing artifact layer over `variants` |
|
|
231 | 231 | and `variantSources`. |
|
|
232 | 232 | |
|
|
233 | 233 | The current model maps: |
|
|
234 | 234 | |
|
|
235 | 235 | - `Variant` to a variant-level consumable outgoing configuration. |
|
|
236 | 236 | - `Slot` to a Gradle outgoing artifact variant inside that configuration. |
|
|
237 | 237 | - `primarySlot` to the primary artifact set of the outgoing configuration. |
|
|
238 | 238 | |
|
|
239 | 239 | ```groovy |
|
|
240 | 240 | apply plugin: 'org.implab.gradle-variants-artifacts' |
|
|
241 | 241 | |
|
|
242 | 242 | variants.layers.create('main') |
|
|
243 | 243 | variants.roles.create('main') |
|
|
244 | 244 | variants.variant('browser') { |
|
|
245 | 245 | role('main') { |
|
|
246 | 246 | layers('main') |
|
|
247 | 247 | } |
|
|
248 | 248 | } |
|
|
249 | 249 | |
|
|
250 | 250 | variantSources.layer('main') { |
|
|
251 | 251 | sourceSet { |
|
|
252 | 252 | declareOutputs('types', 'js') |
|
|
253 | 253 | registerOutput('types', layout.projectDirectory.file('inputs/index.d.ts')) |
|
|
254 | 254 | registerOutput('js', layout.projectDirectory.file('inputs/index.js')) |
|
|
255 | 255 | } |
|
|
256 | 256 | } |
|
|
257 | 257 | |
|
|
258 | 258 | variantArtifacts { |
|
|
259 | 259 | variant('browser') { |
|
|
260 | 260 | primarySlot('typesPackage') { |
|
|
261 | 261 | fromVariant { |
|
|
262 | 262 | output('types') |
|
|
263 | 263 | } |
|
|
264 | 264 | } |
|
|
265 | 265 | slot('js') { |
|
|
266 | 266 | fromVariant { |
|
|
267 | 267 | output('js') |
|
|
268 | 268 | } |
|
|
269 | 269 | } |
|
|
270 | 270 | } |
|
|
271 | 271 | |
|
|
272 | 272 | whenOutgoingConfiguration { publication -> |
|
|
273 | 273 | publication.configuration { |
|
|
274 | 274 | description = "Outgoing contract for ${publication.variant.name}" |
|
|
275 | 275 | } |
|
|
276 | 276 | } |
|
|
277 | 277 | |
|
|
278 | 278 | whenOutgoingSlot { publication -> |
|
|
279 | 279 | println "slot=${publication.artifactSlot.slot.name}, primary=${publication.primary}" |
|
|
280 | 280 | } |
|
|
281 | 281 | } |
|
|
282 | 282 | ``` |
|
|
283 | 283 | |
|
|
284 | Slot bodies have two assembly modes: | |
|
|
285 | ||
|
|
286 | - contribution-based assembly with `from(...)`, `fromVariant(...)`, | |
|
|
287 | `fromRole(...)`, or `fromLayer(...)`; the plugin copies selected inputs into a | |
|
|
288 | managed directory and publishes that directory; | |
|
|
289 | - task-produced assembly with `producedBy(task) { outputFile }`; the mapped task | |
|
|
290 | output file or directory is published directly. | |
|
|
291 | ||
|
|
292 | These modes are mutually exclusive for one slot. | |
|
|
293 | ||
|
|
284 | 294 | The artifact API is still considered pre-1.0 and may change. |
|
|
285 | 295 | |
|
|
286 | 296 | ## Publication Status |
|
|
287 | 297 | |
|
|
288 | 298 | Current status: |
|
|
289 | 299 | |
|
|
290 | 300 | - local Ivy publication only |
|
|
291 | 301 | - no Maven Central publication metadata |
|
|
292 | 302 | - no signing |
|
|
293 | 303 | - no Gradle Plugin Portal marker artifacts |
|
|
294 | 304 | - BSD-2-Clause license committed |
|
|
295 | 305 | |
|
|
296 | 306 | Before external publication, see [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md). |
| @@ -1,698 +1,710 | |||
|
|
1 | 1 | # Variants and Variant Artifacts |
|
|
2 | 2 | |
|
|
3 | 3 | > Design note. The current user-facing DSL and local publication workflow are |
|
|
4 | 4 | > documented in [README.md](README.md). Some snippets below are conceptual and |
|
|
5 | 5 | > describe model direction rather than exact public API. |
|
|
6 | 6 | |
|
|
7 | 7 | ## Overview |
|
|
8 | 8 | |
|
|
9 | 9 | This document describes the artifact model built on top of `variants` and |
|
|
10 | 10 | `variantSources`. |
|
|
11 | 11 | |
|
|
12 | 12 | The goal is to define: |
|
|
13 | 13 | |
|
|
14 | 14 | - a stable artifact-facing model for outgoing contracts; |
|
|
15 | 15 | - a DSL for declaring slots and their inputs; |
|
|
16 | 16 | - a resolver bridge between `variantArtifacts` and `variantSources`; |
|
|
17 | 17 | - extension points for deduplication and similar policies; |
|
|
18 | 18 | - a model that remains live during configuration and does not require an |
|
|
19 | 19 | artificial freeze of slot content. |
|
|
20 | 20 | |
|
|
21 | 21 | The design follows the same split already used elsewhere: |
|
|
22 | 22 | |
|
|
23 | 23 | - `variants` defines the closed domain topology; |
|
|
24 | 24 | - `variantSources` defines source materialization semantics over that topology; |
|
|
25 | 25 | - `variantArtifacts` defines outgoing artifact contracts over that topology. |
|
|
26 | 26 | |
|
|
27 | 27 | --- |
|
|
28 | 28 | |
|
|
29 | 29 | ## Core idea |
|
|
30 | 30 | |
|
|
31 | 31 | `variantArtifacts` is not the owner of the variant model and not the owner of |
|
|
32 | 32 | source-set materialization. |
|
|
33 | 33 | |
|
|
34 | 34 | Its purpose is narrower: |
|
|
35 | 35 | |
|
|
36 | 36 | - decide which variants participate in outgoing publication; |
|
|
37 | 37 | - define artifact slots for those outgoing variants; |
|
|
38 | 38 | - declare how each slot gathers its inputs. |
|
|
39 | 39 | |
|
|
40 | 40 | This makes `variantArtifacts` an outgoing-contract layer, not a compilation or |
|
|
41 | 41 | source-materialization layer. |
|
|
42 | 42 | |
|
|
43 | 43 | --- |
|
|
44 | 44 | |
|
|
45 | 45 | ## Model boundaries |
|
|
46 | 46 | |
|
|
47 | 47 | ### What belongs to `variants` |
|
|
48 | 48 | |
|
|
49 | 49 | - identities: `Variant`, `Role`, `Layer`; |
|
|
50 | 50 | - normalized topology relation `(variant, role, layer)`; |
|
|
51 | 51 | - finalization of the core domain model; |
|
|
52 | 52 | - `VariantsView` and derived topology views. |
|
|
53 | 53 | |
|
|
54 | 54 | ### What belongs to `variantSources` |
|
|
55 | 55 | |
|
|
56 | 56 | - compile units and role projections derived from finalized `variants`; |
|
|
57 | 57 | - source-set naming policy; |
|
|
58 | 58 | - source-set materialization; |
|
|
59 | 59 | - lazy access to `GenericSourceSet` providers and their named outputs. |
|
|
60 | 60 | |
|
|
61 | 61 | ### What belongs to `variantArtifacts` |
|
|
62 | 62 | |
|
|
63 | 63 | - selection of outgoing variants; |
|
|
64 | 64 | - slot identities inside an outgoing variant; |
|
|
65 | 65 | - slot assembly declarations; |
|
|
66 | 66 | - root outgoing configurations for outgoing variants; |
|
|
67 | 67 | - slot-level assembly bodies; |
|
|
68 | 68 | - publication-facing hooks. |
|
|
69 | 69 | |
|
|
70 | 70 | ### What does not belong to `variantArtifacts` |
|
|
71 | 71 | |
|
|
72 | 72 | - ownership of variant existence; |
|
|
73 | 73 | - ownership of source-set naming; |
|
|
74 | 74 | - direct mutation of source materialization internals; |
|
|
75 | 75 | - compiler- or toolchain-specific logic; |
|
|
76 | 76 | - eager flattening of source inputs into files during DSL declaration. |
|
|
77 | 77 | |
|
|
78 | 78 | --- |
|
|
79 | 79 | |
|
|
80 | 80 | ## Outgoing subset |
|
|
81 | 81 | |
|
|
82 | 82 | Not every declared `Variant` must become outgoing. |
|
|
83 | 83 | |
|
|
84 | 84 | `variantArtifacts` defines an outgoing subset of variants. |
|
|
85 | 85 | |
|
|
86 | 86 | For each outgoing variant there is one root outgoing aggregate: |
|
|
87 | 87 | |
|
|
88 | 88 | - one root consumable `Configuration`; |
|
|
89 | 89 | - one or more artifact slots; |
|
|
90 | 90 | - one primary slot; |
|
|
91 | 91 | - optional secondary slots. |
|
|
92 | 92 | |
|
|
93 | 93 | This is why `OutgoingConfiguration` is a real model object and not merely a |
|
|
94 | 94 | publication event payload. |
|
|
95 | 95 | |
|
|
96 | 96 | It represents a live build-facing aggregate: |
|
|
97 | 97 | |
|
|
98 | 98 | - it has its own attributes; |
|
|
99 | 99 | - it is visible to other build logic as soon as registered; |
|
|
100 | 100 | - it contains lazy handles rather than eagerly materialized state; |
|
|
101 | 101 | - it is distinct from slot assembly state. |
|
|
102 | 102 | |
|
|
103 | 103 | --- |
|
|
104 | 104 | |
|
|
105 | 105 | ## Identity-first split |
|
|
106 | 106 | |
|
|
107 | 107 | The artifact model should preserve the same identity-first principle used for |
|
|
108 | 108 | the rest of the project. |
|
|
109 | 109 | |
|
|
110 | 110 | ### Identity objects |
|
|
111 | 111 | |
|
|
112 | 112 | - `Variant` |
|
|
113 | 113 | - `Slot` |
|
|
114 | 114 | - `ArtifactSlot = (variant, slot)` |
|
|
115 | 115 | |
|
|
116 | 116 | These objects are: |
|
|
117 | 117 | |
|
|
118 | 118 | - cheap; |
|
|
119 | 119 | - replayable; |
|
|
120 | 120 | - suitable for discovery and selection. |
|
|
121 | 121 | |
|
|
122 | 122 | ### Stateful objects |
|
|
123 | 123 | |
|
|
124 | 124 | - `OutgoingConfiguration` |
|
|
125 | 125 | - `ArtifactAssembly` |
|
|
126 | 126 | |
|
|
127 | 127 | These objects are: |
|
|
128 | 128 | |
|
|
129 | 129 | - build-facing; |
|
|
130 | 130 | - allowed to contain Gradle lazy handles; |
|
|
131 | 131 | - obtained through dedicated APIs rather than embedded into identity objects. |
|
|
132 | 132 | |
|
|
133 | 133 | ### Rule of thumb |
|
|
134 | 134 | |
|
|
135 | 135 | - slot identity is cheap and replayable; |
|
|
136 | 136 | - inside one `OutgoingConfiguration`, `Slot` is the natural local identity; |
|
|
137 | 137 | - `ArtifactSlot` is useful when slot identity must be referenced outside the |
|
|
138 | 138 | parent outgoing configuration; |
|
|
139 | 139 | - slot body is stateful and resolved separately; |
|
|
140 | 140 | - outgoing configuration is a live aggregate, not a mere snapshot. |
|
|
141 | 141 | |
|
|
142 | 142 | --- |
|
|
143 | 143 | |
|
|
144 | 144 | ## Artifact model |
|
|
145 | 145 | |
|
|
146 | 146 | Conceptually: |
|
|
147 | 147 | |
|
|
148 | 148 | ```java |
|
|
149 | 149 | interface VariantArtifactsContext { |
|
|
150 | 150 | VariantsView getVariants(); |
|
|
151 | 151 | void all(Action<? super OutgoingConfiguration> action); |
|
|
152 | 152 | Optional<OutgoingConfiguration> findArtifacts(Variant variant); |
|
|
153 | 153 | OutgoingConfiguration requireArtifacts(Variant variant); |
|
|
154 | 154 | ArtifactAssemblies getAssemblies(); |
|
|
155 | 155 | } |
|
|
156 | 156 | ``` |
|
|
157 | 157 | |
|
|
158 | 158 | ```java |
|
|
159 | 159 | interface OutgoingConfiguration { |
|
|
160 | 160 | Variant getVariant(); |
|
|
161 | 161 | NamedDomainObjectProvider<Configuration> getOutgoingConfiguration(); |
|
|
162 | 162 | NamedDomainObjectContainer<Slot> getSlots(); |
|
|
163 | 163 | Property<Slot> getPrimarySlot(); |
|
|
164 | 164 | } |
|
|
165 | 165 | ``` |
|
|
166 | 166 | |
|
|
167 | 167 | ```java |
|
|
168 | 168 | interface ArtifactAssemblies { |
|
|
169 | 169 | ArtifactAssembly resolveSlot(ArtifactSlot slot); |
|
|
170 | 170 | } |
|
|
171 | 171 | ``` |
|
|
172 | 172 | |
|
|
173 | 173 | This intentionally uses a Gradle-style local slot container rather than a |
|
|
174 | 174 | separate `ArtifactSlotsView`. |
|
|
175 | 175 | |
|
|
176 | 176 | The reason is simple: |
|
|
177 | 177 | |
|
|
178 | 178 | - inside one `OutgoingConfiguration`, slot identity is local and naturally |
|
|
179 | 179 | expressed as `Slot`; |
|
|
180 | 180 | - a dedicated `ArtifactSlotsView` would suggest a detached readonly projection |
|
|
181 | 181 | without clearly owning mutation/configuration semantics; |
|
|
182 | 182 | - `NamedDomainObjectContainer<Slot>` better matches the live configuration model |
|
|
183 | 183 | and keeps the parent aggregate as the owner of slot structure. |
|
|
184 | 184 | |
|
|
185 | 185 | `ArtifactSlot` remains useful, but only as a fully qualified identity outside |
|
|
186 | 186 | the parent aggregate: |
|
|
187 | 187 | |
|
|
188 | 188 | - resolver APIs; |
|
|
189 | 189 | - global payloads; |
|
|
190 | 190 | - cross-variant references. |
|
|
191 | 191 | |
|
|
192 | 192 | Important distinction: |
|
|
193 | 193 | |
|
|
194 | 194 | - `OutgoingConfiguration` describes outgoing publication structure; |
|
|
195 | 195 | - `ArtifactAssembly` describes how one slot artifact is assembled. |
|
|
196 | 196 | |
|
|
197 | 197 | An `ArtifactAssembly` does not create the root outgoing configuration. It serves |
|
|
198 | 198 | one already declared slot inside that configuration. |
|
|
199 | 199 | |
|
|
200 | 200 | ### Primary slot ownership |
|
|
201 | 201 | |
|
|
202 | 202 | Primary status belongs to the parent outgoing configuration, not to the slot |
|
|
203 | 203 | itself. |
|
|
204 | 204 | |
|
|
205 | 205 | This is why the preferred container-level contract is: |
|
|
206 | 206 | |
|
|
207 | 207 | ```java |
|
|
208 | 208 | Property<Slot> getPrimarySlot(); |
|
|
209 | 209 | ``` |
|
|
210 | 210 | |
|
|
211 | 211 | and not a slot-local boolean or a derived `ArtifactSlot` reference. |
|
|
212 | 212 | |
|
|
213 | 213 | Reasons: |
|
|
214 | 214 | |
|
|
215 | 215 | - the primary role is assigned by the parent aggregate; |
|
|
216 | 216 | - within one `OutgoingConfiguration`, the variant identity is already known, so |
|
|
217 | 217 | `Slot` is sufficient and avoids redundant `(variant, slot)` duplication; |
|
|
218 | 218 | - `Property` matches the rest of the Gradle-facing live model better than a |
|
|
219 | 219 | custom `findPrimarySlot()` style API; |
|
|
220 | 220 | - `Property` gives useful write/configure/finalize semantics without inventing a |
|
|
221 | 221 | separate special lifecycle abstraction. |
|
|
222 | 222 | |
|
|
223 | 223 | Expected usage: |
|
|
224 | 224 | |
|
|
225 | 225 | - DSL or adapters may assign the primary slot through `set(...)`; |
|
|
226 | 226 | - adapters that want to provide a default may use `convention(...)`; |
|
|
227 | 227 | - the property may remain unset while the model is still incomplete; |
|
|
228 | 228 | - at materialization time the property may be finalized; |
|
|
229 | 229 | - if more than one slot exists and `primarySlot` is still unset at the |
|
|
230 | 230 | materialization point, that is a model error. |
|
|
231 | 231 | |
|
|
232 | 232 | The model should still enforce that the selected primary slot belongs to the |
|
|
233 | 233 | same `OutgoingConfiguration`. |
|
|
234 | 234 | |
|
|
235 | 235 | ### Proposed public shape |
|
|
236 | 236 | |
|
|
237 | 237 | With these constraints, the preferred public structure is: |
|
|
238 | 238 | |
|
|
239 | 239 | ```java |
|
|
240 | 240 | interface VariantArtifactsContext { |
|
|
241 | 241 | VariantsView getVariants(); |
|
|
242 | 242 | void all(Action<? super OutgoingConfiguration> action); |
|
|
243 | 243 | Optional<OutgoingConfiguration> findArtifacts(Variant variant); |
|
|
244 | 244 | OutgoingConfiguration requireArtifacts(Variant variant); |
|
|
245 | 245 | ArtifactAssemblies getAssemblies(); |
|
|
246 | 246 | } |
|
|
247 | 247 | |
|
|
248 | 248 | interface OutgoingConfiguration { |
|
|
249 | 249 | Variant getVariant(); |
|
|
250 | 250 | NamedDomainObjectProvider<Configuration> getOutgoingConfiguration(); |
|
|
251 | 251 | NamedDomainObjectContainer<Slot> getSlots(); |
|
|
252 | 252 | Property<Slot> getPrimarySlot(); |
|
|
253 | 253 | } |
|
|
254 | 254 | |
|
|
255 | 255 | interface ArtifactAssemblies { |
|
|
256 | 256 | ArtifactAssembly resolveSlot(ArtifactSlot slot); |
|
|
257 | 257 | } |
|
|
258 | 258 | |
|
|
259 | 259 | record ArtifactSlot(Variant variant, Slot slot) {} |
|
|
260 | 260 | ``` |
|
|
261 | 261 | |
|
|
262 | 262 | This gives: |
|
|
263 | 263 | |
|
|
264 | 264 | - one global registry of outgoing variants; |
|
|
265 | 265 | - one local slot container per outgoing variant; |
|
|
266 | 266 | - one fully qualified slot identity for resolver and cross-aggregate use; |
|
|
267 | 267 | - one explicit service for slot assembly materialization. |
|
|
268 | 268 | |
|
|
269 | 269 | ### Minimal internal shape |
|
|
270 | 270 | |
|
|
271 | 271 | The preferred minimal internal structure is: |
|
|
272 | 272 | |
|
|
273 | 273 | ```java |
|
|
274 | 274 | final class VariantArtifactsRegistry implements VariantArtifactsContext { |
|
|
275 | 275 | OutgoingConfiguration outgoingConfiguration(Variant variant); |
|
|
276 | 276 | ArtifactAssemblyRules slotRules(ArtifactSlot slot); |
|
|
277 | 277 | ArtifactAssemblies assemblies(); |
|
|
278 | 278 | } |
|
|
279 | 279 | |
|
|
280 | 280 | interface ArtifactAssemblyRules { |
|
|
281 | 281 | void from(Object input); |
|
|
282 | <T extends Task> void producedBy( | |
|
|
283 | TaskProvider<T> task, | |
|
|
284 | Function<? super T, ? extends Provider<? extends FileSystemLocation>> output); | |
|
|
282 | 285 | void fromVariant(Action<? super OutputSelectionSpec> action); |
|
|
283 | 286 | void fromRole(String roleName, Action<? super OutputSelectionSpec> action); |
|
|
284 | 287 | void fromLayer(String layerName, Action<? super OutputSelectionSpec> action); |
|
|
285 | 288 | } |
|
|
286 | 289 | ``` |
|
|
287 | 290 | |
|
|
288 | 291 | Responsibility split: |
|
|
289 | 292 | |
|
|
290 | 293 | - `VariantArtifactsRegistry` owns the whole artifact model; |
|
|
291 | 294 | - `OutgoingConfiguration` owns only the structural variant-local aggregate; |
|
|
292 | 295 | - `ArtifactAssemblyRules` owns the content declaration of one slot; |
|
|
293 | 296 | - `ArtifactAssemblies` materializes `ArtifactAssembly` from |
|
|
294 | 297 | `ArtifactSlot -> ArtifactAssemblyRules`. |
|
|
295 | 298 | |
|
|
296 | 299 | This keeps `OutgoingConfiguration` focused on structure and avoids overloading it |
|
|
297 | 300 | with slot-content APIs such as `rules(slot)`. |
|
|
298 | 301 | |
|
|
299 | 302 | ### DSL binding |
|
|
300 | 303 | |
|
|
301 | 304 | The DSL should be connected through the registry, not by making |
|
|
302 | 305 | `OutgoingConfiguration` responsible for content rules. |
|
|
303 | 306 | |
|
|
304 | 307 | Conceptually: |
|
|
305 | 308 | |
|
|
306 | 309 | ```java |
|
|
307 | 310 | variantArtifacts.variant("browser", spec -> { |
|
|
308 | 311 | spec.slot("runtime", assembly -> { |
|
|
309 | 312 | assembly.fromRole("production", out -> out.output("js")); |
|
|
310 | 313 | }); |
|
|
311 | 314 | }); |
|
|
312 | 315 | ``` |
|
|
313 | 316 | |
|
|
314 | 317 | Operationally this means: |
|
|
315 | 318 | |
|
|
316 | 319 | 1. registry creates or returns `OutgoingConfiguration` for the variant; |
|
|
317 | 320 | 2. slot declaration creates or returns `Slot` inside that outgoing configuration; |
|
|
318 | 321 | 3. registry forms `ArtifactSlot(variant, slot)`; |
|
|
319 | 322 | 4. registry resolves `slotRules(artifactSlot)`; |
|
|
320 | 323 | 5. bound `ArtifactAssemblySpec` writes into those rules. |
|
|
321 | 324 | |
|
|
322 | 325 | So the DSL writes: |
|
|
323 | 326 | |
|
|
324 | 327 | - structure into `OutgoingConfiguration`; |
|
|
325 | 328 | - slot content into registry-owned `ArtifactAssemblyRules`. |
|
|
326 | 329 | |
|
|
327 | 330 | This is the intended bridge point between the public DSL and the internal |
|
|
328 | 331 | resolver/materialization model. |
|
|
329 | 332 | |
|
|
330 | 333 | --- |
|
|
331 | 334 | |
|
|
332 | 335 | ## Live model and monotonic structure |
|
|
333 | 336 | |
|
|
334 | 337 | `variantArtifacts` should be treated as a live configuration model during the |
|
|
335 | 338 | whole configuration phase. |
|
|
336 | 339 | |
|
|
337 | 340 | This means: |
|
|
338 | 341 | |
|
|
339 | 342 | - slot inputs remain live; |
|
|
340 | 343 | - `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep |
|
|
341 | 344 | contributing inputs until task execution; |
|
|
345 | - `producedBy(...)` publishes an existing task output directly and does not | |
|
|
346 | create the managed copy assembly for that slot; | |
|
|
342 | 347 | - `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task |
|
|
343 | 348 | wiring; |
|
|
344 | 349 | - external task outputs remain outside the control of this model and must be |
|
|
345 | 350 | accepted as live inputs. |
|
|
346 | 351 | |
|
|
347 | 352 | The model should therefore avoid a mandatory freeze phase for slot content. |
|
|
348 | 353 | |
|
|
349 | 354 | Instead, it should follow a monotonic rule: |
|
|
350 | 355 | |
|
|
351 | 356 | - outgoing variant existence may grow; |
|
|
352 | 357 | - slot existence may grow; |
|
|
353 | 358 | - slot content may grow; |
|
|
354 | 359 | - publication-visible identity should not be retroactively redefined. |
|
|
355 | 360 | |
|
|
356 | 361 | In practice this means: |
|
|
357 | 362 | |
|
|
358 | 363 | - slot names are stable once declared; |
|
|
359 | 364 | - primary slot designation is structural; |
|
|
360 | 365 | - slot input content remains live. |
|
|
361 | 366 | |
|
|
362 | 367 | This also means that the model does not need a dedicated freeze phase for slot |
|
|
363 | 368 | content merely because the root outgoing configuration was registered earlier. |
|
|
364 | 369 | |
|
|
365 | 370 | Early registration of the root `Configuration` and live evolution of slot input |
|
|
366 | 371 | content are compatible concerns. |
|
|
367 | 372 | |
|
|
368 | 373 | --- |
|
|
369 | 374 | |
|
|
370 | 375 | ## DSL principles |
|
|
371 | 376 | |
|
|
372 | 377 | The DSL should remain declarative and symbolic. |
|
|
373 | 378 | |
|
|
374 | 379 | It should describe: |
|
|
375 | 380 | |
|
|
376 | 381 | - which variant is outgoing; |
|
|
377 | 382 | - which slots exist; |
|
|
378 | 383 | - which slot is primary; |
|
|
379 | 384 | - which selectors contribute inputs to each slot. |
|
|
380 | 385 | |
|
|
381 | 386 | It should not directly expose: |
|
|
382 | 387 | |
|
|
383 | 388 | - `GenericSourceSet`; |
|
|
384 | 389 | - `FileCollection`; |
|
|
385 | 390 | - concrete resolved files from `variantSources`; |
|
|
386 | 391 | - internal resolver state. |
|
|
387 | 392 | |
|
|
388 | 393 | ### DSL shape |
|
|
389 | 394 | |
|
|
390 | 395 | Conceptually: |
|
|
391 | 396 | |
|
|
392 | 397 | ```groovy |
|
|
393 | 398 | variantArtifacts { |
|
|
394 | 399 | variant("browser") { |
|
|
395 | 400 | primarySlot("runtime") { |
|
|
396 | 401 | fromRole("production") { |
|
|
397 | 402 | output("js") |
|
|
398 | 403 | output("resources") |
|
|
399 | 404 | } |
|
|
400 | 405 | } |
|
|
401 | 406 | |
|
|
402 | 407 | slot("types") { |
|
|
403 | 408 | fromVariant { |
|
|
404 | 409 | output("dts") |
|
|
405 | 410 | } |
|
|
406 | 411 | } |
|
|
407 | 412 | |
|
|
408 | 413 | slot("sources") { |
|
|
409 | 414 | fromLayer("main") { |
|
|
410 | 415 | output("sources") |
|
|
411 | 416 | } |
|
|
412 | 417 | } |
|
|
413 | 418 | |
|
|
414 | 419 | slot("bundleMetadata") { |
|
|
415 | from(someTask) | |
|
|
416 | from(layout.buildDirectory.file("generated/meta.json")) | |
|
|
420 | producedBy(writePackageMetadata) { | |
|
|
421 | outputFile | |
|
|
422 | } | |
|
|
417 | 423 | } |
|
|
418 | 424 | } |
|
|
419 | 425 | } |
|
|
420 | 426 | ``` |
|
|
421 | 427 | |
|
|
422 | 428 | ### Meaning of contribution forms |
|
|
423 | 429 | |
|
|
424 | 430 | - `from(Object)` adds a direct input independent from `variantSources`; |
|
|
425 | 431 | - `fromVariant { output(...) }` selects named outputs from all compile units of |
|
|
426 | 432 | the current variant; |
|
|
427 | 433 | - `fromRole(role) { output(...) }` selects named outputs from compile units that |
|
|
428 | 434 | belong to the given role projection; |
|
|
429 | 435 | - `fromLayer(layer) { output(...) }` selects named outputs from the compile unit |
|
|
430 | 436 | of the current variant and the given layer, if such unit exists. |
|
|
437 | - `producedBy(task) { outputFile }` maps an existing producing task to the single | |
|
|
438 | file or directory published for the slot. | |
|
|
431 | 439 | |
|
|
440 | Contribution forms and `producedBy(...)` are mutually exclusive for one slot. | |
|
|
432 | 441 | The DSL stores declarations, not resolved file collections. |
|
|
433 | 442 | |
|
|
434 | 443 | --- |
|
|
435 | 444 | |
|
|
436 | 445 | ## Contribution model |
|
|
437 | 446 | |
|
|
438 | 447 | Internally the DSL should compile to slot contributions. |
|
|
439 | 448 | |
|
|
440 | 449 | Conceptually: |
|
|
441 | 450 | |
|
|
442 | 451 | - `DirectContribution` |
|
|
443 | 452 | - `VariantOutputContribution` |
|
|
444 | 453 | - `RoleOutputContribution` |
|
|
445 | 454 | - `LayerOutputContribution` |
|
|
446 | 455 | |
|
|
447 | 456 | These contributions should remain symbolic for as long as possible. |
|
|
448 | 457 | |
|
|
449 | 458 | They should not resolve source sets or files at declaration time. |
|
|
450 | 459 | |
|
|
451 | 460 | Each contribution is expected to provide: |
|
|
452 | 461 | |
|
|
453 | 462 | - its selection scope; |
|
|
454 | 463 | - the requested output names; |
|
|
455 | 464 | - enough symbolic identity for later validation and resolver policies. |
|
|
456 | 465 | |
|
|
457 | 466 | --- |
|
|
458 | 467 | |
|
|
459 | 468 | ## Resolver bridge between `variantSources` and `variantArtifacts` |
|
|
460 | 469 | |
|
|
461 | 470 | This is the central integration point. |
|
|
462 | 471 | |
|
|
463 | 472 | `variantArtifacts` should not access mutable internals of `variantSources`. |
|
|
464 | 473 | |
|
|
465 | 474 | Instead, it should resolve slot inputs through the public finalized |
|
|
466 | 475 | `VariantSourcesContext`. |
|
|
467 | 476 | |
|
|
468 | 477 | ### Bridge responsibilities |
|
|
469 | 478 | |
|
|
470 | 479 | The bridge: |
|
|
471 | 480 | |
|
|
472 | 481 | - takes slot contribution declarations; |
|
|
473 | 482 | - expands them against finalized variant topology; |
|
|
474 | 483 | - maps logical selectors to compile units and role projections; |
|
|
475 | 484 | - obtains source sets lazily through `VariantSourcesContext`; |
|
|
476 | 485 | - resolves named outputs from those source sets; |
|
|
477 | 486 | - builds the live input model for an `ArtifactAssembly`. |
|
|
478 | 487 | |
|
|
479 | 488 | ### Bridge input |
|
|
480 | 489 | |
|
|
481 | 490 | - current outgoing variant identity; |
|
|
482 | 491 | - slot contribution declarations; |
|
|
483 | 492 | - `VariantSourcesContext`. |
|
|
484 | 493 | |
|
|
485 | 494 | ### Bridge output |
|
|
486 | 495 | |
|
|
487 | 496 | - a live collection of logical slot inputs; |
|
|
488 | 497 | - later adapted to `FileCollection` or other assembly-facing input models. |
|
|
489 | 498 | |
|
|
490 | 499 | ### Resolution semantics |
|
|
491 | 500 | |
|
|
492 | 501 | For one outgoing variant: |
|
|
493 | 502 | |
|
|
494 | 503 | - `fromVariant { output(x) }` |
|
|
495 | 504 | - expands to all `CompileUnit` of that variant; |
|
|
496 | 505 | - `fromRole(role) { output(x) }` |
|
|
497 | 506 | - expands to `RoleProjection(variant, role)` and then to its compile units; |
|
|
498 | 507 | - `fromLayer(layer) { output(x) }` |
|
|
499 | 508 | - expands to one compile unit `(variant, layer)` when it exists; |
|
|
500 | 509 | - `from(Object)` |
|
|
501 | 510 | - bypasses `variantSources` completely. |
|
|
511 | - `producedBy(task)` | |
|
|
512 | - bypasses contribution resolution and registers the task output as the slot | |
|
|
513 | artifact directly. | |
|
|
502 | 514 | |
|
|
503 | 515 | After compile units are known, the bridge asks |
|
|
504 | 516 | `ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the |
|
|
505 | 517 | requested named output. |
|
|
506 | 518 | |
|
|
507 | 519 | This keeps `variantArtifacts` independent from source-set naming internals and |
|
|
508 | 520 | other materialization details. |
|
|
509 | 521 | |
|
|
510 | 522 | --- |
|
|
511 | 523 | |
|
|
512 | 524 | ## Validation |
|
|
513 | 525 | |
|
|
514 | 526 | Validation should be structural and symbolic. |
|
|
515 | 527 | |
|
|
516 | 528 | It should validate: |
|
|
517 | 529 | |
|
|
518 | 530 | - outgoing variant refers to an existing `Variant`; |
|
|
519 | 531 | - referenced `Role` exists in that variant projection space; |
|
|
520 | 532 | - referenced `Layer` exists in that variant compile-unit space; |
|
|
521 | 533 | - primary slot is defined when needed; |
|
|
522 | 534 | - primary slot refers to a slot declared in the same outgoing configuration. |
|
|
523 | 535 | |
|
|
524 | 536 | Validation should not require eager materialization of source sets or eager |
|
|
525 | 537 | resolution of files. |
|
|
526 | 538 | |
|
|
527 | 539 | --- |
|
|
528 | 540 | |
|
|
529 | 541 | ## Deduplication and policy extension points |
|
|
530 | 542 | |
|
|
531 | 543 | Deduplication is important, but it should not be baked into the DSL itself. |
|
|
532 | 544 | |
|
|
533 | 545 | The correct place for it is the resolver bridge, after symbolic contributions |
|
|
534 | 546 | have been expanded to logical inputs but before they are finally adapted to |
|
|
535 | 547 | assembly-facing file collections. |
|
|
536 | 548 | |
|
|
537 | 549 | ### Why not in the DSL |
|
|
538 | 550 | |
|
|
539 | 551 | At declaration time it is still unknown whether selectors overlap: |
|
|
540 | 552 | |
|
|
541 | 553 | - `fromVariant` |
|
|
542 | 554 | - `fromRole` |
|
|
543 | 555 | - `fromLayer` |
|
|
544 | 556 | |
|
|
545 | 557 | may all describe the same logical source output. |
|
|
546 | 558 | |
|
|
547 | 559 | ### Why not rely only on `FileCollection` |
|
|
548 | 560 | |
|
|
549 | 561 | `FileCollection` may still provide useful physical deduplication, but it is too |
|
|
550 | 562 | late and too file-oriented to serve as the only semantic mechanism. |
|
|
551 | 563 | |
|
|
552 | 564 | The artifact model should first deduplicate logical inputs, then let Gradle |
|
|
553 | 565 | perform any additional physical deduplication. |
|
|
554 | 566 | |
|
|
555 | 567 | ### Default expectation |
|
|
556 | 568 | |
|
|
557 | 569 | The default resolver should support: |
|
|
558 | 570 | |
|
|
559 | 571 | - deduplication of topology-aware inputs by logical identity; |
|
|
560 | 572 | - no implicit deduplication of direct `from(Object)` inputs. |
|
|
561 | 573 | |
|
|
562 | 574 | Logical identity should be based on domain meaning, for example: |
|
|
563 | 575 | |
|
|
564 | 576 | - `(CompileUnit, outputName)` |
|
|
565 | 577 | |
|
|
566 | 578 | and not on projected source-set names. |
|
|
567 | 579 | |
|
|
568 | 580 | This is important because source-set naming policy belongs to `variantSources` |
|
|
569 | 581 | and must not silently redefine artifact semantics. |
|
|
570 | 582 | |
|
|
571 | 583 | ### Extension points |
|
|
572 | 584 | |
|
|
573 | 585 | The model should provide explicit internal extension points for: |
|
|
574 | 586 | |
|
|
575 | 587 | - deduplication policy; |
|
|
576 | 588 | - logical input identity; |
|
|
577 | 589 | - adaptation of resolved logical inputs to assembly-facing objects. |
|
|
578 | 590 | |
|
|
579 | 591 | Conceptually: |
|
|
580 | 592 | |
|
|
581 | 593 | ```java |
|
|
582 | 594 | interface SlotInputDedupPolicy { ... } |
|
|
583 | 595 | interface LogicalSlotInputIdentity { ... } |
|
|
584 | 596 | interface SlotInputAdapter { ... } |
|
|
585 | 597 | ``` |
|
|
586 | 598 | |
|
|
587 | 599 | The default implementation may remain simple, but these seams should exist from |
|
|
588 | 600 | the start. |
|
|
589 | 601 | |
|
|
590 | 602 | --- |
|
|
591 | 603 | |
|
|
592 | 604 | ## Publication hooks |
|
|
593 | 605 | |
|
|
594 | 606 | Publication hooks remain useful, but they should observe the live structural |
|
|
595 | 607 | model rather than define it. |
|
|
596 | 608 | |
|
|
597 | 609 | Examples: |
|
|
598 | 610 | |
|
|
599 | 611 | - `whenOutgoingConfiguration(...)` |
|
|
600 | 612 | - `whenOutgoingSlot(...)` |
|
|
601 | 613 | |
|
|
602 | 614 | These hooks are adapter-facing customization points over already declared |
|
|
603 | 615 | outgoing structure: |
|
|
604 | 616 | |
|
|
605 | 617 | - root configuration attributes; |
|
|
606 | 618 | - slot artifact attributes; |
|
|
607 | 619 | - assembly task tweaks. |
|
|
608 | 620 | |
|
|
609 | 621 | The recommended way to connect publication-facing `Spec` objects to the |
|
|
610 | 622 | structural model is by backlink, not by moving slot rules into publication |
|
|
611 | 623 | types. |
|
|
612 | 624 | |
|
|
613 | 625 | Conceptually: |
|
|
614 | 626 | |
|
|
615 | 627 | ```java |
|
|
616 | 628 | interface OutgoingConfigurationSpec { |
|
|
617 | 629 | OutgoingConfiguration getOutgoingArtifacts(); |
|
|
618 | 630 | Variant getVariant(); |
|
|
619 | 631 | Configuration getConfiguration(); |
|
|
620 | 632 | } |
|
|
621 | 633 | |
|
|
622 | 634 | interface OutgoingArtifactSlotSpec { |
|
|
623 | 635 | ArtifactSlot getArtifactSlot(); |
|
|
624 | 636 | ArtifactAssembly getAssembly(); |
|
|
625 | 637 | boolean isPrimary(); |
|
|
626 | 638 | } |
|
|
627 | 639 | ``` |
|
|
628 | 640 | |
|
|
629 | 641 | In this arrangement: |
|
|
630 | 642 | |
|
|
631 | 643 | - `OutgoingConfigurationSpec` remains a publication-facing facade; |
|
|
632 | 644 | - `OutgoingConfigurationSpec` may expose the structural aggregate when an |
|
|
633 | 645 | adapter needs it; |
|
|
634 | 646 | - slot rules still belong to `VariantArtifactsRegistry`; |
|
|
635 | 647 | - publication specs do not become owners of declaration or resolver state. |
|
|
636 | 648 | |
|
|
637 | 649 | They should not become the primary structural API for the artifact model. |
|
|
638 | 650 | |
|
|
639 | 651 | This is why a separate phase-oriented `OutgoingPublicationsContext` is not |
|
|
640 | 652 | required. |
|
|
641 | 653 | |
|
|
642 | 654 | The live `OutgoingConfiguration` aggregate is sufficient. |
|
|
643 | 655 | |
|
|
644 | 656 | --- |
|
|
645 | 657 | |
|
|
646 | 658 | ## Design principles |
|
|
647 | 659 | |
|
|
648 | 660 | ### 1. Keep topology ownership in `variants` |
|
|
649 | 661 | |
|
|
650 | 662 | `variantArtifacts` selects from the topology model. It does not own it. |
|
|
651 | 663 | |
|
|
652 | 664 | ### 2. Keep source ownership in `variantSources` |
|
|
653 | 665 | |
|
|
654 | 666 | `variantArtifacts` consumes source materialization through a resolver bridge. It |
|
|
655 | 667 | does not own source-set semantics. |
|
|
656 | 668 | |
|
|
657 | 669 | ### 3. Keep the DSL symbolic |
|
|
658 | 670 | |
|
|
659 | 671 | The DSL declares intent and selection rules, not materialized files. |
|
|
660 | 672 | |
|
|
661 | 673 | ### 4. Keep slot content live |
|
|
662 | 674 | |
|
|
663 | 675 | Do not introduce an artificial finalize phase for slot content unless a real |
|
|
664 | 676 | semantic need appears. |
|
|
665 | 677 | |
|
|
666 | 678 | ### 5. Fix only structural identity |
|
|
667 | 679 | |
|
|
668 | 680 | Slot name, primary designation, and outgoing shape are structural. Slot inputs |
|
|
669 | 681 | remain live. |
|
|
670 | 682 | |
|
|
671 | 683 | ### 6. Resolve through dedicated bridges |
|
|
672 | 684 | |
|
|
673 | 685 | Cross-model integration belongs in a resolver service, not in DSL classes. |
|
|
674 | 686 | |
|
|
675 | 687 | ### 7. Add policy seams early |
|
|
676 | 688 | |
|
|
677 | 689 | Deduplication and similar concerns should have extension points from the start, |
|
|
678 | 690 | even if the initial implementation is conservative. |
|
|
679 | 691 | |
|
|
680 | 692 | --- |
|
|
681 | 693 | |
|
|
682 | 694 | ## Summary |
|
|
683 | 695 | |
|
|
684 | 696 | `variantArtifacts` should be modeled as a live outgoing-contract layer over |
|
|
685 | 697 | `variants`, with source input resolution delegated to a dedicated bridge over |
|
|
686 | 698 | `variantSources`. |
|
|
687 | 699 | |
|
|
688 | 700 | The resulting shape is: |
|
|
689 | 701 | |
|
|
690 | 702 | - `variants` owns topology; |
|
|
691 | 703 | - `variantSources` owns source materialization; |
|
|
692 | 704 | - `variantArtifacts` owns outgoing contract structure; |
|
|
693 | 705 | - a resolver bridge connects symbolic slot declarations to live source-derived |
|
|
694 | 706 | inputs; |
|
|
695 | 707 | - deduplication and similar concerns are policies of that bridge, not of the |
|
|
696 | 708 | DSL itself; |
|
|
697 | 709 | - slot content stays live during configuration; |
|
|
698 | 710 | - only publication-visible structure is treated as stable identity. |
| @@ -1,60 +1,106 | |||
|
|
1 | 1 | package org.implab.gradle.variants.artifacts; |
|
|
2 | 2 | |
|
|
3 | import java.util.function.Function; | |
|
|
4 | ||
|
|
3 | 5 | import org.gradle.api.Action; |
|
|
6 | import org.gradle.api.InvalidUserDataException; | |
|
|
7 | import org.gradle.api.Task; | |
|
|
8 | import org.gradle.api.file.FileSystemLocation; | |
|
|
9 | import org.gradle.api.provider.Provider; | |
|
|
10 | import org.gradle.api.tasks.TaskProvider; | |
|
|
4 | 11 | import groovy.lang.Closure; |
|
|
5 | 12 | import org.implab.gradle.common.core.lang.Closures; |
|
|
6 | 13 | |
|
|
7 | 14 | /** |
|
|
8 | 15 | * DSL model describing how a slot artifact is assembled. |
|
|
9 | 16 | * |
|
|
10 | 17 | * <p>Selection rules declared here may refer to internal build topology such as roles, layers or units. |
|
|
11 | 18 | * Those selectors influence slot assembly only and do not become part of published artifact identity. |
|
|
12 | 19 | * |
|
|
13 | 20 | * <p>Regardless of the number of declared inputs, a slot is expected to materialize to a single published |
|
|
14 | 21 | * artifact. |
|
|
15 | 22 | */ |
|
|
16 | 23 | public interface ArtifactAssemblySpec { |
|
|
17 | 24 | /** |
|
|
18 | 25 | * Contributes direct input material to the slot assembly. |
|
|
19 | 26 | * |
|
|
20 | 27 | * <p>The resulting slot still represents one published artifact. |
|
|
21 | 28 | * |
|
|
22 | 29 | * @param artifact direct input notation understood by the implementation |
|
|
23 | 30 | */ |
|
|
24 | 31 | void from(Object artifact); |
|
|
25 | 32 | |
|
|
26 | 33 | /** |
|
|
34 | * Registers a task that directly produces the published slot artifact. | |
|
|
35 | * | |
|
|
36 | * <p>Use this method when the slot is produced as one file or directory by an | |
|
|
37 | * existing task, for example generated package metadata. Unlike {@link #from(Object)} | |
|
|
38 | * and topology-aware selectors, this does not copy inputs into a managed assembly | |
|
|
39 | * directory. The mapped task output becomes the published artifact itself. | |
|
|
40 | * | |
|
|
41 | * <p>This mode is mutually exclusive with contribution-based assembly methods | |
|
|
42 | * such as {@link #from(Object)}, {@link #fromVariant(Action)}, {@link #fromRole(String, Action)}, | |
|
|
43 | * and {@link #fromLayer(String, Action)} for the same slot. | |
|
|
44 | * | |
|
|
45 | * @param <T> task type | |
|
|
46 | * @param task task provider producing the artifact | |
|
|
47 | * @param artifact maps the producing task to its output file or directory provider | |
|
|
48 | */ | |
|
|
49 | <T extends Task> void producedBy( | |
|
|
50 | TaskProvider<T> task, | |
|
|
51 | Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact); | |
|
|
52 | ||
|
|
53 | default <T extends Task> void producedBy(TaskProvider<T> task, Closure<?> closure) { | |
|
|
54 | producedBy(task, taskInstance -> producedArtifact(closure, taskInstance)); | |
|
|
55 | } | |
|
|
56 | ||
|
|
57 | @SuppressWarnings("unchecked") | |
|
|
58 | private static Provider<? extends FileSystemLocation> producedArtifact(Closure<?> closure, Task task) { | |
|
|
59 | var c = (Closure<?>) closure.clone(); | |
|
|
60 | c.setResolveStrategy(Closure.DELEGATE_FIRST); | |
|
|
61 | c.setDelegate(task); | |
|
|
62 | ||
|
|
63 | var artifact = c.call(task); | |
|
|
64 | if (artifact instanceof Provider<?>) { | |
|
|
65 | return (Provider<? extends FileSystemLocation>) artifact; | |
|
|
66 | } | |
|
|
67 | ||
|
|
68 | throw new InvalidUserDataException("Produced artifact mapper for task '" + task.getName() | |
|
|
69 | + "' must return Provider<? extends FileSystemLocation>"); | |
|
|
70 | } | |
|
|
71 | ||
|
|
72 | /** | |
|
|
27 | 73 | * Selects outputs from the whole variant scope. |
|
|
28 | 74 | * |
|
|
29 | 75 | * @param action output selection rule |
|
|
30 | 76 | */ |
|
|
31 | 77 | void fromVariant(Action<? super OutputSelectionSpec> action); |
|
|
32 | 78 | |
|
|
33 | 79 | default void fromVariant(Closure<?> closure) { |
|
|
34 | 80 | fromVariant(Closures.action(closure)); |
|
|
35 | 81 | } |
|
|
36 | 82 | |
|
|
37 | 83 | /** |
|
|
38 | 84 | * Selects outputs from a role inside the current variant. |
|
|
39 | 85 | * |
|
|
40 | 86 | * @param roleName role name used only for assembly-time selection |
|
|
41 | 87 | * @param action output selection rule |
|
|
42 | 88 | */ |
|
|
43 | 89 | void fromRole(String roleName, Action<? super OutputSelectionSpec> action); |
|
|
44 | 90 | |
|
|
45 | 91 | default void fromRole(String roleName, Closure<?> closure) { |
|
|
46 | 92 | fromRole(roleName, Closures.action(closure)); |
|
|
47 | 93 | } |
|
|
48 | 94 | |
|
|
49 | 95 | /** |
|
|
50 | 96 | * Selects outputs from a layer inside the current variant. |
|
|
51 | 97 | * |
|
|
52 | 98 | * @param layerName layer name used only for assembly-time selection |
|
|
53 | 99 | * @param action output selection rule |
|
|
54 | 100 | */ |
|
|
55 | 101 | void fromLayer(String layerName, Action<? super OutputSelectionSpec> action); |
|
|
56 | 102 | |
|
|
57 | 103 | default void fromLayer(String layerName, Closure<?> closure) { |
|
|
58 | 104 | fromLayer(layerName, Closures.action(closure)); |
|
|
59 | 105 | } |
|
|
60 | 106 | } |
| @@ -1,56 +1,60 | |||
|
|
1 | 1 | package org.implab.gradle.variants.artifacts.internal; |
|
|
2 | 2 | |
|
|
3 | 3 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
4 | 4 | import org.gradle.api.Action; |
|
|
5 | 5 | import org.implab.gradle.variants.artifacts.ArtifactAssemblies; |
|
|
6 | 6 | import org.implab.gradle.variants.artifacts.ArtifactSlot; |
|
|
7 | 7 | import org.implab.gradle.variants.artifacts.OutgoingVariant; |
|
|
8 | 8 | |
|
|
9 | 9 | /** |
|
|
10 | 10 | * Binds materialized slot assemblies to Gradle outgoing publications. |
|
|
11 | 11 | */ |
|
|
12 | 12 | @NonNullByDefault |
|
|
13 | 13 | public class ArtifactAssemblyBinder implements Action<OutgoingVariant> { |
|
|
14 | 14 | |
|
|
15 | 15 | private final ArtifactAssemblies resolver; |
|
|
16 | 16 | |
|
|
17 | 17 | public ArtifactAssemblyBinder(ArtifactAssemblies resolver) { |
|
|
18 | 18 | this.resolver = resolver; |
|
|
19 | 19 | } |
|
|
20 | 20 | |
|
|
21 | 21 | @Override |
|
|
22 | 22 | public void execute(OutgoingVariant outgoingVariant) { |
|
|
23 | 23 | var slots = outgoingVariant.getSlots(); |
|
|
24 | 24 | var primarySlotProvider = outgoingVariant.getPrimarySlot(); |
|
|
25 | 25 | var variant = outgoingVariant.getVariant(); |
|
|
26 | 26 | |
|
|
27 | 27 | // Bind publication state when the owning configuration is materialized. |
|
|
28 | 28 | outgoingVariant.configureOutgoing(configuration -> { |
|
|
29 | 29 | var primarySlot = primarySlotProvider.get(); |
|
|
30 | 30 | var outgoing = configuration.getOutgoing(); |
|
|
31 | 31 | |
|
|
32 | 32 | // Bind the primary artifact set to the root outgoing configuration. |
|
|
33 | 33 | resolver.when( |
|
|
34 | 34 | new ArtifactSlot(variant, primarySlot), |
|
|
35 |
assembly -> outgoing.artifact( |
|
|
|
35 | assembly -> outgoing.artifact( | |
|
|
36 | assembly.getArtifact(), | |
|
|
37 | artifact -> artifact.builtBy(assembly.getAssemblyTask()))); | |
|
|
36 | 38 | |
|
|
37 | 39 | // Bind non-primary slots to Gradle secondary artifact variants. |
|
|
38 | 40 | slots.all(slot -> { |
|
|
39 | 41 | if (slot.equals(primarySlot)) |
|
|
40 | 42 | return; |
|
|
41 | 43 | |
|
|
42 | 44 | resolver.when( |
|
|
43 | 45 | new ArtifactSlot(variant, slot), |
|
|
44 | 46 | // Gradle artifact variants must be created while the owning |
|
|
45 | 47 | // configuration is being materialized. Lazy registration may |
|
|
46 | 48 | // otherwise be realized only after dependency resolution starts. |
|
|
47 | 49 | assembly -> outgoing.getVariants() |
|
|
48 | 50 | .create(slot.getName()) |
|
|
49 |
.artifact( |
|
|
|
51 | .artifact( | |
|
|
52 | assembly.getArtifact(), | |
|
|
53 | artifact -> artifact.builtBy(assembly.getAssemblyTask()))); | |
|
|
50 | 54 | }); |
|
|
51 | 55 | }); |
|
|
52 | 56 | } |
|
|
53 | 57 | |
|
|
54 | 58 | |
|
|
55 | 59 | |
|
|
56 | 60 | } |
| @@ -1,202 +1,321 | |||
|
|
1 | 1 | package org.implab.gradle.variants.artifacts.internal; |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.HashMap; |
|
|
4 | 4 | import java.util.HashSet; |
|
|
5 | 5 | import java.util.Map; |
|
|
6 | 6 | import java.util.Set; |
|
|
7 | import java.util.function.Function; | |
|
|
8 | import java.util.stream.Stream; | |
|
|
7 | 9 | |
|
|
8 | 10 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
9 | 11 | import org.gradle.api.Action; |
|
|
12 | import org.gradle.api.InvalidUserDataException; | |
|
|
13 | import org.gradle.api.Task; | |
|
|
10 | 14 | import org.gradle.api.file.ConfigurableFileCollection; |
|
|
11 | 15 | import org.gradle.api.file.Directory; |
|
|
12 | 16 | import org.gradle.api.file.DirectoryProperty; |
|
|
13 | 17 | import org.gradle.api.file.FileCollection; |
|
|
18 | import org.gradle.api.file.FileSystemLocation; | |
|
|
14 | 19 | import org.gradle.api.model.ObjectFactory; |
|
|
15 | 20 | import org.gradle.api.provider.Provider; |
|
|
16 | 21 | import org.gradle.api.tasks.Sync; |
|
|
17 | 22 | import org.gradle.api.tasks.TaskContainer; |
|
|
23 | import org.gradle.api.tasks.TaskProvider; | |
|
|
18 | 24 | import org.gradle.language.base.plugins.LifecycleBasePlugin; |
|
|
19 | 25 | import org.implab.gradle.common.core.lang.FilePaths; |
|
|
26 | import org.implab.gradle.common.core.lang.Strings; | |
|
|
20 | 27 | import org.implab.gradle.variants.artifacts.ArtifactAssembly; |
|
|
21 | 28 | import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec; |
|
|
22 | 29 | import org.implab.gradle.variants.artifacts.ArtifactSlot; |
|
|
30 | import org.implab.gradle.variants.artifacts.OutputSelectionSpec; | |
|
|
31 | import org.implab.gradle.variants.core.Layer; | |
|
|
32 | import org.implab.gradle.variants.core.Role; | |
|
|
23 | 33 | import org.implab.gradle.variants.sources.CompileUnit; |
|
|
24 | 34 | import org.implab.gradle.variants.sources.CompileUnitsView; |
|
|
25 | 35 | import org.implab.gradle.variants.sources.RoleProjectionsView; |
|
|
26 | 36 | import org.implab.gradle.variants.sources.SourceSetMaterializer; |
|
|
27 | 37 | |
|
|
28 | 38 | /** |
|
|
29 |
* Adapts slot contribution declarations to materialized |
|
|
|
39 | * Adapts slot contribution declarations to materialized | |
|
|
40 | * {@link ArtifactAssembly} | |
|
|
30 | 41 | * handles. |
|
|
31 | 42 | * |
|
|
32 | * <p>The handler creates one {@link Sync} task per {@link ArtifactSlot}. The task | |
|
|
33 | * copies all collected slot inputs into a single output directory. That output | |
|
|
34 | * directory is then registered in {@link ArtifactAssemblyRegistry} as the | |
|
|
35 | * published artifact for the slot. | |
|
|
43 | * <p> | |
|
|
44 | * Contribution-based assemblies create one {@link Sync} task per | |
|
|
45 | * {@link ArtifactSlot}. The task copies all collected slot inputs into a single | |
|
|
46 | * output directory. That output directory is then registered in | |
|
|
47 | * {@link ArtifactAssemblyRegistry} as the published artifact for the slot. | |
|
|
36 | 48 | * |
|
|
37 | * <p>Input collection uses {@link SlotContributionVisitor}. Each contribution is | |
|
|
38 | * converted to a {@link SlotInputKey}; duplicate keys are ignored so that repeated | |
|
|
49 | * <p> | |
|
|
50 | * Task-produced assemblies bypass the managed copy task. The producer task is | |
|
|
51 | * registered directly in {@link ArtifactAssemblyRegistry}, and its mapped output | |
|
|
52 | * file or directory becomes the published slot artifact. | |
|
|
53 | * | |
|
|
54 | * <p> | |
|
|
55 | * Input collection uses {@link SlotContributionVisitor}. Each contribution is | |
|
|
56 | * converted to a {@link SlotInputKey}; duplicate keys are ignored so that | |
|
|
57 | * repeated | |
|
|
39 | 58 | * topology-based selections do not add the same input twice. |
|
|
40 | 59 | */ |
|
|
41 | 60 | @NonNullByDefault |
|
|
42 | 61 | public class ArtifactAssemblyHandler { |
|
|
43 | 62 | private final ObjectFactory objects; |
|
|
44 | 63 | |
|
|
45 | 64 | private final ArtifactAssemblyRegistry assemblyRegistry; |
|
|
46 | 65 | |
|
|
47 | 66 | private final DirectoryProperty assembliesDirectory; |
|
|
48 | 67 | |
|
|
49 | 68 | private final TaskContainer tasks; |
|
|
50 | 69 | |
|
|
51 | 70 | private final CompileUnitsView compileUnitsView; |
|
|
52 | 71 | |
|
|
53 | 72 | private final RoleProjectionsView roleProjectionsView; |
|
|
54 | 73 | |
|
|
55 | 74 | private final SourceSetMaterializer sourceSetMaterializer; |
|
|
56 | 75 | |
|
|
57 | 76 | private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>(); |
|
|
58 | 77 | |
|
|
78 | private final Map<ArtifactSlot, AssemblyMode> assemblyModes = new HashMap<>(); | |
|
|
79 | ||
|
|
59 | 80 | public ArtifactAssemblyHandler( |
|
|
60 | 81 | ObjectFactory objects, |
|
|
61 | 82 | TaskContainer tasks, |
|
|
62 | 83 | ArtifactAssemblyRegistry assemblyRegistry, |
|
|
63 | 84 | CompileUnitsView compileUnitsView, |
|
|
64 | 85 | RoleProjectionsView roleProjectionsView, |
|
|
65 | 86 | SourceSetMaterializer sourceSetMaterializer) { |
|
|
66 | 87 | this.objects = objects; |
|
|
67 | 88 | this.tasks = tasks; |
|
|
68 | 89 | this.assemblyRegistry = assemblyRegistry; |
|
|
69 | 90 | this.compileUnitsView = compileUnitsView; |
|
|
70 | 91 | this.roleProjectionsView = roleProjectionsView; |
|
|
71 | 92 | this.sourceSetMaterializer = sourceSetMaterializer; |
|
|
72 | 93 | |
|
|
73 | 94 | assembliesDirectory = objects.directoryProperty(); |
|
|
74 | 95 | } |
|
|
75 | 96 | |
|
|
76 | 97 | public DirectoryProperty getAssembliesDirectory() { |
|
|
77 | 98 | return assembliesDirectory; |
|
|
78 | 99 | } |
|
|
79 | 100 | |
|
|
80 | 101 | public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) { |
|
|
81 |
var |
|
|
|
82 | var spec = new DefaultArtifactAssemblySpec(objects, c -> c.accept(visitor)); | |
|
|
102 | var spec = new DefaultArtifactAssemblySpec(artifactSlot); | |
|
|
83 | 103 | action.execute(spec); |
|
|
84 | 104 | } |
|
|
85 | 105 | |
|
|
86 | public SlotContributionVisitor contributionVisitor(ArtifactSlot artifactSlot) { | |
|
|
87 |
var |
|
|
|
88 | return new ContributionVisitor(artifactSlot, assembly); | |
|
|
106 | private void useAssemblyMode(ArtifactSlot artifactSlot, AssemblyMode mode) { | |
|
|
107 | var previous = assemblyModes.putIfAbsent(artifactSlot, mode); | |
|
|
108 | if (previous != null && previous != mode) { | |
|
|
109 | throw new InvalidUserDataException("Artifact slot '" + artifactSlot | |
|
|
110 | + "' cannot mix task-produced artifact and contribution-based assembly"); | |
|
|
111 | } | |
|
|
89 | 112 | } |
|
|
90 | 113 | |
|
|
91 | 114 | /** |
|
|
92 |
* Creates the assembly task for the given slot and registers its output |
|
|
|
115 | * Creates the assembly task for the given slot and registers its output | |
|
|
116 | * artifact. | |
|
|
93 | 117 | */ |
|
|
94 | 118 | private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) { |
|
|
95 | 119 | var assembly = new SlotAssembly(); |
|
|
96 | 120 | var fileCollection = assembly.inputs(); |
|
|
97 | 121 | |
|
|
98 | 122 | var outputDirectory = outputDirectory(artifactSlot); |
|
|
99 | 123 | |
|
|
100 | 124 | var task = tasks.register(assembleTaskName(artifactSlot), Sync.class, copy -> { |
|
|
101 | 125 | copy.setGroup(LifecycleBasePlugin.BUILD_GROUP); |
|
|
102 | 126 | copy.into(outputDirectory); |
|
|
103 | 127 | copy.from(fileCollection); |
|
|
104 | 128 | }); |
|
|
105 | 129 | |
|
|
106 | 130 | assemblyRegistry.register(artifactSlot, task, t -> outputDirectory); |
|
|
107 | 131 | |
|
|
108 | 132 | return assembly; |
|
|
109 | 133 | } |
|
|
110 | 134 | |
|
|
111 | 135 | private String assembleTaskName(ArtifactSlot artifactSlot) { |
|
|
112 | 136 | var variantName = artifactSlot.variant().getName(); |
|
|
113 | 137 | var slotName = artifactSlot.slot().getName(); |
|
|
114 | 138 | |
|
|
115 | 139 | return "assembleVariantArtifactSlot" |
|
|
116 | 140 | + "_v" + variantName.length() + "_" + variantName |
|
|
117 | 141 | + "_s" + slotName.length() + "_" + slotName; |
|
|
118 | 142 | } |
|
|
119 | 143 | |
|
|
120 | 144 | private Provider<Directory> outputDirectory(ArtifactSlot artifactSlot) { |
|
|
121 | 145 | return assembliesDirectory.dir( |
|
|
122 | 146 | FilePaths.cat( |
|
|
123 | 147 | artifactSlot.variant().getName(), |
|
|
124 | 148 | artifactSlot.slot().getName())); |
|
|
125 | 149 | } |
|
|
126 | 150 | |
|
|
127 | 151 | /** |
|
|
128 | 152 | * Collects slot contributions into one {@link ConfigurableFileCollection}. |
|
|
129 | 153 | */ |
|
|
130 | 154 | private class ContributionVisitor implements SlotContributionVisitor { |
|
|
131 | 155 | // artifact slot for this assembly |
|
|
132 | 156 | private final ArtifactSlot artifactSlot; |
|
|
133 | 157 | |
|
|
134 | 158 | // seen inputs, used for deduplication |
|
|
135 | 159 | private final SlotAssembly assembly; |
|
|
136 | 160 | |
|
|
137 | 161 | ContributionVisitor(ArtifactSlot artifactSlot, SlotAssembly assembly) { |
|
|
138 | 162 | this.artifactSlot = artifactSlot; |
|
|
139 | 163 | this.assembly = assembly; |
|
|
140 | 164 | } |
|
|
141 | 165 | |
|
|
142 | 166 | @Override |
|
|
143 | 167 | public void visit(DirectContribution contribution) { |
|
|
144 | 168 | contribute( |
|
|
145 | 169 | SlotInputKey.newUniqueKey("Direct input for " + artifactSlot), |
|
|
146 | 170 | contribution.input()); |
|
|
147 | 171 | } |
|
|
148 | 172 | |
|
|
149 | 173 | @Override |
|
|
150 | 174 | public void visit(VariantOutputsContribution contribution) { |
|
|
151 | 175 | var units = compileUnitsView.getUnitsForVariant(artifactSlot.variant()); |
|
|
152 | 176 | contributeCompileUnits(units, contribution.outputs()); |
|
|
153 | 177 | } |
|
|
154 | 178 | |
|
|
155 | 179 | @Override |
|
|
156 | 180 | public void visit(RoleOutputsContribution contribution) { |
|
|
157 | 181 | var roleProjection = roleProjectionsView.requireProjection(artifactSlot.variant(), |
|
|
158 | 182 | contribution.role()); |
|
|
159 | 183 | var units = roleProjectionsView.getUnits(roleProjection); |
|
|
160 | 184 | |
|
|
161 | 185 | contributeCompileUnits(units, contribution.outputs()); |
|
|
162 | 186 | |
|
|
163 | 187 | } |
|
|
164 | 188 | |
|
|
165 | 189 | @Override |
|
|
166 | 190 | public void visit(LayerOutputsContribution contribution) { |
|
|
167 | 191 | var unit = compileUnitsView.requireUnit(artifactSlot.variant(), contribution.layer()); |
|
|
168 | 192 | contributeCompileUnits(Set.of(unit), contribution.outputs()); |
|
|
169 | 193 | } |
|
|
170 | 194 | |
|
|
171 | 195 | private void contributeCompileUnits(Set<CompileUnit> units, Set<String> outputs) { |
|
|
172 | 196 | units.stream() |
|
|
173 | 197 | // expand variant compile units, make (compileUnit, outputName) pairs |
|
|
174 | 198 | .flatMap(unit -> outputs.stream() |
|
|
175 | 199 | .map(output -> new CompileUnitOutputKey(unit, output))) |
|
|
176 | 200 | .forEach(key -> contribute( |
|
|
177 | 201 | key, |
|
|
178 | 202 | sourceSetMaterializer.getSourceSet(key.unit()) |
|
|
179 | 203 | .map(s -> s.output(key.outputName())))); |
|
|
180 | 204 | } |
|
|
181 | 205 | |
|
|
182 | 206 | private void contribute(SlotInputKey key, Object resolvedInput) { |
|
|
183 | 207 | assembly.addSlotInput(key, resolvedInput); |
|
|
184 | 208 | } |
|
|
185 | 209 | } |
|
|
186 | 210 | |
|
|
187 | 211 | /** Mutable input state for one slot assembly. */ |
|
|
188 | 212 | class SlotAssembly { |
|
|
189 | 213 | private final ConfigurableFileCollection inputs = objects.fileCollection(); |
|
|
190 | 214 | private final Set<SlotInputKey> seen = new HashSet<>(); |
|
|
191 | 215 | |
|
|
192 | 216 | public void addSlotInput(SlotInputKey key, Object input) { |
|
|
193 | 217 | if (!seen.add(key)) |
|
|
194 | 218 | return; |
|
|
195 | 219 | inputs.from(input); |
|
|
196 | 220 | } |
|
|
197 | 221 | |
|
|
198 | 222 | public FileCollection inputs() { |
|
|
199 | 223 | return inputs; |
|
|
200 | 224 | } |
|
|
201 | 225 | } |
|
|
226 | ||
|
|
227 | private enum AssemblyMode { | |
|
|
228 | CONTRIBUTIONS, | |
|
|
229 | TASK_PRODUCER | |
|
|
202 | 230 | } |
|
|
231 | ||
|
|
232 | /** | |
|
|
233 | * Default DSL facade for collecting {@link SlotContribution} declarations. | |
|
|
234 | * | |
|
|
235 | * <p> | |
|
|
236 | * The spec does not validate topology references immediately. It translates DSL | |
|
|
237 | * calls to contribution objects and passes them to the supplied consumer; | |
|
|
238 | * semantic | |
|
|
239 | * validation happens later when the assembly handler resolves contributions | |
|
|
240 | * against the finalized source model. | |
|
|
241 | */ | |
|
|
242 | class DefaultArtifactAssemblySpec implements ArtifactAssemblySpec { | |
|
|
243 | ||
|
|
244 | private final ArtifactSlot artifactSlot; | |
|
|
245 | ||
|
|
246 | DefaultArtifactAssemblySpec(ArtifactSlot artifactSlot) { | |
|
|
247 | this.artifactSlot = artifactSlot; | |
|
|
248 | } | |
|
|
249 | ||
|
|
250 | @Override | |
|
|
251 | public void from(Object artifact) { | |
|
|
252 | contribute(new DirectContribution(artifact)); | |
|
|
253 | } | |
|
|
254 | ||
|
|
255 | @Override | |
|
|
256 | public <T extends Task> void producedBy( | |
|
|
257 | TaskProvider<T> task, | |
|
|
258 | Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact) { | |
|
|
259 | registerProducedArtifact(task, artifact); | |
|
|
260 | } | |
|
|
261 | ||
|
|
262 | @Override | |
|
|
263 | public void fromVariant(Action<? super OutputSelectionSpec> action) { | |
|
|
264 | contribute(new VariantOutputsContribution(outputs(action))); | |
|
|
265 | } | |
|
|
266 | ||
|
|
267 | @Override | |
|
|
268 | public void fromRole(String roleName, Action<? super OutputSelectionSpec> action) { | |
|
|
269 | ||
|
|
270 | contribute(new RoleOutputsContribution( | |
|
|
271 | objects.named(Role.class, roleName), | |
|
|
272 | outputs(action))); | |
|
|
273 | } | |
|
|
274 | ||
|
|
275 | @Override | |
|
|
276 | public void fromLayer(String layerName, Action<? super OutputSelectionSpec> action) { | |
|
|
277 | contribute(new LayerOutputsContribution( | |
|
|
278 | objects.named(Layer.class, layerName), | |
|
|
279 | outputs(action))); | |
|
|
280 | } | |
|
|
281 | ||
|
|
282 | private static Set<String> outputs(Action<? super OutputSelectionSpec> action) { | |
|
|
283 | var spec = new OutputsSetSpec(); | |
|
|
284 | action.execute(spec); | |
|
|
285 | return spec.outputs(); | |
|
|
286 | } | |
|
|
287 | ||
|
|
288 | void contribute(SlotContribution contribution) { | |
|
|
289 | useAssemblyMode(artifactSlot, AssemblyMode.CONTRIBUTIONS); | |
|
|
290 | var assembly = slotInputs.computeIfAbsent(artifactSlot, ArtifactAssemblyHandler.this::createSlotAssembly); | |
|
|
291 | var contributionVisitor = new ContributionVisitor(artifactSlot, assembly); | |
|
|
292 | contribution.accept(contributionVisitor); | |
|
|
293 | } | |
|
|
294 | ||
|
|
295 | <T extends Task> void registerProducedArtifact( | |
|
|
296 | TaskProvider<T> task, | |
|
|
297 | Function<? super T, ? extends Provider<? extends FileSystemLocation>> artifact) { | |
|
|
298 | useAssemblyMode(artifactSlot, AssemblyMode.TASK_PRODUCER); | |
|
|
299 | assemblyRegistry.register(artifactSlot, task, artifact); | |
|
|
300 | } | |
|
|
301 | ||
|
|
302 | } | |
|
|
303 | ||
|
|
304 | /** Simple implementation of {@link OutputSelectionSpec}. */ | |
|
|
305 | static class OutputsSetSpec implements OutputSelectionSpec { | |
|
|
306 | private final Set<String> outputs = new HashSet<>(); | |
|
|
307 | ||
|
|
308 | @Override | |
|
|
309 | public void output(String name, String... extra) { | |
|
|
310 | Stream.concat(Stream.of(name), Stream.of(extra)) | |
|
|
311 | .map(Strings::requireNonBlank) | |
|
|
312 | .forEach(outputs::add); | |
|
|
313 | } | |
|
|
314 | ||
|
|
315 | Set<String> outputs() { | |
|
|
316 | return Set.copyOf(outputs); | |
|
|
317 | } | |
|
|
318 | ||
|
|
319 | } | |
|
|
320 | ||
|
|
321 | } | |
| @@ -1,703 +1,867 | |||
|
|
1 | 1 | package org.implab.gradle.variants; |
|
|
2 | 2 | |
|
|
3 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.gradle.testkit.runner.BuildResult; |
|
|
6 | 6 | import org.gradle.testkit.runner.TaskOutcome; |
|
|
7 | 7 | import org.junit.jupiter.api.Test; |
|
|
8 | 8 | |
|
|
9 | 9 | class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest { |
|
|
10 | 10 | |
|
|
11 | 11 | @Test |
|
|
12 | 12 | void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception { |
|
|
13 | 13 | writeFile("settings.gradle", """ |
|
|
14 | 14 | rootProject.name = 'gradle-reference-outgoing-resolution' |
|
|
15 | 15 | include 'producer', 'consumer' |
|
|
16 | 16 | """); |
|
|
17 | 17 | writeFile("producer/inputs/typesPackage", "types\n"); |
|
|
18 | 18 | writeFile("producer/inputs/js", "js\n"); |
|
|
19 | 19 | writeBuildFile(""" |
|
|
20 | 20 | import org.gradle.api.attributes.Attribute |
|
|
21 | 21 | |
|
|
22 | 22 | def variantAttr = Attribute.of('test.variant', String) |
|
|
23 | 23 | def slotAttr = Attribute.of('test.slot', String) |
|
|
24 | 24 | |
|
|
25 | 25 | project(':producer') { |
|
|
26 | 26 | def browserElements = configurations.consumable('browserElements') |
|
|
27 | 27 | |
|
|
28 | 28 | println('reference: registered browserElements provider') |
|
|
29 | 29 | |
|
|
30 | 30 | browserElements.configure { configuration -> |
|
|
31 | 31 | println('reference: configuring browserElements') |
|
|
32 | 32 | |
|
|
33 | 33 | configuration.attributes.attribute(variantAttr, 'browser') |
|
|
34 | 34 | configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage') |
|
|
35 | 35 | configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage')) |
|
|
36 | 36 | |
|
|
37 | 37 | configuration.outgoing.variants.create('js') { secondary -> |
|
|
38 | 38 | println('reference: creating js outgoing variant') |
|
|
39 | 39 | |
|
|
40 | 40 | secondary.attributes.attribute(slotAttr, 'js') |
|
|
41 | 41 | secondary.artifact(layout.projectDirectory.file('inputs/js')) |
|
|
42 | 42 | } |
|
|
43 | 43 | } |
|
|
44 | 44 | } |
|
|
45 | 45 | |
|
|
46 | 46 | project(':consumer') { |
|
|
47 | 47 | configurations { |
|
|
48 | 48 | compileView { |
|
|
49 | 49 | canBeResolved = true |
|
|
50 | 50 | canBeConsumed = false |
|
|
51 | 51 | canBeDeclared = true |
|
|
52 | 52 | attributes { |
|
|
53 | 53 | attribute(variantAttr, 'browser') |
|
|
54 | 54 | attribute(slotAttr, 'typesPackage') |
|
|
55 | 55 | } |
|
|
56 | 56 | } |
|
|
57 | 57 | } |
|
|
58 | 58 | |
|
|
59 | 59 | dependencies { |
|
|
60 | 60 | compileView project(':producer') |
|
|
61 | 61 | } |
|
|
62 | 62 | |
|
|
63 | 63 | tasks.register('probe') { |
|
|
64 | 64 | doLast { |
|
|
65 | 65 | println('reference: resolving primary files') |
|
|
66 | 66 | |
|
|
67 | 67 | def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',') |
|
|
68 | 68 | |
|
|
69 | 69 | println('reference: resolving secondary files') |
|
|
70 | 70 | |
|
|
71 | 71 | def jsFiles = configurations.compileView.incoming.artifactView { |
|
|
72 | 72 | attributes { |
|
|
73 | 73 | attribute(slotAttr, 'js') |
|
|
74 | 74 | } |
|
|
75 | 75 | }.files.files.collect { it.name }.sort().join(',') |
|
|
76 | 76 | |
|
|
77 | 77 | println('compileFiles=' + compileFiles) |
|
|
78 | 78 | println('jsFiles=' + jsFiles) |
|
|
79 | 79 | } |
|
|
80 | 80 | } |
|
|
81 | 81 | } |
|
|
82 | 82 | """); |
|
|
83 | 83 | |
|
|
84 | 84 | BuildResult result = runner(":consumer:probe").build(); |
|
|
85 | 85 | var output = result.getOutput(); |
|
|
86 | 86 | var registered = output.indexOf("reference: registered browserElements provider"); |
|
|
87 | 87 | var resolvingPrimary = output.indexOf("reference: resolving primary files"); |
|
|
88 | 88 | var configuring = output.indexOf("reference: configuring browserElements"); |
|
|
89 | 89 | var creatingSecondary = output.indexOf("reference: creating js outgoing variant"); |
|
|
90 | 90 | var resolvingSecondary = output.indexOf("reference: resolving secondary files"); |
|
|
91 | 91 | |
|
|
92 | 92 | assertTrue(registered >= 0); |
|
|
93 | 93 | assertTrue(resolvingPrimary >= 0); |
|
|
94 | 94 | assertTrue(configuring >= 0); |
|
|
95 | 95 | assertTrue(creatingSecondary >= 0); |
|
|
96 | 96 | assertTrue(resolvingSecondary >= 0); |
|
|
97 | 97 | assertTrue(registered < resolvingPrimary); |
|
|
98 | 98 | assertTrue(resolvingPrimary < configuring); |
|
|
99 | 99 | assertTrue(configuring < creatingSecondary); |
|
|
100 | 100 | assertTrue(creatingSecondary < resolvingSecondary); |
|
|
101 | 101 | assertTrue(output.contains("compileFiles=typesPackage")); |
|
|
102 | 102 | assertTrue(output.contains("jsFiles=js")); |
|
|
103 | 103 | } |
|
|
104 | 104 | |
|
|
105 | 105 | @Test |
|
|
106 | 106 | void gradleReferenceRegisteredSecondaryArtifactVariantIsNotRealizedBeforeResolution() throws Exception { |
|
|
107 | 107 | // Gradle issue: https://github.com/gradle/gradle/issues/27441 |
|
|
108 | 108 | // Registered outgoing artifact variants are not realized before dependency resolution. |
|
|
109 | 109 | writeFile("settings.gradle", """ |
|
|
110 | 110 | rootProject.name = 'gradle-reference-registered-secondary-variant' |
|
|
111 | 111 | include 'producer', 'consumer' |
|
|
112 | 112 | """); |
|
|
113 | 113 | writeFile("producer/inputs/typesPackage", "types\n"); |
|
|
114 | 114 | writeFile("producer/inputs/js", "js\n"); |
|
|
115 | 115 | writeFile("build.gradle", """ |
|
|
116 | 116 | import org.gradle.api.attributes.Attribute |
|
|
117 | 117 | |
|
|
118 | 118 | def variantAttr = Attribute.of('test.variant', String) |
|
|
119 | 119 | def slotAttr = Attribute.of('test.slot', String) |
|
|
120 | 120 | |
|
|
121 | 121 | project(':producer') { |
|
|
122 | 122 | def browserElements = configurations.consumable('browserElements') |
|
|
123 | 123 | |
|
|
124 | 124 | browserElements.configure { configuration -> |
|
|
125 | 125 | configuration.attributes.attribute(variantAttr, 'browser') |
|
|
126 | 126 | configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage') |
|
|
127 | 127 | configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage')) |
|
|
128 | 128 | |
|
|
129 | 129 | configuration.outgoing.variants.register('js') { secondary -> |
|
|
130 | 130 | secondary.attributes.attribute(slotAttr, 'js') |
|
|
131 | 131 | secondary.artifact(layout.projectDirectory.file('inputs/js')) |
|
|
132 | 132 | } |
|
|
133 | 133 | } |
|
|
134 | 134 | } |
|
|
135 | 135 | |
|
|
136 | 136 | project(':consumer') { |
|
|
137 | 137 | configurations { |
|
|
138 | 138 | compileView { |
|
|
139 | 139 | canBeResolved = true |
|
|
140 | 140 | canBeConsumed = false |
|
|
141 | 141 | canBeDeclared = true |
|
|
142 | 142 | attributes { |
|
|
143 | 143 | attribute(variantAttr, 'browser') |
|
|
144 | 144 | attribute(slotAttr, 'typesPackage') |
|
|
145 | 145 | } |
|
|
146 | 146 | } |
|
|
147 | 147 | } |
|
|
148 | 148 | |
|
|
149 | 149 | dependencies { |
|
|
150 | 150 | compileView project(':producer') |
|
|
151 | 151 | } |
|
|
152 | 152 | |
|
|
153 | 153 | tasks.register('probe') { |
|
|
154 | 154 | doLast { |
|
|
155 | 155 | def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',') |
|
|
156 | 156 | def jsFiles = configurations.compileView.incoming.artifactView { |
|
|
157 | 157 | attributes { |
|
|
158 | 158 | attribute(slotAttr, 'js') |
|
|
159 | 159 | } |
|
|
160 | 160 | }.files.files.collect { it.name }.sort().join(',') |
|
|
161 | 161 | |
|
|
162 | 162 | println('compileFiles=' + compileFiles) |
|
|
163 | 163 | println('jsFiles=' + jsFiles) |
|
|
164 | 164 | } |
|
|
165 | 165 | } |
|
|
166 | 166 | } |
|
|
167 | 167 | """); |
|
|
168 | 168 | |
|
|
169 | 169 | assertBuildFails("Cannot create variant 'js' after dependency configuration ':producer:browserElements' has been resolved", |
|
|
170 | 170 | ":consumer:probe"); |
|
|
171 | 171 | } |
|
|
172 | 172 | |
|
|
173 | 173 | @Test |
|
|
174 | 174 | void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception { |
|
|
175 | 175 | writeSettings("variant-artifacts-slots"); |
|
|
176 | 176 | writeFile("inputs/base.js", "console.log('base')\n"); |
|
|
177 | 177 | writeFile("inputs/amd.js", "console.log('amd')\n"); |
|
|
178 | 178 | writeFile("inputs/mainJs.txt", "mainJs marker\n"); |
|
|
179 | 179 | writeFile("inputs/amdJs.txt", "amdJs marker\n"); |
|
|
180 | 180 | writeBuildFile(""" |
|
|
181 | 181 | import org.gradle.api.attributes.Attribute |
|
|
182 | 182 | |
|
|
183 | 183 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
184 | 184 | |
|
|
185 | 185 | def variantAttr = Attribute.of('test.variant', String) |
|
|
186 | 186 | def slotAttr = Attribute.of('test.slot', String) |
|
|
187 | 187 | |
|
|
188 | 188 | variants.layers.create('mainBase') |
|
|
189 | 189 | variants.layers.create('mainAmd') |
|
|
190 | 190 | variants.roles.create('main') |
|
|
191 | 191 | variants.roles.create('test') |
|
|
192 | 192 | variants.variant('browser') { |
|
|
193 | 193 | role('main') { |
|
|
194 | 194 | layers('mainBase', 'mainAmd') |
|
|
195 | 195 | } |
|
|
196 | 196 | } |
|
|
197 | 197 | |
|
|
198 | 198 | variantSources { |
|
|
199 | 199 | layer('mainBase') { |
|
|
200 | 200 | sourceSet { |
|
|
201 | 201 | declareOutputs('js') |
|
|
202 | 202 | registerOutput('js', layout.projectDirectory.file('inputs/base.js')) |
|
|
203 | 203 | } |
|
|
204 | 204 | } |
|
|
205 | 205 | layer('mainAmd') { |
|
|
206 | 206 | sourceSet { |
|
|
207 | 207 | declareOutputs('js') |
|
|
208 | 208 | registerOutput('js', layout.projectDirectory.file('inputs/amd.js')) |
|
|
209 | 209 | } |
|
|
210 | 210 | } |
|
|
211 | 211 | } |
|
|
212 | 212 | |
|
|
213 | 213 | variantArtifacts { |
|
|
214 | 214 | variant('browser') { |
|
|
215 | 215 | primarySlot('mainJs') { |
|
|
216 | 216 | fromRole('main') { |
|
|
217 | 217 | output('js') |
|
|
218 | 218 | } |
|
|
219 | 219 | from(layout.projectDirectory.file('inputs/mainJs.txt')) |
|
|
220 | 220 | } |
|
|
221 | 221 | slot('amdJs') { |
|
|
222 | 222 | fromLayer('mainAmd') { |
|
|
223 | 223 | output('js') |
|
|
224 | 224 | } |
|
|
225 | 225 | from(layout.projectDirectory.file('inputs/amdJs.txt')) |
|
|
226 | 226 | } |
|
|
227 | 227 | } |
|
|
228 | 228 | |
|
|
229 | 229 | whenOutgoingConfiguration { publication -> |
|
|
230 | 230 | publication.configuration { |
|
|
231 | 231 | attributes.attribute(variantAttr, publication.variant.name) |
|
|
232 | 232 | } |
|
|
233 | 233 | } |
|
|
234 | 234 | |
|
|
235 | 235 | whenOutgoingSlot { publication -> |
|
|
236 | 236 | def slotName = publication.artifactSlot.slot.name |
|
|
237 | 237 | publication.artifactAttributes { |
|
|
238 | 238 | attribute(slotAttr, slotName) |
|
|
239 | 239 | } |
|
|
240 | 240 | } |
|
|
241 | 241 | } |
|
|
242 | 242 | |
|
|
243 | 243 | tasks.register('probe') { |
|
|
244 | 244 | dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs' |
|
|
245 | 245 | dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs' |
|
|
246 | 246 | |
|
|
247 | 247 | doLast { |
|
|
248 | 248 | def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile |
|
|
249 | 249 | def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile |
|
|
250 | 250 | |
|
|
251 | 251 | assert new File(mainDir, 'base.js').exists() |
|
|
252 | 252 | assert new File(mainDir, 'amd.js').exists() |
|
|
253 | 253 | assert new File(mainDir, 'mainJs.txt').exists() |
|
|
254 | 254 | |
|
|
255 | 255 | assert !new File(amdDir, 'base.js').exists() |
|
|
256 | 256 | assert new File(amdDir, 'amd.js').exists() |
|
|
257 | 257 | assert new File(amdDir, 'amdJs.txt').exists() |
|
|
258 | 258 | |
|
|
259 | 259 | def elements = configurations.getByName('browserElements') |
|
|
260 | 260 | def amdVariant = elements.outgoing.variants.getByName('amdJs') |
|
|
261 | 261 | |
|
|
262 | 262 | println('variantAttr=' + elements.attributes.getAttribute(variantAttr)) |
|
|
263 | 263 | println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr)) |
|
|
264 | 264 | println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr)) |
|
|
265 | 265 | println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(',')) |
|
|
266 | 266 | println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(',')) |
|
|
267 | 267 | } |
|
|
268 | 268 | } |
|
|
269 | 269 | """); |
|
|
270 | 270 | |
|
|
271 | 271 | BuildResult result = runner("probe").build(); |
|
|
272 | 272 | |
|
|
273 | 273 | assertTrue(result.getOutput().contains("variantAttr=browser")); |
|
|
274 | 274 | assertTrue(result.getOutput().contains("primarySlotAttr=mainJs")); |
|
|
275 | 275 | assertTrue(result.getOutput().contains("amdSlotAttr=amdJs")); |
|
|
276 | 276 | assertTrue(result.getOutput().contains("configurations=browserElements")); |
|
|
277 | 277 | assertTrue(result.getOutput().contains("secondaryVariants=amdJs")); |
|
|
278 | 278 | assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); |
|
|
279 | 279 | } |
|
|
280 | 280 | |
|
|
281 | 281 | @Test |
|
|
282 | 282 | void outgoingSlotHookFollowsMaterializedGradleArtifactVariants() throws Exception { |
|
|
283 | 283 | writeSettings("variant-artifacts-materialized-gradle-variant"); |
|
|
284 | 284 | writeFile("inputs/typesPackage", "types\n"); |
|
|
285 | 285 | writeFile("inputs/js", "js\n"); |
|
|
286 | 286 | writeBuildFile(""" |
|
|
287 | 287 | import org.gradle.api.attributes.Attribute |
|
|
288 | 288 | |
|
|
289 | 289 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
290 | 290 | |
|
|
291 | 291 | def slotAttr = Attribute.of('test.slot', String) |
|
|
292 | 292 | |
|
|
293 | 293 | variants.layers.create('main') |
|
|
294 | 294 | variants.roles.create('main') |
|
|
295 | 295 | variants.variant('browser') { |
|
|
296 | 296 | role('main') { |
|
|
297 | 297 | layers('main') |
|
|
298 | 298 | } |
|
|
299 | 299 | } |
|
|
300 | 300 | |
|
|
301 | 301 | variantArtifacts { |
|
|
302 | 302 | variant('browser') { |
|
|
303 | 303 | primarySlot('typesPackage') { |
|
|
304 | 304 | from(layout.projectDirectory.file('inputs/typesPackage')) |
|
|
305 | 305 | } |
|
|
306 | 306 | } |
|
|
307 | 307 | |
|
|
308 | 308 | whenOutgoingConfiguration { publication -> |
|
|
309 | 309 | publication.configuration { |
|
|
310 | 310 | outgoing.variants.create('js') { secondary -> |
|
|
311 | 311 | secondary.artifact(layout.projectDirectory.file('inputs/js')) |
|
|
312 | 312 | } |
|
|
313 | 313 | } |
|
|
314 | 314 | } |
|
|
315 | 315 | |
|
|
316 | 316 | whenOutgoingSlot { publication -> |
|
|
317 | 317 | publication.artifactAttributes { |
|
|
318 | 318 | attribute(slotAttr, publication.artifactSlot.slot.name) |
|
|
319 | 319 | } |
|
|
320 | 320 | } |
|
|
321 | 321 | } |
|
|
322 | 322 | |
|
|
323 | 323 | tasks.register('probe') { |
|
|
324 | 324 | doLast { |
|
|
325 | 325 | def elements = configurations.getByName('browserElements') |
|
|
326 | 326 | def jsVariant = elements.outgoing.variants.getByName('js') |
|
|
327 | 327 | |
|
|
328 | 328 | println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr)) |
|
|
329 | 329 | println('jsSlotAttr=' + jsVariant.attributes.getAttribute(slotAttr)) |
|
|
330 | 330 | } |
|
|
331 | 331 | } |
|
|
332 | 332 | """); |
|
|
333 | 333 | |
|
|
334 | 334 | BuildResult result = runner("probe").build(); |
|
|
335 | 335 | |
|
|
336 | 336 | assertTrue(result.getOutput().contains("primarySlotAttr=typesPackage")); |
|
|
337 | 337 | assertTrue(result.getOutput().contains("jsSlotAttr=js")); |
|
|
338 | 338 | assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); |
|
|
339 | 339 | } |
|
|
340 | 340 | |
|
|
341 | 341 | @Test |
|
|
342 | 342 | void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception { |
|
|
343 | 343 | writeSettings("variant-artifacts-single-slot"); |
|
|
344 | 344 | writeBuildFile(""" |
|
|
345 | 345 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
346 | 346 | |
|
|
347 | 347 | variants.layers.create('main') |
|
|
348 | 348 | variants.roles.create('main') |
|
|
349 | 349 | variants.variant('browser') { |
|
|
350 | 350 | role('main') { |
|
|
351 | 351 | layers('main') |
|
|
352 | 352 | } |
|
|
353 | 353 | } |
|
|
354 | 354 | |
|
|
355 | 355 | variantSources.layer('main') { |
|
|
356 | 356 | sourceSet { |
|
|
357 | 357 | declareOutputs('types') |
|
|
358 | 358 | } |
|
|
359 | 359 | } |
|
|
360 | 360 | |
|
|
361 | 361 | variantArtifacts { |
|
|
362 | 362 | variant('browser') { |
|
|
363 | 363 | slot('typesPackage') { |
|
|
364 | 364 | fromVariant { |
|
|
365 | 365 | output('types') |
|
|
366 | 366 | } |
|
|
367 | 367 | } |
|
|
368 | 368 | } |
|
|
369 | 369 | } |
|
|
370 | 370 | |
|
|
371 | 371 | tasks.register('probe') { |
|
|
372 | 372 | doLast { |
|
|
373 | 373 | variantArtifacts.whenAvailable { ctx -> |
|
|
374 | 374 | def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser') |
|
|
375 | 375 | println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name) |
|
|
376 | 376 | } |
|
|
377 | 377 | } |
|
|
378 | 378 | } |
|
|
379 | 379 | """); |
|
|
380 | 380 | |
|
|
381 | 381 | BuildResult result = runner("probe").build(); |
|
|
382 | 382 | |
|
|
383 | 383 | assertTrue(result.getOutput().contains("primary=typesPackage")); |
|
|
384 | 384 | } |
|
|
385 | 385 | |
|
|
386 | 386 | @Test |
|
|
387 | 387 | void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception { |
|
|
388 | 388 | writeSettings("variant-artifacts-direct-input"); |
|
|
389 | 389 | writeFile("inputs/bundle.js", "console.log('bundle')\n"); |
|
|
390 | 390 | writeBuildFile(""" |
|
|
391 | 391 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
392 | 392 | |
|
|
393 | 393 | variants.layers.create('main') |
|
|
394 | 394 | variants.roles.create('main') |
|
|
395 | 395 | variants.variant('browser') { |
|
|
396 | 396 | role('main') { |
|
|
397 | 397 | layers('main') |
|
|
398 | 398 | } |
|
|
399 | 399 | } |
|
|
400 | 400 | |
|
|
401 | 401 | variantArtifacts { |
|
|
402 | 402 | variant('browser') { |
|
|
403 | 403 | primarySlot('bundle') { |
|
|
404 | 404 | from(layout.projectDirectory.file('inputs/bundle.js')) |
|
|
405 | 405 | } |
|
|
406 | 406 | } |
|
|
407 | 407 | } |
|
|
408 | 408 | |
|
|
409 | 409 | tasks.register('probe') { |
|
|
410 | 410 | dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle' |
|
|
411 | 411 | |
|
|
412 | 412 | doLast { |
|
|
413 | 413 | def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile |
|
|
414 | 414 | assert new File(bundleDir, 'bundle.js').exists() |
|
|
415 | 415 | } |
|
|
416 | 416 | } |
|
|
417 | 417 | """); |
|
|
418 | 418 | |
|
|
419 | 419 | BuildResult result = runner("probe").build(); |
|
|
420 | 420 | |
|
|
421 | 421 | assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); |
|
|
422 | 422 | } |
|
|
423 | 423 | |
|
|
424 | 424 | @Test |
|
|
425 | void publishesTaskProducedFileArtifactDirectly() throws Exception { | |
|
|
426 | writeFile("settings.gradle", """ | |
|
|
427 | rootProject.name = 'variant-artifacts-task-produced-file' | |
|
|
428 | include 'producer', 'consumer' | |
|
|
429 | """); | |
|
|
430 | writeBuildFile(""" | |
|
|
431 | import org.gradle.api.DefaultTask | |
|
|
432 | import org.gradle.api.attributes.Attribute | |
|
|
433 | import org.gradle.api.file.RegularFileProperty | |
|
|
434 | import org.gradle.api.tasks.OutputFile | |
|
|
435 | import org.gradle.api.tasks.TaskAction | |
|
|
436 | ||
|
|
437 | def variantAttr = Attribute.of('test.variant', String) | |
|
|
438 | def slotAttr = Attribute.of('test.slot', String) | |
|
|
439 | ||
|
|
440 | abstract class WritePackageMetadata extends DefaultTask { | |
|
|
441 | @OutputFile | |
|
|
442 | abstract RegularFileProperty getOutputFile() | |
|
|
443 | ||
|
|
444 | @TaskAction | |
|
|
445 | void write() { | |
|
|
446 | def file = outputFile.get().asFile | |
|
|
447 | file.parentFile.mkdirs() | |
|
|
448 | file.text = '{"name":"demo"}\\n' | |
|
|
449 | } | |
|
|
450 | } | |
|
|
451 | ||
|
|
452 | project(':producer') { | |
|
|
453 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin | |
|
|
454 | ||
|
|
455 | variants.layers.create('main') | |
|
|
456 | variants.roles.create('main') | |
|
|
457 | variants.variant('browser') { | |
|
|
458 | role('main') { | |
|
|
459 | layers('main') | |
|
|
460 | } | |
|
|
461 | } | |
|
|
462 | ||
|
|
463 | def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) { | |
|
|
464 | outputFile.set(layout.buildDirectory.file('generated/package.json')) | |
|
|
465 | } | |
|
|
466 | ||
|
|
467 | variantArtifacts { | |
|
|
468 | variant('browser') { | |
|
|
469 | primarySlot('packageMetadata') { | |
|
|
470 | producedBy(writePackageMetadata) { | |
|
|
471 | outputFile | |
|
|
472 | } | |
|
|
473 | } | |
|
|
474 | } | |
|
|
475 | ||
|
|
476 | whenOutgoingConfiguration { publication -> | |
|
|
477 | publication.configuration { | |
|
|
478 | attributes.attribute(variantAttr, publication.variant.name) | |
|
|
479 | } | |
|
|
480 | } | |
|
|
481 | ||
|
|
482 | whenOutgoingSlot { publication -> | |
|
|
483 | publication.artifactAttributes { | |
|
|
484 | attribute(slotAttr, publication.artifactSlot.slot.name) | |
|
|
485 | } | |
|
|
486 | } | |
|
|
487 | } | |
|
|
488 | ||
|
|
489 | tasks.register('checkNoManagedAssembly') { | |
|
|
490 | doLast { | |
|
|
491 | def assemblyTasks = tasks.names | |
|
|
492 | .findAll { it.startsWith('assembleVariantArtifactSlot') } | |
|
|
493 | .sort() | |
|
|
494 | println('producerAssemblyTasks=' + assemblyTasks.join(',')) | |
|
|
495 | assert assemblyTasks.empty | |
|
|
496 | } | |
|
|
497 | } | |
|
|
498 | } | |
|
|
499 | ||
|
|
500 | project(':consumer') { | |
|
|
501 | configurations { | |
|
|
502 | compileView { | |
|
|
503 | canBeResolved = true | |
|
|
504 | canBeConsumed = false | |
|
|
505 | canBeDeclared = true | |
|
|
506 | attributes { | |
|
|
507 | attribute(variantAttr, 'browser') | |
|
|
508 | attribute(slotAttr, 'packageMetadata') | |
|
|
509 | } | |
|
|
510 | } | |
|
|
511 | } | |
|
|
512 | ||
|
|
513 | dependencies { | |
|
|
514 | compileView project(':producer') | |
|
|
515 | } | |
|
|
516 | ||
|
|
517 | tasks.register('probe') { | |
|
|
518 | dependsOn configurations.compileView | |
|
|
519 | dependsOn ':producer:checkNoManagedAssembly' | |
|
|
520 | ||
|
|
521 | doLast { | |
|
|
522 | def files = configurations.compileView.files | |
|
|
523 | println('resolvedFiles=' + files.collect { it.name }.sort().join(',')) | |
|
|
524 | println('metadata=' + files.iterator().next().text.trim()) | |
|
|
525 | } | |
|
|
526 | } | |
|
|
527 | } | |
|
|
528 | """); | |
|
|
529 | ||
|
|
530 | BuildResult result = runner(":consumer:probe").build(); | |
|
|
531 | ||
|
|
532 | assertTrue(result.getOutput().contains("producerAssemblyTasks=")); | |
|
|
533 | assertTrue(result.getOutput().contains("resolvedFiles=package.json")); | |
|
|
534 | assertTrue(result.getOutput().contains("metadata={\"name\":\"demo\"}")); | |
|
|
535 | assertTrue(result.task(":producer:writePackageMetadata").getOutcome() == TaskOutcome.SUCCESS); | |
|
|
536 | assertTrue(result.task(":consumer:probe").getOutcome() == TaskOutcome.SUCCESS); | |
|
|
537 | } | |
|
|
538 | ||
|
|
539 | @Test | |
|
|
540 | void failsWhenTaskProducedArtifactIsMixedWithContributionAssembly() throws Exception { | |
|
|
541 | writeSettings("variant-artifacts-mixed-assembly-mode"); | |
|
|
542 | writeFile("inputs/marker.txt", "marker\n"); | |
|
|
543 | writeBuildFile(""" | |
|
|
544 | import org.gradle.api.DefaultTask | |
|
|
545 | import org.gradle.api.file.RegularFileProperty | |
|
|
546 | import org.gradle.api.tasks.OutputFile | |
|
|
547 | import org.gradle.api.tasks.TaskAction | |
|
|
548 | ||
|
|
549 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin | |
|
|
550 | ||
|
|
551 | abstract class WritePackageMetadata extends DefaultTask { | |
|
|
552 | @OutputFile | |
|
|
553 | abstract RegularFileProperty getOutputFile() | |
|
|
554 | ||
|
|
555 | @TaskAction | |
|
|
556 | void write() { | |
|
|
557 | outputFile.get().asFile.text = '{}\\n' | |
|
|
558 | } | |
|
|
559 | } | |
|
|
560 | ||
|
|
561 | variants.layers.create('main') | |
|
|
562 | variants.roles.create('main') | |
|
|
563 | variants.variant('browser') { | |
|
|
564 | role('main') { | |
|
|
565 | layers('main') | |
|
|
566 | } | |
|
|
567 | } | |
|
|
568 | ||
|
|
569 | def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) { | |
|
|
570 | outputFile.set(layout.buildDirectory.file('generated/package.json')) | |
|
|
571 | } | |
|
|
572 | ||
|
|
573 | variantArtifacts { | |
|
|
574 | variant('browser') { | |
|
|
575 | primarySlot('packageMetadata') { | |
|
|
576 | producedBy(writePackageMetadata) { | |
|
|
577 | outputFile | |
|
|
578 | } | |
|
|
579 | from(layout.projectDirectory.file('inputs/marker.txt')) | |
|
|
580 | } | |
|
|
581 | } | |
|
|
582 | } | |
|
|
583 | """); | |
|
|
584 | ||
|
|
585 | assertBuildFails("cannot mix task-produced artifact and contribution-based assembly", "help"); | |
|
|
586 | } | |
|
|
587 | ||
|
|
588 | @Test | |
|
|
425 | 589 | void combinesDirectAndTopologyAwareSlotInputs() throws Exception { |
|
|
426 | 590 | writeSettings("variant-artifacts-combined-inputs"); |
|
|
427 | 591 | writeFile("inputs/base.js", "console.log('base')\n"); |
|
|
428 | 592 | writeFile("inputs/marker.txt", "marker\n"); |
|
|
429 | 593 | writeBuildFile(""" |
|
|
430 | 594 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
431 | 595 | |
|
|
432 | 596 | variants.layers.create('main') |
|
|
433 | 597 | variants.roles.create('main') |
|
|
434 | 598 | variants.variant('browser') { |
|
|
435 | 599 | role('main') { |
|
|
436 | 600 | layers('main') |
|
|
437 | 601 | } |
|
|
438 | 602 | } |
|
|
439 | 603 | |
|
|
440 | 604 | variantSources.layer('main') { |
|
|
441 | 605 | sourceSet { |
|
|
442 | 606 | declareOutputs('js') |
|
|
443 | 607 | registerOutput('js', layout.projectDirectory.file('inputs/base.js')) |
|
|
444 | 608 | } |
|
|
445 | 609 | } |
|
|
446 | 610 | |
|
|
447 | 611 | variantArtifacts { |
|
|
448 | 612 | variant('browser') { |
|
|
449 | 613 | primarySlot('bundle') { |
|
|
450 | 614 | fromVariant { |
|
|
451 | 615 | output('js') |
|
|
452 | 616 | } |
|
|
453 | 617 | from(layout.projectDirectory.file('inputs/marker.txt')) |
|
|
454 | 618 | } |
|
|
455 | 619 | } |
|
|
456 | 620 | } |
|
|
457 | 621 | |
|
|
458 | 622 | tasks.register('probe') { |
|
|
459 | 623 | dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle' |
|
|
460 | 624 | |
|
|
461 | 625 | doLast { |
|
|
462 | 626 | def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile |
|
|
463 | 627 | assert new File(bundleDir, 'base.js').exists() |
|
|
464 | 628 | assert new File(bundleDir, 'marker.txt').exists() |
|
|
465 | 629 | } |
|
|
466 | 630 | } |
|
|
467 | 631 | """); |
|
|
468 | 632 | |
|
|
469 | 633 | BuildResult result = runner("probe").build(); |
|
|
470 | 634 | |
|
|
471 | 635 | assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); |
|
|
472 | 636 | } |
|
|
473 | 637 | |
|
|
474 | 638 | @Test |
|
|
475 | 639 | void failsOnUnknownVariantReference() throws Exception { |
|
|
476 | 640 | writeSettings("variant-artifacts-missing-variant"); |
|
|
477 | 641 | writeBuildFile(""" |
|
|
478 | 642 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
479 | 643 | |
|
|
480 | 644 | variants.layers.create('main') |
|
|
481 | 645 | |
|
|
482 | 646 | variantArtifacts { |
|
|
483 | 647 | variant('browser') { |
|
|
484 | 648 | slot('mainJs') { |
|
|
485 | 649 | fromVariant { |
|
|
486 | 650 | output('js') |
|
|
487 | 651 | } |
|
|
488 | 652 | } |
|
|
489 | 653 | } |
|
|
490 | 654 | } |
|
|
491 | 655 | """); |
|
|
492 | 656 | |
|
|
493 | 657 | assertBuildFails("isn't declared", "help"); |
|
|
494 | 658 | } |
|
|
495 | 659 | |
|
|
496 | 660 | @Test |
|
|
497 | 661 | void failsOnUnknownRoleReference() throws Exception { |
|
|
498 | 662 | writeSettings("variant-artifacts-missing-role"); |
|
|
499 | 663 | writeBuildFile(""" |
|
|
500 | 664 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
501 | 665 | |
|
|
502 | 666 | variants.layers.create('main') |
|
|
503 | 667 | variants.roles.create('main') |
|
|
504 | 668 | variants.variant('browser') { |
|
|
505 | 669 | role('main') { |
|
|
506 | 670 | layers('main') |
|
|
507 | 671 | } |
|
|
508 | 672 | } |
|
|
509 | 673 | |
|
|
510 | 674 | variantArtifacts { |
|
|
511 | 675 | variant('browser') { |
|
|
512 | 676 | slot('mainJs') { |
|
|
513 | 677 | fromRole('test') { |
|
|
514 | 678 | output('js') |
|
|
515 | 679 | } |
|
|
516 | 680 | } |
|
|
517 | 681 | } |
|
|
518 | 682 | } |
|
|
519 | 683 | """); |
|
|
520 | 684 | |
|
|
521 | 685 | assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help"); |
|
|
522 | 686 | } |
|
|
523 | 687 | |
|
|
524 | 688 | @Test |
|
|
525 | 689 | void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception { |
|
|
526 | 690 | writeSettings("variant-artifacts-missing-primary"); |
|
|
527 | 691 | writeBuildFile(""" |
|
|
528 | 692 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
529 | 693 | |
|
|
530 | 694 | variants.layers.create('main') |
|
|
531 | 695 | variants.roles.create('main') |
|
|
532 | 696 | variants.variant('browser') { |
|
|
533 | 697 | role('main') { |
|
|
534 | 698 | layers('main') |
|
|
535 | 699 | } |
|
|
536 | 700 | } |
|
|
537 | 701 | |
|
|
538 | 702 | variantSources.layer('main') { |
|
|
539 | 703 | sourceSet { |
|
|
540 | 704 | declareOutputs('types', 'js') |
|
|
541 | 705 | } |
|
|
542 | 706 | } |
|
|
543 | 707 | |
|
|
544 | 708 | variantArtifacts { |
|
|
545 | 709 | variant('browser') { |
|
|
546 | 710 | slot('typesPackage') { |
|
|
547 | 711 | fromVariant { |
|
|
548 | 712 | output('types') |
|
|
549 | 713 | } |
|
|
550 | 714 | } |
|
|
551 | 715 | slot('js') { |
|
|
552 | 716 | fromVariant { |
|
|
553 | 717 | output('js') |
|
|
554 | 718 | } |
|
|
555 | 719 | } |
|
|
556 | 720 | } |
|
|
557 | 721 | } |
|
|
558 | 722 | |
|
|
559 | 723 | tasks.register('probe') { |
|
|
560 | 724 | doLast { |
|
|
561 | 725 | variantArtifacts.whenAvailable { ctx -> |
|
|
562 | 726 | def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser') |
|
|
563 | 727 | ctx.findOutgoing(browser).get().primarySlot.get() |
|
|
564 | 728 | } |
|
|
565 | 729 | } |
|
|
566 | 730 | } |
|
|
567 | 731 | """); |
|
|
568 | 732 | |
|
|
569 | 733 | assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe"); |
|
|
570 | 734 | } |
|
|
571 | 735 | |
|
|
572 | 736 | @Test |
|
|
573 | 737 | void failsOnLayerReferenceOutsideVariantTopology() throws Exception { |
|
|
574 | 738 | writeSettings("variant-artifacts-layer-outside-topology"); |
|
|
575 | 739 | writeBuildFile(""" |
|
|
576 | 740 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
577 | 741 | |
|
|
578 | 742 | variants.layers.create('mainBase') |
|
|
579 | 743 | variants.layers.create('extra') |
|
|
580 | 744 | variants.roles.create('main') |
|
|
581 | 745 | variants.variant('browser') { |
|
|
582 | 746 | role('main') { |
|
|
583 | 747 | layers('mainBase') |
|
|
584 | 748 | } |
|
|
585 | 749 | } |
|
|
586 | 750 | |
|
|
587 | 751 | variantArtifacts { |
|
|
588 | 752 | variant('browser') { |
|
|
589 | 753 | slot('extraJs') { |
|
|
590 | 754 | fromLayer('extra') { |
|
|
591 | 755 | output('js') |
|
|
592 | 756 | } |
|
|
593 | 757 | } |
|
|
594 | 758 | } |
|
|
595 | 759 | } |
|
|
596 | 760 | """); |
|
|
597 | 761 | |
|
|
598 | 762 | assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help"); |
|
|
599 | 763 | } |
|
|
600 | 764 | |
|
|
601 | 765 | @Test |
|
|
602 | 766 | void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception { |
|
|
603 | 767 | writeFile("settings.gradle", """ |
|
|
604 | 768 | rootProject.name = 'variant-artifacts-resolution' |
|
|
605 | 769 | include 'producer', 'consumer' |
|
|
606 | 770 | """); |
|
|
607 | 771 | writeFile("producer/inputs/types.d.ts", "export type Foo = string\n"); |
|
|
608 | 772 | writeFile("producer/inputs/index.js", "export const foo = 'bar'\n"); |
|
|
609 | 773 | writeBuildFile(""" |
|
|
610 | 774 | import org.gradle.api.attributes.Attribute |
|
|
611 | 775 | |
|
|
612 | 776 | def variantAttr = Attribute.of('test.variant', String) |
|
|
613 | 777 | def slotAttr = Attribute.of('test.slot', String) |
|
|
614 | 778 | |
|
|
615 | 779 | subprojects { |
|
|
616 | 780 | apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin |
|
|
617 | 781 | } |
|
|
618 | 782 | |
|
|
619 | 783 | project(':producer') { |
|
|
620 | 784 | variants.layers.create('main') |
|
|
621 | 785 | variants.roles.create('main') |
|
|
622 | 786 | variants.variant('browser') { |
|
|
623 | 787 | role('main') { |
|
|
624 | 788 | layers('main') |
|
|
625 | 789 | } |
|
|
626 | 790 | } |
|
|
627 | 791 | |
|
|
628 | 792 | variantSources.layer('main') { |
|
|
629 | 793 | sourceSet { |
|
|
630 | 794 | declareOutputs('types', 'js') |
|
|
631 | 795 | registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts')) |
|
|
632 | 796 | registerOutput('js', layout.projectDirectory.file('inputs/index.js')) |
|
|
633 | 797 | } |
|
|
634 | 798 | } |
|
|
635 | 799 | |
|
|
636 | 800 | variantArtifacts { |
|
|
637 | 801 | variant('browser') { |
|
|
638 | 802 | primarySlot('typesPackage') { |
|
|
639 | 803 | fromVariant { |
|
|
640 | 804 | output('types') |
|
|
641 | 805 | } |
|
|
642 | 806 | } |
|
|
643 | 807 | slot('js') { |
|
|
644 | 808 | fromVariant { |
|
|
645 | 809 | output('js') |
|
|
646 | 810 | } |
|
|
647 | 811 | } |
|
|
648 | 812 | } |
|
|
649 | 813 | |
|
|
650 | 814 | whenOutgoingConfiguration { publication -> |
|
|
651 | 815 | publication.configuration { |
|
|
652 | 816 | attributes.attribute(variantAttr, publication.variant.name) |
|
|
653 | 817 | } |
|
|
654 | 818 | } |
|
|
655 | 819 | |
|
|
656 | 820 | whenOutgoingSlot { publication -> |
|
|
657 | 821 | publication.artifactAttributes { |
|
|
658 | 822 | attribute(slotAttr, publication.artifactSlot.slot.name) |
|
|
659 | 823 | } |
|
|
660 | 824 | } |
|
|
661 | 825 | } |
|
|
662 | 826 | |
|
|
663 | 827 | } |
|
|
664 | 828 | |
|
|
665 | 829 | project(':consumer') { |
|
|
666 | 830 | configurations { |
|
|
667 | 831 | compileView { |
|
|
668 | 832 | canBeResolved = true |
|
|
669 | 833 | canBeConsumed = false |
|
|
670 | 834 | canBeDeclared = true |
|
|
671 | 835 | attributes { |
|
|
672 | 836 | attribute(variantAttr, 'browser') |
|
|
673 | 837 | attribute(slotAttr, 'typesPackage') |
|
|
674 | 838 | } |
|
|
675 | 839 | } |
|
|
676 | 840 | } |
|
|
677 | 841 | |
|
|
678 | 842 | dependencies { |
|
|
679 | 843 | compileView project(':producer') |
|
|
680 | 844 | } |
|
|
681 | 845 | |
|
|
682 | 846 | tasks.register('probe') { |
|
|
683 | 847 | doLast { |
|
|
684 | 848 | def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',') |
|
|
685 | 849 | def jsFiles = configurations.compileView.incoming.artifactView { |
|
|
686 | 850 | attributes { |
|
|
687 | 851 | attribute(slotAttr, 'js') |
|
|
688 | 852 | } |
|
|
689 | 853 | }.files.files.collect { it.name }.sort().join(',') |
|
|
690 | 854 | |
|
|
691 | 855 | println('compileFiles=' + compileFiles) |
|
|
692 | 856 | println('jsFiles=' + jsFiles) |
|
|
693 | 857 | } |
|
|
694 | 858 | } |
|
|
695 | 859 | } |
|
|
696 | 860 | """); |
|
|
697 | 861 | |
|
|
698 | 862 | BuildResult result = runner(":consumer:probe").build(); |
|
|
699 | 863 | |
|
|
700 | 864 | assertTrue(result.getOutput().contains("compileFiles=typesPackage")); |
|
|
701 | 865 | assertTrue(result.getOutput().contains("jsFiles=js")); |
|
|
702 | 866 | } |
|
|
703 | 867 | } |
|
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now
