Skip to content

Commit 4ca4217

Browse files
committed
ci: add ARM64 cross-compilation support and fix Docker image publishing
1 parent 7b1683c commit 4ca4217

5 files changed

Lines changed: 98 additions & 130 deletions

File tree

.github/workflows/build-artifacts.yaml

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,71 +6,71 @@ on:
66
- "*.md"
77
- "compose.*"
88
branches:
9-
- "main"
9+
- "personal"
1010
release:
1111
types: [published]
1212

13-
env:
13+
env:
1414
CARGO_TERM_COLOR: always
1515

16-
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc
17-
CC_aarch64_unknown_linux_musl: aarch64-linux-gnu-gcc
18-
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-gcc
19-
CC_armv7_unknown_linux_musleabihf: arm-linux-gnueabihf-gcc
20-
2116
jobs:
2217
build:
2318
name: Rust project - latest
2419
runs-on: ubuntu-latest
25-
strategy:
26-
matrix:
27-
target:
28-
- x86_64-unknown-linux-musl
29-
- aarch64-unknown-linux-musl
30-
- armv7-unknown-linux-musleabihf
3120
steps:
3221
- uses: actions/checkout@v4
33-
22+
3423
- uses: actions-rust-lang/setup-rust-toolchain@v1
3524
with:
36-
target: ${{ matrix.target }}
37-
38-
- if: matrix.target == 'x86_64-unknown-linux-musl'
25+
target: aarch64-unknown-linux-musl
26+
27+
- name: Install base dependencies
3928
run: |
4029
sudo apt-get update
41-
sudo apt-get install -y --no-install-recommends musl-tools
42-
43-
- if: matrix.target == 'armv7-unknown-linux-musleabihf'
44-
run: |
45-
sudo apt update
46-
sudo apt install -y gcc-arm-linux-gnueabihf musl-tools
30+
sudo apt-get install -y --no-install-recommends \
31+
build-essential \
32+
cmake \
33+
perl \
34+
pkg-config \
35+
libclang-dev \
36+
musl-tools \
37+
crossbuild-essential-arm64
4738
48-
- if: matrix.target == 'aarch64-unknown-linux-musl'
39+
- name: Install musl.cc aarch64 cross-compiler
4940
run: |
50-
sudo apt update
51-
sudo apt install -y gcc-aarch64-linux-gnu musl-tools
41+
wget -q https://github.com/musl-cc/musl.cc/releases/latest/download/aarch64-linux-musl-cross.tgz
42+
tar xzf aarch64-linux-musl-cross.tgz -C /opt
43+
echo "/opt/aarch64-linux-musl-cross/bin" >> $GITHUB_PATH
5244
5345
- name: Versions
5446
id: version
5547
run: echo "VERSION=$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')" >> "$GITHUB_OUTPUT"
5648

5749
- name: Build
58-
run: cargo build --release --target ${{ matrix.target }}
50+
env:
51+
CC: aarch64-linux-musl-gcc
52+
CXX: aarch64-linux-musl-g++
53+
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-g++
54+
run: cargo build --release --target aarch64-unknown-linux-musl
5955

