-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: prototypical layer implementation
- Loading branch information
Showing
13 changed files
with
397 additions
and
72 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
//! Applies a set of recipes to a system. | ||
use std::{ | ||
collections::{HashMap, HashSet}, | ||
fs, | ||
ops::Deref, | ||
path::Path, | ||
sync::Arc, | ||
}; | ||
|
||
use anyhow::{anyhow, bail}; | ||
use clap::Parser; | ||
use rugpi_common::{mount::Mounted, Anyhow}; | ||
use tempfile::tempdir; | ||
use xscript::{cmd, run, vars, ParentEnv, Run}; | ||
|
||
use crate::project::{ | ||
config::Architecture, | ||
layers::LayerConfig, | ||
library::Library, | ||
recipes::{Recipe, StepKind}, | ||
Project, | ||
}; | ||
|
||
/// The arguments of the `customize` command. | ||
#[derive(Debug, Parser)] | ||
pub struct CustomizeTask { | ||
/// The source archive with the original system. | ||
src: String, | ||
/// The destination archive with the modified system. | ||
dest: String, | ||
} | ||
|
||
pub fn customize( | ||
project: &Project, | ||
arch: Architecture, | ||
layer: &LayerConfig, | ||
src: &Path, | ||
target: &Path, | ||
) -> Anyhow<()> { | ||
let library = project.load_library()?; | ||
// Collect the recipes to apply. | ||
let jobs = recipe_schedule(layer, &library)?; | ||
// Prepare system chroot. | ||
let root_dir = tempdir()?; | ||
let root_dir_path = root_dir.path(); | ||
println!("Extracting system files..."); | ||
run!(["tar", "-x", "-f", &src, "-C", root_dir_path])?; | ||
apply_recipes(arch, &jobs, root_dir_path)?; | ||
println!("Packing system files..."); | ||
run!(["tar", "-c", "-f", &target, "-C", root_dir_path, "."])?; | ||
Ok(()) | ||
} | ||
|
||
struct RecipeJob { | ||
recipe: Arc<Recipe>, | ||
parameters: HashMap<String, String>, | ||
} | ||
|
||
fn recipe_schedule(layer: &LayerConfig, library: &Library) -> Anyhow<Vec<RecipeJob>> { | ||
let mut stack = layer | ||
.recipes | ||
.iter() | ||
.map(|name| { | ||
library | ||
.lookup(library.repositories.root_repository, name.deref()) | ||
.ok_or_else(|| anyhow!("recipe with name {name} not found")) | ||
}) | ||
.collect::<Anyhow<Vec<_>>>()?; | ||
let mut enabled = stack.iter().cloned().collect::<HashSet<_>>(); | ||
while let Some(idx) = stack.pop() { | ||
let recipe = &library.recipes[idx]; | ||
for name in &recipe.info.dependencies { | ||
let dependency_idx = library | ||
.lookup(recipe.repository, name.deref()) | ||
.ok_or_else(|| anyhow!("recipe with name {name} not found"))?; | ||
if enabled.insert(dependency_idx) { | ||
stack.push(dependency_idx); | ||
} | ||
} | ||
} | ||
for excluded in &layer.exclude { | ||
let excluded = library | ||
.lookup(library.repositories.root_repository, excluded.deref()) | ||
.ok_or_else(|| anyhow!("recipe with name {excluded} not found"))?; | ||
enabled.remove(&excluded); | ||
} | ||
let parameters = layer | ||
.parameters | ||
.iter() | ||
.map(|(name, parameters)| { | ||
Ok(( | ||
library | ||
.lookup(library.repositories.root_repository, name.deref()) | ||
.ok_or_else(|| anyhow!("recipe with name {name} not found"))?, | ||
parameters, | ||
)) | ||
}) | ||
.collect::<Anyhow<HashMap<_, _>>>()?; | ||
let mut recipes = enabled | ||
.into_iter() | ||
.map(|idx| { | ||
let recipe = library.recipes[idx].clone(); | ||
let recipe_params = parameters.get(&idx); | ||
if let Some(params) = recipe_params { | ||
for param_name in params.keys() { | ||
if !recipe.info.parameters.contains_key(param_name) { | ||
bail!( | ||
"unknown parameter `{param_name}` of recipe `{}`", | ||
recipe.name | ||
); | ||
} | ||
} | ||
} | ||
let mut parameters = HashMap::new(); | ||
for (name, def) in &recipe.info.parameters { | ||
if let Some(params) = recipe_params { | ||
if let Some(value) = params.get(name) { | ||
parameters.insert(name.to_owned(), value.to_string()); | ||
continue; | ||
} | ||
} | ||
if let Some(default) = &def.default { | ||
parameters.insert(name.to_owned(), default.to_string()); | ||
continue; | ||
} | ||
bail!("unable to find value for parameter `{name}`"); | ||
} | ||
Ok(RecipeJob { recipe, parameters }) | ||
}) | ||
.collect::<Result<Vec<_>, _>>()?; | ||
// 4️⃣ Sort recipes by priority. | ||
recipes.sort_by_key(|job| -job.recipe.info.priority); | ||
Ok(recipes) | ||
} | ||
|
||
fn apply_recipes(arch: Architecture, jobs: &Vec<RecipeJob>, root_dir_path: &Path) -> Anyhow<()> { | ||
let _mounted_dev = Mounted::bind("/dev", root_dir_path.join("dev"))?; | ||
let _mounted_dev_pts = Mounted::bind("/dev/pts", root_dir_path.join("dev/pts"))?; | ||
let _mounted_sys = Mounted::bind("/sys", root_dir_path.join("sys"))?; | ||
let _mounted_proc = Mounted::mount_fs("proc", "proc", root_dir_path.join("proc"))?; | ||
let _mounted_run = Mounted::mount_fs("tmpfs", "tmpfs", root_dir_path.join("run"))?; | ||
let _mounted_tmp = Mounted::mount_fs("tmpfs", "tmpfs", root_dir_path.join("tmp"))?; | ||
|
||
let bakery_recipe_path = root_dir_path.join("run/rugpi/bakery/recipe"); | ||
fs::create_dir_all(&bakery_recipe_path)?; | ||
|
||
for (idx, job) in jobs.iter().enumerate() { | ||
let recipe = &job.recipe; | ||
println!( | ||
"[{:>2}/{}] {} {:?}", | ||
idx + 1, | ||
jobs.len(), | ||
recipe | ||
.info | ||
.description | ||
.as_deref() | ||
.unwrap_or(recipe.name.deref()), | ||
&job.parameters, | ||
); | ||
let _mounted_recipe = Mounted::bind(&recipe.path, &bakery_recipe_path)?; | ||
|
||
for step in &recipe.steps { | ||
println!(" - {}", step.filename); | ||
match &step.kind { | ||
StepKind::Packages { packages } => { | ||
let mut cmd = cmd!("chroot", root_dir_path, "apt-get", "install", "-y"); | ||
cmd.extend_args(packages); | ||
ParentEnv.run(cmd.with_vars(vars! { | ||
DEBIAN_FRONTEND = "noninteractive" | ||
}))?; | ||
} | ||
StepKind::Install => { | ||
let script = format!("/run/rugpi/bakery/recipe/steps/{}", step.filename); | ||
let mut vars = vars! { | ||
DEBIAN_FRONTEND = "noninteractive", | ||
RUGPI_ROOT_DIR = "/", | ||
RUGPI_ARCH = arch.as_str(), | ||
RECIPE_DIR = "/run/rugpi/bakery/recipe/", | ||
RECIPE_STEP_PATH = &script, | ||
}; | ||
for (name, value) in &job.parameters { | ||
vars.set(format!("RECIPE_PARAM_{}", name.to_uppercase()), value); | ||
} | ||
run!(["chroot", root_dir_path, &script].with_vars(vars))?; | ||
} | ||
StepKind::Run => { | ||
let script = recipe.path.join("steps").join(&step.filename); | ||
let mut vars = vars! { | ||
DEBIAN_FRONTEND = "noninteractive", | ||
RUGPI_ROOT_DIR = root_dir_path, | ||
RUGPI_ARCH = arch.as_str(), | ||
RECIPE_DIR = &recipe.path, | ||
RECIPE_STEP_PATH = &script, | ||
}; | ||
for (name, value) in &job.parameters { | ||
vars.set(format!("RECIPE_PARAM_{}", name.to_uppercase()), value); | ||
} | ||
run!([&script].with_vars(vars))?; | ||
} | ||
} | ||
} | ||
} | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
//! Functionality for baking layers and images. | ||
use std::{ | ||
fs, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
||
use anyhow::{anyhow, bail}; | ||
use rugpi_common::{loop_dev::LoopDevice, mount::Mounted, Anyhow}; | ||
use tempfile::tempdir; | ||
use xscript::{run, Run}; | ||
|
||
use crate::{ | ||
project::{config::Architecture, Project}, | ||
utils::{download, sha1}, | ||
}; | ||
|
||
pub mod customize; | ||
pub mod image; | ||
|
||
pub fn bake_image(project: &Project, image: &str, output: &Path) -> Anyhow<()> { | ||
let image_config = project | ||
.config | ||
.images | ||
.get(image) | ||
.ok_or_else(|| anyhow!("unable to find image {image}"))?; | ||
let baked_layer = bake_layer(project, image_config.architecture, &image_config.layer)?; | ||
image::make_image(image_config, &baked_layer, output) | ||
} | ||
|
||
pub fn bake_layer(project: &Project, arch: Architecture, layer: &str) -> Anyhow<PathBuf> { | ||
let layer_config = project | ||
.config | ||
.layers | ||
.get(layer) | ||
.ok_or_else(|| anyhow!("unable to find layer {layer}"))?; | ||
if let Some(url) = &layer_config.url { | ||
let layer_id = sha1(url); | ||
let system_tar = project | ||
.dir | ||
.join(format!(".rugpi/layers/{layer_id}/system.tar")); | ||
if !system_tar.exists() { | ||
extract(url, &system_tar)?; | ||
} | ||
Ok(system_tar) | ||
} else if let Some(parent) = &layer_config.parent { | ||
let src = bake_layer(project, arch, parent)?; | ||
let mut layer_string = layer.to_owned(); | ||
layer_string.push('.'); | ||
layer_string.push_str(arch.as_str()); | ||
let layer_id = sha1(&layer_string); | ||
let target = project | ||
.dir | ||
.join(format!(".rugpi/layers/{layer_id}/system.tar")); | ||
fs::create_dir_all(target.parent().unwrap()).ok(); | ||
if !target.exists() { | ||
customize::customize(project, arch, layer_config, &src, &target)?; | ||
} | ||
Ok(target) | ||
} else { | ||
bail!("invalid layer configuration") | ||
} | ||
} | ||
|
||
fn extract(image_url: &str, layer_path: &Path) -> Anyhow<()> { | ||
let mut image_path = download(image_url)?; | ||
if image_path.extension() == Some("xz".as_ref()) { | ||
let decompressed_image_path = image_path.with_extension(""); | ||
if !decompressed_image_path.is_file() { | ||
eprintln!("Decompressing XZ image..."); | ||
run!(["xz", "-d", "-k", image_path])?; | ||
} | ||
image_path = decompressed_image_path; | ||
} | ||
eprintln!("Creating `.tar` archive with system files..."); | ||
if let Some(parent) = layer_path.parent() { | ||
if !parent.exists() { | ||
fs::create_dir_all(parent)?; | ||
} | ||
} | ||
let loop_dev = LoopDevice::attach(image_path)?; | ||
let temp_dir = tempdir()?; | ||
let temp_dir_path = temp_dir.path(); | ||
let _mounted_root = Mounted::mount(loop_dev.partition(2), temp_dir_path)?; | ||
let _mounted_boot = Mounted::mount(loop_dev.partition(1), temp_dir_path.join("boot"))?; | ||
run!(["tar", "-c", "-f", &layer_path, "-C", temp_dir_path, "."])?; | ||
Ok(()) | ||
} |
Oops, something went wrong.