1 /**
2 	Generator for VisualD project files
3 	
4 	Copyright: © 2012-2013 Matthias Dondorff
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff
7 */
8 module dub.generators.visuald;
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 // Dubbing is developing dub...
28 //version = DUBBING;
29 
30 // TODO: handle pre/post build commands
31 
32 
33 class VisualDGenerator : ProjectGenerator {
34 	private {
35 		Project m_app;
36 		PackageManager m_pkgMgr;
37 		string[string] m_projectUuids;
38 		bool m_combinedProject;
39 	}
40 	
41 	this(Project app, PackageManager mgr, bool combined_project)
42 	{
43 		m_combinedProject = combined_project;
44 		m_app = app;
45 		m_pkgMgr = mgr;
46 	}
47 	
48 	void generateProject(GeneratorSettings settings)
49 	{
50 		auto buildsettings = settings.buildSettings;
51 		m_app.addBuildSettings(buildsettings, settings.platform, settings.config);
52 		
53 		prepareGeneration(buildsettings);
54 
55 		logDebug("About to generate projects for %s, with %s direct dependencies.", m_app.mainPackage().name, m_app.mainPackage().dependencies().length);
56 		generateProjects(m_app.mainPackage(), settings);
57 		generateSolution(settings);
58 		logInfo("VisualD project generated.");
59 
60 		finalizeGeneration(buildsettings, true);
61 	}
62 	
63 	private {
64 		void generateSolution(GeneratorSettings settings)
65 		{
66 			auto ret = appender!(char[])();
67 			auto configs = m_app.getPackageConfigs(settings.platform, settings.config);
68 			
69 			// Solution header
70 			ret.formattedWrite("
71 Microsoft Visual Studio Solution File, Format Version 11.00
72 # Visual Studio 2010");
73 
74 			generateSolutionEntry(ret, m_app.mainPackage, settings);
75 			if (!m_combinedProject) {
76 				performOnDependencies(m_app.mainPackage, configs, (pack){
77 					generateSolutionEntry(ret, pack, settings);
78 				});
79 			}
80 			
81 			// Global section contains configurations
82 			ret.formattedWrite("
83 Global
84   GlobalSection(SolutionConfigurationPlatforms) = preSolution
85     Debug|Win32 = Debug|Win32
86     Release|Win32 = Release|Win32
87     Unittest|Win32 = Unittest|Win32
88   EndGlobalSection
89   GlobalSection(ProjectConfigurationPlatforms) = postSolution");
90 			
91 			generateSolutionConfig(ret, m_app.mainPackage());
92 			
93 			// TODO: for all dependencies
94 			
95 			ret.formattedWrite("
96   GlobalSection(SolutionProperties) = preSolution
97     HideSolutionNode = FALSE
98   EndGlobalSection
99 EndGlobal");
100 
101 			// Writing solution file
102 			logDebug("About to write to .sln file with %s bytes", to!string(ret.data().length));
103 			auto sln = openFile(solutionFileName(), FileMode.CreateTrunc);
104 			scope(exit) sln.close();
105 			sln.put(ret.data());
106 			sln.flush();
107 		}
108 
109 		void generateSolutionEntry(Appender!(char[]) ret, const Package pack, GeneratorSettings settings)
110 		{
111 			auto projUuid = generateUUID();
112 			auto projName = pack.name;
113 			auto projPath = projFileName(pack);
114 			auto projectUuid = guid(projName);
115 			
116 			// Write project header, like so
117 			// Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "derelict", "..\inbase\source\derelict.visualdproj", "{905EF5DA-649E-45F9-9C15-6630AA815ACB}"
118 			ret.formattedWrite("\nProject(\"%s\") = \"%s\", \"%s\", \"%s\"",
119 				projUuid, projName, projPath, projectUuid);
120 
121 			if (!m_combinedProject) {
122 				void addDepsRec(in Package p)
123 				{
124 					foreach(id, dependency; p.dependencies) {
125 						auto deppack = m_app.getDependency(id, true);
126 						if (!deppack) continue;
127 						if (isHeaderOnlyPackage(deppack, settings)) {
128 							addDepsRec(deppack);
129 						} else if (!m_app.isRedundantDependency(p, deppack)) {
130 							// TODO: clarify what "uuid = uuid" should mean
131 							auto uuid = guid(id);
132 							ret.formattedWrite("\n			%s = %s", uuid, uuid);
133 						}
134 					}
135 				}
136 
137 				if(pack.dependencies.length > 0) {
138 					ret.formattedWrite("
139 		ProjectSection(ProjectDependencies) = postProject");
140 					addDepsRec(pack);
141 					ret.formattedWrite("
142 		EndProjectSection");
143 				}
144 			}
145 			
146 			ret.formattedWrite("\nEndProject");
147 		}
148 
149 		void generateSolutionConfig(Appender!(char[]) ret, const Package pack) {
150 			const string[] sub = [ "ActiveCfg", "Build.0" ];
151 			const string[] conf = [ "Debug|Win32", "Release|Win32" /*, "Unittest|Win32" */];
152 			auto projectUuid = guid(pack.name());
153 			foreach(c; conf)
154 				foreach(s; sub)
155 					formattedWrite(ret, "\n\t\t%s.%s.%s = %s", to!string(projectUuid), c, s, c);
156 		}
157 		
158 		void generateProjects(const Package main, GeneratorSettings settings) {
159 		
160 			// TODO: cyclic check
161 			auto configs = m_app.getPackageConfigs(settings.platform, settings.config);
162 			
163 			generateProj(main, settings);
164 			
165 			if (!m_combinedProject) {
166 				bool[string] generatedProjects;
167 				generatedProjects[main.name] = true;
168 				performOnDependencies(main, configs, (const Package dependency) {
169 					if(dependency.name in generatedProjects)
170 						return;
171 					generateProj(dependency, settings);
172 				} );
173 			}
174 		}
175 
176 		bool isHeaderOnlyPackage(in Package pack, in GeneratorSettings settings)
177 		const {
178 			auto configs = m_app.getPackageConfigs(settings.platform, settings.config);
179 			auto pbuildsettings = pack.getBuildSettings(settings.platform, configs[pack.name]);
180 			if (!pbuildsettings.sourceFiles.any!(f => f.endsWith(".d"))())
181 				return true;
182 			return false;
183 		}
184 		
185 		void generateProj(const Package pack, GeneratorSettings settings)
186 		{
187 			int i = 0;
188 			auto ret = appender!(char[])();
189 			
190 			auto projName = pack.name;
191 			auto project_file_dir = m_app.mainPackage.path ~ projFileName(pack).parentPath;
192 			ret.put("<DProject>\n");
193 			ret.formattedWrite("  <ProjectGuid>%s</ProjectGuid>\n", guid(projName));
194 	
195 			// Several configurations (debug, release, unittest)
196 			generateProjectConfiguration(ret, pack, "debug", settings);
197 			generateProjectConfiguration(ret, pack, "release", settings);
198 			generateProjectConfiguration(ret, pack, "unittest", settings);
199 
200 			// Add all files
201 			auto configs = m_app.getPackageConfigs(settings.platform, settings.config);
202 			auto files = pack.getBuildSettings(settings.platform, configs[pack.name]);
203 			bool[SourceFile] sourceFiles;
204 			void addSourceFile(Path file_path, Path structure_path, bool build)
205 			{
206 				SourceFile sf;
207 				sf.filePath = file_path;
208 				sf.structurePath = structure_path;
209 				if (build) {
210 					sf.build = false;
211 					if (sf in sourceFiles) sourceFiles.remove(sf);
212 				} else {
213 					sf.build = true;
214 					if (sf in sourceFiles) return;
215 				}
216 				sf.build = build;
217 				sourceFiles[sf] = true;
218 			}
219 			if (m_combinedProject) {
220 				bool[const(Package)] basePackagesAdded;
221 
222 				// add all package.json files to the project
223 				// and all source files
224 				performOnDependencies(pack, configs, (prj) {
225 					void addFile(string s, bool build) {
226 						auto sp = Path(s);
227 						if( !sp.absolute ) sp = prj.path ~ sp;
228 						// regroup in Folder by base package
229 						addSourceFile(sp.relativeTo(project_file_dir), Path(prj.basePackage().name) ~ sp.relativeTo(prj.path), build);
230 					}
231 
232 					string[] prjFiles;
233 
234 					// Avoid multiples package.json when using sub-packages.
235 					// Only add the package info file if no other package/sub-package from the same base package
236 					// has been seen yet.
237 					{
238 						const(Package) base = prj.basePackage();
239 
240 						if (base !in basePackagesAdded) {
241 							prjFiles ~= prj.packageInfoFile.toNativeString();
242 							basePackagesAdded[base] = true;
243 						}
244 					}
245 
246 					auto settings = prj.getBuildSettings(settings.platform, configs[prj.name]);
247 					foreach (f; prjFiles) addFile(f, false);
248 					foreach (f; settings.sourceFiles) addFile(f, true);
249 					foreach (f; settings.importFiles) addFile(f, false);
250 					foreach (f; settings.stringImportFiles) addFile(f, false);
251 				});
252 			}
253 
254 			void addFile(string s, bool build) {
255 				auto sp = Path(s);
256 				if( !sp.absolute ) sp = pack.path ~ sp;
257 				addSourceFile(sp.relativeTo(project_file_dir), sp.relativeTo(pack.path), build);
258 			}
259 			addFile(pack.packageInfoFile.toNativeString(), false);
260 			foreach(s; files.sourceFiles) addFile(s, true);
261 			foreach(s; files.importFiles) addFile(s, false);
262 			foreach(s; files.stringImportFiles) addFile(s, false);
263 
264 			// Create folders and files
265 			ret.formattedWrite("  <Folder name=\"%s\">", getPackageFileName(pack));
266 			Path lastFolder;
267 			foreach(source; sortedSources(sourceFiles.keys)) {
268 				logDebug("source looking at %s", source.structurePath);
269 				auto cur = source.structurePath[0 .. source.structurePath.length-1];
270 				if(lastFolder != cur) {
271 					size_t same = 0;
272 					foreach(idx; 0..min(lastFolder.length, cur.length))
273 						if(lastFolder[idx] != cur[idx]) break;
274 						else same = idx+1;
275 
276 					const decrease = lastFolder.length - min(lastFolder.length, same);
277 					const increase = cur.length - min(cur.length, same);
278 
279 					foreach(unused; 0..decrease)
280 						ret.put("\n    </Folder>");
281 					foreach(idx; 0..increase)
282 						ret.formattedWrite("\n    <Folder name=\"%s\">", cur[same + idx].toString());
283 					lastFolder = cur;
284 				}
285 				ret.formattedWrite("\n      <File %spath=\"%s\" />", source.build ? "" : "tool=\"None\" ", source.filePath.toNativeString());
286 			}
287 			// Finalize all open folders
288 			foreach(unused; 0..lastFolder.length)
289 				ret.put("\n    </Folder>");
290 			ret.put("\n  </Folder>\n</DProject>");
291 
292 			logDebug("About to write to '%s.visualdproj' file %s bytes", getPackageFileName(pack), ret.data().length);
293 			auto proj = openFile(projFileName(pack), FileMode.CreateTrunc);
294 			scope(exit) proj.close();
295 			proj.put(ret.data());
296 			proj.flush();
297 		}
298 		
299 		void generateProjectConfiguration(Appender!(char[]) ret, const Package pack, string type, GeneratorSettings settings)
300 		{
301 			auto project_file_dir = m_app.mainPackage.path ~ projFileName(pack).parentPath;
302 			auto configs = m_app.getPackageConfigs(settings.platform, settings.config);
303 			auto buildsettings = settings.buildSettings;
304 			auto pbuildsettings = pack.getBuildSettings(settings.platform, configs[pack.name]);
305 			m_app.addBuildSettings(buildsettings, settings.platform, settings.config, pack);
306 			m_app.addBuildTypeSettings(buildsettings, settings.platform, type);
307 			settings.compiler.extractBuildOptions(buildsettings);
308 			
309 			string[] getSettings(string setting)(){ return __traits(getMember, buildsettings, setting); }
310 			string[] getPathSettings(string setting)()
311 			{
312 				auto settings = getSettings!setting();
313 				auto ret = new string[settings.length];
314 				foreach (i; 0 .. settings.length) ret[i] = '"' ~ (Path(settings[i]).relativeTo(project_file_dir)).toNativeString() ~ '"';
315 				return ret;
316 			}
317 			
318 			foreach(architecture; settings.platform.architecture) {
319 				string arch;
320 				switch(architecture) {
321 					default: logWarn("Unsupported platform('%s'), defaulting to x86", architecture); goto case;
322 					case "x86": arch = "Win32"; break;
323 					case "x86_64": arch = "x64"; break;
324 				}
325 				ret.formattedWrite("  <Config name=\"%s\" platform=\"%s\">\n", to!string(type), arch);
326 
327 				// FIXME: handle compiler options in an abstract way instead of searching for DMD specific flags
328 			
329 				// debug and optimize setting
330 				ret.formattedWrite("    <symdebug>%s</symdebug>\n", buildsettings.options & BuildOptions.debugInfo ? "1" : "0");
331 				ret.formattedWrite("    <optimize>%s</optimize>\n", buildsettings.options & BuildOptions.optimize ? "1" : "0");
332 				ret.formattedWrite("    <useInline>%s</useInline>\n", buildsettings.options & BuildOptions.inline ? "1" : "0");
333 				ret.formattedWrite("    <release>%s</release>\n", buildsettings.options & BuildOptions.releaseMode ? "1" : "0");
334 
335 				// Lib or exe?
336 				enum 
337 				{
338 					Executable = 0,
339 					StaticLib = 1,
340 					DynamicLib = 2
341 				}
342 
343 				int output_type = StaticLib; // library
344 				string output_ext = "lib";
345 				if (pbuildsettings.targetType == TargetType.executable)
346 				{
347 					output_type = Executable;
348 					output_ext = "exe";
349 				}
350 				else if (pbuildsettings.targetType == TargetType.dynamicLibrary)
351 				{
352 					output_type = DynamicLib;
353 					output_ext = "dll";
354 				}
355 				string debugSuffix = type == "debug" ? "_d" : "";
356 				auto bin_path = pack is m_app.mainPackage ? Path(pbuildsettings.targetPath) : Path(".dub/lib/");
357 				bin_path.endsWithSlash = true;
358 				ret.formattedWrite("    <lib>%s</lib>\n", output_type);
359 				ret.formattedWrite("    <exefile>%s%s%s.%s</exefile>\n", bin_path.toNativeString(), pbuildsettings.targetName, debugSuffix, output_ext);
360 
361 				// include paths and string imports
362 				string imports = join(getPathSettings!"importPaths"(), " ");
363 				string stringImports = join(getPathSettings!"stringImportPaths"(), " ");
364 				ret.formattedWrite("    <imppath>%s</imppath>\n", imports);
365 				ret.formattedWrite("    <fileImppath>%s</fileImppath>\n", stringImports);
366 
367 				ret.formattedWrite("    <program>%s</program>\n", "$(DMDInstallDir)windows\\bin\\dmd.exe"); // FIXME: use the actually selected compiler!
368 				ret.formattedWrite("    <additionalOptions>%s</additionalOptions>\n", getSettings!"dflags"().join(" "));
369 
370 				// Add version identifiers
371 				string versions = join(getSettings!"versions"(), " ");
372 				ret.formattedWrite("    <versionids>%s</versionids>\n", versions);
373 
374 				// Add libraries, system libs need to be suffixed by ".lib".
375 				string linkLibs = join(map!(a => a~".lib")(getSettings!"libs"()), " ");
376 				string addLinkFiles = join(getSettings!"sourceFiles"().filter!(s => s.endsWith(".lib"))(), " ");
377 				if (output_type != StaticLib) ret.formattedWrite("    <libfiles>%s %s phobos.lib</libfiles>\n", linkLibs, addLinkFiles);
378 
379 				// Unittests
380 				ret.formattedWrite("    <useUnitTests>%s</useUnitTests>\n", buildsettings.options & BuildOptions.unittests ? "1" : "0");
381 
382 				// compute directory for intermediate files (need dummy/ because of how -op determines the resulting path)
383 				auto relpackpath = pack.path.relativeTo(project_file_dir);
384 				uint ndummy = 0;
385 				foreach (i; 0 .. relpackpath.length)
386 					if (relpackpath[i] == "..") ndummy++;
387 				string intersubdir = (ndummy*2 > relpackpath.length ? replicate("dummy/", ndummy*2-relpackpath.length) : "") ~ getPackageFileName(pack);
388 		
389 				ret.put("    <obj>0</obj>\n");
390 				ret.put("    <link>0</link>\n");
391 				ret.put("    <subsystem>0</subsystem>\n");
392 				ret.put("    <multiobj>0</multiobj>\n");
393 				ret.put("    <singleFileCompilation>2</singleFileCompilation>\n");
394 				ret.put("    <oneobj>0</oneobj>\n");
395 				ret.put("    <trace>0</trace>\n");
396 				ret.put("    <quiet>0</quiet>\n");
397 				ret.formattedWrite("    <verbose>%s</verbose>\n", buildsettings.options & BuildOptions.verbose ? "1" : "0");
398 				ret.put("    <vtls>0</vtls>\n");
399 				ret.put("    <cpu>0</cpu>\n");
400 				ret.formattedWrite("    <isX86_64>%s</isX86_64>\n", arch == "x64" ? 1 : 0);
401 				ret.put("    <isLinux>0</isLinux>\n");
402 				ret.put("    <isOSX>0</isOSX>\n");
403 				ret.put("    <isWindows>0</isWindows>\n");
404 				ret.put("    <isFreeBSD>0</isFreeBSD>\n");
405 				ret.put("    <isSolaris>0</isSolaris>\n");
406 				ret.put("    <scheduler>0</scheduler>\n");
407 				ret.put("    <useDeprecated>0</useDeprecated>\n");
408 				ret.put("    <useAssert>0</useAssert>\n");
409 				ret.put("    <useInvariants>0</useInvariants>\n");
410 				ret.put("    <useIn>0</useIn>\n");
411 				ret.put("    <useOut>0</useOut>\n");
412 				ret.put("    <useArrayBounds>0</useArrayBounds>\n");
413 				ret.formattedWrite("    <noboundscheck>%s</noboundscheck>\n", buildsettings.options & BuildOptions.noBoundsChecks ? "1" : "0");
414 				ret.put("    <useSwitchError>0</useSwitchError>\n");
415 				ret.put("    <preservePaths>1</preservePaths>\n");
416 				ret.formattedWrite("    <warnings>%s</warnings>\n", buildsettings.options & BuildOptions.warningsAsErrors ? "1" : "0");
417 				ret.formattedWrite("    <infowarnings>%s</infowarnings>\n", buildsettings.options & BuildOptions.warnings ? "1" : "0");
418 				ret.formattedWrite("    <checkProperty>%s</checkProperty>\n", buildsettings.options & BuildOptions.property ? "1" : "0");
419 				ret.formattedWrite("    <genStackFrame>%s</genStackFrame>\n", buildsettings.options & BuildOptions.alwaysStackFrame ? "1" : "0");
420 				ret.put("    <pic>0</pic>\n");
421 				ret.formattedWrite("    <cov>%s</cov>\n", buildsettings.options & BuildOptions.coverage ? "1" : "0");
422 				ret.put("    <nofloat>0</nofloat>\n");
423 				ret.put("    <Dversion>2</Dversion>\n");
424 				ret.formattedWrite("    <ignoreUnsupportedPragmas>%s</ignoreUnsupportedPragmas>\n", buildsettings.options & BuildOptions.ignoreUnknownPragmas ? "1" : "0");
425 				ret.formattedWrite("    <compiler>%s</compiler>\n", settings.compiler.name == "ldc" ? 2 : settings.compiler.name == "gdc" ? 1 : 0);
426 				ret.formattedWrite("    <otherDMD>0</otherDMD>\n");
427 				ret.formattedWrite("    <outdir>%s</outdir>\n", bin_path.toNativeString());
428 				ret.formattedWrite("    <objdir>.dub/obj/%s/%s</objdir>\n", to!string(type), intersubdir);
429 				ret.put("    <objname />\n");
430 				ret.put("    <libname />\n");
431 				ret.put("    <doDocComments>0</doDocComments>\n");
432 				ret.put("    <docdir />\n");
433 				ret.put("    <docname />\n");
434 				ret.put("    <modules_ddoc />\n");
435 				ret.put("    <ddocfiles />\n");
436 				ret.put("    <doHdrGeneration>0</doHdrGeneration>\n");
437 				ret.put("    <hdrdir />\n");
438 				ret.put("    <hdrname />\n");
439 				ret.put("    <doXGeneration>1</doXGeneration>\n");
440 				ret.put("    <xfilename>$(IntDir)\\$(TargetName).json</xfilename>\n");
441 				ret.put("    <debuglevel>0</debuglevel>\n");
442 				ret.put("    <versionlevel>0</versionlevel>\n");
443 				ret.put("    <debugids />\n");
444 				ret.put("    <dump_source>0</dump_source>\n");
445 				ret.put("    <mapverbosity>0</mapverbosity>\n");
446 				ret.put("    <createImplib>0</createImplib>\n");
447 				ret.put("    <defaultlibname />\n");
448 				ret.put("    <debuglibname />\n");
449 				ret.put("    <moduleDepsFile />\n");
450 				ret.put("    <run>0</run>\n");
451 				ret.put("    <runargs />\n");
452 				ret.put("    <runCv2pdb>1</runCv2pdb>\n");
453 				ret.put("    <pathCv2pdb>$(VisualDInstallDir)cv2pdb\\cv2pdb.exe</pathCv2pdb>\n");
454 				ret.put("    <cv2pdbPre2043>0</cv2pdbPre2043>\n");
455 				ret.put("    <cv2pdbNoDemangle>0</cv2pdbNoDemangle>\n");
456 				ret.put("    <cv2pdbEnumType>0</cv2pdbEnumType>\n");
457 				ret.put("    <cv2pdbOptions />\n");
458 				ret.put("    <objfiles />\n");
459 				ret.put("    <linkswitches />\n");
460 				ret.put("    <libpaths />\n");
461 				ret.put("    <deffile />\n");
462 				ret.put("    <resfile />\n");
463 				ret.put("    <preBuildCommand />\n");
464 				ret.put("    <postBuildCommand />\n");
465 				ret.put("    <filesToClean>*.obj;*.cmd;*.build;*.dep</filesToClean>\n");
466 				ret.put("  </Config>\n");
467 			} // foreach(architecture)
468 		}
469 		
470 		void performOnDependencies(const Package main, string[string] configs, void delegate(const Package pack) op)
471 		{
472 			foreach (p; m_app.getTopologicalPackageList(false, main, configs)) {
473 				if (p is main) continue;
474 				op(p);
475 			}
476 		}
477 		
478 		string generateUUID() const {
479 			return "{" ~ randomUUID().toString() ~ "}";
480 		}
481 		
482 		string guid(string projectName) {
483 			if(projectName !in m_projectUuids)
484 				m_projectUuids[projectName] = generateUUID();
485 			return m_projectUuids[projectName];
486 		}
487 
488 		auto solutionFileName() const {
489 			version(DUBBING) return getPackageFileName(m_app.mainPackage()) ~ ".dubbed.sln";
490 			else return getPackageFileName(m_app.mainPackage()) ~ ".sln";
491 		}
492 
493 		Path projFileName(ref const Package pack) const {
494 			auto basepath = Path(".");//Path(".dub/");
495 			version(DUBBING) return basepath ~ (getPackageFileName(pack) ~ ".dubbed.visualdproj");
496 			else return basepath ~ (getPackageFileName(pack) ~ ".visualdproj");
497 		}
498 	}
499 
500 	// TODO: nice folders
501 	struct SourceFile {
502 		Path structurePath;
503 		Path filePath;
504 		bool build = true;
505 
506 		hash_t toHash() const nothrow @trusted { return structurePath.toHash() ^ filePath.toHash() ^ (build * 0x1f3e7b2c); }
507 		int opCmp(ref const SourceFile rhs) const { return sortOrder(this, rhs); }
508 		// "a < b" for folder structures (deepest folder first, else lexical)
509 		private final static int sortOrder(ref const SourceFile a, ref const SourceFile b) {
510 			enforce(!a.structurePath.empty());
511 			enforce(!b.structurePath.empty());
512 			auto as = a.structurePath;
513 			auto bs = b.structurePath;
514 
515 			// Check for different folders, compare folders only (omit last one).
516 			for(uint idx=0; idx<min(as.length-1, bs.length-1); ++idx)
517 				if(as[idx] != bs[idx])
518 					return as[idx].opCmp(bs[idx]);
519 
520 			if(as.length != bs.length) {
521 				// If length differ, the longer one is "smaller", that is more 
522 				// specialized and will be put out first.
523 				return as.length > bs.length? -1 : 1;
524 			}
525 			else {
526 				// Both paths indicate files in the same directory, use lexical
527 				// ordering for those.
528 				return as.head.opCmp(bs.head);
529 			}
530 		}
531 	}
532 
533 	auto sortedSources(SourceFile[] sources) {
534 		return sort(sources);
535 	}
536 
537 	unittest {
538 		SourceFile[] sfs = [
539 			{ Path("b/file.d"), Path("") },
540 			{ Path("b/b/fileA.d"), Path("") },
541 			{ Path("a/file.d"), Path("") },
542 			{ Path("b/b/fileB.d"), Path("") },
543 			{ Path("b/b/b/fileA.d"), Path("") },
544 			{ Path("b/c/fileA.d"), Path("") },
545 		];
546 		auto sorted = sort(sfs);
547 		SourceFile[] sortedSfs;
548 		foreach(sr; sorted)
549 			sortedSfs ~= sr;
550 		assert(sortedSfs[0].structurePath == Path("a/file.d"), "1");
551 		assert(sortedSfs[1].structurePath == Path("b/b/b/fileA.d"), "2");
552 		assert(sortedSfs[2].structurePath == Path("b/b/fileA.d"), "3");
553 		assert(sortedSfs[3].structurePath == Path("b/b/fileB.d"), "4");
554 		assert(sortedSfs[4].structurePath == Path("b/c/fileA.d"), "5");
555 		assert(sortedSfs[5].structurePath == Path("b/file.d"), "6");
556 	}
557 }
558 
559 private string getPackageFileName(in Package pack)
560 {
561 	return pack.name.replace(":", "_");
562 }