/* * 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. * * Author: hachem * Version: 0.1.0 */ #include #include #include #include #include #include #include #include #include #define mkproj_program "mkproj" #define mkproj_version "0.1.0" #define mkproj_tagline "dumb project generator" enum Language { language_c, language_cpp, language_objc, language_count }; enum Build_System { build_make, build_cmake, build_meson, build_premake, build_system_count }; struct Language_Info { const char* key; const char* name; const char* compiler; const char* extension; const char* const* standards; size_t standard_count; const char* default_standard; }; struct Build_Info { const char* key; const char* name; const char* default_bin; const char* default_bin_int; }; struct Config { const char* name; enum Language language; const char* standard; enum Build_System build; const char* bin_dir; const char* bin_int_dir; int use_git; int use_intermediate; int use_clangd_ignore; }; static const char* const c_standards[] = { "c89", "c99", "c11", "c17", "c23", "c2y" }; static const char* const cpp_standards[] = { "c++98", "c++03", "c++11", "c++14", "c++17", "c++20", "c++23", "c++26" }; static const struct Language_Info languages[language_count] = { [language_c] = { .key = "c", .name = "C", .compiler = "clang", .extension = "c", .standards = c_standards, .standard_count = sizeof c_standards / sizeof c_standards[0], .default_standard = "c17", }, [language_cpp] = { .key = "cpp", .name = "C++", .compiler = "clang++", .extension = "cpp", .standards = cpp_standards, .standard_count = sizeof cpp_standards / sizeof cpp_standards[0], .default_standard = "c++23", }, [language_objc] = { .key = "objc", .name = "Objective-C", .compiler = "clang", .extension = "m", .standards = c_standards, .standard_count = sizeof c_standards / sizeof c_standards[0], .default_standard = "c17", }, }; static const struct Build_Info build_systems[build_system_count] = { [build_make] = { .key = "make", .name = "Make", .default_bin = "bin", .default_bin_int = "bin-int", }, [build_cmake] = { .key = "cmake", .name = "CMake", .default_bin = "build", .default_bin_int = "build", }, [build_meson] = { .key = "meson", .name = "Meson", .default_bin = "builddir", .default_bin_int = "builddir", }, [build_premake] = { .key = "premake", .name = "premake5", .default_bin = "bin", .default_bin_int = "bin-int", }, }; static void print_version(void) { (void) printf("%s %s\n", mkproj_program, mkproj_version); } static void print_usage(FILE* stream) { (void) fprintf( 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" "\n" "usage: %s [-l lang] [-s std] [-b system] [--no-git] [--intermediate]\n" " [--bin dir] [--bin-int dir] [--no-clangd] \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" " --no-git don't initialize a git repository\n" " --intermediate use a separate intermediate object 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" " -v, --version print version and bail\n" " -h, --help print this and bail\n" "\n" "by hachem \n", mkproj_program, mkproj_version, mkproj_tagline, mkproj_program, mkproj_program); } static int find_language(const char* key, enum Language* out) { for (enum Language language = 0; language < language_count; language++) if (strcmp(languages[language].key, key) == 0) { *out = language; return 1; } return 0; } static int find_build_system(const char* key, enum Build_System* out) { for (enum Build_System build = 0; build < build_system_count; build++) if (strcmp(build_systems[build].key, key) == 0) { *out = build; return 1; } return 0; } static int valid_standard(enum Language language, const char* standard) { const struct Language_Info* info = &languages[language]; for (size_t i = 0; i < info->standard_count; i++) if (strcmp(info->standards[i], standard) == 0) return 1; return 0; } static void error(const char* message, const char* detail) { if (detail != NULL) (void) fprintf(stderr, "%s: error: %s: %s\n", mkproj_program, message, detail); else (void) fprintf(stderr, "%s: error: %s\n", mkproj_program, message); } /* * 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 * usage error. */ 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* bin_arg = NULL; const char* bin_int_arg = NULL; const char* name = NULL; for (int i = 1; i < argc; i++) { const char* arg = argv[i]; if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) { print_usage(stdout); return -1; } if (strcmp(arg, "-v") == 0 || strcmp(arg, "--version") == 0) { print_version(); return -1; } if (strcmp(arg, "--no-git") == 0) { config->use_git = 0; continue; } if (strcmp(arg, "--intermediate") == 0) { config->use_intermediate = 1; continue; } if (strcmp(arg, "--no-clangd") == 0) { config->use_clangd_ignore = 0; continue; } if (strcmp(arg, "--bin") == 0) { if (++i >= argc) { error("missing argument for", arg); return 1; } bin_arg = argv[i]; continue; } if (strcmp(arg, "--bin-int") == 0) { if (++i >= argc) { error("missing argument for", arg); return 1; } bin_int_arg = argv[i]; config->use_intermediate = 1; continue; } if (strcmp(arg, "-l") == 0 || strcmp(arg, "--lang") == 0) { if (++i >= argc) { error("missing argument for", arg); return 1; } lang_arg = argv[i]; continue; } if (strcmp(arg, "-s") == 0 || strcmp(arg, "--std") == 0) { if (++i >= argc) { error("missing argument for", arg); return 1; } std_arg = argv[i]; continue; } if (strcmp(arg, "-b") == 0 || strcmp(arg, "--build") == 0) { if (++i >= argc) { error("missing argument for", arg); return 1; } build_arg = argv[i]; continue; } if (arg[0] == '-' && arg[1] != '\0') { error("unknown option", arg); return 1; } if (name != NULL) { error("unexpected extra argument", arg); return 1; } name = arg; } if (name == NULL) { error("missing project name", NULL); return 1; } config->name = name; if (lang_arg != NULL && !find_language(lang_arg, &config->language)) { error("unknown language", lang_arg); return 1; } if (build_arg != NULL && !find_build_system(build_arg, &config->build)) { error("unknown build system", build_arg); return 1; } if (std_arg != NULL) { if (!valid_standard(config->language, std_arg)) { error("invalid standard for language", std_arg); return 1; } config->standard = std_arg; } else { config->standard = languages[config->language].default_standard; } if (config->use_intermediate && config->build != build_make && config->build != build_premake) { error("--intermediate/--bin-int only apply to make and premake", build_systems[config->build].key); return 1; } config->bin_dir = bin_arg != NULL ? bin_arg : build_systems[config->build].default_bin; config->bin_int_dir = bin_int_arg != NULL ? bin_int_arg : build_systems[config->build].default_bin_int; return 0; } /* run argv to completion. returns the exit code, 127 if it could not be * executed, or -1 on a fork/wait failure. */ static int run(char* const argv[], int quiet) { pid_t pid = fork(); if (pid < 0) return -1; if (pid == 0) { if (quiet) { int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { (void) dup2(devnull, STDOUT_FILENO); (void) dup2(devnull, STDERR_FILENO); (void) close(devnull); } } (void) execvp(argv[0], argv); _exit(127); } int status = 0; if (waitpid(pid, &status, 0) < 0) return -1; if (WIFEXITED(status)) return WEXITSTATUS(status); return -1; } static int git_init(const char* root) { char* const argv[] = { "git", "-C", (char*) root, "init", NULL }; return run(argv, 1); } static const char* main_source(const struct Config* config) { if (config->language == language_objc) return "#import \n" "\n" "int main(void)\n" "{\n" " @autoreleasepool {\n" " (void) NSLog(@\"type shi\");\n" " }\n" "}\n"; if (config->language == language_cpp) { const char* std = config->standard; if (strcmp(std, "c++23") == 0 || strcmp(std, "c++26") == 0) return "#include \n" "\n" "auto main() -> int\n" "{\n" " std::println(\"type shi\");\n" "}\n"; if (strcmp(std, "c++98") == 0 || strcmp(std, "c++03") == 0) return "#include \n" "\n" "int main()\n" "{\n" " std::cout << \"type shi\\n\";\n" "}\n"; return "#include \n" "\n" "auto main() -> int\n" "{\n" " std::cout << \"type shi\\n\";\n" "}\n"; } return "#include \n" "\n" "int main(void)\n" "{\n" " (void) puts(\"type shi\");\n" "}\n"; } static void write_main(FILE* out, const struct Config* config) { (void) fputs(main_source(config), out); } /* 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[]) { size_t count = 0; flags[count++] = "-Wall"; flags[count++] = "-Wextra"; flags[count++] = "-Wshadow"; flags[count++] = "-Wpedantic"; flags[count++] = "-Wconversion"; if (config->language != language_cpp) flags[count++] = "-Wstrict-prototypes"; flags[count++] = "-Wsign-conversion"; if (config->language != language_cpp) flags[count++] = "-Wmissing-prototypes"; return count; } static size_t base_flags(const struct Config* config, const char* flags[]) { size_t count = base_warnings(config, flags); flags[count++] = "-g"; flags[count++] = "-fno-omit-frame-pointer"; return count; } static const char* cmake_language(enum Language language) { if (language == language_cpp) return "CXX"; if (language == language_objc) return "OBJC"; return "C"; } static void write_makefile(FILE* out, const struct Config* config) { const struct Language_Info* lang = &languages[config->language]; const char* ext = lang->extension; const char* obj_dir = config->use_intermediate ? config->bin_int_dir : config->bin_dir; char clean[2 * PATH_MAX + 16]; if (config->use_intermediate) (void) snprintf(clean, sizeof clean, "rm -rf %s %s", config->bin_dir, config->bin_int_dir); else (void) snprintf(clean, sizeof clean, "rm -rf %s", config->bin_dir); const char* flags[16]; size_t count = 0; flags[count++] = "-std=$(STD)"; count += base_warnings(config, flags + count); flags[count++] = "-Isrc"; int width = 0; for (size_t i = 0; i < count; i++) 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); for (size_t i = 0; i < count; i++) { if (i > 0) (void) fprintf(out, " "); if (i < count - 1) (void) fprintf(out, "%-*s \\\n", width, flags[i]); else (void) fprintf(out, "%s\n", flags[i]); } (void) fprintf( out, "\n" "ifeq ($(DEBUG),1)\n" "CFLAGS += -g -fno-omit-frame-pointer\n" "else\n" "CFLAGS += -O2\n" "endif\n" "\n" "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" "\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" "\t$(CC) $(CFLAGS) -c $< -o $@\n" "\n" "check:\n" "\tbear -- make clean all\n" "\tcppcheck --enable=warning,style,performance,portability \\\n" "\t\t --project=compile_commands.json \\\n" "\t\t --error-exitcode=1\n" "\n" "clean:\n" "\t%s\n" "\n" ".PHONY: all check clean\n", config->bin_dir, config->name, ext, obj_dir, obj_dir, ext, ext, clean); } static void write_cmake(FILE* out, const struct Config* config) { const char* name = config->name; const char* ext = languages[config->language].extension; const char* flags[10]; size_t count = base_flags(config, flags); (void) fprintf( out, "cmake_minimum_required(VERSION 3.16)\n" "\n" "project(%s %s)\n" "\n" "option(SANITIZE \"build with asan + ubsan\" ON)\n" "\n" "file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS src/*.%s)\n" "\n" "add_executable(%s ${SOURCES})\n" "\n" "target_include_directories(%s PRIVATE src)\n" "\n" "target_compile_options(%s PRIVATE\n", name, cmake_language(config->language), ext, name, name, 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) (void) fprintf( out, "\n" "target_link_libraries(%s PRIVATE \"-framework Foundation\")\n", name); (void) fprintf( out, "\n" "if(SANITIZE)\n" " target_compile_options(%s PRIVATE -fsanitize=address,undefined)\n" " target_link_options(%s PRIVATE -fsanitize=address,undefined)\n" "endif()\n", name, name); } static const char* meson_language(enum Language language) { if (language == language_cpp) return "cpp"; if (language == language_objc) return "objc"; return "c"; } static void write_meson(FILE* out, const struct Config* config) { const char* name = config->name; const char* ext = languages[config->language].extension; const char* lang = meson_language(config->language); const char* flags[10]; size_t count = base_flags(config, flags); (void) fprintf( out, "project('%s', '%s',\n" " default_options: [\n" " '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); for (size_t i = 0; i < count; i++) (void) fprintf(out, " '%s',\n", flags[i]); (void) fprintf( 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); else (void) fprintf( out, "executable('%s', sources,\n" " include_directories: inc)\n", name); } static const char* premake_language(enum Language language) { if (language == language_cpp) return "C++"; return "C"; } static void write_premake(FILE* out, const struct Config* config) { const char* name = config->name; const char* ext = languages[config->language].extension; const char* obj_dir = config->use_intermediate ? config->bin_int_dir : config->bin_dir; const char* flags[10]; size_t count = base_flags(config, flags); (void) fprintf( out, "workspace \"%s\"\n" " configurations { \"debug\" }\n" "\n" "project \"%s\"\n" " kind \"ConsoleApp\"\n" " language \"%s\"\n" " toolset \"clang\"\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); 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" "\n" " linkoptions { \"-fsanitize=address,undefined\" }\n", config->standard); if (config->language == language_objc) (void) fputs( "\n" " links { \"Foundation.framework\" }\n", out); } static void write_gitignore(FILE* out, const struct Config* config) { (void) fprintf(out, "%s/\n", config->bin_dir); if (config->use_intermediate && (config->build == build_make || config->build == build_premake) && strcmp(config->bin_int_dir, config->bin_dir) != 0) (void) fprintf(out, "%s/\n", config->bin_int_dir); if (config->build == build_premake) (void) fputs("Makefile\n*.make\n", out); if (config->use_clangd_ignore) (void) fputs("\n" ".clangd\n" "compile_flags.txt\n" "compile_commands.json\n", out); } /* open path, hand the stream to writer, close. returns 0 on success. */ static int emit(const char* path, const struct Config* config, void (*writer)(FILE*, const struct Config*)) { FILE* file = fopen(path, "w"); if (file == NULL) { error("cannot create file", path); return -1; } writer(file, config); if (fclose(file) != 0) { error("cannot write file", path); return -1; } return 0; } static int make_dir(const char* path) { if (mkdir(path, 0755) == 0) return 0; if (errno == EEXIST) error("already exists", path); else error("cannot create directory", path); return -1; } static void print_next_steps(const struct Config* config) { (void) printf("\ncreated %s (%s, %s, %s)\n\n", config->name, languages[config->language].name, config->standard, build_systems[config->build].name); (void) printf(" cd %s\n", config->name); switch (config->build) { case build_make: (void) printf(" make\n"); (void) printf(" make check\n"); break; case build_cmake: (void) printf(" cmake -B %s\n", config->bin_dir); (void) printf(" cmake --build %s\n", config->bin_dir); break; case build_meson: (void) printf(" meson setup %s\n", config->bin_dir); (void) printf(" meson compile -C %s\n", config->bin_dir); break; case build_premake: (void) printf(" premake5 gmake2\n"); (void) printf(" make\n"); break; default: break; } (void) printf("\n"); } static int generate_project(const struct Config* config) { char path[PATH_MAX]; const char* build_file; void (*write_build)(FILE*, const struct Config*); switch (config->build) { case build_make: build_file = "Makefile"; write_build = write_makefile; break; 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; default: error("unknown build system", NULL); return -1; } if (make_dir(config->name) != 0) return -1; (void) snprintf(path, sizeof path, "%s/src", config->name); if (make_dir(path) != 0) return -1; (void) snprintf(path, sizeof path, "%s/src/main.%s", config->name, languages[config->language].extension); if (emit(path, config, write_main) != 0) return -1; (void) snprintf(path, sizeof path, "%s/%s", config->name, build_file); if (emit(path, config, write_build) != 0) return -1; (void) snprintf(path, sizeof path, "%s/.gitignore", config->name); if (emit(path, config, write_gitignore) != 0) return -1; if (config->use_git && git_init(config->name) != 0) (void) fprintf(stderr, "%s: warning: git is not installed\n", mkproj_program); print_next_steps(config); { char* const argv[] = { "tree", "-a", "--dirsfirst", "-I", ".git", (char*) config->name, NULL }; if (run(argv, 0) == 127) (void) puts(" (tree is not installed)"); } return 0; } int main(int argc, char** argv) { struct Config config = { .name = NULL, .language = language_c, .standard = NULL, .build = build_make, .bin_dir = NULL, .bin_int_dir = NULL, .use_git = 1, .use_intermediate = 0, .use_clangd_ignore = 1, }; int status = parse_args(argc, argv, &config); if (status < 0) return EXIT_SUCCESS; if (status > 0) { print_usage(stderr); return EXIT_FAILURE; } if (generate_project(&config) != 0) return EXIT_FAILURE; return EXIT_SUCCESS; }