Getting emacs flymake.el to work with C# modules


[I’m going to try to keep this up to date, because I periodically change tweak and improve flymake setup… Latest is 23 April 2008]


I Loooooooove Flymake.  Emacs, since a while back, ships with a package called flymake.el, that defines a minor mode.  When you enable this mode, flymake more-or-less continuously compiles the module you’re working on, checking for syntax errors, and highlighting any that are found in your buffer. Pretty cool! 


Bad news: Out of the box, flymake does not work with C# compiles on Windows.   But, it’s possible to get it to work. Here’s what I added to my .emacs file to make it work for me.


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; flymake minor mode – tweaks for csharp
;
; Flymake is built-in to emacs. It more-or-less continually compiles an
; active buffer when the minor mode is enabled. It also flags broken lines
; in the compile as you type.
;
; This is a set of tweaks of flymake for C# on Windows.
;
; last saved Time-stamp: <Wednesday, April 23, 2008 13:44:26 (by dinoch)>
;

(requireflymake)

(setq flymake-log-level 0) ;; insure flymake errors get plopped into the *Messages* buffer

;; There are 2 common ways to build C# files: nmake or msbuild.
;; Here are examples for either.
;;
;; For makefile, use nmake. Configure this stanza to specify where your
;; nmake is. Usually it is in the .NET 2.0 SDK directory, or the platform SDK
;; directory.
;;
;; If you use nmake, then you need a make target like this in your makefile:
;;
;; check-syntax:
;; $(_CSC) /t:module $(CHK_SOURCES)
;;
;; (You could also put this in an alternatively named makefile,
;; like makefile.flymake. In this case you would also need to modify
;; the nmake command line (See below))
;;
;; If you use msbuild, and you are compiling projects that consist of a single
;; source file, you can use a standard (boilerplate) build project
;; file. Call it msbuild.flymake.xml, and define it like this:
;;
;; <Project xmlns=”http://schemas.microsoft.com/developer/msbuild/2003″
;; DefaultTargets=”CompileAll”
;; ToolsVersion=”3.5″
;; >
;;
;; <Import Project=”$(MSBuildToolsPath)\Microsoft.CSharp.targets” />
;;
;; <!– specify reference assemblies for all builds in this project –>
;; <ItemGroup>
;; <Reference Include=”mscorlib” />
;; <Reference Include=”System” />
;; <Reference Include=”System.Core” />
;; <Reference Include=”System.Data” />
;; <Reference Include=”System.Data.Linq” /> <!– LINQ –>
;; <!–Reference Include=”System.ServiceModel” /–> <!– WCF –>
;; <!–Reference Include=”System.ServiceModel.Web” /–> <!– WCF –>
;; <!–Reference Include=”System.Runtime.Serialization” /–> <!– WCF –>
;; </ItemGroup>
;;
;; <Target Name=”CheckSyntax”
;; DependsOnTargets=”ResolveAssemblyReferences”
;; >
;; <CSC
;; Sources=”$(SourceFileToCheck)”
;; References=”@(ReferencePath)”
;; TargetType=”module”
;; Toolpath=”$(MSBuildToolsPath)”
;; Nologo=”true”
;; />
;; </Target>
;;
;; </Project>
;;
;; -ends-
;;
;; (This msbuild file works only with .NET 3.5.)
;;

;; If your projects consist of multiple source files, then you need to get fancier.
;; You need to compile all files, *except* for the original source file, the one
;; being edited currently. In this case, your msbuild.flymake.xml file should look
;; something like this:

