diff --git a/build-files.txt b/build-files.txt index 58e2808..06451c4 100644 --- a/build-files.txt +++ b/build-files.txt @@ -39,6 +39,7 @@ source/dub/internal/vibecompat/data/utils.d source/dub/internal/vibecompat/inet/path.d source/dub/internal/vibecompat/inet/url.d +source/dub/recipe/io.d source/dub/recipe/json.d source/dub/recipe/packagerecipe.d source/dub/recipe/sdl.d diff --git a/source/dub/commandline.d b/source/dub/commandline.d index b345d04..8a83c70 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -68,6 +68,7 @@ new RemoveOverrideCommand, new ListOverridesCommand, new CleanCachesCommand, + new ConvertCommand, ) ]; } @@ -316,6 +317,33 @@ abstract void prepare(scope CommandArgs args); abstract int execute(Dub dub, string[] free_args, string[] app_args); + + private bool loadCwdPackage(Dub dub, bool warn_missing_package) + { + bool found = existsFile(dub.rootPath ~ "source/app.d"); + if (!found) + foreach (f; packageInfoFiles) + if (existsFile(dub.rootPath ~ f.filename)) { + found = true; + break; + } + + if (!found) { + if (warn_missing_package) { + logInfo(""); + logInfo("Neither a package description file, nor source/app.d was found in"); + logInfo(dub.rootPath.toNativeString()); + logInfo("Please run DUB from the root directory of an existing package, or run"); + logInfo("\"dub init --help\" to get information on creating a new package."); + logInfo(""); + } + return false; + } + + dub.loadPackageFromCwd(); + + return true; + } } struct CommandGroup { @@ -499,33 +527,6 @@ dub.loadPackage(pack); return true; } - - private bool loadCwdPackage(Dub dub, bool warn_missing_package) - { - bool found = existsFile(dub.rootPath ~ "source/app.d"); - if (!found) - foreach (f; packageInfoFiles) - if (existsFile(dub.rootPath ~ f.filename)) { - found = true; - break; - } - - if (!found) { - if (warn_missing_package) { - logInfo(""); - logInfo("Neither a package description file, nor source/app.d was found in"); - logInfo(dub.rootPath.toNativeString()); - logInfo("Please run DUB from the root directory of an existing package, or run"); - logInfo("\"dub init --help\" to get information on creating a new package."); - logInfo(""); - } - return false; - } - - dub.loadPackageFromCwd(); - - return true; - } } class GenerateCommand : PackageBuildCommand { @@ -1643,6 +1644,44 @@ /******************************************************************************/ +/* CONVERT command */ +/******************************************************************************/ + +class ConvertCommand : Command { + private { + string m_format; + } + + this() + { + this.name = "convert"; + this.argumentsPattern = ""; + this.description = "Converts the file format of the package recipe."; + this.helpText = [ + "This command will convert between JSON and SDLang formatted package recipe files." + "", + "Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process." + ]; + } + + override void prepare(scope CommandArgs args) + { + args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", " json, sdl"]); + } + + override int execute(Dub dub, string[] free_args, string[] app_args) + { + enforceUsage(app_args.length == 0, "Unexpected application arguments."); + enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" ")); + enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...)."); + if (!loadCwdPackage(dub, true)) return 1; + dub.convertRecipe(m_format); + return 0; + } +} + + +/******************************************************************************/ /* HELP */ /******************************************************************************/ diff --git a/source/dub/dub.d b/source/dub/dub.d index 849538d..dd6be06 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -733,6 +733,28 @@ logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); } + /** Converts the package recipe to the given format. + + Params: + destination_file_ext = The file extension matching the desired + format. Possible values are "json" or "sdl". + */ + void convertRecipe(string destination_file_ext) + { + import std.path : extension; + import dub.recipe.io : writePackageRecipe; + + auto srcfile = m_project.rootPackage.packageInfoFilename; + auto srcext = srcfile[$-1].toString().extension; + if (srcext == "."~destination_file_ext) { + logInfo("Package format is already %s.", destination_file_ext); + return; + } + + writePackageRecipe(srcfile[0 .. $-1] ~ ("dub."~destination_file_ext), m_project.rootPackage.info); + removeFile(srcfile); + } + void runDdox(bool run) { if (m_dryRun) return; diff --git a/source/dub/recipe/io.d b/source/dub/recipe/io.d index 435cc0b..04511de 100644 --- a/source/dub/recipe/io.d +++ b/source/dub/recipe/io.d @@ -89,3 +89,42 @@ assert(pr.configurations[0].buildSettings.targetType == TargetType.executable); } } + + +/** Writes the textual representation of a package recipe to a file. + + Note that the file extension must be either "json" or "sdl". +*/ +void writePackageRecipe(string filename, in ref PackageRecipe recipe) +{ + import dub.internal.vibecompat.core.file : openFile, FileMode; + auto f = openFile(filename, FileMode.createTrunc); + scope(exit) f.close(); + serializePackageRecipe(f, recipe, filename); +} + +/// ditto +void writePackageRecipe(Path filename, in ref PackageRecipe recipe) +{ + writePackageRecipe(filename.toNativeString, recipe); +} + +/** Converts a package recipe to its textual representation. + + The extension of the supplied `filename` must be either "json" or "sdl". + The output format is chosen accordingly. +*/ +void serializePackageRecipe(R)(ref R dst, in ref PackageRecipe recipe, string filename) +{ + import std.algorithm : endsWith; + import dub.internal.vibecompat.data.json : writeJsonString; + import dub.recipe.json : toJson; + import dub.recipe.sdl : toSDL; + + if (filename.endsWith(".json")) + dst.writeJsonString!(R, true)(toJson(recipe)); + else if (filename.endsWith(".sdl")) + toSDL(recipe).toSDLDocument(dst); + else assert(false, "writePackageRecipe called with filename with unknown extension: "~filename); +} + diff --git a/source/dub/recipe/sdl.d b/source/dub/recipe/sdl.d index 49420c9..280dded 100644 --- a/source/dub/recipe/sdl.d +++ b/source/dub/recipe/sdl.d @@ -14,6 +14,7 @@ import dub.internal.vibecompat.inet.path; import dub.recipe.packagerecipe; +import std.algorithm : map; import std.conv; import std.string : startsWith; @@ -82,11 +83,44 @@ } } +Tag toSDL(in ref PackageRecipe recipe) +{ + Tag ret = new Tag; + void add(T)(string field, T value) { ret.add(new Tag(null, field, [Value(value)])); } + add("name", recipe.name); + if (recipe.version_.length) add("version", recipe.version_); + if (recipe.description.length) add("description", recipe.description); + if (recipe.homepage.length) add("homepage", recipe.homepage); + if (recipe.authors.length) ret.add(new Tag(null, "authors", recipe.authors.map!(a => Value(a)).array)); + if (recipe.copyright.length) add("copyright", recipe.copyright); + if (recipe.license.length) add("license", recipe.license); + foreach (name, settings; recipe.buildTypes) { + auto t = new Tag(null, "buildType", [Value(name)]); + t.add(settings.toSDL()); + ret.add(t); + } + if (recipe.ddoxFilterArgs.length) + ret.add(new Tag("x", "ddoxFilterArgs", recipe.ddoxFilterArgs.map!(a => Value(a)).array)); + if (recipe.ddoxTool.length) ret.add(new Tag("x", "ddoxTool", [Value(recipe.ddoxTool)])); + ret.add(recipe.buildSettings.toSDL()); + foreach(config; recipe.configurations) + ret.add(config.toSDL()); + foreach (i, subPackage; recipe.subPackages) { + if (subPackage.path !is null) { + add("subPackage", subPackage.path); + } else { + auto t = subPackage.recipe.toSDL(); + t.name = "subPackage"; + ret.add(t); + } + } + return ret; +} + private void parseBuildSettings(Tag settings, ref BuildSettingsTemplate bs, string package_name) { - foreach (setting; settings.tags) { + foreach (setting; settings.tags) parseBuildSetting(setting, bs, package_name); - } } private void parseBuildSetting(Tag setting, ref BuildSettingsTemplate bs, string package_name) @@ -153,16 +187,6 @@ bs.dependencies[pkg] = dep; } -private string expandPackageName(string name, string parent_name, Tag tag) -{ - import std.algorithm : canFind; - import std.string : format; - if (name.startsWith(":")) { - enforceSDL(!parent_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", parent_name, name), tag); - return parent_name ~ name; - } else return name; -} - private void parseConfiguration(Tag t, ref ConfigurationInfo ret, string package_name) { ret.name = t.stringTagValue(true); @@ -174,6 +198,75 @@ } } +private Tag toSDL(in ref ConfigurationInfo config) +{ + auto ret = new Tag(null, "configuration", [Value(config.name)]); + if (config.platforms.length) ret.add(new Tag(null, "platforms", config.platforms[].map!(p => Value(p)).array)); + ret.add(config.buildSettings.toSDL()); + return ret; +} + +private Tag[] toSDL(in ref BuildSettingsTemplate bs) +{ + Tag[] ret; + void add(string name, string value) { ret ~= new Tag(null, name, [Value(value)]); } + void adda(string name, string suffix, in string[] values) { + ret ~= new Tag(null, name, values[].map!(v => Value(v)).array, + suffix.length ? [new Attribute(null, "platform", Value(suffix[1 .. $]))] : null); + } + + string[] toNameArray(T, U)(U bits) if(is(T == enum)) { + string[] ret; + foreach (m; __traits(allMembers, T)) + if (bits & __traits(getMember, T, m)) + ret ~= m; + return ret; + } + + foreach (pack, d; bs.dependencies) { + Attribute[] attribs; + if (d.path.length) attribs ~= new Attribute(null, "path", Value(d.path.toString())); + else attribs ~= new Attribute(null, "version", Value(d.versionString)); + if (d.optional) attribs ~= new Attribute(null, "optional", Value(true)); + ret ~= new Tag(null, "dependency", [Value(pack)], attribs); + } + if (bs.systemDependencies !is null) add("systemDependencies", bs.systemDependencies); + if (bs.targetType != TargetType.autodetect) add("targetType", bs.targetType.to!string()); + if (bs.targetPath.length) add("targetPath", bs.targetPath); + if (bs.targetName.length) add("targetName", bs.targetName); + if (bs.workingDirectory.length) add("workingDirectory", bs.workingDirectory); + if (bs.mainSourceFile.length) add("mainSourceFile", bs.mainSourceFile); + foreach (pack, conf; bs.subConfigurations) ret ~= new Tag(null, "subConfiguration", [Value(pack), Value(conf)]); + foreach (suffix, arr; bs.dflags) adda("dflags", suffix, arr); + foreach (suffix, arr; bs.lflags) adda("lflags", suffix, arr); + foreach (suffix, arr; bs.libs) adda("libs", suffix, arr); + foreach (suffix, arr; bs.sourceFiles) adda("sourceFiles", suffix, arr); + foreach (suffix, arr; bs.sourcePaths) adda("sourcePaths", suffix, arr); + foreach (suffix, arr; bs.excludedSourceFiles) adda("excludedSourceFiles", suffix, arr); + foreach (suffix, arr; bs.copyFiles) adda("copyFiles", suffix, arr); + foreach (suffix, arr; bs.versions) adda("versions", suffix, arr); + foreach (suffix, arr; bs.debugVersions) adda("debugVersions", suffix, arr); + foreach (suffix, arr; bs.importPaths) adda("importPaths", suffix, arr); + foreach (suffix, arr; bs.stringImportPaths) adda("stringImportPaths", suffix, arr); + foreach (suffix, arr; bs.preGenerateCommands) adda("preGenerateCommands", suffix, arr); + foreach (suffix, arr; bs.postGenerateCommands) adda("postGenerateCommands", suffix, arr); + foreach (suffix, arr; bs.preBuildCommands) adda("preBuildCommands", suffix, arr); + foreach (suffix, arr; bs.postBuildCommands) adda("postBuildCommands", suffix, arr); + foreach (suffix, bits; bs.buildRequirements) adda("buildRequirements", suffix, toNameArray!BuildRequirement(bits)); + foreach (suffix, bits; bs.buildOptions) adda("buildOptions", suffix, toNameArray!BuildOption(bits)); + return ret; +} + +private string expandPackageName(string name, string parent_name, Tag tag) +{ + import std.algorithm : canFind; + import std.string : format; + if (name.startsWith(":")) { + enforceSDL(!parent_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", parent_name, name), tag); + return parent_name ~ name; + } else return name; +} + private string stringTagValue(Tag t, bool allow_child_tags = false) { import std.string : format; @@ -263,6 +356,7 @@ } x:ddoxFilterArgs "-arg1" "-arg2" x:ddoxFilterArgs "-arg3" +x:ddoxTool "ddoxtool" dependency ":subpackage1" optional=false path="." dependency "somedep" version="1.0.0" optional=true @@ -306,8 +400,11 @@ lflags "lf1" "lf2" lflags "lf3" `; + PackageRecipe rec1; + parseSDL(rec1, sdl, null, "testfile"); PackageRecipe rec; - parseSDL(rec, sdl, null, "testfile"); + parseSDL(rec, rec1.toSDL(), null); // verify that all fields are serialized properly + assert(rec.name == "projectname"); assert(rec.description == "project description"); assert(rec.homepage == "http://example.com"); @@ -333,6 +430,7 @@ assert(rec.buildTypes["debug"].dflags == ["": ["-g", "-debug"]]); assert(rec.buildTypes["release"].dflags == ["": ["-release", "-O"]]); assert(rec.ddoxFilterArgs == ["-arg1", "-arg2", "-arg3"], rec.ddoxFilterArgs.to!string); + assert(rec.ddoxTool == "ddoxtool"); assert(rec.buildSettings.dependencies.length == 2); assert(rec.buildSettings.dependencies["projectname:subpackage1"].optional == false); assert(rec.buildSettings.dependencies["projectname:subpackage1"].path == Path(".")); @@ -406,3 +504,18 @@ versions "hello" 10` , null, "testfile")); } + +unittest { // test basic serialization + PackageRecipe p; + p.name = "test"; + p.authors = ["foo", "bar"]; + p.buildSettings.dflags["-windows"] = ["-a"]; + p.buildSettings.lflags[""] = ["-b", "-c"]; + auto sdl = toSDL(p).toSDLDocument(); + assert(sdl == +`name "test" +authors "foo" "bar" +dflags "-a" platform="windows" +lflags "-b" "-c" +`); +} diff --git a/test/5-convert.sh b/test/5-convert.sh new file mode 100755 index 0000000..fac1360 --- /dev/null +++ b/test/5-convert.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e -o pipefail + +cd "$CURR_DIR"/5-convert + +temp_file=$(mktemp $(basename $0).XXXXXX) + +function cleanup { + rm $temp_file +} + +function die { + echo "$@" 1>&2 + exit 1 +} + +trap cleanup EXIT + +cp dub.sdl dub.sdl.ref + +$DUB convert -f json + +if [ -f "dub.sdl" ]; then die 'Old recipe file not removed.'; fi +if [ ! -f "dub.json" ]; then die 'New recipe file not created.'; fi + +$DUB convert -f sdl + +if [ -f "dub.json" ]; then die 'Old recipe file not removed.'; fi +if [ ! -f "dub.sdl" ]; then die 'New recipe file not created.'; fi + +if ! diff "dub.sdl" "dub.sdl.ref"; then + die 'The project data did not match the expected output!' +fi + +rm dub.sdl.ref + +echo OK + diff --git a/test/5-convert/.no_build b/test/5-convert/.no_build new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/test/5-convert/.no_build @@ -0,0 +1 @@ + diff --git a/test/5-convert/dub.sdl b/test/5-convert/dub.sdl new file mode 100644 index 0000000..cd3a039 --- /dev/null +++ b/test/5-convert/dub.sdl @@ -0,0 +1,36 @@ +name "describe-dependency-1" +version "~master" +description "A test describe project" +homepage "fake.com" +authors "nobody" +copyright "Copyright © 2015, nobody" +license "BSD 2-clause" +x:ddoxFilterArgs "dfa1" "dfa2" +x:ddoxTool "ddoxtool" +dependency "describe-dependency-1:sub1" version=">=0.0.0" +targetType "sourceLibrary" +subConfiguration "describe-dependency-1:sub1" "library" +dflags "--another-dflag" +lflags "--another-lflag" +libs "anotherlib" +sourceFiles "dep.lib" platform="windows" +sourcePaths "source/" +copyFiles "data/*" +versions "anotherVerIdent" +debugVersions "anotherDebugVerIdent" +importPaths "source/" +preGenerateCommands "../describe-dependency-1/dependency-preGenerateCommands.sh" platform="posix" +postGenerateCommands "../describe-dependency-1/dependency-postGenerateCommands.sh" platform="posix" +preBuildCommands "../describe-dependency-1/dependency-preBuildCommands.sh" platform="posix" +postBuildCommands "../describe-dependency-1/dependency-postBuildCommands.sh" platform="posix" +buildRequirements "requireContracts" +buildOptions "stackStomping" +configuration "my-dependency-1-config" { + targetType "sourceLibrary" +} +subPackage { + name "sub1" +} +subPackage { + name "sub2" +}