diff --git a/source/dub/commandline.d b/source/dub/commandline.d index 29a2acf..0d1cdff 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -310,6 +310,7 @@ BuildSettings m_buildSettings; string m_defaultConfig; bool m_nodeps; + bool m_forceRemove = false; } override void prepare(scope CommandArgs args) @@ -335,6 +336,9 @@ args.getopt("nodeps", &m_nodeps, [ "Do not check/update dependencies before building" ]); + args.getopt("force-remove", &m_forceRemove, [ + "Force deletion of fetched packages with untracked files when upgrading" + ]); } protected void setupPackage(Dub dub, string package_name) @@ -645,6 +649,7 @@ class UpgradeCommand : Command { private { bool m_prerelease = false; + bool m_forceRemove = false; } this() @@ -662,6 +667,9 @@ args.getopt("prerelease", &m_prerelease, [ "Uses the latest pre-release version, even if release versions are available" ]); + args.getopt("force-remove", &m_forceRemove, [ + "Force deletion of fetched packages with untracked files" + ]); } override int execute(Dub dub, string[] free_args, string[] app_args) @@ -672,6 +680,7 @@ logInfo("Upgrading project in %s", dub.projectPath.toNativeString()); auto options = UpdateOptions.upgrade; if (m_prerelease) options |= UpdateOptions.preRelease; + if (m_forceRemove) options |= UpdateOptions.forceRemove; dub.update(options); return 0; } @@ -682,6 +691,7 @@ string m_version; bool m_system = false; bool m_local = false; + bool m_forceRemove = false; } override void prepare(scope CommandArgs args) @@ -693,6 +703,9 @@ args.getopt("system", &m_system, ["Puts the package into the system wide package cache instead of the user local one."]); args.getopt("local", &m_system, ["Puts the package into a sub folder of the current working directory. Cannot be mixed with --system."]); + args.getopt("force-remove", &m_forceRemove, [ + "Force deletion of fetched packages with untracked files" + ]); } abstract override int execute(Dub dub, string[] free_args, string[] app_args); @@ -739,10 +752,10 @@ auto name = free_args[0]; - if (m_version.length) dub.fetch(name, Dependency(m_version), location, true, false); + if (m_version.length) dub.fetch(name, Dependency(m_version), location, true, false, m_forceRemove); else { try { - dub.fetch(name, Dependency(">=0.0.0"), location, true, false); + dub.fetch(name, Dependency(">=0.0.0"), location, true, false, m_forceRemove); logInfo( "Please note that you need to use `dub run ` " ~ "or add it to dependencies of your package to actually use/run it. " ~ @@ -751,7 +764,7 @@ catch(Exception e){ logInfo("Getting a release version failed: %s", e.msg); logInfo("Retry with ~master..."); - dub.fetch(name, Dependency("~master"), location, true, true); + dub.fetch(name, Dependency("~master"), location, true, true, m_forceRemove); } } return 0; @@ -795,7 +808,7 @@ if (m_local) location = PlacementLocation.local; else if (m_system) location = PlacementLocation.systemWide; - try dub.remove(package_id, m_version, location); + try dub.remove(package_id, m_version, location, m_forceRemove); catch { logError("Please specify a individual version or use the wildcard identifier '%s' (without quotes).", Dub.RemoveVersionWildcard); return 1; diff --git a/source/dub/dub.d b/source/dub/dub.d index 1b0e0cc..9139119 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -180,10 +180,10 @@ // Remove first foreach(Action a; actions.filter!(a => a.type == Action.Type.remove)) { assert(a.pack !is null, "No package specified for removal."); - remove(a.pack); + remove(a.pack, (options & UpdateOptions.forceRemove) != 0); } foreach(Action a; actions.filter!(a => a.type == Action.Type.fetch)) { - fetch(a.packageId, a.vers, a.location, (options & UpdateOptions.upgrade) != 0, (options & UpdateOptions.preRelease) != 0); + fetch(a.packageId, a.vers, a.location, (options & UpdateOptions.upgrade) != 0, (options & UpdateOptions.preRelease) != 0, (options & UpdateOptions.forceRemove) != 0); // never update the same package more than once masterVersionUpgrades[a.packageId] = true; } @@ -326,8 +326,9 @@ /// Returns all cached packages as a "packageId" = "version" associative array string[string] cachedPackages() const { return m_project.cachedPackagesIDs(); } + // TODO: use flags enum instead of bool parameters /// Fetches the package matching the dependency and places it in the specified location. - Package fetch(string packageId, const Dependency dep, PlacementLocation location, bool force_branch_upgrade, bool use_prerelease) + Package fetch(string packageId, const Dependency dep, PlacementLocation location, bool force_branch_upgrade, bool use_prerelease, bool force_remove) { Json pinfo; PackageSupplier supplier; @@ -360,7 +361,7 @@ return pack; } else { logInfo("Removing present package of %s %s", packageId, ver); - if (!m_dryRun) m_packageManager.remove(pack); + if (!m_dryRun) m_packageManager.remove(pack, force_remove); } } @@ -386,10 +387,10 @@ /// Removes a given package from the list of present/cached modules. /// @removeFromApplication: if true, this will also remove an entry in the /// list of dependencies in the application's package.json - void remove(in Package pack) + void remove(in Package pack, bool force_remove) { logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); - if (!m_dryRun) m_packageManager.remove(pack); + if (!m_dryRun) m_packageManager.remove(pack, force_remove); } /// @see remove(string, string, RemoveLocation) @@ -406,7 +407,7 @@ /// exception, if there are multiple versions retrieved. /// Note: as wildcard string only "*" is supported. /// @param location_ - void remove(string package_id, string version_, PlacementLocation location_) + void remove(string package_id, string version_, PlacementLocation location_, bool force_remove) { enforce(!package_id.empty); if (location_ == PlacementLocation.local) { @@ -441,7 +442,7 @@ logDebug("Removing %s packages.", packages.length); foreach(pack; packages) { try { - remove(pack); + remove(pack, force_remove); logInfo("Removing %s, version %s.", package_id, pack.vers); } catch (Exception e) { logError("Failed to remove %s %s: %s", package_id, pack.vers, e.msg); @@ -495,7 +496,7 @@ if (!ddox_pack) ddox_pack = m_packageManager.getBestPackage("ddox", "~master"); if (!ddox_pack) { logInfo("DDOX is not present, getting it and storing user wide"); - ddox_pack = fetch("ddox", Dependency(">=0.0.0"), PlacementLocation.userWide, false, false); + ddox_pack = fetch("ddox", Dependency(">=0.0.0"), PlacementLocation.userWide, false, false, false); } version(Windows) auto ddox_exe = "ddox.exe"; diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 4c6fa06..0431f2c 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -292,11 +292,61 @@ } /// Removes the given the package. - void remove(in Package pack) + void remove(in Package pack, bool force_remove) { logDebug("Remove %s, version %s, path '%s'", pack.name, pack.vers, pack.path); enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); + // delete package files physically + logDebug("Looking up journal"); + auto journalFile = pack.path~JournalJsonFilename; + if (!existsFile(journalFile)) + throw new Exception("Removal failed, no retrieval journal found for '"~pack.name~"'. Please remove the folder '%s' manually.", pack.path.toNativeString()); + + auto packagePath = pack.path; + auto journal = new Journal(journalFile); + + + // Determine all target paths/files + /*auto basebs = pack.getBuildSettings(); + foreach (conf; pack.configurations) { + auto bs = pack.getBuildSettings(conf); + auto tpath = conf.targetPath.length ? conf.targetPath : basebs.targetPath; + auto tname = conf.targetName.length ? conf.targetName : basebs.targetName; + auto ttype = conf.targetType != TargetType.auto_ ? conf.targetType : basebs.targetType; + if (ttype == TargetType.none || ttype == TargetType.auto_) continue; + foreach (n; generatePlatformNames(tname, ttype)) + // ... + }*/ + + // test if there are any untracked files + if (!force_remove) { + bool checkFilesRec(Path p) + { + auto relp = p.relativeTo(pack.path); + // ignore /.dub folder + if (relp == Path(".dub")) return true; + + // TODO: ignore target paths/files + + foreach (fi; iterateDirectory(p)) { + auto fpath = p ~ fi.name; + auto type = fi.isDirectory ? Journal.Type.Directory : Journal.Type.RegularFile; + if (fi.isDirectory) + if (!checkFilesRec(fpath)) + return false; + + if (!journal.containsEntry(type, fpath.relativeTo(pack.path))) { + enforce(force_remove, "Untracked file found, aborting package removal: " ~ fpath.toNativeString()); + logWarn("Deleting untracked files without confirmation."); + return false; + } + } + return true; + } + checkFilesRec(pack.path); + } + // remove package from repositories' list bool found = false; bool removeFrom(Package[] packs, in Package pack) { @@ -317,58 +367,8 @@ found = removeFrom(m_packages, pack); enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); - // delete package files physically - logDebug("Looking up journal"); - auto journalFile = pack.path~JournalJsonFilename; - if (!existsFile(journalFile)) - throw new Exception("Removal failed, no retrieval journal found for '"~pack.name~"'. Please remove the folder '%s' manually.", pack.path.toNativeString()); - - 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)) { - logDebug("Deleting file '%s'", e.relFilename); - auto absFile = pack.path~e.relFilename; - if(!existsFile(absFile)) { - logWarn("Previously retrieved file not found for removal: '%s'", absFile); - continue; - } - - removeFile(absFile); - } - - logDiagnostic("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) { - logDebug("Deleting folder '%s'", p); - if (!existsFile(p)) continue; - if (!isDir(p.toNativeString())) { - logError("%s expected to be a directory, skipping deletion.", p.toNativeString()); - continue; - } - if (!isEmptyDir(p)) { - logError("Found untracked files in %s, skipping deletion.", p.toNativeString()); - continue; - } - rmdir(p.toNativeString()); - } - - // Erase .dub folder, this is completely erased. - auto dubDir = (pack.path ~ ".dub/").toNativeString(); - enforce(!existsFile(dubDir) || isDir(dubDir), ".dub should be a directory, but is a file."); - if(existsFile(dubDir) && isDir(dubDir)) { - logDebug(".dub directory found, removing directory including content."); - rmdirRecurse(dubDir); - } - logDebug("About to delete root folder for package '%s'.", pack.path); - if(!isEmptyDir(pack.path)) - throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually."); - - rmdir(pack.path.toNativeString()); + rmdirRecurse(pack.path.toNativeString()); logInfo("Removed package: '"~pack.name~"'"); } @@ -708,6 +708,14 @@ scope(exit) fileJournal.close(); fileJournal.writePrettyJsonString(jsonJournal); } + + bool containsEntry(Type type, Path path) + const { + foreach (e; entries) + if (e.type == type && e.relFilename == path) + return true; + return false; + } private Json serialize() const { Json[string] files; diff --git a/source/dub/project.d b/source/dub/project.d index ea20cf0..aa67e52 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -779,7 +779,8 @@ { none = 0, upgrade = 1<<1, - preRelease = 1<<2 // inclde pre-release versions in upgrade + preRelease = 1<<2, // inclde pre-release versions in upgrade + forceRemove = 1<<3 }