diff --git a/python/origen/origen/helpers/regressions/cli/origen.py b/python/origen/origen/helpers/regressions/cli/origen.py index 07de3869..ff812646 100644 --- a/python/origen/origen/helpers/regressions/cli/origen.py +++ b/python/origen/origen/helpers/regressions/cli/origen.py @@ -226,7 +226,10 @@ class Names: pls = _CommonNames.pls_cmd() pl = _CommonNames.pl_cmd() # proj = Cmd(names.proj) - # new = Cmd(names.new) + new = Cmd( + names.new, + help="Create a new origen environment (e.g., app, workspace)", + ) creds = _CommonNames.creds_cmd() i = _CommonNames.interactive_cmd() # fmt = Cmd(names.fmt) @@ -234,9 +237,8 @@ class Names: v = _CommonNames.v_cmd() commands = [ - # proj, new, creds, eval, exec, i, - # pls, pl, aux_cmds, fmt, build - creds, eval, exec, i, + # proj, fmt, build + creds, eval, exec, i, new, pls, pl, aux_cmds, ] cmds = commands diff --git a/rust/origen/Cargo.lock b/rust/origen/Cargo.lock index 764f0407..1ec8d2a3 100644 --- a/rust/origen/Cargo.lock +++ b/rust/origen/Cargo.lock @@ -511,7 +511,6 @@ dependencies = [ "sha2", "tar", "tempfile", - "tera", "time 0.1.44", "toml", "walkdir", @@ -2626,6 +2625,7 @@ dependencies = [ "serde_json", "shellexpand", "tempfile", + "tera", "termcolor", "time 0.1.44", "toml", diff --git a/rust/origen/cli/Cargo.toml b/rust/origen/cli/Cargo.toml index 3e9606b7..e526f8b9 100644 --- a/rust/origen/cli/Cargo.toml +++ b/rust/origen/cli/Cargo.toml @@ -24,7 +24,6 @@ lazy_static = "1.4.0" regex = "1" toml = "0.5" serde = {version = "1.0", features = ["derive"]} -tera = "1" indexmap = {version = "1.3.0", features = ["serde-1"]} flate2 = "1.0" tempfile = "3" diff --git a/rust/origen/cli/src/bin.rs b/rust/origen/cli/src/bin.rs index ae4ff1f5..f3b888c2 100644 --- a/rust/origen/cli/src/bin.rs +++ b/rust/origen/cli/src/bin.rs @@ -228,8 +228,17 @@ fn main() -> Result<()> { let plugins = match Plugins::new(&mut extensions) { Ok(pl) => pl, Err(e) => { - log_error!("Failed to collect plugins. Encountered error: {}", e); - None + if python::is_backend_origen_mod_missing_err(&e) { + // _origen is available but plugins failed to load + log_error!("Failed to collect plugins. Encountered error: {}", e); + None + } else { + // _origen isn't available. This could be an error is retrieving plugins. + // Print a warning instead of error, while logging the error + log_trace!("Failed to collect plugins. Encountered error: {}", e); + log_warning!("Failed to collect plugins: _origen module missing"); + None + } } }; let aux_cmds = AuxCmds::new(&mut extensions)?; @@ -428,6 +437,8 @@ fn main() -> Result<()> { commands::env::add_helps(&mut helps); commands::generate::add_helps(&mut helps); commands::target::add_helps(&mut helps); + } else { + commands::new::add_helps(&mut helps); } if STATUS.is_origen_present { @@ -789,6 +800,8 @@ fn main() -> Result<()> { // .help("Update all CHANGED file references from the last generate run"), // ), // ); + } else { + app = commands::new::add_commands(app, &helps, &extensions)?; } let mut all_cmds_and_aliases = vec![]; @@ -1021,7 +1034,7 @@ fn main() -> Result<()> { match matches.subcommand_name() { Some(commands::app::BASE_CMD) => commands::app::run(matches.subcommand_matches(commands::app::BASE_CMD).unwrap(), &app, &extensions, plugins.as_ref(), &app_cmds.as_ref().unwrap())?, - // Some("new") => commands::new::run(matches.subcommand_matches("new").unwrap()), + Some(commands::new::BASE_CMD) => run_non_ext_cmd_match_case!(new), // Some("proj") => commands::proj::run(matches.subcommand_matches("proj").unwrap()), Some(commands::env::BASE_CMD) => run_non_ext_cmd_match_case!(env), Some(commands::eval::BASE_CMD) => run_cmd_match_case!(eval), diff --git a/rust/origen/cli/src/commands/_prelude/mod.rs b/rust/origen/cli/src/commands/_prelude/mod.rs index 55584813..ee9a17de 100644 --- a/rust/origen/cli/src/commands/_prelude/mod.rs +++ b/rust/origen/cli/src/commands/_prelude/mod.rs @@ -10,6 +10,7 @@ pub use crate::framework::{ }; pub use crate::framework::core_cmds::SubCmd; pub use crate::{output_dir_opt, ref_dir_opt}; +pub use crate::{req_sv_arg, sv_opt}; // TODO clap4.0 remove after update to next clap version pub type RunInput<'a> = &'a clap::ArgMatches; diff --git a/rust/origen/cli/src/commands/mod.rs b/rust/origen/cli/src/commands/mod.rs index 5685a8a4..2f487e46 100644 --- a/rust/origen/cli/src/commands/mod.rs +++ b/rust/origen/cli/src/commands/mod.rs @@ -4,7 +4,7 @@ pub mod exec; // pub mod fmt; pub mod interactive; // pub mod mode; -// pub mod new; +pub mod new; // pub mod proj; // pub mod save_ref; pub mod target; diff --git a/rust/origen/cli/src/commands/new/mod.rs b/rust/origen/cli/src/commands/new/mod.rs index 7fbf2f0e..4a14b3cc 100644 --- a/rust/origen/cli/src/commands/new/mod.rs +++ b/rust/origen/cli/src/commands/new/mod.rs @@ -1,14 +1,16 @@ -// This implements the new application command, for the code generators, e.g. 'origen new dut' etc., -// see new_resource.rs +use crate::commands::_prelude::*; +use origen_metal::tera::{Context, Tera}; +use std::process::exit; +use std::env; +use std::fs::{create_dir, File}; +use std::path::PathBuf; +use crate::_generated::python::PYTHONS; -mod new_resource; +pub const BASE_CMD: &'static str = "new"; +pub const WS_CMD: &'static str = "workspace"; +pub const APP_CMD: &'static str = "application"; -use clap::ArgMatches; -use origen::STATUS; -use phf::map::Map; use phf::phf_map; -use std::path::PathBuf; -use tera::{Context, Tera}; // This includes a map of all template files, it is built by cli/build.rs at compile time. // All files in each sub-directory of commands/new/templates are accessible via a map named after the @@ -19,102 +21,116 @@ use tera::{Context, Tera}; // automatically be picked up and included in the new app. include!(concat!(env!("OUT_DIR"), "/new_app_templates.rs")); -struct App { - name: String, - dir: PathBuf, -} - -pub fn run(matches: &ArgMatches) { - if STATUS.is_app_present { - new_resource::run(matches); - return; - } - let name = matches.get_one::<&str>("name").unwrap(); - if &name.to_lowercase() != name { - display_red!("ERROR: "); - displayln!("The application name must be lowercased"); - std::process::exit(1); - } - let app_dir = std::env::current_dir().unwrap().join(name); - - if app_dir.exists() { - if !app_dir.read_dir().unwrap().next().is_none() { - display_red!("ERROR: "); - displayln!("A directory with that name already exists and is not empty, please delete it or use a new name and try again"); - std::process::exit(1); - } - } else { - std::fs::create_dir(&app_dir) - .expect("Could you create the new application directory, do you have permission?"); - } +gen_core_cmd_funcs__no_exts__no_app_opts!( + BASE_CMD, + "Create a new origen environment (e.g., app, workspace)", + { |cmd: App<'a>| { + cmd.arg_required_else_help(true) + }}, + core_subcmd__no_exts__no_app_opts!(WS_CMD, "Create a new workspace", { |cmd: App| { + cmd.visible_alias("ws") + .arg(req_sv_arg!("name", "NAME", "Workspace name")) + .arg(sv_opt!("desc", "DESC", "Description of the workspace").visible_alias("description")) + .arg(sv_opt!("path", "PATH", "Path to build the new workspace").short('p')) + }}) + // TODO origen new - support new app + // core_subcmd__no_exts__no_app_opts!(APP_CMD, "Create a new application", { |cmd: App| { + // cmd.visible_alias("app") + // }}) +); + +pub fn run(invocation: &clap::ArgMatches) -> origen::Result<()> { + if let Some((n, subcmd)) = invocation.subcommand() { + match n { + WS_CMD => { + let mut tera = match Tera::new("templates/workspace/*.tera") { + Ok(t) => t, + Err(e) => { + println!("Failed to parse workspace templates: {}", e); + exit(1); + } + }; + let mut context = Context::new(); + let name = subcmd.get_one::("name").unwrap(); + + let mut out_dir; + if let Some(p) = subcmd.get_one::("path") { + if p.is_relative() { + out_dir = env::current_dir()?; + out_dir.push(p); + } else { + out_dir = p.to_path_buf(); + } + } else { + out_dir = env::current_dir()?; + out_dir.push(&name); + } - let mut context = Context::new(); - //// Converting this to a vector here as the template was printing out the package list - //// in reverse order when given the index map - //let packages: Vec<&Package> = bom.packages.iter().map(|(_id, pkg)| pkg).collect(); - context.insert("app_name", name); - context.insert("origen_version", &origen::STATUS.origen_version.to_string()); - let mut user_info = "".to_string(); - let users = crate::om::users(); - if let Ok(u) = users.current_user() { - if let Ok(username) = u.username() { - user_info += &username; - match u.get_email() { - Ok(e) => { - if let Some(email) = e { - user_info += &format!(" <{}>", &email); + if out_dir.exists() { + // Check directory is empty + if !out_dir.read_dir()?.next().is_none() { + log_error!("Target directory {} is not empty!", &out_dir.display()); + exit(1); } + } else { + create_dir(&out_dir)?; } - Err(e) => { - display_redln!("{}", e.msg); + println!("Creating new workspace at {}", &out_dir.display()); + + context.insert("name", name); + context.insert("desc", subcmd.get_one::("desc").unwrap_or(&"".to_string())); + context.insert("app_gen", &false); + + let users = origen_metal::users(); + let mut author = "".to_string(); + context.insert("python_version", &format!( + ">={},<={}", + PYTHONS[2].strip_prefix("python").unwrap(), + PYTHONS.last().unwrap().strip_prefix("python").unwrap() + )); + if let Ok(u) = users.current_user() { + match u.username() { + Ok(username) => { + match u.get_email() { + Ok(e) => { + if let Some(email) = e { + author += &format!("{} <{}>", &username, &email); + } + } + Err(e) => { + log_warning!("Cannot retrieve current user's email: {}", e.msg); + } + } + }, + Err(e) => { + log_warning!("Cannot retrieve current user: {}", e.msg); + } + } + } else { + log_warning!("Cannot populate current user"); } - } - } - } - context.insert("user_info", &user_info); + context.insert("author", &author); + context.insert("origen_version", &origen::STATUS.origen_version.to_string()); + // TODO origen new - find a better way than hard-coding pytest version + context.insert("pytest_version", "^7"); - let new_app = App { - name: name.to_string(), - dir: app_dir, - }; - - new_app.apply_template(&PY_APP, &context); - - if !matches.contains_id("no-setup") { - new_app.setup(); - } -} - -impl App { - fn apply_template(&self, template: &Map<&str, &str>, context: &Context) { - let mut tera = Tera::default(); - - for (file, content) in template.entries() { - let contents = tera.render_str(content, &context).unwrap(); - - let file = file.replace("app_namespace_dir", &self.name); - let path = self.dir.join(file.clone()); - - if !path.parent().unwrap().exists() { - std::fs::create_dir_all(&path.parent().unwrap()) - .expect("Couldn't create dir within the new app"); - } - - display_green!(" create "); - displayln!("{}", &file); + for (n, contents) in SHARED.entries() { + tera.add_raw_template(&format!("shared/{}", n), contents)?; + } + for (n, contents) in WORKSPACE.entries() { + tera.add_raw_template(&format!("workspace/{}", n), contents)?; + } - std::fs::write(&path, &contents).expect("Couldn't create a file within the new app"); + for (n, _) in WORKSPACE.entries() { + let f = File::create(out_dir.join(n))?; + tera.render_to(&format!("workspace/{}", n), &context, f)?; + } + Ok(()) + }, + APP_CMD => todo!(), // TODO origen enw - support new app + _ => unreachable_invalid_subc!(n) } + } else { + unreachable!() } - - fn setup(&self) { - std::env::set_current_dir(&self.dir).expect("Couldn't cd to the new app"); - - let _ = std::process::Command::new("origen") - .arg("env") - .arg("setup") - .spawn() - .expect("Couldn't execute origen setup") - .wait(); - } -} +} \ No newline at end of file diff --git a/rust/origen/cli/src/commands/new/templates/shared/.gitignore b/rust/origen/cli/src/commands/new/templates/shared/.gitignore new file mode 100644 index 00000000..e54bd4e1 --- /dev/null +++ b/rust/origen/cli/src/commands/new/templates/shared/.gitignore @@ -0,0 +1,29 @@ +# Workspace directories +/.origen +/tmp +/.ref +/log +/.session + +# Output directories +/output +/web/source/_static/build +/web/source/interbuild +/dist + +{% if app_gen -%} +# Release scribe dry-run output and release notes +config/history.generated.dry_run.toml +release_note.txt +{% endif -%} + +# Cache directories +__pycache__ +.pytest_cache + +# Editor cruft +*.swp +*.swo +*~ +*.pylintrc +.vscode \ No newline at end of file diff --git a/rust/origen/cli/src/commands/new/templates/shared/origen.toml.tera b/rust/origen/cli/src/commands/new/templates/shared/origen.toml.tera new file mode 100644 index 00000000..eee0cdec --- /dev/null +++ b/rust/origen/cli/src/commands/new/templates/shared/origen.toml.tera @@ -0,0 +1,14 @@ +# Use this to define your application-specific Origen configuration +# Do not delete it even if you don't use it since it is also used by the Origen +# command line interface to determine when it is in an Origen application workspace + +# Specify what command should be used to invoke python, if not specified +# Origen will try python, python3, python3.8, etc. until one is found that +# satisfies the minimum Python version requirement +#python_cmd = "mypython" + +# If your company has an internal package server enter it here: +#pkg_server = "https://pkgs.company.net:9292" +# or here, if you need to use different urls for push and pull (write and read): +#pkg_server_push = "https://pkgs.company.net:9292" +#pkg_server_pull = "https://pkgs.company.net:9292" \ No newline at end of file diff --git a/rust/origen/cli/src/commands/new/templates/shared/pyproject.toml.tera b/rust/origen/cli/src/commands/new/templates/shared/pyproject.toml.tera new file mode 100644 index 00000000..3b54596a --- /dev/null +++ b/rust/origen/cli/src/commands/new/templates/shared/pyproject.toml.tera @@ -0,0 +1,16 @@ +[tool.poetry] +name = "{{name}}" +version = "0.1.0" +description = "{{desc}}" +authors = ["{{author}}"] + +[tool.poetry.dependencies] +python = "{{python_version}}" +origen = "{{origen_version}}" + +[tool.poetry.dev-dependencies] +pytest = "{{pytest_version}}" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/rust/origen/cli/src/commands/new/templates/workspace/.gitignore.tera b/rust/origen/cli/src/commands/new/templates/workspace/.gitignore.tera new file mode 100644 index 00000000..4f7b7da8 --- /dev/null +++ b/rust/origen/cli/src/commands/new/templates/workspace/.gitignore.tera @@ -0,0 +1 @@ +{% extends "shared/.gitignore" %} \ No newline at end of file diff --git a/rust/origen/cli/src/commands/new/templates/workspace/origen.toml.tera b/rust/origen/cli/src/commands/new/templates/workspace/origen.toml.tera new file mode 100644 index 00000000..290d0f81 --- /dev/null +++ b/rust/origen/cli/src/commands/new/templates/workspace/origen.toml.tera @@ -0,0 +1 @@ +{% extends "shared/origen.toml" %} \ No newline at end of file diff --git a/rust/origen/cli/src/commands/new/templates/workspace/pyproject.toml.tera b/rust/origen/cli/src/commands/new/templates/workspace/pyproject.toml.tera new file mode 100644 index 00000000..c8818d8b --- /dev/null +++ b/rust/origen/cli/src/commands/new/templates/workspace/pyproject.toml.tera @@ -0,0 +1 @@ +{% extends "shared/pyproject.toml" %} \ No newline at end of file diff --git a/rust/origen/cli/src/framework/cmd_gen_helpers.rs b/rust/origen/cli/src/framework/cmd_gen_helpers.rs new file mode 100644 index 00000000..ebbf6a50 --- /dev/null +++ b/rust/origen/cli/src/framework/cmd_gen_helpers.rs @@ -0,0 +1,26 @@ +/// Generate a required, single-value arg +#[macro_export] +macro_rules! req_sv_arg { + ($name: expr, $value_name: expr, $help: expr) => {{ + clap::Arg::new($name) + .help($help) + .action(crate::commands::_prelude::clap_arg_actions::SetArg) + .value_name($value_name) + .multiple(false) + .required(true) + }} +} + +/// Generate an optional, single-value option +#[macro_export] +macro_rules! sv_opt { + ($name: expr, $value_name: expr, $help: expr) => {{ + clap::Arg::new($name) + .long($name) + .help($help) + .action(crate::commands::_prelude::clap_arg_actions::SetArg) + .value_name($value_name) + .multiple(false) + .required(false) + }} +} \ No newline at end of file diff --git a/rust/origen/cli/src/framework/mod.rs b/rust/origen/cli/src/framework/mod.rs index 51df3d90..be694f1d 100644 --- a/rust/origen/cli/src/framework/mod.rs +++ b/rust/origen/cli/src/framework/mod.rs @@ -4,6 +4,7 @@ pub mod plugins; pub mod aux_cmds; pub mod app_cmds; pub mod core_cmds; +pub mod cmd_gen_helpers; use std::collections::HashMap; use origen_metal::indexmap::IndexMap; diff --git a/rust/origen/cli/src/framework/plugins.rs b/rust/origen/cli/src/framework/plugins.rs index d605cf9c..f6278130 100644 --- a/rust/origen/cli/src/framework/plugins.rs +++ b/rust/origen/cli/src/framework/plugins.rs @@ -64,21 +64,22 @@ impl Plugins { slf.plugins.insert(name.to_string(), pl); }, Err(e) => { - log_error!("{}", e); - log_error!("Unable to collect plugin {}", path); + log_trace!("Error collecting plugins: Unable to collect plugin {}: {}", path, e); } } } else { - log_error!("Malformed output when collecting plugin roots (post status): {}", result) + log_trace!("Error collecting plugins: Malformed output when collecting plugin roots (post status): {}", result) } }, - _ => log_error!("Unknown status when collecting plugin roots: {}", status) + _ => log_trace!("Error collecting plugins: Unknown status when collecting plugin roots: {}", status) } } else { - log_error!("Malformed output encountered when collecting plugin roots: {}", line); + log_trace!("Error collecting plugins: Malformed output encountered when collecting plugin roots: {}", line); } }), - None, + Some(&mut |line| { + log_trace!("Error when collecting plugins: {}", line); + }), )?; Ok(Some(slf)) } else { diff --git a/rust/origen/cli/src/python.rs b/rust/origen/cli/src/python.rs index bf2d27e8..cb26d9d9 100644 --- a/rust/origen/cli/src/python.rs +++ b/rust/origen/cli/src/python.rs @@ -3,6 +3,7 @@ use crate::built_info; use origen::{Result, STATUS}; use origen::core::status::DependencySrc; use origen_metal::utils::file::search_backwards_for_first; +use origen_metal::new_cmd; use semver::Version; use std::env; use std::path::PathBuf; @@ -103,7 +104,7 @@ pub fn resolve_pyproject() -> Result { impl Config { pub fn base_cmd(&self) -> Command { let dep_src = STATUS.dependency_src(); - let mut c = Command::new(&self.command); + let mut c = new_cmd!(&self.command); if let Some(dep_src) = dep_src.as_ref() { match dep_src { @@ -207,10 +208,12 @@ impl Default for Config { Err(e) => log_error!("Errors encountered resolving pyproject: {}", e) } for cmd in PYTHONS.iter() { + log_trace!("Searching for installed python at '{}'", cmd); match get_version(cmd) { Some(version) => { available = true; if version >= Version::parse(MIN_PYTHON_VERSION).unwrap() { + log_trace!("Found python version '{}'", cmd); return Config { available: true, command: cmd.to_string(), @@ -228,7 +231,7 @@ impl Default for Config { } Config { available: false, - command: String::new(), + command: String::from("python"), version: Version::parse("0.0.0").unwrap(), error: msg, } @@ -258,7 +261,7 @@ pub fn virtual_env() -> Result { /// Get the Python version from the given command fn get_version(command: &str) -> Option { - match Command::new(command).arg("--version").output() { + match new_cmd!(command).arg("--version").output() { Ok(output) => return extract_version(std::str::from_utf8(&output.stdout).unwrap()), Err(_e) => return None, } @@ -266,8 +269,6 @@ fn get_version(command: &str) -> Option { /// Returns the version of poetry (obtained from running "poetry --version") pub fn poetry_version() -> Option { - //log_trace!("Executing command: {} --version", &PYTHON_CONFIG.poetry_command); - //match Command::new(&PYTHON_CONFIG.poetry_command) match &PYTHON_CONFIG.poetry_command().arg("--version").output() { Ok(output) => { let text = std::str::from_utf8(&output.stdout).unwrap(); @@ -388,6 +389,10 @@ pub fn add_origen_env(cmd: &mut Command) { } } +pub fn is_backend_origen_mod_missing_err(err: &origen::Error) -> bool { + err.to_string().contains("ModuleNotFoundError: No module named '_origen'") +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/origen_metal/Cargo.toml b/rust/origen_metal/Cargo.toml index 942e5ae6..e0c83596 100644 --- a/rust/origen_metal/Cargo.toml +++ b/rust/origen_metal/Cargo.toml @@ -50,6 +50,7 @@ aes-gcm = "0.8" semver = "1.0.1" glob = "0.3.0" email_address = "0.2.4" +tera = "1" [build-dependencies] built = "0.5.2" diff --git a/rust/origen_metal/src/error.rs b/rust/origen_metal/src/error.rs index 5d914320..b68cf68d 100644 --- a/rust/origen_metal/src/error.rs +++ b/rust/origen_metal/src/error.rs @@ -233,3 +233,9 @@ impl std::convert::From for Error { Error::new(&err.to_string()) } } + +impl std::convert::From for Error { + fn from(err: tera::Error) -> Self { + Error::new(&err.to_string()) + } +} diff --git a/rust/origen_metal/src/lib.rs b/rust/origen_metal/src/lib.rs index 3e186155..c6b7fae7 100644 --- a/rust/origen_metal/src/lib.rs +++ b/rust/origen_metal/src/lib.rs @@ -4,6 +4,7 @@ pub extern crate config; pub extern crate indexmap; pub extern crate glob; pub extern crate octocrab; +pub extern crate tera; #[macro_use] extern crate serde; #[macro_use] diff --git a/rust/origen_metal/src/utils/command.rs b/rust/origen_metal/src/utils/command.rs index a2b70af3..45e2037e 100644 --- a/rust/origen_metal/src/utils/command.rs +++ b/rust/origen_metal/src/utils/command.rs @@ -94,6 +94,19 @@ pub fn log_stderr(process: &mut std::process::Child, mut callback: Option<&mut d }); } +#[macro_export] +macro_rules! new_cmd { + ($base_cmd:expr) => {{ + if cfg!(windows) { + let mut c = Command::new("cmd"); + c.arg(r"/c").arg($base_cmd); + c + } else { + Command::new($base_cmd) + } + }}; +} + pub fn exec + Clone>( cmd: Vec, capture: bool,