native-packages 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66eea8a982e99cb22fd0f06318941567630bf486cd78a90be6f119fca9112185
4
- data.tar.gz: 82acabb4632dd27e51088a8f2148a210cef3505d36f5ca46cee9a599bc5c96ed
3
+ metadata.gz: ef21c207476e3d19df85ebdb463eb0ccfb020ab8a4fd8ead551a22c4c2d4de62
4
+ data.tar.gz: 1648f4ffa1698dfdccea0d3693943e696b2810c69f595bf48fa01c386346808d
5
5
  SHA512:
6
- metadata.gz: bdb0d4c085aa2029bfd5fab260a028660a3c63ed0c87841700cca32c41db291394aa71c7ea73b287d862a8f86eceaba02ee4259ac9b64d13c78c0fa144a7060d
7
- data.tar.gz: b3700e1f354a72f46261a56817e01d186bb853a15dd5a98f01968adde00b464fc52f6a9b5b9f7cb7f1b089ad03a5096318eccd7d6d73323806b6637dcbafbe24
6
+ metadata.gz: f3cea3ca8d3d79956a0cca4ca779955b901795b1000a4ef57cad97abebfc32721d678df435ba8a46c2e226a210fdcb92f6c7cac586f81cc416ed5e8028ee300f
7
+ data.tar.gz: 2e6c2eaffc5cdab5cb3ef8ad3969b7058880cabb155f56170955f309d7b2e11701a384ac291f889cc5717d565b7073f1ec75edd51984b25554bad00e2564ec20
data/CONTRIBUTING.md CHANGED
@@ -18,7 +18,7 @@ To check the packaged gem and standalone command:
18
18
 
19
19
  ```sh
20
20
  gem build native-packages.gemspec
21
- ruby test/gem_install.rb native-packages-0.7.0.gem
21
+ ruby test/gem_install.rb native-packages-0.8.1.gem
22
22
  ```
23
23
 
24
24
  See [Releasing the gem](docs/releasing.md) for publication and
data/README.md CHANGED
@@ -48,7 +48,7 @@ record. Local builds work without a GitHub release or credentials.
48
48
 
49
49
  | Platform | Outputs |
50
50
  | --- | --- |
51
- | Linux | DEB, RPM, Arch Linux packages, Alpine APK, IPK, and source RPM |
51
+ | Linux | DEB, RPM, AppImage, Arch Linux packages, Alpine APK, IPK, and source RPM |
52
52
  | macOS | DMG, using your packaging script; optional Apple signing and notarization |
53
53
  | Windows | MSIX, or Inno Setup installers using your packaging script |
54
54
 
@@ -126,6 +126,10 @@ Both targets use the shared package metadata and file mappings. A target can
126
126
  have its own `nfpm` section to override them. Maps merge; lists replace the
127
127
  whole list, so a target-specific `contents` must list all of that target's files.
128
128
 
