devpack 0.3.3 → 0.4.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
  SHA256:
3
- metadata.gz: 3e708e92800fdb16f17e5f15decb2d89fd935ea0c2b473f243fa78b472f571f6
4
- data.tar.gz: 24924053b2ee1c90ebc75e5eb0a8f7ee45d7196c63a059da761f68da5c0f1ee5
3
+ metadata.gz: 88ec4f3a9263a0ce2204ff40a97c9f502897ad2eab40d5ec9356dcab0dccdd10
4
+ data.tar.gz: 5e6e9173e18339fa091cda8780588b0378c95c9589f8344a328d2e368f6649fa
5
5
  SHA512:
6
- metadata.gz: 935db175be08d441d07fb6ad177e2bfa80b971dad327f6f0d3f761f4507a00883bae57414c20bebf19451bef5318aecc3a1d93d0b0591c2fe06dd769b99f1514
7
- data.tar.gz: '089ba6844efef952c3b2f72fbc27d6e87c770f29c6bc39f57e97f0a4000874ea380a829926297f846cdee386e4ab5e661a5dc98c4c02ca35408fbf4381d17948'
6
+ metadata.gz: 99bce1bb4528c5a2da45d6165481a05334688dec0c9a5f8bd6ac8778e4a85ca3dca45331134881a2d57b4bf0a664e1341541158456b48dde40e264dce7badcea
7
+ data.tar.gz: 693c4a80282a14cc25a4be573d8b3de84823083251e3a8417b6c056edcba1d5e3170700962d86ef5dccb1508b8185fd9f124fc138d46af9c4ac3e175a164edef
data/.rubocop.yml CHANGED
@@ -6,3 +6,7 @@ AllCops:
6
6
  NewCops: enable
7
7
  Exclude:
8
8
  - "spec/fixtures/**/*"
9
+
10
+ Bundler/GemFilename:
11
+ Exclude:
12
+ - "lib/devpack/gems.rb"
data/README.md CHANGED
@@ -21,7 +21,7 @@ Add _Devpack_ to any project's `Gemfile`:
21
21
  ```ruby
22
22
  # Gemfile
23
23
  group :development, :test do
24
- gem 'devpack', '~> 0.3.3'
24
+ gem 'devpack', '~> 0.4.0'
25
25
  end
26
26
  ```
27
27
 
