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.internal.utils; 13 import dub.internal.vibecompat.core.file; 14 import dub.internal.vibecompat.core.log; 15 import dub.internal.vibecompat.data.json; 16 import dub.internal.vibecompat.inet.url; 17 import dub.package_; 18 import dub.packagemanager; 19 import dub.packagesupplier; 20 import dub.project; 21 import dub.generators.generator; 22 import dub.init; 23 24 25 // todo: cleanup imports. 26 import std.algorithm; 27 import std.array; 28 import std.conv; 29 import std.datetime; 30 import std.exception; 31 import std.file; 32 import std.process; 33 import std.string; 34 import std.typecons; 35 import std.zip; 36 37 38 39 /// The default supplier for packages, which is the registry 40 /// hosted by code.dlang.org. 41 PackageSupplier[] defaultPackageSuppliers() 42 { 43 Url url = Url.parse("http://code.dlang.org/"); 44 logDiagnostic("Using dub registry url '%s'", url); 45 return [new RegistryPackageSupplier(url)]; 46 } 47 48 /// The Dub class helps in getting the applications 49 /// dependencies up and running. An instance manages one application. 50 class Dub { 51 private { 52 bool m_dryRun = false; 53 PackageManager m_packageManager; 54 PackageSupplier[] m_packageSuppliers; 55 Path m_rootPath, m_tempPath; 56 Path m_userDubPath, m_systemDubPath; 57 Json m_systemConfig, m_userConfig; 58 Path m_projectPath; 59 Project m_project; 60 } 61 62 /// Initiales the package manager for the vibe application 63 /// under root. 64 this(PackageSupplier[] additional_package_suppliers = null, string root_path = ".") 65 { 66 m_rootPath = Path(root_path); 67 if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath; 68 69 version(Windows){ 70 m_systemDubPath = Path(environment.get("ProgramData")) ~ "dub/"; 71 m_userDubPath = Path(environment.get("APPDATA")) ~ "dub/"; 72 m_tempPath = Path(environment.get("TEMP")); 73 } else version(Posix){ 74 m_systemDubPath = Path("/var/lib/dub/"); 75 m_userDubPath = Path(environment.get("HOME")) ~ ".dub/"; 76 if(!m_userDubPath.absolute) 77 m_userDubPath = Path(getcwd()) ~ m_userDubPath; 78 m_tempPath = Path("/tmp"); 79 } 80 81 m_userConfig = jsonFromFile(m_userDubPath ~ "settings.json", true); 82 m_systemConfig = jsonFromFile(m_systemDubPath ~ "settings.json", true); 83 84 PackageSupplier[] ps = additional_package_suppliers; 85 if (auto pp = "registryUrls" in m_userConfig) 86 ps ~= deserializeJson!(string[])(*pp) 87 .map!(url => cast(PackageSupplier)new RegistryPackageSupplier(Url(url))) 88 .array; 89 if (auto pp = "registryUrls" in m_systemConfig) 90 ps ~= deserializeJson!(string[])(*pp) 91 .map!(url => cast(PackageSupplier)new RegistryPackageSupplier(Url(url))) 92 .array; 93 ps ~= defaultPackageSuppliers(); 94 95 m_packageSuppliers = ps; 96 m_packageManager = new PackageManager(m_userDubPath, m_systemDubPath); 97 updatePackageSearchPath(); 98 } 99 100 @property void dryRun(bool v) { m_dryRun = v; } 101 102 /** Returns the root path (usually the current working directory). 103 */ 104 @property Path rootPath() const { return m_rootPath; } 105 /// ditto 106 @property void rootPath(Path root_path) 107 { 108 m_rootPath = root_path; 109 if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath; 110 } 111 112 /// Returns the name listed in the package.json of the current 113 /// application. 114 @property string projectName() const { return m_project.name; } 115 116 @property Path projectPath() const { return m_projectPath; } 117 118 @property string[] configurations() const { return m_project.configurations; } 119 120 @property inout(PackageManager) packageManager() inout { return m_packageManager; } 121 122 @property inout(Project) project() inout { return m_project; } 123 124 /// Loads the package from the current working directory as the main 125 /// project package. 126 void loadPackageFromCwd() 127 { 128 loadPackage(m_rootPath); 129 } 130 131 /// Loads the package from the specified path as the main project package. 132 void loadPackage(Path path) 133 { 134 m_projectPath = path; 135 updatePackageSearchPath(); 136 m_project = new Project(m_packageManager, m_projectPath); 137 } 138 139 /// Loads a specific package as the main project package (can be a sub package) 140 void loadPackage(Package pack) 141 { 142 m_projectPath = pack.path; 143 updatePackageSearchPath(); 144 m_project = new Project(m_packageManager, pack); 145 } 146 147 string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } 148 149 /// Performs retrieval and removal as necessary for 150 /// the application. 151 /// @param options bit combination of UpdateOptions 152 void update(UpdateOptions options) 153 { 154 bool[string] masterVersionUpgrades; 155 while (true) { 156 Action[] allActions = m_project.determineActions(m_packageSuppliers, options); 157 Action[] actions; 158 foreach(a; allActions) 159 if(a.packageId !in masterVersionUpgrades) 160 actions ~= a; 161 162 if (actions.length == 0) break; 163 164 logInfo("The following changes will be performed:"); 165 bool conflictedOrFailed = false; 166 foreach(Action a; actions) { 167 logInfo("%s %s %s, %s", capitalize(to!string(a.type)), a.packageId, a.vers, a.location); 168 if( a.type == Action.Type.conflict || a.type == Action.Type.failure ) { 169 logInfo("Issued by: "); 170 conflictedOrFailed = true; 171 foreach(string pkg, d; a.issuer) 172 logInfo(" "~pkg~": %s", d); 173 } 174 } 175 176 if (conflictedOrFailed || m_dryRun) return; 177 178 // Remove first 179 foreach(Action a; actions.filter!(a => a.type == Action.Type.remove)) { 180 assert(a.pack !is null, "No package specified for removal."); 181 remove(a.pack); 182 } 183 foreach(Action a; actions.filter!(a => a.type == Action.Type.fetch)) { 184 fetch(a.packageId, a.vers, a.location, (options & UpdateOptions.upgrade) != 0, (options & UpdateOptions.preRelease) != 0); 185 // never update the same package more than once 186 masterVersionUpgrades[a.packageId] = true; 187 } 188 189 m_project.reinit(); 190 } 191 } 192 193 /// Generate project files for a specified IDE. 194 /// Any existing project files will be overridden. 195 void generateProject(string ide, GeneratorSettings settings) { 196 auto generator = createProjectGenerator(ide, m_project, m_packageManager); 197 if (m_dryRun) return; // TODO: pass m_dryRun to the generator 198 generator.generate(settings); 199 } 200 201 void testProject(BuildSettings build_settings, BuildPlatform platform, string config, Path custom_main_file, string[] run_args) 202 { 203 if (custom_main_file.length && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; 204 205 if (config.length == 0) { 206 // if a custom main file was given, favor the first library configuration, so that it can be applied 207 if (custom_main_file.length) config = m_project.getDefaultConfiguration(platform, false); 208 // else look for a "unittest" configuration 209 if (!config.length && m_project.mainPackage.configurations.canFind("unittest")) config = "unittest"; 210 // if not found, fall back to the first "library" configuration 211 if (!config.length) config = m_project.getDefaultConfiguration(platform, false); 212 // if still nothing found, use the first executable configuration 213 if (!config.length) config = m_project.getDefaultConfiguration(platform, true); 214 } 215 216 auto generator = createProjectGenerator("build", m_project, m_packageManager); 217 GeneratorSettings settings; 218 settings.platform = platform; 219 settings.compiler = getCompiler(platform.compilerBinary); 220 settings.buildType = "unittest"; 221 settings.buildSettings = build_settings; 222 settings.run = true; 223 settings.runArgs = run_args; 224 225 auto test_config = format("__test__%s__", config); 226 227 BuildSettings lbuildsettings = build_settings; 228 m_project.addBuildSettings(lbuildsettings, platform, config, null, true); 229 if (lbuildsettings.targetType == TargetType.none) { 230 logInfo(`Configuration '%s' has target type "none". Skipping test.`, config); 231 return; 232 } 233 234 if (lbuildsettings.targetType == TargetType.executable) { 235 if (config == "unittest") logInfo("Running custom 'unittest' configuration.", config); 236 else logInfo(`Configuration '%s' does not output a library. Falling back to "dub -b unittest -c %s".`, config, config); 237 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 238 settings.config = config; 239 } else if (lbuildsettings.sourceFiles.empty) { 240 logInfo(`No source files found in configuration '%s'. Falling back to "dub -b unittest".`, config); 241 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 242 settings.config = m_project.getDefaultConfiguration(platform); 243 } else { 244 logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, test_config, config, lbuildsettings.targetType); 245 246 BuildSettingsTemplate tcinfo = m_project.mainPackage.info.getConfiguration(config).buildSettings; 247 tcinfo.targetType = TargetType.executable; 248 tcinfo.targetName = test_config; 249 tcinfo.versions[""] ~= "VibeCustomMain"; // HACK for vibe.d's legacy main() behavior 250 string custommodname; 251 if (custom_main_file.length) { 252 import std.path; 253 tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(m_project.mainPackage.path).toNativeString(); 254 tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString(); 255 custommodname = custom_main_file.head.toString().baseName(".d"); 256 } 257 258 string[] import_modules; 259 foreach (file; lbuildsettings.sourceFiles) { 260 if (file.endsWith(".d") && Path(file).head.toString() != "package.d") 261 import_modules ~= lbuildsettings.determineModuleName(Path(file), m_project.mainPackage.path); 262 } 263 264 // generate main file 265 Path mainfile = getTempDir() ~ "dub_test_root.d"; 266 tcinfo.sourceFiles[""] ~= mainfile.toNativeString(); 267 tcinfo.mainSourceFile = mainfile.toNativeString(); 268 if (!m_dryRun) { 269 auto fil = openFile(mainfile, FileMode.CreateTrunc); 270 scope(exit) fil.close(); 271 fil.write("module dub_test_root;\n"); 272 fil.write("import std.typetuple;\n"); 273 foreach (mod; import_modules) fil.write(format("static import %s;\n", mod)); 274 fil.write("alias allModules = TypeTuple!("); 275 foreach (i, mod; import_modules) { 276 if (i > 0) fil.write(", "); 277 fil.write(mod); 278 } 279 fil.write(");\n"); 280 if (custommodname.length) { 281 fil.write(format("import %s;\n", custommodname)); 282 } else { 283 fil.write(q{ 284 import std.stdio; 285 import core.runtime; 286 287 void main() { writeln("All unit tests were successful."); } 288 shared static this() { 289 version (Have_tested) { 290 import tested; 291 import core.runtime; 292 import std.exception; 293 Runtime.moduleUnitTester = () => true; 294 //runUnitTests!app(new JsonTestResultWriter("results.json")); 295 enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed."); 296 } 297 } 298 }); 299 } 300 } 301 m_project.mainPackage.info.configurations ~= ConfigurationInfo(test_config, tcinfo); 302 m_project = new Project(m_packageManager, m_project.mainPackage); 303 304 settings.config = test_config; 305 } 306 307 generator.generate(settings); 308 } 309 310 /// Outputs a JSON description of the project, including its dependencies. 311 void describeProject(BuildPlatform platform, string config) 312 { 313 auto dst = Json.emptyObject; 314 dst.configuration = config; 315 dst.compiler = platform.compiler; 316 dst.architecture = platform.architecture.serializeToJson(); 317 dst.platform = platform.platform.serializeToJson(); 318 319 m_project.describe(dst, platform, config); 320 logInfo("%s", dst.toPrettyString()); 321 } 322 323 324 /// Returns all cached packages as a "packageId" = "version" associative array 325 string[string] cachedPackages() const { return m_project.cachedPackagesIDs(); } 326 327 /// Fetches the package matching the dependency and places it in the specified location. 328 Package fetch(string packageId, const Dependency dep, PlacementLocation location, bool force_branch_upgrade, bool use_prerelease) 329 { 330 Json pinfo; 331 PackageSupplier supplier; 332 foreach(ps; m_packageSuppliers){ 333 try { 334 pinfo = ps.getPackageDescription(packageId, dep, use_prerelease); 335 supplier = ps; 336 break; 337 } catch(Exception e) { 338 logDiagnostic("Package %s not found at for %s: %s", packageId, ps.description(), e.msg); 339 logDebug("Full error: %s", e.toString().sanitize()); 340 } 341 } 342 enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency "~dep.toString()); 343 string ver = pinfo["version"].get!string; 344 345 Path placement; 346 final switch (location) { 347 case PlacementLocation.local: placement = m_rootPath; break; 348 case PlacementLocation.userWide: placement = m_userDubPath ~ "packages/"; break; 349 case PlacementLocation.systemWide: placement = m_systemDubPath ~ "packages/"; break; 350 } 351 352 // always upgrade branch based versions - TODO: actually check if there is a new commit available 353 if (auto pack = m_packageManager.getPackage(packageId, ver, placement)) { 354 if (!ver.startsWith("~") || !force_branch_upgrade || location == PlacementLocation.local) { 355 // TODO: support git working trees by performing a "git pull" instead of this 356 logInfo("Package %s %s (%s) is already present with the latest version, skipping upgrade.", 357 packageId, ver, placement); 358 return pack; 359 } else { 360 logInfo("Removing present package of %s %s", packageId, ver); 361 if (!m_dryRun) m_packageManager.remove(pack); 362 } 363 } 364 365 logInfo("Fetching %s %s...", packageId, ver); 366 if (m_dryRun) return null; 367 368 logDiagnostic("Acquiring package zip file"); 369 auto dload = m_projectPath ~ ".dub/temp/downloads"; 370 auto tempfname = packageId ~ "-" ~ (ver.startsWith('~') ? ver[1 .. $] : ver) ~ ".zip"; 371 auto tempFile = m_tempPath ~ tempfname; 372 string sTempFile = tempFile.toNativeString(); 373 if (exists(sTempFile)) std.file.remove(sTempFile); 374 supplier.retrievePackage(tempFile, packageId, dep, use_prerelease); // Q: continue on fail? 375 scope(exit) std.file.remove(sTempFile); 376 377 logInfo("Placing %s %s to %s...", packageId, ver, placement.toNativeString()); 378 auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $]; 379 Path dstpath = placement ~ (packageId ~ "-" ~ clean_package_version); 380 381 return m_packageManager.storeFetchedPackage(tempFile, pinfo, dstpath); 382 } 383 384 /// Removes a given package from the list of present/cached modules. 385 /// @removeFromApplication: if true, this will also remove an entry in the 386 /// list of dependencies in the application's package.json 387 void remove(in Package pack) 388 { 389 logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); 390 if (!m_dryRun) m_packageManager.remove(pack); 391 } 392 393 /// @see remove(string, string, RemoveLocation) 394 enum RemoveVersionWildcard = "*"; 395 396 /// This will remove a given package with a specified version from the 397 /// location. 398 /// It will remove at most one package, unless @param version_ is 399 /// specified as wildcard "*". 400 /// @param package_id Package to be removed 401 /// @param version_ Identifying a version or a wild card. An empty string 402 /// may be passed into. In this case the package will be removed from the 403 /// location, if there is only one version retrieved. This will throw an 404 /// exception, if there are multiple versions retrieved. 405 /// Note: as wildcard string only "*" is supported. 406 /// @param location_ 407 void remove(string package_id, string version_, PlacementLocation location_) 408 { 409 enforce(!package_id.empty); 410 if (location_ == PlacementLocation.local) { 411 logInfo("To remove a locally placed package, make sure you don't have any data" 412 ~ "\nleft in it's directory and then simply remove the whole directory."); 413 return; 414 } 415 416 Package[] packages; 417 const bool wildcardOrEmpty = version_ == RemoveVersionWildcard || version_.empty; 418 419 // Use package manager 420 foreach(pack; m_packageManager.getPackageIterator(package_id)) { 421 if( wildcardOrEmpty || pack.vers == version_ ) { 422 packages ~= pack; 423 } 424 } 425 426 if(packages.empty) { 427 logError("Cannot find package to remove. (id:%s, version:%s, location:%s)", package_id, version_, location_); 428 return; 429 } 430 431 if(version_.empty && packages.length > 1) { 432 logError("Cannot remove package '%s', there multiple possibilities at location '%s'.", package_id, location_); 433 logError("Retrieved versions:"); 434 foreach(pack; packages) 435 logError(to!string(pack.vers())); 436 throw new Exception("Failed to remove package."); 437 } 438 439 logDebug("Removing %s packages.", packages.length); 440 foreach(pack; packages) { 441 try { 442 remove(pack); 443 logInfo("Removing %s, version %s.", package_id, pack.vers); 444 } 445 catch logError("Failed to remove %s, version %s. Continuing with other packages (if any).", package_id, pack.vers); 446 } 447 } 448 449 void addLocalPackage(string path, string ver, bool system) 450 { 451 if (m_dryRun) return; 452 m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user); 453 } 454 455 void removeLocalPackage(string path, bool system) 456 { 457 if (m_dryRun) return; 458 m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 459 } 460 461 void addSearchPath(string path, bool system) 462 { 463 if (m_dryRun) return; 464 m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 465 } 466 467 void removeSearchPath(string path, bool system) 468 { 469 if (m_dryRun) return; 470 m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 471 } 472 473 void createEmptyPackage(Path path, string type) 474 { 475 if( !path.absolute() ) path = m_rootPath ~ path; 476 path.normalize(); 477 478 if (m_dryRun) return; 479 480 initPackage(path, type); 481 482 //Act smug to the user. 483 logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); 484 } 485 486 void runDdox(bool run) 487 { 488 if (m_dryRun) return; 489 490 auto ddox_pack = m_packageManager.getBestPackage("ddox", ">=0.0.0"); 491 if (!ddox_pack) ddox_pack = m_packageManager.getBestPackage("ddox", "~master"); 492 if (!ddox_pack) { 493 logInfo("DDOX is not present, getting it and storing user wide"); 494 ddox_pack = fetch("ddox", Dependency(">=0.0.0"), PlacementLocation.userWide, false, false); 495 } 496 497 version(Windows) auto ddox_exe = "ddox.exe"; 498 else auto ddox_exe = "ddox"; 499 500 if( !existsFile(ddox_pack.path~ddox_exe) ){ 501 logInfo("DDOX in %s is not built, performing build now.", ddox_pack.path.toNativeString()); 502 503 auto ddox_dub = new Dub(m_packageSuppliers); 504 ddox_dub.loadPackage(ddox_pack.path); 505 506 auto compiler_binary = "dmd"; 507 508 GeneratorSettings settings; 509 settings.config = "application"; 510 settings.compiler = getCompiler(compiler_binary); 511 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary); 512 settings.buildType = "debug"; 513 ddox_dub.generateProject("build", settings); 514 515 //runCommands(["cd "~ddox_pack.path.toNativeString()~" && dub build -v"]); 516 } 517 518 auto p = ddox_pack.path; 519 p.endsWithSlash = true; 520 auto dub_path = p.toNativeString(); 521 522 string[] commands; 523 string[] filterargs = m_project.mainPackage.info.ddoxFilterArgs.dup; 524 if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; 525 commands ~= dub_path~"ddox filter "~filterargs.join(" ")~" docs.json"; 526 if (!run) { 527 commands ~= dub_path~"ddox generate-html --navigation-type=ModuleTree docs.json docs"; 528 version(Windows) commands ~= "xcopy /S /D "~dub_path~"public\\* docs\\"; 529 else commands ~= "cp -ru \""~dub_path~"public\"/* docs/"; 530 } 531 runCommands(commands); 532 533 if (run) { 534 spawnProcess([dub_path~"ddox", "serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~dub_path~"public"]); 535 browse("http://127.0.0.1:8080/"); 536 } 537 } 538 539 private void updatePackageSearchPath() 540 { 541 auto p = environment.get("DUBPATH"); 542 Path[] paths; 543 544 version(Windows) enum pathsep = ";"; 545 else enum pathsep = ":"; 546 if (p.length) paths ~= p.split(pathsep).map!(p => Path(p))().array(); 547 m_packageManager.searchPath = paths; 548 } 549 550 private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; } 551 private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); } 552 } 553 554 string determineModuleName(BuildSettings settings, Path file, Path base_path) 555 { 556 assert(base_path.absolute); 557 if (!file.absolute) file = base_path ~ file; 558 559 size_t path_skip = 0; 560 foreach (ipath; settings.importPaths.map!(p => Path(p))) { 561 if (!ipath.absolute) ipath = base_path ~ ipath; 562 assert(!ipath.empty); 563 if (file.startsWith(ipath) && ipath.length > path_skip) 564 path_skip = ipath.length; 565 } 566 567 enforce(path_skip > 0, 568 format("Source file '%s' not found in any import path.", file.toNativeString())); 569 570 auto mpath = file[path_skip .. file.length]; 571 auto ret = appender!string; 572 foreach (i; 0 .. mpath.length) { 573 import std.path; 574 auto p = mpath[i].toString(); 575 if (p == "package.d") break; 576 if (i > 0) ret ~= "."; 577 if (i+1 < mpath.length) ret ~= p; 578 else ret ~= p.baseName(".d"); 579 } 580 return ret.data; 581 }