Skip to content

Conversation

@madmiraal
Copy link

Fixes #4809

Currently, the default value for MSVSSCONS used to populate the vcxproj file's NMake*CommandLine lines, uses a rounadabout way to launch SCons. It launches python with -c and a string for the following python script:

from os.path import join
import sys
sys.path = [
    join(sys.prefix, 'Lib', 'site-packages', 'scons-4.9.1'),
    join(sys.prefix, 'scons-4.9.1'),
    join(sys.prefix, 'Lib', 'site-packages', 'scons'),
    join(sys.prefix, 'scons')
] + sys.path;
import SCons.Script
SCons.Script.main()

If the script can't find the scons module in the modified sys.path it fails.

It should be safe to assume that scons is (or at least should be) in the user's path; as it's needed to run the SConstruct that is used to create the vcxproj file. However, if scons is not in the user's path, the user is free to modify MSVSSCONS variable to include the full path to their scons.

Therefore, this PR sets the default MSVSSCONS variable to scons, and removes the scaffolding used to create the previous default value.

Contributor Checklist:

  • I have created a new test or updated the unit tests to cover the new/changed functionality.
  • I have updated CHANGES.txt and RELEASE.txt (and read the README.rst).
  • I have updated the appropriate documentation

@mwichmann mwichmann added the MSVC Microsoft Visual C++ Support label Jan 2, 2026
# invoke scons.
if 'MSVSSCONS' not in env:
env['MSVSSCONS'] = '"%s" -c "%s"' % (python_executable, getExecScriptMain(env))
env['MSVSSCONS'] = 'scons'
Copy link
Contributor

Choose a reason for hiding this comment

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

probably not this as well. MSVSSCONS is meant to override the existing mechanism explicitly, and not by default.

Copy link
Contributor

Choose a reason for hiding this comment

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

The right way to do this is to find the path to the scons being executed and use that, instead of defaulting to site-scons...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmmm, I got email with a comment about using sys.executable, and it doesn't show here on the PR page. Weird.

Anyway, sys.exectuable can be unset, according to the docu (can be emtpy string or None). I'm not sure the circumstances - odd packaging is one known cause (like a pyinstaller bundle) but maybe there are others.

Copy link
Contributor

Choose a reason for hiding this comment

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

sure. but people doing really silly things aren't generally worthy adding bunches of code for.. ?

Copy link
Collaborator

@mwichmann mwichmann Jan 2, 2026

Choose a reason for hiding this comment

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

The right way to do this is to find the path to the scons being executed and use that, instead of defaulting to site-scons...

It seems like this is the intent, anyway. From the manpage:

MSVSSCONS - The SCons used in generated Microsoft Visual C++ project files. The default is the version of SCons being used to generate the project file.

In any case, the stanza used to figure out the value isn't updated for how a pip-installed scons is invoked ("console script", which is a little .exe made during packaging), so it could use some work. But that wasn't the case in the issue report, with the scoop install. Anyway, here's the comment from the msvs tool:

# This is how we re-invoke SCons from inside MSVS Project files.         
# The problem is that we might have been invoked as either scons.bat        
# or scons.py.  If we were invoked directly as scons.py, then we could     
# use sys.argv[0] to find the SCons "executable," but that doesn't work
# if we were invoked as scons.bat, which uses "python -c" to execute
# 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. 

@jcbrill
Copy link
Contributor

jcbrill commented Jan 2, 2026

Feel free to ignore.

In msvs.py, wouldn't the necessary sys.path directory for SCons be the SCons module's parent directory?

The following appears to work for the Scoop issue as reported. Not tested outside of this issue so unsure if it breaks other environment configurations.

  # This is how we re-invoke SCons from inside MSVS Project files.
  # The problem is that we might have been invoked as either scons.bat
  # or scons.py.  If we were invoked directly as scons.py, then we could
  # use sys.argv[0] to find the SCons "executable," but that doesn't work
  # if we were invoked as scons.bat, which uses "python -c" to execute
  # 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.
  def getExecScriptMain(env, xml=None):
      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 not scons_home:
