Repository: tox-dev/tox
Pull request #929 (2018-07-29 01:27:00)

Do not assume all python installs come with virtualenv
Currently, tox runs virtualenv via `python -m virtualenv ...`.
However, that assumes that virtualenv is installed with the python invoked, that is not always the case (for example: pythons installed via pyenv don't come with virutalenv).
A trivial solution for this is to employ the same technique as virtualenv itself uses: invoke virtualenv via `python path/to/virtualenv.py ...`

This change allows to package tox via tools like `pex`, and have a working standalone `tox` that does not assume all pythons come with virtualenv.

Issue: https://github.com/tox-dev/tox/issues/928

codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=h1) Report
> Merging [#929](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=desc) into [master](https://codecov.io/gh/tox-dev/tox/commit/f717a6178716c01fa7c8a0f56434ceeac748cc10?src=pr&el=desc) will **decrease** coverage by `1.66%`.
> The diff coverage is `66.66%`.

[![Impacted file tree graph](https://codecov.io/gh/tox-dev/tox/pull/929/graphs/tree.svg?token=DYodAwDCZ5&height=150&src=pr&width=650)](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #929      +/-   ##
==========================================
- Coverage   92.63%   90.96%   -1.67%     
==========================================
  Files          12       12              
  Lines        2335     2336       +1     
  Branches      409      400       -9     
==========================================
- Hits         2163     2125      -38     
- Misses        109      144      +35     
- Partials       63       67       +4
```


| [Impacted Files](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/tox/session.py](https://codecov.io/gh/tox-dev/tox/pull/929/diff?src=pr&el=tree#diff-c3JjL3RveC9zZXNzaW9uLnB5) | `91.03% <100%> (-0.55%)` | :arrow_down: |
| [src/tox/venv.py](https://codecov.io/gh/tox-dev/tox/pull/929/diff?src=pr&el=tree#diff-c3JjL3RveC92ZW52LnB5) | `90.96% <60%> (-1.96%)` | :arrow_down: |
| [src/tox/interpreters.py](https://codecov.io/gh/tox-dev/tox/pull/929/diff?src=pr&el=tree#diff-c3JjL3RveC9pbnRlcnByZXRlcnMucHk=) | `69.84% <0%> (-19.05%)` | :arrow_down: |
| [src/tox/\_quickstart.py](https://codecov.io/gh/tox-dev/tox/pull/929/diff?src=pr&el=tree#diff-c3JjL3RveC9fcXVpY2tzdGFydC5weQ==) | `75.59% <0%> (-1.16%)` | :arrow_down: |
| [src/tox/\_pytestplugin.py](https://codecov.io/gh/tox-dev/tox/pull/929/diff?src=pr&el=tree#diff-c3JjL3RveC9fcHl0ZXN0cGx1Z2luLnB5) | `91.07% <0%> (-0.9%)` | :arrow_down: |
| [src/tox/config.py](https://codecov.io/gh/tox-dev/tox/pull/929/diff?src=pr&el=tree#diff-c3JjL3RveC9jb25maWcucHk=) | `95.66% <0%> (-0.25%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=footer). Last update [f717a61...dd7e649](https://codecov.io/gh/tox-dev/tox/pull/929?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

asottile: 👎

`tox` [depends on virtualenv](https://github.com/tox-dev/tox/blob/f717a6178716c01fa7c8a0f56434ceeac748cc10/setup.py#L68) which means it _is_ necessarily installed to the same python that `tox` is running in (unless you've intentionally uninstalled `virtualenv`, but then you're in unsupported territory)
zsimic: Yes, `tox` depends on virtualenv, but when you invoke `python -m virtualenv ...`, you don't necessarily get the virtualenv that came with tox, even if the python you invoke is `sys.executable`

To understand what I mean, see my last comment on issue https://github.com/tox-dev/tox/issues/928

This scenario can't happen if you `pip install tox`, but it does happen if you package `tox` with `pex` for example.

If you decide that the only way to package `tox` is via `pip`, then yes: invoking `sys.executable -m virtualenv` will work, however you are preventing `tox` from being packaged standalone with tools like `pex`. 

My solution here makes it **always** use the correct `virtualenv` that came with `tox`, even when `tox` is packaged with `pex`.
gaborbernat: will review this late this week as I need to understand what pex does and
why sys executable does not have virtualenv.

On Mon, Jul 30, 2018, 07:51 Zoran Simic <notifications@github.com> wrote:

> Yes, tox depends on virtualenv, but when you invoke python -m virtualenv
> ..., you don't necessarily get the virtualenv that came with tox, even if
> the python you invoke is sys.executable
>
> To understand what I mean, see my last comment on issue #928
> <https://github.com/tox-dev/tox/issues/928>
>
> This scenario can't happen if you pip install tox, but it does happen if
> you package tox with pex for example.
>
> If you decide that the only way to package tox is via pip, then yes:
> invoking sys.executable -m virtualenv will work, however you are
> preventing tox from being packaged standalone with tools like pex.
>
> My solution here makes it *always* use the correct virtualenv that came
> with tox, even when tox is packaged with pex.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/tox-dev/tox/pull/929#issuecomment-408764367>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AAqIPqK3_3H-HHWyGLM0AgdrUhqdBN8_ks5uLq0CgaJpZM4VlPtM>
> .
>

zsimic: No hurry, whenever you can.

I was annoyed by this (tried to package tox with pex to make it easier for teammates to run tox).
I packaged tox with pex, but get the `No module named virtualenv` error.

I wrote down instructions on how to reproduce (and package with pex) on issue https://github.com/tox-dev/tox/issues/928

Once you have a pex package of tox, you can do this to get a very quick idea of how pex works (it basically creates a standalone venv of the target CLI, the packaged standalone executable unpacks itself in `~/.pex` and runs from there)

Aassuming your pex packaged tox is in `./tox`:
```
rm -rf ~/.pex      # just to clean any other stuff you may have had in ~/.pex cache
./tox --version
tree -L 2 ~/.pex
find ~/.pex -name virtualenv.py
find ~/.pex -name tox
ls -l `find ~/.pex -name tox`
```
zsimic: Also note that this isn't a bug in tox I think, it's just that so far everyone has probably always used a `pip` installed tox, and never ran into this subtle edge case before.

I think a standalone tox package would be great to have.
I'm packaging several CLIs already with pex (in prod), it's very stable and reliable.

I would love to be able to use tox in an automated fashion (working on an easy auto-install/upgrade for it + other CLIs). I think being able to package it with pex will help greatly in that endeavor
gaborbernat: So what's the advantage of having this pex executable over using pip install in a virtual environment? Why it's easier to use pex in the development environment (tox is a developer tool) over pip?
rpkilby: I might be missing something here, but why can't the `pex` build of tox contain `virtualenv`? It seems like that would be the correct thing to do, in addition to including all of `tox`'s other dependencies. 

Also, I would recommend that your macOS developers use something like MacPorts, Homebrew, or pyenv to install an alternative version of python, so they aren't using the system Python at all.
asottile: ah, `pex` 😭 

the fix here should really come from `pex`'s side and not here -- that `pex` fails to put the proper modules on the path (or provide a reasonable value for `sys.executable`) is the root cause here.  Adding hacks to `tox` for this does not seem a maintainable solution moving forward.

An aside, calling directly into a file in `site-packages` can cause some problems especially for virtualenv as `python path/to/file.py` puts `path/to` on the **beginning** of `sys.path` for the duration of execution.  When `path/to` is the `site-packages` directory, this switches the usual ordering of `sys.path` putting user installed packages *before* the standard library.  Suddenly the previously-harmless noop installation of `python-future` / `enum34` is being imported ahead of the standard library.

We'd also be painted into a corner of potential implementation detail.  If virtualenv were to rearrange its modules such that `virtualenv` becomes a package and `virtualenv/__main__.py` is the actual entrypoint then executing `virtualenv/__init__.py` wouldn't work!  There's also the case of `virtualenv` being a zip executable or egg itself in which `__file__` would be `None` (or an `AttributeError`) and execution would fail trying to run `(sys.executable, None, ...)` (but `-m` would succeed since it uses the usual module lookup).

I'm of the opinion that we shouldn't bend over backwards to make `pex` work, `pex` is a hack at best and all of our documentation suggests using the standard packaging tools (`pip` / `virtualenv` / etc.).

Setting up a virtualenv and putting tox on the path isn't all that difficult.  Here's ~roughly how I set this up myself:

```bash
# wow, I don't even need a globally installed `pip` or `virtualenv`!
curl https://asottile.github.io/get-virtualenv.py | python3 - ~/opt/venv
~/opt/venv/bin/pip install tox
ln -sf ~/opt/venv/bin/tox ~/bin/
```
zsimic: @rpkilby the produced `pex` package does contain `virtualenv` (and invoking `path/to/virtualenv.py` as I do here basically invokes explicitly the one that comes in that pex package)
Also, we do use pyenv (and not system python on OSX), we are automating the installation of pyenv, as well as all python versions referenced by a project. All those pyenv installed pythons are clean though (we don't install tox or virtualenv into them)

@asottile I guess I could consider programmatically creating a venv dedicated to tox like that, but I wonder if I'm not signing up for headaches (such as: what happens if I use pyenv python 3.6.5 to create that venv, and then python 3.6.6 comes around, we upgrade to it and remove 3.5.5... will my tox venv be still valid?)

Alternatively, would you be OK with this (in tox's `venv.py`):
```
from virtualenv import create_environment
...
create_environment(...)
```

I'll gladly adapt this PR to do that (instead of `sys.executable -m virtualenv`)
asottile: `virtualenv.create_enviroment` doesn't appear documented either
zsimic: [This virtualenv doc](https://virtualenv.pypa.io/en/stable/installation/) says: "To use locally from source, run `python virtualenv.py ...`". 

It actually seems that invoking `virtualenv.py` is the documented way, I don't see anywhere in the virtualenv doc that recommends `python -m virtualenv`. Wouldn't my PR here do it more by the doc then? :)

Documentation around programmatic usage (`import virtualenv`) is sparse indeed, there's only this page apparently: https://virtualenv.pypa.io/en/stable/reference/ that mentions:
```
import virtualenv
...
virtualenv.create_bootstrap_script()
```

If going by the doc, it seems that:
- `-m virtualenv` is never mentioned
- most common way mentioned is to use the `virtualenv` command itself (no)
- there's one recommendation for invoking `python path/to/virtualenv.py`
asottile: @zsimic out of context yes that's what it says.  however if you read the context the documentation there is how to download the source distribution and invoke the contents -- which to me is unrelated to an actually _installed_ virtualenv.
zsimic: From doc: The `virtualenv.py` script is not supported if run without the necessary pip/setuptools/virtualenv distributions available locally. All of the installation methods above include a virtualenv_support directory alongside `virtualenv.py` which contains a complete set of pip and setuptools distributions, and so are fully supported.

Invoking `virtualenv.py` from the installed path like in my PR seems to comply to that... it is invoking `virtualenv.py` from a complete installed set...

There are tests that will catch any change of API from virtualenv side (like the mentined move from `virtualenv.py` to like a `__main__.py` approach). Invoking `virtualenv.py` seems to be the documented way (and `-m virtualenv` is not documented).

I would be really interested in being able to run a pex packaged tox. Let me know if you'd be interested too, I'll gladly adapt the PR further.

If not, I guess I'll consider a different way of automatically packaging tox, or another way to run tests in general. Having a long README on "how to get going with tox: run this curl, make a venv this way etc" is unfortunately not a great way of doing things :( 
So I'll try and automate the installation of tox
daa: @zsimic If this pull request will not be accepted I'd like to suggest you to consider several opportunities:
1. If you are lucky and may use only Python-3 you may include ``tox-venv`` plugin into your ``tox`` binary and use built-in ``venv`` instead of ``virtualenv``
2. You may try to offer ``pex`` team to include some command-line flag which allows to run python interpreter instead of entry point for packaged application and to set ``sys.executable`` to it
3. At least you may write ``tox`` plugin which does same thing as your pull request

Anyway thanks for raising this issue, I took similar approach to provide ``tox`` for users but I use ``shiv`` instead of ``pex``, and I don't have your problem because I limited usage to our custom Python installation which includes ``virtualenv``.

zsimic: @daa interesting, thanks for the pointers, I will look into it.
Looks like this PR will be ignored indeed, don't feel there's interest.

I was considering having a special case for tox, where I'd create simply a plain old venv, and then symlink to its binary (like @asottile mentioned - end result looks messy, but theoretically I guess would be more the "right way")

Didn't know about shiv, nor the possibility for tox plugins, will try out shiv and see tox-venv, maybe a plugin makes sense yes (made a setuptools plugin BTW: https://pypi.org/project/setupmeta/ for a writing setup.py's without repeating oneself :) maybe I can do a tox plugin too).

I can't assume python3 everywhere, the "python dev auto bootstrapping" I'm trying to achieve has to be able to get itself going with whatever python is available from system (and then auto-install more python versions, tox etc)

rpkilby: > the produced `pex` package does contain `virtualenv`

@zsimic - I guess my question here would be *why*. If `tox` very explicitly depends on `virtualenv`, shouldn't it be included in the `pex` output? If it's not included, this seems like a bug in `pex`, absent some explains why `virtualenv` shouldn't be included in the output.
daa: @rpkilby It seems you have misread the quote, ``pex``-packaged distribution actually contains ``virtualenv`` but in an unusual place - unpacked wheel somewhere inside ``~/.pex``.
zsimic: `tox` works a bit differently from a usual venv:
- in a usual venv, `sys.executable` points to your `venv/bin/python`
- in a pex-packaged CLI, `sys.executable` is the executable used to execute the pex-packaged CLI... usually `/usr/bin/env python`

So, any `import` will work as usual when running from a pex package, however spawning anew process via `sys.executable -m virtualenv` will invoke basically the system python used to execute the pex packaged CLI...
gaborbernat: I would say I'm more and more open to give this a go. Will have a think over and early next week maybe we can merge with tentative nature (meaning if we discover any bugs we'll revert it) and do a test release. Before we go ahead @zsimic please describe exactly how you envision your use case with pex (steps to reproduce, how users will interact, what advantages this would have over the current status quo, etc).
zsimic: I do have this in place BTW, it's working great internally (for internal CLIs, not `tox` yet, hence this PR)

So: end goal is to be able to auto/install upgrade python CLIs in `~/.local/bin`
The way I do it currently:
- chose pex, because it happens to work nicely on OSX/linux, but could change to something else if I find something else that can package python CLIs as standalone executables
- I grab the latest universal pex package for `pexy` from https://github.com/zsimic/pexy/releases/latest and drop it in `~/.local/bin` (if it's not there already)
- I then call `pexy install foo` to install CLI "foo"
- `pexy check foo` to see if there's a new version of "foo" on pypi (or in my case: an internal pypi-like index we use)

I can install `tox` that way currently, but if I run `tox` as a pex-package, I get the "No module virtualenv" error...

I was thinking if this PR doesn't go through, then I'd explore perhaps changing `pexy` so that it builds a regular venv, `pip install tox` into that venv, and then symlink `~/.local/bin/tox` to the `tox` from that venv... 
Won't be as clean as having a standalone exe, but should work too.
rpkilby: Thanks @daa :smile:

> So, any import will work as usual when running from a pex package, however spawning anew process via sys.executable -m virtualenv will invoke basically the system python used to execute the pex packaged CLI...

Would this mean that `pex` generally can't run python subprocesses? e.g., if `pex` is typically invoked with the system python, then `subprocess.run(['python', '-m', 'foo'])` would fail, since `foo` is packaged in the `pex` executable and isn't installed to the system site-packages. 

re: `tox-venv`, that should work for Python 3 environments, but you may want to write `tox-pex` to support Python 2. Writing a hook for `tox_testenv_create` is pretty straightforward - `tox-venv` is a little complicated due to how `venv`'s and `virtualenv`'s interact.
zsimic: Yes @rpkilby , you can't use `sys.executable` as you would from a regular venv in `pex`, as that would spawn whatever python was used to execute the pex (typically a non-virtualized clean python), so `-m foo` wouldn't be a module available there. That technique doesn't work with `pex` for any not stdlib module.

anthrotype: How about tox still calls sys.executable but passes the subprocess a PYTHONPATH environment variable that contains the current value of sys.path?
If virtualenv is indeed packaged in the pex and can be imported from the parent python process, doing what I suggest will make sure the child python process also sees it when invoked with -m
anthrotype: I mean something like this https://gist.github.com/sheilatron/2147199


asottile: In that case, I think it could be done as a plugin (which implements testenv_create by setting PYTHONPATH and then allowing the default tox implementation to run)

I'd be most comfortable supporting pex via plugin rather than in core
daa: I think this ``pex`` issue may be of interest: https://github.com/pantsbuild/pex/issues/392

Patch:
diff --git a/changelog/849.feature.rst b/changelog/849.feature.rst
new file mode 100644
index 00000000..5f0daf69
--- /dev/null
+++ b/changelog/849.feature.rst
@@ -0,0 +1,2 @@
+Allow to run multiple tox instances in parallel by providing the
+```--parallel--safe-build`` flag. - by :user:`gaborbernat`
diff --git a/changelog/929.feature.rst b/changelog/929.feature.rst
new file mode 100644
index 00000000..e95c2942
--- /dev/null
+++ b/changelog/929.feature.rst
@@ -0,0 +1,1 @@
+Call virtualenv by invoking directly the virtualenv file (after import resolve) instead of ``-m`` (allows pex packaging tox) - trial accepted. - by :user:`zsimic`
diff --git a/doc/example/jenkins.rst b/doc/example/jenkins.rst
index 9d8a96d4..8c88ad4c 100644
--- a/doc/example/jenkins.rst
+++ b/doc/example/jenkins.rst
@@ -188,4 +188,27 @@ Linux as a limit of 128). There are two methods to workaround this issue:
     :ref:`long interpreter directives` for more information).
 
 
+Running tox environments in parallel
+------------------------------------
+
+Jenkins has parallel stages allowing you to run commands in parallel, however tox package
+building it is not parallel safe. Use the ``--parallel--safe-build`` flag to enable parallel safe
+builds. Here's a generic stage definition demonstrating this:
+
+.. code-block:: groovy
+
+    stage('run tox envs') {
+      steps {
+        script {
+          def envs = sh(returnStdout: true, script: "tox -l").trim().split('\n')
+          def cmds = envs.collectEntries({ tox_env ->
+            [tox_env, {
+              sh "tox --parallel--safe-build -vve $tox_env"
+            }]
+          })
+          parallel(cmds)
+        }
+      }
+    }
+
 .. include:: ../links.rst
diff --git a/src/tox/config.py b/src/tox/config.py
index d3e6e764..43c80d39 100755
--- a/src/tox/config.py
+++ b/src/tox/config.py
@@ -8,6 +8,7 @@ import re
 import shlex
 import string
 import sys
+import uuid
 import warnings
 from fnmatch import fnmatchcase
 from subprocess import list2cmdline
@@ -390,6 +391,12 @@ def tox_addoption(parser):
         dest="sdistonly",
         help="only perform the sdist packaging activity.",
     )
+    parser.add_argument(
+        "--parallel--safe-build",
+        action="store_true",
+        dest="parallel_safe_build",
+        help="ensure two tox builds can run in parallel",
+    )
     parser.add_argument(
         "--installpkg",
         action="store",
@@ -864,7 +871,7 @@ def make_hashseed():
 
 
 class parseini:
-    def __init__(self, config, inipath):
+    def __init__(self, config, inipath):  # noqa
         config.toxinipath = inipath
         config.toxinidir = config.toxinipath.dirpath()
 
@@ -950,6 +957,10 @@ class parseini:
 
         reader.addsubstitutions(toxworkdir=config.toxworkdir)
         config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
+        if config.option.parallel_safe_build:
+            config.distdir = py.path.local(config.distdir.dirname).join(
+                "{}-{}".format(config.distdir.basename, str(uuid.uuid4()))
+            )
         reader.addsubstitutions(distdir=config.distdir)
         config.distshare = reader.getpath("distshare", distshare_default)
         reader.addsubstitutions(distshare=config.distshare)
diff --git a/src/tox/session.py b/src/tox/session.py
index 8ff104af..c1001f1d 100644
--- a/src/tox/session.py
+++ b/src/tox/session.py
@@ -19,7 +19,7 @@ from packaging.version import InvalidVersion, Version
 import tox
 from tox.config import parseconfig
 from tox.result import ResultLog
-from tox.venv import VirtualEnv
+from tox.venv import VIRTUALENV_FILE, VirtualEnv
 
 
 def prepare(args):
@@ -42,7 +42,11 @@ def cmdline(args=None):
 def main(args):
     try:
         config = prepare(args)
-        retcode = Session(config).runcommand()
+        try:
+            retcode = Session(config).runcommand()
+        finally:
+            if config.option.parallel_safe_build:
+                config.distdir.remove(ignore_errors=True)
         if retcode is None:
             retcode = 0
         raise SystemExit(retcode)
@@ -724,7 +728,7 @@ class Session:
     def info_versions(self):
         versions = ["tox-{}".format(tox.__version__)]
         proc = subprocess.Popen(
-            (sys.executable, "-m", "virtualenv", "--version"), stdout=subprocess.PIPE
+            (sys.executable, VIRTUALENV_FILE, "--version"), stdout=subprocess.PIPE
         )
         out, _ = proc.communicate()
         versions.append("virtualenv-{}".format(out.decode("UTF-8").strip()))
diff --git a/src/tox/venv.py b/src/tox/venv.py
index b4e70984..93a8a4da 100755
--- a/src/tox/venv.py
+++ b/src/tox/venv.py
@@ -7,11 +7,17 @@ import sys
 import warnings
 
 import py
+import virtualenv
 
 import tox
 
 from .config import DepConfig
 
+# Use virtualenv that comes with tox, don't assume other installed pythons have virtualenv
+VIRTUALENV_FILE = virtualenv.__file__
+if VIRTUALENV_FILE.endswith(".pyc"):
+    VIRTUALENV_FILE = VIRTUALENV_FILE[:-1]
+
 
 class CreationConfig:
     def __init__(self, md5, python, version, sitepackages, usedevelop, deps, alwayscopy):
@@ -470,7 +476,7 @@ def prepend_shebang_interpreter(args):
 @tox.hookimpl
 def tox_testenv_create(venv, action):
     config_interpreter = venv.getsupportedinterpreter()
-    args = [sys.executable, "-m", "virtualenv"]
+    args = [sys.executable, VIRTUALENV_FILE]
     if venv.envconfig.sitepackages:
         args.append("--system-site-packages")
     if venv.envconfig.alwayscopy:
diff --git a/tox.ini b/tox.ini
index e6d76ec0..d10b0ea8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -126,7 +126,7 @@ include_trailing_comma = True
 force_grid_wrap = 0
 line_length = 99
 known_first_party = tox
-known_third_party = apiclient,git,httplib2,oauth2client,packaging,pkg_resources,pluggy,py,pytest,setuptools,six
+known_third_party = apiclient,git,httplib2,oauth2client,packaging,pkg_resources,pluggy,py,pytest,setuptools,six,virtualenv
 
 [testenv:release]
 description = do a release, required posarg of the version number


Test patch:
diff --git a/src/tox/_pytestplugin.py b/src/tox/_pytestplugin.py
index 7602a481..564d0505 100644
--- a/src/tox/_pytestplugin.py
+++ b/src/tox/_pytestplugin.py
@@ -1,6 +1,7 @@
 from __future__ import print_function, unicode_literals
 
 import os
+import sys
 import textwrap
 import time
 from fnmatch import fnmatch
@@ -10,10 +11,11 @@ import pytest
 import six
 
 import tox
+from tox import venv
 from tox.config import parseconfig
 from tox.result import ResultLog
 from tox.session import Session, main
-from tox.venv import VirtualEnv
+from tox.venv import CreationConfig, VirtualEnv, getdigest
 
 mark_dont_run_on_windows = pytest.mark.skipif(os.name == "nt", reason="non windows test")
 mark_dont_run_on_posix = pytest.mark.skipif(os.name == "posix", reason="non posix test")
@@ -70,7 +72,16 @@ def cmd(request, capfd, monkeypatch):
         key = str(b"PYTHONPATH")
         python_paths = (i for i in (str(os.getcwd()), os.getenv(key)) if i)
         monkeypatch.setenv(key, os.pathsep.join(python_paths))
+
         with RunResult(capfd, argv) as result:
+            prev_run_command = Session.runcommand
+
+            def run_command(self):
+                result.session = self
+                return prev_run_command(self)
+
+            monkeypatch.setattr(Session, "runcommand", run_command)
+
             try:
                 main([str(x) for x in argv])
                 assert False  # this should always exist with SystemExit
@@ -91,6 +102,7 @@ class RunResult:
         self.duration = None
         self.out = None
         self.err = None
+        self.session = None
 
     def __enter__(self):
         self._start = time.time()
@@ -373,3 +385,38 @@ def create_files(base, filedefs):
         elif isinstance(value, six.string_types):
             s = textwrap.dedent(value)
             base.join(key).write(s)
+
+
+@pytest.fixture()
+def mock_venv(monkeypatch):
+    """This creates a mock virtual environment (e.g. will inherit the current interpreter).
+    Note: because we inherit, to keep things sane you must call the py environment and only that;
+    and cannot install any packages. """
+
+    class ProxyCurrentPython:
+        @classmethod
+        def readconfig(cls, path):
+            assert path.dirname.endswith("{}py".format(os.sep))
+            return CreationConfig(
+                md5=getdigest(sys.executable),
+                python=sys.executable,
+                version=tox.__version__,
+                sitepackages=False,
+                usedevelop=False,
+                deps=[],
+                alwayscopy=False,
+            )
+
+    monkeypatch.setattr(CreationConfig, "readconfig", ProxyCurrentPython.readconfig)
+
+    def venv_lookup(venv, name):
+        assert name == "python"
+        return sys.executable
+
+    monkeypatch.setattr(VirtualEnv, "_venv_lookup", venv_lookup)
+
+    @tox.hookimpl
+    def tox_runenvreport(venv, action):
+        return []
+
+    monkeypatch.setattr(venv, "tox_runenvreport", tox_runenvreport)
diff --git a/tests/test_session.py b/tests/test_session.py
index 010ea141..a4366d56 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -1,4 +1,5 @@
 import re
+import uuid
 
 import pytest
 
@@ -76,3 +77,23 @@ def test_minversion(cmd, initproj):
         r"ERROR: MinVersionError: tox version is .*," r" required is at least 6.0", result.out
     )
     assert result.ret
+
+
+def test_tox_parallel_build_safe(initproj, cmd, mock_venv):
+    initproj(
+        "env_var_test",
+        filedefs={
+            "tox.ini": """
+                          [tox]
+                          envlist = py
+                          [testenv]
+                          skip_install = true
+                          commands = python --version
+                      """
+        },
+    )
+    result = cmd("--parallel--safe-build")
+    basename = result.session.config.distdir.basename
+    assert basename.startswith("dist-")
+    assert uuid.UUID(basename[len("dist-") :], version=4)
+    assert not result.session.config.distdir.exists()
diff --git a/tests/test_venv.py b/tests/test_venv.py
index 5d4b4949..387a9688 100644
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -68,7 +68,7 @@ def test_create(mocksession, newconfig):
     pcalls = mocksession._pcalls
     assert len(pcalls) >= 1
     args = pcalls[0].args
-    assert "virtualenv" == str(args[2])
+    assert str(args[1]).endswith("virtualenv.py")
     if not tox.INFO.IS_WIN:
         # realpath is needed for stuff like the debian symlinks
         our_sys_path = py.path.local(sys.executable).realpath()
@@ -443,7 +443,7 @@ def test_install_python3(newmocksession):
     pcalls = mocksession._pcalls
     assert len(pcalls) == 1
     args = pcalls[0].args
-    assert str(args[2]) == "virtualenv"
+    assert str(args[1]).endswith("virtualenv.py")
     pcalls[:] = []
     action = mocksession.newaction(venv, "hello")
     venv._install(["hello"], action=action)
diff --git a/tests/test_z_cmdline.py b/tests/test_z_cmdline.py
index 42104c97..68b98b04 100644
--- a/tests/test_z_cmdline.py
+++ b/tests/test_z_cmdline.py
@@ -687,7 +687,7 @@ def test_alwayscopy(initproj, cmd):
     )
     result = cmd("-vv")
     assert not result.ret
-    assert "virtualenv --always-copy" in result.out
+    assert "virtualenv.py --always-copy" in result.out
 
 
 def test_alwayscopy_default(initproj, cmd):
@@ -702,7 +702,7 @@ def test_alwayscopy_default(initproj, cmd):
     )
     result = cmd("-vv")
     assert not result.ret
-    assert "virtualenv --always-copy" not in result.out
+    assert "virtualenv.py --always-copy" not in result.out
 
 
 def test_empty_activity_ignored(initproj, cmd):