devpack 0.3.3 → 0.4.1

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: bc62279883bef6f044e47c419b853c89b33e76f699d7106575cd87869967b36d
4
+ data.tar.gz: 48a959cbd1004dce6752b6fb723905ad40f92c02cf73cd8432b969309d94f5a8
5
5
  SHA512:
6
- metadata.gz: 935db175be08d441d07fb6ad177e2bfa80b971dad327f6f0d3f761f4507a00883bae57414c20bebf19451bef5318aecc3a1d93d0b0591c2fe06dd769b99f1514
7
- data.tar.gz: '089ba6844efef952c3b2f72fbc27d6e87c770f29c6bc39f57e97f0a4000874ea380a829926297f846cdee386e4ab5e661a5dc98c4c02ca35408fbf4381d17948'
6
+ metadata.gz: a473937535208f922c6ceed0e137cfa758bf806bb39516e6f01906d748ef219c0fcb69fba33e195c49e95f045cf335267b7b997f300c30e6311792becba6a4ea
7
+ data.tar.gz: 5e4247975b02ec3432344755432bb887ca7036cdb9792a3c8377d3a091dc5c92a464e0e8cf5dcda82248f2039e4c6ca0efedacf4761bec870d0d2fe6101cd254
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/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.5.8
1
+ 2.5.9
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,24 @@ 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
+ ```bash
53
+ bundle exec devpack install
54
+ ```
55
+
56
+ ### Executing Commands Provided by DevPack Gems
57
+
58
+ Use the `exec` command to run a command provided by a gem installed via _Devpack_.
59
+
60
+ An example use case of this is the [Guard](https://github.com/guard/guard) gem:
61
+
62
+ ```bash
63
+ bundle exec devpack exec guard
64
+ ```
65
+
48
66
  ### Initializers
49
67
 
50
68
  Custom initializers can be loaded by creating a directory named `.devpack_initializers` containing a set of `.rb` files.
@@ -75,6 +93,13 @@ To configure globally simply save your `.devpack` configuration file to any pare
75
93
 
76
94
  This strategy also applies to `.devpack_initializers`.
77
95
 
96
+ ### Silencing
97
+
98
+ To prevent _Devpack_ from displaying messages on load, set the environment variable `DEVPACK_SILENT=1` to any value:
99
+ ```bash
100
+ DEVPACK_SILENT=1 bundle exec ruby myapp.rb
101
+ ```
102
+
78
103
  ### Disabling
79
104
 
80
105
  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,20 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ command = ARGV[0]
5
+
6
+ ENV['DEVPACK_DISABLE'] = '1' if command == 'install'
7
+
8
+ require 'open3'
9
+
10
+ require 'devpack'
11
+
12
+ case command
13
+ when 'install'
14
+ require 'devpack/install'
15
+ when 'exec'
16
+ require 'devpack/exec'
17
+ else
18
+ warn "[devpack] Unknown command: #{command}"
19
+ exit 1
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem.module_eval do
4
+ def self.devpack_bin_path(gem_name, command, _version = nil)
5
+ File.join(Gem.loaded_specs[gem_name].full_gem_path, Gem.loaded_specs[gem_name].bindir, command)
6
+ end
7
+
8
+ class << self
9
+ alias_method :_orig_activate_bin_path, :activate_bin_path
10
+ alias_method :_orig_bin_path, :bin_path
11
+
12
+ def activate_bin_path(*args)
13
+ _orig_activate_bin_path(*args)
14
+ rescue Gem::Exception
15
+ devpack_bin_path(*args)
16
+ end
17
+
18
+ def bin_path(*args)
19
+ _orig_bin_path(*args)
20
+ rescue Gem::Exception
21
+ devpack_bin_path(*args)
22
+ end
23
+ end
24
+ end
25
+
26
+ def devpack_exec(args)
27
+ options = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new({ 'keep_file_descriptors' => true })
28
+ Bundler::CLI::Exec.new(options, args).run
29
+ end
30
+
31
+ devpack_exec(ARGV[1..-1])
@@ -44,8 +44,8 @@ module Devpack
44
44
  end
45
45
 
46
46
  def gem_path
47
- return ENV['GEM_PATH'] if ENV.key?('GEM_PATH')
48
- return ENV['GEM_HOME'] if ENV.key?('GEM_HOME')
47
+ return ENV.fetch('GEM_PATH', nil) if ENV.key?('GEM_PATH')
48
+ return ENV.fetch('GEM_HOME', nil) if ENV.key?('GEM_HOME')
49
49
 
50
50
  nil
51
51
  end
@@ -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)
@@ -77,6 +79,7 @@ module Devpack
77
79
 
