Skip to content

Commit 1184c51

Browse files
SNOW-2465273: Add core library import (#2657)
1 parent f23a74a commit 1184c51

File tree

26 files changed

+1198
-13
lines changed

26 files changed

+1198
-13
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1111
- Added support for async I/O. Asynchronous version of connector is available via `snowflake.connector.aio` module.
1212
- Added `SnowflakeCursor.stats` property to expose granular DML statistics (rows inserted, deleted, updated, and duplicates) for operations like CTAS where `rowcount` is insufficient.
1313
- Added support for injecting SPCS service identifier token (`SPCS_TOKEN`) into login requests when present in SPCS containers.
14-
14+
- Introduced shared library for extended telemetry to identify and prepare testing platform for native rust extensions.
1515
- v4.1.1(TBD)
1616
- Relaxed pandas dependency requirements for Python below 3.12.
1717
- Changed CRL cache cleanup background task to daemon to avoid blocking main thread.

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ exclude src/snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_arrow_iter
1313
exclude src/snowflake/connector/nanoarrow_cpp/scripts/.clang-format
1414
exclude src/snowflake/connector/nanoarrow_cpp/scripts/format.sh
1515

16+
include src/snowflake/connector/minicore __init__.py
17+
recursive-include src/snowflake/connector/minicore *.so *.dll *.dylib *.a *.h *.lib
18+
1619
exclude .git-blame-ignore-revs
1720
exclude .pre-commit-config.yaml
1821
exclude license_header.txt

ci/build_linux.sh

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# NOTES:
55
# - This is designed to ONLY be called in our build docker image
66
# - To compile only a specific version(s) pass in versions like: `./build_linux.sh "3.9 3.10"`
7-
set -o pipefail
7+
set -ox pipefail
88

99
U_WIDTH=16
1010
PYTHON_VERSIONS="${1:-3.9 3.10 3.11 3.12 3.13}"
@@ -21,6 +21,39 @@ if [ -d "${DIST_DIR}" ]; then
2121
fi
2222
mkdir -p ${REPAIRED_DIR}
2323

24+
# Clean up unnecessary minicore directories for the current platform
25+
# This ensures only relevant binary files are included in the wheel
26+
MINICORE_DIR="${CONNECTOR_DIR}/src/snowflake/connector/minicore"
27+
arch=$(uname -m)
28+
29+
# Determine libc type (glibc or musl)
30+
if ldd --version 2>&1 | grep -qi musl; then
31+
libc_type="musl"
32+
else
33+
libc_type="glibc"
34+
fi
35+
36+
# Determine which directory to keep based on architecture and libc
37+
if [[ $arch == "x86_64" ]]; then
38+
keep_dir="linux_x86_64_${libc_type}"
39+
elif [[ $arch == "aarch64" ]]; then
40+
keep_dir="linux_aarch64_${libc_type}"
41+
else
42+
echo "[WARN] Unknown architecture: $arch, not cleaning minicore directories"
43+
keep_dir=""
44+
fi
45+
46+
if [[ -n "$keep_dir" && -d "${MINICORE_DIR}" ]]; then
47+
echo "[Info] Cleaning minicore directories, keeping only ${keep_dir}"
48+
for dir in "${MINICORE_DIR}"/*/; do
49+
dir_name=$(basename "$dir")
50+
if [[ "$dir_name" != "$keep_dir" && "$dir_name" != "__pycache__" ]]; then
51+
echo "[Info] Removing minicore/${dir_name}"
52+
rm -rf "$dir"
53+
fi
54+
done
55+
fi
56+
2457
# Necessary for cpython_path
2558
source /home/user/multibuild/manylinux_utils.sh
2659

@@ -39,6 +72,7 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
3972
${PYTHON} -m build --outdir ${BUILD_DIR} .
4073
# On Linux we should repair wheel(s) generated
4174
arch=$(uname -p)
75+
auditwheel show ${BUILD_DIR}/*.whl
4276
if [[ $arch == x86_64 ]]; then
4377
auditwheel repair --plat manylinux2014_x86_64 ${BUILD_DIR}/*.whl -w ${REPAIRED_DIR}
4478
else

ci/download_minicore.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Download minicore binary for the current platform.
4+
Designed to be used by cibuildwheel during wheel building.
5+
6+
Usage:
7+
python scripts/download_minicore.py [VERSION]
8+
9+
Environment variables:
10+
MINICORE_VERSION - Version to download (default: 0.0.1)
11+
MINICORE_OUTPUT_DIR - Output directory (default: src/snowflake/connector/minicore)
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import os
17+
import platform
18+
import sys
19+
import tarfile
20+
import tempfile
21+
from pathlib import Path
22+
from urllib.error import HTTPError, URLError
23+
from urllib.request import Request, urlopen
24+
25+
# Configuration
26+
BASE_URL = "https://sfc-repo.snowflakecomputing.com/minicore"
27+
DEFAULT_VERSION = "0.0.1"
28+
29+
# Target directory for minicore module (relative to repo root)
30+
MINICORE_MODULE_PATH = Path("src/snowflake/connector/minicore")
31+
32+
33+
def get_repo_root() -> Path:
34+
"""Get the repository root directory."""
35+
current = Path(__file__).resolve().parent
36+
while current != current.parent:
37+
if (current / "pyproject.toml").exists() or (current / "setup.py").exists():
38+
return current
39+
current = current.parent
40+
return Path(__file__).resolve().parent.parent
41+
42+
43+
def detect_os() -> str:
44+
"""Detect the operating system."""
45+
system = platform.system().lower()
46+
if system == "linux":
47+
return "linux"
48+
elif system == "darwin":
49+
return "macos"
50+
elif system == "windows":
51+
return "windows"
52+
elif system == "aix":
53+
return "aix"
54+
else:
55+
return "unknown"
56+
57+
58+
def detect_arch() -> str:
59+
"""Detect the CPU architecture."""
60+
machine = platform.machine().lower()
61+
if machine in ("x86_64", "amd64"):
62+
return "x86_64"
63+
elif machine in ("aarch64", "arm64"):
64+
return "aarch64"
65+
elif machine in ("i686", "i386", "x86"):
66+
return "i686"
67+
elif machine == "ppc64":
68+
return "ppc64"
69+
else:
70+
return "unknown"
71+
72+
73+
def detect_libc() -> str:
74+
"""Detect libc type on Linux (glibc vs musl)."""
75+
if detect_os() != "linux":
76+
return ""
77+
78+
# Check if we're on Alpine/musl
79+
if Path("/etc/alpine-release").exists():
80+
return "musl"
81+
82+
# Check for musl by looking at the libc library
83+
try:
84+
import subprocess
85+
86+
result = subprocess.run(
87+
["ldd", "--version"],
88+
capture_output=True,
89+
text=True,
90+
)
91+
if "musl" in result.stdout.lower() or "musl" in result.stderr.lower():
92+
return "musl"
93+
except Exception:
94+
pass
95+
96+
# Default to glibc
97+
return "glibc"
98+
99+
100+
def get_platform_dir(os_name: str, arch: str) -> str:
101+
"""Build platform directory name for URL."""
102+
if os_name == "linux":
103+
return f"linux_{arch}"
104+
elif os_name == "macos":
105+
return f"mac_{arch}"
106+
elif os_name == "windows":
107+
return f"windows_{arch}"
108+
elif os_name == "aix":
109+
return f"aix_{arch}"
110+
else:
111+
return ""
112+
113+
114+
def get_filename_arch(os_name: str, arch: str, libc: str) -> str:
115+
"""Build filename architecture component."""
116+
if os_name == "linux":
117+
return f"linux-{arch}-{libc}"
118+
elif os_name == "macos":
119+
return f"macos-{arch}"
120+
elif os_name == "windows":
121+
return f"windows-{arch}"
122+
elif os_name == "aix":
123+
return f"aix-{arch}"
124+
else:
125+
return ""
126+
127+
128+
def build_download_url(platform_dir: str, filename_arch: str, version: str) -> str:
129+
"""Build the download URL."""
130+
filename = f"sf_mini_core_{filename_arch}_{version}.tar.gz"
131+
return f"{BASE_URL}/{platform_dir}/{version}/{filename}"
132+
133+
134+
def download_file(url: str, dest_path: Path) -> None:
135+
"""Download a file from URL to destination path."""
136+
print(f"Downloading: {url}")
137+
request = Request(url, headers={"User-Agent": "Python/minicore-downloader"})
138+
try:
139+
with urlopen(request, timeout=60) as response:
140+
content = response.read()
141+
dest_path.write_bytes(content)
142+
file_size_mb = len(content) / (1024 * 1024)
143+
print(f"Downloaded {file_size_mb:.2f} MB")
144+
except HTTPError as e:
145+
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
146+
raise
147+
except URLError as e:
148+
print(f"URL Error: {e.reason}", file=sys.stderr)
149+
raise
150+
151+
152+
def extract_tar_gz(tar_path: Path, extract_to: Path) -> None:
153+
"""Extract a tar.gz file to the specified directory."""
154+
print(f"Extracting to: {extract_to}")
155+
extract_to.mkdir(parents=True, exist_ok=True)
156+
157+
with tarfile.open(tar_path, "r:gz") as tar:
158+
# Security check: prevent path traversal attacks
159+
for member in tar.getmembers():
160+
member_path = extract_to / member.name
161+
try:
162+
member_path.resolve().relative_to(extract_to.resolve())
163+
except ValueError:
164+
print(
165+
f"Skipping potentially unsafe path: {member.name}", file=sys.stderr
166+
)
167+
continue
168+
169+
# The 'filter' parameter was added in Python 3.12
170+
if sys.version_info >= (3, 12):
171+
tar.extractall(path=extract_to, filter="data")
172+
else:
173+
tar.extractall(path=extract_to)
174+
175+
176+
def main() -> int:
177+
# Get version from environment or command line
178+
version = os.environ.get("MINICORE_VERSION")
179+
if not version and len(sys.argv) > 1:
180+
version = sys.argv[1]
181+
if not version:
182+
version = DEFAULT_VERSION
183+
184+
# Get output directory
185+
output_dir_env = os.environ.get("MINICORE_OUTPUT_DIR")
186+
if output_dir_env:
187+
output_dir = Path(output_dir_env)
188+
else:
189+
repo_root = get_repo_root()
190+
output_dir = repo_root / MINICORE_MODULE_PATH
191+
192+
# Detect platform
193+
os_name = detect_os()
194+
arch = detect_arch()
195+
libc = detect_libc()
196+
197+
print(f"Detected OS: {os_name}")
198+
print(f"Detected architecture: {arch}")
199+
if libc:
200+
print(f"Detected libc: {libc}")
201+
202+
if os_name == "unknown" or arch == "unknown":
203+
print(
204+
f"Error: Unsupported platform: OS={os_name}, ARCH={arch}", file=sys.stderr
205+
)
206+
return 1
207+
208+
# Build URL components
209+
platform_dir = get_platform_dir(os_name, arch)
210+
filename_arch = get_filename_arch(os_name, arch, libc)
211+
212+
if not platform_dir or not filename_arch:
213+
print(
214+
"Error: Could not determine platform/architecture mapping", file=sys.stderr
215+
)
216+
return 1
217+
218+
url = build_download_url(platform_dir, filename_arch, version)
219+
220+
print(f"Version: {version}")
221+
print(f"Download URL: {url}")
222+
print(f"Output directory: {output_dir}")
223+
224+
# Download to temp file and extract
225+
with tempfile.TemporaryDirectory() as temp_dir:
226+
temp_path = Path(temp_dir) / f"sf_mini_core_{filename_arch}_{version}.tar.gz"
227+
228+
try:
229+
download_file(url, temp_path)
230+
extract_tar_gz(temp_path, output_dir)
231+
except Exception as e:
232+
print(f"Error: {e}", file=sys.stderr)
233+
return 1
234+
235+
print("Done!")
236+
237+
# List extracted files
238+
for item in sorted(output_dir.iterdir()):
239+
if not item.name.startswith("__"):
240+
print(f" {item.name}")
241+
242+
return 0
243+
244+
245+
if __name__ == "__main__":
246+
sys.exit(main())

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ build-verbosity = 1
1515

1616
[tool.cibuildwheel.linux]
1717
archs = ["x86_64", "aarch64"]
18+
# Exclude pre-built minicore libraries from auditwheel repair
19+
repair-wheel-command = ""
1820

1921
[tool.cibuildwheel.macos]
2022
archs = ["x86_64", "arm64"]
@@ -23,3 +25,6 @@ repair-wheel-command = ""
2325

2426
[tool.cibuildwheel.windows]
2527
archs = ["AMD64"]
28+
29+
[tool.check-manifest]
30+
ignore-bad-ideas = ["src/snowflake/connector/minicore/**"]

setup.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ where = src
7272
exclude = snowflake.connector.cpp*
7373
include = snowflake.*
7474

75+
[options.package_data]
76+
snowflake.connector.minicore =
77+
*.so
78+
*.dll
79+
*.dylib
80+
*.a
81+
7582
[options.entry_points]
7683
console_scripts =
7784
snowflake-dump-ocsp-response = snowflake.connector.tool.dump_ocsp_response:main

src/snowflake/connector/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from functools import wraps
77

8+
from ._utils import _core_loader
9+
810
apilevel = "2.0"
911
threadsafety = 2
1012
paramstyle = "pyformat"
@@ -45,6 +47,14 @@
4547
from .log_configuration import EasyLoggingConfigPython
4648
from .version import VERSION
4749

50+
# Load the core library - failures are captured in core_loader and don't prevent module loading
51+
try:
52+
_core_loader.load()
53+
except Exception:
54+
# Silently continue if core loading fails - the error is already captured in core_loader
55+
# This ensures the connector module loads even if the minicore library is unavailable
56+
pass
57+
4858
logging.getLogger(__name__).addHandler(NullHandler())
4959
setup_external_libraries()
5060

0 commit comments

Comments
 (0)