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