crosspack 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da2fde445d69125a1c1ebd8120b06bb06d37400cb65d5bf95ee773c89223a235
4
- data.tar.gz: cdcefb6dcc30071b80b640e319a13e11691cdbd7b8cb8a511f07173b53da6100
3
+ metadata.gz: 134cfe690edd007f3b616d068d71bacac0fe53f8cceed1cd98e355cfde631010
4
+ data.tar.gz: 9f29cd01d22b8ef7dd9984006f955beb6a80c5dbd6c773c4f1b12ef4d1aeb326
5
5
  SHA512:
6
- metadata.gz: e05e92f827a5128d77a58c3d9655d4f5ffe0e4aaa39fd4bee1fde34e20a20ed320b6295d2f5fcffc92698afc3b3a9580a3397d1287b097c83ed6c56969ae6371
7
- data.tar.gz: d8a2a11bfd9ea856eb1fc08322a2848be3f630003767981b5d9bf3b394127800d1bcd4f6eb165db79e64a38446653db159c256a71f27f551948335cb3bf27440
6
+ metadata.gz: cd549f921be34bc876d5fa261d8fffb426c3e9356eb3bc1c30afc70954c98856efafc22b7323b70eeff48d8eb379002b477e6cd69678c9d2c87c933c1d59246c
7
+ data.tar.gz: be9a6cbec90a2e3b42121c504ec6108e9b95d9bbcbd5ed64e72fa929ef793ae5fb7cc7ef9195ac993a73848e65d49cb10d61672ea78d68499eaa1d193d80ec32
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ 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.9.0] - 2026-10-25
9
+
10
+ ### Changed
11
+ - Dependency resolution no longer aborts on an undescribed distro version
12
+ (e.g. `ubuntu-26.04`): when the deps: section has no rule for the exact
13
+ version and no `"*"` wildcard, crosspack now falls back to the rules of
14
+ the newest described version of that family and prints a warning.
15
+ Only a family with no rules at all remains a hard error.
16
+
17
+ [0.9.0]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.9.0
18
+
19
+ ## [0.8.0] - 2026-09-27
20
+
21
+ ### Added
22
+ - Make-style build goals: a matrix entry may declare `goals:` and its `id`
23
+ doubles as a goal name, so a per-binary entry works with no extra syntax.
24
+ `crosspack build` compiles only the `default` goal's entries,
25
+ `crosspack build all` every host-buildable entry, `crosspack build
26
+ <goal>` one goal or binary (`crosspack build helloworld` rebuilds nothing
27
+ else). `crosspack run [goal]` builds just that goal before launching, and
28
+ launches the executable named by a binary goal instead of the first one;
29
+ `crosspack build <package-target>` (e.g. `debian-12`) still works when
30
+ the name is not a goal or entry id. `crosspack matrix` and
31
+ `crosspack validate` list each entry's goals.
32
+
33
+ [0.8.0]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.8.0
34
+
8
35
  ## [0.7.3] - 2026-09-26
9
36
 
10
37
  ### Fixed
@@ -29,8 +56,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
29
56
 
30
57
  ### Added
31
58
  - The PKGBUILD now installs the `.desktop` file and the icon — desktop
32
- integration was deb/rpm-only. The source tarball must carry the icon under
33
- its own file name and the `.desktop` file as `<name>.desktop`.
59
+ integration was deb/rpm-only. Crosspack also builds the source tarball
60
+ (`<name>-<version>.tar.gz`) next to the PKGBUILD with the exact file names
61
+ and layout the install lines expect, so `makepkg` works out of the box —
62
+ and the PKGBUILD carries its real sha256 instead of `SKIP`.
63
+ - deb/rpm packages with a `.desktop` file or a themed icon refresh the
64
+ application database and the hicolor icon cache (`postinst`/`postrm`,
65
+ `%post`/`%postun`); the tools are guarded, minimal systems without them
66
+ are fine.
67
+ - Windows MSI gets an Add/Remove Programs icon (`ARPPRODUCTICON`): the
68
+ manifest `icon:` when it is an `.ico`, else a sibling `.ico` with the same
69
+ basename (`appicon.png` -> `appicon.ico`) when the build produces one.
70
+ - `package.min_macos` sets the .app bundle's `LSMinimumSystemVersion`
71
+ (default stays 11.0).
72
+
73
+ ### Fixed
74
+ - `.desktop` `Exec` is now an absolute path — the launcher symlink from
75
+ `links:` (e.g. `/usr/local/bin/h2voice`) or the executable inside
76
+ `lib_dir`; a bare name only resolved when `prefix/bin` happened to be in
77
+ PATH.
34
78
 
35
79
  [0.7.3]: https://github.com/OrelSokolov/crosspack/releases/tag/v0.7.3
36
80
 
data/README.md CHANGED
@@ -11,16 +11,19 @@ shipped as one gem, one command, one config file and a staged pipeline:
11
11
 
12
12
  ```
13
13
  crosspack deps <target> # verify/install the build host's dependencies
14
- crosspack build <target> # compile the target's matrix entry into builds/
14
+ crosspack build [<goal>] # compile a build goal's matrix entries into builds/
15
15
  crosspack pack <target> # package builds/<target> as a native package
16
16
  ```
17
17
 
18
- Every stage is keyed by the same target vocabulary — `debian-12`,
19
- `ubuntu-24.04`, `fedora-41`, `arch`, `macos`, `windows-11.0` — and gated by
20
- the previous one: `build` refuses to start before the deps stage passes, and
21
- `pack` refuses to run before `build` filled `builds/<target>/`. The build
22
- stage also stamps the version into the tree, so `pack` normally needs no
23
- `--version`:
18
+ The build stage is keyed by **goals** (make-style targets): `crosspack build`
19
+ runs only the `default` goal, `crosspack build all` every goal and
20
+ `crosspack build <goal>` one named goal — unrelated binaries are never
21
+ rebuilt together. The deps and pack stages are keyed by the target
22
+ vocabulary — `debian-12`, `ubuntu-24.04`, `fedora-41`, `arch`, `macos`,
23
+ `windows-11.0` — and gated by the previous one: `build` refuses to start
24
+ before the deps stage passes, and `pack` refuses to run before `build`
25
+ filled `builds/<target>/`. The build stage also stamps the version into the
26
+ tree, so `pack` normally needs no `--version`:
24
27
 
25
28
  ```
26
29
  crosspack deps ubuntu-24.04
@@ -37,6 +40,50 @@ token):
37
40
  crosspack deps && crosspack build && crosspack pack
38
41
  ```
39
42
 
