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
8 changes: 8 additions & 0 deletions e2e/backend/test_aqua_symlink_bins
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ just_root="$(mise where aqua:casey/just@1.46.0)"
assert_directory_exists "$just_root/.mise-bins"
assert "mise which just" "$just_root/.mise-bins/just"
assert_contains "mise x -- just --version" "just 1.46.0"

cat <<EOF >mise.toml
[tools]
"aqua:casey/just" = { version = "1.46", symlink_bins = true }
EOF

assert "mise bin-paths" "$MISE_DATA_DIR/installs/aqua-casey-just/1.46/.mise-bins"
assert "mise which just" "$MISE_DATA_DIR/installs/aqua-casey-just/1.46/.mise-bins/just"
9 changes: 9 additions & 0 deletions e2e/backend/test_github_alias_versions
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,13 @@ assert_contains "mise ls-remote tiny" "1.0.0"
# Installing with @latest should resolve to 1.0.0 from the github releases
mise install
assert "mise where tiny" "$MISE_DATA_DIR/installs/tiny/1.0.0"
assert "mise bin-paths tiny" "$MISE_DATA_DIR/installs/tiny/latest/hello-world-1.0.0/bin"
assert_contains "mise x -- hello-world" "hello world"

# Once the fuzzy request is recorded in the lockfile, PATH-facing bin paths
# should use the concrete locked install path so the runtime label cannot drift.
touch mise.lock
mise install
assert_contains "cat mise.lock" "1.0.0"
assert "mise bin-paths tiny" "$MISE_DATA_DIR/installs/tiny/1.0.0/hello-world-1.0.0/bin"
assert_contains "mise x -- hello-world" "hello world"
19 changes: 19 additions & 0 deletions e2e/backend/test_http_upgrade
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ if [[ ! -L $INSTALL_PATH ]]; then
fi
echo "Verified: Install path is a symlink (HTTP backend caching)"

cat <<EOF >mise.toml
[tools."http:hello-upgrade"]
version = "1.0.0"
url = "https://mise.en.dev/test-fixtures/hello-world-{{version}}.tar.gz"
version_list_url = "http://localhost:$HTTP_PORT/versions.txt"
bin_path = "hello-world-1.0.0/bin"
postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/hello-world-1.0.0/bin/hello-world"

[tools."http:hello-upgrade-latest"]
version = "latest"
url = "https://mise.en.dev/test-fixtures/hello-world-{{version}}.tar.gz"
version_list_url = "http://localhost:$HTTP_PORT/versions.txt"
bin_path = "hello-world-{{version}}/bin"
postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/hello-world-\$MISE_TOOL_VERSION/bin/hello-world"
EOF

mise install
assert "mise bin-paths http:hello-upgrade-latest" "$MISE_DATA_DIR/installs/http-hello-upgrade-latest/latest/hello-world-2.0.0/bin"

