rusty_stack/installers/components/
comfyui.rs1#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ShellCommand {
19 pub program: String,
21 pub args: Vec<String>,
23 pub env: Vec<(String, String)>,
25}
26
27impl ShellCommand {
28 pub fn to_command_string(&self) -> String {
30 let env_prefix = self
31 .env
32 .iter()
33 .map(|(k, v)| format!("{k}={v}"))
34 .collect::<Vec<_>>()
35 .join(" ");
36 let cmd = if self.args.is_empty() {
37 self.program.clone()
38 } else {
39 format!("{} {}", self.program, self.args.join(" "))
40 };
41 if env_prefix.is_empty() {
42 cmd
43 } else {
44 format!("{env_prefix} {cmd}")
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct ComfyuiConfig {
52 pub install_dir: String,
54 pub python_bin: String,
56 pub dry_run: bool,
58}
59
60impl Default for ComfyuiConfig {
61 fn default() -> Self {
62 Self {
63 install_dir: default_install_dir(),
64 python_bin: "python3".to_string(),
65 dry_run: false,
66 }
67 }
68}
69
70pub const REPO_URL: &str = "https://github.com/comfyanonymous/ComfyUI.git";
72
73pub const DEFAULT_BRANCH: &str = "master";
75
76pub const WEB_PORT: u16 = 8188;
78
79pub const EXCLUDED_PACKAGES: &[&str] = &[
82 "torch",
83 "torchvision",
84 "torchaudio",
85 "torchsde",
86 "sentencepiece",
87];
88
89pub const PRESERVE_DIRS: &[&str] = &["models", "input", "output", "user"];
91
92pub fn default_install_dir() -> String {
94 format!("{}/ComfyUI", std::env::var("HOME").unwrap_or_default())
95}
96
97pub struct ComfyuiInstaller {
99 config: ComfyuiConfig,
100}
101
102impl ComfyuiInstaller {
103 pub fn new(config: ComfyuiConfig) -> Self {
105 Self { config }
106 }
107
108 pub fn with_defaults() -> Self {
110 Self::new(ComfyuiConfig::default())
111 }
112
113 pub fn dependencies() -> Vec<&'static str> {
119 vec!["pytorch"]
120 }
121
122 pub fn build_pytorch_check_command(&self) -> ShellCommand {
124 ShellCommand {
125 program: self.config.python_bin.clone(),
126 args: vec!["-c".to_string(), "import torch".to_string()],
127 env: vec![],
128 }
129 }
130
131 pub fn build_git_clone_command(&self) -> ShellCommand {
140 ShellCommand {
141 program: "git".to_string(),
142 args: vec![
143 "clone".to_string(),
144 REPO_URL.to_string(),
145 self.config.install_dir.clone(),
146 ],
147 env: vec![],
148 }
149 }
150
151 pub fn build_git_update_commands(&self, has_user_data: bool) -> Vec<ShellCommand> {
162 let dir = &self.config.install_dir;
163 let mut cmds = vec![
164 ShellCommand {
165 program: "git".to_string(),
166 args: vec![
167 "-C".to_string(),
168 dir.clone(),
169 "remote".to_string(),
170 "set-head".to_string(),
171 "origin".to_string(),
172 "-a".to_string(),
173 ],
174 env: vec![],
175 },
176 ShellCommand {
177 program: "git".to_string(),
178 args: vec![
179 "-C".to_string(),
180 dir.clone(),
181 "checkout".to_string(),
182 DEFAULT_BRANCH.to_string(),
183 ],
184 env: vec![],
185 },
186 ShellCommand {
187 program: "git".to_string(),
188 args: vec![
189 "-C".to_string(),
190 dir.clone(),
191 "fetch".to_string(),
192 "--all".to_string(),
193 ],
194 env: vec![],
195 },
196 ];
197
198 if has_user_data {
199 cmds.push(ShellCommand {
200 program: "git".to_string(),
201 args: vec![
202 "-C".to_string(),
203 dir.clone(),
204 "stash".to_string(),
205 "push".to_string(),
206 "-u".to_string(),
207 "-m".to_string(),
208 "rusty-stack-preserve-user-data".to_string(),
209 "--".to_string(),
210 ]
211 .into_iter()
212 .chain(PRESERVE_DIRS.iter().map(|s| s.to_string()))
213 .collect(),
214 env: vec![],
215 });
216 }
217
218 cmds.push(ShellCommand {
219 program: "git".to_string(),
220 args: vec![
221 "-C".to_string(),
222 dir.clone(),
223 "reset".to_string(),
224 "--hard".to_string(),
225 format!("origin/{DEFAULT_BRANCH}"),
226 ],
227 env: vec![],
228 });
229
230 if has_user_data {
231 cmds.push(ShellCommand {
232 program: "git".to_string(),
233 args: vec![
234 "-C".to_string(),
235 dir.clone(),
236 "stash".to_string(),
237 "pop".to_string(),
238 ],
239 env: vec![],
240 });
241 }
242
243 cmds
244 }
245
246 pub fn build_pip_install_command(&self, requirements_path: &str) -> ShellCommand {
252 ShellCommand {
253 program: self.config.python_bin.clone(),
254 args: vec![
255 "-m".to_string(),
256 "pip".to_string(),
257 "install".to_string(),
258 "--break-system-packages".to_string(),
259 "-r".to_string(),
260 requirements_path.to_string(),
261 ],
262 env: vec![],
263 }
264 }
265
266 pub fn requirements_filter_pattern() -> String {
269 EXCLUDED_PACKAGES
270 .iter()
271 .map(|p| format!("^{}", p))
272 .collect::<Vec<_>>()
273 .join("|")
274 }
275
276 pub fn build_launcher_content(&self, gpu_devices: &str) -> String {
281 format!(
282 "#!/bin/bash\n# ComfyUI launcher for Stan's ML Stack\n# Auto-detected GPU devices: {}\ncd \"{}\" && \\\n HIP_VISIBLE_DEVICES={} \\\n CUDA_VISIBLE_DEVICES={} \\\n {} main.py --enable-manager \"$@\"\n",
283 gpu_devices,
284 self.config.install_dir,
285 gpu_devices,
286 gpu_devices,
287 self.config.python_bin,
288 )
289 }
290
291 pub fn build_systemd_service(&self, gpu_devices: &str) -> String {
293 format!(
294 "[Unit]\nDescription=ComfyUI - Node-based UI for Stable Diffusion\nAfter=network.target\n\n\
295 [Service]\nType=simple\nWorkingDirectory=\"{}\"\n\
296 Environment=\"HIP_VISIBLE_DEVICES={}\"\n\
297 Environment=\"CUDA_VISIBLE_DEVICES={}\"\n\
298 ExecStart={} \"{}\" --enable-manager\nRestart=on-failure\n\n\
299 [Install]\nWantedBy=default.target\n",
300 self.config.install_dir,
301 gpu_devices,
302 gpu_devices,
303 self.config.python_bin,
304 self.config.install_dir,
305 )
306 }
307
308 pub fn detect_gpu_devices(&self) -> String {
314 crate::installer::detect_gpu_list()
315 }
316
317 pub fn install_dir(&self) -> &str {
319 &self.config.install_dir
320 }
321
322 pub fn repo_url(&self) -> &str {
324 REPO_URL
325 }
326
327 pub fn default_branch(&self) -> &str {
329 DEFAULT_BRANCH
330 }
331}
332
333#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn make_installer(dir: &str) -> ComfyuiInstaller {
342 ComfyuiInstaller::new(ComfyuiConfig {
343 install_dir: dir.to_string(),
344 python_bin: "python3".to_string(),
345 dry_run: false,
346 })
347 }
348
349 #[test]
354 fn test_git_clone_url_matches_original_script() {
355 assert_eq!(
356 REPO_URL, "https://github.com/comfyanonymous/ComfyUI.git",
357 "URL must match install_comfyui.sh REPO_URL"
358 );
359 }
360
361 #[test]
362 fn test_git_clone_command() {
363 let installer = make_installer("/home/user/ComfyUI");
364 let cmd = installer.build_git_clone_command();
365 assert_eq!(cmd.program, "git");
366 assert!(cmd.args.contains(&"clone".to_string()));
367 assert!(cmd.args.contains(&REPO_URL.to_string()));
368 assert!(cmd.args.contains(&"/home/user/ComfyUI".to_string()));
369 }
370
371 #[test]
372 fn test_git_clone_command_string() {
373 let installer = make_installer("/home/user/ComfyUI");
374 let cmd = installer.build_git_clone_command();
375 let s = cmd.to_command_string();
376 assert!(s.contains("git clone"));
377 assert!(s.contains(REPO_URL));
378 }
379
380 #[test]
381 fn test_pip_install_command() {
382 let installer = make_installer("/home/user/ComfyUI");
383 let cmd = installer.build_pip_install_command("/tmp/filtered_reqs.txt");
384 assert_eq!(cmd.program, "python3");
385 assert!(cmd.args.contains(&"-m".to_string()));
386 assert!(cmd.args.contains(&"pip".to_string()));
387 assert!(cmd.args.contains(&"install".to_string()));
388 assert!(cmd.args.contains(&"-r".to_string()));
389 assert!(cmd.args.contains(&"/tmp/filtered_reqs.txt".to_string()));
390 }
391
392 #[test]
393 fn test_requirements_filter_excludes_torch_packages() {
394 let pattern = ComfyuiInstaller::requirements_filter_pattern();
395 for pkg in EXCLUDED_PACKAGES {
397 assert!(pattern.contains(pkg), "Pattern should contain '{pkg}'");
398 }
399 }
400
401 #[test]
402 fn test_excluded_packages_match_original_script() {
403 assert_eq!(
405 EXCLUDED_PACKAGES,
406 [
407 "torch",
408 "torchvision",
409 "torchaudio",
410 "torchsde",
411 "sentencepiece"
412 ]
413 );
414 }
415
416 #[test]
417 fn test_default_branch_is_master() {
418 assert_eq!(DEFAULT_BRANCH, "master");
420 }
421
422 #[test]
427 fn test_default_install_dir_is_home_comfyui() {
428 let config = ComfyuiConfig::default();
429 assert!(
430 config.install_dir.ends_with("/ComfyUI"),
431 "Default dir must end with /ComfyUI, got: {}",
432 config.install_dir,
433 );
434 }
435
436 #[test]
437 fn test_custom_install_dir() {
438 let installer = make_installer("/opt/apps/ComfyUI");
439 assert_eq!(installer.install_dir(), "/opt/apps/ComfyUI");
440 }
441
442 #[test]
447 fn test_declares_pytorch_dependency() {
448 let deps = ComfyuiInstaller::dependencies();
449 assert!(
450 deps.contains(&"pytorch"),
451 "ComfyUI must declare pytorch as a dependency"
452 );
453 }
454
455 #[test]
456 fn test_git_update_without_user_data() {
457 let installer = make_installer("/home/user/ComfyUI");
458 let cmds = installer.build_git_update_commands(false);
459 assert_eq!(cmds.len(), 4);
461 let last = cmds.last().unwrap();
463 assert!(last.args.contains(&"reset".to_string()));
464 }
465
466 #[test]
467 fn test_git_update_with_user_data() {
468 let installer = make_installer("/home/user/ComfyUI");
469 let cmds = installer.build_git_update_commands(true);
470 assert_eq!(cmds.len(), 6);
472 let cmd_strings: Vec<String> = cmds.iter().map(|c| c.to_command_string()).collect();
474 let has_stash = cmd_strings.iter().any(|s| s.contains("stash"));
475 assert!(
476 has_stash,
477 "Should contain stash commands when user data exists"
478 );
479 }
480
481 #[test]
482 fn test_launcher_content() {
483 let installer = make_installer("/home/user/ComfyUI");
484 let content = installer.build_launcher_content("0,1");
485 assert!(content.starts_with("#!/bin/bash"));
486 assert!(content.contains("/home/user/ComfyUI"));
487 assert!(content.contains("HIP_VISIBLE_DEVICES=0,1"));
488 assert!(content.contains("python3 main.py --enable-manager"));
489 }
490
491 #[test]
492 fn test_preserve_dirs_match_original_script() {
493 assert_eq!(PRESERVE_DIRS, ["models", "input", "output", "user"]);
495 }
496
497 #[test]
498 fn test_systemd_service_content() {
499 let installer = make_installer("/home/user/ComfyUI");
500 let svc = installer.build_systemd_service("0,1");
501 assert!(svc.contains("[Unit]"));
502 assert!(svc.contains("ComfyUI"));
503 assert!(svc.contains("HIP_VISIBLE_DEVICES=0,1"));
504 assert!(svc.contains("--enable-manager"));
505 }
506}