View Javadoc

1   /*
2    * #%L
3    * Native ARchive plugin for Maven
4    * %%
5    * Copyright (C) 2002 - 2014 NAR Maven Plugin developers.
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   * http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package com.github.maven_nar.cpptasks.msvc;
21  
22  import java.io.BufferedWriter;
23  import java.io.File;
24  import java.io.FileWriter;
25  import java.io.IOException;
26  import java.io.Writer;
27  import java.text.MessageFormat;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Comparator;
31  import java.util.Hashtable;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  
36  import org.apache.tools.ant.BuildException;
37  
38  import com.github.maven_nar.cpptasks.CCTask;
39  import com.github.maven_nar.cpptasks.CUtil;
40  import com.github.maven_nar.cpptasks.TargetInfo;
41  import com.github.maven_nar.cpptasks.compiler.CommandLineCompilerConfiguration;
42  import com.github.maven_nar.cpptasks.compiler.CommandLineLinkerConfiguration;
43  import com.github.maven_nar.cpptasks.compiler.ProcessorConfiguration;
44  import com.github.maven_nar.cpptasks.ide.CommentDef;
45  import com.github.maven_nar.cpptasks.ide.DependencyDef;
46  import com.github.maven_nar.cpptasks.ide.ProjectDef;
47  import com.github.maven_nar.cpptasks.ide.ProjectWriter;
48  
49  /**
50   * Writes a Microsoft Visual Studio 97 or Visual Studio 6 project file.
51   *
52   * Status: Collects file list but does not pick
53   * up libraries and settings from project.
54   *
55   * @author curta
56   */
57  public final class MsvcProjectWriter implements ProjectWriter {
58    private static String toProjectName(final String name) {
59      //
60      // some characters are apparently not allowed in VS project names
61      // but have not been able to find them documented
62      // limiting characters to alphas, numerics and hyphens
63      final StringBuffer projectNameBuf = new StringBuffer(name);
64      for (int i = 0; i < projectNameBuf.length(); i++) {
65        final char ch = projectNameBuf.charAt(i);
66        if (!(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')) {
67          projectNameBuf.setCharAt(i, '_');
68        }
69      }
70      return projectNameBuf.toString();
71  
72    }
73  
74    private static void writeComments(final Writer writer, final List<CommentDef> comments) throws IOException {
75      for (final CommentDef commentDef : comments) {
76        final String comment = commentDef.getText();
77        if (comment != null) {
78          int start = 0;
79          for (int end = comment.indexOf('\n'); end != -1; end = comment.indexOf('\n', start)) {
80            writer.write("#" + comment.substring(start, end) + "\r\n");
81            start = end + 1;
82          }
83        }
84      }
85    }
86  
87    private static void writeWorkspaceProject(final Writer writer, final String projectName, final String projectFile,
88        final List<String> dependsOn) throws IOException {
89      writer.write("############################################");
90      writer.write("###################################\r\n\r\n");
91      String file = projectFile;
92      if (!file.startsWith(".") && !file.startsWith("\\") && !file.startsWith("/")) {
93        file = ".\\" + file;
94      }
95      writer.write("Project: \"" + projectName + "\"=\"" + file + "\" - Package Owner=<4>\r\n\r\n");
96  
97      writer.write("Package=<5>\r\n{{{\r\n}}}\r\n\r\n");
98      writer.write("Package=<4>\r\n{{{\r\n");
99      if (dependsOn != null) {
100       for (final String string : dependsOn) {
101         writer.write("    Begin Project Dependency\r\n");
102         writer.write("    Project_Dep_Name " + toProjectName(String.valueOf(string)) + "\r\n");
103         writer.write("    End Project Dependency\r\n");
104       }
105     }
106     writer.write("}}}\r\n\r\n");
107 
108   }
109 
110   /**
111    * Visual Studio version.
112    */
113   private final String version;
114 
115   /**
116    * Constructor.
117    * 
118    * @param versionArg
119    *          String Visual Studio version.
120    */
121   public MsvcProjectWriter(final String versionArg) {
122     this.version = versionArg;
123   }
124 
125   /**
126    * Gets the first recognized compiler from the
127    * compilation targets.
128    * 
129    * @param targets
130    *          compilation targets
131    * @return representative (hopefully) compiler configuration
132    */
133   private CommandLineCompilerConfiguration getBaseCompilerConfiguration(final Map<String, TargetInfo> targets) {
134     //
135     // find first target with an DevStudio C compilation
136     //
137     CommandLineCompilerConfiguration compilerConfig;
138     //
139     // get the first target and assume that it is representative
140     //
141     for (final TargetInfo targetInfo : targets.values()) {
142       final ProcessorConfiguration config = targetInfo.getConfiguration();
143       //
144       // for the first cl compiler
145       //
146       if (config instanceof CommandLineCompilerConfiguration) {
147         compilerConfig = (CommandLineCompilerConfiguration) config;
148         if (compilerConfig.getCompiler() instanceof MsvcCCompiler) {
149           return compilerConfig;
150         }
151       }
152     }
153     return null;
154   }
155 
156   /**
157    * Get alphabetized array of source files.
158    * 
159    * @param sourceList
160    *          list of source files
161    * @return File[] source files
162    */
163   private File[] getSources(final List<File> sourceList) {
164     final File[] sortedSources = new File[sourceList.size()];
165     sourceList.toArray(sortedSources);
166     Arrays.sort(sortedSources, new Comparator<File>() {
167       @Override
168       public int compare(final File o1, final File o2) {
169         return o1.getName().compareTo(o2.getName());
170       }
171     });
172     return sortedSources;
173   }
174 
175   /**
176    * Returns true if the file has an extension that appears in the group filter.
177    * 
178    * @param filter
179    *          String group filter
180    * @param candidate
181    *          File file
182    * @return boolean true if member of group
183    */
184   private boolean isGroupMember(final String filter, final File candidate) {
185     final String fileName = candidate.getName();
186     final int lastDot = fileName.lastIndexOf('.');
187     if (lastDot >= 0 && lastDot < fileName.length() - 1) {
188       final String extension = ";" + fileName.substring(lastDot + 1).toLowerCase() + ";";
189       final String semiFilter = ";" + filter + ";";
190       return semiFilter.contains(extension);
191     }
192     return false;
193   }
194 
195   /**
196    * Writes compiler options.
197    * 
198    * @param writer
199    *          Writer writer
200    * @param isDebug
201    *          true if debug.
202    * @param baseDir
203    *          String base directory
204    * @param compilerConfig
205    *          compiler configuration
206    * @throws IOException
207    *           if error on writing project
208    */
209   private void writeCompileOptions(final Writer writer, final boolean isDebug, final String baseDir,
210       final CommandLineCompilerConfiguration compilerConfig) throws IOException {
211     final StringBuffer baseOptions = new StringBuffer(50);
212     baseOptions.append("# ADD BASE CPP");
213     final StringBuffer options = new StringBuffer(50);
214     options.append("# ADD CPP");
215     final File[] includePath = compilerConfig.getIncludePath();
216     for (final File element : includePath) {
217       options.append(" /I \"");
218       final String relPath = CUtil.getRelativePath(baseDir, element);
219       options.append(CUtil.toWindowsPath(relPath));
220       options.append('"');
221     }
222     final Hashtable<String, String> optionMap = new Hashtable<>();
223 
224     if (isDebug) {
225       //
226       // release options that should be mapped to debug counterparts
227       //
228       optionMap.put("/MT", "/MTd");
229       optionMap.put("/ML", "/MLd");
230       optionMap.put("/MD", "/MDd");
231       optionMap.put("/O2", "/Od");
232       optionMap.put("/O3", "/Od");
233     } else {
234       //
235       // debug options that should be mapped to release counterparts
236       //
237       optionMap.put("/MTD", "/MT");
238       optionMap.put("/MLD", "/ML");
239       optionMap.put("/MDD", "/MD");
240       optionMap.put("/GM", "");
241       optionMap.put("/ZI", "");
242       optionMap.put("/OD", "/O2");
243       optionMap.put("/GZ", "");
244     }
245 
246     final String[] preArgs = compilerConfig.getPreArguments();
247     for (final String preArg : preArgs) {
248       if (preArg.startsWith("/D")) {
249         options.append(" /D ");
250         baseOptions.append(" /D ");
251         final String body = preArg.substring(2);
252         if (preArg.indexOf('=') >= 0) {
253           options.append(body);
254           baseOptions.append(body);
255         } else {
256           final StringBuffer buf = new StringBuffer("\"");
257           if ("NDEBUG".equals(body) || "_DEBUG".equals(body)) {
258             if (isDebug) {
259               buf.append("_DEBUG");
260             } else {
261               buf.append("NDEBUG");
262             }
263           } else {
264             buf.append(body);
265           }
266           buf.append("\"");
267           options.append(buf);
268           baseOptions.append(buf);
269         }
270       } else if (!preArg.startsWith("/I")) {
271         String option = preArg;
272         final String key = option.toUpperCase(Locale.US);
273         if (optionMap.containsKey(key)) {
274           option = optionMap.get(key);
275         }
276         options.append(" ");
277         options.append(option);
278         baseOptions.append(" ");
279         baseOptions.append(option);
280       }
281     }
282     baseOptions.append("\r\n");
283     options.append("\r\n");
284     writer.write(baseOptions.toString());
285     writer.write(options.toString());
286 
287   }
288 
289   private void writeConfig(final Writer writer, final boolean isDebug, final List<DependencyDef> dependencies,
290       final String basePath, final CommandLineCompilerConfiguration compilerConfig, final TargetInfo linkTarget,
291       final Map<String, TargetInfo> targets) throws IOException {
292     writer.write("# PROP BASE Use_MFC 0\r\n");
293 
294     String configType = "Release";
295     String configInt = "0";
296     String configMacro = "NDEBUG";
297     if (isDebug) {
298       configType = "Debug";
299       configInt = "1";
300       configMacro = "_DEBUG";
301     }
302 
303     writer.write("# PROP BASE Use_Debug_Libraries ");
304     writer.write(configInt);
305     writer.write("\r\n# PROP BASE Output_Dir \"");
306     writer.write(configType);
307     writer.write("\"\r\n");
308     writer.write("# PROP BASE Intermediate_Dir \"");
309     writer.write(configType);
310     writer.write("\"\r\n");
311     writer.write("# PROP BASE Target_Dir \"\"\r\n");
312     writer.write("# PROP Use_MFC 0\r\n");
313     writer.write("# PROP Use_Debug_Libraries ");
314     writer.write(configInt);
315     writer.write("\r\n# PROP Output_Dir \"");
316     writer.write(configType);
317     writer.write("\"\r\n");
318     writer.write("# PROP Intermediate_Dir \"");
319     writer.write(configType);
320     writer.write("\"\r\n");
321     writer.write("# PROP Target_Dir \"\"\r\n");
322     writeCompileOptions(writer, isDebug, basePath, compilerConfig);
323     writer.write("# ADD BASE MTL /nologo /D \"" + configMacro + "\" /mktyplib203 /o NUL /win32\r\n");
324     writer.write("# ADD MTL /nologo /D \"" + configMacro + "\" /mktyplib203 /o NUL /win32\r\n");
325     writer.write("# ADD BASE RSC /l 0x409 /d \"" + configMacro + "\"\r\n");
326     writer.write("# ADD RSC /l 0x409 /d \"" + configMacro + "\"\r\n");
327     writer.write("BSC32=bscmake.exe\r\n");
328     writer.write("# ADD BASE BSC32 /nologo\r\n");
329     writer.write("# ADD BSC32 /nologo\r\n");
330     writer.write("LINK32=link.exe\r\n");
331     writeLinkOptions(writer, isDebug, dependencies, basePath, linkTarget, targets);
332   }
333 
334   /**
335    * Writes link options.
336    * 
337    * @param writer
338    *          Writer writer
339    * @param basePath
340    *          String base path
341    * @param dependencies
342    *          project dependencies, used to suppress explicit linking.
343    * @param linkTarget
344    *          TargetInfo link target
345    * @param targets
346    *          Hashtable all targets
347    * @throws IOException
348    *           if unable to write to project file
349    */
350   private void writeLinkOptions(final Writer writer, final boolean isDebug, final List<DependencyDef> dependencies,
351       final String basePath, final TargetInfo linkTarget, final Map<String, TargetInfo> targets) throws IOException {
352 
353     final StringBuffer baseOptions = new StringBuffer(100);
354     final StringBuffer options = new StringBuffer(100);
355     baseOptions.append("# ADD BASE LINK32");
356     options.append("# ADD LINK32");
357 
358     final ProcessorConfiguration config = linkTarget.getConfiguration();
359     if (config instanceof CommandLineLinkerConfiguration) {
360       final CommandLineLinkerConfiguration linkConfig = (CommandLineLinkerConfiguration) config;
361 
362       final File[] linkSources = linkTarget.getAllSources();
363       for (final File linkSource : linkSources) {
364         //
365         // if file was not compiled or otherwise generated
366         //
367         if (targets.get(linkSource.getName()) == null) {
368           //
369           // if source appears to be a system library or object file
370           // just output the name of the file (advapi.lib for example)
371           // otherwise construct a relative path.
372           //
373           String relPath = linkSource.getName();
374           //
375           // check if file comes from a project dependency
376           // if it does it should not be explicitly linked
377           boolean fromDependency = false;
378           if (relPath.indexOf(".") > 0) {
379             final String baseName = relPath.substring(0, relPath.indexOf("."));
380             for (final DependencyDef depend : dependencies) {
381               if (baseName.compareToIgnoreCase(depend.getName()) == 0) {
382                 fromDependency = true;
383               }
384             }
385           }
386           if (!fromDependency) {
387             if (!CUtil.isSystemPath(linkSource)) {
388               relPath = CUtil.getRelativePath(basePath, linkSource);
389             }
390             //
391             // if path has an embedded space then
392             // must quote
393             if (relPath.indexOf(' ') > 0) {
394               options.append(" \"");
395               options.append(CUtil.toWindowsPath(relPath));
396               options.append("\"");
397             } else {
398               options.append(' ');
399               options.append(CUtil.toWindowsPath(relPath));
400             }
401           }
402         }
403       }
404       final String[] preArgs = linkConfig.getPreArguments();
405       for (final String preArg : preArgs) {
406         if (isDebug || !preArg.equals("/DEBUG")) {
407           options.append(' ');
408           options.append(preArg);
409           baseOptions.append(' ');
410           baseOptions.append(preArg);
411         }
412       }
413       final String[] endArgs = linkConfig.getEndArguments();
414       for (final String endArg : endArgs) {
415         options.append(' ');
416         options.append(endArg);
417         baseOptions.append(' ');
418         baseOptions.append(endArg);
419       }
420     }
421     baseOptions.append("\r\n");
422     options.append("\r\n");
423     writer.write(baseOptions.toString());
424     writer.write(options.toString());
425   }
426 
427   /**
428    * Writes "This is not a makefile" warning.
429    * 
430    * @param writer
431    *          Writer writer
432    * @param projectName
433    *          String project name
434    * @param targtype
435    *          String target type
436    * @throws IOException
437    *           if error writing project
438    */
439 
440   private void writeMessage(final Writer writer, final String projectName, final String targtype) throws IOException {
441     writer.write("!MESSAGE This is not a valid makefile. ");
442     writer.write("To build this project using NMAKE,\r\n");
443     writer.write("!MESSAGE use the Export Makefile command and run\r\n");
444     writer.write("!MESSAGE \r\n");
445     writer.write("!MESSAGE NMAKE /f \"");
446     writer.write(projectName);
447     writer.write(".mak\".\r\n");
448     writer.write("!MESSAGE \r\n");
449     writer.write("!MESSAGE You can specify a configuration when running NMAKE\r\n");
450     writer.write("!MESSAGE by defining the macro CFG on the command line. ");
451     writer.write("For example:\r\n");
452     writer.write("!MESSAGE \r\n");
453     writer.write("!MESSAGE NMAKE /f \"");
454     writer.write(projectName);
455     writer.write(".mak\" CFG=\"");
456     writer.write(projectName);
457     writer.write(" - Win32 Debug\"\r\n");
458     writer.write("!MESSAGE \r\n");
459     writer.write("!MESSAGE Possible choices for configuration are:\r\n");
460     writer.write("!MESSAGE \r\n");
461     final String pattern = "!MESSAGE \"{0} - Win32 {1}\" (based on \"{2}\")\r\n";
462     writer.write(MessageFormat.format(pattern, new Object[] {
463         projectName, "Release", targtype
464     }));
465     writer.write(MessageFormat.format(pattern, new Object[] {
466         projectName, "Debug", targtype
467     }));
468     writer.write("!MESSAGE \r\n");
469     writer.write("\r\n");
470 
471   }
472 
473   /**
474    * Writes a project definition file.
475    * 
476    * @param fileName
477    *          File name base, writer may append appropriate extension
478    * @param task
479    *          cc task for which to write project
480    * @param projectDef
481    *          project element
482    * @param files
483    *          source files
484    * @param targets
485    *          compilation targets
486    * @param linkTarget
487    *          link target
488    * @throws IOException
489    *           if error writing project file
490    */
491   @Override
492   public void writeProject(final File fileName, final CCTask task, final ProjectDef projectDef, final List<File> files,
493       final Map<String, TargetInfo> targets, final TargetInfo linkTarget) throws IOException {
494 
495     //
496     // some characters are apparently not allowed in VS project names
497     // but have not been able to find them documented
498     // limiting characters to alphas, numerics and hyphens
499     String projectName = projectDef.getName();
500     if (projectName != null) {
501       projectName = toProjectName(projectName);
502     } else {
503       projectName = toProjectName(fileName.getName());
504     }
505 
506     final String basePath = fileName.getAbsoluteFile().getParent();
507 
508     final File dspFile = new File(fileName + ".dsp");
509     if (!projectDef.getOverwrite() && dspFile.exists()) {
510       throw new BuildException("Not allowed to overwrite project file " + dspFile.toString());
511     }
512     final File dswFile = new File(fileName + ".dsw");
513     if (!projectDef.getOverwrite() && dswFile.exists()) {
514       throw new BuildException("Not allowed to overwrite project file " + dswFile.toString());
515     }
516 
517     final CommandLineCompilerConfiguration compilerConfig = getBaseCompilerConfiguration(targets);
518     if (compilerConfig == null) {
519       throw new BuildException("Unable to generate Visual Studio project " + "when Microsoft C++ is not used.");
520     }
521 
522     Writer writer = new BufferedWriter(new FileWriter(dspFile));
523     writer.write("# Microsoft Developer Studio Project File - Name=\"");
524     writer.write(projectName);
525     writer.write("\" - Package Owner=<4>\r\n");
526     writer.write("# Microsoft Developer Studio Generated Build File, Format Version ");
527     writer.write(this.version);
528     writer.write("\r\n");
529     writer.write("# ** DO NOT EDIT **\r\n\r\n");
530 
531     writeComments(writer, projectDef.getComments());
532 
533     final String outputType = task.getOuttype();
534     final String subsystem = task.getSubsystem();
535     String targtype = "Win32 (x86) Dynamic-Link Library";
536     String targid = "0x0102";
537     if ("executable".equals(outputType)) {
538       if ("console".equals(subsystem)) {
539         targtype = "Win32 (x86) Console Application";
540         targid = "0x0103";
541       } else {
542         targtype = "Win32 (x86) Application";
543         targid = "0x0101";
544       }
545     } else if ("static".equals(outputType)) {
546       targtype = "Win32 (x86) Static Library";
547       targid = "0x0104";
548     }
549     writer.write("# TARGTYPE \"");
550     writer.write(targtype);
551     writer.write("\" ");
552     writer.write(targid);
553     writer.write("\r\n\r\nCFG=");
554 
555     writer.write(projectName + " - Win32 Debug");
556     writer.write("\r\n");
557 
558     writeMessage(writer, projectName, targtype);
559 
560     writer.write("# Begin Project\r\n");
561     if (this.version.equals("6.00")) {
562       writer.write("# PROP AllowPerConfigDependencies 0\r\n");
563     }
564     writer.write("# PROP Scc_ProjName \"\"\r\n");
565     writer.write("# PROP Scc_LocalPath \"\"\r\n");
566     writer.write("CPP=cl.exe\r\n");
567     writer.write("MTL=midl.exe\r\n");
568     writer.write("RSC=rc.exe\r\n");
569 
570     writer.write("\r\n!IF  \"$(CFG)\" == \"" + projectName + " - Win32 Release\"\r\n");
571 
572     writeConfig(writer, false, projectDef.getDependencies(), basePath, compilerConfig, linkTarget, targets);
573 
574     writer.write("\r\n!ELSEIF  \"$(CFG)\" == \"" + projectName + " - Win32 Debug\"\r\n");
575 
576     writeConfig(writer, true, projectDef.getDependencies(), basePath, compilerConfig, linkTarget, targets);
577 
578     writer.write("\r\n!ENDIF\r\n");
579 
580     writer.write("# Begin Target\r\n\r\n");
581     writer.write("# Name \"" + projectName + " - Win32 Release\"\r\n");
582     writer.write("# Name \"" + projectName + " - Win32 Debug\"\r\n");
583 
584     final File[] sortedSources = getSources(files);
585 
586     if (this.version.equals("6.00")) {
587       final String sourceFilter = "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat";
588       final String headerFilter = "h;hpp;hxx;hm;inl";
589       final String resourceFilter = "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe";
590 
591       writer.write("# Begin Group \"Source Files\"\r\n\r\n");
592       writer.write("# PROP Default_Filter \"" + sourceFilter + "\"\r\n");
593 
594       for (final File sortedSource1 : sortedSources) {
595         if (!isGroupMember(headerFilter, sortedSource1) && !isGroupMember(resourceFilter, sortedSource1)) {
596           writeSource(writer, basePath, sortedSource1);
597         }
598       }
599       writer.write("# End Group\r\n");
600 
601       writer.write("# Begin Group \"Header Files\"\r\n\r\n");
602       writer.write("# PROP Default_Filter \"" + headerFilter + "\"\r\n");
603 
604       for (final File sortedSource : sortedSources) {
605         if (isGroupMember(headerFilter, sortedSource)) {
606           writeSource(writer, basePath, sortedSource);
607         }
608       }
609       writer.write("# End Group\r\n");
610 
611       writer.write("# Begin Group \"Resource Files\"\r\n\r\n");
612       writer.write("# PROP Default_Filter \"" + resourceFilter + "\"\r\n");
613 
614       for (final File sortedSource : sortedSources) {
615         if (isGroupMember(resourceFilter, sortedSource)) {
616           writeSource(writer, basePath, sortedSource);
617         }
618       }
619       writer.write("# End Group\r\n");
620 
621     } else {
622       for (final File sortedSource : sortedSources) {
623         writeSource(writer, basePath, sortedSource);
624       }
625     }
626 
627     writer.write("# End Target\r\n");
628     writer.write("# End Project\r\n");
629     writer.close();
630 
631     //
632     // write workspace file
633     //
634     writer = new BufferedWriter(new FileWriter(dswFile));
635     writeWorkspace(writer, projectDef, projectName, dspFile);
636     writer.close();
637 
638   }
639 
640   /**
641    * Writes the entry for one source file in the project.
642    * 
643    * @param writer
644    *          Writer writer
645    * @param basePath
646    *          String base path for project
647    * @param groupMember
648    *          File project source file
649    * @throws IOException
650    *           if error writing project file
651    */
652   private void writeSource(final Writer writer, final String basePath, final File groupMember) throws IOException {
653     writer.write("# Begin Source File\r\n\r\nSOURCE=");
654     String relativePath = CUtil.getRelativePath(basePath, groupMember);
655     //
656     // if relative path is just a name (hello.c) then
657     // make it .\hello.c
658     if (!relativePath.startsWith(".") && !relativePath.contains(":") && !relativePath.startsWith("\\")) {
659       relativePath = ".\\" + relativePath;
660     }
661     writer.write(CUtil.toWindowsPath(relativePath));
662     writer.write("\r\n# End Source File\r\n");
663   }
664 
665   private void writeWorkspace(final Writer writer, final ProjectDef project, final String projectName,
666       final File dspFile) throws IOException {
667 
668     writer.write("Microsoft Developer Studio Workspace File, Format Version ");
669     writer.write(this.version);
670     writer.write("\r\n");
671     writer.write("# WARNING: DO NOT EDIT OR DELETE");
672     writer.write(" THIS WORKSPACE FILE!\r\n\r\n");
673 
674     writeComments(writer, project.getComments());
675 
676     final List<DependencyDef> dependencies = project.getDependencies();
677     final List<String> projectDeps = new ArrayList<>();
678     final String basePath = dspFile.getParent();
679     for (final DependencyDef dep : dependencies) {
680       if (dep.getFile() != null) {
681         final String projName = toProjectName(dep.getName());
682         projectDeps.add(projName);
683         final String depProject = CUtil
684             .toWindowsPath(CUtil.getRelativePath(basePath, new File(dep.getFile() + ".dsp")));
685         writeWorkspaceProject(writer, projName, depProject, dep.getDependsList());
686       }
687     }
688 
689     writeWorkspaceProject(writer, projectName, dspFile.getName(), projectDeps);
690 
691     writer.write("############################################");
692     writer.write("###################################\r\n\r\n");
693 
694     writer.write("Global:\r\n\r\nPackage=<5>\r\n{{{\r\n}}}");
695     writer.write("\r\n\r\nPackage=<3>\r\n{{{\r\n}}}\r\n\r\n");
696 
697     writer.write("########################################");
698     writer.write("#######################################\r\n\r\n");
699 
700   }
701 }