crosspack 0.7.1 → 0.7.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd9fa9d325c41f9b9b56fcbaf6fca186f28fcfff888002ba7f2f896e9f93fd38
4
- data.tar.gz: c9281e44840c2f238ae49feacebb269f822f3ea15fb38681ac47f41804221bf2
3
+ metadata.gz: da2fde445d69125a1c1ebd8120b06bb06d37400cb65d5bf95ee773c89223a235
4
+ data.tar.gz: cdcefb6dcc30071b80b640e319a13e11691cdbd7b8cb8a511f07173b53da6100
5
5
  SHA512:
6
- metadata.gz: c13e601a7402da31931e3d2a8b93a7e9120369a5a157327992ed8aea827b756684f5e320e8880d4e3d1168e18d4e886ed3e6e0465d749d5c308feb628de9afa2
7
- data.tar.gz: 924c9c9a07cdbfd2661f20e07eb0137566a8935f96d7b420275ecc27c3d513e59e885cf6f3a4f2345552f8540b4466213caf9c6f87f41f6c4c03335fe558a463
6
+ metadata.gz: e05e92f827a5128d77a58c3d9655d4f5ffe0e4aaa39fd4bee1fde34e20a20ed320b6295d2f5fcffc92698afc3b3a9580a3397d1287b097c83ed6c56969ae6371
7
+ data.tar.gz: d8a2a11bfd9ea856eb1fc08322a2848be3f630003767981b5d9bf3b394127800d1bcd4f6eb165db79e64a38446653db159c256a71f27f551948335cb3bf27440
data/CHANGELOG.md CHANGED
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.3] - 2026-09-26
9
+
10
+ ### Fixed
11
+ - The manifest icon keeps its real extension everywhere: an SVG now installs
12
+ to `share/icons/hicolor/scalable/apps/<name>.svg` (it was forced to
13
+ `share/pixmaps/<name>.png`, where desktops never found it); PNG/XPM keep
14
+ going to pixmaps.
15
+ - `.desktop` `Categories` values are terminated with `;` per the spec — a
16
+ manifest value like `Utility;Audio` produced an invalid entry GNOME could
17
+ refuse to show.
18
+ - macOS `Info.plist` escapes XML entities (`&`/`<` in the summary no longer
19
+ corrupt the plist), `CFBundleExecutable` is the first `package.executables`
20
+ entry (not the package name) and the bundle icon from `icon:` is copied to
21
+ `Contents/Resources` and referenced via `CFBundleIconFile`.
22
+ - RPM `%description` carries the full package description, not just the
23
+ summary line.
24
+ - PKGBUILD quotes the `license` value (a license with spaces broke the
25
+ generated bash).
26
+ - deb omits an empty `Depends:` field instead of writing an invalid one.
27
+ - WiX `Source` paths are XML-escaped (`&` in a path no longer breaks the
28
+ generated `.wxs`).
29
+
30
+ ### Added
31
+ - The PKGBUILD now installs the `.desktop` file and the icon — desktop
32
+ integration was deb/rpm-only. The source tarball must carry the icon under
33
+ its own file name and the `.desktop` file as `<name>.desktop`.
34
+
35
+ [0.7.3]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.3
36
+
37
+ ## [0.7.2] - 2026-09-26
38
+
39
+ ### Fixed
40
+ - Windows MSI no longer fails to install with error 1324: the payload
41
+ directory is now `INSTALLFOLDER` (WiX v4+ treats any other Id as "author
42
+ did not define it" and injects a phantom `[Manufacturer] [ProductName]`
43
+ directory). The deb-style `Name <email>` maintainer is sanitized to the
44
+ name half before it reaches the MSI Manufacturer property.
45
+ - calver versions (e.g. `2026.09.26`) map to a valid MSI ProductVersion
46
+ (`26.9.26`) in the `Package@Version` attribute too, not just the file
47
+ name — a major >= 256 tripped WIX1148.
48
+
49
+ ### Added
50
+ - The Windows MSI now has a standard install wizard (`ui:WixUI
51
+ WixUI_InstallDir` with the license dialog): crosspack generates the
52
+ license `.rtf` and builds with `-ext WixToolset.UI.wixext`, installing the
53
+ UI extension itself when missing (`WixToolset.UI.wixext/6.0.1` on WiX 6).
54
+ - Start Menu and Desktop shortcuts for the first `package.executables`
55
+ entry; the display name comes from `package.desktop.name` (falling back
56
+ to the package name). Previously the winget target ignored `desktop:`.
57
+
58
+ [0.7.2]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.2
59
+
8
60
  ## [0.7.0] - 2026-09-13
9
61
 
10
62
  ### Added
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
+ <p align="center">
2
+ <img src="logo.png" width="220" alt="crosspack" />
3
+ </p>
4
+
1
5
  # crosspack
2
6
 
7
+ > Painless packaging for any platform.
8
+
3
9
  Cross-platform build **and** pack toolchain for Wails-style desktop apps,
4
10
  shipped as one gem, one command, one config file and a staged pipeline:
5
11
 
@@ -10,9 +10,11 @@ module Crosspack
10
10
  # as-is.
11
11
  class AppDir
12
12
  # files: { source_path => basename inside Contents/MacOS }
13
- # executables: basenames to chmod 0755
13
+ # executables: basenames to chmod 0755 (the first one is the bundle's
14
+ # CFBundleExecutable, falling back to the package name)
15
+ # icon: path to the bundle icon, copied into Contents/Resources
14
16
  def self.build(name:, version:, summary:, files:, executables: [],
15
- identifier: nil, output:)
17
+ display_name: nil, icon: nil, identifier: nil, output:)
16
18
  app_dir = File.join(output, "#{name}.app")
17
19
  FileUtils.rm_rf(app_dir)
18
20
  macos_dir = File.join(app_dir, 'Contents', 'MacOS')
@@ -34,9 +36,19 @@ module Crosspack
34
36
  FileUtils.chmod(executables.include?(dst) ? 0o755 : 0o644, target)
35
37
  end
36
38
 
39
+ icon_file = nil
40
+ if icon
41
+ raise BuildError, "icon: file not found #{icon}" unless File.file?(icon)
42
+
43
+ icon_file = File.basename(icon)
44
+ FileUtils.cp(icon, File.join(resources_dir, icon_file))
45
+ end
46
+
37
47
  write_info_plist(File.join(app_dir, 'Contents', 'Info.plist'),
38
48
  name: name, version: version, summary: summary,
39
- identifier: identifier)
49
+ executable: executables.first || name,
50
+ display_name: display_name || name,
51
+ icon_file: icon_file, identifier: identifier)
40
52
 
41
53
  File.write(File.join(output, 'BUILD-DMG.txt'), <<~TXT)
42
54
  DMG not built: hdiutil only exists on macOS.
@@ -46,27 +58,31 @@ module Crosspack
46
58
  app_dir
47
59
  end
48
60
 
49
- def self.write_info_plist(path, name:, version:, summary:, identifier: nil)
61
+ def self.write_info_plist(path, name:, version:, summary:, executable:,
62
+ display_name:, icon_file: nil, identifier: nil)
50
63
  bundle_id = identifier || "org.crosspack.#{name}"
64
+ # Without the extension .icns is assumed; other formats (png) must
65
+ # be named in full.
66
+ icon_ref = icon_file.nil? ? nil : icon_file.sub(/\.icns\z/i, '')
51
67
  File.write(path, <<~PLIST)
52
68
  <?xml version="1.0" encoding="UTF-8"?>
53
69
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
54
70
  <plist version="1.0">
55
71
  <dict>
56
72
  <key>CFBundleName</key>
57
- <string>#{name}</string>
73
+ <string>#{escape(name)}</string>
58
74
  <key>CFBundleDisplayName</key>
59
- <string>#{name}</string>
75
+ <string>#{escape(display_name)}</string>
60
76
  <key>CFBundleExecutable</key>
61
- <string>#{name}</string>
77
+ <string>#{escape(executable)}</string>
62
78
  <key>CFBundleIdentifier</key>
63
- <string>#{bundle_id}</string>
79
+ <string>#{escape(bundle_id)}</string>
64
80
  <key>CFBundlePackageType</key>
