cargo_prosa/package/
install.rs1use std::{
2 env, fmt, fs,
3 io::{self, Write as _},
4 path::{Path, PathBuf},
5};
6
7use clap::ArgMatches;
8use tera::Tera;
9use toml_edit::DocumentMut;
10
11use crate::cargo::CargoMetadata;
12
13#[cfg(target_os = "macos")]
14const ASSETS_LAUNCHD_J2: &str =
15 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/launchd.j2"));
16
17pub struct InstanceInstall {
19 name: String,
20 bin_name: String,
21 install_bin_dir: String,
22 install_config_dir: String,
23 install_service_dir: String,
24 ctx: tera::Context,
25 j2_service_asset: &'static str,
26}
27
28impl InstanceInstall {
29 pub fn new(args: &ArgMatches) -> io::Result<InstanceInstall> {
31 let current_path = env::current_dir()?;
32 let path = current_path.as_path();
33 let name = args
34 .get_one::<String>("name")
35 .map(|n| n.as_str())
36 .or(path.file_name().and_then(|p| p.to_str()))
37 .unwrap_or("prosa")
38 .to_lowercase()
39 .replace(['/', ' '], "_");
40
41 let (install_bin_dir, install_config_dir, install_service_dir) = if args.get_flag("system")
43 {
44 #[cfg(target_os = "linux")]
45 {
46 (
47 "/usr/local/bin".to_string(),
48 "/etc/prosa".to_string(),
49 "/etc/systemd/system".to_string(),
50 )
51 }
52 #[cfg(target_os = "macos")]
53 {
54 (
55 "/usr/local/bin".to_string(),
56 "/etc/prosa".to_string(),
57 "/Library/LaunchDaemons".to_string(),
58 )
59 }
60 } else {
61 let install_dir = env::var("HOME").map_err(|ve| {
62 io::Error::new(
63 io::ErrorKind::InvalidInput,
64 format!(
65 "Can't determine the $HOME folder where to install the ProSA {name}: {ve}"
66 ),
67 )
68 })?;
69
70 if install_dir.len() < 2 {
72 return Err(io::Error::new(
73 io::ErrorKind::InvalidInput,
74 format!("Can't install with an empty or root $HOME: {install_dir}"),
75 ));
76 }
77
78 #[cfg(target_os = "linux")]
79 {
80 (
81 format!("{install_dir}/.local/bin"),
82 format!("{install_dir}/.config/prosa"),
83 format!("{install_dir}/.config/systemd/user"),
84 )
85 }
86 #[cfg(target_os = "macos")]
87 {
88 (
89 format!("{install_dir}/.local/bin"),
90 format!("{install_dir}/.config/prosa"),
91 format!("{install_dir}/Library/LaunchAgents"),
92 )
93 }
94 };
95
96 let package_metadata = CargoMetadata::load_package_metadata()?;
97 let mut ctx = tera::Context::new();
98 package_metadata.j2_context(&mut ctx);
99 ctx.insert("name", &name);
100
101 let bin_name = package_metadata
102 .get_targets("bin")
103 .and_then(|b| b.first().cloned())
104 .ok_or(io::Error::new(
105 io::ErrorKind::InvalidInput,
106 "Can't find the ProSA binary from the project",
107 ))?;
108 ctx.insert("bin", &format!("{install_bin_dir}/{bin_name}"));
109 ctx.insert(
110 "config",
111 &format!("{install_config_dir}/{}/prosa.toml", name),
112 );
113
114 if !ctx.contains_key("description") {
116 ctx.insert("description", "Local ProSA instance");
117 }
118
119 Ok(InstanceInstall {
120 name,
121 bin_name,
122 install_bin_dir,
123 install_config_dir,
124 install_service_dir,
125 ctx,
126 #[cfg(target_os = "linux")]
127 j2_service_asset: super::ASSETS_SYSTEMD_J2,
128 #[cfg(target_os = "macos")]
129 j2_service_asset: ASSETS_LAUNCHD_J2,
130 })
131 }
132
133 fn get_install_bin_dir(&self) -> PathBuf {
134 PathBuf::from(self.install_bin_dir.clone())
135 }
136
137 fn get_install_config_path(&self) -> PathBuf {
138 PathBuf::from(self.install_config_dir.clone())
139 }
140
141 fn get_install_service_path(&self) -> PathBuf {
142 PathBuf::from(self.install_service_dir.clone())
143 }
144
145 #[cfg(target_os = "linux")]
146 fn get_service_filename(&self) -> String {
147 format!("{}.service", self.name)
148 }
149
150 #[cfg(target_os = "macos")]
151 fn get_service_filename(&self) -> String {
152 format!("com.prosa.{}.plist", self.name)
153 }
154
155 fn create_service_file(&self) -> tera::Result<()> {
156 let service_name = self.get_service_filename();
157 let service_path = self.get_install_service_path();
158 let service_file_path = service_path.join(&service_name);
159
160 let mut tera_build = Tera::default();
161 tera_build.add_raw_template(&service_name, self.j2_service_asset)?;
162
163 fs::create_dir_all(&service_path)?;
164 let service_file = fs::File::create(service_file_path)?;
165 tera_build.render_to(&service_name, &self.ctx, service_file)
166 }
167
168 fn copy_binary(&self, release: bool) -> io::Result<u64> {
169 let binary_path = if release {
170 format!("target/release/{}", self.bin_name)
171 } else {
172 format!("target/debug/{}", self.bin_name)
173 };
174
175 match fs::exists(Path::new(&binary_path)) {
177 Ok(true) => {}
178 _ => {
179 let build_args = if release {
180 vec!["build", "--release"]
181 } else {
182 vec!["build"]
183 };
184
185 let cargo_build = std::process::Command::new("cargo")
186 .args(build_args)
187 .output()?;
188 io::stdout().write_all(&cargo_build.stdout).unwrap();
189 io::stderr().write_all(&cargo_build.stderr).unwrap();
190
191 if !cargo_build.status.success() {
192 return Err(io::Error::new(
193 io::ErrorKind::InvalidData,
194 "Error during ProSA build",
195 ));
196 }
197 }
198 }
199
200 let binary_output_path = self.get_install_bin_dir();
202 fs::create_dir_all(&binary_output_path)?;
203 fs::copy(
204 binary_path,
205 binary_output_path.join(Path::new(&self.bin_name)),
206 )
207 }
208
209 fn gen_config(&self) -> io::Result<u64> {
210 let config_dir = self.get_install_config_path().join(&self.name);
211 fs::create_dir_all(&config_dir)?;
212
213 let config_path = config_dir.join("prosa.toml");
215 if let Ok(true) = fs::exists(&config_path)
216 && let Ok(new_config_toml) =
217 fs::read_to_string("target/config.toml")?.parse::<DocumentMut>()
218 && let Ok(mut config_toml) = fs::read_to_string(&config_path)?.parse::<DocumentMut>()
219 {
220 let mut modified = false;
221 let config_table = config_toml.as_table_mut();
222 for (new_config_key, new_config_item) in new_config_toml.as_table() {
223 if !config_table.contains_key(new_config_key) {
224 config_table.insert(new_config_key, new_config_item.clone());
225 modified = true;
226 }
227 }
228
229 if modified {
231 let mut config_toml_file = fs::File::create(config_path)?;
232 let config_toml_str = config_toml.to_string();
233 config_toml_file.write_all(config_toml_str.as_bytes())?;
234 Ok(config_toml_str.len() as u64)
235 } else {
236 Ok(0)
237 }
238 } else {
239 fs::copy("target/config.toml", config_path)
240 }
241 }
242
243 pub fn install(&self, release: bool) -> io::Result<u64> {
248 print!("Copying binary ");
249 let mut file_size = self.copy_binary(release)?;
250 println!("OK");
251 print!("Generating configuration ");
252 file_size += self.gen_config()?;
253 println!("OK");
254 print!("Creating service ");
255 self.create_service_file()
256 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
257 println!("OK");
258 Ok(file_size)
259 }
260
261 pub fn uninstall(&self, purge: bool) -> io::Result<()> {
264 if purge {
265 print!("Purge configuration file ");
266 fs::remove_dir_all(self.get_install_config_path().join(&self.name))?;
267 println!("OK");
268 }
269
270 print!("Remove service ");
271 fs::remove_file(
272 self.get_install_service_path()
273 .join(self.get_service_filename()),
274 )?;
275 println!("OK");
276
277 print!("Remove binary ");
278 fs::remove_file(self.get_install_bin_dir().join(&self.bin_name))?;
279 println!("OK");
280 Ok(())
281 }
282}
283
284impl fmt::Display for InstanceInstall {
285 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
286 writeln!(f, "ProSA `{}`", self.name)?;
287 writeln!(
288 f,
289 "Binary file : {}/{}",
290 self.install_bin_dir, self.bin_name
291 )?;
292 writeln!(
293 f,
294 "Config file : {}/{}/prosa.toml",
295 self.install_config_dir, self.name
296 )?;
297 writeln!(
298 f,
299 "Service file: {}/{}",
300 self.install_service_dir,
301 self.get_service_filename()
302 )
303 }
304}