flatpak_rs/
application.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path;
4
5use serde::{Deserialize, Serialize};
6
7use crate::format::FlatpakManifestFormat;
8use crate::module::{FlatpakBuildOptions, FlatpakModule, FlatpakModuleItem};
9use crate::source::FlatpakSourceType;
10
11/// Main structure for a Flatpak application manifest.
12/// See `man flatpak-manifest` for the flatpak manifest specs.
13#[derive(Clone)]
14#[derive(Deserialize)]
15#[derive(Serialize)]
16#[derive(Debug)]
17#[derive(Hash)]
18#[derive(Default)]
19#[serde(rename_all = "kebab-case")]
20#[serde(default)]
21pub struct FlatpakApplication {
22    #[serde(skip_serializing)]
23    pub format: FlatpakManifestFormat,
24
25    /// Name of the application.
26    #[serde(skip_serializing_if = "String::is_empty")]
27    pub app_name: String,
28
29    /// A string defining the application id.
30    /// Both names (app-id and id) are accepted.
31    #[serde(skip_serializing_if = "String::is_empty")]
32    pub app_id: String,
33    #[serde(skip_serializing_if = "String::is_empty")]
34    pub id: String,
35
36    /// The branch to use when exporting the application.
37    /// If this is unset the defaults come from the default-branch option.
38    ///
39    /// This key overrides both the default-branch key, and the --default-branch commandline option.
40    /// Unless you need a very specific branchname (like for a runtime or an extension) it is recommended
41    /// to use the default-branch key instead, because you can then override the default using
42    /// --default-branch when building for instance a test build.
43    #[serde(skip_serializing_if = "String::is_empty")]
44    pub branch: String,
45
46    /// The default branch to use when exporting the application. Defaults to master.
47    /// This key can be overridden by the --default-branch commandline option.
48    #[serde(skip_serializing_if = "String::is_empty")]
49    pub default_branch: String,
50
51    /// The collection ID of the repository, defaults to being unset.
52    /// Setting a globally unique collection ID allows the apps in the
53    /// repository to be shared over peer to peer systems without needing further configuration.
54    /// If building in an existing repository, the collection ID must match the existing
55    /// configured collection ID for that repository.
56    #[serde(skip_serializing_if = "String::is_empty")]
57    pub collection_id: String,
58
59    /// The name of the runtime that the application uses.
60    #[serde(skip_serializing_if = "String::is_empty")]
61    pub runtime: String,
62
63    /// The version of the runtime that the application uses, defaults to master.
64    #[serde(skip_serializing_if = "String::is_empty")]
65    pub runtime_version: String,
66
67    /// The name of the development runtime that the application builds with.
68    #[serde(skip_serializing_if = "String::is_empty")]
69    pub sdk: String,
70
71    /// The name of the development extensions that the application requires to build.
72    #[serde(skip_serializing_if = "Vec::is_empty")]
73    pub sdk_extensions: Vec<String>,
74
75    /// Initialize the (otherwise empty) writable /var in the build with a copy of this runtime.
76    #[serde(skip_serializing_if = "String::is_empty")]
77    pub var: String,
78
79    /// Use this file as the base metadata file when finishing.
80    #[serde(skip_serializing_if = "String::is_empty")]
81    pub metadata: String,
82
83    /// Build a new runtime instead of an application.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub build_runtime: Option<bool>,
86
87    /// Whether the manifest describes an extension to be used by other manifests.
88    /// Extensions can be used to bundle programming langages and their associated
89    /// tools, for example.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub build_extension: Option<bool>,
92
93    /// Start with the files from the specified application.
94    /// This can be used to create applications that extend another application.
95    #[serde(skip_serializing_if = "String::is_empty")]
96    pub base: String,
97
98    /// Use this specific version of the application specified in base.
99    /// If unspecified, this uses the value specified in branch
100    #[serde(skip_serializing_if = "String::is_empty")]
101    pub base_version: String,
102
103    /// Install these extra extensions from the base application when
104    /// initializing the application directory.
105    #[serde(skip_serializing_if = "Vec::is_empty")]
106    pub base_extensions: Vec<String>,
107
108    /// Separate out locale files and translations to an extension runtime. Defaults to true.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub separate_locales: Option<bool>,
111
112    /// Run appstream-compose during cleanup phase. Defaults to true.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub appstream_compose: Option<bool>,
115
116    /// Inherit these extra extensions points from the base application or
117    /// sdk when finishing the build.
118    #[serde(skip_serializing_if = "Vec::is_empty")]
119    pub inherit_extensions: Vec<String>,
120
121    /// Inherit these extra extensions points from the base application or sdk
122    /// when finishing the build, but do not inherit them into the platform.
123    #[serde(skip_serializing_if = "Vec::is_empty")]
124    pub inherit_sdk_extensions: Vec<String>,
125
126    /// Inherit these extra extensions points from the base application or sdk when finishing the build,
127    /// but do not inherit them into the platform.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub build_options: Option<FlatpakBuildOptions>,
130
131    /// The name of the command that the flatpak should run on execution.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub command: Option<String>,
134
135    /// Add these tags to the metadata file.
136    #[serde(skip_serializing_if = "Vec::is_empty")]
137    pub tags: Vec<String>,
138
139    /// This is a dictionary of extension objects.
140    /// The key is the name of the extension.
141    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
142    pub add_extensions: BTreeMap<String, FlatpakExtension>,
143
144    /// This is a dictionary of extension objects similar to add-extensions.
145    /// The main difference is that the extensions are added early and are
146    /// available for use during the build.
147    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
148    pub add_build_extensions: BTreeMap<String, FlatpakExtension>,
149
150    /// An array of file patterns that should be removed at the end.
151    /// Patterns starting with / are taken to be full pathnames (without the /app prefix),
152    /// otherwise they just match the basename.
153    #[serde(skip_serializing_if = "Vec::is_empty")]
154    pub cleanup: Vec<String>,
155
156    /// An array of commandlines that are run during the cleanup phase.
157    #[serde(skip_serializing_if = "Vec::is_empty")]
158    pub cleanup_commands: Vec<String>,
159
160    /// Extra files to clean up in the platform.
161    #[serde(skip_serializing_if = "Vec::is_empty")]
162    pub cleanup_platform: Vec<String>,
163
164    /// An array of commandlines that are run during the cleanup phase of the platform.
165    #[serde(skip_serializing_if = "Vec::is_empty")]
166    pub cleanup_platform_commands: Vec<String>,
167
168    /// An array of commandlines that are run after importing the base platform,
169    /// but before applying the new files from the sdk. This is a good place to e.g. delete
170    /// things from the base that may conflict with the files added in the sdk.
171    #[serde(skip_serializing_if = "Vec::is_empty")]
172    pub prepare_platform_commands: Vec<String>,
173
174    /// An array of arguments passed to the flatpak build-finish command.
175    #[serde(skip_serializing_if = "Vec::is_empty")]
176    pub finish_args: Vec<String>,
177
178    /// Any desktop file with this name will be renamed to a name
179    /// based on id during the cleanup phase.
180    #[serde(skip_serializing_if = "String::is_empty")]
181    pub rename_desktop_file: String,
182
183    /// Any appdata file with this name will be renamed to a name based
184    /// on id during the cleanup phase.
185    #[serde(skip_serializing_if = "String::is_empty")]
186    pub rename_appdata_file: String,
187
188    /// Any icon with this name will be renamed to a name based on id during
189    /// the cleanup phase. Note that this is the icon name, not the full filenames,
190    /// so it should not include a filename extension.
191    #[serde(skip_serializing_if = "String::is_empty")]
192    pub rename_icon: String,
193
194    /// Replace the appdata project-license field with this string.
195    /// This is useful as the upstream license is typically only about
196    /// the application itself, whereas the bundled app can contain other
197    /// licenses too.
198    #[serde(skip_serializing_if = "String::is_empty")]
199    pub appdata_license: String,
200
201    /// If rename-icon is set, keep a copy of the old icon file.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub copy_icon: Option<bool>,
204
205    /// This string will be prefixed to the Name key in the main application desktop file.
206    #[serde(skip_serializing_if = "String::is_empty")]
207    pub desktop_file_name_prefix: String,
208
209    /// This string will be suffixed to the Name key in the main application desktop file.
210    #[serde(skip_serializing_if = "String::is_empty")]
211    pub desktop_file_name_suffix: String,
212
213    /// An array of strings specifying the modules to be built in order.
214    /// String members in the array are interpreted as the name of a separate
215    /// json or yaml file that contains a module. See below for details.
216    #[serde(skip_serializing_if = "Vec::is_empty")]
217    pub modules: Vec<FlatpakModuleItem>,
218}
219impl FlatpakApplication {
220    pub fn get_id(&self) -> String {
221        if !self.app_id.is_empty() {
222            return self.app_id.to_string();
223        }
224        return self.id.to_string();
225    }
226
227    pub fn load_from_file(path: String) -> Result<FlatpakApplication, String> {
228        let file_path = path::Path::new(&path);
229        if !file_path.is_file() {
230            return Err(format!("{} is not a file.", path));
231        }
232
233        let manifest_format = match FlatpakManifestFormat::from_path(&path) {
234            Some(f) => f,
235            None => return Err(format!("{} is not a Flatpak application manifest.", path)),
236        };
237
238        let manifest_content = match fs::read_to_string(file_path) {
239            Ok(content) => content,
240            Err(e) => {
241                return Err(format!("Could not read application manifest at {}: {}", &path, e));
242            }
243        };
244        match FlatpakApplication::parse(manifest_format, &manifest_content) {
245            Ok(m) => Ok(m),
246            Err(e) => Err(format!(
247                "Failed to parse Flatpak application manifest at {}: {}",
248                path, e
249            )),
250        }
251    }
252
253    pub fn file_extension_matches(path: &str) -> bool {
254        crate::filename::extension_is_valid(&path)
255    }
256
257    pub fn file_path_matches(path: &str) -> bool {
258        crate::reverse_dns::is_reverse_dns(&path)
259    }
260
261    pub fn parse(format: FlatpakManifestFormat, manifest_content: &str) -> Result<FlatpakApplication, String> {
262        let mut flatpak_manifest: FlatpakApplication = match format.parse(manifest_content) {
263            Ok(m) => m,
264            Err(e) => {
265                return Err(format!(
266                    "Failed to parse the Flatpak application manifest: {}.",
267                    e
268                ));
269            }
270        };
271        flatpak_manifest.format = format;
272
273        // From https://docs.flatpak.org/en/latest/manifests.html#basic-properties:
274        // Each manifest file should specify basic information about the application that is to be built,
275        // including the app-id, runtime, runtime-version, sdk and command parameters.
276        // That being said, the `command` field is not required by manifests that declare an
277        // extension to be build, using the `build-extension` field.
278        if flatpak_manifest.app_id.is_empty() && flatpak_manifest.id.is_empty() {
279            return Err(
280                "Required top-level field id (or app-id) is missing from Flatpak manifest.".to_string(),
281            );
282        }
283        if flatpak_manifest.runtime.is_empty() {
284            return Err("Required top-level field runtime is missing from Flatpak manifest.".to_string());
285        }
286        if flatpak_manifest.runtime_version.is_empty() {
287            return Err(
288                "Required top-level field runtime-version is missing from Flatpak manifest.".to_string(),
289            );
290        }
291        if flatpak_manifest.sdk.is_empty() {
292            return Err("Required top-level field sdk is missing from Flatpak manifest.".to_string());
293        }
294
295        Ok(flatpak_manifest)
296    }
297
298    pub fn dump(&self) -> Result<String, String> {
299        match self.format.dump(self) {
300            Ok(d) => Ok(d),
301            Err(e) => return Err(format!("Failed to dump the Flatpak manifest: {}.", e)),
302        }
303    }
304
305    pub fn get_urls(
306        &self,
307        include_mirror_urls: bool,
308        include_source_types: Option<Vec<FlatpakSourceType>>,
309    ) -> Vec<String> {
310        let mut urls = vec![];
311        for module in &self.modules {
312            let module: &FlatpakModule = match module {
313                FlatpakModuleItem::Path(_) => continue,
314                FlatpakModuleItem::Description(m) => &m,
315            };
316            urls.append(&mut module.get_urls(include_mirror_urls, include_source_types.clone()));
317        }
318        urls
319    }
320
321    pub fn get_main_module_url(&self) -> Option<String> {
322        let main_module = match self.modules.last() {
323            Some(m) => m,
324            None => return None,
325        };
326        let main_module: &FlatpakModule = match main_module {
327            FlatpakModuleItem::Path(_) => return None,
328            FlatpakModuleItem::Description(m) => m,
329        };
330        return main_module.get_main_url();
331    }
332
333    pub fn get_max_depth(&self) -> i32 {
334        let mut max_depth: i32 = 1;
335        for module in &self.modules {
336            if let FlatpakModuleItem::Description(module_description) = module {
337                let module_depth = module_description.get_max_depth();
338                if module_depth > max_depth {
339                    max_depth = module_depth;
340                }
341            }
342        }
343        return max_depth;
344    }
345
346    pub fn is_extension(&self) -> bool {
347        if let Some(e) = self.build_extension {
348            return e;
349        }
350        false
351    }
352
353    pub fn get_all_modules_recursively(&self) -> Vec<&FlatpakModuleItem> {
354        let mut all_modules: Vec<&FlatpakModuleItem> = vec![];
355        for module in &self.modules {
356            all_modules.push(module);
357
358            let module = match module {
359                FlatpakModuleItem::Description(d) => d,
360                FlatpakModuleItem::Path(_) => continue,
361            };
362            for child_module in module.get_all_modules_recursively() {
363                all_modules.push(child_module);
364            }
365        }
366        all_modules
367    }
368}
369
370#[derive(Clone)]
371#[derive(Deserialize)]
372#[derive(Serialize)]
373#[derive(Debug)]
374#[derive(Default)]
375#[derive(Hash)]
376#[serde(rename_all = "kebab-case")]
377#[serde(default)]
378/// Extensions define extension points in the app/runtime that can be implemented by extensions,
379/// supplying extra files which are available during runtime.
380///
381/// Additionally the standard flatpak extension properties are supported, and put
382/// directly into the metadata file: autodelete, no-autodownload, subdirectories,
383/// add-ld-path, download-if, enable-if, merge-dirs, subdirectory-suffix, locale-subset,
384/// version, versions. See the flatpak metadata documentation for more information on these.
385pub struct FlatpakExtension {
386    /// The directory where the extension is mounted. If the extension point is for an application,
387    /// this path is relative to /app, otherwise it is relative to /usr.
388    pub extension_directory: String,
389
390    /// If this is true, then the data created in the extension directory is omitted from the result,
391    /// and instead packaged in a separate extension.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub bundle: Option<bool>,
394
395    /// If this is true, the extension is removed during when finishing.
396    /// This is only interesting for extensions in the add-build-extensions property.
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub remove_after_build: Option<bool>,
399
400    /// Whether to automatically delete extensions matching this extension point
401    /// when deleting a 'related' application or runtime.
402    pub autodelete: Option<bool>,
403
404    /// Whether to automatically download extensions matching this extension point
405    /// when updating or installing a 'related' application or runtime.
406    pub no_autodownload: Option<bool>,
407
408    /// If this key is set to true, then flatpak will look for extensions whose name is a
409    /// prefix of the extension point name, and mount them at the corresponding
410    /// name below the subdirectory.
411    pub subdirectories: Option<bool>,
412
413    /// A path relative to the extension point directory that will be appended to LD_LIBRARY_PATH.
414    pub add_ld_path: Option<String>,
415
416    /// A list of conditions, separated by semi-colons, that must be true for the extension
417    /// to be auto-downloaded.
418    /// These are the supported conditions:
419    ///  active-gl-driver
420    ///     Is true if the name of the active GL driver matches the extension point basename.
421    ///  active-gtk-theme
422    ///     Is true if the name of the current GTK theme (via org.gnome.desktop.interface GSetting)
423    ///     matches the extension point basename.
424    ///  have-intel-gpu
425    ///     Is true if the i915 kernel module is loaded.
426    ///  on-xdg-desktop-*
427    ///     Is true if the suffix (case-insensitively) is in the XDG_CURRENT_DESKTOP env var.
428    ///     For example on-xdg-desktop-GNOME-classic.
429    pub download_if: Option<String>,
430
431    /// A list of conditions, separated by semi-colons, that must be true for the extension to be
432    /// enabled. See download_if for available conditions.
433    pub enable_if: Option<String>,
434
435    /// A list of relative paths of directories below the extension point directory that will be merged.
436    pub merge_dirs: Option<String>,
437
438    /// A suffix that gets appended to the directory name.
439    /// This is very useful when the extension point naming scheme is "reversed".
440    /// For example, an extension point for GTK+ themes would be /usr/share/themes/$NAME/gtk-3.0,
441    /// which could be achieved using subdirectory-suffix=gtk-3.0.
442    pub subdirectory_suffix: Option<String>,
443
444    /// If set, then the extensions are partially downloaded by default, based on the currently
445    /// configured locales. This means that the extension contents should be
446    /// a set of directories with the language code as name.
447    pub locale_subset: Option<bool>,
448
449    /// The branch to use when looking for the extension.
450    /// If this is not specified, it defaults to the branch of the application or
451    /// runtime that the extension point is for.
452    pub version: Option<String>,
453
454    /// The branches to use when looking for the extension.
455    /// If this is not specified, it defaults to the branch of the application or
456    /// runtime that the extension point is for.
457    pub versions: Option<String>,
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    #[should_panic]
466    pub fn test_parse_invalid_yaml() {
467        FlatpakApplication::parse(FlatpakManifestFormat::YAML, "----------------------------").unwrap();
468    }
469
470    #[test]
471    pub fn test_parse_missing_fields() {
472        let application_manifest = r###"
473            runtime: org.gnome.Platform
474            runtime-version: "3.36"
475            sdk: org.gnome.Sdk
476            command: flatpak-rs
477        "###;
478        assert!(FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest).is_err());
479    }
480
481    #[test]
482    pub fn test_parse() {
483        let application_manifest = r###"
484            app-id: net.louib.flatpak-rs
485            runtime: org.gnome.Platform
486            runtime-version: "3.36"
487            sdk: org.gnome.Sdk
488            command: flatpak-rs
489            tags: ["nightly"]
490            modules:
491              -
492                name: "flatpak-rs"
493                buildsystem: simple
494                cleanup: [ "*" ]
495                config-opts: []
496                sources:
497                  -
498                    type: git
499                    url: https://github.com/louib/flatpak-rs.git
500                    branch: master
501        "###;
502        match FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest) {
503            Err(e) => std::panic::panic_any(e),
504            Ok(app) => {
505                assert_eq!(app.get_id(), "net.louib.flatpak-rs");
506            }
507        }
508    }
509
510    #[test]
511    pub fn test_parse_external_modules() {
512        let application_manifest = r###"
513            app-id: net.louib.flatpak-rs
514            runtime: org.gnome.Platform
515            runtime-version: "3.36"
516            sdk: org.gnome.Sdk
517            command: flatpak-rs
518            tags: ["nightly"]
519            modules:
520              -
521                name: "flatpak-rs"
522                buildsystem: simple
523                cleanup: [ "*" ]
524                config-opts: []
525                sources:
526                  -
527                    type: git
528                    url: https://github.com/louib/flatpak-rs.git
529                    branch: master
530              -
531                "shared-modules/linux-audio/lv2.json"
532        "###;
533        match FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest) {
534            Err(e) => std::panic::panic_any(e),
535            Ok(app) => {
536                assert_eq!(app.get_id(), "net.louib.flatpak-rs");
537            }
538        }
539    }
540
541    #[test]
542    pub fn test_parse_add_extensions() {
543        let application_manifest = r###"
544            app-id: net.pcsx2.PCSX2
545            runtime: org.freedesktop.Platform
546            runtime-version: "19.08"
547            sdk: org.freedesktop.Sdk
548            command: PCSX2
549            tags: ["nightly"]
550            modules: []
551            add-extensions:
552                "org.freedesktop.Platform.Compat.i386":
553                    directory: "lib/i386-linux-gnu"
554                    version: "19.08"
555                "org.freedesktop.Platform.Compat.i386.Debug":
556                    directory: "lib/debug/lib/i386-linux-gnu"
557                    version: "19.08"
558                    no-autodownload: true
559                "org.freedesktop.Platform.GL32":
560                    directory: "lib/i386-linux-gnu/GL"
561                    version: "1.4"
562                    versions: "19.08;1.4"
563                    subdirectories: true
564                    no-autodownload: true
565                    autodelete: false
566                    add-ld-path: "lib"
567                    merge-dirs: "vulkan/icd.d;glvnd/egl_vendor.d"
568                    download-if: "active-gl-driver"
569                    enable-if: "active-gl-driver"
570        "###;
571        match FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest) {
572            Err(e) => std::panic::panic_any(e),
573            Ok(app) => {
574                assert_eq!(app.get_id(), "net.pcsx2.PCSX2");
575                assert_eq!(app.add_extensions.len(), 3);
576            }
577        }
578    }
579
580    #[test]
581    pub fn test_parse_string_source() {
582        let application_manifest = r###"
583            app-id: net.louib.flatpak-rs
584            runtime: org.gnome.Platform
585            runtime-version: "3.36"
586            sdk: org.gnome.Sdk
587            command: flatpak-rs
588            tags: ["nightly"]
589            modules:
590              -
591                name: "flatpak-rs"
592                buildsystem: simple
593                cleanup: [ "*" ]
594                config-opts: []
595                sources:
596                  -
597                    "shared-modules/linux-audio/lv2.json"
598        "###;
599        match FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest) {
600            Err(e) => std::panic::panic_any(e),
601            Ok(app) => {
602                assert_eq!(app.get_id(), "net.louib.flatpak-rs");
603            }
604        }
605    }
606
607    #[test]
608    pub fn test_parse_source_without_type() {
609        let application_manifest = r###"
610            app-id: net.louib.flatpak-rs
611            runtime: org.gnome.Platform
612            runtime-version: "3.36"
613            sdk: org.gnome.Sdk
614            command: flatpak-rs
615            tags: ["nightly"]
616            modules:
617              -
618                name: "gcc"
619                buildsystem: simple
620                cleanup: [ "*" ]
621                config-opts: []
622                sources:
623                  -
624                    url: "https://ftp.gnu.org/gnu/gcc/gcc-7.5.0/gcc-7.5.0.tar.xz"
625                    sha256: "b81946e7f01f90528a1f7352ab08cc602b9ccc05d4e44da4bd501c5a189ee661"
626
627        "###;
628        match FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest) {
629            Err(e) => std::panic::panic_any(e),
630            Ok(application) => {
631                assert_eq!(application.get_id(), "net.louib.flatpak-rs");
632            }
633        }
634    }
635
636    #[test]
637    pub fn test_parse_json() {
638        let application_manifest = r###"
639            {
640                "app-id": "org.gnome.SoundJuicer",
641                "runtime": "org.gnome.Platform",
642                "runtime-version": "master",
643                "sdk": "org.gnome.Sdk",
644                "command": "sound-juicer",
645                "tags": [ "nightly" ],
646                "desktop-file-name-suffix": " ☢️",
647                "finish-args": [
648                    "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*",
649                    "--env=GST_PLUGIN_PATH=/app/lib/codecs/lib/gstreamer-1.0"
650                ],
651                "cleanup": [ "/include", "/share/bash-completion" ],
652                "modules": [
653                    {
654                        "name": "cdparanoia",
655                        "buildsystem": "simple",
656                        "build-commands": [
657                            "cp /usr/share/automake-*/config.{sub,guess} .",
658                            "./configure --prefix=/app",
659                            "make all slib",
660                            "make install"
661                        ],
662                        "sources": [
663                            {
664                                "type": "archive",
665                                "url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz",
666                                "sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df"
667                            },
668                            {
669                                "type": "patch",
670                                "path": "cdparanoia-use-proper-gnu-config-files.patch"
671                            }
672                        ]
673                    },
674                    {
675                        "name": "gst-plugins-base",
676                        "buildsystem": "meson",
677                        "config-opts": [
678                            "--prefix=/app",
679                            "-Dauto_features=disabled",
680                            "-Dcdparanoia=enabled"
681                        ],
682                        "cleanup": [ "*.la", "/share/gtk-doc" ],
683                        "sources": [
684                            {
685                                "type": "git",
686                                "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git",
687                                "branch" : "1.16.2",
688                                "commit" : "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e"
689                            }
690                        ]
691                    }
692                ]
693            }
694        "###;
695        match FlatpakApplication::parse(FlatpakManifestFormat::JSON, application_manifest) {
696            Err(e) => std::panic::panic_any(e),
697            Ok(app) => {
698                assert_eq!(app.get_id(), "org.gnome.SoundJuicer");
699            }
700        }
701    }
702
703    #[test]
704    pub fn test_parse_json_with_comments() {
705        let application_manifest = r###"
706            {
707                "app-id": "org.gnome.SoundJuicer",
708                "runtime": "org.gnome.Platform",
709                "runtime-version": "master",
710                "sdk": "org.gnome.Sdk",
711                "command": "sound-juicer",
712                "tags": [ "nightly" ],
713                "desktop-file-name-suffix": " ☢️",
714                "finish-args": [
715                    /* X11 + XShm access */
716                    "--share=ipc", "--socket=fallback-x11",
717                    /* Wayland access */
718                    "--socket=wayland",
719                    /* audio CDs */
720                    "--device=all",
721                    /* Needs to talk to the network */
722                    "--share=network",
723                    /* Play sounds */
724                    "--socket=pulseaudio",
725                    /* Browse user's Music directory */
726                    "--filesystem=xdg-music",
727                    /* Migrate DConf settings from the host */
728                    "--metadata=X-DConf=migrate-path=/org/gnome/sound-juicer/",
729                    /* optical media detection */
730                    "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*",
731                    /* Ensure cdda gstreamer plugin is picked found for audio CD's */
732                    "--env=GST_PLUGIN_PATH=/app/lib/codecs/lib/gstreamer-1.0"
733                ],
734                "cleanup": [ "/include", "/share/bash-completion" ],
735                "modules": [
736                    /* gst-plugins-base needs cdparanoia to add support for cdda */
737                    {
738                        "name": "cdparanoia",
739                        "buildsystem": "simple",
740                        "build-commands": [
741                            "cp /usr/share/automake-*/config.{sub,guess} .",
742                            "./configure --prefix=/app",
743                            "make all slib",
744                            "make install"
745                        ],
746                        "sources": [
747                            {
748                                "type": "archive",
749                                "url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz",
750                                "sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df"
751                            },
752                            {
753                                "type": "patch",
754                                "path": "cdparanoia-use-proper-gnu-config-files.patch"
755                            }
756                        ]
757                    },
758                    /* To play cdda */
759                    {
760                        "name": "gst-plugins-base",
761                        "buildsystem": "meson",
762                        "config-opts": [
763                            "--prefix=/app",
764                            "-Dauto_features=disabled",
765                            "-Dcdparanoia=enabled"
766                        ],
767                        "cleanup": [ "*.la", "/share/gtk-doc" ],
768                        "sources": [
769                            {
770                                "type": "git",
771                                "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git",
772                                "branch" : "1.16.2",
773                                "commit" : "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e"
774                            }
775                        ]
776                    }
777                ]
778            }
779        "###;
780        match FlatpakApplication::parse(FlatpakManifestFormat::JSON, application_manifest) {
781            Err(e) => std::panic::panic_any(e),
782            Ok(app) => {
783                assert_eq!(app.get_id(), "org.gnome.SoundJuicer");
784            }
785        }
786    }
787
788    #[test]
789    pub fn test_parse_json_with_multi_line_comments() {
790        let application_manifest = r###"
791            {
792              "app-id": "org.gnome.Lollypop",
793              "runtime": "org.gnome.Platform",
794              "runtime-version": "40",
795              "sdk": "org.gnome.Sdk",
796              "command": "lollypop",
797              "finish-args": [
798                "--share=ipc",
799                "--own-name=org.mpris.MediaPlayer2.Lollypop",
800                "--metadata=X-DConf=migrate-path=/org/gnome/Lollypop/"
801              ],
802              /* FFmpeg-full and gst-plugins-ugly required for .wma support
803               * Due to possible legal stubbornness in the USA, it can't be downloaded automatically
804               */
805              "add-extensions": {
806                "org.freedesktop.Platform.ffmpeg-full": {
807                  "directory": "lib/ffmpeg",
808                  "version": "20.08",
809                  "add-ld-path": ".",
810                  "no-autodownload": true,
811                  "autodelete": false
812                }
813              },
814              "cleanup-commands": [
815                "mkdir -p /app/lib/ffmpeg"
816              ],
817              "modules": [
818                "pypi-dependencies.json",
819                {
820                  "name": "gst-plugins-ugly",
821                  "buildsystem": "meson",
822                  "cleanup": [
823                    "*.la",
824                    "/share/gtk-doc"
825                  ],
826                  "sources": [{
827                    "type": "archive",
828                    "url": "https://gstreamer.freedesktop.org/src/gst-plugins-ugly/gst-plugins-ugly-1.16.2.tar.xz",
829                    "sha256": "5500415b865e8b62775d4742cbb9f37146a50caecfc0e7a6fc0160d3c560fbca"
830                  }]
831                }
832              ]
833            }
834        "###;
835        match FlatpakApplication::parse(FlatpakManifestFormat::JSON, application_manifest) {
836            Err(e) => std::panic::panic_any(e),
837            Ok(app) => {
838                assert_eq!(app.get_id(), "org.gnome.Lollypop");
839            }
840        }
841    }
842
843    #[test]
844    pub fn test_parse_extension() {
845        let extension_manifest = r###"
846        {
847            "id": "org.freedesktop.Platform.Icontheme.Paper",
848            "branch": "1.0",
849            "runtime": "org.freedesktop.Sdk",
850            "build-extension": true,
851            "sdk": "org.freedesktop.Sdk",
852            "runtime-version": "1.6",
853            "sdk-extensions": [],
854            "separate-locales": false,
855            "cleanup": [ "/share/info", "/share/man" ],
856            "appstream-compose": false,
857            "build-options" : {
858                "prefix": "/usr/share/runtime"
859            },
860            "modules": []
861        }
862        "###;
863        match FlatpakApplication::parse(FlatpakManifestFormat::JSON, extension_manifest) {
864            Err(e) => std::panic::panic_any(e),
865            Ok(app) => {
866                assert_eq!(app.id, "org.freedesktop.Platform.Icontheme.Paper");
867                assert_eq!(app.get_id(), "org.freedesktop.Platform.Icontheme.Paper");
868                assert_eq!(app.separate_locales, Some(false));
869                assert_eq!(app.appstream_compose, Some(false));
870            }
871        }
872    }
873
874    #[test]
875    pub fn test_parse_recursive_modules() {
876        let application_manifest = r###"
877            app-id: com.georgefb.haruna
878            runtime: org.kde.Platform
879            runtime-version: '5.15'
880            sdk: org.kde.Sdk
881            command: haruna
882            finish-args:
883              - '--share=ipc'
884              - '--share=network'
885              - '--socket=x11'
886              - '--socket=wayland'
887              - '--socket=pulseaudio'
888              - '--device=dri'
889              - '--filesystem=host'
890              - '--talk-name=ca.desrt.dconf'
891              - '--talk-name=org.freedesktop.ScreenSaver'
892              - '--own-name=org.mpris.MediaPlayer2.haruna'
893              - '--env=DCONF_USER_CONFIG_DIR=.config/dconf'
894              - '--env=LC_NUMERIC=C'
895              - '--env=XDG_DATA_DIRS=/usr/share:/app/share/'
896            modules:
897              - name: haruna
898                buildsystem: cmake-ninja
899                sources:
900                  - type: archive
901                    url: 'https://github.com/g-fb/haruna/archive/refs/tags/0.6.3.tar.gz'
902                    sha256: 'c79ec1e351f47faf9a58a6ba7ec3cc05cfdc5423fde0584f2d4081f5058363e3'
903                modules:
904                  - name: taglib
905                    buildsystem: cmake-ninja
906                    config-opts:
907                    - '-DBUILD_SHARED_LIBS=ON'
908                    sources:
909                    - type: archive
910                      url: 'https://github.com/taglib/taglib/releases/download/v1.12/taglib-1.12.tar.gz'
911                      sha256: '7fccd07669a523b07a15bd24c8da1bbb92206cb19e9366c3692af3d79253b703'
912                  - name: ffmpegthumbs
913                    buildsystem: cmake-ninja
914                    sources:
915                    - type: archive
916                      url: 'https://invent.kde.org/multimedia/ffmpegthumbs/-/archive/v20.12.3/ffmpegthumbs-v20.12.3.tar.gz'
917                      sha256: '9292a503808688b37e45ee336efcd28c39035d49c96e1df466b091f2eaf7a529'
918                  - name: kio-extras
919                    buildsystem: cmake-ninja
920                    sources:
921                    - type: archive
922                      url: 'https://invent.kde.org/network/kio-extras/-/archive/v20.12.3/kio-extras-v20.12.3.tar.gz'
923                      sha256: 'bedccdbf3664f2669270c2864c6f7c0e73237d18fae04067bc21ae5e12716b0b'
924                  - name: libmpv
925                    cleanup:
926                      - /include
927                      - /lib/pkgconfig
928                      - /share/man
929                    buildsystem: simple
930                    build-commands:
931                      - python3 waf configure --prefix=/app --enable-libmpv-shared --disable-cplayer --disable-build-date --disable-alsa
932                      - python3 waf build
933                      - python3 waf install
934                    sources:
935                      - type: archive
936                        url: 'https://github.com/mpv-player/mpv/archive/v0.33.1.tar.gz'
937                        sha256: '100a116b9f23bdcda3a596e9f26be3a69f166a4f1d00910d1789b6571c46f3a9'
938                      - type: file
939                        url: 'https://waf.io/waf-2.0.21'
940                        sha256: '7cebf2c5efe53cbb9a4b5bdc4b49ae90ecd64a8fce7a3222d58e591b58215306'
941                        dest-filename: waf
942                    modules:
943                      - name: luajit
944                        no-autogen: true
945                        cleanup:
946                          - /bin
947                          - /include
948                          - /lib/pkgconfig
949                          - /share/man
950                        sources:
951                          - type: archive
952                            url: 'http://luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz'
953                            sha256: '1ad2e34b111c802f9d0cdf019e986909123237a28c746b21295b63c9e785d9c3'
954                          - type: shell
955                            commands:
956                              - sed -i 's|/usr/local|/app|' ./Makefile
957                      - name: libv4l2
958                        cleanup:
959                          - /sbin
960                          - /bin
961                          - /include
962                          - /lib/pkgconfig
963                          - /share/man
964                        config-opts:
965                          - '--disable-static'
966                          - '--disable-bpf'
967                          - '--with-udevdir=/app/lib/udev'
968                        sources:
969                          - type: archive
970                            url: 'https://linuxtv.org/downloads/v4l-utils/v4l-utils-1.20.0.tar.bz2'
971                            sha256: '956118713f7ccb405c55c7088a6a2490c32d54300dd9a30d8d5008c28d3726f7'
972                      - name: nv-codec-headers
973                        cleanup:
974                          - '*'
975                        no-autogen: true
976                        make-install-args:
977                          - PREFIX=/app
978                        sources:
979                          - type: archive
980                            url: 'https://github.com/FFmpeg/nv-codec-headers/releases/download/n11.0.10.0/nv-codec-headers-11.0.10.0.tar.gz'
981                            sha256: 'e5d1fe6b18254a3c8747a38714564030e4fda506961a11a7eafb94f2400419bb'
982                      - name: ffmpeg
983                        cleanup:
984                          - /include
985                          - /lib/pkgconfig
986                          - /share/ffmpeg/examples
987                        config-opts:
988                          - '--enable-shared'
989                          - '--disable-static'
990                          - '--enable-gnutls'
991                          - '--enable-gpl'
992                          - '--disable-doc'
993                          - '--disable-programs'
994                          - '--disable-encoders'
995                          - '--disable-muxers'
996                          - '--enable-encoder=png,libwebp'
997                          - '--enable-libv4l2'
998                          - '--enable-libdav1d'
999                          - '--enable-libfontconfig'
1000                          - '--enable-libfreetype'
1001                          - '--enable-libopus'
1002                          - '--enable-librsvg'
1003                          - '--enable-libvpx'
1004                          - '--enable-libmp3lame'
1005                          - '--enable-libwebp'
1006                        sources:
1007                          - type: archive
1008                            url: 'https://ffmpeg.org/releases/ffmpeg-4.3.2.tar.xz'
1009                            sha256: '46e4e64f1dd0233cbc0934b9f1c0da676008cad34725113fb7f802cfa84ccddb'
1010                      - name: libass
1011                        cleanup:
1012                          - /include
1013                          - /lib/pkgconfig
1014                        config-opts:
1015                          - '--disable-static'
1016                        sources:
1017                          - type: archive
1018                            url: 'https://github.com/libass/libass/releases/download/0.15.0/libass-0.15.0.tar.gz'
1019                            sha256: '9cbddee5e8c87e43a5fe627a19cd2aa4c36552156eb4edcf6c5a30bd4934fe58'
1020                      - name: uchardet
1021                        buildsystem: cmake-ninja
1022                        config-opts:
1023                          - '-DCMAKE_BUILD_TYPE=Release'
1024                          - '-DBUILD_STATIC=0'
1025                        cleanup:
1026                          - /bin
1027                          - /include
1028                          - /lib/pkgconfig
1029                          - /share/man
1030                        sources:
1031                          - type: archive
1032                            url: 'https://gitlab.freedesktop.org/uchardet/uchardet/-/archive/v0.0.7/uchardet-v0.0.7.tar.gz'
1033                            sha256: 'f3635d1d10e1470452bc42c1bf509451a9926b399a11740a9949e86069d69f58'
1034                  - name: youtube-dl
1035                    no-autogen: true
1036                    no-make-install: true
1037                    make-args:
1038                      - youtube-dl
1039                      - PYTHON=/usr/bin/python3
1040                    post-install:
1041                      - install youtube-dl /app/bin
1042                    sources:
1043                      - type: archive
1044                        url: 'https://github.com/ytdl-org/youtube-dl/archive/2021.04.26.tar.gz'
1045                        sha256: 'd80023ab221b3cb89229b632e247035a22c5afaee9a7b3c653bbd702f71c1083'
1046        "###;
1047        match FlatpakApplication::parse(FlatpakManifestFormat::YAML, application_manifest) {
1048            Err(e) => std::panic::panic_any(e),
1049            Ok(app) => {
1050                assert_eq!(app.get_id(), "com.georgefb.haruna");
1051                assert_eq!(app.get_max_depth(), 3);
1052                assert_eq!(app.modules.len(), 1);
1053                assert_eq!(app.get_all_modules_recursively().len(), 12);
1054            }
1055        }
1056    }
1057}