65
81
  <string>APPL</string>
66
82
  <key>CFBundleShortVersionString</key>
67
- <string>#{version}</string>
83
+ <string>#{escape(version)}</string>
68
84
  <key>CFBundleVersion</key>
69
- <string>#{version}</string>
85
+ <string>#{escape(version)}</string>
70
86
  <key>CFBundleInfoDictionaryVersion</key>
71
87
  <string>6.0</string>
72
88
  <key>LSMinimumSystemVersion</key>
@@ -78,11 +94,23 @@ module Crosspack
78
94
  <key>NSSupportsSuddenTermination</key>
79
95
  <false/>
80
96
  <key>NSMicrophoneUsageDescription</key>
81
- <string>#{summary}</string>
97
+ <string>#{escape(summary)}</string>
98
+ #{plist_icon_entry(icon_ref)}
82
99
  </dict>
83
100
  </plist>
84
101
  PLIST
85
102
  end
103
+
104
+ def self.plist_icon_entry(icon_ref)
105
+ return '' if icon_ref.nil?
106
+
107
+ " <key>CFBundleIconFile</key>\n <string>#{escape(icon_ref)}</string>"
108
+ end
109
+
110
+ # Escapes XML entities so manifest values never break the plist.
111
+ def self.escape(text)
112
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
113
+ end
86
114
  end
87
115
  end
88
116
  end
@@ -104,17 +104,13 @@ module Crosspack
104
104
  summary = description_lines.shift || @name
105
105
  body = description_lines.map { |l| " #{l}" }
106
106
 
