123456789_123456789_123456789_123456789_123456789_

Class: MakeMakefile::Depend

Relationships & Source Files
Namespace Children
Classes:
Inherits: Object
Defined in: lib/mkmf/depend.rb

Overview

Generates dependency rules for Ruby and C extension sources.

An extension project can update the autogenerated section of its depend file while running extconf.rb:

ruby extconf.rb --update-depend

The depend file must contain these markers:

# AUTOGENERATED DEPENDENCIES START
# AUTOGENERATED DEPENDENCIES END

Depend scans each source and the headers it includes without invoking a compiler. When updating an extension project, files outside the project tree, such as system and installed Ruby headers, are omitted. Use --depend-root=DIR when the project root cannot be detected from a .git directory.

Constant Summary

Class Method Summary

Instance Attribute Summary

  • #root readonly

    Source tree root used to resolve dependency paths.

Instance Method Summary

Constructor Details

.new(root: DEFAULT_ROOT) ⇒ Depend

Creates a dependency updater for #root.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 360

def initialize(root: DEFAULT_ROOT)
  @root = File.expand_path(root)
  @make_variables = {}
  @dependency_declarations = {}
  @dependency_targets = {}
  @dependency_contents = {}
  @source_cache = {}
  @stat_cache = {}
end

Class Method Details

.execute(inputs, root: DEFAULT_ROOT, **options) (private)

Creates an updater and runs it with the parsed command-line options.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 354

def execute(inputs, root: DEFAULT_ROOT, **options)
  new(root: root).run(inputs, **options)
end

.find_root(path)

Finds the nearest ancestor of path containing .git.

Returns path itself when no repository root can be found.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 272

def find_root(path)
  path = File.expand_path(path)
  root = path
  loop do
    return path if File.exist?(File.join(path, ".git"))
    parent = File.dirname(path)
    break if parent == path
    path = parent
  end
  root
end

.main(argv = ARGV, err: $stderr)

Runs the command-line interface with argv.

Returns whether the requested update or check succeeded.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 259

def main(argv = ARGV, err: $stderr)
  options, inputs = parse_options(argv)
rescue OptionParser::ParseError => error
  err.puts "#{File.basename($0)}: #{error.message}"
  err.puts "Try '#{File.basename($0)} --help' for more information."
  false
else
  execute(inputs, **options)
end

.parse_options(argv) (private)

Parses command-line arguments into runner options and input paths.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 287

def parse_options(argv)
  options = {}
  make_variables = {}
  output = nil
  select_mode = proc do |mode|
    options[:mode] = mode
  end
  parser = OptionParser.new do |opts|
    opts.banner =
      "Usage: #{File.basename($0)} [options] [VAR=value ...] [files]"
    opts.separator ""
    opts.separator "Input selection:"
    opts.on("--root=DIR", "source tree root") {|value| options[:root] = value}
    opts.on(
      "--thread-model=MODEL",
      String,
      "thread model (same as THREAD_MODEL=MODEL)",
    ) do |value|
      make_variables["THREAD_MODEL"] = value
    end
    opts.on(
      "--scope=SCOPE", [:all, :core, :extensions],
      "dependency files to process (all, core, extensions)",
    ) do |value|
      options[:scope] = value
    end

    opts.separator ""
    opts.separator "Update mode (last one wins; default: stdout):"
    opts.on("--output=DIR", "write dependencies under DIR") do |value|
      select_mode[:output]
      output = value
    end
    opts.on("--inplace", "update dependency files in place") do
      select_mode[:inplace]
    end
    opts.on("--check", "check dependency files without updating") do
      select_mode[:check]
    end

    opts.separator ""
    opts.separator "Output options:"
    opts.on("--nmake", "emit NMake dependency rules") do
      options[:nmake] = true
    end
    opts.on("--sources", "emit source mappings") do
      options[:sources] = true
    end
    opts.on("-v", "--verbose", "show scanned sources") do
      options[:verbose] = true
    end
  end
  inputs = []
  rest = parser.order(argv) do |arg|
    if /\A([A-Za-z_]\w*)=(.*)\z/m =~ arg
      make_variables[$1] = $2
    else
      inputs << arg
    end
  end
  inputs.concat(rest)
  options[:make_variables] = make_variables unless make_variables.empty?
  options[:output] = output if options[:mode] == :output
  [options, inputs]
