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
|
# libxcb + friends are needed to *link* the eframe/winit GUI even
|
||||||
# in headless CI. curl + file are used by the packaging script and
|
# in headless CI. curl + file are used by the packaging script and
|
||||||
# appimagetool. rpm provides rpmbuild for cargo-generate-rpm's
|
# 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: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
|
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
|
- name: Cache cargo registry + build
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|
@ -80,5 +87,7 @@ jobs:
|
||||||
- `.deb` — Debian / Ubuntu / Mint
|
- `.deb` — Debian / Ubuntu / Mint
|
||||||
- `.rpm` — Fedora / RHEL / openSUSE
|
- `.rpm` — Fedora / RHEL / openSUSE
|
||||||
- `.AppImage` — any Linux (chmod +x and run)
|
- `.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",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"tower-http 0.5.2",
|
"tower-http 0.5.2",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,14 @@ rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "t
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
rfd = { version = "0.15", features = ["tokio"] }
|
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]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
# Thin LTO does cross-crate inlining without the rust-lld + bundled-sqlite
|
# 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.
|
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
|
**Windows is now a shippable artifact.** `cargo build --release --target
|
||||||
x86_64-pc-windows-gnu` is green (only the upstream egui `f32: From<f64>`
|
x86_64-pc-windows-gnu` doesn't just check — it *links* a working 22 MB
|
||||||
warnings). The Linux-only deps are now target-gated in `Cargo.toml` —
|
`yt-offline.exe` via mingw-w64 (only the upstream egui `f32: From<f64>`
|
||||||
`ksni` and `rfd`'s `xdg-portal` backend are `cfg(target_os = "linux")`
|
warnings). `scripts/package.sh win` (and `all`, when the cross toolchain
|
||||||
only, with `rfd` falling back to its native Win32/AppKit backend
|
is present) cross-compiles and bundles it into
|
||||||
elsewhere. `tray::start` is `cfg`-split: the SNI/ksni implementation on
|
`yt-offline-<ver>-x86_64-windows.zip` (exe + LICENSE + a README listing
|
||||||
Linux, a no-op `None` stub on other OSes (windowed-only, exactly the
|
the yt-dlp/ffmpeg/mpv PATH deps). The Forgejo release workflow installs
|
||||||
no-SNI-host behavior). The rest was already portable — `disk_space`
|
`mingw-w64` + `zip` + the rust target so a tag push produces the Windows
|
||||||
(`statvfs`), `plex` (symlinks; now with a Windows `symlink_file` path),
|
zip alongside the Linux packages — all from the one Linux container, no
|
||||||
the mpv IPC `UnixStream`, and the `chmod`/`PermissionsExt` guards are all
|
Windows runner needed.
|
||||||
`cfg(unix)`-gated. macOS is Unix, so those paths apply there unchanged.
|
|
||||||
|
|
||||||
Remaining for a shipped binary: a real per-OS tray backend (e.g.
|
One Windows-specific runtime fix landed with it: release builds set
|
||||||
`tray-icon`) if the tray is wanted off Linux; a CI matrix that actually
|
`windows_subsystem = "windows"` (no console window — right for the GUI),
|
||||||
*links* (mingw-w64 / an osxcross or macOS runner) and produces artifacts;
|
which would leave a `--web`/CLI run silent. `attach_windows_console()`
|
||||||
and runtime testing on each OS. The hard part — making the tree compile
|
(`main.rs`, `cfg(windows)`, via `windows-sys` `AttachConsole`) reattaches
|
||||||
off Linux — is done.
|
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
|
### 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).
|
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
|
- **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
|
far: WebSocket progress (3.3), smart auto-tagging (3.4), federation
|
||||||
(3.5), comment viewer (3.6), perceptual-hash dedup (3.7), and
|
(3.5), comment viewer (3.6), perceptual-hash dedup (3.7), full-text
|
||||||
full-text search + transcript tooling (3.9). Still open: shipped
|
search + transcript tooling (3.9), and the **Windows** half of 3.1
|
||||||
Windows/macOS binaries (3.1; compile groundwork is done), Android
|
(cross-compiled .zip + release CI). Still open: a **macOS** binary
|
||||||
(3.2), and plugin hooks (3.8).
|
(3.1; needs a Mac runner), Android (3.2), and plugin hooks (3.8).
|
||||||
- **Phase 4** items might be valuable, but commit to nothing.
|
- **Phase 4** items might be valuable, but commit to nothing.
|
||||||
|
|
||||||
Items inside a phase are loosely ordered by user-visible impact, not strict
|
Items inside a phase are loosely ordered by user-visible impact, not strict
|
||||||
prerequisite. With the easy "surpass" wins banked, the highest-leverage
|
prerequisite. With Windows banked, the highest-leverage remaining moves are
|
||||||
remaining moves are **3.1 (Windows/macOS release artifacts)** for reach,
|
**3.8 (plugin hooks)** for extensibility, **3.2 (Android client)** for
|
||||||
**3.8 (plugin hooks)** for extensibility, and **3.2 (Android client)** for
|
mobile-first consumption, and the **macOS** half of 3.1 for reach (a Mac
|
||||||
mobile-first consumption.
|
runner or osxcross).
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# Build distributable packages for yt-offline.
|
# Build distributable packages for yt-offline.
|
||||||
#
|
#
|
||||||
# Usage:
|
# 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.
|
# With no argument, builds everything that's buildable on the current host.
|
||||||
# Output lands in dist/. Each format is independent — a failure in one
|
# Output lands in dist/. Each format is independent — a failure in one
|
||||||
|
|
@ -12,8 +12,12 @@
|
||||||
# - .deb → cargo-deb (cargo install cargo-deb)
|
# - .deb → cargo-deb (cargo install cargo-deb)
|
||||||
# - .rpm → cargo-generate-rpm (cargo install cargo-generate-rpm)
|
# - .rpm → cargo-generate-rpm (cargo install cargo-generate-rpm)
|
||||||
# - AppImage → appimagetool (downloaded to dist/tools/ if missing)
|
# - 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
|
set -uo pipefail
|
||||||
|
|
||||||
|
|
@ -135,22 +139,110 @@ APPRUN
|
||||||
rm -rf "$appdir"
|
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 ─────────────────────────────────────────────────────────────────
|
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
||||||
|
# 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
|
build_binary
|
||||||
|
fi
|
||||||
|
|
||||||
case "$WANT" in
|
case "$WANT" in
|
||||||
deb) build_deb ;;
|
deb) build_deb ;;
|
||||||
rpm) build_rpm ;;
|
rpm) build_rpm ;;
|
||||||
appimage) build_appimage ;;
|
appimage) build_appimage ;;
|
||||||
all) build_deb; build_rpm; build_appimage ;;
|
win) build_win ;;
|
||||||
*) err "unknown target '$WANT' (want: deb|rpm|appimage|all)"; exit 2 ;;
|
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
|
esac
|
||||||
|
|
||||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||||
echo
|
echo
|
||||||
say "package summary"
|
say "package summary"
|
||||||
status=0
|
status=0
|
||||||
for fmt in deb rpm appimage; do
|
for fmt in deb rpm appimage win; do
|
||||||
[[ -v RESULT[$fmt] ]] || continue
|
[[ -v RESULT[$fmt] ]] || continue
|
||||||
state="${RESULT[$fmt]%% *}"
|
state="${RESULT[$fmt]%% *}"
|
||||||
detail="${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<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
|
attach_windows_console();
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
// Load the config first — both modes share the channels_root path,
|
// Load the config first — both modes share the channels_root path,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue