Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ body:
label: Log
description: |
Run QuickPlay from a terminal with `RUST_LOG=debug` (PowerShell: `$env:RUST_LOG="debug"; .\waveflow-quickplay.exe file.flac`) and paste the lines around the bug.
This issue is public and the log prints full file paths, which can show your user name and your folder and file names: replace whatever you would rather keep private before posting.
render: shell

- type: textarea
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Linux build dependencies
Expand Down
30 changes: 24 additions & 6 deletions .github/workflows/label-pr.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
# Labels every pull request: `scope:` from the touched paths
# (.github/labeler.yml), `type:` from the Conventional Commits prefix of the
# title, `size:` from the diff. Same scheme as the WaveFlow repository.
#
# `pull_request_target` runs in the base repository with a token that can
# write labels, so pull requests from forks are labelled too. It is safe here
# because nothing from the pull request is checked out or run: the labeler
# reads its rules from the base branch and the scripts only call the API.
name: Label PR

on:
pull_request:
pull_request_target:
types: [opened, synchronize, edited]

permissions:
contents: read
pull-requests: write

# Title edits and pushes in quick succession: only the latest run labels.
concurrency:
group: label-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
label:
runs-on: ubuntu-latest
Expand All @@ -36,7 +46,13 @@ jobs:
with:
github-token: ${{ env.LABEL_TOKEN }}
script: |
const title = context.payload.pull_request.title;
// The title as it is now, not as it was when this event fired.
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const title = pr.title;
const prefixMap = {
'feat': 'type: feat',
'fix': 'type: fix',
Expand All @@ -49,12 +65,13 @@ jobs:
};
const match = title.match(/^(\w+)(\(.+\))?[!]?:/);
const label = prefixMap[match?.[1]];
const current = await github.rest.issues.listLabelsOnIssue({
const current = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});
for (const l of current.data.filter(l => l.name.startsWith('type:'))) {
for (const l of current.filter(l => l.name.startsWith('type:'))) {
if (l.name !== label) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
Expand Down Expand Up @@ -94,12 +111,13 @@ jobs:
{ max: 500, label: 'size: l' },
];
const sizeLabel = sizeLabels.find(s => lines <= s.max)?.label || 'size: xl';
const current = await github.rest.issues.listLabelsOnIssue({
const current = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});
for (const l of current.data.filter(l => l.name.startsWith('size:'))) {
for (const l of current.filter(l => l.name.startsWith('size:'))) {
if (l.name !== sizeLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
Expand Down
113 changes: 95 additions & 18 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,38 @@ permissions:

env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
# The packaging tools, pinned: packaging/installer.nsi is a copy of
# cargo-packager 0.11.8's template and is refreshed when this moves.
CARGO_PACKAGER_VERSION: "0.11.8"
CARGO_GENERATE_RPM_VERSION: "0.21.1"

# One run per tag at a time: a tag push and a manual rebuild of the same tag
# would otherwise replace each other's files and checksums on the release.
concurrency:
group: release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false

jobs:
version:
name: Tag matches Cargo.toml
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
sha: ${{ steps.tag.outputs.sha }}
steps:
# The input must name a tag, not a branch that happens to be called
# v<version>; every job then checks out the commit it points to now,
# so a tag moved during the run cannot mix two sources.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.RELEASE_TAG }}
ref: refs/tags/${{ env.RELEASE_TAG }}
persist-credentials: false
- id: tag
run: |
sha=$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}")
test "$sha" = "$(git rev-parse HEAD)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "$RELEASE_TAG is $sha"
- run: |
version=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)
if [ "v$version" != "$RELEASE_TAG" ]; then
Expand All @@ -60,15 +81,16 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.RELEASE_TAG }}
ref: ${{ needs.version.outputs.sha }}
persist-credentials: false
- run: rustup show active-toolchain
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- run: cargo test --locked
- run: cargo install cargo-packager --locked
# pwsh, the Windows runners' shell: environment variables are $env:.
- run: cargo install cargo-packager --version "$env:CARGO_PACKAGER_VERSION" --locked
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Everything is compiled before the certificate enters the runner, so
# no build script of any dependency runs while the key is importable;
# cargo-packager then finds the build up to date.
# cargo-packager has no build hook (Cargo.toml) and packages this.
- run: cargo build --release --locked

# Authenticode, with WaveFlow's certificate (the same two secrets as
Expand All @@ -91,14 +113,19 @@ jobs:
}
Write-Host "::add-mask::$env:PFX_PASSWORD"
$pfx = Join-Path $env:RUNNER_TEMP "signing.pfx"
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:PFX_BASE64))
$password = ConvertTo-SecureString $env:PFX_PASSWORD -AsPlainText -Force
$cert = Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $password
Remove-Item $pfx
if (-not $cert.HasPrivateKey) { Write-Error "the PFX has no private key"; exit 1 }
try {
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:PFX_BASE64))
$password = ConvertTo-SecureString $env:PFX_PASSWORD -AsPlainText -Force
$cert = Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $password
} finally {
Remove-Item $pfx -ErrorAction SilentlyContinue
}
$thumbprint = $cert.Thumbprint
Write-Host "Signing with $($cert.Subject) ($thumbprint), valid until $($cert.NotAfter)"
# Recorded before any other check, so the cleanup step removes the
# certificate from the store whichever way this step fails.
"SIGNING_THUMBPRINT=$thumbprint" | Out-File $env:GITHUB_ENV -Append -Encoding utf8
if (-not $cert.HasPrivateKey) { Write-Error "the PFX has no private key"; exit 1 }
Write-Host "Signing with $($cert.Subject) ($thumbprint), valid until $($cert.NotAfter)"
# The thumbprint is public; the private key stays in the store.
@"

