crosspack 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +85 -0
- data/README.md +72 -15
- data/exe/crosspack +37 -19
- data/lib/crossbuild/build_manifest.rb +43 -6
- data/lib/crossbuild/builder.rb +45 -9
- data/lib/crossbuild/matrix.rb +5 -0
- data/lib/crossbuild.rb +7 -6
- data/lib/crosspack/builders/app_dir.rb +43 -12
- data/lib/crosspack/builders/deb.rb +40 -11
- data/lib/crosspack/builders/pkgbuild.rb +7 -6
- data/lib/crosspack/builders/rpm.rb +24 -0
- data/lib/crosspack/builders/wix.rb +120 -16
- data/lib/crosspack/launcher.rb +16 -9
- data/lib/crosspack/package_manifest.rb +8 -1
- data/lib/crosspack/packer.rb +120 -8
- data/lib/crosspack/version.rb +1 -1
- metadata +2 -2
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,91 @@ 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
|
+
|
|
24
|
+
## [0.7.3] - 2026-09-26
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- The manifest icon keeps its real extension everywhere: an SVG now installs
|
|
28
|
+
to `share/icons/hicolor/scalable/apps/<name>.svg` (it was forced to
|
|
29
|
+
`share/pixmaps/<name>.png`, where desktops never found it); PNG/XPM keep
|
|
30
|
+
going to pixmaps.
|
|
31
|
+
- `.desktop` `Categories` values are terminated with `;` per the spec — a
|
|
32
|
+
manifest value like `Utility;Audio` produced an invalid entry GNOME could
|
|
33
|
+
refuse to show.
|
|
34
|
+
- macOS `Info.plist` escapes XML entities (`&`/`<` in the summary no longer
|
|
35
|
+
corrupt the plist), `CFBundleExecutable` is the first `package.executables`
|
|
36
|
+
entry (not the package name) and the bundle icon from `icon:` is copied to
|
|
37
|
+
`Contents/Resources` and referenced via `CFBundleIconFile`.
|
|
38
|
+
- RPM `%description` carries the full package description, not just the
|
|
39
|
+
summary line.
|
|
40
|
+
- PKGBUILD quotes the `license` value (a license with spaces broke the
|
|
41
|
+
generated bash).
|
|
42
|
+
- deb omits an empty `Depends:` field instead of writing an invalid one.
|
|
43
|
+
- WiX `Source` paths are XML-escaped (`&` in a path no longer breaks the
|
|
44
|
+
generated `.wxs`).
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- The PKGBUILD now installs the `.desktop` file and the icon — desktop
|
|
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.
|
|
67
|
+
|
|
68
|
+
[0.7.3]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.3
|
|
69
|
+
|
|
70
|
+
## [0.7.2] - 2026-09-26
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
- Windows MSI no longer fails to install with error 1324: the payload
|
|
74
|
+
directory is now `INSTALLFOLDER` (WiX v4+ treats any other Id as "author
|
|
75
|
+
did not define it" and injects a phantom `[Manufacturer] [ProductName]`
|
|
76
|
+
directory). The deb-style `Name <email>` maintainer is sanitized to the
|
|
77
|
+
name half before it reaches the MSI Manufacturer property.
|
|
78
|
+
- calver versions (e.g. `2026.09.26`) map to a valid MSI ProductVersion
|
|
79
|
+
(`26.9.26`) in the `Package@Version` attribute too, not just the file
|
|
80
|
+
name — a major >= 256 tripped WIX1148.
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- The Windows MSI now has a standard install wizard (`ui:WixUI
|
|
84
|
+
WixUI_InstallDir` with the license dialog): crosspack generates the
|
|
85
|
+
license `.rtf` and builds with `-ext WixToolset.UI.wixext`, installing the
|
|
86
|
+
UI extension itself when missing (`WixToolset.UI.wixext/6.0.1` on WiX 6).
|
|
87
|
+
- Start Menu and Desktop shortcuts for the first `package.executables`
|
|
88
|
+
entry; the display name comes from `package.desktop.name` (falling back
|
|
89
|
+
to the package name). Previously the winget target ignored `desktop:`.
|
|
90
|
+
|
|
91
|
+
[0.7.2]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.2
|
|
92
|
+
|
|
8
93
|
## [0.7.0] - 2026-09-13
|
|
9
94
|
|
|
10
95
|
### Added
|
data/README.md
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
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
|
|
|
6
12
|
```
|
|
7
13
|
crosspack deps <target> # verify/install the build host's dependencies
|
|
8
|
-
crosspack build <
|
|
14
|
+
crosspack build [<goal>] # compile a build goal's matrix entries into builds/
|
|
9
15
|
crosspack pack <target> # package builds/<target> as a native package
|
|
10
16
|
```
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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`:
|
|
18
27
|
|
|
19
28
|
```
|
|
20
29
|
crosspack deps ubuntu-24.04
|
|
@@ -31,6 +40,50 @@ token):
|
|
|
31
40
|
crosspack deps && crosspack build && crosspack pack
|
|
32
41
|
```
|
|
33
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
|
+
|
|
34
87
|
## crosspack.yml — one file, three sections + two host hooks
|
|
35
88
|
|
|
36
89
|
The top level carries the shared facts — `name` (required once) and the
|
|
@@ -65,6 +118,7 @@ build:
|
|
|
65
118
|
|
|
66
119
|
matrix:
|
|
67
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])
|
|
68
122
|
env: # extra env for the steps of this entry
|
|
69
123
|
CGO_ENABLED: "1"
|
|
70
124
|
steps: # shell commands, run from the project root
|
|
@@ -199,11 +253,12 @@ config.
|
|
|
199
253
|
```
|
|
200
254
|
# staged pipeline
|
|
201
255
|
crosspack deps <target> [--check] # verify/install build deps (--check: report only)
|
|
202
|
-
crosspack build <
|
|
256
|
+
crosspack build [<goal>|all] [--no-deps] [--version X]
|
|
257
|
+
# goal: default (implicit), a declared goal, or all
|
|
203
258
|
crosspack pack <target> [--version X] # version defaults to the build stamp
|
|
204
259
|
|
|
205
|
-
# host-only: this machine, no target argument
|
|
206
|
-
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
|
|
207
262
|
crosspack install # launch this host's package with its installer
|
|
208
263
|
|
|
209
264
|
# inspection
|
|
@@ -229,10 +284,11 @@ crosspacks/arch/x86_64/PKGBUILD
|
|
|
229
284
|
### Host-only commands: run and install
|
|
230
285
|
|
|
231
286
|
`run` and `install` apply to the current host only — the build target *is*
|
|
232
|
-
the run target, so they take no target argument. `crosspack run
|
|
233
|
-
build stage for
|
|
234
|
-
launches the first `package.executables`
|
|
235
|
-
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:
|
|
236
292
|
|
|
237
293
|
```
|
|
238
294
|
crosspack run -- --dev
|
|
@@ -258,7 +314,8 @@ executed directly, except a macOS `.app` bundle which is `open`ed.
|
|
|
258
314
|
require 'crosspack' # pulls in Crossbuild too
|
|
259
315
|
|
|
260
316
|
Crossbuild.build('crosspack.yml', root: Dir.pwd) # all host entries
|
|
261
|
-
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
|
|
262
319
|
|
|
263
320
|
Crosspack.pack(
|
|
264
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
|