Class: Licensed::Sources::Cabal
- 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
Instance Method Summary collapse
-
#cabal_file_dependencies ⇒ Object
Returns a set of the top-level dependencies found in cabal files.
-
#cabal_file_regex ⇒ Object
Find ‘build-depends` lists from specified targets in a cabal file.
-
#cabal_file_targets ⇒ Object
Returns the targets to search for ‘build-depends` in a cabal file.
-
#cabal_files ⇒ Object
Returns an array of the local directory cabal package files.
-
#cabal_package_id(package_name) ⇒ Object
Returns an installed package id for the package.
- #enabled? ⇒ Boolean
- #enumerate_dependencies ⇒ Object
-
#ghc? ⇒ Boolean
Returns whether the ghc cli tool is available.
-
#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.
-
#ghc_version ⇒ Object
Returns the ghc cli tool version.
-
#missing_package(id) ⇒ Object
Returns a package info structure with an error set.
-
#package_db_args ⇒ Object
Returns an array of ghc package DB locations as specified in the app configuration.
-
#package_dependencies(id) ⇒ Object
Returns an array of dependency package names for the cabal package given by ‘id`.
-
#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`).
-
#package_docs_dirs(package) ⇒ Object
Returns the packages document directory and search root directory as an array.
-
#package_id_name_version(id) ⇒ Object
Parses the name and version pieces from an id or package requirement string.
-
#package_info(id) ⇒ Object
Returns package information as a hash for the given id.
-
#package_info_command(id) ⇒ Object
Returns the output of running ‘ghc-pkg field` to obtain package information.
-
#packages ⇒ Object
Returns a list of all detected packages.
-
#realized_ghc_package_path(path) ⇒ Object
Returns a ghc package path with template markers replaced by live data.
-
#recursive_dependencies(package_names, results = Set.new) ⇒ Object
Recursively finds the dependencies for each cabal package.
-
#safe_homepage(homepage) ⇒ Object
Returns a homepage url that enforces https and removes url fragments.
Methods inherited from Source
#dependencies, full_type, #ignored?, inherited, #initialize, register_source, require_matched_dependency_version, #source_config, type, type_and_version
Constructor Details
This class inherits a constructor from Licensed::Sources::Source
Instance Method Details
#cabal_file_dependencies ⇒ Object
Returns a set of the top-level dependencies found in cabal files
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/licensed/sources/cabal.rb', line 160 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_regex ⇒ Object
Find ‘build-depends` lists from specified targets in a cabal file
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/licensed/sources/cabal.rb', line 190 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_targets ⇒ Object
Returns the targets to search for ‘build-depends` in a cabal file
207 208 209 210 211 |
# File 'lib/licensed/sources/cabal.rb', line 207 def cabal_file_targets targets = Array(config.dig("cabal", "cabal_file_targets")) targets.push(*DEFAULT_TARGETS) if targets.empty? targets end |
#cabal_files ⇒ Object
Returns an array of the local directory cabal package files
214 215 216 |
# File 'lib/licensed/sources/cabal.rb', line 214 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.
178 179 180 181 182 183 184 185 186 187 |
# File 'lib/licensed/sources/cabal.rb', line 178 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
10 11 12 |
# File 'lib/licensed/sources/cabal.rb', line 10 def enabled? cabal_file_dependencies.any? && ghc? end |
#enumerate_dependencies ⇒ Object
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
225 226 227 |
# File 'lib/licensed/sources/cabal.rb', line 225 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
136 137 138 |
# File 'lib/licensed/sources/cabal.rb', line 136 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_version ⇒ Object
Returns the ghc cli tool version
219 220 221 222 |
# File 'lib/licensed/sources/cabal.rb', line 219 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
230 231 232 233 |
# File 'lib/licensed/sources/cabal.rb', line 230 def missing_package(id) name, version = package_id_name_version(id) { "name" => name, "version" => version, "error" => "package not found" } end |
#package_db_args ⇒ Object
Returns an array of ghc package DB locations as specified in the app configuration
142 143 144 145 146 147 148 149 150 151 |
# File 'lib/licensed/sources/cabal.rb', line 142 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.(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`
103 104 105 |
# File 'lib/licensed/sources/cabal.rb', line 103 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`)
110 111 112 113 |
# File 'lib/licensed/sources/cabal.rb', line 110 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
236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/licensed/sources/cabal.rb', line 236 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
116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/licensed/sources/cabal.rb', line 116 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
129 130 131 132 |
# File 'lib/licensed/sources/cabal.rb', line 129 def package_info_command(id) fields = %w(name version synopsis homepage haddock-html data-dir) ghc_pkg_field_command(id, fields, "--ipid") end |
#packages ⇒ Object
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
155 156 157 |
# File 'lib/licensed/sources/cabal.rb', line 155 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
87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/licensed/sources/cabal.rb', line 87 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 78 79 80 81 82 83 |
# File 'lib/licensed/sources/cabal.rb', line 72 def safe_homepage(homepage) return unless homepage # Ensure there's no denial of service issue with a long homepage # 1000 characters is likely enough for any real project homepage # See https://github.com/github/licensed/security/code-scanning/1 if homepage.length > 1000 raise ArgumentError, "Input too long" end # use https and remove url fragment homepage.gsub(/http:/, "https:") .gsub(/#[^?]*\z/, "") end |