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