Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 110 additions & 57 deletions src/backend/ubi.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::backend::VersionInfo;
use crate::backend::backend_type::BackendType;
use crate::backend::options::{BackendOptions, is_truthy};
use crate::backend::platform_target::PlatformTarget;
use crate::backend::runtime_path_for_install_path;
use crate::backend::static_helpers::{lookup_platform_key, try_with_v_prefix};
use crate::backend::static_helpers::try_with_v_prefix;
use crate::cli::args::BackendArg;
use crate::config::{Config, Settings};
use crate::env::{
Expand Down Expand Up @@ -30,6 +31,81 @@ pub struct UbiBackend {
ba: Arc<BackendArg>,
}

#[derive(Debug, Clone, Copy)]
struct UbiOptions<'a> {
values: BackendOptions<'a>,
}

impl<'a> UbiOptions<'a> {
fn new(raw: &'a ToolVersionOptions) -> Self {
Self {
values: BackendOptions::new(raw),
}
}

fn provider(&self) -> eyre::Result<ForgeType> {
match self.values.str("provider") {
Some(forge) => Ok(ForgeType::from_str(forge)?),
None => Ok(ForgeType::default()),
}
}

fn api_url_override(&self) -> Option<&'a str> {
self.values.str("api_url")
}

fn api_url(&self, forge: &ForgeType) -> eyre::Result<String> {
match self.api_url_override() {
Some(api_url) => Ok(api_url.strip_suffix('/').unwrap_or(api_url).to_string()),
None => match forge {
ForgeType::GitHub => Ok(github::API_URL.to_string()),
ForgeType::GitLab => Ok(gitlab::API_URL.to_string()),
_ => bail!("Unsupported forge type {:?}", forge),
},
}
}

fn tag_regex(&self) -> Option<&'a str> {
self.values.str("tag_regex")
}

fn bin_path(&self) -> Option<String> {
self.values.platform_string("bin_path")
}

fn extract_all(&self) -> bool {
self.values
.string("extract_all")
Comment thread
risu729 marked this conversation as resolved.
Outdated
.is_some_and(|v| is_truthy(&v))
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
greptile-apps[bot] marked this conversation as resolved.

fn exe(&self) -> Option<&'a str> {
self.values.str("exe")
}

fn rename_exe(&self) -> Option<&'a str> {
self.values.str("rename_exe")
}

fn matching(&self) -> Option<&'a str> {
self.values.str("matching")
}

fn matching_regex(&self) -> Option<&'a str> {
self.values.str("matching_regex")
}

fn lockfile_options(&self) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for key in ["exe", "matching", "matching_regex", "provider"] {
if let Some(value) = self.values.str(key) {
result.insert(key.to_string(), value.to_string());
}
}
result
}
}

