licensed 1.0.1 → 1.1.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.
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: