diff --git a/bin/dpm b/bin/dpm new file mode 100644 index 0000000..eaa43ca --- /dev/null +++ b/bin/dpm @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +# delete old vpm.d if another run left it in /tmp +rm -f /tmp/vpm.d + +# find the executable location (note: must stay mac compatible here) +VIBEBINARY=$(readlink "$0" || true) +if [ ! -n "$VIBEBINARY" ]; then VIBEBINARY="$0"; fi +VIBEPATH=$(dirname "$VIBEBINARY") + +# use pkg-config if possible or fallback to default flags +LIBS=$(pkg-config --libs libevent libevent_pthreads libssl 2>/dev/null || echo "-levent_pthreads -levent -lssl -lcrypto") +LIBS=$(echo "$LIBS" | sed 's/^-L/-L-L/; s/ -L/ -L-L/g; s/^-l/-L-l/; s/ -l/ -L-l/g') +export LIBS + +# generate a file name for the temporary compile/run script +START_SCRIPT=`mktemp -t vpm.start.XXXXXXXX` + +# copy vpm.d to /tmp and make it deletable by anyone +cp -p "$VIBEPATH"/vpm.d /tmp/vpm.d +chmod 666 /tmp/vpm.d + +# run VPM and delete the vpm.d file again, VPM will output the compile/run script +rdmd -g -w -property -I"$VIBEPATH"/../source $LIBS -Jviews -Isource /tmp/vpm.d "$VIBEPATH" "$START_SCRIPT" $1 $2 $3 $4 $5 $6 $7 $8 $9 +rm /tmp/vpm.d + +# compile/run the application +chmod +x "$START_SCRIPT" +"$START_SCRIPT" +rm "$START_SCRIPT" diff --git a/bin/dpm.cmd b/bin/dpm.cmd new file mode 100644 index 0000000..3e7809d --- /dev/null +++ b/bin/dpm.cmd @@ -0,0 +1,20 @@ +@echo off +set VIBE_BIN=%~dps0 +set LIBDIR=%VIBE_BIN%..\lib\win-i386 +set BINDIR=%VIBE_BIN%..\lib\bin +set LIBS="%LIBDIR%\event2.lib" "%LIBDIR%\eay.lib" "%LIBDIR%\ssl.lib" ws2_32.lib +set EXEDIR=%TEMP%\.rdmd\source +set START_SCRIPT=%EXEDIR%\vibe.cmd + +if NOT EXIST %EXEDIR% ( + mkdir %EXEDIR% +) +copy "%VIBE_BIN%*.dll" %EXEDIR% > nul 2>&1 +if "%1" == "build" copy "%VIBE_BIN%*.dll" . > nul 2>&1 +copy "%VIBE_BIN%vpm.d" %EXEDIR% > nul 2>&1 + +rem Run, execute, do everything.. but when you do it, do it with the vibe! +rdmd -debug -g -w -property -of%EXEDIR%\vpm.exe -I%VIBE_BIN%..\source %LIBS% %EXEDIR%\vpm.d %VIBE_BIN% %START_SCRIPT% %* + +rem Finally, start the app, if vpm succeded. +if ERRORLEVEL 0 %START_SCRIPT% diff --git a/bin/libeay32.dll b/bin/libeay32.dll new file mode 100644 index 0000000..696b300 --- /dev/null +++ b/bin/libeay32.dll Binary files differ diff --git a/bin/libevent.dll b/bin/libevent.dll new file mode 100644 index 0000000..d2b35db --- /dev/null +++ b/bin/libevent.dll Binary files differ diff --git a/bin/ssleay32.dll b/bin/ssleay32.dll new file mode 100644 index 0000000..c0d6d1f --- /dev/null +++ b/bin/ssleay32.dll Binary files differ diff --git a/bin/vpm.d b/bin/vpm.d new file mode 100644 index 0000000..7bf45c6 --- /dev/null +++ b/bin/vpm.d @@ -0,0 +1,298 @@ +/** + The entry point to vibe.d + + 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 vpm; + +import vibe.core.file; +import vibe.core.log; +import vibe.inet.url; +import vibe.vpm.vpm; +import vibe.vpm.registry; +import vibe.utils.string; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.file; +import std.getopt; +import std.process; + + +int main(string[] args) +{ + string cmd; + + try { + if( args.length < 3 ){ + logError("Usage: %s [] [args...] [-- [applicatio args]]\n", args[0]); + // vibe-binary-path: the installation folder of the vibe installation + // start-script-output-file: destination of the script, which can be used to run the app + return 1; + } + + // parse general options + bool verbose, vverbose, quiet, vquiet; + bool help, nodeps, annotate; + LogLevel loglevel = LogLevel.Info; + getopt(args, + "v|verbose", &verbose, + "vverbose", &vverbose, + "q|quiet", &quiet, + "vquiet", &vquiet, + "h|help", &help, + "nodeps", &nodeps, + "annotate", &annotate + ); + + if( vverbose ) loglevel = LogLevel.Trace; + else if( verbose ) loglevel = LogLevel.Debug; + else if( vquiet ) loglevel = LogLevel.None; + else if( quiet ) loglevel = LogLevel.Warn; + setLogLevel(loglevel); + if( loglevel >= LogLevel.Info ) setPlainLogging(true); + + + // extract the destination paths + enforce(isDir(args[1]), "Specified binary path is not a directory."); + Path vibedDir = Path(args[1]); + Path dstScript = Path(args[2]); + + // extract the command + if( args.length > 3 && !args[3].startsWith("-") ){ + cmd = args[3]; + args = args[0] ~ args[4 .. $]; + } else { + cmd = "run"; + args = args[0] ~ args[3 .. $]; + } + + // contrary to the documentation, getopt does not remove -- + if( args.length >= 2 && args[1] == "--" ) args = args[0] ~ args[2 .. $]; + + // display help if requested + if( help ){ + showHelp(cmd); + return 0; + } + + auto appPath = getcwd(); + string appStartScript; + Url registryUrl = Url.parse("http://registry.vibed.org/"); + logDebug("Using vpm registry url '%s'", registryUrl); + + // handle the command + switch( cmd ){ + default: + enforce(false, "Command is unknown."); + assert(false); + case "init": + string dir = "."; + if( args.length >= 2 ) dir = args[1]; + initDirectory(dir); + break; + case "run": + case "build": + Vpm vpm = new Vpm(Path(appPath), new RegistryPS(registryUrl)); + if( !nodeps ){ + logInfo("Checking dependencies in '%s'", appPath); + logDebug("vpm initialized"); + vpm.update(annotate ? UpdateOptions.JustAnnotate : UpdateOptions.None); + } + + //Added check for existance of [AppNameInPackagejson].d + //If exists, use that as the starting file. + string binName = getBinName(vpm); + version(Windows) { string appName = binName[0..$-4]; } + version(Posix) { string appName = binName; } + + logDebug("Application Name is '%s'", binName); + + // Create start script, which will be used by the calling bash/cmd script. + // build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments + // or with "/" instead of "\" + string[] flags = ["--force"]; + if( cmd == "build" ){ + flags ~= "--build-only"; + flags ~= "-of"~binName; + } + flags ~= "-g"; + flags ~= "-I" ~ (vibedDir ~ ".." ~ "source").toNativeString(); + flags ~= "-Isource"; + flags ~= "-Jviews"; + flags ~= vpm.dflags; + flags ~= getLibs(vibedDir); + flags ~= getPackagesAsVersion(vpm); + flags ~= (Path("source") ~ appName).toNativeString(); + flags ~= args[1 .. $]; + + appStartScript = "rdmd " ~ getDflags() ~ " " ~ join(flags, " "); + break; + case "upgrade": + logInfo("Upgrading application in '%s'", appPath); + Vpm vpm = new Vpm(Path(appPath), new RegistryPS(registryUrl)); + logDebug("vpm initialized"); + vpm.update(UpdateOptions.Reinstall | (annotate ? UpdateOptions.JustAnnotate : UpdateOptions.None)); + break; + } + + auto script = openFile(to!string(dstScript), FileMode.CreateTrunc); + scope(exit) script.close(); + script.write(appStartScript); + + return 0; + } + catch(Throwable e) + { + logError("Error executing command '%s': %s\n", cmd, e.msg); + logDebug("Full exception: %s", sanitizeUTF8(cast(ubyte[])e.toString())); + showHelp(cmd); + return -1; + } +} + + +private void showHelp(string command) +{ + // This help is actually a mixup of help for this application and the + // supporting vibe script / .cmd file. + logInfo( +"Usage: vibe [] [] [-- ] + +Manages the vibe.d application in the current directory. A single -- can be used +to separate vibe options from options passed to the application. + +Possible commands: + init [] Initializes an empy project in the specified directory + run Compiles and runs the application + build Just compiles the application in the project directory + upgrade Forces an upgrade of all dependencies + +Options: + -v --verbose Also output debug messages + --vverbose Also output trace messages (produces a lot of output) + -q --quiet Only output warnings and errors + --vquiet No output + -h --help Print this help screen + --nodeps Do not check dependencies for 'run' or 'build' + --annotate Do not execute dependency installations, just print +"); +} + + +private string getDflags() +{ + auto globVibedDflags = environment.get("DFLAGS"); + if(globVibedDflags == null) + globVibedDflags = "-debug -g -w -property"; + return globVibedDflags; +} + +private string[] getLibs(Path vibedDir) +{ + version(Windows) + { + auto libDir = vibedDir ~ "..\\lib\\win-i386"; + return ["ws2_32.lib", + (libDir ~ "event2.lib").toNativeString(), + (libDir ~ "eay.lib").toNativeString(), + (libDir ~ "ssl.lib").toNativeString()]; + } + version(Posix) + { + return split(environment.get("LIBS", "-L-levent_openssl -L-levent")); + } +} + +private string stripDlangSpecialChars(string s) +{ + char[] ret = s.dup; + for(int i=0; i [] [args...] [-- [applicatio args]]\n", args[0]); + // vibe-binary-path: the installation folder of the vibe installation + // start-script-output-file: destination of the script, which can be used to run the app + return 1; + } + + // parse general options + bool verbose, vverbose, quiet, vquiet; + bool help, nodeps, annotate; + LogLevel loglevel = LogLevel.Info; + getopt(args, + "v|verbose", &verbose, + "vverbose", &vverbose, + "q|quiet", &quiet, + "vquiet", &vquiet, + "h|help", &help, + "nodeps", &nodeps, + "annotate", &annotate + ); + + if( vverbose ) loglevel = LogLevel.Trace; + else if( verbose ) loglevel = LogLevel.Debug; + else if( vquiet ) loglevel = LogLevel.None; + else if( quiet ) loglevel = LogLevel.Warn; + setLogLevel(loglevel); + if( loglevel >= LogLevel.Info ) setPlainLogging(true); + + + // extract the destination paths + enforce(isDir(args[1]), "Specified binary path is not a directory."); + Path vibedDir = Path(args[1]); + Path dstScript = Path(args[2]); + + // extract the command + if( args.length > 3 && !args[3].startsWith("-") ){ + cmd = args[3]; + args = args[0] ~ args[4 .. $]; + } else { + cmd = "run"; + args = args[0] ~ args[3 .. $]; + } + + // contrary to the documentation, getopt does not remove -- + if( args.length >= 2 && args[1] == "--" ) args = args[0] ~ args[2 .. $]; + + // display help if requested + if( help ){ + showHelp(cmd); + return 0; + } + + auto appPath = getcwd(); + string appStartScript; + Url registryUrl = Url.parse("http://registry.vibed.org/"); + logDebug("Using vpm registry url '%s'", registryUrl); + + // handle the command + switch( cmd ){ + default: + enforce(false, "Command is unknown."); + assert(false); + case "init": + string dir = "."; + if( args.length >= 2 ) dir = args[1]; + initDirectory(dir); + break; + case "run": + case "build": + Vpm vpm = new Vpm(Path(appPath), new RegistryPS(registryUrl)); + if( !nodeps ){ + logInfo("Checking dependencies in '%s'", appPath); + logDebug("vpm initialized"); + vpm.update(annotate ? UpdateOptions.JustAnnotate : UpdateOptions.None); + } + + //Added check for existance of [AppNameInPackagejson].d + //If exists, use that as the starting file. + string binName = getBinName(vpm); + version(Windows) { string appName = binName[0..$-4]; } + version(Posix) { string appName = binName; } + + logDebug("Application Name is '%s'", binName); + + // Create start script, which will be used by the calling bash/cmd script. + // build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments + // or with "/" instead of "\" + string[] flags = ["--force"]; + if( cmd == "build" ){ + flags ~= "--build-only"; + flags ~= "-of"~binName; + } + flags ~= "-g"; + flags ~= "-I" ~ (vibedDir ~ ".." ~ "source").toNativeString(); + flags ~= "-Isource"; + flags ~= "-Jviews"; + flags ~= vpm.dflags; + flags ~= getLibs(vibedDir); + flags ~= getPackagesAsVersion(vpm); + flags ~= (Path("source") ~ appName).toNativeString(); + flags ~= args[1 .. $]; + + appStartScript = "rdmd " ~ getDflags() ~ " " ~ join(flags, " "); + break; + case "upgrade": + logInfo("Upgrading application in '%s'", appPath); + Vpm vpm = new Vpm(Path(appPath), new RegistryPS(registryUrl)); + logDebug("vpm initialized"); + vpm.update(UpdateOptions.Reinstall | (annotate ? UpdateOptions.JustAnnotate : UpdateOptions.None)); + break; + } + + auto script = openFile(to!string(dstScript), FileMode.CreateTrunc); + scope(exit) script.close(); + script.write(appStartScript); + + return 0; + } + catch(Throwable e) + { + logError("Error executing command '%s': %s\n", cmd, e.msg); + logDebug("Full exception: %s", sanitizeUTF8(cast(ubyte[])e.toString())); + showHelp(cmd); + return -1; + } +} + + +private void showHelp(string command) +{ + // This help is actually a mixup of help for this application and the + // supporting vibe script / .cmd file. + logInfo( +"Usage: vibe [] [] [-- ] + +Manages the vibe.d application in the current directory. A single -- can be used +to separate vibe options from options passed to the application. + +Possible commands: + init [] Initializes an empy project in the specified directory + run Compiles and runs the application + build Just compiles the application in the project directory + upgrade Forces an upgrade of all dependencies + +Options: + -v --verbose Also output debug messages + --vverbose Also output trace messages (produces a lot of output) + -q --quiet Only output warnings and errors + --vquiet No output + -h --help Print this help screen + --nodeps Do not check dependencies for 'run' or 'build' + --annotate Do not execute dependency installations, just print +"); +} + + +private string getDflags() +{ + auto globVibedDflags = environment.get("DFLAGS"); + if(globVibedDflags == null) + globVibedDflags = "-debug -g -w -property"; + return globVibedDflags; +} + +private string[] getLibs(Path vibedDir) +{ + version(Windows) + { + auto libDir = vibedDir ~ "..\\lib\\win-i386"; + return ["ws2_32.lib", + (libDir ~ "event2.lib").toNativeString(), + (libDir ~ "eay.lib").toNativeString(), + (libDir ~ "ssl.lib").toNativeString()]; + } + version(Posix) + { + return split(environment.get("LIBS", "-L-levent_openssl -L-levent")); + } +} + +private string stripDlangSpecialChars(string s) +{ + char[] ret = s.dup; + for(int i=0; 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=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=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; + } +} \ No newline at end of file diff --git a/source/dub/generators/monod.d b/source/dub/generators/monod.d new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/source/dub/generators/monod.d diff --git a/source/dub/generators/visuald.d b/source/dub/generators/visuald.d new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/source/dub/generators/visuald.d diff --git a/source/dub/installation.d b/source/dub/installation.d new file mode 100644 index 0000000..524c40f --- /dev/null +++ b/source/dub/installation.d Binary files differ diff --git a/source/dub/package_.d b/source/dub/package_.d new file mode 100644 index 0000000..2a15078 --- /dev/null +++ b/source/dub/package_.d @@ -0,0 +1,85 @@ +/** + 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.package_; + +import dub.dependency; +import dub.utils; + +import std.array; +import std.conv; +import vibe.core.file; +import vibe.data.json; +import vibe.inet.url; + + +/// Representing an installed package +// Json file example: +// { +// "name": "MetalCollection", +// "author": "VariousArtists", +// "version": "1.0.0", +// "url": "https://github.org/...", +// "keywords": "a,b,c", +// "category": "music.best", +// "dependencies": { +// "black-sabbath": ">=1.0.0", +// "CowboysFromHell": "<1.0.0", +// "BeneathTheRemains": ">=1.0.3" +// } +// "licenses": { +// ... +// } +// } +class Package { + private { + Json m_meta; + Dependency[string] m_dependencies; + } + + this(Path root) { + m_meta = jsonFromFile(root ~ "package.json"); + m_dependencies = .dependencies(m_meta); + } + this(Json json) { + 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 const(Url) url() const { return Url.parse(cast(string)m_meta["url"]); } + @property const(Dependency[string]) dependencies() const { return m_dependencies; } + @property string[] dflags() const { + if( "dflags" !in m_meta ) return null; + auto flags = m_meta["dflags"].get!(Json[]); + auto ret = appender!(string[])(); + foreach( f; flags ) ret.put(f.get!string); + return ret.data; + } + + string info() const { + string s; + s ~= cast(string)m_meta["name"] ~ ", version '" ~ cast(string)m_meta["version"] ~ "'"; + s ~= "\n Dependencies:"; + foreach(string p, ref const Dependency v; m_dependencies) + s ~= "\n " ~ p ~ ", version '" ~ to!string(v) ~ "'"; + return s; + } + + /// direct access to the json of this package + @property ref Json json() { return m_meta; } + + /// Writes the json file back to the filesystem + void writeJson(Path path) { + auto dstFile = openFile((path~"package.json").toString(), FileMode.CreateTrunc); + scope(exit) dstFile.close(); + Appender!string js; + toPrettyJson(js, m_meta); + dstFile.write( js.data ); + } +} diff --git a/source/dub/packagesupplier.d b/source/dub/packagesupplier.d new file mode 100644 index 0000000..358debf --- /dev/null +++ b/source/dub/packagesupplier.d @@ -0,0 +1,73 @@ +/** + A package manager. + + 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.packagesupplier; + +import dub.utils; +import dub.dependency; + +import std.file; +import std.exception; +import std.zip; +import std.conv; + +import vibe.core.log; +import vibe.core.file; +import vibe.data.json; +import vibe.inet.url; +import vibe.inet.urltransfer; + +/// Supplies packages, this is done by supplying the latest possible version +/// which is available. +interface PackageSupplier { + /// path: absolute path to store the package (usually in a zip format) + void storePackage(const Path path, const string packageId, const Dependency dep); + + /// returns the metadata for the package + Json packageJson(const string packageId, const Dependency dep); +} + +class FSPackageSupplier : PackageSupplier { + private { Path m_path; } + this(Path root) { m_path = root; } + + void storePackage(const Path path, const string packageId, const Dependency dep) { + enforce(path.absolute); + logInfo("Storing package '"~packageId~"', version requirements: %s", dep); + auto filename = bestPackageFile(packageId, dep); + enforce( exists(to!string(filename)) ); + copy(to!string(filename), to!string(path)); + } + + Json packageJson(const string packageId, const Dependency dep) { + auto filename = bestPackageFile(packageId, dep); + return jsonFromZip(to!string(filename), "package.json"); + } + + private Path bestPackageFile( const string packageId, const Dependency dep) const { + Version bestVersion = Version(Version.RELEASE); + foreach(DirEntry d; dirEntries(to!string(m_path), packageId~"*", SpanMode.shallow)) { + Path p = Path(d.name); + logTrace("Entry: %s", p); + enforce(to!string(p.head)[$-4..$] == ".zip"); + string vers = to!string(p.head)[packageId.length+1..$-4]; + logTrace("Version string: "~vers); + Version v = Version(vers); + if(v > bestVersion && dep.matches(v) ) { + bestVersion = v; + } + } + + auto fileName = m_path ~ (packageId ~ "_" ~ to!string(bestVersion) ~ ".zip"); + + if(bestVersion == Version.RELEASE || !exists(to!string(fileName))) + throw new Exception("No matching package found"); + + logDebug("Found best matching package: '%s'", fileName); + return fileName; + } +} \ No newline at end of file diff --git a/source/dub/registry.d b/source/dub/registry.d new file mode 100644 index 0000000..3c28201 --- /dev/null +++ b/source/dub/registry.d Binary files differ diff --git a/source/dub/utils.d b/source/dub/utils.d new file mode 100644 index 0000000..dcd6964 --- /dev/null +++ b/source/dub/utils.d @@ -0,0 +1,52 @@ +/** + ... + + 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.utils; + +import vibe.core.file; +import vibe.core.log; +import vibe.data.json; +import vibe.inet.url; +import vibe.utils.string; + +// todo: cleanup imports. +import std.array; +import std.file; +import std.exception; +import std.algorithm; +import std.zip; +import std.typecons; +import std.conv; + + +package bool isEmptyDir(Path p) { + foreach(DirEntry e; dirEntries(to!string(p), SpanMode.shallow)) + return false; + return true; +} + +package Json jsonFromFile(Path file) { + auto f = openFile(to!string(file), FileMode.Read); + scope(exit) f.close(); + auto text = stripUTF8Bom(cast(string)f.readAll()); + return parseJson(text); +} + +package Json jsonFromZip(string zip, string filename) { + auto f = openFile(zip, FileMode.Read); + ubyte[] b = new ubyte[cast(uint)f.leastSize]; + f.read(b); + f.close(); + auto archive = new ZipArchive(b); + auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename])); + return parseJson(text); +} + +package bool isPathFromZip(string p) { + enforce(p.length > 0); + return p[$-1] == '/'; +} diff --git a/source/dub/vpm.d b/source/dub/vpm.d new file mode 100644 index 0000000..bdbca46 --- /dev/null +++ b/source/dub/vpm.d @@ -0,0 +1,631 @@ +/** + A package manager. + + 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.vpm; + +import dub.dependency; +import dub.installation; +import dub.utils; +import dub.registry; +import dub.package_; +import dub.packagesupplier; + +import vibe.core.file; +import vibe.core.log; +import vibe.data.json; +import vibe.inet.url; + +// todo: cleanup imports. +import std.algorithm; +import std.array; +import std.conv; +import std.datetime; +import std.exception; +import std.file; +import std.path; +import std.string; +import std.typecons; +import std.zip; + +/// Actions to be performed by the vpm +private struct Action { + enum ActionId { + InstallUpdate, + Uninstall, + Conflict, + Failure + } + + this( ActionId id, string pkg, const Dependency d, Dependency[string] issue) { + action = id; packageId = pkg; vers = new Dependency(d); issuer = issue; + } + const ActionId action; + const string packageId; + const Dependency vers; + const Dependency[string] issuer; + + string toString() const { + return to!string(action) ~ ": " ~ packageId ~ ", " ~ to!string(vers); + } +} + +/// During check to build task list, which can then be executed. +private class Application { + private { + Path m_root; + Json m_json; + Package m_main; + Package[string] m_packages; + } + + this(Path rootFolder) { + m_root = rootFolder; + m_json = Json.EmptyObject; + reinit(); + } + + /// Gathers information + string info() const { + if(!m_main) + 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" ~ p.info(); + return s; + } + + /// 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; + return pkgs; + } + + /// Writes the application's metadata to the package.json file + /// in it's root folder. + void writeMetadata() const { + assert(false); + // TODO + } + + /// Rereads the applications state. + void reinit() { + m_packages.clear(); + m_main = null; + + try m_json = jsonFromFile(m_root ~ "vpm.json"); + catch(Exception t) logDebug("Could not open vpm.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~"modules"))) { + foreach( string pkg; dirEntries(to!string(m_root ~ "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; + } + } + } + } + } + + /// Returns the applications name. + @property string name() const { return m_main ? m_main.name : "app"; } + + /// Returns the DFLAGS + @property string[] dflags() const { + auto ret = appender!(string[])(); + if( m_main ) ret.put(m_main.dflags()); + ret.put("-Isource"); + ret.put("-Jviews"); + foreach( string s, pkg; m_packages ){ + void addPath(string prefix, string name){ + auto path = "modules/"~pkg.name~"/"~name; + if( exists(path) ) + ret.put(prefix ~ path); + } + ret.put(pkg.dflags()); + addPath("-I", "source"); + addPath("-J", "views"); + } + return ret.data(); + } + + /// Actions which can be performed to update the application. + Action[] actions(PackageSupplier packageSupplier, int option) { + scope(exit) writeVpmJson(); + + if(!m_main) { + Action[] a; + return a; + } + + auto graph = new DependencyGraph(m_main); + if(!gatherMissingDependencies(packageSupplier, graph) || graph.missing().length > 0) { + logError("The dependency graph could not be filled."); + Action[] actions; + foreach( string pkg, rdp; graph.missing()) + actions ~= Action(Action.ActionId.Failure, pkg, rdp.dependency, rdp.packages); + return actions; + } + + auto conflicts = graph.conflicted(); + if(conflicts.length > 0) { + logDebug("Conflicts found"); + Action[] actions; + foreach( string pkg, dbp; conflicts) + actions ~= Action(Action.ActionId.Conflict, pkg, dbp.dependency, dbp.packages); + return actions; + } + + // 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; + } + + // To see, which could be uninstalled + Package[string] unused = installed.dup; + unused.remove( m_main.name ); + + // Check against installed and add install actions + Action[] actions; + Action[] uninstalls; + foreach( string pkg, d; graph.needed() ) { + auto p = pkg in installed; + // TODO: auto update to latest head revision + if(!p || (!d.dependency.matches(p.vers) && !d.dependency.matches(Version.MASTER))) { + if(!p) logDebug("Application not complete, required package '"~pkg~"', which was not found."); + else logDebug("Application not complete, required package '"~pkg~"', invalid version. Required '%s', available '%s'.", d.dependency, p.vers); + actions ~= Action(Action.ActionId.InstallUpdate, pkg, d.dependency, d.packages); + } else { + 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); + actions ~= Action(Action.ActionId.InstallUpdate, pkg, d.dependency, d.packages); + } + + if( (pkg in unused) !is null ) + unused.remove(pkg); + } + } + + // Add uninstall actions + foreach( string pkg, p; unused ) { + logDebug("Superfluous package found: '"~pkg~"', version '"~p.vers~"'"); + Dependency[string] em; + uninstalls ~= Action( Action.ActionId.Uninstall, pkg, new Dependency("==" ~ p.vers), em); + } + + // Ugly "uninstall" comes first + actions = uninstalls ~ actions; + + return actions; + } + + void createZip(string destination) { + assert(false); // not properly implemented + /* + string[] ignores; + auto ignoreFile = to!string(m_root~"vpm.ignore.txt"); + if(exists(ignoreFile)){ + auto iFile = openFile(ignoreFile); + scope(exit) iFile.close(); + while(!iFile.empty) + ignores ~= to!string(cast(char[])iFile.readLine()); + logDebug("Using '%s' found by the application.", ignoreFile); + } + else { + ignores ~= ".svn/*"; + ignores ~= ".git/*"; + ignores ~= ".hg/*"; + logDebug("The '%s' file was not found, defaulting to ignore:", ignoreFile); + } + ignores ~= "modules/*"; // modules will not be included + foreach(string i; ignores) + logDebug(" " ~ i); + + logDebug("Creating zip file from application: " ~ m_main.name); + auto archive = new ZipArchive(); + foreach( string file; dirEntries(to!string(m_root), SpanMode.depth) ) { + enforce( Path(file).startsWith(m_root) ); + auto p = Path(file); + p = p[m_root.length..p.length]; + if(isDir(file)) continue; + foreach(string ignore; ignores) + if(globMatch(file, ignore)) + would work, as I see it; + continue; + logDebug(" Adding member: %s", p); + ArchiveMember am = new ArchiveMember(); + am.name = to!string(p); + auto f = openFile(file); + scope(exit) f.close(); + am.expandedData = f.readAll(); + archive.addMember(am); + } + + logDebug(" Writing zip: %s", destination); + auto dst = openFile(destination, FileMode.CreateTrunc); + scope(exit) dst.close(); + dst.write(cast(ubyte[])archive.build()); + */ + } + + private bool gatherMissingDependencies(PackageSupplier packageSupplier, DependencyGraph graph) { + RequestedDependency[string] missing = graph.missing(); + RequestedDependency[string] oldMissing; + while( missing.length > 0 ) { + if(missing.length == oldMissing.length) { + bool different = false; + foreach(string pkg, reqDep; missing) { + auto o = pkg in oldMissing; + if(o && reqDep.dependency != o.dependency) { + different = true; + break; + } + } + if(!different) { + logWarn("Could not resolve dependencies"); + return false; + } + } + + oldMissing = missing.dup; + logTrace("There are %s packages missing.", missing.length); + foreach(string pkg, reqDep; missing) { + if(!reqDep.dependency.valid()) { + logTrace("Dependency to "~pkg~" is invalid. Trying to fix by modifying others."); + continue; + } + + // TODO: auto update and update interval by time + logTrace("Adding package to graph: "~pkg); + Package p = null; + + // Try an already installed package first + if(!needsUpToDateCheck(pkg)) { + try { + auto json = jsonFromFile( m_root ~ Path("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(!p) { + try { + p = new Package(packageSupplier.packageJson(pkg, reqDep.dependency)); + logTrace("using package from registry"); + markUpToDate(pkg); + } + catch(Throwable e) { + logError("Geting package metadata for %s failed, exception: %s", pkg, e.toString()); + } + } + + if(p) + graph.insert(p); + } + graph.clearUnused(); + missing = graph.missing(); + } + + return true; + } + + private bool needsUpToDateCheck(string packageId) { + try { + auto time = m_json["vpm"]["lastUpdate"][packageId].to!string; + return (Clock.currTime() - SysTime.fromISOExtString(time)) > dur!"days"(1); + } + catch(Throwable t) { + return true; + } + } + + private void markUpToDate(string packageId) { + logTrace("markUpToDate(%s)", packageId); + Json create(ref Json json, string object) { + if( object !in json ) json[object] = Json.EmptyObject; + return json[object]; + } + create(m_json, "vpm"); + create(m_json["vpm"], "lastUpdate"); + m_json["vpm"]["lastUpdate"][packageId] = Json( Clock.currTime().toISOExtString() ); + + writeVpmJson(); + } + + private void writeVpmJson() { + // don't bother to write an empty file + if( m_json.length == 0 ) return; + + try { + logTrace("writeVpmJson"); + auto dstFile = openFile((m_root~"vpm.json").toString(), FileMode.CreateTrunc); + scope(exit) dstFile.close(); + Appender!string js; + toPrettyJson(js, m_json); + dstFile.write( js.data ); + } catch( Exception e ){ + logWarn("Could not write vpm.json."); + } + } +} + +/// The default supplier for packages, which is the registry +/// hosted by vibed.org. +PackageSupplier defaultPackageSupplier() { + Url url = Url.parse("http://registry.vibed.org/"); + logDebug("Using the registry from %s", url); + return new RegistryPS(url); +} + +enum UpdateOptions +{ + None = 0, + JustAnnotate = 1<<0, + Reinstall = 1<<1 +}; + +/// The Vpm or Vibe Package Manager helps in getting the applications +/// dependencies up and running. +class Vpm { + private { + Path m_root; + Application m_app; + PackageSupplier m_packageSupplier; + } + + /// 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"); + m_root = root; + m_packageSupplier = ps; + m_app = new Application(root); + } + + /// Returns the name listed in the package.json of the current + /// application. + @property string packageName() const { return m_app.name; } + + /// Returns a list of flags which the application needs to be compiled + /// properly. + @property string[] dflags() { return m_app.dflags; } + + /// Lists all installed modules + void list() { + logInfo(m_app.info()); + } + + /// Performs installation and uninstallation as necessary for + /// the application. + /// @param options bit combination of UpdateOptions + bool update(UpdateOptions options) { + Action[] actions = m_app.actions(m_packageSupplier, options); + if( actions.length == 0 ) return true; + + logInfo("The following changes could be performed:"); + bool conflictedOrFailed = false; + foreach(Action a; actions) { + logInfo(capitalize( to!string( a.action ) ) ~ ": " ~ a.packageId ~ ", version %s", a.vers); + if( a.action == Action.ActionId.Conflict || a.action == Action.ActionId.Failure ) { + logInfo("Issued by: "); + conflictedOrFailed = true; + foreach(string pkg, d; a.issuer) + logInfo(" "~pkg~": %s", d); + } + } + + if( conflictedOrFailed || options & UpdateOptions.JustAnnotate ) + return conflictedOrFailed; + + // Uninstall first + + // ?? + // foreach(Action a ; filter!((Action a) => a.action == Action.ActionId.Uninstall)(actions)) + // uninstall(a.packageId); + // 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); + foreach(Action a; actions) + if(a.action == Action.ActionId.InstallUpdate) + install(a.packageId, a.vers); + + m_app.reinit(); + Action[] newActions = m_app.actions(m_packageSupplier, 0); + if(newActions.length > 0) { + logInfo("There are still some actions to perform:"); + foreach(Action a; newActions) + logInfo("%s", a); + } + else + logInfo("You are up to date"); + + return newActions.length == 0; + } + + /// Creates a zip from the application. + void createZip(string zipFile) { + m_app.createZip(zipFile); + } + + /// Prints some information to the log. + void info() { + logInfo("Status for %s", m_root); + logInfo("\n" ~ m_app.info()); + } + + /// Gets all installed packages as a "packageId" = "version" associative array + string[string] installedPackages() const { return m_app.installedPackages(); } + + /// 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 ~ "modules" ~ packageId; + if(exists(to!string(destination))) + throw new Exception(packageId~" needs to be uninstalled prior installation."); + + // download + ZipArchive archive; + { + logDebug("Aquiring package zip file"); + auto dload = m_root ~ "temp/downloads"; + if(!exists(to!string(dload))) + mkdirRecurse(to!string(dload)); + auto tempFile = m_root ~ ("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); + + // 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); + } + + Path getPrefix(ZipArchive a) { + foreach(ArchiveMember am; a.directory) + if( Path(am.name).head == PathEntry("package.json") ) + return Path(am.name).parentPath; + + // 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); + + 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~"'"); + } +}