PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/pacgem/pacgem

https://bitbucket.org/kaendfinger/aur-mirror
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
  1. #!/usr/bin/env ruby
  2. require 'optparse'
  3. module Pacgem
  4. VERSION = '0.9.5'
  5. module Util
  6. def which?(name)
  7. `which #{name.shellescape} 2>/dev/null`
  8. $?.success?
  9. end
  10. def spew(file, content)
  11. File.open(file, 'w') {|f| f.write(content.to_s) }
  12. end
  13. def truncate(s, max, omission = '...')
  14. s = s.to_s
  15. s.length > max ? s[0...max] + omission : s
  16. end
  17. def ruby_package
  18. @ruby_package ||= if RUBY_VERSION > '1.9'
  19. 'ruby'
  20. elsif Gem.ruby.include?('ruby-enterprise')
  21. 'ruby-enterprise'
  22. else
  23. 'ruby1.8'
  24. end
  25. end
  26. def pacman_parse(args)
  27. `LC_ALL=C pacman #{args} 2>/dev/null`
  28. end
  29. def each_gem
  30. Gem.source_index.gems.sort.each do |_, spec|
  31. name = pacman_parse("-Qqo #{spec.loaded_from.shellescape}").chomp
  32. yield(spec, name.empty? ? nil : name, !name.empty? &&
  33. !pacman_parse("-Qi #{name.shellescape}").match(/^Groups\s+:\s+pacgem$/).nil?)
  34. end
  35. end
  36. extend self
  37. end
  38. class Logger
  39. def initialize
  40. @color = STDOUT.isatty
  41. end
  42. def color?
  43. @color
  44. end
  45. def nocolor!
  46. @color = false
  47. end
  48. def msg(s)
  49. print('==> ', :green, :bold)
  50. puts(s, :bold)
  51. end
  52. def msg2(s)
  53. print(' -> ', :blue, :bold)
  54. puts(s, :bold)
  55. end
  56. def warning(s)
  57. print('==> WARNING: ', :yellow, :bold)
  58. puts(s, :bold)
  59. end
  60. def error(s)
  61. print('==> ERROR: ', :red, :bold)
  62. puts(s, :bold)
  63. end
  64. def print(s, *c)
  65. STDOUT.print(color(s, *c))
  66. end
  67. def puts(s, *c)
  68. STDOUT.puts(color(s, *c))
  69. end
  70. private
  71. COLORS = {
  72. :clear => 0,
  73. :bold => 1,
  74. :dark => 2,
  75. :italic => 3, # not widely implemented
  76. :underline => 4,
  77. :blink => 5,
  78. :rapid_blink => 6, # not widely implemented
  79. :reverse => 7,
  80. :concealed => 8,
  81. :strikethrough => 9, # not widely implemented
  82. :black => 30,
  83. :red => 31,
  84. :green => 32,
  85. :yellow => 33,
  86. :blue => 34,
  87. :magenta => 35,
  88. :cyan => 36,
  89. :white => 37,
  90. :bg_black => 40,
  91. :bg_red => 41,
  92. :bg_green => 42,
  93. :bg_yellow => 43,
  94. :bg_blue => 44,
  95. :bg_magenta => 45,
  96. :bg_cyan => 46,
  97. :bg_white => 47,
  98. }
  99. def color(s, *c)
  100. if color?
  101. res = ''
  102. c.each {|c| res << "\e[#{COLORS[c]}m" }
  103. res << "#{s}\e[0m"
  104. else
  105. s
  106. end
  107. end
  108. end
  109. class PkgBuild
  110. include Util
  111. def initialize
  112. @vars = []
  113. @@build ||= DATA.read
  114. end
  115. def []=(key, val)
  116. @vars << [key, val]
  117. end
  118. def to_s
  119. lines = "# Generated by pacgem\n"
  120. @vars.each do |(key,val)|
  121. if Array === val
  122. val = val.map {|v| v.inspect }.join("\n" + (' ' * (key.size + 2)))
  123. lines << "#{key}=(#{val})\n"
  124. else
  125. lines << "#{key}=#{val.inspect}\n"
  126. end
  127. end
  128. lines + @@build
  129. end
  130. def save
  131. spew('PKGBUILD', self)
  132. end
  133. end
  134. class Package
  135. include Util
  136. attr_reader :gemname, :name, :version, :uri
  137. attr_writer :explicit
  138. def initialize(name, version, uri)
  139. @gemname = name
  140. @name = build_name(name)
  141. @version, @uri = version.to_s, uri
  142. end
  143. def explicit?
  144. unless instance_variable_defined?(:@explicit)
  145. @explicit = pacman_parse("-Qqe #{name.shellescape}").chomp == name
  146. end
  147. @explicit
  148. end
  149. def installed_version
  150. unless instance_variable_defined?(:@installed_version)
  151. installed = pacman_parse("-Q #{name.shellescape}").strip.split(/\s+/, 2)
  152. installed[1] =~ /^(.*?)\-\d+$/
  153. @installed_version = $1
  154. end
  155. @installed_version
  156. end
  157. def installed?
  158. installed_version == version
  159. end
  160. def install(options, logger)
  161. FileUtils.mkpath(name)
  162. Dir.chdir(name) do
  163. gemfile = download
  164. gen_pkgbuild(gemfile, options)
  165. pkgfile = makepkg(options)
  166. if options[:nonamcap]
  167. logger.warning "Skipping namcap checks."
  168. else
  169. namcap(pkgfile, logger)
  170. end
  171. installpkg(pkgfile, logger) unless options[:create]
  172. end
  173. end
  174. private
  175. def build_name(gemname)
  176. "#{ruby_package}-#{gemname.downcase.sub(/^ruby-/, '').tr('_', '-')}"
  177. end
  178. def download
  179. gemfile = "#{gemname}-#{version}.gem"
  180. open("#{uri}gems/#{gemfile}") do |i|
  181. File.open(gemfile, 'w') do |o|
  182. FileUtils.copy_stream(i, o)
  183. end
  184. end
  185. gemfile
  186. end
  187. def gen_pkgbuild(gemfile, options)
  188. spec = Gem::Format.from_file_by_path(gemfile).spec
  189. depends = [ruby_package]
  190. conflicts = []
  191. spec.runtime_dependencies.each do |dep|
  192. dep.requirement.requirements.each do |comp, ver|
  193. comp = '>=' if comp == '~>'
  194. if comp == '!='
  195. depends << "#{build_name dep.name}"
  196. conflicts << "#{build_name dep.name}=#{ver}"
  197. else
  198. depends << "#{build_name dep.name}#{comp}#{ver}"
  199. end
  200. end
  201. end
  202. optdepends = []
  203. spec.development_dependencies.each do |dep|
  204. optspec, opturi = Gem::SpecFetcher.fetcher.fetch(dep, true).last
  205. optdepends << "#{build_name dep.name}: #{truncate(optspec.summary, 80)}" if optspec
  206. end
  207. builder = %w(install man license fix)
  208. unless spec.extensions.empty?
  209. builder << 'cleanext'
  210. builder << 'autodepends' unless options[:noautodepends]
  211. end
  212. license, license_file = find_license(spec.licenses, spec.files)
  213. pkg = PkgBuild.new
  214. pkg['_gemname'] = spec.name
  215. pkg['_gembuilder'] = builder
  216. pkg['_ruby'] = Gem.ruby
  217. pkg['_gem'] = File.join(File.dirname(Gem.ruby), 'gem')
  218. pkg['pkgname'] = name
  219. pkg['pkgver'] = spec.version.to_s
  220. pkg['pkgrel'] = 1
  221. pkg['pkgdesc'] = spec.summary
  222. pkg['arch'] = spec.extensions.empty? ? %w(any) : %w(i686 x86_64)
  223. pkg['url'] = spec.homepage
  224. pkg['license'] = license
  225. pkg['_licensefile'] = license_file
  226. pkg['groups'] = %w(pacgem) # Mark this package as installed by pacgem
  227. pkg['makedepends'] = %W(#{ruby_package} binutils)
  228. pkg['depends'] = depends
  229. pkg['conflicts'] = conflicts
  230. pkg['optdepends'] = optdepends
  231. pkg['source'] = %W(#{uri}gems/$_gemname-$pkgver.gem)
  232. pkg['sha256sums'] = [Digest::SHA2.file(gemfile).to_s]
  233. pkg['noextract'] = %w($_gemname-$pkgver.gem)
  234. pkg['options'] = %w(!emptydirs)
  235. pkg.save
  236. end
  237. def makepkg(options)
  238. system("makepkg -f #{options[:create] && '--nodeps'} #{options[:nocolor] && '--nocolor'}")
  239. Dir["#{name}-*.pkg.*"].first || raise("makepkg #{name} failed")
  240. end
  241. def namcap(pkgfile, logger)
  242. if which?('namcap')
  243. logger.msg "Checking #{pkgfile} with namcap..."
  244. system("namcap #{pkgfile.shellescape}")
  245. else
  246. logger.warning 'namcap is not installed'
  247. end
  248. end
  249. def installpkg(pkgfile, logger)
  250. logger.msg "Installing #{pkgfile} with pacman..."
  251. pacman_parse('-Qv') =~ /^Lock File\s+:\s+(.*)$/
  252. lockfile = $1
  253. if File.exists?(lockfile)
  254. logger.msg2 'Pacman is currently in use, please wait.'
  255. sleep 1 while File.exists?(lockfile)
  256. end
  257. cmd = "pacman --as#{explicit? ? 'explicit' : 'deps'} -U #{pkgfile.shellescape}"
  258. if which?('sudo')
  259. system("sudo #{cmd}")
  260. else
  261. system("su -c #{cmd.shellescape}")
  262. end
  263. end
  264. def find_license(licenses, files)
  265. custom = []
  266. licenses = %w(Ruby) if licenses.empty?
  267. licenses = licenses.map do |license|
  268. # Check if this a common license
  269. common = Dir['/usr/share/licenses/common/*'].map {|f| File.basename(f) }.find do |f|
  270. f.casecmp(license.gsub('-', '')) == 0
  271. end
  272. if common
  273. common
  274. else
  275. custom << find_custom_license(license, files)
  276. "custom:#{license}"
  277. end
  278. end
  279. [licenses, custom.compact.uniq]
  280. end
  281. def find_custom_license(name, files)
  282. prefix = ['', "#{name}-"]
  283. names = %w(COPYING LICENSE COPYRIGHT)
  284. suffix = ['', '.txt']
  285. prefix.product(names, suffix).map {|a| a.join }.each do |candidate|
  286. files.each do |file|
  287. return file if file.casecmp(candidate) == 0
  288. end
  289. end
  290. nil
  291. end
  292. end
  293. class Installer
  294. include Util
  295. def initialize(options, logger)
  296. @options, @logger = options, logger
  297. @list = []
  298. @packages = {}
  299. end
  300. def run
  301. @list.each {|pkg| pkg.install(@options, @logger) }
  302. end
  303. def install(name, version = nil)
  304. resolve(Gem::Dependency.new(name, version)).explicit = true
  305. if @options[:resolveonly]
  306. exit
  307. end
  308. end
  309. def update
  310. each_gem do |spec, name, pacgem|
  311. if pacgem
  312. resolve(Gem::Dependency.new(spec.name, nil))
  313. elsif name
  314. @logger.msg2 "Not installed by pacgem: #{spec.full_name} (Package #{name})"
  315. else
  316. @logger.msg2 "Not managed by pacman: #{spec.full_name}"
  317. end
  318. end
  319. end
  320. private
  321. def resolve(dep)
  322. @packages[dep.name] ||= begin
  323. spec, uri = Gem::SpecFetcher.fetcher.fetch(dep, true).last
  324. raise "Gem #{dep} not found" unless spec
  325. pkg = Package.new(dep.name, spec.version, uri)
  326. if pkg.installed?
  327. @logger.msg2 "(Up-to-date) #{spec.full_name}: #{spec.summary}"
  328. elsif pkg.installed_version
  329. @logger.msg2 "(Update from #{pkg.installed_version}) #{spec.full_name}: #{spec.summary}"
  330. else
  331. @logger.msg2 "(New) #{spec.full_name}: #{spec.summary}"
  332. end
  333. spec.runtime_dependencies.each {|d| resolve(d) } if !@options[:noresolve]
  334. @list << pkg if @options[:create] || !pkg.installed?
  335. pkg
  336. end
  337. end
  338. end
  339. class Command
  340. def initialize(args)
  341. @args = args
  342. @options = {}
  343. @logger = Logger.new
  344. end
  345. def run
  346. @opts = OptionParser.new(&method(:set_opts))
  347. @opts.parse!(@args)
  348. process
  349. exit 0
  350. rescue OptionParser::ParseError => ex
  351. STDERR.puts ex.message
  352. STDERR.puts @opts
  353. exit 1
  354. rescue Exception => ex
  355. raise ex if @options[:trace] || SystemExit === ex
  356. @logger.error ex.message
  357. @logger.msg2 'Use --trace for backtrace.'
  358. exit 1
  359. end
  360. private
  361. def load_libraries
  362. require 'tmpdir'
  363. require 'rubygems'
  364. require 'rubygems/user_interaction'
  365. require 'rubygems/format'
  366. require 'shellwords'
  367. require 'open-uri'
  368. require 'digest/sha2'
  369. require 'fileutils'
  370. end
  371. def process
  372. if @options[:update] || @options[:test]
  373. if !@args.empty?
  374. STDERR.puts 'Error: --update and --test accept no arguments.'
  375. exit 1
  376. end
  377. elsif @args.length < 1
  378. STDERR.puts 'Error: No operation specified (use -h for help)'
  379. exit 1
  380. end
  381. if Process.uid == 0
  382. STDERR.puts 'Error: You cannot perform this operation if you are root.'
  383. exit 1
  384. end
  385. trap :SIGINT do
  386. @logger.error 'Aborted by user! Exiting...'
  387. exit 1
  388. end
  389. load_libraries
  390. if @options[:destdir]
  391. dir = File.expand_path(@options[:destdir])
  392. FileUtils.mkpath(dir)
  393. @logger.msg "Saving package files in #{dir}"
  394. else
  395. dir = Dir.mktmpdir('pacgem-')
  396. end
  397. begin
  398. Dir.chdir(dir) do
  399. installer = Installer.new(@options, @logger)
  400. @logger.msg 'Resolving gems...'
  401. if @options[:update] || @options[:test]
  402. installer.update
  403. if @options[:test]
  404. exit
  405. end
  406. else
  407. @args.each do |gem|
  408. if gem =~ /^([-\w]+)((?:[<>]=?|=|~>|-)\d+(?:\.\d+)*)?$/
  409. name, version = $1, $2
  410. installer.install(name, version =~ /^-/ ? version[1..-1] : version)
  411. else
  412. installer.install(gem)
  413. end
  414. end
  415. end
  416. installer.run
  417. end
  418. ensure
  419. FileUtils.remove_entry_secure(dir) unless @options[:destdir]
  420. end
  421. end
  422. def set_opts(opts)
  423. opts.banner = 'Usage: pacgem [options] gems...'
  424. opts.separator %q{
  425. Pacgem installs Ruby Gems using the Arch Linux Package Manager (pacman).
  426. Examples:
  427. pacgem --create rake Create ruby-rake package in the directory ./ruby-rake
  428. pacgem rake-1.0 Create temporary ruby-rake package and install it
  429. pacgem 'rake>1.0' Install ruby-rake version > 1.0
  430. pacgem thin 'rake~>1.0' Install ruby-thin and ruby-rake with version ~>1.0
  431. Options:
  432. }
  433. opts.on('-d DIR', '--destdir DIR', String, 'Destination directory for package files') do |dir|
  434. @options[:destdir] = dir
  435. end
  436. opts.on('-c', '--create', :NONE, 'Create package only, do not install') do
  437. @options[:create] = true
  438. @options[:destdir] = Dir.pwd
  439. end
  440. opts.on('-u', '--update', :NONE, 'Update all installed gems') do
  441. @options[:update] = true
  442. end
  443. opts.on('-t', '--test', :NONE, 'Check if there are any gems to update') do
  444. @options[:test] = true
  445. end
  446. opts.on('-r', '--resolveonly', :NONE, 'Resolve dependencies only, don\'t install anything') do
  447. @options[:resolveonly] = true
  448. end
  449. opts.on('-n', '--noresolve', :NONE, 'Do not resolve dependencies') do
  450. @options[:noresolve] = true
  451. end
  452. opts.on('--noautodepends', :NONE, 'Disable automatic dependency generation for shared objects (*.so)') do
  453. @options[:noautodepends] = true
  454. end
  455. opts.on('--nonamcap', :NONE, 'Disable package checking with namcap') do
  456. @options[:nonamcap] = true
  457. end
  458. opts.on('--nocolor', :NONE, 'Disable colored output') do
  459. @logger.nocolor!
  460. end
  461. opts.on('--trace', :NONE, 'Show a full traceback on error') do
  462. @options[:trace] = true
  463. end
  464. opts.on_tail('-h', '--help', 'Display help and exit') do
  465. puts opts
  466. exit
  467. end
  468. opts.on_tail('-V', '--version', 'Display version and exit') do
  469. puts %{Pacgem Version #{VERSION}
  470. (C) 2011 Daniel Mendler
  471. This program may be freely redistributed under
  472. the terms of the GNU General Public License.}
  473. exit
  474. end
  475. end
  476. end
  477. end
  478. Pacgem::Command.new(ARGV).run if $0 == __FILE__
  479. __END__
  480. _gem_install() {
  481. msg 'Installing gem...'
  482. # Install the gem
  483. install -d -m755 $_bindir $_gemdir
  484. $_gem install --no-ri --no-rdoc --ignore-dependencies --no-user-install \
  485. --bindir $_bindir --install-dir $_gemdir "$srcdir/$_gemname-$pkgver.gem"
  486. }
  487. _gem_man() {
  488. msg 'Installing man pages...'
  489. # Find man pages and move them to the correct directory
  490. local mandir="$_gemdir/gems/$_gemname-$pkgver/man"
  491. if [[ -d $mandir ]]; then
  492. install -d -m755 $_mandir
  493. local file
  494. for file in $(find $mandir -type f -and -name *.[0-9]); do
  495. local dir=$_mandir/man${file##*.}
  496. install -d -m755 $dir
  497. mv $file $dir
  498. done
  499. rm -rf $mandir
  500. fi
  501. }
  502. _gem_license() {
  503. if [[ "${#_licensefile[@]}" -ne 0 ]]; then
  504. msg "Installing license $license..."
  505. install -d -m755 "$pkgdir/usr/share/licenses/$pkgname"
  506. local file
  507. for file in ${_licensefile[@]}; do
  508. ln -s "../../../..$_gemdestdir/gems/$_gemname-$pkgver/$file" "$pkgdir/usr/share/licenses/$pkgname/$file"
  509. done
  510. fi
  511. }
  512. _gem_fix() {
  513. msg 'Fixing gem installation...'
  514. # Set mode of executables to 755
  515. [[ -d "$_gemdir/bin" ]] && find "$_gemdir/bin" -type f -exec chmod 755 -- '{}' ';'
  516. # Remove cached gem file
  517. rm -f "$_gemdir/cache/$_gemname-$pkgver.gem"
  518. # Sometimes there are files which are not world readable. Fix this.
  519. find $pkgdir -type f '!' -perm '-004' -exec chmod o+r -- '{}' ';'
  520. }
  521. _gem_cleanext() {
  522. msg 'Removing native build leftovers...'
  523. local extdir="$_gemdir/gems/$_gemname-$pkgver/ext"
  524. [[ -d $extdir ]] && find "$extdir" -name '*.o' -exec rm -f -- '{}' ';'
  525. }
  526. # Check if dependency is already satisfied
  527. _dependency_satisfied() {
  528. local dep=$1 deps="${depends[@]}"
  529. [[ $(type -t in_array) == 'function' ]] || error "in_array should be provided by makepkg"
  530. while true; do
  531. in_array $dep ${deps[@]} && return 0
  532. local found=0 pkg
  533. # Warning: This could break easily if the pacman output format changes.
  534. for pkg in $(LC_ALL=C pacman -Qi ${deps[@]} 2>/dev/null | sed '/Depends On/!d;s/.*: //;s/None\|[<>]=\?[^ ]*\|=[^ ]*//g'); do
  535. if ! in_array $pkg ${deps[@]}; then
  536. deps=(${deps[@]} $pkg) && found=1
  537. fi
  538. done
  539. (( $found )) || break
  540. done
  541. return 1
  542. }
  543. _gem_autodepends() {
  544. msg 'Automatic dependency resolution...'
  545. # Find all referenced shared libraries
  546. local deps=$(find $pkgdir -type f -name '*.so')
  547. [[ -n $deps ]] || return 0
  548. deps=$(readelf -d $deps | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' | sort | uniq)
  549. # Find referenced libraries on the library search path
  550. local libs=() lib path
  551. for lib in $deps; do
  552. for path in /lib /usr/lib; do
  553. [[ -f "$path/$lib" ]] && libs=(${libs[@]} "$path/$lib")
  554. done
  555. done
  556. (( ${#libs} )) || return 0
  557. msg2 "Referenced libraries: ${libs[*]}"
  558. # Find matching packages with pacman -Qo
  559. # and add them to the depends array
  560. local pkg
  561. for pkg in $(pacman -Qqo ${libs[@]}); do
  562. _dependency_satisfied $pkg || depends=(${depends[@]} $pkg)
  563. done
  564. msg2 "Referenced packages: ${depends[*]}"
  565. }
  566. _rbconfig() {
  567. $_ruby -e "require 'rbconfig'; puts RbConfig::CONFIG['$1']"
  568. }
  569. build() {
  570. # Directories defined inside build() because if ruby is not installed on the system
  571. # makepkg will barf when sourcing the PKGBUILD
  572. _gemdestdir=$($_gem environment gemdir)
  573. _gemdir=$pkgdir$_gemdestdir
  574. _bindir=$pkgdir$(_rbconfig bindir)
  575. _mandir=$pkgdir$(_rbconfig mandir)
  576. local i
  577. for i in ${_gembuilder[@]}; do
  578. _gem_$i
  579. done
  580. }