Dependency Management in .Net: Offline Dependencies with NuGet Command Line Tool
Today I stumbled upon Scott Haselman’s post: How to access NuGet when NuGet.org is down (or you’re on a plane) in which Scott discusses how he recovered from an issue with the nuget.org site being down during his demo at the Dallas Day of .Net. As it turns out, while NuGet stores packages it downloads in a local Cache folder within your AppData folder, it doesn’t actually use this cache by default. Scott was able to remedy the situation by adding his local cache as a source through the the Visual Studio Package Manager plugin.
Last year, I wrote about my philosophy for dependency management and how I use NuGet to facilitate dependency management without using the Visual Studio plugin wherein I discuss using the NuGet.exe command line tool to manage .Net dependencies as part of my rake build. After reading Scott’s post, I got to wondering whether the NuGet.exe command line tool also had the same caching issue and after a bit of testing I discovered that it does. Since I, with the help of a former colleague, Josh Bush, have evolved the solution I wrote about previously a bit, I thought I’d provide an update to my approach which includes the caching fix.
As discussed in my previous article, I maintain a packages.rb file which serves as a central manifest of all the dependencies used project wide. Here’s one from a recent project:
packages = [ [ "Machine.Specifications", "0.5.3.0" ], [ "ExpectedObjects", "1.0.0.2" ], [ "Moq", "4.0.10827" ], [ "RabbitMQ.Client", "2.7.1" ], [ "log4net", "1.2.11" ] ] configatron.packages = packages
This is sourced by a rakefile which which is used by a task which installs any packages not already installed.
The basic template I use for my rakefile is as follows:
require 'rubygems' require 'configatron' ... NUGET_CACHE= File.join(ENV['LOCALAPPDATA'], '/NuGet/Cache/') FEEDS = ["http://[corporate NuGet Server]:8000", "https://go.microsoft.com/fwlink/?LinkID=206669" ] require './packages.rb' task :default => ["build:all"] namespace :build do task :all => [:clean, :dependencies, :compile, :specs, :package] ... task :dependencies do feeds = FEEDS.map {|x|"-Source " + x }.join(' ') configatron.packages.each do | name,version | feeds = "-Source #{NUGET_CACHE} " + feeds unless !version packageExists = File.directory?("#{LIB_PATH}/#{name}") versionInfo="#{LIB_PATH}/#{name}/version.info" currentVersion=IO.read(versionInfo) if File.exists?(versionInfo) if(!packageExists or !version or !versionInfo or currentVersion != version) then versionArg = "-Version #{version}" unless !version sh "nuget Install #{name} #{versionArg} -o #{LIB_PATH} #{feeds} -ExcludeVersion" do | ok, results | File.open(versionInfo, 'w') {|f| f.write(version) } unless !ok end end end end end
This version defines a NUGET_CACHE variable which points to the local cache. In the dependencies task, I join all the feeds into a list of Sources for NuGet to check. I leave out the NUGET_CACHE until I know whether or not a particular package specifies a version number. Otherwise, NuGet would simply check for the latest version which exists within the local cache.
To avoid having to change Visual Studio project references every time I update to a later version of a dependency, I use the –ExcludeVersion option. This means I can’t rely upon the folder name to determine whether the latest version is already installed, so I’ve introduced a version.info file. I imagine this is quite a bit faster than allowing NuGet to determine whether the latest version is installed, but I actually do this for a different reason. If you tell NuGet to install a package into a folder without including the version number as part of the folder and you already have the specified version, it uninstalls and reinstalls the package. Without checking the presence of the correct version beforehand, NuGet would simply reinstall everything every time.
Granted, this rake task is far nastier than it needs to be. It should really only have to be this:
task :dependencies do nuget.exe install depedencyManifest.txt –o lib end
Where the dependencyManifest file might look a little more like this:
Machine.Specifications 0.5.3.0 ExpectedObjects 1.0.0.2 Moq 4.0.10827 RabbitMQ.Client 2.7.1 log4net 1.2.11
Nevertheless, I’ve been able to coerce the tool into doing what I want for the most part and it all works swimmingly once you get it set up.