43
+ ## Build goals
44
+
45
+ Build goals are make-style targets — usually one binary each. A matrix entry
46
+ is one goal's recipe; the entry `id` doubles as the goal name, so a
47
+ per-binary entry needs no extra syntax. `goals:` regroups entries when one
48
+ goal should cover several (an entry without `goals:` belongs to the implicit
49
+ `default` goal; one entry may serve several goals):
50
+
51
+ ```yaml
52
+ build:
53
+ matrix:
54
+ - id: helloworld # goal name = entry id
55
+ build: linux/amd64
56
+ steps: [go build -o build/bin/helloworld ./cmd/helloworld]
57
+ artifacts: { from: build/bin, include: [helloworld], to: [ubuntu-24.04] }
58
+ - id: main
59
+ build: linux/amd64
60
+ goals: [default, app] # what `crosspack build` runs; also buildable as `app`
61
+ steps: [wails build -platform linux/amd64]
62
+ artifacts: { from: build/bin, to: [ubuntu-24.04, debian-12] }
63
+ - id: tools
64
+ build: linux/amd64
65
+ goals: [tools]
66
+ steps: [cargo build --release -p helper]
67
+ ```
68
+
69
+ ```
70
+ crosspack build # the default goal only (same as: build default)
71
+ crosspack build helloworld # one binary — nothing else rebuilds
72
+ crosspack build tools # one named goal
73
+ crosspack build all # every host-buildable entry (--all works too)
74
+ crosspack run helloworld # build just that binary, then launch it
75
+ crosspack run # build the default goal, launch the first executable
76
+ ```
77
+
78
+ `crosspack run <binary>` launches the executable named by the goal (it must
79
+ be listed in `package.executables`); group goals and the default goal launch
80
+ the first one. Goal names use letters/digits/`+._-`; `all` is reserved. A
81
+ positional that is not a declared goal or entry id still works as a package
82
+ target (`crosspack build debian-12` — the single entry distributing to it,
83
+ with fanout limited to that target), so old invocations keep working.
84
+ `crosspack matrix` lists each entry's goals and what the default goal would
85
+ run on this host.
86
+
40
87
  ## crosspack.yml — one file, three sections + two host hooks
41
88
 
42
89
  The top level carries the shared facts — `name` (required once) and the
@@ -71,6 +118,7 @@ build:
71
118
 
72
119
  matrix:
73
120
  - build: linux/amd64 # os/arch this entry builds on (arch may be "any")
121
+ goals: [default] # build goals this entry belongs to (default: [default])
74
122
  env: # extra env for the steps of this entry
75
123
  CGO_ENABLED: "1"
76
124
  steps: # shell commands, run from the project root
@@ -149,6 +197,8 @@ deps:
149
197
  fedora: { "*": [webkit2gtk4.1] }
150
198
  macos: system # OS component
151
199
  windows: system # preinstalled (WebView2)
200
+ # An undescribed version (ubuntu-26.04) falls back to the newest described
201
+ # version of the family with a warning; only a family with no rules errors.
152
202
 
153
203
  # host-only launch hooks (optional): same host selectors as build.deps
154
204
  run: # `crosspack run`: how to launch the built binary
@@ -205,11 +255,12 @@ config.
205
255
  ```
206
256
  # staged pipeline
207
257
  crosspack deps <target> [--check] # verify/install build deps (--check: report only)
208
- crosspack build <target>|--all [--no-deps] [--version X]
258
+ crosspack build [<goal>|all] [--no-deps] [--version X]
259
+ # goal: default (implicit), a declared goal, or all
209
260
  crosspack pack <target> [--version X] # version defaults to the build stamp
210
261
 
211
- # host-only: this machine, no target argument
212
- crosspack run [-- --dev] # build for this host, then launch the main binary
262
+ # host-only: this machine, no package-target argument
263
+ crosspack run [<goal>] [-- --dev] # build the goal (default: default), then launch the main binary
213
264
  crosspack install # launch this host's package with its installer
214
265
 
215
266
  # inspection
@@ -235,10 +286,11 @@ crosspacks/arch/x86_64/PKGBUILD
235
286
  ### Host-only commands: run and install
236
287
 
237
288
  `run` and `install` apply to the current host only — the build target *is*
238
- the run target, so they take no target argument. `crosspack run` runs the
239
- build stage for this host (same as `crosspack build <host target>`), then
240
- launches the first `package.executables` binary straight from `builds/` —
241
- the binary, not the package. Everything after `--` goes to the app:
289
+ the run target, so they take no package-target argument. `crosspack run
290
+ [goal]` runs the build stage for the given goal (default: `default` — same
291
+ as `crosspack build <goal>`), then launches the first `package.executables`
292
+ binary straight from `builds/` — the binary, not the package; only that
293
+ goal's entries are rebuilt. Everything after `--` goes to the app:
242
294
 
243
295
  ```
244
296
  crosspack run -- --dev
@@ -264,7 +316,8 @@ executed directly, except a macOS `.app` bundle which is `open`ed.
264
316
  require 'crosspack' # pulls in Crossbuild too
265
317
 
266
318
  Crossbuild.build('crosspack.yml', root: Dir.pwd) # all host entries
267
- Crossbuild.build('crosspack.yml', target: 'ubuntu-24.04') # the stages' build, library-level
319
+ Crossbuild.build('crosspack.yml', goal: 'default') # one build goal
320
+ Crossbuild.build('crosspack.yml', target: 'ubuntu-24.04') # legacy target selection
268
321
 
