#!/usr/bin/env ruby # -*- encoding: utf-8 -*- # # Author:: Fletcher Nichol () # # Copyright (C) 2015 Fletcher Nichol # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require "pathname" require "optparse" require "open-uri" require "fileutils" require "rubygems/package" require "zlib" require "tempfile" # FIXME: move to helpers mixin def windows? @windows ||= RUBY_PLATFORM =~ /mswin|mingw|windows/ end def chefdk if windows? Pathname.new(File.join(ENV["SYSTEMDRIVE"], "opscode", ARGV[0])) else Pathname.new(File.join("/opt", ARGV[0])) end end def bin_dir chefdk.join("embedded/bin") end ENV_KEYS = %w[ BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH GEM_ROOT IRBRC MY_RUBY_HOME RUBYLIB RUBYOPT RUBY_ENGINE RUBY_ROOT RUBY_VERSION _ORIGINAL_GEM_PATH PATH ].freeze def run(cmd) ENV_KEYS.each { |key| ENV["_YOLO_#{key}"] = ENV[key]; ENV.delete(key) } ENV['PATH'] = bin_dir.to_s + File::PATH_SEPARATOR + ENV['_YOLO_PATH'] puts " running: #{cmd}" output = `#{cmd} 2>&1` #FIXME: bash/zsh-ism, will not work on csh unless $?.exited? && $?.exitstatus == 0 raise("Command [#{cmd}] failed!\n\n---BEGIN OUTPUT--\n#{output}\n---END OUTPUT--\n") end ENV_KEYS.each { |key| ENV[key] = ENV.delete("_YOLO_#{key}") } end TAR_LONGLINK = '././@LongLink' def install_package_dependencies banner("Installing Packages") case `ohai platform_family` when /debian/ run("apt-get install build-essential git") when /fedora/, /rhel/ run("yum install gcc make git") else puts "i do not know how to install compilers and git on this platform..." end end # pure ruby `tar xzf`, handles longlinks and directories ending in '/' # (http://stackoverflow.com/a/31310593/506908) def extract_tgz(file, destination = '.') # NOTE: THIS IS DELIBERATELY PURE RUBY USING NO NATIVE GEMS AND ONLY # THE RUBY STDLIB BY DESIGN Gem::Package::TarReader.new( Zlib::GzipReader.open file ) do |tar| dest = nil tar.each do |entry| if entry.full_name == TAR_LONGLINK dest = File.join destination, entry.read.strip next end dest ||= File.join destination, entry.full_name if entry.directory? || (entry.header.typeflag == '' && entry.full_name.end_with?('/')) File.delete dest if File.file? dest FileUtils.mkdir_p dest, :mode => entry.header.mode, :verbose => false elsif entry.file? || (entry.header.typeflag == '' && !entry.full_name.end_with?('/')) FileUtils.rm_rf dest if File.directory? dest File.open dest, "wb" do |f| f.print entry.read end FileUtils.chmod entry.header.mode, dest, :verbose => false elsif entry.header.typeflag == '2' #Symlink! File.symlink entry.header.linkname, dest else puts "Unkown tar entry: #{entry.full_name} type: #{entry.header.typeflag}." end dest = nil end end end App = Struct.new(:name, :repo, :bundle_without, :install_command) do def to_s name end end chef_install_command = if windows? "#{bin_dir.join("gem")} build chef-windows.gemspec & #{bin_dir.join("gem")} install chef*.gem --no-ri --no-rdoc" else "#{bin_dir.join("rake")} install" end CHEFDK_APPS = [ App.new( "berkshelf", "berkshelf/berkshelf", "guard test", "#{bin_dir.join("rake")} install", ), App.new( "chef", "chef/chef", "server docgen test development", chef_install_command, ), App.new( "chef-dk", "chef/chef-dk", "dev test development", "#{bin_dir.join("rake")} install", ), App.new( "chef-vault", "Nordstrom/chef-vault", "test", "#{bin_dir.join("rake")} install", ), App.new( "foodcritic", "acrmp/foodcritic", nil, "#{bin_dir.join("rake")} install", ), App.new( "ohai", "chef/ohai", "test development", "#{bin_dir.join("rake")} install", ), App.new( "test-kitchen", "test-kitchen/test-kitchen", "guard test", "#{bin_dir.join("rake")} install", ) ].freeze class Updater attr_reader :app, :ref, :tarball, :repo def initialize(options) @app = options[:app] @ref = options[:ref] @tarball = options[:tarball] @repo = options[:repo] || @app.repo end def start if !windows? && Process.uid != 0 abort "#{$0} needs to be run as root user or with sudo" end banner("Cleaning #{app} checkout") app_dir.rmtree if app_dir.directory? top_dir = chefdk.join("embedded/apps") unless File.exist?(top_dir) banner("Creating #{top_dir} directory") FileUtils.mkdir_p top_dir end install_package_dependencies if ( tarball ) # NOTE: THIS IS DELIBERATELY PURE RUBY USING NO NATIVE GEMS AND ONLY # THE RUBY STDLIB BY DESIGN git_url = "https://github.com/#{repo}/archive/#{ref}.tar.gz" banner("Extracting #{app} from #{git_url}") Dir.chdir(chefdk.join("embedded/apps")) do Tempfile.open('appbundle-updater') do |tempfile| tempfile.binmode open(git_url) do |uri| tempfile.write(uri.read) end tempfile.close extract_tgz(tempfile.path) end base = File.basename repo FileUtils.mv "#{base}-#{ref}".gsub(/\//, "-"), "#{app.name}" end else git_url = "https://github.com/#{repo}.git" banner("Cloning #{app} from #{git_url}") run("git clone #{git_url} #{app_dir}") banner("Checking out #{app} to #{ref}") Dir.chdir(app_dir) do run("git checkout #{ref}") end end banner("Installing dependencies") Dir.chdir(app_dir) do cmd = "#{bin_dir.join("bundle")} install" cmd += " --without #{app.bundle_without}" if app.bundle_without ruby(cmd) end banner("Installing gem") Dir.chdir(app_dir) do ruby("#{bin_dir.join("bundle")} exec #{app.install_command}") end banner("Updating appbundler binstubs for #{app}") Dir.chdir(app_dir) do ruby("#{bin_dir.join("appbundler")} #{app_dir} #{chefdk.join("bin")}") end banner("Finished!") end private def app_dir chefdk.join("embedded/apps/#{app}") end def banner(msg) puts "-----> #{msg}" end def ruby(script) ruby = bin_dir.join("ruby").to_s.tap { |p| p.concat(".exe") if windows? } run([ruby, script].join(" ")) end end class CLI def self.options new.options end attr_reader :options, :parser def initialize @options = Hash.new @parser = OptionParser.new { |opts| opts.banner = "Usage: #{$0} PROJECT APP_NAME GIT_REF" opts.on("-t", "--[no-]tarball", "Do a tarball download instead of git clone") do |t| options[:tarball] = t end opts.on("-g", "--github REPO", "Github repo (e.g. chef/chef) to pull from") do |g| options[:repo] = g end opts.on("-h", "--help", "Prints this help") do puts opts exit end opts.separator("") opts.separator("App names:") CHEFDK_APPS.each { |a| opts.separator(" * #{a.name}") } } @parser.parse! validate! end def validate! die("PROJECT APP_NAME GIT_REF options are all required") if ARGV.length < 3 options[:app] = CHEFDK_APPS.find { |a| a.name == ARGV[1] } die("Invalid APP_NAME: #{ARGV[1]}") if options[:app].nil? options[:ref] = ARGV[2] end def die(msg) $stderr.puts msg $stderr.puts parser exit 1 end end Updater.new(CLI.options).start