Is there any accessible API via an MSBuild task, target or otherwise that allows me to query what version of a NuGet package a given PackageReference will resolve (or has already resolved) to?
For example, if I have a csproj with
<PackageReference Include=“MyPkg” Version=“1.*”/>
And I have a custom target where I want to
<MyTarget>
<GetVersionOfResolvedPackageReference Name=“MyPkg” OutputProperty=“IWantToKnowThis” /> <!— or something —>
...
From How NuGet resolves package dependencies:
When the NuGet restore process runs prior to a build, it resolves
dependencies first in memory, then writes the resulting graph to a
file called project.assets.json in the obj folder of a project using
PackageReference. MSBuild then reads this file and translates it into
a set of folders where potential references can be found, and then
adds them to the project tree in memory.
<#
.Synopsis
Represents the method that
returns the project.assets
content as object.
#>
function Get-ProjectAssest([System.String]$Assest) {
return (Get-Content $Assest | ConvertTo-Json | ConvertFrom-Json).value |
ConvertFrom-Json
}
Get-ChildItem -Path . 'project.assets.json' -Recurse | ForEach-Object { Get-ProjectAssest($_.FullName) } | ForEach-Object {
# K = Package Identity
# V = Package Version
$_.libraries | ForEach-Object { $_.PSObject.Properties.Name } | Out-File 'PackageReference.ini' -Append
}
This PowerShell script recursively parses directories to find project.assets.json and writes result in PackageReference.ini file. You can call script from MSBuild via Exec task and after that read file via ReadLinesFromFile and performing further processing.
NOTE: Represented script will be produce duplicates of PackageReference lines for multiple projects.
Related
I have a dynamically created variable inside a CI Pipeline, let's call it "var: $(version.number).$(Date:yyyyMMdd)". I wish to reuse this as a part of the Publish Test Results task in a CD Pipeline so I can link both together and have a valid reference. But I can't fathom how to do this.
This is the yaml for the Publish Test Results task in it's most basic form.
steps:
- task: PublishTestResults#2
displayName: 'Publish Test Results **/TEST-*.xml'
Any pointers will be gratefully accepted.
We could write it out to a json/xml file via power shell task, and publish the file as artifacts. Then read in that file via PowerShell in your release definition.
Build Definition
ConvertTo-Json | Out-File "file.json"
Release Definition
Get-Content "file.json" | ConvertFrom-Json
Also, we could pass the variable from build to release via the extension Variable Tools for Azure DevOps Services.
Steps:
Build Definition and result
Release Definition and result
In addition, I found a blog and save the variable to csv file, you could also refer to Passing variables in VSTS, from Build to Release and between environments.
I'm trying to execute msbuild on Azure Devops. Because of that I cannot use the MSBuild task provided.
When I use a Command Line task the command is not recognised. On my local machine I load vcvarsall.bat before I use msbuild. But I've not been unable to work out how to obtain that path in Azure Devops. Doesn't appear to be a Develop Command Prompt task for Azue Devops either.
Any ideas on how I can use msbuild from a Command Line task or Batch Script task? Using their Hosted VS agent.
The best way to do this in a supported way is to use vswhere. The following bit of script will install vswhere (using chocolatey) and then query the installer registry where msbuild can be found. Replace -latest with a more specific version if you need that:
choco install vswhere
for /f "tokens=*" %%i in ('vswhere -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe -nologo') do set msbuildpath="%%i"
echo "##vso[task.setvariable variable=msbuildpath]%msbuildpath%"
This will save the path to msbuild to the environment variable %msbuildpath% as well as the pipeline variable (for this stage) $(msbuildpath).
You can then either use a second run commandline task and pass in $(msbuildpath) or you can simply call MsBuild from the same piece of script mentioned above by calling:
%msbuildpath%
This will make sure your script will remain working, even if Microsoft upgrades their images and moves some things around (which does happen).
You can also get vswhere using wget or invoke-webrequest -outfile from the following location:
https://github.com/Microsoft/vswhere/releases/latest/download/vswhere.exe
Other samples for vswhere syntax can be found on the project wiki, including the syntax for PowerShell.
If you use Hosted Agent 2017 you can run the msbuild.exe from the Command Line task in this way:
Command Line version 1:
Command Line version 2:
Results:
If you are interested in seeing how the built-in Microsoft task resolves the path, all the Azure Devops tasks are provided open-source. These are the path functions you probably care to review.
Here is the solution I came up with using only built-in pipeline tasks which makes the MSBuild bin directory available on the path environment variable.
Create a PowerShell task to generate an MSBuild project to capture and output to a file the variables you are interested in (ex. MSBuildBinPath)
PowerShell script
"<Project DefaultTargets=`"DetectMsBuild`">
<ItemGroup>
<OutFile Include=`"`$(MsBuildDetectionFile)`" />
<OutFile Condition=`"'`$(OutFile)' == ''`" Include=`"msbuildInfo.json`" />
</ItemGroup>
<Target Name=`"DetectMsBuild`">
<PropertyGroup>
<MsBuildPaths>
[{
`"Name`": `"BinPath`",
`"Value`": `"`$(MSBuildBinPath.Replace('\', '\\'))`"
}]
</MsBuildPaths>
</PropertyGroup>
<WriteLinesToFile
File=`"#(Outfile)`"
Lines=`"`$(MsBuildPaths)`"
Overwrite=`"true`"
Encoding=`"UTF-8`" />
</Target>
</Project>" | Out-File -FilePath "msbuilddetect.proj" -Encoding utf8
Set the working directory and any variables accordingly.
PowerShell task settings screenshot:
Create an MSBuild task to run the project file generated by the previous task. Ensure the MSBuild version is set to the version you want to use.
MSBuild task settings screenshot:
Last, create another PowerShell task that will parse the outputted JSON file of the extracted variables and sets environment variables accordingly.
PowerShell script
Write-Host "Current path: $($env.Path)`n`n"
$msBuildVariables = Get-Content -Path msbuildInfo.json | ConvertFrom-Json
$Path = "$($msBuildVariables[0].Value);$($env:Path)"
Write-Host "##vso[task.setvariable variable=Path;]$Path"
PowerShell task settings screenshot:
Here is a screenshot of the task order in the build pipeline.
Earlier versions of MSBuild could be found here: %programfiles(x86)%\msbuild\<version>\bin\msbuild.exe.
But for Visual Studio 2017RC the path is %programfiles(x86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe. The type of install: enteprise, community or professional, seems to be part of the path. And that makes it difficult to specify the correct path to msbuild in a BAT file that should work on many different computers with different versions of Visual Studio 2017.
What is the best way to call Visual Studio 2017 RC's version of Msbuild from a bat file? Or from PowerShell?
The supported way
VS2017's setup engine has an interop API that allows you to query information about which instance(s) of VS2017 are installed. There's a NuGet package for it, and even a sample of how to query, including the installation path. For consumption in a batch or PS file, you might just rewrite the sample app and call it from your script to output the information you need.
The unsupported way
The VS setup engine keeps a store of its data under %ProgramData%\Microsoft\VisualStudio\Packages\_Instances. There will be a folder for each instance of VS2017, and inside that folder is a state.json file that contains information about that installation, including the install path.
Since you need to extract the info from the .json file, you have the option to either write an app that you call from your script file, or come up with some parsing logic directly in your script. This will obviously be fragile as the JSON schema or file location may change.
The brute force method
Assuming you are using the default installation path, you could just recursively search for msbuild.exe under %ProgramFiles(x86)%\Microsoft Visual Studio\ (it appears that there will be 32-bit and 64-bit msbuild.exe for each VS instance). This would probably be the easiest to do within your script file, but does rely on the default installation path (or whatever hardcoded path you wish to search under).
Change your dev environment requirements
The last thing you could do is require that devs use (or somehow invoke) the vsdevcmd.bat to use the VS dev environment. This will get them MSBuild, and additionally any other tools from the VS environment, onto their %PATH%. This does place a requirement on your dev team, but will always be an officially supported way to find msbuild.exe.
Microsoft have created a tool to find the location of Visual Studio 2017 and newer https://github.com/Microsoft/vswhere
That option and newer are decribed in this blog post.
Simplest way...
Install the Build Tools https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=15
Profit; the build tools will install to a separate location thats standard across everywhere you install it regardless of your VS SKU (professional / enterprise / community all install to different paths)
New location C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe
Posting this for folks still trying to locate the newest version of msbuild on a machine and fall back to an older one if that is availble instead. I find this snippet of powershell does the trick, which can easily be invoked from a bat file.
Function Find-MsBuild([int] $MaxVersion = 2017)
{
$agentPath = "$Env:programfiles (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe"
$devPath = "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe"
$proPath = "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\msbuild.exe"
$communityPath = "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe"
$fallback2015Path = "${Env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe"
$fallback2013Path = "${Env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe"
$fallbackPath = "C:\Windows\Microsoft.NET\Framework\v4.0.30319"
If ((2017 -le $MaxVersion) -And (Test-Path $agentPath)) { return $agentPath }
If ((2017 -le $MaxVersion) -And (Test-Path $devPath)) { return $devPath }
If ((2017 -le $MaxVersion) -And (Test-Path $proPath)) { return $proPath }
If ((2017 -le $MaxVersion) -And (Test-Path $communityPath)) { return $communityPath }
If ((2015 -le $MaxVersion) -And (Test-Path $fallback2015Path)) { return $fallback2015Path }
If ((2013 -le $MaxVersion) -And (Test-Path $fallback2013Path)) { return $fallback2013Path }
If (Test-Path $fallbackPath) { return $fallbackPath }
throw "Yikes - Unable to find msbuild"
}
$msbuildPath = Find-MsBuild 2017
Now that VS 2017 has been released, thought it might be useful to add my 2 cents:
Creating a batch file:
Open Notepad
Copy the following and paste into notepad, replacing the solution you want to build with [My Solution Name Here]:
#echo off
:variables
SET msBuildLocation="C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe"
SET solutionDirectory=[My Solution Location Here]
SET batchFileLocation=[BatchFileLocation Here]
cd %solutionDirectory%
echo Building MS files for [My Solution Name Here].
call %msBuildLocation% [My Solution Name].sln /p:Configuration=Debug /m:4 /v:M /fl /flp:LogFile=msbuild.log;Verbosity=Normal /nr:false /consoleloggerparameters:Summary;ShowTimestamp;ShowEventId;PerformanceSummary
echo -
echo --------Done building [My solution name here].--------------
echo -
cd %batchFileLocation%
Save your batch file as whatever you want with the .bat extension. Be sure to select all files instead of .txt or else it won't work.
Call the batch file by issuing the cd command where you have it saved at, and it should work. Or, just double-click. Done.
Please note that my version of visual studio is community; you'll need to replace the path with the appropriate version.
I also run the build tools from a bat file on my CI server: The new location I've found is this:
%programfiles(x86)%\Microsoft Visual
Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe
If you have installed the .NET Core tools preview 3 or later, the dotnet CLI should be available on the %PATH% by default (even if you're not building a .NET Core project), so you could try dotnet msbuild. Ref https://github.com/dotnet/docs/blob/master/docs/core/preview3/tools/dotnet-msbuild.md
I maintain a very simple PowerShell script (gist) to run CI/CLI builds with properly configured Visual Studio environment variables. It uses vswhere.exe and VsDevCmd.bat. It works well on build agents as a part of Azure DevOps pipelines. Particularly for C++ projects, it's vital to set INCLUDE and LIB properly, that's what VsDevCmd.bat does.
E.g, run a build with MSBuild for the solution in the current folder:
powershell -f _invoke-build.ps1 -buildCommand "msbuild ."
Copy VS' INCLUDE environment variable into clipboard (for fun):
powershell -f _invoke-build.ps1 -buildCommand "set INCLUDE | clip"
The current version of the script:
<#
.Synopsis
Find Visual Studio and run its VsDevCmd.bat, then run a build command.
.Example
powershell -f _invoke-build.ps1 -buildCommand "msbuild ."
.Link
https://gist.github.com/noseratio/843bb4d9c410c42081fac9d8b7a33b5e
#>
#Requires -Version 5.0
# by #noseratio
param([Parameter(Mandatory = $true)] [string] $buildCommand)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
echo "Building via '$buildCommand' ..."
$vs_not_found = "Visual Studio hasn't been detected!"
$vswhere = "${env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe"
if (!(Test-Path $vswhere)) { throw $vs_not_found }
$vs_ide_path = $(& $vswhere -format value -property productPath)
if (!(Test-Path $vs_ide_path)) { throw $vs_not_found }
$vs_ide_folder = "$(Split-Path $vs_ide_path)"
$vs_dev_cmd = "$vs_ide_folder/../Tools/VsDevCmd.bat"
$invoke = "call ""$vs_dev_cmd"" && cd ""$pwd"" && $buildCommand"
# we need to run via CMD for proper propagation of Visual Studio environment vars
& $env:comspec /s /c ""$invoke""
if (!$?) { throw "Failed with error code: $LastExitCode" }
SO I have Community edition 2017, for me path is:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\
Other solutions on this thread throwed me an exception.
In a TFS2013 build, in the Post-build path of the build definition, I call a PowerShell script which need to get the path of the Solution built just before.
But the SolutionPath varibale is not provided in the Environment Variables and we can't pass this variable in the Post-Build Arguments in the Definition Build.
Have you a tip for that without modify the build template ?
Thank you.
The reason there isn't a SolutionPath variable in the tfbuild , in my opinion, is because there aren't any obligation you will only build one solution in your build process.
In order to run the script after the solution done with the build you could add an After..sln.targets and specify a task to do as you please (powershell task inside msbuild)
http://sedodream.com/2010/10/22/MSBuildExtendingTheSolutionBuild.aspx
MSBuild - Project-specific targets for solution does not work
Another option is: in case you want the relative path under your root folder, you could just use the $env:TF_BUILD_SOURCESDIRECTORY inside your script, which will lead you to the \src folder of your build
Get-ChildItem $env:TF_BUILD_SOURCESDIRECTORY | Where-Object FullName -Like "*.sln" | foreach {$_.FullName}
I'm currently using the Jenkins "Version Number Plug-In" to set an Environment Variable for build version. This works fine within jenkins but i need a way to pass this to MSBuild so the Version Number of the exe's and dll's are updated. I tried the config bellow but it does not update the builds version
Environment Variable Name should be written without the percent-marks ('%'), like this:
VERSION
apart from that, I think you are good to go.
May also consider changing
${BUILDS_ALL_TIME}
to
${BUILDS_ALL_TIME, XX}
(this will enforce a two-digit build-number with leading zeros)
This is a MSBuild target that replaces the revision number in the file GlobalAssemblyInfo.cs with the SVN_REVISION variable from Jenkins.
We have a multi project solution where each project references the same GlobalAssemblyInfo for common information (like version). Modify this so it fits your setup.
By having the Exists condition the target executes on developer machines where the MSBuildCommunityTasks is not installed.
In those cases the version number in GlobalAssemblyInfo.cs is left untouched.
<Target Name="InjectSVNRevision" Condition="Exists('$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets')">
<!-- When this build is done by the Jenkins CI server; the svn revision number is set in the environment variable SVN_REVISION
This little snippet replaces the revision number in GlobalAssemblyInfo.cs with this svn revision number
-->
<Message Text="Injecting SVN revision number in GlobalAssemblyInfo.cs" />
<FileUpdate Files="GlobalAssemblyInfo.cs"
Multiline="true"
Singleline="false"
Regex="(AssemblyVersion|AssemblyFileVersionAttribute|AssemblyFileVersion)\("([0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)?"\)"
ReplacementText="$1("$2.$(SVN_REVISION)")"
Condition="$(SVN_REVISION) != '' "/>
</Target>
The easiest way I´ve found to do this is using the Change Assembly Version. Using that plugin you only need to provide the version number (or the environment variable used) and it will replace it in all the AssemblyInfo.cs files in your workspace.
A very quick work around to this is to create a powershell task before your msbuild task. This is assuming you defined VERSION variable as above (without the %%). And you checked delete workspace before build starts.
This is the code for the powershell task.
gci -rec -Filter *AssemblyInfo* | ForEach { (Get-Content $_.FullName) | ForEach-Object {$_ -replace "1.0.0.0",$env:VERSION } | Set-Content $_.FullName}
Here is my summary what was necessary to make it work:
Install this Plugins:
https://plugins.jenkins.io/versionnumber/
https://plugins.jenkins.io/change-assembly-version-plugin/
make sure your AssemblyInfo.cs contains:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
Set your version variable name without '$'.
Define a nice version number like: '1.1.${BUILD_NUMBER}.${SVN_REVISION}'
Add the 'Change Assembly Version'-Build Step.
Enter your variable inside '${...}' example: '${VERSION}'
If you still get build error. Check you project file and AssemblyInfo.cs for merge conflicts.