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