Parcourir la source

feat(bundler) appimage (#37)

* add basic setup

* add basic download logic

* build test

* add yaml parser.

* finish appimage

* cleanup and comment

* add comment

* modify chmod

* fix line endings.

* remove "cat runtime"

* match desktop files

* add path utils

* add opts

* add fileopts and diropts

* create, create all and remove

* copy file function

* add copy logic and new error

* add target to gitignore

* refactor deb::bundle_project

* finish refactor
Tensor-Programming il y a 5 ans
Parent
commit
52e20d2544

+ 8 - 1
.gitignore

@@ -60,7 +60,14 @@ debug.log
 package-lock.json
 .vscode/settings.json
 
+# Quasar output
+bundle.json
+config.json
+
+# rust compiled folders
+target
 
 src-tauri
 test/jest/tmp
-/tauri.conf.js
+/tauri.conf.js
+

+ 1 - 0
tools/rust/cargo-tauri-bundle/Cargo.toml

@@ -21,6 +21,7 @@ image = "0.12"
 libflate = "0.1"
 md5 = "0.3"
 msi = "0.2"
+
 serde = "1.0"
 serde_derive = "1.0"
 strsim = "0.7"

+ 5 - 0
tools/rust/cargo-tauri-bundle/src/bundle.rs

@@ -1,3 +1,4 @@
+mod appimage_bundle;
 mod category;
 mod common;
 mod deb_bundle;
@@ -7,6 +8,7 @@ mod msi_bundle;
 mod osx_bundle;
 mod rpm_bundle;
 mod settings;
+mod path_utils;
 mod wix;
 
 pub use self::common::{print_error, print_finished};
@@ -22,8 +24,11 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<PathBuf>> {
       // use dmg bundler
       // PackageType::OsxBundle => dmg_bundle::bundle_project(&settings)?,
       PackageType::WindowsMsi => msi_bundle::bundle_project(&settings)?,
+      // force appimage on linux
+      // PackageType::Deb => appimage_bundle::bundle_project(&settings)?,
       PackageType::Deb => deb_bundle::bundle_project(&settings)?,
       PackageType::Rpm => rpm_bundle::bundle_project(&settings)?,
+      PackageType::AppImage => appimage_bundle::bundle_project(&settings)?,
       PackageType::Dmg => dmg_bundle::bundle_project(&settings)?,
     });
   }

+ 100 - 0
tools/rust/cargo-tauri-bundle/src/bundle/appimage_bundle.rs

@@ -0,0 +1,100 @@
+use super::common;
+use super::deb_bundle;
+use crate::Settings;
+use super::path_utils;
+
+use handlebars::Handlebars;
+use lazy_static::lazy_static;
+
+use std::collections::BTreeMap;
+use std::fs::write;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+
+// Create handlebars template for shell script
+lazy_static! {
+  static ref HANDLEBARS: Handlebars = {
+    let mut handlebars = Handlebars::new();
+
+    handlebars
+      .register_template_string("appimage", include_str!("templates/appimage"))
+      .unwrap();
+    handlebars
+  };
+}
+
+// bundle the project.
+pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
+  // generate the deb binary name
+  let arch = match settings.binary_arch() {
+    "x86" => "i386",
+    "x86_64" => "amd64",
+    other => other,
+  };
+  let package_base_name = format!(
+    "{}_{}_{}",
+    settings.binary_name(),
+    settings.version_string(),
+    arch
+  );
+  let base_dir = settings.project_out_directory().join("bundle/deb");
+  let package_dir = base_dir.join(&package_base_name);
+  if package_dir.exists() {
+    fs::remove_dir_all(&package_dir)
+      .chain_err(|| format!("Failed to remove old {}", package_base_name))?;
+  }
+
+  // generate deb_folder structure
+  deb_bundle::generate_folders(settings, &package_dir)?;
+
+  let app_dir_path = path_utils::create(
+    settings
+      .project_out_directory()
+      .join(format!("{}.AppDir", settings.binary_name())), true
+  );
+
+  let upcase = settings.binary_name().to_uppercase();
+
+  // setup data to insert into shell script
+  let mut sh_map = BTreeMap::new();
+  sh_map.insert("app_name", settings.binary_name());
+  sh_map.insert("bundle_name", package_base_name.as_str());
+  sh_map.insert("app_name_uppercase", upcase.as_str());
+
+  // initialize shell script template.
+  let temp = HANDLEBARS
+    .render("appimage", &sh_map)
+    .or_else(|e| Err(e.to_string()))?;
+  let output_path = settings.project_out_directory();
+
+  // create the shell script file in the target/ folder.
+  let sh_file = output_path.join("build_appimage");
+  common::print_bundling(
+    format!(
+      "{:?}",
+      &output_path.join(format!("{}.AppImage", settings.binary_name()))
+    )
+    .as_str(),
+  )?;
+  write(&sh_file, temp).or_else(|e| Err(e.to_string()))?;
+
+  // chmod script for execution
+  Command::new("chmod")
+    .arg("777")
+    .arg(&sh_file)
+    .current_dir(output_path)
+    .stdout(Stdio::piped())
+    .stderr(Stdio::piped())
+    .spawn()
+    .expect("Failed to chmod script");
+
+  // execute the shell script to build the appimage.
+  Command::new(&sh_file)
+    .current_dir(output_path)
+    .stdout(Stdio::piped())
+    .stderr(Stdio::piped())
+    .spawn()
+    .expect("Failed to execute shell script");
+
+  Ok(vec![sh_file])
+}

