From 78690ec57f77431c762ede7e224f079e96554556 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sun, 6 Aug 2023 18:13:39 +0300 Subject: [PATCH] maven, forge and neoforge --- Cargo.lock | 1 + Cargo.toml | 11 +- README.md | 12 +- src/core/runner.rs | 10 +- src/core/serverjar.rs | 20 ++- src/model/downloadable/markdown.rs | 20 ++- src/model/downloadable/mod.rs | 84 +++++++---- src/model/servertype/interactive.rs | 20 +-- src/model/servertype/meta.rs | 14 ++ src/model/servertype/mod.rs | 211 +++++++++++++++++++++------- src/sources/curserinth.rs | 13 -- src/sources/forge.rs | 88 ++++++++++++ src/sources/github.rs | 56 +++++--- src/sources/jenkins.rs | 15 -- src/sources/maven.rs | 122 ++++++++++++++++ src/sources/mod.rs | 3 + src/sources/modrinth.rs | 12 -- src/sources/neoforge.rs | 91 ++++++++++++ src/sources/spigot.rs | 14 -- src/util/maven_import.rs | 114 +++++++++++++++ src/util/mod.rs | 12 ++ src/util/mrpack.rs | 4 +- 22 files changed, 774 insertions(+), 173 deletions(-) create mode 100644 src/sources/forge.rs create mode 100644 src/sources/maven.rs create mode 100644 src/sources/neoforge.rs create mode 100644 src/util/maven_import.rs diff --git a/Cargo.lock b/Cargo.lock index fb1a59d..cacab2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,7 @@ dependencies = [ "pathdiff", "regex", "reqwest", + "roxmltree", "rpackwiz", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7e4f668..e66d8df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,16 @@ name = "mcman" version = "0.3.0" edition = "2021" -authors = ["ParadigmMC"] repository = "https://github.com/ParadigmMC/mcman" +homepage = "https://paradigmmc.github.io/mcman/" +authors = ["ParadigmMC"] +license = "gpl" +description = "Powerful Minecraft Server Manager CLI" +documentation = "https://paradigmmc.github.io/mcman/" +keywords = [ + "minecraft", "server" +] +categories = ["command-line-utilities", "config", ""] [profile.release] debug = false @@ -37,3 +45,4 @@ sha2 = "0.10" notify = { version = "6.0.1", default-features = false } glob-match = "0.2" async-trait = "0.1.72" +roxmltree = "0.18" \ No newline at end of file diff --git a/README.md b/README.md index d76e68a..8e65ab1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,17 @@ Submit a PR or open an issue if you have a mcman-server repository that we can a ## Changelog -### `0.3.0` (unreleased) +whats a semver? /s + +### `0.3.1` (unreleased) + +- Added [neoforge](https://neoforged.net/) server type +- Added [Forge](https://forums.minecraftforge.net/) server type +- Added Downloadable type **Maven** +- Improved building process +- Fixed a bug on `mcman run` which messed up the output when server crashes + +### `0.3.0` - Added [CurseRinth](https://curserinth.kuylar.dev/) support - Added **packwiz importing** diff --git a/src/core/runner.rs b/src/core/runner.rs index 4136fff..46c93d7 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -62,7 +62,7 @@ impl BuildContext { let (tx_stop, rx_stop) = oneshot::channel(); // stdout - tokio::spawn(async move { + let stdout_process = tokio::spawn(async move { let mut tx = Some(tx_stop); for buf in BufReader::new(stdout).lines() { let buf = buf.unwrap(); @@ -75,13 +75,14 @@ impl BuildContext { if test_mode && !test_passed_clone.load(std::sync::atomic::Ordering::Relaxed) - && line.contains("INFO]: Done") + && line.contains("]: Done") && line.ends_with("For help, type \"help\"") && tx.is_some() { println!( - "{} Server started successfully, stopping in 5s...", - style("").yellow().bold() + "{} {}", + style("").yellow().bold(), + style("Server started successfully, stopping in 5s...").yellow() ); test_passed_clone.store(true, std::sync::atomic::Ordering::Relaxed); tx.take() @@ -109,6 +110,7 @@ impl BuildContext { } let exit_status = child.wait()?; + stdout_process.await.context("Awaiting stdout proxy printing thread")?; if !exit_status.success() { println!(); diff --git a/src/core/serverjar.rs b/src/core/serverjar.rs index 0163b65..9a6ddea 100644 --- a/src/core/serverjar.rs +++ b/src/core/serverjar.rs @@ -21,7 +21,7 @@ impl BuildContext { let serverjar_name = match self .server .jar - .get_install_method(&self.http_client) + .get_install_method(&self.http_client, &self.server.mc_version) .await? { InstallMethod::Installer { @@ -35,7 +35,7 @@ impl BuildContext { .downloadable(&self.server.jar, None, |state, filename| match state { ReportBackState::Skipped => { println!( - " {name} present ({})", + " {name} is present ({})", style(filename.clone()).dim() ); } @@ -49,15 +49,25 @@ impl BuildContext { }) .await?; + let jar_name = jar_name.replace("${mcver}", &self.server.mc_version); + if !self.force && self.output_dir.join(&jar_name).exists() { println!( " Skipping server jar ({})", - style(jar_name.clone()).dim() + style(if rename_from.is_some() { + jar_name.clone() + } else { + "".to_owned() + }).dim() ); } else { println!( " Installing server jar... ({})", - style(jar_name.clone()).dim() + style(if rename_from.is_some() { + jar_name.clone() + } else { + "".to_owned() + }).dim() ); let mut cmd_args = vec!["-jar", &installer_jar]; @@ -105,7 +115,7 @@ impl BuildContext { } }?; - self.startup_method = self.server.jar.get_startup_method(&serverjar_name); + self.startup_method = self.server.jar.get_startup_method(&self.http_client, &serverjar_name, &self.server.mc_version).await?; Ok(()) } diff --git a/src/model/downloadable/markdown.rs b/src/model/downloadable/markdown.rs index 45fe3d9..2089917 100644 --- a/src/model/downloadable/markdown.rs +++ b/src/model/downloadable/markdown.rs @@ -32,6 +32,9 @@ impl Downloadable { let link = url.clone() + &str_process_job(job); format!("[{job}]({link})") } + Self::Maven { url, group, .. } => { + format!("[{g}]({url}/{g})", g = group.replace('.', "/")) + } Self::Spigot { id } => { format!("[{id}](https://www.spigotmc.org/resources/{id})") } @@ -104,6 +107,11 @@ impl Downloadable { map.insert("Version".to_owned(), format!("{build} / `{artifact}`")); } + Self::Maven { version, .. } => { + map.insert("Name".to_owned(), self.get_md_link()); + map.insert("Version".to_owned(), version.to_owned()); + } + Self::Url { url, filename, @@ -132,11 +140,12 @@ impl Downloadable { pub fn get_type_name(&self) -> String { match self { Self::Url { .. } => "URL", - Self::GithubRelease { .. } => "GithubRelease", + Self::GithubRelease { .. } => "GithubRel", Self::Jenkins { .. } => "Jenkins", Self::Modrinth { .. } => "Modrinth", Self::CurseRinth { .. } => "CurseRinth", Self::Spigot { .. } => "Spigot", + Self::Maven { .. } => "Maven", } .to_owned() } @@ -180,6 +189,12 @@ impl Downloadable { map.insert("Version/Release".to_owned(), build.clone()); map.insert("Asset/File".to_owned(), artifact.clone()); } + + Self::Maven { url, group, artifact, version, filename } => { + map.insert("Project/URL".to_owned(), format!("{group}.{artifact} - ({url})")); + map.insert("Version/Release".to_owned(), version.clone()); + map.insert("Asset/File".to_owned(), filename.clone()); + } } map @@ -199,6 +214,9 @@ impl Downloadable { "URL".to_string() } } + Self::Maven { group, artifact, .. } => { + format!("Maven/{group}.{artifact}") + } } } } diff --git a/src/model/downloadable/mod.rs b/src/model/downloadable/mod.rs index 6b0e4c2..ba43af3 100644 --- a/src/model/downloadable/mod.rs +++ b/src/model/downloadable/mod.rs @@ -2,14 +2,15 @@ use anyhow::Result; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use crate::sources::maven::{get_maven_url, self}; use crate::{model::Server, Source}; use crate::sources::{ - curserinth::{download_curserinth, fetch_curserinth_filename, get_curserinth_url}, + curserinth::{ fetch_curserinth_filename, get_curserinth_url}, github::{download_github_release, fetch_github_release_filename, get_github_release_url}, - jenkins::{download_jenkins, get_jenkins_download_url, get_jenkins_filename}, - modrinth::{download_modrinth, fetch_modrinth_filename, get_modrinth_url}, - spigot::{download_spigot_resource, fetch_spigot_resource_latest_ver, get_spigot_url}, + jenkins::{ get_jenkins_download_url, get_jenkins_filename}, + modrinth::{ fetch_modrinth_filename, get_modrinth_url}, + spigot::{fetch_spigot_resource_latest_ver, get_spigot_url}, }; mod import_url; mod markdown; @@ -56,6 +57,17 @@ pub enum Downloadable { #[serde(default = "first")] artifact: String, }, + + Maven { + url: String, + group: String, + #[serde(default = "first")] + artifact: String, + #[serde(default = "latest")] + version: String, + #[serde(default = "artifact")] + filename: String, + }, } pub fn latest() -> String { @@ -66,13 +78,19 @@ pub fn first() -> String { "first".to_owned() } +pub fn artifact() -> String { + "artifact".to_owned() +} + impl Downloadable { - /// only for exporting uses, every downloadable is supported* hehe :3 -dennis pub async fn get_url( &self, client: &reqwest::Client, + server: &Server, filename_hint: Option<&str>, ) -> Result { + let mcver = &server.mc_version; + match self { Self::Url { url, .. } => Ok(url.clone()), @@ -84,7 +102,7 @@ impl Downloadable { } Self::Spigot { id } => Ok(get_spigot_url(id)), Self::GithubRelease { repo, tag, asset } => { - Ok(get_github_release_url(repo, tag, asset, client, filename_hint).await?) + Ok(get_github_release_url(repo, tag, asset, mcver, client, filename_hint).await?) } Self::Jenkins { @@ -93,6 +111,10 @@ impl Downloadable { build, artifact, } => Ok(get_jenkins_download_url(client, url, job, build, artifact).await?), + + Self::Maven { url, group, artifact, version, filename } => { + Ok(get_maven_url(client, url, group, artifact, version, filename, mcver).await?) + } } } } @@ -101,34 +123,27 @@ impl Downloadable { impl Source for Downloadable { async fn download( &self, - _server: &Server, + server: &Server, client: &reqwest::Client, filename_hint: Option<&str>, ) -> Result { match self { - Self::Url { url, .. } => Ok(client.get(url).send().await?.error_for_status()?), - - Self::Modrinth { id, version } => { - Ok(download_modrinth(id, version, client, None).await?) - } - Self::CurseRinth { id, version } => { - Ok(download_curserinth(id, version, client, None).await?) - } - Self::Spigot { id } => Ok(download_spigot_resource(id, client).await?), Self::GithubRelease { repo, tag, asset } => { - Ok(download_github_release(repo, tag, asset, client, filename_hint).await?) + Ok(download_github_release(repo, tag, asset, &server.mc_version, client, filename_hint).await?) } - Self::Jenkins { - url, - job, - build, - artifact, - } => Ok(download_jenkins(client, url, job, build, artifact).await?), + dl => { + Ok(client.get(dl.get_url(client, server, filename_hint).await?) + .send() + .await? + .error_for_status()?) + } } } - async fn get_filename(&self, _server: &Server, client: &reqwest::Client) -> Result { + async fn get_filename(&self, server: &Server, client: &reqwest::Client) -> Result { + let mcver = &server.mc_version; + match self { Self::Url { url, filename, .. } => { if let Some(filename) = filename { @@ -156,7 +171,7 @@ impl Source for Downloadable { // problematic stuff part 2345 Self::GithubRelease { repo, tag, asset } => { - Ok(fetch_github_release_filename(repo, tag, asset, client).await?) + Ok(fetch_github_release_filename(repo, tag, asset, mcver, client).await?) } Self::Jenkins { @@ -167,6 +182,10 @@ impl Source for Downloadable { } => Ok(get_jenkins_filename(client, url, job, build, artifact) .await? .1), + + Self::Maven { url, group, artifact, version, filename } => { + Ok(maven::get_maven_filename(client, url, group, artifact, version, filename, mcver).await?) + } } } } @@ -209,6 +228,21 @@ impl std::fmt::Display for Downloadable { .field("Build ID", build) .field("Artifact", artifact) .finish(), + + Self::Maven { + url, + group, + artifact, + version, + filename, + } => f + .debug_struct("Maven") + .field("Instance URL", url) + .field("Group", group) + .field("Artifact", artifact) + .field("Version", version) + .field("Filename", filename) + .finish(), } } } diff --git a/src/model/servertype/interactive.rs b/src/model/servertype/interactive.rs index 176d76b..5f08af4 100644 --- a/src/model/servertype/interactive.rs +++ b/src/model/servertype/interactive.rs @@ -8,10 +8,10 @@ use super::ServerType; impl ServerType { pub fn select_jar_interactive() -> Result { let items = vec![ - SelectItem(0, "Vanilla - No patches".to_owned()), - SelectItem(1, "PaperMC/Paper - Spigot fork, most popular".to_owned()), - SelectItem(2, "Purpur - Paper fork".to_owned()), - SelectItem(3, "BuildTools - Spigot or CraftBukkit".to_owned()), + SelectItem(0, "Vanilla - No patches".to_owned()), + SelectItem(1, "PaperMC - Spigot fork, most popular".to_owned()), + SelectItem(2, "Purpur - Paper fork".to_owned()), + SelectItem(3, "BuildTools - Spigot or CraftBukkit".to_owned()), ]; let jar_type = Select::with_theme(&ColorfulTheme::default()) @@ -58,15 +58,16 @@ impl ServerType { pub fn select_modded_jar_interactive() -> Result { let items = [ - (0, "Quilt - Modern, fabric compatible (Beta)"), - (1, "Fabric - Lightweight"), - //(2, "Forge - Ye' olde modde"), + (0, "Quilt (fabric compatible)"), + (1, "Fabric"), + (2, "NeoForged (forge compatible)"), + (3, "Forge"), ]; let items_str: Vec = items.iter().map(|v| v.1.to_owned()).collect(); let jar_type = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which (modded) server software to use?") + .with_prompt("Which mod loader to use?") .default(0) .items(&items_str) .interact()?; @@ -80,7 +81,8 @@ impl ServerType { loader: "latest".to_owned(), installer: "latest".to_owned(), }, - 2 => todo!(), + 2 => Self::NeoForge { loader: "latest".to_owned() }, + 3 => Self::Forge { loader: "latest".to_owned() }, _ => unreachable!(), }) } diff --git a/src/model/servertype/meta.rs b/src/model/servertype/meta.rs index 280272d..67709af 100644 --- a/src/model/servertype/meta.rs +++ b/src/model/servertype/meta.rs @@ -15,6 +15,8 @@ impl ServerType { Self::PaperMC { .. } => "PaperMC".to_owned(), Self::Quilt { .. } => "Quilt".to_owned(), Self::BuildTools { .. } => "BuildTools".to_owned(), + Self::NeoForge { .. } => "NeoForged".to_owned(), + Self::Forge { .. } => "Forge".to_owned(), Self::Downloadable { inner } => inner.get_type_name(), } } @@ -37,6 +39,8 @@ impl ServerType { format!("[PaperMC/{project}](https://github.com/PaperMC/{project}); build {build}") } Self::Quilt { .. } => "[Quilt](https://quiltmc.org/)".to_owned(), + Self::NeoForge { .. } => "[NeoForged](https://neoforged.net/)".to_owned(), + Self::Forge { .. } => "[Forge](https://forums.minecraftforge.net/)".to_owned(), Self::Downloadable { inner } => inner.get_md_link(), } } @@ -59,6 +63,16 @@ impl ServerType { } } + Self::NeoForge { loader } | Self::Forge { loader } => { + map.insert( + "Loader".to_owned(), + match loader.as_str() { + "latest" => "*Latest*".to_owned(), + id => format!("`{id}`"), + }, + ); + } + Self::PaperMC { build, .. } | Self::Purpur { build } => { map.insert( "Build".to_owned(), diff --git a/src/model/servertype/mod.rs b/src/model/servertype/mod.rs index 3cab782..ec38135 100644 --- a/src/model/servertype/mod.rs +++ b/src/model/servertype/mod.rs @@ -6,12 +6,14 @@ use crate::Source; use crate::model::Downloadable; use crate::sources::{ - fabric::{download_fabric, fetch_fabric_latest_installer, fetch_fabric_latest_loader}, - jenkins::{download_jenkins, get_jenkins_filename}, - papermc::{download_papermc_build, fetch_papermc_build}, - purpur::{download_purpurmc_build, fetch_purpurmc_builds}, - quilt::{download_quilt_installer, get_installer_filename, map_quilt_loader_version}, - vanilla::fetch_vanilla, + fabric, + jenkins, + papermc, + purpur, + quilt, + vanilla, + neoforge, + forge, }; use super::{Server, StartupMethod}; @@ -19,6 +21,7 @@ use super::{Server, StartupMethod}; pub mod interactive; pub mod meta; +#[derive(Debug, PartialEq)] pub enum SoftwareType { Normal, Modded, @@ -66,6 +69,17 @@ pub enum ServerType { installer: String, }, + #[serde(alias = "neoforged")] + NeoForge { + #[serde(default = "latest")] + loader: String, + }, + + Forge { + #[serde(default = "latest")] + loader: String, + }, + BuildTools { #[serde(default = "spigot")] software: String, @@ -139,7 +153,10 @@ impl ServerType { Self::PaperMC { project, .. } if project == "velocity" || project == "waterfall" => { SoftwareType::Proxy } - Self::Quilt { .. } | Self::Fabric { .. } => SoftwareType::Modded, + Self::Quilt { .. } + | Self::Fabric { .. } + | Self::NeoForge { .. } + | Self::Forge { .. } => SoftwareType::Modded, Self::Vanilla {} | Self::Paper {} | Self::PaperMC { .. } @@ -149,7 +166,11 @@ impl ServerType { } } - pub async fn get_install_method(&self, http_client: &reqwest::Client) -> Result { + pub async fn get_install_method( + &self, + http_client: &reqwest::Client, + mcver: &str, + ) -> Result { Ok(match self.clone() { Self::Quilt { loader, .. } => { let mut args = vec!["install", "server", "${mcver}"]; @@ -168,12 +189,32 @@ impl ServerType { rename_from: Some("quilt-server-launch.jar".to_owned()), jar_name: format!( "quilt-server-launch-${{mcver}}-{}.jar", - map_quilt_loader_version(http_client, &loader) + quilt::map_quilt_loader_version(http_client, &loader) .await .context("resolving quilt loader version id (latest/latest-beta)")? ), } } + Self::NeoForge { loader } => InstallMethod::Installer { + name: "NeoForged Installer".to_owned(), + label: "nfi".to_owned(), + args: vec!["--installServer".to_owned(), ".".to_owned()], + rename_from: None, + jar_name: format!( + "libraries/net/neoforged/forge/${{mcver}}-{0}/forge-${{mcver}}-{0}-server.jar", + neoforge::map_neoforge_version(&loader, mcver, http_client).await? + ) + }, + Self::Forge { loader } => InstallMethod::Installer { + name: "Forge Installer".to_owned(), + label: "fi".to_owned(), + args: vec!["--installServer".to_owned(), ".".to_owned()], + rename_from: None, + jar_name: format!( + "libraries/net/minecraftforge/forge/${{mcver}}-{0}/forge-${{mcver}}-{0}-server.jar", + forge::map_forge_version(&loader, mcver, http_client).await? + ) + }, Self::BuildTools { args, software } => { let mut buildtools_args = vec![ "--compile", @@ -206,17 +247,43 @@ impl ServerType { }) } - pub fn get_startup_method(&self, serverjar_name: &str) -> StartupMethod { - #[allow(clippy::match_single_binding)] - match self { - //Self::Forge { .. } => StartupMethod::Custom(vec![""]), - //Self::NeoForge { .. } => StartupMethod::Custom(vec!["@libraries/net/neoforged/forge/1.20.1-47.1.57/win_args.txt"]), + pub async fn get_startup_method( + &self, + http_client: &reqwest::Client, + serverjar_name: &str, + mcver: &str, + ) -> Result { + Ok(match self { + Self::NeoForge { loader } => { + let l = neoforge::map_neoforge_version(loader, mcver, http_client).await?; + + StartupMethod::Custom { + windows: vec![format!( + "@libraries/net/neoforged/forge/{mcver}-{l}/win_args.txt" + )], + linux: vec![format!( + "@libraries/net/neoforged/forge/{mcver}-{l}/unix_args.txt" + )], + } + } + Self::Forge { loader } => { + let l = forge::map_forge_version(loader, mcver, http_client).await?; + + StartupMethod::Custom { + windows: vec![format!( + "@libraries/net/minecraftforge/forge/{mcver}-{l}/win_args.txt" + )], + linux: vec![format!( + "@libraries/net/minecraftforge/forge/{mcver}-{l}/unix_args.txt" + )], + } + } _ => StartupMethod::Jar(serverjar_name.to_owned()), - } + }) } pub fn is_modded(&self) -> bool { - matches!(self, Self::Fabric { .. } | Self::Quilt { .. }) + self.get_software_type() == SoftwareType::Modded } pub fn supports_eula_args(&self) -> bool { @@ -234,43 +301,47 @@ impl Source for ServerType { ) -> Result { let mcver = server.mc_version.clone(); match self { - Self::Vanilla {} => Ok(fetch_vanilla(&mcver, client).await?), + Self::Vanilla {} => Ok(vanilla::fetch_vanilla(&mcver, client).await?), Self::PaperMC { project, build } => { - Ok(download_papermc_build(project, &mcver, build, client).await?) + Ok(papermc::download_papermc_build(project, &mcver, build, client).await?) + } + Self::Purpur { build } => { + Ok(purpur::download_purpurmc_build(&mcver, build, client).await?) + } + + Self::Paper {} => { + Ok(papermc::download_papermc_build("paper", &mcver, "latest", client).await?) } - Self::Purpur { build } => Ok(download_purpurmc_build(&mcver, build, client).await?), - - Self::BungeeCord {} => Ok(download_jenkins( - client, - BUNGEECORD_JENKINS, - BUNGEECORD_JOB, - "latest", - BUNGEECORD_ARTIFACT, - ) - .await?), - - Self::BuildTools { .. } => Ok(download_jenkins( - client, - BUILDTOOLS_JENKINS, - "BuildTools", - "latest", - "BuildTools", - ) - .await?), - - Self::Paper {} => Ok(download_papermc_build("paper", &mcver, "latest", client).await?), Self::Velocity {} => { - Ok(download_papermc_build("velocity", &mcver, "latest", client).await?) + Ok(papermc::download_papermc_build("velocity", &mcver, "latest", client).await?) } Self::Waterfall {} => { - Ok(download_papermc_build("waterfall", &mcver, "latest", client).await?) + Ok(papermc::download_papermc_build("waterfall", &mcver, "latest", client).await?) } Self::Fabric { loader, installer } => { - Ok(download_fabric(client, &mcver, loader, installer).await?) + Ok(fabric::download_fabric(client, &mcver, loader, installer).await?) } - Self::Quilt { installer, .. } => Ok(download_quilt_installer(client, installer).await?), + Self::Quilt { installer, .. } => { + Ok(quilt::download_quilt_installer(client, installer).await?) + } + + Self::BungeeCord {} => Ok(bungeecord().download(server, client, filename_hint).await?), + Self::BuildTools { .. } => { + Ok(buildtools().download(server, client, filename_hint).await?) + } + Self::NeoForge { loader } => Ok(client + .get(neoforge::get_neoforge_installer_url(loader, &mcver, client).await?) + .send() + .await? + .error_for_status()?), + + Self::Forge { loader } => Ok(client + .get(forge::get_forge_installer_url(loader, &mcver, client).await?) + .send() + .await? + .error_for_status()?), Self::Downloadable { inner } => inner.download(server, client, filename_hint).await, } @@ -283,9 +354,10 @@ impl Source for ServerType { Self::PaperMC { project, build } => { Ok(get_filename_papermc(project, &mcver, build, client).await?) } + Self::Purpur { build } => { if build == "latest" { - let last_build = fetch_purpurmc_builds(&mcver, client) + let last_build = purpur::fetch_purpurmc_builds(&mcver, client) .await? .last() .cloned() @@ -297,7 +369,7 @@ impl Source for ServerType { } Self::BungeeCord {} => { - let build = get_jenkins_filename( + let build = jenkins::get_jenkins_filename( client, BUNGEECORD_JENKINS, BUNGEECORD_JOB, @@ -310,7 +382,7 @@ impl Source for ServerType { } Self::BuildTools { .. } => { - let build = get_jenkins_filename( + let build = jenkins::get_jenkins_filename( client, BUILDTOOLS_JENKINS, "BuildTools", @@ -326,27 +398,36 @@ impl Source for ServerType { Self::Velocity {} => { Ok(get_filename_papermc("velocity", &mcver, "latest", client).await?) } + Self::Waterfall {} => { Ok(get_filename_papermc("waterfall", &mcver, "latest", client).await?) } Self::Fabric { loader, installer } => { let l = match loader.as_str() { - "latest" => fetch_fabric_latest_loader(client).await?, + "latest" => fabric::fetch_fabric_latest_loader(client).await?, id => id.to_owned(), }; let i = match installer.as_str() { - "latest" => fetch_fabric_latest_installer(client).await?, + "latest" => fabric::fetch_fabric_latest_installer(client).await?, id => id.to_owned(), }; - Ok(format!( - "fabric-server-mc.{mcver}-loader.{l}-launcher.{i}.jar" - )) + Ok(format!("fabric-server-{mcver}-{l}-{i}.jar")) } - Self::Quilt { installer, .. } => Ok(get_installer_filename(client, installer).await?), + Self::Quilt { installer, .. } => { + Ok(quilt::get_installer_filename(client, installer).await?) + } + + Self::NeoForge { loader } => { + Ok(neoforge::get_neoforge_installer_filename(loader, &mcver, client).await?) + } + + Self::Forge { loader } => { + Ok(forge::get_forge_installer_filename(loader, &mcver, client).await?) + } Self::Downloadable { inner } => inner.get_filename(server, client).await, } @@ -370,6 +451,12 @@ impl std::fmt::Display for ServerType { .field("installer", installer) .finish(), + Self::NeoForge { loader } => { + f.debug_struct("NeoForged").field("loader", loader).finish() + } + + Self::Forge { loader } => f.debug_struct("Forge").field("loader", loader).finish(), + Self::BungeeCord {} => f.write_str("BungeeCord"), Self::BuildTools { .. } => f.write_str("BuildTools"), Self::Paper {} => f.write_str("Paper, latest"), @@ -391,7 +478,7 @@ async fn get_filename_papermc( client: &reqwest::Client, ) -> Result { if build == "latest" { - let build_id = fetch_papermc_build(project, mcver, build, client) + let build_id = papermc::fetch_papermc_build(project, mcver, build, client) .await? .build .to_string(); @@ -400,3 +487,21 @@ async fn get_filename_papermc( Ok(format!("{project}-{mcver}-{build}.jar")) } } + +pub fn bungeecord() -> Downloadable { + Downloadable::Jenkins { + url: BUNGEECORD_JENKINS.to_owned(), + job: BUNGEECORD_JOB.to_owned(), + build: "latest".to_owned(), + artifact: BUNGEECORD_ARTIFACT.to_owned(), + } +} + +pub fn buildtools() -> Downloadable { + Downloadable::Jenkins { + url: BUILDTOOLS_JENKINS.to_owned(), + job: "BuildTools".to_owned(), + build: "latest".to_owned(), + artifact: "BuildTools".to_owned(), + } +} diff --git a/src/sources/curserinth.rs b/src/sources/curserinth.rs index 744326e..496e67d 100644 --- a/src/sources/curserinth.rs +++ b/src/sources/curserinth.rs @@ -129,16 +129,3 @@ pub async fn get_curserinth_url( Ok(file.url.clone()) } - -pub async fn download_curserinth( - id: &str, - version: &str, - client: &reqwest::Client, - query: Option<(&str, &str)>, -) -> Result { - let url = get_curserinth_url(id, version, client, query).await?; - - let res = client.get(&url).send().await?.error_for_status()?; - - Ok(res) -} diff --git a/src/sources/forge.rs b/src/sources/forge.rs new file mode 100644 index 0000000..5000501 --- /dev/null +++ b/src/sources/forge.rs @@ -0,0 +1,88 @@ +use anyhow::{anyhow, Result, Context}; + +use crate::util; + +use super::maven; + +pub static FORGE_MAVEN: &str = "https://maven.minecraftforge.net"; +pub static FORGE_GROUP: &str = "net.minecraftforge"; +pub static FORGE_ARTIFACT: &str = "forge"; +pub static FORGE_FILENAME: &str = "${artifact}-${version}-installer.jar"; + +pub async fn get_versions_for(mcver: &str, client: &reqwest::Client) -> Result> { + let (_, versions) = + maven::get_maven_versions(client, FORGE_MAVEN, FORGE_GROUP, FORGE_ARTIFACT).await?; + + Ok(versions + .iter() + .filter_map(|s| { + let (m, l) = s.split_once('-')?; + + if m == mcver { + Some(l.to_owned()) + } else { + None + } + }) + .collect()) +} + +pub async fn get_latest_version_for(mcver: &str, client: &reqwest::Client) -> Result { + let loader_versions = get_versions_for(mcver, client).await?; + + util::get_latest_semver(loader_versions).ok_or(anyhow!("No loader versions for {mcver}")) +} + +pub async fn map_forge_version( + loader: &str, + mcver: &str, + client: &reqwest::Client, +) -> Result { + Ok(if loader == "latest" || loader.is_empty() { + get_latest_version_for(mcver, client) + .await + .context("Getting latest Forge version")? + } else { + loader.to_owned() + }) +} + +pub async fn get_forge_installer_url( + loader: &str, + mcver: &str, + client: &reqwest::Client, +) -> Result { + maven::get_maven_url( + client, + FORGE_MAVEN, + FORGE_GROUP, + FORGE_ARTIFACT, + &format!( + "{mcver}-{}", + map_forge_version(loader, mcver, client).await? + ), + FORGE_FILENAME, + mcver, + ) + .await +} + +pub async fn get_forge_installer_filename( + loader: &str, + mcver: &str, + client: &reqwest::Client, +) -> Result { + maven::get_maven_filename( + client, + FORGE_MAVEN, + FORGE_GROUP, + FORGE_ARTIFACT, + &format!( + "{mcver}-{}", + map_forge_version(loader, mcver, client).await? + ), + FORGE_FILENAME, + mcver, + ) + .await +} diff --git a/src/sources/github.rs b/src/sources/github.rs index 270de3f..41eb51b 100644 --- a/src/sources/github.rs +++ b/src/sources/github.rs @@ -4,8 +4,6 @@ use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use tokio::time::sleep; -use crate::util::match_artifact_name; - async fn wait_ratelimit(res: reqwest::Response) -> Result { let res = if let Some(h) = res.headers().get("x-ratelimit-remaining") { if String::from_utf8_lossy(h.as_bytes()) == "1" { @@ -25,14 +23,14 @@ async fn wait_ratelimit(res: reqwest::Response) -> Result { Ok(res) } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct GithubRelease { pub tag_name: String, pub name: String, pub assets: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct GithubAsset { pub url: String, pub name: String, @@ -59,35 +57,55 @@ pub async fn fetch_github_release_asset( repo: &str, tag: &str, asset: &str, + mcver: &str, client: &reqwest::Client, ) -> Result { let releases = fetch_github_releases(repo, client).await?; - let release = match tag { - "latest" => releases.into_iter().next(), - id => releases.into_iter().find(|r| r.tag_name == id), + let tag = tag.replace("${mcver}", mcver); + let tag = tag.replace("${mcversion}", mcver); + + let release = match tag.as_str() { + "latest" => releases.first(), + id => releases.iter().find(|r| r.tag_name == id), } - .ok_or(anyhow!("release not found"))?; + .ok_or(anyhow!("Github release with tag '{tag}' not found on repository '{repo}'"))?; + + let assets = &release.assets; let resolved_asset = match asset { - "" | "first" | "any" => release.assets.into_iter().next(), - id => release - .assets - .into_iter() - .find(|a| match_artifact_name(id, &a.name)), + "" | "first" | "any" => assets.first(), + id => { + let id = if id.contains('$') { + id.replace("${version}", &release.tag_name) + .replace("${tag}", &release.tag_name) + .replace("${release}", &release.tag_name) + .replace("${mcver}", mcver) + .replace("${mcversion}", mcver) + } else { + id.to_owned() + }; + + assets.iter().find(|a| { + id == a.name + }).or(assets.iter().find(|a| { + a.name.contains(&id) + })) + } } - .ok_or(anyhow!("asset not found"))?; + .ok_or(anyhow!("Github release asset with name '{asset}' on release '{}' not found", release.tag_name))?; - Ok(resolved_asset) + Ok(resolved_asset.to_owned()) } pub async fn fetch_github_release_filename( repo: &str, tag: &str, asset: &str, + mcver: &str, client: &reqwest::Client, ) -> Result { - Ok(fetch_github_release_asset(repo, tag, asset, client) + Ok(fetch_github_release_asset(repo, tag, asset, mcver, client) .await? .name) } @@ -96,13 +114,14 @@ pub async fn get_github_release_url( repo: &str, tag: &str, asset: &str, + mcver: &str, client: &reqwest::Client, filename_hint: Option<&str>, ) -> Result { let filename = if let Some(filename) = filename_hint { filename.to_owned() } else { - let fetched_asset = fetch_github_release_asset(repo, tag, asset, client).await?; + let fetched_asset = fetch_github_release_asset(repo, tag, asset, mcver, client).await?; fetched_asset.name }; @@ -115,13 +134,14 @@ pub async fn download_github_release( repo: &str, tag: &str, asset: &str, + mcver: &str, client: &reqwest::Client, filename_hint: Option<&str>, ) -> Result { let filename = if let Some(filename) = filename_hint { filename.to_owned() } else { - let fetched_asset = fetch_github_release_asset(repo, tag, asset, client).await?; + let fetched_asset = fetch_github_release_asset(repo, tag, asset, mcver, client).await?; fetched_asset.name }; diff --git a/src/sources/jenkins.rs b/src/sources/jenkins.rs index 32b7f73..1a0684b 100644 --- a/src/sources/jenkins.rs +++ b/src/sources/jenkins.rs @@ -111,21 +111,6 @@ pub async fn get_jenkins_download_url( Ok(build_url + "artifact/" + &relative_path) } -pub async fn download_jenkins( - client: &reqwest::Client, - url: &str, - job: &str, - build: &str, - artifact: &str, -) -> Result { - let (build_url, _, relative_path, _build_number) = - get_jenkins_filename(client, url, job, build, artifact).await?; - - let download_url = build_url + "artifact/" + &relative_path; - - Ok(client.get(download_url).send().await?.error_for_status()?) -} - pub async fn fetch_jenkins_description( client: &reqwest::Client, url: &str, diff --git a/src/sources/maven.rs b/src/sources/maven.rs new file mode 100644 index 0000000..3b36b8c --- /dev/null +++ b/src/sources/maven.rs @@ -0,0 +1,122 @@ +use anyhow::{Result, anyhow}; + +pub fn get_metadata_url(url: &str, group_id: &str, artifact_id: &str) -> String { + format!( + "{url}/{}/{artifact_id}/maven-metadata.xml", + group_id.replace('.', "/") + ) +} + +pub async fn get_maven_versions( + client: &reqwest::Client, + url: &str, + group_id: &str, + artifact_id: &str, +) -> Result<(String, Vec)> { + let xml = client + .get(get_metadata_url(url, group_id, artifact_id)) + .send() + .await? + .text() + .await?; + + let doc = roxmltree::Document::parse(&xml)?; + + let latest = doc.descendants().find_map(|t| { + if t.tag_name().name() == "latest" { + Some(t.text()?.to_owned()) + } else { + None + } + }); + + let list = doc + .descendants() + .filter_map(|t| { + if t.tag_name().name() == "version" { + Some(t.text()?.to_owned()) + } else { + None + } + }) + .collect::>(); + + Ok(( + latest.unwrap_or_else(|| list.first().cloned().unwrap_or_default()), + list, + )) +} + +pub async fn get_maven_url( + client: &reqwest::Client, + url: &str, + group_id: &str, + artifact_id: &str, + version: &str, + file: &str, + mcver: &str, +) -> Result { + let version = match_maven_version(client, url, group_id, artifact_id, version, mcver).await?; + + Ok(format!( + "{url}/{}/{artifact_id}/{version}/{}", + group_id.replace('.', "/"), + get_maven_filename(client, url, group_id, artifact_id, &version, file, mcver).await? + )) +} + +pub async fn get_maven_filename( + client: &reqwest::Client, + url: &str, + group_id: &str, + artifact_id: &str, + version: &str, + file: &str, + mcver: &str, +) -> Result { + let version = match_maven_version(client, url, group_id, artifact_id, version, mcver).await?; + + let file = file.replace("${artifact}", artifact_id) + .replace("${version}", &version) + .replace("${mcversion}", mcver) + .replace("${mcver}", mcver); + + Ok(if file.contains('.') { + file + } else { + file + ".jar" + }) +} + +pub async fn match_maven_version( + client: &reqwest::Client, + url: &str, + group_id: &str, + artifact_id: &str, + version: &str, + mcver: &str, +) -> Result { + let fetch_versions = || get_maven_versions(client, url, group_id, artifact_id); + + let version = match version { + "latest" => fetch_versions().await?.0, + id => if id.contains("$") { + let versions = fetch_versions().await?.1; + let id = id.replace("${artifact}", artifact_id) + .replace("${mcversion}", mcver) + .replace("${mcver}", mcver); + versions.iter().find(|v| { + v.to_owned() == &id + }).or_else(|| { + versions.iter().find(|v| { + v.contains(&id) + }) + }).ok_or(anyhow!("Couldn't resolve maven artifact version"))? + .to_owned() + } else { + id.to_owned() + }, + }; + + Ok(version) +} diff --git a/src/sources/mod.rs b/src/sources/mod.rs index 124abca..f41f819 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -8,3 +8,6 @@ pub mod purpur; pub mod quilt; pub mod spigot; pub mod vanilla; +pub mod maven; +pub mod neoforge; +pub mod forge; diff --git a/src/sources/modrinth.rs b/src/sources/modrinth.rs index 4ff29d8..ca6f209 100644 --- a/src/sources/modrinth.rs +++ b/src/sources/modrinth.rs @@ -174,15 +174,3 @@ pub async fn get_modrinth_url( // TODO: more complex version matching ie. mc version and server software // TODO: also impl modrinth in mcapi and use that instead -pub async fn download_modrinth( - id: &str, - version: &str, - client: &reqwest::Client, - query: Option<(&str, &str)>, -) -> Result { - let url = get_modrinth_url(id, version, client, query).await?; - - let res = client.get(&url).send().await?.error_for_status()?; - - Ok(res) -} diff --git a/src/sources/neoforge.rs b/src/sources/neoforge.rs new file mode 100644 index 0000000..280ef39 --- /dev/null +++ b/src/sources/neoforge.rs @@ -0,0 +1,91 @@ +use anyhow::{anyhow, Context, Result}; + +use crate::util; + +use super::maven; + +pub static NEOFORGE_MAVEN: &str = "https://maven.neoforged.net/releases"; +pub static NEOFORGE_GROUP: &str = "net.neoforged"; +pub static NEOFORGE_ARTIFACT: &str = "forge"; +pub static NEOFORGE_FILENAME: &str = "${artifact}-${version}-installer.jar"; + +pub async fn get_versions_for(mcver: &str, client: &reqwest::Client) -> Result> { + let (_, versions) = + maven::get_maven_versions(client, NEOFORGE_MAVEN, NEOFORGE_GROUP, NEOFORGE_ARTIFACT) + .await?; + + Ok(versions + .iter() + .filter_map(|s| { + let (m, l) = s.split_once('-')?; + + if m == mcver { + Some(l.to_owned()) + } else { + None + } + }) + .collect()) +} + +pub async fn get_latest_version_for(mcver: &str, client: &reqwest::Client) -> Result { + let loader_versions = get_versions_for(mcver, client) + .await + .context("Fetching NeoForge versions")?; + + util::get_latest_semver(loader_versions).ok_or(anyhow!("No loader versions for {mcver}")) +} + +pub async fn map_neoforge_version( + loader: &str, + mcver: &str, + client: &reqwest::Client, +) -> Result { + Ok(if loader == "latest" || loader.is_empty() { + get_latest_version_for(mcver, client) + .await + .context("Getting latest NeoForge version")? + } else { + loader.to_owned() + }) +} + +pub async fn get_neoforge_installer_url( + loader: &str, + mcver: &str, + client: &reqwest::Client, +) -> Result { + maven::get_maven_url( + client, + NEOFORGE_MAVEN, + NEOFORGE_GROUP, + NEOFORGE_ARTIFACT, + &format!( + "{mcver}-{}", + map_neoforge_version(loader, mcver, client).await? + ), + NEOFORGE_FILENAME, + mcver, + ) + .await +} + +pub async fn get_neoforge_installer_filename( + loader: &str, + mcver: &str, + client: &reqwest::Client, +) -> Result { + maven::get_maven_filename( + client, + NEOFORGE_MAVEN, + NEOFORGE_GROUP, + NEOFORGE_ARTIFACT, + &format!( + "{mcver}-{}", + map_neoforge_version(loader, mcver, client).await? + ), + NEOFORGE_FILENAME, + mcver, + ) + .await +} diff --git a/src/sources/spigot.rs b/src/sources/spigot.rs index 1774fa0..bf1b248 100644 --- a/src/sources/spigot.rs +++ b/src/sources/spigot.rs @@ -55,17 +55,3 @@ pub fn get_spigot_url(id: &str) -> String { let id_parsed = get_resource_id(id); format!("https://api.spiget.org/v2/resources/{id_parsed}/download") } - -pub async fn download_spigot_resource( - id: &str, - client: &reqwest::Client, -) -> Result { - //let version = fetch_spigot_resource_latest_ver(id, client).await.context("fetching latest version")?; - //let id_parsed = get_resource_id(id); - - Ok(client - .get(get_spigot_url(id)) - .send() - .await? - .error_for_status()?) -} diff --git a/src/util/maven_import.rs b/src/util/maven_import.rs new file mode 100644 index 0000000..9b5554d --- /dev/null +++ b/src/util/maven_import.rs @@ -0,0 +1,114 @@ +use anyhow::{Result, anyhow, bail}; + +use crate::model::Downloadable; + + + +/// Example: +/// ```xml +/// +/// net.neoforged +/// forge +/// 1.20.1-47.1.62 +/// +/// ``` +#[allow(unused)] +pub fn import_from_maven_dependency_xml( + url: &str, + xml: &str +) -> Result { + let xml = roxmltree::Document::parse(xml)?; + + let group = xml.descendants().find(|t| { + t.tag_name().name() == "groupId" + }).ok_or(anyhow!("dependency.groupId must be present"))? + .text() + .ok_or(anyhow!("dependency.groupId must be text"))? + .to_owned(); + + let artifact = xml.descendants().find(|t| { + t.tag_name().name() == "artifactId" + }).ok_or(anyhow!("dependency.artifactId must be present"))? + .text() + .ok_or(anyhow!("dependency.artifactId must be text"))? + .to_owned(); + + let version = xml.descendants().find(|t| { + t.tag_name().name() == "version" + }).ok_or(anyhow!("dependency.version must be present"))? + .text() + .ok_or(anyhow!("dependency.version must be text"))? + .to_owned(); + + Ok(Downloadable::Maven { + url: url.to_owned(), + artifact, + group, + version, + filename: "${artifact}-${version}".to_owned(), + }) +} + +/// Gradle Kotlin: +/// ``` +/// implementation("net.neoforged:forge:1.20.1-47.1.62") +/// ``` +/// +/// Gradle Groovy: +/// +/// ``` +/// implementation "net.neoforged:forge:1.20.1-47.1.62" +/// ``` +#[allow(unused)] +pub fn import_from_gradle_dependency( + url: &str, + imp: &str +) -> Result { + let imp = imp.replace("implementation", ""); + let imp = imp.replace(&[' ', '(', ')', '"'], ""); + let li = imp + .trim() + .split(':') + .collect::>(); + + if li.len() != 3 { + bail!("Gradle dependency should have 3 sections delimeted by ':' inside the quoted string"); + } + + Ok(Downloadable::Maven { + url: url.to_owned(), + group: li[0].to_owned(), + artifact: li[1].to_owned(), + version: li[2].to_owned(), + filename: "${artifact}-${version}".to_owned(), + }) +} + +/// Example: +/// ``` +/// "net.neoforged" %% "forge" %% "1.20.1-47.1.62" +/// ``` +#[allow(unused)] +pub fn import_from_sbt( + url: &str, + sbt: &str, +) -> Result { + let sbt = sbt.replace(char::is_whitespace, ""); + let sbt = sbt.replace('"', ""); + let li = sbt + .split("%") + .filter(|x| !x.is_empty()) + .collect::>(); + + if li.len() != 3 { + bail!("SBT should have 3 sections delimeted by '%' or '%%'"); + } + + Ok(Downloadable::Maven { + url: url.to_owned(), + group: li[0].to_owned(), + artifact: li[1].to_owned(), + version: li[2].to_owned(), + filename: "${artifact}-${version}".to_owned(), + }) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index c36d35e..0b4f561 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,5 +1,6 @@ pub mod env; pub mod hash; +pub mod maven_import; pub mod md; pub mod mrpack; pub mod packwiz; @@ -73,3 +74,14 @@ pub fn is_default_str(s: &str) -> bool { pub fn match_artifact_name(input: &str, artifact_name: &str) -> bool { artifact_name.contains(input) } + +pub fn get_latest_semver(list: Vec) -> Option { + let mut list = list + .iter() + .filter_map(|s| semver::Version::parse(s).ok()) + .collect::>(); + + list.sort_by(semver::Version::cmp); + + list.last().map(|v| v.to_string()) +} diff --git a/src/util/mrpack.rs b/src/util/mrpack.rs index 783fb76..c621635 100644 --- a/src/util/mrpack.rs +++ b/src/util/mrpack.rs @@ -425,7 +425,7 @@ pub async fn export_mrpack( } dl => { let filename = dl.get_filename(server, http_client).await?; - if let Ok(url) = dl.get_url(http_client, Some(&filename)).await { + if let Ok(url) = dl.get_url(http_client, server, Some(&filename)).await { files.push(MRPackFile { hashes: HashMap::new(), // ! todo...??? env: None, @@ -536,7 +536,7 @@ pub async fn export_mrpack( } dl => { let filename = dl.get_filename(server, http_client).await?; - if let Ok(url) = dl.get_url(http_client, Some(&filename)).await { + if let Ok(url) = dl.get_url(http_client, server, Some(&filename)).await { files.push(MRPackFile { hashes: HashMap::new(), // ! todo...??? env: None,