summarylogtreecommitdiffstats
path: root/pacgem
diff options
context:
space:
mode:
Diffstat (limited to 'pacgem')
-rw-r--r--pacgem713
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
+}