107
- File.write(File.join(debian_dir, 'control'), <<~CONTROL)
108
- Package: #{@name}
109
- Version: #{@version}
110
- Section: #{@section}
111
- Priority: #{@priority}
112
- Architecture: #{@architecture}
113
- Maintainer: #{@maintainer}
114
- Depends: #{@depends}
115
- Description: #{summary}
116
- #{body.join("\n")}
117
- CONTROL
107
+ fields = ["Package: #{@name}", "Version: #{@version}", "Section: #{@section}",
108
+ "Priority: #{@priority}", "Architecture: #{@architecture}",
109
+ "Maintainer: #{@maintainer}"]
110
+ # An empty Depends: field is invalid; omit it instead.
111
+ fields << "Depends: #{@depends}" unless @depends.to_s.strip.empty?
112
+ File.write(File.join(debian_dir, 'control'),
113
+ "#{(fields + ["Description: #{summary}"]).join("\n")}\n#{body.join("\n")}\n")
118
114
  end
119
115
  end
120
116
  end
@@ -49,7 +49,7 @@ module Crosspack
49
49
  pkgdesc='#{escaped_desc}'
50
50
  arch=(#{arch.join(' ')})
51
51
  url='#{url}'
52
- license=(#{license})
52
+ license=('#{license}')
53
53
  depends=(#{depends_line})
54
54
  source=(#{source_line})
55
55
  sha256sums=('SKIP')
@@ -12,11 +12,23 @@ module Crosspack
12
12
  # complete .wxs source plus build instructions — packaging is not blocked
13
13
  # by the toolchain.
14
14
  class Wix
15
- # MSI ProductVersion only honours three numeric fields (x.y.z).
15
+ # The UI extension providing the standard install wizard dialogs.
16
+ UI_EXTENSION = 'WixToolset.UI.wixext'
17
+ # WiX v6 cannot use the unpinned extension (it pulls the incompatible
18
+ # 7.0.0, WIX6101 wixext7); 7+ resolves a matching version on its own.
19
+ UI_EXTENSION_V6_PIN = "#{UI_EXTENSION}/6.0.1"
20
+
21
+ # MSI ProductVersion honours three numeric fields (x.y.z), each 0..255:
22
+ # a calver year (2026) would trip WIX1148, so it maps to the short
23
+ # form (26) and leading zeros are dropped (2026.09.26 -> 26.9.26).
16
24
  def self.msi_version(version)
17
25
  fields = version.to_s.split(/[.-]/).first(3)
18
26
  fields << '0' while fields.size < 3
19
- fields.map { |f| f.gsub(/\D/, '').empty? ? '0' : f.gsub(/\D/, '') }.join('.')
27
+ fields.map do |f|
28
+ n = f.to_i
29
+ n %= 100 if n > 255
30
+ n.to_s
31
+ end.join('.')
20
32
  end
21
33
 
22
34
  # Deterministic UpgradeCode derived from the package name, so upgrades
@@ -27,40 +39,73 @@ module Crosspack
27
39
  "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
28
40
  end
29
41
 
30
- # Escapes XML entities so manifest values (e.g. a maintainer with
31
- # "Name <email>") never break the generated .wxs.
42
+ # Escapes XML entities so manifest values never break the generated .wxs.
32
43
  def self.escape(text)
33
44
  text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
34
45
  end
35
46
 
47
+ # The deb-style "Name <email>" maintainer must not reach the MSI
48
+ # Manufacturer property: it lands in ARP/Publisher (and an angle-bracket
49
+ # email is invalid in Windows Installer paths), so only the name half
50
+ # is kept.
51
+ def self.sanitize_manufacturer(manufacturer)
52
+ name = manufacturer.to_s.sub(/\s*<[^<>]*>\s*/, '').strip
53
+ name.empty? ? manufacturer.to_s.delete('<>') : name
54
+ end
55
+
56
+ # Minimal RTF for the WixUI_InstallDir license dialog — it requires
57
+ # WixUILicenseRtf to point at a real .rtf file, else the build fails.
58
+ def self.license_rtf(text)
59
+ escaped = text.to_s.gsub('\\', '\\\\\\\\').gsub('{', '\\{').gsub('}', '\\}')
60
+ lines = escaped.lines.map(&:chomp)
61
+ lines = [''] if lines.empty?
62
+ "{\\rtf1\\ansi\n#{lines.join("\\par\n")}\n}\n"
63
+ end
64
+
65
+ # shortcuts: nil, or { name: "Display Name", target: "app.exe" } (the
66
+ # payload file inside INSTALLFOLDER the shortcuts launch) — installs
67
+ # Start Menu and Desktop shortcuts.
36
68
  def self.generate_wxs(name:, version:, manufacturer:, summary:, files:,
37
- arch: 'x64')
69
+ arch: 'x64', shortcuts: nil)
70
+ license_rtf_file = "#{name}-license.rtf"
38
71
  name = escape(name)
39
- manufacturer = escape(manufacturer)
72
+ manufacturer = escape(sanitize_manufacturer(manufacturer))
40
73
  summary = escape(summary)
41
74
  directory = arch == 'arm64' ? 'ProgramFiles6432Folder' : 'ProgramFiles64Folder'
42
75
  components = files.values.each_with_index.map do |dst, i|
43
- src = files.key(dst).gsub('/', '\\')
76
+ src = escape(files.key(dst).gsub('/', '\\'))
44
77
  " <Component Id=\"c#{i}\" Guid=\"*\">\n" \
45
78
  " <File Id=\"f#{i}\" Source=\"#{src}\" />\n" \
46
79
  ' </Component>'
47
80
  end.join("\n")
48
81
 
82
+ # Id must be INSTALLFOLDER: WiX v4+ auto-creates a directory with
83
+ # this exact Id (named "[Manufacturer] [ProductName]") only when the
84
+ # author has not defined one; a different Id makes it add a phantom
85
+ # sibling directory and Manufacturer leaks into install paths.
86
+ shortcut_xml = shortcuts ? shortcuts_fragment(shortcuts) : nil
87
+ feature_refs = ["<ComponentGroupRef Id=\"Payload\" />"]
88
+ feature_refs << '<ComponentGroupRef Id="Shortcuts" />' if shortcuts
89
+
49
90
  <<~WXS
50
91
  <?xml version="1.0" encoding="UTF-8"?>
51
- <!-- Generated by crosspack. Build with: wix build -arch #{arch} -o #{name}.msi #{name}.wxs -->
52
- <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
92
+ <!-- Generated by crosspack. Build with: wix build -arch #{arch} -ext #{UI_EXTENSION} -o #{name}.msi #{name}.wxs -->
93
+ <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
94
+ xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
53
95
  <Package Name="#{name}" Manufacturer="#{manufacturer}" Version="#{msi_version(version)}"
54
96
  UpgradeCode="{#{upgrade_code(name)}}" Scope="perMachine">
55
97
  <SummaryInformation Description="#{summary}" />
56
98
  <MajorUpgrade DowngradeErrorMessage="A newer version of #{name} is already installed." />
57
99
 
58
100
  <StandardDirectory Id="#{directory}">
59
- <Directory Id="INSTALLDIR" Name="#{name}" />
101
+ <Directory Id="INSTALLFOLDER" Name="#{name}" />
60
102
  </StandardDirectory>
61
103
 
104
+ <Property Id="WixUILicenseRtf" Value="#{license_rtf_file}" />
105
+ <ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
106
+
62
107
  <Feature Id="Main" Title="#{name}" Level="1">
63
- <ComponentGroupRef Id="Payload" />
108
+ #{feature_refs.map { |r| " #{r}" }.join("\n")}
64
109
  </Feature>
65
110
  </Package>
66
111
 
@@ -69,14 +114,40 @@ module Crosspack
69
114
  #{components}
70
115
  </ComponentGroup>
71
116
  </Fragment>
117
+ #{shortcut_xml}
72
118
  </Wix>
73
119
  WXS
74
120
  end
75
121
 
76
- # Writes the .wxs next to the output and, when WiX is installed,
77
- # builds a real MSI. Returns the path of whatever was produced.
122
+ # Start Menu + Desktop shortcuts for the main executable. The shortcut
123
+ # component's KeyPath is a marker registry value (a shortcut cannot be
124
+ # a KeyPath itself); the icon comes from the exe.
125
+ def self.shortcuts_fragment(shortcuts)
126
+ name = escape(shortcuts[:name])
127
+ target = escape(shortcuts[:target])
128
+ <<~XML
129
+ <Fragment>
130
+ <ComponentGroup Id="Shortcuts" Directory="INSTALLFOLDER">
131
+ <Component Id="c.shortcuts" Guid="*">
132
+ <Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder"
133
+ Name="#{name}" Target="[INSTALLFOLDER]#{target}"
134
+ WorkingDirectory="INSTALLFOLDER" />
135
+ <Shortcut Id="DesktopShortcut" Directory="DesktopFolder"
136
+ Name="#{name}" Target="[INSTALLFOLDER]#{target}"
137
+ WorkingDirectory="INSTALLFOLDER" />
138
+ <RegistryValue Root="HKLM" Key="Software\\#{name}"
139
+ Name="installed" Type="integer" Value="1" KeyPath="yes" />
140
+ </Component>
141
+ </ComponentGroup>
142
+ </Fragment>
143
+ XML
144
+ end
145
+
146
+ # Writes the .wxs (plus the license .rtf the UI wizard shows) next to
147
+ # the output and, when WiX is installed, builds a real MSI. Returns
148
+ # the path of whatever was produced.
78
149
  def self.build(name:, version:, manufacturer:, summary:, files:, output:,
79
- arch: 'x64')
150
+ arch: 'x64', shortcuts: nil, license: nil)
80
151
  arch = { 'x86_64' => 'x64', 'aarch64' => 'arm64' }.fetch(arch, arch)
81
152
  missing = files.reject { |src, _dst| File.file?(src) }
82
153
  unless missing.empty?
@@ -92,7 +163,9 @@ module Crosspack
92
163
  File.write(wxs_path, generate_wxs(name: name, version: version,
93
164
  manufacturer: manufacturer,
94
165
  summary: summary, files: files,
95
- arch: arch))
166
+ arch: arch, shortcuts: shortcuts))
167
+ File.write(File.join(out_dir, "#{name}-license.rtf"),
168
+ license_rtf(license || summary))
96
169
 
97
170
  wix = wix_command
98
171
  unless wix
@@ -100,13 +173,17 @@ module Crosspack
100
173
  MSI not built: WiX toolset not found in PATH.
101
174
  Install the .NET SDK and WiX:
102
175
  dotnet tool install -g wix
176
+ The install wizard needs the UI extension:
177
+ wix extension add -g #{UI_EXTENSION_V6_PIN} # WiX 6.x (no pin needed for WiX 7+)
103
178
  Then build with:
104
- wix build -arch #{arch} -o #{File.basename(output)} #{name}.wxs
179
+ wix build -arch #{arch} -ext #{UI_EXTENSION} -o #{File.basename(output)} #{name}.wxs
105
180
  TXT
106
181
  return wxs_path
107
182
  end
108
183
 
184
+ ensure_ui_extension(wix)
109
185
  out, status = Open3.capture2e(wix, 'build', '-arch', arch.to_s,
186
+ '-ext', UI_EXTENSION,
110
187
  '-o', output, wxs_path)
111
188
  unless status.success?
112
189
  raise BuildError, "wix build failed to build #{output}:\n#{out}"
@@ -124,6 +201,21 @@ module Crosspack
124
201
  rescue Errno::ENOENT
125
202
  nil
126
203
  end
204
+
205
+ # The WixUI dialogs live in the UI extension, so it must be installed
206
+ # before `wix build -ext WixToolset.UI.wixext` can resolve it. A no-op
207
+ # when it is already present.
208
+ def self.ensure_ui_extension(wix = 'wix')
209
+ list, = Open3.capture2e(wix, 'extension', 'list', '-g')
210
+ return if list.include?(UI_EXTENSION)
211
+
212
+ version, = Open3.capture2e(wix, '--version')
213
+ spec = version.to_i >= 7 ? UI_EXTENSION : UI_EXTENSION_V6_PIN
214
+ out, status = Open3.capture2e(wix, 'extension', 'add', '-g', spec)
215
+ return if status.success?
216
+
217
+ raise BuildError, "cannot install the WiX UI extension (#{spec}):\n#{out}"
218
+ end
127
219
  end
128
220
  end
129
221
  end
@@ -99,10 +99,7 @@ module Crosspack
99
99
  files = @pkg.payload_for(@target).to_h { |n| [File.join(src_dir, n), File.join(lib_dst, n)] }
100
100
 
101
101
  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")
102
+ files[icon_source] = icon_destination
106
103
  end
107
104
 
108
105
  if @pkg.desktop
@@ -123,6 +120,10 @@ module Crosspack
123
120
  FileUtils.mkdir_p(dir)
124
121
  path = File.join(dir, "#{@pkg.name}.desktop")
125
122
  d = @pkg.desktop
123
+ # The spec requires each list value terminated with ";"; a manifest
124
+ # value like "Utility;Audio" would otherwise produce an invalid entry.
125
+ categories = d['categories'].to_s.empty? ? 'Utility;' : d['categories'].to_s
126
+ categories += ';' unless categories.end_with?(';')
126
127
  File.write(path, <<~DESKTOP)
127
128
  [Desktop Entry]
128
129
  Type=Application
@@ -131,11 +132,30 @@ module Crosspack
131
132
  Exec=#{d['exec'] || @pkg.name}
132
133
  Icon=#{@pkg.name}
133
134
  Terminal=false
134
- Categories=#{d['categories'] || 'Utility;'}
135
+ Categories=#{categories}
135
136
  DESKTOP
136
137
  path
137
138
  end
138
139
 
140
+ def icon_source
141
+ src = File.expand_path(@pkg.icon, @root)
142
+ raise BuildError, "icon: file not found #{src}" unless File.file?(src)
143
+
144
+ src
145
+ end
146
+
147
+ # Desktop icon lookup keeps the real extension: PNG/XPM go to the
148
+ # classic pixmaps dir, SVG needs the hicolor scalable theme dir (a .svg
149
+ # named .png in pixmaps is never found).
150
+ def icon_destination
151
+ ext = File.extname(@pkg.icon).downcase
152
+ if ext == '.svg'
153
+ File.join(@pkg.prefix, 'share', 'icons', 'hicolor', 'scalable', 'apps', "#{@pkg.name}#{ext}")
154
+ else
155
+ File.join(@pkg.prefix, 'share', 'pixmaps', "#{@pkg.name}#{ext}")
156
+ end
157
+ end
158
+
139
159
  def resolved_depends(format)
140
160
  Resolver.new(@deps).depends(@target, format: format)
141
161
  end
@@ -173,7 +193,7 @@ module Crosspack
173
193
  files: @pkg_data[:files],
174
194
  symlinks: @pkg_data[:symlinks],
175
195
  executables: @pkg_data[:executables],
176
- description: @pkg.summary,
196
+ description: @pkg.description,
177
197
  arch: @target.package_arch(:rpm),
178
198
  output: output
179
199
  )
