Class: Licensed::Sources::Cabal

Inherits:
Source
  • Object
show all
Defined in:
lib/licensed/sources/cabal.rb

Constant Summary collapse

DEPENDENCY_REGEX =
/\s*.+?\s*/.freeze
DEFAULT_TARGETS =
%w{executable library}.freeze

Instance Attribute Summary

Attributes inherited from Source

#config

Instance Method Summary collapse

Methods inherited from Source

#dependencies, #ignored?, inherited, #initialize, type

Constructor Details

This class inherits a constructor from Licensed::Sources::Source

Instance Method Details

#cabal_file_dependenciesObject

Returns a set of the top-level dependencies found in cabal files



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/licensed/sources/cabal.rb', line 151

def cabal_file_dependencies
  @cabal_file_dependencies ||= cabal_files.each_with_object(Set.new) do |cabal_file, targets|
    content = File.read(cabal_file)
    next if content.nil? || content.empty?

    # add any dependencies for matched targets from the cabal file.
    # by default this will find executable and library dependencies
    content.scan(cabal_file_regex).each do |match|
      # match[1] is a string of "," separated dependencies.
      # dependency packages might have a version specifier, remove them
      # to get the full id specifier for each package
      dependencies = match[1].split(",").map(&:strip)
      targets.merge(dependencies)
    end
  end
end

#cabal_file_regexObject

Find ‘build-depends` lists from specified targets in a cabal file



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/licensed/sources/cabal.rb', line 181

def cabal_file_regex
  # this will match 0 or more occurences of
  # match[0] - specifier, e.g. executable, library, etc
  # match[1] - full list of matched dependencies
  # match[2] - first matched dependency (required)
  # match[3] - remainder of matched dependencies (not required)
  @cabal_file_regex ||= /
    # match a specifier, e.g. library or executable
    ^(#{cabal_file_targets.join("|")})
      .*? # stuff

      # match a list of 1 or more dependencies
      build-depends:(#{DEPENDENCY_REGEX}(,#{DEPENDENCY_REGEX})*)\n
  /xmi
end

#cabal_file_targetsObject

Returns the targets to search for ‘build-depends` in a cabal file



198
199
200
201
202
# File 'lib/licensed/sources/cabal.rb', line 198

def cabal_file_targets
  targets = Array(config.dig("cabal", "cabal_file_targets"))
  targets.push(*DEFAULT_TARGETS) if targets.empty?
  targets
end

#cabal_filesObject

Returns an array of the local directory cabal package files



205
206
207
# File 'lib/licensed/sources/cabal.rb', line 205

def cabal_files
  @cabal_files ||= Dir.glob(File.join(config.pwd, "*.cabal"))
end

#cabal_package_id(package_name) ⇒ Object

Returns an installed package id for the package.



169
170
171
172
173
174
175
176
177
178
# File 'lib/licensed/sources/cabal.rb', line 169

def cabal_package_id(package_name)
  # using the first returned id assumes that package resolvers
  # order returned package information in the same order that it would
  # be used during build
  field = ghc_pkg_field_command(package_name, ["id"]).lines.first
  return unless field

  id = field.split(":", 2)[1]
  id.strip if id
end

#enabled?Boolean

Returns:

  • (Boolean)


10
11
12
# File 'lib/licensed/sources/cabal.rb', line 10

def enabled?
  cabal_file_dependencies.any? && ghc?
end

#enumerate_dependenciesObject



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/licensed/sources/cabal.rb', line 14

def enumerate_dependencies
  packages.map do |package|
    path, search_root = package_docs_dirs(package)
    Dependency.new(
      name: package["name"],
      version: package["version"],
      path: path,
      search_root: search_root,
      errors: Array(package["error"]),
      metadata: {
        "type"     => Cabal.type,
        "summary"  => package["synopsis"],
        "homepage" => safe_homepage(package["homepage"])
      }
    )
  end
end

#ghc?Boolean

Returns whether the ghc cli tool is available

Returns:

  • (Boolean)


216
217
218
# File 'lib/licensed/sources/cabal.rb', line 216

def ghc?
  @ghc ||= Licensed::Shell.tool_available?("ghc")
end

#ghc_pkg_field_command(id, fields, *args) ⇒ Object

