akabei 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|