1#![deny(missing_docs)]
2use std::{
19 fs,
20 path::{Path, PathBuf},
21 time::Instant,
22};
23
24use anyhow::{Context, bail};
25mod archive;
26pub mod cli;
27pub mod config;
28pub mod lockfile;
29pub mod write;
30use anyhow::Result;
31use cli::Command;
32use config::Config;
33use lockfile::Lockfile;
34
35pub(crate) const NAME: &str = "rpmoci";
36
37fn load_config_and_lock_file(
38 config_file: impl AsRef<Path>,
39) -> Result<(Config, PathBuf, Result<Option<Lockfile>>)> {
40 let config_file = config_file.as_ref();
41 let contents = std::fs::read_to_string(config_file)
42 .context(format!("Failed to read `{}`", config_file.display()))?;
43 let cfg: Config = toml::from_str(&contents)?;
44 let mut lockfile_path = PathBuf::from(config_file);
45 lockfile_path.set_extension("lock");
46 Ok((cfg, lockfile_path.clone(), read_lockfile(&lockfile_path)))
47}
48
49fn read_lockfile(lockfile: impl AsRef<Path>) -> Result<Option<Lockfile>> {
50 match std::fs::read_to_string(lockfile) {
51 Ok(d) => Ok(Some(toml::from_str(&d).context("Invalid lockfile")?)),
52 Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
53 Err(e) => Err(e.into()),
54 }
55}
56
57pub fn main(command: Command) -> anyhow::Result<()> {
59 match command {
60 Command::Update {
61 manifest_path,
62 from_lockfile,
63 } => {
64 let (cfg, lockfile_path, existing_lockfile) = load_config_and_lock_file(manifest_path)?;
65
66 let lockfile = if let Ok(Some(lockfile)) = &existing_lockfile {
67 if lockfile.is_compatible_excluding_local_rpms(&cfg) && from_lockfile {
68 lockfile.resolve_from_previous(&cfg)?
69 } else {
70 if from_lockfile {
71 bail!(
72 "the lock file is not up-to-date. Use of --from-lockfile requires that the lock file is up-to-date"
73 );
74 }
75 Lockfile::resolve_from_config(&cfg)?
76 }
77 } else {
78 if from_lockfile {
79 bail!(
80 "the lock file is not up-to-date. Use of --from-lockfile requires that the lock file is up-to-date"
81 );
82 }
83 Lockfile::resolve_from_config(&cfg)?
84 };
85
86 lockfile.print_updates(existing_lockfile.unwrap_or_default().as_ref())?;
87 lockfile.write_to_file(lockfile_path)?;
88 }
89 Command::Build {
90 locked,
91 image,
92 tag,
93 vendor_dir,
94 manifest_path,
95 label,
96 } => {
97 let now = Instant::now();
98 let mut changed = false;
99 let (cfg, lockfile_path, existing_lockfile) = load_config_and_lock_file(manifest_path)?;
100 let lockfile = match (existing_lockfile, locked) {
101 (Ok(Some(lockfile)), true) => {
102 if !lockfile.is_compatible_excluding_local_rpms(&cfg) {
105 bail!(format!(
106 "the lock file {} needs to be updated but --locked was passed to prevent this",
107 lockfile_path.display()
108 ));
109 }
110 lockfile
111 }
112 (Ok(Some(lockfile)), false) => {
113 if lockfile.is_compatible_including_local_rpms(&cfg)? {
114 lockfile
116 } else {
117 changed = true;
119 write::ok(
120 "Generating",
121 format!(
122 "new lock file. The existing lock file {} is not up-to-date.",
123 lockfile_path.display()
124 ),
125 )?;
126 Lockfile::resolve_from_config(&cfg)?
127 }
128 }
129 (Err(err), false) => {
130 write::error(
131 "Warning",
132 format!(
133 "failed to parse existing lock file. Generating a new one. Error: {err}"
134 ),
135 )?;
136 err.chain()
137 .skip(1)
138 .for_each(|cause| eprintln!("caused by: {cause}"));
139 changed = true;
140 Lockfile::resolve_from_config(&cfg)?
141 }
142 (Err(err), true) => {
143 return Err(err.context(format!(
144 "failed to parse existing lock file {}",
145 lockfile_path.display()
146 )));
147 }
148 (Ok(None), true) => {
149 bail!(format!(
150 "the lock file {} is missing and needs to be generated but --locked was passed to prevent this",
151 lockfile_path.display()
152 ))
153 }
154 (Ok(None), false) => {
155 changed = true;
156 Lockfile::resolve_from_config(&cfg)?
157 }
158 };
159
160 if changed {
161 lockfile.write_to_file(lockfile_path)?;
162 }
163
164 lockfile.build(
165 &cfg,
166 &image,
167 &tag,
168 vendor_dir.as_deref(),
169 label.into_iter().collect(),
170 )?;
171 let elapsed_time = now.elapsed();
172 write::ok(
173 "Success",
174 format!(
175 "image '{}:{}' created in {:2}s",
176 image,
177 tag,
178 elapsed_time.as_secs_f32()
179 ),
180 )?;
181 }
182 Command::Vendor {
183 out_dir,
184 manifest_path,
185 } => {
186 fs::create_dir_all(&out_dir).context("Failed to create vendor directory")?;
187 let (cfg, _lockfile_path, existing_lockfile) =
188 load_config_and_lock_file(manifest_path)?;
189
190 if let Ok(Some(lockfile)) = existing_lockfile {
191 if lockfile.is_compatible_excluding_local_rpms(&cfg) {
192 lockfile.download_rpms(&cfg, &out_dir)?;
193 lockfile.check_gpg_keys(&out_dir)?;
194 } else {
195 bail!(
196 "Lockfile out of date. `vendor` can only be run with a compatible lockfile"
197 )
198 }
199 } else {
200 bail!(
201 "No valid lockfile found. `vendor` can only be run with a compatible lockfile"
202 )
203 }
204 }
205 }
206 Ok(())
207}