1use std::cmp::Ordering;
6
7use super::command::capture_command;
8use crate::constants::QUALITYLESS_SERVER_NAME;
9use crate::update_service::Platform;
10use lazy_static::lazy_static;
11use regex::bytes::Regex as BinRegex;
12use regex::Regex;
13use tokio::fs;
14
15use super::errors::CodeError;
16
17lazy_static! {
18 static ref LDCONFIG_STDC_RE: Regex = Regex::new(r"libstdc\+\+.* => (.+)").unwrap();
19 static ref LDD_VERSION_RE: BinRegex = BinRegex::new(r"^ldd.*(.+)\.(.+)\s").unwrap();
20 static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap();
21 static ref LIBSTD_CXX_VERSION_RE: BinRegex =
22 BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap();
23 static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19);
24 static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0);
25}
26
27const NIXOS_TEST_PATH: &str = "/etc/NIXOS";
28
29pub struct PreReqChecker {}
30
31impl Default for PreReqChecker {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl PreReqChecker {
38 pub fn new() -> PreReqChecker {
39 PreReqChecker {}
40 }
41
42 #[cfg(not(target_os = "linux"))]
43 pub async fn verify(&self) -> Result<Platform, CodeError> {
44 Platform::env_default().ok_or_else(|| {
45 CodeError::UnsupportedPlatform(format!(
46 "{} {}",
47 std::env::consts::OS,
48 std::env::consts::ARCH
49 ))
50 })
51 }
52
53 #[cfg(target_os = "linux")]
54 pub async fn verify(&self) -> Result<Platform, CodeError> {
55 let (is_nixos, skip_glibc_checks, or_musl) = tokio::join!(
56 check_is_nixos(),
57 skip_requirements_check(),
58 check_musl_interpreter()
59 );
60
61 let (gnu_a, gnu_b) = if !skip_glibc_checks {
62 tokio::join!(check_glibc_version(), check_glibcxx_version())
63 } else {
64 println!("!!! WARNING: Skipping server pre-requisite check !!!");
65 println!("!!! Server stability is not guaranteed. Proceed at your own risk. !!!");
66 (Ok(()), Ok(()))
67 };
68
69 if (gnu_a.is_ok() && gnu_b.is_ok()) || is_nixos {
70 return Ok(if cfg!(target_arch = "x86_64") {
71 Platform::LinuxX64
72 } else if cfg!(target_arch = "arm") {
73 Platform::LinuxARM32
74 } else {
75 Platform::LinuxARM64
76 });
77 }
78
79 if or_musl.is_ok() {
80 return Ok(if cfg!(target_arch = "x86_64") {
81 Platform::LinuxAlpineX64
82 } else {
83 Platform::LinuxAlpineARM64
84 });
85 }
86
87 let mut errors: Vec<String> = vec![];
88 if let Err(e) = gnu_a {
89 errors.push(e);
90 } else if let Err(e) = gnu_b {
91 errors.push(e);
92 }
93
94 if let Err(e) = or_musl {
95 errors.push(e);
96 }
97
98 let bullets = errors
99 .iter()
100 .map(|e| format!(" - {}", e))
101 .collect::<Vec<String>>()
102 .join("\n");
103
104 Err(CodeError::PrerequisitesFailed {
105 bullets,
106 name: QUALITYLESS_SERVER_NAME,
107 })
108 }
109}
110
111#[allow(dead_code)]
112async fn check_musl_interpreter() -> Result<(), String> {
113 const MUSL_PATH: &str = if cfg!(target_arch = "aarch64") {
114 "/lib/ld-musl-aarch64.so.1"
115 } else {
116 "/lib/ld-musl-x86_64.so.1"
117 };
118
119 if fs::metadata(MUSL_PATH).await.is_err() {
120 return Err(format!(
121 "find {}, which is required to run the {} in musl environments",
122 MUSL_PATH, QUALITYLESS_SERVER_NAME
123 ));
124 }
125
126 Ok(())
127}
128
129#[allow(dead_code)]
130async fn check_glibc_version() -> Result<(), String> {
131 #[cfg(target_env = "gnu")]
132 let version = {
133 let v = unsafe { libc::gnu_get_libc_version() };
134 let v = unsafe { std::ffi::CStr::from_ptr(v) };
135 let v = v.to_str().unwrap();
136 extract_generic_version(v)
137 };
138 #[cfg(not(target_env = "gnu"))]
139 let version = {
140 capture_command("ldd", ["--version"])
141 .await
142 .ok()
143 .and_then(|o| extract_ldd_version(&o.stdout))
144 };
145
146 if let Some(v) = version {
147 return if v >= *MIN_LDD_VERSION {
148 Ok(())
149 } else {
150 Err(format!(
151 "find GLIBC >= {} (but found {} instead) for GNU environments",
152 *MIN_LDD_VERSION, v
153 ))
154 };
155 }
156
157 Ok(())
158}
159
160#[allow(dead_code)]
163async fn check_is_nixos() -> bool {
164 fs::metadata(NIXOS_TEST_PATH).await.is_ok()
165}
166
167#[cfg(not(windows))]
172pub async fn skip_requirements_check() -> bool {
173 fs::metadata("/tmp/vscode-skip-server-requirements-check")
174 .await
175 .is_ok()
176}
177
178#[cfg(windows)]
179pub async fn skip_requirements_check() -> bool {
180 false
181}
182
183#[allow(dead_code)]
184async fn check_glibcxx_version() -> Result<(), String> {
185 let mut libstdc_path: Option<String> = None;
186
187 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
188 const DEFAULT_LIB_PATH: &str = "/usr/lib64/libstdc++.so.6";
189 #[cfg(any(target_arch = "x86", target_arch = "arm"))]
190 const DEFAULT_LIB_PATH: &str = "/usr/lib/libstdc++.so.6";
191 const LDCONFIG_PATH: &str = "/sbin/ldconfig";
192
193 if fs::metadata(DEFAULT_LIB_PATH).await.is_ok() {
194 libstdc_path = Some(DEFAULT_LIB_PATH.to_owned());
195 } else if fs::metadata(LDCONFIG_PATH).await.is_ok() {
196 libstdc_path = capture_command(LDCONFIG_PATH, ["-p"])
197 .await
198 .ok()
199 .and_then(|o| extract_libstd_from_ldconfig(&o.stdout));
200 }
201
202 match libstdc_path {
203 Some(path) => match fs::read(&path).await {
204 Ok(contents) => check_for_sufficient_glibcxx_versions(contents),
205 Err(e) => Err(format!(
206 "validate GLIBCXX version for GNU environments, but could not: {}",
207 e
208 )),
209 },
210 None => Err("find libstdc++.so or ldconfig for GNU environments".to_owned()),
211 }
212}
213
214#[allow(dead_code)]
215fn check_for_sufficient_glibcxx_versions(contents: Vec<u8>) -> Result<(), String> {
216 let all_versions: Vec<SimpleSemver> = LIBSTD_CXX_VERSION_RE
217 .captures_iter(&contents)
218 .map(|m| SimpleSemver {
219 major: m.get(1).map_or(0, |s| u32_from_bytes(s.as_bytes())),
220 minor: m.get(2).map_or(0, |s| u32_from_bytes(s.as_bytes())),
221 patch: m.get(3).map_or(0, |s| u32_from_bytes(s.as_bytes())),
222 })
223 .collect();
224
225 if !all_versions.iter().any(|v| &*MIN_CXX_VERSION >= v) {
226 return Err(format!(
227 "find GLIBCXX >= {} (but found {} instead) for GNU environments",
228 *MIN_CXX_VERSION,
229 all_versions
230 .iter()
231 .map(String::from)
232 .collect::<Vec<String>>()
233 .join(", ")
234 ));
235 }
236
237 Ok(())
238}
239
240#[allow(dead_code)]
241fn extract_ldd_version(output: &[u8]) -> Option<SimpleSemver> {
242 LDD_VERSION_RE.captures(output).map(|m| SimpleSemver {
243 major: m.get(1).map_or(0, |s| u32_from_bytes(s.as_bytes())),
244 minor: m.get(2).map_or(0, |s| u32_from_bytes(s.as_bytes())),
245 patch: 0,
246 })
247}
248
249#[allow(dead_code)]
250fn extract_generic_version(output: &str) -> Option<SimpleSemver> {
251 GENERIC_VERSION_RE.captures(output).map(|m| SimpleSemver {
252 major: m.get(1).map_or(0, |s| s.as_str().parse().unwrap()),
253 minor: m.get(2).map_or(0, |s| s.as_str().parse().unwrap()),
254 patch: 0,
255 })
256}
257
258fn extract_libstd_from_ldconfig(output: &[u8]) -> Option<String> {
259 String::from_utf8_lossy(output)
260 .lines()
261 .find_map(|l| LDCONFIG_STDC_RE.captures(l))
262 .and_then(|cap| cap.get(1))
263 .map(|cap| cap.as_str().to_owned())
264}
265
266fn u32_from_bytes(b: &[u8]) -> u32 {
267 String::from_utf8_lossy(b).parse::<u32>().unwrap_or(0)
268}
269
270#[derive(Debug, Default, PartialEq, Eq)]
271struct SimpleSemver {
272 major: u32,
273 minor: u32,
274 patch: u32,
275}
276
277impl PartialOrd for SimpleSemver {
278 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
279 Some(self.cmp(other))
280 }
281}
282
283impl Ord for SimpleSemver {
284 fn cmp(&self, other: &Self) -> Ordering {
285 let major = self.major.cmp(&other.major);
286 if major != Ordering::Equal {
287 return major;
288 }
289
290 let minor = self.minor.cmp(&other.minor);
291 if minor != Ordering::Equal {
292 return minor;
293 }
294
295 self.patch.cmp(&other.patch)
296 }
297}
298
299impl From<&SimpleSemver> for String {
300 fn from(s: &SimpleSemver) -> Self {
301 format!("v{}.{}.{}", s.major, s.minor, s.patch)
302 }
303}
304
305impl std::fmt::Display for SimpleSemver {
306 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
307 write!(f, "{}", String::from(self))
308 }
309}
310
311#[allow(dead_code)]
312impl SimpleSemver {
313 fn new(major: u32, minor: u32, patch: u32) -> SimpleSemver {
314 SimpleSemver {
315 major,
316 minor,
317 patch,
318 }
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_extract_libstd_from_ldconfig() {
328 let actual = "
329 libstoken.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstoken.so.1
330 libstemmer.so.0d (libc6,x86-64) => /lib/x86_64-linux-gnu/libstemmer.so.0d
331 libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6
332 libstartup-notification-1.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstartup-notification-1.so.0
333 libssl3.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libssl3.so
334 ".to_owned().into_bytes();
335
336 assert_eq!(
337 extract_libstd_from_ldconfig(&actual),
338 Some("/lib/x86_64-linux-gnu/libstdc++.so.6".to_owned()),
339 );
340
341 assert_eq!(
342 extract_libstd_from_ldconfig(&"nothing here!".to_owned().into_bytes()),
343 None,
344 );
345 }
346
347 #[test]
348 fn test_gte() {
349 assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(1, 2, 3));
350 assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(0, 10, 10));
351 assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(1, 1, 10));
352
353 assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(1, 2, 10));
354 assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(1, 3, 1));
355 assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(2, 2, 1));
356 }
357
358 #[test]
359 fn check_for_sufficient_glibcxx_versions() {
360 let actual = "ldd (Ubuntu GLIBC 2.31-0ubuntu9.7) 2.31
361 Copyright (C) 2020 Free Software Foundation, Inc.
362 This is free software; see the source for copying conditions. There is NO
363 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
364 Written by Roland McGrath and Ulrich Drepper."
365 .to_owned()
366 .into_bytes();
367
368 assert_eq!(
369 extract_ldd_version(&actual),
370 Some(SimpleSemver::new(2, 31, 0)),
371 );
372 }
373}