#[async_trait]
impl Backend for UbiBackend {
fn get_type(&self) -> BackendType {
Expand Down Expand Up @@ -61,19 +137,11 @@ impl Backend for UbiBackend {
..Default::default()
}])
} else {
let opts = config.get_tool_opts_with_overrides(&self.ba).await?;
let forge = match opts.get("provider") {
Some(forge) => ForgeType::from_str(forge)?,
None => ForgeType::default(),
};
let api_url = match opts.get("api_url") {
Some(api_url) => api_url.strip_suffix("/").unwrap_or(api_url),
None => match forge {
ForgeType::GitHub => github::API_URL,
ForgeType::GitLab => gitlab::API_URL,
_ => bail!("Unsupported forge type {:?}", forge),
},
};
let raw_opts = config.get_tool_opts_with_overrides(&self.ba).await?;
let opts = UbiOptions::new(&raw_opts);
let forge = opts.provider()?;
let api_url = opts.api_url(&forge)?;
let tag_regex = opts.tag_regex();

let tag_regex_cell = OnceLock::new();

Expand Down Expand Up @@ -102,7 +170,7 @@ impl Backend for UbiBackend {

// Helper to check if tag matches tag_regex (if provided)
let matches_tag_regex = |tag: &str| -> bool {
if let Some(re_str) = opts.get("tag_regex") {
if let Some(re_str) = tag_regex {
let re = tag_regex_cell.get_or_init(|| Regex::new(re_str).unwrap());
re.is_match(tag)
} else {
Expand All @@ -122,10 +190,10 @@ impl Backend for UbiBackend {
let mut version_infos: Vec<VersionInfo> = match forge {
ForgeType::GitHub => {
let releases =
github::list_releases_from_url(api_url, &self.tool_name()).await?;
github::list_releases_from_url(&api_url, &self.tool_name()).await?;
if releases.is_empty() {
// Fall back to tags (no created_at available)
github::list_tags_from_url(api_url, &self.tool_name())
github::list_tags_from_url(&api_url, &self.tool_name())
.await?
.into_iter()
.filter(|tag| matches_tag_regex(tag))
Expand Down Expand Up @@ -158,10 +226,10 @@ impl Backend for UbiBackend {
}
ForgeType::GitLab => {
let releases =
gitlab::list_releases_from_url(api_url, &self.tool_name()).await?;
gitlab::list_releases_from_url(&api_url, &self.tool_name()).await?;
if releases.is_empty() {
// Fall back to tags (no created_at available)
gitlab::list_tags_from_url(api_url, &self.tool_name())
gitlab::list_tags_from_url(&api_url, &self.tool_name())
.await?
.into_iter()
.filter(|tag| matches_tag_regex(tag))
Expand Down Expand Up @@ -222,11 +290,10 @@ impl Backend for UbiBackend {
.and_then(|p| p.url.clone());

let v = tv.version.to_string();
let opts = tv.request.options();
let bin_path = lookup_platform_key(&opts, "bin_path")
.or_else(|| opts.get("bin_path").map(|s| s.to_string()))
.unwrap_or_else(|| "bin".to_string());
let extract_all = opts.get("extract_all").is_some_and(|v| v == "true");
let raw_opts = tv.request.options();
let opts = UbiOptions::new(&raw_opts);
let bin_path = opts.bin_path().unwrap_or_else(|| "bin".to_string());
let extract_all = opts.extract_all();
let bin_dir = tv.install_path();

// Use lockfile URL if available, otherwise fall back to standard resolution
Expand All @@ -236,7 +303,6 @@ impl Backend for UbiBackend {
install(&self.tool_name(), &v, &bin_dir, extract_all, &opts).await?;
} else {
try_with_v_prefix(&v, None, |candidate| {
let opts = opts.clone();
let bin_dir = bin_dir.clone();
async move {
install(
Expand All @@ -253,10 +319,8 @@ impl Backend for UbiBackend {
}

let mut possible_exes = vec![
tv.request
.options()
.get("exe")
.map(|s| s.to_string())
opts.exe()
.map(str::to_string)
.unwrap_or(tv.ba().short.to_string()),
];
if cfg!(windows) {
Expand Down Expand Up @@ -323,11 +387,13 @@ impl Backend for UbiBackend {
// For ubi backend, generate a more specific platform key that includes tool-specific options
let mut platform_key = self.get_platform_key();
let filename = file.file_name().unwrap().to_string_lossy().to_string();
let raw_opts = tv.request.options();
let opts = UbiOptions::new(&raw_opts);

if let Some(exe) = tv.request.options().get("exe") {
if let Some(exe) = opts.exe() {
platform_key = format!("{platform_key}-{exe}");
}
if let Some(matching) = tv.request.options().get("matching") {
if let Some(matching) = opts.matching() {
platform_key = format!("{platform_key}-{matching}");
}
// Include filename to distinguish different downloads for the same platform
Expand Down Expand Up @@ -358,16 +424,15 @@ impl Backend for UbiBackend {
_config: &Arc<Config>,
tv: &ToolVersion,
) -> eyre::Result<Vec<std::path::PathBuf>> {
let opts = tv.request.options();
if let Some(bin_path) = lookup_platform_key(&opts, "bin_path")
.or_else(|| opts.get("bin_path").map(|s| s.to_string()))
{
let raw_opts = tv.request.options();
let opts = UbiOptions::new(&raw_opts);
if let Some(bin_path) = opts.bin_path() {
// bin_path should always point to a directory containing binaries
Ok(vec![runtime_path_for_install_path(
tv,
tv.install_path().join(&bin_path),
)])
} else if opts.get("extract_all").is_some_and(|v| v == "true") {
} else if opts.extract_all() {
Ok(vec![tv.runtime_path()])
} else {
let bin_path = tv.install_path().join("bin");
Expand All @@ -390,17 +455,8 @@ impl Backend for UbiBackend {
request: &ToolRequest,
_target: &PlatformTarget,
) -> BTreeMap<String, String> {
let opts = request.options();
let mut result = BTreeMap::new();

// These options affect which artifact is downloaded
for key in ["exe", "matching", "matching_regex", "provider"] {
if let Some(value) = opts.get(key) {
result.insert(key.to_string(), value.to_string());
}
}

result
let raw_opts = request.options();
UbiOptions::new(&raw_opts).lockfile_options()
}
}

Expand Down Expand Up @@ -465,7 +521,7 @@ async fn install(
v: &str,
bin_dir: &Path,
extract_all: bool,
opts: &ToolVersionOptions,
opts: &UbiOptions<'_>,
) -> eyre::Result<()> {
let mut builder = UbiBuilder::new().install_dir(bin_dir);

Expand All @@ -479,28 +535,25 @@ async fn install(
if extract_all {
builder = builder.extract_all();
} else {
if let Some(exe) = opts.get("exe") {
if let Some(exe) = opts.exe() {
builder = builder.exe(exe);
}
if let Some(rename_exe) = opts.get("rename_exe") {
if let Some(rename_exe) = opts.rename_exe() {
builder = builder.rename_exe_to(rename_exe)
}
}
if let Some(matching) = opts.get("matching") {
if let Some(matching) = opts.matching() {
builder = builder.matching(matching);
}
if let Some(matching_regex) = opts.get("matching_regex") {
if let Some(matching_regex) = opts.matching_regex() {
builder = builder.matching_regex(matching_regex);
}

let forge = match opts.get("provider") {
Some(forge) => ForgeType::from_str(forge)?,
None => ForgeType::default(),
};
let forge = opts.provider()?;
builder = builder.forge(forge.clone());
builder = set_token(builder, &forge);

if let Some(api_url) = opts.get("api_url")
if let Some(api_url) = opts.api_url_override()
&& !api_url.contains("github.com")
&& !api_url.contains("gitlab.com")
{
Expand Down
Loading