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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +14 -0
- data/akabei.gemspec +29 -0
- data/bin/akabei +9 -0
- data/lib/akabei.rb +5 -0
- data/lib/akabei/abs.rb +60 -0
- data/lib/akabei/archive_utils.rb +75 -0
- data/lib/akabei/attr_path.rb +20 -0
- data/lib/akabei/builder.rb +108 -0
- data/lib/akabei/chroot_tree.rb +81 -0
- data/lib/akabei/cli.rb +172 -0
- data/lib/akabei/error.rb +4 -0
- data/lib/akabei/package.rb +174 -0
- data/lib/akabei/package_entry.rb +110 -0
- data/lib/akabei/package_info.rb +68 -0
- data/lib/akabei/repository.rb +156 -0
- data/lib/akabei/signer.rb +75 -0
- data/lib/akabei/thor_handler.rb +14 -0
- data/lib/akabei/version.rb +3 -0
- data/spec/akabei/abs_spec.rb +95 -0
- data/spec/akabei/archive_utils_spec.rb +44 -0
- data/spec/akabei/builder_spec.rb +129 -0
- data/spec/akabei/chroot_tree_spec.rb +108 -0
- data/spec/akabei/cli_spec.rb +5 -0
- data/spec/akabei/package_entry_spec.rb +70 -0
- data/spec/akabei/package_info_spec.rb +17 -0
- data/spec/akabei/package_spec.rb +54 -0
- data/spec/akabei/repository_spec.rb +157 -0
- data/spec/akabei/signer_spec.rb +5 -0
- data/spec/data/input/abs.tar.gz +0 -0
- data/spec/data/input/htop-vi.tar.gz +0 -0
- data/spec/data/input/makepkg.x86_64.conf +140 -0
- data/spec/data/input/nkf-2.1.3-1-x86_64.pkg.tar.xz +0 -0
- data/spec/data/input/nkf.PKGINFO +22 -0
- data/spec/data/input/nkf.tar.gz +0 -0
- data/spec/data/input/pacman.x86_64.conf +99 -0
- data/spec/data/input/ruby.PKGINFO +38 -0
- data/spec/data/input/test.db +0 -0
- data/spec/data/input/test.files +0 -0
- data/spec/integration/build_spec.rb +105 -0
- data/spec/spec_helper.rb +110 -0
- 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,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
|