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}