Skip to content

feat(tts): expose Kokoro CoreML compute units through the binding - #21

Open
drakulavich wants to merge 10 commits into
FluidInference:mainfrom
drakulavich:feat/kokoro-compute-units
Open

feat(tts): expose Kokoro CoreML compute units through the binding#21
drakulavich wants to merge 10 commits into
FluidInference:mainfrom
drakulavich:feat/kokoro-compute-units

Conversation

@drakulavich

@drakulavich drakulavich commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

KokoroAneManager already takes a computeUnits: argument, but the bridge constructs it with only variant:/defaultVoice: (FluidAudioBridge.swift:168), and the Rust entry point is init_kokoro(&self, default_voice, lang). So every Rust caller is pinned to KokoroAneComputeUnits.default, which puts Albert / PostAlbert / Alignment / Vocoder on the Neural Engine.

That is the right default on real Apple Silicon and fatal without an ANE. On a virtualised macOS guest — a GitHub-hosted macos-14 runner is one — the ANE is not exposed, and CoreML refuses to prepare exactly those stages.

Worth being precise about when that surfaces, because it is not at init. KokoroAneManager.initialize only runs store.loadIfNeeded(), the G2P assets and the voice packs — it never issues a prediction — and KokoroAneError.predictionFailed is thrown from exactly one place, KokoroAneSynthesizer.predict wrapping model.prediction(from:). So the model loads happily with its ANE configuration and the first synthesize is what fails:

