<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic One PDAL init script for every Databricks Runtime (and no apt) in Community Articles</title>
    <link>https://community.databricks.com/t5/community-articles/one-pdal-init-script-for-every-databricks-runtime-and-no-apt/m-p/175630#M1877</link>
    <description>&lt;P&gt;I needed PDAL on Databricks for a LiDAR processing workload. PDAL is a C++ library with a long native dependency chain (GDAL, PROJ, HDF5, a recent libstdc++), and the Python bindings have to match the interpreter exactly. The usual approach, an &lt;CODE&gt;apt-get install&lt;/CODE&gt; init script, doesn’t hold up here, for several reasons:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Each DBR major version runs on a different Ubuntu release, so package names and versions keep shifting.&lt;/LI&gt;
&lt;LI&gt;Ubuntu’s PDAL is well behind upstream.&lt;/LI&gt;
&lt;LI&gt;The Python bindings it installs are built for the OS Python, not the DBR’s Python.&lt;/LI&gt;
&lt;LI&gt;The clusters had no public internet access at all, so apt couldn’t reach its mirrors anyway.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;On top of that, the workspaces ran a mix of Dedicated and Standard (shared) clusters across several runtime versions. I wanted one script that works everywhere and doesn’t need maintaining per runtime. This is what ended up working. It’s built to run on any runtime from 13.3 up, and I verified it end to end on DBR 17.3 in both Dedicated and Standard access modes.&lt;/P&gt;
&lt;H2 id="stop-using-the-os-package-manager"&gt;Stop using the OS package manager&lt;/H2&gt;
&lt;P&gt;micromamba is a single static binary with no system dependencies. The init script downloads it, then builds a self-contained environment from conda-forge:&lt;/P&gt;
&lt;PRE class="bash"&gt;&lt;CODE&gt;ARCH="$(uname -m)"                        # x86_64 or aarch64
case "$ARCH" in x86_64) MAMBA_ARCH=linux-64 ;; aarch64) MAMBA_ARCH=linux-aarch64 ;; esac

curl -fsSL "https://micro.mamba.pm/api/micromamba/${MAMBA_ARCH}/latest" \
  | tar -xj -C /opt/micromamba bin/micromamba

/opt/micromamba/bin/micromamba create -y -p /opt/pdal -c conda-forge --override-channels \
  "python=${DBR_PY}" pdal python-pdal gdal proj hdf5&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The environment carries its own native libraries, so the Ubuntu version underneath stops mattering, and you get current PDAL.&lt;/P&gt;
