add powershell init and activation

This commit is contained in:
Wolf Vollprecht 2020-06-30 12:09:45 +02:00
parent 110add4c46
commit f0ac1b2132
6 changed files with 449 additions and 4 deletions

301
data/Mamba.psm1 Normal file
View File

@ -0,0 +1,301 @@
R"MAMBARAW(
## ENVIRONMENT MANAGEMENT ######################################################
<#
.SYNOPSIS
Obtains a list of valid conda environments.
.EXAMPLE
Get-CondaEnvironment
.EXAMPLE
genv
#>
function Get-CondaEnvironment {
[CmdletBinding()]
param();
begin {}
process {
# NB: the JSON output of conda env list does not include the names
# of each env, so we need to parse the fragile output instead.
& $Env:CONDA_EXE $Env:_CE_M $Env:_CE_CONDA env list | `
Where-Object { -not $_.StartsWith("#") } | `
Where-Object { -not $_.Trim().Length -eq 0 } | `
ForEach-Object {
$envLine = $_ -split "\s+";
$Active = $envLine[1] -eq "*";
[PSCustomObject] @{
Name = $envLine[0];
Active = $Active;
Path = if ($Active) {$envLine[2]} else {$envLine[1]};
} | Write-Output;
}
}
end {}
}
<#
.SYNOPSIS
Adds the entries of sys.prefix to PATH and returns the old PATH.
.EXAMPLE
$OldPath = Add-Sys-Prefix-To-Path
#>
function Add-Sys-Prefix-To-Path() {
$OldPath = $Env:PATH;
if ($Env:OS -eq 'Windows_NT') {
$Env:PATH = $Env:MAMBA_ROOT_PREFIX + ';' +
$Env:MAMBA_ROOT_PREFIX + '\Library\mingw-w64\bin;' +
$Env:MAMBA_ROOT_PREFIX + '\Library\usr\bin;' +
$Env:MAMBA_ROOT_PREFIX + '\Library\bin;' +
$Env:MAMBA_ROOT_PREFIX + '\Scripts;' +
$Env:MAMBA_ROOT_PREFIX + '\bin;' + $Env:PATH;
} else {
$Env:PATH = $Env:MAMBA_ROOT_PREFIX + '/bin:' + $Env:PATH;
}
return $OldPath;
}
<#
.SYNOPSIS
Activates a conda environment, placing its commands and packages at
the head of $Env:PATH.
.EXAMPLE
Enter-MambaEnvironment base
.EXAMPLE
etenv base
.NOTES
This command does not currently support activating environments stored
in a non-standard location.
#>
function Enter-MambaEnvironment {
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)][switch]$Stack,
[Parameter(Position=0)][string]$Name
);
begin {
$OldPath = Add-Sys-Prefix-To-Path;
If (-Not $Name) {
$Name = "base";
}
If ($Stack) {
$activateCommand = (& $Env:MAMBA_EXE shell activate -s powershell --stack --prefix $Name | Out-String);
} Else {
$activateCommand = (& $Env:MAMBA_EXE shell activate -s powershell --prefix $Name | Out-String);
}
$Env:PATH = $OldPath;
Write-Verbose "[micromamba shell activate --shell powershell --prefix $Name]`n$activateCommand";
Invoke-Expression -Command $activateCommand;
}
process {}
end {}
}
<#
.SYNOPSIS
Deactivates the current conda environment, if any.
.EXAMPLE
Exit-MambaEnvironment
.EXAMPLE
exenv
#>
function Exit-MambaEnvironment {
[CmdletBinding()]
param();
begin {
$OldPath = Add-Sys-Prefix-To-Path;
$deactivateCommand = (& $Env:MAMBA_EXE shell deactivate -s powershell | Out-String);
$Env:PATH = $OldPath;
# If deactivate returns an empty string, we have nothing more to do,
# so return early.
if ($deactivateCommand.Trim().Length -eq 0) {
return;
}
Write-Verbose "[micromamba shell deactivate -s powershell]`n$deactivateCommand";
Invoke-Expression -Command $deactivateCommand;
}
process {}
end {}
}
## CONDA WRAPPER ###############################################################
<#
.SYNOPSIS
conda is a tool for managing and deploying applications, environments
and packages.
.PARAMETER Command
Subcommand to invoke.
.EXAMPLE
conda install toolz
#>
function Invoke-Mamba() {
# Don't use any explicit args here, we'll use $args and tab completion
# so that we can capture everything, INCLUDING short options (e.g. -n).
if ($Args.Count -eq 0) {
# No args, just call the underlying conda executable.
& $Env:MAMBA_EXE;
}
else {
$Command = $Args[0];
if ($Args.Count -ge 2) {
$OtherArgs = $Args[1..($Args.Count - 1)];
} else {
$OtherArgs = @();
}
switch ($Command) {
"activate" {
Enter-MambaEnvironment @OtherArgs;
}
"deactivate" {
Exit-MambaEnvironment;
}
default {
# There may be a command we don't know want to handle
# differently in the shell wrapper, pass it through
# verbatim.
$OldPath = Add-Sys-Prefix-To-Path;
& $Env:MAMBA_EXE $Command @OtherArgs;
$Env:PATH = $OldPath;
}
}
}
}
## TAB COMPLETION ##############################################################
# We borrow the approach used by posh-git, in which we override any existing
# functions named TabExpansion, look for commands we can complete on, and then
# default to the previously defined TabExpansion function for everything else.
if (Test-Path Function:\TabExpansion) {
# Since this technique is common, we encounter an infinite loop if it's
# used more than once unless we give our backup a unique name.
Rename-Item Function:\TabExpansion CondaTabExpansionBackup
}
function Expand-CondaEnv() {
param(
[string]
$Filter
);
$ValidEnvs = Get-CondaEnvironment;
$ValidEnvs `
| Where-Object { $_.Name -like "$filter*" } `
| ForEach-Object { $_.Name } `
| Write-Output;
$ValidEnvs `
| Where-Object { $_.Path -like "$filter*" } `
| ForEach-Object { $_.Path } `
| Write-Output;
}
function Expand-CondaSubcommands() {
param(
[string]
$Filter
);
$ValidCommands = Invoke-Mamba shell.powershell commands;
# Add in the commands defined within this wrapper, filter, sort, and return.
$ValidCommands + @('activate', 'deactivate') `
| Where-Object { $_ -like "$Filter*" } `
| Sort-Object `
| Write-Output;
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Pull out conda commands we recognize first before falling through
# to the general patterns for conda itself.
"^micromamba activate (.*)" { Expand-CondaEnv $lastWord; break; }
"^etenv (.*)" { Expand-CondaEnv $lastWord; break; }
# If we got down to here, check arguments to conda itself.
"^conda (.*)" { Expand-CondaSubcommands $lastWord; break; }
# Finally, fall back on existing tab expansion.
default {
if (Test-Path Function:\CondaTabExpansionBackup) {
CondaTabExpansionBackup $line $lastWord
}
}
}
}
## PROMPT MANAGEMENT ###########################################################
<#
.SYNOPSIS
Modifies the current prompt to show the currently activated conda
environment, if any.
.EXAMPLE
Add-CondaEnvironmentToPrompt
Causes the current session's prompt to display the currently activated
conda environment.
#>
# We use the same procedure to nest prompts as we did for nested tab completion.
if (Test-Path Function:\prompt) {
Rename-Item Function:\prompt CondaPromptBackup
} else {
function CondaPromptBackup() {
# Restore a basic prompt if the definition is missing.
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
}
}
function Add-CondaEnvironmentToPrompt() {
function global:prompt() {
if ($Env:CONDA_PROMPT_MODIFIER) {
$Env:CONDA_PROMPT_MODIFIER | Write-Host -NoNewline
}
CondaPromptBackup;
}
}
## ALIASES #####################################################################
New-Alias micromamba Invoke-Mamba -Force
New-Alias genv Get-CondaEnvironment -Force
New-Alias etenv Enter-MambaEnvironment -Force
New-Alias exenv Exit-MambaEnvironment -Force
## EXPORTS ###################################################################
Export-ModuleMember `
-Alias * `
-Function `
Invoke-Mamba, `
Get-CondaEnvironment, Add-CondaEnvironmentToPrompt, `
Enter-MambaEnvironment, Exit-MambaEnvironment, `
prompt
# We don't export TabExpansion as it's currently not implemented for Micromamba
# TabExpansion
)MAMBARAW"

3
data/mamba_hook.ps1 Normal file
View File

@ -0,0 +1,3 @@
R"MAMBARAW(
Import-Module "$Env:MAMBA_ROOT_PREFIX\condabin\Mamba.psm1"
)MAMBARAW"

View File

@ -121,6 +121,7 @@ namespace mamba
class PowerShellActivator
: public Activator
{
public:
PowerShellActivator() = default;
virtual ~PowerShellActivator() = default;

View File

@ -44,6 +44,13 @@ constexpr const char _mamba_activate_bat[] =
constexpr const char mamba_hook_bat[] =
#include "../data/mamba_hook.bat"
;
constexpr const char mamba_hook_ps1[] =
#include "../data/mamba_hook.ps1"
;
constexpr const char mamba_psm1[] =
#include "../data/Mamba.psm1"
;
namespace mamba
{
@ -145,7 +152,7 @@ namespace mamba
"([\\s\\S]*?)"
"# <<< mamba initialize <<<(?:\n|\r\n)?");
static std::regex CONDA_INITIALIZE_PS_RE_BLOCK("^#region mamba initialize(?:\n|\r\n)"
static std::regex CONDA_INITIALIZE_PS_RE_BLOCK("#region mamba initialize(?:\n|\r\n)?"
"([\\s\\S]*?)"
"#endregion(?:\n|\r\n)?");
@ -220,7 +227,7 @@ namespace mamba
std::string original_content = rc_content;
std::string conda_init_content = rcfile_content(conda_prefix, shell, mamba_exe);
Console::stream() << "Adding (or replacing) the following in your " <<file_path << " file\n"
Console::stream() << "Adding (or replacing) the following in your " << file_path << " file\n"
<< termcolor::colorize << termcolor::green << conda_init_content << termcolor::reset;
std::string result = std::regex_replace(
@ -289,6 +296,86 @@ namespace mamba
std::ofstream mamba_hook_bat_f(root_prefix / "condabin" / "mamba_hook.bat");
mamba_hook_bat_f << hook_content;
}
else if (shell == "powershell")
{
fs::create_directories(root_prefix / "condabin");
std::ofstream mamba_hook_f(root_prefix / "condabin" / "mamba_hook.ps1");
mamba_hook_f << mamba_hook_ps1;
std::ofstream mamba_psm1_f(root_prefix / "condabin" / "Mamba.psm1");
mamba_psm1_f << mamba_psm1;
}
}
std::string powershell_contents(const fs::path& conda_prefix)
{
fs::path self_exe = get_self_exe_path();
std::stringstream out;
out << "#region mamba initialize\n";
out << "# !! Contents within this block are managed by 'mamba shell init' !!\n";
out << "$Env:MAMBA_ROOT_PREFIX = " << conda_prefix << "\n";
out << "$Env:MAMBA_EXE = " << self_exe << "\n";
out << "(& " << self_exe << " 'shell' 'hook' -s 'powershell' -p " << conda_prefix << ") | Out-String | Invoke-Expression\n";
out << "#endregion\n";
return out.str();
}
bool init_powershell(const fs::path& profile_path, const fs::path& conda_prefix, bool reverse=false)
{
// NB: the user may not have created a profile. We need to check
// if the file exists first.
std::string profile_content, profile_original_content;
if (fs::exists(profile_path))
{
profile_content = read_contents(profile_path);
profile_original_content = profile_content;
}
if (reverse)
{
profile_content = std::regex_replace(
profile_content,
CONDA_INITIALIZE_PS_RE_BLOCK,
""
);
}
else
{
// # Find what content we need to add.
std::string conda_init_content = powershell_contents(conda_prefix);
std::cout << "Adding: \n" << conda_init_content << std::endl;
Console::stream() << "Adding (or replacing) the following in your " << profile_path << " file\n"
<< termcolor::colorize << termcolor::green << conda_init_content << termcolor::reset;
if (profile_content.find("#region mamba initialize") == profile_content.npos)
{
profile_content += "\n" + conda_init_content + "\n";
}
else
{
profile_content = std::regex_replace(
profile_content,
CONDA_INITIALIZE_PS_RE_BLOCK,
conda_init_content
);
}
}
if (profile_content != profile_original_content)
{
if (!Context::instance().dry_run)
{
if (!fs::exists(profile_path.parent_path()))
{
fs::create_directories(profile_path.parent_path());
}
std::ofstream out(profile_path);
out << profile_content;
return true;
}
}
return false;
}
void init_shell(const std::string& shell, const fs::path& conda_prefix)
@ -314,6 +401,46 @@ namespace mamba
init_cmd_exe_registry(L"Software\\Microsoft\\Command Processor", conda_prefix, false);
#endif
}
else if (shell == "powershell")
{
std::string profile_var("$PROFILE.CurrentUserAllHosts");
// if (for_system)
// profile = "$PROFILE.AllUsersAllHosts"
// There's several places PowerShell can store its path, depending
// on if it's Windows PowerShell, PowerShell Core on Windows, or
// PowerShell Core on macOS/Linux. The easiest way to resolve it is to
// just ask different possible installations of PowerShell where their
// profiles are.
auto find_powershell_paths = [&profile_var](const std::string& exe) -> std::string
{
try
{
auto obuf = subprocess::check_output({exe, "-NoProfile", "-Command", profile_var});
std::string res(obuf.buf.data());
return std::string(strip(res));
}
catch (...)
{
return "";
}
};
std::string profile_path, exe;
for (auto& iter_exe : std::vector<std::string>{"powershell", "pwsh", "pwsh-preview"})
{
auto res = find_powershell_paths(iter_exe);
if (!res.empty())
{
profile_path = res;
exe = iter_exe;
}
}
std::cout << "Found powershell at " << exe << " and user profile at " << profile_path << std::endl;
init_powershell(profile_path, conda_prefix, false);
}
else
{
throw std::runtime_error("Support for other shells not yet implemented.");

View File

@ -263,6 +263,7 @@ namespace mamba
// path_list[0:0] = list(self.path_conversion(self._get_path_dirs(prefix)))
std::vector<fs::path> final_path = get_path_dirs(prefix);
final_path.insert(final_path.end(), path_list.begin(), path_list.end());
final_path.erase(std::unique(final_path.begin(), final_path.end()), final_path.end());
std::string result = join(env::pathsep(), final_path);
return result;
@ -292,11 +293,14 @@ namespace mamba
{
final_path = get_path_dirs(new_prefix);
final_path.insert(final_path.end(), current_path.begin(), current_path.end());
// remove duplicates
final_path.erase(std::unique(final_path.begin(), final_path.end()), final_path.end());
std::string result = join(env::pathsep(), final_path);
return result;
}
else
{
current_path.erase(std::unique(current_path.begin(), current_path.end()), current_path.end());
std::string result = join(env::pathsep(), current_path);
return result;
}
@ -831,7 +835,7 @@ namespace mamba
fs::path PowerShellActivator::hook_source_path()
{
return Context::instance().root_prefix / "shell" / "condabin" / "mamba-hook.ps1";
return Context::instance().root_prefix / "condabin" / "mamba_hook.ps1";
}
std::pair<std::string, std::string> PowerShellActivator::update_prompt(const std::string& conda_prompt_modifier)
@ -855,7 +859,7 @@ namespace mamba
for (const std::string& uvar : env_transform.unset_vars)
{
out << "Remove-Item Env:/" << uvar << "=\n";
out << "Remove-Item Env:/" << uvar << "\n";
}
for (const auto& [skey, svar] : env_transform.set_vars)

View File

@ -152,6 +152,15 @@ void init_shell_parser(CLI::App* subcom)
{
activator = std::make_unique<mamba::CmdExeActivator>();
}
else if (shell_options.shell_type == "powershell")
{
activator = std::make_unique<mamba::PowerShellActivator>();
}
else
{
std::cout << "Currently allowed values are: bash, zsh, cmd.exe & powershell" << std::endl;
exit(1);
}
if (shell_options.action == "init")
{
init_shell(shell_options.shell_type, shell_options.prefix);