View Javadoc
1   package org.codehaus.plexus.compiler.csharp;
2   
3   /*
4    * Copyright 2005 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import org.codehaus.plexus.compiler.AbstractCompiler;
20  import org.codehaus.plexus.compiler.CompilerConfiguration;
21  import org.codehaus.plexus.compiler.CompilerException;
22  import org.codehaus.plexus.compiler.CompilerMessage;
23  import org.codehaus.plexus.compiler.CompilerOutputStyle;
24  import org.codehaus.plexus.compiler.CompilerResult;
25  import org.codehaus.plexus.util.DirectoryScanner;
26  import org.codehaus.plexus.util.IOUtil;
27  import org.codehaus.plexus.util.Os;
28  import org.codehaus.plexus.util.StringUtils;
29  import org.codehaus.plexus.util.cli.CommandLineException;
30  import org.codehaus.plexus.util.cli.CommandLineUtils;
31  import org.codehaus.plexus.util.cli.Commandline;
32  import org.codehaus.plexus.util.cli.StreamConsumer;
33  import org.codehaus.plexus.util.cli.WriterStreamConsumer;
34  
35  import java.io.BufferedReader;
36  import java.io.File;
37  import java.io.FileWriter;
38  import java.io.IOException;
39  import java.io.PrintWriter;
40  import java.io.StringReader;
41  import java.io.StringWriter;
42  import java.io.Writer;
43  import java.nio.file.Paths;
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.HashSet;
47  import java.util.Iterator;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.Set;
51  
52  /**
53   * @author <a href="mailto:gdodinet@karmicsoft.com">Gilles Dodinet</a>
54   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
55   * @author <a href="mailto:matthew.pocock@ncl.ac.uk">Matthew Pocock</a>
56   * @author <a href="mailto:chris.stevenson@gmail.com">Chris Stevenson</a>
57   * @plexus.component role="org.codehaus.plexus.compiler.Compiler"
58   * role-hint="csharp"
59   */
60  public class CSharpCompiler
61      extends AbstractCompiler
62  {
63      private static final String JAR_SUFFIX = ".jar";
64      private static final String DLL_SUFFIX = ".dll";
65      private static final String NET_SUFFIX = ".net";
66      
67      private static final String ARGUMENTS_FILE_NAME = "csharp-arguments";
68  
69      private static final String[] DEFAULT_INCLUDES = { "**/**" };
70      
71      private Map<String, String> compilerArguments;
72  
73      // ----------------------------------------------------------------------
74      //
75      // ----------------------------------------------------------------------
76  
77      public CSharpCompiler()
78      {
79          super( CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES, ".cs", null, null );
80      }
81  
82      // ----------------------------------------------------------------------
83      // Compiler Implementation
84      // ----------------------------------------------------------------------
85  
86      public boolean canUpdateTarget( CompilerConfiguration configuration )
87          throws CompilerException
88      {
89          return false;
90      }
91  
92      public String getOutputFile( CompilerConfiguration configuration )
93          throws CompilerException
94      {
95          return configuration.getOutputFileName() + "." + getTypeExtension( configuration );
96      }
97  
98      public CompilerResult performCompile( CompilerConfiguration config )
99          throws CompilerException
100     {
101         File destinationDir = new File( config.getOutputLocation() );
102 
103         if ( !destinationDir.exists() )
104         {
105             destinationDir.mkdirs();
106         }
107 
108         config.setSourceFiles( null );
109 
110         String[] sourceFiles = CSharpCompiler.getSourceFiles( config );
111 
112         if ( sourceFiles.length == 0 )
113         {
114             return new CompilerResult().success( true );
115         }
116 
117         System.out.println( "Compiling " + sourceFiles.length + " " + "source file" +
118                                 ( sourceFiles.length == 1 ? "" : "s" ) + " to " + destinationDir.getAbsolutePath() );
119 
120         String[] args = buildCompilerArguments( config, sourceFiles );
121 
122         List<CompilerMessage> messages;
123 
124         if ( config.isFork() )
125         {
126             messages =
127                 compileOutOfProcess( config.getWorkingDirectory(), config.getBuildDirectory(), findExecutable( config ),
128                                      args );
129         }
130         else
131         {
132             throw new CompilerException( "This compiler doesn't support in-process compilation." );
133         }
134 
135         return new CompilerResult().compilerMessages( messages );
136     }
137 
138     public String[] createCommandLine( CompilerConfiguration config )
139         throws CompilerException
140     {
141         return buildCompilerArguments( config, CSharpCompiler.getSourceFiles( config ) );
142     }
143 
144     // ----------------------------------------------------------------------
145     //
146     // ----------------------------------------------------------------------
147 
148     private Map<String, String> getCompilerArguments( CompilerConfiguration config )
149     {
150         if (compilerArguments != null)
151         {
152             return compilerArguments;
153         }
154         
155         compilerArguments = config.getCustomCompilerArgumentsAsMap();
156         
157         Iterator<String> i = compilerArguments.keySet().iterator();
158         
159         while ( i.hasNext() ) 
160         {
161             String orig = i.next();
162             String v = compilerArguments.get( orig );
163             if ( orig.contains( ":" ) && v == null ) 
164             {
165                 String[] arr = orig.split( ":" );
166                 i.remove();
167                 String k = arr[0];
168                 v = arr[1];
169                 compilerArguments.put( k, v );
170                 if ( config.isDebug() )
171                 {
172                     System.out.println( "transforming argument from " + orig + " to " + k + " = [" + v + "]" );
173                 }
174             }
175         }
176         
177         config.setCustomCompilerArgumentsAsMap( compilerArguments );
178         
179         return compilerArguments;
180     }
181 
182     private String findExecutable( CompilerConfiguration config )
183     {
184         String executable = config.getExecutable();
185 
186         if ( !StringUtils.isEmpty( executable ) )
187         {
188             return executable;
189         }
190 
191         if ( Os.isFamily( "windows" ) )
192         {
193             return "csc";
194         }
195 
196         return "mcs";
197     }
198 
199     /*
200 $ mcs --help
201 Mono C# compiler, (C) 2001 - 2003 Ximian, Inc.
202 mcs [options] source-files
203    --about            About the Mono C# compiler
204    -addmodule:MODULE  Adds the module to the generated assembly
205    -checked[+|-]      Set default context to checked
206    -codepage:ID       Sets code page to the one in ID (number, utf8, reset)
207    -clscheck[+|-]     Disables CLS Compliance verifications
208    -define:S1[;S2]    Defines one or more symbols (short: /d:)
209    -debug[+|-], -g    Generate debugging information
210    -delaysign[+|-]    Only insert the public key into the assembly (no signing)
211    -doc:FILE          XML Documentation file to generate
212    -keycontainer:NAME The key pair container used to strongname the assembly
213    -keyfile:FILE      The strongname key file used to strongname the assembly
214    -langversion:TEXT  Specifies language version modes: ISO-1 or Default
215    -lib:PATH1,PATH2   Adds the paths to the assembly link path
216    -main:class        Specified the class that contains the entry point
217    -noconfig[+|-]     Disables implicit references to assemblies
218    -nostdlib[+|-]     Does not load core libraries
219    -nowarn:W1[,W2]    Disables one or more warnings
220    -optimize[+|-]     Enables code optimalizations
221    -out:FNAME         Specifies output file
222    -pkg:P1[,Pn]       References packages P1..Pn
223    -recurse:SPEC      Recursively compiles the files in SPEC ([dir]/file)
224    -reference:ASS     References the specified assembly (-r:ASS)
225    -target:KIND       Specifies the target (KIND is one of: exe, winexe,
226                       library, module), (short: /t:)
227    -unsafe[+|-]       Allows unsafe code
228    -warnaserror[+|-]  Treat warnings as errors
229    -warn:LEVEL        Sets warning level (the highest is 4, the default is 2)
230    -help2             Show other help flags
231 
232 Resources:
233    -linkresource:FILE[,ID] Links FILE as a resource
234    -resource:FILE[,ID]     Embed FILE as a resource
235    -win32res:FILE          Specifies Win32 resource file (.res)
236    -win32icon:FILE         Use this icon for the output
237    @file                   Read response file for more options
238 
239 Options can be of the form -option or /option
240     */
241 
242     private String[] buildCompilerArguments( CompilerConfiguration config, String[] sourceFiles )
243         throws CompilerException
244     {
245         List<String> args = new ArrayList<String>();
246 
247         if ( config.isDebug() )
248         {
249             args.add( "/debug+" );
250         }
251         else
252         {
253             args.add( "/debug-" );
254         }
255 
256         // config.isShowWarnings()
257         // config.getSourceVersion()
258         // config.getTargetVersion()
259         // config.getSourceEncoding()
260 
261         // ----------------------------------------------------------------------
262         //
263         // ----------------------------------------------------------------------
264 
265         for ( String element : config.getClasspathEntries() )
266         {
267             File f = new File( element );
268 
269             if ( !f.isFile() )
270             {
271                 continue;
272             }
273             
274             if (element.endsWith(JAR_SUFFIX)) {
275                 try 
276                 {
277                     File dllDir = new File(element + NET_SUFFIX);
278                     if (!dllDir.exists())
279                     {
280                         dllDir.mkdir();
281                     }
282                     JarUtil.extract(dllDir, new File(element));
283                     for (String tmpfile : dllDir.list()) 
284                     {
285                         if ( tmpfile.endsWith(DLL_SUFFIX) )
286                         {
287                             String dll = Paths.get(dllDir.getAbsolutePath(), tmpfile).toString();
288                             args.add( "/reference:\"" + dll + "\"" );
289                         }
290                     }
291                 }
292                 catch ( IOException e )
293                 {
294                     throw new CompilerException( e.toString(), e );
295                 }
296             }
297             else
298             {
299                 args.add( "/reference:\"" + element + "\"" );
300             }
301         }
302 
303         // ----------------------------------------------------------------------
304         // Main class
305         // ----------------------------------------------------------------------
306 
307         Map<String, String> compilerArguments = getCompilerArguments( config );
308 
309         String mainClass = compilerArguments.get( "-main" );
310 
311         if ( !StringUtils.isEmpty( mainClass ) )
312         {
313             args.add( "/main:" + mainClass );
314         }
315 
316         // ----------------------------------------------------------------------
317         // Xml Doc output
318         // ----------------------------------------------------------------------
319 
320         String doc = compilerArguments.get( "-doc" );
321 
322         if ( !StringUtils.isEmpty( doc ) )
323         {
324             args.add( "/doc:" + new File( config.getOutputLocation(),
325                                           config.getOutputFileName() + ".xml" ).getAbsolutePath() );
326         }
327 
328         // ----------------------------------------------------------------------
329         // Xml Doc output
330         // ----------------------------------------------------------------------
331 
332         String nowarn = compilerArguments.get( "-nowarn" );
333 
334         if ( !StringUtils.isEmpty( nowarn ) )
335         {
336             args.add( "/nowarn:" + nowarn );
337         }
338 
339         // ----------------------------------------------------------------------
340         // Out - Override output name, this is required for generating the unit test dll
341         // ----------------------------------------------------------------------
342 
343         String out = compilerArguments.get( "-out" );
344 
345         if ( !StringUtils.isEmpty( out ) )
346         {
347             args.add( "/out:" + new File( config.getOutputLocation(), out ).getAbsolutePath() );
348         }
349         else
350         {
351             args.add( "/out:" + new File( config.getOutputLocation(), getOutputFile( config ) ).getAbsolutePath() );
352         }
353 
354         // ----------------------------------------------------------------------
355         // Resource File - compile in a resource file into the assembly being created
356         // ----------------------------------------------------------------------
357         String resourcefile = compilerArguments.get( "-resourcefile" );
358 
359         if ( !StringUtils.isEmpty( resourcefile ) )
360         {
361             String resourceTarget = (String) compilerArguments.get( "-resourcetarget" );
362             args.add( "/res:" + new File( resourcefile ).getAbsolutePath() + "," + resourceTarget );
363         }
364 
365         // ----------------------------------------------------------------------
366         // Target - type of assembly to produce, lib,exe,winexe etc... 
367         // ----------------------------------------------------------------------
368 
369         String target = compilerArguments.get( "-target" );
370 
371         if ( StringUtils.isEmpty( target ) )
372         {
373             args.add( "/target:library" );
374         }
375         else
376         {
377             args.add( "/target:" + target );
378         }
379 
380         // ----------------------------------------------------------------------
381         // remove MS logo from output (not applicable for mono)
382         // ----------------------------------------------------------------------
383         String nologo = compilerArguments.get( "-nologo" );
384 
385         if ( !StringUtils.isEmpty( nologo ) )
386         {
387             args.add( "/nologo" );
388         }
389 
390         // ----------------------------------------------------------------------
391         // add any resource files
392         // ----------------------------------------------------------------------
393         this.addResourceArgs( config, args );
394 
395         // ----------------------------------------------------------------------
396         // add source files
397         // ----------------------------------------------------------------------
398         for ( String sourceFile : sourceFiles )
399         {
400             args.add( sourceFile );
401         }
402 
403         return args.toArray( new String[args.size()] );
404     }
405 
406     private void addResourceArgs( CompilerConfiguration config, List<String> args )
407     {
408         File filteredResourceDir = this.findResourceDir( config );
409         if ( ( filteredResourceDir != null ) && filteredResourceDir.exists() )
410         {
411             DirectoryScanner scanner = new DirectoryScanner();
412             scanner.setBasedir( filteredResourceDir );
413             scanner.setIncludes( DEFAULT_INCLUDES );
414             scanner.addDefaultExcludes();
415             scanner.scan();
416 
417             List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
418             for ( String name : includedFiles )
419             {
420                 File filteredResource = new File( filteredResourceDir, name );
421                 String assemblyResourceName = this.convertNameToAssemblyResourceName( name );
422                 String argLine = "/resource:\"" + filteredResource + "\",\"" + assemblyResourceName + "\"";
423                 if ( config.isDebug() )
424                 {
425                     System.out.println( "adding resource arg line:" + argLine );
426                 }
427                 args.add( argLine );
428 
429             }
430         }
431     }
432 
433     private File findResourceDir( CompilerConfiguration config )
434     {
435         if ( config.isDebug() )
436         {
437             System.out.println( "Looking for resourcesDir" );
438         }
439         
440         Map<String, String> compilerArguments = getCompilerArguments( config );
441         
442         String tempResourcesDirAsString = (String) compilerArguments.get( "-resourceDir" );
443         File filteredResourceDir = null;
444         if ( tempResourcesDirAsString != null )
445         {
446             filteredResourceDir = new File( tempResourcesDirAsString );
447             if ( config.isDebug() )
448             {
449                 System.out.println( "Found resourceDir at: " + filteredResourceDir.toString() );
450             }
451         }
452         else
453         {
454             if ( config.isDebug() )
455             {
456                 System.out.println( "No resourceDir was available." );
457             }
458         }
459         return filteredResourceDir;
460     }
461 
462     private String convertNameToAssemblyResourceName( String name )
463     {
464         return name.replace( File.separatorChar, '.' );
465     }
466 
467     @SuppressWarnings( "deprecation" )
468     private List<CompilerMessage> compileOutOfProcess( File workingDirectory, File target, String executable,
469                                                        String[] args )
470         throws CompilerException
471     {
472         // ----------------------------------------------------------------------
473         // Build the @arguments file
474         // ----------------------------------------------------------------------
475 
476         File file;
477 
478         PrintWriter output = null;
479 
480         try
481         {
482             file = new File( target, ARGUMENTS_FILE_NAME );
483 
484             output = new PrintWriter( new FileWriter( file ) );
485 
486             for ( String arg : args )
487             {
488                 output.println( arg );
489             }
490         }
491         catch ( IOException e )
492         {
493             throw new CompilerException( "Error writing arguments file.", e );
494         }
495         finally
496         {
497             IOUtil.close( output );
498         }
499 
500         // ----------------------------------------------------------------------
501         // Execute!
502         // ----------------------------------------------------------------------
503 
504         Commandline cli = new Commandline();
505 
506         cli.setWorkingDirectory( workingDirectory.getAbsolutePath() );
507 
508         cli.setExecutable( executable );
509 
510         cli.createArgument().setValue( "@" + file.getAbsolutePath() );
511 
512         Writer stringWriter = new StringWriter();
513 
514         StreamConsumer out = new WriterStreamConsumer( stringWriter );
515 
516         StreamConsumer err = new WriterStreamConsumer( stringWriter );
517 
518         int returnCode;
519 
520         List<CompilerMessage> messages;
521 
522         try
523         {
524             returnCode = CommandLineUtils.executeCommandLine( cli, out, err );
525 
526             messages = parseCompilerOutput( new BufferedReader( new StringReader( stringWriter.toString() ) ) );
527         }
528         catch ( CommandLineException e )
529         {
530             throw new CompilerException( "Error while executing the external compiler.", e );
531         }
532         catch ( IOException e )
533         {
534             throw new CompilerException( "Error while executing the external compiler.", e );
535         }
536 
537         if ( returnCode != 0 && messages.isEmpty() )
538         {
539             // TODO: exception?
540             messages.add( new CompilerMessage(
541                 "Failure executing the compiler, but could not parse the error:" + EOL + stringWriter.toString(),
542                 true ) );
543         }
544 
545         return messages;
546     }
547 
548     public static List<CompilerMessage> parseCompilerOutput( BufferedReader bufferedReader )
549         throws IOException
550     {
551         List<CompilerMessage> messages = new ArrayList<CompilerMessage>();
552 
553         String line = bufferedReader.readLine();
554 
555         while ( line != null )
556         {
557             CompilerMessage compilerError = DefaultCSharpCompilerParser.parseLine( line );
558 
559             if ( compilerError != null )
560             {
561                 messages.add( compilerError );
562             }
563 
564             line = bufferedReader.readLine();
565         }
566 
567         return messages;
568     }
569 
570     private String getType( Map<String, String> compilerArguments )
571     {
572         String type = compilerArguments.get( "-target" );
573 
574         if ( StringUtils.isEmpty( type ) )
575         {
576             return "library";
577         }
578 
579         return type;
580     }
581 
582     private String getTypeExtension( CompilerConfiguration configuration )
583         throws CompilerException
584     {
585         String type = getType( configuration.getCustomCompilerArguments() );
586 
587         if ( "exe".equals( type ) || "winexe".equals( type ) )
588         {
589             return "exe";
590         }
591 
592         if ( "library".equals( type ) || "module".equals( type ) )
593         {
594             return "dll";
595         }
596 
597         throw new CompilerException( "Unrecognized type '" + type + "'." );
598     }
599 
600     // added for debug purposes.... 
601     protected static String[] getSourceFiles( CompilerConfiguration config )
602     {
603         Set<String> sources = new HashSet<String>();
604 
605         //Set sourceFiles = null;
606         //was:
607         Set<File> sourceFiles = config.getSourceFiles();
608 
609         if ( sourceFiles != null && !sourceFiles.isEmpty() )
610         {
611             for ( File sourceFile : sourceFiles )
612             {
613                 sources.add( sourceFile.getAbsolutePath() );
614             }
615         }
616         else
617         {
618             for ( String sourceLocation : config.getSourceLocations() )
619             {
620                 if (!new File(sourceLocation).exists())
621                 {
622                     if ( config.isDebug() )
623                     {
624                         System.out.println( "Ignoring not found sourceLocation at: " + sourceLocation );
625                     }
626                     continue;
627                 }
628                 sources.addAll( getSourceFilesForSourceRoot( config, sourceLocation ) );
629             }
630         }
631 
632         String[] result;
633 
634         if ( sources.isEmpty() )
635         {
636             result = new String[0];
637         }
638         else
639         {
640             result = (String[]) sources.toArray( new String[sources.size()] );
641         }
642 
643         return result;
644     }
645 
646     /**
647      * This method is just here to maintain the public api. This is now handled in the parse
648      * compiler output function.
649      *
650      * @author Chris Stevenson
651      * @deprecated
652      */
653     public static CompilerMessage parseLine( String line )
654     {
655         return DefaultCSharpCompilerParser.parseLine( line );
656     }
657 
658     protected static Set<String> getSourceFilesForSourceRoot( CompilerConfiguration config, String sourceLocation )
659     {
660         DirectoryScanner scanner = new DirectoryScanner();
661 
662         scanner.setBasedir( sourceLocation );
663 
664         Set<String> includes = config.getIncludes();
665 
666         if ( includes != null && !includes.isEmpty() )
667         {
668             String[] inclStrs = includes.toArray( new String[includes.size()] );
669             scanner.setIncludes( inclStrs );
670         }
671         else
672         {
673             scanner.setIncludes( new String[]{ "**/*.cs" } );
674         }
675 
676         Set<String> excludes = config.getExcludes();
677 
678         if ( excludes != null && !excludes.isEmpty() )
679         {
680             String[] exclStrs = excludes.toArray( new String[excludes.size()] );
681             scanner.setIncludes( exclStrs );
682         }
683 
684         scanner.scan();
685 
686         String[] sourceDirectorySources = scanner.getIncludedFiles();
687 
688         Set<String> sources = new HashSet<String>();
689 
690         for ( String source : sourceDirectorySources )
691         {
692             File f = new File( sourceLocation, source );
693 
694             sources.add( f.getPath() );
695         }
696 
697         return sources;
698     }
699 }