diff --git a/common/readme.md b/common/readme.md --- a/common/readme.md +++ b/common/readme.md @@ -3,7 +3,7 @@ ## NAME `gradle-common/common` — набор плагинов для моделирования вариантов сборки, -материализации source sets и интеграции этой модели с toolchain-адаптерами. +регистрации source sets и интеграции этой модели с toolchain-адаптерами. ## SYNOPSIS @@ -46,7 +46,7 @@ variantSources { Модуль состоит из трех логических частей: - `variants` — декларативная доменная модель сборки; -- `sources` — модель физически материализуемых source sets; +- `sources` — модель физически регистрируемых source sets; - `variantSources` — адаптер, который связывает первые две модели. Ниже раскрытие каждой части. @@ -77,14 +77,14 @@ outputs. Это уже "физический" уровень, к которому удобно привязывать задачи, ### variantSources -`variantSources` материализует source sets на основе `variants`, применяет +`variantSources` регистрирует source sets на основе `variants`, применяет конфигурацию layer-bindings и отдает события (`whenRegistered`, `whenBound`) для адаптеров других плагинов. Практический смысл: - переводить логическую модель `variants` в executable-модель `sources`; -- навешивать политики toolchain на materialized source sets; +- навешивать политики toolchain на зарегистрированные source sets; - синхронизировать плагины через replayable callback-контракт. ## DOMAIN MODEL @@ -92,10 +92,10 @@ outputs. Это уже "физический" уровень, к которому удобно привязывать задачи, - `BuildLayer` — глобальный идентификатор слоя. - `BuildVariant` — агрегат ролей, связей, атрибутов, артефактных слотов. - `BuildRole` — роль внутри варианта, содержит ссылки на layer names. -- `BuildLink` — ориентированная связь `from -> to` в графе определенного `kind`. -- `GenericSourceSet` — materialized набор исходников и outputs. -- `BuildLayerBinding` — правила materialization source set для конкретного layer. -- `SourceSetContext` — контекст callback-событий materialization. +- `LayerLink` — ориентированная связь `from -> to` в графе определенного `kind`. +- `GenericSourceSet` — зарегистрированный набор исходников и outputs. +- `BuildLayerBinding` — правила registration source set для конкретного layer. +- `SourceSetContext` — контекст callback-событий registration. ## EVENT CONTRACT @@ -117,7 +117,7 @@ Closure callbacks работают в delegate-first режиме (`@DelegatesTo`). Для - `BuildVariantsExtension` — корневой API модели вариантов. - `BuildVariant` — API ролей, links, attributes и artifact slots варианта. - `VariantsSourcesPlugin` — применяет `variants` + `sources` и запускает адаптер. -- `VariantSourcesExtension` — API bind/events materialization. +- `VariantSourcesExtension` — API bind/events registration. - `BuildLayerBinding` — слой-конкретный DSL для имени и конфигурации source set. - `SourceSetContext` — payload событий и sugar `configureSourceSet(...)`. diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildLink.java b/common/src/main/java/org/implab/gradle/common/sources/BuildLink.java deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/BuildLink.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.implab.gradle.common.sources; - -import javax.inject.Inject; - -import org.gradle.api.Named; -import org.gradle.api.provider.Property; - -/** - * Directed relation between two layers within a variant. - */ -public abstract class BuildLink implements Named { - private final String name; - - @Inject - public BuildLink(String name) { - this.name = name; - } - - @Override - public String getName() { - return name; - } - - public abstract Property getFrom(); - - public abstract Property getTo(); - - public abstract Property getKind(); - - public void from(String value) { - getFrom().set(value); - } - - public void to(String value) { - getTo().set(value); - } - - public void kind(String value) { - getKind().set(value); - } -} diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java b/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java +++ b/common/src/main/java/org/implab/gradle/common/sources/BuildRole.java @@ -5,6 +5,7 @@ import java.util.Objects; import javax.inject.Inject; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.Named; import org.gradle.api.provider.ListProperty; @@ -13,6 +14,7 @@ import org.gradle.api.provider.ListPrope */ public abstract class BuildRole implements Named { private final String name; + private boolean finalized; @Inject public BuildRole(String name) { @@ -30,6 +32,8 @@ public abstract class BuildRole implemen * Binds this role to one or more declared layers. */ public void layers(String layer, String... extra) { + ensureMutable("add role layers"); + var values = new ArrayList(1 + extra.length); values.add(Objects.requireNonNull(layer, "Layer name is required")); @@ -38,4 +42,18 @@ public abstract class BuildRole implemen getLayers().addAll(values); } + + void finalizeModel() { + if (finalized) + return; + + getLayers().finalizeValue(); + getLayers().disallowChanges(); + finalized = true; + } + + private void ensureMutable(String operation) { + if (finalized) + throw new InvalidUserDataException("Role '" + name + "' is finalized and cannot " + operation); + } } diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java b/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java +++ b/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java @@ -1,16 +1,18 @@ package org.implab.gradle.common.sources; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; -import java.util.regex.Pattern; import javax.inject.Inject; import org.implab.gradle.common.core.lang.Closures; import org.gradle.api.Action; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.Named; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Provider; @@ -20,17 +22,16 @@ import org.gradle.api.attributes.Attribu import groovy.lang.Closure; public abstract class BuildVariant implements Named { - private static final Pattern INVALID_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_.-]"); - private final String name; private final ObjectFactory objects; + private boolean finalized; /** * Variant aggregate parts. */ private final VariantAttributes attributes; private final LinkedHashMap roles = new LinkedHashMap<>(); - private final LinkedHashMap links = new LinkedHashMap<>(); + private final List links = new ArrayList<>(); private final LinkedHashMap artifactSlots = new LinkedHashMap<>(); @Inject @@ -53,6 +54,7 @@ public abstract class BuildVariant imple } public void attributes(Action action) { + ensureMutable("configure attributes"); action.execute(new AttributesSpec(attributes)); } @@ -61,10 +63,12 @@ public abstract class BuildVariant imple } public void attribute(Attribute key, T value) { + ensureMutable("set attributes"); attributes.attribute(key, value); } public void attributeProvider(Attribute key, Provider value) { + ensureMutable("set attributes"); attributes.attributeProvider(key, value); } @@ -73,6 +77,7 @@ public abstract class BuildVariant imple } public void roles(Action action) { + ensureMutable("configure roles"); action.execute(new RolesSpec()); } @@ -81,6 +86,7 @@ public abstract class BuildVariant imple } public BuildRole role(String name, Action configure) { + ensureMutable("configure roles"); var role = roles.computeIfAbsent(name, this::newRole); configure.execute(role); return role; @@ -99,11 +105,12 @@ public abstract class BuildVariant imple return roles.get(name); } - public Collection getLinks() { - return Collections.unmodifiableCollection(links.values()); + public Collection getLinks() { + return Collections.unmodifiableList(links); } public void links(Action action) { + ensureMutable("configure links"); action.execute(new LinksSpec()); } @@ -111,43 +118,19 @@ public abstract class BuildVariant imple links(Closures.action(configure)); } - public BuildLink link(String from, String to, String kind, Action configure) { - return link(defaultLinkName(from, to, kind), link -> { - link.from(from); - link.to(to); - link.kind(kind); - configure.execute(link); - }); - } - - public BuildLink link(String from, String to, String kind, Closure configure) { - return link(from, to, kind, Closures.action(configure)); - } - - public BuildLink link(String from, String to, String kind) { - return link(from, to, kind, it -> { - }); - } - - public BuildLink link(String name, Action configure) { - var link = links.computeIfAbsent(name, this::newLink); - configure.execute(link); + public LayerLink link(String from, String to, String kind) { + ensureMutable("add links"); + var link = new LayerLink(from, to, kind); + links.add(link); return link; } - public BuildLink link(String name, Closure configure) { - return link(name, Closures.action(configure)); - } - - public BuildLink getLinkByName(String name) { - return links.get(name); - } - public Collection getArtifactSlots() { return Collections.unmodifiableCollection(artifactSlots.values()); } public void artifactSlots(Action action) { + ensureMutable("configure artifact slots"); action.execute(new ArtifactSlotsSpec()); } @@ -161,6 +144,7 @@ public abstract class BuildVariant imple } public BuildArtifactSlot artifactSlot(String name, Action configure) { + ensureMutable("configure artifact slots"); var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot); configure.execute(slot); return slot; @@ -183,24 +167,28 @@ public abstract class BuildVariant imple return result; } + void finalizeModel() { + if (finalized) + return; + + for (var role : roles.values()) + role.finalizeModel(); + + attributes.finalizeModel(); + finalized = true; + } + private BuildRole newRole(String roleName) { return objects.newInstance(BuildRole.class, roleName); } - private BuildLink newLink(String linkName) { - return objects.newInstance(BuildLink.class, linkName); - } - private BuildArtifactSlot newArtifactSlot(String slotName) { return objects.newInstance(BuildArtifactSlot.class, slotName); } - private static String defaultLinkName(String from, String to, String kind) { - return "link_" + sanitize(from) + "__" + sanitize(to) + "__" + sanitize(kind); - } - - private static String sanitize(String value) { - return INVALID_NAME_CHAR.matcher(String.valueOf(value)).replaceAll("_"); + private void ensureMutable(String operation) { + if (finalized) + throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation); } public final class RolesSpec { @@ -226,25 +214,13 @@ public abstract class BuildVariant imple } public final class LinksSpec { - public BuildLink link(String from, String to, String kind, Action configure) { - return BuildVariant.this.link(from, to, kind, configure); - } - - public BuildLink link(String from, String to, String kind, Closure configure) { - return BuildVariant.this.link(from, to, kind, configure); - } - - public BuildLink link(String from, String to, String kind) { + public LayerLink link(String from, String to, String kind) { return BuildVariant.this.link(from, to, kind); } - public Collection getAll() { + public Collection getAll() { return BuildVariant.this.getLinks(); } - - public BuildLink getByName(String name) { - return BuildVariant.this.getLinkByName(name); - } } public final class ArtifactSlotsSpec { diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java b/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java +++ b/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java @@ -31,6 +31,18 @@ public abstract class BuildVariantsExten public BuildVariantsExtension(ObjectFactory objects) { layers = objects.domainObjectContainer(BuildLayer.class); variants = objects.domainObjectContainer(BuildVariant.class); + + layers.all(layer -> { + if (finalized) + throw new InvalidUserDataException( + "Variants model is finalized and cannot add layer '" + layer.getName() + "'"); + }); + + variants.all(variant -> { + if (finalized) + throw new InvalidUserDataException( + "Variants model is finalized and cannot add variant '" + variant.getName() + "'"); + }); } public NamedDomainObjectContainer getLayers() { @@ -42,6 +54,7 @@ public abstract class BuildVariantsExten } public void layers(Action> action) { + ensureMutable("configure layers"); action.execute(layers); } @@ -50,6 +63,7 @@ public abstract class BuildVariantsExten } public void variants(Action> action) { + ensureMutable("configure variants"); action.execute(variants); } @@ -58,6 +72,7 @@ public abstract class BuildVariantsExten } public BuildLayer layer(String name, Action configure) { + ensureMutable("configure layers"); var layer = layers.maybeCreate(name); configure.execute(layer); return layer; @@ -73,6 +88,7 @@ public abstract class BuildVariantsExten } public BuildVariant variant(String name, Action configure) { + ensureMutable("configure variants"); var variant = variants.maybeCreate(name); configure.execute(variant); return variant; @@ -126,6 +142,10 @@ public abstract class BuildVariantsExten return; validate(); + + for (var variant : variants) + variant.finalizeModel(); + finalized = true; var actions = new ArrayList<>(finalizedActions); @@ -187,24 +207,23 @@ public abstract class BuildVariantsExten var edgesByKind = new HashMap>>(); for (var link : variant.getLinks()) { - var from = normalize(link.getFrom().getOrNull()); - var to = normalize(link.getTo().getOrNull()); - var kind = normalize(link.getKind().getOrNull()); + var from = normalize(link.from()); + var to = normalize(link.to()); + var kind = normalize(link.kind()); if (from == null || to == null || kind == null) { - errors.add("Variant '" + variant.getName() + "' has incomplete link '" + link.getName() - + "' (from/to/kind are required)"); + errors.add("Variant '" + variant.getName() + "' has incomplete link (from/to/kind are required)"); continue; } if (!variantLayers.contains(from)) { - errors.add("Variant '" + variant.getName() + "' link '" + link.getName() + "' references unknown source layer '" + errors.add("Variant '" + variant.getName() + "' link references unknown source layer '" + from + "'"); continue; } if (!variantLayers.contains(to)) { - errors.add("Variant '" + variant.getName() + "' link '" + link.getName() + "' references unknown target layer '" + errors.add("Variant '" + variant.getName() + "' link references unknown target layer '" + to + "'"); continue; } @@ -268,4 +287,9 @@ public abstract class BuildVariantsExten private static boolean isBlank(String value) { return normalize(value) == null; } + + private void ensureMutable(String operation) { + if (finalized) + throw new InvalidUserDataException("Variants model is finalized and cannot " + operation); + } } diff --git a/common/src/main/java/org/implab/gradle/common/sources/LayerLink.java b/common/src/main/java/org/implab/gradle/common/sources/LayerLink.java new file mode 100644 --- /dev/null +++ b/common/src/main/java/org/implab/gradle/common/sources/LayerLink.java @@ -0,0 +1,7 @@ +package org.implab.gradle.common.sources; + +/** + * Directed relation between two layers within a variant. + */ +public record LayerLink(String from, String to, String kind) { +} diff --git a/common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java b/common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java --- a/common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java +++ b/common/src/main/java/org/implab/gradle/common/sources/SourcesPlugin.java @@ -19,6 +19,7 @@ public abstract class SourcesPlugin impl @Override public void apply(Project target) { + logger.debug("Registering '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath()); var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class); target.getExtensions().add(SOURCES_EXTENSION_NAME, sources); } @@ -27,10 +28,13 @@ public abstract class SourcesPlugin impl var extensions = target.getExtensions(); @SuppressWarnings("unchecked") - var extension = (NamedDomainObjectContainer)extensions.getByName(SOURCES_EXTENSION_NAME); + var extension = (NamedDomainObjectContainer) extensions.findByName(SOURCES_EXTENSION_NAME); - if (extension == null) + if (extension == null) { + logger.error("Sources extension '{}' isn't found on project '{}'", SOURCES_EXTENSION_NAME, target.getPath()); throw new GradleException("Sources extension isn't found"); + } + logger.debug("Resolved '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath()); return extension; } } diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantAttributes.java b/common/src/main/java/org/implab/gradle/common/sources/VariantAttributes.java --- a/common/src/main/java/org/implab/gradle/common/sources/VariantAttributes.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantAttributes.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.attributes.Attribute; import org.gradle.api.provider.ProviderFactory; import org.gradle.api.provider.Provider; @@ -14,16 +15,19 @@ import org.gradle.api.provider.Provider; public final class VariantAttributes { private final ProviderFactory providers; private final LinkedHashMap, Provider> values = new LinkedHashMap<>(); + private boolean finalized; VariantAttributes(ProviderFactory providers) { this.providers = providers; } public void attribute(Attribute key, T value) { + ensureMutable("set attribute '" + key.getName() + "'"); attributeProvider(key, providers.provider(() -> value)); } public void attributeProvider(Attribute key, Provider value) { + ensureMutable("set attribute provider '" + key.getName() + "'"); values.put(key, value); } @@ -43,4 +47,13 @@ public final class VariantAttributes { public Map, Provider> asMap() { return Collections.unmodifiableMap(values); } + + void finalizeModel() { + finalized = true; + } + + private void ensureMutable(String operation) { + if (finalized) + throw new InvalidUserDataException("Variant attributes are finalized and cannot " + operation); + } } diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java b/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java --- a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java @@ -17,15 +17,18 @@ import org.gradle.api.InvalidUserDataExc import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.model.ObjectFactory; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; import groovy.lang.Closure; import groovy.lang.DelegatesTo; /** - * Adapter extension that materializes source sets for variant/layer pairs. + * Adapter extension that registers source sets for variant/layer pairs. */ @NonNullByDefault public abstract class VariantSourcesExtension { + private static final Logger logger = Logging.getLogger(VariantSourcesExtension.class); private static final Pattern INVALID_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_.-]"); private static final Pattern SOURCE_SET_NAME_TOKEN = Pattern.compile("\\{([A-Za-z][A-Za-z0-9]*)\\}"); @@ -135,7 +138,25 @@ public abstract class VariantSourcesExte void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer sources) { validateBindings(variants); - layerUsages(variants).forEach(usage -> materializeLayerUsage(usage, sources)); + + var usages = layerUsages(variants).toList(); + var registeredBefore = registeredContexts.size(); + var boundBefore = boundContexts.size(); + + logger.debug( + "Starting variant source-set registration (variants={}, layers={}, bindings={}, usages={})", + variants.getVariants().size(), + variants.getLayers().size(), + bindings.size(), + usages.size()); + + usages.forEach(usage -> registerLayerUsage(usage, sources)); + + logger.debug( + "Completed variant source-set registration (newSourceSets={}, newBounds={}, totalSourceSets={})", + registeredContexts.size() - registeredBefore, + boundContexts.size() - boundBefore, + sourceSetsByName.size()); } private Stream layerUsages(BuildVariantsExtension variants) { @@ -148,7 +169,7 @@ public abstract class VariantSourcesExte normalize(layerName))))); } - private void materializeLayerUsage(LayerUsage usage, NamedDomainObjectContainer sources) { + private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer sources) { var resolvedBinding = bind(usage.layerName()); var sourceSetNamePattern = resolvedBinding.getSourceSetNamePattern(); sourceSetNamePattern.finalizeValueOnRead(); diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java b/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java --- a/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantsPlugin.java @@ -3,24 +3,36 @@ package org.implab.gradle.common.sources import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; /** * Registers {@code variants} extension for build-variant modeling. */ public abstract class VariantsPlugin implements Plugin { + private static final Logger logger = Logging.getLogger(VariantsPlugin.class); public static final String VARIANTS_EXTENSION_NAME = "variants"; @Override public void apply(Project target) { + logger.debug("Registering '{}' extension on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath()); var variants = target.getExtensions().create(VARIANTS_EXTENSION_NAME, BuildVariantsExtension.class); - target.afterEvaluate(project -> variants.finalizeModel()); + target.afterEvaluate(project -> { + logger.debug("Finalizing variants model on project '{}'", project.getPath()); + variants.finalizeModel(); + logger.debug("Variants model finalized on project '{}'", project.getPath()); + }); } public static BuildVariantsExtension getVariantsExtension(Project target) { var extension = target.getExtensions().findByType(BuildVariantsExtension.class); - if (extension == null) + if (extension == null) { + logger.error("Variants extension '{}' isn't found on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath()); throw new GradleException("Variants extension isn't found"); + } + + logger.debug("Resolved '{}' extension on project '{}'", VARIANTS_EXTENSION_NAME, target.getPath()); return extension; } diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java b/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java --- a/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantsSourcesPlugin.java @@ -2,15 +2,19 @@ package org.implab.gradle.common.sources import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; /** - * Binds variant layers to materialized source sets. + * Binds variant layers to registered source sets. */ public abstract class VariantsSourcesPlugin implements Plugin { + private static final Logger logger = Logging.getLogger(VariantsSourcesPlugin.class); public static final String VARIANT_SOURCES_EXTENSION_NAME = "variantSources"; @Override public void apply(Project target) { + logger.debug("Applying variant-sources plugin on project '{}'", target.getPath()); target.getPluginManager().apply(VariantsPlugin.class); target.getPluginManager().apply(SourcesPlugin.class); @@ -20,6 +24,10 @@ public abstract class VariantsSourcesPlu var variantSources = target.getExtensions() .create(VARIANT_SOURCES_EXTENSION_NAME, VariantSourcesExtension.class); - variants.whenFinalized(model -> variantSources.registerSourceSets(model, sources)); + variants.whenFinalized(model -> { + logger.debug("Registering source sets for variants on project '{}'", target.getPath()); + variantSources.registerSourceSets(model, sources); + logger.debug("Registered source sets on project '{}'", target.getPath()); + }); } } diff --git a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java b/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java +++ b/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java @@ -153,13 +153,10 @@ class VariantsPluginFunctionalTest { role('main') { layers('a', 'b') } - link('l1') { - from('a') - to('b') - } + link('a', 'b', null) } } - """, "has incomplete link 'l1'"); + """, "has incomplete link (from/to/kind are required)"); } @Test @@ -177,16 +174,8 @@ class VariantsPluginFunctionalTest { role('main') { layers('a', 'b') } - link('first') { - from('a') - to('b') - kind('ts:api') - } - link('second') { - from('a') - to('b') - kind('ts:api') - } + link('a', 'b', 'ts:api') + link('a', 'b', 'ts:api') } } """, "has duplicated link tuple (from='a', to='b', kind='ts:api')"); @@ -207,11 +196,7 @@ class VariantsPluginFunctionalTest { role('main') { layers('a') } - link('l1') { - from('missing') - to('a') - kind('ts:api') - } + link('missing', 'a', 'ts:api') } } """, "references unknown source layer 'missing'"); @@ -232,16 +217,52 @@ class VariantsPluginFunctionalTest { role('main') { layers('a') } - link('l1') { - from('a') - to('missing') - kind('ts:api') - } + link('a', 'missing', 'ts:api') } } """, "references unknown target layer 'missing'"); } + @Test + void failsOnLateLayerMutationAfterFinalize() throws Exception { + assertBuildFails(""" + plugins { + id 'org.implab.gradle-variants' + } + + variants { + layer('a') + variant('browser') { + role('main') { layers('a') } + } + } + + afterEvaluate { + variants.layer('late') + } + """, "Variants model is finalized and cannot configure layers"); + } + + @Test + void failsOnLateVariantMutationAfterFinalize() throws Exception { + assertBuildFails(""" + plugins { + id 'org.implab.gradle-variants' + } + + variants { + layer('a') + variant('browser') { + role('main') { layers('a') } + } + } + + afterEvaluate { + variants.getByName('browser').role('late') { layers('a') } + } + """, "Variant 'browser' is finalized and cannot configure roles"); + } + private GradleRunner runner(String... arguments) { return GradleRunner.create() .withProjectDir(testProjectDir.toFile()) diff --git a/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java b/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java +++ b/common/src/test/java/org/implab/gradle/common/sources/VariantsSourcesPluginFunctionalTest.java @@ -26,7 +26,7 @@ class VariantsSourcesPluginFunctionalTes Path testProjectDir; @Test - void materializesVariantSourceSetsAndFiresCallbacks() throws Exception { + void registersVariantSourceSetsAndFiresCallbacks() throws Exception { writeFile(SETTINGS_FILE, ROOT_NAME); writeFile(BUILD_FILE, """ plugins { @@ -194,7 +194,7 @@ class VariantsSourcesPluginFunctionalTes } @Test - void replaysLateBindingsAndCallbacksAfterMaterialization() throws Exception { + void replaysLateBindingsAndCallbacksAfterRegistration() throws Exception { writeFile(SETTINGS_FILE, ROOT_NAME); writeFile(BUILD_FILE, """ plugins { diff --git a/common/variant-sources-plugin.md b/common/variant-sources-plugin.md --- a/common/variant-sources-plugin.md +++ b/common/variant-sources-plugin.md @@ -40,13 +40,13 @@ variantSources { ## DESCRIPTION `VariantsSourcesPlugin` применяет `VariantsPlugin` и `SourcesPlugin`, затем -материализует source sets из модели `variants`. +регистрирует source sets из модели `variants`. -Точка запуска materialization: +Точка запуска registration: - `variants.whenFinalized(model -> registerSourceSets(...))` -### materialization +### registration Для каждой usage-связки `variant/role/layer` вычисляется имя source set, регистрируется `GenericSourceSet` (если он еще не существует), затем @@ -63,7 +63,7 @@ variantSources { ### sourceSetNamePattern -`sourceSetNamePattern` определяет naming policy materialized source set. +`sourceSetNamePattern` определяет naming policy зарегистрированного source set. Default: @@ -139,7 +139,7 @@ Sugar: ## NOTES -- `sourceSetNamePattern` фиксируется при первом чтении в materialization +- `sourceSetNamePattern` фиксируется при первом чтении в registration (`finalizeValueOnRead`). - Closure callbacks используют delegate-first. - Для вложенных closure лучше явный параметр (`ctx -> ...`). diff --git a/common/variants-plugin.md b/common/variants-plugin.md --- a/common/variants-plugin.md +++ b/common/variants-plugin.md @@ -88,6 +88,7 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в ## LIFECYCLE - `VariantsPlugin` вызывает `variants.finalizeModel()` на `afterEvaluate`. +- после `finalizeModel()` мутации модели запрещены. - `whenFinalized(...)` replayable. ## API @@ -117,7 +118,7 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в - `BuildVariant` — агрегатная модель варианта. - `BuildLayer` — модель слоя. - `BuildRole` — модель роли. -- `BuildLink` — модель направленной связи. +- `LayerLink` — модель направленной связи. - `BuildArtifactSlot` — модель артефактного слота. - `VariantAttributes` — typed wrapper для variant attributes.