#!/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["PATH"] = ( [ bin_dir ] + ENV["PATH"].split(File::PATH_SEPARATOR) ).join(File::PATH_SEPARATOR) 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") else puts "---BEGIN OUTPUT--\n#{output}\n---END OUTPUT--\n" end ENV_KEYS.each { |key| ENV[key] = ENV.delete("_YOLO_#{key}") } end TAR_LONGLINK = "././@LongLink".freeze def install_package_dependencies banner("Installing Packages") case `#{bin_dir}/ohai platform_family` # rubocop: disable Lint/LiteralAsCondition when /debian/ ENV["DEBIAN_FRONTEND"] = "noninteractive" run("apt-get -y update") run("apt-get -q -y install build-essential git liblzma-dev zlib1g-dev") when /fedora/, /rhel/, /amazon/ if File.exist?("/usr/bin/dnf") run("dnf -y install gcc gcc-c++ make git zlib-devel") # lzma-devel has been replaced in fedora 30+ with xz-devel run("dnf -y install lzma-devel || dnf -y install xz-devel") else run("yum -y install gcc gcc-c++ make git zlib-devel lzma-devel") end when /suse/ run("zypper --non-interactive install gcc gcc-c++ 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 "Unknown tar entry: #{entry.full_name} type: #{entry.header.typeflag}." end dest = nil end end end App = Struct.new(:name, :repo, :bundle_without, :install_commands, :gems) do def initialize(*) super self.gems ||= {} end 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-document" else "#{bin_dir.join("rake")} install" end CHEFDK_APPS = [ App.new( "berkshelf", "berkshelf/berkshelf", "docs changelog", "#{bin_dir.join("rake")} install" ), App.new( "chef", "chef/chef", "server docgen maintenance pry travis integration ci chefstyle", chef_install_command, { "chef" => %w{server docgen maintenance pry travis integration ci chefstyle}, "chef-bin" => %w{server docgen maintenance pry travis integration ci chefstyle}, "ohai" => %w{development docs ci debug}, "inspec-core-bin" => %w{server docgen maintenance pry travis integration ci chefstyle}, }, ), App.new( "chef-dk", "chef/chef-dk", "development test", "#{bin_dir.join("bundle")} install", { "chef" => %w{docgen chefstyle omnibus_package}, "foodcritic" => %w{development test}, "test-kitchen" => %w{changelog debug docs development}, "inspec" => %w{deploy tools maintenance integration}, "chef-run" => %w{changelog docs debug}, "chef-cli" => %w{changelog docs debug}, "berkshelf" => %w{changelog docs debug development}, "chef-bin" => %w{changelog}, "chef-apply" => %w{changelog}, "chef-vault" => %w{changelog}, "ohai" => %w{changelog}, "opscode-pushy-client" => %w{changelog}, "cookstyle" => %w{changelog}, }, ), App.new( "chef-vault", "chef/chef-vault", "development", "#{bin_dir.join("rake")} install" ), App.new( "cookstyle", "chef/cookstyle", "development debug docs", "#{bin_dir.join("rake")} install" ), App.new( "foodcritic", "foodcritic/foodcritic", "development", "#{bin_dir.join("rake")} install" ), App.new( "inspec", "chef/inspec", "test integration tools maintenance deploy", "#{bin_dir.join("rake")} install" ), App.new( "ohai", "chef/ohai", "ci docs debug", "#{bin_dir.join("rake")} install" ), App.new( "test-kitchen", "test-kitchen/test-kitchen", "changelog integration debug chefstyle docs", "#{bin_dir.join("rake")} install" ), ].freeze class Updater attr_reader :app, :ref, :tarball, :repo, :gems, :install_commands def initialize(options) @app = options[:app] @ref = options[:ref] @tarball = options[:tarball] @extra_bin_files = options[:extra_bin_files] @binstubs_source = options[:binstubs_source] @repo = options[:repo] || @app.repo @gems = @app.gems @install_commands = @app.install_commands 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 Dir.glob("#{base}-*")[0], "#{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 Array(install_commands).each do |command| ruby("#{bin_dir.join("bundle")} exec #{command}") end end banner("Updating appbundler binstubs for #{app}") if gems.empty? Dir.chdir(app_dir) do cmd = "#{bin_dir.join("appbundler")} #{app_dir} #{chefdk.join("bin")}" cmd += " --extra-bin-files #{@extra_bin_files}" if @extra_bin_files cmd += " --binstubs-source #{@binstubs_source}" if @binstubs_source ruby(cmd) end else gems.each do |gem_name, without| Dir.chdir(app_dir) do cmd = "#{bin_dir.join("appbundler")} #{app_dir} #{chefdk.join("bin")} #{gem_name}" cmd += " --without #{without.join(",")}" if without cmd += " --extra-bin-files #{@extra_bin_files}" if @extra_bin_files cmd += " --binstubs-source #{@binstubs_source}" if @binstubs_source ruby(cmd) end end 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 = {} @parser = OptionParser.new do |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.on("-E", "--extra-bin-files BIN1,BIN2") do |e| options[:extra_bin_files] = e end opts.on("-B", "--binstubs-source path/to/source") do |e| options[:binstubs_source] = e end opts.separator("") opts.separator("App names:") CHEFDK_APPS.each { |a| opts.separator(" * #{a.name}") } end @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