# Test 2: Verify mise latest sees the newer version
echo "Test 2: Check that mise latest sees version 2.0.0"
assert "mise latest http:hello-upgrade" "2.0.0"
Expand Down
11 changes: 6 additions & 5 deletions src/backend/aqua.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
cache::{CacheManager, CacheManagerBuilder},
};
use crate::{
backend::{Backend, strict_metadata},
backend::{Backend, MISE_BINS_DIR, strict_metadata},
config::Config,
};
use crate::{file, github, minisign};
Expand Down Expand Up @@ -500,9 +500,10 @@ impl Backend for AquaBackend {
_config: &Arc<Config>,
tv: &ToolVersion,
) -> Result<Vec<PathBuf>> {
let mise_bins_dir = tv.install_path().join(".mise-bins");
let runtime_path = tv.runtime_path();
let mise_bins_dir = tv.install_path().join(MISE_BINS_DIR);
if self.symlink_bins(tv) || mise_bins_dir.is_dir() {
return Ok(vec![mise_bins_dir]);
return Ok(vec![runtime_path.join(MISE_BINS_DIR)]);
}

let install_path = tv.install_path();
Expand Down Expand Up @@ -550,7 +551,7 @@ impl Backend for AquaBackend {
})
.await?
.iter()
.map(|p| p.mount(&install_path))
.map(|p| p.mount(&runtime_path))
.collect();
Ok(paths)
}
Expand Down Expand Up @@ -2260,7 +2261,7 @@ impl AquaBackend {
/// Creates a `.mise-bins` directory with symlinks only to the binaries defined in the aqua registry.
/// This prevents bundled dependencies (like Python in aws-cli) from being exposed on PATH.
fn create_symlink_bin_dir(&self, tv: &ToolVersion, srcs: &[AquaFileLink]) -> Result<()> {
let symlink_dir = tv.install_path().join(".mise-bins");
let symlink_dir = tv.install_path().join(MISE_BINS_DIR);
file::create_dir_all(&symlink_dir)?;

for link in srcs {
Expand Down
28 changes: 18 additions & 10 deletions src/backend/conda.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::backend::backend_type::BackendType;
use crate::backend::platform_target::PlatformTarget;
use crate::backend::{VersionInfo, filter_cached_prereleases, mark_prerelease};
use crate::backend::{
MISE_BINS_DIR, VersionInfo, filter_cached_prereleases, mark_prerelease,
runtime_path_for_install_path,
};
use crate::cli::args::BackendArg;
use crate::config::Config;
use crate::config::Settings;
Expand Down Expand Up @@ -541,7 +544,7 @@ impl CondaBackend {
/// Uses the PathsEntry list returned by rattler's link_package to identify which files
/// belong to the main package (excluding transitive dependency binaries).
fn create_symlink_bin_dir(&self, tv: &ToolVersion, main_paths: &[PathsEntry]) -> Result<()> {
let symlink_dir = tv.install_path().join(".mise-bins");
let symlink_dir = tv.install_path().join(MISE_BINS_DIR);
file::create_dir_all(&symlink_dir)?;

let install_path = tv.install_path();
Expand Down Expand Up @@ -770,23 +773,28 @@ impl Backend for CondaBackend {
_config: &Arc<Config>,
tv: &ToolVersion,
) -> Result<Vec<PathBuf>> {
let mise_bins = tv.install_path().join(".mise-bins");
let install_path = tv.install_path();
let mise_bins = install_path.join(MISE_BINS_DIR);
if mise_bins.exists() {
return Ok(vec![mise_bins]);
return Ok(vec![runtime_path_for_install_path(tv, mise_bins)]);
}

// Fallback for tools installed before this change
let install_path = tv.install_path();
if cfg!(windows) {
let bin_paths = if cfg!(windows) {
// Conda packages on Windows can put binaries in either location
// depending on the build variant (MSVC vs MSYS2/MinGW)
Ok(vec![
vec![
install_path.join("Library").join("bin"),
install_path.join("bin"),
])
]
} else {
Ok(vec![install_path.join("bin")])
}
vec![install_path.join("bin")]
};

Ok(bin_paths
.into_iter()
.map(|path| runtime_path_for_install_path(tv, path))
.collect())
}
}

Expand Down
14 changes: 9 additions & 5 deletions src/backend/github.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::backend::SecurityFeature;
use crate::backend::VersionInfo;
use crate::backend::asset_matcher::{self, Asset, AssetPicker, ChecksumFetcher};
use crate::backend::backend_type::BackendType;
Expand All @@ -7,6 +6,7 @@ use crate::backend::static_helpers::{
get_filename_from_url, install_artifact, lookup_platform_key, lookup_platform_key_for_target,
template_string, try_with_v_prefix, try_with_v_prefix_and_repo, verify_artifact,
};
use crate::backend::{MISE_BINS_DIR, SecurityFeature, runtime_path_for_install_path};
use crate::cli::args::{BackendArg, ToolVersionType};
use crate::config::{Config, Settings};
use crate::file;
Expand Down Expand Up @@ -353,12 +353,16 @@ impl Backend for UnifiedGitBackend {
_config: &Arc<Config>,
tv: &ToolVersion,
) -> Result<Vec<std::path::PathBuf>> {
let mise_bins_dir = tv.install_path().join(".mise-bins");
let mise_bins_dir = tv.install_path().join(MISE_BINS_DIR);
if self.get_filter_bins(tv).is_some() || mise_bins_dir.is_dir() {
return Ok(vec![mise_bins_dir]);
return Ok(vec![tv.runtime_path().join(MISE_BINS_DIR)]);
}

self.discover_bin_paths(tv)
Ok(self
.discover_bin_paths(tv)?
.into_iter()
.map(|path| runtime_path_for_install_path(tv, path))
.collect())
}

fn resolve_lockfile_options(
Expand Down Expand Up @@ -1392,7 +1396,7 @@ impl UnifiedGitBackend {

/// Creates a `.mise-bins` directory with symlinks only to the binaries specified in filter_bins.
fn create_symlink_bin_dir(&self, tv: &ToolVersion, bins: Vec<String>) -> Result<()> {
let symlink_dir = tv.install_path().join(".mise-bins");
let symlink_dir = tv.install_path().join(MISE_BINS_DIR);
file::create_dir_all(&symlink_dir)?;

// Find where the actual binaries are
Expand Down
17 changes: 11 additions & 6 deletions src/backend/http.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::backend::Backend;
use crate::backend::VersionInfo;
use crate::backend::backend_type::BackendType;
use crate::backend::runtime_path_for_install_path;
use crate::backend::static_helpers::{
clean_binary_name, get_filename_from_url, list_available_platforms_with_key,
lookup_platform_key, rename_executable_in_dir, template_string, verify_artifact,
Expand Down Expand Up @@ -739,22 +740,23 @@ impl Backend for HttpBackend {
tv: &ToolVersion,
) -> Result<Vec<PathBuf>> {
let opts = tv.request.options();
let install_path = tv.install_path();

// Check for explicit bin_path
if let Some(bin_path_template) = get_opt(&opts, "bin_path") {
let bin_path = template_string(&bin_path_template, tv);
return Ok(vec![tv.install_path().join(bin_path)]);
return Ok(vec![tv.runtime_path().join(bin_path)]);
}

// Check for bin directory
let bin_dir = tv.install_path().join("bin");
let bin_dir = install_path.join("bin");
if bin_dir.exists() {
return Ok(vec![bin_dir]);
return Ok(vec![tv.runtime_path().join("bin")]);
}

// Search subdirectories for bin directories
let mut paths = Vec::new();
if let Ok(entries) = std::fs::read_dir(tv.install_path()) {
if let Ok(entries) = std::fs::read_dir(&install_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
Expand All @@ -767,9 +769,12 @@ impl Backend for HttpBackend {
}

if paths.is_empty() {
Ok(vec![tv.install_path()])
Ok(vec![tv.runtime_path()])
} else {
Ok(paths)
Ok(paths
.into_iter()
.map(|path| runtime_path_for_install_path(tv, path))
.collect())
}
}
}
110 changes: 110 additions & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub type BackendMap = BTreeMap<String, ABackend>;
pub type BackendList = Vec<ABackend>;
pub type VersionCacheManager = CacheManager<Vec<VersionInfo>>;

pub(crate) const MISE_BINS_DIR: &str = ".mise-bins";

const VERSIONS_HOST_LOCAL_OPT_SOURCES: &[ToolOptionSource] = &[
ToolOptionSource::BackendAlias,
ToolOptionSource::Config,
Expand All @@ -91,6 +93,25 @@ fn has_local_version_listing_option_override(
resolved_opts
.has_any_key_from_sources(version_listing_opt_keys, VERSIONS_HOST_LOCAL_OPT_SOURCES)
}
/// Remaps a backend-discovered path from the concrete install dir to the
/// runtime path users put on PATH.
///
/// For fuzzy requests like `latest` or `1.46`, backends may discover bins under
/// the resolved version dir, but PATH-facing callers should use `runtime_path()`
/// for the same version. Paths outside the install dir are returned unchanged.
pub(crate) fn runtime_path_for_install_path(tv: &ToolVersion, path: PathBuf) -> PathBuf {
let install_path = tv.install_path();
if let Ok(relative_path) = path.strip_prefix(&install_path) {
let runtime_path = tv.runtime_path();
if relative_path.as_os_str().is_empty() {
runtime_path
} else {
runtime_path.join(relative_path)
}
} else {
path
}
}

static STRICT_METADATA: AtomicBool = AtomicBool::new(false);

Expand Down Expand Up @@ -403,6 +424,10 @@ pub(crate) fn normalize_idiomatic_contents(contents: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::args::BackendResolution;
use crate::toolset::{ToolSource, ToolVersionOptions};
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn test_normalize_idiomatic_contents() {
Expand Down Expand Up @@ -475,6 +500,91 @@ mod tests {
));
}

#[test]
fn test_runtime_path_for_install_path_remaps_install_subpath() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let short = format!(
"runtime-remap-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let mut backend = BackendArg::new_raw(
short.clone(),
None,
short.clone(),
None,
BackendResolution::new(false),
);
backend.installs_path = temp_dir.path().join("installs").join(&short);
fs::create_dir_all(&backend.installs_path)?;

let install_path = backend.installs_path.join("1.0.1");
fs::create_dir_all(install_path.join("bin"))?;
file::make_symlink_or_file(Path::new("./1.0.1"), &backend.installs_path.join("latest"))?;

let request = ToolRequest::Version {
backend: Arc::new(backend),
version: "latest".into(),
options: ToolVersionOptions::default(),
source: ToolSource::Argument,
};
let tv = ToolVersion::new(request, "1.0.1".into());

assert_eq!(
runtime_path_for_install_path(&tv, install_path.join("bin")),
tv.runtime_path().join("bin")
);

let external_path = temp_dir.path().join("external").join("bin");
assert_eq!(
runtime_path_for_install_path(&tv, external_path.clone()),
external_path
);

Ok(())
}

#[test]
fn test_runtime_path_for_install_path_falls_back_without_symlink() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let short = format!(
"runtime-remap-missing-symlink-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let mut backend = BackendArg::new_raw(
short.clone(),
None,
short.clone(),
None,
BackendResolution::new(false),
);
backend.installs_path = temp_dir.path().join("installs").join(&short);
fs::create_dir_all(&backend.installs_path)?;

let install_path = backend.installs_path.join("1.0.1");
fs::create_dir_all(install_path.join("bin"))?;

let request = ToolRequest::Version {
backend: Arc::new(backend),
version: "latest".into(),
options: ToolVersionOptions::default(),
source: ToolSource::Argument,
};
let tv = ToolVersion::new(request, "1.0.1".into());

assert_eq!(
runtime_path_for_install_path(&tv, install_path.join("bin")),
install_path.join("bin")
);

Ok(())
}

#[test]
fn test_fuzzy_match_versions_filters_prereleases_by_default() {
let versions = vec![
Expand Down
Loading
Loading