Skip to content

Commit

Permalink
Merge pull request #1312 from axodotdev/pkg
Browse files Browse the repository at this point in the history
feat: Mac .pkg installer
  • Loading branch information
Gankra authored Sep 3, 2024
2 parents bc0f80f + 43dd140 commit 3f58b8e
Show file tree
Hide file tree
Showing 22 changed files with 1,077 additions and 36 deletions.
99 changes: 99 additions & 0 deletions cargo-dist/src/backend/installer/macpkg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Code for generating installer.pkg

use std::{collections::BTreeMap, fs};

use axoasset::LocalAsset;
use axoprocess::Cmd;
use camino::Utf8PathBuf;
use serde::Serialize;
use temp_dir::TempDir;
use tracing::info;

use crate::{create_tmp, DistResult};

use super::ExecutableZipFragment;

/// Info about a package installer
#[derive(Debug, Clone, Serialize)]
pub struct PkgInstallerInfo {
/// ExecutableZipFragment for this variant
pub artifact: ExecutableZipFragment,
/// Identifier for the final installer
pub identifier: String,
/// Default install location
pub install_location: String,
/// Final file path of the pkg
pub file_path: Utf8PathBuf,
/// Dir stuff goes to
pub package_dir: Utf8PathBuf,
/// The app version
pub version: String,
/// Executable aliases
pub bin_aliases: BTreeMap<String, Vec<String>>,
}

impl PkgInstallerInfo {
/// Build the pkg installer
pub fn build(&self) -> DistResult<()> {
info!("building a pkg: {}", self.identifier);

// We can't build directly from dist_dir because the
// package installer wants the directory we feed it
// to have the final package layout, which in this case
// is going to be an FHS-ish path installed into a public
// location. So instead we create a new tree with our stuff
// like we want it, and feed that to pkgbuild.
let (_build_dir, build_dir) = create_tmp()?;
let bindir = build_dir.join("bin");
LocalAsset::create_dir_all(&bindir)?;
let libdir = build_dir.join("lib");
LocalAsset::create_dir_all(&libdir)?;

info!("Copying executables");
for exe in &self.artifact.executables {
info!("{} => {:?}", &self.package_dir.join(exe), bindir.join(exe));
LocalAsset::copy_file_to_file(&self.package_dir.join(exe), bindir.join(exe))?;
}
#[cfg(unix)]
for (bin, targets) in &self.bin_aliases {
for target in targets {
std::os::unix::fs::symlink(&bindir.join(bin), &bindir.join(target))?;
}
}
for lib in self
.artifact
.cdylibs
.iter()
.chain(self.artifact.cstaticlibs.iter())
{
LocalAsset::copy_file_to_file(&self.package_dir.join(lib), libdir.join(lib))?;
}

// The path the two pkg files get placed in while building
let pkg_output = TempDir::new()?;
let pkg_output_path = pkg_output.path();
let pkg_path = pkg_output_path.join("package.pkg");
let product_path = pkg_output_path.join("product.pkg");

let mut pkgcmd = Cmd::new("/usr/bin/pkgbuild", "create individual pkg");
pkgcmd.arg("--root").arg(build_dir);
pkgcmd.arg("--identifier").arg(&self.identifier);
pkgcmd.arg("--install-location").arg(&self.install_location);
pkgcmd.arg("--version").arg(&self.version);
pkgcmd.arg(&pkg_path);
// Ensures stdout from the build process doesn't taint the dist-manifest
pkgcmd.stdout_to_stderr();
pkgcmd.run()?;

// OK, we've made a package. Now wrap it in a product pkg.
let mut productcmd = Cmd::new("/usr/bin/productbuild", "create final product .pkg");
productcmd.arg("--package").arg(&pkg_path);
productcmd.arg(&product_path);
productcmd.stdout_to_stderr();
productcmd.run()?;

fs::copy(&product_path, &self.file_path)?;

Ok(())
}
}
4 changes: 4 additions & 0 deletions cargo-dist/src/backend/installer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use std::collections::BTreeMap;

use camino::Utf8PathBuf;
use macpkg::PkgInstallerInfo;
use serde::Serialize;

