Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(packager): cross platform macOS app bundle packaging #198

Merged
merged 7 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading