diff --git a/Cargo.lock b/Cargo.lock index 79563a1ba..1fd61fa40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1273,12 +1273,14 @@ name = "fud" version = "0.0.2" dependencies = [ "anyhow", + "argh", "figment", "fud-core", "include_dir", "insta", "itertools 0.11.0", "manifest-dir-macros", + "toml_edit", ] [[package]] diff --git a/docs/running-calyx/fud2/index.md b/docs/running-calyx/fud2/index.md index 5d7771d72..ec32ad04d 100644 --- a/docs/running-calyx/fud2/index.md +++ b/docs/running-calyx/fud2/index.md @@ -30,6 +30,18 @@ Add these lines: base = "" ``` +### Environment Setup + +Some parts of Calyx and `fud2` require setting up and installing various python packages. With Python removing support for installing packages system wide, it's recommended to install relevant packages into a python virtual environment. `fud2` can set up this environment for you and instruct `fud2` to automatically run relevant tools in the correct virtual environment. + +To do this, simply run: + + $ fud2 env init + +There may be some cases where you want to manually interact with the python virtual environment. The virtual environment is installed to `$XDG_DATA_HOME/fud2/venv` (usually `~/.local/share/fud2/venv`). You can activate the virtual environment in your current shell with: + + $ fud2 env activate + Now you're ready to use fud2. [ninja]: https://ninja-build.org diff --git a/fud2/Cargo.toml b/fud2/Cargo.toml index a5dbff0df..4ca0d8b05 100644 --- a/fud2/Cargo.toml +++ b/fud2/Cargo.toml @@ -22,6 +22,8 @@ fud-core = { path = "fud-core", version = "0.0.2", features = ["egg_planner"] } anyhow.workspace = true manifest-dir-macros = "0.1" include_dir = "0.7" +argh.workspace = true +toml_edit = "0.22.20" [lib] name = "fud2" diff --git a/fud2/fud-core/Cargo.toml b/fud2/fud-core/Cargo.toml index bda0d00c9..22ab4148a 100644 --- a/fud2/fud-core/Cargo.toml +++ b/fud2/fud-core/Cargo.toml @@ -32,4 +32,4 @@ toml_edit = { version = "0.22.20", features = ["serde"] } rand_chacha = "0.3.1" [features] -egg_planner = ["dep:egg"] +egg_planner = ["dep:egg"] \ No newline at end of file diff --git a/fud2/fud-core/src/cli.rs b/fud2/fud-core/src/cli.rs index 0eb13bb39..cc9645808 100644 --- a/fud2/fud-core/src/cli.rs +++ b/fud2/fud-core/src/cli.rs @@ -1,3 +1,4 @@ +pub use crate::cli_ext::{CliExt, FakeCli, FromArgFn, RedactArgFn}; use crate::config; use crate::exec::{plan, Driver, Request, StateRef}; use crate::run::Run; @@ -6,7 +7,7 @@ use argh::FromArgs; use camino::Utf8PathBuf; use figment::providers::Serialized; use itertools::Itertools; -use std::fmt::Display; +use std::fmt::{Debug, Display}; use std::fs; use std::str::FromStr; @@ -120,9 +121,9 @@ pub struct RegisterCommand { } /// supported subcommands -#[derive(FromArgs, PartialEq, Debug)] +#[derive(FromArgs)] #[argh(subcommand)] -pub enum Subcommand { +pub enum Subcommand { /// edit the configuration file EditConfig(EditConfig), @@ -134,13 +135,16 @@ pub enum Subcommand { /// register a plugin Register(RegisterCommand), + + #[argh(dynamic)] + Extended(FakeCli), } #[derive(FromArgs)] /// A generic compiler driver. -struct FakeArgs { +pub struct FakeArgs { #[argh(subcommand)] - pub sub: Option, + pub sub: Option>, /// the input file #[argh(positional)] @@ -223,9 +227,9 @@ fn get_states_with_errors( Ok(states) } -fn from_states( +fn from_states( driver: &Driver, - args: &FakeArgs, + args: &FakeArgs, ) -> anyhow::Result> { get_states_with_errors( driver, @@ -237,7 +241,10 @@ fn from_states( ) } -fn to_state(driver: &Driver, args: &FakeArgs) -> anyhow::Result> { +fn to_state( + driver: &Driver, + args: &FakeArgs, +) -> anyhow::Result> { get_states_with_errors( driver, &args.to, @@ -248,7 +255,10 @@ fn to_state(driver: &Driver, args: &FakeArgs) -> anyhow::Result> { ) } -fn get_request(driver: &Driver, args: &FakeArgs) -> anyhow::Result { +fn get_request( + driver: &Driver, + args: &FakeArgs, +) -> anyhow::Result { // The default working directory (if not specified) depends on the mode. let workdir = args.dir.clone().unwrap_or_else(|| match args.mode { Mode::Generate | Mode::Run => { @@ -363,9 +373,41 @@ fn register_plugin( Ok(()) } -/// Given the name of a Driver, returns a config based on that name and CLI arguments. -pub fn config_from_cli(name: &str) -> anyhow::Result { - let args: FakeArgs = argh::from_env(); +pub trait CliStart { + /// Given the name of a Driver, returns a config based on that name and CLI arguments. + fn config_from_cli(name: &str) -> anyhow::Result; + + /// Given a driver and config, start the CLI. + fn cli(driver: &Driver, config: &figment::Figment) -> anyhow::Result<()>; +} + +/// Default CLI that provides an interface to core actions. +pub struct DefaultCli; + +impl CliStart<()> for DefaultCli { + fn config_from_cli(name: &str) -> anyhow::Result { + config_from_cli_ext::<()>(name) + } + + fn cli(driver: &Driver, config: &figment::Figment) -> anyhow::Result<()> { + cli_ext::<()>(driver, config) + } +} + +impl CliStart for T { + fn config_from_cli(name: &str) -> anyhow::Result { + config_from_cli_ext::(name) + } + + fn cli(driver: &Driver, config: &figment::Figment) -> anyhow::Result<()> { + cli_ext::(driver, config) + } +} + +fn config_from_cli_ext( + name: &str, +) -> anyhow::Result { + let args: FakeArgs = argh::from_env(); let mut config = config::load_config(name); // Use `--set` arguments to override configuration values. @@ -382,9 +424,11 @@ pub fn config_from_cli(name: &str) -> anyhow::Result { Ok(config) } -pub fn cli(driver: &Driver, config: &figment::Figment) -> anyhow::Result<()> { - let args: FakeArgs = argh::from_env(); - +fn cli_ext( + driver: &Driver, + config: &figment::Figment, +) -> anyhow::Result<()> { + let args: FakeArgs = argh::from_env(); // Configure logging. env_logger::Builder::new() .format_timestamp(None) @@ -407,6 +451,9 @@ pub fn cli(driver: &Driver, config: &figment::Figment) -> anyhow::Result<()> { Some(Subcommand::Register(cmd)) => { return register_plugin(driver, cmd); } + Some(Subcommand::Extended(cmd)) => { + return cmd.0.run(driver); + } None => {} } diff --git a/fud2/fud-core/src/cli_ext.rs b/fud2/fud-core/src/cli_ext.rs new file mode 100644 index 000000000..e6bdd3bf3 --- /dev/null +++ b/fud2/fud-core/src/cli_ext.rs @@ -0,0 +1,137 @@ +use std::sync::OnceLock; + +use crate::Driver; + +/// Fn type representing the redact_arg function required for implementing `argh::FromArgs` +pub type RedactArgFn = + fn(&[&str], &[&str]) -> Result, argh::EarlyExit>; + +/// Fn type representing the from_arg function required for implementing `argh::FromArgs` +pub type FromArgFn = fn(&[&str], &[&str]) -> Result; + +/// Trait for extending the cli provided by `fud_core::cli`. +/// +/// Below is an example of how to use this trait to add a subcommand named `test` to +/// the `fud_core` cli. +/// +/// ```rust +/// /// some test command +/// #[derive(FromArgs)] +/// #[argh(subcommand, name = "test")] +/// pub struct TestCommand { +/// /// some arg +/// #[argh(positional)] +/// arg: String, +/// } +/// +/// pub enum TestExt { +/// Test(TestCommand) +/// } +/// +/// impl CliExt for Fud2CliExt { +/// fn run(&self, driver: &fud_core::Driver) -> anyhow::Result<()> { +/// match &self { +/// Fud2CliExt::Test(cmd) => { +/// println!("hi there: {}", cmd.arg); +/// Ok(()) +/// } +/// } +/// } +/// +/// fn inner_command_info() -> Vec { +/// vec![CommandInfo { +/// name: "test", +/// description: "test command", +/// }] +/// } +/// +/// fn inner_redact_arg_values() -> Vec<(&'static str, RedactArgFn)> { +/// vec![("test", TestCommand::redact_arg_values)] +/// } +/// +/// fn inner_from_args() -> Vec<(&'static str, FromArgFn)> { +/// vec![("test", |cmd_name, args| { +/// TestCommand::from_args(cmd_name, args).map(Self::Test) +/// })] +/// } +/// } +/// ``` +pub trait CliExt: Sized { + /// Action to execute when this subcommand is provided to the cli + fn run(&self, driver: &Driver) -> anyhow::Result<()>; + + /// Provides the command names and descriptions for all subcommands in the cli + /// extension. + fn inner_command_info() -> Vec; + + /// Forward `redact_arg_values` parsing of subcommands to `fud_core::cli` parsing. + fn inner_redact_arg_values() -> Vec<(&'static str, RedactArgFn)>; + + /// Forward `from_args` parsing of subcommands to `fud_core::cli` parsing. + fn inner_from_args() -> Vec<(&'static str, FromArgFn)>; +} + +/// Wrapper type over types that implement `CliExt`. This is needed so that we can +/// implement the foreign trait `argh::DynamicSubCommand` on a user provided `CliExt`. +pub struct FakeCli(pub T); + +impl argh::DynamicSubCommand for FakeCli { + fn commands() -> &'static [&'static argh::CommandInfo] { + static RET: OnceLock> = OnceLock::new(); + RET.get_or_init(|| { + T::inner_command_info() + .into_iter() + .map(|cmd_info| &*Box::leak(Box::new(cmd_info))) + .collect() + }) + } + + fn try_redact_arg_values( + command_name: &[&str], + args: &[&str], + ) -> Option, argh::EarlyExit>> { + for (reg_name, f) in T::inner_redact_arg_values() { + if let Some(&name) = command_name.last() { + if name == reg_name { + return Some(f(command_name, args)); + } + } + } + None + } + + fn try_from_args( + command_name: &[&str], + args: &[&str], + ) -> Option> { + for (reg_name, f) in T::inner_from_args() { + if let Some(&name) = command_name.last() { + if name == reg_name { + return Some(f(command_name, args).map(FakeCli)); + } + } + } + None + } +} + +/// The default CliExt used if none is provided. This doesn't define any new commands. +impl CliExt for () { + fn inner_command_info() -> Vec { + vec![] + } + + fn inner_redact_arg_values( + ) -> Vec<(&'static str, crate::cli_ext::RedactArgFn)> { + vec![] + } + + fn inner_from_args() -> Vec<(&'static str, crate::cli_ext::FromArgFn)> + { + vec![] + } + + fn run(&self, _driver: &Driver) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/fud2/fud-core/src/config.rs b/fud2/fud-core/src/config.rs index ef3b45692..bf6dfcc7a 100644 --- a/fud2/fud-core/src/config.rs +++ b/fud2/fud-core/src/config.rs @@ -47,6 +47,16 @@ pub fn config_path(name: &str) -> std::path::PathBuf { config_path } +/// Location of the data directory +pub fn data_dir(name: &str) -> std::path::PathBuf { + // The configuration is usually at `~/.config/driver_name.toml`. + let config_base = env::var("XDG_DATA_HOME").unwrap_or_else(|_| { + let home = env::var("HOME").expect("$HOME not set"); + home + "/.local/share" + }); + Path::new(&config_base).join(name) +} + /// Get raw configuration data with some default options. pub fn default_config() -> Figment { Figment::from(Serialized::defaults(GlobalConfig::default())) diff --git a/fud2/fud-core/src/lib.rs b/fud2/fud-core/src/lib.rs index 161b8463f..985aa6d82 100644 --- a/fud2/fud-core/src/lib.rs +++ b/fud2/fud-core/src/lib.rs @@ -1,4 +1,5 @@ pub mod cli; +mod cli_ext; pub mod config; pub mod exec; pub mod run; diff --git a/fud2/src/cli_pyenv.rs b/fud2/src/cli_pyenv.rs new file mode 100644 index 000000000..f5d01e982 --- /dev/null +++ b/fud2/src/cli_pyenv.rs @@ -0,0 +1,140 @@ +use std::{fs, path::Path, process::Command}; + +use argh::{CommandInfo, FromArgs}; +use fud_core::{ + cli::{CliExt, FromArgFn, RedactArgFn}, + config, +}; + +/// manage a fud2 python environment +#[derive(FromArgs)] +#[argh(subcommand, name = "env")] +pub struct PyenvCommand { + #[argh(subcommand)] + sub: PyenvAction, +} + +#[derive(FromArgs)] +#[argh(subcommand)] +pub enum PyenvAction { + Init(InitCommand), + Activate(ActivateCommand), +} + +/// initialize python venv and install necessary packages +#[derive(FromArgs)] +#[argh(subcommand, name = "init")] +pub struct InitCommand {} + +/// activate the fud2 python venv for manual management +#[derive(FromArgs)] +#[argh(subcommand, name = "activate")] +pub struct ActivateCommand {} + +impl PyenvCommand { + fn init(&self, driver: &fud_core::Driver) -> anyhow::Result<()> { + let data_dir = config::data_dir(&driver.name); + + fs::create_dir_all(&data_dir)?; + + let pyenv = data_dir.join("venv"); + + // create new venv + Command::new("python3") + .args(["-m", "venv"]) + .arg(&pyenv) + .stdout(std::io::stdout()) + .output()?; + + // install flit + Command::new(pyenv.join("bin").join("pip")) + .arg("install") + .arg("flit") + .stdout(std::io::stdout()) + .output()?; + + // grab the location of the calyx base install + let config = config::load_config(&driver.name); + let calyx_base: String = config.extract_inner("calyx.base")?; + + // install fud python library + Command::new(pyenv.join("bin").join("python")) + .args(["-m", "flit", "install"]) + .current_dir(Path::new(&calyx_base).join("fud")) + .stdout(std::io::stdout()) + .output()?; + + // install calyx-py library + Command::new(pyenv.join("bin").join("python")) + .args(["-m", "flit", "install"]) + .current_dir(Path::new(&calyx_base).join("calyx-py")) + .stdout(std::io::stdout()) + .output()?; + + // add python location to fud2.toml + let config_path = config::config_path(&driver.name); + let contents = fs::read_to_string(&config_path)?; + let mut toml_doc: toml_edit::DocumentMut = contents.parse()?; + + toml_doc["python"] = toml_edit::value( + pyenv + .join("bin") + .join("python") + .to_string_lossy() + .to_string(), + ); + + fs::write(&config_path, toml_doc.to_string())?; + + Ok(()) + } + + fn activate(&self, driver: &fud_core::Driver) -> anyhow::Result<()> { + let data_dir = config::data_dir(&driver.name); + let pyenv = data_dir.join("venv"); + + if !pyenv.exists() { + anyhow::bail!("You need to run `fud2 env init` before you can activate the venv") + } + + println!("{}", pyenv.join("bin").join("activate").to_str().unwrap()); + + Ok(()) + } + + fn run(&self, driver: &fud_core::Driver) -> anyhow::Result<()> { + match self.sub { + PyenvAction::Init(_) => self.init(driver), + PyenvAction::Activate(_) => self.activate(driver), + } + } +} + +pub enum Fud2CliExt { + Pyenv(PyenvCommand), +} + +impl CliExt for Fud2CliExt { + fn inner_command_info() -> Vec { + vec![CommandInfo { + name: "env", + description: "manage the fud2 python environment", + }] + } + + fn inner_redact_arg_values() -> Vec<(&'static str, RedactArgFn)> { + vec![("env", PyenvCommand::redact_arg_values)] + } + + fn inner_from_args() -> Vec<(&'static str, FromArgFn)> { + vec![("env", |cmd_name, args| { + PyenvCommand::from_args(cmd_name, args).map(Self::Pyenv) + })] + } + + fn run(&self, driver: &fud_core::Driver) -> anyhow::Result<()> { + match &self { + Fud2CliExt::Pyenv(cmd) => cmd.run(driver), + } + } +} diff --git a/fud2/src/lib.rs b/fud2/src/lib.rs index ca7065a46..a714236bf 100644 --- a/fud2/src/lib.rs +++ b/fud2/src/lib.rs @@ -1,3 +1,6 @@ +mod cli_pyenv; +pub use cli_pyenv::Fud2CliExt; + use std::str::FromStr; use fud_core::{ diff --git a/fud2/src/main.rs b/fud2/src/main.rs index 16152eb1b..76c577287 100644 --- a/fud2/src/main.rs +++ b/fud2/src/main.rs @@ -1,4 +1,5 @@ -use fud_core::{cli, DriverBuilder}; +use fud2::Fud2CliExt; +use fud_core::{cli::CliStart, DriverBuilder}; fn main() -> anyhow::Result<()> { let mut bld = DriverBuilder::new("fud2"); @@ -37,7 +38,7 @@ fn main() -> anyhow::Result<()> { } // Get config values from cli. - let config = cli::config_from_cli(&bld.name)?; + let config = Fud2CliExt::config_from_cli(&bld.name)?; #[cfg(feature = "migrate_to_scripts")] { @@ -45,5 +46,5 @@ fn main() -> anyhow::Result<()> { } let driver = bld.build(); - cli::cli(&driver, &config) + Fud2CliExt::cli(&driver, &config) }