distro_info_binaries/
lib.rs

1use chrono::Datelike;
2use chrono::NaiveDate;
3use chrono::Utc;
4use clap::{App, Arg, ArgGroup, ArgMatches};
5use distro_info::Distro;
6use distro_info::{DistroInfo, DistroRelease};
7use failure::{bail, format_err, Error, ResultExt};
8
9pub const OUTDATED_MSG: &str = "Distribution data outdated.
10Please check for an update for distro-info-data. See /usr/share/doc/distro-info-data/README.Debian for details.";
11
12pub enum DaysMode {
13    Created,
14    Eol,
15    EolServer,
16    Release,
17}
18
19pub enum OutputMode {
20    Codename,
21    FullName,
22    Release,
23    Suppress,
24}
25
26/// Add arguments common to both ubuntu- and debian-distro-info to `app`
27pub fn add_common_args<'a>(app: App<'a, 'a>, additional_selectors: &'a [&str]) -> App<'a, 'a> {
28    let mut selectors = vec![
29        "all",
30        "devel",
31        "series",
32        "stable",
33        "supported",
34        "unsupported",
35    ];
36    selectors.extend(additional_selectors);
37    app.version("0.1.0")
38        .author("Daniel Watkins <daniel@daniel-watkins.co.uk>")
39        .arg(
40            Arg::with_name("all")
41                .short("a")
42                .long("all")
43                .help("list all known versions"),
44        )
45        .arg(
46            Arg::with_name("devel")
47                .short("d")
48                .long("devel")
49                .help("latest development version"),
50        )
51        .arg(
52            Arg::with_name("series")
53                .long("series")
54                .takes_value(true)
55                .help("series to calculate the version for"),
56        )
57        .arg(
58            Arg::with_name("stable")
59                .short("s")
60                .long("stable")
61                .help("latest stable version"),
62        )
63        .arg(
64            Arg::with_name("supported")
65                .long("supported")
66                .help("list of all supported stable versions"),
67        )
68        .arg(
69            Arg::with_name("unsupported")
70                .long("unsupported")
71                .help("list of all unsupported stable versions"),
72        )
73        .arg(
74            Arg::with_name("codename")
75                .short("c")
76                .long("codename")
77                .help("print the codename (default)"),
78        )
79        .arg(
80            Arg::with_name("fullname")
81                .short("f")
82                .long("fullname")
83                .help("print the full name"),
84        )
85        .arg(
86            Arg::with_name("release")
87                .short("r")
88                .long("release")
89                .help("print the release version"),
90        )
91        .arg(
92            Arg::with_name("date")
93                .long("date")
94                .takes_value(true)
95                .help("date for calculating the version (default: today)"),
96        )
97        .arg(
98            Arg::with_name("days")
99                .short("y")
100                .long("days")
101                .takes_value(true)
102                .default_value("release")
103                .possible_values(&["created", "eol", "eol-server", "release"])
104                .value_name("milestone")
105                .help("additionally, display days until milestone"),
106        )
107        .group(
108            ArgGroup::with_name("selector")
109                .args(&selectors)
110                .required(true),
111        )
112        .group(ArgGroup::with_name("output").args(&["codename", "fullname", "release"]))
113}
114
115pub fn common_run(matches: &ArgMatches, distro_info: &impl DistroInfo) -> Result<(), Error> {
116    let date = match matches.value_of("date") {
117        Some(date_str) => NaiveDate::parse_from_str(date_str, "%Y-%m-%d").context(format!(
118            "Failed to parse date '{}'; must be YYYY-MM-DD format",
119            date_str
120        ))?,
121        None => today(),
122    };
123    let distro_releases_iter = select_distro_releases(&matches, date, distro_info)?;
124    let days_mode = if matches.occurrences_of("days") == 0 {
125        None
126    } else {
127        matches.value_of("days").map(|value| match value {
128            "created" => DaysMode::Created,
129            "eol" => DaysMode::Eol,
130            "eol-server" => DaysMode::EolServer,
131            "release" => DaysMode::Release,
132            _ => panic!("unknown days mode found; please report a bug"),
133        })
134    };
135    let distro_name = distro_info.distro().to_string();
136    if matches.is_present("fullname") {
137        output(
138            distro_name,
139            distro_releases_iter,
140            &OutputMode::FullName,
141            &days_mode,
142            date,
143        )?;
144    } else if matches.is_present("release") {
145        output(
146            distro_name,
147            distro_releases_iter,
148            &OutputMode::Release,
149            &days_mode,
150            date,
151        )?;
152    } else if matches.is_present("codename") || days_mode.is_none() {
153        // This should be the default output _unless_ --days is specified
154        output(
155            distro_name,
156            distro_releases_iter,
157            &OutputMode::Codename,
158            &days_mode,
159            date,
160        )?;
161    } else {
162        output(
163            distro_name,
164            distro_releases_iter,
165            &OutputMode::Suppress,
166            &days_mode,
167            date,
168        )?;
169    }
170    Ok(())
171}
172
173fn determine_day_delta(current_date: NaiveDate, target_date: NaiveDate) -> i64 {
174    target_date.signed_duration_since(current_date).num_days()
175}
176
177pub fn output(
178    distro_name: &str,
179    distro_releases: Vec<&DistroRelease>,
180    output_mode: &OutputMode,
181    days_mode: &Option<DaysMode>,
182    date: NaiveDate,
183) -> Result<(), Error> {
184    if distro_releases.len() == 0 {
185        bail!(OUTDATED_MSG);
186    }
187    for distro_release in distro_releases {
188        let mut output_parts = vec![];
189        match output_mode {
190            OutputMode::Codename => output_parts.push(distro_release.series().to_string()),
191            OutputMode::Release => output_parts.push(
192                distro_release
193                    .version()
194                    .as_ref()
195                    .unwrap_or_else(|| distro_release.series())
196                    .to_string(),
197            ),
198            OutputMode::FullName => output_parts.push(format!(
199                "{} {} \"{}\"",
200                distro_name,
201                match distro_release.version() {
202                    Some(version) => version,
203                    None => "",
204                },
205                &distro_release.codename()
206            )),
207            OutputMode::Suppress => (),
208        }
209        let target_date = match days_mode {
210            Some(DaysMode::Created) => Some(distro_release.created().ok_or(format_err!(
211                "No creation date found for {}",
212                &distro_release.series()
213            ))?),
214            Some(DaysMode::Eol) => *distro_release.eol(),
215            Some(DaysMode::EolServer) => *distro_release.eol_server(),
216            Some(DaysMode::Release) => Some(distro_release.release().ok_or(format_err!(
217                "No release date found for {}",
218                &distro_release.series()
219            ))?),
220            None => None,
221        };
222        match target_date {
223            Some(target_date) => {
224                output_parts.push(format!("{}", determine_day_delta(date, target_date)));
225            }
226            None => match days_mode {
227                Some(DaysMode::EolServer) | Some(DaysMode::Eol) => {
228                    output_parts.push("(unknown)".to_string())
229                }
230                _ => (),
231            },
232        };
233        if !output_parts.is_empty() {
234            println!("{}", output_parts.join(" "));
235        }
236    }
237    Ok(())
238}
239
240pub fn select_distro_releases<'a>(
241    matches: &ArgMatches,
242    date: NaiveDate,
243    distro_info: &'a impl DistroInfo,
244) -> Result<Vec<&'a DistroRelease>, Error> {
245    Ok(if matches.is_present("all") {
246        distro_info.iter().collect()
247    } else if matches.is_present("supported") {
248        distro_info.supported(date)
249    } else if matches.is_present("unsupported") {
250        distro_info.unsupported(date)
251    } else if matches.is_present("devel") {
252        match distro_info.distro() {
253            Distro::Ubuntu => distro_info.ubuntu_devel(date),
254            Distro::Debian => distro_info.debian_devel(date),
255        }
256    } else if matches.is_present("testing") {
257        // d-d-i --testing selection matches u-d-i --devel
258        distro_info.ubuntu_devel(date)
259    } else if matches.is_present("latest") {
260        let devel_result = distro_info.ubuntu_devel(date);
261        if devel_result.len() > 0 {
262            vec![*devel_result.last().unwrap()]
263        } else {
264            distro_info
265                .latest(date)
266                .map(|distro_release| vec![distro_release])
267                .unwrap_or_else(|| vec![])
268        }
269    } else if matches.is_present("lts") {
270        let mut lts_releases = vec![];
271        for distro_release in distro_info.all_at(date) {
272            if distro_release.is_lts() {
273                lts_releases.push(distro_release);
274            }
275        }
276        match lts_releases.last() {
277            Some(release) => vec![*release],
278            None => bail!(OUTDATED_MSG),
279        }
280    } else if matches.is_present("stable") {
281        distro_info
282            .latest(date)
283            .map(|distro_release| vec![distro_release])
284            .unwrap_or_else(|| vec![])
285    } else if matches.is_present("series") {
286        match matches.value_of("series") {
287            Some(needle_series) => {
288                if !needle_series.chars().all(|c| c.is_lowercase()) {
289                    bail!("invalid distribution series `{}'", needle_series);
290                };
291                let candidates: Vec<&DistroRelease> = distro_info
292                    .iter()
293                    .filter(|distro_release| distro_release.series() == needle_series)
294                    .collect();
295                if candidates.is_empty() {
296                    bail!("unknown distribution series `{}'", needle_series);
297                };
298                Ok(candidates)
299            }
300            None => Err(format_err!(
301                "--series requires an argument; please report a bug about this \
302                 error"
303            )),
304        }?
305    } else {
306        panic!("clap prevent us from reaching here; report a bug if you see this")
307    })
308}
309
310fn today() -> NaiveDate {
311    let now = Utc::now();
312    NaiveDate::from_ymd(now.year(), now.month(), now.day())
313}