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) {} 338 } 339 enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency "~dep.toString()); 340 string ver = pinfo["version"].get!string; 341 342 Path placement; 343 final switch (location) { 344 case PlacementLocation.local: placement = m_rootPath; break; 345 case PlacementLocation.userWide: placement = m_userDubPath ~ "packages/"; break; 346 case PlacementLocation.systemWide: placement = m_systemDubPath ~ "packages/"; break; 347 } 348 349 // always upgrade branch based versions - TODO: actually check if there is a new commit available 350 if (auto pack = m_packageManager.getPackage(packageId, ver, placement)) { 351 if (!ver.startsWith("~") || !force_branch_upgrade || location == PlacementLocation.local) { 352 // TODO: support git working trees by performing a "git pull" instead of this 353 logInfo("Package %s %s (%s) is already present with the latest version, skipping upgrade.", 354 packageId, ver, placement); 355 return pack; 356 } else { 357 logInfo("Removing present package of %s %s", packageId, ver); 358 if (!m_dryRun) m_packageManager.remove(pack); 359 } 360 } 361 362 logInfo("Fetching %s %s...", packageId, ver); 363 if (m_dryRun) return null; 364 365 logDiagnostic("Acquiring package zip file"); 366 auto dload = m_projectPath ~ ".dub/temp/downloads"; 367 auto tempfname = packageId ~ "-" ~ (ver.startsWith('~') ? ver[1 .. $] : ver) ~ ".zip"; 368 auto tempFile = m_tempPath ~ tempfname; 369 string sTempFile = tempFile.toNativeString(); 370 if (exists(sTempFile)) std.file.remove(sTempFile); 371 supplier.retrievePackage(tempFile, packageId, dep, use_prerelease); // Q: continue on fail? 372 scope(exit) std.file.remove(sTempFile); 373 374 logInfo("Placing %s %s to %s...", packageId, ver, placement.toNativeString()); 375 auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $]; 376 Path dstpath = placement ~ (packageId ~ "-" ~ clean_package_version); 377 378 return m_packageManager.storeFetchedPackage(tempFile, pinfo, dstpath); 379 } 380 381 /// Removes a given package from the list of present/cached modules. 382 /// @removeFromApplication: if true, this will also remove an entry in the 383 /// list of dependencies in the application's package.json 384 void remove(in Package pack) 385 { 386 logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); 387 if (!m_dryRun) m_packageManager.remove(pack); 388 } 389 390 /// @see remove(string, string, RemoveLocation) 391 enum RemoveVersionWildcard = "*"; 392 393 /// This will remove a given package with a specified version from the 394 /// location. 395 /// It will remove at most one package, unless @param version_ is 396 /// specified as wildcard "*". 397 /// @param package_id Package to be removed 398 /// @param version_ Identifying a version or a wild card. An empty string 399 /// may be passed into. In this case the package will be removed from the 400 /// location, if there is only one version retrieved. This will throw an 401 /// exception, if there are multiple versions retrieved. 402 /// Note: as wildcard string only "*" is supported. 403 /// @param location_ 404 void remove(string package_id, string version_, PlacementLocation location_) 405 { 406 enforce(!package_id.empty); 407 if (location_ == PlacementLocation.local) { 408 logInfo("To remove a locally placed package, make sure you don't have any data" 409 ~ "\nleft in it's directory and then simply remove the whole directory."); 410 return; 411 } 412 413 Package[] packages; 414 const bool wildcardOrEmpty = version_ == RemoveVersionWildcard || version_.empty; 415 416 // Use package manager 417 foreach(pack; m_packageManager.getPackageIterator(package_id)) { 418 if( wildcardOrEmpty || pack.vers == version_ ) { 419 packages ~= pack; 420 } 421 } 422 423 if(packages.empty) { 424 logError("Cannot find package to remove. (id:%s, version:%s, location:%s)", package_id, version_, location_); 425 return; 426 } 427 428 if(version_.empty && packages.length > 1) { 429 logError("Cannot remove package '%s', there multiple possibilities at location '%s'.", package_id, location_); 430 logError("Retrieved versions:"); 431 foreach(pack; packages) 432 logError(to!string(pack.vers())); 433 throw new Exception("Failed to remove package."); 434 } 435 436 logDebug("Removing %s packages.", packages.length); 437 foreach(pack; packages) { 438 try { 439 remove(pack); 440 logInfo("Removing %s, version %s.", package_id, pack.vers); 441 } 442 catch logError("Failed to remove %s, version %s. Continuing with other packages (if any).", package_id, pack.vers); 443 } 444 } 445 446 void addLocalPackage(string path, string ver, bool system) 447 { 448 if (m_dryRun) return; 449 m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user); 450 } 451 452 void removeLocalPackage(string path, bool system) 453 { 454 if (m_dryRun) return; 455 m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 456 } 457 458 void addSearchPath(string path, bool system) 459 { 460 if (m_dryRun) return; 461 m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 462 } 463 464 void removeSearchPath(string path, bool system) 465 { 466 if (m_dryRun) return; 467 m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 468 } 469 470 void createEmptyPackage(Path path, string type) 471 { 472 if( !path.absolute() ) path = m_rootPath ~ path; 473 path.normalize(); 474 475 if (m_dryRun) return; 476 477 initPackage(path, type); 478 479 //Act smug to the user. 480 logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); 481 } 482 483 void runDdox(bool run) 484 { 485 if (m_dryRun) return; 486 487 auto ddox_pack = m_packageManager.getBestPackage("ddox", ">=0.0.0"); 488 if (!ddox_pack) ddox_pack = m_packageManager.getBestPackage("ddox", "~master"); 489 if (!ddox_pack) { 490 logInfo("DDOX is not present, getting it and storing user wide"); 491 ddox_pack = fetch("ddox", Dependency(">=0.0.0"), PlacementLocation.userWide, false, false); 492 } 493 494 version(Windows) auto ddox_exe = "ddox.exe"; 495 else auto ddox_exe = "ddox"; 496 497 if( !existsFile(ddox_pack.path~ddox_exe) ){ 498 logInfo("DDOX in %s is not built, performing build now.", ddox_pack.path.toNativeString()); 499 500 auto ddox_dub = new Dub(m_packageSuppliers); 501 ddox_dub.loadPackage(ddox_pack.path); 502 503 auto compiler_binary = "dmd"; 504 505 GeneratorSettings settings; 506 settings.config = "application"; 507 settings.compiler = getCompiler(compiler_binary); 508 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary); 509 settings.buildType = "debug"; 510 ddox_dub.generateProject("build", settings); 511 512 //runCommands(["cd "~ddox_pack.path.toNativeString()~" && dub build -v"]); 513 } 514 515 auto p = ddox_pack.path; 516 p.endsWithSlash = true; 517 auto dub_path = p.toNativeString(); 518 519 string[] commands; 520 string[] filterargs = m_project.mainPackage.info.ddoxFilterArgs.dup; 521 if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; 522 commands ~= dub_path~"ddox filter "~filterargs.join(" ")~" docs.json"; 523 if (!run) { 524 commands ~= dub_path~"ddox generate-html --navigation-type=ModuleTree docs.json docs"; 525 version(Windows) commands ~= "xcopy /S /D "~dub_path~"public\\* docs\\"; 526 else commands ~= "cp -ru \""~dub_path~"public\"/* docs/"; 527 } 528 runCommands(commands); 529 530 if (run) { 531 spawnProcess([dub_path~"ddox", "serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~dub_path~"public"]); 532 browse("http://127.0.0.1:8080/"); 533 } 534 } 535 536 private void updatePackageSearchPath() 537 { 538 auto p = environment.get("DUBPATH"); 539 Path[] paths; 540 541 version(Windows) enum pathsep = ";"; 542 else enum pathsep = ":"; 543 if (p.length) paths ~= p.split(pathsep).map!(p => Path(p))().array(); 544 m_packageManager.searchPath = paths; 545 } 546 547 private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; } 548 private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); } 549 } 550 551 string determineModuleName(BuildSettings settings, Path file, Path base_path) 552 { 553 assert(base_path.absolute); 554 if (!file.absolute) file = base_path ~ file; 555 foreach (ipath; settings.importPaths.map!(p => Path(p))) { 556 if (!ipath.absolute) ipath = base_path ~ ipath; 557 assert(!ipath.empty); 558 if (file.startsWith(ipath)) { 559 auto mpath = file[ipath.length .. file.length]; 560 auto ret = appender!string; 561 foreach (i; 0 .. mpath.length) { 562 import std.path; 563 auto p = mpath[i].toString(); 564 if (p == "package.d") break; 565 if (i > 0) ret ~= "."; 566 if (i+1 < mpath.length) ret ~= p; 567 else ret ~= p.baseName(".d"); 568 } 569 return ret.data; 570 } 571 } 572 throw new Exception(format("Source file '%s' not found in any import path.", file.toNativeString())); 573 }