kaxil commented on code in PR #74556:
URL: https://github.com/apache/airflow/pull/74556#discussion_r4304527065


##########
scripts/docker/install_airflow_when_building_images.sh:
##########
@@ -35,6 +35,55 @@
 # shellcheck source=scripts/docker/common.sh
 . "$( dirname "${BASH_SOURCE[0]}" )/common.sh"
 
+function has_providers_excluded_for_python() {
+    local python_minor="${AIRFLOW_PYTHON_VERSION%.*}"
+    PYTHONPATH="$( dirname "${BASH_SOURCE[0]}" )" python3 -c \
+        'from pathlib import Path; import sys; from filter_ci_install_set 
import split_members; sys.exit(not split_members(Path.cwd(), sys.argv[1])[1])' \
+        "${python_minor}"
+}
+
+# uv sync --all-packages installs every workspace member, including providers 
excluded for this
+# Python minor and the third-party packages only they need. Resolve the 
members that are not excluded
+# for this interpreter, with the lock's pins as constraints, and install 
exactly that result.
+function install_workspace_without_excluded_providers() {
+    local python_minor="${AIRFLOW_PYTHON_VERSION%.*}"
+    local work_dir="/tmp/ci-install-set"
+    local python_bin
+    local platform_arch
+    local -a member_dirs=()
+    local -a editable_args=()
+    local member_dir
+    mkdir -p "${work_dir}"
+    python_bin="$(command -v python3)"
+    case "$(uname -m)" in
+        x86_64 | aarch64) platform_arch="$(uname -m)" ;;
+        *)
+            echo "Unsupported architecture for the filtered install: $(uname 
-m)"
+            exit 1
+            ;;
+    esac
+    echo
+    echo "${COLOR_BLUE}Installing the workspace without providers excluded for 
Python ${python_minor}.${COLOR_RESET}"
+    echo
+    set -x
+    uv export --frozen --all-packages --group ci-image --no-hashes 
--no-emit-workspace \

Review Comment:
   `uv sync --frozen` checks every download against the sha256 in `uv.lock`. 
`--no-hashes` plus `uv pip install` drops that check for the whole 3.14 
install, including the direct-URL `sphinx-airflow-theme` wheel the `ci-image` 
group pulls in through `devel-common[docs]`. Was that intended? If not, 
carrying the lock hashes for the resolved names into the requirements file and 
installing with `--require-hashes` would keep the check.



##########
scripts/docker/install_airflow_when_building_images.sh:
##########
@@ -35,6 +35,55 @@
 # shellcheck source=scripts/docker/common.sh
 . "$( dirname "${BASH_SOURCE[0]}" )/common.sh"
 
