crosspack 0.7.1 → 0.8.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 +4 -4
- data/CHANGELOG.md +85 -0
- data/README.md +72 -15
- data/exe/crosspack +37 -19
- data/lib/crossbuild/build_manifest.rb +43 -6
- data/lib/crossbuild/builder.rb +45 -9
- data/lib/crossbuild/matrix.rb +5 -0
- data/lib/crossbuild.rb +7 -6
- data/lib/crosspack/builders/app_dir.rb +43 -12
- data/lib/crosspack/builders/deb.rb +40 -11
- data/lib/crosspack/builders/pkgbuild.rb +7 -6
- data/lib/crosspack/builders/rpm.rb +24 -0
- data/lib/crosspack/builders/wix.rb +120 -16
- data/lib/crosspack/launcher.rb +16 -9
- data/lib/crosspack/package_manifest.rb +8 -1
- data/lib/crosspack/packer.rb +120 -8
- data/lib/crosspack/version.rb +1 -1
- metadata +2 -2
data/lib/crosspack/packer.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'tmpdir'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'digest'
|
|
5
7
|
|
|
6
8
|
module Crosspack
|
|
7
9
|
# Single entry point: takes a Config (the package: and deps: sections of
|
|
@@ -99,10 +101,7 @@ module Crosspack
|
|
|
99
101
|
files = @pkg.payload_for(@target).to_h { |n| [File.join(src_dir, n), File.join(lib_dst, n)] }
|
|
100
102
|
|
|
101
103
|
if @pkg.icon
|
|
102
|
-
|
|
103
|
-
raise BuildError, "icon: file not found #{icon_src}" unless File.file?(icon_src)
|
|
104
|
-
|
|
105
|
-
files[icon_src] = File.join(@pkg.prefix, 'share', 'pixmaps', "#{@pkg.name}.png")
|
|
104
|
+
files[icon_source] = icon_destination
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
if @pkg.desktop
|
|
@@ -123,19 +122,55 @@ module Crosspack
|
|
|
123
122
|
FileUtils.mkdir_p(dir)
|
|
124
123
|
path = File.join(dir, "#{@pkg.name}.desktop")
|
|
125
124
|
d = @pkg.desktop
|
|
125
|
+
# The spec requires each list value terminated with ";"; a manifest
|
|
126
|
+
# value like "Utility;Audio" would otherwise produce an invalid entry.
|
|
127
|
+
categories = d['categories'].to_s.empty? ? 'Utility;' : d['categories'].to_s
|
|
128
|
+
categories += ';' unless categories.end_with?(';')
|
|
126
129
|
File.write(path, <<~DESKTOP)
|
|
127
130
|
[Desktop Entry]
|
|
128
131
|
Type=Application
|
|
129
132
|
Name=#{d['name'] || @pkg.name}
|
|
130
133
|
Comment=#{d['comment'] || @pkg.summary}
|
|
131
|
-
Exec=#{d['exec'] ||
|
|
134
|
+
Exec=#{d['exec'] || desktop_exec}
|
|
132
135
|
Icon=#{@pkg.name}
|
|
133
136
|
Terminal=false
|
|
134
|
-
Categories=#{
|
|
137
|
+
Categories=#{categories}
|
|
135
138
|
DESKTOP
|
|
136
139
|
path
|
|
137
140
|
end
|
|
138
141
|
|
|
142
|
+
# Absolute launcher path: a bare name only resolves when prefix/bin is
|
|
143
|
+
# in PATH (true for usr/local, not for custom prefixes). Prefer the
|
|
144
|
+
# bin/ symlink from links:, else the executable inside lib_dir.
|
|
145
|
+
def desktop_exec
|
|
146
|
+
link = @pkg.links.keys.find { |l| l.split('/').first == 'bin' }
|
|
147
|
+
return File.join('/', @pkg.prefix, link) if link
|
|
148
|
+
|
|
149
|
+
exe = @pkg.executables.first
|
|
150
|
+
return File.join('/', @pkg.prefix, @pkg.lib_dir, exe) if exe
|
|
151
|
+
|
|
152
|
+
@pkg.name
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def icon_source
|
|
156
|
+
src = File.expand_path(@pkg.icon, @root)
|
|
157
|
+
raise BuildError, "icon: file not found #{src}" unless File.file?(src)
|
|
158
|
+
|
|
159
|
+
src
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Desktop icon lookup keeps the real extension: PNG/XPM go to the
|
|
163
|
+
# classic pixmaps dir, SVG needs the hicolor scalable theme dir (a .svg
|
|
164
|
+
# named .png in pixmaps is never found).
|
|
165
|
+
def icon_destination
|
|
166
|
+
ext = File.extname(@pkg.icon).downcase
|
|
167
|
+
if ext == '.svg'
|
|
168
|
+
File.join(@pkg.prefix, 'share', 'icons', 'hicolor', 'scalable', 'apps', "#{@pkg.name}#{ext}")
|
|
169
|
+
else
|
|
170
|
+
File.join(@pkg.prefix, 'share', 'pixmaps', "#{@pkg.name}#{ext}")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
139
174
|
def resolved_depends(format)
|
|
140
175
|
Resolver.new(@deps).depends(@target, format: format)
|
|
141
176
|
end
|
|
@@ -173,7 +208,7 @@ module Crosspack
|
|
|
173
208
|
files: @pkg_data[:files],
|
|
174
209
|
symlinks: @pkg_data[:symlinks],
|
|
175
210
|
executables: @pkg_data[:executables],
|
|
176
|
-
description: @pkg.
|
|
211
|
+
description: @pkg.description,
|
|
177
212
|
arch: @target.package_arch(:rpm),
|
|
178
213
|
output: output
|
|
179
214
|
)
|
|
@@ -183,6 +218,24 @@ module Crosspack
|
|
|
183
218
|
def pack_pkgbuild
|
|
184
219
|
version, release = split_version
|
|
185
220
|
output = File.join(@target.output_dir(@output_base, :pkgbuild), 'PKGBUILD')
|
|
221
|
+
# Same desktop integration as deb/rpm: the .desktop file and the icon
|
|
222
|
+
# go to the same destinations; their sources are archive-relative
|
|
223
|
+
# names (the icon under its own file name).
|
|
224
|
+
sources = @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:src_dir], n)] }
|
|
225
|
+
files = @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] }
|
|
226
|
+
if @pkg.desktop
|
|
227
|
+
files["#{@pkg.name}.desktop"] = File.join(@pkg.prefix, 'share', 'applications',
|
|
228
|
+
"#{@pkg.name}.desktop")
|
|
229
|
+
sources["#{@pkg.name}.desktop"] = desktop_entry_path
|
|
230
|
+
end
|
|
231
|
+
if @pkg.icon
|
|
232
|
+
files[File.basename(@pkg.icon)] = icon_destination
|
|
233
|
+
sources[File.basename(@pkg.icon)] = icon_source
|
|
234
|
+
end
|
|
235
|
+
sha256 = write_source_tarball(
|
|
236
|
+
File.join(File.dirname(output), "#{@pkg.name}-#{version}.tar.gz"),
|
|
237
|
+
"#{@pkg.name}-#{version}", sources
|
|
238
|
+
)
|
|
186
239
|
Builders::Pkgbuild.generate(
|
|
187
240
|
name: @pkg.name,
|
|
188
241
|
version: version,
|
|
@@ -192,16 +245,41 @@ module Crosspack
|
|
|
192
245
|
depends: resolved_depends(:pkgbuild),
|
|
193
246
|
arch: [@target.package_arch(:pkgbuild)],
|
|
194
247
|
# PKGBUILD sources are archive-relative names, not local paths.
|
|
195
|
-
files:
|
|
248
|
+
files: files,
|
|
196
249
|
symlinks: @pkg_data[:symlinks],
|
|
197
250
|
executables: @pkg_data[:executables],
|
|
198
251
|
source: ["#{@pkg.name}-#{version}.tar.gz"],
|
|
252
|
+
sha256: sha256,
|
|
199
253
|
license: @pkg.license,
|
|
200
254
|
output: output
|
|
201
255
|
)
|
|
202
256
|
output
|
|
203
257
|
end
|
|
204
258
|
|
|
259
|
+
# makepkg needs the source archive referenced by the PKGBUILD, and it
|
|
260
|
+
# must extract into <name>-<version>/ with the exact file names the
|
|
261
|
+
# install lines use. Crosspack builds it from the staged artifacts, so
|
|
262
|
+
# the PKGBUILD works out of the box (with a real sha256 instead of
|
|
263
|
+
# SKIP). Returns the hex digest.
|
|
264
|
+
def write_source_tarball(tarball, root, sources)
|
|
265
|
+
require 'tmpdir'
|
|
266
|
+
missing = sources.values.reject { |src| File.file?(src) }
|
|
267
|
+
unless missing.empty?
|
|
268
|
+
raise BuildError, "Source files for the PKGBUILD tarball not found:\n" \
|
|
269
|
+
"#{missing.map { |s| " ✗ #{s}" }.join("\n")}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
FileUtils.mkdir_p(File.dirname(tarball))
|
|
273
|
+
Dir.mktmpdir('crosspack-pkgbuild') do |stage|
|
|
274
|
+
root_dir = File.join(stage, root)
|
|
275
|
+
FileUtils.mkdir_p(root_dir)
|
|
276
|
+
sources.each_value { |src| FileUtils.cp(src, File.join(root_dir, File.basename(src))) }
|
|
277
|
+
out, status = Open3.capture2e('tar', '-czf', tarball, '-C', stage, root)
|
|
278
|
+
raise BuildError, "tar failed to build #{tarball}:\n#{out}" unless status.success?
|
|
279
|
+
end
|
|
280
|
+
Digest::SHA256.file(tarball).hexdigest
|
|
281
|
+
end
|
|
282
|
+
|
|
205
283
|
def split_version
|
|
206
284
|
parts = @version.split('-', 2)
|
|
207
285
|
parts << '1' if parts.size == 1
|
|
@@ -210,6 +288,7 @@ module Crosspack
|
|
|
210
288
|
|
|
211
289
|
# Windows: flat payload under Program Files/<name>/ via WiX. Unix symlinks
|
|
212
290
|
# do not apply; everything from `sources` goes in as regular files.
|
|
291
|
+
# desktop:/executables: drive the Start Menu / Desktop shortcuts.
|
|
213
292
|
def pack_windows
|
|
214
293
|
output = File.join(@target.output_dir(@output_base, :winget),
|
|
215
294
|
"#{@pkg.name}-#{Builders::Wix.msi_version(@version)}.msi")
|
|
@@ -219,22 +298,55 @@ module Crosspack
|
|
|
219
298
|
version: @version,
|
|
220
299
|
manufacturer: @pkg.maintainer,
|
|
221
300
|
summary: @pkg.summary,
|
|
301
|
+
license: @pkg.license,
|
|
222
302
|
files: files,
|
|
303
|
+
shortcuts: windows_shortcuts,
|
|
304
|
+
icon: windows_arp_icon,
|
|
223
305
|
arch: @target.package_arch(:winget),
|
|
224
306
|
output: output
|
|
225
307
|
)
|
|
226
308
|
end
|
|
227
309
|
|
|
310
|
+
# The ARP icon must be an .ico (the MSI Icons table does not take a
|
|
311
|
+
# png). The manifest icon is used when it is one; otherwise a sibling
|
|
312
|
+
# .ico with the same basename (appicon.png -> appicon.ico) is picked up
|
|
313
|
+
# when the build produces it. A png-only project gets no ARP icon — the
|
|
314
|
+
# exe icon still shows in Start Menu shortcuts.
|
|
315
|
+
def windows_arp_icon
|
|
316
|
+
return nil unless @pkg.icon
|
|
317
|
+
|
|
318
|
+
src = File.expand_path(@pkg.icon, @root)
|
|
319
|
+
return src if File.file?(src) && File.extname(src).casecmp('.ico').zero?
|
|
320
|
+
|
|
321
|
+
sibling = File.join(File.dirname(src), "#{File.basename(src, '.*')}.ico")
|
|
322
|
+
File.file?(sibling) ? sibling : nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Shortcut facts for the MSI: the display name from desktop: (falling
|
|
326
|
+
# back to the package name) and the first executable as the target.
|
|
327
|
+
def windows_shortcuts
|
|
328
|
+
target = @pkg.executables_for(@target).first
|
|
329
|
+
return nil if target.nil?
|
|
330
|
+
|
|
331
|
+
{ name: (@pkg.desktop && @pkg.desktop['name']) || @pkg.name,
|
|
332
|
+
target: target }
|
|
333
|
+
end
|
|
334
|
+
|
|
228
335
|
# macOS: .app bundle staging; DMG itself requires a Mac (hdiutil).
|
|
336
|
+
# desktop.name becomes the display name; the manifest icon lands in
|
|
337
|
+
# Contents/Resources (CFBundleIconFile).
|
|
229
338
|
def pack_macos
|
|
230
339
|
output_dir = @target.output_dir(@output_base, :cask)
|
|
231
340
|
files = @pkg.payload_for(@target).to_h { |n| [File.join(@pkg_data[:src_dir], n), n] }
|
|
232
341
|
Builders::AppDir.build(
|
|
233
342
|
name: @pkg.name,
|
|
343
|
+
display_name: (@pkg.desktop && @pkg.desktop['name']) || @pkg.name,
|
|
234
344
|
version: @version,
|
|
235
345
|
summary: @pkg.summary,
|
|
236
346
|
files: files,
|
|
237
347
|
executables: @pkg.executables_for(@target),
|
|
348
|
+
icon: @pkg.icon && File.expand_path(@pkg.icon, @root),
|
|
349
|
+
min_macos: @pkg.min_macos,
|
|
238
350
|
output: output_dir
|
|
239
351
|
)
|
|
240
352
|
end
|
data/lib/crosspack/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crosspack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Orlov
|
|
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
88
|
version: '0'
|
|
89
89
|
requirements: []
|
|
90
|
-
rubygems_version: 3.6.
|
|
90
|
+
rubygems_version: 3.6.7
|
|
91
91
|
specification_version: 4
|
|
92
92
|
summary: Painless packaging for any platform
|
|
93
93
|
test_files: []
|