Newer
Older
dub_jkp / source / dub / packagemanager.d
  1. /**
  2. Management of packages on the local computer.
  3.  
  4. Copyright: © 2012 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.installation;
  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. import dub.utils;
  18.  
  19. import std.algorithm : countUntil, filter, sort, canFind;
  20. import std.array;
  21. import std.conv;
  22. import std.digest.sha;
  23. import std.encoding : sanitize;
  24. import std.exception;
  25. import std.file;
  26. import std.string;
  27. import std.zip;
  28.  
  29.  
  30. enum JournalJsonFilename = "journal.json";
  31. enum LocalPackagesFilename = "local-packages.json";
  32.  
  33.  
  34. private struct Repository {
  35. Path path;
  36. Path packagePath;
  37. Path[] searchPath;
  38. Package[] localPackages;
  39.  
  40. this(Path path)
  41. {
  42. this.path = path;
  43. this.packagePath = path ~"packages/";
  44. }
  45. }
  46.  
  47. enum LocalPackageType {
  48. user,
  49. system
  50. }
  51.  
  52.  
  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 getPackage(Path path)
  112. {
  113. foreach (p; getPackageIterator())
  114. if (!p.basePackage && p.path == path)
  115. return p;
  116. auto p = new Package(path);
  117. m_temporaryPackages ~= p;
  118. return p;
  119. }
  120.  
  121. Package getBestPackage(string name, string version_spec)
  122. {
  123. return getBestPackage(name, Dependency(version_spec));
  124. }
  125.  
  126. Package getBestPackage(string name, Dependency version_spec)
  127. {
  128. Package ret;
  129. foreach( p; getPackageIterator(name) )
  130. if( version_spec.matches(p.ver) && (!ret || p.ver > ret.ver) )
  131. ret = p;
  132. return ret;
  133. }
  134.  
  135. int delegate(int delegate(ref Package)) getPackageIterator()
  136. {
  137. int iterator(int delegate(ref Package) del)
  138. {
  139. int handlePackage(Package p) {
  140. if (auto ret = del(p)) return ret;
  141. foreach (sp; p.subPackages)
  142. if (auto ret = del(sp))
  143. return ret;
  144. return 0;
  145. }
  146.  
  147. foreach (tp; m_temporaryPackages)
  148. if (auto ret = handlePackage(tp)) return ret;
  149.  
  150. // first search local packages
  151. foreach (tp; LocalPackageType.min .. LocalPackageType.max+1)
  152. foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages)
  153. if (auto ret = handlePackage(p)) return ret;
  154.  
  155. // and then all packages gathered from the search path
  156. foreach( pl; m_packages )
  157. foreach( v; pl )
  158. if( auto ret = handlePackage(v) )
  159. return ret;
  160. return 0;
  161. }
  162.  
  163. return &iterator;
  164. }
  165.  
  166. int delegate(int delegate(ref Package)) getPackageIterator(string name)
  167. {
  168. int iterator(int delegate(ref Package) del)
  169. {
  170. foreach (p; getPackageIterator())
  171. if (p.name == name)
  172. if (auto ret = del(p)) return ret;
  173. return 0;
  174. }
  175.  
  176. return &iterator;
  177. }
  178.  
  179. Package install(Path zip_file_path, Json package_info, Path destination)
  180. {
  181. auto package_name = package_info.name.get!string();
  182. auto package_version = package_info["version"].get!string();
  183. auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $];
  184.  
  185. logDiagnostic("Installing package '%s' version '%s' to location '%s' from file '%s'",
  186. package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString());
  187.  
  188. if( existsFile(destination) ){
  189. throw new Exception(format("%s (%s) needs to be uninstalled from '%s' prior installation.", package_name, package_version, destination));
  190. }
  191.  
  192. // open zip file
  193. ZipArchive archive;
  194. {
  195. logDebug("Opening file %s", zip_file_path);
  196. auto f = openFile(zip_file_path, FileMode.Read);
  197. scope(exit) f.close();
  198. archive = new ZipArchive(f.readAll());
  199. }
  200.  
  201. logDebug("Installing from zip.");
  202.  
  203. // In a github zip, the actual contents are in a subfolder
  204. Path zip_prefix;
  205. auto json_file = PathEntry(PackageJsonFilename);
  206. foreach(ArchiveMember am; archive.directory) {
  207. auto path = Path(am.name);
  208. if (path.length == 2 && path.head == json_file && path.length) {
  209. zip_prefix = path[0 .. $-1];
  210. break;
  211. }
  212. }
  213.  
  214. logDebug("zip root folder: %s", zip_prefix);
  215.  
  216. Path getCleanedPath(string fileName) {
  217. auto path = Path(fileName);
  218. if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path();
  219. return path[zip_prefix.length..path.length];
  220. }
  221.  
  222. // install
  223. mkdirRecurse(destination.toNativeString());
  224. auto journal = new Journal;
  225. logDiagnostic("Copying all files...");
  226. int countFiles = 0;
  227. foreach(ArchiveMember a; archive.directory) {
  228. auto cleanedPath = getCleanedPath(a.name);
  229. if(cleanedPath.empty) continue;
  230. auto dst_path = destination~cleanedPath;
  231.  
  232. logDebug("Creating %s", cleanedPath);
  233. if( dst_path.endsWithSlash ){
  234. if( !existsDirectory(dst_path) )
  235. mkdirRecurse(dst_path.toNativeString());
  236. journal.add(Journal.Entry(Journal.Type.Directory, cleanedPath));
  237. } else {
  238. if( !existsDirectory(dst_path.parentPath) )
  239. mkdirRecurse(dst_path.parentPath.toNativeString());
  240. auto dstFile = openFile(dst_path, FileMode.CreateTrunc);
  241. scope(exit) dstFile.close();
  242. dstFile.put(archive.expand(a));
  243. journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath));
  244. ++countFiles;
  245. }
  246. }
  247. logDiagnostic("%s file(s) copied.", to!string(countFiles));
  248.  
  249. // overwrite package.json (this one includes a version field)
  250. Json pi = jsonFromFile(destination~PackageJsonFilename);
  251. pi["name"] = toLower(pi["name"].get!string());
  252. pi["version"] = package_info["version"];
  253. writeJsonFile(destination~PackageJsonFilename, pi);
  254.  
  255. // Write journal
  256. logDebug("Saving installation journal...");
  257. journal.add(Journal.Entry(Journal.Type.RegularFile, Path(JournalJsonFilename)));
  258. journal.save(destination ~ JournalJsonFilename);
  259.  
  260. if( existsFile(destination~PackageJsonFilename) )
  261. logInfo("%s has been installed with version %s", package_name, package_version);
  262.  
  263. auto pack = new Package(destination);
  264.  
  265. m_packages[package_name] ~= pack;
  266.  
  267. return pack;
  268. }
  269.  
  270. void uninstall(in Package pack)
  271. {
  272. logDebug("Uninstall %s, version %s, path '%s'", pack.name, pack.vers, pack.path);
  273. enforce(!pack.path.empty, "Cannot uninstall package "~pack.name~" without a path.");
  274.  
  275. // remove package from repositories' list
  276. bool found = false;
  277. bool removeFrom(Package[] packs, in Package pack) {
  278. auto packPos = countUntil!("a.path == b.path")(packs, pack);
  279. if(packPos != -1) {
  280. packs = std.algorithm.remove(packs, packPos);
  281. return true;
  282. }
  283. return false;
  284. }
  285. foreach(repo; m_repositories) {
  286. if(removeFrom(repo.localPackages, pack)) {
  287. found = true;
  288. break;
  289. }
  290. }
  291. if(!found) {
  292. foreach(packsOfId; m_packages) {
  293. if(removeFrom(packsOfId, pack)) {
  294. found = true;
  295. break;
  296. }
  297. }
  298. }
  299. enforce(found, "Cannot uninstall, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
  300.  
  301. // delete package files physically
  302. logDebug("Looking up journal");
  303. auto journalFile = pack.path~JournalJsonFilename;
  304. if( !existsFile(journalFile) )
  305. throw new Exception("Uninstall failed, no installation journal found for '"~pack.name~"'. Please uninstall manually.");
  306.  
  307. auto packagePath = pack.path;
  308. auto journal = new Journal(journalFile);
  309. logDebug("Erasing files");
  310. foreach( Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.RegularFile)(journal.entries)) {
  311. logDebug("Deleting file '%s'", e.relFilename);
  312. auto absFile = pack.path~e.relFilename;
  313. if(!existsFile(absFile)) {
  314. logWarn("Previously installed file not found for uninstalling: '%s'", absFile);
  315. continue;
  316. }
  317.  
  318. removeFile(absFile);
  319. }
  320.  
  321. logDiagnostic("Erasing directories");
  322. Path[] allPaths;
  323. foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries))
  324. allPaths ~= pack.path~e.relFilename;
  325. sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first
  326. foreach(Path p; allPaths) {
  327. logDebug("Deleting folder '%s'", p);
  328. if( !existsFile(p) || !isDir(p.toNativeString()) || !isEmptyDir(p) ) {
  329. logError("Alien files found, directory is not empty or is not a directory: '%s'", p);
  330. continue;
  331. }
  332. rmdir(p.toNativeString());
  333. }
  334.  
  335. // Erase .dub folder, this is completely erased.
  336. auto dubDir = (pack.path ~ ".dub/").toNativeString();
  337. enforce(!existsFile(dubDir) || isDir(dubDir), ".dub should be a directory, but is a file.");
  338. if(existsFile(dubDir) && isDir(dubDir)) {
  339. logDebug(".dub directory found, removing directory including content.");
  340. rmdirRecurse(dubDir);
  341. }
  342.  
  343. logDebug("About to delete root folder for package '%s'.", pack.path);
  344. if(!isEmptyDir(pack.path))
  345. throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually.");
  346.  
  347. rmdir(pack.path.toNativeString());
  348. logInfo("Uninstalled package: '"~pack.name~"'");
  349. }
  350.  
  351. Package addLocalPackage(in Path path, in Version ver, LocalPackageType type)
  352. {
  353. Package[]* packs = &m_repositories[type].localPackages;
  354. auto info = jsonFromFile(path ~ PackageJsonFilename, false);
  355. string name;
  356. if( "name" !in info ) info["name"] = path.head.toString();
  357. info["version"] = ver.toString();
  358.  
  359. // don't double-add packages
  360. foreach( p; *packs ){
  361. if( p.path == path ){
  362. enforce(p.ver == ver, "Adding local twice with different versions is not allowed.");
  363. return p;
  364. }
  365. }
  366.  
  367. auto pack = new Package(info, path);
  368.  
  369. *packs ~= pack;
  370.  
  371. writeLocalPackageList(type);
  372.  
  373. return pack;
  374. }
  375.  
  376. void removeLocalPackage(in Path path, LocalPackageType type)
  377. {
  378. Package[]* packs = &m_repositories[type].localPackages;
  379. size_t[] to_remove;
  380. foreach( i, entry; *packs )
  381. if( entry.path == path )
  382. to_remove ~= i;
  383. enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
  384.  
  385. foreach_reverse( i; to_remove )
  386. *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $];
  387.  
  388. writeLocalPackageList(type);
  389. }
  390.  
  391. Package getTemporaryPackage(Path path, Version ver)
  392. {
  393. foreach (p; m_temporaryPackages)
  394. if (p.path == path) {
  395. enforce(p.ver == ver, format("Package in %s is refrenced with two conflicting versions: %s vs %s", path.toNativeString(), p.ver, ver));
  396. return p;
  397. }
  398. auto info = jsonFromFile(path ~ PackageJsonFilename, false);
  399. string name;
  400. if( "name" !in info ) info["name"] = path.head.toString();
  401. info["version"] = ver.toString();
  402.  
  403. auto pack = new Package(info, path);
  404. m_temporaryPackages ~= pack;
  405. return pack;
  406. }
  407.  
  408. void addSearchPath(Path path, LocalPackageType type)
  409. {
  410. m_repositories[type].searchPath ~= path;
  411. writeLocalPackageList(type);
  412. }
  413.  
  414. void removeSearchPath(Path path, LocalPackageType type)
  415. {
  416. m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
  417. writeLocalPackageList(type);
  418. }
  419.  
  420. void refresh(bool refresh_existing_packages)
  421. {
  422. // load locally defined packages
  423. void scanLocalPackages(LocalPackageType type)
  424. {
  425. Path list_path = m_repositories[type].packagePath;
  426. Package[] packs;
  427. Path[] paths;
  428. try {
  429. logDiagnostic("Looking for local package map at %s", list_path.toNativeString());
  430. if( !existsFile(list_path ~ LocalPackagesFilename) ) return;
  431. logDiagnostic("Try to load local package map at %s", list_path.toNativeString());
  432. auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename);
  433. enforce(packlist.type == Json.Type.Array, LocalPackagesFilename~" must contain an array.");
  434. foreach( pentry; packlist ){
  435. try {
  436. auto name = pentry.name.get!string();
  437. auto path = Path(pentry.path.get!string());
  438. if (name == "*") {
  439. paths ~= path;
  440. } else {
  441. auto ver = pentry["version"].get!string();
  442. auto info = Json.EmptyObject;
  443. if( existsFile(path ~ PackageJsonFilename) ) info = jsonFromFile(path ~ PackageJsonFilename);
  444. if( "name" in info && info.name.get!string() != name )
  445. logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, info.name.get!string());
  446. info.name = name;
  447. info["version"] = ver;
  448.  
  449. Package pp;
  450. if (!refresh_existing_packages)
  451. foreach (p; m_repositories[type].localPackages)
  452. if (p.path == path) {
  453. pp = p;
  454. break;
  455. }
  456. if (!pp) pp = new Package(info, path);
  457. packs ~= pp;
  458. }
  459. } catch( Exception e ){
  460. logWarn("Error adding local package: %s", e.msg);
  461. }
  462. }
  463. } catch( Exception e ){
  464. logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
  465. }
  466. m_repositories[type].localPackages = packs;
  467. m_repositories[type].searchPath = paths;
  468. }
  469. scanLocalPackages(LocalPackageType.system);
  470. scanLocalPackages(LocalPackageType.user);
  471.  
  472. Package[][string] old_packages = m_packages;
  473.  
  474. // rescan the system and user package folder
  475. void scanPackageFolder(Path path)
  476. {
  477. if( path.existsDirectory() ){
  478. logDebug("iterating dir %s", path.toNativeString());
  479. try foreach( pdir; iterateDirectory(path) ){
  480. logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
  481. if( !pdir.isDirectory ) continue;
  482. auto pack_path = path ~ pdir.name;
  483. if( !existsFile(pack_path ~ PackageJsonFilename) ) continue;
  484. Package p;
  485. try {
  486. if (!refresh_existing_packages)
  487. foreach (plist; old_packages)
  488. foreach (pp; plist)
  489. if (pp.path == pack_path) {
  490. p = pp;
  491. break;
  492. }
  493. if (!p) p = new Package(pack_path);
  494. m_packages[p.name] ~= p;
  495. } catch( Exception e ){
  496. logError("Failed to load package in %s: %s", pack_path, e.msg);
  497. logDiagnostic("Full error: %s", e.toString().sanitize());
  498. }
  499. }
  500. catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
  501. }
  502. }
  503.  
  504. m_packages = null;
  505. foreach (p; this.completeSearchPath)
  506. scanPackageFolder(p);
  507. }
  508.  
  509. alias ubyte[] Hash;
  510. /// Generates a hash value for a given package.
  511. /// Some files or folders are ignored during the generation (like .dub and
  512. /// .svn folders)
  513. Hash hashPackage(Package pack)
  514. {
  515. string[] ignored_directories = [".git", ".dub", ".svn"];
  516. // something from .dub_ignore or what?
  517. string[] ignored_files = [];
  518. SHA1 sha1;
  519. foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
  520. if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString()))
  521. continue;
  522. else if(ignored_files.canFind(Path(file.name).head.toString()))
  523. continue;
  524.  
  525. sha1.put(cast(ubyte[])Path(file.name).head.toString());
  526. if(file.isDir) {
  527. logDebug("Hashed directory name %s", Path(file.name).head);
  528. }
  529. else {
  530. sha1.put(openFile(Path(file.name)).readAll());
  531. logDebug("Hashed file contents from %s", Path(file.name).head);
  532. }
  533. }
  534. auto hash = sha1.finish();
  535. logDebug("Project hash: %s", hash);
  536. return hash[0..$];
  537. }
  538.  
  539. private void writeLocalPackageList(LocalPackageType type)
  540. {
  541. Json[] newlist;
  542. foreach (p; m_repositories[type].searchPath) {
  543. auto entry = Json.EmptyObject;
  544. entry.name = "*";
  545. entry.path = p.toNativeString();
  546. newlist ~= entry;
  547. }
  548.  
  549. foreach (p; m_repositories[type].localPackages) {
  550. auto entry = Json.EmptyObject;
  551. entry["name"] = p.name;
  552. entry["version"] = p.ver.toString();
  553. entry["path"] = p.path.toNativeString();
  554. newlist ~= entry;
  555. }
  556.  
  557. Path path = m_repositories[type].packagePath;
  558. if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString());
  559. writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
  560. }
  561. }