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, full_type, #ignored?, inherited, #initialize, register_source, type, type_and_version

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



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

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



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

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



201
202
203
204
205
# File 'lib/licensed/sources/cabal.rb', line 201

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



208
209
210
# File 'lib/licensed/sources/cabal.rb', line 208

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.



172
173
174
175
176
177
178
179
180
181
# File 'lib/licensed/sources/cabal.rb', line 172

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)


219
220
221
# File 'lib/licensed/sources/cabal.rb', line 219

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



130
131
132
# File 'lib/licensed/sources/cabal.rb', line 130

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



213
214
215
216
# File 'lib/licensed/sources/cabal.rb', line 213

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

#missing_package(id) ⇒ Object

Returns a package info structure with an error set



224
225
226
227
# File 'lib/licensed/sources/cabal.rb', line 224

def missing_package(id)
  name, version = package_id_name_version(id)
  { "name" => name, "version" => version, "error" => "package not found" }
end

#package_db_argsObject

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



136
137
138
139
140
141
142
143
144
145
# File 'lib/licensed/sources/cabal.rb', line 136

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



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

def package_docs_dirs(package)
  return [nil, nil] if package.nil? || package.empty?

  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_id_name_version(id) ⇒ Object

Parses the name and version pieces from an id or package requirement string



230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/licensed/sources/cabal.rb', line 230

def package_id_name_version(id)
  name, version = id.split(" ", 2)
  return [name, version] if version

  # split by dashes, find the rightmost thing that looks like an
  parts = id.split("-")
  version_start_index = parts.rindex { |part| part.match?(/^[\d\.]+$/) }
  return [id, nil] if version_start_index.nil?

  [
    parts[0...version_start_index].join("-"),
    parts[version_start_index..-1].join("-")
  ]
end

#package_info(id) ⇒ Object

Returns package information as a hash for the given id



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

def package_info(id)
  info = package_info_command(id).strip
  return missing_package(id) if info.empty?

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

    hsh[key] = value
  end
end

#package_info_command(id) ⇒ Object

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



123
124
125
126
# File 'lib/licensed/sources/cabal.rb', line 123

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
# File 'lib/licensed/sources/cabal.rb', line 33

def packages
  package_ids = Set.new
  cabal_file_dependencies.each do |target|
    name = target.split(/\s/)[0]
    package_id = cabal_package_id(name)
    if package_id.nil?
      package_ids << target
    else
      recursive_dependencies([package_id], package_ids)
    end
  end

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

#realized_ghc_package_path(path) ⇒ Object

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



149
150
151
# File 'lib/licensed/sources/cabal.rb', line 149

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



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

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

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

  results.merge new_packages

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

  recursive_dependencies(dependencies, results)
  results
end

#safe_homepage(homepage) ⇒ Object

Returns a homepage url that enforces https and removes url fragments



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

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