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