use crate::config::{Component, Package, SetupSection, Shortcut, ShortcutLocation};
pub fn lower(package: &Package, setup: &SetupSection, license_text: Option<&str>) -> String {
let mut out = String::new();
out.push_str(HEADER);
out.push_str(&format!(
"-- Package: {} {} ({})\n",
package.name, package.version, package.identifier
));
out.push_str("\nlocal pkg = embala.package\n");
if setup.install_mode == crate::config::InstallMode::UserChoice {
out.push_str("\nembala.ui.page{ kind = \"mode\" }\n");
}
out.push_str("\nembala.ui.page{ kind = \"welcome\", title = pkg.display_name, body = pkg.description }\n");
if let Some(text) = license_text {
out.push_str("\nembala.ui.page{ kind = \"license\", must_accept = true, text = ");
out.push_str(&lua_str(text));
out.push_str(" }\n");
}
out.push_str(
"\nembala.ui.page{ kind = \"directory\", default = embala.install_dir, allow_change = true }\n",
);
if !setup.components.is_empty() {
out.push_str("\nembala.ui.page{\n kind = \"components\",\n items = {\n");
for c in &setup.components {
out.push_str(" ");
out.push_str(&component_item(c));
out.push_str(",\n");
}
out.push_str(" },\n}\n");
}
out.push_str("\n-- Payload files (always installed).\n");
for f in &setup.files {
out.push_str(&format!(
"embala.fs.copy({}, {})\n",
lua_str(&f.dest),
lua_str(&f.dest)
));
}
for c in &setup.components {
if c.files.is_empty() {
continue;
}
out.push_str(&format!(
"\n-- Component {} (installed when selected).\nif embala.ui.selected({}) then\n",
lua_str(&c.id),
lua_str(&c.id)
));
for f in &c.files {
out.push_str(&format!(
" embala.fs.copy({}, {})\n",
lua_str(&f.dest),
lua_str(&f.dest)
));
}
out.push_str("end\n");
}
if let Some(main) = &setup.main_executable {
out.push_str(&format!(
"\n-- Start-menu shortcut for the main executable.\nembala.shortcut.create{{ name = pkg.display_name, target = {}, location = \"start-menu\", description = pkg.description }}\n",
lua_str(main)
));
}
if !setup.shortcuts.is_empty() {
out.push_str("\n-- Shortcuts.\n");
for s in &setup.shortcuts {
out.push_str(&shortcut_call(s));
}
}
out.push_str("\n-- Add/Remove Programs registration.\nembala.arp.register{\n");
out.push_str(" display_name = pkg.display_name,\n");
out.push_str(" version = pkg.version,\n");
out.push_str(" publisher = pkg.publisher,\n");
out.push_str(" install_location = embala.install_dir,\n");
out.push_str(" uninstall_string = '\"' .. embala.install_dir .. '\\\\uninstall.exe\"',\n");
if let Some(main) = &setup.main_executable {
out.push_str(&format!(
" display_icon = embala.install_dir .. '\\\\' .. {},\n",
lua_str(&main.replace('/', "\\"))
));
}
out.push_str("}\n");
match &setup.main_executable {
Some(main) => out.push_str(&format!(
"\nembala.ui.page{{ kind = \"finish\", run = {{ target = {} }} }}\n",
lua_str(main)
)),
None => out.push_str("\nembala.ui.page{ kind = \"finish\" }\n"),
}
out
}
const HEADER: &str = "\
-- Generated by embala from [setup]; do not edit by hand.
-- Declarative TOML lowered to the embala.* installer API (spec R19). Change the
-- installer by editing [setup] in embala.toml and rebuilding, or ship a raw
-- script via [setup].script.
";
fn component_item(c: &Component) -> String {
let mut s = format!("{{ id = {}, label = {}", lua_str(&c.id), lua_str(&c.label));
if let Some(desc) = &c.description {
s.push_str(&format!(", description = {}", lua_str(desc)));
}
s.push_str(&format!(", default = {} }}", c.default));
s
}
fn shortcut_call(s: &Shortcut) -> String {
let location = match s.location {
ShortcutLocation::StartMenu => "start-menu",
ShortcutLocation::Desktop => "desktop",
};
let mut call = format!(
"embala.shortcut.create{{ name = {}, target = {}, location = {}",
lua_str(&s.name),
lua_str(&s.target),
lua_str(location)
);
if let Some(args) = &s.args {
call.push_str(&format!(", args = {}", lua_str(args)));
}
if let Some(icon) = &s.icon {
call.push_str(&format!(", icon = {}", lua_str(icon)));
}
call.push_str(" }\n");
call
}
pub fn lua_str(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\{:03}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn fixtures_dir() -> std::path::PathBuf {
std::path::PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../fixtures/hello"))
}
fn hello_config() -> Config {
let text = std::fs::read_to_string(fixtures_dir().join("embala.toml")).expect("fixture");
toml::from_str(&text).expect("fixture parses")
}
#[test]
fn generated_install_lua_matches_golden() {
let config = hello_config();
let setup = config.setup.as_ref().expect("fixture has [setup]");
let license = std::fs::read_to_string(fixtures_dir().join("license.txt")).expect("license");
let generated = lower(&config.package, setup, Some(&license));
let golden = std::fs::read_to_string(fixtures_dir().join("install.lua.expected"))
.expect("golden install.lua.expected");
assert_eq!(generated, golden);
}
#[test]
fn lua_str_escapes_quotes_backslash_newline_and_passes_unicode() {
assert_eq!(lua_str("plain"), "\"plain\"");
assert_eq!(lua_str("a\"b"), "\"a\\\"b\"");
assert_eq!(lua_str("a\\b"), "\"a\\\\b\"");
assert_eq!(lua_str("line1\nline2"), "\"line1\\nline2\"");
assert_eq!(lua_str("tab\there"), "\"tab\\there\"");
assert_eq!(lua_str("\u{1}5"), "\"\\0015\"");
assert_eq!(lua_str("café — ☕"), "\"café — ☕\"");
}
#[test]
fn component_files_are_gated_top_level_files_are_not() {
let config = hello_config();
let setup = config.setup.as_ref().unwrap();
let out = lower(&config.package, setup, None);
assert!(out.contains(
"-- Payload files (always installed).\nembala.fs.copy(\"hello.exe\", \"hello.exe\")\n"
));
assert!(out.contains("if embala.ui.selected(\"docs\") then\n embala.fs.copy(\"readme.txt\", \"readme.txt\")\nend\n"));
}
#[test]
fn user_choice_prepends_a_mode_page_fixed_mode_does_not() {
let config = hello_config();
let mut setup_toml: SetupSection = toml::from_str(
"arch = \"x86_64\"\nfiles = [{ src = \"dist/hello.exe\", dest = \"hello.exe\" }]",
)
.unwrap();
assert!(!lower(&config.package, &setup_toml, None).contains("kind = \"mode\""));
setup_toml.install_mode = crate::config::InstallMode::UserChoice;
assert!(
lower(&config.package, &setup_toml, None).contains("embala.ui.page{ kind = \"mode\" }")
);
}
}