dockerfile_parser/
stage.rs1use std::fmt;
4use std::ops::Index;
5
6use crate::dockerfile_parser::{Dockerfile, Instruction};
7use crate::image::ImageRef;
8
9#[derive(Debug, Eq, PartialEq, Clone)]
11pub enum StageParent<'a> {
12 Image(&'a ImageRef),
14
15 Stage(usize),
17
18 Scratch
20}
21
22impl<'a> fmt::Display for StageParent<'a> {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 StageParent::Image(image) => image.fmt(f),
26 StageParent::Stage(index) => index.fmt(f),
27 StageParent::Scratch => write!(f, "scratch")
28 }
29 }
30}
31
32#[derive(Debug, Eq)]
46pub struct Stage<'a> {
47 pub index: usize,
49
50 pub name: Option<String>,
52
53 pub instructions: Vec<&'a Instruction>,
55
56 pub parent: StageParent<'a>,
60
61 pub root: StageParent<'a>
64}
65
66impl<'a> Ord for Stage<'a> {
67 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
68 self.index.cmp(&other.index)
69 }
70}
71
72impl<'a> PartialOrd for Stage<'a> {
73 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74 Some(self.cmp(&other))
75 }
76}
77
78impl<'a> PartialEq for Stage<'a> {
79 fn eq(&self, other: &Self) -> bool {
80 self.index == other.index
81 }
82}
83
84impl<'a> Stage<'a> {
85 pub fn arg_index(&self, name: &str) -> Option<usize> {
90 self.instructions
91 .iter()
92 .enumerate()
93 .find_map(|(i, ins)| match ins {
94 Instruction::Arg(a) => if a.name.content == name { Some(i) } else { None },
95 _ => None
96 })
97 }
98}
99
100#[derive(Debug)]
119pub struct Stages<'a> {
120 pub stages: Vec<Stage<'a>>
121}
122
123impl<'a> Stages<'a> {
124 pub fn new(dockerfile: &'a Dockerfile) -> Stages<'a> {
125 let mut stages = Stages { stages: vec![] };
129 let mut next_stage_index = 0;
130
131 for ins in &dockerfile.instructions {
132 if let Instruction::From(from) = ins {
133 let image_name = from.image.as_ref().to_ascii_lowercase();
134 let parent = if image_name == "scratch" {
135 StageParent::Scratch
136 } else if let Some(stage) = stages.get_by_name(&image_name) {
137 StageParent::Stage(stage.index)
138 } else {
139 StageParent::Image(&from.image_parsed)
140 };
141
142 let root = if let StageParent::Stage(parent_stage) = parent {
143 stages.stages[parent_stage].root.clone()
144 } else {
145 parent.clone()
146 };
147
148 stages.stages.push(Stage {
149 index: next_stage_index,
150 name: from.alias.as_ref().map(|a| a.as_ref().to_ascii_lowercase()),
151 instructions: vec![ins],
152 parent,
153 root
154 });
155
156 next_stage_index += 1;
157 } else if !stages.stages.is_empty() {
158 let len = stages.stages.len();
159 if let Some(stage) = stages.stages.get_mut(len - 1) {
160 stage.instructions.push(ins);
161 }
162 }
163 }
164
165 stages
166 }
167
168 pub fn get_by_name(&'a self, name: &str) -> Option<&'a Stage<'a>> {
170 self.stages.iter().find(|s| s.name == Some(name.to_ascii_lowercase()))
171 }
172
173 pub fn get(&'a self, s: &str) -> Option<&'a Stage<'a>> {
178 match s.parse::<usize>() {
179 Ok(index) => self.stages.get(index),
180 Err(_) => self.get_by_name(s)
181 }
182 }
183
184 pub fn iter(&self) -> std::slice::Iter<'_, Stage<'a>> {
186 self.stages.iter()
187 }
188}
189
190impl<'a> Index<usize> for Stages<'a> {
191 type Output = Stage<'a>;
192
193 fn index(&self, index: usize) -> &Self::Output {
194 &self.stages[index]
195 }
196}
197
198impl<'a> IntoIterator for Stages<'a> {
199 type Item = Stage<'a>;
200 type IntoIter = std::vec::IntoIter<Stage<'a>>;
201
202 fn into_iter(self) -> Self::IntoIter {
203 self.stages.into_iter()
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use indoc::indoc;
211
212 #[test]
213 fn test_stages() {
214 let dockerfile = Dockerfile::parse(indoc!(r#"
215 FROM alpine:3.12
216
217 FROM ubuntu:18.04 as build
218 RUN echo "hello world"
219
220 FROM build as build2
221 COPY /foo /bar
222 COPY /bar /baz
223
224 FROM build as build3
225 "#)).unwrap();
226
227 let stages = Stages::new(&dockerfile);
228 assert_eq!(stages.stages.len(), 4);
229 assert_eq!(stages[1], Stage {
230 index: 1,
231 name: Some("build".into()),
232 instructions: vec![&dockerfile.instructions[1], &dockerfile.instructions[2]],
233 parent: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
234 root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
235 });
236
237 assert_eq!(stages[2], Stage {
238 index: 2,
239 name: Some("build2".into()),
240 instructions: dockerfile.instructions[3..5].iter().collect(),
241 parent: StageParent::Stage(1),
242 root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
243 });
244
245 assert_eq!(stages[3], Stage {
246 index: 3,
247 name: Some("build3".into()),
248 instructions: vec![&dockerfile.instructions[6]],
249 parent: StageParent::Stage(2),
250 root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
251 });
252 }
253
254 #[test]
255 fn test_stages_get() {
256 let dockerfile = Dockerfile::parse(indoc!(r#"
257 FROM alpine:3.12
258
259 FROM ubuntu:18.04 as build
260
261 FROM build as build2
262 "#)).unwrap();
263
264 let stages = Stages::new(&dockerfile);
265 assert_eq!(stages.get("0").unwrap().index, 0);
266 assert_eq!(stages.get("1"), stages.get("build"));
267 assert_eq!(stages.get("2"), stages.get("build2"));
268 }
269}