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:
+17
-1
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user