diff --git a/source/dub/dependency.d b/source/dub/dependency.d index f5b12b5..bca2df2 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -72,7 +72,8 @@ /** Comparing Versions is generally possible, but comparing Versions - identifying branches other than master will fail. + identifying branches other than master will fail. Only equality + can be tested for these. */ int opCmp(ref const Version other) const { @@ -265,8 +266,6 @@ if(!o.valid()) return new Dependency(o); -/// TODO: continue porting Version / Dependency to use branches and string - Version a = m_versA > o.m_versA? m_versA : o.m_versA; Version b = m_versB < o.m_versB? m_versB : o.m_versB; diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index bc3a737..176c404 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -1,495 +1,495 @@ -/** - Management of packages on the local computer. - - Copyright: © 2012 rejectedsoftware e.K. - License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. - Authors: Sönke Ludwig, Matthias Dondorff -*/ -module dub.packagemanager; - -import dub.dependency; -import dub.installation; -import dub.package_; -import dub.utils; - -import std.algorithm : countUntil, filter, sort; -import std.conv; -import std.exception; -import std.file; -import std.string; -import std.zip; -import vibecompat.core.file; -import vibecompat.core.log; -import vibecompat.data.json; -import vibecompat.inet.path; - - -enum JournalJsonFilename = "journal.json"; -enum LocalPackagesFilename = "local-packages.json"; - -enum LocalPackageType { - temporary, - user, - system -} - - -class PackageManager { - private { - Path m_systemPackagePath; - Path m_userPackagePath; - Path m_projectPackagePath; - Package[][string] m_systemPackages; - Package[][string] m_userPackages; - Package[string] m_projectPackages; - Package[] m_localTemporaryPackages; - Package[] m_localUserPackages; - Package[] m_localSystemPackages; - } - - this(Path system_package_path, Path user_package_path, Path project_package_path = Path()) - { - m_systemPackagePath = system_package_path; - m_userPackagePath = user_package_path; - m_projectPackagePath = project_package_path; - refresh(); - } - - @property Path projectPackagePath() const { return m_projectPackagePath; } - @property void projectPackagePath(Path path) { m_projectPackagePath = path; refresh(); } - - Package getPackage(string name, Version ver) - { - foreach( p; getPackageIterator(name) ) - if( p.ver == ver ) - return p; - return null; - } - - bool hasPackage(string name, string ver, InstallLocation location) - { - foreach(ep; getPackageIterator()){ - if( ep.installLocation == location && ep.name == name && ep.vers == ver ) - return true; - } - return false; - } - - Package getBestPackage(string name, string version_spec) - { - return getBestPackage(name, new Dependency(version_spec)); - } - - Package getBestPackage(string name, in Dependency version_spec) - { - Package ret; - foreach( p; getPackageIterator(name) ) - if( version_spec.matches(p.ver) && (!ret || p.ver > ret.ver) ) - ret = p; - return ret; - } - - int delegate(int delegate(ref Package)) getPackageIterator() - { - int iterator(int delegate(ref Package) del) - { - // first search project local packages - foreach( p; m_localTemporaryPackages ) - if( auto ret = del(p) ) return ret; - foreach( p; m_projectPackages ) - if( auto ret = del(p) ) return ret; - - // then local packages - foreach( p; m_localUserPackages ) - if( auto ret = del(p) ) return ret; - - // then local packages - foreach( p; m_localSystemPackages ) - if( auto ret = del(p) ) return ret; - - // then user installed packages - foreach( pl; m_userPackages ) - foreach( v; pl ) - if( auto ret = del(v) ) - return ret; - - // finally system-wide installed packages - foreach( pl; m_systemPackages ) - foreach( v; pl ) - if( auto ret = del(v) ) - return ret; - - return 0; - } - - return &iterator; - } - - int delegate(int delegate(ref Package)) getPackageIterator(string name) - { - int iterator(int delegate(ref Package) del) - { - // first search project local packages - foreach( p; m_localTemporaryPackages ) - if( p.name == name ) - if( auto ret = del(p) ) return ret; - if( auto pp = name in m_projectPackages ) - if( auto ret = del(*pp) ) return ret; - - // then local packages - foreach( p; m_localUserPackages ) - if( p.name == name ) - if( auto ret = del(p) ) return ret; - - // then local packages - foreach( p; m_localSystemPackages ) - if( p.name == name ) - if( auto ret = del(p) ) return ret; - - // then user installed packages - if( auto pp = name in m_userPackages ) - foreach( v; *pp ) - if( auto ret = del(v) ) - return ret; - - // finally system-wide installed packages - if( auto pp = name in m_systemPackages ) - foreach( v; *pp ) - if( auto ret = del(v) ) - return ret; - - return 0; - } - - return &iterator; - } - - Package install(Path zip_file_path, Json package_info, InstallLocation location) - { - auto package_name = package_info.name.get!string(); - auto package_version = package_info["version"].get!string(); - auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $]; - - logDebug("Installing package '%s' version '%s' to location '%s' from file '%s'", - package_name, package_version, to!string(location), zip_file_path.toNativeString()); - - Path destination; - final switch( location ){ - case InstallLocation.local: destination = Path(package_name); break; - case InstallLocation.projectLocal: enforce(!m_projectPackagePath.empty, "no project path set."); destination = m_projectPackagePath ~ package_name; break; - case InstallLocation.userWide: destination = m_userPackagePath ~ (package_name ~ "/" ~ clean_package_version); break; - case InstallLocation.systemWide: destination = m_systemPackagePath ~ (package_name ~ "/" ~ clean_package_version); break; - } - - if( existsFile(destination) ){ - throw new Exception(format("%s %s needs to be uninstalled prior installation.", package_name, package_version)); - } - - // open zip file - ZipArchive archive; - { - logTrace("Opening file %s", zip_file_path); - auto f = openFile(zip_file_path, FileMode.Read); - scope(exit) f.close(); - archive = new ZipArchive(f.readAll()); - } - - logTrace("Installing from zip."); - - // In a github zip, the actual contents are in a subfolder - Path zip_prefix; - foreach(ArchiveMember am; archive.directory) - if( Path(am.name).head == PathEntry(PackageJsonFilename) ){ - zip_prefix = Path(am.name)[0 .. 1]; - break; - } - - if( zip_prefix.empty ){ - // not correct zip packages HACK - Path minPath; - foreach(ArchiveMember am; archive.directory) - if( isPathFromZip(am.name) && (minPath == Path() || minPath.startsWith(Path(am.name))) ) - zip_prefix = Path(am.name); - } - - logTrace("zip root folder: %s", zip_prefix); - - Path getCleanedPath(string fileName) { - auto path = Path(fileName); - if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path(); - return path[zip_prefix.length..path.length]; - } - - // install - mkdirRecurse(destination.toNativeString()); - auto journal = new Journal; - logDebug("Copying all files..."); - int countFiles = 0; - foreach(ArchiveMember a; archive.directory) { - auto cleanedPath = getCleanedPath(a.name); - if(cleanedPath.empty) continue; - auto dst_path = destination~cleanedPath; - - logTrace("Creating %s", cleanedPath); - if( dst_path.endsWithSlash ){ - if( !existsDirectory(dst_path) ) - mkdirRecurse(dst_path.toNativeString()); - journal.add(Journal.Entry(Journal.Type.Directory, cleanedPath)); - } else { - if( !existsDirectory(dst_path.parentPath) ) - mkdirRecurse(dst_path.parentPath.toNativeString()); - auto dstFile = openFile(dst_path, FileMode.CreateTrunc); - scope(exit) dstFile.close(); - dstFile.put(archive.expand(a)); - journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath)); - ++countFiles; - } - } - logDebug("%s file(s) copied.", to!string(countFiles)); - - // overwrite package.json (this one includes a version field) - Json pi = jsonFromFile(destination~PackageJsonFilename); - pi["version"] = package_info["version"]; - writeJsonFile(destination~PackageJsonFilename, pi); - - // Write journal - logTrace("Saving installation journal..."); - journal.add(Journal.Entry(Journal.Type.RegularFile, Path(JournalJsonFilename))); - journal.save(destination ~ JournalJsonFilename); - - if( existsFile(destination~PackageJsonFilename) ) - logInfo("%s has been installed with version %s", package_name, package_version); - - auto pack = new Package(location, destination); - - final switch( location ){ - case InstallLocation.local: break; - case InstallLocation.projectLocal: m_projectPackages[package_name] = pack; break; - case InstallLocation.userWide: m_userPackages[package_name] ~= pack; break; - case InstallLocation.systemWide: m_systemPackages[package_name] ~= pack; break; - } - - return pack; - } - - void uninstall(in Package pack) - { - enforce(!pack.path.empty, "Cannot uninstall package "~pack.name~" without a path."); - - // remove package from package list - final switch(pack.installLocation){ - case InstallLocation.local: assert(false, "Cannot uninstall locally installed package."); - case InstallLocation.projectLocal: - auto pp = pack.name in m_projectPackages; - assert(pp !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed in project."); - assert(*pp is pack); - m_projectPackages.remove(pack.name); - break; - case InstallLocation.userWide: - auto pv = pack.name in m_systemPackages; - assert(pv !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed in user repository."); - auto idx = countUntil(*pv, pack); - assert(idx < 0 || (*pv)[idx] is pack); - if( idx >= 0 ) *pv = (*pv)[0 .. idx] ~ (*pv)[idx+1 .. $]; - break; - case InstallLocation.systemWide: - auto pv = pack.name in m_userPackages; - assert(pv !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed system repository."); - auto idx = countUntil(*pv, pack); - assert(idx < 0 || (*pv)[idx] is pack); - if( idx >= 0 ) *pv = (*pv)[0 .. idx] ~ (*pv)[idx+1 .. $]; - break; - } - - // delete package files physically - auto journalFile = pack.path~JournalJsonFilename; - if( !existsFile(journalFile) ) - throw new Exception("Uninstall failed, no journal found for '"~pack.name~"'. Please uninstall manually."); - - auto packagePath = pack.path; - auto journal = new Journal(journalFile); - logDebug("Erasing files"); - foreach( Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.RegularFile)(journal.entries)) { - logTrace("Deleting file '%s'", e.relFilename); - auto absFile = pack.path~e.relFilename; - if(!existsFile(absFile)) { - logWarn("Previously installed file not found for uninstalling: '%s'", absFile); - continue; - } - - removeFile(absFile); - } - - logDebug("Erasing directories"); - Path[] allPaths; - foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries)) - allPaths ~= pack.path~e.relFilename; - sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first - foreach(Path p; allPaths) { - logTrace("Deleting folder '%s'", p); - if( !existsFile(p) || !isDir(p.toNativeString()) || !isEmptyDir(p) ) { - logError("Alien files found, directory is not empty or is not a directory: '%s'", p); - continue; - } - rmdir(p.toNativeString()); - } - - if(!isEmptyDir(pack.path)) - throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually."); - - rmdir(pack.path.toNativeString()); - logInfo("Uninstalled package: '"~pack.name~"'"); - } - - void addLocalPackage(in Path path, in Version ver, LocalPackageType type) - { - Package[]* packs = getLocalPackageList(type); - auto info = jsonFromFile(path ~ PackageJsonFilename, false); - string name; - if( "name" !in info ) info["name"] = path.head.toString(); - info["version"] = ver.toString(); - - // don't double-add packages - foreach( p; *packs ){ - if( p.path == path ){ - enforce(p.ver == ver, "Adding local twice with different versions is not allowed."); - return; - } - } - - *packs ~= new Package(info, InstallLocation.local, path); - - writeLocalPackageList(type); - } - - void removeLocalPackage(in Path path, LocalPackageType type) - { - Package[]* packs = getLocalPackageList(type); - size_t[] to_remove; - foreach( i, entry; *packs ) - if( entry.path == path ) - to_remove ~= i; - enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); - - foreach_reverse( i; to_remove ) - *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $]; - - writeLocalPackageList(type); - } - - void refresh() - { - // rescan the system and user package folder - void scanPackageFolder(Path path, ref Package[][string] packs, InstallLocation location) - { - packs = null; - if( path.existsDirectory() ){ - logDebug("iterating dir %s", path.toNativeString()); - try foreach( pdir; iterateDirectory(path) ){ - logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); - if( !pdir.isDirectory ) continue; - Package[] vers; - auto pack_path = path ~ pdir.name; - foreach( vdir; iterateDirectory(pack_path) ){ - if( !vdir.isDirectory ) continue; - auto ver_path = pack_path ~ vdir.name; - if( !existsFile(ver_path ~ PackageJsonFilename) ) continue; - try { - auto p = new Package(location, ver_path); - vers ~= p; - } catch( Exception e ){ - logError("Failed to load package in %s: %s", ver_path, e.msg); - } - } - packs[pdir.name] = vers; - } - catch(Exception e) logDebug("Failed to enumerate %s packages: %s", location, e.toString()); - } - } - scanPackageFolder(m_systemPackagePath, m_systemPackages, InstallLocation.systemWide); - scanPackageFolder(m_userPackagePath, m_userPackages, InstallLocation.userWide); - - - // rescan the project package folder - m_projectPackages = null; - if( !m_projectPackagePath.empty && m_projectPackagePath.existsDirectory() ){ - logDebug("iterating dir %s", m_projectPackagePath.toNativeString()); - try foreach( pdir; m_projectPackagePath.iterateDirectory() ){ - if( !pdir.isDirectory ) continue; - auto pack_path = m_projectPackagePath ~ pdir.name; - if( !existsFile(pack_path ~ PackageJsonFilename) ) continue; - - try { - auto p = new Package(InstallLocation.projectLocal, pack_path); - m_projectPackages[pdir.name] = p; - } catch( Exception e ){ - logError("Failed to load package in %s: %s", pack_path, e.msg); - } - } - catch(Exception e) logDebug("Failed to enumerate project packages: %s", e.toString()); - } - - // load locally defined packages - void scanLocalPackages(Path list_path, ref Package[] packs){ - try { - logDebug("Looking for local package map at %s", list_path.toNativeString()); - if( !existsFile(list_path ~ LocalPackagesFilename) ) return; - logDebug("Try to load local package map at %s", list_path.toNativeString()); - auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename); - enforce(packlist.type == Json.Type.Array, LocalPackagesFilename~" must contain an array."); - foreach( pentry; packlist ){ - try { - auto name = pentry.name.get!string(); - auto ver = pentry["version"].get!string(); - auto path = Path(pentry.path.get!string()); - auto info = Json.EmptyObject; - if( existsFile(path ~ PackageJsonFilename) ) info = jsonFromFile(path ~ PackageJsonFilename); - if( "name" in info && info.name.get!string() != name ) - logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, info.name.get!string()); - info.name = name; - info["version"] = ver; - auto pp = new Package(info, InstallLocation.local, path); - packs ~= pp; - } catch( Exception e ){ - logWarn("Error adding local package: %s", e.msg); - } - } - } catch( Exception e ){ - logDebug("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); - } - } - scanLocalPackages(m_systemPackagePath, m_localSystemPackages); - scanLocalPackages(m_userPackagePath, m_localUserPackages); - } - - private Package[]* getLocalPackageList(LocalPackageType type) - { - final switch(type){ - case LocalPackageType.user: return &m_localUserPackages; - case LocalPackageType.system: return &m_localSystemPackages; - case LocalPackageType.temporary: return &m_localTemporaryPackages; - } - } - - private void writeLocalPackageList(LocalPackageType type) - { - Package[]* packs = getLocalPackageList(type); - Json[] newlist; - foreach( p; *packs ){ - auto entry = Json.EmptyObject; - entry["name"] = p.name; - entry["version"] = p.ver.toString(); - entry["path"] = p.path.toNativeString(); - newlist ~= entry; - } - - Path path; - final switch(type){ - case LocalPackageType.user: path = m_userPackagePath; break; - case LocalPackageType.system: path = m_systemPackagePath; break; - case LocalPackageType.temporary: return; - } - if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString()); - writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); - } -} \ No newline at end of file +/** + Management of packages on the local computer. + + Copyright: © 2012 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig, Matthias Dondorff +*/ +module dub.packagemanager; + +import dub.dependency; +import dub.installation; +import dub.package_; +import dub.utils; + +import std.algorithm : countUntil, filter, sort; +import std.conv; +import std.exception; +import std.file; +import std.string; +import std.zip; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.path; + + +enum JournalJsonFilename = "journal.json"; +enum LocalPackagesFilename = "local-packages.json"; + +enum LocalPackageType { + temporary, + user, + system +} + + +class PackageManager { + private { + Path m_systemPackagePath; + Path m_userPackagePath; + Path m_projectPackagePath; + Package[][string] m_systemPackages; + Package[][string] m_userPackages; + Package[string] m_projectPackages; + Package[] m_localTemporaryPackages; + Package[] m_localUserPackages; + Package[] m_localSystemPackages; + } + + this(Path system_package_path, Path user_package_path, Path project_package_path = Path()) + { + m_systemPackagePath = system_package_path; + m_userPackagePath = user_package_path; + m_projectPackagePath = project_package_path; + refresh(); + } + + @property Path projectPackagePath() const { return m_projectPackagePath; } + @property void projectPackagePath(Path path) { m_projectPackagePath = path; refresh(); } + + Package getPackage(string name, Version ver) + { + foreach( p; getPackageIterator(name) ) + if( p.ver == ver ) + return p; + return null; + } + + bool hasPackage(string name, string ver, InstallLocation location) + { + foreach(ep; getPackageIterator()){ + if( ep.installLocation == location && ep.name == name && ep.vers == ver ) + return true; + } + return false; + } + + Package getBestPackage(string name, string version_spec) + { + return getBestPackage(name, new Dependency(version_spec)); + } + + Package getBestPackage(string name, in Dependency version_spec) + { + Package ret; + foreach( p; getPackageIterator(name) ) + if( version_spec.matches(p.ver) && (!ret || p.ver > ret.ver) ) + ret = p; + return ret; + } + + int delegate(int delegate(ref Package)) getPackageIterator() + { + int iterator(int delegate(ref Package) del) + { + // first search project local packages + foreach( p; m_localTemporaryPackages ) + if( auto ret = del(p) ) return ret; + foreach( p; m_projectPackages ) + if( auto ret = del(p) ) return ret; + + // then local packages + foreach( p; m_localUserPackages ) + if( auto ret = del(p) ) return ret; + + // then local packages + foreach( p; m_localSystemPackages ) + if( auto ret = del(p) ) return ret; + + // then user installed packages + foreach( pl; m_userPackages ) + foreach( v; pl ) + if( auto ret = del(v) ) + return ret; + + // finally system-wide installed packages + foreach( pl; m_systemPackages ) + foreach( v; pl ) + if( auto ret = del(v) ) + return ret; + + return 0; + } + + return &iterator; + } + + int delegate(int delegate(ref Package)) getPackageIterator(string name) + { + int iterator(int delegate(ref Package) del) + { + // first search project local packages + foreach( p; m_localTemporaryPackages ) + if( p.name == name ) + if( auto ret = del(p) ) return ret; + if( auto pp = name in m_projectPackages ) + if( auto ret = del(*pp) ) return ret; + + // then local packages + foreach( p; m_localUserPackages ) + if( p.name == name ) + if( auto ret = del(p) ) return ret; + + // then local packages + foreach( p; m_localSystemPackages ) + if( p.name == name ) + if( auto ret = del(p) ) return ret; + + // then user installed packages + if( auto pp = name in m_userPackages ) + foreach( v; *pp ) + if( auto ret = del(v) ) + return ret; + + // finally system-wide installed packages + if( auto pp = name in m_systemPackages ) + foreach( v; *pp ) + if( auto ret = del(v) ) + return ret; + + return 0; + } + + return &iterator; + } + + Package install(Path zip_file_path, Json package_info, InstallLocation location) + { + auto package_name = package_info.name.get!string(); + auto package_version = package_info["version"].get!string(); + auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $]; + + logDebug("Installing package '%s' version '%s' to location '%s' from file '%s'", + package_name, package_version, to!string(location), zip_file_path.toNativeString()); + + Path destination; + final switch( location ){ + case InstallLocation.local: destination = Path(package_name); break; + case InstallLocation.projectLocal: enforce(!m_projectPackagePath.empty, "no project path set."); destination = m_projectPackagePath ~ package_name; break; + case InstallLocation.userWide: destination = m_userPackagePath ~ (package_name ~ "/" ~ clean_package_version); break; + case InstallLocation.systemWide: destination = m_systemPackagePath ~ (package_name ~ "/" ~ clean_package_version); break; + } + + if( existsFile(destination) ){ + throw new Exception(format("%s %s needs to be uninstalled prior installation.", package_name, package_version)); + } + + // open zip file + ZipArchive archive; + { + logTrace("Opening file %s", zip_file_path); + auto f = openFile(zip_file_path, FileMode.Read); + scope(exit) f.close(); + archive = new ZipArchive(f.readAll()); + } + + logTrace("Installing from zip."); + + // In a github zip, the actual contents are in a subfolder + Path zip_prefix; + foreach(ArchiveMember am; archive.directory) + if( Path(am.name).head == PathEntry(PackageJsonFilename) ){ + zip_prefix = Path(am.name)[0 .. 1]; + break; + } + + if( zip_prefix.empty ){ + // not correct zip packages HACK + Path minPath; + foreach(ArchiveMember am; archive.directory) + if( isPathFromZip(am.name) && (minPath == Path() || minPath.startsWith(Path(am.name))) ) + zip_prefix = Path(am.name); + } + + logTrace("zip root folder: %s", zip_prefix); + + Path getCleanedPath(string fileName) { + auto path = Path(fileName); + if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path(); + return path[zip_prefix.length..path.length]; + } + + // install + mkdirRecurse(destination.toNativeString()); + auto journal = new Journal; + logDebug("Copying all files..."); + int countFiles = 0; + foreach(ArchiveMember a; archive.directory) { + auto cleanedPath = getCleanedPath(a.name); + if(cleanedPath.empty) continue; + auto dst_path = destination~cleanedPath; + + logTrace("Creating %s", cleanedPath); + if( dst_path.endsWithSlash ){ + if( !existsDirectory(dst_path) ) + mkdirRecurse(dst_path.toNativeString()); + journal.add(Journal.Entry(Journal.Type.Directory, cleanedPath)); + } else { + if( !existsDirectory(dst_path.parentPath) ) + mkdirRecurse(dst_path.parentPath.toNativeString()); + auto dstFile = openFile(dst_path, FileMode.CreateTrunc); + scope(exit) dstFile.close(); + dstFile.put(archive.expand(a)); + journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath)); + ++countFiles; + } + } + logDebug("%s file(s) copied.", to!string(countFiles)); + + // overwrite package.json (this one includes a version field) + Json pi = jsonFromFile(destination~PackageJsonFilename); + pi["version"] = package_info["version"]; + writeJsonFile(destination~PackageJsonFilename, pi); + + // Write journal + logTrace("Saving installation journal..."); + journal.add(Journal.Entry(Journal.Type.RegularFile, Path(JournalJsonFilename))); + journal.save(destination ~ JournalJsonFilename); + + if( existsFile(destination~PackageJsonFilename) ) + logInfo("%s has been installed with version %s", package_name, package_version); + + auto pack = new Package(location, destination); + + final switch( location ){ + case InstallLocation.local: break; + case InstallLocation.projectLocal: m_projectPackages[package_name] = pack; break; + case InstallLocation.userWide: m_userPackages[package_name] ~= pack; break; + case InstallLocation.systemWide: m_systemPackages[package_name] ~= pack; break; + } + + return pack; + } + + void uninstall(in Package pack) + { + enforce(!pack.path.empty, "Cannot uninstall package "~pack.name~" without a path."); + + // remove package from package list + final switch(pack.installLocation){ + case InstallLocation.local: assert(false, "Cannot uninstall locally installed package."); + case InstallLocation.projectLocal: + auto pp = pack.name in m_projectPackages; + assert(pp !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed in project."); + assert(*pp is pack); + m_projectPackages.remove(pack.name); + break; + case InstallLocation.userWide: + auto pv = pack.name in m_systemPackages; + assert(pv !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed in user repository."); + auto idx = countUntil(*pv, pack); + assert(idx < 0 || (*pv)[idx] is pack); + if( idx >= 0 ) *pv = (*pv)[0 .. idx] ~ (*pv)[idx+1 .. $]; + break; + case InstallLocation.systemWide: + auto pv = pack.name in m_userPackages; + assert(pv !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed system repository."); + auto idx = countUntil(*pv, pack); + assert(idx < 0 || (*pv)[idx] is pack); + if( idx >= 0 ) *pv = (*pv)[0 .. idx] ~ (*pv)[idx+1 .. $]; + break; + } + + // delete package files physically + auto journalFile = pack.path~JournalJsonFilename; + if( !existsFile(journalFile) ) + throw new Exception("Uninstall failed, no journal found for '"~pack.name~"'. Please uninstall manually."); + + auto packagePath = pack.path; + auto journal = new Journal(journalFile); + logDebug("Erasing files"); + foreach( Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.RegularFile)(journal.entries)) { + logTrace("Deleting file '%s'", e.relFilename); + auto absFile = pack.path~e.relFilename; + if(!existsFile(absFile)) { + logWarn("Previously installed file not found for uninstalling: '%s'", absFile); + continue; + } + + removeFile(absFile); + } + + logDebug("Erasing directories"); + Path[] allPaths; + foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries)) + allPaths ~= pack.path~e.relFilename; + sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first + foreach(Path p; allPaths) { + logTrace("Deleting folder '%s'", p); + if( !existsFile(p) || !isDir(p.toNativeString()) || !isEmptyDir(p) ) { + logError("Alien files found, directory is not empty or is not a directory: '%s'", p); + continue; + } + rmdir(p.toNativeString()); + } + + if(!isEmptyDir(pack.path)) + throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually."); + + rmdir(pack.path.toNativeString()); + logInfo("Uninstalled package: '"~pack.name~"'"); + } + + void addLocalPackage(in Path path, in Version ver, LocalPackageType type) + { + Package[]* packs = getLocalPackageList(type); + auto info = jsonFromFile(path ~ PackageJsonFilename, false); + string name; + if( "name" !in info ) info["name"] = path.head.toString(); + info["version"] = ver.toString(); + + // don't double-add packages + foreach( p; *packs ){ + if( p.path == path ){ + enforce(p.ver == ver, "Adding local twice with different versions is not allowed."); + return; + } + } + + *packs ~= new Package(info, InstallLocation.local, path); + + writeLocalPackageList(type); + } + + void removeLocalPackage(in Path path, LocalPackageType type) + { + Package[]* packs = getLocalPackageList(type); + size_t[] to_remove; + foreach( i, entry; *packs ) + if( entry.path == path ) + to_remove ~= i; + enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); + + foreach_reverse( i; to_remove ) + *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $]; + + writeLocalPackageList(type); + } + + void refresh() + { + // rescan the system and user package folder + void scanPackageFolder(Path path, ref Package[][string] packs, InstallLocation location) + { + packs = null; + if( path.existsDirectory() ){ + logDebug("iterating dir %s", path.toNativeString()); + try foreach( pdir; iterateDirectory(path) ){ + logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); + if( !pdir.isDirectory ) continue; + Package[] vers; + auto pack_path = path ~ pdir.name; + foreach( vdir; iterateDirectory(pack_path) ){ + if( !vdir.isDirectory ) continue; + auto ver_path = pack_path ~ vdir.name; + if( !existsFile(ver_path ~ PackageJsonFilename) ) continue; + try { + auto p = new Package(location, ver_path); + vers ~= p; + } catch( Exception e ){ + logError("Failed to load package in %s: %s", ver_path, e.msg); + } + } + packs[pdir.name] = vers; + } + catch(Exception e) logDebug("Failed to enumerate %s packages: %s", location, e.toString()); + } + } + scanPackageFolder(m_systemPackagePath, m_systemPackages, InstallLocation.systemWide); + scanPackageFolder(m_userPackagePath, m_userPackages, InstallLocation.userWide); + + + // rescan the project package folder + m_projectPackages = null; + if( !m_projectPackagePath.empty && m_projectPackagePath.existsDirectory() ){ + logDebug("iterating dir %s", m_projectPackagePath.toNativeString()); + try foreach( pdir; m_projectPackagePath.iterateDirectory() ){ + if( !pdir.isDirectory ) continue; + auto pack_path = m_projectPackagePath ~ pdir.name; + if( !existsFile(pack_path ~ PackageJsonFilename) ) continue; + + try { + auto p = new Package(InstallLocation.projectLocal, pack_path); + m_projectPackages[pdir.name] = p; + } catch( Exception e ){ + logError("Failed to load package in %s: %s", pack_path, e.msg); + } + } + catch(Exception e) logDebug("Failed to enumerate project packages: %s", e.toString()); + } + + // load locally defined packages + void scanLocalPackages(Path list_path, ref Package[] packs){ + try { + logDebug("Looking for local package map at %s", list_path.toNativeString()); + if( !existsFile(list_path ~ LocalPackagesFilename) ) return; + logDebug("Try to load local package map at %s", list_path.toNativeString()); + auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename); + enforce(packlist.type == Json.Type.Array, LocalPackagesFilename~" must contain an array."); + foreach( pentry; packlist ){ + try { + auto name = pentry.name.get!string(); + auto ver = pentry["version"].get!string(); + auto path = Path(pentry.path.get!string()); + auto info = Json.EmptyObject; + if( existsFile(path ~ PackageJsonFilename) ) info = jsonFromFile(path ~ PackageJsonFilename); + if( "name" in info && info.name.get!string() != name ) + logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, info.name.get!string()); + info.name = name; + info["version"] = ver; + auto pp = new Package(info, InstallLocation.local, path); + packs ~= pp; + } catch( Exception e ){ + logWarn("Error adding local package: %s", e.msg); + } + } + } catch( Exception e ){ + logDebug("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); + } + } + scanLocalPackages(m_systemPackagePath, m_localSystemPackages); + scanLocalPackages(m_userPackagePath, m_localUserPackages); + } + + private Package[]* getLocalPackageList(LocalPackageType type) + { + final switch(type){ + case LocalPackageType.user: return &m_localUserPackages; + case LocalPackageType.system: return &m_localSystemPackages; + case LocalPackageType.temporary: return &m_localTemporaryPackages; + } + } + + private void writeLocalPackageList(LocalPackageType type) + { + Package[]* packs = getLocalPackageList(type); + Json[] newlist; + foreach( p; *packs ){ + auto entry = Json.EmptyObject; + entry["name"] = p.name; + entry["version"] = p.ver.toString(); + entry["path"] = p.path.toNativeString(); + newlist ~= entry; + } + + Path path; + final switch(type){ + case LocalPackageType.user: path = m_userPackagePath; break; + case LocalPackageType.system: path = m_systemPackagePath; break; + case LocalPackageType.temporary: return; + } + if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString()); + writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); + } +}