Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
393 changes: 212 additions & 181 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,18 @@ You can also have environment specific config files like `.mise.production.toml`

### `[tools]` - Dev tools

See [Tools](/dev-tools/).
See [Tools](/dev-tools/). In addition to specifying versions, each tool entry can include options such as:

- `os`: Restrict installation to certain operating systems
- `install_env`: Environment vars used during install
- `postinstall`: Command to run after installation completes for that specific tool

Examples:

```toml
[tools]
node = { version = "22", postinstall = "corepack enable" }
```

### `[env]` - Arbitrary Environment Variables

Expand Down
15 changes: 15 additions & 0 deletions docs/dev-tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ port = 6379

Internally, nested options are flattened to dot notation (e.g., `platforms.macos-x64.url`, `database.host`, `cache.redis.port`) for backend access.

### Tool postinstall commands

Run a command immediately after a tool finishes installing by adding a `postinstall` field to that tool's configuration. This is separate from `[hooks].postinstall` and applies only to when a specific tool is installed.

```toml
[tools]
node = { version = "22", postinstall = "corepack enable" }
```

Behavior:
- The command runs once the install completes successfully for that tool/version.
- The tool's bin path is on PATH during the command, so you can invoke the installed tool directly.
- Environment variables include `MISE_TOOL_INSTALL_PATH` pointing to the tool's install directory.
- If the install fails, the `postinstall` command is not run.

## OS-Specific Tools

You can restrict tools to specific operating systems using the `os` field:
Expand Down
8 changes: 4 additions & 4 deletions man/man1/mise.1
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ mise \- The front\-end to your dev env
mise manages dev tools, env vars, and runs tasks. https://github.com/jdx/mise
.SH OPTIONS
.TP
\fB\-C\fR, \fB\-\-cd\fR=\fIDIR\fR
\fB\-C\fR, \fB\-\-cd\fR \fI<DIR>\fR
Change directory before running command
.TP
\fB\-E\fR, \fB\-\-env\fR=\fIENV\fR
\fB\-E\fR, \fB\-\-env\fR \fI<ENV>\fR
Set the environment for loading `mise.<ENV>.toml`
.TP
\fB\-j\fR, \fB\-\-jobs\fR=\fIJOBS\fR
\fB\-j\fR, \fB\-\-jobs\fR \fI<JOBS>\fR
How many jobs to run in parallel [default: 8]
.RS
May also be specified with the \fBMISE_JOBS\fR environment variable.
.RE
.TP
\fB\-\-output\fR=\fIOUTPUT\fR
\fB\-\-output\fR \fI<OUTPUT>\fR

.TP
\fB\-\-raw\fR
Expand Down
2 changes: 1 addition & 1 deletion schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@
"type": "string"
},
"fetch_remote_versions_timeout": {
"default": "5s",
"default": "10s",
"description": "Timeout in seconds for HTTP requests to fetch new tool versions in mise.",
"type": "string"
},
Expand Down
2 changes: 1 addition & 1 deletion settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ cached. For "slow" commands like `mise ls-remote` or `mise install`:
[fetch_remote_versions_timeout]
env = "MISE_FETCH_REMOTE_VERSIONS_TIMEOUT"
type = "Duration"
default = "5s"
default = "10s"
description = "Timeout in seconds for HTTP requests to fetch new tool versions in mise."
aliases = ["fetch_remote_version_timeout"]

Expand Down
157 changes: 103 additions & 54 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,55 @@ use std::time::Duration;

use eyre::{Report, Result, bail, ensure};
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{ClientBuilder, IntoUrl, Response};
use reqwest::{ClientBuilder, IntoUrl, Method, Response};
use std::sync::LazyLock as Lazy;
use url::Url;

use crate::cli::version;
use crate::config::Settings;
use crate::file::display_path;
use crate::ui::progress_report::SingleReport;
use crate::ui::time::format_duration;
use crate::{env, file};

#[cfg(not(test))]
pub static HTTP_VERSION_CHECK: Lazy<Client> =
Lazy::new(|| Client::new(Duration::from_secs(3)).unwrap());
Lazy::new(|| Client::new(Duration::from_secs(3), ClientKind::VersionCheck).unwrap());