Kokoro synthesize error: predictionFailed(stage: "vocoder", underlying:
  Error Domain=com.apple.CoreML Code=0 "Failed to prepare the model for predictions.
  ML program was KokoroVocoder and the function name was main."
  NSUnderlyingError=... "E5RT: Output rank has changed after reshaping espresso network
  for blob = anchor_classic_cpu (11)")

Not an asset problem: the same binary and voice synthesize fine on an M2 with the same staged bundle. The ASR path is unaffected because FluidAudioBridge.swift:401 sets cfg.computeUnits = .all for ASR, leaving CoreML free to pick CPU/GPU. Kokoro has no equivalent freedom from Rust.

Documentation/TTS/KokoroAne.md already documents the escape hatch (KokoroAneManager(computeUnits: .cpuAndGpu)) — this PR just plumbs it.

Change

  • fluidaudio_initialize_kokoro gains a compute_units C string, parsed with FluidAudio's own TtsComputeUnitPreset(cliValue:) so spellings stay in one place. NULL/empty keeps the empirical mapping; an unrecognised value fails init rather than silently synthesizing on units the caller did not ask for.
  • New KokoroComputeUnits enum (Default / AllAne / CpuAndGpu / CpuOnly) with FromStr + as_str, and init_kokoro_with_compute_units.
  • init_kokoro keeps its signature and delegates on Default — no existing caller changes.
  • examples/kokoro.rs takes the preset as an optional 4th argument.

Verification

M2, macOS 26.5.2, staged ANE bundle, am_michael, same sentence through all four presets:

preset exit output
default 0 24 kHz mono, 2.75 s, RMS 3593
all-ane 0 24 kHz mono, 2.75 s, RMS 3552
cpu-and-gpu 0 24 kHz mono, 2.77 s, RMS 4038
cpu-only 0 24 kHz mono, 2.75 s, RMS 3552

cargo test --lib passes, including two new tests. They cover what a Rust unit test honestly can: that as_str emits the literal kebab-case spellings transcribed from TtsComputeUnitPreset.swift at the pinned 0.14.8, that every alias init?(cliValue:) accepts also parses here, and that an unknown preset is rejected with an error naming both the bad value and the accepted spellings. The Swift parser itself cannot be executed from this crate — there is no Swift test target — so that table is manual sync, and the test says so rather than implying it is enforced. The literal assertion was mutation-checked: renaming as_str's cpu-and-gpu output fails the test.

One finding worth recording for other callers: on cpu-and-gpu CoreML writes E5RT "Data-dependent shapes were disabled" diagnostics to stdout (436 bytes in the run above) and falls back internally — synthesis still succeeds, but anything streaming WAV bytes to stdout has to silence fd 1 around the bridge, as the example's callers already do.

Measured on the runner: premise confirmed, lever not sufficient

That measurement has now been taken, by the temporary macos-14 job in this branch (run). The host reports Apple M1 (Virtual), macOS 14.8.7 — the virtualised guest this PR is about.

All four presets fail, at the vocoder, in two clean groups:

preset vocoder error
default Code=0 — Failed to prepare the model for predictions … KokoroVocoder, E5RT: Output rank has changed after reshaping espresso network for blob = anchor_classic_cpu (11)
all-ane same
cpu-and-gpu Code=1 — Invalid shape for output feature 'anchor'must be of rank 1, instead got a multi-array value of rank 2
cpu-only same

Two things follow, and they pull in opposite directions.

The premise is confirmed. The failure this PR was opened for is now observed in CI rather than inferred from a downstream report, and it is exactly the quoted error. It also lands where the docs in this PR now say it does: init succeeds, the first synthesize fails. The split is explanatory rather than coincidental — the two ANE-targeting presets share one failure, the two non-ANE presets share a different one, so the compute-unit setting demonstrably changes what CoreML does.

The lever is not sufficient on this host. Moving the vocoder off the ANE clears the prepare failure and then hits a second, independent bug: the vocoder returns rank 2 where the model description demands rank 1. cpu-only fails identically to cpu-and-gpu, so this is not GPU-specific. Plausibly the same data-dependent-shape issue behind the E5RT "Data-dependent shapes were disabled" diagnostics noted above — which an M2 recovers from and this virtual M1 does not — but that link is not established here.

So compute-unit selection is necessary but not sufficient for a virtualised host. The API is still the right thing to expose — a caller cannot even attempt the workaround without it, and it is the documented escape hatch — but it does not on its own make Kokoro work on a GitHub-hosted macos-14 runner, and this PR should not be read as claiming it does.

The second failure is upstream, not this binding — filed as FluidInference/FluidAudio#836. Worth noting there: Package.swift declares .macOS(.v14) and KokoroAne carries no @available gate (Qwen3 does, at macOS 15), yet every upstream workflow runs on macos-15 — so the declared floor is not exercised anywhere.

The probe job that produced this has been removed again; ci.yml is byte-identical to main. Keeping it would have left a permanently red check that re-measures a known result, when the thing that must change lives in FluidAudio. The evidence stays in the run linked above.

Pre-existing, untouched

cargo test --doc fails on init_qwen3_streaming's example (undeclared capture_audio_chunk) and cargo clippy --all-targets flags needless_borrows_for_generic_args in build.rs. Both reproduce on unmodified main; left alone to keep this diff to one subject. cargo fmt also rewrites several unrelated files on main — those reverts are excluded here too.

Refs drakulavich/kesha-voice-kit#678

🤖 Generated with Claude Code

https://claude.ai/code/session_01UU2vgxhrf56CN8YDqbg3q1

FluidAudio's KokoroAneManager takes a `computeUnits:` argument, but the
bridge constructed it positionally with only `variant:`/`defaultVoice:`,
so Rust callers were stuck on `KokoroAneComputeUnits.default` — which
pins the Albert, PostAlbert, Alignment and Vocoder stages to the Neural
Engine.

That is fine on real Apple Silicon and fatal without an ANE. On a
virtualised macOS guest (a GitHub-hosted `macos-14` runner is one) the
ANE is not exposed, and CoreML refuses to prepare exactly those stages:

    predictionFailed(stage: "vocoder", underlying: Error Domain=com.apple.CoreML
      Code=0 "Failed to prepare the model for predictions. ML program was
      KokoroVocoder and the function name was main."
      NSUnderlyingError=... "E5RT: Output rank has changed after reshaping
      espresso network for blob = anchor_classic_cpu (11)")

FluidAudio documents the escape hatch in `Documentation/TTS/KokoroAne.md`
(`KokoroAneManager(computeUnits: .cpuAndGpu)`), so this only plumbs it:

- `fluidaudio_initialize_kokoro` gains a `compute_units` C string, parsed
  with FluidAudio's own `TtsComputeUnitPreset(cliValue:)`. NULL/empty
  keeps the empirical mapping; an unrecognised value fails init rather
  than synthesising on units the caller did not ask for.
- New `KokoroComputeUnits` enum + `init_kokoro_with_compute_units`.
  `init_kokoro` keeps its signature and delegates on `Default`, so no
  existing caller changes.
- `examples/kokoro.rs` takes the preset as an optional 4th argument.

Verified on an M2 (macOS 26.5.2) against a staged ANE bundle: all four
presets synthesize `am_michael` to 24 kHz mono, ~2.75 s, RMS 3552-4038.
Worth knowing for the CPU+GPU path: CoreML writes E5RT "Data-dependent
shapes were disabled" diagnostics to *stdout* and falls back internally
— synthesis still succeeds, but callers streaming WAV bytes to stdout
must silence fd 1 around the bridge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU2vgxhrf56CN8YDqbg3q1
drakulavich and others added 3 commits August 5, 2026 15:09
…the preset test overclaiming

Both fixes come from review of FluidInference#21.

The compute-units docs said an ANE-less host makes *init* fail with
"Failed to prepare the model for predictions". It does not. In FluidAudio
0.14.8 `KokoroAneManager.initialize` only runs `store.loadIfNeeded()`, the
G2P assets and the voice packs — it never issues a prediction, and
`KokoroAneError.predictionFailed` is thrown from exactly one place,
`KokoroAneSynthesizer.predict` wrapping `model.prediction(from:)`. So the
CoreML model loads with its ANE configuration and the *first synthesize*
fails. That matches the log in the PR body, which is synthesize-time. The
old wording sends anyone debugging a CI host looking at the wrong call.
Reworded in both the Rust doc comment and the Swift bridge's.

`compute_units_round_trip_through_cli_spellings` claimed `as_str` "must
stay inside what FluidAudio's parser accepts", but only round-tripped
`as_str` through this crate's own `FromStr` — self-consistent by
construction, and blind to a rename that drifts away from the Swift side.
It now asserts the literal kebab-case spellings (verified against
TtsComputeUnitPreset.swift at the pinned 0.14.8) and says plainly that the
Swift parser cannot be executed from here, so the table is manual sync.
The alias test likewise now covers every alias `init?(cliValue:)` accepts
rather than two of them, and asserts the rejection error names the bad
value and the accepted spellings.

No behaviour change: comments, test bodies, and one rustfmt wrap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
…ning the old one

Converges this branch with the int-selector implementation carried on the
fork, keeping the better half of each.

From this branch: the preset crosses the boundary as FluidAudio's own
kebab-case `cliValue` string, parsed by `TtsComputeUnitPreset(cliValue:)`.
The fork's `Int32` selector had to restate the mapping in Swift, and its
`default:` arm silently downgraded an out-of-range selector to `.default`
— the caller asks for cpu-only, gets the ANE, and finds out at synthesize
time. An unknown string still fails init loudly.

From the fork: the shape. `fluidaudio_initialize_kokoro` goes back to three
parameters and `fluidaudio_initialize_kokoro_with_compute_units` carries
the preset, so the existing C symbol's arity is untouched and anything
linking the staticlib's entry points from an older build keeps working.
Widening the original symbol, as this branch did, broke that for no gain.
Both @_cdecls share one Swift body, so there is still a single parse and a
single failure contract.

`init_kokoro` correspondingly calls the three-arg path directly rather than
delegating through the preset one — the default case now touches none of
the new code.

Verified on M2: `default` and `cpu-and-gpu` synthesize through the new
symbol (114044 WAV bytes each), `init_kokoro` through the restored
three-arg symbol (104444 bytes), and `nm` shows both C symbols exported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
Temporary job, to be deleted once the answer is recorded on the PR.

The permanent job runs on macos-15, where the Neural Engine works, so it
cannot exercise the case this feature exists for. The ANE-less failure has
only ever been observed downstream and inferred here; "Not verified here"
in the PR description says as much.

This job runs the same synthesis on macos-14 under both presets and reports
what happens, including whether the host exposes an ANE at all. It measures
rather than asserts: `default` failing is the hypothesis under test, so it
does not fail the build — only a failing `cpu-and-gpu` does, since that is
the escape hatch itself not working. All four outcome quadrants write a
verdict to the step summary, so "not reproduced" is a reportable result and
not a silent pass.

The example is built in its own step so a Swift 6 / Xcode 16 toolchain gap
on the macos-14 image is distinguishable from a compute-unit failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
drakulavich added a commit to drakulavich/fluidaudio-rs that referenced this pull request Aug 5, 2026
…ream PR

e4c24a9 shipped compute-unit selection here as an `Int32` selector while
PR FluidInference#21 proposes the same feature upstream as FluidAudio's own kebab-case
`cliValue` string. Two implementations of one feature would collide the
moment FluidInference#21 lands and the fork syncs, so this converges on the upstream
shape now rather than resolving it in a merge.

The string is the better half on its own merits: `TtsComputeUnitPreset(cliValue:)`
does the parsing, so the spellings live in FluidAudio and cannot drift. The
selector had to restate them in Swift, and its `default:` arm silently
downgraded an out-of-range value to `.default` — the caller asks for
cpu-only, gets the ANE, and finds out at synthesize time, if at all. An
unknown string now fails in Rust before the FFI call, naming both the bad
value and the accepted spellings.

The symbol shape this branch already had is kept: `fluidaudio_initialize_kokoro`
stays at three parameters and the preset rides on
`fluidaudio_initialize_kokoro_with_compute_units`. Both @_cdecls now share
one Swift body. `kokoroComputeUnits(for:)` is gone.

Also corrects the doc claim inherited from e4c24a9 that an ANE-less host
fails at init. `KokoroAneManager.initialize` only loads models — it issues
no prediction — and `predictionFailed` is thrown solely from
`KokoroAneSynthesizer.predict`. Load succeeds; the first synthesize fails.

`examples/kokoro.rs` gains the optional 4th argument, so the API this
branch has had since e4c24a9 is finally reachable from the example. The new
init diagnostic goes to stderr, since the example streams WAV to stdout.

Verified on M2: `default`, `cpu-and-gpu` and `cpu-only` each synthesize
88844 WAV bytes, an unknown preset exits 1 with the parse error, and `nm`
shows both C symbols exported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
The first run of the probe never reached synthesis: the image has no plain
`Xcode_16.app`, so the guard fell through to the warning branch and the
build ran on the image default — Swift 5.10, which cannot build FluidAudio
0.14.8 (Swift tools 6.0). It does ship Xcode_16.1 and Xcode_16.2.

Glob for 16.x and take the newest instead of testing one hardcoded name.
The separate build step did its job here: the failure was legible as a
toolchain gap rather than looking like a compute-unit result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
drakulavich added a commit to drakulavich/fluidaudio-rs that referenced this pull request Aug 5, 2026
…the preset test overclaiming

Both fixes come from review of FluidInference#21.

The compute-units docs said an ANE-less host makes *init* fail with
"Failed to prepare the model for predictions". It does not. In FluidAudio
0.14.8 `KokoroAneManager.initialize` only runs `store.loadIfNeeded()`, the
G2P assets and the voice packs — it never issues a prediction, and
`KokoroAneError.predictionFailed` is thrown from exactly one place,
`KokoroAneSynthesizer.predict` wrapping `model.prediction(from:)`. So the
CoreML model loads with its ANE configuration and the *first synthesize*
fails. That matches the log in the PR body, which is synthesize-time. The
old wording sends anyone debugging a CI host looking at the wrong call.
Reworded in both the Rust doc comment and the Swift bridge's.

`compute_units_round_trip_through_cli_spellings` claimed `as_str` "must
stay inside what FluidAudio's parser accepts", but only round-tripped
`as_str` through this crate's own `FromStr` — self-consistent by
construction, and blind to a rename that drifts away from the Swift side.
It now asserts the literal kebab-case spellings (verified against
TtsComputeUnitPreset.swift at the pinned 0.14.8) and says plainly that the
Swift parser cannot be executed from here, so the table is manual sync.
The alias test likewise now covers every alias `init?(cliValue:)` accepts
rather than two of them, and asserts the rejection error names the bad
value and the accepted spellings.

No behaviour change: comments, test bodies, and one rustfmt wrap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
drakulavich and others added 5 commits August 5, 2026 15:47
The run reached synthesis and `cpu-and-gpu` failed — the outcome that
matters most — but every diagnostic went to GITHUB_STEP_SUMMARY, which
`gh run view --log` does not expose. The failure was legible as "exit 1"
and nothing else.

Dump both stderr files to stdout as well, so the CoreML error is in the log
where it can actually be read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
… a result

The macos-14 run reached synthesis and reported that `cpu-and-gpu` failed.
That was my bug, not a finding:

    Kokoro init error: invalidResponse(description: "am_michael voice pack",
                                       statusCode: 404)

Both presets died there, at init, before any prediction. `am_michael` is not
in the hosted bundle — examples/kokoro.rs says so in its own header, that
English currently hosts only `af_heart`. It works on a dev machine because
the bundle is staged locally, which is exactly the kind of difference a
hosted runner exists to catch. Switched to `af_heart`.

The worse half is that the job blamed the escape hatch for it. A download
failure aborts init identically under every preset, so it can never be
evidence about compute units, yet the verdict logic only looked at exit
codes and reported "the escape hatch does not rescue this host" — a false
negative against the very claim the job was built to test.

So the probe now detects that shape explicitly: both presets failing with
the same download-level error is reported as **inconclusive**, naming the
error, and says plainly that it proves nothing about the ANE either way. It
still fails the build, because a probe that measured nothing is broken — but
it no longer launders a broken probe into a result.

Verified the branch both ways against the real stderr: the 404 pair is
classified inconclusive; a vocoder `predictionFailed` on `default` with
`cpu-and-gpu` succeeding still reads as premise-confirmed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
With a voice that exists, the probe finally measured something real on
macos-14 — and the motivating case reproduced exactly:

    default:     predictionFailed(stage: "vocoder", Code=0
                 "Failed to prepare the model for predictions ... KokoroVocoder"
                 E5RT: Output rank has changed after reshaping espresso network
                 for blob = anchor_classic_cpu (11))

That is the error from the PR description, now observed in CI rather than
inferred from a downstream report. But `cpu-and-gpu` failed too, differently:

    cpu-and-gpu: predictionFailed(stage: "vocoder", Code=1
                 "Invalid shape for output feature 'anchor'" ... must be of
                 rank 1, instead got a multi-array value of rank 2)

So the premise holds and that particular escape hatch does not clear it.
What the probe could not say is whether *any* setting does, because it only
ever tried one of the three. `cpu-only` — the one that avoids the GPU path
these shape errors come from — was never run.

Now all four presets run and the verdict distinguishes "the hatch works,
here is which preset" from "the hatch rescues nothing on this host". Only
the latter fails the build; `default` failing on its own is the hypothesis,
not a regression.

Host info now tees to stdout as well. Like the earlier stderr fix, it was
written only to the step summary, which `gh run view` cannot read — so the
ANE-visibility data this job exists to collect was unreachable.

All four verdict branches exercised locally against mocked exits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
The job was temporary and marked as such. It has produced its result, which
is recorded in the PR description with a link to the run:

  - the premise is confirmed — on `Apple M1 (Virtual)` / macOS 14.8.7 the
    ANE-pinned default fails at the vocoder with exactly the error this PR
    was opened for, observed rather than inferred;
  - the lever is not sufficient there — `cpu-and-gpu` and `cpu-only` clear
    that failure and hit a second one, `feature 'anchor' must be of rank 1,
    instead got a multi-array value of rank 2`.

The second failure is upstream, not this binding, and is filed as
FluidInference/FluidAudio#836.

Keeping the job would leave a permanently red check on every PR while
telling us nothing new: it would keep re-measuring a known result, and the
thing that would have to change for it to go green lives in FluidAudio. The
evidence stays reachable through the run link and the issue.

ci.yml is now byte-identical to main again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
…e boundary

Two LOW findings from the review, both about the diff saying more than it
means to.

The PR text claims unrelated `cargo fmt` rewrites were excluded, but two
were not: `qwen3_streaming_start` and `qwen3_streaming_feed` had been
reflowed in passing. They are back to main's formatting, so the diff is
now only this subject. main is not rustfmt-clean there, and matching it
means inheriting that — which is the intended trade, since the alternative
is unrelated churn.

The second is that an init failure reaches Rust as a bare `-1`: a missing
voice pack and an ANE that cannot prepare the vocoder are indistinguishable
from the caller's side, and the text that would tell them apart goes to
stderr from Swift. Threading a last-error string across the boundary is the
real fix, but that is a wider change than this PR's subject and would touch
the other init entry points to be consistent. Documented instead, with the
two errors actually observed on CI so the reader knows what to go looking
for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKfMMVQbSEgEca4nMYey4D
@Alex-Wengg

Copy link
Copy Markdown
Member

  ## PR #21 — Kokoro compute units

  1. P2 — The public documentation contradicts both probe outcomes.

     The API docs promise that CpuAndGpu or CpuOnly “keeps synthesis working” on virtualized macOS
     (src/lib.rs L507–520

     (https://github.com/FluidInference/fluidaudio-rs/blob/ec2a2d75781946d278f77626dd13b26c7694b6f7/src/lib.rs#L507-L520));
     the example and Swift comments make similar claims.

     However:
      - Attempt 1: all four presets failed.
      - Attempt 2, rerun today
        (https://github.com/FluidInference/fluidaudio-rs/actions/runs/31007985381/attempts/2): all
        four presets succeeded, including default.

     Both attempts used the same commit, macOS 14.8.7, runner-image version, virtual M1, and Xcode
     16.2. The differing result likely came from an external model artifact, though that needs
     confirmation. Update the PR body with both outcomes and describe compute-unit selection as an
     API/debugging control—not a proven macOS-14 workaround. Recording the model artifact digest
     would make future reproduction meaningful.

  2. P2 — The documented example can corrupt its WAV output.

     The example recommends:

     ... cpu-and-gpu > out.wav

     It performs synthesis while stdout is already redirected, then writes the WAV to that same
     stream (example L26–36

     (https://github.com/FluidInference/fluidaudio-rs/blob/ec2a2d75781946d278f77626dd13b26c7694b6f7/examples/kokoro.rs#L26-L36)).
     The PR body says CoreML emitted 436 bytes of diagnostics to stdout on this path. Those bytes
     would precede RIFF and corrupt out.wav.

     Prefer an explicit output-file argument, or suppress/redirect fd 1 around initialization and
     synthesis before writing the WAV.

  The ABI design itself looks good: the original three-argument symbol remains intact, the new
  preset gets its own symbol, invalid strings fail explicitly, and the Rust enum mapping is
  straightforward.

  ## PR #22 — cache isolation

  The previous findings remain unresolved; its head has not changed:

  1. P2 — Add Rust’s toolchain digest to the target cache key. stable moves, but the key only
     identifies Swift. Use id: rust and ${{ steps.rust.outputs.cachekey }}.

  2. P3 — Remove .build and correct the rationale. build.rs uses $OUT_DIR/swift-build, and OUT_DIR
     is already under target (build.rs L9–26

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants