1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
name: Release binaries
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
# The only target here that needs a cross toolchain installed: the macOS pair cross-
# compiles too, but Xcode ships both slices. Added so the VS Code extension can bundle a
# binary for linux-arm64 rather than leaving those users to install one by hand.
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
cross: aarch64-linux-gnu
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
# Both variables are needed, and for different consumers: cargo uses the LINKER to link the
# Rust code, while the `cc` crate uses CC to compile the 24 bundled tree-sitter grammars,
# which are C. Setting only the linker produces host-architecture objects and a link failure
# that reads as if the toolchain were missing.
- name: Install cross toolchain
if: matrix.cross
run: |
sudo apt-get update
sudo apt-get install -y gcc-${{ matrix.cross }}
echo "CARGO_TARGET_$(echo ${{ matrix.target }} | tr 'a-z-' 'A-Z_')_LINKER=${{ matrix.cross }}-gcc" >> "$GITHUB_ENV"
echo "CC_$(echo ${{ matrix.target }} | tr '-' '_')=${{ matrix.cross }}-gcc" >> "$GITHUB_ENV"
# Default features (`tui`) only - this is the `codediff` product binary, not the
# test-fixtures/stats-gated dev tools in src/bin/.
- name: Build
run: cargo build --release --target ${{ matrix.target }} --bin codediff
# A cross-compiled binary cannot be run here to check it, so check what it *is*. A silent
# fallback to the host compiler would otherwise ship an x86-64 binary under an arm64 name,
# and the first sign would be an exec-format error on a user's machine.
- name: Verify architecture
if: matrix.cross
run: |
file target/${{ matrix.target }}/release/codediff
file target/${{ matrix.target }}/release/codediff | grep -q 'ARM aarch64' \
|| { echo "::error::built binary is not aarch64"; exit 1; }
# Deliberately not generated per-target here: `codediff util man`/`util completions` need to
# *run* the binary, and one of these four targets is always cross-compiled (macos-latest is
# arm64, so x86_64-apple-darwin is the odd one out and would need Rosetta). The output is
# identical for every platform anyway - it is derived from the clap definition, not from the
# host - so the `assets` job below builds it once and ships it as its own artifact.
- name: Package (Unix)
if: runner.os != 'Windows'
run: tar czf codediff-${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release codediff
- name: Package (Windows)
if: runner.os == 'Windows'
run: Compress-Archive -Path target/${{ matrix.target }}/release/codediff.exe -DestinationPath codediff-${{ matrix.target }}.zip
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: codediff-${{ matrix.target }}.*
# Harmless to set on all 4 matrix jobs: same tag/commit range each time, so every job
# generates identical notes - GitHub just uses whatever's already there past the first.
generate_release_notes: true
# Shell completions and the man page, generated once on a native Linux runner. Every packaging
# recipe under packaging/ generates these itself from its own freshly built binary, so this
# artifact is for people installing a prebuilt binary by hand rather than through a package.
assets:
name: completions + man page
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: release-assets
- name: Build and generate
run: |
cargo build --release --bin codediff
mkdir -p dist/completions
./target/release/codediff util man > dist/codediff.1
./target/release/codediff util completions bash > dist/completions/codediff.bash
./target/release/codediff util completions zsh > dist/completions/_codediff
./target/release/codediff util completions fish > dist/completions/codediff.fish
./target/release/codediff util completions powershell > dist/completions/codediff.ps1
./target/release/codediff util completions elvish > dist/completions/codediff.elv
tar czf codediff-completions-and-man.tar.gz -C dist .
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: codediff-completions-and-man.tar.gz
# An *unofficial* .deb, built with cargo-deb from the [package.metadata.deb] block in Cargo.toml.
# It is not, and cannot readily become, a package in the Debian archive: that would require all
# 293 dependency crates (24 tree-sitter grammars among them) to exist as `librust-*-dev` first.
# See packaging/README.md.
deb:
name: deb (amd64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: release-deb
- name: Install cargo-deb
run: cargo install cargo-deb --locked
# cargo-deb copies assets from disk and cannot run the binary itself, so the man page and
# completions Cargo.toml's `assets` list points at have to exist before it runs. Building
# here and passing --no-build below also stops cargo-deb from doing a second release build.
- name: Build and generate assets
run: |
cargo build --release --bin codediff
mkdir -p target/dist
./target/release/codediff util man > target/dist/codediff.1
./target/release/codediff util completions bash > target/dist/codediff.bash
./target/release/codediff util completions zsh > target/dist/_codediff
./target/release/codediff util completions fish > target/dist/codediff.fish
- name: Package
run: cargo deb --no-build --output codediff_amd64.deb
# Catches an asset path in Cargo.toml going stale: `cargo deb` would happily produce a
# package missing the man page, and nobody would notice until someone ran `man codediff`.
- name: Verify contents
run: |
dpkg-deb --contents codediff_amd64.deb
for path in usr/bin/codediff usr/share/man/man1/codediff.1 \
usr/share/bash-completion/completions/codediff; do
dpkg-deb --contents codediff_amd64.deb | grep -q " ./$path" \
|| { echo "::error::missing $path in .deb"; exit 1; }
done
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: codediff_amd64.deb
# One SHA256SUMS.txt over every artifact. This is what makes the packaging recipes maintainable:
# a PKGBUILD's `sha256sums=()`, an ebuild's Manifest and a Nix `hash =` all need a checksum
# somebody has to produce, and reading it off the release beats each packager re-downloading and
# hashing by hand. Runs last, after every other job has uploaded.
checksums:
name: SHA256SUMS
runs-on: ubuntu-latest
needs:
steps:
- name: Collect and hash every release asset
env:
GH_TOKEN: ${{ github.token }}
# workflow_dispatch has no tag of its own; fall back to the latest release so a manual
# re-run still produces a usable file instead of failing on an empty ref.
TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
set -euo pipefail
tag="${TAG:-$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)}"
mkdir -p assets && cd assets
gh release download "$tag" --repo "$GITHUB_REPOSITORY" --clobber
# Sorted so the file's line order is stable between runs and a diff between two releases
# is readable. SHA256SUMS.txt itself is excluded: on a re-run `gh release download`
# fetches the previous run's copy along with everything else, and it would otherwise be
# hashed into its own successor.
sha256sum $(ls | grep -vFx SHA256SUMS.txt | sort) > ../SHA256SUMS.txt
cat ../SHA256SUMS.txt
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: SHA256SUMS.txt