use anyhow::{Context, Result};
use clap::Parser;
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
#[derive(Parser, Debug)]
#[command(name = "eweb-tauri")]
#[command(author = "neosun100")]
#[command(version = "0.1.4")]
#[command(about = "Transform any website into a native app with Tauri", long_about = None)]
struct Args {
url: String,
#[arg(short, long)]
name: Option<String>,
#[arg(long, default_value = "1.0.0")]
app_version: String,
#[arg(short, long)]
icon: Option<String>,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
platform: Option<String>,
#[arg(short, long)]
debug: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
println!("\n🚀 eweb-tauri - Ultra lightweight cross-platform app builder\n");
let url = if args.url.starts_with("http") {
args.url.clone()
} else {
format!("https://{}", args.url)
};
let name = args.name.unwrap_or_else(|| infer_name(&url));
let safe_name = name.chars()
.filter(|c| c.is_alphanumeric() || *c == '-')
.collect::<String>()
.to_lowercase();
let output_dir = args.output.unwrap_or_else(|| ".".to_string());
let project_dir = PathBuf::from(&output_dir).join(format!("{}-tauri", name));
println!("📦 Building: {}", name);
println!("🌐 URL: {}", url);
println!("📁 Output: {}\n", project_dir.display());
fs::create_dir_all(project_dir.join("src"))?;
fs::create_dir_all(project_dir.join("src-tauri/src"))?;
fs::create_dir_all(project_dir.join("src-tauri/icons"))?;
fs::create_dir_all(project_dir.join("src-tauri/capabilities"))?;
let icons_dir = project_dir.join("src-tauri/icons");
if let Some(icon_path) = &args.icon {
process_icon(icon_path, &icons_dir)?;
} else {
println!("⚠️ No icon specified, using default");
create_default_icons(&icons_dir)?;
}
let index_html = format!(r#"<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{}</title>
</head>
<body>
<script>window.location.href = "{}";</script>
</body>
</html>"#, name, url);
fs::write(project_dir.join("src/index.html"), index_html)?;
let tauri_config = json!({
"$schema": "https://schema.tauri.app/config/2",
"productName": name,
"version": args.app_version,
"identifier": format!("com.eweb.{}", safe_name),
"build": {
"frontendDist": "../src"
},
"app": {
"windows": [{
"title": name,
"width": 1280,
"height": 800,
"resizable": true,
"fullscreen": false,
"url": url
}],
"security": {
"csp": serde_json::Value::Null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
});
fs::write(
project_dir.join("src-tauri/tauri.conf.json"),
serde_json::to_string_pretty(&tauri_config)?
)?;
let cargo_toml = format!(r#"[package]
name = "{}"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = {{ version = "2", features = [] }}
[dependencies]
tauri = {{ version = "2", features = [] }}
tauri-plugin-opener = "2"
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
[profile.release]
strip = true
lto = true
codegen-units = 1
"#, safe_name);
fs::write(project_dir.join("src-tauri/Cargo.toml"), cargo_toml)?;
let main_rs = r#"#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
"#;
fs::write(project_dir.join("src-tauri/src/main.rs"), main_rs)?;
let lib_rs = r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
"#;
fs::write(project_dir.join("src-tauri/src/lib.rs"), lib_rs)?;
fs::write(project_dir.join("src-tauri/build.rs"), "fn main() { tauri_build::build() }")?;
let capability = json!({
"$schema": "https://schema.tauri.app/config/2",
"identifier": "default",
"description": "Default capability",
"windows": ["main"],
"permissions": ["core:default", "opener:default"]
});
fs::write(
project_dir.join("src-tauri/capabilities/default.json"),
serde_json::to_string_pretty(&capability)?
)?;
println!("✅ Project created successfully!");
println!("\n📁 Project: {}", project_dir.display());
if let Some(platform) = args.platform {
let build_type = if args.debug { "debug" } else { "release" };
println!("\n🔨 Building {} for {}...", build_type, platform);
let src_tauri = project_dir.join("src-tauri");
let result = match platform.as_str() {
"android" => {
let init_result = Command::new("cargo")
.args(["tauri", "android", "init"])
.current_dir(&src_tauri)
.status();
if init_result.is_ok() {
copy_android_icons(&project_dir);
}
init_result.and_then(|_| {
let mut cmd = Command::new("cargo");
cmd.args(["tauri", "android", "build"]);
if args.debug {
cmd.arg("--debug");
}
cmd.current_dir(&src_tauri).status()
})
}
"ios" => {
Command::new("cargo")
.args(["tauri", "ios", "init"])
.current_dir(&src_tauri)
.status()
.and_then(|_| {
Command::new("cargo")
.args(["tauri", "ios", "build"])
.current_dir(&src_tauri)
.status()
})
}
_ => {
Command::new("cargo")
.args(["tauri", "build"])
.current_dir(&src_tauri)
.status()
}
};
match result {
Ok(status) if status.success() => {
println!("\n✅ Build completed!");
rename_build_artifacts(&project_dir, &name, &args.app_version, &platform, args.debug);
}
_ => println!("\n❌ Build failed"),
}
} else {
println!("\n🔨 To build the app:");
println!(" cd \"{}/src-tauri\"", project_dir.display());
println!(" cargo tauri build\n");
println!("📱 Mobile builds:");
println!(" cargo tauri android init && cargo tauri android build");
println!(" cargo tauri ios init && cargo tauri ios build\n");
println!("💡 App size: ~3-10 MB (vs Electron 150+ MB)");
}
Ok(())
}
fn infer_name(url: &str) -> String {
if let Ok(parsed) = url::Url::parse(url) {
parsed.host_str()
.unwrap_or("app")
.replace("www.", "")
.split('.')
.next()
.unwrap_or("app")
.to_string()
} else {
"app".to_string()
}
}
fn process_icon(icon_path: &str, icons_dir: &PathBuf) -> Result<()> {
let source_path = if icon_path.starts_with("http") {
println!("📥 Downloading icon from {}...", icon_path);
let response = reqwest::blocking::get(icon_path)
.context("Failed to download icon")?;
let bytes = response.bytes()?;
let temp_path = icons_dir.join("source.png");
fs::write(&temp_path, &bytes)?;
println!("✅ Icon downloaded");
temp_path
} else {
PathBuf::from(icon_path)
};
println!("🎨 Generating icons...");
let img = image::open(&source_path).context("Failed to open icon")?;
let sizes = [(32, "32x32.png"), (128, "128x128.png"), (256, "128x128@2x.png"), (256, "256x256.png"), (512, "512x512.png")];
for (size, filename) in sizes {
let resized = img.resize_exact(size, size, image::imageops::FilterType::Lanczos3);
resized.save(icons_dir.join(filename))?;
}
let icon_256 = img.resize_exact(256, 256, image::imageops::FilterType::Lanczos3);
icon_256.save(icons_dir.join("icon.png"))?;
fs::copy(icons_dir.join("icon.png"), icons_dir.join("icon.ico"))?;
fs::copy(icons_dir.join("icon.png"), icons_dir.join("icon.icns"))?;
if source_path.file_name().map(|n| n == "source.png").unwrap_or(false) {
let _ = fs::remove_file(source_path);
}
println!("✅ Icons generated");
Ok(())
}
fn create_default_icons(icons_dir: &PathBuf) -> Result<()> {
let img = image::RgbaImage::new(256, 256);
let sizes = [(32, "32x32.png"), (128, "128x128.png"), (256, "128x128@2x.png")];
for (size, filename) in sizes {
let resized = image::imageops::resize(&img, size, size, image::imageops::FilterType::Nearest);
resized.save(icons_dir.join(filename))?;
}
let icon_256 = image::imageops::resize(&img, 256, 256, image::imageops::FilterType::Nearest);
icon_256.save(icons_dir.join("icon.png"))?;
fs::copy(icons_dir.join("icon.png"), icons_dir.join("icon.ico"))?;
fs::copy(icons_dir.join("icon.png"), icons_dir.join("icon.icns"))?;
Ok(())
}
fn copy_android_icons(project_dir: &PathBuf) {
let source_icon = project_dir.join("src-tauri/icons/icon.png");
if !source_icon.exists() {
return;
}
let img = match image::open(&source_icon) {
Ok(img) => img,
Err(_) => return,
};
let res_dir = project_dir.join("src-tauri/gen/android/app/src/main/res");
let sizes = [
("mipmap-mdpi", 48),
("mipmap-hdpi", 72),
("mipmap-xhdpi", 96),
("mipmap-xxhdpi", 144),
("mipmap-xxxhdpi", 192),
];
println!("🎨 Copying icons to Android resources...");
for (dir, size) in sizes {
let mipmap_dir = res_dir.join(dir);
if mipmap_dir.exists() {
let resized = img.resize_exact(size, size, image::imageops::FilterType::Lanczos3);
let _ = resized.save(mipmap_dir.join("ic_launcher.png"));
let _ = resized.save(mipmap_dir.join("ic_launcher_round.png"));
let fg_size = (size as f32 * 1.5) as u32;
let fg = img.resize_exact(fg_size, fg_size, image::imageops::FilterType::Lanczos3);
let _ = fg.save(mipmap_dir.join("ic_launcher_foreground.png"));
}
}
println!("✅ Android icons updated");
}
fn rename_build_artifacts(project_dir: &PathBuf, name: &str, version: &str, platform: &str, debug: bool) {
let output_dir = project_dir.join("dist");
let _ = fs::create_dir_all(&output_dir);
let build_type = if debug { "debug" } else { "release" };
match platform {
"android" => {
let android_dir = project_dir.join("src-tauri/gen/android/app/build/outputs");
let apk_subdir = if debug { "debug" } else { "release" };
let apk_suffix = if debug { "-debug" } else { "-unsigned" };
let apk_src = android_dir.join(format!("apk/universal/{}/app-universal-{}{}.apk", apk_subdir, apk_subdir, apk_suffix));
if apk_src.exists() {
let suffix = if debug { "-debug" } else { "" };
let apk_dst = output_dir.join(format!("{}-{}-universal{}.apk", name, version, suffix));
if fs::copy(&apk_src, &apk_dst).is_ok() {
println!("📦 APK: {}", apk_dst.display());
if debug {
println!(" ✅ Debug APK can be installed directly!");
}
}
}
if !debug {
let aab_src = android_dir.join("bundle/universalRelease/app-universal-release.aab");
if aab_src.exists() {
let aab_dst = output_dir.join(format!("{}-{}-universal.aab", name, version));
if fs::copy(&aab_src, &aab_dst).is_ok() {
println!("📦 AAB: {}", aab_dst.display());
}
}
}
let archs = [("arm64-v8a", "arm64"), ("armeabi-v7a", "arm32"), ("x86_64", "x86_64"), ("x86", "x86")];
for (arch_dir, arch_name) in archs {
let arch_apk = android_dir.join(format!("apk/{}/{}/app-{}-{}{}.apk", arch_dir, apk_subdir, arch_dir, apk_subdir, apk_suffix));
if arch_apk.exists() {
let suffix = if debug { "-debug" } else { "" };
let dst = output_dir.join(format!("{}-{}-{}{}.apk", name, version, arch_name, suffix));
if fs::copy(&arch_apk, &dst).is_ok() {
println!("📦 APK ({}): {}", arch_name, dst.display());
}
}
}
println!("\n📁 All artifacts copied to: {}", output_dir.display());
}
"linux" => {
let bundle_dir = project_dir.join("src-tauri/target/release/bundle");
if let Ok(entries) = fs::read_dir(bundle_dir.join("deb")) {
for entry in entries.flatten() {
if entry.path().extension().map(|e| e == "deb").unwrap_or(false) {
let dst = output_dir.join(format!("{}-{}-amd64.deb", name, version));
if fs::copy(entry.path(), &dst).is_ok() {
println!("📦 DEB: {}", dst.display());
}
}
}
}
if let Ok(entries) = fs::read_dir(bundle_dir.join("rpm")) {
for entry in entries.flatten() {
if entry.path().extension().map(|e| e == "rpm").unwrap_or(false) {
let dst = output_dir.join(format!("{}-{}-x86_64.rpm", name, version));
if fs::copy(entry.path(), &dst).is_ok() {
println!("📦 RPM: {}", dst.display());
}
}
}
}
if let Ok(entries) = fs::read_dir(bundle_dir.join("appimage")) {
for entry in entries.flatten() {
if entry.path().extension().map(|e| e == "AppImage").unwrap_or(false) {
let dst = output_dir.join(format!("{}-{}-x86_64.AppImage", name, version));
if fs::copy(entry.path(), &dst).is_ok() {
println!("📦 AppImage: {}", dst.display());
}
}
}
}
println!("\n📁 All artifacts copied to: {}", output_dir.display());
}
"mac" => {
let bundle_dir = project_dir.join("src-tauri/target/release/bundle");
if let Ok(entries) = fs::read_dir(bundle_dir.join("dmg")) {
for entry in entries.flatten() {
if entry.path().extension().map(|e| e == "dmg").unwrap_or(false) {
let dst = output_dir.join(format!("{}-{}-macos.dmg", name, version));
if fs::copy(entry.path(), &dst).is_ok() {
println!("📦 DMG: {}", dst.display());
}
}
}
}
println!("\n📁 All artifacts copied to: {}", output_dir.display());
}
"windows" => {
let bundle_dir = project_dir.join("src-tauri/target/release/bundle");
if let Ok(entries) = fs::read_dir(bundle_dir.join("msi")) {
for entry in entries.flatten() {
if entry.path().extension().map(|e| e == "msi").unwrap_or(false) {
let dst = output_dir.join(format!("{}-{}-x64.msi", name, version));
if fs::copy(entry.path(), &dst).is_ok() {
println!("📦 MSI: {}", dst.display());
}
}
}
}
if let Ok(entries) = fs::read_dir(bundle_dir.join("nsis")) {
for entry in entries.flatten() {
if entry.path().extension().map(|e| e == "exe").unwrap_or(false) {
let dst = output_dir.join(format!("{}-{}-x64-setup.exe", name, version));
if fs::copy(entry.path(), &dst).is_ok() {
println!("📦 EXE: {}", dst.display());
}
}
}
}
println!("\n📁 All artifacts copied to: {}", output_dir.display());
}
_ => {}
}
}