From 57bb30934695a50911f64f52eabb1f02efdcd9e3 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 30 Jul 2026 05:59:40 -0700 Subject: [PATCH 1/4] fixes blank GIT_HASH and SW_BUILD in FITS headers Dead update_git_commit target replaced with configure-time execute_process. Hash marked -dirty, re-stamps when HEAD moves. --- CMakeLists.txt | 50 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a963f06b..563e5735 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,30 +27,40 @@ find_package( Threads ) message( STATUS "CMAKE_BINARY_DIR" ${CMAKE_BINARY_DIR} ) -# Define the output file for the git commit hash, and -# touch it in case we're starting from scratch. +# Get the git commit hash and the build date so that the config files can use +# them. These are evaluated at configure time and stamped into the generated +# .cfg files, so a re-configure is what refreshes them. # -set(GIT_COMMIT_FILE "${CMAKE_BINARY_DIR}/git_commit.txt") -file( TOUCH ${GIT_COMMIT_FILE} ) +execute_process( COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE ) -# Create a custom target with a command that runs every time make is invoked. -# This creates the file which contains SET commands to set cmake variables. +# Mark the hash when the tree has uncommitted changes, otherwise the hash names +# a commit that does not describe what was actually built. # -add_custom_target(update_git_commit - COMMAND ${CMAKE_COMMAND} -E remove ${GIT_COMMIT_FILE} - COMMAND bash -c "echo -n set '\\( GIT_COMMIT_HASH ' " > ${GIT_COMMIT_FILE} && - bash -c "git rev-parse HEAD " | awk '{print $0}' | xargs echo -n >> ${GIT_COMMIT_FILE} && - bash -c "echo ' \\)'" >> ${GIT_COMMIT_FILE} - COMMAND bash -c "echo -n set '\\( PROJECT_BUILD_DATE ' " >> ${GIT_COMMIT_FILE} && - bash -c "date +%Y-%m-%dT%H:%M:%S_%Z" | awk '{print $0}' | xargs echo -n >> ${GIT_COMMIT_FILE} && - bash -c "echo ' \\)'" >> ${GIT_COMMIT_FILE} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMENT "Updating git commit hash file" -) - -# include the git commit hash output file so that config files can use it +execute_process( COMMAND git diff --quiet HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE GIT_TREE_DIRTY ) + +if ( NOT GIT_TREE_DIRTY EQUAL 0 ) + set( GIT_COMMIT_HASH "${GIT_COMMIT_HASH}-dirty" ) +endif() + +string( TIMESTAMP PROJECT_BUILD_DATE "%Y-%m-%dT%H:%M:%S" UTC ) + +message( STATUS "GIT_COMMIT_HASH " ${GIT_COMMIT_HASH} " PROJECT_BUILD_DATE " ${PROJECT_BUILD_DATE} ) + +# Re-configure whenever the checked-out commit changes, so the stamps above +# cannot go stale. --git-path resolves correctly inside a linked worktree, +# where .git is a file rather than a directory. # -INCLUDE( ${GIT_COMMIT_FILE} ) +execute_process( COMMAND git rev-parse --git-path HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_HEAD_FILE + OUTPUT_STRIP_TRAILING_WHITESPACE ) + +set_property( DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GIT_HEAD_FILE} ) # Here is everything to build # From a40c78a4951bc3bc9315da4dee8608c7dc3c22ae Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 30 Jul 2026 08:18:29 -0700 Subject: [PATCH 2/4] adds build provenance to daemon logs and FITS headers each daemon stamps its git hash and binary build time into every publish camerad aggregates running daemons into GIT_HASH and SW_BUILD --- CMakeLists.txt | 52 +++++++++++++++++++++++--------------- Config/camerad.cfg.in | 3 --- acamd/acamd.cpp | 2 +- calibd/calibd.cpp | 2 +- camerad/astrocam.cpp | 53 +++++++++++++++++++++++++++++++++++++++ camerad/astrocam.h | 7 ++++++ camerad/camerad.cpp | 6 +++-- camerad/camerad.h | 18 ------------- common/common.h | 13 +++++++++- common/fits_header_defs.h | 14 +++++++++++ common/message_keys.h | 6 +++++ flexured/flexured.cpp | 2 +- focusd/focusd.cpp | 2 +- powerd/powerd.cpp | 2 +- sequencerd/sequencerd.cpp | 2 +- slicecamd/slicecamd.cpp | 2 +- slitd/slitd.cpp | 2 +- tcsd/tcsd.cpp | 2 +- telemd/telemd.cpp | 2 +- thermald/thermald.cpp | 2 +- utils/.gitignore | 2 ++ utils/build_date.h | 34 +++++++++++++++++++++++++ utils/version.h.in | 14 +++++++++++ 23 files changed, 189 insertions(+), 55 deletions(-) create mode 100644 utils/.gitignore create mode 100644 utils/version.h.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 563e5735..0d09ff7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,40 +27,52 @@ find_package( Threads ) message( STATUS "CMAKE_BINARY_DIR" ${CMAKE_BINARY_DIR} ) -# Get the git commit hash and the build date so that the config files can use -# them. These are evaluated at configure time and stamped into the generated -# .cfg files, so a re-configure is what refreshes them. +# Get the git commit hash identifying the source that this build came from. +# It is compiled into every daemon via the generated version.h below, so that +# each binary can report the commit it was built from. # execute_process( COMMAND git rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE ) + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE GIT_RESULT + ERROR_QUIET ) -# Mark the hash when the tree has uncommitted changes, otherwise the hash names -# a commit that does not describe what was actually built. +# git failing here would silently leave the hash empty, +# so make the failure visible instead # -execute_process( COMMAND git diff --quiet HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - RESULT_VARIABLE GIT_TREE_DIRTY ) - -if ( NOT GIT_TREE_DIRTY EQUAL 0 ) - set( GIT_COMMIT_HASH "${GIT_COMMIT_HASH}-dirty" ) +if ( NOT GIT_RESULT EQUAL 0 OR GIT_COMMIT_HASH STREQUAL "" ) + set( GIT_COMMIT_HASH "unknown" ) + message( WARNING "unable to determine git commit hash: reporting GIT_HASH=unknown" ) endif() -string( TIMESTAMP PROJECT_BUILD_DATE "%Y-%m-%dT%H:%M:%S" UTC ) +message( STATUS "GIT_COMMIT_HASH " ${GIT_COMMIT_HASH} ) -message( STATUS "GIT_COMMIT_HASH " ${GIT_COMMIT_HASH} " PROJECT_BUILD_DATE " ${PROJECT_BUILD_DATE} ) - -# Re-configure whenever the checked-out commit changes, so the stamps above -# cannot go stale. --git-path resolves correctly inside a linked worktree, -# where .git is a file rather than a directory. +# Re-configure whenever the checked-out commit changes, so the hash cannot go +# stale. --git-path resolves correctly inside a linked worktree, where .git is +# a file rather than a directory. # execute_process( COMMAND git rev-parse --git-path HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_HEAD_FILE - OUTPUT_STRIP_TRAILING_WHITESPACE ) + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET ) + +if ( GIT_HEAD_FILE ) + set_property( DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GIT_HEAD_FILE} ) +endif() + +# Generate version.h into utils/ which is already on every daemon's include +# path. It is written through a temporary so that copy_if_different can leave +# the real header untouched when the hash has not changed, which keeps an +# unrelated re-configure from forcing a rebuild of everything. +# +CONFIGURE_FILE( ${PROJECT_BASE_DIR}/utils/version.h.in + ${CMAKE_BINARY_DIR}/version.h.tmp ) -set_property( DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GIT_HEAD_FILE} ) +execute_process( COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_BINARY_DIR}/version.h.tmp + ${PROJECT_BASE_DIR}/utils/version.h ) # Here is everything to build # diff --git a/Config/camerad.cfg.in b/Config/camerad.cfg.in index 10ff478a..e94712e7 100644 --- a/Config/camerad.cfg.in +++ b/Config/camerad.cfg.in @@ -2,9 +2,6 @@ # configuration file for camerad # @CAMERAD_CFG_COMMENT@ # ----------------------------------------------------------------------------- -GIT_HASH=@GIT_COMMIT_HASH@ # git commit hash -PROJ_BUILD_DATE=@PROJECT_BUILD_DATE@ # the last time make was run -# ----------------------------------------------------------------------------- IMDIR=/data # default root directory for images BASENAME=ngps # default basename for images BLKPORT=@CAMERAD_BLK_PORT@ # server blocking port diff --git a/acamd/acamd.cpp b/acamd/acamd.cpp index 3664b7b8..57d9dfed 100644 --- a/acamd/acamd.cpp +++ b/acamd/acamd.cpp @@ -151,7 +151,7 @@ int main(int argc, char **argv) { message.str(""); message << "[NOTICE] user id = " << uid << " group id = " << gid; logwrite( function, message.str() ); - message.str(""); message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message.str(""); message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << acamd.config.n_entries << " lines read from " << acamd.config.filename; diff --git a/calibd/calibd.cpp b/calibd/calibd.cpp index 70663f55..bd8f3037 100644 --- a/calibd/calibd.cpp +++ b/calibd/calibd.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { calibd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << calibd.config.n_entries << " lines read from " << calibd.config.filename; diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 39db994d..7c2962dd 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -71,6 +71,10 @@ namespace AstroCam { // provider, keyed by topic. The JSON->FITS-keyword conversion is deferred // to exposure lock-in (see do_expose / add_cached_telem). // + void Interface::handletopic_acam( const nlohmann::json &jmessage ) { + std::unique_lock lock(live_telemetry_mtx); + this->live_telemetry[Topic::ACAMD] = jmessage; + } void Interface::handletopic_calib( const nlohmann::json &jmessage ) { std::unique_lock lock(live_telemetry_mtx); this->live_telemetry[Topic::CALIBD] = jmessage; @@ -87,6 +91,10 @@ namespace AstroCam { std::unique_lock lock(live_telemetry_mtx); this->live_telemetry[Topic::POWERD] = jmessage; } + void Interface::handletopic_slicecam( const nlohmann::json &jmessage ) { + std::unique_lock lock(live_telemetry_mtx); + this->live_telemetry[Topic::SLICECAMD] = jmessage; + } void Interface::handletopic_slit( const nlohmann::json &jmessage ) { std::unique_lock lock(live_telemetry_mtx); this->live_telemetry[Topic::SLITD] = jmessage; @@ -129,6 +137,38 @@ namespace AstroCam { /***** AstroCam::Interface::get_live_airmass ********************************/ + /***** AstroCam::Interface::get_live_provenance *****************************/ + /** + * @brief return the build provenance of all running daemons + * @details Every publisher stamps its git hash and build time into every + * message (see Common::PubSub::publish), so the cached snapshots + * describe the software that was actually running. A daemon that + * was rebuilt but not restarted still reports its old build, + * which is correct because the old binary produced the data. + * @param[out] githash common commit, or "MIXED" if running daemons differ + * @param[out] buildtime newest build time among running daemons + * + */ + void Interface::get_live_provenance( std::string &githash, std::string &buildtime ) { + githash = GIT_COMMIT_HASH; + buildtime = get_build_time(); + + std::unique_lock lock(live_telemetry_mtx); + + for ( const auto &[topic, jmsg] : this->live_telemetry ) { + if ( jmsg.contains( Key::GITHASH ) && jmsg.at( Key::GITHASH ).is_string() ) { + const std::string hash = jmsg.at( Key::GITHASH ).get(); + if ( !hash.empty() && hash != githash ) githash = "MIXED"; + } + if ( jmsg.contains( Key::BUILDTIME ) && jmsg.at( Key::BUILDTIME ).is_string() ) { + const std::string btime = jmsg.at( Key::BUILDTIME ).get(); + if ( btime > buildtime ) buildtime = btime; + } + } + } + /***** AstroCam::Interface::get_live_provenance *****************************/ + + long NewAstroCam::new_expose( std::string nseq_in ) { logwrite( "NewAstroCam::new_expose", nseq_in ); return( NO_ERROR ); @@ -2826,6 +2866,19 @@ namespace AstroCam { this->camera_info.telemkeys = telem; } + // Record what software was running for this exposure. Done outside the + // block above because get_live_provenance() takes live_telemetry_mtx + // itself. + // + std::string githash, buildtime; + this->get_live_provenance( githash, buildtime ); + this->camera_info.telemkeys.primary().addkey( FitsHeaderKeys::Provenance::GITHASH_KEY, + githash, + FitsHeaderKeys::Provenance::GITHASH_COMMENT ); + this->camera_info.telemkeys.primary().addkey( FitsHeaderKeys::Provenance::BUILDTIME_KEY, + buildtime, + FitsHeaderKeys::Provenance::BUILDTIME_COMMENT ); + // Make a copy of this->camera_info for this particular exposure buffer number. // This expinfo will be used for this particular exposure. // Any changes to camera_info hereafter will not be used for this exposure. diff --git a/camerad/astrocam.h b/camerad/astrocam.h index 10048284..0901b248 100644 --- a/camerad/astrocam.h +++ b/camerad/astrocam.h @@ -639,6 +639,8 @@ namespace AstroCam { topic_handlers = { { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, + { Topic::ACAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_acam(msg); } ) }, { Topic::CALIBD, std::function( [this](const nlohmann::json &msg) { handletopic_calib(msg); } ) }, { Topic::FLEXURED, std::function( @@ -647,6 +649,8 @@ namespace AstroCam { [this](const nlohmann::json &msg) { handletopic_focus(msg); } ) }, { Topic::POWERD, std::function( [this](const nlohmann::json &msg) { handletopic_power(msg); } ) }, + { Topic::SLICECAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_slicecam(msg); } ) }, { Topic::SLITD, std::function( [this](const nlohmann::json &msg) { handletopic_slit(msg); } ) }, { Topic::TARGETINFO, std::function( @@ -713,10 +717,12 @@ namespace AstroCam { void publish_status(bool force=false); void request_snapshot(); void handletopic_snapshot(const nlohmann::json &jmessage_in); + void handletopic_acam(const nlohmann::json &jmessage_in); void handletopic_calib(const nlohmann::json &jmessage_in); void handletopic_flexure(const nlohmann::json &jmessage_in); void handletopic_focus(const nlohmann::json &jmessage_in); void handletopic_power(const nlohmann::json &jmessage_in); + void handletopic_slicecam(const nlohmann::json &jmessage_in); void handletopic_slit(const nlohmann::json &jmessage_in); void handletopic_targetinfo(const nlohmann::json &jmessage_in); void handletopic_tcs(const nlohmann::json &jmessage_in); @@ -1236,6 +1242,7 @@ std::vector> fitsinfo; long expose(std::string nexp_in); long do_expose(int nexp_in); double get_live_airmass(); ///< latest airmass from cached tcsd telemetry, or NAN + void get_live_provenance( std::string &githash, std::string &buildtime ); ///< build provenance of all running daemons long native(std::string cmdstr); long native(std::string cmdstr, std::string &retstring); diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index f0730868..31b432b0 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -158,7 +158,7 @@ int main(int argc, char **argv) { // log build date and hash // - message.str(""); message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message.str(""); message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite( function, message.str() ); message.str(""); message << server.config.n_entries << " lines read from " << server.config.filename; @@ -180,10 +180,12 @@ int main(int argc, char **argv) { // initialize the pub-sub handler with my subscriber topics // - if ( server.init_pubsub( { Topic::CALIBD, + if ( server.init_pubsub( { Topic::ACAMD, + Topic::CALIBD, Topic::FLEXURED, Topic::FOCUSD, Topic::POWERD, + Topic::SLICECAMD, Topic::SLITD, Topic::TARGETINFO, Topic::TCSD, diff --git a/camerad/camerad.h b/camerad/camerad.h index 5d67c9fb..9dbf6a38 100644 --- a/camerad/camerad.h +++ b/camerad/camerad.h @@ -239,24 +239,6 @@ namespace Camera { applied++; } - // GIT_HASH - if (config.param[entry].compare(0, 8, "GIT_HASH")==0) { - this->camera_info.systemkeys.primary().addkey( "GIT_HASH", config.arg[entry], "software git hash" ); - message.str(""); message << "CAMERAD:config:" << config.param[entry] << "=" << config.arg[entry]; - logwrite( function, message.str() ); - this->camera.async.enqueue( message.str() ); - applied++; - } - - // PROJ_BUILD_DATE - if (config.param[entry].compare(0, 15, "PROJ_BUILD_DATE")==0) { - this->camera_info.systemkeys.primary().addkey( "SW_BUILD", config.arg[entry], "software build date" ); - message.str(""); message << "CAMERAD:config:" << config.param[entry] << "=" << config.arg[entry]; - logwrite( function, message.str() ); - this->camera.async.enqueue( message.str() ); - applied++; - } - } // end loop through the entries in the configuration file message.str(""); diff --git a/common/common.h b/common/common.h index 28072586..88e7ada5 100644 --- a/common/common.h +++ b/common/common.h @@ -25,6 +25,8 @@ #include "logentry.h" #include "network.h" #include "message_keys.h" +#include "build_date.h" +#include "version.h" // generated by CMake, defines GIT_COMMIT_HASH const long NOTHING = -1; const long NO_ERROR = 0; @@ -192,12 +194,21 @@ namespace Common { if ( _mode != Mode::PUB ) { throw std::runtime_error( "(Common::PubSub::publish) not a publisher" ); } + // Every message carries the build provenance of the publishing binary + // so that a consumer can tell which software produced it. This is done + // here rather than at each call site so that no publisher can omit it. + // + static const std::string build_time = get_build_time(); + json message = message_out; + message[Key::GITHASH] = GIT_COMMIT_HASH; + message[Key::BUILDTIME] = build_time; + std::lock_guard lock( _publish_mtx ); // serialize the non-thread-safe socket zmqpp::message message_zmq; // Publish to either class default _topic or topic specified as // optional arg. message_zmq.add( topic.empty() ? _topic : topic ); - message_zmq.add( message_out.dump() ); + message_zmq.add( message.dump() ); _socket.send( message_zmq ); } /***** Common::PubSub::publish ******************************************/ diff --git a/common/fits_header_defs.h b/common/fits_header_defs.h index e62fc210..63d39b42 100644 --- a/common/fits_header_defs.h +++ b/common/fits_header_defs.h @@ -133,4 +133,18 @@ namespace FitsHeaderKeys { { Key::Slitd::SLITW.c_str(), "SLITW", "slit width in arcsec", "FLOAT" } }; + /** + * @namespace Provenance + * @brief build provenance keywords + * @details These are aggregated across every running daemon rather than + * extracted from one JSON message, so they are not part of a + * Primary[] table. They answer "was there a software change?". + */ + namespace Provenance { + inline constexpr const char* GITHASH_KEY = "GIT_HASH"; + inline constexpr const char* GITHASH_COMMENT = "git commit of running software"; + inline constexpr const char* BUILDTIME_KEY = "SW_BUILD"; + inline constexpr const char* BUILDTIME_COMMENT = "newest build of running software"; + } + } diff --git a/common/message_keys.h b/common/message_keys.h index 4fa79450..6219c368 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -54,6 +54,12 @@ namespace Key { inline const std::string SOURCE = "source"; inline const std::string PUBTIME = "pubtime"; + // Build provenance. Every publisher includes these so that a consumer can + // tell which software was running when a message was produced. + // + inline const std::string GITHASH = "githash"; ///< commit the publisher was built from + inline const std::string BUILDTIME = "buildtime"; ///< when the publisher was built, YYYY-MM-DDThh:mm:ss UTC + namespace Broadcast { inline const std::string SEVERITY = "severity"; inline const std::string MESSAGE = "message"; diff --git a/flexured/flexured.cpp b/flexured/flexured.cpp index 2344c8e1..88269996 100644 --- a/flexured/flexured.cpp +++ b/flexured/flexured.cpp @@ -93,7 +93,7 @@ int main(int argc, char **argv) { } logwrite(function, "world"); - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << flexured.config.n_entries << " lines read from " << flexured.config.filename; diff --git a/focusd/focusd.cpp b/focusd/focusd.cpp index 321aa45d..d69683d9 100644 --- a/focusd/focusd.cpp +++ b/focusd/focusd.cpp @@ -101,7 +101,7 @@ int main(int argc, char **argv) { focusd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << focusd.config.n_entries << " lines read from " << focusd.config.filename; diff --git a/powerd/powerd.cpp b/powerd/powerd.cpp index a4154096..23d7ae91 100644 --- a/powerd/powerd.cpp +++ b/powerd/powerd.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { powerd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << powerd.config.n_entries << " lines read from " << powerd.config.filename; diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 4b9269fc..55819243 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -109,7 +109,7 @@ int main(int argc, char **argv) { sequencerd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << sequencerd.config.n_entries << " lines read from " << sequencerd.config.filename; diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index cd507431..edc92a25 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -123,7 +123,7 @@ int main(int argc, char **argv) { slicecamd.exit_cleanly(); } - message.str(""); message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message.str(""); message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << slicecamd.config.n_entries << " lines read from " << slicecamd.config.filename; diff --git a/slitd/slitd.cpp b/slitd/slitd.cpp index 9b215537..2f9ea760 100644 --- a/slitd/slitd.cpp +++ b/slitd/slitd.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { slitd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << slitd.config.n_entries << " lines read from " << slitd.config.filename; diff --git a/tcsd/tcsd.cpp b/tcsd/tcsd.cpp index c21c20b5..0581c7bb 100644 --- a/tcsd/tcsd.cpp +++ b/tcsd/tcsd.cpp @@ -101,7 +101,7 @@ int main(int argc, char **argv) { tcsd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << tcsd.config.n_entries << " lines read from " << tcsd.config.filename; diff --git a/telemd/telemd.cpp b/telemd/telemd.cpp index e95638c4..7e245a83 100644 --- a/telemd/telemd.cpp +++ b/telemd/telemd.cpp @@ -124,7 +124,7 @@ int main(int argc, char **argv) { telemd.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << telemd.config.n_entries << " lines read from " << telemd.config.filename; diff --git a/thermald/thermald.cpp b/thermald/thermald.cpp index 719df6b4..ed971d2e 100644 --- a/thermald/thermald.cpp +++ b/thermald/thermald.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { thermald.exit_cleanly(); } - message << "this version built " << BUILD_DATE << " " << BUILD_TIME; + message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; logwrite(function, message.str()); message.str(""); message << thermald.config.n_entries << " lines read from " << thermald.config.filename; diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 00000000..48240ea0 --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1,2 @@ +# generated by CMake from version.h.in +version.h diff --git a/utils/build_date.h b/utils/build_date.h index dcdc6601..bb583078 100644 --- a/utils/build_date.h +++ b/utils/build_date.h @@ -4,5 +4,39 @@ * @author David Hale * */ +#pragma once + +#include +#include +#include + +#include "version.h" // generated by CMake, defines GIT_COMMIT_HASH + #define BUILD_DATE __DATE__ ///< preprocessor build date #define BUILD_TIME __TIME__ ///< preprocessor build time + + +/***** get_build_time *********************************************************/ +/** + * @brief returns the time that the running executable was built + * @details This uses the mtime of the running binary, which is when it was + * linked. That is more accurate than BUILD_DATE and BUILD_TIME, + * which are per-translation-unit and go stale whenever a header or + * a linked library changes without the main source being rebuilt. + * @return build time as YYYY-MM-DDThh:mm:ss UTC, empty if unavailable + * + */ +inline std::string get_build_time() { + struct stat st; + struct tm tmbuf; + char timestr[32]; + + if ( stat( "/proc/self/exe", &st ) != 0 ) return ""; + + if ( gmtime_r( &st.st_mtime, &tmbuf ) == nullptr ) return ""; + + if ( strftime( timestr, sizeof(timestr), "%Y-%m-%dT%H:%M:%S", &tmbuf ) == 0 ) return ""; + + return std::string( timestr ); +} +/***** get_build_time *********************************************************/ diff --git a/utils/version.h.in b/utils/version.h.in new file mode 100644 index 00000000..a3fad4fc --- /dev/null +++ b/utils/version.h.in @@ -0,0 +1,14 @@ +/** --------------------------------------------------------------------------- + * @file version.h.in + * @brief template for the generated version.h -- do not edit version.h + * @details CMake generates utils/version.h from this file at configure time. + * GIT_COMMIT_HASH identifies the commit this binary was built from + * and is identical for every module built from one configure, so it + * answers "what source is running" while the per-module build time + * (see build_date.h) answers "when was this module last built". + * @author David Hale + * + */ +#pragma once + +#define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@" ///< git commit hash, or "unknown" From 25e20e11a138689dcdba9c64f200a67a1c6d3322 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 30 Jul 2026 09:35:53 -0700 Subject: [PATCH 3/4] adds build provenance to daemon logs and FITS headers each daemon stamps its git hash and binary build time into every publish hash is marked -dirty when built from a modified tree --- CMakeLists.txt | 61 ++++++++++++--------------------------- acamd/acamd.cpp | 2 +- calibd/calibd.cpp | 2 +- camerad/astrocam.cpp | 2 +- camerad/camerad.cpp | 2 +- common/common.h | 5 ++-- flexured/flexured.cpp | 2 +- focusd/focusd.cpp | 2 +- powerd/powerd.cpp | 2 +- sequencerd/sequencerd.cpp | 2 +- slicecamd/slicecamd.cpp | 2 +- slitd/slitd.cpp | 2 +- tcsd/tcsd.cpp | 2 +- telemd/telemd.cpp | 2 +- thermald/thermald.cpp | 2 +- utils/CMakeLists.txt | 6 ++++ utils/build_date.h | 7 ++++- utils/gen_version.cmake | 41 ++++++++++++++++++++++++++ utils/provenance.cpp | 15 ++++++++++ utils/version.h.in | 2 +- 20 files changed, 103 insertions(+), 60 deletions(-) create mode 100644 utils/gen_version.cmake create mode 100644 utils/provenance.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d09ff7f..2a4a0a4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,52 +27,29 @@ find_package( Threads ) message( STATUS "CMAKE_BINARY_DIR" ${CMAKE_BINARY_DIR} ) -# Get the git commit hash identifying the source that this build came from. -# It is compiled into every daemon via the generated version.h below, so that -# each binary can report the commit it was built from. +# version.h carries the git provenance compiled into every daemon. It is +# generated into utils/ which is already on every include path. # -execute_process( COMMAND git rev-parse --short HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE GIT_RESULT - ERROR_QUIET ) - -# git failing here would silently leave the hash empty, -# so make the failure visible instead -# -if ( NOT GIT_RESULT EQUAL 0 OR GIT_COMMIT_HASH STREQUAL "" ) - set( GIT_COMMIT_HASH "unknown" ) - message( WARNING "unable to determine git commit hash: reporting GIT_HASH=unknown" ) -endif() - -message( STATUS "GIT_COMMIT_HASH " ${GIT_COMMIT_HASH} ) - -# Re-configure whenever the checked-out commit changes, so the hash cannot go -# stale. --git-path resolves correctly inside a linked worktree, where .git is -# a file rather than a directory. +set( GEN_VERSION_ARGS + -DSRC_DIR=${CMAKE_SOURCE_DIR} + -DIN_FILE=${PROJECT_BASE_DIR}/utils/version.h.in + -DTMP_FILE=${CMAKE_BINARY_DIR}/version.h.tmp + -DOUT_FILE=${PROJECT_BASE_DIR}/utils/version.h + -P ${PROJECT_BASE_DIR}/utils/gen_version.cmake ) + +# Generate it now so that version.h exists for the first build. # -execute_process( COMMAND git rev-parse --git-path HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_HEAD_FILE - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET ) - -if ( GIT_HEAD_FILE ) - set_property( DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GIT_HEAD_FILE} ) -endif() +execute_process( COMMAND ${CMAKE_COMMAND} ${GEN_VERSION_ARGS} ) -# Generate version.h into utils/ which is already on every daemon's include -# path. It is written through a temporary so that copy_if_different can leave -# the real header untouched when the hash has not changed, which keeps an -# unrelated re-configure from forcing a rebuild of everything. +# and again on every build. Editing a file moves neither HEAD nor any +# CMakeLists, so nothing would re-run configure and a configure-time check +# would report a stale clean/dirty state. The script leaves version.h alone +# when nothing changed, so this does not force a rebuild every time. # -CONFIGURE_FILE( ${PROJECT_BASE_DIR}/utils/version.h.in - ${CMAKE_BINARY_DIR}/version.h.tmp ) - -execute_process( COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${CMAKE_BINARY_DIR}/version.h.tmp - ${PROJECT_BASE_DIR}/utils/version.h ) +add_custom_target( update_version ALL + COMMAND ${CMAKE_COMMAND} ${GEN_VERSION_ARGS} + COMMENT "Checking git provenance" +) # Here is everything to build # diff --git a/acamd/acamd.cpp b/acamd/acamd.cpp index 57d9dfed..2148c8d5 100644 --- a/acamd/acamd.cpp +++ b/acamd/acamd.cpp @@ -151,7 +151,7 @@ int main(int argc, char **argv) { message.str(""); message << "[NOTICE] user id = " << uid << " group id = " << gid; logwrite( function, message.str() ); - message.str(""); message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message.str(""); message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << acamd.config.n_entries << " lines read from " << acamd.config.filename; diff --git a/calibd/calibd.cpp b/calibd/calibd.cpp index bd8f3037..c3baf110 100644 --- a/calibd/calibd.cpp +++ b/calibd/calibd.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { calibd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << calibd.config.n_entries << " lines read from " << calibd.config.filename; diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 7c2962dd..c1152318 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -150,7 +150,7 @@ namespace AstroCam { * */ void Interface::get_live_provenance( std::string &githash, std::string &buildtime ) { - githash = GIT_COMMIT_HASH; + githash = GIT_HASH_STR; buildtime = get_build_time(); std::unique_lock lock(live_telemetry_mtx); diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index 31b432b0..5009443c 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -158,7 +158,7 @@ int main(int argc, char **argv) { // log build date and hash // - message.str(""); message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message.str(""); message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite( function, message.str() ); message.str(""); message << server.config.n_entries << " lines read from " << server.config.filename; diff --git a/common/common.h b/common/common.h index 88e7ada5..97bc3869 100644 --- a/common/common.h +++ b/common/common.h @@ -25,8 +25,7 @@ #include "logentry.h" #include "network.h" #include "message_keys.h" -#include "build_date.h" -#include "version.h" // generated by CMake, defines GIT_COMMIT_HASH +#include "build_date.h" // declares GIT_HASH_STR and get_build_time() const long NOTHING = -1; const long NO_ERROR = 0; @@ -200,7 +199,7 @@ namespace Common { // static const std::string build_time = get_build_time(); json message = message_out; - message[Key::GITHASH] = GIT_COMMIT_HASH; + message[Key::GITHASH] = GIT_HASH_STR; message[Key::BUILDTIME] = build_time; std::lock_guard lock( _publish_mtx ); // serialize the non-thread-safe socket diff --git a/flexured/flexured.cpp b/flexured/flexured.cpp index 88269996..6d425f93 100644 --- a/flexured/flexured.cpp +++ b/flexured/flexured.cpp @@ -93,7 +93,7 @@ int main(int argc, char **argv) { } logwrite(function, "world"); - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << flexured.config.n_entries << " lines read from " << flexured.config.filename; diff --git a/focusd/focusd.cpp b/focusd/focusd.cpp index d69683d9..12859de2 100644 --- a/focusd/focusd.cpp +++ b/focusd/focusd.cpp @@ -101,7 +101,7 @@ int main(int argc, char **argv) { focusd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << focusd.config.n_entries << " lines read from " << focusd.config.filename; diff --git a/powerd/powerd.cpp b/powerd/powerd.cpp index 23d7ae91..5ffa58b3 100644 --- a/powerd/powerd.cpp +++ b/powerd/powerd.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { powerd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << powerd.config.n_entries << " lines read from " << powerd.config.filename; diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 55819243..a89a34d2 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -109,7 +109,7 @@ int main(int argc, char **argv) { sequencerd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << sequencerd.config.n_entries << " lines read from " << sequencerd.config.filename; diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index edc92a25..0fa66bd0 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -123,7 +123,7 @@ int main(int argc, char **argv) { slicecamd.exit_cleanly(); } - message.str(""); message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message.str(""); message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << slicecamd.config.n_entries << " lines read from " << slicecamd.config.filename; diff --git a/slitd/slitd.cpp b/slitd/slitd.cpp index 2f9ea760..1008c001 100644 --- a/slitd/slitd.cpp +++ b/slitd/slitd.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { slitd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << slitd.config.n_entries << " lines read from " << slitd.config.filename; diff --git a/tcsd/tcsd.cpp b/tcsd/tcsd.cpp index 0581c7bb..89371dfe 100644 --- a/tcsd/tcsd.cpp +++ b/tcsd/tcsd.cpp @@ -101,7 +101,7 @@ int main(int argc, char **argv) { tcsd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << tcsd.config.n_entries << " lines read from " << tcsd.config.filename; diff --git a/telemd/telemd.cpp b/telemd/telemd.cpp index 7e245a83..b0eec12c 100644 --- a/telemd/telemd.cpp +++ b/telemd/telemd.cpp @@ -124,7 +124,7 @@ int main(int argc, char **argv) { telemd.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << telemd.config.n_entries << " lines read from " << telemd.config.filename; diff --git a/thermald/thermald.cpp b/thermald/thermald.cpp index ed971d2e..47612893 100644 --- a/thermald/thermald.cpp +++ b/thermald/thermald.cpp @@ -100,7 +100,7 @@ int main(int argc, char **argv) { thermald.exit_cleanly(); } - message << "this version built " << get_build_time() << " from " << GIT_COMMIT_HASH; + message << "this version built " << get_build_time() << " from " << GIT_HASH_STR; logwrite(function, message.str()); message.str(""); message << thermald.config.n_entries << " lines read from " << thermald.config.filename; diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index d8992d58..7b5e52f7 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -14,10 +14,16 @@ add_definitions( -Wall -Wextra -Wconversion -Wshadow -ansi -Og -Wno-variadic-mac add_library(utilities STATIC ${PROJECT_UTILS_DIR}/utilities.cpp + ${PROJECT_UTILS_DIR}/provenance.cpp md5 ) target_link_libraries(utilities PRIVATE stdc++fs) +# provenance.cpp includes the generated version.h, so make sure it has been +# refreshed with the current git state before this library is compiled. +# +add_dependencies(utilities update_version) + add_library(logentry STATIC ${PROJECT_UTILS_DIR}/logentry.cpp ) diff --git a/utils/build_date.h b/utils/build_date.h index bb583078..cb2d1c8e 100644 --- a/utils/build_date.h +++ b/utils/build_date.h @@ -10,7 +10,12 @@ #include #include -#include "version.h" // generated by CMake, defines GIT_COMMIT_HASH +/// git commit this binary was built from, with a "-dirty" marker when the tree +/// carried uncommitted changes. Defined in provenance.cpp, which is the only +/// translation unit that includes the generated version.h, so that a change of +/// hash recompiles one file instead of everything including this header. +/// +extern const std::string GIT_HASH_STR; #define BUILD_DATE __DATE__ ///< preprocessor build date #define BUILD_TIME __TIME__ ///< preprocessor build time diff --git a/utils/gen_version.cmake b/utils/gen_version.cmake new file mode 100644 index 00000000..c2db767f --- /dev/null +++ b/utils/gen_version.cmake @@ -0,0 +1,41 @@ +# ---------------------------------------------------------------------------- +# @file utils/gen_version.cmake +# @brief regenerates version.h with the current git provenance +# @details Run at configure time and again on every build, so that the hash +# and the "-dirty" marker describe the tree as it was when the build +# ran. Editing a file does not move HEAD, so a configure-time-only +# check would report a stale clean/dirty state. +# +# Expects SRC_DIR, IN_FILE, TMP_FILE and OUT_FILE to be passed in +# with -D. Writes through TMP_FILE so that copy_if_different can +# leave OUT_FILE alone when nothing changed, avoiding a rebuild. +# ---------------------------------------------------------------------------- + +execute_process( COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${SRC_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE GIT_RESULT + ERROR_QUIET ) + +if ( NOT GIT_RESULT EQUAL 0 OR GIT_COMMIT_HASH STREQUAL "" ) + set( GIT_COMMIT_HASH "unknown" ) +else() + + # A hash alone would name a commit that does not describe what was built, + # so mark it when the tree carries uncommitted changes. This does not say + # what changed, only that the commit cannot reproduce this binary. + # + execute_process( COMMAND git diff --quiet HEAD + WORKING_DIRECTORY ${SRC_DIR} + RESULT_VARIABLE GIT_TREE_DIRTY ) + + if ( NOT GIT_TREE_DIRTY EQUAL 0 ) + set( GIT_COMMIT_HASH "${GIT_COMMIT_HASH}-dirty" ) + endif() + +endif() + +CONFIGURE_FILE( ${IN_FILE} ${TMP_FILE} ) + +execute_process( COMMAND ${CMAKE_COMMAND} -E copy_if_different ${TMP_FILE} ${OUT_FILE} ) diff --git a/utils/provenance.cpp b/utils/provenance.cpp new file mode 100644 index 00000000..7c3e1bcd --- /dev/null +++ b/utils/provenance.cpp @@ -0,0 +1,15 @@ +/** --------------------------------------------------------------------------- + * @file provenance.cpp + * @brief defines the build provenance of this binary + * @details This is deliberately the only translation unit that includes the + * generated version.h. version.h is regenerated on every build so + * that the "-dirty" marker reflects the tree at the moment of the + * build, and confining it here means a change recompiles only this + * file rather than everything that includes build_date.h. + * @author David Hale + * + */ +#include "build_date.h" +#include "version.h" + +const std::string GIT_HASH_STR = GIT_COMMIT_HASH; diff --git a/utils/version.h.in b/utils/version.h.in index a3fad4fc..47969e5c 100644 --- a/utils/version.h.in +++ b/utils/version.h.in @@ -11,4 +11,4 @@ */ #pragma once -#define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@" ///< git commit hash, or "unknown" +#define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@" ///< hash, hash-dirty, or "unknown" From 42d3b9b84de633fcd933baa5dd63c6a3c4b8a7c9 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 30 Jul 2026 09:55:22 -0700 Subject: [PATCH 4/4] ignores submodule working-tree state in the dirty DSP carries permanent untracked content, which made every build report -dirty regardless of the actual source state. --- utils/gen_version.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/gen_version.cmake b/utils/gen_version.cmake index c2db767f..8c253bd9 100644 --- a/utils/gen_version.cmake +++ b/utils/gen_version.cmake @@ -26,7 +26,9 @@ else() # so mark it when the tree carries uncommitted changes. This does not say # what changed, only that the commit cannot reproduce this binary. # - execute_process( COMMAND git diff --quiet HEAD + # Only tracked sources decide this. + # + execute_process( COMMAND git diff --quiet --ignore-submodules=dirty HEAD WORKING_DIRECTORY ${SRC_DIR} RESULT_VARIABLE GIT_TREE_DIRTY )