+         scons_parent = os.path.abspath(os.path.join(os.path.dirname(SCons.__file__), ".."))
+         if os.path.exists(scons_parent):
+             scons_home = scons_parent
      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()
      if xml:
          exec_script_main = xmlify(exec_script_main)
      return exec_script_main

@mwichmann
Copy link
Collaborator

SCons.__file__ is something we were also looking at in the chat channel, so that's three people that seem to think it might be a cleaner way.

@jcbrill
Copy link
Contributor

jcbrill commented Jan 2, 2026

FYI.

Scoop configuration and call chain with python and SCons installed via scoop.

System path elements:

Path=C:\WINDOWS\system32;
     C:\WINDOWS;
     C:\WINDOWS\System32\Wbem;
     C:\WINDOWS\System32\WindowsPowerShell\v1.0\;
     C:\WINDOWS\System32\OpenSSH\;
     C:\Program Files\PowerShell\7\;
     C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;
     C:\Users\jbrill\scoop\apps\python310\current\Scripts;
     C:\Users\jbrill\scoop\apps\python310\current;
     C:\Users\jbrill\scoop\shims;  # <==== scons.cmd
     C:\Users\jbrill\AppData\Local\Microsoft\WindowsApps

Scoop SCons call chain (scons):

  1. C:\Users\jbrill\scoop\shims\scons.cmd:
    @rem C:\Users\jbrill\scoop\apps\scons\current\scons.ps1
    @echo off
    where /q pwsh.exe
    if %errorlevel% equ 0 (
        pwsh -noprofile -ex unrestricted -file "C:\Users\jbrill\scoop\apps\scons\current\scons.ps1"  %*
    ) else (
        powershell -noprofile -ex unrestricted -file "C:\Users\jbrill\scoop\apps\scons\current\scons.ps1"  %*
    )
    
  2. C:\Users\jbrill\scoop\apps\scons\current\scons.ps1 (current ==> [\??\C:\Users\jbrill\scoop\apps\scons\4.10.1]):
    python "C:\Users\jbrill\scoop\apps\scons\4.10.1\scons.py" @args
    

Scoop SCons layout (C:\Users\jbrill\scoop\apps\scons\4.10.1):

- scons-local-4.10.1\
  |- SCons\
- install.json
- manifest.json
- scons.bat
- scons.ps1
- scons.py
- scons-4.10.1.bat
- scons-configure-cache.py
- sconsign.py
- scons-LICENSE
- scons-README
- scons-time.py

@bdbaddog
Copy link
Contributor

bdbaddog commented Jan 3, 2026

How about

     if not scons_home:
         scons_home = os.path.abspath(SCons.__path__[0])

@jcbrill
Copy link
Contributor

jcbrill commented Jan 3, 2026

Sure.

Although, can't __path__ can be monkey-patched?

@bdbaddog
Copy link
Contributor

bdbaddog commented Jan 3, 2026

Sure.

Although, can't __path__ can be monkey-patched?

The __path__ on SCons won't be patched by us.. So if someone does something silly and is creating a msproject file and running SCons through MSVS.. .. they may have an issue.. I think that's safe enough.

@jcbrill
Copy link
Contributor

jcbrill commented Jan 3, 2026

scons_home has to be the parent directory of SCons.__path__[0]:

scons_home = os.path.abspath(SCons.__path__[0])
    C:\Users\jbrill\scoop\apps\scons\4.10.1\scons-local-4.10.1\SCons

scons_home = os.path.abspath(os.path.join(SCons.__path__[0], ".."))
    C:\Users\jbrill\scoop\apps\scons\4.10.1\scons-local-4.10.1

scons_home will always be defined when reaching the trailing if/then to set exec_script_main so there is no need for the else clause.

I'm not convinced that SCONS_HOME or SCONS_LIB_DIR should be evaluated at all since SCons has been imported for this code to run.

