licensed 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e7af8fc6ca23f14500bc88d7766153dbc21ef30
4
- data.tar.gz: 50c1b4ffdf8ef72d4b7bfd20e3b0aff031e7e843
3
+ metadata.gz: 3baec95cfc92e05345d639e02f55185da93a50a2
4
+ data.tar.gz: 7b970c9ea7bfe7c0724fde3d75ea058011655554
5
5
  SHA512:
6
- metadata.gz: 587c4b056a4741e0d25b41551612f287140636bef7a9ddb76dd1231a8e2316a1e50ea2438b599216f6f63c6fc2268b32761df9e098d04587c8848b8e68cbc0cb
7
- data.tar.gz: ede02755d459ecf6a42f4c7b9c9a0339b0e9294d9db94c271d3b00984de3825c504a43fa8c8792ac3d355356f90119d8757077237078bb3ee2d973654a4c27af
6
+ metadata.gz: bab9dbb1f74d2658cf93f689b4f828a40666836c444b35ca4999a8265cd5f471a60fc0b93a53008130148f8d7ee6f69bc8701a31961eba99d967b28566aae793
7
+ data.tar.gz: 74920ce7597d046570c5f45e9bd4c341ddebdf0958a8059eb1f43015209a1d69d54a498e2d0ec4ee838fb074e27bad2f1c9ba181b18dda2c8f9992265a92dc67
data/.gitignore CHANGED
@@ -7,7 +7,6 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
- .byebug_history
11
10
 
12
11
  # test fixtures
13
12
  test/fixtures/bundler/.bundle/
