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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd9fa9d325c41f9b9b56fcbaf6fca186f28fcfff888002ba7f2f896e9f93fd38
4
- data.tar.gz: c9281e44840c2f238ae49feacebb269f822f3ea15fb38681ac47f41804221bf2
3
+ metadata.gz: 5848f2a2de5e0c3a6d88dbb5160b90699f8e6b0075af0598a703d76a3a67a6fe
4
+ data.tar.gz: c653c7473c232c97d22e13656e7def070da7c478ad18ddbc44e8f9d596faa115
5
5
  SHA512:
6
- metadata.gz: c13e601a7402da31931e3d2a8b93a7e9120369a5a157327992ed8aea827b756684f5e320e8880d4e3d1168e18d4e886ed3e6e0465d749d5c308feb628de9afa2
7
- data.tar.gz: 924c9c9a07cdbfd2661f20e07eb0137566a8935f96d7b420275ecc27c3d513e59e885cf6f3a4f2345552f8540b4466213caf9c6f87f41f6c4c03335fe558a463
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 <target> # compile the target's matrix entry into builds/
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
- Every stage is keyed by the same target vocabulary — `debian-12`,
13
- `ubuntu-24.04`, `fedora-41`, `arch`, `macos`, `windows-11.0` — and gated by
14
- the previous one: `build` refuses to start before the deps stage passes, and
15
- `pack` refuses to run before `build` filled `builds/<target>/`. The build
16
- stage also stamps the version into the tree, so `pack` normally needs no
17
- `--version`:
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 <target>|--all [--no-deps] [--version X]
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] # build for this host, then launch the main binary
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` runs the
233
- build stage for this host (same as `crosspack build <host target>`), then
234
- launches the first `package.executables` binary straight from `builds/` —
235
- the binary, not the package. Everything after `--` goes to the app:
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', target: 'ubuntu-24.04') # the stages' build, library-level
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 [<target>]|--all compile into builds/ (default: this host)\n" \
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>] build for this host, then launch the main binary\n" \
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 (default target selection off)') { options[:all] = true }
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 target for the stage commands (deps/build/pack,
79
- # resolve), a crosspack.yml path for validate.
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/build/pack) run for the current host when no target
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
- if options[:all]
196
- target = nil
197
- else
198
- target = positional || host_target!('build')
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
- abort_with e.message
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: no target argument; anything after `--` goes to the
228
- # launched process (crosspack run -- --dev).
229
- if positional
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')
@@ -19,17 +19,19 @@ module Crossbuild
19
19
  @host_arch = host_arch
20
20
  end
21
21
 
22
- # entry_id: nil — every entry buildable on this host; otherwise the exact
23
- # entry id (which must exist, but may target another host — the user
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
- def run(entry_id: nil, target: nil)
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
- buildable = manifest.buildable_entries(@host_os, @host_arch)
71
- raise Error, "nothing to build on #{@host_os}/#{Platform.display_arch(@host_arch)} " \
72
- "(matrix entries: #{manifest.entries.map(&:id).join(', ')})" if buildable.empty?
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
 
@@ -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. target: builds
22
- # the entry distributing to that target only; deps: false skips the
23
- # pre-build dependency check/install pass. The manifest argument may be a
24
- # Config, a path to crosspack.yml or a ready BuildManifest.
25
- def self.build(manifest, entry_id: nil, target: nil, root: Dir.pwd, output_base: nil, version: nil,
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