Tips, Tools

Use ProGet to Host Your Private Packages

I’ve been using private NuGet packages for a while. The easiest way to get up and running with private packages is to drop them in a folder and setup the NuGet Package Manager to use that folder as a package feed. Like many simple solutions, this works great until you outgrow it. I create NuGet packages whenever I build in release mode, and the package-build automation copies the packages to the feed folder. I use Windows Live Mesh to keep the folder in sync across development workstations.

The drawback with this approach is that the relative path to the feed folder must be consistent (I’m too lazy to specify a custom path in each MSBuild file). This means that I’m not free to put my projects anywhere I would like. For example, GitHub for Windows clones into Documents\GitHub\<project> by default, this won’t work with my hacked together system. Also, Visual Studio 2012 is on the horizon, and by default it will create its own Documents\Visual Studio 2012\Projects folder, this is also incompatible with the relative path I use to find my feed folder during build.

What I need is some kind of “universal” way to “locate” a “resource”. Hmm, where have I heard those words before? Oh yes, the internet is based on this idea. The NuGet documentation explains how to set up an ASP.NET MVC site as a “read-only” remote feed. This is read-only from the perspective of the web in that there is no way to push new packages to the site over HTTP. You can update the packages by placing them into the site’s Packages folder. Essentially, this simple remote feed takes the concept of the local feed and wraps a web server around it. This option is not half bad, I could store the packages on my build server and configure my build automation to use a UNC path pointing at that folder, but it would be really sweet to have a more complete nuget.org style experience. There are a couple ways to do this.  Inspired by Damian Hickey’s post on his NuGet workflow, I decided today was the day to get it going.

Damian has a lot of cool tools in his pipeline, but today I’m going focus on one part and look at a package called ProGet by Inedo

Download the Bits

Here’s the ProGet overview page. You can read a little about the software and download. They have a free (beer) version as well as enterprisey offerings. The download page has a dead link to an installation guide (as of this writing anyway, hopefully that gets fixed soon.) The installation package comes in two flavors, a 60MB version that comes with SQL Server Express, and a 4MB version without the database. Since I already have a database available, I downloaded the smaller package, unblocked it, and copied it over the the build server.

Capture

Install the Bits

After starting the installer, I accept the EULA (which is surprisingly short and simple), then select an edition to install.

Capture1

I choose “ProGet Free” and the installer asks for my email address. I give it my junk mail address, and wait a moment while the installer talks to the mothership. Next, I accept the default installation directory, then its on to database configuration.

Capture2

I click “Test” but the connection fails. Although there is definitely a server installed, I’m not even sure if its running, so now its time to do some SQL Server troubleshooting.  I open the SQL Server Configuration Manager and see that the server is running and that TCP/IP connections are allowed. But I notice that its actually a SQL Express server, and that the ProGet installer is trying to connect to a (default) instance. I update the instance to SQLExpress and the connectivity test passes.

Capture3

Next, the installer asks whether I would like to use its integrated web server, or use IIS. Since I already have IIS up and hosting CruiseControl.NET’s web dashboard, I choose to use IIS. I can also choose the port to host ProGet on, but I leave the default 81.

Capture4

Looks like that’s all the installer wants to know, so I click the Install button. Installation takes about 2 minutes.

Capture5

I click “Launch ProGet” and the ProGet site launches in IE.

Capture6

Adding Packages

I think there’s no point in clicking the giant “Browse Packages” button since I don’t have any yet.  Later on I learn that ProGet will act as a type of proxy for NuGet.org, and would have shown me a bunch of remote packages if I had clicked there.  But I was more interested in adding my own packages anyway, so I clicked on “Add Package”. The site wants to authenticate, but also warns me that I haven’t changed the password for the default Admin user.

Capture7

I like how they say that they “took the liberty” of creating Admin for me.  It gives a nice “this is not your fault” feeling to the text.  I login with the default credentials, then click the “Administration” tab, then “Users and Groups” and finally “Edit” on the Admin user. I change Admin’s password and click “Save User”. Then I create a second administrator using my own name.

Update 2012/7/11: Now that you have created an admin, stay on the Administration tab, then go to “Licensing and Activation”.  Click the button that says “Activate ProGet”.  It’s still free, but if you don’t do this step then your server will stop working in about a week.

Back to the “Add Package” tab. I have the option of Uploading an existing package, using NuGet.exe to push to the repository (sweet), create a package right on the server, or pulling from a remote feed. I’ll try all of these except for “Create New Package”.

Capture8

Pull From Another Repository

Capture9

Just as an experiment, I’ll pull CompositionTests down from nuget.org. I fill out the requested information and check the “Download Dependencies” option. For “Feed URL:” I presume they mean the over all feed, not the specific path to CompositionTests and I copy the URL out of Visual Studio. After clicking “Install Package” I browse over to the “Packages” tab.

Capture10

The first three packages are CompositionTests and its dependencies. The rest of the packages are on Nuget.org (I presume). I didn’t expect to see remote packages, but the page indicates that what I’m seeing is not just local packages, but also “additional packages from any connectors”. The additional packages have a little plug overlay on their icon, indicating they are available via the connector. I click on “Administration” then “Feeds” and I can see that I do indeed have a connection to nuget.org by default.

Capture11

I can also see the “Feed Path” where ProGet stores the local packages, and we can see CompositionTests and friends stored there.

Capture12

Upload From Disk

Under my old system, my local package feeds are stored on my development workstations. So, I’ll open a browser on one of them and see if I can upload a package from the local feed to the remote feed. I hit a snag trying to login, but it just turns out that I need to allow JavaScript (using the correct password also helps).

Capture13

On the “Add Package” tab, I click “Upload From Disk”. Other than the target feed, all ProGet wants to know is where the file is. I choose one of my private packages: “TemporaryFile” (a little class I use with testing that wraps a temp file in an IDisposable container so that the file is deleted even if an exception is thrown). After I click “Upload File”. We can see that the file is stored on the server.

Capture15

And now appears in the feed.

Capture16

Push via NuGet.exe

After I click on “Push via NuGet Command Line Utility” ProGet displays instructions to push via the command line.

Capture17

Since I haven’t created any additional feeds, my <Feed Name> will be Default. Of course, <package path> will depend on the package I want to upload, but what about [API key]? Browsing around I don’t see it anywhere obvious in ProGet. Pushing without it won’t work, NuGet.exe requires an API key for the server. Using a fake value like “foo” also fails.

I poked around Inedo.com and one of the screenshots clued me in to the fact that the API key in ProGet is associated with the feed, not the user. By default it is blank.

Capture18

The ProGet documentation gives some insight into why the key goes with the feed and not the user, but no guidelines on the format of the key. I head over to www.guidgenerator.com and create a GUID and setup the feed to use it, but I still get a 500 error every time I try to push a package.

After poking around IIS, and eventually busting out Fiddler I still couldn’t get it to work so I used the “Live Help” button to open a chat with Inedo support.

Capture16

They were able to reproduce the problem on their side, and indicated it seemed to be a bug when using ProGet with the latest build of NuGet. They’re working on a fix. I found older versions of NuGet rattling around my hard drive (1.5 and 1.7) and still couldn’t get it to work. So I guess I’m stuck waiting for the update for the time being, that’s no fun.  I decided to go have lunch.

A Few Hours Later…

I get an email on my junk account explaining a feature of Buildmaster--one of Inedo’s other products that I never downloaded. I mistook it for a release announcement because I didn’t really read it, and even though ProGet isn’t mentioned in the email I decided to visit the ProGet download page. Sure enough, ProGet 1.0.6 was released today.

As an aside, it makes me sad that they sent me an email about something I haven’t expressed interest in, while failing to send me an email about something I am interested in. But I can’t fault them too much, that’s just the price you pay for free beer. And if this new release fixes my issue, then I’ll gladly receive their marketing emails in exchange for such good service.  (Not promising to read, just receive.)

Upgrading ProGet

Once again I download the smaller package. This time the installer asks if I want to update.

Capture19

I click the update button and ProGet strongly recommends that I backup the database. I let it do so and it fails. There is a button to submit an error report, so I click it. I fill out a little report and submit the issue.

Capture20

Being impatient, I decide to try again. Now, the installer acts like a new install and offers me the EULA again. Not good. Maybe my old install got borked. Yep, same old installation now. I follow the steps outlined above and wait while ProGet installs. It seems to have fewer steps this time, so I think it recognized that the database already existed. I click “Launch ProGet” at the end and the site launches. I click on “Browse Packages” and all the packages I added previously are still there. So, the install only suffered minor borking.  (As an aside, before I could even finish writing up this post, I got an email from their support asking if I had been able to resolve the issue, which included a suggestion on an alternate way to backup the database.)

