Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ issue is not necessary, but good to have. Otherwise, general-purpose changes can
be put in the "Changed" section or, if it's just to remove code or
functionality, under the "Removed" section.
-->
## Unreleased

### Changed

- `nh os info` now support `--fields` to select which field(s) to display;
also add a per-generation "Closure Size" coloumn.
([#375](https://github.com/nix-community/nh/issues/375))

## 4.2.0

### Changed
Expand Down
197 changes: 139 additions & 58 deletions src/generations.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{collections::HashMap, fs, path::Path, process};

use chrono::{DateTime, Local, TimeZone, Utc};
use clap::ValueEnum;
use color_eyre::eyre::{Result, bail};
use tracing::debug;

Expand All @@ -27,8 +28,60 @@ pub struct GenerationInfo {

/// Whether a given generation is the current one.
pub current: bool,

/// Closure size of the generation.
pub closure_size: String,
}

#[derive(ValueEnum, Clone, Debug)]
pub enum Field {
/// Generation Id
Id,

/// Build Date
Date,

/// Nixos Version
Nver,

/// Kernel Version
Kernel,

/// Configuration Revision
#[clap(name = "confRev")]
Confrev,

/// Specialisations
Spec,

/// Closure Size
Size,
}

#[derive(Clone, Copy)]
struct ColumnWidths {
id: usize,
date: usize,
nver: usize,
kernel: usize,
confrev: usize,
spec: usize,
size: usize,
}

impl Field {
fn column_info(&self, width: ColumnWidths) -> (&'static str, usize) {
match self {
Field::Id => ("Generation No", width.id),
Field::Date => ("Build Date", width.date),
Field::Nver => ("NixOS Version", width.nver),
Field::Kernel => ("Kernel", width.kernel),
Field::Confrev => ("Configuration Revision", width.confrev),
Field::Spec => ("Specialisations", width.spec),
Field::Size => ("Closure Size", width.size),
}
}
}
#[must_use]
pub fn from_dir(generation_dir: &Path) -> Option<u64> {
generation_dir
Expand All @@ -42,9 +95,43 @@ pub fn from_dir(generation_dir: &Path) -> Option<u64> {
})
}

#[must_use]
pub fn get_closure_size(generation_dir: &Path) -> Option<String> {
let store_path = generation_dir
.read_link()
.unwrap_or_else(|_| generation_dir.to_path_buf());
match process::Command::new("nix")
.arg("path-info")
.arg(generation_dir)
.arg("-Sh")
.arg("--json")
.output()
{
Ok(output) => {
match serde_json::from_str::<serde_json::Value>(&String::from_utf8_lossy(
&output.stdout,
)) {
#[allow(clippy::cast_precision_loss)]
Ok(json) => {
Some(
json[store_path.to_string_lossy().to_string()]["closureSize"]
.as_u64()
.map_or_else(
|| "Unknown".to_string(),
|bytes| format!("{:.1} GB", bytes as f64 / 1_073_741_824.0),
),
)
},
Err(_) => Some("Unknown".to_string()),
}
},
Err(_) => Some("Unknown".to_string()),
}
}

pub fn describe(generation_dir: &Path) -> Option<GenerationInfo> {
let generation_number = from_dir(generation_dir)?;

let closure_size = get_closure_size(generation_dir)?;
// Get metadata once and reuse for both date and existence checks
let metadata = fs::metadata(generation_dir).ok()?;
let build_date = metadata
Expand Down Expand Up @@ -155,6 +242,7 @@ pub fn describe(generation_dir: &Path) -> Option<GenerationInfo> {
configuration_revision,
specialisations,
current: false,
closure_size,
});
};

Expand All @@ -170,6 +258,7 @@ pub fn describe(generation_dir: &Path) -> Option<GenerationInfo> {
configuration_revision,
specialisations,
current: false,
closure_size,
});
};

Expand All @@ -183,6 +272,7 @@ pub fn describe(generation_dir: &Path) -> Option<GenerationInfo> {
configuration_revision,
specialisations,
current,
closure_size,
})
}

