devpack 0.3.3 → 0.4.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
  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: