Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions profiles/pci/power_management/profiles.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[intel-lpmd]
desc = 'Intel Low Power Mode Daemon for supported hybrid Intel CPUs'
class_ids = "0600"
vendor_ids = "8086"
device_ids = "*"
cpu_family = "6"
cpu_models = "151 154 183 186 191 170 172 189 204"
priority = 5
packages = 'intel-lpmd'
post_install = """
systemctl enable --now intel_lpmd.service
"""
post_remove = """
systemctl disable intel_lpmd.service
"""
78 changes: 78 additions & 0 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ pub fn get_all_devices_of_profile(devices: &ListOfDevicesT, profile: &Profile) -
}
}

if profile.cpu_family.is_some() {
match crate::hwd_misc::get_cpu_info() {
Some(cpu_info) if matches_cpu_filter(profile, &cpu_info) => {},
_ => return vec![],
}
}

if let Some(gc_versions) = &profile.gc_versions {
if let Some(hwd_gc_versions) = crate::hwd_misc::get_gc_versions() {
return get_all_devices_from_gc_versions(devices, &hwd_gc_versions, gc_versions);
Expand Down Expand Up @@ -320,6 +327,20 @@ pub fn get_all_devices_of_profile(devices: &ListOfDevicesT, profile: &Profile) -
found_indices
}

fn matches_cpu_filter(profile: &Profile, cpu_info: &crate::hwd_misc::CpuInfo) -> bool {
if let Some(cpu_family) = &profile.cpu_family {
if cpu_info.family != *cpu_family {
return false;
}
}
if let Some(cpu_models) = &profile.cpu_models {
if !cpu_models.contains(&cpu_info.model) {
return false;
}
}
true
}

fn add_profile_sorted(profiles: &mut Vec<Arc<Profile>>, new_profile: &Profile) {
if profiles.iter().any(|x| new_profile.name == x.name) {
return;
Expand Down Expand Up @@ -772,6 +793,63 @@ mod tests {
]
}

fn cpu_test_profile(
cpu_family: Option<&str>,
cpu_models: Option<Vec<&str>>,
) -> crate::profile::Profile {
crate::profile::Profile {
cpu_family: cpu_family.map(str::to_string),
cpu_models: cpu_models
.map(|v| v.into_iter().map(str::to_string).collect()),
..Default::default()
}
}

#[test]
fn cpu_filter_matches_family_and_model() {
let cpu_info =
crate::hwd_misc::CpuInfo { vendor: "GenuineIntel".into(), family: "6".into(), model: "154".into() };
let profile = cpu_test_profile(Some("6"), Some(vec!["151", "154", "183"]));

assert!(data::matches_cpu_filter(&profile, &cpu_info));
}

#[test]
fn cpu_filter_rejects_wrong_family() {
let cpu_info =
crate::hwd_misc::CpuInfo { vendor: "AuthenticAMD".into(), family: "25".into(), model: "80".into() };
let profile = cpu_test_profile(Some("6"), Some(vec!["151", "154"]));

assert!(!data::matches_cpu_filter(&profile, &cpu_info));
}

#[test]
fn cpu_filter_rejects_wrong_model() {
let cpu_info =
crate::hwd_misc::CpuInfo { vendor: "GenuineIntel".into(), family: "6".into(), model: "142".into() };
let profile = cpu_test_profile(Some("6"), Some(vec!["151", "154"]));

assert!(!data::matches_cpu_filter(&profile, &cpu_info));
}

#[test]
fn cpu_filter_family_only_matches() {
let cpu_info =
crate::hwd_misc::CpuInfo { vendor: "GenuineIntel".into(), family: "6".into(), model: "999".into() };
let profile = cpu_test_profile(Some("6"), None);
// no cpu_models filter — any model in family 6 should match
assert!(data::matches_cpu_filter(&profile, &cpu_info));
}

#[test]
fn cpu_filter_no_filter_matches_all() {
let cpu_info =
crate::hwd_misc::CpuInfo { vendor: "AuthenticAMD".into(), family: "25".into(), model: "80".into() };
let profile = cpu_test_profile(None, None);
// no cpu_family, no cpu_models
assert!(data::matches_cpu_filter(&profile, &cpu_info));
}

#[test]
fn get_devices_from_gc_versions() {
let devices = test_data();
Expand Down
74 changes: 74 additions & 0 deletions src/hwd_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

pub struct CpuInfo {
pub vendor: String,
pub family: String,
pub model: String,
}

#[must_use]
pub fn get_cpu_info() -> Option<CpuInfo> {
let content = std::fs::read_to_string("/proc/cpuinfo").ok()?;
parse_cpu_info(&content)
}

fn parse_cpu_info(content: &str) -> Option<CpuInfo> {
let mut vendor = None;
let mut family = None;
let mut model = None;

for line in content.lines() {
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
match key {
"vendor_id" if vendor.is_none() => vendor = Some(value.to_owned()),
"cpu family" if family.is_none() => family = Some(value.to_owned()),
"model" if model.is_none() => model = Some(value.to_owned()),
_ => {},
}
}
if vendor.is_some() && family.is_some() && model.is_some() {
break;
}
}

Some(CpuInfo { vendor: vendor?, family: family?, model: model? })
}

#[must_use]
pub fn get_sysfs_busid_from_amdgpu_path(amdgpu_path: &str) -> &str {
amdgpu_path.split('/')
Expand Down Expand Up @@ -111,4 +147,42 @@ mod tests {

assert_eq!(hwd_misc::get_sysfs_busid_from_amdgpu_path("/sys/bus/pci/drivers/amdgpu/"), "");
}

#[test]
fn parse_cpu_info_intel() {
let cpuinfo = "\
processor\t: 0
vendor_id\t: GenuineIntel
cpu family\t: 6
model\t\t: 154
model name\t: 12th Gen Intel(R) Core(TM) i7-1260P
stepping\t: 4
microcode\t: 0x432
";
let info = super::parse_cpu_info(cpuinfo).unwrap();
assert_eq!(info.vendor, "GenuineIntel");
assert_eq!(info.family, "6");
assert_eq!(info.model, "154");
}

#[test]
fn parse_cpu_info_amd() {
let cpuinfo = "\
processor\t: 0
vendor_id\t: AuthenticAMD
cpu family\t: 25
model\t\t: 80
model name\t: AMD Ryzen 7 5800X 8-Core Processor
stepping\t: 2
";
let info = super::parse_cpu_info(cpuinfo).unwrap();
assert_eq!(info.vendor, "AuthenticAMD");
assert_eq!(info.family, "25");
assert_eq!(info.model, "80");
}

#[test]
fn parse_cpu_info_empty() {
assert!(super::parse_cpu_info("").is_none());
}
}
93 changes: 85 additions & 8 deletions src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,40 @@ pub struct Profile {
pub device_name_pattern: Option<String>,
pub hwd_product_name_pattern: Option<String>,
pub gc_versions: Option<Vec<String>>,
pub cpu_family: Option<String>,
pub cpu_models: Option<Vec<String>>,

pub hwd_ids: Vec<HardwareID>,
}

impl Default for Profile {
fn default() -> Self {
Self::new()
Self {
is_ai_sdk: false,
prof_path: String::new(),
name: String::new(),
desc: String::new(),
priority: 0,
packages: String::new(),
post_install: String::new(),
post_remove: String::new(),
pre_install: String::new(),
pre_remove: String::new(),
conditional_packages: String::new(),
device_name_pattern: None,
hwd_product_name_pattern: None,
gc_versions: None,
cpu_family: None,
cpu_models: None,
hwd_ids: vec![Default::default()],
}
}
}

impl Profile {
#[must_use]
pub fn new() -> Self {
Self { hwd_ids: vec![Default::default()], ..Default::default() }
Self::default()
}
}

Expand Down Expand Up @@ -224,13 +244,18 @@ fn parse_profile(node: &toml::Table, profile_name: &str) -> Result<Profile> {
hwd_product_name_pattern: node
.get("hwd_product_name_pattern")
.and_then(|x| x.as_str().map(str::to_string)),
gc_versions: node.get("gc_versions").and_then(|x| {
x.as_str()
.map(str::split_ascii_whitespace)
.map(|x| x.map(str::to_string).collect::<Vec<_>>())
}),
gc_versions: parse_whitespace_list(node, "gc_versions"),
cpu_family: node.get("cpu_family").and_then(|x| x.as_str().map(str::to_string)),
cpu_models: parse_whitespace_list(node, "cpu_models"),
};
Comment on lines +247 to 250
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpu_family/cpu_models are parsed only from TOML strings (as_str). If a user writes cpu_models = [151, 154] or cpu_family = 6 (valid TOML), the filter will be silently ignored or downgraded (e.g., model restriction dropped), which can cause unintended profile matches. Consider either supporting integer/array forms here or explicitly erroring when the key exists but is not a string, so misconfigurations fail fast.

Copilot uses AI. Check for mistakes.

if profile.cpu_models.is_some() && profile.cpu_family.is_none() {
let msg =
format!("profile '{profile_name}' specifies cpu_models without cpu_family");
eprintln!("Warning: skipping profile '{profile_name}': {msg}");
anyhow::bail!(msg);
}

let conf_devids = node.get("device_ids").and_then(|x| x.as_str()).unwrap_or("");
let conf_vendorids = node.get("vendor_ids").and_then(|x| x.as_str()).unwrap_or("");
let conf_classids = node.get("class_ids").and_then(|x| x.as_str()).unwrap_or("");
Expand Down Expand Up @@ -302,6 +327,10 @@ fn parse_ids_file(file_path: &str) -> Result<String> {
Ok(parsed_ids.split_ascii_whitespace().collect::<Vec<_>>().join(" "))
}

fn parse_whitespace_list(node: &toml::Table, key: &str) -> Option<Vec<String>> {
node.get(key)?.as_str().map(|s| s.split_ascii_whitespace().map(str::to_string).collect())
}

fn merge_table_left(lhs: &mut toml::Table, rhs: &toml::Table) {
for (rhs_key, rhs_val) in rhs {
// rhs key not found in lhs - direct move
Expand Down Expand Up @@ -414,7 +443,13 @@ fn profile_into_toml(profile: &Profile) -> toml::Table {
table.insert("hwd_product_name_pattern".to_owned(), product_name_pattern.clone().into());
}
if let Some(gc_versions) = &profile.gc_versions {
table.insert("gc_versions".to_owned(), gc_versions.clone().into());
table.insert("gc_versions".to_owned(), gc_versions.join(" ").into());
}
if let Some(cpu_family) = &profile.cpu_family {
table.insert("cpu_family".to_owned(), cpu_family.clone().into());
}
if let Some(cpu_models) = &profile.cpu_models {
table.insert("cpu_models".to_owned(), cpu_models.join(" ").into());
}

let last_hwd_id = profile.hwd_ids.last().unwrap();
Expand Down Expand Up @@ -686,4 +721,46 @@ mod tests {

assert_eq!(orig_content, expected_output);
}

#[test]
fn cpu_profile_parse_test() {
let prof_path = "tests/profiles/cpu-profile-test.toml";
let parsed_profiles = parse_profiles(prof_path);
assert!(parsed_profiles.is_ok());
let parsed_profiles = parsed_profiles.unwrap();

assert_eq!(parsed_profiles.len(), 1);
assert_eq!(parsed_profiles[0].name, "intel-lpmd");
assert_eq!(
parsed_profiles[0].desc,
"Intel Low Power Mode Daemon for supported hybrid Intel CPUs"
);
assert_eq!(parsed_profiles[0].priority, 5);
assert_eq!(parsed_profiles[0].packages, "intel-lpmd");
assert_eq!(parsed_profiles[0].cpu_family, Some("6".to_owned()));
assert_eq!(
parsed_profiles[0].cpu_models,
Some(vec![
"151".to_owned(),
"154".to_owned(),
"183".to_owned(),
"186".to_owned(),
"191".to_owned(),
"170".to_owned(),
"172".to_owned(),
"189".to_owned(),
"204".to_owned(),
])
);
assert!(!parsed_profiles[0].post_install.is_empty());
assert!(!parsed_profiles[0].post_remove.is_empty());
}

#[test]
fn cpu_models_without_family_is_rejected() {
let prof_path = "tests/profiles/cpu-models-no-family-test.toml";
let parsed_profiles = parse_profiles(prof_path);
// Should fail because cpu_models is set without cpu_family
assert!(parsed_profiles.is_err() || parsed_profiles.unwrap().is_empty());
Comment on lines +763 to +764
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion allows any error from parse_profiles (e.g., file missing / unreadable) to satisfy the test, which can mask regressions. Since parse_profiles generally returns Ok(vec![]) for invalid profiles, consider asserting parsed_profiles.is_ok() and unwrap().is_empty(), and/or using get_invalid_profiles to assert bad-profile is reported as invalid.

Suggested change
// Should fail because cpu_models is set without cpu_family
assert!(parsed_profiles.is_err() || parsed_profiles.unwrap().is_empty());
// Should be rejected: no valid profiles must be returned
assert!(parsed_profiles.is_ok());
let parsed_profiles = parsed_profiles.unwrap();
assert!(parsed_profiles.is_empty());

Copilot uses AI. Check for mistakes.
}
}
8 changes: 8 additions & 0 deletions tests/profiles/cpu-models-no-family-test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[bad-profile]
desc = 'Invalid profile with cpu_models but no cpu_family'
class_ids = "0600"
vendor_ids = "8086"
device_ids = "*"
cpu_models = "151 154 183"
priority = 5
packages = 'test-pkg'
15 changes: 15 additions & 0 deletions tests/profiles/cpu-profile-test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[intel-lpmd]
desc = 'Intel Low Power Mode Daemon for supported hybrid Intel CPUs'
class_ids = "0600"
vendor_ids = "8086"
device_ids = "*"
cpu_family = "6"
cpu_models = "151 154 183 186 191 170 172 189 204"
priority = 5
packages = 'intel-lpmd'
post_install = """
systemctl enable --now intel_lpmd.service
"""
post_remove = """
systemctl disable intel_lpmd.service
"""
Loading