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
51 changes: 51 additions & 0 deletions e2e/backend/test_pipx_postinstall_hook
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
require_cmd python3

# Test that pipx postinstall hooks can invoke the installed tool when using uvx.
# This validates the fix for https://github.com/jdx/mise/discussions/7864
# The bug was that uvx creates venv symlinks pointing to minor version paths
# (e.g., python/3.12/) but the symlink didn't exist yet when postinstall ran.

# Create a system pipx that always fails and push it to the front of PATH
cat >"$HOME/bin/pipx" <<'EOF'
#!/usr/bin/env bash
echo "CALL TO SYSTEM pipx! args: $*" >&2
exit 1
EOF
chmod +x "$HOME"/bin/pipx
export PATH="$HOME/bin:$PATH"

# Just to be sure...
assert_fail "pipx"

# Use precompiled python and uvx (the default)
export MISE_PYTHON_COMPILE=0
export MISE_PIPX_UVX=1

# Set up mise-managed Python with a pipx package that has a postinstall hook
# The hook invokes the installed tool, which requires the Python symlink to work
cat >.mise.toml <<EOF
[tools]
python = "3.12.3"
uv = "0.5.5"

[tools."pipx:mkdocs"]
version = "1.6.0"
postinstall = "mkdocs --version"
EOF

# Install the tools - this should succeed with the postinstall hook
mise install

# Verify mkdocs is installed and works
assert_contains "mise x -- mkdocs --version" "1.6.0"

# Verify the minor version symlink was created early (before runtime_symlinks::rebuild)
MINOR_VERSION_DIR="$MISE_DATA_DIR/installs/python/3.12"
if [[ -L $MINOR_VERSION_DIR ]]; then
ok "Minor version symlink 3.12 was created"
elif [[ -d $MINOR_VERSION_DIR ]]; then
ok "Minor version directory 3.12 exists"
else
fail "Minor version symlink/directory 3.12 does not exist"
fi
70 changes: 58 additions & 12 deletions src/backend/pipx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,50 @@ fn path_with_minor_version(path: &Path) -> Option<PathBuf> {
}
}

/// Ensure the minor version symlink exists for a Python installation path.
/// For example, if the path is `.../python/3.12.1/bin/python3`, this ensures
/// that `.../python/3.12` exists as a symlink to `./3.12.1`.
///
/// This is normally done by `runtime_symlinks::rebuild()`, but that runs after
/// postinstall hooks. We need to create it early so that venv symlinks work
/// immediately for postinstall hooks.
#[cfg(unix)]
fn ensure_minor_version_symlink(full_version_path: &Path) -> Result<()> {
// Extract version components from path like .../python/3.12.1/bin/python3
// Use same regex pattern as path_with_minor_version for consistency
let re = regex!(r"/python/(\d+)\.(\d+)\.(\d+)/");
let path_str = match full_version_path.to_str() {
Some(s) => s,
None => return Ok(()),
};

let caps = match re.captures(path_str) {
Some(c) => c,
None => return Ok(()),
};

let minor_version = format!("{}.{}", &caps[1], &caps[2]); // e.g., "3.12"
let full_version = format!("{}.{}.{}", &caps[1], &caps[2], &caps[3]); // e.g., "3.12.1"

let installs_dir = &*env::MISE_INSTALLS_DIR;
let python_installs = installs_dir.join("python");
let minor_version_dir = python_installs.join(&minor_version);
let full_version_dir = python_installs.join(&full_version);

// Only create if the minor version symlink doesn't exist but the full version does
if !minor_version_dir.exists() && full_version_dir.exists() {
trace!(
"Creating early minor version symlink: {:?} -> ./{:?}",
minor_version_dir, full_version
);
// Use relative symlink with "./" prefix like runtime_symlinks does
// This allows is_runtime_symlink() to identify it for cleanup/updates
file::make_symlink(&PathBuf::from(".").join(&full_version), &minor_version_dir)?;
}

Ok(())
}

/// Fix the venv Python symlinks to use mise's minor version path
/// This allows patch upgrades (3.12.1 → 3.12.2) to work without reinstalling
///
Expand Down Expand Up @@ -564,18 +608,20 @@ fn fix_venv_python_symlink(install_path: &Path, pkg_name: &str) -> Result<()> {
continue; // Leave non-mise Python alone (homebrew, uv, etc.)
}

if let Some(minor_path) = path_with_minor_version(&target) {
// The minor version symlink (e.g., python/3.12) might not exist yet
// as runtime_symlinks::rebuild runs after all tools are installed.
// Check if the full version path exists instead (e.g., python/3.12.12/bin/python3).
// The symlink might be temporarily broken but will work after runtime symlinks are created.
if target.exists() {
trace!(
"Updating venv Python symlink {:?} to use minor version: {:?}",
symlink_path, minor_path
);
file::make_symlink(&minor_path, &symlink_path)?;
}
if let Some(minor_path) = path_with_minor_version(&target)
&& target.exists()
{
// Create the minor version symlink (e.g., python/3.12 -> python/3.12.1)
// if it doesn't exist yet. This is normally done by runtime_symlinks::rebuild,
// but that runs after postinstall hooks, so we need to create it now
// to ensure the venv symlink works immediately for postinstall hooks.
ensure_minor_version_symlink(&target)?;

trace!(
"Updating venv Python symlink {:?} to use minor version: {:?}",
symlink_path, minor_path
);
file::make_symlink(&minor_path, &symlink_path)?;
}
}
}
Expand Down
Loading