licensed 0.11.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +13 -4
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +13 -0
  6. data/CODE_OF_CONDUCT.md +14 -12
  7. data/CONTRIBUTING.md +51 -0
  8. data/Gemfile +2 -1
  9. data/{LICENSE.txt → LICENSE} +1 -1
  10. data/README.md +55 -76
  11. data/Rakefile +3 -2
  12. data/docs/configuration.md +131 -0
  13. data/docs/sources/bower.md +5 -0
  14. data/docs/sources/bundler.md +7 -0
  15. data/docs/sources/cabal.md +39 -0
  16. data/docs/sources/go.md +12 -0
  17. data/docs/sources/manifests.md +26 -0
  18. data/docs/sources/npm.md +3 -0
  19. data/docs/sources/stack.md +3 -0
  20. data/exe/licensed +1 -0
  21. data/lib/licensed.rb +9 -5
  22. data/lib/licensed/cli.rb +22 -14
  23. data/lib/licensed/command/cache.rb +46 -29
  24. data/lib/licensed/command/list.rb +17 -9
  25. data/lib/licensed/command/status.rb +78 -0
  26. data/lib/licensed/configuration.rb +127 -25
  27. data/lib/licensed/dependency.rb +8 -2
  28. data/lib/licensed/git.rb +39 -0
  29. data/lib/licensed/license.rb +1 -0
  30. data/lib/licensed/shell.rb +28 -0
  31. data/lib/licensed/source/bower.rb +4 -0
  32. data/lib/licensed/source/bundler.rb +4 -0
  33. data/lib/licensed/source/cabal.rb +72 -24
  34. data/lib/licensed/source/go.rb +23 -36
  35. data/lib/licensed/source/manifest.rb +26 -23
  36. data/lib/licensed/source/npm.rb +19 -8
  37. data/lib/licensed/ui/shell.rb +2 -1
  38. data/lib/licensed/version.rb +2 -1
  39. data/licensed.gemspec +9 -5
  40. data/{bin/setup → script/bootstrap} +13 -8
  41. data/script/cibuild +7 -0
  42. data/{bin → script}/console +1 -0
  43. metadata +53 -158
  44. data/.bowerrc +0 -3
  45. data/exe/licensor +0 -5
  46. data/lib/licensed/command/verify.rb +0 -73
  47. data/lib/licensed/source/stack.rb +0 -66
