php_discovery/
build.rs

1use std::fmt::Display;
2use std::hash::Hash;
3use std::path::Path;
4use std::path::PathBuf;
5
6use crate::error::InstallationError;
7use crate::utils::exec;
8
9#[cfg(target_family = "windows")]
10/// Represents a PHP build architecture.
11///
12/// Note: this is only available on windows.
13#[derive(Debug, PartialEq, Eq, Hash)]
14pub enum Architecture {
15    X86,
16    X64,
17    AArch64,
18}
19
20#[cfg(target_family = "windows")]
21/// Try to parse `Architecture` from a string.
22impl TryFrom<&str> for Architecture {
23    type Error = InstallationError;
24
25    fn try_from(value: &str) -> Result<Self, Self::Error> {
26        match value {
27            "x86" => Ok(Self::X86),
28            "x64" => Ok(Self::X64),
29            "arm64" => Ok(Self::AArch64),
30            _ => Err(InstallationError::FailedToRetrieveArch),
31        }
32    }
33}
34
35#[cfg(target_family = "windows")]
36/// Display `Architecture`.
37impl Display for Architecture {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        let name = match self {
40            Architecture::X86 => "x86",
41            Architecture::X64 => "x64",
42            Architecture::AArch64 => "arm64",
43        };
44
45        write!(f, "{}", name)
46    }
47}
48
49/// Represents a PHP version.
50#[derive(Debug, PartialEq, Eq, Hash)]
51pub struct Version {
52    pub major: u32,
53    pub minor: u32,
54    pub release: u32,
55    pub extra: Option<String>,
56}
57
58/// Display `Version`.
59///
60/// Example:
61///
62/// ```
63/// use php_discovery::build::Version;
64///
65/// let v = Version {
66///     major: 8,
67///     minor: 2,
68///     release: 0,
69///     extra: Some("RC6".to_string())
70/// };
71///
72/// assert_eq!("8.2.0RC6", v.to_string());
73///
74/// let v = Version {
75///     major: 7,
76///     minor: 4,
77///     release: 11,
78///     extra: None
79/// };
80///
81/// assert_eq!("7.4.11", v.to_string());
82/// ```
83impl Display for Version {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(
86            f,
87            "{}.{}.{}{}",
88            self.major,
89            self.minor,
90            self.release,
91            self.extra.as_ref().unwrap_or(&"".to_string())
92        )
93    }
94}
95
96/// Represents a PHP build.
97#[derive(Debug, PartialEq, Eq, Hash)]
98pub struct Build {
99    pub version: Version,
100    pub binary: PathBuf,
101    pub directory: PathBuf,
102    pub is_debug: bool,
103    pub is_thread_safety_enabled: bool,
104    pub php_api: u32,
105    pub zend_api: u32,
106    #[cfg(target_family = "windows")]
107    pub architecture: Architecture,
108}
109
110impl Build {
111    pub fn from_binary<P: AsRef<Path>>(binary: P) -> Result<Self, InstallationError> {
112        let binary = binary.as_ref().to_path_buf();
113        if !is_executable::is_executable(&binary) {
114            return Err(InstallationError::BinaryIsNotExecutable(binary));
115        }
116
117        let directory = binary.parent().unwrap().to_path_buf();
118        let version_string = exec(&binary, &["-r", VERSION_CODE])?;
119        let parts = version_string.split('.').collect::<Vec<&str>>();
120        let version = Version {
121            major: parts[0].parse().unwrap(),
122            minor: parts[1].parse().unwrap(),
123            release: parts[2].parse().unwrap(),
124            extra: {
125                let extra = parts[3].to_string();
126
127                if extra.is_empty() {
128                    None
129                } else {
130                    Some(extra)
131                }
132            },
133        };
134
135        let information = exec(&binary, &["-i"])?;
136
137        let mut is_debug = false;
138        let mut is_thread_safety_enabled = false;
139        let mut php_api = None;
140        let mut zend_api = None;
141        #[cfg(target_family = "windows")]
142        let mut architecture = Err(InstallationError::FailedToRetrieveArch);
143
144        for line in information.lines() {
145            if line.contains("Thread Safety =>") {
146                is_thread_safety_enabled = !line.contains("disabled");
147            } else if line.contains("Debug Build =>") {
148                is_debug = !line.contains("no");
149            } else if line.contains("Zend Extension =>") {
150                zend_api = line.get(18..).and_then(|s| s.parse::<u32>().ok());
151            } else if line.contains("PHP Extension =>") {
152                php_api = line.get(17..).and_then(|s| s.parse::<u32>().ok());
153            } else {
154                #[cfg(target_family = "windows")]
155                if line.contains("Architecture =>") {
156                    architecture = line
157                        .get(16..)
158                        .ok_or(InstallationError::FailedToRetrieveArch)
159                        .and_then(|s| TryInto::<Architecture>::try_into(s));
160                }
161            }
162        }
163
164        Ok(Build {
165            version,
166            binary,
167            directory,
168            is_debug,
169            is_thread_safety_enabled,
170            php_api: php_api.ok_or(InstallationError::FailedToRetrieveAPIVersion)?,
171            zend_api: zend_api.ok_or(InstallationError::FailedToRetrieveAPIVersion)?,
172            #[cfg(target_family = "windows")]
173            architecture: architecture?,
174        })
175    }
176
177    /// Retrieve the path to `php-config`, if available.
178    pub fn config(&self) -> Option<PathBuf> {
179        self.bin("php-config")
180    }
181
182    /// Retrieve the path to `phpdbg`, if available.
183    pub fn cgi(&self) -> Option<PathBuf> {
184        self.bin("php-cgi")
185    }
186
187    /// Retrieve the path to `phpize` binary, if available.
188    pub fn phpize(&self) -> Option<PathBuf> {
189        self.bin("phpize")
190    }
191
192    /// Retrieve the path to `phpdbg`, if available.
193    pub fn phpdbg(&self) -> Option<PathBuf> {
194        self.bin("phpdbg")
195    }
196
197    fn bin(&self, name: &str) -> Option<PathBuf> {
198        let filename = self
199            .binary
200            .file_name()?
201            .to_string_lossy()
202            .replace("php", name);
203
204        let config = self.directory.join(filename);
205        if config.exists() {
206            Some(config)
207        } else {
208            None
209        }
210    }
211}
212
213impl AsRef<Path> for Build {
214    fn as_ref(&self) -> &Path {
215        self.binary.as_path()
216    }
217}
218
219const VERSION_CODE: &str =
220    "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION.'.'.PHP_EXTRA_VERSION;";