-
Notifications
You must be signed in to change notification settings - Fork 55
FEAT: conda recipe for mssql-python (self-contained repackage of the signed wheel) #734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
6cce9af
FEAT: conda recipe for mssql-python (self-contained repackage of the …
jahnvi480 89544fd
Trim recipe comments to reviewer-focused essentials; add two hardenin…
jahnvi480 1af01b3
FIX: assert the extracted ddbc_bindings matches the target Python on …
jahnvi480 f24b366
FIX: revert host/run python to bare `python` (>=3.10 range broke cond…
jahnvi480 de0eefb
DOC: macOS Encrypt=yes needs system OpenSSL (brew/port) - the bundled…
jahnvi480 1aa1bdc
FIX: select the ODBC wheel arch-specifically (py3-none-<arch>) - the …
jahnvi480 441e394
Merge branch 'main' into jahnvi/conda-recipe
jahnvi480 1abc1df
FIX: derive conda win ODBC/wheel arch from target_platform, not Pytho…
jahnvi480 9accb23
Merge origin/main into jahnvi/conda-recipe (PR #737: correct-arch mss…
jahnvi480 95c62f6
FEAT: keep the arm64 mssql_py_core in win-arm64 conda packages (enabl…
jahnvi480 f365f51
Merge branch 'main' into jahnvi/conda-recipe
jahnvi480 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Keep shell build scripts LF so conda-build works on Linux/macOS agents, | ||
| # regardless of the checkout host's core.autocrlf setting. | ||
| *.sh text eol=lf | ||
|
|
||
| # Keep .bat scripts CRLF for Windows conda-build agents, regardless of the checkout | ||
| # host's core.autocrlf. cmd.exe tolerates LF here (no goto/labels), but pinning CRLF | ||
| # removes any doubt on odd checkouts. | ||
| *.bat text eol=crlf |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| @echo on | ||
| REM Repackage the prebuilt, signed mssql-python wheel into a conda package (offline) and | ||
| REM vendor the ODBC Driver 18 payload inside it. conda-build exports PKG_NAME / | ||
| REM PKG_VERSION / CONDA_PY; the pipeline exports WHEELS_DIR + MSSQL_ODBC_VERSION. | ||
| setlocal enabledelayedexpansion | ||
|
|
||
| set "SP=%PREFIX%\Lib\site-packages" | ||
| if not exist "%SP%" mkdir "%SP%" | ||
| set "PKG_UNDERSCORE=%PKG_NAME:-=_%" | ||
|
|
||
| REM Target arch comes from the conda TARGET platform (win-64 / win-arm64), NOT from | ||
| REM whether the host Python can execute -- a native win-arm64 host runs its own Python yet | ||
| REM still needs the win_arm64 payload. conda-build sets target_platform; CONDA_SUBDIR is | ||
| REM the same value. This is the single source for both the code-wheel and ODBC driver arch. | ||
| set "ODBC_ARCH=win_amd64" | ||
| set "TGT_PLATFORM=%target_platform%" | ||
| if not defined TGT_PLATFORM set "TGT_PLATFORM=%CONDA_SUBDIR%" | ||
| if /i "%TGT_PLATFORM%"=="win-arm64" set "ODBC_ARCH=win_arm64" | ||
|
|
||
| REM Native leg: the host Python runs, so pip installs the matching wheel. Cross leg: the | ||
| REM target Python can't run on this agent (e.g. win_arm64 built on x64), so extract the | ||
| REM cp%CONDA_PY% wheel (a zip) with tar. This is orthogonal to the target arch above. | ||
| "%PYTHON%" -c "import sys" >nul 2>&1 | ||
| if errorlevel 1 ( | ||
| echo Host Python "%PYTHON%" not executable here ^(non-emulated cross-build^); extracting cp%CONDA_PY% !ODBC_ARCH! code wheel without Python. | ||
| set "CODE_WHL=" | ||
| for %%W in ("%WHEELS_DIR%\!PKG_UNDERSCORE!-%PKG_VERSION%-cp%CONDA_PY%-*-!ODBC_ARCH!.whl") do if exist "%%~fW" set "CODE_WHL=%%~fW" | ||
| if not defined CODE_WHL ( | ||
| echo ERROR: no %PKG_NAME%==%PKG_VERSION% cp%CONDA_PY% !ODBC_ARCH! wheel in "%WHEELS_DIR%" | ||
| exit /b 1 | ||
| ) | ||
| echo Extracting "!CODE_WHL!" into "%SP%" | ||
| tar -xf "!CODE_WHL!" -C "%SP%" | ||
| if errorlevel 1 exit /b 1 | ||
| REM win-arm64 cross can't run the arm64 Python, so statically prove the extracted | ||
| REM binding is for THIS interpreter (the osx-arm64 twin bug shipped a cp310 .so in | ||
| REM every build); a wrong-Python .pyd would only fail at the user's import. | ||
| if not exist "%SP%\mssql_python\ddbc_bindings.cp%CONDA_PY%-*.pyd" ( | ||
| echo ERROR: extracted "!CODE_WHL!" has no mssql_python\ddbc_bindings.cp%CONDA_PY% pyd ^(wrong-Python binding^). | ||
| exit /b 1 | ||
| ) | ||
| REM Keep mssql_py_core when the wheel provides a matching-arch native ext so bulk copy | ||
| REM ships (PR #737 makes the win-arm64 wheel vendor the arm64 core). If only the legacy | ||
| REM x64 core is present (a pre-#737 wheel), strip it so the package never carries a core | ||
| REM that can't load on the target -- the .pyd name encodes the arch. Bulk copy then lazily | ||
| REM reports "not available"; the rest of the DBAPI works. Mirrors the ddbc check above. | ||
| if exist "%SP%\mssql_py_core\mssql_py_core.cp%CONDA_PY%-!ODBC_ARCH!.pyd" ( | ||
| echo Keeping matching-arch mssql_py_core; bulk copy enabled on the !ODBC_ARCH! package. | ||
| ) else ( | ||
| echo No cp%CONDA_PY%-!ODBC_ARCH! mssql_py_core in the wheel; removing the mismatched core ^(bulk copy unavailable until the arm64-core wheel ships^). | ||
| if exist "%SP%\mssql_py_core" rmdir /s /q "%SP%\mssql_py_core" | ||
| if exist "%SP%\mssql_py_core.libs" rmdir /s /q "%SP%\mssql_py_core.libs" | ||
| ) | ||
| ) else ( | ||
| "%PYTHON%" -m pip install --no-deps --no-index --find-links "%WHEELS_DIR%" %PKG_NAME%==%PKG_VERSION% -vv | ||
| if errorlevel 1 exit /b 1 | ||
| ) | ||
|
|
||
| REM Extract the arch-specific odbc wheel into the SAME site-packages so | ||
| REM mssql_python_odbc\libs\ sits beside mssql_python\ and the loader finds the driver. | ||
| REM The py3-none tag only means "no Python bytecode" -- the vendored driver DLLs ARE | ||
| REM arch-specific, so match the EXACT target arch (%ODBC_ARCH%, derived from the conda | ||
| REM target platform above) rather than py3-none-win_* which also matches the other arch's | ||
| REM wheel and could vendor an x64 driver into a win-arm64 package. The exact | ||
| REM tag is unique per version+arch, so no ambiguous multi-match is possible. | ||
| set "ODBC_WHL=" | ||
| for %%W in ("%WHEELS_DIR%\mssql_python_odbc-%MSSQL_ODBC_VERSION%-py3-none-%ODBC_ARCH%.whl") do if exist "%%~fW" set "ODBC_WHL=%%~fW" | ||
| if not defined ODBC_WHL ( | ||
| echo ERROR: no mssql_python_odbc==%MSSQL_ODBC_VERSION% py3-none-%ODBC_ARCH% wheel in "%WHEELS_DIR%" | ||
| exit /b 1 | ||
| ) | ||
| echo Extracting "!ODBC_WHL!" into "%SP%" | ||
| tar -xf "!ODBC_WHL!" -C "%SP%" | ||
| if errorlevel 1 exit /b 1 | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| #!/bin/bash | ||
| # Repackage the prebuilt, signed mssql-python wheel into a conda package (offline) and | ||
| # vendor the ODBC Driver 18 payload inside it -- no separate mssql-python-odbc conda | ||
| # package. Both the code wheel and the py3-none-<plat> odbc wheel land in the SAME | ||
| # site-packages, so mssql_python_odbc/libs/ sits beside mssql_python/ and the C++ | ||
| # loader finds the driver. conda-build exports PKG_NAME / PKG_VERSION / CONDA_PY; | ||
| # WHEELS_DIR (one wheel per target) + MSSQL_ODBC_VERSION come from the pipeline. | ||
| set -euo pipefail | ||
| # Emulated aarch64 leg: $PYTHON is the aarch64 interpreter under qemu-user; point it at | ||
| # the aarch64 glibc loader so it doesn't abort. Harmless no-op on every other leg. | ||
| [ -d /usr/aarch64-linux-gnu ] && export QEMU_LD_PREFIX="${QEMU_LD_PREFIX:-/usr/aarch64-linux-gnu}" | ||
|
|
||
| odbc_ver="${MSSQL_ODBC_VERSION:?MSSQL_ODBC_VERSION not set}" | ||
|
|
||
| # Native / QEMU-emulated legs: the host Python runs, so pip installs both wheels. Cross | ||
| # osx-arm64 (built on Intel): the arm64 Python can't execute, so extract both wheels | ||
| # (zips) with unzip instead -- same approach as the Windows bld.bat. | ||
| if "$PYTHON" -c "import sys" >/dev/null 2>&1; then | ||
| "$PYTHON" -m pip install --no-deps --no-index --find-links "$WHEELS_DIR" "$PKG_NAME==$PKG_VERSION" -vv | ||
| "$PYTHON" -m pip install --no-deps --no-index --find-links "$WHEELS_DIR" "mssql-python-odbc==$odbc_ver" -vv | ||
| else | ||
| echo "Host Python '$PYTHON' is not executable on this agent (non-emulated cross-build);" | ||
| echo "extracting both wheels into \$SP_DIR without running Python." | ||
| mkdir -p "$SP_DIR" | ||
| pkg_underscore="${PKG_NAME//-/_}" | ||
| # universal2 wheels are cpXY-specific (compiled ddbc_bindings), so filter on the | ||
| # target CONDA_PY to never grab another interpreter's wheel (mirrors bld.bat). | ||
| code_whl="" | ||
| for w in "$WHEELS_DIR/${pkg_underscore}-${PKG_VERSION}-cp${CONDA_PY}-"*.whl; do | ||
| [ -e "$w" ] && { code_whl="$w"; break; } | ||
| done | ||
| [ -n "$code_whl" ] || { echo "ERROR: no ${PKG_NAME}==${PKG_VERSION} cp${CONDA_PY} wheel in '$WHEELS_DIR'" >&2; exit 1; } | ||
| odbc_whl="" | ||
| # This cross branch only runs on macOS; the odbc payload is a single universal2 wheel | ||
| # (both arches in one), so match macosx explicitly rather than py3-none-* which would | ||
| # also match a Linux odbc wheel if one were ever staged in the same dir. | ||
| for w in "$WHEELS_DIR"/mssql_python_odbc-"$odbc_ver"-py3-none-macosx*.whl; do | ||
| [ -e "$w" ] && { odbc_whl="$w"; break; } | ||
| done | ||
| [ -n "$odbc_whl" ] || { echo "ERROR: no mssql_python_odbc==$odbc_ver py3-none-macosx wheel in '$WHEELS_DIR'" >&2; exit 1; } | ||
| echo "Extracting '$code_whl' -> '$SP_DIR'" | ||
| unzip -oq "$code_whl" -d "$SP_DIR" | ||
| # osx-arm64 cross can't run the arm64 Python, so statically prove the extracted | ||
| # binding is for THIS interpreter -- a cpXY ddbc_bindings for another Python (the bug | ||
| # where every osx-arm64 build shipped the cp310 .so) would only fail at the user's | ||
| # import. The python-tag twin of the win-arm64 PE-arch assert. | ||
| ls "$SP_DIR"/mssql_python/ddbc_bindings.cp${CONDA_PY}-*.so >/dev/null 2>&1 || { | ||
| echo "ERROR: '$code_whl' has no mssql_python/ddbc_bindings.cp${CONDA_PY}-*.so (wrong-Python binding)." >&2 | ||
| exit 1 | ||
| } | ||
| echo "Extracting '$odbc_whl' -> '$SP_DIR'" | ||
| unzip -oq "$odbc_whl" -d "$SP_DIR" | ||
| fi | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Linux driver reachability (#563) -- the core fix. | ||
| # --------------------------------------------------------------------------- | ||
| # Declaring krb5/openssl/libltdl as conda deps drops them in $PREFIX/lib, but that is | ||
| # INERT: the vendored ODBC .so ship with a bare DT_RUNPATH=$ORIGIN (no climb), so the | ||
| # loader never looks in $PREFIX/lib and falls through to SYSTEM krb5 (#563 crash) or | ||
| # can't find libltdl.so.7. Fix: stamp a relative "$ORIGIN:$ORIGIN/<climb>" onto | ||
| # libmsodbcsql* + libodbcinst.so.2 so they resolve THIS env's $PREFIX/lib. Safe to | ||
| # patch -- the Linux .so are malware-scanned, not code-signed (only Windows .dll / | ||
| # macOS .dylib are, and those are never touched). Linux-only: the glob is a no-op on | ||
| # macOS. audit_bundled_binaries.py asserts the same exact climb. | ||
| prefix_lib="$PREFIX/lib" | ||
| shopt -s nullglob | ||
| have_linux_payload=0 | ||
| [ -d "$SP_DIR/mssql_python_odbc/libs/linux" ] && have_linux_payload=1 | ||
| # Count the driver (libmsodbcsql) and the driver manager (libodbcinst) separately so a | ||
| # payload missing EITHER fails loudly -- a lone libodbcinst would ship no SQL driver. | ||
| msodbc_seen=0 | ||
| odbcinst_seen=0 | ||
| for libdir in "$SP_DIR"/mssql_python_odbc/libs/linux/*/*/lib; do | ||
| # Exact climb from this driver dir up to $PREFIX/lib (from the real layout, never a | ||
| # hard-coded ../ count). | ||
| climb="$("$PYTHON" -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$prefix_lib" "$libdir")" | ||
| want="\$ORIGIN:\$ORIGIN/$climb" | ||
| for so in "$libdir"/libmsodbcsql-*.so.* "$libdir"/libodbcinst.so.2; do | ||
| [ -e "$so" ] || continue | ||
| case "$(basename "$so")" in | ||
| libmsodbcsql-*.so.*) msodbc_seen=$((msodbc_seen + 1)) ;; | ||
| libodbcinst.so.2) odbcinst_seen=$((odbcinst_seen + 1)) ;; | ||
| esac | ||
| got="$(patchelf --print-rpath "$so" 2>/dev/null || true)" | ||
| if [ "$got" = "$want" ]; then | ||
| echo "RPATH-OK (already baked) $(basename "$so") -> $got" | ||
| continue | ||
| fi | ||
| patchelf --set-rpath "$want" "$so" | ||
| got="$(patchelf --print-rpath "$so")" | ||
| # Assert the EXACT intended RUNPATH, not just "no absolute entry". | ||
| if [ "$got" != "$want" ]; then | ||
| echo "ERROR: patch did not yield the exact expected RUNPATH ('$got' != '$want')." >&2 | ||
| exit 1 | ||
| fi | ||
| echo "RPATH-PATCHED $(basename "$so") -> $got" | ||
| done | ||
| done | ||
| shopt -u nullglob | ||
| # A Linux payload missing the driver OR the driver manager is a bypass hole (a bare | ||
| # `conda build` skipping the orchestrator audit would ship un-asserted binaries). | ||
| if [ "$have_linux_payload" = "1" ] && { [ "$msodbc_seen" = "0" ] || [ "$odbcinst_seen" = "0" ]; }; then | ||
| echo "ERROR: Linux payload present but incomplete (libmsodbcsql=$msodbc_seen, libodbcinst.so.2=$odbcinst_seen)." >&2 | ||
| exit 1 | ||
| fi | ||
| [ "$msodbc_seen" -gt 0 ] && echo "LINUX_RPATH_CLIMB_OK" || true |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| {% set version = environ.get('MSSQL_PYTHON_VERSION', '1.14.0') %} | ||
|
|
||
| package: | ||
| name: mssql-python | ||
| version: "{{ version }}" | ||
|
|
||
| build: | ||
| number: 0 | ||
| # This recipe REPACKAGES a prebuilt, signed wheel -- it compiles nothing -- so | ||
| # conda-build's from-source checks mis-fire on the vendored binaries. Downgrade | ||
| # overlinking/overdepending to warnings, and disable the relocation / prefix scans | ||
| # that would corrupt the signature or fail the pure-repackage packaging step. | ||
| error_overlinking: false | ||
| error_overdepending: false | ||
| binary_relocation: false | ||
| detect_binary_files_with_prefix: false | ||
| # .pyc byte-compilation runs the TARGET Python, which can't execute on the cross legs | ||
| # (osx-arm64 built on Intel, win-arm64 on x64). Skip pyc there; Python regenerates it. | ||
| skip_compile_pyc: | ||
| - "**/*.py" # [osx or win] | ||
| # Passed into the isolated conda-build env so build.sh / bld.bat can install the | ||
| # prebuilt wheel from --find-links offline (WHEELS_DIR) and locate the odbc wheel to | ||
| # vendor in (MSSQL_ODBC_VERSION). | ||
| script_env: | ||
| - WHEELS_DIR | ||
| - MSSQL_ODBC_VERSION | ||
|
|
||
| requirements: | ||
| build: | ||
| # patchelf stamps the $ORIGIN RPATH climb (#563) so the declared conda | ||
| # krb5/openssl/libltdl below are actually reachable. Linux-only, build-time only. | ||
| - patchelf # [linux] | ||
| host: | ||
| # Bare `python` (NO version range): conda-build binds the per-Python variant from the | ||
| # --python matrix here, so each build emits the matching py3XX package. A range such | ||
| # as `python >=3.10` collapses ALL --python builds into ONE cp<newest> package (build | ||
| # string "py>=310") whose python_abi is pinned to 3.14, so `conda create python=3.10 | ||
| # mssql-python` can't solve. The >=3.10 floor is enforced implicitly: only py310-py314 | ||
| # are built and each package's python_abi pins its exact minor. | ||
| - python | ||
| - pip | ||
| run: | ||
| # Bare `python` for the same reason as host (the floor is the per-build python_abi, | ||
| # NOT a range on python -- a range breaks the per-Python solve). | ||
| - python | ||
| # conda does NOT inherit the wheel's install_requires, so pin azure-identity here. | ||
| - azure-identity >=1.12.0 | ||
| # ODBC Driver 18 payload deps (the driver ships inside this package, so its | ||
| # security-serviced deps are declared here). OpenSSL for TLS is dlopen'd, so | ||
| # overlinking can't see it; pinned <4 (Driver 18 supports the OpenSSL 1.1/3.0 ABI | ||
| # only). Linux ONLY -- on macOS the driver dlopens OpenSSL from a FIXED list of | ||
| # system paths (/opt/homebrew, /opt/local, /usr/lib) by unversioned name, with no | ||
| # @loader_path/rpath/bare-leaf hook, so a conda-env copy is unreachable: declaring | ||
| # openssl [osx], DYLD_FALLBACK, RTLD_GLOBAL preload, and payload co-location were ALL | ||
| # proven ineffective (validated on real Apple Silicon, 2026-09). macOS Encrypt=yes | ||
| # therefore needs SYSTEM OpenSSL (brew/port), same as the PyPI wheel; Windows = SChannel. | ||
| - openssl >=3,<4 # [linux] | ||
|
bewithgaurav marked this conversation as resolved.
|
||
| # libmsodbcsql NEEDs libkrb5 / libgssapi_krb5 on Linux (macOS framework, Windows SSPI). | ||
| - krb5 # [linux] | ||
| # libodbcinst.so.2 NEEDs libltdl.so.7 (not bundled); libtool provides it and the | ||
| # #563 climb makes the driver resolve THIS env's copy. | ||
| - libtool # [linux] | ||
| # msodbcsql18.dll imports VCRUNTIME140.dll but the vendored vcredist ships only | ||
| # msvcp140.dll; declare the serviced conda runtime. | ||
| - vc14_runtime # [win] | ||
| # Re-assert the wheel's platform floor (conda drops the wheel tag); never stricter. | ||
| - __glibc >=2.28 # [linux] | ||
| - __osx >=15.0 # [osx] | ||
|
|
||
| test: | ||
| imports: | ||
| - mssql_python | ||
|
|
||
| about: | ||
| home: https://github.com/microsoft/mssql-python | ||
| # Ships MIT code AND the proprietary ODBC Driver 18 payload, so the license is both. | ||
| license: MIT AND LicenseRef-Microsoft-Proprietary | ||
| license_file: | ||
| - ../../LICENSE | ||
| # The proprietary EULAs live with the ODBC payload; reference them there rather than | ||
| # duplicating. conda-build embeds them in info/licenses/. | ||
| - ../../mssql_python_odbc/licenses/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt | ||
| - ../../mssql_python_odbc/licenses/MICROSOFT_VISUAL_STUDIO_LICENSE.txt | ||
| summary: Microsoft driver for Python to interact with SQL Server and Azure SQL. | ||
| description: | | ||
| mssql-python is a DB API 2.0 (PEP 249) compliant driver for SQL Server, | ||
| Azure SQL, and Azure Synapse. This conda package is self-contained: the | ||
| proprietary Microsoft ODBC Driver 18 payload ships inside it (the same model as | ||
| the v1.11.0 wheel), so no separate driver package is required. | ||
|
|
||
| macOS note: encrypted connections (Encrypt=yes, the default) require system OpenSSL | ||
| -- install it with `brew install openssl` (Homebrew) or via MacPorts. The bundled | ||
| ODBC driver loads OpenSSL from standard system locations, not from the conda env, so | ||
| without it an encrypted connect fails with "OpenSSL library could not be loaded". | ||
| Windows (SChannel) and Linux are fully self-contained. | ||
|
|
||
| extra: | ||
| recipe-maintainers: | ||
| - jahnvithakkar | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.