Class: Fastlane::FastFile

Inherits:
Object
  • Object
show all
Defined in:
fastlane/lib/fastlane/fast_file.rb

Constant Summary collapse

SharedValues =
Fastlane::Actions::SharedValues

Instance Attribute Summary collapse

DSL collapse

Other things collapse

Versioning helpers collapse

Overwriting Ruby methods collapse

Instance Method Summary collapse

Constructor Details

#initialize(path = nil) ⇒ Object

Returns The runner which can be executed to trigger the given actions.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'fastlane/lib/fastlane/fast_file.rb', line 15

def initialize(path = nil)
  return unless (path || '').length > 0
  UI.user_error!("Could not find Fastfile at path '#{path}'") unless File.exist?(path)
  @path = File.expand_path(path)
  content = File.read(path, encoding: "utf-8")

  # From https://github.com/orta/danger/blob/master/lib/danger/Dangerfile.rb
  if content.tr!('“”‘’‛', %(""'''))
    UI.error("Your #{File.basename(path)} has had smart quotes sanitised. " \
            'To avoid issues in the future, you should not use ' \
            'TextEdit for editing it. If you are not using TextEdit, ' \
            'you should turn off smart quotes in your editor of choice.')
  end

  content.scan(/^\s*require ["'](.*?)["']/).each do |current|
    gem_name = current.last
    next if gem_name.include?(".") # these are local gems

    begin
      require(gem_name)
    rescue LoadError
      UI.important("You have required a gem, if this is a third party gem, please use `fastlane_require '#{gem_name}'` to ensure the gem is installed locally.")
    end
  end

  parse(content, @path)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_sym, *arguments, &_block) ⇒ Object

Is used to look if the method is implemented as an action



158
159
160
# File 'fastlane/lib/fastlane/fast_file.rb', line 158

def method_missing(method_sym, *arguments, &_block)
  self.runner.trigger_action_by_name(method_sym, nil, false, *arguments)
end

Instance Attribute Details

#current_platformObject

the platform in which we’re currently in when parsing the Fastfile This is used to identify the platform in which the lane is in



10
11
12
# File 'fastlane/lib/fastlane/fast_file.rb', line 10

def current_platform
  @current_platform
end

#runnerObject

Stores all relevant information from the currently running process



6
7
8
# File 'fastlane/lib/fastlane/fast_file.rb', line 6

def runner
  @runner
end

Class Method Details

.sh(*command, step_name: nil, log: true, error_callback: nil, &b) ⇒ Object



220
221
222
223
224
225
226
227
# File 'fastlane/lib/fastlane/fast_file.rb', line 220

def self.sh(*command, step_name: nil, log: true, error_callback: nil, &b)
  command_header = step_name
  command_header ||= log ? Actions.shell_command_from_args(*command) : "shell command"

  Actions.execute_action(command_header) do
    Actions.sh_no_action(*command, log: log, error_callback: error_callback, &b)
  end
end

Instance Method Details

#action_completed(action_name, status: nil) ⇒ Object



463
464
465
466
467
468
# File 'fastlane/lib/fastlane/fast_file.rb', line 463

def action_completed(action_name, status: nil)
  completion_context = FastlaneCore::ActionCompletionContext.context_for_action_name(action_name,
                                                                                     args: ARGV,
                                                                                     status: status)
  FastlaneCore.session.action_completed(completion_context: completion_context)
end

#action_launched(action_name) ⇒ Object



456
457
458
459
460
461
# File 'fastlane/lib/fastlane/fast_file.rb', line 456

def action_launched(action_name)
  action_launch_context = FastlaneCore::ActionLaunchContext.context_for_action_name(action_name,
                                                                                    fastlane_client_language: :ruby,
                                                                                    args: ARGV)
  FastlaneCore.session.action_launched(launch_context: action_launch_context)
end

#actions_path(path) ⇒ Object



183
184
185
186
187
# File 'fastlane/lib/fastlane/fast_file.rb', line 183

def actions_path(path)
  UI.crash!("Path '#{path}' not found!") unless File.directory?(path)

  Actions.load_external_actions(path)
end

#after_all(&block) ⇒ Object

Is executed after each test run



143
144
145
# File 'fastlane/lib/fastlane/fast_file.rb', line 143

def after_all(&block)
  @runner.set_after_all(@current_platform, block)
end

#after_each(&block) ⇒ Object

Is executed before each lane



148
149
150
# File 'fastlane/lib/fastlane/fast_file.rb', line 148

def after_each(&block)
  @runner.set_after_each(@current_platform, block)
end

#before_all(&block) ⇒ Object

Is executed before each test run



133
134
135
# File 'fastlane/lib/fastlane/fast_file.rb', line 133

def before_all(&block)
  @runner.set_before_all(@current_platform, block)
end

#before_each(&block) ⇒ Object

Is executed before each lane



138
139
140
# File 'fastlane/lib/fastlane/fast_file.rb', line 138

def before_each(&block)
  @runner.set_before_each(@current_platform, block)
end

#desc(string) ⇒ Object



229
230
231
# File 'fastlane/lib/fastlane/fast_file.rb', line 229

def desc(string)
  desc_collection << string
end

#desc_collectionObject



233
234
235
# File 'fastlane/lib/fastlane/fast_file.rb', line 233

def desc_collection
  @desc_collection ||= []
end

#error(&block) ⇒ Object

Is executed if an error occurred during fastlane execution



153
154
155
# File 'fastlane/lib/fastlane/fast_file.rb', line 153

def error(&block)
  @runner.set_error(@current_platform, block)
end

#fastlane_require(gem_name) ⇒ Object



237
238
239
# File 'fastlane/lib/fastlane/fast_file.rb', line 237

def fastlane_require(gem_name)
  FastlaneRequire.install_gem_if_needed(gem_name: gem_name, require_gem: true)
end

#find_tag(folder: nil, version: nil, remote: false) ⇒ Object



268
269
270
271
272
273
# File 'fastlane/lib/fastlane/fast_file.rb', line 268

def find_tag(folder: nil, version: nil, remote: false)
  req = Gem::Requirement.new(version)
  all_tags = get_tags(folder: folder, remote: remote)

  return all_tags.select { |t| req =~ FastlaneCore::TagVersion.new(t) }.last
end

#generated_fastfile_id(id) ⇒ Object



241
242
243
# File 'fastlane/lib/fastlane/fast_file.rb', line 241

def generated_fastfile_id(id)
  UI.important("The `generated_fastfile_id` action was deprecated, you can remove the line from your `Fastfile`")
end

#get_tags(folder: nil, remote: false) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'fastlane/lib/fastlane/fast_file.rb', line 410

def get_tags(folder: nil, remote: false)
  if remote
    UI.message("Fetching remote git tags...")
    Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
      Actions.sh("cd #{folder.shellescape} && git fetch --all --tags -q")
    end
  end

  # Fetch all possible tags
  git_tags_string = Actions.sh("cd #{folder.shellescape} && git tag -l")
  git_tags = git_tags_string.split("\n")

  # Sort tags based on their version number
  return git_tags
         .select { |tag| FastlaneCore::TagVersion.correct?(tag) }
         .sort_by { |tag| FastlaneCore::TagVersion.new(tag) }
end

#import(path = nil) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'fastlane/lib/fastlane/fast_file.rb', line 245

def import(path = nil)
  UI.user_error!("Please pass a path to the `import` action") unless path

  path = path.dup.gsub("~", Dir.home)
  unless Pathname.new(path).absolute? # unless an absolute path
    path = File.join(File.expand_path('..', @path), path)
  end

  UI.user_error!("Could not find Fastfile at path '#{path}'") unless File.exist?(path)

  # First check if there are local actions to import in the same directory as the Fastfile
  actions_path = File.join(File.expand_path("..", path), 'actions')
  Fastlane::Actions.load_external_actions(actions_path) if File.directory?(actions_path)

  action_launched('import')

  return_value = parse(File.read(path), path)

  action_completed('import', status: FastlaneCore::ActionCompletionStatus::SUCCESS)

  return return_value
end

#import_from_git(url: nil, branch: 'HEAD', path: 'fastlane/Fastfile', version: nil, dependencies: [], cache_path: nil) ⇒ Object

Parameters:

  • url (String) (defaults to: nil)

    The git URL to clone the repository from

  • branch (String) (defaults to: 'HEAD')

    The branch to checkout in the repository

  • path (String) (defaults to: 'fastlane/Fastfile')

    The path to the Fastfile

  • version (String, Array) (defaults to: nil)

    Version requirement for repo tags

  • dependencies (Array) (defaults to: [])

    An optional array of additional Fastfiles in the repository

  • cache_path (String) (defaults to: nil)

    An optional path to a directory where the repository should be cloned into



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'fastlane/lib/fastlane/fast_file.rb', line 281

def import_from_git(url: nil, branch: 'HEAD', path: 'fastlane/Fastfile', version: nil, dependencies: [], cache_path: nil) # rubocop:disable Metrics/PerceivedComplexity
  UI.user_error!("Please pass a path to the `import_from_git` action") if url.to_s.length == 0

  Actions.execute_action('import_from_git') do
    require 'tmpdir'

    action_launched('import_from_git')

    is_eligible_for_caching = !cache_path.nil?

    UI.message("Eligible for caching") if is_eligible_for_caching

    # Checkout the repo
    repo_name = url.split("/").last
    checkout_param = branch

    import_block = proc do |target_path|
      clone_folder = File.join(target_path, repo_name)

      branch_option = "--branch #{branch}" if branch != 'HEAD'

      checkout_dependencies = dependencies.map(&:shellescape).join(" ")

      # If the current call is eligible for caching, we check out all the
      # files and directories. If not, we only check out the specified
      # `path` and `dependencies`.
      checkout_path = is_eligible_for_caching ? "" : "#{path.shellescape} #{checkout_dependencies}"

      if Dir[clone_folder].empty?
        UI.message("Cloning remote git repo...")
        Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
          # When using cached clones, we need the entire repository history
          # so we can switch between tags or branches instantly, or else,
          # it would defeat the caching's purpose.
          depth = is_eligible_for_caching ? "" : "--depth 1"

          Actions.sh("git clone #{url.shellescape} #{clone_folder.shellescape} #{depth} --no-checkout #{branch_option}")
        end
      end

      unless version.nil?
        if is_eligible_for_caching
          checkout_param = find_tag(folder: clone_folder, version: version, remote: false)

          if checkout_param.nil?
            # Update the repo and try again before failing
            UI.message("Updating git repo...")
            Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
              Actions.sh("cd #{clone_folder.shellescape} && git checkout #{branch} && git reset --hard && git pull --all")
            end

            checkout_param = find_tag(folder: clone_folder, version: version, remote: false)
          else
            UI.message("Found tag #{checkout_param}. No git repo update needed.")
          end
        else
          checkout_param = find_tag(folder: clone_folder, version: version, remote: true)
        end

        UI.user_error!("No tag found matching #{version.inspect}") if checkout_param.nil?
      end

      if is_eligible_for_caching
        if version.nil?
          # Update the repo if it's eligible for caching but the version isn't specified
          UI.message("Fetching remote git branches and updating git repo...")
          Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
            command = "cd #{clone_folder.shellescape} && git fetch --all --quiet && git checkout #{checkout_param.shellescape} #{checkout_path} && git reset --hard"
            # Check if checked out "branch" is actually a branch or a tag
            current_branch = Actions.sh("cd #{clone_folder.shellescape} && git rev-parse --abbrev-ref HEAD")
            command << " && git rebase" unless current_branch.strip.eql?("HEAD")
            Actions.sh(command)
          end
        else
          begin
            # https://stackoverflow.com/a/1593574/865175
            current_tag = Actions.sh("cd #{clone_folder.shellescape} && git describe --exact-match --tags HEAD").strip
          rescue
            current_tag = nil
          end

          if current_tag != version
            Actions.sh("cd #{clone_folder.shellescape} && git checkout #{checkout_param.shellescape} #{checkout_path}")
          end
        end
      else
        Actions.sh("cd #{clone_folder.shellescape} && git checkout #{checkout_param.shellescape} #{checkout_path}")
      end

      # Knowing that we check out all the files and directories when the
      # current call is eligible for caching, we don't need to also
      # explicitly check out the "actions" directory.
      unless is_eligible_for_caching
        # We also want to check out all the local actions of this fastlane setup
        containing = path.split(File::SEPARATOR)[0..-2]
        containing = "." if containing.count == 0
        actions_folder = File.join(containing, "actions")
        begin
          Actions.sh("cd #{clone_folder.shellescape} && git checkout #{checkout_param.shellescape} #{actions_folder.shellescape}")
        rescue
          # We don't care about a failure here, as local actions are optional
        end
      end

      return_value = nil
      if dependencies.any?
        return_value = [import(File.join(clone_folder, path))]
        return_value += dependencies.map { |file_path| import(File.join(clone_folder, file_path)) }
      else
        return_value = import(File.join(clone_folder, path))
      end

      action_completed('import_from_git', status: FastlaneCore::ActionCompletionStatus::SUCCESS)

      return return_value
    end

    if is_eligible_for_caching
      import_block.call(File.expand_path(cache_path))
    else
      Dir.mktmpdir("fl_clone", &import_block)
    end
  end
end

#is_platform_block?(key) ⇒ Boolean

Is the given key a platform block or a lane?

Returns:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'fastlane/lib/fastlane/fast_file.rb', line 167

def is_platform_block?(key)
  UI.crash!('No key given') unless key

  return false if self.runner.lanes.fetch(nil, {}).fetch(key.to_sym, nil)
  return true if self.runner.lanes[key.to_sym].kind_of?(Hash)

  if key.to_sym == :update
    # The user ran `fastlane update`, instead of `fastlane update_fastlane`
    # We're gonna be nice and understand what the user is trying to do
    require 'fastlane/one_off'
    Fastlane::OneOff.run(action: "update_fastlane", parameters: {})
  else
    UI.user_error!("Could not find '#{key}'. Available lanes: #{self.runner.available_lanes.join(', ')}")
  end
end

#lane(lane_name, &block) ⇒ Object

User defines a new lane



83
84
85
86
87
88
89
90
91
92
93
# File 'fastlane/lib/fastlane/fast_file.rb', line 83

def lane(lane_name, &block)
  UI.user_error!("You have to pass a block using 'do' for lane '#{lane_name}'. Make sure you read the docs on GitHub.") unless block

  self.runner.add_lane(Lane.new(platform: self.current_platform,
                                   block: block,
                             description: desc_collection,
                                    name: lane_name,
                              is_private: false))

  @desc_collection = nil # reset the collected description again for the next lane
end

#override_lane(lane_name, &block) ⇒ Object

User defines a lane that can overwrite existing lanes. Useful when importing a Fastfile



109
110
111
112
113
114
115
116
117
118
119
# File 'fastlane/lib/fastlane/fast_file.rb', line 109

def override_lane(lane_name, &block)
  UI.user_error!("You have to pass a block using 'do' for lane '#{lane_name}'. Make sure you read the docs on GitHub.") unless block

  self.runner.add_lane(Lane.new(platform: self.current_platform,
                                   block: block,
                             description: desc_collection,
                                    name: lane_name,
                              is_private: false), true)

  @desc_collection = nil # reset the collected description again for the next lane
end

#parse(data, path = nil) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'fastlane/lib/fastlane/fast_file.rb', line 47

def parse(data, path = nil)
  @runner ||= Runner.new

  Dir.chdir(FastlaneCore::FastlaneFolder.path || Dir.pwd) do # context: fastlane subfolder
    # create nice path that we want to print in case of some problem
    relative_path = path.nil? ? '(eval)' : Pathname.new(path).relative_path_from(Pathname.new(Dir.pwd)).to_s

    begin
      # We have to use #get_binding method, because some test files defines method called `path` (for example SwitcherFastfile)
      # and local variable has higher priority, so it causes to remove content of original Fastfile for example. With #get_binding
      # is this always clear and safe to declare any local variables we want, because the eval function uses the instance scope
      # instead of local.

      # rubocop:disable Security/Eval
      eval(data, parsing_binding, relative_path) # using eval is ok for this case
      # rubocop:enable Security/Eval
    rescue SyntaxError => ex
      match = ex.to_s.match(/#{Regexp.escape(relative_path)}:(\d+)/)
      if match
        line = match[1]
        UI.content_error(data, line)
        UI.user_error!("Syntax error in your Fastfile on line #{line}: #{ex}")
      else
        UI.user_error!("Syntax error in your Fastfile: #{ex}")
      end
    end
  end

  self
end

#parsing_bindingObject



43
44
45
# File 'fastlane/lib/fastlane/fast_file.rb', line 43

def parsing_binding
  binding
end

#platform(platform_name) ⇒ Object

User defines a platform block



122
123
124
125
126
127
128
129
130
# File 'fastlane/lib/fastlane/fast_file.rb', line 122

def platform(platform_name)
  SupportedPlatforms.verify!(platform_name)

  self.current_platform = platform_name

  yield

  self.current_platform = nil
end

#private_lane(lane_name, &block) ⇒ Object

User defines a new private lane, which can’t be called from the CLI



96
97
98
99
100
101
102
103
104
105
106
# File 'fastlane/lib/fastlane/fast_file.rb', line 96

def private_lane(lane_name, &block)
  UI.user_error!("You have to pass a block using 'do' for lane '#{lane_name}'. Make sure you read the docs on GitHub.") unless block

  self.runner.add_lane(Lane.new(platform: self.current_platform,
                                   block: block,
                             description: desc_collection,
                                    name: lane_name,
                              is_private: true))

  @desc_collection = nil # reset the collected description again for the next lane
end

#puts(value) ⇒ Object



441
442
443
444
445
446
447
448
449
# File 'fastlane/lib/fastlane/fast_file.rb', line 441

def puts(value)
  # Overwrite this, since there is already a 'puts' method defined in the Ruby standard library
  value ||= yield if block_given?

  action_launched('puts')
  return_value = Fastlane::Actions::PutsAction.run([value])
  action_completed('puts', status: FastlaneCore::ActionCompletionStatus::SUCCESS)
  return return_value
end

#say(value) ⇒ Object

Speak out loud



433
434
435
436
437
438
439
# File 'fastlane/lib/fastlane/fast_file.rb', line 433

def say(value)
  # Overwrite this, since there is already a 'say' method defined in the Ruby standard library
  value ||= yield

  value = { text: value } if value.kind_of?(String) || value.kind_of?(Array)
  self.runner.trigger_action_by_name(:say, nil, false, value)
end

#sh(*args, &b) ⇒ Object

Execute shell command Accepts arguments with and without the command named keyword so that sh behaves like other actions with named keywords github.com/fastlane/fastlane/issues/14930

Example:

sh("ls")
sh("ls", log: false)
sh(command: "ls")
sh(command: "ls", step_name: "listing the files")
sh(command: "ls", log: false)


200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'fastlane/lib/fastlane/fast_file.rb', line 200

def sh(*args, &b)
  # First accepts hash (or named keywords) like other actions
  # Otherwise uses sh method that doesn't have an interface like an action
  if args.count == 1 && args.first.kind_of?(Hash)
    options = args.first
    command = options.delete(:command)

    raise ArgumentError, "sh requires :command keyword in argument" if command.nil?
    log = options[:log].nil? ? true : options[:log]
    FastFile.sh(*command, step_name: options[:step_name], log: log, error_callback: options[:error_callback], &b)
  elsif args.count != 1 && args.last.kind_of?(Hash)
    new_args = args.dup
    options = new_args.pop
    log = options[:log].nil? ? true : options[:log]
    FastFile.sh(*new_args, step_name: options[:step_name], log: log, error_callback: options[:error_callback], &b)
  else
    FastFile.sh(*args, &b)
  end
end

#test(params = {}) ⇒ Object



451
452
453
454
# File 'fastlane/lib/fastlane/fast_file.rb', line 451

def test(params = {})
  # Overwrite this, since there is already a 'test' method defined in the Ruby standard library
  self.runner.try_switch_to_lane(:test, [params])
end