&lt;H2 id="match-the-runtimes-python-dont-hard-code-it"&gt;Match the runtime’s Python, don’t hard-code it&lt;/H2&gt;
&lt;P&gt;The key detail is &lt;CODE&gt;python=${DBR_PY}&lt;/CODE&gt;. The script asks the runtime which Python it’s running:&lt;/P&gt;
&lt;PRE class="bash"&gt;&lt;CODE&gt;DBR_PY="$(/databricks/python3/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")')"&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;That’s 3.10 on DBR 13.3 and 3.12 on 17.3. conda-forge then solves for bindings that are ABI-compatible with that interpreter. That one line is why the same script runs on every runtime.&lt;/P&gt;
&lt;H2 id="the-standard-access-mode-surprise"&gt;The Standard access mode surprise&lt;/H2&gt;
&lt;P&gt;On Standard mode you can hit a confusing failure: the init script runs without errors, and then &lt;CODE&gt;import pdal&lt;/CODE&gt; fails in the notebook as if nothing had been installed.&lt;/P&gt;
&lt;P&gt;What’s happening: on Standard (USER_ISOLATION) clusters, notebooks run as a non-root user, and after the init scripts finish, the platform re-locks root-created directories under &lt;CODE&gt;/databricks&lt;/CODE&gt; and &lt;CODE&gt;/tmp&lt;/CODE&gt; to 0700. The files are there, but the notebook user can’t see them, and you can’t chmod your way out of it from the init script because the re-lock happens afterwards.&lt;/P&gt;
&lt;P&gt;&lt;CODE&gt;/opt&lt;/CODE&gt; doesn’t get re-locked, and neither does the runtime’s base &lt;CODE&gt;site-packages&lt;/CODE&gt;. So the environment lives in &lt;CODE&gt;/opt/pdal&lt;/CODE&gt;, the script sets &lt;CODE&gt;umask 022&lt;/CODE&gt;, and it explicitly makes the tree world-readable. That last step is needed because micromamba sometimes creates directories as 0700 regardless of umask.&lt;/P&gt;
&lt;H2 id="getting-python-to-find-it-without-environment-variables"&gt;Getting Python to find it without environment variables&lt;/H2&gt;
&lt;P&gt;On Dedicated clusters, setting &lt;CODE&gt;PYTHONPATH&lt;/CODE&gt; and &lt;CODE&gt;LD_LIBRARY_PATH&lt;/CODE&gt; is enough. On Standard, the notebook runs inside a sandboxed virtual environment, and I couldn’t rely on those variables making it in.&lt;/P&gt;
&lt;P&gt;What does make it in is the runtime’s base &lt;CODE&gt;site-packages&lt;/CODE&gt;, because those sandbox environments inherit it. So the script drops two small files there: a helper module and a one-line &lt;CODE&gt;.pth&lt;/CODE&gt; that imports it. At interpreter startup, the helper preloads the native libraries with ctypes (so no &lt;CODE&gt;LD_LIBRARY_PATH&lt;/CODE&gt; is needed), sets &lt;CODE&gt;PROJ_DATA&lt;/CODE&gt; and &lt;CODE&gt;GDAL_DATA&lt;/CODE&gt;, and appends the conda environment’s site-packages to &lt;CODE&gt;sys.path&lt;/CODE&gt;. It appends it last, so runtime packages keep priority. If the environment isn’t there, the helper does nothing, so a bad node can’t break Python startup for everyone.&lt;/P&gt;
&lt;P&gt;For &lt;CODE&gt;%sh&lt;/CODE&gt;, the web terminal and Spark executors, the script also writes the usual exports to &lt;CODE&gt;/etc/profile.d/&lt;/CODE&gt; and &lt;CODE&gt;spark-env.sh&lt;/CODE&gt;.&lt;/P&gt;
&lt;H2 id="two-small-things-that-saved-support-time-later"&gt;Two small things that saved support time later&lt;/H2&gt;
&lt;P&gt;The script is idempotent: if PDAL is already installed for this Python version, it skips the install.&lt;/P&gt;
&lt;P&gt;conda-forge occasionally ships an optional PDAL plugin (the HDF reader, for example) built against a slightly different library version, and every &lt;CODE&gt;import pdal&lt;/CODE&gt; then prints a “Can’t load library” warning. Users reasonably assume something is broken. The script now runs &lt;CODE&gt;ldd&lt;/CODE&gt; on each plugin and removes any with missing dependencies. The core readers (LAS, LAZ, COPC, E57) aren’t affected.&lt;/P&gt;
&lt;H2 id="running-without-internet-access"&gt;Running without internet access&lt;/H2&gt;
&lt;P&gt;All downloads go through two variables:&lt;/P&gt;
&lt;PRE class="bash"&gt;&lt;CODE&gt;CONDA_CHANNEL="conda-forge"                                  # or your internal conda remote
MICROMAMBA_BASE_URL="https://micro.mamba.pm/api/micromamba"  # or an internal copy&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;In my case both point at an internal Artifactory: a conda remote that proxies conda-forge, plus a copy of the micromamba binary. The token comes from a Databricks secret. Nothing else in the script changed.&lt;/P&gt;
&lt;P&gt;For clusters where you can’t add an init script, I also wrote a small &lt;CODE&gt;%run&lt;/CODE&gt; bootstrap notebook. It uses the init-script environment if it exists, and otherwise installs the same environment into node-local scratch space as the notebook user.&lt;/P&gt;
&lt;H2 id="installing-it"&gt;Installing it&lt;/H2&gt;
&lt;P&gt;Put the script in a Unity Catalog volume or workspace file, add it as a cluster-scoped init script, have an admin allowlist it for Standard mode, and do a full restart. Then check it:&lt;/P&gt;
&lt;PRE class="python"&gt;&lt;CODE&gt;%sh pdal --version
import pdal; print(pdal.info.version)&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The same approach (conda-forge, the runtime’s Python version, &lt;CODE&gt;/opt&lt;/CODE&gt;, a &lt;CODE&gt;.pth&lt;/CODE&gt; file) has since worked for other native tools I needed on these clusters, including azcopy (see &lt;A href="https://community.databricks.com/t5/community-articles/backing-up-unity-catalog-volumes-without-melting-the-driver/m-p/175629" target="_blank"&gt;backing up Unity Catalog volumes&lt;/A&gt;). If you’ve been maintaining a different apt script for each runtime version, give it a try.&lt;/P&gt;</description>
    <pubDate>Wed, 04 Nov 2026 19:00:08 GMT</pubDate>
    <dc:creator>DanielMcauley</dc:creator>
    <dc:date>2026-11-04T19:00:08Z</dc:date>
    <item>
      <title>One PDAL init script for every Databricks Runtime (and no apt)</title>
      <link>https://community.databricks.com/t5/community-articles/one-pdal-init-script-for-every-databricks-runtime-and-no-apt/m-p/175630#M1877</link>
      <description>&lt;P&gt;I needed PDAL on Databricks for a LiDAR processing workload. PDAL is a C++ library with a long native dependency chain (GDAL, PROJ, HDF5, a recent libstdc++), and the Python bindings have to match the interpreter exactly. The usual approach, an &lt;CODE&gt;apt-get install&lt;/CODE&gt; init script, doesn’t hold up here, for several reasons:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Each DBR major version runs on a different Ubuntu release, so package names and versions keep shifting.&lt;/LI&gt;
