Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-37354: Make Powershell Activate.ps1 script static to allow for signing #14967

Merged
merged 6 commits into from
Aug 12, 2019
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
231 changes: 203 additions & 28 deletions Lib/venv/scripts/common/Activate.ps1
Original file line number Diff line number Diff line change
@@ -1,56 +1,231 @@
function Script:add-bin([string]$envPath) {
$binPath = Join-Path -Path $env:VIRTUAL_ENV -ChildPath '__VENV_BIN_NAME__'
return ($binPath, $envPath) -join [IO.Path]::PathSeparator
}
<#
.Synopsis
Activate a Python virtual environment for the current Powershell session.

.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.

.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.

.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').

.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.

.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.

.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.

.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.


#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)

<# Function declarations --------------------------------------------------- #>

<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.

.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.

#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
if (Test-Path function:_OLD_VIRTUAL_PROMPT) {
copy-item function:_OLD_VIRTUAL_PROMPT function:prompt
remove-item function:_OLD_VIRTUAL_PROMPT

# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}

if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) {
copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME
remove-item env:_OLD_VIRTUAL_PYTHONHOME
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}

if (Test-Path env:_OLD_VIRTUAL_PATH) {
copy-item env:_OLD_VIRTUAL_PATH env:PATH
remove-item env:_OLD_VIRTUAL_PATH
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}

if (Test-Path env:VIRTUAL_ENV) {
remove-item env:VIRTUAL_ENV
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}

# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}

# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}

<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.

For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.

If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.

.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"

# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue

# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }

if ($pyvenvConfigPath) {

Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath

$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]

# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0,1))) {
$val = $val.Substring(1, $val.Length - 2)
}

$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
d3r3kk marked this conversation as resolved.
Show resolved Hide resolved
}


if (!$NonDestructive) {
# Self destruct!
remove-item function:deactivate
<# Begin Activate script --------------------------------------------------- #>

# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath

Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"

# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
} else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
$VenvDir = $VenvDir.Insert($VenvDir.Length, "/")
Write-Verbose "VenvDir=$VenvDir"
}

# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir

# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
} else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line that gave me the most nightmares when thinking about how to do this, because the prompt value in the config file is quoted and escaped (basically, it's repr(prompt)), so we need to do more processing here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @zooba!

Agreed that the escaped character problem is a bit tricky in the context of Powershell... \n is not really available in Powershell, for instance, but could be replaced with the back-tick-quote `. Finding and replacing all types of escaped characters (newlines, tabs, colors?) might be a bit of a deep dark hole (there be ANSI-escaped coloured dragons there)!

Would it be appropriate to add handling of escaped characters in a different issue/PR? I don't expect that the prior version of this Activate.ps1 script handled this problem particularly well either (a brief test with the latest 3.8 beta shows a few hiccups right away).
Of course, I would be most happy to tackle that problem next!

What sample input would show the shortcoming of this implementation, so that I can write some unit tests and fix it correctly?

Just from thinking about it briefly I can think up input that contains:

  • newlines (\n, \r\n)
  • tabs (\t)
  • colours? (\u001b[31m, ... , \u001b[0m)

Ideally I'd love to show something that would have the class of input we need to be concerned about working with, and have it handled at least as well as the prior implementation handled it.

A specific scenario that is trivial to setup and play with, for example the newline escaped character \n in the prompt:

PS /home/d3r3kk/dev/venvtest> ../cpython/python -m venv --prompt "\nd3r3kk\n" .newline_prompt
PS /home/d3r3kk/dev/venvtest> cat ./.newline_prompt/pyvenv.cfg
home = /home/d3r3kk/dev/cpython
include-system-site-packages = false
version = 3.9.0
prompt = '\\nd3r3kk\\n'
PS /home/d3r3kk/dev/venvtest> ./.newline_prompt/bin/Activate.ps1
(\\nd3r3kk\\n) PS /home/d3r3kk/dev/venvtest> deactivate
PS /home/d3r3kk/dev/venvtest> exit

...and then in bash...

d3r3kk@deb:~/dev/venvtest$ . .newline_prompt/bin/activate
(
d3r3kk
) d3r3kk@deb:~/dev/venvtest$ deactivate
d3r3kk@deb:~/dev/venvtest$ 

...as we can see, the \\n being stored in the pyvenv.cfg is written to the prompt verbatim in Powershell with this change (and is also in the latest 3.8 beta release).

Note: it is possible in Powershell to replace some escaped character's \\ with a ` and it will work. (But definitely not colours, those are weird special creatures!).

}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}

Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"

# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive

$env:VIRTUAL_ENV="__VENV_DIR__"
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir

if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {

Write-Verbose "Setting prompt to '$Prompt'"

if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) {
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT {""}
copy-item function:prompt function:_OLD_VIRTUAL_PROMPT
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt

function global:prompt {
Write-Host -NoNewline -ForegroundColor Green '__VENV_PROMPT__'
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
}

# Clear PYTHONHOME
if (Test-Path env:PYTHONHOME) {
copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME
remove-item env:PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}

# Add the venv to the PATH
copy-item env:PATH env:_OLD_VIRTUAL_PATH
$env:PATH = add-bin $env:PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make Activate.ps1 Powershell script static to allow for signing it.