cli/util/
prereqs.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5use 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/// Check for nixos to avoid mandating glibc versions. See:
161/// https://github.com/microsoft/vscode-remote-release/issues/7129
162#[allow(dead_code)]
163async fn check_is_nixos() -> bool {
164	fs::metadata(NIXOS_TEST_PATH).await.is_ok()
165}
166
167/// Do not remove this check.
168/// Provides a way to skip the server glibc requirements check from
169/// outside the install flow. A system process can create this
170/// file before the server is downloaded and installed.
171#[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}