Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
bb2fc31
docs(spec): add the GDAL 3.13 native runtime spec and ADR-0007
jQuinRivero Oct 2, 2026
8bfbe66
feat(gdal-runtime): pin and verify the native sources
jQuinRivero Oct 2, 2026
18dc24f
feat(gdal-runtime): add the runtime configuration and self-check package
jQuinRivero Oct 2, 2026
dcd934d
docs(spec): record GDAL runtime status and verified build facts
jQuinRivero Oct 2, 2026
fb6c4c1
test: cover GDAL runtime lock parsing, configure and policy evaluation
jQuinRivero Oct 15, 2026
1323862
build: add GDAL native runtime CI pipeline
jQuinRivero Oct 15, 2026
467bc82
feat(gdal-runtime): add baseline report utility
jQuinRivero Oct 15, 2026
7deedc9
test(gdal-runtime): smoke test GDAL binding offline
jQuinRivero Oct 15, 2026
cd3e5c8
docs(gdal-runtime): update smoke test status
jQuinRivero Oct 15, 2026
dd38fa7
fix(gdal-runtime): generate gdal-config for binding build
jQuinRivero Oct 15, 2026
194d47a
fix(gdal-runtime): expose config tool to isolated build
jQuinRivero Oct 15, 2026
098641f
fix(gdal-runtime): fetch binding sdists after native build
jQuinRivero Oct 15, 2026
2a2ad34
fix(gdal-runtime): enforce curl header policy at runtime
jQuinRivero Oct 15, 2026
c7cbb9c
fix(gdal-runtime): disable native Python binding target
jQuinRivero Oct 15, 2026
0014882
fix(gdal-runtime): align OpenSSL library install path
jQuinRivero Oct 15, 2026
c98325c
fix(gdal-runtime): disable unused GNM drivers
jQuinRivero Oct 15, 2026
cecf510
fix(gdal-runtime): retain GNM for pinned GDAL bindings
jQuinRivero Oct 16, 2026
98231ad
fix(gdal-runtime): skip isolated sdist dependency fetches
jQuinRivero Oct 16, 2026
a0dd925
fix(gdal-runtime): package runtime in importable wheel
jQuinRivero Oct 16, 2026
1d9be6a
docs(spec): record GDAL runtime wheel validation
jQuinRivero Oct 16, 2026
a7cf785
test(gdal-runtime): avoid internal feed hostname in tests
jQuinRivero Oct 16, 2026
133d0f2
docs(spec): record upstream GDAL CI blocker
jQuinRivero Oct 16, 2026
c73be73
fix(gdal-runtime): default missing package index to PyPI
jQuinRivero Oct 16, 2026
c956992
test(gdal-runtime): smoke all bindings and raster workflows
jQuinRivero Oct 16, 2026
4162e9c
fix(gdal-runtime): install each binding smoke version
jQuinRivero Oct 16, 2026
d7f3c48
fix(gdal-runtime): load installed runtime during workflow smoke
jQuinRivero Oct 16, 2026
9a96d8a
fix(gdal-runtime): pass WKB bytes to Pyogrio
jQuinRivero Oct 16, 2026
754cb3b
fix(gdal-runtime): trust test CA in HTTPS smoke
jQuinRivero Oct 16, 2026
509cef6
fix(gdal-runtime): use GDAL curl CA bundle option
jQuinRivero Oct 16, 2026
8a7bb31
fix(gdal-runtime): fetch smoke dependencies from public PyPI
jQuinRivero Oct 16, 2026
c6164f5
docs(spec): record all-binding runtime smoke success
jQuinRivero Oct 16, 2026
61e9ad9
docs(spec): record expanded GDAL smoke success
jQuinRivero Oct 16, 2026
8cd0d03
test(gdal-runtime): validate function and imagery base images
jQuinRivero Oct 16, 2026
c7858e5
feat(gdal-runtime): add supply-chain verification pipeline
jQuinRivero Oct 16, 2026
934256d
test(gdal-runtime): smoke training CUDA base
jQuinRivero Oct 16, 2026
40597c8
fix(gdal-runtime): strengthen license metadata and scanning
jQuinRivero Oct 19, 2026
b058f80
fix(gdal-runtime): require the bundled PROJ library
jQuinRivero Oct 19, 2026
62bcc29
fix(gdal-runtime): verify shared runtime during hardening
jQuinRivero Oct 19, 2026
f671a8f
fix(gdal): validate imagery and upload input formats
jQuinRivero Oct 19, 2026
2117774
feat(titiler): verify GDAL runtime at startup
jQuinRivero Oct 19, 2026
7541b4c
fix(gdal-runtime): activate checks on hastegeo import
jQuinRivero Oct 19, 2026
9bdd556
docs(spec): update GDAL runtime integration status
jQuinRivero Oct 19, 2026
c950f2a
feat(gdal-runtime): add gated immutable publisher
jQuinRivero Oct 19, 2026
72a582c
fix(titiler): disable VRT and test reprojection
jQuinRivero Oct 19, 2026
af49834
fix(gdal-runtime): require complete driver inventory
jQuinRivero Oct 19, 2026
7768f4a
test(gdal-runtime): gate publishing on bundle checks
jQuinRivero Oct 19, 2026
6f9f757
ci(gdal-runtime): test consumer and publisher changes
jQuinRivero Oct 19, 2026
b602e68
fix(gdal-runtime): validate licensed SBOM bundles
jQuinRivero Oct 19, 2026
d8d5a64
fix(gdal): apply hardening across deployed runtimes
jQuinRivero Oct 19, 2026
a4cef8c
test(titiler): enforce VRT driver policy
jQuinRivero Oct 19, 2026
fe66769
fix(ci): allow native GDAL settings in drift check
jQuinRivero Oct 19, 2026
cd46b54
fix(ci): skip policy metadata in GDAL guard
jQuinRivero Oct 19, 2026
3b5c913
fix(gdal-runtime): keep dependency pin in wheel headers
jQuinRivero Oct 19, 2026
a532bd3
fix(gdal-runtime): verify binding package versions
jQuinRivero Oct 19, 2026
03c6c93
fix(gdal): enforce directory listing hardening
jQuinRivero Oct 19, 2026
a1b2128
feat(gdal-runtime): pin shared wheels in consumers
jQuinRivero Oct 19, 2026
2dac576
feat(gdal-runtime): build and smoke consumer packages
jQuinRivero Oct 19, 2026
9277d6f
feat(deploy): deploy verified function packages
jQuinRivero Oct 19, 2026
e9a1c69
test(ci): validate the local TiTiler container base
jQuinRivero Oct 19, 2026
95ed7db
fix(gdal-runtime): validate manifest and imageryprep package
jQuinRivero Oct 19, 2026
ad9fce0
fix(gdal): fail closed on driver deregistration errors
jQuinRivero Oct 19, 2026
f95781f
fix(deploy): restore GDAL enforcement after failed publish
jQuinRivero Oct 19, 2026
c47a3df
fix(deploy): require attested function packages
jQuinRivero Oct 19, 2026
88c0a98
fix(titiler): configure runtime before VRT smoke
jQuinRivero Oct 19, 2026
788c988
docs(spec): update GDAL runtime validation status
jQuinRivero Oct 19, 2026
8312045
style: apply formatting to GDAL integration
jQuinRivero Oct 19, 2026
60e73d1
docs(spec): reflect TiTiler hardening integration
jQuinRivero Oct 19, 2026
db04b5b
fix(ci): filter unpublished GDAL wheels in TiTiler tests
jQuinRivero Oct 19, 2026
90f23df
fix(ci): add TiTiler app dir to test import path
jQuinRivero Oct 19, 2026
927fffa
fix(security): require TLS 1.2 in smoke test server
jQuinRivero Oct 19, 2026
16aebea
fix(gdal-runtime): patch zlib CVE-2026-85091 in the runtime build
jQuinRivero Oct 19, 2026
2258e7a
fix(ci): repair native runtime build smoke and scan steps
jQuinRivero Oct 19, 2026
d8263ea
fix(ci): ignore vendored metadata when locking Function package wheels
jQuinRivero Oct 19, 2026
8b03b5c
fix(ci): install the training image's GL runtime in the smoke test
jQuinRivero Oct 19, 2026
2668faf
fix(ci): give Function package import check its data path and runtime…
jQuinRivero Oct 19, 2026
35a07f3
fix(ci): run training smoke as root to install GL runtime
jQuinRivero Oct 19, 2026
452ff9b
fix(ci): verify the full runtime in a process that has not hardened d…
jQuinRivero Oct 19, 2026
a271046
fix(ci): harden before verifying in the Function package runtime check
jQuinRivero Oct 19, 2026
c3d23be
fix(ci): run the imageryprep package smoke as root
jQuinRivero Oct 19, 2026
550e7fd
fix(ci): detect structured name/value GDAL settings in the hardening …
jQuinRivero Oct 19, 2026
76b6f29
fix(ci): build the GDAL package on every packaged source change
jQuinRivero Oct 19, 2026
84bb019
fix(docker): allow the local Azurite origin in the dev TiTiler
jQuinRivero Oct 19, 2026
502033b
fix(docker): install uvicorn in the local TiTiler image
jQuinRivero Oct 19, 2026
02aa9d7
fix(gdal): enable GDAL exceptions after shared runtime verification
jQuinRivero Oct 19, 2026
31f7da2
test(gdal): isolate legacy hardening tests from the shared runtime
jQuinRivero Oct 19, 2026
c3be04b
docs: list the GDAL runtime components in the third-party notices
jQuinRivero Oct 21, 2026
69a7e04
docs(gdal-runtime): record zlib fix release status
jQuinRivero Oct 21, 2026
64f79cd
fix(gdal-runtime): bundle the patched zlib instead of the host libz
jQuinRivero Oct 21, 2026
147c0a0
fix(titiler): keep the VRT driver for tile reprojection
jQuinRivero Oct 26, 2026
ebb2513
fix(training): pin pandas below 3 for geopandas 1.0.1
jQuinRivero Oct 26, 2026
242dff1
docs(spec): record VRT kept for TiTiler and the pandas pin
jQuinRivero Oct 26, 2026
db4f451
fix(docker): verify the shared GDAL runtime when building worker images
jQuinRivero Oct 30, 2026
e43bf38
fix(docker): keep the runtime check clear of Dockerfile instruction k…
jQuinRivero Oct 30, 2026
4eb07d3
docs: state exactly which libraries the runtime wheel bundles
jQuinRivero Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 333 additions & 0 deletions .github/scripts/build_function_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""Build a build-once Azure Functions package against the HASTE GDAL wheels."""

from __future__ import annotations

import argparse
import hashlib
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from email.parser import BytesParser
from pathlib import Path

RUNTIME_BINDINGS = {
"gdal",
"rasterio",
"fiona",
"pyogrio",
"pyproj",
"haste-gdal-runtime",
}
FUNCTION_APP_MODULES = {
"hastefuncapi": "function_app",
"hastefuncqueues": "function_app",
"titilerfuncapi": "app",
"imageryprep-smoke": "hastegeo.workflows.prepare_imagery",
}
HASTEGEO_CONSUMERS = {"hastefuncapi", "hastefuncqueues"}


def normalized_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()


def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()


def wheel_metadata(path: Path) -> tuple[str, str]:
with zipfile.ZipFile(path) as wheel:
metadata_files = [
name
for name in wheel.namelist()
if name.count("/") == 1 and name.endswith(".dist-info/METADATA")
]
if len(metadata_files) != 1:
raise ValueError(f"{path.name}: expected one wheel METADATA file")
metadata = BytesParser().parsebytes(wheel.read(metadata_files[0]))
name = metadata.get("Name")
version = metadata.get("Version")
if not name or not version:
raise ValueError(f"{path.name}: missing distribution name or version")
return normalized_name(name), version


def runtime_wheel_requirements(wheel_dir: Path) -> list[str]:
wheels = sorted(wheel_dir.glob("*.whl"))
if len(wheels) != 6:
raise ValueError(f"expected six runtime wheels, found {len(wheels)}")
requirements: dict[str, str] = {}
for wheel in wheels:
name, version = wheel_metadata(wheel)
if name in requirements:
raise ValueError(f"duplicate runtime wheel for {name}")
requirements[name] = version
if set(requirements) != RUNTIME_BINDINGS:
raise ValueError(
"runtime wheel set mismatch: "
f"missing={sorted(RUNTIME_BINDINGS - set(requirements))}, "
f"unexpected={sorted(set(requirements) - RUNTIME_BINDINGS)}"
)
return [f"{name}=={requirements[name]}" for name in sorted(requirements)]


def _requirement_name(line: str) -> str | None:
content = line.strip()
if not content or content.startswith("#") or content.startswith("-"):
return None
match = re.match(r"([A-Za-z0-9_.-]+)(?=\s|[<=>!~;\[]|$)", content)
return normalized_name(match.group(1)) if match else None


def prepare_requirements(
source: Path,
output: Path,
wheel_dir: Path,
hastegeo_wheel: Path | None,
) -> None:
"""Replace legacy GDAL bindings and editable hastegeo with the built wheels."""
lines = source.read_text(encoding="utf-8").splitlines()
retained = []
for line in lines:
name = _requirement_name(line)
if name in RUNTIME_BINDINGS or (
name == "hastegeo" or line.strip().startswith("-e ")
):
continue
retained.append(line)
additions = runtime_wheel_requirements(wheel_dir)
if hastegeo_wheel is not None:
additions.append(f"hastegeo @ {hastegeo_wheel.resolve().as_uri()}")
output.write_text(
"\n".join(
[
*retained,
"",
"# Exact wheel versions from this build; the lock records hashes.",
*additions,
"",
]
),
encoding="utf-8",
)


def _pip_index_arguments() -> list[str]:
index_url = os.environ.get("PIP_INDEX_URL", "https://pypi.org/simple")
return ["--index-url", index_url]


def write_lock(wheelhouse: Path, path: Path) -> None:
entries: dict[str, tuple[str, str]] = {}
for wheel in sorted(wheelhouse.glob("*.whl")):
name, version = wheel_metadata(wheel)
if name in entries:
raise ValueError(f"wheelhouse has duplicate distributions: {name}")
entries[name] = (version, sha256(wheel))
if not entries:
raise ValueError("dependency resolver produced an empty wheelhouse")
path.write_text(
"".join(
f"{name}=={version} --hash=sha256:{digest}\n"
for name, (version, digest) in sorted(entries.items())
),
encoding="utf-8",
)


def _copy_function_source(source: Path, target: Path) -> None:
ignored = shutil.ignore_patterns(
"__pycache__",
"*.pyc",
".pytest_cache",
".venv",
"venv",
".python_packages",
)
shutil.copytree(source, target, ignore=ignored, dirs_exist_ok=True)


def _write_zip(root: Path, destination: Path) -> None:
with zipfile.ZipFile(
destination, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6
) as archive:
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
relative = path.relative_to(root).as_posix()
info = zipfile.ZipInfo(relative, date_time=(2020, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, path.read_bytes())


def build_package(
*,
app_name: str,
source_dir: Path,
requirements: Path,
wheel_dir: Path,
hastegeo_wheel: Path | None,
output_dir: Path,
) -> Path:
if app_name not in FUNCTION_APP_MODULES:
raise ValueError(f"unsupported Function App: {app_name}")
if (app_name in HASTEGEO_CONSUMERS) != (hastegeo_wheel is not None):
raise ValueError(
f"{app_name} requires hastegeo wheel input "
f"={app_name in HASTEGEO_CONSUMERS}"
)
if not source_dir.is_dir() or not requirements.is_file():
raise FileNotFoundError(
"Function source or requirements file is missing"
)
output_dir.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f"haste-{app_name}-") as temporary:
root = Path(temporary)
wheelhouse = root / "wheelhouse"
wheelhouse.mkdir()
target = root / ".python_packages" / "lib" / "site-packages"
target.mkdir(parents=True)
generated_requirements = root / "requirements.in"
lock_file = root / "requirements.lock"
prepare_requirements(
requirements, generated_requirements, wheel_dir, hastegeo_wheel
)
subprocess.run(
[
sys.executable,
"-m",
"pip",
"download",
"--isolated",
*_pip_index_arguments(),
"--only-binary=:all:",
"--find-links",
str(wheel_dir.resolve()),
"--dest",
str(wheelhouse),
"-r",
str(generated_requirements),
],
check=True,
)
write_lock(wheelhouse, lock_file)
subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"--isolated",
"--no-index",
"--find-links",
str(wheelhouse),
"--only-binary=:all:",
"--require-hashes",
"--target",
str(target),
"-r",
str(lock_file),
],
check=True,
)

environment = os.environ.copy()
environment["PYTHONPATH"] = str(target)
environment["HASTE_GDAL_RUNTIME_REQUIRED"] = "true"
subprocess.run(
[
sys.executable,
"-m",
"pip",
"check",
],
check=True,
env=environment,
)
application_environment = environment.copy()
application_environment["PYTHONPATH"] = os.pathsep.join(
[str(target), str(source_dir.resolve())]
)
application_environment.setdefault("DATA_PATH", str(root / "data"))
subprocess.run(
[
sys.executable,
"-c",
f"import {FUNCTION_APP_MODULES[app_name]}",
],
check=True,
cwd=source_dir,
env=application_environment,
)
subprocess.run(
[
sys.executable,
"-c",
"import haste_gdal_runtime; "
"policy = haste_gdal_runtime.Policy("
"expected_gdal='3.13.3', "
"required_bindings=('osgeo.gdal','rasterio','fiona',"
"'pyogrio','pyproj')); "
"haste_gdal_runtime.configure(policy); "
"print(haste_gdal_runtime.secure_startup(policy))",
],
check=True,
env=environment,
)

package_root = root / "package"
package_root.mkdir()
_copy_function_source(source_dir, package_root)
destination_packages = (
package_root / ".python_packages" / "lib" / "site-packages"
)
destination_packages.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(target, destination_packages)
shutil.copyfile(lock_file, package_root / "requirements.lock")
shutil.copyfile(lock_file, package_root / "requirements.txt")
shutil.copyfile(
wheel_dir / "manifest.json",
package_root / "gdal-runtime-manifest.json",
)
archive_path = output_dir / f"{app_name}.zip"
_write_zip(package_root, archive_path)
(output_dir / f"{app_name}.sha256").write_text(
f"{sha256(archive_path)} {archive_path.name}\n",
encoding="utf-8",
)
return archive_path


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--app-name", required=True)
parser.add_argument("--source-dir", required=True, type=Path)
parser.add_argument("--requirements", required=True, type=Path)
parser.add_argument("--wheel-dir", required=True, type=Path)
parser.add_argument("--hastegeo-wheel", type=Path)
parser.add_argument("--output-dir", required=True, type=Path)
args = parser.parse_args(argv)
archive = build_package(
app_name=args.app_name,
source_dir=args.source_dir,
requirements=args.requirements,
wheel_dir=args.wheel_dir,
hastegeo_wheel=args.hastegeo_wheel,
output_dir=args.output_dir,
)
print(f"Built {archive} ({sha256(archive)})")
return 0


if __name__ == "__main__":
raise SystemExit(main())
6 changes: 6 additions & 0 deletions .github/scripts/check_env_drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
"GDAL_SKIP": "read inside the task container",
"GDAL_WARP_PARAMS": "read inside the task container",
"GDAL_TRANSLATE_PARAMS": "read inside the task container",
# Read by the native GDAL/CPL libraries rather than Python code.
"CPL_VSIL_CURL_HEADER_FILE_KVP_ENABLED": "consumed by GDAL's CPL runtime",
"GDAL_DRIVER_PATH": "consumed by GDAL's driver manager",
"GDAL_DISABLE_READDIR_ON_OPEN": "consumed by GDAL's directory scanner",
"GDAL_VRT_ENABLE_PYTHON": "consumed by GDAL's VRT driver",
"GDAL_VRT_ENABLE_RAWRASTERBAND": "consumed by GDAL's VRT driver",
}


Expand Down
Loading
Loading