aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-08-29 08:50:05 -0700
committerFranck Cuny <franck@fcuny.net>2025-08-29 08:50:05 -0700
commit67b1409f03cdb4435021c3e15daa4cb4affe452f (patch)
treed71c6f767ff1181a8de6766b77b1af7fe9e2a47a /src
parentadd `git-leaderboard` (diff)
parentMerge pull request #3 from fcuny/dependabot/cargo/thiserror-2.0.11 (diff)
downloadx-67b1409f03cdb4435021c3e15daa4cb4affe452f.tar.gz
Merge remote-tracking branch 'import/main' into fcuny/rust
Diffstat (limited to 'src')
-rw-r--r--src/apple_silicon/.editorconfig23
-rw-r--r--src/apple_silicon/.github/dependabot.yml10
-rw-r--r--src/apple_silicon/.github/workflows/ci.yml35
-rw-r--r--src/apple_silicon/.gitignore1
-rw-r--r--src/apple_silicon/Cargo.lock65
-rw-r--r--src/apple_silicon/Cargo.toml18
-rw-r--r--src/apple_silicon/README.md1
-rw-r--r--src/apple_silicon/src/bin/apple_silicon.rs12
-rw-r--r--src/apple_silicon/src/error.rs23
-rw-r--r--src/apple_silicon/src/lib.rs2
-rw-r--r--src/apple_silicon/src/soc.rs302
11 files changed, 492 insertions, 0 deletions
diff --git a/src/apple_silicon/.editorconfig b/src/apple_silicon/.editorconfig
new file mode 100644
index 0000000..9d9ad7a
--- /dev/null
+++ b/src/apple_silicon/.editorconfig
@@ -0,0 +1,23 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+
+[*.rs]
+indent_size = 4
+
+[*.toml]
+indent_size = 2
+
+[*.{diff,patch}]
+end_of_line = unset
+insert_final_newline = unset
+trim_trailing_whitespace = unset
diff --git a/src/apple_silicon/.github/dependabot.yml b/src/apple_silicon/.github/dependabot.yml
new file mode 100644
index 0000000..d062b44
--- /dev/null
+++ b/src/apple_silicon/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "cargo"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/src/apple_silicon/.github/workflows/ci.yml b/src/apple_silicon/.github/workflows/ci.yml
new file mode 100644
index 0000000..59ce8e7
--- /dev/null
+++ b/src/apple_silicon/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: 🦀 Rust CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ build:
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - uses: Swatinem/rust-cache@v2
+
+ - name: Check formatting
+ run: cargo fmt --all -- --check
+
+ - name: Run clippy
+ run: cargo clippy -- -D warnings
+
+ - name: Run tests
+ run: cargo test --verbose
+
+ - name: Build
+ run: cargo build --verbose
diff --git a/src/apple_silicon/.gitignore b/src/apple_silicon/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/src/apple_silicon/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/src/apple_silicon/Cargo.lock b/src/apple_silicon/Cargo.lock
new file mode 100644
index 0000000..1ee0f1d
--- /dev/null
+++ b/src/apple_silicon/Cargo.lock
@@ -0,0 +1,65 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "apple_silicon"
+version = "0.1.0"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
diff --git a/src/apple_silicon/Cargo.toml b/src/apple_silicon/Cargo.toml
new file mode 100644
index 0000000..65dcf5e
--- /dev/null
+++ b/src/apple_silicon/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "apple_silicon"
+version = "0.1.0"
+edition = "2021"
+description = "A common line tool for Apple Silicon"
+
+license = "MIT"
+authors = ["Franck Cuny <franck@fcuny.net>"]
+repository = "https://github.com/fcuny/apple_silicon"
+homepage = "https://github.com/fcuny/apple_silicon"
+categories = ["command-line-utilities"]
+
+[[bin]]
+name = "apple_silicon"
+path = "src/bin/apple_silicon.rs"
+
+[dependencies]
+thiserror = "2.0.11"
diff --git a/src/apple_silicon/README.md b/src/apple_silicon/README.md
new file mode 100644
index 0000000..7d83327
--- /dev/null
+++ b/src/apple_silicon/README.md
@@ -0,0 +1 @@
+A command line utility to report information about Apple Silicon (inspired by [`asitop`](https://github.com/tlkh/asitop/tree/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd)).
diff --git a/src/apple_silicon/src/bin/apple_silicon.rs b/src/apple_silicon/src/bin/apple_silicon.rs
new file mode 100644
index 0000000..0f71eeb
--- /dev/null
+++ b/src/apple_silicon/src/bin/apple_silicon.rs
@@ -0,0 +1,12 @@
+use apple_silicon::soc::SocInfo;
+
+fn main() {
+ let cpu_info = SocInfo::new().unwrap();
+ println!(
+ "our CPU is an {}, and we have {} CPU cores, and {} GPU cores. The TDP is {}.",
+ cpu_info.cpu_brand_name,
+ cpu_info.num_cpu_cores,
+ cpu_info.num_gpu_cores,
+ cpu_info.cpu_max_power.unwrap(),
+ );
+}
diff --git a/src/apple_silicon/src/error.rs b/src/apple_silicon/src/error.rs
new file mode 100644
index 0000000..a70b9b5
--- /dev/null
+++ b/src/apple_silicon/src/error.rs
@@ -0,0 +1,23 @@
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("socinfo parsing error: `{0}`")]
+ Parse(String),
+
+ #[error("I/O error: `{source}`")]
+ Io {
+ #[from]
+ source: std::io::Error,
+ },
+
+ #[error("utf8 conversion error: `{source}`")]
+ Utf8Conversion {
+ #[from]
+ source: std::string::FromUtf8Error,
+ },
+
+ #[error("integer parsing error: `{source}`")]
+ ParseInt {
+ #[from]
+ source: std::num::ParseIntError,
+ },
+}
diff --git a/src/apple_silicon/src/lib.rs b/src/apple_silicon/src/lib.rs
new file mode 100644
index 0000000..74bd62c
--- /dev/null
+++ b/src/apple_silicon/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod error;
+pub mod soc;
diff --git a/src/apple_silicon/src/soc.rs b/src/apple_silicon/src/soc.rs
new file mode 100644
index 0000000..0e2bf5c
--- /dev/null
+++ b/src/apple_silicon/src/soc.rs
@@ -0,0 +1,302 @@
+use crate::error::Error;
+
+use std::process::{Command, Output};
+
+pub type Result<T> = std::result::Result<T, Error>;
+pub type Watts = u32;
+pub type Bandwidth = u32;
+pub type CoreCount = u16;
+
+/// Information about the Silicon chip
+pub struct SocInfo {
+ /// The CPU brand name string
+ pub cpu_brand_name: String,
+ /// Number of CPU cores
+ pub num_cpu_cores: CoreCount,
+ /// Number of GPU cores
+ pub num_gpu_cores: CoreCount,
+ /// Maximum CPU power in watts (if available)
+ pub cpu_max_power: Option<Watts>,
+ /// Maximum GPU power in watts (if available)
+ pub gpu_max_power: Option<Watts>,
+ /// Maximum CPU bandwidth in GB/s (if available)
+ pub cpu_max_bw: Option<Bandwidth>,
+ /// Maximum GPU bandwidth in GB/s (if available)
+ pub gpu_max_bw: Option<Bandwidth>,
+ /// Number of efficiency cores
+ pub e_core_count: CoreCount,
+ /// Number of performance cores
+ pub p_core_count: CoreCount,
+}
+
+#[derive(Debug, PartialEq)]
+enum AppleChip {
+ M1,
+ M1Pro,
+ M1Max,
+ M1Ultra,
+ M2,
+ M2Pro,
+ M2Max,
+ M2Ultra,
+ M3,
+ M3Pro,
+ M3Max,
+ Unknown,
+}
+
+struct ChipSpecs {
+ cpu_tdp: Watts,
+ gpu_tdp: Watts,
+ cpu_bw: Bandwidth,
+ gpu_bw: Bandwidth,
+}
+
+impl AppleChip {
+ fn from_brand_string(brand: &str) -> Self {
+ match brand {
+ s if s.contains("M1 Pro") => AppleChip::M1Pro,
+ s if s.contains("M1 Max") => AppleChip::M1Max,
+ s if s.contains("M1 Ultra") => AppleChip::M1Ultra,
+ s if s.contains("M1") => AppleChip::M1,
+ s if s.contains("M2 Pro") => AppleChip::M2Pro,
+ s if s.contains("M2 Max") => AppleChip::M2Max,
+ s if s.contains("M2 Ultra") => AppleChip::M2Ultra,
+ s if s.contains("M2") => AppleChip::M2,
+ s if s.contains("M3 Pro") => AppleChip::M3Pro,
+ s if s.contains("M3 Max") => AppleChip::M3Max,
+ s if s.contains("M3") => AppleChip::M3,
+ _ => AppleChip::Unknown,
+ }
+ }
+
+ fn get_specs(&self) -> ChipSpecs {
+ match self {
+ AppleChip::M1 => ChipSpecs {
+ cpu_tdp: 20,
+ gpu_tdp: 20,
+ cpu_bw: 70,
+ gpu_bw: 70,
+ },
+ AppleChip::M1Pro => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 30,
+ cpu_bw: 200,
+ gpu_bw: 200,
+ },
+ AppleChip::M1Max => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 60,
+ cpu_bw: 250,
+ gpu_bw: 400,
+ },
+ AppleChip::M1Ultra => ChipSpecs {
+ cpu_tdp: 60,
+ gpu_tdp: 120,
+ cpu_bw: 500,
+ gpu_bw: 800,
+ },
+ AppleChip::M2 => ChipSpecs {
+ cpu_tdp: 25,
+ gpu_tdp: 15,
+ cpu_bw: 100,
+ gpu_bw: 100,
+ },
+ AppleChip::M2Pro => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 35,
+ cpu_bw: 0,
+ gpu_bw: 0,
+ },
+ AppleChip::M2Max => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 40,
+ cpu_bw: 0,
+ gpu_bw: 0,
+ },
+ // Add more variants as needed
+ _ => ChipSpecs {
+ cpu_tdp: 0,
+ gpu_tdp: 0,
+ cpu_bw: 0,
+ gpu_bw: 0,
+ },
+ }
+ }
+}
+
+impl SocInfo {
+ pub fn new() -> Result<SocInfo> {
+ let (cpu_brand_name, num_cpu_cores, e_core_count, p_core_count) = cpu_info(&RealCommand)?;
+ let num_gpu_cores = gpu_info(&RealCommand)?;
+
+ let chip = AppleChip::from_brand_string(&cpu_brand_name);
+ let specs = chip.get_specs();
+
+ Ok(SocInfo {
+ cpu_brand_name,
+ num_cpu_cores,
+ num_gpu_cores,
+ cpu_max_power: Some(specs.cpu_tdp),
+ gpu_max_power: Some(specs.gpu_tdp),
+ cpu_max_bw: Some(specs.cpu_bw),
+ gpu_max_bw: Some(specs.gpu_bw),
+ e_core_count,
+ p_core_count,
+ })
+ }
+}
+
+// https://github.com/tlkh/asitop/blob/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd/asitop/utils.py#L94
+const SYSCTL_PATH: &str = "/usr/sbin/sysctl";
+
+fn cpu_info(cmd: &impl SystemCommand) -> Result<(String, u16, u16, u16)> {
+ let binary = SYSCTL_PATH;
+ let args = &[
+ // don't display the variable name
+ "-n",
+ "machdep.cpu.brand_string",
+ "machdep.cpu.core_count",
+ "hw.perflevel0.logicalcpu",
+ "hw.perflevel1.logicalcpu",
+ ];
+
+ let output = cmd.execute(binary, args)?;
+ let buffer = String::from_utf8(output.stdout)?;
+
+ let mut iter = buffer.split('\n');
+ let cpu_brand_name = match iter.next() {
+ Some(s) => s.to_string(),
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ let num_cpu_cores = match iter.next() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ let num_performance_cores = match iter.next() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ let num_efficiency_cores = match iter.next() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ Ok((
+ cpu_brand_name,
+ num_cpu_cores,
+ num_performance_cores,
+ num_efficiency_cores,
+ ))
+}
+
+// https://github.com/tlkh/asitop/blob/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd/asitop/utils.py#L120
+fn gpu_info(cmd: &impl SystemCommand) -> Result<u16> {
+ let binary = "/usr/sbin/system_profiler";
+ let args = &["-detailLevel", "basic", "SPDisplaysDataType"];
+
+ let output = cmd.execute(binary, args)?;
+ let buffer = String::from_utf8(output.stdout)?;
+
+ let num_gpu_cores_line = buffer
+ .lines()
+ .find(|&line| line.trim_start().starts_with("Total Number of Cores"));
+
+ let num_gpu_cores = match num_gpu_cores_line {
+ Some(s) => match s.split(": ").last() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ },
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ Ok(num_gpu_cores)
+}
+
+/// Trait for system command execution
+pub trait SystemCommand {
+ fn execute(&self, binary: &str, args: &[&str]) -> Result<Output>;
+}
+
+/// Real command executor
+pub struct RealCommand;
+
+impl SystemCommand for RealCommand {
+ fn execute(&self, binary: &str, args: &[&str]) -> Result<Output> {
+ Ok(Command::new(binary).args(args).output()?)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::os::unix::process::ExitStatusExt;
+
+ struct MockCommand {
+ output: Vec<u8>,
+ }
+
+ impl MockCommand {
+ fn new(output: &str) -> Self {
+ Self {
+ output: output.as_bytes().to_vec(),
+ }
+ }
+ }
+
+ impl SystemCommand for MockCommand {
+ fn execute(&self, _binary: &str, _args: &[&str]) -> Result<Output> {
+ Ok(Output {
+ status: std::process::ExitStatus::from_raw(0),
+ stdout: self.output.clone(),
+ stderr: Vec::new(),
+ })
+ }
+ }
+
+ #[test]
+ fn test_gpu_info() {
+ let mock_output = r#"Graphics/Displays:
+ Apple M2:
+ Total Number of Cores: 10"#;
+ let cmd = MockCommand::new(mock_output);
+
+ let result = gpu_info(&cmd);
+ assert_eq!(result.unwrap(), 10);
+ }
+
+ #[test]
+ fn test_cpu_info_success() {
+ let mock_output = "Apple M2\n8\n4\n4\n";
+ let cmd = MockCommand::new(mock_output);
+
+ let result = cpu_info(&cmd);
+ assert!(result.is_ok());
+ let (brand, cores, p_cores, e_cores) = result.unwrap();
+ assert_eq!(brand, "Apple M2");
+ assert_eq!(cores, 8);
+ assert_eq!(p_cores, 4);
+ assert_eq!(e_cores, 4);
+ }
+
+ #[test]
+ fn test_cpu_info_missing_core_count() {
+ let mock_output = "Apple M2\n";
+ let cmd = MockCommand::new(mock_output);
+
+ let result = cpu_info(&cmd);
+ assert!(matches!(result, Err(Error::ParseInt { .. })));
+ }
+
+ #[test]
+ fn test_cpu_info_invalid_core_count() {
+ let mock_output = "Apple M2\ninvalid\n";
+ let cmd = MockCommand::new(mock_output);
+
+ let result = cpu_info(&cmd);
+ assert!(matches!(result, Err(Error::ParseInt { .. })));
+ }
+}