But there is one thing that really annoys me; one thing that I think is fundamentally wrong with .NET ecosystem. There is a tight coupling between your project files in your IDE of choice and your build scripts.
I have yet to experience a .NET project, where the build script would not call msbuild myproject.sln or msbuild myproject.csproj.
Your IDE project file has become your build script. If the Visual Studio developers wanted to implement the Separation of Concerns principles, this is one place where they failed.
And though there are tools that try to help you write .NET code using pure text editors (Emacs, Vim, Sublime, etc), most notably OmniSharp. But most, if not all, of what I have seen still assumes that you have .csproj or .fsproj project file. If not, you are out of luck; in particular, if you depend on functionality from NuGet packages.
For fun, I wanted to create a small web based game using an F# backend, but I wanted to start in Vim, and I wanted to stay in Vim.
So I started to create some build tools to allow me to much more easily create a build script, including support for referencing NuGet packages.
Without too much effort, I had created a nice POC using Rake and Paket. (Writing this blog post has taken considerably more time than creating the POC)
desc "Build main executable" fsc :build do |t| t.output_file = "nde.exe" t.output_folder = "output" t.source_files << "host.fs" t.source_files << "main.fs" t.packages << "packages/Nancy" t.packages << "packages/Nancy.Hosting.Self" end task :default => [:clean, :build]
If I had coded in C#, I could just have selected “*.cs” as source files instead of explicitly specifying every source file, but F# requires that the source files are supplied in a specific order, one thing I hope to automate using this tool.
At the time of writing, it’s in an exploratory state – and has not been distributed as a separate gem.
The code can be found at https://github.com/PeteProgrammer/nde
Note: All my experiments are conducted on a Mac using Mono. I have not bothered to make it cross platform yet, so if you are running this on Windows, you need to adapt, e.g. the F# compiler executable is called fsc in .NET, and fsharpc in Mono.
I have used Paket to handle NuGet packages.
Paket is an alternate to NuGet as a package manager. It supports NuGet as the format for packages, and supports getting then from a standard NuGet feed. But it also allows you install packages from the command line.
This makes it a lot more friendly than bare NuGet if you want to get rid of the IDE.
Rake is a Ruby based make tool. It does everything a proper build tool should, allowing you to specify tasks, task dependencies, and build optimization, i.e. skip building the product of none of the source has been modified since the last build. And it has a great intuitive DSL.
This makes Rake the perfect tool for the task. One particular advantage of this over xml-based configuration tool is that the build script is plain ruby code. So if you need to add functionality, you just create a function. (Ever tried to create custom msbuild tasks?)
The downside for depending on Rake is that you have to install Ruby in order to use it. This may unfortunately scare some (many) .NET developers. But they are probably not reading this to begin with.
First, I wanted to compile this a simple Hello World application
module Main [<EntryPoint>] let main args = printfn "Hello world" 0
This is the build script
desc "Main executable" fsc :build do |t| t.output_file = "nde.exe" t.output_folder = "output" t.source_files << "main.fs" end
The main code is implemented in the class FscBuilder
# Creates the concrete task for building F# code class FscBuilder attr_accessor :output_folder attr_accessor :output_file def initialize *args @args = args @output_folder = "." yield self if block_given? end def source_files @source_files ||= [] end ... def create_compilation_task dest = File.join output_folder, output_file Rake::FileTask::define_task dest do |t| output = "--out:#{dest}" target = "--target:exe" sources = source_files.join(" ") system "fsharpc #{output} #{target} #{sources}" end end end
The heart of this code is the Rake::FileTask::define_task that creates the actual Rake task. The name of the task is the name of the file being generated. The block contains the code for actually generating the file.
This example, this will create a task named “output/nde.exe”. This is not really helpful when we want to run it from command line, or use it as dependency to other tasks, e.g. a task that compiles and runs unit tests. A change to the output folder would change the name of this task. So we create a surrogate task to trigger the execution of this task, and a helper function:
class FscBuilder def initialize *args @args = args @output_folder = "." yield self if block_given? end ... def create_task task = Rake::Task::define_task *@args file_task = create_compilation_task task.enhance [file_task] end end def fsc *args, &block builder = FscBuilder.new *args, &block builder.create_task end
Now, the task name we gave as argument to the fsc call becomes the name of the created task. And any task dependencies that we specify when alling the fsc function will automatically become dependencies of the surrogate task, executing before the actual compilation.
Run this from the command line
$ rake > F# Compiler for F# 3.1 (Open Source Edition) > Freely distributed under the Apache 2.0 Open Source License $ mono output/nde.exe > Hello world $
It’s working. Because we are using a FileTask with a name identical to the output file, Rake will automatically detect if this needs to be rebuild. Running Rake again:
$ rake $
Nothing happens. The file already exists, and Rake avoids generating the file again. But if the source files are modified, the file is not generated again. This is very simply remedied:
def create_compilation_task dest = File.join output_folder, output_file Rake::FileTask::define_task dest => source_files do |t| ... end
We have added the source files as dependencies to the output file. Now the product will rebuild whenever any source file has been modified.
1st step complete. We can compile a collection of source files with a simple build script, and the build script supports optimizing the build, only building what needs to be build.
Let’s add some NuGet packages to the project. For this small test, I will add Nancy and Nancy.SelfHost
The modified main.fs looks like this:
module Main open System open Nancy open Nancy.Hosting.Self [<EntryPoint>] let main args = let bootstrapper = new DefaultNancyBootstrapper () let nancyHost = new NancyHost( bootstrapper, new Uri("http://localhost:1239/"), new Uri("http://127.0.0.1:1239/")) nancyHost.Start() nancyHost.Stop() printfn "Hello world" 0
First, download the Nancy nuget packages using paket:
$ mono paket.exe add nuget Nancy $ mono paket.exe add nuget Nancy.SelfHost
Add dependencies to the Rakefile
fsc :build do |t| t.output_file = "nde.exe" t.source_files << "main.fs" t.packages << "packages/Nancy" t.packages << "packages/Nancy.Hosting.Self" end
And support for NuGet packages when compiling
class FscBuilder ... def packages @packages ||= [] end def get_nuget_dlls package Dir[File.join package, "lib/net40/*.dll"] end def assembly_refs packages.flat_map { |m| get_nuget_dlls m } end def create_compilation_task task_dependencies = source_files | assembly_refs dest = File.join output_folder, output_file Rake::FileTask::define_task dest => task_dependencies do |t| refs = assembly_refs.map { |r| "-r:#{r}" }.join(" ") output = "--out:#{dest}" target = "--target:exe" sources = source_files.join(" ") system "fsharpc #{output} #{refs} #{target} #{sources}" end end end
This is not a production-ready solution. It simply iterates through all .dll files inside the lib/net40 folder of each NuGet package. Of course it should respect what has been specified in the .nuspec file – and it should select the appropriate framework, depending on the target framework of your product. But it will do for now.
Notice that the referenced assemblies have been added as dependency to the compilation task, making sure that the product is rebuild if a new version of a package is downloaded.
Now, the code compiles once more.
With the latest changes, the application builds, but if we try to run it:
$ mono output/nde.exe Could not load signature of Host:get_nancyHost due to: Could not load file or assembly or one of its dependencies. Unhandled Exception: System.TypeLoadException: A type load exception has occurred. [ERROR] FATAL UNHANDLED EXCEPTION: System.TypeLoadException: A type load exception has occurred.
Although the dependent assemblies can be found at compile time, they cannot be found at run time. We need to copy those to the output folder.
Lets add code to copy dependencies to the output folder:
def create_copy_task source dest = File.join output_folder, File.basename(source) Rake::FileTask::define_task dest => source do FileUtils.cp source, dest end end def create_task task = Rake::Task::define_task *@args copy_dependency_tasks = assembly_refs.map { |m| create_copy_task m } ... task.enhance copy_dependency_tasks end
For this, we created a new FileTask for each file to copy, and adding these as dependencies to the build task. This again tells Rake to avoid copying a dependency if there are no changes since the last time we compiled.
And now, if we try to run, it works
$ mono output/nde.exe > Hello World $
… is an exageration. There’s quite some work before this can be a mature production ready tool. But it does show that there is a way forward to create tools, where a small project could start with just a few files in a folder and a build script.
To get there least the following features needs to be implemented:
And probably lots more I have not yet thought of.
Then you could start thinking about the following:
But the first task is probably the most difficult. To come up with a great name for the gem.
I hope I am not the only .NET developer who wants to be free, once and for all, of Visual Studio, and others will join me.
Don’t get me wrong. I like Visual Studio, I think it’s a great product – but it should just be tool for writing code, and not dictate how your build script looks, and how every other tool should behave.
]]>