From 52e20d25448bc66c205b9e597cd405e0fa24d79e Mon Sep 17 00:00:00 2001 From: Tensor-Programming Date: Tue, 26 Nov 2019 15:16:21 -0500 Subject: [PATCH] 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 --- .gitignore | 9 +- tools/rust/cargo-tauri-bundle/Cargo.toml | 1 + tools/rust/cargo-tauri-bundle/src/bundle.rs | 5 + .../src/bundle/appimage_bundle.rs | 100 +++++++ .../src/bundle/deb_bundle.rs | 24 +- .../src/bundle/path_utils.rs | 256 ++++++++++++++++++ .../cargo-tauri-bundle/src/bundle/settings.rs | 3 + .../src/bundle/templates/appimage | 28 ++ tools/rust/cargo-tauri-bundle/src/main.rs | 8 +- 9 files changed, 420 insertions(+), 14 deletions(-) create mode 100644 tools/rust/cargo-tauri-bundle/src/bundle/appimage_bundle.rs create mode 100644 tools/rust/cargo-tauri-bundle/src/bundle/path_utils.rs create mode 100644 tools/rust/cargo-tauri-bundle/src/bundle/templates/appimage diff --git a/.gitignore b/.gitignore index 5533395f3..687174c3c 100644 --- a/.gitignore +++ b/.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 \ No newline at end of file +/tauri.conf.js + diff --git a/tools/rust/cargo-tauri-bundle/Cargo.toml b/tools/rust/cargo-tauri-bundle/Cargo.toml index dffd16b7e..b3736f310 100644 --- a/tools/rust/cargo-tauri-bundle/Cargo.toml +++ b/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" diff --git a/tools/rust/cargo-tauri-bundle/src/bundle.rs b/tools/rust/cargo-tauri-bundle/src/bundle.rs index b58387904..ba042c903 100644 --- a/tools/rust/cargo-tauri-bundle/src/bundle.rs +++ b/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> { // 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)?, }); } diff --git a/tools/rust/cargo-tauri-bundle/src/bundle/appimage_bundle.rs b/tools/rust/cargo-tauri-bundle/src/bundle/appimage_bundle.rs new file mode 100644 index 000000000..c9a9478af --- /dev/null +++ b/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> { + // 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]) +} diff --git a/tools/rust/cargo-tauri-bundle/src/bundle/deb_bundle.rs b/tools/rust/cargo-tauri-bundle/src/bundle/deb_bundle.rs index 2bd5d941b..eb0e2df6d 100644 --- a/tools/rust/cargo-tauri-bundle/src/bundle/deb_bundle.rs +++ b/tools/rust/cargo-tauri-bundle/src/bundle/deb_bundle.rs @@ -56,15 +56,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { } 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> { Ok(vec![package_path]) } +pub fn generate_folders(settings: &Settings, package_dir: &Path) -> crate::Result { + + // 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(); diff --git a/tools/rust/cargo-tauri-bundle/src/bundle/path_utils.rs b/tools/rust/cargo-tauri-bundle/src/bundle/path_utils.rs new file mode 100644 index 000000000..bf443316b --- /dev/null +++ b/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, + pub directories: Vec, +} + +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

(path: P, erase: bool) -> crate::Result<()> +where + P: AsRef, +{ + if erase && path.as_ref().exists() { + remove(&path)?; + } + Ok(create_dir(&path)?) +} + +pub fn create_all

(path: P, erase: bool) -> crate::Result<()> +where + P: AsRef, +{ + if erase && path.as_ref().exists() { + remove(&path)?; + } + Ok(create_dir_all(&path)?) +} + +pub fn remove>(path: P) -> crate::Result<()> { + if path.as_ref().exists() { + Ok(remove_dir_all(path)?) + } else { + Ok(()) + } +} + +pub fn copy_file(from: P, to: Q, options: &FileOpts) -> crate::Result +where + P: AsRef, + Q: AsRef, +{ + 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(from: P, to: Q, options: &Options) -> crate::Result +where + P: AsRef, + Q: AsRef, +{ + 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; + 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

(path: P, options: &DirOpts) -> crate::Result +where + P: AsRef, +{ + let mut depth = 0; + if options.depth != 0 { + depth = options.depth + 1; + } + _get_dir_info(path, depth) +} + +fn _get_dir_info

(path: P, mut depth: u64) -> crate::Result +where + P: AsRef, +{ + 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, + }) +} diff --git a/tools/rust/cargo-tauri-bundle/src/bundle/settings.rs b/tools/rust/cargo-tauri-bundle/src/bundle/settings.rs index 91e45711c..9e6db62ca 100644 --- a/tools/rust/cargo-tauri-bundle/src/bundle/settings.rs +++ b/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", } } diff --git a/tools/rust/cargo-tauri-bundle/src/bundle/templates/appimage b/tools/rust/cargo-tauri-bundle/src/bundle/templates/appimage new file mode 100644 index 000000000..2c5f316f1 --- /dev/null +++ b/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 diff --git a/tools/rust/cargo-tauri-bundle/src/main.rs b/tools/rust/cargo-tauri-bundle/src/main.rs index d1516e4f4..c16048c4c 100644 --- a/tools/rust/cargo-tauri-bundle/src/main.rs +++ b/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))