Over on the admin tab, the credentials I set up previously still work and the API key I created is still associated with the default feed.

Push via NuGet.exe, Redux

Ok, do over time. Lets see if I can push packages with NuGet now.

I open a command window and navigate to my local package folder. This time I’ll try pushing my “Strings” package (a library of helpers for System.String… c’mon, you know you have one too.)

C:\...\PackageSource>NuGet.exe push Strings.1.0.4360.34765.nupkg API_KEY -Source http://dee:81/nuget/default 
Pushing Strings 1.0.4360.34765 to 'http://dee:81/nuget/default'... Failed to process request.
'You are not authorized to add a package to this feed.'. 
The remote server returned an error: (403) Forbidden.. 

So it still fails, but now it fails with a 403 instead of a 500. Note that replaced the API key with a placeholder. That’s more for the sake of brevity than security. Because I control the key, I can reveal it as long as I’m willing to replace it later.

Thinking I had an idea about fixing the 403 error, I go to “Administration” then “Privileges and Roles”. I create a new Role called “Publisher” and give it the “Feeds_AddPackage” privilege.

Capture21

Then I click “Add Privilege” and I grant “Publisher” to “Anonymous”. Lets see if that works.

C:\...\PackageSource>NuGet.exe push Strings.1.0.4360.34765.nupkg API_KEY -Source http://dee:81/nuget/default 
Pushing Strings 1.0.4360.34765 to 'http://dee:81/nuget/default'... 
Your package was pushed. 

Bam! It worked.  I love it when a plan comes together.

Capture22

Capture23

Final Thoughts

It would have been nice if the official install guide was online. However, it wasn’t to hard to figure out the basics. Even, if it hadn’t been my bad luck to hit a bug on day one, then I still would have needed to figure out how to let anonymous users upload packages. The only reason I was able to figure it out quickly was because I had gotten pretty familiar with the configuration options while debugging 1.0.5.

Lucky for you, you won’t have to deal with that bug, and I hope this guide will make your setup experience smoother than mine was. I’m looking forward to playing with this tool and seeing how I can integrate it with my build system. Up until now, I haven’t configured CruiseControl.net to create NuGet packages because it seemed like a pain when my system was based on local feeds. Also, I haven’t been using Package Restore with the build server since the server didn’t have access to the private packages. With ProGet in the mix, I’m hope to have that up and running soon.

Advertisements
Tips, Troubleshooting

Enable NuGet PackageRestore on CC.NET

Last week I decided to revisit my MSBuild/NuGet patterns (see 1 and 2) and see if I could make any improvements to what I had come up with before.  In particular, I wanted to integrate Brad Wilson’s gist on downloading NuGet at runtime.  I did that for a couple projects, including CompositionTests and it works great.

But last night, NuGet 2.0 came out and CompositionTests stopped building on my CruiseControl.NET server, along with another project I haven’t released yet.  I did what I usually do when something breaks first thing in the morning: I hit “Force Build” and see what happens.  But it still failed and eventually I went searching to see if there were any problems with NuGet 2.0.  Sure enough I found something, but its not a bug, it’s a feature!  NuGet 2.0 requires consent for package restore, because we all just know that the NSA is very interested in what packages we are using.  The NuGet team has been very open about this change and gave fair warning to get ready, which I read and ignored.

Well, I can’t ignore it anymore can I?

Symptoms

First and most obvious is a sad red circle in CCTray:

image

Like I said, normally I don’t even check the log before hitting “Force Build” but I already did that dance, so lets look at the log.  I formatted this to fit your screen, and pulled some paths out for brevity, but you get the idea:

<error file="...\CompositionTests\.nuget\nuget.targets"
        line="57"
        column="9"
        timeStamp="06/19/2012 23:36:45">
  <![CDATA[
    Package restore is disabled by default. To give consent, open the Visual Studio
    Options dialog, click on Package Manager node and check 'Allow NuGet to 
    download missing packages during build.' You can also give consent by setting 
    the environment variable 'EnableNuGetPackageRestore' to 'true'.
  ]]>
</error>
<error code="MSB3073"
        file="...\CompositionTests\.nuget\nuget.targets"
        line="57"
        column="9"
        timeStamp="06/19/2012 23:36:45">
  <![CDATA[
    The command ""...\nuget.exe" install "...\packages.config" -source "" -o 
    "...\packages"" exited with code -1.
  ]]>
