Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interactively query for license, suggest SPDX licenses #1902

Merged
merged 10 commits into from
Sep 6, 2023
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
Loading