diff --git a/changelog/init_license.dd b/changelog/init_license.dd new file mode 100644 index 000000000..b585e60a4 --- /dev/null +++ b/changelog/init_license.dd @@ -0,0 +1,3 @@ +Dub init now has a select menu for package format and license + +When creating a package using `dub init` you are now prompted to select a license for the package. \ No newline at end of file diff --git a/source/dub/commandline.d b/source/dub/commandline.d index 3895e079b..5bb5db064 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -990,27 +990,190 @@ class InitCommand : Command { static string input(string caption, string default_value) { - writef("%s [%s]: ", caption, default_value); + writef("%s [%s]: ", caption.color(Mode.bold), default_value); stdout.flush(); auto inp = readln(); return inp.length > 1 ? inp[0 .. $-1] : default_value; } + static string select(string caption, bool free_choice, string default_value, const string[] options...) + { + assert(options.length); + import std.math : floor, log10; + auto ndigits = (size_t val) => log10(cast(double) val).floor.to!uint + 1; + + immutable default_idx = options.countUntil(default_value); + immutable max_width = options.map!(s => s.length).reduce!max + ndigits(options.length) + " ".length; + immutable num_columns = max(1, 82 / max_width); + immutable num_rows = (options.length + num_columns - 1) / num_columns; + + string[] options_matrix; + options_matrix.length = num_rows * num_columns; + foreach (i, option; options) + { + size_t y = i % num_rows; + size_t x = i / num_rows; + options_matrix[x + y * num_columns] = option; + } + + auto idx_to_user = (string option) => cast(uint)options.countUntil(option) + 1; + auto user_to_idx = (size_t i) => cast(uint)i - 1; + + assert(default_idx >= 0); + writeln((free_choice ? "Select or enter " : "Select ").color(Mode.bold), caption.color(Mode.bold), ":".color(Mode.bold)); + foreach (i, option; options_matrix) + { + if (i != 0 && (i % num_columns) == 0) writeln(); + if (!option.length) + continue; + auto user_id = idx_to_user(option); + writef("%*u)".color(Color.cyan, Mode.bold) ~ " %s", ndigits(options.length), user_id, + leftJustifier(option, max_width)); + } + writeln(); + immutable default_choice = (default_idx + 1).to!string; + while (true) + { + auto choice = input(free_choice ? "?" : "#?", default_choice); + if (choice is default_choice) + return default_value; + choice = choice.strip; + uint option_idx = uint.max; + try + option_idx = cast(uint)user_to_idx(to!uint(choice)); + catch (ConvException) + {} + if (option_idx != uint.max) + { + if (option_idx < options.length) + return options[option_idx]; + } + else if (free_choice || options.canFind(choice)) + return choice; + logError("Select an option between 1 and %u%s.", options.length, + free_choice ? " or enter a custom value" : null); + } + } + + static string license_select(string def) + { + static immutable licenses = [ + "BSL-1.0 (Boost)", + "MIT", + "Unlicense (public domain)", + "Apache-", + "-1.0", + "-1.1", + "-2.0", + "AGPL-", + "-1.0-only", + "-1.0-or-later", + "-3.0-only", + "-3.0-or-later", + "GPL-", + "-2.0-only", + "-2.0-or-later", + "-3.0-only", + "-3.0-or-later", + "LGPL-", + "-2.0-only", + "-2.0-or-later", + "-2.1-only", + "-2.1-or-later", + "-3.0-only", + "-3.0-or-later", + "BSD-", + "-1-Clause", + "-2-Clause", + "-3-Clause", + "-4-Clause", + "MPL- (Mozilla)", + "-1.0", + "-1.1", + "-2.0", + "-2.0-no-copyleft-exception", + "EUPL-", + "-1.0", + "-1.1", + "-2.0", + "CC- (Creative Commons)", + "-BY-4.0 (Attribution 4.0 International)", + "-BY-SA-4.0 (Attribution Share Alike 4.0 International)", + "Zlib", + "ISC", + "proprietary", + ]; + + static string sanitize(string license) + { + auto desc = license.countUntil(" ("); + if (desc != -1) + license = license[0 .. desc]; + return license; + } + + string[] root; + foreach (l; licenses) + if (!l.startsWith("-")) + root ~= l; + + string result; + while (true) + { + string picked; + if (result.length) + { + auto start = licenses.countUntil!(a => a == result || a.startsWith(result ~ " (")) + 1; + auto end = start; + while (end < licenses.length && licenses[end].startsWith("-")) + end++; + picked = select( + "variant of " ~ result[0 .. $ - 1], + false, + "(back)", + // https://dub.pm/package-format-json.html#licenses + licenses[start .. end].map!"a[1..$]".array ~ "(back)" + ); + if (picked == "(back)") + { + result = null; + continue; + } + picked = sanitize(picked); + } + else + { + picked = select( + "an SPDX license-identifier (" + ~ "https://spdx.org/licenses/".color(Color.light_blue, Mode.underline) + ~ ")".color(Mode.bold), + true, + def, + // https://dub.pm/package-format-json.html#licenses + root + ); + picked = sanitize(picked); + } + if (picked == def) + return def; + + if (result.length) + result ~= picked; + else + result = picked; + + if (!result.endsWith("-")) + return result; + } + } + void depCallback(ref PackageRecipe p, ref PackageFormat fmt) { import std.datetime: Clock; if (m_nonInteractive) return; - while (true) { - string rawfmt = input("Package recipe format (sdl/json)", fmt.to!string); - if (!rawfmt.length) break; - try { - fmt = rawfmt.to!PackageFormat; - break; - } catch (Exception) { - logError(`Invalid format '%s', enter either 'sdl' or 'json'.`, rawfmt); - } - } + enum free_choice = true; + fmt = select("a package recipe format", !free_choice, fmt.to!string, "sdl", "json").to!PackageFormat; auto author = p.authors.join(", "); while (true) { // Tries getting the name until a valid one is given. @@ -1026,7 +1189,7 @@ class InitCommand : Command { } p.description = input("Description", p.description); p.authors = input("Author name", author).split(",").map!(a => a.strip).array; - p.license = input("License", p.license); + p.license = license_select(p.license); string copyrightString = .format("Copyright © %s, %-(%s, %)", Clock.currTime().year, p.authors); p.copyright = input("Copyright string", copyrightString); diff --git a/source/dub/init.d b/source/dub/init.d index bb7da7f46..c39595642 100644 --- a/source/dub/init.d +++ b/source/dub/init.d @@ -56,6 +56,8 @@ void initPackage(NativePath root_path, VersionRange[string] deps, string type, PackageRecipe p; p.name = root_path.head.name.toLower(); p.authors ~= username; + // Use proprietary as conservative default, so that we don't announce a more + // permissive license than actually chosen in case the dub.json wasn't updated. p.license = "proprietary"; foreach (pack, v; deps) { p.buildSettings.dependencies[pack] = Dependency(v); diff --git a/test/0-init-interactive.default_name.dub.sdl b/test/0-init-interactive.default_name.dub.sdl new file mode 100644 index 000000000..7b8f8ba0f --- /dev/null +++ b/test/0-init-interactive.default_name.dub.sdl @@ -0,0 +1,5 @@ +name "0-init-interactive" +description "desc" +authors "author" +copyright "copy" +license "gpl" diff --git a/test/0-init-interactive.dub.json b/test/0-init-interactive.dub.json new file mode 100644 index 000000000..70481fd67 --- /dev/null +++ b/test/0-init-interactive.dub.json @@ -0,0 +1,9 @@ +{ + "authors": [ + "author" + ], + "copyright": "copy", + "description": "desc", + "license": "gpl", + "name": "test" +} \ No newline at end of file diff --git a/test/0-init-interactive.license_gpl3.dub.sdl b/test/0-init-interactive.license_gpl3.dub.sdl new file mode 100644 index 000000000..8b2979980 --- /dev/null +++ b/test/0-init-interactive.license_gpl3.dub.sdl @@ -0,0 +1,5 @@ +name "test" +description "desc" +authors "author" +copyright "copy" +license "GPL-3.0-only" diff --git a/test/0-init-interactive.license_mpl2.dub.sdl b/test/0-init-interactive.license_mpl2.dub.sdl new file mode 100644 index 000000000..b2a5ee422 --- /dev/null +++ b/test/0-init-interactive.license_mpl2.dub.sdl @@ -0,0 +1,5 @@ +name "test" +description "desc" +authors "author" +copyright "copy" +license "MPL-2.0" diff --git a/test/0-init-interactive.license_proprietary.dub.sdl b/test/0-init-interactive.license_proprietary.dub.sdl new file mode 100644 index 000000000..166cc6c70 --- /dev/null +++ b/test/0-init-interactive.license_proprietary.dub.sdl @@ -0,0 +1,5 @@ +name "test" +description "desc" +authors "author" +copyright "copy" +license "proprietary" diff --git a/test/0-init-interactive.sh b/test/0-init-interactive.sh index 945838bd7..323cec321 100755 --- a/test/0-init-interactive.sh +++ b/test/0-init-interactive.sh @@ -3,20 +3,45 @@ . $(dirname "${BASH_SOURCE[0]}")/common.sh packname="0-init-interactive" -echo -e "sdl\ntest\ndesc\nauthor\ngpl\ncopy\n\n" | $DUB init $packname - function cleanup { rm -rf $packname } -if [ ! -e $packname/dub.sdl ]; then # it failed - cleanup - die $LINENO 'No dub.sdl file has been generated.' -fi - -if ! diff $packname/dub.sdl "$CURR_DIR"/0-init-interactive.dub.sdl; then +function runTest { + local inp=$1 + local comp=$2 + local dub_ext=${comp##*.} + local outp=$(echo -e $inp | $DUB init $packname) + if [ ! -e $packname/dub.$dub_ext ]; then # it failed + cleanup + die $LINENO "No dub.$dub_ext file has been generated for test $comp with input '$inp'. Output: $outp" + fi + if ! diff $packname/dub.$dub_ext "$CURR_DIR"/$comp; then + cleanup + die $LINENO "Contents of generated dub.$dub_ext not as expected." + fi cleanup - die $LINENO 'Contents of generated dub.sdl not as expected.' -fi +} -cleanup +# sdl package format +runTest '1\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl +# select package format out of bounds +runTest '3\n1\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl +# select package format not numeric, but in list +runTest 'sdl\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl +# selected value not numeric and not in list +runTest 'sdlf\n1\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl +# default name +runTest '1\n\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.default_name.dub.sdl +# json package format +runTest '2\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.json +# default package format +runTest '\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.json +# select license +runTest '1\ntest\ndesc\nauthor\n6\n3\ncopy\n\n' 0-init-interactive.license_gpl3.dub.sdl +# select license (with description) +runTest '1\ntest\ndesc\nauthor\n9\n3\ncopy\n\n' 0-init-interactive.license_mpl2.dub.sdl +# select license out of bounds +runTest '1\ntest\ndesc\nauthor\n21\n6\n3\ncopy\n\n' 0-init-interactive.license_gpl3.dub.sdl +# default license +runTest '1\ntest\ndesc\nauthor\n\ncopy\n\n' 0-init-interactive.license_proprietary.dub.sdl