summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Deegan <bill@baddogconsulting.com>2018-11-13 19:11:48 (GMT)
committerWilliam Deegan <bill@baddogconsulting.com>2018-11-13 19:11:48 (GMT)
commit505018d3b0883a9f04e75262923a7ced5c44dcfa (patch)
tree7a08a0d66c46b155e27b625f19151d06fc04990a
parent13bae8b5e7d68c7039032fad237a96198242ac64 (diff)
parent648cf42a89845ccad012e02609ca8958e62ce272 (diff)
downloadSCons-505018d3b0883a9f04e75262923a7ced5c44dcfa.zip
SCons-505018d3b0883a9f04e75262923a7ced5c44dcfa.tar.gz
SCons-505018d3b0883a9f04e75262923a7ced5c44dcfa.tar.bz2
Merge branch 'master' of github.com:SCons/scons
-rw-r--r--.gitignore7
-rw-r--r--.travis.yml6
-rwxr-xr-x.travis/install.sh80
-rw-r--r--doc/man/scons.xml12
-rw-r--r--doc/user/misc.xml49
-rw-r--r--src/CHANGES.txt36
-rw-r--r--src/engine/SCons/Node/FS.py248
-rw-r--r--src/engine/SCons/Node/FSTests.py172
-rw-r--r--src/engine/SCons/Node/NodeTests.py1
-rw-r--r--src/engine/SCons/Node/__init__.py73
-rw-r--r--src/engine/SCons/Platform/PlatformTests.py8
-rw-r--r--src/engine/SCons/Platform/__init__.py4
-rw-r--r--src/engine/SCons/Platform/posix.py5
-rw-r--r--src/engine/SCons/Platform/virtualenv.py120
-rw-r--r--src/engine/SCons/Platform/virtualenvTests.py243
-rw-r--r--src/engine/SCons/Platform/win32.py7
-rw-r--r--src/engine/SCons/Script/Main.py8
-rw-r--r--src/engine/SCons/Script/SConsOptions.py14
-rw-r--r--src/engine/SCons/Script/__init__.py2
-rw-r--r--src/engine/SCons/Tool/docbook/__init__.py2
-rw-r--r--src/engine/SCons/Util.py26
-rw-r--r--src/engine/SCons/UtilTests.py58
-rw-r--r--src/engine/SCons/dblite.py9
-rw-r--r--test/Decider/MD5-timestamp-Repository.py91
-rw-r--r--test/virtualenv/activated/option/enable-virtualenv.py91
-rw-r--r--test/virtualenv/activated/option/ignore-virtualenv.py90
-rw-r--r--test/virtualenv/activated/virtualenv_activated_python.py96
-rw-r--r--test/virtualenv/activated/virtualenv_detect_virtualenv.py58
-rw-r--r--test/virtualenv/always/virtualenv_global_function.py68
-rw-r--r--test/virtualenv/regularenv/virtualenv_detect_regularenv.py57
-rw-r--r--test/virtualenv/unactivated/virtualenv_unactivated_python.py95
31 files changed, 1686 insertions, 150 deletions
diff --git a/.gitignore b/.gitignore
index db2cd3a..546c893 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,8 +8,8 @@ __pycache__/
# Distribution / packaging
.Python
-build/**
-bootstrap/**
+/build/**
+/bootstrap/**
.idea/
src/build/**
@@ -28,8 +28,6 @@ ENV/
env.bak/
venv.bak/
-
-
# mypy
.mypy_cache/
@@ -49,3 +47,4 @@ htmlcov
*.bkp
*.bak
*~
+!/test/Decider/switch-rebuild.py
diff --git a/.travis.yml b/.travis.yml
index ea63e57..76f6db0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,11 @@ addons:
apt:
update: true
+os:
+ - linux
+
+sudo: required
+
install:
- ./.travis/install.sh
@@ -16,7 +21,6 @@ matrix:
allow_failures:
- python: pypy
-
jobs:
include:
- &test_job
diff --git a/.travis/install.sh b/.travis/install.sh
index 69e833a..15a36a3 100755
--- a/.travis/install.sh
+++ b/.travis/install.sh
@@ -1,44 +1,50 @@
#!/usr/bin/env bash
set -x
-# setup clang for clang tests using local clang installation
-
-
-if [ ! -f /usr/local/clang-5.0.0/bin/clang ]; then
- echo "No Clang 5.0.0 trying 7.0.0"
- sudo ln -s /usr/local/clang-7.0.0/bin/clang /usr/bin/clang
- sudo ln -s /usr/local/clang-7.0.0/bin/clang++ /usr/bin/clang++
+if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+ echo "OSX"
+ brew install python --framework --universal
else
- echo "Clang 5.0.0"
- sudo ln -s /usr/local/clang-5.0.0/bin/clang /usr/bin/clang
- sudo ln -s /usr/local/clang-5.0.0/bin/clang++ /usr/bin/clang++
-fi
+ # dependencies for clang tests
+ sudo apt-get -y install clang
-# dependencies for gdc tests
-sudo apt-get -y install gdc
-# dependencies for docbook tests
-sudo apt-get -y install docbook-xml xsltproc libxml2-dev libxslt-dev fop docbook-xsl-doc-pdf
-# dependencies for latex tests
-sudo apt-get -y install texlive texlive-latex3 biber texmaker ghostscript
-# need some things for building dependencies for other tests
-sudo apt-get -y install python-pip python-dev build-essential libpcre3-dev autoconf automake libtool bison subversion git
-# dependencies for docbook tests continued
-sudo pip install lxml
-# dependencies for D tests
-sudo wget http://master.dl.sourceforge.net/project/d-apt/files/d-apt.list -O /etc/apt/sources.list.d/d-apt.list
-wget -qO - https://dlang.org/d-keyring.gpg | sudo apt-key add -
-sudo apt-get update && sudo apt-get -y --allow-unauthenticated install dmd-bin
-# dependencies for ldc tests
-wget https://github.com/ldc-developers/ldc/releases/download/v1.4.0/ldc2-1.4.0-linux-x86_64.tar.xz
-tar xf ldc2-1.4.0-linux-x86_64.tar.xz
-sudo cp -rf ldc2-1.4.0-linux-x86_64/* /
+ # setup clang for clang tests using local clang installation
+ if [ ! -f /usr/local/clang-5.0.0/bin/clang ]; then
+ echo "No Clang 5.0.0 trying 7.0.0"
+ sudo ln -s /usr/local/clang-7.0.0/bin/clang /usr/bin/clang
+ sudo ln -s /usr/local/clang-7.0.0/bin/clang++ /usr/bin/clang++
+ else
+ echo "Clang 5.0.0"
+ sudo ln -s /usr/local/clang-5.0.0/bin/clang /usr/bin/clang
+ sudo ln -s /usr/local/clang-5.0.0/bin/clang++ /usr/bin/clang++
+ fi
-ls -l /usr/lib/*python*{so,a}*
+ # dependencies for gdc tests
+ sudo apt-get -y install gdc
+ # dependencies for docbook tests
+ sudo apt-get -y install docbook-xml xsltproc libxml2-dev libxslt-dev fop docbook-xsl-doc-pdf
+ # dependencies for latex tests
+ sudo apt-get -y install texlive-full biber texmaker
+ # need some things for building dependencies for other tests
+ sudo apt-get -y install python-pip python-dev build-essential libpcre3-dev autoconf automake libtool bison subversion git
+ # dependencies for docbook tests continued
+ sudo pip install lxml
+ # dependencies for D tests
+ sudo wget http://master.dl.sourceforge.net/project/d-apt/files/d-apt.list -O /etc/apt/sources.list.d/d-apt.list
+ wget -qO - https://dlang.org/d-keyring.gpg | sudo apt-key add -
+ sudo apt-get update && sudo apt-get -y --allow-unauthenticated install dmd-bin
+ # dependencies for ldc tests
+ wget https://github.com/ldc-developers/ldc/releases/download/v1.4.0/ldc2-1.4.0-linux-x86_64.tar.xz
+ tar xf ldc2-1.4.0-linux-x86_64.tar.xz
+ sudo cp -rf ldc2-1.4.0-linux-x86_64/* /
-# For now skip swig if py27
-if [[ "$PYVER" == 27 ]]; then
- # dependencies for swig tests
- wget https://github.com/swig/swig/archive/rel-3.0.12.tar.gz
- tar xzf rel-3.0.12.tar.gz
- cd swig-rel-3.0.12 && ./autogen.sh && ./configure --prefix=/usr && make && sudo make install && cd ..
-fi \ No newline at end of file
+ ls -l /usr/lib/*python*{so,a}*
+
+ # For now skip swig if py27
+ if [[ "$PYVER" == 27 ]]; then
+ # dependencies for swig tests
+ wget https://github.com/swig/swig/archive/rel-3.0.12.tar.gz
+ tar xzf rel-3.0.12.tar.gz
+ cd swig-rel-3.0.12 && ./autogen.sh && ./configure --prefix=/usr && make && sudo make install && cd ..
+ fi
+fi
diff --git a/doc/man/scons.xml b/doc/man/scons.xml
index abbed4f..9b48f69 100644
--- a/doc/man/scons.xml
+++ b/doc/man/scons.xml
@@ -997,6 +997,12 @@ the mechanisms in the specified order.</para>
</listitem>
</varlistentry>
<varlistentry>
+ <term>--enable-virtualenv</term>
+ <listitem>
+<para>Import virtualenv-related variables to SCons.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term>-f<emphasis> file</emphasis>, --file=<emphasis>file</emphasis>, --makefile=<emphasis>file</emphasis>, --sconstruct=<emphasis>file</emphasis></term>
<listitem>
<para>Use
@@ -1051,6 +1057,12 @@ are used, the directories are searched in the order specified.</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term>--ignore-virtualenv</term>
+ <listitem>
+<para>Suppress importing virtualenv-related variables to SCons.</para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term>--implicit-cache</term>
<listitem>
diff --git a/doc/user/misc.xml b/doc/user/misc.xml
index e390b7a..a595e59 100644
--- a/doc/user/misc.xml
+++ b/doc/user/misc.xml
@@ -625,4 +625,53 @@ env.Command('directory_build_info',
</section>
+ <section>
+ <title>Virtual environments (virtualenvs)</title>
+
+ <para>
+
+ Virtualenv is a tool to create isolated Python environments.
+ A python application (such as SCons) may be executed within
+ an activated virtualenv. The activation of virtualenv modifies
+ current environment by defining some virtualenv-specific variables
+ and modifying search PATH, such that executables installed within
+ virtualenv's home directory are preferred over the ones installed
+ outside of it.
+
+ </para>
+
+ <para>
+
+ Normally, SCons uses hard-coded PATH when searching for external
+ executables, so it always picks-up executables from these pre-defined
+ locations. This applies also to python interpreter, which is invoked
+ by some custom SCons tools or test suites. This means, when running
+ SCons in a virtualenv, an eventual invocation of python interpreter from
+ SCons script will most probably jump out of virtualenv and execute
+ python executable found in hard-coded SCons PATH, not the one which is
+ executing SCons. Some users may consider this as an inconsistency.
+
+ </para>
+
+ <para>
+ This issue may be overcome by using <literal>--enable-virtualenv</literal>
+ option. The option automatically imports virtualenv-related environment
+ variables to all created construction environment <literal>env['ENV']</literal>,
+ and modifies SCons PATH appropriately to prefer virtualenv's executables.
+ Setting environment variable <literal>SCONS_ENABLE_VIRTUALENV=1</literal>
+ will have same effect. If virtualenv support is enabled system-vide
+ by the environment variable, it may be suppressed with
+ <literal>--ignore-virtualenv</literal> option.
+ </para>
+
+ <para>
+ Inside of SConscript, a global function <literal>Virtualenv</literal> is
+ available. It returns a path to virtualenv's home directory, or
+ <literal>None</literal> if SCons is not running from virtualenv. Note,
+ that this function returns a path even if SCons is run from an
+ unactivated virtualenv.
+ </para>
+
+ </section>
+
</chapter>
diff --git a/src/CHANGES.txt b/src/CHANGES.txt
index bd1e56a..bb9ff98 100644
--- a/src/CHANGES.txt
+++ b/src/CHANGES.txt
@@ -15,7 +15,7 @@ RELEASE 3.1.0.alpha.yyyymmdd - NEW DATE WILL BE INSERTED HERE
- Fix the PATH created by scons.bat (and other .bat files) to provide a normalized
PATH. Some pythons in the 3.6 series are no longer able to handle paths which
have ".." in them and end up crashing. This is done by cd'ing into the directory
- we want to add to the path and then useing %CD% to give us the normalized directory
+ we want to add to the path and then using %CD% to give us the normalized directory
See bug filed under Python 3.6: https://bugs.python.org/issue32457.
Note: On Win32 PATH's which have not been normalized may cause undefined behavior
by other executables being run by SCons (or any subprocesses of executables being run by SCons).
@@ -24,8 +24,6 @@ RELEASE 3.1.0.alpha.yyyymmdd - NEW DATE WILL BE INSERTED HERE
From William Deegan:
- Remove long deprecated SCons.Options code and tests. This removes BoolOption,EnumOption,
ListOption,PackageOption, and PathOption which have been replaced by *Variable() many years ago.
- - Fix issue # 3106 MSVC if using MSVC_BATCH and target dir had a space would fail due to quirk in
- MSVC's handling of escaped targetdirs when batch compiling.
- Re-Enable parallel SCons (-j) when running via Pypy
- Move SCons test framework files to testing/framework and remove all references to QMtest.
QMTest has not been used by SCons for some time now.
@@ -35,27 +33,29 @@ RELEASE 3.1.0.alpha.yyyymmdd - NEW DATE WILL BE INSERTED HERE
- Default path for clang/clangxx : C:\Program Files\LLVM\bin
- Default path for mingw : C:\MinGW\bin and/or C:\mingw-w64\*\mingw64\bin
- Key program to locate mingw : mingw32-make (as the gcc with mingw prefix has no fixed name)
- - Fix GH Issue #3141 unicode string in a TryAction() with python 2.7 crashes.
- Fixed issue causing stack trace when python Action function contains a unicode string when being
run with Python 2.7
- Add alternate path to QT install for Centos in qt tool: /usr/lib64/qt-3.3/bin
- - Fix GH Issue #2580 - # in FRAMEWORKPATH doesn't get properly expanded. The # is left in the
- command line.
- - Fix GH Issue #3212 - Use of Py3 and CacheDir + Configure's TryCompile (or likely and Python Value Nodes)
- yielded trying to combine strings and bytes which threw exception.
- - Updated logic for mingw and clang on win32 to search default tool install paths if not
- found in normal SCons PATH. If the user specifies PATH or tool specific paths they
- will be used and the default paths below will be ignored.
- - Default path for clang/clangxx : C:\Program Files\LLVM\bin
- - Default path for mingw : c:\MinGW\bin
- Fix Java tools to search reasonable default paths for Win32, Linux, macOS. Add required paths
for swig and java native interface to JAVAINCLUDES. You should add these to your CPPPATH if you need
to compile with them. This handles spaces in paths in default Java paths on windows.
- Added more java paths to match install for Centos 7 of openjdk
- Fix new logic which populates JAVAINCLUDES to handle the case where javac is not found.
+ - Fix GH Issue #2580 - # in FRAMEWORKPATH doesn't get properly expanded. The # is left in the
+ command line.
+ - Fix issue #2980 with credit to Piotr Bartosik (and William Blevins). This is an issue where using
+ TimeStamp-MD5 Decider and CacheDir can yield incorrect md5's being written into the .sconsign.
+ The difference between Piotr Bartosik's patch and the current code is that the more complicated
+ creation of file to csig map is only done when the count of children for the current node doesn't
+ match the previous count which is loaded from the sconsign.
+ - Fix issue # 3106 MSVC if using MSVC_BATCH and target dir had a space would fail due to quirk in
+ MSVC's handling of escaped targetdirs when batch compiling.
+ - Fix GH Issue #3141 unicode string in a TryAction() with python 2.7 crashes.
+ - Fix GH Issue #3212 - Use of Py3 and CacheDir + Configure's TryCompile (or likely and Python Value Nodes)
+ yielded trying to combine strings and bytes which threw exception.
- Fix GH Issue #3225 SCons.Util.Flatten() doesn't handle MappingView's produced by dictionary as return
values from dict().{items(), keys(), values()}.
-
+
From Andrew Featherstone
- Removed unused --warn options from the man page and source code.
@@ -104,6 +104,14 @@ RELEASE 3.1.0.alpha.yyyymmdd - NEW DATE WILL BE INSERTED HERE
- In the testing framework, module TestCommon, fixed must_contain(),
must_not_contain(), and related methods of TestCommon class to work with
substrings located at zero offset.
+ - Added virtualenv support. A new function Virtualenv() determines whether
+ SCons runs in a virtualenv. The search PATH may also be extended to
+ prefer executables from the current virtualenv over the ones provided by
+ base environment. New option --enable-virtualenv provided to import some
+ virtualenv-related variables to SCons and extend every env['ENV']['PATH']
+ automatically. New option --ignore-virtualenv disables this. Two
+ environment variables, SCONS_ENABLE_VIRTUALENV and
+ SCONS_IGNORE_VIRTUALENV are supported for the same purpose.
From Richard West:
- Add SConstruct.py, Sconstruct.py, sconstruct.py to the search path for the root SConstruct file.
diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py
index 5dceaa0..77c340f 100644
--- a/src/engine/SCons/Node/FS.py
+++ b/src/engine/SCons/Node/FS.py
@@ -43,6 +43,7 @@ import stat
import sys
import time
import codecs
+from itertools import chain
import SCons.Action
import SCons.Debug
@@ -56,6 +57,7 @@ import SCons.Util
import SCons.Warnings
from SCons.Debug import Trace
+from . import DeciderNeedsNode
print_duplicate = 0
@@ -74,6 +76,9 @@ def sconsign_dir(node):
_sconsign_map = {0 : sconsign_none,
1 : sconsign_dir}
+class FileBuildInfoFileToCsigMappingError(Exception):
+ pass
+
class EntryProxyAttributeError(AttributeError):
"""
An AttributeError subclass for recording and displaying the name
@@ -285,11 +290,13 @@ def set_duplicate(duplicate):
Link_Funcs.append(link_dict[func])
def LinkFunc(target, source, env):
- # Relative paths cause problems with symbolic links, so
- # we use absolute paths, which may be a problem for people
- # who want to move their soft-linked src-trees around. Those
- # people should use the 'hard-copy' mode, softlinks cannot be
- # used for that; at least I have no idea how ...
+ """
+ Relative paths cause problems with symbolic links, so
+ we use absolute paths, which may be a problem for people
+ who want to move their soft-linked src-trees around. Those
+ people should use the 'hard-copy' mode, softlinks cannot be
+ used for that; at least I have no idea how ...
+ """
src = source[0].get_abspath()
dest = target[0].get_abspath()
dir, file = os.path.split(dest)
@@ -690,10 +697,15 @@ class Base(SCons.Node.Node):
@SCons.Memoize.CountMethodCall
def stat(self):
- try: return self._memo['stat']
- except KeyError: pass
- try: result = self.fs.stat(self.get_abspath())
- except os.error: result = None
+ try:
+ return self._memo['stat']
+ except KeyError:
+ pass
+ try:
+ result = self.fs.stat(self.get_abspath())
+ except os.error:
+ result = None
+
self._memo['stat'] = result
return result
@@ -705,13 +717,17 @@ class Base(SCons.Node.Node):
def getmtime(self):
st = self.stat()
- if st: return st[stat.ST_MTIME]
- else: return None
+ if st:
+ return st[stat.ST_MTIME]
+ else:
+ return None
def getsize(self):
st = self.stat()
- if st: return st[stat.ST_SIZE]
- else: return None
+ if st:
+ return st[stat.ST_SIZE]
+ else:
+ return None
def isdir(self):
st = self.stat()
@@ -1056,21 +1072,22 @@ _classEntry = Entry
class LocalFS(object):
-
- # This class implements an abstraction layer for operations involving
- # a local file system. Essentially, this wraps any function in
- # the os, os.path or shutil modules that we use to actually go do
- # anything with or to the local file system.
- #
- # Note that there's a very good chance we'll refactor this part of
- # the architecture in some way as we really implement the interface(s)
- # for remote file system Nodes. For example, the right architecture
- # might be to have this be a subclass instead of a base class.
- # Nevertheless, we're using this as a first step in that direction.
- #
- # We're not using chdir() yet because the calling subclass method
- # needs to use os.chdir() directly to avoid recursion. Will we
- # really need this one?
+ """
+ This class implements an abstraction layer for operations involving
+ a local file system. Essentially, this wraps any function in
+ the os, os.path or shutil modules that we use to actually go do
+ anything with or to the local file system.
+
+ Note that there's a very good chance we'll refactor this part of
+ the architecture in some way as we really implement the interface(s)
+ for remote file system Nodes. For example, the right architecture
+ might be to have this be a subclass instead of a base class.
+ Nevertheless, we're using this as a first step in that direction.
+
+ We're not using chdir() yet because the calling subclass method
+ needs to use os.chdir() directly to avoid recursion. Will we
+ really need this one?
+ """
#def chdir(self, path):
# return os.chdir(path)
def chmod(self, path, mode):
@@ -2470,11 +2487,42 @@ class FileNodeInfo(SCons.Node.NodeInfoBase):
if key not in ('__weakref__',):
setattr(self, key, value)
+ def __eq__(self, other):
+ return self.csig == other.csig and self.timestamp == other.timestamp and self.size == other.size
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
class FileBuildInfo(SCons.Node.BuildInfoBase):
- __slots__ = ()
+ """
+ This is info loaded from sconsign.
+
+ Attributes unique to FileBuildInfo:
+ dependency_map : Caches file->csig mapping
+ for all dependencies. Currently this is only used when using
+ MD5-timestamp decider.
+ It's used to ensure that we copy the correct
+ csig from previous build to be written to .sconsign when current build
+ is done. Previously the matching of csig to file was strictly by order
+ they appeared in bdepends, bsources, or bimplicit, and so a change in order
+ or count of any of these could yield writing wrong csig, and then false positive
+ rebuilds
+ """
+ __slots__ = ('dependency_map')
current_version_id = 2
+ def __setattr__(self, key, value):
+
+ # If any attributes are changed in FileBuildInfo, we need to
+ # invalidate the cached map of file name to content signature
+ # heald in dependency_map. Currently only used with
+ # MD5-timestamp decider
+ if key != 'dependency_map' and hasattr(self, 'dependency_map'):
+ del self.dependency_map
+
+ return super(FileBuildInfo, self).__setattr__(key, value)
+
def convert_to_sconsign(self):
"""
Converts this FileBuildInfo object for writing to a .sconsign file
@@ -3243,9 +3291,108 @@ class File(Base):
def changed_state(self, target, prev_ni):
return self.state != SCons.Node.up_to_date
- def changed_timestamp_then_content(self, target, prev_ni):
+
+ # Caching node -> string mapping for the below method
+ __dmap_cache = {}
+ __dmap_sig_cache = {}
+
+
+ def _build_dependency_map(self, binfo):
+ """
+ Build mapping from file -> signature
+
+ Args:
+ self - self
+ binfo - buildinfo from node being considered
+
+ Returns:
+ dictionary of file->signature mappings
+ """
+
+ # For an "empty" binfo properties like bsources
+ # do not exist: check this to avoid exception.
+ if (len(binfo.bsourcesigs) + len(binfo.bdependsigs) + \
+ len(binfo.bimplicitsigs)) == 0:
+ return {}
+
+
+ # store this info so we can avoid regenerating it.
+ binfo.dependency_map = { str(child):signature for child, signature in zip(chain(binfo.bsources, binfo.bdepends, binfo.bimplicit),
+ chain(binfo.bsourcesigs, binfo.bdependsigs, binfo.bimplicitsigs))}
+
+ return binfo.dependency_map
+
+ def _get_previous_signatures(self, dmap):
+ """
+ Return a list of corresponding csigs from previous
+ build in order of the node/files in children.
+
+ Args:
+ self - self
+ dmap - Dictionary of file -> csig
+
+ Returns:
+ List of csigs for provided list of children
+ """
+ prev = []
+
+ # First try the simple name for node
+ c_str = str(self)
+ if os.altsep:
+ c_str = c_str.replace(os.sep, os.altsep)
+ df = dmap.get(c_str, None)
+ if not df:
+ try:
+ # this should yield a path which matches what's in the sconsign
+ c_str = self.get_path()
+ if os.altsep:
+ c_str = c_str.replace(os.sep, os.altsep)
+
+ df = dmap.get(c_str, None)
+
+ except AttributeError as e:
+ raise FileBuildInfoFileToCsigMappingError("No mapping from file name to content signature for :%s"%c_str)
+
+ return df
+
+ def changed_timestamp_then_content(self, target, prev_ni, node=None):
+ """
+ Used when decider for file is Timestamp-MD5
+
+ NOTE: If the timestamp hasn't changed this will skip md5'ing the
+ file and just copy the prev_ni provided. If the prev_ni
+ is wrong. It will propagate it.
+ See: https://github.com/SCons/scons/issues/2980
+
+ Args:
+ self - dependency
+ target - target
+ prev_ni - The NodeInfo object loaded from previous builds .sconsign
+ node - Node instance. This is the only changed* function which requires
+ node to function. So if we detect that it's not passed.
+ we throw DeciderNeedsNode, and caller should handle this and pass node.
+
+ Returns:
+ Boolean - Indicates if node(File) has changed.
+ """
+ if node is None:
+ # We need required node argument to get BuildInfo to function
+ raise DeciderNeedsNode(self.changed_timestamp_then_content)
+
+ # Now get sconsign name -> csig map and then get proper prev_ni if possible
+ bi = node.get_stored_info().binfo
+ rebuilt = False
+ try:
+ dependency_map = bi.dependency_map
+ except AttributeError as e:
+ dependency_map = self._build_dependency_map(bi)
+ rebuilt = True
+
+ prev_ni = self._get_previous_signatures(dependency_map)
+
if not self.changed_timestamp_match(target, prev_ni):
try:
+ # NOTE: We're modifying the current node's csig in a query.
self.get_ninfo().csig = prev_ni.csig
except AttributeError:
pass
@@ -3259,6 +3406,12 @@ class File(Base):
return 1
def changed_timestamp_match(self, target, prev_ni):
+ """
+ Return True if the timestamps don't match or if there is no previous timestamp
+ :param target:
+ :param prev_ni: Information about the node from the previous build
+ :return:
+ """
try:
return self.get_timestamp() != prev_ni.timestamp
except AttributeError:
@@ -3280,7 +3433,9 @@ class File(Base):
# ...and they'd like a local copy.
e = LocalCopy(self, r, None)
if isinstance(e, SCons.Errors.BuildError):
- raise
+ # Likely this should be re-raising exception e
+ # (which would be BuildError)
+ raise e
SCons.Node.store_info_map[self.store_info](self)
if T: Trace(' 1\n')
return 1
@@ -3301,9 +3456,12 @@ class File(Base):
result = self
if not self.exists():
norm_name = _my_normcase(self.name)
- for dir in self.dir.get_all_rdirs():
- try: node = dir.entries[norm_name]
- except KeyError: node = dir.file_on_disk(self.name)
+ for repo_dir in self.dir.get_all_rdirs():
+ try:
+ node = repo_dir.entries[norm_name]
+ except KeyError:
+ node = repo_dir.file_on_disk(self.name)
+
if node and node.exists() and \
(isinstance(node, File) or isinstance(node, Entry)
or not node.is_derived()):
@@ -3325,6 +3483,28 @@ class File(Base):
self._memo['rfile'] = result
return result
+ def find_repo_file(self):
+ """
+ For this node, find if there exists a corresponding file in one or more repositories
+ :return: list of corresponding files in repositories
+ """
+ retvals = []
+
+ norm_name = _my_normcase(self.name)
+ for repo_dir in self.dir.get_all_rdirs():
+ try:
+ node = repo_dir.entries[norm_name]
+ except KeyError:
+ node = repo_dir.file_on_disk(self.name)
+
+ if node and node.exists() and \
+ (isinstance(node, File) or isinstance(node, Entry) \
+ or not node.is_derived()):
+ retvals.append(node)
+
+ return retvals
+
+
def rstr(self):
return str(self.rfile())
diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py
index 698f574..23ec48e 100644
--- a/src/engine/SCons/Node/FSTests.py
+++ b/src/engine/SCons/Node/FSTests.py
@@ -41,6 +41,7 @@ import SCons.Errors
import SCons.Node.FS
import SCons.Util
import SCons.Warnings
+import SCons.Environment
built_it = None
@@ -1133,7 +1134,10 @@ class FSTestCase(_tempdirTestCase):
e1 = fs.Entry(p)
e2 = fs.Entry(path)
assert e1 is e2, (e1, e2)
- assert str(e1) is str(e2), (str(e1), str(e2))
+ a=str(e1)
+ b=str(e2)
+ assert a == b, ("Strings should match for same file/node\n%s\n%s"%(a,b))
+
# Test for a bug in 0.04 that did not like looking up
# dirs with a trailing slash on Windows.
@@ -2460,6 +2464,139 @@ class FileTestCase(_tempdirTestCase):
assert not build_f1.exists(), "%s did not realize that %s disappeared" % (build_f1, src_f1)
assert not os.path.exists(build_f1.get_abspath()), "%s did not get removed after %s was removed" % (build_f1, src_f1)
+ def test_changed(self):
+ """
+ Verify that changes between BuildInfo's list of souces, depends, and implicit
+ dependencies do not corrupt content signature values written to .SConsign
+ when using CacheDir and Timestamp-MD5 decider.
+ This is for issue #2980
+ """
+ # node should have
+ # 1 source (for example main.cpp)
+ # 0 depends
+ # N implicits (for example ['alpha.h', 'beta.h', 'gamma.h', '/usr/bin/g++'])
+
+ class ChangedNode(SCons.Node.FS.File):
+ def __init__(self, name, directory=None, fs=None):
+ SCons.Node.FS.File.__init__(self, name, directory, fs)
+ self.name = name
+ self.Tag('found_includes', [])
+ self.stored_info = None
+ self.build_env = None
+ self.changed_since_last_build = 4
+ self.timestamp = 1
+
+ def get_stored_info(self):
+ return self.stored_info
+
+ def get_build_env(self):
+ return self.build_env
+
+ def get_timestamp(self):
+ """ Fake timestamp so they always match"""
+ return self.timestamp
+
+ def get_contents(self):
+ return self.name
+
+ def get_ninfo(self):
+ """ mocked to ensure csig will equal the filename"""
+ try:
+ return self.ninfo
+ except AttributeError:
+ self.ninfo = FakeNodeInfo(self.name, self.timestamp)
+ return self.ninfo
+
+ def get_csig(self):
+ ninfo = self.get_ninfo()
+ try:
+ return ninfo.csig
+ except AttributeError:
+ pass
+
+ return "Should Never Happen"
+
+ class ChangedEnvironment(SCons.Environment.Base):
+
+ def __init__(self):
+ SCons.Environment.Base.__init__(self)
+ self.decide_source = self._changed_timestamp_then_content
+
+ class FakeNodeInfo(object):
+ def __init__(self, csig, timestamp):
+ self.csig = csig
+ self.timestamp = timestamp
+
+ #Create nodes
+ fs = SCons.Node.FS.FS()
+ d = self.fs.Dir('.')
+
+ node = ChangedNode('main.o',d,fs) # main.o
+ s1 = ChangedNode('main.cpp',d,fs) # main.cpp
+ s1.timestamp = 2 # this changed
+ i1 = ChangedNode('alpha.h',d,fs) # alpha.h - The bug is caused because the second build adds this file
+ i1.timestamp = 2 # This is the new file.
+ i2 = ChangedNode('beta.h',d,fs) # beta.h
+ i3 = ChangedNode('gamma.h',d,fs) # gamma.h - In the bug beta.h's csig from binfo overwrites this ones
+ i4 = ChangedNode('/usr/bin/g++',d,fs) # /usr/bin/g++
+
+ node.add_source([s1])
+ node.add_dependency([])
+ node.implicit = [i1, i2, i3, i4]
+ node.implicit_set = set()
+ # node._add_child(node.implicit, node.implicit_set, [n7, n8, n9])
+ # node._add_child(node.implicit, node.implicit_set, [n10, n11, n12])
+
+ # Mock out node's scan method
+ # node.scan = lambda *args: None
+
+ # Mock out nodes' children() ?
+ # Should return Node's.
+ # All those nodes should have changed_since_last_build set to match Timestamp-MD5's
+ # decider method...
+
+ # Generate sconsign entry needed
+ sconsign_entry = SCons.SConsign.SConsignEntry()
+ sconsign_entry.binfo = node.new_binfo()
+ sconsign_entry.ninfo = node.new_ninfo()
+
+ # mock out loading info from sconsign
+ # This will cause node.get_stored_info() to return our freshly created sconsign_entry
+ node.stored_info = sconsign_entry
+
+ # binfo = information from previous build (from sconsign)
+ # We'll set the following attributes (which are lists): "bsources", "bsourcesigs",
+ # "bdepends","bdependsigs", "bimplicit", "bimplicitsigs"
+ bi = sconsign_entry.binfo
+ bi.bsources = ['main.cpp']
+ bi.bsourcesigs=[FakeNodeInfo('main.cpp',1),]
+
+ bi.bdepends = []
+ bi.bdependsigs = []
+
+ bi.bimplicit = ['beta.h','gamma.h']
+ bi.bimplicitsigs = [FakeNodeInfo('beta.h',1), FakeNodeInfo('gamma.h',1)]
+
+ ni = sconsign_entry.ninfo
+ # We'll set the following attributes (which are lists): sources, depends, implicit lists
+
+ #Set timestamp-md5
+ #Call changed
+ #Check results
+ node.build_env = ChangedEnvironment()
+
+ changed = node.changed()
+
+ # change to true to debug
+ if False:
+ print("Changed:%s"%changed)
+ print("%15s -> csig:%s"%(s1.name, s1.ninfo.csig))
+ print("%15s -> csig:%s"%(i1.name, i1.ninfo.csig))
+ print("%15s -> csig:%s"%(i2.name, i2.ninfo.csig))
+ print("%15s -> csig:%s"%(i3.name, i3.ninfo.csig))
+ print("%15s -> csig:%s"%(i4.name, i4.ninfo.csig))
+
+ self.assertEqual(i2.name,i2.ninfo.csig, "gamma.h's fake csig should equal gamma.h but equals:%s"%i2.ninfo.csig)
class GlobTestCase(_tempdirTestCase):
@@ -3752,38 +3889,7 @@ class AbsolutePathTestCase(unittest.TestCase):
if __name__ == "__main__":
- suite = unittest.TestSuite()
- suite.addTest(VariantDirTestCase())
- suite.addTest(find_fileTestCase())
- suite.addTest(StringDirTestCase())
- suite.addTest(stored_infoTestCase())
- suite.addTest(has_src_builderTestCase())
- suite.addTest(prepareTestCase())
- suite.addTest(SConstruct_dirTestCase())
- suite.addTest(clearTestCase())
- suite.addTest(disambiguateTestCase())
- suite.addTest(postprocessTestCase())
- suite.addTest(SpecialAttrTestCase())
- suite.addTest(SaveStringsTestCase())
- tclasses = [
- AbsolutePathTestCase,
- BaseTestCase,
- CacheDirTestCase,
- DirTestCase,
- DirBuildInfoTestCase,
- DirNodeInfoTestCase,
- EntryTestCase,
- FileTestCase,
- FileBuildInfoTestCase,
- FileNodeInfoTestCase,
- FSTestCase,
- GlobTestCase,
- RepositoryTestCase,
- ]
- for tclass in tclasses:
- names = unittest.getTestCaseNames(tclass, 'test_')
- suite.addTests(list(map(tclass, names)))
- TestUnit.run(suite)
+ unittest.main()
# Local Variables:
# tab-width:4
diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py
index ca6c883..7dc5f5d 100644
--- a/src/engine/SCons/Node/NodeTests.py
+++ b/src/engine/SCons/Node/NodeTests.py
@@ -1349,6 +1349,7 @@ class NodeListTestCase(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
+
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py
index c18a954..131953b 100644
--- a/src/engine/SCons/Node/__init__.py
+++ b/src/engine/SCons/Node/__init__.py
@@ -45,6 +45,7 @@ from __future__ import print_function
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+import os
import collections
import copy
from itertools import chain
@@ -139,6 +140,7 @@ def exists_entry(node):
node.disambiguate()
return _exists_map[node._func_exists](node)
+
def exists_file(node):
# Duplicate from source path if we are set up to do this.
if node.duplicate and not node.is_derived() and not node.linked:
@@ -246,6 +248,21 @@ _target_from_source_map = {0 : target_from_source_none,
# used by it.
#
+
+class DeciderNeedsNode(Exception):
+ """
+ Indicate that the decider needs the node as well as the target and the dependency.
+ Normally the node and the target are the same, but in the case of repository
+ They may be different. Also the NodeInfo is retrieved from the node
+ """
+ def __init__(self, call_this_decider):
+ """
+ :param call_this_decider: to return the decider to call directly since deciders
+ are called through several levels of indirection
+ """
+ self.decider = call_this_decider
+
+
#
# First, the single decider functions
#
@@ -269,6 +286,7 @@ def changed_since_last_build_node(node, target, prev_ni):
"""
raise NotImplementedError
+
def changed_since_last_build_alias(node, target, prev_ni):
cur_csig = node.get_csig()
try:
@@ -276,19 +294,24 @@ def changed_since_last_build_alias(node, target, prev_ni):
except AttributeError:
return 1
+
def changed_since_last_build_entry(node, target, prev_ni):
node.disambiguate()
return _decider_map[node.changed_since_last_build](node, target, prev_ni)
+
def changed_since_last_build_state_changed(node, target, prev_ni):
- return (node.state != SCons.Node.up_to_date)
+ return node.state != SCons.Node.up_to_date
+
def decide_source(node, target, prev_ni):
return target.get_build_env().decide_source(node, target, prev_ni)
+
def decide_target(node, target, prev_ni):
return target.get_build_env().decide_target(node, target, prev_ni)
+
def changed_since_last_build_python(node, target, prev_ni):
cur_csig = node.get_csig()
try:
@@ -380,6 +403,7 @@ class NodeInfoBase(object):
"""
state = other.__getstate__()
self.__setstate__(state)
+
def format(self, field_list=None, names=0):
if field_list is None:
try:
@@ -1136,7 +1160,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):
binfo.bactsig = SCons.Util.MD5signature(executor.get_contents())
if self._specific_sources:
- sources = [ s for s in self.sources if not s in ignore_set]
+ sources = [s for s in self.sources if not s in ignore_set]
else:
sources = executor.get_unignored_sources(self, self.ignore)
@@ -1145,13 +1169,17 @@ class Node(object, with_metaclass(NoSlotsPyPy)):
binfo.bsources = [s for s in sources if s not in seen and not seen.add(s)]
binfo.bsourcesigs = [s.get_ninfo() for s in binfo.bsources]
+ binfo.bdepends = [d for d in self.depends if d not in ignore_set]
+ binfo.bdependsigs = [d.get_ninfo() for d in self.depends]
- binfo.bdepends = self.depends
- binfo.bdependsigs = [d.get_ninfo() for d in self.depends if d not in ignore_set]
-
- binfo.bimplicit = self.implicit or []
- binfo.bimplicitsigs = [i.get_ninfo() for i in binfo.bimplicit if i not in ignore_set]
-
+ # Because self.implicit is initialized to None (and not empty list [])
+ # we have to handle this case
+ if not self.implicit:
+ binfo.bimplicit = []
+ binfo.bimplicitsigs = []
+ else:
+ binfo.bimplicit = [i for i in self.implicit if i not in ignore_set]
+ binfo.bimplicitsigs = [i.get_ninfo() for i in binfo.bimplicit]
return binfo
@@ -1213,7 +1241,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):
return _exists_map[self._func_exists](self)
def rexists(self):
- """Does this node exist locally or in a repositiory?"""
+ """Does this node exist locally or in a repository?"""
# There are no repositories by default:
return _rexists_map[self._func_rexists](self)
@@ -1452,13 +1480,18 @@ class Node(object, with_metaclass(NoSlotsPyPy)):
result = True
for child, prev_ni in zip(children, then):
- if _decider_map[child.changed_since_last_build](child, self, prev_ni):
- if t: Trace(': %s changed' % child)
- result = True
+ try:
+ if _decider_map[child.changed_since_last_build](child, self, prev_ni):
+ if t: Trace(': %s changed' % child)
+ result = True
+ except DeciderNeedsNode as e:
+ if e.decider(self, prev_ni, node=node):
+ if t: Trace(': %s changed' % child)
+ result = True
- contents = self.get_executor().get_contents()
if self.has_builder():
import SCons.Util
+ contents = self.get_executor().get_contents()
newsig = SCons.Util.MD5signature(contents)
if bi.bactsig != newsig:
if t: Trace(': bactsig %s != newsig %s' % (bi.bactsig, newsig))
@@ -1607,7 +1640,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):
# so we only print them after running them through this lambda
# to turn them into the right relative Node and then return
# its string.
- def stringify( s, E=self.dir.Entry ) :
+ def stringify( s, E=self.dir.Entry):
if hasattr( s, 'dir' ) :
return str(E(s))
return str(s)
@@ -1616,15 +1649,21 @@ class Node(object, with_metaclass(NoSlotsPyPy)):
removed = [x for x in old_bkids if not x in new_bkids]
if removed:
- removed = list(map(stringify, removed))
+ removed = [stringify(r) for r in removed]
fmt = "`%s' is no longer a dependency\n"
lines.extend([fmt % s for s in removed])
for k in new_bkids:
if not k in old_bkids:
lines.append("`%s' is a new dependency\n" % stringify(k))
- elif _decider_map[k.changed_since_last_build](k, self, osig[k]):
- lines.append("`%s' changed\n" % stringify(k))
+ else:
+ try:
+ changed = _decider_map[k.changed_since_last_build](k, self, osig[k])
+ except DeciderNeedsNode as e:
+ changed = e.decider(self, osig[k], node=self)
+
+ if changed:
+ lines.append("`%s' changed\n" % stringify(k))
if len(lines) == 0 and old_bkids != new_bkids:
lines.append("the dependency order changed:\n" +
diff --git a/src/engine/SCons/Platform/PlatformTests.py b/src/engine/SCons/Platform/PlatformTests.py
index 6f720ec..3f42eae 100644
--- a/src/engine/SCons/Platform/PlatformTests.py
+++ b/src/engine/SCons/Platform/PlatformTests.py
@@ -36,7 +36,7 @@ import SCons.Action
class Environment(collections.UserDict):
def Detect(self, cmd):
return cmd
-
+
def AppendENVPath(self, key, value):
pass
@@ -174,9 +174,9 @@ class TempFileMungeTestCase(unittest.TestCase):
SCons.Action.print_actions = 0
# Create an instance of object derived class to allow setattrb
class Node(object) :
- class Attrs(object):
+ class Attrs(object):
pass
- def __init__(self):
+ def __init__(self):
self.attributes = self.Attrs()
target = [Node()]
cmd = t(target, None, env, 0)
@@ -203,7 +203,7 @@ class PlatformEscapeTestCase(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
-
+
# Local Variables:
# tab-width:4
diff --git a/src/engine/SCons/Platform/__init__.py b/src/engine/SCons/Platform/__init__.py
index 1654d0a..8784d5e 100644
--- a/src/engine/SCons/Platform/__init__.py
+++ b/src/engine/SCons/Platform/__init__.py
@@ -87,6 +87,7 @@ def platform_default():
else:
return sys.platform
+
def platform_module(name = platform_default()):
"""Return the imported module for the platform.
@@ -117,11 +118,13 @@ def platform_module(name = platform_default()):
setattr(SCons.Platform, name, mod)
return sys.modules[full_name]
+
def DefaultToolList(platform, env):
"""Select a default tool list for the specified platform.
"""
return SCons.Tool.tool_list(platform, env)
+
class PlatformSpec(object):
def __init__(self, name, generate):
self.name = name
@@ -133,6 +136,7 @@ class PlatformSpec(object):
def __str__(self):
return self.name
+
class TempFileMunge(object):
"""A callable class. You can set an Environment variable to this,
then call it with a string argument, then it will perform temporary
diff --git a/src/engine/SCons/Platform/posix.py b/src/engine/SCons/Platform/posix.py
index ae2ad1a..ad4e859 100644
--- a/src/engine/SCons/Platform/posix.py
+++ b/src/engine/SCons/Platform/posix.py
@@ -41,6 +41,8 @@ import select
import SCons.Util
from SCons.Platform import TempFileMunge
+from SCons.Platform.virtualenv import ImportVirtualenv
+from SCons.Platform.virtualenv import ignore_virtualenv, enable_virtualenv
exitvalmap = {
2 : 127,
@@ -119,6 +121,9 @@ def generate(env):
# Must be able to have GCC and DMD work in the same build, so:
env['__DRPATH'] = '$_DRPATH'
+ if enable_virtualenv and not ignore_virtualenv:
+ ImportVirtualenv(env)
+
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
diff --git a/src/engine/SCons/Platform/virtualenv.py b/src/engine/SCons/Platform/virtualenv.py
new file mode 100644
index 0000000..03fb486
--- /dev/null
+++ b/src/engine/SCons/Platform/virtualenv.py
@@ -0,0 +1,120 @@
+"""SCons.Platform.virtualenv
+
+Support for virtualenv.
+"""
+
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import sys
+import SCons.Util
+
+
+virtualenv_enabled_by_default = False
+
+
+def _enable_virtualenv_default():
+ return SCons.Util.get_bool_envvar('SCONS_ENABLE_VIRTUALENV', virtualenv_enabled_by_default)
+
+
+def _ignore_virtualenv_default():
+ return SCons.Util.get_bool_envvar('SCONS_IGNORE_VIRTUALENV', False)
+
+
+enable_virtualenv = _enable_virtualenv_default()
+ignore_virtualenv = _ignore_virtualenv_default()
+virtualenv_variables = ['VIRTUAL_ENV', 'PIPENV_ACTIVE']
+
+
+def _running_in_virtualenv():
+ """Returns True, if scons is executed within a virtualenv"""
+ # see https://stackoverflow.com/a/42580137
+ return (hasattr(sys, 'real_prefix') or
+ (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
+
+
+def _is_path_in(path, base):
+ """Returns true, if **path** is located under the **base** directory."""
+ if not path or not base: # empty path may happen, base too
+ return False
+ rp = os.path.relpath(path, base)
+ return ((not rp.startswith(os.path.pardir)) and (not rp == os.path.curdir))
+
+
+def _inject_venv_variables(env):
+ if 'ENV' not in env:
+ env['ENV'] = {}
+ ENV = env['ENV']
+ for name in virtualenv_variables:
+ try:
+ ENV[name] = os.environ[name]
+ except KeyError:
+ pass
+
+def _inject_venv_path(env, path_list=None):
+ """Modify environment such that SCons will take into account its virtualenv
+ when running external tools."""
+ if path_list is None:
+ path_list = os.getenv('PATH')
+ env.PrependENVPath('PATH', select_paths_in_venv(path_list))
+
+
+def select_paths_in_venv(path_list):
+ """Returns a list of paths from **path_list** which are under virtualenv's
+ home directory."""
+ if SCons.Util.is_String(path_list):
+ path_list = path_list.split(os.path.pathsep)
+ # Find in path_list the paths under the virtualenv's home
+ return [path for path in path_list if IsInVirtualenv(path)]
+
+
+def ImportVirtualenv(env):
+ """Copies virtualenv-related environment variables from OS environment
+ to ``env['ENV']`` and prepends virtualenv's PATH to ``env['ENV']['PATH']``.
+ """
+ _inject_venv_variables(env)
+ _inject_venv_path(env)
+
+
+def Virtualenv():
+ """Returns path to the virtualenv home if scons is executing within a
+ virtualenv or None, if not."""
+ if _running_in_virtualenv():
+ return sys.prefix
+ return None
+
+
+def IsInVirtualenv(path):
+ """Returns True, if **path** is under virtualenv's home directory. If not,
+ or if we don't use virtualenv, returns False."""
+ return _is_path_in(path, Virtualenv())
+
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/src/engine/SCons/Platform/virtualenvTests.py b/src/engine/SCons/Platform/virtualenvTests.py
new file mode 100644
index 0000000..02b37ab
--- /dev/null
+++ b/src/engine/SCons/Platform/virtualenvTests.py
@@ -0,0 +1,243 @@
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import SCons.compat
+
+import collections
+import unittest
+import os
+import sys
+
+import SCons.Platform.virtualenv
+import SCons.Util
+
+class Environment(collections.UserDict):
+ def Detect(self, cmd):
+ return cmd
+
+ def AppendENVPath(self, key, value):
+ if SCons.Util.is_List(value):
+ value = os.path.pathsep.join(value)
+ if 'ENV' not in self:
+ self['ENV'] = {}
+ current = self['ENV'].get(key)
+ if not current:
+ self['ENV'][key] = value
+ else:
+ self['ENV'][key] = os.path.pathsep.join([current, value])
+
+ def PrependENVPath(self, key, value):
+ if SCons.Util.is_List(value):
+ value = os.path.pathsep.join(value)
+ if 'ENV' not in self:
+ self['ENV'] = {}
+ current = self['ENV'].get(key)
+ if not current:
+ self['ENV'][key] = value
+ else:
+ self['ENV'][key] = os.path.pathsep.join([value, current])
+
+class SysPrefixes(object):
+ """Used to temporarily mock sys.prefix, sys.real_prefix and sys.base_prefix"""
+ def __init__(self, prefix, real_prefix=None, base_prefix=None):
+ self._prefix = prefix
+ self._real_prefix = real_prefix
+ self._base_prefix = base_prefix
+
+ def start(self):
+ self._store()
+ sys.prefix = self._prefix
+ if self._real_prefix is None:
+ if hasattr(sys, 'real_prefix'):
+ del sys.real_prefix
+ else:
+ sys.real_prefix = self._real_prefix
+ if self._base_prefix is None:
+ if hasattr(sys, 'base_prefix'):
+ del sys.base_prefix
+ else:
+ sys.base_prefix = self._base_prefix
+
+ def stop(self):
+ self._restore()
+
+ def __enter__(self):
+ self.start()
+ attrs = ('prefix', 'real_prefix', 'base_prefix')
+ return {k: getattr(sys, k) for k in attrs if hasattr(sys, k)}
+
+ def __exit__(self, *args):
+ self.stop()
+
+ def _store(self):
+ s = dict()
+ if hasattr(sys, 'real_prefix'):
+ s['real_prefix'] = sys.real_prefix
+ if hasattr(sys, 'base_prefix'):
+ s['base_prefix'] = sys.base_prefix
+ s['prefix'] = sys.prefix
+ self._stored = s
+
+ def _restore(self):
+ s = self._stored
+ if 'real_prefix' in s:
+ sys.real_prefix = s['real_prefix']
+ if 'base_prefix' in s:
+ sys.base_prefix = s['base_prefix']
+ if 'prefix' in s:
+ sys.prefix = s['prefix']
+ del self._stored
+
+def _p(p):
+ """Converts path string **p** from posix format to os-specific format."""
+ drive = []
+ if p.startswith('/') and sys.platform == 'win32':
+ drive = ['C:']
+ pieces = p.split('/')
+ return os.path.sep.join(drive + pieces)
+
+
+class _is_path_in_TestCase(unittest.TestCase):
+ def test_false(self):
+ for args in [ ('',''),
+ ('', _p('/foo/bar')),
+ (_p('/foo/bar'), ''),
+ (_p('/foo/bar'), _p('/foo/bar')),
+ (_p('/foo/bar'), _p('/foo/bar/geez')),
+ (_p('/'), _p('/foo')),
+ (_p('foo'), _p('foo/bar')) ]:
+ assert SCons.Platform.virtualenv._is_path_in(*args) is False, "_is_path_in(%r, %r) should be False" % args
+
+ def test__true(self):
+ for args in [ (_p('/foo'), _p('/')),
+ (_p('/foo/bar'), _p('/foo')),
+ (_p('/foo/bar/geez'), _p('/foo/bar')),
+ (_p('/foo//bar//geez'), _p('/foo/bar')),
+ (_p('/foo/bar/geez'), _p('/foo//bar')),
+ (_p('/foo/bar/geez'), _p('//foo//bar')) ]:
+ assert SCons.Platform.virtualenv._is_path_in(*args) is True, "_is_path_in(%r, %r) should be True" % args
+
+class IsInVirtualenvTestCase(unittest.TestCase):
+ def test_false(self):
+ # "without wirtualenv" - always false
+ with SysPrefixes(_p('/prefix')):
+ for p in [ _p(''),
+ _p('/foo'),
+ _p('/prefix'),
+ _p('/prefix/foo') ]:
+ assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, "IsInVirtualenv(%r) should be False" % p
+
+ # "with virtualenv"
+ with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')):
+ for p in [ _p(''),
+ _p('/real/prefix/foo'),
+ _p('/virtualenv/prefix'),
+ _p('/virtualenv/prefix/bar/..'),
+ _p('/virtualenv/prefix/bar/../../bleah'),
+ _p('/virtualenv/bleah') ]:
+ assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, "IsInVirtualenv(%r) should be False" % p
+
+ # "with venv"
+ with SysPrefixes(_p('/virtualenv/prefix'), base_prefix=_p('/base/prefix')):
+ for p in [ _p(''),
+ _p('/base/prefix/foo'),
+ _p('/virtualenv/prefix'),
+ _p('/virtualenv/prefix/bar/..'),
+ _p('/virtualenv/prefix/bar/../../bleah'),
+ _p('/virtualenv/bleah') ]:
+ assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, "IsInVirtualenv(%r) should be False" % p
+
+ def test_true(self):
+ # "with virtualenv"
+ with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')):
+ for p in [ _p('/virtualenv/prefix/foo'),
+ _p('/virtualenv/prefix/foo/bar') ]:
+ assert SCons.Platform.virtualenv.IsInVirtualenv(p) is True, "IsInVirtualenv(%r) should be True" % p
+
+ # "with venv"
+ with SysPrefixes(_p('/virtualenv/prefix'), base_prefix=_p('/base/prefix')):
+ for p in [ _p('/virtualenv/prefix/foo'),
+ _p('/virtualenv/prefix/foo/bar') ]:
+ assert SCons.Platform.virtualenv.IsInVirtualenv(p) is True, "IsInVirtualenv(%r) should be True" % p
+
+class _inject_venv_pathTestCase(unittest.TestCase):
+ def path_list(self):
+ return [
+ _p('/virtualenv/prefix/bin'),
+ _p('/virtualenv/prefix'),
+ _p('/virtualenv/prefix/../bar'),
+ _p('/home/user/.local/bin'),
+ _p('/usr/bin'),
+ _p('/opt/bin')
+ ]
+ def test_with_path_string(self):
+ env = Environment()
+ path_string = os.path.pathsep.join(self.path_list())
+ with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')):
+ SCons.Platform.virtualenv._inject_venv_path(env, path_string)
+ assert env['ENV']['PATH'] == _p('/virtualenv/prefix/bin'), env['ENV']['PATH']
+
+ def test_with_path_list(self):
+ env = Environment()
+ with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')):
+ SCons.Platform.virtualenv._inject_venv_path(env, self.path_list())
+ assert env['ENV']['PATH'] == _p('/virtualenv/prefix/bin'), env['ENV']['PATH']
+
+class VirtualenvTestCase(unittest.TestCase):
+ def test_none(self):
+ def _msg(given):
+ return "Virtualenv() should be None, not %s" % repr(given)
+
+ with SysPrefixes(_p('/prefix')):
+ ve = SCons.Platform.virtualenv.Virtualenv()
+ assert ve is None , _msg(ve)
+ with SysPrefixes(_p('/base/prefix'), base_prefix=_p('/base/prefix')):
+ ve = SCons.Platform.virtualenv.Virtualenv()
+ assert ve is None, _msg(ve)
+
+ def test_not_none(self):
+ def _msg(expected, given):
+ return "Virtualenv() should == %r, not %s" % (_p(expected), repr(given))
+
+ with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')):
+ ve = SCons.Platform.virtualenv.Virtualenv()
+ assert ve == _p('/virtualenv/prefix'), _msg('/virtualenv/prefix', ve)
+ with SysPrefixes(_p('/same/prefix'), real_prefix=_p('/same/prefix')):
+ ve = SCons.Platform.virtualenv.Virtualenv()
+ assert ve == _p('/same/prefix'), _msg('/same/prefix', ve)
+ with SysPrefixes(_p('/virtualenv/prefix'), base_prefix=_p('/base/prefix')):
+ ve = SCons.Platform.virtualenv.Virtualenv()
+ assert ve == _p('/virtualenv/prefix'), _msg('/virtualenv/prefix', ve)
+
+
+if __name__ == "__main__":
+ unittest.main()
+
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/src/engine/SCons/Platform/win32.py b/src/engine/SCons/Platform/win32.py
index ea2fd3f..3b76208 100644
--- a/src/engine/SCons/Platform/win32.py
+++ b/src/engine/SCons/Platform/win32.py
@@ -39,6 +39,8 @@ import tempfile
from SCons.Platform.posix import exitvalmap
from SCons.Platform import TempFileMunge
+from SCons.Platform.virtualenv import ImportVirtualenv
+from SCons.Platform.virtualenv import ignore_virtualenv, enable_virtualenv
import SCons.Util
try:
@@ -436,7 +438,7 @@ def generate(env):
if v:
env['ENV']['COMSPEC'] = v
- env.AppendENVPath('PATH', get_system_root() + '\System32')
+ env.AppendENVPath('PATH', get_system_root() + '\\System32')
env['ENV']['PATHEXT'] = '.COM;.EXE;.BAT;.CMD'
env['OBJPREFIX'] = ''
@@ -462,6 +464,9 @@ def generate(env):
env['HOST_OS'] = 'win32'
env['HOST_ARCH'] = get_architecture().arch
+ if enable_virtualenv and not ignore_virtualenv:
+ ImportVirtualenv(env)
+
# Local Variables:
# tab-width:4
diff --git a/src/engine/SCons/Script/Main.py b/src/engine/SCons/Script/Main.py
index 6516a15..2c59808 100644
--- a/src/engine/SCons/Script/Main.py
+++ b/src/engine/SCons/Script/Main.py
@@ -59,6 +59,7 @@ import SCons.Job
import SCons.Node
import SCons.Node.FS
import SCons.Platform
+import SCons.Platform.virtualenv
import SCons.SConf
import SCons.Script
import SCons.Taskmaster
@@ -863,6 +864,13 @@ def _main(parser):
for warning_type, message in delayed_warnings:
SCons.Warnings.warn(warning_type, message)
+ if not SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ if options.enable_virtualenv:
+ SCons.Platform.virtualenv.enable_virtualenv = True
+
+ if options.ignore_virtualenv:
+ SCons.Platform.virtualenv.ignore_virtualenv = True
+
if options.diskcheck:
SCons.Node.FS.set_diskcheck(options.diskcheck)
diff --git a/src/engine/SCons/Script/SConsOptions.py b/src/engine/SCons/Script/SConsOptions.py
index 60d456e..37dd644 100644
--- a/src/engine/SCons/Script/SConsOptions.py
+++ b/src/engine/SCons/Script/SConsOptions.py
@@ -38,6 +38,7 @@ except ImportError:
_ = gettext
import SCons.Node.FS
+import SCons.Platform.virtualenv
import SCons.Warnings
OptionValueError = optparse.OptionValueError
@@ -706,6 +707,12 @@ def Parser(version):
action="callback", callback=opt_duplicate,
help=opt_duplicate_help)
+ if not SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ op.add_option('--enable-virtualenv',
+ dest="enable_virtualenv",
+ action="store_true",
+ help="Import certain virtualenv variables to SCons")
+
op.add_option('-f', '--file', '--makefile', '--sconstruct',
nargs=1, type="string",
dest="file", default=[],
@@ -733,6 +740,11 @@ def Parser(version):
help="Search DIR for imported Python modules.",
metavar="DIR")
+ op.add_option('--ignore-virtualenv',
+ dest="ignore_virtualenv",
+ action="store_true",
+ help="Do not import virtualenv variables to SCons")
+
op.add_option('--implicit-cache',
dest='implicit_cache', default=False,
action="store_true",
@@ -906,6 +918,7 @@ def Parser(version):
action="append",
help="Search REPOSITORY for source and target files.")
+
# Options from Make and Cons classic that we do not yet support,
# but which we may support someday and whose (potential) meanings
# we don't want to change. These all get a "the -X option is not
@@ -978,7 +991,6 @@ def Parser(version):
action="callback", callback=opt_not_yet,
# help="Warn when an undefined variable is referenced."
help=SUPPRESS_HELP)
-
return op
# Local Variables:
diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py
index 90bc311..d602507 100644
--- a/src/engine/SCons/Script/__init__.py
+++ b/src/engine/SCons/Script/__init__.py
@@ -82,6 +82,7 @@ import SCons.Builder
import SCons.Environment
import SCons.Node.FS
import SCons.Platform
+import SCons.Platform.virtualenv
import SCons.Scanner
import SCons.SConf
import SCons.Subst
@@ -149,6 +150,7 @@ Environment = SCons.Environment.Environment
#OptParser = SCons.SConsOptions.OptParser
FindPathDirs = SCons.Scanner.FindPathDirs
Platform = SCons.Platform.Platform
+Virtualenv = SCons.Platform.virtualenv.Virtualenv
Return = _SConscript.Return
Scanner = SCons.Scanner.Base
Tool = SCons.Tool.Tool
diff --git a/src/engine/SCons/Tool/docbook/__init__.py b/src/engine/SCons/Tool/docbook/__init__.py
index d60789d..5b98db2 100644
--- a/src/engine/SCons/Tool/docbook/__init__.py
+++ b/src/engine/SCons/Tool/docbook/__init__.py
@@ -166,6 +166,8 @@ xsltproc_com_priority = ['xsltproc', 'saxon', 'saxon-xslt', 'xalan']
# see: http://saxon.sourceforge.net/
xsltproc_com = {'xsltproc' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE',
'saxon' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE $DOCBOOK_XSLTPROCPARAMS',
+ # Note if saxon-xslt is version 5.5 the proper arguments are: (swap order of docbook_xsl and source)
+ # 'saxon-xslt' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $SOURCE $DOCBOOK_XSL $DOCBOOK_XSLTPROCPARAMS',
'saxon-xslt' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE $DOCBOOK_XSLTPROCPARAMS',
'xalan' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -q -out $TARGET -xsl $DOCBOOK_XSL -in $SOURCE'}
xmllint_com = {'xmllint' : '$DOCBOOK_XMLLINT $DOCBOOK_XMLLINTFLAGS --xinclude $SOURCE > $TARGET'}
diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py
index 6643b72..4588955 100644
--- a/src/engine/SCons/Util.py
+++ b/src/engine/SCons/Util.py
@@ -1597,6 +1597,32 @@ def cmp(a, b):
return (a > b) - (a < b)
+def get_bool_envvar(name, default=False):
+ """
+ Get a value of OS environment variable converting it to boolean.
+
+ - FOO=1, FOO=123, FOO=true, FOO=yes, FOO=y, FOO=on are examples of ``True``
+ values.
+ - FOO=0, FOO=false, FOO=no, FOO=n, FOO=off are examples of ``False``
+ values.
+
+ If a variable can't be converted to a boolean, default value is returned
+ (``False`` by default)
+ """
+ try:
+ var = os.environ[name]
+ except KeyError:
+ return default
+ try:
+ return bool(int(var))
+ except ValueError:
+ if str(var).lower() in ('true', 'yes', 'y', 'on'):
+ return True
+ elif str(var).lower() in ('false', 'no', 'n', 'off'):
+ return False
+ else:
+ return default
+
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
diff --git a/src/engine/SCons/UtilTests.py b/src/engine/SCons/UtilTests.py
index 209c60f..6067c98 100644
--- a/src/engine/SCons/UtilTests.py
+++ b/src/engine/SCons/UtilTests.py
@@ -884,6 +884,64 @@ class flattenTestCase(unittest.TestCase):
self.assertEqual(sorted(result),[1,2,3])
+class OsEnviron(object):
+ """Used to temporarily mock os.environ"""
+ def __init__(self, environ):
+ self._environ = environ
+
+ def start(self):
+ self._stored = os.environ
+ os.environ = self._environ
+
+ def stop(self):
+ os.environ = self._stored
+ del self._stored
+
+ def __enter__(self):
+ self.start()
+ return os.environ
+
+ def __exit__(self, *args):
+ self.stop()
+
+
+class get_bool_envvarTestCase(unittest.TestCase):
+ def test_missing(self):
+ with OsEnviron(dict()):
+ var = get_bool_envvar('FOO')
+ assert var is False, "var should be False, not %s" % repr(var)
+ with OsEnviron({'FOO': '1'}):
+ var = get_bool_envvar('BAR')
+ assert var is False, "var should be False, not %s" % repr(var)
+
+ def test_true(self):
+ for foo in [ 'TRUE', 'True', 'true',
+ 'YES', 'Yes', 'yes',
+ 'Y', 'y',
+ 'ON', 'On', 'on',
+ '1', '20', '-1']:
+ with OsEnviron({'FOO': foo}):
+ var = get_bool_envvar('FOO')
+ assert var is True, 'var should be True, not %s' % repr(var)
+
+ def test_false(self):
+ for foo in [ 'FALSE', 'False', 'false',
+ 'NO', 'No', 'no',
+ 'N', 'n',
+ 'OFF', 'Off', 'off',
+ '0']:
+ with OsEnviron({'FOO': foo}):
+ var = get_bool_envvar('FOO', True)
+ assert var is False, 'var should be True, not %s' % repr(var)
+
+ def test_default(self):
+ with OsEnviron({'FOO': 'other'}):
+ var = get_bool_envvar('FOO', True)
+ assert var is True, 'var should be True, not %s' % repr(var)
+ var = get_bool_envvar('FOO', False)
+ assert var is False, 'var should be False, not %s' % repr(var)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/src/engine/SCons/dblite.py b/src/engine/SCons/dblite.py
index 87a1763..628182b 100644
--- a/src/engine/SCons/dblite.py
+++ b/src/engine/SCons/dblite.py
@@ -75,7 +75,8 @@ class dblite(object):
def __init__(self, file_base_name, flag, mode):
assert flag in (None, "r", "w", "c", "n")
- if (flag is None): flag = "r"
+ if flag is None:
+ flag = "r"
base, ext = os.path.splitext(file_base_name)
if ext == dblite_suffix:
@@ -106,13 +107,13 @@ class dblite(object):
self._chown_to = -1 # don't chown
self._chgrp_to = -1 # don't chgrp
- if (self._flag == "n"):
+ if self._flag == "n":
self._open(self._file_name, "wb", self._mode)
else:
try:
f = self._open(self._file_name, "rb")
except IOError as e:
- if (self._flag != "c"):
+ if self._flag != "c":
raise e
self._open(self._file_name, "wb", self._mode)
else:
@@ -132,7 +133,7 @@ class dblite(object):
corruption_warning(self._file_name)
def close(self):
- if (self._needs_sync):
+ if self._needs_sync:
self.sync()
def __del__(self):
diff --git a/test/Decider/MD5-timestamp-Repository.py b/test/Decider/MD5-timestamp-Repository.py
new file mode 100644
index 0000000..1826f68
--- /dev/null
+++ b/test/Decider/MD5-timestamp-Repository.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify behavior of the MD5-timestamp Decider() setting when combined with Repository() usage
+"""
+
+import os
+import stat
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.subdir('Repository', 'work')
+repository = test.workpath('Repository')
+
+
+test.write(['Repository','content1.in'], "content1.in 1\n")
+test.write(['Repository','content2.in'], "content2.in 1\n")
+test.write(['Repository','content3.in'], "content3.in 1\n")
+# test.writable('Repository', 0)
+
+
+test.write(['work','SConstruct'], """\
+Repository(r'%s')
+DefaultEnvironment(tools=[])
+m = Environment(tools=[])
+m.Decider('MD5-timestamp')
+m.Command('content1.out', 'content1.in', Copy('$TARGET', '$SOURCE'))
+m.Command('content2.out', 'content2.in', Copy('$TARGET', '$SOURCE'))
+m.Command('content3.out', 'content3.in', Copy('$TARGET', '$SOURCE'))
+"""%repository)
+
+test.run(chdir='work',arguments='.')
+
+test.up_to_date(chdir='work',arguments='.')
+
+test.sleep()
+
+test.write(['Repository','content1.in'], "content1.in 2\n")
+
+test.touch(['Repository','content2.in'])
+
+time_content = os.stat(os.path.join(repository,'content3.in'))[stat.ST_MTIME]
+test.write(['Repository','content3.in'], "content3.in 2\n")
+test.touch(['Repository','content3.in'], time_content)
+
+# We should only see content1.out rebuilt. The timestamp of content2.in
+# has changed, but its content hasn't, so the follow-on content check says
+# to not rebuild it. The content of content3.in has changed, but that's
+# masked by the fact that its timestamp is the same as the last run.
+
+expect = test.wrap_stdout("""\
+Copy("content1.out", "%s")
+"""%os.path.join(repository,'content1.in'))
+
+test.run(chdir='work', arguments='.', stdout=expect)
+
+test.up_to_date(chdir='work', arguments='.')
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/activated/option/enable-virtualenv.py b/test/virtualenv/activated/option/enable-virtualenv.py
new file mode 100644
index 0000000..a5ceecc
--- /dev/null
+++ b/test/virtualenv/activated/option/enable-virtualenv.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Ensure that the --enable-virtualenv flag works.
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import sys
+import os
+import re
+
+test = TestSCons.TestSCons()
+
+if SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ test.skip_test("Virtualenv support enabled by default, the option --enable-virtualenv is unavailable, skipping\n")
+
+if not SCons.Platform.virtualenv.Virtualenv():
+ test.skip_test("No virtualenv detected, skipping\n")
+
+if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH','')):
+ test.skip_test("Virtualenv detected but looks like unactivated, skipping\n")
+
+test.write('SConstruct', """
+import sys
+import SCons.Platform.virtualenv
+env = DefaultEnvironment(tools=[])
+print("sys.executable: %r" % sys.executable)
+print("env.WhereIs('python'): %r" % env.WhereIs('python'))
+""")
+
+test.run(['-Q', '--enable-virtualenv'])
+s = test.stdout()
+m = re.search(r"""^sys\.executable:\s*(?P<py>["'][^"']+["'])\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""\
+can't determine sys.executable from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+interpreter = eval(m.group('py'))
+
+m = re.search(r"""^\s*env.WhereIs\('python'\):\s*(?P<py>["']?[^"']+["']?)\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""
+can't determine env.WhereIs('python') from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+python = eval(m.group('py'))
+
+test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter),
+ message="sys.executable points outside of virtualenv")
+test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(python),
+ message="env.WhereIs('python') points to virtualenv")
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/activated/option/ignore-virtualenv.py b/test/virtualenv/activated/option/ignore-virtualenv.py
new file mode 100644
index 0000000..ac6f945
--- /dev/null
+++ b/test/virtualenv/activated/option/ignore-virtualenv.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Ensure that the --ignore-virtualenv flag works.
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import sys
+import os
+import re
+
+test = TestSCons.TestSCons()
+
+if not SCons.Platform.virtualenv.Virtualenv():
+ test.skip_test("No virtualenv detected, skipping\n")
+
+if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH','')):
+ test.skip_test("Virtualenv detected but looks like unactivated, skipping\n")
+
+test.write('SConstruct', """
+import sys
+import SCons.Platform.virtualenv
+env = DefaultEnvironment(tools=[])
+print("sys.executable: %s" % repr(sys.executable))
+print("env.WhereIs('python'): %s" % repr(env.WhereIs('python')))
+""")
+
+os.environ['SCONS_ENABLE_VIRTUALENV'] = '1'
+
+test.run(['-Q', '--ignore-virtualenv'])
+s = test.stdout()
+m = re.search(r"""^sys\.executable:\s*(?P<py>["']?[^"']+["']?)\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""\
+can't determine sys.executable from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+interpreter = eval(m.group('py'))
+
+m = re.search(r"""^\s*env.WhereIs\('python'\):\s*(?P<py>["']?[^"']+["']?)\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""
+can't determine env.WhereIs('python') from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+python = eval(m.group('py'))
+
+test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter),
+ message="sys.executable points outside of virtualenv")
+test.fail_test(SCons.Platform.virtualenv.IsInVirtualenv(python),
+ message="env.WhereIs('python') points to virtualenv")
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/activated/virtualenv_activated_python.py b/test/virtualenv/activated/virtualenv_activated_python.py
new file mode 100644
index 0000000..c673ae1
--- /dev/null
+++ b/test/virtualenv/activated/virtualenv_activated_python.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Check which python executable is running scons and which python executable
+would be used by scons, when we run under activated virtualenv (i.e. PATH
+contains the virtualenv's bin path). This test is skipped when ran in regular
+environment or in unactivated virtualenv.
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import sys
+import os
+import re
+
+test = TestSCons.TestSCons()
+
+if not SCons.Platform.virtualenv.Virtualenv():
+ test.skip_test("No virtualenv detected, skipping\n")
+
+if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH')):
+ test.skip_test("Virtualenv detected but looks like unactivated, skipping\n")
+
+
+test.write('SConstruct', """
+import sys
+env = DefaultEnvironment(tools=[])
+print("sys.executable: %s" % repr(sys.executable))
+print("env.WhereIs('python'): %s" % repr(env.WhereIs('python')))
+""")
+
+if SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ test.run(['-Q'])
+else:
+ test.run(['-Q', '--enable-virtualenv'])
+
+s = test.stdout()
+m = re.search(r"""^sys\.executable:\s*(?P<py>["']?[^"']+["']?)\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""\
+can't determine sys.executable from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+interpreter = eval(m.group('py'))
+
+m = re.search(r"""^\s*env\.WhereIs\('python'\):\s*(?P<py>["'][^"']+["'])\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""
+can't determine env.WhereIs('python') from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+python = eval(m.group('py'))
+
+# runing in activated virtualenv (after "activate") - PATH includes virtualenv's bin directory
+test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter),
+ message="sys.executable points outside of virtualenv")
+test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(python),
+ message="env.WhereIs('python') points outside of virtualenv")
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/activated/virtualenv_detect_virtualenv.py b/test/virtualenv/activated/virtualenv_detect_virtualenv.py
new file mode 100644
index 0000000..2c00793
--- /dev/null
+++ b/test/virtualenv/activated/virtualenv_detect_virtualenv.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Check if SCons.Platform.virtualenv.Virtualenv() works in SConscripts.
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import sys
+
+test = TestSCons.TestSCons()
+
+ve = SCons.Platform.virtualenv.Virtualenv()
+if not ve:
+ test.skip_test("Virtualenv is not active, skipping\n")
+
+test.write('SConstruct', """
+print("virtualenv: %r" % Virtualenv())
+""")
+
+if SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ test.run(['-Q'])
+else:
+ test.run(['-Q', '--enable-virtualenv'])
+
+test.must_contain_all_lines(test.stdout(), ['virtualenv: %r' % ve])
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/always/virtualenv_global_function.py b/test/virtualenv/always/virtualenv_global_function.py
new file mode 100644
index 0000000..8f2c291
--- /dev/null
+++ b/test/virtualenv/always/virtualenv_global_function.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Check which python executable is running scons and which python executable
+would be used by scons, when we run under activated virtualenv (i.e. PATH
+contains the virtualenv's bin path).
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import re
+
+test = TestSCons.TestSCons()
+
+test.write('SConstruct', """
+print("Virtualenv(): %r" % Virtualenv())
+""")
+
+test.run(['-Q'])
+
+s = test.stdout()
+m = re.search(r"^Virtualenv\(\):\s*(?P<ve>.+\S)\s*$", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""\
+can't determine Virtualenv() result from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+scons_ve = m.group('ve')
+our_ve = "%r" % SCons.Platform.virtualenv.Virtualenv()
+
+# runing in activated virtualenv (after "activate") - PATH includes virtualenv's bin directory
+test.fail_test(scons_ve != our_ve,
+ message="Virtualenv() from SCons != Virtualenv() from caller script (%r != %r)" % (scons_ve, our_ve))
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/regularenv/virtualenv_detect_regularenv.py b/test/virtualenv/regularenv/virtualenv_detect_regularenv.py
new file mode 100644
index 0000000..57a0d4f
--- /dev/null
+++ b/test/virtualenv/regularenv/virtualenv_detect_regularenv.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Check if SCons.Platform.virtualenv.Virtualenv() works in SConscript.
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import sys
+
+test = TestSCons.TestSCons()
+
+if SCons.Platform.virtualenv.Virtualenv():
+ test.skip_test("Virtualenv is active, skipping\n")
+
+test.write('SConstruct', """
+print("virtualenv: %r" % Virtualenv())
+""")
+
+if SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ test.run(['-Q'])
+else:
+ test.run(['-Q', '--enable-virtualenv'])
+
+test.must_contain_all_lines(test.stdout(), ['virtualenv: %r' % None])
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/virtualenv/unactivated/virtualenv_unactivated_python.py b/test/virtualenv/unactivated/virtualenv_unactivated_python.py
new file mode 100644
index 0000000..a4dc240
--- /dev/null
+++ b/test/virtualenv/unactivated/virtualenv_unactivated_python.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Check which python executable is running scons and which python executable
+would be used by scons, when we run under an unactivated virtualenv (i.e. PATH
+does not contain virtualenv's bin path). This test is skipped if ran in
+a regular environment or in an activated virtualenv.
+"""
+
+import TestSCons
+import SCons.Platform.virtualenv
+import sys
+import os
+import re
+
+test = TestSCons.TestSCons()
+
+if not SCons.Platform.virtualenv.Virtualenv():
+ test.skip_test("No virtualenv detected, skipping\n")
+
+if SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH')):
+ test.skip_test("Virtualenv detected and it looks like activated, skipping\n")
+
+test.write('SConstruct', """
+import sys
+env = DefaultEnvironment(tools=[])
+print("sys.executable: %s" % repr(sys.executable))
+print("env.WhereIs('python'): %s" % repr(env.WhereIs('python')))
+""")
+
+if SCons.Platform.virtualenv.virtualenv_enabled_by_default:
+ test.run(['-Q'])
+else:
+ test.run(['-Q', '--enable-virtualenv'])
+
+s = test.stdout()
+m = re.search(r"""^sys\.executable:\s*(?P<py>["']?[^\"']+["']?)\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""\
+can't determine sys.executable from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+interpreter = eval(m.group('py'))
+
+m = re.search(r"""^\s*env\.WhereIs\('python'\):\s*(?P<py>["']?[^"']+[\"']?)\s*$""", s, re.MULTILINE)
+if not m:
+ test.fail_test(message="""
+can't determine env.WhereIs('python') from stdout:
+========= STDOUT =========
+%s
+==========================
+""" % s)
+
+python = eval(m.group('py'))
+
+# running without activating virtualenv (by just /path/to/virtualenv/bin/python runtest.py ...).
+test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter),
+ message="sys.executable points outside of virtualenv")
+test.fail_test(SCons.Platform.virtualenv.IsInVirtualenv(python),
+ message="env.WhereIs('python') points to virtualenv")
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: