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
14 changes: 14 additions & 0 deletions docs/dev-tools/backends/asdf.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
84 changes: 84 additions & 0 deletions e2e/backend/test_asdf_install_dependency_path
Original file line number Diff line number Diff line change
@@ -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"
21 changes: 19 additions & 2 deletions src/backend/asdf.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -435,7 +435,24 @@ impl Backend for AsdfBackend {
async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result<ToolVersion> {
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)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
.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);
}

Expand Down
25 changes: 17 additions & 8 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<dep>.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
Expand All @@ -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),
};
Expand Down Expand Up @@ -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<Config>,
tv: &ToolVersion,
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/backend/vfox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()`).
Expand All @@ -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),
};
Expand Down
Loading