#!/usr/bin/env ruby # # Create a chunk morphology to build a RubyGem in Baserock # # 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_relative 'importer_base' require_relative 'importer_bundler_extensions' BANNER = "Usage: rubygems.to_chunk SOURCE_DIR GEM_NAME [VERSION]" DESCRIPTION = <<-END This tool looks in SOURCE_DIR to generate a chunk morphology with build instructions for GEM_NAME. If VERSION is supplied, it is used to check that the build instructions will produce the expected version of the Gem. It is intended for use with the `baserock-import` tool. END class RubyGemChunkMorphologyGenerator < Importer::Base include Importer::BundlerExtensions def parse_options(arguments) opts = create_option_parser(BANNER, DESCRIPTION) parsed_arguments = opts.parse!(arguments) if parsed_arguments.length != 2 && parsed_arguments.length != 3 STDERR.puts "Expected 2 or 3 arguments, got #{parsed_arguments}." opts.parse(['-?']) exit 255 end source_dir, gem_name, expected_version = parsed_arguments source_dir = File.absolute_path(source_dir) if expected_version != nil expected_version = Gem::Version.new(expected_version.dup) end [source_dir, gem_name, expected_version] end def chunk_name_for_gemspec(spec) # 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. spec.full_name end def is_signed_gem(spec) spec.signing_key != nil end def generate_chunk_morph_for_gem(gemspec_file, spec) description = 'Automatically generated by rubygems.to_chunk' bin_dir = "\"$DESTDIR/$PREFIX/bin\"" gem_dir = "\"$DESTDIR/$(gem environment home)\"" # 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. # # Adding this to Morph's default ruleset is painful, because: # - Changing the default split rules triggers a rebuild of everything. # - The whole split rule code needs reworking to prevent overlaps and to # make it possible to extend rules without creating overlaps. It's # otherwise impossible to reason about. split_rules = [ { 'artifact' => "#{spec.full_name}-doc", 'include' => [ 'usr/lib/ruby/gems/\d[\w.]*/doc/.*' ] } ] # It'd be rather tricky to include these build instructions as a # BuildSystem implementation in Morph. The problem is that there's no # way for the default commands to know what .gemspec file they should # be building. It doesn't help that the .gemspec may be in a subdirectory # (as in Rails, for example). # # Note that `gem help build` says the following: # # The best way to build a gem is to use a Rakefile and the # Gem::PackageTask which ships with RubyGems. # # It's often possible to run `rake gem`, but this may require Hoe, # rake-compiler, Jeweler or other assistance tools to be present at Gem # construction time. It seems that many Ruby projects that use these tools # also maintain an up-to-date generated .gemspec file, which means that we # can get away with using `gem build` just fine in many cases. # # Were we to use `setup.rb install` or `rake install`, programs that loaded # with the 'rubygems' library would complain that required Gems were not # installed. We must have the Gem metadata available, and `gem build; gem # install` seems the easiest way to achieve that. configure_commands = [] if is_signed_gem(spec) # This is a best-guess hack for allowing unsigned builds of Gems that are # normally built signed. There's no value in building signed Gems when we # control the build and deployment environment, and we obviously can't # provide the private key of the Gem's maintainer. configure_commands << "sed -e '/cert_chain\\s*=/d' -e '/signing_key\\s*=/d' -i " \ "#{gemspec_file}" end gemspec_dirname = File.dirname(gemspec_file) gemspec_basename = File.basename(gemspec_file) build_commands = [ "cd #{gemspec_dirname} && gem build #{gemspec_basename}", ] install_commands = [ "mkdir -p #{gem_dir}", "gem install --install-dir #{gem_dir} --bindir #{bin_dir} " \ "--ignore-dependencies --local " \ "#{gemspec_dirname}/#{spec.full_name}.gem" ] { 'name' => chunk_name_for_gemspec(spec), 'kind' => 'chunk', 'description' => description, 'build-system' => 'manual', 'products' => split_rules, 'configure-commands' => configure_commands, 'build-commands' => build_commands, 'install-commands' => install_commands, } end def run source_dir_name, gem_name, expected_version = parse_options(ARGV) log.info("Creating chunk morph for #{gem_name} based on " \ "source code in #{source_dir_name}") gemspec_file = locate_gemspec(gem_name, source_dir_name) resolved_specs = Dir.chdir(source_dir_name) do # FIXME: resolving the specs for all the dependencies of the target gem # isn't necessary here. In fact, just reading/executing the .gemspec # would be enough, and would speed this program up and remove a lot of # pointless network access to rubygems.org. definition = create_bundler_definition_for_gemspec(gem_name, gemspec_file) definition.resolve_remotely! end spec = get_spec_for_gem(resolved_specs, gem_name) validate_spec(spec, source_dir_name, expected_version) gemspec_file_in_chunk_repo = Pathname.new(gemspec_file). relative_path_from(Pathname.new(source_dir_name)) morph = generate_chunk_morph_for_gem(gemspec_file_in_chunk_repo, spec) write_morph(STDOUT, morph) end end RubyGemChunkMorphologyGenerator.new.run