diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15c2050a480fd..a3400aa8193e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1011,6 +1011,54 @@ jobs: run: | (& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression + integration-test-nushell: + timeout-minutes: 10 + needs: build-binary-linux-libc + name: "integration test | activate nushell venv" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Install nushell + env: + # This token only needs read access to the GitHub repository nushell/nushell. + # This token is used (via gh-cli) to avoid hitting GitHub REST API rate limits. + GITHUB_TOKEN: ${{ github.token }} + run: |- + # get latest nushell tag name + nu_latest=$(gh release list --repo nushell/nushell --limit 1 --exclude-pre-releases --exclude-drafts --json "tagName" --jq '.[0].tagName') + # trim any trailing whitespace from output + nu_tag=${nu_latest%%[[:space:]]*} + + # download binary for x86_64-unknown-linux-gnu target + gh release download ${nu_tag} --repo nushell/nushell --pattern "nu-${nu_tag}-x86_64-unknown-linux-gnu.tar.gz" + + # extract nu binary from tar.gz + tar -xf "nu-${nu_tag}-x86_64-unknown-linux-gnu.tar.gz" + # make the binary executable + chmod +x "./nu-${nu_tag}-x86_64-unknown-linux-gnu/nu" + # add it to PATH + echo "${{ github.workspace }}/nu-${nu_tag}-x86_64-unknown-linux-gnu" >> "${GITHUB_PATH}" + + - name: Download binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-linux-libc-${{ github.sha }} + + - name: Prepare binary + run: chmod +x ./uv + + - name: Create venv + # The python version is arbitrary for this test. + # We only want to ensure the activation script behaves properly + run: ./uv venv + + - name: Activate venv + shell: nu {0} + run: overlay use ${{ github.workspace }}/.venv/bin/activate.nu + integration-test-conda: timeout-minutes: 10 needs: build-binary-linux-libc diff --git a/crates/uv-virtualenv/README.md b/crates/uv-virtualenv/README.md index 41ca24630b865..9573d7fa33c6f 100644 --- a/crates/uv-virtualenv/README.md +++ b/crates/uv-virtualenv/README.md @@ -1,3 +1,55 @@ # uv-virtualenv `uv-virtualenv` is a rust library to create Python virtual environments. It also has a CLI. + +## Syncing with upstream virtualenv activation scripts + +This crate tries to stay in sync with pypa/virtualenv project's activation scrips. However, there +are some deviations that are specific to this crate's implementation. + +### License disclaimers added + +This crate includes license information at the top of each activation script. This is done in +accordance with the pypa/virtualenv project's MIT License. Do not remove the declarative license +comments from this crate's activation scripts. + +### Placeholder names are slightly different + +Note, these activation scripts are actually templates that are populated with certain values when a +virtual environment is created. + +In upstream, the placeholder names are found in +[`virtualenv.activation.ViaTemplateActivator.replacements()`][upstream-placeholders]. + +In this crate, the placeholder names are found in +[`uv_virtualenv::virtualenv::create()`][crate-placeholders] + +[upstream-placeholders]: + https://github.com/pypa/virtualenv/blob/dad9369e97f5aef7e33777b18dcdb51b1fdac7bd/src/virtualenv/activation/via_template.py#L43 +[crate-placeholders]: + https://github.com/astral-sh/uv/blob/d8f3f03198308be53de51a3a297c85566eabb084/crates/uv-virtualenv/src/virtualenv.rs#L462 + +It is important that the placeholder names (as used in the activation scripts) conform to the +placeholders names used in [this crate's source][crate-placeholders]. + +### Relocatable virtual environments + +This crate uses some additional tweaks in the activation scripts to ensure the virtual environment +is relocatable. Thus, the patch in [astral-sh/uv#5640] shall be retained. + +[astral-sh/uv#5640]: https://github.com/astral-sh/uv/pull/5640 + +### TCL/TK library locations + +The patches in upstream virtualenv ([pypa/virtualenv#2928] and [pypa/virtualenv#2940]) implement +dynamically locating the TCL/TK libraries of a base Python distribution (see [upstream +approach][upstream-tcl/tk-approach]). + +[pypa/virtualenv#2928]: https://github.com/pypa/virtualenv/pull/2928 +[pypa/virtualenv#2940]: https://github.com/pypa/virtualenv/pull/2940 +[upstream-tcl/tk-approach]: + https://github.com/pypa/virtualenv/blob/dad9369e97f5aef7e33777b18dcdb51b1fdac7bd/src/virtualenv/discovery/py_info.py#L140 + +This upstream implementation is considered an undesirable complexity in this project. As such, the +upstream TCL/TK related patches shall be omitted when syncing activation scripts with upstream +sources. diff --git a/crates/uv-virtualenv/src/activator/activate b/crates/uv-virtualenv/src/activator/activate index 3c6b471b0e0d7..b1b115133624d 100644 --- a/crates/uv-virtualenv/src/activator/activate +++ b/crates/uv-virtualenv/src/activator/activate @@ -127,4 +127,4 @@ pydoc () { # The hash command must be called to get it to forget past # commands. Without forgetting past commands the $PATH changes # we made may not be respected -hash -r 2>/dev/null +hash -r 2>/dev/null || true diff --git a/crates/uv-virtualenv/src/activator/activate.bat b/crates/uv-virtualenv/src/activator/activate.bat index 90a15ec44798f..13c40ffbdaa91 100644 --- a/crates/uv-virtualenv/src/activator/activate.bat +++ b/crates/uv-virtualenv/src/activator/activate.bat @@ -21,8 +21,9 @@ @REM This file is UTF-8 encoded, so we need to update the current code page while executing it @for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do @set _OLD_CODEPAGE=%%a + @if defined _OLD_CODEPAGE ( - @"%SystemRoot%\System32\chcp.com" 65001 > nul + "%SystemRoot%\System32\chcp.com" 65001 > nul ) @for %%i in ("{{ VIRTUAL_ENV_DIR }}") do @set "VIRTUAL_ENV=%%~fi" @@ -64,8 +65,7 @@ @set "PATH=%VIRTUAL_ENV%\{{ BIN_NAME }};%PATH%" -:END @if defined _OLD_CODEPAGE ( - @"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul + "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul @set _OLD_CODEPAGE= ) diff --git a/crates/uv-virtualenv/src/activator/activate.fish b/crates/uv-virtualenv/src/activator/activate.fish index 423a86b5bce22..9968496dfc4c5 100644 --- a/crates/uv-virtualenv/src/activator/activate.fish +++ b/crates/uv-virtualenv/src/activator/activate.fish @@ -39,7 +39,7 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen # reset old environment variables if test -n "$_OLD_VIRTUAL_PATH" # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling - if test (echo $FISH_VERSION | head -c 1) -lt 3 + if test (string sub -s 1 -l 1 $FISH_VERSION) -lt 3 set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH") else set -gx PATH $_OLD_VIRTUAL_PATH @@ -82,7 +82,7 @@ deactivate nondestructive set -gx VIRTUAL_ENV '{{ VIRTUAL_ENV_DIR }}' # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling -if test (echo $FISH_VERSION | head -c 1) -lt 3 +if test (string sub -s 1 -l 1 $FISH_VERSION) -lt 3 set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH) else set -gx _OLD_VIRTUAL_PATH $PATH diff --git a/crates/uv-virtualenv/src/activator/activate.nu b/crates/uv-virtualenv/src/activator/activate.nu index 6ee2c22716a74..a1306799499aa 100644 --- a/crates/uv-virtualenv/src/activator/activate.nu +++ b/crates/uv-virtualenv/src/activator/activate.nu @@ -19,97 +19,82 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# virtualenv activation module -# Activate with `overlay use activate.nu` -# Deactivate with `deactivate`, as usual +# virtualenv activation module: +# - Activate with `overlay use activate.nu` +# - Deactivate with `deactivate`, as usual # -# To customize the overlay name, you can call `overlay use activate.nu as foo`, -# but then simply `deactivate` won't work because it is just an alias to hide -# the "activate" overlay. You'd need to call `overlay hide foo` manually. +# To customize the overlay name, you can call `overlay use activate.nu as foo`, but then simply `deactivate` won't work +# because it is just an alias to hide the "activate" overlay. You'd need to call `overlay hide foo` manually. + +module warning { + export-env { + const file = path self + error make -u { + msg: $"`($file | path basename)` is meant to be used with `overlay use`, not `source`" + } + } + +} + +use warning export-env { + + let nu_ver = (version | get version | split row '.' | take 2 | each { into int }) + if $nu_ver.0 == 0 and $nu_ver.1 < 106 { + error make { + msg: 'virtualenv Nushell activation requires Nushell 0.106 or greater.' + } + } + def is-string [x] { ($x | describe) == 'string' } def has-env [...names] { - $names | each {|n| - $n in $env - } | all {|i| $i == true} + $names | each {|n| $n in $env } | all {|i| $i } } - # Emulates a `test -z`, but better as it handles e.g 'false' def is-env-true [name: string] { - if (has-env $name) { - # Try to parse 'true', '0', '1', and fail if not convertible - let parsed = (do -i { $env | get $name | into bool }) - if ($parsed | describe) == 'bool' { - $parsed + if (has-env $name) { + let val = ($env | get --optional $name) + if ($val | describe) == 'bool' { + $val + } else { + not ($val | is-empty) + } } else { - not ($env | get -i $name | is-empty) + false } - } else { - false - } } let virtual_env = '{{ VIRTUAL_ENV_DIR }}' let bin = '{{ BIN_NAME }}' - - let is_windows = ($nu.os-info.family) == 'windows' - let path_name = (if (has-env 'Path') { - 'Path' - } else { - 'PATH' - } - ) - + let path_name = if (has-env 'Path') { 'Path' } else { 'PATH' } let venv_path = ([$virtual_env $bin] | path join) let new_path = ($env | get $path_name | prepend $venv_path) - - # If there is no default prompt, then use the env name instead - let virtual_env_prompt = (if ('{{ VIRTUAL_PROMPT }}' | is-empty) { + let virtual_env_prompt = if ('{{ VIRTUAL_PROMPT }}' | is-empty) { ($virtual_env | path basename) } else { '{{ VIRTUAL_PROMPT }}' - }) - - let new_env = { - $path_name : $new_path - VIRTUAL_ENV : $virtual_env - VIRTUAL_ENV_PROMPT : $virtual_env_prompt } - - let new_env = (if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') { - $new_env + let new_env = { $path_name: $new_path VIRTUAL_ENV: $virtual_env VIRTUAL_ENV_PROMPT: $virtual_env_prompt } + let old_prompt_command = if (has-env 'PROMPT_COMMAND') { $env.PROMPT_COMMAND } else { '' } + let new_env = if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') { + $new_env } else { - # Creating the new prompt for the session - let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) ' - - # Back up the old prompt builder - let old_prompt_command = (if (has-env 'PROMPT_COMMAND') { - $env.PROMPT_COMMAND - } else { - '' - }) - - let new_prompt = (if (has-env 'PROMPT_COMMAND') { - if 'closure' in ($old_prompt_command | describe) { - {|| $'($virtual_prefix)(do $old_prompt_command)' } - } else { - {|| $'($virtual_prefix)($old_prompt_command)' } - } - } else { - {|| $'($virtual_prefix)' } - }) - - $new_env | merge { - PROMPT_COMMAND : $new_prompt - VIRTUAL_PREFIX : $virtual_prefix - } - }) - - # Environment variables that will be loaded as the virtual env + let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) ' + let new_prompt = if (has-env 'PROMPT_COMMAND') { + if ('closure' in ($old_prompt_command | describe)) { + {|| $'($virtual_prefix)(do $old_prompt_command)' } + } else { + {|| $'($virtual_prefix)($old_prompt_command)' } + } + } else { + {|| $'($virtual_prefix)' } + } + $new_env | merge { PROMPT_COMMAND: $new_prompt VIRTUAL_PREFIX: $virtual_prefix } + } load-env $new_env } diff --git a/crates/uv-virtualenv/src/activator/deactivate.bat b/crates/uv-virtualenv/src/activator/deactivate.bat index 95af1351b0c96..78ed412d6a782 100644 --- a/crates/uv-virtualenv/src/activator/deactivate.bat +++ b/crates/uv-virtualenv/src/activator/deactivate.bat @@ -36,4 +36,4 @@ @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH @set "PATH=%_OLD_VIRTUAL_PATH%" @set _OLD_VIRTUAL_PATH= -:ENDIFVPATH \ No newline at end of file +:ENDIFVPATH diff --git a/crates/uv-virtualenv/src/activator/pydoc.bat b/crates/uv-virtualenv/src/activator/pydoc.bat index 8a8d590d22a32..fe756863e419b 100644 --- a/crates/uv-virtualenv/src/activator/pydoc.bat +++ b/crates/uv-virtualenv/src/activator/pydoc.bat @@ -19,4 +19,4 @@ @REM OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION @REM WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -python.exe -m pydoc %* \ No newline at end of file +python.exe -m pydoc %*