crosspack 0.6.2 → 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 +22 -0
- data/README.md +44 -4
- data/exe/crosspack +34 -2
- 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/version.rb +1 -1
- data/lib/crosspack.rb +9 -1
- metadata +3 -1
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,28 @@ 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
|
+
|
|
8
30
|
## [0.6.2] - 2026-09-13
|
|
9
31
|
|
|
10
32
|
### Added
|
data/README.md
CHANGED
|
@@ -31,14 +31,15 @@ token):
|
|
|
31
31
|
crosspack deps && crosspack build && crosspack pack
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
## crosspack.yml — one file, three sections
|
|
34
|
+
## crosspack.yml — one file, three sections + two host hooks
|
|
35
35
|
|
|
36
36
|
The top level carries the shared facts — `name` (required once) and the
|
|
37
37
|
optional `version` scheme — and three sections: `build:` (what and where to
|
|
38
38
|
build), `package:` (what and how to pack) and `deps:` (runtime dependencies
|
|
39
|
-
resolved to concrete packages per distro).
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
42
43
|
|
|
43
44
|
```yaml
|
|
44
45
|
name: myapp
|
|
@@ -142,6 +143,15 @@ deps:
|
|
|
142
143
|
fedora: { "*": [webkit2gtk4.1] }
|
|
143
144
|
macos: system # OS component
|
|
144
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}}"
|
|
145
155
|
```
|
|
146
156
|
|
|
147
157
|
Placeholders `{{version}}`, `{{name}}`, `{{platform}}`, `{{os}}`, `{{arch}}`
|
|
@@ -192,6 +202,10 @@ crosspack deps <target> [--check] # verify/install build deps (--check: r
|
|
|
192
202
|
crosspack build <target>|--all [--no-deps] [--version X]
|
|
193
203
|
crosspack pack <target> [--version X] # version defaults to the build stamp
|
|
194
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
|
+
|
|
195
209
|
# inspection
|
|
196
210
|
crosspack validate # crosspack.yml: top level + all sections
|
|
197
211
|
crosspack resolve <target> # Depends line for the target
|
|
@@ -212,6 +226,32 @@ crosspacks/fedora/41/x86_64/myapp-2026.08.31-1234.x86_64.rpm (needs rpmbuild)
|
|
|
212
226
|
crosspacks/arch/x86_64/PKGBUILD
|
|
213
227
|
```
|
|
214
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
|
+
|
|
215
255
|
## Library
|
|
216
256
|
|
|
217
257
|
```ruby
|
data/exe/crosspack
CHANGED
|
@@ -48,6 +48,8 @@ parser = OptionParser.new do |opts|
|
|
|
48
48
|
"Stages: deps [<target>] [--check] verify/install build deps (default: this host)\n" \
|
|
49
49
|
" build [<target>]|--all compile into builds/ (default: this host)\n" \
|
|
50
50
|
" pack [<target>] [--version X] package builds/<target> (default: this host; version from the build stamp)\n" \
|
|
51
|
+
"Host: run [-- <app args>] build for this host, then launch the main binary\n" \
|
|
52
|
+
" install launch this host's package with its installer\n" \
|
|
51
53
|
'Commands: validate, resolve, matrix, version, targets'
|
|
52
54
|
|
|
53
55
|
opts.on('-f', '--file PATH', 'path to crosspack.yml (default: ./crosspack.yml)') { |v| options[:file] = v }
|
|
@@ -126,7 +128,11 @@ when 'validate'
|
|
|
126
128
|
puts colorized_report(deps_manifest.error_report)
|
|
127
129
|
puts colorized_report(pkg.error_report)
|
|
128
130
|
puts colorized_report(build_manifest.error_report)
|
|
129
|
-
|
|
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?
|
|
130
136
|
exit(ok ? 0 : 1)
|
|
131
137
|
when 'matrix'
|
|
132
138
|
deps_manifest = config.deps_manifest
|
|
@@ -217,6 +223,32 @@ when 'pack'
|
|
|
217
223
|
abort_with e.message
|
|
218
224
|
end
|
|
219
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?
|
|
220
252
|
when 'targets'
|
|
221
253
|
# What can actually be packed: builds tree + resolvable deps per target.
|
|
222
254
|
pkg = config.package_manifest
|
|
@@ -224,5 +256,5 @@ when 'targets'
|
|
|
224
256
|
root = sources ? File.expand_path(sources, options[:root]) : File.join(options[:root], 'builds')
|
|
225
257
|
puts Crosspack::Builds.report(root)
|
|
226
258
|
else
|
|
227
|
-
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"
|
|
228
260
|
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
|
data/lib/crosspack/version.rb
CHANGED
data/lib/crosspack.rb
CHANGED
|
@@ -1,15 +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
|
+
|
|
4
11
|
require_relative 'crosspack/colors'
|
|
5
12
|
require_relative 'crosspack/target'
|
|
6
13
|
require_relative 'crosspack/manifest'
|
|
14
|
+
require_relative 'crosspack/command_manifest'
|
|
7
15
|
require_relative 'crosspack/package_manifest'
|
|
8
16
|
require_relative 'crosspack/config'
|
|
9
17
|
require_relative 'crosspack/resolver'
|
|
10
18
|
require_relative 'crosspack/matrix'
|
|
11
19
|
require_relative 'crosspack/builds'
|
|
12
20
|
require_relative 'crosspack/packer'
|
|
21
|
+
require_relative 'crosspack/launcher'
|
|
13
22
|
require_relative 'crosspack/builders/deb'
|
|
14
23
|
require_relative 'crosspack/builders/rpm'
|
|
15
24
|
require_relative 'crosspack/builders/pkgbuild'
|
|
@@ -30,7 +39,6 @@ module Crosspack
|
|
|
30
39
|
# files: { source_path => destination_inside_package (no leading /) }
|
|
31
40
|
# symlinks: { link_path_inside_package => link_target }
|
|
32
41
|
# executables: [destination paths to chmod 0755]
|
|
33
|
-
class BuildError < StandardError; end
|
|
34
42
|
|
|
35
43
|
# Convenience wrapper: Crosspack.pack(config: 'crosspack.yml',
|
|
36
44
|
# target: Target.parse('debian-12'), version: '1.0-1')
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
@@ -57,7 +57,9 @@ files:
|
|
|
57
57
|
- lib/crosspack/builders/wix.rb
|
|
58
58
|
- lib/crosspack/builds.rb
|
|
59
59
|
- lib/crosspack/colors.rb
|
|
60
|
+
- lib/crosspack/command_manifest.rb
|
|
60
61
|
- lib/crosspack/config.rb
|
|
62
|
+
- lib/crosspack/launcher.rb
|
|
61
63
|
- lib/crosspack/manifest.rb
|
|
62
64
|
- lib/crosspack/matrix.rb
|
|
63
65
|
- lib/crosspack/package_manifest.rb
|