crosspack 0.2.0 → 0.4.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: cb2e2fb91b4f2215649e739ff6ec7128a11e06abe8d776f870d686261439a974
4
- data.tar.gz: 3b17f1325d46d5147a0d87fee2d62ee2cb021f4a11c4ab16cf54a94562d843ad
3
+ metadata.gz: 527794ea0ef513791363042fcace5c4a973a859e74c4300dedeec931268b00fc
4
+ data.tar.gz: 5bbad1503be58d2190c7f042fdd29ca8271d1d107826d8653589ef17c8587a9d
5
5
  SHA512:
6
- metadata.gz: 19cfe0db0863bff0e3b69d39ccefd0066ae99a85586ae8cabe19951897f0852b70966cc39372740a9a7e779ad36c81827797094befe8e292c7f420d1a17a0578
7
- data.tar.gz: 9b220eff4186f4f1f88875d0b802c0d7a76f1b5d394e0ec828706a2e56d71676f809c1726de82e39f950ad323056b73104c9022c9fa181f554830817748dd612
6
+ metadata.gz: 465b113f78d7e0dce817e0a65c122437998938dbb66cd438c63762289dcc1a9c4a3ccf4df9a7639f73ec983aa20f31a94c34d357118efbdfdfb1415b9828fad1
7
+ data.tar.gz: 021f427e66d2a9c58eb02dde0907cfb7bf5f8c89a081018b2839df819a7c478dc4952f7fc9ba7246c4b2030488065b644401edfd32282ca987809019675c0670
data/CHANGELOG.md CHANGED
@@ -5,6 +5,64 @@ 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.4.0] - 2026-09-13
9
+
10
+ ### Breaking
11
+ - The separate `crossbuild` executable is gone: one command, one staged
12
+ pipeline — `crosspack deps <target>` → `crosspack build <target>` →
13
+ `crosspack pack <target>`. The former crossbuild commands moved under
14
+ `crosspack` (validate/matrix/version gained build.yaml coverage; the gem
15
+ now installs only the `crosspack` binary).
16
+ - build.yaml `deps:` schema: host rules are now mappings with per-system
17
+ `verify` and `install` commands instead of a global `check:` plus a
18
+ bare command string. The 0.3.0 schema was never released outside the
19
+ local gem build, so no migration path is provided.
20
+ - `crosspack pack <target>` takes the target as a positional argument
21
+ (the old `--target` flag is gone) and `--version` is now optional: it
22
+ defaults to the version stamped by the build stage.
23
+
24
+ ### Added
25
+ - Stage pipeline with gating: `crosspack build <target>` resolves the
26
+ matrix entry distributing to that target, runs the deps stage first,
27
+ and fans artifacts out into that target's builds/ directory only;
28
+ `crosspack pack <target>` refuses to run before the build stage
29
+ ("Build stage not done for target ... — Run: crosspack build <target>").
30
+ - The build stage stamps `.crosspack-build` (version) into every
31
+ `builds/<target>/` directory; `crosspack pack` reads it as the default
32
+ package version.
33
+ - deps.yaml schema versioning: an optional top-level `version:` (default
34
+ 1). A deps.yaml declaring a newer schema than this crosspack is
35
+ rejected at load time with an upgrade hint; the key is reserved and no
36
+ longer parsed as a canonical dependency name.
37
+ - deps: rules may be verify-only (no install command) — presence is
38
+ checked, and a missing dependency reports which `install:` to add.
39
+
40
+ ## [0.3.0] - 2026-09-13
41
+
42
+ ### Added
43
+ - build.yaml `deps:` section: build dependencies with per-host install
44
+ commands (e.g. `ubuntu: sudo apt-get install -y imagemagick`, `windows:
45
+ winget install ...`, or any project script). Host selectors resolve in
46
+ order: distro id from `/etc/os-release`, `ID_LIKE` tokens, os, `"*"`.
47
+ - `crossbuild deps` command: doctor + install of the deps: dependencies;
48
+ `--check` reports presence only and exits 1 when something is missing.
49
+ - `crossbuild build` now verifies/installs deps: dependencies before the
50
+ matrix entries; `--no-deps` skips the pass.
51
+ - `crossbuild matrix` lists how each deps: entry resolves on this host.
52
+
53
+ ## [0.2.1] - 2026-09-11
54
+
55
+ ### Fixed
56
+ - WiX builder: `wix build` is now invoked with `-arch <arch>` as separate
57
+ argv entries — the previous single `"-arch x64"` argument could never parse.
58
+ - WiX builder: a bare `dotnet` on PATH is no longer mistaken for the WiX
59
+ toolset (`dotnet build` cannot build an MSI); only a real `wix` command
60
+ triggers the real MSI build, otherwise the `.wxs` + `BUILD-MSI.txt`
61
+ fallback is emitted.
62
+ - WiX builder: manifest values (name, maintainer, summary) are XML-escaped
63
+ in the generated `.wxs`; a maintainer like `Name <email>` previously
64
+ produced invalid XML no WiX build could parse.
65
+
8
66
  ## [0.2.0] - 2026-09-08
9
67
 
10
68
  ### Added
data/README.md CHANGED
@@ -1,32 +1,49 @@
1
1
  # crosspack
2
2
 
3
3
  Cross-platform build **and** pack toolchain for Wails-style desktop apps,
4
- shipped as one gem with two cooperating commands:
4
+ shipped as one gem with one command and a staged pipeline:
5
5
 
6
- - **`crossbuild`** — runs the build steps of a project from a small
7
- `build.yaml` matrix and fans the produced artifacts out into the
8
- per-target tree `builds/<family>[/<version>]/<arch>/`.
9
- - **`crosspack`** — turns that compiled tree into native packages
10
- (deb / rpm / PKGBUILD / WiX / app bundle) for any target
11
- distro × version × arch, driven by two small manifests.
6
+ ```
7
+ crosspack deps <target> # verify/install the build host's dependencies
8
+ crosspack build <target> # compile the target's matrix entry into builds/
9
+ crosspack pack <target> # package builds/<target> as a native package
10
+ ```
12
11
 
13
- The two commands share the same target vocabulary (`Crosspack::Target`),
14
- so the build tree and the pack tree never drift apart. Build and pack
15
- stay cleanly separated: `crossbuild` never packages, `crosspack` never
16
- compiles — feed the output of one straight into the other:
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`:
17
18
 
18
19
  ```
19
- crossbuild build # builds/ tree
20
- crosspack pack --target debian-12 --version 2026.08.31-33837
20
+ crosspack deps ubuntu-24.04
21
+ crosspack build ubuntu-24.04
22
+ crosspack pack ubuntu-24.04
21
23
  ```
22
24
 
23
- ## crossbuild: build.yaml
25
+ ## build.yaml — what and where to build
24
26
 
25
27
  ```yaml
26
28
  name: myapp
27
29
  version: calver # calver | git-tag | env:VAR | any literal string
28
30
  output: builds # default builds; mirrors package.yaml `sources`
29
31
 
32
+ deps: # build dependencies, per-host verify/install
33
+ imagemagick:
34
+ hosts: # first selector matching this host wins
35
+ ubuntu:
36
+ verify: dpkg -s imagemagick
37
+ install: sudo apt-get install -y imagemagick
38
+ darwin:
39
+ verify: magick -version
40
+ install: brew install imagemagick
41
+ windows:
42
+ verify: where magick
43
+ install: winget install -e --id ImageMagick.ImageMagick
44
+ "*": # any command — package manager or a project script
45
+ install: ./scripts/install-imagemagick.sh
46
+
30
47
  matrix:
31
48
  - build: linux/amd64 # os/arch this entry builds on (arch may be "any")
32
49
  env: # extra env for the steps of this entry
@@ -63,6 +80,19 @@ builds/ubuntu/24.04/amd64/myapp -> build/bin/myapp (symlink)
63
80
  builds/windows/11.0/x86_64/myapp.exe