end

Instance Attribute Details

#root (readonly)

Source tree root used to resolve dependency paths.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 253

attr_reader :root

Instance Method Details

#compact_dependencies(rules, group: true) (private)

Sorts and deduplicates rules, optionally grouping targets by dependency.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 778

def compact_dependencies(rules, group: true)
  return rules.lines.uniq.sort.join unless group

  grouped = Hash.new {|hash, dependency| hash[dependency] = []}
  lines = []
  rules.each_line do |line|
    unless /\A(\S):\s(\S+)\s*\z/ =~ line
      lines << line
      next
    end
    target, dependency = $1, $2
    if dependency.end_with?(".c", ".y")
      lines << "#{target}: #{dependency}\n"
    else
      grouped[dependency] << target
    end
  end
  grouped.each do |dependency, targets|
    targets.uniq.sort.each_slice(8) do |slice|
      lines << "#{slice.join(' ')}: #{dependency}\n"
    end
  end
  lines.uniq.sort.join
end

#declaration(map, name, input = nil) (private)

Finds the declaration for name, accounting for an input-relative name.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 533

def declaration(map, name, input = nil)
  candidates = [name]
  if input
    dir = File.dirname(relative_source(input))
    candidates << name.delete_prefix(dir + "/") unless dir == "."
  end
  key = candidates.uniq.find {|candidate| map.key?(candidate)}
  [key, map[key]] if key
end

#dependency_cache_signature(nmake:, sources:) (private)

Returns the cache signature for options affecting generated dependencies.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 980

def dependency_cache_signature(nmake:, sources:)
  [DEPENDENCY_CACHE_VERSION, nmake, sources, @make_variables.sort].inspect
end

#dependency_declarations(input = nil, source: nil, content: nil) (private)

Returns merged global and file-local declarations for an input or source.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 493

def dependency_declarations(input = nil, source: nil, content: nil)
  input ||= dependency_input(source)
  input = relative_source(input || "depend")
  @dependency_declarations[input] ||= begin
    if input == "depend"
      path = File.join(@root, input)
      parse_dependency_declarations(path, dependency_file_content(path, content))
    else
      global = dependency_declarations(File.join(@root, "depend"))
      path = File.absolute_path?(input) ? input : File.join(@root, input)
      local = parse_dependency_declarations(
        path, dependency_file_content(path, content),
      )
      Declarations.new(
        global.scan.merge(local.scan),
        global.dependencies.merge(local.dependencies),
        global.defines.merge(local.defines),
        global.undefines.merge(local.undefines),
      )
    end
  end
end

#dependency_file_content(path, content = nil) (private)

Reads and caches a dependency file, or caches supplied content.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 481

def dependency_file_content(path, content = nil)
  path = File.expand_path(path, @root)
  if content
    @dependency_contents[path] = content
  elsif @dependency_contents.key?(path)
    @dependency_contents[path]
  elsif File.file?(path)
    @dependency_contents[path] = File.read(path)
  end
end

#dependency_files(scope = :all) (private)

Returns marked dependency files selected by scope.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 821

def dependency_files(scope = :all)
  Dir.chdir(@root) do
    files = case scope
    when :all
      %w[depend enc/depend].concat(Dir.glob("ext/**/depend"))
    when :core
      %w[depend enc/depend]
    when :extensions
      Dir.glob("ext/**/depend")
    else
      raise ArgumentError, "unknown dependency scope: #{scope}"
    end
    files.select do |file|
      next false unless File.file?(file)
      content = File.read(file)
      next false unless content.include?(MARK_START)
      dependency_file_content(file, content)
      true
    end
  end
end

#dependency_input(source) (private)

Returns the dependency file that governs source.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 517

