devpack 0.1.0 → 0.1.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: '0853562f5cbb43d560d36d6913f3e1a7bc44ffb49561183146b65a6c69c2f6bb'
4
- data.tar.gz: 63cb7a8c6d3bed3bb8806c7a2b3dcd12a82728cfe177c7e7c8f4f6f64112b071
3
+ metadata.gz: 4e0060c2ad41fd138a702fdd931eafdd5798940ea173e6a2aa30cb638a9975ee
4
+ data.tar.gz: 79d3b3d0c6de5c4dfc3795d6bdb6a58d5e0316e95dd85d609a1bcb6f3172f39e
5
5
  SHA512:
6
- metadata.gz: b814b628151622a7780f74356d77e94528a1053eb14cac5334faa5fb2784ec0114773d0c641cde3091ba0b12dde8010ed74efbed2f457cea5581abb9fc95fbb1
7
- data.tar.gz: 0cea088ec5ba97520eab7734813d972b03cab7332447e3ff3b375716c54bfa87ddcee5168e06fc99c5f469fc4d79a15b599b26d8488e0c8c926ad1871322d62c
6
+ metadata.gz: c78cedc44a1fdab0fd5086776e9b6bb9a96c8ccffd34d83cf62e9b1b48da9fb3ae28161bacb048de8fbfc8c438c4c9bb4bcb63a5a56548e437facc6f3d7de042
7
+ data.tar.gz: e32424c2f6d7cc63fadbff9542c0980469837d0821378987e82830db4262e1c323b62e69c49188b9acf44a2823c78a262697ee383e2887ec748f78bbaee90f28
data/.gitignore CHANGED
@@ -10,3 +10,5 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  Gemfile.lock
13
+
14
+ devpack-*.gem
@@ -2,6 +2,10 @@ Metrics/BlockLength:
2
2
  Exclude:
3
3
  - "spec/**/*"
4
4
 
5
+ AllCops:
6
+ Exclude:
7
+ - "spec/fixtures/**/*"
8
+
5
9
  Layout/EmptyLinesAroundAttributeAccessor:
6
10
  Enabled: true
7
11
  Layout/SpaceAroundMethodCallOperator:
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Core functionality implemented. Load a `.devpack` configuration file from current directory or, if not present, from a parent directory. Attempt to locate and `require` all listed gems.
6
+
7
+ ## 0.1.1
8
+
9
+ Use `GEM_PATH` instead of `GEM_HOME` to locate gems.
10
+
11
+ Optimise load time by searching non-recursively in `/gems` directory (for each path listed in `GEM_PATH`).
12
+
13
+ Load latest version of gem by default. Allow specifying version with rubygems syntax `example:0.1.0`.
14
+
15
+ Permit comments in config file.
16
+
17
+ Use `Gem::Specification` to load "rendered" gemspec (i.e. the file created by rubygems when the gem is installed). This version of the gemspec will load very quickly so no need to do custom gemspec parsing any more. This also accounts for "missing" gemspecs.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Devpack
2
2
 
3
- Conveniently load a set of gems to tailor your development environment without modifying your application's _Gemfile_. Configurable globally or per-project.
3
+ Include a single gem in your `Gemfile` to allow developers to optionally include their preferred set of development gems without cluttering the `Gemfile`. Configurable globally or per-project.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,7 +8,7 @@ Add the gem to your `Gemfile`:
8
8
 
9
9
  ```ruby
10
10
  group :development, :test do
11
- gem 'devpack', '~> 0.1.0'
11
+ gem 'devpack', '~> 0.1.1'
12
12
  end
13
13
  ```
14
14
 
@@ -20,23 +20,27 @@ $ bundle install
20
20
 
21
21
  ## Usage
22
22
 
23
- Create a file named `.devpack` in your project's directory (or any parent directory) containing a list of gems you wish to load:
23
+ Create a file named `.devpack` in your project's directory:
24
24
 
25
25
  ```
26
26
  # .devpack
27
27
  awesome_print
28
28
  byebug
29
29
  better_errors
30
+
31
+ # Optionally specify a version:
32
+ pry:0.13.1
30
33
  ```
31
34
 
32
- All listed gems will be automatically required.
35
+ All listed gems will be automatically required at launch. Any gems that fail to load will generate a warning.
33
36
 
34
- It is recommended that the `.devpack` file is added to your `.gitignore`:
37
+ It is recommended that `.devpack` is added to your `.gitignore`.
35
38
 
36
- ```
37
- # .gitignore
38
- .devpack
39
- ```
39
+ ### Global Configuration
40
+
41
+ To configure globally simply save your `.devpack` configuration file to any parent directory of your project directory, e.g. `~/.devpack`.
42
+
43
+ ### Disabling
40
44
 
