service_install/install/builder.rs
1use std::collections::HashMap;
2use std::fmt::Display;
3use std::marker::PhantomData;
4use std::path::PathBuf;
5
6use crate::schedule::Schedule;
7
8use super::{init, Mode};
9
10pub struct PathIsSet;
11pub struct PathNotSet;
12impl ToAssign for PathIsSet {}
13impl ToAssign for PathNotSet {}
14
15pub struct NameIsSet;
16pub struct NameNotSet;
17impl ToAssign for NameIsSet {}
18impl ToAssign for NameNotSet {}
19
20pub struct TriggerIsSet;
21pub struct TriggerNotSet;
22impl ToAssign for TriggerIsSet {}
23impl ToAssign for TriggerNotSet {}
24
25pub struct InstallTypeNotSet;
26impl ToAssign for InstallTypeNotSet {}
27
28pub struct UserInstall;
29pub struct SystemInstall;
30impl ToAssign for SystemInstall {}
31impl ToAssign for UserInstall {}
32
33pub trait ToAssign {}
34
35#[derive(Debug, Clone)]
36pub(crate) enum Trigger {
37 OnSchedule(Schedule),
38 OnBoot,
39}
40
41/// The configuration for the current install, needed to perform the
42/// installation or remove an existing one. Create this by using the
43/// [`install_system`](crate::install_system) or
44/// [`install_user`](crate::install_user) macros.
45#[must_use]
46#[derive(Debug)]
47pub struct Spec<Path, Name, TriggerSet, InstallType>
48where
49 Path: ToAssign,
50 Name: ToAssign,
51 TriggerSet: ToAssign,
52 InstallType: ToAssign,
53{
54 pub(crate) mode: Mode,
55 pub(crate) path: Option<PathBuf>,
56 pub(crate) service_name: Option<String>,
57 pub(crate) trigger: Option<Trigger>,
58 pub(crate) description: Option<String>,
59 pub(crate) working_dir: Option<PathBuf>,
60 pub(crate) run_as: Option<String>,
61 pub(crate) args: Vec<String>,
62 /// key: Environmental variable, value: the value for that variable
63 pub(crate) environment: HashMap<String, String>,
64 pub(crate) bin_name: &'static str,
65 pub(crate) overwrite_existing: bool,
66 /// None means all
67 pub(crate) init_systems: Option<Vec<init::System>>,
68
69 pub(crate) path_set: PhantomData<Path>,
70 pub(crate) name_set: PhantomData<Name>,
71 pub(crate) trigger_set: PhantomData<TriggerSet>,
72 pub(crate) install_type: PhantomData<InstallType>,
73}
74
75/// Create a new [`Spec`] for a system wide installation
76/// # Example
77/// ```no_run
78/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
79/// # macro_rules! install_system {
80/// # () => {
81/// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
82/// # };
83/// # }
84/// #
85/// install_system!()
86/// .current_exe()?
87/// .service_name("cli")
88/// .on_boot()
89/// .prepare_install()?
90/// .install()?;
91/// # Ok(())
92/// # }
93/// ```
94#[macro_export]
95macro_rules! install_system {
96 () => {
97 service_install::install::Spec::__dont_use_use_the_macro_system(env!("CARGO_BIN_NAME"))
98 };
99}
100
101/// Create a new [`Spec`] for an installation for the current user only
102/// # Example
103/// ```no_run
104/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
105/// # macro_rules! install_user {
106/// # () => {
107/// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
108/// # };
109/// # }
110/// #
111/// install_user!()
112/// .current_exe()?
113/// .service_name("cli")
114/// .on_boot()
115/// .prepare_install()?
116/// .install()?;
117/// # Ok(())
118/// # }
119/// ```
120#[macro_export]
121macro_rules! install_user {
122 () => {
123 service_install::install::Spec::__dont_use_use_the_macro_user(env!("CARGO_BIN_NAME"))
124 };
125}
126
127impl Spec<PathNotSet, NameNotSet, TriggerNotSet, InstallTypeNotSet> {
128 #[doc(hidden)]
129 /// This is an implementation detail and *should not* be called directly!
130 pub fn __dont_use_use_the_macro_system(
131 bin_name: &'static str,
132 ) -> Spec<PathNotSet, NameNotSet, TriggerNotSet, SystemInstall> {
133 Spec {
134 mode: Mode::System,
135 path: None,
136 service_name: None,
137 trigger: None,
138 description: None,
139 working_dir: None,
140 run_as: None,
141 args: Vec::new(),
142 environment: HashMap::new(),
143 bin_name,
144 overwrite_existing: false,
145 init_systems: None,
146
147 path_set: PhantomData {},
148 name_set: PhantomData {},
149 trigger_set: PhantomData {},
150 install_type: PhantomData {},
151 }
152 }
153
154 #[doc(hidden)]
155 /// This is an implementation detail and *should not* be called directly!
156 pub fn __dont_use_use_the_macro_user(
157 bin_name: &'static str,
158 ) -> Spec<PathNotSet, NameNotSet, TriggerNotSet, UserInstall> {
159 Spec {
160 mode: Mode::User,
161 path: None,
162 service_name: None,
163 trigger: None,
164 description: None,
165 working_dir: None,
166 run_as: None,
167 args: Vec::new(),
168 environment: HashMap::new(),
169 bin_name,
170 overwrite_existing: false,
171 init_systems: None,
172
173 path_set: PhantomData {},
174 name_set: PhantomData {},
175 trigger_set: PhantomData {},
176 install_type: PhantomData {},
177 }
178 }
179}
180
181impl<Path, Name, TriggerSet> Spec<Path, Name, TriggerSet, SystemInstall>
182where
183 Path: ToAssign,
184 Name: ToAssign,
185 TriggerSet: ToAssign,
186{
187 /// Only available for [`install_system`](crate::install_system)
188 ///
189 /// # Example
190 /// ```no_run
191 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
192 /// # macro_rules! install_system {
193 /// # () => {
194 /// # service_install::install::Spec::__dont_use_use_the_macro_system("doctest")
195 /// # };
196 /// # }
197 /// #
198 /// install_system!()
199 /// .current_exe()?
200 /// .service_name("weather_checker")
201 /// .run_as("David")
202 /// .on_boot()
203 /// .prepare_install()?
204 /// .install()?;
205 /// # Ok(())
206 /// # }
207 /// ```
208 pub fn run_as(mut self, user: impl Into<String>) -> Self {
209 self.run_as = Some(user.into());
210 self
211 }
212}
213
214impl<Path, Name, TriggerSet, InstallType> Spec<Path, Name, TriggerSet, InstallType>
215where
216 Path: ToAssign,
217 Name: ToAssign,
218 TriggerSet: ToAssign,
219 InstallType: ToAssign,
220{
221 /// Install a copy of the currently running executable.
222 ///
223 /// # Example
224 /// ```no_run
225 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
226 /// # macro_rules! install_user {
227 /// # () => {
228 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
229 /// # };
230 /// # }
231 /// #
232 /// install_user!()
233 /// .path("path/to/binary/weather_checker")
234 /// .service_name("weather_checker")
235 /// .on_boot()
236 /// .prepare_install()?
237 /// .install()?;
238 /// # Ok(())
239 /// # }
240 /// ```
241 pub fn path(self, path: impl Into<PathBuf>) -> Spec<PathIsSet, Name, TriggerSet, InstallType> {
242 Spec {
243 mode: self.mode,
244 path: Some(path.into()),
245 service_name: self.service_name,
246 trigger: self.trigger,
247 description: self.description,
248 working_dir: self.working_dir,
249 run_as: self.run_as,
250 args: self.args,
251 environment: self.environment,
252 bin_name: self.bin_name,
253 overwrite_existing: self.overwrite_existing,
254 init_systems: self.init_systems,
255
256 path_set: PhantomData {},
257 name_set: PhantomData {},
258 trigger_set: PhantomData {},
259 install_type: PhantomData {},
260 }
261 }
262
263 /// Install a copy of the currently running executable.
264 ///
265 /// # Errors
266 /// Will return an error if the path to the current executable could not be gotten.
267 /// This can fail for a number of reasons such as filesystem operations and system call
268 /// failures.
269 ///
270 /// # Example
271 /// ```no_run
272 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
273 /// # macro_rules! install_user {
274 /// # () => {
275 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
276 /// # };
277 /// # }
278 /// #
279 /// install_user!()
280 /// .current_exe()?
281 /// .service_name("weather_checker")
282 /// .on_boot()
283 /// .prepare_install()?
284 /// .install()?;
285 /// # Ok(())
286 /// # }
287 /// ```
288 pub fn current_exe(
289 self,
290 ) -> Result<Spec<PathIsSet, Name, TriggerSet, InstallType>, std::io::Error> {
291 Ok(Spec {
292 mode: self.mode,
293 path: Some(std::env::current_exe()?),
294 service_name: self.service_name,
295 trigger: self.trigger,
296 description: self.description,
297 working_dir: self.working_dir,
298 run_as: self.run_as,
299 args: self.args,
300 environment: self.environment,
301 bin_name: self.bin_name,
302 overwrite_existing: self.overwrite_existing,
303 init_systems: self.init_systems,
304
305 path_set: PhantomData {},
306 name_set: PhantomData {},
307 trigger_set: PhantomData {},
308 install_type: PhantomData {},
309 })
310 }
311
312 /// Name to give the systemd service or cron job
313 ///
314 /// Only needed for *install*. During uninstall we recognize
315 /// the service or con job by the special comment service-install leaves
316 /// at the top of each
317 ///
318 /// # Example
319 /// ```no_run
320 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
321 /// # macro_rules! install_user {
322 /// # () => {
323 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
324 /// # };
325 /// # }
326 /// #
327 /// install_user!()
328 /// .current_exe()?
329 /// .service_name("weather_checker")
330 /// .on_boot()
331 /// .prepare_install()?
332 /// .install()?;
333 /// # Ok(())
334 /// # }
335 /// ```
336 pub fn service_name(
337 self,
338 service_name: impl Display,
339 ) -> Spec<Path, NameIsSet, TriggerSet, InstallType> {
340 Spec {
341 mode: self.mode,
342 path: self.path,
343 service_name: Some(service_name.to_string()),
344 trigger: self.trigger,
345 description: self.description,
346 working_dir: self.working_dir,
347 run_as: self.run_as,
348 args: self.args,
349 environment: self.environment,
350 bin_name: self.bin_name,
351 overwrite_existing: self.overwrite_existing,
352 init_systems: self.init_systems,
353
354 path_set: PhantomData {},
355 name_set: PhantomData {},
356 trigger_set: PhantomData {},
357 install_type: PhantomData {},
358 }
359 }
360
361 /// Start the job on at a certain time every day. See the [Schedule] docs for
362 /// how to configure the time.
363 ///
364 /// # Example
365 /// ```no_run
366 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
367 /// # macro_rules! install_user {
368 /// # () => {
369 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
370 /// # };
371 /// # }
372 /// #
373 /// use time::Time;
374 /// use service_install::Schedule;
375 ///
376 /// let schedule = Schedule::Daily(Time::from_hms(10, 42, 0).unwrap());
377 /// install_user!()
378 /// .current_exe()?
379 /// .service_name("weather_checker")
380 /// .on_schedule(schedule)
381 /// .prepare_install()?
382 /// .install()?;
383 /// # Ok(())
384 /// # }
385 /// ```
386 pub fn on_schedule(self, schedule: Schedule) -> Spec<Path, Name, TriggerIsSet, InstallType> {
387 Spec {
388 mode: self.mode,
389 path: self.path,
390 service_name: self.service_name,
391 trigger: Some(Trigger::OnSchedule(schedule)),
392 description: self.description,
393 working_dir: self.working_dir,
394 run_as: self.run_as,
395 args: self.args,
396 environment: self.environment,
397 bin_name: self.bin_name,
398 overwrite_existing: self.overwrite_existing,
399 init_systems: self.init_systems,
400
401 path_set: PhantomData {},
402 name_set: PhantomData {},
403 trigger_set: PhantomData {},
404 install_type: PhantomData {},
405 }
406 }
407
408 /// Start the job on boot. When cron is used as init the system needs
409 /// to be rebooted before the service is started. On systemd its started
410 /// immediately.
411 ///
412 /// # Example
413 /// ```no_run
414 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
415 /// # macro_rules! install_user {
416 /// # () => {
417 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
418 /// # };
419 /// # }
420 /// #
421 /// install_user!()
422 /// .current_exe()?
423 /// .service_name("weather_checker")
424 /// .on_boot()
425 /// .prepare_install()?
426 /// .install()?;
427 /// # Ok(())
428 /// # }
429 /// ```
430 pub fn on_boot(self) -> Spec<Path, Name, TriggerIsSet, InstallType> {
431 Spec {
432 mode: self.mode,
433 path: self.path,
434 service_name: self.service_name,
435 trigger: Some(Trigger::OnBoot),
436 description: self.description,
437 working_dir: self.working_dir,
438 run_as: self.run_as,
439 args: self.args,
440 environment: self.environment,
441 bin_name: self.bin_name,
442 overwrite_existing: self.overwrite_existing,
443 init_systems: self.init_systems,
444
445 path_set: PhantomData {},
446 name_set: PhantomData {},
447 trigger_set: PhantomData {},
448 install_type: PhantomData {},
449 }
450 }
451
452 /// The description for the installed service
453 /// # Example
454 /// ```no_run
455 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
456 /// # macro_rules! install_user {
457 /// # () => {
458 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
459 /// # };
460 /// # }
461 /// #
462 /// install_user!()
463 /// .current_exe()?
464 /// .service_name("weather_checker")
465 /// .description("Sends a notification if a storm is coming")
466 /// .on_boot()
467 /// .prepare_install()?
468 /// .install()?;
469 /// # Ok(())
470 /// # }
471 /// ```
472 pub fn description(mut self, description: impl Display) -> Self {
473 self.description = Some(description.to_string());
474 self
475 }
476
477 /// Should the installer overwrite existing files? Default is false
478 ///
479 /// Note: we do not even try replace a value if the installed and to be installed
480 /// files are identical. This setting only applies to scenarios where there are
481 /// files taking up the install location that are different to what would be installed.
482 ///
483 /// # Example
484 /// ```no_run
485 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
486 /// # macro_rules! install_user {
487 /// # () => {
488 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
489 /// # };
490 /// # }
491 /// #
492 /// install_user!()
493 /// .current_exe()?
494 /// .service_name("weather_checker")
495 /// .overwrite_existing(true)
496 /// .on_boot()
497 /// .prepare_install()?
498 /// .install()?;
499 /// # Ok(())
500 /// # }
501 /// ```
502 pub fn overwrite_existing(mut self, overwrite: bool) -> Self {
503 self.overwrite_existing = overwrite;
504 self
505 }
506
507 /// The args will be shell escaped. If any arguments where already set
508 /// this adds to them
509 /// # Example
510 /// ```no_run
511 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
512 /// # macro_rules! install_user {
513 /// # () => {
514 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
515 /// # };
516 /// # }
517 /// #
518 /// install_user!()
519 /// .current_exe()?
520 /// .service_name("weather_checker")
521 /// .on_boot()
522 /// .args(["check", "--location", "North Holland"])
523 /// .prepare_install()?
524 /// .install()?;
525 /// # Ok(())
526 /// # }
527 /// ```
528 pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
529 self.args.extend(args.into_iter().map(Into::into));
530 self
531 }
532
533 /// The argument will be shell escaped. This does not clear previous set
534 /// arguments but adds to it
535 /// # Example
536 /// ```no_run
537 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
538 /// # macro_rules! install_user {
539 /// # () => {
540 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
541 /// # };
542 /// # }
543 /// #
544 /// install_user!()
545 /// .current_exe()?
546 /// .service_name("weather_checker")
547 /// .on_boot()
548 /// .arg("check")
549 /// .arg("--location")
550 /// .arg("North Holland")
551 /// .prepare_install()?
552 /// .install()?;
553 /// # Ok(())
554 /// # }
555 /// ```
556 pub fn arg(mut self, arg: impl Into<String>) -> Self {
557 self.args.push(arg.into());
558 self
559 }
560
561 /// Environmental variables passed to the program when it runs. If you set the
562 /// same variable multiple times only the last value will be set for the program.
563 ///
564 /// # Panics
565 /// If any part of any of the environmental variables pairs contains an equal sign.
566 ///
567 /// # Example
568 /// ```no_run
569 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
570 /// # macro_rules! install_user {
571 /// # () => {
572 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
573 /// # };
574 /// # }
575 /// #
576 /// install_user!()
577 /// .current_exe()?
578 /// .service_name("cli")
579 /// .on_boot()
580 /// .env_vars([("WAYLAND_display","wayland-1"), ("SHELL", "/bin/bash")])
581 /// .prepare_install()?
582 /// .install()?;
583 /// # Ok(())
584 /// # }
585 /// ```
586 pub fn env_vars<S: Into<String>>(mut self, args: impl IntoIterator<Item = (S, S)>) -> Self {
587 let vars = args
588 .into_iter()
589 .map(|(a, b)| (a.into(), b.into()))
590 .inspect(|(var, _)| {
591 assert!(
592 var.contains(['=']),
593 "The 'key' of environmental variables may not contain an equal sign"
594 )
595 });
596 self.environment.extend(vars);
597 self
598 }
599
600 /// Environmental variable passed to the program when it runs. If you set the
601 /// same variable multiple times only the last value will be set for the program.
602 ///
603 /// # Panics
604 /// If any part of any of the environmental variables pairs contains an equal sign.
605 ///
606 /// # Example
607 /// ```no_run
608 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
609 /// # macro_rules! install_user {
610 /// # () => {
611 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
612 /// # };
613 /// # }
614 /// #
615 /// install_user!()
616 /// .current_exe()?
617 /// .service_name("cli")
618 /// .on_boot()
619 /// .env_var("SHELL", "/bin/bash")
620 /// .prepare_install()?
621 /// .install()?;
622 /// # Ok(())
623 /// # }
624 /// ```
625 pub fn env_var(mut self, variable: impl Into<String>, value: impl Into<String>) -> Self {
626 self.environment.insert(variable.into(), value.into());
627 self
628 }
629
630 /// The working directory of the program when it is started on a schedule.
631 /// Can be a relative path. Shell variables like ~ and $Home are not expanded.
632 ///
633 /// # Example
634 /// ```no_run
635 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
636 /// # macro_rules! install_user {
637 /// # () => {
638 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
639 /// # };
640 /// # }
641 /// #
642 /// install_user!()
643 /// .current_exe()?
644 /// .service_name("weather_checker")
645 /// .on_boot()
646 /// .working_dir("/home/david/.local/share/weather_checker")
647 /// .prepare_install()?
648 /// .install()?;
649 /// # Ok(())
650 /// # }
651 /// ```
652 pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
653 self.working_dir = Some(dir.into());
654 self
655 }
656
657 /// By default all supported init systems will be tried
658 /// Can be set multiple times to try multiple init systems in the
659 /// order in which this was set.
660 ///
661 /// Note: setting this for an uninstall might cause it to fail
662 ///
663 /// # Example
664 /// ```no_run
665 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
666 /// # macro_rules! install_user {
667 /// # () => {
668 /// # service_install::install::Spec::__dont_use_use_the_macro_user("doctest")
669 /// # };
670 /// # }
671 /// #
672 /// use service_install::install::init;
673 /// install_user!()
674 /// .current_exe()?
675 /// .service_name("weather_checker")
676 /// .on_boot()
677 /// .allowed_inits([init::System::Systemd])
678 /// .prepare_install()?
679 /// .install()?;
680 /// # Ok(())
681 /// # }
682 pub fn allowed_inits(mut self, allowed: impl AsRef<[init::System]>) -> Self {
683 self.init_systems = Some(allowed.as_ref().to_vec());
684 self
685 }
686}