use std::{
fs::{create_dir_all, File},
io::Write,
path::{Path, PathBuf},
};
use crate::ProjectDescriptor;
pub fn build_flatpak(
project_dir: &Path,
project_descriptor: &ProjectDescriptor,
gra_gen_dir: &Path,
) {
let data_dir = gra_gen_dir.join("data");
create_dir_all(&data_dir).expect("Could not create data build directory.");
super::desktop::create_desktop_file(project_descriptor, &data_dir)
.expect("Could not create desktop file.");
let dev_flatpak_manifest = include_str!("../../data/flatpak-dev.template.yml");
create_flatpak_yml(
project_descriptor,
&data_dir,
dev_flatpak_manifest,
Some(".dev"),
)
.expect("Could not create flatpak yml.");
let publish_flatpak_manifest = include_str!("../../data/flatpak.template.yml");
create_flatpak_yml(
project_descriptor,
&data_dir,
publish_flatpak_manifest,
None,
)
.expect("Could not create flatpak yml.");
if let Err(e) = create_app_descriptor_xml(project_descriptor, &data_dir) {
eprintln!("[gra] {}", e);
return;
}
if let Err(e) = create_images(project_dir, project_descriptor, &data_dir) {
eprintln!("[gra] {}", e);
}
}
fn create_images(
project_dir: &Path,
descriptor: &ProjectDescriptor,
path: &Path,
) -> std::io::Result<()> {
let app_desc = &descriptor.app;
let file64 = path.join(format!("{}.64.png", &app_desc.id));
println!("[gra] Generate {:?}", file64);
std::fs::copy(project_dir.join("./assets/icon.64.png"), &file64)?;
let file128 = path.join(format!("{}.128.png", &app_desc.id));
println!("[gra] Generate {:?}", file128);
std::fs::copy(project_dir.join("./assets/icon.128.png"), &file128)?;
let file_svg = path.join(format!("{}.svg", &app_desc.id));
println!("[gra] Generate {:?}", file_svg);
std::fs::copy(project_dir.join("./assets/icon.svg"), &file_svg)?;
Ok(())
}
fn create_flatpak_yml(
descriptor: &ProjectDescriptor,
path: &Path,
template: &str,
infix: Option<&str>,
) -> std::io::Result<()> {
let app_desc = &descriptor.app;
let mut path = PathBuf::from(path);
path.push(format!("{}{}.yml", app_desc.id, infix.unwrap_or("")));
println!("[gra] Generate {:?}", path);
let mut file = File::create(path)?;
let permissions = app_desc
.permissions
.iter()
.map(|p| format!("- --{}", p))
.collect::<Vec<String>>()
.join("\n ");
file.write_all(
template
.replace(
"{name}",
descriptor
.app
.name
.as_ref()
.unwrap_or(&descriptor.package.name),
)
.replace("{id}", &app_desc.id)
.replace("{permissions}", &permissions)
.replace(
"{runtime}",
&app_desc
.flatpak_runtime_version
.as_ref()
.map(|s| format!("\"{}\"", s))
.unwrap_or_else(|| "\"43\"".to_string()),
)
.replace(
"{modules}",
&app_desc
.flatpak_modules
.as_ref()
.map(|modules| modules.join("\n"))
.unwrap_or_default(),
)
.as_bytes(),
)?;
Ok(())
}
fn as_tag_list<T>(
elements: Option<&Vec<T>>,
to_tag: impl Fn(&T) -> String + 'static,
indent: Option<usize>,
) -> Option<String> {
elements.map(|v| {
v.iter()
.map(to_tag)
.collect::<Vec<String>>()
.join(&format!("\n{}", &" ".repeat(indent.unwrap_or(0))))
})
}
fn to_tag(value: &str, tagname: &str, linebreaks: bool, indentation: usize) -> String {
if linebreaks {
format!(
"{0}<{1}>\n{2}\n{0}</{1}>",
" ".repeat(indentation),
tagname,
value
)
} else {
format!("{0}<{1}>{2}</{1}>", " ".repeat(indentation), tagname, value)
}
}
fn create_app_descriptor_xml(descriptor: &ProjectDescriptor, path: &Path) -> std::io::Result<()> {
let template = include_str!("../../data/appdata.template.xml");
let app_desc = &descriptor.app;
let mut path = PathBuf::from(path);
path.push(format!("{}.appdata.xml", app_desc.id));
println!("[gra] Generate {:?}", path);
let mut file = File::create(path)?;
file.write_all(
template
.replace("{id}", &app_desc.id)
.replace("{name}", descriptor
.app
.name
.as_ref()
.unwrap_or(&descriptor.package.name))
.replace("{summary}", &app_desc.summary)
.replace("{description}", &app_desc.description.replace('\n', "\n "))
.replace(
"{license}",
descriptor.package.license.as_ref().unwrap_or(&"".into()),
)
.replace(
"{homepage}",
descriptor.package.homepage.as_ref().unwrap_or(&"".into()),
)
.replace(
"{repository}",
descriptor.package.repository.as_ref().unwrap_or(&"".into()),
)
.replace("{metadata_license}", &app_desc.metadata_license)
.replace(
"{recommends}",
&as_tag_list(Some(&app_desc.recommends), serialize_recommend,
None)
.map(|s| to_tag(&s, "recommends", true, 4))
.unwrap_or_else(|| "".into())
)
.replace(
"{requires}",
&as_tag_list(Some(&app_desc.requires), serialize_recommend,
None)
.map(|s| to_tag(&s, "requires", true, 4))
.unwrap_or_else(|| "".into())
)
.replace(
"{categories}",
&as_tag_list(Some(&app_desc.categories), |category| {
to_tag(category, "category", false, 8)
},None)
.map(|s| to_tag(&s, "categories", true, 4))
.unwrap_or_else(|| "".into()),
)
.replace(
"{releases}",
&as_tag_list(app_desc.releases.as_ref(), |r| {
format!(r#"
<release version="{}" date="{}">
<description>
{}
</description>
</release>"#,
r.version,
r.date,
r.description.replace('\n', "\n ")
)
},Some(4))
.map(|s| to_tag(&s, "releases", true, 4))
.unwrap_or_else(|| "".into()),
)
.replace(
"{screenshots}",
&as_tag_list(app_desc.screenshots.as_ref(), |s| {
format!(" <screenshot {}><image type=\"source\">{}</image></screenshot>",
s.type_.as_ref().map(|t| format!("type=\"{}\"", t)).unwrap_or_else(|| "".into()),
s.url
)
},None)
.map(|s| to_tag(&s, "screenshots", true, 4))
.unwrap_or_else(|| "".into()),
)
.replace(
"{author}",
&to_tag(&descriptor.package.authors.as_ref().map(|authors| authors
.first()
.map(|name|
name.split_once('<')
.map(|t| t.0.trim().to_string())
.unwrap_or_else(|| name.clone())
).unwrap_or_else(|| "".into())).unwrap_or_else(|| "".into())
, "developer_name", false, 0)
)
.replace(
"{content_rating}",
&as_tag_list(app_desc.content_rating.as_ref(), |c| {
format!(" <content_attribute type=\"{}\">{}</content_attribute>", c.id, c.value)
},None)
.map(|cr| format!(" <content_rating type=\"oars-1.1\">\n{}\n </content_rating>", cr))
.unwrap_or_else(|| " <content_rating type=\"oars-1.1\" />".into())
)
.as_bytes(),
)?;
Ok(())
}
fn serialize_recommend(re: &crate::Recommend) -> String {
match re {
crate::Recommend::Display(v) => {
if let Some(v) = v.strip_prefix('>') {
format!(" <display compare=\"gt\">{}</display>", v)
} else if let Some(v) = v.strip_prefix(">=") {
format!(" <display compare=\"ge\">{}</display>", v)
} else if let Some(v) = v.strip_prefix('<') {
format!(" <display compare=\"lt\">{}</display>", v)
} else if let Some(v) = v.strip_prefix("<=") {
format!(" <display compare=\"le\">{}</display>", v)
} else if let Some(v) = v.strip_prefix("==") {
format!(" <display compare=\"eq\">{}</display>", v)
} else if let Some(v) = v.strip_prefix("!=") {
format!(" <display compare=\"ne\">{}</display>", v)
} else {
format!(" <display>{}</display>", v)
}
}
crate::Recommend::DisplayLength(v) => {
if let Some(v) = v.strip_prefix('>') {
format!(
" <display_length compare=\"gt\">{}</display_length>",
v
)
} else if let Some(v) = v.strip_prefix(">=") {
format!(
" <display_length compare=\"ge\">{}</display_length>",
v
)
} else if let Some(v) = v.strip_prefix('<') {
format!(
" <display_length compare=\"lt\">{}</display_length>",
v
)
} else if let Some(v) = v.strip_prefix("<=") {
format!(
" <display_length compare=\"le\">{}</display_length>",
v
)
} else if let Some(v) = v.strip_prefix("==") {
format!(
" <display_length compare=\"eq\">{}</display_length>",
v
)
} else if let Some(v) = v.strip_prefix("!=") {
format!(
" <display_length compare=\"ne\">{}</display_length>",
v
)
} else {
format!(" <display_length>{}</display_length>", v)
}
}
crate::Recommend::Control(v) => to_tag(v, "control", false, 8),
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, env::temp_dir, vec};
use crate::AppDescriptor;
use super::*;
fn desc() -> ProjectDescriptor {
ProjectDescriptor {
package: crate::PackageDescriptor {
name: String::from("example"),
version: String::from("0.1.0"),
authors: Some(vec![String::from("Foo Bar")]),
homepage: Some(String::from("https://foo.bar")),
license: None,
repository: None,
},
app: AppDescriptor {
id: String::from("org.example.Test"),
name: Some(String::from("Test")),
generic_name: Some(String::from("Test")),
summary: String::from("This is a test"),
description: String::from("This is a test description"),
categories: vec![String::from("GTK")],
metadata_license: String::from("Foo"),
screenshots: None,
releases: None,
content_rating: None,
requires: vec![],
recommends: vec![],
permissions: vec!["share=network".into(), "socket=x11".into()],
resources: None,
flatpak_modules: None,
flatpak_runtime_version: None,
mimetype: None,
},
actions: Some(HashMap::new()),
settings: Some(HashMap::new()),
}
}
#[test]
fn test_flatpak_dev_manifest_generation() {
let temp = temp_dir().join("test_flatpak_dev_manifest_generation");
create_dir_all(&temp).unwrap();
let dev_flatpak_manifest = include_str!("../../data/flatpak-dev.template.yml");
create_flatpak_yml(&desc(), temp.as_path(), dev_flatpak_manifest, None)
.expect("Could not generate org.example.Test.yml");
let content = std::fs::read_to_string(temp.join("org.example.Test.yml")).unwrap();
let unreplaced_tag = regex::Regex::new(r"\{.*\}").unwrap();
assert!(!unreplaced_tag.is_match(&content));
}
#[test]
fn test_flatpak_prod_manifest_generation() {
let temp = temp_dir().join("test_flatpak_prod_manifest_generation");
create_dir_all(&temp).unwrap();
let manifest = include_str!("../../data/flatpak.template.yml");
create_flatpak_yml(&desc(), temp.as_path(), manifest, None)
.expect("Could not generate org.example.Test.yml");
let mut content = std::fs::read_to_string(temp.join("org.example.Test.yml")).unwrap();
let unreplaced_tag = regex::Regex::new(r"(?m)\{(.*)\}").unwrap();
assert!(unreplaced_tag.is_match(&content));
assert!(content.contains("{sources}"));
content = content.replace("{sources}", "");
assert!(!unreplaced_tag.is_match(&content));
}
}