1 /** 2 A package manager. 3 4 Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff, Sönke Ludwig 7 */ 8 module dub.dub; 9 10 import dub.compilers.compiler; 11 import dub.dependency; 12 import dub.dependencyresolver; 13 import dub.internal.utils; 14 import dub.internal.vibecompat.core.file; 15 import dub.internal.vibecompat.core.log; 16 import dub.internal.vibecompat.data.json; 17 import dub.internal.vibecompat.inet.url; 18 import dub.package_; 19 import dub.packagemanager; 20 import dub.packagesuppliers; 21 import dub.project; 22 import dub.generators.generator; 23 import dub.init; 24 25 import std.algorithm; 26 import std.array : array, replace; 27 import std.conv : to; 28 import std.exception : enforce; 29 import std.file; 30 import std.process : environment; 31 import std.range : assumeSorted, empty; 32 import std.string; 33 import std.encoding : sanitize; 34 35 // Set output path and options for coverage reports 36 version (DigitalMars) version (D_Coverage) 37 { 38 shared static this() 39 { 40 import core.runtime, std.file, std.path, std.stdio; 41 dmd_coverSetMerge(true); 42 auto path = buildPath(dirName(thisExePath()), "../cov"); 43 if (!path.exists) 44 mkdir(path); 45 dmd_coverDestPath(path); 46 } 47 } 48 49 static this() 50 { 51 import dub.compilers.dmd : DMDCompiler; 52 import dub.compilers.gdc : GDCCompiler; 53 import dub.compilers.ldc : LDCCompiler; 54 registerCompiler(new DMDCompiler); 55 registerCompiler(new GDCCompiler); 56 registerCompiler(new LDCCompiler); 57 } 58 59 deprecated("use defaultRegistryURLs") enum defaultRegistryURL = defaultRegistryURLs[0]; 60 61 /// The URL to the official package registry and it's default fallback registries. 62 static immutable string[] defaultRegistryURLs = [ 63 "https://code.dlang.org/", 64 "https://codemirror.dlang.org/", 65 "https://dub.bytecraft.nl/", 66 "https://code-mirror.dlang.io/", 67 ]; 68 69 /** Returns a default list of package suppliers. 70 71 This will contain a single package supplier that points to the official 72 package registry. 73 74 See_Also: `defaultRegistryURLs` 75 */ 76 PackageSupplier[] defaultPackageSuppliers() 77 { 78 logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]); 79 return [new FallbackPackageSupplier(defaultRegistryURLs.map!getRegistryPackageSupplier.array)]; 80 } 81 82 /** Returns a registry package supplier according to protocol. 83 84 Allowed protocols are dub+http(s):// and maven+http(s)://. 85 */ 86 PackageSupplier getRegistryPackageSupplier(string url) 87 { 88 switch (url.startsWith("dub+", "mvn+", "file://")) 89 { 90 case 1: 91 return new RegistryPackageSupplier(URL(url[4..$])); 92 case 2: 93 return new MavenRegistryPackageSupplier(URL(url[4..$])); 94 case 3: 95 return new FileSystemPackageSupplier(NativePath(url[7..$])); 96 default: 97 return new RegistryPackageSupplier(URL(url)); 98 } 99 } 100 101 unittest 102 { 103 auto dubRegistryPackageSupplier = getRegistryPackageSupplier("dub+https://code.dlang.org"); 104 assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); 105 106 dubRegistryPackageSupplier = getRegistryPackageSupplier("https://code.dlang.org"); 107 assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); 108 109 auto mavenRegistryPackageSupplier = getRegistryPackageSupplier("mvn+http://localhost:8040/maven/libs-release/dubpackages"); 110 assert(mavenRegistryPackageSupplier.description.canFind(" http://localhost:8040/maven/libs-release/dubpackages")); 111 112 auto fileSystemPackageSupplier = getRegistryPackageSupplier("file:///etc/dubpackages"); 113 assert(fileSystemPackageSupplier.description.canFind(" " ~ NativePath("/etc/dubpackages").toNativeString)); 114 } 115 116 /** Provides a high-level entry point for DUB's functionality. 117 118 This class provides means to load a certain project (a root package with 119 all of its dependencies) and to perform high-level operations as found in 120 the command line interface. 121 */ 122 class Dub { 123 private { 124 bool m_dryRun = false; 125 PackageManager m_packageManager; 126 PackageSupplier[] m_packageSuppliers; 127 NativePath m_rootPath; 128 SpecialDirs m_dirs; 129 DubConfig m_config; 130 NativePath m_projectPath; 131 Project m_project; 132 NativePath m_overrideSearchPath; 133 string m_defaultCompiler; 134 string m_defaultArchitecture; 135 } 136 137 /** The default placement location of fetched packages. 138 139 This property can be altered, so that packages which are downloaded as part 140 of the normal upgrade process are stored in a certain location. This is 141 how the "--local" and "--system" command line switches operate. 142 */ 143 PlacementLocation defaultPlacementLocation = PlacementLocation.user; 144 145 146 /** Initializes the instance for use with a specific root package. 147 148 Note that a package still has to be loaded using one of the 149 `loadPackage` overloads. 150 151 Params: 152 root_path = Path to the root package 153 additional_package_suppliers = A list of package suppliers to try 154 before the suppliers found in the configurations files and the 155 `defaultPackageSuppliers`. 156 skip_registry = Can be used to skip using the configured package 157 suppliers, as well as the default suppliers. 158 */ 159 this(string root_path = ".", PackageSupplier[] additional_package_suppliers = null, 160 SkipPackageSuppliers skip_registry = SkipPackageSuppliers.none) 161 { 162 m_rootPath = NativePath(root_path); 163 if (!m_rootPath.absolute) m_rootPath = NativePath(getcwd()) ~ m_rootPath; 164 165 init(m_rootPath); 166 167 if (skip_registry == SkipPackageSuppliers.none) 168 m_packageSuppliers = getPackageSuppliers(additional_package_suppliers); 169 else 170 m_packageSuppliers = getPackageSuppliers(additional_package_suppliers, skip_registry); 171 172 m_packageManager = new PackageManager(m_rootPath, m_dirs.localRepository, m_dirs.systemSettings); 173 174 auto ccps = m_config.customCachePaths; 175 if (ccps.length) 176 m_packageManager.customCachePaths = ccps; 177 178 updatePackageSearchPath(); 179 } 180 181 unittest 182 { 183 scope (exit) environment.remove("DUB_REGISTRY"); 184 auto dub = new Dub(".", null, SkipPackageSuppliers.configured); 185 assert(dub.m_packageSuppliers.length == 0); 186 environment["DUB_REGISTRY"] = "http://example.com/"; 187 dub = new Dub(".", null, SkipPackageSuppliers.configured); 188 assert(dub.m_packageSuppliers.length == 1); 189 environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; 190 dub = new Dub(".", null, SkipPackageSuppliers.configured); 191 assert(dub.m_packageSuppliers.length == 2); 192 dub = new Dub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); 193 assert(dub.m_packageSuppliers.length == 3); 194 } 195 196 /** Get the list of package suppliers. 197 198 Params: 199 additional_package_suppliers = A list of package suppliers to try 200 before the suppliers found in the configurations files and the 201 `defaultPackageSuppliers`. 202 skip_registry = Can be used to skip using the configured package 203 suppliers, as well as the default suppliers. 204 */ 205 public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry) 206 { 207 PackageSupplier[] ps = additional_package_suppliers; 208 209 if (skip_registry < SkipPackageSuppliers.all) 210 { 211 ps ~= environment.get("DUB_REGISTRY", null) 212 .splitter(";") 213 .map!(url => getRegistryPackageSupplier(url)) 214 .array; 215 } 216 217 if (skip_registry < SkipPackageSuppliers.configured) 218 { 219 ps ~= m_config.registryURLs 220 .map!(url => getRegistryPackageSupplier(url)) 221 .array; 222 } 223 224 if (skip_registry < SkipPackageSuppliers.standard) 225 ps ~= defaultPackageSuppliers(); 226 227 return ps; 228 } 229 230 /// ditto 231 public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers) 232 { 233 return getPackageSuppliers(additional_package_suppliers, m_config.skipRegistry); 234 } 235 236 unittest 237 { 238 scope (exit) environment.remove("DUB_REGISTRY"); 239 auto dub = new Dub(".", null, SkipPackageSuppliers.none); 240 241 dub.m_config = new DubConfig(Json(["skipRegistry": Json("none")]), null); 242 assert(dub.getPackageSuppliers(null).length == 1); 243 244 dub.m_config = new DubConfig(Json(["skipRegistry": Json("configured")]), null); 245 assert(dub.getPackageSuppliers(null).length == 0); 246 247 dub.m_config = new DubConfig(Json(["skipRegistry": Json("standard")]), null); 248 assert(dub.getPackageSuppliers(null).length == 0); 249 250 environment["DUB_REGISTRY"] = "http://example.com/"; 251 assert(dub.getPackageSuppliers(null).length == 1); 252 } 253 254 /** Initializes the instance with a single package search path, without 255 loading a package. 256 257 This constructor corresponds to the "--bare" option of the command line 258 interface. Use 259 */ 260 this(NativePath override_path) 261 { 262 init(NativePath()); 263 m_overrideSearchPath = override_path; 264 m_packageManager = new PackageManager(override_path); 265 } 266 267 private void init(NativePath root_path) 268 { 269 import std.file : tempDir; 270 version(Windows) { 271 m_dirs.systemSettings = NativePath(environment.get("ProgramData")) ~ "dub/"; 272 immutable appDataDir = environment.get("APPDATA"); 273 m_dirs.userSettings = NativePath(appDataDir) ~ "dub/"; 274 m_dirs.localRepository = NativePath(environment.get("LOCALAPPDATA", appDataDir)) ~ "dub"; 275 } else version(Posix){ 276 m_dirs.systemSettings = NativePath("/var/lib/dub/"); 277 m_dirs.userSettings = NativePath(environment.get("HOME")) ~ ".dub/"; 278 if (!m_dirs.userSettings.absolute) 279 m_dirs.userSettings = NativePath(getcwd()) ~ m_dirs.userSettings; 280 m_dirs.localRepository = m_dirs.userSettings; 281 } 282 283 m_dirs.temp = NativePath(tempDir); 284 285 m_config = new DubConfig(jsonFromFile(m_dirs.systemSettings ~ "settings.json", true), m_config); 286 m_config = new DubConfig(jsonFromFile(NativePath(thisExePath).parentPath ~ "../etc/dub/settings.json", true), m_config); 287 m_config = new DubConfig(jsonFromFile(m_dirs.userSettings ~ "settings.json", true), m_config); 288 289 if (!root_path.empty) 290 m_config = new DubConfig(jsonFromFile(root_path ~ "dub.settings.json", true), m_config); 291 292 determineDefaultCompiler(); 293 294 m_defaultArchitecture = m_config.defaultArchitecture; 295 } 296 297 @property void dryRun(bool v) { m_dryRun = v; } 298 299 /** Returns the root path (usually the current working directory). 300 */ 301 @property NativePath rootPath() const { return m_rootPath; } 302 /// ditto 303 @property void rootPath(NativePath root_path) 304 { 305 m_rootPath = root_path; 306 if (!m_rootPath.absolute) m_rootPath = NativePath(getcwd()) ~ m_rootPath; 307 } 308 309 /// Returns the name listed in the dub.json of the current 310 /// application. 311 @property string projectName() const { return m_project.name; } 312 313 @property NativePath projectPath() const { return m_projectPath; } 314 315 @property string[] configurations() const { return m_project.configurations; } 316 317 @property inout(PackageManager) packageManager() inout { return m_packageManager; } 318 319 @property inout(Project) project() inout { return m_project; } 320 321 /** Returns the default compiler binary to use for building D code. 322 323 If set, the "defaultCompiler" field of the DUB user or system 324 configuration file will be used. Otherwise the PATH environment variable 325 will be searched for files named "dmd", "gdc", "gdmd", "ldc2", "ldmd2" 326 (in that order, taking into account operating system specific file 327 extensions) and the first match is returned. If no match is found, "dmd" 328 will be used. 329 */ 330 @property string defaultCompiler() const { return m_defaultCompiler; } 331 332 /** Returns the default architecture to use for building D code. 333 334 If set, the "defaultArchitecture" field of the DUB user or system 335 configuration file will be used. Otherwise null will be returned. 336 */ 337 @property string defaultArchitecture() const { return m_defaultArchitecture; } 338 339 /** Loads the package that resides within the configured `rootPath`. 340 */ 341 void loadPackage() 342 { 343 loadPackage(m_rootPath); 344 } 345 346 /// Loads the package from the specified path as the main project package. 347 void loadPackage(NativePath path) 348 { 349 m_projectPath = path; 350 updatePackageSearchPath(); 351 m_project = new Project(m_packageManager, m_projectPath); 352 } 353 354 /// Loads a specific package as the main project package (can be a sub package) 355 void loadPackage(Package pack) 356 { 357 m_projectPath = pack.path; 358 updatePackageSearchPath(); 359 m_project = new Project(m_packageManager, pack); 360 } 361 362 /** Loads a single file package. 363 364 Single-file packages are D files that contain a package receipe comment 365 at their top. A recipe comment must be a nested `/+ ... +/` style 366 comment, containing the virtual recipe file name and a colon, followed by the 367 recipe contents (what would normally be in dub.sdl/dub.json). 368 369 Example: 370 --- 371 /+ dub.sdl: 372 name "test" 373 dependency "vibe-d" version="~>0.7.29" 374 +/ 375 import vibe.http.server; 376 377 void main() 378 { 379 auto settings = new HTTPServerSettings; 380 settings.port = 8080; 381 listenHTTP(settings, &hello); 382 } 383 384 void hello(HTTPServerRequest req, HTTPServerResponse res) 385 { 386 res.writeBody("Hello, World!"); 387 } 388 --- 389 390 The script above can be invoked with "dub --single test.d". 391 */ 392 void loadSingleFilePackage(NativePath path) 393 { 394 import dub.recipe.io : parsePackageRecipe; 395 import std.file : mkdirRecurse, readText; 396 import std.path : baseName, stripExtension; 397 398 path = makeAbsolute(path); 399 400 string file_content = readText(path.toNativeString()); 401 402 if (file_content.startsWith("#!")) { 403 auto idx = file_content.indexOf('\n'); 404 enforce(idx > 0, "The source fine doesn't contain anything but a shebang line."); 405 file_content = file_content[idx+1 .. $]; 406 } 407 408 file_content = file_content.strip(); 409 410 string recipe_content; 411 412 if (file_content.startsWith("/+")) { 413 file_content = file_content[2 .. $]; 414 auto idx = file_content.indexOf("+/"); 415 enforce(idx >= 0, "Missing \"+/\" to close comment."); 416 recipe_content = file_content[0 .. idx].strip(); 417 } else throw new Exception("The source file must start with a recipe comment."); 418 419 auto nidx = recipe_content.indexOf('\n'); 420 421 auto idx = recipe_content.indexOf(':'); 422 enforce(idx > 0 && (nidx < 0 || nidx > idx), 423 "The first line of the recipe comment must list the recipe file name followed by a colon (e.g. \"/+ dub.sdl:\")."); 424 auto recipe_filename = recipe_content[0 .. idx]; 425 recipe_content = recipe_content[idx+1 .. $]; 426 auto recipe_default_package_name = path.toString.baseName.stripExtension.strip; 427 428 auto recipe = parsePackageRecipe(recipe_content, recipe_filename, null, recipe_default_package_name); 429 enforce(recipe.buildSettings.sourceFiles.length == 0, "Single-file packages are not allowed to specify source files."); 430 enforce(recipe.buildSettings.sourcePaths.length == 0, "Single-file packages are not allowed to specify source paths."); 431 enforce(recipe.buildSettings.importPaths.length == 0, "Single-file packages are not allowed to specify import paths."); 432 recipe.buildSettings.sourceFiles[""] = [path.toNativeString()]; 433 recipe.buildSettings.sourcePaths[""] = []; 434 recipe.buildSettings.importPaths[""] = []; 435 recipe.buildSettings.mainSourceFile = path.toNativeString(); 436 if (recipe.buildSettings.targetType == TargetType.autodetect) 437 recipe.buildSettings.targetType = TargetType.executable; 438 439 auto pack = new Package(recipe, path.parentPath, null, "~master"); 440 loadPackage(pack); 441 } 442 /// ditto 443 void loadSingleFilePackage(string path) 444 { 445 loadSingleFilePackage(NativePath(path)); 446 } 447 448 deprecated("Instantiate a Dub instance with the single-argument constructor: `new Dub(path)`") 449 void overrideSearchPath(NativePath path) 450 { 451 if (!path.absolute) path = NativePath(getcwd()) ~ path; 452 m_overrideSearchPath = path; 453 updatePackageSearchPath(); 454 } 455 456 /** Gets the default configuration for a particular build platform. 457 458 This forwards to `Project.getDefaultConfiguration` and requires a 459 project to be loaded. 460 */ 461 string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } 462 463 /** Attempts to upgrade the dependency selection of the loaded project. 464 465 Params: 466 options = Flags that control how the upgrade is carried out 467 packages_to_upgrade = Optional list of packages. If this list 468 contains one or more packages, only those packages will 469 be upgraded. Otherwise, all packages will be upgraded at 470 once. 471 */ 472 void upgrade(UpgradeOptions options, string[] packages_to_upgrade = null) 473 { 474 // clear non-existent version selections 475 if (!(options & UpgradeOptions.upgrade)) { 476 next_pack: 477 foreach (p; m_project.selections.selectedPackages) { 478 auto dep = m_project.selections.getSelectedVersion(p); 479 if (!dep.path.empty) { 480 auto path = dep.path; 481 if (!path.absolute) path = this.rootPath ~ path; 482 try if (m_packageManager.getOrLoadPackage(path)) continue; 483 catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); } 484 } else { 485 if (m_packageManager.getPackage(p, dep.version_)) continue; 486 foreach (ps; m_packageSuppliers) { 487 try { 488 auto versions = ps.getVersions(p); 489 if (versions.canFind!(v => dep.matches(v))) 490 continue next_pack; 491 } catch (Exception e) { 492 logWarn("Error querying versions for %s, %s: %s", p, ps.description, e.msg); 493 logDebug("Full error: %s", e.toString().sanitize()); 494 } 495 } 496 } 497 498 logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep); 499 m_project.selections.deselectVersion(p); 500 } 501 } 502 503 Dependency[string] versions; 504 auto resolver = new DependencyVersionResolver(this, options); 505 foreach (p; packages_to_upgrade) 506 resolver.addPackageToUpgrade(p); 507 versions = resolver.resolve(m_project.rootPackage, m_project.selections); 508 509 if (options & UpgradeOptions.dryRun) { 510 bool any = false; 511 string rootbasename = getBasePackageName(m_project.rootPackage.name); 512 513 foreach (p, ver; versions) { 514 if (!ver.path.empty) continue; 515 516 auto basename = getBasePackageName(p); 517 if (basename == rootbasename) continue; 518 519 if (!m_project.selections.hasSelectedVersion(basename)) { 520 logInfo("Package %s would be selected with version %s.", 521 basename, ver); 522 any = true; 523 continue; 524 } 525 auto sver = m_project.selections.getSelectedVersion(basename); 526 if (!sver.path.empty) continue; 527 if (ver.version_ <= sver.version_) continue; 528 logInfo("Package %s would be upgraded from %s to %s.", 529 basename, sver, ver); 530 any = true; 531 } 532 if (any) logInfo("Use \"dub upgrade\" to perform those changes."); 533 return; 534 } 535 536 foreach (p; versions.byKey) { 537 auto ver = versions[p]; // Workaround for DMD 2.070.0 AA issue (crashes in aaApply2 if iterating by key+value) 538 assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p); 539 Package pack; 540 if (!ver.path.empty) { 541 try pack = m_packageManager.getOrLoadPackage(ver.path); 542 catch (Exception e) { 543 logDebug("Failed to load path based selection: %s", e.toString().sanitize); 544 continue; 545 } 546 } else { 547 pack = m_packageManager.getBestPackage(p, ver); 548 if (pack && m_packageManager.isManagedPackage(pack) 549 && ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0) 550 { 551 // TODO: only re-install if there is actually a new commit available 552 logInfo("Re-installing branch based dependency %s %s", p, ver.toString()); 553 m_packageManager.remove(pack); 554 pack = null; 555 } 556 } 557 558 FetchOptions fetchOpts; 559 fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none; 560 if (!pack) fetch(p, ver, defaultPlacementLocation, fetchOpts, "getting selected version"); 561 if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) { 562 if (ver.path.empty) m_project.selections.selectVersion(p, ver.version_); 563 else { 564 NativePath relpath = ver.path; 565 if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path); 566 m_project.selections.selectVersion(p, relpath); 567 } 568 } 569 } 570 571 string[] missingDependenciesBeforeReinit = m_project.missingDependencies; 572 m_project.reinit(); 573 574 if (!m_project.hasAllDependencies) { 575 auto resolvedDependencies = setDifference( 576 assumeSorted(missingDependenciesBeforeReinit), 577 assumeSorted(m_project.missingDependencies) 578 ); 579 if (!resolvedDependencies.empty) 580 upgrade(options, m_project.missingDependencies); 581 } 582 583 if ((options & UpgradeOptions.select) && !(options & (UpgradeOptions.noSaveSelections | UpgradeOptions.dryRun))) 584 m_project.saveSelections(); 585 } 586 587 /** Generate project files for a specified generator. 588 589 Any existing project files will be overridden. 590 */ 591 void generateProject(string ide, GeneratorSettings settings) 592 { 593 auto generator = createProjectGenerator(ide, m_project); 594 if (m_dryRun) return; // TODO: pass m_dryRun to the generator 595 generator.generate(settings); 596 } 597 598 /** Executes tests on the current project. 599 600 Throws an exception, if unittests failed. 601 */ 602 void testProject(GeneratorSettings settings, string config, NativePath custom_main_file) 603 { 604 if (!custom_main_file.empty && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; 605 606 if (config.length == 0) { 607 // if a custom main file was given, favor the first library configuration, so that it can be applied 608 if (!custom_main_file.empty) config = m_project.getDefaultConfiguration(settings.platform, false); 609 // else look for a "unittest" configuration 610 if (!config.length && m_project.rootPackage.configurations.canFind("unittest")) config = "unittest"; 611 // if not found, fall back to the first "library" configuration 612 if (!config.length) config = m_project.getDefaultConfiguration(settings.platform, false); 613 // if still nothing found, use the first executable configuration 614 if (!config.length) config = m_project.getDefaultConfiguration(settings.platform, true); 615 } 616 617 auto generator = createProjectGenerator("build", m_project); 618 619 auto test_config = format("%s-test-%s", m_project.rootPackage.name.replace(".", "-").replace(":", "-"), config); 620 621 BuildSettings lbuildsettings = settings.buildSettings; 622 m_project.addBuildSettings(lbuildsettings, settings, config, null, true); 623 if (lbuildsettings.targetType == TargetType.none) { 624 logInfo(`Configuration '%s' has target type "none". Skipping test.`, config); 625 return; 626 } 627 628 if (lbuildsettings.targetType == TargetType.executable && config == "unittest") { 629 logInfo("Running custom 'unittest' configuration.", config); 630 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 631 settings.config = config; 632 } else if (lbuildsettings.sourceFiles.empty) { 633 logInfo(`No source files found in configuration '%s'. Falling back to "dub -b unittest".`, config); 634 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 635 settings.config = m_project.getDefaultConfiguration(settings.platform); 636 } else { 637 import std.algorithm : remove; 638 639 logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, test_config, config, lbuildsettings.targetType); 640 641 BuildSettingsTemplate tcinfo = m_project.rootPackage.recipe.getConfiguration(config).buildSettings; 642 tcinfo.targetType = TargetType.executable; 643 tcinfo.targetName = test_config; 644 645 auto mainfil = tcinfo.mainSourceFile; 646 if (!mainfil.length) mainfil = m_project.rootPackage.recipe.buildSettings.mainSourceFile; 647 648 string custommodname; 649 if (!custom_main_file.empty) { 650 import std.path; 651 tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(m_project.rootPackage.path).toNativeString(); 652 tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString(); 653 custommodname = custom_main_file.head.name.baseName(".d"); 654 } 655 656 // prepare the list of tested modules 657 string[] import_modules; 658 foreach (file; lbuildsettings.sourceFiles) { 659 if (file.endsWith(".d")) { 660 auto fname = NativePath(file).head.name; 661 if (NativePath(file).relativeTo(m_project.rootPackage.path) == NativePath(mainfil)) { 662 logWarn("Excluding main source file %s from test.", mainfil); 663 tcinfo.excludedSourceFiles[""] ~= mainfil; 664 continue; 665 } 666 if (fname == "package.d") { 667 logWarn("Excluding package.d file from test due to https://issues.dlang.org/show_bug.cgi?id=11847"); 668 continue; 669 } 670 import_modules ~= dub.internal.utils.determineModuleName(lbuildsettings, NativePath(file), m_project.rootPackage.path); 671 } 672 } 673 674 // generate main file 675 NativePath mainfile = getTempFile("dub_test_root", ".d"); 676 tcinfo.sourceFiles[""] ~= mainfile.toNativeString(); 677 tcinfo.mainSourceFile = mainfile.toNativeString(); 678 if (!m_dryRun) { 679 auto fil = openFile(mainfile, FileMode.createTrunc); 680 scope(exit) fil.close(); 681 fil.write("module dub_test_root;\n"); 682 fil.write("import std.typetuple;\n"); 683 foreach (mod; import_modules) fil.write(format("static import %s;\n", mod)); 684 fil.write("alias allModules = TypeTuple!("); 685 foreach (i, mod; import_modules) { 686 if (i > 0) fil.write(", "); 687 fil.write(mod); 688 } 689 fil.write(");\n"); 690 if (custommodname.length) { 691 fil.write(format("import %s;\n", custommodname)); 692 } else { 693 fil.write(q{ 694 import std.stdio; 695 import core.runtime; 696 697 void main() { writeln("All unit tests have been run successfully."); } 698 shared static this() { 699 version (Have_tested) { 700 import tested; 701 import core.runtime; 702 import std.exception; 703 Runtime.moduleUnitTester = () => true; 704 //runUnitTests!app(new JsonTestResultWriter("results.json")); 705 enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed."); 706 } 707 } 708 }); 709 } 710 } 711 m_project.rootPackage.recipe.configurations ~= ConfigurationInfo(test_config, tcinfo); 712 m_project = new Project(m_packageManager, m_project.rootPackage); 713 714 settings.config = test_config; 715 } 716 717 generator.generate(settings); 718 } 719 720 /** Executes D-Scanner tests on the current project. **/ 721 void lintProject(string[] args) 722 { 723 import std.path : buildPath, buildNormalizedPath; 724 725 if (m_dryRun) return; 726 727 auto tool = "dscanner"; 728 729 auto tool_pack = m_packageManager.getBestPackage(tool, ">=0.0.0"); 730 if (!tool_pack) tool_pack = m_packageManager.getBestPackage(tool, "~master"); 731 if (!tool_pack) { 732 logInfo("%s is not present, getting and storing it user wide", tool); 733 tool_pack = fetch(tool, Dependency(">=0.0.0"), defaultPlacementLocation, FetchOptions.none); 734 } 735 736 auto dscanner_dub = new Dub(null, m_packageSuppliers); 737 dscanner_dub.loadPackage(tool_pack.path); 738 dscanner_dub.upgrade(UpgradeOptions.select); 739 740 auto compiler_binary = this.defaultCompiler; 741 742 GeneratorSettings settings; 743 settings.config = "application"; 744 settings.compiler = getCompiler(compiler_binary); 745 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary, m_defaultArchitecture); 746 settings.buildType = "debug"; 747 settings.run = true; 748 749 foreach (dependencyPackage; m_project.dependencies) 750 { 751 auto cfgs = m_project.getPackageConfigs(settings.platform, null, true); 752 auto buildSettings = dependencyPackage.getBuildSettings(settings.platform, cfgs[dependencyPackage.name]); 753 foreach (importPath; buildSettings.importPaths) { 754 settings.runArgs ~= ["-I", buildNormalizedPath(dependencyPackage.path.toNativeString(), importPath.idup)]; 755 } 756 } 757 758 string configFilePath = buildPath(m_project.rootPackage.path.toNativeString(), "dscanner.ini"); 759 if (!args.canFind("--config") && exists(configFilePath)) { 760 settings.runArgs ~= ["--config", configFilePath]; 761 } 762 763 settings.runArgs ~= args ~ [m_project.rootPackage.path.toNativeString()]; 764 dscanner_dub.generateProject("build", settings); 765 } 766 767 /** Prints the specified build settings necessary for building the root package. 768 */ 769 void listProjectData(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) 770 { 771 import std.stdio; 772 import std.ascii : newline; 773 774 // Split comma-separated lists 775 string[] requestedDataSplit = 776 requestedData 777 .map!(a => a.splitter(",").map!strip) 778 .joiner() 779 .array(); 780 781 auto data = m_project.listBuildSettings(settings, requestedDataSplit, list_type); 782 783 string delimiter; 784 final switch (list_type) with (ListBuildSettingsFormat) { 785 case list: delimiter = newline ~ newline; break; 786 case listNul: delimiter = "\0\0"; break; 787 case commandLine: delimiter = " "; break; 788 case commandLineNul: delimiter = "\0\0"; break; 789 } 790 791 write(data.joiner(delimiter)); 792 if (delimiter != "\0\0") writeln(); 793 } 794 795 /// Cleans intermediate/cache files of the given package 796 void cleanPackage(NativePath path) 797 { 798 logInfo("Cleaning package at %s...", path.toNativeString()); 799 enforce(!Package.findPackageFile(path).empty, "No package found.", path.toNativeString()); 800 801 // TODO: clear target files and copy files 802 803 if (existsFile(path ~ ".dub/build")) rmdirRecurse((path ~ ".dub/build").toNativeString()); 804 if (existsFile(path ~ ".dub/obj")) rmdirRecurse((path ~ ".dub/obj").toNativeString()); 805 if (existsFile(path ~ ".dub/metadata_cache.json")) std.file.remove((path ~ ".dub/metadata_cache.json").toNativeString()); 806 807 auto p = Package.load(path); 808 if (p.getBuildSettings().targetType == TargetType.none) { 809 foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) { 810 cleanPackage(path ~ sp.path); 811 } 812 } 813 } 814 815 /// Fetches the package matching the dependency and places it in the specified location. 816 Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "") 817 { 818 auto basePackageName = getBasePackageName(packageId); 819 Json pinfo; 820 PackageSupplier supplier; 821 foreach(ps; m_packageSuppliers){ 822 try { 823 pinfo = ps.fetchPackageRecipe(basePackageName, dep, (options & FetchOptions.usePrerelease) != 0); 824 if (pinfo.type == Json.Type.null_) 825 continue; 826 supplier = ps; 827 break; 828 } catch(Exception e) { 829 logWarn("Package %s not found for %s: %s", packageId, ps.description, e.msg); 830 logDebug("Full error: %s", e.toString().sanitize()); 831 } 832 } 833 enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency "~dep.toString()); 834 string ver = pinfo["version"].get!string; 835 836 NativePath placement; 837 final switch (location) { 838 case PlacementLocation.local: placement = m_rootPath ~ ".dub/packages/"; break; 839 case PlacementLocation.user: placement = m_dirs.localRepository ~ "packages/"; break; 840 case PlacementLocation.system: placement = m_dirs.systemSettings ~ "packages/"; break; 841 } 842 843 // always upgrade branch based versions - TODO: actually check if there is a new commit available 844 Package existing; 845 try existing = m_packageManager.getPackage(packageId, ver, placement); 846 catch (Exception e) { 847 logWarn("Failed to load existing package %s: %s", ver, e.msg); 848 logDiagnostic("Full error: %s", e.toString().sanitize); 849 } 850 851 if (options & FetchOptions.printOnly) { 852 if (existing && existing.version_ != Version(ver)) 853 logInfo("A new version for %s is available (%s -> %s). Run \"dub upgrade %s\" to switch.", 854 packageId, existing.version_, ver, packageId); 855 return null; 856 } 857 858 if (existing) { 859 if (!ver.startsWith("~") || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) { 860 // TODO: support git working trees by performing a "git pull" instead of this 861 logDiagnostic("Package %s %s (%s) is already present with the latest version, skipping upgrade.", 862 packageId, ver, placement); 863 return existing; 864 } else { 865 logInfo("Removing %s %s to prepare replacement with a new version.", packageId, ver); 866 if (!m_dryRun) m_packageManager.remove(existing); 867 } 868 } 869 870 if (reason.length) logInfo("Fetching %s %s (%s)...", packageId, ver, reason); 871 else logInfo("Fetching %s %s...", packageId, ver); 872 if (m_dryRun) return null; 873 874 logDebug("Acquiring package zip file"); 875 876 auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $]; 877 clean_package_version = clean_package_version.replace("+", "_"); // + has special meaning for Optlink 878 if (!placement.existsFile()) 879 mkdirRecurse(placement.toNativeString()); 880 NativePath dstpath = placement ~ (basePackageName ~ "-" ~ clean_package_version); 881 if (!dstpath.existsFile()) 882 mkdirRecurse(dstpath.toNativeString()); 883 884 // Support libraries typically used with git submodules like ae. 885 // Such libraries need to have ".." as import path but this can create 886 // import path leakage. 887 dstpath = dstpath ~ basePackageName; 888 889 import std.datetime : seconds; 890 auto lock = lockFile(dstpath.toNativeString() ~ ".lock", 30.seconds); // possibly wait for other dub instance 891 if (dstpath.existsFile()) 892 { 893 m_packageManager.refresh(false); 894 return m_packageManager.getPackage(packageId, ver, dstpath); 895 } 896 897 // repeat download on corrupted zips, see #1336 898 foreach_reverse (i; 0..3) 899 { 900 import std.zip : ZipException; 901 902 auto path = getTempFile(basePackageName, ".zip"); 903 supplier.fetchPackage(path, basePackageName, dep, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? 904 scope(exit) std.file.remove(path.toNativeString()); 905 logDiagnostic("Placing to %s...", placement.toNativeString()); 906 907 try { 908 m_packageManager.storeFetchedPackage(path, pinfo, dstpath); 909 return m_packageManager.getPackage(packageId, ver, dstpath); 910 } catch (ZipException e) { 911 logInfo("Failed to extract zip archive for %s %s...", packageId, ver); 912 // rethrow the exception at the end of the loop 913 if (i == 0) 914 throw e; 915 } 916 } 917 assert(0, "Should throw a ZipException instead."); 918 } 919 920 /** Removes a specific locally cached package. 921 922 This will delete the package files from disk and removes the 923 corresponding entry from the list of known packages. 924 925 Params: 926 pack = Package instance to remove 927 */ 928 void remove(in Package pack) 929 { 930 logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); 931 if (!m_dryRun) m_packageManager.remove(pack); 932 } 933 934 /// Compatibility overload. Use the version without a `force_remove` argument instead. 935 void remove(in Package pack, bool force_remove) 936 { 937 remove(pack); 938 } 939 940 /// @see remove(string, string, RemoveLocation) 941 enum RemoveVersionWildcard = "*"; 942 943 /** Removes one or more versions of a locally cached package. 944 945 This will remove a given package with a specified version from the 946 given location. It will remove at most one package, unless `version_` 947 is set to `RemoveVersionWildcard`. 948 949 Params: 950 package_id = Name of the package to be removed 951 location_ = Specifies the location to look for the given package 952 name/version. 953 resolve_version = Callback to select package version. 954 */ 955 void remove(string package_id, PlacementLocation location, 956 scope size_t delegate(in Package[] packages) resolve_version) 957 { 958 enforce(!package_id.empty); 959 if (location == PlacementLocation.local) { 960 logInfo("To remove a locally placed package, make sure you don't have any data" 961 ~ "\nleft in it's directory and then simply remove the whole directory."); 962 throw new Exception("dub cannot remove locally installed packages."); 963 } 964 965 Package[] packages; 966 967 // Retrieve packages to be removed. 968 foreach(pack; m_packageManager.getPackageIterator(package_id)) 969 if (m_packageManager.isManagedPackage(pack)) 970 packages ~= pack; 971 972 // Check validity of packages to be removed. 973 if(packages.empty) { 974 throw new Exception("Cannot find package to remove. (" 975 ~ "id: '" ~ package_id ~ "', location: '" ~ to!string(location) ~ "'" 976 ~ ")"); 977 } 978 979 // Sort package list in ascending version order 980 packages.sort!((a, b) => a.version_ < b.version_); 981 982 immutable idx = resolve_version(packages); 983 if (idx == size_t.max) 984 return; 985 else if (idx != packages.length) 986 packages = packages[idx .. idx + 1]; 987 988 logDebug("Removing %s packages.", packages.length); 989 foreach(pack; packages) { 990 try { 991 remove(pack); 992 logInfo("Removed %s, version %s.", package_id, pack.version_); 993 } catch (Exception e) { 994 logError("Failed to remove %s %s: %s", package_id, pack.version_, e.msg); 995 logInfo("Continuing with other packages (if any)."); 996 } 997 } 998 } 999 1000 /// Compatibility overload. Use the version without a `force_remove` argument instead. 1001 void remove(string package_id, PlacementLocation location, bool force_remove, 1002 scope size_t delegate(in Package[] packages) resolve_version) 1003 { 1004 remove(package_id, location, resolve_version); 1005 } 1006 1007 /** Removes a specific version of a package. 1008 1009 Params: 1010 package_id = Name of the package to be removed 1011 version_ = Identifying a version or a wild card. If an empty string 1012 is passed, the package will be removed from the location, if 1013 there is only one version retrieved. This will throw an 1014 exception, if there are multiple versions retrieved. 1015 location_ = Specifies the location to look for the given package 1016 name/version. 1017 */ 1018 void remove(string package_id, string version_, PlacementLocation location) 1019 { 1020 remove(package_id, location, (in packages) { 1021 if (version_ == RemoveVersionWildcard) 1022 return packages.length; 1023 if (version_.empty && packages.length > 1) { 1024 logError("Cannot remove package '" ~ package_id ~ "', there are multiple possibilities at location\n" 1025 ~ "'" ~ to!string(location) ~ "'."); 1026 logError("Available versions:"); 1027 foreach(pack; packages) 1028 logError(" %s", pack.version_); 1029 throw new Exception("Please specify a individual version using --version=... or use the" 1030 ~ " wildcard --version=" ~ RemoveVersionWildcard ~ " to remove all versions."); 1031 } 1032 foreach (i, p; packages) { 1033 if (p.version_ == Version(version_)) 1034 return i; 1035 } 1036 throw new Exception("Cannot find package to remove. (" 1037 ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'" 1038 ~ ")"); 1039 }); 1040 } 1041 1042 /// Compatibility overload. Use the version without a `force_remove` argument instead. 1043 void remove(string package_id, string version_, PlacementLocation location, bool force_remove) 1044 { 1045 remove(package_id, version_, location); 1046 } 1047 1048 /** Adds a directory to the list of locally known packages. 1049 1050 Forwards to `PackageManager.addLocalPackage`. 1051 1052 Params: 1053 path = Path to the package 1054 ver = Optional version to associate with the package (can be left 1055 empty) 1056 system = Make the package known system wide instead of user wide 1057 (requires administrator privileges). 1058 1059 See_Also: `removeLocalPackage` 1060 */ 1061 void addLocalPackage(string path, string ver, bool system) 1062 { 1063 if (m_dryRun) return; 1064 m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user); 1065 } 1066 1067 /** Removes a directory from the list of locally known packages. 1068 1069 Forwards to `PackageManager.removeLocalPackage`. 1070 1071 Params: 1072 path = Path to the package 1073 system = Make the package known system wide instead of user wide 1074 (requires administrator privileges). 1075 1076 See_Also: `addLocalPackage` 1077 */ 1078 void removeLocalPackage(string path, bool system) 1079 { 1080 if (m_dryRun) return; 1081 m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 1082 } 1083 1084 /** Registers a local directory to search for packages to use for satisfying 1085 dependencies. 1086 1087 Params: 1088 path = Path to a directory containing package directories 1089 system = Make the package known system wide instead of user wide 1090 (requires administrator privileges). 1091 1092 See_Also: `removeSearchPath` 1093 */ 1094 void addSearchPath(string path, bool system) 1095 { 1096 if (m_dryRun) return; 1097 m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 1098 } 1099 1100 /** Unregisters a local directory search path. 1101 1102 Params: 1103 path = Path to a directory containing package directories 1104 system = Make the package known system wide instead of user wide 1105 (requires administrator privileges). 1106 1107 See_Also: `addSearchPath` 1108 */ 1109 void removeSearchPath(string path, bool system) 1110 { 1111 if (m_dryRun) return; 1112 m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 1113 } 1114 1115 /** Queries all package suppliers with the given query string. 1116 1117 Returns a list of tuples, where the first entry is the human readable 1118 name of the package supplier and the second entry is the list of 1119 matched packages. 1120 1121 See_Also: `PackageSupplier.searchPackages` 1122 */ 1123 auto searchPackages(string query) 1124 { 1125 import std.typecons : Tuple, tuple; 1126 Tuple!(string, PackageSupplier.SearchResult[])[] results; 1127 foreach (ps; this.m_packageSuppliers) { 1128 try 1129 results ~= tuple(ps.description, ps.searchPackages(query)); 1130 catch (Exception e) { 1131 logWarn("Searching %s for '%s' failed: %s", ps.description, query, e.msg); 1132 } 1133 } 1134 return results.filter!(tup => tup[1].length); 1135 } 1136 1137 /** Returns a list of all available versions (including branches) for a 1138 particular package. 1139 1140 The list returned is based on the registered package suppliers. Local 1141 packages are not queried in the search for versions. 1142 1143 See_also: `getLatestVersion` 1144 */ 1145 Version[] listPackageVersions(string name) 1146 { 1147 Version[] versions; 1148 auto basePackageName = getBasePackageName(name); 1149 foreach (ps; this.m_packageSuppliers) { 1150 try versions ~= ps.getVersions(basePackageName); 1151 catch (Exception e) { 1152 logWarn("Failed to get versions for package %s on provider %s: %s", name, ps.description, e.msg); 1153 } 1154 } 1155 return versions.sort().uniq.array; 1156 } 1157 1158 /** Returns the latest available version for a particular package. 1159 1160 This function returns the latest numbered version of a package. If no 1161 numbered versions are available, it will return an available branch, 1162 preferring "~master". 1163 1164 Params: 1165 package_name: The name of the package in question. 1166 prefer_stable: If set to `true` (the default), returns the latest 1167 stable version, even if there are newer pre-release versions. 1168 1169 See_also: `listPackageVersions` 1170 */ 1171 Version getLatestVersion(string package_name, bool prefer_stable = true) 1172 { 1173 auto vers = listPackageVersions(package_name); 1174 enforce(!vers.empty, "Failed to find any valid versions for a package name of '"~package_name~"'."); 1175 auto final_versions = vers.filter!(v => !v.isBranch && !v.isPreRelease).array; 1176 if (prefer_stable && final_versions.length) return final_versions[$-1]; 1177 else return vers[$-1]; 1178 } 1179 1180 /** Initializes a directory with a package skeleton. 1181 1182 Params: 1183 path = Path of the directory to create the new package in. The 1184 directory will be created if it doesn't exist. 1185 deps = List of dependencies to add to the package recipe. 1186 type = Specifies the type of the application skeleton to use. 1187 format = Determines the package recipe format to use. 1188 recipe_callback = Optional callback that can be used to 1189 customize the recipe before it gets written. 1190 */ 1191 void createEmptyPackage(NativePath path, string[] deps, string type, 1192 PackageFormat format = PackageFormat.sdl, 1193 scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null, 1194 string[] app_args = []) 1195 { 1196 if (!path.absolute) path = m_rootPath ~ path; 1197 path.normalize(); 1198 1199 string[string] depVers; 1200 string[] notFound; // keep track of any failed packages in here 1201 foreach (dep; deps) { 1202 Version ver; 1203 try { 1204 ver = getLatestVersion(dep); 1205 depVers[dep] = ver.isBranch ? ver.toString() : "~>" ~ ver.toString(); 1206 } catch (Exception e) { 1207 notFound ~= dep; 1208 } 1209 } 1210 1211 if(notFound.length > 1){ 1212 throw new Exception(.format("Couldn't find packages: %-(%s, %).", notFound)); 1213 } 1214 else if(notFound.length == 1){ 1215 throw new Exception(.format("Couldn't find package: %-(%s, %).", notFound)); 1216 } 1217 1218 if (m_dryRun) return; 1219 1220 initPackage(path, depVers, type, format, recipe_callback); 1221 1222 if (!["vibe.d", "deimos", "minimal"].canFind(type)) { 1223 runCustomInitialization(path, type, app_args); 1224 } 1225 1226 //Act smug to the user. 1227 logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); 1228 } 1229 1230 private void runCustomInitialization(NativePath path, string type, string[] runArgs) 1231 { 1232 string packageName = type; 1233 auto template_pack = m_packageManager.getBestPackage(packageName, ">=0.0.0"); 1234 if (!template_pack) template_pack = m_packageManager.getBestPackage(packageName, "~master"); 1235 if (!template_pack) { 1236 logInfo("%s is not present, getting and storing it user wide", packageName); 1237 template_pack = fetch(packageName, Dependency(">=0.0.0"), defaultPlacementLocation, FetchOptions.none); 1238 } 1239 1240 Package initSubPackage = m_packageManager.getSubPackage(template_pack, "init-exec", false); 1241 auto template_dub = new Dub(null, m_packageSuppliers); 1242 template_dub.loadPackage(initSubPackage); 1243 auto compiler_binary = this.defaultCompiler; 1244 1245 GeneratorSettings settings; 1246 settings.config = "application"; 1247 settings.compiler = getCompiler(compiler_binary); 1248 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary, m_defaultArchitecture); 1249 settings.buildType = "debug"; 1250 settings.run = true; 1251 settings.runArgs = runArgs; 1252 initSubPackage.recipe.buildSettings.workingDirectory = path.toNativeString(); 1253 template_dub.generateProject("build", settings); 1254 } 1255 1256 /** Converts the package recipe of the loaded root package to the given format. 1257 1258 Params: 1259 destination_file_ext = The file extension matching the desired 1260 format. Possible values are "json" or "sdl". 1261 print_only = Print the converted recipe instead of writing to disk 1262 */ 1263 void convertRecipe(string destination_file_ext, bool print_only = false) 1264 { 1265 import std.path : extension; 1266 import std.stdio : stdout; 1267 import dub.recipe.io : serializePackageRecipe, writePackageRecipe; 1268 1269 if (print_only) { 1270 auto dst = stdout.lockingTextWriter; 1271 serializePackageRecipe(dst, m_project.rootPackage.rawRecipe, "dub."~destination_file_ext); 1272 return; 1273 } 1274 1275 auto srcfile = m_project.rootPackage.recipePath; 1276 auto srcext = srcfile.head.name.extension; 1277 if (srcext == "."~destination_file_ext) { 1278 logInfo("Package format is already %s.", destination_file_ext); 1279 return; 1280 } 1281 1282 writePackageRecipe(srcfile.parentPath ~ ("dub."~destination_file_ext), m_project.rootPackage.rawRecipe); 1283 removeFile(srcfile); 1284 } 1285 1286 /** Runs DDOX to generate or serve documentation. 1287 1288 Params: 1289 run = If set to true, serves documentation on a local web server. 1290 Otherwise generates actual HTML files. 1291 generate_args = Additional command line arguments to pass to 1292 "ddox generate-html" or "ddox serve-html". 1293 */ 1294 void runDdox(bool run, string[] generate_args = null) 1295 { 1296 import std.process : browse; 1297 1298 if (m_dryRun) return; 1299 1300 // allow to choose a custom ddox tool 1301 auto tool = m_project.rootPackage.recipe.ddoxTool; 1302 if (tool.empty) tool = "ddox"; 1303 1304 auto tool_pack = m_packageManager.getBestPackage(tool, ">=0.0.0"); 1305 if (!tool_pack) tool_pack = m_packageManager.getBestPackage(tool, "~master"); 1306 if (!tool_pack) { 1307 logInfo("%s is not present, getting and storing it user wide", tool); 1308 tool_pack = fetch(tool, Dependency(">=0.0.0"), defaultPlacementLocation, FetchOptions.none); 1309 } 1310 1311 auto ddox_dub = new Dub(null, m_packageSuppliers); 1312 ddox_dub.loadPackage(tool_pack.path); 1313 ddox_dub.upgrade(UpgradeOptions.select); 1314 1315 auto compiler_binary = this.defaultCompiler; 1316 1317 GeneratorSettings settings; 1318 settings.config = "application"; 1319 settings.compiler = getCompiler(compiler_binary); // TODO: not using --compiler ??? 1320 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary, m_defaultArchitecture); 1321 settings.buildType = "debug"; 1322 settings.run = true; 1323 1324 auto filterargs = m_project.rootPackage.recipe.ddoxFilterArgs.dup; 1325 if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; 1326 1327 settings.runArgs = "filter" ~ filterargs ~ "docs.json"; 1328 ddox_dub.generateProject("build", settings); 1329 1330 auto p = tool_pack.path; 1331 p.endsWithSlash = true; 1332 auto tool_path = p.toNativeString(); 1333 1334 if (run) { 1335 settings.runArgs = ["serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~tool_path~"public"] ~ generate_args; 1336 browse("http://127.0.0.1:8080/"); 1337 } else { 1338 settings.runArgs = ["generate-html", "--navigation-type=ModuleTree", "docs.json", "docs"] ~ generate_args; 1339 } 1340 ddox_dub.generateProject("build", settings); 1341 1342 if (!run) { 1343 // TODO: ddox should copy those files itself 1344 version(Windows) runCommand(`xcopy /S /D "`~tool_path~`public\*" docs\`); 1345 else runCommand("rsync -ru '"~tool_path~"public/' docs/"); 1346 } 1347 } 1348 1349 private void updatePackageSearchPath() 1350 { 1351 // TODO: Remove once `overrideSearchPath` is removed 1352 if (!m_overrideSearchPath.empty) { 1353 m_packageManager._disableDefaultSearchPaths = true; 1354 m_packageManager.searchPath = [m_overrideSearchPath]; 1355 return; 1356 } 1357 1358 auto p = environment.get("DUBPATH"); 1359 NativePath[] paths; 1360 1361 version(Windows) enum pathsep = ";"; 1362 else enum pathsep = ":"; 1363 if (p.length) paths ~= p.split(pathsep).map!(p => NativePath(p))().array(); 1364 m_packageManager._disableDefaultSearchPaths = false; 1365 m_packageManager.searchPath = paths; 1366 } 1367 1368 private void determineDefaultCompiler() 1369 { 1370 import std.file : thisExePath; 1371 import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator; 1372 import std.process : environment; 1373 import std.range : front; 1374 1375 m_defaultCompiler = m_config.defaultCompiler.expandTilde; 1376 if (m_defaultCompiler.length && m_defaultCompiler.isAbsolute) 1377 return; 1378 1379 static immutable BinaryPrefix = `$DUB_BINARY_PATH`; 1380 if(m_defaultCompiler.startsWith(BinaryPrefix)) 1381 { 1382 m_defaultCompiler = thisExePath().dirName() ~ m_defaultCompiler[BinaryPrefix.length .. $]; 1383 return; 1384 } 1385 1386 if (!find!isDirSeparator(m_defaultCompiler).empty) 1387 throw new Exception("defaultCompiler specified in a DUB config file cannot use an unqualified relative path:\n\n" ~ m_defaultCompiler ~ 1388 "\n\nUse \"$DUB_BINARY_PATH/../path/you/want\" instead."); 1389 1390 version (Windows) enum sep = ";", exe = ".exe"; 1391 version (Posix) enum sep = ":", exe = ""; 1392 1393 auto compilers = ["dmd", "gdc", "gdmd", "ldc2", "ldmd2"]; 1394 // If a compiler name is specified, look for it next to dub. 1395 // Otherwise, look for any of the common compilers adjacent to dub. 1396 if (m_defaultCompiler.length) 1397 { 1398 string compilerPath = buildPath(thisExePath().dirName(), m_defaultCompiler ~ exe); 1399 if (existsFile(compilerPath)) 1400 { 1401 m_defaultCompiler = compilerPath; 1402 return; 1403 } 1404 } 1405 else 1406 { 1407 auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe))); 1408 if (!nextFound.empty) 1409 { 1410 m_defaultCompiler = buildPath(thisExePath().dirName(), nextFound.front ~ exe); 1411 return; 1412 } 1413 } 1414 1415 // If nothing found next to dub, search the user's PATH, starting 1416 // with the compiler name from their DUB config file, if specified. 1417 if (m_defaultCompiler.length) 1418 compilers = m_defaultCompiler ~ compilers; 1419 auto paths = environment.get("PATH", "").splitter(sep).map!NativePath; 1420 auto res = compilers.find!(bin => paths.canFind!(p => existsFile(p ~ (bin~exe)))); 1421 m_defaultCompiler = res.empty ? compilers[0] : res.front; 1422 } 1423 1424 private NativePath makeAbsolute(NativePath p) const { return p.absolute ? p : m_rootPath ~ p; } 1425 private NativePath makeAbsolute(string p) const { return makeAbsolute(NativePath(p)); } 1426 } 1427 1428 1429 /// Option flags for `Dub.fetch` 1430 enum FetchOptions 1431 { 1432 none = 0, 1433 forceBranchUpgrade = 1<<0, 1434 usePrerelease = 1<<1, 1435 forceRemove = 1<<2, /// Deprecated, does nothing. 1436 printOnly = 1<<3, 1437 } 1438 1439 /// Option flags for `Dub.upgrade` 1440 enum UpgradeOptions 1441 { 1442 none = 0, 1443 upgrade = 1<<1, /// Upgrade existing packages 1444 preRelease = 1<<2, /// inclde pre-release versions in upgrade 1445 forceRemove = 1<<3, /// Deprecated, does nothing. 1446 select = 1<<4, /// Update the dub.selections.json file with the upgraded versions 1447 dryRun = 1<<5, /// Instead of downloading new packages, just print a message to notify the user of their existence 1448 /*deprecated*/ printUpgradesOnly = dryRun, /// deprecated, use dryRun instead 1449 /*deprecated*/ useCachedResult = 1<<6, /// deprecated, has no effect 1450 noSaveSelections = 1<<7, /// Don't store updated selections on disk 1451 } 1452 1453 /// Determines which of the default package suppliers are queried for packages. 1454 enum SkipPackageSuppliers { 1455 none, /// Uses all configured package suppliers. 1456 standard, /// Does not use the default package suppliers (`defaultPackageSuppliers`). 1457 configured, /// Does not use default suppliers or suppliers configured in DUB's configuration file 1458 all /// Uses only manually specified package suppliers. 1459 } 1460 1461 private class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) { 1462 protected { 1463 Dub m_dub; 1464 UpgradeOptions m_options; 1465 Dependency[][string] m_packageVersions; 1466 Package[string] m_remotePackages; 1467 SelectedVersions m_selectedVersions; 1468 Package m_rootPackage; 1469 bool[string] m_packagesToUpgrade; 1470 Package[PackageDependency] m_packages; 1471 TreeNodes[][TreeNode] m_children; 1472 } 1473 1474 1475 this(Dub dub, UpgradeOptions options) 1476 { 1477 m_dub = dub; 1478 m_options = options; 1479 } 1480 1481 void addPackageToUpgrade(string name) 1482 { 1483 m_packagesToUpgrade[name] = true; 1484 } 1485 1486 Dependency[string] resolve(Package root, SelectedVersions selected_versions) 1487 { 1488 m_rootPackage = root; 1489 m_selectedVersions = selected_versions; 1490 return super.resolve(TreeNode(root.name, Dependency(root.version_)), (m_options & UpgradeOptions.printUpgradesOnly) == 0); 1491 } 1492 1493 protected bool isFixedPackage(string pack) 1494 { 1495 return m_packagesToUpgrade !is null && pack !in m_packagesToUpgrade; 1496 } 1497 1498 protected override Dependency[] getAllConfigs(string pack) 1499 { 1500 if (auto pvers = pack in m_packageVersions) 1501 return *pvers; 1502 1503 if ((!(m_options & UpgradeOptions.upgrade) || isFixedPackage(pack)) && m_selectedVersions.hasSelectedVersion(pack)) { 1504 auto ret = [m_selectedVersions.getSelectedVersion(pack)]; 1505 logDiagnostic("Using fixed selection %s %s", pack, ret[0]); 1506 m_packageVersions[pack] = ret; 1507 return ret; 1508 } 1509 1510 logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length); 1511 Version[] versions; 1512 foreach (p; m_dub.packageManager.getPackageIterator(pack)) 1513 versions ~= p.version_; 1514 1515 foreach (ps; m_dub.m_packageSuppliers) { 1516 try { 1517 auto vers = ps.getVersions(pack); 1518 vers.reverse(); 1519 if (!vers.length) { 1520 logDiagnostic("No versions for %s for %s", pack, ps.description); 1521 continue; 1522 } 1523 1524 versions ~= vers; 1525 break; 1526 } catch (Exception e) { 1527 logWarn("Package %s not found in %s: %s", pack, ps.description, e.msg); 1528 logDebug("Full error: %s", e.toString().sanitize); 1529 } 1530 } 1531 1532 // sort by version, descending, and remove duplicates 1533 versions = versions.sort!"a>b".uniq.array; 1534 1535 // move pre-release versions to the back of the list if no preRelease flag is given 1536 if (!(m_options & UpgradeOptions.preRelease)) 1537 versions = versions.filter!(v => !v.isPreRelease).array ~ versions.filter!(v => v.isPreRelease).array; 1538 1539 // filter out invalid/unreachable dependency specs 1540 versions = versions.filter!((v) { 1541 bool valid = getPackage(pack, Dependency(v)) !is null; 1542 if (!valid) logDiagnostic("Excluding invalid dependency specification %s %s from dependency resolution process.", pack, v); 1543 return valid; 1544 }).array; 1545 1546 if (!versions.length) logDiagnostic("Nothing found for %s", pack); 1547 else logDiagnostic("Return for %s: %s", pack, versions); 1548 1549 auto ret = versions.map!(v => Dependency(v)).array; 1550 m_packageVersions[pack] = ret; 1551 return ret; 1552 } 1553 1554 protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes) 1555 { 1556 if (!nodes.configs.path.empty && getPackage(pack, nodes.configs)) return [nodes.configs]; 1557 else return null; 1558 } 1559 1560 1561 protected override TreeNodes[] getChildren(TreeNode node) 1562 { 1563 if (auto pc = node in m_children) 1564 return *pc; 1565 auto ret = getChildrenRaw(node); 1566 m_children[node] = ret; 1567 return ret; 1568 } 1569 1570 private final TreeNodes[] getChildrenRaw(TreeNode node) 1571 { 1572 import std.array : appender; 1573 auto ret = appender!(TreeNodes[]); 1574 auto pack = getPackage(node.pack, node.config); 1575 if (!pack) { 1576 // this can hapen when the package description contains syntax errors 1577 logDebug("Invalid package in dependency tree: %s %s", node.pack, node.config); 1578 return null; 1579 } 1580 auto basepack = pack.basePackage; 1581 1582 foreach (d; pack.getAllDependenciesRange()) { 1583 auto dbasename = getBasePackageName(d.name); 1584 1585 // detect dependencies to the root package (or sub packages thereof) 1586 if (dbasename == basepack.name) { 1587 auto absdeppath = d.spec.mapToPath(pack.path).path; 1588 absdeppath.endsWithSlash = true; 1589 auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true); 1590 if (subpack) { 1591 auto desireddeppath = d.name == dbasename ? basepack.path : subpack.path; 1592 desireddeppath.endsWithSlash = true; 1593 enforce(d.spec.path.empty || absdeppath == desireddeppath, 1594 format("Dependency from %s to root package references wrong path: %s vs. %s", 1595 node.pack, absdeppath.toNativeString(), desireddeppath.toNativeString())); 1596 } 1597 ret ~= TreeNodes(d.name, node.config); 1598 continue; 1599 } 1600 1601 DependencyType dt; 1602 if (d.spec.optional) { 1603 if (d.spec.default_) dt = DependencyType.optionalDefault; 1604 else dt = DependencyType.optional; 1605 } else dt = DependencyType.required; 1606 1607 Dependency dspec = d.spec.mapToPath(pack.path); 1608 1609 // if not upgrading, use the selected version 1610 if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions && m_selectedVersions.hasSelectedVersion(dbasename)) 1611 dspec = m_selectedVersions.getSelectedVersion(dbasename); 1612 1613 // keep selected optional dependencies and avoid non-selected optional-default dependencies by default 1614 if (m_selectedVersions && !m_selectedVersions.bare) { 1615 if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(dbasename)) 1616 dt = DependencyType.optional; 1617 else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(dbasename)) 1618 dt = DependencyType.optionalDefault; 1619 } 1620 1621 ret ~= TreeNodes(d.name, dspec, dt); 1622 } 1623 return ret.data; 1624 } 1625 1626 protected override bool matches(Dependency configs, Dependency config) 1627 { 1628 if (!configs.path.empty) return configs.path == config.path; 1629 return configs.merge(config).valid; 1630 } 1631 1632 private Package getPackage(string name, Dependency dep) 1633 { 1634 auto key = PackageDependency(name, dep); 1635 if (auto pp = key in m_packages) 1636 return *pp; 1637 auto p = getPackageRaw(name, dep); 1638 m_packages[key] = p; 1639 return p; 1640 } 1641 1642 private Package getPackageRaw(string name, Dependency dep) 1643 { 1644 auto basename = getBasePackageName(name); 1645 1646 // for sub packages, first try to get them from the base package 1647 if (basename != name) { 1648 auto subname = getSubPackageName(name); 1649 auto basepack = getPackage(basename, dep); 1650 if (!basepack) return null; 1651 if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) { 1652 return sp; 1653 } else if (!basepack.subPackages.canFind!(p => p.path.length)) { 1654 // note: external sub packages are handled further below 1655 auto spr = basepack.getInternalSubPackage(subname); 1656 if (!spr.isNull) { 1657 auto sp = new Package(spr.get, basepack.path, basepack); 1658 m_remotePackages[sp.name] = sp; 1659 return sp; 1660 } else { 1661 logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep.version_); 1662 return null; 1663 } 1664 } else if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) { 1665 return ret; 1666 } else { 1667 logDiagnostic("External sub package %s %s not found.", name, dep.version_); 1668 return null; 1669 } 1670 } 1671 1672 // shortcut if the referenced package is the root package 1673 if (basename == m_rootPackage.basePackage.name) 1674 return m_rootPackage.basePackage; 1675 1676 if (!dep.path.empty) { 1677 try { 1678 auto ret = m_dub.packageManager.getOrLoadPackage(dep.path); 1679 if (dep.matches(ret.version_)) return ret; 1680 } catch (Exception e) { 1681 logDiagnostic("Failed to load path based dependency %s: %s", name, e.msg); 1682 logDebug("Full error: %s", e.toString().sanitize); 1683 return null; 1684 } 1685 } 1686 1687 if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) 1688 return ret; 1689 1690 auto key = name ~ ":" ~ dep.version_.toString(); 1691 if (auto ret = key in m_remotePackages) 1692 return *ret; 1693 1694 auto prerelease = (m_options & UpgradeOptions.preRelease) != 0; 1695 1696 auto rootpack = name.split(":")[0]; 1697 1698 foreach (ps; m_dub.m_packageSuppliers) { 1699 if (rootpack == name) { 1700 try { 1701 auto desc = ps.fetchPackageRecipe(name, dep, prerelease); 1702 if (desc.type == Json.Type.null_) 1703 continue; 1704 auto ret = new Package(desc); 1705 m_remotePackages[key] = ret; 1706 return ret; 1707 } catch (Exception e) { 1708 logDiagnostic("Metadata for %s %s could not be downloaded from %s: %s", name, dep, ps.description, e.msg); 1709 logDebug("Full error: %s", e.toString().sanitize); 1710 } 1711 } else { 1712 logDiagnostic("Package %s not found in base package description (%s). Downloading whole package.", name, dep.version_.toString()); 1713 try { 1714 FetchOptions fetchOpts; 1715 fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none; 1716 m_dub.fetch(rootpack, dep, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description"); 1717 auto ret = m_dub.m_packageManager.getBestPackage(name, dep); 1718 if (!ret) { 1719 logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep.version_, name); 1720 return null; 1721 } 1722 m_remotePackages[key] = ret; 1723 return ret; 1724 } catch (Exception e) { 1725 logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg); 1726 logDebug("Full error: %s", e.toString().sanitize); 1727 } 1728 } 1729 } 1730 1731 m_remotePackages[key] = null; 1732 1733 logWarn("Package %s %s could not be loaded either locally, or from the configured package registries.", name, dep); 1734 return null; 1735 } 1736 } 1737 1738 private struct SpecialDirs { 1739 NativePath temp; 1740 NativePath userSettings; 1741 NativePath systemSettings; 1742 NativePath localRepository; 1743 } 1744 1745 private class DubConfig { 1746 private { 1747 DubConfig m_parentConfig; 1748 Json m_data; 1749 } 1750 1751 this(Json data, DubConfig parent_config) 1752 { 1753 m_data = data; 1754 m_parentConfig = parent_config; 1755 } 1756 1757 @property string[] registryURLs() 1758 { 1759 string[] ret; 1760 if (auto pv = "registryUrls" in m_data) 1761 ret = (*pv).deserializeJson!(string[]); 1762 if (m_parentConfig) ret ~= m_parentConfig.registryURLs; 1763 return ret; 1764 } 1765 1766 @property SkipPackageSuppliers skipRegistry() 1767 { 1768 if(auto pv = "skipRegistry" in m_data) 1769 return to!SkipPackageSuppliers((*pv).get!string); 1770 1771 if (m_parentConfig) 1772 return m_parentConfig.skipRegistry; 1773 1774 return SkipPackageSuppliers.none; 1775 } 1776 1777 @property NativePath[] customCachePaths() 1778 { 1779 import std.algorithm.iteration : map; 1780 import std.array : array; 1781 1782 NativePath[] ret; 1783 if (auto pv = "customCachePaths" in m_data) 1784 ret = (*pv).deserializeJson!(string[]) 1785 .map!(s => NativePath(s)) 1786 .array; 1787 if (m_parentConfig) 1788 ret ~= m_parentConfig.customCachePaths; 1789 return ret; 1790 } 1791 1792 @property string defaultCompiler() 1793 const { 1794 if (auto pv = "defaultCompiler" in m_data) 1795 return pv.get!string; 1796 if (m_parentConfig) return m_parentConfig.defaultCompiler; 1797 return null; 1798 } 1799 1800 @property string defaultArchitecture() 1801 const { 1802 if(auto pv = "defaultArchitecture" in m_data) 1803 return (*pv).get!string; 1804 if (m_parentConfig) return m_parentConfig.defaultArchitecture; 1805 return null; 1806 } 1807 }