64
81
  ```
65
82
 
83
+ ### Build dependencies (deps:)
84
+
85
+ `deps:` is the host axis to `deps.yaml`'s target axis: deps.yaml declares
86
+ what a *package* needs at runtime; `deps:` declares what the *build host*
87
+ must have. Checks differ per system, so every host rule carries its own
88
+ commands: `verify` (presence probe; exit 0 = present; without it the name is
89
+ looked up in PATH) and `install` (how to install it; may be omitted for
90
+ verify-only rules). Selectors are tried in order: distro id from
91
+ `/etc/os-release` (`ubuntu`), then `ID_LIKE` tokens (`debian`), then the os
92
+ (`linux`, `darwin`, `windows`), then `"*"`. The deps stage runs verify →
93
+ install → re-verify for every dependency; a dependency that is still missing
94
+ fails the stage. Commands support the `{{name}}`/`{{os}}`/... placeholders.
95
+
66
96
  ### Version schemes
67
97
 
68
98
  | scheme | value |
@@ -72,22 +102,9 @@ builds/windows/11.0/x86_64/myapp.exe
72
102
  | `env:VAR` | taken from `VAR`; build fails loudly when unset |
73
103
  | other string | used verbatim, e.g. `version: 1.2.3` |
74
104
 
75
- ### crossbuild CLI
76
-
77
- ```
78
- crossbuild validate # build.yaml against the schema
79
- crossbuild matrix # entries × host buildability table
80
- crossbuild version # computed version
81
- crossbuild build [--target linux/amd64] # all host-buildable entries, or one
82
- crossbuild targets # what is in builds/ ready to pack
83
- ```
84
-
85
- `build` refuses invalid manifests with path-pointing errors, runs only the
86
- entries whose `build:` platform matches the host, then distributes.
105
+ ## package.yaml + deps.yaml — what and how to pack
87
106
 
88
- ## crosspack: two manifests
89
-
90
- **`package.yaml`** — what and how to pack:
107
+ **`package.yaml`**:
91
108
 
92
109
  ```yaml
93
110
  name: myapp
@@ -99,9 +116,9 @@ description: |-
99
116
  license: Proprietary # optional, default Proprietary
100
117
  section: utils # optional, deb only
101
118
 
102
- sources: builds # compiled artifacts tree written by crossbuild:
103
- # builds/<family>/<version>/<arch>/ — pack without
104
- # a matching build directory fails honestly
119
+ sources: builds # compiled artifacts tree written by the build
120
+ # stage: builds/<family>/<version>/<arch>/ —
121
+ # pack without a matching build dir fails honestly
105
122
  prefix: usr/local # install prefix inside the package
106
123
  lib_dir: lib/myapp # optional, default lib/<name>
107
124
 
@@ -124,9 +141,13 @@ icon: build/appicon.png # optional, -> <prefix>/share/pixmaps/<name>.png
124
141
  ```
125
142
 
126
143
  **`deps.yaml`** — canonical dependency names resolved to concrete packages
127
- per distro family/version (see the file itself for the full schema):
144
+ per distro family/version. An optional top-level `version:` declares the
145
+ schema version; a deps.yaml written for a newer crosspack is rejected with
146
+ an upgrade hint:
128
147
 
129
148
  ```yaml
149
+ version: 1 # optional, currently 1
150
+
130
151
  webkit2gtk:
131
152
  targets:
132
153
  debian:
@@ -136,17 +157,24 @@ webkit2gtk:
136
157
  windows: system # preinstalled (WebView2)
137
158
  ```
138
159
 
139
- Both files are validated against embedded schemas with actionable,
140
- path-pointing error messages; every builder refuses to run on an invalid
160
+ All three files are validated against embedded schemas with actionable,
161
+ path-pointing error messages; every stage refuses to run on an invalid
141
162
  manifest.
142
163
 
143
164
  ## CLI
144
165
 
145
166
  ```
146
- crosspack validate # both package.yaml and deps.yaml
147
- crosspack resolve --target debian-12 # Depends line for the target
148
- crosspack matrix # dependency x target table
149
- crosspack pack --target debian-12 --version 2026.08.31-1234 [--root .]
167
+ # staged pipeline
168
+ crosspack deps <target> [--check] # verify/install build deps (--check: report only)
169
+ crosspack build <target>|--all [--no-deps] [--version X]
170
+ crosspack pack <target> [--version X] # version defaults to the build stamp
171
+
172
+ # inspection
173
+ crosspack validate # package.yaml, deps.yaml and build.yaml
174
+ crosspack resolve <target> # Depends line for the target
175
+ crosspack matrix # deps x target + build matrix tables
176
+ crosspack version # computed build version
177
+ crosspack targets # what is in builds/ ready to pack
150
178
  ```
151
179
 
152
180
  `pack` picks the builder from the target and writes to
@@ -164,20 +192,21 @@ crosspacks/arch/x86_64/PKGBUILD
164
192
  require 'crosspack' # pulls in Crossbuild too
165
193
 
166
194
  Crossbuild.build('build.yaml', root: Dir.pwd) # all host entries
167
- Crossbuild.build('build.yaml', entry_id: 'windows') # one entry explicitly
195
+ Crossbuild.build('build.yaml', target: 'ubuntu-24.04') # the stages' build, library-level
168
196
 
169
197
  Crosspack.pack(
170
198
  manifest: 'package.yaml', deps: 'deps.yaml',
171
199
  target: Crosspack::Target.parse('debian-12', arch: 'amd64'),
172
- version: '2026.08.31-1234',
200
+ version: nil, # nil -> the version stamped by the build stage
173
201
  root: Dir.pwd, output_base: 'crosspacks'
174
202
  )
175
203
  ```
