How to get expanded path from EnvDTE / VCProjectEngine - visual-studio-2010

I am trying to write a tool to create a zip file containing all PDBs files from one Visual Studio 2010 solution.
I can get every PDB filepath in the solution with the following code. However, the property value contains Visual Studio macro like $(TargetDir), $(TargetName) and so on.
Is there a function in the EnvDTE API to expand those macros to their current values ?
On the other hand, any other methods that would achieve my initial goal are also welcome !
Thanks
System.Type t = System.Type.GetTypeFromProgID("VisualStudio.DTE.10.0");
object obj = Activator.CreateInstance(t, true);
DTE dte = (DTE)obj;
Solution sln = dte.Solution;
sln.Open(args[0]);
while (sln.IsOpen == false)
{
System.Threading.Thread.Sleep(100);
}
sln.SolutionBuild.SolutionConfigurations.Item("Release").Activate();
foreach (EnvDTE.Project project in sln.Projects)
{
Console.WriteLine("Inspecting project {0}", project.Name);
VCProject vcproj = (VCProject)project.Object;
if (vcproj == null) // this is not a visual c++ project
continue;
IVCCollection cfgs = vcproj.Configurations;
VCConfiguration cfg = cfgs.Item(1);
VCLinkerTool tool = cfg.Tools("VCLinkerTool");
if (tool == null) // this is not a DLL/EXE project
continue;
Console.WriteLine("Program database = " + tool.ProgramDatabaseFile);
}

I haven't tried this with VS2010, but in VS2008 you can call VCConfiguration.Evaluate to do this. In your example, it would be something like this:
string evaluatedPdbPath = cfg.Evaluate(tool.ProgramDatabaseFile);

Related

Problem Generating Html Report Using DbUp during Octopus Deployment

