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 linkdebug!("hard link {:?} {:?}", f, dest_path);std::fs::hard_link(&f, &dest_path).unwrap_or(());}hashing.push(tokio::spawn(async move {// hash + writeinfo!("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 extensionif 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 linkdebug!("hard link {:?} {:?}", f, dest_path);tokio::fs::hard_link(&f, &dest_path).await.unwrap_or(());}hashing.push(tokio::spawn(async move {// hash + writeinfo!("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 symlinkstd::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 + writeinfo!("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) ]inlet socket = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 inlet* () = Lwt_unix.connect socket (List.hd addresses).Unix.ai_addr inlet error_handler e =raise (Error e)inH2_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 inmatch 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 () inlet open Ocaml_protoc_plugin inlet open Elpegrpc.Elpe inlet encode, decode = Service.make_client_functions Elpe.derivation inlet enc = encode req |> Writer.contents inClient.call ~service:"elpe.Elpe" ~rpc:"Derivation"
let rec walk_dir_rec encode f buf path path_name =let open Ocaml_protoc_plugin inlet* dir = Lwt_unix.opendir path inLwt.finalize(fun () ->let open Elpegrpc.Elpe inlet rec walk () =Lwt.catch(fun () ->let* entry = Lwt_unix.readdir dir inif entry = ".." || entry = "." then walk ()elselet path = Filename.concat path entry inlet path_name = Filename.concat path_name entry inlet* stat = Lwt_unix.lstat path inlet* () =match stat.st_kind with| Unix.S_DIR ->let req =AddPathRequest.make~request:(`Directory(AddPathRequest.Directory.make ~name:path_name~permissions:0o644 ()))()inlet enc = encode req |> Writer.contents inf (Some enc);walk_dir_rec encode f buf path path_name| Unix.S_REG ->let* file = Lwt_unix.openfile path [ O_RDONLY ] 0 inlet ff =AddPathRequest.File.make ~name:path_name~length:stat.st_size ~permissions:0o644 ()inlet req = AddPathRequest.make ~request:(`File ff) () inlet enc = encode req |> Writer.contents inlet () = f (Some enc) inlet rec read_all n =let* r = Lwt_unix.read file buf 0 4096 inif r != 0 thenlet req =AddPathRequest.make~request:(`Contents(AddPathRequest.FileContents.make ~start:n~content:(Bytes.sub buf 0 r) ()))()inlet enc = encode req |> Writer.contents inlet () = f (Some enc) inread_all (n + r)else Lwt.return ()inread_all 0| _ -> Lwt.return ()inwalk ())(function End_of_file -> Lwt.return () | e -> Lwt.fail e)inwalk ())(fun () -> Lwt_unix.closedir dir)let add_path connection path0 =let open Ocaml_protoc_plugin inlet open Elpegrpc.Elpe inlet encode, decode = Service.make_client_functions Elpe.addPath inClient.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 inlet* _ = walk_dir_rec encode f buf path0 "" inf None;let+ decoder = response inmatch 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 inextra_paths := path.destdir @ !extra_paths;Lwt.return pathmethod local_src p =let path_drv =objectinherit derivationmethod name = Filename.basename p
method! build =let c =match !backend_conn with| None -> failwith "no conn"| Some c -> cinlet* res = add_path c p inlet res, _ = Result.get_ok res inmatch res with| `Ok r -> Lwt.return { destdir = r.destdir; paths = r.paths }| `Error e -> failwith e| _ -> assert falseendinlet* built = path_drv#build inextra_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 inlet bash = List.hd bash inlet* b = self#build_inputs inlet* b =Lwt_list.map_p
let* bash = self#derivation (ubuntu "bash-static") inlet bash = List.hd bash.destdir inlet* build_inputs = self#build_inputs inlet* () =Lwt_list.iter_p
let path =List.fold_left(List.fold_left (fun acc x -> if acc == "" then x else acc ^ ":" ^ x))"" binLwt.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 inlet port = 50051 inlet address = "127.0.0.1" inlet* c = connection address port inbackend_conn := Some c;let* b = spec#setup in
method unpack_phase =let* src = self#src inlet* all = Lwt.all [ self#pre_unpack; self#post_unpack ] inmatch all with| pre :: post :: _ -> Lwt.return (pre ^ "\ncp -R " ^ src ^ "/* .\n" ^ post)| _ -> assert false
let* bash = (ubuntu "bash-static")#build inlet bash = List.hd bash inlet f = Filename.temp_dir "elpe-" "" ^ "/setup" inUnix.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.stderrinlet p = Unix.openfile f [ Unix.O_WRONLY ] 0o644 inList.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 ] inmatch 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 inlet port = 50051 inlet address = "127.0.0.1" inlet* c = connection address port inbackend_conn := Some c;let* b = spec#build in
method pre_build = Lwt.return ""method post_build = Lwt.return ""
let* bash = (ubuntu "bash-static")#build inlet bash = List.hd bash in
method build_phase =let* all = Lwt.all [ self#pre_build; self#post_build ] inmatch 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.stderrin
method install_phase =let* all = Lwt.all [ self#pre_install; self#post_install ] inmatch 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 inList.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;]inlet builder = String.concat "\n" phases inlet c =match !backend_conn with None -> failwith "no conn" | Some c -> cinlet* res = derivation c ~name:self#name ~builder ~paths:!extra_paths ~target:self#target inlet res, _ = Result.get_ok res inmatch res with| `Ok r -> Lwt.return { destdir = r.destdir; paths = r.paths }| `Error e -> failwith e| _ -> assert falseend
let _ =match Unix.waitpid [] pid with| _, Unix.WEXITED e -> print_endline ("ended waitpid " ^ string_of_int e)| _ -> print_endline "other"inLwt.return ())
let last_built_module : std_derivation option ref = ref Nonelet 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) ]inlet socket = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 inlet* () = Lwt_unix.connect socket (List.hd addresses).Unix.ai_addr inlet error_handler e = raise (Elpe.Error e) inH2_lwt_unix.Client.create_connection ~error_handler socketlet run_shell (spec : Elpe.std_derivation) cmd =Lwt_main.run(let open Lwt.Syntax inlet port = 50051 inlet address = "127.0.0.1" inlet* c = connection address port inElpe.backend_conn := Some c;let* b = spec#setup in
let usage_msg = "elpe [-verbose] [-c <command>] <file1> [<file2>]"let verbose = ref falselet cmd = ref ""let input_files = ref []let anon_fun filename = input_files := filename :: !input_files
let* bash = (Elpe.ubuntu "bash-static")#build inlet bash = List.hd bash.destdir inlet f = Filename.temp_dir "elpe-" "" ^ "/setup" inUnix.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.stderrinlet p = Unix.openfile f [ Unix.O_WRONLY ] 0o644 inlet _ = Unix.write_substring p b 0 (String.length b) inUnix.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 inlet port = 50051 inlet address = "127.0.0.1" inlet* c = connection address port inElpe.backend_conn := Some c;let* b = spec#build inprint_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 objlet 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)) infun () ->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))infun () ->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 != [] thenlet exit = compile_and_run () inUnix._exit exitelse 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-streamsocamlPackages.cstructocamlPackages.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]]