diff --git a/.github/actions/rust-python-env/action.yml b/.github/actions/rust-python-env/action.yml index 3ab72b0..def17ae 100644 --- a/.github/actions/rust-python-env/action.yml +++ b/.github/actions/rust-python-env/action.yml @@ -58,3 +58,4 @@ runs: run: | echo "PYO3_PYTHON=$(which python3.12)" >> "$GITHUB_ENV" echo "LD_LIBRARY_PATH=$(python3.12 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")" >> "$GITHUB_ENV" + echo "DYLD_LIBRARY_PATH=$(python3.12 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")" >> "$GITHUB_ENV" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9bda98a..6e14b19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,9 @@ jobs: - os: windows-latest platform: windows suffix: ".exe" + - os: macos-latest + platform: macos + suffix: "" steps: - name: Check out the repo uses: actions/checkout@v7 @@ -61,18 +64,28 @@ jobs: uses: actions/download-artifact@v7 with: path: dist - - name: Assemble the engine bundles (engine + both players each) + - name: Assemble the engine bundles (engine + player binaries for all platforms) shell: bash run: | - mkdir -p bundle/linux bundle/windows + mkdir -p bundle/linux bundle/windows bundle/macos cp dist/dist-linux/pite-linux bundle/linux/pite cp dist/dist-linux/pite-player-linux bundle/linux/pite-player cp dist/dist-windows/pite-player-windows.exe bundle/linux/pite-player.exe + cp dist/dist-macos/pite-player-macos bundle/linux/pite-player-macos + cp dist/dist-windows/pite-windows.exe bundle/windows/pite.exe cp dist/dist-windows/pite-player-windows.exe bundle/windows/pite-player.exe cp dist/dist-linux/pite-player-linux bundle/windows/pite-player - tar -czf "dist/pite-${{ github.ref_name }}-linux.tar.gz" -C bundle/linux pite pite-player pite-player.exe - zip -j "dist/pite-${{ github.ref_name }}-windows.zip" bundle/windows/pite.exe bundle/windows/pite-player.exe bundle/windows/pite-player + cp dist/dist-macos/pite-player-macos bundle/windows/pite-player-macos + + cp dist/dist-macos/pite-macos bundle/macos/pite + cp dist/dist-macos/pite-player-macos bundle/macos/pite-player + cp dist/dist-windows/pite-player-windows.exe bundle/macos/pite-player.exe + cp dist/dist-linux/pite-player-linux bundle/macos/pite-player-linux + + tar -czf "dist/pite-${{ github.ref_name }}-linux.tar.gz" -C bundle/linux pite pite-player pite-player.exe pite-player-macos + zip -j "dist/pite-${{ github.ref_name }}-windows.zip" bundle/windows/pite.exe bundle/windows/pite-player.exe bundle/windows/pite-player bundle/windows/pite-player-macos + tar -czf "dist/pite-${{ github.ref_name }}-macos.tar.gz" -C bundle/macos pite pite-player pite-player.exe pite-player-linux - name: Publish the bundles and checksums env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 0ee3a88..0a3f9a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ cargo run --locked -p pite-cli -- check - Single crate: `cargo test -p ` (e.g. `-p pite-scene`). Focused check: `cargo run -p pite-cli -- check --path `; `--strict` promotes warnings, `--json` gives machine report. With no `--path` outside a project it falls back to `examples/minimal-2d`. - Build needs CPython 3.12 dev headers + ALSA headers or it fails to link: `libasound2-dev pkg-config` on Debian/Ubuntu, plus `export PYO3_PYTHON=$(which python3.12)` and `LD_LIBRARY_PATH` pointing at its `LIBDIR`. - WSL without Wayland: `WINIT_UNIX_BACKEND=x11 cargo run -p pite-cli -- edit`. -- `pite` dogfood flow: `new --template minimal-2d` → `run --scene …` → `check` → `edit --scene …`. Export via `pite export linux|windows`; offline export needs `--no-bundle-python` (+ `PITE_PYTHON` / `--skip-python-check`). Export ships the player found via `--binary`, else a `pite-player` sibling of `pite` — from a source checkout build both: `cargo build --release -p pite-cli -p pite-player`. +- `pite` dogfood flow: `new --template minimal-2d` → `run --scene …` → `check` → `edit --scene …`. Export via `pite export linux|windows|macos`; offline export needs `--no-bundle-python` (+ `PITE_PYTHON` / `--skip-python-check`). Export ships the player found via `--binary`, else a `pite-player` sibling of `pite` — from a source checkout build both: `cargo build --release -p pite-cli -p pite-player`. ## Architecture (crate → role) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ae2589..3b513a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ The `v*` tag flow automates almost nothing — bump these by hand, every release - `Cargo.toml`: `version` + all 12 internal `pite-*` path-dep pins (nothing enforces lockstep; `cargo publish` breaks otherwise). - `CHANGELOG.md`: move notes from `## Unreleased` to the release heading. - MSRV proof re-runs automatically (weekly + manual); if it goes red, fix code or raise `rust-version`. -- Only when the toolchain changes: `python_bundle.rs` (`PINNED_PYTHON`, `PINNED_BUILD`, 4 filenames, 2 checksums). +- Only when the toolchain changes: `python_bundle.rs` (`PINNED_PYTHON`, `PINNED_BUILD`, filenames, checksums). - Only when pyright changes: `pite-editor/src/lsp.rs`, `docs/lsp.md`, `ci.yml`, `AGENTS.md` (all pin the same version). - Only when the scene format changes: `FORMAT_VERSION`, `CACHE_VERSION`, the `format_version` template literal, `docs/scene-format.md`. - Tag rule: strict `v*.*.*` tags must equal the workspace version (the publish workflow fails otherwise); milestone tags (`v0.1-mN`) never touch the registry. diff --git a/README.md b/README.md index 2329ef3..915f3d2 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ Build games from scenes and nodes (not ECS) - edit visually, script in Python. ## Quickstart -Install the engine bundle (engine + both player binaries, so any machine -exports for either platform) — latest stable, verified on download: +Install the engine bundle (engine + player binaries for all platforms, so any machine +exports for any platform) — latest stable, verified on download: -Linux Terminal: +Linux / macOS Terminal: ```sh curl -fsSL https://raw.githubusercontent.com/piteengine/pite/main/install.sh | sh @@ -48,7 +48,7 @@ Install once, then use `pite`. `new` scaffolds a project, `run` plays a scene (d pite export linux ``` -`export linux|windows` writes a runnable directory (`dist/-/`) with the lean player binary (no editor), a launcher script, a manual for how to run, and only the referenced game content. Provides `[export] include` in `pite.toml` for extra files, plus `vendor/` for third-party Python packages. The player is resolved as `--binary`, else a `pite-player` sibling of `pite` — engine bundles ship both players, so any machine exports for either platform with no download and no compiler. +`export linux|windows|macos` writes a runnable directory (`dist/-/`) with the lean player binary (no editor), a launcher script, a manual for how to run, and only the referenced game content. Provides `[export] include` in `pite.toml` for extra files, plus `vendor/` for third-party Python packages. The player is resolved as `--binary`, else a `pite-player` sibling of `pite` — engine bundles ship player binaries for all platforms, so any machine exports for any platform with no download and no compiler. Python is bundled: export fetches a pinned CPython 3.12 (checksum-verified, cached under `~/.cache/pite/toolchains`) and ships in the exported game. The exported game runs with no system Python installed. Passing `--no-bundle-python` falls back to end-user's system Python 3.12 (`PITE_PYTHON` overrides detection). diff --git a/docs/export.md b/docs/export.md index 3694651..3b24703 100644 --- a/docs/export.md +++ b/docs/export.md @@ -4,7 +4,7 @@ Ships a playable build of the project: a lean `pite-player` binary, the resolved content, and a launcher (`run.sh` / `run.bat`). ```sh -pite export linux|windows [--path ] [--out ] [--binary ] +pite export linux|windows|macos [--path ] [--out ] [--binary ] [--skip-python-check] [--no-bundle-python] ``` @@ -17,10 +17,10 @@ old manifests carrying it still parse with a deprecation warning. ## Player binary Which binary an export ships: an explicit `--binary`, else a `pite-player` -sibling (`pite-player.exe` on Windows) next to the engine binary. Every -engine bundle ships both players, so cross-platform export needs no download -and no compiler. Without either, export fails and tells you to pass -`--binary`. +sibling (`pite-player.exe` on Windows, `pite-player-macos` or `pite-player` on macOS) +next to the engine binary. Engine bundles ship player binaries for all supported +platforms, so cross-platform export needs no download and no compiler. Without +either, export fails and tells you to pass `--binary`. ## Python diff --git a/install.ps1 b/install.ps1 index 724920a..e91642d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,4 +1,4 @@ -# Install Pite (engine + both players) from a release bundle, verified. +# Install Pite (engine + player binaries for all platforms) from a release bundle, verified. # Usage: .\install.ps1 [-Tag v0.1.0] [-Prefix "$env:LOCALAPPDATA\Pite"] param( [string]$Tag = "", @@ -32,7 +32,7 @@ try { [Environment]::SetEnvironmentVariable("PATH", "$path;$Prefix", "User") Write-Host "added $Prefix to the user PATH (restart the terminal to use it)" } - Write-Host "installed $Tag to $Prefix (pite.exe, pite-player.exe, pite-player)" + Write-Host "installed $Tag to $Prefix (pite.exe, pite-player.exe, pite-player, pite-player-macos)" } finally { Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue } diff --git a/install.sh b/install.sh index 04dc7c2..6fb6fe2 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Install Pite (engine + both players) from a release bundle, verified. +# Install Pite (engine + player binaries for all platforms) from a release bundle, verified. # Usage: ./install.sh [tag] (tag defaults to the latest release) # Env: PREFIX (default $HOME/.local), PITE_VERSION (overrides the tag arg). set -e @@ -14,7 +14,13 @@ fi PREFIX="${PREFIX:-$HOME/.local}" BINDIR="$PREFIX/bin" -BUNDLE="pite-$TAG-linux.tar.gz" +OS="$(uname -s)" +case "$OS" in + Linux*) PLATFORM="linux";; + Darwin*) PLATFORM="macos";; + *) echo "error: unsupported operating system $OS" >&2; exit 1;; +esac +BUNDLE="pite-$TAG-$PLATFORM.tar.gz" BASE="https://github.com/$REPO/releases/download/$TAG" work="$(mktemp -d)" @@ -22,13 +28,20 @@ trap 'rm -rf "$work"' EXIT INT TERM cd "$work" curl -fsSL -O "$BASE/$BUNDLE" curl -fsSL -O "$BASE/SHA256SUMS" -grep -F " $BUNDLE" SHA256SUMS | sha256sum -c - >/dev/null +if command -v sha256sum >/dev/null 2>&1; then + grep -F " $BUNDLE" SHA256SUMS | sha256sum -c - >/dev/null +elif command -v shasum >/dev/null 2>&1; then + grep -F " $BUNDLE" SHA256SUMS | shasum -a 256 -c - >/dev/null +else + echo "error: neither sha256sum nor shasum found" >&2 + exit 1 +fi mkdir -p "$BINDIR" -tar -xzf "$BUNDLE" -C "$BINDIR" pite pite-player pite-player.exe -chmod +x "$BINDIR/pite" "$BINDIR/pite-player" +tar -xzf "$BUNDLE" -C "$BINDIR" +chmod +x "$BINDIR/pite" "$BINDIR"/pite-player* -echo "installed $TAG to $BINDIR (pite, pite-player, pite-player.exe)" +echo "installed $TAG to $BINDIR" case ":$PATH:" in *":$BINDIR:"*) ;; *) echo "note: $BINDIR is not on PATH; add: export PATH=\"$BINDIR:\$PATH\"" ;; diff --git a/pite-export/src/lib.rs b/pite-export/src/lib.rs index 7dbe733..7bb4ef8 100644 --- a/pite-export/src/lib.rs +++ b/pite-export/src/lib.rs @@ -11,7 +11,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use pite_render::atlas::{sheet_for_sidecar, SIDECAR_EXT}; -pub const SUPPORTED_PLATFORMS: &[&str] = &["linux", "windows"]; +pub const SUPPORTED_PLATFORMS: &[&str] = &["linux", "windows", "macos"]; #[derive(Debug, Clone)] pub struct ExportOptions { @@ -111,7 +111,7 @@ impl Drop for ExportDir { pub fn export_project(root: &Path, opts: &ExportOptions) -> Result { if !SUPPORTED_PLATFORMS.contains(&opts.platform.as_str()) { anyhow::bail!( - "unsupported export platform {:?} (supported: linux, windows)", + "unsupported export platform {:?} (supported: linux, windows, macos)", opts.platform ); } @@ -269,8 +269,8 @@ pub fn export_project(root: &Path, opts: &ExportOptions) -> Result } /// Bundled mode needs no Python on PATH: the loader finds `libpython` through -/// `LD_LIBRARY_PATH` (or a `$ORIGIN` RUNPATH when the binary was built with one) -/// and the stdlib comes from the zip. Proved on Linux. +/// `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` (or a `$ORIGIN` RUNPATH when the binary was built with one) +/// and the stdlib comes from the zip. Proved on Linux and macOS. fn unix_launcher( game: &str, bin: &str, @@ -287,6 +287,8 @@ fn unix_launcher( HERE=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\ LD_LIBRARY_PATH=\"$HERE/{lib_rel}\"${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n\ export LD_LIBRARY_PATH\n\ + DYLD_LIBRARY_PATH=\"$HERE/{lib_rel}\"${{DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}}\n\ + export DYLD_LIBRARY_PATH\n\ PYTHONPATH=\"$HERE/{zip_rel}\"\n\ export PYTHONPATH\n\ exec \"$HERE/bin/{bin}\" --scene \"$HERE/game/{main_rel}\" \"$@\"\n", @@ -368,8 +370,8 @@ fn windows_launcher( } /// Which binary an export ships: an explicit `--binary`, else a `pite-player` -/// sibling named for the target platform. Every engine bundle ships both -/// players, so cross-platform export needs no download and no compiler. +/// sibling named for the target platform. Engine bundles ship player binaries +/// for all platforms, so cross-platform export needs no download and no compiler. /// The full `pite` binary is never shipped: it carries the editor. fn select_binary(opts: &ExportOptions) -> Result { select_binary_from(opts, std::env::current_exe().ok()) @@ -384,6 +386,23 @@ fn player_bin_name(platform: &str) -> &'static str { } } +/// Candidate sibling player binary names for `platform` when running on `host_os`. +fn candidate_player_names(platform: &str, host_os: &str) -> Vec { + if platform == host_os { + vec![player_bin_name(platform).to_string()] + } else { + match platform { + "windows" => vec![ + "pite-player.exe".to_string(), + "pite-player-windows.exe".to_string(), + ], + "macos" => vec!["pite-player-macos".to_string()], + "linux" => vec!["pite-player-linux".to_string(), "pite-player".to_string()], + _ => vec![format!("pite-player-{platform}")], + } + } +} + fn select_binary_from(opts: &ExportOptions, current_exe: Option) -> Result { if let Some(p) = &opts.binary { if !p.is_file() { @@ -392,14 +411,16 @@ fn select_binary_from(opts: &ExportOptions, current_exe: Option) -> Res return Ok(p.clone()); } if let Some(dir) = current_exe.as_ref().and_then(|e| e.parent()) { - let sibling = dir.join(player_bin_name(&opts.platform)); - if sibling.is_file() { - return Ok(sibling); + for name in candidate_player_names(&opts.platform, std::env::consts::OS) { + let sibling = dir.join(name); + if sibling.is_file() { + return Ok(sibling); + } } } anyhow::bail!( - "export needs a pite-player binary for {} next to {} (engine bundles ship both \ - players), or pass --binary with a player binary", + "export needs a pite-player binary for {} next to {} (engine bundles ship player \ + binaries for all platforms), or pass --binary with a player binary", opts.platform, current_exe .as_deref() @@ -844,6 +865,63 @@ def _once(): std::fs::remove_dir_all(&dir).ok(); } + #[test] + fn macos_bundling_stages_runtime_and_stdlib_next_to_the_game() { + let Some(spec) = python_bundle::pinned("macos") else { + panic!("macos must have a pinned bundle"); + }; + let dir = std::env::temp_dir().join(format!("pite-stage-mac-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join("python/lib/python3.12/site-packages")).unwrap(); + std::fs::write( + dir.join("python/lib").join("libpython3.12.dylib"), + b"fake-dylib", + ) + .unwrap(); + std::fs::write( + dir.join("python/lib/python3.12").join("os.py"), + b"import sys\n", + ) + .unwrap(); + std::fs::write( + dir.join("python/lib/python3.12/site-packages") + .join("pip.py"), + b"raise SystemExit\n", + ) + .unwrap(); + let archive = dir.join("archive.tar.gz"); + let tar = std::process::Command::new("tar") + .arg("-czf") + .arg(&archive) + .arg("-C") + .arg(&dir) + .arg("python") + .output(); + if !tar.as_ref().is_ok_and(|o| o.status.success()) { + eprintln!("skipped: tar unavailable"); + return; + } + let out = dir.join("out"); + let staged = python_bundle::stage(&spec, &archive, &out).unwrap(); + assert!(staged.lib.ends_with("libpython3.12.dylib"), "{staged:?}"); + assert!(staged.lib.is_file()); + assert!(staged.stdlib_zip.is_file()); + let listing = std::process::Command::new("python3") + .arg("-c") + .arg("import zipfile,sys; print(sorted(zipfile.ZipFile(sys.argv[1]).namelist()))") + .arg(&staged.stdlib_zip) + .output() + .expect("python3 lists the zip"); + let listing = String::from_utf8_lossy(&listing.stdout).into_owned(); + assert!(listing.contains("os.py"), "{listing}"); + assert!(!listing.contains("site-packages"), "{listing}"); + assert!( + !out.join(".staging").exists(), + "staging dir must be cleaned up" + ); + std::fs::remove_dir_all(&dir).ok(); + } + /// Real download + export layout. Gated: it fetches a pinned 60 MB archive /// the first time, then runs from the local toolchain cache. #[test] @@ -1152,6 +1230,61 @@ def _once(): std::fs::remove_dir_all(&dir).ok(); } + #[test] + fn foreign_macos_uses_bundled_sibling() { + let dir = sel_dir("foreign-macos"); + let fake_exe = dir.join("pite"); + let sibling = dir.join("pite-player-macos"); + std::fs::write(&fake_exe, "x").unwrap(); + std::fs::write(&sibling, "x").unwrap(); + let got = select_binary_from(&bin_opts("macos", None), Some(fake_exe)).unwrap(); + assert_eq!(got, sibling); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn macos_export_layout_without_bundle() { + let (dir, bin) = fixture("mac-layout"); + let report = export_project( + &dir, + &ExportOptions { + platform: "macos".to_string(), + out_dir: None, + binary: Some(bin), + skip_python_check: true, + bundle_python: false, + }, + ) + .unwrap(); + let out = &report.out_dir; + assert!(out.join("bin").join("fixt").is_file()); + assert!(out.join("run.sh").is_file()); + let sh = std::fs::read_to_string(out.join("run.sh")).unwrap(); + assert!(sh.contains("needs system Python 3.12")); + assert!(sh.contains("--scene")); + std::fs::remove_dir_all(&dir).ok(); + std::fs::remove_dir_all(out).ok(); + } + + #[test] + fn unix_launcher_sets_both_ld_and_dyld_library_paths() { + let bundle = python_bundle::StagedBundle { + root: PathBuf::from("/game/python"), + lib: PathBuf::from("/game/python/lib/libpython3.12.dylib"), + stdlib_zip: PathBuf::from("/game/python/python312.zip"), + }; + let sh = unix_launcher( + "game", + "bin", + "main.ptscn", + Path::new("/game"), + Some(&bundle), + ); + assert!(sh.contains("LD_LIBRARY_PATH")); + assert!(sh.contains("DYLD_LIBRARY_PATH")); + assert!(sh.contains("PYTHONPATH")); + } + #[test] fn unknown_platform_fails_loudly() { let (dir, bin) = fixture("plat"); diff --git a/pite-export/src/python_bundle.rs b/pite-export/src/python_bundle.rs index c0edffa..992d8b8 100644 --- a/pite-export/src/python_bundle.rs +++ b/pite-export/src/python_bundle.rs @@ -44,7 +44,7 @@ pub struct BundleSpec { pub pth_name: Option<&'static str>, } -/// The two `install_only` archives do not agree on layout: Linux nests the +/// The `install_only` archives do not agree on layout: Linux and macOS nest the /// stdlib under `lib/python3.12/`, Windows puts it straight in `Lib/`. Windows /// also ships `python3.dll` next to `python312.dll`, but the first is a 56 KB /// forwarder onto the second, so only the versioned library can be staged. @@ -68,18 +68,41 @@ const WINDOWS: BundleSpec = BundleSpec { pth_name: Some("python312._pth"), }; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +const MACOS: BundleSpec = BundleSpec { + platform: "macos", + file: "cpython-3.12.9+20250317-x86_64-apple-darwin-install_only.tar.gz", + sha256: "1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc", + lib: "lib/libpython3.12.dylib", + stdlib: "lib/python3.12", + zip_name: "python312.zip", + pth_name: None, +}; + +#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] +const MACOS: BundleSpec = BundleSpec { + platform: "macos", + file: "cpython-3.12.9+20250317-aarch64-apple-darwin-install_only.tar.gz", + sha256: "7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381", + lib: "lib/libpython3.12.dylib", + stdlib: "lib/python3.12", + zip_name: "python312.zip", + pth_name: None, +}; + /// The pinned artifact for `platform`, or `None` when we do not ship one. pub fn pinned(platform: &str) -> Option { match platform { "linux" => Some(LINUX), "windows" => Some(WINDOWS), + "macos" => Some(MACOS), _ => None, } } /// Every platform whose bundle artifact is pinned and checksummed. pub fn pinned_platforms() -> Vec<&'static str> { - vec!["linux", "windows"] + vec!["linux", "windows", "macos"] } /// Where downloaded toolchains live. Never inside the project or repo. @@ -367,8 +390,8 @@ fn collect(base: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) -> Result< Ok(()) } -/// Env the launcher must set for a staged bundle. Proved on Linux: the -/// process runs with no Python on `PATH`. +/// Env the launcher must set for a staged bundle. Proved on Linux and macOS: +/// the process runs with no Python on `PATH`. pub fn launcher_env(bundle: &StagedBundle) -> Vec<(String, String)> { vec![ ( @@ -380,6 +403,15 @@ pub fn launcher_env(bundle: &StagedBundle) -> Vec<(String, String)> { .to_string_lossy() .into_owned(), ), + ( + "DYLD_LIBRARY_PATH".to_string(), + bundle + .lib + .parent() + .unwrap_or(&bundle.root) + .to_string_lossy() + .into_owned(), + ), ( "PYTHONPATH".to_string(), bundle.stdlib_zip.to_string_lossy().into_owned(), @@ -416,7 +448,7 @@ mod tests { ); assert!(spec.file.starts_with("cpython-3.12")); } - assert!(pinned("macos").is_none()); + assert!(pinned("ps5").is_none()); } #[test]