update docs
This commit is contained in:
@@ -8,7 +8,7 @@ use fds_common::{
|
||||
manifest::{Cartridge, Class, Manifest, Media},
|
||||
read_text,
|
||||
};
|
||||
use fds_software::{Catalogue, archive};
|
||||
use fds_software::{Catalogue, archive, tree};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
@@ -22,7 +22,8 @@ use std::{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Payload {
|
||||
bundles: Vec<PathBuf>,
|
||||
/// Void software recipes or previously built installed-software directories.
|
||||
sources: Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -55,8 +56,16 @@ fn uuid(bytes: &[u8]) -> [u8; 16] {
|
||||
id[8] = (id[8] & 63) | 0x80;
|
||||
id
|
||||
}
|
||||
pub fn create(recipe: &Path, output: &Path, runner: Option<&Path>) -> Result<Inspection> {
|
||||
pub fn create(
|
||||
recipe: &Path,
|
||||
output: &Path,
|
||||
runner: Option<&Path>,
|
||||
options: &crate::software::BuildOptions,
|
||||
) -> Result<Inspection> {
|
||||
workstation()?;
|
||||
if output.try_exists()? {
|
||||
return Err(Error("Cartridge output already exists".into()));
|
||||
}
|
||||
if unsafe { libc::geteuid() } == 0 {
|
||||
return Err(Error(
|
||||
"Create cartridge images as an ordinary workstation user".into(),
|
||||
@@ -64,12 +73,12 @@ pub fn create(recipe: &Path, output: &Path, runner: Option<&Path>) -> Result<Ins
|
||||
}
|
||||
let plan: Recipe = toml::from_str(&read_text(recipe, 65536)?)
|
||||
.map_err(|e| Error(format!("Invalid cartridge recipe: {e}")))?;
|
||||
if plan.format != 1
|
||||
if plan.format != 2
|
||||
|| !(1..=32).contains(&plan.payload.len())
|
||||
|| plan.payload.iter().any(|p| p.bundles.is_empty())
|
||||
|| plan.payload.iter().any(|p| p.sources.is_empty())
|
||||
{
|
||||
return Err(Error(
|
||||
"Cartridge recipe requires format 1 and 1..32 nonempty payload partitions".into(),
|
||||
"Cartridge recipe requires format 2 and 1..32 nonempty payload partitions".into(),
|
||||
));
|
||||
}
|
||||
let metadata = Manifest {
|
||||
@@ -88,23 +97,33 @@ pub fn create(recipe: &Path, output: &Path, runner: Option<&Path>) -> Result<Ins
|
||||
let base = parent(recipe)?;
|
||||
let tree = work.0.join("metadata");
|
||||
fs::create_dir_all(tree.join("FDS"))?;
|
||||
for directory in [&tree, &tree.join("FDS")] {
|
||||
fs::set_permissions(directory, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
fs::write(tree.join("FDS/CARTRIDGE.TOML"), metadata.to_toml()?)?;
|
||||
let mut catalogue = Catalogue {
|
||||
format: 1,
|
||||
format: 2,
|
||||
software: Vec::new(),
|
||||
};
|
||||
let mut trees = vec![tree.clone()];
|
||||
for (index, part) in plan.payload.iter().enumerate() {
|
||||
let tree = work.0.join(format!("payload{}", index + 2));
|
||||
fs::create_dir_all(tree.join("bundles"))?;
|
||||
for directory in &part.bundles {
|
||||
let directory = base.join(directory).canonicalize()?;
|
||||
fs::create_dir_all(tree.join("programs"))?;
|
||||
for directory in [&tree, &tree.join("programs")] {
|
||||
fs::set_permissions(directory, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
for (number, source) in part.sources.iter().enumerate() {
|
||||
let source = base.join(source).canonicalize()?;
|
||||
let directory = if source.is_dir() {
|
||||
source
|
||||
} else {
|
||||
let directory = work.0.join(format!("installed-{index}-{number}"));
|
||||
crate::software::build(&source, &directory, options)?;
|
||||
directory
|
||||
};
|
||||
let mut entry = crate::software::load(&directory)?;
|
||||
entry.partition = (index + 2) as u8;
|
||||
fs::copy(
|
||||
directory.join(format!("{}.tar.xz", entry.id)),
|
||||
tree.join(entry.archive_path()),
|
||||
)?;
|
||||
tree::copy(&directory.join("root"), &tree.join(entry.root_path()))?;
|
||||
catalogue.software.push(entry);
|
||||
}
|
||||
trees.push(tree);
|
||||
@@ -192,7 +211,7 @@ pub fn inspect(path: &Path, runner: Option<&Path>) -> Result<Inspection> {
|
||||
let mut info = image::inspect(&file, file.metadata()?.len())?;
|
||||
if info.filesystem != "erofs" {
|
||||
return Err(Error(
|
||||
"Use fds-burn inspect for DATA geometry; this inspector validates software bundles"
|
||||
"Use fds-burn inspect for DATA geometry; this inspector validates software cartridges"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
@@ -235,6 +254,13 @@ pub fn inspect(path: &Path, runner: Option<&Path>) -> Result<Inspection> {
|
||||
}
|
||||
for software in &catalogue.software {
|
||||
let root = &trees[usize::from(software.partition) - 1];
|
||||
if software.installed {
|
||||
if !root.join("programs").symlink_metadata()?.is_dir() {
|
||||
return Err(Error("Programs must be a real directory".into()));
|
||||
}
|
||||
tree::verify(&root.join(software.root_path()), software)?;
|
||||
continue;
|
||||
}
|
||||
let directory = root.join("bundles");
|
||||
if !directory.symlink_metadata()?.is_dir() {
|
||||
return Err(Error("Bundle directory must not be a symlink".into()));
|
||||
@@ -247,23 +273,36 @@ pub fn inspect(path: &Path, runner: Option<&Path>) -> Result<Inspection> {
|
||||
}
|
||||
// Reject unlisted files or software hidden in a payload partition.
|
||||
for (index, root) in trees.iter().enumerate().skip(1) {
|
||||
let directory = if catalogue.format == 2 {
|
||||
"programs"
|
||||
} else {
|
||||
"bundles"
|
||||
};
|
||||
if fs::read_dir(root)?.count() != 1 {
|
||||
return Err(Error("Payload partitions may contain only bundles/".into()));
|
||||
return Err(Error(format!(
|
||||
"Payload partitions may contain only {directory}/"
|
||||
)));
|
||||
}
|
||||
let mut actual: Vec<_> = fs::read_dir(root.join("bundles"))?
|
||||
let mut actual: Vec<_> = fs::read_dir(root.join(directory))?
|
||||
.map(|e| e.map(|e| e.file_name()))
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
let mut expected: Vec<_> = catalogue
|
||||
.software
|
||||
.iter()
|
||||
.filter(|s| usize::from(s.partition) == index + 1)
|
||||
.map(|s| std::ffi::OsString::from(format!("{}.tar.xz", s.id)))
|
||||
.map(|s| {
|
||||
std::ffi::OsString::from(if s.installed {
|
||||
s.id.clone()
|
||||
} else {
|
||||
format!("{}.tar.xz", s.id)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
actual.sort();
|
||||
expected.sort();
|
||||
if actual != expected {
|
||||
return Err(Error(
|
||||
"Payload archive inventory disagrees with metadata".into(),
|
||||
"Payload software inventory disagrees with metadata".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn cartridge(runner: Option<&Path>) -> Result<Value> {
|
||||
)));
|
||||
}
|
||||
Ok(
|
||||
json!({"xz":xz,"mkfs.erofs":mkfs,"fsck.erofs":fsck,"unprivileged_sandbox":"available","cross_compiler":"recipe-specific; use aarch64 output or architecture=any scripts"}),
|
||||
json!({"xz_legacy_reader":xz,"mkfs.erofs":mkfs,"fsck.erofs":fsck,"unprivileged_sandbox":"available","software_builder":"prepared Void xbps-src checkout with native XBPS tools; builds aarch64 source packages and installs runtime dependencies"}),
|
||||
)
|
||||
}
|
||||
pub fn emulator(runner: Option<&Path>) -> Result<Value> {
|
||||
|
||||
@@ -5,18 +5,20 @@ use std::{path::PathBuf, process::ExitCode};
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
about = "Build software bundles and metadata-first cartridge images on Linux"
|
||||
about = "Build Void source packages and ready-to-run cartridge images on Linux"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Optional wrapper that accepts TOOL followed by its arguments.
|
||||
#[arg(long, global = true)]
|
||||
image_tool_runner: Option<PathBuf>,
|
||||
#[command(flatten)]
|
||||
build: fds_workstation::software::BuildOptions,
|
||||
#[command(subcommand)]
|
||||
command: Action,
|
||||
}
|
||||
#[derive(Subcommand)]
|
||||
enum Action {
|
||||
/// Check xz, erofs-utils and the unprivileged filesystem inspection sandbox.
|
||||
/// Check EROFS tools, the inspection sandbox and legacy xz reader.
|
||||
Doctor,
|
||||
/// Build or package software on the workstation, never on the Pi.
|
||||
Software {
|
||||
@@ -25,7 +27,7 @@ enum Action {
|
||||
},
|
||||
/// Construct and verify a complete 1+m GPT cartridge image before burning.
|
||||
Create { recipe: PathBuf, output: PathBuf },
|
||||
/// Verify GPT, every filesystem, catalogue and xz tarball in a prepared image.
|
||||
/// Verify GPT, every filesystem, catalogue and installed program tree.
|
||||
Inspect { image: PathBuf },
|
||||
/// Verify a prepared image and save an image/target-bound write preview.
|
||||
Preview {
|
||||
@@ -45,11 +47,9 @@ enum Action {
|
||||
}
|
||||
#[derive(Subcommand)]
|
||||
enum Software {
|
||||
/// Execute an explicit trusted workstation build recipe, then package its output.
|
||||
/// Build a Void source template and install its complete runtime package tree.
|
||||
Build { recipe: PathBuf, output: PathBuf },
|
||||
/// Package an existing software tree without running its build command.
|
||||
Pack { recipe: PathBuf, output: PathBuf },
|
||||
/// Check a built software descriptor, archive digest and extracted contents.
|
||||
/// Check installed package metadata and every program/dependency file.
|
||||
Inspect { directory: PathBuf },
|
||||
}
|
||||
fn run() -> Result<()> {
|
||||
@@ -76,16 +76,18 @@ fn run() -> Result<()> {
|
||||
}
|
||||
Action::Software { command } => serde_json::to_value(match command {
|
||||
Software::Build { recipe, output } => {
|
||||
fds_workstation::software::build(&recipe, &output, true)?
|
||||
}
|
||||
Software::Pack { recipe, output } => {
|
||||
fds_workstation::software::build(&recipe, &output, false)?
|
||||
fds_workstation::software::build(&recipe, &output, &cli.build)?
|
||||
}
|
||||
Software::Inspect { directory } => fds_workstation::software::load(&directory)?,
|
||||
}),
|
||||
Action::Create { recipe, output } => serde_json::to_value(
|
||||
fds_workstation::cartridge::create(&recipe, &output, cli.image_tool_runner.as_deref())?,
|
||||
),
|
||||
Action::Create { recipe, output } => {
|
||||
serde_json::to_value(fds_workstation::cartridge::create(
|
||||
&recipe,
|
||||
&output,
|
||||
cli.image_tool_runner.as_deref(),
|
||||
&cli.build,
|
||||
)?)
|
||||
}
|
||||
Action::Inspect { image } => serde_json::to_value(fds_workstation::cartridge::inspect(
|
||||
&image,
|
||||
cli.image_tool_runner.as_deref(),
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
use crate::{Work, parent, workstation};
|
||||
//! Void source packages are built and installed on the workstation only.
|
||||
use crate::{Work, image_tool, parent, success, workstation};
|
||||
use fds_common::{Error, Result, manifest::identifier, read_text};
|
||||
use fds_software::{Software, archive};
|
||||
use fds_software::{Software, tree};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs::{self, File, OpenOptions},
|
||||
os::unix::fs::{OpenOptionsExt, PermissionsExt},
|
||||
fs::{self, File},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
#[derive(clap::Args, Debug, Clone)]
|
||||
pub struct BuildOptions {
|
||||
/// Prepared Void source checkout used by xbps-src (not the workstation OS).
|
||||
#[arg(long, global = true, default_value = "vendor/void-packages")]
|
||||
pub void_packages: PathBuf,
|
||||
/// Optional rootless XBPS wrapper, for example this checkout's tools/in-void.
|
||||
#[arg(long, global = true)]
|
||||
pub xbps_tool_runner: Option<PathBuf>,
|
||||
/// Native XBPS executables for xbps-src, when they are not already in PATH.
|
||||
#[arg(long, global = true)]
|
||||
pub xbps_bin: Option<PathBuf>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Build {
|
||||
directory: PathBuf,
|
||||
command: Vec<String>,
|
||||
#[serde(default)]
|
||||
environment: BTreeMap<String, String>,
|
||||
struct Source {
|
||||
package: String,
|
||||
/// Omit to build an existing source package from the selected Void checkout.
|
||||
template: Option<PathBuf>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -25,97 +37,215 @@ struct Recipe {
|
||||
id: String,
|
||||
name: String,
|
||||
version: String,
|
||||
architecture: String,
|
||||
root: PathBuf,
|
||||
commands: BTreeMap<String, String>,
|
||||
build: Option<Build>,
|
||||
source: Source,
|
||||
}
|
||||
pub fn build(recipe: &Path, output: &Path, compile: bool) -> Result<Software> {
|
||||
fn xbps(options: &BuildOptions, program: &str) -> Command {
|
||||
let mut command = image_tool(options.xbps_tool_runner.as_deref(), "env");
|
||||
command.args(["XBPS_ARCH=aarch64", "XBPS_TARGET_ARCH=aarch64", program]);
|
||||
if let Some(directory) = &options.xbps_bin {
|
||||
let mut paths = vec![directory.clone()];
|
||||
paths.extend(std::env::split_paths(
|
||||
&std::env::var_os("PATH").unwrap_or_default(),
|
||||
));
|
||||
if let Ok(path) = std::env::join_paths(paths) {
|
||||
command.env("PATH", path);
|
||||
}
|
||||
}
|
||||
command
|
||||
}
|
||||
pub fn build(recipe: &Path, output: &Path, options: &BuildOptions) -> Result<Software> {
|
||||
workstation()?;
|
||||
if unsafe { libc::geteuid() } == 0 {
|
||||
return Err(Error(
|
||||
"Run software builds as an ordinary workstation user".into(),
|
||||
"Build Void software as an ordinary workstation user".into(),
|
||||
));
|
||||
}
|
||||
if output.try_exists()? {
|
||||
return Err(Error("Software output already exists".into()));
|
||||
}
|
||||
let input: Recipe = toml::from_str(&read_text(recipe, 65536)?)
|
||||
.map_err(|e| Error(format!("Invalid build recipe: {e}")))?;
|
||||
if input.format != 1 || !identifier(&input.id) {
|
||||
.map_err(|e| Error(format!("Invalid Void software recipe: {e}")))?;
|
||||
if input.format != 2
|
||||
|| !identifier(&input.id)
|
||||
|| !fds_software::xbps_identifier(&input.source.package)
|
||||
{
|
||||
return Err(Error(
|
||||
"Build recipe requires format 1 and a valid software id".into(),
|
||||
"Software recipe requires format 2, a software id and a Void source package name"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
let base = parent(recipe)?;
|
||||
if compile {
|
||||
let build = input.build.ok_or_else(|| {
|
||||
Error("Build recipe lacks [build]; use software pack for an existing tree".into())
|
||||
})?;
|
||||
let program = build
|
||||
.command
|
||||
.first()
|
||||
.ok_or_else(|| Error("Build command is empty".into()))?;
|
||||
let directory = base.join(build.directory).canonicalize()?;
|
||||
crate::success(
|
||||
Command::new(program)
|
||||
.args(&build.command[1..])
|
||||
.current_dir(directory)
|
||||
.envs(build.environment)
|
||||
.env("FDS_TARGET_ARCH", &input.architecture)
|
||||
.stdin(Stdio::null()),
|
||||
let checkout = options.void_packages.canonicalize()?;
|
||||
if !checkout.join("xbps-src").is_file() {
|
||||
return Err(Error(
|
||||
"--void-packages must name a prepared Void source checkout".into(),
|
||||
));
|
||||
}
|
||||
let destination = checkout.join("srcpkgs").join(&input.source.package);
|
||||
if let Some(template) = input.source.template {
|
||||
let source = parent(recipe)?.join(template).canonicalize()?;
|
||||
if !source.join("template").is_file() {
|
||||
return Err(Error(
|
||||
"Source package directory must contain a Void template".into(),
|
||||
));
|
||||
}
|
||||
if source != destination {
|
||||
let tracked = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&checkout)
|
||||
.args(["ls-files", "--"])
|
||||
.arg(format!("srcpkgs/{}", input.source.package))
|
||||
.output()?;
|
||||
if !tracked.status.success() || !tracked.stdout.is_empty() {
|
||||
return Err(Error(
|
||||
"Custom source packages cannot replace tracked upstream Void files".into(),
|
||||
));
|
||||
}
|
||||
if destination.exists() {
|
||||
if !Command::new("diff")
|
||||
.args(["-qr", "--"])
|
||||
.arg(&source)
|
||||
.arg(&destination)
|
||||
.stdout(Stdio::null())
|
||||
.status()?
|
||||
.success()
|
||||
{
|
||||
return Err(Error(format!(
|
||||
"Stale generated source overlay {}; preserve local edits and remove that copy before rebuilding",
|
||||
destination.display()
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
tree::copy(&source, &destination)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !destination.join("template").is_file() {
|
||||
return Err(Error(format!(
|
||||
"Void source package {} has no template",
|
||||
input.source.package
|
||||
)));
|
||||
}
|
||||
// Source templates are trusted build code. Use normal xbps-src dependency
|
||||
// resolution and cross compilation, including its workstation build hooks.
|
||||
let mut source_build = Command::new(checkout.join("xbps-src"));
|
||||
source_build
|
||||
.args(["-f", "-a", "aarch64", "pkg", &input.source.package])
|
||||
.current_dir(&checkout)
|
||||
.env("XBPS_ARCH", std::env::consts::ARCH)
|
||||
.stdout(Stdio::from(std::io::stderr()));
|
||||
if let Some(directory) = &options.xbps_bin {
|
||||
let mut paths = vec![directory.canonicalize()?];
|
||||
paths.extend(std::env::split_paths(
|
||||
&std::env::var_os("PATH").unwrap_or_default(),
|
||||
));
|
||||
source_build.env(
|
||||
"PATH",
|
||||
std::env::join_paths(paths).map_err(|e| Error(e.to_string()))?,
|
||||
);
|
||||
}
|
||||
success(&mut source_build)?;
|
||||
let output_parent = parent(output)?;
|
||||
let work = Work::new(&output_parent)?;
|
||||
let staged = work.0.join("software");
|
||||
let root = staged.join("root");
|
||||
let config = work.0.join("config");
|
||||
let cache = work.0.join("cache");
|
||||
fs::create_dir_all(root.join("var/db/xbps/keys"))?;
|
||||
fs::create_dir(&config)?;
|
||||
fs::create_dir(&cache)?;
|
||||
for entry in fs::read_dir(checkout.join("common/repo-keys"))? {
|
||||
let entry = entry?;
|
||||
if entry.path().extension().is_some_and(|e| e == "plist") {
|
||||
fs::copy(
|
||||
entry.path(),
|
||||
root.join("var/db/xbps/keys").join(entry.file_name()),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
let mut install = xbps(options, "xbps-install");
|
||||
install
|
||||
.args(["-SyU", "--reproducible", "-i", "-C"])
|
||||
.arg(&config)
|
||||
.arg("-r")
|
||||
.arg(&root)
|
||||
.arg("-c")
|
||||
.arg(&cache)
|
||||
.args(["-R", "https://repo-default.voidlinux.org/current/aarch64"])
|
||||
.arg("-R")
|
||||
.arg(checkout.join("hostdir/binpkgs"))
|
||||
.arg(&input.source.package);
|
||||
// A supplied runner provides its own rootless XBPS environment. Otherwise
|
||||
// root exists only inside a user namespace with this staging area writable.
|
||||
if options.xbps_tool_runner.is_some() {
|
||||
success(install.stdout(Stdio::from(std::io::stderr())))?;
|
||||
} else {
|
||||
success(
|
||||
Command::new("bwrap")
|
||||
.args([
|
||||
"--unshare-user",
|
||||
"--uid",
|
||||
"0",
|
||||
"--gid",
|
||||
"0",
|
||||
"--ro-bind",
|
||||
"/",
|
||||
"/",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--bind",
|
||||
])
|
||||
.arg(&work.0)
|
||||
.arg(&work.0)
|
||||
.arg(install.get_program())
|
||||
.args(install.get_args())
|
||||
.envs(
|
||||
install
|
||||
.get_envs()
|
||||
.filter_map(|(key, value)| value.map(|value| (key, value))),
|
||||
)
|
||||
.stdout(Stdio::from(std::io::stderr())),
|
||||
)?;
|
||||
}
|
||||
let root = base.join(input.root).canonicalize()?;
|
||||
let output_parent = parent(output)?;
|
||||
if output_parent.starts_with(&root) {
|
||||
return Err(Error(
|
||||
"Software output must be outside its source tree".into(),
|
||||
));
|
||||
let list = xbps(options, "xbps-query")
|
||||
.arg("-r")
|
||||
.arg(&root)
|
||||
.arg("-l")
|
||||
.output()?;
|
||||
if !list.status.success() {
|
||||
return Err(Error("Cannot read installed XBPS package inventory".into()));
|
||||
}
|
||||
let work = Work::new(&output_parent)?;
|
||||
let bundle = work.0.join("bundle");
|
||||
fs::create_dir(&bundle)?;
|
||||
let archive_path = bundle.join(format!("{}.tar.xz", input.id));
|
||||
let encoded = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(0o644)
|
||||
.open(&archive_path)?;
|
||||
let mut child = Command::new("xz")
|
||||
.args(["--compress", "--stdout", "--threads=1", "-6"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::from(encoded))
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()?;
|
||||
let stats = archive::write_tar(&root, child.stdin.take().unwrap(), &input.architecture);
|
||||
if stats.is_err() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
let status = child.wait()?;
|
||||
let stats = stats?;
|
||||
if !status.success() {
|
||||
return Err(Error("XZ compression failed".into()));
|
||||
}
|
||||
let file = archive::open(&archive_path)?;
|
||||
let mut packages: Vec<_> = String::from_utf8_lossy(&list.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| line.split_whitespace().nth(1).map(str::to_owned))
|
||||
.collect();
|
||||
packages.sort();
|
||||
packages.dedup();
|
||||
tree::relocate(&root)?;
|
||||
let inventory = tree::inspect(&root, "aarch64")?;
|
||||
let software = Software {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
version: input.version,
|
||||
architecture: input.architecture,
|
||||
architecture: "aarch64".into(),
|
||||
partition: 2,
|
||||
archive_bytes: file.metadata()?.len(),
|
||||
unpacked_bytes: stats.bytes,
|
||||
entries: stats.entries,
|
||||
sha256: archive::digest(&file)?,
|
||||
installed: true,
|
||||
packages,
|
||||
archive_bytes: 0,
|
||||
unpacked_bytes: inventory.bytes,
|
||||
entries: inventory.entries,
|
||||
sha256: inventory.sha256,
|
||||
commands: input.commands,
|
||||
};
|
||||
archive::verify(&file, &software, None)?;
|
||||
let descriptor = toml::to_string(&software).map_err(|e| Error(e.to_string()))?;
|
||||
fs::write(bundle.join("software.toml"), descriptor)?;
|
||||
fs::set_permissions(&bundle, fs::Permissions::from_mode(0o755))?;
|
||||
// renameat2(NO_REPLACE) publishes the complete directory without overwriting.
|
||||
let from = std::ffi::CString::new(bundle.as_os_str().as_encoded_bytes())
|
||||
tree::verify(&root, &software)?;
|
||||
fs::write(
|
||||
staged.join("software.toml"),
|
||||
toml::to_string(&software).map_err(|e| Error(e.to_string()))?,
|
||||
)?;
|
||||
fs::set_permissions(&staged, fs::Permissions::from_mode(0o755))?;
|
||||
let from = std::ffi::CString::new(staged.as_os_str().as_encoded_bytes())
|
||||
.map_err(|e| Error(e.to_string()))?;
|
||||
let to = std::ffi::CString::new(output.as_os_str().as_encoded_bytes())
|
||||
.map_err(|e| Error(e.to_string()))?;
|
||||
@@ -136,17 +266,14 @@ pub fn build(recipe: &Path, output: &Path, compile: bool) -> Result<Software> {
|
||||
}
|
||||
pub fn load(directory: &Path) -> Result<Software> {
|
||||
let path = directory.join("software.toml");
|
||||
let metadata = fs::symlink_metadata(&path)?;
|
||||
if !metadata.is_file() {
|
||||
if !path.symlink_metadata()?.is_file() {
|
||||
return Err(Error("Software descriptor must be a regular file".into()));
|
||||
}
|
||||
let software: Software = toml::from_str(&read_text(&path, 65536)?)
|
||||
.map_err(|e| Error(format!("Invalid software descriptor: {e}")))?;
|
||||
software.validate()?;
|
||||
archive::verify(
|
||||
&archive::open(&directory.join(format!("{}.tar.xz", software.id)))?,
|
||||
&software,
|
||||
None,
|
||||
)?;
|
||||
if !software.installed {
|
||||
return Err(Error("New cartridges require installed Void packages; rebuild this legacy bundle from its source template".into()));
|
||||
}
|
||||
tree::verify(&directory.join("root"), &software)?;
|
||||
Ok(software)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user