fmake

make any project just by typing `fmake`
git clone git@getsh.org:fmake.git
Log | Files | Refs | README | LICENSE

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

Diffstat:
MMakefile | 13+++++++++----
MREADME | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mconfig.h | 77++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mconfig.mk | 22++++++++++++++++++----
Mfmake.c | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
5 files changed, 310 insertions(+), 131 deletions(-)

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); }