Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add process based plugins to Smithy build #1672

Merged
merged 3 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
189 changes: 176 additions & 13 deletions docs/source-2.0/guides/building-models/build-config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ The configuration file accepts the following properties:
* - plugins
- ``map<string, object>``
- Defines the plugins to apply to the model when building every
projection. Plugins are a mapping of plugin names to an arbitrary
plugin configuration object.
projection. Plugins are a mapping of :ref:`plugin IDs <plugin-id>` to
plugin-specific configuration objects.
* - ignoreMissingPlugins
- ``bool``
- If a plugin can't be found, Smithy will by default fail the build. This
Expand Down Expand Up @@ -109,6 +109,9 @@ The following is an example ``smithy-build.json`` configuration:
"plugin-name": {
"plugin-config": "value"
},
"run::custom-artifact-name": {
"command": ["my-codegenerator", "--debug"]
},
"...": {}
}
}
Expand All @@ -121,6 +124,47 @@ The following is an example ``smithy-build.json`` configuration:
}
}

.. _plugin-id:

Plugin ID and artifact names
============================

A plugin ID defines a *plugin name* and *artifact name* in the form of
``plugin-name::artifact-name``.

* ``plugin-name`` is the name of the plugin Smithy finds and runs with the
plugin-specific configuration.
* ``artifact-name`` is the optional artifact name and directory where the
plugin writes artifacts. If no ``::artifact-name`` is specific,
the artifact name defaults to the plugin name. No two plugin IDs in a
single projection can use the same artifact name.

The following example shows that the :ref:`run-plugin` can be used in the
same projection multiple times using a custom artifact name.

.. code-block:: json

{
"version": "1.0",
"projections": {
"source": {
"plugins": {
"run::foo": {
"command": ["sh", "foo.sh"]
},
"run::baz": {
"command": ["baz", "-a", "A"]
}
}
}
}
}

The above example will generate source projection artifacts in the
"source/foo" and "source/baz" directories.

.. seealso:: :ref:`projection-artifacts`


.. _maven-configuration:

Expand Down Expand Up @@ -326,11 +370,12 @@ A projection accepts the following configuration:
* - plugins
- ``map<string, object>``
- Defines the plugins to apply to the model when building this
projection. Plugins are a mapping of plugin names to an arbitrary
plugin configuration object. smithy-build will attempt to resolve
plugin names using `Java SPI`_ to locate an instance of ``software.amazon.smithy.build.SmithyBuildPlugin``
that returns a matching name when calling ``getName``. smithy-build will
emit a warning when a plugin cannot be resolved.
projection. ``plugins`` is a mapping of a :ref:`plugin IDs <plugin-id>`
to plugin-specific configuration objects. smithy-build will attempt
to resolve plugin names using `Java SPI`_ to locate an instance of
``software.amazon.smithy.build.SmithyBuildPlugin`` that returns a
matching name when calling ``getName``. smithy-build will emit a
warning when a plugin cannot be resolved.


.. _projection-artifacts:
Expand All @@ -345,12 +390,9 @@ smithy-build will write artifacts for each projection inside of
* Build information about the projection build result, including the
configuration of the projection and the validation events encountered when
validating the projected model, are written to ``${outputDirectory}/${projectionName}/build-info/smithy-build-info.json``.
* All plugin artifacts are written to ``${outputDirectory}/${projectionName}/${pluginName}/${artifactName}``,
where ``${artifactName}`` is the name of an artifact contributed by an
instance of ``software.amazon.smithy.build.SmithyBuildPlugin``. The relative
path of each artifact is resolved against ``${outputDirectory}/${projectionName}/${pluginName}/``.
For example, given an artifact path of ``foo/baz.json``, the resolved path
would become ``${outputDirectory}/${projectionName}/${pluginName}/foo/baz.json``.
* All plugin artifacts are written to ``${outputDirectory}/${projectionName}/${artifactName}/${files...}``,
where ``${artifactName}`` is the artifact name of the :ref:`plugin ID <plugin-id>`,
and ``${files...}`` are the artifacts created by a plugin.


