diff --git a/source/dub/dependency.d b/source/dub/dependency.d index 3d332a7..ffe6081 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -71,6 +71,7 @@ } bool opEquals(ref const Version oth) const { return v == oth.v; } + bool opEquals(const Version oth) const { return v == oth.v; } int opCmp(ref const Version other) const { @@ -79,6 +80,7 @@ return cast(int)v[i] - cast(int)other.v[i]; return cast(int)v.length - cast(int)other.v.length; } + int opCmp(in Version other) const { return opCmp(other); } string toString() const { diff --git a/source/dub/dub.d b/source/dub/dub.d index 762addd..747e395 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -12,6 +12,7 @@ import dub.utils; import dub.registry; import dub.package_; +import dub.packagemanager; import dub.packagesupplier; import vibe.core.file; @@ -42,14 +43,31 @@ Failure } - this( ActionId id, string pkg, const Dependency d, Dependency[string] issue) { - action = id; packageId = pkg; vers = new Dependency(d); issuer = issue; + immutable { + ActionId action; + string packageId; + Dependency vers; } - const ActionId action; - const string packageId; - const Dependency vers; + const Package pack; const Dependency[string] issuer; + this(ActionId id, string pkg, in Dependency d, Dependency[string] issue) + { + action = id; + packageId = pkg; + vers = new immutable(Dependency)(d); + issuer = issue; + } + + this(ActionId id, Package pkg, Dependency[string] issue) + { + pack = pkg; + action = id; + packageId = pkg.name; + vers = new immutable(Dependency)("==", pkg.vers); + issuer = issue; + } + string toString() const { return to!string(action) ~ ": " ~ packageId ~ ", " ~ to!string(vers); } @@ -59,13 +77,17 @@ private class Application { private { Path m_root; + PackageManager m_packageManager; Json m_json; Package m_main; - Package[string] m_packages; + //Package[string] m_packages; + Package[] m_dependencies; } - this(Path rootFolder) { - m_root = rootFolder; + this(PackageManager package_manager, Path project_path) + { + m_root = project_path; + m_packageManager = package_manager; m_json = Json.EmptyObject; reinit(); } @@ -76,8 +98,8 @@ return "-Unregocgnized application in '"~to!string(m_root)~"' (properly no package.json in this directory)"; string s = "-Application identifier: " ~ m_main.name; s ~= "\n" ~ m_main.info(); - s ~= "\n-Installed modules:"; - foreach(string k, p; m_packages) + s ~= "\n-Installed dependencies:"; + foreach(p; m_dependencies) s ~= "\n" ~ p.info(); return s; } @@ -85,8 +107,8 @@ /// Gets all installed packages as a "packageId" = "version" associative array string[string] installedPackages() const { string[string] pkgs; - foreach(k, p; m_packages) - pkgs[k] = p.vers; + foreach(p; m_dependencies) + pkgs[p.name] = p.vers; return pkgs; } @@ -99,29 +121,20 @@ /// Rereads the applications state. void reinit() { - m_packages.clear(); + m_dependencies = null; m_main = null; - try m_json = jsonFromFile(m_root ~ ".dub/dub.json"); - catch(Exception t) logDebug("Could not open .dub/dub.json: %s", t.msg); + try m_json = jsonFromFile(m_root ~ ".dub/dub.json", true); + catch(Exception t) logDebug("Failed to read .dub/dub.json: %s", t.msg); if(!exists(to!string(m_root~"package.json"))) { logWarn("There was no 'package.json' found for the application in '%s'.", m_root); } else { - m_main = new Package(m_root); - if(exists(to!string(m_root~".dub/modules"))) { - foreach( string pkg; dirEntries(to!string(m_root ~ ".dub/modules"), SpanMode.shallow) ) { - if( !isDir(pkg) ) continue; - try { - auto p = new Package( Path(pkg) ); - enforce( p.name !in m_packages, "Duplicate package: " ~ p.name ); - m_packages[p.name] = p; - } - catch(Throwable e) { - logWarn("The module '%s' in '%s' was not identified as a vibe package.", Path(pkg).head, pkg); - continue; - } - } + m_main = new Package(InstallLocation.Local, m_root); + foreach( name, vspec; m_main.dependencies ){ + auto p = m_packageManager.getBestPackage(name, vspec); + //enforce(p !is null, "Failed to resolve dependency "~name~" "~vspec.toString()); + if( p ) m_dependencies ~= p; } } } @@ -133,7 +146,7 @@ const { string[] ret; if( m_main ) ret = m_main.configurations; - foreach( p; m_packages ){ + foreach( p; m_dependencies ){ auto cfgs = p.configurations; foreach( c; cfgs ) if( !ret.canFind(c) ) ret ~= c; @@ -157,11 +170,10 @@ addImportPath("source", true); addImportPath("views", false); - foreach( string s, pkg; m_packages ){ - auto pack_path = ".dub/modules/"~pkg.name; - processVars(ret, pack_path, pkg.getBuildSettings(platform, config)); - addImportPath(pack_path ~ "/source", true); - addImportPath(pack_path ~ "/views", false); + foreach( pkg; m_dependencies ){ + processVars(ret, pkg.path.toNativeString(), pkg.getBuildSettings(platform, config)); + addImportPath((pkg.path ~ "source").toNativeString(), true); + addImportPath((pkg.path ~ "views").toNativeString(), false); } return ret; @@ -197,14 +209,14 @@ // Gather installed Package[string] installed; installed[m_main.name] = m_main; - foreach(string pkg, ref Package p; m_packages) { - enforce( pkg !in installed, "The package '"~pkg~"' is installed more than once." ); - installed[pkg] = p; + foreach(ref Package p; m_dependencies) { + enforce( p.name !in installed, "The package '"~p.name~"' is installed more than once." ); + installed[p.name] = p; } // To see, which could be uninstalled Package[string] unused = installed.dup; - unused.remove( m_main.name ); + unused.remove(m_main.name); // Check against installed and add install actions Action[] actions; @@ -220,7 +232,8 @@ logDebug("Required package '"~pkg~"' found with version '"~p.vers~"'"); if( option & UpdateOptions.Reinstall ) { Dependency[string] em; - uninstalls ~= Action( Action.ActionId.Uninstall, pkg, new Dependency("==", p.vers), em); + if( p.installLocation == InstallLocation.ProjectLocal ) + uninstalls ~= Action(Action.ActionId.Uninstall, *p, em); actions ~= Action(Action.ActionId.InstallUpdate, pkg, d.dependency, d.packages); } @@ -323,20 +336,13 @@ Package p = null; // Try an already installed package first - if(!needsUpToDateCheck(pkg)) { - try { - auto json = jsonFromFile( m_root ~ Path(".dub/modules") ~ Path(pkg) ~ "package.json"); - auto vers = Version(json["version"].get!string); - if( reqDep.dependency.matches( vers ) ) - p = new Package(json); - logTrace("Using already installed package with version: %s", vers); - } - catch(Throwable e) { - // not yet installed, try the supplied PS - logTrace("An installed package was not found"); - } + if( !needsUpToDateCheck(pkg) ){ + p = m_packageManager.getBestPackage(pkg, reqDep.dependency); + if( p ) logTrace("Using already installed package with version: %s", p.vers); + else logTrace("An installed package was not found"); } - if(!p) { + + if( !p ){ try { p = new Package(packageSupplier.packageJson(pkg, reqDep.dependency)); logTrace("using package from registry"); @@ -359,12 +365,10 @@ private bool needsUpToDateCheck(string packageId) { try { - auto time = m_json["dub"]["lastUpdate"][packageId].to!string; + auto time = m_json["lastUpdate"].opt!(Json[string]).get(packageId, Json("")).get!string; + if( !time.length ) return true; return (Clock.currTime() - SysTime.fromISOExtString(time)) > dur!"days"(1); - } - catch(Throwable t) { - return true; - } + } catch(Exception t) return true; } private void markUpToDate(string packageId) { @@ -419,17 +423,34 @@ class Dub { private { Path m_root; - Application m_app; PackageSupplier m_packageSupplier; + Path m_userDubPath, m_systemDubPath; + Json m_systemConfig, m_userConfig; + PackageManager m_packageManager; + Application m_app; } /// Initiales the package manager for the vibe application /// under root. - this(Path root, PackageSupplier ps = defaultPackageSupplier()) { - enforce(root.absolute, "Specify an absolute path for the VPM"); + this(Path root, PackageSupplier ps = defaultPackageSupplier()) + { + assert(root.absolute, "Need an absolute path for the DUB package."); m_root = root; + + version(Windows){ + m_systemDubPath = Path(environment.get("ProgramData")) ~ "dub/"; + m_userDubPath = Path(environment.get("APPDATA")) ~ "dub/"; + } else version(Posix){ + m_systemDubPath = Path("/etc/dub/"); + m_userDubPath = Path(environment.get("HOME")) ~ ".dub/"; + } + + m_userConfig = jsonFromFile(m_userDubPath ~ "settings.json", true); + m_systemConfig = jsonFromFile(m_systemDubPath ~ "settings.json", true); + + m_packageManager = new PackageManager(m_systemDubPath ~ "packages/", m_userDubPath ~ "packages/", root ~ ".dub/packages/"); + m_app = new Application(m_packageManager, root); m_packageSupplier = ps; - m_app = new Application(root); } /// Returns the name listed in the package.json of the current @@ -477,8 +498,10 @@ // foreach(Action a; filter!((Action a) => a.action == Action.ActionId.InstallUpdate)(actions)) // install(a.packageId, a.vers); foreach(Action a; actions) - if(a.action == Action.ActionId.Uninstall) - uninstall(a.packageId); + if(a.action == Action.ActionId.Uninstall){ + assert(a.pack !is null, "No package specified for uninstall."); + uninstall(a.pack); + } foreach(Action a; actions) if(a.action == Action.ActionId.InstallUpdate) install(a.packageId, a.vers); @@ -513,152 +536,34 @@ /// Installs the package matching the dependency into the application. /// @param addToApplication if true, this will also add an entry in the /// list of dependencies in the application's package.json - void install(string packageId, const Dependency dep, bool addToApplication = false) { - logInfo("Installing "~packageId~"..."); - auto destination = m_root ~ ".dub/modules" ~ packageId; - if(exists(to!string(destination))) - throw new Exception(packageId~" needs to be uninstalled prior installation."); + void install(string packageId, const Dependency dep, InstallLocation location = InstallLocation.ProjectLocal) + { + auto pinfo = m_packageSupplier.packageJson(packageId, dep); + string ver = pinfo["version"].get!string; - // download - ZipArchive archive; - { - logDebug("Aquiring package zip file"); - auto dload = m_root ~ ".dub/temp/downloads"; - if(!exists(to!string(dload))) - mkdirRecurse(to!string(dload)); - auto tempFile = m_root ~ (".dub/temp/downloads/"~packageId~".zip"); - string sTempFile = to!string(tempFile); - if(exists(sTempFile)) remove(sTempFile); - m_packageSupplier.storePackage(tempFile, packageId, dep); // Q: continue on fail? - scope(exit) remove(sTempFile); + logInfo("Installing %s %s...", packageId, ver); - // unpack - auto f = openFile(to!string(tempFile), FileMode.Read); - scope(exit) f.close(); - ubyte[] b = new ubyte[cast(uint)f.leastSize]; - f.read(b); - archive = new ZipArchive(b); - } + logDebug("Aquiring package zip file"); + auto dload = m_root ~ ".dub/temp/downloads"; + if(!exists(to!string(dload))) + mkdirRecurse(to!string(dload)); + auto tempFile = m_root ~ (".dub/temp/downloads/"~packageId~".zip"); + string sTempFile = to!string(tempFile); + if(exists(sTempFile)) remove(sTempFile); + m_packageSupplier.storePackage(tempFile, packageId, dep); // Q: continue on fail? + scope(exit) remove(sTempFile); - Path getPrefix(ZipArchive a) { - foreach(ArchiveMember am; a.directory) - if( Path(am.name).head == PathEntry("package.json") ) - return Path(am.name)[0 .. 1]; - - // not correct zip packages HACK - Path minPath; - foreach(ArchiveMember am; a.directory) - if( isPathFromZip(am.name) && (minPath == Path() || minPath.startsWith(Path(am.name))) ) - minPath = Path(am.name); - - return minPath; - } - - logDebug("Installing from zip."); - - // In a github zip, the actual contents are in a subfolder - auto prefixInPackage = getPrefix(archive); - logDebug("zip root folder: %s", prefixInPackage); - - Path getCleanedPath(string fileName) { - auto path = Path(fileName); - if(prefixInPackage != Path() && !path.startsWith(prefixInPackage)) return Path(); - return path[prefixInPackage.length..path.length]; - } - - // install - mkdirRecurse(to!string(destination)); - Journal journal = new Journal; - foreach(ArchiveMember a; archive.directory) { - if(!isPathFromZip(a.name)) continue; - - auto cleanedPath = getCleanedPath(a.name); - if(cleanedPath.empty) continue; - auto fileName = to!string(destination~cleanedPath); - - if( exists(fileName) && isDir(fileName) ) continue; - - logDebug("Creating %s", fileName); - mkdirRecurse(fileName); - auto subPath = cleanedPath; - for(size_t i=0; i a.type == Journal.Type.RegularFile)(journal.entries)) { - logTrace("Deleting file '%s'", e.relFilename); - auto absFile = packagePath~e.relFilename; - if(!exists(to!string(absFile))) { - logWarn("Previously installed file not found for uninstalling: '%s'", absFile); - continue; - } - - remove(to!string(absFile)); - } - - logDebug("Erasing directories"); - Path[] allPaths; - foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries)) - allPaths ~= packagePath~e.relFilename; - sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first - foreach(Path p; allPaths) { - logTrace("Deleting folder '%s'", p); - if( !exists(to!string(p)) || !isDir(to!string(p)) || !isEmptyDir(p) ) { - logError("Alien files found, directory is not empty or is not a directory: '%s'", p); - continue; - } - rmdir( to!string(p) ); - } - - if(!isEmptyDir(packagePath)) - throw new Exception("Alien files found in '"~to!string(packagePath)~"', manual uninstallation needed."); - - rmdir(to!string(packagePath)); - logInfo("Uninstalled package: '"~packageId~"'"); + m_packageManager.uninstall(pack); } } @@ -667,25 +572,24 @@ dst.addDFlags(processVars(project_path, settings.dflags)); dst.addLFlags(processVars(project_path, settings.lflags)); dst.addLibs(processVars(project_path, settings.libs)); - dst.addFiles(processVars(project_path, settings.files)); // TODO: resolve folders to a recursive search? + dst.addFiles(processVars(project_path, settings.files, true)); dst.addVersions(processVars(project_path, settings.versions)); dst.addImportDirs(processVars(project_path, settings.importPath)); // TODO: prepend project_path to relative paths here dst.addStringImportDirs(processVars(project_path, settings.stringImportPath)); // TODO: prepend project_path to relative paths here } -private string[] processVars(string project_path, string[] vars) +private string[] processVars(string project_path, string[] vars, bool are_paths = false) { auto ret = appender!(string[])(); - processVars(ret, project_path, vars); + processVars(ret, project_path, vars, are_paths); return ret.data; } -private void processVars(ref Appender!(string[]) dst, string project_path, string[] vars) +private void processVars(ref Appender!(string[]) dst, string project_path, string[] vars, bool are_paths = false) { foreach( var; vars ){ auto idx = std.string.indexOf(var, '$'); - if( idx < 0 ) dst.put(var); - else { + if( idx >= 0 ){ auto vres = appender!string(); while( idx >= 0 ){ if( idx+1 >= var.length ) break; @@ -707,8 +611,13 @@ idx = std.string.indexOf(var, '$'); } vres.put(var); - dst.put(vres.data); + var = vres.data; } + if( are_paths ){ + auto p = Path(var); + if( !p.absolute ) p = Path(project_path) ~ p; + dst.put(p.toNativeString()); + } else dst.put(var); } } diff --git a/source/dub/package_.d b/source/dub/package_.d index cd50563..87c1730 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -108,23 +108,36 @@ // } class Package { private { + InstallLocation m_location; + Path m_path; Json m_meta; Dependency[string] m_dependencies; } - this(Path root) { + this(InstallLocation location, Path root) + { + m_location = location; + m_path = root; m_meta = jsonFromFile(root ~ "package.json"); m_dependencies = .dependencies(m_meta); } - this(Json json) { + + this(Json json, InstallLocation location = InstallLocation.Local, Path root = Path()) + { + m_location = location; + m_path = root; m_meta = json; m_dependencies = .dependencies(m_meta); } @property string name() const { return cast(string)m_meta["name"]; } @property string vers() const { return cast(string)m_meta["version"]; } + @property Version ver() const { return Version(m_meta["version"].get!string); } + @property installLocation() const { return m_location; } + @property Path path() const { return m_path; } @property const(Url) url() const { return Url.parse(cast(string)m_meta["url"]); } @property const(Dependency[string]) dependencies() const { return m_dependencies; } + @property string[] configurations() const { auto pv = "configurations" in m_meta; @@ -170,3 +183,10 @@ dstFile.write( js.data ); } } + +enum InstallLocation { + Local, + ProjectLocal, + UserWide, + SystemWide +} diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d new file mode 100644 index 0000000..cdf5916 --- /dev/null +++ b/source/dub/packagemanager.d @@ -0,0 +1,369 @@ +/** + 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.exception; +import std.file; +import std.zip; +import vibe.core.file; +import vibe.core.log; +import vibe.data.json; +import vibe.inet.path; +import vibe.stream.operations; + + +enum PackageJsonFileName = "package.json"; + + +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_localPackages; + } + + this(Path system_package_path, Path user_package_path, Path project_package_path) + { + m_systemPackagePath = system_package_path; + m_userPackagePath = user_package_path; + m_projectPackagePath = project_package_path; + refresh(); + } + + Package getPackage(string name, Version ver) + { + foreach( p; getPackageIterator(name) ) + if( p.ver == ver ) + return p; + return null; + } + + 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(string name) + { + int iterator(int delegate(ref Package) del) + { + // first search project local packages + if( auto pp = name in m_projectPackages ) + if( auto ret = del(*pp) ) return ret; + + // then local packages + foreach( p; m_localPackages ) + 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(); + + Path destination; + final switch( location ){ + case InstallLocation.Local: destination = Path(package_name); break; + case InstallLocation.ProjectLocal: destination = m_projectPackagePath ~ package_name; break; + case InstallLocation.UserWide: destination = m_userPackagePath ~ (package_name ~ "/" ~ package_version); break; + case InstallLocation.SystemWide: destination = m_systemPackagePath ~ (package_name ~ "/" ~ package_version); break; + } + + if( existsFile(destination) ) + throw new Exception(package_name~" needs to be uninstalled prior installation."); + + // open zip file + ZipArchive archive; + { + auto f = openFile(zip_file_path, FileMode.Read); + scope(exit) f.close(); + archive = new ZipArchive(f.readAll()); + } + + logDebug("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("package.json") ){ + 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); + } + + logDebug("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; + foreach(ArchiveMember a; archive.directory) { + auto cleanedPath = getCleanedPath(a.name); + if(cleanedPath.empty) continue; + auto dst_path = destination~cleanedPath; + + logDebug("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.write(archive.expand(a)); + journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath)); + } + } + + { // overwrite package.json (this one includes a version field) + Json pi = jsonFromFile(destination~"package.json"); + auto pj = openFile(destination~"package.json", FileMode.CreateTrunc); + scope(exit) pj.close(); + pi["version"] = package_info["version"]; + toPrettyJson(pj, pi); + } + + // Write journal + logTrace("Saving installation journal..."); + journal.add(Journal.Entry(Journal.Type.RegularFile, Path("journal.json"))); + journal.save(destination ~ "journal.json"); + + if( existsFile(destination~"package.json") ) + 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~"journal.json"; + 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 refresh() + { + // rescan the system package folder + m_systemPackages = null; + if( m_systemPackagePath.existsDirectory() ){ + logDebug("iterating dir %s", m_systemPackagePath.toNativeString()); + try foreach( pdir; iterateDirectory(m_systemPackagePath) ){ + logDebug("iterating dir %s entry %s", m_systemPackagePath.toNativeString(), pdir.name); + if( !pdir.isDirectory ) continue; + Package[] vers; + auto pack_path = m_systemPackagePath ~ 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(InstallLocation.SystemWide, ver_path); + vers ~= p; + } catch( Exception e ){ + logError("Failed to load package in %s: %s", ver_path, e.msg); + } + } + m_systemPackages[pdir.name] = vers; + } + catch(Exception e) logDebug("Failed to enumerate system packages: %s", e.toString()); + } + + // rescan the user package folder + m_userPackages = null; + if( m_systemPackagePath.existsDirectory() ){ + logDebug("iterating dir %s", m_userPackagePath.toNativeString()); + try foreach( pdir; m_userPackagePath.iterateDirectory() ){ + if( !pdir.isDirectory ) continue; + Package[] vers; + auto pack_path = m_userPackagePath ~ pdir.name; + foreach( vdir; pack_path.iterateDirectory() ){ + if( !vdir.isDirectory ) continue; + auto ver_path = pack_path ~ vdir.name; + if( !existsFile(ver_path ~ PackageJsonFileName) ) continue; + try { + auto p = new Package(InstallLocation.UserWide, ver_path); + vers ~= p; + } catch( Exception e ){ + logError("Failed to load package in %s: %s", ver_path, e.msg); + } + } + m_userPackages[pdir.name] = vers; + } + catch(Exception e) logDebug("Failed to enumerate user packages: %s", e.toString()); + } + + // rescan the project package folder + m_projectPackages = null; + if( 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 + foreach( list_path; [m_systemPackagePath, m_userPackagePath] ){ + try { + logDebug("Looking for local package map at %s", list_path.toNativeString()); + if( !existsFile(list_path ~ "local-packages.json") ) continue; + logDebug("Try to load local package map at %s", list_path.toNativeString()); + auto packlist = jsonFromFile(list_path ~ "local-packages.json"); + enforce(packlist.type == Json.Type.Array, "local-packages.json 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 ~ "package.json") ) info = jsonFromFile(path ~ "package.json"); + 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); + m_localPackages ~= 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); + } + } + } +} \ No newline at end of file diff --git a/source/dub/utils.d b/source/dub/utils.d index 7cbab7e..63e785e 100644 --- a/source/dub/utils.d +++ b/source/dub/utils.d @@ -30,7 +30,8 @@ return true; } -package Json jsonFromFile(Path file) { +package Json jsonFromFile(Path file, bool silent_fail = false) { + if( silent_fail && !existsFile(file) ) return Json.EmptyObject; auto f = openFile(to!string(file), FileMode.Read); scope(exit) f.close(); auto text = stripUTF8Bom(cast(string)f.readAll()); @@ -51,3 +52,9 @@ enforce(p.length > 0); return p[$-1] == '/'; } + +package bool existsDirectory(Path path) { + if( !existsFile(path) ) return false; + auto fi = getFileInfo(path); + return fi.isDirectory; +} \ No newline at end of file