269
322
  Crosspack.pack(
270
323
  config: 'crosspack.yml',
data/exe/crosspack CHANGED
@@ -46,9 +46,9 @@ parser = OptionParser.new do |opts|
46
46
  opts.banner = "crosspack #{Crosspack::VERSION} — staged pipeline: deps → build → pack\n" \
47
47
  "Usage: crosspack <command> [options]\n" \
48
48
  "Stages: deps [<target>] [--check] verify/install build deps (default: this host)\n" \
49
- " build [<target>]|--all compile into builds/ (default: this host)\n" \
49
+ " build [<goal>|all] [--no-deps] compile into builds/ (default goal: default)\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" \
51
+ "Host: run [<goal>] [-- <app args>] build the goal (default: default), then launch the main binary\n" \
52
52
  " install launch this host's package with its installer\n" \
53
53
  'Commands: validate, resolve, matrix, version, targets'
54
54
 
@@ -60,7 +60,7 @@ parser = OptionParser.new do |opts|
60
60
  opts.on('--output-base DIR', 'output directory (build stage: the manifest output, default builds; pack: default ./crosspacks)') { |v| options[:output_base] = v }
61
61
  opts.on('--check', 'with `deps`: only report presence, never install (exit 1 if missing)') { options[:check_only] = true }
62
62
  opts.on('--no-deps', 'with `build`: skip the deps stage') { options[:no_deps] = true }
63
- opts.on('--all', 'with `build`: every host-buildable matrix entry (default target selection off)') { options[:all] = true }
63
+ opts.on('--all', 'with `build`: every host-buildable matrix entry (same as: build all)') { options[:all] = true }
64
64
  opts.on('-h', '--help', 'show help') do
65
65
  puts opts
66
66
  exit
@@ -75,8 +75,8 @@ end
75
75
  command = ARGV.shift
76
76
  abort_with 'No command given. See: crosspack --help' if command.nil?
77
77
 
78
- # Positional argument: a target for the stage commands (deps/build/pack,
79
- # resolve), a crosspack.yml path for validate.
78
+ # Positional argument: a build goal for build/run, a package target for
79
+ # deps/pack/resolve, a crosspack.yml path for validate.
80
80
  positional = ARGV.shift if ARGV.first && !ARGV.first.start_with?('-')
81
81
 
82
82
  options[:file] = positional if command == 'validate' && positional
@@ -108,7 +108,7 @@ rescue Crossbuild::Error => e
108
108
  abort_with e.message
109
109
  end
110
110
 
111
- # Stage commands (deps/build/pack) run for the current host when no target
111
+ # Stage commands (deps/pack) run for the current host when no target
112
112
  # is given: macos, windows-11.0 or the distro from /etc/os-release.
113
113
  def host_target!(command)
114
114
  raw = Crosspack::Target.host_string
@@ -192,17 +192,31 @@ when 'deps'
192
192
  when 'build'
193
193
  build_manifest = config.build_manifest
194
194
  abort_with 'the build: section is invalid — run: crosspack validate' unless build_manifest.valid?
195
- if options[:all]
196
- target = nil
197
- else
198
- target = positional || host_target!('build')
195
+ # Positional: a build goal — empty/`default` (the implicit default goal),
196
+ # `all` (every host-buildable entry), a declared goal name or an entry id
197
+ # (make-style binary goal: crosspack build helloworld). Anything else
198
+ # falls back to the legacy package target (crosspack build debian-12).
199
+ arg = positional.to_s
200
+ goal = 'default'
201
+ target = nil
202
+ if options[:all] || arg == 'all'
203
+ goal = 'all'
204
+ elsif !arg.empty? && arg != 'default'
205
+ known_goal = build_manifest.declared_goals.include?(arg) || build_manifest.find_entry(arg)
206
+ goal = known_goal ? arg : nil
207
+ target = arg if goal.nil?
199
208
  end
200
209
  begin
201
210
  result = Crossbuild::Builder.new(build_manifest, root: options[:root],
202
211
  output_base: options[:output_base], version: options[:version],
203
- deps: !options[:no_deps]).run(target: target)
212
+ deps: !options[:no_deps]).run(goal: goal, target: target)
204
213
  rescue Crossbuild::Error, Crosspack::InvalidManifestError => e
205
- abort_with e.message
214
+ message = e.message
215
+ if target
216
+ selectors = (build_manifest.declared_goals | build_manifest.entries.map(&:id)).sort
217
+ message += "\nNot a package target either — buildable selectors: #{selectors.join(', ')} (plus 'all')"
218
+ end
219
+ abort_with message
206
220
  end
207
221
  puts Crosspack::Colors.green("\n📦 #{result.entries.size} entr#{result.entries.size == 1 ? 'y' : 'ies'} built, version #{result.version}")
208
222
  puts 'Pack with: crosspack pack <target>'
@@ -224,19 +238,23 @@ when 'pack'
224
238
  end
225
239
  puts Crosspack::Colors.green("📦 #{path}")
226
240
  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
241
+ # Host-only commands: everything after `--` goes to the launched process
242
+ # (crosspack run -- --dev). `run` optionally takes a build goal; `install`
243
+ # never builds, so it takes no arguments.
233
244
  args = ARGV.dup
234
245
  args.shift if args.first == '--'
246
+ goal = 'default'
247
+ if command == 'run'
248
+ goal = positional if positional && !positional.empty?
249
+ elsif positional
250
+ abort_with "crosspack install launches this host's package — no arguments " \
251
+ '(crosspack run [goal] is the one that builds)'
252
+ end
235
253
  begin
236
254
  status =
237
255
  if command == 'run'
238
256
  Crosspack::Launcher.run(
239
- config, root: options[:root], args: args,
257
+ config, root: options[:root], args: args, goal: goal,
240
258
  output_base: options[:output_base], version: options[:version], deps: !options[:no_deps]
241
259
  )
242
260
  else
@@ -24,18 +24,22 @@ module Crossbuild
24
24
  ARTIFACT_MODES = %w[symlink copy].freeze
25
25
  VERSION_SCHEMES = %w[calver git-tag].freeze
26
26
  TOP_LEVEL_KEYS = %w[name version output deps matrix].freeze
27
- ENTRY_KEYS = %w[id build env steps artifacts].freeze
27
+ ENTRY_KEYS = %w[id goals build env steps artifacts].freeze
28
+ DEFAULT_GOAL = 'default'
29
+ ALL_GOAL = 'all'
28
30
  ARTIFACTS_KEYS = %w[from include to mode].freeze
29
31
  DEP_KEYS = %w[hosts].freeze
