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.

 

flymake-for-csharp.el