cargo_packager/package/
mod.rs

1// Copyright 2023-2023 CrabNebula Ltd.
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::path::PathBuf;
6
7use crate::{config, shell::CommandExt, util, Config, PackageFormat};
8
9use self::context::Context;
10
11mod app;
12#[cfg(any(
13    target_os = "linux",
14    target_os = "dragonfly",
15    target_os = "freebsd",
16    target_os = "netbsd",
17    target_os = "openbsd"
18))]
19mod appimage;
20#[cfg(any(
21    target_os = "linux",
22    target_os = "dragonfly",
23    target_os = "freebsd",
24    target_os = "netbsd",
25    target_os = "openbsd"
26))]
27mod deb;
28#[cfg(target_os = "macos")]
29mod dmg;
30mod nsis;
31#[cfg(any(
32    target_os = "linux",
33    target_os = "dragonfly",
34    target_os = "freebsd",
35    target_os = "netbsd",
36    target_os = "openbsd"
37))]
38mod pacman;
39#[cfg(windows)]
40mod wix;
41
42mod context;
43
44/// Generated Package metadata.
45#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub struct PackageOutput {
48    /// The package type.
49    pub format: PackageFormat,
50    /// All paths for this package.
51    pub paths: Vec<PathBuf>,
52}
53
54impl PackageOutput {
55    /// Creates a new package output.
56    ///
57    /// This is only useful if you need to sign the packages in a different process,
58    /// after packaging the app and storing its paths.
59    pub fn new(format: PackageFormat, paths: Vec<PathBuf>) -> Self {
60        Self { format, paths }
61    }
62}
63
64/// Package an app using the specified config.
65#[tracing::instrument(level = "trace", skip(config))]
66pub fn package(config: &Config) -> crate::Result<Vec<PackageOutput>> {
67    let mut formats = config
68        .formats
69        .clone()
70        .unwrap_or_else(|| PackageFormat::platform_default().to_vec());
71
72    if formats.is_empty() {
73        return Ok(Vec::new());
74    }
75
76    if formats.contains(&PackageFormat::Default) {
77        formats = PackageFormat::platform_default().to_vec();
78    }
79
80    if formats.contains(&PackageFormat::All) {
81        formats = PackageFormat::platform_all().to_vec();
82    }
83
84    formats.sort_by_key(|f| f.priority());
85
86    let formats_comma_separated = formats
87        .iter()
88        .map(|f| f.short_name())
89        .collect::<Vec<_>>()
90        .join(",");
91
92    run_before_packaging_command_hook(config, &formats_comma_separated)?;
93
94    let ctx = Context::new(config)?;
95    tracing::trace!(ctx = ?ctx);
96
97    let mut packages = Vec::new();
98    for format in &formats {
99        run_before_each_packaging_command_hook(
100            config,
101            &formats_comma_separated,
102            format.short_name(),
103        )?;
104
105        let paths = match format {
106            PackageFormat::App => app::package(&ctx),
107            #[cfg(target_os = "macos")]
108            PackageFormat::Dmg => {
109                // PackageFormat::App is required for the DMG bundle
110                if !packages
111                    .iter()
112                    .any(|b: &PackageOutput| b.format == PackageFormat::App)
113                {
114                    let paths = app::package(&ctx)?;
115                    packages.push(PackageOutput {
116                        format: PackageFormat::App,
117                        paths,
118                    });
119                }
120                dmg::package(&ctx)
121            }
122            #[cfg(target_os = "windows")]
123            PackageFormat::Wix => wix::package(&ctx),
124            PackageFormat::Nsis => nsis::package(&ctx),
125            #[cfg(any(
126                target_os = "linux",
127                target_os = "dragonfly",
128                target_os = "freebsd",
129                target_os = "netbsd",
130                target_os = "openbsd"
131            ))]
132            PackageFormat::Deb => deb::package(&ctx),
133            #[cfg(any(
134                target_os = "linux",
135                target_os = "dragonfly",
136                target_os = "freebsd",
137                target_os = "netbsd",
138                target_os = "openbsd"
139            ))]
140            PackageFormat::AppImage => appimage::package(&ctx),
141            #[cfg(any(
142                target_os = "linux",
143                target_os = "dragonfly",
144                target_os = "freebsd",
145                target_os = "netbsd",
146                target_os = "openbsd"
147            ))]
148            PackageFormat::Pacman => pacman::package(&ctx),
149
150            _ => {
151                tracing::warn!("ignoring {}", format.short_name());
152                continue;
153            }
154        }?;
155
156        packages.push(PackageOutput {
157            format: *format,
158            paths,
159        });
160    }
161
162    #[cfg(target_os = "macos")]
163    {
164        // Clean up .app if only building dmg
165        if !formats.contains(&PackageFormat::App) {
166            if let Some(app_bundle_paths) = packages
167                .iter()
168                .position(|b| b.format == PackageFormat::App)
169                .map(|i| packages.remove(i))
170                .map(|b| b.paths)
171            {
172                for p in &app_bundle_paths {
173                    use crate::Error;
174                    use std::fs;
175
176                    tracing::debug!("Cleaning {}", p.display());
177                    match p.is_dir() {
178                        true => {
179                            fs::remove_dir_all(p).map_err(|e| Error::IoWithPath(p.clone(), e))?
180                        }
181                        false => fs::remove_file(p).map_err(|e| Error::IoWithPath(p.clone(), e))?,
182                    };
183                }
184            }
185        }
186    }
187
188    Ok(packages)
189}
190
191fn run_before_each_packaging_command_hook(
192    config: &Config,
193    formats_comma_separated: &str,
194    format: &str,
195) -> crate::Result<()> {
196    if let Some(hook) = &config.before_each_package_command {
197        let (mut cmd, script) = match hook {
198            config::HookCommand::Script(script) => {
199                let cmd = util::cross_command(script);
200                (cmd, script)
201            }
202            config::HookCommand::ScriptWithOptions { script, dir } => {
203                let mut cmd = util::cross_command(script);
204                if let Some(dir) = dir {
205                    cmd.current_dir(dir);
206                }
207                (cmd, script)
208            }
209        };
210
211        tracing::info!("Running beforeEachPackageCommand [{format}] `{script}`");
212        let output = cmd
213            .env("CARGO_PACKAGER_FORMATS", formats_comma_separated)
214            .env("CARGO_PACKAGER_FORMAT", format)
215            .output_ok_info()
216            .map_err(|e| {
217                crate::Error::HookCommandFailure(
218                    "beforeEachPackageCommand".into(),
219                    script.into(),
220                    e,
221                )
222            })?;
223
224        if !output.status.success() {
225            return Err(crate::Error::HookCommandFailureWithExitCode(
226                "beforeEachPackageCommand".into(),
227                script.into(),
228                output.status.code().unwrap_or_default(),
229            ));
230        }
231    }
232
233    Ok(())
234}
235
236fn run_before_packaging_command_hook(
237    config: &Config,
238    formats_comma_separated: &str,
239) -> crate::Result<()> {
240    if let Some(hook) = &config.before_packaging_command {
241        let (mut cmd, script) = match hook {
242            config::HookCommand::Script(script) => {
243                let cmd = util::cross_command(script);
244                (cmd, script)
245            }
246            config::HookCommand::ScriptWithOptions { script, dir } => {
247                let mut cmd = util::cross_command(script);
248                if let Some(dir) = dir {
249                    cmd.current_dir(dir);
250                }
251                (cmd, script)
252            }
253        };
254
255        tracing::info!("Running beforePackageCommand `{script}`");
256        let output = cmd
257            .env("CARGO_PACKAGER_FORMATS", formats_comma_separated)
258            .output_ok_info()
259            .map_err(|e| {
260                crate::Error::HookCommandFailure("beforePackagingCommand".into(), script.into(), e)
261            })?;
262
263        if !output.status.success() {
264            return Err(crate::Error::HookCommandFailureWithExitCode(
265                "beforePackagingCommand".into(),
266                script.into(),
267                output.status.code().unwrap_or_default(),
268            ));
269        }
270    }
271
272    Ok(())
273}