30
32
  BUILD_RE = /\A(linux|darwin|windows)\/(#{ARCH_LIST.join('|')})\z/.freeze
31
33
  NAME_RE = /\A[a-z0-9][a-z0-9+._-]*\z/i.freeze
32
34
 
33
- # One matrix entry: where it builds, what it runs, where artifacts go.
35
+ # One matrix entry: where it builds, what it runs, where artifacts go,
36
+ # which build goals it belongs to.
34
37
  class Entry
35
- attr_reader :id, :os, :arch, :env, :steps, :artifacts, :index
38
+ attr_reader :id, :goals, :os, :arch, :env, :steps, :artifacts, :index
36
39
 
37
- def initialize(id:, os:, arch:, env:, steps:, artifacts:, index:)
40
+ def initialize(id:, goals:, os:, arch:, env:, steps:, artifacts:, index:)
38
41
  @id = id
42
+ @goals = goals
39
43
  @os = os.to_sym
40
44
  @arch = arch
41
45
  @env = env
@@ -116,7 +120,8 @@ module Crossbuild
116
120
  lines = []
117
121
  if valid?
118
122
  noun = @entries.size == 1 ? 'entry' : 'entries'
119
- lines << "#{path}: the build: section is valid (#{@entries.size} #{noun}: #{@entries.map(&:id).join(', ')})."
123
+ lines << "#{path}: the build: section is valid (#{@entries.size} #{noun}: #{@entries.map(&:id).join(', ')}; " \
124
+ "goals: #{declared_goals.join(', ')})."
120
125
  else
121
126
  lines << "#{path}: the build: section is invalid (#{@errors.size} errors):"
122
127
  @errors.each { |e| lines << " ✗ #{e}" }
@@ -134,6 +139,16 @@ module Crossbuild
134
139
  @entries.find { |e| e.id == id }
135
140
  end
136
141
 
142
+ # Every goal name used by the entries, sorted; an entry without `goals:`
143
+ # belongs to the implicit `default` goal.
144
+ def declared_goals
145
+ @entries.flat_map(&:goals).uniq.sort
146
+ end
147
+
148
+ def entries_for_goal(goal)
149
+ @entries.select { |e| e.goals.include?(goal) }
150
+ end
151
+
137
152
  # The single matrix entry that distributes artifacts to the given target
138
153
  # string (e.g. "ubuntu-24.04"). Raises Crossbuild::Error when no entry
139
154
  # claims it or when several do (ambiguous).
@@ -314,6 +329,7 @@ module Crossbuild
314
329
  steps = validate_steps(entry['steps'], path)
315
330
  env = validate_env(entry['env'], path)
316
331
  artifacts = validate_artifacts(entry['artifacts'], path, arch)
332
+ goals = validate_goals(entry['goals'], path)
317
333
  id = entry['id'].nil? ? "#{os}/#{Platform.display_arch(arch == :any ? :x86_64 : arch)}" : entry['id'].to_s
318
334
  if entry['id'] && id.empty?
319
335
  @errors << Issue.new("#{path}.id", 'must be a non-empty string')
@@ -324,10 +340,31 @@ module Crossbuild
324
340
  return
325
341
  end
326
342
  @warnings << Issue.new(path, 'entry has no steps — existing artifacts only will be distributed') if entry['steps'].nil? && artifacts
327
- @entries << Entry.new(id: id, os: os, arch: arch, env: env, steps: steps,
343
+ @entries << Entry.new(id: id, goals: goals, os: os, arch: arch, env: env, steps: steps,
328
344
  artifacts: artifacts, index: index)
329
345
  end
330
346
 
347
+ # goals: names the make-style build goals this entry belongs to
348
+ # (crosspack build <goal>); missing -> the implicit `default` goal.
349
+ def validate_goals(goals, path)
350
+ return [DEFAULT_GOAL] if goals.nil?
351
+
352
+ unless goals.is_a?(Array) && !goals.empty? && goals.all? { |g| g.is_a?(String) && !g.strip.empty? }
353
+ @errors << Issue.new("#{path}.goals", "must be a non-empty list of goal names, e.g. [#{DEFAULT_GOAL}, tools]")
354
+ return []
355
+ end
356
+ goals.each do |goal|
357
+ if goal == ALL_GOAL
358
+ @errors << Issue.new("#{path}.goals", "#{goal.inspect} is reserved — `crosspack build all` builds every goal")
359
+ elsif goal !~ NAME_RE
360
+ @errors << Issue.new("#{path}.goals", "invalid goal name #{goal.inspect} (letters, digits, \"+\", \"_\", \"-\", \".\")")
361
+ end
362
+ end
363
+ @errors << Issue.new("#{path}.goals", "duplicate goal names: #{goals.tally.select { |_, n| n > 1 }.keys.join(', ')}") if goals.uniq.size != goals.size
364
+
365
+ goals.uniq
366
+ end
367
+
331
368
  def parse_build(build, path)
332
369
  if build.nil?
333
370
  @errors << Issue.new("#{path}.build", 'is required: the platform this entry builds on, e.g. linux/amd64')
@@ -19,17 +19,19 @@ module Crossbuild
19
19
  @host_arch = host_arch
20
20
  end
21
21
 
22
- # entry_id: nil — every entry buildable on this host; otherwise the exact
23
- # entry id (which must exist, but may target another host — the user
24
- # asked for it explicitly).
22
+ # entry_id: nil — otherwise the exact entry id (which must exist, but may
23
+ # target another host — the user asked for it explicitly).
25
24
  # target: a raw target string (e.g. "ubuntu-24.04") — builds the single
26
25
  # matrix entry distributing to it and fans artifacts out to that target
27
26
  # only; the entry must be buildable on this host (no cross-compilation).
28
- def run(entry_id: nil, target: nil)
27
+ # goal: nil/'all' — every entry buildable on this host; otherwise the
28
+ # entries declaring that build goal (entries without `goals:` belong to
29
+ # the implicit `default` goal).
30
+ def run(entry_id: nil, target: nil, goal: nil)
29
31
  manifest.validate!
30
32
  ensure_deps
31
33
  version = @version_override || VersionScheme.new(manifest.version).compute(root: @root)
32
- entries = target ? entries_for_target(target) : select_entries(entry_id)
34
+ entries = target ? entries_for_target(target) : select_entries(entry_id, goal)
33
35
 
34
36
  entries.each do |entry|
35
37
  puts "\n==> #{manifest.name} [#{entry.id}] version #{version}"
@@ -60,18 +62,52 @@ module Crossbuild
60
62
  [entry]
61
63
  end
62
64
 
63
- def select_entries(entry_id)
65
+ def select_entries(entry_id, goal)
64
66
  if entry_id
65
67
  entry = manifest.find_entry(entry_id)
66
68
  raise Error, "unknown matrix entry #{entry_id.inspect}; available: #{manifest.entries.map(&:id).join(', ')}" unless entry
67
69
 
68
70
  [entry]
71
+ elsif goal.nil? || goal == BuildManifest::ALL_GOAL
72
+ buildable_on_host
69
73
  else
70
- buildable = manifest.buildable_entries(@host_os, @host_arch)
71
- raise Error, "nothing to build on #{@host_os}/#{Platform.display_arch(@host_arch)} " \
72
- "(matrix entries: #{manifest.entries.map(&:id).join(', ')})" if buildable.empty?
74
+ entries_for_goal(goal)
75
+ end
76
+ end
77
+
78
+ def buildable_on_host
79
+ buildable = manifest.buildable_entries(@host_os, @host_arch)
80
+ raise Error, "nothing to build on #{@host_os}/#{Platform.display_arch(@host_arch)} " \
81
+ "(matrix entries: #{manifest.entries.map(&:id).join(', ')})" if buildable.empty?
82
+
83
+ buildable
84
+ end
85
+
86
+ # A goal name may also be an entry id — `crosspack build helloworld`
87
+ # builds just the entry named helloworld (a make-style binary goal).
88
+ def entries_for_goal(goal)
89
+ if manifest.declared_goals.include?(goal)
90
+ buildable = manifest.entries_for_goal(goal).select { |e| e.buildable_on?(@host_os, @host_arch) }
91
+ if buildable.empty?
92
+ others = manifest.entries_for_goal(goal).map { |e| "#{e.id} (#{e.platform})" }
93
+ raise Error, "nothing to build on #{@host_os}/#{Platform.display_arch(@host_arch)} for goal #{goal.inspect} " \
94
+ "(goal entries: #{others.join(', ')})"
95
+ end
73
96
 
74
97
  buildable
98
+ elsif (entry = manifest.find_entry(goal))
99
+ host = "#{@host_os}/#{Platform.display_arch(@host_arch)}"
100
+ unless entry.buildable_on?(@host_os, @host_arch)
101
+ raise Error,
102
+ "the entry for goal #{goal.inspect} (#{entry.id}) builds on #{entry.platform}, " \
103
+ "this host is #{host} — cross-compilation is not supported"
104
+ end
105
+
106
+ [entry]
107
+ else
108
+ available = (manifest.declared_goals | manifest.entries.map(&:id)).sort
109
+ raise Error, "unknown goal #{goal.inspect}; available: #{available.join(', ')} " \
110
+ "(or '#{BuildManifest::ALL_GOAL}' for every entry)"
75
111
  end
76
112
  end
77
113
 
@@ -22,6 +22,10 @@ module Crossbuild
22
22
  else
23
23
  "\n#{buildable.size} of #{@manifest.entries.size} entries build here: #{buildable.map(&:id).join(', ')}"
24
24
  end
25
+ default_here = buildable.select { |e| e.goals.include?('default') }
26
+ unless default_here.empty?
27
+ lines << "`crosspack build` (default goal) would run: #{default_here.map(&:id).join(', ')}"
28
+ end
25
29
  lines << render_deps
26
30
  lines.join("\n")
27
31
  end
@@ -42,6 +46,7 @@ module Crossbuild
42
46
  else
43
47
  "#{entry.steps.size} step#{'s' unless entry.steps.size == 1}"
44
48
  end
49
+ parts << "goals: #{entry.goals.join(', ')}" unless entry.goals == ['default']
45
50
  parts << artifact_note(entry) if entry.artifacts
46
51
  parts.join(' ')
47
52
  end
data/lib/crossbuild.rb CHANGED
@@ -18,11 +18,12 @@ require_relative 'crossbuild/builder'
18
18
 
19
19
  module Crossbuild
20
20
  # Convenience wrapper: Crossbuild.build('crosspack.yml') runs every
21
- # host-buildable matrix entry and returns Builder::Result. target: builds
22
- # the entry distributing to that target only; deps: false skips the
23
- # pre-build dependency check/install pass. The manifest argument may be a
24
- # Config, a path to crosspack.yml or a ready BuildManifest.
25
- def self.build(manifest, entry_id: nil, target: nil, root: Dir.pwd, output_base: nil, version: nil,
21
+ # host-buildable matrix entry and returns Builder::Result. goal: builds
22
+ # only the entries declaring that build goal ('all' or nil — every entry);
23
+ # target: builds the entry distributing to that target only; deps: false
24
+ # skips the pre-build dependency check/install pass. The manifest argument
25
+ # may be a Config, a path to crosspack.yml or a ready BuildManifest.
26
+ def self.build(manifest, entry_id: nil, target: nil, goal: nil, root: Dir.pwd, output_base: nil, version: nil,
26
27
  deps: true, host_os: Platform.os, host_arch: Platform.arch)
27
28
  m = case manifest
28
29
  when BuildManifest then manifest
@@ -30,6 +31,6 @@ module Crossbuild
30
31
  else Crosspack::Config.coerce(manifest, root: root).build_manifest
31
32
  end
32
33
  Builder.new(m, root: root, output_base: output_base, version: version, deps: deps,
33
- host_os: host_os, host_arch: host_arch).run(entry_id: entry_id, target: target)
34
+ host_os: host_os, host_arch: host_arch).run(entry_id: entry_id, target: target, goal: goal)
34
35
  end
35
36
  end
@@ -14,7 +14,8 @@ module Crosspack
14
14
  # CFBundleExecutable, falling back to the package name)
15
15
  # icon: path to the bundle icon, copied into Contents/Resources
16
16
  def self.build(name:, version:, summary:, files:, executables: [],
17
- display_name: nil, icon: nil, identifier: nil, output:)
17
+ display_name: nil, icon: nil, identifier: nil,
18
+ min_macos: '11.0', output:)
18
19
  app_dir = File.join(output, "#{name}.app")
19
20
  FileUtils.rm_rf(app_dir)
20
21
  macos_dir = File.join(app_dir, 'Contents', 'MacOS')
@@ -48,7 +49,8 @@ module Crosspack
48
49
  name: name, version: version, summary: summary,
49
50
  executable: executables.first || name,
50
51
  display_name: display_name || name,
51
- icon_file: icon_file, identifier: identifier)
52
+ icon_file: icon_file, identifier: identifier,
53
+ min_macos: min_macos)
52
54
 