176
204
 
177
205
  Lower-level pieces are public too: on the build side `Crossbuild::
178
206
  BuildManifest`, `VersionScheme`, `Runner`, `Distributor`, `Matrix`,
179
- `Builder`, `Platform`; on the pack side `Crosspack::PackageManifest`,
180
- `Manifest`, `Resolver`, `Matrix`, `Target`, `Builders::*`.
207
+ `Builder`, `DepInstaller`, `Platform`; on the pack side `Crosspack::
208
+ PackageManifest`, `Manifest`, `Resolver`, `Matrix`, `Target`, `Builds`,
209
+ `Builders::*`.
181
210
 
182
211
  Deb needs `dpkg-deb` (present on any Debian/Ubuntu), rpm needs
183
212
  `rpmbuild` (`sudo apt install rpm`), PKGBUILD generation needs nothing.
data/exe/crosspack CHANGED
@@ -5,8 +5,10 @@ require 'optparse'
5
5
 
6
6
  begin
7
7
  require 'crosspack'
8
+ require 'crossbuild'
8
9
  rescue LoadError
9
10
  require_relative '../lib/crosspack'
11
+ require_relative '../lib/crossbuild'
10
12
  end
11
13
 
12
14
  def abort_with(message)
@@ -14,21 +16,28 @@ def abort_with(message)
14
16
  exit 1
15
17
  end
16
18
 
17
- options = { file: 'deps.yaml', package: 'package.yaml', arch: 'amd64', format: nil,
18
- version: nil, root: Dir.pwd, output_base: 'crosspacks' }
19
+ options = { file: 'deps.yaml', package: 'package.yaml', build: 'build.yaml', arch: 'amd64',
20
+ format: nil, version: nil, root: Dir.pwd, output_base: nil,
21
+ check_only: false, no_deps: false, all: false }
19
22
  parser = OptionParser.new do |opts|
20
- opts.banner = "crosspack #{Crosspack::VERSION} — native package packer (deb/rpm/PKGBUILD/MSI/app)\n" \
23
+ opts.banner = "crosspack #{Crosspack::VERSION} — staged pipeline: deps → build → pack\n" \
21
24
  "Usage: crosspack <command> [options]\n" \
22
- 'Commands: validate, resolve, matrix, pack, targets'
25
+ "Stages: deps <target> [--check] verify/install build deps for the target's host\n" \
26
+ " build <target>|--all compile the target's matrix entry into builds/\n" \
27
+ " pack <target> [--version X] package builds/<target> (version from the build stamp)\n" \
28
+ 'Commands: validate, resolve, matrix, version, targets'
23
29
 
24
30
  opts.on('-f', '--file PATH', 'path to deps.yaml (default: ./deps.yaml)') { |v| options[:file] = v }
25
31
  opts.on('--package PATH', 'path to package.yaml (default: ./package.yaml)') { |v| options[:package] = v }
26
- opts.on('--target TARGET', 'target: debian-12, ubuntu-24.04, fedora-41, arch, macos, windows') { |v| options[:target] = v }
32
+ opts.on('--build PATH', 'path to build.yaml (default: ./build.yaml)') { |v| options[:build] = v }
27
33
  opts.on('--arch ARCH', 'architecture: amd64/x86_64, arm64/aarch64 (default: amd64)') { |v| options[:arch] = v }
28
34
  opts.on('--format FORMAT', 'output format for resolve: deb, rpm, pkgbuild (default: derived from the target)') { |v| options[:format] = v.to_sym }
