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