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
98 changes: 98 additions & 0 deletions .github/workflows/rust-test-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Intel macOS test package

on:
pull_request:
branches: [main, v3-beta]
paths:
- "rust/**"
- "src/jsHelper/**"
- "scripts/build-payload.mjs"
- "package.json"
- "pnpm-lock.yaml"
- "mise.toml"
- ".github/workflows/rust-test-package.yml"

jobs:
package:
runs-on: macos-15-intel
permissions:
contents: read
defaults:
run:
shell: bash
steps:
- name: Checkout PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Setup Node
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
with:
install_args: node

- name: Install portable pnpm
run: |
version="$(node -p \
'require("./package.json").packageManager.replace(/^pnpm@/, "")')"
prefix="$RUNNER_TEMP/pnpm"
npm install --global --prefix "$prefix" "pnpm@$version"
test "$("$prefix/bin/pnpm" --version)" = "$version"
echo "$prefix/bin" >> "$GITHUB_PATH"

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
with:
workspaces: rust -> target

- name: Install Node dependencies and build client payload
run: |
pnpm install --frozen-lockfile
pnpm build:payload

- name: Test on Intel macOS
working-directory: rust
run: >-
cargo test --workspace --all-targets --locked
--features daemon/native-window-controls-tests -- --test-threads=4

- name: Build CLI and matching daemon
working-directory: rust
env:
MACOSX_DEPLOYMENT_TARGET: "15.0"
run: cargo build --release -p cli -p daemon --locked

- name: Package and verify binaries
run: |
mkdir -p staged package
cp rust/target/release/spicetify staged/
cp rust/target/release/spicetify-daemon staged/
chmod +x staged/spicetify staged/spicetify-daemon
for binary in staged/spicetify staged/spicetify-daemon; do
test "$(lipo -archs "$binary")" = x86_64
done
source_sha="$(git rev-parse HEAD)"
version="$(./staged/spicetify --version)"
jq -n --arg sourceCommit "$source_sha" --arg version "$version" \
--arg target x86_64-apple-darwin --arg minimumMacOS 15.0 \
'{experimental: true, sourceCommit: $sourceCommit,
version: $version, target: $target,
minimumMacOS: $minimumMacOS}' > staged/build-info.json
(cd staged && shasum -a 256 spicetify spicetify-daemon > SHA256SUMS)
archive="spicetify-pr-macos-x86_64-${source_sha:0:12}.tar.gz"
tar -czf "package/$archive" -C staged \
spicetify spicetify-daemon build-info.json SHA256SUMS
(cd package && shasum -a 256 "$archive" > "$archive.sha256")
mkdir verified
tar -xzf "package/$archive" -C verified
(cd verified && shasum -a 256 -c SHA256SUMS && ./spicetify --version)

- name: Upload test package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: >-
spicetify-pr-macos-x86_64-${{ github.event.pull_request.head.sha }}
path: package/*
if-no-files-found: error
retention-days: 30
20 changes: 15 additions & 5 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,22 @@ jobs:
run: |
./scripts/test-install-completions.ps1
./scripts/test-install-architecture.ps1
& "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File ./scripts/test-install-architecture.ps1
$systemPowerShell = Join-Path $env:WINDIR `
'System32\WindowsPowerShell\v1.0\powershell.exe'
& $systemPowerShell -NoProfile -ExecutionPolicy Bypass `
-File ./scripts/test-install-architecture.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& "$env:WINDIR\SysWOW64\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File ./scripts/test-install-architecture.ps1
$wowPowerShell = Join-Path $env:WINDIR `
'SysWOW64\WindowsPowerShell\v1.0\powershell.exe'
& $wowPowerShell -NoProfile -ExecutionPolicy Bypass `
-File ./scripts/test-install-architecture.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Setup Node and pnpm
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
with:
workspaces: rust -> target

Expand All @@ -115,11 +121,15 @@ jobs:
run: cargo build --workspace --locked

- name: Test
run: cargo test --workspace --locked --features daemon/native-window-controls-tests
run: >-
cargo test --workspace --locked
--features daemon/native-window-controls-tests

- name: Test experimental Windows updates
if: runner.os == 'Windows'
run: cargo test -p daemon --locked --features experimental-windows-updates update_job
run: >-
cargo test -p daemon --locked
--features experimental-windows-updates update_job

- name: Clippy
run: cargo clippy --workspace --locked -- -D warnings
10 changes: 4 additions & 6 deletions rust/crates/spicetify/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,8 @@ fn run_inner(ctx: &AppContext, mode: ApplyMode, activate: bool) -> Result<()> {
std::fs::rename(&tmp, &dest_xpui)
.map_err(fs_err("installing the patched client to", &dest_xpui))?;

// The update block's endpoint patch is signed while modules are staged,
// before xpui.tmp replaces the served tree. Seal and verify the final
// bundle, not that intermediate resource set.
// The served resources and any legacy endpoint migration must be sealed
// together after the staged tree replaces the previous resources.
super::updates::finalize_app_signature(ctx)?;

if activate {
Expand Down Expand Up @@ -541,9 +540,8 @@ fn stage_modules(ctx: &AppContext, dest: &Path) -> Result<()> {
// Runs before staging so anything seeded is staged in this same apply; a
// no-op once they exist.
super::pkg::ensure_default_modules(ctx);
// A Spotify update wipes the binary patch that blocks its updater, and
// apply is what runs right after one; restore the user's stated policy
// here rather than leaving them silently unprotected.
// Updates can replace protected binaries or staging directories. The
// manifest records the physical protection after reasserting durable intent.
super::updates::reassert_block(ctx);

let updates_blocked = super::updates::is_blocked(ctx).ok();
Expand Down
30 changes: 30 additions & 0 deletions rust/crates/spicetify/src/commands/fixtures/macos-signing.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include <libkern/OSCacheControl.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main(void) {
puts("desktop-update/v2/update");
void *memory = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0);
if (memory == MAP_FAILED) {
perror("mmap MAP_JIT");
return 1;
}
#if defined(SPICETIFY_ARM64)
unsigned int instructions[] = {0x52800540, 0xd65f03c0};
pthread_jit_write_protect_np(0);
#else
unsigned char instructions[] = {0xb8, 42, 0, 0, 0, 0xc3};
#endif
memcpy(memory, instructions, sizeof(instructions));
sys_icache_invalidate(memory, sizeof(instructions));
#if defined(SPICETIFY_ARM64)
pthread_jit_write_protect_np(1);
#endif
int result = ((int (*)(void))memory)();
printf("JIT result: %d\n", result);
munmap(memory, 4096);
return result == 42 ? 0 : 2;
}
8 changes: 3 additions & 5 deletions rust/crates/spicetify/src/commands/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,9 @@ fn add_from_registry(
Ok(())
}

/// Changing the update policy patches Spotify's binary, which means stopping
/// the client; `set_blocked` leaves it stopped because a terminal caller
/// relaunches it themselves. A caller inside the client cannot, so bring it
/// back rather than having the user's Spotify vanish on a button press. A
/// no-op change never stops it, so only relaunch what was actually running.
/// Physical policy changes stop the client while binaries or staging
/// directories are changed. Protocol callers need it relaunched here; terminal
/// callers own that step themselves. No-op changes leave the client running.
fn set_updates_blocked(ctx: &AppContext, block: bool) -> Result<()> {
let was_running = crate::lifecycle::is_running(ctx);
super::updates::set_blocked(ctx, block)?;
Expand Down
Loading
Loading