rust bindings#29
Merged
Merged
Conversation
…ples
Finish the per-PR gates for the Rust binding and add the two postures and
the canonical example set, all on the shared CI rails.
M4 (conformance CI + Windows):
- rust-ci.yml wires fetch-canary + `cargo test -p transcribe-cpp`, so the
model-gated tests actually run (two tiers per requirements §4: no-model
always, incl. forks; model tier gated on HF_TOKEN/canary, skips cleanly).
- Add the Windows/MSVC matrix leg — first cargo-driven MSVC build of this
tree (msvc-dev-cmd + Ninja + vcpkg zlib:x64-windows-static-md +
CMAKE_PREFIX_PATH; dynamic CRT, matching rustc's /MD). ccache launcher
made non-Windows-only.
- cmake/transcribe-install.cmake: complete the provisional Windows static
zlib translation — stage the resolved zlib.lib into the lib dir and link
it by name (vcpkg spells it `zlib`, not `z`); MSVC auto-links the CRT.
M5 (dylib posture — never exercised before; M1–M4 are all static):
- sys/build.rs: guard the shared rpath to non-Windows; new
transcribe-cpp/build.rs re-emits it for the safe crate's own tests/examples
(cargo:rustc-link-arg does not propagate from a dependency's build script).
On Windows (no rpath) the -sys build.rs stages transcribe.dll + the ggml
DLLs next to each cargo artifact (deps/, examples/, profile root).
- Posture matrix {static, dylib} × {linux, macos, windows}; new
tests/degradation.rs asserts the CPU floor / clean unavailable-backend
probe / init_backends in both postures (requirements §5 request path).
M6 (examples): the five canonical CI-executed examples — transcribe-file,
streaming, batch, backend-select, error-handling — plus examples/common,
run on every matrix leg under the same canary skip rules as the model tests.
The Windows legs prove on first push (no MSVC available locally); every
other leg + the full gate suite is green locally on macOS/Metal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lease)
Wire the crates.io distribution path, mirroring the Python publish flow.
- publish.yml gains two jobs:
- rust-rehearsal (workflow_dispatch): `cargo publish --dry-run` the sys
crate (full verify build) + run the packed-crate smoke. crates.io has no
TestPyPI analog, so this is the rehearsal — dry-run + shipped-artifact
test, no upload.
- rust-release (tag v*): publish transcribe-cpp-sys first, then
transcribe-cpp, from CI via the `crates-io` environment (approval gate),
never a laptop (requirements §5).
- scripts/ci/rust_packed_smoke.py — the "test the shipped artifact" gate
(requirements §4): packs the sys crate (which carries the whole C++ tree),
and builds libtranscribe from NOTHING but the packed tarball's vendored
sources, then transcribes the canary through the safe API. The safe crate
is staged + redirected via [patch.crates-io] because it can't be packaged
pre-publish (its sys dep `^0.0.1` isn't satisfied by the 0.0.0 placeholder;
sys-first ordering is inherent).
- READMEs: install one-liner, build prerequisites (CMake + zlib; vcpkg on
Windows), and the quickstart/examples pointer.
Verified locally on macOS: dry-run (sys, full verify) and the packed smoke
both green — built from the packed tarball and transcribed the canary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first Windows CI run got the MSVC native build green on the first try, but windows-static failed linking the test exe with `LNK1181: cannot open input file 'zlib.lib'`. Root cause: the install manifest's `_transcribe_zlib_archive` helper looked up `ZLIB::ZLIB` / `ZLIB_LIBRARY` at the top-level scope where cmake/transcribe-install.cmake runs, but find_package(ZLIB) runs in src/CMakeLists.txt — and its imported target is subdirectory-local while ZLIB_LIBRARY is a normal (scope-local) var. Neither is visible at top level, so staging silently no-op'd and the manifest fell through to a bare `zlib` system-lib that the linker can't find. Fix: src/CMakeLists.txt captures the resolved archive (ZLIB_LIBRARY_RELEASE, falling back to ZLIB_LIBRARY) into a TRANSCRIBE_ZLIB_ARCHIVE cache var while still in find_package's scope; install.cmake prefers that. WIN32-guarded, so linux/macos manifests are unchanged (verified: macOS build green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tic CI The first Windows CI runs got the MSVC build green and the zlib fix landed; two more link-level issues then surfaced across postures: 1. OpenMP (linux-static, and a stray /fopenmp on Windows): TRANSCRIBE_USE_OPENMP defaults ON and auto-detects, so CI builds enabled it — but its `-fopenmp` appears only as a manifest link_flag, i.e. a `cargo:rustc-link-arg`, which does NOT propagate to the safe crate's test/example binaries. Static links then failed with undefined GOMP_*/omp_* symbols (dylib hid it: OpenMP links into the shared lib). Fix: build.rs forces TRANSCRIBE_USE_OPENMP=OFF unless `--features openmp` opts in — a self-contained static default, deterministic across runners. 2. advapi32 (windows-static): ggml-cpu reads the registry for CPU feature detection (RegOpenKeyEx/RegCloseKey) but ggml's CMake never links advapi32 (it rides MSVC default-lib auto-linking, which the manifest reconstruction loses). Add advapi32 to the Windows static system_libs. macOS unaffected (it wasn't using OpenMP; advapi32 is WIN32-only) — local build + safe-crate tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cancel test crashes on Windows x86 with STATUS_INTEGER_DIVIDE_BY_ZERO in whisper's long-form abort path — a native integer ÷0 that faults on x86 but is masked on arm64 (returns 0) and only triggers when the abort lands in a narrow compute window (so a fast machine + UBSan never reproduce it). To get the exact file:line: - tests/whisper_e2e_smoke.cpp: opt-in TRANSCRIBE_DIAG_CANCEL harness — a threaded wall-clock cancel of a long-form run, swept over delays and repeated. No-op unless the env var is set (normal/native-ci runs are unaffected). - .github/workflows/diag-cancel.yml: workflow_dispatch-only job that builds the e2e test with RelWithDebInfo and runs the harness under gdb (Linux) / cdb (Windows) to print the faulting backtrace with source lines. The debugger slowdown also widens the timing window, making the fault reliable. Both are diagnostic scaffolding — remove once the bug is fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fault branch) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TRANSCRIBE_WHISPER_THOLD_DISABLED / _LOGPROB_DISABLED defined ±INF as ((float)(1.0/0.0)) / ((float)(-1.0/0.0)). clang and gcc fold that to IEEE infinity, but MSVC hard-errors on a constant divide-by-zero (C2124), so the public header failed to compile in any MSVC C++ translation unit that touched the sentinels (surfaced building the C++ whisper test on Windows for the first time). Use INFINITY from <math.h> instead — same value, no divide. FFI-neutral: the value is unchanged (+/-inf), so the bindgen drift gate and the abihash are both untouched (verified: `cargo xtask bindgen --check` and `generate.py --check` stay green; these float macros aren't in the hash body). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 1600-run sweep timed out the linux diag leg (each run recomputes the full mel of 132s audio). Drop to 6x jfk (~66s, still long-form) and a small delay sweep around the ~40ms danger point; the debugger quits on the first crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + dllimport (F)
The cancel conformance test tiled jfk 12x to be mid-flight, which pushed it into
whisper's long-form path — and cancelling a long-form run hits a native integer
divide-by-zero on x86 (masked on arm64, timing-gated so only the slow Windows CI
runner faults reliably). That is a pre-existing whisper-compute bug, not a
binding bug: the Rust layer maps the abort correctly.
Decouple the binding from it: tile only 2x (~22s, still short-form / one whisper
window) so the test exercises the cancellation contract (CancelToken ->
Error::Aborted + partial) without entering the long-form abort path. Same
contract, no native ÷0.
File both bugs found during the Windows bringup in docs/known-issues.md with
repro detail:
- B: the long-form-cancel native ÷0 (localized to whisper's abort path; exact
line needs an x86 debugger backtrace — the env-gated TRANSCRIBE_DIAG_CANCEL
harness in tests/whisper_e2e_smoke.cpp reproduces it).
- F: transcribe.h forces __declspec(dllimport) on Windows, breaking static C++
consumers (LNK2019 __imp_*); the Rust/Python bindings are unaffected.
Also drops the temporary diag-cancel.yml workflow (its job is done; the harness
+ docs/known-issues.md are the durable repro).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; correct B filing The short-form (22s) cancel still crashed on Windows with STATUS_INTEGER_DIVIDE_BY_ZERO, so B is NOT long-form-specific — it is reached by any in-flight whisper cancel, and it is Windows/MSVC-specific: the same abort passes on linux x86 (rust-build linux-dylib) and is masked on arm64. So it is an MSVC-codegen issue (most likely an uninitialized divisor the abort path skips initializing), not a generic-x86 or long-form bug. The previous short-form decouple was therefore wrong. Skip cross_thread_cancel_of_in_flight_run on Windows instead (cfg!(target_os = "windows")); the cancellation contract stays covered on linux/macos, and the uncancelled-run test runs everywhere (normal whisper transcription works on Windows). docs/known-issues.md "B" updated with the corrected scope + repro. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-installed ÷0) With cross_thread skipped, the cancel binary still crashed — and the log shows `cross_thread ... ok` then the process dies, so the crasher is uncancelled_run_is_not_aborted: a NORMAL run with a cancel token merely installed (never fired). So B's trigger is the abort callback being present, not the abort firing — Windows/MSVC only (it passes on linux x86, masked on arm64). Skip uncancelled on Windows too, and guard the error-handling example's cancellation demo on Windows (it installs a token). This also lets the rest of the Windows conformance (transcribe/streaming) and the examples run for the first time — normal token-less transcription is expected to work (the Python wheels transcribe on Windows). docs/known-issues.md "B" updated to the token-installed scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…consumers (F) A C++ TU that includes transcribe.h and links the static transcribe.lib on MSVC failed to link (LNK2019 __imp_transcribe_*): the header forced __declspec(dllimport) on every Windows consumer, with no way to say "I'm linking the static archive." Add a TRANSCRIBE_STATIC branch (empty decoration) and have the static `transcribe` CMake target propagate it PUBLIC, so in-tree C++ tests/examples linking the archive resolve the plain symbols. The bindgen/Python FFIs carry no __declspec and are unaffected; ABI/bindgen-neutral (the generator parses with _WIN32 undefined). Unblocks the C++ whisper e2e test on MSVC, which the "B" int-divide diagnostic needs. Closes known-issues "F". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows/MSVC integer divide-by-zero (0xC0000094) faults ANY whisper run,
not just a cancelled one (the alphabetically-first cancel test merely masked
that it crashes before any run completes). None of our source integer divisions
can be zero (all config-derived or guarded), so the ÷0 is in ggml, reached by
our graph on MSVC Release codegen — needs a faulting backtrace to localize.
- whisper_e2e_smoke.cpp: TRANSCRIBE_DIAG_FAULT_TRACE installs a vectored
exception handler that symbolizes the faulting IP + full stack via DbgHelp.
Self-contained: needs no cdb on the runner, only the Release+/Z7 PDB.
- diag-b.yml: push-triggered, path-scoped. Builds the e2e test in the SAME
codegen as the failing cargo build (CMAKE_BUILD_TYPE=Release, matching the
-sys build.rs cfg.profile("Release")) plus /Z7 + linker /DEBUG for symbols,
then runs a PLAIN transcription with the tracer (and under cdb if present).
Diagnostic scaffolding — remove (with the dbghelp link) once "B" is fixed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ths) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… ÷0)
Root cause (caught by the diag-b backtrace, not the prior cancellation theory):
the fault is ggml_time_us+0x24 at ggml/src/ggml.c:532, in whisper_load's
`t_load_start = ggml_time_us()` during MODEL LOAD — not the run/abort path. On
MSVC ggml_time_us divides QueryPerformanceCounter by a global frequency that
ggml_time_init() fills in; ggml_init() calls it, but every family times its own
load before any ggml context exists, so the divisor is still 0 ->
STATUS_INTEGER_DIVIDE_BY_ZERO (0xC0000094). POSIX ggml_time_* read clock_gettime
directly (no divisor) so it is silently fine there, and arm64 masks integer ÷0
as 0 — exactly the observed platform signature. So EVERY whisper run faulted at
load on Windows; the cancel test only looked guilty for being alphabetically
first.
Fix: call ggml_time_init() exactly once (std::call_once) at the first public
entry point — transcribe_model_load_file and transcribe_init_backends — as
ggml.h instructs ("call this once at the beginning of the program"). timer_freq
is process-global, so one init covers all later mel/encode/decode timing.
diag-b: add src/transcribe.cpp to the path filter so this re-runs the e2e test
with the fix (validation before the diagnostic is removed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ggml_time_init fix removed the load-time ÷0 (the diag run shows no fault), but Windows transcription then hangs — rust-ci's cargo-test step ran 40+ min on a 39s (cached) build, and the C++ e2e likewise. Linux runs the same threading (whisper never sets n_threads -> ggml default 4 on 2 vcpus) without hanging, so this is a Windows-specific deadlock, not oversubscription. cdb is absent on the runner, so add a self-contained watchdog (TRANSCRIBE_DIAG_WATCHDOG_SEC=N): after N s it suspends + StackWalks every thread and symbolizes via DbgHelp, so the stuck stack is captured in one fast (cached-build) round-trip. diag-b drops the dead cdb step and runs a 60s watchdog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on Windows With the load-time ÷0 fixed, Windows whisper transcription HUNG: a watchdog thread-dump showed all CPU worker threads wedged in ggml's custom spin-barrier (ggml_barrier, ggml-cpu.c:580) while the main thread waited inside mul_mat's barrier — a deadlock under MSVC codegen. ggml takes that custom-barrier path only when GGML_OPENMP is OFF; the OpenMP `#pragma omp barrier` path is the one production builds use on Windows and it does not deadlock. OpenMP was forced off (f35e899) because GNU/Clang `-fopenmp` is a link flag that doesn't propagate to downstream static binaries — a Unix-only problem. Under MSVC, OpenMP is auto-linked via the /openmp pragma baked into the ggml objects (no -fopenmp anywhere), so force GGML_OPENMP=ON for MSVC even when our knob is off. TRANSCRIBE_USE_OPENMP stays off, so the link manifest emits no -fopenmp and the Parakeet host-decoder TU is unchanged; only ggml's CPU threading flips, which is the correctness fix. vcomp140.dll ships in the VC++ runtime already required. Validated end to end: the whisper e2e transcribes (exit 0, no hang) on the Windows diag with TRANSCRIBE_USE_OPENMP=OFF + this override. diag-b mirrors the cargo knobs to exercise the real fix path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revert the Windows skips added while B was open: the cancel tests and the error-handling example's cancellation demo run everywhere again, now that Windows whisper load + transcription work (ggml_time_init + GGML_OPENMP-on-MSVC). The prior "cancel token installed -> ÷0" framing was a misread — model load itself ÷0'd before any run, and the cancel binary was just alphabetically first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
B is root-caused and fixed (ggml_time_init at load + GGML_OPENMP=ON under MSVC),
validated green on both Windows legs. Remove the temporary diagnostics:
- the whisper_e2e_smoke vectored-exception fault tracer + watchdog thread-dump
+ their Windows includes, and the obsolete TRANSCRIBE_DIAG_CANCEL harness
- the dbghelp link those needed
- .github/workflows/diag-b.yml
- docs/known-issues.md — both tracked bugs (B int-÷0, F dllimport) are fixed;
the resolutions live in the fix commits. De-reference it in transcribe.cpp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MSVC barrier-deadlock workaround (GGML_OPENMP=ON) was being forced for every MSVC consumer, which would have silently flipped the Python Windows wheels to OpenMP — reversing their deliberate no-OpenMP posture (numpy/torch runtime coexistence; python-wheels.yml). Scope it to the Rust binding instead: - CMakeLists: only force GGML_OPENMP OFF (the no-OpenMP posture) when a consumer hasn't explicitly set it; honor an explicit -DGGML_OPENMP=ON. - sys/build.rs: pass -DGGML_OPENMP=ON on Windows (the Rust binding is CPU-default and standalone, so it needs the working CPU threading and has no numpy/torch coexistence concern). vcomp auto-links via the ggml objects' /openmp pragma. The Python Windows wheels keep GGML_OPENMP unset -> still OpenMP-free. Their latent CPU-backend deadlock (they default to Vulkan) is a separate maintainer call (single-threaded CPU vs. OpenMP) and intentionally left untouched here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…::Busy) Review (High): the safe wrapper only locked individual native calls, so an active stream — which spans begin..feed*..finalize — could overlap a second stream or a run on ANOTHER session of the same model, reaching the documented per-model UB (corrupted decodes / command-buffer failures, include/transcribe.h) from safe code. Make the per-model lock carry a "stream active" flag (Mutex<()> -> Mutex<bool>): one-shot run/run_batch still serialize on it, stream begin claims the lease for the whole Stream lifetime (released in Drop), and any run/batch/stream that would overlap an active stream is refused with the new Error::Busy instead of racing. Erroring (not blocking) avoids deadlocking a single thread that holds a Stream and then runs on another session. Adds a regression test: two sessions on one model, second stream and a mid-stream run both return Busy, and the lease frees on drop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rol) Review (High): `transcribe-cpp --no-default-features` still pulled transcribe-cpp-sys's `default = ["metal"]`, so Metal (and TRANSCRIBE_METAL=ON) stayed active — the safe crate's feature flags were not the real control surface. Set default-features=false on the dependency; the backend features forward explicitly, so the default still enables Metal via `metal = ["transcribe-cpp-sys/metal"]` while `--no-default-features` is now a true CPU build. Confirmed with `cargo tree -e features`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review fixes: - WhisperRunOptions.initial_prompt with an interior NUL was silently dropped (CString::new(..).ok()); now propagates Error::Nul like language / target_language. materialize() returns Result and the caller `?`-forwards it. - init_backends() used to_string_lossy(), which mangles a non-UTF-8 path with U+FFFD instead of rejecting/passing it. Reuse model.rs's path_bytes (now pub(crate)): Unix paths pass through as bytes, non-UTF-8 is rejected on Windows — matching model loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review (Medium): the MSVC OpenMP workaround (Rust sets GGML_OPENMP=ON, wheels stay OpenMP-free) read as an inconsistent boundary smell. Document it as the deliberate per-consumer policy it is, centrally in the root CMakeLists "OpenMP — CENTRAL POLICY" block: the Rust binding needs OpenMP (CPU-default, standalone), the Python wheels deliberately stay OpenMP-free (numpy/torch coexistence; default to Vulkan) at the cost of a documented limitation — multi-threaded CPU compute on Windows is unsupported for the wheels. build.rs points at that block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review (High): the safe crate disabled transcribe-cpp-sys/default, but sys/build.rs only ever set TRANSCRIBE_METAL=ON (never OFF), and CMake defaults TRANSCRIBE_METAL ON on Apple Silicon — so a `--no-default-features` build still enabled Metal, contradicting Cargo.toml's "pure CPU build on macOS is default-features = false". Set TRANSCRIBE_METAL explicitly on Apple to track the `metal` feature (ON+embed when present, OFF when absent). Verified: the --no-default-features build cache now has TRANSCRIBE_METAL=OFF / GGML_METAL=OFF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… drop Review (Medium): the lease was held from stream begin until Stream::drop, so a finalized or reset stream still blocked every other session on the model until the handle dropped — over-conservative, since the C contract is "one ACTIVE stream" and finalize/reset end the active stream. Track lease ownership on the Stream (holds_lease) and release it the moment the stream stops being active (finalize/reset), keeping drop as the fallback; the per-stream flag means drop never releases a lease another session has since acquired. Sound because the C side state-guards feed/finalize (INVALID_ARG on a non-active session, before any compute), so a stray call after release cannot race. Adds a test asserting a second session may stream after the first finalizes/resets without dropping it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-cpp) Review (Low): the comment said bindings/rust/safe/; the safe wrapper lives at bindings/rust/transcribe-cpp/ (as the manifest list right below it shows). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…locks on MSVC Build the SHARED libtranscribe both ways and run a CPU whisper transcription through the ctypes Python binding under a faulthandler watchdog: A) GGML_OPENMP=OFF (the official Windows wheel posture) -> expect HANG B) GGML_OPENMP=ON (the Rust-side fix) -> expect PASS This tests the exact native code the wheels ship, at the Python layer, to confirm whether the wheels are exposed to the MSVC ggml-cpu barrier deadlock. Validated locally on macOS (clang barrier works -> PASS, exercises the script). Note: whisper does not apply session n_threads to its ggml graph (uses the ggml default of 4), so n_threads=1 would not avoid the hang for whisper — the decisive axis tested here is OpenMP off/on. Diagnostic scaffolding; remove once the wheel CPU-on-Windows posture is decided. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First diag-py-cpu run was surprising — the SHARED (wheel-posture) build with GGML_OPENMP=OFF transcribed CPU whisper fine, while the STATIC Rust build reliably deadlocked. Re-run to settle whether shared is reliably safe or an intermittent race: loop the shared CPU run x12 (count hangs) and add a same-runner STATIC control (C++ e2e under a 90s timeout) to confirm static still hangs. If shared is N/N clean and static hangs, the deadlock is static-specific and the Python wheels are not exposed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…adlock (W1) The diag-py-cpu run confirmed the wheel posture IS exposed: shared lib with GGML_OPENMP=OFF hangs ~12/12 on CPU whisper (the first run's single pass was a fluke race-win), and the static control hangs too — so the non-OpenMP ggml-cpu barrier deadlock under MSVC is near-deterministic on both static and shared, not static-specific. Remove the throwaway CI scaffolding now that the question is answered, and capture the finding + a Windows-machine debugging plan in docs/known-issues.md (W1) so the deferred root-cause work is ready to resume. The Rust Windows fix (GGML_OPENMP=ON) and the documented Python wheel limitation both stand confirmed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.