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