crosspack 0.2.1 → 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 +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +72 -43
- data/exe/crosspack +119 -31
- data/lib/crossbuild/build_manifest.rb +111 -2
- data/lib/crossbuild/builder.rb +32 -6
- data/lib/crossbuild/dep_installer.rb +126 -0
- data/lib/crossbuild/distributor.rb +15 -2
- data/lib/crossbuild/matrix.rb +28 -1
- data/lib/crossbuild/platform.rb +33 -0
- data/lib/crossbuild/version.rb +1 -1
- data/lib/crossbuild.rb +8 -5
- data/lib/crosspack/builds.rb +12 -0
- data/lib/crosspack/manifest.rb +33 -5
- data/lib/crosspack/packer.rb +23 -5
- data/lib/crosspack/version.rb +1 -1
- metadata +9 -9
- data/exe/crossbuild +0 -76
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 527794ea0ef513791363042fcace5c4a973a859e74c4300dedeec931268b00fc
|
|
4
|
+
data.tar.gz: 5bbad1503be58d2190c7f042fdd29ca8271d1d107826d8653589ef17c8587a9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 465b113f78d7e0dce817e0a65c122437998938dbb66cd438c63762289dcc1a9c4a3ccf4df9a7639f73ec983aa20f31a94c34d357118efbdfdfb1415b9828fad1
|
|
7
|
+
data.tar.gz: 021f427e66d2a9c58eb02dde0907cfb7bf5f8c89a081018b2839df819a7c478dc4952f7fc9ba7246c4b2030488065b644401edfd32282ca987809019675c0670
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ 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
|
+
|
|
8
53
|
## [0.2.1] - 2026-09-11
|
|
9
54
|
|
|
10
55
|
### Fixed
|
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
|
|
4
|
+
shipped as one gem with one command and a staged pipeline:
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
crosspack
|
|
20
|
+
crosspack deps ubuntu-24.04
|
|
21
|
+
crosspack build ubuntu-24.04
|
|
22
|
+
crosspack pack ubuntu-24.04
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
103
|
-
# builds/<family>/<version>/<arch>/ —
|
|
104
|
-
# a matching build
|
|
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
|
|
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
|
-
|
|
140
|
-
path-pointing error messages; every
|
|
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
|
-
|
|
147
|
-
crosspack
|
|
148
|
-
crosspack
|
|
149
|
-
crosspack pack
|
|
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',
|
|
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:
|
|
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::
|
|
180
|
-
`Manifest`, `Resolver`, `Matrix`, `Target`, `
|
|
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',
|
|
18
|
-
version: nil, root: Dir.pwd, output_base:
|
|
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} —
|
|
23
|
+
opts.banner = "crosspack #{Crosspack::VERSION} — staged pipeline: deps → build → pack\n" \
|
|
21
24
|
"Usage: crosspack <command> [options]\n" \
|
|
22
|
-
|
|
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('--
|
|
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', '
|
|
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
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
rescue Crosspack::
|
|
73
|
-
abort_with
|
|
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::
|
|
87
|
-
abort_with "
|
|
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:
|
|
178
|
+
abort_with 'Specify a target: crosspack pack debian-12' unless positional
|
|
91
179
|
begin
|
|
92
|
-
target = Crosspack::Target.parse(
|
|
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 #{
|
|
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,
|
|
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?
|
data/lib/crossbuild/builder.rb
CHANGED
|
@@ -8,12 +8,13 @@ module Crossbuild
|
|
|
8
8
|
|
|
9
9
|
attr_reader :manifest
|
|
10
10
|
|
|
11
|
-
def initialize(manifest, root: Dir.pwd, output_base: nil, version: nil,
|
|
11
|
+
def initialize(manifest, root: Dir.pwd, output_base: nil, version: nil, deps: true,
|
|
12
12
|
host_os: Platform.os, host_arch: Platform.arch)
|
|
13
13
|
@manifest = manifest
|
|
14
14
|
@root = root
|
|
15
15
|
@output_base = output_base || manifest.output
|
|
16
16
|
@version_override = version
|
|
17
|
+
@deps = deps
|
|
17
18
|
@host_os = host_os
|
|
18
19
|
@host_arch = host_arch
|
|
19
20
|
end
|
|
@@ -21,20 +22,44 @@ module Crossbuild
|
|
|
21
22
|
# entry_id: nil — every entry buildable on this host; otherwise the exact
|
|
22
23
|
# entry id (which must exist, but may target another host — the user
|
|
23
24
|
# asked for it explicitly).
|
|
24
|
-
|
|
25
|
+
# target: a raw target string (e.g. "ubuntu-24.04") — builds the single
|
|
26
|
+
# matrix entry distributing to it and fans artifacts out to that target
|
|
27
|
+
# only; the entry must be buildable on this host (no cross-compilation).
|
|
28
|
+
def run(entry_id: nil, target: nil)
|
|
25
29
|
manifest.validate!
|
|
30
|
+
ensure_deps
|
|
26
31
|
version = @version_override || VersionScheme.new(manifest.version).compute(root: @root)
|
|
27
|
-
entries = select_entries(entry_id)
|
|
32
|
+
entries = target ? entries_for_target(target) : select_entries(entry_id)
|
|
28
33
|
|
|
29
34
|
entries.each do |entry|
|
|
30
35
|
puts "\n==> #{manifest.name} [#{entry.id}] version #{version}"
|
|
31
|
-
run_entry(entry, version)
|
|
36
|
+
run_entry(entry, version, only: target)
|
|
32
37
|
end
|
|
33
38
|
Result.new(version: version, entries: entries)
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
private
|
|
37
42
|
|
|
43
|
+
# deps: false (--no-deps) skips the pre-build check/install pass.
|
|
44
|
+
def ensure_deps
|
|
45
|
+
return if !@deps || manifest.deps.empty?
|
|
46
|
+
|
|
47
|
+
puts '⧗ build deps'
|
|
48
|
+
DepInstaller.new(manifest, root: @root).ensure_all
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def entries_for_target(raw)
|
|
52
|
+
entry = manifest.entry_for_target(raw)
|
|
53
|
+
host = "#{@host_os}/#{Platform.display_arch(@host_arch)}"
|
|
54
|
+
unless entry.buildable_on?(@host_os, @host_arch)
|
|
55
|
+
raise Error,
|
|
56
|
+
"the entry for #{raw.inspect} (#{entry.id}) builds on #{entry.platform}, " \
|
|
57
|
+
"this host is #{host} — cross-compilation is not supported"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
[entry]
|
|
61
|
+
end
|
|
62
|
+
|
|
38
63
|
def select_entries(entry_id)
|
|
39
64
|
if entry_id
|
|
40
65
|
entry = manifest.find_entry(entry_id)
|
|
@@ -50,7 +75,7 @@ module Crossbuild
|
|
|
50
75
|
end
|
|
51
76
|
end
|
|
52
77
|
|
|
53
|
-
def run_entry(entry, version)
|
|
78
|
+
def run_entry(entry, version, only: nil)
|
|
54
79
|
arch = entry.arch == :any ? @host_arch : entry.arch
|
|
55
80
|
vars = {
|
|
56
81
|
name: manifest.name,
|
|
@@ -63,7 +88,8 @@ module Crossbuild
|
|
|
63
88
|
|
|
64
89
|
Runner.new(root: @root, vars: vars, env: entry.env).run(entry.steps) unless entry.steps.empty?
|
|
65
90
|
|
|
66
|
-
dirs = Distributor.new(root: @root, output_base: @output_base)
|
|
91
|
+
dirs = Distributor.new(root: @root, output_base: @output_base)
|
|
92
|
+
.distribute(entry, host_arch: @host_arch, version: version, only: only)
|
|
67
93
|
return if dirs.empty?
|
|
68
94
|
|
|
69
95
|
dirs.each { |d| puts "🌳 #{d.sub("#{@root}/", '')}" }
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crossbuild
|
|
4
|
+
# Verifies and installs the build dependencies declared in the deps:
|
|
5
|
+
# section of build.yaml. Host selectors (distro id, ID_LIKE tokens, os,
|
|
6
|
+
# "*" — see Platform.selectors) are matched against each dependency's
|
|
7
|
+
# hosts: rules; the first hit wins. Every rule carries its own commands:
|
|
8
|
+
# `verify` decides presence (exit 0 = installed; default: PATH lookup of
|
|
9
|
+
# the dependency name), `install` says how to install it on that host.
|
|
10
|
+
class DepInstaller
|
|
11
|
+
class DepError < Error; end
|
|
12
|
+
|
|
13
|
+
# Outcome of one dependency: resolved selector + install command, and
|
|
14
|
+
# whether the dependency is present after ensure_all/check_all ran.
|
|
15
|
+
Status = Struct.new(:dep, :selector, :command, :installed, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
attr_reader :statuses
|
|
18
|
+
|
|
19
|
+
def initialize(manifest, root:, selectors: Platform.selectors, os: Platform.os)
|
|
20
|
+
@manifest = manifest
|
|
21
|
+
@root = root
|
|
22
|
+
@selectors = selectors
|
|
23
|
+
@os = os
|
|
24
|
+
@runner = Runner.new(root: root, vars: {
|
|
25
|
+
name: manifest.name,
|
|
26
|
+
version: manifest.version,
|
|
27
|
+
os: os.to_s,
|
|
28
|
+
arch: Platform.display_arch(Platform.arch),
|
|
29
|
+
platform: Platform.to_s,
|
|
30
|
+
id: Platform.to_s
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Verify every dependency; install the missing ones via their host rule
|
|
35
|
+
# and re-verify. Raises DepError when something stays missing.
|
|
36
|
+
def ensure_all
|
|
37
|
+
@statuses = @manifest.deps.map { |dep| ensure_dep(dep) }
|
|
38
|
+
@statuses
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Report-only pass for `crosspack deps <target> --check`: never installs.
|
|
42
|
+
def check_all
|
|
43
|
+
@statuses = @manifest.deps.map do |dep|
|
|
44
|
+
selector, rule = resolve(dep)
|
|
45
|
+
Status.new(dep: dep.name, selector: selector, command: display_command(rule),
|
|
46
|
+
installed: present?(dep, rule))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# One line per dependency for the CLI doctor output.
|
|
51
|
+
def report
|
|
52
|
+
return ' (no deps declared)' if @manifest.deps.empty?
|
|
53
|
+
|
|
54
|
+
(@statuses || check_all).map do |s|
|
|
55
|
+
format(' %-20s %-8s %-8s %s',
|
|
56
|
+
s.dep, s.installed ? 'ok' : 'MISSING', s.selector, s.command)
|
|
57
|
+
end.join("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def ensure_dep(dep)
|
|
63
|
+
selector, rule = resolve(dep)
|
|
64
|
+
if present?(dep, rule)
|
|
65
|
+
return Status.new(dep: dep.name, selector: selector,
|
|
66
|
+
command: display_command(rule), installed: true)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
install(dep, selector, rule)
|
|
70
|
+
unless present?(dep, rule)
|
|
71
|
+
detail = rule['verify'] ? "the verify `#{rule['verify']}` still fails" : "#{dep.name} is still not on PATH"
|
|
72
|
+
raise DepError,
|
|
73
|
+
"#{dep.name}: #{detail} after `#{rule['install']}`. " \
|
|
74
|
+
'Fix the install command or the verify probe.'
|
|
75
|
+
end
|
|
76
|
+
Status.new(dep: dep.name, selector: selector, command: display_command(rule), installed: true)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve(dep)
|
|
80
|
+
@selectors.each do |selector|
|
|
81
|
+
return [selector, dep.hosts[selector]] if dep.hosts.key?(selector)
|
|
82
|
+
end
|
|
83
|
+
raise DepError,
|
|
84
|
+
"#{dep.name}: no rule for this host (selectors tried: #{@selectors.join(', ')}; " \
|
|
85
|
+
"described: #{dep.hosts.keys.join(', ')}) — add one of those or a \"*\" wildcard to deps.#{dep.name}.hosts"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def present?(dep, rule)
|
|
89
|
+
verify = rule['verify']
|
|
90
|
+
return on_path?(dep.name) if verify.nil?
|
|
91
|
+
|
|
92
|
+
system(@runner.interpolate(verify), chdir: @root, out: File::NULL, err: File::NULL)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def install(dep, selector, rule)
|
|
96
|
+
command = rule['install']
|
|
97
|
+
if command.nil?
|
|
98
|
+
raise DepError,
|
|
99
|
+
"#{dep.name}: verification failed on #{selector} and the rule has no install command — " \
|
|
100
|
+
"add deps.#{dep.name}.hosts.#{selector}.install"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
command = @runner.interpolate(command)
|
|
104
|
+
puts "▶ #{command} (#{dep.name} @ #{selector})"
|
|
105
|
+
return if system(command, chdir: @root)
|
|
106
|
+
|
|
107
|
+
status = $?.nil? ? 'unknown status' : "exit #{$?.exitstatus || $?.to_i}"
|
|
108
|
+
raise DepError, "#{dep.name}: install command failed (#{status}): #{command}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def display_command(rule)
|
|
112
|
+
rule['install'] || "(verify only: #{rule['verify']})"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Pure-Ruby PATH probe (portable: no `command -v` / `where` shell dance;
|
|
116
|
+
# `system` skips the shell for metacharacter-free strings, where shell
|
|
117
|
+
# builtins do not resolve). On Windows PATHEXT extensions are honored.
|
|
118
|
+
def on_path?(name)
|
|
119
|
+
exts = @os == :windows ? ENV['PATHEXT'].to_s.split(File::PATH_SEPARATOR) : ['']
|
|
120
|
+
ENV['PATH'].to_s.split(File::PATH_SEPARATOR).any? do |dir|
|
|
121
|
+
base = File.join(dir.strip.empty? ? '.' : dir, name)
|
|
122
|
+
exts.any? { |ext| File.executable?("#{base}#{ext}") }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -8,7 +8,12 @@ module Crossbuild
|
|
|
8
8
|
# crosspacks/ tree crosspack writes, so crosspack can pack straight from it.
|
|
9
9
|
# Items are symlinked by default (packers copy through symlinks); copy mode
|
|
10
10
|
# is available for trees that get archived or shipped as-is.
|
|
11
|
+
#
|
|
12
|
+
# The build version is stamped into .crosspack-build in every target
|
|
13
|
+
# directory, so a later `crosspack pack <target>` needs no --version.
|
|
11
14
|
class Distributor
|
|
15
|
+
STAMP_NAME = Crosspack::Builds::STAMP_NAME
|
|
16
|
+
|
|
12
17
|
attr_reader :placed_dirs
|
|
13
18
|
|
|
14
19
|
def initialize(root:, output_base:)
|
|
@@ -18,7 +23,9 @@ module Crossbuild
|
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
# Returns the list of builds/ directories that received artifacts.
|
|
21
|
-
|
|
26
|
+
# only: distribute to just this raw target string (from artifacts.to)
|
|
27
|
+
# instead of all of them — used by `crosspack build <target>`.
|
|
28
|
+
def distribute(entry, host_arch:, version: nil, only: nil)
|
|
22
29
|
spec = entry.artifacts
|
|
23
30
|
return [] unless spec
|
|
24
31
|
|
|
@@ -30,11 +37,17 @@ module Crossbuild
|
|
|
30
37
|
items = select_items(from_dir, spec.include)
|
|
31
38
|
arch = entry.arch == :any ? host_arch : entry.arch
|
|
32
39
|
|
|
33
|
-
spec.to.
|
|
40
|
+
targets = only ? spec.to.select { |raw| raw == only } : spec.to
|
|
41
|
+
if targets.empty?
|
|
42
|
+
raise Error, "#{entry.id} does not distribute to #{only.inspect} (its targets: #{spec.to.join(', ')})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
targets.each do |raw|
|
|
34
46
|
target = Crosspack::Target.parse(raw, arch: arch)
|
|
35
47
|
dst_dir = target.output_dir(File.expand_path(@output_base, @root))
|
|
36
48
|
FileUtils.mkdir_p(dst_dir)
|
|
37
49
|
items.each { |item| place(item, File.join(dst_dir, File.basename(item)), spec.mode) }
|
|
50
|
+
File.write(File.join(dst_dir, STAMP_NAME), "#{version}\n") if version
|
|
38
51
|
@placed_dirs << dst_dir
|
|
39
52
|
end
|
|
40
53
|
@placed_dirs.uniq
|
data/lib/crossbuild/matrix.rb
CHANGED
|
@@ -4,10 +4,12 @@ module Crossbuild
|
|
|
4
4
|
# Human-readable build matrix: which entries exist, which can run on this
|
|
5
5
|
# host, what they run and where their artifacts land.
|
|
6
6
|
class Matrix
|
|
7
|
-
def initialize(manifest, host_os: Platform.os, host_arch: Platform.arch
|
|
7
|
+
def initialize(manifest, host_os: Platform.os, host_arch: Platform.arch,
|
|
8
|
+
selectors: Platform.selectors)
|
|
8
9
|
@manifest = manifest
|
|
9
10
|
@host_os = host_os
|
|
10
11
|
@host_arch = host_arch
|
|
12
|
+
@selectors = selectors
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def render
|
|
@@ -20,6 +22,7 @@ module Crossbuild
|
|
|
20
22
|
else
|
|
21
23
|
"\n#{buildable.size} of #{@manifest.entries.size} entries build here: #{buildable.map(&:id).join(', ')}"
|
|
22
24
|
end
|
|
25
|
+
lines << render_deps
|
|
23
26
|
lines.join("\n")
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -48,5 +51,29 @@ module Crossbuild
|
|
|
48
51
|
to = entry.artifacts.to.join(', ')
|
|
49
52
|
"#{from} -> #{to}"
|
|
50
53
|
end
|
|
54
|
+
|
|
55
|
+
# How each deps: entry resolves on this host's selector list.
|
|
56
|
+
def render_deps
|
|
57
|
+
return '' if @manifest.deps.empty?
|
|
58
|
+
|
|
59
|
+
lines = ["\ndeps on this host (#{join_selectors}):"]
|
|
60
|
+
@manifest.deps.each do |dep|
|
|
61
|
+
selector = @selectors.find { |s| dep.hosts.key?(s) }
|
|
62
|
+
lines << if selector
|
|
63
|
+
" ⚙ #{dep.name.ljust(18)} #{selector} -> #{rule_note(dep.hosts[selector])}"
|
|
64
|
+
else
|
|
65
|
+
" ✗ #{dep.name.ljust(18)} no rule for this host (described: #{dep.hosts.keys.join(', ')})"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
lines.join("\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def rule_note(rule)
|
|
72
|
+
rule['install'] || "(verify only: #{rule['verify']})"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def join_selectors
|
|
76
|
+
@selectors.join(' > ')
|
|
77
|
+
end
|
|
51
78
|
end
|
|
52
79
|
end
|
data/lib/crossbuild/platform.rb
CHANGED
|
@@ -36,5 +36,38 @@ module Crossbuild
|
|
|
36
36
|
def self.display_arch(arch)
|
|
37
37
|
arch == :x86_64 ? 'amd64' : arch.to_s
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
# Distro id from /etc/os-release ("ubuntu", "fedora", ...), nil off Linux.
|
|
41
|
+
def self.distro
|
|
42
|
+
return nil unless os == :linux
|
|
43
|
+
|
|
44
|
+
facts = os_release
|
|
45
|
+
facts['ID']
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Ordered host selectors for the deps: matrix of build.yaml — distro id,
|
|
49
|
+
# then ID_LIKE tokens (Linux only), then the os, then "*" as the final
|
|
50
|
+
# wildcard. The first selector with a matching rule wins.
|
|
51
|
+
def self.selectors
|
|
52
|
+
sel = []
|
|
53
|
+
if os == :linux
|
|
54
|
+
facts = os_release
|
|
55
|
+
sel << facts['ID'] if facts['ID']
|
|
56
|
+
sel.concat(facts['ID_LIKE'].to_s.split)
|
|
57
|
+
end
|
|
58
|
+
sel << os.to_s << '*'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Parses /etc/os-release into a Hash (ID, ID_LIKE, ...); {} when absent.
|
|
62
|
+
def self.os_release(path: '/etc/os-release')
|
|
63
|
+
File.read(path).each_line.with_object({}) do |line, facts|
|
|
64
|
+
key, value = line.split('=', 2)
|
|
65
|
+
next if value.nil?
|
|
66
|
+
|
|
67
|
+
facts[key.strip] = value.strip.delete_prefix('"').delete_suffix('"')
|
|
68
|
+
end
|
|
69
|
+
rescue Errno::ENOENT
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
39
72
|
end
|
|
40
73
|
end
|
data/lib/crossbuild/version.rb
CHANGED
data/lib/crossbuild.rb
CHANGED
|
@@ -11,17 +11,20 @@ require_relative 'crossbuild/platform'
|
|
|
11
11
|
require_relative 'crossbuild/version_scheme'
|
|
12
12
|
require_relative 'crossbuild/build_manifest'
|
|
13
13
|
require_relative 'crossbuild/runner'
|
|
14
|
+
require_relative 'crossbuild/dep_installer'
|
|
14
15
|
require_relative 'crossbuild/distributor'
|
|
15
16
|
require_relative 'crossbuild/matrix'
|
|
16
17
|
require_relative 'crossbuild/builder'
|
|
17
18
|
|
|
18
19
|
module Crossbuild
|
|
19
20
|
# Convenience wrapper: Crossbuild.build('build.yaml') runs every
|
|
20
|
-
# host-buildable matrix entry and returns Builder::Result.
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
24
|
+
def self.build(manifest, entry_id: nil, target: nil, root: Dir.pwd, output_base: nil, version: nil,
|
|
25
|
+
deps: true, host_os: Platform.os, host_arch: Platform.arch)
|
|
23
26
|
m = manifest.is_a?(BuildManifest) ? manifest : BuildManifest.load(manifest)
|
|
24
|
-
Builder.new(m, root: root, output_base: output_base, version: version,
|
|
25
|
-
host_os: host_os, host_arch: host_arch).run(entry_id: entry_id)
|
|
27
|
+
Builder.new(m, root: root, output_base: output_base, version: version, deps: deps,
|
|
28
|
+
host_os: host_os, host_arch: host_arch).run(entry_id: entry_id, target: target)
|
|
26
29
|
end
|
|
27
30
|
end
|
data/lib/crosspack/builds.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Crosspack
|
|
|
8
8
|
module Builds
|
|
9
9
|
ARCH_DIR_RE = /\A(amd64|x86_64|x64|arm64|aarch64)\z/i.freeze
|
|
10
10
|
|
|
11
|
+
# Written by `crosspack build <target>` into every builds/<target>/
|
|
12
|
+
# directory; `crosspack pack <target>` reads it as the default version.
|
|
13
|
+
STAMP_NAME = '.crosspack-build'
|
|
14
|
+
|
|
11
15
|
# Returns [Target] for every target directory found under root.
|
|
12
16
|
def self.available(root)
|
|
13
17
|
return [] unless File.directory?(root)
|
|
@@ -30,6 +34,14 @@ module Crosspack
|
|
|
30
34
|
lines.join("\n")
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
# The version stamped by `crosspack build <target>` into the target's
|
|
38
|
+
# builds/ directory, or nil when the directory has no stamp (packed by
|
|
39
|
+
# an older build or never built through the stage pipeline).
|
|
40
|
+
def self.version_for(target, root)
|
|
41
|
+
stamp = File.join(target.output_dir(root.to_s, target.format), STAMP_NAME)
|
|
42
|
+
File.exist?(stamp) ? File.read(stamp).strip : nil
|
|
43
|
+
end
|
|
44
|
+
|
|
33
45
|
private
|
|
34
46
|
|
|
35
47
|
def self.scan_family(dir, family)
|
data/lib/crosspack/manifest.rb
CHANGED
|
@@ -33,9 +33,15 @@ module Crosspack
|
|
|
33
33
|
|
|
34
34
|
# Parses and validates deps.yaml. Root keys are canonical dependency
|
|
35
35
|
# names; each maps to { targets: { family: { "version": [pkgs] } } } or a
|
|
36
|
-
# scalar verdict for a whole family.
|
|
36
|
+
# scalar verdict for a whole family. A reserved top-level `version:` key
|
|
37
|
+
# declares the schema version (see SCHEMA_VERSION).
|
|
37
38
|
class Manifest
|
|
38
|
-
|
|
39
|
+
# The deps.yaml schema version this crosspack understands. Files
|
|
40
|
+
# declaring a newer version are rejected at load time with an upgrade
|
|
41
|
+
# hint; older (or absent) versions are accepted.
|
|
42
|
+
SCHEMA_VERSION = 1
|
|
43
|
+
|
|
44
|
+
attr_reader :path, :deps, :schema_version, :errors, :warnings
|
|
39
45
|
|
|
40
46
|
def self.load(path)
|
|
41
47
|
unless File.file?(path)
|
|
@@ -54,10 +60,12 @@ module Crosspack
|
|
|
54
60
|
|
|
55
61
|
def initialize(path, raw)
|
|
56
62
|
@path = path
|
|
57
|
-
@
|
|
58
|
-
@raw = raw
|
|
63
|
+
@schema_version = raw.is_a?(Hash) ? raw['version'] : nil
|
|
59
64
|
@errors = []
|
|
60
65
|
@warnings = []
|
|
66
|
+
check_schema_version!
|
|
67
|
+
@deps = raw.is_a?(Hash) ? raw.reject { |key, _| key == 'version' } : {}
|
|
68
|
+
@raw = raw
|
|
61
69
|
validate
|
|
62
70
|
end
|
|
63
71
|
|
|
@@ -90,6 +98,26 @@ module Crosspack
|
|
|
90
98
|
|
|
91
99
|
private
|
|
92
100
|
|
|
101
|
+
# A schema version newer than this crosspack supports is a hard, loud
|
|
102
|
+
# error at load time — every command (resolve, matrix, pack) reports it,
|
|
103
|
+
# not just validate. Malformed versions (not an integer >= 1) are
|
|
104
|
+
# ordinary schema errors.
|
|
105
|
+
def check_schema_version!
|
|
106
|
+
v = @schema_version
|
|
107
|
+
return if v.nil?
|
|
108
|
+
|
|
109
|
+
unless v.is_a?(Integer) && v >= 1
|
|
110
|
+
@errors << Issue.new('version', "schema version must be an integer >= 1, got #{v.inspect}")
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
return if v <= SCHEMA_VERSION
|
|
114
|
+
|
|
115
|
+
raise InvalidManifestError,
|
|
116
|
+
"#{@path}: deps.yaml declares schema version #{v}, " \
|
|
117
|
+
"but this crosspack (#{Crosspack::VERSION}) supports up to #{SCHEMA_VERSION}. " \
|
|
118
|
+
'Update the gem (gem update crosspack) or lower the declared version.'
|
|
119
|
+
end
|
|
120
|
+
|
|
93
121
|
def validate
|
|
94
122
|
unless @raw.is_a?(Hash) && !@raw.empty?
|
|
95
123
|
@errors << Issue.new(
|
|
@@ -98,7 +126,7 @@ module Crosspack
|
|
|
98
126
|
)
|
|
99
127
|
return
|
|
100
128
|
end
|
|
101
|
-
@raw.each { |name, body| validate_dep(name.to_s, body) }
|
|
129
|
+
@raw.each { |name, body| validate_dep(name.to_s, body) unless name == 'version' }
|
|
102
130
|
end
|
|
103
131
|
|
|
104
132
|
def validate_dep(name, body)
|
data/lib/crosspack/packer.rb
CHANGED
|
@@ -8,8 +8,10 @@ module Crosspack
|
|
|
8
8
|
# package.yaml), resolves dependencies for the target (per deps.yaml) and
|
|
9
9
|
# produces a native package under output_base/<family>/<version>/<arch>/.
|
|
10
10
|
# The builder is chosen by the target format — deb, rpm or PKGBUILD.
|
|
11
|
+
# version: when omitted, the version stamped by `crosspack build <target>`
|
|
12
|
+
# (.crosspack-build in the target's builds/ directory) is used.
|
|
11
13
|
class Packer
|
|
12
|
-
def self.pack(manifest:, deps:, target:, version
|
|
14
|
+
def self.pack(manifest:, deps:, target:, version: nil, root: Dir.pwd,
|
|
13
15
|
output_base: 'crosspacks')
|
|
14
16
|
new(manifest: manifest, deps: deps, target: target, version: version,
|
|
15
17
|
root: root, output_base: output_base).pack
|
|
@@ -19,15 +21,16 @@ module Crosspack
|
|
|
19
21
|
@pkg_path = manifest
|
|
20
22
|
@deps_path = deps
|
|
21
23
|
@target = target
|
|
22
|
-
@version = version.to_s
|
|
24
|
+
@version = version && version.to_s
|
|
23
25
|
@root = root
|
|
24
|
-
@output_base = output_base
|
|
26
|
+
@output_base = output_base || 'crosspacks'
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def pack
|
|
28
30
|
@pkg = PackageManifest.load(@pkg_path)
|
|
29
31
|
@deps = Manifest.load(@deps_path)
|
|
30
32
|
validate_both!
|
|
33
|
+
resolve_version!
|
|
31
34
|
@pkg_data = build_layout
|
|
32
35
|
|
|
33
36
|
case @target.format
|
|
@@ -49,6 +52,21 @@ module Crosspack
|
|
|
49
52
|
|
|
50
53
|
private
|
|
51
54
|
|
|
55
|
+
# --version wins; otherwise the version recorded by the build stage.
|
|
56
|
+
def resolve_version!
|
|
57
|
+
return unless @version.nil? || @version.strip.empty?
|
|
58
|
+
|
|
59
|
+
stamped = Builds.version_for(@target, File.expand_path(@pkg.sources, @root))
|
|
60
|
+
if stamped.nil? || stamped.strip.empty?
|
|
61
|
+
raise BuildError,
|
|
62
|
+
"No version for target #{@target}: no --version given and no build stamp " \
|
|
63
|
+
"(#{Builds::STAMP_NAME}) in #{@pkg.sources}. " \
|
|
64
|
+
"Run: crosspack build #{@target} — or pass --version explicitly."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@version = stamped
|
|
68
|
+
end
|
|
69
|
+
|
|
52
70
|
def validate_both!
|
|
53
71
|
errors = []
|
|
54
72
|
errors << @pkg.error_report unless @pkg.valid?
|
|
@@ -70,10 +88,10 @@ module Crosspack
|
|
|
70
88
|
hint = available.empty? ? 'no built targets at all.' :
|
|
71
89
|
"available builds: #{available.map { |t| "#{t} (#{t.output_dir(@pkg.sources, t.format)})" }.join(', ')}."
|
|
72
90
|
raise BuildError,
|
|
73
|
-
"
|
|
91
|
+
"Build stage not done for target #{@target}: expected compiled artifacts in #{src_dir}.\n" \
|
|
74
92
|
"The builds tree (#{@pkg.sources}) mirrors the output tree: builds/<family>/<version>/<arch>/. " \
|
|
75
93
|
"Currently #{hint}\n" \
|
|
76
|
-
|
|
94
|
+
"Run: crosspack build #{@target}"
|
|
77
95
|
end
|
|
78
96
|
|
|
79
97
|
lib_dst = File.join(@pkg.prefix, @pkg.lib_dir)
|
data/lib/crosspack/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crosspack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Orlov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
description: 'One command, one target vocabulary: crosspack deps <target> verifies
|
|
14
|
+
and installs the build host dependencies, crosspack build <target> runs the build.yaml
|
|
15
|
+
matrix entry and fans artifacts into builds/, crosspack pack <target> turns that
|
|
16
|
+
tree into a native package. Stages are gated — no packing before building — and
|
|
17
|
+
keyed by the same target strings throughout.'
|
|
17
18
|
email:
|
|
18
19
|
- orelcokolov@gmail.com
|
|
19
20
|
executables:
|
|
20
21
|
- crosspack
|
|
21
|
-
- crossbuild
|
|
22
22
|
extensions: []
|
|
23
23
|
extra_rdoc_files: []
|
|
24
24
|
files:
|
|
25
25
|
- CHANGELOG.md
|
|
26
26
|
- LICENSE
|
|
27
27
|
- README.md
|
|
28
|
-
- exe/crossbuild
|
|
29
28
|
- exe/crosspack
|
|
30
29
|
- lib/crossbuild.rb
|
|
31
30
|
- lib/crossbuild/build_manifest.rb
|
|
32
31
|
- lib/crossbuild/builder.rb
|
|
32
|
+
- lib/crossbuild/dep_installer.rb
|
|
33
33
|
- lib/crossbuild/distributor.rb
|
|
34
34
|
- lib/crossbuild/matrix.rb
|
|
35
35
|
- lib/crossbuild/platform.rb
|
|
@@ -74,5 +74,5 @@ requirements: []
|
|
|
74
74
|
rubygems_version: 3.5.22
|
|
75
75
|
signing_key:
|
|
76
76
|
specification_version: 4
|
|
77
|
-
summary:
|
|
77
|
+
summary: 'Staged cross-platform pipeline: deps → build → pack native packages (deb/rpm/PKGBUILD/winget/cask)'
|
|
78
78
|
test_files: []
|
data/exe/crossbuild
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'optparse'
|
|
5
|
-
|
|
6
|
-
begin
|
|
7
|
-
require 'crossbuild'
|
|
8
|
-
rescue LoadError
|
|
9
|
-
require_relative '../lib/crossbuild'
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def abort_with(message)
|
|
13
|
-
warn message
|
|
14
|
-
exit 1
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
options = { file: 'build.yaml', root: Dir.pwd, target: nil, version: nil, output_base: nil }
|
|
18
|
-
parser = OptionParser.new do |opts|
|
|
19
|
-
opts.banner = "crossbuild #{Crossbuild::VERSION} — cross-platform build orchestrator\n" \
|
|
20
|
-
"Usage: crossbuild <command> [options]\n" \
|
|
21
|
-
'Commands: validate, matrix, version, build, targets'
|
|
22
|
-
|
|
23
|
-
opts.on('-f', '--file PATH', 'path to build.yaml (default: ./build.yaml)') { |v| options[:file] = v }
|
|
24
|
-
opts.on('--target ID', 'matrix entry to build, e.g. linux/amd64 (default: every host-buildable entry)') { |v| options[:target] = v }
|
|
25
|
-
opts.on('--version VERSION', 'override the version scheme for build') { |v| options[:version] = v }
|
|
26
|
-
opts.on('--root PATH', 'project root for relative paths (default: .)') { |v| options[:root] = v }
|
|
27
|
-
opts.on('--output-base DIR', 'output directory (default: the manifest output, builds)') { |v| options[:output_base] = v }
|
|
28
|
-
opts.on('-h', '--help', 'show help') do
|
|
29
|
-
puts opts
|
|
30
|
-
exit
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
parser.parse!
|
|
34
|
-
|
|
35
|
-
command = ARGV.shift
|
|
36
|
-
abort_with 'No command given. See: crossbuild --help' if command.nil?
|
|
37
|
-
|
|
38
|
-
# Positional file path: crossbuild validate path/to/build.yaml
|
|
39
|
-
options[:file] = ARGV.shift if ARGV.first && !ARGV.first.start_with?('-')
|
|
40
|
-
|
|
41
|
-
file = File.expand_path(options[:file], options[:root])
|
|
42
|
-
begin
|
|
43
|
-
manifest = Crossbuild::BuildManifest.load(file)
|
|
44
|
-
rescue Crossbuild::InvalidManifestError => e
|
|
45
|
-
abort_with e.message
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
case command
|
|
49
|
-
when 'validate'
|
|
50
|
-
puts manifest.error_report
|
|
51
|
-
exit(manifest.valid? ? 0 : 1)
|
|
52
|
-
when 'matrix'
|
|
53
|
-
abort_with 'Schema is invalid — the matrix cannot be rendered. Run: crossbuild validate' unless manifest.valid?
|
|
54
|
-
puts Crossbuild::Matrix.new(manifest).render
|
|
55
|
-
when 'version'
|
|
56
|
-
abort_with 'Schema is invalid. Run: crossbuild validate' unless manifest.valid?
|
|
57
|
-
begin
|
|
58
|
-
puts Crossbuild::VersionScheme.new(manifest.version).compute(root: options[:root])
|
|
59
|
-
rescue Crossbuild::VersionScheme::Error => e
|
|
60
|
-
abort_with e.message
|
|
61
|
-
end
|
|
62
|
-
when 'build'
|
|
63
|
-
begin
|
|
64
|
-
result = Crossbuild::Builder.new(manifest, root: options[:root], output_base: options[:output_base],
|
|
65
|
-
version: options[:version]).run(entry_id: options[:target])
|
|
66
|
-
rescue Crossbuild::Error, Crosspack::InvalidManifestError => e
|
|
67
|
-
abort_with e.message
|
|
68
|
-
end
|
|
69
|
-
puts "\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}"
|
|
70
|
-
puts 'Pack with: crosspack pack --target <target> --version ' + result.version
|
|
71
|
-
when 'targets'
|
|
72
|
-
base = options[:output_base] || manifest.output
|
|
73
|
-
puts Crosspack::Builds.report(File.expand_path(base, options[:root]))
|
|
74
|
-
else
|
|
75
|
-
abort_with "Unknown command #{command.inspect}. Available: validate, matrix, version, build, targets"
|
|
76
|
-
end
|