apt-dists-merge 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +7 -0
- data/NEWS.md +5 -0
- data/README.md +42 -0
- data/bin/apt-dists-merge +6 -0
- data/lib/apt-dists-merge/command-line.rb +63 -0
- data/lib/apt-dists-merge/merger.rb +321 -0
- data/lib/apt-dists-merge/version.rb +3 -0
- data/lib/apt-dists-merge.rb +3 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 661b0b8dd9099c4af744f1d9026fa0f2474af1f81be6247a5463f4d862b003fb
|
4
|
+
data.tar.gz: 0f2d7ac3478faf2f48d9f5c6fc3832062a9a440705b275fb6729a541405b687e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 49d26cfb67d54dc6fee5ad44a0b6939c4672f04f0bef30cd7d042b50c002afdf10a64e3f46147f95046de48d3facddd8f89a9a46714703801528f087c4c3e06d
|
7
|
+
data.tar.gz: f95558f6ddc923b0a39fc42616eaada2dd937c40319c4ce0be1d4eab05656f1b3437739ed1739d03c52c7f449e1ed86de471ebb121b69162524133831886acc7
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2021 Sutou Kouhei <kou@clear-code.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/NEWS.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# APT dists merge
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
APT dists merge provides a tool and a library to merge `dists/` contents for APT repository.
|
6
|
+
|
7
|
+
Generally, you should use more general APT repository management tool such as [aptly](https://www.aptly.info/) and [reprepro](https://salsa.debian.org/brlink/reprepro).
|
8
|
+
|
9
|
+
APT dists merge is useful when you want to manage `pool/` by yourself and add `.deb`s incrementally without keeping all `.deb`s for the target APT repository on local.
|
10
|
+
|
11
|
+
Use cases:
|
12
|
+
|
13
|
+
* [Apache Arrow](https://github.com/apache/arrow/blob/master/dev/release/binary-task.rb)
|
14
|
+
* [Groonga](https://github.com/groonga/packages.groonga.org)
|
15
|
+
* [Red Data Tools](https://github.com/red-data-tools/packages.red-data-tools.org)
|
16
|
+
|
17
|
+
## Install
|
18
|
+
|
19
|
+
```bash
|
20
|
+
gem install apt-dists-merge
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Tool:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
apt-dists-merge --base base/dists/ --new new/dists/ --output merged/dists/
|
29
|
+
```
|
30
|
+
|
31
|
+
Library:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require "apt-dists-merge"
|
35
|
+
|
36
|
+
merger = APTDistsMerge::Merger.new("base/dists/", "new/dists/")
|
37
|
+
merger.merge("merged/dists/")
|
38
|
+
```
|
39
|
+
|
40
|
+
## License
|
41
|
+
|
42
|
+
The MIT license. See `LICENSE.txt` for details.
|
data/bin/apt-dists-merge
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "optparse"
|
3
|
+
require "zlib"
|
4
|
+
|
5
|
+
module APTDistsMerge
|
6
|
+
class CommandLine
|
7
|
+
def initialize(output=nil)
|
8
|
+
@base_dir = nil
|
9
|
+
@incomping_dir = nil
|
10
|
+
@output_dir = nil
|
11
|
+
@output = output || "-"
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(args)
|
15
|
+
catch do |tag|
|
16
|
+
open_output do |output|
|
17
|
+
parse_args(args, output, tag)
|
18
|
+
process
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def open_output(&block)
|
25
|
+
case @output
|
26
|
+
when "-"
|
27
|
+
yield($stdout)
|
28
|
+
when String
|
29
|
+
File.open(@output, "w", &block)
|
30
|
+
else
|
31
|
+
yield(@output)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_args(args, output, tag)
|
36
|
+
parser = OptionParser.new
|
37
|
+
parser.banner += " BASE_DIR INCOMING_DIR OUTPUT_DIR"
|
38
|
+
parser.on("--version",
|
39
|
+
"Show version and exit") do
|
40
|
+
output.puts(VERSION)
|
41
|
+
throw(tag, true)
|
42
|
+
end
|
43
|
+
parser.on("--help",
|
44
|
+
"Show this message and exit") do
|
45
|
+
output.puts(parser.help)
|
46
|
+
throw(tag, true)
|
47
|
+
end
|
48
|
+
args = parser.parse!(args.dup)
|
49
|
+
if args.size != 3
|
50
|
+
$stderr.puts(parser.help)
|
51
|
+
throw(tag, false)
|
52
|
+
end
|
53
|
+
@base_dir = args[0]
|
54
|
+
@incoming_dir = args[1]
|
55
|
+
@output_dir = args[2]
|
56
|
+
end
|
57
|
+
|
58
|
+
def process
|
59
|
+
merger = Merger.new(@base_dir, @incoming_dir, @output_dir)
|
60
|
+
merger.merge
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,321 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module APTDistsMerge
|
4
|
+
class Merger
|
5
|
+
def initialize(base_dir, incoming_dir, merged_dir)
|
6
|
+
@base_dir = base_dir
|
7
|
+
@incoming_dir = incoming_dir
|
8
|
+
@merged_dir = merged_dir
|
9
|
+
end
|
10
|
+
|
11
|
+
def merge
|
12
|
+
components = (detect_components(@base_dir) |
|
13
|
+
detect_components(@incoming_dir))
|
14
|
+
components.each do |component|
|
15
|
+
return false unless merge_component(component)
|
16
|
+
end
|
17
|
+
return false unless merge_release
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def detect_extensions(base_path)
|
23
|
+
Dir.glob("#{base_path}*").collect do |path|
|
24
|
+
path.gsub(/\A#{Regexp.escape(base_path)}/, "")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def read_data(path)
|
29
|
+
if File.exist?(path)
|
30
|
+
File.read(path)
|
31
|
+
elsif File.exist?("#{path}.gz")
|
32
|
+
real_path = "#{path}.gz"
|
33
|
+
Zlib::GzipReader.open(real_path) do |input|
|
34
|
+
input.read
|
35
|
+
end
|
36
|
+
elsif File.exist?("#{path}.xz")
|
37
|
+
real_path = "#{path}.xz"
|
38
|
+
IO.popen(["xz", "--decompress", "--stdout", real_path]) do |input|
|
39
|
+
input.read
|
40
|
+
end
|
41
|
+
else
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_data(data, path, extensions=nil)
|
47
|
+
FileUtils.mkdir_p(File.dirname(path))
|
48
|
+
extensions ||= [""]
|
49
|
+
extensions.each do |extension|
|
50
|
+
case extension
|
51
|
+
when ".gz"
|
52
|
+
Zlib::GzipWriter.open("#{path}#{extension}") do |output|
|
53
|
+
output.write(data)
|
54
|
+
end
|
55
|
+
when ".xz"
|
56
|
+
IO.popen(["xz", "--stdout"], "r+") do |xz|
|
57
|
+
writer = Thread.new do
|
58
|
+
xz.write(data)
|
59
|
+
xz.close_write
|
60
|
+
end
|
61
|
+
File.open("#{path}#{extension}", "wb") do |output|
|
62
|
+
reader = Thread.new do
|
63
|
+
IO.copy_stream(xz, output)
|
64
|
+
end
|
65
|
+
writer.join
|
66
|
+
reader.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
File.open(path, "w") do |output|
|
71
|
+
output.write(data)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def release_path(dir)
|
78
|
+
"#{dir}/Release"
|
79
|
+
end
|
80
|
+
|
81
|
+
def source_release_path(dir, component)
|
82
|
+
"#{dir}/#{component}/source/Release"
|
83
|
+
end
|
84
|
+
|
85
|
+
def sources_path(dir, component)
|
86
|
+
"#{dir}/#{component}/source/Sources"
|
87
|
+
end
|
88
|
+
|
89
|
+
def contents_path(dir, component, arch)
|
90
|
+
"#{dir}/#{component}/Contents-#{arch}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def packages_path(dir, component, arch)
|
94
|
+
"#{dir}/#{component}/binary-#{arch}/Packages"
|
95
|
+
end
|
96
|
+
|
97
|
+
def binary_release_path(dir, component, arch)
|
98
|
+
"#{dir}/#{component}/binary-#{arch}/Release"
|
99
|
+
end
|
100
|
+
|
101
|
+
def detect_architectures(dir)
|
102
|
+
data = read_data(release_path(dir))
|
103
|
+
return [] if data.nil?
|
104
|
+
data.each_line do |line|
|
105
|
+
case line
|
106
|
+
when /\AArchitectures:\s*/
|
107
|
+
return Regexp.last_match.post_match.split(/\s+/)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
[]
|
111
|
+
end
|
112
|
+
|
113
|
+
def detect_components(dir)
|
114
|
+
components = []
|
115
|
+
Dir.open(dir) do |d|
|
116
|
+
d.each do |path|
|
117
|
+
full_path = "#{dir}/#{path}"
|
118
|
+
next if path.start_with?(".")
|
119
|
+
next unless File.directory?(full_path)
|
120
|
+
components << path
|
121
|
+
end
|
122
|
+
end
|
123
|
+
components
|
124
|
+
end
|
125
|
+
|
126
|
+
def merge_component(component)
|
127
|
+
architectures = (detect_architectures(@base_dir) |
|
128
|
+
detect_architectures(@incoming_dir))
|
129
|
+
architectures.each do |arch|
|
130
|
+
return false unless merge_architecture(component, arch)
|
131
|
+
end
|
132
|
+
if File.exist?(source_release_path(@base_dir, component)) or
|
133
|
+
File.exist?(source_release_path(@incoming_dir, component))
|
134
|
+
return false unless merge_source(component)
|
135
|
+
end
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
def merge_architecture(component, arch)
|
140
|
+
return false unless merge_contents(component, arch)
|
141
|
+
return false unless merge_packages(component, arch)
|
142
|
+
return false unless merge_binary_release(component, arch)
|
143
|
+
true
|
144
|
+
end
|
145
|
+
|
146
|
+
def merge_contents(component, arch)
|
147
|
+
base_path = contents_path(@base_dir, component, arch)
|
148
|
+
incoming_path = contents_path(@incoming_dir, component, arch)
|
149
|
+
base = read_data(base_path)
|
150
|
+
incoming = read_data(incoming_path)
|
151
|
+
merged = (base.lines | incoming.lines).sort
|
152
|
+
merged_path = contents_path(@merged_dir, component, arch)
|
153
|
+
write_data(merged.join,
|
154
|
+
merged_path,
|
155
|
+
detect_extensions(base_path) |
|
156
|
+
detect_extensions(incoming_path))
|
157
|
+
true
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse_deb822(content)
|
161
|
+
data = {}
|
162
|
+
key = nil
|
163
|
+
content.each_line do |line|
|
164
|
+
case line
|
165
|
+
when /\A /
|
166
|
+
data[key] << "\n" << line
|
167
|
+
else
|
168
|
+
key, value = line.split(/:\s*/, 2)
|
169
|
+
data[key] = value.chomp
|
170
|
+
end
|
171
|
+
end
|
172
|
+
data
|
173
|
+
end
|
174
|
+
|
175
|
+
def read_packages(path)
|
176
|
+
read_data(path).split("\n\n").collect do |content|
|
177
|
+
[parse_deb822(content), content]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def debian_version_compare(a, b)
|
182
|
+
if system("dpkg", "--compare-versions", a, "eq", b)
|
183
|
+
0
|
184
|
+
elsif system("dpkg", "--compare-versions", a, "lt", b)
|
185
|
+
-1
|
186
|
+
else
|
187
|
+
1
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def merge_packages_data(base, incoming)
|
192
|
+
(base + incoming).sort do |a, b|
|
193
|
+
a_metadata = a[0]
|
194
|
+
b_metadata = b[0]
|
195
|
+
a_package = a_metadata["Package"]
|
196
|
+
b_package = b_metadata["Package"]
|
197
|
+
package_compare = (a_package <=> b_package)
|
198
|
+
if package_compare == 0
|
199
|
+
debian_version_compare(a_metadata["Version"], b_metadata["Version"])
|
200
|
+
else
|
201
|
+
if a_package.start_with?(b_package)
|
202
|
+
-1
|
203
|
+
else
|
204
|
+
package_compare
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def merge_packages(component, arch)
|
211
|
+
base_path = packages_path(@base_dir, component, arch)
|
212
|
+
incoming_path = packages_path(@incoming_dir, component, arch)
|
213
|
+
base = read_packages(base_path)
|
214
|
+
incoming = read_packages(incoming_path)
|
215
|
+
merged = merge_packages_data(base, incoming)
|
216
|
+
merged_path = packages_path(@merged_dir, component, arch)
|
217
|
+
write_data(merged.collect {|_, content| "#{content}\n\n"}.join,
|
218
|
+
merged_path,
|
219
|
+
detect_extensions(base_path) |
|
220
|
+
detect_extensions(incoming_path))
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
224
|
+
def merge_binary_release(component, arch)
|
225
|
+
base_path = binary_release_path(@base_dir, component, arch)
|
226
|
+
incoming_path = binary_release_path(@incoming_dir, component, arch)
|
227
|
+
base = read_data(base_path)
|
228
|
+
incoming = read_data(incoming_path)
|
229
|
+
merged = incoming || base
|
230
|
+
merged_path = binary_release_path(@merged_dir, component, arch)
|
231
|
+
write_data(merged,
|
232
|
+
merged_path,
|
233
|
+
detect_extensions(base_path) |
|
234
|
+
detect_extensions(incoming_path))
|
235
|
+
true
|
236
|
+
end
|
237
|
+
|
238
|
+
def merge_source(component)
|
239
|
+
return false unless merge_sources(component)
|
240
|
+
return false unless merge_source_release(component)
|
241
|
+
true
|
242
|
+
end
|
243
|
+
|
244
|
+
def merge_sources(component)
|
245
|
+
base_path = sources_path(@base_dir, component)
|
246
|
+
incoming_path = sources_path(@incoming_dir, component)
|
247
|
+
base = read_packages(base_path)
|
248
|
+
incoming = read_packages(incoming_path)
|
249
|
+
merged = merge_packages_data(base, incoming)
|
250
|
+
merged_path = sources_path(@merged_dir, component)
|
251
|
+
write_data(merged.collect {|_, content| "#{content}\n\n"}.join,
|
252
|
+
merged_path,
|
253
|
+
detect_extensions(base_path) |
|
254
|
+
detect_extensions(incoming_path))
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
def merge_source_release(component)
|
259
|
+
base_path = source_release_path(@base_dir, component)
|
260
|
+
incoming_path = source_release_path(@incoming_dir, component)
|
261
|
+
base = read_data(base_path)
|
262
|
+
incoming = read_data(incoming_path)
|
263
|
+
merged = incoming || base
|
264
|
+
merged_path = source_release_path(@merged_dir, component)
|
265
|
+
write_data(merged,
|
266
|
+
merged_path,
|
267
|
+
detect_extensions(base_path) |
|
268
|
+
detect_extensions(incoming_path))
|
269
|
+
true
|
270
|
+
end
|
271
|
+
|
272
|
+
def read_release(path)
|
273
|
+
parse_deb822(read_data(path))
|
274
|
+
end
|
275
|
+
|
276
|
+
def generate_checksums(dir, files, digest_class)
|
277
|
+
checksums = files.collect do |file|
|
278
|
+
checksum = digest_class.file(file).to_s
|
279
|
+
size = File.size(file)
|
280
|
+
relative_path = file.gsub(/\A#{Regexp.escape(dir)}\//, "")
|
281
|
+
" %s %16d %s" % [checksum, size, relative_path]
|
282
|
+
end
|
283
|
+
checksums.join("\n")
|
284
|
+
end
|
285
|
+
|
286
|
+
def merge_release
|
287
|
+
base = read_release(release_path(@base_dir))
|
288
|
+
incoming = read_release(release_path(@incoming_dir))
|
289
|
+
architectures = base["Architectures"].split |
|
290
|
+
incoming["Architectures"].split
|
291
|
+
components = base["Components"].split |
|
292
|
+
incoming["Components"].split
|
293
|
+
files = Dir.glob("#{@merged_dir}/**/*")
|
294
|
+
files = files.reject do |file|
|
295
|
+
File.directory?(file)
|
296
|
+
end
|
297
|
+
files = files.sort
|
298
|
+
merged = <<-RELEASE
|
299
|
+
Architectures: #{architectures.sort.join(" ")}
|
300
|
+
Codename: #{incoming["Codename"]}
|
301
|
+
Components: #{components.sort.join(" ")}
|
302
|
+
Date: #{incoming["Date"]}
|
303
|
+
Description: #{incoming["Description"]}
|
304
|
+
Label: #{incoming["Label"]}
|
305
|
+
Origin: #{incoming["Origin"]}
|
306
|
+
Suite: #{incoming["Suite"]}
|
307
|
+
MD5Sum:
|
308
|
+
#{generate_checksums(@merged_dir, files, Digest::MD5)}
|
309
|
+
SHA1:
|
310
|
+
#{generate_checksums(@merged_dir, files, Digest::SHA1)}
|
311
|
+
SHA256:
|
312
|
+
#{generate_checksums(@merged_dir, files, Digest::SHA256)}
|
313
|
+
SHA512:
|
314
|
+
#{generate_checksums(@merged_dir, files, Digest::SHA512)}
|
315
|
+
RELEASE
|
316
|
+
merged_path = release_path(@merged_dir)
|
317
|
+
write_data(merged, merged_path)
|
318
|
+
true
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apt-dists-merge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sutou Kouhei
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-09-26 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Generally, you should use more general APT repository management tool
|
14
|
+
such as [aptly](https://www.aptly.info/) and [reprepro](https://salsa.debian.org/brlink/reprepro).
|
15
|
+
email:
|
16
|
+
- kou@clear-code.com
|
17
|
+
executables:
|
18
|
+
- apt-dists-merge
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- LICENSE.txt
|
23
|
+
- NEWS.md
|
24
|
+
- README.md
|
25
|
+
- bin/apt-dists-merge
|
26
|
+
- lib/apt-dists-merge.rb
|
27
|
+
- lib/apt-dists-merge/command-line.rb
|
28
|
+
- lib/apt-dists-merge/merger.rb
|
29
|
+
- lib/apt-dists-merge/version.rb
|
30
|
+
homepage: https://github.com/red-data-tools/apt-dists-merge
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubygems_version: 3.3.0.dev
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: APT dists merge provides a tool and a library to merge `dists/` contents
|
53
|
+
for APT repository.
|
54
|
+
test_files: []
|