Skip to content
Open
59 changes: 54 additions & 5 deletions SCons/Tool/msvs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
import uuid
import ntpath
import os
import pathlib
import pickle
import re
import sys
import textwrap

import SCons.Builder
import SCons.Node.FS
Expand Down Expand Up @@ -160,19 +162,66 @@ def msvs_parse_version(s):
# things and ends up with "-c" as sys.argv[0]. Consequently, we have
# the MSVS Project file invoke SCons the same way that scons.bat does,
# which works regardless of how we were invoked.
_exec_script_main_template = None

def getExecScriptMain(env, xml=None):
global _exec_script_main_template

if 'SCONS_HOME' not in env:
env['SCONS_HOME'] = os.environ.get('SCONS_HOME')
scons_home = env.get('SCONS_HOME')
if not scons_home and 'SCONS_LIB_DIR' in os.environ:
scons_home = os.environ['SCONS_LIB_DIR']
if scons_home:
exec_script_main = "from os.path import join; import sys; sys.path = [ r'%s' ] + sys.path; import SCons.Script; SCons.Script.main()" % scons_home
else:
version = SCons.__version__
exec_script_main = "from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-%(version)s'), join(sys.prefix, 'scons-%(version)s'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()" % locals()

scons_abspath = os.path.abspath(os.path.dirname(os.path.dirname(SCons.__file__)))

def _in_pytree(scons_abspath):
py_comps = pathlib.Path(os.path.normcase(os.path.abspath(sys.prefix))).parts
comps = pathlib.Path(os.path.normcase(scons_abspath)).parts
rval = bool(comps[:len(py_comps)] == py_comps)
return rval

in_pytree = _in_pytree(scons_abspath)
# print(f"in_pytree={in_pytree}, scons_abspath=`{scons_abspath}', sys.prefix='{sys.prefix}'")

if _exec_script_main_template is None:
_exec_script_main_template = "; ".join(textwrap.dedent(
"""\
import importlib.util
import sys
from os.path import abspath, dirname, join, normcase
from pathlib import Path
usr = r'{scons_home}'
gen = r'{scons_abspath}'
usrpath = abspath(usr) if usr else ''
genpath = gen if (gen and (not usrpath or normcase(usrpath) != normcase(gen))) else ''
syspath = list(sys.path)
pycomps = Path(normcase(abspath(sys.prefix))).parts
memo = {{}}
origin = lambda l: (sys.path.clear(), sys.path.extend(l), memo.update({{'spec': importlib.util.find_spec('SCons')}}), dirname(dirname(abspath(memo['spec'].origin))) if (memo['spec'] and memo['spec'].origin) else '')[-1]
pytree = lambda p: (memo.update({{'comps': Path(normcase(p)).parts}}), memo['comps'][:len(pycomps)] == pycomps)[-1] if p else False
search = ([usrpath] + syspath if usrpath else [join(sys.prefix, *t) for t in [('Lib', 'site-packages', 'scons-{scons_version}'), ('scons-{scons_version}',), ('Lib', 'site-packages', 'scons'), ('scons',)]] + syspath)
begpath = origin(search)
endpath = (search.insert(0, genpath), origin([genpath]))[-1] if (genpath and (not begpath or pytree(begpath))) else ''
path = endpath if endpath else begpath
_ = (print(f'proj: Error: SCons not found (search=\\\'{{search}}\\\').'), sys.exit(1)) if (not path) else None
sys.path = [path] + syspath
print(f'proj: Using SCons path \\\'{{path}}\\\'.')
import SCons.Script
SCons.Script.main()
"""
).splitlines())

exec_script_main = _exec_script_main_template.format(
scons_home=scons_home if scons_home else '',
scons_abspath=scons_abspath if not in_pytree else '',
scons_version=SCons.__version__,
)
# print("exec_script_main:\n", ' ' + '\n '.join(exec_script_main.split("; ")))

if xml:
exec_script_main = xmlify(exec_script_main)

return exec_script_main

# The string for the Python executable we tell the Project file to use
Expand Down
81 changes: 73 additions & 8 deletions testing/framework/TestSConsMSVS.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@

import os
import sys
import pathlib
import platform
import textwrap
import traceback
from xml.etree import ElementTree

Expand Down Expand Up @@ -832,6 +834,73 @@ def get_tested_proj_file_vc_versions():
return ['8.0', '9.0', '10.0', '11.0', '12.0', '14.0', '14.1', '14.2', '14.3']


_exec_script_main_template = None

def get_exec_script_main(scons_home=''):
"""
Returns the python script string embedded in the msvs project files.
"""
global _exec_script_main_template

scons_home = scons_home
# Some tests may fail if SCONS_HOME is defined in the os environment when
# the tests are run. The SCons location set in the user's os environment
# may point to a different SCons than the SCons currently being tested.
# This behavior is consistent with the main branch code at the time.
# if not scons_home and 'SCONS_HOME' in os.environ:
# scons_home = os.environ['SCONS_HOME']
if not scons_home and 'SCONS_LIB_DIR' in os.environ:
scons_home = os.environ['SCONS_LIB_DIR']

scons_abspath = os.path.abspath(os.path.dirname(os.path.dirname(SCons.__file__)))

