smithy_cargo/
lib.rs

1use std::ffi::{OsStr, OsString};
2use std::{env, io};
3use std::io::ErrorKind;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Output};
6use std::str::from_utf8;
7
8#[derive(Default)]
9pub struct SmithyBuild {
10    // Path to use as root for smithy build process
11    path: PathBuf,
12    // the output dir for the build command (default `$OUT_DIR/smithy`)
13    out_dir: PathBuf,
14    // projection to use for build (default: `source`)
15    projection: Option<OsString>,
16    // Plugin to use for build (default: none)
17    plugin: Option<OsString>,
18    // Additional Model files and directories to load.
19    models: Vec<PathBuf>,
20    // Path to smithy-build.json config
21    configs: Vec<PathBuf>,
22    // Disables config discover. Cannot be set if `configs` are provided.
23    no_config: bool,
24    // Force the use of ANSI colors in output. Default true.
25    force_color: bool,
26    // Determines if debug logging should be printed by smithy CLI. Uses the
27    // `CARGO_LOG` log level by default.
28    debug: bool,
29    // Automatically run `smithy format` on any discovered smithy source files. Default true.
30    format: bool,
31    // Suppress printing of build output. Default false.
32    quiet: bool,
33    // Ignore unknown traits when validating models.
34    allow_unknown_traits: bool,
35    // Environment variables to pass to Smithy build process.
36    env: Vec<(OsString, OsString)>,
37}
38
39impl SmithyBuild {
40    pub fn new() -> SmithyBuild {
41        let path = env::current_dir().unwrap();
42        let out_dir = path
43            .join(env::var("OUT_DIR").unwrap_or("target".into()))
44            .join(String::from("smithy"));
45        SmithyBuild {
46            path,
47            out_dir,
48            projection: None,
49            plugin: None,
50            models: vec![],
51            configs: vec![],
52            no_config: false,
53            debug: match env::var("CARGO_LOG") {
54                Ok(s) => s == "debug",
55                Err(_) => false,
56            },
57            force_color: true,
58            format: true,
59            quiet: false,
60            allow_unknown_traits: false,
61            env: vec![],
62        }
63    }
64
65    /// Set the relative path to use as the root for the Smithy build process
66    ///
67    /// The default path for executing the build process is the crate root dir.
68    pub fn path(mut self, path: impl AsRef<Path>) -> SmithyBuild {
69        self.path = env::current_dir().unwrap().join(path);
70        self
71    }
72
73    /// Sets the output directory for the Smithy Build.
74    ///
75    /// This is automatically set to `$OUT_DIR/smithy` or the setting in the `smithy-build.json` by default.
76    /// Most users will not need to change this.
77    pub fn out_dir<P: AsRef<Path>>(&mut self, out: P) -> &mut SmithyBuild {
78        self.out_dir = out.as_ref().to_path_buf();
79        self
80    }
81
82    /// Sets the projection to use for the Smithy build.
83    ///
84    /// If unset, the `source` projection will be used as the default.
85    pub fn projection<T: AsRef<OsStr>>(&mut self, projection: T) -> &mut SmithyBuild {
86        self.projection = Some(projection.as_ref().to_owned());
87        self
88    }
89
90    /// Sets a single plugin to build.
91    ///
92    /// If unset, the Smithy build process will build all plugins.
93    pub fn plugin<T: AsRef<OsStr>>(&mut self, plugin: T) -> &mut SmithyBuild {
94        self.projection = Some(plugin.as_ref().to_owned());
95        self
96    }
97
98    /// Adds a model file to build discovery path.
99    ///
100    /// By default, any models in the `model/` directory are discovered
101    pub fn model<T: AsRef<PathBuf>>(&mut self, model: T) -> &mut SmithyBuild {
102        self.models.push(model.as_ref().to_owned());
103        self
104    }
105
106    /// Add a smithy-build config
107    ///
108    /// If no configs are specified, the build will default to `smithy-build.json`
109    /// config at root of crate.
110    pub fn config<T: AsRef<PathBuf>>(&mut self, config: T) -> &mut SmithyBuild {
111        self.configs.push(config.as_ref().to_owned());
112        self
113    }
114
115    /// Disable config file detection and use.
116    pub fn no_config(&mut self) -> &mut Self {
117        self.no_config = true;
118        self
119    }
120
121    /// Enables debug printing in Smithy build output.
122    ///
123    /// By default, the `$CARGO_LOG` environment variable is scraped to determine
124    /// whether to set this or not.
125    pub fn debug(&mut self) -> &mut SmithyBuild {
126        self.debug = true;
127        self
128    }
129
130    /// Enable/Disable automatic formatting of smithy files.
131    pub fn format(&mut self) -> &mut SmithyBuild {
132        self.format = true;
133        self
134    }
135
136    /// Silence output except errors.
137    pub fn quiet(&mut self) -> &mut SmithyBuild {
138        self.quiet = true;
139        self
140    }
141
142    /// Ignore unknown traits when validating models.
143    pub fn allow_unknown_traits(&mut self) -> &mut SmithyBuild {
144        self.allow_unknown_traits = true;
145        self
146    }
147
148    /// Configure an environment variable for the Smithy build process.
149    pub fn env<K, V>(&mut self, key: K, value: V) -> &mut SmithyBuild
150    where
151        K: AsRef<OsStr>,
152        V: AsRef<OsStr>,
153    {
154        self.env
155            .push((key.as_ref().to_owned(), value.as_ref().to_owned()));
156        self
157    }
158
159    fn build_args(&self) -> Vec<OsString> {
160        let mut args = vec![OsString::from("build")];
161
162        // Set output directory
163        // TODO: Should this respect settings in smithy-build.json?
164        args.push("--output".into());
165        args.push(self.out_dir.as_os_str().into());
166
167        if let Some(p) = &self.projection {
168            args.push("--projection".into());
169            args.push(p.into());
170        }
171
172        if let Some(p) = &self.plugin {
173            args.push("--plugin".into());
174            args.push(p.into());
175        }
176
177        // Add configs
178        for config in &self.configs {
179            args.push("--config".into());
180            args.push(config.into());
181        }
182
183        // Flags
184        if self.no_config {
185            args.push("--no-config".into())
186        };
187        if self.allow_unknown_traits {
188            args.push("--aut".into())
189        };
190        if self.force_color {
191            args.push("--force-color".into());
192        }
193
194        args.append(&mut self.common_args());
195
196        args
197    }
198
199    fn common_args(&self) -> Vec<OsString> {
200        let mut args = vec![];
201        if self.debug {
202            args.push("--debug".into());
203        }
204        if self.quiet {
205            args.push("--quiet".into())
206        }
207
208        // Add models, starting with model/ default dir if it exists
209        if self.path.join("model").exists() {
210            println!("cargo:rerun-if-changed=model/");
211            args.push("model/".into());
212        }
213        for model in &self.models {
214            println!("cargo:rerun-if-changed={}", model.display());
215            args.push(model.into());
216        }
217        args
218    }
219
220    fn format_args(&self) -> Vec<OsString> {
221        let mut args = vec![OsString::from("build")];
222        // Add models, starting with model/ default dir if it exists
223        args.append(&mut self.common_args());
224        args
225    }
226
227    pub fn execute(&self) -> io::Result<Output> {
228        if self.format {
229            if !self.quiet {
230                println!("cargo:warning=\r   \x1b[32;1mFormatting\x1b[0m smithy models");
231            }
232            Command::new("smithy")
233                .current_dir(&self.path)
234                .args(self.format_args())
235                .envs(self.env.clone())
236                .output()
237                .expect("Failed to execute Smithy format");
238        }
239        let output = Command::new("smithy")
240            .current_dir(&self.path)
241            .args(self.build_args())
242            .envs(self.env.clone())
243            .output()
244            .expect("Failed to execute Smithy build");
245        if !self.quiet {
246            if let Ok(output_str) = from_utf8(output.stderr.as_slice()) {
247                let wrapped = output_str
248                    .split("\n")
249                    .flat_map(wrap)
250                    .collect::<Vec<&str>>()
251                    .join("\x0C\r\t");
252                println!("cargo:warning=\r   \x1b[32;1mBuilding\x1b[0m smithy models \r\x0C\r\t{}", wrapped);
253            }
254        }
255
256        if !output.status.success() {
257            return Err(io::Error::new(ErrorKind::Other, "Smithy build failed"));
258        }
259
260        // Set env var so it can be used to help include output in your code
261        println!(
262            "cargo:rustc-env=SMITHY_OUTPUT_DIR={}",
263            self.out_dir.display()
264        );
265
266        Ok(output)
267    }
268}
269
270// Wraps output at 80 character length so it looks somewhat nicer.
271fn wrap(line: &str) -> Vec<&str> {
272    line.split_at_checked(80)
273        .map_or_else(|| vec![line], |(a, b)| { let mut data = vec![a]; data.append(&mut wrap(b)); return data })
274}