</error>

Since we are working with a build server, checking a checkbox in package manager is not an option.  In this scenario, NuGet gives us the another way to provide consent via an environment variable.  Lets try that.

Cure

These steps will apply to Windows Server 2008 R2, because that’s what I’m running CC.NET on.

Set Environment Variable

  • Log on to the build server and open the Control Panel
  • Go to System & Security > System > Advanced System Settings
  • Click “Environment Variables”
  • Click “New…” under “System Variables”
  • Enter “EnableNuGetPackageRestore” as the name and “true” for the value.
  • Click OK > OK > OK.

image

Restart the CruiseControl.NET service

  • Open Control Panel > System & Security > Administrative Tools > Services
  • Select CruiseControl.NET and click “Restart” to restart the service.

image

Test

Use “Force Build” to force a failing project to start in CC.NET.

image

And you’re done!

Coding, Tips

Tip: Modern INotifyPropertyChanged

Photo Credit: Sanne Roemen

I don’t get much chance to work on the desktop or in Silverlight so I haven’t had too many opportunities to work with the INotifyPropertyChanged interface.  While refactoring an old class toward the Single Responsibility Principle today, I thought to myself “I need an event here,” and since the event was related to a property changing I decided to implement INotifyPropertyChanged instead of using a basic event.

Being rusty with INotifyPropertyChanged I used Google to search for any cool tricks people have come up with since the last time I gave this interface any thought.  I think the most cutting edge way to do it is probably with AOP, but there are some other cool tricks too.  Maybe this is old hat, but it’s new to me.

Generic Setter

There are a few posts out there about using generic setters.  Some of them smell bad.  This one smells good to me: INotifyPropertyChanged, the Anders Hejlsberg Way, by Dan Rigby.  Tips from Anders are always nice :).

Here is what the setter looks like:

