hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-12 06:11:26 +0200
committerhachem <im@hachem.wtf>2026-09-12 06:11:26 +0200
commit875e2a75eae13298a36a9f631305a68e40653606 (patch)
treecbedfaec52fa8479f278f508b00a8df067031b64
parent6e94238c8e534b8ba6736d65cf20094ec1d7800b (diff)
feat: add new options and xmake
-rw-r--r--.gitignore1
-rw-r--r--Makefile5
-rw-r--r--mkproj.195
-rw-r--r--mkproj.c748
-rwxr-xr-xtests.sh139
5 files changed, 859 insertions, 129 deletions
diff --git a/.gitignore b/.gitignore
index ab0ffed..13ce059 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
mkproj
*.dSYM
.DS_Store
+tests/
diff --git a/Makefile b/Makefile
index 96c6db9..7d636e0 100644
--- a/Makefile
+++ b/Makefile
@@ -43,6 +43,9 @@ check:
--project=compile_commands.json \
--error-exitcode=1
+test: $(TARGET)
+ @sh tests.sh
+
clean:
rm -rf $(TARGET) $(TARGET).dSYM
@@ -57,4 +60,4 @@ uninstall:
rm -f $(bindir)/$(TARGET)
rm -f $(mandir)/mkproj.1
-.PHONY: all check clean install uninstall
+.PHONY: all check test clean install uninstall
diff --git a/mkproj.1 b/mkproj.1
index 145f73e..1f716d1 100644
--- a/mkproj.1
+++ b/mkproj.1
@@ -1,4 +1,4 @@
-.TH MKPROJ 1 "2026-09-07" "mkproj 0.1.0" "User Commands"
+.TH MKPROJ 1 "2026-09-12" "mkproj 0.2.0" "User Commands"
.SH NAME
mkproj \- dumb project generator
.SH SYNOPSIS
@@ -13,9 +13,10 @@ mkproj \- dumb project generator
generates boilerplate for
.BR make ,
.BR cmake ,
-.B meson
-or
+.BR meson ,
.B premake5
+or
+.B xmake
so you do not have to keep writing build files for small projects.
.PP
It creates a directory
@@ -23,7 +24,9 @@ It creates a directory
containing a
.B src
directory with a stub
-.IR main ,
+.IR main
+(or the stub at the root, with
+.BR \-\-flat ),
a build file for the chosen build system, a
.B .gitignore
and, unless disabled, an initialized git repository. The stub compiles
@@ -57,10 +60,57 @@ for
.TP
.BR \-b ", " \-\-build " " \fIsystem\fR
Build system to generate:
-.BR make ", " cmake ", " meson " or " premake .
+.BR make ", " cmake ", " meson ", " premake " or " xmake .
Default
.BR make .
.TP
+.BR \-k ", " \-\-kind " " \fIkind\fR
+What to build:
+.B exe
+for an executable or
+.B lib
+for a static library. A library gets a
+.RI < name >.h
+/
+.RI < name >.c
+pair with a stub function instead of a
+.IR main .
+Default
+.BR exe .
+.TP
+.BR \-\-cc " " \fIcc\fR
+Compiler to use. Default
+.B clang
+for
+.B c
+and
+.BR objc ,
+.B clang++
+for
+.BR cpp .
+Applies to the
+.BR make ", " premake " and " xmake
+backends (for
+.B premake
+and
+.B xmake
+it selects the matching toolset);
+.B cmake
+and
+.B meson
+pick the compiler from the environment instead.
+.TP
+.BR \-\-license " " \fIname\fR
+Write a
+.B LICENSE
+file. One of
+.BR mit ", " isc ", " bsd-2 ", " bsd-3 " or " unlicense .
+The copyright holder is taken from
+.B git config user.name
+(falling back to
+.BR $USER ).
+No license is written by default.
+.TP
.B \-\-no\-git
Do not initialize a git repository.
.TP
@@ -72,6 +122,13 @@ Only meaningful for
and
.BR premake .
.TP
+.B \-\-flat
+Flat layout: put the sources at the project root instead of in a
+.B src
+directory. Objects still build into the
+.B \-\-bin
+directory, so the tree stays a single level deep.
+.TP
.BR \-\-bin " " \fIdir\fR
Build directory. The default depends on the build system:
.B bin
@@ -99,10 +156,16 @@ Default
.BR bin\-int .
.TP
.B \-\-no\-clangd
-Do not add clangd files
-.RB ( .clangd ", " compile_flags.txt ", " compile_commands.json )
+Do not generate a
+.B compile_flags.txt
+for clangd, and do not add the clangd artifacts
+.RB ( .clangd ", " compile_commands.json )
to
.BR .gitignore .
+By default a
+.B compile_flags.txt
+with the project's flags is written so an editor's language server works before
+the first build.
.TP
.BR \-v ", " \-\-version
Print version and exit.
@@ -120,9 +183,7 @@ or split objects into
when
.B \-\-intermediate
is given.
-.B cmake
-and
-.B meson
+.BR cmake ", " meson " and " xmake
use a single out\-of\-source build directory named by
.BR \-\-bin ,
so
@@ -141,6 +202,20 @@ A C++20 project built with cmake.
.B mkproj \-b make \-\-bin\-int obj tool
A make project with objects compiled into
.BR obj/ .
+.TP
+.B mkproj \-\-flat quick
+A flat project: the source sits at the root, no
+.B src
+directory.
+.TP
+.B mkproj \-k lib \-\-license mit libfoo
+A static-library project with an MIT
+.BR LICENSE .
+.SH ENVIRONMENT
+These set defaults that command-line options override:
+.BR MKPROJ_LANG ", " MKPROJ_STD ", " MKPROJ_BUILD ", " MKPROJ_CC ", " MKPROJ_KIND
+and
+.BR MKPROJ_LICENSE .
.SH "EXIT STATUS"
.TP
.B 0
diff --git a/mkproj.c b/mkproj.c
index ef2b6f9..6e790f4 100644
--- a/mkproj.c
+++ b/mkproj.c
@@ -1,13 +1,14 @@
/*
* mkproj - dumb project generator.
*
- * generates boilerplate for make, cmake, meson or premake5 so i don't
- * have to keep writing build files for small dumb projects.
+ * generates boilerplate for make, cmake, meson, premake or xmake so you can
+ * stop writing the same fucking build files every time you start a project.
*
* Author: hachem <im@hachem.wtf>
- * Version: 0.1.0
+ * Version: 0.2.0
*/
+#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
@@ -16,10 +17,11 @@
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
+#include <time.h>
#include <unistd.h>
#define mkproj_program "mkproj"
-#define mkproj_version "0.1.0"
+#define mkproj_version "0.2.0"
#define mkproj_tagline "dumb project generator"
enum Language
@@ -36,9 +38,27 @@ enum Build_System
build_cmake,
build_meson,
build_premake,
+ build_xmake,
build_system_count
};
+enum Kind
+{
+ kind_exe,
+ kind_lib
+};
+
+enum License
+{
+ license_none,
+ license_mit,
+ license_isc,
+ license_bsd2,
+ license_bsd3,
+ license_unlicense,
+ license_count
+};
+
struct Language_Info
{
const char* key;
@@ -63,12 +83,16 @@ struct Config
const char* name;
enum Language language;
const char* standard;
+ const char* compiler;
enum Build_System build;
+ enum Kind kind;
+ enum License license;
const char* bin_dir;
const char* bin_int_dir;
int use_git;
int use_intermediate;
- int use_clangd_ignore;
+ int use_clangd;
+ int use_flat;
};
static const char* const c_standards[] =
@@ -149,6 +173,27 @@ static const struct Build_Info build_systems[build_system_count] =
.key = "premake", .name = "premake5",
.default_bin = "bin", .default_bin_int = "bin-int",
},
+ [build_xmake] =
+ {
+ .key = "xmake", .name = "xmake",
+ .default_bin = "build", .default_bin_int = "build",
+ },
+};
+
+struct License_Info
+{
+ const char* key;
+ const char* name;
+};
+
+static const struct License_Info licenses[license_count] =
+{
+ [license_none] = { .key = "none", .name = "none" },
+ [license_mit] = { .key = "mit", .name = "MIT" },
+ [license_isc] = { .key = "isc", .name = "ISC" },
+ [license_bsd2] = { .key = "bsd-2", .name = "BSD 2-Clause" },
+ [license_bsd3] = { .key = "bsd-3", .name = "BSD 3-Clause" },
+ [license_unlicense] = { .key = "unlicense", .name = "The Unlicense" },
};
static void print_version(void)
@@ -162,22 +207,27 @@ static void print_usage(FILE* stream)
stream,
"%s %s - %s\n"
"\n"
- "generates boilerplate for make, cmake, meson or premake5 so i don't\n"
- "have to keep writing build files for small dumb projects.\n"
+ "generates boilerplate for make, cmake, meson, premake or xmake so you can\n"
+ "stop writing the same fucking build files every time you start a project.\n"
"\n"
- "usage: %s [-l lang] [-s std] [-b system] [--no-git] [--intermediate]\n"
- " [--bin dir] [--bin-int dir] [--no-clangd] <name>\n"
+ "usage: %s [-l lang] [-s std] [-b system] [-k kind] [--cc cc] [--license name]\n"
+ " [--no-git] [--intermediate] [--flat] [--bin dir] [--bin-int dir]\n"
+ " [--no-clangd] <name>\n"
" %s [-v | -h]\n"
"\n"
"options:\n"
" -l, --lang LANG language: c, cpp, objc (default c)\n"
" -s, --std STD language standard (default per language)\n"
- " -b, --build SYSTEM build system: make, cmake, meson, premake (default make)\n"
+ " -b, --build SYSTEM build system: make, cmake, meson, premake, xmake (default make)\n"
+ " -k, --kind KIND exe or lib (default exe)\n"
+ " --cc CC compiler (default clang/clang++); make, premake, xmake\n"
+ " --license NAME add a LICENSE: mit, isc, bsd-2, bsd-3, unlicense\n"
" --no-git don't initialize a git repository\n"
" --intermediate use a separate intermediate object directory\n"
+ " --flat flat layout: sources at the root, no src/ directory\n"
" --bin DIR build directory (default per build system)\n"
" --bin-int DIR separate object dir for make/premake; implies --intermediate\n"
- " --no-clangd don't ignore clangd files in .gitignore\n"
+ " --no-clangd don't generate compile_flags.txt for clangd\n"
" -v, --version print version and bail\n"
" -h, --help print this and bail\n"
"\n"
@@ -210,6 +260,18 @@ static int find_build_system(const char* key, enum Build_System* out)
return 0;
}
+static int find_license(const char* key, enum License* out)
+{
+ for (enum License license = 0; license < license_count; license++)
+ if (strcmp(licenses[license].key, key) == 0)
+ {
+ *out = license;
+ return 1;
+ }
+
+ return 0;
+}
+
static int valid_standard(enum Language language, const char* standard)
{
const struct Language_Info* info = &languages[language];
@@ -229,6 +291,75 @@ static void error(const char* message, const char* detail)
(void) fprintf(stderr, "%s: error: %s\n", mkproj_program, message);
}
+/* match a value-taking option. handles both "--opt val" and "--opt=val".
+ * returns 1 and sets *value on a match, -1 if the value is missing, else 0. */
+static int opt_value(const char* arg, const char* shortname, const char* longname,
+ int argc, char** argv, int* i, const char** value)
+{
+ if (strcmp(arg, shortname) == 0 || strcmp(arg, longname) == 0)
+ {
+ if (*i + 1 >= argc)
+ return -1;
+
+ *value = argv[++(*i)];
+ return 1;
+ }
+
+ size_t longlen = strlen(longname);
+
+ if (strncmp(arg, longname, longlen) == 0 && arg[longlen] == '=')
+ {
+ *value = arg + longlen + 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+/* returns an error string if name is not usable as a directory and build
+ * identifier, or NULL when it is fine. */
+static const char* validate_name(const char* name)
+{
+ if (name[0] == '\0')
+ return "project name cannot be empty";
+
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
+ return "project name cannot be . or ..";
+
+ for (const char* c = name; *c != '\0'; c++)
+ if (!isalnum((unsigned char) *c) && *c != '_' && *c != '-' && *c != '.' && *c != '+')
+ return "project name may only contain letters, digits and _-.+";
+
+ return NULL;
+}
+
+/* derive a valid C identifier from name into buf: non-alnum becomes '_', and a
+ * leading digit is prefixed with '_'. */
+static void to_identifier(const char* name, char* buf, size_t size)
+{
+ size_t j = 0;
+
+ if (isdigit((unsigned char) name[0]) && j + 1 < size)
+ buf[j++] = '_';
+
+ for (size_t i = 0; name[i] != '\0' && j + 1 < size; i++)
+ buf[j++] = isalnum((unsigned char) name[i]) ? name[i] : '_';
+
+ buf[j] = '\0';
+}
+
+/* map a compiler binary to a premake/xmake toolset name. */
+static const char* toolset_of(const char* compiler)
+{
+ if (strstr(compiler, "clang") != NULL)
+ return "clang";
+
+ if (strstr(compiler, "gcc") != NULL || strstr(compiler, "g++") != NULL)
+ return "gcc";
+
+ return "clang";
+}
+
/*
* parse argv into config. returns 0 on success, a negative value when the
* program should exit successfully (help/version), or a positive value on a
@@ -236,9 +367,12 @@ static void error(const char* message, const char* detail)
*/
static int parse_args(int argc, char** argv, struct Config* config)
{
- const char* lang_arg = NULL;
- const char* std_arg = NULL;
- const char* build_arg = NULL;
+ const char* lang_arg = getenv("MKPROJ_LANG");
+ const char* std_arg = getenv("MKPROJ_STD");
+ const char* build_arg = getenv("MKPROJ_BUILD");
+ const char* cc_arg = getenv("MKPROJ_CC");
+ const char* kind_arg = getenv("MKPROJ_KIND");
+ const char* license_arg = getenv("MKPROJ_LICENSE");
const char* bin_arg = NULL;
const char* bin_int_arg = NULL;
const char* name = NULL;
@@ -273,68 +407,72 @@ static int parse_args(int argc, char** argv, struct Config* config)
if (strcmp(arg, "--no-clangd") == 0)
{
- config->use_clangd_ignore = 0;
+ config->use_clangd = 0;
continue;
}
- if (strcmp(arg, "--bin") == 0)
+ if (strcmp(arg, "--flat") == 0)
{
- if (++i >= argc)
- {
- error("missing argument for", arg);
- return 1;
- }
-
- bin_arg = argv[i];
+ config->use_flat = 1;
continue;
}
- if (strcmp(arg, "--bin-int") == 0)
+ int matched = 0;
+
+ matched = opt_value(arg, "-l", "--lang", argc, argv, &i, &lang_arg);
+ if (matched != 0)
{
- if (++i >= argc)
- {
- error("missing argument for", arg);
- return 1;
- }
+ if (matched < 0) { error("missing argument for", arg); return 1; }
+ continue;
+ }
- bin_int_arg = argv[i];
- config->use_intermediate = 1;
+ matched = opt_value(arg, "-s", "--std", argc, argv, &i, &std_arg);
+ if (matched != 0)
+ {
+ if (matched < 0) { error("missing argument for", arg); return 1; }
continue;
}
- if (strcmp(arg, "-l") == 0 || strcmp(arg, "--lang") == 0)
+ matched = opt_value(arg, "-b", "--build", argc, argv, &i, &build_arg);
+ if (matched != 0)
{
- if (++i >= argc)
- {
- error("missing argument for", arg);
- return 1;
- }
+ if (matched < 0) { error("missing argument for", arg); return 1; }
+ continue;
+ }
- lang_arg = argv[i];
+ matched = opt_value(arg, "--cc", "--cc", argc, argv, &i, &cc_arg);
+ if (matched != 0)
+ {
+ if (matched < 0) { error("missing argument for", arg); return 1; }
continue;
}
- if (strcmp(arg, "-s") == 0 || strcmp(arg, "--std") == 0)
+ matched = opt_value(arg, "-k", "--kind", argc, argv, &i, &kind_arg);
+ if (matched != 0)
{
- if (++i >= argc)
- {
- error("missing argument for", arg);
- return 1;
- }
+ if (matched < 0) { error("missing argument for", arg); return 1; }
+ continue;
+ }
- std_arg = argv[i];
+ matched = opt_value(arg, "--license", "--license", argc, argv, &i, &license_arg);
+ if (matched != 0)
+ {
+ if (matched < 0) { error("missing argument for", arg); return 1; }
continue;
}
- if (strcmp(arg, "-b") == 0 || strcmp(arg, "--build") == 0)
+ matched = opt_value(arg, "--bin", "--bin", argc, argv, &i, &bin_arg);
+ if (matched != 0)
{
- if (++i >= argc)
- {
- error("missing argument for", arg);
- return 1;
- }
+ if (matched < 0) { error("missing argument for", arg); return 1; }
+ continue;
+ }
- build_arg = argv[i];
+ matched = opt_value(arg, "--bin-int", "--bin-int", argc, argv, &i, &bin_int_arg);
+ if (matched != 0)
+ {
+ if (matched < 0) { error("missing argument for", arg); return 1; }
+ config->use_intermediate = 1;
continue;
}
@@ -359,6 +497,14 @@ static int parse_args(int argc, char** argv, struct Config* config)
return 1;
}
+ const char* name_error = validate_name(name);
+
+ if (name_error != NULL)
+ {
+ error(name_error, name[0] != '\0' ? name : NULL);
+ return 1;
+ }
+
config->name = name;
if (lang_arg != NULL && !find_language(lang_arg, &config->language))
@@ -388,6 +534,27 @@ static int parse_args(int argc, char** argv, struct Config* config)
config->standard = languages[config->language].default_standard;
}
+ config->compiler = cc_arg != NULL ? cc_arg : languages[config->language].compiler;
+
+ if (kind_arg != NULL)
+ {
+ if (strcmp(kind_arg, "exe") == 0)
+ config->kind = kind_exe;
+ else if (strcmp(kind_arg, "lib") == 0)
+ config->kind = kind_lib;
+ else
+ {
+ error("unknown kind (want exe or lib)", kind_arg);
+ return 1;
+ }
+ }
+
+ if (license_arg != NULL && !find_license(license_arg, &config->license))
+ {
+ error("unknown license", license_arg);
+ return 1;
+ }
+
if (config->use_intermediate
&& config->build != build_make
&& config->build != build_premake)
@@ -505,6 +672,34 @@ static void write_main(FILE* out, const struct Config* config)
(void) fputs(main_source(config), out);
}
+static void write_lib_header(FILE* out, const struct Config* config)
+{
+ char ident[256];
+ to_identifier(config->name, ident, sizeof ident);
+
+ (void) fputs("#pragma once\n\n", out);
+
+ if (config->language == language_cpp)
+ (void) fprintf(out, "auto %s_answer() -> int;\n", ident);
+ else
+ (void) fprintf(out, "int %s_answer(void);\n", ident);
+}
+
+static void write_lib_source(FILE* out, const struct Config* config)
+{
+ char ident[256];
+ to_identifier(config->name, ident, sizeof ident);
+
+ const char* include = config->language == language_objc ? "#import" : "#include";
+
+ (void) fprintf(out, "%s \"%s.h\"\n\n", include, config->name);
+
+ if (config->language == language_cpp)
+ (void) fprintf(out, "auto %s_answer() -> int\n{\n return 42;\n}\n", ident);
+ else
+ (void) fprintf(out, "int %s_answer(void)\n{\n return 42;\n}\n", ident);
+}
+
/* fill flags with the shared warning + debug flags and return the count.
* every generator reuses these so all four build systems compile alike. */
static size_t base_warnings(const struct Config* config, const char* flags[])
@@ -538,6 +733,21 @@ static size_t base_flags(const struct Config* config, const char* flags[])
return count;
}
+/* clangd reads compile_flags.txt (one flag per line) to know how to parse the
+ * project, so the editor works before any build has run. */
+static void write_compile_flags(FILE* out, const struct Config* config)
+{
+ const char* warnings[10];
+ size_t count = base_warnings(config, warnings);
+
+ (void) fprintf(out, "-std=%s\n", config->standard);
+
+ for (size_t i = 0; i < count; i++)
+ (void) fprintf(out, "%s\n", warnings[i]);
+
+ (void) fprintf(out, "%s\n", config->use_flat ? "-I." : "-Isrc");
+}
+
static const char* cmake_language(enum Language language)
{
if (language == language_cpp)
@@ -567,7 +777,7 @@ static void write_makefile(FILE* out, const struct Config* config)
flags[count++] = "-std=$(STD)";
count += base_warnings(config, flags + count);
- flags[count++] = "-Isrc";
+ flags[count++] = config->use_flat ? "-I." : "-Isrc";
int width = 0;
@@ -575,7 +785,7 @@ static void write_makefile(FILE* out, const struct Config* config)
if ((int) strlen(flags[i]) > width)
width = (int) strlen(flags[i]);
- (void) fprintf(out, "CC := %s\nSTD := %s\n\nDEBUG ?= 1\nSANITIZE ?= 1\n\nCFLAGS := ", lang->compiler, config->standard);
+ (void) fprintf(out, "CC := %s\nSTD := %s\n\nDEBUG ?= 1\nSANITIZE ?= 1\n\nCFLAGS := ", config->compiler, config->standard);
for (size_t i = 0; i < count; i++)
{
@@ -600,19 +810,41 @@ static void write_makefile(FILE* out, const struct Config* config)
"ifeq ($(SANITIZE),1)\n"
"CFLAGS += -fsanitize=address,undefined\n"
"LDFLAGS += -fsanitize=address,undefined\n"
- "endif\n"
- "\n"
- "TARGET := %s/%s\n"
- "SRC := $(shell find src -name '*.%s')\n"
- "OBJ := $(SRC:src/%%=%s/%%.o)\n"
+ "endif\n");
+
+ if (config->language == language_objc && config->kind == kind_exe)
+ (void) fprintf(out, "\nLDFLAGS += -framework Foundation\n");
+
+ if (config->kind == kind_lib)
+ (void) fprintf(out, "\nTARGET := %s/lib%s.a\n", config->bin_dir, config->name);
+ else
+ (void) fprintf(out, "\nTARGET := %s/%s\n", config->bin_dir, config->name);
+
+ if (config->use_flat)
+ (void) fprintf(out, "SRC := $(wildcard *.%s)\nOBJ := $(SRC:%%=%s/%%.o)\n", ext, obj_dir);
+ else
+ (void) fprintf(out, "SRC := $(shell find src -name '*.%s')\nOBJ := $(SRC:src/%%=%s/%%.o)\n", ext, obj_dir);
+
+ (void) fprintf(
+ out,
"\n"
"all: $(TARGET)\n"
"\n"
"$(TARGET): $(OBJ)\n"
- "\t@mkdir -p $(@D)\n"
- "\t$(CC) $^ -o $@ $(LDFLAGS)\n"
- "\n"
- "%s/%%.%s.o: src/%%.%s\n"
+ "\t@mkdir -p $(@D)\n");
+
+ if (config->kind == kind_lib)
+ (void) fprintf(out, "\t$(AR) rcs $@ $^\n\n");
+ else
+ (void) fprintf(out, "\t$(CC) $^ -o $@ $(LDFLAGS)\n\n");
+
+ if (config->use_flat)
+ (void) fprintf(out, "%s/%%.%s.o: %%.%s\n", obj_dir, ext, ext);
+ else
+ (void) fprintf(out, "%s/%%.%s.o: src/%%.%s\n", obj_dir, ext, ext);
+
+ (void) fprintf(
+ out,
"\t@mkdir -p $(@D)\n"
"\t$(CC) $(CFLAGS) -c $< -o $@\n"
"\n"
@@ -626,9 +858,7 @@ static void write_makefile(FILE* out, const struct Config* config)
"\t%s\n"
"\n"
".PHONY: all check clean\n",
-
- config->bin_dir, config->name, ext,
- obj_dir, obj_dir, ext, ext, clean);
+ clean);
}
static void write_cmake(FILE* out, const struct Config* config)
@@ -645,23 +875,33 @@ static void write_cmake(FILE* out, const struct Config* config)
"project(%s %s)\n"
"\n"
"option(SANITIZE \"build with asan + ubsan\" ON)\n"
+ "\n",
+ name, cmake_language(config->language));
+
+ if (config->use_flat)
+ (void) fprintf(out, "file(GLOB SOURCES CONFIGURE_DEPENDS *.%s)\n", ext);
+ else
+ (void) fprintf(out, "file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS src/*.%s)\n", ext);
+
+ if (config->kind == kind_lib)
+ (void) fprintf(out, "\nadd_library(%s STATIC ${SOURCES})\n", name);
+ else
+ (void) fprintf(out, "\nadd_executable(%s ${SOURCES})\n", name);
+
+ (void) fprintf(
+ out,
"\n"
- "file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS src/*.%s)\n"
- "\n"
- "add_executable(%s ${SOURCES})\n"
- "\n"
- "target_include_directories(%s PRIVATE src)\n"
+ "target_include_directories(%s PRIVATE %s)\n"
"\n"
"target_compile_options(%s PRIVATE\n",
- name, cmake_language(config->language),
- ext, name, name, name);
+ name, config->use_flat ? "." : "src", name);
for (size_t i = 0; i < count; i++)
(void) fprintf(out, " %s\n", flags[i]);
(void) fprintf(out, " -std=%s)\n", config->standard);
- if (config->language == language_objc)
+ if (config->language == language_objc && config->kind == kind_exe)
(void) fprintf(
out,
"\n"
@@ -704,13 +944,27 @@ static void write_meson(FILE* out, const struct Config* config)
" 'b_sanitize=address,undefined',\n"
" 'b_lundef=false',\n"
" ])\n"
- "\n"
- "sources = run_command(\n"
- " 'find', 'src', '-name', '*.%s',\n"
- " check: true).stdout().strip().split('\\n')\n"
- "\n"
- "add_project_arguments(\n",
- name, lang, ext);
+ "\n",
+ name, lang);
+
+ if (config->use_flat)
+ (void) fprintf(
+ out,
+ "sources = run_command(\n"
+ " 'find', '.', '-maxdepth', '1', '-name', '*.%s',\n"
+ " check: true).stdout().strip().split('\\n')\n"
+ "\n",
+ ext);
+ else
+ (void) fprintf(
+ out,
+ "sources = run_command(\n"
+ " 'find', 'src', '-name', '*.%s',\n"
+ " check: true).stdout().strip().split('\\n')\n"
+ "\n",
+ ext);
+
+ (void) fprintf(out, "add_project_arguments(\n");
for (size_t i = 0; i < count; i++)
(void) fprintf(out, " '%s',\n", flags[i]);
@@ -719,26 +973,29 @@ static void write_meson(FILE* out, const struct Config* config)
out,
" '-std=%s',\n"
" language: '%s')\n"
- "\n"
- "inc = include_directories('src')\n"
"\n",
config->standard, lang);
- if (config->language == language_objc)
- (void) fprintf(
- out,
- "foundation = dependency('appleframeworks', modules: ['Foundation'])\n"
- "\n"
- "executable('%s', sources,\n"
- " include_directories: inc,\n"
- " dependencies: foundation)\n",
- name);
+ int link_foundation = config->language == language_objc && config->kind == kind_exe;
+
+ if (!config->use_flat)
+ (void) fprintf(out, "inc = include_directories('src')\n\n");
+
+ if (link_foundation)
+ (void) fprintf(out, "foundation = dependency('appleframeworks', modules: ['Foundation'])\n\n");
+
+ if (config->kind == kind_lib)
+ (void) fprintf(out, "static_library('%s', sources", name);
else
- (void) fprintf(
- out,
- "executable('%s', sources,\n"
- " include_directories: inc)\n",
- name);
+ (void) fprintf(out, "executable('%s', sources", name);
+
+ if (!config->use_flat)
+ (void) fprintf(out, ",\n include_directories: inc");
+
+ if (link_foundation)
+ (void) fprintf(out, ",\n dependencies: foundation");
+
+ (void) fprintf(out, ")\n");
}
static const char* premake_language(enum Language language)
@@ -763,20 +1020,24 @@ static void write_premake(FILE* out, const struct Config* config)
" configurations { \"debug\" }\n"
"\n"
"project \"%s\"\n"
- " kind \"ConsoleApp\"\n"
+ " kind \"%s\"\n"
" language \"%s\"\n"
- " toolset \"clang\"\n"
+ " toolset \"%s\"\n"
"\n"
" targetdir \"%s\"\n"
" targetname \"%s\"\n"
" objdir \"%s\"\n"
- "\n"
- " files { \"src/**.%s\" }\n"
- " includedirs { \"src\" }\n"
- "\n"
- " buildoptions {\n",
- name, name, premake_language(config->language),
- config->bin_dir, name, obj_dir, ext);
+ "\n",
+ name, name, config->kind == kind_lib ? "StaticLib" : "ConsoleApp",
+ premake_language(config->language),
+ toolset_of(config->compiler), config->bin_dir, name, obj_dir);
+
+ if (config->use_flat)
+ (void) fprintf(out, " files { \"*.%s\" }\n includedirs { \".\" }\n", ext);
+ else
+ (void) fprintf(out, " files { \"src/**.%s\" }\n includedirs { \"src\" }\n", ext);
+
+ (void) fprintf(out, "\n buildoptions {\n");
for (size_t i = 0; i < count; i++)
(void) fprintf(out, " \"%s\",\n", flags[i]);
@@ -790,13 +1051,211 @@ static void write_premake(FILE* out, const struct Config* config)
" linkoptions { \"-fsanitize=address,undefined\" }\n",
config->standard);
- if (config->language == language_objc)
+ if (config->language == language_objc && config->kind == kind_exe)
(void) fputs(
"\n"
" links { \"Foundation.framework\" }\n",
out);
}
+static const char* xmake_flags(enum Language language)
+{
+ if (language == language_cpp)
+ return "add_cxxflags";
+
+ if (language == language_objc)
+ return "add_mflags";
+
+ return "add_cflags";
+}
+
+static void write_xmake(FILE* out, const struct Config* config)
+{
+ const char* name = config->name;
+ const char* ext = languages[config->language].extension;
+ const char* flag_fn = xmake_flags(config->language);
+ const char* flags[10];
+ size_t count = base_flags(config, flags);
+
+ (void) fprintf(
+ out,
+ "set_config(\"builddir\", \"%s\")\n"
+ "\n"
+ "set_toolchains(\"%s\")\n"
+ "\n"
+ "target(\"%s\")\n"
+ " set_kind(\"%s\")\n",
+ config->bin_dir, toolset_of(config->compiler), name,
+ config->kind == kind_lib ? "static" : "binary");
+
+ if (config->use_flat)
+ (void) fprintf(out, " add_files(\"*.%s\")\n add_includedirs(\".\")\n", ext);
+ else
+ (void) fprintf(out, " add_files(\"src/**.%s\")\n add_includedirs(\"src\")\n", ext);
+
+ (void) fprintf(out, "\n %s(\n", flag_fn);
+
+ for (size_t i = 0; i < count; i++)
+ (void) fprintf(out, " \"%s\",\n", flags[i]);
+
+ (void) fprintf(
+ out,
+ " \"-std=%s\",\n"
+ " \"-fsanitize=address,undefined\")\n"
+ "\n"
+ " add_ldflags(\"-fsanitize=address,undefined\")\n",
+ config->standard);
+
+ if (config->language == language_objc && config->kind == kind_exe)
+ (void) fputs("\n add_frameworks(\"Foundation\")\n", out);
+}
+
+/* best-effort copyright holder: git's configured name, else $USER. */
+static void get_author(char* buf, size_t size)
+{
+ buf[0] = '\0';
+
+ FILE* pipe = popen("git config user.name", "r");
+
+ if (pipe != NULL)
+ {
+ if (fgets(buf, (int) size, pipe) != NULL)
+ {
+ size_t len = strlen(buf);
+
+ if (len > 0 && buf[len - 1] == '\n')
+ buf[len - 1] = '\0';
+ }
+
+ (void) pclose(pipe);
+ }
+
+ if (buf[0] == '\0')
+ {
+ const char* user = getenv("USER");
+
+ (void) snprintf(buf, size, "%s", user != NULL ? user : "unknown");
+ }
+}
+
+static int current_year(void)
+{
+ time_t now = time(NULL);
+ struct tm* local = localtime(&now);
+
+ return local != NULL ? local->tm_year + 1900 : 1970;
+}
+
+static void write_license(FILE* out, const struct Config* config)
+{
+ char author[256];
+ int year = current_year();
+
+ get_author(author, sizeof author);
+
+ switch (config->license)
+ {
+ case license_mit:
+ (void) fprintf(out,
+ "MIT License\n\n"
+ "Copyright (c) %d %s\n\n"
+ "Permission is hereby granted, free of charge, to any person obtaining a copy\n"
+ "of this software and associated documentation files (the \"Software\"), to deal\n"
+ "in the Software without restriction, including without limitation the rights\n"
+ "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n"
+ "copies of the Software, and to permit persons to whom the Software is\n"
+ "furnished to do so, subject to the following conditions:\n\n"
+ "The above copyright notice and this permission notice shall be included in all\n"
+ "copies or substantial portions of the Software.\n\n"
+ "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
+ "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
+ "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
+ "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n"
+ "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n"
+ "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
+ "SOFTWARE.\n",
+ year, author);
+ break;
+
+ case license_isc:
+ (void) fprintf(out,
+ "ISC License\n\n"
+ "Copyright (c) %d %s\n\n"
+ "Permission to use, copy, modify, and/or distribute this software for any\n"
+ "purpose with or without fee is hereby granted, provided that the above\n"
+ "copyright notice and this permission notice appear in all copies.\n\n"
+ "THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n"
+ "REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n"
+ "AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n"
+ "INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n"
+ "LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n"
+ "OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n"
+ "PERFORMANCE OF THIS SOFTWARE.\n",
+ year, author);
+ break;
+
+ case license_bsd2:
+ case license_bsd3:
+ (void) fprintf(out,
+ "BSD %s License\n\n"
+ "Copyright (c) %d %s\n\n"
+ "Redistribution and use in source and binary forms, with or without\n"
+ "modification, are permitted provided that the following conditions are met:\n\n"
+ "1. Redistributions of source code must retain the above copyright notice, this\n"
+ " list of conditions and the following disclaimer.\n\n"
+ "2. Redistributions in binary form must reproduce the above copyright notice,\n"
+ " this list of conditions and the following disclaimer in the documentation\n"
+ " and/or other materials provided with the distribution.\n\n",
+ config->license == license_bsd3 ? "3-Clause" : "2-Clause", year, author);
+
+ if (config->license == license_bsd3)
+ (void) fputs(
+ "3. Neither the name of the copyright holder nor the names of its\n"
+ " contributors may be used to endorse or promote products derived from\n"
+ " this software without specific prior written permission.\n\n",
+ out);
+
+ (void) fputs(
+ "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n"
+ "AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n"
+ "IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n"
+ "DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n"
+ "FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n"
+ "DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n"
+ "SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n"
+ "CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n"
+ "OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"
+ "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n",
+ out);
+ break;
+
+ case license_unlicense:
+ (void) fputs(
+ "This is free and unencumbered software released into the public domain.\n\n"
+ "Anyone is free to copy, modify, publish, use, compile, sell, or distribute\n"
+ "this software, either in source code form or as a compiled binary, for any\n"
+ "purpose, commercial or non-commercial, and by any means.\n\n"
+ "In jurisdictions that recognize copyright laws, the author or authors of this\n"
+ "software dedicate any and all copyright interest in the software to the public\n"
+ "domain. We make this dedication for the benefit of the public at large and to\n"
+ "the detriment of our heirs and successors. We intend this dedication to be an\n"
+ "overt act of relinquishment in perpetuity of all present and future rights to\n"
+ "this software under copyright law.\n\n"
+ "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
+ "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
+ "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
+ "AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n"
+ "ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n"
+ "WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n"
+ "For more information, please refer to <https://unlicense.org>\n",
+ out);
+ break;
+
+ default:
+ break;
+ }
+}
+
static void write_gitignore(FILE* out, const struct Config* config)
{
(void) fprintf(out, "%s/\n", config->bin_dir);
@@ -810,10 +1269,12 @@ static void write_gitignore(FILE* out, const struct Config* config)
if (config->build == build_premake)
(void) fputs("Makefile\n*.make\n", out);
- if (config->use_clangd_ignore)
- (void) fputs("\n"
+ if (config->build == build_xmake)
+ (void) fputs(".xmake/\n", out);
+
+ if (config->use_clangd)
+ (void) fputs("\n"
".clangd\n"
- "compile_flags.txt\n"
"compile_commands.json\n",
out);
}
@@ -885,6 +1346,12 @@ static void print_next_steps(const struct Config* config)
(void) printf(" make\n");
break;
+ case build_xmake:
+ (void) printf(" xmake\n");
+ if (config->kind == kind_exe)
+ (void) printf(" xmake run\n");
+ break;
+
default:
break;
}
@@ -904,22 +1371,47 @@ static int generate_project(const struct Config* config)
case build_cmake: build_file = "CMakeLists.txt"; write_build = write_cmake; break;
case build_meson: build_file = "meson.build"; write_build = write_meson; break;
case build_premake: build_file = "premake5.lua"; write_build = write_premake; break;
+ case build_xmake: build_file = "xmake.lua"; write_build = write_xmake; break;
default: error("unknown build system", NULL); return -1;
}
+ const char* ext = languages[config->language].extension;
+ char srcdir[PATH_MAX];
+
if (make_dir(config->name) != 0)
return -1;
- (void) snprintf(path, sizeof path, "%s/src", config->name);
+ if (config->use_flat)
+ {
+ (void) snprintf(srcdir, sizeof srcdir, "%s", config->name);
+ }
+ else
+ {
+ (void) snprintf(srcdir, sizeof srcdir, "%s/src", config->name);
+
+ if (make_dir(srcdir) != 0)
+ return -1;
+ }
- if (make_dir(path) != 0)
- return -1;
+ if (config->kind == kind_lib)
+ {
+ (void) snprintf(path, sizeof path, "%s/%s.h", srcdir, config->name);
- (void) snprintf(path, sizeof path, "%s/src/main.%s",
- config->name, languages[config->language].extension);
+ if (emit(path, config, write_lib_header) != 0)
+ return -1;
- if (emit(path, config, write_main) != 0)
- return -1;
+ (void) snprintf(path, sizeof path, "%s/%s.%s", srcdir, config->name, ext);
+
+ if (emit(path, config, write_lib_source) != 0)
+ return -1;
+ }
+ else
+ {
+ (void) snprintf(path, sizeof path, "%s/main.%s", srcdir, ext);
+
+ if (emit(path, config, write_main) != 0)
+ return -1;
+ }
(void) snprintf(path, sizeof path, "%s/%s", config->name, build_file);
@@ -931,6 +1423,22 @@ static int generate_project(const struct Config* config)
if (emit(path, config, write_gitignore) != 0)
return -1;
+ if (config->use_clangd)
+ {
+ (void) snprintf(path, sizeof path, "%s/compile_flags.txt", config->name);
+
+ if (emit(path, config, write_compile_flags) != 0)
+ return -1;
+ }
+
+ if (config->license != license_none)
+ {
+ (void) snprintf(path, sizeof path, "%s/LICENSE", config->name);
+
+ if (emit(path, config, write_license) != 0)
+ return -1;
+ }
+
if (config->use_git && git_init(config->name) != 0)
(void) fprintf(stderr, "%s: warning: git is not installed\n", mkproj_program);
@@ -953,12 +1461,16 @@ int main(int argc, char** argv)
.name = NULL,
.language = language_c,
.standard = NULL,
+ .compiler = NULL,
.build = build_make,
+ .kind = kind_exe,
+ .license = license_none,
.bin_dir = NULL,
.bin_int_dir = NULL,
.use_git = 1,
.use_intermediate = 0,
- .use_clangd_ignore = 1,
+ .use_clangd = 1,
+ .use_flat = 0,
};
int status = parse_args(argc, argv, &config);
diff --git a/tests.sh b/tests.sh
new file mode 100755
index 0000000..f860ae7
--- /dev/null
+++ b/tests.sh
@@ -0,0 +1,139 @@
+#!/bin/sh
+
+set -u
+
+root=$(cd "$(dirname "$0")" && pwd)
+mkproj=${MKPROJ:-$root/mkproj}
+work=$root/tests
+rm -rf "$work"
+mkdir -p "$work"
+trap 'rm -rf "$work"' EXIT
+
+pass=0
+fail=0
+
+ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
+bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; }
+skip() { printf ' skip %s\n' "$1"; }
+
+have() { command -v "$1" >/dev/null 2>&1; }
+
+build_run()
+{
+ bs=$1 dir=$2 name=$3
+
+ case $bs in
+ make)
+ ( cd "$dir" && make ) >/dev/null 2>&1
+ "$dir/bin/$name" 2>&1 ;;
+ premake)
+ ( cd "$dir" && premake5 gmake2 && make ) >/dev/null 2>&1
+ "$dir/bin/$name" 2>&1 ;;
+ cmake)
+ ( cd "$dir" && cmake -B build && cmake --build build ) >/dev/null 2>&1
+ "$dir/build/$name" 2>&1 ;;
+ meson)
+ ( cd "$dir" && meson setup build && meson compile -C build ) >/dev/null 2>&1
+ "$dir/build/$name" 2>&1 ;;
+ xmake)
+ ( cd "$dir" && xmake -y ) >/dev/null 2>&1
+ ( cd "$dir" && xmake run ) 2>&1 ;;
+ esac
+}
+
+check()
+{
+ label=$1 bs=$2 name=$3
+ shift 3
+
+ if ! ( cd "$work" && "$mkproj" --no-git -b "$bs" "$@" "$name" ) >/dev/null 2>&1
+ then
+ bad "$label (generate)"
+ return
+ fi
+
+ if build_run "$bs" "$work/$name" "$name" | grep -q 'type shi'
+ then
+ ok "$label"
+ else
+ bad "$label (build/run)"
+ fi
+}
+
+lib_build()
+{
+ bs=$1 dir=$2
+
+ case $bs in
+ make) ( cd "$dir" && make ) >/dev/null 2>&1 ;;
+ premake) ( cd "$dir" && premake5 gmake2 && make ) >/dev/null 2>&1 ;;
+ cmake) ( cd "$dir" && cmake -B build && cmake --build build ) >/dev/null 2>&1 ;;
+ meson) ( cd "$dir" && meson setup build && meson compile -C build ) >/dev/null 2>&1 ;;
+ xmake) ( cd "$dir" && xmake -y ) >/dev/null 2>&1 ;;
+ esac
+}
+
+check_lib()
+{
+ bs=$1 name=$2
+
+ if ! ( cd "$work" && "$mkproj" --no-git -b "$bs" -k lib "$name" ) >/dev/null 2>&1
+ then
+ bad "$bs / lib (generate)"
+ return
+ fi
+
+ if lib_build "$bs" "$work/$name"
+ then ok "$bs / lib"
+ else bad "$bs / lib (build)"
+ fi
+}
+
+echo "mkproj test harness ($mkproj)"
+echo
+
+echo "cli:"
+"$mkproj" --version >/dev/null 2>&1 && ok "--version" || bad "--version"
+"$mkproj" --help >/dev/null 2>&1 && ok "--help" || bad "--help"
+( cd "$work" && "$mkproj" --no-git --lang=cpp eqform ) >/dev/null 2>&1 &&
+ [ -f "$work/eqform/src/main.cpp" ] && ok "--lang=cpp (=form)" || bad "--lang=cpp (=form)"
+
+echo
+echo "name validation (these must be rejected):"
+for badname in "bad name" "a/b" ".." "" 'x") evil('; do
+ if ( cd "$work" && "$mkproj" --no-git "$badname" ) >/dev/null 2>&1
+ then bad "rejects [$badname]"
+ else ok "rejects [$badname]"
+ fi
+done
+
+echo
+echo "build matrix:"
+for bs in make cmake meson premake xmake; do
+ tool=$bs
+ [ "$bs" = premake ] && tool=premake5
+
+ if ! have "$tool"; then skip "$bs (no $tool)"; continue; fi
+ if [ "$bs" = meson ] && ! have ninja; then skip "meson (no ninja)"; continue; fi
+
+ check "$bs / c" "$bs" "t_${bs}_c"
+ check "$bs / cpp" "$bs" "t_${bs}_cpp" -l cpp
+ check "$bs / flat" "$bs" "t_${bs}_flat" --flat
+ [ "$(uname)" = Darwin ] && check "$bs / objc" "$bs" "t_${bs}_objc" -l objc
+ check_lib "$bs" "t_${bs}_lib"
+done
+
+echo
+echo "generated files:"
+( cd "$work" && "$mkproj" --no-git clangd_on ) >/dev/null 2>&1
+[ -f "$work/clangd_on/compile_flags.txt" ] && ok "compile_flags.txt generated" || bad "compile_flags.txt generated"
+( cd "$work" && "$mkproj" --no-git --no-clangd clangd_off ) >/dev/null 2>&1
+[ -f "$work/clangd_off/compile_flags.txt" ] && bad "--no-clangd skips compile_flags.txt" || ok "--no-clangd skips compile_flags.txt"
+( cd "$work" && "$mkproj" --no-git --license mit lic_mit ) >/dev/null 2>&1
+grep -q 'MIT License' "$work/lic_mit/LICENSE" 2>/dev/null && ok "--license mit" || bad "--license mit"
+( cd "$work" && MKPROJ_BUILD=cmake "$mkproj" --no-git envdef ) >/dev/null 2>&1
+[ -f "$work/envdef/CMakeLists.txt" ] && ok "MKPROJ_BUILD env default" || bad "MKPROJ_BUILD env default"
+
+echo
+printf '%d passed, %d failed\n' "$pass" "$fail"
+[ "$fail" -eq 0 ]