Skip to main content

cargo_shipit/
lib.rs

1pub mod cli;
2
3use cargo_metadata::TargetKind;
4use cli::CliArgs;
5use serde::{Deserialize, Serialize};
6use std::{fs::File, path::Path};
7
8#[derive(Debug, Deserialize, Serialize)]
9pub struct FileConfig {
10    pub host: Option<String>,
11    pub port: Option<u16>,
12    pub username: Option<String>,
13    pub password: Option<String>,
14    pub key: Option<String>,
15    pub target_folder: String,
16    pub target: Option<String>,
17    pub remote_folder: Option<String>,
18    pub profile: Option<String>,
19}
20
21impl FileConfig {
22    pub fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
23        let file = File::open(path)?;
24        let reader = std::io::BufReader::new(file);
25        let mut config: FileConfig = serde_json::from_reader(reader)?;
26        
27        // Convert relative paths to absolute paths based on config file location
28        config.resolve_relative_paths(path)?;
29        
30        Ok(config)
31    }
32    
33    /// Resolves relative paths in the config to absolute paths based on the config file's location
34    fn resolve_relative_paths(&mut self, config_file_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
35        // Get the directory containing the config file
36        let config_dir = config_file_path.parent()
37            .ok_or("Config file has no parent directory")?;
38        
39        // Resolve target_folder if it's a relative path
40        let target_path = Path::new(&self.target_folder);
41        if target_path.is_relative() {
42            self.target_folder = config_dir.join(target_path)
43                .canonicalize()
44                .unwrap_or_else(|_| config_dir.join(target_path))
45                .to_string_lossy()
46                .to_string();
47            
48            // Ensure it ends with a separator
49            if !self.target_folder.ends_with('/') && !self.target_folder.ends_with('\\') {
50                self.target_folder.push('/');
51            }
52        }
53        
54        // Resolve SSH key path if it's relative or contains tilde
55        if let Some(ref key_path) = self.key {
56            if key_path.starts_with("~/") {
57                // Handle tilde expansion
58                if let Ok(home_dir) = std::env::var("HOME") {
59                    let expanded_path = key_path.replacen("~", &home_dir, 1);
60                    self.key = Some(expanded_path);
61                }
62            } else {
63                let key_path_obj = Path::new(key_path);
64                if key_path_obj.is_relative() {
65                    let resolved_key = config_dir.join(key_path_obj)
66                        .canonicalize()
67                        .unwrap_or_else(|_| config_dir.join(key_path_obj));
68                    self.key = Some(resolved_key.to_string_lossy().to_string());
69                }
70            }
71        }
72        
73        Ok(())
74    }
75
76    pub fn cli_overide(&mut self, cli: &CliArgs) {
77        if let Some(host) = &cli.host {
78            self.host = Some(host.clone());
79        }
80        if let Some(port) = cli.port {
81            self.port = Some(port);
82        }
83        if let Some(username) = &cli.username {
84            self.username = Some(username.clone());
85        }
86        if let Some(password) = &cli.password {
87            self.password = Some(password.clone());
88        }
89        if let Some(key) = &cli.key {
90            self.key = Some(key.clone());
91        }
92        if let Some(remote_folder) = &cli.remote_folder {
93            self.remote_folder = Some(remote_folder.clone());
94        }
95        if !cli.profile.is_empty() {
96            self.profile = Some(cli.profile.clone());
97        }
98        if let Some(target_folder) = &cli.target_folder {
99            self.target_folder = target_folder.clone();
100            self.target = None;
101        }
102    }
103
104    pub fn create_empty(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
105        let file = File::create(path)?;
106        serde_json::to_writer_pretty(file, self)?;
107        Ok(())
108    }
109}
110
111#[derive(Debug)]
112pub struct Config {
113    pub host: String,
114    pub port: u16,
115    pub username: String,
116    pub password: String,
117    pub key: Option<String>,
118    pub target_folder: String,
119    pub target: String,
120    pub remote_folder: String,
121    pub debug: bool,
122    pub build: bool,
123    pub binaries: Vec<String>,
124    pub profile: String,
125}
126
127impl Config {
128    pub fn from_cli(cli: CliArgs, file_config: FileConfig) -> Self {
129        let host = cli.host.unwrap_or(file_config.host.unwrap());
130        let port = cli.port.unwrap_or(file_config.port.unwrap_or(22));
131        let username = cli.username.unwrap_or(file_config.username.unwrap());
132        let password = cli.password.unwrap_or(file_config.password.unwrap_or_default());
133        let key = cli.key.or(file_config.key.clone());
134        let target_folder = cli.target_folder.unwrap_or(file_config.target_folder);
135        let target = cli.target.unwrap_or(file_config.target.unwrap());
136        let remote_folder = cli.remote_folder.unwrap_or(
137            file_config
138                .remote_folder
139                .unwrap_or("/tmp/binaries/".to_string()),
140        );
141        let debug = cli.debug;
142        
143        // Auto-detect binaries if not specified
144        let binaries = if cli.binaries.is_empty() {
145            match detect_cargo_binaries() {
146                Ok(auto_binaries) if !auto_binaries.is_empty() => {
147                    println!("Auto-detected binaries: {}", auto_binaries.join(", "));
148                    auto_binaries
149                }
150                Ok(_) => {
151                    eprintln!("Warning: No binaries detected automatically. Please specify --binaries manually.");
152                    cli.binaries
153                }
154                Err(e) => {
155                    eprintln!("Warning: Failed to auto-detect binaries ({}). Please specify --binaries manually.", e);
156                    cli.binaries
157                }
158            }
159        } else {
160            cli.binaries
161        };
162        
163        let profile = file_config.profile.unwrap_or_else(|| cli.profile.clone());
164        
165        // Auto-detect if build is needed when not explicitly specified
166        let build = if cli.build {
167            true
168        } else {
169            // Check if we're in a cargo project and if binaries exist
170            should_auto_build(&target_folder, &target, &binaries, &profile, debug)
171        };
172
173        Config {
174            host,
175            port,
176            username,
177            password,
178            key,
179            target_folder,
180            target,
181            remote_folder,
182            debug,
183            build,
184            binaries,
185            profile,
186        }
187    }
188}
189
190/// Detects binary targets from the current Cargo project using cargo metadata
191pub fn detect_cargo_binaries() -> Result<Vec<String>, Box<dyn std::error::Error>> {
192    let metadata = cargo_metadata::MetadataCommand::new().exec()?;
193    
194    // Get the root package (the current workspace member or the main package)
195    let root_package = metadata.root_package()
196        .ok_or("No root package found - are you in a Cargo project?")?;
197    
198    // Extract binary targets
199    let binaries: Vec<String> = root_package
200        .targets
201        .iter()
202        .filter_map(|target| {
203            // Check if this target is a binary
204            if target.kind.iter().any(|k| k == &TargetKind::Bin) {
205                Some(target.name.clone())
206            } else {
207                None
208            }
209        })
210        .collect();
211    Ok(binaries)
212}
213
214/// Determines if an automatic build is needed based on whether binaries exist and are up-to-date
215fn should_auto_build(target_folder: &str, target: &str, binaries: &[String], profile: &str, debug: bool) -> bool {
216    // First, check if we're in a cargo project
217    if !Path::new("Cargo.toml").exists() {
218        return false; // Not a cargo project, no build needed
219    }
220    
221    // Build the expected binary path
222    let mut binary_path = std::path::PathBuf::from(target_folder);
223    if !target.is_empty() {
224        binary_path.push(target);
225    }
226    
227    // Add profile directory
228    if debug {
229        binary_path.push("debug");
230    } else {
231        binary_path.push(profile);
232    }
233    
234    // Check if any of the binaries are missing or need rebuilding
235    for binary_name in binaries {
236        let full_binary_path = binary_path.join(binary_name);
237        
238        // If binary doesn't exist, we need to build
239        if !full_binary_path.exists() {
240            println!("Auto-build: Binary '{}' not found at {}", binary_name, full_binary_path.display());
241            return true;
242        }
243        
244        // Check if Cargo.toml is newer than the binary (simple staleness check)
245        if let (Ok(cargo_meta), Ok(binary_meta)) = (
246            std::fs::metadata("Cargo.toml"),
247            std::fs::metadata(&full_binary_path)
248        ) {
249            if let (Ok(cargo_time), Ok(binary_time)) = (
250                cargo_meta.modified(),
251                binary_meta.modified()
252            ) {
253                if cargo_time > binary_time {
254                    println!("Auto-build: Binary '{}' is older than Cargo.toml", binary_name);
255                    return true;
256                }
257            }
258        }
259    }
260    
261    // All binaries exist and appear up-to-date
262    println!("Auto-build: Binaries appear up-to-date, skipping build");
263    false
264}