@@ -0,0 +1,5 @@
1
+ # Bower
2
+
3
+ The bower source will detect dependencies when the source is enabled and either `.bowerrc` or `bower.json` files are found at an apps `source_path`.
4
+
5
+ It enumerates dependencies and metadata from parsing `.bower.json` files for bower components.
@@ -0,0 +1,7 @@
1
+ # Bundler (rubygem)
2
+
3
+ The bundler source will detect dependencies when the source is enabled, `Gemfile` and `Gemfile.lock` files are found at an apps `source_path`. The source uses the `Bundler` API to enumerate dependencies from `Gemfile` and `Gemfile.lock`.
4
+
5
+ The bundler source will exclude gems in the `:development` and `:test` groups. Be aware that if you have a local
6
+ bundler configuration (e.g. `.bundle`), that configuration will be respected as well. For example, if you have a local
7
+ configuration set for `without: [':server']`, the bundler source will exclude all gems in the `:server` group.
@@ -0,0 +1,39 @@
1
+ # Cabal
2
+
3
+ The cabal source uses the `ghc-pkg` command to enumerate dependencies and provide metadata. It is un-opinionated on GHC packagedb locations and requires some configuration to ensure that all packages are properly found.
4
+
5
+ The rubygem source will detect dependencies when the source is enabled and a `.cabal` file is found at an apps `source_path`.
6
+
7
+ ### Specifying GHC packagedb locations through environment
8
+ You can configure the `cabal` source to use specific packagedb locations by setting the `GHC_PACKAGE_PATH` environment variable before running `licensed`.
9
+
10
+ For example, the following is a useful configuration for use with `cabal new-build`.
11
+ ```bash
12
+ ghc_version="$(ghc --numeric-version)"
13
+ CABAL_STORE_PACKAGE_DB="$(cd ~/.cabal/store/ghc-$ghc_version/package.db && pwd)"
14
+ LOCAL_PACKAGE_DB="$ROOT/dist-newstyle/packagedb/ghc-$ghc_version"
15
+ GLOBAL_PACKAGE_DB="$(ghc-pkg list --global | head -n 1)"
16
+ export GHC_PACKAGE_PATH="$LOCAL_PACKAGE_DB:$CABAL_STORE_PACKAGE_DB:$GLOBAL_PACKAGE_DB:$GHC_PACKAGE_PATH"
17
+
18
+ bundle exec licensed <args>
19
+ ```
20
+
21
+ ### Specifying GHC packagedb locations through configuration
22
+ Alternatively, the `cabal` source can use packagedb locations set in the app configuration. The following is an example configuration identical to the above environment configuration.
23
+
24
+ ```yml
25
+ cabal:
26
+ ghc_package_db:
27
+ - ~/.cabal/store/ghc-<ghc_version>/package.db
28
+ - dist-newstyle/packagedb/ghc-<ghc_version>
29
+ - global
30
+ ```
31
+
32
+ Ordering is preserved when evaluating the configuration values. Paths are expanded from the root of the `git` repository and used as values for CLI `--package-db="<path>"` arguments. Additionally, in all specified paths, `<ghc_version>` will be replaced with the result of `ghc --numeric-version`.
33
+
34
+ The `global` and `user` keywords are supported and will expand to the `--global` and `--user` CLI arguments, respectively.
35
+
36
+ Like most other settings, the `cabal` configuration can be specified for the root configuration, or per-app. If specified at root, it will be inherited by all apps unless explicitly overridden.
37
+
38
+ ### Stack sandboxes
39
+ The current recommended way to use `licensed` with stack is to run the `licensed` command with `stack exec`: `stack exec -- bundle exec licensed <args>`. This will allow stack to control the GHC packagedb locations.
@@ -0,0 +1,12 @@
1
+ # Go
2
+
3
+ The go source uses `go` CLI commands to enumerate dependencies and properties. It is expected that `go` projects have been built, and that `GO_PATH` and `GO_ROOT` are properly set before running `licensed`.
4
+
5
+ Source paths for go projects should point to a location that contains an entrypoint to the executable or library.
6
+
7
+ An example usage might see a configuration like:
8
+ ```YAML
9
+ source_path: go/path/src/github.com/BurntSushi/toml/cmd/tomlv
10
+ ```
11
+
12
+ Note that this configuration points directly to the tomlv command source, which contains `func main`.
@@ -0,0 +1,26 @@
1
+ # Manifests
2
+
3
+ The manifest source can be used when no package managers are available.
4
+
5
+ Manifest files are used to match source files with their corresponding packages to find package dependencies. Manifest file paths can be specified in the app configuration with the following setting:
6
+ ```yml
7
+ manifest:
8
+ path: 'path/to/manifest.json'
9
+ ```
10
+
11
+ If a manifest path is not specified for an app, the file will be looked for at the apps `<cache_path>/manifest.json`.
12
+
13
+ The manifest can be a JSON or YAML file with a single root object and properties mapping file paths to package names.
14
+ ```JSON
15
+ {
16
+ "file1": "package1",
17
+ "path/to/file2": "package1",
18
+ "other/file3": "package2"
19
+ }
20
+ ```
21
+
22
+ File paths are relative to the git repository root. Package names will be used for the metadata file names at `path/to/cache/manifest/<package>.txt`
23
+
24
+ If multiple source files map to a single package and they share a common path under the git repository root, that directory will be used to find license information, if available.
25
+
26
+ It is the responsibility of the repository owner to maintain the manifest file.
@@ -0,0 +1,3 @@
1
+ # NPM
2
+
3
+ The npm source will detect dependencies when the source is enabled and `package.json` is found at an apps `source_path`. It uses `npm list` to enumerate dependencies and metadata.
@@ -0,0 +1,3 @@
1
+ # HaskellStack
2
+
3
+ It is not recommended to use this source. Please see [cabal documentation](./cabal.md) for using the cabal source with stack.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "licensed/cli"
4
5
  Licensed::CLI.start
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
1
2
  require "licensed/version"
