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 +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +6 -0
- data/lib/crosspack/builders/app_dir.rb +39 -11
- data/lib/crosspack/builders/deb.rb +7 -11
- data/lib/crosspack/builders/pkgbuild.rb +1 -1
- data/lib/crosspack/builders/wix.rb +108 -16
- data/lib/crosspack/packer.rb +53 -7
- data/lib/crosspack/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da2fde445d69125a1c1ebd8120b06bb06d37400cb65d5bf95ee773c89223a235
|
|
4
|
+
data.tar.gz: cdcefb6dcc30071b80b640e319a13e11691cdbd7b8cb8a511f07173b53da6100
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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:,
|
|
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>#{
|
|
75
|
+
<string>#{escape(display_name)}</string>
|
|
60
76
|
<key>CFBundleExecutable</key>
|
|
61
|
-
<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('&', '&').gsub('<', '<').gsub('>', '>')
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
@@ -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
|
-
#
|
|
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
|
|
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
|
|
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('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
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="
|
|
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
|
-
|
|
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
|
-
#
|
|
77
|
-
#
|
|
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
|
data/lib/crosspack/packer.rb
CHANGED
|
@@ -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
|
-
|
|
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=#{
|
|
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.
|
|
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:
|
|
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
|
data/lib/crosspack/version.rb
CHANGED