1 /**
2 	Generator for MonoD project files
3 	
4 	Copyright: © 2013-2013 rejectedsoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dub.generators.monod;
9 
10 import dub.compilers.compiler;
11 import dub.generators.generator;
12 import dub.internal.utils;
13 import dub.internal.vibecompat.core.file;
14 import dub.internal.vibecompat.core.log;
15 import dub.package_;
16 import dub.packagemanager;
17 import dub.project;
18 
19 import std.algorithm;
20 import std.array;
21 import std.conv;
22 import std.format;
23 import std.uuid;
24 import std.exception;
25 
26 
27 // TODO: handle pre/post build commands
28 
29 class MonoDGenerator : ProjectGenerator {
30 	private {
31 		Project m_app;
32 		PackageManager m_pkgMgr;
33 		string[string] m_projectUuids;
34 		bool m_singleProject = true;
35 		Config[] m_allConfigs;
36 	}
37 	
38 	this(Project app, PackageManager mgr)
39 	{
40 		m_app = app;
41 		m_pkgMgr = mgr;
42 		m_allConfigs ~= Config("Debug", "AnyCPU", "Any CPU");
43 	}
44 	
45 	void generateProject(GeneratorSettings settings)
46 	{
47 		logWarn("Note that the latest Mono-D has direct support for building DUB projects. It is recommended to directly open package.json instead of generating a Mono-D project.");
48 
49 		auto buildsettings = settings.buildSettings;
50 		m_app.addBuildSettings(buildsettings, settings.platform, settings.config);
51 
52 		prepareGeneration(buildsettings);
53 
54 		logDebug("About to generate projects for %s, with %s direct dependencies.", m_app.mainPackage().name, m_app.mainPackage().dependencies().length);
55 		generateProjects(m_app.mainPackage(), settings);
56 		generateSolution(settings);
57 
58 		finalizeGeneration(buildsettings, true);
59 	}
60 	
61 	private void generateSolution(GeneratorSettings settings)
62 	{
63 		auto sln = openFile(m_app.mainPackage().name ~ ".sln", FileMode.CreateTrunc);
64 		scope(exit) sln.close();
65 
66 		// Writing solution file
67 		logDebug("About to write to .sln file.");
68 
69 		// Solution header
70 		sln.put('\n');
71 		sln.put("Microsoft Visual Studio Solution File, Format Version 11.00\n");
72 		sln.put("# Visual Studio 2010\n");
73 
74 		generateSolutionEntry(sln, settings, m_app.mainPackage);
75 		if( !m_singleProject )
76 			performOnDependencies(m_app.mainPackage, pack => generateSolutionEntry(sln, settings, pack));
77 		
78 		sln.put("Global\n");
79 
80 		// configuration platforms
81 		sln.put("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n");
82 		foreach(config; m_allConfigs)
83 			sln.formattedWrite("\t\t%s|%s = %s|%s\n", config.configName, config.platformName2,
84 				config.configName, config.platformName2);
85 		sln.put("\tEndGlobalSection\n");
86 
87 		// configuration platforms per project
88 		sln.put("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n");
89 		auto projectUuid = guid(m_app.mainPackage.name);
90 		foreach(config; m_allConfigs)
91 			foreach(s; ["ActiveCfg", "Build.0"])
92 				sln.formattedWrite("\t\t%s.%s|%s.%s = %s|%s\n",
93 					projectUuid, config.configName, config.platformName2, s,
94 					config.configName, config.platformName2);
95 		// TODO: for all dependencies
96 		sln.put("\tEndGlobalSection\n");
97 		
98 		// solution properties
99 		sln.put("\tGlobalSection(SolutionProperties) = preSolution\n");
100 		sln.put("\t\tHideSolutionNode = FALSE\n");
101 		sln.put("\tEndGlobalSection\n");
102 
103 		// monodevelop properties
104 		sln.put("\tGlobalSection(MonoDevelopProperties) = preSolution\n");
105 		sln.formattedWrite("\t\tStartupItem = %s\n", "monodtest/monodtest.dproj");
106 		sln.put("\tEndGlobalSection\n");
107 
108 		sln.put("EndGlobal\n");
109 	}
110 	
111 	private void generateSolutionEntry(RangeFile ret, GeneratorSettings settings, const Package pack)
112 	{
113 		auto projUuid = generateUUID();
114 		auto projName = pack.name;
115 		auto projPath = pack.name ~ ".dproj";
116 		auto projectUuid = guid(projName);
117 		
118 		// Write project header, like so
119 		// Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "derelict", "..\inbase\source\derelict.visualdproj", "{905EF5DA-649E-45F9-9C15-6630AA815ACB}"
120 		ret.formattedWrite("Project(\"%s\") = \"%s\", \"%s\", \"%s\"\n",
121 			projUuid, projName, projPath, projectUuid);
122 
123 		if( !m_singleProject ){
124 			if(pack.dependencies.length > 0) {
125 				ret.put("	ProjectSection(ProjectDependencies) = postProject\n");
126 				foreach(id, dependency; pack.dependencies) {
127 					// TODO: clarify what "uuid = uuid" should mean
128 					auto uuid = guid(id);
129 					ret.formattedWrite("		%s = %s\n", uuid, uuid);
130 				}
131 				ret.put("	EndProjectSection\n");
132 			}
133 		}
134 
135 		ret.put("EndProject\n");
136 	}
137 
138 	private void generateProjects(in Package pack, GeneratorSettings settings)
139 	{
140 		bool[const(Package)] visited;
141 
142 		void generateRec(in Package p){
143 			if( p in visited ) return;
144 			visited[p] = true;
145 
146 			generateProject(p, settings);
147 
148 			if( !m_singleProject )
149 				performOnDependencies(p, &generateRec);
150 		}
151 		generateRec(pack);
152 	}
153 		
154 	private void generateProject(in Package pack, GeneratorSettings settings)
155 	{
156 		logDebug("About to write to '%s.dproj' file", pack.name);
157 		auto sln = openFile(pack.name ~ ".dproj", FileMode.CreateTrunc);
158 		scope(exit) sln.close();
159 
160 		sln.put("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
161 		sln.put("<Project DefaultTargets=\"Build\" ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n");
162 		// TODO: property groups
163 
164 		auto projName = pack.name;
165 
166 		auto buildsettings = settings.buildSettings;
167 		m_app.addBuildSettings(buildsettings, settings.platform, m_app.getDefaultConfiguration(settings.platform));
168 
169 		// Mono-D does not have a setting for string import paths
170 	    settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.all & ~BuildSetting.stringImportPaths);
171 
172 		sln.put("  <PropertyGroup>\n");
173 	    sln.put("    <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>\n");
174     	sln.put("    <Platform Condition=\" '$(Platform)' == '' \">AnyCPU</Platform>\n");
175     	sln.put("    <ProductVersion>10.0.0</ProductVersion>\n");
176     	sln.put("    <SchemaVersion>2.0</SchemaVersion>\n");
177     	sln.formattedWrite("    <ProjectGuid>%s</ProjectGuid>\n", guid(pack.name));
178     	sln.put("    <PreferOneStepBuild>True</PreferOneStepBuild>\n");
179     	sln.put("    <UseDefaultCompiler>True</UseDefaultCompiler>\n");
180     	sln.put("    <IncrementalLinking>True</IncrementalLinking>\n");
181     	sln.put("    <Compiler>DMD2</Compiler>\n");
182     	if( !buildsettings.versions.empty ){
183 			sln.put("    <VersionIds>\n");
184 			sln.put("      <VersionIds>\n");
185 			foreach(ver; buildsettings.versions)
186 				sln.formattedWrite("        <String>%s</String>\n", ver);
187 			sln.put("      </VersionIds>\n");
188 			sln.put("    </VersionIds>\n");
189 		}
190 		if( !buildsettings.importPaths.empty ){
191 	    	sln.put("    <Includes>\n");
192 	    	sln.put("      <Includes>\n");
193 	    	foreach(dir; buildsettings.importPaths)
194 	    		sln.formattedWrite("        <Path>%s</Path>\n", dir);
195 	    	sln.put("      </Includes>\n");
196 	    	sln.put("    </Includes>\n");
197 	    }
198 	    if( !buildsettings.libs.empty ){
199 	    	sln.put("    <Libs>\n");
200 	    	sln.put("      <Libs>\n");
201 	    	foreach(dir; buildsettings.libs)
202 	    		sln.formattedWrite("        <Lib>%s</Lib>\n", settings.platform.platform.canFind("windows") ? dir ~ ".lib" : dir);
203 	    	sln.put("      </Libs>\n");
204 	    	sln.put("    </Libs>\n");
205 	    }
206 		sln.formattedWrite("    <ExtraCompilerArguments>%s</ExtraCompilerArguments>\n", buildsettings.dflags.join(" "));
207 		sln.formattedWrite("    <ExtraLinkerArguments>%s</ExtraLinkerArguments>\n", buildsettings.lflags.join(" "));
208 		sln.put("  </PropertyGroup>\n");
209 
210 		void generateProperties(Config config)
211 		{
212 			sln.formattedWrite("  <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == '%s|%s' \">\n",
213 				config.configName, config.platformName);
214 			
215     		sln.put("    <DebugSymbols>True</DebugSymbols>\n");
216     		auto outpath = Path(buildsettings.targetPath).toNativeString();
217 			sln.formattedWrite("    <OutputPath>%s</OutputPath>\n", outpath.length ? outpath : ".");
218 			sln.put("    <Externalconsole>True</Externalconsole>\n");
219  			sln.put("    <Target>Executable</Target>\n");
220     		sln.formattedWrite("    <OutputName>%s</OutputName>\n", buildsettings.targetName);
221 			sln.put("    <UnittestMode>False</UnittestMode>\n");
222 			sln.formattedWrite("    <ObjectsDirectory>%s</ObjectsDirectory>\n", (Path("obj/")~config.configName).toNativeString());
223 			sln.put("    <DebugLevel>0</DebugLevel>\n");
224 			sln.put("  </PropertyGroup>\n");
225 		}
226 
227 		foreach(config; m_allConfigs)
228 			generateProperties(config);
229 
230 
231 		bool[const(Package)] visited;
232 		void generateSourceEntry(Path path, Path base_path, bool compile = true)
233 		{
234 			auto rel_path = path.relativeTo(pack.path);
235 			rel_path.normalize();
236 
237 			Path pretty_path;
238 			foreach (i; 0 .. rel_path.length)
239 				if (rel_path[i] != "..") {
240 					pretty_path = rel_path[i .. $];
241 					break;
242 				}
243 			string kind = compile ? "Compile" : "None";
244 
245 			if (base_path == pretty_path) {
246 				sln.formattedWrite("    <%s Include=\"%s\" />\n", kind, rel_path.toNativeString());
247 			} else {
248 				sln.formattedWrite("    <%s Include=\"%s\">\n", kind, rel_path.toNativeString());
249 				sln.formattedWrite("      <Link>%s</Link>\n", pretty_path.toNativeString());
250 				sln.formattedWrite("    </%s>\n", kind);
251 			}
252 		}
253 
254 		sln.put("  <ItemGroup>\n");
255 		// add source files
256 		foreach (s; buildsettings.sourceFiles) {
257 			auto sp = Path(s);
258 			if (!sp.absolute) sp = pack.path ~ sp;
259 			generateSourceEntry(sp, pack.path);
260 		}
261 		// TODO: add all files in stringImportFolders
262 		// add package.json files
263 		foreach (p; m_app.getTopologicalPackageList())
264 			generateSourceEntry(p.packageInfoFile, pack.path, false);
265 		sln.put("  </ItemGroup>\n");
266 		sln.put("</Project>");
267 	}
268 		
269 	void performOnDependencies(const Package main, void delegate(const Package pack) op)
270 	{
271 		bool[const(Package)] visited;
272 		void perform_rec(const Package parent_pack){
273 			foreach(id, dependency; parent_pack.dependencies){
274 				logDiagnostic("Retrieving package %s from package manager.", id);
275 				auto pack = m_pkgMgr.getBestPackage(id, dependency);
276 				if( pack in visited ) continue;
277 				visited[pack] = true;
278 				if(pack is null) {
279 				 	logWarn("Package %s (%s) could not be retrieved continuing...", id, to!string(dependency));
280 					continue;
281 				}
282 				logDiagnostic("Performing on retrieved package %s", pack.name);
283 				op(pack);
284 				perform_rec(pack);
285 			}
286 		}
287 
288 		perform_rec(main);
289 	}
290 	
291 	string generateUUID()
292 	const {
293 		return "{" ~ randomUUID().toString() ~ "}";
294 	}
295 	
296 	string guid(string projectName)
297 	{
298 		if(projectName !in m_projectUuids)
299 			m_projectUuids[projectName] = generateUUID();
300 		return m_projectUuids[projectName];
301 	}
302 }
303 
304 struct Config {
305 	string configName;
306 	string platformName;
307 	string platformName2;
308 }