29
- opts.on('--version VERSION', 'package version for pack (required for pack)') { |v| options[:version] = v }
35
+ opts.on('--version VERSION', 'override the version for build/pack') { |v| options[:version] = v }
30
36
  opts.on('--root PATH', 'project root for relative paths (default: .)') { |v| options[:root] = v }
31
- opts.on('--output-base DIR', 'output directory (default: ./crosspacks)') { |v| options[:output_base] = v }
37
+ opts.on('--output-base DIR', 'output directory (build stage: the manifest output, default builds; pack: default ./crosspacks)') { |v| options[:output_base] = v }
38
+ opts.on('--check', 'with `deps`: only report presence, never install (exit 1 if missing)') { options[:check_only] = true }
39
+ opts.on('--no-deps', 'with `build`: skip the deps stage') { options[:no_deps] = true }
40
+ opts.on('--all', 'with `build`: every host-buildable matrix entry (default target selection off)') { options[:all] = true }
32
41
  opts.on('-h', '--help', 'show help') do
33
42
  puts opts
34
43
  exit
@@ -43,63 +52,142 @@ end
43
52
  command = ARGV.shift
44
53
  abort_with 'No command given. See: crosspack --help' if command.nil?
45
54
 
46
- # Positional file path: crosspack validate path/to/deps.yaml
47
- options[:file] = ARGV.shift if ARGV.first && !ARGV.first.start_with?('-')
55
+ # Positional argument: a target for the stage commands (deps/build/pack,
56
+ # resolve), a deps.yaml path for validate.
57
+ positional = ARGV.shift if ARGV.first && !ARGV.first.start_with?('-')
48
58
 
49
- begin
50
- manifest = Crosspack::Manifest.load(options[:file])
51
- rescue Crosspack::InvalidManifestError => e
52
- abort_with e.message
59
+ build_path = File.expand_path(options[:build], options[:root])
60
+ build_manifest = if File.file?(build_path)
61
+ begin
62
+ Crossbuild::BuildManifest.load(build_path)
63
+ rescue Crossbuild::InvalidManifestError => e
64
+ abort_with e.message
65
+ end
66
+ end
67
+
68
+ # The matrix entry that builds `target` — the deps/build stages operate on
69
+ # its host platform.
70
+ def entry_for_target!(manifest, target, host_os, host_arch)
71
+ entry = manifest.entry_for_target(target)
72
+ host = "#{host_os}/#{Crossbuild::Platform.display_arch(host_arch)}"
73
+ unless entry.buildable_on?(host_os, host_arch)
74
+ abort_with "The entry for #{target.inspect} (#{entry.id}) builds on #{entry.platform}, " \
75
+ "this host is #{host} — cross-compilation is not supported"
76
+ end
77
+
78
+ entry
53
79
  end
54
80
 
55
81
  case command
56
82
  when 'validate'
83
+ if positional
84
+ options[:file] = positional
85
+ elsif build_manifest.nil?
86
+ warn "note: #{options[:build]} not found — the deps/build stages need it (validate covered deps/package only)"
87
+ end
57
88
  begin
58
- pkg = Crosspack::PackageManifest.load(options[:package])
89
+ pkg = Crosspack::PackageManifest.load(File.expand_path(options[:package], options[:root]))
90
+ rescue Crosspack::InvalidManifestError => e
91
+ abort_with e.message
92
+ end
93
+ begin
94
+ manifest = Crosspack::Manifest.load(File.expand_path(options[:file], options[:root]))
59
95
  rescue Crosspack::InvalidManifestError => e
60
96
  abort_with e.message
61
97
  end
62
98
  puts manifest.error_report
63
99
  puts pkg.error_report
64
- exit(manifest.valid? && pkg.valid? ? 0 : 1)
100
+ puts build_manifest.error_report if build_manifest
101
+ ok = manifest.valid? && pkg.valid? && (build_manifest.nil? || build_manifest.valid?)
102
+ exit(ok ? 0 : 1)
65
103
  when 'matrix'
66
- abort_with 'Schema is invalid — the matrix cannot be built. Run: crosspack validate' unless manifest.valid?
67
- puts Crosspack::Matrix.new(manifest).render
68
- when 'resolve'
69
- abort_with 'Specify a target: --target debian-12 (see crosspack --help)' unless options[:target]
70
104
  begin
