Skip to content

Commit

Permalink
feat: Dump readymade install state to a file in target root
Browse files Browse the repository at this point in the history
  • Loading branch information
korewaChino committed Feb 13, 2025
1 parent b7956de commit 6da4f17
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 13 deletions.
11 changes: 6 additions & 5 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ The UI code is written in [Relm](https://relm4.org/), a GTK4 UI framework for Ru

The new dedicated backend for Readymade should be written in Rust, and should be able to handle the following tasks:

- Declarative(?) disk partitioning and generation of actions to be fed to UDisks2
- UDisks2 integration for disk partitioning and formatting (and possibly LVM and BTRFS support)
- ~~Declarative(?) disk partitioning and generation of actions to be fed to UDisks2~~ (This is now handled by systemd-repart)
- ~~UDisks2 integration for disk partitioning and formatting (and possibly LVM and BTRFS support)~~ (This is now handled by systemd-repart)
- Smart detection for Chromebook devices and other devices that require special handling (so that we can install extra Submarine bootloader payloads when required)
- Automatic systemd mountpoint hints using GPT partition labels/flags
- Automatic systemd mountpoint hints using GPT partition labels/flags (Handled by systemd-repart, using DDI hints)

### If you're gonna do this in Rust, why not just use [distinst](https://github.com/pop-os/distinst)?

Expand Down Expand Up @@ -85,8 +85,6 @@ It also logs to the systemd journal, so you can view the logs by running
journalctl _COMM=readymade # add -f to follow the logs
```

Currently Readymade only supports Chromebook installations, it is recommended you run Readymade on a Chromebook device to test the installer.

Readymade checks for Dracut's default `live-base` (in `/dev/mapper/live-base`) logical volume for the base filesystem to mount and copy from. This is usually generated with Dracut's live module. It then tries to mount the base filesystem from the logical volume and use the files from there as the source for the installer, **_as it assumes the running environment is a live CD environment generated by Dracut, thus it contains the original overlay filesystem in this exact location_**.

While you may expect it to mount a SquashFS, the default behaviour is to mount an overlay disk image generated _from_ the SquashFS. This is to prevent the SquashFS to be extracted twice, as the live module already mounts the SquashFS and turns it into a Device Mapper device.
Expand All @@ -106,6 +104,9 @@ sudo REPART_COPY_SOURCE=/mnt/rootfs readymade
> [!NOTE]
> If Readymade is built as a debug build, it will dump the installation state and the systemd-repart output to `/tmp/` for debugging purposes.
Readymade also dumps a redacted version of the installation state in `/var/lib/readymade/state.json` for other tools to use, such as system
recovery tools.

## Localization

You can translate Readymade to your language by going to the [Fyra Labs Weblate](https://weblate.fyralabs.com/projects/tauOS/readymade/) page and translating the strings there.
Expand Down
5 changes: 3 additions & 2 deletions src/backend/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use std::path::PathBuf;

use color_eyre::eyre::Context;

use super::export::ReadymadeResult;
use super::install::InstallationState;

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -169,7 +169,8 @@ pub fn install_custom(
.ok_or_else(|| color_eyre::eyre::eyre!("cannot find xbootldr partition"))?;

// TODO: encryption support for custom
container.run(|| state._inner_sys_setup(fstab, None, efi, &xbootldr))??;
let rdm_result = ReadymadeResult::new(state.clone(), None);
container.run(|| state._inner_sys_setup(fstab, None, efi, &xbootldr, rdm_result))??;

Ok(())
}
86 changes: 86 additions & 0 deletions src/backend/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Module for exporting Readymade's install state to a file, Useful for other tools to check
//! the initial state of the system. Not so useful when the user modifies the system after and
//! the state drifts from the initial state. (i.e the user repartitions the disk, adds a new disk,
//! spans the BTRFS volume, etc.)
//!
use std::{collections::BTreeMap, path::Path};

use color_eyre::Result;
use serde::{Deserialize, Serialize};

use super::{install::InstallationState, repartcfg::RepartConfig};
/// The version of the result dump format, for backwards compat reasons
///
/// If there's any changes to the format, this should be bumped up to the next version.
///
const RESULT_DUMP_FORMAT_VERSION: &str = "0.1.0";
#[derive(Serialize, Deserialize, Debug)]
pub struct ReadymadeResult {
pub version: &'static str,
pub readymade_version: &'static str,
pub is_debug_build: bool,
pub state: InstallationState,
pub systemd_repart_data: Option<SystemdRepartData>,
}

impl ReadymadeResult {
pub fn export_string(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(&self)?)
}

pub fn new(state: InstallationState, systemd_repart_data: Option<SystemdRepartData>) -> Self {
Self {
version: RESULT_DUMP_FORMAT_VERSION,
readymade_version: env!("CARGO_PKG_VERSION"),
is_debug_build: cfg!(debug_assertions),
state: prep_state_for_export(state).unwrap(),
systemd_repart_data,
}
}

}

#[derive(Serialize, Deserialize, Debug)]
pub struct SystemdRepartData {
configs: BTreeMap<String, RepartConfig>,
}

impl SystemdRepartData {
pub fn new(configs: BTreeMap<String, RepartConfig>) -> Self {
Self { configs }
}

pub fn get_configs(cfg_path: &Path) -> Result<Self> {
let mut configs = BTreeMap::new();
// Read path
for entry in std::fs::read_dir(&cfg_path)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let file_config = std::fs::read_to_string(&path)?;

// Parse the config
let config: RepartConfig = serde_systemd_unit::from_str(&file_config)?;

// Add to the list
configs.insert(
path.file_name().unwrap().to_string_lossy().to_string(),
config,
);
}
Ok(Self::new(configs))
}
}

pub fn prep_state_for_export(state: InstallationState) -> Result<InstallationState> {
let mut new_state = state.clone();

// Clear out passwords
if let Some(ref mut enc_key) = new_state.encryption_key {
enc_key.clear();
new_state.encryption_key = Some("REDACTED".to_string());
}
Ok(new_state)
}
64 changes: 58 additions & 6 deletions src/backend/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub enum InstallationType {
Custom,
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InstallationState {
pub langlocale: Option<String>,
pub destination_disk: Option<DiskInit>,
Expand Down Expand Up @@ -236,13 +236,15 @@ impl InstallationState {
// todo: not freeze on error, show error message as err handler?
Self::systemd_repart(blockdev, &cfgdir, self.encrypt && self.encryption_key.is_some())?
});

let repartcfg_export = super::export::SystemdRepartData::get_configs(&cfgdir)?;

tracing::info!("Copying files done, Setting up system...");
self.setup_system(repart_out, self.encryption_key.as_ref().map(|s| s.as_str()))?;
self.setup_system(repart_out, self.encryption_key.as_ref().map(|s| s.as_str()), Some(repartcfg_export))?;

if let InstallationType::ChromebookInstall = inst_type {
// FIXME: don't dd?
Self::dd_submarine(blockdev)?;
Self::flash_submarine(blockdev)?;
InstallationType::set_cgpt_flags(blockdev)?;
}

Expand All @@ -267,7 +269,7 @@ impl InstallationState {
}

#[tracing::instrument]
fn setup_system(&self, output: RepartOutput, passphrase: Option<&str>) -> Result<()> {
fn setup_system(&self, output: RepartOutput, passphrase: Option<&str>, repart_cfgs: Option<super::export::SystemdRepartData>) -> Result<()> {
// XXX: This is a bit hacky, but this function should be called before output.generate_fstab() for
// the fstab generator to be correct, IF we're using encryption
//
Expand All @@ -283,8 +285,10 @@ impl InstallationState {
.context("No xbootldr partition found")?;

let crypt_data = output.generate_cryptdata()?;

let rdm_result = super::export::ReadymadeResult::new(self.clone(), repart_cfgs);

container.run(|| self._inner_sys_setup(fstab, crypt_data, esp_node, &xbootldr_node))??;
container.run(|| self._inner_sys_setup(fstab, crypt_data, esp_node, &xbootldr_node, rdm_result))??;

Ok(())
}
Expand All @@ -297,7 +301,8 @@ impl InstallationState {
crypt_data: Option<CryptData>,
esp_node: Option<String>,
xbootldr_node: &str,
) -> Result<()> {
state_dump: super::export::ReadymadeResult
) -> Result<()> {
// We will run the specified postinstall modules now
let context = crate::backend::postinstall::Context {
destination_disk: self.destination_disk.as_ref().unwrap().devpath.clone(),
Expand All @@ -310,6 +315,13 @@ impl InstallationState {

tracing::info!("Writing /etc/fstab...");
std::fs::write("/etc/fstab", fstab).wrap_err("cannot write to /etc/fstab")?;

// Write the state dump to the chroot
let state_dump_path = Path::new(crate::consts::READYMADE_STATE_PATH);
let parent = state_dump_path.parent().ok_or_else(|| eyre!("Invalid state dump path - no parent directory"))?;
std::fs::create_dir_all(parent).wrap_err("Failed to create parent directories for state dump")?;
std::fs::write(&state_dump_path, state_dump.export_string().wrap_err("Failed to serialize state dump")?)
.wrap_err("Failed to write state dump file")?;

if let Some(data) = crypt_data {
tracing::info!("Writing /etc/crypttab...");
Expand All @@ -325,6 +337,45 @@ impl InstallationState {
Ok(())
}

#[tracing::instrument]
fn flash_submarine(blockdev: &Path) -> Result<()> {
tracing::debug!("Flashing submarine…");

// Find target submarine partition
let target_partition = lsblk::BlockDevice::list()?
.into_iter()
.find(|d| d.is_part()
&& d.disk_name().ok().as_deref()
== blockdev
.strip_prefix("/dev/")
.unwrap_or(&PathBuf::from(""))
.to_str()
&& d.name.ends_with('2'))
.ok_or_else(|| eyre!("Failed to find submarine partition"))?;

let source_path = Path::new("/usr/share/submarine/submarine.kpart");
let target_path = Path::new("/dev").join(&target_partition.name);

let mut source_file = std::fs::File::open(source_path)?;
let mut target_file = std::fs::OpenOptions::new()
.write(true)
.open(target_path)?;

std::io::copy(&mut source_file, &mut target_file)?;
target_file.sync_all()?;

Ok(())
}
// As of February 14, 2025, I have disabled the `dd` method for flashing the submarine partition,
// because we shouldn't really be dropping to shell commands for this kind of thing.
//
// The `dd` method is still here for reference if we ever need to use it again.
//
// See above for the new method of flashing the submarine partition,
// programmatically copying the submarine partition to the target disk.
//
// - Cappy
/*
#[tracing::instrument]
fn dd_submarine(blockdev: &Path) -> Result<()> {
tracing::debug!("dd-ing submarine…");
Expand Down Expand Up @@ -352,6 +403,7 @@ impl InstallationState {
}
Ok(())
}
*/

/// Mount a device or file to /mnt/live-base
fn mount_dev(dev: &str) -> std::io::Result<sys_mount::Mount> {
Expand Down
1 change: 1 addition & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod mksys;
pub mod postinstall;
pub mod repart_output;
pub mod repartcfg;
pub mod export;
5 changes: 5 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub const LIVE_BASE: &str = "/dev/mapper/live-base";
pub const ROOTFS_BASE: &str = "/run/rootfsbase";
pub const LUKS_KEYFILE_PATH: &str = "/run/readymade-luks.key";
const REPART_DIR: &str = "/usr/share/readymade/repart-cfgs/";
pub const READYMADE_STATE_PATH: &str = "/var/lib/readymade/state.json";

pub fn repart_dir() -> PathBuf {
PathBuf::from(std::env::var("READYMADE_REPART_DIR").unwrap_or_else(|_| REPART_DIR.into()))
Expand All @@ -15,6 +16,10 @@ pub fn open_keyfile() -> std::io::Result<std::fs::File> {
std::fs::File::open(LUKS_KEYFILE_PATH)
}

// pub fn state_dump_path(chroot: &PathBuf) -> PathBuf {
// chroot.join("var/lib/readymade/state.json")
// }

pub const fn shim_path() -> &'static str {
if cfg!(target_arch = "x86_64") {
EFI_SHIM_X86_64
Expand Down

0 comments on commit 6da4f17

Please sign in to comment.