41
45
  To disable _Devpack_ set the environment variable `DISABLE_DEVPACK` to any value:
42
46
  ```bash
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['git@bob.frl']
10
10
 
11
11
  spec.summary = 'Conveniently tailor your development environment'
12
- spec.description = 'Provide a list of gems to load in your own environment'
12
+ spec.description = 'Allow developers to optionally include a set of development gems without adding to the Gemfile.'
13
13
  spec.homepage = 'https://github.com/bobf/devpack'
14
14
  spec.license = 'MIT'
15
15
  spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
@@ -3,12 +3,18 @@
3
3
  require 'rubygems'
4
4
  require 'pathname'
5
5
 
6
- require 'devpack/version'
6
+ require 'devpack/config'
7
7
  require 'devpack/gems'
8
- require 'devpack/gem_specification_context'
8
+ require 'devpack/gem_glob'
9
+ require 'devpack/gem_path'
10
+ require 'devpack/messages'
11
+ require 'devpack/version'
9
12
 
10
13
  module Devpack
11
14
  class Error < StandardError; end
12
15
  end
13
16
 
14
- Devpack::Gems.new('.').load unless ENV.key?('DISABLE_DEVPACK')
17
+ unless ENV.key?('DISABLE_DEVPACK')
18
+ config = Devpack::Config.new(Dir.pwd)
19
+ Devpack::Gems.new(config).load
20
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devpack
4
+ # Locates and parses .devpack config file
5
+ class Config
6
+ FILENAME = '.devpack'
7
+ MAX_PARENTS = 100 # Avoid infinite loops (symlinks/weird file systems)
8
+
9
+ def initialize(pwd)
10
+ @pwd = Pathname.new(pwd)
11
+ end
12
+
13
+ def requested_gems
14
+ return nil if devpack_path.nil?
15
+
16
+ File.readlines(devpack_path)
17
+ .map(&filter_comments)
18
+ .compact
19
+ end
20
+
21
+ def devpack_path
22
+ @devpack_path ||= located_config_path(@pwd)
23
+ end
24
+
25
+ private
26
+
27
+ def located_config_path(next_parent)
28
+ loop.with_index(1) do |_, index|
29
+ return nil if index > MAX_PARENTS
30
+
31
+ path = next_parent.join(FILENAME)
32
+ next_parent = next_parent.parent
33
+ next unless File.exist?(path)
34
+
35
+ return path
36
+ end
37
+ end
38
+
39
+ def filter_comments
40
+ proc do |line|
41
+ stripped = line.strip
42
+ next nil if stripped.empty?
43
+ next nil if stripped.start_with?('#')
44
+
45
+ stripped.gsub(/\s*#.*$/, '') # Remove inline comments (like this one)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devpack
4
+ # Locates gems by searching in paths listed in GEM_PATH
5
+ class GemGlob
6
+ def find(name)
7
+ glob.select do |path|
8
+ pathname = Pathname.new(path)
9
+ next unless pathname.directory?
10
+
11
+ basename = pathname.basename.to_s
12
+ match?(name, basename)
13
+ end.max # FIXME: Quick-and-dirty way to get latest version - will have many edge cases.
14
+ end
15
+
16
+ private
17
+
18
+ def glob
19
+ @glob ||= gem_paths.map { |path| Dir.glob(path.join('gems', '*')) }.flatten
20
+ end
21
+
22
+ def gem_paths
23
+ return [] unless ENV.key?('GEM_PATH')
24
+
25
+ ENV.fetch('GEM_PATH').split(':').map { |path| Pathname.new(path) }
26
+ end
27
+
28
+ def match?(name_with_version, basename)
29
+ name, _, version = name_with_version.partition(':')
30
+ return true if version.empty? && basename.start_with?("#{name}-")
31
+ return true if !version.empty? && basename == "#{name}-#{version}"
32
+
33
+ false
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devpack
4
+ # Locates relevant gemspec for a given gem and provides a full list of paths
5
+ # for all `require_paths` listed in gemspec.
6
+ class GemPath
7
+ def initialize(glob, name)
8
+ @name = name
9
+ @glob = glob
10
+ end
11
+
12
+ def require_paths
13
+ return [] unless gemspec_path&.exist? && gem_path&.exist?
14
+
15
+ Gem::Specification
16
+ .load(gemspec_path.to_s)
17
+ .require_paths
18
+ .map { |path| gem_path.join(path).to_s }
19
+ end
20
+
21
+ private
22
+
23
+ def gem_path
24
+ return nil if located_gem.nil?
25
+
26
+ Pathname.new(located_gem)
27
+ end
28
+
29
+ def gemspec_path
30
+ return nil if gem_path.nil?
31
+
32
+ gem_path.join('..', '..', 'specifications', "#{gem_path.basename}.gemspec")
33
+ .expand_path
34
+ end
35
+
36
+ def located_gem
37
+ @located_gem ||= @glob.find(@name)
38
+ end
39
+ end
40
+ end
@@ -1,24 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Devpack
4
- # Parses .devpack file and provides/loads a list of specified gems
4
+ # Loads requested gems from configuration
5
5
  class Gems
6
- FILENAME = '.devpack'
7
- MAX_PARENTS = 100 # Avoid infinite loops (symlinks/weird file systems)
8
-
9
- def initialize(path)
10
- @load_path = $LOAD_PATH
11
- @gem_home = Pathname.new(ENV['GEM_HOME'])
12
- @path = Pathname.new(path)
6
+ def initialize(config)
7
+ @config = config
13
8
  end
14
9
 
15
10
  def load
16
- path = devpack_path
17
- return [] if path.nil?
11
+ return [] if @config.requested_gems.nil?
18
12
 
19
- gems, time = timed { load_devpack(path) }
13
+ gems, time = timed { load_devpack }
20
14
  names = gems.map(&:first)
21
- warn(loaded_message(path, gems, time))
15
+ warn(Messages.loaded_message(@config.devpack_path, gems, time.round(2)))
22
16
  names
23
17
  end
24
18
 
@@ -30,106 +24,28 @@ module Devpack
30
24
  [result, Time.now.utc - start]
31
25
  end
32
26
 
33
- def load_devpack(path)
34
- gem_list(path).map { |name| load_gem(name) }.compact
35
- end
36
-
37
- def devpack_path
38
- return default_devpack_path if File.exist?(default_devpack_path)
39
- return parent_devpack_path unless parent_devpack_path.nil?
40
-
41
- nil
42
- end
43
-
44
- def default_devpack_path
45
- @path.join(FILENAME)
46
- end
47
-
48
- def gem_list(path)
49
- File.readlines(path).map(&:chomp)
27
+ def load_devpack
28
+ @config.requested_gems.map { |name| load_gem(name) }.compact
50
29
  end
51
30
 
52
31
  def load_gem(name)
53
- # TODO: Decide what to do when Bundler is not defined.
54
- # Do we want to support this scenario ?
55
- update_load_path(name) if defined?(Bundler)
32
+ update_load_path(name)
56
33
  [name, Kernel.require(name)]
57
34
  rescue LoadError
58
- warn(failure_message(name))
35
+ warn(Messages.failure_message(name))
59
36
  nil
60
37
  end
61
38
 
62
- def parent_devpack_path
63
- next_parent = @path.parent
64
- loop.with_index(1) do |_, index|
65
- break if index >= MAX_PARENTS
66
-
67
- next_parent = next_parent.parent
68
- break if next_parent == next_parent.parent
69
-
70
- path = next_parent.join(FILENAME)
71
- next unless File.exist?(path)
72
-
73
- return path
74
- end
75
- end
76
-
77
39
  def warn(message)
78
40
  Kernel.warn("[devpack] #{message}")
79
41
  end
80
42
 
81
- def gems_glob
82
- @gems_glob ||= Dir.glob(@gem_home.join('gems', '**', '*'))
83
- end
84
-
85
- def gem_path(name)
86
- found = gems_glob.find do |path|
87
- pathname = Pathname.new(path)
88
- next unless pathname.directory?
89
-
90
- # TODO: We should allow optionally specifying a version and default to loading
91
- # the latest version available.
92
- pathname.basename.to_s.start_with?("#{name}-")
93
- end
94
-
95
- found.nil? ? nil : Pathname.new(found)
43
+ def gem_glob
44
+ @gem_glob ||= GemGlob.new
96
45
  end
97
46
 
98
47
  def update_load_path(name)
99
- path = gem_path(name)
100
- return if path.nil?
101
-
102
- $LOAD_PATH.concat(require_paths(path, name))
103
- end
104
-
105
- def require_paths(gem_path, name)
106
- gemspec_path = gem_path.join("#{name}.gemspec")
107
- lib_path = gem_path.join('lib')
108
- # REVIEW: Some gems don't have a .gemspec - need to understand how they are loaded.
109
- # Use `/lib` for now if it exists as this will work for vast majority of cases.
110
- return [lib_path] if !gemspec_path.exist? && lib_path.exist?
111
-
112
- gemspec = File.read(gemspec_path.to_s)
113
- GemSpecificationContext.class_eval(gemspec)
114
- full_require_paths(gem_path)
115
- end
116
-
117
- def full_require_paths(base_path)
118
- GemSpecificationContext.require_paths.map { |path| base_path.join(path) }
119
- end
120
-
121
- def failure_message(name)
122
- base = "Failed to load `#{name}`"
123
- install = (defined?(Bundler) ? 'bundle exec ' : '') + "gem install #{name}"
124
- "#{base}. Try `#{install}`"
125
- end
126
-
127
- def loaded_message(path, gems, time)
128
- already_loaded = gems.size - gems.reject { |_, loaded| loaded }.size
129
- base = "Loaded #{already_loaded} development gem(s) from '#{path}' in #{time} seconds"
130
- return "#{base}." if already_loaded == gems.size
131
-
132
- "#{base} (#{gems.size - already_loaded} gem(s) were already loaded by environment)."
48
+ $LOAD_PATH.concat(GemPath.new(gem_glob, name).require_paths)
133
49
  end
