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