omni_forge/image_builder/
mod.rs

1// main.rs
2mod ensure;
3mod image_gen;
4
5use anyhow::Context;
6use anyhow::Result;
7use ensure::common;
8use ensure::ensure_devcontainers_cli;
9use ensure::ensure_docker;
10use ensure::ensure_npm;
11use serde::{ Deserialize, Serialize };
12use serde_json::Value;
13use std::collections::HashMap;
14use std::fs;
15use std::io;
16use std::path::Path;
17use std::process::Command;
18use anyhow::anyhow;
19#[derive(Debug, Serialize, Deserialize)]
20pub struct DevContainer {
21    pub name: String,
22    pub image: String,
23    pub features: HashMap<String, Option<FeatureData>>,
24}
25
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct FeatureData {
28    pub version: Option<String>,
29}
30
31const DOCKER_REGISTRY: &str = "localhost:5000";
32
33pub fn build_devcontainer(devcontainer_path: &Path) -> Result<String> {
34    println!("Final path: {}", devcontainer_path.display());
35
36    // Read and verify the devcontainer.json content
37    let content = fs
38        ::read_to_string(devcontainer_path)
39        .context("failed to read the path to the dev container")?;
40    let config: Value = serde_json5
41        ::from_str(&content)
42        .context("Failed to serialize 'App/.devcontainer/devcontainer.json'")?;
43
44    // Generate image name from devcontainer.json configuration
45    let image_name = generate_image_name(&config, devcontainer_path).context(
46        "Failed to get image name"
47    )?;
48
49    // Get the workspace folder (two levels up from devcontainer.json)
50    let workspace_folder = devcontainer_path
51        .parent() // Gets .devcontainer folder
52        .and_then(|p| p.parent()) // Gets the workspace folder (App)
53        .ok_or_else(||
54            io::Error::new(io::ErrorKind::Other, "Failed to determine workspace folder")
55        )?;
56
57    println!("Path {}", workspace_folder.display());
58
59    // Use workspace folder path for the CLI command
60    let output = Command::new("devcontainer")
61        .args([
62            "build",
63            "--workspace-folder",
64            workspace_folder.to_str().unwrap(), // Pass the workspace folder, not the devcontainer.json path
65            "--image-name",
66            &image_name,
67        ])
68        .output()?;
69
70    if !output.status.success() {
71        return Err(
72            io::Error::new(io::ErrorKind::Other, String::from_utf8_lossy(&output.stderr)).into()
73        );
74    }
75
76    // Tag the image for the local Docker registry
77    let tagged_image = format!("{}/{}", DOCKER_REGISTRY, image_name);
78    let tag_output = Command::new("docker").args(["tag", &image_name, &tagged_image]).output()?;
79
80    if !tag_output.status.success() {
81        return Err(
82            io::Error::new(io::ErrorKind::Other, String::from_utf8_lossy(&tag_output.stderr)).into()
83        );
84    }
85
86    // Push the image to the local Docker registry
87    let push_output = Command::new("docker").args(["push", &tagged_image]).output()?;
88
89    if !push_output.status.success() {
90        return Err(
91            io::Error
92                ::new(io::ErrorKind::Other, String::from_utf8_lossy(&push_output.stderr))
93                .into()
94        );
95    }
96
97    Ok(tagged_image)
98}
99
100fn generate_image_name(config: &Value, devcontainer_path: &Path) -> io::Result<String> {
101    // Try to get name from devcontainer.json configuration
102    let name = if let Some(name) = config.get("name").and_then(|n| n.as_str()) {
103        // Sanitize the name to be docker-compatible
104        sanitize_docker_name(name)
105    } else {
106        // Fallback to parent directory name if no name in config
107        let dir_name = devcontainer_path
108            .parent()
109            .and_then(|p| p.file_name())
110            .and_then(|n| n.to_str())
111            .ok_or_else(|| {
112                io::Error::new(io::ErrorKind::Other, "Failed to determine container name from path")
113            })?;
114        sanitize_docker_name(dir_name)
115    };
116
117    // Get optional version from config
118    let version = config
119        .get("version")
120        .and_then(|v| v.as_str())
121        .unwrap_or("latest");
122
123    Ok(format!("{}-devcontainer:{}", name, version))
124}
125
126fn sanitize_docker_name(name: &str) -> String {
127    // Docker image names must be lowercase and can only contain:
128    // lowercase letters, digits, dots, underscores, or hyphens
129    name.to_lowercase()
130        .chars()
131        .map(|c| {
132            match c {
133                'a'..='z' | '0'..='9' | '.' | '_' | '-' => c,
134                _ => '-',
135            }
136        })
137        .collect()
138}
139
140// Example usage in main:
141pub fn scan_and_build(path: &Path) -> Result<()> {
142    if !path.exists() {
143        return Err(anyhow!("Failed to locate application build path"));
144    }
145    image_gen::gen_devcontainer(path.to_str().unwrap());
146    let status = ensure::ensure_installations().context("Failed to enture installation")?;
147    println!("Installation status: {:?}", status);
148
149    println!("Building devcontainer image...");
150
151    let dev_ctr_json_str = format!("{}/.devcontainer/devcontainer.json", path.to_str().unwrap());
152    let dev_ctr_json_path: &Path = Path::new(&dev_ctr_json_str);
153
154    match build_devcontainer(dev_ctr_json_path) {
155        Ok(image) => println!("Built container image: {}", image),
156        Err(e) => eprintln!("Failed to build container: {}", e),
157    }
158
159    Ok(())
160}