diff --git a/Cargo.lock b/Cargo.lock index 597d0fe..513bb42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,7 @@ dependencies = [ "once_cell", "parking_lot", "petgraph", + "ptree", "rand", "rayon", "regex", diff --git a/crates/assemble-core/Cargo.toml b/crates/assemble-core/Cargo.toml index b00f0ea..e45c719 100644 --- a/crates/assemble-core/Cargo.toml +++ b/crates/assemble-core/Cargo.toml @@ -72,6 +72,7 @@ merge = { version = "0.1.0", features = ["derive"] } ron-serde = { package = "ron", version = "0.8.0", optional = true } rmp-serde = { version = "1.1.1", optional = true } parking_lot = { version = "0.12.1", features = ["deadlock_detection"] } +ptree = { version = "0.4.0", features = ["petgraph"] } [dev-dependencies] assemble-macros = { path = "../assemble-macros" } diff --git a/crates/assemble-core/src/lib.rs b/crates/assemble-core/src/lib.rs index e03413b..74c0c10 100644 --- a/crates/assemble-core/src/lib.rs +++ b/crates/assemble-core/src/lib.rs @@ -67,6 +67,8 @@ pub mod prelude { #[cfg(feature = "unstable")] pub use unstable::enabled::prelude::*; + pub use startup_api::{invocation::*, initialization::*, listeners}; + pub use crate::project::error::ProjectError; pub use crate::project::error::ProjectResult; pub use identifier::{ProjectId, TaskId}; diff --git a/crates/assemble-core/src/plugins.rs b/crates/assemble-core/src/plugins.rs index 25f8c6a..926df7d 100644 --- a/crates/assemble-core/src/plugins.rs +++ b/crates/assemble-core/src/plugins.rs @@ -25,6 +25,7 @@ pub trait Plugin: Default { } } +/// Some value that can have plugins applied to it. pub trait PluginAware: Sized { /// Apply a plugin to this. fn apply_plugin>(&mut self) -> ProjectResult { @@ -32,7 +33,9 @@ pub trait PluginAware: Sized { manager.apply::

(self) } + /// Gets a reference to the plugin manager for this value. fn plugin_manager(&self) -> &PluginManager; + /// Gets a mutable reference to the plugin manager for this value. fn plugin_manager_mut(&mut self) -> &mut PluginManager; } diff --git a/crates/assemble-core/src/startup_api/initialization/descriptor.rs b/crates/assemble-core/src/startup_api/initialization/descriptor.rs index ef397b6..839a56d 100644 --- a/crates/assemble-core/src/startup_api/initialization/descriptor.rs +++ b/crates/assemble-core/src/startup_api/initialization/descriptor.rs @@ -1,4 +1,317 @@ +use crate::Project; +use petgraph::graph::DefaultIx; +use petgraph::prelude::*; +use petgraph::visit::IntoNodeReferences; +use ptree::PrintConfig; +use std::fmt; +use std::fmt::Write as _; +use std::fmt::{Debug, Display, Formatter}; +use std::io::Write as _; +use std::path::{Path, PathBuf}; + /// A project descriptor is used to define projects. +#[derive(Debug, Clone, Eq, PartialEq)] pub struct ProjectDescriptor { + build_file: ProjectDescriptorLocation, + name: String, +} + +impl Display for ProjectDescriptor { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{} ({:?})", self.name, self.build_file) + } +} + +impl ProjectDescriptor { + /// creates a new project descriptor + fn new>(name: S, build_file: ProjectDescriptorLocation) -> Self { + let name = name.as_ref().to_string(); + Self { build_file, name } + } + + /// if this only has the directory known, this sets the file name + fn set_file_name(&mut self, file_name: &str) { + let file_path = if let ProjectDescriptorLocation::KnownDirectory(dir) = &self.build_file { + dir.join(file_name) + } else { + return; + }; + + self.build_file = ProjectDescriptorLocation::KnownFile(file_path); + } + + /// Gets the name of the project + pub fn name(&self) -> &str { + &self.name + } + + /// Sets the name of the project + pub fn set_name(&mut self, name: impl AsRef) { + self.name = name.as_ref().to_string(); + } + + /// Gets the build file associated with this project, if known + pub fn build_file(&self) -> Option<&Path> { + match &self.build_file { + ProjectDescriptorLocation::KnownFile(f) => Some(&*f), + ProjectDescriptorLocation::KnownDirectory(_) => None, + } + } + + /// Checks if this project descriptor is contained in this directory + pub fn matches_dir(&self, path: impl AsRef) -> bool { + let path = path.as_ref(); + match &self.build_file { + ProjectDescriptorLocation::KnownFile(f) => match f.parent() { + Some(parent) => parent.ends_with(path), + None => path == Path::new(""), + }, + ProjectDescriptorLocation::KnownDirectory(d) => d.ends_with(path), + } + } +} + +#[derive(Clone, Eq, PartialEq)] +enum ProjectDescriptorLocation { + KnownFile(PathBuf), + KnownDirectory(PathBuf), +} + +impl Debug for ProjectDescriptorLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ProjectDescriptorLocation::KnownFile(file) => { + write!(f, "{:?}", file) + } + ProjectDescriptorLocation::KnownDirectory(d) => { + write!(f, "{:?}", d.join("?")) + } + } + } +} + +/// Describes the project graph +#[derive(Debug)] +pub struct ProjectGraph { + graph: DiGraph, + project_dir: PathBuf, + root_project: NodeIndex, + default_build_script_file: Option, +} + +impl ProjectGraph { + /// Creates a new project graph with a pre-initialized root project + pub(crate) fn new>(project_dir: P) -> Self { + let project_name = project_dir + .as_ref() + .file_name() + .unwrap_or_else(|| panic!("{:?} has no file name", project_dir.as_ref())) + .to_str() + .unwrap_or_else(|| { + panic!( + "{:?} file name can not be represented as a utf-8 string", + project_dir.as_ref() + ) + }); + + let root_project = ProjectDescriptor::new( + project_name, + ProjectDescriptorLocation::KnownDirectory(project_dir.as_ref().to_path_buf()), + ); + let mut graph = DiGraph::new(); + let idx = graph.add_node(root_project); + Self { + graph, + project_dir: project_dir.as_ref().to_path_buf(), + root_project: idx, + default_build_script_file: None, + } + } + + /// Gets the root project descriptor + pub fn root_project(&self) -> &ProjectDescriptor { + &self.graph[self.root_project] + } + + /// Gets a mutable reference to the root project descriptor + pub fn root_project_mut(&mut self) -> &mut ProjectDescriptor { + &mut self.graph[self.root_project] + } + + pub fn set_default_build_file_name(&mut self, name: &str) { + if self.default_build_script_file.is_some() { + panic!( + "default build script file name already set to {:?}", + self.default_build_script_file.as_ref().unwrap() + ); + } + + self.default_build_script_file = Some(name.to_string()); + for node in self.graph.node_indices() { + self.graph[node].set_file_name(name); + } + } + + /// Adds a child project to the root project + pub fn project, F: FnOnce(&mut ProjectBuilder)>( + &mut self, + path: S, + configure: F, + ) { + let path = path.as_ref(); + let mut builder = ProjectBuilder::new(&self.project_dir, path.to_string()); + (configure)(&mut builder); + self.add_project_from_builder(self.root_project, builder); + } + + /// Adds a child project to some other project + fn add_project_from_builder(&mut self, parent: NodeIndex, builder: ProjectBuilder) { + let ProjectBuilder { + name, + dir, + children, + } = builder; + + let location = match &self.default_build_script_file { + None => ProjectDescriptorLocation::KnownDirectory(dir), + Some(s) => ProjectDescriptorLocation::KnownFile(dir.join(s)), + }; + let pd = ProjectDescriptor::new(name, location); + + let node = self.graph.add_node(pd); + self.graph.add_edge(parent, node, ()); + + for child_builder in children { + self.add_project_from_builder(node, child_builder); + } + } + + /// Find a project by path + pub fn find_project>(&self, path: P) -> Option<&ProjectDescriptor> { + self.graph + .node_indices() + .find(|&idx| self.graph[idx].matches_dir(&path)) + .map(|idx| &self.graph[idx]) + } + + /// Find a project by path + pub fn find_project_mut>(&mut self, path: P) -> Option<&mut ProjectDescriptor> { + self.graph + .node_indices() + .find(|&idx| self.graph[idx].matches_dir(&path)) + .map(|idx| &mut self.graph[idx]) + } +} + +impl Display for ProjectGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut buffer = Vec::new(); + ptree::graph::write_graph_with( + &self.graph, + self.root_project, + &mut buffer, + &PrintConfig::default(), + ) + .map_err(|_| fmt::Error)?; + let string = String::from_utf8(buffer).map_err(|_| fmt::Error)?; + write!(f, "{}", string) + } +} + +/// Helps to build a project +pub struct ProjectBuilder { + name: String, + dir: PathBuf, + children: Vec, +} + +impl ProjectBuilder { + fn new(parent_dir: &Path, name: String) -> Self { + let dir = parent_dir.join(&name); + Self { + name, + dir, + children: vec![], + } + } + + /// Sets the name of this project + pub fn set_name(&mut self, name: impl AsRef) { + self.name = name.as_ref().to_string(); + } + + /// Sets the directory of this project. by default, the directory is the parent projects + /// directory + name + pub fn set_dir(&mut self, path: impl AsRef) { + self.dir = path.as_ref().to_path_buf(); + } + + /// Adds a child project to this project. `path` is relative to this project, and should + /// be written as a simple identifier + pub fn project, F: FnOnce(&mut ProjectBuilder)>( + &mut self, + path: S, + configure: F, + ) { + let path = path.as_ref(); + let mut builder = ProjectBuilder::new(&self.dir, path.to_string()); + (configure)(&mut builder); + self.children.push(builder); + } +} + +#[cfg(test)] +mod tests { + use crate::startup_api::initialization::ProjectGraph; + use std::env::current_dir; + use std::path::PathBuf; + + #[test] + fn print_graph() { + let path = PathBuf::from("assemble"); + let mut graph = ProjectGraph::new(path); + + graph.project("list", |builder| { + builder.project("linked", |_| {}); + builder.project("array", |_| {}); + }); + graph.project("map", |_| {}); + + println!("{}", graph); + } + + #[test] + fn can_set_default_build_name() { + let path = PathBuf::from("assemble"); + let mut graph = ProjectGraph::new(path); + graph.set_default_build_file_name("build.assemble"); + + println!("{}", graph); + assert_eq!( + graph.root_project().build_file(), + Some(&*PathBuf::from_iter(["assemble", "build.assemble"])) + ) + } + + #[test] + fn can_find_project() { + let path = PathBuf::from("assemble"); + let mut graph = ProjectGraph::new(path); + + graph.project("list", |builder| { + builder.project("linked", |_| {}); + builder.project("array", |_| {}); + }); + graph.project("map", |b| { + b.project("set", |_| {}); + b.project("ordered", |_| {}); + b.project("hashed", |_| {}); + }); + + println!("graph: {:#}", graph); -} \ No newline at end of file + assert!(graph.find_project("assemble/map/hashed").is_some()); + assert!(graph.find_project("assemble/list/array").is_some()); + assert!(graph.find_project("assemble/list/garfunkle").is_none()); + } +} diff --git a/crates/assemble-core/src/startup_api/initialization/settings.rs b/crates/assemble-core/src/startup_api/initialization/settings.rs index 3b1d73d..a46e79d 100644 --- a/crates/assemble-core/src/startup_api/initialization/settings.rs +++ b/crates/assemble-core/src/startup_api/initialization/settings.rs @@ -1,3 +1,11 @@ +use crate::plugins::PluginAware; +use crate::prelude::PluginManager; +use crate::startup_api::initialization::{ProjectBuilder, ProjectDescriptor, ProjectGraph}; +use crate::startup_api::invocation::{Assemble, AssembleAware}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use toml_edit::Item; + /// Declares the configuration required to instantiate and configure the hierarchy of [`SharedProject`](crate::project::SharedProject) /// which are part of this build. There's exactly one settings instance that's created per /// settings file. @@ -15,6 +23,85 @@ /// Depends on the builder.. /// pub struct Settings { + assemble: Arc, + plugin_manager: PluginManager, + project_graph: ProjectGraph, + root_dir: PathBuf, +} + +impl Settings { + /// Gets the root project descriptor + pub fn root_project(&self) -> &ProjectDescriptor { + self.project_graph.root_project() + } + + /// Gets a mutable reference to the root project descriptor + pub fn root_project_mut(&mut self) -> &mut ProjectDescriptor { + self.project_graph.root_project_mut() + } + + /// Adds a child project to the root project + pub fn add_project, F: FnOnce(&mut ProjectBuilder)>( + &mut self, + path: S, + configure: F, + ) { + self.project_graph.project(path, configure) + } + + /// Includes a project a path. + pub fn include>(&mut self, path: S) { + self.add_project(path, |_| {}) + } + + /// Includes a project a path. + pub fn include_all, I: IntoIterator>(&mut self, paths: I) { + for path in paths { + self.include(path) + } + } + + /// Find a project within this build + pub fn find_project(&self, path: impl AsRef) -> Option<&ProjectDescriptor> { + self.project_graph.find_project(path) + } + + /// Find a project within this build + pub fn find_project_mut(&mut self, path: impl AsRef) -> Option<&mut ProjectDescriptor> { + self.project_graph.find_project_mut(path) + } + + /// Gets the root directory of this build + pub fn root_dir(&self) -> &Path { + &self.root_dir + } +} + +impl AssembleAware for Settings { + fn get_assemble(&self) -> &Assemble { + self.assemble.as_ref() + } +} + +/// A type that's aware of the settings value +pub trait SettingsAware { + /// Gets the settings value that this value is aware of + fn get_settings(&self) -> &Settings; +} + +impl SettingsAware for Settings { + /// Gets this instance of settings + fn get_settings(&self) -> &Settings { + &self + } +} +impl PluginAware for Settings { + fn plugin_manager(&self) -> &PluginManager { + &self.plugin_manager + } -} \ No newline at end of file + fn plugin_manager_mut(&mut self) -> &mut PluginManager { + &mut self.plugin_manager + } +}