use crate::{
Expand All @@ -18,6 +19,7 @@ use self::msi::MsiInstallerInfo;
use self::npm::NpmInstallerInfo;

pub mod homebrew;
pub mod macpkg;
pub mod msi;
pub mod npm;
pub mod powershell;
Expand All @@ -37,6 +39,8 @@ pub enum InstallerImpl {
Homebrew(HomebrewInstallerInfo),
/// Windows msi installer
Msi(MsiInstallerInfo),
/// Mac pkg installer
Pkg(PkgInstallerInfo),
}

/// Generic info about an installer
Expand Down
25 changes: 25 additions & 0 deletions cargo-dist/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ pub struct DistMetadata {
/// Any additional steps that need to be performed before building local artifacts
#[serde(default)]
pub github_build_setup: Option<String>,

/// Configuration specific to Mac .pkg installers
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mac_pkg_config: Option<MacPkgConfig>,
}

/// values of the form `permission-name: read`
Expand Down Expand Up @@ -523,6 +528,7 @@ impl DistMetadata {
package_libraries: _,
install_libraries: _,
github_build_setup: _,
mac_pkg_config: _,
} = self;
if let Some(include) = include {
for include in include {
Expand Down Expand Up @@ -618,6 +624,7 @@ impl DistMetadata {
package_libraries,
install_libraries,
github_build_setup,
mac_pkg_config,
} = self;

// Check for global settings on local packages
Expand Down Expand Up @@ -799,6 +806,9 @@ impl DistMetadata {
if install_libraries.is_none() {
install_libraries.clone_from(&workspace_config.install_libraries);
}
if mac_pkg_config.is_none() {
mac_pkg_config.clone_from(&workspace_config.mac_pkg_config);
}

// This was historically implemented as extend, but I'm not convinced the
// inconsistency is worth the inconvenience...
Expand Down Expand Up @@ -956,6 +966,8 @@ pub enum InstallerStyle {
Homebrew,
/// Generate an msi installer that embeds the binary
Msi,
/// Generate an Apple pkg installer that embeds the binary
Pkg,
}

impl std::fmt::Display for InstallerStyle {
Expand All @@ -966,6 +978,7 @@ impl std::fmt::Display for InstallerStyle {
InstallerStyle::Npm => "npm",
InstallerStyle::Homebrew => "homebrew",
InstallerStyle::Msi => "msi",
InstallerStyle::Pkg => "pkg",
};
string.fmt(f)
}
Expand Down Expand Up @@ -1675,6 +1688,18 @@ impl std::fmt::Display for ProductionMode {
}
}

/// Configuration for Mac .pkg installers
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct MacPkgConfig {
/// A unique identifier, in tld.domain.package format
pub identifier: String,
/// The location to which the software should be installed.
/// If not specified, /usr/local will be used.
#[serde(skip_serializing_if = "Option::is_none")]
pub install_location: Option<String>,
}

pub(crate) fn parse_metadata_table_or_manifest(
manifest_path: &Utf8Path,
dist_manifest_path: Option<&Utf8Path>,
Expand Down
20 changes: 15 additions & 5 deletions cargo-dist/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,22 @@ pub enum DistError {
generate_mode: crate::config::GenerateMode,
},

/// msi with too many packages
/// msi/pkg with too many packages
#[error("{artifact_name} depends on multiple packages, which isn't yet supported")]
#[diagnostic(help("depends on {spec1} and {spec2}"))]
MultiPackageMsi {
/// Name of the msi
MultiPackage {
/// Name of the artifact
artifact_name: String,
/// One of the pacakges
spec1: String,
/// A different package
spec2: String,
},

/// msi with too few packages
/// msi/pkg with too few packages
#[error("{artifact_name} has no binaries")]
#[diagnostic(help("This should be impossible, you did nothing wrong, please file an issue!"))]
NoPackageMsi {
NoPackage {
/// Name of the msi
artifact_name: String,
},
Expand Down Expand Up @@ -522,6 +522,16 @@ pub enum DistError {
#[error("We failed to decode the certificate stored in the CODESIGN_CERTIFICATE environment variable.")]
#[diagnostic(help("Is the value of this envirionment variable valid base64?"))]
CertificateDecodeError {},

/// Missing configuration for a .pkg
#[error("A Mac .pkg installer was requested, but the config is missing")]
#[diagnostic(help("Please ensure a dist.mac-pkg-config section is present in your config. For more details see: https://example.com"))]
MacPkgConfigMissing {},

/// User left identifier empty in init
#[error("No bundle identifier was specified")]
#[diagnostic(help("Please either enter a bundle identifier, or disable the Mac .pkg"))]
MacPkgBundleIdentifierMissing {},
}

/// This error indicates we tried to deserialize some YAML with serde_yml
Expand Down
48 changes: 47 additions & 1 deletion cargo-dist/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::Deserialize;
use crate::{
config::{
self, CiStyle, CompressionImpl, Config, DistMetadata, HostingStyle, InstallPathStrategy,
InstallerStyle, PublishStyle, ZipStyle,
InstallerStyle, MacPkgConfig, PublishStyle, ZipStyle,
},
do_generate,
errors::{DistError, DistResult},
Expand Down Expand Up @@ -477,6 +477,7 @@ fn get_new_dist_metadata(
package_libraries: None,
install_libraries: None,
github_build_setup: None,
mac_pkg_config: None,
}
};

Expand Down Expand Up @@ -692,6 +693,7 @@ fn get_new_dist_metadata(
InstallerStyle::Npm => "npm",
InstallerStyle::Homebrew => "homebrew",
InstallerStyle::Msi => "msi",
InstallerStyle::Pkg => "pkg",
});
}

Expand Down Expand Up @@ -971,6 +973,7 @@ fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
github_release,
package_libraries,
install_libraries,
mac_pkg_config,
// These settings are complex enough that we don't support editing them in init
extra_artifacts: _,
github_custom_runners: _,
Expand Down Expand Up @@ -1019,6 +1022,13 @@ fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
installers.as_ref(),
);

apply_optional_mac_pkg(
table,
"mac-pkg-config",
"\n# Configuration for the Mac .pkg installer\n",
mac_pkg_config.as_ref(),
);

apply_optional_value(
table,
"tap",
Expand Down Expand Up @@ -1430,3 +1440,39 @@ where
table.remove(key);
}
}

/// Similar to [`apply_optional_value`][] but specialized to `MacPkgConfig`, since we're not able to work with structs dynamically
fn apply_optional_mac_pkg(
table: &mut toml_edit::Table,
key: &str,
desc: &str,
val: Option<&MacPkgConfig>,
) {
if let Some(mac_pkg_config) = val {
let MacPkgConfig {
identifier,
install_location,
} = mac_pkg_config;

let new_item = &mut table[key];
let mut new_table = toml_edit::table();
if let Some(new_table) = new_table.as_table_mut() {
apply_optional_value(
new_table,
"identifier",
"# A unique identifier, in tld.domain.package format\n",
Some(identifier),
);
apply_optional_value(
new_table,
"install-location",
"# The location to which the software should be installed\n",
install_location.as_ref(),
);
new_table.decor_mut().set_prefix(desc);
}
new_item.or_insert(new_table);
} else {
table.remove(key);
}
}
16 changes: 14 additions & 2 deletions cargo-dist/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use axoasset::LocalAsset;
use axoprocess::Cmd;
use backend::{
ci::CiInfo,
installer::{self, msi::MsiInstallerInfo, InstallerImpl},
installer::{self, macpkg::PkgInstallerInfo, msi::MsiInstallerInfo, InstallerImpl},
};
use build::generic::{build_generic_target, run_extra_artifacts_build};
use build::{
Expand Down Expand Up @@ -312,8 +312,9 @@ fn build_fake(
with_root,
}) => zip_dir(src_path, dest_path, zip_style, with_root.as_deref())?,
BuildStep::GenerateInstaller(installer) => match installer {
// MSI, unlike other installers, isn't safe to generate on any platform
// MSI and pkg, unlike other installers, aren't safe to generate on any platform
InstallerImpl::Msi(msi) => generate_fake_msi(dist_graph, msi, manifest)?,
InstallerImpl::Pkg(pkg) => generate_fake_pkg(dist_graph, pkg, manifest)?,
_ => generate_installer(dist_graph, installer, manifest)?,
},
BuildStep::Checksum(ChecksumImpl {
Expand Down Expand Up @@ -367,6 +368,16 @@ fn generate_fake_msi(
Ok(())
}

fn generate_fake_pkg(
_dist: &DistGraph,
pkg: &PkgInstallerInfo,
_manifest: &DistManifest,
) -> DistResult<()> {
LocalAsset::write_new_all("", &pkg.file_path)?;

Ok(())
}

/// Generate a checksum for the src_path to dest_path
fn generate_and_write_checksum(
manifest: &mut DistManifest,
Expand Down Expand Up @@ -727,6 +738,7 @@ fn generate_installer(
installer::homebrew::write_homebrew_formula(dist, info, manifest)?
}
InstallerImpl::Msi(info) => info.build(dist)?,
InstallerImpl::Pkg(info) => info.build()?,
}
Ok(())
}
Expand Down
5 changes: 5 additions & 0 deletions cargo-dist/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ fn add_manifest_artifact(
description = Some("install via msi".to_owned());
kind = cargo_dist_schema::ArtifactKind::Installer;
}
ArtifactKind::Installer(InstallerImpl::Pkg(..)) => {
install_hint = None;
description = Some("install via pkg".to_owned());
kind = cargo_dist_schema::ArtifactKind::Installer;
}
ArtifactKind::Checksum(_) => {
install_hint = None;
description = None;
Expand Down
Loading

0 comments on commit 3f58b8e

Please sign in to comment.