.. _transforms:
Expand Down Expand Up @@ -1595,6 +1637,18 @@ environment variable set to "hi", this file is equivalent to:
}
}

In addition to environment variables of the process, smithy-build.json
files have access to the following environment variables:

.. list-table::
:header-rows: 1
:widths: 25 75

* - Name
- Description
* - ``SMITHY_ROOT_DIR``
- The root directory of the build (e.g., where the Smithy CLI was invoked).


.. _plugins:

Expand Down Expand Up @@ -1665,6 +1719,115 @@ sources plugin. Lines that start with a number sign (#) are comments and are
ignored. A Smithy manifest file is stored in a JAR as ``META-INF/smithy/manifest``.
All model files referenced by the manifest are relative to ``META-INF/smithy/``.


.. _run-plugin:

run plugin
----------

The ``run`` plugin runs an external program during the build. This plugin is
useful when integrating Smithy's build process with Smithy implementations
that are not written in Java.

When invoking the process, the Smithy model of the projection is serialized
using the :ref:`JSON AST <json-ast>` and sent to the standard input of the
process.

.. important::

The ``run`` plugin requires a custom artifact name in its
:ref:`plugin ID <plugin-id>` (e.g., ``run::artifact-name``).

The ``run`` plugin supports the following properties:

.. list-table::
:header-rows: 1
:widths: 10 20 70

* - Property
- Type
- Description
* - command
- ``[string]``
- **REQUIRED** The name of the program to run, followed by an optional
list of arguments. If the command uses a relative path, Smithy will
first check if the command can be found relative to the current working
directory. Otherwise, the program must be available on the ``$PATH`` or
mtdowling marked this conversation as resolved.
Show resolved Hide resolved
use an absolute path. No arguments are sent other than the arguments
configured in the ``command`` setting.
* - env
- ``Map<String, String>``
- A map of environment variables to send to the process. The process
will inherit the environment variables of the containing process.
The values defined in ``env`` add new variables or overwrite
inherited variables.
* - sendPrelude
- ``boolean``
- Set to true to include prelude shapes when sending the Smithy model to
the standard input of the process. By default, the prelude is omitted.

Smithy will make the following environment variables available to the program:

.. list-table::
:header-rows: 1
:widths: 25 75

* - Name
- Description
* - ``SMITHY_ROOT_DIR``
- The root directory of the build (e.g., where the Smithy CLI was invoked).
* - ``SMITHY_PLUGIN_DIR``
- The working directory of the program. All files written by the program
should be relative to this directory.
* - ``SMITHY_PROJECTION_NAME``
- The projection name the program was called within (e.g., "source").
* - ``SMITHY_ARTIFACT_NAME``
- The :ref:`plugin ID <plugin-id>` artifact name.
* - ``SMITHY_INCLUDES_PRELUDE``
- Contains the value of ``sendPrelude`` in the form of ``true`` or
``false`` to tell the process if the prelude is included in the
serialized model.

The following example applies the ``run`` command with an artifact name
of ``custom-process``:

.. code-block:: json

{
"version": "1.0",
"projections": {
"source": {
"plugins": {
"run::hello": {
"command": ["hello.sh", "--arg", "arg-value"]
}
}
}
}
}

Assuming ``hello.sh`` is on the PATH and might look something like:

.. code-block:: bash

#!/bin/sh

# Command line arguments are provided.
echo "--arg: $2"

# Print out the provided environment variables.
echo "SMITHY_ROOT_DIR: ${SMITHY_ROOT_DIR}"
echo "SMITHY_PLUGIN_DIR: ${SMITHY_PLUGIN_DIR}"
echo "SMITHY_PROJECTION_NAME: ${SMITHY_PROJECTION_NAME}"
echo "SMITHY_ARTIFACT_NAME: ${SMITHY_ARTIFACT_NAME}"
echo "SMITHY_INCLUDES_PRELUDE: ${SMITHY_INCLUDES_PRELUDE}"

# Copy the model from stdin and write it to copy-model.json.
# The process is run in the appropriate working directory for the
# plugin ID's artifact name.
cat >> copy-model.json


.. _Java SPI: https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
.. _Apache Maven: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
.. _Maven Central: https://search.maven.org
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ public final class PluginContext implements ToSmithyBuilder<PluginContext> {
private final FileManifest fileManifest;
private final ClassLoader pluginClassLoader;
private final Set<Path> sources;
private final String artifactName;
private Model nonTraitsModel;

private PluginContext(Builder builder) {
model = SmithyBuilder.requiredState("model", builder.model);
fileManifest = SmithyBuilder.requiredState("fileManifest", builder.fileManifest);
artifactName = builder.artifactName;
projection = builder.projection;
projectionName = builder.projectionName;
originalModel = builder.originalModel;
Expand Down Expand Up @@ -88,6 +90,20 @@ public String getProjectionName() {
return projectionName;
}

/**
* Gets the plugin artifact name, if present.
*
* <p>An artifact name is given to a plugin by defining the plugin as "bar::foo", where "foo" is the artifact
* name and "bar" is the plugin name. An artifact name is useful for cases when a plugin is applied multiple times
* in a single projection. The artifact name changes the directory of where the plugin writes files. A plugin
* implementation should use the plugin name as the artifact name if no explicit artifact name is provided.
*
* @return Returns the optional artifact name.
*/
public Optional<String> getArtifactName() {
return Optional.ofNullable(artifactName);
}

/**
* Gets the model that was projected.
*
Expand Down Expand Up @@ -246,7 +262,8 @@ public Builder toBuilder() {
.settings(settings)
.fileManifest(fileManifest)
.pluginClassLoader(pluginClassLoader)
.sources(sources);
.sources(sources)
.artifactName(artifactName);
}

/**
Expand All @@ -262,6 +279,7 @@ public static final class Builder implements SmithyBuilder<PluginContext> {
private FileManifest fileManifest;
private ClassLoader pluginClassLoader;
private BuilderRef<Set<Path>> sources = BuilderRef.forOrderedSet();
private String artifactName;

private Builder() {}

Expand Down Expand Up @@ -364,5 +382,19 @@ public Builder sources(Collection<Path> sources) {
this.sources.get().addAll(sources);
return this;
}

/**
* Set a custom artifact name used to change the output directory of a plugin.
*
* <p>An artifact name is useful when running plugins like "run" or when running a plugin multiple times
* in a single projection.
*
* @param artifactName Custom artifact name to set.
* @return Returns the builder.
*/
public Builder artifactName(String artifactName) {
this.artifactName = artifactName;
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.build;

import java.util.Objects;

final class PluginId {
private final String pluginName;
private final String artifactName;

PluginId(String pluginName, String artifactName) {
this.pluginName = Objects.requireNonNull(pluginName);
this.artifactName = artifactName;
}

static PluginId from(String identifier) {
String pluginName = identifier;
String artifactName = null;

int separatorPosition = identifier.indexOf("::");
if (separatorPosition > -1) {
pluginName = identifier.substring(0, separatorPosition);
artifactName = identifier.substring(separatorPosition + 2);
}

return new PluginId(pluginName, artifactName);
}

String getPluginName() {
return pluginName;
}

String getArtifactName() {
return hasArtifactName() ? artifactName : pluginName;
}

boolean hasArtifactName() {
return artifactName != null;
}

@Override
public String toString() {
if (!hasArtifactName()) {
return pluginName;
} else {
return pluginName + "::" + artifactName;
}
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof PluginId)) {
return false;
} else {
PluginId pluginId = (PluginId) o;
return pluginName.equals(pluginId.pluginName) && Objects.equals(artifactName, pluginId.artifactName);
}
}

@Override
public int hashCode() {
return Objects.hash(pluginName, artifactName);
}
}
Loading