Skip to content

Commit c4ac520

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 c4ac520

File tree

10 files changed

+271
-29
lines changed

10 files changed

+271
-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: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
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::{
7+
collections::HashSet,
8+
fs,
9+
path::{Path, PathBuf},
10+
};
711
use uucore::parser::parse_size;
12+
use uulinux::join_under_root;
813

914
pub struct CpuVulnerability {
1015
pub name: String,
@@ -42,13 +47,16 @@ pub enum CacheType {
4247
}
4348

4449
impl CpuTopology {
45-
pub fn new() -> Self {
50+
pub fn new(root: &Path) -> Self {
4651
let mut out: Vec<Cpu> = vec![];
4752

48-
let online_cpus = parse_cpu_list(&read_online_cpus());
53+
let online_cpus = parse_cpu_list(&read_online_cpus(root));
4954

5055
for cpu_index in online_cpus {
51-
let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/"));
56+
let cpu_dir = join_under_root(
57+
root,
58+
&PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")),
59+
);
5260

5361
let pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id"))
5462
.unwrap()
@@ -62,7 +70,7 @@ impl CpuTopology {
6270
.parse::<usize>()
6371
.unwrap();
6472

65-
let caches = read_cpu_caches(cpu_index);
73+
let caches = read_cpu_caches(root, cpu_index);
6674

6775
out.push(Cpu {
6876
_index: cpu_index,
@@ -120,15 +128,19 @@ impl CacheSize {
120128
}
121129

122130
// 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")
131+
pub fn read_online_cpus(root: &Path) -> String {
132+
let path = join_under_root(root, Path::new("/sys/devices/system/cpu/online"));
133+
fs::read_to_string(path)
125134
.expect("Could not read sysfs")
126135
.trim()
127136
.to_string()
128137
}
129138

130-
fn read_cpu_caches(cpu_index: usize) -> Vec<CpuCache> {
131-
let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/"));
139+
fn read_cpu_caches(root: &Path, cpu_index: usize) -> Vec<CpuCache> {
140+
let cpu_dir = join_under_root(
141+
root,
142+
&PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")),
143+
);
132144
let cache_dir = fs::read_dir(cpu_dir.join("cache")).unwrap();
133145
let cache_paths = cache_dir
134146
.flatten()
@@ -170,16 +182,18 @@ fn read_cpu_caches(cpu_index: usize) -> Vec<CpuCache> {
170182
caches
171183
}
172184

173-
pub fn read_freq_boost_state() -> Option<bool> {
174-
fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost")
185+
pub fn read_freq_boost_state(root: &Path) -> Option<bool> {
186+
let path = join_under_root(root, Path::new("/sys/devices/system/cpu/cpufreq/boost"));
187+
fs::read_to_string(path)
175188
.map(|content| content.trim() == "1")
176189
.ok()
177190
}
178191

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

182-
if let Ok(dir) = fs::read_dir("/sys/devices/system/cpu/vulnerabilities") {
195+
let path = join_under_root(root, Path::new("/sys/devices/system/cpu/vulnerabilities"));
196+
if let Ok(dir) = fs::read_dir(path) {
183197
let mut files: Vec<_> = dir
184198
.flatten()
185199
.map(|x| x.path())
@@ -203,8 +217,9 @@ pub fn read_cpu_vulnerabilities() -> Vec<CpuVulnerability> {
203217
out
204218
}
205219

206-
pub fn read_cpu_byte_order() -> Option<&'static str> {
207-
if let Ok(byte_order) = fs::read_to_string("/sys/kernel/cpu_byteorder") {
220+
pub fn read_cpu_byte_order(root: &Path) -> Option<&'static str> {
221+
let path = join_under_root(root, Path::new("/sys/kernel/cpu_byteorder"));
222+
if let Ok(byte_order) = fs::read_to_string(path) {
208223
match byte_order.trim() {
209224
"big" => return Some("Big Endian"),
210225
"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,9 +12,10 @@ 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};
1616
use std::str::FromStr;
1717
use uucore::{error::UResult, format_usage, help_about, help_usage};
18+
use uulinux::join_under_root;
1819

1920
const ABOUT: &str = help_about!("lsmem.md");
2021
const USAGE: &str = help_usage!("lsmem.md");
@@ -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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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(
25+
Path::new("/sysroot"),
26+
Path::new("/sys/devices/system/memory")
27+
),
28+
PathBuf::from("/sysroot/sys/devices/system/memory"),
29+
);
30+
}
31+
32+
#[test]
33+
fn relative_path_is_joined_normally() {
34+
assert_eq!(
35+
join_under_root(Path::new("/sysroot"), Path::new("sys/devices")),
36+
PathBuf::from("/sysroot/sys/devices"),
37+
);
38+
}
39+
40+
#[test]
41+
fn root_slash_alone_gives_root() {
42+
assert_eq!(
43+
join_under_root(Path::new("/sysroot"), Path::new("/")),
44+
PathBuf::from("/sysroot"),
45+
);
46+
}
47+
48+
#[test]
49+
fn trailing_slash_on_root_is_handled() {
50+
assert_eq!(
51+
join_under_root(Path::new("/sysroot/"), Path::new("/sys/devices")),
52+
PathBuf::from("/sysroot/sys/devices"),
53+
);
54+
}
55+
}

0 commit comments

Comments
 (0)