crosspack 0.5.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 446cc8ee80c8c72227611451283314f75a8c9a276e915cecd416c72269e10d7a
4
- data.tar.gz: f6186516867c2dbb74871991d33b9eb3e24ab79c627ad32dd169bf8b0be78611
3
+ metadata.gz: 369c6df533b4a7e576fb1bfe4af308d0e422082934a533995355eb1bec6f8854
4
+ data.tar.gz: 65eca41ff0574b5460dffcfe9a2e33f36936ddff9184a4ed2908df0290f711e8
5
5
  SHA512:
6
- metadata.gz: 1208559fef0f7a26b62b375d4ca90578fd52668cb30862f4ba9245e3402f6b4f0e512ef4b0029eea5072328c2818e21f9d3f5ebd584edfd286b34abc9a977fa3
7
- data.tar.gz: 6902bf1a81aa6ecaecc8fead2fed2886e80f948957e8bead59be6cba6ec9710d22457bed4e7ffb9e8b793e956baf764aab0a0c7ff584eb7ed13c81f23f4e3add
6
+ metadata.gz: 7449ad323b7beb15a046ced0dfb3e47caf437d3844f660af93d4da19e3d2fd74cc64ed62f3e531589e6079c49309ee82ec7c1959f3cdef093cc517a9e67cc2d7
7
+ data.tar.gz: e58bf61ce945544bc8ba0dd96c41760ffa8637cdb934bef375c18c3d70ef3aa465369a8122455008964cd608eabf066e79a6dfe60fa5f5bca87051d42b5e5bc0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ 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.6.2] - 2026-09-13
9
+
10
+ ### Added
11
+ - Colored CLI output via the new `colorize` runtime dependency: success
12
+ messages green, errors red, validation warnings yellow (the `ok`/`MISSING`
13
+ statuses in the deps report included). Colors apply only to a TTY and
14
+ respect `NO_COLOR`, so piped and logged output stays plain.
15
+ - The gem now ships `logo.png` in its files.
16
+
17
+ ## [0.6.1] - 2026-09-13
18
+
19
+ ### Fixed
20
+ - `crosspack deps` no longer dumps a Ruby backtrace when the (detected or
21
+ given) target has no matrix entry — it aborts with the plain message, now
22
+ with an actionable hint (pass a declared target or extend artifacts.to).
23
+ - `crosspack --version` (bare, also `-v`) prints the gem version instead of
24
+ demanding an argument; `--version X` keeps overriding the build/pack
25
+ version.
26
+
27
+ [0.6.2]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.6.2
28
+ [0.6.1]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.6.1
29
+
30
+ ## [0.6.0] - 2026-09-13
31
+
32
+ ### Added
33
+ - Host target default: `crosspack deps` / `build` / `pack` invoked without a
34
+ target run for the current host — `macos`, `windows-11.0` or the distro
35
+ family + version from `/etc/os-release` (unknown distro ids fall back to
36
+ the first known `ID_LIKE` token; unrecognizable hosts ask for an explicit
37
+ target).
38
+ - Per-target payload names: `package.payload` entries may now be written as a
39
+ mapping `common key -> {target selector -> real file name}` instead of a
40
+ flat list. The key is the common name (used verbatim on every target
41
+ unless overridden); selectors are an exact target (`windows-11.0`), a
42
+ family (`windows`) or `"*"` (exact target wins, then family, then `"*"`).
43
+ An entry with a selector map applies only to targets it covers, so one
44
+ payload lists `app`/`app.exe`, `.so` libraries and Windows DLLs together —
45
+ each target resolves to the files its build actually produced. The flat
46
+ list form remains valid (same name everywhere). `executables` keeps naming
47
+ common keys and resolves per target at pack time.
48
+
49
+ [0.6.0]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.6.0
50
+
8
51
  ## [0.5.1] - 2026-09-13
9
52
 
10
53
  ### Fixed
data/README.md CHANGED
@@ -22,6 +22,15 @@ crosspack build ubuntu-24.04
22
22
  crosspack pack ubuntu-24.04
