use std::path::{Path, PathBuf};
use std::process::Command;
use super::settings::PackOptions;
use super::{Artifact, PackError, SignTier, run_tool};
use crate::meta::Project;
use crate::ops::{self, status};
use crate::targets::Target;
pub fn pack(
project: &Project,
target: &'static Target,
opts: &PackOptions,
dist: &Path,
) -> Result<Artifact, PackError> {
let Some(linuxdeploy) = tool("linuxdeploy") else {
return Err(PackError::Other(
"linuxdeploy not found — it is what bundles the toolkit into the AppImage.\n \
Download it (and the plugin for your toolkit) from \
https://github.com/linuxdeploy/linuxdeploy/releases, chmod +x, and put it on PATH \
as `linuxdeploy`; or set DAY_LINUXDEPLOY to its path (docs/environment.md).\n \
`day pack --formats flatpak` packs without it."
.into(),
));
};
let outcome = ops::build(project, target, opts.profile).map_err(PackError::Other)?;
let id = project.manifest.app.id.clone();
let title = project
.manifest
.app
.title
.clone()
.unwrap_or_else(|| project.manifest.app.name.clone());
let work = project.root.join("build/day/appimage").join(target.name);
let appdir = work.join("AppDir");
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&appdir).map_err(|e| PackError::Other(e.to_string()))?;
let prefix = appdir.join("usr");
let staged = super::linux::stage_tree(project, target, &outcome.artifact, &prefix)
.map_err(PackError::Other)?;
super::linux::stage_exports(project, &prefix, &id, &title, "AppRun")
.map_err(PackError::Other)?;
let apprun = appdir.join("AppRun");
std::fs::write(
&apprun,
super::linux::launcher(
"$HERE/usr",
"HERE=\"$(dirname \"$(readlink -f \"$0\")\")\"\n",
&staged,
),
)
.map_err(|e| PackError::Other(e.to_string()))?;
super::linux::set_executable(&apprun).map_err(PackError::Other)?;
std::fs::copy(
prefix
.join("share/applications")
.join(format!("{id}.desktop")),
appdir.join(format!("{id}.desktop")),
)
.map_err(|e| PackError::Other(format!("staging the root .desktop: {e}")))?;
let icon = super::linux::largest_icon(&prefix, &id).ok_or_else(|| {
PackError::Other(
"no icon staged for the AppDir root — the AppImage format requires one".into(),
)
})?;
std::fs::copy(&icon, appdir.join(format!("{id}.png")))
.map_err(|e| PackError::Other(format!("staging the root icon: {e}")))?;
let plugin = toolkit_plugin(target.toolkit);
match plugin {
Some(p) if tool(&format!("linuxdeploy-plugin-{p}")).is_some() => {
status("Packing", &format!("linuxdeploy --plugin {p}"));
}
Some(p) => {
status(
"Warning",
&format!(
"linuxdeploy-plugin-{p} not found — the AppImage will carry the library \
closure but NOT {}'s modules, loaders or schemas, so it needs a machine that \
already has {}. Install the plugin for a self-contained image.",
target.toolkit, target.toolkit
),
);
}
None => {}
}
let out = dist.join(super::naming::artifact_file(
project,
target,
opts,
&[arch()],
"appimage",
));
let _ = std::fs::remove_file(&out);
let mut cmd = Command::new(&linuxdeploy);
crate::ops::apply_determinism(&mut cmd);
cmd.current_dir(&work)
.arg("--appdir")
.arg(&appdir)
.arg("--desktop-file")
.arg(appdir.join(format!("{id}.desktop")))
.arg("--icon-file")
.arg(appdir.join(format!("{id}.png")))
.arg("--executable")
.arg(prefix.join("bin").join(format!("{}-bin", staged.name)))
.args(["--output", "appimage"])
.env("OUTPUT", &out)
.env("SOURCE_DATE_EPOCH", super::reproducible_epoch().to_string())
.env("APPIMAGE_EXTRACT_AND_RUN", "1");
if let Some(p) = plugin
&& tool(&format!("linuxdeploy-plugin-{p}")).is_some()
{
cmd.args(["--plugin", p]);
}
run_tool(&mut cmd, "linuxdeploy").map_err(PackError::Other)?;
if !out.is_file() {
return Err(PackError::Other(format!(
"linuxdeploy reported success but produced no {}",
out.display()
)));
}
super::linux::set_executable(&out).map_err(PackError::Other)?;
Ok(Artifact {
path: out,
kind: "appimage",
sha256: String::new(),
tier: SignTier::Unsigned,
})
}
fn toolkit_plugin(toolkit: &str) -> Option<&'static str> {
match toolkit {
"gtk" => Some("gtk"),
"qt" => Some("qt"),
_ => None,
}
}
pub(super) fn tool(name: &str) -> Option<PathBuf> {
let var = format!("DAY_{}", name.replace('-', "_").to_uppercase());
if let Ok(p) = std::env::var(&var) {
let path = PathBuf::from(p);
return path.is_file().then_some(path);
}
std::env::var("PATH").ok().and_then(|p| {
std::env::split_paths(&p)
.map(|d| d.join(name))
.find(|c| c.is_file())
})
}
fn arch() -> &'static str {
if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"x86_64"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_linux_toolkit_names_its_bundling_plugin() {
assert_eq!(toolkit_plugin("gtk"), Some("gtk"));
assert_eq!(toolkit_plugin("qt"), Some("qt"));
assert_eq!(toolkit_plugin("appkit"), None);
}
#[test]
fn a_tool_can_be_pointed_at_explicitly() {
let this = std::env::current_exe().expect("current exe");
unsafe { std::env::set_var("DAY_LINUXDEPLOY_PLUGIN_GTK", &this) };
assert_eq!(tool("linuxdeploy-plugin-gtk"), Some(this));
unsafe { std::env::set_var("DAY_LINUXDEPLOY_PLUGIN_GTK", "/nope/not/here") };
assert_eq!(tool("linuxdeploy-plugin-gtk"), None);
unsafe { std::env::remove_var("DAY_LINUXDEPLOY_PLUGIN_GTK") };
}
}