Skip to main content

source_map_php/
composer.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ComposerExport {
10    pub root: ComposerPackage,
11    #[serde(default)]
12    pub packages: Vec<ComposerPackage>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ComposerPackage {
17    pub name: String,
18    #[serde(default)]
19    pub version: Option<String>,
20    #[serde(rename = "type", default)]
21    pub package_type: Option<String>,
22    #[serde(default)]
23    pub description: Option<String>,
24    #[serde(default)]
25    pub install_path: Option<String>,
26    #[serde(default)]
27    pub keywords: Vec<String>,
28    #[serde(default)]
29    pub is_root: bool,
30}
31
32impl ComposerExport {
33    pub fn package_for_path<'a>(&'a self, absolute_path: &Path) -> &'a ComposerPackage {
34        self.packages
35            .iter()
36            .filter_map(|package| {
37                let install_path = package.install_path.as_ref()?;
38                let install_path = PathBuf::from(install_path);
39                if absolute_path.starts_with(&install_path) {
40                    Some((install_path.components().count(), package))
41                } else {
42                    None
43                }
44            })
45            .max_by_key(|(depth, _)| *depth)
46            .map(|(_, package)| package)
47            .unwrap_or(&self.root)
48    }
49}
50
51pub fn export_packages(repo: &Path) -> Result<ComposerExport> {
52    if let Ok(export) = export_packages_via_php(repo) {
53        return Ok(export);
54    }
55    export_packages_via_lock(repo)
56}
57
58fn export_packages_via_php(repo: &Path) -> Result<ComposerExport> {
59    let script = r#"<?php
60$repo = $argv[1] ?? getcwd();
61$autoload = $repo . '/vendor/autoload.php';
62if (!file_exists($autoload)) {
63    fwrite(STDERR, "vendor autoload missing\n");
64    exit(2);
65}
66require $autoload;
67$root = json_decode(file_get_contents($repo . '/composer.json'), true);
68$packages = [];
69if (class_exists('Composer\\InstalledVersions')) {
70    foreach (Composer\InstalledVersions::getInstalledPackages() as $name) {
71        $packages[] = [
72            'name' => $name,
73            'version' => Composer\InstalledVersions::getPrettyVersion($name),
74            'install_path' => Composer\InstalledVersions::getInstallPath($name),
75            'type' => null,
76            'description' => null,
77            'keywords' => [],
78            'is_root' => false,
79        ];
80    }
81}
82echo json_encode([
83    'root' => [
84        'name' => $root['name'] ?? 'root/app',
85        'version' => $root['version'] ?? null,
86        'type' => $root['type'] ?? null,
87        'description' => $root['description'] ?? null,
88        'install_path' => $repo,
89        'keywords' => $root['keywords'] ?? [],
90        'is_root' => true,
91    ],
92    'packages' => $packages,
93], JSON_UNESCAPED_SLASHES);
94"#;
95
96    let temp = tempfile::NamedTempFile::new().context("create php exporter temp file")?;
97    fs::write(temp.path(), script)?;
98
99    let output = Command::new("php")
100        .arg(temp.path())
101        .arg(repo)
102        .output()
103        .context("run embedded composer exporter")?;
104    if !output.status.success() {
105        anyhow::bail!("{}", String::from_utf8_lossy(&output.stderr));
106    }
107    Ok(serde_json::from_slice(&output.stdout)?)
108}
109
110fn export_packages_via_lock(repo: &Path) -> Result<ComposerExport> {
111    #[derive(Debug, Deserialize)]
112    struct ComposerJson {
113        name: Option<String>,
114        version: Option<String>,
115        #[serde(rename = "type")]
116        package_type: Option<String>,
117        description: Option<String>,
118        #[serde(default)]
119        keywords: Vec<String>,
120    }
121    #[derive(Debug, Deserialize)]
122    struct LockPackage {
123        name: String,
124        version: Option<String>,
125        #[serde(rename = "type")]
126        package_type: Option<String>,
127        description: Option<String>,
128        #[serde(default)]
129        keywords: Vec<String>,
130    }
131    #[derive(Debug, Deserialize)]
132    struct LockFile {
133        #[serde(default)]
134        packages: Vec<LockPackage>,
135    }
136
137    let composer_json: ComposerJson = serde_json::from_slice(
138        &fs::read(repo.join("composer.json")).context("read composer.json")?,
139    )?;
140    let lock: LockFile = serde_json::from_slice(
141        &fs::read(repo.join("composer.lock")).unwrap_or_else(|_| b"{\"packages\":[]}".to_vec()),
142    )?;
143
144    Ok(ComposerExport {
145        root: ComposerPackage {
146            name: composer_json.name.unwrap_or_else(|| "root/app".to_string()),
147            version: composer_json.version,
148            package_type: composer_json.package_type,
149            description: composer_json.description,
150            install_path: Some(repo.display().to_string()),
151            keywords: composer_json.keywords,
152            is_root: true,
153        },
154        packages: lock
155            .packages
156            .into_iter()
157            .map(|package| ComposerPackage {
158                install_path: Some(
159                    repo.join("vendor")
160                        .join(package.name.replace('/', std::path::MAIN_SEPARATOR_STR))
161                        .display()
162                        .to_string(),
163                ),
164                name: package.name,
165                version: package.version,
166                package_type: package.package_type,
167                description: package.description,
168                keywords: package.keywords,
169                is_root: false,
170            })
171            .collect(),
172    })
173}
174
175#[cfg(test)]
176mod tests {
177    use std::fs;
178
179    use tempfile::tempdir;
180
181    use super::export_packages;
182
183    #[test]
184    fn lockfile_fallback_exports_root_and_packages() {
185        let dir = tempdir().unwrap();
186        fs::write(
187            dir.path().join("composer.json"),
188            r#"{"name":"acme/app","description":"demo","keywords":["php","search"]}"#,
189        )
190        .unwrap();
191        fs::write(
192            dir.path().join("composer.lock"),
193            r#"{"packages":[{"name":"laravel/framework","version":"11.0.0","type":"library"}]}"#,
194        )
195        .unwrap();
196
197        let export = export_packages(dir.path()).unwrap();
198        assert_eq!(export.root.name, "acme/app");
199        assert_eq!(export.packages[0].name, "laravel/framework");
200    }
201}