Skip to content

Commit 6da4f17

Browse files
committed
feat: Dump readymade install state to a file in target root
1 parent b7956de commit 6da4f17

File tree

6 files changed

+159
-13
lines changed

6 files changed

+159
-13
lines changed

HACKING.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ The UI code is written in [Relm](https://relm4.org/), a GTK4 UI framework for Ru
88

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

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

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

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

88-
Currently Readymade only supports Chromebook installations, it is recommended you run Readymade on a Chromebook device to test the installer.
89-
9088
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_**.
9189

9290
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.
@@ -106,6 +104,9 @@ sudo REPART_COPY_SOURCE=/mnt/rootfs readymade
106104
> [!NOTE]
107105
> If Readymade is built as a debug build, it will dump the installation state and the systemd-repart output to `/tmp/` for debugging purposes.
108106
107+
Readymade also dumps a redacted version of the installation state in `/var/lib/readymade/state.json` for other tools to use, such as system
108+
recovery tools.
109+
109110
## Localization
110111

111112
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.

src/backend/custom.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
use std::path::PathBuf;
44

55
use color_eyre::eyre::Context;
6-
6+
use super::export::ReadymadeResult;
77
use super::install::InstallationState;
88

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

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

174175
Ok(())
175176
}

src/backend/export.rs

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Module for exporting Readymade's install state to a file, Useful for other tools to check
2+
//! the initial state of the system. Not so useful when the user modifies the system after and
3+
//! the state drifts from the initial state. (i.e the user repartitions the disk, adds a new disk,
4+
//! spans the BTRFS volume, etc.)
5+
//!
6+
use std::{collections::BTreeMap, path::Path};
7+
8+
use color_eyre::Result;
9+
use serde::{Deserialize, Serialize};
10+
11+
use super::{install::InstallationState, repartcfg::RepartConfig};
12+
/// The version of the result dump format, for backwards compat reasons
13+
///
14+
/// If there's any changes to the format, this should be bumped up to the next version.
15+
///
16+
const RESULT_DUMP_FORMAT_VERSION: &str = "0.1.0";
17+
#[derive(Serialize, Deserialize, Debug)]
18+
pub struct ReadymadeResult {
19+
pub version: &'static str,
20+
pub readymade_version: &'static str,
21+
pub is_debug_build: bool,
22+
pub state: InstallationState,
23+
pub systemd_repart_data: Option<SystemdRepartData>,
24+
}
25+
26+
impl ReadymadeResult {
27+
pub fn export_string(&self) -> Result<String> {
28+
Ok(serde_json::to_string_pretty(&self)?)
29+
}
30+
31+
pub fn new(state: InstallationState, systemd_repart_data: Option<SystemdRepartData>) -> Self {
32+
Self {
33+
version: RESULT_DUMP_FORMAT_VERSION,
34+
readymade_version: env!("CARGO_PKG_VERSION"),
35+
is_debug_build: cfg!(debug_assertions),
36+
state: prep_state_for_export(state).unwrap(),
37+
systemd_repart_data,
38+
}
39+
}
40+
41+
}
42+
43+
#[derive(Serialize, Deserialize, Debug)]
44+
pub struct SystemdRepartData {
45+
configs: BTreeMap<String, RepartConfig>,
46+
}
47+
48+
impl SystemdRepartData {
49+
pub fn new(configs: BTreeMap<String, RepartConfig>) -> Self {
50+
Self { configs }
51+
}
52+
53+
pub fn get_configs(cfg_path: &Path) -> Result<Self> {
54+
let mut configs = BTreeMap::new();
55+
// Read path
56+
for entry in std::fs::read_dir(&cfg_path)? {
57+
let entry = entry?;
58+
let path = entry.path();
59+
if !path.is_file() {
60+
continue;
61+
}
62+
let file_config = std::fs::read_to_string(&path)?;
63+
64+
// Parse the config
65+
let config: RepartConfig = serde_systemd_unit::from_str(&file_config)?;
66+
67+
// Add to the list
68+
configs.insert(
69+
path.file_name().unwrap().to_string_lossy().to_string(),
70+
config,
71+
);
72+
}
73+
Ok(Self::new(configs))
74+
}
75+
}
76+
77+
pub fn prep_state_for_export(state: InstallationState) -> Result<InstallationState> {
78+
let mut new_state = state.clone();
79+
80+
// Clear out passwords
81+
if let Some(ref mut enc_key) = new_state.encryption_key {
82+
enc_key.clear();
83+
new_state.encryption_key = Some("REDACTED".to_string());
84+
}
85+
Ok(new_state)
86+
}

src/backend/install.rs

+58-6
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub enum InstallationType {
4343
Custom,
4444
}
4545

