Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
test/tmp/
.direnv/
pkg/

# Offline dependency sources
.codex/deps_src/
.codex/internet_resources/
9 changes: 9 additions & 0 deletions MAINTANERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ Two Ruby gems are published from this repository:
* **codetracer_pure_ruby_recorder** – a pure Ruby fallback without the
native extension.

A helper script is available to build and push all gems in one go:

```bash
ruby scripts/publish_gems.rb
```

### Native extension gem

1. Install the development dependencies:
Expand Down Expand Up @@ -89,3 +95,6 @@ gem push codetracer_pure_ruby_recorder-<version>.gem

Ensure the version matches the native extension gem so that both
packages can be used interchangeably.

All the above steps are automated by `scripts/publish_gems.rb` which
builds and publishes the pure Ruby gem and all native variants.
4 changes: 4 additions & 0 deletions bin/codetracer-pure-ruby-recorder
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require 'rbconfig'
script = File.expand_path('../src/trace.rb', __dir__)
exec RbConfig.ruby, script, *ARGV
16 changes: 16 additions & 0 deletions codetracer_pure_ruby_recorder.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Gem::Specification.new do |spec|
spec.name = 'codetracer_pure_ruby_recorder'
spec.version = '0.1.0'
spec.authors = ['Metacraft Labs']
spec.email = ['[email protected]']

spec.summary = 'CodeTracer Ruby recorder implemented purely in Ruby'
spec.description = 'Ruby tracer that records execution steps using only Ruby code.'
spec.license = 'MIT'
spec.homepage = 'https://github.com/metacraft-labs/codetracer-ruby-recorder'

spec.files = Dir['src/**/*', 'bin/*', 'README.md', 'LICENSE']
spec.require_paths = ['src']
spec.bindir = 'bin'
spec.executables = ['codetracer-pure-ruby-recorder']
end
34 changes: 34 additions & 0 deletions scripts/publish_gems.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'fileutils'

TARGETS = [
'x86_64-unknown-linux-gnu',
'aarch64-unknown-linux-gnu',
'x86_64-apple-darwin',
'aarch64-apple-darwin',
'x86_64-pc-windows-msvc'
].freeze


def run(cmd, env = {})
command = env.map { |k, v| "#{k}=#{v}" }.join(' ')
command = [command, cmd].reject(&:empty?).join(' ')
puts "$ #{command}"
system(env, *cmd.split(' ')) || abort("Command failed: #{command}")
end

# Build and publish native extension gems
TARGETS.each do |target|
run('rake cross_native_gem', 'RB_SYS_CARGO_TARGET' => target)
gem_file = Dir['pkg/codetracer-ruby-recorder-*.gem'].max_by { |f| File.mtime(f) }
run("gem push #{gem_file}")
FileUtils.rm_f(gem_file)
end

# Build and publish pure Ruby gem
run('gem build codetracer_pure_ruby_recorder.gemspec')
pure_gem = Dir['codetracer_pure_ruby_recorder-*.gem'].max_by { |f| File.mtime(f) }
run("gem push #{pure_gem}")
FileUtils.rm_f(pure_gem)
116 changes: 55 additions & 61 deletions src/trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
require 'json'
require_relative 'recorder'

if ARGV[0].nil?
$stderr.puts("ruby trace.rb <program> [<args>]")
exit(1)
end

program = ARGV[0]

# Warning:
# probably related to our development env:
# if we hit an `incompatible library version` error, like
Expand Down Expand Up @@ -82,6 +75,7 @@ def initialize(record)
@trace_stopped = false
@record = record
@ignore_list = []
setup_tracepoints
end

def stop_tracing
Expand All @@ -97,6 +91,32 @@ def ignore(path)
@ignore_list << path
end

def setup_tracepoints
@calls_tracepoint = TracePoint.new(:call) do |tp|
deactivate
record_call(tp)
activate
end

@return_tracepoint = TracePoint.new(:return) do |tp|
deactivate
record_return(tp)
activate
end

@line_tracepoint = TracePoint.new(:line) do |tp|
deactivate
record_step(tp)
activate
end

@raise_tracepoint = TracePoint.new(:raise) do |tp|
deactivate
record_exception(tp)
activate
end
end

def prepare_args(tp)
args_after_self = tp.parameters.map do |(kind, name)|
value = if tp.binding.nil? || name.nil?
Expand Down Expand Up @@ -229,64 +249,38 @@ def load_variables(binding)

$tracer = Tracer.new($codetracer_record)

# also possible :c_call, :b_call: for now record ruby calls (:call)
# c_call: c lang
# b_call: block entry
# https://rubyapi.org/3.4/o/tracepoint
$tracer.calls_tracepoint = TracePoint.new(:call) do |tp|
$tracer.deactivate
$tracer.record_call(tp)
$tracer.activate
end
if __FILE__ == $PROGRAM_NAME
if ARGV[0].nil?
$stderr.puts('ruby trace.rb <program> [<args>]')
exit(1)
end

$tracer.return_tracepoint = TracePoint.new(:return) do |tp|
$tracer.deactivate
$tracer.record_return(tp)
$tracer.activate
end
program = ARGV[0]

$tracer.line_tracepoint = TracePoint.new(:line) do |tp|
$tracer.deactivate
$tracer.record_step(tp)
$tracer.activate
end
$tracer.record.register_call('', 1, '<top-level>', [])
$tracer.ignore('lib/ruby')
$tracer.ignore('trace.rb')
$tracer.ignore('recorder.rb')
$tracer.ignore('<internal:')
$tracer.ignore('gems/')

$tracer.raise_tracepoint = TracePoint.new(:raise) do |tp|
$tracer.deactivate
$tracer.record_exception(tp)
trace_args = ARGV
ARGV = ARGV[1..-1]
$tracer.activate
end
begin
Kernel.load(program)
rescue Exception => e
old_puts ''
old_puts '==== trace.rb error while tracing program ==='
old_puts 'ERROR'
old_puts e
old_puts e.backtrace
old_puts '====================='
old_puts ''
end
ARGV = trace_args

$tracer.record.register_call("", 1, "<top-level>", [])
$tracer.ignore('lib/ruby')
$tracer.ignore('trace.rb')
$tracer.ignore('recorder.rb')
$tracer.ignore('<internal:')
$tracer.ignore('gems/')

$tracer.stop_tracing

trace_args = ARGV
ARGV = ARGV[1..-1]
$tracer.activate
begin
Kernel.load(program)
rescue Exception => e
# important: rescue Exception,
# not just rescue as we originally did
# because a simple `rescue` doesn't catch some errors
# like SystemExit and others
# (when we call `exit` in the trace program and others)
# https://stackoverflow.com/questions/5118745/is-systemexit-a-special-kind-of-exception
old_puts ""
old_puts "==== trace.rb error while tracing program ==="
old_puts "ERROR"
old_puts e
old_puts e.backtrace
old_puts "====================="
old_puts ""
$tracer.record.serialize(program)
end
ARGV = trace_args

$tracer.stop_tracing

$tracer.record.serialize(program)