Skip to content

Commit 78c235e

Browse files
committed
lscpu: add -s/--sysroot support
Add the `-s`/`--sysroot` option present in upstream util-linux but previously missing. All hardcoded `/sys/` and `/proc/` paths in `sysfs.rs` are threaded through a `root: &Path` parameter; the default root is `/` so behaviour without the flag is unchanged. To support this cleanly, introduce a new local shared crate `uulinux` with a `join_under_root(root, path) -> PathBuf` helper that prepends a sysroot to an absolute system path without discarding the root (unlike `Path::join`). Also use it in `lsmem` to replace the existing hand-rolled path construction there. Add unit tests for `join_under_root` and Linux integration tests for `lscpu --sysroot` using a minimal fake sysfs/procfs tree.
1 parent aab7822 commit 78c235e

File tree

10 files changed

+268
-29
lines changed

10 files changed

+268
-29
lines changed

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ tempfile = "3.9.0"
7676
textwrap = { version = "0.16.0", features = ["terminal_size"] }
7777
thiserror = "2.0"
7878
uucore = "0.2.2"
79+
uulinux = { version = "0.0.1", path = "src/uulinux" }
7980
uuid = { version = "1.16.0", features = ["rng-rand"] }
8081
uutests = "0.7.0"
8182
windows = { version = "0.62.2" }

src/uu/lscpu/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ path = "src/main.rs"
1414
regex = { workspace = true }
1515
sysinfo = { workspace = true }
1616
uucore = { workspace = true, features = ["parser"] }
17+
uulinux = { workspace = true }
1718
clap = { workspace = true }
1819
serde = { workspace = true }
1920
serde_json = { workspace = true }

src/uu/lscpu/src/lscpu.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
use clap::{crate_version, Arg, ArgAction, Command};
77
use regex::RegexBuilder;
88
use serde::Serialize;
9-
use std::{cmp, collections::HashMap, fs};
9+
use std::{cmp, collections::HashMap, fs, path::Path};
1010
use sysfs::CacheSize;
1111
use uucore::{error::UResult, format_usage, help_about, help_usage};
12+
use uulinux::join_under_root;
1213

1314
mod options {
1415
pub const BYTES: &str = "bytes";
1516
pub const HEX: &str = "hex";
1617
pub const JSON: &str = "json";
18+
pub const SYSROOT: &str = "sysroot";
1719
}
1820

1921
mod sysfs;
@@ -80,30 +82,37 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
8082
json: matches.get_flag(options::JSON),
8183
};
8284

85+
let sysroot = matches
86+
.get_one::<String>(options::SYSROOT)
87+
.map(std::path::PathBuf::from)
88+
.unwrap_or_else(|| std::path::PathBuf::from("/"));
89+
let root = sysroot.as_path();
90+
8391
let mut cpu_infos = CpuInfos::new();
8492

8593
let mut arch_info = CpuInfo::new("Architecture", &get_architecture());
8694

8795
// TODO: We just silently ignore failures to read `/proc/cpuinfo` currently and treat it as empty
8896
// Perhaps a better solution should be put in place, but what?
89-
let contents = fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
97+
let proc_cpuinfo = join_under_root(root, Path::new("/proc/cpuinfo"));
98+
let contents = fs::read_to_string(proc_cpuinfo).unwrap_or_default();
9099

91100
if let Some(addr_sizes) = find_cpuinfo_value(&contents, "address sizes") {
92101
arch_info.add_child(CpuInfo::new("Address sizes", &addr_sizes))
93102
}
94103

95-
if let Some(byte_order) = sysfs::read_cpu_byte_order() {
104+
if let Some(byte_order) = sysfs::read_cpu_byte_order(root) {
96105
arch_info.add_child(CpuInfo::new("Byte Order", byte_order));
97106
}
98107

99108
cpu_infos.push(arch_info);
100109

101-
let cpu_topology = sysfs::CpuTopology::new();
110+
let cpu_topology = sysfs::CpuTopology::new(root);
102111
let mut cores_info = CpuInfo::new("CPU(s)", &format!("{}", cpu_topology.cpus.len()));
103112

104113
cores_info.add_child(CpuInfo::new(
105114
"On-line CPU(s) list",
106-
&sysfs::read_online_cpus(),
115+
&sysfs::read_online_cpus(root),
107116
));
108117

109118
cpu_infos.push(cores_info);
@@ -139,7 +148,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
139148
));
140149
model_name_info.add_child(CpuInfo::new("Socket(s)", &socket_count.to_string()));
141150

