# HG changeset patch # User cin # Date 2026-04-21 18:55:26 # Node ID 780370baa54c1055b902596f328af69181b08840 # Parent a4138749793f886f24b03ba14a82a395c90f445c variants: move source set layout conventions out of model Keep GenericSourceSet convention-free and apply layout defaults from SourcesPlugin and VariantSourcesPlugin. Add compile unit layout convention and cover standalone and variant source set layouts in functional tests. diff --git a/.vscode/settings.json b/.vscode/settings.json --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "cSpell.words": [ "implab", "materializer", - "rawtypes" + "rawtypes", + "Replayable" ] } \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java b/variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/SourcesPlugin.java @@ -6,6 +6,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; +import org.implab.gradle.common.core.lang.FilePaths; import org.implab.gradle.variants.sources.GenericSourceSet; /** @@ -16,12 +17,27 @@ import org.implab.gradle.variants.source public abstract class SourcesPlugin implements Plugin { private static final Logger logger = Logging.getLogger(SourcesPlugin.class); - private static final String SOURCES_EXTENSION_NAME = "sources"; + public static final String SOURCES_EXTENSION_NAME = "sources"; + + private static final String SOURCES_DIRECTORY = "src"; + + private static final String OUTPUTS_DIRECTORY = "out"; @Override public void apply(Project target) { logger.debug("Registering '{}' extension on project '{}'", SOURCES_EXTENSION_NAME, target.getPath()); + + var layout = target.getLayout(); + var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class); + sources.configureEach(sourceSet -> { + sourceSet.getSourceSetDir() + .convention(layout.getProjectDirectory() + .dir(FilePaths.cat(SOURCES_DIRECTORY, sourceSet.getName()))); + sourceSet.getOutputsDir() + .convention(layout.getBuildDirectory() + .dir(FilePaths.cat(OUTPUTS_DIRECTORY, sourceSet.getName()))); + }); target.getExtensions().add(SOURCES_EXTENSION_NAME, sources); } @@ -32,7 +48,8 @@ public abstract class SourcesPlugin impl var extension = (NamedDomainObjectContainer) extensions.findByName(SOURCES_EXTENSION_NAME); if (extension == null) { - logger.error("Sources extension '{}' isn't found on project '{}'", SOURCES_EXTENSION_NAME, target.getPath()); + 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()); diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/VariantArtifactsPlugin.java @@ -21,6 +21,7 @@ import org.implab.gradle.variants.source import org.implab.gradle.variants.artifacts.OutgoingConfigurationSlotSpec; public abstract class VariantArtifactsPlugin implements Plugin { + public static final String VARIANT_ARTIFACTS_EXTENSION = "variantArtifacts"; @Override public void apply(Project target) { @@ -105,7 +106,7 @@ public abstract class VariantArtifactsPl }; - extensions.add(VariantArtifactsExtension.class, "variantArtifacts", variantArtifacts); + extensions.add(VariantArtifactsExtension.class, VARIANT_ARTIFACTS_EXTENSION, variantArtifacts); } diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java @@ -1,12 +1,9 @@ package org.implab.gradle.variants; import java.util.Objects; -import java.util.function.Predicate; import org.eclipse.jdt.annotation.NonNullByDefault; import org.gradle.api.Action; -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Named; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.implab.gradle.common.core.lang.Deferred; @@ -14,13 +11,13 @@ import org.implab.gradle.common.core.lan import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.core.VariantsExtension; -import org.implab.gradle.variants.core.VariantsView; import org.implab.gradle.variants.sources.CompileUnit; import org.implab.gradle.variants.sources.CompileUnitSourceSetSpec; import org.implab.gradle.variants.sources.CompileUnitsView; import org.implab.gradle.variants.sources.RoleProjectionsView; import org.implab.gradle.variants.sources.VariantSourcesContext; import org.implab.gradle.variants.sources.VariantSourcesExtension; +import org.implab.gradle.variants.sources.internal.CompileUnitLayoutConvention; import org.implab.gradle.variants.sources.internal.CompileUnitNamer; import org.implab.gradle.variants.sources.internal.DefaultCompileUnitNamingPolicy; import org.implab.gradle.variants.sources.internal.DefaultLateConfigurationPolicySpec; @@ -32,6 +29,10 @@ import org.implab.gradle.variants.source public abstract class VariantSourcesPlugin implements Plugin { public static final String VARIANT_SOURCES_EXTENSION = "variantSources"; + private static final String VARIANT_OUTPUTS_DIRECTORY = "variants"; + + private static final String VARIANT_SOURCES_DIRECTORY = "src"; + @Override public void apply(Project target) { var extensions = target.getExtensions(); @@ -40,7 +41,8 @@ public abstract class VariantSourcesPlug target.getPlugins().apply(VariantsPlugin.class); // Access the VariantsExtension to configure variant sources. var variantsExtension = extensions.getByType(VariantsExtension.class); - var objectFactory = target.getObjects(); + var objects = target.getObjects(); + var providers = target.getProviders(); var deferred = new Deferred(); @@ -53,25 +55,31 @@ public abstract class VariantSourcesPlug var roleProjections = RoleProjectionsView.of(variants); // create registries - var sourceSetRegistry = new SourceSetRegistry(objectFactory); + var sourceSetRegistry = new SourceSetRegistry(objects); lateConfigurationPolicy.finalizePolicy(); var sourceSetConfiguration = new SourceSetConfigurationRegistry(lateConfigurationPolicy::mode); // build compile unit namer var compileUnitNamer = CompileUnitNamer.builder() - .addUnits(compileUnits.getUnits()) - .nameCollisionPolicy(namingPolicy.policy()) - .build(); + .addUnits(compileUnits.getUnits()) + .nameCollisionPolicy(namingPolicy.policy()) + .build(); + + var compileUnitLayoutConvention = new CompileUnitLayoutConvention( + providers.provider(() -> target.getLayout().getProjectDirectory().dir(VARIANT_SOURCES_DIRECTORY)), + target.getLayout().getBuildDirectory().dir(VARIANT_OUTPUTS_DIRECTORY)); + + // apply layout convention to all compile unit source sets + sourceSetConfiguration.addAction(compileUnitLayoutConvention); // create the context var context = new DefaultVariantSourcesContext( - variants, - compileUnits, - roleProjections, - compileUnitNamer, - sourceSetRegistry, - sourceSetConfiguration - ); + variants, + compileUnits, + roleProjections, + compileUnitNamer, + sourceSetRegistry, + sourceSetConfiguration); deferred.resolve(context); }); @@ -98,7 +106,9 @@ public abstract class VariantSourcesPlug lateConfigurationPolicy.finalizePolicy(); - whenAvailable(ctx -> ctx.configureVariant(resolveVariant(ctx.getVariants(), variantName), action)); + var variant = objects.named(Variant.class, variantName); + + whenAvailable(ctx -> ctx.configureVariant(variant, action)); } @Override @@ -109,7 +119,9 @@ public abstract class VariantSourcesPlug lateConfigurationPolicy.finalizePolicy(); - whenAvailable(ctx -> ctx.configureLayer(resolveLayer(ctx.getVariants(), layerName), action)); + var layer = objects.named(Layer.class, layerName); + + whenAvailable(ctx -> ctx.configureLayer(layer, action)); } @Override @@ -120,7 +132,11 @@ public abstract class VariantSourcesPlug lateConfigurationPolicy.finalizePolicy(); - whenAvailable(ctx -> ctx.configureUnit(resolveCompileUnit(ctx, variantName, layerName), action)); + var variant = objects.named(Variant.class, variantName); + var layer = objects.named(Layer.class, layerName); + var unit = new CompileUnit(variant, layer); + + whenAvailable(ctx -> ctx.configureUnit(unit, action)); } @Override @@ -136,31 +152,4 @@ public abstract class VariantSourcesPlug extensions.add(VariantSourcesExtension.class, VARIANT_SOURCES_EXTENSION, variantSourcesExtension); } - - private static Layer resolveLayer(VariantsView variants, String name) { - return variants.getLayers().stream() - .filter(named(name)) - .findAny() - .orElseThrow(() -> new InvalidUserDataException("Layer '" + name + "' isn't declared")); - } - - private static Variant resolveVariant(VariantsView variants, String name) { - return variants.getVariants().stream() - .filter(named(name)) - .findAny() - .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't declared")); - } - - private static CompileUnit resolveCompileUnit(VariantSourcesContext ctx, String variantName, String layerName) { - return ctx.getCompileUnits().findUnit( - resolveVariant(ctx.getVariants(), variantName), - resolveLayer(ctx.getVariants(), layerName)) - .orElseThrow(() -> new InvalidUserDataException( - "The CompileUnit isn't declared for variant '" + variantName + "', layer '" + layerName + "'")); - } - - private static Predicate named(String name) { - return named -> named.getName().equals(name); - } - } diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java --- a/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java +++ b/variants/src/main/java/org/implab/gradle/variants/VariantsPlugin.java @@ -23,13 +23,15 @@ import org.implab.gradle.variants.intern * */ public abstract class VariantsPlugin implements Plugin { + public static final String VARIANTS_EXTENSION = "variants"; + @Override public void apply(Project target) { var objectFactory = target.getObjects(); var variantsExtension = new DefaultVariantsExtension(objectFactory); - target.getExtensions().add(VariantsExtension.class, "variants", variantsExtension); + target.getExtensions().add(VariantsExtension.class, VARIANTS_EXTENSION, variantsExtension); target.afterEvaluate(t -> variantsExtension.finalizeExtension()); } diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java b/variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/GenericSourceSet.java @@ -1,7 +1,6 @@ package org.implab.gradle.variants.sources; import java.io.File; -import java.nio.file.Paths; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -22,7 +21,6 @@ import org.gradle.api.Task; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; -import org.gradle.api.file.ProjectLayout; import org.gradle.api.file.SourceDirectorySet; import org.gradle.api.model.ObjectFactory; import org.gradle.api.tasks.TaskProvider; @@ -33,9 +31,10 @@ import org.gradle.api.tasks.TaskProvider *

