Skip to main content

larust_core/
paths.rs

1use std::path::{Path, PathBuf};
2use std::sync::OnceLock;
3
4static PATHS: OnceLock<AppPaths> = OnceLock::new();
5
6/// Canonical locations belonging to one Larust application.
7///
8/// Keeping these paths together avoids a subtle class of bugs where config,
9/// migrations, storage, and static files resolve against different working
10/// directories. `Application::new()` still uses the current directory for
11/// backwards compatibility; production binaries and tests can instead call
12/// `Application::at_root(...)`.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct AppPaths {
15    root: PathBuf,
16}
17
18impl AppPaths {
19    pub fn new(root: impl Into<PathBuf>) -> Self {
20        Self { root: root.into() }
21    }
22
23    pub fn root(&self) -> &Path {
24        &self.root
25    }
26
27    pub fn env(&self) -> PathBuf {
28        self.root.join(".env")
29    }
30
31    pub fn public(&self) -> PathBuf {
32        self.root.join("public")
33    }
34
35    pub fn storage(&self) -> PathBuf {
36        self.root.join("storage")
37    }
38
39    pub fn database(&self) -> PathBuf {
40        self.root.join("database")
41    }
42
43    /// `storage/logs` - where `crate::logging`'s file/stack channel writes
44    /// `larust.log`, mirroring `storage/releases`'s own precedent as an
45    /// established `storage/<subdir>` convention.
46    pub fn logs(&self) -> PathBuf {
47        self.storage().join("logs")
48    }
49
50    /// `storage/sessions` - where `larust_http::session::FileSessionStore`
51    /// writes one file per session when `SESSION_DRIVER=file` (see
52    /// `Config::session_driver`'s own doc comment). Same flat
53    /// `storage/<subdir>` convention `logs()`/`storage/releases` already
54    /// use - not Laravel's own extra `storage/framework/` nesting level,
55    /// which this codebase has never adopted for any of its own
56    /// framework-owned directories.
57    pub fn sessions(&self) -> PathBuf {
58        self.storage().join("sessions")
59    }
60
61    pub fn migrations(&self) -> PathBuf {
62        self.database().join("migrations")
63    }
64
65    pub fn join(&self, relative: impl AsRef<Path>) -> PathBuf {
66        self.root.join(relative)
67    }
68
69    /// Stores `self` as the process-wide paths (`paths()`/`try_paths()`
70    /// below read it back) - called once, from `Application::new()`,
71    /// alongside `Config::publish()`. Same `OnceLock`-backed, first-writer-
72    /// wins shape as `Config::publish` - see that method's own doc comment
73    /// for the identical reasoning (a second `Application::new()` call in
74    /// one process, e.g. a test suite exercising several apps, keeps
75    /// resolving against the *first* call's paths afterward, silently
76    /// wrong rather than panicking).
77    pub(crate) fn publish(self) {
78        let _ = PATHS.set(self);
79    }
80}
81
82impl Default for AppPaths {
83    fn default() -> Self {
84        Self::new(std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
85    }
86}
87
88/// Overrides the compile-time-baked app root a generated app's own
89/// `paths()` function would otherwise always use unconditionally
90/// (`env!("CARGO_MANIFEST_DIR")`, fixed at `cargo build` time to
91/// wherever the build happened to run) - the identical `xr upgrade`/
92/// `LARUST_CHECKOUT_ROOT` pattern (see that command's own `resolve_checkout_root`),
93/// applied here for the same reason: "baked in" doesn't survive every
94/// real scenario. Without this, a binary built on one machine (a CI
95/// runner, a dev box) and then *copied* to a different directory on
96/// whatever actually serves it - rather than built *in place* there,
97/// the way `xr deploy` does it today - would still look for `.env`/
98/// `storage/`/`database/` at the *build* machine's own path, not
99/// wherever it actually ended up. Set `LARUST_APP_ROOT` as a real
100/// process environment variable (e.g. a systemd unit's own
101/// `Environment=LARUST_APP_ROOT=...` line) to unlock exactly that - a
102/// minimal deployment needing nothing but the compiled binary and an
103/// `.env` file sitting next to wherever it's actually run from.
104///
105/// Reads the override from the real process environment only, **never**
106/// from `.env`: `.env`'s own *location* is exactly what this function
107/// decides, so sourcing the override from inside the file it's used to
108/// find in the first place would be circular.
109pub fn resolve_app_root(compiled_in_default: &str) -> String {
110    env_app_root(std::env::var("LARUST_APP_ROOT").ok(), compiled_in_default)
111}
112
113/// The pure decision behind [`resolve_app_root`], split out so it's
114/// unit-testable without touching the real process environment -
115/// mutating `LARUST_APP_ROOT` for a test would race every other test in
116/// this binary reading environment state concurrently (`cargo test`
117/// runs them on shared threads in one process), the same hazard `xr
118/// upgrade`'s own `resolve_checkout_root` and `dev.rs`'s
119/// `LARUST_DEV_RELOAD` already document for the identical reason.
120fn env_app_root(env_override: Option<String>, compiled_in_default: &str) -> String {
121    match env_override {
122        Some(value) if !value.is_empty() => value,
123        _ => compiled_in_default.to_string(),
124    }
125}
126
127/// Returns the process-wide paths `Application::new()` already published -
128/// `None` if no `Application` has been constructed yet in this process
129/// (every narrow test harness that builds a session-bearing router
130/// directly off a bare pool, with no `Application::new()` call anywhere in
131/// the test, hits this case - see `larust_http::session::session_layer`'s
132/// own doc comment for why that's handled gracefully rather than treated
133/// as a contract violation, the identical shape `try_config()` already
134/// has).
135pub fn try_paths() -> Option<&'static AppPaths> {
136    PATHS.get()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn env_app_root_uses_the_override_when_set() {
145        assert_eq!(
146            env_app_root(Some("/opt/myapp".to_string()), "/build/myapp"),
147            "/opt/myapp"
148        );
149    }
150
151    #[test]
152    fn env_app_root_falls_back_to_the_compiled_in_default_when_unset() {
153        assert_eq!(env_app_root(None, "/build/myapp"), "/build/myapp");
154    }
155
156    #[test]
157    fn env_app_root_treats_an_empty_override_the_same_as_unset() {
158        // `LARUST_APP_ROOT=` (set but empty) degrades to the compiled-in
159        // default rather than treating an empty path as a real root -
160        // same reasoning `xr upgrade`'s own `resolve_checkout_root` test
161        // already establishes for `LARUST_CHECKOUT_ROOT`.
162        assert_eq!(
163            env_app_root(Some(String::new()), "/build/myapp"),
164            "/build/myapp"
165        );
166    }
167}