46-
#[derive(Debug, Serialize, Deserialize)]
46+
#[derive(Debug, Serialize, Deserialize, Clone)]
4747
pub struct InstallationState {
4848
pub langlocale: Option<String>,
4949
pub destination_disk: Option<DiskInit>,
@@ -236,13 +236,15 @@ impl InstallationState {
236236
// todo: not freeze on error, show error message as err handler?
237237
Self::systemd_repart(blockdev, &cfgdir, self.encrypt && self.encryption_key.is_some())?
238238
});
239+
240+
let repartcfg_export = super::export::SystemdRepartData::get_configs(&cfgdir)?;
239241

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

243245
if let InstallationType::ChromebookInstall = inst_type {
244246
// FIXME: don't dd?
245-
Self::dd_submarine(blockdev)?;
247+
Self::flash_submarine(blockdev)?;
246248
InstallationType::set_cgpt_flags(blockdev)?;
247249
}
248250

@@ -267,7 +269,7 @@ impl InstallationState {
267269
}
268270

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

285287
let crypt_data = output.generate_cryptdata()?;
288+
289+
let rdm_result = super::export::ReadymadeResult::new(self.clone(), repart_cfgs);
286290

287-
container.run(|| self._inner_sys_setup(fstab, crypt_data, esp_node, &xbootldr_node))??;
291+
container.run(|| self._inner_sys_setup(fstab, crypt_data, esp_node, &xbootldr_node, rdm_result))??;
288292

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

311316
tracing::info!("Writing /etc/fstab...");
312317
std::fs::write("/etc/fstab", fstab).wrap_err("cannot write to /etc/fstab")?;
318+
319+
// Write the state dump to the chroot
320+
let state_dump_path = Path::new(crate::consts::READYMADE_STATE_PATH);
321+
let parent = state_dump_path.parent().ok_or_else(|| eyre!("Invalid state dump path - no parent directory"))?;
322+
std::fs::create_dir_all(parent).wrap_err("Failed to create parent directories for state dump")?;
323+
std::fs::write(&state_dump_path, state_dump.export_string().wrap_err("Failed to serialize state dump")?)
324+
.wrap_err("Failed to write state dump file")?;
313325

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

340+
#[tracing::instrument]
341+
fn flash_submarine(blockdev: &Path) -> Result<()> {
342+
tracing::debug!("Flashing submarine…");
343+
344+
// Find target submarine partition
345+
let target_partition = lsblk::BlockDevice::list()?
346+
.into_iter()
347+
.find(|d| d.is_part()
348+
&& d.disk_name().ok().as_deref()
349+
== blockdev
350+
.strip_prefix("/dev/")
351+
.unwrap_or(&PathBuf::from(""))
352+
.to_str()
353+
&& d.name.ends_with('2'))
354+
.ok_or_else(|| eyre!("Failed to find submarine partition"))?;
355+
356+
let source_path = Path::new("/usr/share/submarine/submarine.kpart");
357+
let target_path = Path::new("/dev").join(&target_partition.name);
358+
359+
let mut source_file = std::fs::File::open(source_path)?;
360+
let mut target_file = std::fs::OpenOptions::new()
361+
.write(true)
362+
.open(target_path)?;
363+
364+
std::io::copy(&mut source_file, &mut target_file)?;
365+
target_file.sync_all()?;
366+
367+
Ok(())
368+
}
369+
// As of February 14, 2025, I have disabled the `dd` method for flashing the submarine partition,
370+
// because we shouldn't really be dropping to shell commands for this kind of thing.
371+
//
372+
// The `dd` method is still here for reference if we ever need to use it again.
373+
//
374+
// See above for the new method of flashing the submarine partition,
375+
// programmatically copying the submarine partition to the target disk.
376+
//
377+
// - Cappy
378+
/*
328379
#[tracing::instrument]
329380
fn dd_submarine(blockdev: &Path) -> Result<()> {
330381
tracing::debug!("dd-ing submarine…");
@@ -352,6 +403,7 @@ impl InstallationState {
352403
}
353404
Ok(())
354405
}
406+
*/
355407

356408
/// Mount a device or file to /mnt/live-base
357409
fn mount_dev(dev: &str) -> std::io::Result<sys_mount::Mount> {

src/backend/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pub mod mksys;
44
pub mod postinstall;
55
pub mod repart_output;
66
pub mod repartcfg;
7+
pub mod export;

src/consts.rs

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub const LIVE_BASE: &str = "/dev/mapper/live-base";
66
pub const ROOTFS_BASE: &str = "/run/rootfsbase";
77
pub const LUKS_KEYFILE_PATH: &str = "/run/readymade-luks.key";
88
const REPART_DIR: &str = "/usr/share/readymade/repart-cfgs/";
9+
pub const READYMADE_STATE_PATH: &str = "/var/lib/readymade/state.json";
910

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

19+
// pub fn state_dump_path(chroot: &PathBuf) -> PathBuf {
20+
// chroot.join("var/lib/readymade/state.json")
21+
// }
22+
1823
pub const fn shim_path() -> &'static str {
1924
if cfg!(target_arch = "x86_64") {
2025
EFI_SHIM_X86_64

0 commit comments

Comments
 (0)