Skip to content

Instantly share code, notes, and snippets.

@manveru
Last active January 27, 2016 21:46
Show Gist options
  • Save manveru/98d779e92ec4b9c02ae2 to your computer and use it in GitHub Desktop.
Save manveru/98d779e92ec4b9c02ae2 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'bundler'
require 'json'
require 'open-uri'
require 'open3'
require 'pp'
class Bundix
VERSION = '2.0.0'
NIX_INSTANTIATE = 'nix-instantiate'
NIX_PREFETCH_URL = 'nix-prefetch-url'
NIX_PREFETCH_GIT = 'nix-prefetch-git'
NIX_HASH = 'nix-hash'
SHA256_32 = %r(^[a-z0-9]{52}$)
SHA256_16 = %r(^[a-f0-9]{64}$)
attr_reader :options
def initialize(options)
@options = {
gemset: './gemset.nix',
lockfile: './Gemfile.lock',
quiet: false,
tempfile: nil,
deps: false
}.merge(options)
end
def convert
cache = parse_gemset
lock = parse_lockfile
# reverse so git comes last
lock.specs.reverse_each.with_object({}) do |spec, gems|
name, cached = cache.find{|k,v|
k == spec.name &&
v['version'] == spec.version.to_s &&
v.dig('source', 'sha256').to_s.size == 52
}
if cached
gems[name] = cached
next
end
gems[spec.name] = {
version: spec.version.to_s,
source: Source.new(spec).convert
}
if options[:deps] && spec.dependencies.any?
gems[spec.name][:dependencies] = spec.dependencies.map(&:name) - ['bundler']
end
end
end
def parse_gemset
return {} unless File.file?(options[:gemset])
json = Bundix.sh(NIX_INSTANTIATE, '--eval', '-E',
"builtins.toJSON(import #{options[: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
out << "{\n"
obj.each do |k, v|
out << ' ' * level
if k.to_s =~ /^[a-zA-Z_-]+[a-zA-Z0-9_-]*$/
out << k.to_s
else
object2nix(k, level + 2, out)
end
out << ' = '
object2nix(v, level + 2, out)
out << (v.is_a?(Hash) ? "\n" : ";\n")
end
out << (' ' * (level - 2)) << (level == 2 ? '}' : '};')
when Array
out << '[' << obj.map{|o| o.to_str.dump }.join(' ') << ']'
when String
out << obj.dump
when Symbol
out << obj.to_s.dump
when true, false
out << obj.to_s
else
fail obj.inspect
end
end
def self.sh(*args)
out, status = Open3.capture2e(*args)
unless status.success?
puts "$ #{args.join(' ')}" if $VERBOSE
puts out if $VERBOSE
fail "command execution failed: #{status}"
end
out
end
class Source < Struct.new(:spec)
def convert
case spec.source
when Bundler::Source::Rubygems
convert_rubygems
when Bundler::Source::Git
convert_git
else
pp spec
fail 'unkown bundler source'
end
end
def sh(*args)
Bundix.sh(*args)
end
def nix_prefetch_url(*args)
sh(NIX_PREFETCH_URL, '--type', 'sha256', *args)
end
def nix_prefetch_git(uri, revision)
home = ENV['HOME']
ENV['HOME'] = '/homeless-shelter'
sh(NIX_PREFETCH_GIT, '--url', uri, '--rev', revision, '--hash', 'sha256', '--leave-dotGit')
ensure
ENV['HOME'] = home
end
def fetch_local_hash(spec)
spec.source.caches.each do |cache|
path = File.join(cache, "#{spec.name}-#{spec.version}.gem")
next unless File.file?(path)
begin
return nix_prefetch_url("file://#{path}")[SHA256_32]
rescue
end
end
nil
end
def fetch_remotes_hash(spec, remotes)
remotes.each do |remote|
begin
return fetch_remote_hash(spec, remote)
rescue
end
end
nil
end
def fetch_remote_hash(spec, remote)
hash = nil
if URI(remote).host == 'rubygems.org'
uri = "#{remote}/api/v1/versions/#{spec.name}.json"
puts "Getting SHA256 from: #{uri}" if $VERBOSE
open uri do |io|
versions = JSON.parse(io.read)
if found_version = versions.find{|obj| obj['number'] == spec.version.to_s }
hash = found_version['sha']
break
end
end
end
uri = "#{remote}/gems/#{spec.name}-#{spec.version}.gem"
if hash
begin
nix_prefetch_url(uri, hash)[SHA256_16]
rescue
nix_prefetch_url(uri)[SHA256_32]
end
else
nix_prefetch_url(uri)[SHA256_32]
end
end
def convert_rubygems
remotes = spec.source.remotes.map{|remote| remote.to_s.sub(/\/+$/, '') }
hash = fetch_local_hash(spec) || fetch_remotes_hash(spec, remotes)
hash = sh(NIX_HASH, '--type', 'sha256', '--to-base32', hash)[SHA256_32]
fail "couldn't fetch hash for #{spec.name}-#{spec.version}" unless hash
puts "#{hash} => #{spec.name}-#{spec.version}.gem" if $VERBOSE
{
type: 'gem',
remotes: remotes,
sha256: hash
}
end
def convert_git
revision = spec.source.options.fetch('revision')
uri = spec.source.options.fetch('uri')
hash = nix_prefetch_git(uri, revision)[/^\h{64}$/m]
hash = sh(NIX_HASH, '--type', 'sha256', '--to-base32', hash)[SHA256_32]
fail "couldn't fetch hash for #{spec.name}-#{spec.version}" unless hash
puts "#{hash} => #{uri}" if $VERBOSE
{
type: 'git',
url: uri.to_s,
rev: revision,
sha256: hash,
fetchSubmodules: false
}
end
end
end
exit unless $PROGRAM_NAME == __FILE__
require 'optparse'
options = {}
op = OptionParser.new do |o|
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
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
op.parse!
$VERBOSE = !options[:quiet]
gemset = Bundix.new(options).convert
tempfile = Tempfile.new('gemset.nix', encoding: 'UTF-8')
begin
Bundix.object2nix(gemset, 2, tempfile)
tempfile.close
FileUtils.cp(tempfile.path, options[:gemset])
ensure
tempfile.close!
tempfile.unlink
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment