fmake

make any project just by typing `fmake`

commit 6e85efd733da510dcbac2d7f44724c2afabd75f7
parent a3a7be4f1dcf5e5dec804abb2e54eeb05aa7ddf8
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 3 Sep 2024 01:21:10 +0530

Implement build chains

Implement custom chains

Fix Makefile to work on both gmake and nmake

Add build type specifier for both msvc and gcc/clang in Makefile

Add usage in README

Add section for "Build Chains" in README

Bump version to 0.2.3
5 files changed, 310 insertions(+), 131 deletions(-)
M
Makefile
|
13
+++++++++----
M
README
|
79
++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
M
config.h
|
77
++++++++++++++++++++++++++++++++++++++++-------------------------------------
M
config.mk
|
22
++++++++++++++++++----
M
fmake.c
|
250
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
diff --git a/Makefile b/Makefile
@@ -1,16 +1,21 @@
 include config.mk
 
+# not nmake \
+!ifdef 0
+.DEFAULT_GOAL:=fmake
+# \
+!endif
+
 SOURCE = fmake.c config.h
 
 fmake.exe: $(SOURCE)
-	cl /nologo $(cl_CFLAGS) $(CFLAGS) fmake.c
+	cl /nologo $(CFLAGS) fmake.c
 
 fmake: $(SOURCE)
-	$(CC) -o $@ $(_CFLAGS) $(CFLAGS) $(LDFLAGS) fmake.c
+	$(CC) -o $@ $(CFLAGS) $(LDFLAGS) fmake.c
 
 clean:
-	-rm fmake
-	-del fmake.exe
+	-$(RM) fmake fmake.exe fmake.i fmake.pdb fmake.ilk fmake.obj
 
 install: fmake
 	mkdir -p $(DESTDIR)$(PREFIX)/bin
diff --git a/README b/README
@@ -7,43 +7,72 @@ fmake
 | ++ gradle buildDebug         |
 !______________________________!
 
-A tool for the repo nomad.
+fmake is a tool that brings `make`s interface to almost any build system.
 
-fmake is a tool that brings `make`s interface to
-almost any build system.
+fmake offers a functionality similar to vim's makeprg, but does so in a
+way that it can be used in various other programs and applications.
 
-fmake "intelligently" knows what targets to build which can be
+fmake "intelligently" knows what targets to build and can be
 configured in the config.h file.
 
-Opinionates build directory as 'out' in case of no clear build out
-directory standard. Avoids in-source builds.
-
 Usage
 -----
 
-Just type `fmake`, in your project directory to quickly run
-fmake commands.
+fmake [options] [target] ...
+Options:
+  -?            Prints fmake usage
+  -1|2|3        Force fmake to start from the level specified
+  -C path       Change to directory before calling the programs
+  -D            Print various types of debugging information.
+  -N            Don't actually run any build commands; just print them.
+  -V            Print the version number of make and exit.
 
-When none is found, it just defaults to running `make`.
+Build Chains
+------------
+fmake automatically understands the following(and many more) sequences,
+and tries to run it one after the other
 
-Supported build files
----------------------
-* *make
-* bazel
-* cmake
-* configure
-* gradle
-* ninja
-* nobuild
+	$ cmake -Bout .
+	$ cd out
+	$ make
+
+--------(vs)----------
+
+    $ fmake    ------------------=> [ You only have to type fmake! ]
+    ++ cmake -Bout
+        ...
+    ++ cmake --build out
+
+fmake can be forced to re-run the generator using `fmake -2`
 
-and much more! see the config.h for the complete list
 
-Find the complete list in config.h
+Supported build files
+---------------------
+fmake queries filenames from `maker_config_t makers[]` defined in config.h.
 
 Building fmake
 --------------
-For *NIX,
-    make
+*NIX,
+        $ make
+        $ make install
+
+MSVC,
+        * Open Developer Command Prompt *
+        C:\fmake> nmake
+
+Additionally 'type=release' can be passed to build as release.
+Default is debug.
+
+FAQ
+----
+> Why ?
+fmake was born out of my frustration with having to remember specific
+commands for every project I worked on.
+
+> Why default to 'out/' ?
+'out/' is preferred, as more and more projects are using the 'build'
+directory for other purposes like setting up CI/CD.
 
