150 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Rust
		
	
	
	
			
		
		
	
	
			150 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Rust
		
	
	
	
| use std::fmt;
 | |
| use std::fs;
 | |
| use std::os::unix::fs::MetadataExt;
 | |
| use std::path::{Path, PathBuf};
 | |
| 
 | |
| use anyhow::{anyhow, Context, Result};
 | |
| use bootspec::generation::Generation as BootspecGeneration;
 | |
| use bootspec::BootJson;
 | |
| use bootspec::SpecialisationName;
 | |
| use time::Date;
 | |
| 
 | |
| /// (Possibly) extended Bootspec.
 | |
| ///
 | |
| /// This struct currently does not have any extensions. We keep it around so that extension becomes
 | |
| /// easy if/when we have to do it.
 | |
| #[derive(Debug, Clone)]
 | |
| pub struct ExtendedBootJson {
 | |
|     pub bootspec: BootJson,
 | |
| }
 | |
| 
 | |
| /// A system configuration.
 | |
| ///
 | |
| /// Can be built from a GenerationLink.
 | |
| ///
 | |
| /// NixOS represents a generation as a symlink to a toplevel derivation. This toplevel derivation
 | |
| /// contains most of the information necessary to install the generation onto the EFI System
 | |
| /// Partition. The only information missing is the version number which is encoded in the file name
 | |
| /// of the generation link.
 | |
| #[derive(Debug)]
 | |
| pub struct Generation {
 | |
|     /// Profile symlink index
 | |
|     version: u64,
 | |
|     /// Build time
 | |
|     build_time: Option<Date>,
 | |
|     /// Top-level specialisation name
 | |
|     specialisation_name: Option<SpecialisationName>,
 | |
|     /// Top-level extended boot specification
 | |
|     pub spec: ExtendedBootJson,
 | |
| }
 | |
| 
 | |
| impl Generation {
 | |
|     pub fn from_link(link: &GenerationLink) -> Result<Self> {
 | |
|         let bootspec_path = link.path.join("boot.json");
 | |
|         let generation: BootspecGeneration = serde_json::from_slice(
 | |
|             &fs::read(bootspec_path).context("Failed to read bootspec file")?,
 | |
|         )
 | |
|         .context("Failed to parse bootspec json")?;
 | |
| 
 | |
|         let bootspec: BootJson = generation
 | |
|             .try_into()
 | |
|             .map_err(|err: &'static str| anyhow!(err))?;
 | |
| 
 | |
|         Ok(Self {
 | |
|             version: link.version,
 | |
|             build_time: link.build_time,
 | |
|             specialisation_name: None,
 | |
|             spec: ExtendedBootJson { bootspec },
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     pub fn specialise(&self, name: &SpecialisationName, bootspec: &BootJson) -> Result<Self> {
 | |
|         Ok(Self {
 | |
|             version: self.version,
 | |
|             build_time: self.build_time,
 | |
|             specialisation_name: Some(name.clone()),
 | |
|             spec: ExtendedBootJson {
 | |
|                 bootspec: bootspec.clone(),
 | |
|             },
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     pub fn is_specialised(&self) -> Option<SpecialisationName> {
 | |
|         self.specialisation_name.clone()
 | |
|     }
 | |
| 
 | |
|     /// Describe the generation in a single line.
 | |
|     ///
 | |
|     /// Emulates how NixOS's current systemd-boot-builder.py describes generations so that the user
 | |
|     /// interface remains similar.
 | |
|     ///
 | |
|     /// This is currently implemented by poking around the filesystem to find the necessary data.
 | |
|     /// Ideally, the needed data should be included in the bootspec.
 | |
|     pub fn describe(&self) -> String {
 | |
|         let build_time = self
 | |
|             .build_time
 | |
|             .map(|x| x.to_string())
 | |
|             .unwrap_or_else(|| String::from("Unknown"));
 | |
|         format!("Generation {}, Built on {}", self.version, build_time)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl fmt::Display for Generation {
 | |
|     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | |
|         write!(f, "{}", self.version)
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn read_build_time(path: &Path) -> Result<Date> {
 | |
|     let build_time = time::OffsetDateTime::from_unix_timestamp(fs::metadata(path)?.mtime())?.date();
 | |
|     Ok(build_time)
 | |
| }
 | |
| 
 | |
| /// A link pointing to a generation.
 | |
| ///
 | |
| /// Can be built from a symlink in /nix/var/nix/profiles/ alone because the name of the
 | |
| /// symlink encodes the version number.
 | |
| #[derive(Debug)]
 | |
| pub struct GenerationLink {
 | |
|     pub version: u64,
 | |
|     pub path: PathBuf,
 | |
|     pub build_time: Option<Date>,
 | |
| }
 | |
| 
 | |
| impl GenerationLink {
 | |
|     pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
 | |
|         Ok(Self {
 | |
|             version: parse_version(&path).context("Failed to parse version")?,
 | |
|             path: PathBuf::from(path.as_ref()),
 | |
|             build_time: read_build_time(path.as_ref()).ok(),
 | |
|         })
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Parse version number from a path.
 | |
| ///
 | |
| /// Expects a path in the format of "system-{version}-link".
 | |
| fn parse_version(path: impl AsRef<Path>) -> Result<u64> {
 | |
|     let generation_version = path
 | |
|         .as_ref()
 | |
|         .file_name()
 | |
|         .and_then(|x| x.to_str())
 | |
|         .and_then(|x| x.split('-').nth(1))
 | |
|         .and_then(|x| x.parse::<u64>().ok())
 | |
|         .with_context(|| format!("Failed to extract version from: {:?}", path.as_ref()))?;
 | |
| 
 | |
|     Ok(generation_version)
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod tests {
 | |
|     use super::*;
 | |
| 
 | |
|     #[test]
 | |
|     fn parse_version_correctly() {
 | |
|         let path = Path::new("system-2-link");
 | |
|         let parsed_version = parse_version(path).unwrap();
 | |
|         assert_eq!(parsed_version, 2,);
 | |
|     }
 | |
| }
 |