53
55
  File.write(File.join(output, 'BUILD-DMG.txt'), <<~TXT)
54
56
  DMG not built: hdiutil only exists on macOS.
@@ -59,7 +61,8 @@ module Crosspack
59
61
  end
60
62
 
61
63
  def self.write_info_plist(path, name:, version:, summary:, executable:,
62
- display_name:, icon_file: nil, identifier: nil)
64
+ display_name:, icon_file: nil, identifier: nil,
65
+ min_macos: '11.0')
63
66
  bundle_id = identifier || "org.crosspack.#{name}"
64
67
  # Without the extension .icns is assumed; other formats (png) must
65
68
  # be named in full.
@@ -86,7 +89,7 @@ module Crosspack
86
89
  <key>CFBundleInfoDictionaryVersion</key>
87
90
  <string>6.0</string>
88
91
  <key>LSMinimumSystemVersion</key>
89
- <string>11.0</string>
92
+ <string>#{escape(min_macos)}</string>
90
93
  <key>NSPrincipalClass</key>
91
94
  <string>NSApplication</string>
92
95
  <key>NSSupportsAutomaticTermination</key>
@@ -45,6 +45,7 @@ module Crosspack
45
45
  stage_files(pkg_root)
46
46
  stage_symlinks(pkg_root)
47
47
  write_control(File.join(pkg_root, 'DEBIAN'))
48
+ write_maintainer_scripts(File.join(pkg_root, 'DEBIAN'))
48
49
 
49
50
  FileUtils.mkdir_p(File.dirname(@output))
50
51
  FileUtils.rm_f(@output)
