commit 5f87d9846c4e16942834dd191010a0dc3f94b22a Author: Zed A. Shaw Date: Tue Jul 30 06:01:39 2024 -0400 First drop the game's core mechanic that compiles. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2743a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.*.sw* +.DS_Store +*.sqlite3 +*.sqlite3-wal +*.sqlite3-shm +debug +coverage/ +.coverage +builddir +subprojects +*.csv +*.exe diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..789b51d --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Copyright (C) 2024 Zed A. Shaw diff --git a/README.md b/README.md new file mode 100644 index 0000000..986359d --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +A game I'm working. It's a weird one. + +Right now I've got a command that watches your files for changes, and runs a build command. It's +entirely overengineered for this purpose, but it has what I'll need later. To build it do this: + +```shell +./scripts/reset_build.ps1 +meson compile -C builddir +``` + +If you get a bad compile because of libgit2's `src/util/process.h` then do this: + +```shell +cp patches/process.h subprojects/libgit2/src/util/ +meson compile -C builddir +``` + +I don't know why it fails at this, and only on Windows but I've talked to them repeatedly and it's +mostly "works for me" responses. I also can't figure out how Meson exactly applies patches. I've +generated every possible patch I can and Meson just can't apply them. + +Once it's running, we have even more annoying BS to deal with, and that's because Meson doesn't +actually statically compile efsw or libgit2. Even though I said `--default-librart=static --prefer-static` in the setup it just ignores that and makes DLLs. The "fix" for this asinine stupidity is this: + +```shell +meson devenv -C builddir +cd .. +cp builddir/watchgit . +./watchgit . "meson compile -C builddir +``` + +Do all that garbage and yay, this little program runs. Finally. + +## Future Changes + +1. If I can't figure out why libgit2 has compilation errors in `src/util/process.h` then I may just rip it out and just shell out to `git` directly. +2. Ultimately I can jsut use efsw to watch the directory for all changes, but I wanted to only focus on what's in git. Oh well. +3. I also have to find out why efsw and libgit2 refuse to compile statically. Once I do that I can static compile `watchgit` and then work on the next part of the game. diff --git a/dbc.h b/dbc.h new file mode 100644 index 0000000..6e1a70c --- /dev/null +++ b/dbc.h @@ -0,0 +1,49 @@ +#include +#include + +using namespace std; + +namespace dbc { + class Error { + public: + const string message; + Error(string m) : message{m} {} + Error(const char *m) : message{m} {} + }; + + class CheckError : public Error {}; + + class SentinelError : public Error {}; + class PreCondError : public Error {}; + class PostCondError : public Error {}; + + void log(const string &message) { + fmt::print("{}\n", message); + } + + void sentinel(const string &message) { + string err = fmt::format("[SENTINEL!] {}\n", message); + throw SentinelError{err}; + } + + void pre(const string &message, std::function tester) { + if(!tester()) { + string err = fmt::format("[PRE!] {}\n", message); + throw PreCondError{err}; + } + } + + void post(const string &message, std::function tester) { + if(!tester()) { + string err = fmt::format("[POST!] {}\n", message); + throw PostCondError{err}; + } + } + + void check(bool test, const string &message) { + if(!test) { + string err = fmt::format("[CHECK!] {}\n", message); + throw CheckError{err}; + } + } +} diff --git a/efsw.wrap b/efsw.wrap new file mode 100644 index 0000000..4de1f1f --- /dev/null +++ b/efsw.wrap @@ -0,0 +1,10 @@ +[wrap-git] +url = https://github.com/SpartanJ/efsw.git +revision = 1.3.1 +depth = 1 +# patch_filename = +# patch_hash = + + +[provide] +efsw = efsw_dep diff --git a/libgit2.wrap b/libgit2.wrap new file mode 100644 index 0000000..cefe2ec --- /dev/null +++ b/libgit2.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = libgit2-1.8.1 +source_url = https://github.com/libgit2/libgit2/archive/refs/tags/v1.8.1.tar.gz +source_filename = v1.8.1.tar.gz +source_hash = 8c1eaf0cf07cba0e9021920bfba9502140220786ed5d8a8ec6c7ad9174522f8e +wrapdb_version = 2.4.1-3 +# patch_filename = +# patch_hash = + + +[provide] +libgit2 = libgit2_dep diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..221a5a7 --- /dev/null +++ b/meson.build @@ -0,0 +1,26 @@ +project('lcppthw', 'cpp', + default_options: ['cpp_std=c++20']) + +cmake = import('cmake') +opts = cmake.subproject_options() +opts.add_cmake_defines({ + 'USE_ICONV': false, + 'USE_SSH': false, + 'USE_NTLMCLIENT': false, + 'BUILD_SHARED_LIBS': true, + 'BUILD_TESTS': false, + }) +libgit2_proj = cmake.subproject('libgit2', options: opts) +libgit2package_dep = libgit2_proj.dependency('libgit2package') + +efsw_proj = cmake.subproject('efsw') +efsw_dep = efsw_proj.dependency('efsw') + +fmt = dependency('fmt') + +dependencies = [ + fmt, libgit2package_dep, efsw_dep, +] + +executable('watchgit', 'watchgit.cpp', + dependencies: dependencies) diff --git a/patches/process.h b/patches/process.h new file mode 100644 index 0000000..44e009b --- /dev/null +++ b/patches/process.h @@ -0,0 +1,223 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_process_h__ +#define INCLUDE_process_h__ + +typedef struct git_str git_str; +typedef struct git_process git_process; + +typedef struct { + unsigned int capture_in : 1, + capture_out : 1, + capture_err : 1, + exclude_env : 1; + + char *cwd; +} git_process_options; + +typedef enum { + GIT_PROCESS_STATUS_NONE, + GIT_PROCESS_STATUS_NORMAL, + GIT_PROCESS_STATUS_ERROR +} git_process_result_status; + +#define GIT_PROCESS_RESULT_INIT { GIT_PROCESS_STATUS_NONE } + +typedef struct { + git_process_result_status status; + int exitcode; + int signal; +} git_process_result; + +#define GIT_PROCESS_OPTIONS_INIT { 0 } + +#ifdef GIT_WIN32 +# define p_pid_t DWORD +#else +# define p_pid_t pid_t +#endif + +/** + * Create a new process. The command to run should be specified as the + * element of the `arg` array, execv-style. This should be the full path + * to the command to run, the PATH is not obeyed. + * + * This function will add the given environment variables (in `env`) + * to the current environment. Operations on environment variables + * are not thread safe, so you may not modify the environment during + * this call. You can avoid this by setting `exclude_env` in the + * options and providing the entire environment yourself. + * + * @param out location to store the process + * @param args the command (with arguments) to run + * @param args_len the length of the args array + * @param env environment variables to add (or NULL) + * @param env_len the length of the env len + * @param opts the options for creating the process + * @return 0 or an error code + */ +extern int git_process_new( + git_process **out, + const char **args, + size_t args_len, + const char **env, + size_t env_len, + git_process_options *opts); + +/** + * Create a new process. The command to run should be specified as the + * `cmdline` option - which is the full text of the command line as it + * would be specified or run by a user. The command to run will be + * looked up in the PATH. + * + * On Unix, this will be executed by the system's shell (`/bin/sh`) + * and may contain _Bourne-style_ shell quoting rules. On Windows, + * this will be passed to `CreateProcess`, and similarly, may + * contain _Windows-style_ shell quoting rules. + * + * This function will add the given environment variables (in `env`) + * to the current environment. Operations on environment variables + * are not thread safe, so you may not modify the environment during + * this call. You can avoid this by setting `exclude_env` in the + * options and providing the entire environment yourself. + */ +extern int git_process_new_from_cmdline( + git_process **out, + const char *cmdline, + const char **env, + size_t env_len, + git_process_options *opts); + +#ifdef GIT_WIN32 + +extern int git_process__appname( + git_str *out, + const char *cmdline); + +/* Windows path parsing is tricky; this helper function is for testing. */ +extern int git_process__cmdline( + git_str *out, + const char **in, + size_t in_len); + +#endif + +/* + * Whether the given string looks like a command line option (starts + * with a dash). This is useful for examining strings that will become + * cmdline arguments to ensure that they are not erroneously treated + * as an option. For example, arguments to `ssh`. + */ +static __inline__ bool git_process__is_cmdline_option(const char *str) +{ + return (str && str[0] == '-'); +} + +/** + * Start the process. + * + * @param process the process to start + * @return 0 or an error code + */ +extern int git_process_start(git_process *process); + +/** + * Returns the process id of the process. + * + * @param out pointer to a pid_t to store the process id + * @param process the process to query + * @return 0 or an error code + */ +extern int git_process_id(p_pid_t *out, git_process *process); + +/** + * Read from the process's stdout. The process must have been created with + * `capture_out` set to true. + * + * @param process the process to read from + * @param buf the buf to read into + * @param count maximum number of bytes to read + * @return number of bytes read or an error code + */ +extern ssize_t git_process_read(git_process *process, void *buf, size_t count); + +/** + * Read from the process's stderr. The process must have been created with + * `capture_err` set to true. + * + * @param process the process to read from + * @param buf the buf to read into + * @param count maximum number of bytes to read + * @return number of bytes read or an error code + */ +extern ssize_t git_process_read_err(git_process *process, void *buf, size_t count); + +/** + * Write to the process's stdin. The process must have been created with + * `capture_in` set to true. + * + * @param process the process to write to + * @param buf the buf to write + * @param count maximum number of bytes to write + * @return number of bytes written or an error code + */ +extern ssize_t git_process_write(git_process *process, const void *buf, size_t count); + +/** + * Wait for the process to finish. + * + * @param result the result of the process or NULL + * @param process the process to wait on + */ +extern int git_process_wait(git_process_result *result, git_process *process); + +/** + * Close the input pipe from the child. + * + * @param process the process to close the pipe on + */ +extern int git_process_close_in(git_process *process); + +/** + * Close the output pipe from the child. + * + * @param process the process to close the pipe on + */ +extern int git_process_close_out(git_process *process); + +/** + * Close the error pipe from the child. + * + * @param process the process to close the pipe on + */ +extern int git_process_close_err(git_process *process); + +/** + * Close all resources that are used by the process. This does not + * wait for the process to complete. + * + * @parma process the process to close + */ +extern int git_process_close(git_process *process); + +/** + * Place a human-readable error message in the given git buffer. + * + * @param msg the buffer to store the message + * @param result the process result that produced an error + */ +extern int git_process_result_msg(git_str *msg, git_process_result *result); + +/** + * Free a process structure + * + * @param process the process to free + */ +extern void git_process_free(git_process *process); + +#endif diff --git a/scripts/reset_build.ps1 b/scripts/reset_build.ps1 new file mode 100644 index 0000000..75d8515 --- /dev/null +++ b/scripts/reset_build.ps1 @@ -0,0 +1,14 @@ +mv .\subprojects\packagecache . +rm -recurse -force .\subprojects\,.\builddir\ +mkdir subprojects +mv .\packagecache .\subprojects\ +cp *.wrap subprojects +# cp -recurse -force packagefiles subprojects +mkdir builddir +meson wrap install fmt +meson wrap install sqlite3 +meson wrap install sqlitecpp +meson wrap install ftxui +# $env:CC="clang" +# $env:CXX="clang++" +meson setup --default-library=static --prefer-static builddir diff --git a/scripts/reset_build.sh b/scripts/reset_build.sh new file mode 100644 index 0000000..3a35aa3 --- /dev/null +++ b/scripts/reset_build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +mv -f ./subprojects/packagecache . +rm -rf subprojects builddir +mkdir subprojects +mv packagecache ./subprojects/ +mkdir builddir +cp *.wrap subprojects +meson wrap install fmt +meson wrap install sqlite3 +meson wrap install sqlitecpp +meson wrap install ftxui +meson setup builddir diff --git a/watchgit.cpp b/watchgit.cpp new file mode 100644 index 0000000..9a4c35f --- /dev/null +++ b/watchgit.cpp @@ -0,0 +1,208 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "dbc.h" +#include +#include +#include "test.hpp" +#include +#include +#include + +using namespace std; +using namespace fmt; + +#define BUF_MAX 1024 + +/* + * No idea what the semantics of this are. Will need + * to research git's dumb terminology to figure out why + * they have 4 different versions of the path for status. + */ +const char *unfuck_path(const git_status_entry *entry) { + if(entry->head_to_index != nullptr) { + if(entry->head_to_index->new_file.path) { + return entry->head_to_index->new_file.path; + } else { + return entry->head_to_index->old_file.path; + } + } + + if(entry->index_to_workdir != nullptr) { + if(entry->index_to_workdir->new_file.path) { + return entry->index_to_workdir->new_file.path; + } else { + return entry->index_to_workdir->old_file.path; + } + } + + return nullptr; +} + +void add_status(const git_status_entry *entry, unsigned int status_flags, vector &updates) { + const char *path = unfuck_path(entry); + + if(status_flags & GIT_STATUS_WT_NEW + || status_flags & GIT_STATUS_INDEX_NEW) + { + updates.push_back(string{path}); + } + + if(status_flags & GIT_STATUS_WT_MODIFIED + || status_flags & GIT_STATUS_INDEX_MODIFIED) + { + updates.push_back(string{path}); + } + + // need to confirm this gets the new name + if(status_flags & GIT_STATUS_WT_RENAMED + || status_flags & GIT_STATUS_INDEX_RENAMED) + { + updates.push_back(string{path}); + } +} + +class UpdateListener : public efsw::FileWatchListener { + public: + bool changes = false; + + void handleFileAction(efsw::WatchID watchid, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename) override + { + changes = true; + + switch(action) { + case efsw::Actions::Add: + print("ADD {} {} {}\n", dir, filename, oldFilename); + break; + case efsw::Actions::Delete: + print("DEL {} {} {}\n", dir, filename, oldFilename); + break; + case efsw::Actions::Modified: + print("MOD {} {} {}\n", dir, filename, oldFilename); + break; + case efsw::Actions::Moved: + print("MOV {} {} {}\n", dir, filename, oldFilename); + break; + default: + dbc::sentinel("Unknown efsw action."); + } + + } + + void reset_state() { + changes = false; + } +}; + +void list_git_changes(git_repository* repo) { + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *statuses = nullptr; + + //TODO: does this leak? + int err = git_status_list_new(&statuses, repo, &opts); + dbc::check(err == 0, git_error_last()->message); + size_t count = git_status_list_entrycount(statuses); + + vector updates; + + for(size_t i=0; i < count; i++) { + const git_status_entry *entry = git_status_byindex(statuses, i); + add_status(entry, entry->status, updates); + } + + for(string path : updates) { + print("PATH {}\n", path); + } +} + +void run_build(const char* command) { + regex err_re("(.*?):([0-9]+):([0-9]+):\\s*(.*?):\\s*(.*)"); + char buffer[BUF_MAX]; // BUF_MAX is a define already? + smatch err; + ofstream stats_out; + stats_out.open("stats.csv", ios::out | ios::app); + auto t = time(nullptr); + auto tm = *std::gmtime(&t); + + dbc::check(stats_out.good(), "Error opening stats.csv file."); + dbc::pre("simple test", [&]() { return stats_out.good(); }); + + FILE *build_out = popen(command, "r"); + dbc::check(build_out != nullptr, "Failed to run command."); + + while(fgets(buffer, BUF_MAX, build_out) != nullptr) { + string line(buffer); // yeah, that's probably a problem + + print("{}\n", line); + if(regex_match(line, err, err_re)) { + string file_name = err[1].str(); + string line = err[2].str(); + string col = err[3].str(); + string type = err[4].str(); + string message = err[5].str(); + + stats_out << put_time(&tm, "%FT%TZ"); + stats_out << format(",{},{},{},{},{}\n", file_name, line, col, type, message); + } + } + + stats_out.close(); + dbc::post("a post test", [&]() { return !stats_out.is_open(); }); +} + + +int main(int argc, char *argv[]) +{ + git_repository* repo = nullptr; + + try { + dbc::check(argc == 3, "USAGE: watchgit PATH BUILD_CMD"); + const char *git_path = argv[1]; + const char *build_cmd = argv[2]; + + print("Using build command: {}", build_cmd); + efsw::FileWatcher* fileWatcher = new efsw::FileWatcher(); + dbc::check(fileWatcher != nullptr, "Failed to create filewatcher."); + + git_libgit2_init(); + + int err = git_repository_open(&repo, git_path); + dbc::check(err == 0, git_error_last()->message); + + UpdateListener* listener = new UpdateListener(); + dbc::check(listener != nullptr, "Failed to create listener."); + + print("Watching directory {} for changes...\n", git_path); + efsw::WatchID wid = fileWatcher->addWatch(git_path, listener, true); + + while(true) { + fileWatcher->watch(); + + if(listener->changes) { + sleep(1); + list_git_changes(repo); + listener->reset_state(); + run_build(build_cmd); + } + + sleep(1); + } + + git_libgit2_shutdown(); + } catch(dbc::Error &err) { + print("ERROR: {}\n", err.message); + if(repo != nullptr) git_repository_free(repo); + return 1; + } + + git_libgit2_shutdown(); +}