Skip to content

Commit

Permalink
new: Add setting for customizing detection strategy. (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj authored Nov 15, 2023
1 parent fcb53fb commit 3f17dec
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 38 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

#### Updates

- Added a `detect-strategy` setting to `~/.proto/config.toml` to configure which strategy to use when detecting a version. Accepts:
- `first-available` (default) - Will use the first available version that is found. Either from `.prototools` or a tool specific file (`.nvmrc`, etc).
- `prefer-prototools` - Prefer a `.prototools` version, even if found in a parent directory. If none found, falls back to tool specific file.
- Added support to plugins to ignore certain paths when detecting a version.
- WASM API
- Added `DetectVersionOutput.ignore` field.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ url = "2.4.1"

[dev-dependencies]
starbase_sandbox = { workspace = true }
tokio = { workspace = true }
10 changes: 10 additions & 0 deletions crates/core/src/user_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ use warpgate::{HttpOptions, Id, PluginLocator};

pub const USER_CONFIG_NAME: &str = "config.toml";

#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DetectStrategy {
#[default]
FirstAvailable,
PreferPrototools,
}

#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PinType {
Expand All @@ -22,6 +30,7 @@ pub enum PinType {
pub struct UserConfig {
pub auto_clean: bool,
pub auto_install: bool,
pub detect_strategy: DetectStrategy,
pub node_intercept_globals: bool,
pub pin_latest: Option<PinType>,
pub http: HttpOptions,
Expand Down Expand Up @@ -93,6 +102,7 @@ impl Default for UserConfig {
Self {
auto_clean: from_var("PROTO_AUTO_CLEAN", false),
auto_install: from_var("PROTO_AUTO_INSTALL", false),
detect_strategy: DetectStrategy::default(),
http: HttpOptions::default(),
node_intercept_globals: from_var("PROTO_NODE_INTERCEPT_GLOBALS", true),
pin_latest: None,
Expand Down
153 changes: 116 additions & 37 deletions crates/core/src/version_detector.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,117 @@
use crate::error::ProtoError;
use crate::tool::Tool;
use crate::tools_config::ToolsConfig;
use crate::user_config::DetectStrategy;
use std::{env, path::Path};
use tracing::{debug, trace};
use version_spec::*;

pub async fn detect_version_first_available(
tool: &Tool,
start_dir: &Path,
end_dir: &Path,
) -> miette::Result<Option<UnresolvedVersionSpec>> {
let mut current_dir: Option<&Path> = Some(start_dir);

while let Some(dir) = current_dir {
trace!(
tool = tool.id.as_str(),
dir = ?dir,
"Checking directory",
);

let config = ToolsConfig::load_from(dir)?;

if let Some(version) = config.tools.get(&tool.id) {
debug!(
tool = tool.id.as_str(),
version = version.to_string(),
file = ?config.path,
"Detected version from .prototools file",
);

return Ok(Some(version.to_owned()));
}

if let Some(version) = tool.detect_version_from(dir).await? {
debug!(
tool = tool.id.as_str(),
version = version.to_string(),
"Detected version from tool's ecosystem"
);

return Ok(Some(version));
}

if dir == end_dir {
break;
}

current_dir = dir.parent();
}

Ok(None)
}

pub async fn detect_version_prefer_prototools(
tool: &Tool,
start_dir: &Path,
end_dir: &Path,
) -> miette::Result<Option<UnresolvedVersionSpec>> {
let mut config_version = None;
let mut config_path = None;
let mut ecosystem_version = None;
let mut current_dir: Option<&Path> = Some(start_dir);

while let Some(dir) = current_dir {
trace!(
tool = tool.id.as_str(),
dir = ?dir,
"Checking directory",
);

if config_version.is_none() {
let mut config = ToolsConfig::load_from(dir)?;

config_version = config.tools.remove(&tool.id);
config_path = Some(config.path);
}

if ecosystem_version.is_none() {
ecosystem_version = tool.detect_version_from(dir).await?;
}

if dir == end_dir {
break;
}

current_dir = dir.parent();
}

if let Some(version) = config_version {
debug!(
tool = tool.id.as_str(),
version = version.to_string(),
file = ?config_path.unwrap(),
"Detected version from .prototools file",
);

return Ok(Some(version.to_owned()));
}

if let Some(version) = ecosystem_version {
debug!(
tool = tool.id.as_str(),
version = version.to_string(),
"Detected version from tool's ecosystem"
);

return Ok(Some(version));
}

Ok(None)
}

pub async fn detect_version(
tool: &Tool,
forced_version: Option<UnresolvedVersionSpec>,
Expand Down Expand Up @@ -45,46 +152,18 @@ pub async fn detect_version(

// Traverse upwards and attempt to detect a local version
if let Ok(working_dir) = env::current_dir() {
let mut current_dir: Option<&Path> = Some(&working_dir);

while let Some(dir) = current_dir {
trace!(
tool = tool.id.as_str(),
dir = ?dir,
"Checking directory",
);

// Detect from our config file
let config = ToolsConfig::load_from(dir)?;

if let Some(local_version) = config.tools.get(&tool.id) {
debug!(
tool = tool.id.as_str(),
version = local_version.to_string(),
file = ?config.path,
"Detected version from .prototools file",
);

return Ok(local_version.to_owned());
let user_config = tool.proto.load_user_config()?;
let detected_version = match user_config.detect_strategy {
DetectStrategy::FirstAvailable => {
detect_version_first_available(tool, &working_dir, &tool.proto.home).await?
}

// Detect using the tool
if let Some(detected_version) = tool.detect_version_from(dir).await? {
debug!(
tool = tool.id.as_str(),
version = detected_version.to_string(),
"Detected version from tool's ecosystem"
);

return Ok(detected_version);
}

// Don't traverse passed the home directory
if dir == tool.proto.home {
break;
DetectStrategy::PreferPrototools => {
detect_version_prefer_prototools(tool, &working_dir, &tool.proto.home).await?
}
};

current_dir = dir.parent();
if let Some(version) = detected_version {
return Ok(version);
}
}

Expand Down
5 changes: 4 additions & 1 deletion crates/core/tests/user_config_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use proto_core::{PinType, UserConfig, USER_CONFIG_NAME};
use proto_core::{DetectStrategy, PinType, UserConfig, USER_CONFIG_NAME};
use starbase_sandbox::create_empty_sandbox;
use std::collections::BTreeMap;
use std::env;
Expand All @@ -17,6 +17,7 @@ mod user_config {
UserConfig {
auto_clean: false,
auto_install: false,
detect_strategy: DetectStrategy::default(),
node_intercept_globals: true,
http: HttpOptions::default(),
pin_latest: None,
Expand Down Expand Up @@ -46,6 +47,7 @@ pin-latest = "global"
UserConfig {
auto_clean: true,
auto_install: true,
detect_strategy: DetectStrategy::default(),
node_intercept_globals: false,
http: HttpOptions::default(),
pin_latest: Some(PinType::Global),
Expand All @@ -71,6 +73,7 @@ pin-latest = "global"
UserConfig {
auto_clean: true,
auto_install: true,
detect_strategy: DetectStrategy::default(),
node_intercept_globals: false,
http: HttpOptions::default(),
pin_latest: None,
Expand Down
103 changes: 103 additions & 0 deletions crates/core/tests/version_detector_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use proto_core::{
detect_version_first_available, detect_version_prefer_prototools, load_tool_from_locator,
ProtoEnvironment, Tool, ToolsConfig, UnresolvedVersionSpec, UserConfig,
};
use starbase_sandbox::create_empty_sandbox;
use std::path::Path;
use warpgate::Id;

mod version_detector {
use super::*;

async fn create_node(_root: &Path) -> Tool {
load_tool_from_locator(
Id::raw("node"),
ProtoEnvironment::new().unwrap(),
ToolsConfig::builtin_plugins().get("node").unwrap(),
&UserConfig::default(),
)
.await
.unwrap()
}

#[tokio::test]
async fn uses_deepest_prototools() {
let sandbox = create_empty_sandbox();
sandbox.create_file("a/.prototools", "node = \"20\"");
sandbox.create_file("a/b/.prototools", "node = \"18\"");
sandbox.create_file("a/b/c/.prototools", "node = \"16\"");

let tool = create_node(sandbox.path()).await;

assert_eq!(
detect_version_first_available(&tool, &sandbox.path().join("a/b/c"), sandbox.path())
.await
.unwrap(),
Some(UnresolvedVersionSpec::parse("~16").unwrap())
);

assert_eq!(
detect_version_first_available(&tool, &sandbox.path().join("a/b"), sandbox.path())
.await
.unwrap(),
Some(UnresolvedVersionSpec::parse("~18").unwrap())
);

assert_eq!(
detect_version_first_available(&tool, &sandbox.path().join("a"), sandbox.path())
.await
.unwrap(),
Some(UnresolvedVersionSpec::parse("~20").unwrap())
);
}

#[tokio::test]
async fn finds_first_available_prototools() {
let sandbox = create_empty_sandbox();
sandbox.create_file("a/.prototools", "node = \"20\"");
sandbox.create_file("package.json", r#"{ "engines": { "node": "18" } }"#);

let tool = create_node(sandbox.path()).await;

assert_eq!(
detect_version_first_available(&tool, &sandbox.path().join("a/b"), sandbox.path())
.await
.unwrap(),
Some(UnresolvedVersionSpec::parse("~20").unwrap())
);
}

#[tokio::test]
async fn finds_first_available_ecosystem() {
let sandbox = create_empty_sandbox();
sandbox.create_file(".prototools", "node = \"20\"");
sandbox.create_file("a/package.json", r#"{ "engines": { "node": "18" } }"#);

let tool = create_node(sandbox.path()).await;

assert_eq!(
detect_version_first_available(&tool, &sandbox.path().join("a/b"), sandbox.path())
.await
.unwrap(),
Some(UnresolvedVersionSpec::parse("~18").unwrap())
);
}

#[tokio::test]
async fn prefers_prototools() {
let sandbox = create_empty_sandbox();
sandbox.create_file("a/.prototools", "node = \"20\"");
sandbox.create_file("a/b/.prototools", "node = \"18\"");
sandbox.create_file("a/b/package.json", r#"{ "engines": { "node": "17" } }"#);
sandbox.create_file("a/b/c/package.json", r#"{ "engines": { "node": "19" } }"#);

let tool = create_node(sandbox.path()).await;

assert_eq!(
detect_version_prefer_prototools(&tool, &sandbox.path().join("a/b/c"), sandbox.path())
.await
.unwrap(),
Some(UnresolvedVersionSpec::parse("~18").unwrap())
);
}
}

0 comments on commit 3f17dec

Please sign in to comment.