Windows: shippable cross-compiled .zip + console fix + release CI
The tree already links a working x86_64-pc-windows-gnu exe; turn that into a shipped artifact: - scripts/package.sh gains a `win` target (and `all` picks it up when the mingw toolchain is present) that cross-compiles and zips the exe + LICENSE + a README listing the yt-dlp/ffmpeg/mpv PATH deps. - Forgejo release workflow installs mingw-w64 + zip + the rust target, so a tag push produces yt-offline-<ver>-x86_64-windows.zip from the same Linux container — no Windows runner needed. - main.rs: attach_windows_console() (cfg(windows), windows-sys AttachConsole) reattaches a release build to the launching terminal so --web/CLI output is visible, while a double-click stays windowless (release sets windows_subsystem = "windows"). - ROADMAP 3.1 updated: Windows ships; macOS deferred (needs a Mac runner). Native Linux build + 128 unit / 11 integration tests still green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8b2578783a
commit
207013e957
6 changed files with 187 additions and 34 deletions
|
|
@ -31,12 +31,19 @@ jobs:
|
|||
# libxcb + friends are needed to *link* the eframe/winit GUI even
|
||||
# in headless CI. curl + file are used by the packaging script and
|
||||
# appimagetool. rpm provides rpmbuild for cargo-generate-rpm's
|
||||
# payload assembly on a Debian host.
|
||||
# payload assembly on a Debian host. mingw-w64 + zip let the same
|
||||
# script cross-compile the Windows .zip from this Linux container.
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
|
||||
curl file rpm ca-certificates
|
||||
curl file rpm ca-certificates mingw-w64 zip
|
||||
|
||||
- name: Add Windows cross-compile target
|
||||
# package.sh's `all` only builds the Windows zip when this target +
|
||||
# mingw + zip are all present (win_available), so this step is what
|
||||
# opts the release build into producing yt-offline-*-x86_64-windows.zip.
|
||||
run: rustup target add x86_64-pc-windows-gnu
|
||||
|
||||
- name: Cache cargo registry + build
|
||||
uses: actions/cache@v4
|
||||
|
|
@ -80,5 +87,7 @@ jobs:
|
|||
- `.deb` — Debian / Ubuntu / Mint
|
||||
- `.rpm` — Fedora / RHEL / openSUSE
|
||||
- `.AppImage` — any Linux (chmod +x and run)
|
||||
- `-x86_64-windows.zip` — Windows 10/11 (unzip, run yt-offline.exe)
|
||||
|
||||
Runtime requirements: yt-dlp, ffmpeg, mpv, xdg-utils on PATH.
|
||||
Runtime requirements: yt-dlp, ffmpeg, mpv on PATH (plus
|
||||
xdg-utils on Linux). The Windows zip's README lists the same.
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5432,6 +5432,7 @@ dependencies = [
|
|||
"tokio-util",
|
||||
"toml",
|
||||
"tower-http 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "t
|
|||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
rfd = { version = "0.15", features = ["tokio"] }
|
||||
|
||||
# Release Windows builds are linked as a GUI subsystem app (no console window —
|
||||
# see the windows_subsystem attribute in main.rs), which is right for the egui
|
||||
# GUI but means a `--web`/CLI run launched from a terminal has nowhere to print.
|
||||
# windows-sys gives us AttachConsole to reattach to the launching console at
|
||||
# runtime (see attach_windows_console in main.rs).
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_System_Console"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
# Thin LTO does cross-crate inlining without the rust-lld + bundled-sqlite
|
||||
|
|
|
|||
66
ROADMAP.md
66
ROADMAP.md
|
|
@ -126,25 +126,41 @@ instead of cascading one handler's panic into a dead server.
|
|||
|
||||
Once we're at parity, we push past Tartube on its own ground.
|
||||
|
||||
### 3.1 Cross-compile macOS + Windows binaries — GROUNDWORK DONE
|
||||
### 3.1 Cross-compile macOS + Windows binaries — WINDOWS SHIPS; macOS DEFERRED
|
||||
|
||||
The compile blockers are cleared: `cargo check --target
|
||||
x86_64-pc-windows-gnu` is green (only the upstream egui `f32: From<f64>`
|
||||
warnings). The Linux-only deps are now target-gated in `Cargo.toml` —
|
||||
`ksni` and `rfd`'s `xdg-portal` backend are `cfg(target_os = "linux")`
|
||||
only, with `rfd` falling back to its native Win32/AppKit backend
|
||||
elsewhere. `tray::start` is `cfg`-split: the SNI/ksni implementation on
|
||||
Linux, a no-op `None` stub on other OSes (windowed-only, exactly the
|
||||
no-SNI-host behavior). The rest was already portable — `disk_space`
|
||||
(`statvfs`), `plex` (symlinks; now with a Windows `symlink_file` path),
|
||||
the mpv IPC `UnixStream`, and the `chmod`/`PermissionsExt` guards are all
|
||||
`cfg(unix)`-gated. macOS is Unix, so those paths apply there unchanged.
|
||||
**Windows is now a shippable artifact.** `cargo build --release --target
|
||||
x86_64-pc-windows-gnu` doesn't just check — it *links* a working 22 MB
|
||||
`yt-offline.exe` via mingw-w64 (only the upstream egui `f32: From<f64>`
|
||||
warnings). `scripts/package.sh win` (and `all`, when the cross toolchain
|
||||
is present) cross-compiles and bundles it into
|
||||
`yt-offline-<ver>-x86_64-windows.zip` (exe + LICENSE + a README listing
|
||||
the yt-dlp/ffmpeg/mpv PATH deps). The Forgejo release workflow installs
|
||||
`mingw-w64` + `zip` + the rust target so a tag push produces the Windows
|
||||
zip alongside the Linux packages — all from the one Linux container, no
|
||||
Windows runner needed.
|
||||
|
||||
Remaining for a shipped binary: a real per-OS tray backend (e.g.
|
||||
`tray-icon`) if the tray is wanted off Linux; a CI matrix that actually
|
||||
*links* (mingw-w64 / an osxcross or macOS runner) and produces artifacts;
|
||||
and runtime testing on each OS. The hard part — making the tree compile
|
||||
off Linux — is done.
|
||||
One Windows-specific runtime fix landed with it: release builds set
|
||||
`windows_subsystem = "windows"` (no console window — right for the GUI),
|
||||
which would leave a `--web`/CLI run silent. `attach_windows_console()`
|
||||
(`main.rs`, `cfg(windows)`, via `windows-sys` `AttachConsole`) reattaches
|
||||
to the launching terminal so headless mode prints its logs there, while a
|
||||
double-click stays windowless.
|
||||
|
||||
The portability groundwork that made this possible: Linux-only deps are
|
||||
target-gated in `Cargo.toml` — `ksni` and `rfd`'s `xdg-portal` backend
|
||||
are `cfg(target_os = "linux")` only, `rfd` falling back to its native
|
||||
Win32/AppKit backend elsewhere; `tray::start` is a no-op `None` stub off
|
||||
Linux; `disk_space` (`statvfs`), `plex` (symlinks, with a Windows
|
||||
`symlink_file` path), the mpv IPC `UnixStream`, and the
|
||||
`chmod`/`PermissionsExt` guards are all `cfg(unix)`-gated.
|
||||
|
||||
**macOS is deferred, not blocked.** The code is Unix and cfg-gated, so it
|
||||
*should* compile on a Mac unchanged, but it can't be linked from the
|
||||
Linux host (no osxcross/SDK) and Codeberg has no macOS runners — so a real
|
||||
tested `.app`/binary needs either a GitHub Actions `macos-latest` runner
|
||||
(mirror CI there) or a local osxcross setup. Remaining for either OS: a
|
||||
real per-OS tray backend (e.g. `tray-icon`) if the tray is wanted off
|
||||
Linux, and on-hardware runtime testing.
|
||||
|
||||
### 3.2 Android client
|
||||
|
||||
|
|
@ -319,14 +335,14 @@ Roughly reverse-chronological. Items that closed out a roadmap line.
|
|||
error recovery (2.3), restore (2.4), stability hardening (2.5).
|
||||
- **Phase 3** is the "surpass" work now that we're at parity. Shipped so
|
||||
far: WebSocket progress (3.3), smart auto-tagging (3.4), federation
|
||||
(3.5), comment viewer (3.6), perceptual-hash dedup (3.7), and
|
||||
full-text search + transcript tooling (3.9). Still open: shipped
|
||||
Windows/macOS binaries (3.1; compile groundwork is done), Android
|
||||
(3.2), and plugin hooks (3.8).
|
||||
(3.5), comment viewer (3.6), perceptual-hash dedup (3.7), full-text
|
||||
search + transcript tooling (3.9), and the **Windows** half of 3.1
|
||||
(cross-compiled .zip + release CI). Still open: a **macOS** binary
|
||||
(3.1; needs a Mac runner), Android (3.2), and plugin hooks (3.8).
|
||||
- **Phase 4** items might be valuable, but commit to nothing.
|
||||
|
||||
Items inside a phase are loosely ordered by user-visible impact, not strict
|
||||
prerequisite. With the easy "surpass" wins banked, the highest-leverage
|
||||
remaining moves are **3.1 (Windows/macOS release artifacts)** for reach,
|
||||
**3.8 (plugin hooks)** for extensibility, and **3.2 (Android client)** for
|
||||
mobile-first consumption.
|
||||
prerequisite. With Windows banked, the highest-leverage remaining moves are
|
||||
**3.8 (plugin hooks)** for extensibility, **3.2 (Android client)** for
|
||||
mobile-first consumption, and the **macOS** half of 3.1 for reach (a Mac
|
||||
runner or osxcross).
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Build distributable packages for yt-offline.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/package.sh [deb|rpm|appimage|all]
|
||||
# scripts/package.sh [deb|rpm|appimage|win|all]
|
||||
#
|
||||
# With no argument, builds everything that's buildable on the current host.
|
||||
# Output lands in dist/. Each format is independent — a failure in one
|
||||
|
|
@ -12,8 +12,12 @@
|
|||
# - .deb → cargo-deb (cargo install cargo-deb)
|
||||
# - .rpm → cargo-generate-rpm (cargo install cargo-generate-rpm)
|
||||
# - AppImage → appimagetool (downloaded to dist/tools/ if missing)
|
||||
# - win .zip → x86_64-pc-windows-gnu target + mingw-w64 gcc + zip
|
||||
#
|
||||
# The release binary is built once up front and reused by every format.
|
||||
# The Linux release binary is built once up front and reused by the Linux
|
||||
# formats; the Windows .zip cross-compiles a separate target on demand.
|
||||
# `all` skips the Windows zip unless the cross toolchain is present, so a
|
||||
# plain `all` on a stock Linux box still succeeds.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
|
|
@ -135,22 +139,110 @@ APPRUN
|
|||
rm -rf "$appdir"
|
||||
}
|
||||
|
||||
# ── Windows .zip (cross-compiled via mingw-w64) ──────────────────────────────
|
||||
# Like the AppImage we don't bundle the subprocess deps (yt-dlp / ffmpeg /
|
||||
# mpv) — on Windows they're expected on PATH, and the README in the zip says
|
||||
# so. The release build links a GUI-subsystem exe (no console window); the
|
||||
# binary reattaches to the parent console at runtime for `--web`/CLI use
|
||||
# (see attach_windows_console in main.rs).
|
||||
WIN_TARGET="x86_64-pc-windows-gnu"
|
||||
build_win() {
|
||||
say "building Windows .zip ($WIN_TARGET)"
|
||||
if ! rustup target list --installed 2>/dev/null | grep -qx "$WIN_TARGET"; then
|
||||
RESULT[win]="skip rust target $WIN_TARGET not installed (rustup target add $WIN_TARGET)"
|
||||
return
|
||||
fi
|
||||
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
|
||||
RESULT[win]="skip mingw-w64 gcc not found (install mingw-w64)"
|
||||
return
|
||||
fi
|
||||
if ! command -v zip >/dev/null 2>&1; then
|
||||
RESULT[win]="skip zip not found"
|
||||
return
|
||||
fi
|
||||
if ! cargo build --release --target "$WIN_TARGET"; then
|
||||
RESULT[win]="fail cargo build --target $WIN_TARGET returned nonzero"
|
||||
return
|
||||
fi
|
||||
local exe="target/$WIN_TARGET/release/$PKG.exe"
|
||||
if [[ ! -f "$exe" ]]; then
|
||||
RESULT[win]="fail expected $exe after build"
|
||||
return
|
||||
fi
|
||||
|
||||
local staging="$DIST/${PKG}-win"
|
||||
rm -rf "$staging"
|
||||
mkdir -p "$staging"
|
||||
install -m755 "$exe" "$staging/$PKG.exe"
|
||||
install -m644 LICENSE "$staging/LICENSE.txt"
|
||||
cat > "$staging/README.txt" <<README
|
||||
yt-offline ${VERSION} — Windows build
|
||||
|
||||
Run the GUI by double-clicking yt-offline.exe.
|
||||
|
||||
To run the headless web server, open PowerShell or Command Prompt in this
|
||||
folder and run:
|
||||
|
||||
.\\yt-offline.exe --web 8080
|
||||
|
||||
then browse to http://localhost:8080. (Launched from a terminal the server
|
||||
prints its logs to that terminal; double-clicked it runs windowless.)
|
||||
|
||||
Runtime requirements — these external tools must be installed and on your
|
||||
PATH (the .exe shells out to them, it does not bundle them):
|
||||
|
||||
yt-dlp https://github.com/yt-dlp/yt-dlp (the downloader engine)
|
||||
ffmpeg https://ffmpeg.org (muxing / transcode / dedup)
|
||||
mpv https://mpv.io (desktop playback; optional)
|
||||
|
||||
config.toml and cookies.txt are read from the working directory, the same
|
||||
as on Linux. See the project README for configuration details.
|
||||
|
||||
Licensed under the GNU Affero General Public License v3 or later; see
|
||||
LICENSE.txt. Source: https://codeberg.org/${PKG}
|
||||
README
|
||||
|
||||
local out="$DIST/${PKG}-${VERSION}-x86_64-windows.zip"
|
||||
rm -f "$out"
|
||||
# -j junks paths so the zip has the three files at its root.
|
||||
if (cd "$staging" && zip -q -j "$out" "$PKG.exe" LICENSE.txt README.txt); then
|
||||
RESULT[win]="ok $out"
|
||||
else
|
||||
RESULT[win]="fail zip returned nonzero"
|
||||
fi
|
||||
rm -rf "$staging"
|
||||
}
|
||||
|
||||
# Whether to fold the Windows zip into a bare `all` run: only when the cross
|
||||
# toolchain is actually present, so `all` on a stock Linux box still passes.
|
||||
win_available() {
|
||||
rustup target list --installed 2>/dev/null | grep -qx "$WIN_TARGET" \
|
||||
&& command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 \
|
||||
&& command -v zip >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
||||
build_binary
|
||||
# The Windows zip doesn't need the Linux release binary, so skip that slow
|
||||
# build when Windows is all that was asked for.
|
||||
if [[ "$WANT" != "win" ]]; then
|
||||
build_binary
|
||||
fi
|
||||
|
||||
case "$WANT" in
|
||||
deb) build_deb ;;
|
||||
rpm) build_rpm ;;
|
||||
appimage) build_appimage ;;
|
||||
all) build_deb; build_rpm; build_appimage ;;
|
||||
*) err "unknown target '$WANT' (want: deb|rpm|appimage|all)"; exit 2 ;;
|
||||
win) build_win ;;
|
||||
all) build_deb; build_rpm; build_appimage
|
||||
if win_available; then build_win; fi ;;
|
||||
*) err "unknown target '$WANT' (want: deb|rpm|appimage|win|all)"; exit 2 ;;
|
||||
esac
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
echo
|
||||
say "package summary"
|
||||
status=0
|
||||
for fmt in deb rpm appimage; do
|
||||
for fmt in deb rpm appimage win; do
|
||||
[[ -v RESULT[$fmt] ]] || continue
|
||||
state="${RESULT[$fmt]%% *}"
|
||||
detail="${RESULT[$fmt]#* }"
|
||||
|
|
|
|||
27
src/main.rs
27
src/main.rs
|
|
@ -65,7 +65,34 @@ fn renderer_from_args(args: &[String]) -> eframe::Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Reattach stdout/stderr to the parent console on Windows.
|
||||
///
|
||||
/// Release builds set `windows_subsystem = "windows"`, so the process starts
|
||||
/// detached from any console — correct for the GUI (no flashing console
|
||||
/// window), but it means a `--web` or other CLI invocation launched from a
|
||||
/// terminal would print nothing. `AttachConsole(ATTACH_PARENT_PROCESS)` hooks
|
||||
/// us back onto the launching terminal's console when there is one (e.g. run
|
||||
/// from PowerShell/cmd). If the process has no parent console (double-clicked
|
||||
/// from Explorer) the call fails harmlessly and we stay silent, which is the
|
||||
/// desired GUI behavior. No-op on every non-Windows platform.
|
||||
#[cfg(windows)]
|
||||
fn attach_windows_console() {
|
||||
use windows_sys::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS};
|
||||
// SAFETY: AttachConsole takes no pointers and is safe to call with an
|
||||
// invalid/absent parent console — it just returns 0. We don't touch the
|
||||
// returned handles; the CRT picks up the now-attached console on the next
|
||||
// stdout/stderr write.
|
||||
unsafe {
|
||||
AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn attach_windows_console() {}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
attach_windows_console();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
// Load the config first — both modes share the channels_root path,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue