This is an automated email from the ASF dual-hosted git repository.

jscheffl pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-1-test by this push:
     new 3f8e625e059 [v3-1-test] Add Windows filesystem detection in Breeze 
startup (#61562) (#61603)
3f8e625e059 is described below

commit 3f8e625e0591230f2ac1811825e1bd86724b9dbd
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Feb 7 16:46:09 2026 +0100

    [v3-1-test] Add Windows filesystem detection in Breeze startup (#61562) 
(#61603)
    
    * Add Windows filesystem detection in Breeze startup
    
    Detect when Airflow sources are on a Windows (NTFS) filesystem
    mounted via WSL2 and fail early with a clear error message before
    starting Docker containers.
    
    The check runs on the host side during Breeze environment checks,
    using stat to identify the Plan 9 (9p) filesystem type that WSL2
    uses for Windows drive mounts. This detection cannot be done from
    inside the container because Docker Desktop abstracts the 9p layer.
    
    Closes: #58932
    
    * Fix ruff lint errors: rename shadowed 'platform' loop var and add 
check=False to subprocess.run
    (cherry picked from commit ca90ac34ed4dda846f3618565d9c1817b58f27f0)
    
    Co-authored-by: AndrĂ© Ahlert <[email protected]>
---
 .../airflow_breeze/utils/docker_command_utils.py   | 58 ++++++++++++++++++++--
 1 file changed, 53 insertions(+), 5 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py 
b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
index dc0bf13d0cf..3de4a5cbb77 100644
--- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
@@ -21,7 +21,9 @@ from __future__ import annotations
 import copy
 import json
 import os
+import platform
 import re
+import subprocess
 import sys
 from functools import lru_cache
 from subprocess import DEVNULL, CompletedProcess
@@ -469,11 +471,11 @@ def construct_docker_push_command(
 
 def build_cache(image_params: CommonBuildParams, output: Output | None) -> 
RunCommandResult:
     build_command_result: RunCommandResult = CompletedProcess(args=[], 
returncode=0)
-    for platform in image_params.platforms:
+    for build_platform in image_params.platforms:
         platform_image_params = copy.deepcopy(image_params)
         # override the platform in the copied params to only be single 
platform per run
         # as a workaround to https://github.com/docker/buildx/issues/1044
-        platform_image_params.platform = platform
+        platform_image_params.platform = build_platform
         cmd = 
prepare_docker_build_cache_command(image_params=platform_image_params)
         build_command_result = run_command(
             cmd,
@@ -518,6 +520,51 @@ def prepare_broker_url(params, env_variables):
         env_variables["AIRFLOW__CELERY__BROKER_URL"] = 
url_map[params.celery_broker]
 
 
+def check_windows_filesystem_mount(quiet: bool = False):
+    """
+    Checks if Airflow sources are on a Windows (NTFS) filesystem mounted via 
WSL2.
+
+    Airflow only works with POSIX-compliant filesystems. When sources are 
checked out on Windows
+    and accessed via /mnt/c (or similar) in WSL2, Docker bind mounts inherit 
the NTFS limitations:
+    broken permissions, missing executable bits, symlink issues, etc.
+
+    This check uses ``stat -f -c %T`` on the host to detect the filesystem 
type. On WSL2,
+    Windows drives mounted via Plan 9 (9p) protocol report as ``v9fs``, while 
native Linux
+    filesystems report as ``ext2/ext3``. This detection only works on the host 
side - inside
+    Docker containers, the 9p layer is abstracted away by Docker Desktop.
+    """
+    if platform.system().lower() != "linux":
+        return
+    try:
+        with open("/proc/version") as f:
+            if "microsoft" not in f.read().lower():
+                return
+    except FileNotFoundError:
+        return
+    result = subprocess.run(
+        ["stat", "-f", "-c", "%T", str(AIRFLOW_ROOT_PATH)],
+        capture_output=True,
+        text=True,
+        timeout=5,
+        check=False,
+    )
+    fs_type = result.stdout.strip()
+    if fs_type in ("v9fs", "9p"):
+        get_console().print(
+            f"[error]Airflow sources are on a Windows filesystem 
({AIRFLOW_ROOT_PATH})![/]\n\n"
+            f"Airflow requires a POSIX-compliant filesystem. Running Breeze 
with sources on\n"
+            f"Windows (NTFS) mounted via WSL2 will cause permission errors, 
broken executable\n"
+            f"bits, and other issues with Docker bind mounts.\n\n"
+            f"Clone the repository inside WSL2 on a Linux filesystem 
instead:\n\n"
+            f"    git clone https://github.com/apache/airflow.git ~/airflow\n"
+            f"    cd ~/airflow\n"
+            f"    breeze\n"
+        )
+        sys.exit(1)
+    if get_verbose() and not quiet:
+        get_console().print(f"[success]Filesystem check passed (type: 
{fs_type})[/]")
+
+
 def check_executable_entrypoint_permissions(quiet: bool = False):
     """
     Checks if the user has executable permissions on the entrypoints in 
checked-out airflow repository..
@@ -548,6 +595,7 @@ def perform_environment_checks(quiet: bool = False):
     else:
         check_docker_version(quiet)
         check_docker_compose_version(quiet)
+        check_windows_filesystem_mount(quiet)
         check_executable_entrypoint_permissions(quiet)
     if not quiet:
         get_console().print(f"[success]Host python version is 
{sys.version}[/]")
@@ -566,7 +614,7 @@ def warm_up_docker_builder(image_params_list: 
list[CommonBuildParams]):
     for image_params in image_params_list:
         platforms.add(image_params.platform)
     get_console().print(f"[info]Warming up the builder for platforms: 
{platforms}")
-    for platform in platforms:
+    for build_platform in platforms:
         docker_context = get_and_use_docker_context(image_params.builder)
         if docker_context == "default":
             return
@@ -574,12 +622,12 @@ def warm_up_docker_builder(image_params_list: 
list[CommonBuildParams]):
         get_console().print(f"[info]Warming up the {docker_context} builder 
for syntax: {docker_syntax}")
         warm_up_image_param = copy.deepcopy(image_params_list[0])
         warm_up_image_param.push = False
-        warm_up_image_param.platform = platform
+        warm_up_image_param.platform = build_platform
         build_command = 
prepare_base_build_command(image_params=warm_up_image_param)
         warm_up_command = []
         warm_up_command.extend(["docker"])
         warm_up_command.extend(build_command)
-        warm_up_command.extend(["--platform", platform, "-"])
+        warm_up_command.extend(["--platform", build_platform, "-"])
         warm_up_command_result = run_command(
             warm_up_command,
             input=f"""{docker_syntax}

Reply via email to