diff options
Diffstat (limited to 'pacgem')
-rw-r--r-- | pacgem | 713 |
1 files changed, 713 insertions, 0 deletions
diff --git a/pacgem b/pacgem new file mode 100644 index 000000000000..64fe3a80a804 --- /dev/null +++ b/pacgem @@ -0,0 +1,713 @@ +#!/usr/bin/env ruby + +require 'optparse' + +module Pacgem + VERSION = '0.9.12' + + module Util + def which?(name) + `which #{name.shellescape} 2>/dev/null` + $?.success? + end + + def reset_rubygems + Gem::Specification.reset + + # Perform only operation on global gems + Gem::Specification.dirs.reject! {|dir| dir !~ %r{\A/usr} } + raise 'No system gem directory found - are you using local ruby installation (rvm, rbenv, ...)?' if Gem::Specification.dirs.empty? + end + + def fetch_spec(dep) + fetcher = Gem::SpecFetcher.fetcher + if fetcher.respond_to? :fetch + # Pre Ruby 2.0 gem spec fetcher + fetcher.fetch(dep, true).last + else + # Ruby 2.0 and newer + spec, source = fetcher.spec_for_dependency(dep, true).first.last + spec && [spec, source.uri] + end + end + + def spew(file, content) + File.open(file, 'w') {|f| f.write(content.to_s) } + end + + def truncate(s, max, omission = '...') + s = s.to_s + s.length > max ? s[0...max] + omission : s + end + + def ruby_package + @ruby_package ||= if RUBY_VERSION > '2.0' + 'ruby' + elsif RUBY_VERSION > '1.9' + 'ruby1.9' + elsif Gem.ruby.include?('ruby-enterprise') + 'ruby-enterprise' + else + 'ruby1.8' + end + end + + def pacman_parse(args) + `LC_ALL=C pacman #{args} 2>/dev/null` + end + + extend self + end + + class Logger + def initialize + @color = STDOUT.isatty + end + + def color? + @color + end + + def nocolor! + @color = false + end + + def msg(s) + print('==> ', :green, :bold) + puts(s, :bold) + end + + def msg2(s) + print(' -> ', :blue, :bold) + puts(s, :bold) + end + + def warning(s) + print('==> WARNING: ', :yellow, :bold) + puts(s, :bold) + end + + def error(s) + print('==> ERROR: ', :red, :bold) + puts(s, :bold) + end + + def print(s, *c) + STDOUT.print(color(s, *c)) + end + + def puts(s, *c) + STDOUT.puts(color(s, *c)) + end + + private + + COLORS = { + :clear => 0, + :bold => 1, + :dark => 2, + :italic => 3, # not widely implemented + :underline => 4, + :blink => 5, + :rapid_blink => 6, # not widely implemented + :reverse => 7, + :concealed => 8, + :strikethrough => 9, # not widely implemented + :black => 30, + :red => 31, + :green => 32, + :yellow => 33, + :blue => 34, + :magenta => 35, + :cyan => 36, + :white => 37, + :bg_black => 40, + :bg_red => 41, + :bg_green => 42, + :bg_yellow => 43, + :bg_blue => 44, + :bg_magenta => 45, + :bg_cyan => 46, + :bg_white => 47, + } + + def color(s, *c) + if color? + res = '' + c.each {|c| res << "\e[#{COLORS[c]}m" } + res << "#{s}\e[0m" + else + s + end + end + end + + class PkgBuild + include Util + + def initialize + @vars = [] + @@build ||= DATA.read + end + + def []=(key, val) + @vars << [key, val] + end + + def to_s + lines = "# Generated by pacgem #{Pacgem::VERSION}\n" + @vars.each do |(key,val)| + if Array === val + val = val.map {|v| v.inspect }.join("\n" + (' ' * (key.size + 2))) + lines << "#{key}=(#{val})\n" + else + lines << "#{key}=#{val.inspect}\n" + end + end + lines + @@build + end + + def save + spew('PKGBUILD', self) + end + end + + class Package + include Util + + attr_reader :gemname, :name, :version, :uri + attr_writer :explicit + + def initialize(name, version, uri) + @gemname = name + @name = build_name(name) + @version, @uri = version.to_s, uri + end + + def explicit? + unless instance_variable_defined?(:@explicit) + @explicit = pacman_parse("-Qqe #{name.shellescape}").chomp == name + end + @explicit + end + + def find_installed(name = gemname) + reset_rubygems + spec = Gem::Dependency.new(name, nil).to_spec + pkg = pacman_parse("-Qqo #{spec.loaded_from.shellescape}").chomp + pkg.empty? ? nil : [pkg, spec.version.to_s] + rescue Exception + end + + def install(options, logger) + FileUtils.mkpath(name) + Dir.chdir(name) do + gemfile = download + gen_pkgbuild(gemfile, options) + pkgfile = makepkg(options) + if options[:nonamcap] + logger.warning 'Skipping namcap checks.' + else + namcap(pkgfile, logger) + end + installpkg(pkgfile, logger) unless options[:create] + end + end + + private + + def build_name(gemname) + "#{ruby_package}-#{gemname.downcase.sub(/^ruby-/, '').tr('_', '-')}" + end + + def download + gemfile = "#{gemname}-#{version}.gem" + open("#{uri}gems/#{gemfile}") do |i| + File.open(gemfile, 'w') do |o| + FileUtils.copy_stream(i, o) + end + end + gemfile + end + + def gen_pkgbuild(gemfile, options) + # Gem::Format is pre 2.0, Gem::Package is the new API + spec = defined?(Gem::Format) ? Gem::Format.from_file_by_path(gemfile).spec : Gem::Package.new(gemfile).spec + + depends = [ruby_package] + conflicts = [] + spec.runtime_dependencies.each do |dep| + owner_pkg, installed_version = find_installed(dep.name) + pkgname = build_name(dep.name) + if owner_pkg && owner_pkg != pkgname + depends << owner_pkg + else + dep.requirement.requirements.each do |comp, ver| + comp = '>=' if comp == '~>' + if comp == '!=' + depends << pkgname + conflicts << "#{pkgname}=#{ver}" + else + depends << "#{pkgname}#{comp}#{ver}" + end + end + end + end + + optdepends = [] + spec.development_dependencies.each do |dep| + optspec, opturi = fetch_spec(dep) + optdepends << "#{build_name dep.name}: #{truncate(optspec.summary, 80)} (Development dependency)" if optspec + end + + depends.uniq! + conflicts.uniq! + optdepends.uniq! + + builder = %w(install man license fix) + unless spec.extensions.empty? + builder << 'cleanext' + builder << 'autodepends' unless options[:noautodepends] + end + + license, license_file = find_license(name, spec) + + pkg = PkgBuild.new + pkg['_gemname'] = spec.name + pkg['_gembuilder'] = builder + pkg['_ruby'] = Gem.ruby + pkg['_gem'] = File.join(File.dirname(Gem.ruby), 'gem') + pkg['pkgname'] = name + pkg['pkgver'] = spec.version.to_s + pkg['pkgrel'] = 1 + pkg['pkgdesc'] = spec.summary + pkg['arch'] = spec.extensions.empty? ? %w(any) : %w(i686 x86_64) + pkg['url'] = spec.homepage + pkg['license'] = license + pkg['_licensefile'] = license_file + pkg['groups'] = %w(pacgem) # Mark this package as installed by pacgem + pkg['makedepends'] = %W(#{ruby_package} binutils) + pkg['depends'] = depends + pkg['conflicts'] = conflicts + pkg['optdepends'] = optdepends + pkg['source'] = %W(#{uri}gems/$_gemname-$pkgver.gem) + pkg['sha256sums'] = [Digest::SHA2.file(gemfile).to_s] + pkg['noextract'] = %w($_gemname-$pkgver.gem) + pkg['options'] = %w(!emptydirs) + pkg.save + end + + def makepkg(options) + ENV['PACKAGER'] = 'pacgem' + system("makepkg -f #{options[:create] && '--nodeps'} #{options[:nocolor] && '--nocolor'}") + Dir["#{name}-*.pkg.*"].first || raise("makepkg #{name} failed") + end + + def namcap(pkgfile, logger) + if which?('namcap') + logger.msg "Checking #{pkgfile} with namcap..." + system("namcap #{pkgfile.shellescape}") + else + logger.warning 'namcap is not installed' + end + end + + def installpkg(pkgfile, logger) + logger.msg "Installing #{pkgfile} with pacman..." + pacman_parse('-Qv') =~ /^Lock File\s+:\s+(.*)$/ + lockfile = $1 + if File.exists?(lockfile) + logger.msg2 'Pacman is currently in use, please wait.' + sleep 1 while File.exists?(lockfile) + end + cmd = "pacman --as#{explicit? ? 'explicit' : 'deps'} -U #{pkgfile.shellescape}" + if which?('sudo') + system("sudo #{cmd}") + else + system("su -c #{cmd.shellescape}") + end + end + + def find_license(name, spec) + custom = false + licenses = + if spec.licenses.empty? + custom = true + ["custom:#{name}"] + else + spec.licenses.map do |license| + # Check if this a common license + Dir['/usr/share/licenses/common/*'].map {|f| File.basename(f) }.find do |f| + f.casecmp(license.gsub('-', '')) == 0 + end || + %w(BSD MIT ZLIB Python).find {|f| f.casecmp(license) == 0 && (custom = true) } || + (custom = "custom:#{license}") + end + end + files = {} + if custom + spec.files.sort_by(&:size).each do |file| + if %w(COPYING LICENSE COPYRIGHT).any? {|s| file =~ /#{s}/i } + files[File.basename(file)] ||= file + end + end + end + [licenses, files.values] + end + end + + class Installer + include Util + + def initialize(options, logger) + @options, @logger = options, logger + @list = [] + @packages = {} + end + + def run + @list.each {|pkg| pkg.install(@options, @logger) } + end + + def install(name, version = nil) + resolve(Gem::Dependency.new(name, version)).explicit = true + if @options[:resolveonly] + exit + end + end + + def update + reset_rubygems + Gem::Specification.each do |spec| + resolve(Gem::Dependency.new(spec.name, nil)) + end + end + + private + + def resolve(dep) + @packages[dep.name] ||= + begin + spec, uri = fetch_spec(dep) + raise "Gem #{dep} not found" unless spec + pkg = Package.new(dep.name, spec.version, uri) + install = @options[:create] + owner_pkg, installed_version = pkg.find_installed + if installed_version + if owner_pkg != pkg.name || pacman_parse("-Qi #{pkg.name.shellescape}").match(/^Groups\s+:\s+pacgem$/).nil? + @logger.msg2 "(Not installed with pacgem, part of #{owner_pkg}) #{pkg.gemname}-#{installed_version}: #{spec.summary}" + elsif installed_version == pkg.version + @logger.msg2 "(Up-to-date) #{spec.full_name}: #{spec.summary}" + else + @logger.msg2 "(Update from #{installed_version}) #{spec.full_name}: #{spec.summary}" + install = true + end + else + @logger.msg2 "(New) #{spec.full_name}: #{spec.summary}" + install = true + end + spec.runtime_dependencies.each {|d| resolve(d) } if !@options[:noresolve] + @list << pkg if install + pkg + end + end + end + + class Command + include Util + + def initialize(args) + @args = args + @options = {} + @logger = Logger.new + end + + def run + @opts = OptionParser.new(&method(:set_opts)) + @opts.parse!(@args) + process + exit 0 + rescue OptionParser::ParseError => ex + STDERR.puts ex.message + STDERR.puts @opts + exit 1 + rescue Exception => ex + raise ex if @options[:trace] || SystemExit === ex + @logger.error ex.message + @logger.msg2 'Use --trace for backtrace.' + exit 1 + end + + private + + def load_libraries + require 'tmpdir' + require 'rubygems' + require 'rubygems/user_interaction' + begin + require 'rubygems/format' + rescue LoadError + require 'rubygems/package' + end + require 'shellwords' + require 'open-uri' + require 'digest/sha2' + require 'fileutils' + + reset_rubygems + end + + def process + if @options[:update] || @options[:test] + if !@args.empty? + STDERR.puts 'Error: --update and --test accept no arguments.' + exit 1 + end + elsif @args.length < 1 + STDERR.puts 'Error: No operation specified (use -h for help)' + exit 1 + end + + if Process.uid == 0 + STDERR.puts 'Error: You cannot perform this operation if you are root.' + exit 1 + end + + trap :SIGINT do + @logger.error 'Aborted by user! Exiting...' + exit 1 + end + + load_libraries + + if @options[:destdir] + dir = File.expand_path(@options[:destdir]) + FileUtils.mkpath(dir) + @logger.msg "Saving package files in #{dir}" + else + dir = Dir.mktmpdir('pacgem-') + end + + begin + Dir.chdir(dir) do + installer = Installer.new(@options, @logger) + @logger.msg 'Resolving gems...' + if @options[:update] || @options[:test] + installer.update + if @options[:test] + exit + end + else + @args.each do |gem| + if gem =~ /^([-\w]+)((?:[<>]=?|=|~>|-)\d+(?:\.\d+)*)?$/ + name, version = $1, $2 + installer.install(name, version =~ /^-/ ? version[1..-1] : version) + else + installer.install(gem) + end + end + end + installer.run + end + ensure + FileUtils.remove_entry_secure(dir) unless @options[:destdir] + end + end + + def set_opts(opts) + opts.banner = 'Usage: pacgem [options] gems...' + + opts.separator %q{ +Pacgem installs Ruby Gems using the Arch Linux Package Manager (pacman). + +Examples: + pacgem --create slim Create ruby-slim package in the directory ./ruby-slim + pacgem slim-1.0 Create temporary ruby-slim package and install it + pacgem 'slim>1.0' Install ruby-slim version > 1.0 + pacgem thin 'slim~>1.0' Install ruby-thin and ruby-slim with version ~>1.0 + +Options: +} + + opts.on('-d DIR', '--destdir DIR', String, 'Destination directory for package files') do |dir| + @options[:destdir] = dir + end + + opts.on('-c', '--create', :NONE, 'Create package only, do not install') do + @options[:create] = true + @options[:destdir] = Dir.pwd + end + + opts.on('-u', '--update', :NONE, 'Update all installed gems') do + @options[:update] = true + end + + opts.on('-t', '--test', :NONE, 'Check if there are any gems to update') do + @options[:test] = true + end + + opts.on('-r', '--resolveonly', :NONE, 'Resolve dependencies only, don\'t install anything') do + @options[:resolveonly] = true + end + + opts.on('-n', '--noresolve', :NONE, 'Do not resolve dependencies') do + @options[:noresolve] = true + end + + opts.on('--noautodepends', :NONE, 'Disable automatic dependency generation for shared objects (*.so)') do + @options[:noautodepends] = true + end + + opts.on('--nonamcap', :NONE, 'Disable package checking with namcap') do + @options[:nonamcap] = true + end + + opts.on('--nocolor', :NONE, 'Disable colored output') do + @logger.nocolor! + end + + opts.on('--trace', :NONE, 'Show a full traceback on error') do + @options[:trace] = true + end + + opts.on_tail('-h', '--help', 'Display help and exit') do + puts opts + exit + end + + opts.on_tail('-V', '--version', 'Display version and exit') do + puts %{Pacgem Version #{VERSION} +(C) 2011 Daniel Mendler + +This program may be freely redistributed under +the terms of the GNU General Public License.} + exit + end + end + end +end + +Pacgem::Command.new(ARGV).run if $0 == __FILE__ + +__END__ + +_gem_install() { + msg 'Installing gem...' + + # Install the gem + install -d -m755 $_bindir $_gemdir + $_gem install --no-ri --no-rdoc --ignore-dependencies --no-user-install \ + --bindir $_bindir --install-dir $_gemdir "$srcdir/$_gemname-$pkgver.gem" +} + +_gem_man() { + msg 'Installing man pages...' + + # Find man pages and move them to the correct directory + local mandir="$_gemdir/gems/$_gemname-$pkgver/man" + if [[ -d $mandir ]]; then + install -d -m755 $_mandir + local file + for file in $(find $mandir -type f -and -name *.[0-9]); do + local dir=$_mandir/man${file##*.} + install -d -m755 $dir + mv $file $dir + done + rm -rf $mandir + fi +} + +_gem_license() { + if [[ "${#_licensefile[@]}" -ne 0 ]]; then + msg "Installing license $license..." + install -d -m755 "$pkgdir/usr/share/licenses/$pkgname" + local file + for file in ${_licensefile[@]}; do + ln -s "../../../..$_gemdestdir/gems/$_gemname-$pkgver/$file" "$pkgdir/usr/share/licenses/$pkgname/$(basename $file)" || true + done + fi +} + +_gem_fix() { + msg 'Fixing gem installation...' + + # Set mode of executables to 755 + [[ -d "$_gemdir/bin" ]] && find "$_gemdir/bin" -type f -exec chmod 755 -- '{}' ';' + + # Remove cached gem file + rm -f "$_gemdir/cache/$_gemname-$pkgver.gem" + + # Sometimes there are files which are not world readable. Fix this. + find $pkgdir -type f '!' -perm '-004' -exec chmod o+r -- '{}' ';' +} + +_gem_cleanext() { + msg 'Removing native build leftovers...' + local extdir="$_gemdir/gems/$_gemname-$pkgver/ext" + [[ -d $extdir ]] && find "$extdir" -name '*.o' -exec rm -f -- '{}' ';' +} + +# Check if dependency is already satisfied +_dependency_satisfied() { + local dep=$1 deps="${depends[@]}" + [[ $(type -t in_array) == 'function' ]] || error "in_array should be provided by makepkg" + while true; do + in_array $dep ${deps[@]} && return 0 + local found=0 pkg + # Warning: This could break easily if the pacman output format changes. + for pkg in $(LC_ALL=C pacman -Qi ${deps[@]} 2>/dev/null | sed '/Depends On/!d;s/.*: //;s/None\|[<>]=\?[^ ]*\|=[^ ]*//g'); do + if ! in_array $pkg ${deps[@]}; then + deps=(${deps[@]} $pkg) && found=1 + fi + done + (( $found )) || break + done + return 1 +} + +_gem_autodepends() { + msg 'Automatic dependency resolution...' + + # Find all referenced shared libraries + local deps=$(find $pkgdir -type f -name '*.so') + [[ -n $deps ]] || return 0 + + deps=$(readelf -d $deps | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' | sort | uniq) + + # Find referenced libraries on the library search path + local libs=() lib path + for lib in $deps; do + for path in /lib /usr/lib; do + [[ -f "$path/$lib" ]] && libs=(${libs[@]} "$path/$lib") + done + done + (( ${#libs} )) || return 0 + + msg2 "Referenced libraries: ${libs[*]}" + + # Find matching packages with pacman -Qo + # and add them to the depends array + local pkg + for pkg in $(pacman -Qqo ${libs[@]}); do + _dependency_satisfied $pkg || depends=(${depends[@]} $pkg) + done + msg2 "Referenced packages: ${depends[*]}" +} + +_rbconfig() { + $_ruby -e "require 'rbconfig'; puts RbConfig::CONFIG['$1']" +} + +package() { + # Directories defined inside build() because if ruby is not installed on the system + # makepkg will barf when sourcing the PKGBUILD + _gemdestdir=$($_gem environment gemdir) + _gemdir=$pkgdir$_gemdestdir + _bindir=$pkgdir$(_rbconfig bindir) + _mandir=$pkgdir$(_rbconfig mandir) + + local i + for i in ${_gembuilder[@]}; do + _gem_$i + done +} |