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)>
;
(require 'flymake)
(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="https://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="https://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"))
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'flymake-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.
Comments
Anonymous
April 15, 2008
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.Anonymous
April 15, 2008
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.Anonymous
April 15, 2008
[I'm going to try to keep this up to date, because I periodically change tweak and improve flymake setup...Anonymous
April 21, 2008
In my prior post I wrote that I have a dream of getting c# code completion in emacs. Jason Rumney wroteAnonymous
September 12, 2008
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.Anonymous
September 12, 2008
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)