hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-07 22:48:26 +0200
committerhachem <im@hachem.wtf>2026-09-07 22:48:26 +0200
commit774b625c781a563e92224624f757b1e979f73d23 (patch)
tree8a646fcb6e84983209904d9c70f92b064455ad86
parentb8f982f6b9fc9d430c7132456706059218bc6fc2 (diff)
feat: makefile project generation
-rw-r--r--mkproj.c348
1 files changed, 339 insertions, 9 deletions
diff --git a/mkproj.c b/mkproj.c
index 903a16c..66d3728 100644
--- a/mkproj.c
+++ b/mkproj.c
@@ -8,9 +8,15 @@
* Version: 0.1.0
*/
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
#define mkproj_program "mkproj"
#define mkproj_version "0.1.0"
@@ -56,6 +62,8 @@ struct Config
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;
@@ -140,7 +148,7 @@ static void print_usage(FILE *stream)
"have to keep writing build files for small dumb projects.\n"
"\n"
"usage: %s [-l lang] [-s std] [-b system] [--no-git] [--intermediate]\n"
- " [--no-clangd] <name>\n"
+ " [--bin dir] [--bin-int dir] [--no-clangd] <name>\n"
" %s [-v | -h]\n"
"\n"
"options:\n"
@@ -149,6 +157,8 @@ static void print_usage(FILE *stream)
" -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 output directory (default bin)\n"
+ " --bin-int DIR object dir; implies --intermediate (default bin-int)\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"
@@ -248,6 +258,31 @@ static int parse_args(int argc, char **argv, struct Config *config)
continue;
}
+ if (strcmp(arg, "--bin") == 0)
+ {
+ if (++i >= argc)
+ {
+ error("missing argument for", arg);
+ return 1;
+ }
+
+ config->bin_dir = argv[i];
+ continue;
+ }
+
+ if (strcmp(arg, "--bin-int") == 0)
+ {
+ if (++i >= argc)
+ {
+ error("missing argument for", arg);
+ return 1;
+ }
+
+ config->bin_int_dir = argv[i];
+ config->use_intermediate = 1;
+ continue;
+ }
+
if (strcmp(arg, "-l") == 0 || strcmp(arg, "--lang") == 0)
{
if (++i >= argc)
@@ -337,6 +372,305 @@ static int parse_args(int argc, char **argv, struct Config *config)
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 <Foundation/Foundation.h>\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 <print>\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 <iostream>\n"
+ "\n"
+ "int main()\n"
+ "{\n"
+ " std::cout << \"type shi\\n\";\n"
+ "}\n";
+
+ return
+ "#include <iostream>\n"
+ "\n"
+ "auto main() -> int\n"
+ "{\n"
+ " std::cout << \"type shi\\n\";\n"
+ "}\n";
+ }
+
+ return
+ "#include <stdio.h>\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);
+}
+
+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[10];
+ size_t count = 0;
+
+ flags[count++] = "-Wall";
+ flags[count++] = "-Wextra";
+ flags[count++] = "-Wpedantic";
+ flags[count++] = "-Wconversion";
+ flags[count++] = "-Wsign-conversion";
+ flags[count++] = "-Wshadow";
+
+ if (config->language != language_cpp)
+ {
+ flags[count++] = "-Wstrict-prototypes";
+ flags[count++] = "-Wmissing-prototypes";
+ }
+
+ flags[count++] = "-g";
+ flags[count++] = "-fno-omit-frame-pointer";
+
+ (void) fprintf(out, "CC := %s\n\nSANITIZE ?= 1\n\nCFLAGS := ", lang->compiler);
+
+ for (size_t i = 0; i < count; i++)
+ (void) fprintf(out, "%s \\\n ", flags[i]);
+
+ (void) fprintf(out, "-std=%s \\\n -Isrc\n", config->standard);
+
+ (void) fprintf(
+ out,
+ "\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 \\\n"
+ "\t\t--project=compile_commands.json \\\n"
+ "\t\t--enable=warning,style,performance,portability \\\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_gitignore(FILE *out, const struct Config *config)
+{
+ (void) fprintf(out, "%s/\n", config->bin_dir);
+
+ if (config->use_intermediate)
+ (void) fprintf(out, "%s/\n", config->bin_int_dir);
+
+ 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);
+ (void) printf(" make\n");
+ (void) printf(" make check\n\n");
+}
+
+static int generate_project(const struct Config *config)
+{
+ char path[PATH_MAX];
+
+ if (config->build != build_make)
+ {
+ error("build system not yet implemented",
+ build_systems[config->build].key);
+ 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/Makefile", config->name);
+
+ if (emit(path, config, write_makefile) != 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 =
@@ -345,6 +679,8 @@ int main(int argc, char **argv)
.language = language_c,
.standard = NULL,
.build = build_make,
+ .bin_dir = "bin",
+ .bin_int_dir = "bin-int",
.use_git = 1,
.use_intermediate = 0,
.use_clangd_ignore = 1,
@@ -361,14 +697,8 @@ int main(int argc, char **argv)
return EXIT_FAILURE;
}
- /* TODO: generation. for now, echo the resolved config. */
- (void) printf("name: %s\n", config.name);
- (void) printf("language: %s\n", languages[config.language].name);
- (void) printf("standard: %s\n", config.standard);
- (void) printf("build: %s\n", build_systems[config.build].name);
- (void) printf("git: %s\n", config.use_git ? "yes" : "no");
- (void) printf("interm.: %s\n", config.use_intermediate ? "yes" : "no");
- (void) printf("clangd: %s\n", config.use_clangd_ignore ? "yes" : "no");
+ if (generate_project(&config) != 0)
+ return EXIT_FAILURE;
return EXIT_SUCCESS;
}