aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/composer.rs
blob: 2e252c669ebd15d330821ab85699b62ab82421da (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Composer-equivalent root state: composer.json + effective config.
//!
//! Mirrors the role of `Composer\Composer` (PHP) to the extent that command
//! handlers need today: a single struct loaded from the project directory,
//! exposing a `config()` accessor over the merged Composer config.
//!
//! See `Composer\Command\BaseCommand::requireComposer()` /
//! `Composer\Command\BaseCommand::tryComposer()` for the upstream contract
//! that [`Composer::require`] and [`Composer::try_load`] are modelled on.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use crate::config::{Config, resolve_references};
use crate::factory::create_config;

/// Return the Composer home directory, respecting `COMPOSER_HOME` and falling
/// back to the platform default using Composer-compatible logic.
///
/// On Unix:
/// - If XDG is in use (any `XDG_*` env var exists, or `/etc/xdg` exists),
///   prefer `$XDG_CONFIG_HOME/composer` (or `$HOME/.config/composer`).
/// - Always include `$HOME/.composer` as a fallback candidate.
/// - Return the first candidate directory that exists on disk;
///   if none exist, return the first candidate.
pub fn composer_home() -> PathBuf {
    if let Ok(val) = std::env::var("COMPOSER_HOME")
        && !val.is_empty()
    {
        return PathBuf::from(val);
    }

    #[cfg(target_os = "windows")]
    {
        if let Ok(appdata) = std::env::var("APPDATA")
            && !appdata.is_empty()
        {
            return PathBuf::from(appdata).join("Composer");
        }
        return PathBuf::from("C:/ProgramData/ComposerSetup/bin");
    }

    #[cfg(not(target_os = "windows"))]
    {
        let home_dir = std::env::var("HOME")
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from("/tmp"));

        let mut candidates: Vec<PathBuf> = Vec::new();

        if use_xdg() {
            let xdg_config = std::env::var("XDG_CONFIG_HOME")
                .map(PathBuf::from)
                .unwrap_or_else(|_| home_dir.join(".config"));
            candidates.push(xdg_config.join("composer"));
        }

        candidates.push(home_dir.join(".composer"));

        // Return first candidate that exists; otherwise return the first
        candidates
            .iter()
            .find(|p| p.is_dir())
            .cloned()
            .unwrap_or_else(|| candidates.into_iter().next().unwrap())
    }
}

#[cfg(not(target_os = "windows"))]
fn use_xdg() -> bool {
    std::env::vars().any(|(k, _)| k.starts_with("XDG_"))
        || std::path::Path::new("/etc/xdg").is_dir()
}

/// Project-level Composer state. Currently only carries the merged
/// [`Config`]; additional accessors (root package, locker, …) can be
/// layered on as commands need them.
pub struct Composer {
    project_dir: PathBuf,
    config: Config,
}

impl Composer {
    /// Load Composer state for `project_dir`, requiring a composer.json.
    /// Mirrors `BaseCommand::requireComposer()`.
    pub fn require(project_dir: impl Into<PathBuf>) -> anyhow::Result<Self> {
        let project_dir = project_dir.into();
        let composer_json = project_dir.join("composer.json");
        if !composer_json.exists() {
            anyhow::bail!(
                "Composer could not find a composer.json file in {}",
                project_dir.display()
            );
        }
        Self::load(project_dir, &composer_json)
    }

    /// Load Composer state for `project_dir`, returning `None` if no
    /// composer.json exists. Other I/O or parse errors still propagate.
    /// Mirrors `BaseCommand::tryComposer()`.
    pub fn try_load(project_dir: impl Into<PathBuf>) -> anyhow::Result<Option<Self>> {
        let project_dir = project_dir.into();
        let composer_json = project_dir.join("composer.json");
        if !composer_json.exists() {
            return Ok(None);
        }
        Self::load(project_dir, &composer_json).map(Some)
    }

    fn load(project_dir: PathBuf, composer_json: &Path) -> anyhow::Result<Self> {
        let content = std::fs::read_to_string(composer_json)?;
        let value: serde_json::Value = serde_json::from_str(&content)?;
        let mut config = create_config()?;
        if let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) {
            let overrides: BTreeMap<String, serde_json::Value> = cfg_obj
                .iter()
                .map(|(k, v)| (k.clone(), v.clone()))
                .collect();
            config.merge(&overrides)?;
        }
        resolve_references(&mut config);
        Ok(Self {
            project_dir,
            config,
        })
    }

    pub fn project_dir(&self) -> &Path {
        &self.project_dir
    }

    pub fn config(&self) -> &Config {
        &self.config
    }
}