+function has_providers_excluded_for_python() {
+    local python_minor="${AIRFLOW_PYTHON_VERSION%.*}"
+    PYTHONPATH="$( dirname "${BASH_SOURCE[0]}" )" python3 -c \
+        'from pathlib import Path; import sys; from filter_ci_install_set 
import split_members; sys.exit(not split_members(Path.cwd(), sys.argv[1])[1])' \
+        "${python_minor}"
+}
+
+# uv sync --all-packages installs every workspace member, including providers 
excluded for this
+# Python minor and the third-party packages only they need. Resolve the 
members that are not excluded
+# for this interpreter, with the lock's pins as constraints, and install 
exactly that result.
+function install_workspace_without_excluded_providers() {
+    local python_minor="${AIRFLOW_PYTHON_VERSION%.*}"
+    local work_dir="/tmp/ci-install-set"
+    local python_bin
+    local platform_arch
+    local -a member_dirs=()
+    local -a editable_args=()
+    local member_dir
+    mkdir -p "${work_dir}"
+    python_bin="$(command -v python3)"

Review Comment:
   In the PROD build stage this resolves to `/usr/python/bin/python3`, not the 
venv. `VIRTUAL_ENV` is `~/.local` there, but `ENV PATH="/usr/python/bin:$PATH"` 
(Dockerfile line 2407) puts the base interpreter ahead of `~/.local/bin`. So a 
from-sources `breeze prod-image build --python 3.14` (installation method `.` 
is the default, and cassandra, tinkerpop and vespa are excluded on 3.14) points 
both `uv pip install` calls and `uv pip check` at `/usr/python` instead of the 
venv the old `uv sync --active` path installed into. Running as `airflow`, I'd 
expect that to fail on permissions.
   
   Could this prefer `${VIRTUAL_ENV:+${VIRTUAL_ENV}/bin/python}` and fall back 
to `command -v python3` only when no venv is set? The CI image has no 
`VIRTUAL_ENV`, so it would behave as it does now.



##########
scripts/tests/docker/test_install_airflow_when_building_images.py:
##########
@@ -0,0 +1,187 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[3]
+INSTALLER = ROOT / "scripts" / "docker" / 
"install_airflow_when_building_images.sh"
+HELPER = INSTALLER.with_name("filter_ci_install_set.py")
+
+
[email protected]
+def image_scripts(tmp_path: Path) -> Path:
+    scripts = tmp_path / "scripts"
+    scripts.mkdir()
+    source = 
INSTALLER.read_text(encoding="utf-8").split("\ncommon::get_colors\n", 1)[0]
+    source = source.replace('local work_dir="/tmp/ci-install-set"', f'local 
work_dir="{tmp_path}/work"')
+    (scripts / INSTALLER.name).write_text(source, encoding="utf-8")
+    (scripts / "common.sh").write_text("set -euo pipefail\n", encoding="utf-8")
+    (scripts / HELPER.name).write_text(HELPER.read_text(encoding="utf-8"), 
encoding="utf-8")
+    return scripts
+
+
+def _build_environment(bin_dir: Path) -> dict[str, str]:
+    environment = {
+        **os.environ,
+        "PATH": f"{bin_dir}:{os.environ['PATH']}",
+        "AIRFLOW_PYTHON_VERSION": "3.15.0",
+        "PACKAGING_TOOL_CMD": "uv",
+        "AIRFLOW_FALLBACK_NO_CONSTRAINTS_INSTALLATION": "false",
+        "UPGRADE_RANDOM_INDICATOR_STRING": "",
+        "COLOR_BLUE": "",
+        "COLOR_RESET": "",
+    }
+    environment.pop("AIRFLOW_SOURCES", None)
+    return environment
+
+
[email protected](
+    ("metadata", "requires_python", "filtered"),
+    [
+        ('excluded-python-versions:\n  - "3.15"\n', ">=3.11,!=3.15.*", True),
+        ("excluded-python-versions:\n  - '3.15'\n", ">=3.11,!=3.15.*", True),
+        ("excluded-python-versions: ['3.15']\n", ">=3.11,!=3.15.*", True),
+        ("", ">=3.11", False),
+    ],
+)
[email protected]("upgrade", [False, True])
+def test_source_install_selects_path_without_ci_sources(
+    image_scripts: Path, tmp_path: Path, metadata: str, requires_python: str, 
filtered: bool, upgrade: bool
+):
+    workspace = tmp_path / "workspace"
+    provider = workspace / "providers" / "example"
+    provider.mkdir(parents=True)
+    (workspace / "pyproject.toml").write_text(
+        '[project]\nname = "test-root"\n[tool.uv.workspace]\nmembers = [".", 
"providers/example"]\n',
+        encoding="utf-8",
+    )
+    (provider / "pyproject.toml").write_text(
+        f'[project]\nname = "test-provider"\nrequires-python = 
"{requires_python}"\n', encoding="utf-8"
+    )
+    (provider / "provider.yaml").write_text(metadata, encoding="utf-8")
+    bin_dir = tmp_path / "bin"
+    bin_dir.mkdir()
+    (bin_dir / "uv").write_text('#!/usr/bin/env bash\nprintf "uv %s\\n" 
"$*"\n', encoding="utf-8")
+    (bin_dir / "uv").chmod(0o755)
+
+    result = subprocess.run(
+        [
+            "bash",
+            "-c",
+            'source "$1"; function 
install_workspace_without_excluded_providers() { printf "filtered\\n"; }; 
install_from_sources',
+            "bash",
+            str(image_scripts / INSTALLER.name),
+        ],
+        cwd=workspace,
+        env={**_build_environment(bin_dir), "UPGRADE_RANDOM_INDICATOR_STRING": 
"random" if upgrade else ""},
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+
+    assert result.returncode == 0, result.stderr
+    if filtered and upgrade:
+        assert "uv lock --resolution highest" in result.stdout
+        assert result.stdout.endswith("filtered\n")
+        assert "uv sync" not in result.stdout
+    elif filtered:
+        assert result.stdout == "filtered\n"
+    elif upgrade:
+        assert "uv sync --all-packages --resolution highest --group ci-image" 
in result.stdout
+        assert "uv lock" not in result.stdout
+    else:
+        assert "uv sync --all-packages --frozen --group ci-image" in 
result.stdout
+
+
[email protected]("helper_fails", [False, True])
+def test_filtered_install_consumes_set_and_propagates_helper_failure(
+    image_scripts: Path, tmp_path: Path, helper_fails: bool
+):
+    bin_dir = tmp_path / "bin"
+    bin_dir.mkdir()
+    log = tmp_path / "commands"
+    (bin_dir / "uv").write_text(
+        '#!/usr/bin/env bash\nprintf "%s\\0" uv "$@" >> "$COMMAND_LOG"\nprintf 
"\\0" >> "$COMMAND_LOG"\n',
+        encoding="utf-8",
+    )
+    (bin_dir / "python3").write_text(
+        "#!/usr/bin/env bash\n"
+        'printf "%s\\0" python3 "$@" >> "$COMMAND_LOG"\nprintf "\\0" >> 
"$COMMAND_LOG"\n'
+        'if [[ "$HELPER_FAILS" == "true" ]]; then exit 23; fi\n'
+        "while [[ $# -gt 0 ]]; do\n"
+        '  case "$1" in\n'
+        '    --output) printf "shared==1.0\\n" > "$2"; shift ;;\n'
+        '    --editables) printf "%s\\n" "$KEPT_MEMBER" "$KEPT_WITH_SPACE" > 
"$2"; shift ;;\n'
+        "  esac\n"
+        "  shift\n"
+        "done\n",
+        encoding="utf-8",
+    )
+    for command in ("uv", "python3"):
+        (bin_dir / command).chmod(0o755)
+    kept = str(tmp_path / "kept")
+    kept_with_space = str(tmp_path / "kept with space")
+    result = subprocess.run(
+        [
+            "bash",
+            "-c",
+            'source "$1"; install_workspace_without_excluded_providers',
+            "bash",
+            str(image_scripts / INSTALLER.name),
+        ],
+        cwd=tmp_path,
+        env={
+            **_build_environment(bin_dir),
+            "COMMAND_LOG": str(log),
+            "HELPER_FAILS": str(helper_fails).lower(),
+            "KEPT_MEMBER": kept,
+            "KEPT_WITH_SPACE": kept_with_space,
+        },
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+    commands = [entry.decode().split("\0") for entry in 
log.read_bytes().split(b"\0\0") if entry]

Review Comment:
   On macOS both cases fail here with `FileNotFoundError`: `uname -m` returns 
`arm64`, the installer exits before `uv export`, and the log is never written. 
Stubbing `uname` in `bin_dir` like `uv` and `python3`, or asserting 
`returncode` before reading the log, would fix it and give a clearer failure.



##########
scripts/docker/install_airflow_when_building_images.sh:
##########
@@ -62,6 +120,10 @@ function install_from_sources() {
             --no-python-downloads --no-managed-python
     else
         set +x
+        if has_providers_excluded_for_python; then

Review Comment:
   This returns before the `AIRFLOW_FALLBACK_NO_CONSTRAINTS_INSTALLATION` 
handling below, and `Dockerfile.ci` defaults that flag to `true`. When a PR 
adds a dependency without updating `uv.lock`, 3.11 to 3.13 fall back to an 
unfrozen `uv sync`, but 3.14 stops in the helper with `... is not in the lock`. 
Should the filtered path honour the flag (re-lock, then filter, like the 
upgrade branch above), or is the stricter behaviour intended here?



##########
scripts/tests/docker/test_install_airflow_when_building_images.py:
##########
@@ -0,0 +1,187 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[3]
+INSTALLER = ROOT / "scripts" / "docker" / 
"install_airflow_when_building_images.sh"
+HELPER = INSTALLER.with_name("filter_ci_install_set.py")
+
+
[email protected]
+def image_scripts(tmp_path: Path) -> Path:
+    scripts = tmp_path / "scripts"
+    scripts.mkdir()
+    source = 
INSTALLER.read_text(encoding="utf-8").split("\ncommon::get_colors\n", 1)[0]
+    source = source.replace('local work_dir="/tmp/ci-install-set"', f'local 
work_dir="{tmp_path}/work"')
+    (scripts / INSTALLER.name).write_text(source, encoding="utf-8")
+    (scripts / "common.sh").write_text("set -euo pipefail\n", encoding="utf-8")
+    (scripts / HELPER.name).write_text(HELPER.read_text(encoding="utf-8"), 
encoding="utf-8")
+    return scripts
+
+
+def _build_environment(bin_dir: Path) -> dict[str, str]:
+    environment = {
+        **os.environ,
+        "PATH": f"{bin_dir}:{os.environ['PATH']}",
+        "AIRFLOW_PYTHON_VERSION": "3.15.0",
+        "PACKAGING_TOOL_CMD": "uv",
+        "AIRFLOW_FALLBACK_NO_CONSTRAINTS_INSTALLATION": "false",
+        "UPGRADE_RANDOM_INDICATOR_STRING": "",
+        "COLOR_BLUE": "",
+        "COLOR_RESET": "",
+    }
+    environment.pop("AIRFLOW_SOURCES", None)
+    return environment
+
+
[email protected](
+    ("metadata", "requires_python", "filtered"),
+    [
+        ('excluded-python-versions:\n  - "3.15"\n', ">=3.11,!=3.15.*", True),
+        ("excluded-python-versions:\n  - '3.15'\n", ">=3.11,!=3.15.*", True),
+        ("excluded-python-versions: ['3.15']\n", ">=3.11,!=3.15.*", True),
+        ("", ">=3.11", False),
+    ],
+)
[email protected]("upgrade", [False, True])
+def test_source_install_selects_path_without_ci_sources(
+    image_scripts: Path, tmp_path: Path, metadata: str, requires_python: str, 
filtered: bool, upgrade: bool
+):
+    workspace = tmp_path / "workspace"
+    provider = workspace / "providers" / "example"
+    provider.mkdir(parents=True)
+    (workspace / "pyproject.toml").write_text(
+        '[project]\nname = "test-root"\n[tool.uv.workspace]\nmembers = [".", 
"providers/example"]\n',
+        encoding="utf-8",
+    )
+    (provider / "pyproject.toml").write_text(
+        f'[project]\nname = "test-provider"\nrequires-python = 
"{requires_python}"\n', encoding="utf-8"
+    )
+    (provider / "provider.yaml").write_text(metadata, encoding="utf-8")
+    bin_dir = tmp_path / "bin"
+    bin_dir.mkdir()
+    (bin_dir / "uv").write_text('#!/usr/bin/env bash\nprintf "uv %s\\n" 
"$*"\n', encoding="utf-8")
+    (bin_dir / "uv").chmod(0o755)
+
+    result = subprocess.run(
+        [
+            "bash",
+            "-c",
+            'source "$1"; function 
install_workspace_without_excluded_providers() { printf "filtered\\n"; }; 
install_from_sources',
+            "bash",
+            str(image_scripts / INSTALLER.name),
+        ],
+        cwd=workspace,
+        env={**_build_environment(bin_dir), "UPGRADE_RANDOM_INDICATOR_STRING": 
"random" if upgrade else ""},
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+
+    assert result.returncode == 0, result.stderr
+    if filtered and upgrade:
+        assert "uv lock --resolution highest" in result.stdout
+        assert result.stdout.endswith("filtered\n")
+        assert "uv sync" not in result.stdout
+    elif filtered:
+        assert result.stdout == "filtered\n"
+    elif upgrade:
+        assert "uv sync --all-packages --resolution highest --group ci-image" 
in result.stdout
+        assert "uv lock" not in result.stdout
+    else:
+        assert "uv sync --all-packages --frozen --group ci-image" in 
result.stdout
+
+
[email protected]("helper_fails", [False, True])
+def test_filtered_install_consumes_set_and_propagates_helper_failure(
+    image_scripts: Path, tmp_path: Path, helper_fails: bool
+):
+    bin_dir = tmp_path / "bin"
+    bin_dir.mkdir()
+    log = tmp_path / "commands"
+    (bin_dir / "uv").write_text(
+        '#!/usr/bin/env bash\nprintf "%s\\0" uv "$@" >> "$COMMAND_LOG"\nprintf 
"\\0" >> "$COMMAND_LOG"\n',
+        encoding="utf-8",
+    )
+    (bin_dir / "python3").write_text(
+        "#!/usr/bin/env bash\n"
+        'printf "%s\\0" python3 "$@" >> "$COMMAND_LOG"\nprintf "\\0" >> 
"$COMMAND_LOG"\n'
+        'if [[ "$HELPER_FAILS" == "true" ]]; then exit 23; fi\n'
+        "while [[ $# -gt 0 ]]; do\n"
+        '  case "$1" in\n'
+        '    --output) printf "shared==1.0\\n" > "$2"; shift ;;\n'
+        '    --editables) printf "%s\\n" "$KEPT_MEMBER" "$KEPT_WITH_SPACE" > 
"$2"; shift ;;\n'
+        "  esac\n"
+        "  shift\n"
+        "done\n",
+        encoding="utf-8",
+    )
+    for command in ("uv", "python3"):
+        (bin_dir / command).chmod(0o755)
+    kept = str(tmp_path / "kept")
+    kept_with_space = str(tmp_path / "kept with space")
+    result = subprocess.run(
+        [
+            "bash",
+            "-c",
+            'source "$1"; install_workspace_without_excluded_providers',
+            "bash",
+            str(image_scripts / INSTALLER.name),
+        ],
+        cwd=tmp_path,
+        env={
+            **_build_environment(bin_dir),
+            "COMMAND_LOG": str(log),
+            "HELPER_FAILS": str(helper_fails).lower(),
+            "KEPT_MEMBER": kept,
+            "KEPT_WITH_SPACE": kept_with_space,
+        },
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+    commands = [entry.decode().split("\0") for entry in 
log.read_bytes().split(b"\0\0") if entry]
+
+    assert commands[0][:3] == ["uv", "export", "--frozen"]

Review Comment:
   Only the first three tokens of the export are checked. Dropping `--group 
ci-image` or `--no-emit-workspace` from the export, or `--no-binary lxml 
--no-binary xmlsec` from the installs, would still pass. A membership assert 
for each would cover the flags this path depends on.



##########
scripts/docker/filter_ci_install_set.py:
##########
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Compute the CI image install set for one Python minor without excluded 
workspace members.
+
+``uv sync --all-packages`` installs every workspace member, including the dev 
groups of providers
+excluded for a Python minor (``Requires-Python`` carries ``!=<minor>``). Their 
third-party
+dependencies would still be installed, and some do not build for the new 
interpreter.
+
+The helper resolves the requirements of the members that are not excluded, not 
the lock's export
+graph. Each kept member becomes a local ``file://`` requirement, and the 
requirements of each kept
+member's ``dev`` group (the root's ``ci-image`` group for the root) are added 
verbatim, with
+excluded members removed. ``uv pip compile`` resolves that input for the 
target interpreter and
+platform, so markers apply per edge. The lock's pins are passed as 
constraints, so every version
+matches the lock. Output is the third-party pins and the editable member 
directories.
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import subprocess
+import sys
+import tomllib
+from glob import glob
+from pathlib import Path
+from typing import Any
+from urllib.parse import unquote, urlparse
+
+MEMBER_DEFAULT_GROUP = "dev"
+ROOT_CI_GROUP = "ci-image"
+MINOR_VERSION = re.compile(r"\d+\.\d+")
+REQUIREMENT_NAME = re.compile(r"\s*([A-Za-z0-9][A-Za-z0-9._-]*)")
+RESOLVED_MEMBER = re.compile(r"^([^\s@]+)\s*@\s*(file://\S+)$")
+PINNED_NAME = 
re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?\s*(?:==|@)")
+PINNED_VERSION = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==([^\s;]+)")
+GLOB_CHARACTERS = frozenset("*?[")
+PYTHON_PLATFORMS = {"x86_64": "x86_64-manylinux_2_28", "aarch64": 
"aarch64-manylinux_2_28"}
+
+
+def normalize_name(name: str) -> str:
+    return re.sub(r"[-_.]+", "-", name).lower()
+
+
+def requirement_name(entry: str) -> str:
+    match = REQUIREMENT_NAME.match(entry)
+    if not match:
+        raise ValueError(f"Cannot read a requirement name from {entry!r}")
+    return normalize_name(match.group(1))
+
+
+def is_python_minor_excluded(requires_python: str, minor: str) -> bool:
+    # The lookahead keeps longer versions such as !=3.150 or !=3.15.1 from 
matching a whole-minor exclusion.
+    return re.search(rf"!=\s*{re.escape(minor)}(\.\*)?(?![\d.])", 
requires_python) is not None
+
+
+def load_manifest(directory: Path) -> dict[str, Any]:
+    with (directory / "pyproject.toml").open("rb") as manifest:
+        return tomllib.load(manifest)
+
+
+def has_glob_characters(pattern: str) -> bool:
+    return any(character in GLOB_CHARACTERS for character in pattern)
+
+
+def find_workspace_members(root: Path, patterns: list[str]) -> list[Path]:
+    members: list[Path] = []
+    for pattern in patterns:
+        matches = sorted(glob(str(root / pattern)))
+        if not matches and not has_glob_characters(pattern):
+            raise ValueError(f"Workspace member {pattern!r} does not exist 
under {root}")
+        for match in matches:
+            candidate = Path(match)
+            if (candidate / "pyproject.toml").is_file():
+                members.append(candidate)
+            elif not has_glob_characters(pattern):
+                raise ValueError(f"Workspace member {pattern!r} has no 
pyproject.toml")
+    return members
+
+
+def expand_group(groups: dict[str, list[Any]], group: str, seen: set[str] | 
None = None) -> list[str]:
+    """Return the raw requirement strings of a dependency group, following 
``include-group`` entries."""
+    if seen is None:
+        seen = set()
+    if group in seen:
+        return []
+    seen.add(group)
+    entries: list[str] = []
+    for entry in groups.get(group, []):
+        if isinstance(entry, dict):
+            entries.extend(expand_group(groups, entry["include-group"], seen))
+        else:
+            entries.append(entry)
+    return entries
+
+
+def split_members(root: Path, minor: str) -> tuple[dict[str, Path], set[str]]:
+    """Return the members kept for installation (name -> directory) and the 
excluded member names."""
+    patterns = load_manifest(root)["tool"]["uv"]["workspace"]["members"]
+    kept: dict[str, Path] = {}
+    excluded: set[str] = set()
+    for member in find_workspace_members(root, patterns):
+        project = load_manifest(member).get("project", {})
+        name = normalize_name(project.get("name", member.name))
+        if is_python_minor_excluded(project.get("requires-python", ""), minor):
+            excluded.add(name)
+        else:
+            kept[name] = member
+    return kept, excluded
+
+
+def build_requirement_input(root: Path, kept: dict[str, Path], excluded: 
set[str]) -> list[str]:
+    """Return the requirements to resolve: kept members as local references 
and their dev requirements."""
+    lines = [f"{name} @ {path.resolve().as_uri()}" for name, path in 
sorted(kept.items())]
+    for path in kept.values():
+        groups = load_manifest(path).get("dependency-groups", {})
+        group = ROOT_CI_GROUP if path == root else MEMBER_DEFAULT_GROUP
+        for entry in expand_group(groups, group):
+            if requirement_name(entry) not in excluded:
+                lines.append(entry)
+    return lines
+
+
+def resolve_requirements(
+    requirements: list[str],
+    constraints: Path,
+    minor: str,
+    platform: str,
+    workdir: Path,
+    root: Path,
+    find_links: list[Path],
+) -> list[str]:
+    """Resolve the requirements for the target interpreter with the lock's 
pins as constraints.
+
+    The command runs from the workspace root so that the root's 
``exclude-newer`` applies to the resolver.
+    """
+    # uv runs from the workspace root, so relative paths would resolve there 
rather than here.
+    workdir = workdir.resolve()
+    constraints = constraints.resolve()
+    requirements_in = workdir / "requirements.in"
+    resolved = workdir / "resolved.txt"
+    requirements_in.write_text("\n".join(requirements) + "\n", 
encoding="utf-8")
+    find_links_args = [argument for directory in find_links for argument in 
("--find-links", str(directory))]
+    subprocess.run(
+        [
+            "uv",
+            "pip",
+            "compile",
+            str(requirements_in),
+            "--constraints",
+            str(constraints),
+            "--python-version",
+            minor,
+            "--python-platform",
+            platform,
+            *find_links_args,
+            "--no-header",
+            "--no-annotate",
+            "--output-file",
+            str(resolved),
+        ],
+        check=True,
+        cwd=root,
+    )
+    return resolved.read_text(encoding="utf-8").splitlines()
+
+
+def split_resolved(lines: list[str]) -> tuple[list[str], list[Path]]:
+    """Separate third-party pins from the resolved local member directories 
(``-e file://`` or ``name @ file://``)."""
+    pins: list[str] = []
+    members: list[Path] = []
+    for line in lines:
+        stripped = line.strip()
+        if not stripped or stripped.startswith("#"):
+            continue
+        if stripped.startswith("-e "):
+            members.append(member_path(stripped[len("-e ") :]))
+            continue
+        match = RESOLVED_MEMBER.match(stripped)
+        if match:
+            members.append(member_path(match.group(2)))
+        elif "file://" in stripped:
+            raise ValueError(f"Cannot read a local member reference from the 
resolved output: {stripped!r}")
+        else:
+            pins.append(line)
+    return pins, members
+
+
+def member_path(file_uri: str) -> Path:
+    return Path(unquote(urlparse(file_uri.strip()).path))
+
+
+def verify_resolved_pins(pins: list[str], lock_pins: str, excluded: set[str]) 
-> None:
+    """Fail when the resolver returns an excluded member, a package the lock 
does not contain, or a locked version differing."""
+    locked_names: set[str] = set()
+    locked_versions: dict[str, set[str]] = {}
+    for line in lock_pins.splitlines():
+        stripped = line.strip()
+        name_match = PINNED_NAME.match(stripped)
+        if name_match:
+            locked_names.add(normalize_name(name_match.group(1)))
+        version_match = PINNED_VERSION.match(stripped)
+        if version_match:
+            locked_versions.setdefault(normalize_name(version_match.group(1)), 
set()).add(
+                version_match.group(2)
+            )
+    problems: list[str] = []
+    for line in pins:
+        stripped = line.strip()
+        name_match = PINNED_NAME.match(stripped)
+        if not name_match:
+            problems.append(f"unreadable resolved requirement {stripped!r}")
+            continue
+        name = normalize_name(name_match.group(1))
+        if name in excluded:
+            problems.append(f"excluded member {name} was resolved from an 
index")
+        elif name not in locked_names:
+            problems.append(f"{name} is not in the lock")
+        else:
+            version_match = PINNED_VERSION.match(stripped)
+            if version_match and version_match.group(2) not in 
locked_versions.get(name, set()):
+                problems.append(
+                    f"{name}=={version_match.group(2)} differs from the locked 
versions "
+                    f"{sorted(locked_versions.get(name, set()))}"
+                )
+    if problems:
+        raise ValueError("Resolved pins disagree with the lock: " + "; 
".join(problems))
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser(
+        description=__doc__, 
formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    parser.add_argument(
+        "--root", type=Path, required=True, help="Workspace root holding the 
root pyproject.toml"
+    )
+    parser.add_argument(
+        "--constraints", type=Path, required=True, help="Lock pins (uv export) 
used as constraints"
+    )
+    parser.add_argument("--python-minor", required=True, help="Target Python 
major.minor, e.g. 3.15")
+    parser.add_argument(
+        "--platform", choices=sorted(PYTHON_PLATFORMS), required=True, 
help="Target CPU architecture"
+    )
+    parser.add_argument("--workdir", type=Path, required=True, help="Scratch 
directory for resolver inputs")
+    parser.add_argument("--output", type=Path, required=True, help="Where to 
write the third-party pins")
+    parser.add_argument(
+        "--editables", type=Path, required=True, help="Where to write the kept 
member directories"
+    )
+    parser.add_argument(
+        "--find-links",
+        type=Path,
+        action="append",
+        default=[],
+        help="Directory of local wheels the resolver may use, e.g. temporary 
wheels for a missing release",
+    )
+    args = parser.parse_args(argv)
+    if not MINOR_VERSION.fullmatch(args.python_minor):
+        parser.error(f"--python-minor must be major.minor, got 
{args.python_minor!r}")
+    root = args.root.resolve()
+    args.workdir.mkdir(parents=True, exist_ok=True)
+
+    kept, excluded = split_members(root, args.python_minor)
+    requirements = build_requirement_input(root, kept, excluded)
+    resolved = resolve_requirements(
+        requirements,
+        args.constraints,
+        args.python_minor,
+        PYTHON_PLATFORMS[args.platform],
+        args.workdir,
+        root,
+        [directory.resolve() for directory in args.find_links],
+    )
+    pins, member_dirs = split_resolved(resolved)
+    verify_resolved_pins(pins, args.constraints.read_text(encoding="utf-8"), 
excluded)

Review Comment:
   These guards aren't exercised through `main()`. Both `main()` tests hand 
back a resolved set that already agrees with the lock, so removing this call, 
or the "not kept" / "missing" checks below, keeps the suite green. The "not 
kept" and "missing" checks have no test at all. Parametrizing `fake_resolve` in 
`test_main_writes_pins_and_editables_for_kept_members` to return a version 
outside the lock, an excluded member's `-e` line, and a set missing a kept 
member would cover all three.



##########
scripts/tests/docker/test_install_airflow_when_building_images.py:
##########
@@ -0,0 +1,187 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[3]
+INSTALLER = ROOT / "scripts" / "docker" / 
"install_airflow_when_building_images.sh"
+HELPER = INSTALLER.with_name("filter_ci_install_set.py")
+
+
[email protected]
+def image_scripts(tmp_path: Path) -> Path:
+    scripts = tmp_path / "scripts"
+    scripts.mkdir()
+    source = 
INSTALLER.read_text(encoding="utf-8").split("\ncommon::get_colors\n", 1)[0]
+    source = source.replace('local work_dir="/tmp/ci-install-set"', f'local 
work_dir="{tmp_path}/work"')
+    (scripts / INSTALLER.name).write_text(source, encoding="utf-8")
+    (scripts / "common.sh").write_text("set -euo pipefail\n", encoding="utf-8")
+    (scripts / HELPER.name).write_text(HELPER.read_text(encoding="utf-8"), 
encoding="utf-8")
+    return scripts
+
+
+def _build_environment(bin_dir: Path) -> dict[str, str]:
+    environment = {
+        **os.environ,
+        "PATH": f"{bin_dir}:{os.environ['PATH']}",
+        "AIRFLOW_PYTHON_VERSION": "3.15.0",
+        "PACKAGING_TOOL_CMD": "uv",
+        "AIRFLOW_FALLBACK_NO_CONSTRAINTS_INSTALLATION": "false",
+        "UPGRADE_RANDOM_INDICATOR_STRING": "",
+        "COLOR_BLUE": "",
+        "COLOR_RESET": "",
+    }
+    environment.pop("AIRFLOW_SOURCES", None)
+    return environment
+
+
[email protected](
+    ("metadata", "requires_python", "filtered"),

Review Comment:
   Nothing reads `provider.yaml` in this path (the check only looks at 
`requires-python`), so the three `excluded-python-versions` spellings run the 
same case. Can the `metadata` parameter go?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to