-For MSVC,
-    nmake
+See Also
+--------
+* errorformat by reviewdog
diff --git a/config.h b/config.h
@@ -1,43 +1,46 @@
-/*   level    filetype   lookup name         cmd               cmd args */
-#define BUILD_SYSTEMS \
-	X( 1,     FMAKE_FIL, "Makefile"       , "make") \
-	X( 1,     FMAKE_FIL, "makefile"       , "make") \
-	X( 1,     FMAKE_FIL, "GNUMakefile"    , "gmake") \
-	X( 1,     FMAKE_FIL, "BSDMakefile"    , "bmake") \
-	X( 1,     FMAKE_FIL, "Justfile"       , "just") \
-	X( 1,     FMAKE_EXT, "*.pro"          , "qmake") \
-	X( 1,     FMAKE_EXT, "*.sln"          , "msbuild") \
-	X( 1,     FMAKE_EXT, "*.vcxproj"      , "msbuild") \
-	X( 1,     FMAKE_FIL, "make"           , "sh"               , "make") \
-	X( 1,     FMAKE_FIL, "Gruntfile"      , "grunt") \
-	X( 1,     FMAKE_FIL, "build.sh"       , "sh"               , "build.sh") \
-	X( 1,     FMAKE_FIL, "build.ninja"    , "ninja") \
-	X( 1,     FMAKE_FIL, "OMakefile"      , "omake") \
-	X( 2,     FMAKE_FIL, "configure"      , "sh"               , "configure") \
-	X( 1,     FMAKE_FIL, "configure.ac"   , "autoreconf"       , "-fiv") \
-	X( 2,     FMAKE_FIL, "CMakeLists.txt" , "cmake"            , "-B"           , "out/") \
-	X( 2,     FMAKE_FIL, "BUILD.gn"       , "gn"               , "gen"          , "out/") \
-	X( 1,     FMAKE_FIL, "BUILD"          , "bazel"            , "build") \
-	X( 1,     FMAKE_FIL, "nob"            , "./nob") \
-	X( 1,     FMAKE_FIL, "nob.c"          , "cc"               , "./nob.c"      , "-o"      , "nob") \
-	X( 1,     FMAKE_FIL, "nobuild"        , "./nobuild"        , ) \
-	X( 1,     FMAKE_FIL, "nobuild.c"      , "cc"               , "./nobuild.c"  , "-o"      , "nobuild") \
-	X( 1,     FMAKE_FIL, "packages.json"  , "npm"              , "install") \
-	X( 1,     FMAKE_DIR, "node_modules"   , "npm"              , "run") \
-	X( 1,     FMAKE_FIL, "Cargo.toml"     , "cargo"            , "build") \
-	X( 1,     FMAKE_FIL, "setup.py"       , "pip"              , "install"      , ".") \
-	X( 1,     FMAKE_FIL, "gradlew.bat"    , "./gradlew.bat") \
-	X( 1,     FMAKE_FIL, "gradlew"        , "sh"               , "gradlew") \
-	X( 3,     FMAKE_FIL, "APKBUILD"       , "abuild"           , "-r") \
-	X( 3,     FMAKE_FIL, "PKGBUILD"       , "makepkg"          , "-i")
+#define X(BLEVEL, FTYPE, OTYPE, LOOKFOR, CMD, ...) \
+          BLEVEL, FTYPE, OTYPE, LOOKFOR, CMD, { CMD, __VA_ARGS__}, \
+          (sizeof((char*[]){"" __VA_ARGS__}) / sizeof(char**)) + 1
+
+const maker_config_t cmake_chain = { X(1, FM_FIL, 0, "CMakeCache.txt", "cmake", "--build", ".") };
 
