solverforge_service/
jar.rs1use crate::error::{ServiceError, ServiceResult};
2use crate::util::{find_java, find_maven, find_submodule_dir, get_cache_dir};
3use log::{debug, info, warn};
4use std::fs::{self, File};
5use std::io::Write;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9const MAVEN_GROUP_ID: &str = "org.solverforge";
11const MAVEN_ARTIFACT_ID: &str = "solverforge-wasm-service";
12const MAVEN_VERSION: &str = "0.2.1";
13const MAVEN_CENTRAL_URL: &str = "https://repo1.maven.org/maven2";
14
15pub struct JarManager {
16 submodule_dir: PathBuf,
17 cache_dir: PathBuf,
18 java_home: Option<PathBuf>,
19}
20
21impl JarManager {
22 pub fn new() -> ServiceResult<Self> {
23 let submodule_dir = find_submodule_dir()?;
24 let cache_dir = get_cache_dir();
25 Ok(Self {
26 submodule_dir,
27 cache_dir,
28 java_home: None,
29 })
30 }
31
32 pub fn with_paths(submodule_dir: PathBuf, cache_dir: PathBuf) -> Self {
33 Self {
34 submodule_dir,
35 cache_dir,
36 java_home: None,
37 }
38 }
39
40 pub fn with_java_home(mut self, java_home: Option<&Path>) -> Self {
41 self.java_home = java_home.map(|p| p.to_path_buf());
42 self
43 }
44
45 pub fn ensure_jar(&self) -> ServiceResult<PathBuf> {
46 let jar_path = self.jar_path();
47
48 if jar_path.exists() {
50 debug!("Using cached JAR: {}", jar_path.display());
51 return Ok(jar_path);
52 }
53
54 if self.submodule_dir.join("pom.xml").exists() {
56 info!("Building solverforge-wasm-service JAR from submodule...");
57 match self.build_jar() {
58 Ok(()) => {
59 if jar_path.exists() {
60 return Ok(jar_path);
61 }
62 }
63 Err(e) => {
64 warn!("Local build failed: {}, trying Maven download...", e);
65 }
66 }
67 }
68
69 info!("Downloading solverforge-wasm-service from Maven Central...");
71 self.download_from_maven()?;
72
73 if !jar_path.exists() {
74 return Err(ServiceError::BuildFailed(
75 "JAR not found after download".to_string(),
76 ));
77 }
78
79 Ok(jar_path)
80 }
81
82 pub fn jar_exists(&self) -> bool {
83 self.jar_path().exists()
84 }
85
86 pub fn jar_path(&self) -> PathBuf {
87 self.cache_dir.join(format!(
89 "{}-{}-runner.jar",
90 MAVEN_ARTIFACT_ID, MAVEN_VERSION
91 ))
92 }
93
94 pub fn rebuild(&self) -> ServiceResult<PathBuf> {
95 let jar_path = self.jar_path();
96 if jar_path.exists() {
97 fs::remove_file(&jar_path)?;
98 }
99 self.ensure_jar()
100 }
101
102 fn build_jar(&self) -> ServiceResult<()> {
103 let mvn = find_maven()?;
104
105 fs::create_dir_all(&self.cache_dir)?;
106
107 let java_home = if let Some(ref home) = self.java_home {
110 home.clone()
111 } else {
112 let java = find_java(None)?;
114 java.parent()
116 .and_then(|bin| bin.parent())
117 .map(|home| home.to_path_buf())
118 .ok_or_else(|| {
119 ServiceError::JavaNotFound("Cannot determine JAVA_HOME from java path".into())
120 })?
121 };
122
123 info!(
124 "Running mvn package in {} with JAVA_HOME={}",
125 self.submodule_dir.display(),
126 java_home.display()
127 );
128
129 let output = Command::new(&mvn)
130 .current_dir(&self.submodule_dir)
131 .env("JAVA_HOME", &java_home)
132 .args(["package", "-DskipTests", "-q"])
133 .output()?;
134
135 if !output.status.success() {
136 let stderr = String::from_utf8_lossy(&output.stderr);
137 return Err(ServiceError::BuildFailed(format!(
138 "Maven build failed: {}",
139 stderr
140 )));
141 }
142
143 let built_jar = self.submodule_dir.join("target").join(format!(
145 "{}-{}-runner.jar",
146 MAVEN_ARTIFACT_ID, MAVEN_VERSION
147 ));
148
149 if !built_jar.exists() {
150 return Err(ServiceError::BuildFailed(format!(
151 "Expected JAR not found at {}",
152 built_jar.display()
153 )));
154 }
155
156 fs::create_dir_all(&self.cache_dir)?;
157 let cached_jar = self.jar_path();
158
159 info!("Copying JAR to cache: {}", cached_jar.display());
160 fs::copy(&built_jar, &cached_jar)?;
161
162 Ok(())
163 }
164
165 fn download_from_maven(&self) -> ServiceResult<()> {
166 let group_path = MAVEN_GROUP_ID.replace('.', "/");
168 let jar_url = format!(
169 "{}/{}/{}/{}/{}-{}-runner.jar",
170 MAVEN_CENTRAL_URL,
171 group_path,
172 MAVEN_ARTIFACT_ID,
173 MAVEN_VERSION,
174 MAVEN_ARTIFACT_ID,
175 MAVEN_VERSION
176 );
177
178 info!("Downloading from: {}", jar_url);
179
180 let response = reqwest::blocking::get(&jar_url)
181 .map_err(|e| ServiceError::DownloadFailed(format!("Failed to download JAR: {}", e)))?;
182
183 if !response.status().is_success() {
184 return Err(ServiceError::DownloadFailed(format!(
185 "HTTP {}: {}",
186 response.status(),
187 jar_url
188 )));
189 }
190
191 let bytes = response
192 .bytes()
193 .map_err(|e| ServiceError::DownloadFailed(format!("Failed to read response: {}", e)))?;
194
195 fs::create_dir_all(&self.cache_dir)?;
196 let jar_path = self.jar_path();
197
198 let mut file = File::create(&jar_path)?;
199 file.write_all(&bytes)?;
200
201 info!("Downloaded JAR to: {}", jar_path.display());
202 Ok(())
203 }
204
205 pub fn cache_dir(&self) -> &Path {
206 &self.cache_dir
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use tempfile::TempDir;
214
215 #[test]
216 fn test_jar_path() {
217 let temp = TempDir::new().unwrap();
218 let manager =
219 JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
220
221 let jar_path = manager.jar_path();
222 assert!(jar_path
224 .to_string_lossy()
225 .contains("solverforge-wasm-service"));
226 assert!(jar_path.to_string_lossy().contains("-runner.jar"));
227 }
228
229 #[test]
230 fn test_jar_exists_false() {
231 let temp = TempDir::new().unwrap();
232 let manager =
233 JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
234
235 assert!(!manager.jar_exists());
236 }
237
238 #[test]
239 fn test_cache_dir() {
240 let temp = TempDir::new().unwrap();
241 let manager =
242 JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
243
244 assert_eq!(manager.cache_dir(), temp.path());
245 }
246}