From 22184eb09e149bda7f4a1050214e4709de56c103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 8 Mar 2026 14:17:39 +0100 Subject: [PATCH 1/6] common/encode_lavc: prefix global functions with mp_ To avoid conflicts with xavs2. --- audio/out/ao_lavc.c | 10 +++++----- common/encode_lavc.c | 10 +++++----- common/encode_lavc.h | 12 ++++++------ player/main.c | 2 +- video/out/vo_lavc.c | 8 ++++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/audio/out/ao_lavc.c b/audio/out/ao_lavc.c index 4bba27de3a3b1..0771062d167e6 100644 --- a/audio/out/ao_lavc.c +++ b/audio/out/ao_lavc.c @@ -98,7 +98,7 @@ static int init(struct ao *ao) { struct priv *ac = ao->priv; - ac->enc = encoder_context_alloc(ao->encode_lavc_ctx, STREAM_AUDIO, ao->log); + ac->enc = mp_encoder_context_alloc(ao->encode_lavc_ctx, STREAM_AUDIO, ao->log); if (!ac->enc) return -1; talloc_steal(ac, ac->enc); @@ -137,10 +137,10 @@ static int init(struct ao *ao) encoder->sample_fmt = af_to_avformat(ao->format); encoder->bits_per_raw_sample = ac->sample_size * 8; - if (!encoder_init_codec_and_muxer(ac->enc)) + if (!mp_encoder_init_codec_and_muxer(ac->enc)) goto fail; - ac->worst_time_base = encoder_get_mux_timebase_unlocked(ac->enc); + ac->worst_time_base = mp_encoder_get_mux_timebase_unlocked(ac->enc); ac->pcmhack = 0; if (encoder->frame_size <= 1) ac->pcmhack = av_get_bits_per_sample(encoder->codec_id) / 8; @@ -183,7 +183,7 @@ static void uninit(struct ao *ao) if (!ac->shutdown) { if (!write_frame(ao, MP_EOF_FRAME)) MP_WARN(ao, "could not flush last frame\n"); - encoder_encode(ac->enc, NULL); + mp_encoder_encode(ac->enc, NULL); } talloc_free(ac->filter_root); @@ -217,7 +217,7 @@ static void encode(struct ao *ao, struct mp_aframe *af) ac->lastpts = frame_pts; frame->quality = encoder->global_quality; - encoder_encode(ac->enc, frame); + mp_encoder_encode(ac->enc, frame); av_frame_free(&frame); } diff --git a/common/encode_lavc.c b/common/encode_lavc.c index 4772759ee3a7d..512be0c4a83b0 100644 --- a/common/encode_lavc.c +++ b/common/encode_lavc.c @@ -424,7 +424,7 @@ static void encode_lavc_add_packet(struct mux_stream *dst, AVPacket *pkt) av_packet_unref(pkt); } -AVRational encoder_get_mux_timebase_unlocked(struct encoder_context *p) +AVRational mp_encoder_get_mux_timebase_unlocked(struct encoder_context *p) { return p->mux_stream->st->time_base; } @@ -746,7 +746,7 @@ bool encode_lavc_stream_type_ok(struct encode_lavc_context *ctx, return !!find_codec_for(ctx, type, &auto_codec) || !auto_codec; } -struct encoder_context *encoder_context_alloc(struct encode_lavc_context *ctx, +struct encoder_context *mp_encoder_context_alloc(struct encode_lavc_context *ctx, enum stream_type type, struct mp_log *log) { @@ -826,7 +826,7 @@ static void encoder_2pass_prepare(struct encoder_context *p) talloc_free(filename); } -bool encoder_init_codec_and_muxer(struct encoder_context *p) +bool mp_encoder_init_codec_and_muxer(struct encoder_context *p) { mp_assert(!avcodec_is_open(p->encoder)); @@ -893,7 +893,7 @@ bool encoder_init_codec_and_muxer(struct encoder_context *p) return false; } -bool encoder_encode(struct encoder_context *p, AVFrame *frame) +bool mp_encoder_encode(struct encoder_context *p, AVFrame *frame) { int status = avcodec_send_frame(p->encoder, frame); if (status < 0) { @@ -929,7 +929,7 @@ bool encoder_encode(struct encoder_context *p, AVFrame *frame) return false; } -void encoder_update_log(struct mpv_global *global) +void mp_encoder_update_log(struct mpv_global *global) { struct encode_opts *options = mp_get_config_group(NULL, global, &encode_config); if (options->file && (!strcmp(options->file, "-") || diff --git a/common/encode_lavc.h b/common/encode_lavc.h index 9dd8d5535958b..36c33bc2c3a68 100644 --- a/common/encode_lavc.h +++ b/common/encode_lavc.h @@ -59,7 +59,7 @@ struct encode_lavc_context { // Static information after encoder init. This never changes (even if there are // dynamic runtime changes, they have to work over AVPacket side data). // For use in encoder_context, most fields are copied from encoder_context.encoder -// by encoder_init_codec_and_muxer(). +// by mp_encoder_init_codec_and_muxer(). struct encoder_stream_info { AVRational timebase; // timebase used by the encoder (in frames/out packets) AVCodecParameters *codecpar; @@ -91,20 +91,20 @@ struct encoder_context { // Free with talloc_free(). (Keep in mind actual deinitialization requires // sending a flush packet.) // This can fail and return NULL. -struct encoder_context *encoder_context_alloc(struct encode_lavc_context *ctx, +struct encoder_context *mp_encoder_context_alloc(struct encode_lavc_context *ctx, enum stream_type type, struct mp_log *log); // After setting your codec parameters on p->encoder, you call this to "open" // the encoder. This also initializes p->mux_stream. Returns false on failure. -bool encoder_init_codec_and_muxer(struct encoder_context *p); +bool mp_encoder_init_codec_and_muxer(struct encoder_context *p); // Encode the frame and write the packet. frame is ref'ed as need. -bool encoder_encode(struct encoder_context *p, AVFrame *frame); +bool mp_encoder_encode(struct encoder_context *p, AVFrame *frame); // Return muxer timebase (only available if p->mux_stream is initialized). -AVRational encoder_get_mux_timebase_unlocked(struct encoder_context *p); +AVRational mp_encoder_get_mux_timebase_unlocked(struct encoder_context *p); -void encoder_update_log(struct mpv_global *global); +void mp_encoder_update_log(struct mpv_global *global); #endif diff --git a/player/main.c b/player/main.c index 0ef3d7f6d6581..5cc273c3ed74b 100644 --- a/player/main.c +++ b/player/main.c @@ -154,7 +154,7 @@ void mp_update_logging(struct MPContext *mpctx, bool preinit) terminal_setup_getch(mpctx->input); if (enabled) - encoder_update_log(mpctx->global); + mp_encoder_update_log(mpctx->global); } void mp_print_version(struct mp_log *log, int always) diff --git a/video/out/vo_lavc.c b/video/out/vo_lavc.c index a03901c71358f..54117dcf56fee 100644 --- a/video/out/vo_lavc.c +++ b/video/out/vo_lavc.c @@ -46,7 +46,7 @@ struct priv { static int preinit(struct vo *vo) { struct priv *vc = vo->priv; - vc->enc = encoder_context_alloc(vo->encode_lavc_ctx, STREAM_VIDEO, vo->log); + vc->enc = mp_encoder_context_alloc(vo->encode_lavc_ctx, STREAM_VIDEO, vo->log); if (!vc->enc) return -1; talloc_steal(vc, vc->enc); @@ -59,7 +59,7 @@ static void uninit(struct vo *vo) struct encoder_context *enc = vc->enc; if (!vc->shutdown) - encoder_encode(enc, NULL); // finish encoding + mp_encoder_encode(enc, NULL); // finish encoding } static int reconfig2(struct vo *vo, struct mp_image *img) @@ -143,7 +143,7 @@ static int reconfig2(struct vo *vo, struct mp_image *img) else encoder->framerate = (AVRational){ 240, 1 }; - if (!encoder_init_codec_and_muxer(vc->enc)) + if (!mp_encoder_init_codec_and_muxer(vc->enc)) goto error; return 0; @@ -232,7 +232,7 @@ static bool draw_frame(struct vo *vo, struct vo_frame *voframe) frame->pts = rint(outpts * av_q2d(av_inv_q(avc->time_base))); frame->pict_type = 0; // keep this at unknown/undefined frame->quality = avc->global_quality; - encoder_encode(enc, frame); + mp_encoder_encode(enc, frame); av_frame_free(&frame); done: From 8f4c11ec66a1acd99ed517e348609e5f460cedb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 8 Mar 2026 14:22:02 +0100 Subject: [PATCH 2/6] vulkan/context_win: load vkCreateWin32SurfaceKHR https://github.com/BtbN/Vulkan-Shim-Loader doesn't expose those symbols, so just load them manually. --- video/out/vulkan/context_win.c | 10 +++++++++- video/out/vulkan/utils.c | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/video/out/vulkan/context_win.c b/video/out/vulkan/context_win.c index a05124347876d..a68a3ef51e807 100644 --- a/video/out/vulkan/context_win.c +++ b/video/out/vulkan/context_win.c @@ -97,7 +97,15 @@ static bool win_init(struct ra_ctx *ctx) }; VkInstance inst = vk->vkinst->instance; - VkResult res = vkCreateWin32SurfaceKHR(inst, &wininfo, NULL, &vk->surface); + mp_assert(vk->vkinst->get_proc_addr); + PFN_vkCreateWin32SurfaceKHR pCreateWin32SurfaceKHR = (PFN_vkCreateWin32SurfaceKHR) + vk->vkinst->get_proc_addr(inst, "vkCreateWin32SurfaceKHR"); + if (!pCreateWin32SurfaceKHR) { + MP_MSG(ctx, msgl, "Failed to load vkCreateWin32SurfaceKHR\n"); + goto error; + } + + VkResult res = pCreateWin32SurfaceKHR(inst, &wininfo, NULL, &vk->surface); if (res != VK_SUCCESS) { MP_MSG(ctx, msgl, "Failed creating Windows surface\n"); goto error; diff --git a/video/out/vulkan/utils.c b/video/out/vulkan/utils.c index d01fc3be0b883..c535291b1e750 100644 --- a/video/out/vulkan/utils.c +++ b/video/out/vulkan/utils.c @@ -34,7 +34,11 @@ void mpvk_uninit(struct mpvk_ctx *vk) { if (vk->surface) { mp_assert(vk->vkinst); - vkDestroySurfaceKHR(vk->vkinst->instance, vk->surface, NULL); + mp_assert(vk->vkinst->get_proc_addr); + PFN_vkDestroySurfaceKHR pDestroySurfaceKHR = (PFN_vkDestroySurfaceKHR) + vk->vkinst->get_proc_addr(vk->vkinst->instance, "vkDestroySurfaceKHR"); + if (pDestroySurfaceKHR) + pDestroySurfaceKHR(vk->vkinst->instance, vk->surface, NULL); vk->surface = VK_NULL_HANDLE; } From b6a183888594e67bb313d1a2c5ac64eaa5b7eddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 25 Sep 2026 22:20:19 +0200 Subject: [PATCH 3/6] ci/mingw64-full: add full static mingw64 builds Build mpv and libmpv inside the FFmpeg-Builds container images, which ship a mingw-w64 cross toolchain and every FFmpeg dependency as a static library. FFmpeg itself, LuaJIT, subrandr and the C++/WinRT projection are built on top. One day we might fork the FFmpeg-Builds and do mpv-Builds, but the overhead of maintaining that is bigger than current cost of building few deps on top. x86_64 and aarch64 are built in the GPL and the LGPL variant. The GPL variant publishes mpv.exe, both variants publish a libmpv package with the DLL, its import library and the headers. rav1e and librsvg are Rust libraries with their own copy of the Rust standard library, which collides with subrandr's at link time, so both are left out of FFmpeg for now. --- .github/workflows/build.yml | 60 +++++++++++++++++ DOCS/compile-windows.md | 10 +++ ci/build-mingw64-full.sh | 128 ++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100755 ci/build-mingw64-full.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3b21b7a14ecf..2e8bf675883ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -129,6 +129,66 @@ jobs: path: ${{ env.CCACHE_DIR }} key: ${{ env.TARGET }}-${{ steps.get_time.outputs.timestamp }} + mingw64-full: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [win64, winarm64] + variant: [gpl, lgpl] + container: + image: ghcr.io/btbn/ffmpeg-builds/${{ matrix.target }}-${{ matrix.variant }}:latest + env: + WINEPREFIX: /tmp/wine + steps: + - uses: actions/checkout@v7 + + - name: Build + id: build + run: | + ./ci/build-mingw64-full.sh + + - name: Print meson log + if: ${{ failure() && steps.build.outcome == 'failure' }} + run: | + cat ./build/meson-logs/meson-log.txt + + - name: Functional test + if: ${{ matrix.target == 'win64' }} + run: | + cd artifact && wine ./mpv.com -v --no-config + + - name: Run meson tests + if: ${{ matrix.target == 'win64' }} + id: tests + run: | + meson test -C build + + - name: Print meson test log + if: ${{ failure() && steps.tests.outcome == 'failure' }} + run: | + cat ./build/meson-logs/testlog.txt + + - name: Artifact name + id: artifact + run: | + VERSION_FILE=$(find . -path "*/common/version.h" -type f | head -n 1) + VER=$(grep VERSION "$VERSION_FILE" | cut -d'"' -f2) + PR=${{ github.event.pull_request.number && format('-pr{0}', github.event.pull_request.number) || '' }} + echo "mpv=mpv-$VER$PR-${{ github.run_id }}-$FFBUILD_TOOLCHAIN-full" >> "$GITHUB_OUTPUT" + echo "libmpv=libmpv-$VER$PR-${{ github.run_id }}-$FFBUILD_TOOLCHAIN-${{ matrix.variant }}" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@v7 + if: ${{ matrix.variant == 'gpl' }} + with: + name: ${{ steps.artifact.outputs.mpv }} + path: artifact/* + + - uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifact.outputs.libmpv }} + path: artifact-libmpv/* + win32: runs-on: ${{ matrix.os }} strategy: diff --git a/DOCS/compile-windows.md b/DOCS/compile-windows.md index 705271ecc8718..a53de28bc1564 100644 --- a/DOCS/compile-windows.md +++ b/DOCS/compile-windows.md @@ -30,6 +30,16 @@ cross-compiling environment. For a basic example, please refer to Alternatively, consider using [mpv-winbuild-cmake](https://github.com/shinchiro/mpv-winbuild-cmake), which bootstraps a MinGW-w64 toolchain and builds mpv along with its dependencies. +The full Windows builds on the [release page](https://github.com/mpv-player/mpv/releases) +are cross-compiled in the [FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds) +container images, which provide a toolchain and all dependencies as static +libraries. [ci/build-mingw64-full.sh](https://github.com/mpv-player/mpv/blob/master/ci/build-mingw64-full.sh) +reproduces such a build from the mpv source tree: + +``` bash +docker run --rm -v "$PWD:/mpv" -w /mpv ghcr.io/btbn/ffmpeg-builds/win64-gpl:latest ./ci/build-mingw64-full.sh +``` + ### Example with Meson 1. Create ``cross-file.txt`` with definitions for your toolchain and target platform. diff --git a/ci/build-mingw64-full.sh b/ci/build-mingw64-full.sh new file mode 100755 index 0000000000000..eaace16c060bd --- /dev/null +++ b/ci/build-mingw64-full.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# +# Static mpv and libmpv builds on top of the FFmpeg-Builds container images +# (ghcr.io/btbn/ffmpeg-builds/-, see +# https://github.com/BtbN/FFmpeg-Builds). The image ships the cross toolchain, +# a matching /cross.meson and every FFmpeg dependency as static library in +# $FFBUILD_PREFIX, so only FFmpeg itself and the things FFmpeg does not need +# are built here. Run it from the mpv source tree inside the container: +# +# docker run --rm -v "$PWD:/mpv" -w /mpv \ +# ghcr.io/btbn/ffmpeg-builds/win64-gpl:latest ./ci/build-mingw64-full.sh +# +# The mpv binaries end up in artifact/, the libmpv package in artifact-libmpv/. +set -euo pipefail + +: "${FFBUILD_PREFIX:?not running in a FFmpeg-Builds image}" +: "${VARIANT:?not running in a FFmpeg-Builds image}" + +. ./ci/build-common.sh + +# The dependency headers are not ours to fix, keep --werror to mpv's own code. +CFLAGS="${CFLAGS/-I${FFBUILD_PREFIX}\/include/-isystem${FFBUILD_PREFIX}/include}" +CXXFLAGS="${CXXFLAGS/-I${FFBUILD_PREFIX}\/include/-isystem${FFBUILD_PREFIX}/include}" + +gpl=false +[[ $VARIANT == gpl* ]] && gpl=true + +gitclone="git clone --depth=1 --recursive --shallow-submodules" +group() { echo "::group::$1"; } +endgroup() { echo "::endgroup::"; } + +group "Building FFmpeg" +$gitclone https://github.com/FFmpeg/FFmpeg.git ffmpeg +pushd ffmpeg +# rav1e and librsvg bring their own Rust runtime, which clashes with subrandr's. +./configure --prefix="$FFBUILD_PREFIX" --pkg-config-flags="--static" \ + $FFBUILD_TARGET_FLAGS $FF_CONFIGURE \ + --extra-cflags="$FF_CFLAGS" --extra-cxxflags="$FF_CXXFLAGS" \ + --extra-libs="$FF_LIBS" --extra-ldflags="$FF_LDFLAGS" \ + --extra-ldexeflags="$FF_LDEXEFLAGS" \ + --cc="$CC" --cxx="$CXX" --ar="$AR" --ranlib="$RANLIB" --nm="$NM" \ + --enable-static --disable-shared --disable-{doc,programs} \ + --disable-{librav1e,librsvg,openal} || { cat ffbuild/config.log; exit 1; } +make -j"$(nproc)" +make install +popd +endgroup + +group "Building LuaJIT" +$gitclone -b v2.1 https://github.com/LuaJIT/LuaJIT.git +pushd LuaJIT +# Strip -ldl, not needed for Windows. +sed -i "/^Libs\.private/d" etc/luajit.pc +# FILE_T and INSTALL_DEP are only needed to glue install target. +make TARGET_SYS=Windows PREFIX="$FFBUILD_PREFIX" HOST_CC="$HOST_CC" \ + CFLAGS="$HOST_CFLAGS" CROSS="$FFBUILD_CROSS_PREFIX" TARGET_CFLAGS="$CFLAGS" \ + BUILDMODE=static XCFLAGS=-DLUAJIT_ENABLE_LUA52COMPAT FILE_T=luajit.exe \ + INSTALL_DEP=src/luajit.exe amalg install +popd +endgroup + +group "Building subrandr" +build_subrandr "$FFBUILD_PREFIX" --target "$FFBUILD_RUST_TARGET" \ + --static-library true --shared-library false +endgroup + +group "Building cppwinrt" +# win32-smtc needs the C++/WinRT projection headers, generated from the +# Windows metadata that windows-rs carries. +cppwinrt_ver=2.0.250303.1 +windows_rs_ver=73 +wget -q "https://github.com/microsoft/cppwinrt/archive/${cppwinrt_ver}.tar.gz" -O - | tar -xz +git clone --depth=1 --filter=blob:none --sparse -b "$windows_rs_ver" \ + https://github.com/microsoft/windows-rs.git +git -C windows-rs sparse-checkout set crates/libs/bindgen/default +mkdir -p "cppwinrt-$cppwinrt_ver/build" +pushd "cppwinrt-$cppwinrt_ver/build" +CFLAGS="$HOST_CFLAGS" CXXFLAGS="$HOST_CXXFLAGS" LDFLAGS= \ + cmake -GNinja -DCMAKE_CXX_COMPILER="$HOST_CXX" -DCMAKE_BUILD_TYPE=Release \ + -DCPPWINRT_BUILD_VERSION="$cppwinrt_ver" .. +ninja cppwinrt +./cppwinrt -input ../../windows-rs/crates/libs/bindgen/default \ + -output "$FFBUILD_PREFIX/include" +popd +endgroup + +group "Building mpv" +mkdir -p subprojects +meson wrap install mujs +meson subprojects download + +mpv_args=( + --cross-file /cross.meson $common_args + --buildtype=release + --prefer-static + --default-library=shared + -Dc_link_args="$FF_LIBS" + -Dcpp_link_args="$FF_LIBS" + -Dgpl=$gpl + --force-fallback-for=mujs + -Dmujs:werror=false + -Dmujs:default_library=static + -Dlua=luajit + -D{amf,d3d11,javascript,lcms2,libbluray,libcurl,shaderc,spirv-cross}=enabled + -D{subrandr,vulkan,win32-smtc,zimg}=enabled +) +if $gpl; then + # Only the GPL image carries the dependencies of the GPL features. + mpv_args+=(-D{dvda,dvdnav,rubberband}=enabled) +fi +meson setup build "${mpv_args[@]}" +meson compile -C build +endgroup + +group "Packaging" +license=LICENSE.LGPL +if $gpl; then + license=LICENSE.GPL +fi +mkdir -p artifact artifact-libmpv/include/mpv +cp -p build/mpv.{exe,com} etc/mpv-*.bat "$license" artifact/ +cp -p build/libmpv*.dll build/libmpv*.dll.a "$license" artifact-libmpv/ +cp -p include/mpv/*.h artifact-libmpv/include/mpv/ +cp -p "$FFBUILD_PREFIX"/share/java/libbluray-*.jar artifact/ +cp -p "$FFBUILD_PREFIX"/share/java/libbluray-*.jar artifact-libmpv/ +"${FFBUILD_CROSS_PREFIX}strip" artifact/mpv.{exe,com} artifact-libmpv/libmpv*.dll +ls -l artifact artifact-libmpv +endgroup From cab192a466ae89ba9817c9835b86682465feb326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 25 Sep 2026 22:22:10 +0200 Subject: [PATCH 4/6] ci: move release publishing into ci/publish-release.sh --- .github/workflows/build.yml | 85 +---------------------------------- ci/publish-release.sh | 90 +++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 84 deletions(-) create mode 100755 ci/publish-release.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e8bf675883ea..55aaa7579a8cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -810,87 +810,4 @@ jobs: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -eu - tag=git-release - base_url="https://github.com/$GH_REPO/releases/download/$tag" - - artifacts=$(gh api "repos/{owner}/{repo}/actions/runs/${{ github.run_id }}/artifacts") - - # Attach every build artifact unchanged; the notes link to them with - # labels derived from the verbose names. - mkdir -p release - jq -r '.artifacts[] | "\(.id)\t\(.name)"' <<< "$artifacts" | - while IFS=$'\t' read -r id name; do - gh api "repos/{owner}/{repo}/actions/artifacts/$id/zip" > "release/$name.zip" - done - - section() { - jq -r --arg url "$base_url" --arg re "$1" ' - def pretty: - (if test("i686") then "i686" - elif test("aarch64") then "arm64" - elif test("-arm$") then "arm64" - elif test("intel") then "x86_64" - else "x86_64" end) as $arch - | [ (if test("mingw") then "GCC" elif test("msvc") then "Clang" else empty end), - (if test("macos") then "macOS " + capture("macos-(?[0-9]+)").v else empty end), - (if test("-pdb") then "debug symbols" else empty end) ] as $q - | $arch + (if ($q | length) > 0 then " (" + ($q | join(", ")) + ")" else "" end); - # primary builds first (x86_64, arm64, i686; Clang before GCC), debug symbols last - def rank: - [ (if test("-pdb") then 1 else 0 end), - (if test("x86_64|intel") then 0 elif test("i686") then 2 else 1 end), - (if test("mingw") then 1 else 0 end), - (if test("macos") then (capture("macos-(?[0-9]+)").v | tonumber) else 0 end) ]; - [ .artifacts[] | select(.name | test($re)) ] | sort_by(.name | rank) - | map("* [\(.name | pretty)](\($url)/\(.name).zip)") | join("\n") - ' <<< "$artifacts" - } - - # Match the version string baked into the build (see common/meson.build). - version=$(git describe --abbrev=9 --tags --dirty --match "v0.*") - notes=$(cat < [!WARNING] - > These are untested development builds of the very latest \`master\`. - > They are **not** official releases. For stable builds see the - > [latest release](https://github.com/$GH_REPO/releases/latest). - EOF - ) - - # Create the release once (which also creates the tag), then only edit - # it in place so watchers aren't notified on every master push. Stale - # assets are dropped first since their names embed the run id. - if gh release view "$tag" >/dev/null 2>&1; then - gh release view "$tag" --json assets --jq '.assets[].name' | - while read -r asset; do - gh release delete-asset "$tag" "$asset" --yes - done - gh release edit "$tag" \ - --prerelease \ - --latest=false \ - --title "mpv development build" \ - --notes "$notes" - gh release upload "$tag" release/* - else - gh release create "$tag" \ - --target "$GITHUB_SHA" \ - --prerelease \ - --latest=false \ - --title "mpv development build" \ - --notes "$notes" \ - release/* - fi + ./ci/publish-release.sh diff --git a/ci/publish-release.sh b/ci/publish-release.sh new file mode 100755 index 0000000000000..f81692be685ab --- /dev/null +++ b/ci/publish-release.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# +# Attaches the artifacts of the current workflow run to the rolling +# "git-release" pre-release and rewrites its notes. +# +# Needs GH_REPO and GH_TOKEN on top of the GITHUB_* variables of the run. +set -euo pipefail +tag=git-release +base_url="https://github.com/$GH_REPO/releases/download/$tag" + +artifacts=$(gh api "repos/{owner}/{repo}/actions/runs/$GITHUB_RUN_ID/artifacts") + +# Attach every build artifact unchanged; the notes link to them with +# labels derived from the verbose names. +mkdir -p release +jq -r '.artifacts[] | "\(.id)\t\(.name)"' <<< "$artifacts" | +while IFS=$'\t' read -r id name; do + gh api "repos/{owner}/{repo}/actions/artifacts/$id/zip" > "release/$name.zip" +done + +section() { + jq -r --arg url "$base_url" --arg re "$1" ' + def pretty: + (if test("i686") then "i686" + elif test("aarch64") then "arm64" + elif test("-arm$") then "arm64" + elif test("intel") then "x86_64" + else "x86_64" end) as $arch + | [ (if test("mingw") then "GCC" elif test("msvc") then "Clang" else empty end), + (if test("macos") then "macOS " + capture("macos-(?[0-9]+)").v else empty end), + (if test("-pdb") then "debug symbols" else empty end) ] as $q + | $arch + (if ($q | length) > 0 then " (" + ($q | join(", ")) + ")" else "" end); + # primary builds first (x86_64, arm64, i686; Clang before GCC), debug symbols last + def rank: + [ (if test("-pdb") then 1 else 0 end), + (if test("x86_64|intel") then 0 elif test("i686") then 2 else 1 end), + (if test("mingw") then 1 else 0 end), + (if test("macos") then (capture("macos-(?[0-9]+)").v | tonumber) else 0 end) ]; + [ .artifacts[] | select(.name | test($re)) ] | sort_by(.name | rank) + | map("* [\(.name | pretty)](\($url)/\(.name).zip)") | join("\n") + ' <<< "$artifacts" +} + +# Match the version string baked into the build (see common/meson.build). +version=$(git describe --abbrev=9 --tags --dirty --match "v0.*") +notes=$(cat < [!WARNING] +> These are untested development builds of the very latest \`master\`. +> They are **not** official releases. For stable builds see the +> [latest release](https://github.com/$GH_REPO/releases/latest). +EOF +) + +# Create the release once (which also creates the tag), then only edit +# it in place so watchers aren't notified on every master push. Stale +# assets are dropped first since their names embed the run id. +if gh release view "$tag" >/dev/null 2>&1; then + gh release view "$tag" --json assets --jq '.assets[].name' | + while read -r asset; do + gh release delete-asset "$tag" "$asset" --yes + done + gh release edit "$tag" \ + --prerelease \ + --latest=false \ + --title "mpv development build" \ + --notes "$notes" + gh release upload "$tag" release/* +else + gh release create "$tag" \ + --target "$GITHUB_SHA" \ + --prerelease \ + --latest=false \ + --title "mpv development build" \ + --notes "$notes" \ + release/* +fi From 723e709e4ebbf64f514861d375487a30362957c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 25 Sep 2026 22:22:11 +0200 Subject: [PATCH 5/6] ci/publish-release: publish the full builds and libmpv Attach the full x86_64 and aarch64 builds instead of the shared x86_64 mingw-w64 build, which they supersede. The i686 build stays, it exists only as the shared build. Add a libmpv section with the GPL and LGPL packages, label every entry by architecture and flavour. --- .github/workflows/build.yml | 2 +- ci/publish-release.sh | 123 ++++++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55aaa7579a8cd..fe8286d74907a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -790,7 +790,7 @@ jobs: publish: name: Publish git release - needs: [mingw, win32, macos] + needs: [mingw, mingw64-full, win32, macos] if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'mpv-player/mpv' runs-on: ubuntu-slim diff --git a/ci/publish-release.sh b/ci/publish-release.sh index f81692be685ab..72fe28ccff727 100755 --- a/ci/publish-release.sh +++ b/ci/publish-release.sh @@ -5,45 +5,57 @@ # # Needs GH_REPO and GH_TOKEN on top of the GITHUB_* variables of the run. set -euo pipefail -tag=git-release -base_url="https://github.com/$GH_REPO/releases/download/$tag" artifacts=$(gh api "repos/{owner}/{repo}/actions/runs/$GITHUB_RUN_ID/artifacts") -# Attach every build artifact unchanged; the notes link to them with -# labels derived from the verbose names. +# Only the builds people are meant to download are published, the other +# artifacts exist for CI coverage. +windows='mingw32-full|i686-w64-mingw32|windows-msvc' +published="$windows|macos-|^libmpv-" + +tag=git-release + mkdir -p release -jq -r '.artifacts[] | "\(.id)\t\(.name)"' <<< "$artifacts" | +jq -r --arg re "$published" ' + .artifacts[] | select(.name | test($re)) | "\(.id)\t\(.name)" +' <<< "$artifacts" | while IFS=$'\t' read -r id name; do - gh api "repos/{owner}/{repo}/actions/artifacts/$id/zip" > "release/$name.zip" + gh api "repos/{owner}/{repo}/actions/artifacts/$id/zip" > "release/$name.zip" done +base_url="https://github.com/$GH_REPO/releases/download/$tag" + section() { - jq -r --arg url "$base_url" --arg re "$1" ' - def pretty: - (if test("i686") then "i686" - elif test("aarch64") then "arm64" - elif test("-arm$") then "arm64" - elif test("intel") then "x86_64" - else "x86_64" end) as $arch - | [ (if test("mingw") then "GCC" elif test("msvc") then "Clang" else empty end), - (if test("macos") then "macOS " + capture("macos-(?[0-9]+)").v else empty end), - (if test("-pdb") then "debug symbols" else empty end) ] as $q - | $arch + (if ($q | length) > 0 then " (" + ($q | join(", ")) + ")" else "" end); - # primary builds first (x86_64, arm64, i686; Clang before GCC), debug symbols last - def rank: - [ (if test("-pdb") then 1 else 0 end), - (if test("x86_64|intel") then 0 elif test("i686") then 2 else 1 end), - (if test("mingw") then 1 else 0 end), - (if test("macos") then (capture("macos-(?[0-9]+)").v | tonumber) else 0 end) ]; - [ .artifacts[] | select(.name | test($re)) ] | sort_by(.name | rank) - | map("* [\(.name | pretty)](\($url)/\(.name).zip)") | join("\n") - ' <<< "$artifacts" + jq -r --arg url "$base_url" --arg re "$1" ' + def pretty: + (if test("i686") then "i686" + elif test("aarch64") then "arm64" + elif test("-arm$") then "arm64" + else "x86_64" end) as $arch + | [ (if test("mingw32-full") then "full" + elif test("mingw32$") then "GCC" + elif test("windows-msvc") then "Clang" + elif test("-lgpl$") then "LGPL" + elif test("-gpl$") then "GPL" + else empty end), + (if test("macos") then "macOS " + capture("macos-(?[0-9]+)").v else empty end), + (if test("-pdb$") then "debug symbols" else empty end) ] as $q + | $arch + (if ($q | length) > 0 then " (" + ($q | join(", ")) + ")" else "" end); + # x86_64, arm64, i686 in that order, full before Clang, GPL before LGPL, + # debug symbols last + def rank: + [ (if test("-pdb$") then 1 else 0 end), + (if test("x86_64|intel") then 0 elif test("i686") then 2 else 1 end), + (if test("windows-msvc|-lgpl$") then 1 else 0 end), + (if test("macos") then (capture("macos-(?[0-9]+)").v | tonumber) else 0 end) ]; + [ .artifacts[] | select(.name | test($re)) ] | sort_by(.name | rank) + | map("* [\(.name | pretty)](\($url)/\(.name).zip)") | join("\n") + ' <<< "$artifacts" } # Match the version string baked into the build (see common/meson.build). version=$(git describe --abbrev=9 --tags --dirty --match "v0.*") -notes=$(cat < These are untested development builds of the very latest \`master\`. > They are **not** official releases. For stable builds see the > [latest release](https://github.com/$GH_REPO/releases/latest). -EOF +NOTES ) -# Create the release once (which also creates the tag), then only edit -# it in place so watchers aren't notified on every master push. Stale -# assets are dropped first since their names embed the run id. +# Create the release once (which also creates the tag), then only edit it in +# place so watchers aren't notified on every master push. Stale assets are +# dropped first since their names embed the run id. if gh release view "$tag" >/dev/null 2>&1; then - gh release view "$tag" --json assets --jq '.assets[].name' | - while read -r asset; do - gh release delete-asset "$tag" "$asset" --yes - done - gh release edit "$tag" \ - --prerelease \ - --latest=false \ - --title "mpv development build" \ - --notes "$notes" - gh release upload "$tag" release/* + gh release view "$tag" --json assets --jq '.assets[].name' | + while read -r asset; do + gh release delete-asset "$tag" "$asset" --yes + done + gh release edit "$tag" \ + --prerelease \ + --latest=false \ + --title "mpv development build" \ + --notes "$notes" + gh release upload "$tag" release/* else - gh release create "$tag" \ - --target "$GITHUB_SHA" \ - --prerelease \ - --latest=false \ - --title "mpv development build" \ - --notes "$notes" \ - release/* + gh release create "$tag" \ + --target "$GITHUB_SHA" \ + --prerelease \ + --latest=false \ + --title "mpv development build" \ + --notes "$notes" \ + release/* fi From ff39a85fc69f3966546b52132bdf905e26cea829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 25 Sep 2026 22:22:11 +0200 Subject: [PATCH 6/6] ci: attach build artifacts to tagged releases Run the build workflow for v0.* tags and let the publish job upload the artifacts to the release of that tag, with the run id stripped from the asset names to match the previous hand-made uploads. The release is created as a draft when it does not exist yet, so the notes can be filled in before publishing. --- .github/workflows/build.yml | 13 ++++++++----- ci/publish-release.sh | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe8286d74907a..6318ad91cfa57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,8 @@ on: - master - ci - 'release/**' + tags: + - 'v0.*' paths-ignore: - 'DOCS/**' - 'TOOLS/lua/**' @@ -789,23 +791,24 @@ jobs: cat ./build/meson-logs/testlog.txt publish: - name: Publish git release + name: Publish release needs: [mingw, mingw64-full, win32, macos] - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && - github.repository == 'mpv-player/mpv' + if: github.event_name == 'push' && github.repository == 'mpv-player/mpv' && + (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v0.')) runs-on: ubuntu-slim permissions: contents: write actions: read concurrency: - group: publish-git + group: publish cancel-in-progress: false steps: - uses: actions/checkout@v7 with: fetch-depth: 0 + fetch-tags: true - - name: Publish to git release + - name: Publish env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ci/publish-release.sh b/ci/publish-release.sh index 72fe28ccff727..22bee115788ba 100755 --- a/ci/publish-release.sh +++ b/ci/publish-release.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash # -# Attaches the artifacts of the current workflow run to the rolling -# "git-release" pre-release and rewrites its notes. +# Attaches the artifacts of the current workflow run to a GitHub release. +# A push to master updates the rolling "git-release" pre-release along with +# its notes. A pushed release tag gets the assets attached to its release, +# which is created as a draft when it does not exist yet, so the notes can be +# filled in before publishing. # # Needs GH_REPO and GH_TOKEN on top of the GITHUB_* variables of the run. set -euo pipefail @@ -13,16 +16,32 @@ artifacts=$(gh api "repos/{owner}/{repo}/actions/runs/$GITHUB_RUN_ID/artifacts") windows='mingw32-full|i686-w64-mingw32|windows-msvc' published="$windows|macos-|^libmpv-" -tag=git-release +if [[ $GITHUB_REF == refs/tags/* ]]; then + tag=$GITHUB_REF_NAME + # Release assets carry the version only, the run id is CI bookkeeping. + strip="-$GITHUB_RUN_ID" +else + tag=git-release + strip= +fi mkdir -p release jq -r --arg re "$published" ' .artifacts[] | select(.name | test($re)) | "\(.id)\t\(.name)" ' <<< "$artifacts" | while IFS=$'\t' read -r id name; do - gh api "repos/{owner}/{repo}/actions/artifacts/$id/zip" > "release/$name.zip" + gh api "repos/{owner}/{repo}/actions/artifacts/$id/zip" > "release/${name/$strip/}.zip" done +if [[ $tag != git-release ]]; then + if gh release view "$tag" >/dev/null 2>&1; then + gh release upload "$tag" --clobber release/* + else + gh release create "$tag" --draft --title "$tag" --notes-file RELEASE_NOTES release/* + fi + exit 0 +fi + base_url="https://github.com/$GH_REPO/releases/download/$tag" section() {