crosspack 0.2.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/CHANGELOG.md +36 -0
- data/LICENSE +18 -0
- data/README.md +192 -0
- data/exe/crossbuild +76 -0
- data/exe/crosspack +119 -0
- data/lib/crossbuild/build_manifest.rb +374 -0
- data/lib/crossbuild/builder.rb +72 -0
- data/lib/crossbuild/distributor.rb +76 -0
- data/lib/crossbuild/matrix.rb +52 -0
- data/lib/crossbuild/platform.rb +40 -0
- data/lib/crossbuild/runner.rb +62 -0
- data/lib/crossbuild/version.rb +5 -0
- data/lib/crossbuild/version_scheme.rb +51 -0
- data/lib/crossbuild.rb +27 -0
- data/lib/crosspack/builders/app_dir.rb +88 -0
- data/lib/crosspack/builders/deb.rb +121 -0
- data/lib/crosspack/builders/pkgbuild.rb +64 -0
- data/lib/crosspack/builders/rpm.rb +149 -0
- data/lib/crosspack/builders/wix.rb +119 -0
- data/lib/crosspack/builds.rb +61 -0
- data/lib/crosspack/manifest.rb +237 -0
- data/lib/crosspack/matrix.rb +67 -0
- data/lib/crosspack/package_manifest.rb +219 -0
- data/lib/crosspack/packer.rb +223 -0
- data/lib/crosspack/resolver.rb +91 -0
- data/lib/crosspack/target.rb +93 -0
- data/lib/crosspack/version.rb +5 -0
- data/lib/crosspack.rb +38 -0
- metadata +78 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Crosspack
|
|
6
|
+
# Canonical dependency names crosspack knows about. Anything else in
|
|
7
|
+
# deps.yaml still resolves, but produces a warning (possible typo).
|
|
8
|
+
KNOWN_CANONICAL = %w[
|
|
9
|
+
webkit2gtk gtk3 openmp ayatana-appindicator
|
|
10
|
+
glib2 cairo pango gdk-pixbuf alsa-lib pulseaudio dbus
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
# Scalar verdicts a target may carry instead of a package list.
|
|
14
|
+
VERDICTS = %w[system none bundled].freeze
|
|
15
|
+
|
|
16
|
+
CANONICAL_NAME_RE = /\A[a-z0-9][a-z0-9+._-]*\z/.freeze
|
|
17
|
+
|
|
18
|
+
# A single schema problem: full path into deps.yaml + human explanation.
|
|
19
|
+
class Issue
|
|
20
|
+
attr_reader :path, :message
|
|
21
|
+
|
|
22
|
+
def initialize(path, message)
|
|
23
|
+
@path = path
|
|
24
|
+
@message = message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
"#{@path}: #{@message}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class InvalidManifestError < StandardError; end
|
|
33
|
+
|
|
34
|
+
# Parses and validates deps.yaml. Root keys are canonical dependency
|
|
35
|
+
# names; each maps to { targets: { family: { "version": [pkgs] } } } or a
|
|
36
|
+
# scalar verdict for a whole family.
|
|
37
|
+
class Manifest
|
|
38
|
+
attr_reader :path, :deps, :errors, :warnings
|
|
39
|
+
|
|
40
|
+
def self.load(path)
|
|
41
|
+
unless File.file?(path)
|
|
42
|
+
raise InvalidManifestError,
|
|
43
|
+
"Dependencies file not found: #{path}\n" \
|
|
44
|
+
'Create deps.yaml next to the Rakefile (see crosspack/README.md).'
|
|
45
|
+
end
|
|
46
|
+
begin
|
|
47
|
+
raw = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
|
|
48
|
+
rescue Psych::SyntaxError => e
|
|
49
|
+
raise InvalidManifestError,
|
|
50
|
+
"#{path}: YAML syntax error on line #{e.line}: #{e.problem}"
|
|
51
|
+
end
|
|
52
|
+
new(path, raw)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(path, raw)
|
|
56
|
+
@path = path
|
|
57
|
+
@deps = raw.is_a?(Hash) ? raw : {}
|
|
58
|
+
@raw = raw
|
|
59
|
+
@errors = []
|
|
60
|
+
@warnings = []
|
|
61
|
+
validate
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def valid?
|
|
65
|
+
@errors.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raises with the full, formatted list of problems.
|
|
69
|
+
def validate!
|
|
70
|
+
return self if valid?
|
|
71
|
+
|
|
72
|
+
lines = ["#{path}: deps.yaml schema is invalid (#{@errors.size} errors):"]
|
|
73
|
+
@errors.each { |e| lines << " ✗ #{e}" }
|
|
74
|
+
lines << 'Build aborted: fix the listed nodes and retry.'
|
|
75
|
+
raise InvalidManifestError, lines.join("\n")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Report used by rake abort: errors (if any) plus warnings.
|
|
79
|
+
def error_report
|
|
80
|
+
lines = []
|
|
81
|
+
if valid?
|
|
82
|
+
lines << "#{path}: schema is valid."
|
|
83
|
+
else
|
|
84
|
+
lines << "#{path}: deps.yaml schema is invalid (#{@errors.size} errors):"
|
|
85
|
+
@errors.each { |e| lines << " ✗ #{e}" }
|
|
86
|
+
end
|
|
87
|
+
@warnings.each { |w| lines << " ⚠ #{w}" }
|
|
88
|
+
lines.join("\n")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def validate
|
|
94
|
+
unless @raw.is_a?(Hash) && !@raw.empty?
|
|
95
|
+
@errors << Issue.new(
|
|
96
|
+
'(root)',
|
|
97
|
+
'file must be a non-empty mapping: canonical name -> { targets: ... }'
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
@raw.each { |name, body| validate_dep(name.to_s, body) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def validate_dep(name, body)
|
|
105
|
+
if name !~ CANONICAL_NAME_RE
|
|
106
|
+
@errors << Issue.new(
|
|
107
|
+
name,
|
|
108
|
+
"invalid canonical name #{name.inspect} " \
|
|
109
|
+
'(lowercase letters, digits, "-", "_", "."; must start with a letter or digit)'
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
unless KNOWN_CANONICAL.include?(name)
|
|
113
|
+
@warnings << Issue.new(
|
|
114
|
+
name,
|
|
115
|
+
'unknown to crosspack — not an error if the rules below are correct (possible typo?)'
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
unless body.is_a?(Hash) && body.key?('targets')
|
|
119
|
+
@errors << Issue.new(
|
|
120
|
+
name,
|
|
121
|
+
"missing required field targets; expected:\n" \
|
|
122
|
+
" #{name}:\n targets:\n debian:\n \"12\": [package]"
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
targets = body['targets']
|
|
128
|
+
unless targets.is_a?(Hash) && !targets.empty?
|
|
129
|
+
@errors << Issue.new(
|
|
130
|
+
"#{name}.targets",
|
|
131
|
+
"must be a non-empty mapping: family -> rules (debian, ubuntu, fedora, ...)"
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
targets.each { |family, rules| validate_family(name, family.to_s, rules) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def validate_family(name, family, rules)
|
|
139
|
+
path = "#{name}.targets.#{family}"
|
|
140
|
+
known = Target::FAMILIES.map(&:to_s)
|
|
141
|
+
unless known.include?(family)
|
|
142
|
+
suggestion = suggest(family, known)
|
|
143
|
+
hint = suggestion ? " (did you mean #{suggestion.inspect}?)" : ''
|
|
144
|
+
@errors << Issue.new(
|
|
145
|
+
path,
|
|
146
|
+
"unknown distro family #{family.inspect}#{hint}; " \
|
|
147
|
+
"allowed: #{known.join(', ')}"
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if rules.is_a?(String)
|
|
153
|
+
validate_verdict(path, rules)
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
unless rules.is_a?(Hash) && !rules.empty?
|
|
157
|
+
@errors << Issue.new(
|
|
158
|
+
path,
|
|
159
|
+
"expected a mapping { \"version\": [packages], ... } or a scalar: #{VERDICTS.join(' / ')}"
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
rules.each do |version, value|
|
|
165
|
+
vpath = "#{path}.#{version.inspect}"
|
|
166
|
+
unless version.is_a?(String)
|
|
167
|
+
@errors << Issue.new(
|
|
168
|
+
vpath,
|
|
169
|
+
"version key must be a quoted string: \"#{version}\" — " \
|
|
170
|
+
'without quotes YAML parses it as a number'
|
|
171
|
+
)
|
|
172
|
+
next
|
|
173
|
+
end
|
|
174
|
+
if value.is_a?(String)
|
|
175
|
+
validate_verdict(vpath, value)
|
|
176
|
+
elsif value.is_a?(Array)
|
|
177
|
+
validate_package_list(vpath, value)
|
|
178
|
+
else
|
|
179
|
+
@errors << Issue.new(
|
|
180
|
+
vpath,
|
|
181
|
+
"value must be a list of packages [pkg, ...] or a scalar: #{VERDICTS.join(' / ')}"
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate_verdict(path, value)
|
|
188
|
+
return if VERDICTS.include?(value)
|
|
189
|
+
|
|
190
|
+
@errors << Issue.new(
|
|
191
|
+
path,
|
|
192
|
+
"unknown verdict #{value.inspect}; allowed: #{VERDICTS.join(' / ')}"
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def validate_package_list(path, value)
|
|
197
|
+
if value.empty?
|
|
198
|
+
@errors << Issue.new(
|
|
199
|
+
path,
|
|
200
|
+
'package list is empty — specify at least one package or the none verdict'
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
end
|
|
204
|
+
value.each_with_index do |pkg, i|
|
|
205
|
+
next if pkg.is_a?(String) && !pkg.strip.empty? && pkg == pkg.strip
|
|
206
|
+
|
|
207
|
+
@errors << Issue.new(
|
|
208
|
+
"#{path}[#{i}]",
|
|
209
|
+
'package name must be a non-empty string without surrounding whitespace'
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def suggest(word, candidates)
|
|
215
|
+
best = candidates.min_by { |c| levenshtein(word, c) }
|
|
216
|
+
distance = levenshtein(word, best)
|
|
217
|
+
distance <= 2 ? best : nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def levenshtein(a, b)
|
|
221
|
+
prev = (0..b.length).to_a
|
|
222
|
+
a.chars.each_with_index do |ca, i|
|
|
223
|
+
curr = [i + 1]
|
|
224
|
+
b.chars.each_with_index do |cb, j|
|
|
225
|
+
cost = ca == cb ? 0 : 1
|
|
226
|
+
curr[j + 1] = [
|
|
227
|
+
curr[j] + 1, # insertion
|
|
228
|
+
prev[j + 1] + 1, # deletion
|
|
229
|
+
prev[j] + cost # substitution
|
|
230
|
+
].min
|
|
231
|
+
end
|
|
232
|
+
prev = curr
|
|
233
|
+
end
|
|
234
|
+
prev[b.length]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Crosspack
|
|
6
|
+
# Renders the dependency x target matrix straight from the manifest.
|
|
7
|
+
# Columns come from the targets declared in deps.yaml itself.
|
|
8
|
+
class Matrix
|
|
9
|
+
def initialize(manifest)
|
|
10
|
+
@manifest = manifest
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render
|
|
14
|
+
columns = target_columns
|
|
15
|
+
return 'deps.yaml is empty — nothing to show.' if columns.empty?
|
|
16
|
+
|
|
17
|
+
header = ['dependency'] + columns.map(&:to_s)
|
|
18
|
+
rows = @manifest.deps.map do |name, body|
|
|
19
|
+
[name] + columns.map { |t| cell(body['targets'], t) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
widths = header.each_index.map do |i|
|
|
23
|
+
([header[i].length] + rows.map { |r| r[i].to_s.length }).max
|
|
24
|
+
end
|
|
25
|
+
lines = ([header] + rows).map do |row|
|
|
26
|
+
row.each_with_index.map { |c, i| c.to_s.ljust(widths[i]) }.join(' ')
|
|
27
|
+
end
|
|
28
|
+
lines.join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Every target mentioned in the manifest: family + version pairs,
|
|
34
|
+
# ordered by the canonical family list.
|
|
35
|
+
def target_columns
|
|
36
|
+
cols = []
|
|
37
|
+
Target::FAMILIES.each do |family|
|
|
38
|
+
versions = Set.new
|
|
39
|
+
@manifest.deps.each_value do |body|
|
|
40
|
+
rule = body['targets'][family.to_s]
|
|
41
|
+
next if rule.nil?
|
|
42
|
+
|
|
43
|
+
if rule.is_a?(String)
|
|
44
|
+
versions.add(nil)
|
|
45
|
+
else
|
|
46
|
+
rule.each_key { |v| versions.add(v == '*' ? nil : v) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
versions.to_a.sort_by(&:to_s).each do |v|
|
|
50
|
+
cols << Target.new(family, v)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
cols
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cell(targets, target)
|
|
57
|
+
rule = targets[target.family.to_s]
|
|
58
|
+
return '—' if rule.nil?
|
|
59
|
+
|
|
60
|
+
value = rule.is_a?(String) ? rule : (rule[target.version] || rule['*'])
|
|
61
|
+
return '(no rule)' if value.nil?
|
|
62
|
+
return value unless value.is_a?(Array)
|
|
63
|
+
|
|
64
|
+
value.join(' | ')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Crosspack
|
|
6
|
+
# Packaging manifest (package.yaml): declares WHAT and HOW to pack —
|
|
7
|
+
# payload, executables, symlinks, desktop entry, icon. Compilation is not
|
|
8
|
+
# crosspack's business; artifacts are picked from `sources` as-is.
|
|
9
|
+
class PackageManifest
|
|
10
|
+
REQUIRED = %w[name maintainer description sources prefix payload].freeze
|
|
11
|
+
OPTIONAL = %w[summary license section lib_dir executables links desktop icon].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :path, :data, :errors, :warnings
|
|
14
|
+
|
|
15
|
+
def self.load(path)
|
|
16
|
+
unless File.file?(path)
|
|
17
|
+
raise InvalidManifestError,
|
|
18
|
+
"Packaging manifest not found: #{path}\n" \
|
|
19
|
+
'Create package.yaml next to the Rakefile (see crosspack/README.md).'
|
|
20
|
+
end
|
|
21
|
+
begin
|
|
22
|
+
raw = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
|
|
23
|
+
rescue Psych::SyntaxError => e
|
|
24
|
+
raise InvalidManifestError,
|
|
25
|
+
"#{path}: YAML syntax error on line #{e.line}: #{e.problem}"
|
|
26
|
+
end
|
|
27
|
+
new(path, raw)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(path, raw)
|
|
31
|
+
@path = path
|
|
32
|
+
@data = raw.is_a?(Hash) ? raw : {}
|
|
33
|
+
@raw = raw
|
|
34
|
+
@errors = []
|
|
35
|
+
@warnings = []
|
|
36
|
+
validate
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def valid?
|
|
40
|
+
@errors.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate!
|
|
44
|
+
return self if valid?
|
|
45
|
+
|
|
46
|
+
lines = ["#{path}: package.yaml schema is invalid (#{@errors.size} errors):"]
|
|
47
|
+
@errors.each { |e| lines << " ✗ #{e}" }
|
|
48
|
+
lines << 'Packing aborted: fix the listed nodes and retry.'
|
|
49
|
+
raise InvalidManifestError, lines.join("\n")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def error_report
|
|
53
|
+
if valid?
|
|
54
|
+
"#{path}: schema is valid."
|
|
55
|
+
else
|
|
56
|
+
["#{path}: package.yaml schema is invalid (#{@errors.size} errors):",
|
|
57
|
+
*@errors.map { |e| " ✗ #{e}" }].join("\n")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ---- typed accessors (validated values) ----
|
|
62
|
+
|
|
63
|
+
def name
|
|
64
|
+
@data['name']
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def maintainer
|
|
68
|
+
@data['maintainer']
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def description
|
|
72
|
+
@data['description'].to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def license
|
|
76
|
+
@data['license'] || 'Proprietary'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def section
|
|
80
|
+
@data['section'] || 'utils'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def sources
|
|
84
|
+
@data['sources']
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def prefix
|
|
88
|
+
@data['prefix']
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def payload
|
|
92
|
+
Array(@data['payload'])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def executables
|
|
96
|
+
Array(@data['executables'])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def summary
|
|
100
|
+
@data['summary'] || description.lines.map(&:chomp).reject(&:empty?).first || name
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def lib_dir
|
|
104
|
+
@data['lib_dir'] || File.join('lib', name.to_s)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def links
|
|
108
|
+
@data['links'].is_a?(Hash) ? @data['links'] : {}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def desktop
|
|
112
|
+
@data['desktop'].is_a?(Hash) ? @data['desktop'] : nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def icon
|
|
116
|
+
@data['icon']
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def validate
|
|
122
|
+
unless @raw.is_a?(Hash) && !@raw.empty?
|
|
123
|
+
@errors << Issue.new('(root)', 'file must be a non-empty mapping: name, maintainer, payload, ...')
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
REQUIRED.each do |key|
|
|
128
|
+
next unless @data[key].nil?
|
|
129
|
+
|
|
130
|
+
@errors << Issue.new(key, 'required field is missing')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unknown = @data.keys - REQUIRED - OPTIONAL
|
|
134
|
+
unknown.each do |key|
|
|
135
|
+
@errors << Issue.new(key, "unknown field; allowed: #{(REQUIRED + OPTIONAL).sort.join(', ')}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
validate_string('name', /\A[a-z0-9][a-z0-9+._-]*\z/)
|
|
139
|
+
validate_string('maintainer', nil)
|
|
140
|
+
validate_string('description', nil)
|
|
141
|
+
validate_string('sources', nil)
|
|
142
|
+
validate_string('prefix', /\A[a-z0-9][a-z0-9+._\/-]*\z/)
|
|
143
|
+
validate_payload
|
|
144
|
+
validate_executables
|
|
145
|
+
validate_links
|
|
146
|
+
validate_desktop
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_string(key, re)
|
|
150
|
+
value = @data[key]
|
|
151
|
+
return if value.nil? # already reported via REQUIRED
|
|
152
|
+
|
|
153
|
+
if !value.is_a?(String) || value.strip.empty?
|
|
154
|
+
@errors << Issue.new(key, 'must be a non-empty string')
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
return unless re && !value.match?(re)
|
|
158
|
+
|
|
159
|
+
@errors << Issue.new(key, "invalid value #{value.inspect}")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def validate_payload
|
|
163
|
+
value = @data['payload']
|
|
164
|
+
return if value.nil? # already reported as missing
|
|
165
|
+
|
|
166
|
+
unless value.is_a?(Array) && !value.empty?
|
|
167
|
+
@errors << Issue.new('payload', 'must be a non-empty list of artifact files from sources')
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
value.each_with_index do |item, i|
|
|
171
|
+
next if item.is_a?(String) && !item.strip.empty? && !item.start_with?('/')
|
|
172
|
+
|
|
173
|
+
@errors << Issue.new("payload[#{i}]", 'file name must be a non-empty relative string (no leading "/")')
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def validate_executables
|
|
178
|
+
value = @data['executables']
|
|
179
|
+
return if value.nil?
|
|
180
|
+
|
|
181
|
+
unless value.is_a?(Array)
|
|
182
|
+
@errors << Issue.new('executables', 'must be a list of names from payload')
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
value.each do |exe|
|
|
186
|
+
next if payload.include?(exe)
|
|
187
|
+
|
|
188
|
+
@errors << Issue.new('executables', "#{exe.inspect} is not in payload — only files from payload can be executable")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def validate_links
|
|
193
|
+
value = @data['links']
|
|
194
|
+
return if value.nil?
|
|
195
|
+
|
|
196
|
+
unless value.is_a?(Hash) && !value.empty?
|
|
197
|
+
@errors << Issue.new('links', 'must be a mapping: relative symlink path -> target')
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
value.each do |link, target|
|
|
201
|
+
if link.to_s.start_with?('/')
|
|
202
|
+
@errors << Issue.new("links.#{link}", 'symlink path must be relative to prefix (no leading "/")')
|
|
203
|
+
end
|
|
204
|
+
unless target.is_a?(String) && !target.strip.empty?
|
|
205
|
+
@errors << Issue.new("links.#{link}", 'symlink target must be a non-empty string')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def validate_desktop
|
|
211
|
+
value = @data['desktop']
|
|
212
|
+
return if value.nil?
|
|
213
|
+
|
|
214
|
+
unless value.is_a?(Hash) && !value.empty?
|
|
215
|
+
@errors << Issue.new('desktop', 'must be a mapping: name, comment, categories, exec (optional)')
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|