@@ -112,6 +113,38 @@ module Crosspack
112
113
  File.write(File.join(debian_dir, 'control'),
113
114
  "#{(fields + ["Description: #{summary}"]).join("\n")}\n#{body.join("\n")}\n")
114
115
  end
116
+
117
+ # Desktop integration caches: refresh the application database and the
118
+ # hicolor icon cache after (un)install when the package carries a
119
+ # .desktop file or a themed icon. The tools are guarded — they are not
120
+ # dpkg dependencies, minimal systems may lack them.
121
+ def write_maintainer_scripts(debian_dir)
122
+ commands = desktop_cache_commands
123
+ return if commands.empty?
124
+
125
+ script = "#!/bin/sh\nset -e\n#{commands.join("\n")}\n"
126
+ %w[postinst postrm].each do |script_name|
127
+ path = File.join(debian_dir, script_name)
128
+ File.write(path, script)
129
+ FileUtils.chmod(0o755, path)
130
+ end
131
+ end
132
+
133
+ def desktop_cache_commands
134
+ commands = @files.values.select { |d| d.end_with?('.desktop') }
135
+ .map { |d| File.dirname(d) }.uniq
136
+ .map do |dir|
137
+ "if command -v update-desktop-database >/dev/null 2>&1; then " \
138
+ "update-desktop-database -q /#{dir} || true; fi"
139
+ end
140
+ commands += @files.values.select { |d| d.include?('/share/icons/') }
141
+ .map { |d| File.dirname(d) }.uniq
142
+ .map do |dir|
143
+ "if command -v gtk-update-icon-cache >/dev/null 2>&1; then " \
144
+ "gtk-update-icon-cache -q -t -f /#{dir} || true; fi"
145
+ end
146
+ commands
147
+ end
115
148
  end
116
149
  end
117
150
  end
@@ -11,12 +11,13 @@ module Crosspack
11
11
  class Pkgbuild
12
12
  def self.generate(name:, version:, release:, pkgdesc:, depends:, arch:,
13
13
  files:, symlinks: {}, executables: [], source:,
14
- maintainer:, url: '', license: 'Proprietary', output:)
14
+ sha256: nil, maintainer:, url: '', license: 'Proprietary',
15
+ output:)
15
16
  content = render(
16
17
  name: name, version: version, release: release, pkgdesc: pkgdesc,
17
18
  depends: Array(depends), arch: Array(arch), files: files,
18
19
  symlinks: symlinks, executables: executables, source: Array(source),
19
- maintainer: maintainer, url: url, license: license
20
+ sha256: sha256, maintainer: maintainer, url: url, license: license
20
21
  )
21
22
  FileUtils.mkdir_p(File.dirname(output))
22
23
  File.write(output, content)
@@ -24,8 +25,8 @@ module Crosspack
24
25
  end
25
26
 
26
27
  def self.render(name:, version:, release:, pkgdesc:, depends:, arch:,
27
- files:, symlinks:, executables:, source:, maintainer:,
28
- url:, license:)
28
+ files:, symlinks:, executables:, source:, sha256:,
29
+ maintainer:, url:, license:)
29
30
  depends_line = depends.map { |d| "'#{d}'" }.join(' ')
30
31
  source_line = source.map { |s| "'#{s}'" }.join(' ')
31
32
  srcbase = "$srcdir/$pkgname-$pkgver"
@@ -52,7 +53,7 @@ module Crosspack
52
53
  license=('#{license}')
53
54
  depends=(#{depends_line})
54
55
  source=(#{source_line})
55
- sha256sums=('SKIP')
56
+ sha256sums=('#{sha256 || 'SKIP'}')
56
57
 
57
58
  package() {
58
59
  #{(install_lines + link_lines).join("\n")}
@@ -29,6 +29,7 @@ module Crosspack
29
29
  description ||= summary
30
30
  requires_line = Array(requires).join(', ')
31
31
  entries = spec_entries(files: files, symlinks: symlinks)
32
+ scriptlets = desktop_cache_scriptlets(files)
32
33
 
33
34
  <<~SPEC
34
35
  Name: #{name}
@@ -48,12 +49,35 @@ module Crosspack
48
49
  %install
49
50
  # Files are staged into the buildroot by crosspack.
50
51
 
52
+ #{scriptlets}
51
53
  %files
52
54
  %defattr(-,root,root,-)
53
55
  #{entries.join("\n")}
54
56
  SPEC
55
57
  end
56
58
 
59
+ # %post/%postun: refresh the application database and the hicolor icon
60
+ # cache when the package carries a .desktop file or a themed icon; the
61
+ # tools are guarded (minimal systems may lack them).
62
+ def self.desktop_cache_scriptlets(files)
63
+ commands = files.values.select { |d| d.end_with?('.desktop') }
64
+ .map { |d| File.dirname(d) }.uniq
65
+ .map do |dir|
66
+ "command -v update-desktop-database >/dev/null 2>&1 && " \
67
+ "update-desktop-database -q /#{dir} || :"
68
+ end
69
+ commands += files.values.select { |d| d.include?('/share/icons/') }
70
+ .map { |d| File.dirname(d) }.uniq
71
+ .map do |dir|
72
+ "command -v gtk-update-icon-cache >/dev/null 2>&1 && " \
73
+ "gtk-update-icon-cache -q -t -f /#{dir} || :"
74
+ end
75
+ return '' if commands.empty?
76
+
77
+ body = commands.join("\n")
78
+ "%post\n#{body}\n\n%postun\n#{body}\n\n"
79
+ end
80
+
57
81
  # %files entries: %dir for every directory so we never claim ownership
58
82
  # of shared paths like /usr or /usr/local.
59
83
  def self.spec_entries(files:, symlinks:)
@@ -64,9 +64,10 @@ module Crosspack
64
64
 
65
65
  # shortcuts: nil, or { name: "Display Name", target: "app.exe" } (the
66
66
  # payload file inside INSTALLFOLDER the shortcuts launch) — installs
67
- # Start Menu and Desktop shortcuts.
67
+ # Start Menu and Desktop shortcuts. icon: local path to an .ico for
68
+ # the Add/Remove Programs entry (ARPPRODUCTICON), or nil.
68
69
  def self.generate_wxs(name:, version:, manufacturer:, summary:, files:,
69
- arch: 'x64', shortcuts: nil)
70
+ arch: 'x64', shortcuts: nil, icon: nil)
70
71
  license_rtf_file = "#{name}-license.rtf"
71
72
  name = escape(name)
72
73
  manufacturer = escape(sanitize_manufacturer(manufacturer))
@@ -86,6 +87,7 @@ module Crosspack
86
87
  shortcut_xml = shortcuts ? shortcuts_fragment(shortcuts) : nil
87
88
  feature_refs = ["<ComponentGroupRef Id=\"Payload\" />"]
88
89
  feature_refs << '<ComponentGroupRef Id="Shortcuts" />' if shortcuts
90
+ arp_icon = icon ? arp_icon_fragment(icon) : nil
89
91
 
90
92
  <<~WXS
91
93
  <?xml version="1.0" encoding="UTF-8"?>
@@ -103,7 +105,7 @@ module Crosspack
103
105
 
104
106
  <Property Id="WixUILicenseRtf" Value="#{license_rtf_file}" />
105
107
  <ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
106
-
108
+ #{arp_icon}
107
109
  <Feature Id="Main" Title="#{name}" Level="1">
108
110
  #{feature_refs.map { |r| " #{r}" }.join("\n")}
109
111
  </Feature>
@@ -147,7 +149,7 @@ module Crosspack
147
149
  # the output and, when WiX is installed, builds a real MSI. Returns
148
150
  # the path of whatever was produced.
149
151
  def self.build(name:, version:, manufacturer:, summary:, files:, output:,
150
- arch: 'x64', shortcuts: nil, license: nil)
152
+ arch: 'x64', shortcuts: nil, license: nil, icon: nil)
151
153
  arch = { 'x86_64' => 'x64', 'aarch64' => 'arm64' }.fetch(arch, arch)
152
154
  missing = files.reject { |src, _dst| File.file?(src) }
153
155
  unless missing.empty?
@@ -163,9 +165,11 @@ module Crosspack
163
165
  File.write(wxs_path, generate_wxs(name: name, version: version,
164
166
  manufacturer: manufacturer,
165
167
  summary: summary, files: files,
166
- arch: arch, shortcuts: shortcuts))
168
+ arch: arch, shortcuts: shortcuts,
169
+ icon: icon))
167
170
  File.write(File.join(out_dir, "#{name}-license.rtf"),
168
171
  license_rtf(license || summary))
