Skip to content
Merged

Fixes #1807

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/build-updater-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ for (const sig of readdirSync(dir).filter((f) => f.endsWith('.sig'))) {
platforms['darwin-aarch64'] = entry;
platforms['darwin-x86_64'] = entry;
} else if (name.endsWith('.appimage')) {
platforms['linux-x86_64'] = entry;
platforms[name.includes('-linux-aarch64') ? 'linux-aarch64' : 'linux-x86_64'] = entry;
} else if (name.endsWith('-setup.exe') || name.endsWith('.nsis.zip')) {
platforms['windows-x86_64'] = entry;
windowsIsNsis = true;
Expand Down
32 changes: 21 additions & 11 deletions .github/workflows/tauri-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -614,16 +614,25 @@ jobs:

# Nightlies use `--snapshot`, which packages without publishing.
linux:
name: Build Linux (CEF)
name: Build Linux ${{ matrix.arch }} (CEF)
needs: setup-release
runs-on: ubuntu-22.04
timeout-minutes: 60
runs-on: ${{ matrix.runner }}
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-22.04
- arch: aarch64
runner: ubuntu-22.04-arm
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
env:
ARCH: ${{ matrix.arch }}
TAG: ${{ needs.setup-release.outputs.tag }}
VERSION: ${{ needs.setup-release.outputs.version }}
VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }}
Expand Down Expand Up @@ -652,6 +661,7 @@ jobs:
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: src-tauri
key: ${{ matrix.arch }}

- name: Stamp release version into tauri.conf.json
shell: bash
Expand All @@ -673,7 +683,7 @@ jobs:
src-tauri/target/release/sable "$DISPLAY_NAME" \
src-tauri/target/release/sable-updater

DEB="src-tauri/target/release/bundle/deb/Sable-${VERSION}-linux-x86_64.deb"
DEB="src-tauri/target/release/bundle/deb/Sable-${VERSION}-linux-${ARCH}.deb"
dpkg-deb --contents "$DEB" | grep 'etc/apparmor.d/sable$' >/dev/null
dpkg-deb --ctrl-tarfile "$DEB" | tar -t | grep -x './postinst' >/dev/null
dpkg-deb --ctrl-tarfile "$DEB" | tar -t | grep -x './prerm' >/dev/null
Expand All @@ -685,14 +695,14 @@ jobs:
src-tauri/target/release/cef-pkg/stage/share "$RELEASE_DIR/"
test -f "$RELEASE_DIR/runtime/CEF-LICENSE.txt"
tar -C "$RELEASE_DIR" -czf \
"src-tauri/target/release/bundle/Sable-${VERSION}-linux-x86_64.tar.gz" \
"src-tauri/target/release/bundle/Sable-${VERSION}-linux-${ARCH}.tar.gz" \
sable runtime share

- name: Sign the AppImage for the updater
if: ${{ env.TAURI_SIGNING_PRIVATE_KEY != '' }}
shell: bash
run: |
for f in src-tauri/target/release/bundle/appimage/Sable-*-linux-x86_64.AppImage; do
for f in src-tauri/target/release/bundle/appimage/Sable-*-linux-${ARCH}.AppImage; do
[ -e "$f" ] || continue
pnpm tauri signer sign "$f"
done
Expand All @@ -714,9 +724,9 @@ jobs:
run: |
for f in src-tauri/target/release/bundle/deb/Sable-*.deb \
src-tauri/target/release/bundle/rpm/Sable-*.rpm \
src-tauri/target/release/bundle/Sable-*-linux-x86_64.tar.gz \
src-tauri/target/release/bundle/appimage/Sable-*-linux-x86_64.AppImage \
src-tauri/target/release/bundle/appimage/Sable-*-linux-x86_64.AppImage.sig; do
src-tauri/target/release/bundle/Sable-*-linux-${ARCH}.tar.gz \
src-tauri/target/release/bundle/appimage/Sable-*-linux-${ARCH}.AppImage \
src-tauri/target/release/bundle/appimage/Sable-*-linux-${ARCH}.AppImage.sig; do
[ -e "$f" ] || continue
echo "Uploading $f"
gh release upload "$TAG" "$f" --clobber
Expand All @@ -725,7 +735,7 @@ jobs:
- name: Upload .deb as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sable-linux-deb
name: sable-linux-deb-${{ matrix.arch }}
path: src-tauri/target/release/bundle/deb/Sable-*.deb
if-no-files-found: error
retention-days: 1
Expand All @@ -750,7 +760,7 @@ jobs:
- name: Download .deb artifact and compute checksum
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sable-linux-deb
name: sable-linux-deb-x86_64
path: deb-artifact