;; <Project xmlns=”http://schemas.microsoft.com/developer/msbuild/2003″
;; DefaultTargets=”CompileAll”
;; ToolsVersion=”3.5″
;; >
;;
;; <Import Project=”$(MSBuildToolsPath)\Microsoft.CSharp.targets” />
;;
;; <PropertyGroup>
;; <Optimize>false</Optimize>
;; <DebugSymbols>true</DebugSymbols>
;; <!– <OutputPath>.\bin\</OutputPath> –>
;; <OutputPath>.\</OutputPath>
;; <OutDir>.\</OutDir>
;; <IntermediateOutputPath>.\obj\</IntermediateOutputPath>
;; </PropertyGroup>
;;
;; <!– specify reference assemblies for all builds in this project –>
;; <ItemGroup>
;; <Reference Include=”mscorlib” />
;; <Reference Include=”System” />
;; <Reference Include=”System.Core” />
;; <Reference Include=”System.Data” />
;; <Reference Include=”System.Data.Linq” /> <!– LINQ –>
;; <!–Reference Include=”System.ServiceModel” /–> <!– WCF –>
;; <!–Reference Include=”System.ServiceModel.Web” /–> <!– WCF –>
;; <!–Reference Include=”System.Runtime.Serialization” /–> <!– WCF –>
;; </ItemGroup>
;;
;; <!– This ItemGroup includes every .cs source file in the directory, –>
;; <!– except for the one indicated by OriginalSourceFile. In flymake, that –>
;; <!– property indicates the currently edited file. So the result is that the –>
;; <!– ItemGroup CSFile will include all files, including the _flymake.cs clone, –>
;; <!– but not including the original file. Which is what we want. –>
;; <ItemGroup>
;; <CSFile Include=”*.cs” Exclude=”$(OriginalSourceFile)” />
;; </ItemGroup>
;;
;; <!– Stuff the OriginalSourceFile property into an ItemGroup. –>
;; <!– We do this so we can get at the metadata, which I Think is available only –>
;; <!– through an item within an ItemGroup. We want the root filename, which –>
;; <!– we use to name the output netmodule. –>
;; <ItemGroup>
;; <ExcludedCSFile Include=”$(OriginalSourceFile)” />
;; </ItemGroup>
;;
;; <Target Name=”CheckSyntax”
;; DependsOnTargets=”ResolveAssemblyReferences”
;; >
;; <!– Run the Visual C# compilation on the specified set of .cs files. –>
;; <CSC
;; Sources=”@(CSFile)”
;; References=”@(ReferencePath)”
;; TargetType=”module”
;; Toolpath=”$(MSBuildToolsPath)”
;; OutputAssembly=”%(ExcludedCSFile.Filename)_flymake.netmodule”
;; Nologo=”true”
;; />
;; </Target>
;;
;; </Project>
;;

;; These variables are ones I made up for help with C#:
(defvar dino-flymake-netsdk-location “c:\\netsdk2.0”
“Location of .NET SDK, for finding nmake.exe. The nmake is found in the bin subdir.  Example value is: c:\\Program Files\\Microsoft Visual Studio 8\\SDK\\v2.0 .”)

(defvar dino-flymake-msbuild-location “c:\\.net3.5”
“Directory containing MSBuild.exe. Typically, c:\\windows\\Microsoft.NET\\Framework\\v3.5 .”)

(defvar dino-flymake-csharp-msbuild-buildfile “msbuild.flymake.xml”
“Build file if using MSBuild.exe.”)

(defvar dino-flymake-csharp-nmake-buildfile “makefile”
“Build file if using nmake.exe.”)

(defvar dino-flymake-csharp-use-msbuild t
“If t, then flymake uses msbuild.exe and the msbuild.flymake.xml
file. If nil, then flymake uses nmake and the makefile with a
check-status target. Keep in mind the buildfile for either msbuild or nmake
is customizable. See the vars dino-flymake-csharp-{nmake,msbuild}-buildfile .”
)

(defun dino-flymake-csharp-cleanup ()
“Delete the temporary .netmodule file created in syntax checking,
then call through to flymake-simple-cleanup.”

(if flymake-temp-source-file-name
(let* ((netmodule-name
(concat (file-name-sans-extension flymake-temp-source-file-name)
“.netmodule”))
(expanded-netmodule-name (expand-file-name netmodule-name “.”))
)
(if (file-exists-p expanded-netmodule-name)
(flymake-safe-delete-file expanded-netmodule-name)
)
)
)
(flymake-simple-cleanup)

)