def _in_pytree(scons_abspath):
py_comps = pathlib.Path(os.path.normcase(os.path.abspath(sys.prefix))).parts
comps = pathlib.Path(os.path.normcase(scons_abspath)).parts
rval = bool(comps[:len(py_comps)] == py_comps)
return rval

in_pytree = _in_pytree(scons_abspath)
# print(f"in_pytree={in_pytree}, scons_abspath=`{scons_abspath}', sys.prefix='{sys.prefix}'")

if _exec_script_main_template is None:
_exec_script_main_template = "; ".join(textwrap.dedent(
"""\
import importlib.util
Copy link
Contributor

Choose a reason for hiding this comment

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

realistically I think we can just do python <path to current scons.py> where python is the python the scons being run was using.

All the rest of this like likely no longer needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that the SCons module path should only be generated for non-library installations of SCons.

Keep in mind that the generated python path can be deferred until execution of the vcxproj file by Visual Studio. As shown below, it is possible to have one python version executable importing SCons from another python version library site package location. That just seems like a bad idea.

See comment #4818 (comment) below.

import sys
from os.path import abspath, dirname, join, normcase
from pathlib import Path
usr = r'{scons_home}'
gen = r'{scons_abspath}'
usrpath = abspath(usr) if usr else ''
genpath = gen if (gen and (not usrpath or normcase(usrpath) != normcase(gen))) else ''
syspath = list(sys.path)
pycomps = Path(normcase(abspath(sys.prefix))).parts
memo = {{}}
origin = lambda l: (sys.path.clear(), sys.path.extend(l), memo.update({{'spec': importlib.util.find_spec('SCons')}}), dirname(dirname(abspath(memo['spec'].origin))) if (memo['spec'] and memo['spec'].origin) else '')[-1]
pytree = lambda p: (memo.update({{'comps': Path(normcase(p)).parts}}), memo['comps'][:len(pycomps)] == pycomps)[-1] if p else False
search = ([usrpath] + syspath if usrpath else [join(sys.prefix, *t) for t in [('Lib', 'site-packages', 'scons-{scons_version}'), ('scons-{scons_version}',), ('Lib', 'site-packages', 'scons'), ('scons',)]] + syspath)
begpath = origin(search)
endpath = (search.insert(0, genpath), origin([genpath]))[-1] if (genpath and (not begpath or pytree(begpath))) else ''
path = endpath if endpath else begpath
_ = (print(f'proj: Error: SCons not found (search=\\\'{{search}}\\\').'), sys.exit(1)) if (not path) else None
sys.path = [path] + syspath
print(f'proj: Using SCons path \\\'{{path}}\\\'.')
import SCons.Script
SCons.Script.main()
"""
).splitlines())

exec_script_main = _exec_script_main_template.format(
scons_home=scons_home if scons_home else '',
scons_abspath=scons_abspath if not in_pytree else '',
scons_version=SCons.__version__,
)
# print("exec_script_main:\n", ' ' + '\n '.join(exec_script_main.split("; ")))

return exec_script_main


class TestSConsMSVS(TestSCons):
"""Subclass for testing MSVS-specific portions of SCons."""

Expand Down Expand Up @@ -879,6 +948,7 @@ def msvs_substitute(
sconscript=None,
python=None,
project_guid=None,
scons_home: str = '',
vcproj_sccinfo: str = '',
sln_sccinfo: str = '',
):
Expand All @@ -899,10 +969,7 @@ def msvs_substitute(
if project_guid is None:
project_guid = PROJECT_GUID

if 'SCONS_LIB_DIR' in os.environ:
exec_script_main = f"from os.path import join; import sys; sys.path = [ r'{os.environ['SCONS_LIB_DIR']}' ] + sys.path; import SCons.Script; SCons.Script.main()"
else:
exec_script_main = f"from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-{self.scons_version}'), join(sys.prefix, 'scons-{self.scons_version}'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()"
exec_script_main = get_exec_script_main(scons_home=scons_home)
exec_script_main_xml = exec_script_main.replace("'", "&apos;")

result = input.replace(r'<WORKPATH>', workpath)
Expand Down Expand Up @@ -1147,6 +1214,7 @@ def msvs_substitute_projects(
project_guid_2=None,
solution_guid_1=None,
solution_guid_2=None,
scons_home: str = '',
vcproj_sccinfo: str = '',
sln_sccinfo: str = '',
):
Expand Down Expand Up @@ -1176,10 +1244,7 @@ def msvs_substitute_projects(
if solution_guid_2 is None:
solution_guid_2 = SOLUTION_GUID_2

if 'SCONS_LIB_DIR' in os.environ:
exec_script_main = f"from os.path import join; import sys; sys.path = [ r'{os.environ['SCONS_LIB_DIR']}' ] + sys.path; import SCons.Script; SCons.Script.main()"
else:
exec_script_main = f"from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-{self.scons_version}'), join(sys.prefix, 'scons-{self.scons_version}'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()"
exec_script_main = get_exec_script_main(scons_home=scons_home)
exec_script_main_xml = exec_script_main.replace("'", "&apos;")

result = input.replace(r'<WORKPATH>', workpath)
Expand Down
Loading