def dependency_input(source)
  return "depend" unless source
  source = relative_source(source)
  if source.start_with?("ext/")
    dir = File.dirname(source)
    until dir == "." || dir == "ext"
      return File.join(dir, "depend") if File.file?(File.join(@root, dir, "depend"))
      dir = File.dirname(dir)
    end
  elsif source.start_with?("enc/")
    return "enc/depend"
  end
  "depend"
end

#dependency_output_fresh?(destination, signature) ⇒ Boolean (private)

Returns whether destination and all recorded scan inputs are unchanged.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 995

def dependency_output_fresh?(destination, signature)
  cache_path = destination + ".mkdepend"
  return false unless (output_stat = dependency_stat(destination))
  return false unless File.file?(cache_path)

  lines = File.readlines(cache_path, chomp: true)
  header = lines.shift&.split("\t", 5)
  return false unless header == [
    "mkdepend", signature, output_stat.size.to_s,
    output_stat.mtime.to_i.to_s, output_stat.mtime.nsec.to_s,
  ]

  lines.all? do |line|
    state, path = line.split("\t", 2)
    next false unless path

    stat = dependency_stat(path)
    state == "+" ? stat && stat.mtime <= output_stat.mtime : !stat
  end
rescue Errno::ENOENT
  false
end

#dependency_scanner(src, declarations, input) (private)

Builds a Depend::Scanner configured by the declarations governing src.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 717

def dependency_scanner(src, declarations, input)
  macros = {}
  macros["RUBY_EXTCONF_H"] = "extconf.h" if src.start_with?("ext/")
  defined = declaration(declarations.defines, src, input)&.last || []
  undefined = %w[__cplusplus]
  if undefined_macros = declaration(declarations.undefines, src, input)
    undefined.concat(undefined_macros[1])
  end
  Scanner.new(
    root: @root,
    include_dirs: [
      @root,
      File.join(@root, "include"),
      File.join(@root, "prism"),
      File.dirname(src),
    ],
    macros: macros,
    targets: dependency_targets(input),
    aliases: declarations.scan,
    dependencies: declarations.dependencies,
    defined: defined,
    undefined: undefined,
    cache: @source_cache,
  )
end

#dependency_source?(source, input = nil) ⇒ Boolean (private)

Returns whether source is declared as generated or otherwise resolvable.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 709

def dependency_source?(source, input = nil)
  declarations = dependency_declarations(input, source: source)
  declaration(declarations.scan, source, input) ||
    declaration(declarations.dependencies, source, input) ||
    dependency_target(source, input || dependency_input(source))
end

#dependency_source_map(rules, input) (private)

Returns an object-target to source-path mapping extracted from rules.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 865

def dependency_source_map(rules, input)
  dependency_source_rules(rules, input).each_with_object({}) do |(target, data), result|
    result[target] = data[1]
  end
end

#dependency_source_rules(rules, input) (private)

Extracts the best source rule for each object target in rules.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 844

def dependency_source_rules(rules, input)
  input = relative_source(input)
  srcs = {}
  rules.each_line do |line|
    next unless %r[\A([-.\w/])\.(?:\$\(OBJEXT\)|o):\s(.+?)\s*\z] =~ line
    target, dependency = $1, $2
    next unless src = dependency[%r{[-.\w/]+\.[cy]\z}]
    src = src.delete_prefix("/")
    source_name = File.basename(src, ".*")
    target_name = File.basename(target)
    next unless target_name == source_name || target_name.end_with?("-#{source_name}")
    local = input.start_with?("ext/") && !dependency.include?("$(top_srcdir)")
    src = File.join(File.dirname(input), src) if local && !src.start_with?("ext/")
    rank = src.end_with?(".y") ? 2 : local ? 1 : 0
    current = srcs[target]
    srcs[target] = [rank, src, line] if !current || rank >= current.first
  end
  srcs.sort.to_h
end

#dependency_sources(rules, input) (private)

Returns the source paths identified by rules.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 872

def dependency_sources(rules, input)
  dependency_source_map(rules, input).values
end

#dependency_stat(path) (private)

Returns cached stat information, or false when path does not exist.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 985

def dependency_stat(path)
  path = File.expand_path(path)
  return @stat_cache[path] if @stat_cache.key?(path)

  @stat_cache[path] = File.stat(path)
