assemble_core/
project.rs

1use crate::defaults::plugins::BasePlugin;
2use std::cell::RefCell;
3
4use crate::dependencies::dependency_container::ConfigurationHandler;
5
6use crate::dependencies::RegistryContainer;
7
8use crate::file::RegularFile;
9
10use crate::flow::output::VariantHandler;
11use crate::flow::shared::ConfigurableArtifact;
12use crate::identifier::{Id, InvalidId, ProjectId, TaskId, TaskIdFactory};
13use crate::lazy_evaluation::{Prop, Provider};
14use crate::logging::LOGGING_CONTROL;
15use crate::plugins::extensions::{ExtensionAware, ExtensionContainer};
16use crate::plugins::{Plugin, PluginAware, PluginManager};
17
18use crate::task::task_container::TaskContainer;
19use crate::task::AnyTaskHandle;
20use crate::task::{Task, TaskHandle};
21use crate::workspace::WorkspaceDirectory;
22use crate::Workspace;
23use log::debug;
24use once_cell::sync::OnceCell;
25
26use std::collections::HashMap;
27
28use std::fmt::{Debug, Display, Formatter};
29
30use std::ops::{Deref, DerefMut, Not};
31use std::path::{Path, PathBuf};
32
33use parking_lot::{ReentrantMutex, ReentrantMutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
34use std::sync::{Arc, Mutex, TryLockError, Weak};
35use tempfile::TempDir;
36
37pub mod buildable;
38#[cfg(test)]
39pub mod dev;
40pub mod error;
41pub mod finder;
42pub mod requests;
43pub mod shared;
44pub mod variant;
45
46use crate::error::PayloadError;
47use crate::prelude::{Settings, SettingsAware};
48use crate::project::finder::TaskPath;
49pub use error::*;
50use shared::{SharedProject, TrueSharableProject, WeakSharedProject};
51
52pub mod prelude {
53    pub use super::Project;
54    pub use crate::dependencies::project_dependency::CreateProjectDependencies;
55}
56
57/// The Project contains the tasks, layout information, and other related objects that would help
58/// with project building.
59///
60/// The project itself should be able to provide all information required to build a project, but
61/// should not be the driver of the building itself. Instead, project visitors should be driven to
62/// create project files.
63///
64/// By default, projects are created in the current directory.
65///
66/// # Example
67/// ```
68/// # use assemble_core::Project;
69/// # use assemble_core::defaults::tasks::Empty;
70/// # let mut project = Project::temp(None);
71/// let mut task_provider = project.tasks().register_task::<Empty>("hello_world").expect("Couldn't create 'hello_task'");
72/// task_provider.configure_with(|empty, _project| {
73///     empty.do_first(|_, _| {
74///         println!("Hello, World");
75///         Ok(())
76///     }).unwrap();
77///     Ok(())
78/// }).unwrap();
79/// ```
80#[derive(Debug)]
81pub struct Project {
82    settings: Option<Weak<RwLock<Settings>>>,
83    project_id: ProjectId,
84    task_id_factory: TaskIdFactory,
85    task_container: TaskContainer,
86    workspace: Workspace,
87    build_dir: Prop<PathBuf>,
88    applied_plugins: Vec<String>,
89    variants: VariantHandler,
90    self_reference: OnceCell<WeakSharedProject>,
91    properties: HashMap<String, Option<String>>,
92    default_tasks: Vec<TaskId>,
93    registries: Arc<Mutex<RegistryContainer>>,
94    configurations: ConfigurationHandler,
95    subprojects: HashMap<ProjectId, SharedProject>,
96    parent_project: OnceCell<WeakSharedProject>,
97    root_project: OnceCell<WeakSharedProject>,
98    extensions: ExtensionContainer,
99    plugin_manager: PluginManager<Project>,
100
101    is_root: bool,
102}
103
104impl Drop for Project {
105    fn drop(&mut self) {
106        warn!("dropping project {}", self.project_id);
107    }
108}
109//
110// impl Debug for Project {
111//     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112//         write!(f, "Project {:?}", self.project_id)
113//     }
114// }
115
116impl Display for Project {
117    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118        write!(f, "Project {}", self.project_id)
119    }
120}
121
122impl Project {
123    #[doc(hidden)]
124    pub fn temp<'a, I: Into<Option<&'a str>>>(id: I) -> SharedProject {
125        Self::in_dir_with_id(TempDir::new().unwrap().path(), id.into().unwrap_or("root")).unwrap()
126    }
127
128    /// Create a new Project, with the current directory as the the directory to load
129    pub fn new() -> error::Result<SharedProject> {
130        let file = std::env::current_dir().unwrap();
131        let name = file.file_name().unwrap().to_str().unwrap().to_string();
132        Self::in_dir_with_id(file, ProjectId::new(&name).map_err(PayloadError::new)?)
133    }
134
135    /// Creates an assemble project in a specified directory.
136    pub fn in_dir(path: impl AsRef<Path>) -> error::Result<SharedProject> {
137        let path = path.as_ref();
138        Self::in_dir_with_id(path, path)
139    }
140
141    /// Creates an assemble project in the current directory using an identifier
142    pub fn with_id<I: TryInto<ProjectId>>(id: I) -> error::Result<SharedProject>
143    where
144        ProjectError: From<<I as TryInto<ProjectId>>::Error>,
145    {
146        Self::in_dir_with_id(std::env::current_dir().unwrap(), id)
147    }
148
149    /// Creates an assemble project in a specified directory.
150    pub fn in_dir_with_id<Id: TryInto<ProjectId>, P: AsRef<Path>>(
151        path: P,
152        id: Id,
153    ) -> error::Result<SharedProject>
154    where
155        ProjectError: From<<Id as TryInto<ProjectId>>::Error>,
156    {
157        Self::in_dir_with_id_and_root(path, id, None, None)
158    }
159
160    /// Creates an assemble project in a specified directory.
161    pub fn in_dir_with_id_and_root<Id: TryInto<ProjectId>, P: AsRef<Path>>(
162        path: P,
163        id: Id,
164        root: Option<&SharedProject>,
165        settings: Option<Weak<RwLock<Settings>>>,
166    ) -> error::Result<SharedProject>
167    where
168        ProjectError: From<<Id as TryInto<ProjectId>>::Error>,
169    {
170        let id = id.try_into().map_err(PayloadError::new)?;
171        LOGGING_CONTROL.in_project(id.clone());
172        let factory = TaskIdFactory::new(id.clone());
173        let mut build_dir = Prop::new(id.join("buildDir").map_err(PayloadError::new)?);
174        build_dir
175            .set(path.as_ref().join("build"))
176            .map_err(PayloadError::new)?;
177        let registries = Arc::new(Mutex::new(Default::default()));
178        let dependencies = ConfigurationHandler::new(id.clone(), &registries);
179
180        let project = SharedProject::new_cyclic(|cycle| {
181            let mut project = Self {
182                settings: settings.clone(),
183                project_id: id,
184                task_id_factory: factory.clone(),
185                task_container: TaskContainer::new(factory),
186                workspace: Workspace::new(path),
187                build_dir,
188                applied_plugins: Default::default(),
189                variants: VariantHandler::new(),
190                self_reference: OnceCell::new(),
191                properties: Default::default(),
192                default_tasks: vec![],
193                registries,
194                configurations: dependencies,
195                subprojects: Default::default(),
196                parent_project: OnceCell::new(),
197                root_project: OnceCell::new(),
198                extensions: ExtensionContainer::default(),
199                plugin_manager: PluginManager::default(),
200                is_root: root.is_none(),
201            };
202
203            project.task_container.init(cycle);
204            project.self_reference.set(cycle.clone()).unwrap();
205            if let Some(root) = root {
206                project.root_project.set(root.weak()).unwrap();
207            } else {
208                project.root_project.set(cycle.clone()).unwrap();
209            }
210
211            project
212        });
213
214        project.apply_plugin::<BasePlugin>()?;
215
216        LOGGING_CONTROL.reset();
217        Ok(project)
218    }
219
220    /// Get the id of the project
221    pub fn id(&self) -> &ProjectId {
222        &self.project_id
223    }
224
225    /// Gets the directory where created files should be stored
226    pub fn build_dir(&self) -> impl Provider<PathBuf> + Clone {
227        self.build_dir.clone()
228    }
229
230    /// Always set as relative to the project dir
231    pub fn set_build_dir(&mut self, dir: &str) {
232        let dir = self.workspace.dir(dir).unwrap();
233        let path = dir.rel_path();
234        self.build_dir.set(path).unwrap();
235    }
236
237    /// Gets a list of all tasks by [`TaskId`](TaskId)
238    pub fn registered_tasks(&self) -> Vec<TaskId> {
239        self.task_container
240            .get_tasks()
241            .into_iter()
242            .cloned()
243            .collect()
244    }
245
246    /// Create files using some valid types
247    ///
248    /// Allowed types:
249    /// - &str
250    /// - String
251    /// - Path
252    /// - Regular File
253    pub fn file<T: AsRef<Path>>(&self, any_value: T) -> error::Result<RegularFile> {
254        let path = any_value.as_ref();
255        debug!("trying to create/get file {:?}", path);
256        self.workspace.create_file(path).map_err(PayloadError::new)
257    }
258
259    /// Run a visitor on the project
260    pub fn visitor<R, V: VisitProject<R>>(&self, visitor: &mut V) -> R {
261        visitor.visit(self)
262    }
263
264    /// Run a mutable visitor on the project
265    pub fn visitor_mut<R, V: VisitMutProject<R>>(&mut self, visitor: &mut V) -> R {
266        visitor.visit_mut(self)
267    }
268
269    /// The directory of the project
270    pub fn project_dir(&self) -> PathBuf {
271        self.workspace.absolute_path()
272    }
273
274    /// The project directory for the root directory
275    pub fn root_dir(&self) -> PathBuf {
276        self.root_project().with(|p| p.project_dir())
277    }
278
279    /// Get access to the task container
280    pub fn task_container(&self) -> &TaskContainer {
281        &self.task_container
282    }
283
284    /// Get access to the task container
285    pub fn task_container_mut(&mut self) -> &mut TaskContainer {
286        &mut self.task_container
287    }
288
289    /// Get an outgoing variant
290    pub fn variant(&self, variant: &str) -> Option<impl Provider<ConfigurableArtifact>> {
291        self.variants.get_artifact(variant)
292    }
293
294    /// Gets the shared reference version of this project
295    pub fn as_shared(&self) -> SharedProject {
296        SharedProject::try_from(self.self_reference.get().unwrap().clone()).unwrap()
297    }
298
299    /// Gets the factory for generating task ids
300    pub fn task_id_factory(&self) -> &TaskIdFactory {
301        &self.task_id_factory
302    }
303
304    pub fn properties(&self) -> &HashMap<String, Option<String>> {
305        &self.properties
306    }
307
308    pub fn set_property(&mut self, key: String, value: impl Into<Option<String>>) {
309        self.properties.insert(key, value.into());
310    }
311
312    pub fn get_property(&self, key: &str) -> Option<&Option<String>> {
313        self.properties.get(key)
314    }
315
316    pub fn has_property(&self, key: &str) -> bool {
317        self.properties.contains_key(key)
318    }
319
320    /// Gets the subprojects for this project.
321    pub fn subprojects(&self) -> Vec<&SharedProject> {
322        self.subprojects.values().collect()
323    }
324
325    /// Gets the default tasks for this project.
326    ///
327    /// Default tasks are executed if no other tasks are provided.
328    pub fn default_tasks(&self) -> &Vec<TaskId> {
329        &self.default_tasks
330    }
331
332    /// Set the default tasks for this project.
333    pub fn set_default_tasks<I: IntoIterator<Item = TaskId>>(&mut self, iter: I) {
334        self.default_tasks = Vec::from_iter(iter);
335    }
336
337    /// apply a configuration function on the registries container
338    pub fn registries_mut<F: FnOnce(&mut RegistryContainer) -> ProjectResult>(
339        &mut self,
340        configure: F,
341    ) -> ProjectResult {
342        let mut registries = self.registries.lock().map_err(PayloadError::new)?;
343        configure(registries.deref_mut())
344    }
345
346    /// apply a function on the registries container
347    pub fn registries<R, F: FnOnce(&RegistryContainer) -> ProjectResult<R>>(
348        &self,
349        configure: F,
350    ) -> ProjectResult<R> {
351        let registries = self.registries.lock().map_err(PayloadError::new)?;
352        configure(registries.deref())
353    }
354
355    /// Get the dependencies for this project
356    pub fn configurations(&self) -> &ConfigurationHandler {
357        &self.configurations
358    }
359
360    /// Get a mutable reference to the dependencies container for this project
361    pub fn configurations_mut(&mut self) -> &mut ConfigurationHandler {
362        &mut self.configurations
363    }
364
365    pub fn get_subproject<P: AsRef<str>>(&self, project: P) -> error::Result<&SharedProject> {
366        let id = ProjectId::from(self.project_id().join(project).map_err(PayloadError::new)?);
367        self.subprojects
368            .get(&id)
369            .ok_or(ProjectError::NoIdentifiersFound(id.to_string()).into())
370    }
371
372    /// Create a sub project with a given name. The path used is the `$PROJECT_DIR/name`
373    pub fn subproject<F>(&mut self, name: &str, configure: F) -> ProjectResult
374    where
375        F: FnOnce(&mut Project) -> ProjectResult,
376    {
377        let path = self.project_dir().join(name);
378        self.subproject_in(name, path, configure)
379    }
380
381    /// Create a sub project with a given name at a path.
382    pub fn subproject_in<P, F>(&mut self, name: &str, path: P, configure: F) -> ProjectResult
383    where
384        F: FnOnce(&mut Project) -> ProjectResult,
385        P: AsRef<Path>,
386    {
387        let root_shared = self.root_project();
388        let self_shared = self.as_shared();
389        let id = ProjectId::from(self.project_id.join(name).map_err(PayloadError::new)?);
390        let shared = self.subprojects.entry(id.clone()).or_insert_with(|| {
391            Project::in_dir_with_id_and_root(
392                path,
393                id.clone(),
394                Some(&root_shared),
395                self.settings.clone(),
396            )
397            .unwrap()
398        });
399        shared.with_mut(|p| p.parent_project.set(self_shared.weak()).unwrap());
400        shared.with_mut(configure)
401    }
402
403    /// Gets the root project of this project.
404    pub fn root_project(&self) -> SharedProject {
405        SharedProject::try_from(self.root_project.get().unwrap().clone()).unwrap()
406    }
407
408    /// Gets the parent project of this project, if it exists
409    pub fn parent_project(&self) -> Option<SharedProject> {
410        self.parent_project
411            .get()
412            .and_then(|p| p.clone().upgrade().ok())
413            .map(|s| s.into())
414    }
415
416    /// The variants of this project
417    pub fn variants(&self) -> &VariantHandler {
418        &self.variants
419    }
420
421    /// The mutable variants of this project
422    pub fn variants_mut(&mut self) -> &mut VariantHandler {
423        &mut self.variants
424    }
425
426    /// Gets the reference to the settings object
427    fn settings(&self) -> Arc<RwLock<Settings>> {
428        self.settings
429            .as_ref()
430            .and_then(|weak| weak.upgrade())
431            .expect("settings not set")
432    }
433}
434
435impl PluginAware for Project {
436    fn plugin_manager(&self) -> &PluginManager<Self> {
437        &self.plugin_manager
438    }
439
440    fn plugin_manager_mut(&mut self) -> &mut PluginManager<Self> {
441        &mut self.plugin_manager
442    }
443}
444
445impl ExtensionAware for Project {
446    fn extensions(&self) -> &ExtensionContainer {
447        &self.extensions
448    }
449
450    fn extensions_mut(&mut self) -> &mut ExtensionContainer {
451        &mut self.extensions
452    }
453}
454
455///  trait for visiting projects
456pub trait VisitProject<R = ()> {
457    /// Visit the project
458    fn visit(&mut self, project: &Project) -> R;
459}
460
461/// trait for visiting project thats mutable
462pub trait VisitMutProject<R = ()> {
463    /// Visit a mutable project.
464    fn visit_mut(&mut self, project: &mut Project) -> R;
465}
466
467impl SettingsAware for Project {
468    fn with_settings<F: FnOnce(&Settings) -> R, R>(&self, func: F) -> R {
469        self.settings().with_settings(func)
470    }
471
472    fn with_settings_mut<F: FnOnce(&mut Settings) -> R, R>(&mut self, func: F) -> R {
473        self.settings().with_settings_mut(func)
474    }
475}
476
477pub trait GetProjectId {
478    fn project_id(&self) -> ProjectId;
479    fn parent_id(&self) -> Option<ProjectId>;
480    fn root_id(&self) -> ProjectId;
481
482    /// Get whether this project is a root
483    fn is_root(&self) -> bool {
484        self.root_id() == self.project_id()
485    }
486}
487
488impl GetProjectId for Project {
489    fn project_id(&self) -> ProjectId {
490        self.id().clone()
491    }
492
493    fn parent_id(&self) -> Option<ProjectId> {
494        self.parent_project().as_ref().map(GetProjectId::project_id)
495    }
496
497    fn root_id(&self) -> ProjectId {
498        self.root_project().project_id()
499    }
500
501    fn is_root(&self) -> bool {
502        self.is_root
503    }
504}
505
506#[cfg(test)]
507mod test {
508    use crate::defaults::tasks::Empty;
509    use crate::logging::init_root_log;
510    use crate::project::Project;
511
512    use crate::project::shared::SharedProject;
513    use crate::workspace::WorkspaceDirectory;
514    use log::LevelFilter;
515    use std::env;
516    use std::path::PathBuf;
517    use tempfile::TempDir;
518
519    #[test]
520    fn create_tasks() {
521        let project = SharedProject::default();
522
523        let mut provider = project.tasks().register_task::<Empty>("arbitrary").unwrap();
524        provider.configure_with(|_, _| Ok(())).unwrap();
525    }
526
527    #[test]
528    fn project_name_based_on_directory() {
529        let path = PathBuf::from("parent_dir/ProjectName");
530        let project = Project::in_dir(path).unwrap();
531
532        assert_eq!(
533            project.with(|p| p.id().clone()).to_string(),
534            ":parent_dir:ProjectName"
535        );
536    }
537
538    #[test]
539    fn create_files_in_project() {
540        init_root_log(LevelFilter::Debug, None);
541        let temp_dir = TempDir::new_in(env::current_dir().unwrap()).unwrap();
542        assert!(temp_dir.path().exists());
543        let project = Project::temp(None);
544        project.with(|project| {
545            let file = project.file("test1").expect("Couldn't make file from &str");
546            assert_eq!(file.path(), project.project_dir().join("test1"));
547            let file = project
548                .file("test2")
549                .expect("Couldn't make file from String");
550            assert_eq!(file.path(), project.project_dir().join("test2"));
551        })
552    }
553}