diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index cc1ee5e..ff4e4e0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,8 +12,8 @@ jobs:
windows:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-dotnet@v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
- name: Restore
@@ -36,7 +36,7 @@ jobs:
run: dotnet publish MusicDrop3/MFlacDrop.csproj -c Release -r win-x64 --self-contained true --no-restore -o artifacts/gui
- name: Publish CLI
run: dotnet publish MusicDrop3.Cli/MusicDrop3.Cli.csproj -c Release -r win-x64 --self-contained true --no-restore -o artifacts/cli
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: MusicDrop-community-win-x64-slim
path: |
diff --git a/.github/workflows/portable.yml b/.github/workflows/portable.yml
index 486cc1f..56b3f1c 100644
--- a/.github/workflows/portable.yml
+++ b/.github/workflows/portable.yml
@@ -16,8 +16,8 @@ jobs:
os: [windows-latest, macos-15]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-dotnet@v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
- run: dotnet restore MusicDrop.Portable.slnx --configfile NuGet.Config
diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml
index d6279f5..8217abe 100644
--- a/.github/workflows/release-macos.yml
+++ b/.github/workflows/release-macos.yml
@@ -4,6 +4,11 @@ on:
push:
tags: ['v*']
workflow_dispatch:
+ inputs:
+ version:
+ description: Package version used for a non-release validation run
+ required: true
+ default: v3.1.0-preview.1-ci
permissions:
contents: write
@@ -19,16 +24,20 @@ jobs:
- runner: macos-15
rid: osx-arm64
runs-on: ${{ matrix.runner }}
+ env:
+ PACKAGE_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }}
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-dotnet@v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
- dotnet-version: 10.0.x
+ dotnet-version: 10.0.301
- name: Build pinned LGPL FFmpeg
run: bash tools/build_ffmpeg_macos.sh "$RUNNER_TEMP/musicdrop-ffmpeg"
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
- name: Build and package app
- run: bash tools/package_macos.sh "${{ matrix.rid }}" "$RUNNER_TEMP/musicdrop-ffmpeg" "$RUNNER_TEMP/dist"
- - uses: actions/upload-artifact@v4
+ run: bash tools/package_macos.sh "${{ matrix.rid }}" "$RUNNER_TEMP/musicdrop-ffmpeg" "$RUNNER_TEMP/dist" "$PACKAGE_VERSION"
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: MusicDrop-${{ matrix.rid }}
path: |
@@ -41,12 +50,12 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: MusicDrop-osx-*
path: release
merge-multiple: true
- - uses: softprops/action-gh-release@v2
+ - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
files: |
release/MusicDrop-*.zip
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7ea6887..c5ddc3e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,34 +3,50 @@ name: release
on:
push:
tags: ['v*']
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Package version used for a non-release validation run
+ required: true
+ default: v3.1.0-preview.1-ci
permissions:
contents: write
jobs:
- release:
+ build:
runs-on: windows-latest
+ env:
+ PACKAGE_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }}
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-dotnet@v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
- dotnet-version: 10.0.x
+ dotnet-version: 10.0.301
- run: dotnet restore MusicDrop.slnx --configfile NuGet.Config
- - run: dotnet publish MusicDrop3/MFlacDrop.csproj -c Release -r win-x64 --self-contained true --no-restore -o publish/gui
- - run: dotnet publish MusicDrop3.Cli/MusicDrop3.Cli.csproj -c Release -r win-x64 --self-contained true --no-restore -o publish/cli
- - name: Assemble slim archive
+ - name: Build verified Full and Slim packages
shell: pwsh
- run: |
- New-Item -ItemType Directory -Path package | Out-Null
- Copy-Item publish/gui/MusicDrop3.exe package/
- Copy-Item publish/cli/MusicDrop3.Cli.exe package/
- Copy-Item README.md,LICENSE,THIRD-PARTY-NOTICES.md,TRADEMARKS.md,PRIVACY.md package/
- Compress-Archive -Path package/* -DestinationPath MusicDrop-${{ github.ref_name }}-Slim-Windows-x64.zip
- (Get-FileHash MusicDrop-${{ github.ref_name }}-Slim-Windows-x64.zip -Algorithm SHA256).Hash.ToLowerInvariant() |
- Set-Content MusicDrop-${{ github.ref_name }}-Slim-Windows-x64.zip.sha256.txt -Encoding ascii
- - uses: softprops/action-gh-release@v2
+ run: ./tools/package_windows.ps1 -Version $env:PACKAGE_VERSION -OutputDirectory release
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: MusicDrop-Windows-x64
+ path: |
+ release/MusicDrop-*.zip
+ release/MusicDrop-*.sha256.txt
+ if-no-files-found: error
+
+ release:
+ if: startsWith(github.ref, 'refs/tags/')
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: MusicDrop-Windows-x64
+ path: release
+ - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
files: |
- MusicDrop-*.zip
- MusicDrop-*.sha256.txt
+ release/MusicDrop-*.zip
+ release/MusicDrop-*.sha256.txt
generate_release_notes: true
diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md
index 193c5ad..a67afb9 100644
--- a/THIRD-PARTY-NOTICES.md
+++ b/THIRD-PARTY-NOTICES.md
@@ -37,7 +37,7 @@ Upstream: · build scripts:
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/third_party/licenses/LIBOGG-COPYING.txt b/third_party/licenses/LIBOGG-COPYING.txt
new file mode 100644
index 0000000..6111c6c
--- /dev/null
+++ b/third_party/licenses/LIBOGG-COPYING.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2002, Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/third_party/licenses/LIBVORBIS-COPYING.txt b/third_party/licenses/LIBVORBIS-COPYING.txt
new file mode 100644
index 0000000..fb456a8
--- /dev/null
+++ b/third_party/licenses/LIBVORBIS-COPYING.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2002-2020 Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tools/build_ffmpeg_macos.sh b/tools/build_ffmpeg_macos.sh
index a1ca482..b89e0d8 100644
--- a/tools/build_ffmpeg_macos.sh
+++ b/tools/build_ffmpeg_macos.sh
@@ -2,9 +2,14 @@
set -euo pipefail
output_dir="${1:?usage: build_ffmpeg_macos.sh }"
+repo_root="$(cd "$(dirname "$0")/.." && pwd)"
source_commit="9b6c8969e0"
source_sha256="7e779215eae16ad7e93ddad59bd82822bd3d34e4dc61f9996f9481b2c0605bc3"
source_url="https://github.com/FFmpeg/FFmpeg/archive/${source_commit}.tar.gz"
+lame_license_source_url="https://downloads.sourceforge.net/project/lame/lame/4.0/lame-4.0.tar.gz"
+lame_license_source_sha256="3df5124d5ad3a98312ffd7ba6a9b36230e4f8a3e66d3ce0f425e336c32d216eb"
+ogg_license_source_sha256="83e6704730683d004d20e21b8f7f55dcb3383cdf84c0daedf30bde175f774638"
+vorbis_license_source_sha256="b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b"
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/musicdrop-ffmpeg.XXXXXX")"
trap 'rm -rf "$work_dir"' EXIT
@@ -13,23 +18,60 @@ output_dir="$(cd "$output_dir" && pwd)"
archive="$work_dir/ffmpeg.tar.gz"
brew install pkg-config nasm lame libogg libvorbis
-curl --fail --location --retry 3 --output "$archive" "$source_url"
+curl_args=(
+ --fail
+ --location
+ --retry 10
+ --retry-all-errors
+ --retry-delay 5
+ --retry-max-time 900
+ --connect-timeout 30
+ --max-time 900
+ --user-agent "MusicDrop-release-builder"
+ --output "$archive"
+)
+if [[ -n "${GITHUB_TOKEN:-}" ]]; then
+ curl_args+=(--header "Authorization: Bearer ${GITHUB_TOKEN}")
+fi
+curl "${curl_args[@]}" "$source_url"
printf '%s %s\n' "$source_sha256" "$archive" | shasum -a 256 --check
tar -xzf "$archive" -C "$work_dir"
source_dir="$(find "$work_dir" -mindepth 1 -maxdepth 1 -type d -name 'FFmpeg-*' -print -quit)"
test -n "$source_dir"
lame_prefix="$(brew --prefix lame)"
+mpg123_prefix="$(brew --prefix mpg123)"
ogg_prefix="$(brew --prefix libogg)"
vorbis_prefix="$(brew --prefix libvorbis)"
+
+# Homebrew installs both static archives and dylibs. FFmpeg's pkg-config checks
+# emit -l flags, which otherwise let Apple's linker select Homebrew dylibs.
+# Put only the required archives in the first search path and explicitly use
+# Apple's per-directory search order. This preserves FFmpeg's native
+# pkg-config probes while producing standalone binaries.
+static_lib_dir="$work_dir/static-libs"
+mkdir -p "$static_lib_dir"
+static_archives=(
+ "$lame_prefix/lib/libmp3lame.a"
+ "$mpg123_prefix/lib/libmpg123.a"
+ "$ogg_prefix/lib/libogg.a"
+ "$vorbis_prefix/lib/libvorbis.a"
+ "$vorbis_prefix/lib/libvorbisenc.a"
+)
+for static_archive in "${static_archives[@]}"; do
+ test -f "$static_archive"
+ ln -s "$static_archive" "$static_lib_dir/$(basename "$static_archive")"
+done
+
cd "$source_dir"
-PKG_CONFIG_PATH="$lame_prefix/lib/pkgconfig:$ogg_prefix/lib/pkgconfig:$vorbis_prefix/lib/pkgconfig" \
-./configure \
+if ! PKG_CONFIG_PATH="$lame_prefix/lib/pkgconfig:$ogg_prefix/lib/pkgconfig:$vorbis_prefix/lib/pkgconfig" \
+ ./configure \
--prefix="$output_dir" \
--disable-debug \
--disable-doc \
--disable-ffplay \
--disable-network \
+ --disable-autodetect \
--disable-shared \
--enable-static \
--disable-gpl \
@@ -40,7 +82,12 @@ PKG_CONFIG_PATH="$lame_prefix/lib/pkgconfig:$ogg_prefix/lib/pkgconfig:$vorbis_pr
--enable-libvorbis \
--pkg-config-flags="--static" \
--extra-cflags="-I$lame_prefix/include -I$ogg_prefix/include -I$vorbis_prefix/include" \
- --extra-ldflags="-L$lame_prefix/lib -L$ogg_prefix/lib -L$vorbis_prefix/lib"
+ --extra-ldflags="-Wl,-search_paths_first -L$static_lib_dir" \
+ --extra-libs="-lmpg123"; then
+ echo "FFmpeg configure failed; showing the diagnostic log tail." >&2
+ tail -n 250 ffbuild/config.log >&2 || true
+ exit 1
+fi
make -j"$(sysctl -n hw.ncpu)"
make install
@@ -53,14 +100,29 @@ make install
"$output_dir/bin/ffmpeg" -hide_banner -encoders | grep -Eq '[[:space:]]flac[[:space:]]'
"$output_dir/bin/ffmpeg" -hide_banner -encoders | grep -Eq 'pcm_s(16|24|32)le'
-if otool -L "$output_dir/bin/ffmpeg" "$output_dir/bin/ffprobe" | grep -Eq '/(opt/homebrew|usr/local/opt)/'; then
+dependencies_file="$output_dir/FFMPEG-DEPENDENCIES.txt"
+otool -L "$output_dir/bin/ffmpeg" "$output_dir/bin/ffprobe" | tee "$dependencies_file"
+if grep -Eq '/(opt/homebrew|usr/local/opt)/' "$dependencies_file"; then
echo "FFmpeg unexpectedly links to Homebrew dylibs; refusing a non-portable package." >&2
exit 1
fi
+musicdrop_revision="${GITHUB_SHA:-main}"
+cat > "$output_dir/FFMPEG-SOURCE.txt" < "$output_dir/SHA256SUMS.txt"
diff --git a/tools/package_macos.sh b/tools/package_macos.sh
index c963041..8979e89 100644
--- a/tools/package_macos.sh
+++ b/tools/package_macos.sh
@@ -1,17 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
-rid="${1:?usage: package_macos.sh }"
+rid="${1:?usage: package_macos.sh }"
ffmpeg_prefix="${2:?missing FFmpeg prefix}"
dist_dir="${3:?missing distribution directory}"
+version="${4:?missing package version}"
case "$rid" in osx-arm64|osx-x64) ;; *) echo "Unsupported RID: $rid" >&2; exit 2 ;; esac
+if [[ ! "$version" =~ ^v[0-9A-Za-z.-]+$ ]]; then
+ echo "Unsupported version: $version" >&2
+ exit 2
+fi
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
mkdir -p "$dist_dir"
dist_dir="$(cd "$dist_dir" && pwd)"
publish_root="$dist_dir/publish-$rid"
app="$dist_dir/MusicDrop.app"
-archive="$dist_dir/MusicDrop-3.1.0-preview.1-${rid}.zip"
+archive="$dist_dir/MusicDrop-${version}-${rid}.zip"
dotnet publish "$repo_root/MusicDrop.Desktop/MusicDrop.Desktop.csproj" \
-c Release -r "$rid" --self-contained true -o "$publish_root/desktop"
@@ -26,12 +31,23 @@ cp "$publish_root/cli/musicdrop" "$app/Contents/Resources/musicdrop-cli"
cp "$ffmpeg_prefix/bin/ffmpeg" "$ffmpeg_prefix/bin/ffprobe" "$app/Contents/Resources/ffmpeg/bin/"
cp -R "$ffmpeg_prefix/licenses/." "$app/Contents/Resources/ffmpeg/licenses/"
cp "$ffmpeg_prefix/FFMPEG-VERSION.txt" "$ffmpeg_prefix/FFMPEG-BUILDCONF.txt" \
+ "$ffmpeg_prefix/FFMPEG-DEPENDENCIES.txt" "$ffmpeg_prefix/FFMPEG-SOURCE.txt" \
"$ffmpeg_prefix/SHA256SUMS.txt" "$app/Contents/Resources/ffmpeg/"
cp "$repo_root/tools/macos/Info.plist" "$app/Contents/Info.plist"
cp "$repo_root/README.md" "$repo_root/README.zh-CN.md" "$repo_root/LICENSE" \
- "$repo_root/THIRD-PARTY-NOTICES.md" "$repo_root/PRIVACY.md" \
+ "$repo_root/THIRD-PARTY-NOTICES.md" "$repo_root/TRADEMARKS.md" "$repo_root/PRIVACY.md" \
"$app/Contents/SharedSupport/"
+dotnet_root="${DOTNET_ROOT:-}"
+if [[ -z "$dotnet_root" || ! -f "$dotnet_root/LICENSE.txt" ]]; then
+ dotnet_root="$(cd "$(dirname "$(command -v dotnet)")" && pwd)"
+fi
+test -f "$dotnet_root/LICENSE.txt"
+test -f "$dotnet_root/ThirdPartyNotices.txt"
+cp "$dotnet_root/LICENSE.txt" "$app/Contents/SharedSupport/DOTNET-LICENSE.txt"
+cp "$dotnet_root/ThirdPartyNotices.txt" \
+ "$app/Contents/SharedSupport/DOTNET-THIRD-PARTY-NOTICES.txt"
+
iconset="$publish_root/MusicDrop.iconset"
mkdir -p "$iconset"
source_icon="$repo_root/MusicDrop3/Assets/musicdrop-logo-1024.png"
diff --git a/tools/package_windows.ps1 b/tools/package_windows.ps1
new file mode 100644
index 0000000..20dc13d
--- /dev/null
+++ b/tools/package_windows.ps1
@@ -0,0 +1,183 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [ValidatePattern('^v[0-9A-Za-z.-]+$')]
+ [string]$Version,
+
+ [string]$OutputDirectory = 'artifacts/release-windows',
+
+ [string]$FfmpegArchive = ''
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
+$distRoot = if ([IO.Path]::IsPathFullyQualified($OutputDirectory)) {
+ [IO.Path]::GetFullPath($OutputDirectory)
+} else {
+ [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDirectory))
+}
+[IO.Directory]::CreateDirectory($distRoot) | Out-Null
+
+$workRoot = Join-Path ([IO.Path]::GetTempPath()) ('MusicDropWindowsRelease-' + [guid]::NewGuid().ToString('N'))
+[IO.Directory]::CreateDirectory($workRoot) | Out-Null
+$createdOutputs = [Collections.Generic.List[string]]::new()
+
+$archiveName = 'ffmpeg-n8.1.2-34-g9b6c8969e0-win64-lgpl-shared-8.1.zip'
+$archiveRoot = 'ffmpeg-n8.1.2-34-g9b6c8969e0-win64-lgpl-shared-8.1'
+$archiveUrl = 'https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-11-13-11/' + $archiveName
+$archiveSha256 = '026f3ba22f0acf4fe58bf4da28a7eb64ffb107b270119684b91e4cace3b577aa'
+$ffmpegRevision = 'n8.1.2-34-g9b6c8969e0-20260811'
+$ffmpegFiles = [ordered]@{
+ 'bin/ffmpeg.exe' = '86a84607db881c93ac23ec8216b454e05ca8ae035ee8209fc2a9b10a845c2c84'
+ 'bin/ffprobe.exe' = '8e174683e435b089d7a9942afec5019e30ae6c550fcabfca3f917beb0768f7a6'
+ 'bin/avcodec-62.dll' = 'cc91ca4fc909f3d5a512e5b0d50a3d161305e005ca7febe969b5737acaef2475'
+ 'bin/avdevice-62.dll' = '2a229adf099eb360aad5bdda24a7f3d1a9d151db0e28365b6f428277360c320f'
+ 'bin/avfilter-11.dll' = 'e0d301cf78679caf8337a0babde8879924227a892e2e08abe04e9ec88bb9c351'
+ 'bin/avformat-62.dll' = '2fbd044d2a910035032d83dfd81d0f7fe442b73bea56341ccc171c941c62eb91'
+ 'bin/avutil-60.dll' = 'fd951227b0d1b574ed964d44ccca59422be1a821b67820600a4ac0a1b558e95a'
+ 'bin/swresample-6.dll' = '81d46648a06852f7123bc05501ec8c12bc396ad6f35b9ef2130ff9e3cadf80e5'
+ 'bin/swscale-9.dll' = '6f1214e30b4ebcef4468ff05954413c36ec83e4c8a0ed3dc7c6a04d42c26b0bd'
+ 'LICENSE.txt' = 'da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768'
+}
+
+function Assert-Sha256([string]$Path, [string]$Expected) {
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Missing release input: $Path" }
+ $actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
+ if ($actual -ne $Expected) { throw "SHA-256 mismatch: $Path" }
+}
+
+function Copy-ReleaseDocuments([string]$Destination) {
+ foreach ($name in @('README.md', 'README.zh-CN.md', 'LICENSE', 'THIRD-PARTY-NOTICES.md',
+ 'TRADEMARKS.md', 'PRIVACY.md', 'SECURITY.md')) {
+ Copy-Item -LiteralPath (Join-Path $repoRoot $name) -Destination $Destination
+ }
+
+ $dotnetRoot = Split-Path -Parent (Get-Command dotnet -ErrorAction Stop).Source
+ Copy-Item -LiteralPath (Join-Path $dotnetRoot 'LICENSE.txt') `
+ -Destination (Join-Path $Destination 'DOTNET-LICENSE.txt')
+ Copy-Item -LiteralPath (Join-Path $dotnetRoot 'ThirdPartyNotices.txt') `
+ -Destination (Join-Path $Destination 'DOTNET-THIRD-PARTY-NOTICES.txt')
+}
+
+function Write-RecursiveSums([string]$Directory) {
+ $lines = foreach ($file in Get-ChildItem -LiteralPath $Directory -Recurse -File |
+ Where-Object Name -ne 'SHA256SUMS.txt' | Sort-Object FullName) {
+ $relative = $file.FullName.Substring($Directory.TrimEnd('\').Length + 1).Replace('\', '/')
+ $hash = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
+ "$hash $relative"
+ }
+ $lines | Set-Content -LiteralPath (Join-Path $Directory 'SHA256SUMS.txt') -Encoding ascii
+}
+
+function Write-ZipHash([string]$ZipPath) {
+ $hashPath = $ZipPath + '.sha256.txt'
+ $hash = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant()
+ "$hash $([IO.Path]::GetFileName($ZipPath))" | Set-Content -LiteralPath $hashPath -Encoding ascii
+ $createdOutputs.Add($hashPath)
+}
+
+try {
+ $archive = if ([string]::IsNullOrWhiteSpace($FfmpegArchive)) {
+ $download = Join-Path $workRoot $archiveName
+ Invoke-WebRequest -Uri $archiveUrl -OutFile $download
+ $download
+ } else {
+ [IO.Path]::GetFullPath($FfmpegArchive)
+ }
+ $archiveInfo = Get-Item -LiteralPath $archive
+ if ($archiveInfo.Length -le 0 -or $archiveInfo.Length -gt 128MB) {
+ throw 'FFmpeg archive size is outside the allowed range.'
+ }
+ Assert-Sha256 $archive $archiveSha256
+
+ $expanded = Join-Path $workRoot 'ffmpeg-expanded'
+ Expand-Archive -LiteralPath $archive -DestinationPath $expanded
+ $ffmpegRoot = Join-Path $expanded $archiveRoot
+ foreach ($entry in $ffmpegFiles.GetEnumerator()) {
+ Assert-Sha256 (Join-Path $ffmpegRoot $entry.Key.Replace('/', '\')) $entry.Value
+ }
+
+ $ffmpegExe = Join-Path $ffmpegRoot 'bin\ffmpeg.exe'
+ $versionOutput = (& $ffmpegExe -hide_banner -version 2>&1 | Out-String)
+ if ($LASTEXITCODE -ne 0 -or -not $versionOutput.Contains($ffmpegRevision) -or
+ -not $versionOutput.Contains('--enable-version3') -or
+ -not $versionOutput.Contains('--enable-shared') -or
+ -not $versionOutput.Contains('--enable-libmp3lame') -or
+ -not $versionOutput.Contains('--enable-libvorbis') -or
+ $versionOutput.Contains('--enable-gpl')) {
+ throw 'FFmpeg version or LGPL capability verification failed.'
+ }
+ $encoders = (& $ffmpegExe -hide_banner -encoders 2>&1 | Out-String)
+ if ($LASTEXITCODE -ne 0 -or -not $encoders.Contains('libmp3lame') -or
+ -not $encoders.Contains('libvorbis') -or -not $encoders.Contains(' flac ')) {
+ throw 'FFmpeg is missing a required audio encoder.'
+ }
+
+ $publishGui = Join-Path $workRoot 'publish-gui'
+ $publishCli = Join-Path $workRoot 'publish-cli'
+ & dotnet publish (Join-Path $repoRoot 'MusicDrop3\MFlacDrop.csproj') `
+ -c Release -r win-x64 --self-contained true --no-restore -o $publishGui
+ if ($LASTEXITCODE -ne 0) { throw 'GUI publish failed.' }
+ & dotnet publish (Join-Path $repoRoot 'MusicDrop3.Cli\MusicDrop3.Cli.csproj') `
+ -c Release -r win-x64 --self-contained true --no-restore -o $publishCli
+ if ($LASTEXITCODE -ne 0) { throw 'CLI publish failed.' }
+
+ $slimRoot = Join-Path $workRoot 'package-slim'
+ $fullRoot = Join-Path $workRoot 'package-full'
+ [IO.Directory]::CreateDirectory($slimRoot) | Out-Null
+ [IO.Directory]::CreateDirectory($fullRoot) | Out-Null
+ Copy-Item -LiteralPath (Join-Path $publishGui 'MusicDrop3.exe') -Destination $slimRoot
+ Copy-Item -LiteralPath (Join-Path $publishCli 'MusicDrop3.Cli.exe') -Destination $slimRoot
+ Copy-ReleaseDocuments $slimRoot
+ Get-ChildItem -LiteralPath $slimRoot -Force | Copy-Item -Destination $fullRoot -Recurse
+
+ $ffmpegTarget = Join-Path $fullRoot 'ffmpeg'
+ foreach ($entry in $ffmpegFiles.GetEnumerator()) {
+ $destination = Join-Path $ffmpegTarget $entry.Key.Replace('/', '\')
+ [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
+ Copy-Item -LiteralPath (Join-Path $ffmpegRoot $entry.Key.Replace('/', '\')) -Destination $destination
+ }
+ @(
+ 'MusicDrop bundled FFmpeg provenance',
+ '',
+ "Build: $ffmpegRevision",
+ "Archive: $archiveName",
+ "Archive SHA-256: $archiveSha256",
+ 'Build project: https://github.com/BtbN/FFmpeg-Builds',
+ 'FFmpeg project: https://ffmpeg.org/',
+ 'Source revision: https://github.com/FFmpeg/FFmpeg/commit/9b6c8969e0'
+ ) | Set-Content -LiteralPath (Join-Path $ffmpegTarget 'FFMPEG-SOURCE.txt') -Encoding utf8
+
+ Write-RecursiveSums $slimRoot
+ Write-RecursiveSums $fullRoot
+
+ $slimZip = Join-Path $distRoot "MusicDrop-$Version-Slim-Windows-x64.zip"
+ $fullZip = Join-Path $distRoot "MusicDrop-$Version-Full-Windows-x64.zip"
+ foreach ($path in @($slimZip, $fullZip, $slimZip + '.sha256.txt', $fullZip + '.sha256.txt')) {
+ if (Test-Path -LiteralPath $path) { throw "Refusing to overwrite release output: $path" }
+ }
+ Compress-Archive -Path (Join-Path $slimRoot '*') -DestinationPath $slimZip -CompressionLevel Optimal
+ $createdOutputs.Add($slimZip)
+ Compress-Archive -Path (Join-Path $fullRoot '*') -DestinationPath $fullZip -CompressionLevel Optimal
+ $createdOutputs.Add($fullZip)
+ Write-ZipHash $slimZip
+ Write-ZipHash $fullZip
+
+ Get-Item -LiteralPath $slimZip, $fullZip | Select-Object FullName, Length
+}
+catch {
+ foreach ($path in $createdOutputs) {
+ try { if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } } catch { }
+ }
+ throw
+}
+finally {
+ $tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
+ $resolvedWork = [IO.Path]::GetFullPath($workRoot)
+ if ($resolvedWork.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -and
+ (Test-Path -LiteralPath $resolvedWork)) {
+ Remove-Item -LiteralPath $resolvedWork -Recurse -Force
+ }
+}