diff --git a/.changes/next-release/feature-aa81f6943bf8a339f4e4952ec9d2a403e6d99440.json b/.changes/next-release/feature-aa81f6943bf8a339f4e4952ec9d2a403e6d99440.json new file mode 100644 index 00000000000..1b2bd74cc60 --- /dev/null +++ b/.changes/next-release/feature-aa81f6943bf8a339f4e4952ec9d2a403e6d99440.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "description": "This adds a space for plugins to write data that is intended to be consumable by other plugins. This appears under a directory called `shared` in the the projection's output directory.", + "pull_requests": [ + "[#2764](https://github.com/awslabs/smithy/pull/2764)" + ] +} diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java b/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java index 0497197692e..d7e953f4299 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java @@ -34,6 +34,7 @@ public final class PluginContext implements ToSmithyBuilder { private final List events; private final ObjectNode settings; private final FileManifest fileManifest; + private final FileManifest sharedFileManifest; private final ClassLoader pluginClassLoader; private final Set sources; private final String artifactName; @@ -42,6 +43,7 @@ public final class PluginContext implements ToSmithyBuilder { private PluginContext(Builder builder) { model = SmithyBuilder.requiredState("model", builder.model); fileManifest = SmithyBuilder.requiredState("fileManifest", builder.fileManifest); + sharedFileManifest = builder.sharedFileManifest; artifactName = builder.artifactName; projection = builder.projection; projectionName = builder.projectionName; @@ -133,7 +135,13 @@ public ObjectNode getSettings() { * Gets the FileManifest used to create files in the projection. * *

All files written by a plugin should either be written using this - * manifest or added to the manifest via {@link FileManifest#addFile}. + * manifest, the shared manifest ({@link #getSharedFileManifest()}), or + * added to the manifest via {@link FileManifest#addFile}. + * + *

Files written to this manifest are specific to this plugin and cannot + * be read or modified by other plugins. To write files that should be + * shared with other plugins, use the shared manifest from + * {@link #getSharedFileManifest()}. * * @return Returns the file manifest. */ @@ -141,6 +149,35 @@ public FileManifest getFileManifest() { return fileManifest; } + /** + * Gets the FileManifest used to create files in the projection's shared + * file space. + * + *

All files written by a plugin should either be written using this + * manifest, the plugin's isolated manifest ({@link #getFileManifest()}), + * or added to the manifest via {@link FileManifest#addFile}. + * + *

Files written to this manifest may be read or modified by other + * plugins. Plugins SHOULD NOT write files to this manifest unless they + * specifically intend for them to be consumed by other plugins. Files + * that are not intended to be shared should be written to the manifest + * from {@link #getFileManifest()}. + * + * @return Returns the file manifest. + */ + public FileManifest getSharedFileManifest() { + // This will always be set in actual Smithy builds, as it is set by + // SmithyBuildImpl. We therefore don't want the return type to be + // optional, since in real usage it isn't. This was introduced after + // the class was made public, however, and this class is likely being + // manually constructed in tests. So instead of checking if it's set + // in the builder, we check when it's actually used. + if (sharedFileManifest == null) { + SmithyBuilder.requiredState("sharedFileManifest", sharedFileManifest); + } + return sharedFileManifest; + } + /** * Gets the ClassLoader that should be used in build plugins to load * services. @@ -248,6 +285,7 @@ public Builder toBuilder() { .events(events) .settings(settings) .fileManifest(fileManifest) + .sharedFileManifest(sharedFileManifest) .pluginClassLoader(pluginClassLoader) .sources(sources) .artifactName(artifactName); @@ -267,6 +305,7 @@ public static final class Builder implements SmithyBuilder { private List events = Collections.emptyList(); private ObjectNode settings = Node.objectNode(); private FileManifest fileManifest; + private FileManifest sharedFileManifest; private ClassLoader pluginClassLoader; private final BuilderRef> sources = BuilderRef.forOrderedSet(); private String artifactName; @@ -290,6 +329,18 @@ public Builder fileManifest(FileManifest fileManifest) { return this; } + /** + * Sets the required shared space {@link FileManifest} to use + * in the plugin. + * + * @param sharedFileManifest FileManifest to use for shared space. + * @return Returns the builder. + */ + public Builder sharedFileManifest(FileManifest sharedFileManifest) { + this.sharedFileManifest = sharedFileManifest; + return this; + } + /** * Sets the required model that is being built. * diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/ProjectionResult.java b/smithy-build/src/main/java/software/amazon/smithy/build/ProjectionResult.java index 18d0aa18d78..6b0ebcbd898 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/ProjectionResult.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/ProjectionResult.java @@ -21,6 +21,7 @@ public final class ProjectionResult { private final String projectionName; private final Model model; private final Map pluginManifests; + private final FileManifest sharedFileManifest; private final List events; private ProjectionResult(Builder builder) { @@ -28,6 +29,7 @@ private ProjectionResult(Builder builder) { this.model = SmithyBuilder.requiredState("model", builder.model); this.events = builder.events.copy(); this.pluginManifests = builder.pluginManifests.copy(); + this.sharedFileManifest = builder.sharedFileManifest; } /** @@ -102,6 +104,24 @@ public Optional getPluginManifest(String artifactName) { return Optional.ofNullable(pluginManifests.get(artifactName)); } + /** + * Gets the shared result manifest. + * + * @return Returns files created by plugins in shared space. + */ + public FileManifest getSharedManifest() { + // This will always be set in actual Smithy builds, as it is set by + // SmithyBuildImpl. We therefore don't want the return type to be + // optional, since in real usage it isn't. This was introduced after + // the class was made public, however, and this class is likely being + // manually constructed in tests. So instead of checking if it's set + // in the builder, we check when it's actually used. + if (sharedFileManifest == null) { + SmithyBuilder.requiredState("sharedFileManifest", sharedFileManifest); + } + return sharedFileManifest; + } + /** * Builds up a {@link ProjectionResult}. */ @@ -109,6 +129,7 @@ public static final class Builder implements SmithyBuilder { private String projectionName; private Model model; private final BuilderRef> pluginManifests = BuilderRef.forUnorderedMap(); + private FileManifest sharedFileManifest; private final BuilderRef> events = BuilderRef.forList(); @Override @@ -153,6 +174,17 @@ public Builder addPluginManifest(String artifactName, FileManifest manifest) { return this; } + /** + * Sets the shared result manifest. + * + * @param sharedFileManifest File manifest shared by all plugins. + * @return Returns the builder. + */ + public Builder sharedFileManifest(FileManifest sharedFileManifest) { + this.sharedFileManifest = sharedFileManifest; + return this; + } + /** * Adds validation events to the result. * diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java index 4df3995e3f1..e56d5325195 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java @@ -49,6 +49,8 @@ final class SmithyBuildImpl { private static final Pattern PLUGIN_PATTERN = Pattern .compile("^" + PATTERN_PART + "(::" + PATTERN_PART + ")?$"); + private static final String SHARED_MANIFEST_NAME = "shared"; + private final SmithyBuildConfig config; private final Function fileManifestFactory; private final Supplier modelAssemblerSupplier; @@ -357,12 +359,17 @@ private ProjectionResult applyProjection( LOGGER.fine(() -> String.format("No transforms to apply for projection %s", projectionName)); } + // Create the manifest where shared artifacts are stored. + Path sharedPluginDir = baseProjectionDir.resolve(SHARED_MANIFEST_NAME); + FileManifest sharedManifest = fileManifestFactory.apply(sharedPluginDir); + // Keep track of the first error created by plugins to fail the build after all plugins have run. Throwable firstPluginError = null; ProjectionResult.Builder resultBuilder = ProjectionResult.builder() .projectionName(projectionName) .model(projectedModel) - .events(modelResult.getValidationEvents()); + .events(modelResult.getValidationEvents()) + .sharedFileManifest(sharedManifest); for (ResolvedPlugin resolvedPlugin : resolvedPlugins) { if (pluginFilter.test(resolvedPlugin.id.getArtifactName())) { @@ -374,7 +381,8 @@ private ProjectionResult applyProjection( projectedModel, resolvedModel, modelResult, - resultBuilder); + resultBuilder, + sharedManifest); } catch (Throwable e) { if (firstPluginError == null) { firstPluginError = e; @@ -427,7 +435,8 @@ private void applyPlugin( Model projectedModel, Model resolvedModel, ValidatedResult modelResult, - ProjectionResult.Builder resultBuilder + ProjectionResult.Builder resultBuilder, + FileManifest sharedManifest ) { PluginId id = resolvedPlugin.id; @@ -449,6 +458,7 @@ private void applyPlugin( .events(modelResult.getValidationEvents()) .settings(resolvedPlugin.config) .fileManifest(manifest) + .sharedFileManifest(sharedManifest) .pluginClassLoader(pluginClassLoader) .sources(sources) .artifactName(id.hasArtifactName() ? id.getArtifactName() : null) diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java index b34481ec8e8..8842fe185c3 100644 --- a/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java +++ b/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java @@ -72,6 +72,7 @@ public void convertsToBuilder() { PluginContext context = PluginContext.builder() .projection("foo", ProjectionConfig.builder().build()) .fileManifest(new MockManifest()) + .sharedFileManifest(new MockManifest()) .model(Model.builder().build()) .originalModel(Model.builder().build()) .settings(Node.objectNode().withMember("foo", "bar")) @@ -83,6 +84,7 @@ public void convertsToBuilder() { assertThat(context.getModel(), equalTo(context2.getModel())); assertThat(context.getOriginalModel(), equalTo(context2.getOriginalModel())); assertThat(context.getFileManifest(), is(context2.getFileManifest())); + assertThat(context.getSharedFileManifest(), is(context2.getSharedFileManifest())); assertThat(context.getSources(), equalTo(context2.getSources())); assertThat(context.getEvents(), equalTo(context2.getEvents())); } diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java index f41d4665ae6..1c514266d7e 100644 --- a/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java +++ b/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java @@ -12,7 +12,9 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; @@ -459,6 +461,47 @@ public void appliesPlugins() throws Exception { assertTrue(b.getPluginManifest("test2").get().hasFile("hello2")); } + @Test + public void appliesPluginsWithSharedSpace() throws Exception { + Map plugins = MapUtils.of( + "testSharing1", + new TestSharingPlugin1(), + "testSharing2", + new TestSharingPlugin2()); + Function> factory = SmithyBuildPlugin.createServiceFactory(); + Function> composed = name -> OptionalUtils.or( + Optional.ofNullable(plugins.get(name)), + () -> factory.apply(name)); + + SmithyBuild builder = new SmithyBuild().pluginFactory(composed); + builder.fileManifestFactory(MockManifest::new); + builder.config(SmithyBuildConfig.builder() + .load(Paths.get(getClass().getResource("applies-plugins-with-shared-space.json").toURI())) + .outputDirectory("/foo") + .build()); + + SmithyBuildResult results = builder.build(); + ProjectionResult source = results.getProjectionResult("source").get(); + ProjectionResult projection = results.getProjectionResult("testProjection").get(); + + assertNotNull(source.getSharedManifest()); + assertNotNull(projection.getSharedManifest()); + + MockManifest sourceManifest = (MockManifest) source.getSharedManifest(); + MockManifest projectionManifest = (MockManifest) projection.getSharedManifest(); + + assertTrue(sourceManifest.hasFile("helloShare1")); + assertEquals("1", sourceManifest.getFileString("helloShare1").get()); + + assertTrue(projectionManifest.hasFile("helloShare1")); + assertEquals("2", projectionManifest.getFileString("helloShare1").get()); + + assertFalse(sourceManifest.hasFile("helloShare2")); + + assertTrue(projectionManifest.hasFile("helloShare2")); + assertEquals("2", projectionManifest.getFileString("helloShare2").get()); + } + @Test public void appliesSerialPlugins() throws Exception { Map plugins = new LinkedHashMap<>(); diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/TestSharingPlugin1.java b/smithy-build/src/test/java/software/amazon/smithy/build/TestSharingPlugin1.java new file mode 100644 index 00000000000..e1109011656 --- /dev/null +++ b/smithy-build/src/test/java/software/amazon/smithy/build/TestSharingPlugin1.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.build; + +public class TestSharingPlugin1 implements SmithyBuildPlugin { + @Override + public String getName() { + return "testSharing1"; + } + + @Override + public void execute(PluginContext context) { + FileManifest manifest = context.getSharedFileManifest(); + String count = String.valueOf(manifest.getFiles().size() + 1); + manifest.getFiles().forEach(file -> { + manifest.writeFile(file, count); + }); + manifest.writeFile("helloShare1", count); + } +} diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/TestSharingPlugin2.java b/smithy-build/src/test/java/software/amazon/smithy/build/TestSharingPlugin2.java new file mode 100644 index 00000000000..a90c4e1f18e --- /dev/null +++ b/smithy-build/src/test/java/software/amazon/smithy/build/TestSharingPlugin2.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.build; + +public class TestSharingPlugin2 implements SmithyBuildPlugin { + @Override + public String getName() { + return "testSharing2"; + } + + @Override + public void execute(PluginContext context) { + FileManifest manifest = context.getSharedFileManifest(); + String count = String.valueOf(manifest.getFiles().size() + 1); + manifest.getFiles().forEach(file -> { + manifest.writeFile(file, count); + }); + manifest.writeFile("helloShare2", count); + } +} diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/applies-plugins-with-shared-space.json b/smithy-build/src/test/resources/software/amazon/smithy/build/applies-plugins-with-shared-space.json new file mode 100644 index 00000000000..a131766a636 --- /dev/null +++ b/smithy-build/src/test/resources/software/amazon/smithy/build/applies-plugins-with-shared-space.json @@ -0,0 +1,13 @@ +{ + "version": "2.0", + "plugins": { + "testSharing1": {} + }, + "projections": { + "testProjection": { + "plugins": { + "testSharing2": {} + } + } + } +}