1 /** 2 Management of packages on the local computer. 3 4 Copyright: © 2012-2013 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Matthias Dondorff 7 */ 8 module dub.packagemanager; 9 10 import dub.dependency; 11 import dub.internal.utils; 12 import dub.internal.vibecompat.core.file; 13 import dub.internal.vibecompat.core.log; 14 import dub.internal.vibecompat.data.json; 15 import dub.internal.vibecompat.inet.path; 16 import dub.package_; 17 18 import std.algorithm : countUntil, filter, sort, canFind, remove; 19 import std.array; 20 import std.conv; 21 import std.digest.sha; 22 import std.encoding : sanitize; 23 import std.exception; 24 import std.file; 25 import std..string; 26 import std.zip; 27 28 29 /// The PackageManager can retrieve present packages and get / remove 30 /// packages. 31 class PackageManager { 32 private { 33 Repository[LocalPackageType] m_repositories; 34 Path[] m_searchPath; 35 Package[] m_packages; 36 Package[] m_temporaryPackages; 37 bool m_disableDefaultSearchPaths = false; 38 } 39 40 this(Path user_path, Path system_path, bool refresh_packages = true) 41 { 42 m_repositories[LocalPackageType.user] = Repository(user_path); 43 m_repositories[LocalPackageType.system] = Repository(system_path); 44 if (refresh_packages) refresh(true); 45 } 46 47 @property void searchPath(Path[] paths) 48 { 49 if (paths == m_searchPath) return; 50 m_searchPath = paths.dup; 51 refresh(false); 52 } 53 @property const(Path)[] searchPath() const { return m_searchPath; } 54 55 @property void disableDefaultSearchPaths(bool val) 56 { 57 if (val == m_disableDefaultSearchPaths) return; 58 m_disableDefaultSearchPaths = val; 59 refresh(true); 60 } 61 62 @property const(Path)[] completeSearchPath() 63 const { 64 auto ret = appender!(Path[])(); 65 ret.put(m_searchPath); 66 if (!m_disableDefaultSearchPaths) { 67 ret.put(m_repositories[LocalPackageType.user].searchPath); 68 ret.put(m_repositories[LocalPackageType.user].packagePath); 69 ret.put(m_repositories[LocalPackageType.system].searchPath); 70 ret.put(m_repositories[LocalPackageType.system].packagePath); 71 } 72 return ret.data; 73 } 74 75 76 /** Looks up a specific package. 77 78 Looks up a package matching the given version/path in the set of 79 registered packages. The lookup order is done according the the 80 usual rules (see getPackageIterator). 81 82 Params: 83 name = The name of the package 84 ver = The exact version of the package to query 85 path = An exact path that the package must reside in. Note that 86 the package must still be registered in the package manager. 87 enable_overrides = Apply the local package override list before 88 returning a package (enabled by default) 89 90 Returns: 91 The matching package or null if no match was found. 92 */ 93 Package getPackage(string name, Version ver, bool enable_overrides = true) 94 { 95 if (enable_overrides) { 96 foreach (tp; [LocalPackageType.user, LocalPackageType.system]) 97 foreach (ovr; m_repositories[tp].overrides) 98 if (ovr.package_ == name && ovr.version_.matches(ver)) { 99 Package pack; 100 if (!ovr.targetPath.empty) pack = getPackage(name, ovr.targetPath); 101 else pack = getPackage(name, ovr.targetVersion, false); 102 if (pack) return pack; 103 104 logWarn("Package override %s %s -> %s %s doesn't reference an existing package.", 105 ovr.package_, ovr.version_, ovr.targetVersion, ovr.targetPath); 106 } 107 } 108 109 foreach (p; getPackageIterator(name)) 110 if (p.ver == ver) 111 return p; 112 113 return null; 114 } 115 116 /// ditto 117 Package getPackage(string name, string ver, bool enable_overrides = true) 118 { 119 return getPackage(name, Version(ver), enable_overrides); 120 } 121 122 /// ditto 123 Package getPackage(string name, Version ver, Path path) 124 { 125 auto ret = getPackage(name, path); 126 if (!ret || ret.ver != ver) return null; 127 return ret; 128 } 129 130 /// ditto 131 Package getPackage(string name, string ver, Path path) 132 { 133 return getPackage(name, Version(ver), path); 134 } 135 136 /// ditto 137 Package getPackage(string name, Path path) 138 { 139 foreach( p; getPackageIterator(name) ) 140 if (p.path.startsWith(path)) 141 return p; 142 return null; 143 } 144 145 146 /** Looks up the first package matching the given name. 147 */ 148 Package getFirstPackage(string name) 149 { 150 foreach (ep; getPackageIterator(name)) 151 return ep; 152 return null; 153 } 154 155 Package getOrLoadPackage(Path path, PathAndFormat infoFile = PathAndFormat()) 156 { 157 path.endsWithSlash = true; 158 foreach (p; getPackageIterator()) 159 if (!p.parentPackage && p.path == path) 160 return p; 161 auto pack = new Package(path, infoFile); 162 addPackages(m_temporaryPackages, pack); 163 return pack; 164 } 165 166 167 /** Searches for the latest version of a package matching the given dependency. 168 */ 169 Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true) 170 { 171 Package ret; 172 foreach (p; getPackageIterator(name)) 173 if (version_spec.matches(p.ver) && (!ret || p.ver > ret.ver)) 174 ret = p; 175 176 if (enable_overrides && ret) { 177 if (auto ovr = getPackage(name, ret.ver)) 178 return ovr; 179 } 180 return ret; 181 } 182 183 /// ditto 184 Package getBestPackage(string name, string version_spec) 185 { 186 return getBestPackage(name, Dependency(version_spec)); 187 } 188 189 Package getSubPackage(Package base_package, string sub_name, bool silent_fail) 190 { 191 foreach (p; getPackageIterator(base_package.name~":"~sub_name)) 192 if (p.parentPackage is base_package) 193 return p; 194 enforce(silent_fail, "Sub package "~base_package.name~":"~sub_name~" doesn't exist."); 195 return null; 196 } 197 198 199 /** Determines if a package is managed by DUB. 200 201 Managed packages can be upgraded and removed. 202 */ 203 bool isManagedPackage(Package pack) 204 const { 205 auto ppath = pack.basePackage.path; 206 foreach (rep; m_repositories) { 207 auto rpath = rep.packagePath; 208 if (ppath.startsWith(rpath)) 209 return true; 210 } 211 return false; 212 } 213 214 int delegate(int delegate(ref Package)) getPackageIterator() 215 { 216 int iterator(int delegate(ref Package) del) 217 { 218 foreach (tp; m_temporaryPackages) 219 if (auto ret = del(tp)) return ret; 220 221 // first search local packages 222 foreach (tp; LocalPackageType.min .. LocalPackageType.max+1) 223 foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages) 224 if (auto ret = del(p)) return ret; 225 226 // and then all packages gathered from the search path 227 foreach( p; m_packages ) 228 if( auto ret = del(p) ) 229 return ret; 230 return 0; 231 } 232 233 return &iterator; 234 } 235 236 int delegate(int delegate(ref Package)) getPackageIterator(string name) 237 { 238 int iterator(int delegate(ref Package) del) 239 { 240 foreach (p; getPackageIterator()) 241 if (p.name == name) 242 if (auto ret = del(p)) return ret; 243 return 0; 244 } 245 246 return &iterator; 247 } 248 249 250 /** Returns a list of all package overrides for the given scope. 251 */ 252 const(PackageOverride)[] getOverrides(LocalPackageType scope_) 253 const { 254 return m_repositories[scope_].overrides; 255 } 256 257 /** Adds a new override for the given package. 258 */ 259 void addOverride(LocalPackageType scope_, string package_, Dependency version_spec, Version target) 260 { 261 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 262 writeLocalPackageOverridesFile(scope_); 263 } 264 /// ditto 265 void addOverride(LocalPackageType scope_, string package_, Dependency version_spec, Path target) 266 { 267 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 268 writeLocalPackageOverridesFile(scope_); 269 } 270 271 /** Removes an existing package override. 272 */ 273 void removeOverride(LocalPackageType scope_, string package_, Dependency version_spec) 274 { 275 Repository* rep = &m_repositories[scope_]; 276 foreach (i, ovr; rep.overrides) { 277 if (ovr.package_ != package_ || ovr.version_ != version_spec) 278 continue; 279 rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $]; 280 writeLocalPackageOverridesFile(scope_); 281 return; 282 } 283 throw new Exception(format("No override exists for %s %s", package_, version_spec)); 284 } 285 286 /// Extracts the package supplied as a path to it's zip file to the 287 /// destination and sets a version field in the package description. 288 Package storeFetchedPackage(Path zip_file_path, Json package_info, Path destination) 289 { 290 auto package_name = package_info.name.get!string; 291 auto package_version = package_info["version"].get!string; 292 auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $]; 293 294 logDiagnostic("Placing package '%s' version '%s' to location '%s' from file '%s'", 295 package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString()); 296 297 if( existsFile(destination) ){ 298 throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination)); 299 } 300 301 // open zip file 302 ZipArchive archive; 303 { 304 logDebug("Opening file %s", zip_file_path); 305 auto f = openFile(zip_file_path, FileMode.Read); 306 scope(exit) f.close(); 307 archive = new ZipArchive(f.readAll()); 308 } 309 310 logDebug("Extracting from zip."); 311 312 // In a github zip, the actual contents are in a subfolder 313 Path zip_prefix; 314 outer: foreach(ArchiveMember am; archive.directory) { 315 auto path = Path(am.name); 316 foreach (fil; packageInfoFiles) 317 if (path.length == 2 && path.head.toString == fil.filename) { 318 zip_prefix = path[0 .. $-1]; 319 break outer; 320 } 321 } 322 323 logDebug("zip root folder: %s", zip_prefix); 324 325 Path getCleanedPath(string fileName) { 326 auto path = Path(fileName); 327 if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path(); 328 return path[zip_prefix.length..path.length]; 329 } 330 331 // extract & place 332 mkdirRecurse(destination.toNativeString()); 333 logDiagnostic("Copying all files..."); 334 int countFiles = 0; 335 foreach(ArchiveMember a; archive.directory) { 336 auto cleanedPath = getCleanedPath(a.name); 337 if(cleanedPath.empty) continue; 338 auto dst_path = destination~cleanedPath; 339 340 logDebug("Creating %s", cleanedPath); 341 if( dst_path.endsWithSlash ){ 342 if( !existsDirectory(dst_path) ) 343 mkdirRecurse(dst_path.toNativeString()); 344 } else { 345 if( !existsDirectory(dst_path.parentPath) ) 346 mkdirRecurse(dst_path.parentPath.toNativeString()); 347 auto dstFile = openFile(dst_path, FileMode.CreateTrunc); 348 scope(exit) dstFile.close(); 349 dstFile.put(archive.expand(a)); 350 ++countFiles; 351 } 352 } 353 logDiagnostic("%s file(s) copied.", to!string(countFiles)); 354 355 // overwrite dub.json (this one includes a version field) 356 auto pack = new Package(destination, PathAndFormat(), null, package_info["version"].get!string); 357 358 if (pack.packageInfoFilename.head != defaultPackageFilename) 359 // Storeinfo saved a default file, this could be different to the file from the zip. 360 removeFile(pack.packageInfoFilename); 361 pack.storeInfo(); 362 addPackages(m_packages, pack); 363 return pack; 364 } 365 366 /// Removes the given the package. 367 void remove(in Package pack, bool force_remove) 368 { 369 logDebug("Remove %s, version %s, path '%s'", pack.name, pack.vers, pack.path); 370 enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); 371 372 // remove package from repositories' list 373 bool found = false; 374 bool removeFrom(Package[] packs, in Package pack) { 375 auto packPos = countUntil!("a.path == b.path")(packs, pack); 376 if(packPos != -1) { 377 packs = .remove(packs, packPos); 378 return true; 379 } 380 return false; 381 } 382 foreach(repo; m_repositories) { 383 if(removeFrom(repo.localPackages, pack)) { 384 found = true; 385 break; 386 } 387 } 388 if(!found) 389 found = removeFrom(m_packages, pack); 390 enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); 391 392 logDebug("About to delete root folder for package '%s'.", pack.path); 393 rmdirRecurse(pack.path.toNativeString()); 394 logInfo("Removed package: '"~pack.name~"'"); 395 } 396 397 Package addLocalPackage(Path path, string verName, LocalPackageType type) 398 { 399 path.endsWithSlash = true; 400 auto pack = new Package(path); 401 enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString()); 402 if (verName.length) 403 pack.ver = Version(verName); 404 405 // don't double-add packages 406 Package[]* packs = &m_repositories[type].localPackages; 407 foreach (p; *packs) { 408 if (p.path == path) { 409 enforce(p.ver == pack.ver, "Adding the same local package twice with differing versions is not allowed."); 410 logInfo("Package is already registered: %s (version: %s)", p.name, p.ver); 411 return p; 412 } 413 } 414 415 addPackages(*packs, pack); 416 417 writeLocalPackageList(type); 418 419 logInfo("Registered package: %s (version: %s)", pack.name, pack.ver); 420 return pack; 421 } 422 423 void removeLocalPackage(Path path, LocalPackageType type) 424 { 425 path.endsWithSlash = true; 426 427 Package[]* packs = &m_repositories[type].localPackages; 428 size_t[] to_remove; 429 foreach( i, entry; *packs ) 430 if( entry.path == path ) 431 to_remove ~= i; 432 enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); 433 434 string[Version] removed; 435 foreach_reverse( i; to_remove ) { 436 removed[(*packs)[i].ver] = (*packs)[i].name; 437 *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $]; 438 } 439 440 writeLocalPackageList(type); 441 442 foreach(ver, name; removed) 443 logInfo("Unregistered package: %s (version: %s)", name, ver); 444 } 445 446 /// For the given type add another path where packages will be looked up. 447 void addSearchPath(Path path, LocalPackageType type) 448 { 449 m_repositories[type].searchPath ~= path; 450 writeLocalPackageList(type); 451 } 452 453 /// Removes a search path from the given type. 454 void removeSearchPath(Path path, LocalPackageType type) 455 { 456 m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array(); 457 writeLocalPackageList(type); 458 } 459 460 void refresh(bool refresh_existing_packages) 461 { 462 logDiagnostic("Refreshing local packages (refresh existing: %s)...", refresh_existing_packages); 463 464 // load locally defined packages 465 void scanLocalPackages(LocalPackageType type) 466 { 467 Path list_path = m_repositories[type].packagePath; 468 Package[] packs; 469 Path[] paths; 470 if (!m_disableDefaultSearchPaths) try { 471 auto local_package_file = list_path ~ LocalPackagesFilename; 472 logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString()); 473 if( !existsFile(local_package_file) ) return; 474 logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString()); 475 auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename); 476 enforce(packlist.type == Json.Type.array, LocalPackagesFilename~" must contain an array."); 477 foreach( pentry; packlist ){ 478 try { 479 auto name = pentry.name.get!string; 480 auto path = Path(pentry.path.get!string); 481 if (name == "*") { 482 paths ~= path; 483 } else { 484 auto ver = Version(pentry["version"].get!string); 485 486 Package pp; 487 if (!refresh_existing_packages) { 488 foreach (p; m_repositories[type].localPackages) 489 if (p.path == path) { 490 pp = p; 491 break; 492 } 493 } 494 495 if (!pp) { 496 auto infoFile = Package.findPackageFile(path); 497 if (!infoFile.empty) pp = new Package(path, infoFile); 498 else { 499 logWarn("Locally registered package %s %s was not found. Please run \"dub remove-local %s\".", 500 name, ver, path.toNativeString()); 501 auto info = Json.emptyObject; 502 info.name = name; 503 pp = new Package(info, path); 504 } 505 } 506 507 if (pp.name != name) 508 logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name); 509 pp.ver = ver; 510 511 addPackages(packs, pp); 512 } 513 } catch( Exception e ){ 514 logWarn("Error adding local package: %s", e.msg); 515 } 516 } 517 } catch( Exception e ){ 518 logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); 519 } 520 m_repositories[type].localPackages = packs; 521 m_repositories[type].searchPath = paths; 522 } 523 scanLocalPackages(LocalPackageType.system); 524 scanLocalPackages(LocalPackageType.user); 525 526 auto old_packages = m_packages; 527 528 // rescan the system and user package folder 529 void scanPackageFolder(Path path) 530 { 531 if( path.existsDirectory() ){ 532 logDebug("iterating dir %s", path.toNativeString()); 533 try foreach( pdir; iterateDirectory(path) ){ 534 logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); 535 if( !pdir.isDirectory ) continue; 536 auto pack_path = path ~ (pdir.name ~ "/"); 537 auto packageFile = Package.findPackageFile(pack_path); 538 if (packageFile.empty) continue; 539 Package p; 540 try { 541 if (!refresh_existing_packages) 542 foreach (pp; old_packages) 543 if (pp.path == pack_path) { 544 p = pp; 545 break; 546 } 547 if (!p) p = new Package(pack_path, packageFile); 548 addPackages(m_packages, p); 549 } catch( Exception e ){ 550 logError("Failed to load package in %s: %s", pack_path, e.msg); 551 logDiagnostic("Full error: %s", e.toString().sanitize()); 552 } 553 } 554 catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString()); 555 } 556 } 557 558 m_packages = null; 559 foreach (p; this.completeSearchPath) 560 scanPackageFolder(p); 561 562 void loadOverrides(LocalPackageType type) 563 { 564 m_repositories[type].overrides = null; 565 auto ovrfilepath = m_repositories[type].packagePath ~ LocalOverridesFilename; 566 if (existsFile(ovrfilepath)) { 567 foreach (entry; jsonFromFile(ovrfilepath)) { 568 PackageOverride ovr; 569 ovr.package_ = entry.name.get!string; 570 ovr.version_ = Dependency(entry["version"].get!string); 571 if (auto pv = "targetVersion" in entry) ovr.targetVersion = Version(pv.get!string); 572 if (auto pv = "targetPath" in entry) ovr.targetPath = Path(pv.get!string); 573 m_repositories[type].overrides ~= ovr; 574 } 575 } 576 } 577 loadOverrides(LocalPackageType.user); 578 loadOverrides(LocalPackageType.system); 579 } 580 581 alias Hash = ubyte[]; 582 /// Generates a hash value for a given package. 583 /// Some files or folders are ignored during the generation (like .dub and 584 /// .svn folders) 585 Hash hashPackage(Package pack) 586 { 587 string[] ignored_directories = [".git", ".dub", ".svn"]; 588 // something from .dub_ignore or what? 589 string[] ignored_files = []; 590 SHA1 sha1; 591 foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) { 592 if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString())) 593 continue; 594 else if(ignored_files.canFind(Path(file.name).head.toString())) 595 continue; 596 597 sha1.put(cast(ubyte[])Path(file.name).head.toString()); 598 if(file.isDir) { 599 logDebug("Hashed directory name %s", Path(file.name).head); 600 } 601 else { 602 sha1.put(openFile(Path(file.name)).readAll()); 603 logDebug("Hashed file contents from %s", Path(file.name).head); 604 } 605 } 606 auto hash = sha1.finish(); 607 logDebug("Project hash: %s", hash); 608 return hash[].dup; 609 } 610 611 private void writeLocalPackageList(LocalPackageType type) 612 { 613 Json[] newlist; 614 foreach (p; m_repositories[type].searchPath) { 615 auto entry = Json.emptyObject; 616 entry.name = "*"; 617 entry.path = p.toNativeString(); 618 newlist ~= entry; 619 } 620 621 foreach (p; m_repositories[type].localPackages) { 622 if (p.parentPackage) continue; // do not store sub packages 623 auto entry = Json.emptyObject; 624 entry["name"] = p.name; 625 entry["version"] = p.ver.toString(); 626 entry["path"] = p.path.toNativeString(); 627 newlist ~= entry; 628 } 629 630 Path path = m_repositories[type].packagePath; 631 if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString()); 632 writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); 633 } 634 635 private void writeLocalPackageOverridesFile(LocalPackageType type) 636 { 637 Json[] newlist; 638 foreach (ovr; m_repositories[type].overrides) { 639 auto jovr = Json.emptyObject; 640 jovr.name = ovr.package_; 641 jovr["version"] = ovr.version_.versionString; 642 if (!ovr.targetPath.empty) jovr.targetPath = ovr.targetPath.toNativeString(); 643 else jovr.targetVersion = ovr.targetVersion.toString(); 644 newlist ~= jovr; 645 } 646 auto path = m_repositories[type].packagePath; 647 if (!existsDirectory(path)) mkdirRecurse(path.toNativeString()); 648 writeJsonFile(path ~ LocalOverridesFilename, Json(newlist)); 649 } 650 651 /// Adds the package and scans for subpackages. 652 private void addPackages(ref Package[] dst_repos, Package pack) 653 const { 654 // Add the main package. 655 dst_repos ~= pack; 656 657 // Additionally to the internally defined subpackages, whose metadata 658 // is loaded with the main dub.json, load all externally defined 659 // packages after the package is available with all the data. 660 foreach (spr; pack.subPackages) { 661 Package sp; 662 663 if (spr.path.length) { 664 auto p = Path(spr.path); 665 p.normalize(); 666 enforce(!p.absolute, "Sub package paths must be sub paths of the parent package."); 667 auto path = pack.path ~ p; 668 if (!existsFile(path)) { 669 logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString()); 670 continue; 671 } 672 sp = new Package(path, PathAndFormat(), pack); 673 } else sp = new Package(spr.recipe, pack.path, pack); 674 675 // Add the subpackage. 676 try { 677 dst_repos ~= sp; 678 } catch (Exception e) { 679 logError("Package '%s': Failed to load sub-package %s: %s", pack.name, 680 spr.path.length ? spr.path : spr.recipe.name, e.msg); 681 logDiagnostic("Full error: %s", e.toString().sanitize()); 682 } 683 } 684 } 685 } 686 687 struct PackageOverride { 688 string package_; 689 Dependency version_; 690 Version targetVersion; 691 Path targetPath; 692 693 this(string package_, Dependency version_, Version target_version) 694 { 695 this.package_ = package_; 696 this.version_ = version_; 697 this.targetVersion = target_version; 698 } 699 700 this(string package_, Dependency version_, Path target_path) 701 { 702 this.package_ = package_; 703 this.version_ = version_; 704 this.targetPath = target_path; 705 } 706 } 707 708 enum LocalPackageType { 709 user, 710 system 711 } 712 713 enum LocalPackagesFilename = "local-packages.json"; 714 enum LocalOverridesFilename = "local-overrides.json"; 715 716 717 private struct Repository { 718 Path path; 719 Path packagePath; 720 Path[] searchPath; 721 Package[] localPackages; 722 PackageOverride[] overrides; 723 724 this(Path path) 725 { 726 this.path = path; 727 this.packagePath = path ~"packages/"; 728 } 729 }