use std::io::Write;
use std::path::PathBuf;
use std::fs::File;
use std::fs;
use std::str;
use std::env;
use std::process;
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;
use toml;
#[macro_use] extern crate clap;
use clap::App;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json;
#[macro_use] extern crate failure;
use failure::Error;
use failure::ResultExt;
use rusoto_core::Region;
use rusoto_cognito_idp::*;
use rusoto_cognito_identity::*;
mod prompt;
mod account;
mod config;
mod template;
mod cache;
mod builder;
mod components;
mod upload_client;
mod file_upload;
use config::*;
use template::load_templates;
use cache::FileCache;
use builder::AppBuilder;
use components::wasm::WasmComponent;
use components::pwa::PwaComponent;
use components::icon::IconComponent;
use components::splashscreen::SplashscreenComponent;
use components::landing_page::LandingPageComponent;
enum Command {
Init,
NewProject,
Setup,
Deploy,
Update,
Build,
Unknown,
}
impl From<&str> for Command {
fn from(s: &str) -> Command {
match s {
"init" => Command::Init,
"new" => Command::NewProject,
"setup" => Command::Setup,
"deploy" => Command::Deploy,
"update" => Command::Update,
"build" => Command::Build,
_ => Command::Unknown
}
}
}
fn git_hash() -> Result<String, Error> {
let proc = process::Command::new("sh")
.arg("-c")
.arg("git rev-parse --verify HEAD --short")
.stdout(process::Stdio::piped())
.spawn()
.context("Failed to spawn git process")?;
let output = proc.wait_with_output().context("Failed to wait for git")?;
if !output.status.success() {
return Err(format_err!("git failed"))
};
let hash = std::str::from_utf8(&output.stdout)
.context("Failed to parse git output to string")?
.trim_end();
Ok(hash.to_string())
}
#[test]
fn git_hash_works() {
assert!(git_hash().is_ok());
assert_eq!(7, git_hash().unwrap().len());
}
fn random_version() -> String {
thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.collect()
}
#[test]
fn random_version_works() {
assert_eq!(7, random_version().len());
}
fn run() -> Result<(), Error> {
let handlebars = load_templates().context("Failed to load templates")?;
let yaml = load_yaml!("cli.yaml");
let app = App::from_yaml(yaml);
let input = app.get_matches();
let project_path = input.args.get("project")
.map_or(env::current_dir(),
|arg| Ok(PathBuf::from(&arg.vals[0])))
.context("Failed to get project path")?;
println!("Using project path {}", project_path.to_str().unwrap());
let conf_path = input.args.get("config")
.map_or({let mut c_path = project_path.clone();
c_path.push("woz.toml");
c_path},
|arg| PathBuf::from(&arg.vals[0]));
println!("Using config path {}", conf_path.to_str().unwrap());
let home_path = input.args.get("home")
.map_or(default_home_path(),
|arg| Ok(PathBuf::from(&arg.vals[0])))
.context("Failed to get woz home path")?;
let encryption_key = FileCache::make_key(ENCRYPTION_PASSWORD, ENCRYPTION_SALT);
let cache = FileCache::new(encryption_key, home_path.clone());
println!("Using home path {}", home_path.to_str().unwrap());
if let Some(sub) = input.subcommand_name() {
match Command::from(sub) {
Command::Setup => {
fs::create_dir_all(&home_path).context("Failed to make home directory")?;
let values = prompt::signup();
let id_provider_client = CognitoIdentityProviderClient::new(Region::UsWest2);
let id_client = CognitoIdentityClient::new(Region::UsWest2);
let user_id = account::signup(&id_provider_client,
values.email.clone(),
values.username.clone(),
values.password.clone())
.sync()
.context("Signup failed")?
.user_sub;
cache.set("user", user_id.as_bytes().to_vec())
.context("Failed to add user ID to cache")?;
let mut email_verified = false;
while !email_verified {
println!("Please check your inbox for an email to verify your account.");
if prompt::is_email_verified() {
account::setup(&id_provider_client,
&id_client,
&cache,
values.username.clone(),
values.password.clone())
.or_else(|e| {
match e {
InitiateAuthError::UserNotConfirmed(_) => {
println!("Auth failed, have you clicked the link in the verification email?");
email_verified = false;
Ok(())
},
_ => Err(e)
}
})
.context("Failed to set up account")?;
}
};
println!("Your account has been successfully set up! You can now deploy to your applications using 'woz deploy'");
},
Command::NewProject => {
let subcommand_args = input.subcommand_matches("new").unwrap();
let project_name = subcommand_args.value_of("NAME").unwrap();
let command = format!("cargo new {} --lib", project_name);
process::Command::new("sh")
.arg("-c")
.arg(command)
.output()
.context("Failed to create new project using cargo")?;
let mut cargo_conf = fs::OpenOptions::new()
.append(true)
.open(PathBuf::from(format!("{}/Cargo.toml", project_name)))
.context("Failed to open Cargo.toml")?;
cargo_conf.write_all("seed = \"0.2.9\"
wasm-bindgen = \"0.2.40\"
web-sys = \"0.3.14\"
[lib]
crate-type = [\"cdylib\"]".as_bytes()).unwrap();
let mut woz_conf = File::create(
PathBuf::from(format!("{}/woz.toml", project_name))
).context("Failed to create woz config")?;
woz_conf.write_all(format!("name=\"Example: My App\"
project_id=\"{}\"
short_name=\"MyApp\"
lib=\"wasm-bindgen\"
wasm_path=\"target/wasm32-unknown-unknown/release/{}.wasm\"
", project_name, project_name).as_bytes()).unwrap();
let mut default_lib_rs = File::create(
PathBuf::from(format!("{}/src/lib.rs", project_name))
).context("Failed to create lib.rs")?;
default_lib_rs.write_all(DEFAULT_PROJECT_LIB_RS.as_bytes()).unwrap();
println!("New project created! Please cd to ./{}", project_name);
},
Command::Init => {
println!("Initializing current project directory...");
let cargo_conf = fs::read_to_string("./Config.toml")
.context("You must be in a cargo project")?
.parse::<toml::Value>()
.context("Failed to read Cargo.toml")?;
let project_name = &cargo_conf["package"]["name"];
let mut woz_conf = File::create(
PathBuf::from(format!("{}/woz.toml", project_name))
).context("Failed to create woz config")?;
woz_conf.write_all(format!("name=\"{}\"
project_id=\"{}\"
short_name=\"{}\"
lib=\"wasm-bindgen\"
wasm_path=\"target/wasm32-unknown-unknown/release/{}.wasm\"
", project_name, project_name, project_name, project_name).as_bytes()).unwrap();
println!("Ready to be deployed with 'woz deploy'");
},
Command::Build => {
println!("Building...");
let version = random_version();
let conf_str = fs::read_to_string(conf_path.clone())
.context(format!("Couldn't find woz config file at {}",
conf_path.clone().to_str().unwrap()))?;
let conf: Config = toml::from_str(&conf_str)
.context("Failed to parse woz config")?;
let ProjectId(project_id) = conf.project_id.clone();
let mut out_path = home_path.clone();
out_path.push(&project_id);
out_path.push("pkg");
fs::create_dir_all(&out_path).context("Failed to make pkg directory")?;
let mut wasm_path = project_path.clone();
wasm_path.push(conf.wasm_path.clone());
let identity_id = cache.get("identity")
.context("Missing identity ID in cache")?;
let url = format!(
"{}://{}/{}/{}/index.html",
SCHEME,
NETLOC,
identity_id,
project_id
);
let landing_page_cmpnt = LandingPageComponent::new(
&conf,
&url,
&handlebars
);
let wasm_cmpnt = WasmComponent::new(wasm_path, &out_path);
let pwa_cmpnt = PwaComponent::new(
&conf,
&url,
&handlebars,
&version
);
let icon_cmpnt = IconComponent::new(&conf);
let splashscreen_cmpnt = SplashscreenComponent::new(&conf);
let file_prefix = String::from(out_path.to_str().unwrap());
let build_env = &conf.env.to_owned().unwrap_or(Environment::Development);
let mut app = AppBuilder::new();
app
.component(&landing_page_cmpnt)
.component(&wasm_cmpnt)
.component(&pwa_cmpnt)
.component(&icon_cmpnt)
.component(&splashscreen_cmpnt)
.build(&project_path, &file_prefix, &build_env)
.context("Failed to build app")?;
app.download().context("Failed to download files from the build")?;
println!("App package directory can be found at {}", file_prefix);
},
Command::Deploy => {
println!("Deploying...");
let version = random_version();
let conf_str = fs::read_to_string(conf_path.clone())
.context(format!("Couldn't find woz config file at {}",
conf_path.clone().to_str().unwrap()))?;
let conf: Config = toml::from_str(&conf_str)
.context("Failed to parse woz config")?;
let s3_client = upload_client::authenticated_client(&cache)
.context("Unable to initialize upload client")?;
let identity_id = cache.get("identity")
.context("Unable to retrieve user ID")?;
let ProjectId(project_id) = conf.project_id.clone();
let mut out_path = home_path.clone();
out_path.push(&project_id);
out_path.push("pkg");
fs::create_dir_all(&out_path).context("Failed to make pkg directory")?;
let key_prefix = format!("{}/{}", &identity_id, &project_id);
let mut wasm_path = project_path.clone();
wasm_path.push(conf.wasm_path.clone());
let url = format!(
"{}://{}/{}/{}/index.html",
SCHEME,
NETLOC,
identity_id,
project_id
);
let landing_page_cmpnt = LandingPageComponent::new(
&conf,
&url,
&handlebars
);
let wasm_cmpnt = WasmComponent::new(wasm_path, &out_path);
let pwa_cmpnt = PwaComponent::new(
&conf,
&url,
&handlebars,
&version
);
let icon_cmpnt = IconComponent::new(&conf);
let splashscreen_cmpnt = SplashscreenComponent::new(&conf);
let build_env = &conf.env.to_owned().unwrap_or(Environment::Development);
let mut app = AppBuilder::new();
app
.component(&landing_page_cmpnt)
.component(&wasm_cmpnt)
.component(&pwa_cmpnt)
.component(&icon_cmpnt)
.component(&splashscreen_cmpnt)
.build(&project_path, &key_prefix, &build_env)
.context("Failed to build app")?;
let app_size = app.size();
if (app_size / 1_000_000) > MAX_APP_SIZE_MB {
return Err(
format_err!(
"The maximum size for deploying an app is {}MB. Your app is {}MB",
MAX_APP_SIZE_MB,
app_size
)
)
}
app.upload(s3_client).context("Failed to upload app")?;
println!("{}", format!("Your app is available at {}", url));
}
_ => unimplemented!()
};
};
Ok(())
}
fn main() {
run()
.map_err(|e| {
println!("{}\n{}", e,
e.iter_causes()
.map(|f| format!("Caused by: {}", f))
.collect::<Vec<String>>()
.join("\n"));
std::process::exit(1)
})
.ok();
std::process::exit(0)
}