129
+ Add `appimage` to a Linux target's formats for a single file that runs without
130
+ installation. It needs one desktop entry and its icon among the contents; see
131
+ [AppImage](../_reference/platforms.md#appimage).
132
+
129
133
  Use a separate musl or static build for Alpine. Changing `formats` alone
130
134
  does not change which systems a binary can run on.
131
135
 
@@ -17,7 +17,7 @@ own compiled executable, or build the tiny example below.
17
17
  You need Ruby 3.2 or later. Install the version used by these guides:
18
18
 
19
19
  ```sh
20
- gem install native-packages --version 0.7.0
20
+ gem install native-packages --version 0.8.1
21
21
  ```
22
22
 
23
23
  For Linux packages, install **nFPM 2.47.0**, the helper native-packages uses to
@@ -79,7 +79,7 @@ Open the generated `native-packages.yaml` and replace its contents with:
79
79
  ```yaml
80
80
  schema: 1
81
81
  tool:
82
- version: '0.7.0'
82
+ version: '0.8.1'
83
83
  nfpm: '2.47.0'
84
84
 
85
85
  nfpm:
@@ -24,7 +24,7 @@ packaging:
24
24
  needs: release
25
25
  permissions:
26
26
  contents: write
27
- uses: crmne/native-packages/.github/workflows/package.yml@v0.7.0
27
+ uses: crmne/native-packages/.github/workflows/package.yml@v0.8.1
28
28
  with:
29
29
  version: ${{ github.ref_name }}
30
30
  publish: true
@@ -39,7 +39,7 @@ The configuration must include `release.repository` and a `release_asset`
39
39
  for each selected target. See [Publishing a release](publishing.md).
40
40
 
41
41
  Use the same native-packages release for the workflow and `tool.version`.
42
- For an immutable workflow reference, replace `v0.7.0` with that release's
42
+ For an immutable workflow reference, replace `v0.8.1` with that release's
43
43
  full commit SHA.
44
44
 
45
45
  ## Build without publishing
@@ -57,7 +57,7 @@ permissions:
57
57
  contents: read
58
58
  jobs:
59
59
  packaging:
60
- uses: crmne/native-packages/.github/workflows/package.yml@v0.7.0
60
+ uses: crmne/native-packages/.github/workflows/package.yml@v0.8.1
61
61
  ```
62
62
 
63
63
  Validation does not compile the app or check that future release inputs exist.
@@ -72,7 +72,7 @@ packaging:
72
72
  needs: build
73
73
  permissions:
74
74
  contents: write
75
- uses: crmne/native-packages/.github/workflows/package.yml@v0.7.0
75
+ uses: crmne/native-packages/.github/workflows/package.yml@v0.8.1
76
76
  with:
77
77
  version: '1.2.3'
78
78
  source-artifact: linux-binaries
@@ -125,10 +125,14 @@ GitHub repository:
125
125
  | Destination | Repository variable | Secrets |
126
126
  | --- | --- | --- |
127
127
  | AUR group `aur` | `PUBLISH_AUR=true` | `AUR_SSH_KEY`, `AUR_KNOWN_HOSTS` |
128
- | Destination or group `homebrew` | `PUBLISH_HOMEBREW=true` | `HOMEBREW_TAP_GITHUB_TOKEN` |
129
-
130
- These steps run only with `publish: true`. The Homebrew token needs write
131
- access to your tap; the AUR key needs access to the configured repositories.
128
+ | Destination or group `homebrew` | `PUBLISH_HOMEBREW=true` | `HOMEBREW_TAP_SSH_KEY` or `HOMEBREW_TAP_GITHUB_TOKEN` |
129
+
130
+ These steps run only with `publish: true`. For Homebrew, prefer a deploy key
131
+ with write access to the tap alone (`gh repo deploy-key add KEY.pub --repo
132
+ OWNER/homebrew-tap --allow-write`) stored as `HOMEBREW_TAP_SSH_KEY`: GitHub URLs in that step then go over SSH,
133
+ checked against GitHub's published host keys. A token
134
+ in `HOMEBREW_TAP_GITHUB_TOKEN` also works and needs write access to the tap.
135
+ The AUR key needs access to the configured repositories.
132
136
  Use `secrets: inherit` as in the release example, or pass the named secrets
133
137
  explicitly. If another tool already publishes a destination, keep one
134
138
  publisher responsible for it.
@@ -31,7 +31,7 @@ Here is a complete configuration for an ARM64 Mac app and an x86-64 Windows app:
31
31
  ```yaml
32
32
  schema: 1
33
33
  tool:
34
- version: '0.7.0'
34
+ version: '0.8.1'
35
35
  nfpm: '2.47.0'
36
36
  nfpm:
37
37
  name: hello
@@ -8,7 +8,7 @@ nav_order: 9
8
8
 
9
9
  Use a prerelease to let people test an upcoming version before the stable
10
10
  release. native-packages supports alpha, beta, and release candidate packages
11
- for **DEB, RPM, DMG, and Inno Setup**.
11
+ for **DEB, RPM, AppImage, DMG, and Inno Setup**.
12
12
 
13
13
  ## Enable preview versions
14
14
 
@@ -67,7 +67,7 @@ Package publication's separate `packaging-checksums.txt` is also unchanged.
67
67
 
68
68
  ## Attest in the application's build job
69
69
 
70
- The examples use `v0.7.0` for readability. For release workflows, replace it
70
+ The examples use `v0.8.1` for readability. For release workflows, replace it
71
71
  with that release's reviewed full commit SHA, not a mutable branch.
72
72
  After producing the final package, before uploading it, add this step to the
73
73
  job that built it:
@@ -79,7 +79,7 @@ permissions:
79
79
  attestations: write
80
80
  steps:
81
81
  # Existing checkout, build, platform signing and packaging steps go here.
82
- - uses: crmne/native-packages/.github/actions/attest@v0.7.0
82
+ - uses: crmne/native-packages/.github/actions/attest@v0.8.1
83
83
  with:
84
84
  subject-path: dist/*.tar.gz
85
85
  ```
@@ -108,7 +108,7 @@ and install Ruby 3.2 or later with Ed25519-capable OpenSSL before the signing
108
108
  step. Then call:
109
109
 
110
110
  ```yaml
111
- - uses: crmne/native-packages/.github/actions/sign-release@v0.7.0
111
+ - uses: crmne/native-packages/.github/actions/sign-release@v0.8.1
112
112
  with:
113
113
  directory: dist/release
114
114
  public-key: assets/update-public-key.hex
@@ -193,7 +193,7 @@ If several gem versions are installed, RubyGems lets you select the one required
193
193
  by the configuration:
194
194
 
195
195
  ```sh
196
- native-packages _0.7.0_ build --version 1.2.3
196
+ native-packages _0.8.1_ build --version 1.2.3
197
197
  ```
198
198
 
199
199
  Bundler also works if you prefer to manage the gem in a Gemfile.
@@ -28,7 +28,7 @@ expressions. Use the replacement values listed below.
28
28
  | Field | Purpose |
29
29
  | --- | --- |
30
30
  | `schema` | Required. Set to `1`. |
31
- | `tool.version` | Required. Exact native-packages version, such as `'0.7.0'`. |
31
+ | `tool.version` | Required. Exact native-packages version, such as `'0.8.1'`. |
32
32
  | `tool.nfpm` | Required. Set to `'2.47.0'`, including in native-only configurations. |
33
33
  | `nfpm` | Shared package metadata and installed-file mappings, or a relative YAML filename. Requires `name`. |
34
34
  | `targets` | Required. Named build targets; use `{}` for recipe-only projects. |
@@ -97,8 +97,8 @@ letters, digits, and hyphens, starting with a letter or digit.
97
97
  | `after_package` | Command arguments run on each package before its hash is recorded. |
98
98
  | `native` | Required for DMG/Inno. Contains `command` and `output`. |
99
99
 
100
- Formats are `deb`, `rpm`, `archlinux`, `apk`, `ipk`, `srpm`, `msix`, `dmg`,
101
- and `inno`. See [Supported platforms](platforms.md) for their requirements.
100
+ Formats are `deb`, `rpm`, `archlinux`, `apk`, `ipk`, `appimage`, `srpm`,
101
+ `msix`, `dmg`, and `inno`. See [Supported platforms](platforms.md) for their requirements.
102
102
 
103
103
  Windows targets select only `[msix]` or `[inno]`; macOS targets select `[dmg]`.
104
104
  DMG and Inno each require a separate binary target. SRPM requires a separate
@@ -19,6 +19,7 @@ libraries.
19
19
  | `archlinux` | Linux | Arch-compatible files and explicit dependencies. | nFPM, `readelf` for binaries. |
20
20
  | `apk` | Linux | Suitable Alpine files, usually musl or static binaries, and dependencies. | nFPM, `readelf` for binaries. |
21
21
  | `ipk` | Linux | Files for the device/distribution baseline, dependencies, and an `abi` label. | nFPM, `readelf` for binaries. |
22
+ | `appimage` | Linux | The same files as the DEB/RPM, including one desktop entry and its icon. | `mksquashfs` (squashfs-tools), `curl`, `readelf`. |
22
23
  | `srpm` | Linux source target | RPM spec and source files. | nFPM; native RPM tooling for rebuild tests. |
23
24
  | `msix` | Windows | Windows binaries, identity, application assets, and capabilities. | nFPM; Windows tools for signing and installation tests. |
24
25
  | `dmg` | macOS | Complete app and a DMG script. | macOS, `lipo`, and your script's tools, such as `hdiutil`. |
@@ -48,6 +49,45 @@ For IPK, check which format your device's distribution release actually uses.
48
49
  The repository's IPK fixture targets OpenWrt 24.10.8; its coverage should not
49
50
  be treated as a claim about every OpenWrt version.
50
51
 
52
+ ## AppImage
53
+
54
+ An AppImage is one executable file that runs without installation. Add
55
+ `appimage` to a Linux binary target's `formats`; it uses the same `contents`
56
+ as the other formats, so there is nothing else to describe:
57
+
58
+ ```yaml
59
+ linux-amd64:
60
+ platform: linux
61
+ arch: amd64
62
+ libc: glibc
63
+ formats: [deb, rpm, appimage]
64
+ ```
65
+
66
+ The package must install exactly one desktop entry under
67
+ `/usr/share/applications`. Its `Exec` names the program the AppImage runs
68
+ (found in `/usr/bin`, or an absolute path inside the package) and its `Icon`
69
+ names an SVG or PNG under `/usr/share/icons` or `/usr/share/pixmaps`. A
70
+ contents entry with `packager: appimage` is included only in the AppImage, and
71
+ one with another packager is left out. Entries of type `ghost` are skipped.
72
+
73
+ The output is `NAME-VERSION-ARCH.AppImage`, with AppImage's architecture names
74
+ (`x86_64`, `aarch64`, `i686`, `armhf` for `amd64`, `arm64`, `386`, `arm7`).
75
+ The file is the pinned AppImage runtime (type2-runtime 20251108, downloaded
76
+ once into `.cache/native-packages` and checked against a built-in SHA-256)
77
+ followed by a zstd SquashFS image. No `appimagetool` or nFPM is involved, the
78
+ build does not run the binary, and an ARM64 AppImage can be built on an x86
79
+ host. The same inputs and `SOURCE_DATE_EPOCH` give the same bytes.
80
+
81
+ **Libraries are not bundled.** The AppImage uses the host's libraries, as the
82
+ DEB and RPM do, and `build.json` lists what the binary links under
83
+ `required_libraries` with `"libraries": "host"`. It therefore runs where your
84
+ binary's glibc floor and those libraries are met, not on every Linux system.
85
+ Build on the oldest distribution you support, or link statically, to widen
86
+ that. The AppImage carries no update information and is not signed.
87
+
88
+ Running an AppImage needs FUSE on the user's system, or
89
+ `./App.AppImage --appimage-extract-and-run`.
90
+
51
91
  ## MSIX
52
92
 
53
93
  MSIX targets use `platform: windows`, `formats: [msix]`, and PE binaries with
@@ -87,7 +127,7 @@ An Arch package file and an AUR recipe are separate outputs: the former can
87
127
  be installed directly; the latter tells Arch tooling how to obtain and package
88
128
  your app.
89
129
 
90
- There is no Flatpak build/publication adapter, no NSIS/WiX adapter, and no
130
+ There is no Flatpak or Snap build/publication adapter, no NSIS/WiX adapter, and no
91
131
  hosted APT/DNF index service. Keep those workflows in your app's release
92
132
  process if you need them.
93
133
 
@@ -23,8 +23,8 @@ working on with `doctor --target ID` and `build --target ID --dry-run`.
23
23
  Install and select the version named in the error:
24
24
 
25
25
  ```sh
26
- gem install native-packages --version 0.7.0
27
- native-packages _0.7.0_ doctor
26
+ gem install native-packages --version 0.8.1
27
+ native-packages _0.8.1_ doctor
28
28
  ```
29
29
 
30
30
  `tool.version` is an exact match. In CI, use the matching released reusable
data/docs/index.md CHANGED
@@ -25,7 +25,7 @@ features:
25
25
  details: Describe your app's files, package metadata, and build targets in a single YAML file.
26
26
  - icon: 🐧
27
27
  title: Linux packages
28
- details: Create DEB, RPM, Arch, Alpine, and IPK packages from your compatible Linux builds.
28
+ details: Create DEB, RPM, AppImage, Arch, Alpine, and IPK packages from your compatible Linux builds.
29
29
  - icon: 🖥️
30
30
  title: Native installers
31
31
  details: Connect your macOS DMG and Windows Inno Setup scripts, or package Windows apps as MSIX.
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.7.0 --target main --title 'native-packages 0.7.0' --notes-file release-notes.md
26
+ gh release create v0.8.1 --target main --title 'native-packages 0.8.1' --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.1
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.
@@ -1,7 +1,7 @@
1
1
  # Supply the listed build outputs before running this example.
2
2
  schema: 1
3
3
  tool:
4
- version: '0.7.0'
4
+ version: '0.8.1'
5
5
  nfpm: '2.47.0'
6
6
  nfpm:
7
7
  name: example-app
@@ -1,6 +1,6 @@
1
1
  schema: 1
2
2
  tool:
3
- version: '0.7.0'
3
+ version: '0.8.1'
4
4
  nfpm: '2.47.0'
5
5
  nfpm:
6
6
  name: example-app
@@ -0,0 +1,166 @@
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
+ # As nFPM does: a glob source, or a destination ending in a slash,
105
+ # makes the destination a directory that takes each match's name.
106
+ pattern = !package["disable_globbing"] && item.fetch("src").match?(/[*?\[{]/)
107
+ single = paths.length == 1 && File.file?(paths.first) && type != "tree" && !pattern && !item.fetch("dst").end_with?("/")
108
+ paths.each do |path|
109
+ if File.directory?(path)
110
+ target.mkpath
111
+ FileUtils.cp_r(Dir.children(path).map { |child| File.join(path, child) }, target)
112
+ else
113
+ file = single ? target : target / File.basename(path)
114
+ file.dirname.mkpath
115
+ FileUtils.cp(path, file)
116
+ file.chmod(mode) if mode
117
+ end
118
+ end
119
+ else
120
+ raise Error, "appimage: unsupported content type #{type}"
121
+ end
122
+ end
123
+ end
124
+
125
+ # AppImage's root needs the desktop entry, its icon and AppRun.
126
+ def integrate(appdir)
127
+ entries = Pathname.glob(appdir / "usr/share/applications/*.desktop")
128
+ raise Error, "appimage needs exactly one desktop entry in /usr/share/applications; found #{entries.length}" unless entries.length == 1
129
+ desktop = entries.first
130
+ fields = desktop.read.lines.each_with_object({}) do |line, result|
131
+ key, value = line.strip.split("=", 2)
132
+ result[key] ||= value if value && %w[Exec Icon].include?(key)
133
+ end
134
+ command = Shellwords.split(fields.fetch("Exec") { raise Error, "appimage: #{desktop.basename} has no Exec" }).first
135
+ executable = command.start_with?("/") ? inside(appdir, command) : appdir / "usr/bin" / relative_path(command)
136
+ raise Error, "appimage: #{desktop.basename} runs #{command}, which the package does not install" unless executable.file? && executable.executable?
137
+ name = fields.fetch("Icon") { raise Error, "appimage: #{desktop.basename} has no Icon" }
138
+ raise Error, "appimage: Icon must be a name, not a path" if name.include?("/")
139
+ icons = ICON_DIRECTORIES.flat_map { |path| Pathname.glob(appdir / path / "**" / "#{name}.{svg,png}") }
140
+ # A scalable icon first, else the largest bitmap.
141
+ icon = icons.find { |path| path.extname == ".svg" } || icons.max_by(&:size)
142
+ raise Error, "appimage: the package installs no #{name}.svg or #{name}.png icon" unless icon
143
+ FileUtils.cp(desktop, appdir / desktop.basename)
144
+ FileUtils.cp(icon, appdir / icon.basename)
145
+ File.symlink(icon.basename.to_s, appdir / ".DirIcon")
146
+ File.symlink(executable.relative_path_from(appdir).to_s, appdir / "AppRun")
147
+ { "desktop" => desktop.basename.to_s, "icon" => icon.basename.to_s, "executable" => executable.relative_path_from(appdir).to_s }
148
+ end
149
+
150
+ # The same bytes for the same inputs: fixed modes and times.
151
+ def normalize(appdir, epoch)
152
+ time = Time.at(epoch)
153
+ Pathname.glob(appdir / "**/*", File::FNM_DOTMATCH).each do |path|
154
+ next if path.basename.to_s == "."
155
+ if path.symlink?
156
+ File.lutime(time, time, path)
157
+ else
158
+ path.chmod(path.directory? || path.executable? ? 0o755 : 0o644)
159
+ File.utime(time, time, path)
160
+ end
161
+ end
162
+ appdir.chmod(0o755)
163
+ File.utime(time, time, appdir)
164
+ end
165
+ end
166
+ 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") & Configuration::NATIVE_FORMATS).empty? }
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)
@@ -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
@@ -12,7 +12,7 @@ require "tmpdir"
12
12
  require "yaml"
13
13
 
14
14
  module NativePackages
15
- VERSION = "0.7.0"
15
+ VERSION = "0.8.1"
16
16
  class Error < StandardError; end
17
17
 
18
18
  module Support
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.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -52,6 +52,7 @@ files:
52
52
  - examples/packaging/repositories.yml
53
53
  - exe/native-packages
54
54
  - lib/native_packages.rb
55
+ - lib/native_packages/appimage.rb
55
56
  - lib/native_packages/build.rb
56
57
  - lib/native_packages/cli.rb
57
58
  - lib/native_packages/configuration.rb