+ 15 - 9
tools/rust/cargo-tauri-bundle/src/bundle/deb_bundle.rs

@@ -56,15 +56,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
   }
   let package_path = base_dir.join(package_name);
 
-  // Generate data files.
-  let data_dir = package_dir.join("data");
-  let binary_dest = data_dir.join("usr/bin").join(settings.binary_name());
-  common::copy_file(settings.binary_path(), &binary_dest)
-    .chain_err(|| "Failed to copy binary file")?;
-  transfer_resource_files(settings, &data_dir).chain_err(|| "Failed to copy resource files")?;
-  generate_icon_files(settings, &data_dir).chain_err(|| "Failed to create icon files")?;
-  generate_desktop_file(settings, &data_dir).chain_err(|| "Failed to create desktop file")?;
-
+  let data_dir = generate_folders(settings, &package_dir).chain_err(|| "Failed to build folders")?;
   // Generate control files.
   let control_dir = package_dir.join("control");
   generate_control_file(settings, arch, &control_dir, &data_dir)
@@ -90,6 +82,20 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
   Ok(vec![package_path])
 }
 
+pub fn generate_folders(settings: &Settings, package_dir: &Path) -> crate::Result<PathBuf> {
+  
+  // Generate data files.
+  let data_dir = package_dir.join("data");
+  let binary_dest = data_dir.join("usr/bin").join(settings.binary_name());
+  common::copy_file(settings.binary_path(), &binary_dest)
+    .chain_err(|| "Failed to copy binary file")?;
+  transfer_resource_files(settings, &data_dir).chain_err(|| "Failed to copy resource files")?;
+  generate_icon_files(settings, &data_dir).chain_err(|| "Failed to create icon files")?;
+  generate_desktop_file(settings, &data_dir).chain_err(|| "Failed to create desktop file")?;
+
+  Ok(data_dir)
+}
+
 /// Generate the application desktop file and store it under the `data_dir`.
 fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
   let bin_name = settings.binary_name();

+ 256 - 0
tools/rust/cargo-tauri-bundle/src/bundle/path_utils.rs