(defun dino-flymake-csharp-buildfile ()
(if dino-flymake-csharp-use-msbuild
dino-flymake-csharp-msbuild-buildfile
dino-flymake-csharp-nmake-buildfile
)
)

(defun dino-flymake-find-csharp-buildfile (source-file-name)
(let ((actual-build-file-name (dino-flymake-csharp-buildfile)))
(if (file-exists-p (expand-file-name actual-build-file-name “.”))
“.”
(flymake-log 1 “no buildfile (%s) for %s” actual-build-file-name source-file-name)
(flymake-report-fatal-status
“NOMK” (format “No buildfile (%s) found for %s”
actual-build-file-name source-file-name))
nil
)

)
)

;(debug-on-entry ‘flymake-create-temp-inplace)

(defun dino-flymake-csharp-init ()
(dino-flymake-csharp-init-impl ‘flymake-create-temp-inplace t t ‘flymake-get-make-cmdline))

(defun dino-flymake-csharp-init-impl (create-temp-f use-relative-base-dir use-relative-source get-cmdline-f)
“Create syntax check command line for a directly checked source file.
Use CREATE-TEMP-F for creating temp copy.”

(let* ((args nil)
(source-file-name buffer-file-name)
(buildfile-dir (dino-flymake-find-csharp-buildfile source-file-name)))
(if buildfile-dir
(let* ((temp-source-file-name (flymake-init-create-temp-buffer-copy create-temp-f)))
(setq args (flymake-get-syntax-check-program-args temp-source-file-name buildfile-dir
use-relative-base-dir use-relative-source
get-cmdline-f))))
args))

;(debug-on-entry ‘dino-flymake-csharp-init)

