2 use std
::path
::{Path, PathBuf}
;
3 use yaml_rust
::{Yaml, YamlEmitter, YamlLoader}
;
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.
8 static TO_EXPAND
: &[(&str, &str)] = &[
9 ("src/ci/github-actions", ".github/workflows"),
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";
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 #############################################################
23 # This file was automatically generated by the expand-yaml-anchors tool. The
24 # source file that generated this one is:
28 # Once you make changes to that file you need to run:
30 # ./x.py run src/tools/expand-yaml-anchors/
32 # The CI build will fail if the tool is not run after changes to this file.
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
)),
54 eprintln
!("usage: expand-yaml-anchors <source-dir> <dest-dir>");
55 std
::process
::exit(1);
59 Ok(App { mode, base }
)
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") {
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 {}",
79 Mode
::Check
=> format
!("{} is not up to date", self.path(&dest_path
)),
86 fn expand(&self, source
: &Path
, dest
: &Path
) -> Result
<(), Box
<dyn Error
>> {
87 let content
= std
::fs
::read_to_string(source
)
88 .with_context(|| format
!("failed to read {}", self.path(source
)))?
;
90 let mut buf
= HEADER_MESSAGE
.replace("{source}", &self.path(source
).to_string());
92 let documents
= YamlLoader
::load_from_str(&content
)
93 .with_context(|| format
!("failed to parse {}", self.path(source
)))?
;
94 for mut document
in documents
.into_iter() {
95 document
= yaml_merge_keys
::merge_keys(document
)
96 .with_context(|| format
!("failed to expand {}", self.path(source
)))?
;
97 document
= filter_document(document
);
99 YamlEmitter
::new(&mut buf
).dump(&document
).map_err(|err
| WithContext
{
100 context
: "failed to serialize the expanded yaml".into(),
101 source
: Box
::new(err
),
108 let old
= std
::fs
::read_to_string(dest
)
109 .with_context(|| format
!("failed to read {}", self.path(dest
)))?
;
111 return Err(Box
::new(StrError(format
!(
112 "{} and {} are different",
119 std
::fs
::write(dest
, buf
.as_bytes())
120 .with_context(|| format
!("failed to write to {}", self.path(dest
)))?
;
126 fn path
<'a
>(&self, path
: &'a Path
) -> impl std
::fmt
::Display
+ 'a
{
127 path
.strip_prefix(&self.base
).unwrap_or(path
).display()
131 fn filter_document(document
: Yaml
) -> Yaml
{
133 Yaml
::Hash(map
) => Yaml
::Hash(
136 if let Yaml
::String(string
) = &key { string != REMOVE_MAP_KEY }
else { true }
138 .map(|(key
, value
)| (filter_document(key
), filter_document(value
)))
141 Yaml
::Array(vec
) => {
142 Yaml
::Array(vec
.into_iter().map(|item
| filter_document(item
)).collect())
149 if let Err(err
) = App
::from_args().and_then(|app
| app
.run()) {
150 eprintln
!("error: {}", err
);
152 let mut source
= err
.as_ref() as &dyn Error
;
153 while let Some(err
) = source
.source() {
154 eprintln
!("caused by: {}", err
);
158 std
::process
::exit(1);
163 struct StrError(String
);
165 impl Error
for StrError {}
167 impl std
::fmt
::Display
for StrError
{
168 fn fmt(&self, f
: &mut std
::fmt
::Formatter
) -> std
::fmt
::Result
{
169 std
::fmt
::Display
::fmt(&self.0, f
)
176 source
: Box
<dyn Error
>,
179 impl std
::fmt
::Display
for WithContext
{
180 fn fmt(&self, f
: &mut std
::fmt
::Formatter
) -> std
::fmt
::Result
{
181 write
!(f
, "{}", self.context
)
185 impl Error
for WithContext
{
186 fn source(&self) -> Option
<&(dyn Error
+ '
static)> {
187 Some(self.source
.as_ref())
191 pub(crate) trait ResultExt
<T
> {
192 fn with_context
<F
: FnOnce() -> String
>(self, f
: F
) -> Result
<T
, Box
<dyn Error
>>;
195 impl<T
, E
: Into
<Box
<dyn Error
>>> ResultExt
<T
> for Result
<T
, E
> {
196 fn with_context
<F
: FnOnce() -> String
>(self, f
: F
) -> Result
<T
, Box
<dyn Error
>> {
199 Err(err
) => Err(WithContext { source: err.into(), context: f() }
.into()),