View Javadoc

1   package com.github.maven_nar;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Collections;
8   import java.util.Comparator;
9   import java.util.LinkedHashSet;
10  import java.util.List;
11  import java.util.ListIterator;
12  import java.util.Map.Entry;
13  import java.util.Set;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
18  import org.apache.maven.plugin.MojoExecutionException;
19  import org.apache.maven.plugin.MojoFailureException;
20  import org.apache.maven.plugins.annotations.Parameter;
21  import org.apache.tools.ant.types.Environment.Variable;
22  import org.codehaus.plexus.util.StringUtils;
23  
24  import com.github.maven_nar.cpptasks.CCTask;
25  import com.github.maven_nar.cpptasks.CompilerDef;
26  import com.github.maven_nar.cpptasks.LinkerDef;
27  import com.github.maven_nar.cpptasks.types.SystemIncludePath;
28  import com.google.common.collect.Sets;
29  
30  public class Msvc {
31  
32    @Parameter
33    private File home;
34  
35    private AbstractNarMojo mojo;
36  
37    private final Set<String> paths = new LinkedHashSet<>();
38  
39    @Parameter
40    private String version;
41  
42    @Parameter
43    private File windowsSdkHome;
44  
45    @Parameter
46    private String windowsSdkVersion;
47  
48    @Parameter
49    private String tempPath;
50  
51    private File windowsHome;
52    private String toolPathWindowsSDK;
53    private String toolPathLinker;
54    private List<File> sdkIncludes = new ArrayList<>();
55    private List<File> sdkLibs = new ArrayList<>();
56    private Set<String> libsRequired = Sets.newHashSet("ucrt", "um", "shared", "winrt");
57  
58    private boolean addIncludePath(final CCTask task, final File home, final String subDirectory)
59        throws MojoExecutionException {
60      if (home == null) {
61        return false;
62      }
63      final File file = new File(home, subDirectory);
64      if (file.exists())
65        return addIncludePathToTask(task, file);
66  
67      return false;
68    }
69  
70    private boolean addIncludePathToTask(final CCTask task, final File file)
71         throws MojoExecutionException {
72      try {
73        final SystemIncludePath includePath = task.createSysIncludePath();
74        final String fullPath = file.getCanonicalPath();
75        includePath.setPath(fullPath);
76        return true;
77      } catch (final IOException e) {
78        throw new MojoExecutionException("Unable to add system include: " + file.getAbsolutePath(), e);
79      }
80    }
81  
82    private boolean addPath(final File home, final String path) {
83      if (home != null) {
84        final File directory = new File(home, path);
85        if (directory.exists()) {
86          try {
87            final String fullPath = directory.getCanonicalPath();
88            this.paths.add(fullPath);
89            return true;
90          } catch (final IOException e) {
91            throw new IllegalArgumentException("Unable to get path: " + directory, e);
92          }
93        }
94      }
95      return false;
96    }
97  
98    public void configureCCTask(final CCTask task) throws MojoExecutionException {
99      if (OS.WINDOWS.equals(mojo.getOS())
100           && "msvc".equalsIgnoreCase(mojo.getLinker().getName())) {
101       addIncludePath(task, this.home, "VC/include");
102       addIncludePath(task, this.home, "VC/atlmfc/include");
103       if (compareVersion(this.windowsSdkVersion, "7.1A") <= 0) {
104         addIncludePath(task, this.windowsSdkHome, "include");
105       } else {
106         for (File sdkInclude : sdkIncludes)
107           addIncludePathToTask(task, sdkInclude);
108 
109       }
110       task.addEnv(getPathVariable());
111       // TODO: supporting running with clean environment - addEnv sets
112       // newEnvironemnt by default
113       // task.setNewenvironment(false);
114       Variable envVariable = new Variable();
115       // cl needs SystemRoot env var set, otherwise D8037 is raised (bogus
116       // message)
117       // - https://msdn.microsoft.com/en-us/library/bb385201.aspx
118       // -
119       // http://stackoverflow.com/questions/10560779/cl-exe-when-launched-via-createprocess-does-not-seem-to-have-write-permissions
120       envVariable.setKey("SystemRoot");
121       envVariable.setValue(this.windowsHome.getAbsolutePath());
122       task.addEnv(envVariable);
123       // cl needs TMP otherwise D8050 is raised c1xx.dll
124       envVariable = new Variable();
125       envVariable.setKey("TMP");
126       envVariable.setValue(getTempPath());
127       task.addEnv(envVariable);
128     }
129   }
130 
131   public void configureLinker(final LinkerDef linker) throws MojoExecutionException {
132     final String os = mojo.getOS();
133     if (os.equals(OS.WINDOWS) && "msvc".equalsIgnoreCase(mojo.getLinker().getName())) {
134       final String arch = mojo.getArchitecture();
135 
136       // Visual Studio
137       if ("x86".equals(arch)) {
138         linker.addLibraryDirectory(this.home, "VC/lib");
139         linker.addLibraryDirectory(this.home, "VC/atlmfc/lib");
140       } else {
141         linker.addLibraryDirectory(this.home, "VC/lib/" + arch);
142         linker.addLibraryDirectory(this.home, "VC/atlmfc/lib/" + arch);
143       }
144       // Windows SDK
145       String sdkArch = arch;
146       if ("amd64".equals(arch)) {
147         sdkArch = "x64";
148       }
149       // 6 lib ?+ lib/x86 or lib/x64
150       if (compareVersion(this.windowsSdkVersion, "8.0") < 0) {
151         if ("x86".equals(arch)) {
152           linker.addLibraryDirectory(this.windowsSdkHome, "lib");
153         } else {
154           linker.addLibraryDirectory(this.windowsSdkHome, "lib/" + sdkArch);
155         }
156       } else
157         for (File sdkLib : sdkLibs)
158           linker.addLibraryDirectory(sdkLib, sdkArch);
159 
160     }
161   }
162 
163   private String getTempPath(){
164     if( null == tempPath ){
165       tempPath = System.getenv("TMP");
166       if( null == tempPath )
167         tempPath = System.getenv("TEMP");
168       if( null == tempPath )
169         tempPath = "C:\\Temp";
170     }
171     return tempPath;
172   }
173 
174   public Variable getPathVariable() {
175     if (this.paths.isEmpty()) {
176       return null;
177     }
178     final Variable pathVariable = new Variable();
179     pathVariable.setKey("PATH");
180     pathVariable.setValue(StringUtils.join(this.paths.iterator(), File.pathSeparator));
181     return pathVariable;
182   }
183 
184   public String getVersion() {
185     return this.version;
186   }
187 
188   public String getWindowsSdkVersion() {
189     return this.windowsSdkVersion;
190   }
191 
192   private void init() throws MojoFailureException, MojoExecutionException {
193     final String mojoOs = this.mojo.getOS();
194     if (NarUtil.isWindows() && OS.WINDOWS.equals(mojoOs)) {
195       windowsHome = new File(System.getenv("SystemRoot"));
196       initVisualStudio();
197       initWindowsSdk();
198       initPath();
199     } else {
200       this.version = "";
201       this.windowsSdkVersion = "";
202     }
203   }
204 
205   private void initPath() throws MojoExecutionException {
206     final String mojoArchitecture = this.mojo.getArchitecture();
207     final String osArchitecture = NarUtil.getArchitecture(null);
208     // 32 bit build on 64 bit OS can be built with 32 bit tool, or 64 bit tool
209     // in amd64_x86 - currently defaulting to prefer 64 bit tools - match os
210     final boolean matchMojo = false;
211     // TODO: toolset architecture
212     // match os - os x86 mojo(x86 / x86_amd64); os x64 mojo(amd64_x86 / amd64);
213     // 32bit - force 32 on 64bit mojo(x86 / x86_amd64)
214     // match mojo - os x86 is as above; os x64 mojo (x86 / amd64)
215 
216     // Cross tools first if necessary, platform tools second, more generic tools
217     // later
218     if (!osArchitecture.equals(mojoArchitecture) && !matchMojo) {
219       if (!addPath(this.home, "VC/bin/" + osArchitecture + "_" + mojoArchitecture)) {
220         throw new MojoExecutionException("Unable to find compiler for architecture " + mojoArchitecture + ".\n"
221             + new File(this.home, "VC/bin/" + osArchitecture + "_" + mojoArchitecture));
222       }
223       toolPathLinker = new File(this.home, "VC/bin/" + osArchitecture + "_" + mojoArchitecture).getAbsolutePath();
224     }
225     if (null == toolPathLinker) {
226       if ("amd64".equals(mojoArchitecture))
227         toolPathLinker = new File(this.home, "VC/bin/amd64").getAbsolutePath();
228       else
229         toolPathLinker = new File(this.home, "VC/bin").getAbsolutePath();
230     }
231     if ("amd64".equals(osArchitecture) && !matchMojo) {
232       addPath(this.home, "VC/bin/amd64");
233     } else {
234       addPath(this.home, "VC/bin");
235     }
236     addPath(this.home, "VC/VCPackages");
237     addPath(this.home, "Common7/Tools");
238     addPath(this.home, "Common7/IDE");
239 
240     // 64 bit tools if present are preferred
241     if (compareVersion(this.windowsSdkVersion, "7.1A") <= 0) {
242       if ("amd64".equals(osArchitecture) && !matchMojo) {
243         addPath(this.windowsSdkHome, "bin/x64");
244       }
245       addPath(this.windowsSdkHome, "bin");
246     } else {
247       if ("amd64".equals(osArchitecture) && !matchMojo) {
248         addPath(this.windowsSdkHome, "bin/x64");
249       }
250       addPath(this.windowsSdkHome, "bin/x86");
251     }
252     if ("amd64".equals(mojoArchitecture)) {
253       toolPathWindowsSDK = new File(this.windowsSdkHome, "bin/x64").getAbsolutePath();
254     } else if (compareVersion(this.windowsSdkVersion, "7.1A") <= 0) {
255       toolPathWindowsSDK = new File(this.windowsSdkHome, "bin").getAbsolutePath();
256     } else {
257       toolPathWindowsSDK = new File(this.windowsSdkHome, "bin/x86").getAbsolutePath();
258     }
259 
260     // clearing the path, add back the windows system folders
261     addPath(this.windowsHome, "System32");
262     addPath(this.windowsHome, "");
263     addPath(this.windowsHome, "System32/wbem");
264   }
265 
266   private void initVisualStudio() throws MojoFailureException, MojoExecutionException {
267     mojo.getLog().debug(" -- Searching for usable VisualStudio ");
268     if (this.version != null && this.version.trim().length() > 1) {
269       String internalVersion;
270       Pattern r = Pattern.compile("(\\d+)\\.*(\\d)");
271       Matcher matcher = r.matcher(this.version);
272       if (matcher.find()) {
273         internalVersion = matcher.group(1) + matcher.group(2);
274         this.version = matcher.group(1) + "." + matcher.group(2);
275       } else {
276         throw new MojoExecutionException("msvc.version must be the internal version in the form 10.0 or 120");
277       }
278       if (this.home == null) {
279         final String commontToolsVar = System.getenv("VS" + internalVersion + "COMNTOOLS");
280         if (commontToolsVar != null && commontToolsVar.trim().length() > 0) {
281           final File commonToolsDirectory = new File(commontToolsVar);
282           if (commonToolsDirectory.exists()) {
283             this.home = commonToolsDirectory.getParentFile().getParentFile();
284           }
285         }
286         // TODO: else Registry might be more reliable but adds dependency to be
287         // able to acccess - HKLM\SOFTWARE\Microsoft\Visual Studio\Major.Minor:InstallDir
288       }
289       mojo.getLog()
290           .debug(String.format(" VisualStudio %1s (%2s) found %3s ", this.version, internalVersion, this.home));
291     } else {
292       this.version = "";
293       for (final Entry<String, String> entry : System.getenv().entrySet()) {
294         final String key = entry.getKey();
295         final String value = entry.getValue();
296         final Pattern versionPattern = Pattern.compile("VS(\\d+)(\\d)COMNTOOLS");
297         final Matcher matcher = versionPattern.matcher(key);
298         if (matcher.matches()) {
299           final String version = matcher.group(1) + "." + matcher.group(2);
300           if (versionStringComparator.compare(version, this.version) > 0) {
301             final File commonToolsDirectory = new File(value);
302             if (commonToolsDirectory.exists()) {
303               this.version = version;
304               this.home = commonToolsDirectory.getParentFile().getParentFile();
305               mojo.getLog().debug(
306                   String.format(" VisualStudio %1s (%2s) found %3s ", this.version,
307                       matcher.group(1) + matcher.group(2), this.home));
308             }
309           }
310         }
311       }
312       if (this.version.length() == 0) {
313         final TextStream out = new StringTextStream();
314         final TextStream err = new StringTextStream();
315         final TextStream dbg = new StringTextStream();
316 
317         NarUtil.runCommand("link", new String[] {
318           "/?"
319         }, null, null, out, err, dbg, null, true);
320         final Pattern p = Pattern.compile("(\\d+\\.\\d+)\\.\\d+(\\.\\d+)?");
321         final Matcher m = p.matcher(out.toString());
322         if (m.find()) {
323           this.version = m.group(1);
324           mojo.getLog().debug(
325               String.format(" VisualStudio Not found but link runs and reports version %1s (%2s)", this.version,
326                   m.group(0)));
327         } else {
328           throw new MojoExecutionException(
329               "msvc.version not specified and no VS<Version>COMNTOOLS environment variable can be found");
330         }
331       }
332     }
333   }
334 
335   private final Comparator<String> versionStringComparator = new Comparator<String>() {
336     @Override
337     public int compare(String o1, String o2) {
338       DefaultArtifactVersion version1 = new DefaultArtifactVersion(o1);
339       DefaultArtifactVersion version2 = new DefaultArtifactVersion(o2);
340       return version1.compareTo(version2);
341     }
342   };
343 
344   private final Comparator<File> versionComparator = new Comparator<File>() {
345     @Override
346     public int compare(File o1, File o2) {
347       // will be sorted smallest first, so we need to invert the order of
348       // the objects
349       String firstDir = o2.getName(), secondDir = o1.getName();
350       if (firstDir.charAt(0) == 'v') { // remove 'v' and 'A' at the end
351         firstDir = firstDir.substring(1, firstDir.length() - 1);
352         secondDir = secondDir.substring(1, secondDir.length() - 1);
353       }
354       // impossible that two dirs are the same
355       String[] firstVersionString = firstDir.split("\\."), secondVersionString = secondDir.split("\\.");
356 
357       int maxIdx = Math.min(firstVersionString.length, secondVersionString.length);
358       int deltaVer;
359       try {
360         for (int i = 0; i < maxIdx; i++)
361           if ((deltaVer = Integer.parseInt(firstVersionString[i]) - Integer.parseInt(secondVersionString[i])) != 0)
362             return deltaVer;
363 
364       } catch (NumberFormatException e) {
365         return firstDir.compareTo(secondDir);
366       }
367       if (firstVersionString.length > maxIdx) // 10.0.150 > 10.0
368         return 1;
369       else if (secondVersionString.length > maxIdx) // 10.0 < 10.0.150
370         return -1;
371       return 0; // impossible that they are the same
372     }
373   };
374   private boolean foundSDK = false;
375   private void initWindowsSdk() throws MojoExecutionException {
376     if(this.windowsSdkVersion != null && this.windowsSdkVersion.trim().equals(""))
377       this.windowsSdkVersion = null;
378 
379     mojo.getLog().debug(" -- Searching for usable WindowSDK ");
380     // newer first: 10 -> 8.1 -> 8.0 -> 7.1 and look for libs specified
381     for (final File directory : Arrays.asList(
382             new File("C:/Program Files (x86)/Windows Kits"),
383             new File("C:/Program Files (x86)/Microsoft SDKs/Windows"),
384             new File("C:/Program Files/Windows Kits"),
385             new File("C:/Program Files/Microsoft SDKs/Windows") )) {
386       if (directory.exists()) {
387         final File[] kitDirectories = directory.listFiles();
388         Arrays.sort(kitDirectories, versionComparator);
389         if (kitDirectories != null) {
390           for (final File kitDirectory : kitDirectories) {
391 
392             if (new File(kitDirectory, "Include").exists()) {
393               // legacy SDK
394               String kitVersion = kitDirectory.getName();
395               if (kitVersion.charAt(0) == 'v') {
396                 kitVersion = kitVersion.substring(1);
397               }
398               if (this.windowsSdkVersion!=null && compareVersion(kitVersion,this.windowsSdkVersion)>0)
399                 continue; // skip versions higher than the previous version
400               mojo.getLog()
401                   .debug(String.format(" WindowSDK %1s found %2s", kitVersion, kitDirectory.getAbsolutePath()));
402               if (kitVersion.matches("\\d+\\.\\d+?[A-Z]?")) {
403                 // windows <= 8.1
404                 legacySDK(kitDirectory);
405               } else if (kitVersion.matches("\\d+?")) {
406                 // windows 10 SDK supports
407                 addNewSDKLibraries(kitDirectory);
408               }
409 	        }
410 	      }
411           if (libsRequired.size() == 0) // need it here to break out of the outer loop
412               break;
413 	    }
414 	  }
415     }
416     if (!foundSDK)
417       throw new MojoExecutionException("msvc.windowsSdkVersion not specified and versions cannot be found");
418     mojo.getLog().debug(String.format(" Using WindowSDK %1s found %2s", this.windowsSdkVersion, this.windowsSdkHome));
419   }
420 
421   private void addNewSDKLibraries(final File kitDirectory) {
422     // multiple installs
423     List<File> kitVersionDirectories = Arrays.asList(new File(kitDirectory, "Include").listFiles());
424     Collections.sort(kitVersionDirectories,  versionComparator);
425     ListIterator<File> kitVersionDirectoriesIt = kitVersionDirectories.listIterator();
426     File kitVersionDirectory = null;
427     while (kitVersionDirectoriesIt.hasNext() && (kitVersionDirectory = kitVersionDirectoriesIt.next()) != null) {
428       if (new File(kitVersionDirectory, "ucrt").exists()) {
429         break;
430       }
431     }
432 
433     if (kitVersionDirectory != null) {
434       String version = kitVersionDirectory.getName();
435       mojo.getLog().debug(String.format(" Latest Win %1s KitDir at %2s", kitVersionDirectory.getName(), kitVersionDirectory.getAbsolutePath()));
436       // add the libraries found:
437       File includeDir = new File(kitDirectory, "Include/" + version);
438       File libDir = new File(kitDirectory, "Lib/" + version);
439       addSDKLibs(includeDir, libDir);
440     }
441   }
442 
443   private void setKit(File home) {
444     if (!foundSDK) {
445       if(this.windowsSdkVersion==null)
446         this.windowsSdkVersion = home.getName();
447       if(this.windowsSdkHome==null)
448         this.windowsSdkHome = home;
449       foundSDK=true;
450     }
451   }
452 
453   private void legacySDK(final File kitDirectory) {
454     File includeDir = new File(kitDirectory, "Include");
455     File libDir = new File(kitDirectory, "Lib");
456     if(includeDir.exists() && libDir.exists()){
457       File usableLibDir = null;
458       for( final File libSubDir : libDir.listFiles()){
459         final File um = new File(libSubDir,"um");
460         if( um.exists())
461           usableLibDir = libSubDir;
462       }
463       if( null == usableLibDir )
464         usableLibDir = libDir.listFiles()[0];
465       
466       addSDKLibs(includeDir, usableLibDir);
467       setKit(kitDirectory);
468     }
469   }
470 
471   private void addSDKLibs(File includeDir, File libdir) {
472     final File[] libs = includeDir.listFiles();
473     for (final File libIncludeDir : libs) {
474       // <libName> <include path> <lib path>
475       if (libsRequired.remove(libIncludeDir.getName())) {
476         mojo.getLog().debug(String.format(" Using directory %1s for library %2s", libIncludeDir.getAbsolutePath(), libIncludeDir.getName()));
477         sdkIncludes.add(libIncludeDir);
478         sdkLibs.add(new File(libdir, libIncludeDir.getName()));
479       }
480     }
481   }
482 
483   public void setMojo(final AbstractNarMojo mojo) throws MojoFailureException, MojoExecutionException {
484     if (mojo != this.mojo) {
485       this.mojo = mojo;
486       init();
487     }
488   }
489 
490   @Override
491   public String toString() {
492     return this.home + "\n" + this.windowsSdkHome;
493   }
494 
495   public String getToolPath() {
496     return this.toolPathLinker;
497   }
498 
499   public String getSDKToolPath() {
500     return this.toolPathWindowsSDK;
501   }
502 
503   public void setToolPath(CompilerDef compilerDef, String name) {
504     if ("res".equals(name) || "mc".equals(name) || "idl".equals(name)) {
505       compilerDef.setToolPath(this.toolPathWindowsSDK);
506     } else {
507       compilerDef.setToolPath(this.toolPathLinker);
508     }
509   }
510 
511   public int compareVersion(Object o1, Object o2) {
512     String version1 = (String) o1;
513     String version2 = (String) o2;
514 
515     VersionTokenizer tokenizer1 = new VersionTokenizer(version1);
516     VersionTokenizer tokenizer2 = new VersionTokenizer(version2);
517 
518     int number1 = 0, number2 = 0;
519     String suffix1 = "", suffix2 = "";
520 
521     while (tokenizer1.MoveNext()) {
522       if (!tokenizer2.MoveNext()) {
523         do {
524           number1 = tokenizer1.getNumber();
525           suffix1 = tokenizer1.getSuffix();
526           if (number1 != 0 || suffix1.length() != 0) {
527             // Version one is longer than number two, and non-zero
528             return 1;
529           }
530         } while (tokenizer1.MoveNext());
531 
532         // Version one is longer than version two, but zero
533         return 0;
534       }
535 
536       number1 = tokenizer1.getNumber();
537       suffix1 = tokenizer1.getSuffix();
538       number2 = tokenizer2.getNumber();
539       suffix2 = tokenizer2.getSuffix();
540 
541       if (number1 < number2) {
542         // Number one is less than number two
543         return -1;
544       }
545       if (number1 > number2) {
546         // Number one is greater than number two
547         return 1;
548       }
549 
550       boolean empty1 = suffix1.length() == 0;
551       boolean empty2 = suffix2.length() == 0;
552 
553       if (empty1 && empty2)
554         continue; // No suffixes
555       if (empty1)
556         return 1; // First suffix is empty (1.2 > 1.2b)
557       if (empty2)
558         return -1; // Second suffix is empty (1.2a < 1.2)
559 
560       // Lexical comparison of suffixes
561       int result = suffix1.compareTo(suffix2);
562       if (result != 0)
563         return result;
564 
565     }
566     if (tokenizer2.MoveNext()) {
567       do {
568         number2 = tokenizer2.getNumber();
569         suffix2 = tokenizer2.getSuffix();
570         if (number2 != 0 || suffix2.length() != 0) {
571           // Version one is longer than version two, and non-zero
572           return -1;
573         }
574       } while (tokenizer2.MoveNext());
575 
576       // Version two is longer than version one, but zero
577       return 0;
578     }
579     return 0;
580   }
581 
582   // VersionTokenizer.java
583   class VersionTokenizer {
584     private final String _versionString;
585     private final int _length;
586 
587     private int _position;
588     private int _number;
589     private String _suffix;
590     private boolean _hasValue;
591 
592     public int getNumber() {
593       return _number;
594     }
595 
596     public String getSuffix() {
597       return _suffix;
598     }
599 
600     public boolean hasValue() {
601       return _hasValue;
602     }
603 
604     public VersionTokenizer(String versionString) {
605       if (versionString == null)
606         throw new IllegalArgumentException("versionString is null");
607 
608       _versionString = versionString;
609       _length = versionString.length();
610     }
611 
612     public boolean MoveNext() {
613       _number = 0;
614       _suffix = "";
615       _hasValue = false;
616 
617       // No more characters
618       if (_position >= _length)
619         return false;
620 
621       _hasValue = true;
622 
623       while (_position < _length) {
624         char c = _versionString.charAt(_position);
625         if (c < '0' || c > '9')
626           break;
627         _number = _number * 10 + (c - '0');
628         _position++;
629       }
630 
631       int suffixStart = _position;
632 
633       while (_position < _length) {
634         char c = _versionString.charAt(_position);
635         if (c == '.')
636           break;
637         _position++;
638       }
639 
640       _suffix = _versionString.substring(suffixStart, _position);
641 
642       if (_position < _length)
643         _position++;
644 
645       return true;
646     }
647   }
648 }