licensed 0.11.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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