rescue Errno::ENOENT, Errno::ENOTDIR
  @stat_cache[path] = false
end

#dependency_target(name, input) (private)

Finds the declared target matching name in input.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 564

def dependency_target(name, input)
  input = relative_source(input)
  targets = dependency_targets(input)
  candidates = [name]
  dir = File.dirname(input)
  candidates << name.delete_prefix(dir + "/") unless dir == "."
  candidates.uniq.find {|candidate| targets.include?(candidate)}
end

#dependency_targets(input, content = nil) (private)

Returns generated targets declared outside the marked section of input.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 544

def dependency_targets(input, content = nil)
  input = relative_source(input)
  @dependency_targets[input] ||= begin
    targets = []
    path = File.absolute_path?(input) ? input : File.join(@root, input)
    content = dependency_file_content(path, content)
    if content
      content.each_line do |line|
        if line.start_with?(MARK_START)..line.start_with?(MARK_END)
          next
        elsif target = line[/\A[^\s#][^:=]*?(?=\s*:(?!=))/]
          targets.concat(target.split)
        end
      end
    end
    targets.uniq
  end
end

#dependency_vpath(input, source) (private)

Returns the VPATH prefix used for dependencies of source.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 702

def dependency_vpath(input, source)
  return if source.start_with?("enc/") &&
    (!input || relative_source(input) == "enc/depend")
  "{$(VPATH)}"
end

#depends(files, vpath, source: nil, input: nil, declarations: nil, project: false) (private)

Converts scanned files into sorted Make dependencies for source.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 638

def depends(files, vpath, source: nil, input: nil, declarations: nil,
            project: false)
  declaration_input = input || dependency_input(source)
  declarations ||= dependency_declarations(declaration_input, source: source)
  extension_dir = File.dirname(source) if source&.start_with?("ext/")
  project_dir = File.dirname(source) if project
  expand = lambda do |file, expanded|
    if !expanded.include?(file) &&
        (dependencies = declaration(declarations.dependencies, file,
                                    declaration_input))
      dependencies[1].flat_map {|dep| expand.call(dep, expanded | [file])}
    else
      file
    end
  end
  files = files.flat_map {|file| expand.call(file, [])}
  files.each_with_object([]) do |file, deps|
    file = relative_dependency(file)
    dep = if file.start_with?('$(', '{$(')
      file
    elsif target = dependency_target(file, declaration_input)
      target
    elsif project_dir
      project_dependency(file, project_dir)
    elsif extension_dir
      extension_dependency(file, extension_dir)
    else
      case file
      when %r[\Aenc/unicode/[\d.]+/]
        "$(UNICODE_HDR_DIR)/#$'"
      when 'encindex.h', 'transcode_data.h', /\Areg(?!ex\b)\w+\.h\z/
        "#{vpath || '$(top_srcdir)/'}#{file}"
      when /\.e?rb\z/
      when %r[\Atool/]
        "$(top_srcdir)/#{file}"
      when %r[\Ainclude/\Kruby(?:/ruby|/version)?\.h\z]
        "$(hdrdir)/#$&"
      when %r[\A(?:internal|prism)\/]
        "$(top_srcdir)/#{file}"
      when %r[\Accan/]
        "$(CCAN_DIR)/#$'"
      when %r[\Aenc/]
        "#{vpath}#{file}"
      when %r[\Ainclude/ruby/], %r[\Amissing/]
        "#{vpath}#$'"
      else
        "#{vpath}#{file}"
      end
    end
    deps << dep if dep
  end.uniq.sort
end

#expand_dependency_variables(dependencies) (private)

Expands known Make variables that stand for dependency lists.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 402

def expand_dependency_variables(dependencies)
  dependencies.flat_map do |dependency|
    if /\A\$\((\w+)\)\z/ =~ dependency &&
        (values = @make_variables[$1])
      values.flat_map(&:split)
    else
      expand_make_variables(dependency)
    end
  end
end

#expand_make_variables(value, defaults: {}) (private)

Expands known Make variables embedded in value.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 384

def expand_make_variables(value, defaults: {})
  value.gsub(/\$\((\w+)\)/) do |variable|
    name = $1
    values = if @make_variables.key?(name)
      @make_variables[name]
    elsif defaults.key?(name)
      Array(defaults[name])
    else
      next variable
    end
    unless values.one?
      raise ArgumentError, "#{name} is not a scalar Make variable"
    end
    values.first.to_s
  end
end

#expand_scan_declaration(name, source) (private)

Expands matching wildcards in a scan declaration.

Returns a generated-name to source-path mapping.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 416

def expand_scan_declaration(name, source)
  source_parts = source.split(/(\*\*|\*)/)
  captures = source_parts.count {|part| part == "*" || part == "**"}
  return unless captures == name.scan(/\*\*|\*/).size
  return {name => source} if captures.zero?

  pattern = source_parts.map do |part|
    case part
    when "**" then "(.*)"
    when "*" then "([^/]*)"
    else Regexp.escape(part)
    end
  end.join
  pattern = /\A#{pattern}\z/
  Dir.glob(File.join(@root, source)).sort.each_with_object({}) do |path, map|
    path = relative_source(path)
    match = pattern.match(path) or next
    values = match.captures
    generated = name.gsub(/\*\*|\*/) {values.shift}
    map[generated] = path
  end
end

#extension_dependency(file, source_dir) (private)

Converts an extension dependency to the Make variable path it requires.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 599

def extension_dependency(file, source_dir)
  case file
  when %r[\A(?:#{Regexp.escape(source_dir)}/)?extconf\.h\z]
    "$(RUBY_EXTCONF_H)"
  when 'config.h'
    "$(arch_hdrdir)/ruby/config.h"
  when %r[\Ainclude/]
    "$(hdrdir)/#$'"
  when 'probes.h'
  else
    if file.start_with?(prefix = source_dir + "/")
      file.delete_prefix(prefix)
    elsif file.start_with?(prefix = File.dirname(source_dir) + "/")
      "$(srcdir)/../#{file.delete_prefix(prefix)}"
    elsif !file.include?("/") && File.file?(File.join(@root, source_dir, file))
      file
    elsif /\.e?rb\z/.match?(file)
      nil
    else
      "$(top_srcdir)/#{file}"
    end
  end
end

#makedepend(src, out = [], target: nil, input: nil, project: false, accessed: nil) (private)

Appends Make dependency rules for src to out and returns out.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 744

def makedepend(src, out = [], target: nil, input: nil, project: false,
               accessed: nil)
  src = relative_dependency(src)
  declaration_input = input || dependency_input(src)
  declarations = dependency_declarations(declaration_input, source: src)
  vpath = dependency_vpath(input, src)
  scanner = dependency_scanner(src, declarations, declaration_input)
  scan_source = if mapping = declaration(declarations.scan, src,
                                         declaration_input)
    mapping[1]
  else
    src
  end
  files = scanner.scan(scan_source)
  accessed&.merge(scanner.accessed)
  if dependencies = declaration(declarations.dependencies, src,
                                declaration_input)
    files.concat(dependencies[1])
  end
  files << src if scan_source != src
  files << src.sub(/\.y\z/, ".c") if src.end_with?(".y")
  obj = if target
    src.start_with?("ext/") ? "#{target}.o" : "#{target}.$(OBJEXT)"
  else
    object_name(src)
  end
  depends(files, vpath, source: src, input: input,
          declarations: declarations, project: project).each do |dep|
    out << "#{obj}: #{dep}\n"
  end
  out
end

#minimize_deps(rules, input) (private)

Reduces rules to source mappings needed to regenerate dependencies.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 888

def minimize_deps(rules, input)
  sources = dependency_source_rules(rules, input)
  targets = rules.scan(%r[^([-\.\w/]+)\.(?:\$\(OBJEXT\)|o):]).flatten.uniq
  unless (missing = targets - sources.keys).empty?
    raise "no source files for #{missing.join(', ')} in #{input}"
  end
  sources.each_value {|_, src| resolve_dependency_source(src, input)}
  minimal = sources.values.map(&:last)
  if relative_source(input).start_with?("ext/")
    rules.each_line do |line|
      minimal << line if /\A.*:\s+\$\(srcdir\)\//.match?(line)
    end
  end
  minimal.uniq.sort.join
end

#normalize_dependency_rules(rules) (private)

Removes VPATH markers that are unnecessary in build-directory output.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 804

def normalize_dependency_rules(rules)
  rules.gsub(/\{(?:\.;)?\$\(VPATH\)\}/, '')
end

#object_name(src) (private)

Returns the Make object name for source path src.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 692

def object_name(src)
  stem = src.sub(/\.[cy]\z/, "")
  if src.start_with?("ext/")
    "#{File.basename(stem)}.o"
  else
    "#{stem.delete_prefix('missing/')}.$(OBJEXT)"
  end
end

#parse_dependency_declarations(path, content = nil) (private)

Parses mkdepend declarations from path or the supplied content.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 440

def parse_dependency_declarations(path, content = nil)
  declarations = Declarations.new({}, {}, {}, {})
  content ||= File.read(path) if File.file?(path)
  return declarations unless content

  lineno = 0
  content.each_line do |line|
    lineno += 1
    case line[/\A#\s*mkdepend:\s*\K.*/]
    when nil
      next
    when /\Ascan\s(\S)\s*=>\s*(\S+)\s*\z/
      scan = expand_scan_declaration($1, $2)
      unless scan && (!scan.empty? || !$2.include?("*"))
        raise "#{path}:#{lineno}: invalid mkdepend declaration: #{line.strip}"
      end
      declarations.scan.update(scan)
    when /\Adepends\s(\S)\s*=>\s*(.+?)\s*\z/
      name, dependencies = $1, $2.split
      if declarations.dependencies.key?(name)
        raise "#{path}:#{lineno}: duplicate mkdepend declaration: #{name}"
      end
      declarations.dependencies[name] = dependencies
    when /\Adefine\s(\S)\s*=>\s*(.+?)\s*\z/
      declarations.defines[$1] = $2.split
    when /\Aundef\s(\S)\s*=>\s*(.+?)\s*\z/
      declarations.undefines[$1] = $2.split
    else
      raise "#{path}:#{lineno}: invalid mkdepend declaration: #{line.strip}"
    end
  end
  declarations.scan.transform_values! do |source|
    expand_make_variables(source, defaults: {"THREAD_MODEL" => "pthread"})
  end
  declarations.dependencies.transform_values! do |dependencies|
    expand_dependency_variables(dependencies)
  end
  declarations
end

#project_dependency(file, source_dir) (private)

Converts a standalone project's file to a dependency relative to its source.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 624

def project_dependency(file, source_dir)
  source_dir = File.join(@root, source_dir)
  path = File.expand_path(file, @root)
  prefix = @root + File::SEPARATOR
  return unless path.start_with?(prefix)

  dependency = Pathname.new(path).relative_path_from(
    Pathname.new(source_dir)
  ).to_s.tr(File::SEPARATOR, "/")
  return dependency unless dependency.start_with?("../")
  "$(srcdir)/#{dependency}"
end

#relative_dependency(path) (private)

Makes path relative to #root without consulting the current directory. Unlike #relative_source, a relative name that does not refer to a source-tree file is kept as-is: generated dependencies such as builtin_binary.rbbin live in the build directory, and must keep the name their Make rules use even when the tool runs in a build directory nested inside the source tree.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 587

def relative_dependency(path)
  expanded = File.expand_path(path, @root)
  prefix = @root + File::SEPARATOR
  if expanded.start_with?(prefix) &&
      (File.absolute_path?(path) || File.exist?(expanded))
    expanded.delete_prefix(prefix)
  else
    path
  end
end

#relative_source(path) (private)

Makes path relative to #root when it refers to a source-tree file.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 574

def relative_source(path)
  expanded = File.expand_path(path)
  expanded = File.expand_path(path, @root) unless File.exist?(expanded)
  prefix = @root + File::SEPARATOR
  expanded.start_with?(prefix) ? expanded.delete_prefix(prefix) : path
end

#replace_file(path, content) (private)

Atomically replaces path with content, preserving its file mode.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 962

def replace_file(path, content)
  dir = File.dirname(path)
  FileUtils.mkdir_p(dir)
  mode = File.exist?(path) ? File.stat(path).mode & 0777 : 0644
  Tempfile.create([File.basename(path), ".tmp"], dir) do |file|
    file.binmode
    file.write(content)
    file.flush
    file.chmod(mode)
    file.close
    File.rename(file.path, path)
  end
end

#report_outdated_dependencies(input, current, expected, err: $stderr) (private)

Reports how dependency rules in input differ from expected.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 1039

def report_outdated_dependencies(input, current, expected, err: $stderr)
  current_lines = current.lines
  expected_lines = expected.lines
  label = relative_source(input)
  err.puts "#{label}: dependencies are outdated"
  if current_lines.size <= 20
    err.puts "  current dependency rules:"
    current_lines.each {|line| err.puts "    #{line.chomp}"}
  else
    current_word = current_lines.one? ? "rule" : "rules"
    expected_word = expected_lines.one? ? "source rule" : "source rules"
    err.puts "  current section has #{current_lines.size} #{current_word}; " \
             "expected #{expected_lines.size} #{expected_word}"
  end
  err.puts "  expected source rules:"
  expected_lines.each {|line| err.puts "    #{line.chomp}"}
end

#resolve_dependency_source(src, input) (private)

Resolves src to an existing, missing-library, or declared source path.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 877

def resolve_dependency_source(src, input)
  return src if File.file?(File.join(@root, src))

  missing = "missing/#{src}"
  return missing if File.file?(File.join(@root, missing))
  return src if dependency_source?(src, input)

  raise "source file not found: #{src} in #{input}"
end

#run(inputs = ARGV, out: $stdout, err: $stderr, mode: :stdout, output: nil, make_variables: {}, nmake: false, sources: false, scope: nil, thread_model: nil, verbose: false)

Processes dependency inputs according to the selected update mode.

make_variables supplies values for Make variables in declarations. thread_model is a shortcut for its THREAD_MODEL entry.

Returns false only when check mode finds an outdated dependency file.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 1065

def run(inputs = ARGV, out: $stdout, err: $stderr, mode: :stdout,
        output: nil, make_variables: {}, nmake: false, sources: false,
        scope: nil, thread_model: nil, verbose: false)
  @source_cache.clear
  @stat_cache.clear
  case mode
  when :output
    raise ArgumentError, "output directory is missing" unless output
  when :stdout, :inplace, :check
    raise ArgumentError, "output requires output mode" if output
  else
    raise ArgumentError, "unknown update mode: #{mode.inspect}"
  end
  unless thread_model.nil?
    if thread_model.empty?
      raise ArgumentError, "thread model must not be empty"
    end
    make_variables = make_variables.merge("THREAD_MODEL" => thread_model)
  end
  select_make_variables(make_variables)
  if scope
    inputs = dependency_files(scope).map {|file| File.join(@root, file)}
  end
  output = File.expand_path(output) if output
  cache_signature = dependency_cache_signature(
    nmake: nmake, sources: sources,
  )
  changed = false
  inputs.each do |input|
    if input.end_with?(".c", ".y")
      out.puts makedepend(relative_source(input))
    else
      deps = dependency_file_content(input) || File.read(input)
      dependency_declarations(input, content: deps)
      dependency_targets(input, deps)
      match = MARK_SECTION.match(deps)
      next unless match
      raise "missing #{MARK_END} in #{input}" unless match.begin(1)

      destination = File.join(output, relative_source(input)) if output
      if destination && dependency_output_fresh?(destination, cache_signature)
        next
      end

      current_rules = match[0]
      accessed = Set.new
      expected = update_deps(
        current_rules, input, group: !nmake, verbose: verbose,
        accessed: accessed,
      )
      expected = minimize_deps(expected, input) if sources
      updated = match.pre_match + expected + match.post_match
      if mode == :output
        updated = normalize_dependency_rules(updated) unless nmake
        replace_file(destination, updated)
        accessed.add(File.expand_path(input))
        accessed.add(File.join(@root, "depend"))
        accessed.add(File.expand_path(__FILE__))
        accessed.add(File.join(@root, "tool/mkdepend.rb"))
        write_dependency_cache(destination, cache_signature, accessed)
      elsif same_dependency_rules?(current_rules, expected)
        next
      elsif mode == :inplace
        replace_file(input, updated)
        changed = true
      elsif mode == :check
        report_outdated_dependencies(input, current_rules, expected, err: err)
        changed = true
      else
        out.puts updated
        changed = true
      end
    end
  end
  if mode == :check && changed
    options = " --sources" if sources
    err.puts "\nupdate with:"
    err.puts "  ruby tool/mkdepend.rb --scope=all#{options} --inplace"
  end
  mode != :check || !changed
end

#same_dependency_rules?(left, right) ⇒ Boolean (private)

Returns whether two rule sets differ only by removable VPATH markers.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 809

def same_dependency_rules?(left, right)
  normalize_dependency_rules(left) == normalize_dependency_rules(right)
end

#select_make_variables(make_variables) (private)

Selects known Make variable values used during dependency generation.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 373

def select_make_variables(make_variables)
  make_variables = make_variables.to_h do |name, value|
    [name.to_s, Array(value).map(&:to_s)]
  end
  return if @make_variables == make_variables

  @make_variables = make_variables
  @dependency_declarations.clear
end

#update_deps(rules, input, group: true, verbose: false, accessed: nil) (private)

Regenerates dependency rules for the sources identified in input.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 905

def update_deps(rules, input, group: true, verbose: false, accessed: nil)
  sources = dependency_source_map(rules, input)
  targets = rules.scan(%r[^([-.\w/]+)\.(?:\$\(OBJEXT\)|o):]).flatten.uniq
  unless (missing = targets - sources.keys).empty?
    raise "no source files for #{missing.join(', ')} in #{input}"
  end
  generated = []
  sources.each do |target, src|
    src = resolve_dependency_source(src, input)
    warn "dependencies for #{src}:" if verbose
    makedepend(
      src, generated, target: target, input: input, accessed: accessed,
    )
  end
  compact_dependencies(generated.join, group: group)
end

#update_extension(input, source_map, make_variables: {}, nmake: false, verbose: false)

Updates the marked dependency section in extension file input.

source_map maps object targets to source paths. make_variables supplies values of Make variables used by manual dependency rules. Returns true when the file changed and false when its dependencies were already current.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 930

def update_extension(input, source_map, make_variables: {}, nmake: false,
                     verbose: false)
  select_make_variables(make_variables)
  input = File.expand_path(input)
  deps = File.read(input) if File.file?(input)
  match = MARK_SECTION.match(deps) if deps
  unless match
    raise "#{input}: no autogenerated dependency section"
  end
  raise "missing #{MARK_END} in #{input}" unless match.begin(1)

  dependency_declarations(input, content: deps)
  dependency_targets(input, deps)
  generated = []
  source_map.each do |target, source|
    source = relative_source(source)
    warn "dependencies for #{source}:" if verbose
    makedepend(
      source, generated, target: target, input: input, project: true
    )
  end
  expected = compact_dependencies(generated.join, group: !nmake)
  updated = match.pre_match + expected + match.post_match
  return false if same_dependency_rules?(match[0], expected)

  replace_file(input, updated)
  true
end

#write_dependency_cache(destination, signature, paths) (private)

Records scan inputs used to produce destination.

[ GitHub ]

  
# File 'lib/mkmf/depend.rb', line 1019

def write_dependency_cache(destination, signature, paths)
  output_stat = File.stat(destination)
  lines = [
    [
      "mkdepend", signature, output_stat.size,
      output_stat.mtime.to_i, output_stat.mtime.nsec,
    ].join("\t") + "\n",
  ]
  paths.sort.each do |path|
    exists = if @source_cache.key?(path)
      @source_cache[path]
    else
      File.file?(path)
    end
    lines << "#{exists ? '+' : '-'}\t#{path}\n"
  end
  replace_file(destination + ".mkdepend", lines.join)
end