Skip to content

Commit

Permalink
feat(packager): cross platform macOS app bundle packaging (#198)
Browse files Browse the repository at this point in the history
* feat: allow bundling macOS .app on any platform

* packageoutput refactor

* revert change

* add change file

* fmt and clippy

* fix deny check

---------

Co-authored-by: amr-crabnebula <[email protected]>
  • Loading branch information
lucasfernog-crabnebula and amr-crabnebula authored Apr 15, 2024
1 parent 4c4d919 commit 2164d02
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changes/app-bundle-cross-platform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cargo-packager": minor
---

Allow packaging the macOS app bundle on Linux and Windows hosts (without codesign support).
5 changes: 5 additions & 0 deletions .changes/package-output-struct-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cargo-packager": minor
---

Renamed `PackageOuput` to `PackageOutput` and added `PackageOutput::new`.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ build
*.sh
*.desktop
*.xml
*.md

pnpm-lock.yaml

Expand Down
27 changes: 9 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions crates/packager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ strsim = "0.10"
schemars = { workspace = true, optional = true }
native-tls = { version = "0.2", optional = true }
cargo-packager-utils = { version = "0.1.0", path = "../utils", features = [ "serde" ] }
icns = { package = "tauri-icns", version = "0.1" }
time = { workspace = true, features = [ "formatting" ] }
image = "0.24"
tempfile = "3"
plist = "1"

[target."cfg(target_os = \"windows\")".dependencies]
winreg = "0.52"
Expand All @@ -86,10 +91,3 @@ image = "0.24"
md5 = "0.7"
heck = "0.4"
ar = "0.9"

[target."cfg(target_os = \"macos\")".dependencies]
icns = { package = "tauri-icns", version = "0.1" }
time = { workspace = true, features = [ "formatting" ] }
plist = "1"
image = "0.24"
tempfile = "3"
10 changes: 0 additions & 10 deletions crates/packager/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,6 @@ pub enum Error {
#[error("Wix language {0} not found. It must be one of {1}")]
UnsupportedWixLanguage(String, String),
/// Image crate errors.
#[cfg(any(
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[error(transparent)]
ImageError(#[from] image::ImageError),
/// walkdir crate errors.
Expand All @@ -140,11 +132,9 @@ pub enum Error {
RelativeToError(#[from] relative_path::RelativeToError),
/// Time error.
#[error("`{0}`")]
#[cfg(target_os = "macos")]
TimeError(#[from] time::error::Error),
/// Plist error.
#[error(transparent)]
#[cfg(target_os = "macos")]
Plist(#[from] plist::Error),
/// Framework not found.
#[error("Framework {0} not found")]
Expand Down
6 changes: 3 additions & 3 deletions crates/packager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub use error::{Error, Result};
use flate2::{write::GzEncoder, Compression};
pub use sign::SigningConfig;

pub use package::{package, PackageOuput};
pub use package::{package, PackageOutput};
use util::PathExt;

fn parse_log_level(verbose: u8) -> tracing::Level {
Expand Down Expand Up @@ -136,7 +136,7 @@ pub fn init_tracing_subscriber(verbosity: u8) {
#[tracing::instrument(level = "trace")]
pub fn sign_outputs(
config: &SigningConfig,
packages: &mut Vec<PackageOuput>,
packages: &mut Vec<PackageOutput>,
) -> crate::Result<Vec<PathBuf>> {
let mut signatures = Vec::new();
for package in packages {
Expand Down Expand Up @@ -171,7 +171,7 @@ pub fn sign_outputs(
pub fn package_and_sign(
config: &Config,
signing_config: &SigningConfig,
) -> crate::Result<(Vec<PackageOuput>, Vec<PathBuf>)> {
) -> crate::Result<(Vec<PackageOutput>, Vec<PathBuf>)> {
let mut packages = package(config)?;
let signatures = sign_outputs(signing_config, &mut packages)?;
Ok((packages, signatures))
Expand Down
36 changes: 23 additions & 13 deletions crates/packager/src/package/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{
collections::BinaryHeap,
ffi::OsStr,
path::{Path, PathBuf},
process::Command,
};
use std::path::{Path, PathBuf};

use super::Context;
use crate::{config::Config, util};

#[cfg(target_os = "macos")]
use crate::{
codesign::macos::{self as codesign, SignTarget},
config::Config,
shell::CommandExt,
util,
};

#[tracing::instrument(level = "trace")]
Expand Down Expand Up @@ -44,22 +40,24 @@ pub(crate) fn package(ctx: &Context) -> crate::Result<Vec<PathBuf>> {
let bin_dir = contents_directory.join("MacOS");
std::fs::create_dir_all(&bin_dir)?;

let mut sign_paths = BinaryHeap::new();
#[cfg(target_os = "macos")]
let mut sign_paths = std::collections::BinaryHeap::new();

let bundle_icon_file = util::create_icns_file(&resources_dir, config)?;

tracing::debug!("Creating Info.plist");
create_info_plist(&contents_directory, bundle_icon_file, config)?;

tracing::debug!("Copying frameworks");
let framework_paths = copy_frameworks_to_bundle(&contents_directory, config)?;
let _framework_paths = copy_frameworks_to_bundle(&contents_directory, config)?;

#[cfg(target_os = "macos")]
sign_paths.extend(
framework_paths
_framework_paths
.into_iter()
.filter(|p| {
let ext = p.extension();
ext == Some(OsStr::new("framework"))
ext == Some(std::ffi::OsStr::new("framework"))
})
.map(|path| SignTarget {
path,
Expand Down Expand Up @@ -125,12 +123,14 @@ pub(crate) fn package(ctx: &Context) -> crate::Result<Vec<PathBuf>> {
continue;
}

#[cfg(target_os = "macos")]
sign_paths.push(SignTarget {
path: file,
is_native_binary: true,
});
}

#[cfg(target_os = "macos")]
if let Some(identity) = config
.macos()
.and_then(|macos| macos.signing_identity.as_ref())
Expand Down Expand Up @@ -329,7 +329,16 @@ fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
let dest_path = to.join(rel_path);
if entry.file_type().is_symlink() {
let target = std::fs::read_link(entry.path())?;
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &dest_path)?;
#[cfg(windows)]
{
if entry.file_type().is_file() {
std::os::windows::fs::symlink_file(&target, &dest_path)?;
} else {
std::os::windows::fs::symlink_dir(&target, &dest_path)?;
}
}
} else if entry.file_type().is_dir() {
std::fs::create_dir(dest_path)?;
} else {
Expand Down Expand Up @@ -416,8 +425,9 @@ fn copy_frameworks_to_bundle(
Ok(paths)
}

#[cfg(target_os = "macos")]
fn remove_extra_attr(app_bundle_path: &Path) -> crate::Result<()> {
Command::new("xattr")
std::process::Command::new("xattr")
.arg("-cr")
.arg(app_bundle_path)
.output_ok()
Expand Down
22 changes: 15 additions & 7 deletions crates/packager/src/package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::{config, shell::CommandExt, util, Config, PackageFormat};

use self::context::Context;

#[cfg(target_os = "macos")]
mod app;
#[cfg(any(
target_os = "linux",
Expand Down Expand Up @@ -45,16 +44,26 @@ mod context;
/// Generated Package metadata.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PackageOuput {
pub struct PackageOutput {
/// The package type.
pub format: PackageFormat,
/// All paths for this package.
pub paths: Vec<PathBuf>,
}

impl PackageOutput {
/// Creates a new package output.
///
/// This is only useful if you need to sign the packages in a different process,
/// after packaging the app and storing its paths.
pub fn new(format: PackageFormat, paths: Vec<PathBuf>) -> Self {
Self { format, paths }
}
}

/// Package an app using the specified config.
#[tracing::instrument(level = "trace")]
pub fn package(config: &Config) -> crate::Result<Vec<PackageOuput>> {
pub fn package(config: &Config) -> crate::Result<Vec<PackageOutput>> {
let mut formats = config
.formats
.clone()
Expand Down Expand Up @@ -93,17 +102,16 @@ pub fn package(config: &Config) -> crate::Result<Vec<PackageOuput>> {
)?;

let paths = match format {
#[cfg(target_os = "macos")]
PackageFormat::App => app::package(&ctx),
#[cfg(target_os = "macos")]
PackageFormat::Dmg => {
// PackageFormat::App is required for the DMG bundle
if !packages
.iter()
.any(|b: &PackageOuput| b.format == PackageFormat::App)
.any(|b: &PackageOutput| b.format == PackageFormat::App)
{
let paths = app::package(&ctx)?;
packages.push(PackageOuput {
packages.push(PackageOutput {
format: PackageFormat::App,
paths,
});
Expand Down Expand Up @@ -144,7 +152,7 @@ pub fn package(config: &Config) -> crate::Result<Vec<PackageOuput>> {
}
}?;

packages.push(PackageOuput {
packages.push(PackageOutput {
format: *format,
paths,
});
Expand Down
12 changes: 1 addition & 11 deletions crates/packager/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,15 +283,7 @@ pub(crate) fn os_bitness() -> crate::Result<Bitness> {
/// Returns true if the path has a filename indicating that it is a high-density
/// "retina" icon. Specifically, returns true the file stem ends with
/// "@2x" (a convention specified by the [Apple developer docs](
/// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html)).
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos",
))]
/// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html)).xw
pub(crate) fn is_retina<P: AsRef<Path>>(path: P) -> bool {
path.as_ref()
.file_stem()
Expand All @@ -303,7 +295,6 @@ pub(crate) fn is_retina<P: AsRef<Path>>(path: P) -> bool {
// Given a list of icon files, try to produce an ICNS file in the out_dir
// and return the path to it. Returns `Ok(None)` if no usable icons
// were provided.
#[cfg(target_os = "macos")]
pub fn create_icns_file(out_dir: &Path, config: &crate::Config) -> crate::Result<Option<PathBuf>> {
use image::GenericImageView;

Expand Down Expand Up @@ -395,7 +386,6 @@ pub fn create_icns_file(out_dir: &Path, config: &crate::Config) -> crate::Result
}

// Converts an image::DynamicImage into an icns::Image.
#[cfg(target_os = "macos")]
fn make_icns_image(img: image::DynamicImage) -> std::io::Result<icns::Image> {
let pixel_format = match img.color() {
image::ColorType::Rgba8 => icns::PixelFormat::RGBA,
Expand Down
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Target triples to include when checking. This is essentially our supported target list.
[graph]
targets = [
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "aarch64-unknown-linux-gnu" },
Expand Down

0 comments on commit 2164d02

Please sign in to comment.