Skip to content

Instantly share code, notes, and snippets.

@aflatter
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aflatter/5ab21c9b032f84c27361 to your computer and use it in GitHub Desktop.
Save aflatter/5ab21c9b032f84c27361 to your computer and use it in GitHub Desktop.
Writes a gems.nix for use with the nixpkgs/pleasant-ruby
require 'bundler'
require 'erb'
# Generates a Nix expression for bundler-managed dependencies.
module Bundix
class << self
def run(gemfile = './Gemfile', lockfile = 'Gemfile.lock')
gems = resolve_deps(gemfile, lockfile)
checksums = compute_checksums(gems)
write_expr(gems, checksums)
end
def resolve_deps(gemfile, lockfile)
definition = Bundler::Definition.build(gemfile, lockfile, {})
definition.resolve.tsort.map(&Bundix::Gem.method(:new))
end
def compute_checksums(gems)
Checksummer.run(gems)
end
def write_expr(gems, checksums)
template = File.read(__FILE__).split('__END__').last.strip
ERB.new(template, nil, '->').result(binding)
end
def to_attr_name(str)
# TODO: This could lead to naming conflicts. Investigate.
str.gsub('.', '_')
end
end
class Dependency
def initialize(dependency)
@dependency = dependency
end
def attr_name
Bundix.to_attr_name(@dependency.name)
end
end
class Gem
def initialize(spec)
@spec = spec
end
def attr_name
Bundix.to_attr_name(@spec.name)
end
def drv_name
"#{@spec.name}-#{version}"
end
def version
@spec.version.to_s
end
def url
"http://rubygems.org/downloads/#{filename}"
end
def filename
"#{@spec.name}-#{@spec.version}.gem"
end
def dependencies
@spec.dependencies.map { |dep| Dependency.new(dep) }
end
end
# Uses nix-prefetch-scripts to get the checksums of a set of gems.
# The results are cached in order to speed up repeated runs.
class Checksummer
class << self
def run(gems)
new(gems).run
end
end
def initialize(gems, cache_path = '.bundix/checksums')
@gems = gems
@cache_path = cache_path
end
def run
cache = load_cache
result = @gems.each_with_object(Hash.new) do |gem, hash|
drv_name = gem.drv_name
hash[drv_name] = cache[drv_name] || prefetch(gem.url)
end
write_cache(result)
result
end
def load_cache
File.readlines(@cache_path).each_with_object(Hash.new) do |line, hash|
drv_name, checksum = line.split(':').map(&:strip)
hash[drv_name] = checksum
end
end
def prefetch(url)
output = `nix-prefetch-url #{url} 2>/dev/null`.strip
raise "Prefetch failed: #{url}" unless $?.success?
unless output.length == 52 && output =~ /^[a-z0-9]+$/
raise "Invalid checksum for #{url}: #{output}"
end
output
end
def write_cache(checksums)
File.open(@cache_path, 'w') do |file|
checksums.each do |drv_name, checksum|
file.write("#{drv_name}: #{checksum}\n")
end
end
end
end
end
puts Bundix.run
__END__
{ }:
rec {
<%- gems.each do |gem| %>
<%= gem.attr_name %> = {
name = "<%= gem.drv_name %>";
sha256 = "<%= checksums[gem.drv_name] %>";
<%- if gem.dependencies.any? -%>
dependencies = [
<%= gem.dependencies.map { |dep| %Q{"#{dep.attr_name}"} }.join("\n ") %>
];
<%- end -%>
};
<%- end -%>
bundler = {
name = "bundler-1.7.1";
sha256 = "144yqbmi89gl933rh8dv58bm7ia14s4a098qdi2z0q09ank9n5h2";
};
buildInputs = [
<%- gems.each do |gem| -%>
"<%= gem.attr_name %>"
<%- end -%>
];
}
@pikajude
Copy link

This needs to create .bundix/checksums if it doesn't already exist.

@pikajude
Copy link

Also, no indication of progress while it's running. I know that might be difficult to do; in that case, why not piggyback on Bundler's existing progress reporting?

@cstrahan
Copy link

@aflatter Regarding this comment:

# TODO: This could lead to naming conflicts. Investigate.

An attribute path in nix may contain periods, you just need to do something like:

let attrs = {
  # definition:
  "foo.bar" = "baz";
};
# use:
in attrs."foo.bar"

So you shouldn't need to escape the name, as long as it's quoted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment