From 5f87d9846c4e16942834dd191010a0dc3f94b22a Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Tue, 30 Jul 2024 06:01:39 -0400 Subject: [PATCH] First drop the game's core mechanic that compiles. --- .gitignore | 12 +++ LICENSE | 1 + README.md | 38 +++++++ dbc.h | 49 +++++++++ efsw.wrap | 10 ++ libgit2.wrap | 12 +++ meson.build | 26 +++++ patches/process.h | 223 ++++++++++++++++++++++++++++++++++++++++ scripts/reset_build.ps1 | 14 +++ scripts/reset_build.sh | 14 +++ watchgit.cpp | 208 +++++++++++++++++++++++++++++++++++++ 11 files changed, 607 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dbc.h create mode 100644 efsw.wrap create mode 100644 libgit2.wrap create mode 100644 meson.build create mode 100644 patches/process.h create mode 100644 scripts/reset_build.ps1 create mode 100644 scripts/reset_build.sh create mode 100644 watchgit.cpp 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(); +}