-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathapt.rs
More file actions
229 lines (197 loc) · 7.63 KB
/
apt.rs
File metadata and controls
229 lines (197 loc) · 7.63 KB
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
use super::run_with_sudo::run_with_sudo;
use crate::prelude::*;
use crate::system::{SupportedOs, SystemInfo};
use std::path::Path;
use std::process::Command;
const METADATA_FILENAME: &str = "./tmp/codspeed-cache-metadata.txt";
fn is_system_compatible(system_info: &SystemInfo) -> bool {
matches!(system_info.os, SupportedOs::Linux(ref distro) if distro.is_supported())
}
/// Installs packages with caching support.
///
/// This function provides a common pattern for installing tools on Ubuntu/Debian systems
/// with automatic caching to speed up subsequent installations (e.g., in CI environments).
///
/// # Arguments
///
/// * `system_info` - System information to determine compatibility
/// * `setup_cache_dir` - Optional directory to restore from/save to cache
/// * `is_installed` - Function that checks if the tool is already installed
/// * `install_packages` - Async closure that:
/// 1. Performs the installation (e.g., downloads .deb files, calls `apt::install`)
/// 2. Returns a Vec of package names that should be cached via `dpkg -L`
///
/// # Flow
///
/// 1. Check if already installed - if yes, skip everything
/// 2. Try to restore from cache (if cache_dir provided)
/// 3. Check again if installed - if yes, we're done
/// 4. Run the install closure to install and get package names
/// 5. Save installed packages to cache (if cache_dir provided)
///
/// # Example
///
/// ```rust,ignore
/// apt::install_cached(
/// system_info,
/// setup_cache_dir,
/// || Command::new("which").arg("perf").status().is_ok(),
/// || async {
/// let packages = vec!["linux-tools-common".to_string()];
/// let refs: Vec<&str> = packages.iter().map(|s| s.as_str()).collect();
/// apt::install(system_info, &refs)?;
/// Ok(packages) // Return package names for caching
/// },
/// ).await?;
/// ```
pub async fn install_cached<F, I, Fut>(
system_info: &SystemInfo,
setup_cache_dir: Option<&Path>,
is_installed: F,
install_packages: I,
) -> Result<()>
where
F: Fn() -> bool,
I: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Vec<String>>>,
{
if is_installed() {
debug!("Tool already installed, skipping installation");
return Ok(());
}
// Try to restore from cache first
if let Some(cache_dir) = setup_cache_dir {
restore_from_cache(system_info, cache_dir)?;
if is_installed() {
info!("Tool has been successfully restored from cache");
return Ok(());
}
}
// Install and get the package names for caching
let cache_packages = install_packages().await?;
info!("Installation completed successfully");
// Save to cache after successful installation
if let Some(cache_dir) = setup_cache_dir {
let cache_refs: Vec<&str> = cache_packages.iter().map(|s| s.as_str()).collect();
save_to_cache(system_info, cache_dir, &cache_refs)?;
}
Ok(())
}
pub fn install(system_info: &SystemInfo, packages: &[&str]) -> Result<()> {
if !is_system_compatible(system_info) {
bail!(
"Package installation is not supported on this system, please install necessary packages manually"
);
}
info!("Installing packages: {}", packages.join(", "));
run_with_sudo("apt-get", ["update"])?;
let mut install_argv = vec!["install", "-y", "--allow-downgrades"];
install_argv.extend_from_slice(packages);
run_with_sudo("apt-get", &install_argv)?;
debug!("Packages installed successfully");
Ok(())
}
/// Restore cached tools from the cache directory to the root filesystem
fn restore_from_cache(system_info: &SystemInfo, cache_dir: &Path) -> Result<()> {
if !is_system_compatible(system_info) {
info!("Cache restore is not supported on this system, skipping");
return Ok(());
}
if !cache_dir.exists() {
debug!("Cache directory does not exist: {}", cache_dir.display());
return Ok(());
}
// Check if the directory has any contents
let has_contents = std::fs::read_dir(cache_dir)
.map(|mut entries| entries.next().is_some())
.unwrap_or(false);
if !has_contents {
debug!("Cache directory is empty: {}", cache_dir.display());
return Ok(());
}
debug!(
"Restoring tools from cache directory: {}",
cache_dir.display()
);
// Read and log the metadata file if it exists
let metadata_path = cache_dir.join(METADATA_FILENAME);
if metadata_path.exists() {
match std::fs::read_to_string(&metadata_path) {
Ok(content) => {
info!(
"Packages restored from cache: {}",
content.lines().join(", ")
);
}
Err(e) => {
warn!("Failed to read metadata file: {e}");
}
}
} else {
debug!("No metadata file found in cache directory");
}
// Use bash to properly handle glob expansion
let cache_dir_str = cache_dir
.to_str()
.ok_or_else(|| anyhow!("Invalid cache directory path"))?;
// IMPORTANT: We have to use 'bash' here to ensure that glob patterns are expanded correctly
let copy_cmd = format!("cp -r {cache_dir_str}/* /");
run_with_sudo("bash", ["-c", ©_cmd])?;
debug!("Cache restored successfully");
Ok(())
}
/// Save installed packages to the cache directory
fn save_to_cache(system_info: &SystemInfo, cache_dir: &Path, packages: &[&str]) -> Result<()> {
if !is_system_compatible(system_info) {
info!("Caching of installed package is not supported on this system, skipping");
return Ok(());
}
debug!(
"Saving installed packages to cache: {}",
cache_dir.display()
);
// Create cache directory if it doesn't exist
std::fs::create_dir_all(cache_dir).context("Failed to create cache directory")?;
let cache_dir_str = cache_dir
.to_str()
.ok_or_else(|| anyhow!("Invalid cache directory path"))?;
// Logic taken from https://stackoverflow.com/a/59277514
// This shell command lists all the files outputted by the given packages and copy them to the cache directory
let packages_str = packages.join(" ");
let shell_cmd = format!(
"dpkg -L {packages_str} | while IFS= read -r f; do if test -f \"$f\"; then echo \"$f\"; fi; done | xargs cp --parents --target-directory {cache_dir_str}",
);
debug!("Running cache save command: {shell_cmd}");
let output = Command::new("sh")
.arg("-c")
.arg(&shell_cmd)
.output()
.context("Failed to execute cache save command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("stderr: {stderr}");
bail!("Failed to save packages to cache");
}
// Create metadata file containing the installed packages
let metadata_path = cache_dir.join(METADATA_FILENAME);
let metadata_content = packages.join("\n"); // TODO: add package versions as well, by using the output of the install command for example
if let Ok(()) = std::fs::create_dir_all(metadata_path.parent().unwrap()) {
if let Ok(()) = std::fs::write(&metadata_path, metadata_content)
.context("Failed to write metadata file")
{
debug!("Metadata file created at: {}", metadata_path.display());
} else {
warn!(
"Failed to create metadata file at: {}",
metadata_path.display()
);
}
} else {
warn!(
"Failed to create metadata file parent directory for: {}",
metadata_path.display()
);
}
debug!("Packages cached successfully");
Ok(())
}