crosspack 0.7.3 → 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 +35 -2
- data/README.md +66 -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 +7 -4
- data/lib/crosspack/builders/deb.rb +33 -0
- data/lib/crosspack/builders/pkgbuild.rb +6 -5
- data/lib/crosspack/builders/rpm.rb +24 -0
- data/lib/crosspack/builders/wix.rb +17 -5
- data/lib/crosspack/launcher.rb +16 -9
- data/lib/crosspack/package_manifest.rb +8 -1
- data/lib/crosspack/packer.rb +68 -2
- data/lib/crosspack/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5848f2a2de5e0c3a6d88dbb5160b90699f8e6b0075af0598a703d76a3a67a6fe
|
|
4
|
+
data.tar.gz: c653c7473c232c97d22e13656e7def070da7c478ad18ddbc44e8f9d596faa115
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52df014c507a0746bc1573c9642ddda9cfa4cbaebbbdc763cda877bd6507ebf2119ce716b0ab4e69eb1abd8408025d5e0b5aa7e7928464f1e811784ffc0ccd24
|
|
7
|
+
data.tar.gz: 548c13c22c057b1d737673421cb3b9ce71c9dc5cb192cf5e0e0f9a7962f9251ddc1011e85516503c7fad110b9c1bb05ec91ae45605382bf8dd3fc6f0b9a3c29a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.8.0] - 2026-09-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Make-style build goals: a matrix entry may declare `goals:` and its `id`
|
|
12
|
+
doubles as a goal name, so a per-binary entry works with no extra syntax.
|
|
13
|
+
`crosspack build` compiles only the `default` goal's entries,
|
|
14
|
+
`crosspack build all` every host-buildable entry, `crosspack build
|
|
15
|
+
<goal>` one goal or binary (`crosspack build helloworld` rebuilds nothing
|
|
16
|
+
else). `crosspack run [goal]` builds just that goal before launching, and
|
|
17
|
+
launches the executable named by a binary goal instead of the first one;
|
|
18
|
+
`crosspack build <package-target>` (e.g. `debian-12`) still works when
|
|
19
|
+
the name is not a goal or entry id. `crosspack matrix` and
|
|
20
|
+
`crosspack validate` list each entry's goals.
|
|
21
|
+
|
|
22
|
+
[0.8.0]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.8.0
|
|
23
|
+
|
|
8
24
|
## [0.7.3] - 2026-09-26
|
|
9
25
|
|
|
10
26
|
### Fixed
|
|
@@ -29,8 +45,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
29
45
|
|
|
30
46
|
### Added
|
|
31
47
|
- The PKGBUILD now installs the `.desktop` file and the icon — desktop
|
|
32
|
-
integration was deb/rpm-only.
|
|
33
|
-
|
|
48
|
+
integration was deb/rpm-only. Crosspack also builds the source tarball
|
|
49
|
+
(`<name>-<version>.tar.gz`) next to the PKGBUILD with the exact file names
|
|
50
|
+
and layout the install lines expect, so `makepkg` works out of the box —
|
|
51
|
+
and the PKGBUILD carries its real sha256 instead of `SKIP`.
|
|
52
|
+
- deb/rpm packages with a `.desktop` file or a themed icon refresh the
|
|
53
|
+
application database and the hicolor icon cache (`postinst`/`postrm`,
|
|
54
|
+
`%post`/`%postun`); the tools are guarded, minimal systems without them
|
|
55
|
+
are fine.
|
|
56
|
+
- Windows MSI gets an Add/Remove Programs icon (`ARPPRODUCTICON`): the
|
|
57
|
+
manifest `icon:` when it is an `.ico`, else a sibling `.ico` with the same
|
|
58
|
+
basename (`appicon.png` -> `appicon.ico`) when the build produces one.
|
|
59
|
+
- `package.min_macos` sets the .app bundle's `LSMinimumSystemVersion`
|
|
60
|
+
(default stays 11.0).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- `.desktop` `Exec` is now an absolute path — the launcher symlink from
|
|
64
|
+
`links:` (e.g. `/usr/local/bin/h2voice`) or the executable inside
|
|
65
|
+
`lib_dir`; a bare name only resolved when `prefix/bin` happened to be in
|
|
66
|
+
PATH.
|
|
34
67
|
|
|
35
68
|
[0.7.3]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.3
|
|
36
69
|
|
data/README.md
CHANGED
|
@@ -11,16 +11,19 @@ shipped as one gem, one command, one config file and a staged pipeline:
|
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
crosspack deps <target> # verify/install the build host's dependencies
|
|
14
|
-
crosspack build <
|
|
14
|
+
crosspack build [<goal>] # compile a build goal's matrix entries into builds/
|
|
15
15
|
crosspack pack <target> # package builds/<target> as a native package
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
The build stage is keyed by **goals** (make-style targets): `crosspack build`
|
|
19
|
+
runs only the `default` goal, `crosspack build all` every goal and
|
|
20
|
+
`crosspack build <goal>` one named goal — unrelated binaries are never
|
|
21
|
+
rebuilt together. The deps and pack stages are keyed by the target
|
|
22
|
+
vocabulary — `debian-12`, `ubuntu-24.04`, `fedora-41`, `arch`, `macos`,
|
|
23
|
+
`windows-11.0` — and gated by the previous one: `build` refuses to start
|
|
24
|
+
before the deps stage passes, and `pack` refuses to run before `build`
|
|
25
|
+
filled `builds/<target>/`. The build stage also stamps the version into the
|
|
26
|
+
tree, so `pack` normally needs no `--version`:
|
|
24
27
|
|
|
25
28
|
```
|
|
26
29
|
crosspack deps ubuntu-24.04
|
|
@@ -37,6 +40,50 @@ token):
|
|
|
37
40
|
crosspack deps && crosspack build && crosspack pack
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
## Build goals
|
|
44
|
+
|
|
45
|
+
Build goals are make-style targets — usually one binary each. A matrix entry
|
|
46
|
+
is one goal's recipe; the entry `id` doubles as the goal name, so a
|
|
47
|
+
per-binary entry needs no extra syntax. `goals:` regroups entries when one
|
|
48
|
+
goal should cover several (an entry without `goals:` belongs to the implicit
|
|
49
|
+
`default` goal; one entry may serve several goals):
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
build:
|
|
53
|
+
matrix:
|
|
54
|
+
- id: helloworld # goal name = entry id
|
|
55
|
+
build: linux/amd64
|
|
56
|
+
steps: [go build -o build/bin/helloworld ./cmd/helloworld]
|
|
57
|
+
artifacts: { from: build/bin, include: [helloworld], to: [ubuntu-24.04] }
|
|
58
|
+
- id: main
|
|
59
|
+
build: linux/amd64
|
|
60
|
+
goals: [default, app] # what `crosspack build` runs; also buildable as `app`
|
|
61
|
+
steps: [wails build -platform linux/amd64]
|
|
62
|
+
artifacts: { from: build/bin, to: [ubuntu-24.04, debian-12] }
|
|
63
|
+
- id: tools
|
|
64
|
+
build: linux/amd64
|
|
65
|
+
goals: [tools]
|
|
66
|
+
steps: [cargo build --release -p helper]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
crosspack build # the default goal only (same as: build default)
|
|
71
|
+
crosspack build helloworld # one binary — nothing else rebuilds
|
|
72
|
+
crosspack build tools # one named goal
|
|
73
|
+
crosspack build all # every host-buildable entry (--all works too)
|
|
74
|
+
crosspack run helloworld # build just that binary, then launch it
|
|
75
|
+
crosspack run # build the default goal, launch the first executable
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`crosspack run <binary>` launches the executable named by the goal (it must
|
|
79
|
+
be listed in `package.executables`); group goals and the default goal launch
|
|
80
|
+
the first one. Goal names use letters/digits/`+._-`; `all` is reserved. A
|
|
81
|
+
positional that is not a declared goal or entry id still works as a package
|
|
82
|
+
target (`crosspack build debian-12` — the single entry distributing to it,
|
|
83
|
+
with fanout limited to that target), so old invocations keep working.
|
|
84
|
+
`crosspack matrix` lists each entry's goals and what the default goal would
|
|
85
|
+
run on this host.
|
|
86
|
+
|
|
40
87
|
## crosspack.yml — one file, three sections + two host hooks
|
|
41
88
|
|
|
42
89
|
The top level carries the shared facts — `name` (required once) and the
|
|
@@ -71,6 +118,7 @@ build:
|
|
|
71
118
|
|
|
72
119
|
matrix:
|
|
73
120
|
- build: linux/amd64 # os/arch this entry builds on (arch may be "any")
|
|
121
|
+
goals: [default] # build goals this entry belongs to (default: [default])
|
|
74
122
|
env: # extra env for the steps of this entry
|
|
75
123
|
CGO_ENABLED: "1"
|
|
76
124
|
steps: # shell commands, run from the project root
|
|
@@ -205,11 +253,12 @@ config.
|
|
|
205
253
|
```
|
|
206
254
|
# staged pipeline
|
|
207
255
|
crosspack deps <target> [--check] # verify/install build deps (--check: report only)
|
|
208
|
-
crosspack build <
|
|
256
|
+
crosspack build [<goal>|all] [--no-deps] [--version X]
|
|
257
|
+
# goal: default (implicit), a declared goal, or all
|
|
209
258
|
crosspack pack <target> [--version X] # version defaults to the build stamp
|
|
210
259
|
|
|
211
|
-
# host-only: this machine, no target argument
|
|
212
|
-
crosspack run [-- --dev]
|
|
260
|
+
# host-only: this machine, no package-target argument
|
|
261
|
+
crosspack run [<goal>] [-- --dev] # build the goal (default: default), then launch the main binary
|
|
213
262
|
crosspack install # launch this host's package with its installer
|
|
214
263
|
|
|
215
264
|
# inspection
|
|
@@ -235,10 +284,11 @@ crosspacks/arch/x86_64/PKGBUILD
|
|
|
235
284
|
### Host-only commands: run and install
|
|
236
285
|
|
|
237
286
|
`run` and `install` apply to the current host only — the build target *is*
|
|
238
|
-
the run target, so they take no target argument. `crosspack run
|
|
239
|
-
build stage for
|
|
240
|
-
launches the first `package.executables`
|
|
241
|
-
the binary, not the package
|
|
287
|
+
the run target, so they take no package-target argument. `crosspack run
|
|
288
|
+
[goal]` runs the build stage for the given goal (default: `default` — same
|
|
289
|
+
as `crosspack build <goal>`), then launches the first `package.executables`
|
|
290
|
+
binary straight from `builds/` — the binary, not the package; only that
|
|
291
|
+
goal's entries are rebuilt. Everything after `--` goes to the app:
|
|
242
292
|
|
|
243
293
|
```
|
|
244
294
|
crosspack run -- --dev
|
|
@@ -264,7 +314,8 @@ executed directly, except a macOS `.app` bundle which is `open`ed.
|
|
|
264
314
|
require 'crosspack' # pulls in Crossbuild too
|
|
265
315
|
|
|
266
316
|
Crossbuild.build('crosspack.yml', root: Dir.pwd) # all host entries
|
|
267
|
-
Crossbuild.build('crosspack.yml',
|
|
317
|
+
Crossbuild.build('crosspack.yml', goal: 'default') # one build goal
|
|
318
|
+
Crossbuild.build('crosspack.yml', target: 'ubuntu-24.04') # legacy target selection
|
|
268
319
|
|
|
269
320
|
Crosspack.pack(
|
|
270
321
|
config: 'crosspack.yml',
|
data/exe/crosspack
CHANGED
|
@@ -46,9 +46,9 @@ parser = OptionParser.new do |opts|
|
|
|
46
46
|
opts.banner = "crosspack #{Crosspack::VERSION} — staged pipeline: deps → build → pack\n" \
|
|
47
47
|
"Usage: crosspack <command> [options]\n" \
|
|
48
48
|
"Stages: deps [<target>] [--check] verify/install build deps (default: this host)\n" \
|
|
49
|
-
" build [<
|
|
49
|
+
" build [<goal>|all] [--no-deps] compile into builds/ (default goal: default)\n" \
|
|
50
50
|
" pack [<target>] [--version X] package builds/<target> (default: this host; version from the build stamp)\n" \
|
|
51
|
-
"Host: run [-- <app args>]
|
|
51
|
+
"Host: run [<goal>] [-- <app args>] build the goal (default: default), then launch the main binary\n" \
|
|
52
52
|
" install launch this host's package with its installer\n" \
|
|
53
53
|
'Commands: validate, resolve, matrix, version, targets'
|
|
54
54
|
|
|
@@ -60,7 +60,7 @@ parser = OptionParser.new do |opts|
|
|
|
60
60
|
opts.on('--output-base DIR', 'output directory (build stage: the manifest output, default builds; pack: default ./crosspacks)') { |v| options[:output_base] = v }
|
|
61
61
|
opts.on('--check', 'with `deps`: only report presence, never install (exit 1 if missing)') { options[:check_only] = true }
|
|
62
62
|
opts.on('--no-deps', 'with `build`: skip the deps stage') { options[:no_deps] = true }
|
|
63
|
-
opts.on('--all', 'with `build`: every host-buildable matrix entry (
|
|
63
|
+
opts.on('--all', 'with `build`: every host-buildable matrix entry (same as: build all)') { options[:all] = true }
|
|
64
64
|
opts.on('-h', '--help', 'show help') do
|
|
65
65
|
puts opts
|
|
66
66
|
exit
|
|
@@ -75,8 +75,8 @@ end
|
|
|
75
75
|
command = ARGV.shift
|
|
76
76
|
abort_with 'No command given. See: crosspack --help' if command.nil?
|
|
77
77
|
|
|
78
|
-
# Positional argument: a
|
|
79
|
-
# resolve
|
|
78
|
+
# Positional argument: a build goal for build/run, a package target for
|
|
79
|
+
# deps/pack/resolve, a crosspack.yml path for validate.
|
|
80
80
|
positional = ARGV.shift if ARGV.first && !ARGV.first.start_with?('-')
|
|
81
81
|
|
|
82
82
|
options[:file] = positional if command == 'validate' && positional
|
|
@@ -108,7 +108,7 @@ rescue Crossbuild::Error => e
|
|
|
108
108
|
abort_with e.message
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
# Stage commands (deps/
|
|
111
|
+
# Stage commands (deps/pack) run for the current host when no target
|
|
112
112
|
# is given: macos, windows-11.0 or the distro from /etc/os-release.
|
|
113
113
|
def host_target!(command)
|
|
114
114
|
raw = Crosspack::Target.host_string
|
|
@@ -192,17 +192,31 @@ when 'deps'
|
|
|
192
192
|
when 'build'
|
|
193
193
|
build_manifest = config.build_manifest
|
|
194
194
|
abort_with 'the build: section is invalid — run: crosspack validate' unless build_manifest.valid?
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
else
|
|
198
|
-
|
|
195
|
+
# Positional: a build goal — empty/`default` (the implicit default goal),
|
|
196
|
+
# `all` (every host-buildable entry), a declared goal name or an entry id
|
|
197
|
+
# (make-style binary goal: crosspack build helloworld). Anything else
|
|
198
|
+
# falls back to the legacy package target (crosspack build debian-12).
|
|
199
|
+
arg = positional.to_s
|
|
200
|
+
goal = 'default'
|
|
201
|
+
target = nil
|
|
202
|
+
if options[:all] || arg == 'all'
|
|
203
|
+
goal = 'all'
|
|
204
|
+
elsif !arg.empty? && arg != 'default'
|
|
205
|
+
known_goal = build_manifest.declared_goals.include?(arg) || build_manifest.find_entry(arg)
|
|
206
|
+
goal = known_goal ? arg : nil
|
|
207
|
+
target = arg if goal.nil?
|
|
199
208
|
end
|
|
200
209
|
begin
|
|
201
210
|
result = Crossbuild::Builder.new(build_manifest, root: options[:root],
|
|
202
211
|
output_base: options[:output_base], version: options[:version],
|
|
203
|
-
deps: !options[:no_deps]).run(target: target)
|
|
212
|
+
deps: !options[:no_deps]).run(goal: goal, target: target)
|
|
204
213
|
rescue Crossbuild::Error, Crosspack::InvalidManifestError => e
|
|
205
|
-
|
|
214
|
+
message = e.message
|
|
215
|
+
if target
|
|
216
|
+
selectors = (build_manifest.declared_goals | build_manifest.entries.map(&:id)).sort
|
|
217
|
+
message += "\nNot a package target either — buildable selectors: #{selectors.join(', ')} (plus 'all')"
|
|
218
|
+
end
|
|
219
|
+
abort_with message
|
|
206
220
|
end
|
|
207
221
|
puts Crosspack::Colors.green("\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}")
|
|
208
222
|
puts 'Pack with: crosspack pack <target>'
|
|
@@ -224,19 +238,23 @@ when 'pack'
|
|
|
224
238
|
end
|
|
225
239
|
puts Crosspack::Colors.green("📦 #{path}")
|
|
226
240
|
when 'run', 'install'
|
|
227
|
-
# Host-only commands:
|
|
228
|
-
#
|
|
229
|
-
|
|
230
|
-
abort_with "crosspack #{command} works only for the current host — no target argument " \
|
|
231
|
-
"(other targets make no sense here)"
|
|
232
|
-
end
|
|
241
|
+
# Host-only commands: everything after `--` goes to the launched process
|
|
242
|
+
# (crosspack run -- --dev). `run` optionally takes a build goal; `install`
|
|
243
|
+
# never builds, so it takes no arguments.
|
|
233
244
|
args = ARGV.dup
|
|
234
245
|
args.shift if args.first == '--'
|
|
246
|
+
goal = 'default'
|
|
247
|
+
if command == 'run'
|
|
248
|
+
goal = positional if positional && !positional.empty?
|
|
249
|
+
elsif positional
|
|
250
|
+
abort_with "crosspack install launches this host's package — no arguments " \
|
|
251
|
+
'(crosspack run [goal] is the one that builds)'
|
|
252
|
+
end
|
|
235
253
|
begin
|
|
236
254
|
status =
|
|
237
255
|
if command == 'run'
|
|
238
256
|
Crosspack::Launcher.run(
|
|
239
|
-
config, root: options[:root], args: args,
|
|
257
|
+
config, root: options[:root], args: args, goal: goal,
|
|
240
258
|
output_base: options[:output_base], version: options[:version], deps: !options[:no_deps]
|
|
241
259
|
)
|
|
242
260
|
else
|
|
@@ -24,18 +24,22 @@ module Crossbuild
|
|
|
24
24
|
ARTIFACT_MODES = %w[symlink copy].freeze
|
|
25
25
|
VERSION_SCHEMES = %w[calver git-tag].freeze
|
|
26
26
|
TOP_LEVEL_KEYS = %w[name version output deps matrix].freeze
|
|
27
|
-
ENTRY_KEYS = %w[id build env steps artifacts].freeze
|
|
27
|
+
ENTRY_KEYS = %w[id goals build env steps artifacts].freeze
|
|
28
|
+
DEFAULT_GOAL = 'default'
|
|
29
|
+
ALL_GOAL = 'all'
|
|
28
30
|
ARTIFACTS_KEYS = %w[from include to mode].freeze
|
|
29
31
|
DEP_KEYS = %w[hosts].freeze
|
|
30
32
|
BUILD_RE = /\A(linux|darwin|windows)\/(#{ARCH_LIST.join('|')})\z/.freeze
|
|
31
33
|
NAME_RE = /\A[a-z0-9][a-z0-9+._-]*\z/i.freeze
|
|
32
34
|
|
|
33
|
-
# One matrix entry: where it builds, what it runs, where artifacts go
|
|
35
|
+
# One matrix entry: where it builds, what it runs, where artifacts go,
|
|
36
|
+
# which build goals it belongs to.
|
|
34
37
|
class Entry
|
|
35
|
-
attr_reader :id, :os, :arch, :env, :steps, :artifacts, :index
|
|
38
|
+
attr_reader :id, :goals, :os, :arch, :env, :steps, :artifacts, :index
|
|
36
39
|
|
|
37
|
-
def initialize(id:, os:, arch:, env:, steps:, artifacts:, index:)
|
|
40
|
+
def initialize(id:, goals:, os:, arch:, env:, steps:, artifacts:, index:)
|
|
38
41
|
@id = id
|
|
42
|
+
@goals = goals
|
|
39
43
|
@os = os.to_sym
|
|
40
44
|
@arch = arch
|
|
41
45
|
@env = env
|
|
@@ -116,7 +120,8 @@ module Crossbuild
|
|
|
116
120
|
lines = []
|
|
117
121
|
if valid?
|
|
118
122
|
noun = @entries.size == 1 ? 'entry' : 'entries'
|
|
119
|
-
lines << "#{path}: the build: section is valid (#{@entries.size} #{noun}: #{@entries.map(&:id).join(', ')}
|
|
123
|
+
lines << "#{path}: the build: section is valid (#{@entries.size} #{noun}: #{@entries.map(&:id).join(', ')}; " \
|
|
124
|
+
"goals: #{declared_goals.join(', ')})."
|
|
120
125
|
else
|
|
121
126
|
lines << "#{path}: the build: section is invalid (#{@errors.size} errors):"
|
|
122
127
|
@errors.each { |e| lines << " ✗ #{e}" }
|
|
@@ -134,6 +139,16 @@ module Crossbuild
|
|
|
134
139
|
@entries.find { |e| e.id == id }
|
|
135
140
|
end
|
|
136
141
|
|
|
142
|
+
# Every goal name used by the entries, sorted; an entry without `goals:`
|
|
143
|
+
# belongs to the implicit `default` goal.
|
|
144
|
+
def declared_goals
|
|
145
|
+
@entries.flat_map(&:goals).uniq.sort
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def entries_for_goal(goal)
|
|
149
|
+
@entries.select { |e| e.goals.include?(goal) }
|
|
150
|
+
end
|
|
151
|
+
|
|
137
152
|
# The single matrix entry that distributes artifacts to the given target
|
|
138
153
|
# string (e.g. "ubuntu-24.04"). Raises Crossbuild::Error when no entry
|
|
139
154
|
# claims it or when several do (ambiguous).
|
|
@@ -314,6 +329,7 @@ module Crossbuild
|
|
|
314
329
|
steps = validate_steps(entry['steps'], path)
|
|
315
330
|
env = validate_env(entry['env'], path)
|
|
316
331
|
artifacts = validate_artifacts(entry['artifacts'], path, arch)
|
|
332
|
+
goals = validate_goals(entry['goals'], path)
|
|
317
333
|
id = entry['id'].nil? ? "#{os}/#{Platform.display_arch(arch == :any ? :x86_64 : arch)}" : entry['id'].to_s
|
|
318
334
|
if entry['id'] && id.empty?
|
|
319
335
|
@errors << Issue.new("#{path}.id", 'must be a non-empty string')
|
|
@@ -324,10 +340,31 @@ module Crossbuild
|
|
|
324
340
|
return
|
|
325
341
|
end
|
|
326
342
|
@warnings << Issue.new(path, 'entry has no steps — existing artifacts only will be distributed') if entry['steps'].nil? && artifacts
|
|
327
|
-
@entries << Entry.new(id: id, os: os, arch: arch, env: env, steps: steps,
|
|
343
|
+
@entries << Entry.new(id: id, goals: goals, os: os, arch: arch, env: env, steps: steps,
|
|
328
344
|
artifacts: artifacts, index: index)
|
|
329
345
|
end
|
|
330
346
|
|
|
347
|
+
# goals: names the make-style build goals this entry belongs to
|
|
348
|
+
# (crosspack build <goal>); missing -> the implicit `default` goal.
|
|
349
|
+
def validate_goals(goals, path)
|
|
350
|
+
return [DEFAULT_GOAL] if goals.nil?
|
|
351
|
+
|
|
352
|
+
unless goals.is_a?(Array) && !goals.empty? && goals.all? { |g| g.is_a?(String) && !g.strip.empty? }
|
|
353
|
+
@errors << Issue.new("#{path}.goals", "must be a non-empty list of goal names, e.g. [#{DEFAULT_GOAL}, tools]")
|
|
354
|
+
return []
|
|
355
|
+
end
|
|
356
|
+
goals.each do |goal|
|
|
357
|
+
if goal == ALL_GOAL
|
|
358
|
+
@errors << Issue.new("#{path}.goals", "#{goal.inspect} is reserved — `crosspack build all` builds every goal")
|
|
359
|
+
elsif goal !~ NAME_RE
|
|
360
|
+
@errors << Issue.new("#{path}.goals", "invalid goal name #{goal.inspect} (letters, digits, \"+\", \"_\", \"-\", \".\")")
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
@errors << Issue.new("#{path}.goals", "duplicate goal names: #{goals.tally.select { |_, n| n > 1 }.keys.join(', ')}") if goals.uniq.size != goals.size
|
|
364
|
+
|
|
365
|
+
goals.uniq
|
|
366
|
+
end
|
|
367
|
+
|
|
331
368
|
def parse_build(build, path)
|
|
332
369
|
if build.nil?
|
|
333
370
|
@errors << Issue.new("#{path}.build", 'is required: the platform this entry builds on, e.g. linux/amd64')
|
data/lib/crossbuild/builder.rb
CHANGED
|
@@ -19,17 +19,19 @@ module Crossbuild
|
|
|
19
19
|
@host_arch = host_arch
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# entry_id: nil —
|
|
23
|
-
#
|
|
24
|
-
# asked for it explicitly).
|
|
22
|
+
# entry_id: nil — otherwise the exact entry id (which must exist, but may
|
|
23
|
+
# target another host — the user asked for it explicitly).
|
|
25
24
|
# target: a raw target string (e.g. "ubuntu-24.04") — builds the single
|
|
26
25
|
# matrix entry distributing to it and fans artifacts out to that target
|
|
27
26
|
# only; the entry must be buildable on this host (no cross-compilation).
|
|
28
|
-
|
|
27
|
+
# goal: nil/'all' — every entry buildable on this host; otherwise the
|
|
28
|
+
# entries declaring that build goal (entries without `goals:` belong to
|
|
29
|
+
# the implicit `default` goal).
|
|
30
|
+
def run(entry_id: nil, target: nil, goal: nil)
|
|
29
31
|
manifest.validate!
|
|
30
32
|
ensure_deps
|
|
31
33
|
version = @version_override || VersionScheme.new(manifest.version).compute(root: @root)
|
|
32
|
-
entries = target ? entries_for_target(target) : select_entries(entry_id)
|
|
34
|
+
entries = target ? entries_for_target(target) : select_entries(entry_id, goal)
|
|
33
35
|
|
|
34
36
|
entries.each do |entry|
|
|
35
37
|
puts "\n==> #{manifest.name} [#{entry.id}] version #{version}"
|
|
@@ -60,18 +62,52 @@ module Crossbuild
|
|
|
60
62
|
[entry]
|
|
61
63
|
end
|
|
62
64
|
|
|
63
|
-
def select_entries(entry_id)
|
|
65
|
+
def select_entries(entry_id, goal)
|
|
64
66
|
if entry_id
|
|
65
67
|
entry = manifest.find_entry(entry_id)
|
|
66
68
|
raise Error, "unknown matrix entry #{entry_id.inspect}; available: #{manifest.entries.map(&:id).join(', ')}" unless entry
|
|
67
69
|
|
|
68
70
|
[entry]
|
|
71
|
+
elsif goal.nil? || goal == BuildManifest::ALL_GOAL
|
|
72
|
+
buildable_on_host
|
|
69
73
|
else
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
entries_for_goal(goal)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def buildable_on_host
|
|
79
|
+
buildable = manifest.buildable_entries(@host_os, @host_arch)
|
|
80
|
+
raise Error, "nothing to build on #{@host_os}/#{Platform.display_arch(@host_arch)} " \
|
|
81
|
+
"(matrix entries: #{manifest.entries.map(&:id).join(', ')})" if buildable.empty?
|
|
82
|
+
|
|
83
|
+
buildable
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A goal name may also be an entry id — `crosspack build helloworld`
|
|
87
|
+
# builds just the entry named helloworld (a make-style binary goal).
|
|
88
|
+
def entries_for_goal(goal)
|
|
89
|
+
if manifest.declared_goals.include?(goal)
|
|
90
|
+
buildable = manifest.entries_for_goal(goal).select { |e| e.buildable_on?(@host_os, @host_arch) }
|
|
91
|
+
if buildable.empty?
|
|
92
|
+
others = manifest.entries_for_goal(goal).map { |e| "#{e.id} (#{e.platform})" }
|
|
93
|
+
raise Error, "nothing to build on #{@host_os}/#{Platform.display_arch(@host_arch)} for goal #{goal.inspect} " \
|
|
94
|
+
"(goal entries: #{others.join(', ')})"
|
|
95
|
+
end
|
|
73
96
|
|
|
74
97
|
buildable
|
|
98
|
+
elsif (entry = manifest.find_entry(goal))
|
|
99
|
+
host = "#{@host_os}/#{Platform.display_arch(@host_arch)}"
|
|
100
|
+
unless entry.buildable_on?(@host_os, @host_arch)
|
|
101
|
+
raise Error,
|
|
102
|
+
"the entry for goal #{goal.inspect} (#{entry.id}) builds on #{entry.platform}, " \
|
|
103
|
+
"this host is #{host} — cross-compilation is not supported"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
[entry]
|
|
107
|
+
else
|
|
108
|
+
available = (manifest.declared_goals | manifest.entries.map(&:id)).sort
|
|
109
|
+
raise Error, "unknown goal #{goal.inspect}; available: #{available.join(', ')} " \
|
|
110
|
+
"(or '#{BuildManifest::ALL_GOAL}' for every entry)"
|
|
75
111
|
end
|
|
76
112
|
end
|
|
77
113
|
|
data/lib/crossbuild/matrix.rb
CHANGED
|
@@ -22,6 +22,10 @@ module Crossbuild
|
|
|
22
22
|
else
|
|
23
23
|
"\n#{buildable.size} of #{@manifest.entries.size} entries build here: #{buildable.map(&:id).join(', ')}"
|
|
24
24
|
end
|
|
25
|
+
default_here = buildable.select { |e| e.goals.include?('default') }
|
|
26
|
+
unless default_here.empty?
|
|
27
|
+
lines << "`crosspack build` (default goal) would run: #{default_here.map(&:id).join(', ')}"
|
|
28
|
+
end
|
|
25
29
|
lines << render_deps
|
|
26
30
|
lines.join("\n")
|
|
27
31
|
end
|
|
@@ -42,6 +46,7 @@ module Crossbuild
|
|
|
42
46
|
else
|
|
43
47
|
"#{entry.steps.size} step#{'s' unless entry.steps.size == 1}"
|
|
44
48
|
end
|
|
49
|
+
parts << "goals: #{entry.goals.join(', ')}" unless entry.goals == ['default']
|
|
45
50
|
parts << artifact_note(entry) if entry.artifacts
|
|
46
51
|
parts.join(' ')
|
|
47
52
|
end
|
data/lib/crossbuild.rb
CHANGED
|
@@ -18,11 +18,12 @@ require_relative 'crossbuild/builder'
|
|
|
18
18
|
|
|
19
19
|
module Crossbuild
|
|
20
20
|
# Convenience wrapper: Crossbuild.build('crosspack.yml') runs every
|
|
21
|
-
# host-buildable matrix entry and returns Builder::Result.
|
|
22
|
-
# the
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
|
|
21
|
+
# host-buildable matrix entry and returns Builder::Result. goal: builds
|
|
22
|
+
# only the entries declaring that build goal ('all' or nil — every entry);
|
|
23
|
+
# target: builds the entry distributing to that target only; deps: false
|
|
24
|
+
# skips the pre-build dependency check/install pass. The manifest argument
|
|
25
|
+
# may be a Config, a path to crosspack.yml or a ready BuildManifest.
|
|
26
|
+
def self.build(manifest, entry_id: nil, target: nil, goal: nil, root: Dir.pwd, output_base: nil, version: nil,
|
|
26
27
|
deps: true, host_os: Platform.os, host_arch: Platform.arch)
|
|
27
28
|
m = case manifest
|
|
28
29
|
when BuildManifest then manifest
|
|
@@ -30,6 +31,6 @@ module Crossbuild
|
|
|
30
31
|
else Crosspack::Config.coerce(manifest, root: root).build_manifest
|
|
31
32
|
end
|
|
32
33
|
Builder.new(m, root: root, output_base: output_base, version: version, deps: deps,
|
|
33
|
-
host_os: host_os, host_arch: host_arch).run(entry_id: entry_id, target: target)
|
|
34
|
+
host_os: host_os, host_arch: host_arch).run(entry_id: entry_id, target: target, goal: goal)
|
|
34
35
|
end
|
|
35
36
|
end
|
|
@@ -14,7 +14,8 @@ module Crosspack
|
|
|
14
14
|
# CFBundleExecutable, falling back to the package name)
|
|
15
15
|
# icon: path to the bundle icon, copied into Contents/Resources
|
|
16
16
|
def self.build(name:, version:, summary:, files:, executables: [],
|
|
17
|
-
display_name: nil, icon: nil, identifier: nil,
|
|
17
|
+
display_name: nil, icon: nil, identifier: nil,
|
|
18
|
+
min_macos: '11.0', output:)
|
|
18
19
|
app_dir = File.join(output, "#{name}.app")
|
|
19
20
|
FileUtils.rm_rf(app_dir)
|
|
20
21
|
macos_dir = File.join(app_dir, 'Contents', 'MacOS')
|
|
@@ -48,7 +49,8 @@ module Crosspack
|
|
|
48
49
|
name: name, version: version, summary: summary,
|
|
49
50
|
executable: executables.first || name,
|
|
50
51
|
display_name: display_name || name,
|
|
51
|
-
icon_file: icon_file, identifier: identifier
|
|
52
|
+
icon_file: icon_file, identifier: identifier,
|
|
53
|
+
min_macos: min_macos)
|
|
52
54
|
|
|
53
55
|
File.write(File.join(output, 'BUILD-DMG.txt'), <<~TXT)
|
|
54
56
|
DMG not built: hdiutil only exists on macOS.
|
|
@@ -59,7 +61,8 @@ module Crosspack
|
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
def self.write_info_plist(path, name:, version:, summary:, executable:,
|
|
62
|
-
display_name:, icon_file: nil, identifier: nil
|
|
64
|
+
display_name:, icon_file: nil, identifier: nil,
|
|
65
|
+
min_macos: '11.0')
|
|
63
66
|
bundle_id = identifier || "org.crosspack.#{name}"
|
|
64
67
|
# Without the extension .icns is assumed; other formats (png) must
|
|
65
68
|
# be named in full.
|
|
@@ -86,7 +89,7 @@ module Crosspack
|
|
|
86
89
|
<key>CFBundleInfoDictionaryVersion</key>
|
|
87
90
|
<string>6.0</string>
|
|
88
91
|
<key>LSMinimumSystemVersion</key>
|
|
89
|
-
<string
|
|
92
|
+
<string>#{escape(min_macos)}</string>
|
|
90
93
|
<key>NSPrincipalClass</key>
|
|
91
94
|
<string>NSApplication</string>
|
|
92
95
|
<key>NSSupportsAutomaticTermination</key>
|
|
@@ -45,6 +45,7 @@ module Crosspack
|
|
|
45
45
|
stage_files(pkg_root)
|
|
46
46
|
stage_symlinks(pkg_root)
|
|
47
47
|
write_control(File.join(pkg_root, 'DEBIAN'))
|
|
48
|
+
write_maintainer_scripts(File.join(pkg_root, 'DEBIAN'))
|
|
48
49
|
|
|
49
50
|
FileUtils.mkdir_p(File.dirname(@output))
|
|
50
51
|
FileUtils.rm_f(@output)
|
|
@@ -112,6 +113,38 @@ module Crosspack
|
|
|
112
113
|
File.write(File.join(debian_dir, 'control'),
|
|
113
114
|
"#{(fields + ["Description: #{summary}"]).join("\n")}\n#{body.join("\n")}\n")
|
|
114
115
|
end
|
|
116
|
+
|
|
117
|
+
# Desktop integration caches: refresh the application database and the
|
|
118
|
+
# hicolor icon cache after (un)install when the package carries a
|
|
119
|
+
# .desktop file or a themed icon. The tools are guarded — they are not
|
|
120
|
+
# dpkg dependencies, minimal systems may lack them.
|
|
121
|
+
def write_maintainer_scripts(debian_dir)
|
|
122
|
+
commands = desktop_cache_commands
|
|
123
|
+
return if commands.empty?
|
|
124
|
+
|
|
125
|
+
script = "#!/bin/sh\nset -e\n#{commands.join("\n")}\n"
|
|
126
|
+
%w[postinst postrm].each do |script_name|
|
|
127
|
+
path = File.join(debian_dir, script_name)
|
|
128
|
+
File.write(path, script)
|
|
129
|
+
FileUtils.chmod(0o755, path)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def desktop_cache_commands
|
|
134
|
+
commands = @files.values.select { |d| d.end_with?('.desktop') }
|
|
135
|
+
.map { |d| File.dirname(d) }.uniq
|
|
136
|
+
.map do |dir|
|
|
137
|
+
"if command -v update-desktop-database >/dev/null 2>&1; then " \
|
|
138
|
+
"update-desktop-database -q /#{dir} || true; fi"
|
|
139
|
+
end
|
|
140
|
+
commands += @files.values.select { |d| d.include?('/share/icons/') }
|
|
141
|
+
.map { |d| File.dirname(d) }.uniq
|
|
142
|
+
.map do |dir|
|
|
143
|
+
"if command -v gtk-update-icon-cache >/dev/null 2>&1; then " \
|
|
144
|
+
"gtk-update-icon-cache -q -t -f /#{dir} || true; fi"
|
|
145
|
+
end
|
|
146
|
+
commands
|
|
147
|
+
end
|
|
115
148
|
end
|
|
116
149
|
end
|
|
117
150
|
end
|
|
@@ -11,12 +11,13 @@ module Crosspack
|
|
|
11
11
|
class Pkgbuild
|
|
12
12
|
def self.generate(name:, version:, release:, pkgdesc:, depends:, arch:,
|
|
13
13
|
files:, symlinks: {}, executables: [], source:,
|
|
14
|
-
maintainer:, url: '', license: 'Proprietary',
|
|
14
|
+
sha256: nil, maintainer:, url: '', license: 'Proprietary',
|
|
15
|
+
output:)
|
|
15
16
|
content = render(
|
|
16
17
|
name: name, version: version, release: release, pkgdesc: pkgdesc,
|
|
17
18
|
depends: Array(depends), arch: Array(arch), files: files,
|
|
18
19
|
symlinks: symlinks, executables: executables, source: Array(source),
|
|
19
|
-
maintainer: maintainer, url: url, license: license
|
|
20
|
+
sha256: sha256, maintainer: maintainer, url: url, license: license
|
|
20
21
|
)
|
|
21
22
|
FileUtils.mkdir_p(File.dirname(output))
|
|
22
23
|
File.write(output, content)
|
|
@@ -24,8 +25,8 @@ module Crosspack
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def self.render(name:, version:, release:, pkgdesc:, depends:, arch:,
|
|
27
|
-
files:, symlinks:, executables:, source:,
|
|
28
|
-
url:, license:)
|
|
28
|
+
files:, symlinks:, executables:, source:, sha256:,
|
|
29
|
+
maintainer:, url:, license:)
|
|
29
30
|
depends_line = depends.map { |d| "'#{d}'" }.join(' ')
|
|
30
31
|
source_line = source.map { |s| "'#{s}'" }.join(' ')
|
|
31
32
|
srcbase = "$srcdir/$pkgname-$pkgver"
|
|
@@ -52,7 +53,7 @@ module Crosspack
|
|
|
52
53
|
license=('#{license}')
|
|
53
54
|
depends=(#{depends_line})
|
|
54
55
|
source=(#{source_line})
|
|
55
|
-
sha256sums=('SKIP')
|
|
56
|
+
sha256sums=('#{sha256 || 'SKIP'}')
|
|
56
57
|
|
|
57
58
|
package() {
|
|
58
59
|
#{(install_lines + link_lines).join("\n")}
|
|
@@ -29,6 +29,7 @@ module Crosspack
|
|
|
29
29
|
description ||= summary
|
|
30
30
|
requires_line = Array(requires).join(', ')
|
|
31
31
|
entries = spec_entries(files: files, symlinks: symlinks)
|
|
32
|
+
scriptlets = desktop_cache_scriptlets(files)
|
|
32
33
|
|
|
33
34
|
<<~SPEC
|
|
34
35
|
Name: #{name}
|
|
@@ -48,12 +49,35 @@ module Crosspack
|
|
|
48
49
|
%install
|
|
49
50
|
# Files are staged into the buildroot by crosspack.
|
|
50
51
|
|
|
52
|
+
#{scriptlets}
|
|
51
53
|
%files
|
|
52
54
|
%defattr(-,root,root,-)
|
|
53
55
|
#{entries.join("\n")}
|
|
54
56
|
SPEC
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
# %post/%postun: refresh the application database and the hicolor icon
|
|
60
|
+
# cache when the package carries a .desktop file or a themed icon; the
|
|
61
|
+
# tools are guarded (minimal systems may lack them).
|
|
62
|
+
def self.desktop_cache_scriptlets(files)
|
|
63
|
+
commands = files.values.select { |d| d.end_with?('.desktop') }
|
|
64
|
+
.map { |d| File.dirname(d) }.uniq
|
|
65
|
+
.map do |dir|
|
|
66
|
+
"command -v update-desktop-database >/dev/null 2>&1 && " \
|
|
67
|
+
"update-desktop-database -q /#{dir} || :"
|
|
68
|
+
end
|
|
69
|
+
commands += files.values.select { |d| d.include?('/share/icons/') }
|
|
70
|
+
.map { |d| File.dirname(d) }.uniq
|
|
71
|
+
.map do |dir|
|
|
72
|
+
"command -v gtk-update-icon-cache >/dev/null 2>&1 && " \
|
|
73
|
+
"gtk-update-icon-cache -q -t -f /#{dir} || :"
|
|
74
|
+
end
|
|
75
|
+
return '' if commands.empty?
|
|
76
|
+
|
|
77
|
+
body = commands.join("\n")
|
|
78
|
+
"%post\n#{body}\n\n%postun\n#{body}\n\n"
|
|
79
|
+
end
|
|
80
|
+
|
|
57
81
|
# %files entries: %dir for every directory so we never claim ownership
|
|
58
82
|
# of shared paths like /usr or /usr/local.
|
|
59
83
|
def self.spec_entries(files:, symlinks:)
|
|
@@ -64,9 +64,10 @@ module Crosspack
|
|
|
64
64
|
|
|
65
65
|
# shortcuts: nil, or { name: "Display Name", target: "app.exe" } (the
|
|
66
66
|
# payload file inside INSTALLFOLDER the shortcuts launch) — installs
|
|
67
|
-
# Start Menu and Desktop shortcuts.
|
|
67
|
+
# Start Menu and Desktop shortcuts. icon: local path to an .ico for
|
|
68
|
+
# the Add/Remove Programs entry (ARPPRODUCTICON), or nil.
|
|
68
69
|
def self.generate_wxs(name:, version:, manufacturer:, summary:, files:,
|
|
69
|
-
arch: 'x64', shortcuts: nil)
|
|
70
|
+
arch: 'x64', shortcuts: nil, icon: nil)
|
|
70
71
|
license_rtf_file = "#{name}-license.rtf"
|
|
71
72
|
name = escape(name)
|
|
72
73
|
manufacturer = escape(sanitize_manufacturer(manufacturer))
|
|
@@ -86,6 +87,7 @@ module Crosspack
|
|
|
86
87
|
shortcut_xml = shortcuts ? shortcuts_fragment(shortcuts) : nil
|
|
87
88
|
feature_refs = ["<ComponentGroupRef Id=\"Payload\" />"]
|
|
88
89
|
feature_refs << '<ComponentGroupRef Id="Shortcuts" />' if shortcuts
|
|
90
|
+
arp_icon = icon ? arp_icon_fragment(icon) : nil
|
|
89
91
|
|
|
90
92
|
<<~WXS
|
|
91
93
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -103,7 +105,7 @@ module Crosspack
|
|
|
103
105
|
|
|
104
106
|
<Property Id="WixUILicenseRtf" Value="#{license_rtf_file}" />
|
|
105
107
|
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
|
|
106
|
-
|
|
108
|
+
#{arp_icon}
|
|
107
109
|
<Feature Id="Main" Title="#{name}" Level="1">
|
|
108
110
|
#{feature_refs.map { |r| " #{r}" }.join("\n")}
|
|
109
111
|
</Feature>
|
|
@@ -147,7 +149,7 @@ module Crosspack
|
|
|
147
149
|
# the output and, when WiX is installed, builds a real MSI. Returns
|
|
148
150
|
# the path of whatever was produced.
|
|
149
151
|
def self.build(name:, version:, manufacturer:, summary:, files:, output:,
|
|
150
|
-
arch: 'x64', shortcuts: nil, license: nil)
|
|
152
|
+
arch: 'x64', shortcuts: nil, license: nil, icon: nil)
|
|
151
153
|
arch = { 'x86_64' => 'x64', 'aarch64' => 'arm64' }.fetch(arch, arch)
|
|
152
154
|
missing = files.reject { |src, _dst| File.file?(src) }
|
|
153
155
|
unless missing.empty?
|
|
@@ -163,9 +165,11 @@ module Crosspack
|
|
|
163
165
|
File.write(wxs_path, generate_wxs(name: name, version: version,
|
|
164
166
|
manufacturer: manufacturer,
|
|
165
167
|
summary: summary, files: files,
|
|
166
|
-
arch: arch, shortcuts: shortcuts
|
|
168
|
+
arch: arch, shortcuts: shortcuts,
|
|
169
|
+
icon: icon))
|
|
167
170
|
File.write(File.join(out_dir, "#{name}-license.rtf"),
|
|
168
171
|
license_rtf(license || summary))
|
|
172
|
+
FileUtils.cp(icon, File.join(out_dir, File.basename(icon))) if icon
|
|
169
173
|
|
|
170
174
|
wix = wix_command
|
|
171
175
|
unless wix
|
|
@@ -202,6 +206,14 @@ module Crosspack
|
|
|
202
206
|
nil
|
|
203
207
|
end
|
|
204
208
|
|
|
209
|
+
# The Add/Remove Programs entry shows this icon (ARP/Publisher uses
|
|
210
|
+
# the MSI Icons table, not the installed files).
|
|
211
|
+
def self.arp_icon_fragment(icon)
|
|
212
|
+
icon_file = File.basename(icon)
|
|
213
|
+
" <Property Id=\"ARPPRODUCTICON\" Value=\"AppIcon\" />\n" \
|
|
214
|
+
" <Icon Id=\"AppIcon\" SourceFile=\"#{escape(icon_file)}\" />"
|
|
215
|
+
end
|
|
216
|
+
|
|
205
217
|
# The WixUI dialogs live in the UI extension, so it must be installed
|
|
206
218
|
# before `wix build -ext WixToolset.UI.wixext` can resolve it. A no-op
|
|
207
219
|
# when it is already present.
|
data/lib/crosspack/launcher.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Crosspack
|
|
4
|
-
# Host-only launch commands: `crosspack run` (build the
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# Host-only launch commands: `crosspack run [goal]` (build the goal's
|
|
5
|
+
# host-buildable entries — default goal: `default` —, then launch the
|
|
6
|
+
# application's main binary) and `crosspack install` (launch the package
|
|
7
|
+
# packed for this host). Both resolve the current host target, so they
|
|
8
|
+
# take no package-target argument and are meaningless on any other system.
|
|
8
9
|
#
|
|
9
10
|
# Commands may be overridden per host in crosspack.yml (run:/install:
|
|
10
11
|
# sections, selectors like the deps: rules of build:); placeholders:
|
|
@@ -15,11 +16,14 @@ module Crosspack
|
|
|
15
16
|
class Launcher
|
|
16
17
|
class Error < BuildError; end
|
|
17
18
|
|
|
18
|
-
# Builds the host
|
|
19
|
-
#
|
|
19
|
+
# Builds the given goal's host-buildable entries first (the same stage
|
|
20
|
+
# `crosspack build <goal>` runs; default goal: `default`), then launches
|
|
21
|
+
# a package.executables binary from builds/ — the one named by the goal
|
|
22
|
+
# when it matches, otherwise the first.
|
|
20
23
|
# Returns the launched process's exit code.
|
|
21
24
|
def self.run(config, root: Dir.pwd, args: [], output_base: nil, version: nil,
|
|
22
|
-
deps: true,
|
|
25
|
+
deps: true, goal: Crossbuild::BuildManifest::DEFAULT_GOAL, host_string: nil,
|
|
26
|
+
host_os: Crossbuild::Platform.os,
|
|
23
27
|
host_arch: Crossbuild::Platform.arch, selectors: Crossbuild::Platform.selectors)
|
|
24
28
|
config = Config.coerce(config, root: root)
|
|
25
29
|
target = host_target('run', host_string, host_arch)
|
|
@@ -30,9 +34,12 @@ module Crosspack
|
|
|
30
34
|
Crossbuild::Builder.new(build_manifest, root: root, output_base: output_base,
|
|
31
35
|
version: version, deps: deps,
|
|
32
36
|
host_os: host_os, host_arch: host_arch)
|
|
33
|
-
.run(
|
|
37
|
+
.run(goal: goal)
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
# A binary goal (`crosspack run helloworld`) launches that executable;
|
|
40
|
+
# a group goal or the default goal launches the first one.
|
|
41
|
+
executables = pkg.executables_for(target)
|
|
42
|
+
exe = executables.include?(goal) ? goal : executables.first
|
|
36
43
|
if exe.nil?
|
|
37
44
|
raise Error,
|
|
38
45
|
'run: the package: section does not name the binary to launch — ' \
|
|
@@ -14,7 +14,7 @@ module Crosspack
|
|
|
14
14
|
# no .so libraries and vice versa.
|
|
15
15
|
class PackageManifest
|
|
16
16
|
REQUIRED = %w[maintainer description sources prefix payload].freeze
|
|
17
|
-
OPTIONAL = %w[name summary license section lib_dir executables links desktop icon].freeze
|
|
17
|
+
OPTIONAL = %w[name summary license section lib_dir executables links desktop icon min_macos].freeze
|
|
18
18
|
|
|
19
19
|
# Payload selectors may name an os in addition to a family: every distro
|
|
20
20
|
# family packs a linux target, so "linux:" covers deb/rpm/PKGBUILD
|
|
@@ -142,6 +142,12 @@ module Crosspack
|
|
|
142
142
|
@data['icon']
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
+
# Minimum macOS version for the .app bundle (LSMinimumSystemVersion);
|
|
146
|
+
# defaults to 11.0 in the builder.
|
|
147
|
+
def min_macos
|
|
148
|
+
@data['min_macos']
|
|
149
|
+
end
|
|
150
|
+
|
|
145
151
|
private
|
|
146
152
|
|
|
147
153
|
def validate
|
|
@@ -172,6 +178,7 @@ module Crosspack
|
|
|
172
178
|
validate_string('description', nil)
|
|
173
179
|
validate_string('sources', nil)
|
|
174
180
|
validate_string('prefix', /\A[a-z0-9][a-z0-9+._\/-]*\z/)
|
|
181
|
+
validate_string('min_macos', /\A\d+(\.\d+)*\z/)
|
|
175
182
|
validate_payload
|
|
176
183
|
validate_executables
|
|
177
184
|
validate_links
|
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
|
|
@@ -129,7 +131,7 @@ module Crosspack
|
|
|
129
131
|
Type=Application
|
|
130
132
|
Name=#{d['name'] || @pkg.name}
|
|
131
133
|
Comment=#{d['comment'] || @pkg.summary}
|
|
132
|
-
Exec=#{d['exec'] ||
|
|
134
|
+
Exec=#{d['exec'] || desktop_exec}
|
|
133
135
|
Icon=#{@pkg.name}
|
|
134
136
|
Terminal=false
|
|
135
137
|
Categories=#{categories}
|
|
@@ -137,6 +139,19 @@ module Crosspack
|
|
|
137
139
|
path
|
|
138
140
|
end
|
|
139
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
|
+
|
|
140
155
|
def icon_source
|
|
141
156
|
src = File.expand_path(@pkg.icon, @root)
|
|
142
157
|
raise BuildError, "icon: file not found #{src}" unless File.file?(src)
|
|
@@ -206,12 +221,21 @@ module Crosspack
|
|
|
206
221
|
# Same desktop integration as deb/rpm: the .desktop file and the icon
|
|
207
222
|
# go to the same destinations; their sources are archive-relative
|
|
208
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)] }
|
|
209
225
|
files = @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] }
|
|
210
226
|
if @pkg.desktop
|
|
211
227
|
files["#{@pkg.name}.desktop"] = File.join(@pkg.prefix, 'share', 'applications',
|
|
212
228
|
"#{@pkg.name}.desktop")
|
|
229
|
+
sources["#{@pkg.name}.desktop"] = desktop_entry_path
|
|
213
230
|
end
|
|
214
|
-
|
|
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
|
+
)
|
|
215
239
|
Builders::Pkgbuild.generate(
|
|
216
240
|
name: @pkg.name,
|
|
217
241
|
version: version,
|
|
@@ -225,12 +249,37 @@ module Crosspack
|
|
|
225
249
|
symlinks: @pkg_data[:symlinks],
|
|
226
250
|
executables: @pkg_data[:executables],
|
|
227
251
|
source: ["#{@pkg.name}-#{version}.tar.gz"],
|
|
252
|
+
sha256: sha256,
|
|
228
253
|
license: @pkg.license,
|
|
229
254
|
output: output
|
|
230
255
|
)
|
|
231
256
|
output
|
|
232
257
|
end
|
|
233
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
|
+
|
|
234
283
|
def split_version
|
|
235
284
|
parts = @version.split('-', 2)
|
|
236
285
|
parts << '1' if parts.size == 1
|
|
@@ -252,11 +301,27 @@ module Crosspack
|
|
|
252
301
|
license: @pkg.license,
|
|
253
302
|
files: files,
|
|
254
303
|
shortcuts: windows_shortcuts,
|
|
304
|
+
icon: windows_arp_icon,
|
|
255
305
|
arch: @target.package_arch(:winget),
|
|
256
306
|
output: output
|
|
257
307
|
)
|
|
258
308
|
end
|
|
259
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
|
+
|
|
260
325
|
# Shortcut facts for the MSI: the display name from desktop: (falling
|
|
261
326
|
# back to the package name) and the first executable as the target.
|
|
262
327
|
def windows_shortcuts
|
|
@@ -281,6 +346,7 @@ module Crosspack
|
|
|
281
346
|
files: files,
|
|
282
347
|
executables: @pkg.executables_for(@target),
|
|
283
348
|
icon: @pkg.icon && File.expand_path(@pkg.icon, @root),
|
|
349
|
+
min_macos: @pkg.min_macos,
|
|
284
350
|
output: output_dir
|
|
285
351
|
)
|
|
286
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: []
|