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}