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