akabei 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +55 -0
  8. data/Rakefile +14 -0
  9. data/akabei.gemspec +29 -0
  10. data/bin/akabei +9 -0
  11. data/lib/akabei.rb +5 -0
  12. data/lib/akabei/abs.rb +60 -0
  13. data/lib/akabei/archive_utils.rb +75 -0
  14. data/lib/akabei/attr_path.rb +20 -0
  15. data/lib/akabei/builder.rb +108 -0
  16. data/lib/akabei/chroot_tree.rb +81 -0
  17. data/lib/akabei/cli.rb +172 -0
  18. data/lib/akabei/error.rb +4 -0
  19. data/lib/akabei/package.rb +174 -0
  20. data/lib/akabei/package_entry.rb +110 -0
  21. data/lib/akabei/package_info.rb +68 -0
  22. data/lib/akabei/repository.rb +156 -0
  23. data/lib/akabei/signer.rb +75 -0
  24. data/lib/akabei/thor_handler.rb +14 -0
  25. data/lib/akabei/version.rb +3 -0
  26. data/spec/akabei/abs_spec.rb +95 -0
  27. data/spec/akabei/archive_utils_spec.rb +44 -0
  28. data/spec/akabei/builder_spec.rb +129 -0
  29. data/spec/akabei/chroot_tree_spec.rb +108 -0
  30. data/spec/akabei/cli_spec.rb +5 -0
  31. data/spec/akabei/package_entry_spec.rb +70 -0
  32. data/spec/akabei/package_info_spec.rb +17 -0
  33. data/spec/akabei/package_spec.rb +54 -0
  34. data/spec/akabei/repository_spec.rb +157 -0
  35. data/spec/akabei/signer_spec.rb +5 -0
  36. data/spec/data/input/abs.tar.gz +0 -0
  37. data/spec/data/input/htop-vi.tar.gz +0 -0
  38. data/spec/data/input/makepkg.x86_64.conf +140 -0
  39. data/spec/data/input/nkf-2.1.3-1-x86_64.pkg.tar.xz +0 -0
  40. data/spec/data/input/nkf.PKGINFO +22 -0
  41. data/spec/data/input/nkf.tar.gz +0 -0
  42. data/spec/data/input/pacman.x86_64.conf +99 -0
  43. data/spec/data/input/ruby.PKGINFO +38 -0
  44. data/spec/data/input/test.db +0 -0
  45. data/spec/data/input/test.files +0 -0
  46. data/spec/integration/build_spec.rb +105 -0
  47. data/spec/spec_helper.rb +110 -0
  48. metadata +225 -0