@@ -183,6 +203,15 @@ module Crosspack
183
203
  def pack_pkgbuild
184
204
  version, release = split_version
185
205
  output = File.join(@target.output_dir(@output_base, :pkgbuild), 'PKGBUILD')
206
+ # Same desktop integration as deb/rpm: the .desktop file and the icon
207
+ # go to the same destinations; their sources are archive-relative
208
+ # names (the icon under its own file name).
209
+ files = @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] }
210
+ if @pkg.desktop
211
+ files["#{@pkg.name}.desktop"] = File.join(@pkg.prefix, 'share', 'applications',
212
+ "#{@pkg.name}.desktop")
213
+ end
214
+ files[File.basename(@pkg.icon)] = icon_destination if @pkg.icon
186
215
  Builders::Pkgbuild.generate(
187
216
  name: @pkg.name,
188
217
  version: version,
@@ -192,7 +221,7 @@ module Crosspack
192
221
  depends: resolved_depends(:pkgbuild),
193
222
  arch: [@target.package_arch(:pkgbuild)],
194
223
  # 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)] },
224
+ files: files,
196
225
  symlinks: @pkg_data[:symlinks],
197
226
  executables: @pkg_data[:executables],
198
227
  source: ["#{@pkg.name}-#{version}.tar.gz"],
@@ -210,6 +239,7 @@ module Crosspack
210
239
 
211
240
  # Windows: flat payload under Program Files/<name>/ via WiX. Unix symlinks
212
241
  # do not apply; everything from `sources` goes in as regular files.
242
+ # desktop:/executables: drive the Start Menu / Desktop shortcuts.
213
243
  def pack_windows
214
244
  output = File.join(@target.output_dir(@output_base, :winget),
215
245
  "#{@pkg.name}-#{Builders::Wix.msi_version(@version)}.msi")
@@ -219,22 +249,38 @@ module Crosspack
219
249
  version: @version,
220
250
  manufacturer: @pkg.maintainer,
221
251
  summary: @pkg.summary,
252
+ license: @pkg.license,
222
253
  files: files,
254
+ shortcuts: windows_shortcuts,
223
255
  arch: @target.package_arch(:winget),
224
256
  output: output
225
257
  )
