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
This commit is contained in:
Tensor-Programming 2019-11-26 15:16:21 -05:00 committed by nothingismagick
parent 70ccd1659d
commit 52e20d2544
9 changed files with 420 additions and 14 deletions

7
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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)?,
});
}

View File

@ -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])
}

View File

@ -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();

View File

@ -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,
})
}

View File

@ -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",
}
}

View File

@ -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

View File

@ -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))