; This fixup sets flymake to use a different cleanup routine for c# compiles
(defun dino-fixup-flymake-for-csharp ()
(let (elt
(csharp-entry nil)
(masks flymake-allowed-file-name-masks)
)

;; The “flymake-allowed-file-name-masks” variable stores a filename pattern as
;; well as the make-init function, and a cleanup function. In the case of csharp,
;; the setting in flymake.el has the cleanup fn as nil, which means it gets the
;; standard cleanup : the *_flymake.cs cloned source file gets deleted. But the
;; way I have done the syntax checking, I compile the .cs file into a module,
;; which needs to be deleted afterwards.
;;

;; Here, we remove the C# entry in the “flymake-allowed-file-name-masks”
;; variable, and replace it with an entry that includes a custom csharp cleanup
;; routine. In that cleanup routine, I delete the .netmodule file.

;; I could just setq the “flymake-allowed-file-name-masks” var to the C# thing I
;; want, but that would obliterate all the masks for all other languages, which
;; would be bad manners.

;; You know, come to think of it, I could just delete the generated .netmodule
;; file in the msbuild or makefile. That might be simpler.

;; But the main point is this ought to be more easily configurable or customizable
;; in flymake.el. And also, flymake ought to do something reasonable for csharp builds,
;; rather than completely punt.

;; This fixup is really hacky, relying on the string that is used for csharp in
;; flymake.el. But it will do for now…

;; Find the entry
(while (consp masks)
(setq elt (car masks))
(if (string= “\\.cs\\'” (car elt))
(setq csharp-entry elt)
)
(setq masks (cdr masks))
)

;; remove the original one …
(if csharp-entry
(setq flymake-allowed-file-name-masks
(delete csharp-entry flymake-allowed-file-name-masks)))

;; Now add a new one, with the custom cleanup method.
(setq flymake-allowed-file-name-masks
(cons
‘(“\\.cs\\'” dino-flymake-csharp-init dino-flymake-csharp-cleanup)
flymake-allowed-file-name-masks))
)
)

; need to do this only once, not every time csharp-mode is invoked
(dino-fixup-flymake-for-csharp)

; This method re-defines the defun shipped in flymake, for csharp. Re-defining
; this function *will* definitely break flymake for all other languages. One
; way to fix that problem is to make the “get-make-cmdline” function a
; configurable hook within flymake!

(defun flymake-get-make-cmdline (source base-dir)
(if dino-flymake-csharp-use-msbuild
(list (concat dino-flymake-msbuild-location “\\msbuild.exe”)
(list (concat base-dir “/” (dino-flymake-csharp-buildfile))
“/nologo”
“/t:CheckSyntax”
“/v:quiet” ;; normal
;; use file-relative-name to remove the fully-qualified directory name
(concat “/property:SourceFileToCheck=” (file-relative-name source))
(concat “/property:OriginalSourceFile=” (file-relative-name buffer-file-name))
))

(list (concat dino-flymake-netsdk-location “\\bin\\nmake.exe”)
(list “/f”
(concat base-dir “/” (dino-flymake-csharp-buildfile))
(concat “CHK_SOURCES=” source)
“SYNTAX_CHECK_MODE=1”
“check-syntax”))

)
)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(provideflymake-for-csharp)

;;; end of flymake-for-csharp.el


I think there are probably still some problems with this. For example, there’s no direct support for projects, so for a module that refers to a class in another source file, you’re gonna have to fiddle with the msbuild file to get the references right. But as a first attempt, it’s basically working.


I do love flymake, it is very handy.  But just to repeat my previous statement about it:


Flymake is at least 5 years old, but it still seems to be a bit rough. There’s no doc in the el file. It doesn’t respect the “compile-command” variable of compile.el, instead hard-coding make. There are a bunch of other missed opportunities for customization in flymake, too. There’s no doc for how to specify a different check-syntax build. There’s no doc for how to do a better cleanup – so I have temporary output files hanging around. Why isn’t flymake-allowed-file-name-masks an alist? Basically flymake is bad manners all around. But it’s mostly working now, and it seems very handy.

 

flymake-for-csharp.el

Comments (6)

  1. John Connors says:

    In all that time no-one has hacked on flymake to improve it? There must be a serious shortage of elisp hackers.

    I’ve been using C++ not C# and have been winging it with a mixture of etags/hippie-expand/ecb/cedet. It works, mostly.

  2. cheeso says:

    No, no,no, Flymake.el is being actively maintained by the fella who is named in the el file.  It’s being improved, although I don’t know the details. I’m just saying it is not as modular as I would like to see.  

    Elisp is sort of a niche, hm?  Anyway I shot a mail to Pavel and we’ll see if he has the time and interest to pursue some of the suggestions I made. I tried to take a broader view and suggest that all languages in flymake be supported by the same extensibility or customization mechanisms.  Rather than having it default to C compiles, and then doing something special for everything that is not C (ruby, python, C#, tex, etc), what I suggested was having the entire thing be pluggable, where each support in flymakr for a language is defined in terms of a handful of override or extension methods.

    we’ll see where that goes.

  3. [I’m going to try to keep this up to date, because I periodically change tweak and improve flymake setup…

  4. In my prior post I wrote that I have a dream of getting c# code completion in emacs. Jason Rumney wrote

  5. I just use the following with the standard package:

    (require ‘flymake)

    (defun my-flymake-simple-make-init ()

     (basic-save-buffer-1)

     (list "MSBuild.exe" (list "/nologo" "/verbosity:quiet")))

    (setq flymake-allowed-file-name-masks

         (cons ‘(".+\.cs$"

                 my-flymake-simple-make-init

                 flymake-simple-cleanup

                 flymake-get-real-file-name)

               flymake-allowed-file-name-masks))

    I set up my projects using Visual Studio, and have emacs configured as an external tool I can hop over to quickly.

    drawbacks: it saves the buffer every time you change it

    benefits: it just works, no need to set up (and maintain) another project file just for flymake.

  6. Oh, I also have these defined to pick up the error messages:

    (require ‘compile)

    (push ‘("^\(.*\)(\([0-9]+\),\([0-9]+\)): error" 1 2 3 ) compilation-error-regexp-alist)

    (push ‘("^\(.*\)(\([0-9]+\),\([0-9]+\)): warning" 1 2 3 ) compilation-error-regexp-alist)