use super::run;
use anyhow::{Context as _, Result, bail};
use bezel::gpui::App;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
pub(super) const SUPPORTED: bool = cfg!(target_arch = "aarch64");
pub(super) const SUFFIX: &str = "-arm64.dmg";
pub(super) fn remote(version: &str) -> String {
super::asset(version)
}
pub(super) fn bundle(cx: &App) -> Option<PathBuf> {
let path = cx.app_path().ok()?;
(path.extension()? == "app").then_some(path)
}
pub(super) fn stage(version: &str, app: &Path) -> Result<PathBuf> {
let parent = app.parent().context("the app is at the root of a volume")?;
let staging = parent.join(format!("{}{version}", super::STAGING));
let _ = std::fs::remove_dir_all(&staging);
std::fs::create_dir_all(&staging)
.with_context(|| format!("{} cannot be written to", parent.display()))?;
let image = super::download(version)?;
let mount = super::cache()?.join("mount");
let _ = std::fs::create_dir_all(&mount);
let _ = run(
"/usr/bin/hdiutil",
[
OsStr::new("detach"),
mount.as_os_str(),
OsStr::new("-quiet"),
],
);
run(
"/usr/bin/hdiutil",
[
OsStr::new("attach"),
image.as_os_str(),
OsStr::new("-nobrowse"),
OsStr::new("-readonly"),
OsStr::new("-noautoopen"),
OsStr::new("-mountpoint"),
mount.as_os_str(),
],
)?;
let staged = copy_out(&mount, &staging);
let _ = run(
"/usr/bin/hdiutil",
[
OsStr::new("detach"),
mount.as_os_str(),
OsStr::new("-quiet"),
],
);
let staged = staged?;
match verify(&staged, app) {
Ok(()) => Ok(staged),
Err(err) => {
let _ = std::fs::remove_dir_all(&staging);
Err(err)
}
}
}
fn copy_out(mount: &Path, staging: &Path) -> Result<PathBuf> {
let bundle = std::fs::read_dir(mount)?
.flatten()
.map(|entry| entry.path())
.find(|path| path.extension().is_some_and(|ext| ext == "app"))
.context("the image holds no app")?;
let staged = staging.join(bundle.file_name().context("the app has no name")?);
run("/usr/bin/ditto", [bundle.as_os_str(), staged.as_os_str()])?;
Ok(staged)
}
fn verify(staged: &Path, running: &Path) -> Result<()> {
run(
"/usr/bin/codesign",
[
OsStr::new("--verify"),
OsStr::new("--strict"),
staged.as_os_str(),
],
)
.context("the release does not match its own signature")?;
run(
"/usr/sbin/spctl",
[
OsStr::new("--assess"),
OsStr::new("--type"),
OsStr::new("exec"),
staged.as_os_str(),
],
)
.context("the release is not notarized")?;
if team(staged)? != team(running)? {
bail!("the release is signed by another developer than this copy");
}
Ok(())
}
fn team(bundle: &Path) -> Result<Option<String>> {
let out = run(
"/usr/bin/codesign",
[
OsStr::new("-d"),
OsStr::new("--verbose=4"),
bundle.as_os_str(),
],
)?;
Ok(String::from_utf8_lossy(&out.stderr)
.lines()
.find_map(|line| line.strip_prefix("TeamIdentifier="))
.filter(|team| *team != "not set")
.map(str::to_owned))
}
pub(super) fn swap_on_exit(app: &Path, staged: &Path) -> Result<()> {
super::swap_on_exit(app, staged, r#"open "$app""#)
}