crosspack 0.5.1 → 0.7.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 +65 -0
- data/README.md +71 -6
- data/exe/crosspack +89 -22
- data/lib/crossbuild/build_manifest.rb +2 -1
- data/lib/crossbuild/dep_installer.rb +3 -1
- data/lib/crosspack/colors.rb +33 -0
- data/lib/crosspack/command_manifest.rb +111 -0
- data/lib/crosspack/config.rb +11 -2
- data/lib/crosspack/launcher.rb +194 -0
- data/lib/crosspack/package_manifest.rb +123 -7
- data/lib/crosspack/packer.rb +6 -6
- data/lib/crosspack/target.rb +39 -0
- data/lib/crosspack/version.rb +1 -1
- data/lib/crosspack.rb +10 -1
- data/logo.png +0 -0
- metadata +22 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d03bbc2ce62c3f8cc511df1f4e9ade332adc838a361e2730154c4d9b94403a3
|
|
4
|
+
data.tar.gz: 8b3785216051ef0e2c926e9feb4a61d8473286fb3ba1a6b91d4ac19cf0969e88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5372d580eb8e5730c13f1fffcd08c626e272ae436dc8f0d53a9a504e398f750d25948e29e8741c138044b3a2981f2082aa73e1ca418eaa0f399988ce56cf3d94
|
|
7
|
+
data.tar.gz: 63de5f8eafe2856831605c1e667c3d2ed32a65b29c405eb8ebf854c7792616e3fa2b7370ba963cd8d78e7aa9a02edec9ed14458a61e9d4f204772856abfba6f9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,71 @@ 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.7.0] - 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Host-only commands: `crosspack run` builds for the current host (the same
|
|
12
|
+
stage `crosspack build <host target>` runs) and then launches the main
|
|
13
|
+
binary straight from `builds/` — the first `package.executables` entry,
|
|
14
|
+
not the package. Arguments after `--` are passed to the app
|
|
15
|
+
(`crosspack run -- --dev`); the app's exit code is propagated.
|
|
16
|
+
- `crosspack install` launches this host's package with its system
|
|
17
|
+
installer: the newest package in `crosspacks/<host target>/` goes to
|
|
18
|
+
`xdg-open` (deb/rpm), `msiexec /i` (MSI) or `open` (macOS dmg/.app);
|
|
19
|
+
for arch crosspack generates only a PKGBUILD, so it points at
|
|
20
|
+
`makepkg -si`.
|
|
21
|
+
- crosspack.yml `run:` / `install:` sections: per-host launch-command
|
|
22
|
+
overrides using the same host selectors as `build.deps` (distro id,
|
|
23
|
+
ID_LIKE, os, `"*"`), with the `{{path}}` placeholder (the built binary
|
|
24
|
+
for run, the package file for install) plus the usual build facts.
|
|
25
|
+
Default without a `run:` section: execute the binary directly (a macOS
|
|
26
|
+
`.app` bundle is `open`ed).
|
|
27
|
+
|
|
28
|
+
[0.7.0]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.0
|
|
29
|
+
|
|
30
|
+
## [0.6.2] - 2026-09-13
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Colored CLI output via the new `colorize` runtime dependency: success
|
|
34
|
+
messages green, errors red, validation warnings yellow (the `ok`/`MISSING`
|
|
35
|
+
statuses in the deps report included). Colors apply only to a TTY and
|
|
36
|
+
respect `NO_COLOR`, so piped and logged output stays plain.
|
|
37
|
+
- The gem now ships `logo.png` in its files.
|
|
38
|
+
|
|
39
|
+
## [0.6.1] - 2026-09-13
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- `crosspack deps` no longer dumps a Ruby backtrace when the (detected or
|
|
43
|
+
given) target has no matrix entry — it aborts with the plain message, now
|
|
44
|
+
with an actionable hint (pass a declared target or extend artifacts.to).
|
|
45
|
+
- `crosspack --version` (bare, also `-v`) prints the gem version instead of
|
|
46
|
+
demanding an argument; `--version X` keeps overriding the build/pack
|
|
47
|
+
version.
|
|
48
|
+
|
|
49
|
+
[0.6.2]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.6.2
|
|
50
|
+
[0.6.1]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.6.1
|
|
51
|
+
|
|
52
|
+
## [0.6.0] - 2026-09-13
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- Host target default: `crosspack deps` / `build` / `pack` invoked without a
|
|
56
|
+
target run for the current host — `macos`, `windows-11.0` or the distro
|
|
57
|
+
family + version from `/etc/os-release` (unknown distro ids fall back to
|
|
58
|
+
the first known `ID_LIKE` token; unrecognizable hosts ask for an explicit
|
|
59
|
+
target).
|
|
60
|
+
- Per-target payload names: `package.payload` entries may now be written as a
|
|
61
|
+
mapping `common key -> {target selector -> real file name}` instead of a
|
|
62
|
+
flat list. The key is the common name (used verbatim on every target
|
|
63
|
+
unless overridden); selectors are an exact target (`windows-11.0`), a
|
|
64
|
+
family (`windows`) or `"*"` (exact target wins, then family, then `"*"`).
|
|
65
|
+
An entry with a selector map applies only to targets it covers, so one
|
|
66
|
+
payload lists `app`/`app.exe`, `.so` libraries and Windows DLLs together —
|
|
67
|
+
each target resolves to the files its build actually produced. The flat
|
|
68
|
+
list form remains valid (same name everywhere). `executables` keeps naming
|
|
69
|
+
common keys and resolves per target at pack time.
|
|
70
|
+
|
|
71
|
+
[0.6.0]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.6.0
|
|
72
|
+
|
|
8
73
|
## [0.5.1] - 2026-09-13
|
|
9
74
|
|
|
10
75
|
### Fixed
|
data/README.md
CHANGED
|
@@ -22,14 +22,24 @@ crosspack build ubuntu-24.04
|
|
|
22
22
|
crosspack pack ubuntu-24.04
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The target may be omitted — the stage commands then run for the current
|
|
26
|
+
host: `macos`, `windows-11.0` or the distro family + version from
|
|
27
|
+
`/etc/os-release` (unknown ids fall back to the first known `ID_LIKE`
|
|
28
|
+
token):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
crosspack deps && crosspack build && crosspack pack
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## crosspack.yml — one file, three sections + two host hooks
|
|
26
35
|
|
|
27
36
|
The top level carries the shared facts — `name` (required once) and the
|
|
28
37
|
optional `version` scheme — and three sections: `build:` (what and where to
|
|
29
38
|
build), `package:` (what and how to pack) and `deps:` (runtime dependencies
|
|
30
|
-
resolved to concrete packages per distro).
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
resolved to concrete packages per distro). Two optional hooks — `run:` and
|
|
40
|
+
`install:` — override the host-only launch commands. The shared name/version
|
|
41
|
+
feed the build section, the name feeds the package section; each section
|
|
42
|
+
keeps its own schema.
|
|
33
43
|
|
|
34
44
|
```yaml
|
|
35
45
|
name: myapp
|
|
@@ -91,11 +101,27 @@ package:
|
|
|
91
101
|
lib_dir: lib/myapp # optional, default lib/<name>
|
|
92
102
|
|
|
93
103
|
payload: # files from sources -> <prefix>/<lib_dir>/
|
|
94
|
-
- myapp
|
|
104
|
+
- myapp # flat form: the same name on every target
|
|
95
105
|
- helper
|
|
96
106
|
- model.onnx
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
# Mapping form: per-target file names. The key is the common name (and the
|
|
109
|
+
# default file name); selectors are an exact target (windows-11.0), a
|
|
110
|
+
# family (windows), an os (linux — every distro family) or "*" — exact
|
|
111
|
+
# target wins, then family, then os, then "*". An entry with selectors
|
|
112
|
+
# applies only to targets it covers, so .so libraries and Windows DLLs can
|
|
113
|
+
# share one payload:
|
|
114
|
+
#
|
|
115
|
+
# payload:
|
|
116
|
+
# myapp:
|
|
117
|
+
# "*": myapp
|
|
118
|
+
# windows: myapp.exe
|
|
119
|
+
# libonnxruntime.so.1:
|
|
120
|
+
# linux: libonnxruntime.so.1
|
|
121
|
+
# onnxruntime.dll:
|
|
122
|
+
# windows: onnxruntime.dll
|
|
123
|
+
|
|
124
|
+
executables: [myapp, helper] # common keys; chmod 755, resolved per target
|
|
99
125
|
|
|
100
126
|
links: # symlinks relative to prefix
|
|
101
127
|
bin/myapp: ../lib/myapp/myapp
|
|
@@ -117,6 +143,15 @@ deps:
|
|
|
117
143
|
fedora: { "*": [webkit2gtk4.1] }
|
|
118
144
|
macos: system # OS component
|
|
119
145
|
windows: system # preinstalled (WebView2)
|
|
146
|
+
|
|
147
|
+
# host-only launch hooks (optional): same host selectors as build.deps
|
|
148
|
+
run: # `crosspack run`: how to launch the built binary
|
|
149
|
+
hosts:
|
|
150
|
+
darwin: open build/bin/MyApp.app # .app bundles must be opened, not exec'd
|
|
151
|
+
|
|
152
|
+
install: # `crosspack install`: how to launch this host's package
|
|
153
|
+
hosts:
|
|
154
|
+
ubuntu: sudo apt-get install -y "{{path}}"
|
|
120
155
|
```
|
|
121
156
|
|
|
122
157
|
Placeholders `{{version}}`, `{{name}}`, `{{platform}}`, `{{os}}`, `{{arch}}`
|
|
@@ -167,6 +202,10 @@ crosspack deps <target> [--check] # verify/install build deps (--check: r
|
|
|
167
202
|
crosspack build <target>|--all [--no-deps] [--version X]
|
|
168
203
|
crosspack pack <target> [--version X] # version defaults to the build stamp
|
|
169
204
|
|
|
205
|
+
# host-only: this machine, no target argument
|
|
206
|
+
crosspack run [-- --dev] # build for this host, then launch the main binary
|
|
207
|
+
crosspack install # launch this host's package with its installer
|
|
208
|
+
|
|
170
209
|
# inspection
|
|
171
210
|
crosspack validate # crosspack.yml: top level + all sections
|
|
172
211
|
crosspack resolve <target> # Depends line for the target
|
|
@@ -187,6 +226,32 @@ crosspacks/fedora/41/x86_64/myapp-2026.08.31-1234.x86_64.rpm (needs rpmbuild)
|
|
|
187
226
|
crosspacks/arch/x86_64/PKGBUILD
|
|
188
227
|
```
|
|
189
228
|
|
|
229
|
+
### Host-only commands: run and install
|
|
230
|
+
|
|
231
|
+
`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:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
crosspack run -- --dev
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`crosspack install` takes the newest package packed for this host in
|
|
242
|
+
`crosspacks/` and hands it to the system installer. Defaults: `xdg-open`
|
|
243
|
+
for deb/rpm (opens GNOME Software/installer), `msiexec /i` for the MSI,
|
|
244
|
+
`open` for a macOS dmg/.app; for arch crosspack only generates a PKGBUILD,
|
|
245
|
+
so it points you at `makepkg -si` instead.
|
|
246
|
+
|
|
247
|
+
Both are per-system by nature, so each accepts a `hosts:` override with the
|
|
248
|
+
same selectors as `build.deps` (distro id, `ID_LIKE`, os, `"*"` — first
|
|
249
|
+
match wins). Commands support the `{{path}}` placeholder (the binary in
|
|
250
|
+
`builds/` for run, the package file for install) plus the usual
|
|
251
|
+
`{{name}}`/`{{version}}`/`{{os}}`/`{{arch}}` facts; quote values that start
|
|
252
|
+
with `{{` (YAML flow indicators). Without a `run:` section the binary is
|
|
253
|
+
executed directly, except a macOS `.app` bundle which is `open`ed.
|
|
254
|
+
|
|
190
255
|
## Library
|
|
191
256
|
|
|
192
257
|
```ruby
|
data/exe/crosspack
CHANGED
|
@@ -12,25 +12,50 @@ rescue LoadError
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def abort_with(message)
|
|
15
|
-
warn message
|
|
15
|
+
warn Crosspack::Colors.red(message)
|
|
16
16
|
exit 1
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Validate reports line by line: valid → green, invalid → red, warnings → yellow.
|
|
20
|
+
def colorized_report(text)
|
|
21
|
+
text.lines.map do |line|
|
|
22
|
+
chomped = line.chomp
|
|
23
|
+
colored =
|
|
24
|
+
if chomped.include?('is invalid')
|
|
25
|
+
Crosspack::Colors.red(chomped)
|
|
26
|
+
elsif chomped.include?('is valid')
|
|
27
|
+
Crosspack::Colors.green(chomped)
|
|
28
|
+
elsif chomped.strip.start_with?('⚠')
|
|
29
|
+
Crosspack::Colors.yellow(chomped)
|
|
30
|
+
end
|
|
31
|
+
colored ? "#{colored}\n" : line
|
|
32
|
+
end.join
|
|
33
|
+
end
|
|
34
|
+
|
|
19
35
|
options = { file: Crosspack::Config::DEFAULT_PATH, arch: 'amd64',
|
|
20
36
|
format: nil, version: nil, root: Dir.pwd, output_base: nil,
|
|
21
37
|
check_only: false, no_deps: false, all: false }
|
|
38
|
+
|
|
39
|
+
# `crosspack --version` (also bare -v) prints the gem version like any other
|
|
40
|
+
# CLI; with a value the flag stays the build/pack version override.
|
|
41
|
+
if ARGV.size == 1 && %w[--version -v].include?(ARGV.first)
|
|
42
|
+
puts "crosspack #{Crosspack::VERSION}"
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
22
45
|
parser = OptionParser.new do |opts|
|
|
23
46
|
opts.banner = "crosspack #{Crosspack::VERSION} — staged pipeline: deps → build → pack\n" \
|
|
24
47
|
"Usage: crosspack <command> [options]\n" \
|
|
25
|
-
"Stages: deps <target> [--check] verify/install build deps
|
|
26
|
-
" build <target
|
|
27
|
-
" pack <target> [--version X] package builds/<target> (version from the build stamp)\n" \
|
|
48
|
+
"Stages: deps [<target>] [--check] verify/install build deps (default: this host)\n" \
|
|
49
|
+
" build [<target>]|--all compile into builds/ (default: this host)\n" \
|
|
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" \
|
|
52
|
+
" install launch this host's package with its installer\n" \
|
|
28
53
|
'Commands: validate, resolve, matrix, version, targets'
|
|
29
54
|
|
|
30
55
|
opts.on('-f', '--file PATH', 'path to crosspack.yml (default: ./crosspack.yml)') { |v| options[:file] = v }
|
|
31
56
|
opts.on('--arch ARCH', 'architecture: amd64/x86_64, arm64/aarch64 (default: amd64)') { |v| options[:arch] = v }
|
|
32
57
|
opts.on('--format FORMAT', 'output format for resolve: deb, rpm, pkgbuild (default: derived from the target)') { |v| options[:format] = v.to_sym }
|
|
33
|
-
opts.on('--version VERSION', 'override the version for build/pack') { |v| options[:version] = v }
|
|
58
|
+
opts.on('--version VERSION', 'override the version for build/pack; a bare `crosspack --version` prints the gem version') { |v| options[:version] = v }
|
|
34
59
|
opts.on('--root PATH', 'project root for relative paths (default: .)') { |v| options[:root] = v }
|
|
35
60
|
opts.on('--output-base DIR', 'output directory (build stage: the manifest output, default builds; pack: default ./crosspacks)') { |v| options[:output_base] = v }
|
|
36
61
|
opts.on('--check', 'with `deps`: only report presence, never install (exit 1 if missing)') { options[:check_only] = true }
|
|
@@ -79,6 +104,19 @@ def entry_for_target!(manifest, target, host_os, host_arch)
|
|
|
79
104
|
end
|
|
80
105
|
|
|
81
106
|
entry
|
|
107
|
+
rescue Crossbuild::Error => e
|
|
108
|
+
abort_with e.message
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Stage commands (deps/build/pack) run for the current host when no target
|
|
112
|
+
# is given: macos, windows-11.0 or the distro from /etc/os-release.
|
|
113
|
+
def host_target!(command)
|
|
114
|
+
raw = Crosspack::Target.host_string
|
|
115
|
+
abort_with "Could not detect this host's target — pass one explicitly: crosspack #{command} <target> " \
|
|
116
|
+
'(families: ubuntu-24.04, debian-12, fedora-41, arch, macos, windows-11.0)' if raw.nil?
|
|
117
|
+
|
|
118
|
+
puts "→ no target given; using this host's target: #{raw}"
|
|
119
|
+
raw
|
|
82
120
|
end
|
|
83
121
|
|
|
84
122
|
case command
|
|
@@ -86,11 +124,15 @@ when 'validate'
|
|
|
86
124
|
deps_manifest = config.deps_manifest
|
|
87
125
|
pkg = config.package_manifest
|
|
88
126
|
build_manifest = config.build_manifest
|
|
89
|
-
puts config.error_report
|
|
90
|
-
puts deps_manifest.error_report
|
|
91
|
-
puts pkg.error_report
|
|
92
|
-
puts build_manifest.error_report
|
|
93
|
-
|
|
127
|
+
puts colorized_report(config.error_report)
|
|
128
|
+
puts colorized_report(deps_manifest.error_report)
|
|
129
|
+
puts colorized_report(pkg.error_report)
|
|
130
|
+
puts colorized_report(build_manifest.error_report)
|
|
131
|
+
[config.run_manifest, config.install_manifest].each do |m|
|
|
132
|
+
puts colorized_report(m.error_report) if m.present?
|
|
133
|
+
end
|
|
134
|
+
ok = config.valid? && deps_manifest.valid? && pkg.valid? && build_manifest.valid? &&
|
|
135
|
+
config.run_manifest.valid? && config.install_manifest.valid?
|
|
94
136
|
exit(ok ? 0 : 1)
|
|
95
137
|
when 'matrix'
|
|
96
138
|
deps_manifest = config.deps_manifest
|
|
@@ -131,12 +173,12 @@ when 'version'
|
|
|
131
173
|
abort_with e.message
|
|
132
174
|
end
|
|
133
175
|
when 'deps'
|
|
134
|
-
|
|
176
|
+
target_str = positional || host_target!('deps')
|
|
135
177
|
build_manifest = config.build_manifest
|
|
136
178
|
abort_with 'the build: section is invalid — run: crosspack validate' unless build_manifest.valid?
|
|
137
|
-
entry_for_target!(build_manifest, positional, Crossbuild::Platform.os, Crossbuild::Platform.arch)
|
|
138
|
-
installer = Crossbuild::DepInstaller.new(build_manifest, root: options[:root])
|
|
139
179
|
begin
|
|
180
|
+
entry_for_target!(build_manifest, target_str, Crossbuild::Platform.os, Crossbuild::Platform.arch)
|
|
181
|
+
installer = Crossbuild::DepInstaller.new(build_manifest, root: options[:root])
|
|
140
182
|
if options[:check_only]
|
|
141
183
|
installer.check_all
|
|
142
184
|
puts "deps on this host:\n#{installer.report}"
|
|
@@ -146,15 +188,14 @@ when 'deps'
|
|
|
146
188
|
rescue Crossbuild::Error => e
|
|
147
189
|
abort_with e.message
|
|
148
190
|
end
|
|
149
|
-
puts "deps ready:\n#{installer.report}"
|
|
191
|
+
puts Crosspack::Colors.green("deps ready:\n#{installer.report}")
|
|
150
192
|
when 'build'
|
|
151
193
|
build_manifest = config.build_manifest
|
|
152
194
|
abort_with 'the build: section is invalid — run: crosspack validate' unless build_manifest.valid?
|
|
153
195
|
if options[:all]
|
|
154
196
|
target = nil
|
|
155
197
|
else
|
|
156
|
-
|
|
157
|
-
target = positional
|
|
198
|
+
target = positional || host_target!('build')
|
|
158
199
|
end
|
|
159
200
|
begin
|
|
160
201
|
result = Crossbuild::Builder.new(build_manifest, root: options[:root],
|
|
@@ -163,12 +204,12 @@ when 'build'
|
|
|
163
204
|
rescue Crossbuild::Error, Crosspack::InvalidManifestError => e
|
|
164
205
|
abort_with e.message
|
|
165
206
|
end
|
|
166
|
-
puts "\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}"
|
|
207
|
+
puts Crosspack::Colors.green("\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}")
|
|
167
208
|
puts 'Pack with: crosspack pack <target>'
|
|
168
209
|
when 'pack'
|
|
169
|
-
|
|
210
|
+
target_str = positional || host_target!('pack')
|
|
170
211
|
begin
|
|
171
|
-
target = Crosspack::Target.parse(
|
|
212
|
+
target = Crosspack::Target.parse(target_str, arch: options[:arch])
|
|
172
213
|
path = Crosspack.pack(
|
|
173
214
|
config: config,
|
|
174
215
|
target: target,
|
|
@@ -177,11 +218,37 @@ when 'pack'
|
|
|
177
218
|
output_base: options[:output_base] || 'crosspacks'
|
|
178
219
|
)
|
|
179
220
|
rescue Crosspack::Target::Error => e
|
|
180
|
-
abort_with "Invalid target #{
|
|
221
|
+
abort_with "Invalid target #{target_str.inspect}: #{e.message}"
|
|
181
222
|
rescue Crosspack::InvalidManifestError, Crosspack::ResolveError, Crosspack::BuildError => e
|
|
182
223
|
abort_with e.message
|
|
183
224
|
end
|
|
184
|
-
puts "📦 #{path}"
|
|
225
|
+
puts Crosspack::Colors.green("📦 #{path}")
|
|
226
|
+
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
|
|
233
|
+
args = ARGV.dup
|
|
234
|
+
args.shift if args.first == '--'
|
|
235
|
+
begin
|
|
236
|
+
status =
|
|
237
|
+
if command == 'run'
|
|
238
|
+
Crosspack::Launcher.run(
|
|
239
|
+
config, root: options[:root], args: args,
|
|
240
|
+
output_base: options[:output_base], version: options[:version], deps: !options[:no_deps]
|
|
241
|
+
)
|
|
242
|
+
else
|
|
243
|
+
Crosspack::Launcher.install(
|
|
244
|
+
config, root: options[:root], args: args,
|
|
245
|
+
output_base: options[:output_base] || 'crosspacks'
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
rescue Crosspack::InvalidManifestError, Crosspack::BuildError, Crossbuild::Error => e
|
|
249
|
+
abort_with e.message
|
|
250
|
+
end
|
|
251
|
+
exit(status) unless status.zero?
|
|
185
252
|
when 'targets'
|
|
186
253
|
# What can actually be packed: builds tree + resolvable deps per target.
|
|
187
254
|
pkg = config.package_manifest
|
|
@@ -189,5 +256,5 @@ when 'targets'
|
|
|
189
256
|
root = sources ? File.expand_path(sources, options[:root]) : File.join(options[:root], 'builds')
|
|
190
257
|
puts Crosspack::Builds.report(root)
|
|
191
258
|
else
|
|
192
|
-
abort_with "Unknown command #{command.inspect}. Available: deps, build, pack, validate, resolve, matrix, version, targets"
|
|
259
|
+
abort_with "Unknown command #{command.inspect}. Available: deps, build, pack, run, install, validate, resolve, matrix, version, targets"
|
|
193
260
|
end
|
|
@@ -143,7 +143,8 @@ module Crossbuild
|
|
|
143
143
|
if matches.empty?
|
|
144
144
|
raise Error,
|
|
145
145
|
"no matrix entry distributes to target #{raw.inspect}; " \
|
|
146
|
-
"declared targets: #{declared.join(', ')}"
|
|
146
|
+
"declared targets: #{declared.join(', ')}. Pass one of them explicitly, " \
|
|
147
|
+
"or add #{raw.inspect} to the entry's artifacts.to in crosspack.yml"
|
|
147
148
|
end
|
|
148
149
|
if matches.size > 1
|
|
149
150
|
raise Error,
|
|
@@ -53,8 +53,10 @@ module Crossbuild
|
|
|
53
53
|
return ' (no deps declared)' if @manifest.deps.empty?
|
|
54
54
|
|
|
55
55
|
(@statuses || check_all).map do |s|
|
|
56
|
+
state = format('%-8s', s.installed ? 'ok' : 'MISSING')
|
|
57
|
+
state = Crosspack::Colors.public_send(s.installed ? :green : :red, state)
|
|
56
58
|
format(' %-20s %-8s %-8s %s',
|
|
57
|
-
s.dep,
|
|
59
|
+
s.dep, state, s.selector, s.command)
|
|
58
60
|
end.join("\n")
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'colorize'
|
|
4
|
+
|
|
5
|
+
module Crosspack
|
|
6
|
+
# CLI message coloring (green = success, red = errors, yellow = warnings),
|
|
7
|
+
# powered by the colorize gem. Colors are turned off automatically when the
|
|
8
|
+
# output is not a TTY or NO_COLOR is set, so piped and logged output stays
|
|
9
|
+
# clean.
|
|
10
|
+
module Colors
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :enabled
|
|
13
|
+
|
|
14
|
+
def enabled?
|
|
15
|
+
return @enabled unless @enabled.nil?
|
|
16
|
+
|
|
17
|
+
ENV['NO_COLOR'].nil? && ($stdout.tty? || $stderr.tty?)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def red(text)
|
|
21
|
+
enabled? ? text.to_s.colorize(:red) : text
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def green(text)
|
|
25
|
+
enabled? ? text.to_s.colorize(:green) : text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def yellow(text)
|
|
29
|
+
enabled? ? text.to_s.colorize(:yellow) : text
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crosspack
|
|
4
|
+
# Parses and validates the optional run:/install: sections of crosspack.yml.
|
|
5
|
+
# Both map host selectors (the same vocabulary as the deps: rules of the
|
|
6
|
+
# build: section: distro id from os-release, ID_LIKE token, os name, "*")
|
|
7
|
+
# to a shell command with {{path}} and the usual build facts:
|
|
8
|
+
#
|
|
9
|
+
# run:
|
|
10
|
+
# hosts:
|
|
11
|
+
# darwin: open build/bin/MyApp.app
|
|
12
|
+
# "*": "{{path}} --dev"
|
|
13
|
+
#
|
|
14
|
+
# A section is optional; when absent the host-only default applies
|
|
15
|
+
# (see Launcher).
|
|
16
|
+
class CommandManifest
|
|
17
|
+
SECTION_KEYS = %w[hosts].freeze
|
|
18
|
+
# A host selector: "*" or a distro id / ID_LIKE token / os name —
|
|
19
|
+
# the same vocabulary as the deps: rules of the build: section.
|
|
20
|
+
SELECTOR_RE = /\A(\*|[a-z0-9][a-z0-9+._-]*)\z/i.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :path, :section, :errors, :warnings
|
|
23
|
+
|
|
24
|
+
def initialize(path, raw, section)
|
|
25
|
+
@path = path
|
|
26
|
+
@section = section
|
|
27
|
+
@raw = raw
|
|
28
|
+
@hosts = {}
|
|
29
|
+
@errors = []
|
|
30
|
+
@warnings = []
|
|
31
|
+
validate
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def present?
|
|
35
|
+
!@raw.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid?
|
|
39
|
+
@errors.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The command for the given host selectors (see Crossbuild::Platform
|
|
43
|
+
# .selectors), first match wins; nil when nothing applies.
|
|
44
|
+
def command_for(selectors)
|
|
45
|
+
selectors.each { |s| return @hosts[s] if @hosts.key?(s) }
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def error_report
|
|
50
|
+
return "#{@path}: the #{@section}: section is absent — defaults apply" unless present?
|
|
51
|
+
|
|
52
|
+
lines = ["#{@path}: the #{@section}: section is #{@errors.empty? ? 'valid' : "invalid (#{@errors.size} errors)"}:"]
|
|
53
|
+
@errors.each { |e| lines << " ✗ #{e}" }
|
|
54
|
+
@warnings.each { |w| lines << " ⚠ #{w}" }
|
|
55
|
+
lines.join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate!
|
|
59
|
+
return self if valid?
|
|
60
|
+
|
|
61
|
+
raise InvalidManifestError, error_report
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate
|
|
67
|
+
return unless present?
|
|
68
|
+
|
|
69
|
+
unless @raw.is_a?(Hash) && !@raw.empty?
|
|
70
|
+
@errors << Issue.new("#{@section}", "must be a non-empty mapping with the key: hosts; expected:\n" \
|
|
71
|
+
" #{@section}:\n" \
|
|
72
|
+
" hosts:\n" \
|
|
73
|
+
" ubuntu: #{@section == 'install' ? 'xdg-open {{path}}' : '{{path}} --dev'}")
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
check_unknown_keys
|
|
77
|
+
|
|
78
|
+
hosts = @raw['hosts']
|
|
79
|
+
return unless hosts
|
|
80
|
+
|
|
81
|
+
unless hosts.is_a?(Hash) && !hosts.empty?
|
|
82
|
+
@errors << Issue.new("#{@section}.hosts", 'must be a non-empty mapping of host selector -> shell command')
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
hosts.each do |selector, command|
|
|
87
|
+
validate_rule(selector.to_s, command)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check_unknown_keys
|
|
92
|
+
@raw.keys.map(&:to_s).each do |key|
|
|
93
|
+
next if SECTION_KEYS.include?(key)
|
|
94
|
+
|
|
95
|
+
@errors << Issue.new("#{@section}.#{key}", "unknown key; allowed: #{SECTION_KEYS.join(', ')}")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_rule(selector, command)
|
|
100
|
+
path = "#{@section}.hosts.#{selector}"
|
|
101
|
+
unless selector =~ SELECTOR_RE
|
|
102
|
+
@errors << Issue.new(path, "invalid host selector (expected a distro id, os name or \"*\")")
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
unless command.is_a?(String) && !command.strip.empty?
|
|
106
|
+
@errors << Issue.new(path, 'must be a non-empty shell command')
|
|
107
|
+
end
|
|
108
|
+
@hosts[selector] = command if command.is_a?(String) && !command.strip.empty?
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/crosspack/config.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Crosspack
|
|
|
15
15
|
# happens per section, not per file.
|
|
16
16
|
class Config
|
|
17
17
|
DEFAULT_PATH = 'crosspack.yml'
|
|
18
|
-
TOP_LEVEL_KEYS = %w[name version build package deps].freeze
|
|
18
|
+
TOP_LEVEL_KEYS = %w[name version build package deps run install].freeze
|
|
19
19
|
NAME_RE = /\A[a-z0-9][a-z0-9+._-]*\z/i.freeze
|
|
20
20
|
|
|
21
21
|
attr_reader :path, :errors, :warnings
|
|
@@ -81,6 +81,15 @@ module Crosspack
|
|
|
81
81
|
@deps_manifest ||= Manifest.new(@path, deps_section)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# The optional run:/install: sections (host-only launch commands).
|
|
85
|
+
def run_manifest
|
|
86
|
+
@run_manifest ||= CommandManifest.new(@path, @raw['run'], 'run')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def install_manifest
|
|
90
|
+
@install_manifest ||= CommandManifest.new(@path, @raw['install'], 'install')
|
|
91
|
+
end
|
|
92
|
+
|
|
84
93
|
private
|
|
85
94
|
|
|
86
95
|
def validate
|
|
@@ -124,7 +133,7 @@ module Crosspack
|
|
|
124
133
|
end
|
|
125
134
|
|
|
126
135
|
def check_section_types
|
|
127
|
-
%w[build package deps].each do |key|
|
|
136
|
+
%w[build package deps run install].each do |key|
|
|
128
137
|
next unless @raw.key?(key) && !@raw[key].is_a?(Hash)
|
|
129
138
|
|
|
130
139
|
@errors << Issue.new(key, 'must be a mapping')
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crosspack
|
|
4
|
+
# Host-only launch commands: `crosspack run` (build the host target, then
|
|
5
|
+
# launch the application's main binary) and `crosspack install` (launch the
|
|
6
|
+
# package packed for this host). Both resolve the current host target, so
|
|
7
|
+
# they take no target argument and are meaningless on any other system.
|
|
8
|
+
#
|
|
9
|
+
# Commands may be overridden per host in crosspack.yml (run:/install:
|
|
10
|
+
# sections, selectors like the deps: rules of build:); placeholders:
|
|
11
|
+
# {{path}} plus the usual facts ({{name}}, {{version}}, {{os}}, {{arch}}).
|
|
12
|
+
# Defaults when nothing is configured:
|
|
13
|
+
# run: darwin + .app bundle -> `open {{path}}`, otherwise {{path}}
|
|
14
|
+
# install: deb/rpm -> xdg-open, msi -> msiexec /i, dmg/.app -> open
|
|
15
|
+
class Launcher
|
|
16
|
+
class Error < BuildError; end
|
|
17
|
+
|
|
18
|
+
# Builds the host target first (the same stage `crosspack build` runs),
|
|
19
|
+
# then launches the first package.executables binary from builds/.
|
|
20
|
+
# Returns the launched process's exit code.
|
|
21
|
+
def self.run(config, root: Dir.pwd, args: [], output_base: nil, version: nil,
|
|
22
|
+
deps: true, host_string: nil, host_os: Crossbuild::Platform.os,
|
|
23
|
+
host_arch: Crossbuild::Platform.arch, selectors: Crossbuild::Platform.selectors)
|
|
24
|
+
config = Config.coerce(config, root: root)
|
|
25
|
+
target = host_target('run', host_string, host_arch)
|
|
26
|
+
pkg = validated_package(config)
|
|
27
|
+
build_manifest = config.build_manifest
|
|
28
|
+
build_manifest.validate!
|
|
29
|
+
|
|
30
|
+
Crossbuild::Builder.new(build_manifest, root: root, output_base: output_base,
|
|
31
|
+
version: version, deps: deps,
|
|
32
|
+
host_os: host_os, host_arch: host_arch)
|
|
33
|
+
.run(target: target.to_s)
|
|
34
|
+
|
|
35
|
+
exe = pkg.executables_for(target).first
|
|
36
|
+
if exe.nil?
|
|
37
|
+
raise Error,
|
|
38
|
+
'run: the package: section does not name the binary to launch — ' \
|
|
39
|
+
"add executables: [#{pkg.name}] to it (the first entry is what `crosspack run` launches)"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
path = File.join(File.expand_path(target.output_dir(pkg.sources, target.format), root), exe)
|
|
43
|
+
unless File.exist?(path)
|
|
44
|
+
raise Error,
|
|
45
|
+
"run: expected the built binary at #{path}, but it is not there — " \
|
|
46
|
+
'does the build step really produce it? Check package.executables against the build output'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
command = config.run_manifest.command_for(selectors) || default_run_command(path, host_os)
|
|
50
|
+
execute(command, path: path, pkg: pkg, target: target, root: root, args: args)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Finds the package packed for this host in crosspacks/ and launches it
|
|
54
|
+
# with the system installer. Returns the launched process's exit code.
|
|
55
|
+
def self.install(config, root: Dir.pwd, args: [], output_base: 'crosspacks',
|
|
56
|
+
host_string: nil, host_os: Crossbuild::Platform.os,
|
|
57
|
+
host_arch: Crossbuild::Platform.arch, selectors: Crossbuild::Platform.selectors)
|
|
58
|
+
config = Config.coerce(config, root: root)
|
|
59
|
+
target = host_target('install', host_string, host_arch)
|
|
60
|
+
pkg = validated_package(config)
|
|
61
|
+
|
|
62
|
+
command = config.install_manifest.command_for(selectors)
|
|
63
|
+
raise Error, pkgbuild_hint(target, root, output_base) if command.nil? && target.format == :pkgbuild
|
|
64
|
+
|
|
65
|
+
# {{path}} resolves to the package file (for arch: the PKGBUILD dir).
|
|
66
|
+
path = target.format == :pkgbuild ? packed_dir(target, root, output_base)
|
|
67
|
+
: package_file(target, pkg, root, output_base)
|
|
68
|
+
command ||= default_install_command(target.format)
|
|
69
|
+
|
|
70
|
+
execute(command, path: path, pkg: pkg, target: target, root: root, args: args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# -- defaults ------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
# A macOS .app bundle must be opened, not exec'd; plain binaries and
|
|
76
|
+
# Windows exes run directly.
|
|
77
|
+
def self.default_run_command(path, host_os)
|
|
78
|
+
host_os == :darwin && File.directory?(path) ? 'open {{path}}' : '{{path}}'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.default_install_command(format)
|
|
82
|
+
case format
|
|
83
|
+
when :deb, :rpm then 'xdg-open {{path}}'
|
|
84
|
+
when :winget then 'msiexec /i {{path}}'
|
|
85
|
+
when :cask then 'open {{path}}'
|
|
86
|
+
else
|
|
87
|
+
raise Error, "install: no default installer command for format #{format.inspect} — " \
|
|
88
|
+
'add an install: section with hosts: rules to crosspack.yml'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# -- internals -----------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
class << self
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# "this host's target" — the same detection the stage commands use.
|
|
98
|
+
def host_target(command, host_string, host_arch)
|
|
99
|
+
raw = host_string || Target.host_string
|
|
100
|
+
if raw.nil?
|
|
101
|
+
raise Error,
|
|
102
|
+
"Could not detect this host's target — crosspack #{command} runs only on this host " \
|
|
103
|
+
'(families: ubuntu-24.04, debian-12, fedora-41, arch, macos, windows-11.0)'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Target.parse(raw, arch: host_arch)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validated_package(config)
|
|
110
|
+
pkg = config.package_manifest
|
|
111
|
+
pkg.validate!
|
|
112
|
+
pkg
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def packed_dir(target, root, output_base)
|
|
116
|
+
File.expand_path(target.output_dir(output_base, target.format), root)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The packed tree must exist before any installer runs — even with a
|
|
120
|
+
# custom command (its {{path}} resolves into this directory).
|
|
121
|
+
def check_packed_dir(target, root, output_base)
|
|
122
|
+
dir = packed_dir(target, root, output_base)
|
|
123
|
+
return if File.directory?(dir)
|
|
124
|
+
|
|
125
|
+
raise Error,
|
|
126
|
+
"Pack stage not done for target #{target}: expected a packed package in #{dir}.\n" \
|
|
127
|
+
"Run: crosspack pack #{target}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The single package file this host's installer should receive.
|
|
131
|
+
def package_file(target, pkg, root, output_base)
|
|
132
|
+
check_packed_dir(target, root, output_base)
|
|
133
|
+
dir = packed_dir(target, root, output_base)
|
|
134
|
+
file =
|
|
135
|
+
case target.format
|
|
136
|
+
when :deb then newest(dir, '*.deb')
|
|
137
|
+
when :rpm then newest(dir, '*.rpm')
|
|
138
|
+
when :winget then newest(dir, '*.msi')
|
|
139
|
+
when :cask then newest(dir, '*.dmg') || app_bundle(dir, pkg.name)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if file.nil?
|
|
143
|
+
raise winget_sources_hint(dir) if target.format == :winget && Dir[File.join(dir, '*.wxs')].any?
|
|
144
|
+
|
|
145
|
+
raise Error,
|
|
146
|
+
"No package for target #{target} in #{dir} — the pack stage produced nothing this " \
|
|
147
|
+
"command recognizes.\nRun: crosspack pack #{target}"
|
|
148
|
+
end
|
|
149
|
+
file
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def newest(dir, pattern)
|
|
153
|
+
Dir[File.join(dir, pattern)].max_by { |f| File.mtime(f) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def app_bundle(dir, name)
|
|
157
|
+
app = File.join(dir, "#{name}.app")
|
|
158
|
+
File.directory?(app) ? app : nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def winget_sources_hint(dir)
|
|
162
|
+
'install: the pack stage produced only WiX sources (.wxs) — build the MSI first, ' \
|
|
163
|
+
"see #{File.join(dir, 'BUILD-MSI.txt')}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def pkgbuild_hint(target, root, output_base)
|
|
167
|
+
"install: packing for #{target} generates a PKGBUILD, not a built package — " \
|
|
168
|
+
"install it yourself:\n cd #{packed_dir(target, root, output_base)} && makepkg -si\n" \
|
|
169
|
+
'or add an install: section with hosts: rules to crosspack.yml'
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Expands the {{...}} placeholders, appends extra CLI args and runs the
|
|
173
|
+
# command from the project root, passing the child's exit code through.
|
|
174
|
+
def execute(command, path:, pkg:, target:, root:, args:)
|
|
175
|
+
vars = {
|
|
176
|
+
name: pkg.name,
|
|
177
|
+
version: Builds.version_for(target, File.expand_path(pkg.sources, root)).to_s,
|
|
178
|
+
os: target.family.to_s,
|
|
179
|
+
arch: Crossbuild::Platform.display_arch(target.arch == :any ? :x86_64 : target.arch),
|
|
180
|
+
platform: target.to_s,
|
|
181
|
+
path: path
|
|
182
|
+
}
|
|
183
|
+
command = Crossbuild::Runner.new(root: root, vars: vars).interpolate(command)
|
|
184
|
+
command = "#{command} #{args.join(' ')}" unless args.empty?
|
|
185
|
+
|
|
186
|
+
puts "▶ #{command}"
|
|
187
|
+
result = system(command, chdir: root)
|
|
188
|
+
raise Error, "launch failed (command not found): #{command}" if result.nil?
|
|
189
|
+
|
|
190
|
+
$?.exitstatus || 0
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -5,10 +5,25 @@ module Crosspack
|
|
|
5
5
|
# WHAT and HOW to pack — payload, executables, symlinks, desktop entry,
|
|
6
6
|
# icon. Compilation is not crosspack's business; artifacts are picked from
|
|
7
7
|
# `sources` as-is.
|
|
8
|
+
#
|
|
9
|
+
# Payload entries may carry per-target file names: the key is the common
|
|
10
|
+
# name (used verbatim everywhere unless a selector map says otherwise),
|
|
11
|
+
# and each selector (exact target "windows-11.0", family "windows" or "*")
|
|
12
|
+
# maps to the real file name that target's build produces. An entry with a
|
|
13
|
+
# selector map applies only to targets it covers — a Windows build dir has
|
|
14
|
+
# no .so libraries and vice versa.
|
|
8
15
|
class PackageManifest
|
|
9
16
|
REQUIRED = %w[maintainer description sources prefix payload].freeze
|
|
10
17
|
OPTIONAL = %w[name summary license section lib_dir executables links desktop icon].freeze
|
|
11
18
|
|
|
19
|
+
# Payload selectors may name an os in addition to a family: every distro
|
|
20
|
+
# family packs a linux target, so "linux:" covers deb/rpm/PKGBUILD
|
|
21
|
+
# without enumerating them (macos and windows are their own families).
|
|
22
|
+
OS_BY_FAMILY = Hash.new('linux').merge(macos: 'macos', windows: 'windows').freeze
|
|
23
|
+
|
|
24
|
+
# One payload file: its common key plus the per-target name overrides.
|
|
25
|
+
PayloadFile = Struct.new(:key, :names, keyword_init: true)
|
|
26
|
+
|
|
12
27
|
attr_reader :path, :data, :errors, :warnings
|
|
13
28
|
|
|
14
29
|
def initialize(path, raw)
|
|
@@ -17,6 +32,7 @@ module Crosspack
|
|
|
17
32
|
@raw = raw
|
|
18
33
|
@errors = []
|
|
19
34
|
@warnings = []
|
|
35
|
+
@payload_entries = []
|
|
20
36
|
validate
|
|
21
37
|
end
|
|
22
38
|
|
|
@@ -72,14 +88,40 @@ module Crosspack
|
|
|
72
88
|
@data['prefix']
|
|
73
89
|
end
|
|
74
90
|
|
|
91
|
+
# Common keys of the payload entries (the executables list and links
|
|
92
|
+
# reference these); use payload_for(target) for the real file names.
|
|
75
93
|
def payload
|
|
76
|
-
|
|
94
|
+
@payload_entries.map(&:key)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Real file names the given target's package carries. Every entry
|
|
98
|
+
# resolves through its selector map — exact target string, then family,
|
|
99
|
+
# then "*"; entries without a map use their key on every target; entries
|
|
100
|
+
# whose selectors do not cover the target are skipped.
|
|
101
|
+
def payload_for(target)
|
|
102
|
+
@payload_entries.filter_map { |entry| resolve_entry(entry, target) }
|
|
77
103
|
end
|
|
78
104
|
|
|
79
105
|
def executables
|
|
80
106
|
Array(@data['executables'])
|
|
81
107
|
end
|
|
82
108
|
|
|
109
|
+
# Executable file names for the target. The executables list names
|
|
110
|
+
# common payload keys; they resolve to the target's real file names.
|
|
111
|
+
def executables_for(target)
|
|
112
|
+
executables.map do |key|
|
|
113
|
+
entry = @payload_entries.find { |e| e.key == key }
|
|
114
|
+
resolved = entry && resolve_entry(entry, target)
|
|
115
|
+
if resolved.nil?
|
|
116
|
+
raise InvalidManifestError,
|
|
117
|
+
"executables: #{key.inspect} resolves to no file for target #{target} — " \
|
|
118
|
+
'its payload entry has no name for that target'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
resolved
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
83
125
|
def summary
|
|
84
126
|
@data['summary'] || description.lines.map(&:chomp).reject(&:empty?).first || name
|
|
85
127
|
end
|
|
@@ -153,17 +195,91 @@ module Crosspack
|
|
|
153
195
|
value = @data['payload']
|
|
154
196
|
return if value.nil? # already reported as missing
|
|
155
197
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
198
|
+
@payload_entries =
|
|
199
|
+
if value.is_a?(Array)
|
|
200
|
+
validate_payload_list(value)
|
|
201
|
+
elsif value.is_a?(Hash)
|
|
202
|
+
validate_payload_map(value)
|
|
203
|
+
else
|
|
204
|
+
@errors << Issue.new('payload', 'must be a non-empty list of artifact files or a mapping: key -> {target -> real file name}')
|
|
205
|
+
[]
|
|
206
|
+
end
|
|
207
|
+
return unless @payload_entries.empty?
|
|
208
|
+
|
|
209
|
+
@errors << Issue.new('payload', 'must name at least one artifact file from sources')
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Legacy/simple form: a flat list — every name applies to every target.
|
|
213
|
+
def validate_payload_list(value)
|
|
214
|
+
value.each_with_index.with_object([]) do |(item, i), entries|
|
|
215
|
+
if item.is_a?(String) && !item.strip.empty? && !item.start_with?('/')
|
|
216
|
+
entries << PayloadFile.new(key: item, names: {})
|
|
217
|
+
else
|
|
218
|
+
@errors << Issue.new("payload[#{i}]", 'file name must be a non-empty relative string (no leading "/")')
|
|
219
|
+
end
|
|
159
220
|
end
|
|
160
|
-
|
|
161
|
-
next if item.is_a?(String) && !item.strip.empty? && !item.start_with?('/')
|
|
221
|
+
end
|
|
162
222
|
|
|
163
|
-
|
|
223
|
+
# Mapping form: common key -> null (key everywhere) or a selector map
|
|
224
|
+
# (exact target / family / "*") of real file names per target.
|
|
225
|
+
def validate_payload_map(value)
|
|
226
|
+
value.each_with_object([]) do |(key, names), entries|
|
|
227
|
+
if !key.is_a?(String) || key.strip.empty? || key.start_with?('/')
|
|
228
|
+
@errors << Issue.new("payload.#{key.inspect}", 'must be a non-empty relative file name (no leading "/")')
|
|
229
|
+
next
|
|
230
|
+
end
|
|
231
|
+
if names.nil? || names == {}
|
|
232
|
+
entries << PayloadFile.new(key: key, names: {})
|
|
233
|
+
next
|
|
234
|
+
end
|
|
235
|
+
unless names.is_a?(Hash)
|
|
236
|
+
@errors << Issue.new("payload.#{key}", 'must be null or a mapping of target selector -> real file name')
|
|
237
|
+
next
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
resolved = {}
|
|
241
|
+
entry_valid = true
|
|
242
|
+
names.each do |selector, name|
|
|
243
|
+
unless valid_selector?(selector)
|
|
244
|
+
@errors << Issue.new("payload.#{key}.#{selector}",
|
|
245
|
+
"unknown target selector (allowed: \"*\", an os (linux) or a family " \
|
|
246
|
+
"(#{Target::FAMILIES.join(', ')}), optionally with a version, e.g. windows-11.0)")
|
|
247
|
+
entry_valid = false
|
|
248
|
+
next
|
|
249
|
+
end
|
|
250
|
+
if !name.is_a?(String) || name.strip.empty? || name.start_with?('/')
|
|
251
|
+
@errors << Issue.new("payload.#{key}.#{selector}", 'file name must be a non-empty relative string (no leading "/")')
|
|
252
|
+
entry_valid = false
|
|
253
|
+
next
|
|
254
|
+
end
|
|
255
|
+
resolved[selector] = name
|
|
256
|
+
end
|
|
257
|
+
entries << PayloadFile.new(key: key, names: resolved) if entry_valid
|
|
164
258
|
end
|
|
165
259
|
end
|
|
166
260
|
|
|
261
|
+
def valid_selector?(selector)
|
|
262
|
+
return true if selector == '*'
|
|
263
|
+
return true if selector == 'linux' # the one os selector (macos/windows are families)
|
|
264
|
+
|
|
265
|
+
Target.parse(selector.to_s)
|
|
266
|
+
true
|
|
267
|
+
rescue Target::Error
|
|
268
|
+
false
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Exact target string ("windows-11.0") wins over the family ("windows"),
|
|
272
|
+
# then the os ("linux"), then "*"; an entry without a map carries its
|
|
273
|
+
# key on every target.
|
|
274
|
+
def resolve_entry(entry, target)
|
|
275
|
+
return entry.key if entry.names.empty?
|
|
276
|
+
|
|
277
|
+
entry.names[target.to_s] ||
|
|
278
|
+
entry.names[target.family.to_s] ||
|
|
279
|
+
entry.names[OS_BY_FAMILY[target.family]] ||
|
|
280
|
+
entry.names['*']
|
|
281
|
+
end
|
|
282
|
+
|
|
167
283
|
def validate_executables
|
|
168
284
|
value = @data['executables']
|
|
169
285
|
return if value.nil?
|
data/lib/crosspack/packer.rb
CHANGED
|
@@ -96,7 +96,7 @@ module Crosspack
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
lib_dst = File.join(@pkg.prefix, @pkg.lib_dir)
|
|
99
|
-
files = @pkg.
|
|
99
|
+
files = @pkg.payload_for(@target).to_h { |n| [File.join(src_dir, n), File.join(lib_dst, n)] }
|
|
100
100
|
|
|
101
101
|
if @pkg.icon
|
|
102
102
|
icon_src = File.expand_path(@pkg.icon, @root)
|
|
@@ -110,7 +110,7 @@ module Crosspack
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
symlinks = @pkg.links.to_h { |link, target| [File.join(@pkg.prefix, link), target] }
|
|
113
|
-
executables = @pkg.
|
|
113
|
+
executables = @pkg.executables_for(@target).map { |exe| File.join(lib_dst, exe) }
|
|
114
114
|
|
|
115
115
|
{ files: files, symlinks: symlinks, executables: executables,
|
|
116
116
|
src_dir: src_dir, lib_dst: lib_dst }
|
|
@@ -192,7 +192,7 @@ module Crosspack
|
|
|
192
192
|
depends: resolved_depends(:pkgbuild),
|
|
193
193
|
arch: [@target.package_arch(:pkgbuild)],
|
|
194
194
|
# PKGBUILD sources are archive-relative names, not local paths.
|
|
195
|
-
files: @pkg.
|
|
195
|
+
files: @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] },
|
|
196
196
|
symlinks: @pkg_data[:symlinks],
|
|
197
197
|
executables: @pkg_data[:executables],
|
|
198
198
|
source: ["#{@pkg.name}-#{version}.tar.gz"],
|
|
@@ -213,7 +213,7 @@ module Crosspack
|
|
|
213
213
|
def pack_windows
|
|
214
214
|
output = File.join(@target.output_dir(@output_base, :winget),
|
|
215
215
|
"#{@pkg.name}-#{Builders::Wix.msi_version(@version)}.msi")
|
|
216
|
-
files = @pkg.
|
|
216
|
+
files = @pkg.payload_for(@target).to_h { |n| [File.join(@pkg_data[:src_dir], n), n] }
|
|
217
217
|
Builders::Wix.build(
|
|
218
218
|
name: @pkg.name,
|
|
219
219
|
version: @version,
|
|
@@ -228,13 +228,13 @@ module Crosspack
|
|
|
228
228
|
# macOS: .app bundle staging; DMG itself requires a Mac (hdiutil).
|
|
229
229
|
def pack_macos
|
|
230
230
|
output_dir = @target.output_dir(@output_base, :cask)
|
|
231
|
-
files = @pkg.
|
|
231
|
+
files = @pkg.payload_for(@target).to_h { |n| [File.join(@pkg_data[:src_dir], n), n] }
|
|
232
232
|
Builders::AppDir.build(
|
|
233
233
|
name: @pkg.name,
|
|
234
234
|
version: @version,
|
|
235
235
|
summary: @pkg.summary,
|
|
236
236
|
files: files,
|
|
237
|
-
executables: @pkg.
|
|
237
|
+
executables: @pkg.executables_for(@target),
|
|
238
238
|
output: output_dir
|
|
239
239
|
)
|
|
240
240
|
end
|
data/lib/crosspack/target.rb
CHANGED
|
@@ -55,6 +55,45 @@ module Crosspack
|
|
|
55
55
|
new(family_sym, version, norm_arch)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# The target of the machine crosspack runs on, for stage commands invoked
|
|
59
|
+
# without an explicit target: "macos", "windows-11.0" or the distro
|
|
60
|
+
# family + version from the os-release file ("ubuntu-24.04",
|
|
61
|
+
# "debian-12", "fedora-41", "arch"). Unknown distro ids fall back to the
|
|
62
|
+
# first ID_LIKE token that names a family. Returns nil when the host is
|
|
63
|
+
# not recognizable — the CLI then asks for an explicit target.
|
|
64
|
+
# Windows defaults to the 11.0 target vocabulary (10 is EOL); pass a
|
|
65
|
+
# target explicitly when packing for another release.
|
|
66
|
+
def self.host_string(ruby_platform: RUBY_PLATFORM, release_path: '/etc/os-release')
|
|
67
|
+
case ruby_platform
|
|
68
|
+
when /darwin/ then 'macos'
|
|
69
|
+
when /mingw|mswin|cygwin/i then 'windows-11.0'
|
|
70
|
+
when /linux/
|
|
71
|
+
facts = host_os_release(release_path)
|
|
72
|
+
family = ([facts['ID']] + facts['ID_LIKE'].to_s.split)
|
|
73
|
+
.filter_map { |id| id.to_s.to_sym if id }
|
|
74
|
+
.find { |f| FAMILIES.include?(f) }
|
|
75
|
+
return nil if family.nil?
|
|
76
|
+
|
|
77
|
+
version = facts['VERSION_ID'].to_s
|
|
78
|
+
return family.to_s if VERSIONLESS_FAMILIES.include?(family) ||
|
|
79
|
+
OPTIONAL_VERSION_FAMILIES.include?(family)
|
|
80
|
+
return nil if version.empty? # e.g. Debian sid: no pin-able version
|
|
81
|
+
|
|
82
|
+
"#{family}-#{version}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.host_os_release(path)
|
|
87
|
+
File.read(path).each_line.with_object({}) do |line, facts|
|
|
88
|
+
key, value = line.split('=', 2)
|
|
89
|
+
next if value.nil?
|
|
90
|
+
|
|
91
|
+
facts[key.strip] = value.strip.delete_prefix('"').delete_suffix('"')
|
|
92
|
+
end
|
|
93
|
+
rescue Errno::ENOENT
|
|
94
|
+
{}
|
|
95
|
+
end
|
|
96
|
+
|
|
58
97
|
def initialize(family, version = nil, arch = :x86_64)
|
|
59
98
|
@family = family.to_sym
|
|
60
99
|
@version = version && version.to_s
|
data/lib/crosspack/version.rb
CHANGED
data/lib/crosspack.rb
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'crosspack/version'
|
|
4
|
+
|
|
5
|
+
module Crosspack
|
|
6
|
+
# A stage failed (deps/build/pack/launch). Defined before the requires:
|
|
7
|
+
# launcher.rb subclasses it at load time.
|
|
8
|
+
class BuildError < StandardError; end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative 'crosspack/colors'
|
|
4
12
|
require_relative 'crosspack/target'
|
|
5
13
|
require_relative 'crosspack/manifest'
|
|
14
|
+
require_relative 'crosspack/command_manifest'
|
|
6
15
|
require_relative 'crosspack/package_manifest'
|
|
7
16
|
require_relative 'crosspack/config'
|
|
8
17
|
require_relative 'crosspack/resolver'
|
|
9
18
|
require_relative 'crosspack/matrix'
|
|
10
19
|
require_relative 'crosspack/builds'
|
|
11
20
|
require_relative 'crosspack/packer'
|
|
21
|
+
require_relative 'crosspack/launcher'
|
|
12
22
|
require_relative 'crosspack/builders/deb'
|
|
13
23
|
require_relative 'crosspack/builders/rpm'
|
|
14
24
|
require_relative 'crosspack/builders/pkgbuild'
|
|
@@ -29,7 +39,6 @@ module Crosspack
|
|
|
29
39
|
# files: { source_path => destination_inside_package (no leading /) }
|
|
30
40
|
# symlinks: { link_path_inside_package => link_target }
|
|
31
41
|
# executables: [destination paths to chmod 0755]
|
|
32
|
-
class BuildError < StandardError; end
|
|
33
42
|
|
|
34
43
|
# Convenience wrapper: Crosspack.pack(config: 'crosspack.yml',
|
|
35
44
|
# target: Target.parse('debian-12'), version: '1.0-1')
|
data/logo.png
ADDED
|
Binary file
|
metadata
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crosspack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Orlov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: colorize
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
13
26
|
description: 'One command, one config file, one target vocabulary: crosspack deps
|
|
14
27
|
<target> verifies and installs the build host dependencies, crosspack build <target>
|
|
15
28
|
runs the crosspack.yml build matrix entry and fans artifacts into builds/, crosspack
|
|
@@ -43,7 +56,10 @@ files:
|
|
|
43
56
|
- lib/crosspack/builders/rpm.rb
|
|
44
57
|
- lib/crosspack/builders/wix.rb
|
|
45
58
|
- lib/crosspack/builds.rb
|
|
59
|
+
- lib/crosspack/colors.rb
|
|
60
|
+
- lib/crosspack/command_manifest.rb
|
|
46
61
|
- lib/crosspack/config.rb
|
|
62
|
+
- lib/crosspack/launcher.rb
|
|
47
63
|
- lib/crosspack/manifest.rb
|
|
48
64
|
- lib/crosspack/matrix.rb
|
|
49
65
|
- lib/crosspack/package_manifest.rb
|
|
@@ -51,13 +67,13 @@ files:
|
|
|
51
67
|
- lib/crosspack/resolver.rb
|
|
52
68
|
- lib/crosspack/target.rb
|
|
53
69
|
- lib/crosspack/version.rb
|
|
70
|
+
- logo.png
|
|
54
71
|
homepage: https://github.com/OrelSokolov/crosspack
|
|
55
72
|
licenses:
|
|
56
73
|
- Nonstandard
|
|
57
74
|
metadata:
|
|
58
75
|
source_code_uri: https://github.com/OrelSokolov/crosspack
|
|
59
76
|
changelog_uri: https://github.com/OrelSokolov/crosspack/blob/main/CHANGELOG.md
|
|
60
|
-
post_install_message:
|
|
61
77
|
rdoc_options: []
|
|
62
78
|
require_paths:
|
|
63
79
|
- lib
|
|
@@ -72,8 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
88
|
- !ruby/object:Gem::Version
|
|
73
89
|
version: '0'
|
|
74
90
|
requirements: []
|
|
75
|
-
rubygems_version: 3.
|
|
76
|
-
signing_key:
|
|
91
|
+
rubygems_version: 3.6.9
|
|
77
92
|
specification_version: 4
|
|
78
93
|
summary: 'One config (crosspack.yml), staged pipeline: deps → build → pack native
|
|
79
94
|
packages (deb/rpm/PKGBUILD/winget/cask)'
|