native-packages 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +86 -0
- data/README.md +52 -113
- data/docs/404.md +12 -0
- data/docs/_guides/apple-notarization.md +116 -0
- data/docs/_guides/building-packages.md +190 -0
- data/docs/_guides/distribution-recipes.md +192 -0
- data/docs/_guides/getting-started.md +184 -0
- data/docs/_guides/github-actions.md +146 -0
- data/docs/_guides/multi-platform.md +153 -0
- data/docs/_guides/native-recipes.md +189 -0
- data/docs/_guides/prereleases.md +72 -0
- data/docs/_guides/publishing.md +143 -0
- data/docs/_guides/release-signing.md +137 -0
- data/docs/_reference/commands.md +206 -0
- data/docs/_reference/configuration.md +323 -0
- data/docs/_reference/platforms.md +104 -0
- data/docs/_reference/troubleshooting.md +172 -0
- data/docs/apple-notarization.md +2 -87
- data/docs/assets/images/logo.svg +27 -0
- data/docs/configuration.md +2 -123
- data/docs/index.md +41 -0
- data/docs/native-recipes.md +3 -134
- data/docs/platforms.md +3 -32
- data/docs/releasing.md +1 -1
- data/examples/native-packages-all-formats.yaml +1 -1
- data/examples/native-packages.yaml +1 -1
- data/lib/native_packages/build.rb +7 -7
- data/lib/native_packages/cli.rb +25 -2
- data/lib/native_packages/configuration.rb +2 -2
- data/lib/native_packages/release_signing.rb +106 -0
- data/lib/native_packages/support.rb +1 -1
- metadata +22 -3
|
@@ -85,7 +85,7 @@ module NativePackages
|
|
|
85
85
|
configuration.validate
|
|
86
86
|
selected = configuration.select(ids: ids, formats: formats)
|
|
87
87
|
number = version(value, release)
|
|
88
|
-
configuration.check_prerelease(number, selected)
|
|
88
|
+
configuration.check_prerelease(number, selected, defer_recipes: defer_recipes)
|
|
89
89
|
if release && selected.values.any? { |target| target["native"] }
|
|
90
90
|
raise Error, "native recipes consume local prepared directories; use --version with native build artifacts"
|
|
91
91
|
end
|
|
@@ -330,16 +330,16 @@ module NativePackages
|
|
|
330
330
|
raise Error, "missing recipe outputs: #{missing.join(', ')}" unless missing.empty?
|
|
331
331
|
end
|
|
332
332
|
|
|
333
|
-
def verify(output, complete: true)
|
|
333
|
+
def verify(output, complete: true, ids: [])
|
|
334
334
|
output = Pathname.new(output).expand_path(root)
|
|
335
335
|
manifest = JSON.parse((output / "build.json").read)
|
|
336
336
|
raise Error, "unsupported build manifest" unless manifest.fetch("schema") == 1
|
|
337
337
|
raise Error, "build belongs to another project/configuration" unless manifest.fetch("name") == configuration.name && manifest.fetch("configuration") == configuration.digest
|
|
338
338
|
number = configuration.package_version(manifest.fetch("version"))
|
|
339
339
|
selected = configuration.select(ids: manifest.fetch("targets").keys)
|
|
340
|
-
configuration.check_prerelease(number, selected)
|
|
341
|
-
expected = configuration.
|
|
342
|
-
raise Error, "incomplete target set;
|
|
340
|
+
configuration.check_prerelease(number, selected, defer_recipes: manifest.dig("recipes", "state") == "deferred")
|
|
341
|
+
expected = configuration.select(ids: ids).transform_values { |target| target.fetch("formats") }
|
|
342
|
+
raise Error, "incomplete target set; supply every format for all requested targets before publishing" if complete && manifest.fetch("targets") != expected
|
|
343
343
|
expected_pairs = manifest.fetch("targets").flat_map { |id, formats| formats.map { |format| [id, format] } }.sort
|
|
344
344
|
actual_pairs = manifest.fetch("packages").map { |entry| entry.values_at("target", "format") }.sort
|
|
345
345
|
raise Error, "manifest packages do not match its target set" unless actual_pairs == expected_pairs
|
|
@@ -357,10 +357,10 @@ module NativePackages
|
|
|
357
357
|
manifest
|
|
358
358
|
end
|
|
359
359
|
|
|
360
|
-
def publish(output, destinations, body_file: nil)
|
|
360
|
+
def publish(output, destinations, body_file: nil, ids: [])
|
|
361
361
|
configuration.validate
|
|
362
362
|
output = Pathname.new(output).expand_path(root)
|
|
363
|
-
manifest = verify(output)
|
|
363
|
+
manifest = verify(output, ids: ids)
|
|
364
364
|
raise Error, "supply at least one --to destination" if destinations.empty?
|
|
365
365
|
destinations.each { |name| project.repositories.select(name) unless name == "github" }
|
|
366
366
|
if manifest.fetch("version").include?("-")
|
data/lib/native_packages/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "build"
|
|
4
4
|
require_relative "scaffold"
|
|
5
|
+
require_relative "release_signing"
|
|
5
6
|
|
|
6
7
|
module NativePackages
|
|
7
8
|
module CLI
|
|
@@ -15,9 +16,14 @@ module NativePackages
|
|
|
15
16
|
[--output DIRECTORY] [--dry-run] Choose output or inspect the plan
|
|
16
17
|
[--defer-recipes] Build targets without recipe assets/tools
|
|
17
18
|
notarize-macos DIRECTORY --output DIRECTORY Copy, sign and notarize portable Mac code
|
|
19
|
+
release-checksums DIRECTORY --output FILE Hash a flat directory of release artifacts
|
|
20
|
+
sign-checksums FILE --public-key HEX_FILE Sign verified checksums with Ed25519
|
|
21
|
+
[--key-env VARIABLE] Default: NATIVE_PACKAGES_SIGNING_KEY
|
|
22
|
+
verify-checksums FILE --public-key HEX_FILE Verify signature and artifact hashes
|
|
18
23
|
aggregate DIR... --output DIRECTORY Combine target builds before publishing
|
|
19
24
|
[--finalize-recipes] Generate recipes from deferred builds
|
|
20
25
|
publish --from DIRECTORY --to github,aur Publish a complete, verified build
|
|
26
|
+
[--target ID] Require exactly these complete targets
|
|
21
27
|
migrate [--dry-run] Convert packaging/project.yml
|
|
22
28
|
repositories List downstream destinations
|
|
23
29
|
stage TARGET DIRECTORY Stage prepared recipes
|
|
@@ -49,8 +55,13 @@ module NativePackages
|
|
|
49
55
|
flags.on("--help", "-h") { puts HELP; return }
|
|
50
56
|
flags.on("--output DIRECTORY") { |value| options[:output] = Pathname.new(value).expand_path(root) } if %w[build aggregate artifacts prepare notarize-macos].include?(command)
|
|
51
57
|
flags.on("--body-file FILE") { |value| options[:body_file] = Pathname.new(value).expand_path(root) } if command == "publish"
|
|
58
|
+
flags.on("--output FILE") { |value| options[:output] = Pathname.new(value).expand_path(root) } if command == "release-checksums"
|
|
59
|
+
if %w[sign-checksums verify-checksums].include?(command)
|
|
60
|
+
flags.on("--public-key FILE") { |value| options[:public_key] = Pathname.new(value).expand_path(root) }
|
|
61
|
+
end
|
|
62
|
+
flags.on("--key-env VARIABLE") { |value| options[:key_env] = value } if command == "sign-checksums"
|
|
63
|
+
flags.on("--target ID") { |value| options[:ids] << value } if %w[build doctor publish].include?(command)
|
|
52
64
|
if %w[build doctor].include?(command)
|
|
53
|
-
flags.on("--target ID") { |value| options[:ids] << value }
|
|
54
65
|
flags.on("--format FORMAT") { |value| options[:formats] << value }
|
|
55
66
|
flags.on("--release TAG") { |value| options[:release] = value }
|
|
56
67
|
flags.on("--defer-recipes") { options[:defer_recipes] = true }
|
|
@@ -77,11 +88,23 @@ module NativePackages
|
|
|
77
88
|
end
|
|
78
89
|
end
|
|
79
90
|
parser.parse!(arguments)
|
|
91
|
+
raise Error, "publish --target requires --from" if command == "publish" && !options[:from] && !options[:ids].empty?
|
|
80
92
|
arity = { "init" => 0..0, "migrate" => 0..0, "build" => 0..0, "doctor" => 0..0, "aggregate" => 1..,
|
|
93
|
+
"release-checksums" => 1..1, "sign-checksums" => 1..1, "verify-checksums" => 1..1,
|
|
81
94
|
"prepare" => 1..1, "check" => 1..1, "artifacts" => 1..1, "publish-release" => 2..2,
|
|
82
95
|
"notarize-macos" => 1..1, "repositories" => 0..0, "stage" => 2..2, "diff" => 1..1, "publish" => options[:from] ? 0..0 : 1..1,
|
|
83
96
|
"publish-aur" => 1..1, "status" => 0..1, "check-version" => 1..1, "validate" => 0..0 }[command]
|
|
84
97
|
raise Error, HELP unless arity&.cover?(arguments.length)
|
|
98
|
+
if %w[release-checksums sign-checksums verify-checksums].include?(command)
|
|
99
|
+
path = Pathname.new(arguments.first).expand_path(root)
|
|
100
|
+
if command == "release-checksums"
|
|
101
|
+
raise Error, "release-checksums requires --output" unless options[:output]
|
|
102
|
+
return ReleaseSigning.checksums(path, output: options.fetch(:output))
|
|
103
|
+
end
|
|
104
|
+
raise Error, "#{command} requires --public-key" unless options[:public_key]
|
|
105
|
+
return ReleaseSigning.sign(path, **options.slice(:public_key, :key_env)) if command == "sign-checksums"
|
|
106
|
+
return ReleaseSigning.verify(path, public_key: options.fetch(:public_key))
|
|
107
|
+
end
|
|
85
108
|
if command == "init"
|
|
86
109
|
raise Error, "init writes native-packages.yaml in the current directory" if config_path
|
|
87
110
|
return Scaffold.new(root).init(**options.slice(:interactive, :name, :input).merge(formats: options[:init_formats]))
|
|
@@ -104,7 +127,7 @@ module NativePackages
|
|
|
104
127
|
when "aggregate"
|
|
105
128
|
raise Error, "aggregate requires --output" unless options[:output]
|
|
106
129
|
return builder.aggregate(arguments, output: options.fetch(:output), finalize_recipes: options.fetch(:finalize_recipes, false))
|
|
107
|
-
when "publish" then return builder.publish(options.fetch(:from), options.fetch(:destinations), body_file: options[:body_file])
|
|
130
|
+
when "publish" then return builder.publish(options.fetch(:from), options.fetch(:destinations), body_file: options[:body_file], ids: options[:ids])
|
|
108
131
|
end
|
|
109
132
|
end
|
|
110
133
|
if configuration
|
|
@@ -56,10 +56,10 @@ module NativePackages
|
|
|
56
56
|
def package(target) = merge(data.fetch("nfpm"), target.fetch("nfpm", {}))
|
|
57
57
|
def package_version(value) = version_arg(value, prerelease: data.fetch("release").fetch("prereleases", false))
|
|
58
58
|
|
|
59
|
-
def check_prerelease(version, selected)
|
|
59
|
+
def check_prerelease(version, selected, defer_recipes: false)
|
|
60
60
|
return unless version.include?("-")
|
|
61
61
|
raise Error, "prerelease builds require release.prereleases: true" unless data.fetch("release")["prereleases"] == true
|
|
62
|
-
unless selected.values.all? { |target| (target.fetch("formats") - %w[deb rpm dmg inno]).empty? } && data.fetch("templates").empty?
|
|
62
|
+
unless selected.values.all? { |target| (target.fetch("formats") - %w[deb rpm dmg inno]).empty? } && (defer_recipes || data.fetch("templates").empty?)
|
|
63
63
|
raise Error, "prereleases support deb, rpm, dmg and inno only, without downstream recipes"
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module NativePackages
|
|
6
|
+
# Portable release manifests, independent of package format or CI provider.
|
|
7
|
+
# Ed25519 signs the exact file bytes (no prehash, encoding or canonicalization).
|
|
8
|
+
module ReleaseSigning
|
|
9
|
+
LIMIT = 1024 * 1024
|
|
10
|
+
PUBLIC_PREFIX = ["302a300506032b6570032100"].pack("H*").freeze
|
|
11
|
+
FILENAME = /\A[A-Za-z0-9][A-Za-z0-9._+\-]*\z/
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def checksums(directory, output:)
|
|
15
|
+
directory = Pathname.new(directory).realpath
|
|
16
|
+
output = Pathname.new(output).expand_path
|
|
17
|
+
raise Error, "invalid checksum filename" unless FILENAME.match?(output.basename.to_s)
|
|
18
|
+
raise Error, "checksum output must be inside the artifact directory" unless output.dirname.realpath == directory
|
|
19
|
+
raise Error, "refusing to overwrite checksum output" if output.exist? || output.symlink?
|
|
20
|
+
entries = directory.children.sort
|
|
21
|
+
raise Error, "no release artifacts" if entries.empty?
|
|
22
|
+
names = entries.map { |path| path.basename.to_s }
|
|
23
|
+
raise Error, "ambiguous artifact filenames" unless names.map(&:downcase).uniq.length == names.length
|
|
24
|
+
manifest = entries.map do |path|
|
|
25
|
+
name = path.basename.to_s
|
|
26
|
+
raise Error, "invalid artifact filename" unless FILENAME.match?(name)
|
|
27
|
+
raise Error, "signature already exists" if name == "#{output.basename}.sig"
|
|
28
|
+
regular_file!(path)
|
|
29
|
+
"#{Digest::SHA256.file(path).hexdigest} #{name}\n"
|
|
30
|
+
end.join
|
|
31
|
+
raise Error, "release manifest exceeds 1 MiB" if manifest.bytesize > LIMIT
|
|
32
|
+
write_new(output, manifest)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sign(manifest, public_key:, key_env: "NATIVE_PACKAGES_SIGNING_KEY")
|
|
36
|
+
raise Error, "invalid signing key variable name" unless /\A[A-Z][A-Z0-9_]*\z/.match?(key_env)
|
|
37
|
+
pem = ENV.delete(key_env)
|
|
38
|
+
raise Error, "missing signing key environment variable" if pem.nil? || pem.empty?
|
|
39
|
+
# No subprocess receives the private key, and no private file is created.
|
|
40
|
+
key = OpenSSL::PKey.read(pem, "")
|
|
41
|
+
trusted = read_public_key(public_key)
|
|
42
|
+
raise Error, "signing key does not match trusted Ed25519 public key" unless key.oid == "ED25519" && key.public_to_der == trusted.public_to_der
|
|
43
|
+
manifest = Pathname.new(manifest)
|
|
44
|
+
bytes = bounded_read(manifest, LIMIT)
|
|
45
|
+
verify_files(manifest, bytes)
|
|
46
|
+
signature = key.sign(nil, bytes)
|
|
47
|
+
raise Error, "could not verify generated signature" unless signature.bytesize == 64 && trusted.verify(nil, signature, bytes)
|
|
48
|
+
write_new(Pathname.new("#{manifest}.sig"), signature)
|
|
49
|
+
rescue OpenSSL::OpenSSLError
|
|
50
|
+
# Do not expose parser diagnostics from secret input.
|
|
51
|
+
raise Error, "could not use Ed25519 signing key"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def verify(manifest, public_key:)
|
|
55
|
+
manifest = Pathname.new(manifest)
|
|
56
|
+
bytes = bounded_read(manifest, LIMIT)
|
|
57
|
+
signature = bounded_read(Pathname.new("#{manifest}.sig"), 64)
|
|
58
|
+
trusted = read_public_key(public_key)
|
|
59
|
+
raise Error, "invalid release signature" unless signature.bytesize == 64 && trusted.verify(nil, signature, bytes)
|
|
60
|
+
verify_files(manifest, bytes)
|
|
61
|
+
true
|
|
62
|
+
rescue OpenSSL::OpenSSLError
|
|
63
|
+
raise Error, "invalid Ed25519 public key or release signature"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_public_key(path)
|
|
67
|
+
hex = bounded_read(Pathname.new(path), 128).strip
|
|
68
|
+
raise Error, "public key must be 32 bytes encoded as hex" unless /\A[0-9a-fA-F]{64}\z/.match?(hex)
|
|
69
|
+
OpenSSL::PKey.read(PUBLIC_PREFIX + [hex].pack("H*"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def verify_files(manifest, bytes)
|
|
73
|
+
raise Error, "empty release manifest" if bytes.empty?
|
|
74
|
+
seen = {}
|
|
75
|
+
bytes.each_line do |line|
|
|
76
|
+
match = /\A([0-9a-fA-F]{64}) ([A-Za-z0-9][A-Za-z0-9._+\-]*)\n\z/.match(line)
|
|
77
|
+
raise Error, "invalid release checksum entry" unless match
|
|
78
|
+
digest, name = match.captures
|
|
79
|
+
raise Error, "duplicate or self-referencing checksum entry" if seen[name.downcase] || [manifest.basename.to_s.downcase, "#{manifest.basename}.sig".downcase].include?(name.downcase)
|
|
80
|
+
seen[name.downcase] = true
|
|
81
|
+
path = manifest.dirname / name
|
|
82
|
+
regular_file!(path)
|
|
83
|
+
raise Error, "release artifact checksum mismatch: #{name}" unless Digest::SHA256.file(path).hexdigest == digest.downcase
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def regular_file!(path)
|
|
88
|
+
raise Error, "release input must be a regular file: #{path.basename}" unless path.lstat.file?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def bounded_read(path, limit)
|
|
92
|
+
regular_file!(path)
|
|
93
|
+
bytes = File.binread(path, limit + 1) || "".b
|
|
94
|
+
raise Error, "release metadata exceeds size limit" if bytes.bytesize > limit
|
|
95
|
+
bytes
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def write_new(path, bytes)
|
|
99
|
+
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o644) do |file|
|
|
100
|
+
file.binmode
|
|
101
|
+
file.write(bytes)
|
|
102
|
+
end
|
|
103
|
+
path
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: native-packages
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carmine Paolino
|
|
@@ -16,12 +16,30 @@ executables:
|
|
|
16
16
|
extensions: []
|
|
17
17
|
extra_rdoc_files: []
|
|
18
18
|
files:
|
|
19
|
+
- CONTRIBUTING.md
|
|
19
20
|
- LICENSE
|
|
20
21
|
- README.md
|
|
22
|
+
- docs/404.md
|
|
23
|
+
- docs/_guides/apple-notarization.md
|
|
24
|
+
- docs/_guides/building-packages.md
|
|
25
|
+
- docs/_guides/distribution-recipes.md
|
|
26
|
+
- docs/_guides/getting-started.md
|
|
27
|
+
- docs/_guides/github-actions.md
|
|
28
|
+
- docs/_guides/multi-platform.md
|
|
29
|
+
- docs/_guides/native-recipes.md
|
|
30
|
+
- docs/_guides/prereleases.md
|
|
31
|
+
- docs/_guides/publishing.md
|
|
32
|
+
- docs/_guides/release-signing.md
|
|
33
|
+
- docs/_reference/commands.md
|
|
34
|
+
- docs/_reference/configuration.md
|
|
35
|
+
- docs/_reference/platforms.md
|
|
36
|
+
- docs/_reference/troubleshooting.md
|
|
21
37
|
- docs/acceptance/0.5.0-notarization.md
|
|
22
38
|
- docs/apple-notarization.md
|
|
39
|
+
- docs/assets/images/logo.svg
|
|
23
40
|
- docs/cli-design.md
|
|
24
41
|
- docs/configuration.md
|
|
42
|
+
- docs/index.md
|
|
25
43
|
- docs/legacy.md
|
|
26
44
|
- docs/native-recipes.md
|
|
27
45
|
- docs/platforms.md
|
|
@@ -41,6 +59,7 @@ files:
|
|
|
41
59
|
- lib/native_packages/macos_signing.rb
|
|
42
60
|
- lib/native_packages/native_recipe.rb
|
|
43
61
|
- lib/native_packages/project.rb
|
|
62
|
+
- lib/native_packages/release_signing.rb
|
|
44
63
|
- lib/native_packages/repositories.rb
|
|
45
64
|
- lib/native_packages/scaffold.rb
|
|
46
65
|
- lib/native_packages/support.rb
|
|
@@ -64,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
64
83
|
- !ruby/object:Gem::Version
|
|
65
84
|
version: '0'
|
|
66
85
|
requirements: []
|
|
67
|
-
rubygems_version: 4.0.
|
|
86
|
+
rubygems_version: 4.0.20
|
|
68
87
|
specification_version: 4
|
|
69
|
-
summary:
|
|
88
|
+
summary: Build native application packages and publish release updates
|
|
70
89
|
test_files: []
|