Skip to content

Commit

Permalink
chore(macos): sign macos application (#699)
Browse files Browse the repository at this point in the history
* sign macos app
* notarize macos app
* bundle app as .dmg image
* sign image
* fix how framework files are bundled in .app
  • Loading branch information
maxjoehnk authored Sep 9, 2024
1 parent f47b9c8 commit 7e5d3d0
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 39 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ jobs:
- name: Package
run: make Mizer.dmg
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_NAME: ${{ secrets.MACOS_CERTIFICATE_NAME }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
LIBUSB_STATIC: "true"
- uses: actions/upload-artifact@v4
with:
Expand Down
17 changes: 10 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: mizer test benchmarks build-headless build build-release run clean flatpak
.PHONY: mizer test benchmarks build-headless build build-release run clean flatpak artifact

mizer: test build-headless build

Expand Down Expand Up @@ -34,6 +34,7 @@ clean:
rm -f mizer.flatpak

artifact: build-release
rm -rf artifact || true
cargo run -p mizer-package

package-headless:
Expand All @@ -45,9 +46,12 @@ build-docker:
mizer.zip: artifact
cd artifact && zip -r ../mizer.zip *

Mizer.dmg: mizer.zip
mkdir mizer_dmg_content
unzip mizer.zip -d mizer_dmg_content
Mizer.dmg: artifact
./scripts/sign-macos-app.sh
./scripts/notarize-macos-app.sh
./scripts/package-dmg.sh

Mizer_unsigned.dmg: mizer.zip
create-dmg --volname Mizer \
--volicon "artifact/Mizer.app/Contents/Resources/AppIcon.icns" \
--window-pos 200 120 \
Expand All @@ -56,9 +60,8 @@ Mizer.dmg: mizer.zip
--icon "Mizer.app" 200 190 \
--hide-extension "Mizer.app" \
--app-drop-link 600 185 \
Mizer.dmg \
mizer_dmg_content
rm -rf mizer_dmg_content
Mizer_unsigned.dmg \
artifact

build-in-docker:
./.ci/test-local.sh
Expand Down
156 changes: 124 additions & 32 deletions crates/util/package/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::fs;
use std::fs::File;
use std::fs::{DirEntry, File};
use std::path::{Path, PathBuf};
use anyhow::Context;

use mizer_settings::Settings;

Expand Down Expand Up @@ -66,30 +67,30 @@ fn main() -> anyhow::Result<()> {
#[cfg(target_os = "macos")]
fn main() -> anyhow::Result<()> {
let artifact = Artifact::new()?;
artifact.link("Mizer.app")?;
artifact.link_all_with_suffix_to(".dylib", "Mizer.app/Contents/Frameworks")?;
artifact.link_to(
artifact.copy("Mizer.app")?;
artifact.copy_all_with_suffix_to(".dylib", "Mizer.app/Contents/Frameworks")?;
artifact.copy_to(
"deps/libndi.dylib",
"Mizer.app/Contents/Frameworks/libndi.dylib",
)?;
artifact.link_all_with_suffix_to(".framework", "Mizer.app/Contents/Frameworks")?;
artifact.link_source(
artifact.copy_all_with_suffix_to(".framework", "Mizer.app/Contents/Frameworks")?;
artifact.copy_source(
"crates/components/fixtures/open-fixture-library/.fixtures",
"Mizer.app/Contents/Resources/fixtures/open-fixture-library",
)?;
artifact.link_source(
artifact.copy_source(
"crates/components/fixtures/qlcplus/.fixtures",
"Mizer.app/Contents/Resources/fixtures/qlcplus",
)?;
artifact.link_source(
artifact.copy_source(
"crates/components/fixtures/mizer-definitions/.fixtures",
"Mizer.app/Contents/Resources/fixtures/mizer",
)?;
artifact.link_source(
artifact.copy_source(
"crates/components/connections/protocols/midi/device-profiles/profiles",
"Mizer.app/Contents/Resources/device-profiles/midi",
)?;
artifact.copy_settings("Mizer.app/Contents/MacOS/settings.toml", |settings| {
artifact.copy_settings("Mizer.app/Contents/Resources/settings.toml", |settings| {
settings.paths.media_storage = PathBuf::from("~/.mizer-media");
settings.paths.midi_device_profiles = vec![
PathBuf::from("../Resources/device-profiles/midi"),
Expand Down Expand Up @@ -199,51 +200,72 @@ impl Artifact {
})
}

fn link<P: AsRef<Path>>(&self, file: P) -> anyhow::Result<()> {
self.link_to(file.as_ref(), file.as_ref())
fn copy<P: AsRef<Path>>(&self, file: P) -> anyhow::Result<()> {
self.copy_to(file.as_ref(), file.as_ref())
}

fn link_to<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> anyhow::Result<()> {
fn copy_to<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> anyhow::Result<()> {
let from = self.build_dir.join(from);
let to = self.artifact_dir.join(to);

fs::create_dir_all(to.parent().unwrap())?;

#[cfg(target_family = "unix")]
{
println!("Linking from {:?} to {:?}", from, to);
std::os::unix::fs::symlink(&from, &to)?;
}
copy_all(&from, &to).context(format!("Copying from {from:?} to {to:?}"))?;

Ok(())
}

fn link_source<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> anyhow::Result<()> {
fn copy_source<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> anyhow::Result<()> {
let from = self.cwd.join(from);
let to = self.artifact_dir.join(to);

if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
}

#[cfg(target_family = "unix")]
{
println!("Linking from {:?} to {:?}", from, to);
std::os::unix::fs::symlink(&from, &to)?;
}
copy_all(&from, &to).context(format!("Copying from {from:?} to {to:?}"))?;

Ok(())
}

fn copy_settings<P: AsRef<Path>, F: FnOnce(&mut Settings)>(
fn copy_all_with_suffix_to<P: AsRef<Path>>(
&self,
to: P,
editor: F,
suffix: &str,
target: P,
) -> anyhow::Result<()> {
let files = self.get_files_with_suffix(suffix)?;

for file in files {
self.copy_to(file.path(), target.as_ref().join(&file.file_name()))?;
}

Ok(())
}

fn link<P: AsRef<Path>>(&self, file: P) -> anyhow::Result<()> {
self.link_to(file.as_ref(), file.as_ref())
}

fn link_to<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> anyhow::Result<()> {
let from = self.build_dir.join(from);
let to = self.artifact_dir.join(to);
let mut settings = Settings::load()?;
editor(&mut settings);
settings.save_to(to)?;

fs::create_dir_all(to.parent().unwrap())?;

link(&from, &to)?;

Ok(())
}

fn link_source<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> anyhow::Result<()> {
let from = self.cwd.join(from);
let to = self.artifact_dir.join(to);

if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
}

link(&from, &to)?;

Ok(())
}
Expand All @@ -253,6 +275,16 @@ impl Artifact {
suffix: &str,
target: P,
) -> anyhow::Result<()> {
let files = self.get_files_with_suffix(suffix)?;

for file in files {
self.link_to(file.path(), target.as_ref().join(&file.file_name()))?;
}

Ok(())
}

fn get_files_with_suffix(&self, suffix: &str) -> anyhow::Result<Vec<DirEntry>> {
let files = fs::read_dir(&self.build_dir)?;
let files = files
.into_iter()
Expand All @@ -277,11 +309,71 @@ impl Artifact {
})
.filter_map(|file: anyhow::Result<_>| file.ok().flatten())
.collect::<Vec<_>>();
Ok(files)
}

fn copy_settings<P: AsRef<Path>, F: FnOnce(&mut Settings)>(
&self,
to: P,
editor: F,
) -> anyhow::Result<()> {
let to = self.artifact_dir.join(to);
let mut settings = Settings::load()?;
editor(&mut settings);
settings.save_to(to)?;

Ok(())
}
}

fn link(from: &Path, to: &Path) -> anyhow::Result<()> {
if fs::symlink_metadata(to).is_ok() {
fs::remove_file(to)?;
}

#[cfg(target_family = "unix")]
{
println!("Linking from {:?} to {:?}", from, to);
std::os::unix::fs::symlink(from, to)?;
}

Ok(())
}

fn copy_all(from: &Path, to: &Path) -> anyhow::Result<()> {
if from.is_dir() {
fs::remove_dir_all(to).ok();
fs::create_dir_all(to)?;
let files = fs::read_dir(from)?;
for file in files {
self.link_to(file.path(), target.as_ref().join(&file.file_name()))?;
let file = file?;
let from = file.path();
let to = to.join(file.file_name());
if fs::symlink_metadata(&from)?.is_symlink() {
let target = fs::read_link(&from)?;
if target.is_absolute() {
println!("Copying from {:?} to {:?}", target, to);
fs::copy(&target, &to)?;
}else {
link(&target, &to)?;
}
continue;
}
if from.is_dir() {
println!("Copying directory from {:?} to {:?}", from, to);
fs::create_dir_all(&to)?;
copy_all(&from, &to)?;
continue;
}
println!("Copying from {:?} to {:?}", from, to);
fs::copy(&from, &to)?;
}
}else {
fs::remove_file(to).ok();

Ok(())
println!("Copying from {:?} to {:?}", from, to);
fs::copy(from, to).context("Copying single file")?;
}

Ok(())
}
25 changes: 25 additions & 0 deletions scripts/notarize-macos-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e

echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PASSWORD"

# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service

echo "Creating temp notarization archive"
ditto -c -k --keepParent "artifact/Mizer.app" "notarization.zip"

# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious

echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait

# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "Attach staple"
xcrun stapler staple "artifact/Mizer.app"
15 changes: 15 additions & 0 deletions scripts/package-dmg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e

echo "Packaging as .dmg..."
create-dmg --volname Mizer \
--volicon "artifact/Mizer.app/Contents/Resources/AppIcon.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "Mizer.app" 200 190 \
--hide-extension "Mizer.app" \
--app-drop-link 600 185 \
--codesign "$MACOS_CERTIFICATE_NAME" \
Mizer.dmg \
artifact
15 changes: 15 additions & 0 deletions scripts/sign-macos-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e

echo "Preparing keychain..."
echo "$MACOS_CERTIFICATE" | base64 --decode >certificate.p12
security create-keychain -p "$MACOS_KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWORD" build.keychain
wget https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
security import DeveloperIDG2CA.cer -k build.keychain -T /usr/bin/codesign
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_KEYCHAIN_PASSWORD" build.keychain

echo "Signing app..."
/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime --deep ./artifact/Mizer.app -v

0 comments on commit 7e5d3d0

Please sign in to comment.