1 /*******************************************************************************
2  * Manage configs and commands
3  * 
4  * Mediation of gendoc configuration file and command line arguments.
5  * Also get dub package informations.
6  */
7 module gendoc.config;
8 
9 import dub.dub, dub.project, dub.package_, dub.generators.generator, dub.compilers.compiler;
10 import dub.internal.vibecompat.core.log, dub.internal.vibecompat.data.json, dub.internal.vibecompat.inet.path;
11 
12 /*******************************************************************************
13  * 
14  */
15 struct PackageConfig
16 {
17 	///
18 	string   path;
19 	///
20 	string   name;
21 	///
22 	string[] files;
23 	///
24 	string[] options;
25 	///
26 	string   packageVersion;
27 	///
28 	PackageConfig[] subPackages;
29 	
30 	private static string determineConfigName(Package pkg, BuildPlatform platform, string configName)
31 	{
32 		import std.algorithm: canFind;
33 		import std.exception: enforce;
34 		auto allCfgs = pkg.getPlatformConfigurations(platform, true);
35 		if (configName.length > 0)
36 		{
37 			enforce(allCfgs.canFind(configName), "Cannot found configuration: " ~ configName);
38 			return configName;
39 		}
40 		if (allCfgs.canFind("gendoc"))
41 			return "gendoc";
42 		return pkg.getDefaultConfiguration(platform, true);
43 	}
44 	
45 	/***************************************************************************
46 	 * 
47 	 */
48 	void loadPackage(
49 		Dub dub,
50 		Package pkg,
51 		string archType,
52 		string buildType,
53 		string configName,
54 		string compiler)
55 	{
56 		import std.algorithm, std.array, std.file, std.path;
57 		Package bkupPkg = dub.project.rootPackage;
58 		if (pkg)
59 			dub.loadPackage(pkg);
60 		scope (exit) if (pkg)
61 			dub.loadPackage(bkupPkg);
62 		if (!dub.project.hasAllDependencies)
63 			dub.upgrade(UpgradeOptions.select);
64 		
65 		dub.project.validate();
66 		
67 		auto compilerData = getCompiler(compiler);
68 		BuildSettings buildSettings;
69 		auto buildPlatform = compilerData.determinePlatform(buildSettings, compiler, archType);
70 		
71 		GeneratorSettings settings;
72 		settings.config    = determineConfigName(pkg ? pkg : dub.project.rootPackage, buildPlatform, configName);
73 		settings.force     = true;
74 		settings.buildType = buildType;
75 		settings.compiler  = compilerData;
76 		settings.platform  = buildPlatform;
77 		name    = pkg ? pkg.name : dub.project.rootPackage.name;
78 		if (dub.project.rootPackage.getBuildSettings(settings.config).targetType != TargetType.none)
79 		{
80 			auto lists = dub.project.listBuildSettings(settings,
81 				["dflags", "versions", "debug-versions",
82 				"import-paths", "string-import-paths", "options",
83 				"source-files"],
84 				ListBuildSettingsFormat.commandLineNul).map!(a => a.split("\0")).array;
85 			// -oq, -od が付与されているゴミが紛れ込む場合がある。dubのバグか?回避する。
86 			auto importDirs = lists[3].filter!(a => a.startsWith("-I") && a[2..$].exists);
87 			path    = importDirs.empty
88 				? pkg ? pkg.path.toNativeString() : dub.project.rootPackage.path.toNativeString()
89 				: importDirs.front[2..$];
90 			options = lists[0].reduce!((a, b) => a.canFind(b) ? a : a ~ [b])(lists[1..6].join);
91 			files   = lists[6].filter!(a => a.exists && canFind([".d", ".dd", ".di"], a.extension)).array;
92 			packageVersion = pkg
93 				? pkg.version_.toString()
94 				: dub.project.rootPackage.version_.toString();
95 		}
96 		foreach (spkg; dub.project.rootPackage.subPackages)
97 		{
98 			PackageConfig pkgcfg;
99 			if (spkg.recipe.name.length > 0)
100 			{
101 				auto basepkg = packageVersion.length > 0
102 					? dub.packageManager.getPackage(name, packageVersion)
103 					: dub.packageManager.getLatestPackage(name);
104 				auto subpkg = dub.packageManager.getSubPackage(basepkg, spkg.recipe.name, false);
105 				pkgcfg.loadPackage(dub, subpkg,
106 					archType, buildType, configName, compiler);
107 			}
108 			else
109 			{
110 				auto tmppkgpath = dub.rootPath ~ NativePath(spkg.path);
111 				auto subpkg = dub.packageManager.getOrLoadPackage(tmppkgpath, NativePath.init, true);
112 				pkgcfg.loadPackage(dub, subpkg,
113 					archType, buildType, configName, compiler);
114 			}
115 			subPackages ~= pkgcfg;
116 		}
117 	}
118 	
119 	/// ditto
120 	void loadPackage(
121 		string dir,
122 		string archType,
123 		string buildType,
124 		string configName,
125 		ref string compiler)
126 	{
127 		import std.algorithm, std..string, std.array, std.path;
128 		setLogLevel(LogLevel.error);
129 		auto absDir = dir.absolutePath.buildNormalizedPath;
130 		auto dub = new Dub(absDir);
131 		if (compiler.length == 0)
132 			compiler = dub.defaultCompiler;
133 		if (archType.length == 0)
134 			archType = dub.defaultArchitecture;
135 		dub.loadPackage();
136 		loadPackage(dub, null,
137 			archType,
138 			buildType,
139 			configName,
140 			compiler);
141 	}
142 	
143 	@system unittest
144 	{
145 		import std.file;
146 		if ("testcases/case002".exists)
147 		{
148 			PackageConfig cfg;
149 			string compiler;
150 			cfg.loadPackage("testcases/case002", "x86_64", "debug", null, compiler);
151 		}
152 	}
153 	
154 	@system unittest
155 	{
156 		import std.file;
157 		import std.path;
158 		import std.algorithm;
159 		import std.array;
160 		if ("testcases/issue32".exists)
161 		{
162 			PackageConfig cfg;
163 			string compiler;
164 			cfg.loadPackage("testcases/issue32", "x86_64", "debug", "valid", compiler);
165 			assert(cfg.files.length == 1);
166 			assert(cfg.subPackages.length == 1);
167 			assert(cfg.subPackages[0].name == "issue32:subpkg");
168 			assert(cfg.subPackages[0].files.length == 2);
169 			assert(cfg.subPackages[0].files.map!(a => a.baseName).array.sort().equal(["foo.d", "lib.d"]));
170 			
171 			cfg = PackageConfig.init;
172 			cfg.loadPackage("testcases/issue32", "x86_64", "debug", null, compiler);
173 			assert(cfg.files.length == 1);
174 			assert(cfg.subPackages.length == 1);
175 			assert(cfg.subPackages[0].name == "issue32:subpkg");
176 			assert(cfg.subPackages[0].files.length == 2);
177 			assert(cfg.subPackages[0].files.map!(a => a.baseName).array.sort().equal(["bar.d", "lib.d"]));
178 		}
179 	}
180 }
181 
182 private string _getHomeDirectory()
183 {
184 	import std.file, std.path;
185 	version (Posix)
186 	{
187 		return expandTilde("~");
188 	}
189 	else version (Windows)
190 	{
191 		import core.runtime;
192 		import core.sys.windows.windows, core.sys.windows.shlobj;
193 		wchar[MAX_PATH+1] dst;
194 		auto mod = LoadLibraryW("Shell32.dll");
195 		if (!mod)
196 			return getcwd;
197 		
198 		alias SHGetFolderPathProc = extern (Windows) HRESULT function(HWND, int, HANDLE, DWORD, LPWSTR);
199 		auto getFolderPath = cast(SHGetFolderPathProc)GetProcAddress(cast(HMODULE)mod, "SHGetFolderPathW");
200 		if (getFolderPath is null)
201 			return getcwd;
202 		if (getFolderPath(null, CSIDL_PROFILE, null, 0, dst.ptr) == S_OK)
203 		{
204 			import std.algorithm, std.conv;
205 			auto idx = dst.ptr.lstrlen();
206 			if (idx < dst.length)
207 				return dst[0..idx].to!string();
208 		}
209 		return getcwd;
210 	}
211 }
212 
213 /*******************************************************************************
214  * Gendoc's configuration data structure
215  */
216 struct GendocConfig
217 {
218 	///
219 	@optional
220 	string[] ddocs;
221 	///
222 	@optional
223 	string[] sourceDocs;
224 	///
225 	@optional
226 	string   target;
227 	///
228 	@optional
229 	string[] excludePaths;
230 	///
231 	@optional
232 	string[] excludePatterns = [
233 		"(?:(?<=/)|^)\\.[^/.]+$",
234 		"(?:(?<=[^/]+/)|^)_[^/]+$",
235 		"(?:(?<=[^/]+/)|^)internal(?:\\.d)?$"];
236 	///
237 	@optional
238 	string[] excludePackages;
239 	/// {"keyName": ["dub_package_name_regex_pattern1", "dub_package_name_regex_pattern2"]}
240 	@optional
241 	string[][string] combinedDubPackagePatterns;
242 	///
243 	@optional
244 	string[] excludePackagePatterns = [
245 		"(?:(?<=[^:]+/)|^)_[^/]+$",
246 		":docs?$"];
247 	
248 	///
249 	@optional
250 	bool enableGenerateJSON = true;
251 	
252 	///
253 	void fixPath(string dirPath)
254 	{
255 		import std.algorithm, std.path, std.file, std.process;
256 		import gendoc.misc;
257 		ddocs      = ddocs.remove!(a => a.length == 0);
258 		sourceDocs = sourceDocs.remove!(a => a.length == 0);
259 		auto map = [
260 			"GENDOC_DIR":  thisExePath.dirName.absolutePath,
261 			"GENDOC_SD_DIR": thisExePath.dirName.buildPath("source_docs").exists
262 				? thisExePath.dirName.buildPath("source_docs")
263 				: thisExePath.dirName.buildPath("../etc/.gendoc/docs").exists
264 					? thisExePath.dirName.buildNormalizedPath("../etc/.gendoc/docs")
265 					: thisExePath.dirName.absolutePath,
266 			"GENDOC_DD_DIR": thisExePath.dirName.buildPath("ddoc").exists
267 				? thisExePath.dirName.buildPath("ddoc")
268 				: thisExePath.dirName.buildPath("../etc/.gendoc/ddoc").exists
269 					? thisExePath.dirName.buildNormalizedPath("../etc/.gendoc/ddoc")
270 					: thisExePath.dirName.absolutePath,
271 			"PROJECT_DIR": dirPath.absolutePath,
272 			"WORK_DIR":    getcwd.absolutePath];
273 		bool mapFunc(ref string arg, MacroType type)
274 		{
275 			if (auto val = map.get(arg, null))
276 			{
277 				arg = val;
278 				return true;
279 			}
280 			if (auto val = environment.get(arg, null))
281 			{
282 				arg = val;
283 				return true;
284 			}
285 			return false;
286 		}
287 		foreach (ref d; ddocs)
288 			d = d.expandMacro(&mapFunc);
289 		foreach (ref d; sourceDocs)
290 			d = d.expandMacro(&mapFunc);
291 		target = target.expandMacro(&mapFunc);
292 		
293 		foreach (ref d; ddocs)
294 		{
295 			if (!d.isAbsolute)
296 				d = buildPath(dirPath, d);
297 		}
298 		foreach (ref d; sourceDocs)
299 		{
300 			if (!d.isAbsolute)
301 				d = buildPath(dirPath, d);
302 		}
303 		if (target.length > 0 && !target.isAbsolute)
304 			target = buildPath(dirPath, target);
305 	}
306 	
307 	///
308 	private bool _loadConfigFromFile(string p)
309 	{
310 		debug import std.stdio;
311 		import dub.internal.utils;
312 		import std.file, std.path;
313 		if (!p.exists || !p.isFile)
314 			return false;
315 		debug writeln("Configuration loaded from: " ~ p);
316 		this.deserializeJson!GendocConfig(jsonFromFile(NativePath(p)));
317 		fixPath(p.dirName);
318 		return true;
319 	}
320 	
321 	///
322 	private bool _isExistsDir(string p)
323 	{
324 		import std.file, std.path;
325 		return p.exists && p.isDir;
326 	}
327 	
328 	///
329 	private bool _loadConfigFromDir(string p, bool enableRawDocs)
330 	{
331 		debug import std.stdio;
332 		import std.file, std.path;
333 		auto ddocDir = p.buildPath("ddoc");
334 		if (!_isExistsDir(ddocDir))
335 			return false;
336 		string sourceDocsDir;
337 		if (enableRawDocs)
338 		{
339 			sourceDocsDir = p.buildPath("docs");
340 			if (_isExistsDir(sourceDocsDir))
341 			{
342 				ddocs      = [ddocDir];
343 				sourceDocs = [sourceDocsDir];
344 				debug writeln("Configuration loaded from: " ~ p);
345 				return true;
346 			}
347 		}
348 		sourceDocsDir = p.buildPath("source_docs");
349 		if (_isExistsDir(sourceDocsDir))
350 		{
351 			ddocs      = [ddocDir];
352 			sourceDocs = [sourceDocsDir];
353 			debug writeln("Configuration loaded from: " ~ p);
354 			return true;
355 		}
356 		return false;
357 	}
358 	
359 	/***************************************************************************
360 	 * Load configuration from specifiered path
361 	 * 
362 	 */
363 	bool loadConfig(string path)
364 	{
365 		import std.file, std.path;
366 		// 1.1. (--gendocConfig=<jsonfile>)
367 		if (_loadConfigFromFile(path))
368 			return true;
369 		
370 		// 1.2. (--gendocConfig=<directory>)/settings.json
371 		if (_loadConfigFromFile(path.buildPath("settings.json")))
372 			return true;
373 		
374 		// 1.3. (--gendocConfig=<directory>)/gendoc.json
375 		if (_loadConfigFromFile(path.buildPath("gendoc.json")))
376 			return true;
377 		
378 		// 1.4. (--gendocConfig=<directory>)/ddoc and (--gendocConfig=<directory>)/docs
379 		// 1.5. (--gendocConfig=<directory>)/ddoc and (--gendocConfig=<directory>)/source_docs
380 		if (_loadConfigFromDir(path, true))
381 			return true;
382 		
383 		return false;
384 	}
385 	
386 	/// ditto
387 	bool loadDefaultConfig(string root)
388 	{
389 		import std.file, std.path;
390 		// 2.1. ./.gendoc.json
391 		if (_loadConfigFromFile(root.buildPath(".gendoc.json")))
392 			return true;
393 		
394 		// 2.2. ./gendoc.json
395 		if (_loadConfigFromFile(root.buildPath("gendoc.json")))
396 			return true;
397 		
398 		// 2.3. ./.gendoc/settings.json
399 		if (_loadConfigFromFile(root.buildPath(".gendoc/settings.json")))
400 			return true;
401 		
402 		// 2.4. ./.gendoc/gendoc.json
403 		if (_loadConfigFromFile(root.buildPath(".gendoc/gendoc.json")))
404 			return true;
405 		
406 		// 2.5.  ./.gendoc/ddoc and ./.gendoc/docs
407 		// 2.6. ./.gendoc/ddoc and ./.gendoc/source_docs
408 		if (_loadConfigFromDir(root.buildPath(".gendoc"), true))
409 			return true;
410 		
411 		// 2.7. ./ddoc and ./source_docs
412 		// (docs may be a target)
413 		if (_loadConfigFromDir(root, false))
414 			return true;
415 		
416 		auto homeDir = _getHomeDirectory();
417 		
418 		
419 		// 3.1. $(HOME)/.gendoc.json
420 		if (_loadConfigFromFile(homeDir.buildPath(".gendoc.json")))
421 			return true;
422 		
423 		// 3.2. $(HOME)/gendoc.json
424 		if (_loadConfigFromFile(homeDir.buildPath("gendoc.json")))
425 			return true;
426 		
427 		// 3.3. $(HOME)/.gendoc/settings.json
428 		if (_loadConfigFromFile(homeDir.buildPath(".gendoc/settings.json")))
429 			return true;
430 		
431 		// 3.4. $(HOME)/.gendoc/gendoc.json
432 		if (_loadConfigFromFile(homeDir.buildPath(".gendoc/gendoc.json")))
433 			return true;
434 		
435 		// 3.5. $(HOME)/.gendoc/ddoc and $(HOME)/.gendoc/docs
436 		// 3.6. $(HOME)/.gendoc/ddoc and $(HOME)/.gendoc/sourcec_docs
437 		if (_loadConfigFromDir(homeDir.buildPath(".gendoc"), true))
438 			return true;
439 		
440 		auto gendocExeDir = thisExePath.dirName;
441 		
442 		// 4.1. (gendocExeDir)/gendoc.json
443 		if (_loadConfigFromFile(gendocExeDir.buildPath(".gendoc.json")))
444 			return true;
445 		
446 		// 4.2. (gendocExeDir)/gendoc.json
447 		if (_loadConfigFromFile(gendocExeDir.buildPath("gendoc.json")))
448 			return true;
449 		
450 		// 4.3. (gendocExeDir)/.gendoc/settings.json
451 		if (_loadConfigFromFile(gendocExeDir.buildPath(".gendoc/settings.json")))
452 			return true;
453 		
454 		// 4.4. (gendocExeDir)/.gendoc/gendoc.json
455 		if (_loadConfigFromFile(gendocExeDir.buildPath(".gendoc/gendoc.json")))
456 			return true;
457 		
458 		// 4.5. (gendocExeDir)/.gendoc/ddoc and (gendocExeDir)/.gendoc/docs
459 		// 4.6. (gendocExeDir)/.gendoc/ddoc and (gendocExeDir)/.gendoc/source_docs
460 		if (_loadConfigFromDir(gendocExeDir.buildPath(".gendoc"), true))
461 			return true;
462 		
463 		// 4.7. (gendocExeDir)/ddoc and (gendocExeDir)/source_docs
464 		// (docs may be a gendoc's document target)
465 		if (_loadConfigFromDir(gendocExeDir, false))
466 			return true;
467 		
468 		auto gendocEtcDir = gendocExeDir.dirName.buildPath("etc");
469 		
470 		// 5.1. (gendocEtcDir)/gendoc.json
471 		if (_loadConfigFromFile(gendocEtcDir.buildPath(".gendoc.json")))
472 			return true;
473 		
474 		// 5.2. (gendocEtcDir)/gendoc.json
475 		if (_loadConfigFromFile(gendocEtcDir.buildPath("gendoc.json")))
476 			return true;
477 		
478 		// 5.3. (gendocEtcDir)/.gendoc/settings.json
479 		if (_loadConfigFromFile(gendocEtcDir.buildPath(".gendoc/settings.json")))
480 			return true;
481 		
482 		// 5.4. (gendocEtcDir)/.gendoc/gendoc.json
483 		if (_loadConfigFromFile(gendocEtcDir.buildPath(".gendoc/gendoc.json")))
484 			return true;
485 		
486 		// 5.5. (gendocEtcDir)/.gendoc/ddoc and (gendocEtcDir)/.gendoc/docs
487 		// 5.6. (gendocEtcDir)/.gendoc/ddoc and (gendocEtcDir)/.gendoc/source_docs
488 		if (_loadConfigFromDir(gendocEtcDir.buildPath(".gendoc"), true))
489 			return true;
490 		
491 		return false;
492 	}
493 	
494 	///
495 	void setup(string root, string configFile, string[] optDdocs, string[] optSourceDocs, string optTarget)
496 	{
497 		import std.algorithm, std.array, std.path, std.file, std.exception;
498 		if (configFile.length > 0)
499 		{
500 			// 1.コマンドライン引数によってファイルの指定があった場合
501 			auto filepath = root.buildPath(configFile);
502 			// コマンドラインの指定があるのに構成が見つからない場合はエラー
503 			loadConfig(filepath).enforce("Cannot load configuration: " ~ filepath);
504 		}
505 		else
506 		{
507 			// 2.コマンドライン引数がない場合はデフォルトから読み込み
508 			loadDefaultConfig(root);
509 		}
510 		
511 		if (optDdocs.length > 0)
512 			ddocs = optDdocs.map!(
513 				a => a.isAbsolute ? a : root.buildPath(a)).array;
514 		if (optSourceDocs.length > 0)
515 			sourceDocs = optSourceDocs.map!(
516 				a => a.isAbsolute ? a : root.buildPath(a)).array;
517 		if (optTarget.length > 0)
518 			target = optTarget.isAbsolute ? optTarget : root.buildPath(optTarget);
519 		
520 		// default settings
521 		if (ddocs.length == 0 && root.buildPath("ddoc").exists)
522 			ddocs = [root.buildPath("ddoc")];
523 		if (ddocs.length == 0 && thisExePath.dirName.buildPath("ddoc").exists)
524 			ddocs = [thisExePath.dirName.buildPath("ddoc")];
525 		if (sourceDocs.length == 0 && root.buildPath("source_docs").exists)
526 			sourceDocs = [root.buildPath("source_docs")];
527 		if (sourceDocs.length == 0 && thisExePath.dirName.buildPath("source_docs").exists)
528 			sourceDocs = [thisExePath.dirName.buildPath("source_docs")];
529 		if (target.length == 0)
530 			target = root.buildPath("docs");
531 		
532 		// check
533 		foreach (ref d; ddocs)
534 		{
535 			enforce(d.exists && d.isDir, "ddoc directory is missing: " ~ d);
536 			enforce(filenameCmp(target.absolutePath.buildNormalizedPath, d.absolutePath.buildNormalizedPath) != 0,
537 				"ddoc dir cannot be same to target: " ~ target);
538 		}
539 		foreach (ref d; sourceDocs)
540 		{
541 			enforce(d.exists && d.isDir, "source_docs directory is missing: " ~ d);
542 			enforce(filenameCmp(target.absolutePath.buildNormalizedPath, d.absolutePath.buildNormalizedPath) != 0,
543 				"source_docs dir cannot be same to target: " ~ target);
544 		}
545 	}
546 }
547 
548 
549 
550 /*******************************************************************************
551  * 
552  */
553 struct Config
554 {
555 	import std.getopt;
556 	///
557 	string compiler;
558 	///
559 	PackageConfig packageData;
560 	///
561 	GendocConfig gendocData;
562 	///
563 	bool singleFile;
564 	///
565 	bool quiet;
566 	///
567 	bool varbose;
568 	/***************************************************************************
569 	 * 
570 	 */
571 	GetoptResult setup(string[] commandlineArgs)
572 	{
573 		import std.file, std.path;
574 		
575 		string configFile;
576 		Config tmp;
577 		bool saveConfig;
578 		string archType;
579 		string buildType = "debug";
580 		string configName;
581 		string root = ".";
582 		
583 		string   gendocConfig;
584 		string[] ddocs;
585 		string[] sourceDocs;
586 		string   target;
587 		
588 		auto ret = commandlineArgs.getopt(
589 			config.caseSensitive,
590 			config.bundling,
591 			"a|arch",           "Archtecture of dub project.",                            &archType,
592 			"b|build",          "Build type of dub project.",                             &buildType,
593 			"c|config",         "Configuration of dub project.",                          &configName,
594 			"compiler",         "Specifies the compiler binary to use (can be a path).",  &compiler,
595 			"gendocDdocs",      "Ddoc sources of document files.",                        &ddocs,
596 			"gendocSourceDocs", "Source of document files.",                              &sourceDocs,
597 			"gendocTarget",     "Target directory of generated documents.",               &target,
598 			"gendocConfig",     "Configuration file of gendoc.",                          &configFile,
599 			"root",             "Path to operate in instead of the current working dir.", &root,
600 			"singleFile",       "Single file generation mode.",                           &singleFile,
601 			"v|varbose",        "Display varbose messages.",                              &varbose,
602 			"q|quiet",          "Non-display messages.",                                  &quiet
603 		);
604 		
605 		packageData.loadPackage(root, archType, buildType, configName, compiler);
606 		gendocData.setup(root, configFile, ddocs, sourceDocs, target);
607 		
608 		return ret;
609 	}
610 }