226
258
  end
227
259
 
260
+ # Shortcut facts for the MSI: the display name from desktop: (falling
261
+ # back to the package name) and the first executable as the target.
262
+ def windows_shortcuts
263
+ target = @pkg.executables_for(@target).first
264
+ return nil if target.nil?
265
+
266
+ { name: (@pkg.desktop && @pkg.desktop['name']) || @pkg.name,
267
+ target: target }
268
+ end
269
+
228
270
  # macOS: .app bundle staging; DMG itself requires a Mac (hdiutil).
271
+ # desktop.name becomes the display name; the manifest icon lands in
272
+ # Contents/Resources (CFBundleIconFile).
229
273
  def pack_macos
230
274
  output_dir = @target.output_dir(@output_base, :cask)
231
275
  files = @pkg.payload_for(@target).to_h { |n| [File.join(@pkg_data[:src_dir], n), n] }
232
276
  Builders::AppDir.build(
233
277
  name: @pkg.name,
278
+ display_name: (@pkg.desktop && @pkg.desktop['name']) || @pkg.name,
234
279
  version: @version,
235
280
  summary: @pkg.summary,
236
281
  files: files,
237
282
  executables: @pkg.executables_for(@target),
283
+ icon: @pkg.icon && File.expand_path(@pkg.icon, @root),
238
284
  output: output_dir
239
285
  )
240
286
  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.7.3'
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.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Orlov