installer: compute filesystem_size from the package set (target-agnostic)

--filesystem-size previously just echoed config.general.filesystem_size (or 0),
so every target hardcoded a magic image size that either wasted space or
overflowed (ENOSPC / os error 28) as the package set changed.

Add compute_filesystem_size_mb(): when filesystem_size is not pinned, size the
RedoxFS image from the ACTUAL package contents — walk the transitive package
closure (config [packages] + each pkgar's `depends`), do per-file block
accounting (ceil(size/BLOCK_SIZE) data blocks + one node block per entry), add
record-tree/header structural overhead and a free-space margin so the live/
writable filesystem is usable. An explicit filesystem_size still overrides.

Identical logic for mini/full/bare/grub/any config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Red Bear OS
2026-07-29 11:26:31 +09:00
parent bb243fb3e5
commit 89e1c67006
2 changed files with 115 additions and 1 deletions
+17 -1
View File
@@ -157,7 +157,23 @@ fn main() {
}
if parser.found("filesystem-size") {
println!("{}", config.general.filesystem_size.unwrap_or(0));
// An explicit `filesystem_size` in the config is an override and is
// honored as-is; otherwise the size is COMPUTED from the actual package
// set (see redox_installer::compute_filesystem_size_mb) — never guessed.
let size = match config.general.filesystem_size {
Some(size) => u64::from(size),
None => {
let cookbook = parser.get_opt("cookbook").unwrap_or_else(|| ".".to_string());
match redox_installer::compute_filesystem_size_mb(&config, &cookbook) {
Ok(mb) => mb,
Err(err) => {
eprintln!("installer: filesystem-size: {err}");
process::exit(1);
}
}
}
};
println!("{size}");
} else if parser.found("list-packages") {
// List the packages that should be fetched or built by the cookbook
for (packagename, package) in &config.packages {
+98
View File
@@ -130,6 +130,104 @@ fn install_packages(config: &Config, dest: &Path, cookbook: Option<&str>) -> any
Ok(())
}
/// Compute the RedoxFS image size (in MiB) needed to hold this config's package
/// set, derived from the ACTUAL package contents — never a hardcoded guess. It
/// walks the transitive package closure (config `[packages]` plus each pkgar's
/// `depends`), does per-file RedoxFS block accounting (`ceil(size/BLOCK_SIZE)`
/// data blocks + one node block per file/dir), adds the record-tree/header
/// structural overhead, and a free-space margin so the resulting live/writable
/// filesystem is functional (a 100%-full RedoxFS cannot be created or used).
///
/// Target-agnostic: identical logic sizes mini, full, bare, grub, or any config.
/// Reads pkgars from `<cookbook>/repo/<target>/` using the repo's public key.
pub fn compute_filesystem_size_mb(config: &Config, cookbook: &str) -> Result<u64> {
use pkgar::PackageFile;
use pkgar_core::PackageSrc;
let target = get_target();
let repo = Path::new(cookbook).join("repo").join(&target);
let pubkey_path = Path::new(cookbook).join("build").join("id_ed25519.pub.toml");
let pkey = pkgar_keys::PublicKeyFile::open(&pubkey_path)
.map_err(|e| anyhow::anyhow!("opening public key {}: {:?}", pubkey_path.display(), e))?
.pkey;
// Transitive closure: explicitly-requested packages + their pkgar `depends`.
let mut stack: Vec<String> = config
.packages
.iter()
.filter_map(|(name, pkg)| match pkg {
PackageConfig::Build(rule) if rule == "ignore" => None,
_ => Some(name.clone()),
})
.collect();
let mut seen: BTreeMap<String, ()> = BTreeMap::new();
let block = BLOCK_SIZE; // redoxfs block size (4096)
let mut data_blocks: u64 = 0;
let mut nodes: u64 = 0;
let mut missing: Vec<String> = Vec::new();
while let Some(name) = stack.pop() {
if seen.insert(name.clone(), ()).is_some() {
continue;
}
// Follow dependencies recorded in the published repo metadata.
let toml_path = repo.join(format!("{name}.toml"));
if let Ok(text) = fs::read_to_string(&toml_path) {
if let Ok(value) = text.parse::<toml::Value>() {
if let Some(deps) = value.get("depends").and_then(|d| d.as_array()) {
for dep in deps.iter().filter_map(|d| d.as_str()) {
stack.push(dep.to_string());
}
}
}
}
// Per-file block accounting from the pkgar itself.
let pkgar_path = repo.join(format!("{name}.pkgar"));
if !pkgar_path.is_file() {
missing.push(name.clone());
continue;
}
let mut package = PackageFile::new(&pkgar_path, &pkey)
.map_err(|e| anyhow::anyhow!("opening {}: {:?}", pkgar_path.display(), e))?;
let entries = package
.read_entries()
.map_err(|e| anyhow::anyhow!("reading entries of {}: {:?}", pkgar_path.display(), e))?;
for entry in &entries {
data_blocks += entry.size().div_ceil(block);
nodes += 1; // one RedoxFS node per file/dir
}
}
// Config `[[files]]` payloads (init.d services, filesystem.toml, os-release …).
for file in &config.files {
data_blocks += (file.data.len() as u64).div_ceil(block);
nodes += 1;
}
if !missing.is_empty() {
eprintln!(
"installer: filesystem-size: warning: {} package(s) missing a pkgar in {} \
(size may be under-counted): {}",
missing.len(),
repo.display(),
missing.join(", "),
);
}
// RedoxFS structural overhead: one node block per entry is already counted in
// `nodes`; the record tree holding those nodes needs ~1 block per 64 entries,
// plus a fixed header/root/allocator allowance (~1 MiB).
let tree_blocks = nodes / 64 + 256;
let content_bytes = (data_blocks + nodes + tree_blocks).saturating_mul(block);
// Free-space margin so the live/writable image is usable, not 100% full.
let margin = std::cmp::max(content_bytes / 4, 64 * 1024 * 1024); // +25%, min +64 MiB
let total_bytes = content_bytes.saturating_add(margin);
Ok((total_bytes.div_ceil(1024 * 1024)).max(64))
}
pub fn install_dir(
config: Config,
output_dir: impl AsRef<Path>,