private void SetProperty<T>(ref T field, T value, string name)
{
    if (!EqualityComparer<T>.Default.Equals(field, value))
    {
        field = value;
        var handler = PropertyChanged;
        if (handler != null)
        {
          handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

The only issue I have with the implementation is that it still requires a magic string.  We’d like to avoid magic strings because they aren’t code, and because they aren’t code they are usually ignored by refactoring tools and can’t be checked by the compiler.

No Magic

Several posts explain how to get rid of the magic string using a lambda expression.  I used this article as reference: Silverlight/WPF: Implementing PropertyChanged with Expression Tree, by Michael Sync.  Michael’s post takes a different approach and just passes the lambda into the setter.  Later he shows some extension methods that can eliminate the need to specify the type parameter explicitly.  But, when I combined his technique with Dan’s I didn’t find the need to specify the type parameter, since the compiler can infer this information from the field and value parameters.

Here’s what we get after combining the two:

private void SetProperty<T>(ref T field, T value, Expression<Func<T>> member)
{
    Contract.Requires(member != null, "member must not be null.");
    var me = member.Body as MemberExpression;
    if (me == null)
    {
        throw new InvalidOperationException("member.Body must be a MemberExpression");
    }

    if (!EqualityComparer<T>.Default.Equals(field, value))
    {
        field = value;
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(me.Member.Name));
        }
    }
}

Here is how we use it:

public int Threshold
{
    get
    {
        return this.threshold;
    }
    private set
    {
        this.SetProperty(ref this.threshold, value, () => this.Threshold);
    }
}

Its no longer refactoring resistant, yay!  I think the error-checking in the setter is a little ugly though.

Cleaner Code with .NET 4.5

I actually found this article first: INotifyPropertyChanged, The .NET 4.5 Way, by Dan Rigby, then followed a link there to the Anders-inspired example.  In the post, Dan shows how to use the CallerMemberNameAttribute (new in .NET 4.5) to eliminate the need for the expression tree.  Without the expression tree, we don’t need the error checking code related to the tree and we can implement a cleaner version of SetProperty.  Be sure to check out Dan’s article as he mentions a couple more useful attributes coming in .NET 4.5.

Tips

Install NuGet Packages from the Cache

Why?

We all love NuGet, but like all cloud services, it is only available when the cloud is available. If you are on the proverbial airplane, you cannot install new packages or update existing packages. In my case, I ride the train back and forth from school twice a week, which is not a bad commute (30 minutes each way.) It is just long enough that sometimes I feel like getting out my laptop and doing some work. Just last week I started a new project on the train and I wanted to install Moq and write a test, and I ran into this snag.

Since I already had a local feed configured, this was easy enough to resolve. I navigated to a project that already had the latest version of Moq installed, grabbed the nupkg from that solution’s packages folder, and dropped it into my local feed. Problem solved. I know from experience that doing this would not cause me any problems. When the Moq team releases the next update to Moq, NuGet will see that version on line and start offering that version instead of the copy in the local feed.

Around the same time, Rory Becker suggested to the world (via Twitter – @RoryBecker) that NuGet should use its cache as a package source. I thought that configuring this manually by hand would be possible, but did not look into it. With the NuGet.org outage today, David Fowler (@davidfowl) suggested the idea. Now everyone knows. Oh well, that will teach me to sit on an idea.

NuGetCache6

How To

Setting up local package sources is easy. The official guide is located here on docs.nuget.org, but you can follow along here to setup this specific scenario.

First, start Visual Studio and open the Package Manager Options:

NuGetCache5

Click on the “Package Sources” item. You can enter whatever makes sense to you as the name. The name “Cache” makes sense to me. You cannot follow David’s tweet too literally because the options page will not accept the %LOCALAPPDATA% variable. If you try, the Package Manager Options will complain with the message: “The source specified is invalid. Please provide a valid source.”

NuGetCache4

Instead, paste the path “%LOCALAPPDATA%\NuGet\Cache” into your Start Menu’s search box and press enter to open the folder:

NuGetCache3

Then copy the full path from the explorer window’s address bar and paste it into your Package Manager options:

NuGetCache2

Click the add button and this time the package manager should accept the new source:

NuGetCache1

That’s it, thanks for reading!

Tips

Tip: nuspec intellisense

I ran into this exact problem when playing with nuspec intellisense:

“The XML namespace actually contains a placeholder which is replaced during release. So don’t use the file from Codeplex.”

But Xavier Decoster has solved the problem by putting the nuspec XSD into a NuGet package.

Check out his post for details.

Tips

Getting Started with Visual MEFX

I noticed that someone found their way to my blog by searching for “visual mefx how to open”.  This person probably went away disappointed, because although I mention Visual MEFX on this site, I never explained how to use it.  I ran the same search on Google and looked at the results, apparently no one who talks about Visual MEFX explains how to get it set up. Since its not obvious to at least one person out there, here’s how to do it.

Get MefContrib-Tools

From what I can tell, it’s a source only distribution.  So here’s a direct link to the source.  After downloading the zip file, be sure to right click on it, access the properties, and click “Unblock”.

This step will ensure that Visual Studio will not try to warn you about untrustworthy sources.  Unzip the source in a location of your choosing.

Build the Tools

There are two flavors of Visual MEFX, one for Silverlight, and one for the desktop.  Each has it’s own solution in the the “src” folder of the source you just downloaded.  Choose the one you are interested in, and open the solution. Choose Debug or Release mode according to your preference and build the project.

Run the Tools

Lets assume for the moment that you wanted Release versions of the tools.  Minimize Visual Studio and navigate to the bin\Release folder for the project you just built.  There are a few dlls, pdbs, xml manifests, and config files in the folder.  There are only two executable files.  The Visual MEFX desktop executable is “MefContrib.Tools.Visualizer.exe”.  Double click.

Silverlight

If you are interested in the Silverlight version, follow the same steps to build the project, but you will need to load the application in a browser.  Lucky for you, there will be an html file “TestPage.html” in the Release directory that you can use to get the application started.

Here it is running in Firefox and examining its own xap file. Hope this helps.

Tips, Troubleshooting

How NuGet Chooses the Nuspec File When Building From a Project File

If you have a class library project, it’s a good idea to learn how to use NuGet to package the resulting assembly. It’s a good idea even if you do not plan to share your assembly with the world, because you can still take advantage of the smarts built into NuGet to make managing your own packages easier. It’s not very hard at all to get up and running with your first package, and then you can start customizing from there.

Once you start customizing, you begin to realize there are some scenarios where you might want some slightly different packages. For example, maybe you have some code contract assemblies, but you don’t always want them included. Perhaps you have a library and a shell, and you want to give people the option of downloading just the library in one package, but to download both in another.

I have a scenario like that in at least a couple of my projects. I copied my nuspec file, customized it, added it back to my project then setup a script in the post-build event to create both packages. Worked great the first time I tried it, then failed when I tried to replicate the process in the next project. In the both projects, I kept the “easy” path (described below) which builds against the project file and finds the nuspec on its own. In both projects, I used the same scripts and naming conventions. Yet when it came time to create the project-based package the first project correctly selected the nuspec file that belongs with the project-based package, while the second project consistently selected the nuspec file that belongs with the customized package.

I searched around for an answer to how NuGet was making its decision, but could not find anything. Maybe I wasn’t using the right search terms, but I decided it would be best to stop being lazy and just go read the source. I have written a few CLI applications over the years, but I have to say I have never designed one as nice as NuGet. Next time I have a CLI to write, I’m going to study NuGet more carefully before writing any code.

As you may have guessed by the title of this post, I did find the answer to my question. You can go read the source, or you can read this post.

(Go ahead and read the source anyway, it is great.)

The Easy Way

The easiest way to get your assembly into a NuGet package is to follow the outline here: Creating and Publishing a Package. First download NuGet.exe then skip down to the section under the heading “From a Project.” Read the docs.nuget.org page for full details, but it boils down to this once you are used to the process:

  • Open command window and navigate to your project folder. I will assume your project is called “foo.csproj”
  • nuget spec
  • notepad foo.nuspec
  • Delete a few lines of junk from the nuspec then save.
  • nuget pack foo.csproj –Prop Configuration=Release

Done, now you can copy your package to your local package source or upload it to NuGet.org.

The Question

Now you want to create a custom nuspec to go with the one that you generated with NuGet.exe. Let’s call it “foo.custom.nuspec.” Now when you run “nuget pack” the process may or may not pick the correct nuspec. If it picks the custom nuspec file, it will build the incorrect package. After all, the premise is that you want both the default package and a custom package. If NuGet does pick the wrong metadata file, how do you get it to change its mind? You could be like me and try a bunch of things.

  • Maybe it’s picking the newest file?
  • Maybe it’s picking the oldest?
  • Shortest file name?
  • Longest?
  • Blah, blah blah, blah blah blah

Actually, I didn’t try all of those. I did try a couple, and then I went to the code.

The Answer

Here is the code that selects the nuspec file in version 1.6:

private string GetNuspec()
{
  return GetNuspecPaths().FirstOrDefault(File.Exists);
}

private IEnumerable<string> GetNuspecPaths()
{
  // Check for a nuspec in the project file
  yield return GetContentOrNone(file => Path.GetExtension(file).Equals(
      Constants.ManifestExtension, 
      StringComparison.OrdinalIgnoreCase));

  // Check for a nuspec named after the project
  yield return Path.Combine(
    _project.DirectoryPath, 
    Path.GetFileNameWithoutExtension(_project.FullPath) + Constants.ManifestExtension);
}

We can see right away that the search ends as soon as it finds an existing file that ends with “nuspec.” We can also see that the process prefers to get that file from the project file before it goes looking in the project directory. The “GetContentOrNone” method looks at the project file and chooses files marked with build action “Content” or “None” and yields back the matches. That was the key for me to figuring out why NuGet picked out the wrong file.

I added my nuspec files to the project for convenience. In one project, I had decided from the start to keep both Nuspec files, so I left the original alone and created a copy, renamed and added the copy to the project. This caused the new file’s ProjectItem to appear after the original in the csproj xml file.

In the other project, I had only thought that I wanted to customize the nuspec, then realized later that I wanted both. In the second project, I had customized the nuspec that was already there, then generated a new “default” nuspec, then added the new default to the project. This caused the default nuspec file’s ProjectItem to appear after the customized nuspec in the project xml. The order of operations mattered. I opened the misbehaving csproj in a text editor, rearranged the order of the ProjectItems, and tried again, problem solved.

Summary

For easy reference, here is the search path NuGet takes to choosing the file when the pack target is a project file:

  1. Project Members are preferred
    • NuGet checks items that have “None” or “Content” build actions for the .nuspec extension.
    • The first of these found is used.
  2. If there are no nuspec files in the project, NuGet tries to find a .nuspec file in the project file’s directory with the same name as the file.

Maybe this information is already out there somewhere, but if it is, I had a hard time finding it. If you have this problem, I hope this helps you.