Add project files.
This commit is contained in:
commit
3087fcea8a
71 files changed
+8325
No files matched your search
@@ -0,0 +1,11 @@
|
||||
# _discover_module_subdirectories() (cmake/BuildHelpers.cmake) only
|
||||
# add_subdirectory()'s the IMMEDIATE children of source/examples/ --
|
||||
# this file exists purely so that mechanism finds this folder at all,
|
||||
# then hands off to its three real targets: one host executable and
|
||||
# two builds of the same plugin (see PluginV1/CMakeLists.txt for why
|
||||
# two separate builds rather than one). Shared/ has no CMakeLists.txt
|
||||
# of its own -- it is just PluginApi.h, pulled in by PluginV1/PluginV2
|
||||
# via target_include_directories(), not add_subdirectory().
|
||||
add_subdirectory(PluginV1)
|
||||
add_subdirectory(PluginV2)
|
||||
add_subdirectory(Host)
|
||||
@@ -0,0 +1,15 @@
|
||||
add_example_module(hotreload_demo_host OUTPUT_NAME "hotreload_demo_host" DEPENDS hotreload_lib)
|
||||
|
||||
target_include_directories(hotreload_demo_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Shared)
|
||||
|
||||
# The Host loads PluginV1/PluginV2 itself, at runtime, through
|
||||
# source/HotReload -- it must NOT link against them directly (that
|
||||
# would defeat the entire point of loading them dynamically via
|
||||
# dlopen()/LoadLibrary()), so this is a build-order-only dependency
|
||||
# plus each one's exact build output path baked in as a default, never
|
||||
# a target_link_libraries().
|
||||
add_dependencies(hotreload_demo_host hotreload_demo_plugin_v1 hotreload_demo_plugin_v2)
|
||||
target_compile_definitions(hotreload_demo_host PRIVATE
|
||||
"PLUGIN_V1_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v1>\""
|
||||
"PLUGIN_V2_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v2>\""
|
||||
)
|
||||
@@ -0,0 +1,202 @@
|
||||
/* Main.cpp -- loads PluginV1, calls into it, reloads to PluginV2, and
|
||||
* calls the SAME already-resolved function pointers again to show
|
||||
* they now run the new code -- without the Host re-resolving anything.
|
||||
* Along the way this exercises every HotReloadSymbolEvent kind: watch
|
||||
* the printed event log against PluginV1/src/Plugin.cpp and
|
||||
* PluginV2/src/Plugin.cpp side by side. The second half does the same
|
||||
* thing for CppWidgetApi.h's real C++ (non-extern-"C") surface -- see
|
||||
* that header's comments for why its two methods are each reached a
|
||||
* different way below.
|
||||
*/
|
||||
#include "CppWidgetApi.h"
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using PluginFunction = int (*)(int);
|
||||
|
||||
// CppWidget::tick's real ABI: an implicit this-pointer followed by
|
||||
// its one declared parameter, same calling convention the
|
||||
// compiler itself would generate for widget->tick(x) -- this is
|
||||
// just spelling that out explicitly so it can be called through a
|
||||
// manually-resolved address instead.
|
||||
using CppWidgetTickFunction = int (*)(CppWidget*, int);
|
||||
|
||||
int hostSideDeletedFunctionStub(int)
|
||||
{
|
||||
std::printf(" !! called a deleted plugin function -- this is the Host's stub, not a crash !!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Runs once per HOT_RELOAD_SYMBOL_REMOVED, before onSymbolEvent
|
||||
* below sees the same event -- see hot_reload.h's doc comment on
|
||||
* HotReloadDeletedSymbolFn. Redirecting a removed function to a
|
||||
* stub living in the HOST executable (not in either plugin build)
|
||||
* demonstrates that the target of a patch can be anywhere at all,
|
||||
* not just inside the module being reloaded. */
|
||||
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
|
||||
{
|
||||
std::printf(" [event] REMOVED %-22s -> redirecting old callers to the Host's stub\n", symbolName);
|
||||
return reinterpret_cast<void*>(&hostSideDeletedFunctionStub);
|
||||
}
|
||||
|
||||
const char* eventKindName(HotReloadSymbolEvent kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case HOT_RELOAD_SYMBOL_PATCHED:
|
||||
return "PATCHED";
|
||||
case HOT_RELOAD_SYMBOL_ADDED:
|
||||
return "ADDED";
|
||||
case HOT_RELOAD_SYMBOL_REMOVED:
|
||||
return "REMOVED";
|
||||
case HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE:
|
||||
return "SKIPPED (data, not code)";
|
||||
case HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL:
|
||||
return "SKIPPED (too small to patch)";
|
||||
case HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE:
|
||||
return "SKIPPED (page not writable)";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
||||
{
|
||||
// REMOVED is already reported, with more context, by
|
||||
// onDeletedSymbol() above -- it fires exactly once per removed
|
||||
// symbol, same as this callback would.
|
||||
if (event->kind == HOT_RELOAD_SYMBOL_REMOVED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// event->name is always the real symbol-table name (the exact
|
||||
// string hot_reload_get_symbol() would need) -- demangling it
|
||||
// is purely a display choice, made here and only here; a
|
||||
// plain extern "C" name like "plugin_tick" comes back
|
||||
// unchanged, so this is safe to apply unconditionally.
|
||||
char* readableName = hot_reload_demangle(event->name);
|
||||
std::printf(" [event] %-30s %s\n", eventKindName(event->kind), readableName != nullptr ? readableName : event->name);
|
||||
hot_reload_free_string(readableName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
std::string pluginV1Path = argc > 1 ? argv[1] : PLUGIN_V1_PATH;
|
||||
std::string pluginV2Path = argc > 2 ? argv[2] : PLUGIN_V2_PATH;
|
||||
|
||||
HotReloadOptions options{};
|
||||
options.on_symbol_event = &onSymbolEvent;
|
||||
options.on_deleted_symbol = &onDeletedSymbol;
|
||||
options.user_data = nullptr;
|
||||
|
||||
HotReloadContext* ctx = hot_reload_context_create(&options);
|
||||
if (ctx == nullptr)
|
||||
{
|
||||
std::fprintf(stderr, "hot_reload_context_create failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::printf("== loading plugin V1 (%s) ==\n", pluginV1Path.c_str());
|
||||
HotReloadStatus status = hot_reload_load(ctx, "plugin", pluginV1Path.c_str());
|
||||
if (status != HOT_RELOAD_OK)
|
||||
{
|
||||
std::fprintf(stderr, "hot_reload_load failed: %s (%s)\n", hot_reload_status_string(status),
|
||||
hot_reload_last_error(ctx));
|
||||
hot_reload_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolved ONCE, before the reload below -- and never re-resolved.
|
||||
// The point of this demo is that these exact pointers keep working
|
||||
// correctly after the plugin underneath them changes.
|
||||
auto tick = reinterpret_cast<PluginFunction>(hot_reload_get_symbol(ctx, "plugin", "plugin_tick"));
|
||||
auto legacy = reinterpret_cast<PluginFunction>(hot_reload_get_symbol(ctx, "plugin", "plugin_legacy_feature"));
|
||||
auto markerBefore = static_cast<int*>(hot_reload_get_symbol(ctx, "plugin", "plugin_build_marker"));
|
||||
|
||||
std::printf("\n-- calling V1 through freshly-resolved pointers --\n");
|
||||
tick(21);
|
||||
legacy(7);
|
||||
std::printf(" plugin_build_marker = %d\n", markerBefore != nullptr ? *markerBefore : -1);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// The same plugin build ALSO exports a real C++ class
|
||||
// (CppWidgetApi.h) with no extern "C" anywhere -- a virtual
|
||||
// method and an ordinary out-of-line method, both with real
|
||||
// compiler-mangled names. Same "plugin" module name as above,
|
||||
// since it is physically the same shared library on disk.
|
||||
// ---------------------------------------------------------------
|
||||
std::printf("\n== the same plugin build's C++ surface (CppWidgetApi.h, no extern \"C\") ==\n");
|
||||
|
||||
auto makeCppWidget = reinterpret_cast<CppWidget* (*) ()>(hot_reload_get_symbol(ctx, "plugin", "make_cpp_widget"));
|
||||
CppWidget* widget = makeCppWidget != nullptr ? makeCppWidget() : nullptr;
|
||||
|
||||
// CppWidget::tick(int)'s real, compiler-mangled name -- see
|
||||
// CppWidgetApi.h's comment on why this one is resolved manually
|
||||
// rather than called as widget->tick(x). Re-derive this yourself
|
||||
// with `nm -D --defined-only <plugin path> | c++filt` if you
|
||||
// change CppWidget's signature; there is no portable way to ask
|
||||
// the compiler for another declaration's mangled name at compile
|
||||
// time, which is exactly the inconvenience extern "C" exists to
|
||||
// avoid for anything you need to look up this way.
|
||||
auto tickByMangledName =
|
||||
reinterpret_cast<CppWidgetTickFunction>(hot_reload_get_symbol(ctx, "plugin", "_ZN9CppWidget4tickEi"));
|
||||
|
||||
std::printf("-- V1: virtual method through REAL polymorphic dispatch (widget->virtualTick) --\n");
|
||||
if (widget != nullptr)
|
||||
{
|
||||
widget->virtualTick(21);
|
||||
}
|
||||
std::printf("-- V1: ordinary method through a manually-resolved mangled-name pointer --\n");
|
||||
if (tickByMangledName != nullptr && widget != nullptr)
|
||||
{
|
||||
tickByMangledName(widget, 21);
|
||||
}
|
||||
|
||||
std::printf("\n== reloading \"plugin\" to V2 (%s) ==\n", pluginV2Path.c_str());
|
||||
status = hot_reload_reload(ctx, "plugin", pluginV2Path.c_str());
|
||||
if (status != HOT_RELOAD_OK)
|
||||
{
|
||||
std::fprintf(stderr, "hot_reload_reload failed: %s (%s)\n", hot_reload_status_string(status),
|
||||
hot_reload_last_error(ctx));
|
||||
delete widget;
|
||||
hot_reload_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::printf("\n-- calling the exact same pointers from before, un-re-resolved --\n");
|
||||
tick(21); // same function pointer as above -- now runs V2's tripling logic
|
||||
legacy(7); // same function pointer as above -- now runs the Host's stub instead of V1's body
|
||||
|
||||
std::printf("\n-- plugin_new_feature only exists in V2, resolved fresh --\n");
|
||||
auto newFeature = reinterpret_cast<PluginFunction>(hot_reload_get_symbol(ctx, "plugin", "plugin_new_feature"));
|
||||
if (newFeature != nullptr)
|
||||
{
|
||||
newFeature(21);
|
||||
}
|
||||
|
||||
auto markerAfter = static_cast<int*>(hot_reload_get_symbol(ctx, "plugin", "plugin_build_marker"));
|
||||
std::printf("\n-- plugin_build_marker: proof that data is never patched --\n");
|
||||
std::printf(" resolved fresh from V2 right now = %d\n", markerAfter != nullptr ? *markerAfter : -1);
|
||||
std::printf(" V1's ORIGINAL address, read again = %d (still V1's value -- never touched)\n",
|
||||
markerBefore != nullptr ? *markerBefore : -1);
|
||||
|
||||
std::printf("\n-- the SAME widget object, same vtable slot, same manually-resolved pointer --\n");
|
||||
if (widget != nullptr)
|
||||
{
|
||||
widget->virtualTick(21); // same object, same vtable slot -- now V2's tripling logic
|
||||
}
|
||||
if (tickByMangledName != nullptr && widget != nullptr)
|
||||
{
|
||||
tickByMangledName(widget, 21); // same resolved pointer as before -- also now V2's logic
|
||||
}
|
||||
|
||||
delete widget;
|
||||
hot_reload_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Not built through add_lib_module(): a hot-reloadable plugin is a
|
||||
# different shape from an ordinary library module. PluginV1 and its
|
||||
# sibling PluginV2 are two SEPARATE binaries that deliberately export
|
||||
# the SAME extern "C" symbol names (see ../Shared/PluginApi.h) --
|
||||
# that's exactly what lets HotReloadContext line them up and patch one
|
||||
# into the other at runtime. add_lib_module() assumes one canonical
|
||||
# build per module name and would not fit two builds of the same name
|
||||
# needing to coexist, so this is plain add_library(), reusing the
|
||||
# project's usual output-directory/warnings helpers directly. Gated on
|
||||
# BUILD_EXAMPLES exactly like every other target under source/examples/.
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
add_library(hotreload_demo_plugin_v1 SHARED src/Plugin.cpp src/CppWidget.cpp)
|
||||
else()
|
||||
add_library(hotreload_demo_plugin_v1 SHARED EXCLUDE_FROM_ALL src/Plugin.cpp src/CppWidget.cpp)
|
||||
endif()
|
||||
|
||||
target_include_directories(hotreload_demo_plugin_v1 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Shared)
|
||||
set_target_properties(hotreload_demo_plugin_v1 PROPERTIES
|
||||
OUTPUT_NAME "hotreload_demo_plugin_v1"
|
||||
# Matches add_lib_module()'s own SHARED-library branch
|
||||
# (cmake/BuildHelpers.cmake) exactly, for exactly the same reason:
|
||||
# MinGW's linker exports every global symbol from a DLL by
|
||||
# default, but MSVC exports NONE unless told to -- without this,
|
||||
# PluginApi.h's functions would not be visible in the export table
|
||||
# at all when built with MSVC, silently breaking the whole demo on
|
||||
# that one toolchain.
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
_set_output_dirs(hotreload_demo_plugin_v1
|
||||
"${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples"
|
||||
"${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}/examples")
|
||||
_apply_compile_options(hotreload_demo_plugin_v1)
|
||||
|
||||
# The whole point of this target: give source/HotReload's jump-patch
|
||||
# backends guaranteed room to write into. See cmake/HotReload.cmake.
|
||||
hot_reload_enable_patchable_functions(hotreload_demo_plugin_v1)
|
||||
|
||||
_register_module_target(EXAMPLE hotreload_demo_plugin_v1)
|
||||
add_dependencies(all_examples hotreload_demo_plugin_v1)
|
||||
@@ -0,0 +1,32 @@
|
||||
/* CppWidget V1 -- "before your edit". See PluginV2/src/CppWidget.cpp
|
||||
* for "after": same three exports change behavior together, exactly
|
||||
* like PluginV1/src/Plugin.cpp's plain-C functions do.
|
||||
*/
|
||||
#include "CppWidgetApi.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
int CppWidget::virtualTick(int input)
|
||||
{
|
||||
int result = input * 2;
|
||||
std::printf(" CppWidget::virtualTick(%d) -> %d [V1: input * 2, via vtable]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
int CppWidget::tick(int input)
|
||||
{
|
||||
int result = input * 2;
|
||||
std::printf(" CppWidget::tick(%d) -> %d [V1: input * 2, via mangled-name lookup]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
CppWidget CppWidget::operator+(const CppWidget& /*other*/) const
|
||||
{
|
||||
std::printf(" CppWidget::operator+ [V1]\n");
|
||||
return CppWidget();
|
||||
}
|
||||
|
||||
extern "C" CppWidget* make_cpp_widget()
|
||||
{
|
||||
return new CppWidget();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* PluginV1 -- "before your edit". See PluginV2/src/Plugin.cpp for
|
||||
* "after"; the two deliberately export the identical set of names
|
||||
* (minus plugin_legacy_feature, removed in V2) so HotReloadContext can
|
||||
* line them up. See ../Shared/PluginApi.h for what each export is
|
||||
* there to demonstrate.
|
||||
*/
|
||||
#include "PluginApi.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
int plugin_tick(int input)
|
||||
{
|
||||
int result = input * 2;
|
||||
std::printf(" plugin_tick(%d) -> %d [V1: input * 2]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
int plugin_legacy_feature(int input)
|
||||
{
|
||||
std::printf(" plugin_legacy_feature(%d) -> %d [V1, about to be removed in V2]\n", input, input);
|
||||
return input;
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
int plugin_tiny_function(int input)
|
||||
{
|
||||
return input + 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int plugin_build_marker = 1;
|
||||
@@ -0,0 +1,24 @@
|
||||
# See ../PluginV1/CMakeLists.txt for why this is hand-written instead
|
||||
# of add_lib_module() -- this is that same explanation's other half:
|
||||
# PluginV2 exports the identical extern "C" names as PluginV1 (see
|
||||
# ../Shared/PluginApi.h) with a different implementation standing in
|
||||
# for "the same file, after your edit" in this demo's story.
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
add_library(hotreload_demo_plugin_v2 SHARED src/Plugin.cpp src/CppWidget.cpp)
|
||||
else()
|
||||
add_library(hotreload_demo_plugin_v2 SHARED EXCLUDE_FROM_ALL src/Plugin.cpp src/CppWidget.cpp)
|
||||
endif()
|
||||
|
||||
target_include_directories(hotreload_demo_plugin_v2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Shared)
|
||||
set_target_properties(hotreload_demo_plugin_v2 PROPERTIES
|
||||
OUTPUT_NAME "hotreload_demo_plugin_v2"
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
_set_output_dirs(hotreload_demo_plugin_v2
|
||||
"${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples"
|
||||
"${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}/examples")
|
||||
_apply_compile_options(hotreload_demo_plugin_v2)
|
||||
hot_reload_enable_patchable_functions(hotreload_demo_plugin_v2)
|
||||
|
||||
_register_module_target(EXAMPLE hotreload_demo_plugin_v2)
|
||||
add_dependencies(all_examples hotreload_demo_plugin_v2)
|
||||
@@ -0,0 +1,31 @@
|
||||
/* CppWidget V2 -- "after your edit". See PluginV1/src/CppWidget.cpp
|
||||
* for "before".
|
||||
*/
|
||||
#include "CppWidgetApi.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
int CppWidget::virtualTick(int input)
|
||||
{
|
||||
int result = input * 3;
|
||||
std::printf(" CppWidget::virtualTick(%d) -> %d [V2: input * 3, via vtable]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
int CppWidget::tick(int input)
|
||||
{
|
||||
int result = input * 3;
|
||||
std::printf(" CppWidget::tick(%d) -> %d [V2: input * 3, via mangled-name lookup]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
CppWidget CppWidget::operator+(const CppWidget& /*other*/) const
|
||||
{
|
||||
std::printf(" CppWidget::operator+ [V2]\n");
|
||||
return CppWidget();
|
||||
}
|
||||
|
||||
extern "C" CppWidget* make_cpp_widget()
|
||||
{
|
||||
return new CppWidget();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* PluginV2 -- "after your edit". See PluginV1/src/Plugin.cpp for
|
||||
* "before". plugin_tick's behavior changed, plugin_legacy_feature was
|
||||
* deleted, and plugin_new_feature was added -- exactly the three kinds
|
||||
* of change hot_reload.h's HotReloadSymbolEvent enum distinguishes.
|
||||
*/
|
||||
#include "PluginApi.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
int plugin_tick(int input)
|
||||
{
|
||||
int result = input * 3;
|
||||
std::printf(" plugin_tick(%d) -> %d [V2: input * 3]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* plugin_legacy_feature deliberately does not exist in V2 anymore. */
|
||||
|
||||
int plugin_new_feature(int input)
|
||||
{
|
||||
int result = input * 10;
|
||||
std::printf(" plugin_new_feature(%d) -> %d [V2: brand new]\n", input, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
int plugin_tiny_function(int input)
|
||||
{
|
||||
return input + 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
int plugin_build_marker = 2;
|
||||
@@ -0,0 +1,47 @@
|
||||
/* CppWidgetApi.h -- a C++ (not extern "C") surface living alongside
|
||||
* PluginApi.h's plain C one, in the SAME plugin build. Every member
|
||||
* function below keeps its real, compiler-mangled name; nothing here
|
||||
* needs special handling by source/HotReload, which classifies
|
||||
* symbols by their type and section, never by what their name looks
|
||||
* like -- see hot_reload.h's "REQUIREMENTS ON A HOT-RELOADABLE
|
||||
* MODULE" and README.md's "Hot reloading C++ symbols" for the full
|
||||
* picture.
|
||||
*
|
||||
* The two methods below are reached two different ways from
|
||||
* Host/Main.cpp, deliberately: see that file's comments for why.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class CppWidget
|
||||
{
|
||||
public:
|
||||
virtual ~CppWidget() = default;
|
||||
|
||||
/* Virtual: Host/Main.cpp calls this through ordinary C++
|
||||
* polymorphic dispatch (widget->virtualTick(x)) and needs no
|
||||
* manual symbol lookup at all -- the call compiles down to an
|
||||
* indirect call through the object's vtable slot, not a direct
|
||||
* reference to this specific symbol, so it needs no link-time
|
||||
* dependency on whichever plugin build actually defines it. */
|
||||
virtual int virtualTick(int input);
|
||||
|
||||
/* An ordinary out-of-line method. Calling this as widget->tick(x)
|
||||
* would compile to a direct call to CppWidget::tick's own mangled
|
||||
* symbol, which would need the Host to link against a plugin
|
||||
* build at build time -- exactly what hot-reloading a plugin
|
||||
* means avoiding. So Host/Main.cpp instead resolves it manually
|
||||
* by its mangled name via hot_reload_get_symbol(), the same way
|
||||
* it resolves PluginApi.h's plain C functions. */
|
||||
int tick(int input);
|
||||
|
||||
/* An overloaded operator: mechanically just a function with an
|
||||
* unusual mangled name, reached the same manual way as tick()
|
||||
* above. */
|
||||
CppWidget operator+(const CppWidget& other) const;
|
||||
};
|
||||
|
||||
/* The one entry point Host/Main.cpp looks up by a plain, human-typed
|
||||
* name -- extern "C" here for exactly the reason hot_reload.h
|
||||
* recommends it: something to hand a human-readable name to, without
|
||||
* needing to know or hardcode a mangled string. */
|
||||
extern "C" CppWidget* make_cpp_widget();
|
||||
@@ -0,0 +1,55 @@
|
||||
/* PluginApi.h -- the contract PluginV1 and PluginV2 both implement,
|
||||
* and the Host loads by name. Every function here follows
|
||||
* cpp-style-guide.md section 11's rule for anything loaded at runtime:
|
||||
* plain C linkage, no classes or exceptions crossing the boundary.
|
||||
*
|
||||
* This header is deliberately identical for both plugin versions --
|
||||
* in a real project this is the file that stays stable while
|
||||
* PluginV1/src/Plugin.cpp and PluginV2/src/Plugin.cpp (standing in for
|
||||
* "the same file, before and after your edit") diverge.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/* Changes behavior between V1 and V2 -- watch this one get
|
||||
* HOT_RELOAD_SYMBOL_PATCHED. */
|
||||
int plugin_tick(int input);
|
||||
|
||||
/* Only in V1, gone in V2 -- watch this one get
|
||||
* HOT_RELOAD_SYMBOL_REMOVED, and the Host's stub take over for any
|
||||
* call still aimed at it afterwards. */
|
||||
int plugin_legacy_feature(int input);
|
||||
|
||||
/* Only in V2 -- watch this one get HOT_RELOAD_SYMBOL_ADDED, then
|
||||
* get called for the first time right after. */
|
||||
int plugin_new_feature(int input);
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
/* Deliberately too small to safely hold a jump on any of this
|
||||
* library's backends (its patchable-function-entry padding is
|
||||
* overridden down to zero) -- watch this one get
|
||||
* HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than corrupt whatever
|
||||
* comes after it. MSVC has no equivalent per-function override
|
||||
* this demo could apply, so this one only exists on GCC/Clang.
|
||||
*/
|
||||
__attribute__((patchable_function_entry(0))) int plugin_tiny_function(int input);
|
||||
#endif
|
||||
|
||||
/* An exported VARIABLE, not a function, sitting right next to
|
||||
* real functions in the same library and changing value between
|
||||
* V1 and V2 just like plugin_tick's behavior does -- watch this
|
||||
* one get HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE. This is the
|
||||
* mechanical proof behind hot_reload.h's "only executable code
|
||||
* changes" guarantee: nothing here tells the library "this one is
|
||||
* data, don't touch it" -- it discovers that itself from the
|
||||
* symbol table, the same way it discovers plugin_tick is code.
|
||||
*/
|
||||
extern int plugin_build_marker;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
# See source/examples/HotReloadDemo/CMakeLists.txt for why this
|
||||
# grouping file needs to exist at all (one-level-deep discovery in
|
||||
# cmake/BuildHelpers.cmake).
|
||||
add_subdirectory(Plugin)
|
||||
add_subdirectory(Host)
|
||||
@@ -0,0 +1,8 @@
|
||||
add_example_module(hotreload_live_demo_host OUTPUT_NAME "hotreload_live_demo_host" DEPENDS hotreload_lib)
|
||||
|
||||
# Build-order-only, exactly like HotReloadDemo/Host -- see that file's
|
||||
# CMakeLists.txt comment. This Host must NOT link against the plugin
|
||||
# directly; it loads it dynamically through source/HotReload instead.
|
||||
add_dependencies(hotreload_live_demo_host hotreload_live_plugin)
|
||||
target_compile_definitions(hotreload_live_demo_host PRIVATE
|
||||
"LIVE_PLUGIN_PATH=\"$<TARGET_FILE:hotreload_live_plugin>\"")
|
||||
@@ -0,0 +1,202 @@
|
||||
/* Main.cpp -- a persistent process, unlike source/examples/
|
||||
* HotReloadDemo/Host (which runs a fixed script and exits). This one
|
||||
* loads Plugin/src/LivePlugin.cpp's compiled output once, then polls
|
||||
* that file's last-write time every second: whenever it changes (i.e.
|
||||
* you rebuilt it), it reloads automatically. Run this, then edit and
|
||||
* rebuild the plugin in another terminal -- see LivePlugin.cpp's own
|
||||
* comment for exactly what to try. Stop with Ctrl+C.
|
||||
*/
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
namespace
|
||||
{
|
||||
|
||||
std::atomic<bool> g_running{true};
|
||||
|
||||
void handleSigint(int /*signal*/)
|
||||
{
|
||||
g_running = false;
|
||||
}
|
||||
|
||||
const char* eventKindName(HotReloadSymbolEvent kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case HOT_RELOAD_SYMBOL_PATCHED:
|
||||
return "PATCHED";
|
||||
case HOT_RELOAD_SYMBOL_ADDED:
|
||||
return "ADDED";
|
||||
case HOT_RELOAD_SYMBOL_REMOVED:
|
||||
return "REMOVED";
|
||||
case HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE:
|
||||
return "SKIPPED (data, not code)";
|
||||
case HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL:
|
||||
return "SKIPPED (too small to patch)";
|
||||
case HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE:
|
||||
return "SKIPPED (page not writable)";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
||||
{
|
||||
if (event->kind == HOT_RELOAD_SYMBOL_REMOVED)
|
||||
{
|
||||
return; // reported, with more context, by onDeletedSymbol below
|
||||
}
|
||||
std::printf(" [reload] %-24s %s\n", eventKindName(event->kind), event->name);
|
||||
}
|
||||
|
||||
int fallbackForDeletedTick(int frame)
|
||||
{
|
||||
std::printf(
|
||||
" live_tick was deleted -- this is the Host's fallback (frame %d). Bring it back under the same "
|
||||
"name and rebuild to try LivePlugin.cpp's suggestion #2 through to the end.\n",
|
||||
frame);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
|
||||
{
|
||||
std::printf(" [reload] REMOVED %s -> falling back to the Host's stub\n", symbolName);
|
||||
return reinterpret_cast<void*>(&fallbackForDeletedTick);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Force line buffering regardless of whether stdout is a real
|
||||
// terminal: this is a "watch it happen live" demo, and glibc (and
|
||||
// most libc implementations) fully buffer stdout instead of
|
||||
// line-buffering it the moment it's not connected to a TTY -- a
|
||||
// redirect to a log file, a `| tee`, or some IDEs' embedded
|
||||
// terminals all trigger this. Without this call, output can sit
|
||||
// in the buffer for a long time (observed directly while building
|
||||
// this: zero output appeared for several seconds of real,
|
||||
// correctly-running execution until the buffer was force-flushed).
|
||||
std::setvbuf(stdout, nullptr, _IOLBF, 4096);
|
||||
|
||||
std::string pluginPath = argc > 1 ? argv[1] : LIVE_PLUGIN_PATH;
|
||||
|
||||
std::printf("================================================================\n");
|
||||
std::printf(" Hot Reload -- live demo\n");
|
||||
std::printf("================================================================\n");
|
||||
std::printf(" Watching: %s\n", pluginPath.c_str());
|
||||
std::printf(" Edit: source/examples/HotReloadLiveDemo/Plugin/src/LivePlugin.cpp\n");
|
||||
std::printf(" Rebuild: cmake --build <your build dir> --target hotreload_live_plugin\n");
|
||||
std::printf(" (or run watch.sh, in this example's own directory, in another\n");
|
||||
std::printf(" terminal to rebuild automatically every time you save)\n");
|
||||
std::printf(" Stop: Ctrl+C\n");
|
||||
std::printf("================================================================\n\n");
|
||||
|
||||
std::signal(SIGINT, &handleSigint);
|
||||
|
||||
HotReloadOptions options{};
|
||||
options.on_symbol_event = &onSymbolEvent;
|
||||
options.on_deleted_symbol = &onDeletedSymbol;
|
||||
|
||||
HotReloadContext* ctx = hot_reload_context_create(&options);
|
||||
if (ctx == nullptr)
|
||||
{
|
||||
std::fprintf(stderr, "hot_reload_context_create failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (hot_reload_load(ctx, "live", pluginPath.c_str()) != HOT_RELOAD_OK)
|
||||
{
|
||||
std::fprintf(stderr,
|
||||
"Could not load '%s': %s\n"
|
||||
"(if the file exists but this still fails, that's usually a genuinely undefined symbol in the "
|
||||
"plugin -- e.g. a non-inline static class member that's declared but never defined, only caught "
|
||||
"once something actually calls into the code path that uses it; the message above names it)\n",
|
||||
pluginPath.c_str(), hot_reload_last_error(ctx));
|
||||
hot_reload_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolved ONCE, here, and deliberately never re-resolved below --
|
||||
// the whole point of this demo is watching this same pointer keep
|
||||
// working, correctly, across as many reloads as you make. The one
|
||||
// case where that stops being true: if you delete live_tick (see
|
||||
// LivePlugin.cpp suggestion #2) and later re-add it under the same
|
||||
// name, the re-added one is a NEW, unrelated symbol as far as
|
||||
// diffing is concerned -- there is no "old" address to patch for
|
||||
// it, so it comes back as HOT_RELOAD_SYMBOL_ADDED, not PATCHED, and
|
||||
// this specific pointer (already redirected to the fallback below)
|
||||
// keeps running that fallback rather than picking the new one back
|
||||
// up automatically. Restart the Host to resolve it fresh, or call
|
||||
// hot_reload_get_symbol() again yourself, if you want to see that.
|
||||
using TickFunction = int (*)(int);
|
||||
auto tick = reinterpret_cast<TickFunction>(hot_reload_get_symbol(ctx, "live", "live_tick"));
|
||||
|
||||
std::error_code timeStatus;
|
||||
auto lastWriteTime = std::filesystem::last_write_time(pluginPath, timeStatus);
|
||||
|
||||
int frame = 0;
|
||||
while (g_running)
|
||||
{
|
||||
std::error_code currentTimeStatus;
|
||||
auto currentWriteTime = std::filesystem::last_write_time(pluginPath, currentTimeStatus);
|
||||
if (!currentTimeStatus && currentWriteTime != lastWriteTime)
|
||||
{
|
||||
// A short settle delay for a linker that might still be
|
||||
// mid-write on a slower filesystem -- not load-bearing for
|
||||
// correctness either way: a reload attempted too early
|
||||
// just fails cleanly (hot_reload_reload() never disturbs
|
||||
// the running version on failure) and gets retried next
|
||||
// iteration, since lastWriteTime below is only updated on
|
||||
// success.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
||||
|
||||
std::printf("\n-- change detected in %s, reloading --\n", pluginPath.c_str());
|
||||
HotReloadStatus status = hot_reload_reload(ctx, "live", pluginPath.c_str());
|
||||
if (status == HOT_RELOAD_OK)
|
||||
{
|
||||
lastWriteTime = currentWriteTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf("-- reload failed: %s -- still running the previous version; fix and save again --\n",
|
||||
hot_reload_last_error(ctx));
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
|
||||
if (tick != nullptr)
|
||||
{
|
||||
tick(frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf(" (live_tick not found -- build the plugin; see instructions above)\n");
|
||||
}
|
||||
|
||||
// Unlike tick above, deliberately re-resolved every frame:
|
||||
// live_extra is not expected to exist until you add it (see
|
||||
// LivePlugin.cpp suggestion #3), so the Host has to keep
|
||||
// checking rather than assume it already knows the answer.
|
||||
// This is a cheap lookup over a handful of symbols, not a
|
||||
// reload -- fine to do every frame.
|
||||
using ExtraFunction = int (*)(int);
|
||||
auto extra = reinterpret_cast<ExtraFunction>(hot_reload_get_symbol(ctx, "live", "live_extra"));
|
||||
if (extra != nullptr)
|
||||
{
|
||||
extra(frame);
|
||||
}
|
||||
|
||||
++frame;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
}
|
||||
|
||||
std::printf("\nShutting down.\n");
|
||||
hot_reload_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# The one target this whole example is about rebuilding, in place,
|
||||
# while Host is already running and has it loaded. Hand-written rather
|
||||
# than add_lib_module() for the same reason as HotReloadDemo/PluginV1
|
||||
# -- see that file's comment -- even though there's only one build of
|
||||
# this one: what makes this target unusual isn't needing two versions,
|
||||
# it's needing to be a completely ordinary, repeatedly-overwritable
|
||||
# build output while another process holds an OLDER build of it open
|
||||
# via source/HotReload (see that library's HotReloadContext::
|
||||
# stagePathForLoad() for why the running Host never itself locks this
|
||||
# exact path).
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
add_library(hotreload_live_plugin SHARED src/LivePlugin.cpp)
|
||||
else()
|
||||
add_library(hotreload_live_plugin SHARED EXCLUDE_FROM_ALL src/LivePlugin.cpp)
|
||||
endif()
|
||||
|
||||
set_target_properties(hotreload_live_plugin PROPERTIES
|
||||
OUTPUT_NAME "hotreload_live_plugin"
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
_set_output_dirs(hotreload_live_plugin
|
||||
"${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples"
|
||||
"${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}/examples")
|
||||
_apply_compile_options(hotreload_live_plugin)
|
||||
hot_reload_enable_patchable_functions(hotreload_live_plugin)
|
||||
|
||||
_register_module_target(EXAMPLE hotreload_live_plugin)
|
||||
add_dependencies(all_examples hotreload_live_plugin)
|
||||
@@ -0,0 +1,50 @@
|
||||
/* LivePlugin.cpp -- THE FILE TO EDIT.
|
||||
*
|
||||
* With source/examples/HotReloadLiveDemo/Host running in one
|
||||
* terminal, rebuild just this file from another with:
|
||||
*
|
||||
* cmake --build <your build dir> --target hotreload_live_plugin
|
||||
*
|
||||
* or run watch.sh (in this same directory) in that other terminal to
|
||||
* have it rebuild automatically every time you save. Either way, the
|
||||
* running Host notices within about a second of a successful rebuild
|
||||
* and hot-reloads -- no restart.
|
||||
*
|
||||
* Things worth trying, roughly in order:
|
||||
*
|
||||
* 1. Change the message or the multiplier in live_tick() below and
|
||||
* rebuild -- watch the running Host's output change mid-flight,
|
||||
* still counting the same frame number it already had.
|
||||
*
|
||||
* 2. Delete live_tick() entirely (or rename it) and rebuild -- the
|
||||
* Host supplies a fallback for exactly this case, so you'll see
|
||||
* that kick in instead of a crash. Bring the function back under
|
||||
* the same name and rebuild again... and notice it does NOT
|
||||
* automatically take back over -- see the Host's own comment on
|
||||
* why, right where it resolves live_tick.
|
||||
*
|
||||
* 3. Add a brand new function:
|
||||
*
|
||||
* extern "C" int live_extra(int frame) { ... }
|
||||
*
|
||||
* and rebuild -- the Host looks for this one on every frame and
|
||||
* starts calling it the moment it exists.
|
||||
*
|
||||
* 4. Add a new exported variable, e.g.
|
||||
*
|
||||
* extern "C" int live_marker = 1;
|
||||
*
|
||||
* then change its value on a later edit and rebuild -- watch the
|
||||
* reload log call it out as data, classified and left alone,
|
||||
* never patched, exactly like plugin_build_marker in
|
||||
* source/examples/HotReloadDemo.
|
||||
*/
|
||||
#include <cstdio>
|
||||
|
||||
extern "C" int live_tick(int frame)
|
||||
{
|
||||
int multiplier = 2; // <- try changing this
|
||||
int value = frame * multiplier;
|
||||
std::printf(" live_tick(%d) -> %d\n", frame, value);
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Hot Reload -- live demo
|
||||
|
||||
Unlike `source/examples/HotReloadDemo` (a fixed script that loads two
|
||||
pre-built versions and exits), this one is interactive: a host that
|
||||
keeps running, and one plugin file you edit and rebuild yourself,
|
||||
however many times you like, while it's live.
|
||||
|
||||
## Run it
|
||||
|
||||
```sh
|
||||
cmake -S . -B out/build -DBUILD_EXAMPLES=ON
|
||||
cmake --build out/build
|
||||
./out/build/bin/examples/hotreload_live_demo_host
|
||||
```
|
||||
|
||||
Leave that running. In another terminal, either rebuild by hand after
|
||||
each edit:
|
||||
|
||||
```sh
|
||||
cmake --build out/build --target hotreload_live_plugin
|
||||
```
|
||||
|
||||
or run the watcher in this directory so saving is all you need to do:
|
||||
|
||||
```sh
|
||||
source/examples/HotReloadLiveDemo/watch.sh out/build
|
||||
```
|
||||
|
||||
Then open `Plugin/src/LivePlugin.cpp` -- it has its own comment with
|
||||
several concrete things worth trying, in order (change a value, delete
|
||||
the function, add a new one, add a data symbol). Each one lines up
|
||||
with something `source/HotReload/README.md`'s "Hot reloading C++
|
||||
symbols" and "The two guarantees" sections describe -- this is a place
|
||||
to go watch each of those claims happen, not just read about them.
|
||||
|
||||
## What you should see
|
||||
|
||||
Editing and saving `live_tick()`'s multiplier and rebuilding should
|
||||
change the running Host's printed values within about a second, on
|
||||
the frame count it was already at -- not a restart. This was verified
|
||||
in exactly this shape while building the feature: host running,
|
||||
`watch.sh` running, and nothing else done except editing the file and
|
||||
saving -- the running process picked up the new multiplier on its own
|
||||
a couple of seconds later.
|
||||
|
||||
## If a reload doesn't seem to happen
|
||||
|
||||
- Check the target actually rebuilt -- `watch.sh` prints a build
|
||||
failure clearly rather than silently doing nothing; a plain compile
|
||||
error leaves the previous `.so`/`.dll` completely untouched, which
|
||||
is why the Host would keep running old code without any error of
|
||||
its own in that case.
|
||||
- The Host polls once a second (see `Host/Main.cpp`) -- give it a
|
||||
moment.
|
||||
- Confirm you rebuilt the target this example actually watches,
|
||||
`hotreload_live_plugin`, not the whole project (rebuilding
|
||||
everything works too, it's just slower per iteration).
|
||||
|
||||
## Why rebuilding a `.so`/`.dll` the Host already has loaded works at all
|
||||
|
||||
On Linux/macOS this is unremarkable -- a running process's existing
|
||||
mapping of a file survives the file being replaced, which is exactly
|
||||
this project's normal build-then-relink cycle.
|
||||
|
||||
On Windows it's normally *not* possible to overwrite or delete a DLL
|
||||
file a running process has loaded, which would otherwise make this
|
||||
exact workflow (keep the Host running, rebuild the DLL it already
|
||||
loaded) fail with the linker unable to write its output. This example
|
||||
avoids that for a reason that already exists elsewhere in this
|
||||
library, not anything specific to this demo:
|
||||
`HotReloadContext::stagePathForLoad()` (`source/HotReload/src/
|
||||
HotReloadContext.cpp`) copies a plugin to a fresh, uniquely-named file
|
||||
before ever calling `LoadLibrary`/`dlopen` on it -- built originally to
|
||||
sidestep a dlopen caching gotcha on POSIX (see that function's
|
||||
comment) -- so the path this example's build actually writes to is
|
||||
never the one Windows locks. This reasoning was not verified against
|
||||
a real Windows rebuild-while-running cycle specifically (unlike the
|
||||
rest of this library's Windows behavior, which was exercised under
|
||||
Wine -- see `source/HotReload/README.md`'s verification notes); if a
|
||||
rebuild on Windows still fails while the Host is running, that
|
||||
mismatch between reasoning and reality is worth reporting.
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rebuilds hotreload_live_plugin every time LivePlugin.cpp's mtime
|
||||
# changes, so you can just edit-and-save instead of switching to a
|
||||
# terminal to rebuild by hand. Run this in one terminal; run
|
||||
# hotreload_live_demo_host (built once) in another; edit
|
||||
# Plugin/src/LivePlugin.cpp in your editor. Matches this project's
|
||||
# other scripts/*.sh conventions (see scripts/run.sh) even though it
|
||||
# lives here rather than in scripts/, since it's specific to this one
|
||||
# example rather than general project tooling.
|
||||
#
|
||||
# Usage (from anywhere):
|
||||
# source/examples/HotReloadLiveDemo/watch.sh [build-dir]
|
||||
#
|
||||
# build-dir defaults to out/build (relative to the project root, same
|
||||
# default as scripts/build.sh); pass your own if you configured
|
||||
# somewhere else, e.g.:
|
||||
# source/examples/HotReloadLiveDemo/watch.sh out/build-clang
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR/../../.."
|
||||
|
||||
BUILD_DIR="${1:-out/build}"
|
||||
SOURCE_FILE="$SCRIPT_DIR/Plugin/src/LivePlugin.cpp"
|
||||
TARGET="hotreload_live_plugin"
|
||||
|
||||
if [ ! -d "$BUILD_DIR" ]; then
|
||||
echo "error: build directory '$BUILD_DIR' not found." >&2
|
||||
echo "Configure it first, e.g.:" >&2
|
||||
echo " cmake -S . -B $BUILD_DIR -DBUILD_EXAMPLES=ON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file_mtime() {
|
||||
# GNU stat (Linux) and BSD/macOS stat use incompatible flags --
|
||||
# try one, fall back to the other.
|
||||
stat -c %Y "$SOURCE_FILE" 2>/dev/null || stat -f %m "$SOURCE_FILE"
|
||||
}
|
||||
|
||||
echo "Watching: $SOURCE_FILE"
|
||||
echo "Rebuilds into: $BUILD_DIR (target: $TARGET)"
|
||||
echo "Edit and save that file -- Ctrl+C here to stop watching (the running Host keeps going either way)."
|
||||
echo ""
|
||||
|
||||
last="$(file_mtime)"
|
||||
while true; do
|
||||
current="$(file_mtime)"
|
||||
if [ "$current" != "$last" ]; then
|
||||
last="$current"
|
||||
echo "---- rebuilding ($(date '+%H:%M:%S')) ----"
|
||||
if cmake --build "$BUILD_DIR" --target "$TARGET"; then
|
||||
echo "---- done -- the running Host should pick this up within about a second ----"
|
||||
else
|
||||
echo "---- build failed -- fix the error above and save again ----"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
Reference in new issue
Block a user