Verify that a user-specified script via MSVC_USE_SCRIPT exists#4063
Conversation
… the user script is not found.
…ues and raise appropriate exception. Build call string rather than inline string in two separate invocations of _subproc.
…plicitly requesting _subproc to raise exceptions bypassing dummyPopen objects.
| stderr=subprocess.PIPE) | ||
| stderr=subprocess.PIPE, | ||
| error='raise') | ||
| except OSError as e: |
There was a problem hiding this comment.
what's the intent here? catch a few specific exceptions and re-raise them, then catch essentially everything else and re-raise it too - and a bare raise would be plenty in any case.
There was a problem hiding this comment.
Simply an attempt at raising the same kind of exception with a reduced traceback to provide the most information possible.
As an example,
OSError: [WinError 2] The system cannot find the file specified:
versus
Exception: [WinError 2] The system cannot find the file specified:
Is there any utility in distinguishing between a ValueError (invalid subprocess arguments) and other exceptions?
I can certainly remove all but the last clause if that is desired. The three Exception types listed are the most likely exceptions raised by subprocess.
There was a problem hiding this comment.
Results from a "file-not-found" error in subprocess are shown below.
Raising a MSVCScriptNotFound exception was manually commented out to allow subprocess to raise the error..
Both a bare raise:
try:
call ...
except:
raise
and a direct call without a try/except
call ...
result in:
FileNotFoundError: [WinError 2] The system cannot find the file specified:
File "S:\SCons\Test-Arg\SConstruct", line 29:
env = Environment(MSVC_USE_SCRIPT=r'C:\test_batch-file_does-not_exist.bat')
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 1010:
apply_tools(self, tools, toolpath)
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 116:
_ = env.Tool(tool)
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 1879:
tool(self)
File "S:\SCons\scons\scripts\..\SCons\Tool\__init__.py", line 273:
self.generate(env, *args, **kw)
File "S:\SCons\scons\scripts\..\SCons\Tool\default.py", line 41:
SCons.Tool.Tool(t)(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\__init__.py", line 273:
self.generate(env, *args, **kw)
File "S:\SCons\scons\scripts\..\SCons\Tool\mslink.py", line 310:
msvc_setup_env_once(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 805:
msvc_setup_env(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 949:
d = script_env(use_script)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 747:
stdout = common.get_output(script, args)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\common.py", line 274:
error='raise')
File "S:\SCons\scons\scripts\..\SCons\Action.py", line 797:
pobj = subprocess.Popen(cmd, **kw)
File "subprocess.py", line 729:
File "subprocess.py", line 1017:
where:
try:
call ...
except Exception as e:
raise Exception(str(e))
is only about 6 lines shorter in this case:
Exception: [WinError 2] The system cannot find the file specified:
File "S:\SCons\Test-Arg\SConstruct", line 29:
env = Environment(MSVC_USE_SCRIPT=r'C:\test_batch-file_does-not_exist.bat')
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 1010:
apply_tools(self, tools, toolpath)
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 116:
_ = env.Tool(tool)
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 1879:
tool(self)
File "S:\SCons\scons\scripts\..\SCons\Tool\__init__.py", line 273:
self.generate(env, *args, **kw)
File "S:\SCons\scons\scripts\..\SCons\Tool\default.py", line 41:
SCons.Tool.Tool(t)(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\__init__.py", line 273:
self.generate(env, *args, **kw)
File "S:\SCons\scons\scripts\..\SCons\Tool\mslink.py", line 310:
msvc_setup_env_once(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 805:
msvc_setup_env(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 949:
d = script_env(use_script)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 747:
stdout = common.get_output(script, args)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\common.py", line 276:
raise Exception(str(e))
Other than the length of the traceback, is there a compelling reason to even have the try/except block?
I'm happy to make whatever changes that are deemed best in this case.
There was a problem hiding this comment.
After sleeping on it and a few more tests this morning, I propose the following change which uses a bare raise and also writes a message to the debug output and to stderr:
try:
popen = SCons.Action._subproc(env,
call_str,
stdin='devnull',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
error='raise')
except:
errmsg = 'Subprocess failed calling command: {}'.format(call_str)
debug(errmsg)
print(errmsg, file=sys.stderr)
raise
Debug output to stdout (SCONS_MSCOMMON_DEBUG=-) yields:
...
use_script 1 'C:\\test_batch-file_does-not_exist.bat'
PATH: C:\Windows\System32;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0
Calling '"C:\test_batch-file_does-not_exist.bat"'
Subprocess failed calling command: "C:\test_batch-file_does-not_exist.bat" & set
Stderr yields:
Subprocess failed calling command: "C:\test_batch-file_does-not_exist.bat" & set
FileNotFoundError: [WinError 2] The system cannot find the file specified:
File "S:\SCons\Test-Arg\SConstruct", line 29:
env = Environment(MSVC_USE_SCRIPT=r'C:\test_batch-file_does-not_exist.bat')
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 1010:
apply_tools(self, tools, toolpath)
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 116:
_ = env.Tool(tool)
File "S:\SCons\scons\scripts\..\SCons\Environment.py", line 1879:
tool(self)
File "S:\SCons\scons\scripts\..\SCons\Tool\__init__.py", line 273:
self.generate(env, *args, **kw)
File "S:\SCons\scons\scripts\..\SCons\Tool\default.py", line 41:
SCons.Tool.Tool(t)(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\__init__.py", line 273:
self.generate(env, *args, **kw)
File "S:\SCons\scons\scripts\..\SCons\Tool\mslink.py", line 310:
msvc_setup_env_once(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 805:
msvc_setup_env(env)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 949:
d = script_env(use_script)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 747:
stdout = common.get_output(script, args)
File "S:\SCons\scons\scripts\..\SCons\Tool\MSCommon\common.py", line 278:
error='raise')
File "S:\SCons\scons\scripts\..\SCons\Action.py", line 797:
pobj = subprocess.Popen(cmd, **kw)
File "subprocess.py", line 729:
File "subprocess.py", line 1017:
As this is a user-facing error, writing the call string to stderr prior to the exception could prove useful for diagnostic purposes.
@mwichmann Would this be a better approach?
There was a problem hiding this comment.
Have to think about it. In this case we could instead raise an SCons error, which will reduce the call stack. Although maybe a little too terse?
except FileNotFoundError as e:
raise SCons.Errors.UserError(e) from NoneThere was a problem hiding this comment.
Yes, it's a UserError only if we can prove the SConscript requested a non-existent file, not if something went wrong on the system. It was just an illustration I could get to trigger to make sure I didn't have the syntax wrong :)
There was a problem hiding this comment.
A bit off topic: an enhancement that I was planning on proposing once this was resolved is the addition of a user specified script argument: MSVC_USE_SCRIPT_ARG to be used in conjunction with MSVC_USE_SCRIPT. This would allow any flags recognized by the msvc batch files to be passed along with the script name.
For example, this would allow the following which currently can't be achieved directly in a SConstruct/SConscript file without resorting to a user-defined batch file (VS2022 Preview, amd64 target, toolset 14.2, sdk 10.0.19041.0):
use_version = '14.3'
use_script = 'c:\\software\\msvs143bt-pre\\VC\\Auxiliary\\Build\\vcvarsall.bat'
use_script_arg = 'amd64 --vcvars_ver=14.2 10.0.19041.0'
env = Environment(MSVC_VERSION = use_version, MSVC_USE_SCRIPT = use_script, MSVC_USE_SCRIPT_ARG = use_script_arg)
If something like this were added, there would be no good way to validate the user provided script argument other than call subprocess. In this case it could be harder to distinguish between a user error and a system error.
Unfortunately, current msvc logic will not pick up batch file errors correctly (unless cl.exe is not added to the path). There is room for improvement in this area.
There was a problem hiding this comment.
>>> "room for improvement" in msvc_tool
True:-)
There was a problem hiding this comment.
Further off-topic: If a user script argument were allowed and an overloaded version when MSVC_USE_SCRIPT=None allowing a dictionary to be passed, it is possible in user space to unify/resolve most of the outstanding msvc issues/requests (preview, toolsets, components, etc).
Working SConstruct fragments via script argument enhancements:
from vsdetect import scons_msvc_byinstance as scons_msvc
# SCons runs batch file: MSVC_USE_SCRIPT (string) and MSVC_USE_SCRIPT_ARG (string)
env = Environment(**scons_msvc(MSVC_VERSION='14.3', MSVC_PREVIEW=True, MSVC_COMPONENT='BuildTools', MSVC_TOOLSET='14.2', TARGET_ARCH='amd64', MSVC_ADD_ARG='10.0.19041.0', run_script=False))
# user runs batch file: MSVC_USE_SCRIPT (None) and MSVC_USE_SCRIPT_ARG (dictionary)
env = Environment(**scons_msvc(MSVC_VERSION='14.3', MSVC_PREVIEW=True, MSVC_COMPONENT='BuildTools', MSVC_TOOLSET='14.2', TARGET_ARCH='amd64', MSVC_ADD_ARG='10.0.19041.0', run_script=True))
# lookup by c runtime
env = Environment(**scons_msvc(MSVC_RUNTIME='140', MSVC_PREVIEW=True, MSVC_COMPONENT='BuildTools', MSVC_TOOLSET='14.2', TARGET_ARCH='amd64', MSVC_ADD_ARG='10.0.19041.0', run_script=True))
# lookup by toolset
env = Environment(**scons_msvc(MSVC_TOOLSET='14.2', MSVC_PREVIEW=True, MSVC_COMPONENT='BuildTools', TARGET_ARCH='amd64', MSVC_ADD_ARG='10.0.19041.0', run_script=True))
This would also allow user-side caching if the overloaded dictionary support were added.
There was a problem hiding this comment.
@jcbrill - can you file an issue about additional arguments to vcvarsall.bat, I have some additional thoughts which would be good to capture in such an issue rather than here.
|
I'm getting a little confused here... there's already a try block in # from subprocess.run
if check and retcode:
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr) |
|
Passing It would seem better to have some of the error handling in _subproc and do as little as possible in get_output. There are a number of calls to _subproc from other modules though so I would be a bit wary of unintended consequences for other code paths. From the 3.10 subprocess documentation:
From my reading of the documentation, CalledProcessError is one of at least three base exception types that can be raised by subprocess. For better or worse, that was why the code as submitted was catching/re-raising OSError, ValueError, and SubprocessError and then any other exception (Exception) solely to preserve the base exception type and minimize the traceback. I'm leaning towards believing that the value added of that approach is minimal at best compared to catching an exception, writing any debug messages, writing any diagnostic messages to stderr, and then re-raising whatever was caught in get_output. |
| From Joseph Brill: | ||
| - Verify that a user specified msvc script (via MSVC_USE_SCRIPT) exists and raise an exception | ||
| when the user specified msvc script does not exist. | ||
| - Explicitly request _subproc to raise exceptions rather than returning dummyPopen objects when |
There was a problem hiding this comment.
Change to SCons.Action._subproc() please.
There was a problem hiding this comment.
also please add blurb for both of these to RELEASE.txt (in the appropriate sections)
|
Can you break this into two PR's one to check if the specified script exists, and one to deal with the exception issue? Maybe drop the exception handling out of this one and file a separate for the rest. The checking for specified script is a no-brainer to merge, rather not hold that up while pondering the impact of the other changes. In general single item PR's are preferred. |
Easy enough.
Understood.
@bdbaddog The existing RELEASE.txt (differences since 4.2) or should there be a "new" empty one (differences since 4.3)? |
|
Can you add a test and and update to RELEASE.txt ? |
Will issue other PR after this PR is resolved.
Will do.
Release updated. Perhaps adding RELEASE.txt to the contributor checklist template would be helpful as well:
I will try to add a test. Python unit tests in general, and scons tests in particular, are completely foreign to me. Any hints or pointing me in the direction of a known existing test might accelerate the process. Teach a man to fish... |
|
@jcbrill - Hmm I'm surprised we don't have any tests for MSVC_USE_SCRIPT. Here's the wiki page on testing. Also you can take a look at test/MSVC/TARGET_ARCH.py or other test in that dir. Ideally you'd specify a non-existing MSVC_USE_SCRIPT and then run scons and check that stderr matches what you expect. and that SCons exits with error code. |
…ed via MSVC_USE_SCRIPT. Update CHANGES.txt and RELEASE.txt and adjust whitespace.
Manually resolve conflicts in CHANGES.txt and RELEASE.txt
|
Test for user-specified script not found exception added. Whitespace updates in both CHANGES.txt and RELEASE.txt for consistency with earlier/existing style/versions. If not desired, original style can be restored. |
| @@ -0,0 +1,53 @@ | |||
| #!/usr/bin/env python | |||
There was a problem hiding this comment.
Can you update the header copyright blurb with the same content from templates/Tests.py
That has the updated copyright blurb.
This PR eliminates one cause of subprocess/_subproc failure: an unchecked user script file name being passed to subprocess that may not exist.
The issues raised in #4058 are still possible in the face of _subproc/subprocess failures (i.e., this fixes the most likely symptom).
Behavior changed:
Contributor Checklist:
CHANGES.txt(and read theREADME.rst)RELEASE.txt