-
-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
resolve Gemfile to support groups #3
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,92 +1,93 @@ | ||
#!/usr/bin/env ruby | ||
#! /usr/bin/env ruby | ||
|
||
require 'optparse' | ||
require 'tmpdir' | ||
require 'pathname' | ||
|
||
require_relative '../lib/bundix' | ||
|
||
options = { | ||
ruby: 'ruby', | ||
bundle_pack_path: 'vendor/bundle', | ||
lockfile: 'Gemfile.lock', | ||
gemset: 'gemset.nix' | ||
} | ||
|
||
op = OptionParser.new do |o| | ||
o.on '-m', '--magic', 'lock, pack, and write dependencies' do | ||
options[:magic] = true | ||
end | ||
|
||
o.on '--ruby=ruby', 'ruby version to use for magic and init, defaults to latest' do |value| | ||
options[:ruby] = value | ||
end | ||
|
||
o.on '--bundle-pack-path=vendor/bundle', "path to pack the magic" do |value| | ||
options[:bundle_pack_path] = value | ||
require_relative '../lib/bundix/version' | ||
|
||
class Bundix | ||
OPTIONS = { | ||
ruby: 'ruby', | ||
bundle_pack_path: 'vendor/bundle', | ||
gemfile: 'Gemfile', | ||
lockfile: 'Gemfile.lock', | ||
gemset: 'gemset.nix', | ||
lock: false, | ||
cache: false, | ||
groups: [] | ||
} | ||
|
||
op = OptionParser.new do |o| | ||
o.on '--ruby=ruby', 'ruby version to use for init, defaults to latest' do |value| | ||
OPTIONS[:ruby] = value | ||
end | ||
|
||
o.on '-i', '--init', "initialize a new shell.nix for nix-shell (won't overwrite old ones)" do | ||
OPTIONS[:init] = true | ||
end | ||
|
||
o.on '--gemset=gemset.nix', 'path to the gemset.nix' do |value| | ||
OPTIONS[:gemset] = File.expand_path(value) | ||
end | ||
|
||
o.on '--gemfile=Gemfile', 'path to the Gemfile' do |value| | ||
OPTIONS[:gemfile] = File.expand_path(value) | ||
end | ||
|
||
o.on '--lockfile=Gemfile.lock', 'path to the Gemfile.lock' do |value| | ||
OPTIONS[:lockfile] = File.expand_path(value) | ||
end | ||
|
||
o.on '-l', '--lock', 'create Gemfile.lock for given groups' do |groups| | ||
OPTIONS[:lock] = true | ||
end | ||
|
||
o.on '-c', '--cache', 'resolve dependencies from cache' do | ||
OPTIONS[:cache] = true | ||
end | ||
|
||
o.on '-g', '--groups [GROUP1,GROUP2]', Array, 'only use these groups for the lockfile' do |groups| | ||
OPTIONS[:groups].concat groups.map(&:to_sym) | ||
end | ||
|
||
o.on '-q', '--quiet', 'only output errors' do | ||
OPTIONS[:quiet] = true | ||
end | ||
|
||
o.on '-v', '--version', 'show the version of bundix' do | ||
puts Bundix::VERSION | ||
exit | ||
end | ||
end | ||
|
||
o.on '-i', '--init', "initialize a new shell.nix for nix-shell (won't overwrite old ones)" do | ||
options[:init] = true | ||
op.parse! | ||
$VERBOSE = !OPTIONS[:quiet] | ||
|
||
require_relative '../lib/bundix' | ||
|
||
if OPTIONS[:init] | ||
if File.file?('shell.nix') | ||
warn "won't override existing shell.nix" | ||
else | ||
shell_nix = File.read(File.expand_path('../template/shell.nix', __dir__)) | ||
shell_nix.gsub!('PROJECT', File.basename(Dir.pwd)) | ||
shell_nix.gsub!('RUBY', OPTIONS[:ruby]) | ||
shell_nix.gsub!('LOCKFILE', "./#{Pathname(OPTIONS[:lockfile]).relative_path_from(Pathname('./'))}") | ||
shell_nix.gsub!('GEMSET', "./#{Pathname(OPTIONS[:gemset]).relative_path_from(Pathname('./'))}") | ||
File.write('shell.nix', shell_nix) | ||
end | ||
end | ||
|
||
o.on '--gemset=gemset.nix', 'path to the gemset.nix' do |value| | ||
options[:gemset] = File.expand_path(value) | ||
end | ||
|
||
o.on '--lockfile=Gemfile.lock', 'path to the Gemfile.lock' do |value| | ||
options[:lockfile] = File.expand_path(value) | ||
end | ||
|
||
o.on '-d', '--dependencies', 'include gem dependencies' do | ||
options[:deps] = true | ||
end | ||
gemset = Bundix.new(OPTIONS).convert | ||
|
||
o.on '-q', '--quiet', 'only output errors' do | ||
options[:quiet] = true | ||
tempfile = Tempfile.new('gemset.nix', encoding: 'UTF-8') | ||
begin | ||
Bundix.object2nix(gemset, 2, tempfile) | ||
tempfile.flush | ||
FileUtils.cp(tempfile.path, OPTIONS[:gemset]) | ||
ensure | ||
tempfile.close! | ||
tempfile.unlink | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't you want to move the option parsing in the lib instead ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe, once we have some tests :P |
||
|
||
o.on '-v', '--version', 'show the version of bundix' do | ||
puts Bundix::VERSION | ||
exit | ||
end | ||
end | ||
|
||
op.parse! | ||
$VERBOSE = !options[:quiet] | ||
|
||
if options[:magic] | ||
fail unless system( | ||
Bundix::NIX_SHELL, '-p', options[:ruby], | ||
"bundler.override { ruby = #{options[:ruby]}; }", | ||
"--command", "bundle lock --lockfile=#{options[:lockfile]}") | ||
fail unless system( | ||
Bundix::NIX_SHELL, '-p', options[:ruby], | ||
"bundler.override { ruby = #{options[:ruby]}; }", | ||
"--command", "bundle pack --all --path #{options[:bundle_pack_path]}") | ||
end | ||
|
||
if options[:init] | ||
if File.file?('shell.nix') | ||
warn "won't override existing shell.nix" | ||
else | ||
shell_nix = File.read(File.expand_path('../template/shell.nix', __dir__)) | ||
shell_nix.gsub!('PROJECT', File.basename(Dir.pwd)) | ||
shell_nix.gsub!('RUBY', options[:ruby]) | ||
shell_nix.gsub!('LOCKFILE', "./#{Pathname(options[:lockfile]).relative_path_from(Pathname('./'))}") | ||
shell_nix.gsub!('GEMSET', "./#{Pathname(options[:gemset]).relative_path_from(Pathname('./'))}") | ||
File.write('shell.nix', shell_nix) | ||
end | ||
end | ||
|
||
gemset = Bundix.new(options).convert | ||
|
||
tempfile = Tempfile.new('gemset.nix', encoding: 'UTF-8') | ||
begin | ||
Bundix.object2nix(gemset, 2, tempfile) | ||
tempfile.flush | ||
FileUtils.cp(tempfile.path, options[:gemset]) | ||
ensure | ||
tempfile.close! | ||
tempfile.unlink | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,5 +11,5 @@ let | |
}; | ||
in stdenv.mkDerivation { | ||
name = "bundix"; | ||
buildInputs = [bundix]; | ||
buildInputs = [bundler bundix]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,73 +2,84 @@ | |
require 'json' | ||
require 'open-uri' | ||
require 'open3' | ||
require 'tsort' | ||
require 'set' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see where you're using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, my |
||
require 'pp' | ||
|
||
require_relative 'bundix/version' | ||
require_relative 'bundix/source' | ||
require_relative 'bundix/gemfile_dependency_tree' | ||
|
||
class Bundix | ||
NIX_INSTANTIATE = 'nix-instantiate' | ||
NIX_PREFETCH_URL = 'nix-prefetch-url' | ||
NIX_PREFETCH_GIT = 'nix-prefetch-git' | ||
NIX_HASH = 'nix-hash' | ||
NIX_SHELL = 'nix-shell' | ||
|
||
SHA256_32 = %r(^[a-z0-9]{52}$) | ||
SHA256_16 = %r(^[a-f0-9]{64}$) | ||
|
||
attr_reader :options | ||
|
||
def initialize(options) | ||
@options = { | ||
quiet: false, | ||
tempfile: nil, | ||
gemfile: Bundler.default_gemfile, | ||
lockfile: Bundler.default_lockfile, | ||
lock: false, | ||
deps: false | ||
}.merge(options) | ||
end | ||
|
||
def convert | ||
cache = parse_gemset | ||
lock = parse_lockfile | ||
@cache = parse_gemset | ||
puts "resolving dependencies..." if $VERBOSE | ||
tree = GemfileDependencyTree.run(options) | ||
gems = {} | ||
|
||
tree.each do |name, node| | ||
gems[name] = convert_one(name, node) | ||
@cache[name] = gems[name] | ||
end | ||
|
||
# reverse so git comes last | ||
lock.specs.reverse_each.with_object({}) do |spec, gems| | ||
gem = find_cached_spec(spec, cache) || convert_spec(spec, cache) | ||
gems.merge!(gem) | ||
gems | ||
end | ||
|
||
if options[:deps] && spec.dependencies.any? | ||
gems[spec.name]['dependencies'] = spec.dependencies.map(&:name) - ['bundler'] | ||
end | ||
end | ||
def convert_one(name, node) | ||
find_cached_spec(node) || convert_spec(node) | ||
end | ||
|
||
def convert_spec(spec, cache) | ||
{spec.name => {version: spec.version.to_s, source: Source.new(spec).convert}} | ||
def convert_spec(spec, definition = nil) | ||
{ | ||
'version' => spec.version.to_s, | ||
'groups' => spec.groups, | ||
'dependencies' => spec.dependencies, | ||
'source' => Source.new(spec, definition).convert | ||
} | ||
rescue => ex | ||
warn "Skipping #{spec.name}: #{ex}" | ||
puts ex.backtrace | ||
{spec.name => {}} | ||
{} | ||
end | ||
|
||
def find_cached_spec(spec, cache) | ||
name, cached = cache.find{|k, v| | ||
next unless k == spec.name | ||
def find_cached_spec(node) | ||
_, cached = @cache.find{|k, v| | ||
next unless k == node.name | ||
next unless cached_source = v['source'] | ||
|
||
case spec_source = spec.source | ||
case spec_source = node.source | ||
when Bundler::Source::Git | ||
next unless cached_rev = cached_source['rev'] | ||
next unless spec_rev = spec_source.options['revision'] | ||
spec_rev == cached_rev | ||
when Bundler::Source::Rubygems | ||
v['version'] == spec.version.to_s | ||
v['version'] == node.version | ||
end | ||
} | ||
|
||
{name => cached} if cached | ||
cached | ||
end | ||
|
||
|
||
def parse_gemset | ||
path = File.expand_path(options[:gemset]) | ||
return {} unless File.file?(path) | ||
|
@@ -77,10 +88,6 @@ def parse_gemset | |
JSON.parse(json.strip.gsub(/\\"/, '"')[1..-2]) | ||
end | ||
|
||
def parse_lockfile | ||
Bundler::LockfileParser.new(File.read(options[:lockfile])) | ||
end | ||
|
||
def self.object2nix(obj, level = 2, out = '') | ||
case obj | ||
when Hash | ||
|
@@ -97,7 +104,7 @@ def self.object2nix(obj, level = 2, out = '') | |
out << (v.is_a?(Hash) ? "\n" : ";\n") | ||
end | ||
out << (' ' * (level - 2)) << (level == 2 ? '}' : '};') | ||
when Array | ||
when Array, Set | ||
out << '[' << obj.sort.map{|o| o.to_str.dump }.join(' ') << ']' | ||
when String | ||
out << obj.dump | ||
|
@@ -110,6 +117,10 @@ def self.object2nix(obj, level = 2, out = '') | |
end | ||
end | ||
|
||
def sh(*args) | ||
self.class.sh(*args) | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could also define the main method here and then use |
||
|
||
def self.sh(*args) | ||
out, status = Open3.capture2e(*args) | ||
unless status.success? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
require 'bundler' | ||
|
||
class GemfileDependencyTree | ||
Spec = Struct.new(:name, :groups, :source, :version, :dependencies) | ||
|
||
def self.run(options) | ||
definitions = Bundler::Dsl.evaluate(options.fetch(:gemfile), nil, {}) | ||
specs = | ||
if options.fetch(:cache) | ||
definitions.resolve_with_cache! | ||
else | ||
definitions.resolve_remotely! | ||
end | ||
definitions.lock(Bundler.default_lockfile) if options.fetch(:lock) | ||
|
||
result = {} | ||
definitions.dependencies.each do |dependency| | ||
new(dependency, specs, dependency.groups).run([], result) | ||
end | ||
|
||
result | ||
end | ||
|
||
def initialize(dep, specs, groups) | ||
@dep = dep | ||
@spec = specs.find{|s| s.name == dep.name } | ||
@groups = groups.map(&:to_s) | ||
@children = dependencies.map{|d| self.class.new(d, specs, groups) } | ||
end | ||
|
||
def run(seen = [], result = {}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
children = @children.reject{|c| seen.include?(c.name) } | ||
add_group(result, @spec) | ||
|
||
children.each do |child| | ||
seen << child.name | ||
child.run(seen, result) | ||
end | ||
|
||
result | ||
end | ||
|
||
def name | ||
@spec.name | ||
end | ||
|
||
private | ||
|
||
def add_group(result, dep) | ||
if result[dep.name] | ||
result[dep.name].groups |= @groups | ||
else | ||
result[dep.name] = Spec.new( | ||
dep.name, | ||
@groups, | ||
dep.source, | ||
dep.version.to_s, | ||
dependencies.map(&:name) | ||
) | ||
end | ||
end | ||
|
||
def dependencies | ||
@spec.dependencies.reject{|d| d.type == :development } | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only place where I see this space is in the nix world. Do you know the motivation behind ? It's not wrong, I'm just curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just think it's more readable this way, don't care much either way, this is a change left over from an experiment using the
nix-shell
shebang for ruby, which unfortunately is still blocked by NixOS/nix#877 with a pending PR.