6

Okay, here's essentially what I am trying to do. I have a process P1. This process needs to invoke the Visual Studio command-line compiler cl.exe in a separate process P2 (obviously). However, as everyone who has ever used the Visual Studio command-line compiler knows, you cannot simply invoke cl.exe and expect a good experience. You instead have to first run the batch script %VSXXXCOMNTOOLS%\vsvars32.bat (where XXX is the Visual Studio version number). This script sets a few key environment variables used by the compiler (such as what to use as the include path). Using a batch script, this is insanely easy to do:

call "%VS110COMNTOOLS%\vsvars32.bat"
...
cl Foo.cpp Bar.cpp ...

since just calling a batch file from a batch script runs in the same process (and thus the added environment variables are persistent). This is what I used to do before I realized that I need more flexibility and decided to port my script to C++ which, so far, has worked wonderfully. That is, until I got to the point where I need to implement the actual compilation.

So, that's the problem I am ultimately trying to solve. The best idea I have come up with is to invoke cmd.exe /c "%VS110COMNTOOLS%\vsvars32.bat" in a separate process P3 using CreateProcess, wait for that process to terminate, and then extract the modified environment variables from that child process. That is, P1 creates P3 and waits for it to finish. P1 then sets P3's environment variables as its own. P1 then creates P2 with these environment variables set. So the code looks roughly as follows (minus all error checking):

...
CreateProcess(TEXT("cmd"), TEXT("/c \"%VS110COMNTOOLS%\vsvars32.bat\""), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

WaitForSingleObject(pi.hProcess, INFINITE);

/* Set current process environment using pi.hProcess */
CloseHandle(pi.hProcess);

...

CreateProcess(TEXT("cl"), TEXT("..."), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

That would be the preferred solution. I am not entirely sure if such a thing is possible, but based off my research, it appears there is a way to do this in .NET, and ProcessExplorer appears to be able to read the environment of arbitrary processes, so I would assume such a solution is possible. I just can't seem to find any documented functions that are able to get environment variables from child processes. There's also an old discussion that is similar to this on MSDN. One of the responses mentions setting Merge Environment to yes. Anyone know what that is/means? I can't seem to find any documentation on it.

If it turns out this is not possible, alternate solutions I have thought about is (1) writing a batch script that simply calls vsvars32.bat and then invokes cl.exe with the input arguments, (2) invoking cmd instead of cl with arguments to run vsvars32.bat and then compile (similar to 1, but more extensible... but not sure if possible), or (3) print the environment variables to a file and then read those in. I'd prefer not to use any of such solutions if possible.

I'm also open to alternate suggestions. Just know that 99% of what I need to do is already done, so clean, non-hacky solutions are preferred.

Duncan
  • 980
  • 6
  • 17

2 Answers2

5

The clean way to do this is to run vcvars32 to set all the environment variables and then run your process P1.

cmd /C vcvars32.bat && P1

Note that the user doesn't have to do this manually. Create a shortcut with this target:

cmd /C ""C:\Some Path\vcvarsall.bat" && start /separate C:\SomeOtherPath\YourGui.exe"

This sets the environment variables and then launches your GUI app. The start /separate stops the command-prompt lingering once the GUI app has started. If you also want this to be convenient to run from the command-line you can put it all in a batch file.

If for some reason you don't want to do this, the simplest way to get the environment variables from a batch script is to run:

cmd /U /C vcvars32.bat && set

This writes the values to standard output in Unicode. You can use a pipe to retrieve the values. This is much less hacky than trying to retrieve the variables from the memory of another process.

N.B. If you want to test this at a command-prompt you need to run:

cmd /U /C "vcvars32.bat && set"

The quotes ensure that the set runs in the child command processor.

arx
  • 16,686
  • 2
  • 44
  • 61
  • 2
    +1 for piping the output so it can be captured. The original process can call `CreateProcess()` to run `cmd.exe` with redirected output (there are examples on MSDN), and then include the captured values in the `lpEnvironment` parameter when calling `CreateProcess()` to run `cl.exe`. – Remy Lebeau Jan 19 '14 at 23:21
  • The "clean" way is how I have been testing so far. The downside (apart from the user having to remember to run the script - VS120COMNTOOLS\vsvars32.bat is a long name!) is that it requires the process to have originated from the command line. I plan to add a graphical client for configuring options and what-not later, so having to invoke that from the command line **after** running vsvars would be slightly inconvenient. Piping the output is something I was trying to avoid if I could, but it appears to be the most viable solution at this point. – Duncan Jan 20 '14 at 09:58
  • 1
    @Duncan: Although the command-processor is involved, your app doesn't have to be launched by the user from the command-line. I've updated my answer to explain how to handle this. – arx Jan 20 '14 at 13:02
  • Eeeh, that's still not preferable for a number of reasons, debugging being a big one. – Duncan Jan 21 '14 at 18:53
1

Do not retrieve and set environment variables across process boundaries. Run vsvars32.bat and cl.exe in the same process, like they are meant to be. cmd.exe lets you execute multiple commands at one time using its && operator:

cmd.exe /c "\"%VS110COMNTOOLS%\vsvars32.bat\" && cl ..."
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Is there any particular reason not to? I need to potentially compile hundreds of executables and I'd rather avoid taking the time to run `vsvars` before each one – Duncan Jan 19 '14 at 21:14