@@ -45,6 +45,14 @@ It is recommended to use a [global configuration](#global-configuration).
45
45
 
46
46
  When using a per-project configuration, `.devpack` files should be added to `.gitignore`.
47
47
 
48
+ ### Gem Installation
49
+
50
+ A convenience command is provided to install all gems listed in `.devpack` file that are not already installed:
51
+
52
+ ```ruby
53
+ bundle exec devpack install
54
+ ```
55
+
48
56
  ### Initializers
49
57
 
50
58
  Custom initializers can be loaded by creating a directory named `.devpack_initializers` containing a set of `.rb` files.
@@ -75,6 +83,13 @@ To configure globally simply save your `.devpack` configuration file to any pare
75
83
 
76
84
  This strategy also applies to `.devpack_initializers`.
77
85
 
86
+ ### Silencing
87
+
88
+ To prevent _Devpack_ from displaying messages on load, set the environment variable `DEVPACK_SILENT=1` to any value:
89
+ ```bash
90
+ DEVPACK_SILENT=1 bundle exec ruby myapp.rb
91
+ ```
92
+
78
93
  ### Disabling
79
94
 
80
95
  To disable _Devpack_ set the environment variable `DEVPACK_DISABLE` to any value:
data/devpack.gemspec CHANGED
@@ -17,12 +17,13 @@ Gem::Specification.new do |spec|
17
17
  spec.metadata['homepage_uri'] = spec.homepage
18
18
  spec.metadata['source_code_uri'] = spec.homepage
19
19
  spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
20
+ spec.metadata['rubygems_mfa_required'] = 'true'
20
21
 
21
22
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
23
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
24
  end
24
- spec.bindir = 'bin'
25
- spec.executables = []
25
+ spec.bindir = 'exe'
26
+ spec.executables = ['devpack']
26
27
  spec.require_paths = ['lib']
27
28
 
28
29
  spec.add_development_dependency 'byebug', '~> 11.1'
data/exe/devpack ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV['DEVPACK_DISABLE'] = '1'
5
+
6
+ require 'open3'
7
+
8
+ require 'devpack'
9
+
10
+ command = ARGV[0]
11
+
12
+ if command == 'install'
13
+ missing = Devpack::Gems.new(Devpack.config).tap { |gems| gems.load(silent: true) }.missing
14
+ install_command = "bundle exec gem install -V #{missing.map(&:required_version).join(' ')}" unless missing.empty?
15
+ if install_command.nil?
16
+ warn '[devpack] No gems to install.'
17
+ else
18
+ warn "[devpack] [exec] #{install_command}"
19
+ output, status = Open3.capture2e(install_command)
20
+ puts output
21
+ puts status
22
+ if status.success?
23
+ warn '[devpack] Installation complete.'
24
+ else
25
+ warn "[devpack] Installation failed. Manually verify this command: #{install_command}"
26
+ end
27
+ end
28
+ exit 0
29
+ end
30
+
31
+ warn "[devpack] Unknown command: #{command}"
32
+ exit 1
@@ -3,56 +3,104 @@
3
3
  module Devpack
4
4
  # Locates relevant gemspec for a given gem and provides a full list of paths
5
5
  # for all `require_paths` listed in gemspec.
6
+ # rubocop:disable Metrics/ClassLength
6
7
  class GemSpec
7
- def initialize(glob, name, requirement)
8
+ attr_reader :name, :root
9
+
10
+ def initialize(glob, name, requirement, root: nil)
8
11
  @name = name
9
12
  @glob = glob
10
13
  @requirement = requirement
14
+ @root = root || self
11
15
  @dependency = Gem::Dependency.new(@name, @requirement)
12
16
  end
13
17
 
14
18
  def require_paths(visited = Set.new)
15
- raise GemNotFoundError, @requirement.nil? ? '-' : required_version if gemspec.nil?
19
+ raise GemNotFoundError.new("Gem not found: #{required_version}", self) if gemspec.nil?
16
20
 
17
21
  (immediate_require_paths + dependency_require_paths(visited)).compact.flatten.uniq
18
22
  end
19
23
 
20
24
  def gemspec
25
+ return Gem.loaded_specs[@name] if compatible?(Gem.loaded_specs[@name])
26
+
21
27
  @gemspec ||= gemspecs.find do |spec|
22
28
  next false if spec.nil?
23
29
 
24
- @dependency.requirement.satisfied_by?(spec.version) && compatible?(spec)
30
+ raise_incompatible(spec) unless compatible?(spec)
31
+
32
+ @dependency.requirement.satisfied_by?(spec.version)
25
33
  end
26
34
  end
27
35
 
36
+ def pretty_name
37
+ return @name.to_s if @requirement.nil?
38
+
39
+ "#{@name} #{@requirement}"
40
+ end
41
+
42
+ def root?
43
+ self == @root
44
+ end
45
+
46
+ def required_version
47
+ return @name.to_s if compatible_spec.nil? && @requirement.nil?
48
+ return "#{@name}:#{compatible_version}" if compatible_spec.nil?
49
+
50
+ "#{@name}:#{compatible_spec.version}"
51
+ end
52
+
28
53
  private
29
54
 
30
55
  def compatible?(spec)
31
56
  return false if spec.nil?
32
57
  return false if incompatible_version_loaded?(spec)
33
58
 
34
- compatible_specs?(Gem.loaded_specs.values, [@dependency] + spec.runtime_dependencies)
59
+ compatible_specs?([@dependency] + spec.runtime_dependencies)
60
+ end
61
+
62
+ def compatible_spec
63
+ @compatible_spec ||= gemspecs.compact
64
+ .select { |spec| requirements_satisfied_by?(spec.version) }
65
+ .max_by(&:version)
35
66
  end
36
67
 
37
68
  def incompatible_version_loaded?(spec)
38
69
  matched = Gem.loaded_specs[spec.name]
39
70
  return false if matched.nil?
40
71
 
41
- matched.version != spec.version
72
+ !matched.satisfies_requirement?(@dependency)
42
73
  end
43
74
 
44
- def required_version
45
- @requirement.requirements.first.last.version
75
+ def raise_incompatible(spec)
76
+ raise GemIncompatibilityError.new('Incompatible dependencies', incompatible_dependencies(spec))
77
+ end
78
+
79
+ def compatible_version
80
+ @requirement.requirements.map(&:last).max_by { |version| @requirement.satisfied_by?(version) }
46
81
  end
47
82
 
48
- def compatible_specs?(specs, dependencies)
49
- specs.all? { |spec| compatible_dependencies?(dependencies, spec) }
83
+ def requirements_satisfied_by?(version)
84
+ @dependency.requirement.satisfied_by?(version)
85
+ end
86
+
87
+ def compatible_specs?(dependencies)
88
+ Gem.loaded_specs.values.all? { |spec| compatible_dependencies?(dependencies, spec) }
50
89
  end
51
90
 
52
91
  def compatible_dependencies?(dependencies, spec)
53
92
  dependencies.all? { |dependency| compatible_dependency?(dependency, spec) }
54
93
  end
55
94
 
95
+ def incompatible_dependencies(spec)
96
+ dependencies = [@dependency] + spec.runtime_dependencies
97
+ Gem.loaded_specs.map do |_name, loaded_spec|
98
+ next nil if compatible_dependencies?(dependencies, loaded_spec)
99
+
100
+ [@root, dependencies.reject { |dependency| compatible_dependency?(dependency, loaded_spec) }]
101
+ end.compact
102
+ end
103
+
56
104
  def compatible_dependency?(dependency, spec)
57
105
  return false if spec.nil?
58
106
  return true unless dependency.name == spec.name
@@ -69,7 +117,7 @@ module Devpack
69
117
  next nil if visited.include?(dependency)
70
118
 
71
119
  visited << dependency
72
- GemSpec.new(@glob, dependency.name, dependency.requirement).require_paths(visited)
120
+ GemSpec.new(@glob, dependency.name, dependency.requirement, root: @root).require_paths(visited)
73
121
  end
74
122
  end
75
123
 
@@ -101,4 +149,5 @@ module Devpack
101
149
  @candidates ||= @glob.find(@name)
102
150
  end
103
151
  end
152
+ # rubocop:enable Metrics/ClassLength
104
153
  end
data/lib/devpack/gems.rb CHANGED
@@ -5,36 +5,38 @@ module Devpack
5
5
  class Gems
6
6
  include Timeable
7
7
 
8
+ attr_reader :missing
9
+
8
10
  def initialize(config, glob = GemGlob.new)
9
11
  @config = config
10
12
  @gem_glob = glob
11
13
  @failures = []
12
14
  @missing = []
15
+ @incompatible = []
13
16
  end
14
17
 
15
- def load
18
+ def load(silent: false)
16
19
  return [] if @config.requested_gems.nil?
17
20
 
18
21
  gems, time = timed { load_devpack }
19
22
  names = gems.map(&:first)
20
- summarize(gems, time)
23
+ summarize(gems, time) unless silent
21
24
  names
22
25
  end
23
26
 
24
27
  private
25
28
 
26
29
  def summarize(gems, time)
27
- @failures.each do |failure|
28
- warn(:error, Messages.failure(failure[:name], failure[:message]))
29
- end
30
- warn(:success, Messages.loaded(@config.devpack_path, gems, time.round(2)))
30
+ @failures.each { |failure| warn(:error, Messages.failure(failure[:name], failure[:message])) }
31
+ warn(:success, Messages.loaded(@config, gems, time.round(2)))
31
32
  warn(:info, Messages.install_missing(@missing)) unless @missing.empty?
33
+ warn(:info, Messages.alert_incompatible(@incompatible.flatten(1))) unless @incompatible.empty?
32
34
  end
33
35
 
34
36
  def load_devpack
35
37
  @config.requested_gems.map do |requested|
36
38
  name, _, version = requested.partition(':')
37
- load_gem(name, version.empty? ? nil : Gem::Requirement.new("= #{version}"))
39
+ load_gem(name, version.empty? ? nil : Gem::Requirement.new(version))
38
40
  end.compact
39
41
  end
40
42
 
@@ -42,11 +44,11 @@ module Devpack
42
44
  [name, activate(name, requirement)]
43
45
  rescue LoadError => e
44
46
  deactivate(name)
45
- @failures << { name: name, message: load_error_message(e) }
46
- nil
47
+ nil.tap { @failures << { name: name, message: load_error_message(e) } }
47
48
  rescue GemNotFoundError => e
48
- @missing << { name: name, version: e.message == '-' ? nil : e.message }
49
- nil
49
+ nil.tap { @missing << e.meta }
50
+ rescue GemIncompatibilityError => e
51
+ nil.tap { @incompatible << e.meta }
50
52
  end
51
53
 
52
54
  def activate(name, version)
@@ -13,24 +13,43 @@ module Devpack
13
13
  "Failed to load initializer `#{path}`: #{error_message}"
14
14
  end
15
15
 
16
- def loaded(path, gems, time)
17
- already_loaded = gems.size - gems.reject { |_, loaded| loaded }.size
18
- base = "Loaded #{already_loaded} development gem(s) from '#{path}' in #{time} seconds"
19
- return "#{base}." if already_loaded == gems.size
16
+ # rubocop:disable Metrics/AbcSize
17
+ def loaded(config, gems, time)
18
+ loaded = gems.size - gems.reject { |_, devpack_loaded| devpack_loaded }.size
19
+ of_total = gems.size == config.requested_gems.size ? nil : " of #{color(:cyan) { config.requested_gems.size }}"
20
+ path = color(:cyan) { config.devpack_path }
21
+ base = "Loaded #{color(:green) { loaded }}#{of_total} development gem(s) from #{path} in #{time} seconds"
22
+ return "#{base}." if loaded == gems.size
20
23
 
21
- "#{base} (#{gems.size - already_loaded} gem(s) were already loaded by environment)."
24
+ "#{base} (#{color(:cyan) { gems.size - loaded }} gem(s) were already loaded by environment)."
22
25
  end
26
+ # rubocop:enable Metrics/AbcSize
23
27
 
24
28
  def loaded_initializers(path, initializers, time)
25
- "Loaded #{initializers.compact.size} initializer(s) from '#{path}' in #{time} seconds"
29
+ "Loaded #{color(:green) { initializers.compact.size }} initializer(s) from '#{path}' in #{time} seconds"
26
30
  end
27
31
 
28
32
  def install_missing(missing)
29
- gems = missing.map do |spec|
30
- spec[:version].nil? ? spec[:name] : "#{spec[:name]}==#{spec[:version]}"
33
+ command = color(:cyan) { 'bundle exec devpack install' }
34
+ grouped_missing = missing
35
+ .group_by(&:root)
36
+ .map do |root, dependencies|
37
+ next (color(:cyan) { root.pretty_name }).to_s if dependencies.all?(&:root?)
38
+
39
+ formatted_dependencies = dependencies.map { |dependency| color(:yellow) { dependency.pretty_name } }
40
+ "#{color(:cyan) { root.pretty_name }}: #{formatted_dependencies.join(', ')}"
31
41
  end
42
+ "Install #{missing.size} missing gem(s): #{command} # [#{grouped_missing.join(', ')}]"
43
+ end
32
44
 
33
- "Install #{missing.size} missing gem(s): #{color(:cyan) { command(gems) }}"
45
+ def alert_incompatible(incompatible)
46
+ grouped_dependencies = {}
47
+ incompatible.each do |spec, dependencies|
48
+ key = spec.root.pretty_name
49
+ grouped_dependencies[key] ||= []
50
+ grouped_dependencies[key] << dependencies
51
+ end
52
+ alert_incompatible_message(grouped_dependencies)
34
53
  end
35
54
 
36
55
  def test
@@ -52,6 +71,15 @@ module Devpack
52
71
  "bundle exec gem install #{gems.join(' ')}"
53
72
  end
54
73
 
74
+ def alert_incompatible_message(grouped_dependencies)
75
+ incompatible_dependencies = grouped_dependencies.sort.map do |name, dependencies|
76
+ "#{color(:cyan) { name }}: "\
77
+ "#{dependencies.flatten.map { |dependency| color(:yellow) { dependency.to_s } }.join(', ')}"
78
+ end
79
+ "Unable to resolve version conflicts for #{color(:yellow) { incompatible_dependencies.size }} "\
80
+ "dependencies: #{incompatible_dependencies.join(', ')}}"
81
+ end
82
+
55
83
  def palette
56
84
  {
57
85
  reset: "\e[39m",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Devpack
4
- VERSION = '0.3.3'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/devpack.rb CHANGED
@@ -15,12 +15,24 @@ require 'devpack/version'
15
15
 
16
16
  # Provides helper method for writing warning messages.
17
17
  module Devpack
18
- class Error < StandardError; end
18
+ # Base class for all Devpack errors. Accepts additional argument `meta` to store object info.
19
+ class Error < StandardError
20
+ attr_reader :message, :meta
21
+
22
+ def initialize(message = nil, meta = nil)
23
+ @message = message
24
+ @meta = meta
25
+ super(message)
26
+ end
27
+ end
19
28
 
20
29
  class GemNotFoundError < Error; end
30
+ class GemIncompatibilityError < Error; end
21
31
 
22
32
  class << self
23
33
  def warn(level, message)
34
+ return if silent?
35
+
24
36
  prefixed = message.split("\n").map { |line| "#{prefix(level)} #{line}" }.join("\n")
25
37
  Kernel.warn(prefixed)
26
38
  end
@@ -33,6 +45,10 @@ module Devpack
33
45
  ENV.key?('DEVPACK_DISABLE')
34
46
  end
35
47
 
48
+ def silent?
49
+ ENV.key?('DEVPACK_SILENT')
50
+ end
51
+
36
52
  def rails?
37
53
  defined?(Rails::Railtie)
38
54
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devpack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-22 00:00:00.000000000 Z
11
+ date: 2021-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: byebug
@@ -98,7 +98,8 @@ description: Allow developers to optionally include a set of development gems wi
98
98
  adding to the Gemfile.
99
99
  email:
100
100
  - git@bob.frl
101
- executables: []
101
+ executables:
102
+ - devpack
102
103
  extensions: []
103
104
  extra_rdoc_files: []
104
105
  files:
@@ -115,6 +116,7 @@ files:
115
116
  - bin/console
116
117
  - bin/setup
117
118
  - devpack.gemspec
119
+ - exe/devpack
118
120
  - lib/devpack.rb
119
121
  - lib/devpack/config.rb
120
122
  - lib/devpack/gem_glob.rb
@@ -132,6 +134,7 @@ metadata:
132
134
  homepage_uri: https://github.com/bobf/devpack
133
135
  source_code_uri: https://github.com/bobf/devpack
134
136
  changelog_uri: https://github.com/bobf/devpack/blob/master/CHANGELOG.md
137
+ rubygems_mfa_required: 'true'
135
138
  post_install_message:
136
139
  rdoc_options: []
137
140
  require_paths: