/** Stuff with dependencies. Copyright: © 2012 Matthias Dondorff License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff */ module dub.dependency; import dub.utils; import dub.package_; import vibe.core.log; import vibe.core.file; import vibe.data.json; import vibe.inet.url; // todo: cleanup imports import std.array; import std.exception; import std.algorithm; import std.zip; import std.typecons; import std.conv; static import std.compiler; Dependency[string] dependencies(const Json json) { if( "dependencies" !in json ) return null; Dependency[string] dep; foreach( string pkg, ref const Json vers; json["dependencies"] ) { enforce( pkg !in dep, "The dependency '"~pkg~"' is specified more than once." ); dep[pkg] = new Dependency(cast(string)vers); } return dep; } /** A version in the format "major.update.bugfix". */ struct Version { static const Version RELEASE = Version("0.0.0"); static const Version HEAD = Version(to!string(MAX_VERS)~"."~to!string(MAX_VERS)~"."~to!string(MAX_VERS)); static const Version INVALID = Version(); static const Version MASTER = Version(MASTER_STRING); static const string MASTER_STRING = "~master"; private { static const size_t MAX_VERS = 9999; static const size_t MASTER_VERS = cast(size_t)(-1); size_t[] v; } this(string vers) { enforce( vers == MASTER_STRING || count(vers, ".") == 2); if(vers == MASTER_STRING) { v = new size_t[3]; v[0] = v[1] = v[2] = MASTER_VERS; } else { string[] tkns = split(vers, "."); v = new size_t[tkns.length]; for(size_t i=0; i<tkns.length; ++i) v[i] = to!size_t(tkns[i]); } } this(const Version o) { foreach(size_t vers; o.v) v ~= vers; } bool opEquals(ref const Version oth) const { Version o = cast(Version)oth; if(v.length != o.v.length) return false; for(size_t i=0; i<v.length; i++) if( v[i] != o.v[i]) return false; return true; } int opCmp(ref const Version other) const { //logTrace("vers_opCmp: a=%s_b=%s", this, other); enforce(v.length == other.v.length); for(size_t i=0; i<v.length; i++) if( v[i] < other.v[i] ) return -1; else if( v[i] > other.v[i] ) return 1; return 0; } string toString() const { enforce( v.length == 3 && (v[0] != MASTER_VERS || v[1] == v[2] && v[1] == MASTER_VERS) ); if(v[0] == MASTER_VERS) return MASTER_STRING; string r; for(size_t i=0; i<v.length; ++i) { if(i!=0) r ~= "."; r ~= to!string(v[i]); } return r; } } /// Representing a dependency, which is basically a version string and a /// compare methode, e.g. '>=1.0.0 <2.0.0' (i.e. a space separates the two /// version numbers) class Dependency { this( string ves ) { enforce( ves.length > 0); string orig = ves; if(ves == Version.MASTER_STRING) { m_cmpA = ">="; m_cmpB = "<="; m_versA = m_versB = Version(Version.MASTER); } else { m_cmpA = skipComp(ves); size_t idx2 = std.string.indexOf(ves, " "); if( idx2 == -1 ) { if( m_cmpA == "<=" || m_cmpA == "<" ) { m_versA = Version(Version.RELEASE); m_cmpB = m_cmpA; m_cmpA = ">="; m_versB = Version(ves); } else if( m_cmpA == ">=" || m_cmpA == ">" ) { m_versA = Version(ves); m_versB = Version(Version.HEAD); m_cmpB = "<="; } else { // Converts "==" to ">=a&&<=a", which makes merging easier m_versA = m_versB = Version(ves); m_cmpA = ">="; m_cmpB = "<="; } } else { enforce( ves[idx2+1] == ' ' ); m_versA = Version(ves[0..idx2]); string v2 = ves[idx2+2..$]; m_cmpB = skipComp(v2); m_versB = Version(v2); if( m_versB < m_versA ) { swap(m_versA, m_versB); swap(m_cmpA, m_cmpB); } enforce( m_cmpA != "==" && m_cmpB != "==", "For equality, please specify a single version."); } } } this(const Dependency o) { m_cmpA = o.m_cmpA; m_versA = Version(o.m_versA); m_cmpB = o.m_cmpB; m_versB = Version(o.m_versB); enforce( m_cmpA != "==" || m_cmpB == "=="); enforce(m_versA <= m_versB); } override string toString() const { string r; // Special "==" case if( m_versA == m_versB && m_cmpA == ">=" && m_cmpB == "<=" ) r = "==" ~ to!string(m_versA); else { if( m_versA != Version.RELEASE ) r = m_cmpA ~ to!string(m_versA); if( m_versB != Version.HEAD ) r ~= (r.length==0?"" : " ") ~ m_cmpB ~ to!string(m_versB); if( m_versA == Version.RELEASE && m_versB == Version.HEAD ) r = ">=0.0.0"; } return r; } override bool opEquals(Object b) { if (this is b) return true; if (b is null) return false; if (typeid(this) != typeid(b)) return false; Dependency o = cast(Dependency) b; return o.m_cmpA == m_cmpA && o.m_cmpB == m_cmpB && o.m_versA == m_versA && o.m_versB == m_versB; } bool valid() const { return m_versA == m_versB // compare not important || (m_versA < m_versB && doCmp(m_cmpA, m_versB, m_versA) && doCmp(m_cmpB, m_versA, m_versB)); } bool matches(const string vers) const { return matches(Version(vers)); } bool matches(ref const(Version) v) const { //logTrace(" try match: %s with: %s", v, this); // Master only matches master if(m_versA == Version.MASTER || v == Version.MASTER) return m_versA == v; if( !doCmp(m_cmpA, v, m_versA) ) return false; if( !doCmp(m_cmpB, v, m_versB) ) return false; return true; } /// Merges to versions Dependency merge(ref const(Dependency) o) const { if(!valid()) return new Dependency(this); if(!o.valid()) return new Dependency(o); Version a = m_versA > o.m_versA? Version(m_versA) : Version(o.m_versA); Version b = m_versB < o.m_versB? Version(m_versB) : Version(o.m_versB); //logTrace(" this : %s", this); //logTrace(" other: %s", o); Dependency d = new Dependency(this); d.m_cmpA = !doCmp(m_cmpA, a,a)? m_cmpA : o.m_cmpA; d.m_versA = a; d.m_cmpB = !doCmp(m_cmpB, b,b)? m_cmpB : o.m_cmpB; d.m_versB = b; //logTrace(" merged: %s", d); return d; } private static bool isDigit(char ch) { return ch >= '0' && ch <= '9'; } private static string skipComp(ref string c) { size_t idx = 0; while( idx < c.length && !isDigit(c[idx]) ) idx++; enforce( idx < c.length ); string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx]; c = c[idx..$]; switch(cmp) { default: enforce(false, "No/Unknown comparision specified: '"~cmp~"'"); return ">="; case ">=": goto case; case ">": goto case; case "<=": goto case; case "<": goto case; case "==": return cmp; } } private static bool doCmp(string mthd, ref const Version a, ref const Version b) { enforce( mthd==">=" || mthd==">" || mthd=="<=" || mthd=="<"); //logTrace("Calling %s%s%s", a, mthd, b); switch(mthd) { case ">=": return a>=b; case ">": return a>b; case "<=": return a<=b; case "<": return a<b; default: enforce(false); return false; } } private { string m_cmpA; Version m_versA; string m_cmpB; Version m_versB; } } unittest { Dependency a = new Dependency(">=1.1.0"), b = new Dependency(">=1.3.0"); assert( a.merge(b).valid() && to!string(a.merge(b)) == ">=1.3.0", to!string(a.merge(b)) ); a = new Dependency("<=1.0.0 >=2.0.0"); assert( !a.valid(), to!string(a) ); a = new Dependency(">=1.0.0 <=5.0.0"), b = new Dependency(">=2.0.0"); assert( a.merge(b).valid() && to!string(a.merge(b)) == ">=2.0.0 <=5.0.0", to!string(a.merge(b)) ); try { a = new Dependency(">1.0.0 ==5.0.0"); assert( false, "Construction is invalid"); } catch( Exception ) {} a = new Dependency(">1.0.0"), b = new Dependency("<2.0.0"); assert( a.merge(b).valid(), to!string(a.merge(b))); assert( to!string(a.merge(b)) == ">1.0.0 <2.0.0", to!string(a.merge(b)) ); a = new Dependency(">2.0.0"), b = new Dependency("<1.0.0"); assert( !(a.merge(b)).valid(), to!string(a.merge(b))); a = new Dependency(">=2.0.0"), b = new Dependency("<=1.0.0"); assert( !(a.merge(b)).valid(), to!string(a.merge(b))); a = new Dependency("==2.0.0"), b = new Dependency("==1.0.0"); assert( !(a.merge(b)).valid(), to!string(a.merge(b))); a = new Dependency("<=2.0.0"), b = new Dependency("==1.0.0"); Dependency m = a.merge(b); assert( m.valid(), to!string(m)); assert( m.matches( Version("1.0.0") ) ); assert( !m.matches( Version("1.1.0") ) ); assert( !m.matches( Version("0.0.1") ) ); } struct RequestedDependency { this( string pkg, const Dependency de) { dependency = new Dependency(de); packages[pkg] = new Dependency(de); } Dependency dependency; Dependency[string] packages; } class DependencyGraph { this(const Package root) { m_root = root; m_packages[m_root.name] = root; } void insert(const Package p) { enforce(p.name != m_root.name); m_packages[p.name] = p; } void remove(const Package p) { enforce(p.name != m_root.name); Rebindable!(const Package)* pkg = p.name in m_packages; if( pkg ) m_packages.remove(p.name); } private { alias Rebindable!(const Package) PkgType; } void clearUnused() { Rebindable!(const Package)[string] unused = m_packages.dup; unused.remove(m_root.name); forAllDependencies( (const PkgType* avail, string s, const Dependency d, const Package issuer) { if(avail && d.matches(avail.vers)) unused.remove(avail.name); }); foreach(string unusedPkg, d; unused) { logTrace("Removed unused package: "~unusedPkg); m_packages.remove(unusedPkg); } } RequestedDependency[string] conflicted() const { RequestedDependency[string] deps = needed(); RequestedDependency[string] conflicts; foreach(string pkg, d; deps) if(!d.dependency.valid()) conflicts[pkg] = d; return conflicts; } RequestedDependency[string] missing() const { RequestedDependency[string] deps; forAllDependencies( (const PkgType* avail, string pkgId, const Dependency d, const Package issuer) { if(!avail || !d.matches(avail.vers)) addDependency(deps, pkgId, d, issuer); }); return deps; } RequestedDependency[string] needed() const { RequestedDependency[string] deps; forAllDependencies( (const PkgType* avail, string pkgId, const Dependency d, const Package issuer) { addDependency(deps, pkgId, d, issuer); }); return deps; } private void forAllDependencies(void delegate (const PkgType* avail, string pkgId, const Dependency d, const Package issuer) dg) const { foreach(string issuerPackag, issuer; m_packages) { foreach(string depPkg, dependency; issuer.dependencies) { auto availPkg = depPkg in m_packages; dg(availPkg, depPkg, dependency, issuer); } } } private static void addDependency(ref RequestedDependency[string] deps, string packageId, const Dependency d, const Package issuer) { logTrace("addDependency "~packageId~", '%s'", d); auto d2 = packageId in deps; if(!d2) { deps[packageId] = RequestedDependency(issuer.name, d); } else { d2.dependency = d2.dependency.merge(d); d2.packages[issuer.name] = new Dependency(d); } } private { const Package m_root; PkgType[string] m_packages; } }