5

I'm developing a small C# GUI tool which is supposed to fetch some C++ code and compile it after going through some wizard. This works all nice if I run it from a command prompt after running the famous vcvarsall.bat. Now I would like the user not to go to a command prompt first but have the program call vcvars followed by nmake and other tools I need. For that to work the environment variables set by vcvars should obviously be kept.

How can I do that?

The best solution I could find yet was to create a temporary cmd/bat script which will call the other tools, but I wonder if there is a better way.


Update: I meanwhile experimented with batch files and cmd. When using batch files vcvars will terminate the complete batch execution so my second command (i.e. nmake) won't be executed. My current workaround is like this (shortened):

string command = "nmake";
string args = "";
string vcvars = "...vcvarsall.bat";
ProcessStartInfo info = new ProcessStartInfo();
info.WorkingDirectory = workingdir;
info.FileName = "cmd";
info.Arguments = "/c \"" + vcvars + " x86 && " + command + " " + args + "\"";
info.CreateNoWindow = true;
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
Process p = Process.Start(info);

This works, but the output from the cmd call is not captured. Still looking for something better

3
  • Currently I open a VC command prompt, here I start my program, this program creates a ProcessStartInfo object with proper WorkingDir, Filename="namke" and redirected ourput ... there nmake inherits my programs environment, which inherited the vc environment Commented Dec 22, 2012 at 22:10
  • Curious: Downvoter, could you please explain the downvote? Thanks. Commented Dec 22, 2012 at 22:46
  • I think my answer to the following question may help you. stackoverflow.com/questions/280559/… Commented Dec 31, 2012 at 21:15

3 Answers 3

3
+50

I have a couple of different suggestions

  1. You may want to research using MSBuild instead of NMake

    It's more complex, but it can be controlled directly from .Net, and it is the format of VS project files for all projects starting with VS 2010, and for C#/VB/etc. projects earlier than that

  2. You could capture the environment using a small helper program and inject it into your processes

    This is probably a bit overkill, but it would work. vsvarsall.bat doesn't do anything more magical than set a few environment variables, so all you have to do is record the result of running it, and then replay that into the environment of processes you create.

The helper program (envcapture.exe) is trivial. It just lists all the variables in its environment and prints them to standard output. This is the entire program code; stick it in Main():

XElement documentElement = new XElement("Environment");
foreach (DictionaryEntry envVariable in Environment.GetEnvironmentVariables())
{
    documentElement.Add(new XElement(
        "Variable",
        new XAttribute("Name", envVariable.Key),
        envVariable.Value
        ));
}

Console.WriteLine(documentElement);

You might be able to get away with just calling set instead of this program and parsing that output, but that would likely break if any environment variables contained newlines.

In your main program:

First, the environment initialized by vcvarsall.bat must be captured. To do that, we'll use a command line that looks like cmd.exe /s /c " "...\vcvarsall.bat" x86 && "...\envcapture.exe" ". vcvarsall.bat modifies the environment, and then envcapture.exe prints it out. Then, the main program captures that output and parses it into a dictionary. (note: vsVersion here would be something like 90 or 100 or 110)

private static Dictionary<string, string> CaptureBuildEnvironment(
    int vsVersion, 
    string architectureName
    )
{
    // assume the helper is in the same directory as this exe
    string myExeDir = Path.GetDirectoryName(
        Assembly.GetExecutingAssembly().Location
        );
    string envCaptureExe = Path.Combine(myExeDir, "envcapture.exe");
    string vsToolsVariableName = String.Format("VS{0}COMNTOOLS", vsVersion);
    string envSetupScript = Path.Combine(
        Environment.GetEnvironmentVariable(vsToolsVariableName),
        @"..\..\VC\vcvarsall.bat"
        );

    using (Process envCaptureProcess = new Process())
    {
        envCaptureProcess.StartInfo.FileName = "cmd.exe";
        // the /s and the extra quotes make sure that paths with
        // spaces in the names are handled properly
        envCaptureProcess.StartInfo.Arguments = String.Format(
            "/s /c \" \"{0}\" {1} && \"{2}\" \"",
            envSetupScript,
            architectureName,
            envCaptureExe
            );
        envCaptureProcess.StartInfo.RedirectStandardOutput = true;
        envCaptureProcess.StartInfo.RedirectStandardError = true;
        envCaptureProcess.StartInfo.UseShellExecute = false;
        envCaptureProcess.StartInfo.CreateNoWindow = true;

        envCaptureProcess.Start();

        // read and discard standard error, or else we won't get output from
        // envcapture.exe at all
        envCaptureProcess.ErrorDataReceived += (sender, e) => { };
        envCaptureProcess.BeginErrorReadLine();

        string outputString = envCaptureProcess.StandardOutput.ReadToEnd();

        // vsVersion < 110 prints out a line in vcvars*.bat. Ignore 
        // everything before the first '<'.
        int xmlStartIndex = outputString.IndexOf('<');
        if (xmlStartIndex == -1)
        {
            throw new Exception("No environment block was captured");
        }
        XElement documentElement = XElement.Parse(
            outputString.Substring(xmlStartIndex)
            );

        Dictionary<string, string> capturedVars 
            = new Dictionary<string, string>();

        foreach (XElement variable in documentElement.Elements("Variable"))
        {
            capturedVars.Add(
                (string)variable.Attribute("Name"),
                (string)variable
                );
        }
        return capturedVars;
    }
}

