1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Updater bundle creation.
//!
//! Creates zip/tar.gz archives of bundle artifacts for auto-update distribution.
use super::{Bundle, BundleContext};
use crate::PackageType;
use anyhow::{Context, Result};
use std::{
fs::File,
io,
path::{Path, PathBuf},
};
impl BundleContext<'_> {
/// Repackage previously-built bundle artifacts into updater-friendly archives.
///
/// This step never builds the application itself. Instead, it consumes the
/// [`Bundle`] records produced earlier in the main bundling pass and wraps those
/// artifacts into archive formats that are convenient for update distribution.
///
/// Archive mapping:
/// - macOS `.app` bundles become `.app.tar.gz` archives so the bundle directory
/// layout is preserved exactly.
/// - Windows installers (`.msi` and NSIS `.exe`) become single-file `.zip`
/// archives.
/// - Linux artifacts (`.AppImage` and `.deb`) become single-file `.tar.gz`
/// archives.
///
/// All outputs are written to `project_out_directory()/bundle/updater`. This
/// method assumes the input bundles are already finalized and signed as needed; it
/// performs no platform-specific mutation beyond wrapping them in the selected
/// archive container.
pub(crate) async fn bundle_updater(&self, bundles: &[Bundle]) -> Result<Vec<PathBuf>> {
let mut updater_paths = Vec::new();
let output_dir = self.project_out_directory().join("updater");
std::fs::create_dir_all(&output_dir)?;
for bundle in bundles {
match bundle.package_type {
PackageType::MacOsBundle => {
// Create .tar.gz of the .app bundle
for app_path in &bundle.bundle_paths {
let tar_path = output_dir.join(format!(
"{}_{}.app.tar.gz",
self.product_name(),
self.version_string()
));
create_tar_gz(app_path, &tar_path)?;
tracing::info!("Created updater archive: {}", tar_path.display());
updater_paths.push(tar_path);
}
}
PackageType::Nsis | PackageType::WindowsMsi => {
// Create .zip of the installer
for installer_path in &bundle.bundle_paths {
let zip_path = output_dir.join(format!(
"{}.zip",
installer_path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
));
create_zip(installer_path, &zip_path)?;
tracing::info!("Created updater archive: {}", zip_path.display());
updater_paths.push(zip_path);
}
}
PackageType::AppImage | PackageType::Deb => {
// Create .tar.gz of the artifact
for artifact_path in &bundle.bundle_paths {
let tar_path = output_dir.join(format!(
"{}.tar.gz",
artifact_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
));
create_tar_gz_single_file(artifact_path, &tar_path)?;
tracing::info!("Created updater archive: {}", tar_path.display());
updater_paths.push(tar_path);
}
}
_ => {}
}
}
Ok(updater_paths)
}
}
/// Create a .tar.gz of a directory (e.g., a .app bundle).
fn create_tar_gz(src_dir: &Path, dest: &Path) -> Result<()> {
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
let dir_name = src_dir
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
tar.append_dir_all(&dir_name, src_dir)
.with_context(|| format!("Failed to add {} to tar", src_dir.display()))?;
tar.into_inner()?.finish()?;
Ok(())
}
/// Create a .tar.gz containing a single file.
fn create_tar_gz_single_file(src_file: &Path, dest: &Path) -> Result<()> {
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
let file_name = src_file
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
tar.append_path_with_name(src_file, &file_name)
.with_context(|| format!("Failed to add {} to tar", src_file.display()))?;
tar.into_inner()?.finish()?;
Ok(())
}
/// Create a .zip containing a single file.
fn create_zip(src_file: &Path, dest: &Path) -> Result<()> {
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let mut zip = zip::ZipWriter::new(file);
let file_name = src_file
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file(&file_name, options)?;
let mut src = File::open(src_file)?;
io::copy(&mut src, &mut zip)?;
zip.finish()?;
Ok(())
}