takecrate/inst/
exec.rs

1use std::{io::Write, path::Path};
2
3use crate::{
4    error::{AddInstallerContext, InstallerError, InstallerErrorKind},
5    manifest::{AppId, DiskDirEntry, DiskFileEntry, DiskManifest, DiskPaths, FileType},
6    os::FileChecksum,
7};
8
9use super::plan::{InstallPlan, PlanFileEntry};
10
11pub struct Executor {
12    app_id: AppId,
13    plan: InstallPlan,
14    progress_callback: Box<dyn FnMut(u64, u64)>,
15}
16
17impl Executor {
18    pub fn new(app_id: &AppId, plan: &InstallPlan) -> Self {
19        Self {
20            app_id: app_id.clone(),
21            plan: plan.clone(),
22            progress_callback: Box::new(|_, _| {}),
23        }
24    }
25
26    pub fn with_progress_callback<F>(mut self, progress_callback: F) -> Self
27    where
28        F: FnMut(u64, u64) + 'static,
29    {
30        self.progress_callback = Box::new(progress_callback);
31        self
32    }
33
34    pub fn run(&mut self) -> Result<(), InstallerError> {
35        let disk_manifest = self.populate_disk_manifest();
36
37        self.check_existing_manifest()?;
38        self.persist_disk_manifest(&disk_manifest)
39            .inst_context("failed to persist disk manifest")?;
40        self.copy_files()?;
41        self.add_path_env_var()
42            .inst_context("failed to add PATH environment variable")?;
43        self.add_app_path().inst_context("failed to add App Path")?;
44        self.add_uninstall_entry()
45            .inst_context("failed to add uninstall entry")?;
46
47        Ok(())
48    }
49
50    fn populate_disk_manifest(&self) -> DiskManifest {
51        let mut disk_manifest = DiskManifest {
52            manifest_version: 0,
53            manifest_path: Default::default(),
54            app_id: self.app_id.clone(),
55            app_name: self.plan.display_name.clone(),
56            app_version: self.plan.display_version.clone(),
57            access_scope: self.plan.access_scope,
58            app_paths: DiskPaths {
59                prefix: self.plan.destination.clone(),
60                ..Default::default()
61            },
62            dirs: Default::default(),
63            files: Default::default(),
64            search_path: self.plan.search_path.clone(),
65            #[cfg(windows)]
66            app_path_exe_name: self.plan.app_path.clone().map(|item| item.exe_name),
67            #[cfg(unix)]
68            shell_profile_path: self.plan.shell_profile_path.clone(),
69        };
70
71        for entry in &self.plan.dirs {
72            disk_manifest.dirs.push(DiskDirEntry {
73                path: entry.destination_path.clone(),
74                preserve: entry.preserve,
75            });
76
77            if let Some(file_type) = entry.content_file_type {
78                let path = entry.destination_path.clone();
79                match file_type {
80                    FileType::Executable => disk_manifest.app_paths.executable = path,
81                    FileType::Library => disk_manifest.app_paths.library = path,
82                    FileType::Configuration => disk_manifest.app_paths.configuration = path,
83                    FileType::Documentation => disk_manifest.app_paths.documentation = path,
84                    FileType::Data => disk_manifest.app_paths.data = path,
85                }
86            }
87        }
88
89        for entry in &self.plan.files {
90            disk_manifest.files.push(DiskFileEntry {
91                path: entry.destination_path.clone(),
92                len: entry.len,
93                crc32c: entry.crc32c,
94                file_type: entry.file_type,
95                is_main_executable: entry.is_main_executable,
96            });
97        }
98
99        disk_manifest
100    }
101
102    fn check_existing_manifest(&self) -> Result<(), InstallerError> {
103        if self.plan.manifest_path.exists() {
104            Err(InstallerErrorKind::AlreadyInstalled.into())
105        } else {
106            Ok(())
107        }
108    }
109
110    fn persist_disk_manifest(&self, disk_manifest: &DiskManifest) -> Result<(), InstallerError> {
111        tracing::debug!("persist disk manifest");
112
113        let mut manifest_temp_file = tempfile::NamedTempFile::new()?;
114        disk_manifest.to_writer(&mut manifest_temp_file)?;
115        manifest_temp_file.flush()?;
116
117        let manifest_checksum = crate::os::file_checksum(manifest_temp_file.path())?;
118
119        self.copy_file(
120            manifest_temp_file.path(),
121            &manifest_checksum,
122            &self.plan.manifest_path,
123        )?;
124        #[cfg(unix)]
125        {
126            use crate::error::AddContext;
127            let mode = crate::os::unix::get_effective_posix_permission(FileType::Data);
128            crate::os::unix::set_posix_permission(&self.plan.manifest_path, mode)
129                .with_context("failed to set disk manifest file permissions")?;
130        }
131
132        Ok(())
133    }
134
135    fn copy_files(&mut self) -> Result<(), InstallerError> {
136        let mut current = 0;
137        let total = self.plan.total_file_size();
138
139        for entry in &self.plan.files {
140            let span =
141                tracing::debug_span!("executor file entry", source_path = ?entry.source_path);
142            let _guard = span.enter();
143
144            let checksum = FileChecksum {
145                crc32c: entry.crc32c,
146                len: entry.len,
147            };
148            self.copy_file(&entry.source_path, &checksum, &entry.destination_path)
149                .inst_contextc(|| {
150                    format!(
151                        "failed to copy file {:?} {:?}",
152                        entry.source_path, entry.destination_path
153                    )
154                })?;
155            self.apply_posix_permission(entry).inst_contextc(|| {
156                format!(
157                    "failed to set file permissions {:?}",
158                    entry.destination_path
159                )
160            })?;
161
162            current += entry.len;
163            (self.progress_callback)(current, total);
164        }
165
166        Ok(())
167    }
168
169    fn copy_file(
170        &self,
171        source: &Path,
172        source_checksum: &FileChecksum,
173        destination: &Path,
174    ) -> Result<(), InstallerError> {
175        if destination.exists() {
176            let checksum = crate::os::file_checksum(destination)?;
177
178            if source_checksum == &checksum {
179                tracing::info!(?destination, "destination file already exists");
180
181                return Ok(());
182            } else {
183                tracing::error!(?destination, "unknown file in destination");
184                return Err(InstallerErrorKind::UnknownFileInDestination.into());
185            }
186        }
187
188        tracing::info!(?source, ?destination, "copying file");
189
190        if let Some(parent) = destination.parent() {
191            tracing::debug!(dir = ?parent, "creating directories");
192            std::fs::create_dir_all(parent)?;
193        }
194
195        std::fs::copy(source, destination)?;
196
197        Ok(())
198    }
199
200    fn apply_posix_permission(&self, entry: &PlanFileEntry) -> Result<(), InstallerError> {
201        #[cfg(unix)]
202        {
203            let mode = entry.posix_permissions;
204            tracing::debug!(mode, ?entry.destination_path, "set POSIX permissions");
205
206            crate::os::unix::set_posix_permission(&entry.destination_path, mode)?;
207        }
208
209        let _ = entry;
210
211        Ok(())
212    }
213
214    fn add_path_env_var(&self) -> Result<(), InstallerError> {
215        #[cfg(windows)]
216        if let Some(part) = &self.plan.search_path {
217            tracing::info!(?part, "modifying Path environment variable");
218            crate::os::windows::add_path_env_var(self.plan.access_scope, part.as_os_str())?;
219        }
220
221        #[cfg(unix)]
222        if let Some(part) = &self.plan.search_path {
223            if let Some(profile) = &self.plan.shell_profile_path {
224                let profile = profile.clone();
225                tracing::info!(?part, ?profile, "modifying PATH environment variable");
226                crate::os::unix::add_path_env_var(
227                    self.plan.access_scope,
228                    part.as_os_str(),
229                    &profile,
230                )?;
231            }
232        }
233        Ok(())
234    }
235
236    fn add_app_path(&self) -> Result<(), InstallerError> {
237        #[cfg(windows)]
238        if let Some(app_path) = &self.plan.app_path {
239            tracing::info!(name = ?app_path.exe_name, "modifying App Paths");
240            let config = crate::os::windows::AppPathConfig::default();
241            crate::os::windows::add_app_path(
242                self.plan.access_scope,
243                &app_path.exe_name,
244                app_path.exe_path.as_os_str(),
245                &config,
246            )?;
247        }
248
249        Ok(())
250    }
251
252    fn add_uninstall_entry(&self) -> Result<(), InstallerError> {
253        #[cfg(windows)]
254        {
255            if self.plan.interactive_uninstall_args.is_empty() {
256                tracing::warn!("no uninstall arguments provided");
257                return Ok(());
258            }
259
260            if let Some(entry) = self.plan.main_executable() {
261                tracing::info!("adding uninstall entry");
262
263                let config = crate::os::windows::UninstallEntryConfig {
264                    manifest_path: self.plan.manifest_path.clone(),
265                    display_name: self.plan.display_name.clone(),
266                    display_version: self.plan.display_version.clone(),
267                    publisher: String::new(),
268                    estimated_size: self.plan.total_file_size(),
269                    quiet_exe_args: self.plan.quiet_uninstall_args.clone(),
270                };
271
272                crate::os::windows::add_uninstall_entry(
273                    self.plan.access_scope,
274                    &self.app_id,
275                    entry.destination_path.as_os_str(),
276                    &self.plan.interactive_uninstall_args,
277                    &config,
278                )?;
279            }
280        }
281        #[cfg(unix)]
282        {
283            let _ = self.plan.main_executable();
284        }
285
286        Ok(())
287    }
288}