Invoke source control compare operation in vs2012 extension - visual-studio

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;

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 😣

Retrieve Application Version in ASP Net Core 2.0 MVC

I am finishing off building an mvc web application using .net core 2.0 with vs2017 on Win10.In writing an 'About' page I looked to put in the current project version number (at present still set at 1.0.0). I would have thought that pretty straightforward!
The only reference I could find suggested:
AppVersion = typeof(RuntimeEnvironment).GetTypeInfo ().Assembly
.GetCustomAttribute<AssemblyFileVersionAttribute> ().Version;
However, this returns, in my case, '4.6.25814.01' - not what is required.
Can anyone suggest how to retrieve the version in code, please?
I assume that I want the 'Package Version' but admit I am not clear on the distinction between/how one would use 'Package Version', 'Assembly Version' and 'Assembly File Version'.
When you call typeof(RuntimeEnvironment).Assembly, you're querying the containing assembly of that type. In this case this would be System.Runtime.InteropServices.dll or Microsoft.Dotnet.PlatformAbstractions.dll, depending on the namespace you've imported.
To get the information of your own assembly, you could simply replace RuntimeEnvironment with one of your own types, for example
var appVersion = typeof(Program).Assembly
.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
or even
var appVersion = typeof(HomeController).Assembly
.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
This would return "6.6.7.0" if the Package version if your project is set as follows:
You were close!
Here you can find more information on reflection for .NET in general, but it should work fine for .NET Core.
Tried on version 2.0
using System.Reflection;
var appVersion = string.Empty;
var customAttribute = typeof(Program).Assembly.GetCustomAttributes(false).SingleOrDefault(o => o.GetType() == typeof(AssemblyFileVersionAttribute));
if (null != customAttribute)
{
if (customAttribute is AssemblyFileVersionAttribute)
{
var fileVersionAttribute = customAttribute as AssemblyFileVersionAttribute;
appVersion = fileVersionAttribute.Version;
}
}
AssemblyFileVersionAttribute type is in System.Reflection namespace.

How to get expanded path from EnvDTE / VCProjectEngine

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);

How to get the installation directory?

The MSI stores the installation directory for the future uninstall tasks.
Using the INSTALLPROPERTY_INSTALLLOCATION property (that is "InstallLocation") works only the installer has set the ARPINSTALLLOCATION property during the installation. But this property is optional and almost nobody uses it.
How could I retrieve the installation directory?
Use a registry key to keep track of your install directory, that way you can reference it when upgrading and removing the product.
Using WIX I would create a Component that creates the key, right after the Directy tag of the install directory, declaration
I'd use MsiGetComponentPath() - you need the ProductId and a ComponentId, but you get the full path to the installed file - just pick one that goes to the location of your installation directory. If you want to get the value of a directory for any random MSI, I do not believe there is an API that lets you do that.
I would try to use Installer.OpenProduct(productcode). This opens a session, on which you can then ask for Property("TARGETDIR").
Try this:
var sPath = this.Context.Parameters["assemblypath"].ToString();
As stated elsewhere in the thread, I normally write a registry key in HKLM to be able to easily retrieve the installation directory for subsequent installs.
In cases when I am dealing with a setup that hasn't done this, I use the built-in Windows Installer feature AppSearch: http://msdn.microsoft.com/en-us/library/aa367578(v=vs.85).aspx to locate the directory of the previous install by specifying a file signature to look for.
A file signature can consist of the file name, file size and file version and other file properties. Each signature can be specified with a certain degree of flexibility so you can find different versions of the the same file for instance by specifying a version range to look for. Please check the SDK documentation: http://msdn.microsoft.com/en-us/library/aa371853(v=vs.85).aspx
In most cases I use the main application EXE and set a tight signature by looking for a narrow version range of the file with the correct version and date.
Recently I needed to automate Natural Docs install through Ketarin. I could assume it was installed into default path (%ProgramFiles(x86)%\Natural Docs), but I decided to take a safe approach. Sadly, even if the installer created a key on HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall, none of it's value lead me to find the install dir.
The Stein answer suggests AppSearch MSI function, and it looks interesting, but sadly Natural Docs MSI installer doesn't provide a Signature table to his approach works.
So I decided to search through registry to find any reference to Natural Docs install dir, and I find one into HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components key.
I developed a Reg Class in C# for Ketarin that allows recursion. So I look all values through HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components and if the Main application executable (NaturalDocs.exe) is found into one of subkeys values, it's extracted (C:\Program Files (x86)\Natural Docs\NaturalDocs.exe becomes C:\Program Files (x86)\Natural Docs) and it's added to the system environment variable %PATH% (So I can call "NaturalDocs.exe" directly instead of using full path).
The Registry "class" (functions, actually) can be found on GitHub (RegClassCS).
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo("NaturalDocs.exe", "-h");
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
var process = System.Diagnostics.Process.Start (startInfo);
process.WaitForExit();
if (process.ExitCode != 0)
{
string Components = #"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components";
bool breakFlag = false;
string hKeyName = "HKEY_LOCAL_MACHINE";
if (Environment.Is64BitOperatingSystem)
{
hKeyName = "HKEY_LOCAL_MACHINE64";
}
string[] subKeyNames = RegGetSubKeyNames(hKeyName, Components);
// Array.Reverse(subKeyNames);
for(int i = 0; i <= subKeyNames.Length - 1; i++)
{
string[] valueNames = RegGetValueNames(hKeyName, subKeyNames[i]);
foreach(string valueName in valueNames)
{
string valueKind = RegGetValueKind(hKeyName, subKeyNames[i], valueName);
switch(valueKind)
{
case "REG_SZ":
// case "REG_EXPAND_SZ":
// case "REG_BINARY":
string valueSZ = (RegGetValue(hKeyName, subKeyNames[i], valueName) as String);
if (valueSZ.IndexOf("NaturalDocs.exe") != -1)
{
startInfo = new System.Diagnostics.ProcessStartInfo("setx", "path \"%path%;" + System.IO.Path.GetDirectoryName(valueSZ) + "\" /M");
startInfo.Verb = "runas";
process = System.Diagnostics.Process.Start (startInfo);
process.WaitForExit();
if (process.ExitCode != 0)
{
Abort("SETX failed.");
}
breakFlag = true;
}
break;
/*
case "REG_MULTI_SZ":
string[] valueMultiSZ = (string[])RegGetValue("HKEY_CURRENT_USER", subKeyNames[i], valueKind);
for(int k = 0; k <= valueMultiSZ.Length - 1; k++)
{
Ketarin.Forms.LogDialog.Log("valueMultiSZ[" + k + "] = " + valueMultiSZ[k]);
}
break;
*/
default:
break;
}
if (breakFlag)
{
break;
}
}
if (breakFlag)
{
break;
}
}
}
Even if you don't use Ketarin, you can easily paste the function and build it through Visual Studio or CSC.
A more general approach can be taken using RegClassVBS that allow registry key recursion and doesn't depend on .NET Framework platform or build processes.
Please note that the process of enumerating the Components Key can be CPU intense. The example above has a Length parameter, that you can use to show some progress to the user (maybe something like "i from (subKeysName.Length - 1) keys remaining" - be creative). A similar approach can be taken in RegClassVBS.
Both classes (RegClassCS and RegClassVBS) have documentation and examples that can guide you, and you can use it in any software and contribute to the development of them making a commit on the git repo, and (of course) opening a issue on it's github pages if you find any problem that you couldn't resolve yourself so we can try to reproduce the issue to figure out what we can do about it. =)

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