- name: Compute checksum
Expand Down
2 changes: 1 addition & 1 deletion nfpm.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json

name: sable
arch: amd64
arch: ${PKG_ARCH}
platform: linux
version: ${PKG_VERSION}
release: ${PKG_RELEASE}
Expand Down
10 changes: 8 additions & 2 deletions scripts/cef/copy-libs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ PROFILE="${1:-debug}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DEST="${2:-$ROOT/src-tauri/target/$PROFILE}"

case "$(uname -m)" in
x86_64) CEF_ARCH=x86_64 ;;
aarch64 | arm64) CEF_ARCH=aarch64 ;;
*) echo "❌ unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

# --target moves build-script output under target/<triple>/.
CEF_DIR="$(
find "$ROOT/src-tauri/target" -type d -name cef_linux_x86_64 \
find "$ROOT/src-tauri/target" -type d -name "cef_linux_$CEF_ARCH" \
-path "*/$PROFILE/build/*" -print -quit 2>/dev/null || true
)"
if [ -z "$CEF_DIR" ]; then
echo "❌ CEF dist not found under target/**/$PROFILE/build — build with --features cef first." >&2
echo "❌ cef_linux_$CEF_ARCH not found under target/**/$PROFILE/build — build with --features cef first." >&2
exit 1
fi

Expand Down
21 changes: 14 additions & 7 deletions scripts/cef/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ if [[ "$VERSION" == *-* ]]; then
RPM_ITERATION="0.${PRERELEASE}"
fi

# ARCH is read by appimagetool.
case "$(uname -m)" in
x86_64) export ARCH=x86_64; NFPM_ARCH=amd64 ;;
aarch64 | arm64) export ARCH=aarch64; NFPM_ARCH=arm64 ;;
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

STAGE="$ROOT/src-tauri/target/release"
OUT="$STAGE/bundle"
WORK="$STAGE/cef-pkg"
Expand All @@ -28,7 +35,7 @@ DISPLAY_NAME="${3:-}"
APPIMAGE_BIN_PATH="${4:-}"
if [ -z "$BIN_PATH" ]; then
for candidate in "$STAGE/Sable Nightly" "$STAGE/Sable" "$STAGE/sable" \
"$ROOT/src-tauri/target/x86_64-unknown-linux-gnu/release/sable"; do
"$ROOT/src-tauri/target/$ARCH-unknown-linux-gnu/release/sable"; do
[ -x "$candidate" ] || continue
BIN_PATH="$candidate"
break
Expand Down Expand Up @@ -111,10 +118,10 @@ EOF
chmod 755 "$PKGROOT/usr/bin/sable"
cp -a "$WORK/stage/share/." "$PKGROOT/usr/share/"

PKGROOT="$PKGROOT" PKG_VERSION="$DEB_VERSION" PKG_RELEASE=1 nfpm pkg -f nfpm.yaml -p deb \
-t "$OUT/deb/Sable-${VERSION}-linux-x86_64.deb"
PKGROOT="$PKGROOT" PKG_VERSION="$RPM_VERSION" PKG_RELEASE="$RPM_ITERATION" nfpm pkg -f nfpm.yaml -p rpm \
-t "$OUT/rpm/Sable-${VERSION}-linux-x86_64.rpm"
PKGROOT="$PKGROOT" PKG_ARCH="$NFPM_ARCH" PKG_VERSION="$DEB_VERSION" PKG_RELEASE=1 nfpm pkg -f nfpm.yaml -p deb \
-t "$OUT/deb/Sable-${VERSION}-linux-${ARCH}.deb"
PKGROOT="$PKGROOT" PKG_ARCH="$NFPM_ARCH" PKG_VERSION="$RPM_VERSION" PKG_RELEASE="$RPM_ITERATION" nfpm pkg -f nfpm.yaml -p rpm \
-t "$OUT/rpm/Sable-${VERSION}-linux-${ARCH}.rpm"
else
echo "nfpm not found" >&2
exit 1
Expand All @@ -130,7 +137,7 @@ exec "$HERE/usr/bin/sable" "$@"
EOF
chmod 755 "$APPDIR/AppRun"

APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 "$APPIMAGETOOL_CMD" "$APPDIR" \
"$OUT/appimage/Sable-${VERSION}-linux-x86_64.AppImage"
APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGETOOL_CMD" "$APPDIR" \
"$OUT/appimage/Sable-${VERSION}-linux-${ARCH}.AppImage"

echo "Packages in: $OUT"
2 changes: 1 addition & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ pub fn run() {
network::media_protocol::clear_media_session,
network::media_protocol::set_media_encryption,
#[cfg(target_os = "android")]
network::media_protocol::prepare_loopback_video,
network::media_protocol::prepare_loopback_media,
sentry::set_native_sentry_enabled,
share_inbox::share_inbox_drain,
share_inbox::share_inbox_read,
Expand Down
93 changes: 4 additions & 89 deletions src-tauri/src/network/media_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const READ_TIMEOUT: Duration = Duration::from_secs(30);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
// Small and multiplexed over one HTTP/2 connection, so a tight cap only serialises the timeline.
const MAX_CONCURRENT_THUMBNAIL_REQUESTS: usize = 12;
// Originals stay capped: more parallelism just splits the same mobile bandwidth.
const MAX_CONCURRENT_DOWNLOAD_REQUESTS: usize = 6;
// The frontend mounts (and starts requesting media) before it hands us the session, so a request
// may arrive first. `<img>` never retries, so waiting beats answering 503.
Expand All @@ -61,16 +60,6 @@ const MAX_TEMP_CACHE_BYTES: u64 = 2 * 1024 * 1024 * 1024; // 2 GiB

type FetchResult = Result<(String, Option<Arc<Vec<u8>>>, PathBuf), StatusCode>;

/// Uninhabited off Android, so the streaming branches compile out.
#[cfg(target_os = "android")]
type FetchProgress = Option<Arc<android_loopback::PendingMedia>>;
#[cfg(not(target_os = "android"))]
type FetchProgress = Option<std::convert::Infallible>;

// Published per flushed batch, so a reader never sees bytes still sitting in the write buffer.
#[cfg(target_os = "android")]
const PROGRESS_FLUSH_BYTES: u64 = 64 * 1024;

pub struct MediaSessionState {
session_store: SessionStore,
encryption: EncryptionStore,
Expand Down Expand Up @@ -276,7 +265,7 @@ pub fn set_media_encryption(

#[cfg(target_os = "android")]
#[tauri::command]
pub async fn prepare_loopback_video<R: Runtime>(
pub async fn prepare_loopback_media<R: Runtime>(
app: AppHandle<R>,
url: String,
) -> Result<String, String> {
Expand Down Expand Up @@ -401,44 +390,11 @@ async fn handle_request<R: Runtime>(
let dir = cache_dir(app).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let temp_dir = temp_cache_dir(app).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// Wry blocks the webview's `shouldInterceptRequest` thread for all of this and drops the
// response after 30s, so redirect before fetching. Range requests seek media already cached.
#[cfg(target_os = "android")]
if !loopback && range.is_none() {
if let Some(server) = &state.loopback {
let (redirect, pending) = server.redirect_pending(&session, &key);
if let Some(pending) = pending {
let app = app.clone();
let session = session.clone();
let key = key.clone();
tauri::async_runtime::spawn(async move {
let state = app.state::<MediaSessionState>();
let progress = Some(Arc::clone(&pending));
let stored =
ensure_cached(&state, &session, &key, media_url, dir, temp_dir, &progress)
.await
.ok()
.and_then(|(content_type, in_memory_body, disk_path)| {
// An in-memory body means there is no file for the loopback to open.
in_memory_body
.is_none()
.then_some((disk_path, content_type))
});
if let Some(server) = &state.loopback {
server.publish(&session, &key, stored.clone());
}
pending.resolve(stored);
});
}
return Ok(redirect);
}
}

let (content_type, in_memory_body, disk_path) =
ensure_cached(&state, &session, &key, media_url, dir, temp_dir, &None).await?;
ensure_cached(&state, &session, &key, media_url, dir, temp_dir).await?;

#[cfg(target_os = "android")]
if loopback && in_memory_body.is_none() && content_type.starts_with("video/") {
if loopback && in_memory_body.is_none() {
if let Some(loopback) = &state.loopback {
return Ok(loopback.redirect_response(&session, &key, disk_path, &content_type));
}
Expand All @@ -465,7 +421,6 @@ async fn ensure_cached(
media_url: Url,
dir: PathBuf,
temp_dir: PathBuf,
progress: &FetchProgress,
) -> Result<(String, Option<Arc<Vec<u8>>>, PathBuf), StatusCode> {
ensure_cached_with_limits(
state,
Expand All @@ -476,7 +431,6 @@ async fn ensure_cached(
temp_dir,
MAX_CACHE_BYTES,
MAX_TEMP_CACHE_BYTES,
progress,
)
.await
}
Expand All @@ -491,7 +445,6 @@ async fn ensure_cached_with_limits(
temp_dir: PathBuf,
max_persistent_cache_bytes: u64,
max_temp_cache_bytes: u64,
progress: &FetchProgress,
) -> Result<(String, Option<Arc<Vec<u8>>>, PathBuf), StatusCode> {
let body_path = dir.join(key);
let content_type_path = dir.join(format!("{key}.ct"));
Expand Down Expand Up @@ -569,7 +522,6 @@ async fn ensure_cached_with_limits(
temp_content_type_path,
max_persistent_cache_bytes,
max_temp_cache_bytes,
progress,
)
.await;

Expand Down Expand Up @@ -625,7 +577,6 @@ async fn fetch_and_cache(
temp_content_type_path: PathBuf,
max_persistent_cache_bytes: u64,
max_temp_cache_bytes: u64,
progress: &FetchProgress,
) -> Result<(String, Option<Arc<Vec<u8>>>, PathBuf), StatusCode> {
let permit = acquire_lane(state, &media_url).await;

Expand Down Expand Up @@ -697,15 +648,7 @@ async fn fetch_and_cache(

// Plaintext media streams to disk, so peak memory is one chunk instead of the whole file.
let staging_path = temp_body_path.with_extension("part");
match stream_to_staging_file(
&mut upstream,
temp_dir.clone(),
staging_path.clone(),
progress,
&content_type,
)
.await
{
match stream_to_staging_file(&mut upstream, temp_dir.clone(), staging_path.clone()).await {
StreamOutcome::Written(size) => {
drop(permit);
let (target_dir, target_body, target_ct, max_bytes) =
Expand Down Expand Up @@ -765,8 +708,6 @@ async fn stream_to_staging_file(
upstream: &mut tauri_plugin_http::reqwest::Response,
temp_dir: PathBuf,
staging_path: PathBuf,
progress: &FetchProgress,
content_type: &str,
) -> StreamOutcome {
if tokio::fs::create_dir_all(&temp_dir).await.is_err() {
return StreamOutcome::Unstorable;
Expand All @@ -777,16 +718,6 @@ async fn stream_to_staging_file(

let mut file = tokio::io::BufWriter::new(file);
let mut written: u64 = 0;
#[cfg(target_os = "android")]
let mut published: u64 = 0;

// Without a length the response cannot be framed, so readers wait for the finished file.
#[cfg(target_os = "android")]
if let (Some(pending), Some(total)) = (progress, upstream.content_length()) {
pending.begin_stream(staging_path.clone(), content_type.to_owned(), total);
}
#[cfg(not(target_os = "android"))]
let _ = (progress, content_type);

loop {
match upstream.chunk().await {
Expand All @@ -798,24 +729,9 @@ async fn stream_to_staging_file(
break;
}
written += chunk.len() as u64;

#[cfg(target_os = "android")]
if let Some(pending) = progress {
if written - published >= PROGRESS_FLUSH_BYTES {
if tokio::io::AsyncWriteExt::flush(&mut file).await.is_err() {
break;
}
published = written;
pending.advance(written);
}
}
}
Ok(None) => {
if tokio::io::AsyncWriteExt::flush(&mut file).await.is_ok() {
#[cfg(target_os = "android")]
if let Some(pending) = progress {
pending.advance(written);
}
return StreamOutcome::Written(written);
}
break;
Expand Down Expand Up @@ -1127,7 +1043,6 @@ mod tests {
temp,
1024 * 1024,
1024 * 1024,
&None,
)
.await;
fs::remove_dir_all(root).ok();
Expand Down
Loading
Loading