3
+ require "licensed/shell"
2
4
  require "licensed/configuration"
3
5
  require "licensed/license"
4
6
  require "licensed/dependency"
7
+ require "licensed/git"
5
8
  require "licensed/source/bundler"
6
9
  require "licensed/source/bower"
7
10
  require "licensed/source/manifest"
8
11
  require "licensed/source/npm"
9
- require "licensed/source/stack"
10
12
  require "licensed/source/go"
11
13
  require "licensed/source/cabal"
12
14
  require "licensed/command/cache"
13
- require "licensed/command/verify"
15
+ require "licensed/command/status"
14
16
  require "licensed/command/list"
15
17
  require "licensed/ui/shell"
16
18
  require "octokit"
@@ -25,18 +27,20 @@ module Licensed
25
27
  GITHUB_URL = %r{\Ahttps://github.com/([a-z0-9]+(-[a-z0-9]+)*/(\w|\.|\-)+)}
26
28
  LICENSE_CONTENT_TYPE = "application/vnd.github.drax-preview+json"
27
29
 
30
+ # Load license content from a GitHub url. Returns nil if the url does not point
31
+ # to a GitHub repository, or if the license content is not found
28
32
  def self.from_github(url)
29
33
  return unless use_github && match = GITHUB_URL.match(url)
30
34
 
31
35
  license_url = Octokit::Repository.path(match[1]) + "/license"
32
- response = octokit.get license_url, :accept => LICENSE_CONTENT_TYPE
33
- content = Base64.decode64(response["content"]).force_encoding('UTF-8')
36
+ response = octokit.get license_url, accept: LICENSE_CONTENT_TYPE
37
+ content = Base64.decode64(response["content"]).force_encoding("UTF-8")
34
38
  Licensee::ProjectFiles::LicenseFile.new(content, response["name"])
35
39
  rescue Octokit::NotFound
36
40
  nil
37
41
  end
38
42
 
39
43
  def self.octokit
40
- @octokit ||= Octokit::Client.new(:access_token => ENV["GITHUB_TOKEN"])
44
+ @octokit ||= Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
41
45
  end
42
46
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "licensed"
2
3
  require "thor"
3
4
 
@@ -5,35 +6,42 @@ module Licensed
5
6
  class CLI < Thor
6
7
 
7
8
  desc "cache", "Cache the licenses of dependencies"
8
- method_option :force, :type => :boolean,
9
- :desc => "Overwrite licenses even if version has not changed."
10
- method_option :offline, :type => :boolean,
11
- :desc => "Do not make network calls."
12
- method_option "license-dir", :type => :string,
13
- :desc => "Path to license storage directory (defaults to vendor/licenses)"
9
+ method_option :force, type: :boolean,
10
+ desc: "Overwrite licenses even if version has not changed."
11
+ method_option :offline, type: :boolean,
12
+ desc: "Do not make network calls."
13
+ method_option :config, aliases: "-c", type: :string,
14
+ desc: "Path to licensed configuration file"
14
15
  def cache
15
16
  Licensed.use_github = false if options[:offline]
16
17
  run Licensed::Command::Cache.new(config), force: options[:force]
17
18
  end
18
19
 
19
- desc "verify", "Verify licenses of dependencies"
20
- method_option "license-dir", :type => :string,
21
- :desc => "Path to license storage directory (defaults to vendor/licenses)"
22
- def verify
23
- run Licensed::Command::Verify.new(config)
20
+ desc "status", "Check status of dependencies' cached licenses"
21
+ method_option :config, aliases: "-c", type: :string,
22
+ desc: "Path to licensed configuration file"
23
+ def status
24
+ run Licensed::Command::Status.new(config)
24
25
  end
25
26
 
26
27
  desc "list", "List dependencies"
27
- method_option "license-dir", :type => :string,
28
- :desc => "Path to license storage directory (defaults to vendor/licenses)"
28
+ method_option :config, aliases: "-c", type: :string,
29
+ desc: "Path to licensed configuration file"
29
30
  def list
30
31
  run Licensed::Command::List.new(config)
31
32
  end
32
33
 
33
34
  private
34
35
 
36
+ # Returns a configuration object for the CLI command
35
37
  def config
36
- @config ||= Configuration.new(options)
38
+ @config ||= Configuration.load_from(config_path)
39
+ end
40
+
41
+ # Returns a config path from the CLI if set.
42
+ # Defaults to the current directory if CLI option is not set
43
+ def config_path
44
+ options["config"] || Dir.pwd
37
45
  end
38
46
 
39
47
  def run(command, *args)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Licensed
2
3
  module Command
3
4
  class Cache
@@ -8,49 +9,65 @@ module Licensed
8
9
  end
9
10
 
10
11
  def run(force: false)
11
- @config.sources.each do |source|
12
- @config.ui.info "Caching licenses for #{source.type} dependencies:"
12
+ summary = @config.apps.flat_map do |app|
13
+ app_name = app["name"]
14
+ @config.ui.info "Caching licenes for #{app_name}:"
13
15
 
14
- dependencies(source).each do |dependency|
15
- filename = @config.path.join("#{source.type}/#{dependency["name"]}.txt")
16
+ # load the app environment
17
+ Dir.chdir app.source_path do
16
18
 
17
- if File.exist?(filename)
18
- license = Licensed::License.read(filename)
19
+ # map each available app source to it's dependencies
20
+ app.sources.map do |source|
21
+ @config.ui.info " #{source.type} dependencies:"
19
22
 
20
- # Version did not change, no need to re-cache
21
- if !force && dependency["version"] == license["version"]
22
- @config.ui.info " Using #{dependency["name"]} (#{dependency["version"]})"
23
- next
24
- end
25
- end
23
+ names = []
24
+ cache_path = app.cache_path.join(source.type)
26
25
 
27
- @config.ui.info " Caching #{dependency["name"]} (#{dependency["version"]})"
26
+ # exclude ignored dependencies
27
+ dependencies = source.dependencies.select { |d| !app.ignored?(d) }
28
28
 
29
- dependency.detect_license!
30
- dependency.save(filename)
31
- end
29
+ # ensure each dependency is cached
30
+ dependencies.each do |dependency|
31
+ name = dependency["name"]
32
+ version = dependency["version"]
33
+
34
+ names << name
35
+ filename = cache_path.join("#{name}.txt")
32
36
 
33
- # Clean up dependencies that have been removed
34
- names = dependencies(source).map { |d| d["name"] }
37
+ if File.exist?(filename)
38
+ license = Licensed::License.read(filename)
35
39
 
36
- license_source_path = @config.path.join(source.type)
37
- Dir.glob(license_source_path.join("**/*.txt")).each do |file|
38
- file_path = Pathname.new(file)
39
- relative_path = file_path.relative_path_from(license_source_path).to_s
40
- FileUtils.rm(file) unless names.include?(relative_path.chomp(".txt"))
40
+ # Version did not change, no need to re-cache
41
+ if !force && version == license["version"]
42
+ @config.ui.info " Using #{name} (#{version})"
43
+ next
44
+ end
45
+ end
46
+
47
+ @config.ui.info " Caching #{name} (#{version})"
48
+
49
+ dependency.detect_license!
50
+ dependency.save(filename)
51
+ end
52
+
53
+ # Clean up cached files that dont match current dependencies
54
+ Dir.glob(cache_path.join("**/*.txt")).each do |file|
55
+ file_path = Pathname.new(file)
56
+ relative_path = file_path.relative_path_from(cache_path).to_s
57
+ FileUtils.rm(file) unless names.include?(relative_path.chomp(".txt"))
58
+ end
59
+
60
+ "* #{app_name} #{source.type} dependencies: #{dependencies.size}"
61
+ end
41
62
  end
42
63
  end
43
64
 
44
65
  @config.ui.confirm "License caching complete!"
45
- @config.sources.each do |source|
46
- @config.ui.confirm "* #{source.type} dependencies: #{dependencies(source).size}"
66
+ summary.each do |message|
67
+ @config.ui.confirm message
47
68
  end
48
69
  end
49
70
 
50
- def dependencies(source)
51
- source.dependencies.select { |d| !@config.ignored?(d) }
52
- end
53
-
54
71
  def success?
55
72
  true
56
73
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Licensed
2
3
  module Command
3
4
  class List
@@ -8,21 +9,28 @@ module Licensed
8
9
  end
9
10
 
10
11
  def run
11
- @config.sources.each do |source|
12
- @config.ui.info "Displaying licenses for #{source.type} dependencies:"
12
+ @config.apps.each do |app|
13
+ @config.ui.info "Displaying dependencies for #{app['name']}"
14
+ Dir.chdir app.source_path do
15
+ app.sources.each do |source|
16
+ @config.ui.info " #{source.type} dependencies:"
13
17
 
14
- dependencies(source).each do |dependency|
15
- @config.ui.info " Found #{dependency['name']} (#{dependency['version']})"
16
- end
18
+ source_dependencies = dependencies(app, source)
19
+ source_dependencies.each do |dependency|
20
+ @config.ui.info " Found #{dependency['name']} (#{dependency['version']})"
21
+ end
17
22
 
18
- @config.ui.confirm "* #{source.type} dependencies: #{dependencies(source).size}"
23
+ @config.ui.confirm " * #{source.type} dependencies: #{source_dependencies.size}"
24
+ end
25
+ end
19
26
  end
20
27
  end
21
28
 
22
- def dependencies(source)
29
+ # Returns an apps non-ignored dependencies, sorted by name
30
+ def dependencies(app, source)
23
31
  source.dependencies
24
- .select { |d| !@config.ignored?(d) }
25
- .sort_by { |d| d['name'] }
32
+ .select { |d| !app.ignored?(d) }
33
+ .sort_by { |d| d["name"] }
26
34
  end
27
35
 
28
36
  def success?
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require "yaml"
3
+
4
+ module Licensed
5
+ module Command
6
+ class Status
7
+ attr_reader :config
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def allowed_or_reviewed?(app, dependency)
14
+ app.allowed?(dependency) || app.reviewed?(dependency)
15
+ end
16
+
17
+ def app_dependencies(app)
18
+ app.sources.flat_map(&:dependencies).select { |d| !app.ignored?(d) }
19
+ end
20
+
21
+ def run
22
+ @results = @config.apps.flat_map do |app|
23
+ Dir.chdir app.source_path do
24
+ dependencies = app_dependencies(app)
25
+ @config.ui.info "Checking licenses for #{app['name']}: #{dependencies.size} dependencies"
26
+
27
+ results = dependencies.map do |dependency|
28
+ filename = app.cache_path.join(dependency["type"], "#{dependency["name"]}.txt")
29
+
30
+ warnings = []
31
+
32
+ # verify cached license data for dependency
33
+ if File.exist?(filename)
34
+ license = License.read(filename)
35
+
36
+ if license["version"] != dependency["version"]
37
+ warnings << "cached license data out of date"
38
+ end
39
+ warnings << "missing license text" if license.text.strip.empty?
40
+ unless allowed_or_reviewed?(app, license)
41
+ warnings << "license needs reviewed: #{license["license"]}."
42
+ end
43
+ else
44
+ warnings << "cached license data missing"
45
+ end
46
+
47
+ if warnings.size > 0
48
+ @config.ui.error("F", false)
49
+ [filename, warnings]
50
+ else
51
+ @config.ui.confirm(".", false)
52
+ nil
53
+ end
54
+ end.compact
55
+
56
+ unless results.empty?
57
+ @config.ui.warn "\n\nWarnings:"
58
+
59
+ results.each do |filename, warnings|
60
+ @config.ui.info "\n#{filename}:"
61
+ warnings.each do |warning|
62
+ @config.ui.error " - #{warning}"
63
+ end
64
+ end
65
+ end
66
+
67
+ puts "\n#{dependencies.size} dependencies checked, #{results.size} warnings found."
68
+ results
69
+ end
70
+ end
71
+ end
72
+
73
+ def success?
74
+ @results.empty?
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,38 +1,50 @@
1
+ # frozen_string_literal: true
1
2
  require "pathname"
2
3
 
3
4
  module Licensed
4
- class Configuration < Hash
5
- attr_accessor :ui
6
-
7
- def initialize(options = {})
5
+ class AppConfiguration < Hash
6
+ DEFAULT_CACHE_PATH = ".licenses".freeze
7
+ DEFAULT_CONFIG_FILES = [
8
+ ".licensed.yml".freeze,
9
+ ".licensed.yaml".freeze,
10
+ ".licensed.json".freeze
11
+ ].freeze
12
+
13
+ def initialize(options = {}, inherited_options = {})
8
14
  super()
9
- self.path = options["license-dir"] if options["license-dir"]
10
- update config_path.exist? ? YAML.load_file(config_path) : {}
15
+
16
+ # update order:
17
+ # 1. anything inherited from root config
18
+ # 2. app defaults
19
+ # 3. explicitly configured app settings
20
+ update(inherited_options)
21
+ update(defaults_for(options, inherited_options))
22
+ update(options)
11
23
 
12
24
  self["sources"] ||= {}
13
25
  self["reviewed"] ||= {}
14
26
  self["ignored"] ||= {}
15
- self["whitelist"] ||= []
27
+ self["allowed"] ||= []
16
28
 
17
- @ui = Licensed::UI::Shell.new
29
+ verify_arg "source_path"
30
+ verify_arg "cache_path"
18
31
  end
19
32
 
20
- def path
21
- @path ||= Pathname.new("vendor/licenses")
33
+ # Returns the path to the app cache directory as a Pathname
34
+ def cache_path
35
+ Licensed::Git.repository_root.join(self["cache_path"])
22
36
  end
23
37
 
24
- def path=(value)
25
- @path = Pathname.new(value)
26
- end
27
-
28
- def config_path
29
- path.join("config.yml")
38
+ # Returns the path to the app source directory as a Pathname
39
+ def source_path
40
+ Licensed::Git.repository_root.join(self["source_path"])
30
41
  end
31
42
 
32
43
  def pwd
33
- Pathname.new(Dir.pwd)
44
+ Pathname.pwd
34
45
  end
35
46
 
47
+ # Returns an array of enabled app sources
36
48
  def sources
37
49
  @sources ||= [
38
50
  Source::Bundler.new(self),
@@ -40,16 +52,16 @@ module Licensed
40
52
  Source::Cabal.new(self),
41
53
  Source::Go.new(self),
42
54
  Source::Manifest.new(self),
43
- Source::NPM.new(self),
44
- Source::Stack.new(self)
55
+ Source::NPM.new(self)
45
56
  ].select(&:enabled?)
46
57
  end
47
58
 
59
+ # Returns whether a source type is enabled
48
60
  def enabled?(source_type)
49
61
  self["sources"].fetch(source_type, true)
50
62
  end
51
63
 
52
- # Is the given dependency approved?
64
+ # Is the given dependency reviewed?
53
65
  def reviewed?(dependency)
54
66
  Array(self["reviewed"][dependency["type"]]).include?(dependency["name"])
55
67
  end
@@ -59,21 +71,111 @@ module Licensed
59
71
  Array(self["ignored"][dependency["type"]]).include?(dependency["name"])
60
72
  end
61
73
 
62
- # Is the license of the dependency whitelisted?
63
- def whitelisted?(dependency)
64
- Array(self["whitelist"]).include?(dependency["license"])
74
+ # Is the license of the dependency allowed?
75
+ def allowed?(dependency)
76
+ Array(self["allowed"]).include?(dependency["license"])
65
77
  end
66
78
 
79
+ # Ignore a dependency
67
80
  def ignore(dependency)
68
81
  (self["ignored"][dependency["type"]] ||= []) << dependency["name"]
69
82
  end
70
83
 
84
+ # Set a dependency as reviewed
71
85
  def review(dependency)
72
86
  (self["reviewed"][dependency["type"]] ||= []) << dependency["name"]
73
87
  end
74
88
 
75
- def whitelist(license)
76
- self["whitelist"] << license
89
+ # Set a license as explicitly allowed
90
+ def allow(license)
91
+ self["allowed"] << license
92
+ end
93
+
94
+ def defaults_for(options, inherited_options)
95
+ name = options["name"] || File.basename(options["source_path"])
96
+ cache_path = inherited_options["cache_path"] || DEFAULT_CACHE_PATH
97
+ {
98
+ "name" => name,
99
+ "cache_path" => File.join(cache_path, name)
100
+ }
101
+ end
102
+
103
+ def verify_arg(property)
104
+ return if self[property]
105
+ raise Licensed::Configuration::LoadError,
106
+ "App #{self["name"]} is missing required property #{property}"
107
+ end
108
+ end
109
+
110
+ class Configuration < AppConfiguration
111
+ class LoadError < StandardError; end
112
+
113
+ attr_accessor :ui
114
+
115
+ # Loads and returns a Licensed::Configuration object from the given path.
116
+ # The path can be relative or absolute, and can point at a file or directory.
117
+ # If the path given is a directory, the directory will be searched for a
118
+ # `config.yml` file.
119
+ def self.load_from(path)
120
+ config_path = Pathname.pwd.join(path)
121
+ config_path = find_config(config_path) if config_path.directory?
122
+ Configuration.new(parse_config(config_path))
123
+ end
124
+
125
+ def initialize(options = {})
126
+ @ui = Licensed::UI::Shell.new
127
+
128
+ apps = options.delete("apps") || []
129
+ super(default_options.merge(options))
130
+
131
+ self["apps"] = apps.map { |app| AppConfiguration.new(app, options) }
132
+ end
133
+
134
+ # Returns an array of the applications for this licensed configuration.
135
+ # If the configuration did not explicitly configure any applications,
136
+ # return self as an application configuration.
137
+ def apps
138
+ return [self] if self["apps"].empty?
139
+ self["apps"]
140
+ end
141
+
142
+ private
143
+
144
+ # Find a default configuration file in the given directory.
145
+ # File preference is given by the order of elements in DEFAULT_CONFIG_FILES
146
+ #
147
+ # Raises Licensed::Configuration::LoadError if a file isn't found
148
+ def self.find_config(directory)
149
+ config_file = DEFAULT_CONFIG_FILES.map { |file| directory.join(file) }
150
+ .find { |file| file.exist? }
151
+
152
+ config_file || raise(LoadError, "Licensed configuration not found in #{directory}")
153
+ end
154
+
155
+ # Parses the configuration given at `config_path` and returns the values
156
+ # as a Hash
157
+ #
158
+ # Raises Licensed::Configuration::LoadError if the file type isn't known
159
+ def self.parse_config(config_path)
160
+ return {} unless config_path.file?
161
+
162
+ extension = config_path.extname.downcase.delete "."
163
+ case extension
164
+ when "json"
165
+ JSON.parse(File.read(config_path))
166
+ when "yml", "yaml"
167
+ YAML.load_file(config_path)
168
+ else
169
+ raise LoadError, "Unknown file type #{extension} for #{config_path}"
170
+ end
171
+ end
172
+
173
+ def default_options
174
+ # manually set a cache path without additional name
175
+ {
176
+ "source_path" => Dir.pwd,
177
+ "cache_path" => DEFAULT_CACHE_PATH
178
+ }
77
179
  end
78
180
  end
79
181
  end