Expand All @@ -191,35 +281,10 @@ pub fn describe(generation_dir: &Path) -> Option<GenerationInfo> {
/// # Errors
///
/// Returns an error if output or formatting fails.
pub fn print_info(mut generations: Vec<GenerationInfo>) -> Result<()> {
// Get path information for the current generation from /run/current-system
// By using `--json` we can avoid splitting whitespaces to get the correct
// closure size, which has created issues in the past.
let closure = match process::Command::new("nix")
.arg("path-info")
.arg("/run/current-system")
.arg("-Sh")
.arg("--json")
.output()
{
Ok(output) => {
debug!("Got the following output for nix path-info: {:#?}", &output);
match serde_json::from_str::<serde_json::Value>(&String::from_utf8_lossy(
&output.stdout,
)) {
#[allow(clippy::cast_precision_loss)]
Ok(json) => {
json[0]["closureSize"].as_u64().map_or_else(
|| "Unknown".to_string(),
|bytes| format!("{:.1} GB", bytes as f64 / 1_073_741_824.0),
)
},
Err(_) => "Unknown".to_string(),
}
},
Err(_) => "Unknown".to_string(),
};

pub fn print_info(
mut generations: Vec<GenerationInfo>,
fields: &[Field],
) -> Result<()> {
// Parse all dates at once and cache them
let mut parsed_dates = HashMap::with_capacity(generations.len());
for generation in &generations {
Expand Down Expand Up @@ -247,9 +312,6 @@ pub fn print_info(mut generations: Vec<GenerationInfo>) -> Result<()> {
bail!("Error getting current generation!");
}

println!("Closure Size: {closure}");
println!();

// Determine column widths for pretty printing
let max_nixos_version_len = generations
.iter()
Expand All @@ -263,16 +325,25 @@ pub fn print_info(mut generations: Vec<GenerationInfo>) -> Result<()> {
.max()
.unwrap_or(12); // arbitrary value

println!(
"{:<13} {:<20} {:<width_nixos$} {:<width_kernel$} {:<22} Specialisations",
"Generation No",
"Build Date",
"NixOS Version",
"Kernel",
"Configuration Revision",
width_nixos = max_nixos_version_len,
width_kernel = max_kernel_len
);
let widths = ColumnWidths {
id: 13, // "Generation No"
date: 20, // "Build Date"
nver: max_nixos_version_len,
kernel: max_kernel_len,
confrev: 22, // "Configuration Revision"
spec: 15, // "Specialisations"
size: 12, // "Closure Size"
};

let header = fields
.iter()
.map(|f| {
let (name, width) = f.column_info(widths);
format!("{:<width$}", name)
})
.collect::<Vec<String>>()
.join(" ");
println!("{}", header);

// Print generations in descending order
for generation in generations.iter().rev() {
Expand All @@ -292,21 +363,31 @@ pub fn print_info(mut generations: Vec<GenerationInfo>) -> Result<()> {
.join(" ")
};

println!(
"{:<13} {:<20} {:<width_nixos$} {:<width_kernel$} {:<25} {}",
format!(
"{}{}",
generation.number,
if generation.current { " (current)" } else { "" }
),
formatted_date,
generation.nixos_version,
generation.kernel_version,
generation.configuration_revision,
specialisations,
width_nixos = max_nixos_version_len,
width_kernel = max_kernel_len
);
let row: String = fields
.iter()
.map(|f| {
let (_, width) = f.column_info(widths);
let cell_content = match f {
Field::Id => {
format!(
"{}{}",
generation.number,
if generation.current { " (current)" } else { "" }
)
},
Field::Date => formatted_date.clone(),
Field::Nver => generation.nixos_version.clone(),
Field::Kernel => generation.kernel_version.clone(),
Field::Confrev => generation.configuration_revision.clone(),
Field::Spec => specialisations.clone(),
Field::Size => generation.closure_size.clone(),
};
format!("{:width$}", cell_content)
})
.collect::<Vec<String>>()
.join(" ");
println!("{}", row);
}

Ok(())
}
10 changes: 10 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
OsReplFeatures,
},
commands::ElevationStrategy,
generations::Field,
installable::Installable,
};

Expand Down Expand Up @@ -345,6 +346,15 @@ pub struct OsGenerationsArgs {
/// Path to Nix' profiles directory
#[arg(long, short = 'P', default_value = "/nix/var/nix/profiles/system")]
pub profile: Option<String>,

/// Specify which field(s) to show (comma-delimited).Defaults to show all
/// fields.
#[arg(
long,
value_delimiter = ',',
default_value = "id,date,nver,kernel,confRev,spec,size"
)]
pub fields: Vec<Field>,
}

#[derive(Args, Debug)]
Expand Down
2 changes: 1 addition & 1 deletion src/nixos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ impl OsGenerationsArgs {
.filter_map(|gen_dir| generations::describe(gen_dir))
.collect();

let _ = generations::print_info(descriptions);
generations::print_info(descriptions, &self.fields)?;

Ok(())
}
Expand Down