Newer
Older
dub_jkp / source / dub / packagemanager.d
@Dicebot Dicebot on 1 Nov 2013 19 KB Remove 'installation' references.
  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;
  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. enum JournalJsonFilename = "journal.json";
  30. enum LocalPackagesFilename = "local-packages.json";
  31.  
  32.  
  33. private struct Repository {
  34. Path path;
  35. Path packagePath;
  36. Path[] searchPath;
  37. Package[] localPackages;
  38.  
  39. this(Path path)
  40. {
  41. this.path = path;
  42. this.packagePath = path ~"packages/";
  43. }
  44. }
  45.  
  46. enum LocalPackageType {
  47. user,
  48. system
  49. }
  50.  
  51. /// The PackageManager can retrieve present packages and get / remove
  52. /// packages.
  53. class PackageManager {
  54. private {
  55. Repository[LocalPackageType] m_repositories;
  56. Path[] m_searchPath;
  57. Package[][string] m_packages;
  58. Package[] m_temporaryPackages;
  59. }
  60.  
  61. this(Path user_path, Path system_path)
  62. {
  63. m_repositories[LocalPackageType.user] = Repository(user_path);
  64. m_repositories[LocalPackageType.system] = Repository(system_path);
  65. refresh(true);
  66. }
  67.  
  68. @property void searchPath(Path[] paths) { m_searchPath = paths.dup; refresh(false); }
  69. @property const(Path)[] searchPath() const { return m_searchPath; }
  70.  
  71. @property const(Path)[] completeSearchPath()
  72. const {
  73. auto ret = appender!(Path[])();
  74. ret.put(m_searchPath);
  75. ret.put(m_repositories[LocalPackageType.user].searchPath);
  76. ret.put(m_repositories[LocalPackageType.user].packagePath);
  77. ret.put(m_repositories[LocalPackageType.system].searchPath);
  78. ret.put(m_repositories[LocalPackageType.system].packagePath);
  79. return ret.data;
  80. }
  81.  
  82. Package getPackage(string name, Version ver)
  83. {
  84. foreach( p; getPackageIterator(name) )
  85. if( p.ver == ver )
  86. return p;
  87. return null;
  88. }
  89.  
  90. Package getPackage(string name, string ver, Path in_path)
  91. {
  92. return getPackage(name, Version(ver), in_path);
  93. }
  94. Package getPackage(string name, Version ver, Path in_path)
  95. {
  96. foreach( p; getPackageIterator(name) )
  97. if (p.ver == ver && p.path.startsWith(in_path))
  98. return p;
  99. return null;
  100. }
  101.  
  102. Package getPackage(string name, string ver)
  103. {
  104. foreach (ep; getPackageIterator(name)) {
  105. if (ep.vers == ver)
  106. return ep;
  107. }
  108. return null;
  109. }
  110.  
  111. Package getFirstPackage(string name)
  112. {
  113. foreach (ep; getPackageIterator(name))
  114. return ep;
  115. return null;
  116. }
  117.  
  118. Package getPackage(Path path)
  119. {
  120. foreach (p; getPackageIterator())
  121. if (!p.basePackage && p.path == path)
  122. return p;
  123. auto p = new Package(path);
  124. m_temporaryPackages ~= p;
  125. return p;
  126. }
  127.  
  128. Package getBestPackage(string name, string version_spec)
  129. {
  130. return getBestPackage(name, Dependency(version_spec));
  131. }
  132.  
  133. Package getBestPackage(string name, Dependency version_spec)
  134. {
  135. Package ret;
  136. foreach( p; getPackageIterator(name) )
  137. if( version_spec.matches(p.ver) && (!ret || p.ver > ret.ver) )
  138. ret = p;
  139. return ret;
  140. }
  141.  
  142. int delegate(int delegate(ref Package)) getPackageIterator()
  143. {
  144. int iterator(int delegate(ref Package) del)
  145. {
  146. int handlePackage(Package p) {
  147. if (auto ret = del(p)) return ret;
  148. foreach (sp; p.subPackages)
  149. if (auto ret = del(sp))
  150. return ret;
  151. return 0;
  152. }
  153.  
  154. foreach (tp; m_temporaryPackages)
  155. if (auto ret = handlePackage(tp)) return ret;
  156.  
  157. // first search local packages
  158. foreach (tp; LocalPackageType.min .. LocalPackageType.max+1)
  159. foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages)
  160. if (auto ret = handlePackage(p)) return ret;
  161.  
  162. // and then all packages gathered from the search path
  163. foreach( pl; m_packages )
  164. foreach( v; pl )
  165. if( auto ret = handlePackage(v) )
  166. return ret;
  167. return 0;
  168. }
  169.  
  170. return &iterator;
  171. }
  172.  
  173. int delegate(int delegate(ref Package)) getPackageIterator(string name)
  174. {
  175. int iterator(int delegate(ref Package) del)
  176. {
  177. foreach (p; getPackageIterator())
  178. if (p.name == name)
  179. if (auto ret = del(p)) return ret;
  180. return 0;
  181. }
  182.  
  183. return &iterator;
  184. }
  185.  
  186. /// Retrieves the package supplied as a path to it's zip file to the
  187. /// destination.
  188. // FIXNAME
  189. Package get(Path zip_file_path, Json package_info, Path destination)
  190. {
  191. auto package_name = package_info.name.get!string();
  192. auto package_version = package_info["version"].get!string();
  193. auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $];
  194.  
  195. logDiagnostic("Placing package '%s' version '%s' to location '%s' from file '%s'",
  196. package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString());
  197.  
  198. if( existsFile(destination) ){
  199. throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination));
  200. }
  201.  
  202. // open zip file
  203. ZipArchive archive;
  204. {
  205. logDebug("Opening file %s", zip_file_path);
  206. auto f = openFile(zip_file_path, FileMode.Read);
  207. scope(exit) f.close();
  208. archive = new ZipArchive(f.readAll());
  209. }
  210.  
  211. logDebug("Extracting from zip.");
  212.  
  213. // In a github zip, the actual contents are in a subfolder
  214. Path zip_prefix;
  215. auto json_file = PathEntry(PackageJsonFilename);
  216. foreach(ArchiveMember am; archive.directory) {
  217. auto path = Path(am.name);
  218. if (path.length == 2 && path.head == json_file && path.length) {
  219. zip_prefix = path[0 .. $-1];
  220. break;
  221. }
  222. }
  223.  
  224. logDebug("zip root folder: %s", zip_prefix);
  225.  
  226. Path getCleanedPath(string fileName) {
  227. auto path = Path(fileName);
  228. if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path();
  229. return path[zip_prefix.length..path.length];
  230. }
  231.  
  232. // extract & place
  233. mkdirRecurse(destination.toNativeString());
  234. auto journal = new Journal;
  235. logDiagnostic("Copying all files...");
  236. int countFiles = 0;
  237. foreach(ArchiveMember a; archive.directory) {
  238. auto cleanedPath = getCleanedPath(a.name);
  239. if(cleanedPath.empty) continue;
  240. auto dst_path = destination~cleanedPath;
  241.  
  242. logDebug("Creating %s", cleanedPath);
  243. if( dst_path.endsWithSlash ){
  244. if( !existsDirectory(dst_path) )
  245. mkdirRecurse(dst_path.toNativeString());
  246. journal.add(Journal.Entry(Journal.Type.Directory, cleanedPath));
  247. } else {
  248. if( !existsDirectory(dst_path.parentPath) )
  249. mkdirRecurse(dst_path.parentPath.toNativeString());
  250. auto dstFile = openFile(dst_path, FileMode.CreateTrunc);
  251. scope(exit) dstFile.close();
  252. dstFile.put(archive.expand(a));
  253. journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath));
  254. ++countFiles;
  255. }
  256. }
  257. logDiagnostic("%s file(s) copied.", to!string(countFiles));
  258.  
  259. // overwrite package.json (this one includes a version field)
  260. Json pi = jsonFromFile(destination~PackageJsonFilename);
  261. pi["name"] = toLower(pi["name"].get!string());
  262. pi["version"] = package_info["version"];
  263. writeJsonFile(destination~PackageJsonFilename, pi);
  264.  
  265. // Write journal
  266. logDebug("Saving retrieval action journal...");
  267. journal.add(Journal.Entry(Journal.Type.RegularFile, Path(JournalJsonFilename)));
  268. journal.save(destination ~ JournalJsonFilename);
  269.  
  270. if( existsFile(destination~PackageJsonFilename) )
  271. logInfo("%s is present with version %s", package_name, package_version);
  272.  
  273. auto pack = new Package(destination);
  274.  
  275. m_packages[package_name] ~= pack;
  276.  
  277. return pack;
  278. }
  279.  
  280. /// Removes the given the package.
  281. void remove(in Package pack)
  282. {
  283. logDebug("Remove %s, version %s, path '%s'", pack.name, pack.vers, pack.path);
  284. enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
  285.  
  286. // remove package from repositories' list
  287. bool found = false;
  288. bool removeFrom(Package[] packs, in Package pack) {
  289. auto packPos = countUntil!("a.path == b.path")(packs, pack);
  290. if(packPos != -1) {
  291. packs = std.algorithm.remove(packs, packPos);
  292. return true;
  293. }
  294. return false;
  295. }
  296. foreach(repo; m_repositories) {
  297. if(removeFrom(repo.localPackages, pack)) {
  298. found = true;
  299. break;
  300. }
  301. }
  302. if(!found) {
  303. foreach(packsOfId; m_packages) {
  304. if(removeFrom(packsOfId, pack)) {
  305. found = true;
  306. break;
  307. }
  308. }
  309. }
  310. enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
  311.  
  312. // delete package files physically
  313. logDebug("Looking up journal");
  314. auto journalFile = pack.path~JournalJsonFilename;
  315. if( !existsFile(journalFile) )
  316. throw new Exception("Removal failed, no retrieval journal found for '"~pack.name~"'. Please remove manually.");
  317.  
  318. auto packagePath = pack.path;
  319. auto journal = new Journal(journalFile);
  320. logDebug("Erasing files");
  321. foreach( Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.RegularFile)(journal.entries)) {
  322. logDebug("Deleting file '%s'", e.relFilename);
  323. auto absFile = pack.path~e.relFilename;
  324. if(!existsFile(absFile)) {
  325. logWarn("Previously retrieved file not found for removal: '%s'", absFile);
  326. continue;
  327. }
  328.  
  329. removeFile(absFile);
  330. }
  331.  
  332. logDiagnostic("Erasing directories");
  333. Path[] allPaths;
  334. foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries))
  335. allPaths ~= pack.path~e.relFilename;
  336. sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first
  337. foreach(Path p; allPaths) {
  338. logDebug("Deleting folder '%s'", p);
  339. if( !existsFile(p) || !isDir(p.toNativeString()) || !isEmptyDir(p) ) {
  340. logError("Alien files found, directory is not empty or is not a directory: '%s'", p);
  341. continue;
  342. }
  343. rmdir(p.toNativeString());
  344. }
  345.  
  346. // Erase .dub folder, this is completely erased.
  347. auto dubDir = (pack.path ~ ".dub/").toNativeString();
  348. enforce(!existsFile(dubDir) || isDir(dubDir), ".dub should be a directory, but is a file.");
  349. if(existsFile(dubDir) && isDir(dubDir)) {
  350. logDebug(".dub directory found, removing directory including content.");
  351. rmdirRecurse(dubDir);
  352. }
  353.  
  354. logDebug("About to delete root folder for package '%s'.", pack.path);
  355. if(!isEmptyDir(pack.path))
  356. throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually.");
  357.  
  358. rmdir(pack.path.toNativeString());
  359. logInfo("Removed package: '"~pack.name~"'");
  360. }
  361.  
  362. Package addLocalPackage(in Path path, in Version ver, LocalPackageType type)
  363. {
  364. Package[]* packs = &m_repositories[type].localPackages;
  365. auto info = jsonFromFile(path ~ PackageJsonFilename, false);
  366. string name;
  367. if( "name" !in info ) info["name"] = path.head.toString();
  368. info["version"] = ver.toString();
  369.  
  370. // don't double-add packages
  371. foreach( p; *packs ){
  372. if( p.path == path ){
  373. enforce(p.ver == ver, "Adding local twice with different versions is not allowed.");
  374. logInfo("Package is already registered: %s (version: %s)", p.name, p.ver);
  375. return p;
  376. }
  377. }
  378.  
  379. auto pack = new Package(info, path);
  380.  
  381. *packs ~= pack;
  382.  
  383. writeLocalPackageList(type);
  384.  
  385. logInfo("Registered package: %s (version: %s)", pack.name, pack.ver);
  386. return pack;
  387. }
  388.  
  389. void removeLocalPackage(in Path path, LocalPackageType type)
  390. {
  391. Package[]* packs = &m_repositories[type].localPackages;
  392. size_t[] to_remove;
  393. foreach( i, entry; *packs )
  394. if( entry.path == path )
  395. to_remove ~= i;
  396. enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
  397.  
  398. string[Version] removed;
  399. foreach_reverse( i; to_remove ) {
  400. removed[(*packs)[i].ver] = (*packs)[i].name;
  401. *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $];
  402. }
  403.  
  404. writeLocalPackageList(type);
  405.  
  406. foreach(ver, name; removed)
  407. logInfo("Unregistered package: %s (version: %s)", name, ver);
  408. }
  409.  
  410. Package getTemporaryPackage(Path path, Version ver)
  411. {
  412. foreach (p; m_temporaryPackages)
  413. if (p.path == path) {
  414. enforce(p.ver == ver, format("Package in %s is refrenced with two conflicting versions: %s vs %s", path.toNativeString(), p.ver, ver));
  415. return p;
  416. }
  417. auto info = jsonFromFile(path ~ PackageJsonFilename, false);
  418. string name;
  419. if( "name" !in info ) info["name"] = path.head.toString();
  420. info["version"] = ver.toString();
  421.  
  422. auto pack = new Package(info, path);
  423. m_temporaryPackages ~= pack;
  424. return pack;
  425. }
  426.  
  427. /// For the given type add another path where packages will be looked up.
  428. void addSearchPath(Path path, LocalPackageType type)
  429. {
  430. m_repositories[type].searchPath ~= path;
  431. writeLocalPackageList(type);
  432. }
  433.  
  434. /// Removes a search path from the given type.
  435. void removeSearchPath(Path path, LocalPackageType type)
  436. {
  437. m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
  438. writeLocalPackageList(type);
  439. }
  440.  
  441. void refresh(bool refresh_existing_packages)
  442. {
  443. // load locally defined packages
  444. void scanLocalPackages(LocalPackageType type)
  445. {
  446. Path list_path = m_repositories[type].packagePath;
  447. Package[] packs;
  448. Path[] paths;
  449. try {
  450. auto local_package_file = list_path ~ LocalPackagesFilename;
  451. logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString());
  452. if( !existsFile(local_package_file) ) return;
  453. logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString());
  454. auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename);
  455. enforce(packlist.type == Json.Type.Array, LocalPackagesFilename~" must contain an array.");
  456. foreach( pentry; packlist ){
  457. try {
  458. auto name = pentry.name.get!string();
  459. auto path = Path(pentry.path.get!string());
  460. if (name == "*") {
  461. paths ~= path;
  462. } else {
  463. auto ver = pentry["version"].get!string();
  464. auto info = Json.EmptyObject;
  465. if( existsFile(path ~ PackageJsonFilename) ) info = jsonFromFile(path ~ PackageJsonFilename);
  466. if( "name" in info && info.name.get!string() != name )
  467. logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, info.name.get!string());
  468. info.name = name;
  469. info["version"] = ver;
  470.  
  471. Package pp;
  472. if (!refresh_existing_packages)
  473. foreach (p; m_repositories[type].localPackages)
  474. if (p.path == path) {
  475. pp = p;
  476. break;
  477. }
  478. if (!pp) pp = new Package(info, path);
  479. packs ~= pp;
  480. }
  481. } catch( Exception e ){
  482. logWarn("Error adding local package: %s", e.msg);
  483. }
  484. }
  485. } catch( Exception e ){
  486. logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
  487. }
  488. m_repositories[type].localPackages = packs;
  489. m_repositories[type].searchPath = paths;
  490. }
  491. scanLocalPackages(LocalPackageType.system);
  492. scanLocalPackages(LocalPackageType.user);
  493.  
  494. Package[][string] old_packages = m_packages;
  495.  
  496. // rescan the system and user package folder
  497. void scanPackageFolder(Path path)
  498. {
  499. if( path.existsDirectory() ){
  500. logDebug("iterating dir %s", path.toNativeString());
  501. try foreach( pdir; iterateDirectory(path) ){
  502. logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
  503. if( !pdir.isDirectory ) continue;
  504. auto pack_path = path ~ pdir.name;
  505. if( !existsFile(pack_path ~ PackageJsonFilename) ) continue;
  506. Package p;
  507. try {
  508. if (!refresh_existing_packages)
  509. foreach (plist; old_packages)
  510. foreach (pp; plist)
  511. if (pp.path == pack_path) {
  512. p = pp;
  513. break;
  514. }
  515. if (!p) p = new Package(pack_path);
  516. m_packages[p.name] ~= p;
  517. } catch( Exception e ){
  518. logError("Failed to load package in %s: %s", pack_path, e.msg);
  519. logDiagnostic("Full error: %s", e.toString().sanitize());
  520. }
  521. }
  522. catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
  523. }
  524. }
  525.  
  526. m_packages = null;
  527. foreach (p; this.completeSearchPath)
  528. scanPackageFolder(p);
  529. }
  530.  
  531. alias ubyte[] Hash;
  532. /// Generates a hash value for a given package.
  533. /// Some files or folders are ignored during the generation (like .dub and
  534. /// .svn folders)
  535. Hash hashPackage(Package pack)
  536. {
  537. string[] ignored_directories = [".git", ".dub", ".svn"];
  538. // something from .dub_ignore or what?
  539. string[] ignored_files = [];
  540. SHA1 sha1;
  541. foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
  542. if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString()))
  543. continue;
  544. else if(ignored_files.canFind(Path(file.name).head.toString()))
  545. continue;
  546.  
  547. sha1.put(cast(ubyte[])Path(file.name).head.toString());
  548. if(file.isDir) {
  549. logDebug("Hashed directory name %s", Path(file.name).head);
  550. }
  551. else {
  552. sha1.put(openFile(Path(file.name)).readAll());
  553. logDebug("Hashed file contents from %s", Path(file.name).head);
  554. }
  555. }
  556. auto hash = sha1.finish();
  557. logDebug("Project hash: %s", hash);
  558. return hash[0..$];
  559. }
  560.  
  561. private void writeLocalPackageList(LocalPackageType type)
  562. {
  563. Json[] newlist;
  564. foreach (p; m_repositories[type].searchPath) {
  565. auto entry = Json.EmptyObject;
  566. entry.name = "*";
  567. entry.path = p.toNativeString();
  568. newlist ~= entry;
  569. }
  570.  
  571. foreach (p; m_repositories[type].localPackages) {
  572. auto entry = Json.EmptyObject;
  573. entry["name"] = p.name;
  574. entry["version"] = p.ver.toString();
  575. entry["path"] = p.path.toNativeString();
  576. newlist ~= entry;
  577. }
  578.  
  579. Path path = m_repositories[type].packagePath;
  580. if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString());
  581. writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
  582. }
  583. }
  584.  
  585.  
  586. /**
  587. Retrieval journal for later removal, keeping track of placed files
  588. files.
  589. Example Json:
  590. {
  591. "version": 1,
  592. "files": {
  593. "file1": "typeoffile1",
  594. ...
  595. }
  596. }
  597. */
  598. private class Journal {
  599. private enum Version = 1;
  600. enum Type {
  601. RegularFile,
  602. Directory,
  603. Alien
  604. }
  605. struct Entry {
  606. this( Type t, Path f ) { type = t; relFilename = f; }
  607. Type type;
  608. Path relFilename;
  609. }
  610. @property const(Entry[]) entries() const { return m_entries; }
  611. this() {}
  612. /// Initializes a Journal from a json file.
  613. this(Path journalFile) {
  614. auto jsonJournal = jsonFromFile(journalFile);
  615. enforce(cast(int)jsonJournal["Version"] == Version, "Mismatched version: "~to!string(cast(int)jsonJournal["Version"]) ~ "vs. " ~to!string(Version));
  616. foreach(string file, type; jsonJournal["Files"])
  617. m_entries ~= Entry(to!Type(cast(string)type), Path(file));
  618. }
  619.  
  620. void add(Entry e) {
  621. foreach(Entry ent; entries) {
  622. if( e.relFilename == ent.relFilename ) {
  623. enforce(e.type == ent.type, "Duplicate('"~to!string(e.relFilename)~"'), different types: "~to!string(e.type)~" vs. "~to!string(ent.type));
  624. return;
  625. }
  626. }
  627. m_entries ~= e;
  628. }
  629. /// Save the current state to the path.
  630. void save(Path path) {
  631. Json jsonJournal = serialize();
  632. auto fileJournal = openFile(path, FileMode.CreateTrunc);
  633. scope(exit) fileJournal.close();
  634. fileJournal.writePrettyJsonString(jsonJournal);
  635. }
  636. private Json serialize() const {
  637. Json[string] files;
  638. foreach(Entry e; m_entries)
  639. files[to!string(e.relFilename)] = to!string(e.type);
  640. Json[string] json;
  641. json["Version"] = Version;
  642. json["Files"] = files;
  643. return Json(json);
  644. }
  645. private {
  646. Entry[] m_entries;
  647. }
  648. }