6056
- name: Package release
61-
run: tar czf redlib-${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release/ redlib
57+
run: tar czf redlib-aarch64-unknown-linux-musl.tar.gz -C target/aarch64-unknown-linux-musl/release/ redlib
58+
59+
- name: Upload binary as workflow artifact
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: redlib-aarch64-unknown-linux-musl
63+
path: target/aarch64-unknown-linux-musl/release/redlib
64+
retention-days: 1
6265

6366
- name: Upload release
64-
uses: softprops/action-gh-release@v1
67+
uses: softprops/action-gh-release@v2
6568
with:
6669
tag_name: ${{ steps.version.outputs.VERSION }}
6770
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
6871
draft: true
6972
files: |
70-
redlib-${{ matrix.target }}.tar.gz
73+
redlib-aarch64-unknown-linux-musl.tar.gz
7174
body: |
7275
- ${{ github.event.head_commit.message }} ${{ github.sha }}
7376
generate_release_notes: true
74-
75-
76-

.github/workflows/main-docker.yml

Lines changed: 35 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,105 +5,63 @@ on:
55
workflows: ["Release Build"]
66
types:
77
- completed
8+
89
env:
9-
REGISTRY_IMAGE: quay.io/redlib/redlib
10+
REGISTRY_IMAGE: ghcr.io/algorithm5838/redlib
1011

1112
jobs:
1213
build:
1314
runs-on: ubuntu-latest
14-
strategy:
15-
fail-fast: false
16-
matrix:
17-
include:
18-
- { platform: linux/amd64, target: x86_64-unknown-linux-musl }
19-
- { platform: linux/arm64, target: aarch64-unknown-linux-musl }
20-
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf }
15+
# Only build if the Release Build succeeded
16+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
2117
steps:
2218
- name: Checkout
2319
uses: actions/checkout@v4
20+
21+
- name: Download binary artifact from Release Build
22+
uses: actions/download-artifact@v4
23+
with:
24+
name: redlib-aarch64-unknown-linux-musl
25+
path: docker-bin/
26+
run-id: ${{ github.event.workflow_run.id }}
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
29+
- name: Make binary executable
30+
run: chmod +x docker-bin/redlib
31+
2432
- name: Docker meta
2533
id: meta
2634
uses: docker/metadata-action@v5
2735
with:
2836
images: ${{ env.REGISTRY_IMAGE }}
2937
tags: |
30-
type=sha
38+
type=sha,prefix=sha-,format=short,event=branch
3139
type=raw,value=latest,enable={{is_default_branch}}
32-
- name: Set up QEMU
33-
uses: docker/setup-qemu-action@v3
40+
3441
- name: Set up Docker Buildx
3542
uses: docker/setup-buildx-action@v3
36-
- name: Login to Quay.io Container Registry
43+
44+
- name: Login to GitHub Container Registry
3745
uses: docker/login-action@v3
3846
with:
39-
registry: quay.io
40-
username: ${{ secrets.QUAY_USERNAME }}
41-
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
42-
- name: Build and push
43-
id: build
47+
registry: ghcr.io
48+
username: ${{ github.actor }}
49+
password: ${{ secrets.GITHUB_TOKEN }}
50+
51+
- name: Build and push (ARM64)
4452
uses: docker/build-push-action@v5
4553
with:
4654
context: .
47-
platforms: ${{ matrix.platform }}
55+
platforms: linux/arm64
56+
push: true
57+
tags: ${{ steps.meta.outputs.tags }}
4858
labels: ${{ steps.meta.outputs.labels }}
49-
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
50-
file: Dockerfile
51-
build-args: TARGET=${{ matrix.target }}
52-
- name: Export digest
53-
run: |
54-
mkdir -p /tmp/digests
55-
digest="${{ steps.build.outputs.digest }}"
56-
touch "/tmp/digests/${digest#sha256:}"
57-
- name: Upload digest
58-
uses: actions/upload-artifact@v4
59-
with:
60-
name: digests-${{ matrix.target }}
61-
path: /tmp/digests/*
62-
if-no-files-found: error
63-
retention-days: 1
64-
merge:
65-
runs-on: ubuntu-latest
66-
needs:
67-
- build
68-
steps:
69-
- name: Download digests
70-
uses: actions/download-artifact@v4.1.7
71-
with:
72-
path: /tmp/digests
73-
pattern: digests-*
74-
merge-multiple: true
75-
76-
- name: Set up Docker Buildx
77-
uses: docker/setup-buildx-action@v3
78-
- name: Docker meta
79-
id: meta
80-
uses: docker/metadata-action@v5
81-
with:
82-
images: ${{ env.REGISTRY_IMAGE }}
83-
tags: |
84-
type=sha
85-
type=raw,value=latest,enable={{is_default_branch}}
86-
- name: Login to Quay.io Container Registry
87-
uses: docker/login-action@v3
88-
with:
89-
registry: quay.io
90-
username: ${{ secrets.QUAY_USERNAME }}
91-
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
92-
- name: Create manifest list and push
93-
working-directory: /tmp/digests
94-
run: |
95-
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
96-
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
97-
98-
# - name: Push README to Quay.io
99-
# uses: christian-korneck/update-container-description-action@v1
100-
# env:
101-
# DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
102-
# with:
103-
# destination_container_repo: quay.io/redlib/redlib
104-
# provider: quay
105-
# readme_file: 'README.md'
59+
file: Dockerfile.prebuilt
10660

107-
- name: Inspect image
61+
- name: Print image tags
10862
run: |
109-
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
63+
echo "## Docker Image" >> $GITHUB_STEP_SUMMARY
64+
echo '```' >> $GITHUB_STEP_SUMMARY
65+
echo "docker pull ${{ env.REGISTRY_IMAGE }}:latest" >> $GITHUB_STEP_SUMMARY
66+
echo "docker pull ${{ env.REGISTRY_IMAGE }}:sha-$(echo '${{ github.event.workflow_run.head_sha }}' | cut -c1-7)" >> $GITHUB_STEP_SUMMARY
67+
echo '```' >> $GITHUB_STEP_SUMMARY

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
FROM alpine:3.19
22

3-
ARG TARGET
3+
ARG TARGET=aarch64-unknown-linux-musl
4+
ARG GITHUB_REPOSITORY=Algorithm5838/redlib
45

56
RUN apk add --no-cache curl
67

7-
RUN curl -L "https://github.com/redlib-org/redlib/releases/latest/download/redlib-${TARGET}.tar.gz" | \
8+
RUN curl -L "https://github.com/${GITHUB_REPOSITORY}/releases/latest/download/redlib-${TARGET}.tar.gz" | \
89
tar xz -C /usr/local/bin/
910

1011
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
@@ -17,4 +18,3 @@ EXPOSE 8080
1718
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
1819

1920
CMD ["redlib"]
20-

Dockerfile.prebuilt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM alpine:3.19
2+
3+
# Binary is copied from the workflow artifact (docker-bin/redlib)
4+
COPY docker-bin/redlib /usr/local/bin/redlib
5+
6+
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
7+
USER redlib
8+
9+
# Tell Docker to expose port 8080
10+
EXPOSE 8080
11+
12+
# Run a healthcheck every minute to make sure redlib is functional
13+
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
14+
15+
CMD ["redlib"]

src/client.rs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ pub static CLIENT: LazyLock<WreqClient> = LazyLock::new(build_client);
3333

3434
/// A separate wreq client used only for media proxying (images, videos, HLS).
3535
/// Unlike CLIENT (which uses Policy::none() + browser emulation for the Reddit
36-
/// OAuth API), this client:
37-
/// - follows redirects (Policy::limited(10)) so CDN 302s are resolved server-side
38-
/// - has NO browser emulation — Reddit's image CDN (i.redd.it, preview.redd.it)
39-
/// detects BoringSSL/Chrome fingerprints and serves its HTML website instead
40-
/// of the image bytes. A plain TLS client gets the actual image.
36+
/// OAuth API), this client follows redirects (Policy::limited(10)) and has no
37+
/// browser emulation. Reddit's image CDN (i.redd.it, preview.redd.it) detects
38+
/// BoringSSL/Chrome fingerprints and serves its HTML website instead of the
39+
/// image bytes; a plain TLS client gets the actual image.
4140
pub static MEDIA_CLIENT: LazyLock<WreqClient> = LazyLock::new(|| {
4241
WreqClient::builder()
4342
.redirect(Policy::limited(10))
@@ -96,7 +95,7 @@ trait IntoHyperResponse {
9695

9796
impl IntoHyperResponse for wreq::Response {
9897
async fn into_hyper(self) -> Result<Response<Body>, String> {
99-
// wreq uses http v1.x, hyper uses http v0.2.x — convert via primitives
98+
// wreq uses http v1.x; hyper uses http v0.2.x. Convert via primitives.
10099
let status_u16 = self.status().as_u16();
101100
let status = hyper::StatusCode::from_u16(status_u16).map_err(|e| e.to_string())?;
102101

@@ -166,7 +165,6 @@ pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, S
166165

167166
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
168167
let status = res.status().as_u16();
169-
// Use string literals: wreq uses http v1.x, hyper::header constants are http v0.2
170168
let policy_error = res.headers().get("retry-after").is_some();
171169

172170
match status {
@@ -217,12 +215,12 @@ pub async fn proxy(req: hyper::Request<Body>, format: &str) -> Result<Response<B
217215
}
218216

219217
async fn stream(url: &str, req: &hyper::Request<Body>) -> Result<Response<Body>, String> {
220-
// Use MEDIA_CLIENT (Policy::limited(10)) so CDN 302 redirects are followed
221-
// server-side. CLIENT uses Policy::none() for Reddit API redirect handling.
218+
// MEDIA_CLIENT follows CDN redirects; CLIENT keeps Policy::none() for the
219+
// Reddit API redirect logic in request().
222220
let mut builder = MEDIA_CLIENT.get(url);
223221

224-
// Copy useful headers from original request
225-
// Convert hyper header values (http v0.2) to bytes for wreq (http v1.x)
222+
// Forward caching/range headers from the browser request. hyper header
223+
// values (http v0.2) are passed as bytes so wreq (http v1.x) accepts them.
226224
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
227225
if let Some(value) = req.headers().get(key) {
228226
builder = builder.header(key, value.as_bytes());
@@ -235,10 +233,7 @@ async fn stream(url: &str, req: &hyper::Request<Body>) -> Result<Response<Body>,
235233
builder = builder.header("User-Agent", client.user_agent());
236234
}
237235

238-
let resp = builder.send().await.map_err(|e| e.to_string())?;
239-
240-
let status = resp.status();
241-
let mut hyper_resp = resp.into_hyper().await?;
236+
let mut hyper_resp = builder.send().await.map_err(|e| e.to_string())?.into_hyper().await?;
242237

243238
// Strip tracking/CDN headers
244239
{
@@ -261,7 +256,6 @@ async fn stream(url: &str, req: &hyper::Request<Body>) -> Result<Response<Body>,
261256
}
262257
}
263258

264-
let _ = status; // used implicitly via hyper_resp
265259
Ok(hyper_resp)
266260
}
267261

@@ -320,7 +314,8 @@ fn request(method: Method, path: String, redirect: bool, quarantine: bool, base_
320314
return resp.into_hyper().await;
321315
}
322316

323-
// Use string literal: wreq uses http v1.x, hyper::header constants are http v0.2
317+
// Use a string key: wreq (http v1.x) header names are not compatible
318+
// with hyper::header constants (http v0.2).
324319
let location = resp.headers().get("location").map(|v| v.to_str().unwrap_or_default().to_string());
325320

326321
if location.as_deref() == Some(ALTERNATIVE_REDDIT_URL_BASE) {

0 commit comments

Comments
 (0)