Runs a ‘ghc-pkg field` command for a given set of fields and arguments Automatically includes ghc package DB locations in the command



127
128
129
# File 'lib/licensed/sources/cabal.rb', line 127

def ghc_pkg_field_command(id, fields, *args)
  Licensed::Shell.execute("ghc-pkg", "field", id, fields.join(","), *args, *package_db_args, allow_failure: true)
end

#ghc_versionObject

Returns the ghc cli tool version



210
211
212
213
# File 'lib/licensed/sources/cabal.rb', line 210

def ghc_version
  return unless ghc?
  @version ||= Licensed::Shell.execute("ghc", "--numeric-version")
end

#package_db_argsObject

Returns an array of ghc package DB locations as specified in the app configuration



133
134
135
136
137
138
139
140
141
142
# File 'lib/licensed/sources/cabal.rb', line 133

def package_db_args
  @package_db_args ||= Array(config.dig("cabal", "ghc_package_db")).map do |path|
    next "--#{path}" if %w(global user).include?(path)
    path = realized_ghc_package_path(path)
    path = File.expand_path(path, config.root)

    next unless File.exist?(path)
    "--package-db=#{path}"
  end.compact
end

#package_dependencies(id) ⇒ Object

Returns an array of dependency package names for the cabal package given by ‘id`



97
98
99
# File 'lib/licensed/sources/cabal.rb', line 97

def package_dependencies(id)
  package_dependencies_command(id).gsub("depends:", "").split.map(&:strip)
end

#package_dependencies_command(id) ⇒ Object

Returns the output of running ‘ghc-pkg field depends` for a package id Optionally allows for interpreting the given id as an installed package id (`–ipid`)



104
105
106
107
# File 'lib/licensed/sources/cabal.rb', line 104

def package_dependencies_command(id)
  fields = %w(depends)
  ghc_pkg_field_command(id, fields, "--ipid")
end

#package_docs_dirs(package) ⇒ Object

Returns the packages document directory and search root directory as an array



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/licensed/sources/cabal.rb', line 51

def package_docs_dirs(package)
  unless package["haddock-html"]
    # default to a local vendor directory if haddock-html property
    # isn't available
    return [File.join(config.pwd, "vendor", package["name"]), nil]
  end

  html_dir = package["haddock-html"]
  data_dir = package["data-dir"]
  return [html_dir, nil] unless data_dir

  # only allow data directories that are ancestors of the html directory
  unless Pathname.new(html_dir).fnmatch?(File.join(data_dir, "**"))
    data_dir = nil
  end

  [html_dir, data_dir]
end

#package_info(id) ⇒ Object

Returns package information as a hash for the given id



110
111
112
113
114
115
116
117
# File 'lib/licensed/sources/cabal.rb', line 110

def package_info(id)
  package_info_command(id).lines.each_with_object({}) do |line, info|
    key, value = line.split(":", 2).map(&:strip)
    next unless key && value

    info[key] = value
  end
end

#package_info_command(id) ⇒ Object

Returns the output of running ‘ghc-pkg field` to obtain package information



120
121
122
123
# File 'lib/licensed/sources/cabal.rb', line 120

def package_info_command(id)
  fields = %w(name version synopsis homepage haddock-html data-dir)
  ghc_pkg_field_command(id, fields, "--ipid")
end

#packagesObject

Returns a list of all detected packages



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/licensed/sources/cabal.rb', line 33

def packages
  missing = []
  package_ids = Set.new
  cabal_file_dependencies.each do |target|
    name, version = target.split(/\s/, 2)
    package_id = cabal_package_id(name)
    if package_id.nil?
      missing << { "name" => name, "version" => version, "error" => "package not found" }
    else
      recursive_dependencies([package_id], package_ids)
    end
  end

  Parallel.map(package_ids) { |id| package_info(id) }.concat(missing)
end

#realized_ghc_package_path(path) ⇒ Object

Returns a ghc package path with template markers replaced by live data



146
147
148
# File 'lib/licensed/sources/cabal.rb', line 146

def realized_ghc_package_path(path)
  path.gsub("<ghc_version>", ghc_version)
end

#recursive_dependencies(package_names, results = Set.new) ⇒ Object

Recursively finds the dependencies for each cabal package. Returns a ‘Set` containing the package names for all dependencies



80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/licensed/sources/cabal.rb', line 80

def recursive_dependencies(package_names, results = Set.new)
  return [] if package_names.nil? || package_names.empty?

  new_packages = Set.new(package_names) - results
  return [] if new_packages.empty?

  results.merge new_packages

  dependencies = Parallel.map(new_packages, &method(:package_dependencies)).flatten

  return results if dependencies.empty?

  results.merge recursive_dependencies(dependencies, results)
end

#safe_homepage(homepage) ⇒ Object

Returns a homepage url that enforces https and removes url fragments



71
72
73
74
75
76
# File 'lib/licensed/sources/cabal.rb', line 71

def safe_homepage(homepage)
  return unless homepage
  # use https and remove url fragment
  homepage.gsub(/http:/, "https:")
          .gsub(/#[^?]*\z/, "")
end