&lt;LI&gt;Ubuntu’s PDAL is well behind upstream.&lt;/LI&gt;
&lt;LI&gt;The Python bindings it installs are built for the OS Python, not the DBR’s Python.&lt;/LI&gt;
&lt;LI&gt;The clusters had no public internet access at all, so apt couldn’t reach its mirrors anyway.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;On top of that, the workspaces ran a mix of Dedicated and Standard (shared) clusters across several runtime versions. I wanted one script that works everywhere and doesn’t need maintaining per runtime. This is what ended up working. It’s built to run on any runtime from 13.3 up, and I verified it end to end on DBR 17.3 in both Dedicated and Standard access modes.&lt;/P&gt;
&lt;H2 id="stop-using-the-os-package-manager"&gt;Stop using the OS package manager&lt;/H2&gt;
&lt;P&gt;micromamba is a single static binary with no system dependencies. The init script downloads it, then builds a self-contained environment from conda-forge:&lt;/P&gt;
&lt;PRE class="bash"&gt;&lt;CODE&gt;ARCH="$(uname -m)"                        # x86_64 or aarch64
case "$ARCH" in x86_64) MAMBA_ARCH=linux-64 ;; aarch64) MAMBA_ARCH=linux-aarch64 ;; esac

curl -fsSL "https://micro.mamba.pm/api/micromamba/${MAMBA_ARCH}/latest" \
  | tar -xj -C /opt/micromamba bin/micromamba

/opt/micromamba/bin/micromamba create -y -p /opt/pdal -c conda-forge --override-channels \
  "python=${DBR_PY}" pdal python-pdal gdal proj hdf5&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The environment carries its own native libraries, so the Ubuntu version underneath stops mattering, and you get current PDAL.&lt;/P&gt;
