Publish .deb and .rpm packages to the forge

af1c436 · gmorell · 2026-09-22 03:54

32 files +1192 -217
Message
{commit_body(@commit)}

Files changed

added .forgejo/workflows/packages.yml
+233 −0
@@ -0,0 +1,233 @@
1 +# Builds Icon Studio and publishes a .deb and an .rpm to this forge's
2 +# package registries, the way the other neiam Rust projects do.
3 +#
4 +# x86_64 only: that is what the desktops this runs on are, and a second
5 +# architecture is a second runner to keep alive for no one.
6 +#
7 +# Nothing to configure. `${{ github.token }}` is minted for the job and
8 +# authorised to publish to THIS repository's packages and nowhere else,
9 +# so there is no secret to create first.
10 +name: packages
11 +
12 +on:
13 + push:
14 + branches: [master]
15 + tags: ['v*']
16 + pull_request:
17 + branches: [master]
18 +
19 +env:
20 + CARGO_TERM_COLOR: always
21 +
22 +jobs:
23 + package:
24 + runs-on: self-hosted
25 + steps:
26 + - uses: actions/checkout@v4
27 +
28 + - name: Install system dependencies
29 + run: |
30 + set -euo pipefail
31 +
32 + # A hosted runner is an unprivileged user with sudo; a job
33 + # container is already root and often ships no sudo at all, so
34 + # hardcoding either one fails before the first package is
35 + # fetched. Ask what we are.
36 + if [ "$(id -u)" -eq 0 ]; then
37 + SUDO=
38 + elif command -v sudo >/dev/null 2>&1; then
39 + SUDO=sudo
40 + else
41 + echo "Not root and no sudo; cannot install system dependencies." >&2
42 + exit 1
43 + fi
44 +
45 + # eframe/winit need the windowing and GL headers; rfd is built
46 + # against the XDG portal rather than GTK, so there is no GTK
47 + # stack here. dpkg-dev matters more than it looks: without it
48 + # cargo-deb cannot run dpkg-shlibdeps, and the .deb ships with
49 + # an empty Depends that installs anywhere and then fails to
50 + # start on a machine missing libxkbcommon.
51 + if command -v apt-get >/dev/null 2>&1; then
52 + $SUDO apt-get update
53 + $SUDO apt-get install -y --no-install-recommends \
54 + build-essential pkg-config dpkg-dev rpm \
55 + libxkbcommon-dev libwayland-dev \
56 + libx11-dev libxcursor-dev libxrandr-dev libxi-dev \
57 + libgl1-mesa-dev
58 + elif command -v dnf >/dev/null 2>&1; then
59 + $SUDO dnf install -y gcc pkgconf-pkg-config rpm-build dpkg-dev \
60 + libxkbcommon-devel wayland-devel \
61 + libX11-devel libXcursor-devel libXrandr-devel libXi-devel \
62 + mesa-libGL-devel
63 + else
64 + echo "Unsupported package manager." >&2
65 + exit 1
66 + fi
67 +
68 + - name: Install Rust
69 + uses: dtolnay/rust-toolchain@stable
70 + with:
71 + components: rustfmt, clippy
72 +
73 + - name: Setup Rust cache
74 + uses: Swatinem/rust-cache@v2
75 +
76 + - name: Check formatting
77 + run: cargo fmt -- --check
78 +
79 + - name: Run clippy
80 + run: cargo clippy --all-targets -- -D warnings
81 +
82 + # Includes the comparison against the icons the original studio
83 + # shipped, so a rendering regression stops the release rather than
84 + # being packaged.
85 + - name: Run tests
86 + run: cargo test --verbose
87 +
88 + - name: Build release binary
89 + run: cargo build --release --verbose
90 +
91 + - name: Strip binary
92 + run: strip target/release/icon-studio
93 +
94 + # The icons and desktop entry are committed, but regenerating them
95 + # proves presets/icon-studio.toml still renders — the packaged icon
96 + # and the document that describes it cannot drift apart.
97 + - name: Regenerate the application icon
98 + run: |
99 + set -euo pipefail
100 + cargo run --release --example make_app_icon
101 + git --no-pager diff --stat -- assets/icons || true
102 +
103 + # Built with stable, not pinned: cargo-deb has repeatedly needed a
104 + # newer rustc than its own declared MSRV admits. These are
105 + # build-time utilities that never link into the binary, so holding
106 + # them to this crate's toolchain buys nothing and breaks whenever
107 + # the ecosystem moves. `--locked` keeps two runs a month apart
108 + # installing the same bytes.
109 + - name: Install the packaging tools
110 + run: |
111 + set -euo pipefail
112 + cargo install cargo-deb --locked
113 + cargo install cargo-generate-rpm --locked
114 +
115 + # Every CI build has to carry a distinct version, or the second one
116 + # publishes the same filename with different bytes and the registry
117 + # correctly refuses it with a 409 — and nothing that installed the
118 + # first ever sees the second.
119 + #
120 + # `+` rather than `-`: RPM forbids `-` in a version outright, and in
121 + # both ecosystems 0.1.0+dev.<stamp> sorts *after* 0.1.0, which is
122 + # what you want from a repository you added to run the latest build.
123 + # A tag publishes the bare Cargo version.
124 + #
125 + # One stamp for the whole job, so the .deb and .rpm from a single
126 + # run agree about what they are.
127 + - name: Stamp the package version
128 + id: version
129 + run: |
130 + set -euo pipefail
131 + base=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
132 +
133 + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
134 + version="$base"
135 + else
136 + version="$base+dev.$(date -u +%s)"
137 + fi
138 +
139 + echo "version: $version"
140 + echo "version=$version" >> "$GITHUB_OUTPUT"
141 +
142 + # Rewritten after the release build, so the binary is cached
143 + # against the unstamped manifest and only packaging sees this.
144 + sed -i "0,/^version = .*/s//version = \"$version\"/" Cargo.toml
145 +
146 + # The build cache can restore a target/ that still holds packages
147 + # from an earlier run. The publish step globs these directories, so
148 + # a stale .deb would be re-uploaded under its old version alongside
149 + # the new one.
150 + - name: Clear previous package output
151 + run: rm -rf target/debian target/generate-rpm
152 +
153 + - name: Build the Debian package
154 + run: cargo deb --no-build --no-strip
155 +
156 + - name: Build the RPM package
157 + run: cargo generate-rpm
158 +
159 + - name: Show what was built
160 + run: ls -l target/debian/*.deb target/generate-rpm/*.rpm
161 +
162 + # `vars.CI_FORGE_URL` is a forge built-in, but an older forge build
163 + # will not inject it; fall back to what the runner already knows.
164 + - name: Resolve the forge URL
165 + env:
166 + CI_FORGE_URL: ${{ vars.CI_FORGE_URL }}
167 + run: |
168 + set -euo pipefail
169 + forge_url=${CI_FORGE_URL:-${GITHUB_SERVER_URL:-}}
170 +
171 + if [ -z "$forge_url" ]; then
172 + echo "No forge URL available; not publishing."
173 + echo "PUBLISH=false" >> "$GITHUB_ENV"
174 + exit 0
175 + fi
176 +
177 + echo "publishing to $forge_url"
178 + echo "FORGE_URL=$forge_url" >> "$GITHUB_ENV"
179 + echo "PUBLISH=true" >> "$GITHUB_ENV"
180 +
181 + # Pull requests do not publish: a PR build is a check, and a
182 + # registry carrying every proposed change is not one anyone can
183 + # install from.
184 + - name: Publish the .deb and the .rpm
185 + if: github.event_name != 'pull_request' && env.PUBLISH == 'true'
186 + env:
187 + REPOSITORY: ${{ github.repository }}
188 + TOKEN: ${{ github.token }}
189 + run: |
190 + set -euo pipefail
191 +
192 + for deb in target/debian/*.deb; do
193 + echo "publishing $deb"
194 + curl --fail-with-body -sS \
195 + -u "ci:$TOKEN" \
196 + -H "Content-Type: application/vnd.debian.binary-package" \
197 + --data-binary "@$deb" \
198 + "$FORGE_URL/pkg/deb/$REPOSITORY/upload?dist=stable&component=main"
199 + echo
200 + done
201 +
202 + for rpm in target/generate-rpm/*.rpm; do
203 + echo "publishing $rpm"
204 + curl --fail-with-body -sS \
205 + -u "ci:$TOKEN" \
206 + -H "Content-Type: application/x-rpm" \
207 + --data-binary "@$rpm" \
208 + "$FORGE_URL/pkg/rpm/$REPOSITORY/upload"
209 + echo
210 + done
211 +
212 + - name: How to install it
213 + if: github.event_name != 'pull_request' && env.PUBLISH == 'true'
214 + env:
215 + OWNER: ${{ github.repository_owner }}
216 + run: |
217 + cat <<EOF
218 + # Debian / Ubuntu
219 + curl -fsSL $FORGE_URL/pkg/_keys/deb/$OWNER | sudo tee /etc/apt/keyrings/$OWNER.asc >/dev/null
220 + echo "deb [signed-by=/etc/apt/keyrings/$OWNER.asc] $FORGE_URL/pkg/deb/$OWNER/_all stable main" | sudo tee /etc/apt/sources.list.d/$OWNER.list
221 + sudo apt update && sudo apt install neiam-icon-studio
222 +
223 + # Fedora / RHEL / openSUSE — /etc/yum.repos.d/$OWNER.repo
224 + [$OWNER]
225 + name=$OWNER
226 + baseurl=$FORGE_URL/pkg/rpm/$OWNER/_all
227 + enabled=1
228 + repo_gpgcheck=1
229 + # cargo-generate-rpm produces unsigned packages; the forge signs
230 + # the index, not what it was handed.
231 + gpgcheck=0
232 + gpgkey=$FORGE_URL/pkg/_keys/rpm/$OWNER
233 + EOF
modified Cargo.toml
+39 −0
@@ -3,6 +3,45 @@ name = "icon-studio"
3 3 version = "0.1.0"
4 4 edition = "2021"
5 5 description = "Application icon designer for the Casabeza and neiam palettes"
6 +authors = ["Gabe <gabe@neiam.org>"]
7 +license = "GPL-3.0-or-later"
8 +
9 +# Packaging. Both tools read the release binary out of target/release, so
10 +# `cargo build --release` has to have run first — `cargo deb --no-build`
11 +# and `cargo generate-rpm` only assemble.
12 +#
13 +# This is a GUI application, so unlike markprompter it also installs a
14 +# .desktop entry and hicolor icons; without them it would install and
15 +# then be invisible in every application menu. The icon is generated by
16 +# the studio itself from presets/icon-studio.toml.
17 +[package.metadata.deb]
18 +name = "neiam-icon-studio"
19 +section = "graphics"
20 +assets = [
21 + # target/release is special: cargo-deb rewrites it to the real
22 + # target directory.
23 + ["target/release/icon-studio", "usr/bin/", "755"],
24 + ["icon-studio.desktop", "usr/share/applications/", "644"],
25 + ["assets/icons/icon-studio-48.png", "usr/share/icons/hicolor/48x48/apps/icon-studio.png", "644"],
26 + ["assets/icons/icon-studio-64.png", "usr/share/icons/hicolor/64x64/apps/icon-studio.png", "644"],
27 + ["assets/icons/icon-studio-128.png", "usr/share/icons/hicolor/128x128/apps/icon-studio.png", "644"],
28 + ["assets/icons/icon-studio-256.png", "usr/share/icons/hicolor/256x256/apps/icon-studio.png", "644"],
29 + ["README.md", "usr/share/doc/neiam-icon-studio/README.md", "644"],
30 + ["presets/*.toml", "usr/share/neiam-icon-studio/presets/", "644"],
31 +]
32 +
33 +[package.metadata.generate-rpm]
34 +name = "neiam-icon-studio"
35 +assets = [
36 + { source = "target/release/icon-studio", dest = "/usr/bin/icon-studio", mode = "0755" },
37 + { source = "icon-studio.desktop", dest = "/usr/share/applications/icon-studio.desktop", mode = "0644" },
38 + { source = "assets/icons/icon-studio-48.png", dest = "/usr/share/icons/hicolor/48x48/apps/icon-studio.png", mode = "0644" },
39 + { source = "assets/icons/icon-studio-64.png", dest = "/usr/share/icons/hicolor/64x64/apps/icon-studio.png", mode = "0644" },
40 + { source = "assets/icons/icon-studio-128.png", dest = "/usr/share/icons/hicolor/128x128/apps/icon-studio.png", mode = "0644" },
41 + { source = "assets/icons/icon-studio-256.png", dest = "/usr/share/icons/hicolor/256x256/apps/icon-studio.png", mode = "0644" },
42 + { source = "README.md", dest = "/usr/share/doc/neiam-icon-studio/README.md", doc = true, mode = "0644" },
43 + { source = "presets/*.toml", dest = "/usr/share/neiam-icon-studio/presets/", mode = "0644" },
44 +]
6 45
7 46 [lib]
8 47 name = "icon_studio"
modified README.md
+28 −0
@@ -116,6 +116,30 @@ Vendored in `assets/`:
116 116 for text marks and sub-banners. Rendering text through the Symbols font
117 117 lands on `.notdef`, which is why text needs its own face.
118 118
119 +## Installing
120 +
121 +Packages are published to this forge's registries on every push to master,
122 +x86_64 only.
123 +
124 +```sh
125 +# Debian / Ubuntu
126 +curl -fsSL $FORGE/pkg/_keys/deb/neiam | sudo tee /etc/apt/keyrings/neiam.asc >/dev/null
127 +echo "deb [signed-by=/etc/apt/keyrings/neiam.asc] $FORGE/pkg/deb/neiam/_all stable main" \
128 + | sudo tee /etc/apt/sources.list.d/neiam.list
129 +sudo apt update && sudo apt install neiam-icon-studio
130 +```
131 +
132 +For Fedora / RHEL / openSUSE the workflow prints a matching `.repo` stanza at
133 +the end of each run.
134 +
135 +The package installs the binary, a desktop entry with the application icon,
136 +and the preset documents under `/usr/share/neiam-icon-studio/presets/`.
137 +
138 +Builds off a tag publish the bare Cargo version; builds off master publish
139 +`<version>+dev.<timestamp>`, which sorts *after* the release — so a machine
140 +tracking this repository gets the latest build, and two runs never collide on
141 +one filename.
142 +
119 143 ## Running
120 144
121 145 ```
@@ -124,8 +148,12 @@ cargo test # including the comparison against shipped icons
124 148 cargo run --example reproduce_cbz # re-render the five cbz icons
125 149 cargo run --example showcase # every background generator
126 150 cargo run --example make_presets # regenerate presets/
151 +cargo run --example make_app_icon # regenerate the desktop icon
127 152 ```
128 153
154 +The application's own icon is made by the application, from
155 +`presets/icon-studio.toml` — change it in the studio and regenerate.
156 +
129 157 Export writes `ic_icon`, `ic_icon_background` and `ic_icon_foreground` across
130 158 `mipmap-mdpi` through `mipmap-xxxhdpi` (108–432px), matching the layout the
131 159 cbz apps use.
modified ?
+0 −0

Binary file — not rendered.

modified ?
+0 −0

Binary file — not rendered.

modified ?
+0 −0

Click to load diff…

modified ?
+0 −0

Click to load diff…

added examples/make_app_icon.rs
+22 −0

Click to load diff…

modified examples/make_presets.rs
+22 −8

Click to load diff…

modified examples/reproduce_cbz.rs
+16 −7

Click to load diff…

modified examples/showcase.rs
+12 −3

Click to load diff…

added icon-studio.desktop
+13 −0

Click to load diff…

added presets/icon-studio.toml
+41 −0

Click to load diff…

modified src/app.rs
+24 −6

Click to load diff…

modified src/doc.rs
+17 −4

Click to load diff…

modified src/export.rs
+7 −2

Click to load diff…

modified src/icons.rs
+1 −7

Click to load diff…

modified src/palette/generated.rs
+560 −140

Click to load diff…

modified src/palette/mod.rs
+16 −3

Click to load diff…

modified src/render.rs
+52 −12

Click to load diff…

modified src/spec.rs
+12 −3

Click to load diff…

modified tests/documents.rs
+40 −9

Click to load diff…

modified tests/reproduce.rs
+37 −13

Click to load diff…

Parents: f232944