akabei 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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