Skip to content
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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 81 additions & 80 deletions bin/bundix
Original file line number Diff line number Diff line change
@@ -1,92 +1,93 @@
#!/usr/bin/env ruby
#! /usr/bin/env ruby
Copy link
Member

@zimbatm zimbatm Apr 18, 2016

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.

Copy link
Collaborator Author

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.


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
Copy link
Member

Choose a reason for hiding this comment

The 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 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
2 changes: 1 addition & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ let
};
in stdenv.mkDerivation {
name = "bundix";
buildInputs = [bundix];
buildInputs = [bundler bundix];
}
65 changes: 38 additions & 27 deletions lib/bundix.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,84 @@
require 'json'
require 'open-uri'
require 'open3'
require 'tsort'
require 'set'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where you're using set and/or tsort here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my ctrl+f was off. So I see Set in use, but not TSort.

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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -110,6 +117,10 @@ def self.object2nix(obj, level = 2, out = '')
end
end

def sh(*args)
self.class.sh(*args)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also define the main method here and then use module_function(:sh) to export it to the module level.


def self.sh(*args)
out, status = Open3.capture2e(*args)
unless status.success?
Expand Down
66 changes: 66 additions & 0 deletions lib/bundix/gemfile_dependency_tree.rb
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 = {})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seen should probably be a Set.

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
Loading