Skip to content
Merged
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
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(())
}
9 changes: 9 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,14 @@ pub struct OsGenerationsArgs {
/// Path to Nix' profiles directory
#[arg(long, short = 'P', default_value = "/nix/var/nix/profiles/system")]
pub profile: Option<String>,

/// Comma-delimited list of field(s) to display
#[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