From b3fa5584b6124df1dd5923b9db4cfccf1d1bef19 Mon Sep 17 00:00:00 2001 From: Marukome0743 Date: Fri, 31 Jul 2026 21:03:14 +0900 Subject: [PATCH] fix(asdf): expose dependencies to install scripts --- docs/dev-tools/backends/asdf.md | 14 ++++ e2e/backend/test_asdf_install_dependency_path | 84 +++++++++++++++++++ src/backend/asdf.rs | 21 ++++- src/backend/mod.rs | 25 ++++-- src/backend/vfox.rs | 4 +- 5 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 e2e/backend/test_asdf_install_dependency_path diff --git a/docs/dev-tools/backends/asdf.md b/docs/dev-tools/backends/asdf.md index 4e12388f104..46c883fdf0d 100644 --- a/docs/dev-tools/backends/asdf.md +++ b/docs/dev-tools/backends/asdf.md @@ -73,3 +73,17 @@ Set environment variables for asdf plugin install scripts: [tools] "asdf:owner/plugin" = { version = "latest", install_env = { MAKEFLAGS = "-j8" } } ``` + +### Install dependencies + +Tools declared with the [`depends` option](/dev-tools/#tool-dependencies) are installed before the +asdf tool and added to the `PATH` used by its `bin/download` and `bin/install` scripts: + +```toml +[tools] +python = "3.12" +"asdf:owner/plugin" = { version = "latest", depends = ["python"] } +``` + +This allows an asdf plugin to invoke an executable supplied by another mise-managed tool during +the same `mise install`. diff --git a/e2e/backend/test_asdf_install_dependency_path b/e2e/backend/test_asdf_install_dependency_path new file mode 100644 index 00000000000..521cbaa8777 --- /dev/null +++ b/e2e/backend/test_asdf_install_dependency_path @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +# An asdf plugin's bin/download and bin/install scripts must be able to execute a +# dependency that was installed earlier in the same `mise install` (#4384). +PLUGIN_DIR="$MISE_DATA_DIR/plugins/asdf-asdf-path-dependent" +DEPENDENCY_PLUGIN_DIR="$MISE_DATA_DIR/plugins/node" +mkdir -p "$PLUGIN_DIR/bin" "$DEPENDENCY_PLUGIN_DIR/bin" "$HOME/bin" + +# A same-named executable already on the ambient PATH must not outrank the +# dependency declared by the tool. +export PATH="$HOME/bin:$PATH" +cat >"$HOME/bin/node" <<'EOF' +#!/usr/bin/env bash +exit 99 +EOF +chmod +x "$HOME/bin/node" + +cat >"$DEPENDENCY_PLUGIN_DIR/bin/list-all" <<'EOF' +#!/usr/bin/env bash +echo 1.0.0 +EOF + +cat >"$DEPENDENCY_PLUGIN_DIR/bin/install" <<'EOF' +#!/usr/bin/env bash +set -eu +mkdir -p "$ASDF_INSTALL_PATH/bin" +cat >"$ASDF_INSTALL_PATH/bin/node" <<'SCRIPT' +#!/usr/bin/env bash +echo "node dependency: $1" +SCRIPT +chmod +x "$ASDF_INSTALL_PATH/bin/node" +EOF + +chmod +x "$DEPENDENCY_PLUGIN_DIR/bin/list-all" "$DEPENDENCY_PLUGIN_DIR/bin/install" + +cat >"$PLUGIN_DIR/bin/list-all" <<'EOF' +#!/usr/bin/env bash +echo 1.0.0 +EOF + +cat >"$PLUGIN_DIR/bin/download" <<'EOF' +#!/usr/bin/env bash +set -eu +mkdir -p "$ASDF_DOWNLOAD_PATH" +command -v node >"$ASDF_DOWNLOAD_PATH/dependency-path" +node download >/dev/null +EOF + +cat >"$PLUGIN_DIR/bin/install" <<'EOF' +#!/usr/bin/env bash +set -eu +mkdir -p "$ASDF_INSTALL_PATH/bin" +cp "$ASDF_DOWNLOAD_PATH/dependency-path" "$ASDF_INSTALL_PATH/download-dependency-path" +command -v node >"$ASDF_INSTALL_PATH/install-dependency-path" +node install >/dev/null +printf '%s\n' "$PATH" >"$ASDF_INSTALL_PATH/install-path" +cat >"$ASDF_INSTALL_PATH/bin/asdf-path-dependent" <<'SCRIPT' +#!/usr/bin/env bash +echo installed +SCRIPT +chmod +x "$ASDF_INSTALL_PATH/bin/asdf-path-dependent" +EOF + +chmod +x "$PLUGIN_DIR/bin/list-all" "$PLUGIN_DIR/bin/download" "$PLUGIN_DIR/bin/install" + +# List the dependent first and allow parallel jobs so the install graph, rather +# than config order or serial execution, must enforce the dependency. Use the +# supported `nodejs` alias to verify PATH resolution follows the same +# normalization as dependency scheduling. +cat >mise.toml <<'EOF' +[tools] +"asdf:asdf-path-dependent" = { version = "1.0.0", depends = ["nodejs"] } +node = "1.0.0" +EOF + +MISE_JOBS=4 mise install + +DEPENDENT_PATH="$(mise where asdf:asdf-path-dependent)" +DEPENDENCY_BIN="$(mise which node)" + +assert "cat '$DEPENDENT_PATH/download-dependency-path'" "$DEPENDENCY_BIN" +assert "cat '$DEPENDENT_PATH/install-dependency-path'" "$DEPENDENCY_BIN" +assert "tr ':' '\n' <'$DEPENDENT_PATH/install-path' | grep -Fxc '$(dirname "$DEPENDENCY_BIN")'" "1" +assert "mise exec -- asdf-path-dependent" "installed" diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index a93ce5339ca..dcc2a50d878 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::ffi::OsString; use std::fmt::{Debug, Formatter}; use std::fs; @@ -435,7 +435,24 @@ impl Backend for AsdfBackend { async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { let mut sm = self.script_man_for_tv(&ctx.config, &tv).await?; - for p in ctx.ts.list_paths(&ctx.config).await { + // `ctx.ts` is the unresolved install toolset during a combined install, so it + // does not expose tools that just finished installing. Resolve this tool's + // declared dependencies separately so asdf install scripts can execute them + // on the first install (#4384). Keep the existing active-tool paths after the + // dependencies for compatibility, and preserve each toolset's path order. + let dependency_paths = self + .install_dependency_toolset(&ctx.config, &tv) + .await? + .list_paths(&ctx.config) + .await; + let active_paths = ctx.ts.list_paths(&ctx.config).await; + let mut seen = HashSet::new(); + let paths: Vec<_> = dependency_paths + .into_iter() + .chain(active_paths) + .filter(|path| seen.insert(path.clone())) + .collect(); + for p in paths.into_iter().rev() { sm.prepend_path(p); } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 744be950b57..0c3c86194ab 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -2795,7 +2795,7 @@ pub trait Backend: Debug + Send + Sync { // "{{ tools.python.path }}/bin/python3"`) for the tool-level `postinstall` // hook, resolved against this tool's already-installed dependencies. The // config env added above is resolved without tools (`NonToolsOnly`), so it - // omits these; `install_value_toolset` is fully resolved (offline) so + // omits these; `install_dependency_toolset` is fully resolved (offline) so // `{{ tools..path }}` maps to a real install path — `ctx.ts` is the raw, // unresolved install toolset during a combined install. PATH stays owned by // `path_env` below. Best-effort: any resolution error leaves the tool-less @@ -2820,7 +2820,10 @@ pub trait Backend: Debug + Send + Sync { .is_some_and(|deps| !deps.is_empty()); if declares_deps { let base = env_vars.clone(); - let tool_vals = match self.install_value_toolset(&ctx.config, &tv_exact).await { + let tool_vals = match self + .install_dependency_toolset(&ctx.config, &tv_exact) + .await + { Ok(dep_ts) => dep_ts.tool_val_env(&ctx.config, &base).await, Err(e) => Err(e), }; @@ -3132,11 +3135,13 @@ pub trait Backend: Debug + Send + Sync { /// Like [`Self::dependency_toolset`] but also includes this tool's per-instance /// mise.toml `depends` option (`tv.request.options().depends`). `get_dependencies` /// only covers backend/plugin-metadata deps, so a user-declared - /// `gcloud = { depends = ["python"] }` is invisible to `dependency_toolset`. Used - /// to resolve `tools = true` `[env]` value templates like `{{ tools.python.path }}` - /// at install time. Resolved offline; the declared deps are installed before the - /// dependent (depends ordering), so their install paths are present. (#10282) - async fn install_value_toolset( + /// `gcloud = { depends = ["python"] }` is invisible to `dependency_toolset`. + /// Used anywhere an install needs the concrete paths or values of its declared + /// dependencies, including `tools = true` `[env]` value templates and asdf + /// install scripts. Resolved offline; the declared deps are installed before the + /// dependent (depends ordering), so their install paths are present. (#10282, + /// #4384) + async fn install_dependency_toolset( &self, config: &Arc, tv: &ToolVersion, @@ -3148,7 +3153,11 @@ pub trait Backend: Debug + Send + Sync { .collect(); let opts = tv.request.options(); if let Some(user_deps) = opts.core.depends { - names.extend(user_deps); + names.extend( + user_deps + .into_iter() + .flat_map(|dep| BackendArg::from(dep).all_fulls()), + ); } let mut ts: Toolset = config .get_tool_request_set() diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index 5e94cb9eeff..9ebb4010c2a 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -198,7 +198,7 @@ impl Backend for VfoxBackend { // ctx.ts: ctx.ts is the raw install toolset (`Toolset::from(ToolRequestSet)`) // whose `.versions` are empty until `resolve()` runs *after* installs, so its // `tools.*` tera map is empty and `{{ tools.python.path }}` would render "". - // `install_value_toolset` is resolved offline and includes both backend deps + // `install_dependency_toolset` is resolved offline and includes both backend deps // and the per-tool mise.toml `depends` option (`gcloud = { depends = // ["python"] }`) with real install paths, and is install-safe (it uses // `get_tool_request_set()`, not the deadlock-prone `config.get_toolset()`). @@ -207,7 +207,7 @@ impl Backend for VfoxBackend { // `dependency_env`. (#10282, follow-up to #10432) { let base: EnvMap = cmd_env.clone().into_iter().collect(); - let tool_vals = match self.install_value_toolset(&ctx.config, &tv).await { + let tool_vals = match self.install_dependency_toolset(&ctx.config, &tv).await { Ok(dep_ts) => dep_ts.tool_val_env(&ctx.config, &base).await, Err(e) => Err(e), };