@@ -0,0 +1,256 @@
+use std::fs::{create_dir, create_dir_all, read_dir, remove_dir_all};
+use std::path::{Path, PathBuf};
+
+#[derive(Clone)]
+pub struct DirOpts {
+  pub depth: u64,
+}
+
+pub struct FileOpts {
+  pub overwrite: bool,
+  pub skip: bool,
+  pub buffer_size: usize,
+}
+
+#[derive(Clone)]
+pub struct Options {
+  pub overwrite: bool,
+  pub skip: bool,
+  pub buffer_size: usize,
+  pub copy_files: bool,
+  pub content_only: bool,
+  pub depth: u64,
+}
+
+pub struct DirInfo {
+  pub size: u64,
+  pub files: Vec<String>,
+  pub directories: Vec<String>,
+}
+
+impl Options {
+  pub fn new() -> Options {
+    Options {
+      overwrite: false,
+      skip: false,
+      buffer_size: 64000,
+      copy_files: false,
+      content_only: false,
+      depth: 0,
+    }
+  }
+}
+
+impl DirOpts {
+  pub fn new() -> DirOpts {
+    DirOpts { depth: 0 }
+  }
+}
+
+impl FileOpts {
+  pub fn new() -> FileOpts {
+    FileOpts {
+      overwrite: false,
+      skip: false,
+      buffer_size: 64000,
+    }
+  }
+}
+
+pub fn create<P>(path: P, erase: bool) -> crate::Result<()>
+where
+  P: AsRef<Path>,
+{
+  if erase && path.as_ref().exists() {
+    remove(&path)?;
+  }
+  Ok(create_dir(&path)?)
+}
+
+pub fn create_all<P>(path: P, erase: bool) -> crate::Result<()>
+where
+  P: AsRef<Path>,
+{
+  if erase && path.as_ref().exists() {
+    remove(&path)?;
+  }
+  Ok(create_dir_all(&path)?)
+}
+
+pub fn remove<P: AsRef<Path>>(path: P) -> crate::Result<()> {
+  if path.as_ref().exists() {
+    Ok(remove_dir_all(path)?)
+  } else {
+    Ok(())
+  }
+}
+
+pub fn copy_file<P, Q>(from: P, to: Q, options: &FileOpts) -> crate::Result<u64>
+where
+  P: AsRef<Path>,
+  Q: AsRef<Path>,
+{
+  let from = from.as_ref();
+  if !from.exists() {
+    if let Some(msg) = from.to_str() {
+      let msg = format!("Path \"{}\" does not exist or you don't have access", msg);
+      return Err(msg.into());
+    }
+    return Err("Path does not exist Or you don't have access!".into());
+  }
+
+  if !from.is_file() {
+    if let Some(msg) = from.to_str() {
+      let msg = format!("Path \"{}\" is not a file!", msg);
+      return Err(msg.into());
+    }
+    return Err("Path is not a file!".into());
+  }
+  if !options.overwrite && to.as_ref().exists() {
+    if options.skip {
+      return Ok(0);
+    }
+
+    if let Some(msg) = to.as_ref().to_str() {
+      let msg = format!("Path \"{}\" is exist", msg);
+      return Err(msg.into());
+    }
+  }
+
+  Ok(std::fs::copy(from, to)?)
+}
+
+pub fn copy<P, Q>(from: P, to: Q, options: &Options) -> crate::Result<u64>
+where
+  P: AsRef<Path>,
+  Q: AsRef<Path>,
+{
+  let from = from.as_ref();
+  if !from.exists() {
+    if let Some(msg) = from.to_str() {
+      let msg = format!("Path \"{}\" does not exist or you don't have access!", msg);
+      return Err(msg.into());
+    }
+    return Err("Path does not exist Or you don't have access!".into());
+  }
+  if !from.is_dir() {
+    if let Some(msg) = from.to_str() {
+      let msg = format!("Path \"{}\" is not a directory!", msg);
+      return Err(msg.into());
+    }
+    return Err("Path is not a directory!".into());
+  }
+  let dir_name;
+  if let Some(val) = from.components().last() {
+    dir_name = val.as_os_str();
+  } else {
+    return Err("Invalid folder from".into());
+  }
+  let mut to: PathBuf = to.as_ref().to_path_buf();
+  if !options.content_only && ((options.copy_files && to.exists()) || !options.copy_files) {
+    to.push(dir_name);
+  }
+
+  let mut read_options = DirOpts::new();
+  if options.depth > 0 {
+    read_options.depth = options.depth;
+  }
+
+  let dir_content = get_dir_info(from, &read_options)?;
+  for directory in dir_content.directories {
+    let tmp_to = Path::new(&directory).strip_prefix(from)?;
+    let dir = to.join(&tmp_to);
+    if !dir.exists() {
+      if options.copy_files {
+        create_all(dir, false)?;
+      } else {
+        create(dir, false)?;
+      }
+    }
+  }
+  let mut result: u64 = 0;
+  for file in dir_content.files {
+    let to = to.to_path_buf();
+    let tp = Path::new(&file).strip_prefix(from)?;
+    let path = to.join(&tp);
+
+    let file_options = FileOpts {
+      overwrite: options.overwrite,
+      skip: options.skip,
+      buffer_size: options.buffer_size,
+    };
+    let mut result_copy: crate::Result<u64>;
+    let mut work = true;
+
+    while work {
+      result_copy = copy_file(&file, &path, &file_options);
+      match result_copy {
+        Ok(val) => {
+          result += val;
+          work = false;
+        }
+        Err(err) => {
+          let err_msg = err.to_string();
+          return Err(err_msg.into());
+        }
+      }
+    }
+  }
+  Ok(result)
+}
+
+pub fn get_dir_info<P>(path: P, options: &DirOpts) -> crate::Result<DirInfo>
+where
+  P: AsRef<Path>,
+{
+  let mut depth = 0;
+  if options.depth != 0 {
+    depth = options.depth + 1;
+  }
+  _get_dir_info(path, depth)
+}
+
+fn _get_dir_info<P>(path: P, mut depth: u64) -> crate::Result<DirInfo>
+where
+  P: AsRef<Path>,
+{
+  let mut directories = Vec::new();
+  let mut files = Vec::new();
+  let mut size = 0;
+  let item = path.as_ref().to_str();
+  if !item.is_some() {
+    return Err("Invalid path".into());
+  }
+  let item = item.unwrap().to_string();
+
+  if path.as_ref().is_dir() {
+    directories.push(item);
+    if depth == 0 || depth > 1 {
+      if depth > 1 {
+        depth -= 1;
+      }
+      for entry in read_dir(&path)? {
+        let _path = entry?.path();
+
+        match _get_dir_info(_path, depth) {
+          Ok(items) => {
+            let mut _files = items.files;
+            let mut _dirrectories = items.directories;
+            size += items.size;
+            files.append(&mut _files);
+            directories.append(&mut _dirrectories);
+          }
+          Err(err) => return Err(err),
+        }
+      }
+    }
+  } else {
+    size = path.as_ref().metadata()?.len();
+    files.push(item);
+  }
+  Ok(DirInfo {
+    size: size,
+    files: files,
+    directories: directories,
+  })
+}

