/pacgem/pacgem
Ruby | 683 lines | 565 code | 103 blank | 15 comment | 59 complexity | a3b56a86220f0a71c6db473fcd9b9e78 MD5 | raw file
Possible License(s): LGPL-2.0, Unlicense, AGPL-1.0, BitTorrent-1.0, EPL-1.0, GPL-3.0, BSD-3-Clause, GPL-2.0, MIT, CC-BY-SA-3.0, BSD-2-Clause, MPL-2.0, BSD-3-Clause-No-Nuclear-License-2014, JSON, AGPL-3.0, MPL-2.0-no-copyleft-exception, IPL-1.0, LGPL-2.1, ISC, CC-BY-3.0, WTFPL, 0BSD, CC0-1.0, LGPL-3.0, Cube, Apache-2.0
- #!/usr/bin/env ruby
- require 'optparse'
- module Pacgem
- VERSION = '0.9.5'
- module Util
- def which?(name)
- `which #{name.shellescape} 2>/dev/null`
- $?.success?
- 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 > '1.9'
- 'ruby'
- 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
- def each_gem
- Gem.source_index.gems.sort.each do |_, spec|
- name = pacman_parse("-Qqo #{spec.loaded_from.shellescape}").chomp
- yield(spec, name.empty? ? nil : name, !name.empty? &&
- !pacman_parse("-Qi #{name.shellescape}").match(/^Groups\s+:\s+pacgem$/).nil?)
- end
- 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\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 installed_version
- unless instance_variable_defined?(:@installed_version)
- installed = pacman_parse("-Q #{name.shellescape}").strip.split(/\s+/, 2)
- installed[1] =~ /^(.*?)\-\d+$/
- @installed_version = $1
- end
- @installed_version
- end
- def installed?
- installed_version == version
- 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)
- spec = Gem::Format.from_file_by_path(gemfile).spec
- depends = [ruby_package]
- conflicts = []
- spec.runtime_dependencies.each do |dep|
- dep.requirement.requirements.each do |comp, ver|
- comp = '>=' if comp == '~>'
- if comp == '!='
- depends << "#{build_name dep.name}"
- conflicts << "#{build_name dep.name}=#{ver}"
- else
- depends << "#{build_name dep.name}#{comp}#{ver}"
- end
- end
- end
- optdepends = []
- spec.development_dependencies.each do |dep|
- optspec, opturi = Gem::SpecFetcher.fetcher.fetch(dep, true).last
- optdepends << "#{build_name dep.name}: #{truncate(optspec.summary, 80)}" if optspec
- end
- builder = %w(install man license fix)
- unless spec.extensions.empty?
- builder << 'cleanext'
- builder << 'autodepends' unless options[:noautodepends]
- end
- license, license_file = find_license(spec.licenses, spec.files)
- 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)
- 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(licenses, files)
- custom = []
- licenses = %w(Ruby) if licenses.empty?
- licenses = licenses.map do |license|
- # Check if this a common license
- common = Dir['/usr/share/licenses/common/*'].map {|f| File.basename(f) }.find do |f|
- f.casecmp(license.gsub('-', '')) == 0
- end
- if common
- common
- else
- custom << find_custom_license(license, files)
- "custom:#{license}"
- end
- end
- [licenses, custom.compact.uniq]
- end
- def find_custom_license(name, files)
- prefix = ['', "#{name}-"]
- names = %w(COPYING LICENSE COPYRIGHT)
- suffix = ['', '.txt']
- prefix.product(names, suffix).map {|a| a.join }.each do |candidate|
- files.each do |file|
- return file if file.casecmp(candidate) == 0
- end
- end
- nil
- 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
- each_gem do |spec, name, pacgem|
- if pacgem
- resolve(Gem::Dependency.new(spec.name, nil))
- elsif name
- @logger.msg2 "Not installed by pacgem: #{spec.full_name} (Package #{name})"
- else
- @logger.msg2 "Not managed by pacman: #{spec.full_name}"
- end
- end
- end
- private
- def resolve(dep)
- @packages[dep.name] ||= begin
- spec, uri = Gem::SpecFetcher.fetcher.fetch(dep, true).last
- raise "Gem #{dep} not found" unless spec
- pkg = Package.new(dep.name, spec.version, uri)
- if pkg.installed?
- @logger.msg2 "(Up-to-date) #{spec.full_name}: #{spec.summary}"
- elsif pkg.installed_version
- @logger.msg2 "(Update from #{pkg.installed_version}) #{spec.full_name}: #{spec.summary}"
- else
- @logger.msg2 "(New) #{spec.full_name}: #{spec.summary}"
- end
- spec.runtime_dependencies.each {|d| resolve(d) } if !@options[:noresolve]
- @list << pkg if @options[:create] || !pkg.installed?
- pkg
- end
- end
- end
- class Command
- 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'
- require 'rubygems/format'
- require 'shellwords'
- require 'open-uri'
- require 'digest/sha2'
- require 'fileutils'
- 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 rake Create ruby-rake package in the directory ./ruby-rake
- pacgem rake-1.0 Create temporary ruby-rake package and install it
- pacgem 'rake>1.0' Install ruby-rake version > 1.0
- pacgem thin 'rake~>1.0' Install ruby-thin and ruby-rake 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/$file"
- 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']"
- }
- build() {
- # 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
- }