+/*X(level type operation           lookup name         cmd               cmd args) opt chain */
+#define BUILD_SYSTEMS \
+{ X(1, FM_FIL,               0     , "Makefile"       , "make")  },\
+{ X(1, FM_FIL,               0     , "makefile"       , "make")  },\
+{ X(1, FM_FIL,               0     , "GNUMakefile"    , "gmake")  },\
+{ X(1, FM_FIL,               0     , "BSDMakefile"    , "bmake")  },\
+{ X(1, FM_FIL,               0     , "Justfile"       , "just")  },\
+{ X(1, FM_FIL,               0     , "OMakefile"      , "omake")  },\
+{ X(1, FM_FIL,               0     , "make"           , "sh"               , "make")  },\
+{ X(1, FM_FIL,               0     , "build.sh"       , "sh"               , "build.sh")  },\
+{ X(1, FM_FIL,               0     , "Gruntfile"      , "grunt")  },\
+{ X(1, FM_FIL,               0     , "build.ninja"    , "ninja")  },\
+{ X(1, FM_FIL,               0     , "WORKSPACE"      , "bazel"            , "build")  },\
+{ X(1, FM_FIL,               0     , "nob"            , "./nob")  },\
+{ X(1, FM_FIL,               0     , "nob.c"          , "cc"               , "./nob.c"      , "-o"      , "nob")  },\
+{ X(1, FM_FIL,               0     , "nobuild"        , "./nobuild"        , )  },\
+{ X(1, FM_FIL,               0     , "nobuild.c"      , "cc"               , "./nobuild.c"  , "-o"      , "nobuild")  },\
+{ X(1, FM_FIL,               0     , "packages.json"  , "npm"              , "install")  },\
+{ X(1, FM_DIR,               0     , "node_modules"   , "npm"              , "run")  },\
+{ X(1, FM_FIL,               0     , "Cargo.toml"     , "cargo"            , "build")  },\
+{ X(1, FM_FIL,               0     , "setup.py"       , "pip"              , "install"      , ".")  },\
+{ X(1, FM_FIL,               0     , "gradlew.bat"    , "cmd"              , "/c", "gradlew.bat")  },\
+{ X(1, FM_FIL,               0     , "gradlew"        , "sh"               , "gradlew")  },\
+{ X(1, FM_EXT,               0     , "*.pro"          , "qmake")  },\
+{ X(1, FM_EXT,               0     , "*.sln"          , "msbuild")  },\
+{ X(1, FM_EXT,               0     , "*.vcxproj"      , "msbuild")  },\
+{ X(2, FM_FIL, FM_MKD | FM_CBB     , "configure"      , "sh"               , "../configure")  },\
+{ X(2, FM_FIL, FM_CAB              , "CMakeLists.txt" , "cmake"            , "-Bout"), &cmake_chain  },\
+{ X(2, FM_FIL, FM_CAB              , "BUILD.gn"       , "gn"               , "gen"          , "out/")  },\
+{ X(3, FM_FIL,               0     , "configure.ac"   , "autoreconf"       , "-fiv")  },\
+{ X(3, FM_FIL,               0   , "CMakePresets.json", "cmake"            , "--preset", "default")  },\
+{ X(3, FM_FIL,               0     , "APKBUILD"       , "abuild"           , "-r")  },\
+{ X(3, FM_FIL,               0     , "PKGBUILD"       , "makepkg"          , "-i")}
 
 static const maker_config_t makers[] = {
-#define X(MLEVEL, ISEXT, LOOKFOR, CMD, ...) {MLEVEL, ISEXT, LOOKFOR, \
-    CMD, { CMD, __VA_ARGS__}, (sizeof((char*[]){"" __VA_ARGS__}) / sizeof(char**)) },
 BUILD_SYSTEMS
-#undef X
-#undef NUMARGS
 };
 
-static const size_t makers_len = sizeof(makers) / sizeof(maker_config_t);
+#undef BUILD_SYSTEMS
+#undef X
diff --git a/config.mk b/config.mk
@@ -1,6 +1,20 @@
-VERSION = 0.1.6
-
-$(CC)_CFLAGS = -DFMAKE_VERSION=\"$(VERSION)\" -Wall -Wextra -g
-cl_CFLAGS = /DFMAKE_VERSION=\"$(VERSION)\" /Zi
+VERSION = 0.2.3
 