23
23
  ```
24
24
 
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
+
25
34
  ## crosspack.yml — one file, three sections
26
35
 
27
36
  The top level carries the shared facts — `name` (required once) and the
@@ -91,11 +100,27 @@ package:
91
100
  lib_dir: lib/myapp # optional, default lib/<name>
92
101
 
93
102
  payload: # files from sources -> <prefix>/<lib_dir>/
94
- - myapp
103
+ - myapp # flat form: the same name on every target
95
104
  - helper
96
105
  - model.onnx
97
106
 
98
- executables: [myapp, helper] # chmod 755, must be in payload
107
+ # Mapping form: per-target file names. The key is the common name (and the
108
+ # default file name); selectors are an exact target (windows-11.0), a
109
+ # family (windows), an os (linux — every distro family) or "*" — exact
110
+ # target wins, then family, then os, then "*". An entry with selectors
111
+ # applies only to targets it covers, so .so libraries and Windows DLLs can
112
+ # share one payload:
113
+ #
114
+ # payload:
115
+ # myapp:
116
+ # "*": myapp
117
+ # windows: myapp.exe
118
+ # libonnxruntime.so.1:
119
+ # linux: libonnxruntime.so.1
120
+ # onnxruntime.dll:
121
+ # windows: onnxruntime.dll
122
+
123
+ executables: [myapp, helper] # common keys; chmod 755, resolved per target
99
124
 
100
125
  links: # symlinks relative to prefix
101
126
  bin/myapp: ../lib/myapp/myapp
data/exe/crosspack CHANGED
@@ -12,25 +12,48 @@ 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 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" \
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" \
28
51
  'Commands: validate, resolve, matrix, version, targets'
29
52
 
30
53
  opts.on('-f', '--file PATH', 'path to crosspack.yml (default: ./crosspack.yml)') { |v| options[:file] = v }
31
54
  opts.on('--arch ARCH', 'architecture: amd64/x86_64, arm64/aarch64 (default: amd64)') { |v| options[:arch] = v }
32
55
  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 }
56
+ opts.on('--version VERSION', 'override the version for build/pack; a bare `crosspack --version` prints the gem version') { |v| options[:version] = v }
34
57
  opts.on('--root PATH', 'project root for relative paths (default: .)') { |v| options[:root] = v }
35
58
  opts.on('--output-base DIR', 'output directory (build stage: the manifest output, default builds; pack: default ./crosspacks)') { |v| options[:output_base] = v }
36
59
  opts.on('--check', 'with `deps`: only report presence, never install (exit 1 if missing)') { options[:check_only] = true }
@@ -79,6 +102,19 @@ def entry_for_target!(manifest, target, host_os, host_arch)
79
102
  end
80
103
 
81
104
  entry
105
+ rescue Crossbuild::Error => e
106
+ abort_with e.message
107
+ end
108
+
109
+ # Stage commands (deps/build/pack) run for the current host when no target
110
+ # is given: macos, windows-11.0 or the distro from /etc/os-release.
111
+ def host_target!(command)
112
+ raw = Crosspack::Target.host_string
113
+ abort_with "Could not detect this host's target — pass one explicitly: crosspack #{command} <target> " \
114
+ '(families: ubuntu-24.04, debian-12, fedora-41, arch, macos, windows-11.0)' if raw.nil?
115
+
116
+ puts "→ no target given; using this host's target: #{raw}"
117
+ raw
82
118
  end
83
119
 
84
120
  case command
@@ -86,10 +122,10 @@ when 'validate'
86
122
  deps_manifest = config.deps_manifest
87
123
  pkg = config.package_manifest
88
124
  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
125
+ puts colorized_report(config.error_report)
126
+ puts colorized_report(deps_manifest.error_report)
127
+ puts colorized_report(pkg.error_report)
128
+ puts colorized_report(build_manifest.error_report)
93
129
  ok = config.valid? && deps_manifest.valid? && pkg.valid? && build_manifest.valid?
94
130
  exit(ok ? 0 : 1)
95
131
  when 'matrix'
@@ -131,12 +167,12 @@ when 'version'
131
167
  abort_with e.message
132
168
  end
133
169
  when 'deps'
134
- abort_with 'Specify a target: crosspack deps debian-12 (see crosspack --help)' unless positional
170
+ target_str = positional || host_target!('deps')
135
171
  build_manifest = config.build_manifest
136
172
  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
173
  begin
174
+ entry_for_target!(build_manifest, target_str, Crossbuild::Platform.os, Crossbuild::Platform.arch)
175
+ installer = Crossbuild::DepInstaller.new(build_manifest, root: options[:root])
140
176
  if options[:check_only]
141
177
  installer.check_all
142
178
  puts "deps on this host:\n#{installer.report}"
@@ -146,15 +182,14 @@ when 'deps'
146
182
  rescue Crossbuild::Error => e
147
183
  abort_with e.message
148
184
  end
149
- puts "deps ready:\n#{installer.report}"
185
+ puts Crosspack::Colors.green("deps ready:\n#{installer.report}")
150
186
  when 'build'
151
187
  build_manifest = config.build_manifest
152
188
  abort_with 'the build: section is invalid — run: crosspack validate' unless build_manifest.valid?
153
189
  if options[:all]
154
190
  target = nil
155
191
  else
156
- abort_with 'Specify a target: crosspack build ubuntu-24.04 (or --all for every host-buildable entry)' unless positional
157
- target = positional
192
+ target = positional || host_target!('build')
158
193
  end
159
194
  begin
160
195
  result = Crossbuild::Builder.new(build_manifest, root: options[:root],
@@ -163,12 +198,12 @@ when 'build'
163
198
  rescue Crossbuild::Error, Crosspack::InvalidManifestError => e
164
199
  abort_with e.message
165
200
  end
166
- puts "\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}"
201
+ puts Crosspack::Colors.green("\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}")
167
202
  puts 'Pack with: crosspack pack <target>'
168
203
  when 'pack'
169
- abort_with 'Specify a target: crosspack pack debian-12' unless positional
204
+ target_str = positional || host_target!('pack')
170
205
  begin
171
- target = Crosspack::Target.parse(positional, arch: options[:arch])
206
+ target = Crosspack::Target.parse(target_str, arch: options[:arch])
172
207
  path = Crosspack.pack(
173
208
  config: config,
174
209
  target: target,
@@ -177,11 +212,11 @@ when 'pack'
177
212
  output_base: options[:output_base] || 'crosspacks'
178
213
  )
179
214
  rescue Crosspack::Target::Error => e
180
- abort_with "Invalid target #{positional.inspect}: #{e.message}"
215
+ abort_with "Invalid target #{target_str.inspect}: #{e.message}"
181
216
  rescue Crosspack::InvalidManifestError, Crosspack::ResolveError, Crosspack::BuildError => e
182
217
  abort_with e.message
183
218
  end
184
- puts "📦 #{path}"
219
+ puts Crosspack::Colors.green("📦 #{path}")
185
220
  when 'targets'
186
221
  # What can actually be packed: builds tree + resolvable deps per target.
187
222
  pkg = config.package_manifest
@@ -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, s.installed ? 'ok' : 'MISSING', s.selector, s.command)
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
@@ -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
- Array(@data['payload'])
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
- unless value.is_a?(Array) && !value.empty?
157
- @errors << Issue.new('payload', 'must be a non-empty list of artifact files from sources')
158
- return
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
- value.each_with_index do |item, i|
161
- next if item.is_a?(String) && !item.strip.empty? && !item.start_with?('/')
221
+ end
162
222
 
163
- @errors << Issue.new("payload[#{i}]", 'file name must be a non-empty relative string (no leading "/")')
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?
@@ -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.payload.to_h { |n| [File.join(src_dir, n), File.join(lib_dst, n)] }
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.executables.map { |exe| File.join(lib_dst, exe) }
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.payload.to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] },
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.payload.to_h { |n| [File.join(@pkg_data[:src_dir], n), n] }
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.payload.to_h { |n| [File.join(@pkg_data[:src_dir], n), n] }
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.executables,
237
+ executables: @pkg.executables_for(@target),
238
238
  output: output_dir
239
239
  )
240
240
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crosspack
4
- VERSION = '0.5.1'
4
+ VERSION = '0.6.2'
5
5
  end
data/lib/crosspack.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'crosspack/version'
4
+ require_relative 'crosspack/colors'
4
5
  require_relative 'crosspack/target'
5
6
  require_relative 'crosspack/manifest'
6
7
  require_relative 'crosspack/package_manifest'
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.5.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Orlov
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-09-13 00:00:00.000000000 Z
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,6 +56,7 @@ 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
46
60
  - lib/crosspack/config.rb
47
61
  - lib/crosspack/manifest.rb
48
62
  - lib/crosspack/matrix.rb
@@ -51,13 +65,13 @@ files:
51
65
  - lib/crosspack/resolver.rb
52
66
  - lib/crosspack/target.rb
53
67
  - lib/crosspack/version.rb
68
+ - logo.png
54
69
  homepage: https://github.com/OrelSokolov/crosspack
55
70
  licenses:
56
71
  - Nonstandard
57
72
  metadata:
58
73
  source_code_uri: https://github.com/OrelSokolov/crosspack
59
74
  changelog_uri: https://github.com/OrelSokolov/crosspack/blob/main/CHANGELOG.md
60
- post_install_message:
61
75
  rdoc_options: []
62
76
  require_paths:
63
77
  - lib
@@ -72,8 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
86
  - !ruby/object:Gem::Version
73
87
  version: '0'
74
88
  requirements: []
75
- rubygems_version: 3.5.22
76
- signing_key:
89
+ rubygems_version: 3.6.9
77
90
  specification_version: 4
78
91
  summary: 'One config (crosspack.yml), staged pipeline: deps → build → pack native
79
92
  packages (deb/rpm/PKGBUILD/winget/cask)'