# HG changeset patch # User cin # Date 2026-03-05 15:07:15 # Node ID 2dd3356774b2de38c8c1ff9ab83b920e84d36b6d # Parent b379fb9b52c4001a73fd9afb5dc8cb8302c8d935 Removed layer links from variants diff --git a/common/readme.md b/common/readme.md --- a/common/readme.md +++ b/common/readme.md @@ -18,7 +18,6 @@ variants { variant('browser') { role('main') { layers('mainBase', 'mainAmd') } - link('mainBase', 'mainAmd', 'ts:api') } } @@ -54,13 +53,12 @@ variantSources { ### variants `variants` задает структуру пространства сборки: какие есть слои, какие роли -используют эти слои в каждом варианте, какие направленные связи между слоями -существуют. Модель не создает задачи и не привязана к TS/JS. +используют эти слои в каждом варианте, какие есть атрибуты и artifact slots. +Модель не создает задачи и не привязана к TS/JS. Практический смысл: - формализовать архитектуру сборки; -- централизовать валидацию связей; - дать адаптерам единый источник правды. ### sources @@ -90,9 +88,8 @@ outputs. Это уже "физический" уровень, к которому удобно привязывать задачи, ## DOMAIN MODEL - `BuildLayer` — глобальный идентификатор слоя. -- `BuildVariant` — агрегат ролей, связей, атрибутов, артефактных слотов. +- `BuildVariant` — агрегат ролей, атрибутов, артефактных слотов. - `BuildRole` — роль внутри варианта, содержит ссылки на layer names. -- `LayerLink` — ориентированная связь `from -> to` в графе определенного `kind`. - `GenericSourceSet` — зарегистрированный набор исходников и outputs. - `BuildLayerBinding` — правила registration source set для конкретного layer. - `SourceSetContext` — контекст callback-событий registration. @@ -115,7 +112,7 @@ Closure callbacks работают в delegate-first режиме (`@DelegatesTo`). Для - `GenericSourceSet` — модель источников/outputs для конкретного имени. - `VariantsPlugin` — регистрирует extension `variants` и lifecycle finalize. - `BuildVariantsExtension` — корневой API модели вариантов. -- `BuildVariant` — API ролей, links, attributes и artifact slots варианта. +- `BuildVariant` — API ролей, attributes и artifact slots варианта. - `VariantsSourcesPlugin` — применяет `variants` + `sources` и запускает адаптер. - `VariantSourcesExtension` — API bind/events registration. - `BuildLayerBinding` — слой-конкретный DSL для имени и конфигурации source set. diff --git a/common/src/main/java/org/implab/gradle/common/core/lang/Strings.java b/common/src/main/java/org/implab/gradle/common/core/lang/Strings.java --- a/common/src/main/java/org/implab/gradle/common/core/lang/Strings.java +++ b/common/src/main/java/org/implab/gradle/common/core/lang/Strings.java @@ -10,6 +10,8 @@ public class Strings { private static final Pattern firstLetter = Pattern.compile("^\\w"); + private static final Pattern INVALID_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_.-]"); + public static String capitalize(String string) { return string == null ? null : string.length() == 0 ? string @@ -43,6 +45,15 @@ public class Strings { throw new IllegalArgumentException(String.format("Argument %s can't be null or empty", argumentName)); } + public static void argumentNotNullOrBlank(String value, String argumentName) { + if (value == null || value.trim().length() == 0) + throw new IllegalArgumentException(String.format("Argument %s can't be null or blank", argumentName)); + } + + public static String sanitizeName(String value) { + return INVALID_NAME_CHAR.matcher(value).replaceAll("_"); + } + public static String asString(Object value) { if (value == null) return null; 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,10 +1,8 @@ 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.List; import java.util.Optional; import javax.inject.Inject; @@ -30,7 +28,6 @@ public abstract class BuildVariant imple */ private final VariantAttributes attributes; private final LinkedHashMap roles = new LinkedHashMap<>(); - private final List links = new ArrayList<>(); private final LinkedHashMap artifactSlots = new LinkedHashMap<>(); @Inject @@ -110,29 +107,6 @@ public abstract class BuildVariant imple "Variant '" + this.name + "' doesn't define role '" + name + "'")); } - public Collection getLinks() { - return Collections.unmodifiableList(links); - } - - public void links(Action action) { - ensureMutable("configure links"); - action.execute(new LinksSpec()); - } - - public void links(Closure configure) { - links(Closures.action(configure)); - } - - public LayerLink link(String from, String to, String kind) { - ensureMutable("add links"); - var link = new LayerLink( - requireLinkValue("from", from), - requireLinkValue("to", to), - requireLinkValue("kind", kind)); - links.add(link); - return link; - } - public Collection getArtifactSlots() { return Collections.unmodifiableCollection(artifactSlots.values()); } @@ -196,13 +170,6 @@ public abstract class BuildVariant imple throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation); } - private static String requireLinkValue(String field, String value) { - if (value == null || value.trim().isEmpty()) - throw new InvalidUserDataException("Link '" + field + "' must not be null or blank"); - - return value.trim(); - } - public final class RolesSpec { public BuildRole role(String name, Action configure) { return BuildVariant.this.role(name, configure); @@ -229,16 +196,6 @@ public abstract class BuildVariant imple } } - public final class LinksSpec { - public LayerLink link(String from, String to, String kind) { - return BuildVariant.this.link(from, to, kind); - } - - public Collection getAll() { - return BuildVariant.this.getLinks(); - } - } - public final class ArtifactSlotsSpec { public BuildArtifactSlot artifactSlot(String name, Action configure) { return BuildVariant.this.artifactSlot(name, configure); 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 @@ -3,14 +3,11 @@ package org.implab.gradle.common.sources import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import javax.inject.Inject; @@ -197,8 +194,7 @@ public abstract class BuildVariantsExten } validateRoleAndArtifactNames(variant, errors); - var variantLayers = validateRoleMappings(variant, layersByName, errors); - validateLinks(variant, variantLayers, errors); + validateRoleMappings(variant, layersByName, errors); } private static void validateRoleAndArtifactNames(BuildVariant variant, List errors) { @@ -227,10 +223,8 @@ public abstract class BuildVariantsExten } } - private static Set validateRoleMappings(BuildVariant variant, Map layersByName, + private static void validateRoleMappings(BuildVariant variant, Map layersByName, List errors) { - var variantLayers = new LinkedHashSet(); - for (var role : variant.getRoles()) { var seenLayers = new LinkedHashSet(); for (var layerName : role.getLayers().getOrElse(List.of())) { @@ -249,88 +243,9 @@ public abstract class BuildVariantsExten if (!seenLayers.add(normalizedLayerName)) { errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains duplicated layer reference '" + normalizedLayerName + "'"); - continue; } - - variantLayers.add(normalizedLayerName); } } - - return variantLayers; - } - - private static void validateLinks(BuildVariant variant, Set variantLayers, List errors) { - var seenLinks = new HashSet(); - var edgesByKind = new HashMap>>(); - - for (var link : variant.getLinks()) { - 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 (from/to/kind are required)"); - continue; - } - - if (!variantLayers.contains(from)) { - errors.add("Variant '" + variant.getName() + "' link references unknown source layer '" - + from + "'"); - continue; - } - - if (!variantLayers.contains(to)) { - errors.add("Variant '" + variant.getName() + "' link references unknown target layer '" - + to + "'"); - continue; - } - - var linkKey = from + "\u0000" + to + "\u0000" + kind; - if (!seenLinks.add(linkKey)) { - errors.add("Variant '" + variant.getName() + "' has duplicated link tuple (from='" + from - + "', to='" + to + "', kind='" + kind + "')"); - } - - edgesByKind - .computeIfAbsent(kind, x -> new LinkedHashMap<>()) - .computeIfAbsent(from, x -> new LinkedHashSet<>()) - .add(to); - } - - for (var entry : edgesByKind.entrySet()) { - if (hasCycle(variantLayers, entry.getValue())) { - errors.add("Variant '" + variant.getName() + "' contains cycle in links with kind '" + entry.getKey() + "'"); - } - } - } - - private static boolean hasCycle(Set nodes, Map> edges) { - var state = new HashMap(); - - for (var node : nodes) { - if (dfs(node, state, edges)) - return true; - } - - return false; - } - - private static boolean dfs(String node, Map state, Map> edges) { - var current = state.getOrDefault(node, 0); - if (current == 1) - return true; - if (current == 2) - return false; - - state.put(node, 1); - - for (var next : edges.getOrDefault(node, Set.of())) { - if (dfs(next, state, edges)) - return true; - } - - state.put(node, 2); - return false; } private static String normalize(String value) { @@ -341,10 +256,6 @@ public abstract class BuildVariantsExten return trimmed.isEmpty() ? null : trimmed; } - 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 deleted file mode 100644 --- a/common/src/main/java/org/implab/gradle/common/sources/LayerLink.java +++ /dev/null @@ -1,7 +0,0 @@ -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/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 @@ -12,6 +12,7 @@ import javax.inject.Inject; import org.implab.gradle.common.core.lang.Closures; import org.implab.gradle.common.core.lang.Strings; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.gradle.api.Action; import org.gradle.api.InvalidUserDataException; import org.gradle.api.NamedDomainObjectContainer; @@ -23,16 +24,16 @@ import org.gradle.api.logging.Logging; import groovy.lang.Closure; import groovy.lang.DelegatesTo; +import static org.implab.gradle.common.core.lang.Strings.sanitizeName; + /** * 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]*)\\}"); - private final ObjectFactory objects; private final NamedDomainObjectContainer bindings; private final List> registeredActions = new ArrayList<>(); private final List> boundActions = new ArrayList<>(); @@ -44,7 +45,6 @@ public abstract class VariantSourcesExte @Inject public VariantSourcesExtension(ObjectFactory objects) { - this.objects = objects; bindings = objects.domainObjectContainer(BuildLayerBinding.class); } @@ -62,7 +62,7 @@ public abstract class VariantSourcesExte } public BuildLayerBinding bind(String layer) { - return bindings.maybeCreate(normalize(layer)); + return bindings.maybeCreate(normalize(layer, "Layer name must not be null or blank")); } /** @@ -173,7 +173,7 @@ public abstract class VariantSourcesExte .map(layerName -> new LayerUsage( variant.getName(), role.getName(), - normalize(layerName))))); + normalize(layerName, "Layer name in variant '" + variant.getName() + "' and role '" + role.getName() + "' must not be null or blank"))))); } private void registerLayerUsage(LayerUsage usage, NamedDomainObjectContainer sources) { @@ -255,7 +255,7 @@ public abstract class VariantSourcesExte private static String sourceSetName(LayerUsage usage, String pattern) { var normalizedPattern = normalize(pattern, "sourceSetNamePattern must not be null or blank"); var resolved = resolveSourceSetNamePattern(normalizedPattern, usage); - var result = sanitize(resolved); + var result = sanitizeName(resolved); if (result.isEmpty()) throw new InvalidUserDataException("sourceSetNamePattern '" + pattern + "' resolved to empty source set name"); @@ -278,26 +278,18 @@ public abstract class VariantSourcesExte private static String tokenValue(String token, LayerUsage usage) { return switch (token) { - case "variant" -> sanitize(usage.variantName()); - case "variantCap" -> Strings.capitalize(sanitize(usage.variantName())); - case "role" -> sanitize(usage.roleName()); - case "roleCap" -> Strings.capitalize(sanitize(usage.roleName())); - case "layer" -> sanitize(usage.layerName()); - case "layerCap" -> Strings.capitalize(sanitize(usage.layerName())); + case "variant" -> sanitizeName(usage.variantName()); + case "variantCap" -> Strings.capitalize(sanitizeName(usage.variantName())); + case "role" -> sanitizeName(usage.roleName()); + case "roleCap" -> Strings.capitalize(sanitizeName(usage.roleName())); + case "layer" -> sanitizeName(usage.layerName()); + case "layerCap" -> Strings.capitalize(sanitizeName(usage.layerName())); default -> throw new InvalidUserDataException( "sourceSetNamePattern contains unsupported token '{" + token + "}'"); }; } - private static String sanitize(String value) { - return INVALID_NAME_CHAR.matcher(value).replaceAll("_"); - } - - private static String normalize(String value) { - return normalize(value, "Value must not be null or blank"); - } - - private static String normalize(String value, String errorMessage) { + private static String normalize(@Nullable String value, String errorMessage) { if (value == null) throw new InvalidUserDataException(errorMessage); var trimmed = value.trim(); 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 @@ -48,7 +48,6 @@ class VariantsPluginFunctionalTest { role('main') { layers('mainBase', 'mainAmd') } - link('mainBase', 'mainAmd', 'ts:api') artifactSlot('mainCompiled') } } @@ -58,7 +57,6 @@ class VariantsPluginFunctionalTest { def browser = variants.require('browser') println('attributes=' + browser.attributes.size()) println('roles=' + browser.roles.size()) - println('links=' + browser.links.size()) println('slots=' + browser.artifactSlots.size()) } } @@ -68,7 +66,6 @@ class VariantsPluginFunctionalTest { assertTrue(result.getOutput().contains("attributes=2")); assertTrue(result.getOutput().contains("roles=1")); - assertTrue(result.getOutput().contains("links=1")); assertTrue(result.getOutput().contains("slots=1")); assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS); } @@ -94,28 +91,6 @@ class VariantsPluginFunctionalTest { } @Test - void failsOnCycleInLinksByKind() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') - layer('b') - - variant('browser') { - role('main') { - layers('a', 'b') - } - link('a', 'b', 'ts:api') - link('b', 'a', 'ts:api') - } - } - """, "contains cycle in links with kind 'ts:api'"); - } - - @Test void allowsUsingLayerFromDifferentVariantRole() throws Exception { writeFile(SETTINGS_FILE, ROOT_NAME); writeFile(BUILD_FILE, """ @@ -139,91 +114,6 @@ class VariantsPluginFunctionalTest { } @Test - void failsOnIncompleteLink() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') - layer('b') - - variant('browser') { - role('main') { - layers('a', 'b') - } - link('a', 'b', null) - } - } - """, "Link 'kind' must not be null or blank"); - } - - @Test - void failsOnDuplicatedLinkTuple() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') - layer('b') - - variant('browser') { - role('main') { - layers('a', 'b') - } - link('a', 'b', 'ts:api') - link('a', 'b', 'ts:api') - } - } - """, "has duplicated link tuple (from='a', to='b', kind='ts:api')"); - } - - @Test - void failsOnUnknownSourceLayerInLink() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') { - } - - variant('browser') { - role('main') { - layers('a') - } - link('missing', 'a', 'ts:api') - } - } - """, "references unknown source layer 'missing'"); - } - - @Test - void failsOnUnknownTargetLayerInLink() throws Exception { - assertBuildFails(""" - plugins { - id 'org.implab.gradle-variants' - } - - variants { - layer('a') { - } - - variant('browser') { - role('main') { - layers('a') - } - link('a', 'missing', 'ts:api') - } - } - """, "references unknown target layer 'missing'"); - } - - @Test void failsOnDuplicatedLayerReferenceInRole() throws Exception { assertBuildFails(""" plugins { diff --git a/common/variants-plugin.md b/common/variants-plugin.md --- a/common/variants-plugin.md +++ b/common/variants-plugin.md @@ -25,7 +25,6 @@ variants { layers('mainBase', 'mainAmd') } - link('mainBase', 'mainAmd', 'ts:api') artifactSlot('mainCompiled') } } @@ -39,31 +38,18 @@ variants { ### layers Глобальные логические слои. Служат единым словарем имен, на которые затем -ссылаются роли и связи. +ссылаются роли. ### variants Именованные варианты исполнения/пакетирования (`browser`, `node`, и т.д.). -Вариант агрегирует роли, связи, атрибуты и artifact slots. +Вариант агрегирует роли, атрибуты и artifact slots. ### roles Роль описывает набор слоев в пределах варианта (`main`, `test`, `tools`). Одна роль может ссылаться на несколько слоев. -### links - -`link(from, to, kind)` — ориентированная связь между слоями внутри варианта. - -`kind` задает независимый тип графа (например `ts:api`, `bundle:runtime`). Это -позволяет вести несколько параллельных графов зависимостей над теми же слоями. - -Практические сценарии использования `link` в адаптерах: - -- расчет topological order по выбранному `kind`; -- wiring task inputs/outputs между слоями; -- проверка допустимости дополнительных pipeline-зависимостей. - ### attributes Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в @@ -80,10 +66,8 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в - роль не может ссылаться на неизвестный layer; - пустые имена layer запрещены; -- у link обязательны `from`, `to`, `kind`; -- `from`/`to` должны входить в слойную область варианта; -- tuple `(from, to, kind)` должен быть уникален; -- циклы в графе одного `kind` запрещены. +- имена ролей в варианте должны быть уникальны; +- имена artifact slots в варианте должны быть уникальны. ## LIFECYCLE @@ -108,7 +92,6 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в - `attributes { ... }` — атрибуты варианта (+ sugar `string/bool/integer`). - `role(...)`, `roles { ... }` — роли варианта. -- `link(...)`, `links { ... }` — связи слоев внутри варианта. - `artifactSlot(...)`, `artifactSlots { ... }` — артефактные слоты. ## KEY CLASSES @@ -118,7 +101,6 @@ Typed-атрибуты (`Attribute -> Provider`) для передачи параметров в - `BuildVariant` — агрегатная модель варианта. - `BuildLayer` — модель слоя. - `BuildRole` — модель роли. -- `LayerLink` — модель направленной связи. - `BuildArtifactSlot` — модель артефактного слота. - `VariantAttributes` — typed wrapper для variant attributes.