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.
@@ -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
- icon_src = File.expand_path(@pkg.icon, @root)
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'] || @pkg.name}
134
+ Exec=#{d['exec'] || desktop_exec}
132
135
  Icon=#{@pkg.name}
133
136
  Terminal=false
134
- Categories=#{d['categories'] || 'Utility;'}
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.summary,
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: @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] },
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crosspack
4
- VERSION = '0.7.1'
4
+ VERSION = '0.8.0'
5
5
  end
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.7.1
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.9
90
+ rubygems_version: 3.6.7
91
91
  specification_version: 4
92
92
  summary: Painless packaging for any platform
93
93
  test_files: []