use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs;
use std::path;
use std::process::Command;
use serde::{Deserialize, Serialize};
use crate::build_system::FlatpakBuildSystem;
use crate::format::FlatpakManifestFormat;
use crate::source::{FlatpakSourceItem, FlatpakSourceType};
#[derive(Clone)]
#[derive(Deserialize)]
#[derive(Serialize)]
#[derive(Debug)]
#[derive(Hash)]
#[serde(untagged)]
pub enum FlatpakModuleItem {
Path(String),
Description(FlatpakModule),
}
#[derive(Clone)]
#[derive(Deserialize)]
#[derive(Serialize)]
#[derive(Debug)]
#[derive(Default)]
#[derive(Hash)]
#[serde(rename_all = "kebab-case")]
#[serde(default)]
pub struct FlatpakModule {
#[serde(skip_serializing)]
pub format: FlatpakManifestFormat,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub sources: Vec<FlatpakSourceItem>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub config_opts: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub make_args: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub make_install_args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rm_configure: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_autogen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_parallel_make: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub install_rule: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_make_install: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_python_timestamp_fix: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmake: Option<bool>,
#[serde(deserialize_with = "crate::build_system::FlatpakBuildSystem::deserialize")]
#[serde(serialize_with = "crate::build_system::FlatpakBuildSystem::serialize")]
#[serde(skip_serializing_if = "Option::is_none")]
pub buildsystem: Option<FlatpakBuildSystem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub builddir: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub subdir: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_options: Option<FlatpakBuildOptions>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub build_commands: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub post_install: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cleanup: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ensure_writable: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub only_arches: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub skip_arches: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cleanup_platform: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_tests: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub test_rule: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub test_commands: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub modules: Vec<FlatpakModuleItem>,
}
impl FlatpakModule {
pub fn uses_external_data_checker(&self) -> bool {
for source in &self.sources {
let source_description = match source {
FlatpakSourceItem::Description(d) => d,
FlatpakSourceItem::Path(_) => continue,
};
if source_description.x_checker_data.is_some() {
return true;
}
}
return false;
}
pub fn get_mirror_urls(&self) -> Vec<String> {
let mut mirror_urls = vec![];
for module in &self.modules {
if let FlatpakModuleItem::Description(module_description) = module {
mirror_urls.append(&mut module_description.get_mirror_urls());
}
}
for source in &self.sources {
let source_description = match source {
FlatpakSourceItem::Description(d) => d,
FlatpakSourceItem::Path(_p) => continue,
};
for url in source_description.get_mirror_urls() {
mirror_urls.push(url.to_string());
}
}
mirror_urls
}
pub fn get_buildsystem(&self) -> Option<String> {
if self.cmake.unwrap_or(false) {
return Some(crate::build_system::CMAKE.to_string());
}
if let Some(buildsystem) = &self.buildsystem {
return Some(buildsystem.to_string());
}
None
}
pub fn is_patched(&self) -> bool {
for source in &self.sources {
if let FlatpakSourceItem::Description(sd) = source {
if sd.get_type() == Some(FlatpakSourceType::Patch) {
return true;
}
}
}
false
}
pub fn load_from_file(path: String) -> Result<FlatpakModule, String> {
let file_path = path::Path::new(&path);
if !file_path.is_file() {
return Err(format!("{} is not a file.", path));
}
let manifest_format = match FlatpakManifestFormat::from_path(&path) {
Some(f) => f,
None => return Err(format!("{} is not a Flatpak module manifest.", path)),
};
let manifest_content = match fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => {
return Err(format!("Could not read module manifest at {}: {}", &path, e));
}
};
match FlatpakModule::parse(manifest_format, &manifest_content) {
Ok(m) => Ok(m),
Err(e) => Err(format!(
"Failed to parse Flatpak module manifest at {}: {}",
path, e
)),
}
}
pub fn parse(format: FlatpakManifestFormat, manifest_content: &str) -> Result<FlatpakModule, String> {
let mut flatpak_module: FlatpakModule = match format.parse(manifest_content) {
Ok(m) => m,
Err(e) => {
return Err(format!("Failed to parse the Flatpak module manifest: {}.", e));
}
};
flatpak_module.format = format;
if flatpak_module.name.is_empty() {
return Err("Required top-level field name is missing from Flatpak module.".to_string());
}
if flatpak_module.sources.is_empty() {
return Err("Required sources were not found in Flatpak module.".to_string());
}
for source in &flatpak_module.sources {
let source_path = match source {
FlatpakSourceItem::Description(_) => continue,
FlatpakSourceItem::Path(p) => p,
};
if source_path.starts_with("http://") || source_path.starts_with("https://") {
return Err("Sources provided as strings cannot be URLs!".to_string());
}
}
for source in &flatpak_module.sources {
let source_description = match source {
FlatpakSourceItem::Path(_) => continue,
FlatpakSourceItem::Description(d) => d,
};
if let Err(e) = source_description.is_valid() {
return Err(e);
}
}
Ok(flatpak_module)
}
pub fn dump(&self) -> Result<String, String> {
match self.format.dump(self) {
Ok(d) => Ok(d),
Err(e) => return Err(format!("Failed to dump the Flatpak manifest: {}.", e)),
}
}
pub fn file_path_matches(path: &str) -> bool {
crate::filename::extension_is_valid(path)
}
pub fn get_urls(
&self,
include_mirror_urls: bool,
include_source_types: Option<Vec<FlatpakSourceType>>,
) -> Vec<String> {
let mut urls = vec![];
for module in &self.modules {
if let FlatpakModuleItem::Description(module_description) = module {
urls.append(
&mut module_description.get_urls(include_mirror_urls, include_source_types.clone()),
);
}
}
for source in &self.sources {
let source_description = match source {
FlatpakSourceItem::Description(d) => d,
FlatpakSourceItem::Path(_p) => continue,
};
for url in source_description.get_urls(include_mirror_urls, include_source_types.clone()) {
urls.push(url);
}
}
urls
}
pub fn get_max_depth(&self) -> i32 {
let mut max_depth: i32 = 0;
for module in &self.modules {
if let FlatpakModuleItem::Description(module_description) = module {
let module_depth = module_description.get_max_depth();
if module_depth > max_depth {
max_depth = module_depth;
}
}
}
return max_depth + 1;
}
pub fn get_main_url(&self) -> Option<String> {
if self.sources.len() < 1 {
return None;
}
let main_module_source = self.sources.first().unwrap();
let main_module_source_description = match main_module_source {
FlatpakSourceItem::Description(d) => d,
FlatpakSourceItem::Path(_p) => return None,
};
let main_module_source_url: Option<String> = main_module_source_description.get_url();
match &main_module_source_url {
Some(s) => Some(s.to_string()),
None => None,
}
}
pub fn get_all_modules_recursively(&self) -> Vec<&FlatpakModuleItem> {
let mut all_modules: Vec<&FlatpakModuleItem> = vec![];
let mut next_modules: Vec<&FlatpakModuleItem> = vec![];
for module in &self.modules {
next_modules.push(module);
}
while !next_modules.is_empty() {
let module = next_modules.pop().unwrap();
all_modules.push(module);
let module = match module {
FlatpakModuleItem::Description(d) => d,
FlatpakModuleItem::Path(_) => continue,
};
for next_module in &module.modules {
next_modules.push(next_module);
}
}
all_modules
}
pub fn is_composite(&self) -> bool {
let mut code_sources_count = 0;
for source in &self.sources {
let source_description = match source {
FlatpakSourceItem::Description(d) => d,
FlatpakSourceItem::Path(_) => continue,
};
let source_type = match source_description.get_type() {
Some(t) => t,
None => continue,
};
if source_type.is_code() {
code_sources_count += 1;
}
if code_sources_count > 1 {
return true;
}
}
false
}
pub fn get_commands<I, S>(
&self,
args: I,
reconfigure: bool,
root_path: &str,
build_path: &str,
out_path: Option<&str>,
num_cpus: i64,
) -> Vec<Command>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args: Vec<_> = args.into_iter().collect();
let out_path = out_path.unwrap_or("/app");
let build_system = self
.buildsystem
.to_owned()
.unwrap_or(FlatpakBuildSystem::default());
match build_system {
FlatpakBuildSystem::Autotools => {
let mut commands = Vec::new();
if !reconfigure {
let mut cmd = Command::new("./configure");
cmd.arg(format!("--prefix={}", out_path));
cmd.args(self.config_opts.clone());
cmd.current_dir(root_path);
commands.push(cmd);
}
let mut cmd = Command::new("make");
cmd.args(&["-p", "-n", "-s"]);
cmd.current_dir(root_path);
commands.push(cmd);
let mut cmd = Command::new("make");
cmd.arg("V=0");
cmd.arg(format!("-j{}", num_cpus));
cmd.arg("install");
cmd.args(args);
cmd.current_dir(root_path);
commands.push(cmd);
commands
}
FlatpakBuildSystem::CMake | FlatpakBuildSystem::CMakeNinja => {
let mut commands = Vec::new();
if !reconfigure {
let mut cmd = Command::new("mkdir");
cmd.arg("-p").arg(build_path);
commands.push(cmd);
let mut cmd = Command::new("cmake");
cmd.args(&["-G", "Ninja", "..", "."]);
cmd.arg("-DCMAKE_EXPORT_COMPILE_COMMANDS=1");
cmd.arg("-DCMAKE_BUILD_TYPE=RelWithDebInfo");
cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", out_path));
cmd.args(self.config_opts.clone());
cmd.current_dir(build_path);
commands.push(cmd);
}
let mut cmd = Command::new("ninja");
cmd.current_dir(build_path);
commands.push(cmd);
let mut cmd = Command::new("ninja");
cmd.arg("install");
cmd.current_dir(build_path);
commands.push(cmd);
commands
}
FlatpakBuildSystem::Meson => {
let mut commands = Vec::new();
if !reconfigure {
let mut cmd = Command::new("meson");
cmd.arg(format!("--prefix={}", out_path));
cmd.arg(build_path);
cmd.args(self.config_opts.clone());
cmd.current_dir(root_path);
commands.push(cmd);
}
let mut cmd = Command::new("ninja");
cmd.arg("-C");
cmd.arg(build_path);
cmd.current_dir(root_path);
commands.push(cmd);
let mut cmd = Command::new("meson");
cmd.args(args);
cmd.args(&["install", "-C"]);
cmd.arg(build_path);
cmd.current_dir(root_path);
commands.push(cmd);
commands
}
FlatpakBuildSystem::QMake => panic!("Not implemented yet."),
FlatpakBuildSystem::Simple => self
.build_commands
.iter()
.map(|step| {
let mut cmd = Command::new("/bin/sh");
cmd.arg("-c");
cmd.arg(step);
cmd.current_dir(root_path);
cmd
})
.collect(),
}
}
}
#[derive(Clone)]
#[derive(Deserialize)]
#[derive(Serialize)]
#[derive(Debug)]
#[derive(Hash)]
#[serde(untagged)]
pub enum FlatpakBuildOptionsEnv {
Dict(BTreeMap<String, String>),
Array(Vec<String>),
}
impl Default for FlatpakBuildOptionsEnv {
fn default() -> Self {
FlatpakBuildOptionsEnv::Array(vec![])
}
}
#[derive(Clone)]
#[derive(Deserialize)]
#[derive(Serialize)]
#[derive(Debug)]
#[derive(Default)]
#[derive(Hash)]
#[serde(rename_all = "kebab-case")]
#[serde(default)]
pub struct FlatpakBuildOptions {
#[serde(skip_serializing_if = "String::is_empty")]
pub cflags: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cflags_override: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub cppflags: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cppflags_override: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub cxxflags: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cxxflags_override: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub ldflags: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ldflags_override: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
pub prefix: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub libdir: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub append_path: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub prepend_path: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub append_ld_library_path: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub prepend_ld_library_path: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub append_pkg_config_path: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub prepend_pkg_config_path: String,
pub env: FlatpakBuildOptionsEnv,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub build_args: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub test_args: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub config_opts: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub make_args: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub make_install_args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strip: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_debuginfo: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_debuginfo_compression: Option<bool>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub arch: BTreeMap<String, FlatpakBuildOptions>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn test_parse_build_options() {
let module_manifest = r###"
name: "flatpak-rs"
buildsystem: simple
cleanup: [ "*" ]
build-options:
cflags: "-O2 -g"
cxxflags: "-O2 -g"
env:
V: "1"
X: "2"
arch:
x86_64:
cflags: "-O3 -g"
config-opts: []
sources:
-
"shared-modules/linux-audio/lv2.json"
"###;
match FlatpakModule::parse(FlatpakManifestFormat::YAML, module_manifest) {
Err(e) => std::panic::panic_any(e),
Ok(module) => {
assert_eq!(module.name, "flatpak-rs");
assert!(module.build_options.is_some());
let env: BTreeMap<String, String> = match module.build_options.unwrap().env {
FlatpakBuildOptionsEnv::Dict(env) => env,
FlatpakBuildOptionsEnv::Array(env) => panic!("Env should be a dict."),
};
assert_eq!(env.get("V").unwrap(), "1");
assert_eq!(env.get("X").unwrap(), "2");
}
}
}
#[test]
pub fn test_parse_build_options_env() {
let module_manifest = r###"
name: "flatpak-rs"
buildsystem: simple
cleanup: [ "*" ]
build-options:
cflags: "-O2 -g"
cxxflags: "-O2 -g"
env:
- "V=1"
- "Y=2"
arch:
x86_64:
cflags: "-O3 -g"
config-opts: []
sources:
-
"shared-modules/linux-audio/lv2.json"
"###;
match FlatpakModule::parse(FlatpakManifestFormat::YAML, module_manifest) {
Err(e) => std::panic::panic_any(e),
Ok(module) => {
assert_eq!(module.name, "flatpak-rs");
assert!(module.build_options.is_some());
let env: Vec<String> = match module.build_options.unwrap().env {
FlatpakBuildOptionsEnv::Array(env) => env,
FlatpakBuildOptionsEnv::Dict(env) => panic!("Env should be an array."),
};
assert_eq!(env, vec!["V=1", "Y=2"]);
}
}
}
#[test]
#[ignore]
pub fn test_parse_builddir() {
let module_manifest = r###"
name: fmt
buildsystem: cmake-ninja
builddir: yes
config-opts:
- "-DFMT_TEST=OFF"
sources:
- type: git
url: https://github.com/fmtlib/fmt.git
commit: 561834650aa77ba37b15f7e5c9d5726be5127df9
"###;
match FlatpakModule::parse(FlatpakManifestFormat::YAML, module_manifest) {
Err(e) => std::panic::panic_any(e),
Ok(module) => {
assert_eq!(module.name, "fmt");
}
}
}
#[test]
pub fn test_parse_extra_data() {
let module_manifest = r###"
name: wps
buildsystem: simple
build-commands:
- install -Dm755 apply_extra.sh /app/bin/apply_extra
- install -Dm755 wps.sh /app/bin/wps
- ln -s wps /app/bin/et
- ln -s wps /app/bin/wpp
- ln -s wps /app/bin/wpspdf
- install -Dm644 ${FLATPAK_ID}.metainfo.xml -t /app/share/metainfo/
- install -Dm755 /usr/bin/desktop-file-edit -t /app/bin/
- install -Dm755 /usr/bin/ar -t /app/bin/
- install -Dm755 /usr/lib/$(gcc -print-multiarch)/libbfd-*.so -t /app/lib/
sources:
- type: file
path: apply_extra.sh
- type: file
path: com.wps.Office.metainfo.xml
- type: file
path: wps.sh
- type: extra-data
filename: wps-office.deb
only-arches:
- x86_64
url: https://wdl1.pcfg.cache.wpscdn.com/wps-office_11.1.0.10702.XA_amd64.deb
sha256: 390a8b358aaccdfda54740d10d5306c2543c5cd42a7a8fd5c776ccff38492992
size: 275210770
installed-size: 988671247
x-checker-data:
type: html
url: https://linux.wps.com/js/meta.js
version-pattern: version\s*=\s*"([\d.-]+)"
url-pattern: download_link_deb\s*=\s*"(http[s]?://[\w\d$-_@.&+]+)"
"###;
let module = FlatpakModule::parse(FlatpakManifestFormat::YAML, module_manifest).unwrap();
assert_eq!(module.name, "wps");
}
#[test]
pub fn test_parse_helm_files() {
let helm_file = r###"
name: wps
sources:
- "https://github.com/user/repo"
"###;
assert!(FlatpakModule::parse(FlatpakManifestFormat::YAML, helm_file,).is_err())
}
#[test]
pub fn test_parse_invalid_source() {
let file = r###"
name: wps
sources:
- path: "^empty\\.c$"
isGenerated: false
sourceGroupName: "Source Files",
compileGroupLanguage: C
"###;
assert!(FlatpakModule::parse(FlatpakManifestFormat::YAML, file).is_err())
}
#[test]
pub fn test_parse_no_buildsystem() {
let module_manifest = r###"
name: dbus-glib
sources:
- type: archive
url: "https://dbus.freedesktop.org/releases/dbus-glib/dbus-glib-0.110.tar.gz"
sha256: 7ce4760cf66c69148f6bd6c92feaabb8812dee30846b24cd0f7395c436d7e825
config-opts:
- "--disable-static"
- "--disable-gtk-doc"
cleanup:
- "*.la"
- /bin
- /etc
- /include
- /libexec
- /share/gtk-doc
- /share/man
"###;
let flatpak_application = FlatpakModule::parse(FlatpakManifestFormat::YAML, module_manifest).unwrap();
assert!(flatpak_application.buildsystem.is_none());
let application_dump = flatpak_application.dump().unwrap();
assert!(!application_dump.contains("buildsystem"))
}
}