Later, when you want to run a command in the build environment, you just have to replace the environment variables in the new process with the environment variables captured earlier. You should only need to call CaptureBuildEnvironment once per argument combination, each time your program is run. Don't try to save it between runs though or it'll get stale.

static void Main()
{
    string command = "nmake";
    string args = "";

    Dictionary<string, string> buildEnvironment = 
        CaptureBuildEnvironment(100, "x86");

    ProcessStartInfo info = new ProcessStartInfo();
    // the search path from the adjusted environment doesn't seem
    // to get used in Process.Start, but cmd will use it.
    info.FileName = "cmd.exe";
    info.Arguments = String.Format(
        "/s /c \" \"{0}\" {1} \"",
        command,
        args
        );
    info.CreateNoWindow = true;
    info.UseShellExecute = false;
    info.RedirectStandardOutput = true;
    info.RedirectStandardError = true;
    foreach (var i in buildEnvironment)
    {
        info.EnvironmentVariables[(string)i.Key] = (string)i.Value;
    }

    using (Process p = Process.Start(info))
    {
        // do something with your process. If you're capturing standard output,
        // you'll also need to capture standard error. Be careful to avoid the
        // deadlock bug mentioned in the docs for
        // ProcessStartInfo.RedirectStandardOutput. 
    }
}

If you use this, be aware that it will probably die horribly if vcvarsall.bat is missing or fails, and there may be problems with systems with locales other than en-US.

Sign up to request clarification or add additional context in comments.

5 Comments

Option one is no choice as the system in it's entirely is more complex and out of my control. How do I get the environment after the vcvars call?
I've added some more clarifying comments about what the three code snippets are actually doing
Have you tried that? - I have no Windows box at hand right now, but when running my code from the question I can't capture the cmd output as this creates a, due to CreateNoWIndow, invisble window and pastes the output there, making it inaccesible via the process's StandardOutput stream
CreateNoWindow isn't the problem. It actually doesn't create an invisible console window; it simply says to not create a new one. The problem seems to be that cmd.exe doesn't forward on the standard output handle to processes it starts unless it also has a valid handle for standard error. I've added a workaround for that to my answer.
Ha! The process.RedirectStandardError = true; is what I really needed. I still consider this solution a hack, but I'll follow the approach. Thanks!
1

There is probably no better way than collect all the data you need, generate bat file and run it using Process class. As you wrote, you are redirecting output, which means you must set UseShellExecute = false; so I think there is no way to set your variables other then calling SET from the bat file.

4 Comments

Yeah, as said that's my current workaround idea ... while that has soooo many flaws (hard to see which step broke in case of error, hard to split output-logfile, ...) so I'm looking for something better :-)
@johannes Actually not that hard, just handle process.ErrorDataReceived event and insert some echo between processes.
Yes and then parse the output and hope that the echo sequence is truly unique and stuff ... as said: It's possible but annoying.
@johannes but why not add something like **ECHO ***********command name ******** ** before every command?
0

EDIT: adding a specific use case for nmake calling

I've needed to get various "build path stuff" in the past, and this is what I've used - you may need to tweak things here or there to suit, but basically, the only thing that vcvars does is set up a bunch of paths; these helper methods go fetch those path names, you'd just need to pass them into your start info:

public static string GetFrameworkPath()
{
    var frameworkVersion = string.Format("v{0}.{1}.{2}", Environment.Version.Major, Environment.Version.Minor, Environment.Version.Build);
    var is64BitProcess = Environment.Is64BitProcess;
    var windowsPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
    return Path.Combine(windowsPath, "Microsoft.NET", is64BitProcess ? "Framework64" : "Framework", frameworkVersion);  
}

public static string GetPathToVisualStudio(string version)
{   
    var is64BitProcess = Environment.Is64BitProcess;
    var registryKeyName = string.Format(@"Software\{0}Microsoft\VisualStudio\SxS\VC7", is64BitProcess ? @"Wow6432Node\" : string.Empty);
    var vsKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyName);
    var versionExists = vsKey.GetValueNames().Any(valueName => valueName.Equals(version));
    if(versionExists)
    {
        return vsKey.GetValue(version).ToString();
    }
    else
    {
        return null;
    }
}

And you'd take advantage of this stuff via something like:

var paths = new[]
    { 
        GetFrameworkPath(), 
        GetPathToVisualStudio("10.0"),
        Path.Combine(GetPathToVisualStudio("10.0"), "bin"),
    };  

var previousPaths = Environment.GetEnvironmentVariable("PATH").ToString();
var newPaths = string.Join(";", previousPaths.Split(';').Concat(paths));
Environment.SetEnvironmentVariable("PATH", newPaths);

var startInfo = new ProcessStartInfo()
{
    FileName = "nmake",
    Arguments = "whatever you'd pass in here",
};
var process = Process.Start(startInfo);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.