@@ -19,11 +18,13 @@ test/fixtures/npm/package-lock.json
19
18
  test/fixtures/go/src/*
20
19
  test/fixtures/go/pkg
21
20
  !test/fixtures/go/src/test
22
- test/fixtures/haskell/dist*
21
+ test/fixtures/cabal/*
22
+ !test/fixtures/cabal/app*
23
23
 
24
24
  vendor/licenses
25
25
  .licenses
26
26
  *.gem
27
27
  vendor/gems
28
+ .byebug_history
28
29
 
29
30
  bin/
data/.travis.yml CHANGED
@@ -8,13 +8,27 @@ matrix:
8
8
  script: bundle exec rake rubocop build
9
9
  env: NAME="Lint and Build"
10
10
 
11
- # go tests
11
+ # go 1.7 tests
12
12
  - language: go
13
- go: 1.7.x
13
+ go: "1.7.x"
14
14
  before_script: ./script/source-setup/go
15
15
  script: ./script/test go
16
16
  env: NAME="go"
17
17
 
18
+ # go 1.10 tests
19
+ - language: go
20
+ go: "1.10.x"
21
+ before_script: ./script/source-setup/go
22
+ script: ./script/test go
23
+ env: NAME="go"
24
+
25
+ # dep tests
26
+ - language: go
27
+ go: "1.10.x"
28
+ before_script: ./script/source-setup/go
29
+ script: ./script/test dep
30
+ env: NAME="go dep"
31
+
18
32
  # cabal tests
19
33
  - language: haskell
20
34
  ghc: "8.2"
@@ -51,5 +65,21 @@ matrix:
51
65
  script: ./script/test manifest
52
66
  env: NAME="manifest"
53
67
 
68
+ # python 2.7 tests
69
+ - language: python
70
+ python:
71
+ - "2.7"
72
+ before_script: ./script/source-setup/pip
73
+ script: ./script/test pip
74
+ env: NAME="pip"
75
+
76
+ # python 3.6 tests
77
+ - language: python
78
+ python:
79
+ - "3.6"
80
+ before_script: ./script/source-setup/pip
81
+ script: ./script/test pip
82
+ env: NAME="pip"
83
+
54
84
  notifications:
55
85
  disable: true
data/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 1.1.0 - 2018-06-04
10
+ ### Added
11
+ - Pip dependency source :tada:
12
+ - Go Dep dependency source :tada:
13
+
14
+ ### Changed
15
+ - Changed how `sources` configuration property affects which sources are enabled
16
+ - Raise informative error messages when shell commands fail
17
+
18
+ ### Fixed
19
+ - Don't reuse cached license when cached version metadata is missing
20
+ - Disable dependency sources when dependent tools are not available
21
+ - Vendored packages from the go std library are properly excluded
22
+ - Cabal dependency enumeration properly includes executable targets
23
+
9
24
  ## 1.0.1 - 2018-04-26
10
25
  ### Added
11
26
  - GOPATH settable in configuration file
data/README.md CHANGED
@@ -77,8 +77,10 @@ Dependencies will be automatically detected for
77
77
  2. [Bundler (rubygem)](./docs/sources/bundler.md)
78
78
  3. [Cabal](./docs/sources/cabal.md)
79
79
  4. [Go](./docs/sources/go.md)
80
- 5. [Manifest lists](./docs/sources/manifests.md)
81
- 6. [NPM](./docs/sources/npm.md)
80
+ 5. [Go Dep](./docs/sources/dep.md)
81
+ 6. [Manifest lists](./docs/sources/manifests.md)
82
+ 7. [NPM](./docs/sources/npm.md)
83
+ 8. [Pip](./docs/source/pip.md)
82
84
 
83
85
  You can disable any of them in the configuration file:
84
86
 
@@ -92,7 +94,13 @@ sources:
92
94
 
93
95
  ## Development
94
96
 
95
- After checking out the repo, run `script/bootstrap` to install dependencies. Then, run `script/cibuild` to run the tests. You can also run `script/console` for an interactive prompt that will allow you to experiment.
97
+ To get started after checking out the repo, run
98
+ 1. `script/bootstrap` to install dependencies
99
+ 2. `script/setup` to setup test fixtures.
100
+ - `script/setup -f` will force a clean test fixture environment
101
+ 3. `script/cibuild` to run the tests.
102
+
103
+ You can also run `script/console` for an interactive prompt that will allow you to experiment.
96
104
 
97
105
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
98
106
 
data/Rakefile CHANGED
@@ -4,12 +4,15 @@ require "rake/testtask"
4
4
  require "rubocop/rake_task"
5
5
 
6
6
  desc "Run source setup scripts"
7
- task :setup do
7
+ task :setup, [:arguments] do |task, args|
8
+ arguments = args[:arguments].to_s.split
9
+ force = arguments.include?("-f") ? "-f" : ""
10
+
8
11
  Dir["script/source-setup/*"].each do |script|
9
12
  # green
10
13
  puts "\033[32mRunning #{script}.\e[0m"
11
14
 
12
- if system(script)
15
+ if Bundler.with_clean_env { system(script, force) }
13
16
  # green
14
17
  puts "\033[32mCompleted #{script}.\e[0m"
15
18
  elsif $?.exitstatus == 127
@@ -4,6 +4,37 @@ A configuration file specifies the details of enumerating and operating on licen
4
4
 
5
5
  Configuration can be specified in either YML or JSON formats. Examples below are given in YML.
6
6
 
7
+ ## Restricting sources
8
+
9
+ The `sources` configuration property specifies which sources `licensed` will use to enumerate dependencies.
10
+ By default, `licensed` will generally try to enumerate dependencies from all sources. As a result,
11
+ the configuration property should be used to explicitly disable sources rather than to enable a particular source.
12
+
13
+ Be aware that this configuration is separate from an individual sources `#enabled?` method, which determines
14
+ whether the source is valid for the current project. Even if a source is enabled in the configuration
15
+ it may still determine that it can't enumerate dependencies for a project.
16
+
17
+ ```yml
18
+ sources:
19
+ bower: true
20
+ rubygem: false
21
+ ```
22
+
23
+ `licensed` determines which sources will try to enumerate dependencies based on the following rules:
24
+ 1. If no sources are configured, all sources are enabled
25
+ 2. If no sources are set to true, any unconfigured sources are enabled
26
+ ```yml
27
+ sources:
28
+ bower: false
29
+ # all other sources are enabled by default since there are no sources set to true
30
+ ```
31
+ 3. If any sources are set to true, any unconfigured sources are disabled
32
+ ```yml
33
+ sources:
34
+ bower: true
35
+ # all other sources are disabled by default because a source was set to true
36
+ ```
37
+
7
38
  ## Applications
8
39
 
9
40
  What is an "app"? In the context of `licensed`, an app is a combination of a source path and a cache path.
@@ -23,7 +54,6 @@ cache_path: 'relative/path/to/cache'
23
54
  source_path: 'relative/path/to/source'
24
55
 
25
56
  # Sources of metadata
26
- # All sources will attempt to run unless explicitly disabled
27
57
  sources:
28
58
  bower: true
29
59
  rubygem: false
@@ -1,5 +1,5 @@
1
1
  # Bower
2
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`.
3
+ The bower source will detect dependencies when either `.bowerrc` or `bower.json` files are found at an apps `source_path`.
4
4
 
5
5
  It enumerates dependencies and metadata from parsing `.bower.json` files for bower components.
@@ -1,6 +1,6 @@
1
1
  # Bundler (rubygem)
2
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`.
3
+ The bundler source will detect dependencies `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
4
 
5
5
  The bundler source will exclude gems in the `:development` and `:test` groups. Be aware that if you have a local
6
6
  bundler configuration (e.g. `.bundle`), that configuration will be respected as well. For example, if you have a local
@@ -2,7 +2,25 @@
2
2
 
3
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
4
 
5
- The rubygem source will detect dependencies when the source is enabled and a `.cabal` file is found at an apps `source_path`.
5
+ The cabal source will detect dependencies when a `.cabal` file is found at an apps `source_path`. By default, the cabal source will enumerate dependencies for all executable and library targets in a cabal file.
6
+
7
+ ### Specifying which cabal file targets should enumerate dependencies
8
+ The cabal source can be configured to override which cabal file targets contain dependencies that need to be documented.
9
+
10
+ The default configuration is equivalent to:
11
+ ```yml
12
+ cabal:
13
+ cabal_file_targets:
14
+ - executable
15
+ - library
16
+ ```
17
+
18
+ However if you only wanted to enumerate dependencies for a `my_cabal_exe` executable target, you could specify:
19
+ ```yml
20
+ cabal:
21
+ cabal_file_targets:
22
+ - executable my_cabal_exe
23
+ ```
6
24
 
7
25
  ### Specifying GHC packagedb locations through environment
8
26
  You can configure the `cabal` source to use specific packagedb locations by setting the `GHC_PACKAGE_PATH` environment variable before running `licensed`.
@@ -0,0 +1,33 @@
1
+ # Warning!
2
+ This source is intended to be used when all of a projects dependencies have been vendored and does not detect non-vendored packages installed at `$GOPATH/pkg`. If your project uses dependencies that are not listed in `Gopkg.lock`, then you must use the go source to enumerate all project dependencies.
3
+
4
+ # Go Dep
5
+
6
+ The dep source will detect dependencies when the source is enabled and both `Gopkg.toml` and `Gopkg.lock` are found at an apps `source_path`. It
7
+ parses the `Gopkg.lock` file to find packages that have been vendored into the project directory.
8
+
9
+ This source will self-disable if the `ignored` property in `Gopkg.toml` has any values. While strongly discouraged, the source can be forced to run
10
+ via configuration.
11
+
12
+ ```yml
13
+ dep:
14
+ allow_ignored: true # force source to run even if `Gopkg.toml` is non-empty
15
+ ```
16
+
17
+ #### Limitations
18
+
19
+ The dep dependency source has some limitations compared to the general-purpose go source.
20
+ 1. Go std libraries are not filtered from enumerated dependencies if `go list std` is not available
21
+ 2. Summary information is not available for packages
22
+
23
+ #### Go or Dep
24
+
25
+ Reasons to choose the dep source over the go source
26
+ 1. The dep source does not have a hard dependency on go being installed
27
+ - some functionality is only available if go is available
28
+ 1. filtering go std libs from the found dependencies
29
+ 2. The dep source should generally run much more quickly then the go source
30
+
31
+ Reasons to choose the go source over the dep source
32
+ 1. Your project has dependencies not specified by `Gopkpg.lock`
33
+ 2. You require dependency summary information
data/docs/sources/npm.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # NPM
2
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.
3
+ The npm source will detect dependencies `package.json` is found at an apps `source_path`. It uses `npm list` to enumerate dependencies and metadata.
@@ -0,0 +1,23 @@
1
+ # Pip
2
+
3
+ The pip source uses `pip` CLI commands to enumerate dependencies and properties. It is expected that `pip` is available in the `virtual_env_dir` specific directory before running `licensed`.
4
+
5
+ Your repository root should also contain a `requirements.txt` file which contains all the packages and dependences that are needed. You can generate one with `pip` using the command:
6
+ ```
7
+ pip freeze > requirements.txt
8
+ ```
9
+
10
+ A `virtualenv` directory is required before running `licensed`. You can setup a `virtualenv` by running the command:
11
+ ```
12
+ virtualenv <your_venv_dir>
13
+ ```
14
+ _note_: `<your_venv_dir>` path should be relative to the repository root or can be specified as an absolute path.
15
+
16
+ #### virtual_env_dir (Required)
17
+
18
+ The `pip` command will be sourced from this directory.
19
+ An example usage of this might look like:
20
+ ```yaml
21
+ python:
22
+ virtual_env_dir:"/path/to/your/venv_dir"
23
+ ```
@@ -1,3 +1,3 @@
1
1
  # HaskellStack
2
2
 
3
- It is not recommended to use this source. Please see [cabal documentation](./cabal.md) for using the cabal source with stack.
3
+ Please see [cabal documentation](./cabal.md) for using the cabal source with stack.
data/lib/licensed.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "licensed/version"
3
3
  require "licensed/shell"
4
- require "licensed/configuration"
5
4
  require "licensed/license"
6
5
  require "licensed/dependency"
7
6
  require "licensed/git"
@@ -10,7 +9,10 @@ require "licensed/source/bower"
10
9
  require "licensed/source/manifest"
11
10
  require "licensed/source/npm"
12
11
  require "licensed/source/go"
12
+ require "licensed/source/dep"
13
13
  require "licensed/source/cabal"
14
+ require "licensed/source/pip"
15
+ require "licensed/configuration"
14
16
  require "licensed/command/cache"
15
17
  require "licensed/command/status"
16
18
  require "licensed/command/list"
@@ -11,17 +11,19 @@ module Licensed
11
11
  def run(force: false)
12
12
  summary = @config.apps.flat_map do |app|
13
13
  app_name = app["name"]
14
- @config.ui.info "Caching licenes for #{app_name}:"
14
+ @config.ui.info "Caching licenses for #{app_name}:"
15
15
 
16
16
  # load the app environment
17
17
  Dir.chdir app.source_path do
18
18
 
19
19
  # map each available app source to it's dependencies
20
20
  app.sources.map do |source|
21
- @config.ui.info " #{source.type} dependencies:"
21
+ type = source.class.type
22
+
23
+ @config.ui.info " #{type} dependencies:"
22
24
 
23
25
  names = []
24
- cache_path = app.cache_path.join(source.type)
26
+ cache_path = app.cache_path.join(type)
25
27
 
26
28
  # exclude ignored dependencies
27
29
  dependencies = source.dependencies.select { |d| !app.ignored?(d) }
@@ -38,8 +40,9 @@ module Licensed
38
40
  # or default to a blank license
39
41
  license = Licensed::License.read(filename) || Licensed::License.new
40
42
 
41
- # Version did not change, no need to re-cache
42
- if !force && version == license["version"]
43
+ # cached version string exists and did not change, no need to re-cache
44
+ has_version = !license["version"].nil? && !license["version"].empty?
45
+ if !force && has_version && version == license["version"]
43
46
  @config.ui.info " Using #{name} (#{version})"
44
47
  next
45
48
  end
@@ -60,7 +63,7 @@ module Licensed
60
63
  FileUtils.rm(file) unless names.include?(relative_path.chomp(".txt"))
61
64
  end
62
65
 
63
- "* #{app_name} #{source.type} dependencies: #{dependencies.size}"
66
+ "* #{app_name} #{type} dependencies: #{dependencies.size}"
64
67
  end
65
68
  end
66
69
  end
@@ -13,14 +13,16 @@ module Licensed
13
13
  @config.ui.info "Displaying dependencies for #{app['name']}"
14
14
  Dir.chdir app.source_path do
15
15
  app.sources.each do |source|
16
- @config.ui.info " #{source.type} dependencies:"
16
+ type = source.class.type
17
+
18
+ @config.ui.info " #{type} dependencies:"
17
19
 
18
20
  source_dependencies = dependencies(app, source)
19
21
  source_dependencies.each do |dependency|
20
22
  @config.ui.info " Found #{dependency['name']} (#{dependency['version']})"
21
23
  end
22
24
 
23
- @config.ui.confirm " * #{source.type} dependencies: #{source_dependencies.size}"
25
+ @config.ui.confirm " * #{type} dependencies: #{source_dependencies.size}"
24
26
  end
25
27
  end
26
28
  end
@@ -9,6 +9,7 @@ module Licensed
9
9
  ".licensed.yaml".freeze,
10
10
  ".licensed.json".freeze
11
11
  ].freeze
12
+ SOURCE_TYPES = Source.constants.map { |c| Source.const_get(c) }.freeze
12
13
 
13
14
  def initialize(options = {}, inherited_options = {})
14
15
  super()
@@ -46,19 +47,16 @@ module Licensed
46
47
 
47
48
  # Returns an array of enabled app sources
48
49
  def sources
49
- @sources ||= [
50
- Source::Bundler.new(self),
51
- Source::Bower.new(self),
52
- Source::Cabal.new(self),
53
- Source::Go.new(self),
54
- Source::Manifest.new(self),
55
- Source::NPM.new(self)
56
- ].select(&:enabled?)
50
+ @sources ||= SOURCE_TYPES.select { |source_class| enabled?(source_class.type) }
51
+ .map { |source_class| source_class.new(self) }
52
+ .select(&:enabled?)
57
53
  end
58
54
 
59
55
  # Returns whether a source type is enabled
60
56
  def enabled?(source_type)
61
- self["sources"].fetch(source_type, true)
57
+ # the default is false if any sources are set to true, true otherwise
58
+ default = !self["sources"].any? { |_, enabled| enabled }
59
+ self["sources"].fetch(source_type, default)
62
60
  end
63
61
 
64
62
  # Is the given dependency reviewed?
data/lib/licensed/git.rb CHANGED
@@ -23,7 +23,7 @@ module Licensed
23
23
  file = File.directory?(descriptor) ? "." : File.basename(descriptor)
24
24
 
25
25
  Dir.chdir dir do
26
- Licensed::Shell.execute("git", "rev-list", "-1", "HEAD", "--", file)
26
+ Licensed::Shell.execute("git", "rev-list", "-1", "HEAD", "--", file, allow_failure: true)
27
27
  end
28
28
  end
29
29
 
@@ -3,12 +3,19 @@ require "open3"
3
3
 
4
4
  module Licensed
5
5
  module Shell
6
- # Executes a command, returning it's STDOUT on success. Returns an empty
7
- # string on failure
8
- def self.execute(cmd, *args)
9
- output, _, status = Open3.capture3(cmd, *args)
10
- return "" unless status.success?
11
- output.strip
6
+ # Executes a command, returning its standard output on success. On failure,
7
+ # it raises an exception that contains the error output, unless
8
+ # `allow_failure` is true, in which case it returns an empty string.
9
+ def self.execute(cmd, *args, allow_failure: false)
10
+ stdout, stderr, status = Open3.capture3(cmd, *args)
11
+
12
+ if status.success?
13
+ stdout.strip
14
+ elsif allow_failure
15
+ ""
16
+ else
17
+ raise Error.new([cmd, *args], status.exitstatus, stderr)
18
+ end
12
19
  end
13
20
 
14
21
  # Executes a command and returns a boolean value indicating if the command
@@ -24,5 +31,31 @@ module Licensed
24
31
  output, err, status = Open3.capture3("which", tool)
25
32
  status.success? && !output.strip.empty? && err.strip.empty?
26
33
  end
34
+
35
+ class Error < RuntimeError
36
+ def initialize(cmd, status, stderr)
37
+ super()
38
+ @cmd = cmd
39
+ @exitstatus = status
40
+ @output = stderr
41
+ end
42
+
43
+ def message
44
+ output = @output.to_s.strip
45
+ extra = output.empty?? "" : "\n#{output.gsub(/^/, " ")}"
46
+ "command exited with status #{@exitstatus}\n #{escape_cmd}#{extra}"
47
+ end
48
+
49
+ def escape_cmd
50
+ @cmd.map do |arg|
51
+ if arg =~ /[\s'"]/
52
+ escaped = arg.gsub(/([\\"])/, '\\\\\1')
53
+ %("#{escaped}")
54
+ else
55
+ arg
56
+ end
57
+ end.join(" ")
58
+ end
59
+ end
27
60
  end
28
61
  end
@@ -4,17 +4,15 @@ require "json"
4
4
  module Licensed
5
5
  module Source
6
6
  class Bower
7
- def initialize(config)
8
- @config = config
7
+ def self.type
8
+ "bower"
9
9
  end
10
10
 
11
- def type
12
- "bower"
11
+ def initialize(config)
12
+ @config = config
13
13
  end
14
14
 
15
15
  def enabled?
16
- return false unless @config.enabled?(type)
17
-
18
16
  [@config.pwd.join(".bowerrc"), @config.pwd.join("bower.json")].any? do |path|
19
17
  File.exist?(path)
20
18
  end
@@ -25,7 +23,7 @@ module Licensed
25
23
  package = JSON.parse(File.read(file))
26
24
  path = bower_path.join(file).dirname.to_path
27
25
  Dependency.new(path, {
28
- "type" => type,
26
+ "type" => Bower.type,
29
27
  "name" => package["name"],
30
28
  "version" => package["version"] || package["_release"],
31
29
  "summary" => package["description"],
@@ -1,28 +1,31 @@
1
1
  # frozen_string_literal: true
2
- require "bundler"
2
+ begin
3
+ require "bundler"
4
+ rescue LoadError
5
+ end
3
6
 
4
7
  module Licensed
5
8
  module Source
6
9
  class Bundler
7
10
  GEMFILES = %w{Gemfile gems.rb}.freeze
8
11
 
12
+ def self.type
13
+ "rubygem"
14
+ end
15
+
9
16
  def initialize(config)
10
17
  @config = config
11
18
  end
12
19
 
13
20
  def enabled?
14
- @config.enabled?(type) && lockfile_path && lockfile_path.exist?
15
- end
16
-
17
- def type
18
- "rubygem"
21
+ defined?(::Bundler) && lockfile_path && lockfile_path.exist?
19
22
  end
20
23
 
21
24
  def dependencies
22
25
  @dependencies ||= with_local_configuration do
23
26
  definition.specs_for(groups).map do |spec|
24
27
  Dependency.new(spec.gem_dir, {
25
- "type" => type,
28
+ "type" => Bundler.type,
26
29
  "name" => spec.name,
27
30
  "version" => spec.version.to_s,
28
31
  "summary" => spec.summary,
@@ -4,16 +4,19 @@ require "English"
4
4
  module Licensed
5
5
  module Source
6
6
  class Cabal
7
- def initialize(config)
8
- @config = config
9
- end
7
+ DEPENDENCY_REGEX = /\s*.+?\s*/.freeze
8
+ DEFAULT_TARGETS = %w{executable library}.freeze
10
9
 