172
+ FileUtils.cp(icon, File.join(out_dir, File.basename(icon))) if icon
169
173
 
170
174
  wix = wix_command
171
175
  unless wix
@@ -202,6 +206,14 @@ module Crosspack
202
206
  nil
203
207
  end
204
208
 
209
+ # The Add/Remove Programs entry shows this icon (ARP/Publisher uses
210
+ # the MSI Icons table, not the installed files).
211
+ def self.arp_icon_fragment(icon)
212
+ icon_file = File.basename(icon)
213
+ " <Property Id=\"ARPPRODUCTICON\" Value=\"AppIcon\" />\n" \
214
+ " <Icon Id=\"AppIcon\" SourceFile=\"#{escape(icon_file)}\" />"
215
+ end
216
+
205
217
  # The WixUI dialogs live in the UI extension, so it must be installed
206
218
  # before `wix build -ext WixToolset.UI.wixext` can resolve it. A no-op
207
219
  # when it is already present.
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
+ # Host-only launch commands: `crosspack run [goal]` (build the goal's
5
+ # host-buildable entries — default goal: `default` —, then launch the
6
+ # application's main binary) and `crosspack install` (launch the package
7
+ # packed for this host). Both resolve the current host target, so they
8
+ # take no package-target argument and are meaningless on any other system.
8
9
  #
9
10
  # Commands may be overridden per host in crosspack.yml (run:/install:
10
11
  # sections, selectors like the deps: rules of build:); placeholders:
@@ -15,11 +16,14 @@ module Crosspack
15
16
  class Launcher
16
17
  class Error < BuildError; end
17
18
 
18
- # Builds the host target first (the same stage `crosspack build` runs),
19
- # then launches the first package.executables binary from builds/.
19
+ # Builds the given goal's host-buildable entries first (the same stage
20
+ # `crosspack build <goal>` runs; default goal: `default`), then launches
21
+ # a package.executables binary from builds/ — the one named by the goal
22
+ # when it matches, otherwise the first.
20
23
  # Returns the launched process's exit code.
21
24
  def self.run(config, root: Dir.pwd, args: [], output_base: nil, version: nil,
22
- deps: true, host_string: nil, host_os: Crossbuild::Platform.os,
25
+ deps: true, goal: Crossbuild::BuildManifest::DEFAULT_GOAL, host_string: nil,
26
+ host_os: Crossbuild::Platform.os,
23
27
  host_arch: Crossbuild::Platform.arch, selectors: Crossbuild::Platform.selectors)
24
28
  config = Config.coerce(config, root: root)
25
29
  target = host_target('run', host_string, host_arch)
@@ -30,9 +34,12 @@ module Crosspack
30
34
  Crossbuild::Builder.new(build_manifest, root: root, output_base: output_base,
31
35
  version: version, deps: deps,
32
36
  host_os: host_os, host_arch: host_arch)
33
- .run(target: target.to_s)
37
+ .run(goal: goal)
34
38
 
35
- exe = pkg.executables_for(target).first
39
+ # A binary goal (`crosspack run helloworld`) launches that executable;
40
+ # a group goal or the default goal launches the first one.
41
+ executables = pkg.executables_for(target)
42
+ exe = executables.include?(goal) ? goal : executables.first
36
43
  if exe.nil?
37
44
  raise Error,
38
45
  'run: the package: section does not name the binary to launch — ' \
@@ -14,7 +14,7 @@ module Crosspack
14
14
  # no .so libraries and vice versa.
15
15
  class PackageManifest
16
16
  REQUIRED = %w[maintainer description sources prefix payload].freeze
17
- OPTIONAL = %w[name summary license section lib_dir executables links desktop icon].freeze
17
+ OPTIONAL = %w[name summary license section lib_dir executables links desktop icon min_macos].freeze
18
18
 
19
19
  # Payload selectors may name an os in addition to a family: every distro
20
20
  # family packs a linux target, so "linux:" covers deb/rpm/PKGBUILD
@@ -142,6 +142,12 @@ module Crosspack
142
142
  @data['icon']
143
143
  end
144
144
 
145
+ # Minimum macOS version for the .app bundle (LSMinimumSystemVersion);
146
+ # defaults to 11.0 in the builder.
147
+ def min_macos
148
+ @data['min_macos']
149
+ end
150
+
145
151
  private
146
152
 
147
153
  def validate
@@ -172,6 +178,7 @@ module Crosspack
172
178
  validate_string('description', nil)
173
179
  validate_string('sources', nil)
174
180
  validate_string('prefix', /\A[a-z0-9][a-z0-9+._\/-]*\z/)
181
+ validate_string('min_macos', /\A\d+(\.\d+)*\z/)
175
182
  validate_payload
176
183
  validate_executables
177
184
  validate_links
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'tmpdir'
5
+ require 'open3'
6
+ require 'digest'
5
7
 
6
8
  module Crosspack
7
9
  # Single entry point: takes a Config (the package: and deps: sections of
@@ -129,7 +131,7 @@ module Crosspack
129
131
  Type=Application
130
132
  Name=#{d['name'] || @pkg.name}
131
133
  Comment=#{d['comment'] || @pkg.summary}
132
- Exec=#{d['exec'] || @pkg.name}
134
+ Exec=#{d['exec'] || desktop_exec}
133
135
  Icon=#{@pkg.name}
134
136
  Terminal=false
135
137
  Categories=#{categories}
@@ -137,6 +139,19 @@ module Crosspack
137
139
  path
138
140
  end
139
141
 
142
+ # Absolute launcher path: a bare name only resolves when prefix/bin is
143
+ # in PATH (true for usr/local, not for custom prefixes). Prefer the
144
+ # bin/ symlink from links:, else the executable inside lib_dir.
145
+ def desktop_exec
146
+ link = @pkg.links.keys.find { |l| l.split('/').first == 'bin' }
147
+ return File.join('/', @pkg.prefix, link) if link
148
+
149
+ exe = @pkg.executables.first
150
+ return File.join('/', @pkg.prefix, @pkg.lib_dir, exe) if exe
151
+
152
+ @pkg.name
153
+ end
154
+
140
155
  def icon_source
141
156
  src = File.expand_path(@pkg.icon, @root)
142
157
  raise BuildError, "icon: file not found #{src}" unless File.file?(src)
@@ -206,12 +221,21 @@ module Crosspack
206
221
  # Same desktop integration as deb/rpm: the .desktop file and the icon