71
- target = Crosspack::Target.parse(options[:target], arch: options[:arch])
72
- rescue Crosspack::Target::Error => e
73
- abort_with "Invalid target #{options[:target].inspect}: #{e.message}"
105
+ manifest = Crosspack::Manifest.load(File.expand_path(options[:file], options[:root]))
106
+ rescue Crosspack::InvalidManifestError => e
107
+ abort_with e.message
108
+ end
109
+ abort_with 'deps.yaml schema is invalid — run: crosspack validate' unless manifest.valid?
110
+ puts Crosspack::Matrix.new(manifest).render
111
+ if build_manifest
112
+ puts "\n#{Crossbuild::Matrix.new(build_manifest).render}"
113
+ else
114
+ warn "\nnote: #{options[:build]} not found — build matrix not shown"
74
115
  end
116
+ when 'resolve'
117
+ target_str = positional
118
+ abort_with 'Specify a target: crosspack resolve debian-12 (see crosspack --help)' unless target_str
75
119
  begin
120
+ manifest = Crosspack::Manifest.load(File.expand_path(options[:file], options[:root]))
121
+ target = Crosspack::Target.parse(target_str, arch: options[:arch])
76
122
  resolver = Crosspack::Resolver.new(manifest)
77
123
  format = options[:format] || target.format
78
124
  result = resolver.depends(target, format: format)
125
+ puts "# #{target} (#{format})"
79
126
  if result.is_a?(Array)
80
- puts "# #{target} (#{format})"
81
127
  result.each { |d| puts d }
82
128
  else
83
- puts "# #{target} (#{format})"
84
129
  puts "Depends: #{result}"
85
130
  end
86
- rescue Crosspack::ResolveError => e
87
- abort_with "Failed to resolve dependencies for #{target}:\n #{e.message}"
131
+ rescue Crosspack::Target::Error => e
132
+ abort_with "Invalid target #{target_str.inspect}: #{e.message}"
133
+ rescue Crosspack::InvalidManifestError, Crosspack::ResolveError => e
134
+ abort_with e.message
135
+ end
136
+ when 'version'
137
+ abort_with "#{options[:build]} not found — crosspack version needs a build manifest" unless build_manifest
138
+ abort_with 'Schema is invalid. Run: crosspack validate' unless build_manifest.valid?
139
+ begin
140
+ puts Crossbuild::VersionScheme.new(build_manifest.version).compute(root: options[:root])
141
+ rescue Crossbuild::VersionScheme::Error => e
142
+ abort_with e.message
143
+ end
144
+ when 'deps'
145
+ abort_with 'Specify a target: crosspack deps debian-12 (see crosspack --help)' unless positional
146
+ abort_with "#{options[:build]} not found — crosspack deps needs a build manifest" unless build_manifest
147
+ entry_for_target!(build_manifest, positional, Crossbuild::Platform.os, Crossbuild::Platform.arch)
148
+ installer = Crossbuild::DepInstaller.new(build_manifest, root: options[:root])
149
+ begin
150
+ if options[:check_only]
151
+ installer.check_all
152
+ puts "deps on this host:\n#{installer.report}"
153
+ exit(installer.statuses.any? { |s| !s.installed } ? 1 : 0)
154
+ end
155
+ installer.ensure_all
156
+ rescue Crossbuild::Error => e
157
+ abort_with e.message
158
+ end
159
+ puts "deps ready:\n#{installer.report}"
160
+ when 'build'
161
+ abort_with "#{options[:build]} not found — crosspack build needs a build manifest" unless build_manifest
162
+ if options[:all]
163
+ target = nil
164
+ else
165
+ abort_with 'Specify a target: crosspack build ubuntu-24.04 (or --all for every host-buildable entry)' unless positional
166
+ target = positional
167
+ end
168
+ begin
169
+ result = Crossbuild::Builder.new(build_manifest, root: options[:root],
170
+ output_base: options[:output_base], version: options[:version],
171
+ deps: !options[:no_deps]).run(target: target)
172
+ rescue Crossbuild::Error, Crosspack::InvalidManifestError => e
173
+ abort_with e.message
88
174
  end
175
+ puts "\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}"
176
+ puts 'Pack with: crosspack pack <target>'
89
177
  when 'pack'
