use std::collections::{BTreeMap, HashSet};
use std::ffi::{OsStr, OsString};
use std::io;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use onelf_format::cache_layout as layout;
use onelf_format::resolve::{self, Gl, Request, Resolution};
use onelf_format::{HostLibsPolicy, WorkingDir, drivers};
use crate::assembly;
use crate::pack::HostLibs;
use crate::recipe;
struct Loaded {
path: PathBuf,
recipe: recipe::Recipe,
}
struct Assembled {
request: assembly::AssemblyRequest,
prepared: Vec<crate::bundle::PreparedPin>,
}
fn assemble_for_launch(loaded: Loaded) -> io::Result<Assembled> {
onelf_rt::platform::supply_https_fetch(https_fetch);
let request = assembly::AssemblyRequest::plan(&loaded.path, loaded.recipe, None)?;
let report = request.stage()?;
Ok(Assembled {
request,
prepared: report.prepared,
})
}
fn carry_rules(records: Vec<onelf_format::runtime_metadata::RuntimeRuleRecord>) -> io::Result<()> {
let invalid = |e: onelf_format::runtime_metadata::MetadataError| {
io::Error::new(io::ErrorKind::InvalidData, format!("runtime rules: {e}"))
};
let lowered = crate::pack::lower_runtime_metadata(records, None).map_err(invalid)?;
if let Some(bytes) = lowered.runtime_rules {
let decoded =
onelf_format::runtime_metadata::RuntimeRuleMetadata::decode(&bytes).map_err(invalid)?;
onelf_rt::env::carry_rules(decoded);
}
Ok(())
}
fn https_fetch(url: &str, into: &mut std::fs::File, max: u64) -> Result<(), String> {
let response = ureq::get(url).call().map_err(|e| format!("{url}: {e}"))?;
let copied = io::copy(
&mut io::Read::take(response.into_body().into_reader(), max.saturating_add(1)),
into,
)
.map_err(|e| format!("{url}: {e}"))?;
if copied > max {
return Err(format!(
"{url}: the body is larger than the {max}-byte bound a staged build may take"
));
}
Ok(())
}
pub fn run(
app_spec: &Path,
command_override: Option<&str>,
entrypoint: Option<&str>,
run_bundle: bool,
passthrough_args: &[String],
) -> io::Result<()> {
onelf_rt::hostenv::capture_starting_env();
let (dir, mut found) = resolve_app(app_spec)?;
let dir = dir.canonicalize()?;
if let Some(loaded) = &found {
assembly::refuse_library_search_env(&loaded.path, &loaded.recipe.env)?;
let declared: Vec<(String, String)> = loaded
.recipe
.paths
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
crate::pack::paths_record(&crate::pack::path_pairs(&declared))
.map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", loaded.path.display())))?;
}
let mut assembled: Option<Assembled> = None;
if run_bundle {
assembled = match found.take() {
Some(loaded) => Some(assemble_for_launch(loaded)?),
None => {
eprintln!("onelf run: --bundle skipped: no recipe found in AppDir");
None
}
};
}
let (ep_name, ep_path, ep_args, working_dir) = match &assembled {
Some(a) => request_entrypoint(&a.request, command_override, entrypoint)?,
None => resolve_entrypoint(
found.as_ref().map(|l| &l.recipe),
&dir,
command_override,
entrypoint,
)?,
};
let target = dir.join(&ep_path);
if !target.exists() {
return Err(missing_target_error(
&dir,
&ep_path,
found.as_ref().map(|l| &l.recipe),
));
}
if let Some(loaded) = &found {
refuse_unacquired_sets(&loaded.recipe, &ep_name)?;
}
let lib_dirs = detect_lib_dirs(&dir);
let lib_subpath = lib_dirs
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(":");
let target_is_elf = is_elf_file_at(&target);
let policy = match &assembled {
Some(a) => a.request.package.host_libs,
None => found
.as_ref()
.and_then(|l| l.recipe.package.host_libs)
.map(HostLibs::from)
.unwrap_or(HostLibs::Auto),
};
let policy = match policy {
HostLibs::Always => HostLibsPolicy::Always,
HostLibs::Never => HostLibsPolicy::Never,
HostLibs::Auto => HostLibsPolicy::Auto,
};
let disabled = std::env::var_os("ONELF_NO_RESOLVER").is_some_and(|v| !v.is_empty() && v != "0");
let lib_dir_strs: Vec<&str> = lib_dirs.iter().filter_map(|p| p.to_str()).collect();
let mut set_roots: Vec<PathBuf> = Vec::new();
let mut set_lib_dirs: Vec<PathBuf> = Vec::new();
let mut pinned_gl: Option<(PathBuf, std::fs::File)> = None;
if let Some(a) = &assembled {
let selection = selected_sets(&a.request, &ep_name)?;
acquire_sets(&dir, selection, &ep_name)?;
set_roots = onelf_rt::sets::roots();
set_lib_dirs = set_roots
.iter()
.flat_map(|root| onelf_rt::sets::lib_dirs(root))
.collect();
if target_is_elf && !lib_dirs.is_empty() && !disabled && policy != HostLibsPolicy::Never {
pinned_gl = obtain_gl_pin(&dir, &lib_dir_strs, &set_lib_dirs, &target);
}
}
let resolution = match run_store(&dir) {
Some(store) if target_is_elf && !lib_dirs.is_empty() && !disabled => {
resolve::resolve_selected(
&Request {
pkg_root: &dir,
lib_dirs: &lib_dir_strs,
plugin_dirs: &[],
policy,
store: &store,
ld_cache: &drivers::cache_file(),
icd_dirs: resolve::ICD_DIRS,
extra_root: pinned_gl.as_ref().map(|(root, _)| root.as_path()),
set_roots: &set_roots,
host_libc: false,
loader_store: resolve::LoaderStore::Refused(
"a development run never takes the host's glibc",
),
nsswitch: resolve::nsswitch_file(),
uid: rustix::process::getuid().as_raw(),
trust: None,
},
&onelf_rt::sets::pins(),
)
}
_ => Resolution::default(),
};
for name in &resolution.incomparable {
eprintln!("onelf run: resolver: cannot order {name}; keeping the bundled copy");
}
let runpath_dirs = onelf_rt::links::origin_runpath(&target);
let link_dir = onelf_rt::links::dir_where(&dir, &runpath_dirs, &lib_dir_strs, |rel| {
dir.join(rel).is_dir()
});
let links = match &link_dir {
Some(rel) => onelf_rt::links::wanted_from(
resolution.farm.as_deref(),
&set_roots,
&carried_names(&dir.join(rel)),
),
None => onelf_rt::links::Links::new(),
};
let shadows = shadowed_copies(&dir, &lib_dir_strs, resolution.farm.as_deref());
let (recipe_env, declared_paths, declared_preload, needs_setuid) = match &assembled {
Some(a) => (
a.request.env.clone(),
a.request.paths.clone(),
a.request.preload.clone(),
a.request.package.needs_setuid,
),
None => match found.as_ref() {
Some(l) => (
l.recipe
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
l.recipe
.paths
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
l.recipe.preload.clone(),
l.recipe.package.needs_setuid,
),
None => (Vec::new(), Vec::new(), Vec::new(), false),
},
};
let env_data = env_bytes(&recipe_env);
let carried = match (&assembled, &found) {
(Some(a), _) => a.request.bundle.rules.runtime_records(&a.prepared)?,
(None, Some(l)) => {
let recipe_dir = l.path.parent().unwrap_or(Path::new("."));
crate::rules::RuleCatalog::load(
&l.path,
recipe_dir,
&l.recipe.bundle.rules,
&l.recipe.bundle.disable_rules,
)?
.runtime_records(&[])?
}
(None, None) => None,
};
if let Some(records) = carried {
carry_rules(records)?;
}
let argv0 = match target.to_str() {
Some(path) if target.file_name() == Some(ep_name.as_ref()) => path,
_ => ep_name.as_str(),
};
let base_env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
let dir_str = dir.to_string_lossy().into_owned();
let target_str = target.to_string_lossy().into_owned();
let _ = onelf_rt::env::setup_env(
&dir_str,
argv0,
&target_str,
&ep_name,
"dev",
&lib_subpath,
&target_str,
resolution.farm.as_deref(),
pinned_gl.as_ref().map(|(root, _)| root.as_path()),
&set_lib_dirs,
&set_roots,
&[],
);
onelf_rt::env::apply_custom_env(&env_data, &dir_str);
onelf_rt::hostenv::record_host_env(&dir, &set_roots);
let applied_env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
let assignments =
onelf_rt::env::view_env_assignments(&dir, &lib_dir_strs, &set_lib_dirs, &set_roots, &[]);
let view_record = onelf_rt::env::encode_view_env(&base_env, &applied_env, &assignments)
.map_err(|e| io::Error::other(format!("the generated environment input is {e}")))?;
restore_env(&base_env, &applied_env);
let tree_id = tree_digest(&dir, &shadows)?;
let pins = onelf_rt::sets::pins();
let inputs = onelf_rt::links::view::ViewInputs {
uid: rustix::process::getuid().as_raw(),
package: &tree_id,
metadata: None,
pins: &pins,
policy,
host_libc: false,
loader: resolution.host_interp.as_ref().map(|l| l.path.as_os_str()),
entrypoint: Some(ep_name.as_str()),
links: &links,
runtime_env: &view_record,
root: dir.as_os_str(),
};
let identity = onelf_rt::links::view::ViewIdentity::new(&inputs).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"this launch's generated environment input is beyond what a development view records",
)
})?;
let store = view_store()?;
let view_name = onelf_rt::links::view::dir_name(identity.key()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"the view identity names no directory",
)
})?;
let view = store.join(view_name);
let lease_path = onelf_rt::links::view::lease_lock_path(&view)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "the view names no lease"))?;
let lease = onelf_rt::paths::open_lock_inheritable(&lease_path)?;
rustix::fs::flock(&lease, rustix::fs::FlockOperation::LockShared)
.map_err(|e| io::Error::other(format!("the view lease: {e}")))?;
let path_pairs = crate::pack::path_pairs(&declared_paths);
let published = onelf_rt::links::view::ensure_view(&store, &identity, &lease, |staging| {
stage_tree(staging, &dir)?;
for (rel, host) in &shadows {
let staged = staging.dir().join(rel);
std::fs::remove_file(&staged)?;
std::os::unix::fs::symlink(host, &staged)?;
}
let expanded = expand_view_roots(&view_record, dir.as_os_str(), view.as_os_str());
staging.write_file(onelf_rt::env::VIEW_ENV_FILE, &expanded, 0o644)?;
if let Some(rel) = &link_dir {
staging.place_free_links(rel, &links)?;
}
if needs_setuid {
staging.write_file(".onelf/needs-setuid", b"1\n", 0o644)?;
}
Ok(Some(expanded))
})?;
let view_root = published.dir().to_path_buf();
let view_str = view_root.to_string_lossy().into_owned();
let view_target = view_root.join(&ep_path);
let view_target_str = view_target.to_string_lossy().into_owned();
let lib_path = onelf_rt::env::setup_env(
&view_str,
argv0,
&view_target_str,
&ep_name,
"dev",
&lib_subpath,
&view_target_str,
resolution.farm.as_deref(),
pinned_gl.as_ref().map(|(root, _)| root.as_path()),
&set_lib_dirs,
&set_roots,
&[],
);
onelf_rt::env::apply_custom_env(&env_data, &view_str);
onelf_rt::hostenv::record_host_env(&view_root, &set_roots);
if !declared_preload.is_empty() {
eprintln!(
"onelf run: the recipe's preload list is opened by the packaged constructor, \
which a development launch does not carry"
);
}
eprintln!(
"onelf run: a development launch prepares the initial environment only; \
a re-exec that clears the environment is not hooked"
);
if !declared_paths.is_empty() {
if onelf_rt::hostpaths::apply(&view_root, &path_pairs, &std::collections::BTreeMap::new()) {
eprintln!(
"onelf run: the view's directories answer for the absolute paths the recipe built with"
);
} else {
eprintln!(
"onelf run: the recipe's absolute paths are not overlaid: this launch could not \
make the view's directories answer for them, so the application reads the host's"
);
}
}
let requested_cwd = match working_dir {
WorkingDir::PackageRoot => Some(view_root.clone()),
WorkingDir::EntrypointParent => view_target.parent().map(Path::to_path_buf),
WorkingDir::Inherit => None,
};
let kernel_lib_path =
onelf_rt::launch::kernel_library_path(&view_root, &lib_dir_strs, &lib_path, &runpath_dirs);
let mut cmd = build_exec_command(
&view_target,
&view_root,
&lib_path,
kernel_lib_path.as_deref(),
argv0,
resolution.host_interp.as_ref().map(|l| l.path.as_path()),
)?;
cmd.args(&ep_args);
cmd.args(passthrough_args);
if let Some(c) = requested_cwd {
cmd.env(
"ONELF_USER_CWD",
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
);
cmd.current_dir(c);
}
let _ = std::os::fd::IntoRawFd::into_raw_fd(lease);
let err = cmd.exec();
Err(io::Error::other(format!("exec failed: {err}")))
}
fn env_bytes(env: &[(String, String)]) -> Vec<u8> {
let mut data = Vec::new();
for (key, value) in env {
data.extend_from_slice(format!("{key}={value}\n").as_bytes());
}
data
}
fn carried_names(dir: &Path) -> HashSet<String> {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| e.file_name().to_str().map(String::from))
.collect()
}
fn restore_env(base: &BTreeMap<OsString, OsString>, applied: &BTreeMap<OsString, OsString>) {
for (name, value) in base {
if applied.get(name) != Some(value) {
unsafe { std::env::set_var(name, value) };
}
}
for name in applied.keys() {
if !base.contains_key(name) {
unsafe { std::env::remove_var(name) };
}
}
}
fn shadowed_copies(dir: &Path, lib_dirs: &[&str], farm: Option<&Path>) -> Vec<(String, PathBuf)> {
let Some(entries) = farm.and_then(|farm| std::fs::read_dir(farm).ok()) else {
return Vec::new();
};
let mut shadows = Vec::new();
for entry in entries.flatten() {
let (Ok(host), Some(name)) = (
std::fs::read_link(entry.path()),
entry.file_name().to_str().map(String::from),
) else {
continue;
};
for lib in lib_dirs {
let rel = format!("{lib}/{name}");
if dir.join(&rel).is_file() {
shadows.push((rel, host.clone()));
}
}
}
shadows.sort();
shadows
}
fn tree_digest(root: &Path, shadows: &[(String, PathBuf)]) -> io::Result<[u8; 32]> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let mut hasher = blake3::Hasher::new();
for (rel, host) in shadows {
hasher.update(b"s");
hasher.update(rel.as_bytes());
hasher.update(b"\0");
hasher.update(host.as_os_str().as_encoded_bytes());
hasher.update(b"\0");
}
for entry in jwalk::WalkDir::new(root).skip_hidden(false).sort(true) {
let entry = entry.map_err(|e| io::Error::other(e.to_string()))?;
let path = entry.path();
let rel = path
.strip_prefix(root)
.map_err(|_| io::Error::other("the staged tree escapes its root"))?;
hasher.update(rel.as_os_str().as_encoded_bytes());
hasher.update(b"\0");
let kind = entry.file_type();
if kind.is_dir() {
hasher.update(b"d");
} else if kind.is_symlink() {
hasher.update(b"l");
let target = std::fs::read_link(entry.path())?;
hasher.update(target.as_os_str().as_encoded_bytes());
hasher.update(b"\0");
} else if kind.is_file() {
hasher.update(b"f");
let md = std::fs::symlink_metadata(entry.path())?;
hasher.update(&md.permissions().mode().to_le_bytes());
hasher.update(&md.len().to_le_bytes());
hasher.update(&md.mtime().to_le_bytes());
hasher.update(&md.mtime_nsec().to_le_bytes());
hasher.update(&md.dev().to_le_bytes());
hasher.update(&md.ino().to_le_bytes());
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{}: not a regular file, directory or symlink",
entry.path().display()
),
));
}
}
Ok(*hasher.finalize().as_bytes())
}
fn stage_tree(staging: &onelf_rt::links::view::Staging, root: &Path) -> io::Result<()> {
for entry in jwalk::WalkDir::new(root).skip_hidden(false).sort(true) {
let entry = entry.map_err(|e| io::Error::other(e.to_string()))?;
let path = entry.path();
let rel = path
.strip_prefix(root)
.map_err(|_| io::Error::other("the staged tree escapes its root"))?;
let rel = rel.to_str().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{}: not a UTF-8 path", path.display()),
)
})?;
if rel.is_empty() {
continue;
}
let kind = entry.file_type();
if kind.is_dir() {
std::fs::create_dir_all(staging.dir().join(rel))?;
} else if kind.is_symlink() {
let target = std::fs::read_link(entry.path())?;
let target = target.to_str().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{}: not a UTF-8 symlink target", entry.path().display()),
)
})?;
staging.symlink(rel, target)?;
} else if kind.is_file() {
clone_file(&entry.path(), &staging.dir().join(rel))?;
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{}: not a regular file, directory or symlink",
entry.path().display()
),
));
}
}
Ok(())
}
fn clone_file(source: &Path, dest: &Path) -> io::Result<()> {
let from = std::fs::File::open(source)?;
let mut to = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(dest)?;
if rustix::fs::ioctl_ficlone(&to, &from).is_err() {
io::copy(&mut &from, &mut to)?;
}
to.set_permissions(from.metadata()?.permissions())
}
fn expand_view_roots(bytes: &[u8], from: &OsStr, to: &OsStr) -> Vec<u8> {
let (from, to) = (from.as_encoded_bytes(), to.as_encoded_bytes());
if from.is_empty() || from == to {
return bytes.to_vec();
}
let mut out = Vec::with_capacity(bytes.len());
let mut rest = bytes;
while let Some(at) = rest.windows(from.len()).position(|w| w == from) {
out.extend_from_slice(&rest[..at]);
out.extend_from_slice(to);
rest = &rest[at + from.len()..];
}
out.extend_from_slice(rest);
out
}
fn view_store() -> io::Result<PathBuf> {
let root = layout::resolve_root(rustix::process::getuid().as_raw(), None).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"no safe cache directory (set XDG_CACHE_HOME or HOME)",
)
})?;
let store = root.join("views");
use std::os::unix::fs::DirBuilderExt;
match std::fs::DirBuilder::new().mode(0o700).create(&store) {
Ok(()) => Ok(store),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(store),
Err(e) => Err(e),
}
}
fn missing_target_error(dir: &Path, ep_path: &str, staged: Option<&recipe::Recipe>) -> io::Error {
let target = dir.join(ep_path);
let stages = staged.is_some_and(|r| r.input.is_some() || r.sysroot.is_some());
let hint = if stages {
" (the recipe stages its contents: run with --bundle, or build first)"
} else {
""
};
io::Error::new(
io::ErrorKind::NotFound,
format!("entrypoint target not found: {}{hint}", target.display()),
)
}
fn refuse_unacquired_sets(recipe: &recipe::Recipe, ep_name: &str) -> io::Result<()> {
let selections = recipe::SetSelections::normalize(recipe)?;
let selected: Vec<&str> = match selections.named.get(ep_name).unwrap_or(&selections.primary) {
recipe::SetSelection::All => recipe
.sysroot
.iter()
.flat_map(|sr| sr.sets.keys().map(String::as_str))
.collect(),
recipe::SetSelection::Names(names) => names.iter().map(String::as_str).collect(),
};
if selected.is_empty() {
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"entrypoint '{ep_name}' selects the sets {}, which only an assembled launch \
acquires; without them their libraries would come from the host. Run with \
--bundle, or build the package",
selected.join(", ")
),
))
}
fn request_entrypoint(
request: &assembly::AssemblyRequest,
command_override: Option<&str>,
entrypoint: Option<&str>,
) -> io::Result<(String, String, Vec<String>, WorkingDir)> {
let working_dir = request.package.working_dir;
if let Some(cmd) = command_override {
let name = Path::new(cmd)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("app")
.to_string();
return Ok((name, cmd.to_string(), Vec::new(), working_dir));
}
if let Some(name) = entrypoint {
let ep = request
.entrypoints
.iter()
.find(|e| e.name == name)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("entrypoint '{name}' not defined in recipe"),
)
})?;
return Ok((
ep.name.clone(),
ep.path.clone(),
ep.args.clone(),
working_dir,
));
}
if let Some(default) = &request.default_entrypoint
&& let Some(ep) = request.entrypoints.iter().find(|e| &e.name == default)
{
return Ok((
ep.name.clone(),
ep.path.clone(),
ep.args.clone(),
working_dir,
));
}
let name = request.package.name.clone().unwrap_or_else(|| {
request
.package
.command
.rsplit('/')
.next()
.unwrap_or("app")
.to_string()
});
Ok((
name,
request.package.command.clone(),
Vec::new(),
working_dir,
))
}
fn selected_sets(
request: &assembly::AssemblyRequest,
ep_name: &str,
) -> io::Result<recipe::SetSelection> {
let row = request
.selections
.iter()
.find(|row| row.name == ep_name)
.unwrap_or(&request.selections[0]);
Ok(row.sets.clone())
}
fn acquire_sets(dir: &Path, selection: recipe::SetSelection, ep_name: &str) -> io::Result<()> {
let pins = match std::fs::read_to_string(dir.join(onelf_rt::sets::SETS_FILE)) {
Ok(text) => onelf_rt::sets::parse(&text),
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
let selected = match selection {
recipe::SetSelection::All => pins,
recipe::SetSelection::Names(names) => {
let mut out = Vec::with_capacity(names.len());
for name in names {
let found = pins.iter().find(|p| p.label == name).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("entrypoint '{ep_name}': set '{name}' is not declared"),
)
})?;
out.push(found.clone());
}
out
}
};
onelf_rt::sets::init(&selected, ep_name);
Ok(())
}
fn obtain_gl_pin(
dir: &Path,
lib_dirs: &[&str],
set_lib_dirs: &[PathBuf],
target: &Path,
) -> Option<(PathBuf, std::fs::File)> {
let situation = onelf_format::resolve::gl_situation(
dir,
lib_dirs,
set_lib_dirs,
Some(target),
&drivers::cache_file(),
resolve::ICD_DIRS,
);
if !situation.lacking() {
return None;
}
match onelf_rt::platform::obtain(dir) {
Some(Ok(fetched)) => Some(fetched),
Some(Err(why)) => {
eprintln!(
"onelf run: this host has no GL stack and the pinned build cannot be used \
({why}); continuing without one"
);
None
}
None => {
if situation == Gl::Missing {
eprintln!(
"onelf run: this host has no GL stack, which the entrypoint links, and \
the recipe pins no GL build; continuing without one"
);
}
None
}
}
}
fn resolve_app(spec: &Path) -> io::Result<(PathBuf, Option<Loaded>)> {
if spec.is_dir() {
let toml = spec.join("onelf.toml");
let loaded = toml
.is_file()
.then(|| recipe::load(&toml).map(|recipe| Loaded { path: toml, recipe }))
.transpose()?;
return Ok((spec.to_path_buf(), loaded));
}
if spec.is_file() && spec.extension().and_then(|s| s.to_str()) == Some("toml") {
let recipe = recipe::load(spec)?;
let dir = spec
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
return Ok((
dir,
Some(Loaded {
path: spec.to_path_buf(),
recipe,
}),
));
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{}: expected an AppDir (directory) or an onelf.toml file",
spec.display()
),
))
}
fn resolve_entrypoint(
recipe: Option<&recipe::Recipe>,
dir: &Path,
command_override: Option<&str>,
entrypoint: Option<&str>,
) -> io::Result<(String, String, Vec<String>, WorkingDir)> {
let working_dir: WorkingDir = recipe
.map(|r| r.package.working_dir.into())
.unwrap_or(WorkingDir::Inherit);
if let Some(cmd) = command_override {
let name = Path::new(cmd)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("app")
.to_string();
return Ok((name, cmd.to_string(), Vec::new(), working_dir));
}
if let Some(r) = recipe {
if let Some(name) = entrypoint {
let ep = r
.entrypoint
.iter()
.find(|e| e.name == name)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("entrypoint '{name}' not defined in recipe"),
)
})?;
return Ok((
ep.name.clone(),
ep.path.clone(),
ep.args.clone(),
working_dir,
));
}
if let Some(ep) = r.entrypoint.iter().find(|e| e.default) {
return Ok((
ep.name.clone(),
ep.path.clone(),
ep.args.clone(),
working_dir,
));
}
let name = r.package.name.clone().unwrap_or_else(|| {
r.package
.command
.rsplit('/')
.next()
.unwrap_or("app")
.to_string()
});
return Ok((name, r.package.command.clone(), Vec::new(), working_dir));
}
if entrypoint.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"--entrypoint requires an onelf.toml in the AppDir",
));
}
let bin = dir.join("bin");
if !bin.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"{}: no onelf.toml and no bin/ subdirectory; pass --command PATH",
dir.display()
),
));
}
let entries: Vec<PathBuf> = std::fs::read_dir(&bin)?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file())
.collect();
if entries.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{}: no executables found", bin.display()),
));
}
if entries.len() > 1 {
let names: Vec<String> = entries
.iter()
.filter_map(|p| p.file_name().and_then(|n| n.to_str()).map(String::from))
.collect();
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{}: multiple binaries found; pass --command bin/NAME (have: {})",
bin.display(),
names.join(", ")
),
));
}
let bin_path = &entries[0];
let name = bin_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("app")
.to_string();
let rel = format!("bin/{name}");
Ok((name, rel, Vec::new(), working_dir))
}
fn build_exec_command(
target: &Path,
app_dir: &Path,
lib_path: &str,
kernel_lib_path: Option<&str>,
argv0: &str,
host_interp: Option<&Path>,
) -> io::Result<Command> {
let interp = read_elf_interp(target);
if let Some(host_interp) = host_interp {
if interp.is_none() {
let mut cmd = Command::new(target);
cmd.arg0(argv0).env("ONELF_INTERP", host_interp);
if let Some(path) = &kernel_lib_path {
cmd.env("LD_LIBRARY_PATH", path);
}
return Ok(cmd);
}
let mut cmd = Command::new(host_interp);
cmd.arg("--inhibit-cache");
if !lib_path.is_empty() {
cmd.arg("--library-path").arg(lib_path);
}
cmd.arg("--argv0").arg(argv0).arg(target);
return Ok(cmd);
}
if let Some(interp) = interp {
let interp_path = Path::new(&interp);
if let Some(bundled) = find_bundled_interp(&interp, app_dir) {
let is_musl = interp_path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("ld-musl-"));
let mut cmd = Command::new(&bundled);
if !is_musl {
cmd.arg("--inhibit-cache");
}
if !lib_path.is_empty() {
cmd.arg("--library-path").arg(lib_path);
}
cmd.arg("--argv0").arg(argv0).arg(target);
return Ok(cmd);
}
if !interp_path.exists() {
eprintln!(
"warning: ELF interpreter {} not found on host and no bundled \
equivalent in the AppDir; exec will likely fail",
interp
);
}
}
let mut cmd = Command::new(target);
cmd.arg0(argv0);
if let Some(path) = &kernel_lib_path {
cmd.env("LD_LIBRARY_PATH", path);
}
Ok(cmd)
}
fn read_elf_interp(path: &Path) -> Option<String> {
let data = std::fs::read(path).ok()?;
goblin::elf::Elf::parse(&data)
.ok()?
.interpreter
.map(|s| s.trim_end_matches('\0').to_string())
}
fn find_bundled_interp(interp: &str, app_dir: &Path) -> Option<PathBuf> {
let name = Path::new(interp).file_name()?.to_os_string();
for rel in detect_lib_dirs(app_dir) {
let candidate = app_dir.join(&rel).join(&name);
if candidate.exists() {
return Some(candidate);
}
}
None
}
fn is_elf_file_at(path: &Path) -> bool {
use std::io::Read;
let Ok(mut f) = std::fs::File::open(path) else {
return false;
};
let mut buf = [0u8; 4];
matches!(f.read(&mut buf), Ok(4)) && buf == *b"\x7fELF"
}
fn run_store(dir: &Path) -> Option<PathBuf> {
let root = layout::resolve_root(rustix::process::getuid().as_raw(), None)?;
let id = blake3::hash(dir.as_os_str().as_encoded_bytes()).to_hex();
Some(root.join("run").join(&id[..16]))
}
fn detect_lib_dirs(dir: &Path) -> Vec<PathBuf> {
let mut lib_dirs: Vec<PathBuf> = Vec::new();
for entry in jwalk::WalkDir::new(dir).skip_hidden(false).sort(true) {
let Ok(entry) = entry else { continue };
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.contains(".so") {
continue;
}
let Some(parent) = path.parent() else {
continue;
};
let Ok(rel) = parent.strip_prefix(dir) else {
continue;
};
let rel_str = rel.to_string_lossy();
if rel_str.is_empty()
|| rel_str.starts_with(".onelf")
|| rel_str.starts_with("share/")
|| rel_str == "bin"
|| rel_str.starts_with("bin/")
|| rel_str.starts_with("etc/")
{
continue;
}
let rel_path = rel.to_path_buf();
if !lib_dirs.contains(&rel_path) {
lib_dirs.push(rel_path);
}
}
lib_dirs
}
#[cfg(test)]
mod selection_tests {
use super::selected_sets;
use crate::assembly::AssemblyRequest;
use crate::recipe::{self, SetSelection};
const HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
fn temp_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"onelf-run-selection-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn request(dir: &std::path::Path, body: &str) -> AssemblyRequest {
let path = dir.join("onelf.toml");
std::fs::write(&path, body).unwrap();
let recipe = recipe::load(&path).unwrap();
AssemblyRequest::plan(&path, recipe, None).unwrap()
}
#[test]
fn selection_scopes_resolve_like_a_builds_table() {
let dir = temp_dir("scopes");
let body = format!(
"[package]\ncommand = \"bin/app\"\n\n[bundle]\nskip = true\n\n\
[sysroot.sets.toolkit]\npackages = [\"t\"]\nurl = \"https://e/t.onelf\"\n\
blake3 = \"{HASH}\"\n\n\
[[entrypoint]]\nname = \"cli\"\npath = \"bin/app\"\nsets = []\ndefault = true\n\n\
[[entrypoint]]\nname = \"gui\"\npath = \"bin/app\"\nsets = [\"toolkit\"]\n"
);
let request = request(&dir, &body);
assert_eq!(
selected_sets(&request, "gui").unwrap(),
SetSelection::Names(vec!["toolkit".to_string()])
);
assert_eq!(
selected_sets(&request, "cli").unwrap(),
SetSelection::Names(Vec::new())
);
assert_eq!(
selected_sets(&request, "elsewhere").unwrap(),
SetSelection::Names(Vec::new())
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_package_command_carries_the_primary_selection() {
let dir = temp_dir("primary");
let body = format!(
"[package]\ncommand = \"bin/app\"\n\n\
[sysroot.sets.toolkit]\npackages = [\"t\"]\nurl = \"https://e/t.onelf\"\n\
blake3 = \"{HASH}\"\n"
);
let request = request(&dir, &body);
assert_eq!(selected_sets(&request, "app").unwrap(), SetSelection::All);
std::fs::remove_dir_all(&dir).unwrap();
}
}