1 module workspaced.com.dub;
2 
3 import core.sync.mutex;
4 import core.thread;
5 
6 import std.json : JSONValue;
7 import std.conv;
8 import std.stdio;
9 import std.regex;
10 import std.string;
11 import std.parallelism;
12 import std.algorithm;
13 
14 import painlessjson : toJSON, fromJSON;
15 
16 import workspaced.api;
17 
18 import dub.dub;
19 import dub.project;
20 import dub.package_;
21 import dub.description;
22 
23 import dub.generators.generator;
24 import dub.compilers.compiler;
25 
26 import dub.compilers.buildsettings;
27 
28 import dub.internal.vibecompat.inet.url;
29 import dub.internal.vibecompat.core.log;
30 
31 @component("dub") :
32 
33 /// Load function for dub. Call with `{"cmd": "load", "components": ["dub"]}`
34 /// This will start dub and load all import paths. All dub methods are used with `"cmd": "dub"`
35 /// Note: This will block any incoming requests while loading.
36 @load void startup(string dir, bool registerImportProvider = true,
37 		bool registerStringImportProvider = true, bool registerImportFilesProvider = true)
38 {
39 	setLogLevel(LogLevel.none);
40 
41 	if (registerImportProvider)
42 		importPathProvider = &imports;
43 	if (registerStringImportProvider)
44 		stringImportPathProvider = &stringImports;
45 	if (registerImportFilesProvider)
46 		importFilesProvider = &fileImports;
47 
48 	_cwdStr = dir;
49 	_cwd = Path(dir);
50 
51 	start();
52 	upgrade();
53 
54 	_compilerBinaryName = _dub.defaultCompiler;
55 	Compiler compiler = getCompiler(_compilerBinaryName);
56 	BuildSettings settings;
57 	auto platform = compiler.determinePlatform(settings, _compilerBinaryName);
58 
59 	_configuration = _dub.project.getDefaultConfiguration(platform);
60 	assert(_dub.project.configurations.canFind(_configuration), "No configuration available");
61 	updateImportPaths(false);
62 }
63 
64 /// Stops dub when called.
65 @unload void stop()
66 {
67 	_dub.destroy();
68 }
69 
70 private void start()
71 {
72 	_dub = new Dub(_cwdStr, null, SkipPackageSuppliers.none);
73 	_dub.packageManager.getOrLoadPackage(_cwd);
74 	_dub.loadPackage();
75 	_dub.project.validate();
76 }
77 
78 private void restart()
79 {
80 	stop();
81 	start();
82 }
83 
84 /// Reloads the dub.json or dub.sdl file from the cwd
85 /// Returns: `false` if there are no import paths available
86 /// Call_With: `{"subcmd": "update"}`
87 @arguments("subcmd", "update")
88 @async void update(AsyncCallback callback)
89 {
90 	restart();
91 	new Thread({ /**/
92 		try
93 		{
94 			auto result = updateImportPaths(false);
95 			callback(null, result.toJSON);
96 		}
97 		catch (Throwable t)
98 		{
99 			callback(t, null.toJSON);
100 		}
101 	}).start();
102 }
103 
104 bool updateImportPaths(bool restartDub = true)
105 {
106 	if (restartDub)
107 		restart();
108 
109 	auto compiler = getCompiler(_compilerBinaryName);
110 	BuildSettings buildSettings;
111 	auto buildPlatform = compiler.determinePlatform(buildSettings, _compilerBinaryName, _archType);
112 
113 	GeneratorSettings settings;
114 	settings.platform = buildPlatform;
115 	settings.config = _configuration;
116 	settings.buildType = _buildType;
117 	settings.compiler = compiler;
118 	settings.buildSettings = buildSettings;
119 	settings.buildSettings.options |= BuildOption.syntaxOnly;
120 	settings.combined = true;
121 	settings.run = false;
122 
123 	try
124 	{
125 		auto paths = _dub.project.listBuildSettings(settings, ["import-paths",
126 				"string-import-paths", "source-files"], ListBuildSettingsFormat.listNul);
127 		_importPaths = paths[0].split('\0');
128 		_stringImportPaths = paths[1].split('\0');
129 		_importFiles = paths[2].split('\0');
130 		return _importPaths.length > 0 || _importFiles.length > 0;
131 	}
132 	catch (Exception e)
133 	{
134 		stderr.writeln("Exception while listing import paths: ", e);
135 		_importPaths = [];
136 		_stringImportPaths = [];
137 		return false;
138 	}
139 }
140 
141 /// Calls `dub upgrade`
142 /// Call_With: `{"subcmd": "upgrade"}`
143 @arguments("subcmd", "upgrade")
144 void upgrade()
145 {
146 	_dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade);
147 }
148 
149 /// Lists all dependencies
150 /// Returns: `[{dependencies: [string], ver: string, name: string}]`
151 /// Call_With: `{"subcmd": "list:dep"}`
152 @arguments("subcmd", "list:dep")
153 auto dependencies() @property
154 {
155 	return _dub.project.listDependencies();
156 }
157 
158 /// Lists all import paths
159 /// Call_With: `{"subcmd": "list:import"}`
160 @arguments("subcmd", "list:import")
161 string[] imports() @property
162 {
163 	return _importPaths;
164 }
165 
166 /// Lists all string import paths
167 /// Call_With: `{"subcmd": "list:string-import"}`
168 @arguments("subcmd", "list:string-import")
169 string[] stringImports() @property
170 {
171 	return _stringImportPaths;
172 }
173 
174 /// Lists all import paths to files
175 /// Call_With: `{"subcmd": "list:file-import"}`
176 @arguments("subcmd", "list:file-import")
177 string[] fileImports() @property
178 {
179 	return _importFiles;
180 }
181 
182 /// Lists all configurations defined in the package description
183 /// Call_With: `{"subcmd": "list:configurations"}`
184 @arguments("subcmd", "list:configurations")
185 string[] configurations() @property
186 {
187 	return _dub.project.configurations;
188 }
189 
190 /// Lists all build types defined in the package description AND the predefined ones from dub ("plain", "debug", "release", "release-debug", "release-nobounds", "unittest", "docs", "ddox", "profile", "profile-gc", "cov", "unittest-cov")
191 /// Call_With: `{"subcmd": "list:build-types"}`
192 @arguments("subcmd", "list:build-types")
193 string[] buildTypes() @property
194 {
195 	string[] types = [
196 		"plain", "debug", "release", "release-debug", "release-nobounds", "unittest", "docs",
197 		"ddox", "profile", "profile-gc", "cov", "unittest-cov"
198 	];
199 	foreach (type, info; _dub.project.rootPackage.recipe.buildTypes)
200 		types ~= type;
201 	return types;
202 }
203 
204 /// Gets the current selected configuration
205 /// Call_With: `{"subcmd": "get:configuration"}`
206 @arguments("subcmd", "get:configuration")
207 string configuration() @property
208 {
209 	return _configuration;
210 }
211 
212 /// Selects a new configuration and updates the import paths accordingly
213 /// Returns: `false` if there are no import paths in the new configuration
214 /// Call_With: `{"subcmd": "set:configuration"}`
215 @arguments("subcmd", "set:configuration")
216 bool setConfiguration(string configuration)
217 {
218 	if (!_dub.project.configurations.canFind(configuration))
219 		return false;
220 	_configuration = configuration;
221 	return updateImportPaths(false);
222 }
223 
224 /// List all possible arch types for current set compiler
225 /// Call_With: `{"subcmd": "list:arch-types"}`
226 @arguments("subcmd", "list:arch-types")
227 string[] archTypes() @property
228 {
229 	string[] types = ["x86_64", "x86"];
230 
231 	string compilerName = getCompiler(_compilerBinaryName).name;
232 
233 	version (Windows)
234 	{
235 		if (compilerName == "dmd")
236 		{
237 			types ~= "x86_mscoff";
238 		}
239 	}
240 	if (compilerName == "gdc")
241 	{
242 		types ~= ["arm", "arm_thumb"];
243 	}
244 
245 	return types;
246 }
247 
248 /// Returns the current selected arch type
249 /// Call_With: `{"subcmd": "get:arch-type"}`
250 @arguments("subcmd", "get:arch-type")
251 string archType() @property
252 {
253 	return _archType;
254 }
255 
256 /// Selects a new arch type and updates the import paths accordingly
257 /// Returns: `false` if there are no import paths in the new arch type
258 /// Call_With: `{"subcmd": "set:arch-type"}`
259 @arguments("subcmd", "set:arch-type")
260 bool setArchType(JSONValue request)
261 {
262 	assert("arch-type" in request, "arch-type not in request");
263 	auto type = request["arch-type"].fromJSON!string;
264 	if (archTypes.canFind(type))
265 	{
266 		_archType = type;
267 		return updateImportPaths(false);
268 	}
269 	else
270 	{
271 		return false;
272 	}
273 }
274 
275 /// Returns the current selected build type
276 /// Call_With: `{"subcmd": "get:build-type"}`
277 @arguments("subcmd", "get:build-type")
278 string buildType() @property
279 {
280 	return _buildType;
281 }
282 
283 /// Selects a new build type and updates the import paths accordingly
284 /// Returns: `false` if there are no import paths in the new build type
285 /// Call_With: `{"subcmd": "set:build-type"}`
286 @arguments("subcmd", "set:build-type")
287 bool setBuildType(JSONValue request)
288 {
289 	assert("build-type" in request, "build-type not in request");
290 	auto type = request["build-type"].fromJSON!string;
291 	if (buildTypes.canFind(type))
292 	{
293 		_buildType = type;
294 		return updateImportPaths(false);
295 	}
296 	else
297 	{
298 		return false;
299 	}
300 }
301 
302 /// Returns the current selected compiler
303 /// Call_With: `{"subcmd": "get:compiler"}`
304 @arguments("subcmd", "get:compiler")
305 string compiler() @property
306 {
307 	return _compilerBinaryName;
308 }
309 
310 /// Selects a new compiler for building
311 /// Returns: `false` if the compiler does not exist
312 /// Call_With: `{"subcmd": "set:compiler"}`
313 @arguments("subcmd", "set:compiler")
314 bool setCompiler(string compiler)
315 {
316 	try
317 	{
318 		_compilerBinaryName = compiler;
319 		Compiler comp = getCompiler(compiler); // make sure it gets a valid compiler
320 		return true;
321 	}
322 	catch (Exception e)
323 	{
324 		return false;
325 	}
326 }
327 
328 /// Returns the project name
329 /// Call_With: `{"subcmd": "get:name"}`
330 @arguments("subcmd", "get:name")
331 string name() @property
332 {
333 	return _dub.projectName;
334 }
335 
336 /// Returns the project path
337 /// Call_With: `{"subcmd": "get:path"}`
338 @arguments("subcmd", "get:path")
339 auto path() @property
340 {
341 	return _dub.projectPath;
342 }
343 
344 /// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE.
345 /// Returns: `[{line: int, column: int, type: ErrorType, text: string}]` where type is an integer
346 /// Call_With: `{"subcmd": "build"}`
347 @arguments("subcmd", "build")
348 @async void build(AsyncCallback cb)
349 {
350 	new Thread({
351 		try
352 		{
353 			auto compiler = getCompiler(_compilerBinaryName);
354 			BuildSettings buildSettings;
355 			auto buildPlatform = compiler.determinePlatform(buildSettings,
356 				_compilerBinaryName, _archType);
357 
358 			GeneratorSettings settings;
359 			settings.platform = buildPlatform;
360 			settings.config = _configuration;
361 			settings.buildType = _buildType;
362 			settings.compiler = compiler;
363 			settings.tempBuild = true;
364 			settings.buildSettings = buildSettings;
365 			settings.buildSettings.addOptions(BuildOption.syntaxOnly);
366 			settings.buildSettings.addDFlags("-o-");
367 
368 			BuildIssue[] issues;
369 
370 			settings.compileCallback = (status, output) {
371 				string[] lines = output.splitLines;
372 				foreach (line; lines)
373 				{
374 					auto match = line.matchFirst(errorFormat);
375 					if (match)
376 					{
377 						issues ~= BuildIssue(match[2].to!int,
378 							match[3].toOr!int(0), match[1], match[4].to!ErrorType, match[5]);
379 					}
380 					else
381 					{
382 						if (line.canFind("from"))
383 						{
384 							auto contMatch = line.matchFirst(errorFormatCont);
385 							if (contMatch)
386 							{
387 								issues ~= BuildIssue(contMatch[2].to!int, contMatch[3].toOr!int(1),
388 									contMatch[1], ErrorType.Error, contMatch[4]);
389 							}
390 						}
391 						if (line.canFind("is deprecated"))
392 						{
393 							auto deprMatch = line.matchFirst(deprecationFormat);
394 							if (deprMatch)
395 							{
396 								issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1),
397 									deprMatch[1], ErrorType.Deprecation, deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead.");
398 								// TODO: maybe add special type or output
399 							}
400 						}
401 					}
402 				}
403 			};
404 			try
405 			{
406 				_dub.generateProject("build", settings);
407 			}
408 			catch (Exception e)
409 			{
410 				if (!e.msg.matchFirst(harmlessExceptionFormat))
411 					throw e;
412 			}
413 			cb(null, issues.toJSON);
414 		}
415 		catch (Throwable t)
416 		{
417 			ubyte[] empty;
418 			cb(t, empty.toJSON);
419 		}
420 	}).start();
421 }
422 
423 ///
424 enum ErrorType : ubyte
425 {
426 	///
427 	Error = 0,
428 	///
429 	Warning = 1,
430 	///
431 	Deprecation = 2
432 }
433 
434 private:
435 
436 __gshared
437 {
438 	Dub _dub;
439 	Path _cwd;
440 	string _configuration;
441 	string _archType = "x86_64";
442 	string _buildType = "debug";
443 	string _cwdStr;
444 	string _compilerBinaryName;
445 	BuildSettings _settings;
446 	Compiler _compiler;
447 	BuildPlatform _platform;
448 	string[] _importPaths, _stringImportPaths, _importFiles;
449 }
450 
451 T toOr(T)(string s, T defaultValue)
452 {
453 	if (!s || !s.length)
454 		return defaultValue;
455 	return s.to!T;
456 }
457 
458 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g");
459 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi"); // `
460 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (.*)`, "g"); // `
461 enum deprecationFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g"); // `
462 
463 struct BuildIssue
464 {
465 	int line, column;
466 	string file;
467 	ErrorType type;
468 	string text;
469 }
470 
471 struct DubPackageInfo
472 {
473 	string[string] dependencies;
474 	string ver;
475 	string name;
476 }
477 
478 DubPackageInfo getInfo(in Package dep)
479 {
480 	DubPackageInfo info;
481 	info.name = dep.name;
482 	info.ver = dep.version_.toString;
483 	foreach (subDep; dep.getAllDependencies())
484 	{
485 		info.dependencies[subDep.name] = subDep.spec.toString;
486 	}
487 	return info;
488 }
489 
490 auto listDependencies(Project project)
491 {
492 	auto deps = project.dependencies;
493 	DubPackageInfo[] dependencies;
494 	if (deps is null)
495 		return dependencies;
496 	foreach (dep; deps)
497 	{
498 		dependencies ~= getInfo(dep);
499 	}
500 	return dependencies;
501 }