diff --git a/configure.ac b/configure.ac index 3ec2098d..b6268582 100644 --- a/configure.ac +++ b/configure.ac @@ -27,6 +27,17 @@ if test -z "$RUBY"; then AC_CHECK_PROG(RUBY,[ruby],[ruby],[no]) test "$RUBY" == "no" && AC_MSG_ERROR([Required program 'ruby' not found.]) fi +# iprof needs Ruby >= 2.7. It's a runtime dependency, but we assume people use +# the same ruby version at compile and runtime. Will also be used to potentially +# update other parts of the project. +RUBY_MIN_VERSION=2.7.0 +AC_SUBST([RUBY_MIN_VERSION]) +AC_MSG_CHECKING([for ruby >= $RUBY_MIN_VERSION]) +RUBY_VERSION_FOUND=`$RUBY -e 'print RUBY_VERSION'` +AX_COMPARE_VERSION([$RUBY_VERSION_FOUND], [ge], [$RUBY_MIN_VERSION], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([ruby version $RUBY_VERSION_FOUND is too old, need >= $RUBY_MIN_VERSION])]) if test -z "$ERB"; then AC_CHECK_PROG(ERB,[erb],[erb],[no]) test "$ERB" == "no" && AC_MSG_ERROR([Required program 'erb' not found.]) @@ -155,6 +166,7 @@ AC_CONFIG_FILES([backends/mpi/tracer_mpi.sh], [chmod +x backends/mpi/tracer_mpi. AC_CONFIG_FILES([backends/itt/tracer_itt.sh], [chmod +x backends/itt/tracer_itt.sh]) AC_CONFIG_FILES([utils/babeltrace_thapi], [chmod +x utils/babeltrace_thapi]) AC_CONFIG_FILES([xprof/xprof.rb], [chmod +x xprof/xprof.rb]) +AC_CONFIG_FILES([xprof/iprof:xprof/xprof_wrapper.rb.in], [chmod +x xprof/iprof]) AC_CONFIG_FILES([thapi.pc]) AC_OUTPUT diff --git a/integration_tests/old_ruby_frontend.bats b/integration_tests/old_ruby_frontend.bats new file mode 100644 index 00000000..39f0f23d --- /dev/null +++ b/integration_tests/old_ruby_frontend.bats @@ -0,0 +1,34 @@ +bats_require_minimum_version 1.5.0 + +# The `iprof` launcher must stay parseable by an old Ruby (the system default on +# some machines) so that, on a too-old interpreter, it prints a friendly version +# message instead of a raw SyntaxError. All modern-syntax logic lives in the +# required `xprof.rb`, which is only parsed after the version gate passes. + +setup() { + # ruby CLI need a path + iprof_path=$(command -v iprof) + + # Single source of truth: read the minimal ruby version baked into iprof itself + # (configure.ac may not be present, e.g. CI only ships the install tree). The + # value keeps its quotes, so it drops straight into the ruby below. + min_version=$(awk '/^THAPI_RUBY_MINIMAL_VERSION/ {print $3}' "${iprof_path}") + + # The ruby to test against. Defaults to `ruby`; point THAPI_OLD_RUBY at an old + # interpreter to exercise this on a machine whose default ruby is recent. + old_ruby="${THAPI_OLD_RUBY:-ruby}" + if "${old_ruby}" -e "exit(Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(${min_version}))"; then + skip "${old_ruby} is >= ${min_version}; set THAPI_OLD_RUBY to an older ruby to run this test" + fi +} + +@test "frontend_parses_under_old_ruby" { + run -0 "${old_ruby}" -c "${iprof_path}" +} + +@test "frontend_prints_friendly_message_under_old_ruby" { + run ! "${old_ruby}" "${iprof_path}" --help + [[ "${output}" == *"too old"* ]] + [[ "${output}" != *"SyntaxError"* ]] + [[ "${output}" != *"unexpected"* ]] +} diff --git a/xprof/Makefile.am b/xprof/Makefile.am index affe3316..9777678b 100644 --- a/xprof/Makefile.am +++ b/xprof/Makefile.am @@ -13,6 +13,13 @@ bin_SCRIPTS = \ iprof \ sync_daemon_fs +# `iprof` is the old-Ruby-safe wrapper, generated from xprof_wrapper.rb.in by +# configure (see configure.ac). It requires xprof.rb (the implementation) from +# DATADIR at runtime, the same place as optparse_thapi.rb and version (see +# utils/Makefile.am). +data_DATA = \ + xprof.rb + if FOUND_MPI bin_SCRIPTS += sync_daemon_mpi endif @@ -20,9 +27,6 @@ endif sync_daemon_mpi: $(srcdir)/sync_daemon_mpi.cpp $(MPICXX) $(CXXFLAGS) $(AM_CXXFLAGS) -I$(top_srcdir)/utils/include $< -o $@ -iprof: xprof.rb - cp xprof.rb $@ - xprof_utils.hpp: $(top_srcdir)/utils/xprof_utils.hpp cp $< $@ @@ -227,7 +231,6 @@ EXTRA_DIST = \ sync_daemon_fs CLEANFILES = \ - iprof \ xprof_utils.hpp \ perfetto_pruned.pb.h \ perfetto_pruned.pb.cc \ diff --git a/xprof/sync_daemon_fs b/xprof/sync_daemon_fs index 1166dbfd..d188143f 100755 --- a/xprof/sync_daemon_fs +++ b/xprof/sync_daemon_fs @@ -1,8 +1,9 @@ #!/usr/bin/env ruby -# Cannot use require_relative as iprof is not a rb file -# Load MPITopo and related helpers -load(File.join(__dir__, 'iprof')) +# Load MPITopo and related helpers from the implementation (installed in +# ../share), not the iprof wrapper, which would set $thapi_launch and run the +# whole program instead of just defining helpers. +require_relative File.join('..', 'share', 'xprof') require 'open3' require 'fileutils' @@ -66,12 +67,11 @@ until msg == SyncDaemon::MSG_FINISH raise 'sync_daemon_fs: parent closed socket without sending FINISH' if msg.empty? case msg - when SyncDaemon::MSG_INIT then nil + when SyncDaemon::MSG_INIT, SyncDaemon::MSG_FINISH then nil when SyncDaemon::MSG_LOCAL_BARRIER local_barrier(local_barrier_count.to_s) local_barrier_count += 1 when SyncDaemon::MSG_GLOBAL_BARRIER then global_barrier(global_handle) - when SyncDaemon::MSG_FINISH then nil else warn("sync_daemon_fs: unknown message '#{msg}'") exit(1) diff --git a/xprof/xprof.rb.in b/xprof/xprof.rb.in index 5e0012d9..24907c63 100755 --- a/xprof/xprof.rb.in +++ b/xprof/xprof.rb.in @@ -1,11 +1,6 @@ #!/usr/bin/env ruby -# 2.7 for Lazy in Enumerable. 2.7 was released 25 Dec 2019 -THAPI_RUBY_MINIMAL_VERSION = '2.7.0' -if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(THAPI_RUBY_MINIMAL_VERSION) - warn("Your ruby version #{RUBY_VERSION} is too old. #{THAPI_RUBY_MINIMAL_VERSION} or newer required") - exit(1) -end +# This file doesn't do any ruby version check. All the check are done in `iprof`. # We Cannot use "@ .. @" for libdir, bindir, and datarootdir # as they will appear as bash "${exec_prefix}/lib" @@ -1007,8 +1002,9 @@ def last_trace_saved Dir[File.join(thapi_trace_home, 'thapi*')].max_by { |f| File.mtime(f) } end -# Avoid load problem -if __FILE__ == $PROGRAM_NAME +# Run when launched through the `iprof` wrapper (which sets $thapi_launch before +# requiring this file) or when executed directly. +if $thapi_launch || __FILE__ == $PROGRAM_NAME parser = OptionParserWithDefaultAndValidation.new parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] [--] [command]" diff --git a/xprof/xprof_wrapper.rb.in b/xprof/xprof_wrapper.rb.in new file mode 100755 index 00000000..71f52a76 --- /dev/null +++ b/xprof/xprof_wrapper.rb.in @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +# User-facing `iprof` launcher. It MUST stay parseable by old Ruby: Ruby parses +# the whole file before running any of it, so modern syntax here (endless ranges, +# pattern matching, ...) would raise a raw SyntaxError before the version check +# below could run. All real logic lives in xprof.rb, required only after the check +# passes, so users on an old Ruby get the friendly message below instead. + +# Named constant (not inlined) so bats can grep the minimal version out of iprof. +THAPI_RUBY_MINIMAL_VERSION = '@RUBY_MIN_VERSION@' +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(THAPI_RUBY_MINIMAL_VERSION) + warn("Your ruby version #{RUBY_VERSION} is too old. #{THAPI_RUBY_MINIMAL_VERSION} or newer required") + exit(1) +end + +$LOAD_PATH.unshift(File.join('@prefix@', 'share')) +$thapi_launch = true +require 'xprof'