+# nmake \
+!ifndef 0 # \
+CFLAGS = /DFMAKE_VERSION=\"$(VERSION)\"  # \
+!if "$(type)" == "release" # \
+CFLAGS = $(CFLAGS) /ZI /DNDEBUG /O2 # \
+!else # \
+CFLAGS = $(CFLAGS) /Zi # \
+!endif # \
+RM=del # \
+!else
+type?=debug
+debugCFLAGS = -g
+releaseCFLAGS = -O2
+CFLAGS ?= -DFMAKE_VERSION=\"$(VERSION)\" -Wall -Wextra $($(type)CFLAGS)
 PREFIX = /usr/local
+# \
+!endif
+
diff --git a/fmake.c b/fmake.c
@@ -10,71 +10,66 @@
 #include <dirent.h>
 #endif
 
+#ifdef _WIN32
+static HANDLE dir;
+static WIN32_FIND_DATA entry;
+#else
+static DIR *dir;
+static struct dirent *entry;
+#endif
+
 typedef enum {
-	FMAKE_FIL = 0,
-	FMAKE_DIR = 1,
-	FMAKE_EXT = 2
+	FM_FIL = 1,
+	FM_DIR = 2,
+	FM_EXT = 4,
 } filetype_t;
 
-typedef struct {
+typedef enum {
+	FM_NOP = 0,  /* no-op */
+	FM_CBB = 1,  /* chdir before building */
+	FM_CAB = 2,  /* chdir after building */
+	FM_MKD = 4,  /* mkdir */
+} operation_t;
+
+typedef struct maker_config {
 	short level;
 	filetype_t filetype;
-	const char* filename;
-	const char* cmd;
-	const char* args[256];
+	operation_t operation;
+	const char *filename;
+	const char *cmd;
+	const char *args[32];
 	size_t args_len;
+	const struct maker_config *chain;
 } maker_config_t;
 
 #include "config.h"
 
+static const size_t makers_len = sizeof(makers) / sizeof(maker_config_t);
 static maker_config_t maker;
 
-static short should_execute_commands = 1;
-static short is_accepting_cmd_args = 0;
-static short is_verbose = 0;
+static short
+	should_execute_commands = 1, /* if 0 commands will be printed to stdout */
+	level_requested = 1,         /* -1 indicates no level requested */
+	is_verbose = 0;
+	bs_i = 0;
 
-#ifdef _WIN32
-static HANDLE dir;
-static WIN32_FIND_DATA entry;
-#else
-static DIR *dir;
-static struct dirent *entry;
-#endif
+char *errormsg;
+size_t errormsg_len;
 
 #define info(...) \
-	if (should_execute_commands) { \
-		fprintf(stderr, __VA_ARGS__); \
+	if (is_verbose) { \
+		fprintf(stderr, "fmake: " __VA_ARGS__); \
 	}
 
-int
-process_build(int argc, char* argv[]) {
-	int status = -1;
-	info("++");
-	for(size_t bs_i = 0; bs_i < maker.args_len && maker.args[bs_i] != NULL; bs_i++) {
-		fprintf(should_execute_commands? stderr : stdout, " %s", maker.args[bs_i]);
-	}
-	info("\n");
-	if (should_execute_commands) {
-		fflush(stderr);
-		if (*maker.args[0] == '\0') {
-			status = execlp(maker.cmd, maker.cmd, '\0', (void*)0);
-		}
-		else {
-			status = execvp(maker.cmd, (char* const*)maker.args);
-		}
-		if (status == -1) {
-			perror(maker.cmd);
-		}
-	}
-	return status;
-}
+#define errprint(...) \
+		fprintf(stderr, "fmake: " __VA_ARGS__);
 
 inline int
 is_file_present(short filetype, const char* name, size_t name_len) {
 	switch(filetype) {
 #ifdef _WIN32
-		case FMAKE_FIL:
-		case FMAKE_EXT:
+		case FM_FIL:
+		case FM_EXT:
 			dir = FindFirstFile(name, &entry);
 			if (dir == INVALID_HANDLE_VALUE) {
 				return -1;

@@ -91,7 +86,7 @@ is_file_present(short filetype, const char* name, size_t name_len) {
 				}
 			}
 			break;
-		case FMAKE_EXT:
+		case FM_EXT:
 			while ((entry = readdir(dir)) != NULL) {
 				if (entry->d_type == DT_REG || entry->d_type == DT_LNK) {
 					char* dot = strrchr(entry->d_name, '.');

@@ -102,7 +97,7 @@ is_file_present(short filetype, const char* name, size_t name_len) {
 			}
 			break;
 #endif
-		case FMAKE_DIR:
+		case FM_DIR:
 			if (access(name, 0) == 0) {
 				goto FMAKE_FOUND_MATCH;
 			}

@@ -116,15 +111,135 @@ FMAKE_FOUND_MATCH:
 	return 0;
 }
 
+int
+check_files(int maker_i) {
+	do {
+		maker = makers[maker_i];
+		if (is_file_present(maker.filetype,
+				maker.filename, strlen(maker.filename)) == 0)
+			break;
+
+		info("%s\n", maker.filename);
+	} while(++maker_i < makers_len || makers[maker_i].level <= level_requested);
+
+	if (maker_i == makers_len) maker_i = 0;
+	return maker_i;
+}
+
+int
+process_build(int argc, char* argv[]) {
+	int status = -1;
+	FILE* fd = should_execute_commands? stderr : stdout;
+	STARTUPINFO si = {0};
+	PROCESS_INFORMATION pi = {0};
+	int tmpint;
+	size_t cmdargs_len = 0;
+
+	if (should_execute_commands && (maker.operation & FM_MKD)) {
+		errprint("Running mkdir for 'out'\n");
+		if (CreateDirectory("out", NULL) == 0) {
+			switch(GetLastError()) {
+				case ERROR_ALREADY_EXISTS:
+					break;
+				case ERROR_PATH_NOT_FOUND:
+					errprint("Cannot create 'out' directory\n");
+				default:
+					return -1;
+			}
+		}
+	}
+
+	if (maker.operation & FM_CBB) {
+		errprint("Entering directory 'out'\n");
+		if (chdir("out") != 0) {
+			perror("fmake");
+			return -1;
+		}
+	}
+
+	if (should_execute_commands) fprintf(fd, "++ ");
+	for(size_t args_i = 0; args_i < maker.args_len && maker.args[args_i] != NULL; args_i++) {
+		tmpint = strlen(maker.args[args_i]);
+		/* +1 for space */
+		cmdargs_len += tmpint + 1;
+		fprintf(fd, "%.*s ", tmpint, maker.args[args_i]);
+	}
+	if (should_execute_commands) fprintf(fd, "\n");
+	fflush(fd);
+
+	if (!should_execute_commands) return 0;
+
+#ifdef _WIN32
+	char *cmdargs = (char *)malloc(cmdargs_len);
+	tmpint = 0;
+
+	/* Windows can't accept argv[] style args. This converts it to a string */
+	for(int i = 0; i < maker.args_len && maker.args[i] != NULL; i++) {
+		size_t cmdargs_pointer = (size_t) maker.args[i];
+		strcpy(&cmdargs[tmpint], maker.args[i]);
+		tmpint += strlen(maker.args[i]);
+		cmdargs[tmpint++] = ' ';
+	}
+	cmdargs[cmdargs_len] = '\0';
+
+	if (!CreateProcess( NULL, cmdargs, NULL, NULL, FALSE, 0, NULL, NULL,
+				&si, &pi)) {
+		if (GetLastError() == ERROR_FILE_NOT_FOUND) {
+			errprint("%s: not found\n", maker.cmd);
+		}
+		return -1;
+	}
+
+	WaitForSingleObject(pi.hProcess, INFINITE);
+
+	if (GetExitCodeProcess(pi.hProcess, &status) == 0) {
+		errprint("fatal: cannot get process status for %d",  pi.dwProcessId);
+		return -1;
+	}
+
+	CloseHandle(pi.hProcess);
+	CloseHandle(pi.hThread);
+
+#else
+	if (*maker.args[0] == '\0') {
+		status = execlp(maker.cmd, maker.cmd, '\0', (void*)0);
+	}
+	else {
+		status = execvp(maker.cmd, maker.args);
+	}
+#endif
+
+	if (status != 0) {
+		info("'%s' exited with non-zero exit code\n", maker.cmd);
+		return status;
+	}
+
+	if (maker.operation & FM_CAB) {
+		errprint("Entering directory 'out'\n");
+		if (chdir("out") != 0) {
+			perror("fmake");
+			return -1;
+		}
+	}
+
+	if (maker.level > 1) {
+		maker = maker.chain? *maker.chain :
+			makers[check_files(maker.level - 1)];
+		return process_build(argc, argv);
+	}
+	return 0;
+}
+
 void
 fmake_usage(int status) {
 	printf("Usage: fmake [options] [target] ...\n");
 	printf("Options:\n");
 	char* opt_array[] = {
-		"-?", "Prints fmake usage",
-		"-D", "Print various types of debugging information.",
-		"-N", "Don't actually run any build commands; just print them.",
-		"-V", "Print the version number of make and exit."
+		"-?",       "Prints fmake usage",
+		"-C path",  "Change to directory before calling the programs",
+		"-D",       "Print various types of debugging information.",
+		"-N",       "Don't actually run any build commands; just print them.",
+		"-V",       "Print the version number of make and exit."
 	};
 	for(int i = 0; i < sizeof(opt_array) / sizeof(char**); i += 2) {
 		printf("  %-27s %s\n", opt_array[i], opt_array[i + 1]);

@@ -134,15 +249,13 @@ fmake_usage(int status) {
 
 int
 main(int argc, char* argv[]) {
-	int bs_i = 0;
-	int makers_len = sizeof(makers) / sizeof(maker_config_t);
+	char working_dir[256];
 	for(bs_i = 1; bs_i < argc; bs_i++) {
 		if (!(argv[bs_i][0] == '-' && argv[bs_i][1] != '\0')) {
 			exit(-1);
 		}
 		switch(argv[bs_i][1]) {
 			case '-':
-				is_accepting_cmd_args = 1;
 				goto FMAKE_AFTER_ARG_CHECK;
 				break;
 			case 'l':

@@ -155,6 +268,21 @@ main(int argc, char* argv[]) {
 					printf("\n");
 				}
 				break;
+			case 'C':
+				if (++bs_i == argc) {
+					fprintf(stderr, "fmake.exe: Option requires an argument\n");
+					fmake_usage(-1);
+				}
+				if (chdir(argv[bs_i]) == -1){
+					errprint("fmake: *** %s: %s.  Stop.\n", argv[bs_i], strerror(errno));
+					return -1;
+				}
+				break;
+			case '1':
+			case '2':
+			case '3':
+				level_requested = atoi(&argv[bs_i][1]);
+				break;
 			case 'N':
 				should_execute_commands = 0;
 				break;

@@ -170,9 +298,12 @@ main(int argc, char* argv[]) {
 				break;
 		}
 	}
+
 FMAKE_AFTER_ARG_CHECK:
 	argc = argc - bs_i;
 	argv = argv + bs_i;
+	bs_i = -1;
+
 #ifndef _WIN32
 	dir = opendir("./");
 	if (dir == NULL) {

@@ -180,14 +311,11 @@ FMAKE_AFTER_ARG_CHECK:
 		return -1;
 	}
 #endif
-	for (bs_i = 0; bs_i < makers_len; bs_i++) {
-		const char *str = makers[bs_i].filename;
-		if (is_file_present(makers[bs_i].filetype, str, sizeof(makers[bs_i].filename)) == 0) {
-			break;
-		} else if (is_verbose) {
-			fprintf(stderr, "fmake: %s (%d)\n", makers[bs_i].filename, makers[bs_i].filetype);
-		}
-	}
-	maker = makers[bs_i == makers_len? 0 : bs_i];
+
+	/* foward through levels if requested */
+	while(makers[++bs_i].level < level_requested);
+
+	maker = makers[check_files(bs_i)];
+
 	return process_build(argc, argv);
 }