Class: Ocran::Runner

Inherits:
Object
  • Object
show all
Includes:
CommandOutput
Defined in:
lib/ocran/runner.rb

Instance Method Summary collapse

Methods included from CommandOutput

#error, #say, #verbose, #warning

Constructor Details

#initializeRunner

Returns a new instance of Runner.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/ocran/runner.rb', line 14

def initialize
  load File.expand_path("runtime_environment.rb", __dir__)
  @pre_env = RuntimeEnvironment.save

  load File.expand_path("option.rb", __dir__)
  @option = Option.new.tap do |opt|
    opt.parse(ARGV)
  rescue RuntimeError => e
    # Capture RuntimeError during parsing and display an appropriate
    # error message to the user. This error usually occurs from invalid
    # option arguments.
    fatal_error e.message
  else
    # Update ARGV with the parsed command line arguments to pass to
    # the user's script. This ensures the script executes based on
    # the user-specified arguments.
    ARGV.replace(opt.argv)
  end

  Ocran.option = @option

  @ignore_modules = ObjectSpace.each_object(Module).to_a
end

Instance Method Details

#active_bundler_gemfileObject

The Gemfile this process was started against, if any.



114
115
116
117
118
119
120
121
122
123
# File 'lib/ocran/runner.rb', line 114

def active_bundler_gemfile
  return ENV["BUNDLE_GEMFILE"] unless ENV["BUNDLE_GEMFILE"].to_s.empty?
  return nil unless defined?(Bundler) && Bundler.respond_to?(:default_gemfile)

  Bundler.default_gemfile.to_s
rescue StandardError
  # Bundler raises when there is no Gemfile anywhere above the working
  # directory, which just means there is no ambient bundle to displace.
  nil
end

#apply_gemfile_to_bundler_envObject

--gemfile names the Gemfile the application runs under, so it has to govern the dependency run as well, not just the later Gemfile scan. The script is loaded in this very process, and a BUNDLE_GEMFILE inherited from the environment would otherwise win: invoke OCRAN from inside another project's bundle exec and the script's require "bundler/setup" sets up that project's bundle instead of the application's. Gems only the application's Gemfile provides are then missing, and local development gems declared with path: or gemspec always are, since they exist nowhere else. The dependency run dies with a LoadError before anything can be packed (github issue #34).



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ocran/runner.rb', line 66

def apply_gemfile_to_bundler_env
  gemfile = @option.gemfile.to_s
  active = active_bundler_gemfile

  ENV["BUNDLE_GEMFILE"] = gemfile
  return if active.nil? || same_file?(active, gemfile)

  # Past this point the ambient bundle is a different one, so anything
  # else describing it has to go: BUNDLE_LOCKFILE names the lockfile of
  # the bundle being displaced, and Bundler would sooner believe it than
  # the Gemfile we just pointed it at.
  ENV.delete("BUNDLE_LOCKFILE")
  return unless defined?(Bundler) && Bundler.respond_to?(:reset!)

  # `bundle exec` puts -rbundler/setup in RUBYOPT as well, so the wrong
  # bundle may already be set up by the time this runs. The variable
  # alone is then too late: bundler/setup sits in $LOADED_FEATURES, and
  # the script's own `require "bundler/setup"` would be a no-op. Drop
  # Bundler's memoized state and let it be set up afresh, against the
  # Gemfile we were given.
  verbose "Rebinding Bundler from #{active} to #{gemfile}"
  Bundler.reset!
  $LOADED_FEATURES.delete_if { |feature| feature.match?(RuntimeEnvironment::BUNDLER_SETUP_FEATURE) }
end

#attempt_load_autoload(ignore_modules = []) ⇒ Object

Force loading autoloaded constants. Searches through all modules (and hence classes), and checks their constants for autoloaded ones, then attempts to load them.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/ocran/runner.rb', line 132

