Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get nuget (powershell) to insert <DependentUpon> elements in the target csproj file

I'm writing a nuget package for a windows service, the idea is that my colleagues can create a windows service install the package and all of the default logging settings, entlib libraries and other house keep tasks will be set up for them.

I've gotten pretty much everything to work except for one thing which is driving me nuts.

In the content folder of my nuget directory I have Service.cs and Service.Designer.cs these are added to the target csproj but they are not being related.

When I look at the csproj file I see:

<Compile Include="Service.cs">
  <SubType>Component</SubType>
</Compile>
<Compile Include="Service.Designer.cs" />

But I want to see:

<Compile Include="Service.cs">
  <SubType>Component</SubType>
</Compile>
<Compile Include="Service.Designer.cs">
  <DependentUpon>Service.cs</DependentUpon>
</Compile>

Any ideas, pretty sure it will involve the install.ps script but my powershell skills are non existant?

As an aside, can nuget be used to delete/overwrite files? So far it just seems to skip.

like image 545
CianM Avatar asked May 31 '12 23:05

CianM


3 Answers

Well after around 2 days of powershell and msbuild hell I finally got a working solution. See below. A word of warning calling project.Save() is crucial - without this you will get a "conflicting file modification detected" warning. If you call project.Save() first you will be merely asked to reload the solution once you are done.

param($installPath, $toolsPath, $package, $project)

#save the project file first - this commits the changes made by nuget before this     script runs.
$project.Save()

#Load the csproj file into an xml object
$xml = [XML] (gc $project.FullName)

#grab the namespace from the project element so your xpath works.
$nsmgr = New-Object System.Xml.XmlNamespaceManager -ArgumentList $xml.NameTable
$nsmgr.AddNamespace('a',$xml.Project.GetAttribute("xmlns"))

#link the service designer to the service.cs
$node = $xml.Project.SelectSingleNode("//a:Compile[@Include='Service.Designer.cs']", $nsmgr)
$depUpon = $xml.CreateElement("DependentUpon", $xml.Project.GetAttribute("xmlns"))
$depUpon.InnerXml = "Service.cs"
$node.AppendChild($depUpon)

#link the settings file to the settings.designer.cs 
$settings = $xml.Project.SelectSingleNode("//a:None[@Include='ServiceSettings.settings']", $nsmgr)
$generator = $xml.CreateElement("Generator", $xml.Project.GetAttribute("xmlns"))
$generator.InnerXml = "SettingsSingleFileGenerator"
$settings.AppendChild($generator)

$LastGenOutput = $xml.CreateElement("LastGenOutput", $xml.Project.GetAttribute("xmlns"))
$LastGenOutput.InnerXml = "ServiceSettings.Designer.cs"
$settings.AppendChild($LastGenOutput)

#set the settings designer to be autogen
$settingsDesigner = $xml.Project.SelectSingleNode("//a:Compile[@Include='ServiceSettings.Designer.cs']", $nsmgr)
$autoGen = $xml.CreateElement("AutoGen", $xml.Project.GetAttribute("xmlns"))
$autoGen.InnerXml = "True"

$DesignTimeSharedInput = $xml.CreateElement("DesignTimeSharedInput", $xml.Project.GetAttribute("xmlns"))
$DesignTimeSharedInput.InnerXml = "True"

$AGDependentUpon = $xml.CreateElement("DependentUpon", $xml.Project.GetAttribute("xmlns"))
$AGDependentUpon.InnerXml = "ServiceSettings.settings"

$settingsDesigner.AppendChild($autoGen)
$settingsDesigner.AppendChild($DesignTimeSharedInput)
$settingsDesigner.AppendChild($AGDependentUpon)

#fix up the project installer.    
$projectInstallerRes = $xml.Project.SelectSingleNode("//a:EmbeddedResource[@Include='ProjectInstaller.resx']", $nsmgr)
$projectInstallerResDepUpon = $xml.CreateElement("DependentUpon", $xml.Project.GetAttribute("xmlns"))
$projectInstallerResDepUpon.InnerXml = "ProjectInstaller.cs"
$projectInstallerRes.AppendChild($projectInstallerResDepUpon)

