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