native-packages 0.6.0 → 0.8.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 -121
- data/docs/404.md +12 -0
- data/docs/_guides/apple-notarization.md +116 -0
- data/docs/_guides/building-packages.md +194 -0
- data/docs/_guides/distribution-recipes.md +192 -0
- data/docs/_guides/getting-started.md +184 -0
- data/docs/_guides/github-actions.md +150 -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 +144 -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 -159
- data/docs/index.md +41 -0
- data/docs/native-recipes.md +3 -134
- data/docs/platforms.md +3 -32
- data/docs/releasing.md +14 -1
- data/examples/native-packages-all-formats.yaml +1 -1
- data/examples/native-packages.yaml +1 -1
- data/lib/native_packages/appimage.rb +163 -0
- data/lib/native_packages/build.rb +12 -1
- data/lib/native_packages/cli.rb +21 -0
- data/lib/native_packages/configuration.rb +8 -3
- data/lib/native_packages/inspection.rb +2 -0
- data/lib/native_packages/release_signing.rb +106 -0
- data/lib/native_packages/support.rb +1 -1
- metadata +23 -3
data/docs/platforms.md
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Supported platforms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This guide is now part of the documentation site:
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
| --- | --- | --- |
|
|
7
|
-
| DEB | nFPM, ELF inspection, known dependency inference, release upload | Supported distro baseline, runtime-loaded libraries, services/assets and installation testing |
|
|
8
|
-
| RPM | Same, with RPM dependency names | Distribution-specific dependencies and compatibility |
|
|
9
|
-
| Arch package | nFPM `archlinux`, native filename, ELF inspection | Arch-compatible files and explicit dependencies |
|
|
10
|
-
| Alpine APK | nFPM `apk`, ELF/libc checks | Suitable musl/static inputs and Alpine dependencies |
|
|
11
|
-
| IPK | nFPM `ipk`, ELF checks, explicit ABI label | Device/distribution ABI, architecture and dependencies |
|
|
12
|
-
| MSIX | nFPM `msix`, PE architecture checks, `after_package` hook for native signing | Windows executables/DLLs, application identity, assets, capabilities and signing certificate |
|
|
13
|
-
| SRPM | nFPM `srpm`, source/spec input validation | A correct spec and sources, native rebuild testing |
|
|
14
|
-
| AUR | Template rendering, `.SRCINFO`, staged Git publication | Native PKGBUILD source/bin/git variants and build checks |
|
|
15
|
-
| Homebrew | Hash/render supplied formulae or casks and publish a tap | Completed macOS assets, native recipe logic |
|
|
16
|
-
| Nixpkgs/Gentoo/Void and similar repositories | Template rendering and configured Git/PR/MR publication | Native recipes, dependency hashes, distro checks and review |
|
|
17
|
-
| Flatpak | No build/publication adapter yet | Use flatpak-builder and native manifests/runtime/sandbox settings |
|
|
18
|
-
| Inno Setup | Native command adapter, PE inspection, copied input and final output hashes | Inno recipe/compiler, application version mapping, signing and installation tests |
|
|
19
|
-
| macOS DMG | Native command adapter, Mach-O inspection, automatic Developer ID signing/notarization with Apple credentials, final stapled hashes | Bundle, hdiutil recipe, Apple credentials/entitlements, update/rollback testing |
|
|
20
|
-
| Other Windows installers | No adapter for NSIS/WiX yet | Existing native tools and WinGet/Scoop integrations |
|
|
21
|
-
|
|
22
|
-
The IPK acceptance fixture targets OpenWrt 24.10.8. OpenWrt 25.12 switched to APK, so IPK is not the package format for every OpenWrt release. [OpenWrt 25.12 release notes](https://openwrt.org/releases/25.12/notes-25.12.0)
|
|
23
|
-
|
|
24
|
-
nFPM ends at constructing the package file, including its supported signing operations. It does not compile the application or host package indexes. [nFPM configuration](https://nfpm.goreleaser.com/docs/configuration/)
|
|
25
|
-
|
|
26
|
-
The Windows acceptance test uses Windows SDK SignTool through `after_package`. Windows rejected nFPM 2.47.0's built-in signature with `0x80096010` in the initial acceptance run; do not assume its signing configuration alone produces a Windows-installable result.
|
|
27
|
-
|
|
28
|
-
The CLI uses known ELF machine/class/endianness mappings and PE machine mappings. Unsupported inspection architectures fail explicitly. Data and source targets do not require an executable. The small DEB/RPM dependency table can be extended in configuration; other formats require explicit runtime dependencies. These checks do not detect every dynamically loaded library or establish compatibility with every version of a distribution.
|
|
29
|
-
|
|
30
|
-
CI builds fixtures in every format and defines native Linux install/upgrade/remove checks, an SRPM rebuild and a signed MSIX installation on Windows. A build manifest reports `installation: not-tested` unless such testing was actually performed for that application output. Fixture coverage is separate from app-specific validation.
|
|
31
|
-
|
|
32
|
-
The [acceptance run for v0.2 development](https://github.com/crmne/native-packages/actions/runs/34748247545) passed on Debian trixie, Fedora 41, Arch's base image, Alpine 3.24.0, OpenWrt 24.10.8 and the GitHub Windows runner. Linux binary tests use a small static executable; the SRPM rebuilds its C source, and the Windows package contains a compiled Windows executable. These fixtures test package construction and lifecycle, not the compatibility of every application's GUI or device integrations.
|
|
33
|
-
|
|
34
|
-
Version 0.1 and its pinned workflow retain their DEB/RPM behavior. The new formats are enabled through the v0.2 single-file configuration and `build` command. Existing v0.1 application configurations do not silently gain more output formats.
|
|
5
|
+
[Supported platforms](_reference/platforms.md).
|
data/docs/releasing.md
CHANGED
|
@@ -23,7 +23,7 @@ Update the gemspec, `NativePackages::VERSION`, example/version references and ch
|
|
|
23
23
|
Create a GitHub release for that commit:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
gh release create v0.
|
|
26
|
+
gh release create v0.8.0 --target main --title 'native-packages 0.8.0' --notes-file release-notes.md
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Adding `--draft` permits review before publication and does not trigger the publisher. The tag must match the exact gem version and point to a commit on `main`. Set `--prerelease` only for a prerelease gem version.
|
|
@@ -31,3 +31,16 @@ Adding `--draft` permits review before publication and does not trigger the publ
|
|
|
31
31
|
The release workflow verifies the tag, tested commit and prerelease setting, then runs the test workflow, including native package acceptance. It downloads the tested gem from the Ruby 4.0 job, publishes it to RubyGems and GitHub Packages, and attaches it to the GitHub release. It does not rebuild a different artifact after the tests.
|
|
32
32
|
|
|
33
33
|
If authentication is missing, leave the tested gem and release draft available until the secret is configured. Failed release jobs can be rerun after configuration; already-published gem versions are not overwritten. Never move an existing release tag to retry publication.
|
|
34
|
+
|
|
35
|
+
If the workflow itself needs a correction, fix it on `main` and dispatch the
|
|
36
|
+
corrected release workflow against the existing published tag:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
gh workflow run release.yml --ref main -f tag=v0.8.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Validation resolves the original tag to an exact commit, requires it to be on
|
|
43
|
+
`main`, and checks the existing release's version and prerelease state. Every
|
|
44
|
+
test and publication checkout uses that commit, not the newer workflow commit.
|
|
45
|
+
The gem is taken from that retry's tested Ruby 4.0 artifact. This permits a
|
|
46
|
+
workflow repair without moving a tag or publishing untested source.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NativePackages
|
|
4
|
+
# Builds a type 2 AppImage from the same contents nFPM packages: the files
|
|
5
|
+
# are laid out in an AppDir, packed with mksquashfs and appended to a pinned
|
|
6
|
+
# AppImage runtime. Libraries are not bundled; the AppImage uses the host's,
|
|
7
|
+
# as the deb and rpm packages do.
|
|
8
|
+
class AppImage
|
|
9
|
+
include Support
|
|
10
|
+
RUNTIME = "20251108"
|
|
11
|
+
RUNTIME_URL = "https://github.com/AppImage/type2-runtime/releases/download/#{RUNTIME}"
|
|
12
|
+
# nFPM architecture => [AppImage architecture, runtime SHA-256].
|
|
13
|
+
RUNTIMES = {
|
|
14
|
+
"amd64" => ["x86_64", "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d"],
|
|
15
|
+
"arm64" => ["aarch64", "00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444"],
|
|
16
|
+
"386" => ["i686", "e72ea0b140a0a16e680713238a6f30aad278b62c4ca17919c554864124515498"],
|
|
17
|
+
"arm7" => ["armhf", "e9060d37577b8a29914ec12d8740add24e19ff29012fb1fa0f60daf62db0688d"]
|
|
18
|
+
}.freeze
|
|
19
|
+
ICON_DIRECTORIES = %w[usr/share/icons usr/share/pixmaps].freeze
|
|
20
|
+
attr_reader :root
|
|
21
|
+
|
|
22
|
+
def initialize(root)
|
|
23
|
+
@root = root
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.architecture(target)
|
|
27
|
+
RUNTIMES.fetch(target.fetch("arch")) { raise Error, "appimage does not support #{target.fetch('arch')}; use #{RUNTIMES.keys.join(', ')}" }.first
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.filename(name, version, target) = "#{name}-#{version}-#{architecture(target)}.AppImage"
|
|
31
|
+
|
|
32
|
+
def doctor(target)
|
|
33
|
+
self.class.architecture(target)
|
|
34
|
+
missing = %w[mksquashfs curl].reject { |tool| available?(tool) }
|
|
35
|
+
raise Error, "install required AppImage tools: #{missing.join(', ')} (mksquashfs is in squashfs-tools)" unless missing.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def runtime(target)
|
|
39
|
+
architecture, digest = RUNTIMES.fetch(target.fetch("arch"))
|
|
40
|
+
path = root / ".cache/native-packages/appimage-runtime" / RUNTIME / "runtime-#{architecture}"
|
|
41
|
+
download("#{RUNTIME_URL}/runtime-#{architecture}", path)
|
|
42
|
+
unless sha256(path) == digest
|
|
43
|
+
path.delete
|
|
44
|
+
raise Error, "AppImage runtime checksum mismatch: runtime-#{architecture}"
|
|
45
|
+
end
|
|
46
|
+
path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the AppImage's path and what was put at the AppDir's root.
|
|
50
|
+
def build(package, target, destination, name:, version:, epoch:)
|
|
51
|
+
doctor(target)
|
|
52
|
+
output = destination / self.class.filename(name, version, target)
|
|
53
|
+
Dir.mktmpdir("native-packages-appimage-") do |directory|
|
|
54
|
+
appdir = Pathname.new(directory) / "AppDir"
|
|
55
|
+
appdir.mkpath
|
|
56
|
+
stage(package, appdir)
|
|
57
|
+
entry = integrate(appdir)
|
|
58
|
+
normalize(appdir, epoch)
|
|
59
|
+
image = Pathname.new(directory) / "filesystem.squashfs"
|
|
60
|
+
run "mksquashfs", appdir, image, "-root-owned", "-noappend", "-no-xattrs", "-no-progress", "-quiet", "-comp", "zstd",
|
|
61
|
+
env: { "SOURCE_DATE_EPOCH" => epoch.to_s }
|
|
62
|
+
File.open(output, "wb") do |file|
|
|
63
|
+
file.write(runtime(target).binread)
|
|
64
|
+
IO.copy_stream(image.to_s, file)
|
|
65
|
+
end
|
|
66
|
+
output.chmod(0o755)
|
|
67
|
+
check(output)
|
|
68
|
+
[output, entry.merge("runtime" => RUNTIME, "libraries" => "host")]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check(path)
|
|
73
|
+
header = path.binread(11)
|
|
74
|
+
raise Error, "not a type 2 AppImage: #{path}" unless header&.start_with?("\x7fELF".b) && header.byteslice(8, 3) == "AI\x02".b
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# nFPM overrides replace the contents list when one is supplied.
|
|
78
|
+
def contents(package)
|
|
79
|
+
package.fetch("overrides", {}).fetch("appimage", {}).fetch("contents", package.fetch("contents"))
|
|
80
|
+
.reject { |item| item["packager"] && item["packager"] != "appimage" }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def inside(appdir, destination)
|
|
84
|
+
raise Error, "appimage: content destinations must be absolute: #{destination}" unless destination.to_s.start_with?("/")
|
|
85
|
+
appdir / relative_path(destination.to_s.delete_prefix("/"))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stage(package, appdir)
|
|
89
|
+
contents(package).each do |item|
|
|
90
|
+
type = item.fetch("type", "file")
|
|
91
|
+
next if type == "ghost"
|
|
92
|
+
target = inside(appdir, item.fetch("dst"))
|
|
93
|
+
mode = item.dig("file_info", "mode")
|
|
94
|
+
case type
|
|
95
|
+
when "dir"
|
|
96
|
+
target.mkpath
|
|
97
|
+
when "symlink"
|
|
98
|
+
target.dirname.mkpath
|
|
99
|
+
File.symlink(item.fetch("src"), target)
|
|
100
|
+
when "file", "tree", "config", "config|noreplace", "config|missingok"
|
|
101
|
+
source = File.expand_path(item.fetch("src"), root)
|
|
102
|
+
paths = package["disable_globbing"] ? [source].select { |path| File.exist?(path) } : Dir.glob(source).sort
|
|
103
|
+
raise Error, "missing package content: #{source}" if paths.empty?
|
|
104
|
+
single = paths.length == 1 && File.file?(paths.first) && type != "tree"
|
|
105
|
+
paths.each do |path|
|
|
106
|
+
if File.directory?(path)
|
|
107
|
+
target.mkpath
|
|
108
|
+
FileUtils.cp_r(Dir.children(path).map { |child| File.join(path, child) }, target)
|
|
109
|
+
else
|
|
110
|
+
file = single ? target : target / File.basename(path)
|
|
111
|
+
file.dirname.mkpath
|
|
112
|
+
FileUtils.cp(path, file)
|
|
113
|
+
file.chmod(mode) if mode
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
raise Error, "appimage: unsupported content type #{type}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# AppImage's root needs the desktop entry, its icon and AppRun.
|
|
123
|
+
def integrate(appdir)
|
|
124
|
+
entries = Pathname.glob(appdir / "usr/share/applications/*.desktop")
|
|
125
|
+
raise Error, "appimage needs exactly one desktop entry in /usr/share/applications; found #{entries.length}" unless entries.length == 1
|
|
126
|
+
desktop = entries.first
|
|
127
|
+
fields = desktop.read.lines.each_with_object({}) do |line, result|
|
|
128
|
+
key, value = line.strip.split("=", 2)
|
|
129
|
+
result[key] ||= value if value && %w[Exec Icon].include?(key)
|
|
130
|
+
end
|
|
131
|
+
command = Shellwords.split(fields.fetch("Exec") { raise Error, "appimage: #{desktop.basename} has no Exec" }).first
|
|
132
|
+
executable = command.start_with?("/") ? inside(appdir, command) : appdir / "usr/bin" / relative_path(command)
|
|
133
|
+
raise Error, "appimage: #{desktop.basename} runs #{command}, which the package does not install" unless executable.file? && executable.executable?
|
|
134
|
+
name = fields.fetch("Icon") { raise Error, "appimage: #{desktop.basename} has no Icon" }
|
|
135
|
+
raise Error, "appimage: Icon must be a name, not a path" if name.include?("/")
|
|
136
|
+
icons = ICON_DIRECTORIES.flat_map { |path| Pathname.glob(appdir / path / "**" / "#{name}.{svg,png}") }
|
|
137
|
+
# A scalable icon first, else the largest bitmap.
|
|
138
|
+
icon = icons.find { |path| path.extname == ".svg" } || icons.max_by(&:size)
|
|
139
|
+
raise Error, "appimage: the package installs no #{name}.svg or #{name}.png icon" unless icon
|
|
140
|
+
FileUtils.cp(desktop, appdir / desktop.basename)
|
|
141
|
+
FileUtils.cp(icon, appdir / icon.basename)
|
|
142
|
+
File.symlink(icon.basename.to_s, appdir / ".DirIcon")
|
|
143
|
+
File.symlink(executable.relative_path_from(appdir).to_s, appdir / "AppRun")
|
|
144
|
+
{ "desktop" => desktop.basename.to_s, "icon" => icon.basename.to_s, "executable" => executable.relative_path_from(appdir).to_s }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The same bytes for the same inputs: fixed modes and times.
|
|
148
|
+
def normalize(appdir, epoch)
|
|
149
|
+
time = Time.at(epoch)
|
|
150
|
+
Pathname.glob(appdir / "**/*", File::FNM_DOTMATCH).each do |path|
|
|
151
|
+
next if path.basename.to_s == "."
|
|
152
|
+
if path.symlink?
|
|
153
|
+
File.lutime(time, time, path)
|
|
154
|
+
else
|
|
155
|
+
path.chmod(path.directory? || path.executable? ? 0o755 : 0o644)
|
|
156
|
+
File.utime(time, time, path)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
appdir.chmod(0o755)
|
|
160
|
+
File.utime(time, time, appdir)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -4,6 +4,7 @@ require_relative "configuration"
|
|
|
4
4
|
require_relative "inspection"
|
|
5
5
|
require_relative "native_recipe"
|
|
6
6
|
require_relative "macos_signing"
|
|
7
|
+
require_relative "appimage"
|
|
7
8
|
|
|
8
9
|
module NativePackages
|
|
9
10
|
class Build
|
|
@@ -31,7 +32,8 @@ module NativePackages
|
|
|
31
32
|
def doctor(ids: [], formats: [], release: false, defer_recipes: false)
|
|
32
33
|
configuration.validate
|
|
33
34
|
selected = configuration.select(ids: ids, formats: formats)
|
|
34
|
-
nfpm_version if selected.values.any? { |target| (target.fetch("formats")
|
|
35
|
+
nfpm_version if selected.values.any? { |target| !(target.fetch("formats") - Configuration::NATIVE_FORMATS - ["appimage"]).empty? }
|
|
36
|
+
selected.each_value { |target| AppImage.new(root).doctor(target) if target.fetch("formats").include?("appimage") }
|
|
35
37
|
selected.each do |id, target|
|
|
36
38
|
next unless target["native"]
|
|
37
39
|
metadata = configuration.target_tokens(configuration.tokens("9.8.7"), id, target, "/payload").merge("PACKAGE" => "/output/package", "FORMAT" => target.fetch("formats").first)
|
|
@@ -155,6 +157,14 @@ module NativePackages
|
|
|
155
157
|
packages = files(destination)
|
|
156
158
|
raise Error, "native recipe must create only its declared output" unless packages == [package_path]
|
|
157
159
|
native.check_output(package_path, format)
|
|
160
|
+
elsif format == "appimage"
|
|
161
|
+
package = render_tree(configuration.package(target), configuration.target_tokens(info, id, target, payload))
|
|
162
|
+
inspection = Inspection.new(root, configuration.data.fetch("libraries")).check(package, target, format)
|
|
163
|
+
package_path, details = AppImage.new(root).build(package, target, destination, name: configuration.name, version: number,
|
|
164
|
+
epoch: info.fetch("SOURCE_DATE_EPOCH"))
|
|
165
|
+
inspection["appimage"] = details
|
|
166
|
+
packages = files(destination)
|
|
167
|
+
raise Error, "AppImage build must create only its package" unless packages == [package_path]
|
|
158
168
|
else
|
|
159
169
|
package = render_tree(configuration.package(target), configuration.target_tokens(info, id, target, payload))
|
|
160
170
|
if number.include?("-") && (package.fetch("version_schema", "semver") != "semver" || %w[prerelease version_metadata].any? { |key| package.key?(key) && !package[key].to_s.empty? })
|
|
@@ -174,6 +184,7 @@ module NativePackages
|
|
|
174
184
|
hook_tokens = configuration.target_tokens(info, id, target, payload).merge("PACKAGE" => packages.first.to_s, "FORMAT" => format)
|
|
175
185
|
run(*render_tree(target.fetch("after_package"), hook_tokens), env: { "NATIVE_PACKAGES_TARGET" => id, "NATIVE_PACKAGES_VERSION" => number })
|
|
176
186
|
raise Error, "after_package must preserve the package path and output set" unless files(destination) == packages
|
|
187
|
+
AppImage.new(root).check(packages.first) if format == "appimage"
|
|
177
188
|
end
|
|
178
189
|
if native
|
|
179
190
|
native.check_output(packages.first, format)
|
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,6 +16,10 @@ 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
|
|
@@ -50,6 +55,11 @@ module NativePackages
|
|
|
50
55
|
flags.on("--help", "-h") { puts HELP; return }
|
|
51
56
|
flags.on("--output DIRECTORY") { |value| options[:output] = Pathname.new(value).expand_path(root) } if %w[build aggregate artifacts prepare notarize-macos].include?(command)
|
|
52
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"
|
|
53
63
|
flags.on("--target ID") { |value| options[:ids] << value } if %w[build doctor publish].include?(command)
|
|
54
64
|
if %w[build doctor].include?(command)
|
|
55
65
|
flags.on("--format FORMAT") { |value| options[:formats] << value }
|
|
@@ -80,10 +90,21 @@ module NativePackages
|
|
|
80
90
|
parser.parse!(arguments)
|
|
81
91
|
raise Error, "publish --target requires --from" if command == "publish" && !options[:from] && !options[:ids].empty?
|
|
82
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,
|
|
83
94
|
"prepare" => 1..1, "check" => 1..1, "artifacts" => 1..1, "publish-release" => 2..2,
|
|
84
95
|
"notarize-macos" => 1..1, "repositories" => 0..0, "stage" => 2..2, "diff" => 1..1, "publish" => options[:from] ? 0..0 : 1..1,
|
|
85
96
|
"publish-aur" => 1..1, "status" => 0..1, "check-version" => 1..1, "validate" => 0..0 }[command]
|
|
86
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
|
|
87
108
|
if command == "init"
|
|
88
109
|
raise Error, "init writes native-packages.yaml in the current directory" if config_path
|
|
89
110
|
return Scaffold.new(root).init(**options.slice(:interactive, :name, :input).merge(formats: options[:init_formats]))
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "project"
|
|
4
|
+
require_relative "appimage"
|
|
4
5
|
|
|
5
6
|
module NativePackages
|
|
6
7
|
class Configuration
|
|
7
8
|
include Support
|
|
8
9
|
NATIVE_FORMATS = %w[dmg inno].freeze
|
|
9
|
-
FORMATS = (%w[deb rpm archlinux apk ipk msix srpm] + NATIVE_FORMATS).freeze
|
|
10
|
+
FORMATS = (%w[deb rpm archlinux apk ipk msix srpm appimage] + NATIVE_FORMATS).freeze
|
|
10
11
|
NFPM_VERSION = "2.47.0"
|
|
11
12
|
NAMES = %w[native-packages.yaml native-packages.yml].freeze
|
|
12
13
|
KEYS = %w[schema tool nfpm targets release assets templates repositories revisions libraries version_file version_section].freeze
|
|
@@ -59,8 +60,8 @@ module NativePackages
|
|
|
59
60
|
def check_prerelease(version, selected, defer_recipes: false)
|
|
60
61
|
return unless version.include?("-")
|
|
61
62
|
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? } && (defer_recipes || data.fetch("templates").empty?)
|
|
63
|
-
raise Error, "prereleases support deb, rpm, dmg and inno only, without downstream recipes"
|
|
63
|
+
unless selected.values.all? { |target| (target.fetch("formats") - %w[deb rpm appimage dmg inno]).empty? } && (defer_recipes || data.fetch("templates").empty?)
|
|
64
|
+
raise Error, "prereleases support deb, rpm, appimage, dmg and inno only, without downstream recipes"
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
@@ -121,6 +122,10 @@ module NativePackages
|
|
|
121
122
|
if kind == "binary" && target["platform"] == "linux"
|
|
122
123
|
raise Error, "#{id}.libc: declare glibc, musl or static" unless %w[glibc musl static].include?(target["libc"])
|
|
123
124
|
end
|
|
125
|
+
if formats.include?("appimage")
|
|
126
|
+
raise Error, "#{id}: AppImage requires a Linux binary target" unless target["platform"] == "linux" && kind == "binary"
|
|
127
|
+
AppImage.architecture(target)
|
|
128
|
+
end
|
|
124
129
|
raise Error, "#{id}: IPK needs an explicit abi (device/distribution baseline)" if formats.include?("ipk") && target.fetch("abi", "").empty?
|
|
125
130
|
input = mapping(target.fetch("input"), "#{id}.input")
|
|
126
131
|
raise Error, "#{id}.input: declare local or release_asset" unless input["local"] || input["release_asset"]
|
|
@@ -115,6 +115,8 @@ module NativePackages
|
|
|
115
115
|
depends << (format == "deb" ? "libc6 (>= #{floor})" : "glibc >= #{floor}")
|
|
116
116
|
end
|
|
117
117
|
override["depends"] = depends.uniq
|
|
118
|
+
elsif format == "appimage"
|
|
119
|
+
# Nothing is bundled: the host supplies these, as for deb and rpm.
|
|
118
120
|
elsif !external.empty? && package.fetch("overrides", {}).fetch(format, {}).fetch("depends", package.fetch("depends", [])).empty?
|
|
119
121
|
raise Error, "#{format}: declare dependencies for #{external.join(', ')}"
|
|
120
122
|
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.8.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
|
|
@@ -34,6 +52,7 @@ files:
|
|
|
34
52
|
- examples/packaging/repositories.yml
|
|
35
53
|
- exe/native-packages
|
|
36
54
|
- lib/native_packages.rb
|
|
55
|
+
- lib/native_packages/appimage.rb
|
|
37
56
|
- lib/native_packages/build.rb
|
|
38
57
|
- lib/native_packages/cli.rb
|
|
39
58
|
- lib/native_packages/configuration.rb
|
|
@@ -41,6 +60,7 @@ files:
|
|
|
41
60
|
- lib/native_packages/macos_signing.rb
|
|
42
61
|
- lib/native_packages/native_recipe.rb
|
|
43
62
|
- lib/native_packages/project.rb
|
|
63
|
+
- lib/native_packages/release_signing.rb
|
|
44
64
|
- lib/native_packages/repositories.rb
|
|
45
65
|
- lib/native_packages/scaffold.rb
|
|
46
66
|
- lib/native_packages/support.rb
|
|
@@ -64,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
64
84
|
- !ruby/object:Gem::Version
|
|
65
85
|
version: '0'
|
|
66
86
|
requirements: []
|
|
67
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.20
|
|
68
88
|
specification_version: 4
|
|
69
|
-
summary:
|
|
89
|
+
summary: Build native application packages and publish release updates
|
|
70
90
|
test_files: []
|