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}