1
Fork 0
mirror of https://github.com/RGBCube/nu_scripts synced 2025-07-30 13:47:46 +00:00

Fix(conda.nu): Update Mamba/Conda info parsing and fix syntax errors (#1104)

This PR updates the `modules/virtual_environments/nu_conda_2/conda.nu`
script to address several issues encountered with newer versions of
Mamba/Conda and Nushell.

**Problem:**

The existing script failed when used with recent Mamba versions (e.g.,
Mamba 2.x) due to changes in the JSON output format of `mamba info
--json`. Specifically:
* The key for environment directories is now `"envs directories"`
instead of `"envs_dirs"`.
* The key for the base environment path is now `"base environment"`
instead of `"root_prefix"`.
* The `mamba info --envs --json` command returns minimal information,
requiring separate calls to `mamba info --json` to get necessary details
like `envs_dirs` and `root_prefix`.

Additionally, several Nushell syntax errors were present:
* Incorrect syntax for assigning the result of a multi-branch `if/else`
expression to a variable using `let`.
* Incorrect usage of `run-external`, attempting to pass flags like
`--json` directly instead of as separate string arguments.
*   The `--no-banner` flag is not supported by `mamba info`.

These issues resulted in errors like `Cannot find column 'envs_dirs'`,
`Could not find environment named '...'`, `keyword_missing_arg`, and
`unknown_flag`.

**Solution:**

This PR implements the following fixes:

1.  **Updated `load-conda-info-env`:**
    *   Detects Mamba/Conda/Micromamba correctly.
* For Mamba, makes separate calls to `mamba info --json` and `mamba info
--envs --json`.
* Explicitly extracts `envs_dirs` and `root_prefix` using the correct
key names (`"envs directories"`, `"base environment"`) observed in Mamba
2.x output.
* For Conda, assumes `conda info --json` provides all necessary keys
(`envs_dirs`, `root_prefix`, `envs`).
    *   Includes basic logic for Micromamba (parsing text output).
* Constructs the `$env.CONDA_INFO` record reliably with the required
keys.
2.  **Corrected Nushell Syntax:**
* Fixed the `let cmd_base = ...` assignment by wrapping the `if/else`
expression in parentheses `()`.
* Fixed all `run-external` calls to pass the command and arguments as
separate strings (e.g., `run-external "mamba" "info" "--json"`).
3. **Removed Unsupported Flag:** Removed `--no-banner` from `mamba info`
calls.
4. **Improved Error Handling:** Added checks in `activate` to ensure
`$env.CONDA_INFO` was loaded successfully. Changed `error make` to
`print --stderr` and `return null` in `check-if-env-exists` for
potentially smoother failure modes.
5. **Minor Improvements:** Cleaned up PATH manipulation, improved the
completer function, added comments.

**Testing:**

This version has been tested successfully with:
*   Mamba 2.1.0
*   Nushell 0.103.0
* Activating environments by name (`activate myenv`) and by full path
(`activate /path/to/myenv`).
*   Deactivating environments (`deactivate`).

It should also work correctly with standard Conda installations.
Micromamba support is based on the original script's logic and may
require further testing.

Fixes issues like those encountered during the debugging session leading
to this PR.
This commit is contained in:
Amen 2025-04-22 22:34:46 -04:00 committed by GitHub
parent 67e74c5657
commit 9560df9370
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,23 +1,97 @@
# Nushell Conda/Mamba/Micromamba Activation Script
#
# Based on nu_conda_2 from nushell/nu_scripts, with fixes for:
# - Mamba JSON output changes (key names like "envs directories", "base environment")
# - Nushell syntax errors (let assignment, run-external usage)
# - Tested with Mamba 2.1.0 / Conda (implicitly via Mamba) / Nushell 0.103.0
# Loads conda info once per session and caches in $env.CONDA_INFO
def --env load-conda-info-env [] {
# Check only once per session if CONDA_INFO is already loaded
if (not (has-env CONDA_INFO)) {
export-env {
$env.CONDA_INFO = (
# Determine which command to use (prioritize micromamba > mamba > conda)
# Corrected syntax: Wrap the entire if/else expression in parentheses for assignment
let cmd_base = (
if not (which micromamba | is-empty) {
mut info = micromamba env list --json | from json
let extra_info = micromamba info --json | from json
$info.envs_dirs = $extra_info."envs directories"
$info.root_prefix = $extra_info."base environment"
$info
} else if not (which mamba | is-empty) {
(mamba info --envs --json --no-banner | from json)
} else if not (which conda | is-empty) {
(conda info --envs --json | from json)
"micromamba"
} else {
null
if not (which mamba | is-empty) {
"mamba"
} else {
if not (which conda | is-empty) {
"conda"
} else {
null # No command found
}
}
}
)
}
}
# If a command was found, try to load info
$env.CONDA_INFO = if ($cmd_base == "mamba") {
try {
# Mamba requires separate calls as `info --envs` is minimal
# Corrected syntax: Pass arguments as separate strings to run-external
let mamba_info = (run-external $cmd_base "info" "--json" | from json)
let mamba_envs = (run-external $cmd_base "info" "--envs" "--json" | from json | get envs)
# Explicitly extract keys using names observed in Mamba 2.x output
let root_prefix = ($mamba_info | get "base environment")
let envs_dirs = ($mamba_info | get "envs directories")
# Construct the final record cleanly
{
root_prefix: $root_prefix,
envs_dirs: $envs_dirs,
envs: $mamba_envs
}
} catch { |err|
print --stderr $"WARN: Failed to get info from 'mamba': ($err)"
null # Indicate failure
}
} else if ($cmd_base == "conda") {
# Conda 'info --json' usually contains all necessary info
try {
# Corrected syntax: Pass arguments as separate strings to run-external
let conda_info_all = (run-external $cmd_base "info" "--json" | from json)
# Extract required fields, assuming standard conda output keys
{
root_prefix: ($conda_info_all | get root_prefix),
envs_dirs: ($conda_info_all | get envs_dirs),
envs: ($conda_info_all | get envs)
}
} catch { |err|
print --stderr $"WARN: Failed to get info from 'conda': ($err)"
null # Indicate failure
}
} else if ($cmd_base == "micromamba") {
# Micromamba requires parsing text output and separate calls
try {
# Corrected syntax: Pass arguments as separate strings to run-external
let mi_info_lines = (run-external $cmd_base "info" | lines)
let base = ($mi_info_lines | where $it =~ "Base Environment" | parse "{key}: {value}" | get value | str trim | first)
let dirs_line = ($mi_info_lines | where $it =~ "Envs Directories" | first)
let dirs = if ($dirs_line | is-empty) { [] } else { $dirs_line | parse "{key}: {value}" | get value | str trim | split row " " }
# Corrected syntax: Pass arguments as separate strings to run-external
let env_list = (run-external $cmd_base "env" "list" "--json" | from json | get envs)
# Construct record using consistent key names where possible
{ root_prefix: $base, envs_dirs: $dirs, envs: $env_list }
} catch { |err|
print --stderr $"WARN: Failed to get info from 'micromamba': ($err)"
null
}
} else {
# No command found
print --stderr "WARN: No conda, mamba, or micromamba command found."
null
}
} # End export-env
} # End if not (has-env CONDA_INFO)
}
# Activate conda environment
@ -27,20 +101,38 @@ export def --env activate [
load-conda-info-env
let conda_info = $env.CONDA_INFO
if ($conda_info == null) {
print "Error: No Conda, Mamba or Micromamba install could be found in the environment. Please install either and add them to the environment. See: https://www.nushell.sh/book/environment.html for more info"
print --stderr "Error: Conda/Mamba/Micromamba info could not be loaded. Cannot activate."
return
}
# Check if essential keys were populated correctly
if ($conda_info.envs_dirs == null) or ($conda_info.root_prefix == null) or ($conda_info.envs == null) {
print --stderr "Error: Failed to load essential Conda/Mamba/Micromamba info (envs_dirs, root_prefix, envs). Cannot activate."
return
}
let env_dir = if $env_name != "base" {
if ($env_name | path exists) and (($env_name | path expand) in $conda_info.envs ) {
($env_name | path expand)
# Check if env_name is already a valid path to a known environment
let expanded_env_name = ($env_name | path expand)
if ($env_name | path exists) and ($expanded_env_name in $conda_info.envs ) {
$expanded_env_name
} else {
((check-if-env-exists $env_name $conda_info) | into string)
# Otherwise, try to find the environment by name in the known envs_dirs
(check-if-env-exists $env_name $conda_info)
}
} else {
$conda_info.root_prefix
}
# If check-if-env-exists returned an error (implicitly null/empty), stop
if ($env_dir | is-empty) {
# Error message was already printed by check-if-env-exists
return
}
# Ensure env_dir is a string for path operations
let env_dir = ($env_dir | into string)
let old_path = (system-path | str join (char esep))
let new_path = if (windows?) {
@ -51,14 +143,19 @@ export def --env activate [
let virtual_prompt = $'[($env_name)] '
let new_env = ({
CONDA_DEFAULT_ENV: $env_name
CONDA_PREFIX: $env_dir
CONDA_PROMPT_MODIFIER: $virtual_prompt
CONDA_SHLVL: "1"
# Base environment variables to set
let new_env_base = {
CONDA_DEFAULT_ENV: $env_name,
CONDA_PREFIX: $env_dir,
CONDA_PROMPT_MODIFIER: $virtual_prompt,
CONDA_SHLVL: ((($env.CONDA_SHLVL? | default 0) | into int) + 1 | into string), # Increment shell level
CONDA_OLD_PATH: $old_path
} | merge $new_path)
}
# Merge the new path (PATH or Path)
let new_env = ($new_env_base | merge $new_path)
# Handle prompt modification only if CONDA_NO_PROMPT is not set
let new_env = if not (has-env CONDA_NO_PROMPT) {
let old_prompt_command = if (has-env CONDA_OLD_PROMPT_COMMAND) {
$env.CONDA_OLD_PROMPT_COMMAND
@ -66,117 +163,177 @@ export def --env activate [
if (has-env 'PROMPT_COMMAND') {
$env.PROMPT_COMMAND
} else {
''
null # Use null if no prompt command exists
}
}
# Store the old prompt command only if it wasn't null
let env_update_prompt = if ($old_prompt_command != null) {
{ CONDA_OLD_PROMPT_COMMAND: $old_prompt_command }
} else {
{} # Empty record if no old prompt to save
}
let new_prompt = if (has-env 'PROMPT_COMMAND') {
# Create the new prompt command
let new_prompt = if ($old_prompt_command != null) {
# Check if the old command is a closure
if 'closure' in ($old_prompt_command | describe) {
{|| $'($virtual_prompt)(do $old_prompt_command)' }
} else {
# Assume it's a string or something else printable
{|| $'($virtual_prompt)($old_prompt_command)' }
}
} else {
# If no old prompt, just use the virtual prompt
{|| $'($virtual_prompt)' }
}
$new_env | merge {
CONDA_OLD_PROMPT_COMMAND: $old_prompt_command
PROMPT_COMMAND: $new_prompt
}
# Merge prompt updates into the environment changes
$new_env | merge $env_update_prompt | merge { PROMPT_COMMAND: $new_prompt }
} else {
$new_env | merge { CONDA_OLD_PROMPT_COMMAND: null }
# If prompt is disabled, ensure old prompt command is cleared if it exists
$new_env | merge { CONDA_OLD_PROMPT_COMMAND: null }
}
# Load the calculated environment variables
load-env $new_env
}
# Deactivate currently active conda environment
export def --env deactivate [] {
let path_name = if "PATH" in $env { "PATH" } else { "Path" }
$env.$path_name = $env.CONDA_OLD_PATH
# Check if CONDA_OLD_PATH exists (indicates an environment is active)
if not (has-env CONDA_OLD_PATH) {
print --stderr "WARN: No active conda environment detected to deactivate."
return
}
# Restore the old PATH
let path_name = if "PATH" in $env { "PATH" } else { "Path" }
load-env { ($path_name): $env.CONDA_OLD_PATH }
# Decrement or remove CONDA_SHLVL
let current_shlvl = (($env.CONDA_SHLVL? | default 1) | into int)
if $current_shlvl <= 1 {
hide-env CONDA_SHLVL
} else {
load-env { CONDA_SHLVL: ($current_shlvl - 1 | into string) }
}
# Hide other conda variables
hide-env CONDA_PROMPT_MODIFIER
hide-env CONDA_PREFIX
hide-env CONDA_SHLVL
hide-env CONDA_DEFAULT_ENV
hide-env CONDA_OLD_PATH
$env.PROMPT_COMMAND = (
# Restore the old prompt command if it was saved
if (has-env CONDA_OLD_PROMPT_COMMAND) {
if $env.CONDA_OLD_PROMPT_COMMAND == null {
$env.PROMPT_COMMAND
# If saved value is null, maybe hide PROMPT_COMMAND? Or leave as is?
# Let's assume leaving it is safer if it was null.
# hide-env PROMPT_COMMAND # Optional: uncomment to fully remove prompt if old was null
} else {
$env.CONDA_OLD_PROMPT_COMMAND
load-env { PROMPT_COMMAND: $env.CONDA_OLD_PROMPT_COMMAND }
}
)
hide-env CONDA_OLD_PROMPT_COMMAND
hide-env CONDA_OLD_PROMPT_COMMAND # Hide the saved value itself
}
}
# Finds the full path for a given environment name
def check-if-env-exists [ env_name: string, conda_info: record ] {
let env_dirs = (
$conda_info.envs_dirs |
each { || path join $env_name }
# Get the list of base directories where envs might live
# Ensure it's a list, default to empty list if key is missing or null
let base_env_dirs = ($conda_info | get envs_dirs | default [])
# Construct potential full paths for the named environment in each base directory
let potential_env_paths = (
$base_env_dirs | each { |dir| $dir | path join $env_name }
)
let en = ($env_dirs | each {|en| $conda_info.envs | where $it == $en } | where ($it | length) == 1 | flatten)
if ($en | length) > 1 {
error make --unspanned {msg: $"You have environments in multiple locations: ($en)"}
# Ensure $conda_info.envs is also a list, default to empty
let known_envs = ($conda_info | get envs | default [])
# Find which of these potential paths actually exist in the list of known environments
let found_envs = (
$potential_env_paths | where {|potential_path| $potential_path in $known_envs }
)
# --- Error Checking ---
if ($found_envs | is-empty) {
# Use print --stderr instead of error make to avoid stopping script execution in some contexts
print --stderr $"Error: Could not find environment named '($env_name)' in any of the search directories: ($base_env_dirs | str join ', ')"
return null # Return null to indicate failure
}
if ($en | length) == 0 {
error make --unspanned {msg: $"Could not find given environment: ($env_name)"}
if ($found_envs | length) > 1 {
# Removed debug print
print --stderr $"Error: Found multiple environments named '($env_name)' in different locations: ($found_envs | str join ', ')"
return null # Return null to indicate failure
}
$en.0
# Return the single found path
$found_envs | first
}
# Completer function for environment names
def 'nu-complete conda envs' [] {
load-conda-info-env
$env.CONDA_INFO
| get env_vars.CONDA_ENVS
| lines
| where not ($it | str starts-with '#')
| where not ($it | is-empty)
| each {|entry| $entry | split row ' ' | get 0 }
if (has-env CONDA_INFO) and ($env.CONDA_INFO != null) {
$env.CONDA_INFO
| get envs # Get the list of full paths
# FIX: Pipe the path into path basename
| each { |env_path| $env_path | path basename } # Get just the name part
| append "base" # Always include 'base' as an option
| uniq # Remove duplicates (e.g., if base path was listed)
| sort # Sort the names alphabetically
} else {
[] # Return empty list if info failed to load
}
}
def conda-create-path-windows [env_dir: path] {
# Helper to create PATH string on Windows
def conda-create-path-windows [env_dir: string] {
# Conda on Windows needs a few additional Path elements
let env_path = [
$env_dir
([$env_dir "Scripts"] | path join)
([$env_dir "Library" "mingw-w64"] | path join)
let env_path_parts = [
$env_dir,
([$env_dir "Scripts"] | path join),
([$env_dir "Library" "mingw-w64" "bin"] | path join), # Added bin here
([$env_dir "Library" "usr" "bin"] | path join),
([$env_dir "Library" "bin"] | path join)
([$env_dir "Library" "usr" "bin"] | path join)
]
let new_path = ([$env_path (system-path)]
# Prepend these parts to the existing system path
let new_path = ([$env_path_parts (system-path | split row (char esep))]
| flatten
| uniq # Ensure uniqueness
| str join (char esep))
{ Path: $new_path }
}
def conda-create-path-unix [env_dir: path] {
let env_path = [
([$env_dir "bin"] | path join)
]
# Helper to create PATH string on Unix-like systems
def conda-create-path-unix [env_dir: string] {
let env_bin_path = ([$env_dir "bin"] | path join)
let new_path = ([$env_path $env.PATH]
# Prepend the env bin path to the existing PATH
let new_path = ([$env_bin_path ($env.PATH | split row (char esep))]
| flatten
| uniq # Ensure uniqueness
| str join (char esep))
{ PATH: $new_path }
}
# Helper to check if running on Windows
def windows? [] {
$nu.os-info.name == 'windows'
}
# Helper to get the system PATH variable name correctly
def system-path [] {
if "PATH" in $env { $env.PATH } else { $env.Path }
}
# Helper to check if an environment variable exists
def has-env [name: string] {
$name in $env
}
}