134
50
  end
135
51
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devpack
4
+ # Generates output messages.
5
+ class Messages
6
+ def self.failure_message(name)
7
+ base = "Failed to load `#{name}`"
8
+ install = "bundle exec gem install #{name}"
9
+ "#{base}. Try `#{install}`"
10
+ end
11
+
12
+ def self.loaded_message(path, gems, time)
13
+ already_loaded = gems.size - gems.reject { |_, loaded| loaded }.size
14
+ base = "Loaded #{already_loaded} development gem(s) from '#{path}' in #{time} seconds"
15
+ return "#{base}." if already_loaded == gems.size
16
+
17
+ "#{base} (#{gems.size - already_loaded} gem(s) were already loaded by environment)."
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Devpack
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-04 00:00:00.000000000 Z
11
+ date: 2020-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: byebug
@@ -80,7 +80,8 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.4.4
83
- description: Provide a list of gems to load in your own environment
83
+ description: Allow developers to optionally include a set of development gems without
84
+ adding to the Gemfile.
84
85
  email:
85
86
  - git@bob.frl
86
87
  executables: []
@@ -90,6 +91,7 @@ files:
90
91
  - ".gitignore"
91
92
  - ".rspec"
92
93
  - ".rubocop.yml"
94
+ - CHANGELOG.md
93
95
  - Gemfile
