ODUDDQRY373JMDR6W2BHUWSKEU6TEICQMNZIAIFKJPNUA5UN3C4QC
UWQB743KR36X6A6JVNK3VH6KMEFUHOUT6ZS2Y2SWIJR55JSF5WQAC
FRNJITN6RQ2HFEEQX6EKZVZ7PYT2JAIGZX5J25LIVLPV7OLUNYFQC
VR4QJBTN4467XRJIZ6YYN64YEY5IRCJBRP7X5LCIBCARYYE76TBQC
E6W7X7U3IC5542LG2KFLCFIVWZS4BSNT4KF7FRKCM6XVZTN3CNJAC
AR2X3TLAWGOJH4LY5MHR37V7LP5K6F7LA7CGOKO2FAS7IOTHLXJAC
5PJAXSZ7AV7WEGMHTKWFOYWN5YUFRFZDZUXNIMLRPRTADZQEN4JAC
ZV57M4VN53TJFCOLDOP7I4M4VAEN667SA37LR2HFFKFN5G2MTWIAC
Z43EQIWNBMXB4VUXQGLJWFGVO6ZJNRL5I7HRY2622LQVYBBTUHAAC
pub fn bind(source: &str, target: &str) -> Result<Mount, std::io::Error> {
let target = std::ffi::CString::new(target).unwrap();
pub fn bind<S: AsRef<std::path::Path>, T: AsRef<std::path::Path>>(
source: S,
target: T,
) -> Result<Mount, std::io::Error> {
let target = target.as_ref().to_str().unwrap();
let target = CString::new(target).unwrap();
let source = source.as_ref().to_str().unwrap();
let source = CString::new(source).unwrap();
error!("err {:?}", err);
if err == 0 {
return Ok(Mount {
target: Some(target),
});
}
Err(std::io::Error::last_os_error())
}
}
pub fn ramfs<T: AsRef<std::path::Path>>(target: T) -> Result<Mount, std::io::Error> {
let target = target.as_ref().to_str().unwrap();
let target = CString::new(target).unwrap();
unsafe {
let err = mount(
b"ramfs\0".as_ptr() as *const i8,
target.as_ptr(),
b"ramfs\0".as_ptr() as *const i8,
0,
std::ptr::null(),
);
request: tonic::Request<DerivationRequest>,
) -> Result<tonic::Response<DerivationReply>, tonic::Status> {
debug!("Got a request from {:?}", request.remote_addr());
request: tonic::Request<tonic::Streaming<proto::AddPathRequest>>,
) -> Result<tonic::Response<proto::DerivationReply>, tonic::Status> {
let mut r = request.into_inner();
let mut current_file = None;
let ref store = self.deb_client.store_path;
let tmp_dir = tempfile::tempdir_in(store)?;
let mut hasher = blake3::Hasher::new();
// let reply = hello_world::HelloReply {
// message: format!("Hello {}!", request.into_inner().name),
// };
// Ok(Response::new(reply))
unimplemented!()
loop {
trace!("waiting for next in stream");
let Some(r) = r.next().await else { break };
let r = r.unwrap();
match r.request {
Some(proto::add_path_request::Request::File(f)) => {
hasher.update(b"\0f");
hasher.update(f.name.as_bytes());
hasher.update(b"\0");
let p = tmp_dir.path().join(&f.name);
tokio::fs::create_dir_all(p.parent().unwrap()).await?;
current_file = Some(tokio::fs::File::create(&p).await?)
}
Some(proto::add_path_request::Request::Directory(d)) => {
hasher.update(b"\0d");
hasher.update(d.name.as_bytes());
hasher.update(b"\0");
let p = tmp_dir.path().join(&d.name);
tokio::fs::create_dir_all(&p).await.unwrap();
}
Some(proto::add_path_request::Request::Contents(c)) => {
hasher.update(&c.content);
current_file.as_mut().unwrap().write_all(&c.content).await?;
}
None => break,
}
}
let path = store.join(data_encoding::HEXLOWER.encode(hasher.finalize().as_bytes()));
use tokio::io::ErrorKind;
let new = tmp_dir.into_path();
match tokio::fs::rename(&new, &path).await {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::DirectoryNotEmpty => (),
Err(e) => {
tokio::fs::remove_dir_all(&new).await?;
return Err(e.into());
}
}
info!("add_path extracted to {:?}", path);
Ok(tonic::Response::new(proto::DerivationReply {
result: Some(proto::derivation_reply::Result::Ok(
proto::DerivationResult {
destdir: vec![path.to_str().unwrap().to_string()],
paths: Vec::new(),
path_patterns: Vec::new(),
},
)),
}))
}
async fn derivation(
&self,
request: tonic::Request<proto::DerivationRequest>,
) -> Result<tonic::Response<proto::DerivationReply>, tonic::Status> {
debug!("derivation request");
let r = request.into_inner();
let (tx, rx) = tokio::sync::oneshot::channel();
self.sender
.send((
crate::container::BuildRequest {
name: r.name,
paths: r.paths,
script: r.builder,
target: r.target,
},
tx,
))
.unwrap();
Ok(tonic::Response::new(match rx.await.unwrap() {
Ok(out) => proto::DerivationReply {
result: Some(proto::derivation_reply::Result::Ok(
proto::DerivationResult {
destdir: vec![out.to_str().unwrap().to_string()],
paths: Vec::new(),
path_patterns: Vec::new(),
},
)),
},
Err(e) => proto::DerivationReply {
result: Some(proto::derivation_reply::Result::Error(e)),
},
}))
path: p
.iter()
.rev()
.map(|x| x.to_str().unwrap().to_string())
.collect(),
error: None,
result: Some(proto::derivation_reply::Result::Ok(
proto::DerivationResult {
destdir: p
.result
.iter()
.rev()
.map(|x| x.to_str().unwrap().to_string())
.collect(),
paths: p.paths.into_iter().filter_map(Arc::into_inner).collect(),
path_patterns: Vec::new(),
},
)),
}
}
pub fn child<P: AsRef<Path>>(path: &[P]) {
let store = Path::new("/home/pe/Projets/frix/store/ailpe/store");
std::fs::create_dir_all(store).unwrap();
let vm_store = Path::new("/ailpe/store");
let mut path_env = "PATH=".to_string();
let mut ld_env = "LD_LIBRARY_PATH=".to_string();
let mut mounts = Vec::new();
for p in path {
use std::fmt::Write;
let host = store.join(p.as_ref().file_name().unwrap());
let guest = vm_store.join(p.as_ref().file_name().unwrap());
std::fs::create_dir_all(&host).unwrap();
write!(
&mut path_env,
"{}/usr/bin:{}/bin:",
guest.to_str().unwrap(),
guest.to_str().unwrap()
)
.unwrap();
write!(
&mut ld_env,
"{}/lib64/x86_64-linux-gnu:{}/lib/x86_64-linux-gnu:",
guest.to_str().unwrap(),
guest.to_str().unwrap()
)
.unwrap();
mounts.push(Mount::bind(p.as_ref().to_str().unwrap(), host.to_str().unwrap()).unwrap());
println!("{:?}", mounts);
// Fork in order to be able to unmount despite the chroot.
let f = unsafe { fork() };
if f == 0 {
let c = std::ffi::CString::new("/home/pe/Projets/frix/store").unwrap();
if unsafe { chroot(c.as_ptr()) } < 0 {
println!("child {:?}", std::io::Error::last_os_error());
}
println!("{:?}\n{:?}", path_env, ld_env);
let penv = CString::new(path_env).unwrap();
let lenv = CString::new(ld_env).unwrap();
unsafe {
let c = std::ffi::CString::new(
"/ailpe/store/tmp3/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
)
.unwrap();
execve(
c.as_ptr(),
[c.as_ptr(), std::ptr::null()].as_ptr(),
[penv.as_ptr(), lenv.as_ptr(), std::ptr::null()].as_ptr(),
);
println!("execve {:?}", std::io::Error::last_os_error());
}
} else {
let mut status = 0;
unsafe { waitpid(f, &mut status, 0) };
std::mem::drop(mounts);
}
/*
let lua = mlua::Lua::new();
let map_table = lua.create_table().unwrap();
map_table.set(1, "one").unwrap();
map_table.set("two", 2).unwrap();
lua.globals().set("map_table", map_table).unwrap();
let derivation = lua
.create_function(|st, t: mlua::Table| {
t.for_each(|k: String, v: mlua::Value| {
println!("{:?} {:?}", k, v);
Ok(())
}).unwrap();
Ok(1)
})
.unwrap();
lua.globals().set("__derivation", derivation).unwrap();
let ubuntu = lua
.create_function(|st, (t, name): (mlua::Table, String)| {
t.for_each(|k: String, v: mlua::Value| {
println!("{:?} {:?}", k, v);
Ok(())
}).unwrap();
let mut t = st.create_table().unwrap();
t.set(1, "one").unwrap();
Ok(t)
})
.unwrap();
lua.globals().set("__ubuntu", ubuntu).unwrap();
let ubuntu_ctx = lua
.create_function(|st: &mlua::Lua, t: mlua::Table| -> mlua::Result<mlua::Table> {
t.for_each(|k: String, v: mlua::Value| {
println!("{:?} {:?}", k, v);
Ok(())
}).unwrap();
let mut t = st.create_table().unwrap();
t.set(1, "one").unwrap();
Ok(t)
})
.unwrap();
lua.globals().set("__ubuntu_ctx", ubuntu_ctx).unwrap();
lua.load(std::fs::read_to_string(&std::env::args().nth(1).unwrap()).unwrap())
.exec()
.unwrap();
return;
*/
#[tokio::main]
async fn main() {
fn main() {
let elpe = Elpe {
deb_client: Arc::new(Client {
c: lazy_init::Lazy::new(),
mirror: "http://fr.archive.ubuntu.com/ubuntu".to_string(),
store_path: Path::new("/home/pe/Projets/frix/store").to_path_buf(),
in_release: None.into(),
}),
};
let addr = "0.0.0.0:50051".parse().unwrap();
tonic::transport::Server::builder()
.add_service(elpe_server::ElpeServer::new(elpe))
.serve(addr)
.await
.unwrap();
/*
let h_secu = c.in_release("noble-security").await.unwrap();
let h = c.in_release("noble").await.unwrap();
let store_path = Path::new("/home/pe/Projets/frix/store");
let user = "pe";
let home = "/home/pe";
let container_channel = crate::container::serve(user, store_path);
let p_secu = c.packages(&h_secu, "main", "amd64").await.unwrap();
let p_main = c.packages(&h, "main", "amd64").await.unwrap();
let p_universe = c.packages(&h, "universe", "amd64").await.unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
privdrop::PrivDrop::default().user(user).apply().unwrap();
unsafe {
std::env::set_var("HOME", home);
}
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::<(
crate::container::BuildRequest,
tokio::sync::oneshot::Sender<Result<PathBuf, String>>,
)>();
let h = Arc::new(Mutex::new(HashMap::new()));
let index = &[index_main, index_universe, index_secu];
download_extract_deps(index, &c, h.clone(), "hello-traditional")
.await
.unwrap();
let elpe = Elpe {
deb_client: Client::new(store_path, "http://fr.archive.ubuntu.com/ubuntu"),
sender,
};
let pid = unsafe {
syscall(
SYS_clone3,
&libc::clone_args {
flags: (CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS) as u64,
pidfd: 0,
parent_tid: 0,
child_tid: 0,
stack: 0,
stack_size: 0,
tls: 0,
set_tid: 0,
set_tid_size: 0,
cgroup: 0,
exit_signal: SIGCHLD as u64,
},
std::mem::size_of::<libc::clone_args>(),
)
};
println!("pid {:?}", pid);
if pid == 0 {
child(&[
"/home/pe/Projets/frix/tmp",
"/home/pe/Projets/frix/tmp2",
"/home/pe/Projets/frix/tmp3",
]);
} else {
println!("{:?}", std::io::Error::last_os_error());
let x = unsafe { waitpid(pid as i32, std::ptr::null_mut(), 0) };
println!("x {:?}", x);
}
*/
let addr = "0.0.0.0:50051".parse().unwrap();
tokio::select! {
_ = tonic::transport::Server::builder()
.add_service(proto::elpe_server::ElpeServer::new(elpe))
.serve(addr)
=> {}
_ = t => {}
}
})
/// Wrong result symlink: Elpe packages have one "input name"
/// given by the hash of their inputs, and that directory is
/// symlinked from the "output name", which is the hash of their
/// output. This makes the outputs verifiable all the way down:
/// failures to verify result in this error.
WrongResultSymlink { expected: PathBuf, got: PathBuf },
WrongResultSymlink {
/// Expected output.
expected: PathBuf,
/// Obtained output.
got: PathBuf
},
/// The contained process failed to produce the file or directory
/// called `$DESTDIR`.
#[error("Failed to produce $DESTDIR")]
NoDestDir,
/// The build process returned a status other than 0.
#[error("Build process returned {status}")]
BuildReturn {
/// Return status.
status: i32
},
// dest: PathBuf,
pub c: lazy_init::Lazy<reqwest::Client>,
pub mirror: String,
pub store_path: PathBuf,
pub in_release: tokio::sync::Mutex<Option<Arc<InRelease>>>,
c: lazy_init::Lazy<reqwest::Client>,
mirror: String,
store_path: PathBuf,
in_release: Arc<Mutex<Option<Arc<InRelease>>>>,
download_sem: Arc<Semaphore>,
store_locks: Arc<std::sync::Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>>,
timeout: std::time::Duration,
}
/// A locked store path, to prevent concurrent writes to the
/// store. For performance reasons, these are memory locks rather than
/// filesystem locks.
pub struct StoreLock {
locks: Arc<std::sync::Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>>,
p: PathBuf,
_lock: tokio::sync::OwnedMutexGuard<()>,
}
impl Drop for StoreLock {
fn drop(&mut self) {
debug!("store lock: dropping {:?}", self.p);
self.locks.lock().unwrap().remove(&self.p);
}
pub fn new<P: AsRef<std::path::Path>>(store_path: P, mirror: &str) -> Self {
Client {
c: lazy_init::Lazy::new(),
mirror: mirror.to_string(),
store_path: store_path.as_ref().to_path_buf(),
in_release: Arc::new(None.into()),
download_sem: Arc::new(Semaphore::new(MAX_PARALLEL_DOWNLOADS)),
store_locks: Arc::new(std::sync::Mutex::new(HashMap::new())),
timeout: std::time::Duration::from_secs(30),
}
}
self.c.get_or_create(|| reqwest::Client::new())
self.c.get_or_create(|| reqwest::ClientBuilder::new()
.read_timeout(self.timeout)
.build().unwrap()
)
}
pub async fn command<R: Into<std::borrow::Cow<'static, str>>, P: AsRef<std::path::Path>>(
&self,
release: R,
pkg: &str,
cmd: P,
) -> Command {
let h = self.in_release(release.into()).await.unwrap();
let arch = if cfg!(target_arch = "x86_64") {
"amd64"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
unreachable!()
};
let (main, universe) = tokio::join!(
self.packages(&h, "main", arch),
self.packages(&h, "universe", arch)
);
let main = main.unwrap();
let universe = universe.unwrap();
let index = vec![
deb::Index::open(&main).unwrap(),
deb::Index::open(&universe).unwrap(),
];
let p = extract::download_extract_deps(&index, self, pkg)
.await
.unwrap();
debug!("running {:?}", p.result.last().unwrap().join(&cmd));
Command::new(p.result.last().unwrap().join(&cmd))
}
pub async fn lock_store_path<P: AsRef<std::path::Path>>(&self, path: P) -> StoreLock {
debug!("locking store path {:?}", path.as_ref());
let p = path.as_ref().to_path_buf();
let mutex = {
let mut locks = self.store_locks.lock().unwrap();
locks
.entry(p.clone())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
};
let lock = mutex.lock_owned().await;
debug!("lock acquired: {:?}", p);
StoreLock {
locks: self.store_locks.clone(),
p,
_lock: lock,
}
let r = self.client().get(url).send().await?;
const MAX_ATTEMPTS: usize = 10;
for i in 1..=MAX_ATTEMPTS {
match self.try_download(url, sha256, &path).await {
Ok(()) => break,
Err(e) if i == MAX_ATTEMPTS => return Err(e),
Err(e) => {
error!("attempt {:?} at downloading {:?}: {:?}", i, url, e);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
}
drop(lock); // Ensure lock lives until here.
Ok((Downloaded { path }, true))
}
async fn try_download(
&self,
url: &str,
sha256: &str,
path: &std::path::Path,
) -> Result<(), Error> {
let r = self
.client()
.get(url)
.send()
.await?;
use std::path::PathBuf;
use tracing::*;
pub fn find_files(path: PathBuf) -> Result<FindFiles, std::io::Error> {
debug!("find_files {:?}", path);
let meta = std::fs::metadata(&path)?;
Ok(FindFiles {
stack: vec![(path, meta)],
})
}
pub struct FindFiles {
stack: Vec<(PathBuf, std::fs::Metadata)>,
}
impl Iterator for FindFiles {
type Item = (PathBuf, std::fs::Metadata);
fn next(&mut self) -> Option<Self::Item> {
while let Some((p, meta)) = self.stack.pop() {
debug!("find_files {:?}", p);
if meta.is_symlink() || meta.is_file() {
return Some((p, meta));
} else if let Ok(dir) = std::fs::read_dir(&p) {
for e in dir {
if let Ok(e) = e {
self.stack.push((e.path(), e.metadata().unwrap()));
}
}
return Some((p, meta));
} else {
error!("could not read {:?}", p);
continue;
}
}
None
}
}
pub fn find_dirs(path: PathBuf) -> Result<FindDirs, std::io::Error> {
debug!("find_files {:?}", path);
let meta = std::fs::metadata(&path)?;
Ok(FindDirs {
stack: vec![(path, meta)],
})
}
pub struct FindDirs {
stack: Vec<(PathBuf, std::fs::Metadata)>,
}
impl Iterator for FindDirs {
type Item = (PathBuf, std::fs::Metadata);
fn next(&mut self) -> Option<Self::Item> {
while let Some((p, meta)) = self.stack.pop() {
if meta.is_symlink() {
continue;
}
if let Ok(dir) = std::fs::read_dir(&p) {
for e in dir {
if let Ok(e) = e {
self.stack.push((e.path(), e.metadata().unwrap()));
}
}
return Some((p, meta));
}
}
None
}
}
/// The result of downloading/extracting a package.
#[derive(Debug, Clone)]
pub struct Packages {
/// Paths of each of the extracted packages, in reverse order from
/// the root (i.e. the root is the last).
pub result: Vec<Arc<PathBuf>>,
/// Paths to directories (i.e. */usr/bin, */usr/lib etc), useful
/// to create $PATH or $CFLAGS from the client.
pub paths: Vec<Arc<String>>,
}
for dep in deps {
if last.deps_h.insert(dep.clone()) {
last.deps.push(dep.clone());
for dep in transitive_deps {
if last.transitive_deps_h.insert(dep.clone()) {
debug!("adding transitive dep: {:?} -> {:?}", last.package.package, dep);
last.transitive_deps.push(dep.clone());
let client = client.clone();
let url = client.url(&elt.package);
let sha256 = elt.package.sha256.unwrap().to_string();
let (tx, rx) = tokio::sync::oneshot::channel();
let files = files.clone();
let tasks = tasks.clone();
tokio::spawn(async move {
let permit = tasks.acquire_owned().await.unwrap();
info!("downloading {:?}", url);
let (mut task, downloaded) = client.download_url(&url, &sha256).await.unwrap();
info!("finished downloading {:?}", url);
if downloaded {
tokio::task::spawn_blocking(move || {
extract_task(&mut task, &mut *files.lock().unwrap()).unwrap();
info!("finished extracting {:?}", url);
tx.send(task).unwrap_or(());
info!("sent {:?}", url);
})
.await
.unwrap();
} else {
task.path.set_extension("");
let mut files = files.lock().unwrap();
for f in find_files(task.path.clone()) {
if let Ok(p) = f.strip_prefix(&task.path) {
use std::collections::hash_map::Entry;
if let Entry::Vacant(e) = files.entry(p.to_path_buf()) {
debug!("cached {:?} -> {:?}", p, f);
e.insert(task.path.clone());
}
}
}
tx.send(task).unwrap_or(());
elt.spawn_extract(client.clone())
.await;
elt.push_deps(index, &mut stack).await;
}
}
Ok(Packages {
result,
paths: paths.into_iter().collect(),
})
}
async fn add_subst(downloaded: PathBuf, target: &Path, files: Files) -> Result<(), std::io::Error> {
let mut files = files.lock().unwrap();
for (f, m) in find_files(downloaded.clone())? {
if !m.is_dir() {
if let Ok(p) = f.strip_prefix(&downloaded) {
use std::collections::hash_map::Entry;
if let Entry::Vacant(e) = files.entry(p.to_path_buf()) {
debug!("add_subst {:?} -> {:?}", p, target);
e.insert(target.to_path_buf());
let d = deb::Deb::read(&mut f)?;
download.path.set_extension("");
std::fs::create_dir_all(&download.path)?;
d.decompress(&mut f, &download.path, |path| {
use std::collections::hash_map::Entry;
let path = if let Ok(p) = path.strip_prefix(".") {
p.to_path_buf()
} else {
path.to_path_buf()
};
debug!("extract {:?} -> {:?}", path, download.path);
if let Entry::Vacant(e) = files.entry(path) {
e.insert(download.path.clone());
let d = match deb::Deb::read(&mut f) {
Ok(d) => d,
Err(e) => {
std::fs::remove_file(&download.path).unwrap_or(());
return Err(e.into());
}
};
std::fs::create_dir_all(&path).unwrap_or(());
match d.decompress(&mut f, &path) {
Ok(()) => {
download.path = path;
drop(lock);
Ok(())
const MAX_PARALLEL_DOWNLOADS: usize = 20;
fn multi_lookup<'a>(index: &'a [deb::Index], package: &str) -> Option<deb::Stanza<'a>> {
async fn multi_lookup<'a>(index: &'a [deb::Index], package: &str) -> Option<deb::Stanza<'a>> {
for ind in index {
if let Some(i) = ind.lookup_async(package).await {
return Some(i);
}
}
fn find_files(path: PathBuf) -> FindFiles {
let meta = std::fs::metadata(&path).unwrap();
FindFiles {
stack: vec![(path, meta, false)],
}
}
impl<'a> StackElt<'a> {
/// Some libexec files (e.g. gcc) are searched relative to their
/// original package /bin path. Propagating the /usr/libexec
/// folder avoids overrides and ad-hoc code in the client.
async fn link_extra(&mut self, final_path: &Path, extra: &[&str]) -> Result<(), Error> {
debug!("linking extra path in {:?}: {:?}", final_path, extra);
for path in extra {
let final_extra = final_path.join(path);
impl Iterator for FindFiles {
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
while let Some((p, meta, visited)) = self.stack.pop() {
if visited {
continue;
}
if let Ok(dir) = std::fs::read_dir(&p) {
self.stack.push((p, meta, true));
for e in dir {
if let Ok(e) = e {
self.stack.push((e.path(), e.metadata().unwrap(), false));
for d in self.deps.iter() {
let d_extra = d.join(path);
let Ok(meta) = tokio::fs::metadata(&d_extra).await else {
continue;
};
let mut stack = vec![(d_extra.clone(), meta)];
while let Some((elt, meta)) = stack.pop() {
if let Ok(mut dir) = tokio::fs::read_dir(&elt).await {
let p = elt.strip_prefix(&d_extra).unwrap();
tokio::fs::create_dir_all(&final_extra.join(&p)).await?;
while let Some(e) = dir.next_entry().await? {
stack.push((e.path(), e.metadata().await?));
}
} else if meta.is_file() {
let p = elt.strip_prefix(&d_extra).unwrap();
debug!("libexec hard link {:?} to {:?}", elt, final_extra.join(&p));
tokio::fs::hard_link(&elt, &final_extra.join(&p))
.await
.unwrap_or(());
/// Let `path` be the path to the raw extracted deb package, and
/// `dest` be the final destination of this package in the store
/// (when taking the package's context into account).
///
/// Then, `find_libs` finds all paths in `path` that match the
/// patterns in self.ld_path (`self.ld_path` paths are of the form
/// `/usr/lib/x86_64-linux`), and add their equivalent in `dest` to
/// `self.transitive_deps`.
///
/// This makes sure that packages depending on `self` will be able
/// to find the correct paths to these libs.
info!("patching");
// Patch the ELFs, now that we have all the deps.
let mut hashing = Vec::new();
for f in find_files(downloaded.path.clone()) {
let dest_path = dest.join(&f.strip_prefix(&downloaded.path).unwrap());
std::fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
let patched = self.patch_elf(&f, &dest_path, &files).unwrap_or(false);
if !patched {
// Hard link
debug!("hard link {:?} {:?}", f, dest_path);
std::fs::hard_link(&f, &dest_path).unwrap_or(());
}
hashing.push(tokio::spawn(async move {
// hash + write
info!("hashing {:?}", f);
if let Ok(link) = std::fs::read_link(&dest_path) {
Ok(Some((dest_path, link.to_str().unwrap().to_string())))
} else if let Ok(file) = tokio::fs::File::open(&dest_path).await {
let mut hasher = blake3::Hasher::new();
hash_reader(file, &mut hasher).await?;
let hex = data_encoding::HEXLOWER.encode(hasher.finalize().as_bytes());
Ok::<_, Error>(Some((f, hex)))
} else {
Ok(None)
}
}));
}
info!("patched all");
let mut hashes = Vec::with_capacity(hashing.len());
for h in hashing.into_iter() {
if let Some(h) = h.await.unwrap().unwrap() {
hashes.push(h)
info!("create final path for {dest:?}");
match self
.create_final_path(client, &files, &downloaded.path, &dest, &base_package_name)
.await
{
Ok(x) => x,
Err(e) => {
tokio::fs::remove_dir_all(&dest).await.unwrap_or(());
return Err(e);
hashes.sort_by(|a, b| a.0.cmp(&b.0));
info!("hashed all");
let mut output_hasher = blake3::Hasher::new();
let blakesums = dest.join("blake3sums");
let mut file = std::fs::File::create(&blakesums).unwrap();
for (path, hash) in hashes {
debug!("strip {:?} {:?}", path, downloaded.path);
let path = path.to_str().unwrap();
writeln!(file, "{} {}", hash, path)?;
writeln!(output_hasher, "{} {}", hash, path)?;
}
client.store_path.join(&format!(
"{}-{}",
data_encoding::HEXLOWER.encode(output_hasher.finalize().as_bytes()),
base_package_name,
))
for dep in self.deps.iter() {
if last.deps_h.insert(dep.clone()) {
last.deps.push(dep.clone());
for dep in self.transitive_deps.iter() {
if last.transitive_deps_h.insert(dep.clone()) {
debug!("adding transitive dep: {:?} -> {:?}", last.package.package, dep);
last.transitive_deps.push(dep.clone());
fn push_deps<'a>(index: &'a [deb::Index], elt: StackElt<'a>, stack: &mut Vec<StackElt<'a>>) {
let depends = elt.package.depends.clone();
stack.push(elt);
for dep in depends.iter() {
match dep {
deb::Dep::Simple(s) => {
debug!("dep {:?}", s);
let dep = multi_lookup(index, &s.name).unwrap();
stack.push(StackElt {
package: dep,
rx: None,
deps: Vec::new(),
deps_h: BTreeSet::new(),
ld_path: Vec::new(),
ld_path_h: BTreeSet::new(),
})
async fn spawn_extract(
&mut self,
client: Client,
) {
let client = client.clone();
let url = client.url(self.package.file_name.as_deref());
let sha256 = self.package.sha256.unwrap().to_string();
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let permit = client.download_sem.clone().acquire_owned().await.unwrap();
info!("downloading {:?}", url);
let (mut task, _) = match client.download_url(&url, &sha256).await {
Ok(x) => x,
Err(e) => {
tx.send(Err(e)).unwrap_or(());
return Ok(());
}
};
let is_extracted = std::fs::metadata(&task.path.with_extension("")).is_ok();
info!("finished downloading {:?}", url);
if !is_extracted {
// Sets extension
if let Err(e) = extract_task(&client, &mut task).await {
info!("finished extracting {:?} {:?}", url, e);
tx.send(Err(e)).unwrap_or(());
} else {
info!("finished extracting {:?}, Ok", url);
tx.send(Ok(task)).unwrap_or(());
}
info!("sent {:?}", url);
} else {
task.path.set_extension("");
tx.send(Ok(task)).unwrap_or(());
deb::Dep::Alternatives { alt } => {
debug!("alt {:?}", alt);
for dep in alt {
if let Some(dep_) = multi_lookup(index, &dep.name) {
stack.push(StackElt {
package: dep_,
rx: None,
deps: Vec::new(),
deps_h: BTreeSet::new(),
ld_path: Vec::new(),
ld_path_h: BTreeSet::new(),
});
return;
drop(permit);
Ok::<_, Error>(())
});
self.rx = Some(rx);
}
async fn push_deps(self, index: &'a [deb::Index], stack: &mut Vec<StackElt<'a>>) {
let depends = self.package.depends.clone();
stack.push(self);
for dep in depends.iter() {
match dep {
deb::Dep::Simple(s) => {
debug!("dep {:?}", s);
let Some(dep) = multi_lookup(index, &s.name).await else {
panic!("could not find {:?}", s.name)
};
stack.push(StackElt {
package: dep,
rx: None,
deps: Vec::new(),
transitive_deps: Vec::new(),
transitive_deps_h: BTreeSet::new(),
ld_path: Vec::new(),
ld_path_h: BTreeSet::new(),
files: BTreeSet::new(),
})
}
deb::Dep::Alternatives { alt } => {
debug!("alt {:?}", alt);
for dep in alt {
if let Some(dep_) = multi_lookup(index, &dep.name).await {
stack.push(StackElt {
package: dep_,
rx: None,
deps: Vec::new(),
transitive_deps: Vec::new(),
transitive_deps_h: BTreeSet::new(),
ld_path: Vec::new(),
ld_path_h: BTreeSet::new(),
files: BTreeSet::new(),
});
return;
}
panic!("Not found: {:?}", alt);
}
}
}
async fn create_final_path(
&mut self,
client: &Client,
files: &Files,
downloaded_path: &Path,
dest: &Path,
base_package_name: &str,
) -> Result<PathBuf, Error> {
// Link the required libexec before hashing.
let tmp = async_tempfile::TempDir::new_in(dest.parent().unwrap()).await?;
self.link_extra(&tmp.dir_path(), &["usr/libexec", "usr/lib/gcc"])
.await?;
// Patch the ELFs, now that we have all the deps.
let mut hashing = Vec::new();
let mut hashes = Vec::with_capacity(hashing.len());
debug!("create_final_path {:?}", downloaded_path);
for (f, meta) in find_files(downloaded_path.to_path_buf())? {
debug!("f = {:?}", f);
let rel = f.strip_prefix(&downloaded_path).unwrap();
let dest_path = tmp.dir_path().join(&rel);
if meta.is_dir() {
tokio::fs::create_dir_all(dest_path).await.unwrap_or(());
continue;
}
if meta.is_symlink() {
// Relink the file to the subst.
let target = tokio::fs::read_link(&f).await?;
debug!("relink {:?} -> {:?} {:?}", f, target, target.is_relative());
let subst = {
let l = files.lock().unwrap();
let target_rel = rel.parent().unwrap().join(&target);
if let Some(subst) = l.get(&target_rel) {
Some(subst.join(&target_rel))
} else {
None
}
};
if let Some(subst) = subst {
debug!("relink to {:?}", dest_path);
tokio::fs::symlink(&subst, &dest_path).await?;
hashes.push((f, subst.to_str().unwrap().to_string()))
} else {
// Leave the symlink untouched
// tokio::fs::create_dir_all(dest_path.parent().unwrap()).await.unwrap_or(());
debug!("symlink {:?} {:?} {:?}", target, dest_path, f);
tokio::fs::symlink(&target, &dest_path).await?;
hashes.push((f, target.to_str().unwrap().to_string()));
}
continue;
}
if !self
.patch_elf(&f, &dest_path, &files)
.await
.unwrap_or(false)
{
// Hard link
debug!("hard link {:?} {:?}", f, dest_path);
tokio::fs::hard_link(&f, &dest_path).await.unwrap_or(());
}
hashing.push(tokio::spawn(async move {
// hash + write
info!("hashing {:?}", f);
if let Ok(file) = tokio::fs::File::open(&dest_path).await {
let mut hasher = blake3::Hasher::new();
hash_reader(file, &mut hasher).await.unwrap();
let hex = data_encoding::HEXLOWER.encode(hasher.finalize().as_bytes());
Ok::<_, Error>(Some((f, hex)))
} else {
Ok(None)
}
}));
}
info!("patched all");
for h in hashing.into_iter() {
if let Some(h) = h.await.unwrap().unwrap() {
hashes.push(h)
}
}
hashes.sort_by(|a, b| a.0.cmp(&b.0));
info!("hashed all");
let mut output_hasher = blake3::Hasher::new();
{
let blakesums = tmp.dir_path().join("blake3sums");
let mut file = tokio::fs::File::create(&blakesums).await?;
for (path, hash) in hashes {
let path = path
.strip_prefix(&downloaded_path)
.unwrap()
.to_str()
.unwrap();
file.write_all(hash.as_bytes()).await?;
file.write_all(b" ").await?;
file.write_all(path.as_bytes()).await?;
file.write_all(b"\n").await?;
writeln!(output_hasher, "{} {}", hash, path)?;
let final_path = client.store_path.join(&format!(
"{}-{}",
data_encoding::HEXLOWER.encode(output_hasher.finalize().as_bytes()),
base_package_name,
));
{
let transitive = tmp.dir_path().join("paths");
let mut file = tokio::fs::File::create(&transitive).await?;
for path in self.files.iter() {
file.write_all(path.as_bytes()).await?;
file.write_all(b"\n").await?;
}
}
for (f, _) in find_dirs(downloaded_path.to_path_buf())? {
let rel = f.strip_prefix(&downloaded_path).unwrap();
self.files
.insert(Arc::new(final_path.join(rel).to_str().unwrap().to_string()));
}
info!("rename {:?} to {:?}", tmp.dir_path(), dest);
tokio::fs::rename(tmp.dir_path(), dest).await?;
std::mem::forget(tmp);
Ok(final_path)
}
/*
impl<'a> From<Dep<'a>> for OwnedDep {
fn from(e: Dep<'a>) -> Self {
match e {
Dep::Simple(s) => OwnedDep::Simple(s.to_owned()),
Dep::Alternatives { alt } => OwnedDep::Alternatives {
alt: alt.iter().map(From::from).collect()
},
}
}
}
/// A dependency, which is either a "simple" dependency, or a list of
/// alternatives.
#[derive(Debug, Clone)]
pub enum OwnedDep {
Simple(OwnedSimpleDep),
Alternatives { alt: Vec<OwnedSimpleDep> },
}
/// A single dependency.
#[derive(Debug, Clone)]
pub struct OwnedSimpleDep {
pub name: String,
pub any: bool,
pub constraints: Vec<(String, OwnedVersion)>,
#[test]
fn test_grub_common() {
use tracing_subscriber::prelude::*;
use tracing_subscriber::util::SubscriberInitExt;
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| String::new().into()),
)
.with(tracing_subscriber::fmt::layer())
.try_init();
let (rest, p) = parse_control("Package: grub-common\nArchitecture: amd64\nVersion: 2.12-1ubuntu7\nBuilt-Using: lzo2 (= 2.10-2build3)\nMulti-Arch: foreign\nPriority: optional\nSection: admin\nSource: grub2\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\nOriginal-Maintainer: GRUB Maintainers <pkg-grub-devel@alioth-lists.debian.net>\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 12476\nDepends: libc6 (>= 2.38), libdevmapper1.02.1 (>= 2:1.02.36), libefiboot1t64 (>=38), libefivar1t64 (>= 38), libfreetype6 (>= 2.2.1), libfuse3-3 (>= 3.2.3), liblzma5 (>= 5.1.1alpha+20120614), debconf (>= 0.5) | debconf-2.0, gettext-base, lsb-base (>= 3.0-6), python3, python3-apt\nRecommends: os-prober (>= 1.33)\nSuggests: multiboot-doc, grub-emu, mtools, xorriso (>= 0.5.6.pl00), desktop-base (>= 4.0.6), console-setup\nConflicts: init-select\nBreaks: apport (<< 2.1.1), friendly-recovery (<< 0.2.13), lupin-support (<< 0.55), mdadm (<< 2.6.7-2)\nReplaces: grub-coreboot (<< 2.00-4), grub-efi (<< 1.99-1), grub-efi-amd64 (<< 2.00-4), grub-efi-ia32 (<< 2.00-4), grub-efi-ia64 (<< 2.00-4), grub-ieee1275 (<< 2.00-4), grub-linuxbios (<< 1.96+20080831-1), grub-pc (<< 2.00-4), grub-yeeloong (<< 2.00-4), init-select\nFilename: pool/main/g/grub2/grub-common_2.12-1ubuntu7_amd64.deb\nSize: 2119862\nMD5sum: 86e63081ae4ee34d6a4f408ac6dbf0e4\nSHA1: b98f9f88e5480423e041fd647b438ba2f6e6f5ea\nSHA256: d8110b97b6fb6da40449c74b9cb527f02965dffaae8344399a711617f5a69249\nSHA512: 85c63b8d3e6a870df48533ab525df13abe9ec4861ff4b5577def94f65a2ebf6ac44a54a6cd0eca1c825e1c7aa2bd14e4d4ed53f83d381963ac1dc51daa16482a\nHomepage: https://www.gnu.org/software/grub/\nDescription: GRand Unified Bootloader (common files)\nTask: ubuntu-desktop-minimal, ubuntu-desktop, ubuntu-desktop-raspi, kubuntu-desktop, xubuntu-minimal, xubuntu-desktop, lubuntu-desktop, ubuntustudio-desktop-core, ubuntustudio-desktop, ubuntukylin-desktop,ubuntukylin-desktop-minimal, ubuntu-mate-core, ubuntu-mate-desktop, ubuntu-budgie-desktop-minimal, ubuntu-budgie-desktop, ubuntu-budgie-desktop-raspi, ubuntu-unity-desktop, edubuntu-desktop-gnome-minimal, edubuntu-desktop-gnome-raspi, ubuntucinnamon-desktop-minimal, ubuntucinnamon-desktop-raspi\nDescription-md5: 9c75036dc0a0792fedbc58df208ed227").unwrap();
assert!(p.depends.iter().any(|x| match x {
Dep::Simple(s) => s.name == "liblzma5",
_ => false
}));
assert!(rest.is_empty())
}
regex::Regex::new(r"^[a-z0-9.~+-]+-([a-z0-9.~+]+)").unwrap();
static ref RE: regex::Regex = regex::Regex::new(r"^[a-z0-9.~+]+").unwrap();
regex::Regex::new(r"^[a-z0-9\.~+-]+-([a-z0-9\.~+]+)").unwrap();
static ref RE: regex::Regex = regex::Regex::new(r"^[a-z0-9\.~+]+").unwrap();
for e in ar.entries()? {
let mut e = e?;
f(&e.path()?);
e.unpack_in(&path)?;
for e in ar.entries().unwrap() {
let mut e = e.unwrap();
debug!("zstd decompress {:?}", e.path());
// f(&e.path()?);
e.unpack_in(&path).unwrap();
}
} else if self.data.file_id().ends_with(".tar.xz") {
let mut ar = tar::Archive::new(xz::read::XzDecoder::new(r.take(self.data.file_size)));
for e in ar.entries().unwrap() {
let mut e = e.unwrap();
debug!("xz decompress {:?}", e.path());
// f(&e.path()?);
e.unpack_in(&path).unwrap();
}
} else if self.data.file_id().ends_with(".tar") {
let mut ar = tar::Archive::new(r.take(self.data.file_size));
for e in ar.entries().unwrap() {
let mut e = e.unwrap();
debug!("tar extract {:?}", e.path());
// f(&e.path()?);
e.unpack_in(&path).unwrap();
lru: Arc::new(Mutex::new(lru::LruCache::new(NonZeroUsize::new(256).unwrap()))),
lru: Mutex::new(lru::LruCache::new(NonZeroUsize::new(256).unwrap())),
})))
}
pub async fn lookup_async<'a>(&'a self, package: &str) -> Option<Stanza<'a>> {
let s = self.clone();
let p = package.to_string();
tokio::task::spawn_blocking(move || s.lookup_range(&p))
.await
.unwrap()
.map(|(m1, m2)| {
let s = &*self.0.map;
let line = std::str::from_utf8(&s[m1..m2]).unwrap().trim();
parse_control(line).unwrap().1
})
}
pub async fn lookup_virtual_async<'a>(&'a self, package: &str) -> Vec<Stanza<'a>> {
let s = self.clone();
let r =
regex::bytes::Regex::new(&format!(r#"\nProvides:[^\n]*[, ]{package}[,\s\n]"#)).unwrap();
tokio::task::spawn_blocking(move || {
r.find_iter(&*s.0.map)
.map(|m| {
let map = &*s.0.map;
s.frame(0, map.len(), m.start())
})
.collect::<Vec<_>>()
.await
.unwrap()
.into_iter()
.map(|(m1, m2)| {
let line = std::str::from_utf8(&self.0.map[m1..m2]).unwrap().trim();
parse_control(line).unwrap().1
})
.collect()
}
pub fn lookup<'a>(&'a self, package: &str) -> Option<Stanza<'a>> {
self.lookup_range(package).map(|(m1, m2)| {
let s = &*self.0.map;
let line = std::str::from_utf8(&s[m1..m2]).unwrap().trim();
parse_control(line).unwrap().1
})
pub fn lookup(&self, package: &str) -> Option<Stanza> {
fn frame(&self, a: usize, b: usize, mut m1: usize) -> (usize, usize) {
let s = &*self.0.map;
while m1 > a && (s[m1] != b'\n' || s[m1 + 1] != b'\n') {
m1 -= 1;
}
let mut m2 = m1 + 2;
while m2 + 1 < b && (s[m2] != b'\n' || s[m2 + 1] != b'\n') {
m2 += 1;
}
if m2 < b {
m2 += 1
}
(m1, m2)
}
fn lookup_range(&self, package: &str) -> Option<(usize, usize)> {
use crate::{Error, find_files::*, mount};
use futures::{SinkExt, StreamExt};
use std::collections::{HashMap, HashSet};
use std::ffi::CString;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::*;
/// A request to build a package.
#[derive(bincode::Encode, bincode::Decode, Debug)]
pub struct BuildRequest {
pub name: String,
pub paths: Vec<String>,
pub script: String,
pub target: String,
}
/// The channel used to communicate with the container process.
pub struct ContainerChannel {
r: std::os::unix::net::UnixStream,
w: std::os::unix::net::UnixStream,
}
/// A function that forwards messages from the receiver to the
/// container process and back.
pub async fn forward(
mut receiver: tokio::sync::mpsc::UnboundedReceiver<(
BuildRequest,
tokio::sync::oneshot::Sender<Result<PathBuf, String>>,
)>,
c: ContainerChannel,
) -> Result<(), Error> {
let r = tokio::net::UnixStream::from_std(c.r).unwrap();
let w = tokio::net::UnixStream::from_std(c.w).unwrap();
let encoder = tokio_util::codec::LengthDelimitedCodec::new();
let mut writer = tokio_util::codec::FramedWrite::new(w, encoder);
let decoder = tokio_util::codec::LengthDelimitedCodec::new();
let mut reader = tokio_util::codec::FramedRead::new(r, decoder);
let mut pending = HashMap::new();
let mut id = 1u64;
loop {
tokio::select! {
x = receiver.recv() => {
if let Some((msg, resp)) = x {
pending.insert(id, resp);
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&bincode::encode_to_vec(&(id, msg), bincode::config::standard()).unwrap());
debug!("sending to process {:?}", bytes.len());
writer.send(bytes.into()).await?;
id += 1;
}
}
x = reader.next() => {
debug!("received process response {:?}", x);
if let Some(Ok(msg)) = x {
let ((id, resp), _) = bincode::decode_from_slice::<(u64, ProcessResult), _>(&msg, bincode::config::standard()).unwrap();
let chan = pending.remove(&id).unwrap();
chan.send(resp).unwrap_or(());
} else {
panic!("received none");
}
}
}
}
}
type ProcessResult = Result<PathBuf, String>;
/// Start the container process. This function forks: the child blocks
/// indefinitely, while the parent returns. This needs to be a
/// separate process in order to run as root and create namespaces.
pub fn serve(user: &str, store_path: &Path) -> ContainerChannel {
let (r0, w0) = std::os::unix::net::UnixStream::pair().unwrap();
let (r1, w1) = std::os::unix::net::UnixStream::pair().unwrap();
r0.set_nonblocking(true).unwrap();
r1.set_nonblocking(true).unwrap();
w0.set_nonblocking(true).unwrap();
w1.set_nonblocking(true).unwrap();
let pid = unsafe { libc::fork() };
if pid == 0 {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
let r1 = tokio::net::UnixStream::from_std(r1).unwrap();
let w0 = tokio::net::UnixStream::from_std(w0).unwrap();
async_serve(user, store_path, r1, w0).await
});
unreachable!()
} else {
ContainerChannel { r: r0, w: w1 }
}
}
/// Inner server callable from a Tokio runtime, used since Tokio
/// runtimes don't survive forks.
async fn async_serve(
user: &str,
store: &Path,
r: tokio::net::UnixStream,
w: tokio::net::UnixStream,
) {
let encoder = tokio_util::codec::LengthDelimitedCodec::new();
let writer = Arc::new(tokio::sync::Mutex::new(
tokio_util::codec::FramedWrite::new(w, encoder),
));
let decoder = tokio_util::codec::LengthDelimitedCodec::new();
let mut reader = tokio_util::codec::FramedRead::new(r, decoder);
debug!("drv process waiting");
while let Some(received) = reader.next().await {
if let Ok(received) = received {
debug!("drv_process received {:?}", received.len());
let ((id, rec_msg), _) = bincode::decode_from_slice::<(u64, BuildRequest), _>(
&received,
bincode::config::standard(),
)
.unwrap();
let writer = writer.clone();
let store = store.to_path_buf();
let user = user.to_string();
tokio::spawn(async move {
let result: ProcessResult =
run_in_container(&user, &store, rec_msg).map_err(|e| format!("{:?}", e));
debug!("result {:?}", result);
let v = bincode::encode_to_vec(&(id, result), bincode::config::standard()).unwrap();
let mut bytes = bytes::BytesMut::new();
debug!("drv_process replying {:?}", v.len());
bytes.extend_from_slice(&v);
writer.lock().await.send(bytes.into()).await.unwrap()
});
}
}
info!("drv_process exited");
}
fn run_in_container(user: &str, store: &Path, r: BuildRequest) -> Result<PathBuf, Error> {
let mut hasher = blake3::Hasher::new();
hasher.update(r.name.as_bytes());
hasher.update(b"\n");
hasher.update(r.target.as_bytes());
hasher.update(b"\n");
debug!("run in container, path = {:#?}", r.paths);
for p in r.paths.iter() {
hasher.update(p.as_bytes());
hasher.update(b"\n");
}
hasher.update(r.script.as_bytes());
hasher.update(b"\n");
let name = data_encoding::HEXLOWER.encode(hasher.finalize().as_bytes());
// Guest dest.
let dest = store.join(&name);
if std::fs::metadata(&dest).is_ok() {
let mut output_hasher = blake3::Hasher::new();
let blakesums = dest.join("blake3sums");
let file = match std::fs::File::open(&blakesums) {
Ok(file) => file,
Err(e) => {
error!("Error {:?} {:?}: {:?}", blakesums, dest, e);
return Err(e.into());
}
};
output_hasher.update_reader(file)?;
return Ok(store.join(&format!(
"{}-{}",
data_encoding::HEXLOWER.encode(output_hasher.finalize().as_bytes()),
r.name,
)));
}
// Tmp host path where things will be mounted.
let tmp_dir = store.join(format!("{}.drv", name));
// Full path of the store in the host.
let tmp_store = Path::new(&tmp_dir).join(store.strip_prefix("/").unwrap());
std::fs::create_dir_all(&tmp_store)?;
// Host dest.
let tmp_dest = tmp_store.join(&name);
let pid = unsafe {
libc::syscall(
libc::SYS_clone3,
&libc::clone_args {
flags: (libc::CLONE_NEWPID | libc::CLONE_NEWNET | libc::CLONE_NEWNS) as u64,
pidfd: 0,
parent_tid: 0,
child_tid: 0,
stack: 0,
stack_size: 0,
tls: 0,
set_tid: 0,
set_tid_size: 0,
cgroup: 0,
exit_signal: libc::SIGCHLD as u64,
},
size_of::<libc::clone_args>(),
)
};
if pid == 0 {
match std::panic::catch_unwind(|| {
inner_process(
user, &r, &tmp_dir, &dest, &store, &tmp_store, &name,
)
}) {
Ok(Ok(())) => std::process::exit(0),
Ok(Err(Error::BuildReturn { status })) => std::process::exit(status),
_ => std::process::exit(1),
}
} else {
debug!("waitpid");
let mut status = 0;
unsafe { libc::waitpid(pid as i32, &mut status, 0) };
info!("return status {:?}", status);
if status != 0 {
debug!("returning error");
return Err(Error::BuildReturn { status });
}
// Now that the paths have been unmounted, delete.
if let Ok(dir) = std::fs::read_dir(&tmp_store) {
for entry in dir {
std::fs::remove_dir(&entry?.path()).unwrap_or(());
}
std::fs::remove_dir(&tmp_store).unwrap_or(());
}
}
// And hash the output.
debug!("tmp_dest {:?}", tmp_dest);
let Ok(hashed) = hash_all(&tmp_dest) else {
return Err(Error::NoDestDir);
};
let out = store.join(&format!(
"{}-{}",
data_encoding::HEXLOWER.encode(hashed.as_bytes()),
r.name,
));
std::fs::remove_dir_all(&dest).unwrap_or(());
// Should be a symlink
std::fs::rename(&tmp_dest, &dest).unwrap();
match std::os::unix::fs::symlink(&dest, &out) {
Ok(()) => (),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
let got = std::fs::read_link(&out).unwrap();
if dest != got {
Err::<(), _>(Error::WrongResultSymlink {
expected: dest,
got,
})
.unwrap();
}
}
Err(e) => return Err(e.into()),
}
Ok(out)
}
fn patch_result_elf(root: &Path, f: &Path, target: &str) -> Result<bool, Error> {
use elfedit::*;
info!("patch_elf {:?}", f);
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&f)?;
let mut elf = match Elf::open(&file) {
Ok(elf) => elf,
Err(e) => {
info!("error opening {:?}: {:?}", file, e);
return Ok(false);
}
};
info!("patching {:?}", f);
let Some(parsed) = elf.parse().unwrap() else {
info!("No dynamic section");
return Ok(false);
};
let needed: Vec<_> = parsed
.needed()
.map(|x| x.unwrap().to_str().unwrap().to_string())
.collect();
let interp = parsed.interpreter();
if let Some(interp) = interp.unwrap() {
let mut interp_ = interp.to_str().unwrap();
let mut interp = Path::new(interp_).to_path_buf();
debug!("existing interp: {interp:?}");
if interp_.starts_with("/usr") || interp_.starts_with("/lib") {
while interp_.starts_with("/usr") || interp_.starts_with("/lib") {
if let Ok(target) = std::fs::read_link(&interp) {
let target = if target.is_relative() {
interp.parent().unwrap().join(target)
} else {
target
};
interp = target;
interp_ = interp.to_str().unwrap();
} else {
break;
}
}
debug!("target: {target:?}");
let subst = CString::new(interp_).unwrap();
info!("set interpreter {:?}", subst);
elf.set_interpreter(subst.to_bytes_with_nul());
} else {
// TODO: not sure what else to do here, we
// might want to set the interpreter to a
// different value (equivalent to "recompiling
// everything" on Nix).
info!("Interpreter is {interp_}. Already patched?");
return Ok(false);
}
} else if needed.is_empty() {
return Ok(false);
}
let mut deps_h = HashSet::new();
let mut path = String::new();
for n in needed.iter() {
for p in &["/usr/lib", "/usr/lib64", &format!("/usr/lib/{target}")] {
let Ok(dep) = std::fs::read_link(Path::new(p).join(n)) else {
continue;
};
if !deps_h.insert(dep.clone()) {
continue;
}
debug!("patch_elf needed: {:?}", dep);
debug!("root: {:?}", root);
if !path.is_empty() {
path.push(':')
}
path.push_str(dep.parent().unwrap().to_str().unwrap());
}
}
path.push('\0');
info!("Setting path {:?}", path);
if path.len() > 1 {
elf.set_runpath(&path.as_bytes());
}
Ok(elf.update(None).unwrap()) // map_err(From::from)
}
/// Can't be async since this needs to fork and Tokio doesn't work
/// across forks.
///
/// The fork is used to manage the mounts.
fn inner_process(
user: &str,
r: &BuildRequest,
tmp_dir: &Path,
dest: &Path,
store: &Path,
tmp_store: &Path,
name: &str,
) -> Result<(), Error> {
let tmp_usr = tmp_dir.join("usr");
std::fs::create_dir_all(&tmp_usr)?;
mount::make_root_private().unwrap();
std::mem::forget(mount::Mount::ramfs(&tmp_usr).unwrap());
std::os::unix::fs::symlink("/usr/lib", &tmp_dir.join("lib")).unwrap_or(());
std::os::unix::fs::symlink("/usr/lib64", &tmp_dir.join("lib64")).unwrap_or(());
for host in r.paths.iter() {
let guest = tmp_dir.join(host.strip_prefix("/").unwrap());
debug!("mounting {:?} to {:?}", host, guest);
std::fs::create_dir_all(&guest).unwrap();
debug!("created {:?} to {:?}", host, guest);
match mount::Mount::bind(host, guest.to_str().unwrap()) {
Ok(m) => std::mem::forget(m),
Err(e) => error!("{e:?}"),
}
debug!("mounted {:?}", guest);
if let Ok(target) = std::fs::read_link(&host) {
info!("mounting link {:?}", target);
if target.is_absolute() {
let guest = tmp_dir.join(target.strip_prefix("/").unwrap());
std::fs::create_dir_all(&guest).unwrap();
match mount::Mount::bind(&target, guest.to_str().unwrap()) {
Ok(m) => std::mem::forget(m),
Err(e) => error!("{e:?}"),
}
}
}
let guest_usr = guest.join("usr");
if let Ok(find) = find_files(guest_usr.clone()) {
std::fs::create_dir(&guest_usr).unwrap_or(());
for (f, m) in find {
// Stripped: guest path of the link.
let stripped = f.strip_prefix(&guest).unwrap();
// Target: host path of the link.
let target = tmp_dir.join(stripped);
debug!("mount link {:?} {:?} {:?}", f, target, m);
if m.is_dir() {
std::fs::create_dir(&target).unwrap_or(());
} else if let Ok(out) = std::fs::read_link(&f) {
if std::fs::remove_file(&target).is_err() {
std::fs::remove_dir_all(&target).unwrap_or(());
}
std::os::unix::fs::symlink(&out, &target).unwrap();
} else {
if std::fs::remove_file(&target).is_err() {
std::fs::remove_dir_all(&target).unwrap_or(());
}
std::os::unix::fs::symlink(Path::new(&host).join(&stripped), &target).unwrap();
}
}
}
}
let builder_base = format!("{name}-builder.sh");
let tmp_builder = tmp_store.join(&builder_base);
let builder = store.join(&builder_base);
std::fs::write(&tmp_builder, &r.script).unwrap();
let mut perm = std::fs::metadata(&tmp_builder).unwrap().permissions();
perm.set_mode(0o555);
std::fs::set_permissions(&tmp_builder, perm).unwrap();
let (uid, gid) = {
let user_ffi = CString::new(user).unwrap();
let pw = unsafe { libc::getpwnam(user_ffi.as_ptr()) };
assert!(!pw.is_null());
let pw = unsafe { &*pw };
(pw.pw_uid, pw.pw_gid)
};
std::os::unix::fs::chown(tmp_dir, Some(uid), Some(gid)).unwrap();
std::os::unix::fs::chown(tmp_store, Some(uid), Some(gid)).unwrap();
let out_env = format!("DESTDIR={}", dest.to_str().unwrap()).to_string();
let out_env = CString::new(out_env.as_str()).unwrap();
let c = CString::new(builder.to_str().unwrap()).unwrap();
let pid = unsafe { libc::fork() };
if pid == 0 {
info!("chrooting to {:?}", tmp_dir);
privdrop::PrivDrop::default()
.chroot(tmp_dir)
.user(user)
.apply()
.unwrap();
std::env::set_current_dir("/").unwrap();
debug!("execve {:?}", c);
unsafe {
libc::execve(
c.as_ptr(),
[c.as_ptr(), std::ptr::null()].as_ptr(),
[out_env.as_ptr(), std::ptr::null()].as_ptr(),
);
}
panic!("execve failed: {:?}", std::io::Error::last_os_error())
} else {
let mut status = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
debug!("fork returned {status}");
if status != 0 {
debug!("returning error");
return Err(Error::BuildReturn { status });
}
}
info!("chrooting to {:?}", tmp_dir);
unsafe {
let c = CString::new(tmp_dir.to_str().unwrap()).unwrap();
libc::chroot(c.as_ptr());
}
if let Ok(f) = find_files(dest.to_path_buf()) {
for (f, _meta) in f {
// Potentially patch the ELF.
debug!("patching {f:?}");
if let Err(e) = patch_result_elf(&tmp_dir, &f, &r.target) {
error!("{:?}", e);
}
}
}
Ok(())
}
/// Create the `blake3sums` file of `p` and hash that file.
fn hash_all(p: &Path) -> Result<blake3::Hash, Error> {
let mut hashes = Vec::new();
for (f, _meta) in find_files(p.to_path_buf())? {
// hash + write
info!("hashing {:?}", f);
if let Ok(link) = std::fs::read_link(&f) {
hashes.push((f, link.to_str().unwrap().to_string()))
} else if f.is_file() {
let file = std::fs::File::open(&f)?;
let mut hasher = blake3::Hasher::new();
hasher.update_reader(file).unwrap();
let hex = data_encoding::HEXLOWER.encode(hasher.finalize().as_bytes());
hashes.push((f, hex))
}
}
hashes.sort_by(|a, b| a.0.cmp(&b.0));
info!("hashed all");
let mut output_hasher = blake3::Hasher::new();
let blakesums = p.join("blake3sums");
let mut file = std::fs::File::create(&blakesums).unwrap();
use std::io::Write;
for (path, hash) in hashes {
let path = path.to_str().unwrap();
writeln!(file, "{} {}", hash, path)?;
writeln!(output_hasher, "{} {}", hash, path)?;
}
Ok(output_hasher.finalize())
}
message AddPathRequest {
message File {
string name = 1;
int64 length = 2;
int32 permissions = 3;
}
message FileContents {
int64 start = 1;
bytes content = 2;
}
message Directory {
string name = 1;
int32 permissions = 3;
}
oneof request {
File file = 1;
Directory directory = 2;
FileContents contents = 3;
}
}
let connection address port =
(* Setup Http/2 connection *)
let* addresses =
Lwt_unix.getaddrinfo address (string_of_int port)
[ Unix.(AI_FAMILY PF_INET) ]
in
let socket = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
let* () = Lwt_unix.connect socket (List.hd addresses).Unix.ai_addr in
let error_handler e =
raise (Error e)
in
H2_lwt_unix.Client.create_connection ~error_handler socket
type store_path = { path : string }
~do_request:
(H2_lwt_unix.Client.request connection ~error_handler:(fun _ ->
failwith "Error"))
~handler:
(Client.Rpc.unary enc ~f:(fun decoder ->
let+ decoder = decoder in
match decoder with
| Some decoder -> (
Reader.create decoder |> decode |> function
| Ok v -> v
| Error e ->
failwith
(Printf.sprintf "Could not decode request: %s"
(Result.show_error e)))
| None -> Elpe.Derivation.Response.make ()))
()
let derivation connection ~name ~builder ~paths ~target =
let req = Elpegrpc.Elpe.DerivationRequest.make ~name ~builder ~paths ~target () in
let open Ocaml_protoc_plugin in
let open Elpegrpc.Elpe in
let encode, decode = Service.make_client_functions Elpe.derivation in
let enc = encode req |> Writer.contents in
Client.call ~service:"elpe.Elpe" ~rpc:"Derivation"
let rec walk_dir_rec encode f buf path path_name =
let open Ocaml_protoc_plugin in
let* dir = Lwt_unix.opendir path in
Lwt.finalize
(fun () ->
let open Elpegrpc.Elpe in
let rec walk () =
Lwt.catch
(fun () ->
let* entry = Lwt_unix.readdir dir in
if entry = ".." || entry = "." then walk ()
else
let path = Filename.concat path entry in
let path_name = Filename.concat path_name entry in
let* stat = Lwt_unix.lstat path in
let* () =
match stat.st_kind with
| Unix.S_DIR ->
let req =
AddPathRequest.make
~request:
(`Directory
(AddPathRequest.Directory.make ~name:path_name
~permissions:0o644 ()))
()
in
let enc = encode req |> Writer.contents in
f (Some enc);
walk_dir_rec encode f buf path path_name
| Unix.S_REG ->
let* file = Lwt_unix.openfile path [ O_RDONLY ] 0 in
let ff =
AddPathRequest.File.make ~name:path_name
~length:stat.st_size ~permissions:0o644 ()
in
let req = AddPathRequest.make ~request:(`File ff) () in
let enc = encode req |> Writer.contents in
let () = f (Some enc) in
let rec read_all n =
let* r = Lwt_unix.read file buf 0 4096 in
if r != 0 then
let req =
AddPathRequest.make
~request:
(`Contents
(AddPathRequest.FileContents.make ~start:n
~content:(Bytes.sub buf 0 r) ()))
()
in
let enc = encode req |> Writer.contents in
let () = f (Some enc) in
read_all (n + r)
else Lwt.return ()
in
read_all 0
| _ -> Lwt.return ()
in
walk ())
(function End_of_file -> Lwt.return () | e -> Lwt.fail e)
in
walk ())
(fun () -> Lwt_unix.closedir dir)
let add_path connection path0 =
let open Ocaml_protoc_plugin in
let open Elpegrpc.Elpe in
let encode, decode = Service.make_client_functions Elpe.addPath in
Client.call ~service:"elpe.Elpe" ~rpc:"AddPath"
~do_request:
(H2_lwt_unix.Client.request connection ~error_handler:(fun _ ->
failwith "Error"))
~handler:
(Client.Rpc.client_streaming ~f:(fun f response ->
let buf = Bytes.create 4096 in
let* _ = walk_dir_rec encode f buf path0 "" in
f None;
let+ decoder = response in
match decoder with
| Some decoder -> (
Reader.create decoder |> decode |> function
| Ok v -> v
| Error e ->
failwith
(Printf.sprintf "Could not decode request: %s"
(Result.show_error e)))
| None -> Elpe.Derivation.Response.make ()))
()
type build_result = { destdir : string list; paths : string list }
method setup : string list Lwt.t = Lwt.return []
method build : string list Lwt.t = self#setup
(* Returns the script to setup the build process, also used in shells. *)
method setup : string Lwt.t = Lwt.return ""
(* List of paths resulting from building the package. *)
method build : build_result Lwt.t = failwith "Not implemented"
method target : string = "x86_64-linux-gnu"
val extra_paths : string list ref = ref []
method derivation (drv : derivation) =
let* path = drv#build in
extra_paths := path.destdir @ !extra_paths;
Lwt.return path
method local_src p =
let path_drv =
object
inherit derivation
method name = Filename.basename p
method! build =
let c =
match !backend_conn with
| None -> failwith "no conn"
| Some c -> c
in
let* res = add_path c p in
let res, _ = Result.get_ok res in
match res with
| `Ok r -> Lwt.return { destdir = r.destdir; paths = r.paths }
| `Error e -> failwith e
| _ -> assert false
end
in
let* built = path_drv#build in
extra_paths := built.destdir @ !extra_paths;
Lwt.return (List.hd built.destdir)
method src : string Lwt.t = failwith ("No src defined for " ^ self#name)
let* bash = (ubuntu "bash-static")#build in
let bash = List.hd bash in
let* b = self#build_inputs in
let* b =
Lwt_list.map_p
let* bash = self#derivation (ubuntu "bash-static") in
let bash = List.hd bash.destdir in
let* build_inputs = self#build_inputs in
let* () =
Lwt_list.iter_p
let path =
List.fold_left
(List.fold_left (fun acc x -> if acc == "" then x else acc ^ ":" ^ x))
"" b
in
Lwt.return [ "#!" ^ bash ^ "\n" ^ "export PATH=" ^ path ^ "\n" ]
end
Lwt.return
("#!" ^ bash
^ "/usr/bin/bash-static\n\
set -xe\n\
export PATH=/usr/bin\n\
export LIBRARY_PATH=/lib/" ^ self#target ^ ":/lib64/" ^ self#target
^ ":/usr/lib:/usr/lib64:/usr/lib/x86_64-linux-gnu\n\n")
let run_shell (spec : std_derivation) cmd =
Lwt_main.run
(let open Lwt.Syntax in
let port = 50051 in
let address = "127.0.0.1" in
let* c = connection address port in
backend_conn := Some c;
let* b = spec#setup in
method unpack_phase =
let* src = self#src in
let* all = Lwt.all [ self#pre_unpack; self#post_unpack ] in
match all with
| pre :: post :: _ -> Lwt.return (pre ^ "\ncp -R " ^ src ^ "/* .\n" ^ post)
| _ -> assert false
let* bash = (ubuntu "bash-static")#build in
let bash = List.hd bash in
let f = Filename.temp_dir "elpe-" "" ^ "/setup" in
Unix.mkfifo f 0o666;
let pid =
Unix.create_process
(bash ^ "/usr/bin/bash-static")
(match cmd with
| None -> [| "bash"; "--init-file"; f; "-i" |]
| Some cmd -> [| "bash"; "--init-file"; f; "-i"; "-c"; cmd |])
Unix.stdin Unix.stdout Unix.stderr
in
let p = Unix.openfile f [ Unix.O_WRONLY ] 0o644 in
List.fold_left
(fun () b ->
let _ = Unix.write_substring p b 0 (String.length b) in
())
() b;
Unix.close p;
method pre_configure = Lwt.return ""
method post_configure = Lwt.return ""
match Unix.waitpid [] pid with
| _, Unix.WEXITED e -> Lwt.return e
| _ -> failwith "Unknown")
method configure_phase =
let* all = Lwt.all [ self#pre_configure; self#post_configure ] in
match all with
| pre :: post :: _ ->
Lwt.return
(pre ^ "\nif [[ -e configure ]]; then ./configure; fi\n" ^ post)
| _ -> assert false
let run_build (spec : derivation) =
Lwt_main.run
(let open Lwt.Syntax in
let port = 50051 in
let address = "127.0.0.1" in
let* c = connection address port in
backend_conn := Some c;
let* b = spec#build in
method pre_build = Lwt.return ""
method post_build = Lwt.return ""
let* bash = (ubuntu "bash-static")#build in
let bash = List.hd bash in
method build_phase =
let* all = Lwt.all [ self#pre_build; self#post_build ] in
match all with
| pre :: post :: _ -> Lwt.return (pre ^ "\nif [[ -e Makefile ]]; then make; fi\n" ^ post)
| _ -> assert false
let pid =
Unix.create_process
(bash ^ "/usr/bin/bash-static")
[| "bash"; "--init-file"; f; "-i" |]
Unix.stdin Unix.stdout Unix.stderr
in
method install_phase =
let* all = Lwt.all [ self#pre_install; self#post_install ] in
match all with
| pre :: post :: _ -> Lwt.return (pre ^ "\nif [[ -e Makefile ]]; then make install; fi\n" ^ post)
| _ -> assert false
let p = Unix.openfile f [ Unix.O_WRONLY ] 0o644 in
List.fold_left
(fun () b ->
let _ = Unix.write_substring p b 0 (String.length b) in
())
() b;
Unix.close p;
method! build =
let* phases =
Lwt.all
[
self#setup;
self#unpack_phase;
self#configure_phase;
self#build_phase;
self#install_phase;
]
in
let builder = String.concat "\n" phases in
let c =
match !backend_conn with None -> failwith "no conn" | Some c -> c
in
let* res = derivation c ~name:self#name ~builder ~paths:!extra_paths ~target:self#target in
let res, _ = Result.get_ok res in
match res with
| `Ok r -> Lwt.return { destdir = r.destdir; paths = r.paths }
| `Error e -> failwith e
| _ -> assert false
end
let _ =
match Unix.waitpid [] pid with
| _, Unix.WEXITED e -> print_endline ("ended waitpid " ^ string_of_int e)
| _ -> print_endline "other"
in
Lwt.return ())
let last_built_module : std_derivation option ref = ref None
let build (spec : std_derivation) = last_built_module := Some spec
open Elpe
let connection address port =
let open Lwt.Syntax in
(* Setup Http/2 connection *)
let* addresses =
Lwt_unix.getaddrinfo address (string_of_int port)
[ Unix.(AI_FAMILY PF_INET) ]
in
let socket = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
let* () = Lwt_unix.connect socket (List.hd addresses).Unix.ai_addr in
let error_handler e = raise (Elpe.Error e) in
H2_lwt_unix.Client.create_connection ~error_handler socket
let run_shell (spec : Elpe.std_derivation) cmd =
Lwt_main.run
(let open Lwt.Syntax in
let port = 50051 in
let address = "127.0.0.1" in
let* c = connection address port in
Elpe.backend_conn := Some c;
let* b = spec#setup in
let usage_msg = "elpe [-verbose] [-c <command>] <file1> [<file2>]"
let verbose = ref false
let cmd = ref ""
let input_files = ref []
let anon_fun filename = input_files := filename :: !input_files
let* bash = (Elpe.ubuntu "bash-static")#build in
let bash = List.hd bash.destdir in
let f = Filename.temp_dir "elpe-" "" ^ "/setup" in
Unix.mkfifo f 0o666;
let speclist =
[
("-verbose", Arg.Set verbose, "Output debug information");
("-c", Arg.Set_string cmd, "Run the specified command");
]
let pid =
Unix.create_process
(bash ^ "/usr/bin/bash-static")
(match cmd with
| None -> [| "bash"; "--init-file"; f; "-i" |]
| Some cmd -> [| "bash"; "--init-file"; f; "-i"; "-c"; cmd |])
Unix.stdin Unix.stdout Unix.stderr
in
let p = Unix.openfile f [ Unix.O_WRONLY ] 0o644 in
let _ = Unix.write_substring p b 0 (String.length b) in
Unix.close p;
match Unix.waitpid [] pid with
| _, Unix.WEXITED e -> Lwt.return e
| _ -> failwith "Unknown")
let run_build (spec : Elpe.derivation) =
Lwt_main.run
(let open Lwt.Syntax in
let port = 50051 in
let address = "127.0.0.1" in
let* c = connection address port in
Elpe.backend_conn := Some c;
let* b = spec#build in
print_endline (List.fold_left (fun _ x -> x) "" b.destdir);
Lwt.return ())
Dynlink.loadfile obj;
match !last_built_module with
| None -> 0
| Some last ->
run_shell last (if String.length !cmd == 0 then None else Some !cmd)
Dynlink.loadfile obj
let shell =
Core.Command.basic ~summary:""
(let%map_open.Command cmd =
flag "-c" (optional string) ~doc:"Command to run in this shell"
and files = anon (non_empty_sequence_as_list ("file" %: string)) in
fun () ->
Elpe.last_built_module := None;
compile files;
match !Elpe.last_built_module with
| None -> Unix._exit 0
| Some last -> Unix._exit (run_shell last cmd))
let build =
Core.Command.basic ~summary:""
(let%map_open.Command files =
anon (non_empty_sequence_as_list ("file" %: string))
in
fun () ->
Elpe.last_built_module := None;
compile files;
let _ =
match !Elpe.last_built_module with
| None -> ()
| Some last -> run_build (last :> Elpe.derivation)
in
())
let () =
Arg.parse speclist anon_fun usage_msg;
if !input_files != [] then
let exit = compile_and_run () in
Unix._exit exit
else Printf.eprintf "\n"
let command =
Core.Command.group ~summary:"Elpe" [ ("shell", shell); ("build", build) ]
let () = Command_unix.run command
(* if !input_files != [] then *)
(* let exit = compile_and_run () in *)
(* Unix._exit exit *)
(* else Printf.eprintf "\n" *)
};
# grpc-src = pkgs.fetchFromGitHub {
# owner = "dialohq";
# repo = "ocaml-grpc";
# rev = "b71fba7067bad2cad62df9abd8b4e190e3c4fc94";
# sha256 = "sha256-NtNU0ANMLRFFLoGtHMB0ynHbyh8YTfRF+PttJi9pYHU=";
# };
grpc-src = pkgs.fetchFromGitHub {
owner = "dialohq";
repo = "ocaml-grpc";
rev = "0.2.0";
sha256 = "sha256-YARAm3EVTbptAoKA0lysJRYeKPaCaeWU6X9Lzkn+30E=";
url = "https://github.com/dialohq/ocaml-grpc/archive/refs/tags/0.2.0.tar.gz";
sha256 = "sha256-5myWWT3qziJ9m84aRXodGRZ5sGlUNBcY/6nkkzJ4in4=";
url = "https://github.com/mirage/ocaml-tar/archive/refs/tags/v3.3.0.tar.gz";
sha256 = "sha256-xxwUxq8G9G8LH8EedLaPAWNvxtpRFiWZyEv8YPgad+g=";
ocamlPackages.stringext
ocamlPackages.camlp-streams
ocamlPackages.cstruct
ocamlPackages.decompress
];
};
tar-unix = ocamlPackages.buildDunePackage rec {
pname = "tar-unix";
version = "3.3.0";
src = pkgs.fetchurl {
url = "https://github.com/mirage/ocaml-tar/archive/refs/tags/v3.3.0.tar.gz";
sha256 = "sha256-xxwUxq8G9G8LH8EedLaPAWNvxtpRFiWZyEv8YPgad+g=";
};
propagatedBuildInputs = with pkgs; [
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "clap"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "tokio-bincode"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0caad59482605518068065f14e2f91f43297d7240fa78dab9b4ba289ad0cf5b1"
dependencies = [
"bincode 1.3.3",
"bytes 0.4.12",
"serde",
"tokio-codec",
]
[[package]]
name = "tokio-codec"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b"
dependencies = [
"bytes 0.4.12",
"futures 0.1.31",
"tokio-io",
]
[[package]]