diff --git a/source/dub/dub.d b/source/dub/dub.d index 75f268b6c..709e828d7 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -11,8 +11,8 @@ import dub.compilers.compiler; import dub.data.settings : SPS = SkipPackageSuppliers, Settings; import dub.dependency; import dub.dependencyresolver; +import dub.internal.io.realfs; import dub.internal.utils; -import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.url; import dub.internal.logging; @@ -28,7 +28,7 @@ import std.array : array, replace; import std.conv : text, to; import std.encoding : sanitize; import std.exception : enforce; -import std.file; +import std.file : tempDir, thisExePath; import std.process : environment; import std.range : assumeSorted, empty; import std.string; @@ -118,6 +118,7 @@ deprecated unittest */ class Dub { protected { + Filesystem fs; bool m_dryRun = false; PackageManager m_packageManager; PackageSupplier[] m_packageSuppliers; @@ -155,8 +156,15 @@ class Dub { this(string root_path = ".", PackageSupplier[] base = null, SkipPackageSuppliers skip = SkipPackageSuppliers.none) { + this(new RealFS(), root_path, base, skip); + } + + package this (Filesystem fs, string root_path, PackageSupplier[] base = null, + SkipPackageSuppliers skip = SkipPackageSuppliers.none) + { + this.fs = fs; m_rootPath = NativePath(root_path); - if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath; + if (!m_rootPath.absolute) m_rootPath = fs.getcwd() ~ m_rootPath; init(); @@ -201,9 +209,10 @@ class Dub { { // Note: We're doing `init()` before setting the `rootPath`, // to prevent `init` from reading the project's settings. + this.fs = new RealFS(); init(); this.m_rootPath = root; - m_packageManager = new PackageManager(pkg_root); + m_packageManager = new PackageManager(pkg_root, this.fs); } deprecated("Use the overload that takes `(NativePath pkg_root, NativePath root)`") @@ -222,12 +231,15 @@ class Dub { */ protected PackageManager makePackageManager() { - return new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false); + const local = this.m_rootPath ~ ".dub/packages/"; + const user = m_dirs.userPackages ~ "packages/"; + const system = m_dirs.systemSettings ~ "packages/"; + return new PackageManager(this.fs, local, user, system); } protected void init() { - this.m_dirs = SpecialDirs.make(); + this.m_dirs = SpecialDirs.make(this.fs); this.m_config = this.loadConfig(this.m_dirs); this.m_defaultCompiler = this.determineDefaultCompiler(); } @@ -253,13 +265,13 @@ class Dub { { import dub.internal.configy.Read; - static void readSettingsFile (NativePath path_, ref Settings current) + static void readSettingsFile (in Filesystem fs, NativePath path_, ref Settings current) { // TODO: Remove `StrictMode.Warn` after v1.40 release // The default is to error, but as the previous parser wasn't // complaining, we should first warn the user. const path = path_.toNativeString(); - if (path.exists) { + if (fs.existsFile(path_)) { auto newConf = parseConfigFileSimple!Settings(path, StrictMode.Warn); if (!newConf.isNull()) current = current.merge(newConf.get()); @@ -289,11 +301,11 @@ class Dub { } } - readSettingsFile(dirs.systemSettings ~ "settings.json", result); - readSettingsFile(dubFolderPath ~ "../etc/dub/settings.json", result); + readSettingsFile(this.fs, dirs.systemSettings ~ "settings.json", result); + readSettingsFile(this.fs, dubFolderPath ~ "../etc/dub/settings.json", result); version (Posix) { if (dubFolderPath.absolute && dubFolderPath.startsWith(NativePath("usr"))) - readSettingsFile(NativePath("/etc/dub/settings.json"), result); + readSettingsFile(this.fs, NativePath("/etc/dub/settings.json"), result); } // Override user + local package path from system / binary settings @@ -307,11 +319,11 @@ class Dub { } // load user config: - readSettingsFile(dirs.userSettings ~ "settings.json", result); + readSettingsFile(this.fs, dirs.userSettings ~ "settings.json", result); // load per-package config: if (!this.m_rootPath.empty) - readSettingsFile(this.m_rootPath ~ "dub.settings.json", result); + readSettingsFile(this.fs, this.m_rootPath ~ "dub.settings.json", result); // same as userSettings above, but taking into account the // config loaded from user settings and per-package config as well. @@ -444,7 +456,7 @@ class Dub { @property void rootPath(NativePath root_path) { m_rootPath = root_path; - if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath; + if (!m_rootPath.absolute) m_rootPath = this.fs.getcwd() ~ m_rootPath; } /// Returns the name listed in the dub.json of the current @@ -559,12 +571,11 @@ class Dub { void loadSingleFilePackage(NativePath path) { import dub.recipe.io : parsePackageRecipe; - import std.file : readText; import std.path : baseName, stripExtension; path = makeAbsolute(path); - string file_content = readText(path.toNativeString()); + string file_content = this.fs.readText(path); if (file_content.startsWith("#!")) { auto idx = file_content.indexOf('\n'); @@ -827,9 +838,9 @@ class Dub { } } - string configFilePath = (m_project.rootPackage.path ~ "dscanner.ini").toNativeString(); - if (!args.canFind("--config") && exists(configFilePath)) { - settings.runArgs ~= ["--config", configFilePath]; + const configFilePath = (m_project.rootPackage.path ~ "dscanner.ini"); + if (!args.canFind("--config") && this.fs.existsFile(configFilePath)) { + settings.runArgs ~= ["--config", configFilePath.toNativeString()]; } settings.runArgs ~= args ~ [m_project.rootPackage.path.toNativeString()]; @@ -880,8 +891,7 @@ class Dub { const cache = this.m_dirs.cache; logInfo("Cleaning", Color.green, "all artifacts at %s", cache.toNativeString().color(Mode.bold)); - if (existsFile(cache)) - rmdirRecurse(cache.toNativeString()); + this.fs.removeDir(cache, true); } /// Ditto @@ -891,10 +901,8 @@ class Dub { logInfo("Cleaning", Color.green, "artifacts for package %s at %s", pack.name.color(Mode.bold), cache.toNativeString().color(Mode.bold)); - // TODO: clear target files and copy files - if (existsFile(cache)) - rmdirRecurse(cache.toNativeString()); + this.fs.removeDir(cache, true); } deprecated("Use the overload that accepts either a `Version` or a `VersionRange` as second argument") @@ -1464,7 +1472,7 @@ class Dub { } writePackageRecipe(srcfile.parentPath ~ ("dub."~destination_file_ext), m_project.rootPackage.rawRecipe); - removeFile(srcfile); + this.fs.removeFile(srcfile); } /** Runs DDOX to generate or serve documentation. @@ -1594,7 +1602,6 @@ class Dub { */ protected string determineDefaultCompiler() const { - import std.file : thisExePath; import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator; import std.range : front; @@ -1624,12 +1631,13 @@ class Dub { if (result.length) { string compilerPath = buildPath(thisExePath().dirName(), result ~ exe); - if (existsFile(compilerPath)) + if (this.fs.existsFile(NativePath(compilerPath))) return compilerPath; } else { - auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe))); + auto nextFound = compilers.find!( + bin => this.fs.existsFile(NativePath(buildPath(thisExePath().dirName(), bin ~ exe)))); if (!nextFound.empty) return buildPath(thisExePath().dirName(), nextFound.front ~ exe); } @@ -1637,10 +1645,10 @@ class Dub { // If nothing found next to dub, search the user's PATH, starting // with the compiler name from their DUB config file, if specified. auto paths = environment.get("PATH", "").splitter(sep).map!NativePath; - if (result.length && paths.canFind!(p => existsFile(p ~ (result ~ exe)))) + if (result.length && paths.canFind!(p => this.fs.existsFile(p ~ (result ~ exe)))) return result; foreach (p; paths) { - auto res = compilers.find!(bin => existsFile(p ~ (bin~exe))); + auto res = compilers.find!(bin => this.fs.existsFile(p ~ (bin~exe))); if (!res.empty) return res.front; } @@ -1654,7 +1662,7 @@ class Dub { import dub.test.base : TestDub; auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); - immutable testdir = getWorkingDirectory() ~ "test-determineDefaultCompiler"; + immutable testdir = dub.fs.getcwd() ~ "test-determineDefaultCompiler"; immutable olddc = environment.get("DC", null); immutable oldpath = environment.get("PATH", null); @@ -1667,19 +1675,18 @@ class Dub { } scope (exit) repairenv("DC", olddc); scope (exit) repairenv("PATH", oldpath); - scope (exit) std.file.rmdirRecurse(testdir.toNativeString()); version (Windows) enum sep = ";", exe = ".exe"; version (Posix) enum sep = ":", exe = ""; immutable dmdpath = testdir ~ "dmd" ~ "bin"; immutable ldcpath = testdir ~ "ldc" ~ "bin"; - ensureDirectory(dmdpath); - ensureDirectory(ldcpath); + dub.fs.mkdir(dmdpath); + dub.fs.mkdir(ldcpath); immutable dmdbin = dmdpath ~ ("dmd" ~ exe); immutable ldcbin = ldcpath ~ ("ldc2" ~ exe); - writeFile(dmdbin, null); - writeFile(ldcbin, null); + dub.fs.writeFile(dmdbin, "dmd"); + dub.fs.writeFile(ldcbin, "ldc"); environment["DC"] = dmdbin.toNativeString(); assert(dub.determineDefaultCompiler() == dmdbin.toNativeString()); @@ -2053,9 +2060,14 @@ package struct SpecialDirs { NativePath cache; /// Returns: An instance of `SpecialDirs` initialized from the environment + deprecated("Use the overload that accepts a `Filesystem`") public static SpecialDirs make () { - import std.file : tempDir; + scope fs = new RealFS(); + return SpecialDirs.make(fs); + } + /// Ditto + public static SpecialDirs make (scope Filesystem fs) { SpecialDirs result; result.temp = NativePath(tempDir); @@ -2069,7 +2081,7 @@ package struct SpecialDirs { result.systemSettings = NativePath("/var/lib/dub/"); result.userSettings = NativePath(environment.get("HOME")) ~ ".dub/"; if (!result.userSettings.absolute) - result.userSettings = getWorkingDirectory() ~ result.userSettings; + result.userSettings = fs.getcwd() ~ result.userSettings; result.userPackages = result.userSettings; } result.cache = result.userPackages ~ "cache"; diff --git a/source/dub/internal/io/filesystem.d b/source/dub/internal/io/filesystem.d index 414005551..d1373fadc 100644 --- a/source/dub/internal/io/filesystem.d +++ b/source/dub/internal/io/filesystem.d @@ -26,6 +26,9 @@ public interface Filesystem /// Returns: The `path` of this FSEntry public abstract NativePath getcwd () const scope; + /// Change current directory to `path`. Equivalent to `cd` in shell. + public abstract void chdir (in NativePath path) scope; + /** * Implements `mkdir -p`: Create a directory and every intermediary * diff --git a/source/dub/internal/io/mockfs.d b/source/dub/internal/io/mockfs.d index 20954d2db..390b381da 100644 --- a/source/dub/internal/io/mockfs.d +++ b/source/dub/internal/io/mockfs.d @@ -23,10 +23,29 @@ public final class MockFS : Filesystem { /// private FSEntry root; - /// - public this () scope - { - this.root = this.cwd = new FSEntry(); + /*************************************************************************** + + Instantiate a `MockFS` with a given root + + A parameter-less overload exists for POSIX, while on Windows a parameter + needs to be provided, as Windows' root has a drive letter. + + Params: + root = The name of the root, e.g. "C:\" + + ***************************************************************************/ + + version (Windows) { + public this (char dir = 'C') scope + { + this.root = this.cwd = new FSEntry(); + this.root.name = [ dir, ':' ]; + } + } else { + public this () scope + { + this.root = this.cwd = new FSEntry(); + } } public override NativePath getcwd () const scope @@ -34,6 +53,14 @@ public final class MockFS : Filesystem { return this.cwd.path(); } + public override void chdir (in NativePath path) scope + { + auto tmp = this.lookup(path); + enforce(tmp !is null, "No such directory: " ~ path.toNativeString()); + enforce(tmp.isDirectory(), "Cannot chdir into non-directory: " ~ path.toNativeString()); + this.cwd = tmp; + } + /// public override bool existsDirectory (in NativePath path) const scope { @@ -350,8 +377,10 @@ public class FSEntry protected inout(FSEntry) lookup(string name) inout return scope { assert(!name.canFind('/')); - if (name == ".") return this; - if (name == "..") return this.parent; + version (POSIX) { + if (name == ".") return this; + if (name == "..") return this.parent; + } foreach (c; this.children) if (c.name == name) return c; @@ -428,7 +457,8 @@ public class FSEntry public NativePath path () const scope { if (this.parent is null) - return NativePath("/"); + // The first runtime branch is for Windows, the second for POSIX + return this.name ? NativePath(this.name) : NativePath("/"); auto thisPath = this.parent.path ~ this.name; thisPath.endsWithSlash = (this.attributes.type == Type.Directory); return thisPath; @@ -476,3 +506,42 @@ public class FSEntry this.attributes.attrs = attributes; } } + +unittest { + alias P = NativePath; + scope fs = new MockFS(); + + version (Windows) immutable NativePath root = NativePath(`C:\`); + else immutable NativePath root = NativePath(`/`); + + assert(fs.getcwd == root); + // We shouldn't be able to chdir into a non-existent directory + assertThrown(fs.chdir(P("foo/bar"))); + // Even with an absolute path + assertThrown(fs.chdir(root ~ "foo/bar")); + // Now we should be + fs.mkdir(P("foo/bar")); + fs.chdir(P("foo/bar")); + assert(fs.getcwd == root ~ "foo/bar/"); + // chdir with absolute path + import std.stdio; + writeln("===== ROOT ====="); + fs.root.print(); + writeln("===== CWD ====="); + fs.cwd.print(); + fs.chdir(root ~ "foo"); + assert(fs.getcwd == root ~ "foo/"); + // This still does not exists + assertThrown(fs.chdir(root ~ "bar")); + // Test pseudo entries / meta locations + version (POSIX) { + fs.chdir(P(".")); + assert(fs.getcwd == P("/foo/")); + fs.chdir(P("..")); + assert(fs.getcwd == P("/")); + fs.chdir(P(".")); + assert(fs.getcwd == P("/")); + fs.chdir(NativePath("/foo/bar/../")); + assert(fs.getcwd == P("/foo/")); + } +} diff --git a/source/dub/internal/io/realfs.d b/source/dub/internal/io/realfs.d index 9497169c0..2f67f0ce3 100644 --- a/source/dub/internal/io/realfs.d +++ b/source/dub/internal/io/realfs.d @@ -28,6 +28,12 @@ public final class RealFS : Filesystem { return this.path_; } + /// + public override void chdir (in NativePath path) scope + { + std.file.chdir(path.toNativeString()); + } + /// protected override bool existsDirectory (in NativePath path) const scope { diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 79029cc4b..7ec0df178 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -130,14 +130,15 @@ class PackageManager { Params: path = Path of the single repository */ - this(NativePath path) + this(NativePath path, Filesystem fs = null) { import dub.internal.io.realfs; - this.fs = new RealFS(); + this.fs = fs !is null ? fs : new RealFS(); this.m_internal.searchPath = [ path ]; this.refresh(); } + deprecated("Use the overload that accepts a `Filesystem`") this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true) { import dub.internal.io.realfs; @@ -1002,8 +1003,7 @@ symlink_exit: enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); logDebug("About to delete root folder for package '%s'.", pack.path); - import std.file : rmdirRecurse; - rmdirRecurse(pack.path.toNativeString()); + this.fs.removeDir(pack.path, true); logInfo("Removed", Color.yellow, "%s %s", pack.name.color(Mode.bold), pack.version_); } diff --git a/source/dub/test/base.d b/source/dub/test/base.d index 859ba2e79..4f08a24cb 100644 --- a/source/dub/test/base.d +++ b/source/dub/test/base.d @@ -175,9 +175,6 @@ public void disableLogging() */ public class TestDub : Dub { - /// The virtual filesystem that this instance acts on - public MockFS fs; - /** * Redundant reference to the registry * @@ -190,7 +187,7 @@ public class TestDub : Dub /// Convenience constants for use in unittests version (Windows) - public static immutable Root = NativePath("T:\\dub\\"); + public static immutable Root = NativePath(`C:\dub\`); else public static immutable Root = NativePath("/dub/"); @@ -235,8 +232,9 @@ public class TestDub : Dub fs_.mkdir(Paths.userPackages); fs_.mkdir(Paths.cache); fs_.mkdir(ProjectPath); + fs_.chdir(Root); if (dg !is null) dg(fs_); - this(fs_, root, extras, skip); + super(fs_, root, extras, skip); } /// Workaround https://issues.dlang.org/show_bug.cgi?id=24388 when called @@ -251,11 +249,10 @@ public class TestDub : Dub } /// Internal constructor - private this(MockFS fs_, string root, PackageSupplier[] extras, + private this(Filesystem fs_, string root, PackageSupplier[] extras, SkipPackageSuppliers skip) { - this.fs = fs_; - super(root, extras, skip); + super(fs_, root, extras, skip); } /*************************************************************************** @@ -333,6 +330,14 @@ public class TestDub : Dub assert(this.registry !is null, "The registry hasn't been instantiated?"); return this.registry; } + + /** + * Exposes our test filesystem to unittests + */ + public @property inout(Filesystem) fs() inout + { + return super.fs; + } } /**