native-packages 0.4.0 → 0.5.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/README.md +6 -2
- data/docs/acceptance/0.5.0-notarization.md +30 -0
- data/docs/apple-notarization.md +86 -0
- data/docs/configuration.md +1 -1
- data/docs/legacy.md +1 -1
- data/docs/native-recipes.md +16 -13
- data/docs/platforms.md +2 -2
- data/examples/native-packages-all-formats.yaml +1 -1
- data/examples/native-packages.yaml +1 -1
- data/lib/native_packages/build.rb +54 -38
- data/lib/native_packages/cli.rb +7 -2
- data/lib/native_packages/macos_signing.rb +169 -0
- data/lib/native_packages/support.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccc727393fcd07bd511465e0b2c46b19679a387b6ffee081693231061424fbd9
|
|
4
|
+
data.tar.gz: 9a402ad593dd1035f1ab350e9cdc1a81292f0976303d6454c2ad9a3ecc53ae91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee7950fbfd3a6d8e8d934e2c51a2620e717644c30803d660617d3e44d9c2274819fdc37dd815208493244b553976df685d2d3ec2d129eede0301bb61f7dd80d5
|
|
7
|
+
data.tar.gz: a57e20ba12b60b53bcbe0b9e5d896fb874d2535ae73f6f24a03b067ef9310eb35133bd51654d29a58d6e2990577a7dd6d4d056bf796ed44905c00866a087d983
|
data/README.md
CHANGED
|
@@ -61,6 +61,10 @@ the application's existing packaging commands on macOS or Windows, using the
|
|
|
61
61
|
same verified build manifests as Linux packages. Native recipes consume prepared
|
|
62
62
|
directories and need no nFPM installation. The gem checks Mach-O/PE architecture,
|
|
63
63
|
preserves safe internal bundle links and hashes the output after signing hooks.
|
|
64
|
+
With complete Apple credentials, macOS DMG builds automatically sign their owned
|
|
65
|
+
app copy, notarize the image, staple and validate its ticket before recording
|
|
66
|
+
checksums. `notarize-macos INPUT --output OUTPUT` prepares signed, notarized
|
|
67
|
+
portable code for an application-owned archive. See [Apple notarization](docs/apple-notarization.md).
|
|
64
68
|
App compilation, installer policy and signing identities stay in the application
|
|
65
69
|
repository.
|
|
66
70
|
|
|
@@ -103,7 +107,7 @@ packaging:
|
|
|
103
107
|
needs: release
|
|
104
108
|
permissions:
|
|
105
109
|
contents: write
|
|
106
|
-
uses: crmne/native-packages/.github/workflows/package.yml@v0.
|
|
110
|
+
uses: crmne/native-packages/.github/workflows/package.yml@v0.5.0
|
|
107
111
|
with:
|
|
108
112
|
version: ${{ github.ref_name }}
|
|
109
113
|
publish: true
|
|
@@ -129,7 +133,7 @@ Migration combines `packaging/project.yml`, its nFPM definition and repository r
|
|
|
129
133
|
bundle install
|
|
130
134
|
bundle exec ruby -Ilib -e 'Dir["test/*_test.rb"].sort.each { |path| require_relative path }'
|
|
131
135
|
gem build native-packages.gemspec
|
|
132
|
-
ruby test/gem_install.rb native-packages-0.
|
|
136
|
+
ruby test/gem_install.rb native-packages-0.5.0.gem
|
|
133
137
|
```
|
|
134
138
|
|
|
135
139
|
Tests build real packages, inspect their payloads and exercise repository publication against local Git fixtures. CI additionally runs disposable Linux install/upgrade/remove checks, SRPM rebuilds and Windows MSIX acceptance. Runtime build manifests report installation as `not-tested`: CI fixture coverage is not a substitute for testing each application's packages.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Native Apple notarization acceptance, 2026-09-13
|
|
2
|
+
|
|
3
|
+
A private Mac test used the candidate 0.5.0 implementation with the owner's
|
|
4
|
+
Developer ID Application certificate and app-specific notarization credentials.
|
|
5
|
+
No application release was created or replaced.
|
|
6
|
+
|
|
7
|
+
- Verified the published Fastpotify v0.7.1 universal DMG against its release
|
|
8
|
+
checksum, then copied its app and notices into an isolated test input.
|
|
9
|
+
- The shared native build signed its owned copy, verified both arm64 and x86_64
|
|
10
|
+
Mach-O slices, packaged a new DMG, obtained Apple acceptance, and stapled and
|
|
11
|
+
validated the notarization ticket before recording the package hash.
|
|
12
|
+
- Gatekeeper accepted both the resulting DMG and its contained app with
|
|
13
|
+
`source=Notarized Developer ID`. The app binary ran `--version` successfully.
|
|
14
|
+
- The portable command signed a newly compiled ARM64 executable, companion dylib
|
|
15
|
+
and app bundle, submitted a temporary ZIP, and obtained Apple acceptance
|
|
16
|
+
(submission `6a096e26-0f47-409f-b15d-654ff2a32292`). The app's stapled ticket
|
|
17
|
+
validated and Gatekeeper accepted it. Both standalone and bundled executables
|
|
18
|
+
successfully called the signed dylib after packaging. Non-code fixture data
|
|
19
|
+
remained unchanged, and the returned tree was archived as a tarball.
|
|
20
|
+
|
|
21
|
+
All images were detached after inspection. The gem's temporary signing keychains
|
|
22
|
+
were deleted. Original source payloads were not signed in place. Unit coverage
|
|
23
|
+
also exercises absent/partial credentials, failed acceptance/stapling, private
|
|
24
|
+
error redaction, inside-out signing and existing after-package hooks on both Mac
|
|
25
|
+
and Windows, including Apple variables present on a Windows target.
|
|
26
|
+
|
|
27
|
+
This is actual Apple service and native Gatekeeper evidence on the available
|
|
28
|
+
Apple Silicon Mac. It is not an Intel execution test, a fresh-machine GUI test,
|
|
29
|
+
or validation of Fastpotify playback, RekordFlash inference/models, FastsApp
|
|
30
|
+
messaging or TonePush hardware. Those remain application acceptance work.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Apple signing and notarization
|
|
2
|
+
|
|
3
|
+
Since native-packages 0.5.0, a native macOS DMG build automatically signs and
|
|
4
|
+
notarizes when all six environment variables below are present. No variables
|
|
5
|
+
preserves existing local/unsigned behavior. Any incomplete set fails with the
|
|
6
|
+
missing variable names before packaging. Linux and Windows targets ignore these
|
|
7
|
+
Apple variables. `validate` and `build --dry-run` remain offline.
|
|
8
|
+
|
|
9
|
+
| Environment variable | Value |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `APPLE_CERTIFICATE_P12` | Base64-encoded Developer ID Application certificate and private key, exported as PKCS#12 |
|
|
12
|
+
| `APPLE_CERTIFICATE_PASSWORD` | Password protecting that export |
|
|
13
|
+
| `APPLE_SIGNING_IDENTITY` | Exact `Developer ID Application: Name (TEAMID)` identity |
|
|
14
|
+
| `APPLE_ID` | Apple Account email |
|
|
15
|
+
| `APPLE_TEAM_ID` | Developer Program Team ID |
|
|
16
|
+
| `APPLE_APP_PASSWORD` | Apple Account app-specific password |
|
|
17
|
+
|
|
18
|
+
Store these in CI secrets and expose them as environment variables only in the
|
|
19
|
+
trusted macOS release job. The gem does not fetch secrets from a password manager
|
|
20
|
+
or GitHub. A free Apple account cannot replace Developer Program membership and
|
|
21
|
+
a valid Developer ID Application certificate.
|
|
22
|
+
|
|
23
|
+
## Native DMG builds
|
|
24
|
+
|
|
25
|
+
Build the application's complete `.app` and prepare its configured native input
|
|
26
|
+
first, then run the normal command on macOS:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
native-packages --config native-packages.macos.yaml build \
|
|
30
|
+
--version 1.2.3 --target macos-universal --output dist/macos-packages
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The gem imports the certificate and notary credentials into a fresh, temporary
|
|
34
|
+
keychain. It never replaces the default keychain or its search list, and deletes
|
|
35
|
+
its keychain after success or failure. Credential values are excluded from
|
|
36
|
+
manifests and redacted from tool errors.
|
|
37
|
+
|
|
38
|
+
The input is copied and checked before signing. All Mach-O code and nested
|
|
39
|
+
frameworks/app bundles in the owned copy are signed from the inside out with a
|
|
40
|
+
secure timestamp and hardened runtime. Existing entitlements and requirements
|
|
41
|
+
are preserved; the gem does not invent exceptions such as disabling library
|
|
42
|
+
validation. The application remains responsible for any required entitlements.
|
|
43
|
+
|
|
44
|
+
Signing finishes **before `native.command`**. Its recipe must copy `@PAYLOAD@`
|
|
45
|
+
unchanged, create the configured DMG, and avoid replacing signatures with ad-hoc
|
|
46
|
+
ones. The original application input remains unchanged. `after_package` still
|
|
47
|
+
runs, then the gem signs the DMG, submits it with `notarytool`, waits for Apple's
|
|
48
|
+
`Accepted` result, staples and validates the ticket, and verifies the image.
|
|
49
|
+
Only then are package hashes and the build manifest written. The manifest's
|
|
50
|
+
package validation records Apple acceptance, submission ID and stapling.
|
|
51
|
+
|
|
52
|
+
Remove redundant Apple signing and notarization from application hooks when
|
|
53
|
+
adopting this flow. Existing Inno/MSIX signing hooks remain independent.
|
|
54
|
+
Submission waits up to 30 minutes; failure or timeout prevents publication of a
|
|
55
|
+
completed build. Apple may continue processing a timed-out submission.
|
|
56
|
+
|
|
57
|
+
## Portable macOS archives
|
|
58
|
+
|
|
59
|
+
A standalone executable cannot carry a stapled ticket, and Apple does not accept
|
|
60
|
+
`.tar.gz` as a submission. Prepare the entire portable tree, including binaries,
|
|
61
|
+
dylibs, models and notices, then use:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
native-packages notarize-macos portable-input \
|
|
65
|
+
--output signed/Example-1.2.3-macos-arm64
|
|
66
|
+
tar czf Example-1.2.3-macos-arm64.tar.gz \
|
|
67
|
+
-C signed Example-1.2.3-macos-arm64
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The destination must be new and outside the input directory. This command needs
|
|
71
|
+
no application configuration. With credentials, it signs only its copy, submits
|
|
72
|
+
that exact code through a temporary ZIP, waits for acceptance, and staples and
|
|
73
|
+
validates any `.app` bundles. It then publishes the signed directory atomically.
|
|
74
|
+
Archive only that returned tree; do not modify its signed contents. Without
|
|
75
|
+
credentials it copies the input unchanged and explicitly reports that signing
|
|
76
|
+
and notarization were skipped.
|
|
77
|
+
|
|
78
|
+
Standalone binaries rely on Gatekeeper's online ticket lookup. An accepted
|
|
79
|
+
portable submission does not claim offline stapling support for raw executables.
|
|
80
|
+
Notarization also does not replace app startup, model/GPU, entitlement, OS-version
|
|
81
|
+
or installation testing. Test a fresh downloaded package with Gatekeeper before
|
|
82
|
+
announcing the first notarized release.
|
|
83
|
+
|
|
84
|
+
Apple references: [Developer ID certificates](https://developer.apple.com/help/account/certificates/create-developer-id-certificates/),
|
|
85
|
+
[signing distribution code](https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac/),
|
|
86
|
+
[notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution).
|
data/docs/configuration.md
CHANGED
|
@@ -39,7 +39,7 @@ before_build: [./scripts/build-linux.sh, '@ARCH@']
|
|
|
39
39
|
|
|
40
40
|
It runs once per selected target in the configuration directory, before copying local inputs. `NATIVE_PACKAGES_TARGET` and `NATIVE_PACKAGES_VERSION` are also set. It is skipped in release mode and never executed by `validate`, `doctor` or a dry run. A shell script remains responsible for complex build operations.
|
|
41
41
|
|
|
42
|
-
`after_package` is an optional argument array invoked once per generated package, before output hashes are recorded. It additionally receives `@PACKAGE@` and `@FORMAT@`. Use it for existing native signing tools, for example a Windows SDK SignTool script. It runs in local and release mode and must preserve the output filename and file set. The MSIX acceptance test uses SignTool because Windows rejected the signature produced by nFPM 2.47.0's built-in signer in that test.
|
|
42
|
+
`after_package` is an optional argument array invoked once per generated package, before output hashes are recorded. For native DMG targets, automatic [Apple notarization](apple-notarization.md) runs after this hook; input signing completes before the native recipe. It additionally receives `@PACKAGE@` and `@FORMAT@`. Use it for existing native signing tools, for example a Windows SDK SignTool script. It runs in local and release mode and must preserve the output filename and file set. The MSIX acceptance test uses SignTool because Windows rejected the signature produced by nFPM 2.47.0's built-in signer in that test.
|
|
43
43
|
|
|
44
44
|
## Tokens and additional assets
|
|
45
45
|
|
data/docs/legacy.md
CHANGED
|
@@ -8,7 +8,7 @@ Applications keep their own configuration and templates. This repository holds t
|
|
|
8
8
|
|
|
9
9
|
| Layer | Responsibility |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| Application build jobs | Compile for each OS and architecture; create compatible binaries, macOS bundles and Windows installers;
|
|
11
|
+
| Application build jobs | Compile for each OS and architecture; create compatible binaries, macOS bundles and Windows installers; provide Apple credentials to shared native builds or retain their existing signing tools. |
|
|
12
12
|
| nFPM | Turn a set of built files and package metadata into native package files. |
|
|
13
13
|
| native-packages | Verify release checksums, render recipes, check Linux ELF dependencies, invoke nFPM, attach packaging assets, and stage or publish downstream updates. |
|
|
14
14
|
| Distribution infrastructure | Build and review submitted recipes, host package indexes, and make packages available to users. |
|
data/docs/native-recipes.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
# Native DMG and Inno recipes
|
|
1
|
+
# Native DMG and Inno recipes
|
|
2
2
|
|
|
3
3
|
The shared build command can coordinate existing macOS and Windows packaging
|
|
4
4
|
scripts. These are native command adapters, separate from nFPM. They do not
|
|
5
|
-
generate an installer recipe
|
|
6
|
-
automatically
|
|
5
|
+
generate an installer recipe or compile an app. Since 0.5.0, complete Apple
|
|
6
|
+
credentials automatically enable Developer ID signing and notarization for DMG
|
|
7
|
+
targets. The application supplies its identity, bundle contents and entitlements.
|
|
8
|
+
See [Apple notarization](apple-notarization.md).
|
|
7
9
|
|
|
8
10
|
```yaml
|
|
9
11
|
schema: 1
|
|
10
12
|
tool:
|
|
11
|
-
version: '0.
|
|
13
|
+
version: '0.5.0'
|
|
12
14
|
nfpm: '2.47.0'
|
|
13
15
|
nfpm:
|
|
14
16
|
name: example-app
|
|
@@ -55,9 +57,9 @@ the configuration for compatibility with mixed Linux/native builds.
|
|
|
55
57
|
|
|
56
58
|
`native.command` is a nonempty argument array, including `@PACKAGE@`, which
|
|
57
59
|
resolves to the exact output filename in a fresh directory. `@PAYLOAD@` is an
|
|
58
|
-
owned copy of the input directory's contents.
|
|
59
|
-
|
|
60
|
-
`Example.app` folder before calling hdiutil. Other existing target tokens apply.
|
|
60
|
+
owned copy of the input directory's contents. A top-level `.app` retains its basename; other input directories use `payload`.
|
|
61
|
+
A DMG script should copy the owned input into its own temporary
|
|
62
|
+
`Example.app` folder or image staging directory before calling hdiutil. Other existing target tokens apply.
|
|
61
63
|
Commands run in the application configuration directory and receive the same
|
|
62
64
|
target/version environment as other build hooks.
|
|
63
65
|
|
|
@@ -70,17 +72,18 @@ must check its own additional tools. Mac architecture inspection also needs
|
|
|
70
72
|
Apple's `lipo`. Supported Mac targets are `amd64`, `arm64` and `universal` (both).
|
|
71
73
|
|
|
72
74
|
Input copying preserves file modes and internal relative symlinks, including
|
|
73
|
-
framework links. Escaping, dangling and special-file entries fail. The staged
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
framework links. Escaping, dangling and special-file entries fail. The initial staged tree must match the original digest. Automatic Apple
|
|
76
|
+
signing then signs only that owned copy. The signed staged tree must remain
|
|
77
|
+
unchanged after application packaging and `after_package` hooks. Package only this staged input; do not read another application
|
|
76
78
|
build from a global location. Recipes should preserve existing destinations,
|
|
77
79
|
clean only their own temporary files and use argument arrays for subprocesses.
|
|
78
80
|
|
|
79
81
|
The command must create exactly its declared `.dmg` or `.exe` file. The adapter
|
|
80
82
|
checks a UDIF trailer or PE container respectively. This is a container check,
|
|
81
|
-
not proof of a valid installer, signature or application. The existing
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
not proof of a valid installer, signature or application. The existing `after_package` hook runs before automatic Apple DMG signing,
|
|
84
|
+
notarization and stapling, all before the final hash is recorded. Remove redundant
|
|
85
|
+
Apple signing/notarization hooks when enabling automatic credentials. Other
|
|
86
|
+
application-specific output hooks remain supported. Do not modify a package after building its manifest: signature
|
|
84
87
|
stapling changes bytes and must run inside that hook if the manifest is to verify.
|
|
85
88
|
|
|
86
89
|
Applications should invoke hdiutil's verification, platform signature checks and
|
data/docs/platforms.md
CHANGED
|
@@ -12,11 +12,11 @@ All seven nFPM 2.47.0 packagers are exposed by the v0.2 CLI. Each target explici
|
|
|
12
12
|
| MSIX | nFPM `msix`, PE architecture checks, `after_package` hook for native signing | Windows executables/DLLs, application identity, assets, capabilities and signing certificate |
|
|
13
13
|
| SRPM | nFPM `srpm`, source/spec input validation | A correct spec and sources, native rebuild testing |
|
|
14
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 | macOS
|
|
15
|
+
| Homebrew | Hash/render supplied formulae or casks and publish a tap | Completed macOS assets, native recipe logic |
|
|
16
16
|
| Nixpkgs/Gentoo/Void and similar repositories | Template rendering and configured Git/PR/MR publication | Native recipes, dependency hashes, distro checks and review |
|
|
17
17
|
| Flatpak | No build/publication adapter yet | Use flatpak-builder and native manifests/runtime/sandbox settings |
|
|
18
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,
|
|
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
20
|
| Other Windows installers | No adapter for NSIS/WiX yet | Existing native tools and WinGet/Scoop integrations |
|
|
21
21
|
|
|
22
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)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "configuration"
|
|
4
4
|
require_relative "inspection"
|
|
5
5
|
require_relative "native_recipe"
|
|
6
|
+
require_relative "macos_signing"
|
|
6
7
|
|
|
7
8
|
module NativePackages
|
|
8
9
|
class Build
|
|
@@ -35,6 +36,7 @@ module NativePackages
|
|
|
35
36
|
next unless target["native"]
|
|
36
37
|
metadata = configuration.target_tokens(configuration.tokens("9.8.7"), id, target, "/payload").merge("PACKAGE" => "/output/package", "FORMAT" => target.fetch("formats").first)
|
|
37
38
|
NativeRecipe.new(root).doctor(target, render_tree(target.fetch("native").fetch("command"), metadata))
|
|
39
|
+
MacosSigning.new(root).doctor if target["platform"] == "macos"
|
|
38
40
|
end
|
|
39
41
|
required = []
|
|
40
42
|
required << "readelf" if selected.values.any? { |target| target["platform"] == "linux" && target.fetch("kind", "binary") == "binary" }
|
|
@@ -129,52 +131,58 @@ module NativePackages
|
|
|
129
131
|
digest = native ? native.digest(source) : input_digest(source)
|
|
130
132
|
sources << { "target" => id, "path" => source.to_s, "sha256" => digest }
|
|
131
133
|
Dir.mktmpdir("native-packages-input-") do |directory|
|
|
132
|
-
payload = Pathname.new(directory) / "payload"
|
|
134
|
+
payload = Pathname.new(directory) / (native && source.extname == ".app" ? source.basename : "payload")
|
|
133
135
|
if native
|
|
134
136
|
FileUtils.cp_r(source, payload, preserve: true)
|
|
135
137
|
raise Error, "native input changed while staging" unless native.digest(payload) == digest
|
|
136
138
|
else
|
|
137
139
|
copy_input(source, payload, input.fetch("kind", "archive"))
|
|
138
140
|
end
|
|
139
|
-
target.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
signer = target["platform"] == "macos" ? MacosSigning.new(root) : nil
|
|
142
|
+
with_signing(signer) do |signing|
|
|
143
|
+
signing.sign_payload(payload) if signing
|
|
144
|
+
payload_digest = native && native.digest(payload)
|
|
145
|
+
target.fetch("formats").each do |format|
|
|
146
|
+
destination = staging / "packages" / id / format
|
|
147
|
+
destination.mkpath
|
|
148
|
+
if native
|
|
149
|
+
inspection = native.inspect(payload, target)
|
|
150
|
+
recipe = target.fetch("native")
|
|
151
|
+
tokens = configuration.target_tokens(info, id, target, payload)
|
|
152
|
+
package_path = destination / render(recipe.fetch("output"), tokens)
|
|
153
|
+
run(*render_tree(recipe.fetch("command"), tokens.merge("PACKAGE" => package_path.to_s, "FORMAT" => format)),
|
|
154
|
+
env: { "SOURCE_DATE_EPOCH" => info.fetch("SOURCE_DATE_EPOCH").to_s, "NATIVE_PACKAGES_TARGET" => id, "NATIVE_PACKAGES_VERSION" => number })
|
|
155
|
+
packages = files(destination)
|
|
156
|
+
raise Error, "native recipe must create only its declared output" unless packages == [package_path]
|
|
157
|
+
native.check_output(package_path, format)
|
|
158
|
+
else
|
|
159
|
+
package = render_tree(configuration.package(target), configuration.target_tokens(info, id, target, payload))
|
|
160
|
+
if number.include?("-") && (package.fetch("version_schema", "semver") != "semver" || %w[prerelease version_metadata].any? { |key| package.key?(key) && !package[key].to_s.empty? })
|
|
161
|
+
raise Error, "prerelease package versions must use unmodified nFPM semver conversion"
|
|
162
|
+
end
|
|
163
|
+
package.merge!("name" => configuration.name, "version" => number, "arch" => target.fetch("arch"),
|
|
164
|
+
"platform" => target.fetch("platform"), "mtime" => info.fetch("DATE"))
|
|
165
|
+
inspection = Inspection.new(root, configuration.data.fetch("libraries")).check(package, target, format)
|
|
166
|
+
config_path = Pathname.new(directory) / "nfpm.yaml"
|
|
167
|
+
write(config_path, YAML.dump(package))
|
|
168
|
+
run "nfpm", "package", "--config", config_path, "--packager", format, "--target", destination,
|
|
169
|
+
env: { "SOURCE_DATE_EPOCH" => info.fetch("SOURCE_DATE_EPOCH").to_s }
|
|
170
|
+
packages = files(destination)
|
|
171
|
+
raise Error, "nFPM did not create exactly one #{format} package" unless packages.length == 1
|
|
156
172
|
end
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
run(*render_tree(target.fetch("after_package"), signing), env: { "NATIVE_PACKAGES_TARGET" => id, "NATIVE_PACKAGES_VERSION" => number })
|
|
170
|
-
raise Error, "after_package must preserve the package path and output set" unless files(destination) == packages
|
|
171
|
-
end
|
|
172
|
-
if native
|
|
173
|
-
native.check_output(packages.first, format)
|
|
174
|
-
raise Error, "native recipes/signing must not change their input payload" unless native.digest(payload) == digest
|
|
173
|
+
if target["after_package"]
|
|
174
|
+
hook_tokens = configuration.target_tokens(info, id, target, payload).merge("PACKAGE" => packages.first.to_s, "FORMAT" => format)
|
|
175
|
+
run(*render_tree(target.fetch("after_package"), hook_tokens), env: { "NATIVE_PACKAGES_TARGET" => id, "NATIVE_PACKAGES_VERSION" => number })
|
|
176
|
+
raise Error, "after_package must preserve the package path and output set" unless files(destination) == packages
|
|
177
|
+
end
|
|
178
|
+
if native
|
|
179
|
+
native.check_output(packages.first, format)
|
|
180
|
+
raise Error, "native recipes/signing must not change their input payload" unless native.digest(payload) == payload_digest
|
|
181
|
+
end
|
|
182
|
+
inspection["apple"] = signing.notarize(packages.first) if signing
|
|
183
|
+
records << { "target" => id, "format" => format, "path" => packages.first.relative_path_from(staging).to_s,
|
|
184
|
+
"sha256" => sha256(packages.first), "validation" => inspection }
|
|
175
185
|
end
|
|
176
|
-
records << { "target" => id, "format" => format, "path" => packages.first.relative_path_from(staging).to_s,
|
|
177
|
-
"sha256" => sha256(packages.first), "validation" => inspection }
|
|
178
186
|
end
|
|
179
187
|
end
|
|
180
188
|
end
|
|
@@ -194,6 +202,14 @@ module NativePackages
|
|
|
194
202
|
output
|
|
195
203
|
end
|
|
196
204
|
|
|
205
|
+
def with_signing(signer)
|
|
206
|
+
if signer&.enabled?
|
|
207
|
+
signer.with_identity { |active| yield active }
|
|
208
|
+
else
|
|
209
|
+
yield nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
197
213
|
def release_cache(info)
|
|
198
214
|
repository = configuration.data.fetch("release").fetch("repository")
|
|
199
215
|
raise Error, "release.repository must be owner/repository" unless /\A[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\z/.match?(repository)
|
data/lib/native_packages/cli.rb
CHANGED
|
@@ -14,6 +14,7 @@ module NativePackages
|
|
|
14
14
|
[--target ID] [--format FORMAT] Select targets/formats (repeatable)
|
|
15
15
|
[--output DIRECTORY] [--dry-run] Choose output or inspect the plan
|
|
16
16
|
[--defer-recipes] Build targets without recipe assets/tools
|
|
17
|
+
notarize-macos DIRECTORY --output DIRECTORY Copy, sign and notarize portable Mac code
|
|
17
18
|
aggregate DIR... --output DIRECTORY Combine target builds before publishing
|
|
18
19
|
[--finalize-recipes] Generate recipes from deferred builds
|
|
19
20
|
publish --from DIRECTORY --to github,aur Publish a complete, verified build
|
|
@@ -46,7 +47,7 @@ module NativePackages
|
|
|
46
47
|
parser = OptionParser.new do |flags|
|
|
47
48
|
flags.on("--config FILE") { |value| config_path = value }
|
|
48
49
|
flags.on("--help", "-h") { puts HELP; return }
|
|
49
|
-
flags.on("--output DIRECTORY") { |value| options[:output] = Pathname.new(value).expand_path(root) } if %w[build aggregate artifacts prepare].include?(command)
|
|
50
|
+
flags.on("--output DIRECTORY") { |value| options[:output] = Pathname.new(value).expand_path(root) } if %w[build aggregate artifacts prepare notarize-macos].include?(command)
|
|
50
51
|
flags.on("--body-file FILE") { |value| options[:body_file] = Pathname.new(value).expand_path(root) } if command == "publish"
|
|
51
52
|
if %w[build doctor].include?(command)
|
|
52
53
|
flags.on("--target ID") { |value| options[:ids] << value }
|
|
@@ -78,7 +79,7 @@ module NativePackages
|
|
|
78
79
|
parser.parse!(arguments)
|
|
79
80
|
arity = { "init" => 0..0, "migrate" => 0..0, "build" => 0..0, "doctor" => 0..0, "aggregate" => 1..,
|
|
80
81
|
"prepare" => 1..1, "check" => 1..1, "artifacts" => 1..1, "publish-release" => 2..2,
|
|
81
|
-
"repositories" => 0..0, "stage" => 2..2, "diff" => 1..1, "publish" => options[:from] ? 0..0 : 1..1,
|
|
82
|
+
"notarize-macos" => 1..1, "repositories" => 0..0, "stage" => 2..2, "diff" => 1..1, "publish" => options[:from] ? 0..0 : 1..1,
|
|
82
83
|
"publish-aur" => 1..1, "status" => 0..1, "check-version" => 1..1, "validate" => 0..0 }[command]
|
|
83
84
|
raise Error, HELP unless arity&.cover?(arguments.length)
|
|
84
85
|
if command == "init"
|
|
@@ -86,6 +87,10 @@ module NativePackages
|
|
|
86
87
|
return Scaffold.new(root).init(**options.slice(:interactive, :name, :input).merge(formats: options[:init_formats]))
|
|
87
88
|
end
|
|
88
89
|
return Scaffold.new(root).migrate(dry_run: options[:dry_run]) if command == "migrate"
|
|
90
|
+
if command == "notarize-macos"
|
|
91
|
+
raise Error, "notarize-macos requires --output" unless options[:output]
|
|
92
|
+
return MacosSigning.new(root).prepare_directory(arguments.first, output: options.fetch(:output))
|
|
93
|
+
end
|
|
89
94
|
path = Configuration.discover(root, config_path)
|
|
90
95
|
configuration = path && Configuration.new(path)
|
|
91
96
|
if %w[build doctor aggregate].include?(command) || options[:from]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NativePackages
|
|
4
|
+
# Apple credentials opt native DMG builds into Developer ID distribution.
|
|
5
|
+
# Only the owned staging copy is signed; caller inputs and keychains survive.
|
|
6
|
+
class MacosSigning
|
|
7
|
+
include Support
|
|
8
|
+
VARIABLES = %w[APPLE_CERTIFICATE_P12 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_TEAM_ID APPLE_APP_PASSWORD].freeze
|
|
9
|
+
BUNDLES = %w[.app .framework .xpc .appex .bundle .plugin].freeze
|
|
10
|
+
attr_reader :root
|
|
11
|
+
|
|
12
|
+
def initialize(root, environment = ENV)
|
|
13
|
+
@root = root
|
|
14
|
+
@credentials = VARIABLES.to_h { |name| [name, environment[name].to_s] }
|
|
15
|
+
@redactions = @credentials.values.reject(&:empty?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def enabled? = @credentials.values.any? { |value| !value.empty? }
|
|
19
|
+
|
|
20
|
+
def doctor
|
|
21
|
+
return unless enabled?
|
|
22
|
+
missing = @credentials.select { |_, value| value.strip.empty? }.keys
|
|
23
|
+
raise Error, "incomplete Apple notarization credentials: configure #{missing.join(', ')}" unless missing.empty?
|
|
24
|
+
identity = @credentials.fetch("APPLE_SIGNING_IDENTITY")
|
|
25
|
+
team = @credentials.fetch("APPLE_TEAM_ID")
|
|
26
|
+
unless identity.start_with?("Developer ID Application: ") && identity.end_with?("(#{team})")
|
|
27
|
+
raise Error, "APPLE_SIGNING_IDENTITY must be a Developer ID Application identity for APPLE_TEAM_ID"
|
|
28
|
+
end
|
|
29
|
+
begin
|
|
30
|
+
@certificate = @credentials.fetch("APPLE_CERTIFICATE_P12").delete("\r\n").unpack1("m0")
|
|
31
|
+
raise ArgumentError if @certificate.empty?
|
|
32
|
+
rescue ArgumentError
|
|
33
|
+
raise Error, "APPLE_CERTIFICATE_P12 must contain a base64-encoded PKCS#12 certificate and private key"
|
|
34
|
+
end
|
|
35
|
+
missing_tools = %w[security codesign xcrun hdiutil ditto].reject { |tool| available?(tool) }
|
|
36
|
+
raise Error, "install required Apple tools: #{missing_tools.join(', ')}" unless missing_tools.empty?
|
|
37
|
+
execute "xcrun", "--find", "notarytool"
|
|
38
|
+
execute "xcrun", "--find", "stapler"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def with_identity
|
|
42
|
+
doctor
|
|
43
|
+
return yield(nil) unless enabled?
|
|
44
|
+
Dir.mktmpdir("native-packages-signing-") do |directory|
|
|
45
|
+
@keychain = Pathname.new(directory) / "signing.keychain-db"
|
|
46
|
+
certificate = Pathname.new(directory) / "certificate.p12"
|
|
47
|
+
File.write(certificate, @certificate, mode: "wb", perm: 0o600)
|
|
48
|
+
password = OpenSSL::Random.random_bytes(32).unpack1("H*")
|
|
49
|
+
@redactions << password
|
|
50
|
+
begin
|
|
51
|
+
execute "security", "create-keychain", "-p", password, @keychain
|
|
52
|
+
execute "security", "set-keychain-settings", "-lut", "21600", @keychain
|
|
53
|
+
execute "security", "unlock-keychain", "-p", password, @keychain
|
|
54
|
+
execute "security", "import", certificate, "-k", @keychain,
|
|
55
|
+
"-P", @credentials.fetch("APPLE_CERTIFICATE_PASSWORD"), "-T", "/usr/bin/codesign"
|
|
56
|
+
execute "security", "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-k", password, @keychain
|
|
57
|
+
identities = execute "security", "find-identity", "-v", "-p", "codesigning", @keychain
|
|
58
|
+
unless identities.include?(%Q{"#{@credentials.fetch("APPLE_SIGNING_IDENTITY")}"})
|
|
59
|
+
raise Error, "imported P12 has no valid private signing identity matching APPLE_SIGNING_IDENTITY"
|
|
60
|
+
end
|
|
61
|
+
execute "xcrun", "notarytool", "store-credentials", "native-packages", "--keychain", @keychain,
|
|
62
|
+
"--apple-id", @credentials.fetch("APPLE_ID"), "--team-id", @credentials.fetch("APPLE_TEAM_ID"),
|
|
63
|
+
"--password", @credentials.fetch("APPLE_APP_PASSWORD")
|
|
64
|
+
yield self
|
|
65
|
+
ensure
|
|
66
|
+
# Never replace the user's search list or default keychain. Deleting
|
|
67
|
+
# just this keychain also removes its own entry from the search list.
|
|
68
|
+
execute "security", "delete-keychain", @keychain if @keychain.exist?
|
|
69
|
+
@keychain = nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def sign_payload(payload)
|
|
75
|
+
tree = NativeRecipe.new(root).tree(payload)
|
|
76
|
+
paths = tree.filter_map do |relative, kind, *|
|
|
77
|
+
path = payload / relative
|
|
78
|
+
if kind == "file" && NativeRecipe::MACHO.include?(path.binread(4).unpack1("H*"))
|
|
79
|
+
path
|
|
80
|
+
elsif kind == "directory" && (BUNDLES.include?(path.extname) || (path / "Contents/Info.plist").file?)
|
|
81
|
+
path
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
raise Error, "Apple signing needs a payload containing Mach-O code" if paths.empty?
|
|
85
|
+
# Files first, then their containing code bundles, including frameworks.
|
|
86
|
+
paths.sort_by { |path| [-path.each_filename.count, path.to_s] }.each do |path|
|
|
87
|
+
metadata = path.directory? ? "entitlements,requirements" : "identifier,entitlements,requirements"
|
|
88
|
+
execute "codesign", "--force", "--timestamp", "--options", "runtime",
|
|
89
|
+
"--preserve-metadata=#{metadata}", "--keychain", @keychain,
|
|
90
|
+
"--sign", @credentials.fetch("APPLE_SIGNING_IDENTITY"), path
|
|
91
|
+
execute "codesign", "--verify", "--strict", path
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def notarize(package)
|
|
96
|
+
execute "hdiutil", "verify", package
|
|
97
|
+
execute "codesign", "--force", "--timestamp", "--keychain", @keychain,
|
|
98
|
+
"--sign", @credentials.fetch("APPLE_SIGNING_IDENTITY"), package
|
|
99
|
+
execute "codesign", "--verify", "--strict", package
|
|
100
|
+
response = submit(package)
|
|
101
|
+
execute "xcrun", "stapler", "staple", package
|
|
102
|
+
execute "xcrun", "stapler", "validate", package
|
|
103
|
+
execute "hdiutil", "verify", package
|
|
104
|
+
puts "Apple accepted #{package.basename}; its notarization ticket is stapled and validated."
|
|
105
|
+
{ "signature" => "developer-id", "notarization" => "accepted", "submission_id" => response.fetch("id"), "stapled" => true }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def submit(package)
|
|
109
|
+
puts "Submitting #{package.basename} to Apple's notary service..."
|
|
110
|
+
response = JSON.parse(execute("xcrun", "notarytool", "submit", package,
|
|
111
|
+
"--keychain-profile", "native-packages", "--keychain", @keychain,
|
|
112
|
+
"--wait", "--timeout", "30m", "--output-format", "json"))
|
|
113
|
+
unless response["status"] == "Accepted"
|
|
114
|
+
raise Error, "Apple notarization was not accepted (submission #{response.fetch('id', 'unknown')}); inspect the notarytool log before distributing"
|
|
115
|
+
end
|
|
116
|
+
response.fetch("id")
|
|
117
|
+
response
|
|
118
|
+
rescue JSON::ParserError, KeyError
|
|
119
|
+
raise Error, "Apple notary service returned an invalid result; no package was published"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A portable tarball cannot itself be submitted or stapled. Apple accepts
|
|
123
|
+
# the exact signed code through a temporary ZIP; the app owns the final tar.
|
|
124
|
+
def prepare_directory(source, output:)
|
|
125
|
+
source = Pathname.new(source).expand_path(root)
|
|
126
|
+
output = Pathname.new(output).expand_path(root)
|
|
127
|
+
raise Error, "output exists: #{output}; choose a fresh --output" if output.exist?
|
|
128
|
+
if output.to_s.start_with?(source.to_s + File::SEPARATOR)
|
|
129
|
+
raise Error, "signed output must be outside the input directory"
|
|
130
|
+
end
|
|
131
|
+
native = NativeRecipe.new(root)
|
|
132
|
+
original = native.digest(source)
|
|
133
|
+
raise Error, "macOS signing must run on macOS" if enabled? && NativeRecipe.host != "macos"
|
|
134
|
+
output.dirname.mkpath
|
|
135
|
+
Dir.mktmpdir(".native-packages-macos-", output.dirname) do |directory|
|
|
136
|
+
copied = Pathname.new(directory) / source.basename
|
|
137
|
+
FileUtils.cp_r(source, copied, preserve: true)
|
|
138
|
+
raise Error, "native input changed while staging" unless native.digest(copied) == original
|
|
139
|
+
with_identity do |signing|
|
|
140
|
+
if signing
|
|
141
|
+
sign_payload(copied)
|
|
142
|
+
archive = Pathname.new(directory) / "notarization.zip"
|
|
143
|
+
execute "ditto", "-c", "-k", "--keepParent", copied, archive
|
|
144
|
+
response = submit(archive)
|
|
145
|
+
native.tree(copied).each do |relative, kind, *|
|
|
146
|
+
bundle = copied / relative
|
|
147
|
+
next unless kind == "directory" && bundle.extname == ".app"
|
|
148
|
+
execute "xcrun", "stapler", "staple", bundle
|
|
149
|
+
execute "xcrun", "stapler", "validate", bundle
|
|
150
|
+
end
|
|
151
|
+
puts "Apple accepted portable code (submission #{response.fetch('id')}). Standalone executables use online ticket lookup."
|
|
152
|
+
else
|
|
153
|
+
puts "Apple credentials are absent; copying portable code without signing or notarization."
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
copied.rename(output)
|
|
157
|
+
end
|
|
158
|
+
output
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def execute(*arguments)
|
|
162
|
+
capture(*arguments)
|
|
163
|
+
rescue Error => error
|
|
164
|
+
message = error.message.b
|
|
165
|
+
@redactions.sort_by { |value| -value.bytesize }.each { |value| message.gsub!(value.b, "[REDACTED]") }
|
|
166
|
+
raise Error, message
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carmine Paolino
|
|
@@ -18,6 +18,8 @@ extra_rdoc_files: []
|
|
|
18
18
|
files:
|
|
19
19
|
- LICENSE
|
|
20
20
|
- README.md
|
|
21
|
+
- docs/acceptance/0.5.0-notarization.md
|
|
22
|
+
- docs/apple-notarization.md
|
|
21
23
|
- docs/cli-design.md
|
|
22
24
|
- docs/configuration.md
|
|
23
25
|
- docs/legacy.md
|
|
@@ -36,6 +38,7 @@ files:
|
|
|
36
38
|
- lib/native_packages/cli.rb
|
|
37
39
|
- lib/native_packages/configuration.rb
|
|
38
40
|
- lib/native_packages/inspection.rb
|
|
41
|
+
- lib/native_packages/macos_signing.rb
|
|
39
42
|
- lib/native_packages/native_recipe.rb
|
|
40
43
|
- lib/native_packages/project.rb
|
|
41
44
|
- lib/native_packages/repositories.rb
|