78
80
  def update_load_path(paths)
79
81
  $LOAD_PATH.concat(paths)
82
+ ENV['RUBYLIB'] = $LOAD_PATH.join(':')
80
83
  end
81
84
  end
82
85
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ missing = Devpack::Gems.new(Devpack.config).tap { |gems| gems.load(silent: true) }.missing
4
+ install_command = "bundle exec gem install -V #{missing.map(&:required_version).join(' ')}" unless missing.empty?
5
+ if install_command.nil?
6
+ Devpack.warn(:info, Devpack::Messages.no_gems_to_install)
7
+ else
8
+ Devpack.warn(:info, install_command)
9
+ output, status = Open3.capture2e(install_command)
10
+ if status.success?
11
+ Devpack.warn(:success, 'Installation complete.')
12
+ else
13
+ Devpack.warn(:error, "Installation failed. Manually verify this command: #{install_command}")
14
+ puts output
15
+ end
16
+ end
17
+ exit 0
@@ -13,24 +13,48 @@ 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(', ')}"
41
+ end
42
+ "Install #{missing.size} missing gem(s): #{command} # [#{grouped_missing.join(', ')}]"
43
+ end
44
+
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
31
51
  end
52
+ alert_incompatible_message(grouped_dependencies)
53
+ end
32
54
 
33
- "Install #{missing.size} missing gem(s): #{color(:cyan) { command(gems) }}"
55
+ def no_gems_to_install
56
+ "No gems to install: #{Devpack::Messages.color(:green) { Devpack.config.requested_gems.size.to_s }} "\
57
+ "gems already installed from #{Devpack::Messages.color(:cyan) { Devpack.config.devpack_path }}"
34
58
  end
35
59
 
36
60
  def test
@@ -52,6 +76,15 @@ module Devpack
52
76
  "bundle exec gem install #{gems.join(' ')}"
53
77
  end
54
78
 
79
+ def alert_incompatible_message(grouped_dependencies)
80
+ incompatible_dependencies = grouped_dependencies.sort.map do |name, dependencies|
81
+ "#{color(:cyan) { name }}: "\
82
+ "#{dependencies.flatten.map { |dependency| color(:yellow) { dependency.to_s } }.join(', ')}"
83
+ end
84
+ "Unable to resolve version conflicts for #{color(:yellow) { incompatible_dependencies.size }} "\
85
+ "dependencies: #{incompatible_dependencies.join(', ')}}"
86
+ end
87
+
55
88
  def palette
56
89
  {
57
90
  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.1'
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.1
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: 2023-01-01 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,12 +116,15 @@ 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
122
+ - lib/devpack/exec.rb
120
123
  - lib/devpack/gem_glob.rb
121
124
  - lib/devpack/gem_spec.rb
122
125
  - lib/devpack/gems.rb
123
126
  - lib/devpack/initializers.rb
127
+ - lib/devpack/install.rb
124
128
  - lib/devpack/messages.rb
125
129
  - lib/devpack/railtie.rb
126
130
  - lib/devpack/timeable.rb
@@ -132,6 +136,7 @@ metadata:
132
136
  homepage_uri: https://github.com/bobf/devpack
133
137
  source_code_uri: https://github.com/bobf/devpack
134
138
  changelog_uri: https://github.com/bobf/devpack/blob/master/CHANGELOG.md
139
+ rubygems_mfa_required: 'true'
135
140
  post_install_message:
136
141
  rdoc_options: []
137
142
  require_paths:
@@ -147,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
152
  - !ruby/object:Gem::Version
148
153
  version: '0'
149
154
  requirements: []
150
- rubyforge_project:
151
- rubygems_version: 2.7.6.2
155
+ rubygems_version: 3.2.3
152
156
  signing_key:
153
157
  specification_version: 4
154
158
  summary: Conveniently tailor your development environment