$projectInstallerDesigner = $xml.Project.SelectSingleNode("//a:Compile[@Include='ProjectInstaller.Designer.cs']", $nsmgr)
$projectInstallerDesignerDepUpon = $xml.CreateElement("DependentUpon", $xml.Project.GetAttribute("xmlns"))
$projectInstallerDesignerDepUpon.InnerXml = "ProjectInstaller.cs"
$projectInstallerDesigner.AppendChild($projectInstallerDesignerDepUpon)

#delete the bundled program.cs file.
$prog = $xml.Project.SelectSingleNode("//a:Compile[@Include='Program.cs']", $nsmgr)
$prog.SelectSingleNode("..").RemoveChild($prog)

#delete the bundled service1 file
$oldServiceFile = $xml.Project.SelectSingleNode("//a:Compile[@Include='Service1.cs']", $nsmgr)
$oldServiceFile.SelectSingleNode("..").RemoveChild($oldServiceFile)

$oldServiceDesignerFile = $xml.Project.SelectSingleNode("//a:Compile[@Include='Service1.Designer.cs']", $nsmgr)
$oldServiceDesignerFile.SelectSingleNode("..").RemoveChild($oldServiceDesignerFile)

#save the changes.
$xml.Save($project.FullName)

Shameless self plugging: I'll do a full write up of the solution and issues I encountered on my blog cianm.com Hope this saves someone out there some time.

like image 59
CianM Avatar answered Nov 09 '22 22:11

CianM


I think an easier way to do this (and possibly more supported) is through the $project variable that points to the DTE project.

Note: I'm coming into this a year later, but facing a similar problem. I found this answer and started using it, but I had trouble with the Save call at the end of the PowerShell script - if I had one package getting installed that depended on the package with the PowerShell script, the Save call "broke things" so the other packages couldn't properly install. Anyway, I'm now using NuGet 2.5, but DTE hasn't changed with any significance since VS 2003 so this should work even in the older NuGet you were using.

param($installPath, $toolsPath, $package, $project)

# Selections of items in the project are done with Where-Object rather than
# direct access into the ProjectItems collection because if the object is
# moved or doesn't exist then Where-Object will give us a null response rather
# than the error that DTE will give us.

# The Service.cs will show with a sub-item if it's already got the designer
# file set. In the package upgrade scenario, you don't want to re-set all
# this, so skip it if it's set.
$service = $project.ProjectItems | Where-Object { $_.Properties.Item("Filename").Value -eq "Service.cs" -and  $_.ProjectItems.Count -eq 0 }

if($service -eq $null)
{
    # Upgrade scenario - user has moved/removed the Service.cs
    # or it already has the sub-items set.
    return
}

$designer = $project.ProjectItems | Where-Object { $_.Properties.Item("Filename").Value -eq "Service.Designer.cs" }

if($designer -eq $null)
{
    # Upgrade scenario - user has moved/removed the Service.Desginer.cs.
    return
}

# Here's where you set the designer to be a dependent file of
# the primary code file.
$service.ProjectItems.AddFromFile($designer.Properties.Item("FullPath").Value)

Hopefully that helps folks in the future looking to accomplish something similar. Doing it this way, you avoid the problem with dependent package installs failing because the project is getting saved in the middle of package install.

like image 40
Travis Illig Avatar answered Nov 09 '22 20:11

Travis Illig


Here's a short example using Build.Evaluation that is part of Visual Studio 2010+

$buildProject = @([Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection.GetLoadedProjects($project.FullName))[0]


$toEdit = $buildProject.Xml.Items | where-object { $_.Include -eq "Service.Designer.cs" }
$toEdit.AddMetaData("DependentUpon", "Service.cs")
# ... for the other files

$project.Save()
like image 29
Jay Walker Avatar answered Nov 09 '22 21:11

Jay Walker