Package macOS desktop releases as DMGs (#82)
This commit is contained in:
@@ -53,6 +53,7 @@ jobs:
|
||||
|
||||
- name: Build, package, and upload Unix releases
|
||||
env:
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS: -C link-arg=-Wl,-headerpad,0x1000
|
||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
@@ -62,9 +63,10 @@ jobs:
|
||||
set -eu
|
||||
version=$(printf '%s' "$GITEA_REF_NAME" | sed 's/[^A-Za-z0-9._-]/-/g')
|
||||
for build in \
|
||||
'aarch64-apple-darwin|aarch64-apple-darwin|darwin-arm64' \
|
||||
'x86_64-apple-darwin|x86_64-apple-darwin|darwin-x64' \
|
||||
'aarch64-unknown-linux-gnu|aarch64-unknown-linux-gnu.2.28|linux-arm64' \
|
||||
'x86_64-unknown-linux-gnu|x86_64-unknown-linux-gnu.2.28|linux-x64' \
|
||||
'aarch64-apple-darwin|aarch64-apple-darwin|darwin-arm64'
|
||||
'x86_64-unknown-linux-gnu|x86_64-unknown-linux-gnu.2.28|linux-x64'
|
||||
do
|
||||
target=${build%%|*}
|
||||
rest=${build#*|}
|
||||
@@ -77,17 +79,31 @@ jobs:
|
||||
|
||||
cli="ironstorage-cli-${version}-${platform}"
|
||||
desktop="ironstorage-desktop-${version}-${platform}"
|
||||
mkdir -p "dist/$cli" "dist/$desktop"
|
||||
mkdir -p "dist/$cli"
|
||||
cp "target/$target/release/ironstorage" "dist/$cli/"
|
||||
cp "target/$target/release/ironstorage-tui" "dist/$cli/"
|
||||
cp "target/$target/release/ironstorage" "dist/$desktop/"
|
||||
cp "target/$target/release/ironstorage-tui" "dist/$desktop/"
|
||||
cp "target/$target/release/ironstorage-desktop" "dist/$desktop/"
|
||||
tar -C dist -czf "dist/${cli}.tar.gz" "$cli"
|
||||
tar -C dist -czf "dist/${desktop}.tar.gz" "$desktop"
|
||||
rm -r "dist/$cli" "dist/$desktop"
|
||||
rm -r "dist/$cli"
|
||||
|
||||
for asset in "dist/${cli}.tar.gz" "dist/${desktop}.tar.gz"; do
|
||||
case "$platform" in
|
||||
darwin-*)
|
||||
desktop_asset="dist/${desktop}.dmg"
|
||||
cargo run --release --locked --package ironstorage-macos-packager -- \
|
||||
"$GITEA_REF_NAME" "target/$target/release" \
|
||||
assets/icon-candidates/vault-classic@2x.png "$desktop_asset"
|
||||
;;
|
||||
*)
|
||||
desktop_asset="dist/${desktop}.tar.gz"
|
||||
mkdir -p "dist/$desktop"
|
||||
cp "target/$target/release/ironstorage" "dist/$desktop/"
|
||||
cp "target/$target/release/ironstorage-tui" "dist/$desktop/"
|
||||
cp "target/$target/release/ironstorage-desktop" "dist/$desktop/"
|
||||
tar -C dist -czf "$desktop_asset" "$desktop"
|
||||
rm -r "dist/$desktop"
|
||||
;;
|
||||
esac
|
||||
|
||||
for asset in "dist/${cli}.tar.gz" "$desktop_asset"; do
|
||||
name=$(basename "$asset")
|
||||
curl --fail --silent --show-error --retry 3 \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
|
||||
1288
Cargo.lock
generated
1288
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ members = [
|
||||
"apps/tui",
|
||||
"crates/apple",
|
||||
"crates/storage",
|
||||
"tools/macos-packager",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
@@ -15,6 +16,9 @@ rust-version = "1.92"
|
||||
|
||||
[workspace.dependencies]
|
||||
apple-native-keyring-store = { version = "1.0", default-features = false, features = ["keychain", "protected"] }
|
||||
apple-bundles = "0.21"
|
||||
apple-codesign = { version = "0.29", default-features = false }
|
||||
apple-dmg = "0.5"
|
||||
arboard = { version = "3.6", default-features = false, features = ["wayland-data-control"] }
|
||||
ashpd = { version = "0.13", default-features = false, features = ["file_chooser", "tokio"] }
|
||||
cap-std = "4.0"
|
||||
@@ -29,6 +33,7 @@ gix = { version = "0.86", default-features = false, features = ["blocking-http-t
|
||||
gix-config = "0.59"
|
||||
hmac = "0.12"
|
||||
iced = { version = "0.14", features = ["canvas", "tokio"] }
|
||||
icns = "0.4"
|
||||
image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
|
||||
ironstorage = { path = "crates/storage" }
|
||||
keyring-core = "1.0"
|
||||
@@ -49,6 +54,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shlex = "1.3"
|
||||
simple-file-manifest = "0.11"
|
||||
toml = "0.9"
|
||||
uniffi = "0.32"
|
||||
url = { version = "2.5", default-features = false }
|
||||
|
||||
14
tools/macos-packager/Cargo.toml
Normal file
14
tools/macos-packager/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "ironstorage-macos-packager"
|
||||
description = "Build IronStorage macOS release bundles and DMGs"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
apple-bundles.workspace = true
|
||||
apple-codesign.workspace = true
|
||||
apple-dmg.workspace = true
|
||||
icns.workspace = true
|
||||
simple-file-manifest.workspace = true
|
||||
153
tools/macos-packager/src/main.rs
Normal file
153
tools/macos-packager/src/main.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use apple_bundles::MacOsApplicationBundleBuilder;
|
||||
use apple_codesign::{SigningSettings, UnifiedSigner};
|
||||
use icns::{IconFamily, Image};
|
||||
use simple_file_manifest::FileEntry;
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
ffi::OsString,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
const APP_NAME: &str = "IronStorage";
|
||||
const APP_EXECUTABLE: &str = "ironstorage-desktop";
|
||||
const BUNDLE_ID: &str = "de.rfc1437.ironstorage";
|
||||
const MIB: u64 = 1024 * 1024;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<OsString> = env::args_os().collect();
|
||||
if args.len() != 5 {
|
||||
return Err(
|
||||
"usage: ironstorage-macos-packager <version> <release-dir> <icon.png> <output.dmg>"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
package(
|
||||
args[1].to_str().ok_or("version is not valid UTF-8")?,
|
||||
Path::new(&args[2]),
|
||||
Path::new(&args[3]),
|
||||
Path::new(&args[4]),
|
||||
)
|
||||
}
|
||||
|
||||
fn package(
|
||||
release_tag: &str,
|
||||
release_dir: &Path,
|
||||
icon_path: &Path,
|
||||
output: &Path,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let version = bundle_version(release_tag)?;
|
||||
let output_dir = output.parent().unwrap_or_else(|| Path::new("."));
|
||||
fs::create_dir_all(output_dir)?;
|
||||
|
||||
if output.try_exists()? {
|
||||
return Err(format!("output already exists: {}", output.display()).into());
|
||||
}
|
||||
|
||||
let bundle_path = output_dir.join(format!("{APP_NAME}.app"));
|
||||
if bundle_path.try_exists()? {
|
||||
return Err(format!("staging bundle already exists: {}", bundle_path.display()).into());
|
||||
}
|
||||
|
||||
let desktop = release_dir.join("ironstorage-desktop");
|
||||
let cli = release_dir.join("ironstorage");
|
||||
let tui = release_dir.join("ironstorage-tui");
|
||||
for executable in [&desktop, &cli, &tui] {
|
||||
if !executable.is_file() {
|
||||
return Err(format!("release executable is missing: {}", executable.display()).into());
|
||||
}
|
||||
}
|
||||
|
||||
let mut bundle = MacOsApplicationBundleBuilder::new(APP_NAME)?;
|
||||
bundle.set_info_plist_required_keys(APP_NAME, BUNDLE_ID, version, "IrSt", APP_EXECUTABLE)?;
|
||||
bundle.set_info_plist_key("CFBundleShortVersionString", version)?;
|
||||
bundle.set_info_plist_key("CFBundleIconFile", format!("{APP_NAME}.icns"))?;
|
||||
bundle.set_info_plist_key("LSApplicationCategoryType", "public.app-category.utilities")?;
|
||||
bundle.set_info_plist_key("NSHighResolutionCapable", true)?;
|
||||
bundle.add_icon(icns(icon_path)?)?;
|
||||
bundle.add_file_macos(APP_EXECUTABLE, FileEntry::new_from_path(desktop, true))?;
|
||||
bundle.add_file_macos("ironstorage", FileEntry::new_from_path(cli, true))?;
|
||||
bundle.add_file_macos("ironstorage-tui", FileEntry::new_from_path(tui, true))?;
|
||||
let materialized = bundle.materialize_bundle(output_dir)?;
|
||||
|
||||
let signer = UnifiedSigner::new(SigningSettings::default());
|
||||
signer.sign_path_in_place(&materialized)?;
|
||||
apple_dmg::create_dmg(
|
||||
&materialized,
|
||||
output,
|
||||
APP_NAME,
|
||||
dmg_sectors(directory_size(&materialized)?)?,
|
||||
)?;
|
||||
signer.sign_path_in_place(output)?;
|
||||
fs::remove_dir_all(materialized)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundle_version(tag: &str) -> Result<&str, Box<dyn Error>> {
|
||||
let version = tag.strip_prefix('v').unwrap_or(tag);
|
||||
let version = version.split(['-', '+']).next().unwrap_or_default();
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
if !(1..=3).contains(&parts.len())
|
||||
|| parts
|
||||
.iter()
|
||||
.any(|part| part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
{
|
||||
return Err(format!("release tag is not a numeric Apple bundle version: {tag}").into());
|
||||
}
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
fn icns(path: &Path) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let image = Image::read_png(BufReader::new(File::open(path)?))?;
|
||||
let mut family = IconFamily::new();
|
||||
family.add_icon(&image)?;
|
||||
let mut bytes = Vec::new();
|
||||
family.write(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn directory_size(path: &Path) -> io::Result<u64> {
|
||||
fs::read_dir(path)?.try_fold(0_u64, |total, entry| {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
let size = if metadata.is_dir() {
|
||||
directory_size(&entry.path())?
|
||||
} else {
|
||||
metadata.len()
|
||||
};
|
||||
total
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| io::Error::other("bundle size overflow"))
|
||||
})
|
||||
}
|
||||
|
||||
fn dmg_sectors(bundle_bytes: u64) -> Result<u32, Box<dyn Error>> {
|
||||
let capacity = bundle_bytes
|
||||
.checked_add((bundle_bytes / 10).max(16 * MIB))
|
||||
.ok_or("DMG capacity overflow")?
|
||||
.max(64 * MIB);
|
||||
Ok(u32::try_from(capacity.div_ceil(512))?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apple_bundle_version_comes_from_semver_tag() {
|
||||
assert_eq!(bundle_version("v1.2.3").unwrap(), "1.2.3");
|
||||
assert_eq!(bundle_version("2.0.0-rc.1").unwrap(), "2.0.0");
|
||||
assert!(bundle_version("nightly").is_err());
|
||||
assert!(bundle_version("1.2.3.4").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmg_capacity_has_filesystem_headroom() {
|
||||
let sectors = dmg_sectors(100 * MIB).unwrap();
|
||||
assert!(u64::from(sectors) * 512 >= 116 * MIB);
|
||||
assert_eq!(dmg_sectors(1).unwrap(), 131_072);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user