A possible candidate solution:

  # This is how we re-invoke SCons from inside MSVS Project files.
  # The problem is that we might have been invoked as either scons.bat
  # or scons.py.  If we were invoked directly as scons.py, then we could
  # use sys.argv[0] to find the SCons "executable," but that doesn't work
  # if we were invoked as scons.bat, which uses "python -c" to execute
  # 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.
  def getExecScriptMain(env, xml=None):
-     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_home = os.path.abspath(os.path.join(SCons.__path__[0], ".."))
+     exec_script_main = "from os.path import join; import sys; sys.path = [ r'%s' ] + sys.path; import SCons.Script; SCons.Script.main()" % scons_home
      if xml:
          exec_script_main = xmlify(exec_script_main)
      return exec_script_main

Edit 1:

There is no need for from os.path import join; above:

+     scons_home = os.path.abspath(os.path.join(SCons.__path__[0], ".."))
+     exec_script_main = "import sys; sys.path = [ r'%s' ] + sys.path; import SCons.Script; SCons.Script.main()" % scons_home

@bdbaddog
Copy link
Contributor

bdbaddog commented Jan 3, 2026

@jcbrill those two env vars have been there historically and a user could specify them as a system env and they should be respected if they're set.

@jcbrill
Copy link
Contributor

jcbrill commented Jan 3, 2026

@bdbaddog Fair enough.

Given that, the original proposed implementation (#4810 (comment)) is likely the most robust.

@jcbrill
Copy link
Contributor

jcbrill commented Jan 6, 2026

Revised strawman proposal for consideration and/or discussion:

def getExecScriptMain(env, xml=None):
    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 = f"import sys; sys.path = [ r'{scons_home}' ] + sys.path; import SCons.Script; SCons.Script.main()"
    else:
        version = SCons.__version__
        # *** ADDED SCONS PARENT PATH ***
        scons_parent = os.path.abspath(os.path.join(os.path.dirname(SCons.__file__), ".."))
        # *** ADDED SCONS PARENT PATH TO END OF THE PREFIX LIST ***
        exec_script_main = f"from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-{version}'), join(sys.prefix, 'scons-{version}'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons'), r'{scons_parent}' ] + sys.path; import SCons.Script; SCons.Script.main()"
    if xml:
        exec_script_main = xmlify(exec_script_main)
    return exec_script_main

Changes to the current SCons msvs.py source code:

  • The original exec_script_main strings were converted to f-strings.
  • When SCONS_HOME and SCONS_LIB_DIR are undefined, the currently executing SCons module parent folder is added to the end of the generated sys.path prefix list.

Example generated vcxproj file contents:

  • original file contents:
    &quot;from os.path import join; import sys; sys.path = [ join(sys.prefix, &apos;Lib&apos;, &apos;site-packages&apos;, &apos;scons-4.10.1&apos;), join(sys.prefix, &apos;scons-4.10.1&apos;), join(sys.prefix, &apos;Lib&apos;, &apos;site-packages&apos;, &apos;scons&apos;), join(sys.prefix, &apos;scons&apos;)] + sys.path; import SCons.Script; SCons.Script.main()&quot;
  • proposed file contents:
                                                                                                                                                                                                                                                                                                                       |--------------------added: scons parent path ------------------------|
    &quot;from os.path import join; import sys; sys.path = [ join(sys.prefix, &apos;Lib&apos;, &apos;site-packages&apos;, &apos;scons-4.10.1&apos;), join(sys.prefix, &apos;scons-4.10.1&apos;), join(sys.prefix, &apos;Lib&apos;, &apos;site-packages&apos;, &apos;scons&apos;), join(sys.prefix, &apos;scons&apos;), r&apos;C:\Users\jbrill\scoop\apps\scons\4.10.1\scons-local-4.10.1&apos; ] + sys.path; import SCons.Script; SCons.Script.main()&quot;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

MSVC Microsoft Visual C++ Support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The vcxproj file created using MSVSProject fails to build the project when SCons installed via scoop

4 participants