207
222
  # go to the same destinations; their sources are archive-relative
208
223
  # names (the icon under its own file name).
224
+ sources = @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:src_dir], n)] }
209
225
  files = @pkg.payload_for(@target).to_h { |n| [n, File.join(@pkg_data[:lib_dst], n)] }
210
226
  if @pkg.desktop
211
227
  files["#{@pkg.name}.desktop"] = File.join(@pkg.prefix, 'share', 'applications',
212
228
  "#{@pkg.name}.desktop")
229
+ sources["#{@pkg.name}.desktop"] = desktop_entry_path
213
230
  end
214
- files[File.basename(@pkg.icon)] = icon_destination if @pkg.icon
231
+ if @pkg.icon
232
+ files[File.basename(@pkg.icon)] = icon_destination
233
+ sources[File.basename(@pkg.icon)] = icon_source
234
+ end
235
+ sha256 = write_source_tarball(
236
+ File.join(File.dirname(output), "#{@pkg.name}-#{version}.tar.gz"),
237
+ "#{@pkg.name}-#{version}", sources
238
+ )
215
239
  Builders::Pkgbuild.generate(
216
240
  name: @pkg.name,
217
241
  version: version,
@@ -225,12 +249,37 @@ module Crosspack
225
249
  symlinks: @pkg_data[:symlinks],
226
250
  executables: @pkg_data[:executables],
227
251
  source: ["#{@pkg.name}-#{version}.tar.gz"],
252
+ sha256: sha256,
228
253
  license: @pkg.license,
229
254
  output: output
230
255
  )
231
256
  output
232
257
  end
233
258
 
259
+ # makepkg needs the source archive referenced by the PKGBUILD, and it
260
+ # must extract into <name>-<version>/ with the exact file names the
261
+ # install lines use. Crosspack builds it from the staged artifacts, so
262
+ # the PKGBUILD works out of the box (with a real sha256 instead of
263
+ # SKIP). Returns the hex digest.
264
+ def write_source_tarball(tarball, root, sources)
265
+ require 'tmpdir'
266
+ missing = sources.values.reject { |src| File.file?(src) }
267
+ unless missing.empty?
268
+ raise BuildError, "Source files for the PKGBUILD tarball not found:\n" \
269
+ "#{missing.map { |s| " ✗ #{s}" }.join("\n")}"
270
+ end
271
+
272
+ FileUtils.mkdir_p(File.dirname(tarball))
273
+ Dir.mktmpdir('crosspack-pkgbuild') do |stage|
274
+ root_dir = File.join(stage, root)
275
+ FileUtils.mkdir_p(root_dir)
276
+ sources.each_value { |src| FileUtils.cp(src, File.join(root_dir, File.basename(src))) }
277
+ out, status = Open3.capture2e('tar', '-czf', tarball, '-C', stage, root)
278
+ raise BuildError, "tar failed to build #{tarball}:\n#{out}" unless status.success?
279
+ end
280
+ Digest::SHA256.file(tarball).hexdigest
281
+ end
282
+
234
283
  def split_version
235
284
  parts = @version.split('-', 2)
236
285
  parts << '1' if parts.size == 1
@@ -252,11 +301,27 @@ module Crosspack
252
301
  license: @pkg.license,
253
302
  files: files,
254
303
  shortcuts: windows_shortcuts,
304
+ icon: windows_arp_icon,
255
305
  arch: @target.package_arch(:winget),
256
306
  output: output
257
307
  )
258
308
  end
259
309
 
310
+ # The ARP icon must be an .ico (the MSI Icons table does not take a
311
+ # png). The manifest icon is used when it is one; otherwise a sibling
312
+ # .ico with the same basename (appicon.png -> appicon.ico) is picked up
313
+ # when the build produces it. A png-only project gets no ARP icon — the
314
+ # exe icon still shows in Start Menu shortcuts.
315
+ def windows_arp_icon
316
+ return nil unless @pkg.icon
317
+
318
+ src = File.expand_path(@pkg.icon, @root)
319
+ return src if File.file?(src) && File.extname(src).casecmp('.ico').zero?
320
+
321
+ sibling = File.join(File.dirname(src), "#{File.basename(src, '.*')}.ico")
322
+ File.file?(sibling) ? sibling : nil
323
+ end
324
+
260
325
  # Shortcut facts for the MSI: the display name from desktop: (falling
261
326
  # back to the package name) and the first executable as the target.
262
327
  def windows_shortcuts
@@ -281,6 +346,7 @@ module Crosspack
281
346
  files: files,
282
347
  executables: @pkg.executables_for(@target),
283
348
  icon: @pkg.icon && File.expand_path(@pkg.icon, @root),
349
+ min_macos: @pkg.min_macos,
284
350
  output: output_dir
285
351
  )
286
352
  end
@@ -81,11 +81,28 @@ module Crosspack
81
81
  fallback = rule['*']
82
82
  return fallback unless fallback.nil?
83
83
 
84
- versions = rule.keys.map { |v| v == '*' ? '"*" (any)' : v.inspect }.join(', ')
85
- raise ResolveError,
86
- "#{name}: no rules for #{family} #{target.version.inspect} and no \"*\" key. " \
87
- "Versions described in the file: #{versions}. Add targets.#{family}.\"#{target.version}\" " \
88
- 'or a "*" wildcard rule'
84
+ newest = newest_version_key(rule.keys)
85
+ if newest.nil?
86
+ versions = rule.keys.map { |v| v == '*' ? '"*" (any)' : v.inspect }.join(', ')
87
+ raise ResolveError,
88
+ "#{name}: no rules for #{family} #{target.version.inspect} and no \"*\" key. " \
89
+ "Versions described in the file: #{versions}. Add targets.#{family}.\"#{target.version}\" " \
90
+ 'or a "*" wildcard rule'
91
+ end
92
+
93
+ warn Colors.yellow(
94
+ "#{name}: no recipe for #{family} #{target.version.inspect} — " \
95
+ "using the rules of the newest described version #{newest.inspect}. " \
96
+ "Add targets.#{family}.\"#{target.version}\" or a \"*\" wildcard to pin it explicitly"
97
+ )
98
+ rule[newest]
99
+ end
100
+
101
+ # The newest version key among the described ones ("24.10" beats "24.4",
102
+ # which beats "9"), comparing dot-separated numeric segments.
103
+ def newest_version_key(keys)
104
+ keys.reject { |k| k == '*' }
105
+ .max_by { |k| k.split('.').map(&:to_i) }
89
106
  end
90
107
  end
91
108
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crosspack
4
- VERSION = '0.7.3'
4
+ VERSION = '0.9.0'
5
5
  end
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.7.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Orlov
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
89
  requirements: []
90
- rubygems_version: 3.6.9
90
+ rubygems_version: 3.6.7
91
91
  specification_version: 4
92
92
  summary: Painless packaging for any platform
93
93
  test_files: []