@@ -0,0 +1,68 @@
1
+ require 'akabei/error'
2
+
3
+ module Akabei
4
+ class PackageInfo
5
+ # See write_pkginfo() in /usr/bin/makepkg
6
+ ARRAY_ATTRIBUTES = %w[
7
+ license
8
+ replaces
9
+ group
10
+ conflict
11
+ provides
12
+ backup
13
+ depend
14
+ optdepend
15
+ makedepend
16
+ checkdepend
17
+
18
+ makepkgopt
19
+ ].freeze
20
+
21
+ ATTRIBUTES = %w[
22
+ pkgname
23
+ pkgbase
24
+ pkgver
25
+ pkgdesc
26
+ url
27
+ builddate
28
+ packager
29
+ size
30
+ arch
31
+ ].freeze
32
+
33
+ attr_accessor *ARRAY_ATTRIBUTES
34
+ attr_accessor *ATTRIBUTES
35
+
36
+ def initialize
37
+ ARRAY_ATTRIBUTES.each do |attr|
38
+ send("#{attr}=", [])
39
+ end
40
+ end
41
+
42
+ def self.parse(data)
43
+ info = new
44
+ data.each_line do |line|
45
+ line.strip!
46
+ next if line.start_with?('#')
47
+ if m = line.match(/\A(\w+)\s*=\s*(.+)\z/)
48
+ key = m[1]
49
+ val = m[2]
50
+ if ARRAY_ATTRIBUTES.include?(key)
51
+ info.send(key) << val
52
+ elsif ATTRIBUTES.include?(key)
53
+ if v = info.send(key)
54
+ raise Error.new("Duplicated entry #{key}: #{v} and #{val}")
55
+ else
56
+ info.send("#{key}=", val)
57
+ end
58
+ else
59
+ raise Error.new("Unknown attribute: #{key}: #{val}")
60
+ end
61
+ else
62
+ raise Error.new("Malformed line: #{line}")
63
+ end
64
+ end
65
+ info
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,156 @@
1
+ require 'akabei/archive_utils'
2
+ require 'akabei/error'
3
+ require 'akabei/package_entry'
4
+ require 'forwardable'
5
+ require 'pathname'
6
+ require 'tmpdir'
7
+
8
+ module Akabei
9
+ class Repository
10
+ attr_accessor :signer, :include_files
11
+
12
+ def initialize
13
+ @db = {}
14
+ @include_files = false
15
+ end
16
+
17
+ extend Forwardable
18
+ include Enumerable
19
+ def_delegator(:@db, :each)
20
+
21
+ def [](package_name)
22
+ @db.each do |_, entry|
23
+ if entry.name == package_name
24
+ return entry
25
+ end
26
+ end
27
+ end
28
+
29
+ def ==(other)
30
+ other.is_a?(self.class) &&
31
+ signer == other.signer &&
32
+ include_files == other.include_files &&
33
+ @db == other.instance_variable_get(:@db)
34
+ end
35
+
36
+ def load(path)
37
+ path = Pathname.new(path)
38
+ return unless path.readable?
39
+ verify!(path)
40
+ ArchiveUtils.each_entry(path) do |entry, archive|
41
+ pkgname, key = *entry.pathname.split('/', 2)
42
+ if key.include?('/')
43
+ raise Error.new("Malformed repository database: #{path}: #{entry.pathname}")
44
+ end
45
+ @db[pkgname] ||= PackageEntry.new
46
+ case key
47
+ when ''
48
+ # Ignore
49
+ when 'desc', 'depends', 'files'
50
+ load_entries(@db[pkgname], archive.read_data)
51
+ else
52
+ raise Error.new("Unknown repository database key: #{key}")
53
+ end
54
+ end
55
+ nil
56
+ end
57
+
58
+ def self.load(path)
59
+ new.tap do |repo|
60
+ repo.load(path)
61
+ end
62
+ end
63
+
64
+ def load_entries(entry, data)
65
+ key = nil
66
+ data.each_line do |line|
67
+ line.strip!
68
+ if m = line.match(/\A%([A-Z0-9]+)%\z/)
69
+ key = m[1].downcase
70
+ elsif line.empty?
71
+ key = nil
72
+ else
73
+ entry.add(key, line)
74
+ end
75
+ end
76
+ end
77
+
78
+ def add(package)
79
+ @db[package.db_name] = package.to_entry
80
+ end
81
+
82
+ def remove(pkgname)
83
+ @db.keys.each do |key|
84
+ if @db[key].name == pkgname
85
+ @db.delete(key)
86
+ return true
87
+ end
88
+ end
89
+ false
90
+ end
91
+
92
+ def verify!(path)
93
+ if signer && File.readable?("#{path}.sig")
94
+ signer.verify!(path)
95
+ true
96
+ else
97
+ false
98
+ end
99
+ end
100
+
101
+ def save(path)
102
+ Archive::Writer.open_filename(path.to_s, Archive::COMPRESSION_GZIP, Archive::FORMAT_TAR) do |archive|
103
+ Dir.mktmpdir do |dir|
104
+ dir = Pathname.new(dir)
105
+ store_tree(dir)
106
+ create_db(dir, archive)
107
+ end
108
+ end
109
+ if signer
110
+ signer.detach_sign(path)
111
+ end
112
+ nil
113
+ end
114
+
115
+ def store_tree(topdir)
116
+ @db.each do |db_name, pkg_entry|
117
+ pkgdir = topdir.join(db_name)
118
+ pkgdir.mkpath
119
+ pkgdir.join('desc').open('w') do |f|
120
+ pkg_entry.write_desc(f)
121
+ end
122
+ pkgdir.join('depends').open('w') do |f|
123
+ pkg_entry.write_depends(f)
124
+ end
125
+ if @include_files
126
+ pkgdir.join('files').open('w') do |f|
127
+ pkg_entry.write_files(f)
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def create_db(topdir, archive)
134
+ @db.keys.sort.each do |db_name|
135
+ pkg_entry = @db[db_name]
136
+ archive.new_entry do |entry|
137
+ entry.pathname = "#{db_name}/"
138
+ entry.copy_stat(topdir.join(entry.pathname).to_s)
139
+ archive.write_header(entry)
140
+ end
141
+ %w[desc depends files].each do |fname|
142
+ pathname = "#{db_name}/#{fname}"
143
+ path = topdir.join(pathname)
144
+ if path.readable?
145
+ archive.new_entry do |entry|
146
+ entry.pathname = pathname
147
+ entry.copy_stat(path.to_s)
148
+ archive.write_header(entry)
149
+ archive.write_data(path.read)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,75 @@
1
+ require 'akabei/error'
2
+ require 'gpgme'
3
+
4
+ module Akabei
5
+ class Signer
6
+ class KeyNotFound < Error
7
+ attr_reader :key_name
8
+ def initialize(key)_name
9
+ @key_name = key_name
10
+ super("No such GPG key: #{key_name}")
11
+ end
12
+ end
13
+
14
+ class AmbiguousKey < Error
15
+ attr_reader :key_name, :found_keys
16
+ def initialize(key_name, found_keys)
17
+ @key_name = key_name
18
+ @found_keys = found_keys
19
+ super("Ambiguous GPG key: #{key_name}: #{formatted_keys}")
20
+ end
21
+
22
+ def formatted_keys
23
+ @found_keys.map do |key|
24
+ subkey = key.primary_subkey
25
+ "#{subkey.length}#{subkey.pubkey_algo_letter}/#{subkey.fingerprint[-8 .. -1]}"
26
+ end
27
+ end
28
+ end
29
+
30
+ class InvalidSignature < Error
31
+ attr_reader :path, :from
32
+ def initialize(path, from)
33
+ @path = path
34
+ @from = from
35
+ super("Invalid signature from #{from}: #{path}")
36
+ end
37
+ end
38
+
39
+ def initialize(gpg_key, crypto = GPGME::Crypto.new)
40
+ @gpg_key = find_secret_key(gpg_key)
41
+ @crypto = crypto
42
+ end
43
+
44
+ def detach_sign(path)
45
+ File.open(path) do |inp|
46
+ File.open("#{path}.sig", 'w') do |out|
47
+ @crypto.detach_sign(inp, signer: @gpg_key, output: out)
48
+ end
49
+ end
50
+ end
51
+
52
+ def verify!(path)
53
+ File.open("#{path}.sig") do |sig|
54
+ File.open(path) do |f|
55
+ @crypto.verify(sig, signed_text: f) do |signature|
56
+ unless signature.valid?
57
+ raise InvalidSignature.new(path, signature.from)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def find_secret_key(key_name)
65
+ keys = GPGME::Key.find(:secret, key_name, :sign)
66
+ if keys.empty?
67
+ raise KeyNotFound.new(key_name)
68
+ elsif keys.size > 1
69
+ raise AmbiguousKey.new(key_name, keys)
70
+ else
71
+ keys.first
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,14 @@
1
+ require 'thor'
2
+
3
+ module Akabei
4
+ module ThorHandler
5
+ module_function
6
+ def wrap(&block)
7
+ block.call
8
+ 0
9
+ rescue Thor::Error => e
10
+ $stderr.puts e.message
11
+ 10
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Akabei
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'akabei/abs'
3
+ require 'akabei/archive_utils'
4
+
5
+ describe Akabei::Abs do
6
+ let(:repo_name) { 'test' }
7
+ let(:abs_path) { test_dest('abs.tar.gz') }
8
+ let(:abs) { described_class.new(abs_path, repo_name) }
9
+
10
+ describe '#add' do
11
+ let(:builder) { double('Builder') }
12
+ let(:dir) { double('dir') }
13
+ let(:srcpkg) { test_input('nkf.tar.gz') }
14
+
15
+ before do
16
+ allow(builder).to receive(:with_source_package).and_yield(srcpkg)
17
+ end
18
+
19
+ context 'with a new tarball' do
20
+ it 'adds a new source tree' do
21
+ abs.add(dir, builder)
22
+ expect(abs_path).to be_file
23
+ files = Akabei::ArchiveUtils.list_paths(abs_path)
24
+ expect(files).to include('test/nkf/PKGBUILD')
25
+ end
26
+ end
27
+
28
+ context 'with an existing tarball' do
29
+ before do
30
+ FileUtils.cp(test_input('abs.tar.gz').to_s, abs_path)
31
+ end
32
+
33
+ it 'adds a new source tree' do
34
+ old_files = Akabei::ArchiveUtils.list_paths(abs_path)
35
+ expect(old_files).to_not include('test/nkf/PKGBUILD')
36
+ abs.add(dir, builder)
37
+ new_files = Akabei::ArchiveUtils.list_paths(abs_path)
38
+ expect(new_files.to_set).to be_superset(old_files.to_set)
39
+ expect(new_files).to include('test/nkf/PKGBUILD')
40
+ end
41
+
42
+ context 'with an existing package' do
43
+ let(:srcpkg) { test_input('htop-vi.tar.gz') }
44
+
45
+ it 'replaces the package' do
46
+ old_files = Akabei::ArchiveUtils.list_paths(abs_path)
47
+ abs.add(dir, builder)
48
+ new_files = Akabei::ArchiveUtils.list_paths(abs_path)
49
+ expect(new_files).to match_array(old_files)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ describe '#remove' do
56
+ context 'without tarbal' do
57
+ it 'raises an error' do
58
+ expect { abs.remove('htop-vi') }.to raise_error(Akabei::Error, /#{Regexp.escape(abs_path.to_s)}/)
59
+ end
60
+ end
61
+
62
+ context 'with tarball' do
63
+ before do
64
+ FileUtils.cp(test_input('abs.tar.gz').to_s, abs_path.to_s)
65
+ end
66
+
67
+ context 'with valid repository name' do
68
+ it 'removes the package' do
69
+ old_files = Akabei::ArchiveUtils.list_paths(abs_path)
70
+ expect(old_files).to include('test/htop-vi/PKGBUILD')
71
+ abs.remove('htop-vi')
72
+ new_files = Akabei::ArchiveUtils.list_paths(abs_path)
73
+ expect(new_files.to_set).to be_subset(old_files.to_set)
74
+ expect(new_files).to_not include('test/htop-vi/PKGBUILD')
75
+ end
76
+
77
+ context 'without the package' do
78
+ it 'does nothing' do
79
+ old_files = Akabei::ArchiveUtils.list_paths(abs_path)
80
+ expect(old_files).to include('test/htop-vi/PKGBUILD')
81
+ abs.remove('nkf')
82
+ new_files = Akabei::ArchiveUtils.list_paths(abs_path)
83
+ expect(new_files).to match_array(old_files)
84
+ end
85
+ end
86
+ end
87
+ context 'with invalid repository name' do
88
+ let(:repo_name) { 'vim-latest' }
89
+ it 'raises an error' do
90
+ expect { abs.remove('htop-vi') }.to raise_error(Akabei::Error, /vim-latest/)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'akabei/archive_utils'
3
+
4
+ describe Akabei::ArchiveUtils do
5
+ let(:archive_path) { test_input('htop-vi.tar.gz') }
6
+
7
+ def list_tree(dir)
8
+ Akabei::ArchiveUtils.list_tree_paths(dir).map { |path| path.relative_path_from(dir).to_s }
9
+ end
10
+
11
+ describe '.list_paths' do
12
+ it 'acts like tar -t' do
13
+ expect(described_class.list_paths(archive_path)).to match_array(tar('tf', archive_path.to_s))
14
+ end
15
+ end
16
+
17
+ describe '.extract_all' do
18
+ let(:dest) { test_dest('got').tap(&:mkpath) }
19
+ let(:tar_dest) { test_dest('expected').tap(&:mkpath) }
20
+
21
+ it 'acts like tar -x -C' do
22
+ described_class.extract_all(archive_path, dest)
23
+ tar('xf', archive_path.to_s, '-C', tar_dest.to_s)
24
+ expect(list_tree(dest)).to match_array(list_tree(tar_dest))
25
+ end
26
+ end
27
+
28
+ describe '.archive_all' do
29
+ let(:dest) { test_dest('got.tar.gz') }
30
+ let(:tar_dest) { test_dest('expected.tar.gz') }
31
+ let(:src_path) { test_input('htop-vi.tar.gz') }
32
+ let(:tree_path) { test_dest('tree').tap(&:mkpath) }
33
+
34
+ before do
35
+ tar('xf', src_path.to_s, '-C', tree_path.to_s)
36
+ end
37
+
38
+ it 'acts like tar -c -C' do
39
+ described_class.archive_all(tree_path, dest, Archive::COMPRESSION_GZIP, Archive::FORMAT_TAR)
40
+ tar('cf', tar_dest.to_s, '-C', tree_path.to_s, 'htop-vi')
41
+ expect(tar('tf', dest.to_s)).to match_array(tar('tf', tar_dest.to_s))
42
+ end
43
+ end
44
+ end