Expand All @@ -117,8 +144,27 @@ jobs:
$files = @(Get-ChildItem target/packager/*.exe) + @(Get-Item target/release/waveflow-quickplay.exe)
foreach ($file in $files) {
$sig = Get-AuthenticodeSignature $file.FullName
# A self-signed certificate reads "UnknownError" (untrusted root):
# what matters is that it is ours and timestamped.
# A self-signed certificate reads "UnknownError" (untrusted root)
# or "NotTrusted"; any other status, HashMismatch first, means
# the signature does not cover the file as it is.
if ($sig.Status.ToString() -notin @('Valid', 'UnknownError', 'NotTrusted')) {
Write-Error "$($file.Name) has a bad signature ($($sig.Status)): $($sig.StatusMessage)"
exit 1
}
# UnknownError covers any chain failure, so it passes only for the
# one this certificate has: a chain of itself alone whose only
# fault is an untrusted root. The chain is read, not the message,
# which is in the runner's language.
if ($sig.Status.ToString() -eq 'UnknownError') {
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
$null = $chain.Build($sig.SignerCertificate)
$faults = @($chain.ChainStatus | ForEach-Object { $_.Status.ToString() })
if ($chain.ChainElements.Count -ne 1 -or $faults.Count -ne 1 -or $faults[0] -ne 'UntrustedRoot') {
Write-Error "$($file.Name): signature chain fails for another reason ($($faults -join ', ')): $($sig.StatusMessage)"
exit 1
}
}
if ($sig.SignerCertificate.Thumbprint -ne $env:SIGNING_THUMBPRINT) {
Write-Error "$($file.Name) is not signed with the release certificate ($($sig.Status))"
exit 1
Expand Down Expand Up @@ -165,7 +211,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.RELEASE_TAG }}
ref: ${{ needs.version.outputs.sha }}
persist-credentials: false
# The packaging definitions (desktop entries, rpm.toml) come from the
# workflow's own ref: on a tag push that is the tag itself, on a
Expand All @@ -184,15 +230,33 @@ jobs:
- run: rustup show active-toolchain
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- run: cargo test --locked
- run: cargo install cargo-packager cargo-generate-rpm --locked
- run: cargo install cargo-packager --version "$CARGO_PACKAGER_VERSION" --locked
- run: cargo install cargo-generate-rpm --version "$CARGO_GENERATE_RPM_VERSION" --locked
# The .deb depends on the glibc the binary was linked against: the
# newest GLIBC_ symbol version it references.
- name: glibc floor
run: |
cargo build --release --locked
glibc=$(LC_ALL=C objdump -T target/release/waveflow-quickplay | grep -o 'GLIBC_[0-9.]*' | cut -d_ -f2 | sort -uV | tail -1)
echo "needs glibc $glibc"
sed -i "s/^depends = \[/depends = [\"libc6 (>= $glibc)\", /" Cargo.toml
grep -m1 '^depends' Cargo.toml
- run: cargo packager --release --formats deb,appimage
# The .rpm comes from cargo-generate-rpm (cargo-packager has no rpm
# format), from the binary cargo-packager just built; its contents
# format), from the release binary built above; its contents
# and dependencies are the [package.metadata.generate-rpm] section
# kept in packaging/rpm.toml, appended to Cargo.toml here.
- name: RPM
run: |
cat packaging/rpm.toml >> Cargo.toml
# What the binary links, with its symbol versions, the way rpm's
# ELF scan would declare it (automatic detection is off, see
# packaging/rpm.toml).
LC_ALL=C objdump -p target/release/waveflow-quickplay | awk '
$1 == "NEEDED" { print "\"" $2 "()(64bit)\" = \"*\"" }
/required from/ { lib = $NF; sub(/:$/, "", lib) }
lib && NF == 4 && $4 ~ /_/ { print "\"" lib "(" $4 ")(64bit)\" = \"*\"" }
' | sort -u >> Cargo.toml
cargo generate-rpm --payload-compress zstd
rpm=$(ls target/generate-rpm/*.rpm)
mv "$rpm" target/packager/
Expand All @@ -205,6 +269,7 @@ jobs:
rpm -qpl "$rpm" | grep -qx /usr/bin/waveflow-quickplay
rpm -qpl "$rpm" | grep -qx /usr/share/applications/app.waveflow.quickplay.desktop
rpm -qpR "$rpm" | grep -q 'libasound.so.2'
rpm -qpR "$rpm" | grep -q 'libc.so.6(GLIBC_'
- name: Checksums
run: |
cd target/packager
Expand Down Expand Up @@ -232,7 +297,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.RELEASE_TAG }}
ref: ${{ needs.version.outputs.sha }}
persist-credentials: false
# The bundle definition comes from the workflow's own ref, as the
# Linux packaging does, so a manual run can attach it to an older tag.
Expand Down Expand Up @@ -289,7 +354,7 @@ jobs:

publish:
name: GitHub release
needs: [windows, linux, macos]
needs: [version, windows, linux, macos]
# Runs when every job that ran succeeded; `windows` and `macos` may
# have been skipped by `linux_only`.
if: ${{ always() && needs.linux.result == 'success' && (needs.windows.result == 'success' || needs.windows.result == 'skipped') && (needs.macos.result == 'success' || needs.macos.result == 'skipped') }}
Expand All @@ -300,13 +365,25 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.RELEASE_TAG }}
ref: ${{ needs.version.outputs.sha }}
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: dist
merge-multiple: true
- run: ls -la dist
# The tag must still point where the packages were built from, or the
# release would pair them with another commit's tag.
- name: Tag unchanged
env:
GH_TOKEN: ${{ github.token }}
BUILT_SHA: ${{ needs.version.outputs.sha }}
run: |
now=$(gh api "repos/$GITHUB_REPOSITORY/commits/refs/tags/$RELEASE_TAG" --jq .sha)
if [ "$now" != "$BUILT_SHA" ]; then
echo "::error::$RELEASE_TAG moved from $BUILT_SHA to $now during the run"
exit 1
fi
# On an existing release the files replace their namesakes, the
# others are kept, and the body is rewritten from the notes at the tag.
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/target
/.claude
# Agent skills installed locally (npx skills), not part of the project.
/.agents
/skills-lock.json
*.json.tmp
# Stray captures from local test scripts; real screenshots live in docs/screenshots.
/*.png
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 19 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ eframe = { version = "0.36", default-features = false, features = [
"x11",
] }
egui = "0.36"
# The font parser egui itself uses (same version, so one build): a system
# fallback font is checked with it before egui is given the file, since
# egui panics on one it cannot read.
skrifa = { version = "0.44", default-features = false, features = ["std"] }

# ── Audio transport (the same crates and versions the desktop engine uses) ─
# Decoding, channel conversion and resampling come from `waveflow-core`
Expand Down Expand Up @@ -91,8 +95,10 @@ sys-locale = "0.3"

[target.'cfg(windows)'.dependencies]
# Owner relationship between the playlist window and the player (one taskbar
# button, minimized together), the registry, and the version resource of an
# installed WaveFlow; same version winit already pulls.
# button, minimized together), the registry, the version resource of an
# installed WaveFlow, who runs the other end of the single-instance pipe and
# whether it holds data yet;
# same version winit already pulls.
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_System_Threading",
Expand All @@ -101,8 +107,15 @@ windows-sys = { version = "0.61", features = [
"Win32_System_Console",
"Win32_System_Registry",
"Win32_Storage_FileSystem",
"Win32_Security",
"Win32_System_Pipes",
] }

[target.'cfg(unix)'.dependencies]
# `geteuid`: the single-instance socket checks who is at the other end on
# the Linux abstract namespace, and who owns the directory it lives in.
libc = "0.2"

[target.'cfg(target_os = "macos")'.dependencies]
# Files opened from Finder arrive as `application:openURLs:` on the
# application delegate (src/macos.rs): the event loop is built here so the
Expand Down Expand Up @@ -142,14 +155,16 @@ panic = "unwind"
# on Linux. The NSIS installer is per user, no administrator rights, and runs
# `--register` / `--unregister` (packaging/installer.nsi); the Linux packages
# carry the desktop entry with the audio MIME types (packaging/quickplay.desktop).
# It packages the binary already built: run `cargo build --release --locked`
# first. No build hook here, so the release workflow compiles everything
# before the signing certificate is imported and nothing compiles after.
[package.metadata.packager]
product-name = "WaveFlow QuickPlay"
identifier = "app.waveflow.quickplay"
publisher = "InstaZDLL"
category = "Music"
icons = ["assets/icon.ico", "assets/icon-128.png"]
out-dir = "target/packager"
before-packaging-command = "cargo build --release"
binaries = [{ path = "waveflow-quickplay", main = true }]
license-file = "LICENSE"
resources = [{ src = "LICENSE", target = "LICENSE.txt" }]
Expand All @@ -164,5 +179,5 @@ display-language-selector = false
[package.metadata.packager.deb]
package-name = "waveflow-quickplay"
desktop-template = "packaging/quickplay.desktop"
depends = ["libasound2 | libasound2t64", "libxkbcommon0", "libwayland-client0", "libgl1"]
depends = ["libgcc-s1", "libasound2 | libasound2t64", "libxkbcommon0", "libwayland-client0", "libegl1", "libgl1", "libx11-6", "libx11-xcb1", "libxcursor1", "libxi6", "libxkbcommon-x11-0"]
section = "sound"
Loading
Loading