+ 3 - 0
tools/rust/cargo-tauri-bundle/src/bundle/settings.rs

@@ -17,6 +17,7 @@ pub enum PackageType {
   WindowsMsi,
   Deb,
   Rpm,
+  AppImage,
   Dmg,
 }
 
@@ -29,6 +30,7 @@ impl PackageType {
       "msi" => Some(PackageType::WindowsMsi),
       "osx" => Some(PackageType::OsxBundle),
       "rpm" => Some(PackageType::Rpm),
+      "appimage" => Some(PackageType::AppImage),
       "dmg" => Some(PackageType::Dmg),
       _ => None,
     }
@@ -41,6 +43,7 @@ impl PackageType {
       PackageType::WindowsMsi => "msi",
       PackageType::OsxBundle => "osx",
       PackageType::Rpm => "rpm",
+      PackageType::AppImage => "appimage",
       PackageType::Dmg => "dmg",
     }
   }

+ 28 - 0
tools/rust/cargo-tauri-bundle/src/bundle/templates/appimage

@@ -0,0 +1,28 @@
+#!/bin/bash
+
+mkdir -p {{app_name}}.AppDir
+cp -r bundle/deb/{{bundle_name}}/data/usr {{app_name}}.AppDir
+cp {{app_name}} {{app_name}}.AppDir/AppRun
+
+cd {{app_name}}.AppDir
+
+cp usr/share/icons/hicolor/256x265/apps/{{app_name}}.png {{app_name}}.png
+
+echo '[Desktop Entry]' > {{app_name}}.desktop
+echo 'Version=1.0' >> {{app_name}}.desktop
+echo 'Comment=A Tauri App' >> {{app_name}}.desktop
+echo 'Exec={{app_name}}' >> {{app_name}}.desktop
+echo 'Icon={{app_name}}' >> {{app_name}}.desktop
+echo 'Name={{app_name_uppercase}}' >> {{app_name}}.desktop
+echo 'Terminal=false' >> {{app_name}}.desktop
+echo 'Type=Application' >> {{app_name}}.desktop
+echo 'Categories=X-Web;' >> {{app_name}}.desktop
+
+cp {{app_name}}.desktop {{app_name}}.AppDir/usr/share/applications/{{app_name}}.desktop
+
+cd ..
+
+mksquashfs {{app_name}}.AppDir {{app_name}}.squashfs -root-owned -noappend
+# cat runtime >> {{app_name}}.AppImage
+cat {{app_name}}.squashfs >> {{app_name}}.AppImage
+chmod a+x {{app_name}}.AppImage

+ 4 - 4
tools/rust/cargo-tauri-bundle/src/main.rs

@@ -18,7 +18,7 @@ use std::env;
 use std::process;
 
 error_chain! {
-    foreign_links {
+  foreign_links {
         Glob(::glob::GlobError);
         GlobPattern(::glob::PatternError);
         Io(::std::io::Error);
@@ -27,8 +27,9 @@ error_chain! {
         Term(::term::Error);
         Toml(::toml::de::Error);
         Walkdir(::walkdir::Error);
+        StripError(std::path::StripPrefixError);
     }
-    errors { }
+    errors {}
 }
 
 /// Runs `cargo build` to make sure the binary file is up-to-date.
@@ -133,8 +134,7 @@ fn run() -> crate::Result<()> {
   if let Some(m) = m.subcommand_matches("tauri-bundle") {
     if m.is_present("version") {
       println!("{}", crate_version!());
-    }
-    else {
+    } else {
       let output_paths = env::current_dir()
         .map_err(From::from)
         .and_then(|d| Settings::new(d, m))