diff --git a/recipes/recipes_emscripten/duckdb/build.sh b/recipes/recipes_emscripten/duckdb/build.sh new file mode 100644 index 0000000000..7c1c118219 --- /dev/null +++ b/recipes/recipes_emscripten/duckdb/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +# Setup emscripten toolchain for scikit-build-core +emscripten_root=$(em-config EMSCRIPTEN_ROOT) +toolchain_path="${emscripten_root}/cmake/Modules/Platform/Emscripten.cmake" + +export CMAKE_ARGS="${CMAKE_ARGS} -DCMAKE_TOOLCHAIN_FILE=${toolchain_path} -DCMAKE_PROJECT_INCLUDE=${RECIPE_DIR}/overwriteProp.cmake" + +# DuckDB-specific CMake flags for wasm passed via scikit-build-core config settings +${PYTHON} -m pip install . ${PIP_ARGS} \ + -Ccmake.define.OVERRIDE_GIT_DESCRIBE="v${PKG_VERSION}" \ + -Ccmake.define.BUILD_EXTENSIONS="parquet;json;autocomplete" \ + -Ccmake.define.BUILD_SHELL=OFF \ + -Ccmake.define.BUILD_UNITTESTS=OFF \ + -Ccmake.define.ENABLE_EXTENSION_AUTOLOADING=OFF \ + -Ccmake.define.ENABLE_EXTENSION_AUTOINSTALL=OFF \ + -Ccmake.define.CMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF diff --git a/recipes/recipes_emscripten/duckdb/overwriteProp.cmake b/recipes/recipes_emscripten/duckdb/overwriteProp.cmake new file mode 100644 index 0000000000..58bad2809d --- /dev/null +++ b/recipes/recipes_emscripten/duckdb/overwriteProp.cmake @@ -0,0 +1,9 @@ +set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS TRUE) +set(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS "-sSIDE_MODULE=1 -sWASM_BIGINT") +set(CMAKE_SHARED_LIBRARY_CREATE_CXX_FLAGS "-sSIDE_MODULE=1 -sWASM_BIGINT") +set(CMAKE_STRIP FALSE) + +# DuckDB links libduckdb_static.a twice to resolve circular dependencies +# between the core and extensions. Native linkers handle this but wasm-ld +# does not. Allow multiple definitions to work around this. +add_link_options("LINKER:--allow-multiple-definition") diff --git a/recipes/recipes_emscripten/duckdb/patches/emscripten-python-target.patch b/recipes/recipes_emscripten/duckdb/patches/emscripten-python-target.patch new file mode 100644 index 0000000000..803364e599 --- /dev/null +++ b/recipes/recipes_emscripten/duckdb/patches/emscripten-python-target.patch @@ -0,0 +1,29 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index abcdef1..1234567 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -36,6 +36,15 @@ endif() + # Dependencies + # ──────────────────────────────────────────── + # PyBind11 ++ ++# On Emscripten, FindPython only provides Python::Module, not Python::Python ++# (there is no shared libpython to link against). pybind11_add_module internally ++# calls python_add_library which expects Python::Python, so we provide a dummy ++# IMPORTED target to satisfy that requirement. ++if(EMSCRIPTEN AND NOT TARGET Python::Python) ++ add_library(Python::Python INTERFACE IMPORTED) ++endif() ++ + find_package(pybind11 REQUIRED CONFIG) + + # DuckDB +@@ -100,7 +109,7 @@ if(APPLE) + target_link_options( + _duckdb PRIVATE "LINKER:-exported_symbol,_duckdb_adbc_init" + "LINKER:-exported_symbol,_PyInit__duckdb") +-elseif(UNIX AND NOT APPLE) ++elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) + target_link_options( + _duckdb PRIVATE "LINKER:--export-dynamic-symbol=duckdb_adbc_init" + "LINKER:--export-dynamic-symbol=PyInit__duckdb") diff --git a/recipes/recipes_emscripten/duckdb/patches/fix-dirty-version-scheme.patch b/recipes/recipes_emscripten/duckdb/patches/fix-dirty-version-scheme.patch new file mode 100644 index 0000000000..8c7cea2525 --- /dev/null +++ b/recipes/recipes_emscripten/duckdb/patches/fix-dirty-version-scheme.patch @@ -0,0 +1,13 @@ +diff --git a/duckdb_packaging/setuptools_scm_version.py b/duckdb_packaging/setuptools_scm_version.py +index 1234567..abcdef0 100644 +--- a/duckdb_packaging/setuptools_scm_version.py ++++ b/duckdb_packaging/setuptools_scm_version.py +@@ -53,7 +53,7 @@ def version_scheme(version: _VersionObject) -> str: + + distance = int(version.distance or 0) + try: +- if distance == 0 and not version.dirty: ++ if distance == 0: + return _tag_to_version(str(version.tag)) + return _bump_dev_version(str(version.tag), distance) + except Exception as e: diff --git a/recipes/recipes_emscripten/duckdb/recipe.yaml b/recipes/recipes_emscripten/duckdb/recipe.yaml new file mode 100644 index 0000000000..d41263d7ba --- /dev/null +++ b/recipes/recipes_emscripten/duckdb/recipe.yaml @@ -0,0 +1,72 @@ +context: + version: 1.4.4 + +package: + name: python-duckdb + version: ${{ version }} + +source: + git: https://github.com/duckdb/duckdb-python.git + tag: v${{ version }} + # expected_commit: a12f36ca411007f5eb48919448f61c7498112553 + patches: + - patches/emscripten-python-target.patch + - patches/fix-dirty-version-scheme.patch + +build: + number: 0 + script: build.sh + + files: + exclude: + - '**/__pycache__/**' + - '**/*.pyc' + - '**/test_*.py' + - 'duckdb_build/**' + python: + skip_pyc_compilation: + - '**/*.py' + +requirements: + build: + - python + - cross-python_${{ target_platform }} + - ${{ compiler('cxx') }} + - pip + - setuptools + - setuptools-scm + - pybind11 + - scikit-build-core + - cmake + - ninja + host: + - python + - pybind11 + run: + - python + +tests: +- script: pytester + files: + recipe: + - test_duckdb.py + requirements: + build: + - pytester + run: + - pytester-run + +about: + homepage: https://duckdb.org/ + license: MIT + license_file: LICENSE + summary: DuckDB is an analytical in-process SQL database management system + description: | + DuckDB is an in-process SQL OLAP database management system. + It is designed to support analytical query workloads (OLAP) while + being embeddable in Python and other languages. + repository: https://github.com/duckdb/duckdb-python + +extra: + recipe-maintainers: + - wolfv diff --git a/recipes/recipes_emscripten/duckdb/test_duckdb.py b/recipes/recipes_emscripten/duckdb/test_duckdb.py new file mode 100644 index 0000000000..cc0bf738ec --- /dev/null +++ b/recipes/recipes_emscripten/duckdb/test_duckdb.py @@ -0,0 +1,26 @@ +import duckdb + + +def test_import(): + assert duckdb is not None + + +def test_basic_query(): + con = duckdb.connect() + result = con.execute("SELECT 42 AS answer").fetchall() + assert result == [(42,)] + + +def test_create_table(): + con = duckdb.connect() + con.execute("CREATE TABLE t (x INTEGER, y VARCHAR)") + con.execute("INSERT INTO t VALUES (1, 'hello'), (2, 'world')") + result = con.execute("SELECT * FROM t ORDER BY x").fetchall() + assert result == [(1, "hello"), (2, "world")] + + +def test_aggregation(): + con = duckdb.connect() + con.execute("CREATE TABLE nums AS SELECT * FROM range(10) t(i)") + result = con.execute("SELECT SUM(i), COUNT(*) FROM nums").fetchone() + assert result == (45, 10)