pub static HTTP: Lazy<Client> = Lazy::new(|| Client::new(Settings::get().http_timeout()).unwrap());
pub static HTTP: Lazy<Client> =
Lazy::new(|| Client::new(Settings::get().http_timeout(), ClientKind::Http).unwrap());

pub static HTTP_FETCH: Lazy<Client> =
Lazy::new(|| Client::new(Settings::get().fetch_remote_versions_timeout()).unwrap());
pub static HTTP_FETCH: Lazy<Client> = Lazy::new(|| {
Client::new(
Settings::get().fetch_remote_versions_timeout(),
ClientKind::Fetch,
)
.unwrap()
});

#[derive(Debug)]
pub struct Client {
reqwest: reqwest::Client,
timeout: Duration,
kind: ClientKind,
}

#[derive(Debug, Clone, Copy)]
enum ClientKind {
Http,
Fetch,
VersionCheck,
}

impl Client {
fn new(timeout: Duration) -> Result<Self> {
fn new(timeout: Duration, kind: ClientKind) -> Result<Self> {
Ok(Self {
reqwest: Self::_new()
.read_timeout(timeout)
.connect_timeout(timeout)
.build()?,
timeout,
kind,
})
}

Expand Down Expand Up @@ -65,30 +83,10 @@ impl Client {
headers: &HeaderMap,
) -> Result<Response> {
ensure!(!*env::OFFLINE, "offline mode is enabled");
let get = |url: Url| async move {
debug!("GET {}", &url);
let mut req = self.reqwest.get(url.clone());
req = req.headers(headers.clone());
let resp = req.send().await?;
if *env::MISE_LOG_HTTP {
eprintln!("GET {url} {}", resp.status());
}
debug!("GET {url} {}", resp.status());
display_github_rate_limit(&resp);
resp.error_for_status_ref()?;
Ok(resp)
};
let mut url = url.into_url().unwrap();
let resp = match get(url.clone()).await {
Ok(resp) => resp,
Err(_) if url.scheme() == "http" => {
// try with https since http may be blocked
url.set_scheme("https").unwrap();
get(url).await?
}
Err(err) => return Err(err),
};

let url = url.into_url().unwrap();
let resp = self
.send_with_https_fallback(Method::GET, url, headers, "GET")
.await?;
resp.error_for_status_ref()?;
Ok(resp)
}
Expand All @@ -105,30 +103,10 @@ impl Client {
headers: &HeaderMap,
) -> Result<Response> {
ensure!(!*env::OFFLINE, "offline mode is enabled");
let head = |url: Url| async move {
debug!("HEAD {}", &url);
let mut req = self.reqwest.head(url.clone());
req = req.headers(headers.clone());
let resp = req.send().await?;
if *env::MISE_LOG_HTTP {
eprintln!("HEAD {url} {}", resp.status());
}
debug!("HEAD {url} {}", resp.status());
display_github_rate_limit(&resp);
resp.error_for_status_ref()?;
Ok(resp)
};
let mut url = url.into_url().unwrap();
let resp = match head(url.clone()).await {
Ok(resp) => resp,
Err(_) if url.scheme() == "http" => {
// try with https since http may be blocked
url.set_scheme("https").unwrap();
head(url).await?
}
Err(err) => return Err(err),
};

let url = url.into_url().unwrap();
let resp = self
.send_with_https_fallback(Method::HEAD, url, headers, "HEAD")
.await?;
resp.error_for_status_ref()?;
Ok(resp)
}
Expand Down Expand Up @@ -241,6 +219,77 @@ impl Client {
file.persist(path)?;
Ok(())
}

async fn send_with_https_fallback(
&self,
method: Method,
mut url: Url,
headers: &HeaderMap,
verb_label: &str,
) -> Result<Response> {
match self
.send_once(method.clone(), url.clone(), headers, verb_label)
.await
{
Ok(resp) => Ok(resp),
Err(_) if url.scheme() == "http" => {
url.set_scheme("https").unwrap();
self.send_once(method, url, headers, verb_label).await
}
Err(err) => Err(err),
}
}

async fn send_once(
&self,
method: Method,
url: Url,
headers: &HeaderMap,
verb_label: &str,
) -> Result<Response> {
debug!("{} {}", verb_label, &url);
let mut req = self.reqwest.request(method, url.clone());
req = req.headers(headers.clone());
let resp = match req.send().await {
Ok(resp) => resp,
Err(err) => {
if err.is_timeout() {
let (setting, env_var) = match self.kind {
ClientKind::Http => ("http_timeout", "MISE_HTTP_TIMEOUT"),
ClientKind::Fetch => (
"fetch_remote_versions_timeout",
"MISE_FETCH_REMOTE_VERSIONS_TIMEOUT",
),
ClientKind::VersionCheck => ("version_check_timeout", ""),
};
let hint = if env_var.is_empty() {
format!(
"HTTP timed out after {} for {}.",
format_duration(self.timeout),
url
)
} else {
format!(
"HTTP timed out after {} for {} (change with `{}` or env `{}`).",
format_duration(self.timeout),
url,
setting,
env_var
)
};
bail!(hint);
}
return Err(err.into());
}
};
if *env::MISE_LOG_HTTP {
eprintln!("{} {url} {}", verb_label, resp.status());
}
debug!("{} {url} {}", verb_label, resp.status());
display_github_rate_limit(&resp);
resp.error_for_status_ref()?;
Ok(resp)
}
}

pub fn error_code(e: &Report) -> Option<u16> {
Expand Down
18 changes: 16 additions & 2 deletions src/plugins/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use color_eyre::eyre::Context;
use eyre::Result;
use std::ffi::OsString;
use std::sync::Arc;
Expand All @@ -8,7 +9,7 @@ use crate::cli::args::BackendArg;
use crate::config::Settings;
use crate::env;
use crate::env::PATH_KEY;
use crate::timeout::run_with_timeout;
use crate::timeout::{TimeoutError, run_with_timeout};
use crate::toolset::ToolVersion;

mod bun;
Expand Down Expand Up @@ -57,7 +58,20 @@ where
F: FnOnce() -> Result<T> + Send,
T: Send,
{
run_with_timeout(f, Settings::get().fetch_remote_versions_timeout())
let timeout = Settings::get().fetch_remote_versions_timeout();
match run_with_timeout(f, timeout) {
Ok(v) => Ok(v),
Err(err) => {
// Only add a hint when the error was actually caused by a timeout
if err.downcast_ref::<TimeoutError>().is_some() {
Err(err).context(
"change with `fetch_remote_versions_timeout` or env `MISE_FETCH_REMOTE_VERSIONS_TIMEOUT`",
)
} else {
Err(err)
}
}
}
}

pub fn new_backend_arg(tool_name: &str) -> BackendArg {
Expand Down
26 changes: 22 additions & 4 deletions src/timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@ use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use color_eyre::eyre::{Context, Result};
use crate::ui::time::format_duration;
use color_eyre::eyre::{Report, Result};
use std::fmt::{Display, Formatter};

#[derive(Debug, Clone, Copy)]
pub struct TimeoutError {
pub duration: Duration,
}

impl Display for TimeoutError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "timed out after {}", format_duration(self.duration))
}
}

impl std::error::Error for TimeoutError {}

pub fn run_with_timeout<F, T>(f: F, timeout: Duration) -> Result<T>
where
Expand All @@ -16,8 +31,11 @@ where
// If sending fails, the timeout has already been reached.
let _ = tx.send(result);
});
rx.recv_timeout(timeout).context("timed out")
})?
let recv: Result<T> = rx
.recv_timeout(timeout)
.map_err(|_| Report::from(TimeoutError { duration: timeout }))?;
recv
})
}

pub async fn run_with_timeout_async<F, Fut, T>(f: F, timeout: Duration) -> Result<T>
Expand All @@ -29,6 +47,6 @@ where
match tokio::time::timeout(timeout, f()).await {
Ok(Ok(output)) => Ok(output),
Ok(Err(e)) => Err(e),
Err(_) => Err(eyre::eyre!("timed out")),
Err(_) => Err(TimeoutError { duration: timeout }.into()),
}
}
Loading