def attempt_load_autoload(ignore_modules = [])
  checked_modules = ignore_modules.inject({}) { |h, mod| h[mod] = true; h }
  while ObjectSpace.each_object(Module).count { |mod|
    next if checked_modules.include?(mod)
    mod.constants.each do |const|
      next unless mod.autoload?(const)
      say "Attempting to trigger autoload of #{mod}::#{const}"
      begin
        mod.const_get(const)
      rescue ScriptError, StandardError => e
        # Some autoload constants may throw exceptions beyond the expected
        # errors. This includes issues dependent on the system or execution
        # environment, so it is preferable to ignore exceptions other than
        # critical errors.
        warning "#{mod}::#{const} loading failed: #{e.message}"
      end
    end
    checked_modules[mod] = true
  }.nonzero?
    # Loops until all constants have been checked.
  end
end

#buildObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ocran/runner.rb', line 155

def build
  # If the script was run and autoload is enabled, attempt to autoload libraries.
  if @option.force_autoload?
    attempt_load_autoload(@ignore_modules)
  end

  @post_env = RuntimeEnvironment.save
  # NOTE: From this point, $LOADED_FEATURES has been captured, so it is now
  # safe to call require_relative.

  ENV.replace(@pre_env.env)

  # It might be useful to reset the current directory to the point where the
  # command was launched, especially when implementing the builder object.
  Dir.chdir(@pre_env.pwd)

  require_relative "direction"
  direction = Direction.new(@post_env, @pre_env, @option)

  if @option.use_inno_setup?
    # Native on Windows; on POSIX allow it when an ISCC command is
    # available (e.g. via Wine wrapper scripts or in tests).
    if Gem.win_platform? || system("command -v ISCC > /dev/null 2>&1")
      direction.build_inno_setup_installer
    else
      raise "Inno Setup is only supported on Windows (no ISCC command found in PATH)"
    end
  elsif @option.macosx_bundle
    direction.build_macosx_bundle(@option.macosx_bundle)
  elsif @option.output_dir
    direction.build_output_dir(@option.output_dir)
  elsif @option.output_zip
    direction.build_zip(@option.output_zip)
  elsif @option.cosmo_zip?
    direction.build_cosmo_zip_exe
  else
    direction.build_stab_exe
  end
rescue RuntimeError => e
  fatal_error e.message
end

#fatal_error(statement) ⇒ Object



9
10
11
12
# File 'lib/ocran/runner.rb', line 9

def fatal_error(statement)
  error statement
  exit false
end

#runObject



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/ocran/runner.rb', line 38

def run
  at_exit do
    if $!.nil? or $!.kind_of?(SystemExit)
      build
      exit
    end
  end

  exit unless @option.run_script?
  if @option.gemfile
    apply_gemfile_to_bundler_env
  else
    warn_about_foreign_bundle
  end
  say "Loading script to check dependencies"
  $PROGRAM_NAME = @option.script.to_s
end

#same_file?(a, b) ⇒ Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/ocran/runner.rb', line 125

def same_file?(a, b)
  File.identical?(a, b) || File.expand_path(a) == File.expand_path(b)
end

#warn_about_foreign_bundleObject

Without --gemfile the dependency run inherits whatever bundle the environment carries, and when that is some other project's bundle the script's own gems are simply absent. The failure that follows is a LoadError from deep inside the script, which says nothing about the bundle being the wrong one, so say it here while the reason is still visible.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ocran/runner.rb', line 97

def warn_about_foreign_bundle
  # Only when a bundle really is set up. BUNDLE_GEMFILE alone is
  # inherited by any child process and governs nothing on its own.
  return unless @pre_env.bundler_setup_loaded?

  active = active_bundler_gemfile
  return if active.nil? || active.empty?

  app_gemfile = @option.application_gemfile
  return if app_gemfile && same_file?(active, app_gemfile)

  warning "Running under Bundler with #{active}, which is not the Gemfile " \
          "#{@option.script} runs under" +
          (app_gemfile ? "; pass --gemfile #{app_gemfile} to package against that bundle" : "")
end