diff --git a/docs/dev-tools/backends/spm.md b/docs/dev-tools/backends/spm.md
index 15f56a3888..b286ac883d 100644
--- a/docs/dev-tools/backends/spm.md
+++ b/docs/dev-tools/backends/spm.md
@@ -1,6 +1,6 @@
# SPM Backend
-You may install executables managed by [Swift Package Manager](https://www.swift.org/documentation/package-manager) directly from GitHub releases.
+You may install executables managed by [Swift Package Manager](https://www.swift.org/documentation/package-manager) directly from GitHub or GitLab releases.
The code for this is inside of the mise repository at [`./src/backend/spm.rs`](https://github.com/jdx/mise/blob/main/src/backend/spm.rs).
@@ -42,3 +42,28 @@ The version will be set in `~/.config/mise/config.toml` with the following forma
| GitHub url for specific release version | `spm:https://github.com/tuist/tuist.git@4.15.0` |
Other syntax may work but is unsupported and untested.
+
+## Tool Options
+
+The following [tool-options](/dev-tools/#tool-options) are available for the backend — these
+go in `[tools]` in `mise.toml`.
+
+### `provider`
+
+Set the provider type to use for fetching assets and release information. Either `github` or `gitlab` (default is `github`).
+Ensure the `provider` is set to the correct type if you use shorthand notation and `api_url` for self-hosted repositories
+as the type probably cannot be derived correctly from the URL.
+
+```toml
+[tools]
+"spm:patricklorran/ios-settings" = { version = "latest", provider = "gitlab" }
+```
+
+### `api_url`
+
+Set the URL for the provider's API. This is useful when using a self-hosted instance.
+
+```toml
+[tools]
+"spm:acme/my-tool" = { version = "latest", provider = "gitlab", api_url = "https://gitlab.acme.com/api/v4" }
+```
diff --git a/src/backend/spm.rs b/src/backend/spm.rs
index b9ec9c2a86..4e3b5b8a66 100644
--- a/src/backend/spm.rs
+++ b/src/backend/spm.rs
@@ -6,7 +6,7 @@ use crate::config::{Config, Settings};
use crate::git::{CloneOptions, Git};
use crate::install_context::InstallContext;
use crate::toolset::ToolVersion;
-use crate::{dirs, file, github};
+use crate::{dirs, file, github, gitlab};
use async_trait::async_trait;
use eyre::WrapErr;
use serde::Deserializer;
@@ -17,6 +17,7 @@ use std::{
fmt::{self, Debug},
sync::Arc,
};
+use strum::{AsRefStr, EnumString, VariantNames};
use url::Url;
use xx::regex;
@@ -40,13 +41,26 @@ impl Backend for SPMBackend {
}
async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> {
- let repo = SwiftPackageRepo::new(&self.tool_name())?;
- Ok(github::list_releases(repo.shorthand.as_str())
- .await?
- .into_iter()
- .map(|r| r.tag_name)
- .rev()
- .collect())
+ let provider = GitProvider::from_ba(&self.ba);
+ let repo = SwiftPackageRepo::new(&self.tool_name(), &provider)?;
+ let releases = match provider.kind {
+ GitProviderKind::GitLab => {
+ gitlab::list_releases_from_url(&provider.api_url, repo.shorthand.as_str())
+ .await?
+ .into_iter()
+ .map(|r| r.tag_name)
+ .rev()
+ .collect()
+ }
+ _ => github::list_releases_from_url(&provider.api_url, repo.shorthand.as_str())
+ .await?
+ .into_iter()
+ .map(|r| r.tag_name)
+ .rev()
+ .collect(),
+ };
+
+ Ok(releases)
}
async fn install_version_(
@@ -66,8 +80,8 @@ impl Backend for SPMBackend {
Or install Swift via https://swift.org/download/",
)
.await;
-
- let repo = SwiftPackageRepo::new(&self.tool_name())?;
+ let provider = GitProvider::from_ba(&self.ba);
+ let repo = SwiftPackageRepo::new(&self.tool_name(), &provider)?;
let revision = if tv.version == "latest" {
self.latest_stable_version(&ctx.config)
.await?
@@ -208,6 +222,56 @@ impl SPMBackend {
}
}
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct GitProvider {
+ pub api_url: String,
+ pub kind: GitProviderKind,
+}
+
+impl Default for GitProvider {
+ fn default() -> Self {
+ Self {
+ api_url: github::API_URL.to_string(),
+ kind: GitProviderKind::GitHub,
+ }
+ }
+}
+
+#[derive(AsRefStr, Clone, Debug, Eq, PartialEq, EnumString, VariantNames)]
+pub enum GitProviderKind {
+ #[strum(serialize = "github")]
+ GitHub,
+ #[strum(serialize = "gitlab")]
+ GitLab,
+}
+
+impl GitProvider {
+ fn from_ba(ba: &BackendArg) -> Self {
+ let opts = ba.opts();
+
+ let default_provider = GitProviderKind::GitHub.as_ref().to_string();
+ let provider = opts.get("provider").unwrap_or(&default_provider);
+ let kind = if ba.tool_name.contains("gitlab.com") {
+ GitProviderKind::GitLab
+ } else {
+ match provider.to_lowercase().as_str() {
+ "gitlab" => GitProviderKind::GitLab,
+ _ => GitProviderKind::GitHub,
+ }
+ };
+
+ let api_url = match opts.get("api_url") {
+ Some(api_url) => api_url.trim_end_matches('/').to_string(),
+ None => match kind {
+ GitProviderKind::GitHub => github::API_URL.to_string(),
+ GitProviderKind::GitLab => gitlab::API_URL.to_string(),
+ },
+ };
+
+ Self { api_url, kind }
+ }
+}
+
#[derive(Debug)]
struct SwiftPackageRepo {
/// https://github.com/owner/repo.git
@@ -218,26 +282,31 @@ struct SwiftPackageRepo {
impl SwiftPackageRepo {
/// Parse the slug or the full URL of a GitHub package repository.
- fn new(name: &str) -> Result {
+ fn new(name: &str, provider: &GitProvider) -> Result {
let name = name.strip_prefix("spm:").unwrap_or(name);
- let shorthand_regex = regex!(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+$");
- let shorthand_in_url_regex =
- regex!(r"https://github.com/([a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+)\.git");
-
- let shorthand = if let Some(Some(m)) =
- shorthand_in_url_regex.captures(name).map(|c| c.get(1))
- {
- m.as_str()
+ let shorthand_regex = regex!(r"^(?:[a-zA-Z0-9_-]+/)+[a-zA-Z0-9._-]+$");
+ let shorthand_in_url_regex = regex!(
+ r"^https://(?P[^/]+)/(?P(?:[a-zA-Z0-9_-]+/)+[a-zA-Z0-9._-]+)\.git"
+ );
+
+ let (shorthand, url) = if let Some(caps) = shorthand_in_url_regex.captures(name) {
+ let shorthand = caps.name("shorthand").unwrap().as_str();
+ let url = Url::parse(name)?;
+ (shorthand, url)
} else if shorthand_regex.is_match(name) {
- name
+ let host = match provider.kind {
+ GitProviderKind::GitHub => "github.com",
+ GitProviderKind::GitLab => "gitlab.com",
+ };
+ let url_str = format!("https://{}/{}.git", host, name);
+ let url = Url::parse(&url_str)?;
+ (name, url)
} else {
Err(eyre::eyre!(
- "Invalid Swift package repository: {}. The repository should either be a GitHub repository slug, owner/name, or the complete URL, https://github.com/owner/name.",
+ "Invalid Swift package repository: {}. The repository should either be a repository slug (owner/name), or the complete URL (e.g. https://github.com/owner/name.git).",
name
))?
};
- let url_str = format!("https://github.com/{shorthand}.git");
- let url = Url::parse(&url_str)?;
Ok(Self {
url,
@@ -248,54 +317,134 @@ impl SwiftPackageRepo {
#[cfg(test)]
mod tests {
- use crate::config::Config;
+ use crate::{config::Config, toolset::ToolVersionOptions};
use super::*;
+ use indexmap::indexmap;
use pretty_assertions::assert_str_eq;
+ #[tokio::test]
+ async fn test_git_provider_from_ba() {
+ // Example of defining a capture (closure) in Rust:
+ let get_ba = |tool: String, opts: Option| {
+ BackendArg::new_raw("spm".to_string(), Some(tool.clone()), tool, opts)
+ };
+
+ assert_eq!(
+ GitProvider::from_ba(&get_ba("tool".to_string(), None)),
+ GitProvider {
+ api_url: github::API_URL.to_string(),
+ kind: GitProviderKind::GitHub
+ }
+ );
+
+ assert_eq!(
+ GitProvider::from_ba(&get_ba(
+ "tool".to_string(),
+ Some(ToolVersionOptions {
+ opts: indexmap![
+ "provider".to_string() => "gitlab".to_string()
+ ],
+ ..Default::default()
+ })
+ )),
+ GitProvider {
+ api_url: gitlab::API_URL.to_string(),
+ kind: GitProviderKind::GitLab
+ }
+ );
+
+ assert_eq!(
+ GitProvider::from_ba(&get_ba(
+ "tool".to_string(),
+ Some(ToolVersionOptions {
+ opts: indexmap![
+ "api_url".to_string() => "https://gitlab.acme.com/api/v4".to_string(),
+ "provider".to_string() => "gitlab".to_string(),
+ ],
+ ..Default::default()
+ })
+ )),
+ GitProvider {
+ api_url: "https://gitlab.acme.com/api/v4".to_string(),
+ kind: GitProviderKind::GitLab
+ }
+ );
+ }
+
#[tokio::test]
async fn test_spm_repo_init_by_shorthand() {
let _config = Config::get().await.unwrap();
- let package_name = "nicklockwood/SwiftFormat";
- let package_repo = SwiftPackageRepo::new(package_name).unwrap();
+ let package_repo =
+ SwiftPackageRepo::new("nicklockwood/SwiftFormat", &GitProvider::default()).unwrap();
assert_str_eq!(
package_repo.url.as_str(),
"https://github.com/nicklockwood/SwiftFormat.git"
);
assert_str_eq!(package_repo.shorthand, "nicklockwood/SwiftFormat");
+
+ let package_repo = SwiftPackageRepo::new(
+ "acme/nicklockwood/SwiftFormat",
+ &GitProvider {
+ api_url: gitlab::API_URL.to_string(),
+ kind: GitProviderKind::GitLab,
+ },
+ )
+ .unwrap();
+ assert_str_eq!(
+ package_repo.url.as_str(),
+ "https://gitlab.com/acme/nicklockwood/SwiftFormat.git"
+ );
+ assert_str_eq!(package_repo.shorthand, "acme/nicklockwood/SwiftFormat");
}
#[tokio::test]
async fn test_spm_repo_init_name() {
let _config = Config::get().await.unwrap();
assert!(
- SwiftPackageRepo::new("owner/name.swift").is_ok(),
+ SwiftPackageRepo::new("owner/name.swift", &GitProvider::default()).is_ok(),
"name part can contain ."
);
assert!(
- SwiftPackageRepo::new("owner/name_swift").is_ok(),
+ SwiftPackageRepo::new("owner/name_swift", &GitProvider::default()).is_ok(),
"name part can contain _"
);
assert!(
- SwiftPackageRepo::new("owner/name-swift").is_ok(),
+ SwiftPackageRepo::new("owner/name-swift", &GitProvider::default()).is_ok(),
"name part can contain -"
);
assert!(
- SwiftPackageRepo::new("owner/name$swift").is_err(),
+ SwiftPackageRepo::new("owner/name$swift", &GitProvider::default()).is_err(),
"name part cannot contain characters other than a-zA-Z0-9._-"
);
}
#[tokio::test]
async fn test_spm_repo_init_by_url() {
- let _config = Config::get().await.unwrap();
- let package_name = "https://github.com/nicklockwood/SwiftFormat.git";
- let package_repo = SwiftPackageRepo::new(package_name).unwrap();
+ let package_repo = SwiftPackageRepo::new(
+ "https://github.com/nicklockwood/SwiftFormat.git",
+ &GitProvider::default(),
+ )
+ .unwrap();
assert_str_eq!(
package_repo.url.as_str(),
"https://github.com/nicklockwood/SwiftFormat.git"
);
assert_str_eq!(package_repo.shorthand, "nicklockwood/SwiftFormat");
+
+ let package_repo = SwiftPackageRepo::new(
+ "https://gitlab.acme.com/acme/someuser/SwiftTool.git",
+ &GitProvider {
+ api_url: "https://api.gitlab.acme.com/api/v4".to_string(),
+ kind: GitProviderKind::GitLab,
+ },
+ )
+ .unwrap();
+ assert_str_eq!(
+ package_repo.url.as_str(),
+ "https://gitlab.acme.com/acme/someuser/SwiftTool.git"
+ );
+ assert_str_eq!(package_repo.shorthand, "acme/someuser/SwiftTool");
}
}