concinnity_dev/project.rs
1//! The project a dev session works on.
2//!
3//! Every tier below this one takes its paths as arguments: the cook is handed
4//! the tree it builds into, the engine's `App` carries the one it runs against,
5//! and the two content-addressed caches are told which files they are. What
6//! remains is that a dev session works on exactly one project for the length of
7//! a process, and that the editor's panels, the hot-reload passes, and the
8//! background workers all reach for it from places with no caller to thread it
9//! down from.
10//!
11//! So the session holds it, and the `concinnity` binary [`open`]s it once at
12//! startup from whatever directory it decided the project lives in. Nothing
13//! here resolves a root: `open` is given one.
14
15use std::sync::{Mutex, OnceLock};
16
17use concinnity_host::store::paths::StateTree;
18
19fn opened() -> &'static Mutex<Option<StateTree>> {
20 static OPENED: OnceLock<Mutex<Option<StateTree>>> = OnceLock::new();
21 OPENED.get_or_init(|| Mutex::new(None))
22}
23
24/// Open `tree` as this session's project, and point the build cache at the
25/// segment it names. Until a host calls this the session has no project: every
26/// build resolves no `assets/`, has nowhere to write blobs, and warms nothing.
27pub fn open(tree: StateTree) {
28 concinnity_cook::cache::anchor(&tree.build_cache_path());
29 *opened().lock().unwrap() = Some(tree);
30}
31
32/// Close the session's project, leaving it with none.
33pub fn close() {
34 concinnity_cook::cache::clear_anchor();
35 *opened().lock().unwrap() = None;
36}
37
38/// The session's project, or `None` when nothing opened one.
39pub fn tree() -> Option<StateTree> {
40 opened().lock().unwrap().clone()
41}
42
43/// An app that reads and writes under the open project: its blobs, settings,
44/// saves and the caches it warms. Without a project the app still runs a world,
45/// and everything it would persist does nothing.
46pub(crate) fn app() -> concinnity_engine::App {
47 let app = concinnity_engine::App::new();
48 match tree() {
49 Some(tree) => app.in_tree(tree),
50 None => app,
51 }
52}
53
54/// The open project, or the error a command reports when the session has none:
55/// every build writes into a tree, so there is nothing sensible to do without
56/// one.
57pub(crate) fn require() -> std::io::Result<StateTree> {
58 tree().ok_or_else(|| {
59 std::io::Error::new(
60 std::io::ErrorKind::NotFound,
61 "no project state directory to build into",
62 )
63 })
64}
65
66/// The `assets/` a bare source filename is resolved against: the open
67/// project's, or `None` for a session with no project, which resolves nothing.
68pub(crate) fn assets_dir() -> Option<std::path::PathBuf> {
69 tree().map(|tree| tree.assets_dir())
70}
71
72/// The `data/` a build writes its blobs into, and a run reads them from.
73pub(crate) fn data_dir() -> Option<std::path::PathBuf> {
74 tree().map(|tree| tree.data_dir())
75}
76
77/// The `worlds/` a named world is looked up in.
78pub(crate) fn worlds_dir() -> Option<std::path::PathBuf> {
79 tree().map(|tree| tree.worlds_dir())
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 // Opening hands the session a tree and nothing else resolves one: the
87 // directories below are the open project's, and closing leaves the session
88 // with none rather than with a guess.
89 #[test]
90 fn opening_a_project_is_what_gives_the_session_its_directories() {
91 // The one exclusive guard: this moves the session-wide project, which
92 // every other test in this binary reads.
93 let _guard = crate::test_support::lock();
94 let dir = concinnity_testing::TempTree::new();
95
96 close();
97 assert_eq!(tree(), None);
98 assert_eq!(assets_dir(), None);
99 assert_eq!(data_dir(), None);
100 assert_eq!(worlds_dir(), None);
101 assert_eq!(
102 require().unwrap_err().kind(),
103 std::io::ErrorKind::NotFound,
104 "a build with no project reports it rather than writing somewhere"
105 );
106
107 open(StateTree::at(dir.path()));
108 assert_eq!(
109 tree().as_ref().map(StateTree::content_root),
110 Some(dir.path())
111 );
112 assert_eq!(assets_dir(), Some(dir.path().join("assets")));
113 assert_eq!(data_dir(), Some(dir.path().join("data")));
114 assert_eq!(worlds_dir(), Some(dir.path().join("worlds")));
115 assert!(require().is_ok());
116
117 // An app built for the session runs against that same tree.
118 assert_eq!(app().state_tree(), tree().as_ref());
119
120 // Leave the binary's other tests the project they expect.
121 crate::test_support::isolate_state_dir();
122 }
123}