I'm building a visual studio solution with msbuild
msbuild.exe my.sln
This way it outputs everything to the output paths specified in each project (bin\ by default), but in this case I need all the output artefacts to be in different folder, used for packaging. If I run
msbuild.exe my.sln /p:OutputhPath=<someFolder>
Then all the artifacts will end up in the specified folder, but the structure will be flat. What I would like it to be, is:
\package
\project1
\project2
...
But I can't think of a good way to do this, without modifying individual project files (which is almost out of question). Any ideas? (msbuild 4.0, VS2010 - if that changes anything)
There is probably a better way, but one thing you could do is build in place with msbuild.exe my.sln, and then copy the outputs to your \package dir so you keep the hierarchy. It should be pretty simple to do. You can use this as a starting point:
<Target Name="Package">
<PropertyGroup>
<SourceFolder>$(MSBuildProjectDirectory)\src</SourceFolder>
<TargetFolder>$(MSBuildProjectDirectory)\package</TargetFolder>
</PropertyGroup>
<ItemGroup>
<FilesToCopy Include="$(SourceFolder)\**\bin\Debug\**\*.*" />
</ItemGroup>
<!-- Recursive copy w/o flattening folder structure: -->
<Copy
SourceFiles="#(FilesToCopy)"
DestinationFiles="#(FilesToCopy->'$(TargetFolder)\%(RecursiveDir)%(Filename)%(Extension)')"
/>
</Target>
You can also define a property to keep track of your build configuration, and replace the hardcoded bin\Debug with bin\$(BuildConfig).
Related
I have a file named Common.targets defined like so:
<Project>
<PropertyGroup>
<TlbExpPath>"c:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\x64\tlbexp"</TlbExpPath>
<TlbOutPath>"$(OutDir)..\TLB\$(TargetName).tlb"</TlbOutPath>
</PropertyGroup>
<Target Name="TlbExp" AfterTargets="CopyFilesToOutputDirectory" Inputs="$(TargetPath)" Outputs="$(TlbOutPath)">
<Exec Command='$(TlbExpPath) "$(TargetPath)" /nologo /win64 /out:$(TlbOutPath) /verbose' />
</Target>
</Project>
When I inspect the output of the TlbOutPath property, it looks like:
"..\TLB\.tlb"
Apparently, $(OutDir) and $(TargetName) produce nothing when used within a PropertyGroup. I'm not sure why. How can I make these paths/values reusable while still having access to built-in properties when they are defined?
I'm using MSBuild that comes bundled with Visual Studio 2019. I add an Import element to my actual .csproj projects to include this target where I need it. The csproj projects use the SDK format for the projects, e.g. <Project Sdk="Microsoft.NET.Sdk">.
Here is an example of what the import looks like:
<Project Sdk="Microsoft.NET.Sdk">
<!-- etc -->
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<!-- etc -->
</ItemGroup>
<Import Project="$(RepositoryRoot)\Common.targets" />
</Project>
MSBuild: How may I access built-in properties while defining my own
custom properties?
This is quite an issue in the new sdk format project. I have tested it and got the same issue as you said which quite bother me a lot. Like $(OutDir),$(TargetName),$(OutputPath),$(TargetPath) and some other common system properties cannot be used in a new property while $(Configuration) and $(AssemblyName) works well.
And not only us but also someone else also face the same issue about it.See this thread.
For the traditional old csproj format project, there was no problem with these properties being used this way, but in the new SDK format project, it is impossible to assign some common properties such as $(OutDir),$(TargetName) and $(TargetPath) to a new property. As we know, most of the common properties are defined in the Microsoft.Common.props file(old csproj format) which is quite different from the new sdk format project which does not have such file.
In order to get an answer,l have reported this issue to DC Forum. See this.You can enter this link and add any detailed comments to describe this issue. And anyone who interested in this issue will also vote it so that it will get more Microsoft staff's attention. All these efforts will speed up and get the final answer.
This process may take a while or you could try my suggestion.
Suggestion
1) You can customize this property $(OutDir) in Common.targets file, and use $(TargetFramework) instead of $(TargetName) since $(TargetFramework) is defined in the xxxx.csproj file.
<Project>
<PropertyGroup>
<OutDir>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName)\</OutDir>
<TlbExpPath>"c:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\x64\tlbexp"</TlbExpPath>
<TlbOutPath>"$(OutDir)..\TLB\$(TargetName).tlb"</TlbOutPath>
<TargetPath>xxxx\xxxx.dll(exe)</TargetPath>--------the absolute path of the output file
</PropertyGroup>
<Target Name="TlbExp" AfterTargets="CopyFilesToOutputDirectory" Inputs="$(TargetPath)" Outputs="$(TlbOutPath)">
<Exec Command='$(TlbExpPath) "$(TargetPath)" /nologo /win64 /out:$(TlbOutPath) /verbose' />
</Target>
</Project>
2) use Directory.Build.targets file rather than a custom targets file.
A) You should add a file named Directory.Build.targets(it must be named this and have its own rule to be imported into xxx.csproj) under the project folder.
B) add the content of Common.targets into it without any changes and then build your project directly. The Directory.Build.targets will be imported into your project automatically while build.
This function works well and will not lose any properties. However, l stil bother why it works.
Conclusion
I think #2 is more suitable and easier for you to achieve your goal.
There are some files residing in other directories that, I would like to copy to project folder automatically before build and publishing.
After some research and experimentation, I have come up with the following .csproj file.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeFrameworkVersion>2.1.4</RuntimeFrameworkVersion>
<TieredCompilation>true</TieredCompilation>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<ItemGroup>
<APIDefinition Include="D:\SomePlace\*.API.*.yaml" />
</ItemGroup>
<Target Name="CopyFiles" BeforeTargets="Compile;Build;Publish">
<Copy SourceFiles="#(APIDefinition)" DestinationFolder="wwwroot" />
<Copy SourceFiles="D:\SomePlaceElse\BaseAPISettings.json" DestinationFolder="$(MSBuildProjectDirectory)" />
</Target>
<ItemGroup>
<Compile Remove="wwwroot\**\*;node_modules;bower_components" />
<None Update="**.user;**.vspscc">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>
</Project>
Here I have defined CopyFiles target, which should be run before the targets I've placed there. This target uses Copy task to copy YAML format API definition files and base API settings to the project directory.
This works well during build, publish etc. Also, if I delete the local file in the IDE, it instantly recopies it from the source.
Sometimes I make changes to these files between debugging sessions. Then, when I start debugging from Visual Studio, since the project files aren't changed, obviously the already built project is run.
Since the project is not built, my copy tasks are not triggered, and I end up with stale files during debuging.
Is there anything I can do to have my Copy tasks triggered, when I do "Start Debugging F5" in the IDE, regardless of the project build state ?
P.S. : I'm using Visual Studio 2017 15.8.5 and targeting .NET Core 2.1.4 runtime, if it makes any difference.
To integrate fully into the up-to-date check of the project system inside Visual Studio, I susggest the following changes:
Make the items' source and target paths known before
Register them to the up-to-date check system. (Also needs a hack to make sure the project source code is recompiled so that the output will have a newer time stamp)
Make the MSBuild target itself incremental. This also helps for command-line builds when the files don't have to be copied.
The complete changes look like this:
<ItemGroup>
<CustomCopyFile Include="..\TestFiles\*.API.*.yaml"
TargetPath="wwwroot\%(Filename)%(Extension)" />
<CustomCopyFile Include="..\TestFiles\BaseAPISettings.json"
TargetPath="%(Filename)%(Extension)" />
<UpToDateCheckInput Include="#(CustomCopyFile)" />
<UpToDateCheckBuild Include="#(CustomCopyFile->'%(TargetPath)')"
Original="#(CustomCopyFile)" />
<CustomAdditionalCompileInputs Include="#(CustomCopyFile->'%(TargetPath)')" />
</ItemGroup>
<Target Name="CopyFiles"
BeforeTargets="BeforeBuild;BeforePublish"
Inputs="#(CustomCopyFile)"
Outputs="#(CustomCopyFile->'%(TargetPath)')">
<Copy SourceFiles="#(CustomCopyFile)"
DestinationFiles="#(CustomCopyFile->'%(TargetPath)')" />
</Target>
CustomCopyFile now collects all the source files and we put the expected destination file name into the TargetPath metadata.
UpToDateCheckInput items tell Visual Studio to rebuild the project if one of these items change.
UpToDateCheckBuild items instruct Visual Studio to only check these items against special source items. This is redundant for this example project but may be helpful if the target path wasn't inside the project directory but some intermediate output (obj..) folder and no re-evaluation would see these new files. It would also be helpful if the files were also modified as part of processing (e.g. replacing variables inside the files).
CustomAdditionalCompileInputs is a hack here since the items are copied to the project folder and are considered to be "inputs to the output" automatically.. So we force the project to recompile if our source files change. If we don't do so, it would never consider the project up-to-date after a change to the source yaml files since they would be newer than the compiled app.dll file.
Solution1.sln contains two projects:
ProjectA.csproj
ProjectB.csproj
ProjectB has a custom target called "Foo". I want to run:
msbuild Solution1.sln /t:Foo
This will fail because ProjectA doesn't define the "Foo" target.
Is there a way to make the solution ignore the missing target? (E.g., do nothing if the target doesn't exist for a specific project) without modifying the SLN or project files?
There is a two-part solution if you don't want to edit the solution or project files and you're happy for it to work from MSBuild command-line but not from Visual Studio.
Firstly, the error you get when you run:
MSBuild Solution1.sln /t:Foo
Is not that ProjectA does not contain a Foo target but that the solution itself does not contain a Foo target. As #Jaykul suggests, setting the MSBuildEmitSolution environment variable will reveal the default targets contained within the solution metaproj.
Using the metaproj as inspiration you can introduce a new file "before.Solution1.sln.targets" next to the solution file (the file name pattern is important) with contents like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo">
<MSBuild Projects="#(ProjectReference)" Targets="Foo" BuildInParallel="True" Properties="CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)" />
</Target>
</Project>
The MSBuild element is mostly just copied from the solution metaproj's Publish target. Adjust the target name and any other details to suit your scenario.
With this file in place, you'll now get the error that ProjectA does not contain the Foo target. ProjectB may or may not build anyway depending on inter-project dependencies.
So, secondly, to solve this problem we need to give every project an empty Foo target which is then overridden in projects that actually already contain one.
We do this by introducing another file, eg "EmptyFoo.targets" (name not important) that looks like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo" />
</Project>
And then we get every project to automatically import this targets file either by running MSBuild with an extra property, eg:
MSBuild Solution1.sln /t:Foo /p:CustomBeforeMicrosoftCommonTargets=c:\full_path_to\EmptyFoo.targets
Or include the CustomerBeforeMicrosoftCommonTargets property in the Properties attribute on the MSBuild element in the first targets file where you could optionally specify the full path relative to the $(SolutionDir) property.
However, if you're willing to run Foo in conjunction with any of the default solution targets (ie Build, Rebuild, Clean, or Publish) you could take some inspiration for how the Web Publishing Pipeline in MSBuild uses the DeployOnBuild property to call the Publish target on Web projects in a solution containing other project types that don't support publishing.
More info on the before.Solution1.sln.targets file here:
http://sedodream.com/2010/10/22/MSBuildExtendingTheSolutionBuild.aspx
You can target those by project name, like /t:project:target (might need quotes, I can't remember).
You can find all the generated targets by setting the environment variable MSBuildEmitSolution = 1 ... which causes msbuild to save to disk the temp .metaproj file which it generates for your solution. That file has all those targets defined in it, just open it up and take a look ;)
Maybe not the best answer but a reasonable hack.
msbuild ProjectA.csproj
msbuild ProjectB.csproj /t:Foo
When msbuild building solution - msbuild emits only limited set of targets into it's .metaproj file, and afaik - you can't build custom target through building sln file, you have to use original project1.csproj or custom build script.
Just for reference:
Use ContinueOnError when using MSBuildTask or -p:ContinueOnError=ErrorAndContinue when using (dotnet) msbuild
It may be in limited scenarios helpful: For example you have a list of .csproj files and want attach metadata only to specific project file items then you could write something like this:
<Target Name="UniqueTargetName" Condition="'$(PackAsExecutable)' == 'Package' Or '$(PackAsExecutable)' == 'Publish'" Outputs="#(_Hello)">
<ItemGroup>
<_Hello Include="$(MSBuildProjectFullPath)" />
</ItemGroup>
</Target>
<Target Name="BuildEachTargetFramework" DependsOnTargets="_GetTargetFrameworksOutput;AssignProjectConfiguration;_SplitProjectReferencesByFileExistence"
Condition="$(ExecutableProjectFullPath) != ''">
<Message Text="[$(MSBuildThisFilename)] Target BuildEachTargetFramework %(_MSBuildProjectReferenceExistent.Identity)" Importance="high" />
<MSBuild
Projects="%(ProjectReferenceWithConfiguration.Identity)"
Targets="UniqueTargetName"
ContinueOnError="true">
<Output TaskParameter="TargetOutputs" ItemName="_Hallo2" />
</MSBuild>
<Message Text="[$(MSBuildThisFilename)] ########### HELLO %(_Hallo2.Identity)" Importance="high" />
</Target>
I have an MVC application which I have got working on Azure apart from getting the published .cspkg file to include css/jscript that is created in an afterbuild process (this works if I publish to a normal server which isn't using Azure).
In the afterbuild process I minify and merge files then add them to a deploy zip:
<PackageLocation>..\Deploy\Website.zip</PackageLocation>
<PropertyGroup>
<CopyAllFilesToSingleFolderForPackageDependsOn>
CustomCollectFiles;
$(CopyAllFilesToSingleFolderForPackageDependsOn);
</CopyAllFilesToSingleFolderForPackageDependsOn>
</PropertyGroup>
What MSBuild code do I need to change in order to do the same task but adding to the cspkg instead?
Here is how I just did it. In this example I have a .csproj file that is part of an Azure solution and the dll produced by my C# project needs a particular Xml file to live right next to it in the deployment. Here are some msbuild fragments from my .csproj file that show the technique. You can place all of this code below the import of Microsoft.CSharp.targets in your .csproj file.
<!-- Identify the Xml input file that must be deployed next to our dll. -->
<ItemGroup>
<SpecialXmlFileItem Include="c:\temp\MySpecialFile.xml" />
</ItemGroup>
<PropertyGroup>
<!-- In my case I needed the as-deployed Xml filename to be fixed and yet I wanted it to be possible
to provide any filename at all to be provided as the source. Here we are defining the fixed,
as-deployed filename. -->
<AsDeployedXmlFilename>MyServiceStorageConfig.xml</AsDeployedXmlFilename>
<!-- Wire our own AddFilesToProjectDeployment target into the GetCopyToOutputDirectoryItems
target. That target is evaluated not only as part of normal .csproj evaluation, but also as part
of .ccproj evaluation. It is how the .ccproj manages to interrogate your dll producing projects
about all of the project files that need to be packaged. -->
<GetCopyToOutputDirectoryItemsDependsOn>
AddFilesToProjectDeployment;
$(GetCopyToOutputDirectoryItemsDependsOn)
</GetCopyToOutputDirectoryItemsDependsOn>
</PropertyGroup>
<Target Name="AddFilesToProjectDeployment">
<Error Condition="!Exists('#(SpecialXmlFileItem)')"
Text="The all important and very special XML file is not found: %(SpecialXmlFileItem.ItemSpec)" />
<ItemGroup>
<ContentWithTargetPath Include="#(SpecialXmlFileItem->'%(FullPath)')">
<!-- In my case I wanted to deploy my xml file right next to my .dll, so I included no relative
path information in the below value of TargetPath, just the simple filename. But, I think if you
included relative path information in the below value that it would be preserved in the deployment. -->
<TargetPath>$(AsDeployedXmlFilename)</TargetPath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</ContentWithTargetPath>
</ItemGroup>
</Target>
-Bern McCarty
I think this is just a question of timing... make sure the files get combined, minified, and placed into build before the publishing (packaging) step happens.
Sorry I don't have more details; I've never tried to do this sort of thing.
I'm trying to make a batch file to publish the few ClickOnce application we have in one click. I'm using msbuild for that, and as an example the below command line shows how I'm doing it:
msbuild
MyApp.sln
/t:Publish
/p:Configuration=Release
/p:PublishUrl="C:\Apps\"
/v:normal > Log.txt
(wrapped for easier reading)
when I run the above command it builds and publish the application in the release directory, i.e. bin\release! Any idea why msbuild doesn't respect PublishUrl property in my example above?
PS: I tried also different combinations including remove 'Configuration', use 'Rebuild' and 'PublishOnly' as targets, and remove the the quotation marks but without any success.
You are setting the wrong property. Try PublishDir instead.
You can pass it into MSBuild as you are or you can set it in the project file (or maybe the sln file too, not sure I always use the project file.) like this
<PropertyGroup>
<PublishDir>C:\Dev\Release\$(BuildEnvironment)\</PublishDir>
</PropertyGroup>
I've just done a few blog posts on MsBuild and ClickOnce stuff, check it out you 'should' find them useful...
Some features are done by Visual-Studio and not by the MSBuild-script. So the click-once-deployment behaves differently when it's executed from the command-line.
The ApplicationRevision isn't increased with every build. This works only when is exectued from Visual Studio
In in somecases, the PublishUrl isn't used. Quote from MSDN:
For example, you could set the PublishURL to an FTP path and set the InstallURL to a Web URL. In this case, the PublishURL is only used in the IDE to transfer the files, but not used in the command-line builds. Finally, you can use UpdateUrl if you want to publish a ClickOnce application that updates itself from a separate location from which it is installed.
I've created a special MSBuild-file which does this things. It runs the publish-target and copies then the files to the right location.
An example of the build-file, as requested by alhambraeidos. It basically runs the regular VisualStudio-build and then copies the click-once data to the real release folder. Note that removed some project-specific stuff, so it's maybe broken. Furthermore it doesn't increase the build-number. Thats done by our Continues-Build-Server:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Publish" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- the folder of the project to build -->
<ProjLocation>..\YourProjectFolder</ProjLocation>
<ProjLocationReleaseDir>$(ProjLocation)\bin\Release</ProjLocationReleaseDir>
<ProjPublishLocation>$(ProjLocationReleaseDir)\app.publish</ProjPublishLocation>
<!-- This is the web-folder, which provides the artefacts for click-once. After this
build the project is actually deployed on the server -->
<DeploymentFolder>D:\server\releases\</DeploymentFolder>
</PropertyGroup>
<Target Name="Publish" DependsOnTargets="Clean">
<Message Text="Publish-Build started for build no $(ApplicationRevision)" />
<MSBuild Projects="$(ProjLocation)/YourProject.csproj" Properties="Configuration=Release" Targets="Publish"/>
<ItemGroup>
<SchoolPlannerSetupFiles Include="$(ProjPublishLocation)\*.*"/>
<SchoolPlannerUpdateFiles Include="$(ProjPublishLocation)\Application Files\**\*.*"/>
</ItemGroup>
<Copy
SourceFiles="#(SchoolPlannerSetupFiles)"
DestinationFolder="$(DeploymentFolder)\"
/>
<Copy
SourceFiles="#(SchoolPlannerUpdateFiles)"
DestinationFolder="$(DeploymentFolder)\Application Files\%(RecursiveDir)"
/>
<CallTarget Targets="RestoreLog"/>
</Target>
<Target Name="Clean">
<Message Text="Clean project:" />
<MSBuild Projects="$(ProjLocation)/YourProject.csproj" Properties="Configuration=Release" Targets="Clean"/>
</Target>
</Project>
I'll put in my 2 cents, this syntax seems to work (right or wrong):
/p:publishUrl="C:\\_\\Projects\\Samples\\artifacts\\Web\\"
For me, the soultion was to escape the path.
Instead of:
/p:PublishUrl="C:\Apps\"
Put:
/p:PublishUrl="C:\\Apps\\"