Module: McptaskRailsRunner::Binary

Defined in:
lib/mcptask_rails_runner/binary.rb

Overview

Locates, installs and executes the bundled Go binary.

Constant Summary collapse

GEM_ROOT =
File.expand_path("../..", __dir__)
SUPPORTED_PLATFORMS =
%w[
  arm64-darwin
  x86_64-darwin
  aarch64-linux
  x86_64-linux
  x64-mingw-ucrt
].freeze
INSTALL_DIR =

Where the binary is installed to live, outside every gem directory.

The scheduled job runs this path, and it has to be one that survives a bundle update: a job pointing into gems/mcptask-rails-runner--/libexec dies the moment the gem is bumped, at 08:00, in a log nobody reads. Under the runner's own ~/.mcptask rather than /usr/local/bin, because bundle install has no sudo and a gem cannot write there. The Go side knows the same path — see InstalledBinary in internal/install/schedule.go.

File.join(Dir.home, ".mcptask", "bin")
VERSION_LINE =

The line mcptask_runner version prints the version on.

/Version:\s*(\S+)/

Class Method Summary collapse

Class Method Details

.bundled_path(gem_root: GEM_ROOT) ⇒ Object

The copy that ships inside this gem, which is the one the Gemfile pins.

Raises:



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/mcptask_rails_runner/binary.rb', line 49

def bundled_path(gem_root: GEM_ROOT)
  exe = File.join(gem_root, "libexec", executable_name)
  return exe if File.file?(exe)

  raise Error, <<~MSG
    mcptask_runner binary not found at #{exe}.

    The platform gem for #{Gem::Platform.local} was probably not installed.
    Supported platforms: #{SUPPORTED_PLATFORMS.join(', ')}.

    On a supported platform, re-run `bundle install` (and make sure the
    platform is in Gemfile.lock: `bundle lock --add-platform #{Gem::Platform.local}`).
    On an unsupported platform, install the binary from
    https://github.com/jchsoft/mcptask-releases and point the
    MCPTASK_RUNNER_BIN environment variable at it.
  MSG
end

.copy(source, target) ⇒ Object

Written beside the target and renamed into place: a half-copied binary over a working one is a scheduled job that fails on a corrupt file, and the rename also sidesteps the ETXTBSY a running copy would give.



146
147
148
149
150
151
152
# File 'lib/mcptask_rails_runner/binary.rb', line 146

def copy(source, target)
  FileUtils.mkdir_p(File.dirname(target))
  staged = "#{target}.new"
  FileUtils.cp(source, staged)
  FileUtils.chmod(0o755, staged)
  File.rename(staged, target)
end

.exec!(*args) ⇒ Object

Replaces the current process, so the binary's exit code and signal handling reach the caller (rake) unchanged.



114
115
116
# File 'lib/mcptask_rails_runner/binary.rb', line 114

def exec!(*args)
  Kernel.exec(path, *args)
end

.exec_installed!(*args) ⇒ Object

Installs the binary outside the gem first, then runs that copy.

Used by install and update, the two tasks that generate the scheduled job: the job runs whichever binary is executing them, so it has to be the copy at the version-free path rather than the one inside this gem.



123
124
125
# File 'lib/mcptask_rails_runner/binary.rb', line 123

def exec_installed!(*args)
  Kernel.exec(install!, *args)
end

.executable_nameObject



34
35
36
# File 'lib/mcptask_rails_runner/binary.rb', line 34

def executable_name
  Gem.win_platform? ? "mcptask_runner.exe" : "mcptask_runner"
end

.install!(install_dir: INSTALL_DIR, gem_root: GEM_ROOT, out: $stdout) ⇒ Object

Copies the bundled binary out of the gem, and returns what to run.

The gem bootstraps the binary; it does not host it. Everything the scheduled job touches has to outlive bundle update, and a gem directory does not — so install and update put a copy where nothing but they will ever move it. A copy rather than a symlink: a link into the gem's libexec dangles on the same bundle update, which is the identical 08:00 failure moved one step along, and Windows wants a privilege for symlinks besides.

Teaching the launcher to run bundle exec rake instead would fix the path and put bundler and Gemfile.lock back into the 08:00 path — the layer the Go port set out to remove.

Newest wins, and says so. A host with several projects has several gem versions and one installed binary, so a project pinning an older gem must not quietly drag the machine's runner backwards.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/mcptask_rails_runner/binary.rb', line 88

def install!(install_dir: INSTALL_DIR, gem_root: GEM_ROOT, out: $stdout)
  override = ENV["MCPTASK_RUNNER_BIN"]
  unless override.nil? || override.empty?
    out.puts "[mcptask_runner] MCPTASK_RUNNER_BIN is set — running #{override} " \
             "and leaving #{installed_path(install_dir: install_dir)} alone."
    return override
  end

  source = bundled_path(gem_root: gem_root)
  target = installed_path(install_dir: install_dir)

  existing = installed_version(target)
  if existing && existing > Gem::Version.new(VERSION)
    out.puts "[mcptask_runner] #{target} already holds #{existing}, which is newer than this " \
             "gem's #{VERSION} — keeping it. Delete that file to install this gem's binary instead."
    return target
  end

  copy(source, target)
  out.puts "[mcptask_runner] installed mcptask_runner #{VERSION} into #{target}" \
           "#{existing ? " (replacing #{existing})" : ''}."
  target
end

.installed_path(install_dir: INSTALL_DIR) ⇒ Object

The version-free path the installed copy lives at.



68
69
70
# File 'lib/mcptask_rails_runner/binary.rb', line 68

def installed_path(install_dir: INSTALL_DIR)
  File.join(install_dir, executable_name)
end

.installed_version(target) ⇒ Object

What the installed copy reports about itself, or nil when there is none — or when it cannot be asked, which a stray file at that path cannot be.

Asked of the binary rather than recorded in a marker file beside it: the binary is what actually runs at 08:00, and a marker can only be right about installs that went through here.



133
134
135
136
137
138
139
140
141
# File 'lib/mcptask_rails_runner/binary.rb', line 133

def installed_version(target)
  return nil unless File.file?(target)

  banner = IO.popen([target, "version"], err: File::NULL, &:read)
  raw = banner.to_s[VERSION_LINE, 1]
  raw && Gem::Version.new(raw)
rescue StandardError
  nil
end

.path(gem_root: GEM_ROOT) ⇒ Object

Absolute path to the binary this gem runs. Never resolved through PATH: the binary shares the mcptask_runner name with the legacy gem's rake namespace, and a PATH lookup could pick up a stale or wrong install.



41
42
43
44
45
46
# File 'lib/mcptask_rails_runner/binary.rb', line 41

def path(gem_root: GEM_ROOT)
  override = ENV["MCPTASK_RUNNER_BIN"]
  return override unless override.nil? || override.empty?

  bundled_path(gem_root: gem_root)
end