diff options
| author | hachem <im@hachem.wtf> | 2026-09-12 06:11:26 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-12 06:11:26 +0200 |
| commit | 875e2a75eae13298a36a9f631305a68e40653606 (patch) | |
| tree | cbedfaec52fa8479f278f508b00a8df067031b64 | |
| parent | 6e94238c8e534b8ba6736d65cf20094ec1d7800b (diff) | |
feat: add new options and xmake
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | mkproj.1 | 95 | ||||
| -rw-r--r-- | mkproj.c | 748 | ||||
| -rwxr-xr-x | tests.sh | 139 |
5 files changed, 859 insertions, 129 deletions
@@ -1,4 +1,5 @@ mkproj *.dSYM .DS_Store +tests/ @@ -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 @@ -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 @@ -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 ] |