* Each instance aggregates multiple {@link SourceDirectorySet source sets} * under a shared name and exposes typed outputs that must be declared up front. - * Default locations are {@code src/} for sources and - * {@code build/} for outputs, both of which can be customized via the - * exposed {@link DirectoryProperty} setters. + * Source and output base directories are explicit model properties. They do + * not define source directories or outputs by themselves; plugins and adapters + * may attach conventions or use them as layout hints for their own + * materialization rules. *

* *

@@ -62,7 +61,7 @@ public abstract class GenericSourceSet i private final Set declaredOutputs = new HashSet<>(); @Inject - public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) { + public GenericSourceSet(String name, ObjectFactory objects) { this.name = name; this.objects = objects; @@ -75,14 +74,6 @@ public abstract class GenericSourceSet i allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider()); allOutputs = objects.fileCollection().from(outputsProvider()); - - getSourceSetDir().convention(layout - .getProjectDirectory() - .dir(Paths.get("src", name).toString())); - - getOutputsDir().convention(layout - .getBuildDirectory() - .dir(name)); } @Override @@ -91,14 +82,24 @@ public abstract class GenericSourceSet i } /** - * Base directory for this source set. Defaults to {@code src/} under - * the project directory. + * Base directory associated with this source set. + * + *

+ * This property is intentionally convention-free in the base model. A + * plugin may attach a convention when it knows the surrounding layout + * policy, for example when materializing variant compile units. + *

*/ public abstract DirectoryProperty getSourceSetDir(); /** - * Base directory for outputs of this source set. Defaults to - * {@code build/}. + * Base directory associated with outputs of this source set. + * + *

+ * This property is a layout hint. Actual named outputs are declared and + * populated through {@link #declareOutputs(String, String...)} and + * {@link #registerOutput(String, Object...)}. + *

*/ public abstract DirectoryProperty getOutputsDir(); diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/CompileUnitLayoutConvention.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/CompileUnitLayoutConvention.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/CompileUnitLayoutConvention.java @@ -0,0 +1,45 @@ +package org.implab.gradle.variants.sources.internal; + +import org.gradle.api.Action; +import org.gradle.api.file.Directory; +import org.gradle.api.provider.Provider; +import org.implab.gradle.common.core.lang.FilePaths; +import org.implab.gradle.variants.sources.CompileUnitSourceSetSpec; + +/** + * Applies the default layout policy for source sets materialized from variant + * compile units. + * + *

+ * The convention keeps sources grouped by layer and outputs grouped by + * variant/layer. It only sets directory properties on the materialized source + * set; declaring source directories and named outputs remains the + * responsibility of adapters or user DSL. + *

+ */ +public class CompileUnitLayoutConvention implements Action { + private final Provider sourcesBaseDir; + private final Provider outputsBaseDir; + + public CompileUnitLayoutConvention( + Provider baseDirectory, + Provider outputsBaseDir) { + this.sourcesBaseDir = baseDirectory; + this.outputsBaseDir = outputsBaseDir; + } + + @Override + public void execute(CompileUnitSourceSetSpec spec) { + var sourceSet = spec.getSourceSet(); + var unit = spec.getCompileUnit(); + var layerName = unit.layer().getName(); + var variantName = unit.variant().getName(); + + sourceSet.getSourceSetDir() + .convention(sourcesBaseDir.map(base -> base.dir(FilePaths.cat(layerName)))); + + sourceSet.getOutputsDir() + .convention(outputsBaseDir.map(base -> base.dir(FilePaths.cat(variantName, layerName)))); + } + +} diff --git a/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java --- a/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java +++ b/variants/src/test/java/org/implab/gradle/variants/PluginMarkerFunctionalTest.java @@ -63,6 +63,8 @@ class PluginMarkerFunctionalTest extends doLast { def main = sources.named('main').get() println("sourceSet=" + main.name) + println("sourceSetDir=" + project.relativePath(main.sourceSetDir.get().asFile)) + println("outputsDir=" + project.relativePath(main.outputsDir.get().asFile)) println("jsFiles=" + main.output('js').files.collect { it.name }.sort().join(',')) } } @@ -71,6 +73,8 @@ class PluginMarkerFunctionalTest extends BuildResult result = runner("probe").build(); assertTrue(result.getOutput().contains("sourceSet=main")); + assertTrue(result.getOutput().contains("sourceSetDir=src/main")); + assertTrue(result.getOutput().contains("outputsDir=build/out/main")); assertTrue(result.getOutput().contains("jsFiles=main.js")); } diff --git a/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java --- a/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java +++ b/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java @@ -40,12 +40,15 @@ class VariantSourcesPluginFunctionalTest def left = ctx.sourceSets.getSourceSet(unit) def right = ctx.sourceSets.getSourceSet(unit) + def sourceSet = left.get() lines << "projectionUnits=" + ctx.roleProjections.getUnits(projection) .collect { it.layer().name } .sort() .join(',') - lines << "mainSourceSet=" + left.name + lines << "mainSourceSet=" + sourceSet.name + lines << "mainSourceSetDir=" + project.relativePath(sourceSet.sourceSetDir.get().asFile) + lines << "mainOutputsDir=" + project.relativePath(sourceSet.outputsDir.get().asFile) lines << "sameProvider=" + left.is(right) } @@ -67,6 +70,8 @@ class VariantSourcesPluginFunctionalTest assertTrue(result.getOutput().contains("units=browser:main,browser:test")); assertTrue(result.getOutput().contains("projectionUnits=main")); assertTrue(result.getOutput().contains("mainSourceSet=browserMain")); + assertTrue(result.getOutput().contains("mainSourceSetDir=src/main")); + assertTrue(result.getOutput().contains("mainOutputsDir=build/variants/browser/main")); assertTrue(result.getOutput().contains("sameProvider=true")); assertTrue(result.getOutput().contains("late:variants=browser")); } @@ -372,7 +377,7 @@ class VariantSourcesPluginFunctionalTest } """); - assertBuildFails("Variant 'missing' isn't declared", "help"); + assertBuildFails("The specified variant 'missing' isn't declared", "help"); } @Test @@ -423,7 +428,7 @@ class VariantSourcesPluginFunctionalTest } """); - assertBuildFails("Layer 'missing' isn't declared", "help"); + assertBuildFails("The specified layer 'missing' isn't declared", "help"); } @Test @@ -475,7 +480,7 @@ class VariantSourcesPluginFunctionalTest } """); - assertBuildFails("The CompileUnit isn't declared for variant 'browser', layer 'test'", "help"); + assertBuildFails("Compile unit for variant 'browser' and layer 'test' not found", "help"); } @Test