]> git.lizzy.rs Git - rust.git/blob - src/tools/expand-yaml-anchors/src/main.rs
Mark 'atomic_mut_ptr' methods const
[rust.git] / src / tools / expand-yaml-anchors / src / main.rs
1 use std::error::Error;
2 use std::path::{Path, PathBuf};
3 use yaml_rust::{Yaml, YamlEmitter, YamlLoader};
4
5 /// List of directories containing files to expand. The first tuple element is the source
6 /// directory, while the second tuple element is the destination directory.
7 #[rustfmt::skip]
8 static TO_EXPAND: &[(&str, &str)] = &[
9     ("src/ci/github-actions", ".github/workflows"),
10 ];
11
12 /// Name of a special key that will be removed from all the maps in expanded configuration files.
13 /// This key can then be used to contain shared anchors.
14 static REMOVE_MAP_KEY: &str = "x--expand-yaml-anchors--remove";
15
16 /// Message that will be included at the top of all the expanded files. {source} will be replaced
17 /// with the source filename relative to the base path.
18 static HEADER_MESSAGE: &str = "\
19 #############################################################
20 #   WARNING: automatically generated file, DO NOT CHANGE!   #
21 #############################################################
22
23 # This file was automatically generated by the expand-yaml-anchors tool. The
24 # source file that generated this one is:
25 #
26 #   {source}
27 #
28 # Once you make changes to that file you need to run:
29 #
30 #   ./x.py run src/tools/expand-yaml-anchors/
31 #
32 # The CI build will fail if the tool is not run after changes to this file.
33
34 ";
35
36 enum Mode {
37     Check,
38     Generate,
39 }
40
41 struct App {
42     mode: Mode,
43     base: PathBuf,
44 }
45
46 impl App {
47     fn from_args() -> Result<Self, Box<dyn Error>> {
48         // Parse CLI arguments
49         let args = std::env::args().skip(1).collect::<Vec<_>>();
50         let (mode, base) = match args.iter().map(|s| s.as_str()).collect::<Vec<_>>().as_slice() {
51             ["generate", ref base] => (Mode::Generate, PathBuf::from(base)),
52             ["check", ref base] => (Mode::Check, PathBuf::from(base)),
53             _ => {
54                 eprintln!("usage: expand-yaml-anchors <source-dir> <dest-dir>");
55                 std::process::exit(1);
56             }
57         };
58
59         Ok(App { mode, base })
60     }
61
62     fn run(&self) -> Result<(), Box<dyn Error>> {
63         for (source, dest) in TO_EXPAND {
64             let source = self.base.join(source);
65             let dest = self.base.join(dest);
66             for entry in std::fs::read_dir(&source)? {
67                 let path = entry?.path();
68                 if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("yml") {
69                     continue;
70                 }
71
72                 let dest_path = dest.join(path.file_name().unwrap());
73                 self.expand(&path, &dest_path).with_context(|| match self.mode {
74                     Mode::Generate => format!(
75                         "failed to expand {} into {}",
76                         self.path(&path),
77                         self.path(&dest_path)
78                     ),
79                     Mode::Check => format!(
80                         "{} is not up to date; please run \
81                         `x.py run src/tools/expand-yaml-anchors`.",
82                         self.path(&dest_path)
83                     ),
84                 })?;
85             }
86         }
87         Ok(())
88     }
89
90     fn expand(&self, source: &Path, dest: &Path) -> Result<(), Box<dyn Error>> {
91         let content = std::fs::read_to_string(source)
92             .with_context(|| format!("failed to read {}", self.path(source)))?;
93
94         let mut buf =
95             HEADER_MESSAGE.replace("{source}", &self.path(source).to_string().replace("\\", "/"));
96
97         let documents = YamlLoader::load_from_str(&content)
98             .with_context(|| format!("failed to parse {}", self.path(source)))?;
99         for mut document in documents.into_iter() {
100             document = yaml_merge_keys::merge_keys(document)
101                 .with_context(|| format!("failed to expand {}", self.path(source)))?;
102             document = filter_document(document);
103
104             YamlEmitter::new(&mut buf).dump(&document).map_err(|err| WithContext {
105                 context: "failed to serialize the expanded yaml".into(),
106                 source: Box::new(err),
107             })?;
108             buf.push('\n');
109         }
110
111         match self.mode {
112             Mode::Check => {
113                 let old = std::fs::read_to_string(dest)
114                     .with_context(|| format!("failed to read {}", self.path(dest)))?;
115                 if old != buf {
116                     return Err(Box::new(StrError(format!(
117                         "{} and {} are different",
118                         self.path(source),
119                         self.path(dest),
120                     ))));
121                 }
122             }
123             Mode::Generate => {
124                 std::fs::write(dest, buf.as_bytes())
125                     .with_context(|| format!("failed to write to {}", self.path(dest)))?;
126             }
127         }
128         Ok(())
129     }
130
131     fn path<'a>(&self, path: &'a Path) -> impl std::fmt::Display + 'a {
132         path.strip_prefix(&self.base).unwrap_or(path).display()
133     }
134 }
135
136 fn filter_document(document: Yaml) -> Yaml {
137     match document {
138         Yaml::Hash(map) => Yaml::Hash(
139             map.into_iter()
140                 .filter(|(key, _)| {
141                     if let Yaml::String(string) = &key { string != REMOVE_MAP_KEY } else { true }
142                 })
143                 .map(|(key, value)| (filter_document(key), filter_document(value)))
144                 .collect(),
145         ),
146         Yaml::Array(vec) => Yaml::Array(vec.into_iter().map(filter_document).collect()),
147         other => other,
148     }
149 }
150
151 fn main() {
152     if let Err(err) = App::from_args().and_then(|app| app.run()) {
153         eprintln!("error: {}", err);
154
155         let mut source = err.as_ref() as &dyn Error;
156         while let Some(err) = source.source() {
157             eprintln!("caused by: {}", err);
158             source = err;
159         }
160
161         std::process::exit(1);
162     }
163 }
164
165 #[derive(Debug)]
166 struct StrError(String);
167
168 impl Error for StrError {}
169
170 impl std::fmt::Display for StrError {
171     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172         std::fmt::Display::fmt(&self.0, f)
173     }
174 }
175
176 #[derive(Debug)]
177 struct WithContext {
178     context: String,
179     source: Box<dyn Error>,
180 }
181
182 impl std::fmt::Display for WithContext {
183     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184         write!(f, "{}", self.context)
185     }
186 }
187
188 impl Error for WithContext {
189     fn source(&self) -> Option<&(dyn Error + 'static)> {
190         Some(self.source.as_ref())
191     }
192 }
193
194 pub(crate) trait ResultExt<T> {
195     fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, Box<dyn Error>>;
196 }
197
198 impl<T, E: Into<Box<dyn Error>>> ResultExt<T> for Result<T, E> {
199     fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, Box<dyn Error>> {
200         match self {
201             Ok(ok) => Ok(ok),
202             Err(err) => Err(WithContext { source: err.into(), context: f() }.into()),
203         }
204     }
205 }