use crate::prelude::*;
use async_std::fs::*;
use async_std::path::{Path, PathBuf};
use regex::Regex;
use std::collections::HashSet;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Manifest {
pub application: Application,
pub description: Description,
pub package: Package,
#[serde(rename = "dependency")]
pub dependencies: Option<Vec<Dependency>>,
pub nwjs: NWJS,
pub windows: Option<Windows>,
pub innosetup: Option<InnoSetup>,
pub firewall: Option<Firewall>,
pub languages: Option<Languages>,
pub macos_disk_image: Option<MacOsDiskImage>,
pub snap: Option<Snap>,
pub images: Option<Images>,
pub action: Option<Vec<Action>>,
}
impl Manifest {
pub async fn locate(location: Option<String>) -> Result<PathBuf> {
let cwd = current_dir().await;
let location = if let Some(location) = location {
if let Some(stripped) = location.strip_prefix("~/") {
home::home_dir()
.expect("unable to get home directory")
.join(stripped)
.into()
} else {
let location = Path::new(&location).to_path_buf();
if location.is_absolute() {
location
} else {
cwd.join(&location)
}
}
} else {
cwd
};
let locations = [
&location,
&location.with_extension("toml"),
&location.join("nw.toml"),
];
for location in locations.iter() {
if let Ok(location) = location.canonicalize().await {
if location.is_file().await {
return Ok(sanitize(location));
}
}
}
Err("Unable to locate 'nw.toml' manifest".into())
}
pub async fn load(toml: &PathBuf) -> Result<Manifest> {
let nw_toml = read_to_string(toml).await?;
let mut manifest: Manifest = match toml::from_str(&nw_toml) {
Ok(manifest) => manifest,
Err(err) => {
return Err(format!("Error loading nw.toml: {err}").into());
}
};
let folder = toml.parent().unwrap();
resolve_value_paths(
folder,
&mut [
&mut manifest.application.name,
&mut manifest.application.title,
&mut manifest.application.version,
&mut manifest.application.organization,
&mut manifest.description.short,
&mut manifest.description.long,
],
)
.await?;
manifest.sanity_checks()?;
Ok(manifest)
}
pub fn sanity_checks(&self) -> Result<()> {
let regex = Regex::new(r"^[^\s]*[a-z0-9-_]*$").unwrap();
if !regex.is_match(&self.application.name) {
return Err(format!("invalid application name '{}'", self.application.name).into());
}
if self.description.short.len() > 78 {
return Err(Error::ShortDescriptionIsTooLong);
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Application {
pub name: String,
pub version: String,
pub title: String,
pub authors: Option<String>,
pub organization: String,
pub copyright: Option<String>,
pub trademarks: Option<String>,
pub license: Option<String>,
pub eula: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Description {
pub short: String,
pub long: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionContext {
pub name: Option<String>,
pub argv: Option<Vec<String>>,
pub cmd: Option<String>,
pub cwd: Option<String>,
pub platform: Option<Platform>,
pub arch: Option<Architecture>,
pub family: Option<PlatformFamily>,
pub env: Option<Vec<String>>,
}
impl ExecutionContext {
pub fn validate(&self) -> Result<()> {
if self.argv.is_none() && self.cmd.is_none() {
Err("no command or arguments specified".into())
} else if self.argv.is_some() && self.cmd.is_some() {
Err(format!("invalid execution arguments - both 'argv' and 'cmd' are not allowed argv: {:?} cmd: {:?}",
self.argv.as_ref().unwrap(),
self.cmd.as_ref().unwrap()
).into())
} else {
Ok(())
}
}
pub fn get_args(&self) -> Result<ExecArgs> {
ExecArgs::try_new(&self.cmd, &self.argv)
}
pub fn display(&self, tpl: &Tpl) -> String {
self.name.clone().unwrap_or_else(|| {
let descr = self.get_args().unwrap().get(tpl).join(" ");
if descr.len() > 30 {
format!("{} ...", &descr[0..30])
} else {
descr
}
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Execute {
#[serde(rename = "build")]
Build(ExecutionContext),
#[serde(rename = "pack")]
Pack(ExecutionContext),
#[serde(rename = "deploy")]
Deploy(ExecutionContext),
#[serde(rename = "publish")]
Publish(ExecutionContext),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Build {
WASM {
clean: Option<bool>,
purge: Option<bool>,
dev: Option<bool>,
name: Option<String>,
outdir: Option<String>,
args: Option<String>,
env: Option<Vec<String>>,
},
NPM {
clean: Option<bool>,
#[serde(rename = "clean-package-lock")]
clean_package_lock: Option<bool>,
dev: Option<bool>,
args: Option<String>,
env: Option<Vec<String>>,
},
#[serde(rename = "custom")]
Custom(ExecutionContext),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Package {
pub gitignore: Option<bool>,
pub build: Option<Vec<Build>>,
pub archive: Option<Archive>,
pub disable: Option<Vec<Target>>,
pub signatures: Option<Vec<Signature>>,
pub resources: Option<String>,
pub source: Option<String>,
pub include: Option<Vec<CopyFilter>>,
pub exclude: Option<Vec<CopyFilter>>,
pub hidden: Option<bool>,
pub root: Option<String>,
pub output: Option<String>,
pub use_app_nw: Option<bool>,
pub update_package_json: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum CopyFilter {
#[serde(rename = "glob")]
Glob(Vec<String>),
#[serde(rename = "regex")]
Regex(Vec<String>),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename = "copy", deny_unknown_fields)]
pub struct Copy {
pub glob: Option<Vec<String>>,
pub regex: Option<Vec<String>>,
pub to: String,
pub hidden: Option<bool>,
pub flatten: Option<bool>,
pub file: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Git {
pub url: String,
pub branch: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Dependency {
pub name: Option<String>,
pub platform: Option<Vec<Platform>>,
pub arch: Option<Vec<Architecture>>,
pub git: Option<Git>,
pub run: Vec<ExecutionContext>,
pub copy: Vec<Copy>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename = "node-webkit", deny_unknown_fields)]
pub struct NWJS {
version: String,
#[allow(unused)]
windows: Option<String>,
#[allow(unused)]
macos: Option<String>,
#[allow(unused)]
linux: Option<String>,
pub ffmpeg: Option<bool>,
pub sdk: Option<bool>,
}
impl NWJS {
pub fn version(&self) -> String {
cfg_if! {
if #[cfg(target_os = "windows")] {
self.windows.as_ref().unwrap_or(&self.version).clone()
} else if #[cfg(target_os = "macos")] {
self.macos.as_ref().unwrap_or(&self.version).clone()
} else if #[cfg(target_os = "linux")] {
self.linux.as_ref().unwrap_or(&self.version).clone()
}
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InnoSetup {
pub resize_wizard_files: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Windows {
pub uuid: String,
pub group: String,
pub executable: Option<String>,
pub run_on_startup: Option<String>,
pub run_after_setup: Option<bool>,
pub setup_icon: Option<String>,
pub resources: Option<Vec<WindowsResourceString>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum WindowsResourceString {
ProductName(String),
ProductVersion(String),
FileVersion(String),
FileDescription(String),
CompanyName(String),
LegalCopyright(String),
LegalTrademarks(String),
InternalName(String),
Custom { name: String, value: String },
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Snap {
pub channel: Option<Channel>,
pub confinement: Option<Confinement>,
pub interfaces: Option<HashSet<String>>,
pub packages: Option<HashSet<String>>,
pub base: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Firewall {
pub application: Option<FirewallApplication>,
pub rules: Option<Vec<FirewallRule>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FirewallApplication {
pub direction: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FirewallRule {
pub name: String,
pub program: String,
pub direction: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Languages {
pub languages: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageJson {
pub name: String,
pub main: String,
pub description: Option<String>,
pub version: Option<String>,
}
impl PackageJson {
pub fn try_load<P>(filepath: P) -> Result<PackageJson>
where
P: AsRef<std::path::Path>,
{
let text = std::fs::read_to_string(filepath)?;
let package_json: PackageJson = serde_json::from_str(&text)?;
Ok(package_json)
}
pub async fn try_store<P>(&self, filepath: P) -> Result<()>
where
P: AsRef<std::path::Path>,
{
let text = serde_json::to_string(self)?;
std::fs::write(filepath.as_ref(), text)?;
Ok(())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub enum Algorithm {
STORE,
BZIP2,
#[default]
DEFLATE,
ZSTD,
}
impl From<Algorithm> for zip::CompressionMethod {
fn from(algorithm: Algorithm) -> zip::CompressionMethod {
match algorithm {
Algorithm::STORE => zip::CompressionMethod::Stored,
Algorithm::BZIP2 => zip::CompressionMethod::Bzip2,
Algorithm::DEFLATE => zip::CompressionMethod::Deflated,
Algorithm::ZSTD => zip::CompressionMethod::Zstd,
}
}
}
impl ToString for Algorithm {
fn to_string(&self) -> String {
match self {
Algorithm::STORE => "STORE",
Algorithm::BZIP2 => "BZIP2",
Algorithm::DEFLATE => "DEFLATE",
Algorithm::ZSTD => "ZSTD",
}
.into()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Archive {
pub include: Option<bool>,
pub algorithm: Option<Algorithm>,
pub subfolder: Option<bool>,
}
impl Default for Archive {
fn default() -> Self {
Archive {
include: Some(true),
algorithm: Some(Algorithm::default()),
subfolder: Some(true),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Signature {
SHA256,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MacOsDiskImage {
pub window_caption_height: Option<i32>,
pub window_position: Option<[i32; 2]>,
pub window_size: Option<[i32; 2]>,
pub icon_size: Option<i32>,
pub application_icon_position: Option<[i32; 2]>,
pub system_applications_folder_position: Option<[i32; 2]>,
}
impl MacOsDiskImage {
pub fn window_caption_height(&self) -> i32 {
self.window_caption_height.unwrap_or(60)
}
pub fn window_position(&self) -> [i32; 2] {
self.window_position.unwrap_or([200, 200])
}
pub fn window_size(&self) -> [i32; 2] {
self.window_size.unwrap_or([485, 330])
}
pub fn icon_size(&self) -> i32 {
self.icon_size.unwrap_or(72)
}
pub fn application_icon_position(&self) -> [i32; 2] {
self.application_icon_position.unwrap_or([100, 158])
}
pub fn system_applications_folder_position(&self) -> [i32; 2] {
self.system_applications_folder_position
.unwrap_or([385, 158])
}
}
async fn resolve_value_paths(folder: &Path, paths: &mut [&mut String]) -> Result<()> {
for path in paths.iter_mut() {
if is_value_path(path) {
let location = path.clone();
path.clear();
let value = match load_value_path(folder, &location).await {
Ok(value) => value,
Err(err) => {
return Err(format!(
"unable to locate `{}` in `{}`: {}",
location,
folder.display(),
err
)
.into())
}
};
path.push_str(&value);
}
}
Ok(())
}
fn is_value_path(v: &str) -> bool {
v.contains("::")
}
async fn load_value_path(folder: &Path, location: &str) -> Result<String> {
let parts = location.split("::").collect::<Vec<_>>();
let filename = sanitize(folder.join(parts[0]).canonicalize().await?);
let value_path = parts[1].split('.').collect::<Vec<_>>();
let extension = filename
.extension()
.unwrap_or_else(|| {
panic!(
"unable to determine file type for file `{}` due to missing extension",
filename.display()
)
})
.to_str()
.unwrap();
match extension {
"toml" => {
let text = std::fs::read_to_string(&filename)?;
let mut v: &toml::Value = &toml::from_str(&text)?;
for field in value_path.iter() {
v = v.get(field).ok_or_else(|| {
Error::String(format!(
"unable to resolve the value `{}` in `{}`",
value_path.join("."),
filename.display()
))
})?;
}
Ok(v.as_str().unwrap().to_string())
}
"json" => {
let text = std::fs::read_to_string(&filename)?;
let mut v: &serde_json::Value = &serde_json::from_str(&text)?;
for field in value_path.iter() {
v = v.get(field).ok_or_else(|| {
Error::String(format!(
"unable to resolve the value `{}` in `{}`",
value_path.join("."),
filename.display()
))
})?;
}
Ok(v.as_str().unwrap().to_string())
}
_ => Err(format!("path parser: file extension `{extension}` is not supported").into()),
}
}