142-
if let Some(freq_boost_enabled) = sysfs::read_freq_boost_state() {
151+
if let Some(freq_boost_enabled) = sysfs::read_freq_boost_state(root) {
143152
let s = if freq_boost_enabled {
144153
"enabled"
145154
} else {
@@ -158,7 +167,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
158167
cpu_infos.push(cache_info);
159168
}
160169

161-
let vulns = sysfs::read_cpu_vulnerabilities();
170+
let vulns = sysfs::read_cpu_vulnerabilities(root);
162171
if !vulns.is_empty() {
163172
let mut vuln_info = CpuInfo::new("Vulnerabilities", "");
164173
for vuln in vulns {
@@ -350,4 +359,12 @@ pub fn uu_app() -> Command {
350359
Setting this flag instead prints the decimal amount of bytes with no suffix.",
351360
),
352361
)
362+
.arg(
363+
Arg::new(options::SYSROOT)
364+
.short('s')
365+
.long("sysroot")
366+
.action(ArgAction::Set)
367+
.value_name("dir")
368+
.help("Gather CPU data from the specified directory as the system root."),
369+
)
353370
}

src/uu/lscpu/src/sysfs.rs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
use std::{collections::HashSet, fs, path::PathBuf};
6+
use std::{collections::HashSet, fs, path::{Path, PathBuf}};
77
use uucore::parser::parse_size;
8+
use uulinux::join_under_root;
89

910
pub struct CpuVulnerability {
1011
pub name: String,
@@ -42,13 +43,16 @@ pub enum CacheType {
4243
}
4344

4445
impl CpuTopology {
45-
pub fn new() -> Self {
46+
pub fn new(root: &Path) -> Self {
4647
let mut out: Vec<Cpu> = vec![];
4748

48-
let online_cpus = parse_cpu_list(&read_online_cpus());
49+
let online_cpus = parse_cpu_list(&read_online_cpus(root));
4950

5051
for cpu_index in online_cpus {
51-
let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/"));
52+
let cpu_dir = join_under_root(
53+
root,
54+
&PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")),
55+
);
5256

5357
let pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id"))
5458
.unwrap()
@@ -62,7 +66,7 @@ impl CpuTopology {
6266
.parse::<usize>()
6367
.unwrap();
6468

65-
let caches = read_cpu_caches(cpu_index);
69+
let caches = read_cpu_caches(root, cpu_index);
6670

6771
out.push(Cpu {
6872
_index: cpu_index,
@@ -120,15 +124,19 @@ impl CacheSize {
120124
}
121125

122126
// TODO: respect `--hex` option and output the bitmask instead of human-readable range
123-
pub fn read_online_cpus() -> String {
124-
fs::read_to_string("/sys/devices/system/cpu/online")
127+
pub fn read_online_cpus(root: &Path) -> String {
128+
let path = join_under_root(root, Path::new("/sys/devices/system/cpu/online"));
129+
fs::read_to_string(path)
125130
.expect("Could not read sysfs")
126131
.trim()
127132
.to_string()
128133
}
129134

130-
fn read_cpu_caches(cpu_index: usize) -> Vec<CpuCache> {
131-
let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/"));
135+
fn read_cpu_caches(root: &Path, cpu_index: usize) -> Vec<CpuCache> {
136+
let cpu_dir = join_under_root(
137+
root,
138+
&PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")),
139+
);
132140
let cache_dir = fs::read_dir(cpu_dir.join("cache")).unwrap();
133141
let cache_paths = cache_dir
134142
.flatten()
@@ -170,16 +178,18 @@ fn read_cpu_caches(cpu_index: usize) -> Vec<CpuCache> {
170178
caches
171179
}
172180

173-
pub fn read_freq_boost_state() -> Option<bool> {
174-
fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost")
181+
pub fn read_freq_boost_state(root: &Path) -> Option<bool> {
182+
let path = join_under_root(root, Path::new("/sys/devices/system/cpu/cpufreq/boost"));
183+
fs::read_to_string(path)
175184
.map(|content| content.trim() == "1")
176185
.ok()
177186
}
178187

179-
pub fn read_cpu_vulnerabilities() -> Vec<CpuVulnerability> {
188+
pub fn read_cpu_vulnerabilities(root: &Path) -> Vec<CpuVulnerability> {
180189
let mut out: Vec<CpuVulnerability> = vec![];
181190

182-
if let Ok(dir) = fs::read_dir("/sys/devices/system/cpu/vulnerabilities") {
191+
let path = join_under_root(root, Path::new("/sys/devices/system/cpu/vulnerabilities"));
192+
if let Ok(dir) = fs::read_dir(path) {
183193
let mut files: Vec<_> = dir
184194
.flatten()
185195
.map(|x| x.path())
@@ -203,8 +213,9 @@ pub fn read_cpu_vulnerabilities() -> Vec<CpuVulnerability> {
203213
out
204214
}
205215

206-
pub fn read_cpu_byte_order() -> Option<&'static str> {
207-
if let Ok(byte_order) = fs::read_to_string("/sys/kernel/cpu_byteorder") {
216+
pub fn read_cpu_byte_order(root: &Path) -> Option<&'static str> {
217+
let path = join_under_root(root, Path::new("/sys/kernel/cpu_byteorder"));
218+
if let Ok(byte_order) = fs::read_to_string(path) {
208219
match byte_order.trim() {
209220
"big" => return Some("Big Endian"),
210221
"little" => return Some("Little Endian"),

src/uu/lsmem/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ path = "src/main.rs"
1212

1313
[dependencies]
1414
uucore = { workspace = true }
15+
uulinux = { workspace = true }
1516
clap = { workspace = true }
1617
serde = { workspace = true }
1718
serde_json = { workspace = true }

src/uu/lsmem/src/lsmem.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use serde::{Deserialize, Serialize};
1212
use std::borrow::Borrow;
1313
use std::fs;
1414
use std::io::{self, BufRead, BufReader};
15-
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
15+
use std::path::{Path, PathBuf};
16+
use uulinux::join_under_root;
1617
use std::str::FromStr;
1718
use uucore::{error::UResult, format_usage, help_about, help_usage};
1819

@@ -783,12 +784,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
783784
}
784785

785786
if let Some(sysroot) = matches.get_one::<String>(options::SYSROOT) {
786-
opts.sysmem = format!(
787-
"{}{}{}",
788-
sysroot.trim_end_matches(MAIN_SEPARATOR),
789-
MAIN_SEPARATOR,
790-
opts.sysmem.trim_start_matches(MAIN_SEPARATOR)
791-
);
787+
opts.sysmem = join_under_root(Path::new(sysroot), Path::new(&opts.sysmem))
788+
.display()
789+
.to_string();
792790
}
793791

794792
read_info(&mut lsmem, &mut opts);

src/uulinux/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "uulinux"
3+
version = "0.0.1"
4+
edition = "2021"
5+
6+
[dependencies]

src/uulinux/src/lib.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// This file is part of the uutils util-linux package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use std::path::{Path, PathBuf};
7+
8+
/// Join `path` under `root`, ignoring any leading `/` in `path`.
9+
///
10+
/// Unlike [`Path::join`], this never discards `root` when `path` is absolute.
11+
/// Useful for prepending a sysroot to a system path like `/sys/devices/...`.
12+
pub fn join_under_root(root: &Path, path: &Path) -> PathBuf {
13+
let relative = path.strip_prefix("/").unwrap_or(path);
14+
root.join(relative)
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::*;
20+
21+
#[test]
22+
fn absolute_path_is_joined_under_root() {
23+
assert_eq!(
24+
join_under_root(Path::new("/sysroot"), Path::new("/sys/devices/system/memory")),
25+
PathBuf::from("/sysroot/sys/devices/system/memory"),
26+
);
27+
}
28+
29+
#[test]
30+
fn relative_path_is_joined_normally() {
31+
assert_eq!(
32+
join_under_root(Path::new("/sysroot"), Path::new("sys/devices")),
33+
PathBuf::from("/sysroot/sys/devices"),
34+
);
35+
}
36+
37+
#[test]
38+
fn root_slash_alone_gives_root() {
39+
assert_eq!(
40+
join_under_root(Path::new("/sysroot"), Path::new("/")),
41+
PathBuf::from("/sysroot"),
42+
);
43+
}
44+
45+
#[test]
46+
fn trailing_slash_on_root_is_handled() {
47+
assert_eq!(
48+
join_under_root(Path::new("/sysroot/"), Path::new("/sys/devices")),
49+
PathBuf::from("/sysroot/sys/devices"),
50+
);
51+
}
52+
}

0 commit comments

Comments
 (0)