94
96
  - LICENSE.txt
95
97
  - Makefile
@@ -99,8 +101,11 @@ files:
99
101
  - bin/setup
100
102
  - devpack.gemspec
101
103
  - lib/devpack.rb
102
- - lib/devpack/gem_specification_context.rb
104
+ - lib/devpack/config.rb
105
+ - lib/devpack/gem_glob.rb
106
+ - lib/devpack/gem_path.rb
103
107
  - lib/devpack/gems.rb
108
+ - lib/devpack/messages.rb
104
109
  - lib/devpack/version.rb
105
110
  homepage: https://github.com/bobf/devpack
106
111
  licenses:
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Devpack
4
- # Stubbed instance_eval context to evaluate a .gemspec file and extract required_paths
5
- # Avoids doing expensive operations (e.g. attempting to run `git ls-files`) which are
6
- # typically invoked when using `Gem::Specification.load`.
7
- class GemSpecificationContext
8
- def initialize(*_); end
9
-
10
- class << self
11
- attr_accessor :require_paths
12
-
13
- @require_paths = []
14
-
15
- def require(*_); end
16
-
17
- def __dir__
18
- '.'
19
- end
20
-
21
- def require_relative(*_); end
22
- end
23
-
24
- module Gem
25
- # Stubbed Rubygems Gem::Specification. Everything except `require_paths=` is a no-op.
26
- # Catches errors for missing constants and attempts to set them in the class_eval context.
27
- # Constants are set recursively by setting each constant to GemSpecificationContext.
28
- class Specification
29
- def initialize(*_)
30
- GemSpecificationContext.require_paths = []
31
- begin
32
- yield self
33
- rescue NameError => e
34
- __devpack_resolved_receiver(e).const_set(e.name, GemSpecificationContext)
35
- retry
36
- end
37
- end
38
-
39
- def require_paths=(paths)
40
- GemSpecificationContext.require_paths.concat(paths)
41
- end
42
-
43
- private
44
-
45
- def __devpack_resolved_receiver(error)
46
- error.receiver
47
- rescue ArgumentError
48
- GemSpecificationContext
49
- end
50
-
51
- # rubocop:disable Style/MethodMissingSuper
52
- def method_missing(method_name, *args)
53
- return self unless Kernel.respond_to?(method_name)
54
-
55
- Kernel.public_send(method_name, *args)
56
- end
57
- # rubocop:enable Style/MethodMissingSuper
58
-
59
- def respond_to_missing?(*_)
60
- true
61
- end
62
- end
63
- end
64
- end
65
- end