Skip to content

Commit

Permalink
interactively query for license, suggest SPDX licenses (#1902)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Nowak <[email protected]>
  • Loading branch information
WebFreak001 and MartinNowak authored Sep 6, 2023
1 parent 4716b7d commit 2ea8838
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 23 deletions.
3 changes: 3 additions & 0 deletions changelog/init_license.dd
Original file line number Diff line number Diff line change
@@ -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.
187 changes: 175 additions & 12 deletions source/dub/commandline.d
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions source/dub/init.d
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions test/0-init-interactive.default_name.dub.sdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name "0-init-interactive"
description "desc"
authors "author"
copyright "copy"
license "gpl"
9 changes: 9 additions & 0 deletions test/0-init-interactive.dub.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"authors": [
"author"
],
"copyright": "copy",
"description": "desc",
"license": "gpl",
"name": "test"
}
5 changes: 5 additions & 0 deletions test/0-init-interactive.license_gpl3.dub.sdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name "test"
description "desc"
authors "author"
copyright "copy"
license "GPL-3.0-only"
5 changes: 5 additions & 0 deletions test/0-init-interactive.license_mpl2.dub.sdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name "test"
description "desc"
authors "author"
copyright "copy"
license "MPL-2.0"
5 changes: 5 additions & 0 deletions test/0-init-interactive.license_proprietary.dub.sdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name "test"
description "desc"
authors "author"
copyright "copy"
license "proprietary"
47 changes: 36 additions & 11 deletions test/0-init-interactive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 2ea8838

Please sign in to comment.