Using Octopus Deploy to deploy a simple API.
The first step of our deployment process is to generate an HTML report with the delta of the scripts run vs the scripts required to run. I used this tutorial to create the step.
The relevant code in my console application is:
var reportLocationSection = appConfiguration.GetSection(previewReportCmdLineFlag);
if (reportLocationSection.Value is not null)
{
// Generate a preview file so Octopus Deploy can generate an artifact for approvals
try
{
var report = reportLocationSection.Value;
var fullReportPath = Path.Combine(report, deltaReportName);
Console.WriteLine($"Generating upgrade report at {fullReportPath}");
upgrader.GenerateUpgradeHtmlReport(fullReportPath);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return operationError;
}
}
The Powershell which I am using in the script step is:
# Get the extracted path for the package
$packagePath = $OctopusParameters["Octopus.Action.Package[DatabaseUpdater].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]
$reportPath = $OctopusParameters["Project.HtmlReport.Location"]
Write-Host "Report Path: $($reportPath)"
$exeToRun = "$($packagePath)\DatabaseUpdater.exe"
$generatedReport = "$($reportPath)\UpgradeReport.html"
Write-Host "Generated Report: $($generatedReport)"
if ((test-path $reportPath) -eq $false){
New-Item "Creating new directory..."
} else {
New-Item "Directory already exists."
}
# Run this .NET app, passing in the Connection String and a flag
# which tells the app to create a report, but not update the database
& $exeToRun --connectionString="$($connectionString)" --previewReportPath="$($reportPath)"
New-OctopusArtifact -Path "$($generatedReport)"
The error reported by Octopus is:
'Could not find file 'C:\DeltaReports\Some API\2.9.15-DbUp-Test-9\UpgradeReport.html'.'
I'm guessing that is being thrown when this powershell line is hit: New-OctopusArtifact ...
And that seems to indicate that the report was never created.
I've used a bit of logging to log out certain variables and the values look sound:
Report Path: C:\DeltaReports\Some API\2.9.15-DbUp-Test-9
Generated Report: C:\DeltaReports\Some API\2.9.15-DbUp-Test-9\UpgradeReport.html
Generating upgrade report at C:\DeltaReports\Some API\2.9.15-DbUp-Test-9\UpgradeReport.html
As you can see in the C#, the relevant code is wrapped in a try/catch block, but I'm not sure whether the error is being written out there or at a later point by Octopus (I'd need to do a pull request to add a marker in the code).
Can anyone see a way forward win resolving this? Has anyone else encountered this?
Cheers
I recently redid some of the work from that article for this video up on YouTube. I did run into some issues with the .SQL files not being included in the assembly. I think it was after I upgraded to .NET 6. But that might be a coincidence.
Anyway, because the files weren't being included in the assembly, when I ran the command line app via Octopus, it wouldn't properly generate the file for me. I ended up configuring the project to copy the .SQL files to a folder in the output directory instead of embedding them in the assembly. You can view a sample package here.
One thing that helped me is running the app in a debugger with the same parameters just to make sure it was actually generating the file. I'm sure you already thought of that, but I'd be remiss if I forgot to include it in my answer. :)
FWIW, this is my updated scripts.
First, the Octopus Script:
$packagePath = $OctopusParameters["Octopus.Action.Package[Trident.Database].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Connection.String"]
$environmentName = $OctopusParameters["Octopus.Environment.Name"]
$reportPath = $OctopusParameters["Project.Database.Report.Path"]
cd $packagePath
$appToRun = ".\Octopus.Trident.Database.DbUp"
$generatedReport = "$reportPath\UpgradeReport.html"
& $appToRun --ConnectionString="$connectionString" --PreviewReportPath="$reportPath"
New-OctopusArtifact -Path "$generatedReport" -Name "$environmentName.UpgradeReport.html"
My C# code can be found here but for ease of use, you can see it all here (I'm not proud of how I parse the parameters).
static void Main(string[] args)
{
var connectionString = args.FirstOrDefault(x => x.StartsWith("--ConnectionString", StringComparison.OrdinalIgnoreCase));
connectionString = connectionString.Substring(connectionString.IndexOf("=") + 1).Replace(#"""", string.Empty);
var executingPath = Assembly.GetExecutingAssembly().Location.Replace("Octopus.Trident.Database.DbUp", "").Replace(".dll", "").Replace(".exe", "");
Console.WriteLine($"The execution location is {executingPath}");
var deploymentScriptPath = Path.Combine(executingPath, "DeploymentScripts");
Console.WriteLine($"The deployment script path is located at {deploymentScriptPath}");
var postDeploymentScriptsPath = Path.Combine(executingPath, "PostDeploymentScripts");
Console.WriteLine($"The deployment script path is located at {postDeploymentScriptsPath}");
var upgradeEngineBuilder = DeployChanges.To
.SqlDatabase(connectionString, null)
.WithScriptsFromFileSystem(deploymentScriptPath, new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 1 })
.WithScriptsFromFileSystem(postDeploymentScriptsPath, new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 2 })
.WithTransactionPerScript()
.LogToConsole();
var upgrader = upgradeEngineBuilder.Build();
Console.WriteLine("Is upgrade required: " + upgrader.IsUpgradeRequired());
if (args.Any(a => a.StartsWith("--PreviewReportPath", StringComparison.InvariantCultureIgnoreCase)))
{
// Generate a preview file so Octopus Deploy can generate an artifact for approvals
var report = args.FirstOrDefault(x => x.StartsWith("--PreviewReportPath", StringComparison.OrdinalIgnoreCase));
report = report.Substring(report.IndexOf("=") + 1).Replace(#"""", string.Empty);
if (Directory.Exists(report) == false)
{
Directory.CreateDirectory(report);
}
var fullReportPath = Path.Combine(report, "UpgradeReport.html");
if (File.Exists(fullReportPath) == true)
{
File.Delete(fullReportPath);
}
Console.WriteLine($"Generating the report at {fullReportPath}");
upgrader.GenerateUpgradeHtmlReport(fullReportPath);
}
else
{
var result = upgrader.PerformUpgrade();
// Display the result
if (result.Successful)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Success!");
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(result.Error);
Console.WriteLine("Failed!");
}
}
}
I hope that helps!
After long and detailed investigation, we discovered the answer was quite obvious.
We assumed the existing deploy process configuration was sound. Because we never had a problem with it (until now). As it transpires, there was a problem which led to the Development deployments being deployed twice.
Hence, the errors like the one above and others which talked about file handles being held by another process.
It was actually obvious in hindsight, but we were blind to it as we thought the existing process was sound 😣

How do I add global properties to generated Visual Studio projects and solutions via premake5?