11
- def type
10
+ def self.type
12
11
  "cabal"
13
12
  end
14
13
 
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
15
18
  def enabled?
16
- @config.enabled?(type) && cabal_packages.any? && ghc?
19
+ cabal_file_dependencies.any? && ghc?
17
20
  end
18
21
 
19
22
  def dependencies
@@ -22,7 +25,7 @@ module Licensed
22
25
 
23
26
  path, search_root = package_docs_dirs(package)
24
27
  Dependency.new(path, {
25
- "type" => type,
28
+ "type" => Cabal.type,
26
29
  "name" => package["name"],
27
30
  "version" => package["version"],
28
31
  "summary" => package["synopsis"],
@@ -63,8 +66,7 @@ module Licensed
63
66
 
64
67
  # Returns a `Set` of the package ids for all cabal dependencies
65
68
  def package_ids
66
- deps = cabal_packages.flat_map { |n| package_dependencies(n, false) }
67
- recursive_dependencies(deps)
69
+ recursive_dependencies(cabal_file_dependencies)
68
70
  end
69
71
 
70
72
  # Recursively finds the dependencies for each cabal package.
@@ -125,7 +127,7 @@ module Licensed
125
127
  # Runs a `ghc-pkg field` command for a given set of fields and arguments
126
128
  # Automatically includes ghc package DB locations in the command
127
129
  def ghc_pkg_field_command(id, fields, *args)
128
- Licensed::Shell.execute("ghc-pkg", "field", id, fields.join(","), *args, *package_db_args)
130
+ Licensed::Shell.execute("ghc-pkg", "field", id, fields.join(","), *args, *package_db_args, allow_failure: true)
129
131
  end
130
132
 
131
133
  # Returns an array of ghc package DB locations as specified in the app
@@ -148,12 +150,56 @@ module Licensed
148
150
  path.gsub("<ghc_version>", ghc_version)
149
151
  end
150
152
 
151
- # Return an array of the top-level cabal packages for the current app
152
- def cabal_packages
153
- cabal_files.map do |f|
154
- name_match = File.read(f).match(/^name:\s*(.*)$/)
155
- name_match[1] if name_match
156
- end.compact
153
+ # Returns a set containing the top-level dependencies found in cabal files
154
+ def cabal_file_dependencies
155
+ cabal_files.each_with_object(Set.new) do |cabal_file, packages|
156
+ content = File.read(cabal_file)
157
+ next if content.nil? || content.empty?
158
+
159
+ # add any dependencies for matched targets from the cabal file.
160
+ # by default this will find executable and library dependencies
161
+ content.scan(cabal_file_regex).each do |match|
162
+ # match[1] is a string of "," separated dependencies
163
+ dependencies = match[1].split(",").map(&:strip)
164
+ dependencies.each do |dep|
165
+ # the dependency might have a version specifier.
166
+ # remove it so we can get the full id specifier for each package
167
+ id = cabal_package_id(dep.split(/\s/)[0])
168
+ packages.add(id) if id
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Returns an installed package id for the package.
175
+ def cabal_package_id(package_name)
176
+ field = ghc_pkg_field_command(package_name, ["id"])
177
+ id = field.split(":", 2)[1]
178
+ id.strip if id
179
+ end
180
+
181
+ # Find `build-depends` lists from specified targets in a cabal file
182
+ def cabal_file_regex
183
+ # this will match 0 or more occurences of
184
+ # match[0] - specifier, e.g. executable, library, etc
185
+ # match[1] - full list of matched dependencies
186
+ # match[2] - first matched dependency (required)
187
+ # match[3] - remainder of matched dependencies (not required)
188
+ @cabal_file_regex ||= /
189
+ # match a specifier, e.g. library or executable
190
+ ^(#{cabal_file_targets.join("|")})
191
+ .*? # stuff
192
+
193
+ # match a list of 1 or more dependencies
194
+ build-depends:(#{DEPENDENCY_REGEX}(,#{DEPENDENCY_REGEX})*)\n
195
+ /xmi
196
+ end
197
+
198
+ # Returns the targets to search for `build-depends` in a cabal file
199
+ def cabal_file_targets
200
+ targets = Array(@config.dig("cabal", "cabal_file_targets"))
201
+ targets.push(*DEFAULT_TARGETS) if targets.empty?
202
+ targets
157
203
  end
158
204
 
159
205
  # Returns an array of the local directory cabal package files
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require "tomlrb"
3
+
4
+ module Licensed
5
+ module Source
6
+ class Dep
7
+ def self.type
8
+ "dep"
9
+ end
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def enabled?
16
+ go_dep_available?
17
+ end
18
+
19
+ def dependencies
20
+ @dependencies ||= begin
21
+ packages.map do |package|
22
+ package_dir = @config.pwd.join("vendor", package[:name])
23
+ search_root = @config.pwd.join("vendor", package[:project])
24
+
25
+ unless package_dir.exist?
26
+ next if @config.ignored?("type" => Dep.type, "name" => package[:name])
27
+ raise "couldn't find package for #{package[:name]}"
28
+ end
29
+
30
+ Dependency.new(package_dir.to_s, {
31
+ "type" => Dep.type,
32
+ "name" => package[:name],
33
+ "homepage" => "https://#{package[:name]}",
34
+ "search_root" => search_root.to_s,
35
+ "version" => package[:version]
36
+ })
37
+ end
38
+ end
39
+ end
40
+
41
+ # Returns an array of dependency packages specified from Gopkg.lock
42
+ def packages
43
+ gopkg_lock = Tomlrb.load_file(gopkg_lock_path, symbolize_keys: true)
44
+ return [] unless gopkg_lock && gopkg_lock[:projects]
45
+
46
+ gopkg_lock[:projects].flat_map do |project|
47
+ # map each package to a full import path
48
+ # then return a hash for each import path containing the path and the version
49
+ project[:packages].map { |package| package == "." ? project[:name] : "#{project[:name]}/#{package}" }
50
+ .reject { |import_path| go_std_package?(import_path) }
51
+ .map { |import_path| { name: import_path, version: project[:revision], project: project[:name] } }
52
+ end
53
+ end
54
+
55
+ # Returns whether the package is part of the go std list. Replaces
56
+ # "golang.org" with "golang_org" to match packages listed in `go list std`
57
+ # as "vendor/golang_org/*" but are vendored as "vendor/golang.org/*"
58
+ def go_std_package?(import_path)
59
+ go_std_packages.include? "vendor/#{import_path.sub(/^golang.org/, "golang_org")}"
60
+ end
61
+
62
+ def go_dep_available?
63
+ return false unless gopkg_lock_path.exist? && gopkg_toml_path.exist?
64
+ return true if @config.dig("dep", "allow_ignored") == true
65
+
66
+ gopkg_toml = Tomlrb.load_file(gopkg_toml_path, symbolize_keys: true)
67
+ gopkg_toml[:ignored].nil? || gopkg_toml[:ignored].empty?
68
+ end
69
+
70
+ def gopkg_lock_path
71
+ @config.pwd.join("Gopkg.lock")
72
+ end
73
+
74
+ def gopkg_toml_path
75
+ @config.pwd.join("Gopkg.toml")
76
+ end
77
+
78
+ # Returns a list of go standard packages
79
+ def go_std_packages
80
+ @std_packages ||= begin
81
+ return [] unless Licensed::Shell.tool_available?("go")
82
+ Licensed::Shell.execute("go", "list", "std").lines.map(&:strip)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
3
  require "English"
4
+ require "pathname"
4
5
 
5
6
  module Licensed
6
7
  module Source
7
8
  class Go
8
- def initialize(config)
9
- @config = config
9
+ def self.type
10
+ "go"
10
11
  end
11
12
 
12
- def type
13
- "go"
13
+ def initialize(config)
14
+ @config = config
14
15
  end
15
16
 
16
17
  def enabled?
17
- @config.enabled?(type) && go_source?
18
+ Licensed::Shell.tool_available?("go") && go_source?
18
19
  end
19
20
 
20
21
  def dependencies
@@ -24,13 +25,13 @@ module Licensed
24
25
  import_path = non_vendored_import_path(package_name)
25
26
 
26
27
  if package.empty?
27
- next if @config.ignored?("type" => type, "name" => package_name)
28
+ next if @config.ignored?("type" => Go.type, "name" => package_name)
28
29
  raise "couldn't find package for #{import_path}"
29
30
  end
30
31
 
31
32
  package_dir = package["Dir"]
32
33
  Dependency.new(package_dir, {
33
- "type" => type,
34
+ "type" => Go.type,
34
35
  "name" => import_path,
35
36
  "summary" => package["Doc"],
36
37
  "homepage" => homepage(import_path),
@@ -61,6 +62,13 @@ module Licensed
61
62
  .uniq
62
63
  .select { |d| !go_std_packages.include?(d) }
63
64
  .select { |d| !d.start_with?(root_package["ImportPath"]) || vendored_path?(d) }
65
+ .select do |d|
66
+ # this removes the packages listed in `go list std` as "vendor/golang_org/*" but are vendored
67
+ # as "vendor/golang.org/*"
68
+ go_std_packages.none? do |std_pkg|
69
+ std_pkg.sub(%r{^vendor/golang_org/}, "#{root_package["ImportPath"]}/vendor/golang.org/") == d
70
+ end
71
+ end
64
72
  end
65
73
 
66
74
  # Returns the root directory to search for a package license
@@ -106,7 +114,7 @@ module Licensed
106
114
  # package - Go package import path
107
115
  def package_info_command(package)
108
116
  package ||= ""
109
- Licensed::Shell.execute("go", "list", "-json", package)
117
+ Licensed::Shell.execute("go", "list", "-json", package, allow_failure: true)
110
118
  end
111
119
 
112
120
  # Returns the info for the package under test
@@ -133,7 +141,12 @@ module Licensed
133
141
  @gopath = if path.nil? || path.empty?
134
142
  ENV["GOPATH"]
135
143
  else
136
- File.expand_path(path, Licensed::Git.repository_root)
144
+ root = begin
145
+ Licensed::Git.repository_root
146
+ rescue Licensed::Shell::Error
147
+ Pathname.pwd
148
+ end
149
+ File.expand_path(path, root)
137
150
  end
138
151
  end
139
152
 
@@ -4,22 +4,22 @@ require "pathname/common_prefix"
4
4
  module Licensed
5
5
  module Source
6
6
  class Manifest
7
+ def self.type
8
+ "manifest"
9
+ end
10
+
7
11
  def initialize(config)
8
12
  @config = config
9
13
  end
10
14
 
11
15
  def enabled?
12
- @config.enabled?(type) && File.exist?(manifest_path)
13
- end
14
-
15
- def type
16
- "manifest"
16
+ File.exist?(manifest_path)
17
17
  end
18
18
 
19
19
  def dependencies
20
20
  @dependencies ||= packages.map do |package_name, sources|
21
21
  Dependency.new(sources_license_path(sources), {
22
- "type" => type,
22
+ "type" => Manifest.type,
23
23
  "name" => package_name,
24
24
  "version" => package_version(sources)
25
25
  })
@@ -4,16 +4,16 @@ require "json"
4
4
  module Licensed
5
5
  module Source
6
6
  class NPM
7
- def initialize(config)
8
- @config = config
7
+ def self.type
8
+ "npm"
9
9
  end
10
10
 
11
- def type
12
- "npm"
11
+ def initialize(config)
12
+ @config = config
13
13
  end
14
14
 
15
15
  def enabled?
16
- @config.enabled?(type) && File.exist?(@config.pwd.join("package.json"))
16
+ Licensed::Shell.tool_available?("npm") && File.exist?(@config.pwd.join("package.json"))
17
17
  end
18
18
 
19
19
  def dependencies
@@ -31,7 +31,7 @@ module Licensed
31
31
  path = package["realPath"] || locations["#{package["name"]}@#{package["version"]}"]
32
32
  fail "couldn't locate #{name} under node_modules/" unless path
33
33
  Dependency.new(path, {
34
- "type" => type,
34
+ "type" => NPM.type,
35
35
  "name" => package["name"],
36
36
  "version" => package["version"],
37
37
  "summary" => package["description"],
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require "English"
4
+
5
+ module Licensed
6
+ module Source
7
+ class Pip
8
+ def self.type
9
+ "pip"
10
+ end
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ end
15
+
16
+ def enabled?
17
+ File.exist?(@config.pwd.join("requirements.txt"))
18
+ end
19
+
20
+ def dependencies
21
+ @dependencies ||= parse_requirements_txt.map do |package_name|
22
+ package = package_info(package_name)
23
+ location = File.join(package["Location"], package["Name"] + "-" + package["Version"] + ".dist-info")
24
+ Dependency.new(location, {
25
+ "type" => Pip.type,
26
+ "name" => package["Name"],
27
+ "summary" => package["Summary"],
28
+ "homepage" => package["Home-page"],
29
+ "version" => package["Version"]
30
+ })
31
+ end
32
+ end
33
+
34
+ # Build the list of packages from a 'requirements.txt'
35
+ # Assumes that the requirements.txt follow the format pkg=1.0.0 or pkg==1.0.0
36
+ def parse_requirements_txt
37
+ File.open(@config.pwd.join("requirements.txt")).map do |line|
38
+ p_split = line.split("=")
39
+ p_split[0]
40
+ end
41
+ end
42
+
43
+ def package_info(package_name)
44
+ p_info = pip_command(package_name).lines
45
+ p_info.each_with_object(Hash.new(0)) { |pkg, a|
46
+ k, v = pkg.split(":", 2)
47
+ next if k.nil? || k.empty?
48
+ a[k.strip] = v&.strip
49
+ }
50
+ end
51
+
52
+ def pip_command(*args)
53
+ venv_dir = @config.dig("python", "virtual_env_dir")
54
+ if venv_dir.nil?
55
+ raise "Virtual env directory not set."
56
+ end
57
+ venv_dir = File.expand_path(venv_dir, Licensed::Git.repository_root)
58
+ pip = File.join(venv_dir, "bin", "pip")
59
+ Licensed::Shell.execute(pip, "--disable-pip-version-check", "show", *args)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Licensed
3
- VERSION = "1.0.1".freeze
3
+ VERSION = "1.1.0".freeze
4
4
  end
data/licensed.gemspec CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency "thor", "~>0.19"
26
26
  spec.add_dependency "octokit", "~>4.0"
27
27
  spec.add_dependency "pathname-common_prefix", "~>0.0.1"
28
+ spec.add_dependency "tomlrb", "~>1.2"
28
29
 
29
30
  spec.add_development_dependency "bundler", "~> 1.10"
30
31
  spec.add_development_dependency "rake", "~> 10.0"
data/script/setup CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  set -e
4
4
 
5
- bundle exec rake setup
5
+ bundle exec rake setup["$@"]
@@ -9,4 +9,9 @@ fi
9
9
  # setup test fixtures
10
10
  BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
11
11
  cd $BASE_PATH/test/fixtures/bower
12
+
13
+ if [ "$1" == "-f" ]; then
14
+ find . -not -regex "\.*" -and -not -name "bower\.json" -print0 | xargs -0 rm -rf
15
+ fi
16
+
12
17
  bower install
@@ -13,4 +13,8 @@ cd $BASE_PATH/test/fixtures/bundler
13
13
  # unset any pre-existing gemfile when installing test fixtures
14
14
  unset BUNDLE_GEMFILE
15
15
 
16
- bundle install --path $BASE_PATH/test/fixtures/bundler/vendor/gems
16
+ if [ "$1" == "-f" ]; then
17
+ find . -not -regex "\.*" -and -not -name "Gemfile" -print0 | xargs -0 rm -rf
18
+ fi
19
+
20
+ bundle install --path vendor/gems
@@ -8,5 +8,10 @@ fi
8
8
 
9
9
  # setup test fixtures
10
10
  BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
11
- cd $BASE_PATH/test/fixtures/haskell
11
+ cd $BASE_PATH/test/fixtures/cabal
12
+
13
+ if [ "$1" == "-f" ]; then
14
+ find . -not -regex "\.*" -and -not -path "*app*" -print0 | xargs -0 rm -rf
15
+ fi
16
+
12
17
  cabal new-build
@@ -9,5 +9,11 @@ fi
9
9
  # setup test fixtures
10
10
  BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
11
11
  export GOPATH="$BASE_PATH/test/fixtures/go"
12
- cd $BASE_PATH/test/fixtures/go/src/test
12
+ cd $BASE_PATH/test/fixtures/go
13
+
14
+ if [ "$1" == "-f" ]; then
15
+ find . -not -regex "\.*" -and -not -path "*/src/test*" -and -not -path "*/src" -print0 | xargs -0 rm -rf
16
+ fi
17
+
18
+ cd src/test
13
19
  go get
@@ -9,4 +9,9 @@ fi
9
9
  # setup test fixtures
10
10
  BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
11
11
  cd $BASE_PATH/test/fixtures/npm
12
+
13
+ if [ "$1" == "-f" ]; then
14
+ find . -not -regex "\.*" -and -not -name "package\.json" -print0 | xargs -0 rm -rf
15
+ fi
16
+
12
17
  npm install
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ if [ -z "$(which pip)" ]; then
5
+ echo "A local pip installation is required for python development." >&2
6
+ exit 127
7
+ fi
8
+
9
+ if [ -z "$(which virtualenv)" ]; then
10
+ echo "A local virtualenv installation is required for python development." >&2
11
+ exit 127
12
+ fi
13
+
14
+
15
+ # setup test fixtures
16
+ BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
17
+
18
+
19
+ # clean up any previous fixture venv that might have been created.
20
+ if [ "$1" == "-f" ]; then
21
+ echo "removing old fixture setup..."
22
+ rm -rf $BASE_PATH/test/fixtures/pip/venv
23
+ fi
24
+
25
+ # set up a virtualenv and install the packages in the test requirements
26
+ virtualenv $BASE_PATH/test/fixtures/pip/venv
27
+ . $BASE_PATH/test/fixtures/pip/venv/bin/activate
28
+ pip install -r $BASE_PATH/test/fixtures/pip/requirements.txt
29
+ deactivate
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: licensed
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-04-26 00:00:00.000000000 Z
11
+ date: 2018-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: licensee
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.0.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: tomlrb
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.2'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: bundler
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -201,9 +215,11 @@ files:
201
215
  - docs/sources/bower.md
202
216
  - docs/sources/bundler.md
203
217
  - docs/sources/cabal.md
218
+ - docs/sources/dep.md
204
219
  - docs/sources/go.md
205
220
  - docs/sources/manifests.md
206
221
  - docs/sources/npm.md
222
+ - docs/sources/pip.md
207
223
  - docs/sources/stack.md
208
224
  - exe/licensed
209
225
  - lib/licensed.rb
@@ -219,9 +235,11 @@ files:
219
235
  - lib/licensed/source/bower.rb
220
236
  - lib/licensed/source/bundler.rb
221
237
  - lib/licensed/source/cabal.rb
238
+ - lib/licensed/source/dep.rb
222
239
  - lib/licensed/source/go.rb
223
240
  - lib/licensed/source/manifest.rb
224
241
  - lib/licensed/source/npm.rb
242
+ - lib/licensed/source/pip.rb
225
243
  - lib/licensed/ui/shell.rb
226
244
  - lib/licensed/version.rb
227
245
  - licensed.gemspec
@@ -234,6 +252,7 @@ files:
234
252
  - script/source-setup/cabal
235
253
  - script/source-setup/go
236
254
  - script/source-setup/npm
255
+ - script/source-setup/pip
237
256
  - script/test
238
257
  homepage: https://github.com/github/licensed
239
258
  licenses: