1use std::collections::{BTreeMap, HashMap};
6use std::fmt;
7use std::time::{Duration, Instant};
8
9use bollard::exec::{CreateExecOptions, StartExecResults};
10use std::pin::Pin;
11
12use bollard::models::{
13 ChangeType, ContainerConfig, ContainerCreateBody, CreateImageInfo, HostConfig, Mount,
14 NetworkCreateRequest, SystemInfo, VolumeCreateRequest,
15};
16use bollard::query_parameters::{
17 AttachContainerOptionsBuilder, CommitContainerOptionsBuilder, CreateContainerOptionsBuilder,
18 CreateImageOptionsBuilder, DataUsageOptionsBuilder, InspectContainerOptionsBuilder,
19 ListContainersOptionsBuilder, ListImagesOptionsBuilder, LogsOptionsBuilder,
20 RemoveContainerOptionsBuilder, RemoveImageOptionsBuilder, RemoveVolumeOptionsBuilder,
21 ResizeContainerTTYOptionsBuilder, UploadToContainerOptionsBuilder, WaitContainerOptionsBuilder,
22};
23use futures_util::stream::BoxStream;
24use futures_util::{StreamExt, TryStreamExt};
25use tracing::info;
26
27use crate::config::ByteSize;
28use crate::isolation::Isolation;
29
30pub use bollard::errors::Error;
31
32#[derive(Debug, Clone)]
34pub struct RunnerSpec {
35 pub name: String,
37 pub image: String,
39 pub profile: String,
41 pub slice: String,
43 pub cpus: u32,
45 pub memory: ByteSize,
47 pub env: Vec<String>,
49 pub mounts: Vec<Mount>,
51 pub network: Option<String>,
53 pub labels: BTreeMap<String, String>,
55 pub entrypoint: Option<Vec<String>>,
57 pub jit_config: Option<JitConfig>,
59}
60
61pub const JIT_CONFIG_PATH: &str = "/run/runnerd/jitconfig";
64
65#[derive(Clone)]
67pub struct JitConfig(pub String);
68
69impl fmt::Debug for JitConfig {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str("JitConfig(<redacted>)")
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct RunnerState {
78 pub running: bool,
80 pub exit_code: Option<i64>,
82 pub oom_killed: bool,
84 pub privileged: bool,
86 pub runtime: Option<String>,
88 pub pid: Option<u32>,
90 pub address: Option<String>,
92 pub started_at: Option<u64>,
94 pub memory: Option<u64>,
96 pub cpus: Option<u64>,
98}
99
100pub type Output = BoxStream<'static, Result<Vec<u8>, Error>>;
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct Image {
106 pub reference: String,
108 pub labels: HashMap<String, String>,
110 pub bytes: u64,
112}
113
114pub struct Tty {
116 pub container: String,
118 pub output: BoxStream<'static, Result<Vec<u8>, Error>>,
120 pub input: Pin<Box<dyn tokio::io::AsyncWrite + Send>>,
122}
123
124impl fmt::Debug for Tty {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.debug_struct("Tty")
127 .field("container", &self.container)
128 .finish_non_exhaustive()
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct TtySize {
135 pub cols: u16,
137 pub rows: u16,
139}
140
141pub fn create_body(spec: &RunnerSpec, isolation: &dyn Isolation) -> ContainerCreateBody {
143 let memory = i64::try_from(spec.memory.0).unwrap_or(i64::MAX);
145 let mut host_config = HostConfig {
146 cgroup_parent: Some(spec.slice.clone()),
147 memory: Some(memory),
148 memory_swap: Some(memory),
150 nano_cpus: Some(i64::from(spec.cpus) * 1_000_000_000),
151 init: Some(true),
152 mounts: Some(spec.mounts.clone()),
153 network_mode: spec.network.clone(),
154 ..Default::default()
155 };
156 isolation.configure(&mut host_config);
157 let mut labels: HashMap<String, String> = spec.labels.clone().into_iter().collect();
158 labels.insert("runnerd.profile".to_owned(), spec.profile.clone());
159 labels.insert("runnerd.isolation".to_owned(), isolation.name().to_owned());
160 ContainerCreateBody {
161 image: Some(spec.image.clone()),
162 env: Some(spec.env.clone()),
163 entrypoint: spec.entrypoint.clone(),
164 labels: Some(labels),
165 host_config: Some(host_config),
166 ..Default::default()
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct Container {
173 pub id: String,
175 pub name: String,
177 pub running: bool,
179 pub labels: HashMap<String, String>,
181}
182
183pub fn is_not_found(error: &Error) -> bool {
185 matches!(
186 error,
187 Error::DockerResponseServerError {
188 status_code: 404,
189 ..
190 }
191 )
192}
193
194pub trait DockerApi: Send + Sync {
197 fn start(
199 &self,
200 spec: &RunnerSpec,
201 isolation: &dyn Isolation,
202 ) -> impl Future<Output = Result<String, Error>> + Send;
203 fn inspect(&self, container: &str) -> impl Future<Output = Result<RunnerState, Error>> + Send;
205 fn remove(&self, container: &str) -> impl Future<Output = Result<(), Error>> + Send;
207 fn run(
209 &self,
210 name: &str,
211 body: ContainerCreateBody,
212 ) -> impl Future<Output = Result<String, Error>> + Send;
213 fn list(&self, label: &str) -> impl Future<Output = Result<Vec<Container>, Error>> + Send;
215 fn wait(&self, container: &str) -> impl Future<Output = Result<i64, Error>> + Send;
217 fn logs(&self, container: &str) -> impl Future<Output = Result<String, Error>> + Send;
219 fn logs_since(
222 &self,
223 container: &str,
224 since: u64,
225 ) -> impl Future<Output = Result<String, Error>> + Send;
226 fn exec(
229 &self,
230 container: &str,
231 command: Vec<String>,
232 ) -> impl Future<Output = Result<Output, Error>> + Send;
233 fn image_present(&self, image: &str) -> impl Future<Output = Result<bool, Error>> + Send;
235 fn pull(&self, image: &str) -> impl Future<Output = Result<(), Error>> + Send;
237 fn ensure_network(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
239 fn ensure_volume(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
241 fn remove_volume(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
243 fn volume_sizes(&self) -> impl Future<Output = Result<BTreeMap<String, u64>, Error>> + Send;
245 fn changes(&self, container: &str) -> impl Future<Output = Result<Vec<String>, Error>> + Send;
247 fn written(&self, container: &str) -> impl Future<Output = Result<u64, Error>> + Send;
249 fn commit(
252 &self,
253 container: &str,
254 reference: &str,
255 labels: BTreeMap<String, String>,
256 ) -> impl Future<Output = Result<u64, Error>> + Send;
257 fn images(&self, label: &str) -> impl Future<Output = Result<Vec<Image>, Error>> + Send;
259 fn remove_image(&self, reference: &str) -> impl Future<Output = Result<(), Error>> + Send;
261 fn free_space(&self) -> impl Future<Output = Result<u64, Error>> + Send;
263 fn shell(
266 &self,
267 name: &str,
268 body: ContainerCreateBody,
269 size: TtySize,
270 ) -> impl Future<Output = Result<Tty, Error>> + Send;
271}
272
273#[derive(Debug)]
275pub struct Docker {
276 api: bollard::Docker,
277}
278
279impl Docker {
280 pub fn connect() -> Result<Docker, Error> {
282 connect_to(std::env::var("DOCKER_HOST").ok().as_deref())
283 }
284
285 pub async fn info(&self) -> Result<SystemInfo, Error> {
287 self.api.info().await
288 }
289
290 pub async fn image_labels(
292 &self,
293 image: &str,
294 ) -> Result<Option<HashMap<String, String>>, Error> {
295 match self.api.inspect_image(image).await {
296 Ok(inspect) => Ok(Some(
297 inspect
298 .config
299 .and_then(|config| config.labels)
300 .unwrap_or_default(),
301 )),
302 Err(error) if is_not_found(&error) => Ok(None),
303 Err(error) => Err(error),
304 }
305 }
306
307 pub async fn registry_platforms(&self, image: &str) -> Result<Vec<String>, Error> {
310 let inspect = self.api.inspect_registry_image(image, None).await?;
311 Ok(inspect
312 .platforms
313 .into_iter()
314 .filter_map(|platform| Some(format!("{}/{}", platform.os?, platform.architecture?)))
315 .filter(|platform| platform != "unknown/unknown")
317 .collect())
318 }
319}
320
321impl DockerApi for Docker {
322 async fn start(&self, spec: &RunnerSpec, isolation: &dyn Isolation) -> Result<String, Error> {
323 let options = CreateContainerOptionsBuilder::new()
324 .name(&spec.name)
325 .build();
326 let id = self
327 .api
328 .create_container(Some(options), create_body(spec, isolation))
329 .await?
330 .id;
331 let started = async {
332 if let Some(jit_config) = &spec.jit_config {
333 let options = UploadToContainerOptionsBuilder::new().path("/").build();
334 let tar = jit_config_tar(jit_config)?;
335 let tar = futures_util::stream::once(std::future::ready(tar.into()));
337 self.api
338 .upload_to_container(&id, Some(options), bollard::body_stream(tar))
339 .await?;
340 }
341 self.api.start_container(&id, None).await
342 };
343 if let Err(error) = started.await {
344 let _ = self.remove(&id).await;
346 return Err(error);
347 }
348 Ok(id)
349 }
350
351 async fn inspect(&self, container: &str) -> Result<RunnerState, Error> {
352 let response = self.api.inspect_container(container, None).await?;
353 let state = response.state.unwrap_or_default();
354 let host_config = response.host_config.unwrap_or_default();
355 let address = response
356 .network_settings
357 .and_then(|settings| settings.networks)
358 .and_then(|networks| {
359 networks
360 .into_values()
361 .find_map(|network| network.ip_address.filter(|address| !address.is_empty()))
362 });
363 Ok(RunnerState {
364 running: state.running.unwrap_or(false),
365 exit_code: state.exit_code,
366 oom_killed: state.oom_killed.unwrap_or(false),
367 privileged: host_config.privileged.unwrap_or(false),
368 runtime: host_config.runtime,
369 pid: state
370 .pid
371 .and_then(|pid| u32::try_from(pid).ok())
372 .filter(|pid| *pid > 0),
373 address,
374 started_at: state.started_at.as_deref().and_then(crate::time::parse_ms),
375 memory: host_config
376 .memory
377 .and_then(|memory| u64::try_from(memory).ok())
378 .filter(|memory| *memory > 0),
379 cpus: host_config
380 .nano_cpus
381 .and_then(|nano| u64::try_from(nano / 1_000_000).ok())
382 .filter(|cpus| *cpus > 0),
383 })
384 }
385
386 async fn remove(&self, container: &str) -> Result<(), Error> {
387 let options = RemoveContainerOptionsBuilder::new()
389 .force(true)
390 .v(true)
391 .build();
392 self.api.remove_container(container, Some(options)).await
393 }
394
395 async fn run(&self, name: &str, body: ContainerCreateBody) -> Result<String, Error> {
396 let options = CreateContainerOptionsBuilder::new().name(name).build();
397 let id = self.api.create_container(Some(options), body).await?.id;
398 if let Err(error) = self.api.start_container(&id, None).await {
399 let _ = self.remove(&id).await;
400 return Err(error);
401 }
402 Ok(id)
403 }
404
405 async fn list(&self, label: &str) -> Result<Vec<Container>, Error> {
406 let options = ListContainersOptionsBuilder::new()
407 .all(true)
408 .filters(&HashMap::from([("label", vec![label])]))
409 .build();
410 let containers = self.api.list_containers(Some(options)).await?;
411 Ok(containers
412 .into_iter()
413 .map(|container| Container {
414 id: container.id.unwrap_or_default(),
415 name: container
416 .names
417 .and_then(|names| names.into_iter().next())
418 .unwrap_or_default()
419 .trim_start_matches('/')
420 .to_owned(),
421 running: container.state
422 == Some(bollard::models::ContainerSummaryStateEnum::RUNNING),
423 labels: container.labels.unwrap_or_default(),
424 })
425 .collect())
426 }
427
428 async fn wait(&self, container: &str) -> Result<i64, Error> {
429 let options = WaitContainerOptionsBuilder::new()
430 .condition("not-running")
431 .build();
432 let mut responses = self.api.wait_container(container, Some(options));
433 match responses.next().await {
434 Some(Ok(response)) => Ok(response.status_code),
435 Some(Err(Error::DockerContainerWaitError { code, .. })) => Ok(code),
437 Some(Err(error)) => Err(error),
438 None => Ok(0),
439 }
440 }
441
442 async fn logs(&self, container: &str) -> Result<String, Error> {
443 let options = LogsOptionsBuilder::new().stdout(true).stderr(true).build();
444 let chunks: Vec<_> = self
445 .api
446 .logs(container, Some(options))
447 .try_collect()
448 .await?;
449 Ok(chunks.iter().map(ToString::to_string).collect())
450 }
451
452 async fn logs_since(&self, container: &str, since: u64) -> Result<String, Error> {
453 let options = LogsOptionsBuilder::new()
454 .stdout(true)
455 .stderr(true)
456 .timestamps(true)
457 .since(i32::try_from(since).unwrap_or(i32::MAX))
458 .build();
459 let chunks: Vec<_> = self
460 .api
461 .logs(container, Some(options))
462 .try_collect()
463 .await?;
464 Ok(chunks.iter().map(ToString::to_string).collect())
465 }
466
467 async fn exec(&self, container: &str, command: Vec<String>) -> Result<Output, Error> {
468 let options = CreateExecOptions {
469 cmd: Some(command),
470 user: Some("root".to_owned()),
471 attach_stdout: Some(true),
472 attach_stderr: Some(true),
473 ..Default::default()
474 };
475 let exec = self.api.create_exec(container, options).await?;
476 match self.api.start_exec(&exec.id, None).await? {
477 StartExecResults::Attached { output, .. } => Ok(output
478 .try_filter_map(|output| {
479 std::future::ready(Ok(match output {
480 bollard::container::LogOutput::StdOut { message } => Some(message.to_vec()),
481 _ => None,
482 }))
483 })
484 .boxed()),
485 StartExecResults::Detached => Ok(futures_util::stream::empty().boxed()),
486 }
487 }
488
489 async fn image_present(&self, image: &str) -> Result<bool, Error> {
490 Ok(self.image_labels(image).await?.is_some())
491 }
492
493 async fn pull(&self, image: &str) -> Result<(), Error> {
494 let options = CreateImageOptionsBuilder::new().from_image(image).build();
495 let mut stream = self.api.create_image(Some(options), None, None);
496 let mut progress = PullProgress::default();
497 let mut logged = Instant::now();
498 info!("pulling {image}");
499 while let Some(update) = stream.try_next().await? {
500 progress.update(&update);
501 if logged.elapsed() >= PULL_LOG_INTERVAL {
502 logged = Instant::now();
503 info!("pulling {image}: {progress}");
504 }
505 }
506 info!("pulled {image}");
507 Ok(())
508 }
509
510 async fn ensure_network(&self, name: &str) -> Result<(), Error> {
511 match self.api.inspect_network(name, None).await {
512 Err(error) if is_not_found(&error) => {
513 let request = NetworkCreateRequest {
514 name: name.to_owned(),
515 driver: Some("bridge".to_owned()),
516 ..Default::default()
517 };
518 self.api.create_network(request).await.map(|_| ())
519 }
520 other => other.map(|_| ()),
521 }
522 }
523
524 async fn ensure_volume(&self, name: &str) -> Result<(), Error> {
525 let request = VolumeCreateRequest {
527 name: Some(name.to_owned()),
528 ..Default::default()
529 };
530 self.api.create_volume(request).await.map(|_| ())
531 }
532
533 async fn remove_volume(&self, name: &str) -> Result<(), Error> {
534 let options = RemoveVolumeOptionsBuilder::new().build();
535 match self.api.remove_volume(name, Some(options)).await {
536 Err(error) if is_not_found(&error) => Ok(()),
537 other => other,
538 }
539 }
540
541 async fn volume_sizes(&self) -> Result<BTreeMap<String, u64>, Error> {
542 let options = DataUsageOptionsBuilder::new().verbose(true).build();
544 let usage = self.api.df(Some(options)).await?;
545 Ok(usage
547 .volume_usage
548 .and_then(|volumes| volumes.items)
549 .unwrap_or_default()
550 .into_iter()
551 .filter_map(|volume| {
552 let name = volume.get("Name")?.as_str()?.to_owned();
553 let size = volume.get("UsageData")?.get("Size")?.as_u64()?;
554 Some((name, size))
555 })
556 .collect())
557 }
558
559 async fn changes(&self, container: &str) -> Result<Vec<String>, Error> {
560 Ok(self
561 .api
562 .container_changes(container)
563 .await?
564 .unwrap_or_default()
565 .into_iter()
566 .filter(|change| change.kind != ChangeType::_2)
567 .map(|change| change.path)
568 .collect())
569 }
570
571 async fn written(&self, container: &str) -> Result<u64, Error> {
572 let options = InspectContainerOptionsBuilder::new().size(true).build();
573 let response = self.api.inspect_container(container, Some(options)).await?;
574 Ok(response
575 .size_rw
576 .and_then(|bytes| u64::try_from(bytes).ok())
577 .unwrap_or(0))
578 }
579
580 async fn commit(
581 &self,
582 container: &str,
583 reference: &str,
584 labels: BTreeMap<String, String>,
585 ) -> Result<u64, Error> {
586 let (repository, tag) = reference.rsplit_once(':').unwrap_or((reference, "latest"));
587 let options = CommitContainerOptionsBuilder::new()
588 .container(container)
589 .repo(repository)
590 .tag(tag)
591 .pause(false)
592 .build();
593 let config = ContainerConfig {
594 labels: Some(labels.into_iter().collect()),
595 ..Default::default()
596 };
597 let parent = match self.api.inspect_container(container, None).await?.image {
599 Some(parent) => self.api.inspect_image(&parent).await?.size.unwrap_or(0),
600 None => 0,
601 };
602 let id = self.api.commit_container(options, config).await?.id;
603 let size = self.api.inspect_image(&id).await?.size.unwrap_or(0);
604 Ok(u64::try_from(size - parent).unwrap_or(0))
605 }
606
607 async fn images(&self, label: &str) -> Result<Vec<Image>, Error> {
608 let options = ListImagesOptionsBuilder::new()
609 .filters(&HashMap::from([("label", vec![label])]))
610 .build();
611 let images = self.api.list_images(Some(options)).await?;
612 let mut parents: HashMap<String, i64> = HashMap::new();
613 let mut found = Vec::new();
614 for image in images {
615 let parent = if image.parent_id.is_empty() {
616 0
617 } else if let Some(size) = parents.get(&image.parent_id) {
618 *size
619 } else {
620 let size = self
621 .api
622 .inspect_image(&image.parent_id)
623 .await
624 .ok()
625 .and_then(|parent| parent.size)
626 .unwrap_or(0);
627 parents.insert(image.parent_id.clone(), size);
628 size
629 };
630 found.push(Image {
631 reference: image.repo_tags.into_iter().next().unwrap_or(image.id),
632 labels: image.labels,
633 bytes: u64::try_from(image.size - parent).unwrap_or(0),
634 });
635 }
636 Ok(found)
637 }
638
639 async fn remove_image(&self, reference: &str) -> Result<(), Error> {
640 let options = RemoveImageOptionsBuilder::new().build();
641 match self.api.remove_image(reference, Some(options), None).await {
642 Err(error) if is_not_found(&error) => Ok(()),
643 other => other.map(|_| ()),
644 }
645 }
646
647 async fn free_space(&self) -> Result<u64, Error> {
648 let root = self.api.info().await?.docker_root_dir.unwrap_or_default();
649 let stat = rustix::fs::statvfs(root.as_str()).map_err(|error| Error::IOError {
651 err: std::io::Error::other(format!("cannot measure the disk of {root}: {error}")),
652 })?;
653 Ok(stat.f_bavail.saturating_mul(stat.f_frsize))
654 }
655
656 async fn shell(
657 &self,
658 name: &str,
659 body: ContainerCreateBody,
660 size: TtySize,
661 ) -> Result<Tty, Error> {
662 let options = CreateContainerOptionsBuilder::new().name(name).build();
663 let id = self.api.create_container(Some(options), body).await?.id;
664 let attached = async {
665 let options = AttachContainerOptionsBuilder::new()
666 .stream(true)
667 .stdin(true)
668 .stdout(true)
669 .stderr(true)
670 .build();
671 let attached = self.api.attach_container(&id, Some(options)).await?;
672 self.api.start_container(&id, None).await?;
673 let resize = ResizeContainerTTYOptionsBuilder::new()
674 .w(i32::from(size.cols))
675 .h(i32::from(size.rows))
676 .build();
677 self.api.resize_container_tty(&id, resize).await?;
678 Ok::<_, Error>(attached)
679 };
680 match attached.await {
681 Ok(attached) => Ok(Tty {
682 container: id,
683 output: attached
684 .output
685 .map_ok(|output| output.into_bytes().to_vec())
686 .boxed(),
687 input: attached.input,
688 }),
689 Err(error) => {
690 let _ = self.remove(&id).await;
691 Err(error)
692 }
693 }
694 }
695}
696
697const PULL_LOG_INTERVAL: Duration = Duration::from_secs(10);
699
700#[derive(Debug, Default)]
702struct PullProgress {
703 layers: BTreeMap<String, Layer>,
704}
705
706#[derive(Debug, Default)]
707struct Layer {
708 downloaded: u64,
709 size: u64,
710 done: bool,
711}
712
713impl PullProgress {
714 fn update(&mut self, update: &CreateImageInfo) {
715 let (Some(id), Some(status)) = (&update.id, &update.status) else {
716 return;
717 };
718 let bytes = |value: Option<i64>| value.and_then(|value| u64::try_from(value).ok());
719 let detail = update.progress_detail.as_ref();
720 match status.as_str() {
721 "Pulling fs layer" | "Waiting" => {
722 self.layers.entry(id.clone()).or_default();
723 }
724 "Downloading" => {
725 let layer = self.layers.entry(id.clone()).or_default();
726 layer.downloaded =
727 bytes(detail.and_then(|detail| detail.current)).unwrap_or(layer.downloaded);
728 layer.size = bytes(detail.and_then(|detail| detail.total)).unwrap_or(layer.size);
729 }
730 "Download complete" => {
731 if let Some(layer) = self.layers.get_mut(id) {
732 layer.downloaded = layer.size;
733 }
734 }
735 "Pull complete" | "Already exists" => {
736 let layer = self.layers.entry(id.clone()).or_default();
737 layer.downloaded = layer.size;
738 layer.done = true;
739 }
740 _ => {}
741 }
742 }
743}
744
745impl fmt::Display for PullProgress {
746 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
747 let done = self.layers.values().filter(|layer| layer.done).count();
748 let downloaded: u64 = self.layers.values().map(|layer| layer.downloaded).sum();
749 let size: u64 = self.layers.values().map(|layer| layer.size).sum();
750 write!(
751 f,
752 "{done} of {} layers done, {} of {} downloaded",
753 self.layers.len(),
754 ByteSize(downloaded),
755 ByteSize(size)
756 )
757 }
758}
759
760fn connect_to(docker_host: Option<&str>) -> Result<Docker, Error> {
761 let api = match docker_host.filter(|host| !host.is_empty()) {
762 None => bollard::Docker::connect_with_socket_defaults()?,
763 Some(host) if host.starts_with("unix://") => {
764 bollard::Docker::connect_with_socket(host, 120, bollard::API_DEFAULT_VERSION)?
765 }
766 Some(host) => {
768 return Err(Error::UnsupportedURISchemeError {
769 uri: host.to_owned(),
770 });
771 }
772 };
773 Ok(Docker { api })
774}
775
776fn jit_config_tar(jit_config: &JitConfig) -> std::io::Result<Vec<u8>> {
778 let mut tar = tar::Builder::new(Vec::new());
779 let path = JIT_CONFIG_PATH.trim_start_matches('/');
780 let mut dir = tar::Header::new_gnu();
781 dir.set_entry_type(tar::EntryType::Directory);
782 dir.set_mode(0o700);
783 dir.set_size(0);
784 tar.append_data(
785 &mut dir,
786 path.rsplit_once('/').map_or("", |(dir, _)| dir),
787 std::io::empty(),
788 )?;
789 let mut file = tar::Header::new_gnu();
790 file.set_mode(0o400);
791 file.set_size(jit_config.0.len() as u64);
792 tar.append_data(&mut file, path, jit_config.0.as_bytes())?;
793 tar.into_inner()
794}
795
796#[cfg(test)]
797mod tests;