#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellCommand {
pub program: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}
impl ShellCommand {
pub fn to_command_string(&self) -> String {
let env_prefix = self
.env
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ");
let cmd = if self.args.is_empty() {
self.program.clone()
} else {
format!("{} {}", self.program, self.args.join(" "))
};
if env_prefix.is_empty() {
cmd
} else {
format!("{env_prefix} {cmd}")
}
}
}
#[derive(Debug, Clone)]
pub struct ComfyuiConfig {
pub install_dir: String,
pub python_bin: String,
pub dry_run: bool,
}
impl Default for ComfyuiConfig {
fn default() -> Self {
Self {
install_dir: default_install_dir(),
python_bin: "python3".to_string(),
dry_run: false,
}
}
}
pub const REPO_URL: &str = "https://github.com/comfyanonymous/ComfyUI.git";
pub const DEFAULT_BRANCH: &str = "master";
pub const WEB_PORT: u16 = 8188;
pub const EXCLUDED_PACKAGES: &[&str] = &[
"torch",
"torchvision",
"torchaudio",
"torchsde",
"sentencepiece",
];
pub const PRESERVE_DIRS: &[&str] = &["models", "input", "output", "user"];
pub fn default_install_dir() -> String {
format!("{}/ComfyUI", std::env::var("HOME").unwrap_or_default())
}
pub struct ComfyuiInstaller {
config: ComfyuiConfig,
}
impl ComfyuiInstaller {
pub fn new(config: ComfyuiConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(ComfyuiConfig::default())
}
pub fn dependencies() -> Vec<&'static str> {
vec!["pytorch"]
}
pub fn build_pytorch_check_command(&self) -> ShellCommand {
ShellCommand {
program: self.config.python_bin.clone(),
args: vec!["-c".to_string(), "import torch".to_string()],
env: vec![],
}
}
pub fn build_git_clone_command(&self) -> ShellCommand {
ShellCommand {
program: "git".to_string(),
args: vec![
"clone".to_string(),
REPO_URL.to_string(),
self.config.install_dir.clone(),
],
env: vec![],
}
}
pub fn build_git_update_commands(&self, has_user_data: bool) -> Vec<ShellCommand> {
let dir = &self.config.install_dir;
let mut cmds = vec![
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"remote".to_string(),
"set-head".to_string(),
"origin".to_string(),
"-a".to_string(),
],
env: vec![],
},
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"checkout".to_string(),
DEFAULT_BRANCH.to_string(),
],
env: vec![],
},
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"fetch".to_string(),
"--all".to_string(),
],
env: vec![],
},
];
if has_user_data {
cmds.push(ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"stash".to_string(),
"push".to_string(),
"-u".to_string(),
"-m".to_string(),
"rusty-stack-preserve-user-data".to_string(),
"--".to_string(),
]
.into_iter()
.chain(PRESERVE_DIRS.iter().map(|s| s.to_string()))
.collect(),
env: vec![],
});
}
cmds.push(ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"reset".to_string(),
"--hard".to_string(),
format!("origin/{DEFAULT_BRANCH}"),
],
env: vec![],
});
if has_user_data {
cmds.push(ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"stash".to_string(),
"pop".to_string(),
],
env: vec![],
});
}
cmds
}
pub fn build_pip_install_command(&self, requirements_path: &str) -> ShellCommand {
ShellCommand {
program: self.config.python_bin.clone(),
args: vec![
"-m".to_string(),
"pip".to_string(),
"install".to_string(),
"--break-system-packages".to_string(),
"-r".to_string(),
requirements_path.to_string(),
],
env: vec![],
}
}
pub fn requirements_filter_pattern() -> String {
EXCLUDED_PACKAGES
.iter()
.map(|p| format!("^{}", p))
.collect::<Vec<_>>()
.join("|")
}
pub fn build_launcher_content(&self, gpu_devices: &str) -> String {
format!(
"#!/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",
gpu_devices,
self.config.install_dir,
gpu_devices,
gpu_devices,
self.config.python_bin,
)
}
pub fn build_systemd_service(&self, gpu_devices: &str) -> String {
format!(
"[Unit]\nDescription=ComfyUI - Node-based UI for Stable Diffusion\nAfter=network.target\n\n\
[Service]\nType=simple\nWorkingDirectory=\"{}\"\n\
Environment=\"HIP_VISIBLE_DEVICES={}\"\n\
Environment=\"CUDA_VISIBLE_DEVICES={}\"\n\
ExecStart={} \"{}\" --enable-manager\nRestart=on-failure\n\n\
[Install]\nWantedBy=default.target\n",
self.config.install_dir,
gpu_devices,
gpu_devices,
self.config.python_bin,
self.config.install_dir,
)
}
pub fn detect_gpu_devices(&self) -> String {
crate::installer::detect_gpu_list()
}
pub fn install_dir(&self) -> &str {
&self.config.install_dir
}
pub fn repo_url(&self) -> &str {
REPO_URL
}
pub fn default_branch(&self) -> &str {
DEFAULT_BRANCH
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_installer(dir: &str) -> ComfyuiInstaller {
ComfyuiInstaller::new(ComfyuiConfig {
install_dir: dir.to_string(),
python_bin: "python3".to_string(),
dry_run: false,
})
}
#[test]
fn test_git_clone_url_matches_original_script() {
assert_eq!(
REPO_URL, "https://github.com/comfyanonymous/ComfyUI.git",
"URL must match install_comfyui.sh REPO_URL"
);
}
#[test]
fn test_git_clone_command() {
let installer = make_installer("/home/user/ComfyUI");
let cmd = installer.build_git_clone_command();
assert_eq!(cmd.program, "git");
assert!(cmd.args.contains(&"clone".to_string()));
assert!(cmd.args.contains(&REPO_URL.to_string()));
assert!(cmd.args.contains(&"/home/user/ComfyUI".to_string()));
}
#[test]
fn test_git_clone_command_string() {
let installer = make_installer("/home/user/ComfyUI");
let cmd = installer.build_git_clone_command();
let s = cmd.to_command_string();
assert!(s.contains("git clone"));
assert!(s.contains(REPO_URL));
}
#[test]
fn test_pip_install_command() {
let installer = make_installer("/home/user/ComfyUI");
let cmd = installer.build_pip_install_command("/tmp/filtered_reqs.txt");
assert_eq!(cmd.program, "python3");
assert!(cmd.args.contains(&"-m".to_string()));
assert!(cmd.args.contains(&"pip".to_string()));
assert!(cmd.args.contains(&"install".to_string()));
assert!(cmd.args.contains(&"-r".to_string()));
assert!(cmd.args.contains(&"/tmp/filtered_reqs.txt".to_string()));
}
#[test]
fn test_requirements_filter_excludes_torch_packages() {
let pattern = ComfyuiInstaller::requirements_filter_pattern();
for pkg in EXCLUDED_PACKAGES {
assert!(pattern.contains(pkg), "Pattern should contain '{pkg}'");
}
}
#[test]
fn test_excluded_packages_match_original_script() {
assert_eq!(
EXCLUDED_PACKAGES,
[
"torch",
"torchvision",
"torchaudio",
"torchsde",
"sentencepiece"
]
);
}
#[test]
fn test_default_branch_is_master() {
assert_eq!(DEFAULT_BRANCH, "master");
}
#[test]
fn test_default_install_dir_is_home_comfyui() {
let config = ComfyuiConfig::default();
assert!(
config.install_dir.ends_with("/ComfyUI"),
"Default dir must end with /ComfyUI, got: {}",
config.install_dir,
);
}
#[test]
fn test_custom_install_dir() {
let installer = make_installer("/opt/apps/ComfyUI");
assert_eq!(installer.install_dir(), "/opt/apps/ComfyUI");
}
#[test]
fn test_declares_pytorch_dependency() {
let deps = ComfyuiInstaller::dependencies();
assert!(
deps.contains(&"pytorch"),
"ComfyUI must declare pytorch as a dependency"
);
}
#[test]
fn test_git_update_without_user_data() {
let installer = make_installer("/home/user/ComfyUI");
let cmds = installer.build_git_update_commands(false);
assert_eq!(cmds.len(), 4);
let last = cmds.last().unwrap();
assert!(last.args.contains(&"reset".to_string()));
}
#[test]
fn test_git_update_with_user_data() {
let installer = make_installer("/home/user/ComfyUI");
let cmds = installer.build_git_update_commands(true);
assert_eq!(cmds.len(), 6);
let cmd_strings: Vec<String> = cmds.iter().map(|c| c.to_command_string()).collect();
let has_stash = cmd_strings.iter().any(|s| s.contains("stash"));
assert!(
has_stash,
"Should contain stash commands when user data exists"
);
}
#[test]
fn test_launcher_content() {
let installer = make_installer("/home/user/ComfyUI");
let content = installer.build_launcher_content("0,1");
assert!(content.starts_with("#!/bin/bash"));
assert!(content.contains("/home/user/ComfyUI"));
assert!(content.contains("HIP_VISIBLE_DEVICES=0,1"));
assert!(content.contains("python3 main.py --enable-manager"));
}
#[test]
fn test_preserve_dirs_match_original_script() {
assert_eq!(PRESERVE_DIRS, ["models", "input", "output", "user"]);
}
#[test]
fn test_systemd_service_content() {
let installer = make_installer("/home/user/ComfyUI");
let svc = installer.build_systemd_service("0,1");
assert!(svc.contains("[Unit]"));
assert!(svc.contains("ComfyUI"));
assert!(svc.contains("HIP_VISIBLE_DEVICES=0,1"));
assert!(svc.contains("--enable-manager"));
}
}