mod build;
mod pack;
mod pipeline;
mod test;
use crate::{
build::{build_project, BuildProfile},
pack::pack_assets_and_write_to_file,
pipeline::{Pipeline, PipelineCommand},
test::{test_project, TestProfile},
};
use cargo_metadata::MetadataCommand;
use clap::{Arg, Command as App};
use dirs::home_dir;
use hotwatch::{Event, Hotwatch};
use oxygengine_ignite_types::IgniteTypeDefinition;
use serde::Deserialize;
use std::{
collections::{HashMap, HashSet},
env::{current_dir, current_exe, set_current_dir, vars},
fs::{copy, create_dir_all, read_dir, read_to_string, remove_dir_all, remove_file, write},
io::{Error, ErrorKind, Result},
path::{Path, PathBuf},
process::{Command, Stdio},
sync::mpsc::channel,
thread::spawn,
time::Instant,
};
enum ActionType {
PreCreate,
PostCreate,
PreBuild,
PostBuild,
}
#[derive(Default, Deserialize)]
struct PresetManifest {
#[serde(default)]
pub notes: Actions,
#[serde(default)]
pub scripts: Actions,
}
impl PresetManifest {
pub fn print_note(&self, action: ActionType) {
if let Some(text) = self.notes.format(action) {
println!("{}", text);
}
}
pub fn execute_script(&self, action: ActionType, wkdir: &Path) -> Result<()> {
if let Some(mut text) = self.scripts.format(action) {
for (key, value) in vars() {
text = text.replace(&format!("~${}$~", key), &value);
}
let parts = text
.split("<|>")
.map(|part| part.trim())
.collect::<Vec<_>>();
let output = Command::new(parts[0])
.args(&parts[1..])
.envs(vars().map(|(k, v)| (k, v)))
.current_dir(wkdir)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output();
println!("{}", parts.join(" "));
output?;
}
Ok(())
}
}
#[derive(Default, Deserialize)]
struct Actions {
pub precreate: Option<String>,
pub postcreate: Option<String>,
pub prebuild: Option<String>,
pub postbuild: Option<String>,
#[serde(skip)]
pub dictionary: HashMap<String, String>,
}
impl Actions {
pub fn format(&self, action: ActionType) -> Option<String> {
let text = match action {
ActionType::PreCreate => &self.precreate,
ActionType::PostCreate => &self.postcreate,
ActionType::PreBuild => &self.prebuild,
ActionType::PostBuild => &self.postbuild,
};
let mut text = if let Some(text) = text {
text.clone()
} else {
return None;
};
for (key, value) in &self.dictionary {
text = text.replace(&format!("~%{}%~", key), value);
}
Some(text)
}
}
#[derive(Default, Deserialize)]
struct ProjectMeta {
#[serde(default)]
pub sccache_bin: Option<PathBuf>,
#[serde(default)]
pub sccache_dir: Option<PathBuf>,
}
#[allow(clippy::cognitive_complexity)]
#[tokio::main]
async fn main() -> Result<()> {
let meta = MetadataCommand::new().exec();
let (mut root_path, project_meta) = if let Ok(meta) = meta {
meta.packages
.iter()
.find_map(|p| {
if p.name == env!("CARGO_PKG_NAME") && !p.metadata.is_null() {
let root_path = p.manifest_path.clone();
let project_meta = if let Ok(project_meta) =
serde_json::from_value::<ProjectMeta>(p.metadata.clone())
{
project_meta
} else {
ProjectMeta::default()
};
Some((root_path.into(), project_meta))
} else {
None
}
})
.unwrap_or_else(|| (current_exe().unwrap(), ProjectMeta::default()))
} else {
(current_exe()?, ProjectMeta::default())
};
root_path.pop();
let presets_path = if let Ok(path) = std::env::var("OXY_PRESETS_DIR") {
PathBuf::from(path)
} else {
let mut presets_path = root_path.join("presets");
if !presets_path.exists() {
presets_path = if let Some(path) = home_dir() {
path
} else {
return Err(Error::new(
ErrorKind::NotFound,
"There is no HOME directory on this machine",
));
}
.join(".ignite")
.join("presets");
}
presets_path
};
let has_presets = std::env::var("OXY_DONT_AUTO_UPDATE").is_ok()
|| read_dir(&presets_path)
.map(|iter| iter.count() > 0)
.unwrap_or_default();
let update_presets = std::env::var("OXY_UPDATE_PRESETS").is_ok();
let update_pack_file = std::env::var("OXY_UPDATE_FILE");
if !has_presets || update_presets {
if update_presets {
let _ = remove_dir_all(&presets_path);
}
let bytes = if let Ok(ref update_pack_file) = update_pack_file {
std::fs::read(update_pack_file)
.unwrap_or_else(|_| panic!("Could not get bytes from {:?} file", update_pack_file))
} else {
let url = format!(
"https://oxygengine.io/ignite-presets/oxygengine-presets-{}.pack",
env!("CARGO_PKG_VERSION")
);
println!(
"There are no presets installed in {:?} - trying to download them now from: {:?}",
presets_path, url
);
let bytes = reqwest::blocking::get(&url)
.unwrap_or_else(|_| panic!("Request for {:?} failed", url))
.bytes()
.unwrap_or_else(|_| panic!("Could not get bytes from {:?} response", url));
bytes.as_ref().to_owned()
};
let files = bincode::deserialize::<HashMap<String, Vec<u8>>>(&bytes)
.unwrap_or_else(|_| panic!("Could not unpack files from presets pack"));
let _ = create_dir_all(&presets_path);
for (fname, bytes) in files {
let path = presets_path.join(fname);
let mut dir_path = path.clone();
dir_path.pop();
let _ = create_dir_all(&dir_path);
println!("Store file: {:?}", path);
write(path, bytes)?;
}
}
let presets_list = if presets_path.is_dir() {
read_dir(&presets_path)?
.filter_map(|entry| {
let path = entry.unwrap().path();
if path.is_dir() {
Some(path.file_name().unwrap().to_str().unwrap().to_owned())
} else {
None
}
})
.collect::<Vec<_>>()
} else {
vec![]
};
if presets_list.is_empty() && std::env::var("OXY_DONT_AUTO_UPDATE").is_err() {
return Err(Error::new(
ErrorKind::NotFound,
"There are no presets installed - consider reinstalling oxygengine-ignite, it might be corrupted",
));
}
if let Some(path) = &project_meta.sccache_bin {
std::env::set_var("RUSTC_WRAPPER", path.to_str().unwrap());
}
if let Some(path) = &project_meta.sccache_dir {
std::env::set_var("SCCACHE_DIR", path.to_str().unwrap());
}
let matches = App::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.subcommand(
App::new("new")
.about("Create new project")
.arg(
Arg::new("id")
.value_name("ID")
.help("Project ID")
.takes_value(true)
.required(true),
)
.arg(
Arg::new("destination")
.short('d')
.long("destination")
.value_name("PATH")
.help("Project destination path")
.takes_value(true)
.required(false),
)
.arg(
Arg::new("preset")
.short('p')
.long("preset")
.help(&*format!("Project preset ({})", presets_list.join(", ")))
.takes_value(false)
.required(false)
.default_value("web-ha-base"),
)
.arg(
Arg::new("dont-build")
.long("dont-build")
.help("Prepare project and exit without building it")
.takes_value(false)
.required(false),
)
)
.subcommand(
App::new("pack")
.about("Pack assets")
.arg(
Arg::new("input")
.short('i')
.long("input")
.value_name("PATH")
.help("Assets root folder")
.takes_value(true)
.required(true),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("PATH")
.help("Asset pack output file")
.takes_value(true)
.required(true),
)
)
.subcommand(
App::new("pipeline")
.about("Execute project pipeline")
.arg(
Arg::new("config")
.short('c')
.long("config")
.value_name("PATH")
.help("Pipeline JSON descriptor file")
.takes_value(true)
.required(false)
.default_value("./pipeline.json")
)
.arg(
Arg::new("template")
.short('t')
.long("template")
.help("Create and save pipeline template")
.takes_value(false)
.required(false),
)
)
.subcommand(
App::new("build")
.about("Build project")
.arg(
Arg::new("profile")
.short('p')
.long("profile")
.value_name("PROFILE")
.help("Project build profile. Possible values: debug, release")
.takes_value(true)
.required(false)
.default_value("debug"),
)
.arg(
Arg::new("crate_dir")
.short('c')
.long("crate_dir")
.value_name("PATH")
.help("Project crate directory")
.takes_value(true)
.required(false),
)
.arg(
Arg::new("out_dir")
.short('o')
.long("out_dir")
.value_name("PATH")
.help("Binaries output directory relative to crate directory")
.takes_value(true)
.required(false),
)
.arg(
Arg::new("extras")
.last(true)
.allow_hyphen_values(true)
.multiple_values(true)
),
)
.subcommand(
App::new("test")
.about("Test project")
.arg(
Arg::new("profile")
.short('p')
.long("profile")
.value_name("PROFILE")
.help("Project build profile. Possible values: debug, release")
.takes_value(true)
.required(false)
.default_value("debug"),
)
.arg(
Arg::new("crate_dir")
.short('c')
.long("crate_dir")
.value_name("PATH")
.help("Project crate directory")
.takes_value(true)
.required(false),
)
.arg(
Arg::new("extras")
.last(true)
.allow_hyphen_values(true)
.multiple_values(true)
),
)
.subcommand(
App::new("serve")
.about("Serve project binary and baked asset files to browsers")
.arg(
Arg::new("port")
.short('p')
.long("port")
.value_name("NUMBER")
.help("HTTP server port")
.takes_value(true)
.required(false)
.default_value("8080")
)
.arg(
Arg::new("binaries")
.short('b')
.long("binaries")
.value_name("PATH")
.help("Path to the binaries folder")
.takes_value(true)
.required(false)
.default_value("./bin/")
)
.arg(
Arg::new("assets")
.short('a')
.long("assets")
.value_name("PATH")
.help("Path to the baked assets folder")
.takes_value(true)
.required(false)
.default_value("./assets-baked/")
)
.arg(
Arg::new("open")
.short('o')
.long("open")
.help("Open URL in the browser")
.required(false)
)
)
.subcommand(
App::new("serve-dir")
.about("Serve directory files to browsers")
.arg(
Arg::new("port")
.short('p')
.long("port")
.value_name("NUMBER")
.help("HTTP server port")
.takes_value(true)
.required(false)
.default_value("8080")
)
.arg(
Arg::new("root")
.short('r')
.long("root")
.value_name("PATH")
.help("Path to the root folder")
.takes_value(true)
.required(false)
.default_value("./")
)
.arg(
Arg::new("open")
.short('o')
.long("open")
.help("Open URL in the browser")
.required(false)
)
)
.subcommand(
App::new("live")
.about("Listen for changes in binary and asset sources and rebuild them when they change")
.arg(
Arg::new("profile")
.short('r')
.long("profile")
.value_name("PROFILE")
.help("Project build profile. Possible values: debug, release")
.takes_value(true)
.required(false)
.default_value("debug"),
)
.arg(
Arg::new("binaries")
.short('b')
.long("binaries")
.value_name("PATH")
.help("Path to the binaries source folders (Rust or JS code)")
.takes_value(true)
.required(false)
.multiple_values(true)
)
.arg(
Arg::new("assets")
.short('a')
.long("assets")
.value_name("PATH")
.help("Path to the assets sources folders")
.takes_value(true)
.required(false)
.multiple_values(true)
)
.arg(
Arg::new("crate_dir")
.short('c')
.long("crate_dir")
.value_name("PATH")
.help("Project crate directory")
.takes_value(true)
.required(false)
.default_value("./")
)
.arg(
Arg::new("pipeline")
.short('p')
.long("pipeline")
.value_name("PATH")
.help("Pipeline JSON descriptor file")
.takes_value(true)
.required(false)
.default_value("./pipeline.json")
)
.arg(
Arg::new("extras")
.last(true)
.allow_hyphen_values(true)
.multiple_values(true)
)
)
.subcommand(
App::new("package")
.about("Make distribution package of the project")
.arg(
Arg::new("debug")
.short('d')
.long("debug")
.help("Package with debug profile")
.takes_value(false)
.required(false)
)
.arg(
Arg::new("crate_dir")
.short('c')
.long("crate_dir")
.value_name("PATH")
.help("Crate directory")
.takes_value(true)
.required(false)
.default_value("./")
)
.arg(
Arg::new("pipeline")
.short('p')
.long("pipeline")
.value_name("PATH")
.help("Assets pipeline config file")
.takes_value(true)
.required(false)
.default_value("./pipeline.json")
)
.arg(
Arg::new("assets")
.short('a')
.long("assets")
.value_name("PATH")
.help("Baked assets directory")
.takes_value(true)
.required(false)
.default_value("./assets-baked/")
)
.arg(
Arg::new("out_dir")
.short('o')
.long("out_dir")
.value_name("PATH")
.help("Output directory relative to crate directory")
.takes_value(true)
.required(false)
.default_value("./dist/")
)
)
.subcommand(
App::new("types")
.about("Performs operation on ignite type definitions")
.subcommand(
App::new("validate")
.about("Validate types")
.arg(
Arg::new("path")
.short('p')
.long("path")
.value_name("PATH")
.help("Path to the crate root directory")
.takes_value(true)
.required(false)
)
.arg(
Arg::new("ignore")
.short('i')
.long("ignore")
.value_name("NAME")
.help("Names of types to ignore in report")
.takes_value(true)
.required(false)
.multiple_values(true)
)
.arg(
Arg::new("ignore-file")
.short('f')
.long("ignore-file")
.value_name("PATH")
.help("Path to list of type names to ignore in report")
.takes_value(true)
.required(false)
)
)
)
.get_matches();
if let Some(matches) = matches.subcommand_matches("new") {
let id = matches.value_of("id").unwrap();
let destination = matches.value_of("destination");
let preset = matches.value_of("preset").unwrap();
let dont_build = matches.is_present("dont-build");
let preset_path = presets_path.join(preset);
if !preset_path.exists() {
return Err(Error::new(
ErrorKind::NotFound,
format!("Preset not found: {} (in path: {:?})", preset, preset_path),
));
}
let mut destination_path = if let Some(destination) = destination {
destination.into()
} else {
current_dir()?
};
destination_path.push(id);
if let Err(err) = create_dir_all(&destination_path) {
if err.kind() != ErrorKind::AlreadyExists {
return Err(Error::new(
ErrorKind::Other,
format!("Could not create directory: {:?}", destination_path),
));
}
}
let preset_manifest_path = presets_path.join(format!("{}.toml", preset));
let preset_manifest = if preset_manifest_path.exists() {
let contents = read_to_string(&preset_manifest_path)?;
if let Ok(mut manifest) = toml::from_str::<PresetManifest>(&contents) {
manifest.notes.dictionary.insert(
"IGNITE_DESTINATION_PATH".to_owned(),
destination_path.to_str().unwrap().to_owned(),
);
manifest.scripts.dictionary = manifest.notes.dictionary.clone();
Some(manifest)
} else {
None
}
} else {
None
};
println!("Make project: {:?}", &destination_path);
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.print_note(ActionType::PreCreate);
}
println!("* Prepare project structure...");
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.execute_script(ActionType::PreCreate, &destination_path)?;
}
copy_dir(&preset_path, &destination_path, id)?;
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.execute_script(ActionType::PostCreate, &destination_path)?;
}
println!("Done!");
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.print_note(ActionType::PostCreate);
}
if !dont_build {
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.print_note(ActionType::PreBuild);
}
println!("* Build rust project...");
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.execute_script(ActionType::PreBuild, &destination_path)?;
}
Command::new("cargo")
.arg("build")
.current_dir(&destination_path)
.output()?;
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.execute_script(ActionType::PostBuild, &destination_path)?;
}
println!("Done!");
if let Some(preset_manifest) = &preset_manifest {
preset_manifest.print_note(ActionType::PostBuild);
}
}
} else if let Some(matches) = matches.subcommand_matches("pack") {
let input = matches.values_of("input").unwrap().collect::<Vec<_>>();
let output = matches.value_of("output").unwrap();
pack_assets_and_write_to_file(&input, output)?;
} else if let Some(matches) = matches.subcommand_matches("pipeline") {
let config = matches.value_of("config").unwrap();
let mut config = Path::new(&config).to_owned();
let template = matches.is_present("template");
if template {
let pipeline = Pipeline {
source: "static".into(),
destination: "static".into(),
commands: vec![
PipelineCommand::Pipeline(Pipeline {
disabled: false,
destination: "assets-generated".into(),
clear_destination: true,
..Default::default()
}),
PipelineCommand::Pipeline(Pipeline {
disabled: false,
source: "assets-source".into(),
destination: "assets-generated".into(),
commands: vec![PipelineCommand::Copy {
disabled: false,
from: vec!["assets.txt".into()],
to: "".into(),
}],
..Default::default()
}),
PipelineCommand::Pack {
disabled: false,
paths: vec!["assets-generated".into()],
output: "assets.pack".into(),
},
],
..Default::default()
};
let contents = match serde_json::to_string_pretty(&pipeline) {
Ok(contents) => contents,
Err(error) => {
return Err(Error::new(
ErrorKind::Other,
format!(
"Could not stringify pipeline JSON config: {:?}. Error: {:?}",
config, error
),
))
}
};
write(config, &contents)?;
} else if config.exists() {
let contents = read_to_string(&config)?;
match serde_json::from_str::<Pipeline>(&contents) {
Ok(pipeline) => {
verify_used_plugins(&pipeline);
if config.is_file() {
config.pop();
}
set_current_dir(config)?;
pipeline.execute()?;
}
Err(error) => println!(
"Could not parse pipeline JSON config: {:?}. Error: {:?}",
config, error
),
}
} else {
println!("Could not find pipeline config file: {:?}", config);
}
} else if let Some(matches) = matches.subcommand_matches("build") {
let profile = match matches.value_of("profile").unwrap() {
"debug" => BuildProfile::Debug,
"release" => BuildProfile::Release,
profile => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Unknown build profile: {}", profile),
))
}
};
let crate_dir = matches.value_of("crate_dir").map(|v| v.to_owned());
let out_dir = matches.value_of("out_dir").map(|v| v.to_owned());
let extras = matches
.values_of("extras")
.map(|iter| iter.map(|arg| arg.to_owned()).collect::<Vec<_>>())
.unwrap_or_default();
build_project(profile, crate_dir, out_dir, extras)?;
} else if let Some(matches) = matches.subcommand_matches("test") {
let profile = match matches.value_of("profile").unwrap() {
"debug" => TestProfile::Debug,
"release" => TestProfile::Release,
profile => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Unknown build profile: {}", profile),
))
}
};
let crate_dir = matches.value_of("crate_dir").map(|v| v.to_owned());
let extras = matches
.values_of("extras")
.map(|iter| iter.map(|arg| arg.to_owned()).collect::<Vec<_>>())
.unwrap_or_default();
test_project(profile, crate_dir, extras)?;
} else if let Some(matches) = matches.subcommand_matches("serve") {
let port = matches
.value_of("port")
.unwrap()
.parse::<u16>()
.expect("Could not parse port number");
let binaries = matches.value_of("binaries").unwrap().to_owned();
let assets = matches.value_of("assets").unwrap().to_owned();
if matches.is_present("open") {
open::that(format!("http://localhost:{}", port))
.expect("Could not open URL in the browser");
}
serve_files(port, binaries, assets).await;
} else if let Some(matches) = matches.subcommand_matches("serve-dir") {
let port = matches
.value_of("port")
.unwrap()
.parse::<u16>()
.expect("Could not parse port number");
let root = matches.value_of("root").unwrap().to_owned();
if matches.is_present("open") {
open::that(format!("http://localhost:{}", port))
.expect("Could not open URL in the browser");
}
serve_dir(port, root).await;
} else if let Some(matches) = matches.subcommand_matches("live") {
let profile = matches.value_of("profile").unwrap().to_owned();
let binaries = matches
.values_of("binaries")
.map(|v| v.collect::<Vec<_>>())
.unwrap_or_else(|| vec!["./src"]);
let assets = matches
.values_of("assets")
.map(|v| v.collect::<Vec<_>>())
.unwrap_or_else(|| vec!["./assets"]);
let crate_dir = matches.value_of("crate_dir").unwrap().to_owned();
let pipeline = matches.value_of("pipeline").unwrap().to_owned();
let extras = matches
.values_of("extras")
.map(|iter| iter.map(|arg| arg.to_owned()).collect::<Vec<_>>());
let mut watcher = Hotwatch::new().expect("Could not start files watcher");
let (build_sender, build_receiver) = channel();
let (pipeline_sender, pipeline_receiver) = channel();
build_sender
.send(())
.expect("Cannot send build run command");
pipeline_sender
.send(())
.expect("Cannot send pipeline run command");
for path in binaries {
let build_sender = build_sender.clone();
watcher
.watch(&path, move |event| match event {
Event::Create(_) | Event::Write(_) | Event::Remove(_) => {
if build_sender.send(()).is_ok() {
println!("* Rebuild project binaries");
}
}
_ => {}
})
.unwrap_or_else(|_| panic!("Could not watch for binaries sources: {:?}", path));
println!("* Watching binaries sources: {:?}", path);
}
for path in assets {
let pipeline_sender = pipeline_sender.clone();
watcher
.watch(&path, move |event| match event {
Event::Create(_) | Event::Write(_) | Event::Remove(_) => {
if pipeline_sender.send(()).is_ok() {
println!("* Rebake project assets");
}
}
_ => {}
})
.unwrap_or_else(|_| panic!("Could not watch for assets sources: {:?}", path));
println!("* Watching assets sources: {:?}", path);
}
let builds = spawn(move || {
let exe = current_exe().expect("Could not get path to the running executable");
while build_receiver.recv().is_ok() {
if let Err(error) = Command::new(&exe)
.arg("build")
.arg("--profile")
.arg(&profile)
.arg("--crate_dir")
.arg(&crate_dir)
.status()
{
println!("Error during build process execution: {:#?}", error);
} else {
println!("* Done building binaries!");
}
}
});
let pipelines = spawn(move || {
let exe = current_exe().expect("Could not get path to the running executable");
while pipeline_receiver.recv().is_ok() {
if let Err(error) = Command::new(&exe)
.arg("pipeline")
.arg("--config")
.arg(&pipeline)
.status()
{
println!("* Error during pipeline process execution: {:#?}", error);
} else {
println!("* Done baking assets!");
}
}
});
let exe = current_exe().expect("Could not get path to the running executable");
if let Some(extras) = extras {
Command::new(exe)
.arg("serve")
.args(extras)
.status()
.expect("Could not run HTTP server");
}
let _ = builds.join();
let _ = pipelines.join();
} else if let Some(matches) = matches.subcommand_matches("package") {
let debug = matches.is_present("debug");
let crate_dir = matches.value_of("crate_dir").unwrap();
let pipeline = matches.value_of("pipeline").unwrap();
let assets = Path::new(matches.value_of("assets").unwrap());
let out_dir = matches.value_of("out_dir").unwrap();
let exe = current_exe().expect("Could not get path to the running executable");
let timer = Instant::now();
let mode = if debug { "debug" } else { "release" };
println!("* Packaging application in {} mode", mode);
Command::new(&exe)
.arg("build")
.arg("--profile")
.arg(mode)
.arg("--crate_dir")
.arg(crate_dir)
.arg("--out_dir")
.arg(out_dir)
.status()
.unwrap_or_else(|_| panic!("Could not run build in {} mode", mode));
let out_dir = Path::new(crate_dir).join(out_dir);
if remove_file(out_dir.join(".gitignore")).is_err() {
println!("Could not remove .gitignore file");
}
if remove_file(out_dir.join("package.json")).is_err() {
println!("Could not remove package.json file");
}
println!("* Executing assets pipeline");
Command::new(exe)
.arg("pipeline")
.arg("--config")
.arg(pipeline)
.status()
.expect("Could not run assets pipeline");
println!("* Copying assets to output directory");
copy_dir(assets, &out_dir, "").expect("Could not copy baked assets to output directory");
println!("* Done in: {:?}", timer.elapsed());
} else if let Some(matches) = matches.subcommand_matches("types") {
if let Some(matches) = matches.subcommand_matches("validate") {
let path = if let Some(path) = matches.value_of("path") {
Path::new(path).to_owned()
} else {
std::env::current_dir().unwrap()
};
let path = path.join("target").join("ignite").join("types");
if !path.is_dir() {
panic!("Ignite types directory does not exists: {:?}", path);
}
let mut ignore = if let Some(ignore) = matches.values_of("ignore") {
ignore.map(|item| item.to_owned()).collect::<Vec<_>>()
} else {
vec![]
};
if let Some(path) = matches.value_of("ignore-file") {
let contents = read_to_string(path).expect("Could not read ignored types file");
for line in contents.lines() {
let line = line.trim();
if !line.is_empty() {
ignore.push(line.to_owned());
}
}
}
let types = path
.read_dir()
.expect("Could not scan ignite types directory")
.filter_map(|entry| {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
let extension = path
.extension()
.unwrap_or_else(|| panic!("Unknown file type: {:?}", path));
let extension = extension.to_str().unwrap_or_else(|| {
panic!("Could not parse file extension: {:?}", path)
});
let definition = {
match extension {
"json" => {
let contents = read_to_string(&path).unwrap_or_else(|_| {
panic!("Could not read ignite type file: {:?}", path)
});
serde_json::from_str::<IgniteTypeDefinition>(&contents)
.expect("Could not parse YAML type definition file")
}
extension => panic!("Unsupported file type: {:?}", extension),
}
};
let key = definition.name();
let value = definition.referenced();
return Some((key, value));
}
}
None
})
.collect::<HashMap<_, _>>();
let referenced = types.values().flatten().cloned().collect::<HashSet<_>>();
let type_names = types.keys().cloned().collect::<HashSet<_>>();
let mut diff = referenced.difference(&type_names).collect::<Vec<_>>();
diff.sort();
println!("* Referenced types without definition:");
for name in diff {
if !ignore.contains(name) {
println!("{}:", name);
for (n, item) in &types {
if item.contains(name) {
println!("- {}", n);
}
}
}
}
}
}
Ok(())
}
async fn serve_files(port: u16, binaries: String, assets: String) {
println!("* Serve project files at: localhost:{}", port);
println!("- binaries:\t{:?}", binaries);
println!("- assets:\t{:?}", assets);
use warp::Filter;
let binaries = warp::fs::dir(binaries);
let assets = warp::fs::dir(assets);
let routes = warp::get().and(binaries.or(assets));
warp::serve(routes).run(([127, 0, 0, 1], port)).await;
}
async fn serve_dir(port: u16, root: String) {
println!("* Serve directory files at: localhost:{}", port);
println!("- root:\t{:?}", root);
use warp::Filter;
let root = warp::fs::dir(root);
let routes = warp::get().and(root);
warp::serve(routes).run(([127, 0, 0, 1], port)).await;
}
fn copy_dir(from: &Path, to: &Path, id: &str) -> Result<()> {
if from.is_dir() {
for entry in read_dir(from)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let dir = to.join(path.file_name().unwrap());
create_dir_all(&dir)?;
copy_dir(&path, &dir, id)?;
} else if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "chrobry" {
if let Ok(contents) = read_to_string(&path) {
let mut vars = HashMap::new();
vars.insert("IGNITE_ID".to_owned(), id.to_owned());
match chrobry_core::generate(&contents, "\n", vars, |_| Ok("".to_owned())) {
Ok(contents) => {
let to = to.join(path.file_stem().unwrap());
write(to, contents)?;
}
Err(error) => println!(
"Could not generate file from Chrobry template: {:?}. Error: {:?}",
path, error
),
}
} else {
println!("Could not open Chrobry template file: {:?}", path);
}
continue;
}
}
let to = to.join(path.file_name().unwrap());
copy(&path, to)?;
}
}
}
Ok(())
}
fn verify_used_plugins(pipeline: &Pipeline) {
for command in &pipeline.commands {
match command {
PipelineCommand::Plugin {
name,
do_not_verify: false,
..
} => {
if which::which(name).is_err() {
let plugin = name.rfind('-').map(|index| &name[0..index]).unwrap_or(name);
let plugin = format!("{}-tools", plugin);
println!(
"Plugin not found: {}. Trying to install it: {}",
name, plugin
);
let _ = Command::new("cargo").arg("install").arg(plugin).status();
}
}
PipelineCommand::Pipeline(pipeline) => verify_used_plugins(pipeline),
_ => {}
}
}
}