I would like to add two conditional properties to my project configuration to target a non-default vcpkg triplet: https://github.com/microsoft/vcpkg/blob/master/docs/users/integration.md#with-msbuild
My project files are generated by premake. How would I go about this?
you could try to override the global function from the visual studio generator some thing like this ... not tested ...
local vs2010 = premake.vstudio.vs2010
function vcPkgOverride(prj)
-- go trough the configs and platforms and figure out which conditions to put
for _, cfg in pairs(prj.cfgs) do
local condition = vs2010.condition(cfg)
if cfg.platform == "Win32" then
vs2010.vc2010.element("VcpkgTriplet ", condition, "x86-windows-static")
else if cfg.platform == "x64" then
vs2010.vc2010.element("VcpkgTriplet ", condition, "x64-windows-static")
end
end
end
premake.override(vc2010.elements, "globals", function (oldfn, prj)
local elements = oldfn(prj)
elements = table.join(elements, {
vcPkgOverride
})
end
return elements
end)
UPDATE
The code above doesn't seem to work for premake5.0.0-alpha14 so I tweaked it based on the docs and here you have a less general, but working version:
require('vstudio')
local vs = premake.vstudio.vc2010
local function premakeVersionComment(prj)
premake.w('<!-- Generated by Premake ' .. _PREMAKE_VERSION .. ' -->')
end
local function vcpkg(prj)
premake.w('<VcpkgTriplet>x64-windows-static</VcpkgTriplet>')
premake.w('<VcpkgEnabled>true</VcpkgEnabled>')
end
premake.override(premake.vstudio.vc2010.elements, "project", function(base, prj)
local calls = base(prj)
table.insertafter(calls, vs.xmlDeclaration, premakeVersionComment)
return calls
end)
premake.override(premake.vstudio.vc2010.elements, "globals", function(base, prj)
local calls = base(prj)
table.insertafter(calls, vs.globals, vcpkg)
return calls
end)
Add this at the start of your main premake5.lua script, or find a way to include it from somewhere else (I don't know much about lua or premake I just needed to fix this and wanted to show it to the community)
Have you tried using CMake instead? It's a far more sophisticated build system that should handle this trivially.

Invoke source control compare operation in vs2012 extension

I am writing a vs2012 extension that will talk to TFS 2010 (though I would prefer if it could also work with tfs2012).
I need to invoke a compare operations on a file from the extension.
I want to use the default compare tool that is configured in visual studio at the moment of the innovation (because the user can configure a different compare tool).
I have the location of the file and I want to be able to invoke the following:
open the default compare.
open a compare with latest version
open a compare with workspace version
Use IVsDifferenceService to invoke Visual Studio diff tool from your VSPackage:
private void Compare(string leftFile, string rightFile)
{
var diffService = (IVsDifferenceService)GetService(typeof(SVsDifferenceService));
if (diffService != null)
{
ErrorHandler.ThrowOnFailure(
diffService.OpenComparisonWindow(leftFile, rightFile).Show()
);
}
}
To test it you need to set the workspace and download the file you want to compare:
// TODO: add some error handling
var tpc = new TfsTeamProjectCollection(new Uri("http://tfs.company.com:8080/tfs"));
var vcs = tpc.GetService<VersionControlServer>();
var workspace = vcs.GetWorkspace(Environment.MachineName, vcs.AuthorizedUser);
string localItem = #"C:\workspace\project\somefile.cs";
var folder = workspace.GetWorkingFolderForLocalItem(localItem);
var item = vcs.GetItem(folder.ServerItem, VersionSpec.Latest);
var latestItem = string.Format("{0}~{1}", localItem, item.ChangesetId);
item.DownloadFile(latestItem);
Compare(localItem, latestItem);
References:
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;

Save all files in Visual Studio project as UTF-8

I wonder if it's possible to save all files in a Visual Studio 2008 project into a specific character encoding. I got a solution with mixed encodings and I want to make them all the same (UTF-8 with signature).
I know how to save single files, but how about all files in a project?
Since you're already in Visual Studio, why not just simply write the code?
foreach (var f in new DirectoryInfo(#"...").GetFiles("*.cs", SearchOption.AllDirectories)) {
string s = File.ReadAllText(f.FullName);
File.WriteAllText (f.FullName, s, Encoding.UTF8);
}
Only three lines of code! I'm sure you can write this in less than a minute :-)
This may be of some help.
link removed due to original reference being defaced by spam site.
Short version: edit one file, select File -> Advanced Save Options. Instead of changing UTF-8 to Ascii, change it to UTF-8. Edit: Make sure you select the option that says no byte-order-marker (BOM)
Set code page & hit ok. It seems to persist just past the current file.
In case you need to do this in PowerShell, here is my little move:
Function Write-Utf8([string] $path, [string] $filter='*.*')
{
[IO.SearchOption] $option = [IO.SearchOption]::AllDirectories;
[String[]] $files = [IO.Directory]::GetFiles((Get-Item $path).FullName, $filter, $option);
foreach($file in $files)
{
"Writing $file...";
[String]$s = [IO.File]::ReadAllText($file);
[IO.File]::WriteAllText($file, $s, [Text.Encoding]::UTF8);
}
}
I would convert the files programmatically (outside VS), e.g. using a Python script:
import glob, codecs
for f in glob.glob("*.py"):
data = open("f", "rb").read()
if data.startswith(codecs.BOM_UTF8):
# Already UTF-8
continue
# else assume ANSI code page
data = data.decode("mbcs")
data = codecs.BOM_UTF8 + data.encode("utf-8")
open("f", "wb").write(data)
This assumes all files not in "UTF-8 with signature" are in the ANSI code page - this is the same what VS 2008 apparently also assumes. If you know that some files have yet different encodings, you would have to specify what these encodings are.
Using C#:
1) Create a new ConsoleApplication, then install Mozilla Universal Charset Detector
2) Run code:
static void Main(string[] args)
{
const string targetEncoding = "utf-8";
foreach (var f in new DirectoryInfo(#"<your project's path>").GetFiles("*.cs", SearchOption.AllDirectories))
{
var fileEnc = GetEncoding(f.FullName);
if (fileEnc != null && !string.Equals(fileEnc, targetEncoding, StringComparison.OrdinalIgnoreCase))
{
var str = File.ReadAllText(f.FullName, Encoding.GetEncoding(fileEnc));
File.WriteAllText(f.FullName, str, Encoding.GetEncoding(targetEncoding));
}
}
Console.WriteLine("Done.");
Console.ReadKey();
}
private static string GetEncoding(string filename)
{
using (var fs = File.OpenRead(filename))
{
var cdet = new Ude.CharsetDetector();
cdet.Feed(fs);
cdet.DataEnd();
if (cdet.Charset != null)
Console.WriteLine("Charset: {0}, confidence: {1} : " + filename, cdet.Charset, cdet.Confidence);
else
Console.WriteLine("Detection failed: " + filename);
return cdet.Charset;
}
}
I have created a function to change encoding files written in asp.net.
I searched a lot. And I also used some ideas and codes from this page. Thank you.
And here is the function.
Function ChangeFileEncoding(pPathFolder As String, pExtension As String, pDirOption As IO.SearchOption) As Integer
Dim Counter As Integer
Dim s As String
Dim reader As IO.StreamReader
Dim gEnc As Text.Encoding
Dim direc As IO.DirectoryInfo = New IO.DirectoryInfo(pPathFolder)
For Each fi As IO.FileInfo In direc.GetFiles(pExtension, pDirOption)
s = ""
reader = New IO.StreamReader(fi.FullName, Text.Encoding.Default, True)
s = reader.ReadToEnd
gEnc = reader.CurrentEncoding
reader.Close()
If (gEnc.EncodingName <> Text.Encoding.UTF8.EncodingName) Then
s = IO.File.ReadAllText(fi.FullName, gEnc)
IO.File.WriteAllText(fi.FullName, s, System.Text.Encoding.UTF8)
Counter += 1
Response.Write("<br>Saved #" & Counter & ": " & fi.FullName & " - <i>Encoding was: " & gEnc.EncodingName & "</i>")
End If
Next
Return Counter
End Function
It can placed in .aspx file and then called like:
ChangeFileEncoding("C:\temp\test", "*.ascx", IO.SearchOption.TopDirectoryOnly)
if you are using TFS with VS :
http://msdn.microsoft.com/en-us/library/1yft8zkw(v=vs.100).aspx
Example :
tf checkout -r -type:utf-8 src/*.aspx
Thanks for your solutions, this code has worked for me :
Dim s As String = ""
Dim direc As DirectoryInfo = New DirectoryInfo("Your Directory path")
For Each fi As FileInfo In direc.GetFiles("*.vb", SearchOption.AllDirectories)
s = File.ReadAllText(fi.FullName, System.Text.Encoding.Default)
File.WriteAllText(fi.FullName, s, System.Text.Encoding.Unicode)
Next
If you want to avoid this type of error :
Use this following code :
foreach (var f in new DirectoryInfo(#"....").GetFiles("*.cs", SearchOption.AllDirectories))
{
string s = File.ReadAllText(f.FullName, Encoding.GetEncoding(1252));
File.WriteAllText(f.FullName, s, Encoding.UTF8);
}
Encoding number 1252 is the default Windows encoding used by Visual Studio to save your files.
Convert from UTF-8-BOM to UTF-8
Building on rasx's answer, here is a PowerShell function that assumes your current files are already encoded in UTF-8 (but maybe with BOM) and converts them to UTF-8 without BOM, therefore preserving existing Unicode characters.
Function Write-Utf8([string] $path, [string] $filter='*')
{
[IO.SearchOption] $option = [IO.SearchOption]::AllDirectories;
[String[]] $files = [IO.Directory]::GetFiles((Get-Item $path).FullName, $filter, $option);
foreach($file in $files)
{
"Writing $file...";
[String]$s = [IO.File]::ReadAllText($file, [Text.Encoding]::UTF8);
[Text.Encoding]$e = New-Object -TypeName Text.UTF8Encoding -ArgumentList ($false);
[IO.File]::WriteAllText($file, $s, $e);
}
}
Experienced encoding problems after converting solution from VS2008 to VS2015. After conversion all project files was encoded in ANSI, but they contained UTF8 content and was recongnized as ANSI files in VS2015. Tried many conversion tactics, but worked only this solution.
Encoding encoding = Encoding.Default;
String original = String.Empty;
foreach (var f in new DirectoryInfo(path).GetFiles("*.cs", SearchOption.AllDirectories))
{
using (StreamReader sr = new StreamReader(f.FullName, Encoding.Default))
{
original = sr.ReadToEnd();
encoding = sr.CurrentEncoding;
sr.Close();
}
if (encoding == Encoding.UTF8)
continue;
byte[] encBytes = encoding.GetBytes(original);
byte[] utf8Bytes = Encoding.Convert(encoding, Encoding.UTF8, encBytes);
var utf8Text = Encoding.UTF8.GetString(utf8Bytes);
File.WriteAllText(f.FullName, utf8Text, Encoding.UTF8);
}
the item is removed from the menu in Visual Studio 2017
You can still access the functionality through File-> Save As -> then clicking the down arrow on the Save button and clicking "Save With Encoding...".
You can also add it back to the File menu through Tools->Customize->Commands if you want to.
I'm only offering this suggestion in case there's no way to automatically do this in Visual Studio (I'm not even sure this would work):
Create a class in your project named 足の不自由なハッキング (or some other unicode text that will force Visual Studio to encode as UTF-8).
Add "using MyProject.足の不自由なハッキング;" to the top of each file. You should be able to do it on everything by doing a global replace of "using System.Text;" with "using System.Text;using MyProject.足の不自由なハッキング;".
Save everything. You may get a long string of "Do you want to save X.cs using UTF-8?" messages or something.

Programmatically retrieve Visual Studio install directory

I know there is a registry key indicating the install directory, but I don't remember what it is off-hand.
I am currently interested in Visual Studio 2008 install directory, though it wouldn't hurt to list others for future reference.
I use this method to find the installation path of Visual Studio 2010:
private string GetVisualStudioInstallationPath()
{
string installationPath = null;
if (Environment.Is64BitOperatingSystem)
{
installationPath = (string)Registry.GetValue(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\10.0\\",
"InstallDir",
null);
}
else
{
installationPath = (string)Registry.GetValue(
"HKEY_LOCAL_MACHINE\\SOFTWARE \\Microsoft\\VisualStudio\\10.0\\",
"InstallDir",
null);
}
return installationPath;
}
I'm sure there's a registry entry as well but I couldn't easily locate it. There is the VS90COMNTOOLS environment variable that you could use as well.
Registry Method
I recommend querying the registry for this information. This gives the actual installation directory without the need for combining paths, and it works for express editions as well. This could be an important distinction depending on what you need to do (e.g. templates get installed to different directories depending on the edition of Visual Studio). The registry locations are as follows (note that Visual Studio is a 32-bit program and will be installed to the 32-bit section of the registry on x64 machines):
Visual Studio: HKLM\SOFTWARE\Microsoft\Visual Studio\Major.Minor:InstallDir
Visual C# Express: HKLM\SOFTWARE\Microsoft\VCSExpress\Major.Minor:InstallDir
Visual Basic Express: HKLM\SOFTWARE\Microsoft\VBExpress\Major.Minor:InstallDir
Visual C++ Express: HKLM\SOFTWARE\Microsoft\VCExpress\Major.Minor:InstallDir
where Major is the major version number, Minor is the minor version number, and the text after the colon is the name of the registry value. For example, the installation directory of Visual Studio 2008 Professional would be located at the HKLM\SOFTWARE\Microsoft\Visual Studio\9.0 key, in the InstallDir value.
Here's a code example that prints the installation directory of several versions of Visual Studio and Visual C# Express:
string visualStudioRegistryKeyPath = #"SOFTWARE\Microsoft\VisualStudio";
string visualCSharpExpressRegistryKeyPath = #"SOFTWARE\Microsoft\VCSExpress";
List<Version> vsVersions = new List<Version>() { new Version("10.0"), new Version("9.0"), new Version("8.0") };
foreach (var version in vsVersions)
{
foreach (var isExpress in new bool[] { false, true })
{
RegistryKey registryBase32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
RegistryKey vsVersionRegistryKey = registryBase32.OpenSubKey(
string.Format(#"{0}\{1}.{2}", (isExpress) ? visualCSharpExpressRegistryKeyPath : visualStudioRegistryKeyPath, version.Major, version.Minor));
if (vsVersionRegistryKey == null) { continue; }
Console.WriteLine(vsVersionRegistryKey.GetValue("InstallDir", string.Empty).ToString());
}
Environment Variable Method
The non-express editions of Visual Studio also write an environment variable that you could check, but it gives the location of the common tools directory, not the installation directory, so you'll have to do some path combining. The format of the environment variable is VS*COMNTOOLS where * is the major and minor version number. For example, the environment variable for Visual Studio 2010 is VS100COMNTOOLS and contains a value like C:\Program Files\Microsoft Visual Studio 10.0\Common7\Tools.
Here's some example code to print the environment variable for several versions of Visual Studio:
List<Version> vsVersions = new List<Version>() { new Version("10.0"), new Version("9.0"), new Version("8.0") };
foreach (var version in vsVersions)
{
Console.WriteLine(Path.Combine(Environment.GetEnvironmentVariable(string.Format("VS{0}{1}COMNTOOLS", version.Major, version.Minor)), #"..\IDE"));
}
Environment: Thanks to Zeb and Sam for the VS*COMNTOOLS environment variable suggestion. To get to the IDE in PowerShell:
$vs = Join-Path $env:VS90COMNTOOLS '..\IDE\devenv.exe'
Registry: Looks like the registry location is HKLM\Software\Microsoft\VisualStudio, with version-specific subkeys for each install. In PowerShell:
$vsRegPath = 'HKLM:\Software\Microsoft\VisualStudio\9.0'
$vs = (Get-ItemProperty $vsRegPath).InstallDir + 'devenv.exe'
[Adapted from here]
For Visual Studio 2017 and Visual Studio 2019 there is the Setup API from Microsoft.
In C#, just add the NuGet package "Microsoft.VisualStudio.Setup.Configuration.Interop", and use it in this way:
try {
var query = new SetupConfiguration();
var query2 = (ISetupConfiguration2)query;
var e = query2.EnumAllInstances();
var helper = (ISetupHelper)query;
int fetched;
var instances = new ISetupInstance[1];
do {
e.Next(1, instances, out fetched);
if (fetched > 0)
Console.WriteLine(instances[0].GetInstallationPath());
}
while (fetched > 0);
return 0;
}
catch (COMException ex) when (ex.HResult == REGDB_E_CLASSNOTREG) {
Console.WriteLine("The query API is not registered. Assuming no instances are installed.");
return 0;
}
You can find more samples for VC, C#, and VB here.
It is a real problem that all Visual Studio versions have their own location. So the solutions here proposed are not generic. However, Microsoft has made a utility available for free (including the source code) that solved this problem (i.e. annoyance). It is called vswhere.exe and you can download it from here. I am very happy with it, and hopefully it will also do for future releases. It makes the whole discussion on this page redundant.
#Dim-Ka has a great answer. If you were curious how you'd implement this in a batch script, this is how.
#echo off
:: BATCH doesn't have logical or, otherwise I'd use it
SET platform=
IF /I [%PROCESSOR_ARCHITECTURE%]==[amd64] set platform=true
IF /I [%PROCESSOR_ARCHITEW6432%]==[amd64] set platform=true
:: default to VS2012 = 11.0
:: the Environment variable VisualStudioVersion is set by devenv.exe
:: if this batch is a child of devenv.exe external tools, we know which version to look at
if not defined VisualStudioVersion SET VisualStudioVersion=11.0
if defined platform (
set VSREGKEY=HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\%VisualStudioVersion%
) ELSE (
set VSREGKEY=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\%VisualStudioVersion%
)
for /f "skip=2 tokens=2,*" %%A in ('reg query "%VSREGKEY%" /v InstallDir') do SET VSINSTALLDIR=%%B
echo %VSINSTALLDIR%
Ah, the 64-bit machine part was the issue. It turns out I need to make sure I'm running the PowerShell.exe under the syswow64 directory in order to get the x86 registry keys.
Now that wasn't very fun.
Use Environment.GetEnvironmentVariable("VS90COMNTOOLS");.
Also in a 64-bit environment, it works for me.
Here's a solution to always get the path for the latest version:
$vsEnvVars = (dir Env:).Name -match "VS[0-9]{1,3}COMNTOOLS"
$latestVs = $vsEnvVars | Sort-Object | Select -Last 1
$vsPath = Get-Content Env:\$latestVs
You can read the VSINSTALLDIR environment variable.
Here is something I have been updating over the years... (for CudaPAD)
Usage examples:
var vsPath = VS_Tools.GetVSPath(avoidPrereleases:true, requiredWorkload:"NativeDesktop");
var vsPath = VS_Tools.GetVSPath();
var vsPath = VS_Tools.GetVSPath(specificVersion:"15");
The drop-in function:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.Setup.Configuration;
using System.IO;
using Microsoft.Win32;
static class VS_Tools
{
public static string GetVSPath(string specificVersion = "", bool avoidPrereleases = true, string requiredWorkload = "")
{
string vsPath = "";
// Method 1 - use "Microsoft.VisualStudio.Setup.Configuration.SetupConfiguration" method.
// Note: This code has is a heavily modified version of Heath Stewart's code.
// original source: (Heath Stewart, May 2016) https://github.com/microsoft/vs-setup-samples/blob/80426ad4ba10b7901c69ac0fc914317eb65deabf/Setup.Configuration.CS/Program.cs
try
{
var e = new SetupConfiguration().EnumAllInstances();
int fetched;
var instances = new ISetupInstance[1];
do
{
e.Next(1, instances, out fetched);
if (fetched > 0)
{
var instance2 = (ISetupInstance2)instances[0];
var state = instance2.GetState();
// Let's make sure this install is complete.
if (state != InstanceState.Complete)
continue;
// If we have a version to match lets make sure to match it.
if (!string.IsNullOrWhiteSpace(specificVersion))
if (!instances[0].GetInstallationVersion().StartsWith(specificVersion))
continue;
// If instances[0] is null then skip
var catalog = instances[0] as ISetupInstanceCatalog;
if (catalog == null)
continue;
// If there is not installation path lets skip
if ((state & InstanceState.Local) != InstanceState.Local)
continue;
// Let's make sure it has the required workload - if one was given.
if (!string.IsNullOrWhiteSpace(requiredWorkload))
{
if ((state & InstanceState.Registered) == InstanceState.Registered)
{
if (!(from package in instance2.GetPackages()
where string.Equals(package.GetType(), "Workload", StringComparison.OrdinalIgnoreCase)
where package.GetId().Contains(requiredWorkload)
orderby package.GetId()
select package).Any())
{
continue;
}
}
else
{
continue;
}
}
// Let's save the installation path and make sure it has a value.
vsPath = instance2.GetInstallationPath();
if (string.IsNullOrWhiteSpace(vsPath))
continue;
// If specified, avoid Pre-release if possible
if (avoidPrereleases && catalog.IsPrerelease())
continue;
// We found the one we need - lets get out of here
return vsPath;
}
}
while (fetched > 0);
}
catch (Exception){ }
if (string.IsNullOrWhiteSpace(vsPath))
return vsPath;
// Fall-back Method: Find the location of visual studio (%VS90COMNTOOLS%\..\..\vc\vcvarsall.bat)
// Note: This code has is a heavily modified version of Kevin Kibler's code.
// source: (Kevin Kibler, 2014) http://stackoverflow.com/questions/30504/programmatically-retrieve-visual-studio-install-directory
List<Version> vsVersions = new List<Version>() { new Version("15.0"), new Version("14.0"),
new Version("13.0"), new Version("12.0"), new Version("11.0") };
foreach (var version in vsVersions)
{
foreach (var isExpress in new bool[] { false, true })
{
RegistryKey registryBase32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
RegistryKey vsVersionRegistryKey = registryBase32.OpenSubKey(
string.Format(#"{0}\{1}.{2}",
(isExpress) ? #"SOFTWARE\Microsoft\VCSExpress" : #"SOFTWARE\Microsoft\VisualStudio",
version.Major, version.Minor));
if (vsVersionRegistryKey == null) { continue; }
string path = vsVersionRegistryKey.GetValue("InstallDir", string.Empty).ToString();
if (!string.IsNullOrEmpty(path))
{
path = Directory.GetParent(path).Parent.Parent.FullName;
if (File.Exists(path + #"\VC\bin\cl.exe") && File.Exists(path + #"\VC\vcvarsall.bat"))
{
vsPath = path;
break;
}
}
}
if (!string.IsNullOrWhiteSpace(vsPath))
break;
}
return vsPath;
}
}
Nowadays, I use the following PowerShell command to get the Visual Studio 2017/2019 path (here with the Common7\IDE suffix, so it mimics the DevEnvDir property):
Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | foreach { Get-ItemProperty $_.PsPath } | where { $_.DisplayName -like '*Visual Studio*' -and $_.InstallLocation.Length -gt 0 } | sort InstallDate -Descending | foreach { (Join-Path $_.InstallLocation 'Common7\IDE') } | where { Test-Path $_ } | select -First 1
If you want to execute it from cmd.exe, the command would look like this:
powershell.exe -ExecutionPolicy Bypass -Command "Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | foreach { Get-ItemProperty $_.PsPath } | where { $_.DisplayName -like '*Visual Studio*' -and $_.InstallLocation.Length -gt 0 } | sort InstallDate -Descending | foreach { (Join-Path $_.InstallLocation 'Common7\IDE') } | where { Test-Path $_ } | select -First 1"
I am using it in a C# project, where I use Rider instead of Visual Studio as my IDE (of course I could have also just manually setup the DevEnvDir property in Rider's settings):
<Target Name="MyTarget" BeforeTargets="Build">
<Exec Condition="'$(DevEnvDir)' == '' Or '$(DevEnvDir)' == '*Undefined*' Or !Exists('$(DevEnvDir)')"
Command="powershell.exe -ExecutionPolicy Bypass -Command "Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | foreach { Get-ItemProperty $_.PsPath } | where { $_.DisplayName -like '*Visual Studio*' -and $_.InstallLocation.Length -gt 0 } | sort InstallDate -Descending | foreach { (Join-Path $_.InstallLocation 'Common7\IDE') } | where { Test-Path $_ } | select -First 1""
ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="DevEnvDir" />
</Exec>
</Target>
I use it to get the path to the VS command prompt batch files (like vcvars64.bat or vcvarsall.bat), so I can invoke them before calling MIDL.exe to generate a type library for my IDL file, so my .NET 5 COM classes can register a type library for themselves when the comhost.dll is being registered via regsvr32.exe.
Note that if you're using Visual Studio Express or Visual C++ Express the keynames contain WDExpress or VCExpress, respectively, instead of VisualStudio.
Aren't there environment settings?
I have VCToolkitInstallDir and VS71COMNTOOLS although I'm using Visual Studio 2003, I don't know if that changed for later versions. Type "set V" at the command line and see if you have them.
This is the easiest solution I came with. It works for x86 and x64, regardless of VS version:
Use
Environment.GetEnvironmentVariable("VSAPPIDDIR")
To get the IDE folder, such as:
"C:\Program Files\Microsoft Visual Studio\2019\Community\Common7\IDE\" On x86 machine.
You can use that to go to any other directory you want, such as:
Dim x = Environment.GetEnvironmentVariable("VSAPPIDDIR").Trim("\"c, "/"c)
x = System.IO.Path.GetDirectoryName(x)
Dim XsdFile = IO.Path.Combine(x, "Packages\Schemas\html\html_5.xsd")
In x64 machine XsdFile will refer to:
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Packages\Schemas\html\html_5.xsd"
Caution: This sems to work with Community edition only!
For newer versions of VS it is better to use from Microsoft provided APIs, because install information is no longer maintained in registry correctly.
install Nuget package Microsoft.VisualStudio.Setup.Configuration.Native
do the trick (returned is tuple with version and path of all VS instances):
private const int REGDB_E_CLASSNOTREG = unchecked((int)0x80040154);
public static IEnumerable<(string, string)> GetVisualStudioInstallPaths()
{
var result = new List<(string, string)>();
try
{
var query = new SetupConfiguration() as ISetupConfiguration2;
var e = query.EnumAllInstances();
int fetched;
var instances = new ISetupInstance[1];
do
{
e.Next(1, instances, out fetched);
if (fetched > 0)
{
var instance2 = (ISetupInstance2)instances[0];
result.Add((instance2.GetInstallationVersion(), instance2.GetInstallationPath()));
}
}
while (fetched > 0);
}
catch (COMException ex) when (ex.HResult == REGDB_E_CLASSNOTREG)
{
}
catch (Exception)
{
}
return result;
}
Regards

Resources