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 }