1use anyhow::Result;
6use clap::Subcommand;
7use std::process::Command;
8
9#[derive(Subcommand)]
10pub enum EnvCommands {
11 Snapshot {
13 #[arg(long, default_value = "zernel-env.yml")]
15 output: String,
16 },
17 Diff {
19 a: String,
21 b: String,
23 },
24 Reproduce {
26 file: String,
28 },
29 Export {
31 #[arg(long, default_value = "docker")]
33 format: String,
34 #[arg(long, default_value = "Dockerfile.zernel")]
36 output: String,
37 },
38 Show,
40}
41
42fn run_cmd(cmd: &str, args: &[&str]) -> String {
43 Command::new(cmd)
44 .args(args)
45 .output()
46 .ok()
47 .filter(|o| o.status.success())
48 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
49 .unwrap_or_else(|| "N/A".into())
50}
51
52pub async fn run(cmd: EnvCommands) -> Result<()> {
53 match cmd {
54 EnvCommands::Show | EnvCommands::Snapshot { .. } => {
55 let os_info = run_cmd("uname", &["-srm"]);
56 let python = run_cmd("python3", &["--version"]);
57 let pip_list = run_cmd("pip", &["list", "--format=freeze"]);
58 let cuda = run_cmd("nvcc", &["--version"]);
59 let driver = run_cmd(
60 "nvidia-smi",
61 &["--query-gpu=driver_version", "--format=csv,noheader"],
62 );
63 let gpu_name = run_cmd("nvidia-smi", &["--query-gpu=name", "--format=csv,noheader"]);
64 let torch_ver = run_cmd("python3", &["-c", "import torch; print(torch.__version__)"]);
65 let torch_cuda = run_cmd(
66 "python3",
67 &["-c", "import torch; print(torch.version.cuda)"],
68 );
69
70 let snapshot = format!(
71 "# Zernel Environment Snapshot\n\
72 # Generated: {}\n\n\
73 os: {}\n\
74 python: {}\n\
75 nvidia_driver: {}\n\
76 gpu: {}\n\
77 cuda_toolkit: {}\n\
78 torch: {}\n\
79 torch_cuda: {}\n\n\
80 # pip packages\n\
81 packages:\n{}\n",
82 chrono::Utc::now().to_rfc3339(),
83 os_info,
84 python,
85 driver.lines().next().unwrap_or("N/A"),
86 gpu_name.lines().next().unwrap_or("N/A"),
87 cuda.lines().last().unwrap_or("N/A"),
88 torch_ver,
89 torch_cuda,
90 pip_list
91 .lines()
92 .map(|l| format!(" - {l}"))
93 .collect::<Vec<_>>()
94 .join("\n"),
95 );
96
97 if let EnvCommands::Snapshot { output } = cmd {
98 std::fs::write(&output, &snapshot)?;
99 println!("Environment snapshot saved to: {output}");
100 } else {
101 print!("{snapshot}");
102 }
103 }
104
105 EnvCommands::Diff { a, b } => {
106 let a_content = std::fs::read_to_string(&a)?;
107 let b_content = std::fs::read_to_string(&b)?;
108
109 println!("Environment Diff: {a} vs {b}");
110 println!("{}", "=".repeat(60));
111
112 let a_lines: std::collections::HashSet<&str> = a_content.lines().collect();
113 let b_lines: std::collections::HashSet<&str> = b_content.lines().collect();
114
115 let only_a: Vec<&&str> = a_lines.difference(&b_lines).collect();
116 let only_b: Vec<&&str> = b_lines.difference(&a_lines).collect();
117
118 if !only_a.is_empty() {
119 println!("\nOnly in {a}:");
120 for line in &only_a {
121 if !line.starts_with('#') && !line.is_empty() {
122 println!(" - {line}");
123 }
124 }
125 }
126
127 if !only_b.is_empty() {
128 println!("\nOnly in {b}:");
129 for line in &only_b {
130 if !line.starts_with('#') && !line.is_empty() {
131 println!(" + {line}");
132 }
133 }
134 }
135
136 if only_a.is_empty() && only_b.is_empty() {
137 println!("Environments are identical.");
138 }
139 }
140
141 EnvCommands::Reproduce { file } => {
142 let content = std::fs::read_to_string(&file)?;
143 println!("Reproducing environment from: {file}");
144
145 let mut in_packages = false;
147 let mut packages = Vec::new();
148 for line in content.lines() {
149 if line.starts_with("packages:") {
150 in_packages = true;
151 continue;
152 }
153 if in_packages {
154 if let Some(pkg) = line.strip_prefix(" - ") {
155 packages.push(pkg.to_string());
156 } else {
157 in_packages = false;
158 }
159 }
160 }
161
162 if packages.is_empty() {
163 println!("No packages found in snapshot.");
164 return Ok(());
165 }
166
167 println!("Installing {} packages...", packages.len());
168 let reqs = packages.join("\n");
169 let req_file = "/tmp/zernel-requirements.txt";
170 std::fs::write(req_file, &reqs)?;
171
172 let status = Command::new("pip")
173 .args(["install", "-r", req_file])
174 .status()?;
175
176 if status.success() {
177 println!("Environment reproduced successfully.");
178 } else {
179 println!("Some packages failed to install.");
180 }
181 }
182
183 EnvCommands::Export { format, output } => match format.as_str() {
184 "docker" => {
185 let pip_list = run_cmd("pip", &["list", "--format=freeze"]);
186 let dockerfile =
187 "# Generated by zernel env export\n\
188 FROM nvidia/cuda:12.6.0-runtime-ubuntu24.04\n\n\
189 RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*\n\n\
190 COPY requirements.txt /tmp/\n\
191 RUN pip install --no-cache-dir -r /tmp/requirements.txt\n\n\
192 WORKDIR /workspace\n";
193 std::fs::write(&output, dockerfile)?;
194 std::fs::write("requirements.txt", &pip_list)?;
195 println!("Exported to: {output} + requirements.txt");
196 println!("Build: docker build -f {output} -t my-env .");
197 }
198 "pip" => {
199 let pip_list = run_cmd("pip", &["list", "--format=freeze"]);
200 std::fs::write(&output, &pip_list)?;
201 println!("Exported to: {output}");
202 println!("Reproduce: pip install -r {output}");
203 }
204 other => {
205 println!("Unknown format: {other}. Available: docker, pip");
206 }
207 },
208 }
209 Ok(())
210}