90
- abort_with 'Specify a target: --target debian-12 and a version: --version 1.0-1' unless options[:target] && options[:version]
178
+ abort_with 'Specify a target: crosspack pack debian-12' unless positional
91
179
  begin
92
- target = Crosspack::Target.parse(options[:target], arch: options[:arch])
180
+ target = Crosspack::Target.parse(positional, arch: options[:arch])
93
181
  path = Crosspack.pack(
94
182
  manifest: options[:package],
95
183
  deps: options[:file],
96
184
  target: target,
97
185
  version: options[:version],
98
186
  root: options[:root],
99
- output_base: options[:output_base]
187
+ output_base: options[:output_base] || 'crosspacks'
100
188
  )
101
189
  rescue Crosspack::Target::Error => e
102
- abort_with "Invalid target #{options[:target].inspect}: #{e.message}"
190
+ abort_with "Invalid target #{positional.inspect}: #{e.message}"
103
191
  rescue Crosspack::InvalidManifestError, Crosspack::ResolveError, Crosspack::BuildError => e
104
192
  abort_with e.message
105
193
  end
@@ -115,5 +203,5 @@ when 'targets'
115
203
  root = sources ? File.expand_path(sources, options[:root]) : File.join(options[:root], 'builds')
116
204
  puts Crosspack::Builds.report(root)
117
205
  else
118
- abort_with "Unknown command #{command.inspect}. Available: validate, resolve, matrix, pack, targets"
206
+ abort_with "Unknown command #{command.inspect}. Available: deps, build, pack, validate, resolve, matrix, version, targets"
119
207
  end
@@ -24,9 +24,10 @@ module Crossbuild
24
24
  ARCH_LIST = %w[amd64 x86_64 arm64 aarch64 any].freeze
25
25
  ARTIFACT_MODES = %w[symlink copy].freeze
26
26
  VERSION_SCHEMES = %w[calver git-tag].freeze
27
- TOP_LEVEL_KEYS = %w[name version output matrix].freeze
27
+ TOP_LEVEL_KEYS = %w[name version output deps matrix].freeze
28
28
  ENTRY_KEYS = %w[id build env steps artifacts].freeze
29
29
  ARTIFACTS_KEYS = %w[from include to mode].freeze
