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