diff --git a/README.md b/README.md index 0bd1d66..532b51e 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,49 @@ Package and build manager for [D](http://dlang.org/) applications and libraries. +There is a central [package registry](https://github.com/rejectedsoftware/dub-registry/) located at . The location will likely change to a dedicated domain at some point. + Introduction ------------ -DUB emerged as a more general replacement for [vibe.d's](http://vibed.org/) package manager. It does not imply a dependecy to vibe.d for packages and was extended to not only directly build projects, but also to generate project files (currently [VisualD](https://github.com/rainers/visuald)). +DUB emerged as a more general replacement for [vibe.d's](http://vibed.org/) package manager. It does not imply a dependecy to vibe.d for packages and was extended to not only directly build projects, but also to generate project files (currently [VisualD](https://github.com/rainers/visuald) and [Mono-D](http://mono-d.alexanderbothe.com/)). The project's pilosophy is to keep things as simple as possible. All that is needed to make a project a dub package is to write a short [package.json](http://registry.vibed.org/publish) file and put the source code into a `source` subfolder. It *can* then be registered on the public [package registry](http://registry.vibed.org) to be made available for everyone. Any dependencies specified in `package.json` are automatically downloaded and made available to the project during the build process. +Key features +------------ + + - Simple package and build description not getting in your way + + - Integrated with Git, avoiding maintainance tasks such as incrementing version numbers or uploading new project releases + + - Generation of VisualD and Mono-D project/solution files + + - Support for DMD, GDC and LDC (common DMD flags are translated automatically) + + - Supports development workflows by optionally using local directories as a package source + + Future direction ---------------- -To make things as flexible as they need to for certain projects, it is planned to gradually add more options to the package file format and eventually to add the possibility to specify an external build tool along with the path of it's output files. The idea is that DUB provides a convenient build management that suffices for 99% of projects, but is also usable as a bare package manager that doesn't get in your way if needed. +To make things as flexible as they need to be for certain projects, it is planned to gradually add more options to the package file format and eventually to add the possibility to specify an external build tool along with the path of it's output files. The idea is that DUB provides a convenient build management that suffices for 99% of projects, but is also usable as a bare package manager that doesn't get in your way if needed. Installation ------------ -DUB comes precompiled for Windows, Mac OS, Linux and FreeBSD. It needs to have the following dependencies installed: - - - libevent 2.0.x - - OpenSSL +DUB comes [precompiled](http://registry.vibed.org/download) for Windows, Mac OS, Linux and FreeBSD. It needs to have libcurl with SSL support installed (except on Windows). The `dub` executable then just needs to be accessible from `PATH` and can be invoked from the root folder of any DUB enabled project to build and run it. + +If you want to build for yourself, just install [DMD](http://dlang.org/download.html) and libcurl development headers and run `./build.sh`. On Windows you can simply run `build.cmd` without installing anything besides DMD. + +### Arch Linux + +Moritz Maxeiner has created a PKGBUILD file for Arch: + + - Latest release: + - GIT master: diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..c70df78 --- /dev/null +++ b/build.cmd @@ -0,0 +1 @@ +rdmd --build-only -ofdub.exe -g -debug -Isource curl.lib %* source\app.d \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..270caee --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +LIBS=`pkg-config --libs libcurl 2>/dev/null || echo "-lcurl"` +LIBS=`echo "$LIBS" | sed 's/^-L/-L-L/; s/ -L/ -L-L/g; s/^-l/-L-l/; s/ -l/ -L-l/g'` +rdmd --build-only -ofdub -g -debug -Isource $LIBS $* source/app.d \ No newline at end of file diff --git a/curl.lib b/curl.lib new file mode 100644 index 0000000..1a1e192 --- /dev/null +++ b/curl.lib Binary files differ diff --git a/libcurl.dll b/libcurl.dll new file mode 100644 index 0000000..a09dc34 --- /dev/null +++ b/libcurl.dll Binary files differ diff --git a/libeay32.dll b/libeay32.dll new file mode 100644 index 0000000..696b300 --- /dev/null +++ b/libeay32.dll Binary files differ diff --git a/package.json b/package.json index b792a8f..c777f76 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,10 @@ "name": "dub", "description": "Package manager for D packages", "copyright": "Copyright 2012 rejectedsoftware e.K.", - "version": "0.0.1", "authors": [ "Matthias Dondorff", "Sönke Ludwig" ], - "dependencies": { - "vibe-d": "~master" - }, - "versions": [ - "VibeCustomMain" - ] + "libs": ["curl"], + "files-windows": ["curllib.dll", "libeay32.dll", "openldap.dll", "ssleay32.dll"] } \ No newline at end of file diff --git a/source/app.d b/source/app.d index d64ae81..0a7b291 100644 --- a/source/app.d +++ b/source/app.d @@ -7,21 +7,22 @@ */ module app; +import dub.compilers.compiler; import dub.dependency; import dub.dub; +import dub.generators.generator; import dub.package_; -import dub.platform; import dub.project; import dub.registry; -import vibe.core.file; -import vibe.core.log; -import vibe.inet.url; -import vibe.utils.string; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.inet.url; import std.algorithm; import std.array; import std.conv; +import std.encoding; import std.exception; import std.file; import std.getopt; @@ -45,6 +46,9 @@ bool help, nodeps, annotate; LogLevel loglevel = LogLevel.Info; string build_type = "debug", build_config; + string compiler_name = "dmd"; + string arch; + bool rdmd = false; bool print_platform, print_builds, print_configs; bool install_system = false, install_local = false; string install_version; @@ -57,6 +61,9 @@ "nodeps", &nodeps, "annotate", &annotate, "build", &build_type, + "compiler", &compiler_name, + "arch", &arch, + "rdmd", &rdmd, "config", &build_config, "print-builds", &print_builds, "print-configs", &print_configs, @@ -91,11 +98,9 @@ Url registryUrl = Url.parse("http://registry.vibed.org/"); logDebug("Using dub registry url '%s'", registryUrl); - // FIXME: take into account command line flags - BuildPlatform build_platform; - build_platform.platform = determinePlatform(); - build_platform.architecture = determineArchitecture(); - build_platform.compiler = "dmd"; + BuildSettings build_settings; + auto compiler = getCompiler(compiler_name); + auto build_platform = compiler.determinePlatform(build_settings, compiler_name, arch); if( print_platform ){ logInfo("Build platform:"); @@ -114,15 +119,89 @@ assert(false); case "help": showHelp(cmd); - break; + return 0; case "init": - string dir = "."; + string dir; if( args.length >= 2 ) dir = args[1]; - initDirectory(dir); + dub.createEmptyPackage(Path(dir)); + return 0; + case "upgrade": + dub.loadPackageFromCwd(); + logInfo("Upgrading project in %s", dub.projectPath.toNativeString); + logDebug("dub initialized"); + dub.update(UpdateOptions.Reinstall | (annotate ? UpdateOptions.JustAnnotate : UpdateOptions.None)); + return 0; + case "install": + enforce(args.length >= 2, "Missing package name."); + auto location = InstallLocation.userWide; + auto name = args[1]; + enforce(!install_local || !install_system, "Cannot install locally and system wide at the same time."); + if( install_local ) location = InstallLocation.local; + else if( install_system ) location = InstallLocation.systemWide; + if( install_version.length ) dub.install(name, new Dependency(install_version), location); + else { + try dub.install(name, new Dependency(">=0.0.0"), location); + catch(Exception e){ + logInfo("Installing a release version failed: %s", e.msg); + logInfo("Retry with ~master..."); + dub.install(name, new Dependency("~master"), location); + } + } + break; + case "uninstall": + enforce(args.length >= 2, "Missing package name."); + /*auto location = InstallLocation.userWide; + auto name = args[1]; + enforce(!install_local || !install_system, "Cannot install locally and system wide at the same time."); + if( install_local ) location = InstallLocation.local; + else if( install_system ) location = InstallLocation.systemWide; + if( install_version.length ) dub.uninstall(name, new Dependency(install_version), location); + else { + assert(false); + }*/ + enforce(false, "Not implemented."); + break; + case "add-local": + enforce(args.length >= 3, "Missing arguments."); + dub.addLocalPackage(args[1], args[2], install_system); + break; + case "remove-local": + enforce(args.length >= 2, "Missing path to package."); + dub.removeLocalPackage(args[1], install_system); + break; + case "list-locals": + logInfo("Locals:"); + foreach( p; dub.packageManager.getPackageIterator() ) + if( p.installLocation == InstallLocation.local ) + logInfo(" %s %s: %s", p.name, p.ver, p.path.toNativeString()); + logInfo(""); break; case "run": case "build": + case "generate": + if( !existsFile("package.json") && !existsFile("source/app.d") ){ + logInfo(""); + logInfo("Neither package.json, nor source/app.d was found in the current directory."); + logInfo("Please run dub from the root directory of an existing package, or create a new"); + logInfo("package using \"dub init \"."); + logInfo(""); + showHelp(null); + return 1; + } + dub.loadPackageFromCwd(); + + string generator; + if( cmd == "run" || cmd == "build" ) generator = rdmd ? "rdmd" : "build"; + else { + if( args.length >= 2 ) generator = args[1]; + if(generator.empty) { + logInfo("Usage: dub generate "); + return 1; + } + } + + auto def_config = dub.getDefaultConfiguration(build_platform); if( !build_config.length ) build_config = def_config; @@ -136,7 +215,7 @@ if( print_configs ){ logInfo("Available configurations:"); foreach( tp; dub.configurations ) - logInfo(" %s%s", tp, tp == def_config ? " [deault]" : null); + logInfo(" %s%s", tp, tp == def_config ? " [default]" : null); logInfo(""); } @@ -148,153 +227,18 @@ enforce(build_config.length == 0 || dub.configurations.canFind(build_config), "Unknown build configuration: "~build_config); - //Added check for existance of [AppNameInPackagejson].d - //If exists, use that as the starting file. - auto outfile = getBinName(dub); - auto mainsrc = getMainSourceFile(dub); + GeneratorSettings gensettings; + gensettings.platform = build_platform; + gensettings.config = build_config; + gensettings.buildType = build_type; + gensettings.compiler = compiler; + gensettings.compilerBinary = compiler_name; + gensettings.buildSettings = build_settings; + gensettings.run = cmd == "run"; + gensettings.runArgs = args[1 .. $]; - logDebug("Application output name is '%s'", outfile); - - // 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", "--build-only"]; - Path run_exe_file; - if( cmd == "build" ){ - flags ~= "-of"~(dub.binaryPath~outfile).toNativeString(); - } else { - import std.random; - auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; - auto tmp = environment.get("TEMP"); - if( !tmp.length ) tmp = environment.get("TMP"); - if( !tmp.length ){ - version(Posix) tmp = "/tmp"; - else tmp = "."; - } - run_exe_file = Path(tmp~"/.rdmd/source/"~rnd~outfile); - flags ~= "-of"~run_exe_file.toNativeString(); - } - - auto settings = dub.getBuildSettings(build_platform, build_config); - settings.addDFlags(["-w", "-property"]); - settings.addVersions(getPackagesAsVersion(dub)); - - // TODO: this belongs to the builder/generator - if( settings.libs.length ){ - try { - logDebug("Trying to use pkg-config to resolve library flags for %s.", settings.libs); - auto libflags = execute("pkg-config", "--libs" ~ settings.libs.map!(l => "lib"~l)().array()); - enforce(libflags.status == 0, "pkg-config exited with error code "~to!string(libflags.status)); - settings.addLFlags(libflags.output.split()); - settings.libs = null; - } catch( Exception e ){ - logDebug("pkg-config failed: %s", e.msg); - logDebug("Falling back to direct -lxyz flags."); - version(Windows) settings.addDFlags(settings.libs.map!(l => l~".lib")().array()); - else settings.addLFlags(settings.libs.map!(l => "-l"~l)().array()); - settings.libs = null; - } - } - - flags ~= settings.dflags; - flags ~= settings.lflags.map!(f => "-L"~f)().array(); - flags ~= settings.importPaths.map!(f => "-I"~f)().array(); - flags ~= settings.stringImportPaths.map!(f => "-J"~f)().array(); - flags ~= settings.versions.map!(f => "-version="~f)().array(); - flags ~= settings.files; - flags ~= (mainsrc).toNativeString(); - - string dflags = environment.get("DFLAGS"); - if( dflags ){ - build_type = "$DFLAGS"; - } else { - switch( build_type ){ - default: throw new Exception("Unknown build configuration: "~build_type); - case "plain": dflags = ""; break; - case "debug": dflags = "-g -debug"; break; - case "release": dflags = "-release -O -inline"; break; - case "unittest": dflags = "-g -unittest"; break; - case "profile": dflags = "-g -O -inline -profile"; break; - case "docs": assert(false, "docgen not implemented"); - } - } - - if( build_config.length ) logInfo("Building configuration "~build_config~", build type "~build_type); - else logInfo("Building default configuration, build type "~build_type); - - logInfo("Running %s", "rdmd " ~ dflags ~ " " ~ join(flags, " ")); - auto rdmd_pid = spawnProcess("rdmd " ~ dflags ~ " " ~ join(flags, " ")); - auto result = rdmd_pid.wait(); - enforce(result == 0, "Build command failed with exit code "~to!string(result)); - - if( settings.copyFiles.length ){ - logInfo("Copying files..."); - foreach( f; settings.copyFiles ){ - auto src = Path(f); - auto dst = (run_exe_file.empty ? dub.binaryPath : run_exe_file.parentPath) ~ Path(f).head; - logDebug(" %s to %s", src.toNativeString(), dst.toNativeString()); - try copyFile(src, dst, true); - catch logWarn("Failed to copy to %s", dst.toNativeString()); - } - } - - if( cmd == "run" ){ - auto prg_pid = spawnProcess(run_exe_file.toNativeString(), args[1 .. $]); - result = prg_pid.wait(); - remove(run_exe_file.toNativeString()); - foreach( f; settings.copyFiles ) - remove((run_exe_file.parentPath ~ Path(f).head).toNativeString()); - enforce(result == 0, "Program exited with code "~to!string(result)); - } - - break; - case "upgrade": - dub.loadPackageFromCwd(); - logInfo("Upgrading project in '%s'", dub.projectPath); - logDebug("dub initialized"); - dub.update(UpdateOptions.Reinstall | (annotate ? UpdateOptions.JustAnnotate : UpdateOptions.None)); - break; - case "install": - enforce(args.length >= 2, "Missing package name."); - auto location = InstallLocation.UserWide; - auto name = args[1]; - enforce(!install_local || !install_system, "Cannot install locally and system wide at the same time."); - if( install_local ) location = InstallLocation.Local; - else if( install_system ) location = InstallLocation.SystemWide; - if( install_version.length ) dub.install(name, new Dependency(install_version), location); - else { - try dub.install(name, new Dependency(">=0.0.0"), location); - catch(Exception) dub.install(name, new Dependency("~master"), location); - } - break; - case "uninstall": - enforce("Not implemented."); - break; - case "add-local": - enforce(args.length >= 3, "Missing arguments."); - dub.addLocalPackage(args[1], args[2], install_system); - break; - case "remove-local": - enforce(args.length >= 2, "Missing path to package."); - dub.removeLocalPackage(args[1], install_system); - break; - case "list-locals": - logInfo("Locals:"); - foreach( p; dub.packageManager.getPackageIterator() ) - if( p.installLocation == InstallLocation.Local ) - logInfo(" %s %s: %s", p.name, p.ver, p.path.toNativeString()); - logInfo(""); - break; - case "generate": - string ide; - if( args.length >= 2 ) ide = args[1]; - if(ide.empty) { - logInfo("Usage: dub generate "); - return -1; - } - dub.loadPackageFromCwd(); - dub.generateProject(ide, build_platform); - logDebug("Project files generated."); + logDebug("Generating using %s", generator); + dub.generateProject(generator, gensettings); break; } @@ -303,7 +247,7 @@ catch(Throwable e) { logError("Error: %s\n", e.msg); - logDebug("Full exception: %s", sanitizeUTF8(cast(ubyte[])e.toString())); + logDebug("Full exception: %s", sanitize(e.toString())); logInfo("Run 'dub help' for usage information."); return 1; } @@ -317,7 +261,8 @@ `Usage: dub [] [] [-- ] Manages the DUB project in the current directory. "--" can be used to separate -DUB options from options passed to the application. +DUB options from options passed to the application. If the command is omitted, +dub will default to "run". Possible commands: help Prints this help screen @@ -331,7 +276,8 @@ Adds a local package directory (e.g. a git repository) remove-local Removes a local package directory list-locals Prints a list of all locals - generate Generates project files for a specified IDE. + generate Generates project files using the specified generator: + visuald, mono-d, build, rdmd General options: --annotate Do not execute dependency installations, just print @@ -346,11 +292,15 @@ plain --config=NAME Builds the specified configuration. Configurations can be defined in package.json + --compiler=NAME Uses one of the supported compilers: + dmd (default), gcc, ldc, gdmd, ldmd + --arch=NAME Force a different architecture (e.g. x86 or x86_64) --nodeps Do not check dependencies for 'run' or 'build' --print-builds Prints the list of available build types --print-configs Prints the list of available configurations --print-platform Prints the identifiers for the current build platform as used for the build fields in package.json + --rdmd Use rdmd instead of directly invoking the compiler Install options: --version Use the specified version/branch instead of the latest @@ -359,106 +309,3 @@ `); } - -private string stripDlangSpecialChars(string s) -{ - char[] ret = s.dup; - for(int i=0; i "lib"~l)().array()); + enforce(libflags.status == 0, "pkg-config exited with error code "~to!string(libflags.status)); + settings.addLFlags(libflags.output.split()); + } catch( Exception e ){ + logDebug("pkg-config failed: %s", e.msg); + logDebug("Falling back to direct -lxyz flags."); + version(Windows) settings.addFiles(settings.libs.map!(l => l~".lib")().array()); + else settings.addLFlags(settings.libs.map!(l => "-l"~l)().array()); + } + settings.libs = null; + } + + if( !(fields & BuildSetting.versions) ){ + settings.addDFlags(settings.versions.map!(s => "-version="~s)().array()); + settings.versions = null; + } + + if( !(fields & BuildSetting.importPaths) ){ + settings.addDFlags(settings.importPaths.map!(s => "-I"~s)().array()); + settings.importPaths = null; + } + + if( !(fields & BuildSetting.stringImportPaths) ){ + settings.addDFlags(settings.stringImportPaths.map!(s => "-J"~s)().array()); + settings.stringImportPaths = null; + } + + if( !(fields & BuildSetting.files) ){ + settings.addDFlags(settings.files); + settings.files = null; + } + + if( !(fields & BuildSetting.lflags) ){ + settings.addDFlags(settings.lflags.map!(f => "-L"~f)().array()); + settings.lflags = null; + } + + assert(fields & BuildSetting.dflags); + assert(fields & BuildSetting.copyFiles); + } + + void setTarget(ref BuildSettings settings, Path binary_path) + { + settings.addDFlags("-of"~binary_path.toNativeString()); + } +} diff --git a/source/dub/compilers/gdc.d b/source/dub/compilers/gdc.d new file mode 100644 index 0000000..39a47de --- /dev/null +++ b/source/dub/compilers/gdc.d @@ -0,0 +1,139 @@ +/** + GDC compiler support. + + Copyright: © 2013 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.compilers.gdc; + +import dub.compilers.compiler; +import dub.platform; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import stdx.process; +import vibecompat.core.log; +import vibecompat.inet.path; + + +class GdcCompiler : Compiler { + @property string name() const { return "gdc"; } + + BuildPlatform determinePlatform(ref BuildSettings settings, string compiler_binary, string arch_override) + { + // TODO: determine platform by invoking the compiler instead + BuildPlatform build_platform; + build_platform.platform = .determinePlatform(); + build_platform.architecture = .determineArchitecture(); + build_platform.compiler = this.name; + + enforce(arch_override.length == 0, "Architecture override not implemented for GDC."); + return build_platform; + } + + void prepareBuildSettings(ref BuildSettings settings, BuildSetting fields = BuildSetting.all) + { + // convert common DMD flags to the corresponding GDC flags + string[] newdflags; + foreach(f; settings.dflags){ + switch(f){ + default: newdflags ~= f; break; + case "-cov": newdflags ~= ["-fprofile-arcs", "-ftest-coverage"]; break; + case "-D": newdflags ~= "-fdoc"; break; + //case "-Dd[dir]": newdflags ~= ""; break; + //case "-Df[file]": newdflags ~= ""; break; + case "-d": newdflags ~= "-fdeprecated"; break; + case "-dw": break; + case "-de": break; + case "-debug": newdflags ~= "-fdebug"; break; + //case "-debug=[level/ident]": newdflags ~= ""; break; + //case "-debuglib=[ident]": newdflags ~= ""; break; + //case "-defaultlib=[ident]": newdflags ~= ""; break; + //case "-deps=[file]": newdflags ~= ""; break; + case "-fPIC": newdflags ~= ""; break; + case "-g": newdflags ~= "-g"; break; + case "-gc": newdflags ~= ["-g" ~ "-fdebug-c"]; break; + case "-gs": break; + case "-H": newdflags ~= "-fintfc"; break; + //case "-Hd[dir]": newdflags ~= ""; break; + //case "-Hf[file]": newdflags ~= ""; break; + case "-ignore": newdflags ~= "-fignore-unknown-pragmas"; break; + case "-inline": newdflags ~= "-finline-functions"; break; + //case "-lib": newdflags ~= ""; break; + //case "-m32": newdflags ~= ""; break; + //case "-m64": newdflags ~= ""; break; + case "-noboundscheck": newdflags ~= "-fno-bounds-check"; break; + case "-O": newdflags ~= "-O3"; break; + case "-o-": newdflags ~= "-fsyntax-only"; break; + //case "-od[dir]": newdflags ~= ""; break; + //case "-of[file]": newdflags ~= ""; break; + //case "-op": newdflags ~= ""; break; + //case "-profile": newdflags ~= "-pg"; break; + case "-property": newdflags ~= "-fproperty"; break; + //case "-quiet": newdflags ~= ""; break; + case "-release": newdflags ~= "-frelease"; break; + case "-shared": newdflags ~= "-shared"; break; + case "-unittest": newdflags ~= "-funittest"; break; + case "-v": newdflags ~= "-fd-verbose"; break; + //case "-version=[level/ident]": newdflags ~= ""; break; + case "-vtls": newdflags ~= "-fd-vtls"; break; + case "-w": newdflags ~= "-Werror"; break; + case "-wi": newdflags ~= "-Wall"; break; + //case "-X": newdflags ~= ""; break; + //case "-Xf[file]": newdflags ~= ""; break; + } + } + settings.dflags = newdflags; + + if( !(fields & BuildSetting.libs) ){ + try { + logDebug("Trying to use pkg-config to resolve library flags for %s.", settings.libs); + auto libflags = execute("pkg-config", "--libs" ~ settings.libs.map!(l => "lib"~l)().array()); + enforce(libflags.status == 0, "pkg-config exited with error code "~to!string(libflags.status)); + settings.addLFlags(libflags.output.split()); + } catch( Exception e ){ + logDebug("pkg-config failed: %s", e.msg); + logDebug("Falling back to direct -lxyz flags."); + settings.addLFlags(settings.libs.map!(l => "-l"~l)().array()); + } + settings.libs = null; + } + + if( !(fields & BuildSetting.versions) ){ + settings.addDFlags(settings.versions.map!(s => "-fversion="~s)().array()); + settings.versions = null; + } + + if( !(fields & BuildSetting.importPaths) ){ + settings.addDFlags(settings.importPaths.map!(s => "-I"~s)().array()); + settings.importPaths = null; + } + + if( !(fields & BuildSetting.stringImportPaths) ){ + settings.addDFlags(settings.stringImportPaths.map!(s => "-J"~s)().array()); + settings.stringImportPaths = null; + } + + if( !(fields & BuildSetting.files) ){ + settings.addDFlags(settings.files); + settings.files = null; + } + + if( !(fields & BuildSetting.lflags) ){ + foreach( f; settings.lflags ) + settings.addDFlags(["-Xlinker", f]); + settings.lflags = null; + } + + assert(fields & BuildSetting.dflags); + assert(fields & BuildSetting.copyFiles); + } + + void setTarget(ref BuildSettings settings, Path binary_path) + { + settings.addDFlags("-o", binary_path.toNativeString()); + } +} diff --git a/source/dub/compilers/ldc.d b/source/dub/compilers/ldc.d new file mode 100644 index 0000000..d94a67c --- /dev/null +++ b/source/dub/compilers/ldc.d @@ -0,0 +1,95 @@ +/** + LDC compiler support. + + Copyright: © 2013 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.compilers.ldc; + +import dub.compilers.compiler; +import dub.platform; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import stdx.process; +import vibecompat.core.log; +import vibecompat.inet.path; + + +class LdcCompiler : Compiler { + @property string name() const { return "ldc"; } + + BuildPlatform determinePlatform(ref BuildSettings settings, string compiler_binary, string arch_override) + { + // TODO: determine platform by invoking the compiler instead + BuildPlatform build_platform; + build_platform.platform = .determinePlatform(); + build_platform.architecture = .determineArchitecture(); + build_platform.compiler = this.name; + + enforce(arch_override.length == 0, "Architecture override not implemented for LDC."); + return build_platform; + } + + void prepareBuildSettings(ref BuildSettings settings, BuildSetting fields = BuildSetting.all) + { + // convert common DMD flags to the corresponding GDC flags + string[] newdflags; + foreach(f; settings.dflags){ + switch(f){ + default: newdflags ~= f; break; + } + } + settings.dflags = newdflags; + + if( !(fields & BuildSetting.libs) ){ + try { + logDebug("Trying to use pkg-config to resolve library flags for %s.", settings.libs); + auto libflags = execute("pkg-config", "--libs" ~ settings.libs.map!(l => "lib"~l)().array()); + enforce(libflags.status == 0, "pkg-config exited with error code "~to!string(libflags.status)); + settings.addLFlags(libflags.output.split()); + } catch( Exception e ){ + logDebug("pkg-config failed: %s", e.msg); + logDebug("Falling back to direct -lxyz flags."); + settings.addLFlags(settings.libs.map!(l => "-l"~l)().array()); + } + settings.libs = null; + } + + if( !(fields & BuildSetting.versions) ){ + settings.addDFlags(settings.versions.map!(s => "-d-version="~s)().array()); + settings.versions = null; + } + + if( !(fields & BuildSetting.importPaths) ){ + settings.addDFlags(settings.importPaths.map!(s => "-I"~s)().array()); + settings.importPaths = null; + } + + if( !(fields & BuildSetting.stringImportPaths) ){ + settings.addDFlags(settings.stringImportPaths.map!(s => "-J"~s)().array()); + settings.stringImportPaths = null; + } + + if( !(fields & BuildSetting.files) ){ + settings.addDFlags(settings.files); + settings.files = null; + } + + if( !(fields & BuildSetting.lflags) ){ + settings.addDFlags(settings.stringImportPaths.map!(s => "-L="~s)().array()); + settings.lflags = null; + } + + assert(fields & BuildSetting.dflags); + assert(fields & BuildSetting.copyFiles); + } + + void setTarget(ref BuildSettings settings, Path binary_path) + { + settings.addDFlags("-of"~binary_path.toNativeString()); + } +} diff --git a/source/dub/dependency.d b/source/dub/dependency.d index 4eea5f8..b9948d7 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -10,10 +10,10 @@ import dub.utils; import dub.package_; -import vibe.core.log; -import vibe.core.file; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports import std.array; diff --git a/source/dub/dub.d b/source/dub/dub.d index df4db7a..277a78a 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -7,6 +7,7 @@ */ module dub.dub; +import dub.compilers.compiler; import dub.dependency; import dub.installation; import dub.utils; @@ -17,10 +18,10 @@ import dub.project; import dub.generators.generator; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports. import std.algorithm; @@ -68,7 +69,7 @@ m_userDubPath = Path(environment.get("APPDATA")) ~ "dub/"; m_tempPath = Path(environment.get("TEMP")); } else version(Posix){ - m_systemDubPath = Path("/etc/dub/"); + m_systemDubPath = Path("/var/lib/dub/"); m_userDubPath = Path(environment.get("HOME")) ~ ".dub/"; m_tempPath = Path("/tmp"); } @@ -99,10 +100,6 @@ m_app = new Project(m_packageManager, m_root); } - /// Returns a list of flags which the application needs to be compiled - /// properly. - BuildSettings getBuildSettings(BuildPlatform platform, string config) { return m_app.getBuildSettings(platform, config); } - string getDefaultConfiguration(BuildPlatform platform) const { return m_app.getDefaultConfiguration(platform); } /// Lists all installed modules @@ -120,8 +117,8 @@ 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("%s %s %s, %s", capitalize(to!string(a.type)), a.packageId, a.vers, a.location); + if( a.type == Action.Type.conflict || a.type == Action.Type.failure ) { logInfo("Issued by: "); conflictedOrFailed = true; foreach(string pkg, d; a.issuer) @@ -135,18 +132,18 @@ // Uninstall first // ?? - // foreach(Action a ; filter!((Action a) => a.action == Action.ActionId.Uninstall)(actions)) + // foreach(Action a ; filter!((Action a) => a.type == Action.Type.Uninstall)(actions)) // uninstall(a.packageId); - // foreach(Action a; filter!((Action a) => a.action == Action.ActionId.InstallUpdate)(actions)) + // foreach(Action a; filter!((Action a) => a.type == Action.Type.InstallUpdate)(actions)) // install(a.packageId, a.vers); foreach(Action a; actions) - if(a.action == Action.ActionId.Uninstall){ + if(a.type == Action.Type.uninstall){ assert(a.pack !is null, "No package specified for uninstall."); uninstall(a.pack); } foreach(Action a; actions) - if(a.action == Action.ActionId.InstallUpdate) - install(a.packageId, a.vers); + if(a.type == Action.Type.install) + install(a.packageId, a.vers, a.location); m_app.reinit(); Action[] newActions = m_app.determineActions(m_packageSupplier, 0); @@ -163,13 +160,9 @@ /// Generate project files for a specified IDE. /// Any existing project files will be overridden. - void generateProject(string ide, BuildPlatform build_platform) { + void generateProject(string ide, GeneratorSettings settings) { auto generator = createProjectGenerator(ide, m_app, m_packageManager); - if(generator is null ) { - logError("Unsupported IDE, there is no generator available for '"~ide~"'"); - throw new Exception("Unsupported IDE, there is no generator available for '"~ide~"'"); - } - generator.generateProject(build_platform); + generator.generateProject(settings); } /// Creates a zip from the application. @@ -189,21 +182,29 @@ /// 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, InstallLocation location = InstallLocation.ProjectLocal) + void install(string packageId, const Dependency dep, InstallLocation location = InstallLocation.projectLocal) { auto pinfo = m_packageSupplier.packageJson(packageId, dep); string ver = pinfo["version"].get!string; - logInfo("Installing %s %s...", packageId, ver); + if( m_packageManager.hasPackage(packageId, ver, location) ){ + logInfo("Package %s %s (%s) is already installed with the latest version, skipping upgrade.", + packageId, ver, location); + return; + } - logDebug("Aquiring package zip file"); + logInfo("Downloading %s %s...", packageId, ver); + + logDebug("Acquiring package zip file"); auto dload = m_root ~ ".dub/temp/downloads"; - auto tempFile = m_tempPath ~ ("dub-download-"~packageId~"-"~ver~".zip"); + auto tempfname = packageId ~ "-" ~ (ver.startsWith('~') ? ver[1 .. $] : ver) ~ ".zip"; + auto tempFile = m_tempPath ~ tempfname; string sTempFile = tempFile.toNativeString(); if(exists(sTempFile)) remove(sTempFile); m_packageSupplier.storePackage(tempFile, packageId, dep); // Q: continue on fail? scope(exit) remove(sTempFile); + logInfo("Installing %s %s...", packageId, ver); m_packageManager.install(tempFile, pinfo, location); } @@ -230,4 +231,59 @@ if( !abs_path.absolute ) abs_path = m_cwd ~ abs_path; m_packageManager.removeLocalPackage(abs_path, system ? LocalPackageType.system : LocalPackageType.user); } + + void createEmptyPackage(Path path) + { + path.normalize(); + + //Check to see if a target directory needs to be created + if( !path.empty ){ + if( !existsFile(path) ) + createDirectory(path); + } + + //Make sure we do not overwrite anything accidentally + if( existsFile(path ~ PackageJsonFilename) || + existsFile(path ~ "source") || + existsFile(path ~ "views") || + existsFile(path ~ "public") ) + { + throw new Exception("The current directory is not empty.\n"); + } + + //raw strings must be unindented. + immutable packageJson = +`{ + "name": "`~(path.empty ? "my-project" : path.head.toString())~`", + "description": "An example project skeleton", + "homepage": "http://example.org", + "copyright": "Copyright © 2000, Your Name", + "authors": [ + "Your Name" + ], + "dependencies": { + } +} +`; + immutable appFile = +`import std.stdio; + +void main() +{ + writeln("Edit source/app.d to start your project."); +} +`; + + //Create the common directories. + createDirectory(path ~ "source"); + createDirectory(path ~ "views"); + createDirectory(path ~ "public"); + + //Create the common files. + openFile(path ~ PackageJsonFilename, FileMode.Append).write(packageJson); + openFile(path ~ "source/app.d", FileMode.Append).write(appFile); + + //Act smug to the user. + logInfo("Successfully created an empty project in '"~path.toNativeString()~"'."); + } } diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d new file mode 100644 index 0000000..7ed19b2 --- /dev/null +++ b/source/dub/generators/build.d @@ -0,0 +1,141 @@ +/** + Generator for direct compiler builds. + + Copyright: © 2013 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.generators.build; + +import dub.compilers.compiler; +import dub.generators.generator; +import dub.package_; +import dub.packagemanager; +import dub.project; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.file; +import std.string; +import stdx.process; + +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.inet.path; + + +class BuildGenerator : ProjectGenerator { + private { + Project m_project; + PackageManager m_pkgMgr; + } + + this(Project app, PackageManager mgr) + { + m_project = app; + m_pkgMgr = mgr; + } + + void generateProject(GeneratorSettings settings) + { + + //Added check for existance of [AppNameInPackagejson].d + //If exists, use that as the starting file. + auto outfile = getBinName(m_project); + auto mainsrc = getMainSourceFile(m_project); + + logDebug("Application output name is '%s'", outfile); + + auto buildsettings = settings.buildSettings; + m_project.addBuildSettings(buildsettings, settings.platform, settings.config); + buildsettings.addDFlags(["-w"/*, "-property"*/]); + string dflags = environment.get("DFLAGS"); + if( dflags.length ){ + settings.buildType = "$DFLAGS"; + buildsettings.addDFlags(dflags.split()); + } else { + addBuildTypeFlags(buildsettings, settings.buildType); + } + + // add all .d files + void addPackageFiles(in Package pack){ + foreach(s; pack.sources){ + if( pack !is m_project.mainPackage && s == Path("source/app.d") ) + continue; + auto relpath = (pack.path ~ s).relativeTo(m_project.mainPackage.path); + buildsettings.addFiles(relpath.toNativeString()); + } + } + addPackageFiles(m_project.mainPackage); + foreach(dep; m_project.installedPackages) + addPackageFiles(dep); + + // setup for command line + settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); + + Path run_exe_file; + if( !settings.run ){ + settings.compiler.setTarget(buildsettings, m_project.binaryPath~outfile); + } else { + import std.random; + auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; + auto tmp = environment.get("TEMP"); + if( !tmp.length ) tmp = environment.get("TMP"); + if( !tmp.length ){ + version(Posix) tmp = "/tmp"; + else tmp = "."; + } + run_exe_file = Path(tmp~"/.rdmd/source/"~rnd~outfile); + settings.compiler.setTarget(buildsettings, run_exe_file); + } + + string[] flags = buildsettings.dflags; + + if( settings.config.length ) logInfo("Building configuration "~settings.config~", build type "~settings.buildType); + else logInfo("Building default configuration, build type "~settings.buildType); + + logInfo("Running %s", settings.compilerBinary ~ " " ~ join(flags, " ")); + auto compiler_pid = spawnProcess(settings.compilerBinary, flags); + auto result = compiler_pid.wait(); + enforce(result == 0, "Build command failed with exit code "~to!string(result)); + + // TODO: move to a common place - this is not generator specific + if( buildsettings.copyFiles.length ){ + logInfo("Copying files..."); + foreach( f; buildsettings.copyFiles ){ + auto src = Path(f); + auto dst = (run_exe_file.empty ? m_project.binaryPath : run_exe_file.parentPath) ~ Path(f).head; + logDebug(" %s to %s", src.toNativeString(), dst.toNativeString()); + try copyFile(src, dst, true); + catch logWarn("Failed to copy to %s", dst.toNativeString()); + } + } + + if( settings.run ){ + auto prg_pid = spawnProcess(run_exe_file.toNativeString(), settings.runArgs); + result = prg_pid.wait(); + remove(run_exe_file.toNativeString()); + foreach( f; buildsettings.copyFiles ) + remove((run_exe_file.parentPath ~ Path(f).head).toNativeString()); + enforce(result == 0, "Program exited with code "~to!string(result)); + } + } +} + +private string getBinName(in Project prj) +{ + // take the project name as the base or fall back to "app" + string ret = prj.name; + if( ret.length == 0 ) ret ="app"; + version(Windows) { ret ~= ".exe"; } + return ret; +} + +private Path getMainSourceFile(in Project prj) +{ + auto p = Path("source") ~ (prj.name ~ ".d"); + return existsFile(p) ? p : Path("source/app.d"); +} + diff --git a/source/dub/generators/compiler.d b/source/dub/generators/compiler.d deleted file mode 100644 index 52056b9..0000000 --- a/source/dub/generators/compiler.d +++ /dev/null @@ -1,11 +0,0 @@ -/** - - 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.generators.compiler; - -struct Compiler { - -} \ No newline at end of file diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index 5e27fcd..90e1468 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -7,31 +7,76 @@ */ module dub.generators.generator; -import dub.project; +import dub.compilers.compiler; +import dub.generators.build; +import dub.generators.monod; +import dub.generators.rdmd; +import dub.generators.visuald; import dub.package_; import dub.packagemanager; -import dub.generators.monod; -import dub.generators.visuald; -import vibe.core.log; -import std.exception; +import dub.project; -/// A project generator generates projects :-/ +import std.exception; +import vibecompat.core.log; + + +/** + Common interface for project generators/builders. +*/ interface ProjectGenerator { - void generateProject(BuildPlatform build_platform); + void generateProject(GeneratorSettings settings); } -/// Creates a project generator. -ProjectGenerator createProjectGenerator(string projectType, Project app, PackageManager mgr) { + +struct GeneratorSettings { + BuildPlatform platform; + string config; + Compiler compiler; + string compilerBinary; // compiler executable name + BuildSettings buildSettings; + + // only used for generator "rdmd" + bool run; + string[] runArgs; + string buildType; +} + + +/** + Creates a project generator of the given type for the specified project. +*/ +ProjectGenerator createProjectGenerator(string generator_type, Project app, PackageManager mgr) +{ enforce(app !is null, "app==null, Need an application to work on!"); enforce(mgr !is null, "mgr==null, Need a package manager to work on!"); - switch(projectType) { - default: return null; - case "MonoD": + switch(generator_type) { + default: + throw new Exception("Unknown project generator: "~generator_type); + case "build": + logTrace("Generating build generator."); + return new BuildGenerator(app, mgr); + case "rdmd": + logTrace("Generating rdmd generator."); + return new RdmdGenerator(app, mgr); + case "mono-d": logTrace("Generating MonoD generator."); return new MonoDGenerator(app, mgr); - case "VisualD": + case "visuald": logTrace("Generating VisualD generator."); return new VisualDGenerator(app, mgr); } -} \ No newline at end of file +} + +void addBuildTypeFlags(ref BuildSettings dst, string build_type) +{ + switch(build_type){ + default: throw new Exception("Unknown build type: "~build_type); + case "plain": break; + case "debug": dst.addDFlags("-g", "-debug"); break; + case "release": dst.addDFlags("-release", "-O", "-inline"); break; + case "unittest": dst.addDFlags("-g", "-unittest"); break; + case "profile": dst.addDFlags("-g", "-O", "-inline", "-profile"); break; + case "docs": dst.addDFlags("-c", "-o-", "-D", "-Dfdocs", "-Xfdocs.json"); break; + } +} diff --git a/source/dub/generators/monod.d b/source/dub/generators/monod.d index 3eada2c..4858c56 100644 --- a/source/dub/generators/monod.d +++ b/source/dub/generators/monod.d @@ -7,6 +7,12 @@ */ module dub.generators.monod; +import dub.compilers.compiler; +import dub.generators.generator; +import dub.package_; +import dub.packagemanager; +import dub.project; + import std.algorithm; import std.array; import std.conv; @@ -14,13 +20,9 @@ import std.uuid; import std.exception; -import vibe.core.file; -import vibe.core.log; +import vibecompat.core.file; +import vibecompat.core.log; -import dub.project; -import dub.package_; -import dub.packagemanager; -import dub.generators.generator; class MonoDGenerator : ProjectGenerator { private { @@ -28,22 +30,24 @@ PackageManager m_pkgMgr; string[string] m_projectUuids; bool m_singleProject = true; + Config[] m_allConfigs; } this(Project app, PackageManager mgr) { m_app = app; m_pkgMgr = mgr; + m_allConfigs ~= Config("Debug", "AnyCPU", "Any CPU"); } - void generateProject(BuildPlatform build_platform) + void generateProject(GeneratorSettings settings) { - logTrace("About to generate projects for %s, with %s direct dependencies.", m_app.mainPackage().name, to!string(m_app.mainPackage().dependencies().length)); - /+generateProjects(m_app.mainPackage()); - generateSolution();+/ + logTrace("About to generate projects for %s, with %s direct dependencies.", m_app.mainPackage().name, m_app.mainPackage().dependencies().length); + generateProjects(m_app.mainPackage(), settings); + generateSolution(settings); } - /+private void generateSolution() + private void generateSolution(GeneratorSettings settings) { auto sln = openFile(m_app.mainPackage().name ~ ".sln", FileMode.CreateTrunc); scope(exit) sln.close(); @@ -56,27 +60,27 @@ sln.put("Microsoft Visual Studio Solution File, Format Version 11.00\n"); sln.put("# Visual Studio 2010\n"); - generateSolutionEntry(sln, main); - if( m_singleProject ) enforce(main == m_app.mainPackage()); - else performOnDependencies(main, (const Package pack) { generateSolutionEntries(sln, pack); } ); + generateSolutionEntry(sln, settings, m_app.mainPackage); + if( !m_singleProject ) + performOnDependencies(m_app.mainPackage, pack => generateSolutionEntry(sln, settings, pack)); sln.put("Global\n"); // configuration platforms sln.put("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n"); - foreach(config; allconfigs) - sln.formattedWrite("\t\t%s|%s = %s|%s\n", config.configName, config.plaformName); + foreach(config; m_allConfigs) + sln.formattedWrite("\t\t%s|%s = %s|%s\n", config.configName, config.platformName2, + config.configName, config.platformName2); sln.put("\tEndGlobalSection\n"); // configuration platforms per project sln.put("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n"); - generateSolutionConfig(sln, m_app.mainPackage()); - auto projectUuid = guid(pack.name()); - foreach(config; allconfigs) + auto projectUuid = guid(m_app.mainPackage.name); + foreach(config; m_allConfigs) foreach(s; ["ActiveCfg", "Build.0"]) - sln.formattedWrite("\n\t\t%s.%s|%s.%s = %s|%s", - projectUuid, config.configName, config.platformName, s, - config.configName, config.platformName); + sln.formattedWrite("\t\t%s.%s|%s.%s = %s|%s\n", + projectUuid, config.configName, config.platformName2, s, + config.configName, config.platformName2); // TODO: for all dependencies sln.put("\tEndGlobalSection\n"); @@ -93,37 +97,34 @@ sln.put("EndGlobal\n"); } - private void generateSolutionEntry(OutputStream ret, const Package pack) + private void generateSolutionEntry(RangeFile ret, GeneratorSettings settings, const Package pack) { auto projUuid = generateUUID(); auto projName = pack.name; - auto projPath = pack.name ~ ".visualdproj"; + auto projPath = pack.name ~ ".dproj"; auto projectUuid = guid(projName); // Write project header, like so // Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "derelict", "..\inbase\source\derelict.visualdproj", "{905EF5DA-649E-45F9-9C15-6630AA815ACB}" - ret.formattedWrite("\nProject(\"%s\") = \"%s\", \"%s\", \"%s\"", + ret.formattedWrite("Project(\"%s\") = \"%s\", \"%s\", \"%s\"\n", projUuid, projName, projPath, projectUuid); if( !m_singleProject ){ if(pack.dependencies.length > 0) { - ret.formattedWrite(" - ProjectSection(ProjectDependencies) = postProject"); + ret.put(" ProjectSection(ProjectDependencies) = postProject\n"); foreach(id, dependency; pack.dependencies) { // TODO: clarify what "uuid = uuid" should mean auto uuid = guid(id); - ret.formattedWrite(" - %s = %s", uuid, uuid); + ret.formattedWrite(" %s = %s\n", uuid, uuid); } - ret.formattedWrite(" - EndProjectSection"); + ret.put(" EndProjectSection\n"); } } - - ret.formattedWrite("\nEndProject"); + + ret.put("EndProject\n"); } - private void generateProjects(in Package pack) + private void generateProjects(in Package pack, GeneratorSettings settings) { bool[const(Package)] visited; @@ -131,14 +132,16 @@ if( p in visited ) return; visited[p] = true; - generateProject(p); + generateProject(p, settings); if( !m_singleProject ) performOnDependencies(p, &generateRec); } + generateRec(pack); } - private void generateProject(in Package pack) { + private void generateProject(in Package pack, GeneratorSettings settings) + { logTrace("About to write to '%s.dproj' file", pack.name); auto sln = openFile(pack.name ~ ".dproj", FileMode.CreateTrunc); scope(exit) sln.close(); @@ -149,16 +152,67 @@ auto projName = pack.name; - void generateProperties(Configuration config) + auto buildsettings = settings.buildSettings; + m_app.addBuildSettings(buildsettings, settings.platform, m_app.getDefaultConfiguration(settings.platform)); + + // Mono-D does not have a setting for string import paths + settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.all & ~BuildSetting.stringImportPaths); + + sln.put(" \n"); + sln.put(" Debug\n"); + sln.put(" AnyCPU\n"); + sln.put(" 10.0.0\n"); + sln.put(" 2.0\n"); + sln.formattedWrite(" %s\n", guid(pack.name)); + sln.put(" True\n"); + sln.put(" True\n"); + sln.put(" True\n"); + sln.put(" DMD2\n"); + if( !buildsettings.versions.empty ){ + sln.put(" \n"); + sln.put(" \n"); + foreach(ver; buildsettings.versions) + sln.formattedWrite(" %s\n", ver); + sln.put(" \n"); + sln.put(" \n"); + } + if( !buildsettings.importPaths.empty ){ + sln.put(" \n"); + sln.put(" \n"); + foreach(dir; buildsettings.importPaths) + sln.formattedWrite(" %s\n", dir); + sln.put(" \n"); + sln.put(" \n"); + } + if( !buildsettings.libs.empty ){ + sln.put(" \n"); + sln.put(" \n"); + foreach(dir; buildsettings.libs) + sln.formattedWrite(" %s\n", settings.platform.platform.canFind("windows") ? dir ~ ".lib" : dir); + sln.put(" \n"); + sln.put(" \n"); + } + sln.formattedWrite(" %s\n", buildsettings.dflags.join(" ")); + sln.formattedWrite(" %s\n", buildsettings.lflags.join(" ")); + sln.put(" \n"); + + void generateProperties(Config config) { - sln.formattedWrite("\t Condition=\" '$(Configuration)|$(Platform)' == '%s|%s' \"\n", + sln.formattedWrite(" \n", config.configName, config.platformName); - // TODO! - sln.put("\n\n"); + sln.put(" True\n"); + sln.formattedWrite(" bin\\%s\n", config.configName); + sln.put(" True\n"); + sln.put(" Executable\n"); + sln.formattedWrite(" %s\n", pack.name); + sln.put(" False\n"); + sln.formattedWrite(" obj\\%s\n", config.configName); + sln.put(" 0\n"); + sln.put(" \n"); } - foreach(config; allconfigs) + foreach(config; m_allConfigs) generateProperties(config); @@ -168,34 +222,45 @@ if( p in visited ) return; visited[p] = true; - foreach( s; p.sources ) - sln.formattedWrite("\t\t\n", s); - - if( m_singleProject ){ - foreach( dep; p.dependencies ) - generateSources(dep); + foreach( s; p.sources ){ + if( p !is m_app.mainPackage && s == Path("source/app.d") ) + continue; + sln.formattedWrite(" \n", (p.path.relativeTo(pack.path) ~ s).toNativeString()); } + foreach( s; buildsettings.files ) + sln.formattedWrite(" \n", s); } - sln.put("\t\n"); + sln.put(" \n"); generateSources(pack); - sln.put("\t\n"); + if( m_singleProject ) + foreach(dep; m_app.installedPackages) + generateSources(dep); + sln.put(" \n"); sln.put(""); } void performOnDependencies(const Package main, void delegate(const Package pack) op) { - foreach(id, dependency; main.dependencies){ - logDebug("Retrieving package %s from package manager.", id); - auto pack = m_pkgMgr.getBestPackage(id, dependency); - if(pack is null) { - logWarn("Package %s (%s) could not be retrieved continuing...", id, to!string(dependency)); - continue; + bool[const(Package)] visited; + void perform_rec(const Package parent_pack){ + foreach(id, dependency; parent_pack.dependencies){ + logDebug("Retrieving package %s from package manager.", id); + auto pack = m_pkgMgr.getBestPackage(id, dependency); + if( pack in visited ) continue; + visited[pack] = true; + if(pack is null) { + logWarn("Package %s (%s) could not be retrieved continuing...", id, to!string(dependency)); + continue; + } + logDebug("Performing on retrieved package %s", pack.name); + op(pack); + perform_rec(pack); } - logDebug("Performing on retrieved package %s", pack.name); - op(pack); } + + perform_rec(main); } string generateUUID() @@ -208,5 +273,11 @@ if(projectName !in m_projectUuids) m_projectUuids[projectName] = generateUUID(); return m_projectUuids[projectName]; - }+/ + } +} + +struct Config { + string configName; + string platformName; + string platformName2; } \ No newline at end of file diff --git a/source/dub/generators/rdmd.d b/source/dub/generators/rdmd.d new file mode 100644 index 0000000..8602001 --- /dev/null +++ b/source/dub/generators/rdmd.d @@ -0,0 +1,130 @@ +/** + Generator for direct RDMD builds. + + Copyright: © 2013 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.generators.rdmd; + +import dub.compilers.compiler; +import dub.generators.generator; +import dub.package_; +import dub.packagemanager; +import dub.project; + +import std.array; +import std.conv; +import std.exception; +import std.file; +import std.string; +import stdx.process; + +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.inet.path; + + +class RdmdGenerator : ProjectGenerator { + private { + Project m_project; + PackageManager m_pkgMgr; + } + + this(Project app, PackageManager mgr) + { + m_project = app; + m_pkgMgr = mgr; + } + + void generateProject(GeneratorSettings settings) + { + + //Added check for existance of [AppNameInPackagejson].d + //If exists, use that as the starting file. + auto outfile = getBinName(m_project); + auto mainsrc = getMainSourceFile(m_project); + + logDebug("Application output name is '%s'", outfile); + + // 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", "--build-only", "--compiler="~settings.compilerBinary]; + Path run_exe_file; + if( !settings.run ){ + flags ~= "-of"~(m_project.binaryPath~outfile).toNativeString(); + } else { + import std.random; + auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; + auto tmp = environment.get("TEMP"); + if( !tmp.length ) tmp = environment.get("TMP"); + if( !tmp.length ){ + version(Posix) tmp = "/tmp"; + else tmp = "."; + } + run_exe_file = Path(tmp~"/.rdmd/source/"~rnd~outfile); + flags ~= "-of"~run_exe_file.toNativeString(); + } + + auto buildsettings = settings.buildSettings; + m_project.addBuildSettings(buildsettings, settings.platform, settings.config); + buildsettings.addDFlags(["-w"/*, "-property"*/]); + string dflags = environment.get("DFLAGS"); + if( dflags ){ + settings.buildType = "$DFLAGS"; + buildsettings.addDFlags(dflags.split()); + } else { + addBuildTypeFlags(buildsettings, settings.buildType); + } + + settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); + flags ~= buildsettings.dflags; + flags ~= (mainsrc).toNativeString(); + + if( settings.config.length ) logInfo("Building configuration "~settings.config~", build type "~settings.buildType); + else logInfo("Building default configuration, build type "~settings.buildType); + + logInfo("Running %s", "rdmd " ~ join(flags, " ")); + auto rdmd_pid = spawnProcess("rdmd", flags); + auto result = rdmd_pid.wait(); + enforce(result == 0, "Build command failed with exit code "~to!string(result)); + + // TODO: move to a common place - this is not generator specific + if( buildsettings.copyFiles.length ){ + logInfo("Copying files..."); + foreach( f; buildsettings.copyFiles ){ + auto src = Path(f); + auto dst = (run_exe_file.empty ? m_project.binaryPath : run_exe_file.parentPath) ~ Path(f).head; + logDebug(" %s to %s", src.toNativeString(), dst.toNativeString()); + try copyFile(src, dst, true); + catch logWarn("Failed to copy to %s", dst.toNativeString()); + } + } + + if( settings.run ){ + auto prg_pid = spawnProcess(run_exe_file.toNativeString(), settings.runArgs); + result = prg_pid.wait(); + remove(run_exe_file.toNativeString()); + foreach( f; buildsettings.copyFiles ) + remove((run_exe_file.parentPath ~ Path(f).head).toNativeString()); + enforce(result == 0, "Program exited with code "~to!string(result)); + } + } +} + +private string getBinName(in Project prj) +{ + // take the project name as the base or fall back to "app" + string ret = prj.name; + if( ret.length == 0 ) ret ="app"; + version(Windows) { ret ~= ".exe"; } + return ret; +} + +private Path getMainSourceFile(in Project prj) +{ + auto p = Path("source") ~ (prj.name ~ ".d"); + return existsFile(p) ? p : Path("source/app.d"); +} + diff --git a/source/dub/generators/visuald.d b/source/dub/generators/visuald.d index ff3700d..7c3a21e 100644 --- a/source/dub/generators/visuald.d +++ b/source/dub/generators/visuald.d @@ -7,6 +7,12 @@ */ module dub.generators.visuald; +import dub.compilers.compiler; +import dub.generators.generator; +import dub.package_; +import dub.packagemanager; +import dub.project; + import std.algorithm; import std.array; import std.conv; @@ -14,13 +20,8 @@ import std.uuid; import std.exception; -import vibe.core.file; -import vibe.core.log; - -import dub.project; -import dub.package_; -import dub.packagemanager; -import dub.generators.generator; +import vibecompat.core.file; +import vibecompat.core.log; version = VISUALD_SEPERATE_PROJECT_FILES; //version = VISUALD_SINGLE_PROJECT_FILE; @@ -40,9 +41,9 @@ m_pkgMgr = mgr; } - void generateProject(BuildPlatform buildPlatform) { - logTrace("About to generate projects for %s, with %s direct dependencies.", m_app.mainPackage().name, to!string(m_app.mainPackage().dependencies().length)); - generateProjects(m_app.mainPackage(), buildPlatform); + void generateProject(GeneratorSettings settings) { + logTrace("About to generate projects for %s, with %s direct dependencies.", m_app.mainPackage().name, m_app.mainPackage().dependencies().length); + generateProjects(m_app.mainPackage(), settings); generateSolution(); } @@ -61,7 +62,13 @@ Microsoft Visual Studio Solution File, Format Version 11.00 # Visual Studio 2010"); - generateSolutionEntries(ret, m_app.mainPackage()); + generateSolutionEntry(ret, m_app.mainPackage); + version(VISUALD_SEPERATE_PROJECT_FILES) + { + performOnDependencies(m_app.mainPackage, (pack){ + generateSolutionEntry(ret, pack); + }); + } // Global section contains configurations ret.formattedWrite(" @@ -87,20 +94,10 @@ logTrace("About to write to .sln file with %s bytes", to!string(ret.data().length)); auto sln = openFile(solutionFileName(), FileMode.CreateTrunc); scope(exit) sln.close(); - sln.write(ret.data()); + sln.put(ret.data()); sln.flush(); } - - void generateSolutionEntries(Appender!(char[]) ret, const Package main) { - generateSolutionEntry(ret, main); - version(VISUALD_SEPERATE_PROJECT_FILES) { - performOnDependencies(main, (const Package pack) { generateSolutionEntries(ret, pack); } ); - } - version(VISUALD_SINGLE_PROJECT_FILE) { - enforce(main == m_app.mainPackage()); - } - } - + void generateSolutionEntry(Appender!(char[]) ret, const Package pack) { auto projUuid = generateUUID(); auto projName = pack.name; @@ -139,11 +136,11 @@ formattedWrite(ret, "\n\t\t%s.%s.%s = %s", to!string(projectUuid), c, s, c); } - void generateProjects(const Package main, BuildPlatform buildPlatform) { + void generateProjects(const Package main, GeneratorSettings settings) { // TODO: cyclic check - generateProj(main, buildPlatform); + generateProj(main, settings); version(VISUALD_SEPERATE_PROJECT_FILES) { @@ -152,12 +149,13 @@ performOnDependencies(main, (const Package dependency) { if(dependency.name in generatedProjects) return; - generateProjects(dependency, buildPlatform); + generateProj(dependency, settings); } ); } } - void generateProj(const Package pack, BuildPlatform buildPlatform) { + void generateProj(const Package pack, GeneratorSettings settings) + { int i = 0; auto ret = appender!(char[])(); @@ -167,16 +165,22 @@ %s", guid(projName)); // Several configurations (debug, release, unittest) - generateProjectConfiguration(ret, pack, Config.Debug, buildPlatform); - generateProjectConfiguration(ret, pack, Config.Release, buildPlatform); - generateProjectConfiguration(ret, pack, Config.Unittest, buildPlatform); + generateProjectConfiguration(ret, pack, Config.Debug, settings); + generateProjectConfiguration(ret, pack, Config.Release, settings); + generateProjectConfiguration(ret, pack, Config.Unittest, settings); // Add all files bool[SourceFile] sourceFiles; void gatherSources(const(Package) pack, bool prefixPkgId) { - logTrace("Gathering sources for %s", pack.name); + logTrace("Gathering sources for %s (%s)", pack.name, pack is m_app.mainPackage); foreach(source; pack.sources) { - SourceFile f = { pack.name, prefixPkgId? Path(pack.name)~source : source, pack.path ~ source }; + if( pack !is m_app.mainPackage && source == Path("source/app.d") ) + continue; + SourceFile f = { + pack.name, + prefixPkgId ? Path(pack.name)~source : source, + (pack.path ~ source).relativeTo(m_app.mainPackage.path) + }; sourceFiles[f] = true; logTrace(" pkg file: %s", source); } @@ -185,16 +189,8 @@ version(VISUALD_SINGLE_PROJECT_FILE) { // gather all sources enforce(pack == m_app.mainPackage(), "Some setup has gone wrong in VisualD.generateProj()"); - bool[string] gathered; - void gatherAll(const Package package_) { - logDebug("Looking at %s", package_.name); - if(package_.name in gathered) - return; - gathered[package_.name] = true; - gatherSources(package_, true); - performOnDependencies(package_, (const Package dependency) { gatherAll(dependency); }); - } - gatherAll(pack); + gatherSources(pack, true); + performOnDependencies(pack, (dependency) { gatherSources(dependency, true); }); } version(VISUALD_SEPERATE_PROJECT_FILES) { // gather sources for this package only @@ -205,7 +201,8 @@ ret.formattedWrite("\n ", pack.name); Path lastFolder; foreach(source; sortedSources(sourceFiles.keys)) { - auto cur = source.structurePath[0..$-1]; + logTrace("source looking at %s", source.structurePath); + auto cur = source.structurePath[0 .. source.structurePath.length-1]; if(lastFolder != cur) { size_t same = 0; foreach(idx; 0..min(lastFolder.length, cur.length)) @@ -231,20 +228,22 @@ logTrace("About to write to '%s.visualdproj' file %s bytes", pack.name, ret.data().length); auto proj = openFile(projFileName(pack), FileMode.CreateTrunc); scope(exit) proj.close(); - proj.write(ret.data()); + proj.put(ret.data()); proj.flush(); } - void generateProjectConfiguration(Appender!(char[]) ret, const Package pack, Config type, BuildPlatform platform) { - auto settings = m_app.getBuildSettings(platform, m_app.getDefaultConfiguration(platform)); - string[] getSettings(string setting)(){ return __traits(getMember, settings, setting); } + void generateProjectConfiguration(Appender!(char[]) ret, const Package pack, Config type, GeneratorSettings settings) + { + auto buildsettings = settings.buildSettings; + m_app.addBuildSettings(buildsettings, settings.platform, m_app.getDefaultConfiguration(settings.platform)); + string[] getSettings(string setting)(){ return __traits(getMember, buildsettings, setting); } - foreach(architecture; platform.architecture) { + foreach(architecture; settings.platform.architecture) { string arch; switch(architecture) { default: logWarn("Unsupported platform('%s'), defaulting to x86", architecture); goto case; case "x86": arch = "Win32"; break; - case "x64": arch = "x64"; break; + case "x86_64": arch = "x64"; break; } ret.formattedWrite(" ", to!string(type), arch); @@ -378,18 +377,24 @@ } void performOnDependencies(const Package main, void delegate(const Package pack) op) { - // TODO: cyclic check - - foreach(id, dependency; main.dependencies) { - logDebug("Retrieving package %s from package manager.", id); - auto pack = m_pkgMgr.getBestPackage(id, dependency); - if(pack is null) { - logWarn("Package %s (%s) could not be retrieved continuing...", id, to!string(dependency)); - continue; + bool[const(Package)] visited; + void perform_rec(const Package parent_pack){ + foreach(id, dependency; parent_pack.dependencies){ + logDebug("Retrieving package %s from package manager.", id); + auto pack = m_pkgMgr.getBestPackage(id, dependency); + if( pack in visited ) continue; + visited[pack] = true; + if(pack is null) { + logWarn("Package %s (%s) could not be retrieved continuing...", id, to!string(dependency)); + continue; + } + logDebug("Performing on retrieved package %s", pack.name); + op(pack); + perform_rec(pack); } - logDebug("Performing on retrieved package %s", pack.name); - op(pack); } + + perform_rec(main); } string generateUUID() const { diff --git a/source/dub/installation.d b/source/dub/installation.d index 524c40f..97b94ec 100644 --- a/source/dub/installation.d +++ b/source/dub/installation.d Binary files differ diff --git a/source/dub/package_.d b/source/dub/package_.d index 828703a..92e359c 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -7,6 +7,7 @@ */ module dub.package_; +import dub.compilers.compiler; import dub.dependency; import dub.utils; @@ -14,124 +15,20 @@ import std.conv; import std.exception; import std.file; -import vibe.core.file; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.data.json; +import vibecompat.inet.url; enum PackageJsonFilename = "package.json"; -/// Represents a platform a package can be build upon. -struct BuildPlatform { - /// e.g. ["posix", "windows"] - string[] platform; - /// e.g. ["x86", "x64"] - string[] architecture; - /// e.g. "dmd" - string compiler; -} - -/// BuildPlatform specific settings, like needed libraries or additional -/// include paths. -struct BuildSettings { - string[] dflags; - string[] lflags; - string[] libs; - string[] files; - string[] copyFiles; - string[] versions; - string[] importPaths; - string[] stringImportPaths; - - void parse(in Json root, BuildPlatform platform) - { - addDFlags(getPlatformField(root, "dflags", platform)); - addLFlags(getPlatformField(root, "lflags", platform)); - addLibs(getPlatformField(root, "libs", platform)); - addFiles(getPlatformField(root, "files", platform)); - addCopyFiles(getPlatformField(root, "copyFiles", platform)); - addVersions(getPlatformField(root, "versions", platform)); - addImportDirs(getPlatformField(root, "importPaths", platform)); - addStringImportDirs(getPlatformField(root, "stringImportPaths", platform)); - } - - void addDFlags(string[] value) { add(dflags, value); } - void addLFlags(string[] value) { add(lflags, value); } - void addLibs(string[] value) { add(libs, value); } - void addFiles(string[] value) { add(files, value); } - void addCopyFiles(string[] value) { add(copyFiles, value); } - void addVersions(string[] value) { add(versions, value); } - void addImportDirs(string[] value) { add(importPaths, value); } - void addStringImportDirs(string[] value) { add(stringImportPaths, value); } - - // Adds vals to arr without adding duplicates. - private void add(ref string[] arr, string[] vals) - { - foreach( v; vals ){ - bool found = false; - foreach( i; 0 .. arr.length ) - if( arr[i] == v ){ - found = true; - break; - } - if( !found ) arr ~= v; - } - } - - // Parses json and returns the values of the corresponding field - // by the platform. - private string[] getPlatformField(in Json json, string name, BuildPlatform platform) - const { - auto ret = appender!(string[])(); - foreach( suffix; getPlatformSuffixIterator(platform) ){ - foreach( j; json[name~suffix].opt!(Json[]) ) - ret.put(j.get!string); - } - return ret.data; - } -} - -/// Based on the BuildPlatform, creates an iterator with all suffixes. -/// -/// Suffixes are build upon the following scheme, where each component -/// is optional (indicated by []), but the order is obligatory. -/// "[-platform][-architecture][-compiler]" -/// -/// So the following strings are valid suffixes: -/// "-windows-x86-dmd" -/// "-dmd" -/// "-arm" -/// -int delegate(scope int delegate(ref string)) getPlatformSuffixIterator(BuildPlatform platform) -{ - int iterator(scope int delegate(ref string s) del) - { - auto c = platform.compiler; - int delwrap(string s) { return del(s); } - if( auto ret = delwrap(null) ) return ret; - if( auto ret = delwrap("-"~c) ) return ret; - foreach( p; platform.platform ){ - if( auto ret = delwrap("-"~p) ) return ret; - if( auto ret = delwrap("-"~p~"-"~c) ) return ret; - foreach( a; platform.architecture ){ - if( auto ret = delwrap("-"~p~"-"~a) ) return ret; - if( auto ret = delwrap("-"~p~"-"~a~"-"~c) ) return ret; - } - } - foreach( a; platform.architecture ){ - if( auto ret = delwrap("-"~a) ) return ret; - if( auto ret = delwrap("-"~a~"-"~c) ) return ret; - } - return 0; - } - return &iterator; -} /// Indicates where a package has been or should be installed to. enum InstallLocation { - Local, - ProjectLocal, - UserWide, - SystemWide + local, + projectLocal, + userWide, + systemWide } /// Representing an installed package, usually constructed from a json object. @@ -186,7 +83,7 @@ this(jsonFromFile(root ~ PackageJsonFilename), location, root); } - this(Json packageInfo, InstallLocation location = InstallLocation.Local, Path root = Path()) + this(Json packageInfo, InstallLocation location = InstallLocation.local, Path root = Path()) { m_location = location; m_path = root; @@ -252,6 +149,7 @@ auto customSourcePath = "sourcePath" in m_meta; if(customSourcePath) sourcePath = Path(customSourcePath.get!string()); + logTrace("Parsing directory for sources: %s", m_path ~ sourcePath); foreach(d; dirEntries((m_path ~ sourcePath).toNativeString(), "*.d", SpanMode.depth)) { // direct assignment allSources ~= Path(d.name)[...] spawns internal compiler/linker error if(isDir(d.name)) continue; @@ -289,8 +187,6 @@ void writeJson(Path path) { auto dstFile = openFile((path~PackageJsonFilename).toString(), FileMode.CreateTrunc); scope(exit) dstFile.close(); - Appender!string js; - toPrettyJson(js, m_meta); - dstFile.write( js.data ); + dstFile.writePrettyJsonString(m_meta); } } \ No newline at end of file diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 61dcdd4..535c519 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -16,12 +16,12 @@ import std.conv; import std.exception; import std.file; +import std.string; import std.zip; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.path; -import vibe.stream.operations; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.path; enum JournalJsonFilename = "journal.json"; @@ -66,6 +66,15 @@ return null; } + bool hasPackage(string name, string ver, InstallLocation location) + { + foreach(ep; getPackageIterator()){ + if( ep.installLocation == location && ep.name == name && ep.vers == ver ) + return true; + } + return false; + } + Package getBestPackage(string name, string version_spec) { return getBestPackage(name, new Dependency(version_spec)); @@ -162,14 +171,15 @@ Path destination; final switch( location ){ - case InstallLocation.Local: destination = Path(package_name); break; - case InstallLocation.ProjectLocal: enforce(!m_projectPackagePath.empty, "no project path set."); destination = m_projectPackagePath ~ package_name; break; - case InstallLocation.UserWide: destination = m_userPackagePath ~ (package_name ~ "/" ~ package_version); break; - case InstallLocation.SystemWide: destination = m_systemPackagePath ~ (package_name ~ "/" ~ package_version); break; + case InstallLocation.local: destination = Path(package_name); break; + case InstallLocation.projectLocal: enforce(!m_projectPackagePath.empty, "no project path set."); destination = m_projectPackagePath ~ package_name; break; + case InstallLocation.userWide: destination = m_userPackagePath ~ (package_name ~ "/" ~ package_version); break; + case InstallLocation.systemWide: destination = m_systemPackagePath ~ (package_name ~ "/" ~ package_version); break; } - if( existsFile(destination) ) - throw new Exception(package_name~" needs to be uninstalled prior installation."); + if( existsFile(destination) ){ + throw new Exception(format("%s %s needs to be uninstalled prior installation.", package_name, package_version)); + } // open zip file ZipArchive archive; @@ -223,7 +233,7 @@ mkdirRecurse(dst_path.parentPath.toNativeString()); auto dstFile = openFile(dst_path, FileMode.CreateTrunc); scope(exit) dstFile.close(); - dstFile.write(archive.expand(a)); + dstFile.put(archive.expand(a)); journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath)); } } @@ -243,10 +253,10 @@ auto pack = new Package(location, destination); final switch( location ){ - case InstallLocation.Local: break; - case InstallLocation.ProjectLocal: m_projectPackages[package_name] = pack; break; - case InstallLocation.UserWide: m_userPackages[package_name] ~= pack; break; - case InstallLocation.SystemWide: m_systemPackages[package_name] ~= pack; break; + case InstallLocation.local: break; + case InstallLocation.projectLocal: m_projectPackages[package_name] = pack; break; + case InstallLocation.userWide: m_userPackages[package_name] ~= pack; break; + case InstallLocation.systemWide: m_systemPackages[package_name] ~= pack; break; } return pack; } @@ -257,21 +267,21 @@ // remove package from package list final switch(pack.installLocation){ - case InstallLocation.Local: assert(false, "Cannot uninstall locally installed package."); - case InstallLocation.ProjectLocal: + case InstallLocation.local: assert(false, "Cannot uninstall locally installed package."); + case InstallLocation.projectLocal: auto pp = pack.name in m_projectPackages; assert(pp !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed in project."); assert(*pp is pack); m_projectPackages.remove(pack.name); break; - case InstallLocation.UserWide: + case InstallLocation.userWide: auto pv = pack.name in m_systemPackages; assert(pv !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed in user repository."); auto idx = countUntil(*pv, pack); assert(idx < 0 || (*pv)[idx] is pack); if( idx >= 0 ) *pv = (*pv)[0 .. idx] ~ (*pv)[idx+1 .. $]; break; - case InstallLocation.SystemWide: + case InstallLocation.systemWide: auto pv = pack.name in m_userPackages; assert(pv !is null, "Package "~pack.name~" at "~pack.path.toNativeString()~" is not installed system repository."); auto idx = countUntil(*pv, pack); @@ -336,7 +346,7 @@ } } - *packs ~= new Package(info, InstallLocation.Local, path); + *packs ~= new Package(info, InstallLocation.local, path); writeLocalPackageList(type); } @@ -382,11 +392,11 @@ } packs[pdir.name] = vers; } - catch(Exception e) logDebug("Failed to enumerate %s packages: %s", to!string(location), e.toString()); + catch(Exception e) logDebug("Failed to enumerate %s packages: %s", location, e.toString()); } } - scanPackageFolder(m_systemPackagePath, m_systemPackages, InstallLocation.SystemWide); - scanPackageFolder(m_userPackagePath, m_userPackages, InstallLocation.UserWide); + scanPackageFolder(m_systemPackagePath, m_systemPackages, InstallLocation.systemWide); + scanPackageFolder(m_userPackagePath, m_userPackages, InstallLocation.userWide); // rescan the project package folder @@ -399,7 +409,7 @@ if( !existsFile(pack_path ~ PackageJsonFilename) ) continue; try { - auto p = new Package(InstallLocation.ProjectLocal, pack_path); + auto p = new Package(InstallLocation.projectLocal, pack_path); m_projectPackages[pdir.name] = p; } catch( Exception e ){ logError("Failed to load package in %s: %s", pack_path, e.msg); @@ -427,7 +437,7 @@ logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, info.name.get!string()); info.name = name; info["version"] = ver; - auto pp = new Package(info, InstallLocation.Local, path); + auto pp = new Package(info, InstallLocation.local, path); packs ~= pp; } catch( Exception e ){ logWarn("Error adding local package: %s", e.msg); diff --git a/source/dub/packagesupplier.d b/source/dub/packagesupplier.d index b3f6edf..bf80316 100644 --- a/source/dub/packagesupplier.d +++ b/source/dub/packagesupplier.d @@ -15,11 +15,11 @@ 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; +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.data.json; +import vibecompat.inet.url; +import vibecompat.inet.urltransfer; /// Supplies packages, this is done by supplying the latest possible version /// which is available. @@ -39,18 +39,18 @@ 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)); + enforce(existsFile(filename)); + copyFile(filename, path); } Json packageJson(const string packageId, const Dependency dep) { auto filename = bestPackageFile(packageId, dep); - return jsonFromZip(to!string(filename), "package.json"); + return jsonFromZip(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)) { + foreach(DirEntry d; dirEntries(m_path.toNativeString(), packageId~"*", SpanMode.shallow)) { Path p = Path(d.name); logTrace("Entry: %s", p); enforce(to!string(p.head)[$-4..$] == ".zip"); @@ -64,7 +64,7 @@ auto fileName = m_path ~ (packageId ~ "_" ~ to!string(bestVersion) ~ ".zip"); - if(bestVersion == Version.RELEASE || !exists(to!string(fileName))) + if(bestVersion == Version.RELEASE || !existsFile(fileName)) throw new Exception("No matching package found"); logDebug("Found best matching package: '%s'", fileName); diff --git a/source/dub/project.d b/source/dub/project.d index 30c3e7d..7152cd0 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -7,6 +7,7 @@ */ module dub.project; +import dub.compilers.compiler; import dub.dependency; import dub.installation; import dub.utils; @@ -16,10 +17,10 @@ import dub.packagesupplier; import dub.generators.generator; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports. import std.algorithm; @@ -55,6 +56,31 @@ @property Path binaryPath() const { auto p = m_main.binaryPath; return p.length ? Path(p) : Path("./"); } + /// Gathers information + @property string info() + const { + if(!m_main) + return "-Unregocgnized application in '"~m_root.toNativeString()~"' (properly no package.json in this directory)"; + string s = "-Application identifier: " ~ m_main.name; + s ~= "\n" ~ m_main.info(); + s ~= "\n-Installed dependencies:"; + foreach(p; m_dependencies) + s ~= "\n" ~ p.info(); + return s; + } + + /// Gets all installed packages as a "packageId" = "version" associative array + @property string[string] installedPackagesIDs() const { + string[string] pkgs; + foreach(p; m_dependencies) + pkgs[p.name] = p.vers; + return pkgs; + } + + @property const(Package[]) installedPackages() const { return m_dependencies; } + + @property const (Package) mainPackage() const { return m_main; } + string getDefaultConfiguration(BuildPlatform platform) const { string ret; @@ -67,34 +93,7 @@ return ret; } - /// 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 dependencies:"; - foreach(p; m_dependencies) - s ~= "\n" ~ p.info(); - return s; - } - /// Gets all installed packages as a "packageId" = "version" associative array - string[string] installedPackagesIDs() const { - string[string] pkgs; - foreach(p; m_dependencies) - pkgs[p.name] = p.vers; - return pkgs; - } - - const(Package[]) installedPackages() const { - return m_dependencies; - } - - const (Package) mainPackage() const { - return m_main; - } - /// Writes the application's metadata to the package.json file /// in it's root folder. void writeMetadata() const { @@ -113,10 +112,13 @@ if( !existsFile(m_root~PackageJsonFilename) ){ logWarn("There was no '"~PackageJsonFilename~"' found for the application in '%s'.", m_root.toNativeString()); + auto json = Json.EmptyObject; + json.name = ""; + m_main = new Package(json, InstallLocation.local, m_root); return; } - m_main = new Package(InstallLocation.Local, m_root); + m_main = new Package(InstallLocation.local, m_root); // TODO: compute the set of mutual dependencies first // (i.e. ">=0.0.1 <=0.0.5" and "<= 0.0.4" get ">=0.0.1 <=0.0.4") @@ -162,28 +164,28 @@ } /// Returns the DFLAGS - BuildSettings getBuildSettings(BuildPlatform platform, string config) + void addBuildSettings(ref BuildSettings dst, BuildPlatform platform, string config) const { - BuildSettings ret; - void addImportPath(string path, bool src) { if( !exists(path) ) return; - if( src ) ret.addImportDirs([path]); - else ret.addStringImportDirs([path]); + if( src ) dst.addImportDirs([path]); + else dst.addStringImportDirs([path]); } - if( m_main ) processVars(ret, ".", m_main.getBuildSettings(platform, config)); + if( m_main ) processVars(dst, ".", m_main.getBuildSettings(platform, config)); addImportPath("source", true); addImportPath("views", false); foreach( pkg; m_dependencies ){ - processVars(ret, pkg.path.toNativeString(), pkg.getBuildSettings(platform, config)); + processVars(dst, pkg.path.toNativeString(), pkg.getBuildSettings(platform, config)); addImportPath((pkg.path ~ "source").toNativeString(), true); addImportPath((pkg.path ~ "views").toNativeString(), false); } - return ret; + // add version identifiers for available packages + foreach(pack; this.installedPackages) + dst.addVersions(["Have_" ~ stripDlangSpecialChars(pack.name)]); } /// Actions which can be performed to update the application. @@ -200,7 +202,7 @@ 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); + actions ~= Action.failure(pkg, rdp.dependency, rdp.packages); return actions; } @@ -209,7 +211,7 @@ logDebug("Conflicts found"); Action[] actions; foreach( string pkg, dbp; conflicts) - actions ~= Action(Action.ActionId.Conflict, pkg, dbp.dependency, dbp.packages); + actions ~= Action.conflict(pkg, dbp.dependency, dbp.packages); return actions; } @@ -239,15 +241,16 @@ 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); + actions ~= Action.install(pkg, InstallLocation.projectLocal, d.dependency, d.packages); } else { logDebug("Required package '"~pkg~"' found with version '"~p.vers~"'"); if( option & UpdateOptions.Reinstall ) { - if( p.installLocation != InstallLocation.Local ){ + if( p.installLocation != InstallLocation.local ){ Dependency[string] em; - if( p.installLocation == InstallLocation.ProjectLocal ) - uninstalls ~= Action(Action.ActionId.Uninstall, *p, em); - actions ~= Action(Action.ActionId.InstallUpdate, pkg, d.dependency, d.packages); + // user and system packages are not uninstalled (could be needed by other projects) + if( p.installLocation == InstallLocation.projectLocal ) + uninstalls ~= Action.uninstall(*p, em); + actions ~= Action.install(pkg, p.installLocation, d.dependency, d.packages); } else { logInfo("Skipping local package %s at %s", p.name, p.path.toNativeString()); } @@ -259,10 +262,12 @@ } // Add uninstall actions - foreach( string pkg, p; unused ) { - logDebug("Superfluous package found: '"~pkg~"', version '"~p.vers~"'"); + foreach( pname, pkg; unused ){ + if( pkg.installLocation != InstallLocation.projectLocal ) + continue; + logDebug("Superfluous package found: '"~pname~"', version '"~pkg.vers~"'"); Dependency[string] em; - uninstalls ~= Action( Action.ActionId.Uninstall, pkg, new Dependency("==", p.vers), em); + uninstalls ~= Action.uninstall(pkg, em); } // Ugly "uninstall" comes first @@ -354,7 +359,7 @@ if( p ) logTrace("Found installed package %s %s", pkg, p.ver); // Try an already installed package first - if( p && p.installLocation != InstallLocation.Local && needsUpToDateCheck(pkg) ){ + if( p && p.installLocation != InstallLocation.local && needsUpToDateCheck(pkg) ){ logInfo("Triggering update of package %s", pkg); p = null; } @@ -410,9 +415,7 @@ if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString()); auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.CreateTrunc); scope(exit) dstFile.close(); - Appender!string js; - toPrettyJson(js, m_json); - dstFile.write( js.data ); + dstFile.writePrettyJsonString(m_json); } catch( Exception e ){ logWarn("Could not write .dub/dub.json."); } @@ -421,40 +424,62 @@ /// Actions to be performed by the dub struct Action { - enum ActionId { - InstallUpdate, - Uninstall, - Conflict, - Failure + enum Type { + install, + uninstall, + conflict, + failure } immutable { - ActionId action; + Type type; string packageId; + InstallLocation location; Dependency vers; } const Package pack; const Dependency[string] issuer; - this(ActionId id, string pkg, in Dependency d, Dependency[string] issue) + static Action install(string pkg, InstallLocation location, in Dependency dep, Dependency[string] context) { - action = id; - packageId = pkg; - vers = new immutable(Dependency)(d); - issuer = issue; + return Action(Type.install, pkg, location, dep, context); } - this(ActionId id, Package pkg, Dependency[string] issue) + static Action uninstall(Package pkg, Dependency[string] context) + { + return Action(Type.uninstall, pkg, context); + } + + static Action conflict(string pkg, in Dependency dep, Dependency[string] context) + { + return Action(Type.conflict, pkg, InstallLocation.projectLocal, dep, context); + } + + static Action failure(string pkg, in Dependency dep, Dependency[string] context) + { + return Action(Type.failure, pkg, InstallLocation.projectLocal, dep, context); + } + + private this(Type id, string pkg, InstallLocation location, in Dependency d, Dependency[string] issue) + { + this.type = id; + this.packageId = pkg; + this.location = location; + this.vers = new immutable(Dependency)(d); + this.issuer = issue; + } + + private this(Type id, Package pkg, Dependency[string] issue) { pack = pkg; - action = id; + type = id; packageId = pkg.name; vers = new immutable(Dependency)("==", pkg.vers); issuer = issue; } string toString() const { - return to!string(action) ~ ": " ~ packageId ~ ", " ~ to!string(vers); + return to!string(type) ~ ": " ~ packageId ~ ", " ~ to!string(vers); } } @@ -524,7 +549,17 @@ } } -private bool isIdentChar(char ch) +private bool isIdentChar(dchar ch) { return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_'; -} \ No newline at end of file +} + +private string stripDlangSpecialChars(string s) +{ + import std.array; + import std.uni; + auto ret = appender!string(); + foreach(ch; s) + ret.put(isIdentChar(ch) ? ch : '_'); + return ret.data; +} diff --git a/source/dub/registry.d b/source/dub/registry.d index 0b47578..7a7acb8 100644 --- a/source/dub/registry.d +++ b/source/dub/registry.d Binary files differ diff --git a/source/dub/utils.d b/source/dub/utils.d index d890bf4..8ea6c54 100644 --- a/source/dub/utils.d +++ b/source/dub/utils.d @@ -7,12 +7,10 @@ */ module dub.utils; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.url; -import vibe.stream.operations; -import vibe.utils.string; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports. import std.array; @@ -25,23 +23,23 @@ package bool isEmptyDir(Path p) { - foreach(DirEntry e; dirEntries(to!string(p), SpanMode.shallow)) + foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow)) return false; return true; } package Json jsonFromFile(Path file, bool silent_fail = false) { if( silent_fail && !existsFile(file) ) return Json.EmptyObject; - auto f = openFile(to!string(file), FileMode.Read); + auto f = openFile(file.toNativeString(), FileMode.Read); scope(exit) f.close(); auto text = stripUTF8Bom(cast(string)f.readAll()); return parseJson(text); } -package Json jsonFromZip(string zip, string filename) { +package Json jsonFromZip(Path zip, string filename) { auto f = openFile(zip, FileMode.Read); - ubyte[] b = new ubyte[cast(uint)f.leastSize]; - f.read(b); + ubyte[] b = new ubyte[cast(size_t)f.size]; + f.rawRead(b); f.close(); auto archive = new ZipArchive(b); auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename])); @@ -52,7 +50,7 @@ { auto f = openFile(path, FileMode.CreateTrunc); scope(exit) f.close(); - toPrettyJson(f, json); + f.writePrettyJsonString(json); } package bool isPathFromZip(string p) { @@ -64,4 +62,11 @@ if( !existsFile(path) ) return false; auto fi = getFileInfo(path); return fi.isDirectory; -} \ No newline at end of file +} + +private string stripUTF8Bom(string str) +{ + if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) + return str[3 ..$]; + return str; +} diff --git a/source/stdx/process.d b/source/stdx/process.d new file mode 100644 index 0000000..3dd7c1e --- /dev/null +++ b/source/stdx/process.d @@ -0,0 +1,1419 @@ +// Written in the D programming language. + +/** This is a proposal for a replacement for the $(D std._process) module. + + This is a summary of the functions in this module: + $(UL $(LI + $(LREF spawnProcess) spawns a new _process, optionally assigning it an + arbitrary set of standard input, output, and error streams. + The function returns immediately, leaving the child _process to execute + in parallel with its parent. All other functions in this module that + spawn processes are built around $(LREF spawnProcess).) + $(LI + $(LREF wait) makes the parent _process wait for a child _process to + terminate. In general one should always do this, to avoid + child _processes becoming "zombies" when the parent _process exits. + Scope guards are perfect for this – see the $(LREF spawnProcess) + documentation for examples.) + $(LI + $(LREF pipeProcess) and $(LREF pipeShell) also spawn a child _process + which runs in parallel with its parent. However, instead of taking + arbitrary streams, they automatically create a set of + pipes that allow the parent to communicate with the child + through the child's standard input, output, and/or error streams. + These functions correspond roughly to C's $(D popen) function.) + $(LI + $(LREF execute) and $(LREF shell) start a new _process and wait for it + to complete before returning. Additionally, they capture + the _process' standard output and error streams and return + the output of these as a string. + These correspond roughly to C's $(D system) function.) + ) + $(LREF shell) and $(LREF pipeShell) both run the given command + through the user's default command interpreter. On Windows, this is + the $(I cmd.exe) program, on POSIX it is determined by the SHELL environment + variable (defaulting to $(I /bin/sh) if it cannot be determined). The + command is specified as a single string which is sent directly to the + shell. + + The other commands all have two forms, one where the program name + and its arguments are specified in a single string parameter, separated + by spaces, and one where the arguments are specified as an array of + strings. Use the latter whenever the program name or any of the arguments + contain spaces. + + Unless a directory is specified in the program name, all functions will + search for the executable in the directories specified in the PATH + environment variable. + + Macros: + WIKI=Phobos/StdProcess +*/ +module stdx.process; + + +version(Posix) +{ + import core.stdc.errno; + import core.stdc.string; + import core.sys.posix.stdio; + import core.sys.posix.unistd; + import core.sys.posix.sys.wait; +} +version(Windows) +{ + import core.sys.windows.windows; + import std.utf; + import std.windows.syserror; + import core.stdc.stdio; + version(DigitalMars) + { + // this helps on Wine + version = PIPE_USE_ALT_FDOPEN; + } +} + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.path; +import std.stdio; +import std.string; +import std.typecons; + + +version(Posix) +{ + version(OSX) + { + // https://www.gnu.org/software/gnulib/manual/html_node/environ.html + private extern(C) char*** _NSGetEnviron(); + private __gshared char** environ; + + shared static this() + { + environ = *_NSGetEnviron(); + } + } + else + { + // Made available by the C runtime: + private extern(C) extern __gshared char** environ; + } +} +else version(Windows) +{ + // Use the same spawnProcess() implementations on both Windows + // and POSIX, only the spawnProcessImpl() function has to be + // different. + private __gshared LPVOID environ = null; + + extern(System) BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode); +} + + + + +/** A handle corresponding to a spawned process. */ +final class Pid +{ + /** The ID number assigned to the process by the operating + system. + */ + @property int processID() const + { + enforce(_processID >= 0, + "Pid doesn't correspond to a running process."); + return _processID; + } + + + // See module-level wait() for documentation. + version(Posix){ + int wait() + { + if (_processID == terminated) return _exitCode; + + int exitCode; + while(true) + { + int status; + auto check = waitpid(processID, &status, 0); + enforce (check != -1 || errno != ECHILD, + "Process does not exist or is not a child process."); + + if (WIFEXITED(status)) + { + exitCode = WEXITSTATUS(status); + break; + } + else if (WIFSIGNALED(status)) + { + exitCode = -WTERMSIG(status); + break; + } + // Process has stopped, but not terminated, so we continue waiting. + } + + // Mark Pid as terminated, and cache and return exit code. + _processID = terminated; + _exitCode = exitCode; + return exitCode; + } + + bool kill() + { + return .kill(_processID, SIGKILL) == 0; + } + } + else version(Windows) + { + int wait() + { + if (_processID == terminated) return _exitCode; + + if(_handle != INVALID_HANDLE_VALUE) + { + auto result = WaitForSingleObject(_handle, INFINITE); + enforce(result == WAIT_OBJECT_0, "Wait failed"); + // the process has exited, get the return code + enforce(GetExitCodeProcess(_handle, cast(LPDWORD)&_exitCode)); + CloseHandle(_handle); + _handle = INVALID_HANDLE_VALUE; + _processID = terminated; + } + return _exitCode; + } + + bool kill() + { + if(_handle == INVALID_HANDLE_VALUE) + return false; + return TerminateProcess(_handle, -1) != 0; + } + + ~this() + { + if(_handle != INVALID_HANDLE_VALUE) + { + CloseHandle(_handle); + _handle = INVALID_HANDLE_VALUE; + } + } + } + + +private: + + // Special values for _processID. + enum invalid = -1, terminated = -2; + + // OS process ID number. Only nonnegative IDs correspond to + // running processes. + int _processID = invalid; + + + // Exit code cached by wait(). This is only expected to hold a + // sensible value if _processID == terminated. + int _exitCode; + + + // Pids are only meant to be constructed inside this module, so + // we make the constructor private. + version(Windows) + { + HANDLE _handle; + this(int pid, HANDLE handle) + { + _processID = pid; + _handle = handle; + } + } + else + { + this(int id) + { + _processID = id; + } + } +} + + + + +/** Spawns a new process. + + This function returns immediately, and the child process + executes in parallel with its parent. + + Unless a directory is specified in the $(D _command) (or $(D name)) + parameter, this function will search the directories in the + PATH environment variable for the program. To run an executable in + the current directory, use $(D "./$(I executable_name)"). + + Params: + command = A string containing the program name and + its arguments, separated by spaces. If the program + name or any of the arguments contain spaces, use + the third or fourth form of this function, where + they are specified separately. + + environmentVars = The environment variables for the + child process can be specified using this parameter. + If it is omitted, the child process executes in the + same environment as the parent process. + + stdin_ = The standard input stream of the child process. + This can be any $(XREF stdio,File) that is opened for reading. + By default the child process inherits the parent's input + stream. + + stdout_ = The standard output stream of the child process. + This can be any $(XREF stdio,File) that is opened for writing. + By default the child process inherits the parent's output + stream. + + stderr_ = The standard error stream of the child process. + This can be any $(XREF stdio,File) that is opened for writing. + By default the child process inherits the parent's error + stream. + + config = Options controlling the behaviour of $(D spawnProcess). + See the $(LREF Config) documentation for details. + + name = The name of the executable file. + + args = The _command line arguments to give to the program. + (There is no need to specify the program name as the + zeroth argument; this is done automatically.) + + Note: + If you pass an $(XREF stdio,File) object that is $(I not) one of the standard + input/output/error streams of the parent process, that stream + will by default be closed in the parent process when this + function returns. See the $(LREF Config) documentation below for information + about how to disable this behaviour. + + Examples: + Open Firefox on the D homepage and wait for it to complete: + --- + auto pid = spawnProcess("firefox http://www.d-programming-language.org"); + wait(pid); + --- + Use the $(I ls) _command to retrieve a list of files: + --- + string[] files; + auto p = pipe(); + + auto pid = spawnProcess("ls", stdin, p.writeEnd); + scope(exit) wait(pid); + + foreach (f; p.readEnd.byLine()) files ~= f.idup; + --- + Use the $(I ls -l) _command to get a list of files, pipe the output + to $(I grep) and let it filter out all files except D source files, + and write the output to the file $(I dfiles.txt): + --- + // Let's emulate the command "ls -l | grep \.d > dfiles.txt" + auto p = pipe(); + auto file = File("dfiles.txt", "w"); + + auto lsPid = spawnProcess("ls -l", stdin, p.writeEnd); + scope(exit) wait(lsPid); + + auto grPid = spawnProcess("grep \\.d", p.readEnd, file); + scope(exit) wait(grPid); + --- + Open a set of files in OpenOffice Writer, and make it print + any error messages to the standard output stream. Note that since + the filenames contain spaces, we have to pass them in an array: + --- + spawnProcess("oowriter", ["my document.odt", "your document.odt"], + stdin, stdout, stdout); + --- +*/ +Pid spawnProcess(string command, + File stdin_ = std.stdio.stdin, + File stdout_ = std.stdio.stdout, + File stderr_ = std.stdio.stderr, + Config config = Config.none) +{ + auto splitCmd = split(command); + return spawnProcessImpl(splitCmd[0], splitCmd[1 .. $], + environ, + stdin_, stdout_, stderr_, config); +} + + +/// ditto +Pid spawnProcess(string command, string[string] environmentVars, + File stdin_ = std.stdio.stdin, + File stdout_ = std.stdio.stdout, + File stderr_ = std.stdio.stderr, + Config config = Config.none) +{ + auto splitCmd = split(command); + return spawnProcessImpl(splitCmd[0], splitCmd[1 .. $], + toEnvz(environmentVars), + stdin_, stdout_, stderr_, config); +} + + +/// ditto +Pid spawnProcess(string name, const string[] args, + File stdin_ = std.stdio.stdin, + File stdout_ = std.stdio.stdout, + File stderr_ = std.stdio.stderr, + Config config = Config.none) +{ + return spawnProcessImpl(name, args, + environ, + stdin_, stdout_, stderr_, config); +} + + +/// ditto +Pid spawnProcess(string name, const string[] args, + string[string] environmentVars, + File stdin_ = std.stdio.stdin, + File stdout_ = std.stdio.stdout, + File stderr_ = std.stdio.stderr, + Config config = Config.none) +{ + return spawnProcessImpl(name, args, + toEnvz(environmentVars), + stdin_, stdout_, stderr_, config); +} + + +// The actual implementation of the above. +version(Posix) private Pid spawnProcessImpl + (string name, const string[] args, const char** envz, + File stdin_, File stdout_, File stderr_, Config config) +{ + // Make sure the file exists and is executable. + if (any!isDirSeparator(name)) + { + enforce(isExecutable(name), "Not an executable file: "~name); + } + else + { + name = searchPathFor(name); + enforce(name != null, "Executable file not found: "~name); + } + + // Get the file descriptors of the streams. + auto stdinFD = core.stdc.stdio.fileno(stdin_.getFP()); + errnoEnforce(stdinFD != -1, "Invalid stdin stream"); + auto stdoutFD = core.stdc.stdio.fileno(stdout_.getFP()); + errnoEnforce(stdoutFD != -1, "Invalid stdout stream"); + auto stderrFD = core.stdc.stdio.fileno(stderr_.getFP()); + errnoEnforce(stderrFD != -1, "Invalid stderr stream"); + + auto namez = toStringz(name); + auto argz = toArgz(name, args); + + auto id = fork(); + errnoEnforce (id >= 0, "Cannot spawn new process"); + + if (id == 0) + { + // Child process + + // Redirect streams and close the old file descriptors. + // In the case that stderr is redirected to stdout, we need + // to backup the file descriptor since stdout may be redirected + // as well. + if (stderrFD == STDOUT_FILENO) stderrFD = dup(stderrFD); + dup2(stdinFD, STDIN_FILENO); + dup2(stdoutFD, STDOUT_FILENO); + dup2(stderrFD, STDERR_FILENO); + + // Close the old file descriptors, unless they are + // either of the standard streams. + if (stdinFD > STDERR_FILENO) close(stdinFD); + if (stdoutFD > STDERR_FILENO) close(stdoutFD); + if (stderrFD > STDERR_FILENO) close(stderrFD); + + // Execute program + execve(namez, argz, envz); + + // If execution fails, exit as quick as possible. + perror("spawnProcess(): Failed to execute program"); + _exit(1); + assert (0); + } + else + { + // Parent process: Close streams and return. + + with (Config) + { + if (stdinFD > STDERR_FILENO && !(config & noCloseStdin)) + stdin_.close(); + if (stdoutFD > STDERR_FILENO && !(config & noCloseStdout)) + stdout_.close(); + if (stderrFD > STDERR_FILENO && !(config & noCloseStderr)) + stderr_.close(); + } + + return new Pid(id); + } +} +else version(Windows) private Pid spawnProcessImpl + (string name, const string[] args, LPVOID envz, + File stdin_, File stdout_, File stderr_, Config config) +{ + // Create a process info structure. Note that we don't care about wide + // characters yet. + STARTUPINFO startinfo; + startinfo.cb = startinfo.sizeof; + + // Create a process information structure. + PROCESS_INFORMATION pi; + + // + // Windows is a little strange when passing command line. It requires the + // command-line to be one single command line, and the quoting processing + // is rather bizzare. Through trial and error, here are the rules I've + // discovered that Windows uses to parse the command line WRT quotes: + // + // inside or outside quote mode: + // 1. if 2 or more backslashes are followed by a quote, the first + // 2 backslashes are reduced to 1 backslash which does not + // affect anything after it. + // 2. one backslash followed by a quote is interpreted as a + // literal quote, which cannot be used to close quote mode, and + // does not affect anything after it. + // + // outside quote mode: + // 3. a quote enters quote mode + // 4. whitespace delineates an argument + // + // inside quote mode: + // 5. 2 quotes sequentially are interpreted as a literal quote and + // an exit from quote mode. + // 6. a quote at the end of the string, or one that is followed by + // anything other than a quote exits quote mode, but does not + // affect the character after the quote. + // 7. end of line exits quote mode + // + // In our 'reverse' routine, we will only utilize the first 2 rules + // for escapes. + // + char[] cmdline; + uint minsize = 0; + foreach(s; args) + minsize += args.length; + + // reserve enough space to hold the program and all the arguments, plus 3 + // extra characters per arg for the quotes and the space, plus 5 extra + // chars for good measure (in case we have to add escaped quotes). + cmdline.reserve(minsize + name.length + 3 * args.length + 5); + + // this could be written more optimized... + void addArg(string a) + { + if(cmdline.length) + cmdline ~= " "; + // first, determine if we need a quote + bool needquote = false; + foreach(dchar d; a) + if(d == ' ') + { + needquote = true; + break; + } + if(needquote) + cmdline ~= '"'; + foreach(dchar d; a) + { + if(d == '"') + cmdline ~= '\\'; + cmdline ~= d; + } + if(needquote) + cmdline ~= '"'; + } + + addArg(name); + foreach(a; args) + addArg(a); + + cmdline ~= '\0'; + + // ok, the command line is ready. Figure out the startup info + startinfo.dwFlags = STARTF_USESTDHANDLES; + // Get the file descriptors of the streams. + auto stdinFD = _fileno(stdin_.getFP()); + errnoEnforce(stdinFD != -1, "Invalid stdin stream"); + auto stdoutFD = _fileno(stdout_.getFP()); + errnoEnforce(stdoutFD != -1, "Invalid stdout stream"); + auto stderrFD = _fileno(stderr_.getFP()); + errnoEnforce(stderrFD != -1, "Invalid stderr stream"); + + // need to convert file descriptors to HANDLEs + startinfo.hStdInput = _fdToHandle(stdinFD); + startinfo.hStdOutput = _fdToHandle(stdoutFD); + startinfo.hStdError = _fdToHandle(stderrFD); + + // TODO: need to fix this for unicode + if(!CreateProcessA(null, cmdline.ptr, null, null, true, (config & Config.gui) ? CREATE_NO_WINDOW : 0, envz, null, &startinfo, &pi)) + { + throw new Exception("Error starting process: " ~ sysErrorString(GetLastError()), __FILE__, __LINE__); + } + + // figure out if we should close any of the streams + with (Config) + { + if (stdinFD > STDERR_FILENO && !(config & noCloseStdin)) + stdin_.close(); + if (stdoutFD > STDERR_FILENO && !(config & noCloseStdout)) + stdout_.close(); + if (stderrFD > STDERR_FILENO && !(config & noCloseStderr)) + stderr_.close(); + } + + // close the thread handle in the process info structure + CloseHandle(pi.hThread); + + return new Pid(pi.dwProcessId, pi.hProcess); +} + +// Searches the PATH variable for the given executable file, +// (checking that it is in fact executable). +version(Posix) private string searchPathFor(string executable) +{ + auto pathz = environment["PATH"]; + if (pathz == null) return null; + + foreach (dir; splitter(to!string(pathz), ':')) + { + auto execPath = buildPath(dir, executable); + if (isExecutable(execPath)) return execPath; + } + + return null; +} + +// Converts a C array of C strings to a string[] array, +// setting the program name as the zeroth element. +version(Posix) private const(char)** toArgz(string prog, const string[] args) +{ + alias const(char)* stringz_t; + auto argz = new stringz_t[](args.length+2); + + argz[0] = toStringz(prog); + foreach (i; 0 .. args.length) + { + argz[i+1] = toStringz(args[i]); + } + argz[$-1] = null; + return argz.ptr; +} + +// Converts a string[string] array to a C array of C strings +// on the form "key=value". +version(Posix) private const(char)** toEnvz(const string[string] env) +{ + alias const(char)* stringz_t; + auto envz = new stringz_t[](env.length+1); + int i = 0; + foreach (k, v; env) + { + envz[i] = (k~'='~v~'\0').ptr; + i++; + } + envz[$-1] = null; + return envz.ptr; +} +else version(Windows) private LPVOID toEnvz(const string[string] env) +{ + uint len = 1; // reserve 1 byte for termination of environment block + foreach(k, v; env) + { + len += k.length + v.length + 2; // one for '=', one for null char + } + + char [] envz; + envz.reserve(len); + foreach(k, v; env) + { + envz ~= k ~ '=' ~ v ~ '\0'; + } + + envz ~= '\0'; + return envz.ptr; +} + + +// Checks whether the file exists and can be executed by the +// current user. +version(Posix) private bool isExecutable(string path) +{ + return (access(toStringz(path), X_OK) == 0); +} + + + + +/** Flags that control the behaviour of $(LREF spawnProcess). + Use bitwise OR to combine flags. + + Example: + --- + auto logFile = File("myapp_error.log", "w"); + + // Start program in a console window (Windows only), redirect + // its error stream to logFile, and leave logFile open in the + // parent process as well. + auto pid = spawnProcess("myapp", stdin, stdout, logFile, + Config.noCloseStderr | Config.gui); + scope(exit) + { + auto exitCode = wait(pid); + logFile.writeln("myapp exited with code ", exitCode); + logFile.close(); + } + --- +*/ +enum Config +{ + none = 0, + + /** Unless the child process inherits the standard + input/output/error streams of its parent, one almost + always wants the streams closed in the parent when + $(LREF spawnProcess) returns. Therefore, by default, this + is done. If this is not desirable, pass any of these + options to spawnProcess. + */ + noCloseStdin = 1, + noCloseStdout = 2, /// ditto + noCloseStderr = 4, /// ditto + + /** On Windows, this option causes the process to run in + a console window. On POSIX it has no effect. + */ + gui = 8, +} + + + + +/** Waits for a specific spawned process to terminate and returns + its exit status. + + In general one should always _wait for child processes to terminate + before exiting the parent process. Otherwise, they may become + "$(WEB en.wikipedia.org/wiki/Zombie_process,zombies)" – processes + that are defunct, yet still occupy a slot in the OS process table. + + Note: + On POSIX systems, if the process is terminated by a signal, + this function returns a negative number whose absolute value + is the signal number. (POSIX restricts normal exit codes + to the range 0-255.) + + Examples: + See the $(LREF spawnProcess) documentation. +*/ +int wait(Pid pid) +{ + enforce(pid !is null, "Called wait on a null Pid."); + return pid.wait(); +} + + +// BIG HACK: works around the use of a private File() contrctor in pipe() +private File encapPipeAsFile(FILE* fil) +{ + static struct Impl + { + FILE * handle = null; // Is null iff this Impl is closed by another File + uint refs = uint.max / 2; + bool isPipe; + } + auto f = File.wrapFile(fil); + auto imp = *cast(Impl**)&f; + imp.refs = 1; + imp.isPipe = true; + return f; +} + +/** Creates a unidirectional _pipe. + + Data is written to one end of the _pipe and read from the other. + --- + auto p = pipe(); + p.writeEnd.writeln("Hello World"); + assert (p.readEnd.readln().chomp() == "Hello World"); + --- + Pipes can, for example, be used for interprocess communication + by spawning a new process and passing one end of the _pipe to + the child, while the parent uses the other end. See the + $(LREF spawnProcess) documentation for examples of this. +*/ +version(Posix) Pipe pipe() +{ + int[2] fds; + errnoEnforce(core.sys.posix.unistd.pipe(fds) == 0, + "Unable to create pipe"); + + Pipe p; + + p._read = encapPipeAsFile(errnoEnforce(fdopen(fds[0], "r"), "Cannot open read end of pipe")); + p._write = encapPipeAsFile(errnoEnforce(fdopen(fds[1], "w"), "Cannot open write end of pipe")); + + return p; +} +else version(Windows) Pipe pipe() +{ + // use CreatePipe to create an anonymous pipe + HANDLE readHandle; + HANDLE writeHandle; + SECURITY_ATTRIBUTES sa; + sa.nLength = sa.sizeof; + sa.lpSecurityDescriptor = null; + sa.bInheritHandle = true; + if(!CreatePipe(&readHandle, &writeHandle, &sa, 0)) + { + throw new Exception("Error creating pipe: " ~ sysErrorString(GetLastError()), __FILE__, __LINE__); + } + + // Create file descriptors from the handles + auto readfd = _handleToFD(readHandle, FHND_DEVICE); + auto writefd = _handleToFD(writeHandle, FHND_DEVICE); + + Pipe p; + version(PIPE_USE_ALT_FDOPEN) + { + // This is a re-implementation of DMC's fdopen, but without the + // mucking with the file descriptor. POSIX standard requires the + // new fdopen'd file to retain the given file descriptor's + // position. + FILE * local_fdopen(int fd, const(char)* mode) + { + auto fp = core.stdc.stdio.fopen("NUL", mode); + if(!fp) + return null; + FLOCK(fp); + auto iob = cast(_iobuf*)fp; + .close(iob._file); + iob._file = fd; + iob._flag &= ~_IOTRAN; + FUNLOCK(fp); + return fp; + } + + p._read = encapPipeAsFile(errnoEnforce(local_fdopen(readfd, "r"), "Cannot open read end of pipe")); + p._write = encapPipeAsFile(errnoEnforce(local_fdopen(writefd, "a"), "Cannot open write end of pipe")); + } + else + { + p._read = encapPipeAsFile(errnoEnforce(fdopen(readfd, "r"), "Cannot open read end of pipe")); + p._write = encapPipeAsFile(errnoEnforce(fdopen(writefd, "a"), "Cannot open write end of pipe")); + } + + return p; +} + + +/// ditto +struct Pipe +{ + /** The read end of the pipe. */ + @property File readEnd() { return _read; } + + + /** The write end of the pipe. */ + @property File writeEnd() { return _write; } + + + /** Closes both ends of the pipe. + + Normally it is not necessary to do this manually, as $(XREF stdio,File) + objects are automatically closed when there are no more references + to them. + + Note that if either end of the pipe has been passed to a child process, + it will only be closed in the parent process. + */ + void close() + { + _read.close(); + _write.close(); + } + + +private: + File _read, _write; +} + + +/*unittest +{ + auto p = pipe(); + p.writeEnd.writeln("Hello World"); + p.writeEnd.flush(); + assert (p.readEnd.readln().chomp() == "Hello World"); +}*/ + + + + +// ============================== pipeProcess() ============================== + + +/** Starts a new process, creating pipes to redirect its standard + input, output and/or error streams. + + These functions return immediately, leaving the child process to + execute in parallel with the parent. + $(LREF pipeShell) invokes the user's _command interpreter + to execute the given program or _command. + + Example: + --- + auto pipes = pipeProcess("my_application"); + + // Store lines of output. + string[] output; + foreach (line; pipes.stdout.byLine) output ~= line.idup; + + // Store lines of errors. + string[] errors; + foreach (line; pipes.stderr.byLine) errors ~= line.idup; + --- +*/ +ProcessPipes pipeProcess(string command, + Redirect redirectFlags = Redirect.all) +{ + auto splitCmd = split(command); + return pipeProcess(splitCmd[0], splitCmd[1 .. $], redirectFlags); +} + + +/// ditto +ProcessPipes pipeProcess(string name, string[] args, + Redirect redirectFlags = Redirect.all) +{ + File stdinFile, stdoutFile, stderrFile; + + ProcessPipes pipes; + pipes._redirectFlags = redirectFlags; + + if (redirectFlags & Redirect.stdin) + { + auto p = pipe(); + stdinFile = p.readEnd; + pipes._stdin = p.writeEnd; + } + else + { + stdinFile = std.stdio.stdin; + } + + if (redirectFlags & Redirect.stdout) + { + enforce((redirectFlags & Redirect.stdoutToStderr) == 0, + "Invalid combination of options: Redirect.stdout | " + ~"Redirect.stdoutToStderr"); + auto p = pipe(); + stdoutFile = p.writeEnd; + pipes._stdout = p.readEnd; + } + else + { + stdoutFile = std.stdio.stdout; + } + + if (redirectFlags & Redirect.stderr) + { + enforce((redirectFlags & Redirect.stderrToStdout) == 0, + "Invalid combination of options: Redirect.stderr | " + ~"Redirect.stderrToStdout"); + auto p = pipe(); + stderrFile = p.writeEnd; + pipes._stderr = p.readEnd; + } + else + { + stderrFile = std.stdio.stderr; + } + + if (redirectFlags & Redirect.stdoutToStderr) + { + if (redirectFlags & Redirect.stderrToStdout) + { + // We know that neither of the other options have been + // set, so we assign the std.stdio.std* streams directly. + stdoutFile = std.stdio.stderr; + stderrFile = std.stdio.stdout; + } + else + { + stdoutFile = stderrFile; + } + } + else if (redirectFlags & Redirect.stderrToStdout) + { + stderrFile = stdoutFile; + } + + pipes._pid = spawnProcess(name, args, stdinFile, stdoutFile, stderrFile); + return pipes; +} + + +/// ditto +ProcessPipes pipeShell(string command, Redirect redirectFlags = Redirect.all) +{ + return pipeProcess(getShell(), [shellSwitch, command], redirectFlags); +} + + + + +/** Flags that can be passed to $(LREF pipeProcess) and $(LREF pipeShell) + to specify which of the child process' standard streams are redirected. + Use bitwise OR to combine flags. +*/ +enum Redirect +{ + none = 0, + + /** Redirect the standard input, output or error streams, respectively. */ + stdin = 1, + stdout = 2, /// ditto + stderr = 4, /// ditto + all = stdin | stdout | stderr, /// ditto + + /** Redirect the standard error stream into the standard output + stream, and vice versa. + */ + stderrToStdout = 8, + stdoutToStderr = 16, /// ditto +} + + + + +/** Object containing $(XREF stdio,File) handles that allow communication with + a child process through its standard streams. +*/ +struct ProcessPipes +{ + /** Returns the $(LREF Pid) of the child process. */ + @property Pid pid() + { + enforce (_pid !is null); + return _pid; + } + + + /** Returns an $(XREF stdio,File) that allows writing to the child process' + standard input stream. + */ + @property File stdin() + { + enforce ((_redirectFlags & Redirect.stdin) > 0, + "Child process' standard input stream hasn't been redirected."); + return _stdin; + } + + + /** Returns an $(XREF stdio,File) that allows reading from the child + process' standard output/error stream. + */ + @property File stdout() + { + enforce ((_redirectFlags & Redirect.stdout) > 0, + "Child process' standard output stream hasn't been redirected."); + return _stdout; + } + + /// ditto + @property File stderr() + { + enforce ((_redirectFlags & Redirect.stderr) > 0, + "Child process' standard error stream hasn't been redirected."); + return _stderr; + } + + +private: + + Redirect _redirectFlags; + Pid _pid; + File _stdin, _stdout, _stderr; +} + + + + +// ============================== execute() ============================== + + +/** Executes the given program and returns its exit code and output. + + This function blocks until the program terminates. + The $(D output) string includes what the program writes to its + standard error stream as well as its standard output stream. + --- + auto dmd = execute("dmd myapp.d"); + if (dmd.status != 0) writeln("Compilation failed:\n", dmd.output); + --- +*/ +Tuple!(int, "status", string, "output") execute(string command) +{ + auto p = pipeProcess(command, + Redirect.stdout | Redirect.stderrToStdout); + + Appender!(ubyte[]) a; + foreach (ubyte[] chunk; p.stdout.byChunk(4096)) a.put(chunk); + + typeof(return) r; + r.output = cast(string) a.data; + r.status = wait(p.pid); + return r; +} + + +/// ditto +Tuple!(int, "status", string, "output") execute(string name, string[] args...) +{ + auto p = pipeProcess(name, args, + Redirect.stdout | Redirect.stderrToStdout); + + Appender!(ubyte[]) a; + foreach (ubyte[] chunk; p.stdout.byChunk(4096)) a.put(chunk); + + typeof(return) r; + r.output = cast(string) a.data; + r.status = wait(p.pid); + return r; +} + + + + +// ============================== shell() ============================== + + +version(Posix) private immutable string shellSwitch = "-c"; +version(Windows) private immutable string shellSwitch = "/C"; + + +// Gets the user's default shell. +version(Posix) private string getShell() +{ + return environment.get("SHELL", "/bin/sh"); +} + +version(Windows) private string getShell() +{ + return "cmd.exe"; +} + + + + +/** Executes $(D _command) in the user's default _shell and returns its + exit code and output. + + This function blocks until the command terminates. + The $(D output) string includes what the command writes to its + standard error stream as well as its standard output stream. + --- + auto ls = shell("ls -l"); + writefln("ls exited with code %s and said: %s", ls.status, ls.output); + --- +*/ +Tuple!(int, "status", string, "output") shell(string command) +{ + version(Windows) + return execute(getShell() ~ " " ~ shellSwitch ~ " " ~ command); + else version(Posix) + return execute(getShell(), shellSwitch, command); + else assert(0); +} + + + + +// ============================== thisProcessID ============================== + + +/** Returns the process ID number of the current process. */ +version(Posix) @property int thisProcessID() +{ + return getpid(); +} + +version(Windows) @property int thisProcessID() +{ + return GetCurrentProcessId(); +} + + + + +// ============================== environment ============================== + + +/** Manipulates environment variables using an associative-array-like + interface. + + Examples: + --- + // Return variable, or throw an exception if it doesn't exist. + string path = environment["PATH"]; + + // Add/replace variable. + environment["foo"] = "bar"; + + // Remove variable. + environment.remove("foo"); + + // Return variable, or null if it doesn't exist. + string foo = environment.get("foo"); + + // Return variable, or a default value if it doesn't exist. + string foo = environment.get("foo", "default foo value"); + + // Return an associative array containing all the environment variables. + string[string] aa = environment.toAA(); + --- +*/ +alias Environment environment; + +abstract final class Environment +{ +static: + + // Retrieves an environment variable, throws on failure. + string opIndex(string name) + { + string value; + enforce(getImpl(name, value), "Environment variable not found: "~name); + return value; + } + + + + // Assigns a value to an environment variable. If the variable + // exists, it is overwritten. + string opIndexAssign(string value, string name) + { + version(Posix) + { + if (core.sys.posix.stdlib.setenv(toStringz(name), + toStringz(value), 1) != -1) + { + return value; + } + + // The default errno error message is very uninformative + // in the most common case, so we handle it manually. + enforce(errno != EINVAL, + "Invalid environment variable name: '"~name~"'"); + errnoEnforce(false, + "Failed to add environment variable"); + assert(0); + } + + else version(Windows) + { + enforce( + SetEnvironmentVariableW(toUTF16z(name), toUTF16z(value)), + sysErrorString(GetLastError()) + ); + return value; + } + + else static assert(0); + } + + + + // Removes an environment variable. The function succeeds even + // if the variable isn't in the environment. + void remove(string name) + { + version(Posix) + { + core.sys.posix.stdlib.unsetenv(toStringz(name)); + } + + else version(Windows) + { + SetEnvironmentVariableW(toUTF16z(name), null); + } + + else static assert(0); + } + + + + // Same as opIndex, except it returns a default value if + // the variable doesn't exist. + string get(string name, string defaultValue = null) + { + string value; + auto found = getImpl(name, value); + return found ? value : defaultValue; + } + + + + // Returns all environment variables in an associative array. + // Environment variable of zero-length is not retrieved. + string[string] toAA() + { + string[string] aa; + + version(Posix) + { + for (int i=0; environ[i] != null; ++i) + { + immutable varDef = to!string(environ[i]); + immutable eq = std.string.indexOf(varDef, '='); + assert (eq >= 0); + + immutable name = varDef[0 .. eq]; + if (!name.length) + continue; + + immutable value = varDef[eq+1 .. $]; + + // In POSIX, environment variables may be defined more + // than once. This is a security issue, which we avoid + // by checking whether the key already exists in the array. + // For more info: + // http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/environment-variables.html + if (name !in aa) aa[name] = value; + } + } + else version(Windows) + { + auto envBlock = GetEnvironmentStringsW(); + enforce (envBlock, "Failed to retrieve environment variables."); + scope(exit) FreeEnvironmentStringsW(envBlock); + + for (int i=0; envBlock[i] != '\0'; ++i) + { + auto start = i; + while (envBlock[i] != '=') + { + assert (envBlock[i] != '\0'); + ++i; + } + immutable name = toUTF8(envBlock[start .. i]); + + start = i+1; + while (envBlock[i] != '\0') ++i; + if (name.length) + aa[name] = toUTF8(envBlock[start .. i]); + } + } + + else static assert(0); + + return aa; + } + + +private: + + // Returns the length of an environment variable (in number of + // wchars, including the null terminator), or 0 if it doesn't exist. + version(Windows) + int varLength(LPCWSTR namez) + { + return GetEnvironmentVariableW(namez, null, 0); + } + + + // Retrieves the environment variable, returns false on failure. + // Environment variable with zero-length name sets an empty value. + bool getImpl(string name, out string value) + { + if (!name.length) + return true; // return empty + + version(Posix) + { + const vz = core.sys.posix.stdlib.getenv(toStringz(name)); + if (vz == null) return false; + auto v = vz[0 .. strlen(vz)]; + + // Cache the last call's result. + static string lastResult; + if (v != lastResult) lastResult = v.idup; + value = lastResult; + return true; + } + + else version(Windows) + { + const namez = toUTF16z(name); + immutable len = varLength(namez); + if (len == 0) return false; + if (len == 1) return true; + + auto buf = new WCHAR[len]; + GetEnvironmentVariableW(namez, buf.ptr, buf.length); + value = toUTF8(buf[0 .. $-1]); + return true; + } + + else static assert(0); + } +} + + +/*unittest +{ + // New variable + environment["std_process"] = "foo"; + assert (environment["std_process"] == "foo"); + + // Set variable again + environment["std_process"] = "bar"; + assert (environment["std_process"] == "bar"); + + // Remove variable + environment.remove("std_process"); + + // Remove again, should succeed + environment.remove("std_process"); + + // Throw on not found. + try { environment["std_process"]; assert(0); } catch(Exception e) { } + + // get() without default value + assert (environment.get("std.process") == null); + + // get() with default value + assert (environment.get("std_process", "baz") == "baz"); + + // Convert to associative array + auto aa = environment.toAA(); + assert (aa.length > 0); + foreach (n, v; aa) + { + // Wine has some bugs related to environment variables: + // - Wine allows the existence of an env. variable with the name + // "\0", but GetEnvironmentVariable refuses to retrieve it. + // - If an env. variable has zero length, i.e. is "\0", + // GetEnvironmentVariable should return 1. Instead it returns + // 0, indicating the variable doesn't exist. + version(Windows) if (n.length == 0 || v.length == 0) continue; + + import std.string; + auto rhs = environment[n]; + assert (v == rhs, format("key %s -- '%s' != '%s'", n, v, rhs)); + } +}*/ diff --git a/source/vibecompat/core/file.d b/source/vibecompat/core/file.d new file mode 100644 index 0000000..6d3da32 --- /dev/null +++ b/source/vibecompat/core/file.d @@ -0,0 +1,291 @@ +/** + File handling. + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.core.file; + +public import vibecompat.inet.url; +public import std.stdio; + +import vibecompat.core.log; + +import std.conv; +import std.c.stdio; +import std.datetime; +import std.exception; +import std.file; +import std.path; +import std.string; +import std.utf; + + +version(Posix){ + private extern(C) int mkstemps(char* templ, int suffixlen); +} + + +/* Add output range support to File +*/ +struct RangeFile { + File file; + alias file this; + + void put(in ubyte[] bytes) { file.rawWrite(bytes); } + void put(in char[] str) { put(cast(ubyte[])str); } + void put(char ch) { put((&ch)[0 .. 1]); } + void put(dchar ch) { char[4] chars; put(chars[0 .. encode(chars, ch)]); } + + ubyte[] readAll() + { + file.seek(0, SEEK_END); + auto sz = file.tell(); + enforce(sz <= size_t.max, "File is too big to read to memory."); + file.seek(0, SEEK_SET); + auto ret = new ubyte[cast(size_t)sz]; + return file.rawRead(ret); + } +} + + +/** + Opens a file stream with the specified mode. +*/ +RangeFile openFile(Path path, FileMode mode = FileMode.Read) +{ + string strmode; + final switch(mode){ + case FileMode.Read: strmode = "rb"; break; + case FileMode.ReadWrite: strmode = "rb+"; break; + case FileMode.CreateTrunc: strmode = "wb+"; break; + case FileMode.Append: strmode = "ab"; break; + } + auto ret = File(path.toNativeString(), strmode); + assert(ret.isOpen()); + return RangeFile(ret); +} +/// ditto +RangeFile openFile(string path, FileMode mode = FileMode.Read) +{ + return openFile(Path(path), mode); +} + +/** + Creates and opens a temporary file for writing. +*/ +RangeFile createTempFile(string suffix = null) +{ + version(Windows){ + char[L_tmpnam] tmp; + tmpnam(tmp.ptr); + auto tmpname = to!string(tmp.ptr); + if( tmpname.startsWith("\\") ) tmpname = tmpname[1 .. $]; + tmpname ~= suffix; + logDebug("tmp %s", tmpname); + return openFile(tmpname, FileMode.CreateTrunc); + } else { + import core.sys.posix.stdio; + enum pattern ="/tmp/vtmp.XXXXXX"; + scope templ = new char[pattern.length+suffix.length+1]; + templ[0 .. pattern.length] = pattern; + templ[pattern.length .. $-1] = suffix; + templ[$-1] = '\0'; + assert(suffix.length <= int.max); + auto fd = mkstemps(templ.ptr, cast(int)suffix.length); + enforce(fd >= 0, "Failed to create temporary file."); + auto ret = File.wrapFile(fdopen(fd, "wb+")); + return RangeFile(ret); + } +} + +/** + Moves or renames a file. +*/ +void moveFile(Path from, Path to) +{ + moveFile(from.toNativeString(), to.toNativeString()); +} +/// ditto +void moveFile(string from, string to) +{ + std.file.rename(from, to); +} + +/** + Copies a file. + + Note that attributes and time stamps are currently not retained. + + Params: + from = Path of the source file + to = Path for the destination file + overwrite = If true, any file existing at the destination path will be + overwritten. If this is false, an excpetion will be thrown should + a file already exist at the destination path. + + Throws: + An Exception if the copy operation fails for some reason. +*/ +void copyFile(Path from, Path to, bool overwrite = false) +{ + enforce(overwrite || !existsFile(to), "Destination file already exists."); + .copy(from.toNativeString(), to.toNativeString()); +} +/// ditto +void copyFile(string from, string to) +{ + copyFile(Path(from), Path(to)); +} + +/** + Removes a file +*/ +void removeFile(Path path) +{ + removeFile(path.toNativeString()); +} +/// ditto +void removeFile(string path) { + std.file.remove(path); +} + +/** + Checks if a file exists +*/ +bool existsFile(Path path) { + return existsFile(path.toNativeString()); +} +/// ditto +bool existsFile(string path) +{ + return std.file.exists(path); +} + +/** Stores information about the specified file/directory into 'info' + + Returns false if the file does not exist. +*/ +FileInfo getFileInfo(Path path) +{ + auto ent = std.file.dirEntry(path.toNativeString()); + return makeFileInfo(ent); +} +/// ditto +FileInfo getFileInfo(string path) +{ + return getFileInfo(Path(path)); +} + +/** + Creates a new directory. +*/ +void createDirectory(Path path) +{ + mkdir(path.toNativeString()); +} +/// ditto +void createDirectory(string path) +{ + createDirectory(Path(path)); +} + +/** + Enumerates all files in the specified directory. +*/ +void listDirectory(Path path, scope bool delegate(FileInfo info) del) +{ + foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) ) + if( !del(makeFileInfo(ent)) ) + break; +} +/// ditto +void listDirectory(string path, scope bool delegate(FileInfo info) del) +{ + listDirectory(Path(path), del); +} +/// ditto +int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path) +{ + int iterator(scope int delegate(ref FileInfo) del){ + int ret = 0; + listDirectory(path, (fi){ + ret = del(fi); + return ret == 0; + }); + return ret; + } + return &iterator; +} +/// ditto +int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path) +{ + return iterateDirectory(Path(path)); +} + + +/** + Returns the current working directory. +*/ +Path getWorkingDirectory() +{ + return Path(std.file.getcwd()); +} + + +/** Contains general information about a file. +*/ +struct FileInfo { + /// Name of the file (not including the path) + string name; + + /// Size of the file (zero for directories) + ulong size; + + /// Time of the last modification + SysTime timeModified; + + /// Time of creation (not available on all operating systems/file systems) + SysTime timeCreated; + + /// True if this is a symlink to an actual file + bool isSymlink; + + /// True if this is a directory or a symlink pointing to a directory + bool isDirectory; +} + +/** + Specifies how a file is manipulated on disk. +*/ +enum FileMode { + /// The file is opened read-only. + Read, + /// The file is opened for read-write random access. + ReadWrite, + /// The file is truncated if it exists and created otherwise and the opened for read-write access. + CreateTrunc, + /// The file is opened for appending data to it and created if it does not exist. + Append +} + +/** + Accesses the contents of a file as a stream. +*/ + +private FileInfo makeFileInfo(DirEntry ent) +{ + FileInfo ret; + ret.name = baseName(ent.name); + if( ret.name.length == 0 ) ret.name = ent.name; + assert(ret.name.length > 0); + ret.size = ent.size; + ret.timeModified = ent.timeLastModified; + version(Windows) ret.timeCreated = ent.timeCreated; + else ret.timeCreated = ent.timeLastModified; + ret.isSymlink = ent.isSymlink; + ret.isDirectory = ent.isDir; + return ret; +} + diff --git a/source/vibecompat/core/log.d b/source/vibecompat/core/log.d new file mode 100644 index 0000000..c015bae --- /dev/null +++ b/source/vibecompat/core/log.d @@ -0,0 +1,97 @@ +/** + Central logging facility for vibe. + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.core.log; + +import std.array; +import std.datetime; +import std.format; +import std.stdio; +import core.thread; + +private { + shared LogLevel s_minLevel = LogLevel.Info; + shared LogLevel s_logFileLevel; + shared bool s_plainLogging = false; +} + +/// Sets the minimum log level to be printed. +void setLogLevel(LogLevel level) nothrow +{ + s_minLevel = level; +} + +/// Disables output of thread/task ids with each log message +void setPlainLogging(bool enable) +{ + s_plainLogging = enable; +} + +/** + Logs a message. + + Params: + level = The log level for the logged message + fmt = See http://dlang.org/phobos/std_format.html#format-string +*/ +void logTrace(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Trace, fmt, args); } +/// ditto +void logDebug(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Debug, fmt, args); } +/// ditto +void logInfo(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Info, fmt, args); } +/// ditto +void logWarn(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Warn, fmt, args); } +/// ditto +void logError(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Error, fmt, args); } + +/// ditto +void log(T...)(LogLevel level, string fmt, auto ref T args) +nothrow { + if( level < s_minLevel ) return; + string pref; + final switch( level ){ + case LogLevel.Trace: pref = "trc"; break; + case LogLevel.Debug: pref = "dbg"; break; + case LogLevel.Info: pref = "INF"; break; + case LogLevel.Warn: pref = "WRN"; break; + case LogLevel.Error: pref = "ERR"; break; + case LogLevel.Fatal: pref = "FATAL"; break; + case LogLevel.None: assert(false); + } + + try { + auto txt = appender!string(); + txt.reserve(256); + formattedWrite(txt, fmt, args); + + auto threadid = cast(ulong)cast(void*)Thread.getThis(); + auto fiberid = cast(ulong)cast(void*)Fiber.getThis(); + threadid ^= threadid >> 32; + fiberid ^= fiberid >> 32; + + if( level >= s_minLevel ){ + if( s_plainLogging ) writeln(txt.data()); + else writefln("[%08X:%08X %s] %s", threadid, fiberid, pref, txt.data()); + stdout.flush(); + } + } catch( Exception e ){ + // this is bad but what can we do.. + debug assert(false, e.msg); + } +} + +/// Specifies the log level for a particular log message. +enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + Fatal, + None +} + diff --git a/source/vibecompat/data/json.d b/source/vibecompat/data/json.d new file mode 100644 index 0000000..3fb6e9d --- /dev/null +++ b/source/vibecompat/data/json.d @@ -0,0 +1,1282 @@ +/** + JSON serialization and value handling. + + This module provides the Json struct for reading, writing and manipulating JSON values in a seamless, + JavaScript like way. De(serialization) of arbitrary D types is also supported. + + Examples: + + --- + void manipulateJson(Json j) + { + // object members can be accessed using member syntax, just like in JavaScript + j = Json.EmptyObject; + j.name = "Example"; + j.id = 1; + + // retrieving the values is done using get() + assert(j["name"].get!string == "Example"); + assert(j["id"].get!int == 1); + + // semantic convertions can be done using to() + assert(j.id.to!string == "1"); + + // prints: + // name: "Example" + // id: 1 + foreach( string key, value; j ){ + writefln("%s: %s", key, value); + } + + // print out as JSON: {"name": "Example", "id": 1} + writefln("JSON: %s", j.toString()); + } + --- + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.data.json; + +import vibecompat.data.utils; + +import std.array; +import std.conv; +import std.datetime; +import std.exception; +import std.format; +import std.string; +import std.range; +import std.traits; + + +/******************************************************************************/ +/* public types */ +/******************************************************************************/ + +/** + Represents a single JSON value. + + Json values can have one of the types defined in the Json.Type enum. They + behave mostly like values in ECMA script in the way that you can + transparently perform operations on them. However, strict typechecking is + done, so that operations between differently typed JSON values will throw + an exception. Additionally, an explicit cast or using get!() or to!() is + required to convert a JSON value to the corresponding static D type. +*/ +struct Json { + private { + union { + bool m_bool; + long m_int; + double m_float; + string m_string; + Json[] m_array; + Json[string] m_object; + }; + Type m_type = Type.Undefined; + } + + /** Represents the run time type of a JSON value. + */ + enum Type { + /// A non-existent value in a JSON object + Undefined, + /// Null value + Null, + /// Boolean value + Bool, + /// 64-bit integer value + Int, + /// 64-bit floating point value + Float, + /// UTF-8 string + String, + /// Array of JSON values + Array, + /// JSON object aka. dictionary from string to Json + Object + } + + /// New JSON value of Type.Undefined + static @property Json Undefined() { return Json(); } + + /// New JSON value of Type.Object + static @property Json EmptyObject() { return Json(cast(Json[string])null); } + + /// New JSON value of Type.Array + static @property Json EmptyArray() { return Json(cast(Json[])null); } + + version(JsonLineNumbers) int line; + + /** + Constructor for a JSON object. + */ + this(typeof(null)) { m_type = Type.Null; } + /// ditto + this(bool v) { m_type = Type.Bool; m_bool = v; } + /// ditto + this(int v) { m_type = Type.Int; m_int = v; } + /// ditto + this(long v) { m_type = Type.Int; m_int = v; } + /// ditto + this(double v) { m_type = Type.Float; m_float = v; } + /// ditto + this(string v) { m_type = Type.String; m_string = v; } + /// ditto + this(Json[] v) { m_type = Type.Array; m_array = v; } + /// ditto + this(Json[string] v) { m_type = Type.Object; m_object = v; } + + /** + Allows assignment of D values to a JSON value. + */ + ref Json opAssign(Json v){ + m_type = v.m_type; + final switch(m_type){ + case Type.Undefined: m_string = null; break; + case Type.Null: m_string = null; break; + case Type.Bool: m_bool = v.m_bool; break; + case Type.Int: m_int = v.m_int; break; + case Type.Float: m_float = v.m_float; break; + case Type.String: m_string = v.m_string; break; + case Type.Array: m_array = v.m_array; break; + case Type.Object: m_object = v.m_object; break; + } + return this; + } + /// ditto + void opAssign(typeof(null)) { m_type = Type.Null; m_string = null; } + /// ditto + bool opAssign(bool v) { m_type = Type.Bool; m_bool = v; return v; } + /// ditto + int opAssign(int v) { m_type = Type.Int; m_int = v; return v; } + /// ditto + long opAssign(long v) { m_type = Type.Int; m_int = v; return v; } + /// ditto + double opAssign(double v) { m_type = Type.Float; m_float = v; return v; } + /// ditto + string opAssign(string v) { m_type = Type.String; m_string = v; return v; } + /// ditto + Json[] opAssign(Json[] v) { m_type = Type.Array; m_array = v; return v; } + /// ditto + Json[string] opAssign(Json[string] v) { m_type = Type.Object; m_object = v; return v; } + + /** + The current type id of this JSON object. + */ + @property Type type() const { return m_type; } + + /** + Allows direct indexing of array typed JSON values. + */ + ref inout(Json) opIndex(size_t idx) inout { checkType!(Json[])(); return m_array[idx]; } + + /** + Allows direct indexing of object typed JSON values using a string as + the key. + */ + const(Json) opIndex(string key) const { + checkType!(Json[string])(); + if( auto pv = key in m_object ) return *pv; + Json ret = Json.Undefined; + ret.m_string = key; + return ret; + } + /// ditto + ref Json opIndex(string key){ + checkType!(Json[string])(); + if( auto pv = key in m_object ) + return *pv; + m_object[key] = Json(); + m_object[key].m_type = Type.Undefined; // DMDBUG: AAs are teh $H1T!!!11 + assert(m_object[key].type == Type.Undefined); + m_object[key].m_string = key; + return m_object[key]; + } + + /** + Returns a slice of a JSON array. + */ + inout(Json[]) opSlice() inout { checkType!(Json[])(); return m_array; } + /// + inout(Json[]) opSlice(size_t from, size_t to) inout { checkType!(Json[])(); return m_array[from .. to]; } + + /** + Returns the number of entries of string, array or object typed JSON values. + */ + @property size_t length() + const { + switch(m_type){ + case Type.String: return m_string.length; + case Type.Array: return m_array.length; + case Type.Object: return m_object.length; + default: + enforce(false, "Json.length() can only be called on strings, arrays and objects, not "~.to!string(m_type)~"."); + return 0; + } + } + + /** + Allows foreach iterating over JSON objects and arrays. + */ + int opApply(int delegate(ref Json obj) del) + { + enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~"."); + if( m_type == Type.Array ){ + foreach( ref v; m_array ) + if( auto ret = del(v) ) + return ret; + return 0; + } else { + foreach( ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(v) ) + return ret; + return 0; + } + } + /// ditto + int opApply(int delegate(ref const Json obj) del) + const { + enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~"."); + if( m_type == Type.Array ){ + foreach( ref v; m_array ) + if( auto ret = del(v) ) + return ret; + return 0; + } else { + foreach( ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(v) ) + return ret; + return 0; + } + } + /// ditto + int opApply(int delegate(ref size_t idx, ref Json obj) del) + { + enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~""); + foreach( idx, ref v; m_array ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + /// ditto + int opApply(int delegate(ref size_t idx, ref const Json obj) del) + const { + enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~"."); + foreach( idx, ref v; m_array ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + /// ditto + int opApply(int delegate(ref string idx, ref Json obj) del) + { + enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~"."); + foreach( idx, ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + /// ditto + int opApply(int delegate(ref string idx, ref const Json obj) del) + const { + enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~"."); + foreach( idx, ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + + /** + Converts the JSON value to the corresponding D type - types must match exactly. + */ + inout(T) opCast(T)() inout { return get!T; } + /// ditto + @property inout(T) get(T)() + inout { + checkType!T(); + static if( is(T == bool) ) return m_bool; + else static if( is(T == double) ) return m_float; + else static if( is(T == float) ) return cast(T)m_float; + else static if( is(T == long) ) return m_int; + else static if( is(T : long) ){ enforce(m_int <= T.max && m_int >= T.min); return cast(T)m_int; } + else static if( is(T == string) ) return m_string; + else static if( is(T == Json[]) ) return m_array; + else static if( is(T == Json[string]) ) return m_object; + else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); + } + /// ditto + @property const(T) opt(T)(const(T) def = T.init) + const { + if( typeId!T != m_type ) return def; + return get!T; + } + /// ditto + @property T opt(T)(T def = T.init) + { + if( typeId!T != m_type ) return def; + return get!T; + } + + /** + Converts the JSON value to the corresponding D type - types are converted as neccessary. + */ + @property inout(T) to(T)() + inout { + static if( is(T == bool) ){ + final switch( m_type ){ + case Type.Undefined: return false; + case Type.Null: return false; + case Type.Bool: return m_bool; + case Type.Int: return m_int != 0; + case Type.Float: return m_float != 0; + case Type.String: return m_string.length > 0; + case Type.Array: return m_array.length > 0; + case Type.Object: return m_object.length > 0; + } + } else static if( is(T == double) ){ + final switch( m_type ){ + case Type.Undefined: return T.init; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return m_int; + case Type.Float: return m_float; + case Type.String: return .to!double(cast(string)m_string); + case Type.Array: return double.init; + case Type.Object: return double.init; + } + } else static if( is(T == float) ){ + final switch( m_type ){ + case Type.Undefined: return T.init; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return m_int; + case Type.Float: return m_float; + case Type.String: return .to!float(cast(string)m_string); + case Type.Array: return float.init; + case Type.Object: return float.init; + } + } + else static if( is(T == long) ){ + final switch( m_type ){ + case Type.Undefined: return 0; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return m_int; + case Type.Float: return cast(long)m_float; + case Type.String: return .to!long(m_string); + case Type.Array: return 0; + case Type.Object: return 0; + } + } else static if( is(T : long) ){ + final switch( m_type ){ + case Type.Undefined: return 0; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return cast(T)m_int; + case Type.Float: return cast(T)m_float; + case Type.String: return cast(T).to!long(cast(string)m_string); + case Type.Array: return 0; + case Type.Object: return 0; + } + } else static if( is(T == string) ){ + switch( m_type ){ + default: return toString(); + case Type.String: return m_string; + } + } else static if( is(T == Json[]) ){ + switch( m_type ){ + default: return Json([this]); + case Type.Array: return m_array; + } + } else static if( is(T == Json[string]) ){ + switch( m_type ){ + default: return Json(["value": this]); + case Type.Object: return m_object; + } + } else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); + } + + /** + Performs unary operations on the JSON value. + + The following operations are supported for each type: + + $(DL + $(DT Null) $(DD none) + $(DT Bool) $(DD ~) + $(DT Int) $(DD +, -, ++, --) + $(DT Float) $(DD +, -, ++, --) + $(DT String) $(DD none) + $(DT Array) $(DD none) + $(DT Object) $(DD none) + ) + */ + Json opUnary(string op)() + const { + static if( op == "~" ){ + checkType!bool(); + return Json(~m_bool); + } else static if( op == "+" || op == "-" || op == "++" || op == "--" ){ + if( m_type == Type.Int ) mixin("return Json("~op~"m_int);"); + else if( m_type == Type.Float ) mixin("return Json("~op~"m_float);"); + else enforce(false, "'"~op~"' only allowed on scalar types, not on "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"' for type JSON."); + } + + /** + Performs binary operations between JSON values. + + The two JSON values must be of the same run time type or an exception + will be thrown. Only the operations listed are allowed for each of the + types. + + $(DL + $(DT Null) $(DD none) + $(DT Bool) $(DD &&, ||) + $(DT Int) $(DD +, -, *, /, %) + $(DT Float) $(DD +, -, *, /, %) + $(DT String) $(DD ~) + $(DT Array) $(DD ~) + $(DT Object) $(DD none) + ) + */ + Json opBinary(string op)(ref const(Json) other) + const { + enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); + static if( op == "&&" ){ + enforce(m_type == Type.Bool, "'&&' only allowed for Type.Bool, not "~.to!string(m_type)~"."); + return Json(m_bool && other.m_bool); + } else static if( op == "||" ){ + enforce(m_type == Type.Bool, "'||' only allowed for Type.Bool, not "~.to!string(m_type)~"."); + return Json(m_bool || other.m_bool); + } else static if( op == "+" ){ + if( m_type == Type.Int ) return Json(m_int + other.m_int); + else if( m_type == Type.Float ) return Json(m_float + other.m_float); + else enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "-" ){ + if( m_type == Type.Int ) return Json(m_int - other.m_int); + else if( m_type == Type.Float ) return Json(m_float - other.m_float); + else enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "*" ){ + if( m_type == Type.Int ) return Json(m_int * other.m_int); + else if( m_type == Type.Float ) return Json(m_float * other.m_float); + else enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "/" ){ + if( m_type == Type.Int ) return Json(m_int / other.m_int); + else if( m_type == Type.Float ) return Json(m_float / other.m_float); + else enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "%" ){ + if( m_type == Type.Int ) return Json(m_int % other.m_int); + else if( m_type == Type.Float ) return Json(m_float % other.m_float); + else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "~" ){ + if( m_type == Type.String ) return Json(m_string ~ other.m_string); + else enforce(false, "'~' only allowed for strings, not "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"' for type JSON."); + assert(false); + } + /// ditto + Json opBinary(string op)(Json other) + if( op == "~" ) + { + static if( op == "~" ){ + if( m_type == Type.String ) return Json(m_string ~ other.m_string); + else if( m_type == Type.Array ) return Json(m_array ~ other.m_array); + else enforce(false, "'~' only allowed for strings and arrays, not "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"' for type JSON."); + assert(false); + } + /// ditto + void opOpAssign(string op)(Json other) + if( op == "+" || op == "-" || op == "*" ||op == "/" || op == "%" ) + { + enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); + static if( op == "+" ){ + if( m_type == Type.Int ) m_int += other.m_int; + else if( m_type == Type.Float ) m_float += other.m_float; + else enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "-" ){ + if( m_type == Type.Int ) m_int -= other.m_int; + else if( m_type == Type.Float ) m_float -= other.m_float; + else enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "*" ){ + if( m_type == Type.Int ) m_int *= other.m_int; + else if( m_type == Type.Float ) m_float *= other.m_float; + else enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "/" ){ + if( m_type == Type.Int ) m_int /= other.m_int; + else if( m_type == Type.Float ) m_float /= other.m_float; + else enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "%" ){ + if( m_type == Type.Int ) m_int %= other.m_int; + else if( m_type == Type.Float ) m_float %= other.m_float; + else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); + } /*else static if( op == "~" ){ + if( m_type == Type.String ) m_string ~= other.m_string; + else if( m_type == Type.Array ) m_array ~= other.m_array; + else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); + }*/ else static assert("Unsupported operator '"~op~"' for type JSON."); + assert(false); + } + /// ditto + Json opBinary(string op)(bool other) const { checkType!bool(); mixin("return Json(m_bool "~op~" other);"); } + /// ditto + Json opBinary(string op)(long other) const { checkType!long(); mixin("return Json(m_int "~op~" other);"); } + /// ditto + Json opBinary(string op)(double other) const { checkType!double(); mixin("return Json(m_float "~op~" other);"); } + /// ditto + Json opBinary(string op)(string other) const { checkType!string(); mixin("return Json(m_string "~op~" other);"); } + /// ditto + Json opBinary(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(m_array "~op~" other);"); } + /// ditto + Json opBinaryRight(string op)(bool other) const { checkType!bool(); mixin("return Json(other "~op~" m_bool);"); } + /// ditto + Json opBinaryRight(string op)(long other) const { checkType!long(); mixin("return Json(other "~op~" m_int);"); } + /// ditto + Json opBinaryRight(string op)(double other) const { checkType!double(); mixin("return Json(other "~op~" m_float);"); } + /// ditto + Json opBinaryRight(string op)(string other) const if(op == "~") { checkType!string(); return Json(other ~ m_string); } + /// ditto + inout(Json)* opBinaryRight(string op)(string other) inout if(op == "in") { + checkType!(Json[string])(); + auto pv = other in m_object; + if( !pv ) return null; + if( pv.type == Type.Undefined ) return null; + return pv; + } + /// ditto + Json opBinaryRight(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(other "~op~" m_array);"); } + + /** + Allows to access existing fields of a JSON object using dot syntax. + */ + @property const(Json) opDispatch(string prop)() const { return opIndex(prop); } + /// ditto + @property ref Json opDispatch(string prop)() { return opIndex(prop); } + + /** + Compares two JSON values for equality. + + If the two values have different types, they are considered unequal. + This differs with ECMA script, which performs a type conversion before + comparing the values. + */ + bool opEquals(ref const Json other) + const { + if( m_type != other.m_type ) return false; + final switch(m_type){ + case Type.Undefined: return false; + case Type.Null: return true; + case Type.Bool: return m_bool == other.m_bool; + case Type.Int: return m_int == other.m_int; + case Type.Float: return m_float == other.m_float; + case Type.String: return m_string == other.m_string; + case Type.Array: return m_array == other.m_array; + case Type.Object: return m_object == other.m_object; + } + } + /// ditto + bool opEquals(const Json other) const { return opEquals(other); } + /// ditto + bool opEquals(typeof(null)) const { return m_type == Type.Null; } + /// ditto + bool opEquals(bool v) const { return m_type == Type.Bool && m_bool == v; } + /// ditto + bool opEquals(long v) const { return m_type == Type.Int && m_int == v; } + /// ditto + bool opEquals(double v) const { return m_type == Type.Float && m_float == v; } + /// ditto + bool opEquals(string v) const { return m_type == Type.String && m_string == v; } + + /** + Compares two JSON values. + + If the types of the two values differ, the value with the smaller type + id is considered the smaller value. This differs from ECMA script, which + performs a type conversion before comparing the values. + + JSON values of type Object cannot be compared and will throw an + exception. + */ + int opCmp(ref const Json other) + const { + if( m_type != other.m_type ) return m_type < other.m_type ? -1 : 1; + final switch(m_type){ + case Type.Undefined: return 0; + case Type.Null: return 0; + case Type.Bool: return m_bool < other.m_bool ? -1 : m_bool == other.m_bool ? 0 : 1; + case Type.Int: return m_int < other.m_int ? -1 : m_int == other.m_int ? 0 : 1; + case Type.Float: return m_float < other.m_float ? -1 : m_float == other.m_float ? 0 : 1; + case Type.String: return m_string < other.m_string ? -1 : m_string == other.m_string ? 0 : 1; + case Type.Array: return m_array < other.m_array ? -1 : m_array == other.m_array ? 0 : 1; + case Type.Object: + enforce(false, "JSON objects cannot be compared."); + assert(false); + } + } + + + + /** + Returns the type id corresponding to the given D type. + */ + static @property Type typeId(T)() { + static if( is(T == typeof(null)) ) return Type.Null; + else static if( is(T == bool) ) return Type.Bool; + else static if( is(T == double) ) return Type.Float; + else static if( is(T == float) ) return Type.Float; + else static if( is(T : long) ) return Type.Int; + else static if( is(T == string) ) return Type.String; + else static if( is(T == Json[]) ) return Type.Array; + else static if( is(T == Json[string]) ) return Type.Object; + else static assert(false, "Unsupported JSON type '"~T.stringof~"'. Only bool, long, double, string, Json[] and Json[string] are allowed."); + } + + /** + Returns the JSON object as a string. + + For large JSON values use writeJsonString instead as this function will store the whole string + in memory, whereas writeJsonString writes it out bit for bit. + + See_Also: writeJsonString, toPrettyString + */ + string toString() + const { + auto ret = appender!string(); + writeJsonString(ret, this); + return ret.data; + } + + /** + Returns the JSON object as a "pretty" string. + + --- + auto json = Json(["foo": Json("bar")]); + writeln(json.toPrettyString()); + + // output: + // { + // "foo": "bar" + // } + --- + + Params: + level = Specifies the base amount of indentation for the output. Indentation is always + done using tab characters. + + See_Also: writePrettyJsonString, toString + */ + string toPrettyString(int level = 0) + const { + auto ret = appender!string(); + writePrettyJsonString(ret, this, level); + return ret.data; + } + + private void checkType(T)() + const { + string dbg; + if( m_type == Type.Undefined ) dbg = " field "~m_string; + enforce(typeId!T == m_type, "Trying to access JSON"~dbg~" of type "~.to!string(m_type)~" as "~T.stringof~"."); + } + + /*invariant() + { + assert(m_type >= Type.Undefined && m_type <= Type.Object); + }*/ +} + + +/******************************************************************************/ +/* public functions */ +/******************************************************************************/ + +/** + Parses the given range as a JSON string and returns the corresponding Json object. + + The range is shrunk during parsing, leaving any remaining text that is not part of + the JSON contents. + + Throws an Exception if any parsing error occured. +*/ +Json parseJson(R)(ref R range, int* line = null) + if( is(R == string) ) +{ + Json ret; + enforce(!range.empty, "JSON string is empty."); + + skipWhitespace(range, line); + + version(JsonLineNumbers){ + import vibecompat.core.log; + int curline = line ? *line : 0; + scope(failure) logError("Error in line: %d", curline); + } + + switch( range.front ){ + case 'f': + enforce(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. 5]~"'."); + range.popFrontN(5); + ret = false; + break; + case 'n': + enforce(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. 4]~"'."); + range.popFrontN(4); + ret = null; + break; + case 't': + enforce(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. 4]~"'."); + range.popFrontN(4); + ret = true; + break; + case '0': .. case '9'+1: + case '-': + bool is_float; + auto num = skipNumber(range, is_float); + if( is_float ) ret = to!double(num); + else ret = to!long(num); + break; + case '\"': + ret = skipJsonString(range); + break; + case '[': + Json[] arr; + range.popFront(); + while(true) { + skipWhitespace(range, line); + enforce(!range.empty); + if(range.front == ']') break; + arr ~= parseJson(range, line); + skipWhitespace(range, line); + enforce(!range.empty && (range.front == ',' || range.front == ']'), "Expected ']' or ','."); + if( range.front == ']' ) break; + else range.popFront(); + } + range.popFront(); + ret = arr; + break; + case '{': + Json[string] obj; + range.popFront(); + while(true) { + skipWhitespace(range, line); + enforce(!range.empty); + if(range.front == '}') break; + string key = skipJsonString(range); + skipWhitespace(range, line); + enforce(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'"); + range.popFront(); + skipWhitespace(range, line); + Json itm = parseJson(range, line); + obj[key] = itm; + skipWhitespace(range, line); + enforce(!range.empty && (range.front == ',' || range.front == '}'), "Expected '}' or ',' - got '"~range[0]~"'."); + if( range.front == '}' ) break; + else range.popFront(); + } + range.popFront(); + ret = obj; + break; + default: + enforce(false, "Expected valid json token, got '"~to!string(range.length)~range[0 .. range.length>12?12:range.length]~"'."); + } + + assert(ret.type != Json.Type.Undefined); + version(JsonLineNumbers) ret.line = curline; + return ret; +} + +/** + Parses the given JSON string and returns the corresponding Json object. + + Throws an Exception if any parsing error occurs. +*/ +Json parseJsonString(string str) +{ + auto ret = parseJson(str); + enforce(str.strip().length == 0, "Expected end of string after JSON value."); + return ret; +} + +unittest { + assert(parseJsonString("null") == Json(null)); + assert(parseJsonString("true") == Json(true)); + assert(parseJsonString("false") == Json(false)); + assert(parseJsonString("1") == Json(1)); + assert(parseJsonString("2.0") == Json(2.0)); + assert(parseJsonString("\"test\"") == Json("test")); + assert(parseJsonString("[1, 2, 3]") == Json([Json(1), Json(2), Json(3)])); + assert(parseJsonString("{\"a\": 1}") == Json(["a": Json(1)])); + assert(parseJsonString(`"\\\/\b\f\n\r\t\u1234"`).get!string == "\\/\b\f\n\r\t\u1234"); +} + + +/** + Serializes the given value to JSON. + + The following types of values are supported: + + $(DL + $(DT Json) $(DD Used as-is) + $(DT null) $(DD Converted to Json.Type.Null) + $(DT bool) $(DD Converted to Json.Type.Bool) + $(DT float, double) $(DD Converted to Json.Type.Double) + $(DT short, ushort, int, uint, long, ulong) $(DD Converted to Json.Type.Int) + $(DT string) $(DD Converted to Json.Type.String) + $(DT T[]) $(DD Converted to Json.Type.Array) + $(DT T[string]) $(DD Converted to Json.Type.Object) + $(DT struct) $(DD Converted to Json.Type.Object) + $(DT class) $(DD Converted to Json.Type.Object or Json.Type.Null) + ) + + All entries of an array or an associative array, as well as all R/W properties and + all public fields of a struct/class are recursively serialized using the same rules. + + Fields ending with an underscore will have the last underscore stripped in the + serialized output. This makes it possible to use fields with D keywords as their name + by simply appending an underscore. + + The following methods can be used to customize the serialization of structs/classes: + + --- + Json toJson() const; + static T fromJson(Json src); + + string toString() const; + static T fromString(string src); + --- + + The methods will have to be defined in pairs. The first pair that is implemented by + the type will be used for serialization (i.e. toJson overrides toString). +*/ +Json serializeToJson(T)(T value) +{ + alias Unqual!T TU; + static if( is(TU == Json) ) return value; + else static if( is(TU == typeof(null)) ) return Json(null); + else static if( is(TU == bool) ) return Json(value); + else static if( is(TU == float) ) return Json(cast(double)value); + else static if( is(TU == double) ) return Json(value); + else static if( is(TU == DateTime) ) return Json(value.toISOExtString()); + else static if( is(TU == SysTime) ) return Json(value.toISOExtString()); + else static if( is(TU : long) ) return Json(cast(long)value); + else static if( is(TU == string) ) return Json(value); + else static if( isArray!T ){ + auto ret = new Json[value.length]; + foreach( i; 0 .. value.length ) + ret[i] = serializeToJson(value[i]); + return Json(ret); + } else static if( isAssociativeArray!TU ){ + Json[string] ret; + foreach( string key, value; value ) + ret[key] = serializeToJson(value); + return Json(ret); + } else static if( __traits(compiles, value = T.fromJson(value.toJson())) ){ + return value.toJson(); + } else static if( __traits(compiles, value = T.fromString(value.toString())) ){ + return Json(value.toString()); + } else static if( is(TU == struct) ){ + Json[string] ret; + foreach( m; __traits(allMembers, T) ){ + static if( isRWField!(TU, m) ){ + auto mv = __traits(getMember, value, m); + ret[underscoreStrip(m)] = serializeToJson(mv); + } + } + return Json(ret); + } else static if( is(TU == class) ){ + if( value is null ) return Json(null); + Json[string] ret; + foreach( m; __traits(allMembers, T) ){ + static if( isRWField!(TU, m) ){ + auto mv = __traits(getMember, value, m); + ret[underscoreStrip(m)] = serializeToJson(mv); + } + } + return Json(ret); + } else static if( isPointer!TU ){ + if( value is null ) return Json(null); + return serializeToJson(*value); + } else { + static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); + } +} + + +/** + Deserializes a JSON value into the destination variable. + + The same types as for serializeToJson() are supported and handled inversely. +*/ +void deserializeJson(T)(ref T dst, Json src) +{ + dst = deserializeJson!T(src); +} +/// ditto +T deserializeJson(T)(Json src) +{ + static if( is(T == Json) ) return src; + else static if( is(T == typeof(null)) ){ return null; } + else static if( is(T == bool) ) return src.get!bool; + else static if( is(T == float) ) return src.to!float; // since doubles are frequently serialized without + else static if( is(T == double) ) return src.to!double; // a decimal point, we allow conversions here + else static if( is(T == DateTime) ) return DateTime.fromISOExtString(src.get!string); + else static if( is(T == SysTime) ) return SysTime.fromISOExtString(src.get!string); + else static if( is(T : long) ) return cast(T)src.get!long; + else static if( is(T == string) ) return src.get!string; + else static if( isArray!T ){ + alias typeof(T.init[0]) TV; + auto dst = new Unqual!TV[src.length]; + foreach( size_t i, v; src ) + dst[i] = deserializeJson!(Unqual!TV)(v); + return dst; + } else static if( isAssociativeArray!T ){ + alias typeof(T.init.values[0]) TV; + Unqual!TV[string] dst; + foreach( string key, value; src ) + dst[key] = deserializeJson!(Unqual!TV)(value); + return dst; + } else static if( __traits(compiles, { T dst; dst = T.fromJson(dst.toJson()); }()) ){ + return T.fromJson(src); + } else static if( __traits(compiles, { T dst; dst = T.fromString(dst.toString()); }()) ){ + return T.fromString(src.get!string); + } else static if( is(T == struct) ){ + T dst; + foreach( m; __traits(allMembers, T) ){ + static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + alias typeof(__traits(getMember, dst, m)) TM; + __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); + } + } + return dst; + } else static if( is(T == class) ){ + if( src.type == Json.Type.Null ) return null; + auto dst = new T; + foreach( m; __traits(allMembers, T) ){ + static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + alias typeof(__traits(getMember, dst, m)) TM; + __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); + } + } + return dst; + } else static if( isPointer!T ){ + if( src.type == Json.Type.Null ) return null; + alias typeof(*T.init) TD; + dst = new TD; + *dst = deserializeJson!TD(src); + return dst; + } else { + static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); + } +} + +unittest { + import std.stdio; + static struct S { float a; double b; bool c; int d; string e; byte f; ubyte g; long h; ulong i; float[] j; } + immutable S t = {1.5, -3.0, true, int.min, "Test", -128, 255, long.min, ulong.max, [1.1, 1.2, 1.3]}; + S u; + deserializeJson(u, serializeToJson(t)); + assert(t.a == u.a); + assert(t.b == u.b); + assert(t.c == u.c); + assert(t.d == u.d); + assert(t.e == u.e); + assert(t.f == u.f); + assert(t.g == u.g); + assert(t.h == u.h); + assert(t.i == u.i); + assert(t.j == u.j); +} + +unittest { + static class C { + int a; + private int _b; + @property int b() const { return _b; } + @property void b(int v) { _b = v; } + + @property int test() const { return 10; } + + void test2() {} + } + C c = new C; + c.a = 1; + c.b = 2; + + C d; + deserializeJson(d, serializeToJson(c)); + assert(c.a == d.a); + assert(c.b == d.b); +} + + +/** + Writes the given JSON object as a JSON string into the destination range. + + This function will convert the given JSON value to a string without adding + any white space between tokens (no newlines, no indentation and no padding). + The output size is thus minizized, at the cost of bad human readability. + + Params: + dst = References the string output range to which the result is written. + json = Specifies the JSON value that is to be stringified. + + See_Also: Json.toString, writePrettyJsonString +*/ +void writeJsonString(R)(ref R dst, in Json json) +// if( isOutputRange!R && is(ElementEncodingType!R == char) ) +{ + final switch( json.type ){ + case Json.Type.Undefined: dst.put("undefined"); break; + case Json.Type.Null: dst.put("null"); break; + case Json.Type.Bool: dst.put(cast(bool)json ? "true" : "false"); break; + case Json.Type.Int: formattedWrite(dst, "%d", json.get!long); break; + case Json.Type.Float: formattedWrite(dst, "%.16g", json.get!double); break; + case Json.Type.String: + dst.put("\""); + jsonEscape(dst, cast(string)json); + dst.put("\""); + break; + case Json.Type.Array: + dst.put("["); + bool first = true; + foreach( ref const Json e; json ){ + if( e.type == Json.Type.Undefined ) continue; + if( !first ) dst.put(","); + first = false; + writeJsonString(dst, e); + } + dst.put("]"); + break; + case Json.Type.Object: + dst.put("{"); + bool first = true; + foreach( string k, ref const Json e; json ){ + if( e.type == Json.Type.Undefined ) continue; + if( !first ) dst.put(","); + first = false; + dst.put("\""); + jsonEscape(dst, k); + dst.put("\":"); + writeJsonString(dst, e); + } + dst.put("}"); + break; + } +} + +/** + Writes the given JSON object as a prettified JSON string into the destination range. + + The output will contain newlines and indents to make the output human readable. + + Params: + dst = References the string output range to which the result is written. + json = Specifies the JSON value that is to be stringified. + level = Specifies the base amount of indentation for the output. Indentation is always + done using tab characters. + + See_Also: Json.toPrettyString, writeJsonString +*/ +void writePrettyJsonString(R)(ref R dst, in Json json, int level = 0) +// if( isOutputRange!R && is(ElementEncodingType!R == char) ) +{ + final switch( json.type ){ + case Json.Type.Undefined: dst.put("undefined"); break; + case Json.Type.Null: dst.put("null"); break; + case Json.Type.Bool: dst.put(cast(bool)json ? "true" : "false"); break; + case Json.Type.Int: formattedWrite(dst, "%d", json.get!long); break; + case Json.Type.Float: formattedWrite(dst, "%.16g", json.get!double); break; + case Json.Type.String: + dst.put("\""); + jsonEscape(dst, cast(string)json); + dst.put("\""); + break; + case Json.Type.Array: + dst.put("["); + bool first = true; + foreach( e; json ){ + if( e.type == Json.Type.Undefined ) continue; + if( !first ) dst.put(","); + first = false; + dst.put("\n"); + foreach( tab; 0 .. level ) dst.put('\t'); + writePrettyJsonString(dst, e, level+1); + } + if( json.length > 0 ) { + dst.put('\n'); + foreach( tab; 0 .. (level-1) ) dst.put('\t'); + } + dst.put("]"); + break; + case Json.Type.Object: + dst.put("{"); + bool first = true; + foreach( string k, e; json ){ + if( e.type == Json.Type.Undefined ) continue; + if( !first ) dst.put(","); + dst.put("\n"); + first = false; + foreach( tab; 0 .. level ) dst.put('\t'); + dst.put("\""); + jsonEscape(dst, k); + dst.put("\": "); + writePrettyJsonString(dst, e, level+1); + } + if( json.length > 0 ) { + dst.put('\n'); + foreach( tab; 0 .. (level-1) ) dst.put('\t'); + } + dst.put("}"); + break; + } +} + + +/** Deprecated aliases for backwards compatibility. + + Use writeJsonString and writePrettyJsonString instead. +*/ +deprecated("Please use writeJsonString instead.") alias writeJsonString toJson; +/// +deprecated("Please use writePrettyJsonString instead.") alias writePrettyJsonString toPrettyJson; + + +/// private +private void jsonEscape(R)(ref R dst, string s) +{ + foreach( ch; s ){ + switch(ch){ + default: dst.put(ch); break; + case '\\': dst.put("\\\\"); break; + case '\r': dst.put("\\r"); break; + case '\n': dst.put("\\n"); break; + case '\t': dst.put("\\t"); break; + case '\"': dst.put("\\\""); break; + } + } +} + +/// private +private string jsonUnescape(R)(ref R range) +{ + auto ret = appender!string(); + while(!range.empty){ + auto ch = range.front; + switch( ch ){ + case '"': return ret.data; + case '\\': + range.popFront(); + enforce(!range.empty, "Unterminated string escape sequence."); + switch(range.front){ + default: enforce("Invalid string escape sequence."); break; + case '"': ret.put('\"'); range.popFront(); break; + case '\\': ret.put('\\'); range.popFront(); break; + case '/': ret.put('/'); range.popFront(); break; + case 'b': ret.put('\b'); range.popFront(); break; + case 'f': ret.put('\f'); range.popFront(); break; + case 'n': ret.put('\n'); range.popFront(); break; + case 'r': ret.put('\r'); range.popFront(); break; + case 't': ret.put('\t'); range.popFront(); break; + case 'u': + range.popFront(); + dchar uch = 0; + foreach( i; 0 .. 4 ){ + uch *= 16; + enforce(!range.empty, "Unicode sequence must be '\\uXXXX'."); + auto dc = range.front; + range.popFront(); + if( dc >= '0' && dc <= '9' ) uch += dc - '0'; + else if( dc >= 'a' && dc <= 'f' ) uch += dc - 'a' + 10; + else if( dc >= 'A' && dc <= 'F' ) uch += dc - 'A' + 10; + else enforce(false, "Unicode sequence must be '\\uXXXX'."); + } + ret.put(uch); + break; + } + break; + default: + ret.put(ch); + range.popFront(); + break; + } + } + return ret.data; +} + +private string skipNumber(ref string s, out bool is_float) +{ + size_t idx = 0; + is_float = false; + if( s[idx] == '-' ) idx++; + if( s[idx] == '0' ) idx++; + else { + enforce(isDigit(s[idx++]), "Digit expected at beginning of number."); + while( idx < s.length && isDigit(s[idx]) ) idx++; + } + + if( idx < s.length && s[idx] == '.' ){ + idx++; + is_float = true; + while( idx < s.length && isDigit(s[idx]) ) idx++; + } + + if( idx < s.length && (s[idx] == 'e' || s[idx] == 'E') ){ + idx++; + is_float = true; + if( idx < s.length && (s[idx] == '+' || s[idx] == '-') ) idx++; + enforce( idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx]); + idx++; + while( idx < s.length && isDigit(s[idx]) ) idx++; + } + + string ret = s[0 .. idx]; + s = s[idx .. $]; + return ret; +} + +private string skipJsonString(ref string s, int* line = null) +{ + enforce(s.length >= 2 && s[0] == '\"', "too small: '" ~ s ~ "'"); + s = s[1 .. $]; + string ret = jsonUnescape(s); + enforce(s.length > 0 && s[0] == '\"', "Unterminated string literal."); + s = s[1 .. $]; + return ret; +} + +private void skipWhitespace(ref string s, int* line = null) +{ + while( s.length > 0 ){ + switch( s[0] ){ + default: return; + case ' ', '\t': s = s[1 .. $]; break; + case '\n': + s = s[1 .. $]; + if( s.length > 0 && s[0] == '\r' ) s = s[1 .. $]; + if( line ) (*line)++; + break; + case '\r': + s = s[1 .. $]; + if( s.length > 0 && s[0] == '\n' ) s = s[1 .. $]; + if( line ) (*line)++; + break; + } + } +} + +/// private +private bool isDigit(T)(T ch){ return ch >= '0' && ch <= '9'; } + +private string underscoreStrip(string field_name) +{ + if( field_name.length < 1 || field_name[$-1] != '_' ) return field_name; + else return field_name[0 .. $-1]; +} diff --git a/source/vibecompat/data/utils.d b/source/vibecompat/data/utils.d new file mode 100644 index 0000000..338dd61 --- /dev/null +++ b/source/vibecompat/data/utils.d @@ -0,0 +1,30 @@ +/** + Utility functions for data serialization + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.data.utils; + +public import std.traits; + + +template isRWPlainField(T, string M) +{ + static if( !__traits(compiles, typeof(__traits(getMember, T, M))) ){ + enum isRWPlainField = false; + } else { + //pragma(msg, T.stringof~"."~M~":"~typeof(__traits(getMember, T, M)).stringof); + enum isRWPlainField = isRWField!(T, M) && __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M))); + } +} + +template isRWField(T, string M) +{ + enum isRWField = __traits(compiles, __traits(getMember, Tgen!T(), M) = __traits(getMember, Tgen!T(), M)); + //pragma(msg, T.stringof~"."~M~": "~(isRWField?"1":"0")); +} + +/// private +private T Tgen(T)(){ return T.init; } diff --git a/source/vibecompat/inet/path.d b/source/vibecompat/inet/path.d new file mode 100644 index 0000000..87df835 --- /dev/null +++ b/source/vibecompat/inet/path.d @@ -0,0 +1,303 @@ +/** + Contains routines for high level path handling. + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.inet.path; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.string; + + +/** + Represents an absolute or relative file system path. + + This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks + are done to disallow invalid operations such as concatenating two absolute paths. It also + validates path strings and allows for easy checking of malicious relative paths. +*/ +struct Path { + private { + immutable(PathEntry)[] m_nodes; + bool m_absolute = false; + bool m_endsWithSlash = false; + } + + /// Constructs a Path object by parsing a path string. + this(string pathstr) + { + m_nodes = cast(immutable)splitPath(pathstr); + m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && m_nodes[0].toString().countUntil(':')>0); + m_endsWithSlash = pathstr.endsWith("/"); + foreach( e; m_nodes ) assert(e.toString().length > 0); + } + + /// Constructs a path object from a list of PathEntry objects. + this(immutable(PathEntry)[] nodes, bool absolute) + { + m_nodes = nodes; + m_absolute = absolute; + } + + /// Constructs a relative path with one path entry. + this(PathEntry entry){ + m_nodes = [entry]; + m_absolute = false; + } + + /// Determines if the path is absolute. + @property bool absolute() const { return m_absolute; } + + /// Resolves all '.' and '..' path entries as far as possible. + void normalize() + { + immutable(PathEntry)[] newnodes; + foreach( n; m_nodes ){ + switch(n.toString()){ + default: + newnodes ~= n; + break; + case ".": break; + case "..": + enforce(!m_absolute || newnodes.length > 0, "Path goes below root node."); + if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1]; + else newnodes ~= n; + break; + } + } + m_nodes = newnodes; + } + + /// Converts the Path back to a string representation using slashes. + string toString() + const { + if( m_nodes.empty ) return absolute ? "/" : ""; + + Appender!string ret; + + // for absolute paths start with / + if( absolute ) ret.put('/'); + + foreach( i, f; m_nodes ){ + if( i > 0 ) ret.put('/'); + ret.put(f.toString()); + } + + if( m_nodes.length > 0 && m_endsWithSlash ) + ret.put('/'); + + return ret.data; + } + + /// Converts the Path object to a native path string (backslash as path separator on Windows). + string toNativeString() + const { + Appender!string ret; + + // for absolute unix paths start with / + version(Posix) { if(absolute) ret.put('/'); } + + foreach( i, f; m_nodes ){ + version(Windows) { if( i > 0 ) ret.put('\\'); } + version(Posix) { if( i > 0 ) ret.put('/'); } + else { enforce("Unsupported OS"); } + ret.put(f.toString()); + } + + if( m_nodes.length > 0 && m_endsWithSlash ){ + version(Windows) { ret.put('\\'); } + version(Posix) { ret.put('/'); } + } + + return ret.data; + } + + /// Tests if `rhs` is an anchestor or the same as this path. + bool startsWith(const Path rhs) const { + if( rhs.m_nodes.length > m_nodes.length ) return false; + foreach( i; 0 .. rhs.m_nodes.length ) + if( m_nodes[i] != rhs.m_nodes[i] ) + return false; + return true; + } + + /// Computes the relative path from `parentPath` to this path. + Path relativeTo(const Path parentPath) const { + version(Windows){ + // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case + if( this.absolute && !this.empty && m_nodes[0].toString().endsWith(":") && + !parentPath.startsWith(this[0 .. 1]) ) + { + return this; + } + } + int nup = 0; + while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){ + nup++; + } + Path ret = Path(null, false); + ret.m_endsWithSlash = true; + foreach( i; 0 .. nup ) ret ~= ".."; + ret ~= Path(m_nodes[parentPath.length-nup .. $], false); + return ret; + } + + /// The last entry of the path + @property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; } + + /// The parent path + @property Path parentPath() const { return this[0 .. length-1]; } + + /// The ist of path entries of which this path is composed + @property immutable(PathEntry)[] nodes() const { return m_nodes; } + + /// The number of path entries of which this path is composed + @property size_t length() const { return m_nodes.length; } + + /// True if the path contains no entries + @property bool empty() const { return m_nodes.length == 0; } + + /// Determines if the path ends with a slash (i.e. is a directory) + @property bool endsWithSlash() const { return m_endsWithSlash; } + /// ditto + @property void endsWithSlash(bool v) { m_endsWithSlash = v; } + + /// Determines if this path goes outside of its base path (i.e. begins with '..'). + @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } + + ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } + Path opSlice(size_t start, size_t end) const { + auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false); + if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; + return ret; + } + size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } + + + Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) { + Path ret; + ret.m_nodes = m_nodes; + ret.m_absolute = m_absolute; + ret.m_endsWithSlash = rhs.m_endsWithSlash; + ret.normalize(); // needed to avoid "."~".." become "" instead of ".." + + assert(!rhs.absolute, "Trying to append absolute path."); + size_t idx = m_nodes.length; + foreach(folder; rhs.m_nodes){ + switch(folder.toString()){ + default: ret.m_nodes = ret.m_nodes ~ folder; break; + case ".": break; + case "..": + enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); + if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) + ret.m_nodes = ret.m_nodes[0 .. $-1]; + else ret.m_nodes = ret.m_nodes ~ folder; + break; + } + } + return ret; + } + + Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0); return opBinary!"~"(Path(rhs)); } + Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0); return opBinary!"~"(Path(rhs)); } + void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0); opOpAssign!"~"(Path(rhs)); } + void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0); opOpAssign!"~"(Path(rhs)); } + void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } + + /// Tests two paths for equality using '=='. + bool opEquals(ref const Path rhs) const { + if( m_absolute != rhs.m_absolute ) return false; + if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; + if( m_nodes.length != rhs.length ) return false; + foreach( i; 0 .. m_nodes.length ) + if( m_nodes[i] != rhs.m_nodes[i] ) + return false; + return true; + } + /// ditto + bool opEquals(const Path other) const { return opEquals(other); } + + int opCmp(ref const Path rhs) const { + if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; + foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) + if( m_nodes[i] != rhs.m_nodes[i] ) + return m_nodes[i].opCmp(rhs.m_nodes[i]); + if( m_nodes.length > rhs.m_nodes.length ) return 1; + if( m_nodes.length < rhs.m_nodes.length ) return -1; + return 0; + } +} + +struct PathEntry { + private { + string m_name; + } + + this(string str) + { + assert(str.countUntil('/') < 0 && str.countUntil('\\') < 0); + m_name = str; + } + + string toString() const { return m_name; } + + Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path(cast(immutable)[this, rhs], false); } + + bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; } + bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; } + bool opEquals(string rhs) const { return m_name == rhs; } + int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); } + int opCmp(string rhs) const { return m_name.cmp(rhs); } +} + +private bool isValidFilename(string str) +{ + foreach( ch; str ) + if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false; + return true; +} + +/// Joins two path strings. subpath must be relative. +string joinPath(string basepath, string subpath) +{ + Path p1 = Path(basepath); + Path p2 = Path(subpath); + return (p1 ~ p2).toString(); +} + +/// Splits up a path string into its elements/folders +PathEntry[] splitPath(string path) +{ + if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; + if( path.empty ) return null; + if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; + + // count the number of path nodes + size_t nelements = 0; + foreach( i, char ch; path ) + if( ch == '\\' || ch == '/' ) + nelements++; + nelements++; + + // reserve space for the elements + auto elements = new PathEntry[nelements]; + + // read and return the elements + size_t startidx = 0; + size_t eidx = 0; + foreach( i, char ch; path ) + if( ch == '\\' || ch == '/' ){ + enforce(i - startidx > 0, "Empty path entries not allowed."); + elements[eidx++] = PathEntry(path[startidx .. i]); + startidx = i+1; + } + elements[eidx++] = PathEntry(path[startidx .. $]); + enforce(path.length - startidx > 0, "Empty path entries not allowed."); + assert(eidx == nelements); + return elements; +} diff --git a/source/vibecompat/inet/url.d b/source/vibecompat/inet/url.d new file mode 100644 index 0000000..66955c6 --- /dev/null +++ b/source/vibecompat/inet/url.d @@ -0,0 +1,277 @@ +/** + URL parsing routines. + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.inet.url; + +public import vibecompat.inet.path; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.string; +import std.uri; + + +/** + Represents a URL decomposed into its components. +*/ +struct Url { + private { + string m_schema; + string m_pathString; + Path m_path; + string m_host; + ushort m_port; + string m_username; + string m_password; + string m_queryString; + string m_anchor; + } + + /// Constructs a new URL object from its components. + this(string schema, string host, ushort port, Path path) + { + m_schema = schema; + m_host = host; + m_port = port; + m_path = path; + m_pathString = path.toString(); + } + /// ditto + this(string schema, Path path) + { + this(schema, null, 0, path); + } + + /** Constructs a URL from its string representation. + + TODO: additional validation required (e.g. valid host and user names and port) + */ + this(string url_string) + { + auto str = url_string; + enforce(str.length > 0, "Empty URL."); + if( str[0] != '/' ){ + auto idx = str.countUntil(':'); + enforce(idx > 0, "No schema in URL:"~str); + m_schema = str[0 .. idx]; + str = str[idx+1 .. $]; + bool requires_host = false; + + switch(m_schema){ + case "http": + case "https": + case "ftp": + case "spdy": + case "sftp": + case "file": + // proto://server/path style + enforce(str.startsWith("//"), "URL must start with proto://..."); + requires_host = true; + str = str[2 .. $]; + goto default; + default: + auto si = str.countUntil('/'); + if( si < 0 ) si = str.length; + auto ai = str[0 .. si].countUntil('@'); + sizediff_t hs = 0; + if( ai >= 0 ){ + hs = ai+1; + auto ci = str[0 .. ai].countUntil(':'); + if( ci >= 0 ){ + m_username = str[0 .. ci]; + m_password = str[ci+1 .. ai]; + } else m_username = str[0 .. ai]; + enforce(m_username.length > 0, "Empty user name in URL."); + } + + m_host = str[hs .. si]; + auto pi = m_host.countUntil(':'); + if(pi > 0) { + enforce(pi < m_host.length-1, "Empty port in URL."); + m_port = to!ushort(m_host[pi+1..$]); + m_host = m_host[0 .. pi]; + } + + enforce(!requires_host || m_schema == "file" || m_host.length > 0, + "Empty server name in URL."); + str = str[si .. $]; + } + } + + this.localURI = str; + } + /// ditto + static Url parse(string url_string) + { + return Url(url_string); + } + + /// The schema/protocol part of the URL + @property string schema() const { return m_schema; } + /// ditto + @property void schema(string v) { m_schema = v; } + + /// The path part of the URL in the original string form + @property string pathString() const { return m_pathString; } + + /// The path part of the URL + @property Path path() const { return m_path; } + /// ditto + @property void path(Path p) + { + m_path = p; + auto pstr = p.toString(); + m_pathString = pstr; + } + + /// The host part of the URL (depends on the schema) + @property string host() const { return m_host; } + /// ditto + @property void host(string v) { m_host = v; } + + /// The port part of the URL (optional) + @property ushort port() const { return m_port; } + /// ditto + @property port(ushort v) { m_port = v; } + + /// The user name part of the URL (optional) + @property string username() const { return m_username; } + /// ditto + @property void username(string v) { m_username = v; } + + /// The password part of the URL (optional) + @property string password() const { return m_password; } + /// ditto + @property void password(string v) { m_password = v; } + + /// The query string part of the URL (optional) + @property string queryString() const { return m_queryString; } + /// ditto + @property void queryString(string v) { m_queryString = v; } + + /// The anchor part of the URL (optional) + @property string anchor() const { return m_anchor; } + + /// The path part plus query string and anchor + @property string localURI() + const { + auto str = appender!string(); + str.reserve(m_pathString.length + 2 + queryString.length + anchor.length); + str.put(encode(path.toString())); + if( queryString.length ) { + str.put("?"); + str.put(queryString); + } + if( anchor.length ) { + str.put("#"); + str.put(anchor); + } + return str.data; + } + /// ditto + @property void localURI(string str) + { + auto ai = str.countUntil('#'); + if( ai >= 0 ){ + m_anchor = str[ai+1 .. $]; + str = str[0 .. ai]; + } + + auto qi = str.countUntil('?'); + if( qi >= 0 ){ + m_queryString = str[qi+1 .. $]; + str = str[0 .. qi]; + } + + m_pathString = str; + m_path = Path(decode(str)); + } + + /// The URL to the parent path with query string and anchor stripped. + @property Url parentUrl() const { + Url ret; + ret.schema = schema; + ret.host = host; + ret.port = port; + ret.username = username; + ret.password = password; + ret.path = path.parentPath; + return ret; + } + + /// Converts this URL object to its string representation. + string toString() + const { + import std.format; + auto dst = appender!string(); + dst.put(schema); + dst.put(":"); + switch(schema){ + default: break; + case "file": + case "http": + case "https": + case "ftp": + case "spdy": + case "sftp": + dst.put("//"); + break; + } + dst.put(host); + if( m_port > 0 ) formattedWrite(dst, ":%d", m_port); + dst.put(localURI); + return dst.data; + } + + bool startsWith(const Url rhs) const { + if( m_schema != rhs.m_schema ) return false; + if( m_host != rhs.m_host ) return false; + // FIXME: also consider user, port, querystring, anchor etc + return path.startsWith(rhs.m_path); + } + + Url opBinary(string OP)(Path rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); } + Url opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); } + void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { m_path ~= rhs; } + void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; } + + /// Tests two URLs for equality using '=='. + bool opEquals(ref const Url rhs) const { + if( m_schema != rhs.m_schema ) return false; + if( m_host != rhs.m_host ) return false; + if( m_path != rhs.m_path ) return false; + return true; + } + /// ditto + bool opEquals(const Url other) const { return opEquals(other); } + + int opCmp(ref const Url rhs) const { + if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema); + if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host); + if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path); + return true; + } +} + +unittest { + auto url = Url.parse("https://www.example.net/index.html"); + assert(url.schema == "https", url.schema); + assert(url.host == "www.example.net", url.host); + assert(url.path == Path("/index.html"), url.path.toString()); + + url = Url.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor"); + assert(url.schema == "http", url.schema); + assert(url.username == "jo.doe", url.username); + assert(url.password == "password", url.password); + assert(url.port == 4711, to!string(url.port)); + assert(url.host == "sub.www.example.net", url.host); + assert(url.path.toString() == "/sub2/index.html", url.path.toString()); + assert(url.queryString == "query", url.queryString); + assert(url.anchor == "anchor", url.anchor); +} diff --git a/source/vibecompat/inet/urltransfer.d b/source/vibecompat/inet/urltransfer.d new file mode 100644 index 0000000..0daca29 --- /dev/null +++ b/source/vibecompat/inet/urltransfer.d @@ -0,0 +1,37 @@ +/** + Downloading and uploading of data from/to URLs. + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibecompat.inet.urltransfer; + +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.inet.url; + +import std.exception; +import std.net.curl; +import std.string; + + +/** + Downloads a file from the specified URL. + + Any redirects will be followed until the actual file resource is reached or if the redirection + limit of 10 is reached. Note that only HTTP(S) is currently supported. +*/ +void download(string url, string filename) +{ + auto conn = HTTP(); + static if( is(typeof(&conn.verifyPeer)) ) + conn.verifyPeer = false; + std.net.curl.download(url, filename, conn); +} + +/// ditto +void download(Url url, Path filename) +{ + download(url.toString(), filename.toNativeString()); +} diff --git a/ssleay32.dll b/ssleay32.dll new file mode 100644 index 0000000..c0d6d1f --- /dev/null +++ b/ssleay32.dll Binary files differ