Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "feature",
"description": "Add the ability for smithy build plugins to declare that they must be run before or after other plugins. These dependencies are soft, so missing dependencies will be logged and ignored.",
"pull_requests": [
"[#2774](https://github.com/smithy-lang/smithy/pull/2774)"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "feature",
"description": "Add a generic dependency graph to smithy-utils to be used for sorting various dependent objects, such as integrations and plugins.",
"pull_requests": [
"[#2774](https://github.com/smithy-lang/smithy/pull/2774)"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.transform.ModelTransformer;
import software.amazon.smithy.model.validation.ValidatedResult;
import software.amazon.smithy.utils.DependencyGraph;
import software.amazon.smithy.utils.Pair;
import software.amazon.smithy.utils.SmithyBuilder;

Expand Down Expand Up @@ -214,7 +215,8 @@ void applyAllProjections(
private List<ResolvedPlugin> resolvePlugins(String projectionName, ProjectionConfig config) {
// Ensure that no two plugins use the same artifact name.
Set<String> seenArtifactNames = new HashSet<>();
List<ResolvedPlugin> resolvedPlugins = new ArrayList<>();
DependencyGraph<PluginId> dependencyGraph = new DependencyGraph<>();
Map<PluginId, ResolvedPlugin> resolvedPlugins = new HashMap<>();

for (Map.Entry<String, ObjectNode> pluginEntry : getCombinedPlugins(config).entrySet()) {
PluginId id = PluginId.from(pluginEntry.getKey());
Expand All @@ -224,12 +226,45 @@ private List<ResolvedPlugin> resolvePlugins(String projectionName, ProjectionCon
id.getArtifactName(),
projectionName));
}

createPlugin(projectionName, id).ifPresent(plugin -> {
resolvedPlugins.add(new ResolvedPlugin(id, plugin, pluginEntry.getValue()));
dependencyGraph.add(id);
for (String dependency : plugin.runAfter()) {
dependencyGraph.addDependency(id, PluginId.from(dependency));
}
for (String dependant : plugin.runBefore()) {
dependencyGraph.addDependency(PluginId.from(dependant), id);
}
resolvedPlugins.put(id, new ResolvedPlugin(id, plugin, pluginEntry.getValue()));
});
}

return resolvedPlugins;
List<PluginId> sorted;
try {
sorted = dependencyGraph.toSortedList();
} catch (IllegalStateException e) {
throw new SmithyBuildException(e.getMessage(), e);
}

List<ResolvedPlugin> result = new ArrayList<>();
for (PluginId id : sorted) {
ResolvedPlugin resolvedPlugin = resolvedPlugins.get(id);
if (resolvedPlugin != null) {
result.add(resolvedPlugin);
continue;
}

// If the plugin wasn't resolved, that's either because it was declared but not
// available on the classpath or not declared at all. In the former case we
// already have a log message that covers it, including a default build failure.
// If the plugin was seen, it was declared, and we don't need to log a second
// time.
if (!seenArtifactNames.contains(id.getArtifactName())) {
logMissingPluginDependency(dependencyGraph, id);
}
}

return result;
}

private Map<String, ObjectNode> getCombinedPlugins(ProjectionConfig projection) {
Expand Down Expand Up @@ -257,6 +292,33 @@ private Optional<SmithyBuildPlugin> createPlugin(String projectionName, PluginId
throw new SmithyBuildException(message);
}

private void logMissingPluginDependency(DependencyGraph<PluginId> dependencyGraph, PluginId name) {
StringBuilder message = new StringBuilder("Could not find plugin named '");
message.append(name).append('\'');
if (!dependencyGraph.getDirectDependants(name).isEmpty()) {
message.append(" that was supposed to run before plugins [");
message.append(dependencyGraph.getDirectDependants(name)
.stream()
.map(PluginId::toString)
.collect(Collectors.joining(", ")));
message.append("]");
}
if (!dependencyGraph.getDirectDependencies(name).isEmpty()) {
if (!dependencyGraph.getDirectDependants(name).isEmpty()) {
message.append(" and ");
} else {
message.append(" that ");
}
message.append("was supposed to run after plugins [");
message.append(dependencyGraph.getDirectDependencies(name)
.stream()
.map(PluginId::toString)
.collect(Collectors.joining(", ")));
message.append("]");
}
LOGGER.warning(message.toString());
}

private boolean areAnyResolvedPluginsSerial(List<ResolvedPlugin> resolvedPlugins) {
for (ResolvedPlugin plugin : resolvedPlugins) {
if (plugin.plugin.isSerial()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package software.amazon.smithy.build;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
Expand Down Expand Up @@ -104,4 +105,28 @@ static Function<String, Optional<SmithyBuildPlugin>> createServiceFactory() {
static Function<String, Optional<SmithyBuildPlugin>> createServiceFactory(ClassLoader classLoader) {
return createServiceFactory(ServiceLoader.load(SmithyBuildPlugin.class, classLoader));
}

/**
* Gets the names of plugins that this plugin must come before.
*
* <p>Dependencies are soft. Dependencies on plugin names that cannot be found
* log a warning and are ignored.
*
* @return Returns the plugin names this must come before.
*/
default List<String> runBefore() {
return Collections.emptyList();
}

/**
* Gets the name of the plugins that this plugin must come after.
*
* <p>Dependencies are soft. Dependencies on plugin names that cannot be found
* log a warning and are ignored.
*
* @return Returns the plugins names this must come after.
*/
default List<String> runAfter() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.build;

import java.util.List;
import software.amazon.smithy.utils.ListUtils;

public class CyclicPlugin1 implements SmithyBuildPlugin {
@Override
public String getName() {
return "cyclicplugin1";
}

@Override
public void execute(PluginContext context) {}

@Override
public List<String> runBefore() {
return ListUtils.of("cyclicplugin2");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.build;

import java.util.List;
import software.amazon.smithy.utils.ListUtils;

public class CyclicPlugin2 implements SmithyBuildPlugin {
@Override
public String getName() {
return "cyclicplugin2";
}

@Override
public void execute(PluginContext context) {}

@Override
public List<String> runBefore() {
return ListUtils.of("cyclicplugin1");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
Expand All @@ -23,6 +24,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
Expand Down Expand Up @@ -539,6 +541,92 @@ private void assertPluginPresent(String pluginName, String outputFileName, Proje
}
}

@Test
public void topologicallySortsPlugins() throws Exception {
Map<String, SmithyBuildPlugin> plugins = MapUtils.of(
"timestampPlugin1",
new TimestampPlugin1(),
"timestampPlugin2",
new TimestampPlugin2(),
"timestampPlugin3",
new TimestampPlugin3());
Function<String, Optional<SmithyBuildPlugin>> factory = SmithyBuildPlugin.createServiceFactory();
Function<String, Optional<SmithyBuildPlugin>> 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("topologically-sorts-plugins.json").toURI()))
.outputDirectory("/foo")
.build());

SmithyBuildResult results = builder.build();
ProjectionResult source = results.getProjectionResult("source").get();
MockManifest manifest1 = (MockManifest) source.getPluginManifest("timestampPlugin1").get();
MockManifest manifest2 = (MockManifest) source.getPluginManifest("timestampPlugin2").get();
MockManifest manifest3 = (MockManifest) source.getPluginManifest("timestampPlugin3").get();

Instant instant1 = Instant.ofEpochMilli(Long.parseLong(manifest1.getFileString("timestamp").get()));
Instant instant2 = Instant.ofEpochMilli(Long.parseLong(manifest2.getFileString("timestamp").get()));
Instant instant3 = Instant.ofEpochMilli(Long.parseLong(manifest3.getFileString("timestamp").get()));

assertTrue(instant2.isBefore(instant1));
assertTrue(instant3.isAfter(instant1));
}

@Test
public void dependenciesAreSoft() throws Exception {
Map<String, SmithyBuildPlugin> plugins = MapUtils.of(
"timestampPlugin2",
new TimestampPlugin2(),
"timestampPlugin3",
new TimestampPlugin3());
Function<String, Optional<SmithyBuildPlugin>> factory = SmithyBuildPlugin.createServiceFactory();
Function<String, Optional<SmithyBuildPlugin>> 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("soft-plugin-dependencies.json").toURI()))
.outputDirectory("/foo")
.build());

SmithyBuildResult results = builder.build();
ProjectionResult source = results.getProjectionResult("source").get();
MockManifest manifest2 = (MockManifest) source.getPluginManifest("timestampPlugin2").get();
MockManifest manifest3 = (MockManifest) source.getPluginManifest("timestampPlugin3").get();

assertTrue(manifest2.hasFile("timestamp"));
assertTrue(manifest3.hasFile("timestamp"));
}

@Test
public void detectsPluginCycles() throws Exception {
Map<String, SmithyBuildPlugin> plugins = MapUtils.of(
"cyclicplugin1",
new CyclicPlugin1(),
"cyclicplugin2",
new CyclicPlugin2());
Function<String, Optional<SmithyBuildPlugin>> factory = SmithyBuildPlugin.createServiceFactory();
Function<String, Optional<SmithyBuildPlugin>> 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("detects-plugin-cycles.json").toURI()))
.outputDirectory("/foo")
.build());

SmithyBuildException e = assertThrows(SmithyBuildException.class, builder::build);
assertThat(e.getMessage(), containsString("Cycle(s) detected in dependency graph"));
}

@Test
public void buildCanOverrideConfigOutputDirectory() throws Exception {
Path outputDirectory = Paths.get("/custom/foo");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.build;

import java.time.Instant;

public class TimestampPlugin1 implements SmithyBuildPlugin {
@Override
public String getName() {
return "timestampPlugin1";
}

@Override
public void execute(PluginContext context) {
context.getFileManifest().writeFile("timestamp", String.valueOf(Instant.now().toEpochMilli()));
try {
Thread.sleep(1);
} catch (InterruptedException ignored) {}
}

@Override
public boolean isSerial() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.build;

import java.time.Instant;
import java.util.List;
import software.amazon.smithy.utils.ListUtils;

public class TimestampPlugin2 implements SmithyBuildPlugin {
@Override
public String getName() {
return "timestampPlugin2";
}

@Override
public void execute(PluginContext context) {
context.getFileManifest().writeFile("timestamp", String.valueOf(Instant.now().toEpochMilli()));
try {
Thread.sleep(1);
} catch (InterruptedException ignored) {}
}

@Override
public List<String> runBefore() {
return ListUtils.of("timestampPlugin1");
}

@Override
public boolean isSerial() {
return true;
}
}
Loading
Loading