diff --git a/build-files.txt b/build-files.txt index 3a9ec14d1..c9f011a27 100644 --- a/build-files.txt +++ b/build-files.txt @@ -47,6 +47,8 @@ source/dub/internal/dyaml/style.d source/dub/internal/dyaml/tagdirective.d source/dub/internal/dyaml/token.d source/dub/internal/git.d +source/dub/internal/io/filesystem.d +source/dub/internal/io/realfs.d source/dub/internal/libInputVisitor.d source/dub/internal/sdlang/ast.d source/dub/internal/sdlang/exception.d diff --git a/source/dub/dub.d b/source/dub/dub.d index d0ef808ff..75f268b6c 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -376,15 +376,15 @@ class Dub { import dub.test.base : TestDub; scope (exit) environment.remove("DUB_REGISTRY"); - auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); + auto dub = new TestDub(null, "/dub/project/", null, SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 0); environment["DUB_REGISTRY"] = "http://example.com/"; - dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); + dub = new TestDub(null, "/dub/project/", null, SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 1); environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; - dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); + dub = new TestDub(null, "/dub/project/", null, SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 2); - dub = new TestDub(null, ".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); + dub = new TestDub(null, "/dub/project/", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 3); dub = new TestDub(); diff --git a/source/dub/internal/io/filesystem.d b/source/dub/internal/io/filesystem.d new file mode 100644 index 000000000..414005551 --- /dev/null +++ b/source/dub/internal/io/filesystem.d @@ -0,0 +1,96 @@ +/** + * An abstract filesystem representation + * + * This interface allows to represent the file system to various part of Dub. + * Instead of direct use of `std.file`, an implementation of this interface can + * be used, allowing to mock all I/O in unittest on a thread-local basis. + */ +module dub.internal.io.filesystem; + +public import std.datetime.systime; + +public import dub.internal.vibecompat.inet.path; + +/// Ditto +public interface Filesystem +{ + static import dub.internal.vibecompat.core.file; + + /// TODO: Remove, the API should be improved + public alias IterateDirDg = int delegate( + scope int delegate(ref dub.internal.vibecompat.core.file.FileInfo)); + + /// Ditto + public IterateDirDg iterateDirectory (in NativePath path) scope; + + /// Returns: The `path` of this FSEntry + public abstract NativePath getcwd () const scope; + + /** + * Implements `mkdir -p`: Create a directory and every intermediary + * + * There is no way to error out on intermediate directory, + * like standard mkdir does. If you want this behavior, + * simply check (`existsDirectory`) if the parent directory exists. + * + * Params: + * path = The path of the directory to be created. + */ + public abstract void mkdir (in NativePath path) scope; + + /// Checks the existence of a file + public abstract bool existsFile (in NativePath path) const scope; + + /// Checks the existence of a directory + public abstract bool existsDirectory (in NativePath path) const scope; + + /// Reads a file, returns the content as `ubyte[]` + public abstract ubyte[] readFile (in NativePath path) const scope; + + /// Reads a file, returns the content as text + public abstract string readText (in NativePath path) const scope; + + /// Write to this file + public final void writeFile (in NativePath path, const(char)[] data) scope + { + import std.string : representation; + + this.writeFile(path, data.representation); + } + + /// Ditto + public abstract void writeFile (in NativePath path, const(ubyte)[] data) scope; + + /** Remove a file + * + * Always error if the target is a directory. + * Does not error if the target does not exists + * and `force` is set to `true`. + * + * Params: + * path = Path to the file to remove + * force = Whether to ignore non-existing file, + * default to `false`. + */ + public void removeFile (in NativePath path, bool force = false); + + /** Remove a directory + * + * Remove an existing empty directory. + * If `force` is set to `true`, no error will be thrown + * if the directory is empty or non-existing. + * + * Params: + * path = Path to the directory to remove + * force = Whether to ignore non-existing / non-empty directories, + * default to `false`. + */ + public void removeDir (in NativePath path, bool force = false); + + /// Implement `std.file.setTimes` + public void setTimes (in NativePath path, in SysTime accessTime, + in SysTime modificationTime); + + /// Implement `std.file.setAttributes` + public void setAttributes (in NativePath path, uint attributes); +} diff --git a/source/dub/internal/io/mockfs.d b/source/dub/internal/io/mockfs.d new file mode 100644 index 000000000..4c0f3d660 --- /dev/null +++ b/source/dub/internal/io/mockfs.d @@ -0,0 +1,491 @@ +/******************************************************************************* + + An unittest implementation of `Filesystem` + +*******************************************************************************/ + +module dub.internal.io.mockfs; + +public import dub.internal.io.filesystem; + +static import dub.internal.vibecompat.core.file; + +import std.algorithm; +import std.exception; +import std.range; +import std.string; + +/// Ditto +public final class MockFS : Filesystem { + /// + private FSEntry cwd; + + /// + public this () scope + { + this.cwd = new FSEntry(); + } + + public override NativePath getcwd () const scope + { + return this.cwd.path(); + } + + /// + public override bool existsDirectory (in NativePath path) const scope + { + auto entry = this.cwd.lookup(path); + return entry !is null && entry.isDirectory(); + } + + /// Ditto + public override void mkdir (in NativePath path) scope + { + this.cwd.mkdir(path); + } + + /// Ditto + public override bool existsFile (in NativePath path) const scope + { + auto entry = this.cwd.lookup(path); + return entry !is null && entry.isFile(); + } + + /// Ditto + public override void writeFile (in NativePath path, const(ubyte)[] data) + scope + { + return this.cwd.writeFile(path, data); + } + + /// Reads a file, returns the content as `ubyte[]` + public override ubyte[] readFile (in NativePath path) const scope + { + return this.cwd.readFile(path); + } + + /// Ditto + public override string readText (in NativePath path) const scope + { + return this.cwd.readText(path); + } + + /// Ditto + public override IterateDirDg iterateDirectory (in NativePath path) scope + { + enforce(this.existsDirectory(path), + path.toNativeString() ~ " does not exists or is not a directory"); + auto dir = this.cwd.lookup(path); + int iterator(scope int delegate(ref dub.internal.vibecompat.core.file.FileInfo) del) { + foreach (c; dir.children) { + dub.internal.vibecompat.core.file.FileInfo fi; + fi.name = c.name; + fi.timeModified = c.attributes.modification; + final switch (c.attributes.type) { + case FSEntry.Type.File: + fi.size = c.content.length; + break; + case FSEntry.Type.Directory: + fi.isDirectory = true; + break; + } + if (auto res = del(fi)) + return res; + } + return 0; + } + return &iterator; + } + + /// Ditto + public override void removeFile (in NativePath path, bool force = false) scope + { + return this.cwd.removeFile(path); + } + + /// + public override void removeDir (in NativePath path, bool force = false) + { + this.cwd.removeDir(path, force); + } + + /// Ditto + public override void setTimes (in NativePath path, in SysTime accessTime, + in SysTime modificationTime) + { + auto e = this.cwd.lookup(path); + enforce(e !is null, + "setTimes: No such file or directory: " ~ path.toNativeString()); + e.setTimes(accessTime, modificationTime); + } + + /// Ditto + public override void setAttributes (in NativePath path, uint attributes) + { + auto e = this.cwd.lookup(path); + enforce(e !is null, + "setAttributes: No such file or directory: " ~ path.toNativeString()); + e.setAttributes(attributes); + } + + /** + * Converts an `Filesystem` and its children to a `ZipFile` + */ + public ubyte[] serializeToZip (string rootPath) { + import std.path; + import std.zip; + + scope z = new ZipArchive(); + void addToZip(scope string dir, scope FSEntry e) { + auto m = new ArchiveMember(); + m.name = dir.buildPath(e.name); + m.fileAttributes = e.attributes.attrs; + m.time = e.attributes.modification; + + final switch (e.attributes.type) { + case FSEntry.Type.Directory: + // We need to ensure the directory entry ends with a slash + // otherwise it will be considered as a file. + if (m.name[$-1] != '/') + m.name ~= '/'; + z.addMember(m); + foreach (c; e.children) + addToZip(m.name, c); + break; + case FSEntry.Type.File: + m.expandedData = e.content; + z.addMember(m); + } + } + addToZip(rootPath, this.cwd); + return cast(ubyte[]) z.build(); + } +} + +/// The backing logic behind `MockFS` +public class FSEntry +{ + /// Type of file system entry + public enum Type : ubyte { + Directory, + File, + } + + /// List FSEntry attributes + protected struct Attributes { + /// The type of FSEntry, see `FSEntry.Type` + public Type type; + /// System-specific attributes for this `FSEntry` + public uint attrs; + /// Last access time + public SysTime access; + /// Last modification time + public SysTime modification; + } + /// Ditto + protected Attributes attributes; + + /// The name of this node + protected string name; + /// The parent of this entry (can be null for the root) + protected FSEntry parent; + union { + /// Children for this FSEntry (with type == Directory) + protected FSEntry[] children; + /// Content for this FDEntry (with type == File) + protected ubyte[] content; + } + + /// Creates a new FSEntry + package(dub) this (FSEntry p, Type t, string n) + { + // Avoid 'DOS File Times cannot hold dates prior to 1980.' exception + import std.datetime.date; + SysTime DefaultTime = SysTime(DateTime(2020, 01, 01)); + + this.attributes.type = t; + this.parent = p; + this.name = n; + this.attributes.access = DefaultTime; + this.attributes.modification = DefaultTime; + } + + /// Create the root of the filesystem, only usable from this module + package(dub) this () + { + import std.datetime.date; + SysTime DefaultTime = SysTime(DateTime(2020, 01, 01)); + + this.attributes.type = Type.Directory; + this.attributes.access = DefaultTime; + this.attributes.modification = DefaultTime; + } + + /// Get a direct children node, returns `null` if it can't be found + protected inout(FSEntry) lookup(string name) inout return scope + { + assert(!name.canFind('/')); + foreach (c; this.children) + if (c.name == name) + return c; + return null; + } + + /// Get an arbitrarily nested children node + protected inout(FSEntry) lookup(NativePath path) inout return scope + { + auto relp = this.relativePath(path); + relp.normalize(); // try to get rid of `..` + if (relp.empty) + return this; + auto segments = relp.bySegment; + if (auto c = this.lookup(segments.front.name)) { + segments.popFront(); + return !segments.empty ? c.lookup(NativePath(segments)) : c; + } + return null; + } + + /** Get the parent `FSEntry` of a `NativePath` + * + * If the parent doesn't exist, an `Exception` will be thrown + * unless `silent` is provided. If the parent path is a file, + * an `Exception` will be thrown regardless of `silent`. + * + * Params: + * path = The path to look up the parent for + * silent = Whether to error on non-existing parent, + * default to `false`. + */ + protected inout(FSEntry) getParent(NativePath path, bool silent = false) + inout return scope + { + // Relative path in the current directory + if (!path.hasParentPath()) + return this; + + // If we're not in the right `FSEntry`, recurse + const parentPath = path.parentPath(); + auto p = this.lookup(parentPath); + enforce(silent || p !is null, + "No such directory: " ~ parentPath.toNativeString()); + enforce(p is null || p.attributes.type == Type.Directory, + "Parent path is not a directory: " ~ parentPath.toNativeString()); + return p; + } + + /// Returns: A path relative to `this.path` + protected NativePath relativePath(NativePath path) const scope + { + assert(!path.absolute() || path.startsWith(this.path), + "Calling relativePath with a differently rooted path"); + return path.absolute() ? path.relativeTo(this.path) : path; + } + + /*+************************************************************************* + + Utility function + + Below this banners are functions that are provided for the convenience + of writing tests for `Dub`. + + ***************************************************************************/ + + /// Prints a visual representation of the filesystem to stdout for debugging + public void print(bool content = false) const scope + { + import std.range : repeat; + static import std.stdio; + + size_t indent; + for (auto p = &this.parent; (*p) !is null; p = &p.parent) + indent++; + // Don't print anything (even a newline) for root + if (this.parent is null) + std.stdio.write('/'); + else + std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' '); + + final switch (this.attributes.type) { + case Type.Directory: + std.stdio.writeln('(', this.children.length, " entries):"); + foreach (c; this.children) + c.print(content); + break; + case Type.File: + if (!content) + std.stdio.writeln('(', this.content.length, " bytes)"); + else if (this.name.endsWith(".json") || this.name.endsWith(".sdl")) + std.stdio.writeln('(', this.content.length, " bytes): ", + cast(string) this.content); + else + std.stdio.writeln('(', this.content.length, " bytes): ", + this.content); + break; + } + } + + /*+************************************************************************* + + Public filesystem functions + + Below this banners are functions which mimic the behavior of a file + system. + + ***************************************************************************/ + + /// Returns: The `path` of this FSEntry + public NativePath path () const scope + { + if (this.parent is null) + return NativePath("/"); + auto thisPath = this.parent.path ~ this.name; + thisPath.endsWithSlash = (this.attributes.type == Type.Directory); + return thisPath; + } + + /// Implements `mkdir -p`, returns the created directory + public FSEntry mkdir (in NativePath path) scope + { + auto relp = this.relativePath(path); + // Check if the child already exists + auto segments = relp.bySegment; + auto child = this.lookup(segments.front.name); + if (child is null) { + child = new FSEntry(this, Type.Directory, segments.front.name); + this.children ~= child; + } + // Recurse if needed + segments.popFront(); + return !segments.empty ? child.mkdir(NativePath(segments)) : child; + } + + /// + public bool isFile () const scope + { + return this.attributes.type == Type.File; + } + + /// + public bool isDirectory () const scope + { + return this.attributes.type == Type.Directory; + } + + /// Reads a file, returns the content as `ubyte[]` + public ubyte[] readFile (in NativePath path) const scope + { + auto entry = this.lookup(path); + enforce(entry !is null, "No such file: " ~ path.toNativeString()); + enforce(entry.attributes.type == Type.File, "Trying to read a directory"); + // This is a hack to make poisoning a file possible. + // However, it is rather crude and doesn't allow to poison directory. + // Consider introducing a derived type to allow it. + assert(entry.content != "poison".representation, + "Trying to access poisoned path: " ~ path.toNativeString()); + return entry.content.dup; + } + + /// Reads a file, returns the content as text + public string readText (in NativePath path) const scope + { + import std.utf : validate; + + const content = this.readFile(path); + // Ignore BOM: If it's needed for a test, add support for it. + validate(cast(const(char[])) content); + // `readFile` just `dup` the content, so it's safe to cast. + return cast(string) content; + } + + /// Ditto + public void writeFile (in NativePath path, const(ubyte)[] data) scope + { + enforce(!path.endsWithSlash(), + "Cannot write to directory: " ~ path.toNativeString()); + if (auto file = this.lookup(path)) { + // If the file already exists, override it + enforce(file.attributes.type == Type.File, + "Trying to write to directory: " ~ path.toNativeString()); + file.content = data.dup; + } else { + auto p = this.getParent(path); + auto file = new FSEntry(p, Type.File, path.head.name()); + file.content = data.dup; + p.children ~= file; + } + } + + /** Remove a file + * + * Always error if the target is a directory. + * Does not error if the target does not exists + * and `force` is set to `true`. + * + * Params: + * path = Path to the file to remove + * force = Whether to ignore non-existing file, + * default to `false`. + */ + public void removeFile (in NativePath path, bool force = false) + { + import std.algorithm.searching : countUntil; + + assert(!path.empty, "Empty path provided to `removeFile`"); + enforce(!path.endsWithSlash(), + "Cannot remove file with directory path: " ~ path.toNativeString()); + auto p = this.getParent(path, force); + const idx = p.children.countUntil!(e => e.name == path.head.name()); + if (idx < 0) { + enforce(force, + "removeFile: No such file: " ~ path.toNativeString()); + } else { + enforce(p.children[idx].attributes.type == Type.File, + "removeFile called on a directory: " ~ path.toNativeString()); + p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $]; + } + } + + /** Remove a directory + * + * Remove an existing empty directory. + * If `force` is set to `true`, no error will be thrown + * if the directory is empty or non-existing. + * + * Params: + * path = Path to the directory to remove + * force = Whether to ignore non-existing / non-empty directories, + * default to `false`. + */ + public void removeDir (in NativePath path, bool force = false) + { + import std.algorithm.searching : countUntil; + + assert(!path.empty, "Empty path provided to `removeFile`"); + auto p = this.getParent(path, force); + const idx = p.children.countUntil!(e => e.name == path.head.name()); + if (idx < 0) { + enforce(force, + "removeDir: No such directory: " ~ path.toNativeString()); + } else { + enforce(p.children[idx].attributes.type == Type.Directory, + "removeDir called on a file: " ~ path.toNativeString()); + enforce(force || p.children[idx].children.length == 0, + "removeDir called on non-empty directory: " ~ path.toNativeString()); + p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $]; + } + } + + /// Implement `std.file.setTimes` + public void setTimes (in SysTime accessTime, in SysTime modificationTime) + { + this.attributes.access = accessTime; + this.attributes.modification = modificationTime; + } + + /// Implement `std.file.setAttributes` + public void setAttributes (uint attributes) + { + this.attributes.attrs = attributes; + } +} diff --git a/source/dub/internal/io/realfs.d b/source/dub/internal/io/realfs.d new file mode 100644 index 000000000..9497169c0 --- /dev/null +++ b/source/dub/internal/io/realfs.d @@ -0,0 +1,102 @@ +/******************************************************************************* + + An implementation of `Filesystem` using vibe.d functions + +*******************************************************************************/ + +module dub.internal.io.realfs; + +public import dub.internal.io.filesystem; + +/// Ditto +public final class RealFS : Filesystem { + static import dub.internal.vibecompat.core.file; + static import std.file; + + /// + private NativePath path_; + + /// + public this (NativePath cwd = NativePath(std.file.getcwd())) + scope @safe pure nothrow @nogc + { + this.path_ = cwd; + } + + public override NativePath getcwd () const scope + { + return this.path_; + } + + /// + protected override bool existsDirectory (in NativePath path) const scope + { + return dub.internal.vibecompat.core.file.existsDirectory(path); + } + + /// Ditto + protected override void mkdir (in NativePath path) scope + { + dub.internal.vibecompat.core.file.ensureDirectory(path); + } + + /// Ditto + protected override bool existsFile (in NativePath path) const scope + { + return dub.internal.vibecompat.core.file.existsFile(path); + } + + /// Ditto + protected override void writeFile (in NativePath path, const(ubyte)[] data) + scope + { + return dub.internal.vibecompat.core.file.writeFile(path, data); + } + + /// Reads a file, returns the content as `ubyte[]` + public override ubyte[] readFile (in NativePath path) const scope + { + return cast(ubyte[]) std.file.read(path.toNativeString()); + } + + /// Ditto + protected override string readText (in NativePath path) const scope + { + return dub.internal.vibecompat.core.file.readText(path); + } + + /// Ditto + protected override IterateDirDg iterateDirectory (in NativePath path) scope + { + return dub.internal.vibecompat.core.file.iterateDirectory(path); + } + + /// Ditto + protected override void removeFile (in NativePath path, bool force = false) scope + { + return std.file.remove(path.toNativeString()); + } + + /// + public override void removeDir (in NativePath path, bool force = false) + { + if (force) + std.file.rmdirRecurse(path.toNativeString()); + else + std.file.rmdir(path.toNativeString()); + } + + /// Ditto + protected override void setTimes (in NativePath path, in SysTime accessTime, + in SysTime modificationTime) + { + std.file.setTimes( + path.toNativeString(), accessTime, modificationTime); + } + + /// Ditto + protected override void setAttributes (in NativePath path, uint attributes) + { + std.file.setAttributes(path.toNativeString(), attributes); + } +} diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index fd0c69ec6..79029cc4b 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -8,8 +8,8 @@ module dub.packagemanager; import dub.dependency; +import dub.internal.io.filesystem; import dub.internal.utils; -import dub.internal.vibecompat.core.file : FileInfo; import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.path; import dub.internal.logging; @@ -116,6 +116,8 @@ class PackageManager { } /// Ditto InitializationState m_state; + /// The `Filesystem` object, used to interact with directory / files + Filesystem fs; } /** @@ -130,20 +132,40 @@ class PackageManager { */ this(NativePath path) { + import dub.internal.io.realfs; + this.fs = new RealFS(); this.m_internal.searchPath = [ path ]; this.refresh(); } this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true) { - m_repositories = [ - Location(package_path ~ ".dub/packages/"), - Location(user_path ~ "packages/"), - Location(system_path ~ "packages/")]; - + import dub.internal.io.realfs; + this(new RealFS(), package_path ~ ".dub/packages/", + user_path ~ "packages/", system_path ~ "packages/"); if (refresh_packages) refresh(); } + /** + * Instantiate a `PackageManager` with the provided `Filesystem` and paths + * + * Unlike the other overload, paths are taken as-if, e.g. `packages/` is not + * appended to them. + * + * Params: + * fs = Filesystem abstraction to handle all folder/file I/O. + * local = Path to the local package cache (usually the one in the project), + * whih takes preference over `user` and `system`. + * user = Path to the user package cache (usually ~/.dub/packages/), takes + * precedence over `system` but not over `local`. + * system = Path to the system package cache, this has the least precedence. + */ + public this(Filesystem fs, NativePath local, NativePath user, NativePath system) + { + this.fs = fs; + this.m_repositories = [ Location(local), Location(user), Location(system) ]; + } + /** Gets/sets the list of paths to search for local packages. */ @property void searchPath(NativePath[] paths) @@ -412,7 +434,7 @@ class PackageManager { const PackageName pname = parent ? PackageName(parent.name) : PackageName.init; - string text = this.readText(recipe); + string text = this.fs.readText(recipe); auto content = parsePackageRecipe( text, recipe.toNativeString(), pname, null, mode); auto ret = new Package(content, path, parent, version_); @@ -433,7 +455,7 @@ class PackageManager { { foreach (file; packageInfoFiles) { auto filename = directory ~ file.filename; - if (this.existsFile(filename)) return filename; + if (this.fs.existsFile(filename)) return filename; } return NativePath.init; } @@ -487,7 +509,7 @@ class PackageManager { NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_); // Before doing a git clone, let's see if the package exists locally - if (this.existsDirectory(destination)) { + if (this.fs.existsDirectory(destination)) { // It exists, check if we already loaded it. // Either we loaded it on refresh and it's in PlacementLocation.user, // or we just added it and it's in m_internal. @@ -835,13 +857,13 @@ class PackageManager { { assert(!name.sub.length, "Cannot store a subpackage, use main package instead"); NativePath dstpath = this.getPackagePath(dest, name, vers.toString()); - this.ensureDirectory(dstpath.parentPath()); + this.fs.mkdir(dstpath.parentPath()); const lockPath = dstpath.parentPath() ~ ".lock"; // possibly wait for other dub instance import core.time : seconds; auto lock = lockFile(lockPath.toNativeString(), 30.seconds); - if (this.existsFile(dstpath)) { + if (this.fs.existsFile(dstpath)) { return this.getPackage(name, vers, dest); } return this.store_(data, dstpath, name, vers); @@ -858,7 +880,7 @@ class PackageManager { logDebug("Placing package '%s' version '%s' to location '%s'", name, vers, destination.toNativeString()); - enforce(!this.existsFile(destination), + enforce(!this.fs.existsFile(destination), "%s (%s) needs to be removed from '%s' prior placement." .format(name, vers, destination)); @@ -891,13 +913,13 @@ class PackageManager { import std.datetime : DosFileTimeToSysTime; auto mtime = DosFileTimeToSysTime(am.time); - this.setTimes(path, mtime, mtime); + this.fs.setTimes(path, mtime, mtime); if (auto attrs = am.fileAttributes) - this.setAttributes(path, attrs); + this.fs.setAttributes(path, attrs); } // extract & place - this.ensureDirectory(destination); + this.fs.mkdir(destination); logDebug("Copying all files..."); int countFiles = 0; foreach(ArchiveMember a; archive.directory) { @@ -907,9 +929,9 @@ class PackageManager { logDebug("Creating %s", cleanedPath); if (dst_path.endsWithSlash) { - this.ensureDirectory(dst_path); + this.fs.mkdir(dst_path); } else { - this.ensureDirectory(dst_path.parentPath); + this.fs.mkdir(dst_path.parentPath); // for symlinks on posix systems, use the symlink function to // create them. Windows default unzip doesn't handle symlinks, // so we don't need to worry about it for Windows. @@ -925,7 +947,7 @@ class PackageManager { } } - this.writeFile(dst_path, archive.expand(a)); + this.fs.writeFile(dst_path, archive.expand(a)); setAttributes(dst_path, a); symlink_exit: ++countFiles; @@ -938,10 +960,10 @@ symlink_exit: if (pack.recipePath.head != defaultPackageFilename) // Storeinfo saved a default file, this could be different to the file from the zip. - this.removeFile(pack.recipePath); + this.fs.removeFile(pack.recipePath); auto app = appender!string(); app.writePrettyJsonString(pack.recipe.toJson()); - this.writeFile(pack.recipePath.parentPath ~ defaultPackageFilename, app.data); + this.fs.writeFile(pack.recipePath.parentPath ~ defaultPackageFilename, app.data); addPackages(this.m_internal.localPackages, pack); return pack; } @@ -1179,7 +1201,7 @@ symlink_exit: // parent directories and look for inheritable dub.selections.json files const path = this.findSelections(absProjectPath); if (path.empty) return N.init; - const content = this.readText(path); + const content = this.fs.readText(path); // 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. @@ -1198,7 +1220,7 @@ symlink_exit: private NativePath findSelections(in NativePath dir) { const path = dir ~ "dub.selections.json"; - if (this.existsFile(path)) + if (this.fs.existsFile(path)) return path; if (!dir.hasParentPath) return NativePath.init; @@ -1223,9 +1245,9 @@ symlink_exit: bool overwrite = true) { const path = project.path ~ "dub.selections.json"; - if (!overwrite && this.existsFile(path)) + if (!overwrite && this.fs.existsFile(path)) return; - this.writeFile(path, selectionsToString(selections)); + this.fs.writeFile(path, selectionsToString(selections)); } /// Package function to avoid code duplication with deprecated @@ -1301,81 +1323,6 @@ symlink_exit: } } } - - /// Used for dependency injection - protected bool existsDirectory(NativePath path) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.existsDirectory(path); - } - - /// Ditto - protected void ensureDirectory(NativePath path) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.ensureDirectory(path); - } - - /// Ditto - protected bool existsFile(NativePath path) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.existsFile(path); - } - - /// Ditto - protected void writeFile(NativePath path, const(ubyte)[] data) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.writeFile(path, data); - } - - /// Ditto - protected void writeFile(NativePath path, const(char)[] data) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.writeFile(path, data); - } - - /// Ditto - protected string readText(NativePath path) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.readText(path); - } - - /// Ditto - protected alias IterateDirDg = int delegate(scope int delegate(ref FileInfo)); - - /// Ditto - protected IterateDirDg iterateDirectory(NativePath path) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.iterateDirectory(path); - } - - /// Ditto - protected void removeFile(NativePath path) - { - static import dub.internal.vibecompat.core.file; - return dub.internal.vibecompat.core.file.removeFile(path); - } - - /// Ditto - protected void setTimes(in NativePath path, in SysTime accessTime, - in SysTime modificationTime) - { - static import std.file; - std.file.setTimes( - path.toNativeString(), accessTime, modificationTime); - } - - /// Ditto - protected void setAttributes(in NativePath path, uint attributes) - { - static import std.file; - std.file.setAttributes(path.toNativeString(), attributes); - } } deprecated(OverrideDepMsg) @@ -1519,11 +1466,11 @@ package struct Location { { this.overrides = null; auto ovrfilepath = this.packagePath ~ LocalOverridesFilename; - if (mgr.existsFile(ovrfilepath)) { + if (mgr.fs.existsFile(ovrfilepath)) { logWarn("Found local override file: %s", ovrfilepath); logWarn(OverrideDepMsg); logWarn("Replace with a path-based dependency in your project or a custom cache path"); - const text = mgr.readText(ovrfilepath); + const text = mgr.fs.readText(ovrfilepath); auto json = parseJsonString(text, ovrfilepath.toNativeString()); foreach (entry; json) { PackageOverride_ ovr; @@ -1550,10 +1497,10 @@ package struct Location { newlist ~= jovr; } auto path = this.packagePath; - mgr.ensureDirectory(path); + mgr.fs.mkdir(path); auto app = appender!string(); app.writePrettyJsonString(Json(newlist)); - mgr.writeFile(path ~ LocalOverridesFilename, app.data); + mgr.fs.writeFile(path ~ LocalOverridesFilename, app.data); } private void writeLocalPackageList(PackageManager mgr) @@ -1576,10 +1523,10 @@ package struct Location { } NativePath path = this.packagePath; - mgr.ensureDirectory(path); + mgr.fs.mkdir(path); auto app = appender!string(); app.writePrettyJsonString(Json(newlist)); - mgr.writeFile(path ~ LocalPackagesFilename, app.data); + mgr.fs.writeFile(path ~ LocalPackagesFilename, app.data); } // load locally defined packages @@ -1590,10 +1537,10 @@ package struct Location { NativePath[] paths; try { auto local_package_file = list_path ~ LocalPackagesFilename; - if (!manager.existsFile(local_package_file)) return; + if (!manager.fs.existsFile(local_package_file)) return; logDiagnostic("Loading local package map at %s", local_package_file.toNativeString()); - const text = manager.readText(local_package_file); + const text = manager.fs.readText(local_package_file); auto packlist = parseJsonString( text, local_package_file.toNativeString()); enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array."); @@ -1668,7 +1615,7 @@ package struct Location { void scanPackageFolder(NativePath path, PackageManager mgr, Package[] existing_packages) { - if (!mgr.existsDirectory(path)) + if (!mgr.fs.existsDirectory(path)) return; void loadInternal (NativePath pack_path, NativePath packageFile) @@ -1692,7 +1639,7 @@ package struct Location { } logDebug("iterating dir %s", path.toNativeString()); - try foreach (pdir; mgr.iterateDirectory(path)) { + try foreach (pdir; mgr.fs.iterateDirectory(path)) { logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); if (!pdir.isDirectory) continue; @@ -1714,10 +1661,10 @@ package struct Location { // This is the most common code path // Iterate over versions of a package - foreach (versdir; mgr.iterateDirectory(pack_path)) { + foreach (versdir; mgr.fs.iterateDirectory(pack_path)) { if (!versdir.isDirectory) continue; auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/"); - if (!mgr.existsDirectory(vers_path)) continue; + if (!mgr.fs.existsDirectory(vers_path)) continue; packageFile = mgr.findPackageFile(vers_path); loadInternal(vers_path, packageFile); } @@ -1787,7 +1734,7 @@ package struct Location { string versStr = vers.toString(); const path = this.getPackagePath(name, versStr); - if (!mgr.existsDirectory(path)) + if (!mgr.fs.existsDirectory(path)) return null; logDiagnostic("Lazily loading package %s:%s from %s", name.main, vers, path); diff --git a/source/dub/test/base.d b/source/dub/test/base.d index 81e96fa8e..859ba2e79 100644 --- a/source/dub/test/base.d +++ b/source/dub/test/base.d @@ -60,8 +60,9 @@ import dub.data.settings; public import dub.dependency; public import dub.dub; public import dub.package_; +import dub.internal.io.mockfs; import dub.internal.vibecompat.core.file : FileInfo; -public import dub.internal.vibecompat.inet.path; +public import dub.internal.io.filesystem; import dub.packagemanager; import dub.packagesuppliers.packagesupplier; import dub.project; @@ -82,7 +83,7 @@ unittest // which receives an `FSEntry` representing the root of the filesystem. // Various low-level functions are exposed (mkdir, writeFile, ...), // as well as higher-level functions (`writePackageFile`). - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { // `a` will be loaded as the project while `b` will be loaded // as a simple package. The recipe files can be in JSON or SDL format, // here we use both to demonstrate this. @@ -129,11 +130,12 @@ version "1.2.0"`, PackageFormat.sdl); assert(dub.project.getDependency("b", true).version_ == Version("1.2.0")); /// Adding a package to the registry require the version and at list a recipe - dub.getRegistry().add(Version("1.3.0"), (scope FSEntry pkg) { + dub.getRegistry().add(Version("1.3.0"), (scope Filesystem pkg) { // This is required pkg.writeFile(NativePath(`dub.sdl`), `name "b"`); // Any other files can be present, as a normal package - pkg.mkdir(NativePath("source/b/")).writeFile( + pkg.mkdir(NativePath("source/b/")); + pkg.writeFile( NativePath("main.d"), "module b.main; void main() {}"); }); // Fetch the package from the registry @@ -174,7 +176,7 @@ public void disableLogging() public class TestDub : Dub { /// The virtual filesystem that this instance acts on - public FSEntry fs; + public MockFS fs; /** * Redundant reference to the registry @@ -220,13 +222,13 @@ public class TestDub : Dub ***************************************************************************/ - public this (scope void delegate(scope FSEntry root) dg = null, + public this (scope void delegate(scope Filesystem root) dg = null, string root = ProjectPath.toNativeString(), PackageSupplier[] extras = null, SkipPackageSuppliers skip = SkipPackageSuppliers.none) { /// Create the fs & its base structure - auto fs_ = new FSEntry(); + auto fs_ = new MockFS(); fs_.mkdir(Paths.temp); fs_.mkdir(Paths.systemSettings); fs_.mkdir(Paths.userSettings); @@ -244,12 +246,12 @@ public class TestDub : Dub PackageSupplier[] extras = null, SkipPackageSuppliers skip = SkipPackageSuppliers.none) { - alias TType = void delegate(scope FSEntry); + alias TType = void delegate(scope Filesystem); this(TType.init, root, extras, skip); } /// Internal constructor - private this(FSEntry fs_, string root, PackageSupplier[] extras, + private this(MockFS fs_, string root, PackageSupplier[] extras, SkipPackageSuppliers skip) { this.fs = fs_; @@ -275,7 +277,7 @@ public class TestDub : Dub ***************************************************************************/ - public TestDub newTest (scope void delegate(scope FSEntry root) dg = null, + public TestDub newTest (scope void delegate(scope Filesystem root) dg = null, string root = ProjectPath.toNativeString(), PackageSupplier[] extras = null, SkipPackageSuppliers skip = SkipPackageSuppliers.none) @@ -364,16 +366,13 @@ package class TestPackageManager : PackageManager /// List of all SCM packages that can be fetched by this instance protected string[GitReference] scm; - /// The virtual filesystem that this PackageManager acts on - protected FSEntry fs; - this(FSEntry filesystem) + this(Filesystem filesystem) { - NativePath local = TestDub.ProjectPath; - NativePath user = TestDub.Paths.userSettings; - NativePath system = TestDub.Paths.systemSettings; - this.fs = filesystem; - super(local, user, system, false); + NativePath local = TestDub.ProjectPath ~ ".dub/packages/"; + NativePath user = TestDub.Paths.userSettings ~ "packages/"; + NativePath system = TestDub.Paths.systemSettings ~ "packages/"; + super(filesystem, local, user, system); } /** @@ -406,94 +405,12 @@ package class TestPackageManager : PackageManager // Most of the code is copied from the base method assert(!name.sub.length, "Cannot store a subpackage, use main package instead"); NativePath dstpath = this.getPackagePath(dest, name, vers.toString()); - this.ensureDirectory(dstpath.parentPath()); + this.fs.mkdir(dstpath.parentPath()); - if (this.existsFile(dstpath)) + if (this.fs.existsFile(dstpath)) return this.getPackage(name, vers, dest); return this.store_(data, dstpath, name, vers); } - - /// - protected override bool existsDirectory(NativePath path) - { - return this.fs.existsDirectory(path); - } - - /// - protected override void ensureDirectory(NativePath path) - { - this.fs.mkdir(path); - } - - /// - protected override bool existsFile(NativePath path) - { - return this.fs.existsFile(path); - } - - /// - protected override void writeFile(NativePath path, const(ubyte)[] data) - { - return this.fs.writeFile(path, data); - } - - /// - protected override void writeFile(NativePath path, const(char)[] data) - { - return this.fs.writeFile(path, data); - } - - /// - protected override string readText(NativePath path) - { - return this.fs.readText(path); - } - - /// - protected override void removeFile(NativePath path) - { - return this.fs.removeFile(path); - } - - /// - protected override IterateDirDg iterateDirectory(NativePath path) - { - enforce(this.fs.existsDirectory(path), - path.toNativeString() ~ " does not exists or is not a directory"); - auto dir = this.fs.lookup(path); - int iterator(scope int delegate(ref FileInfo) del) { - foreach (c; dir.children) { - FileInfo fi; - fi.name = c.name; - fi.timeModified = c.attributes.modification; - final switch (c.attributes.type) { - case FSEntry.Type.File: - fi.size = c.content.length; - break; - case FSEntry.Type.Directory: - fi.isDirectory = true; - break; - } - if (auto res = del(fi)) - return res; - } - return 0; - } - return &iterator; - } - - /// Ditto - protected override void setTimes(in NativePath path, in SysTime accessTime, - in SysTime modificationTime) - { - this.fs.setTimes(path, accessTime, modificationTime); - } - - /// Ditto - protected override void setAttributes(in NativePath path, uint attributes) - { - this.fs.setAttributes(path, attributes); - } } /** @@ -528,7 +445,7 @@ public class MockPackageSupplier : PackageSupplier * Adds a package to this `PackageSupplier` * * The registry API bakes in Zip files / binary data. - * When adding a package here, just provide an `FSEntry` + * When adding a package here, just provide an `Filesystem` * representing the package directory, which will be converted * to ZipFile / `ubyte[]` and returned by `fetchPackage`. * @@ -545,21 +462,23 @@ public class MockPackageSupplier : PackageSupplier * dg = A delegate that will populate its parameter with the * content of the package. */ - public void add (in Version vers, scope void delegate(scope FSEntry root) dg) + public void add (in Version vers, scope void delegate(scope Filesystem root) dg) { - scope pkgRoot = new FSEntry(); + scope pkgRoot = new MockFS(); dg(pkgRoot); - scope recipe = pkgRoot.lookup("dub.json"); - if (recipe is null) recipe = pkgRoot.lookup("dub.sdl"); - if (recipe is null) recipe = pkgRoot.lookup("package.json"); + string recipe = pkgRoot.existsFile(NativePath("dub.json")) ? "dub.json" : null; + if (recipe is null) + recipe = pkgRoot.existsFile(NativePath("dub.sdl")) ? "dub.sdl" : null; + if (recipe is null) + recipe = pkgRoot.existsFile(NativePath("package.json")) ? "package.json" : null; // Note: If you want to provide an invalid package, override // [Mock]PackageSupplier. Most tests will expect a well-behaving // registry so this assert is here to help with writing tests. assert(recipe !is null, "No package recipe found: Expected dub.json or dub.sdl"); auto pkgRecipe = parsePackageRecipe( - pkgRoot.readText(NativePath(recipe.name)), recipe.name); + pkgRoot.readText(NativePath(recipe)), recipe); pkgRecipe.version_ = vers.toString(); const name = PackageName(pkgRecipe.name); this.pkgs[name][vers] = PkgData( @@ -626,374 +545,6 @@ public class MockPackageSupplier : PackageSupplier } } -/// An abstract filesystem representation -public class FSEntry -{ - /// Type of file system entry - public enum Type : ubyte { - Directory, - File, - } - - /// List FSEntry attributes - protected struct Attributes { - /// The type of FSEntry, see `FSEntry.Type` - public Type type; - /// System-specific attributes for this `FSEntry` - public uint attrs; - /// Last access time - public SysTime access; - /// Last modification time - public SysTime modification; - } - /// Ditto - protected Attributes attributes; - - /// The name of this node - protected string name; - /// The parent of this entry (can be null for the root) - protected FSEntry parent; - union { - /// Children for this FSEntry (with type == Directory) - protected FSEntry[] children; - /// Content for this FDEntry (with type == File) - protected ubyte[] content; - } - - /// Creates a new FSEntry - private this (FSEntry p, Type t, string n) - { - // Avoid 'DOS File Times cannot hold dates prior to 1980.' exception - import std.datetime.date; - SysTime DefaultTime = SysTime(DateTime(2020, 01, 01)); - - this.attributes.type = t; - this.parent = p; - this.name = n; - this.attributes.access = DefaultTime; - this.attributes.modification = DefaultTime; - } - - /// Create the root of the filesystem, only usable from this module - private this () - { - import std.datetime.date; - SysTime DefaultTime = SysTime(DateTime(2020, 01, 01)); - - this.attributes.type = Type.Directory; - this.attributes.access = DefaultTime; - this.attributes.modification = DefaultTime; - } - - /// Get a direct children node, returns `null` if it can't be found - protected inout(FSEntry) lookup(string name) inout return scope - { - assert(!name.canFind('/')); - foreach (c; this.children) - if (c.name == name) - return c; - return null; - } - - /// Get an arbitrarily nested children node - protected inout(FSEntry) lookup(NativePath path) inout return scope - { - auto relp = this.relativePath(path); - relp.normalize(); // try to get rid of `..` - if (relp.empty) - return this; - auto segments = relp.bySegment; - if (auto c = this.lookup(segments.front.name)) { - segments.popFront(); - return !segments.empty ? c.lookup(NativePath(segments)) : c; - } - return null; - } - - /** Get the parent `FSEntry` of a `NativePath` - * - * If the parent doesn't exist, an `Exception` will be thrown - * unless `silent` is provided. If the parent path is a file, - * an `Exception` will be thrown regardless of `silent`. - * - * Params: - * path = The path to look up the parent for - * silent = Whether to error on non-existing parent, - * default to `false`. - */ - protected inout(FSEntry) getParent(NativePath path, bool silent = false) - inout return scope - { - // Relative path in the current directory - if (!path.hasParentPath()) - return this; - - // If we're not in the right `FSEntry`, recurse - const parentPath = path.parentPath(); - auto p = this.lookup(parentPath); - enforce(silent || p !is null, - "No such directory: " ~ parentPath.toNativeString()); - enforce(p is null || p.attributes.type == Type.Directory, - "Parent path is not a directory: " ~ parentPath.toNativeString()); - return p; - } - - /// Returns: A path relative to `this.path` - protected NativePath relativePath(NativePath path) const scope - { - assert(!path.absolute() || path.startsWith(this.path), - "Calling relativePath with a differently rooted path"); - return path.absolute() ? path.relativeTo(this.path) : path; - } - - /*+************************************************************************* - - Utility function - - Below this banners are functions that are provided for the convenience - of writing tests for `Dub`. - - ***************************************************************************/ - - /// Prints a visual representation of the filesystem to stdout for debugging - public void print(bool content = false) const scope - { - import std.range : repeat; - static import std.stdio; - - size_t indent; - for (auto p = &this.parent; (*p) !is null; p = &p.parent) - indent++; - // Don't print anything (even a newline) for root - if (this.parent is null) - std.stdio.write('/'); - else - std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' '); - - final switch (this.attributes.type) { - case Type.Directory: - std.stdio.writeln('(', this.children.length, " entries):"); - foreach (c; this.children) - c.print(content); - break; - case Type.File: - if (!content) - std.stdio.writeln('(', this.content.length, " bytes)"); - else if (this.name.endsWith(".json") || this.name.endsWith(".sdl")) - std.stdio.writeln('(', this.content.length, " bytes): ", - cast(string) this.content); - else - std.stdio.writeln('(', this.content.length, " bytes): ", - this.content); - break; - } - } - - /// Returns: The final destination a specific package needs to be stored in - public static NativePath getPackagePath(in string name_, string vers, - PlacementLocation location = PlacementLocation.user) - { - PackageName name = PackageName(name_); - // Keep in sync with `dub.packagemanager: PackageManager.getPackagePath` - // and `Location.getPackagePath` - NativePath result (in NativePath base) - { - NativePath res = base ~ name.main.toString() ~ vers ~ - name.main.toString(); - res.endsWithSlash = true; - return res; - } - - final switch (location) { - case PlacementLocation.user: - return result(TestDub.Paths.userSettings ~ "packages/"); - case PlacementLocation.system: - return result(TestDub.Paths.systemSettings ~ "packages/"); - case PlacementLocation.local: - return result(TestDub.ProjectPath ~ "/.dub/packages/"); - } - } - - /*+************************************************************************* - - Public filesystem functions - - Below this banners are functions which mimic the behavior of a file - system. - - ***************************************************************************/ - - /// Returns: The `path` of this FSEntry - public NativePath path() const scope - { - if (this.parent is null) - return NativePath("/"); - auto thisPath = this.parent.path ~ this.name; - thisPath.endsWithSlash = (this.attributes.type == Type.Directory); - return thisPath; - } - - /// Implements `mkdir -p`, returns the created directory - public FSEntry mkdir (NativePath path) scope - { - auto relp = this.relativePath(path); - // Check if the child already exists - auto segments = relp.bySegment; - auto child = this.lookup(segments.front.name); - if (child is null) { - child = new FSEntry(this, Type.Directory, segments.front.name); - this.children ~= child; - } - // Recurse if needed - segments.popFront(); - return !segments.empty ? child.mkdir(NativePath(segments)) : child; - } - - /// Checks the existence of a file - public bool existsFile (NativePath path) const scope - { - auto entry = this.lookup(path); - return entry !is null && entry.attributes.type == Type.File; - } - - /// Checks the existence of a directory - public bool existsDirectory (NativePath path) const scope - { - auto entry = this.lookup(path); - return entry !is null && entry.attributes.type == Type.Directory; - } - - /// Reads a file, returns the content as `ubyte[]` - public ubyte[] readFile (NativePath path) const scope - { - auto entry = this.lookup(path); - enforce(entry !is null, "No such file: " ~ path.toNativeString()); - enforce(entry.attributes.type == Type.File, "Trying to read a directory"); - // This is a hack to make poisoning a file possible. - // However, it is rather crude and doesn't allow to poison directory. - // Consider introducing a derived type to allow it. - assert(entry.content != "poison".representation, - "Trying to access poisoned path: " ~ path.toNativeString()); - return entry.content.dup; - } - - /// Reads a file, returns the content as text - public string readText (NativePath path) const scope - { - import std.utf : validate; - - const content = this.readFile(path); - // Ignore BOM: If it's needed for a test, add support for it. - validate(cast(const(char[])) content); - // `readFile` just `dup` the content, so it's safe to cast. - return cast(string) content; - } - - /// Write to this file - public void writeFile (NativePath path, const(char)[] data) scope - { - this.writeFile(path, data.representation); - } - - /// Ditto - public void writeFile (NativePath path, const(ubyte)[] data) scope - { - enforce(!path.endsWithSlash(), - "Cannot write to directory: " ~ path.toNativeString()); - if (auto file = this.lookup(path)) { - // If the file already exists, override it - enforce(file.attributes.type == Type.File, - "Trying to write to directory: " ~ path.toNativeString()); - file.content = data.dup; - } else { - auto p = this.getParent(path); - auto file = new FSEntry(p, Type.File, path.head.name()); - file.content = data.dup; - p.children ~= file; - } - } - - /** Remove a file - * - * Always error if the target is a directory. - * Does not error if the target does not exists - * and `force` is set to `true`. - * - * Params: - * path = Path to the file to remove - * force = Whether to ignore non-existing file, - * default to `false`. - */ - public void removeFile (NativePath path, bool force = false) - { - import std.algorithm.searching : countUntil; - - assert(!path.empty, "Empty path provided to `removeFile`"); - enforce(!path.endsWithSlash(), - "Cannot remove file with directory path: " ~ path.toNativeString()); - auto p = this.getParent(path, force); - const idx = p.children.countUntil!(e => e.name == path.head.name()); - if (idx < 0) { - enforce(force, - "removeFile: No such file: " ~ path.toNativeString()); - } else { - enforce(p.children[idx].attributes.type == Type.File, - "removeFile called on a directory: " ~ path.toNativeString()); - p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $]; - } - } - - /** Remove a directory - * - * Remove an existing empty directory. - * If `force` is set to `true`, no error will be thrown - * if the directory is empty or non-existing. - * - * Params: - * path = Path to the directory to remove - * force = Whether to ignore non-existing / non-empty directories, - * default to `false`. - */ - public void removeDir (NativePath path, bool force = false) - { - import std.algorithm.searching : countUntil; - - assert(!path.empty, "Empty path provided to `removeFile`"); - auto p = this.getParent(path, force); - const idx = p.children.countUntil!(e => e.name == path.head.name()); - if (idx < 0) { - enforce(force, - "removeDir: No such directory: " ~ path.toNativeString()); - } else { - enforce(p.children[idx].attributes.type == Type.Directory, - "removeDir called on a file: " ~ path.toNativeString()); - enforce(force || p.children[idx].children.length == 0, - "removeDir called on non-empty directory: " ~ path.toNativeString()); - p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $]; - } - } - - /// Implement `std.file.setTimes` - public void setTimes(in NativePath path, in SysTime accessTime, - in SysTime modificationTime) - { - auto e = this.lookup(path); - enforce(e !is null, - "setTimes: No such file or directory: " ~ path.toNativeString()); - e.attributes.access = accessTime; - e.attributes.modification = modificationTime; - } - - /// Implement `std.file.setAttributes` - public void setAttributes(in NativePath path, uint attributes) - { - auto e = this.lookup(path); - enforce(e !is null, - "setTimes: No such file or directory: " ~ path.toNativeString()); - e.attributes.attrs = attributes; - } -} - /** * Convenience function to write a package file * @@ -1001,52 +552,45 @@ public class FSEntry * package name and version. * * Params: - * root = The root FSEntry + * root = The root Filesystem * name = The package name (typed as string for convenience) * vers = The package version * recipe = The text of the package recipe * fmt = The format used for `recipe` (default to JSON) * location = Where to place the package (default to user location) */ -public void writePackageFile (FSEntry root, in string name, in string vers, +public void writePackageFile (Filesystem root, in string name, in string vers, in string recipe, in PackageFormat fmt = PackageFormat.json, in PlacementLocation location = PlacementLocation.user) { - const path = FSEntry.getPackagePath(name, vers, location); - root.mkdir(path).writeFile( - NativePath(fmt == PackageFormat.json ? "dub.json" : "dub.sdl"), + const path = getPackagePath(name, vers, location); + root.mkdir(path); + root.writeFile( + path ~ (fmt == PackageFormat.json ? "dub.json" : "dub.sdl"), recipe); } -/** - * Converts an `FSEntry` and its children to a `ZipFile` - */ -public ubyte[] serializeToZip (scope FSEntry root, string rootPath) { - import std.path; - import std.zip; - - scope z = new ZipArchive(); - void addToZip(scope string dir, scope FSEntry e) { - auto m = new ArchiveMember(); - m.name = dir.buildPath(e.name); - m.fileAttributes = e.attributes.attrs; - m.time = e.attributes.modification; - - final switch (e.attributes.type) { - case FSEntry.Type.Directory: - // We need to ensure the directory entry ends with a slash - // otherwise it will be considered as a file. - if (m.name[$-1] != '/') - m.name ~= '/'; - z.addMember(m); - foreach (c; e.children) - addToZip(m.name, c); - break; - case FSEntry.Type.File: - m.expandedData = e.content; - z.addMember(m); - } +/// Returns: The final destination a specific package needs to be stored in +public static NativePath getPackagePath(in string name_, string vers, + PlacementLocation location = PlacementLocation.user) +{ + PackageName name = PackageName(name_); + // Keep in sync with `dub.packagemanager: PackageManager.getPackagePath` + // and `Location.getPackagePath` + NativePath result (in NativePath base) + { + NativePath res = base ~ name.main.toString() ~ vers ~ + name.main.toString(); + res.endsWithSlash = true; + return res; + } + + final switch (location) { + case PlacementLocation.user: + return result(TestDub.Paths.userSettings ~ "packages/"); + case PlacementLocation.system: + return result(TestDub.Paths.systemSettings ~ "packages/"); + case PlacementLocation.local: + return result(TestDub.ProjectPath ~ "/.dub/packages/"); } - addToZip(rootPath, root); - return cast(ubyte[]) z.build(); } diff --git a/source/dub/test/dependencies.d b/source/dub/test/dependencies.d index 180f31d9f..4b31f7dc3 100644 --- a/source/dub/test/dependencies.d +++ b/source/dub/test/dependencies.d @@ -31,7 +31,7 @@ import dub.test.base; // Ensure that simple dependencies get resolved correctly unittest { - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" version "1.0.0" dependency "b" version="*" @@ -55,7 +55,7 @@ version "1.0.0"`, PackageFormat.sdl); // Test that indirect dependencies get resolved correctly unittest { - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" dependency "b" version="*"`); root.writePackageFile("b", "1.0.0", `name "b" @@ -77,7 +77,7 @@ version "1.0.0"`, PackageFormat.sdl); // Simple diamond dependency unittest { - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" dependency "b" version="*" dependency "c" version="*"`); @@ -105,7 +105,7 @@ version "1.0.0"`, PackageFormat.sdl); // Missing dependencies trigger an error unittest { - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" dependency "b" version="*"`); }); diff --git a/source/dub/test/other.d b/source/dub/test/other.d index 0e860aada..be406a2cd 100644 --- a/source/dub/test/other.d +++ b/source/dub/test/other.d @@ -21,7 +21,7 @@ unittest const Template = `{"name": "%s", "version": "1.0.0", "dependencies": { "dep1": { "repository": "%s", "version": "%s" }}}`; - scope dub = new TestDub((scope FSEntry fs) { + scope dub = new TestDub((scope Filesystem fs) { // Invalid URL, valid hash fs.writePackageFile("a", "1.0.0", Template.format("a", "git+https://nope.nope", ValidHash)); // Valid URL, invalid hash @@ -53,7 +53,7 @@ unittest { const AddPathDir = TestDub.Paths.temp ~ "addpath/"; const BDir = AddPathDir ~ "b/"; - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { root.writeFile(TestDub.ProjectPath ~ "dub.json", `{ "name": "a", "dependencies": { "b": "~>1.0" } }`); @@ -89,7 +89,7 @@ unittest const Template = `{"name": "%s", "version": "1.0.0", "dependencies": { "dep1": { "repository": "%s", "version": "%s" }}}`; - scope dub = new TestDub((scope FSEntry fs) { + scope dub = new TestDub((scope Filesystem fs) { // This should never be read fs.writePackageFile("poison", "1.0.0", `poison`); fs.writeFile(TestDub.ProjectPath ~ "dub.json", @@ -106,7 +106,7 @@ unittest // Check that a simple build does not lead to the cache being scanned unittest { - scope dub = new TestDub((scope FSEntry fs) { + scope dub = new TestDub((scope Filesystem fs) { // This should never be read fs.writePackageFile("b", "1.0.0", `poison`); fs.writePackageFile("b", "1.1.0", `poison`); diff --git a/source/dub/test/selections_from_parent_dir.d b/source/dub/test/selections_from_parent_dir.d index bf90c3f6c..1a7c418c2 100644 --- a/source/dub/test/selections_from_parent_dir.d +++ b/source/dub/test/selections_from_parent_dir.d @@ -29,10 +29,12 @@ unittest } `; - scope dub = new TestDub((scope FSEntry fs) { - fs.mkdir(pkg1Dir).writeFile(NativePath("dub.sdl"), `name "pkg1" + scope dub = new TestDub((scope Filesystem fs) { + fs.mkdir(pkg1Dir); + fs.writeFile(pkg1Dir ~ "dub.sdl", `name "pkg1" targetType "none"`); - fs.mkdir(pkg2Dir).writeFile(NativePath("dub.sdl"), `name "pkg2" + fs.mkdir(pkg2Dir); + fs.writeFile(pkg2Dir ~ "dub.sdl", `name "pkg2" targetType "library" # don't specify a path, require inherited dub.selections.json to make it path-based (../pkg1) @@ -66,9 +68,10 @@ unittest const root_a = root ~ "a"; const root_a_b = root_a ~ "b"; - scope dub_ = new TestDub((scope FSEntry fs) { + scope dub_ = new TestDub((scope Filesystem fs) { // inheritable root/dub.selections.json - fs.mkdir(root).writeFile(NativePath("dub.selections.json"), `{ + fs.mkdir(root); + fs.writeFile(root ~ "dub.selections.json", `{ "fileVersion": 1, "inheritable": true, "versions": { @@ -77,7 +80,8 @@ unittest } `); // non-inheritable root/a/dub.selections.json - fs.mkdir(root_a).writeFile(NativePath("dub.selections.json"), `{ + fs.mkdir(root_a); + fs.writeFile(root_a ~ "dub.selections.json", `{ "fileVersion": 1, "versions": { "dub": "1.37.0" @@ -133,7 +137,7 @@ unittest // after removing non-inheritable root/a/dub.selections.json: inherited root selections for root/a/b/ { - auto dub = dub_.newTest((scope FSEntry fs) { + auto dub = dub_.newTest((scope Filesystem fs) { fs.removeFile(root_a ~ "dub.selections.json"); }); const result = dub.packageManager.readSelections(root_a_b); diff --git a/source/dub/test/subpackages.d b/source/dub/test/subpackages.d index a84ae8355..a56ac0c3e 100644 --- a/source/dub/test/subpackages.d +++ b/source/dub/test/subpackages.d @@ -17,7 +17,7 @@ import dub.test.base; /// Test of the PackageManager APIs unittest { - scope dub = new TestDub((scope FSEntry root) { + scope dub = new TestDub((scope Filesystem root) { root.writeFile(TestDub.ProjectPath ~ "dub.json", `{ "name": "a", "dependencies": { "b:a": "~>1.0", "b:b": "~>1.0" } }`); root.writePackageFile("b", "1.0.0",