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
17 changes: 15 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ pub struct Settings {"#
"ListString" => "Vec<String>",
"ListPath" => "Vec<PathBuf>",
"SetString" => "BTreeSet<String>",
"IndexMap<String, String>" => "IndexMap<String, String>",
t => panic!("Unknown type: {t}"),
}));
if let Some(type_) = type_ {
Expand Down Expand Up @@ -276,9 +277,15 @@ pub static SETTINGS_META: Lazy<IndexMap<&'static str, SettingsMeta>> = Lazy::new
for (name, props) in &settings {
let props = props.as_table().unwrap();
if let Some(type_) = props.get("type").map(|v| v.as_str().unwrap()) {
// We could shadow the 'type_' variable, but its a best practice to avoid shadowing.
// Thus, we introduce 'meta_type' here.
let meta_type = match type_ {
"IndexMap<String, String>" => "IndexMap",
other => other,
};
lines.push(format!(
r#" "{name}" => SettingsMeta {{
type_: SettingsType::{type_},"#,
type_: SettingsType::{meta_type},"#,
));
if let Some(description) = props.get("description") {
let description = description.as_str().unwrap().to_string();
Expand All @@ -293,9 +300,15 @@ pub static SETTINGS_META: Lazy<IndexMap<&'static str, SettingsMeta>> = Lazy::new
for (key, props) in props.as_table().unwrap() {
let props = props.as_table().unwrap();
if let Some(type_) = props.get("type").map(|v| v.as_str().unwrap()) {
// We could shadow the 'type_' variable, but its a best practice to avoid shadowing.
// Thus, we introduce 'meta_type' here.
let meta_type = match type_ {
"IndexMap<String, String>" => "IndexMap",
other => other,
};
lines.push(format!(
r#" "{name}.{key}" => SettingsMeta {{
type_: SettingsType::{type_},"#,
type_: SettingsType::{meta_type},"#,
));
}
if let Some(description) = props.get("description") {
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export default withMermaid(
{ text: "Architecture", link: "/architecture" },
{ text: "Paranoid", link: "/paranoid" },
{ text: "Templates", link: "/templates" },
{ text: "URL Replacements", link: "/url-replacements" },
{ text: "Model Context Protocol", link: "/mcp" },
{ text: "How I Use mise", link: "/how-i-use-mise" },
{ text: "Directory Structure", link: "/directories" },
Expand Down
2 changes: 2 additions & 0 deletions docs/settings.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export default {
type = "string";
} else if (type === "ListString" || type === "ListPath") {
type = "string[]";
} else if (type === "IndexMap<String, String>") {
type = "object";
}
// } else if (type === "String" || type === "PathBuf") {
// type = 'string';
Expand Down
141 changes: 141 additions & 0 deletions docs/url-replacements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# URL Replacements

mise does not include a built-in registry for downloading artifacts.
Instead, it retrieves remote registry manifests, which specify the URLs for downloading tools.

In some environments — such as enterprises or DMZs — these URLs may not be directly accessible and must be accessed through a proxy or internal mirror.

URL replacements allow you to modify or redirect any URL that mise attempts to access, making it possible to use internal proxies, mirrors, or alternative sources as needed.

## Configuration Examples

Environment variable (JSON format):
```bash
# Simple hostname replacement
export MISE_URL_REPLACEMENTS='
{
"github.com": "nexus.mycompany.net",
"releases.hashicorp.com": "artifactory.xmpl.com"
}'

# Regex pattern (note the escaped backslashes in JSON)
export MISE_URL_REPLACEMENTS='
{
"regex:^http://(.+)" = "https://$1",
"regex:https://github\.com/([^/]+)/([^/]+)/releases/download/(.+)":
"https://hub.corp.com/artifactory/github/$1/$2/$3"
}'
```

In mise.toml:
```toml
[settings]
# Simple hostname replacement
url_replacements = {
"github.com" = "nexus.mycompany.net",
"releases.hashicorp.com" = "artifactory.xmpl.com"
}

# Regex patterns
url_replacements = {
"regex:^http://(.+)" = "https://$1",
"regex:https://github\\.com/([^/]+)/([^/]+)/releases/download/(.+)" =
"https://hub.corp.com/artifactory/github/$1/$2/$3"
}
```

## Simple Hostname Replacement

For simple hostname-based mirroring, the key is the original hostname/domain to replace,
and the value is the replacement string. The replacement happens by searching and replacing
the pattern anywhere in the full URL string (including protocol, hostname, path, and query parameters).

Examples:
- `github.com` -> `nexus.mycompany.net` replaces GitHub hostnames
- `https://github.com` -> `https://nexus.mycompany.net` with protocol excludes e.g. 'api.github.com'
- `https://github.com` -> `https://proxy.corp.com/github-mirror` replaces GitHub with corporate proxy
- `http://host.net` -> `https://host.net` replaces protocol from HTTP to HTTPS

## Advanced Regex Replacement

For more complex URL transformations, you can use regex patterns. When a key starts with `regex:`,
it is treated as a regular expression pattern that can match and transform any part of the URL.
The value can use capture groups from the regex pattern.

### Regex Examples

#### 1. Protocol Conversion (HTTP to HTTPS)
```toml
[settings]
url_replacements = {
"regex:^http://(.+)" = "https://$1"
}
```
This converts any HTTP URL to HTTPS by capturing everything after "http://" and replacing it with "https://".

#### 2. GitHub Release Mirroring with Path Restructuring
```toml
[settings]
url_replacements = {
"regex:https://github\\.com/([^/]+)/([^/]+)/releases/download/(.+)" =
"https://hub.corp.com/artifactory/github/$1/$2/$3"
}
```
Transforms `https://github.com/owner/repo/releases/download/v1.0.0/file.tar.gz`
to `https://hub.corp.com/artifactory/github/owner/repo/v1.0.0/file.tar.gz`

#### 3. Subdomain to Path Conversion
```toml
[settings]
url_replacements = {
"regex:https://([^.]+)\\.cdn\\.example\\.com/(.+)" =
"https://unified-cdn.com/$1/$2"
}
```
Converts subdomain-based URLs to path-based URLs on a unified CDN.

#### 4. Multiple Replacement Patterns (processed in order)
```toml
[settings]
url_replacements = {
"regex:https://github\\.com/microsoft/(.+)" =
"https://internal-mirror.com/microsoft/$1",
"regex:https://github\\.com/(.+)" =
"https://public-mirror.com/github/$1",
"releases.hashicorp.com" = "hashicorp-mirror.internal.com"
}
```
First regex catches Microsoft repositories specifically, second catches all other GitHub URLs,
and the simple replacement handles HashiCorp.

## Use Cases

1. **Corporate Mirrors**: Replace public download URLs with internal corporate mirrors
2. **Custom Registries**: Redirect package downloads to custom or private registries
3. **Geographic Optimization**: Route downloads to geographically closer mirrors
4. **Protocol Changes**: Convert HTTP URLs to HTTPS or vice versa

## Regex Syntax

mise uses Rust regex engine which supports:
- `^` and `$` for anchors (start/end of string)
- `(.+)` for capture groups (use `$1`, `$2`, etc. in replacement)
- `[^/]+` for character classes (matches any character except `/`)
- `\\.` for escaping special characters (note: double backslash required in TOML)
- `*`, `+`, `?` for quantifiers
- `|` for alternation

You can check on regex101.com if your regex works (see [example](https://regex101.com/r/rmcIE1/1)).
Full regex syntax documentation: <https://docs.rs/regex/latest/regex/#syntax>

## Precedence and Matching

- URL replacements are processed in the order they appear in the configuration (IndexMap insertion order)
- Both regex patterns (keys starting with `regex:`) and simple string replacements are processed in this same order
- The first matching pattern is used; subsequent patterns are ignored for that URL
- If no patterns match, the original URL is used unchanged

## Security Considerations

When using regex patterns, ensure your replacement URLs point to trusted sources,
as this feature can redirect tool downloads to arbitrary locations.
7 changes: 7 additions & 0 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,13 @@
"description": "List of default shell arguments for unix to be used with inline commands. For example, `sh`, `-c` for sh.",
"type": "string"
},
"url_replacements": {
"description": "Map of URL patterns to replacement URLs applied to all requests.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"use_file_shell_for_executable_tasks": {
"default": false,
"description": "Determines whether to use a specified shell for executing tasks in the tasks directory. When set to true, the shell defined in the file will be used, or the default shell specified by `windows_default_file_shell_args` or `unix_default_file_shell_args` will be applied. If set to false, tasks will be executed directly as programs.",
Expand Down
13 changes: 13 additions & 0 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,19 @@ type = "String"
default = "sh -c -o errexit"
description = "List of default shell arguments for unix to be used with inline commands. For example, `sh`, `-c` for sh."

[url_replacements]
env = "MISE_URL_REPLACEMENTS"
type = "IndexMap<String, String>"
optional = true
parse_env = "parse_url_replacements"
description = "Map of URL patterns to replacement URLs applied to all requests."
docs = '''
Map of URL patterns to replacement URLs. This feature supports both simple hostname replacements
and advanced regex-based URL transformations for download mirroring and custom registries.

See [URL Replacements](/url-replacements.html) for more information.
'''

[use_file_shell_for_executable_tasks]
env = "MISE_USE_FILE_SHELL_FOR_EXECUTABLE_TASKS"
type = "Bool"
Expand Down
1 change: 1 addition & 0 deletions src/cli/config/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl ConfigSet {
SettingsType::ListString => TomlValueTypes::List,
SettingsType::ListPath => TomlValueTypes::List,
SettingsType::SetString => TomlValueTypes::Set,
SettingsType::IndexMap => TomlValueTypes::String,
},
None => match self.value.as_str() {
"true" | "false" => TomlValueTypes::Bool,
Expand Down
1 change: 1 addition & 0 deletions src/cli/settings/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fn settings_type_to_string(st: &SettingsType) -> String {
SettingsType::ListString => "array".to_string(),
SettingsType::ListPath => "array".to_string(),
SettingsType::SetString => "array".to_string(),
SettingsType::IndexMap => "object".to_string(),
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/cli/settings/set.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use eyre::{Result, bail, eyre};
use toml_edit::DocumentMut;

use crate::config::settings::{SETTINGS_META, SettingsFile, SettingsType};
use crate::config::settings::{SETTINGS_META, SettingsFile, SettingsType, parse_url_replacements};
use crate::toml::dedup_toml_array;
use crate::{config, duration, file};

Expand Down Expand Up @@ -43,6 +43,7 @@ pub fn set(mut key: &str, value: &str, add: bool, local: bool) -> Result<()> {
SettingsType::ListString => parse_list_by_comma(value)?,
SettingsType::ListPath => parse_list_by_colon(value)?,
SettingsType::SetString => parse_set_by_comma(value)?,
SettingsType::IndexMap => parse_indexmap_by_json(value)?,
};

let path = if local {
Expand Down Expand Up @@ -133,6 +134,18 @@ fn parse_duration(value: &str) -> Result<toml_edit::Value> {
Ok(value.into())
}

fn parse_indexmap_by_json(value: &str) -> Result<toml_edit::Value> {
let index_map = parse_url_replacements(value)
.map_err(|e| eyre!("Failed to parse JSON for IndexMap: {}", e))?;
Ok(toml_edit::Value::InlineTable({
let mut table = toml_edit::InlineTable::new();
for (k, v) in index_map {
table.insert(&k, toml_edit::Value::String(toml_edit::Formatted::new(v)));
}
table
}))
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>

Expand Down
7 changes: 7 additions & 0 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub enum SettingsType {
ListString,
ListPath,
SetString,
IndexMap,
}

pub struct SettingsMeta {
Expand Down Expand Up @@ -536,3 +537,9 @@ where
.collect::<Result<BTreeSet<_>, _>>()
.map(|set| set.into_iter().collect())
}

/// Parse URL replacements from JSON string format
/// Expected format: {"source_domain": "replacement_domain", ...}
pub fn parse_url_replacements(input: &str) -> Result<IndexMap<String, String>, serde_json::Error> {
serde_json::from_str(input)
}
Loading
Loading