From e01a86898aaebee8ba14f5b35fe2059717014241 Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Thu, 24 Jul 2014 16:32:21 +0100 Subject: Add scripts/import-rubygem to generate morphologies for RubyGems This generates a stratum and a set of chunk morphologies for integrating a given RubyGem project into Baserock. It uses the 'bundler' Gem to collate the list dependencies. This means that it works for any Gem that has a "Gemfile", and will also honour the "Gemfile.lock" file created by Bundler if the file exists. It requires the new 'rubygem' build mode. --- scripts/import-rubygem | 189 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100755 scripts/import-rubygem diff --git a/scripts/import-rubygem b/scripts/import-rubygem new file mode 100755 index 00000000..6b869a7d --- /dev/null +++ b/scripts/import-rubygem @@ -0,0 +1,189 @@ +#!/usr/bin/env ruby +# +# Create a stratum to integrate a Ruby project in Baserock, using RubyGems +# +# Copyright (C) 2014 Codethink Limited +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +require 'bundler' +require 'optparse' +require 'yaml' + +BASEROCK_RUBY_VERSION = '2.0.0' + +def parse_options(arguments) + # No options so far .. + opts = OptionParser.new + + opts.banner = "Usage: import-ruby PROJECT_DIR OUTPUT_DIR" + opts.separator "" + opts.separator "This tool reads the Gemfile and optionally the " + + "Gemfile.lock from a Ruby project " + opts.separator "source tree in PROJECT_DIR. It outputs a stratum " + + "morphology and a set of chunk " + opts.separator "morphology files to OUTPUT_DIR." + + parsed_arguments = opts.parse!(arguments) + + if parsed_arguments.length != 2 then + STDERR.puts opts.help + exit 1 + end + + parsed_arguments +end + +def get_project_name(project_dir_name) + # One Git repo can produce any number of Gems, or none, so it's hard to + # work out a project name that way. Instead, use the repo name :) + project_name = File.basename(project_dir_name) +end + +def load_gemfile() + # Load and parse the Gemfile and, if found, the Gemfile.lock file. + definition = Bundler::Definition.build( + 'Gemfile', 'Gemfile.lock', update=false) +end + +def get_all_specs_for_project(dir_name) + Dir.chdir(dir_name) { begin + load_gemfile.specs + rescue Bundler::GemNotFound + # If we're missing some Gem info, try remotely resolving. This is very + # slow so it's nice if it can be avoided. Perhaps setting up a local + # mirror of the necessary specs would avoid this problem. There seems + # to be no way to "reset" the Definition instance after the exception, + # so we have to call load_gemfile again. + STDERR.puts "Resolving definitions remotely (this may take a while!)" + load_gemfile.resolve_remotely! + rescue Bundler::GemfileNotFound + STDERR.puts "Did not find a Gemfile in #{dir_name}." + exit + end } +end + +def generate_morphs_for_specset(project_name, specs) + # Chunk names are the Gem's "full name" (name + version number), so that we + # don't break in the rare but possible case that two different versions of + # the same Gem are required for something to work. It'd be nicer to only + # use the full_name if we detect such a conflict. If we do, at least this + # function below can be removed with the much simpler: + # spec.deps.collect |dep| dep.name + runtime_depends = proc do |spec| + result = [] + spec.dependencies.each do |dep| + next if dep.type == :development + found = specs[dep] + if found.length != 1 + raise Exception, + "Unsure which Gem to use for #{dep}, got #{found}" + end + result << found[0].full_name + end + result + end + + description = 'Automatically generated by import-ruby. This is a ' + + 'prototype of a method for integrating RubyGems into ' + + 'Baserock.' + + bin_dir = "\"$DESTDIR/$PREFIX/bin\"" + gem_dir = "\"$DESTDIR/$PREFIX/lib/ruby/gems/#{BASEROCK_RUBY_VERSION}\"" + + chunk_morphs = specs.collect do |spec| + # There's more splitting to be done, but putting the docs in the + # correct artifact is the single biggest win for enabling smaller + # system images. + split_rules = [ + { + 'artifact' => "#{spec.full_name}-doc", + 'include' => [ + "usr/lib/ruby/gems/#{BASEROCK_RUBY_VERSION}/doc/.*" + ] + } + ] + + install_commands = [ + "mkdir -p #{gem_dir}", + "gem install --install-dir #{gem_dir} --bindir #{bin_dir} " + + "--ignore-dependencies --local #{spec.full_name}.gem" + ] + + { + 'name' => spec.full_name, + 'kind' => 'chunk', + 'description' => description, + 'build-system' => 'manual', + # FIXME: this is not how we should calculate the URL field! + 'gem-url' => "http://rubygems.org/downloads/#{spec.full_name}.gem", + 'products' => split_rules, + 'install-commands' => install_commands + } + end + + chunks = specs.collect do |spec| + { + 'name' => spec.full_name, + 'description' => description, + # This is a dummy value; there is no repo for these chunks. + # The 'repo' field should perhaps become optional! + 'repo' => 'baserock:baserock/definitions', + 'ref' => 'master', + 'morph' => File.join(project_name, spec.full_name + '.morph'), + # Runtime depends must be present at "build" (Gem install) time. + 'build-depends' => runtime_depends.call(spec), + # This feature is not in morph.git master yet + 'build-mode' => 'rubygem', + } + end + + stratum_morph = { + 'name' => project_name, + 'kind' => 'stratum', + 'description' => description, + 'build-depends' => [ + { 'morph' => 'ruby' } + ], + 'chunks' => chunks, + } + + return [stratum_morph] + chunk_morphs +end + +def write_morphs(morphs, project_name, target_dir_name) + target_dir_name = File.join(target_dir_name, project_name) + FileUtils.makedirs(target_dir_name) + Dir.chdir(target_dir_name) do + morphs.each do |morph| + morph_filename = morph['name'] + '.morph' + File.open(morph_filename, 'w') do |file| + file.write(YAML.dump(morph)) + end + end + end +end + +def run + project_dir_name, target_dir_name = parse_options(ARGV) + + project_name = get_project_name(project_dir_name) + specset = get_all_specs_for_project(project_dir_name) + + morphs = generate_morphs_for_specset(project_name, specset) + + write_morphs(morphs, project_name, target_dir_name) +end + +run -- cgit v1.2.1