mod app;
mod config;
use std::fmt;
use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
use clap::{Parser, Subcommand, ValueEnum};
use config::Config;
#[derive(Parser)]
#[command(version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Build {
#[arg(long, default_value = "embala.toml")]
config: PathBuf,
#[arg(long, value_delimiter = ',')]
formats: Vec<Format>,
#[arg(long, default_value = "dist")]
out_dir: PathBuf,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
Msi,
App,
Pkg,
Nupkg,
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Format::Msi => "msi",
Format::App => "app",
Format::Pkg => "pkg",
Format::Nupkg => "nupkg",
})
}
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Build {
config,
formats,
out_dir,
} => build(&config, &formats, &out_dir),
}
}
fn build(config_path: &Path, formats: &[Format], out_dir: &Path) -> Result<()> {
let config = Config::load(config_path)?;
let formats: Vec<Format> = if formats.is_empty() {
let configured = [
(Format::Msi, config.msi.is_some()),
(Format::App, config.app.is_some()),
(Format::Pkg, config.pkg.is_some()),
(Format::Nupkg, config.nupkg.is_some()),
];
let present: Vec<Format> = configured
.iter()
.filter(|(_, some)| *some)
.map(|(format, _)| *format)
.collect();
if present.is_empty() {
bail!(
"no format sections ([msi], [app], [pkg], [nupkg]) in {}",
config_path.display()
);
}
present
} else {
formats.to_vec()
};
for format in formats {
let section_present = match format {
Format::Msi => config.msi.is_some(),
Format::App => config.app.is_some(),
Format::Pkg => config.pkg.is_some(),
Format::Nupkg => config.nupkg.is_some(),
};
if !section_present {
bail!(
"{format} requested but the config has no [{format}] section ({})",
config_path.display()
);
}
build_format(format, &config, config_path, out_dir)?;
}
Ok(())
}
fn build_format(format: Format, config: &Config, config_path: &Path, out_dir: &Path) -> Result<()> {
match format {
Format::Msi => build_msi(config, config_path, out_dir),
Format::App => build_app(config, config_path, out_dir),
Format::Pkg => build_pkg(config, config_path, out_dir),
Format::Nupkg => build_nupkg(config, config_path, out_dir),
}
}
fn build_app(config: &Config, config_path: &Path, out_dir: &Path) -> Result<()> {
let section = config.app.as_ref().expect("caller checked [app] presence");
let base = config_path.parent().unwrap_or(Path::new("."));
let out = app::build(&config.package, section, base, out_dir)?;
println!("app: wrote {}", out.display());
Ok(())
}
fn build_pkg(config: &Config, config_path: &Path, out_dir: &Path) -> Result<()> {
let section = config.pkg.as_ref().expect("caller checked [pkg] presence");
let base = config_path.parent().unwrap_or(Path::new("."));
let package = &config.package;
let spec = embala_pkg::PkgSpec {
name: package.name.clone(),
display_name: package.display_name.clone(),
identifier: package.identifier.clone(),
version: package.version.clone(),
install_location: section.install_location.clone(),
enable_user_home: section.enable_user_home,
files: section
.files
.iter()
.map(|f| embala_pkg::FileSpec {
src: base.join(&f.src),
dest: f.dest.clone(),
})
.collect(),
};
let out = out_dir.join(format!("{}-{}.pkg", package.name, package.version));
std::fs::create_dir_all(out_dir)?;
embala_pkg::build(&spec, &out)?;
println!("pkg: wrote {}", out.display());
Ok(())
}
fn build_nupkg(config: &Config, config_path: &Path, out_dir: &Path) -> Result<()> {
let section = config
.nupkg
.as_ref()
.expect("caller checked [nupkg] presence");
let base = config_path.parent().unwrap_or(Path::new("."));
let package = &config.package;
let style = match section.style {
config::NupkgStyle::Embedded => embala_nupkg::Style::Embedded {
files: section
.files
.iter()
.map(|f| embala_nupkg::FileSpec {
src: base.join(&f.src),
dest: f.dest.clone(),
})
.collect(),
},
config::NupkgStyle::Download => {
let [file] = section.files.as_slice() else {
bail!(
"nupkg: style \"download\" needs exactly one files entry (its dest \
names the downloaded payload), got {}",
section.files.len()
);
};
embala_nupkg::Style::Download {
url: section.url.clone().expect("config validation requires url"),
checksum: section
.checksum
.clone()
.expect("config validation requires checksum"),
dest: file.dest.clone(),
}
}
};
let spec = embala_nupkg::NupkgSpec {
name: package.name.clone(),
display_name: package.display_name.clone(),
version: package.version.clone(),
identifier: package.identifier.clone(),
publisher: package.publisher.clone(),
description: package.description.clone(),
homepage: package.homepage.clone(),
license: package.license.clone(),
style,
};
let out = out_dir.join(format!("{}-{}.nupkg", package.name, package.version));
std::fs::create_dir_all(out_dir)?;
embala_nupkg::build(&spec, &out)?;
println!("nupkg: wrote {}", out.display());
Ok(())
}
fn build_msi(config: &Config, config_path: &Path, out_dir: &Path) -> Result<()> {
let section = config.msi.as_ref().expect("caller checked [msi] presence");
let base = config_path.parent().unwrap_or(Path::new("."));
let package = &config.package;
let spec = embala_msi::MsiSpec {
name: package.name.clone(),
display_name: package.display_name.clone(),
version: package.version.clone(),
identifier: package.identifier.clone(),
publisher: package.publisher.clone(),
description: package.description.clone(),
homepage: package.homepage.clone(),
arch: match section.arch {
config::MsiArch::X86_64 => embala_msi::MsiArch::X86_64,
config::MsiArch::Aarch64 => embala_msi::MsiArch::Aarch64,
},
main_executable: section.main_executable.clone(),
files: section
.files
.iter()
.map(|f| embala_msi::FileSpec {
src: base.join(&f.src),
dest: f.dest.clone(),
})
.collect(),
};
let out = out_dir.join(format!(
"{}-{}-{}.msi",
package.name,
package.version,
spec.arch.as_str()
));
std::fs::create_dir_all(out_dir)?;
embala_msi::build(&spec, &out)?;
println!("msi: wrote {}", out.display());
Ok(())
}