30
+ DEP_KEYS = %w[hosts].freeze
30
31
  BUILD_RE = /\A(linux|darwin|windows)\/(#{ARCH_LIST.join('|')})\z/.freeze
31
32
  NAME_RE = /\A[a-z0-9][a-z0-9+._-]*\z/i.freeze
32
33
 
@@ -54,6 +55,22 @@ module Crossbuild
54
55
  end
55
56
  end
56
57
 
58
+ # A build dependency from the top-level deps: section. `hosts` maps a
59
+ # host selector (distro id from os-release, ID_LIKE token, os name or
60
+ # "*") to a rule with per-system commands:
61
+ # verify: presence probe (default: PATH lookup of the name)
62
+ # install: how to install it (a verify-only rule may omit this)
63
+ class Dependency
64
+ RULE_KEYS = %w[verify install].freeze
65
+
66
+ attr_reader :name, :hosts
67
+
68
+ def initialize(name, hosts)
69
+ @name = name
70
+ @hosts = hosts
71
+ end
72
+ end
73
+
57
74
  # Where artifacts come from and which builds/ directories they fan out to.
58
75
  class Artifacts
59
76
  attr_reader :from, :include, :to, :mode
@@ -66,7 +83,7 @@ module Crossbuild
66
83
  end
67
84
  end
68
85
 
69
- attr_reader :path, :name, :version, :output, :entries, :errors, :warnings
86
+ attr_reader :path, :name, :version, :output, :deps, :entries, :errors, :warnings
70
87
 
71
88
  def self.load(path)
72
89
  unless File.file?(path)
@@ -89,6 +106,7 @@ module Crossbuild
89
106
  @name = nil
90
107
  @version = nil
91
108
  @output = 'builds'
109
+ @deps = []
92
110
  @entries = []
93
111
  @errors = []
94
112
  @warnings = []
@@ -132,6 +150,26 @@ module Crossbuild
132
150
  @entries.find { |e| e.id == id }
133
151
  end
134
152
 
153
+ # The single matrix entry that distributes artifacts to the given target
154
+ # string (e.g. "ubuntu-24.04"). Raises Crossbuild::Error when no entry
155
+ # claims it or when several do (ambiguous).
156
+ def entry_for_target(raw)
157
+ matches = @entries.select { |e| e.artifacts&.to&.include?(raw) }
158
+ declared = @entries.filter_map { |e| e.artifacts&.to }.flatten.uniq
159
+ if matches.empty?
160
+ raise Error,
161
+ "no matrix entry distributes to target #{raw.inspect}; " \
162
+ "declared targets: #{declared.join(', ')}"
163
+ end
164
+ if matches.size > 1
165
+ raise Error,
166
+ "target #{raw.inspect} is distributed by several entries " \
167
+ "(#{matches.map(&:id).join(', ')}) — crosspack build <target> needs exactly one"
168
+ end
169
+
170
+ matches.first
171
+ end
172
+
135
173
  private
136
174
 
137
175
  def validate
@@ -144,6 +182,7 @@ module Crossbuild
144
182
  validate_name
145
183
  validate_version
146
184
  validate_output
185
+ validate_deps
147
186
  validate_matrix
148
187
  end
149
188
 
@@ -184,6 +223,76 @@ module Crossbuild
184
223
  end
185
224
  end
186
225
 
226
+ def validate_deps
227
+ deps = @raw['deps']
228
+ return if deps.nil?
229
+
230
+ unless deps.is_a?(Hash)
231
+ @errors << Issue.new(
232
+ 'deps',
233
+ "must be a mapping of dependency name -> {check?, hosts}; expected:\n" \
234
+ " deps:\n" \
235
+ " imagemagick:\n" \
236
+ " check: magick -version\n" \
237
+ " hosts:\n" \
238
+ " ubuntu: sudo apt-get install -y imagemagick"
239
+ )
240
+ return
241
+ end
242
+
243
+ deps.each { |name, body| validate_dep(name, body) }
244
+ end
245
+
246
+ def validate_dep(name, body)
247
+ path = "deps.#{name}"
248
+ if name !~ NAME_RE
249
+ @errors << Issue.new(path, "invalid dependency name #{name.inspect} (letters, digits, \"+\", \"_\", \"-\", \".\")")
250
+ return
251
+ end
252
+ unless body.is_a?(Hash)
253
+ @errors << Issue.new(path, 'must be a mapping with the key: hosts')
254
+ return
255
+ end
256
+ check_unknown_keys(path, body.keys, DEP_KEYS)
257
+
258
+ hosts = body['hosts']
259
+ unless hosts.is_a?(Hash) && !hosts.empty?
260
+ @errors << Issue.new(
261
+ "#{path}.hosts",
262
+ "must be a non-empty mapping of host selector -> {verify?, install?}; expected:\n" \
263
+ " deps:\n" \
264
+ " #{name}:\n" \
265
+ " hosts:\n" \
266
+ " ubuntu:\n" \
267
+ " verify: dpkg -s #{name}\n" \
268
+ " install: sudo apt-get install -y #{name}"
269
+ )
270
+ return
271
+ end
272
+ hosts.each { |selector, rule| validate_host_rule(name, selector, rule) }
273
+ return unless valid_dep?(name, body)
274
+
275
+ @deps << Dependency.new(name, hosts)
276
+ end
277
+
278
+ def validate_host_rule(name, selector, rule)
279
+ path = "deps.#{name}.hosts.#{selector}"
280
+ unless rule.is_a?(Hash) && !rule.empty?
281
+ @errors << Issue.new(path, "must be a mapping with verify and/or install commands, e.g. { install: sudo apt-get install -y #{name} }")
282
+ return
283
+ end
284
+ check_unknown_keys(path, rule.keys, Dependency::RULE_KEYS)
285
+ rule.each do |key, command|
286
+ unless command.is_a?(String) && !command.strip.empty?
287
+ @errors << Issue.new("#{path}.#{key}", 'must be a non-empty shell command')
288
+ end
289
+ end
290
+ end
291
+
292
+ def valid_dep?(name, body)
293
+ @errors.none? { |e| e.path.start_with?("deps.#{name}") }
294
+ end
295
+
187
296
  def validate_matrix
188
297
  matrix = @raw['matrix']
189
298
  unless matrix.is_a?(Array) && !matrix.empty?