1use std::collections::HashMap;
7use std::collections::HashSet;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use a3s_box_core::error::{BoxError, Result};
12use a3s_box_core::platform::Platform;
13
14use super::cache::{hash_context_sources, BuildCache};
15use super::dockerfile::{Dockerfile, Instruction};
16use super::dockerignore::DockerIgnore;
17use super::layer::{sha256_bytes, sha256_file, LayerInfo};
18use crate::oci::image::OciImageConfig;
19use crate::oci::layers::extract_layer;
20use crate::oci::store::ImageStore;
21use crate::oci::{ImagePuller, RegistryAuth};
22
23mod handlers;
24mod stages;
25mod utils;
26
27#[cfg(test)]
28mod tests;
29
30use handlers::{
31 apply_base_config, execute_onbuild_trigger, handle_add, handle_copy, handle_run,
32 instruction_to_string,
33};
34use stages::{global_arg_decls, resolve_stage_rootfs, split_into_stages};
35use utils::{compute_diff_id, expand_args, format_size, resolve_path};
36
37#[derive(Debug, Clone)]
39pub struct BuildConfig {
40 pub context_dir: PathBuf,
42 pub dockerfile_path: PathBuf,
44 pub tag: Option<String>,
46 pub build_args: HashMap<String, String>,
48 pub quiet: bool,
50 pub platforms: Vec<Platform>,
53 pub target: Option<String>,
56 pub no_cache: bool,
58 pub metrics: Option<crate::prom::RuntimeMetrics>,
60}
61
62#[derive(Debug)]
64pub struct BuildResult {
65 pub reference: String,
67 pub digest: String,
69 pub size: u64,
71 pub layer_count: usize,
73}
74
75pub(super) struct BuildState {
77 pub(super) workdir: String,
79 pub(super) env: Vec<(String, String)>,
81 pub(super) entrypoint: Option<Vec<String>>,
83 pub(super) cmd: Option<Vec<String>>,
85 pub(super) user: Option<String>,
87 pub(super) exposed_ports: Vec<String>,
89 pub(super) labels: HashMap<String, String>,
91 pub(super) layers: Vec<LayerInfo>,
93 pub(super) diff_ids: Vec<String>,
95 pub(super) history: Vec<HistoryEntry>,
97 pub(super) build_args: HashMap<String, String>,
100 pub(super) declared_args: HashSet<String>,
104 pub(super) shell: Vec<String>,
106 pub(super) stop_signal: Option<String>,
108 pub(super) health_check: Option<OciHealthCheck>,
110 pub(super) onbuild: Vec<String>,
112 pub(super) volumes: Vec<String>,
114}
115
116#[derive(Debug, Clone)]
118pub(super) struct HistoryEntry {
119 pub(super) created_by: String,
120 pub(super) empty_layer: bool,
121}
122
123pub use crate::oci::image::OciHealthCheck;
124
125impl BuildState {
126 fn new(build_args: HashMap<String, String>) -> Self {
127 Self {
128 workdir: "/".to_string(),
129 env: Vec::new(),
130 entrypoint: None,
131 cmd: None,
132 user: None,
133 exposed_ports: Vec::new(),
134 labels: HashMap::new(),
135 layers: Vec::new(),
136 diff_ids: Vec::new(),
137 history: Vec::new(),
138 build_args,
139 declared_args: HashSet::new(),
140 shell: vec!["/bin/sh".to_string(), "-c".to_string()],
141 stop_signal: None,
142 health_check: None,
143 onbuild: Vec::new(),
144 volumes: Vec::new(),
145 }
146 }
147
148 fn declared_build_args(&self) -> HashMap<String, String> {
151 self.build_args
152 .iter()
153 .filter(|(name, _)| self.declared_args.contains(*name))
154 .map(|(name, value)| (name.clone(), value.clone()))
155 .collect()
156 }
157
158 fn expansion_vars(&self) -> HashMap<String, String> {
162 let mut vars = self.declared_build_args();
163 for (key, value) in &self.env {
164 vars.insert(key.clone(), value.clone());
165 }
166 vars
167 }
168
169 fn seed_global_arg(&mut self, name: &str, default: Option<&str>) {
172 self.declared_args.insert(name.to_string());
173 if !self.build_args.contains_key(name) {
174 if let Some(val) = default {
175 self.build_args.insert(name.to_string(), val.to_string());
176 }
177 }
178 }
179}
180
181pub async fn build(config: BuildConfig, store: Arc<ImageStore>) -> Result<BuildResult> {
196 validate_build_config(&config)?;
197
198 let dockerfile = Dockerfile::from_file(&config.dockerfile_path)?;
200
201 let dockerignore = DockerIgnore::load(&config.context_dir);
203
204 if !config.quiet {
205 println!("Building from {}", config.dockerfile_path.display());
206 if !dockerignore.is_empty() {
207 println!("Using .dockerignore");
208 }
209 }
210
211 let stages = split_into_stages(&dockerfile.instructions);
213 let global_args = global_arg_decls(&dockerfile.instructions);
217 let total_stages = stages.len();
218
219 let output_stage_idx = match config.target.as_deref() {
223 Some(target) => stages
224 .iter()
225 .position(|s| s.alias.as_deref() == Some(target))
226 .or_else(|| target.parse::<usize>().ok().filter(|i| *i < total_stages))
227 .ok_or_else(|| {
228 BoxError::BuildError(format!("target build stage '{}' not found", target))
229 })?,
230 None => total_stages - 1,
231 };
232
233 let mut completed_stages: Vec<(Option<String>, PathBuf)> = Vec::new();
235 let mut external_from_rootfs: HashMap<String, PathBuf> = HashMap::new();
238
239 let build_dir = tempfile::TempDir::new()
241 .map_err(|e| BoxError::BuildError(format!("Failed to create build directory: {}", e)))?;
242
243 let mut final_state = BuildState::new(config.build_args.clone());
244 let mut final_base_layers: Vec<LayerInfo> = Vec::new();
245 let mut final_base_diff_ids: Vec<String> = Vec::new();
246
247 let total_instructions = dockerfile.instructions.len();
248 let mut global_step = 0;
249
250 for (stage_idx, stage) in stages.iter().enumerate() {
251 let is_final_stage = stage_idx == output_stage_idx;
252
253 let rootfs_dir = build_dir.path().join(format!("rootfs_{}", stage_idx));
254 let layers_dir = build_dir.path().join(format!("layers_{}", stage_idx));
255 std::fs::create_dir_all(&rootfs_dir).map_err(|e| {
256 BoxError::BuildError(format!("Failed to create rootfs directory: {}", e))
257 })?;
258 std::fs::create_dir_all(&layers_dir).map_err(|e| {
259 BoxError::BuildError(format!("Failed to create layers directory: {}", e))
260 })?;
261
262 let mut state = BuildState::new(config.build_args.clone());
263 if stage_idx > 0 {
267 for (name, default) in &global_args {
268 state.seed_global_arg(name, default.as_deref());
269 }
270 }
271 let mut base_layers: Vec<LayerInfo> = Vec::new();
272 let mut base_diff_ids: Vec<String> = Vec::new();
273
274 let cache = if config.no_cache {
276 None
277 } else {
278 BuildCache::open()
279 };
280 let mut chain_key = String::new();
282 let mut cache_valid = true;
284
285 for instruction in &stage.instructions {
286 global_step += 1;
287 let step = global_step;
288
289 if !matches!(instruction, Instruction::From { .. }) {
294 let repr = match instruction {
300 Instruction::Env { vars } => {
301 let pairs: Vec<String> = vars
302 .iter()
303 .map(|(k, v)| {
304 format!("{}={}", k, expand_args(v, &state.expansion_vars()))
305 })
306 .collect();
307 format!("ENV {}", pairs.join(" "))
308 }
309 Instruction::Arg { name, default } => {
310 let effective = state
311 .build_args
312 .get(name)
313 .cloned()
314 .or_else(|| default.clone())
315 .unwrap_or_default();
316 format!("ARG {}={}", name, effective)
317 }
318 other => instruction_to_string(other),
319 };
320 let input_hash = match instruction {
321 Instruction::Copy {
322 src, from: None, ..
323 } => hash_context_sources(&config.context_dir, src),
324 Instruction::Copy {
325 src,
326 from: Some(from_ref),
327 ..
328 } => {
329 resolve_stage_rootfs(from_ref, &completed_stages)
337 .ok()
338 .and_then(|rootfs| hash_context_sources(rootfs, src))
339 }
340 Instruction::Add { src, .. } => hash_context_sources(&config.context_dir, src),
341 _ => None,
342 };
343 chain_key = BuildCache::chain(&chain_key, &repr, input_hash.as_deref());
344 }
345
346 match instruction {
347 Instruction::From { image, alias } => {
348 if !config.quiet {
349 if total_stages > 1 {
350 println!(
351 "Step {}/{}: FROM {} (stage {}/{}{})",
352 step,
353 total_instructions,
354 image,
355 stage_idx + 1,
356 total_stages,
357 alias
358 .as_ref()
359 .map(|a| format!(" as {}", a))
360 .unwrap_or_default()
361 );
362 } else {
363 println!("Step {}/{}: FROM {}", step, total_instructions, image);
364 }
365 }
366 let (layers, diff_ids, base_config) = handle_from(
367 image,
368 &rootfs_dir,
369 &layers_dir,
370 &store,
371 &state.declared_build_args(),
372 )
373 .await?;
374 base_layers = layers;
375 base_diff_ids = diff_ids;
376
377 chain_key = sha256_bytes(base_diff_ids.join(",").as_bytes());
381 cache_valid = true;
382
383 apply_base_config(&mut state, &base_config);
385
386 if !base_config.onbuild.is_empty() && !config.quiet {
388 println!(
389 " Executing {} ONBUILD trigger(s) from base image",
390 base_config.onbuild.len()
391 );
392 }
393 for trigger in &base_config.onbuild {
394 execute_onbuild_trigger(
395 trigger,
396 &mut state,
397 &config,
398 &rootfs_dir,
399 &layers_dir,
400 &base_layers,
401 &completed_stages,
402 )?;
403 }
404
405 state.history.push(HistoryEntry {
406 created_by: format!("FROM {}", image),
407 empty_layer: true,
408 });
409 }
410
411 Instruction::Copy {
412 src,
413 dst,
414 from,
415 chown,
416 } => {
417 let created_by = if let Some(from_ref) = from {
418 format!("COPY --from={} {} {}", from_ref, src.join(" "), dst)
419 } else if let Some(owner) = chown {
420 format!("COPY --chown={} {} {}", owner, src.join(" "), dst)
421 } else {
422 format!("COPY {} {}", src.join(" "), dst)
423 };
424 if try_reuse_cached_layer(
425 cache_valid,
426 cache.as_ref(),
427 &chain_key,
428 &rootfs_dir,
429 &mut state,
430 &created_by,
431 )?
432 .is_some()
433 {
434 if !config.quiet {
435 println!(
436 "Step {}/{}: {} (CACHED)",
437 step, total_instructions, created_by
438 );
439 }
440 continue;
441 }
442 cache_valid = false;
443
444 if let Some(from_ref) = from {
445 if !config.quiet {
446 println!(
447 "Step {}/{}: COPY --from={} {} {}",
448 step,
449 total_instructions,
450 from_ref,
451 src.join(" "),
452 dst
453 );
454 }
455 let from_rootfs: PathBuf =
459 match resolve_stage_rootfs(from_ref, &completed_stages) {
460 Ok(stage_rootfs) => stage_rootfs.to_path_buf(),
461 Err(_) => {
462 resolve_external_image_rootfs(
463 from_ref,
464 &store,
465 build_dir.path(),
466 &mut external_from_rootfs,
467 )
468 .await?
469 }
470 };
471 let layer_info = handle_copy(
474 src,
475 dst,
476 chown.as_deref(),
477 &from_rootfs,
478 &rootfs_dir,
479 &layers_dir,
480 &state.workdir,
481 state.layers.len() + base_layers.len(),
482 None,
483 )?;
484 let diff_id = compute_diff_id(&layer_info.path)?;
485 if let Some(c) = &cache {
486 c.store(&chain_key, &layer_info, &diff_id);
487 }
488 state.diff_ids.push(diff_id);
489 state.layers.push(layer_info);
490 state.history.push(HistoryEntry {
491 created_by: format!(
492 "COPY --from={} {} {}",
493 from_ref,
494 src.join(" "),
495 dst
496 ),
497 empty_layer: false,
498 });
499 } else {
500 if !config.quiet {
501 println!(
502 "Step {}/{}: COPY {} {}",
503 step,
504 total_instructions,
505 src.join(" "),
506 dst
507 );
508 }
509 let layer_info = handle_copy(
510 src,
511 dst,
512 chown.as_deref(),
513 &config.context_dir,
514 &rootfs_dir,
515 &layers_dir,
516 &state.workdir,
517 state.layers.len() + base_layers.len(),
518 Some(&dockerignore),
519 )?;
520 let diff_id = compute_diff_id(&layer_info.path)?;
521 if let Some(c) = &cache {
522 c.store(&chain_key, &layer_info, &diff_id);
523 }
524 state.diff_ids.push(diff_id);
525 state.layers.push(layer_info);
526 state.history.push(HistoryEntry {
527 created_by: format!("COPY {} {}", src.join(" "), dst),
528 empty_layer: false,
529 });
530 }
531 }
532
533 Instruction::Add { src, dst, chown } => {
534 let created_by = format!("ADD {} {}", src.join(" "), dst);
535 if try_reuse_cached_layer(
536 cache_valid,
537 cache.as_ref(),
538 &chain_key,
539 &rootfs_dir,
540 &mut state,
541 &created_by,
542 )?
543 .is_some()
544 {
545 if !config.quiet {
546 println!(
547 "Step {}/{}: {} (CACHED)",
548 step, total_instructions, created_by
549 );
550 }
551 continue;
552 }
553 cache_valid = false;
554
555 if !config.quiet {
556 println!(
557 "Step {}/{}: ADD {} {}",
558 step,
559 total_instructions,
560 src.join(" "),
561 dst
562 );
563 }
564 let layer_info = handle_add(
565 src,
566 dst,
567 chown.as_deref(),
568 &config.context_dir,
569 &rootfs_dir,
570 &layers_dir,
571 &state.workdir,
572 state.layers.len() + base_layers.len(),
573 Some(&dockerignore),
574 )?;
575 let diff_id = compute_diff_id(&layer_info.path)?;
576 if let Some(c) = &cache {
577 c.store(&chain_key, &layer_info, &diff_id);
578 }
579 state.diff_ids.push(diff_id);
580 state.layers.push(layer_info);
581 state.history.push(HistoryEntry {
582 created_by: format!("ADD {} {}", src.join(" "), dst),
583 empty_layer: false,
584 });
585 }
586
587 Instruction::Run {
588 command,
589 cache_mounts,
590 } => {
591 let created_by = instruction_to_string(instruction);
592 if try_reuse_cached_layer(
593 cache_valid,
594 cache.as_ref(),
595 &chain_key,
596 &rootfs_dir,
597 &mut state,
598 &created_by,
599 )?
600 .is_some()
601 {
602 if !config.quiet {
603 println!(
604 "Step {}/{}: {} (CACHED)",
605 step, total_instructions, created_by
606 );
607 }
608 continue;
609 }
610 cache_valid = false;
611
612 if !config.quiet {
613 println!("Step {}/{}: {}", step, total_instructions, created_by);
614 }
615 let layer_opt = handle_run(
616 command,
617 cache_mounts,
618 &rootfs_dir,
619 &layers_dir,
620 &state.workdir,
621 &state.env,
622 &state.shell,
623 state.layers.len() + base_layers.len(),
624 config.quiet,
625 )?;
626 if let Some(layer_info) = layer_opt {
627 let diff_id = compute_diff_id(&layer_info.path)?;
628 if let Some(c) = &cache {
629 c.store(&chain_key, &layer_info, &diff_id);
630 }
631 state.diff_ids.push(diff_id);
632 state.layers.push(layer_info);
633 state.history.push(HistoryEntry {
634 created_by: created_by.clone(),
635 empty_layer: false,
636 });
637 } else {
638 state.history.push(HistoryEntry {
639 created_by: created_by.clone(),
640 empty_layer: true,
641 });
642 }
643 }
644
645 Instruction::Workdir { path } => {
646 if !config.quiet {
647 println!("Step {}/{}: WORKDIR {}", step, total_instructions, path);
648 }
649 let expanded_path = expand_args(path, &state.expansion_vars());
651 state.workdir = resolve_path(&state.workdir, &expanded_path);
652 let full = rootfs_dir.join(state.workdir.trim_start_matches('/'));
653 let _ = std::fs::create_dir_all(&full);
654 state.history.push(HistoryEntry {
655 created_by: format!("WORKDIR {}", path),
656 empty_layer: true,
657 });
658 }
659
660 Instruction::Env { vars } => {
661 let display: Vec<String> =
662 vars.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
663 let display = display.join(" ");
664 if !config.quiet {
665 println!("Step {}/{}: ENV {}", step, total_instructions, display);
666 }
667 for (key, value) in vars {
668 let expanded_value = expand_args(value, &state.expansion_vars());
671 if let Some(existing) = state.env.iter_mut().find(|(k, _)| k == key) {
672 existing.1 = expanded_value;
673 } else {
674 state.env.push((key.clone(), expanded_value));
675 }
676 }
677 state.history.push(HistoryEntry {
678 created_by: format!("ENV {}", display),
679 empty_layer: true,
680 });
681 }
682
683 Instruction::Entrypoint { exec } => {
684 if !config.quiet {
685 println!(
686 "Step {}/{}: ENTRYPOINT {:?}",
687 step, total_instructions, exec
688 );
689 }
690 state.entrypoint = Some(exec.clone());
691 state.history.push(HistoryEntry {
692 created_by: format!("ENTRYPOINT {:?}", exec),
693 empty_layer: true,
694 });
695 }
696
697 Instruction::Cmd { exec } => {
698 if !config.quiet {
699 println!("Step {}/{}: CMD {:?}", step, total_instructions, exec);
700 }
701 state.cmd = Some(exec.clone());
702 state.history.push(HistoryEntry {
703 created_by: format!("CMD {:?}", exec),
704 empty_layer: true,
705 });
706 }
707
708 Instruction::Expose { ports } => {
709 let joined = ports.join(" ");
710 if !config.quiet {
711 println!("Step {}/{}: EXPOSE {}", step, total_instructions, joined);
712 }
713 for port in ports {
714 if !state.exposed_ports.contains(port) {
715 state.exposed_ports.push(port.clone());
716 }
717 }
718 state.history.push(HistoryEntry {
719 created_by: format!("EXPOSE {}", joined),
720 empty_layer: true,
721 });
722 }
723
724 Instruction::Label { pairs } => {
725 let joined = pairs
726 .iter()
727 .map(|(k, v)| format!("{}={}", k, v))
728 .collect::<Vec<_>>()
729 .join(" ");
730 if !config.quiet {
731 println!("Step {}/{}: LABEL {}", step, total_instructions, joined);
732 }
733 for (key, value) in pairs {
734 state.labels.insert(key.clone(), value.clone());
735 }
736 state.history.push(HistoryEntry {
737 created_by: format!("LABEL {}", joined),
738 empty_layer: true,
739 });
740 }
741
742 Instruction::User { user } => {
743 if !config.quiet {
744 println!("Step {}/{}: USER {}", step, total_instructions, user);
745 }
746 state.user = Some(user.clone());
747 state.history.push(HistoryEntry {
748 created_by: format!("USER {}", user),
749 empty_layer: true,
750 });
751 }
752
753 Instruction::Arg { name, default } => {
754 if !config.quiet {
755 println!("Step {}/{}: ARG {}", step, total_instructions, name);
756 }
757 state.declared_args.insert(name.clone());
758 if !state.build_args.contains_key(name) {
759 if let Some(val) = default {
760 state.build_args.insert(name.clone(), val.clone());
761 }
762 }
763 state.history.push(HistoryEntry {
764 created_by: format!("ARG {}", name),
765 empty_layer: true,
766 });
767 }
768
769 Instruction::Shell { exec } => {
770 if !config.quiet {
771 println!("Step {}/{}: SHELL {:?}", step, total_instructions, exec);
772 }
773 state.shell = exec.clone();
774 state.history.push(HistoryEntry {
775 created_by: format!("SHELL {:?}", exec),
776 empty_layer: true,
777 });
778 }
779
780 Instruction::StopSignal { signal } => {
781 if !config.quiet {
782 println!(
783 "Step {}/{}: STOPSIGNAL {}",
784 step, total_instructions, signal
785 );
786 }
787 state.stop_signal = Some(signal.clone());
788 state.history.push(HistoryEntry {
789 created_by: format!("STOPSIGNAL {}", signal),
790 empty_layer: true,
791 });
792 }
793
794 Instruction::HealthCheck {
795 cmd,
796 interval,
797 timeout,
798 retries,
799 start_period,
800 } => {
801 if !config.quiet {
802 if cmd.is_some() {
803 println!("Step {}/{}: HEALTHCHECK CMD ...", step, total_instructions);
804 } else {
805 println!("Step {}/{}: HEALTHCHECK NONE", step, total_instructions);
806 }
807 }
808 state.health_check = cmd.as_ref().map(|c| OciHealthCheck {
809 test: c.clone(),
810 interval: *interval,
811 timeout: *timeout,
812 retries: *retries,
813 start_period: *start_period,
814 });
815 state.history.push(HistoryEntry {
816 created_by: if cmd.is_some() {
817 "HEALTHCHECK CMD ...".to_string()
818 } else {
819 "HEALTHCHECK NONE".to_string()
820 },
821 empty_layer: true,
822 });
823 }
824
825 Instruction::OnBuild { instruction } => {
826 let trigger = format!("{:?}", instruction);
827 if !config.quiet {
828 println!("Step {}/{}: ONBUILD {}", step, total_instructions, trigger);
829 }
830 state.onbuild.push(instruction_to_string(instruction));
832 state.history.push(HistoryEntry {
833 created_by: format!("ONBUILD {}", instruction_to_string(instruction)),
834 empty_layer: true,
835 });
836 }
837
838 Instruction::Volume { paths } => {
839 if !config.quiet {
840 println!(
841 "Step {}/{}: VOLUME {}",
842 step,
843 total_instructions,
844 paths.join(" ")
845 );
846 }
847 for p in paths {
848 if !state.volumes.contains(p) {
849 state.volumes.push(p.clone());
850 }
851 }
852 for p in paths {
854 let full = rootfs_dir.join(p.trim_start_matches('/'));
855 let _ = std::fs::create_dir_all(&full);
856 }
857 state.history.push(HistoryEntry {
858 created_by: format!("VOLUME {}", paths.join(" ")),
859 empty_layer: true,
860 });
861 }
862 }
863 }
864
865 completed_stages.push((stage.alias.clone(), rootfs_dir.clone()));
867
868 if is_final_stage {
869 final_state = state;
870 final_base_layers = base_layers;
871 final_base_diff_ids = base_diff_ids;
872 break;
874 }
875 }
876
877 let reference = config
879 .tag
880 .clone()
881 .unwrap_or_else(|| "a3s-build:latest".to_string());
882
883 let final_layers_dir = build_dir
884 .path()
885 .join(format!("layers_{}", output_stage_idx));
886
887 let target_platform = config
889 .platforms
890 .first()
891 .cloned()
892 .unwrap_or_else(default_target_platform);
893
894 let result = assemble_image(
895 &reference,
896 &final_state,
897 &final_base_layers,
898 &final_base_diff_ids,
899 &final_layers_dir,
900 &store,
901 &target_platform,
902 )
903 .await?;
904
905 if !config.quiet {
906 println!(
907 "Successfully built {} ({} layers, {}, {})",
908 reference,
909 result.layer_count,
910 format_size(result.size),
911 target_platform,
912 );
913 }
914
915 if let Some(ref m) = config.metrics {
916 m.image_build_total.inc();
917 }
918
919 Ok(result)
920}
921
922fn try_reuse_cached_layer(
934 cache_valid: bool,
935 cache: Option<&BuildCache>,
936 chain_key: &str,
937 rootfs_dir: &Path,
938 state: &mut BuildState,
939 created_by: &str,
940) -> Result<Option<()>> {
941 if !cache_valid {
942 return Ok(None);
943 }
944 let Some(cached) = cache.and_then(|c| c.lookup(chain_key)) else {
945 return Ok(None);
946 };
947
948 extract_layer(&cached.blob_path, rootfs_dir)?;
950
951 state.layers.push(LayerInfo {
952 path: cached.blob_path,
953 digest: cached.digest,
954 size: cached.size,
955 });
956 state.diff_ids.push(cached.diff_id);
957 state.history.push(HistoryEntry {
958 created_by: created_by.to_string(),
959 empty_layer: false,
960 });
961 Ok(Some(()))
962}
963
964async fn handle_from(
968 image: &str,
969 rootfs_dir: &Path,
970 _layers_dir: &Path,
971 store: &Arc<ImageStore>,
972 build_args: &HashMap<String, String>,
973) -> Result<(Vec<LayerInfo>, Vec<String>, OciImageConfig)> {
974 let image_ref = expand_args(image, build_args);
975 if image_ref == "scratch" {
976 return Ok((Vec::new(), Vec::new(), scratch_config()));
977 }
978
979 let puller = ImagePuller::new(store.clone(), RegistryAuth::from_env());
981 let oci_image = puller.pull(&image_ref).await?;
982
983 for layer_path in oci_image.layer_paths() {
985 extract_layer(layer_path, rootfs_dir)?;
986 }
987
988 let mut base_layers = Vec::new();
990 let mut base_diff_ids = Vec::new();
991
992 for layer_path in oci_image.layer_paths() {
993 let digest = sha256_file(layer_path)?;
994 let size = std::fs::metadata(layer_path).map(|m| m.len()).unwrap_or(0);
995
996 let diff_id = compute_diff_id(layer_path)?;
998 base_diff_ids.push(diff_id);
999
1000 base_layers.push(LayerInfo {
1001 path: layer_path.to_path_buf(),
1002 digest,
1003 size,
1004 });
1005 }
1006
1007 let config = oci_image.config().clone();
1008 Ok((base_layers, base_diff_ids, config))
1009}
1010
1011async fn resolve_external_image_rootfs(
1015 image_ref: &str,
1016 store: &Arc<ImageStore>,
1017 build_dir: &Path,
1018 cache: &mut HashMap<String, PathBuf>,
1019) -> Result<PathBuf> {
1020 if let Some(dir) = cache.get(image_ref) {
1021 return Ok(dir.clone());
1022 }
1023
1024 let dir = build_dir.join(format!("copyfrom_{}", cache.len()));
1025 std::fs::create_dir_all(&dir).map_err(|e| {
1026 BoxError::BuildError(format!(
1027 "Failed to create COPY --from image rootfs {}: {}",
1028 dir.display(),
1029 e
1030 ))
1031 })?;
1032
1033 let puller = ImagePuller::new(store.clone(), RegistryAuth::from_env());
1034 let oci_image = puller.pull(image_ref).await.map_err(|e| {
1035 BoxError::BuildError(format!(
1036 "COPY --from={}: not a build stage and could not be pulled as an image: {}",
1037 image_ref, e
1038 ))
1039 })?;
1040 for layer_path in oci_image.layer_paths() {
1041 extract_layer(layer_path, &dir)?;
1042 }
1043
1044 cache.insert(image_ref.to_string(), dir.clone());
1045 Ok(dir)
1046}
1047
1048fn validate_build_config(config: &BuildConfig) -> Result<()> {
1049 if config.platforms.len() > 1 {
1050 return Err(BoxError::BuildError(
1051 "Multi-platform builds are not implemented yet; pass a single target platform"
1052 .to_string(),
1053 ));
1054 }
1055
1056 for platform in &config.platforms {
1057 if platform.os != "linux" {
1058 return Err(BoxError::BuildError(format!(
1059 "Only linux target platforms are supported for image builds, got {}",
1060 platform
1061 )));
1062 }
1063 }
1064
1065 Ok(())
1066}
1067
1068fn default_target_platform() -> Platform {
1069 let host = Platform::host();
1070 Platform::new("linux", host.architecture)
1071}
1072
1073fn scratch_config() -> OciImageConfig {
1074 OciImageConfig {
1075 entrypoint: None,
1076 cmd: None,
1077 env: Vec::new(),
1078 working_dir: None,
1079 user: None,
1080 exposed_ports: Vec::new(),
1081 labels: HashMap::new(),
1082 volumes: Vec::new(),
1083 stop_signal: None,
1084 health_check: None,
1085 onbuild: Vec::new(),
1086 }
1087}
1088
1089async fn assemble_image(
1091 reference: &str,
1092 state: &BuildState,
1093 base_layers: &[LayerInfo],
1094 base_diff_ids: &[String],
1095 layers_dir: &Path,
1096 store: &Arc<ImageStore>,
1097 target_platform: &Platform,
1098) -> Result<BuildResult> {
1099 let output_dir = layers_dir.join("_output");
1101 let blobs_dir = output_dir.join("blobs").join("sha256");
1102 std::fs::create_dir_all(&blobs_dir)
1103 .map_err(|e| BoxError::BuildError(format!("Failed to create output blobs dir: {}", e)))?;
1104
1105 let mut all_layer_descriptors = Vec::new();
1107 let mut all_diff_ids: Vec<String> = base_diff_ids.to_vec();
1108
1109 for layer in base_layers {
1111 let blob_path = blobs_dir.join(&layer.digest);
1112 if !blob_path.exists() {
1113 std::fs::copy(&layer.path, &blob_path)
1114 .map_err(|e| BoxError::BuildError(format!("Failed to copy base layer: {}", e)))?;
1115 }
1116 all_layer_descriptors.push(serde_json::json!({
1117 "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
1118 "digest": layer.prefixed_digest(),
1119 "size": layer.size
1120 }));
1121 }
1122
1123 for (i, layer) in state.layers.iter().enumerate() {
1125 let blob_path = blobs_dir.join(&layer.digest);
1126 if !blob_path.exists() {
1127 std::fs::copy(&layer.path, &blob_path)
1128 .map_err(|e| BoxError::BuildError(format!("Failed to copy layer {}: {}", i, e)))?;
1129 }
1130 all_layer_descriptors.push(serde_json::json!({
1131 "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
1132 "digest": layer.prefixed_digest(),
1133 "size": layer.size
1134 }));
1135 }
1136
1137 all_diff_ids.extend(state.diff_ids.iter().cloned());
1139
1140 let now = chrono::Utc::now().to_rfc3339();
1142 let arch = target_platform.oci_arch();
1143
1144 let env_list: Vec<String> = state
1145 .env
1146 .iter()
1147 .map(|(k, v)| format!("{}={}", k, v))
1148 .collect();
1149
1150 let mut config_obj = serde_json::json!({
1151 "architecture": arch,
1152 "os": "linux",
1153 "created": now,
1154 "config": {},
1155 "rootfs": {
1156 "type": "layers",
1157 "diff_ids": all_diff_ids.iter()
1158 .map(|d| format!("sha256:{}", d))
1159 .collect::<Vec<_>>()
1160 },
1161 "history": state.history.iter().map(|h| {
1162 let mut entry = serde_json::json!({
1163 "created": now,
1164 "created_by": h.created_by
1165 });
1166 if h.empty_layer {
1167 entry["empty_layer"] = serde_json::json!(true);
1168 }
1169 entry
1170 }).collect::<Vec<_>>()
1171 });
1172
1173 let config_section = config_obj["config"].as_object_mut().unwrap();
1175 if !env_list.is_empty() {
1176 config_section.insert("Env".to_string(), serde_json::json!(env_list));
1177 }
1178 if let Some(ref ep) = state.entrypoint {
1179 config_section.insert("Entrypoint".to_string(), serde_json::json!(ep));
1180 }
1181 if let Some(ref cmd) = state.cmd {
1182 config_section.insert("Cmd".to_string(), serde_json::json!(cmd));
1183 }
1184 if state.workdir != "/" {
1185 config_section.insert("WorkingDir".to_string(), serde_json::json!(state.workdir));
1186 }
1187 if let Some(ref user) = state.user {
1188 config_section.insert("User".to_string(), serde_json::json!(user));
1189 }
1190 if !state.exposed_ports.is_empty() {
1191 let ports: HashMap<String, serde_json::Value> = state
1192 .exposed_ports
1193 .iter()
1194 .map(|p| (p.clone(), serde_json::json!({})))
1195 .collect();
1196 config_section.insert("ExposedPorts".to_string(), serde_json::json!(ports));
1197 }
1198 if !state.labels.is_empty() {
1199 config_section.insert("Labels".to_string(), serde_json::json!(state.labels));
1200 }
1201 if let Some(ref sig) = state.stop_signal {
1202 config_section.insert("StopSignal".to_string(), serde_json::json!(sig));
1203 }
1204 if let Some(ref hc) = state.health_check {
1205 let mut hc_obj = serde_json::json!({
1206 "Test": hc.test,
1207 });
1208 if let Some(interval) = hc.interval {
1209 hc_obj["Interval"] = serde_json::json!(interval * 1_000_000_000);
1211 }
1212 if let Some(timeout) = hc.timeout {
1213 hc_obj["Timeout"] = serde_json::json!(timeout * 1_000_000_000);
1214 }
1215 if let Some(retries) = hc.retries {
1216 hc_obj["Retries"] = serde_json::json!(retries);
1217 }
1218 if let Some(start_period) = hc.start_period {
1219 hc_obj["StartPeriod"] = serde_json::json!(start_period * 1_000_000_000);
1220 }
1221 config_section.insert("Healthcheck".to_string(), hc_obj);
1222 }
1223 if !state.onbuild.is_empty() {
1224 config_section.insert("OnBuild".to_string(), serde_json::json!(state.onbuild));
1225 }
1226 if !state.volumes.is_empty() {
1227 let vols: HashMap<String, serde_json::Value> = state
1228 .volumes
1229 .iter()
1230 .map(|v| (v.clone(), serde_json::json!({})))
1231 .collect();
1232 config_section.insert("Volumes".to_string(), serde_json::json!(vols));
1233 }
1234
1235 let config_bytes = serde_json::to_vec_pretty(&config_obj)?;
1237 let config_digest = sha256_bytes(&config_bytes);
1238 std::fs::write(blobs_dir.join(&config_digest), &config_bytes)
1239 .map_err(|e| BoxError::BuildError(format!("Failed to write config blob: {}", e)))?;
1240
1241 let manifest = serde_json::json!({
1243 "schemaVersion": 2,
1244 "mediaType": "application/vnd.oci.image.manifest.v1+json",
1245 "config": {
1246 "mediaType": "application/vnd.oci.image.config.v1+json",
1247 "digest": format!("sha256:{}", config_digest),
1248 "size": config_bytes.len()
1249 },
1250 "layers": all_layer_descriptors
1251 });
1252
1253 let manifest_bytes = serde_json::to_vec_pretty(&manifest)?;
1254 let manifest_digest = sha256_bytes(&manifest_bytes);
1255 std::fs::write(blobs_dir.join(&manifest_digest), &manifest_bytes)
1256 .map_err(|e| BoxError::BuildError(format!("Failed to write manifest blob: {}", e)))?;
1257
1258 let mut platform_obj = serde_json::json!({
1260 "os": target_platform.os,
1261 "architecture": target_platform.architecture
1262 });
1263 if let Some(ref variant) = target_platform.variant {
1264 platform_obj["variant"] = serde_json::json!(variant);
1265 }
1266
1267 let index = serde_json::json!({
1268 "schemaVersion": 2,
1269 "mediaType": "application/vnd.oci.image.index.v1+json",
1270 "manifests": [{
1271 "mediaType": "application/vnd.oci.image.manifest.v1+json",
1272 "digest": format!("sha256:{}", manifest_digest),
1273 "size": manifest_bytes.len(),
1274 "platform": platform_obj
1275 }]
1276 });
1277 std::fs::write(
1278 output_dir.join("index.json"),
1279 serde_json::to_string_pretty(&index)?,
1280 )
1281 .map_err(|e| BoxError::BuildError(format!("Failed to write index.json: {}", e)))?;
1282
1283 std::fs::write(
1285 output_dir.join("oci-layout"),
1286 r#"{"imageLayoutVersion":"1.0.0"}"#,
1287 )
1288 .map_err(|e| BoxError::BuildError(format!("Failed to write oci-layout: {}", e)))?;
1289
1290 let digest_str = format!("sha256:{}", manifest_digest);
1292 let stored = store.put(reference, &digest_str, &output_dir).await?;
1293
1294 let total_layers = base_layers.len() + state.layers.len();
1295
1296 Ok(BuildResult {
1297 reference: reference.to_string(),
1298 digest: digest_str,
1299 size: stored.size_bytes,
1300 layer_count: total_layers,
1301 })
1302}