&lt;H2 id="match-the-runtimes-python-dont-hard-code-it"&gt;Match the runtime’s Python, don’t hard-code it&lt;/H2&gt;
&lt;P&gt;The key detail is &lt;CODE&gt;python=${DBR_PY}&lt;/CODE&gt;. The script asks the runtime which Python it’s running:&lt;/P&gt;
&lt;PRE class="bash"&gt;&lt;CODE&gt;DBR_PY="$(/databricks/python3/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")')"&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;That’s 3.10 on DBR 13.3 and 3.12 on 17.3. conda-forge then solves for bindings that are ABI-compatible with that interpreter. That one line is why the same script runs on every runtime.&lt;/P&gt;
&lt;H2 id="the-standard-access-mode-surprise"&gt;The Standard access mode surprise&lt;/H2&gt;
&lt;P&gt;On Standard mode you can hit a confusing failure: the init script runs without errors, and then &lt;CODE&gt;import pdal&lt;/CODE&gt; fails in the notebook as if nothing had been installed.&lt;/P&gt;
&lt;P&gt;What’s happening: on Standard (USER_ISOLATION) clusters, notebooks run as a non-root user, and after the init scripts finish, the platform re-locks root-created directories under &lt;CODE&gt;/databricks&lt;/CODE&gt; and &lt;CODE&gt;/tmp&lt;/CODE&gt; to 0700. The files are there, but the notebook user can’t see them, and you can’t chmod your way out of it from the init script because the re-lock happens afterwards.&lt;/P&gt;
&lt;P&gt;&lt;CODE&gt;/opt&lt;/CODE&gt; doesn’t get re-locked, and neither does the runtime’s base &lt;CODE&gt;site-packages&lt;/CODE&gt;. So the environment lives in &lt;CODE&gt;/opt/pdal&lt;/CODE&gt;, the script sets &lt;CODE&gt;umask 022&lt;/CODE&gt;, and it explicitly makes the tree world-readable. That last step is needed because micromamba sometimes creates directories as 0700 regardless of umask.&lt;/P&gt;
&lt;H2 id="getting-python-to-find-it-without-environment-variables"&gt;Getting Python to find it without environment variables&lt;/H2&gt;
&lt;P&gt;On Dedicated clusters, setting &lt;CODE&gt;PYTHONPATH&lt;/CODE&gt; and &lt;CODE&gt;LD_LIBRARY_PATH&lt;/CODE&gt; is enough. On Standard, the notebook runs inside a sandboxed virtual environment, and I couldn’t rely on those variables making it in.&lt;/P&gt;
&lt;P&gt;What does make it in is the runtime’s base &lt;CODE&gt;site-packages&lt;/CODE&gt;, because those sandbox environments inherit it. So the script drops two small files there: a helper module and a one-line &lt;CODE&gt;.pth&lt;/CODE&gt; that imports it. At interpreter startup, the helper preloads the native libraries with ctypes (so no &lt;CODE&gt;LD_LIBRARY_PATH&lt;/CODE&gt; is needed), sets &lt;CODE&gt;PROJ_DATA&lt;/CODE&gt; and &lt;CODE&gt;GDAL_DATA&lt;/CODE&gt;, and appends the conda environment’s site-packages to &lt;CODE&gt;sys.path&lt;/CODE&gt;. It appends it last, so runtime packages keep priority. If the environment isn’t there, the helper does nothing, so a bad node can’t break Python startup for everyone.&lt;/P&gt;
&lt;P&gt;For &lt;CODE&gt;%sh&lt;/CODE&gt;, the web terminal and Spark executors, the script also writes the usual exports to &lt;CODE&gt;/etc/profile.d/&lt;/CODE&gt; and &lt;CODE&gt;spark-env.sh&lt;/CODE&gt;.&lt;/P&gt;
&lt;H2 id="two-small-things-that-saved-support-time-later"&gt;Two small things that saved support time later&lt;/H2&gt;
&lt;P&gt;The script is idempotent: if PDAL is already installed for this Python version, it skips the install.&lt;/P&gt;
&lt;P&gt;conda-forge occasionally ships an optional PDAL plugin (the HDF reader, for example) built against a slightly different library version, and every &lt;CODE&gt;import pdal&lt;/CODE&gt; then prints a “Can’t load library” warning. Users reasonably assume something is broken. The script now runs &lt;CODE&gt;ldd&lt;/CODE&gt; on each plugin and removes any with missing dependencies. The core readers (LAS, LAZ, COPC, E57) aren’t affected.&lt;/P&gt;
&lt;H2 id="running-without-internet-access"&gt;Running without internet access&lt;/H2&gt;
&lt;P&gt;All downloads go through two variables:&lt;/P&gt;
&lt;PRE class="bash"&gt;&lt;CODE&gt;CONDA_CHANNEL="conda-forge"                                  # or your internal conda remote
MICROMAMBA_BASE_URL="https://micro.mamba.pm/api/micromamba"  # or an internal copy&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;In my case both point at an internal Artifactory: a conda remote that proxies conda-forge, plus a copy of the micromamba binary. The token comes from a Databricks secret. Nothing else in the script changed.&lt;/P&gt;
&lt;P&gt;For clusters where you can’t add an init script, I also wrote a small &lt;CODE&gt;%run&lt;/CODE&gt; bootstrap notebook. It uses the init-script environment if it exists, and otherwise installs the same environment into node-local scratch space as the notebook user.&lt;/P&gt;
&lt;H2 id="installing-it"&gt;Installing it&lt;/H2&gt;
&lt;P&gt;Put the script in a Unity Catalog volume or workspace file, add it as a cluster-scoped init script, have an admin allowlist it for Standard mode, and do a full restart. Then check it:&lt;/P&gt;
&lt;PRE class="python"&gt;&lt;CODE&gt;%sh pdal --version
import pdal; print(pdal.info.version)&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The same approach (conda-forge, the runtime’s Python version, &lt;CODE&gt;/opt&lt;/CODE&gt;, a &lt;CODE&gt;.pth&lt;/CODE&gt; file) has since worked for other native tools I needed on these clusters, including azcopy (see &lt;A href="https://community.databricks.com/t5/community-articles/backing-up-unity-catalog-volumes-without-melting-the-driver/m-p/175629" target="_blank"&gt;backing up Unity Catalog volumes&lt;/A&gt;). If you’ve been maintaining a different apt script for each runtime version, give it a try.&lt;/P&gt;</description>
      <pubDate>Wed, 04 Nov 2026 19:00:08 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/one-pdal-init-script-for-every-databricks-runtime-and-no-apt/m-p/175630#M1877</guid>
      <dc:creator>DanielMcauley</dc:creator>
      <dc:date>2026-11-04T19:00:08Z</dc:date>
    </item>
  </channel>
</rss>

