use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
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;
const GNOME_RUNTIME_VERSION: &str = "48";
const KDE_RUNTIME_VERSION: &str = "6.9";
const QT_WEBENGINE_BASEAPP: &str = "io.qt.qtwebengine.BaseApp";
pub fn pack(
project: &Project,
target: &'static Target,
opts: &PackOptions,
dist: &Path,
) -> Result<Artifact, PackError> {
for tool in ["flatpak", "flatpak-builder"] {
if !on_path(tool) {
return Err(PackError::Other(format!(
"{tool} not found — install flatpak + flatpak-builder and add the flathub remote:\n \
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo"
)));
}
}
let outcome = ops::build(project, target, opts.profile).map_err(PackError::Other)?;
let name = project.manifest.app.name.clone();
let id = project.manifest.app.id.clone();
let title = project
.manifest
.app
.title
.clone()
.unwrap_or_else(|| name.clone());
let work = project.root.join("build/day/flatpak").join(target.name);
let stage = work.join("stage");
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&stage).map_err(|e| PackError::Other(e.to_string()))?;
let staged = super::linux::stage_tree(project, target, &outcome.artifact, &stage)
.map_err(PackError::Other)?;
let launcher_path = stage.join("bin").join(&id);
std::fs::write(&launcher_path, super::linux::launcher("/app", "", &staged))
.map_err(|e| PackError::Other(e.to_string()))?;
super::linux::set_executable(&launcher_path).map_err(PackError::Other)?;
super::linux::stage_exports(project, &stage, &id, &title, &id).map_err(PackError::Other)?;
let webengine = target.toolkit == "qt" && links_qt_webengine(&outcome.artifact).unwrap_or(true);
if target.toolkit == "qt" {
status(
"Packing",
if webengine {
"linked against QtWebEngine — bundling the Qt WebEngine BaseApp"
} else {
"no QtWebEngine link — packing without the Qt WebEngine BaseApp"
},
);
}
let manifest_path = work.join(format!("{id}.yml"));
std::fs::write(&manifest_path, manifest_yaml(target, &id, &name, webengine))
.map_err(|e| PackError::Other(e.to_string()))?;
status("Packing", "flatpak-builder");
let mut fb = Command::new("flatpak-builder");
crate::ops::apply_determinism(&mut fb);
run_tool(
fb.current_dir(&work)
.args(["--force-clean", "--user", "--install-deps-from=flathub"])
.arg("--repo=repo")
.arg("builddir")
.arg(&manifest_path),
"flatpak-builder",
)
.map_err(PackError::Other)?;
let arch = flatpak_arch();
let bundle = dist.join(super::naming::artifact_file(
project,
target,
opts,
&[arch],
"flatpak",
));
let _ = std::fs::remove_file(&bundle);
status("Packing", "flatpak build-bundle");
let mut bundle_cmd = Command::new("flatpak");
crate::ops::apply_determinism(&mut bundle_cmd);
run_tool(
bundle_cmd
.current_dir(&work)
.arg("build-bundle")
.arg("repo")
.arg(&bundle)
.arg(&id)
.arg("--runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo"),
"flatpak build-bundle",
)
.map_err(PackError::Other)?;
Ok(Artifact {
path: bundle,
kind: "flatpak",
sha256: String::new(),
tier: SignTier::Unsigned,
})
}
pub(crate) fn runtime_for(target: &Target) -> (&'static str, String) {
match target.toolkit {
"qt" => (
"org.kde.Platform",
std::env::var("DAY_KDE_RUNTIME").unwrap_or_else(|_| KDE_RUNTIME_VERSION.into()),
),
_ => (
"org.gnome.Platform",
std::env::var("DAY_GNOME_RUNTIME").unwrap_or_else(|_| GNOME_RUNTIME_VERSION.into()),
),
}
}
pub(crate) fn manifest_yaml(target: &Target, id: &str, name: &str, webengine: bool) -> String {
let (runtime, runtime_version) = runtime_for(target);
let sdk = runtime.replace(".Platform", ".Sdk");
let base = if webengine {
format!("base: {QT_WEBENGINE_BASEAPP}\nbase-version: '{runtime_version}'\n")
} else {
String::new()
};
format!(
r#"id: {id}
runtime: {runtime}
runtime-version: '{runtime_version}'
sdk: {sdk}
{base}command: {id}
# The payload is a prebuilt release binary with no debug info — skip flatpak-builder's
# debuginfo split (it shells out to elfutils' eu-strip, which isn't installed everywhere,
# e.g. ubuntu-24.04 CI runners) and its strip pass.
build-options:
no-debuginfo: true
strip: false
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --share=network
modules:
- name: {name}
buildsystem: simple
build-commands:
- cp -a . /app
sources:
- type: dir
path: stage
"#
)
}
fn links_qt_webengine(binary: &Path) -> Option<bool> {
const SHT_DYNAMIC: u32 = 6;
const DT_NULL: u64 = 0;
const DT_NEEDED: u64 = 1;
const SHDR_LEN: usize = 64;
let mut f = std::fs::File::open(binary).ok()?;
let mut ehdr = [0u8; 64];
f.read_exact(&mut ehdr).ok()?;
if ehdr[..4] != *b"\x7fELF" || ehdr[4] != 2 || ehdr[5] != 1 {
return None;
}
fn u16_at(b: &[u8], o: usize) -> Option<u16> {
Some(u16::from_le_bytes(b.get(o..o + 2)?.try_into().ok()?))
}
fn u32_at(b: &[u8], o: usize) -> Option<u32> {
Some(u32::from_le_bytes(b.get(o..o + 4)?.try_into().ok()?))
}
fn u64_at(b: &[u8], o: usize) -> Option<u64> {
Some(u64::from_le_bytes(b.get(o..o + 8)?.try_into().ok()?))
}
let e_shoff = u64_at(&ehdr, 0x28)?;
let e_shentsize = u16_at(&ehdr, 0x3a)? as usize;
let e_shnum = u16_at(&ehdr, 0x3c)? as usize;
if e_shentsize < SHDR_LEN || e_shnum == 0 {
return None;
}
let mut read_shdr = |i: usize| -> Option<(u32, u64, u64, u32)> {
let at = e_shoff.checked_add((i * e_shentsize) as u64)?;
f.seek(SeekFrom::Start(at)).ok()?;
let mut sh = [0u8; SHDR_LEN];
f.read_exact(&mut sh).ok()?;
Some((
u32_at(&sh, 4)?,
u64_at(&sh, 24)?,
u64_at(&sh, 32)?,
u32_at(&sh, 40)?,
))
};
let (dyn_off, dyn_size, strtab_idx) = (0..e_shnum)
.filter_map(&mut read_shdr)
.find(|(kind, ..)| *kind == SHT_DYNAMIC)
.map(|(_, off, size, link)| (off, size, link as usize))?;
let (_, str_off, str_size, _) = read_shdr(strtab_idx)?;
let mut needed = Vec::new();
for i in 0..(dyn_size / 16) {
f.seek(SeekFrom::Start(dyn_off.checked_add(i * 16)?)).ok()?;
let mut ent = [0u8; 16];
f.read_exact(&mut ent).ok()?;
match u64_at(&ent, 0)? {
DT_NULL => break,
DT_NEEDED => needed.push(u64_at(&ent, 8)?),
_ => {}
}
}
for off in needed {
if off >= str_size {
continue;
}
f.seek(SeekFrom::Start(str_off.checked_add(off)?)).ok()?;
let mut buf = [0u8; 256];
let n = f.read(&mut buf).ok()?;
let name = buf[..n].split(|b| *b == 0).next().unwrap_or_default();
if name.starts_with(b"libQt6WebEngine") {
return Some(true);
}
}
Some(false)
}
fn on_path(tool: &str) -> bool {
std::env::var("PATH").is_ok_and(|p| std::env::split_paths(&p).any(|d| d.join(tool).is_file()))
}
fn flatpak_arch() -> &'static str {
if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"x86_64"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::targets;
#[test]
fn manifest_runtime_per_toolkit() {
let gtk = manifest_yaml(
targets::find("linux-gtk").unwrap(),
"dev.x.app",
"app",
false,
);
assert!(gtk.contains("runtime: org.gnome.Platform"));
assert!(gtk.contains("sdk: org.gnome.Sdk"));
assert!(!gtk.contains("base:"));
let qt = manifest_yaml(targets::find("linux-qt").unwrap(), "dev.x.app", "app", true);
assert!(qt.contains("runtime: org.kde.Platform"));
assert!(qt.contains("base: io.qt.qtwebengine.BaseApp"));
assert!(qt.contains("command: dev.x.app"));
let lean = manifest_yaml(
targets::find("linux-qt").unwrap(),
"dev.x.app",
"app",
false,
);
assert!(lean.contains("runtime: org.kde.Platform"));
assert!(!lean.contains("base:"));
for manifest in [>k, &qt, &lean] {
let parsed: serde_json::Value = serde_norway::from_str(manifest).unwrap();
assert_eq!(parsed["build-options"]["no-debuginfo"], true);
assert_eq!(parsed["build-options"]["strip"], false);
}
}
fn elf_needing(names: &[&str]) -> Vec<u8> {
const STR_OFF: usize = 0x100;
const DYN_OFF: usize = 0x400;
const SH_OFF: usize = 0x800;
let mut dynstr = vec![0u8];
let mut dynamic = Vec::new();
for n in names {
dynamic.extend_from_slice(&1u64.to_le_bytes()); dynamic.extend_from_slice(&(dynstr.len() as u64).to_le_bytes());
dynstr.extend_from_slice(n.as_bytes());
dynstr.push(0);
}
dynamic.extend_from_slice(&[0u8; 16]);
let mut f = vec![0u8; SH_OFF + 3 * 64];
f[..4].copy_from_slice(b"\x7fELF");
(f[4], f[5]) = (2, 1); f[0x28..0x30].copy_from_slice(&(SH_OFF as u64).to_le_bytes()); f[0x3a..0x3c].copy_from_slice(&64u16.to_le_bytes()); f[0x3c..0x3e].copy_from_slice(&3u16.to_le_bytes()); f[STR_OFF..STR_OFF + dynstr.len()].copy_from_slice(&dynstr);
f[DYN_OFF..DYN_OFF + dynamic.len()].copy_from_slice(&dynamic);
let mut shdr = |i: usize, kind: u32, off: usize, size: usize, link: u32| {
let at = SH_OFF + i * 64;
f[at + 4..at + 8].copy_from_slice(&kind.to_le_bytes());
f[at + 24..at + 32].copy_from_slice(&(off as u64).to_le_bytes());
f[at + 32..at + 40].copy_from_slice(&(size as u64).to_le_bytes());
f[at + 40..at + 44].copy_from_slice(&link.to_le_bytes());
};
shdr(1, 3, STR_OFF, dynstr.len(), 0); shdr(2, 6, DYN_OFF, dynamic.len(), 1); f
}
#[test]
fn webengine_probe_reads_dt_needed() {
let dir = std::env::temp_dir().join(format!("day-flatpak-elf-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let write = |stem: &str, bytes: &[u8]| {
let p = dir.join(stem);
std::fs::write(&p, bytes).unwrap();
p
};
let with = write(
"with",
&elf_needing(&[
"libQt6Widgets.so.6",
"libQt6WebEngineWidgets.so.6",
"libc.so.6",
]),
);
assert_eq!(links_qt_webengine(&with), Some(true));
let without = write(
"without",
&elf_needing(&["libQt6Widgets.so.6", "libQt6Gui.so.6", "libc.so.6"]),
);
assert_eq!(links_qt_webengine(&without), Some(false));
let alien = write("alien", b"\xcf\xfa\xed\xfe not an elf at all");
assert_eq!(links_qt_webengine(&alien), None);
assert_eq!(links_qt_webengine(&dir.join("nonexistent")), None);
let _ = std::fs::remove_dir_all(&dir);
}
}