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 +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +17 -0
- data/README.md +13 -9
- data/devpack.gemspec +1 -1
- data/lib/devpack.rb +9 -3
- data/lib/devpack/config.rb +49 -0
- data/lib/devpack/gem_glob.rb +36 -0
- data/lib/devpack/gem_path.rb +40 -0
- data/lib/devpack/gems.rb +13 -97
- data/lib/devpack/messages.rb +20 -0
- data/lib/devpack/version.rb +1 -1
- metadata +9 -4
- data/lib/devpack/gem_specification_context.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e0060c2ad41fd138a702fdd931eafdd5798940ea173e6a2aa30cb638a9975ee
|
4
|
+
data.tar.gz: 79d3b3d0c6de5c4dfc3795d6bdb6a58d5e0316e95dd85d609a1bcb6f3172f39e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c78cedc44a1fdab0fd5086776e9b6bb9a96c8ccffd34d83cf62e9b1b48da9fb3ae28161bacb048de8fbfc8c438c4c9bb4bcb63a5a56548e437facc6f3d7de042
|
7
|
+
data.tar.gz: e32424c2f6d7cc63fadbff9542c0980469837d0821378987e82830db4262e1c323b62e69c49188b9acf44a2823c78a262697ee383e2887ec748f78bbaee90f28
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -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
|
-
|
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.
|
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
|
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
|
37
|
+
It is recommended that `.devpack` is added to your `.gitignore`.
|
35
38
|
|
36
|
-
|
37
|
-
|
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
|
data/devpack.gemspec
CHANGED
@@ -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 = '
|
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')
|
data/lib/devpack.rb
CHANGED
@@ -3,12 +3,18 @@
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'pathname'
|
5
5
|
|
6
|
-
require 'devpack/
|
6
|
+
require 'devpack/config'
|
7
7
|
require 'devpack/gems'
|
8
|
-
require 'devpack/
|
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
|
-
|
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
|
data/lib/devpack/gems.rb
CHANGED
@@ -1,24 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Devpack
|
4
|
-
#
|
4
|
+
# Loads requested gems from configuration
|
5
5
|
class Gems
|
6
|
-
|
7
|
-
|
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
|
-
|
17
|
-
return [] if path.nil?
|
11
|
+
return [] if @config.requested_gems.nil?
|
18
12
|
|
19
|
-
gems, time = timed { load_devpack
|
13
|
+
gems, time = timed { load_devpack }
|
20
14
|
names = gems.map(&:first)
|
21
|
-
warn(loaded_message(
|
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
|
34
|
-
|
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
|
-
|
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
|
82
|
-
@
|
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
|
-
|
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
|
data/lib/devpack/version.rb
CHANGED
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.
|
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-
|
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:
|
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/
|
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
|