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 { command } => {
588 let created_by = format!("RUN {}", command);
589 if try_reuse_cached_layer(
590 cache_valid,
591 cache.as_ref(),
592 &chain_key,
593 &rootfs_dir,
594 &mut state,
595 &created_by,
596 )?
597 .is_some()
598 {
599 if !config.quiet {
600 println!(
601 "Step {}/{}: {} (CACHED)",
602 step, total_instructions, created_by
603 );
604 }
605 continue;
606 }
607 cache_valid = false;
608
609 if !config.quiet {
610 println!("Step {}/{}: RUN {}", step, total_instructions, command);
611 }
612 let layer_opt = handle_run(
613 command,
614 &rootfs_dir,
615 &layers_dir,
616 &state.workdir,
617 &state.env,
618 &state.shell,
619 state.layers.len() + base_layers.len(),
620 config.quiet,
621 )?;
622 if let Some(layer_info) = layer_opt {
623 let diff_id = compute_diff_id(&layer_info.path)?;
624 if let Some(c) = &cache {
625 c.store(&chain_key, &layer_info, &diff_id);
626 }
627 state.diff_ids.push(diff_id);
628 state.layers.push(layer_info);
629 state.history.push(HistoryEntry {
630 created_by: format!("RUN {}", command),
631 empty_layer: false,
632 });
633 } else {
634 state.history.push(HistoryEntry {
635 created_by: format!("RUN {}", command),
636 empty_layer: true,
637 });
638 }
639 }
640
641 Instruction::Workdir { path } => {
642 if !config.quiet {
643 println!("Step {}/{}: WORKDIR {}", step, total_instructions, path);
644 }
645 let expanded_path = expand_args(path, &state.expansion_vars());
647 state.workdir = resolve_path(&state.workdir, &expanded_path);
648 let full = rootfs_dir.join(state.workdir.trim_start_matches('/'));
649 let _ = std::fs::create_dir_all(&full);
650 state.history.push(HistoryEntry {
651 created_by: format!("WORKDIR {}", path),
652 empty_layer: true,
653 });
654 }
655
656 Instruction::Env { vars } => {
657 let display: Vec<String> =
658 vars.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
659 let display = display.join(" ");
660 if !config.quiet {
661 println!("Step {}/{}: ENV {}", step, total_instructions, display);
662 }
663 for (key, value) in vars {
664 let expanded_value = expand_args(value, &state.expansion_vars());
667 if let Some(existing) = state.env.iter_mut().find(|(k, _)| k == key) {
668 existing.1 = expanded_value;
669 } else {
670 state.env.push((key.clone(), expanded_value));
671 }
672 }
673 state.history.push(HistoryEntry {
674 created_by: format!("ENV {}", display),
675 empty_layer: true,
676 });
677 }
678
679 Instruction::Entrypoint { exec } => {
680 if !config.quiet {
681 println!(
682 "Step {}/{}: ENTRYPOINT {:?}",
683 step, total_instructions, exec
684 );
685 }
686 state.entrypoint = Some(exec.clone());
687 state.history.push(HistoryEntry {
688 created_by: format!("ENTRYPOINT {:?}", exec),
689 empty_layer: true,
690 });
691 }
692
693 Instruction::Cmd { exec } => {
694 if !config.quiet {
695 println!("Step {}/{}: CMD {:?}", step, total_instructions, exec);
696 }
697 state.cmd = Some(exec.clone());
698 state.history.push(HistoryEntry {
699 created_by: format!("CMD {:?}", exec),
700 empty_layer: true,
701 });
702 }
703
704 Instruction::Expose { ports } => {
705 let joined = ports.join(" ");
706 if !config.quiet {
707 println!("Step {}/{}: EXPOSE {}", step, total_instructions, joined);
708 }
709 for port in ports {
710 if !state.exposed_ports.contains(port) {
711 state.exposed_ports.push(port.clone());
712 }
713 }
714 state.history.push(HistoryEntry {
715 created_by: format!("EXPOSE {}", joined),
716 empty_layer: true,
717 });
718 }
719
720 Instruction::Label { pairs } => {
721 let joined = pairs
722 .iter()
723 .map(|(k, v)| format!("{}={}", k, v))
724 .collect::<Vec<_>>()
725 .join(" ");
726 if !config.quiet {
727 println!("Step {}/{}: LABEL {}", step, total_instructions, joined);
728 }
729 for (key, value) in pairs {
730 state.labels.insert(key.clone(), value.clone());
731 }
732 state.history.push(HistoryEntry {
733 created_by: format!("LABEL {}", joined),
734 empty_layer: true,
735 });
736 }
737
738 Instruction::User { user } => {
739 if !config.quiet {
740 println!("Step {}/{}: USER {}", step, total_instructions, user);
741 }
742 state.user = Some(user.clone());
743 state.history.push(HistoryEntry {
744 created_by: format!("USER {}", user),
745 empty_layer: true,
746 });
747 }
748
749 Instruction::Arg { name, default } => {
750 if !config.quiet {
751 println!("Step {}/{}: ARG {}", step, total_instructions, name);
752 }
753 state.declared_args.insert(name.clone());
754 if !state.build_args.contains_key(name) {
755 if let Some(val) = default {
756 state.build_args.insert(name.clone(), val.clone());
757 }
758 }
759 state.history.push(HistoryEntry {
760 created_by: format!("ARG {}", name),
761 empty_layer: true,
762 });
763 }
764
765 Instruction::Shell { exec } => {
766 if !config.quiet {
767 println!("Step {}/{}: SHELL {:?}", step, total_instructions, exec);
768 }
769 state.shell = exec.clone();
770 state.history.push(HistoryEntry {
771 created_by: format!("SHELL {:?}", exec),
772 empty_layer: true,
773 });
774 }
775
776 Instruction::StopSignal { signal } => {
777 if !config.quiet {
778 println!(
779 "Step {}/{}: STOPSIGNAL {}",
780 step, total_instructions, signal
781 );
782 }
783 state.stop_signal = Some(signal.clone());
784 state.history.push(HistoryEntry {
785 created_by: format!("STOPSIGNAL {}", signal),
786 empty_layer: true,
787 });
788 }
789
790 Instruction::HealthCheck {
791 cmd,
792 interval,
793 timeout,
794 retries,
795 start_period,
796 } => {
797 if !config.quiet {
798 if cmd.is_some() {
799 println!("Step {}/{}: HEALTHCHECK CMD ...", step, total_instructions);
800 } else {
801 println!("Step {}/{}: HEALTHCHECK NONE", step, total_instructions);
802 }
803 }
804 state.health_check = cmd.as_ref().map(|c| OciHealthCheck {
805 test: c.clone(),
806 interval: *interval,
807 timeout: *timeout,
808 retries: *retries,
809 start_period: *start_period,
810 });
811 state.history.push(HistoryEntry {
812 created_by: if cmd.is_some() {
813 "HEALTHCHECK CMD ...".to_string()
814 } else {
815 "HEALTHCHECK NONE".to_string()
816 },
817 empty_layer: true,
818 });
819 }
820
821 Instruction::OnBuild { instruction } => {
822 let trigger = format!("{:?}", instruction);
823 if !config.quiet {
824 println!("Step {}/{}: ONBUILD {}", step, total_instructions, trigger);
825 }
826 state.onbuild.push(instruction_to_string(instruction));
828 state.history.push(HistoryEntry {
829 created_by: format!("ONBUILD {}", instruction_to_string(instruction)),
830 empty_layer: true,
831 });
832 }
833
834 Instruction::Volume { paths } => {
835 if !config.quiet {
836 println!(
837 "Step {}/{}: VOLUME {}",
838 step,
839 total_instructions,
840 paths.join(" ")
841 );
842 }
843 for p in paths {
844 if !state.volumes.contains(p) {
845 state.volumes.push(p.clone());
846 }
847 }
848 for p in paths {
850 let full = rootfs_dir.join(p.trim_start_matches('/'));
851 let _ = std::fs::create_dir_all(&full);
852 }
853 state.history.push(HistoryEntry {
854 created_by: format!("VOLUME {}", paths.join(" ")),
855 empty_layer: true,
856 });
857 }
858 }
859 }
860
861 completed_stages.push((stage.alias.clone(), rootfs_dir.clone()));
863
864 if is_final_stage {
865 final_state = state;
866 final_base_layers = base_layers;
867 final_base_diff_ids = base_diff_ids;
868 break;
870 }
871 }
872
873 let reference = config
875 .tag
876 .clone()
877 .unwrap_or_else(|| "a3s-build:latest".to_string());
878
879 let final_layers_dir = build_dir
880 .path()
881 .join(format!("layers_{}", output_stage_idx));
882
883 let target_platform = config
885 .platforms
886 .first()
887 .cloned()
888 .unwrap_or_else(default_target_platform);
889
890 let result = assemble_image(
891 &reference,
892 &final_state,
893 &final_base_layers,
894 &final_base_diff_ids,
895 &final_layers_dir,
896 &store,
897 &target_platform,
898 )
899 .await?;
900
901 if !config.quiet {
902 println!(
903 "Successfully built {} ({} layers, {}, {})",
904 reference,
905 result.layer_count,
906 format_size(result.size),
907 target_platform,
908 );
909 }
910
911 if let Some(ref m) = config.metrics {
912 m.image_build_total.inc();
913 }
914
915 Ok(result)
916}
917
918fn try_reuse_cached_layer(
930 cache_valid: bool,
931 cache: Option<&BuildCache>,
932 chain_key: &str,
933 rootfs_dir: &Path,
934 state: &mut BuildState,
935 created_by: &str,
936) -> Result<Option<()>> {
937 if !cache_valid {
938 return Ok(None);
939 }
940 let Some(cached) = cache.and_then(|c| c.lookup(chain_key)) else {
941 return Ok(None);
942 };
943
944 extract_layer(&cached.blob_path, rootfs_dir)?;
946
947 state.layers.push(LayerInfo {
948 path: cached.blob_path,
949 digest: cached.digest,
950 size: cached.size,
951 });
952 state.diff_ids.push(cached.diff_id);
953 state.history.push(HistoryEntry {
954 created_by: created_by.to_string(),
955 empty_layer: false,
956 });
957 Ok(Some(()))
958}
959
960async fn handle_from(
964 image: &str,
965 rootfs_dir: &Path,
966 _layers_dir: &Path,
967 store: &Arc<ImageStore>,
968 build_args: &HashMap<String, String>,
969) -> Result<(Vec<LayerInfo>, Vec<String>, OciImageConfig)> {
970 let image_ref = expand_args(image, build_args);
971 if image_ref == "scratch" {
972 return Ok((Vec::new(), Vec::new(), scratch_config()));
973 }
974
975 let puller = ImagePuller::new(store.clone(), RegistryAuth::from_env());
977 let oci_image = puller.pull(&image_ref).await?;
978
979 for layer_path in oci_image.layer_paths() {
981 extract_layer(layer_path, rootfs_dir)?;
982 }
983
984 let mut base_layers = Vec::new();
986 let mut base_diff_ids = Vec::new();
987
988 for layer_path in oci_image.layer_paths() {
989 let digest = sha256_file(layer_path)?;
990 let size = std::fs::metadata(layer_path).map(|m| m.len()).unwrap_or(0);
991
992 let diff_id = compute_diff_id(layer_path)?;
994 base_diff_ids.push(diff_id);
995
996 base_layers.push(LayerInfo {
997 path: layer_path.to_path_buf(),
998 digest,
999 size,
1000 });
1001 }
1002
1003 let config = oci_image.config().clone();
1004 Ok((base_layers, base_diff_ids, config))
1005}
1006
1007async fn resolve_external_image_rootfs(
1011 image_ref: &str,
1012 store: &Arc<ImageStore>,
1013 build_dir: &Path,
1014 cache: &mut HashMap<String, PathBuf>,
1015) -> Result<PathBuf> {
1016 if let Some(dir) = cache.get(image_ref) {
1017 return Ok(dir.clone());
1018 }
1019
1020 let dir = build_dir.join(format!("copyfrom_{}", cache.len()));
1021 std::fs::create_dir_all(&dir).map_err(|e| {
1022 BoxError::BuildError(format!(
1023 "Failed to create COPY --from image rootfs {}: {}",
1024 dir.display(),
1025 e
1026 ))
1027 })?;
1028
1029 let puller = ImagePuller::new(store.clone(), RegistryAuth::from_env());
1030 let oci_image = puller.pull(image_ref).await.map_err(|e| {
1031 BoxError::BuildError(format!(
1032 "COPY --from={}: not a build stage and could not be pulled as an image: {}",
1033 image_ref, e
1034 ))
1035 })?;
1036 for layer_path in oci_image.layer_paths() {
1037 extract_layer(layer_path, &dir)?;
1038 }
1039
1040 cache.insert(image_ref.to_string(), dir.clone());
1041 Ok(dir)
1042}
1043
1044fn validate_build_config(config: &BuildConfig) -> Result<()> {
1045 if config.platforms.len() > 1 {
1046 return Err(BoxError::BuildError(
1047 "Multi-platform builds are not implemented yet; pass a single target platform"
1048 .to_string(),
1049 ));
1050 }
1051
1052 for platform in &config.platforms {
1053 if platform.os != "linux" {
1054 return Err(BoxError::BuildError(format!(
1055 "Only linux target platforms are supported for image builds, got {}",
1056 platform
1057 )));
1058 }
1059 }
1060
1061 Ok(())
1062}
1063
1064fn default_target_platform() -> Platform {
1065 let host = Platform::host();
1066 Platform::new("linux", host.architecture)
1067}
1068
1069fn scratch_config() -> OciImageConfig {
1070 OciImageConfig {
1071 entrypoint: None,
1072 cmd: None,
1073 env: Vec::new(),
1074 working_dir: None,
1075 user: None,
1076 exposed_ports: Vec::new(),
1077 labels: HashMap::new(),
1078 volumes: Vec::new(),
1079 stop_signal: None,
1080 health_check: None,
1081 onbuild: Vec::new(),
1082 }
1083}
1084
1085async fn assemble_image(
1087 reference: &str,
1088 state: &BuildState,
1089 base_layers: &[LayerInfo],
1090 base_diff_ids: &[String],
1091 layers_dir: &Path,
1092 store: &Arc<ImageStore>,
1093 target_platform: &Platform,
1094) -> Result<BuildResult> {
1095 let output_dir = layers_dir.join("_output");
1097 let blobs_dir = output_dir.join("blobs").join("sha256");
1098 std::fs::create_dir_all(&blobs_dir)
1099 .map_err(|e| BoxError::BuildError(format!("Failed to create output blobs dir: {}", e)))?;
1100
1101 let mut all_layer_descriptors = Vec::new();
1103 let mut all_diff_ids: Vec<String> = base_diff_ids.to_vec();
1104
1105 for layer in base_layers {
1107 let blob_path = blobs_dir.join(&layer.digest);
1108 if !blob_path.exists() {
1109 std::fs::copy(&layer.path, &blob_path)
1110 .map_err(|e| BoxError::BuildError(format!("Failed to copy base layer: {}", e)))?;
1111 }
1112 all_layer_descriptors.push(serde_json::json!({
1113 "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
1114 "digest": layer.prefixed_digest(),
1115 "size": layer.size
1116 }));
1117 }
1118
1119 for (i, layer) in state.layers.iter().enumerate() {
1121 let blob_path = blobs_dir.join(&layer.digest);
1122 if !blob_path.exists() {
1123 std::fs::copy(&layer.path, &blob_path)
1124 .map_err(|e| BoxError::BuildError(format!("Failed to copy layer {}: {}", i, e)))?;
1125 }
1126 all_layer_descriptors.push(serde_json::json!({
1127 "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
1128 "digest": layer.prefixed_digest(),
1129 "size": layer.size
1130 }));
1131 }
1132
1133 all_diff_ids.extend(state.diff_ids.iter().cloned());
1135
1136 let now = chrono::Utc::now().to_rfc3339();
1138 let arch = target_platform.oci_arch();
1139
1140 let env_list: Vec<String> = state
1141 .env
1142 .iter()
1143 .map(|(k, v)| format!("{}={}", k, v))
1144 .collect();
1145
1146 let mut config_obj = serde_json::json!({
1147 "architecture": arch,
1148 "os": "linux",
1149 "created": now,
1150 "config": {},
1151 "rootfs": {
1152 "type": "layers",
1153 "diff_ids": all_diff_ids.iter()
1154 .map(|d| format!("sha256:{}", d))
1155 .collect::<Vec<_>>()
1156 },
1157 "history": state.history.iter().map(|h| {
1158 let mut entry = serde_json::json!({
1159 "created": now,
1160 "created_by": h.created_by
1161 });
1162 if h.empty_layer {
1163 entry["empty_layer"] = serde_json::json!(true);
1164 }
1165 entry
1166 }).collect::<Vec<_>>()
1167 });
1168
1169 let config_section = config_obj["config"].as_object_mut().unwrap();
1171 if !env_list.is_empty() {
1172 config_section.insert("Env".to_string(), serde_json::json!(env_list));
1173 }
1174 if let Some(ref ep) = state.entrypoint {
1175 config_section.insert("Entrypoint".to_string(), serde_json::json!(ep));
1176 }
1177 if let Some(ref cmd) = state.cmd {
1178 config_section.insert("Cmd".to_string(), serde_json::json!(cmd));
1179 }
1180 if state.workdir != "/" {
1181 config_section.insert("WorkingDir".to_string(), serde_json::json!(state.workdir));
1182 }
1183 if let Some(ref user) = state.user {
1184 config_section.insert("User".to_string(), serde_json::json!(user));
1185 }
1186 if !state.exposed_ports.is_empty() {
1187 let ports: HashMap<String, serde_json::Value> = state
1188 .exposed_ports
1189 .iter()
1190 .map(|p| (p.clone(), serde_json::json!({})))
1191 .collect();
1192 config_section.insert("ExposedPorts".to_string(), serde_json::json!(ports));
1193 }
1194 if !state.labels.is_empty() {
1195 config_section.insert("Labels".to_string(), serde_json::json!(state.labels));
1196 }
1197 if let Some(ref sig) = state.stop_signal {
1198 config_section.insert("StopSignal".to_string(), serde_json::json!(sig));
1199 }
1200 if let Some(ref hc) = state.health_check {
1201 let mut hc_obj = serde_json::json!({
1202 "Test": hc.test,
1203 });
1204 if let Some(interval) = hc.interval {
1205 hc_obj["Interval"] = serde_json::json!(interval * 1_000_000_000);
1207 }
1208 if let Some(timeout) = hc.timeout {
1209 hc_obj["Timeout"] = serde_json::json!(timeout * 1_000_000_000);
1210 }
1211 if let Some(retries) = hc.retries {
1212 hc_obj["Retries"] = serde_json::json!(retries);
1213 }
1214 if let Some(start_period) = hc.start_period {
1215 hc_obj["StartPeriod"] = serde_json::json!(start_period * 1_000_000_000);
1216 }
1217 config_section.insert("Healthcheck".to_string(), hc_obj);
1218 }
1219 if !state.onbuild.is_empty() {
1220 config_section.insert("OnBuild".to_string(), serde_json::json!(state.onbuild));
1221 }
1222 if !state.volumes.is_empty() {
1223 let vols: HashMap<String, serde_json::Value> = state
1224 .volumes
1225 .iter()
1226 .map(|v| (v.clone(), serde_json::json!({})))
1227 .collect();
1228 config_section.insert("Volumes".to_string(), serde_json::json!(vols));
1229 }
1230
1231 let config_bytes = serde_json::to_vec_pretty(&config_obj)?;
1233 let config_digest = sha256_bytes(&config_bytes);
1234 std::fs::write(blobs_dir.join(&config_digest), &config_bytes)
1235 .map_err(|e| BoxError::BuildError(format!("Failed to write config blob: {}", e)))?;
1236
1237 let manifest = serde_json::json!({
1239 "schemaVersion": 2,
1240 "mediaType": "application/vnd.oci.image.manifest.v1+json",
1241 "config": {
1242 "mediaType": "application/vnd.oci.image.config.v1+json",
1243 "digest": format!("sha256:{}", config_digest),
1244 "size": config_bytes.len()
1245 },
1246 "layers": all_layer_descriptors
1247 });
1248
1249 let manifest_bytes = serde_json::to_vec_pretty(&manifest)?;
1250 let manifest_digest = sha256_bytes(&manifest_bytes);
1251 std::fs::write(blobs_dir.join(&manifest_digest), &manifest_bytes)
1252 .map_err(|e| BoxError::BuildError(format!("Failed to write manifest blob: {}", e)))?;
1253
1254 let mut platform_obj = serde_json::json!({
1256 "os": target_platform.os,
1257 "architecture": target_platform.architecture
1258 });
1259 if let Some(ref variant) = target_platform.variant {
1260 platform_obj["variant"] = serde_json::json!(variant);
1261 }
1262
1263 let index = serde_json::json!({
1264 "schemaVersion": 2,
1265 "mediaType": "application/vnd.oci.image.index.v1+json",
1266 "manifests": [{
1267 "mediaType": "application/vnd.oci.image.manifest.v1+json",
1268 "digest": format!("sha256:{}", manifest_digest),
1269 "size": manifest_bytes.len(),
1270 "platform": platform_obj
1271 }]
1272 });
1273 std::fs::write(
1274 output_dir.join("index.json"),
1275 serde_json::to_string_pretty(&index)?,
1276 )
1277 .map_err(|e| BoxError::BuildError(format!("Failed to write index.json: {}", e)))?;
1278
1279 std::fs::write(
1281 output_dir.join("oci-layout"),
1282 r#"{"imageLayoutVersion":"1.0.0"}"#,
1283 )
1284 .map_err(|e| BoxError::BuildError(format!("Failed to write oci-layout: {}", e)))?;
1285
1286 let digest_str = format!("sha256:{}", manifest_digest);
1288 let stored = store.put(reference, &digest_str, &output_dir).await?;
1289
1290 let total_layers = base_layers.len() + state.layers.len();
1291
1292 Ok(BuildResult {
1293 reference: reference.to_string(),
1294 digest: digest_str,
1295 size: stored.size_bytes,
1296 layer_count: total_layers,
1297 })
1298}