Skip to main content

conduit_cli/core/engine/workflow/loader/
install.rs

1use crate::core::domain::loader::Loader;
2use crate::core::engine::resolver::loader::ResolvedLoader;
3use crate::core::engine::workflow::Workflow;
4use crate::core::schemas::lock::{HashKind, Lockfile};
5use crate::core::schemas::manifest::Manifest;
6use crate::errors::{ConduitError, ConduitResult};
7use std::io::Error;
8use std::path::Path;
9use std::process::Command;
10
11impl Workflow {
12    pub async fn execute_installation(
13        &self,
14        resolved: &ResolvedLoader,
15        hash: &str,
16        kind: HashKind,
17        loader_type: &Loader,
18        mc_version: &str,
19    ) -> ConduitResult<()> {
20        let is_installer = resolved.file_name.contains("installer");
21        let target_name = if is_installer {
22            "installer.jar"
23        } else {
24            "server.jar"
25        };
26        let target_path = self.project_root.join(target_name);
27
28        if is_installer {
29            let source = self.ctx.store.object_path(hash, kind);
30            tokio::fs::copy(source, &target_path).await?;
31
32            self.run_java_installer(&target_path, loader_type, mc_version)?;
33            let _ = tokio::fs::remove_file(target_path).await;
34        } else {
35            self.ctx.store.link_object(hash, kind, &target_path).await?;
36        }
37
38        self.post_install_cleanup(loader_type).await
39    }
40
41    fn run_java_installer(
42        &self,
43        path: &std::path::Path,
44        loader_type: &Loader,
45        mc_version: &str,
46    ) -> ConduitResult<()> {
47        let mut cmd = Command::new("java");
48        cmd.arg("-jar").arg(path);
49
50        match loader_type {
51            Loader::Fabric => {
52                cmd.arg("server")
53                    .arg("-mcversion")
54                    .arg(mc_version)
55                    .arg("-downloadMinecraft");
56            }
57            _ => {
58                cmd.arg("--installServer");
59            }
60        }
61
62        let status = cmd
63            .current_dir(&self.project_root)
64            .stdout(std::process::Stdio::null())
65            .stderr(std::process::Stdio::null())
66            .status()
67            .map_err(|e| ConduitError::Io(Error::other(e.to_string())))?;
68
69        if !status.success() {
70            return Err(ConduitError::Io(Error::other(
71                "installer exited with error",
72            )));
73        }
74        Ok(())
75    }
76
77    async fn post_install_cleanup(&self, loader_type: &Loader) -> ConduitResult<()> {
78        tokio::fs::write(self.project_root.join("eula.txt"), "eula=true").await?;
79
80        if let Loader::Forge { version } = loader_type {
81            let mc_version = version.split('-').next().unwrap_or("");
82            let is_old = mc_version
83                .split('.')
84                .nth(1)
85                .and_then(|v| v.parse::<u32>().ok())
86                .is_some_and(|v| v <= 16);
87
88            if is_old {
89                let mut entries = tokio::fs::read_dir(&self.project_root).await?;
90                while let Some(entry) = entries.next_entry().await? {
91                    let name = entry.file_name().to_string_lossy().to_lowercase();
92                    if name.contains("forge")
93                        && Path::new(&name)
94                            .extension()
95                            .is_some_and(|ext| ext.eq_ignore_ascii_case("jar"))
96                        && !name.contains("installer")
97                    {
98                        let _ =
99                            tokio::fs::rename(entry.path(), self.project_root.join("server.jar"))
100                                .await;
101                        break;
102                    }
103                }
104            }
105        }
106
107        let files_to_delete = [
108            "installer.jar.log",
109            "run.sh",
110            "run.bat",
111            "user_jvm_args.txt",
112        ];
113        for file in files_to_delete {
114            let path = self.project_root.join(file);
115            if path.exists() {
116                let _ = tokio::fs::remove_file(path).await;
117            }
118        }
119        Ok(())
120    }
121
122    pub fn ensure_loader_presence(
123        &self,
124        lock: &Lockfile,
125        manifest: &Manifest,
126    ) -> ConduitResult<bool> {
127        if lock.instance.loader != manifest.project.loader
128            || lock.instance.minecraft_version != manifest.project.minecraft
129        {
130            return Ok(false);
131        }
132
133        match &lock.instance.loader {
134            Loader::Neoforge { .. } => {
135                let libs = self.project_root.join("libraries");
136                Ok(libs.exists())
137            }
138            Loader::Forge { version } => {
139                if Self::is_modern_forge(version) {
140                    let libs = self.project_root.join("libraries");
141                    Ok(libs.exists())
142                } else {
143                    Ok(self.project_root.join("server.jar").exists())
144                }
145            }
146            Loader::Fabric => {
147                let has_launch_jar = self.project_root.join("fabric-server-launch.jar").exists();
148                let has_server_jar = self.project_root.join("server.jar").exists();
149                Ok(has_launch_jar || has_server_jar)
150            }
151            _ => Ok(self.project_root.join("server.jar").exists()),
152        }
153    }
154}