1 /** 2 A package manager. 3 4 Copyright: © 2012-2013 Matthias Dondorff 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.packagesupplier; 21 import dub.project; 22 import dub.generators.generator; 23 import dub.init; 24 25 26 // todo: cleanup imports. 27 import std.algorithm; 28 import std.array; 29 import std.conv; 30 import std.datetime; 31 import std.exception; 32 import std.file; 33 import std.process; 34 import std..string; 35 import std.typecons; 36 import std.zip; 37 import std.encoding : sanitize; 38 39 // Workaround for libcurl liker errors when building with LDC 40 version (LDC) pragma(lib, "curl"); 41 42 43 /// The default supplier for packages, which is the registry 44 /// hosted by code.dlang.org. 45 PackageSupplier[] defaultPackageSuppliers() 46 { 47 URL url = URL.parse("http://code.dlang.org/"); 48 logDiagnostic("Using dub registry url '%s'", url); 49 return [new RegistryPackageSupplier(url)]; 50 } 51 52 /// Option flags for fetch 53 enum FetchOptions 54 { 55 none = 0, 56 forceBranchUpgrade = 1<<0, 57 usePrerelease = 1<<1, 58 forceRemove = 1<<2, 59 printOnly = 1<<3, 60 } 61 62 /// The Dub class helps in getting the applications 63 /// dependencies up and running. An instance manages one application. 64 class Dub { 65 private { 66 bool m_dryRun = false; 67 PackageManager m_packageManager; 68 PackageSupplier[] m_packageSuppliers; 69 Path m_rootPath; 70 Path m_tempPath; 71 Path m_userDubPath, m_systemDubPath; 72 Json m_systemConfig, m_userConfig; 73 Path m_projectPath; 74 Project m_project; 75 Path m_overrideSearchPath; 76 } 77 78 /// Initiales the package manager for the vibe application 79 /// under root. 80 this(PackageSupplier[] additional_package_suppliers = null, string root_path = ".") 81 { 82 m_rootPath = Path(root_path); 83 if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath; 84 85 version(Windows){ 86 m_systemDubPath = Path(environment.get("ProgramData")) ~ "dub/"; 87 m_userDubPath = Path(environment.get("APPDATA")) ~ "dub/"; 88 m_tempPath = Path(environment.get("TEMP")); 89 } else version(Posix){ 90 m_systemDubPath = Path("/var/lib/dub/"); 91 m_userDubPath = Path(environment.get("HOME")) ~ ".dub/"; 92 if(!m_userDubPath.absolute) 93 m_userDubPath = Path(getcwd()) ~ m_userDubPath; 94 m_tempPath = Path("/tmp"); 95 } 96 97 m_userConfig = jsonFromFile(m_userDubPath ~ "settings.json", true); 98 m_systemConfig = jsonFromFile(m_systemDubPath ~ "settings.json", true); 99 100 PackageSupplier[] ps = additional_package_suppliers; 101 if (auto pp = "registryUrls" in m_userConfig) 102 ps ~= deserializeJson!(string[])(*pp) 103 .map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))) 104 .array; 105 if (auto pp = "registryUrls" in m_systemConfig) 106 ps ~= deserializeJson!(string[])(*pp) 107 .map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))) 108 .array; 109 ps ~= defaultPackageSuppliers(); 110 111 auto cacheDir = m_userDubPath ~ "cache/"; 112 foreach (p; ps) 113 p.cacheOp(cacheDir, CacheOp.load); 114 115 m_packageSuppliers = ps; 116 m_packageManager = new PackageManager(m_userDubPath, m_systemDubPath); 117 updatePackageSearchPath(); 118 } 119 120 /// Initializes DUB with only a single search path 121 this(Path override_path) 122 { 123 m_overrideSearchPath = override_path; 124 m_packageManager = new PackageManager(Path(), Path(), false); 125 updatePackageSearchPath(); 126 } 127 128 /// Perform cleanup and persist caches to disk 129 void shutdown() 130 { 131 auto cacheDir = m_userDubPath ~ "cache/"; 132 foreach (p; m_packageSuppliers) 133 p.cacheOp(cacheDir, CacheOp.store); 134 } 135 136 /// cleans all metadata caches 137 void cleanCaches() 138 { 139 auto cacheDir = m_userDubPath ~ "cache/"; 140 foreach (p; m_packageSuppliers) 141 p.cacheOp(cacheDir, CacheOp.clean); 142 } 143 144 @property void dryRun(bool v) { m_dryRun = v; } 145 146 /** Returns the root path (usually the current working directory). 147 */ 148 @property Path rootPath() const { return m_rootPath; } 149 /// ditto 150 @property void rootPath(Path root_path) 151 { 152 m_rootPath = root_path; 153 if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath; 154 } 155 156 /// Returns the name listed in the dub.json of the current 157 /// application. 158 @property string projectName() const { return m_project.name; } 159 160 @property Path projectPath() const { return m_projectPath; } 161 162 @property string[] configurations() const { return m_project.configurations; } 163 164 @property inout(PackageManager) packageManager() inout { return m_packageManager; } 165 166 @property inout(Project) project() inout { return m_project; } 167 168 /// Loads the package from the current working directory as the main 169 /// project package. 170 void loadPackageFromCwd() 171 { 172 loadPackage(m_rootPath); 173 } 174 175 /// Loads the package from the specified path as the main project package. 176 void loadPackage(Path path) 177 { 178 m_projectPath = path; 179 updatePackageSearchPath(); 180 m_project = new Project(m_packageManager, m_projectPath); 181 } 182 183 /// Loads a specific package as the main project package (can be a sub package) 184 void loadPackage(Package pack) 185 { 186 m_projectPath = pack.path; 187 updatePackageSearchPath(); 188 m_project = new Project(m_packageManager, pack); 189 } 190 191 void overrideSearchPath(Path path) 192 { 193 if (!path.absolute) path = Path(getcwd()) ~ path; 194 m_overrideSearchPath = path; 195 updatePackageSearchPath(); 196 } 197 198 string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } 199 200 void upgrade(UpgradeOptions options) 201 { 202 // clear non-existent version selections 203 if (!(options & UpgradeOptions.upgrade)) { 204 next_pack: 205 foreach (p; m_project.selections.selectedPackages) { 206 auto dep = m_project.selections.getSelectedVersion(p); 207 if (!dep.path.empty) { 208 try if (m_packageManager.getOrLoadPackage(dep.path)) continue; 209 catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); } 210 } else { 211 if (m_packageManager.getPackage(p, dep.version_)) continue; 212 foreach (ps; m_packageSuppliers) { 213 try { 214 auto versions = ps.getVersions(p); 215 if (versions.canFind!(v => dep.matches(v))) 216 continue next_pack; 217 } catch (Exception e) { 218 logDiagnostic("Error querying versions for %s, %s: %s", p, ps.description, e.msg); 219 logDebug("Full error: %s", e.toString().sanitize()); 220 } 221 } 222 } 223 224 logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep); 225 m_project.selections.deselectVersion(p); 226 } 227 } 228 229 Dependency[string] versions; 230 if ((options & UpgradeOptions.useCachedResult) && m_project.isUpgradeCacheUpToDate()) { 231 logDiagnostic("Using cached upgrade results..."); 232 versions = m_project.getUpgradeCache(); 233 } else { 234 auto resolver = new DependencyVersionResolver(this, options); 235 versions = resolver.resolve(m_project.rootPackage, m_project.selections); 236 if (options & UpgradeOptions.useCachedResult) { 237 logDiagnostic("Caching upgrade results..."); 238 m_project.setUpgradeCache(versions); 239 } 240 } 241 242 if (options & UpgradeOptions.printUpgradesOnly) { 243 bool any = false; 244 string rootbasename = getBasePackageName(m_project.rootPackage.name); 245 246 foreach (p, ver; versions) { 247 if (!ver.path.empty) continue; 248 249 auto basename = getBasePackageName(p); 250 if (basename == rootbasename) continue; 251 252 if (!m_project.selections.hasSelectedVersion(basename)) { 253 logInfo("Package %s can be installed with version %s.", 254 basename, ver); 255 any = true; 256 continue; 257 } 258 auto sver = m_project.selections.getSelectedVersion(basename); 259 if (!sver.path.empty) continue; 260 if (ver.version_ <= sver.version_) continue; 261 logInfo("Package %s can be upgraded from %s to %s.", 262 basename, sver, ver); 263 any = true; 264 } 265 if (any) logInfo("Use \"dub upgrade\" to perform those changes."); 266 return; 267 } 268 269 foreach (p, ver; versions) { 270 assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p); 271 Package pack; 272 if (!ver.path.empty) { 273 try pack = m_packageManager.getOrLoadPackage(ver.path); 274 catch (Exception e) { 275 logDebug("Failed to load path based selection: %s", e.toString().sanitize); 276 continue; 277 } 278 } else { 279 pack = m_packageManager.getBestPackage(p, ver); 280 if (pack && m_packageManager.isManagedPackage(pack) 281 && ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0) 282 { 283 // TODO: only re-install if there is actually a new commit available 284 logInfo("Re-installing branch based dependency %s %s", p, ver.toString()); 285 m_packageManager.remove(pack, (options & UpgradeOptions.forceRemove) != 0); 286 pack = null; 287 } 288 } 289 290 FetchOptions fetchOpts; 291 fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none; 292 fetchOpts |= (options & UpgradeOptions.forceRemove) != 0 ? FetchOptions.forceRemove : FetchOptions.none; 293 if (!pack) fetch(p, ver, defaultPlacementLocation, fetchOpts, "getting selected version"); 294 if ((options & UpgradeOptions.select) && ver.path.empty && p != m_project.rootPackage.name) 295 m_project.selections.selectVersion(p, ver.version_); 296 } 297 298 m_project.reinit(); 299 300 if (options & UpgradeOptions.select) 301 m_project.saveSelections(); 302 } 303 304 /// Generate project files for a specified IDE. 305 /// Any existing project files will be overridden. 306 void generateProject(string ide, GeneratorSettings settings) { 307 auto generator = createProjectGenerator(ide, m_project); 308 if (m_dryRun) return; // TODO: pass m_dryRun to the generator 309 generator.generate(settings); 310 } 311 312 /// Executes tests on the current project. Throws an exception, if 313 /// unittests failed. 314 void testProject(GeneratorSettings settings, string config, Path custom_main_file) 315 { 316 if (custom_main_file.length && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; 317 318 if (config.length == 0) { 319 // if a custom main file was given, favor the first library configuration, so that it can be applied 320 if (custom_main_file.length) config = m_project.getDefaultConfiguration(settings.platform, false); 321 // else look for a "unittest" configuration 322 if (!config.length && m_project.rootPackage.configurations.canFind("unittest")) config = "unittest"; 323 // if not found, fall back to the first "library" configuration 324 if (!config.length) config = m_project.getDefaultConfiguration(settings.platform, false); 325 // if still nothing found, use the first executable configuration 326 if (!config.length) config = m_project.getDefaultConfiguration(settings.platform, true); 327 } 328 329 auto generator = createProjectGenerator("build", m_project); 330 331 auto test_config = format("__test__%s__", config); 332 333 BuildSettings lbuildsettings = settings.buildSettings; 334 m_project.addBuildSettings(lbuildsettings, settings.platform, config, null, true); 335 if (lbuildsettings.targetType == TargetType.none) { 336 logInfo(`Configuration '%s' has target type "none". Skipping test.`, config); 337 return; 338 } 339 340 if (lbuildsettings.targetType == TargetType.executable) { 341 if (config == "unittest") logInfo("Running custom 'unittest' configuration.", config); 342 else logInfo(`Configuration '%s' does not output a library. Falling back to "dub -b unittest -c %s".`, config, config); 343 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 344 settings.config = config; 345 } else if (lbuildsettings.sourceFiles.empty) { 346 logInfo(`No source files found in configuration '%s'. Falling back to "dub -b unittest".`, config); 347 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 348 settings.config = m_project.getDefaultConfiguration(settings.platform); 349 } else { 350 logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, test_config, config, lbuildsettings.targetType); 351 352 BuildSettingsTemplate tcinfo = m_project.rootPackage.info.getConfiguration(config).buildSettings; 353 tcinfo.targetType = TargetType.executable; 354 tcinfo.targetName = test_config; 355 tcinfo.versions[""] ~= "VibeCustomMain"; // HACK for vibe.d's legacy main() behavior 356 string custommodname; 357 if (custom_main_file.length) { 358 import std.path; 359 tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(m_project.rootPackage.path).toNativeString(); 360 tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString(); 361 custommodname = custom_main_file.head.toString().baseName(".d"); 362 } 363 364 string[] import_modules; 365 foreach (file; lbuildsettings.sourceFiles) { 366 if (file.endsWith(".d") && Path(file).head.toString() != "package.d") 367 import_modules ~= lbuildsettings.determineModuleName(Path(file), m_project.rootPackage.path); 368 } 369 370 // generate main file 371 Path mainfile = getTempFile("dub_test_root", ".d"); 372 tcinfo.sourceFiles[""] ~= mainfile.toNativeString(); 373 tcinfo.mainSourceFile = mainfile.toNativeString(); 374 if (!m_dryRun) { 375 auto fil = openFile(mainfile, FileMode.CreateTrunc); 376 scope(exit) fil.close(); 377 fil.write("module dub_test_root;\n"); 378 fil.write("import std.typetuple;\n"); 379 foreach (mod; import_modules) fil.write(format("static import %s;\n", mod)); 380 fil.write("alias allModules = TypeTuple!("); 381 foreach (i, mod; import_modules) { 382 if (i > 0) fil.write(", "); 383 fil.write(mod); 384 } 385 fil.write(");\n"); 386 if (custommodname.length) { 387 fil.write(format("import %s;\n", custommodname)); 388 } else { 389 fil.write(q{ 390 import std.stdio; 391 import core.runtime; 392 393 void main() { writeln("All unit tests have been run successfully."); } 394 shared static this() { 395 version (Have_tested) { 396 import tested; 397 import core.runtime; 398 import std.exception; 399 Runtime.moduleUnitTester = () => true; 400 //runUnitTests!app(new JsonTestResultWriter("results.json")); 401 enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed."); 402 } 403 } 404 }); 405 } 406 } 407 m_project.rootPackage.info.configurations ~= ConfigurationInfo(test_config, tcinfo); 408 m_project = new Project(m_packageManager, m_project.rootPackage); 409 410 settings.config = test_config; 411 } 412 413 generator.generate(settings); 414 } 415 416 /// Outputs a JSON description of the project, including its dependencies. 417 deprecated void describeProject(BuildPlatform platform, string config) 418 { 419 import std.stdio; 420 auto desc = m_project.describe(platform, config); 421 writeln(desc.serializeToPrettyJson()); 422 } 423 424 void listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) 425 { 426 import std.stdio; 427 428 foreach(path; m_project.listImportPaths(platform, config, buildType, nullDelim)) { 429 writeln(path); 430 } 431 } 432 433 void listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) 434 { 435 import std.stdio; 436 437 foreach(path; m_project.listStringImportPaths(platform, config, buildType, nullDelim)) { 438 writeln(path); 439 } 440 } 441 442 void listProjectData(BuildPlatform platform, string config, string buildType, 443 string[] requestedData, Compiler formattingCompiler, bool nullDelim) 444 { 445 import std.stdio; 446 import std.ascii : newline; 447 448 auto data = m_project.listBuildSettings(platform, config, buildType, requestedData, formattingCompiler, nullDelim); 449 write( data.joiner(nullDelim? "\0" : newline) ); 450 if(!nullDelim) 451 writeln(); 452 } 453 454 /// Cleans intermediate/cache files of the given package 455 void cleanPackage(Path path) 456 { 457 logInfo("Cleaning package at %s...", path.toNativeString()); 458 enforce(!Package.findPackageFile(path).empty, "No package found.", path.toNativeString()); 459 460 // TODO: clear target files and copy files 461 462 if (existsFile(path ~ ".dub/build")) rmdirRecurse((path ~ ".dub/build").toNativeString()); 463 if (existsFile(path ~ ".dub/obj")) rmdirRecurse((path ~ ".dub/obj").toNativeString()); 464 } 465 466 467 /// Returns all cached packages as a "packageId" = "version" associative array 468 string[string] cachedPackages() const { return m_project.cachedPackagesIDs; } 469 470 /// Fetches the package matching the dependency and places it in the specified location. 471 Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "") 472 { 473 Json pinfo; 474 PackageSupplier supplier; 475 foreach(ps; m_packageSuppliers){ 476 try { 477 pinfo = ps.getPackageDescription(packageId, dep, (options & FetchOptions.usePrerelease) != 0); 478 supplier = ps; 479 break; 480 } catch(Exception e) { 481 logDiagnostic("Package %s not found for %s: %s", packageId, ps.description, e.msg); 482 logDebug("Full error: %s", e.toString().sanitize()); 483 } 484 } 485 enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency "~dep.toString()); 486 string ver = pinfo["version"].get!string; 487 488 Path placement; 489 final switch (location) { 490 case PlacementLocation.local: placement = m_rootPath; break; 491 case PlacementLocation.user: placement = m_userDubPath ~ "packages/"; break; 492 case PlacementLocation.system: placement = m_systemDubPath ~ "packages/"; break; 493 } 494 495 // always upgrade branch based versions - TODO: actually check if there is a new commit available 496 Package existing; 497 try existing = m_packageManager.getPackage(packageId, ver, placement); 498 catch (Exception e) { 499 logWarn("Failed to load existing package %s: %s", ver, e.msg); 500 logDiagnostic("Full error: %s", e.toString().sanitize); 501 } 502 503 if (options & FetchOptions.printOnly) { 504 if (existing && existing.vers != ver) 505 logInfo("A new version for %s is available (%s -> %s). Run \"dub upgrade %s\" to switch.", 506 packageId, existing.vers, ver, packageId); 507 return null; 508 } 509 510 if (existing) { 511 if (!ver.startsWith("~") || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) { 512 // TODO: support git working trees by performing a "git pull" instead of this 513 logDiagnostic("Package %s %s (%s) is already present with the latest version, skipping upgrade.", 514 packageId, ver, placement); 515 return existing; 516 } else { 517 logInfo("Removing %s %s to prepare replacement with a new version.", packageId, ver); 518 if (!m_dryRun) m_packageManager.remove(existing, (options & FetchOptions.forceRemove) != 0); 519 } 520 } 521 522 if (reason.length) logInfo("Fetching %s %s (%s)...", packageId, ver, reason); 523 else logInfo("Fetching %s %s...", packageId, ver); 524 if (m_dryRun) return null; 525 526 logDiagnostic("Acquiring package zip file"); 527 auto dload = m_projectPath ~ ".dub/temp/downloads"; 528 auto tempfname = packageId ~ "-" ~ (ver.startsWith('~') ? ver[1 .. $] : ver) ~ ".zip"; 529 auto tempFile = m_tempPath ~ tempfname; 530 string sTempFile = tempFile.toNativeString(); 531 if (exists(sTempFile)) std.file.remove(sTempFile); 532 supplier.retrievePackage(tempFile, packageId, dep, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? 533 scope(exit) std.file.remove(sTempFile); 534 535 logInfo("Placing %s %s to %s...", packageId, ver, placement.toNativeString()); 536 auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $]; 537 clean_package_version = clean_package_version.replace("+", "_"); // + has special meaning for Optlink 538 Path dstpath = placement ~ (packageId ~ "-" ~ clean_package_version); 539 540 return m_packageManager.storeFetchedPackage(tempFile, pinfo, dstpath); 541 } 542 543 /// Removes a given package from the list of present/cached modules. 544 /// @removeFromApplication: if true, this will also remove an entry in the 545 /// list of dependencies in the application's dub.json 546 void remove(in Package pack, bool force_remove) 547 { 548 logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); 549 if (!m_dryRun) m_packageManager.remove(pack, force_remove); 550 } 551 552 /// @see remove(string, string, RemoveLocation) 553 enum RemoveVersionWildcard = "*"; 554 555 /// This will remove a given package with a specified version from the 556 /// location. 557 /// It will remove at most one package, unless @param version_ is 558 /// specified as wildcard "*". 559 /// @param package_id Package to be removed 560 /// @param version_ Identifying a version or a wild card. An empty string 561 /// may be passed into. In this case the package will be removed from the 562 /// location, if there is only one version retrieved. This will throw an 563 /// exception, if there are multiple versions retrieved. 564 /// Note: as wildcard string only RemoveVersionWildcard ("*") is supported. 565 /// @param location_ 566 void remove(string package_id, string version_, PlacementLocation location_, bool force_remove) 567 { 568 enforce(!package_id.empty); 569 if (location_ == PlacementLocation.local) { 570 logInfo("To remove a locally placed package, make sure you don't have any data" 571 ~ "\nleft in it's directory and then simply remove the whole directory."); 572 throw new Exception("dub cannot remove locally installed packages."); 573 } 574 575 Package[] packages; 576 const bool wildcardOrEmpty = version_ == RemoveVersionWildcard || version_.empty; 577 578 // Retrieve packages to be removed. 579 foreach(pack; m_packageManager.getPackageIterator(package_id)) 580 if( wildcardOrEmpty || pack.vers == version_ ) 581 packages ~= pack; 582 583 // Check validity of packages to be removed. 584 if(packages.empty) { 585 throw new Exception("Cannot find package to remove. (" 586 ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location_) ~ "'" 587 ~ ")"); 588 } 589 if(version_.empty && packages.length > 1) { 590 logError("Cannot remove package '" ~ package_id ~ "', there are multiple possibilities at location\n" 591 ~ "'" ~ to!string(location_) ~ "'."); 592 logError("Available versions:"); 593 foreach(pack; packages) 594 logError(" %s", pack.vers); 595 throw new Exception("Please specify a individual version using --version=... or use the" 596 ~ " wildcard --version=" ~ RemoveVersionWildcard ~ " to remove all versions."); 597 } 598 599 logDebug("Removing %s packages.", packages.length); 600 foreach(pack; packages) { 601 try { 602 remove(pack, force_remove); 603 logInfo("Removed %s, version %s.", package_id, pack.vers); 604 } catch (Exception e) { 605 logError("Failed to remove %s %s: %s", package_id, pack.vers, e.msg); 606 logInfo("Continuing with other packages (if any)."); 607 } 608 } 609 } 610 611 void addLocalPackage(string path, string ver, bool system) 612 { 613 if (m_dryRun) return; 614 m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user); 615 } 616 617 void removeLocalPackage(string path, bool system) 618 { 619 if (m_dryRun) return; 620 m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 621 } 622 623 void addSearchPath(string path, bool system) 624 { 625 if (m_dryRun) return; 626 m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 627 } 628 629 void removeSearchPath(string path, bool system) 630 { 631 if (m_dryRun) return; 632 m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 633 } 634 635 void createEmptyPackage(Path path, string[] deps, string type) 636 { 637 if (!path.absolute) path = m_rootPath ~ path; 638 path.normalize(); 639 640 if (m_dryRun) return; 641 string[string] depVers; 642 string[] notFound; // keep track of any failed packages in here 643 foreach(ps; this.m_packageSuppliers){ 644 foreach(dep; deps){ 645 try{ 646 auto versionStrings = ps.getVersions(dep); 647 depVers[dep] = versionStrings[$-1].toString; 648 } catch(Exception e){ 649 notFound ~= dep; 650 } 651 } 652 } 653 if(notFound.length > 1){ 654 throw new Exception(format("Couldn't find packages: %-(%s, %).", notFound)); 655 } 656 else if(notFound.length == 1){ 657 throw new Exception(format("Couldn't find package: %-(%s, %).", notFound)); 658 } 659 660 initPackage(path, depVers, type); 661 662 //Act smug to the user. 663 logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); 664 } 665 666 void runDdox(bool run) 667 { 668 if (m_dryRun) return; 669 670 auto ddox_pack = m_packageManager.getBestPackage("ddox", ">=0.0.0"); 671 if (!ddox_pack) ddox_pack = m_packageManager.getBestPackage("ddox", "~master"); 672 if (!ddox_pack) { 673 logInfo("DDOX is not present, getting it and storing user wide"); 674 ddox_pack = fetch("ddox", Dependency(">=0.0.0"), defaultPlacementLocation, FetchOptions.none); 675 } 676 677 version(Windows) auto ddox_exe = "ddox.exe"; 678 else auto ddox_exe = "ddox"; 679 680 if( !existsFile(ddox_pack.path~ddox_exe) ){ 681 logInfo("DDOX in %s is not built, performing build now.", ddox_pack.path.toNativeString()); 682 683 auto ddox_dub = new Dub(m_packageSuppliers); 684 ddox_dub.loadPackage(ddox_pack.path); 685 ddox_dub.upgrade(UpgradeOptions.select); 686 687 auto compiler_binary = "dmd"; 688 689 GeneratorSettings settings; 690 settings.config = "application"; 691 settings.compiler = getCompiler(compiler_binary); 692 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary); 693 settings.buildType = "debug"; 694 ddox_dub.generateProject("build", settings); 695 696 //runCommands(["cd "~ddox_pack.path.toNativeString()~" && dub build -v"]); 697 } 698 699 auto p = ddox_pack.path; 700 p.endsWithSlash = true; 701 auto dub_path = p.toNativeString(); 702 703 string[] commands; 704 string[] filterargs = m_project.rootPackage.info.ddoxFilterArgs.dup; 705 if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; 706 commands ~= dub_path~"ddox filter "~filterargs.join(" ")~" docs.json"; 707 if (!run) { 708 commands ~= dub_path~"ddox generate-html --navigation-type=ModuleTree docs.json docs"; 709 version(Windows) commands ~= "xcopy /S /D "~dub_path~"public\\* docs\\"; 710 else commands ~= "rsync -ru '"~dub_path~"public/' docs/"; 711 } 712 runCommands(commands); 713 714 if (run) { 715 auto proc = spawnProcess([dub_path~"ddox", "serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~dub_path~"public"]); 716 browse("http://127.0.0.1:8080/"); 717 wait(proc); 718 } 719 } 720 721 private void updatePackageSearchPath() 722 { 723 if (m_overrideSearchPath.length) { 724 m_packageManager.disableDefaultSearchPaths = true; 725 m_packageManager.searchPath = [m_overrideSearchPath]; 726 } else { 727 auto p = environment.get("DUBPATH"); 728 Path[] paths; 729 730 version(Windows) enum pathsep = ";"; 731 else enum pathsep = ":"; 732 if (p.length) paths ~= p.split(pathsep).map!(p => Path(p))().array(); 733 m_packageManager.disableDefaultSearchPaths = false; 734 m_packageManager.searchPath = paths; 735 } 736 } 737 738 private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; } 739 private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); } 740 } 741 742 string determineModuleName(BuildSettings settings, Path file, Path base_path) 743 { 744 assert(base_path.absolute); 745 if (!file.absolute) file = base_path ~ file; 746 747 size_t path_skip = 0; 748 foreach (ipath; settings.importPaths.map!(p => Path(p))) { 749 if (!ipath.absolute) ipath = base_path ~ ipath; 750 assert(!ipath.empty); 751 if (file.startsWith(ipath) && ipath.length > path_skip) 752 path_skip = ipath.length; 753 } 754 755 enforce(path_skip > 0, 756 format("Source file '%s' not found in any import path.", file.toNativeString())); 757 758 auto mpath = file[path_skip .. file.length]; 759 auto ret = appender!string; 760 761 //search for module keyword in file 762 string moduleName = getModuleNameFromFile(file.to!string); 763 764 if(moduleName.length) return moduleName; 765 766 //create module name from path 767 foreach (i; 0 .. mpath.length) { 768 import std.path; 769 auto p = mpath[i].toString(); 770 if (p == "package.d") break; 771 if (i > 0) ret ~= "."; 772 if (i+1 < mpath.length) ret ~= p; 773 else ret ~= p.baseName(".d"); 774 } 775 776 return ret.data; 777 } 778 779 /** 780 * Search for module keyword in D Code 781 */ 782 string getModuleNameFromContent(string content) { 783 import std.regex; 784 import std..string; 785 786 content = content.strip; 787 if (!content.length) return null; 788 789 static bool regex_initialized = false; 790 static Regex!char comments_pattern, module_pattern; 791 792 if (!regex_initialized) { 793 comments_pattern = regex(`(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)`, "g"); 794 module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); 795 regex_initialized = true; 796 } 797 798 content = replaceAll(content, comments_pattern, ""); 799 auto result = matchFirst(content, module_pattern); 800 801 string moduleName; 802 if(!result.empty) moduleName = result.front; 803 804 if (moduleName.length >= 7) moduleName = moduleName[7..$-1]; 805 806 return moduleName; 807 } 808 809 unittest { 810 //test empty string 811 string name = getModuleNameFromContent(""); 812 assert(name == "", "can't get module name from empty string"); 813 814 //test simple name 815 name = getModuleNameFromContent("module myPackage.myModule;"); 816 assert(name == "myPackage.myModule", "can't parse module name"); 817 818 //test if it can ignore module inside comments 819 name = getModuleNameFromContent("/** 820 module fakePackage.fakeModule; 821 */ 822 module myPackage.myModule;"); 823 824 assert(name == "myPackage.myModule", "can't parse module name"); 825 826 name = getModuleNameFromContent("//module fakePackage.fakeModule; 827 module myPackage.myModule;"); 828 829 assert(name == "myPackage.myModule", "can't parse module name"); 830 } 831 832 /** 833 * Search for module keyword in file 834 */ 835 string getModuleNameFromFile(string filePath) { 836 string fileContent = filePath.readText; 837 838 logDiagnostic("Get module name from path: " ~ filePath); 839 return getModuleNameFromContent(fileContent); 840 } 841 842 enum UpgradeOptions 843 { 844 none = 0, 845 upgrade = 1<<1, /// Upgrade existing packages 846 preRelease = 1<<2, /// inclde pre-release versions in upgrade 847 forceRemove = 1<<3, /// Force removing package folders, which contain unknown files 848 select = 1<<4, /// Update the dub.selections.json file with the upgraded versions 849 printUpgradesOnly = 1<<5, /// Instead of downloading new packages, just print a message to notify the user of their existence 850 useCachedResult = 1<<6, /// Use cached information stored with the package to determine upgrades 851 } 852 853 class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) { 854 protected { 855 Dub m_dub; 856 UpgradeOptions m_options; 857 Dependency[][string] m_packageVersions; 858 Package[string] m_remotePackages; 859 SelectedVersions m_selectedVersions; 860 Package m_rootPackage; 861 } 862 863 864 this(Dub dub, UpgradeOptions options) 865 { 866 m_dub = dub; 867 m_options = options; 868 } 869 870 Dependency[string] resolve(Package root, SelectedVersions selected_versions) 871 { 872 m_rootPackage = root; 873 m_selectedVersions = selected_versions; 874 return super.resolve(TreeNode(root.name, Dependency(root.ver)), (m_options & UpgradeOptions.printUpgradesOnly) == 0); 875 } 876 877 protected override Dependency[] getAllConfigs(string pack) 878 { 879 if (auto pvers = pack in m_packageVersions) 880 return *pvers; 881 882 if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(pack)) { 883 auto ret = [m_selectedVersions.getSelectedVersion(pack)]; 884 logDiagnostic("Using fixed selection %s %s", pack, ret[0]); 885 m_packageVersions[pack] = ret; 886 return ret; 887 } 888 889 logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length); 890 Version[] versions; 891 foreach (p; m_dub.packageManager.getPackageIterator(pack)) 892 versions ~= p.ver; 893 894 foreach (ps; m_dub.m_packageSuppliers) { 895 try { 896 auto vers = ps.getVersions(pack); 897 vers.reverse(); 898 if (!vers.length) { 899 logDiagnostic("No versions for %s for %s", pack, ps.description); 900 continue; 901 } 902 903 versions ~= vers; 904 break; 905 } catch (Exception e) { 906 logDebug("Package %s not found in %s: %s", pack, ps.description, e.msg); 907 logDebug("Full error: %s", e.toString().sanitize); 908 } 909 } 910 911 // sort by version, descending, and remove duplicates 912 versions = versions.sort!"a>b".uniq.array; 913 914 // move pre-release versions to the back of the list if no preRelease flag is given 915 if (!(m_options & UpgradeOptions.preRelease)) 916 versions = versions.filter!(v => !v.isPreRelease).array ~ versions.filter!(v => v.isPreRelease).array; 917 918 if (!versions.length) logDiagnostic("Nothing found for %s", pack); 919 920 auto ret = versions.map!(v => Dependency(v)).array; 921 m_packageVersions[pack] = ret; 922 return ret; 923 } 924 925 protected override Dependency[] getSpecificConfigs(TreeNodes nodes) 926 { 927 if (!nodes.configs.path.empty) return [nodes.configs]; 928 else return null; 929 } 930 931 932 protected override TreeNodes[] getChildren(TreeNode node) 933 { 934 auto ret = appender!(TreeNodes[]); 935 auto pack = getPackage(node.pack, node.config); 936 if (!pack) { 937 // this can hapen when the package description contains syntax errors 938 logDebug("Invalid package in dependency tree: %s %s", node.pack, node.config); 939 return null; 940 } 941 auto basepack = pack.basePackage; 942 943 foreach (dname, dspec; pack.dependencies) { 944 auto dbasename = getBasePackageName(dname); 945 946 // detect dependencies to the root package (or sub packages thereof) 947 if (dbasename == basepack.name) { 948 auto absdeppath = dspec.mapToPath(pack.path).path; 949 auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(dname), true); 950 if (subpack) { 951 auto desireddeppath = dname == dbasename ? basepack.path : subpack.path; 952 enforce(dspec.path.empty || absdeppath == desireddeppath, 953 format("Dependency from %s to root package references wrong path: %s vs. %s", 954 node.pack, absdeppath.toNativeString(), desireddeppath.toNativeString())); 955 } 956 ret ~= TreeNodes(dname, node.config); 957 continue; 958 } 959 960 if (dspec.optional && !m_dub.packageManager.getFirstPackage(dname)) 961 continue; 962 if (m_options & UpgradeOptions.upgrade || !m_selectedVersions || !m_selectedVersions.hasSelectedVersion(dbasename)) 963 ret ~= TreeNodes(dname, dspec.mapToPath(pack.path)); 964 else ret ~= TreeNodes(dname, m_selectedVersions.getSelectedVersion(dbasename)); 965 } 966 return ret.data; 967 } 968 969 protected override bool matches(Dependency configs, Dependency config) 970 { 971 if (!configs.path.empty) return configs.path == config.path; 972 return configs.merge(config).valid; 973 } 974 975 private Package getPackage(string name, Dependency dep) 976 { 977 auto basename = getBasePackageName(name); 978 979 // for sub packages, first try to get them from the base package 980 if (basename != name) { 981 auto subname = getSubPackageName(name); 982 auto basepack = getPackage(basename, dep); 983 if (!basepack) return null; 984 if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) { 985 return sp; 986 } else if (!basepack.subPackages.canFind!(p => p.path.length)) { 987 // note: external sub packages are handled further below 988 auto spr = basepack.getInternalSubPackage(subname); 989 if (!spr.isNull) { 990 auto sp = new Package(spr, basepack.path, basepack); 991 m_remotePackages[sp.name] = sp; 992 return sp; 993 } else { 994 logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep.version_); 995 return null; 996 } 997 } else if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) { 998 return ret; 999 } else { 1000 logDiagnostic("External sub package %s %s not found.", name, dep.version_); 1001 return null; 1002 } 1003 } 1004 1005 if (!dep.path.empty) { 1006 try { 1007 auto ret = m_dub.packageManager.getOrLoadPackage(dep.path); 1008 if (dep.matches(ret.ver)) return ret; 1009 } catch (Exception e) { 1010 logDiagnostic("Failed to load path based dependency %s: %s", name, e.msg); 1011 logDebug("Full error: %s", e.toString().sanitize); 1012 return null; 1013 } 1014 } 1015 1016 if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) 1017 return ret; 1018 1019 auto key = name ~ ":" ~ dep.version_.toString(); 1020 if (auto ret = key in m_remotePackages) 1021 return *ret; 1022 1023 auto prerelease = (m_options & UpgradeOptions.preRelease) != 0; 1024 1025 auto rootpack = name.split(":")[0]; 1026 1027 foreach (ps; m_dub.m_packageSuppliers) { 1028 if (rootpack == name) { 1029 try { 1030 auto desc = ps.getPackageDescription(name, dep, prerelease); 1031 auto ret = new Package(desc); 1032 m_remotePackages[key] = ret; 1033 return ret; 1034 } catch (Exception e) { 1035 logDiagnostic("Metadata for %s %s could not be downloaded from %s: %s", name, dep, ps.description, e.msg); 1036 logDebug("Full error: %s", e.toString().sanitize); 1037 } 1038 } else { 1039 logDiagnostic("Package %s not found in base package description (%s). Downloading whole package.", name, dep.version_.toString()); 1040 try { 1041 FetchOptions fetchOpts; 1042 fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none; 1043 fetchOpts |= (m_options & UpgradeOptions.forceRemove) != 0 ? FetchOptions.forceRemove : FetchOptions.none; 1044 m_dub.fetch(rootpack, dep, defaultPlacementLocation, fetchOpts, "need sub package description"); 1045 auto ret = m_dub.m_packageManager.getBestPackage(name, dep); 1046 if (!ret) { 1047 logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep.version_, name); 1048 return null; 1049 } 1050 m_remotePackages[key] = ret; 1051 return ret; 1052 } catch (Exception e) { 1053 logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg); 1054 logDebug("Full error: %s", e.toString().sanitize); 1055 } 1056 } 1057 } 1058 1059 m_remotePackages[key] = null; 1060 1061 logWarn("Package %s %s could not be loaded either locally, or from the configured package registries.", name, dep); 1062 return null; 1063 } 1064 } 1065